simplicio-loop 3.14.1__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.14.1/simplicio_loop.egg-info → simplicio_loop-3.16.0}/PKG-INFO +1 -1
  2. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/README.md +15 -3
  3. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/pyproject.toml +1 -1
  4. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/__init__.py +2 -2
  5. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/loop_stop.py +129 -21
  6. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-loop/SKILL.md +26 -5
  7. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-review/SKILL.md +10 -0
  8. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/SKILL.md +33 -0
  9. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/extension-points.md +2 -2
  10. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/orchestration.md +15 -0
  11. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/quality-safety-delivery.md +63 -0
  12. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0/simplicio_loop.egg-info}/PKG-INFO +1 -1
  13. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop.egg-info/SOURCES.txt +2 -0
  14. simplicio_loop-3.16.0/tests/test_flow_audit.py +72 -0
  15. simplicio_loop-3.16.0/tests/test_impact_audit.py +125 -0
  16. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/tests/test_worker_selftests.py +17 -3
  17. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/LICENSE +0 -0
  18. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/MANIFEST.in +0 -0
  19. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/PYPI.md +0 -0
  20. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/setup.cfg +0 -0
  21. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/README.md +0 -0
  22. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/action_gate.py +0 -0
  23. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/hooks.claude.json +0 -0
  24. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/hooks.json +0 -0
  25. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/learn_stop.py +0 -0
  26. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/loop_capture.py +0 -0
  27. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/orient_clamp.py +0 -0
  28. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/orient_rewrite.py +0 -0
  29. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/simplicio_dashboard.py +0 -0
  30. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/hooks/simplicio_watch.py +0 -0
  31. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-compress/SKILL.md +0 -0
  32. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-learn/SKILL.md +0 -0
  33. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-orient/SKILL.md +0 -0
  34. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/agentsview-adapter.md +0 -0
  35. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/azure-devops-adapter.md +0 -0
  36. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/lmcache-adapter.md +0 -0
  37. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/standing-loop-247.md +0 -0
  38. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-capture.md +0 -0
  39. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-economy.md +0 -0
  40. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/understand-anything-adapter.md +0 -0
  41. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/video-evidence.md +0 -0
  42. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/web-evidence.md +0 -0
  43. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop/cli.py +0 -0
  44. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop.egg-info/dependency_links.txt +0 -0
  45. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop.egg-info/entry_points.txt +0 -0
  46. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop.egg-info/requires.txt +0 -0
  47. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/simplicio_loop.egg-info/top_level.txt +0 -0
  48. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/tests/test_action_gate.py +0 -0
  49. {simplicio_loop-3.14.1 → simplicio_loop-3.16.0}/tests/test_loop_e2e.py +0 -0
  50. {simplicio_loop-3.14.1 → 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.14.1
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
@@ -84,7 +84,7 @@ protocol on 11 runtimes**, and it does all of this with **aggressive, honest tok
84
84
  ## 📘 Official capability record
85
85
 
86
86
  The complete, official roster of what `simplicio-tasks` ships — every capability below is **real,
87
- runnable, and tested** (`python3 scripts/check.py`: claims-audit 4/4 + 28 tests). Each links to its
87
+ runnable, and tested** (`python3 scripts/check.py`: claims-audit 5/5 + local test suite). Each links to its
88
88
  deep section and its worker.
89
89
 
90
90
  | Capability | What it does | Proof / worker | Details |
@@ -92,6 +92,8 @@ deep section and its worker.
92
92
  | 🎬 **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
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) |
94
94
  | 🧭 **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
