simplicio-loop 3.15.0__tar.gz → 3.16.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 (50) hide show
  1. {simplicio_loop-3.15.0/simplicio_loop.egg-info → simplicio_loop-3.16.0}/PKG-INFO +1 -1
  2. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/pyproject.toml +1 -1
  3. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/loop_stop.py +129 -21
  4. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-loop/SKILL.md +1 -1
  5. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0/simplicio_loop.egg-info}/PKG-INFO +1 -1
  6. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/LICENSE +0 -0
  7. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/MANIFEST.in +0 -0
  8. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/PYPI.md +0 -0
  9. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/README.md +0 -0
  10. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/setup.cfg +0 -0
  11. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/__init__.py +0 -0
  12. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/README.md +0 -0
  13. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/action_gate.py +0 -0
  14. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/hooks.claude.json +0 -0
  15. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/hooks.json +0 -0
  16. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/learn_stop.py +0 -0
  17. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/loop_capture.py +0 -0
  18. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/orient_clamp.py +0 -0
  19. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/orient_rewrite.py +0 -0
  20. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/simplicio_dashboard.py +0 -0
  21. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/simplicio_watch.py +0 -0
  22. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-compress/SKILL.md +0 -0
  23. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-learn/SKILL.md +0 -0
  24. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-orient/SKILL.md +0 -0
  25. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-review/SKILL.md +0 -0
  26. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/SKILL.md +0 -0
  27. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/agentsview-adapter.md +0 -0
  28. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/azure-devops-adapter.md +0 -0
  29. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/extension-points.md +0 -0
  30. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/lmcache-adapter.md +0 -0
  31. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/orchestration.md +0 -0
  32. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/quality-safety-delivery.md +0 -0
  33. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/standing-loop-247.md +0 -0
  34. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-capture.md +0 -0
  35. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-economy.md +0 -0
  36. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/understand-anything-adapter.md +0 -0
  37. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/video-evidence.md +0 -0
  38. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/web-evidence.md +0 -0
  39. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop/cli.py +0 -0
  40. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop.egg-info/SOURCES.txt +0 -0
  41. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop.egg-info/dependency_links.txt +0 -0
  42. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop.egg-info/entry_points.txt +0 -0
  43. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop.egg-info/requires.txt +0 -0
  44. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/simplicio_loop.egg-info/top_level.txt +0 -0
  45. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/tests/test_action_gate.py +0 -0
  46. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/tests/test_flow_audit.py +0 -0
  47. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/tests/test_impact_audit.py +0 -0
  48. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/tests/test_loop_e2e.py +0 -0
  49. {simplicio_loop-3.15.0 → simplicio_loop-3.16.0}/tests/test_worker_selftests.py +0 -0
  50. {simplicio_loop-3.15.0 → simplicio_loop-3.16.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.15.0
3
+ Version: 3.16.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simplicio-loop"
7
- version = "3.15.0"
7
+ version = "3.16.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"
@@ -11,17 +11,26 @@ hook must never trap the agent in an endless loop. The real guards are the
11
11
 
12
12
  State (single source of truth): .orchestrator/loop/scratchpad.md (+ sibling `done` flag)
13
13
  Reads stdin JSON from the host (Claude: {transcript_path,...}; Cursor: {text,...}).
14
+
15
+ Cross-agent handoff: an INCOMPLETE stop (budget halted, iteration cap, manual STOP
16
+ signal) writes `.orchestrator/loop/HANDOFF.md` before clearing the scratchpad, so a
17
+ different agent/runtime picking up this repo cold — because the first one ran out of
18
+ budget — can resume without re-deriving the goal, the verified acceptance criteria, or
19
+ the dead-end attempts. A successful (promise-fulfilled) stop needs no handoff.
14
20
  """
15
21
  import json
16
22
  import os
17
23
  import re
18
24
  import sys
25
+ import time
19
26
 
20
27
  LOOP_DIR = os.path.join(".orchestrator", "loop")
21
28
  SCRATCHPAD = os.path.join(LOOP_DIR, "scratchpad.md")
22
29
  DONE_FLAG = os.path.join(LOOP_DIR, "done")
23
30
  LAST_RESP = os.path.join(LOOP_DIR, "last_response.txt")
24
31
  ANCHOR = os.path.join(LOOP_DIR, "anchor.json")
32
+ JOURNAL = os.path.join(LOOP_DIR, "journal.jsonl")
33
+ HANDOFF = os.path.join(LOOP_DIR, "HANDOFF.md")
25
34
  STOP_SIGNAL = os.path.join(".orchestrator", "STOP")
26
35
  BUDGET = os.path.join(".orchestrator", "loop-budget.json")
27
36
  GATE_LOCK = os.path.join(LOOP_DIR, "gate.lock")
@@ -122,31 +131,118 @@ def gate_running():
122
131
  try:
123
132
  if not os.path.exists(GATE_LOCK):
124
133
  return False
125
- import time
126
134
  return (time.time() - os.path.getmtime(GATE_LOCK)) < GATE_TTL_SEC
127
135
  except Exception:
128
136
  return False
129
137
 
130
138
 
139
+ def read_anchor():
140
+ """Return the parsed task anchor dict, or None if absent/corrupt. Fail-open."""
141
+ try:
142
+ with open(ANCHOR, encoding="utf-8") as f:
143
+ return json.load(f)
144
+ except Exception:
145
+ return None
146
+
147
+
131
148
  def anchor_pending():
132
149
  """Return the unverified acceptance-criteria ids from the task anchor, or [].
133
150
 
134
151
  The mechanical anti-drift gate: a `<promise>` must not end the loop while the frozen task anchor
135
- still has criteria that are not `done`. Read the anchor JSON DIRECTLY (no dependency on
136
- `scripts/task_anchor.py`, which the lean marketplace plugin does not ship — the hook must stay
137
- self-contained). FAIL-OPEN: a missing / unreadable / empty anchor, or one with no criteria,
138
- returns [] so the gate never blocks — a buggy anchor must never trap the loop, and the rejection
139
- it does cause is still bounded by `max_iterations` + the budget. Only a cleanly-parsed anchor
140
- with ≥1 criterion that is not `done` reports pending.
152
+ still has criteria that are not `done`. FAIL-OPEN: a missing / unreadable / empty anchor, or one
153
+ with no criteria, returns [] so the gate never blocks — a buggy anchor must never trap the loop.
141
154
  """
155
+ data = read_anchor()
156
+ if not data:
157
+ return []
158
+ crit = data.get("criteria") or []
159
+ return [c.get("id") for c in crit
160
+ if isinstance(c, dict) and c.get("status") != "done"]
161
+
162
+
163
+ def tail_journal(n=8):
164
+ """Last N attempt records from the journal, oldest first. [] on any read error."""
142
165
  try:
143
- with open(ANCHOR, encoding="utf-8") as f:
144
- data = json.load(f)
145
- crit = data.get("criteria") or []
146
- return [c.get("id") for c in crit
147
- if isinstance(c, dict) and c.get("status") != "done"]
166
+ with open(JOURNAL, encoding="utf-8") as f:
167
+ lines = [ln for ln in f if ln.strip()]
168
+ out = []
169
+ for ln in lines[-n:]:
170
+ try:
171
+ out.append(json.loads(ln))
172
+ except Exception:
173
+ continue
174
+ return out
148
175
  except Exception:
149
- return [] # fail-open: anchor unreadable ≠ trap
176
+ return []
177
+
178
+
179
+ def write_handoff(reason, meta=None, body=None):
180
+ """Write the cross-agent continuation artifact before an INCOMPLETE stop.
181
+
182
+ Aggregates the frozen task anchor (goal + acceptance criteria + evidence), the last journal
183
+ attempts (what was already tried, to avoid re-running a dead end), and the live scratchpad
184
+ iteration/promise — everything a fresh agent needs to resume cold, without this conversation.
185
+ Fail-open: any error here must never block the stop itself.
186
+ """
187
+ try:
188
+ anchor = read_anchor() or {}
189
+ criteria = anchor.get("criteria") or []
190
+ attempts = tail_journal()
191
+ lines = [
192
+ "# simplicio-loop handoff",
193
+ "",
194
+ "Stop reason: %s" % reason,
195
+ "Stopped at: %s" % time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
196
+ ]
197
+ if meta:
198
+ lines += [
199
+ "Iteration: %s / %s" % (meta.get("iteration", "?"), meta.get("max_iterations", "?")),
200
+ "Completion promise: %s" % (meta.get("completion_promise") or "(none set)"),
201
+ ]
202
+ if anchor.get("goal"):
203
+ lines += ["", "## Frozen goal", "", anchor["goal"]]
204
+ elif body:
205
+ lines += ["", "## Goal (from scratchpad, no anchor set)", "", body]
206
+ if criteria:
207
+ lines += ["", "## Acceptance criteria"]
208
+ for c in criteria:
209
+ if not isinstance(c, dict):
210
+ continue
211
+ mark = "x" if c.get("status") == "done" else " "
212
+ ev = (" — %s" % c["evidence"]) if c.get("evidence") else ""
213
+ lines.append(
214
+ "- [%s] %s (%s)%s"
215
+ % (mark, c.get("text", c.get("id", "?")), c.get("status", "pending"), ev)
216
+ )
217
+ if attempts:
218
+ lines += ["", "## Last attempts (`scripts/loop_journal.py resume` for the full read)"]
219
+ for a in attempts:
220
+ lines.append(
221
+ "- iter %s: %s -> %s (fp %s)%s"
222
+ % (
223
+ a.get("iteration", "?"),
224
+ a.get("action", "?"),
225
+ a.get("gate", "?"),
226
+ (a.get("fingerprint") or "")[:12],
227
+ (" — %s" % a["note"]) if a.get("note") else "",
228
+ )
229
+ )
230
+ lines += [
231
+ "",
232
+ "## Resume",
233
+ "",
234
+ "1. `python3 scripts/task_anchor.py status` (or `gate --exit-code`) — verified vs open.",
235
+ "2. `python3 scripts/loop_journal.py resume` — dead-end actions to avoid.",
236
+ "3. `git log --oneline -10` / `git diff` — what already landed.",
237
+ "4. Re-arm the loop once the stop cause (budget/cap/manual) is resolved.",
238
+ "",
239
+ ]
240
+ tmp = HANDOFF + ".tmp"
241
+ with open(tmp, "w", encoding="utf-8") as f:
242
+ f.write("\n".join(lines))
243
+ os.replace(tmp, HANDOFF)
244
+ except Exception:
245
+ pass # fail-open: a broken handoff write must never block the stop
150
246
 
151
247
 
152
248
  def budget_halted():
@@ -177,8 +273,18 @@ def emit_refeed(followup):
177
273
 
178
274
  def main():
179
275
  try:
180
- # Explicit STOP signal beats everything.
276
+ meta, body = None, None
277
+ if os.path.exists(SCRATCHPAD):
278
+ try:
279
+ with open(SCRATCHPAD, encoding="utf-8") as f:
280
+ meta, body = parse_frontmatter(f.read())
281
+ except OSError:
282
+ meta, body = None, None
283
+
284
+ # Explicit STOP signal beats everything — but still hand off if there was live state.
181
285
  if os.path.exists(STOP_SIGNAL):
286
+ if meta is not None:
287
+ write_handoff("manual STOP signal", meta, body)
182
288
  cleanup_and_stop()
183
289
  # Waiting on a background gate (workflow / CI / long task)? Let the turn end WITHOUT
184
290
  # consuming an iteration or re-feeding — we are blocked on that gate, not idle. The gate's
@@ -188,9 +294,6 @@ def main():
188
294
  # (1) No active loop.
189
295
  if not os.path.exists(SCRATCHPAD):
190
296
  allow_stop()
191
- with open(SCRATCHPAD, encoding="utf-8") as f:
192
- content = f.read()
193
- meta, body = parse_frontmatter(content)
194
297
  # (2) Corrupt state.
195
298
  if meta is None:
196
299
  cleanup_and_stop()
@@ -215,21 +318,26 @@ def main():
215
318
  # in the task anchor — the mechanical anti-drift gate. Pending ACs ⇒ ignore the
216
319
  # promise and keep looping (still bounded by max_iter), never a false "done".
217
320
  if ((not evidence_required) or has_evidence) and not anchor_pending():
218
- cleanup_and_stop() # (3) promise fulfilled → stop
321
+ cleanup_and_stop() # (3) promise fulfilled → stop, no handoff needed
219
322
  # promise without evidence, or anchor still has open ACs → ignore, keep looping
220
323
  # (3') Cursor capture may have raised the flag.
221
324
  if os.path.exists(DONE_FLAG):
222
325
  cleanup_and_stop()
223
- # (4) Iteration cap.
326
+ # (4) Iteration cap — incomplete stop, hand off.
224
327
  if max_iter > 0 and iteration >= max_iter:
328
+ write_handoff("max_iterations cap reached", meta, body)
225
329
  cleanup_and_stop()
226
- # (5) Budget halted.
330
+ # (5) Budget halted — incomplete stop, hand off. This is the exact "ran out of tokens/$"
331
+ # case: a different agent must be able to pick this up cold.
227
332
  if budget_halted():
333
+ write_handoff("budget halted", meta, body)
228
334
  cleanup_and_stop()
229
335
  # (6) Continue: bump iteration in place, re-feed the goal body.
230
336
  nxt = iteration + 1
337
+ with open(SCRATCHPAD, encoding="utf-8") as f:
338
+ raw = f.read()
231
339
  new_content = re.sub(
232
- r"^iteration:\s*\d+", "iteration: %d" % nxt, content, count=1, flags=re.M
340
+ r"^iteration:\s*\d+", "iteration: %d" % nxt, raw, count=1, flags=re.M
233
341
  )
234
342
  try:
235
343
  tmp = SCRATCHPAD + ".tmp"
@@ -312,7 +312,7 @@ Where the host runtime supports lifecycle hooks, bind the two cross-platform hoo
312
312
  | Hook | Fires | Job |
313
313
  |---|---|---|
314
314
  | `afterAgentResponse` → `loop_capture.py` | after every turn | extract `<promise>…</promise>`; if it exactly equals `completion_promise` AND in-turn evidence exists → `touch .orchestrator/loop/done`. Fire-and-forget, `exit 0`. Never stops the loop itself. |
315
- | `stop` → `loop_stop.py` | when the turn ends | guard clauses, each ends the loop cleanly (remove state, `exit 0`): (1) no scratchpad → stop; (2) corrupt frontmatter → stop; (3) `done` flag present → stop (promise fulfilled); (4) `iteration >= max_iterations > 0` → stop (cap); (5) budget halted → stop; else increment `iteration` in place and emit `{"followup_message": "<header>\n\n<goal body>"}` to re-feed. |
315
+ | `stop` → `loop_stop.py` | when the turn ends | guard clauses, each ends the loop cleanly (remove state, `exit 0`): (1) no scratchpad → stop; (2) corrupt frontmatter → stop; (3) `done` flag present → stop (promise fulfilled); (4) `iteration >= max_iterations > 0` → write `HANDOFF.md`, then stop (cap); (5) budget halted → write `HANDOFF.md` (frozen goal + AC status + last attempts) for a different agent to resume, then stop; else increment `iteration` in place and emit `{"followup_message": "<header>\n\n<goal body>"}` to re-feed. |
316
316
 
317
317
  Detection (`capture`) and termination (`stop`) are split on purpose — neither parses the
318
318
  other's inline state. Iteration carries forward through git history + the working tree, not
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simplicio-loop
3
- Version: 3.15.0
3
+ Version: 3.16.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
File without changes
File without changes