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.
- {simplicio_loop-3.16.0/simplicio_loop.egg-info → simplicio_loop-3.20.0}/PKG-INFO +1 -1
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/README.md +20 -3
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/pyproject.toml +1 -1
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/loop_stop.py +264 -10
- simplicio_loop-3.20.0/simplicio_loop/_bundle/scripts/__pycache__/cross_agent_wiki.cpython-314.pyc +0 -0
- simplicio_loop-3.20.0/simplicio_loop/_bundle/scripts/__pycache__/hierarchical_planner.cpython-314.pyc +0 -0
- simplicio_loop-3.20.0/simplicio_loop/_bundle/scripts/cross_agent_wiki.py +606 -0
- simplicio_loop-3.20.0/simplicio_loop/_bundle/scripts/hierarchical_planner.py +388 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-loop/SKILL.md +317 -17
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-review/SKILL.md +5 -5
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/SKILL.md +17 -4
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/orchestration.md +38 -13
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/quality-safety-delivery.md +8 -6
- simplicio_loop-3.20.0/simplicio_loop/_bundle/tests/__pycache__/_selfrun.cpython-314.pyc +0 -0
- simplicio_loop-3.20.0/simplicio_loop/_bundle/tests/__pycache__/test_loop_e2e.cpython-314-pytest-9.0.3.pyc +0 -0
- simplicio_loop-3.20.0/simplicio_loop/_bundle/tests/__pycache__/test_loop_e2e.cpython-314.pyc +0 -0
- simplicio_loop-3.20.0/simplicio_loop/_bundle/tests/_selfrun.py +46 -0
- simplicio_loop-3.20.0/simplicio_loop/_bundle/tests/test_cross_agent_wiki.py +106 -0
- simplicio_loop-3.20.0/simplicio_loop/_bundle/tests/test_loop_e2e.py +304 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0/simplicio_loop.egg-info}/PKG-INFO +1 -1
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop.egg-info/SOURCES.txt +12 -0
- simplicio_loop-3.20.0/tests/test_cross_agent_wiki.py +106 -0
- simplicio_loop-3.20.0/tests/test_hierarchical_planner.py +63 -0
- simplicio_loop-3.20.0/tests/test_loop_e2e.py +304 -0
- simplicio_loop-3.16.0/tests/test_loop_e2e.py +0 -146
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/LICENSE +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/MANIFEST.in +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/PYPI.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/setup.cfg +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/__init__.py +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/README.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/action_gate.py +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/hooks.claude.json +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/hooks.json +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/learn_stop.py +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/loop_capture.py +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/orient_clamp.py +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/orient_rewrite.py +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/simplicio_dashboard.py +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/simplicio_watch.py +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-compress/SKILL.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-learn/SKILL.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-orient/SKILL.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/agentsview-adapter.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/azure-devops-adapter.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/extension-points.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/lmcache-adapter.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/standing-loop-247.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-capture.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-economy.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/understand-anything-adapter.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/video-evidence.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/web-evidence.md +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/cli.py +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop.egg-info/dependency_links.txt +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop.egg-info/entry_points.txt +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop.egg-info/requires.txt +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop.egg-info/top_level.txt +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/tests/test_action_gate.py +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/tests/test_flow_audit.py +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/tests/test_impact_audit.py +0 -0
- {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/tests/test_worker_selftests.py +0 -0
- {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.
|
|
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`
|
|
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
|
|
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.
|
|
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
|
|
318
|
-
# in the task anchor
|
|
319
|
-
#
|
|
320
|
-
|
|
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
|
|
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]" % (
|
|
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()
|