+ | 🧩 **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
+ | 🕸️ **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) |
95
97
  | 🔒 **Fail-closed safety gate** (`action_gate`) | A `PreToolUse`/git-pre-push hook that **mechanically blocks** force-push, history rewrite, mass-delete, destructive DDL, infra teardown, and secret-laden commits/pushes — Step 5 made executable, not prose | `hooks/action_gate.py` · `selftest` 15/15 | [§ Safety](#-safety-non-negotiable) |
96
98
  | 🔬 **Local verification** | A test suite (worker selftests + an **e2e of the loop driver** proving evidence-gated exit) + a **claims-audit** (referenced scripts exist · counts consistent · `_bundle ≡ source`) — all local, **no paid CI** | `scripts/check.py` · `scripts/claims_audit.py` · `tests/` | [§ Tests & local checks](#-tests--local-checks-no-paid-ci) |
97
99
  | ✅ **Honest savings** | The savings line is now **evidence-gated, not mandatory** — a number is shown only with a measured receipt (clamp/signatures/cache/`deterministic_edit`/ledger); never fabricated | token-economy contract | [§ Token economy](#-token-economy) |
@@ -227,8 +229,8 @@ flowchart TD
227
229
  POOL --> QG
228
230
  subgraph QG["7 · Quality gates"]
229
231
  direction LR
230
- Q1["AC gate = real DoD"]
231
- Q2["WORKS not just compiles · web_verify (Playwright) · video_evidence (Playwright recording · hyperframes on request)"]
232
+ Q1["AC gate + impact_audit = real DoD"]
233
+ Q2["WORKS not just compiles · web_verify · video_evidence · flow_audit"]
232
234
  Q3["adversarial review · thermos rubrics"]
233
235
  end
234
236
  QG --> SG
@@ -535,6 +537,16 @@ python3 scripts/check.py # the whole gate (audit + tests)
535
537
  - **Claims audit** (`scripts/claims_audit.py`, fail-closed) — every `scripts/*.py` the docs
536
538
  reference exists · the extension-point count agrees across all files · each cited worker command
537
539
  actually runs · the shipped `simplicio_loop/_bundle/` skills are **byte-identical** to source.
540
+ - **Impact audit** (`scripts/impact_audit.py`) — for any code task, proves the declared task
541
+ surface covers the local blast radius: dependencies, reverse dependents, and related tests.
542
+ ```bash
543
+ python3 scripts/impact_audit.py audit . --file path/to/seed.py --cover path/to/seed.py --fail-on high
544
+ ```
545
+ - **Flow audit** (`scripts/flow_audit.py`) — for mixed front/back/service repos, produces the
546
+ `endpoint_compare` evidence map and fails on objective integration gaps:
547
+ ```bash
548
+ python3 scripts/flow_audit.py audit . --fail-on high
549
+ ```
538
550
  - **Wire it as a git pre-push hook** to keep `main` honest for free:
539
551
  ```bash
540
552
  printf '#!/bin/sh\npython3 scripts/check.py\n' > .git/hooks/pre-push && chmod +x .git/hooks/pre-push
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simplicio-loop"
7
- version = "3.14.1"
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"
@@ -13,6 +13,6 @@ try:
13
13
  try:
14
14
  __version__ = _v("simplicio-loop")
15
15
  except PackageNotFoundError:
16
- __version__ = "3.14.0"
16
+ __version__ = "3.15.0"
17
17
  except Exception: # pragma: no cover
18
- __version__ = "3.14.0"
18
+ __version__ = "3.15.0"
@@ -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"
@@ -173,14 +173,29 @@ detector below. It is the difference between a loop that converges and one that
173
173
  last recorded turn's commit. **And re-read the task anchor** — `python3 scripts/task_anchor.py
174
174
  check --goal "<the goal worked this turn>" --exit-code` — so the turn stays on the SAME frozen
175
175
  acceptance criteria and cannot drift: a `DRIFT` verdict (exit 11) means the goal moved; STOP and
176
- re-anchor explicitly (`--force`), never wander silently. The journal is the loop's memory for
177
- ATTEMPTS; the anchor is its memory for SCOPE. Act only on what is still genuinely open; never redo
178
- done work or act on a stale picture (idempotency).
176
+ re-anchor explicitly (`--force`), never wander silently. Before deciding the next code change,
177
+ refresh the local impact map for the planned seed files with
178
+ `python3 scripts/impact_audit.py audit <root> --file <seed> --cover <known-reviewed-file> --json
179
+ > .orchestrator/impact-audit.json` so the turn sees callers, neighboring dependencies, and
180
+ related tests before it edits. For shared/public contracts or signature changes, tighten that gate
181
+ to `--fail-on medium`. For mixed front/back/service workspaces or any cross-surface user flow,
182
+ also refresh the flow map with
183
+ `python3 scripts/flow_audit.py audit <root> --fail-on high --json > .orchestrator/flow-audit.json`
184
+ so triage sees UI actions, frontend calls, backend endpoints, and service calls before deciding
185
+ the next move. The journal is the loop's memory for ATTEMPTS; the anchor is its memory for SCOPE;
186
+ the impact audit is its memory for BLAST RADIUS; the flow audit is its memory for INTEGRATION.
187
+ Act only on what is still genuinely open; never redo done work or act on a stale picture
188
+ (idempotency).
179
189
  3. **Work the goal** each turn as if fresh, against that triaged state. The model DECIDES the
180
190
  AC-scoped change; the **`simplicio-dev-cli` operator APPLIES and verifies it**
181
191
  (`simplicio-dev-cli task "<change>" --target <file>`) — do not hand-edit inside the loop. End EVERY
182
192
  iteration with a short, concrete verification — the operator's passing test run, or one gate /
183
- command / `file:line` receipt. **Then RECORD the attempt** in the journal:
193
+ command / `file:line` receipt. If the actual edit surface expands, rerun `impact_audit.py` with
194
+ the new seeds/cover and treat uncovered reverse dependents as failed verification; use
195
+ `--fail-on medium` for shared/public contracts or signature changes. If the change crosses
196
+ UI/API/service boundaries, rerun
197
+ `flow_audit.py` after the edit and treat high gaps as failed verification; use `--fail-on medium`
198
+ when the AC promises backend integration for that UI flow. **Then RECORD the attempt** in the journal:
184
199
  `loop_journal.py record --iteration N --action "<what you changed>" --hypothesis "<why>"
185
200
  --gate pass|fail --gate-output <test.log>` — on a failure the gate output is fingerprinted so the
186
201
  SAME failure is recognised next turn. Keep iterations small and verifiable: a turn that only
@@ -262,6 +277,12 @@ only if, in the SAME turn, there is concrete evidence the work is truly done:
262
277
 
263
278
  - the run-verification gate passed ("works, not just compiles" — `simplicio-tasks` Step 4b) —
264
279
  the `simplicio-dev-cli` operator's passing test+verify pass (its contract step 5/6) satisfies this, or
280
+ - the flow coverage gate passed for a mixed front/back/service change —
281
+ `python3 scripts/flow_audit.py audit <root> --fail-on high` (or `--fail-on medium` for ACs that
282
+ promise backend integration) found no unhandled UI/API/service gaps, or
283
+ - the scope/impact gate passed for the changed shared files —
284
+ `python3 scripts/impact_audit.py audit <root> --file <seed> ...` found no uncovered reverse
285
+ dependents (and, for shared/public contracts, no uncovered local deps/tests under `--fail-on medium`), or
265
286
  - the named acceptance criteria are each checked with a `file:line` or command-output receipt —
266
287
  mechanically enforced by the task anchor: `python3 scripts/task_anchor.py gate --exit-code` must
267
288
  return READY (every anchored AC `done` with a receipt; exit 12 = still pending) before the promise
@@ -291,7 +312,7 @@ Where the host runtime supports lifecycle hooks, bind the two cross-platform hoo
291
312
  | Hook | Fires | Job |
292
313
  |---|---|---|
293
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. |
294
- | `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. |
295
316
 
296
317
  Detection (`capture`) and termination (`stop`) are split on purpose — neither parses the
297
318
  other's inline state. Iteration carries forward through git history + the working tree, not
@@ -60,6 +60,16 @@ down, no proportional token blow-up). Each gets the SAME context bundle and a DI
60
60
  ledger entry with a screenshot + trace path AND 0 console errors (see the orchestrator's
61
61
  `references/web-evidence.md`, Playwright). Missing or failing → `fix-required`. Evidence is the
62
62
  artifact PATH, never pasted DOM/pixels.
63
+ - **Cross-surface change → require flow evidence.** If the diff touches frontend + backend/service
64
+ files, or an AC promises a UI→API/service flow, REQUIRE `scripts/flow_audit.py audit <root>
65
+ --fail-on high` evidence (`--fail-on medium` when backend integration is part of the AC). Missing
66
+ audit, frontend call without backend endpoint, stubbed endpoint, or unclassified UI/API/service
67
+ loose end → `fix-required`.
68
+ - **Shared-contract change → require impact evidence.** If the diff changes exported/shared
69
+ functions, controllers, DTOs/schemas, shared types, or a widely imported module, REQUIRE
70
+ `scripts/impact_audit.py audit <root> --file <seed> --cover <reviewed-file> ...` evidence.
71
+ Any uncovered reverse dependency is `fix-required`; use `--fail-on medium` when the contract
72
+ itself changed so uncovered neighboring deps/tests also block.
63
73
 
64
74
  Each reviewer's task: **"Refute this change. Find any AC not met, any fake return, any break.
65
75
  Default to 'not done' if uncertain. Cite every finding as `file:line` with a one-line why."**
@@ -173,6 +173,27 @@ wander off the task (the "desvio de tarefas" fix, Step 4 drift guard). Detail:
173
173
 
174
174
  > **Understand Anything (optional).** If `.understand-anything/knowledge-graph.json` exists, use Understand Anything as the primary orientation — the graph already holds the complete code structure, relationships, and guided tours. Query it via semantic search instead of signatures-only reads.
175
175
 
176
+ > **Scope reflection / impact coverage (mandatory for any coded change).** Before editing, declare
177
+ > the seed files you expect to touch and the files you already know must be reviewed/adjusted, then
178
+ > run `python3 scripts/impact_audit.py audit <root> --file <seed> --cover <planned-file> --json >
179
+ > .orchestrator/impact-audit.json`. This maps local dependencies, reverse dependents, and related
180
+ > tests so the task cannot silently ignore callers, adjacent modules, or tests that prove the same
181
+ > flow. Any `high` gap (`uncovered_reverse_dependency`) blocks planning until that caller/dependent
182
+ > file is added to the review surface or explicitly ruled out by evidence. For shared/public
183
+ > contracts, signature changes, DTO/schema changes, or refactors in widely imported modules, use
184
+ > `--fail-on medium` so uncovered local dependencies and related tests also block the task plan.
185
+
186
+ > **Full-stack flow coverage (mandatory for mixed front/back/service workspaces).** When a repo
187
+ > root contains frontend + backend + services together — or the item touches a user flow that crosses
188
+ > those surfaces — run the flow audit BEFORE planning: `python3 scripts/flow_audit.py audit <root>
189
+ > --fail-on high --json > .orchestrator/flow-audit.json`. This maps UI actions, frontend HTTP
190
+ > calls, backend endpoints, and backend service calls. Any `high` gap (frontend call with no backend
191
+ > endpoint, or stubbed/incomplete backend endpoint) is a BLOCKER and must become an AC/fix before
192
+ > `done`. `medium` gaps (button/action with no observed backend call, endpoint with no observed
193
+ > caller, backend local-looking service call with no local endpoint) must be classified explicitly:
194
+ > local-only/internal/external by design, or promoted to an AC. For flows whose AC promises backend
195
+ > integration, rerun with `--fail-on medium`; do not leave loose ends unclassified.
196
+
176
197
  > **Video-creation work-items (`video_evidence`).** A work-item — or the skill argument itself
177
198
  > (e.g. `/simplicio-tasks make an explainer video of the login screen`) — may ASK for a demo video.
178
199
  > Classify it cheaply in the terminal: `python3 scripts/video_evidence.py detect --goal "<text>"`.
@@ -220,12 +241,24 @@ Never mark done without green gates + evidence; a failure is NOT a blocker — i
220
241
  is mechanical: `python3 scripts/task_anchor.py gate --exit-code` (exit 12 = criteria still
221
242
  pending) MUST pass before you declare done or open the PR — "done" requires every anchored AC
222
243
  verified with a receipt.
244
+ - **4a' Scope/impact gate:** if the actual change surface expanded, rerun
245
+ `python3 scripts/impact_audit.py audit <root> --file <seed> ... --cover <reviewed-file> ...`
246
+ and refresh `.orchestrator/impact-audit.json`. The task is not done while a changed shared file
247
+ still has uncovered reverse dependents, or while a contract/signature change leaves callers,
248
+ neighboring dependencies, or related tests outside the declared review surface. Use
249
+ `--fail-on medium` for shared/public contracts.
223
250
  - **4b WORKS, not just compiles:** RUN it (`--help` + happy path / affected tests). Front-end
224
251
  change → `web_verify` (screenshot + trace, `references/web-evidence.md`). For moving proof of a UI
225
252
  change, `video_evidence verify --url <url>` records the **real session with Playwright** (default
226
253
  engine) → a video attached to the PR. Only when the item ITSELF asks for a personalized explainer
227
254
  ("make a video of screen X") use `--engine hyperframes` (deterministic captioned slideshow).
228
255
  Contract: `references/video-evidence.md`. Compiles-but-never-run = PARTIAL.
256
+ - **4b' Flow coverage gate (mixed front/back/service workspaces):** rerun
257
+ `python3 scripts/flow_audit.py audit <root> --fail-on high` after the change (or
258
+ `--fail-on medium` when the AC promises backend integration). The task is not done while a
259
+ frontend call lacks a backend endpoint, a backend endpoint is stubbed/incomplete, or any UI/API/
260
+ service gap remains unclassified. Attach `.orchestrator/flow-audit.json` or the human summary as
261
+ evidence alongside tests/screenshots.
229
262
  - **4c Adversarial verify (MEDIUM+):** 2–3 independent verifiers prompted to REFUTE + check each
230
263
  AC; majority-refute → back to fix. Delegate to `simplicio-review` when loaded. Full: `references/quality-safety-delivery.md`.
231
264
 
@@ -32,7 +32,7 @@ fallback with standard tools. The skill depends on the ABSTRACTION, never on a s
32
32
  | `status` | Live observability dashboard | `gh` queries |
33
33
  | `security` | Supply-chain / secret scan | `rg` for secrets |
34
34
  | `intake` | Ingest work from sprint/board link | `gh issue list` |
35
- | `dependency_graph` | Inter-item ordering as a resumable DAG (B after A; independents fan out); re-run skips done nodes | LLM topo-sorts by depends-on/blocked-by, runs ready first, journals done node-ids to resume |
35
+ | `dependency_graph` | Inter-item ordering as a resumable DAG (B after A; independents fan out); re-run skips done nodes. At code level, it also means "what else breaks if I touch this?" — runnable form: `scripts/impact_audit.py audit <root> --file <seed> --cover <planned-file> --json > .orchestrator/impact-audit.json` maps local dependencies, reverse dependents, and related tests for the planned task surface. `high` gaps block missing callers/dependents; `--fail-on medium` blocks uncovered local deps/tests for shared contracts. | LLM topo-sorts by depends-on/blocked-by, runs ready first, journals done node-ids to resume; for code impact, it uses `rg`/imports/git grep to enumerate local dependencies, reverse callers, and related tests, then widens the plan before editing |
36
36
  | `durable_workflow` | Per-item pipeline (intake→plan→edit→validate→deliver) as a resumable phase state-machine; retry skips done phases | LLM drives phases, journals which phase each item reached, resumes from last completed |
37
37
  | `work_queue` | Durable priority queue that runs+auto-retries+requeues-stuck, with a write-serialization lock for shared checkouts | LLM keeps queue in JSONL/SQLite, pops by priority, re-enqueues on fail, lockfile+TTL guards shared-tree writes |
38
38
  | `resource_governor` | Dynamic mid-loop throttle: decide when to back off + machine-tier ceilings before scaling a wave | LLM re-probes CPU/RAM/load each tick, reduces fleet / sleeps longer under load, degrades tiers |
@@ -49,7 +49,7 @@ fallback with standard tools. The skill depends on the ABSTRACTION, never on a s
49
49
  | `toolchain_detect` | Detect which build/lint/typecheck/test toolchains the repo actually has so validate/diagnostics route right | LLM inspects manifests/lockfiles/config + probes PATH to pick the correct toolchain per stack |
50
50
  | `checkpoint_restore` | Snapshot run/repo state before a risky batch; restore to known-good if validation/delivery fails | LLM tags a commit / stashes / copies the journal before destructive ops, restores on failure |
51
51
  | `notify` | Push progress/blocker/digest to a human channel + receive inbound approvals (async approval I/O) | LLM writes digest/approval-request to a file or session; no-reply = block the destructive op (headless rule) |
52
- | `endpoint_compare` | Compare web/API/agent surfaces to detect drift; gaps become follow-up items (full-stack coverage) | LLM lists routes on each side (grep handlers / read OpenAPI) and diffs by hand to flag mismatches |
52
+ | `endpoint_compare` | Compare web/API/service surfaces to detect drift; gaps become follow-up items or blockers. Runnable form: `scripts/flow_audit.py audit <root> --fail-on high --json > .orchestrator/flow-audit.json` maps UI actions, frontend HTTP calls, backend endpoints, and backend service calls. It blocks on frontend calls without backend endpoints and stubbed backend endpoints; `--fail-on medium` blocks unclassified UI/API/service loose ends for flows whose AC promises integration. | LLM lists UI actions, frontend calls, backend routes/controllers, OpenAPI entries, and service calls with `rg`, then diffs by hand to flag missing endpoints, stub handlers, orphan callers, and unclassified loose ends |
53
53
  | `web_verify` | Drive a real browser (navigate/click/console) to prove a UI/web change works end-to-end; capture screenshot+trace as evidence | Playwright via `playwright-mcp` or headless `npx playwright` / `pytest-playwright`; evidence = artifact path, not pixels (see web-evidence.md) |
54
54
  | `video_evidence` | Produce a demo video of a screen/feature as evidence — two engines | **DEFAULT = Playwright** native session recording (`video_evidence verify --url …` records the real browser drive to `.webm`→`.mp4`) for the normal moving-proof flow; **hyperframes** (`npx hyperframes render` — heygen-com/hyperframes) only for an EXPLICIT custom explainer request ("make a video of screen X"), assembling the `web_verify` screenshots into a deterministic captioned MP4. Evidence = video path, not bytes (see video-evidence.md); BLOCK if the toolchain is absent |
55
55
  | `web_research` | Fetch current external knowledge (docs/CVE/version/SDK error), gated behind local-memory-miss, with provenance | LLM uses built-in web search/fetch only after local miss; records source URL as provenance |
@@ -45,6 +45,21 @@ API surface — a 600-line file → ~40 lines; full-body read only when editing
45
45
 
46
46
  **2b-3 Build the plan BEFORE coding:** files to change, files to read first, AC checklist, risks/
47
47
  unknowns, complexity (trivial|small|medium|large|critical). Coding starts only after the plan.
48
+ Make the plan mechanically accountable with the impact audit:
49
+
50
+ ```bash
51
+ python3 scripts/impact_audit.py audit <root> \
52
+ --file <seed-you-expect-to-touch> \
53
+ --cover <files-already-in-plan> \
54
+ --fail-on high \
55
+ --json > .orchestrator/impact-audit.json
56
+ ```
57
+
58
+ Treat a `high` issue as a planning failure: a caller/dependent file sits outside the declared task
59
+ surface, so the plan is incomplete. For shared/public contracts, signature changes, DTO/schema
60
+ changes, or refactors inside widely imported modules, tighten the gate to `--fail-on medium` so
61
+ uncovered local dependencies and related tests also block the plan. The point is to know the blast
62
+ radius before editing, not after the regression.
48
63
 
49
64
  ## Step 3 — Route: fast-path vs heavy-path
50
65
  - **Fast-path** (queue small AND every item complexity ≤ 3): inline, solo, minimal receipt,
@@ -34,6 +34,41 @@ The DoD gate is then mechanical: `task_anchor.py gate --exit-code` (exit 12 = cr
34
34
  pending) MUST pass before "done" or PR-open. This is the loop's durable working memory for SCOPE,
35
35
  the sibling of `loop_journal`'s working memory for ATTEMPTS.
36
36
 
37
+ ### 4a' — Scope/impact gate for dependency-aware tasks
38
+ Before editing, and again if the changed surface expands, make the task's blast radius explicit:
39
+
40
+ ```bash
41
+ python3 scripts/impact_audit.py audit <root> \
42
+ --file <seed-you-touch> \
43
+ --cover <reviewed-or-adjusted-files> \
44
+ --fail-on high \
45
+ --json > .orchestrator/impact-audit.json
46
+ ```
47
+
48
+ The audit maps three things around each seed file:
49
+
50
+ - local dependencies used by the seed
51
+ - reverse dependents/callers that reach the seed, including transitive import chains
52
+ - related tests that prove the same path
53
+
54
+ Default interpretation:
55
+
56
+ - `uncovered_reverse_dependency` is `high` and blocks the task: a caller/dependent file exists
57
+ outside the declared review/edit surface.
58
+ - `uncovered_local_dependency` and `uncovered_related_test` are `medium`: the plan is missing a
59
+ neighbor or proof point that should at least be reviewed.
60
+
61
+ For shared/public contracts, signature changes, DTO/schema changes, or refactors in widely
62
+ imported modules, run the stricter gate:
63
+
64
+ ```bash
65
+ python3 scripts/impact_audit.py audit <root> --fail-on medium
66
+ ```
67
+
68
+ The final evidence can cite `.orchestrator/impact-audit.json` or summarize the explicit caller/test
69
+ classification. "I changed one file" is not enough when the dependency map says the task reached
70
+ farther.
71
+
37
72
  ### 4b — WORKS, not just compiles (run-verification, mandatory)
38
73
  "Compiles" ≠ "done". Before done it must RUN:
39
74
  - New/changed command → invoke for real: `--help` returns 0 AND a minimal happy-path produces the
@@ -45,6 +80,34 @@ the sibling of `loop_journal`'s working memory for ATTEMPTS.
45
80
  - Use `validate`/`smoke` if bound. **Front-end change → `web_verify`** (see web-evidence.md):
46
81
  screenshot + trace as evidence. An item that compiles but was never run is PARTIAL.
47
82
 
83
+ ### 4b' — Flow coverage gate for front/back/service workspaces
84
+ When a workspace contains frontend, backend, and services under the same root — or the task touches
85
+ any cross-surface user flow — run a structural flow audit before planning and again before done:
86
+
87
+ ```
88
+ python3 scripts/flow_audit.py audit <root> --fail-on high --json > .orchestrator/flow-audit.json
89
+ ```
90
+
91
+ The audit builds a static map of UI actions, frontend HTTP calls, backend endpoints, and backend
92
+ service calls. It fails the default gate on objective high-confidence gaps:
93
+
94
+ - `frontend_call_without_backend_endpoint`: the UI/client calls an API path that no scanned backend
95
+ exposes.
96
+ - `backend_endpoint_stub`: an endpoint body still looks like TODO, `pass`, `NotImplemented`, 501, or
97
+ a thrown "not implemented" error.
98
+
99
+ Medium gaps are still work, not noise. They must be classified in the task anchor or promoted to an
100
+ AC before done: UI action with no observed backend call, backend endpoint with no observed frontend
101
+ caller, or backend local-looking service call with no local endpoint. If the AC promises backend
102
+ integration for a UI flow, run the stricter gate:
103
+
104
+ ```
105
+ python3 scripts/flow_audit.py audit <root> --fail-on medium
106
+ ```
107
+
108
+ The final evidence must include either `.orchestrator/flow-audit.json` or the human summary. A green
109
+ unit test is not enough when the flow graph still has an unclassified loose end.
110
+
48
111
  ### 4c — Adversarial verify for MEDIUM+ items (multi-vote)
49
112
  Spawn 2–3 INDEPENDENT verifiers, each prompted to REFUTE the implementation AND check each AC.
50
113
  Majority-refute → back to fix. TRIVIAL/SMALL keep single self-review. When `simplicio-review` is
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simplicio-loop
3
- Version: 3.14.1
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
@@ -41,6 +41,8 @@ simplicio_loop/_bundle/skills/simplicio-tasks/references/understand-anything-ada
41
41
  simplicio_loop/_bundle/skills/simplicio-tasks/references/video-evidence.md
42
42
  simplicio_loop/_bundle/skills/simplicio-tasks/references/web-evidence.md
43
43
  tests/test_action_gate.py
44
+ tests/test_flow_audit.py
45
+ tests/test_impact_audit.py
44
46
  tests/test_loop_e2e.py
45
47
  tests/test_worker_selftests.py
46
48
  tests/test_worker_smoke.py
@@ -0,0 +1,72 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+
5
+ REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
6
+ FLOW = os.path.join(REPO, "scripts", "flow_audit.py")
7
+
8
+
9
+ def _write(path, text):
10
+ path.parent.mkdir(parents=True, exist_ok=True)
11
+ path.write_text(text.strip(), encoding="utf-8")
12
+
13
+
14
+ def _run(args, cwd):
15
+ return subprocess.run([sys.executable, FLOW] + args, capture_output=True, text=True, cwd=cwd)
16
+
17
+
18
+ def test_flow_audit_fails_on_frontend_call_without_endpoint(tmp_path):
19
+ _write(tmp_path / "frontend" / "Checkout.tsx", """
20
+ export function Checkout() {
21
+ return <button onClick={() => fetch("/api/checkout", { method: "POST" })}>Pay</button>
22
+ }
23
+ """)
24
+ _write(tmp_path / "backend" / "routes.py", """
25
+ @app.get("/api/health")
26
+ def health():
27
+ return {"ok": True}
28
+ """)
29
+
30
+ r = _run(["audit", str(tmp_path), "--fail-on", "high"], cwd=REPO)
31
+ assert r.returncode == 1, r.stdout
32
+ assert "frontend_call_without_backend_endpoint" in r.stdout, r.stdout
33
+
34
+
35
+ def test_flow_audit_detects_backend_stub(tmp_path):
36
+ _write(tmp_path / "frontend" / "Login.tsx", """
37
+ export function Login() {
38
+ return <button onClick={() => fetch("/api/login", { method: "POST" })}>Login</button>
39
+ }
40
+ """)
41
+ _write(tmp_path / "backend" / "routes.py", """
42
+ @app.post("/api/login")
43
+ def login():
44
+ raise NotImplementedError("TODO")
45
+ """)
46
+
47
+ r = _run(["audit", str(tmp_path), "--fail-on", "high"], cwd=REPO)
48
+ assert r.returncode == 1, r.stdout
49
+ assert "backend_endpoint_stub" in r.stdout, r.stdout
50
+
51
+
52
+ def test_flow_audit_passes_matched_non_stub_flow(tmp_path):
53
+ _write(tmp_path / "frontend" / "Login.tsx", """
54
+ export function Login() {
55
+ return <button onClick={() => fetch("/api/login", { method: "POST" })}>Login</button>
56
+ }
57
+ """)
58
+ _write(tmp_path / "backend" / "routes.py", """
59
+ @app.post("/api/login")
60
+ def login():
61
+ return {"ok": True}
62
+ """)
63
+
64
+ r = _run(["audit", str(tmp_path), "--fail-on", "high"], cwd=REPO)
65
+ assert r.returncode == 0, r.stdout
66
+ assert "flow-audit: PASS" in r.stdout, r.stdout
67
+
68
+
69
+ if __name__ == "__main__":
70
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
71
+ from _selfrun import run_module
72
+ run_module(globals(), "test_flow_audit")
@@ -0,0 +1,125 @@
1
+ import json
2
+ import os
3
+ import subprocess
4
+ import sys
5
+
6
+ REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
7
+ IMPACT = os.path.join(REPO, "scripts", "impact_audit.py")
8
+
9
+
10
+ def _write(path, text):
11
+ path.parent.mkdir(parents=True, exist_ok=True)
12
+ path.write_text(text.strip() + "\n", encoding="utf-8")
13
+
14
+
15
+ def _run(args, cwd):
16
+ return subprocess.run([sys.executable, IMPACT] + args, capture_output=True, text=True, cwd=cwd)
17
+
18
+
19
+ def test_impact_audit_fails_on_transitive_reverse_dependency(tmp_path):
20
+ _write(tmp_path / "app" / "util.py", """
21
+ def helper():
22
+ return 1
23
+ """)
24
+ _write(tmp_path / "app" / "service.py", """
25
+ from .util import helper
26
+
27
+ def compute():
28
+ return helper()
29
+ """)
30
+ _write(tmp_path / "app" / "controller.py", """
31
+ from app.service import compute
32
+
33
+ def run():
34
+ return compute()
35
+ """)
36
+
37
+ r = _run(["audit", str(tmp_path), "--file", "app/util.py", "--cover", "app/util.py"], cwd=REPO)
38
+ assert r.returncode == 1, r.stdout
39
+ assert "app/service.py" in r.stdout, r.stdout
40
+ assert "app/controller.py" in r.stdout, r.stdout
41
+ assert "uncovered_reverse_dependency" in r.stdout, r.stdout
42
+
43
+
44
+ def test_impact_audit_passes_when_callers_and_tests_are_covered(tmp_path):
45
+ _write(tmp_path / "app" / "util.py", """
46
+ def helper():
47
+ return 1
48
+ """)
49
+ _write(tmp_path / "app" / "service.py", """
50
+ from .util import helper
51
+
52
+ def compute():
53
+ return helper()
54
+ """)
55
+ _write(tmp_path / "tests" / "test_service.py", """
56
+ from app.service import compute
57
+
58
+ def test_compute():
59
+ assert compute() == 1
60
+ """)
61
+
62
+ r = _run(
63
+ [
64
+ "audit",
65
+ str(tmp_path),
66
+ "--file",
67
+ "app/util.py",
68
+ "--cover",
69
+ "app/util.py",
70
+ "--cover",
71
+ "app/service.py",
72
+ "--cover",
73
+ "tests/test_service.py",
74
+ ],
75
+ cwd=REPO,
76
+ )
77
+ assert r.returncode == 0, r.stdout
78
+ assert "impact-audit: PASS" in r.stdout, r.stdout
79
+
80
+
81
+ def test_impact_audit_json_ok_tracks_fail_threshold(tmp_path):
82
+ _write(tmp_path / "app" / "service.py", """
83
+ from .util import helper
84
+
85
+ def compute():
86
+ return helper()
87
+ """)
88
+ _write(tmp_path / "app" / "util.py", """
89
+ def helper():
90
+ return 1
91
+ """)
92
+
93
+ r = _run(
94
+ [
95
+ "audit",
96
+ str(tmp_path),
97
+ "--file",
98
+ "app/service.py",
99
+ "--cover",
100
+ "app/service.py",
101
+ "--fail-on",
102
+ "medium",
103
+ "--json",
104
+ ],
105
+ cwd=REPO,
106
+ )
107
+ assert r.returncode == 1, r.stdout
108
+ payload = json.loads(r.stdout)
109
+ assert payload["fail_on"] == "medium"
110
+ assert payload["ok"] is False
111
+ assert payload["counts"]["blocking_issues"] >= 1
112
+ assert any(issue["code"] == "uncovered_local_dependency" for issue in payload["blocking_issues"])
113
+
114
+
115
+ def test_impact_audit_without_seed_is_blocked(tmp_path):
116
+ r = _run(["audit", str(tmp_path)], cwd=REPO)
117
+ assert r.returncode == 2, r.stdout
118
+ assert "BLOCKED" in r.stdout, r.stdout
119
+
120
+
121
+ if __name__ == "__main__":
122
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
123
+ from _selfrun import run_module
124
+
125
+ run_module(globals(), "test_impact_audit")
@@ -1,8 +1,8 @@
1
1
  """Aggregate the deterministic `selftest` of every worker that ships one.
2
2
 
3
- Each worker (`loop_journal`, `billing_aggregator`, `savings_harness`) carries a model-free
4
- `selftest` that proves its arithmetic with no files. This runs them as subprocesses and asserts
5
- exit 0 + a PASS line — so `python3 scripts/check.py` (or pytest) re-proves them on every change.
3
+ Each worker with a model-free `selftest` proves its own contract with no external services. This
4
+ runs them as subprocesses and asserts exit 0 + a PASS line — so `python3 scripts/check.py`
5
+ (or pytest) re-proves them on every change.
6
6
  """
7
7
  import os
8
8
  import subprocess
@@ -17,6 +17,8 @@ SELFTESTS = [
17
17
  ("scripts/savings_harness.py", "selftest"),
18
18
  ("scripts/task_anchor.py", "selftest"),
19
19
  ("scripts/pr_evidence.py", "selftest"),
20
+ ("scripts/flow_audit.py", "selftest"),
21
+ ("scripts/impact_audit.py", "selftest"),
20
22
  ]
21
23
 
22
24
 
@@ -56,6 +58,18 @@ def test_pr_evidence_selftest():
56
58
  assert "PASS" in r.stdout, r.stdout
57
59
 
58
60
 
61
+ def test_flow_audit_selftest():
62
+ r = _run("scripts/flow_audit.py", "selftest")
63
+ assert r.returncode == 0, "flow_audit selftest failed:\n%s%s" % (r.stdout, r.stderr)
64
+ assert "PASS" in r.stdout, r.stdout
65
+
66
+
67
+ def test_impact_audit_selftest():
68
+ r = _run("scripts/impact_audit.py", "selftest")
69
+ assert r.returncode == 0, "impact_audit selftest failed:\n%s%s" % (r.stdout, r.stderr)
70
+ assert "PASS" in r.stdout, r.stdout
71
+
72
+
59
73
  if __name__ == "__main__":
60
74
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
61
75
  from _selfrun import run_module
File without changes
File without changes