simplicio-loop 3.11.0__tar.gz → 3.14.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 (48) hide show
  1. {simplicio_loop-3.11.0/simplicio_loop.egg-info → simplicio_loop-3.14.0}/PKG-INFO +1 -1
  2. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/README.md +1 -1
  3. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/pyproject.toml +1 -1
  4. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/__init__.py +2 -2
  5. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/hooks/loop_capture.py +20 -1
  6. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/hooks/loop_stop.py +37 -3
  7. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-loop/SKILL.md +36 -15
  8. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-tasks/SKILL.md +27 -3
  9. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/extension-points.md +3 -3
  10. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/quality-safety-delivery.md +23 -0
  11. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/understand-anything-adapter.md +1 -1
  12. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0/simplicio_loop.egg-info}/PKG-INFO +1 -1
  13. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/tests/test_loop_e2e.py +36 -0
  14. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/tests/test_worker_selftests.py +14 -0
  15. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/tests/test_worker_smoke.py +69 -0
  16. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/LICENSE +0 -0
  17. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/MANIFEST.in +0 -0
  18. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/PYPI.md +0 -0
  19. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/setup.cfg +0 -0
  20. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/hooks/README.md +0 -0
  21. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/hooks/action_gate.py +0 -0
  22. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/hooks/hooks.claude.json +0 -0
  23. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/hooks/hooks.json +0 -0
  24. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/hooks/learn_stop.py +0 -0
  25. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/hooks/orient_clamp.py +0 -0
  26. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/hooks/orient_rewrite.py +0 -0
  27. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/hooks/simplicio_dashboard.py +0 -0
  28. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/hooks/simplicio_watch.py +0 -0
  29. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-compress/SKILL.md +0 -0
  30. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-learn/SKILL.md +0 -0
  31. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-orient/SKILL.md +0 -0
  32. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-review/SKILL.md +0 -0
  33. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/agentsview-adapter.md +0 -0
  34. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/azure-devops-adapter.md +0 -0
  35. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/lmcache-adapter.md +0 -0
  36. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/orchestration.md +0 -0
  37. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/standing-loop-247.md +0 -0
  38. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-capture.md +0 -0
  39. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-economy.md +0 -0
  40. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/video-evidence.md +0 -0
  41. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/web-evidence.md +0 -0
  42. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop/cli.py +0 -0
  43. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop.egg-info/SOURCES.txt +0 -0
  44. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop.egg-info/dependency_links.txt +0 -0
  45. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop.egg-info/entry_points.txt +0 -0
  46. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop.egg-info/requires.txt +0 -0
  47. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/simplicio_loop.egg-info/top_level.txt +0 -0
  48. {simplicio_loop-3.11.0 → simplicio_loop-3.14.0}/tests/test_action_gate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simplicio-loop
3
- Version: 3.11.0
3
+ Version: 3.14.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
@@ -81,7 +81,7 @@ protocol on 11 runtimes**, and it does all of this with **aggressive, honest tok
81
81
 
82
82
  ---
83
83
 
84
- ## 📘 Official capability record (v3.10.2)
84
+ ## 📘 Official capability record
85
85
 
86
86
  The complete, official roster of what `simplicio-tasks` ships — every capability below is **real,
87
87
  runnable, and tested** (`python3 scripts/check.py`: claims-audit 4/4 + 28 tests). Each links to its
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simplicio-loop"
7
- version = "3.11.0"
7
+ version = "3.14.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.11.0"
16
+ __version__ = "3.14.0"
17
17
  except Exception: # pragma: no cover
18
- __version__ = "3.11.0"
18
+ __version__ = "3.14.0"
@@ -18,6 +18,7 @@ LOOP_DIR = os.path.join(".orchestrator", "loop")
18
18
  SCRATCHPAD = os.path.join(LOOP_DIR, "scratchpad.md")
19
19
  DONE_FLAG = os.path.join(LOOP_DIR, "done")
20
20
  LAST_RESP = os.path.join(LOOP_DIR, "last_response.txt")
21
+ ANCHOR = os.path.join(LOOP_DIR, "anchor.json")
21
22
 
22
23
  PROMISE_RE = re.compile(r"<promise>\s*(.*?)\s*</promise>", re.IGNORECASE | re.DOTALL)
23
24
  EVIDENCE_RE = re.compile(
@@ -26,6 +27,22 @@ EVIDENCE_RE = re.compile(
26
27
  )
27
28
 
28
29
 
30
+ def anchor_pending():
31
+ """Unverified acceptance-criteria ids from the task anchor, or [] (fail-open).
32
+
33
+ Mirror of loop_stop.anchor_pending: the done flag is raised only when every anchored AC is
34
+ `done`. Reads the anchor JSON directly (no dependency on scripts/, which the lean plugin omits);
35
+ a missing/unreadable/empty anchor returns [] so it never blocks completion.
36
+ """
37
+ try:
38
+ with open(ANCHOR, encoding="utf-8") as f:
39
+ data = json.load(f)
40
+ return [c.get("id") for c in (data.get("criteria") or [])
41
+ if isinstance(c, dict) and c.get("status") != "done"]
42
+ except Exception:
43
+ return []
44
+
45
+
29
46
  def main():
30
47
  try:
31
48
  raw = sys.stdin.read()
@@ -53,7 +70,9 @@ def main():
53
70
  evidence_required = "evidence_required: false" not in content.lower()
54
71
  m = PROMISE_RE.search(resp)
55
72
  if m and m.group(1).strip() == promise:
56
- if (not evidence_required) or EVIDENCE_RE.search(resp):
73
+ # Evidence-gated AND anchor-gated: raise `done` only with in-turn evidence and no
74
+ # acceptance criterion still open in the task anchor (mechanical anti-drift).
75
+ if ((not evidence_required) or EVIDENCE_RE.search(resp)) and not anchor_pending():
57
76
  try:
58
77
  open(DONE_FLAG, "w").close() # raise the flag; stop hook acts
59
78
  except OSError:
@@ -21,6 +21,7 @@ LOOP_DIR = os.path.join(".orchestrator", "loop")
21
21
  SCRATCHPAD = os.path.join(LOOP_DIR, "scratchpad.md")
22
22
  DONE_FLAG = os.path.join(LOOP_DIR, "done")
23
23
  LAST_RESP = os.path.join(LOOP_DIR, "last_response.txt")
24
+ ANCHOR = os.path.join(LOOP_DIR, "anchor.json")
24
25
  STOP_SIGNAL = os.path.join(".orchestrator", "STOP")
25
26
  BUDGET = os.path.join(".orchestrator", "loop-budget.json")
26
27
  GATE_LOCK = os.path.join(LOOP_DIR, "gate.lock")
@@ -127,6 +128,27 @@ def gate_running():
127
128
  return False
128
129
 
129
130
 
131
+ def anchor_pending():
132
+ """Return the unverified acceptance-criteria ids from the task anchor, or [].
133
+
134
+ 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.
141
+ """
142
+ 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"]
148
+ except Exception:
149
+ return [] # fail-open: anchor unreadable ≠ trap
150
+
151
+
130
152
  def budget_halted():
131
153
  try:
132
154
  if not os.path.exists(BUDGET):
@@ -189,9 +211,12 @@ def main():
189
211
  m = PROMISE_RE.search(resp)
190
212
  if m and m.group(1).strip() == promise.strip():
191
213
  has_evidence = bool(EVIDENCE_RE.search(resp))
192
- if (not evidence_required) or has_evidence:
214
+ # The promise is honored only with evidence AND no acceptance criterion still open
215
+ # in the task anchor — the mechanical anti-drift gate. Pending ACs ⇒ ignore the
216
+ # promise and keep looping (still bounded by max_iter), never a false "done".
217
+ if ((not evidence_required) or has_evidence) and not anchor_pending():
193
218
  cleanup_and_stop() # (3) promise fulfilled → stop
194
- # promise without evidence → ignore, keep looping
219
+ # promise without evidence, or anchor still has open ACs → ignore, keep looping
195
220
  # (3') Cursor capture may have raised the flag.
196
221
  if os.path.exists(DONE_FLAG):
197
222
  cleanup_and_stop()
@@ -219,7 +244,16 @@ def main():
219
244
  if promise
220
245
  else ""
221
246
  )
222
- header = "[simplicio-loop iteration %d.%s]" % (nxt, promise_hint)
247
+ # Surface the still-open acceptance criteria so the next turn knows exactly what blocks
248
+ # "done" — the anchor gate is why a promise would be ignored, so name the gap.
249
+ pending = anchor_pending()
250
+ ac_hint = (
251
+ " Open acceptance criteria (verify each before the promise): %s."
252
+ % ", ".join(p for p in pending if p)
253
+ if pending
254
+ else ""
255
+ )
256
+ header = "[simplicio-loop iteration %d.%s%s]" % (nxt, promise_hint, ac_hint)
223
257
  emit_refeed(header + "\n\n" + (body or ""))
224
258
  except Exception:
225
259
  allow_stop() # fail-open, always
@@ -49,25 +49,37 @@ hard dependencies of the `simplicio-loop` package (`pip install simplicio-loop`
49
49
 
50
50
  | Operator | CLI (binary) | Binds | Role in the loop |
51
51
  |---|---|---|---|
52
- | **simplicio-mapper** | `simplicio-mapper` | `orient` / `recall` | **Survey** — maps the repo(s) into `.simplicio/*.json` (project-map, precedent-index, symbol-index, call-graph, docs). This survey, not an ad-hoc LLM read, is what feeds the goal each turn. |
52
+ | **simplicio-mapper** | `simplicio-mapper` | `orient` / `recall` | **Survey** — maps the repo(s) into `.simplicio/*.json` (project-map, precedent-index, symbol-index, call-graph, docs). Two-tier (v0.9+): `macro` is an instant shallow skeleton (no content reads), `scan` returns that skeleton now and runs the deep index in the background, `status` reports the deep-pass phase. This survey, not an ad-hoc LLM read, is what feeds the goal each turn. |
53
53
  | **simplicio-dev-cli** | `simplicio-dev-cli` | `execute` / `deterministic_edit` / `validate` / `diagnostics` | **Operate** — applies a DECIDED change through its 6-layer contract (mapper context → precedent → prompt → diff → test → verify, ≤3 retries). The CLI edits and verifies; the AI does not hand-write the diff. |
54
54
 
55
- **Preflight (MANDATORY, BLOCKING).** Before iteration 1, confirm both operators are on PATH:
55
+ **Preflight (MANDATORY, BLOCKING).** Before iteration 1, auto-update both operators to their latest
56
+ release (so every run uses the newest `simplicio-mapper`/`simplicio-cli`), then confirm both are on
57
+ PATH:
56
58
  ```bash
57
- simplicio-mapper --version # survey operator
59
+ # Always run the loop on the latest operators. FAIL-OPEN: offline / no-pip / a pin keeps the
60
+ # currently-installed build; this never blocks. Runs ONCE per loop preflight, not per turn.
61
+ python3 -m pip install -qU simplicio-mapper simplicio-cli 2>/dev/null \
62
+ || python3 -m pip install -qU --user --break-system-packages simplicio-mapper simplicio-cli 2>/dev/null || true
63
+ simplicio-mapper --version # survey operator (now latest)
58
64
  simplicio-dev-cli --help # action operator (pkg simplicio-cli; exposes `simplicio-dev-cli`)
59
65
  ```
60
- The action binary is `simplicio-dev-cli` (from `pip install simplicio-cli`) — NOT the bare
66
+ The auto-update is best-effort and offline-safe — a network/pip failure leaves the working version
67
+ in place and the loop proceeds. The action binary is `simplicio-dev-cli` (from `pip install simplicio-cli`) — NOT the bare
61
68
  `simplicio`, which is reserved for the separate `simplicio-runtime` and is not what this loop
62
69
  binds. `simplicio-dev-cli` has no `--version` subcommand; `--help` exiting 0 is the readiness
63
70
  proof. If either operator is missing, do NOT fall back to LLM survey/editing — STOP and emit
64
71
  `simplicio-loop: BLOCKED — missing operator <name>; run: pip install simplicio-loop` (the install
65
72
  re-pulls `simplicio-mapper` + `simplicio-cli`). This requirement is scoped to the loop drive.
66
73
 
67
- **Survey step (each loop start + on any structural change).** Run
68
- `simplicio-mapper index . --json` (add `--watch` for long runs) to (re)build `.simplicio/`. Read
69
- the survey artifacts — never re-scan the tree by hand when a fresh map exists. For a multi-repo
70
- survey, run the mapper per repo root and aggregate the JSON.
74
+ **Survey step (each loop start + on any structural change).** Prefer the two-tier flow (v0.9+):
75
+ `simplicio-mapper scan . --json` returns an instant `macro` skeleton AND kicks the deep index off in
76
+ the background — the loop starts working immediately instead of blocking on a full crawl. Poll
77
+ `simplicio-mapper status . --json` (`phase`: `deep_running` → terminal) before relying on the deep
78
+ artifacts; pass `--await [--timeout <s>]` to block until terminal, or `scan --sync` (forced when
79
+ `CI=true`) for the old single-shot behavior. `simplicio-mapper index . --json` (add `--watch` for
80
+ long runs) remains the synchronous full (re)build of `.simplicio/`. Read the survey artifacts —
81
+ never re-scan the tree by hand when a fresh map exists. For a multi-repo survey, run the mapper per
82
+ repo root and aggregate the JSON.
71
83
 
72
84
  **Operate step (every turn that mutates code).** Once the AC and the change are DECIDED, delegate
73
85
  the mutation to the operator, one decided change at a time:
@@ -84,9 +96,9 @@ merge/close gates); the operators do survey + apply:
84
96
 
85
97
  | Phase | Operator | Command |
86
98
  |---|---|---|
87
- | Preflight (before iteration 1) | both | `simplicio-mapper --version` · `simplicio-dev-cli --help` → BLOCK if missing |
88
- | Survey (loop start; multi-repo: per root) | mapper | `simplicio-mapper index . --json` → `.simplicio/*.json` |
89
- | Loop contract step 2 — Triage (every turn) | mapper | re-read `.simplicio/*.json`; `simplicio-mapper index . --json` to refresh if the tree changed |
99
+ | Preflight (before iteration 1) | both | `python3 -m pip install -qU simplicio-mapper simplicio-cli` (auto-update to latest, fail-open) → `simplicio-mapper --version` · `simplicio-dev-cli --help` → BLOCK if missing |
100
+ | Survey (loop start; multi-repo: per root) | mapper | `simplicio-mapper scan . --json` (instant macro + deep index in background; `--sync`/`--await` to block) → `.simplicio/*.json`. `index . --json` for a forced synchronous build |
101
+ | Loop contract step 2 — Triage (every turn) | mapper | re-read `.simplicio/*.json`; `simplicio-mapper macro . --json` for an instant skeleton, or `scan`/`status` to refresh if the tree changed |
90
102
  | Loop contract step 3 — Work the goal | dev-cli | `simplicio-dev-cli task "<decided change>" --target <file> [--json]` |
91
103
  | Evidence-gated `<promise>` / `simplicio-tasks` Step 4b | dev-cli | the operator's passing test+verify pass = in-turn evidence |
92
104
 
@@ -151,14 +163,19 @@ detector below. It is the difference between a loop that converges and one that
151
163
  `.orchestrator/loop-budget.json` $ kill-switch (see `simplicio-tasks` Step 1a/7).
152
164
  2. **Triage the live state FIRST (mandatory).** Before any action each turn, re-read the ground
153
165
  truth — the **`simplicio-mapper` survey** (`.simplicio/*.json`; refresh it with
154
- `simplicio-mapper index . --json` if the tree changed), `git status`/`git diff`, the working
166
+ `simplicio-mapper macro . --json` for an instant skeleton or `scan . --json` if the tree changed),
167
+ `git status`/`git diff`, the working
155
168
  tree, the scratchpad notes, AND the source of record (re-query the open issues/PRs, existing
156
169
  branches, the `.orchestrator/loop/done` flag). **Also read the attempt memory FIRST**:
157
170
  `python3 scripts/loop_journal.py resume` — it lists what was already tried and the dead-end
158
171
  actions to AVOID, so the turn never re-runs a known-failing approach. For **incremental triage**
159
172
  (don't re-scan the whole tree every turn), `loop_journal.py since` shows only the delta since the
160
- last recorded turn's commit. Act only on what is still genuinely open; never redo done work or
161
- act on a stale picture (idempotency).
173
+ last recorded turn's commit. **And re-read the task anchor** — `python3 scripts/task_anchor.py
174
+ check --goal "<the goal worked this turn>" --exit-code` — so the turn stays on the SAME frozen
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).
162
179
  3. **Work the goal** each turn as if fresh, against that triaged state. The model DECIDES the
163
180
  AC-scoped change; the **`simplicio-dev-cli` operator APPLIES and verifies it**
164
181
  (`simplicio-dev-cli task "<change>" --target <file>`) — do not hand-edit inside the loop. End EVERY
@@ -245,7 +262,11 @@ only if, in the SAME turn, there is concrete evidence the work is truly done:
245
262
 
246
263
  - the run-verification gate passed ("works, not just compiles" — `simplicio-tasks` Step 4b) —
247
264
  the `simplicio-dev-cli` operator's passing test+verify pass (its contract step 5/6) satisfies this, or
248
- - the named acceptance criteria are each checked with a `file:line` or command-output receipt, or
265
+ - the named acceptance criteria are each checked with a `file:line` or command-output receipt —
266
+ mechanically enforced by the task anchor: `python3 scripts/task_anchor.py gate --exit-code` must
267
+ return READY (every anchored AC `done` with a receipt; exit 12 = still pending) before the promise
268
+ is allowed. An anchor with pending criteria makes the `<promise>` a contract violation, exactly
269
+ like missing evidence, or
249
270
  - for a queue, the source re-query confirms the items are actually closed/merged, or
250
271
  - a **demo video** of the change running on screen — a deterministic MP4 rendered with
251
272
  **hyperframes** via the `video_evidence` producer (below) — whose ledger row + MP4 path prove
@@ -164,7 +164,11 @@ List candidates by METADATA only; normalize to the canonical schema; dedup by so
164
164
  normalized-title + fingerprint AND by existing branch/PR (idempotency). Before implementing an
165
165
  item, do the MANDATORY deep intake: read full body + ALL comments, extract acceptance criteria
166
166
  (an obvious-but-missing AC is a BLOCKER — ask once), orient the existing code (signatures-only
167
- reads for API surface), then write a short plan with an AC checklist + complexity. Detail:
167
+ reads for API surface), then write a short plan with an AC checklist + complexity. **FREEZE the
168
+ acceptance criteria as the task anchor** (`python3 scripts/task_anchor.py set --item <id> --goal
169
+ "<verbatim>" --ac "<AC>" …`) — this is the loop's memory for SCOPE (sibling to `loop_journal`'s
170
+ memory for ATTEMPTS): it is what every later turn re-checks so the run cannot silently narrow or
171
+ wander off the task (the "desvio de tarefas" fix, Step 4 drift guard). Detail:
168
172
  `references/orchestration.md`.
169
173
 
170
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.
@@ -203,8 +207,19 @@ Never mark done without green gates + evidence; a failure is NOT a blocker — i
203
207
  re-trying the same approach: switch strategy, or escalate via the human gate (Step 5) with the
204
208
  fingerprint + dead-ends. Start each turn with `loop_journal.py resume` to avoid known dead-ends.
205
209
  Delegate to `simplicio-loop` when loaded (§ Run-journal + stall detector).
210
+ - **AC anchor + drift guard (anti-deviation).** Every turn, BEFORE acting, re-read the frozen
211
+ anchor and verify you are still on the SAME task: `python3 scripts/task_anchor.py check --goal
212
+ "<the goal you are working now>" --exit-code` (verdict `DRIFT` ⇒ exit 11 — the goal moved; STOP
213
+ and re-anchor explicitly with `--force`, never drift silently). As each AC is genuinely met,
214
+ record its receipt — `task_anchor.py mark --id ACk --status done --evidence "<file:line / cmd /
215
+ screenshot>"` (a `done` with no evidence is REFUSED). The anchor is the runnable form of
216
+ "never narrow the task": it makes the orchestrator's working memory for SCOPE durable, exactly as
217
+ `loop_journal` does for ATTEMPTS.
206
218
  - **4a AC gate (real DoD):** verify EVERY AC explicitly; no placeholder/stub success, no
207
- `todo!()`/`panic!` in prod paths, reads from context, compiles clean on changed files.
219
+ `todo!()`/`panic!` in prod paths, reads from context, compiles clean on changed files. The gate
220
+ is mechanical: `python3 scripts/task_anchor.py gate --exit-code` (exit 12 = criteria still
221
+ pending) MUST pass before you declare done or open the PR — "done" requires every anchored AC
222
+ verified with a receipt.
208
223
  - **4b WORKS, not just compiles:** RUN it (`--help` + happy path / affected tests). Front-end
209
224
  change → `web_verify` (screenshot + trace, `references/web-evidence.md`). For moving proof of a UI
210
225
  change, `video_evidence verify --url <url>` records the **real session with Playwright** (default
@@ -234,7 +249,16 @@ Never mark done without green gates + evidence; a failure is NOT a blocker — i
234
249
 
235
250
  ## Step 6 — Deliver + close + self-audit · Step 6b — Feedback loop
236
251
  Per completed item: commit (Conventional Commits, English), push, Draft PR, close in-source with a
237
- short evidence comment (PR link + verification). If the `pr_template` extension point is available, use it to auto-fill the PR body from the discovered PULL_REQUEST_TEMPLATE.md and the item's acceptance criteria. Otherwise, read .github/PULL_REQUEST_TEMPLATE.md directly and fill in the sections. **Verify reality, never trust self-report** — the
252
+ short evidence comment (PR link + verification). **Assemble the PR body mechanically so it ALWAYS
253
+ carries prints + an item-by-item AC check** — `python3 scripts/pr_evidence.py build --item <id>
254
+ --title "<t>" --summary "<s>" --require-evidence --out .orchestrator/pr_body.md` pulls the
255
+ item-by-item acceptance-criteria checklist from the task anchor (Step 2b/4) AND embeds the
256
+ screenshots/recordings captured by `web_verify`/`video_evidence` under `.orchestrator/tee/web`; with
257
+ `--require-evidence` it EXITS 3 (blocked) rather than open a PR with no prints and no checklist (the
258
+ "PR sem evidência" fix). It honors the discovered `.github/PULL_REQUEST_TEMPLATE.md` (the
259
+ `pr_template` extension point) — appending the checklist + prints under the maintainer's sections —
260
+ and `pr_evidence.py comment --item <id> --pr <N>` emits the matching in-source evidence comment.
261
+ **Verify reality, never trust self-report** — the
238
262
  final step re-runs the merged build/test + smoke + a source re-query; the run's status = that
239
263
  measured state. Then self-audit (score, fix P0/P1, converge). Pursue the feedback loop until
240
264
  merge-ready: CI fail → fix root cause; review comments → adjust; branch behind main → additive
@@ -36,10 +36,10 @@ fallback with standard tools. The skill depends on the ABSTRACTION, never on a s
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 |
39
- | `delivery_gate` | One DoD gate: AC check + run-verification + regression guard + diff self-review + delivery certificate | LLM walks the AC checklist, runs affected tests, reviews own diff, writes a certificate into the receipt |
39
+ | `delivery_gate` | One DoD gate: AC check + run-verification + regression guard + diff self-review + delivery certificate. Runnable form: the **task anchor** (`scripts/task_anchor.py`) freezes the ACs at intake, `check` flags goal-drift each turn (anti-deviation), `mark` records a per-AC receipt, and `gate` (exit 12) BLOCKS "done"/PR-open while any AC is unverified. | LLM walks the AC checklist, runs affected tests, reviews own diff, writes a certificate into the receipt |
40
40
  | `action_gate` | Risk-classify every mutation (safe/auto/ask) vs allow/deny + hardline blocklist before it runs | LLM pattern-matches action vs irreversible-op list, secret-scans, proceeds/auto-runs/escalates to `human_gate` |
41
41
  | `repo_conventions` | LEARN the repo's own playbook, not just what's documented. Worker `scripts/repo_conventions.py learn` mines the git history (branch-name scheme, commit convention + REAL scope list, ticket pattern — by frequency) + merged PRs via `gh` (title pattern, label vocab, PR-body sections) + static config (CONTRIBUTING.md/AGENTS.md/pyproject.toml for a Conventional-Commits/commitizen hint, and the PR template for body-section structure) → one hash-pinned `.orchestrator/conventions.json` (`source=history\|config\|default`, confidence-gated: sparse history degrades to an honest default, never an over-fit guess). Steps 4–6 apply it deterministically via `repo_conventions.py branch`/`commit`. PR bodies are untrusted data; a learned convention never overrides a safety gate. | LLM reads CONTRIBUTING.md + AGENTS.md + .github/ + pyproject.toml AND skims `git log`/`gh pr list` for the dominant branch/commit/PR pattern, emits the same structured conventions summary that shapes Step 4–6 (branch naming, commit scopes, PR checklist, CI/lint/test gates). |
42
- | `pr_template` | Discover .github/PULL_REQUEST_TEMPLATE.md, parse structured sections (what/why, how to test, checklist), map completed ACs to checklist items, and auto-fill the PR body before creation. Ensures every PR matches the maintainer's expected format on first submission. | LLM reads .github/PULL_REQUEST_TEMPLATE.md, maps completed acceptance criteria to each checklist item, fills in what/why from the item description and the implementation summary, lists changed files with rationale. |
42
+ | `pr_template` | Discover .github/PULL_REQUEST_TEMPLATE.md, parse structured sections (what/why, how to test, checklist), map completed ACs to checklist items, and auto-fill the PR body before creation. Ensures every PR matches the maintainer's expected format on first submission. Runnable form: `scripts/pr_evidence.py build --require-evidence` assembles the body with the **item-by-item AC checklist** (from the task anchor) + the **prints/recordings** captured under `.orchestrator/tee/web`, honoring the discovered template; it FAILS CLOSED (exit 3) rather than open an evidence-less PR. | LLM reads .github/PULL_REQUEST_TEMPLATE.md, maps completed acceptance criteria to each checklist item, fills in what/why from the item description and the implementation summary, lists changed files with rationale. |
43
43
  | `reuse_precedent` | Match item by fingerprint to a prior SOLVED run → reuse not regenerate → ingest the new solution back | LLM greps past PRs/closed issues/solved-patterns journal for the fingerprint, applies it, appends new solution |
44
44
  | `sibling_search` | Given a changed file/function/pattern, find all other call sites or implementations with the same pattern across the repo. Enumerate sibling paths so the fix covers the entire bug class, not just one reported instance. Critical for projects where maintainers reject single-site fixes. | LLM uses `grep -rn` / `rg` for the symbol/pattern, traces each candidate with `git log -p -S`, reads each sibling to confirm the same bug class, then includes all confirmed siblings in the fix scope. |
45
45
  | `source_adapter` | Uniform source connector contract (list_ready/get_details/claim/update/attach/close) bound per source | LLM calls the source CLI/REST per verb; lockfile/label claim with TTL for cross-session safety |
@@ -66,7 +66,7 @@ them to two installed CLIs (hard deps of `pip install simplicio-loop`) and BLOCK
66
66
 
67
67
  | Point(s) | Bound CLI | What replaces the LLM fallback |
68
68
  |---|---|---|
69
- | `orient` / `recall` | `simplicio-mapper` (`simplicio-mapper index . --json`) | the repo SURVEY — `.simplicio/*.json` (project-map, precedent-index, symbol-index, call-graph) instead of ad-hoc LLM reads |
69
+ | `orient` / `recall` | `simplicio-mapper` (`simplicio-mapper scan . --json`; `macro` for an instant skeleton, `status` for the deep-pass phase, `index . --json` for a forced synchronous build) | the repo SURVEY — `.simplicio/*.json` (project-map, precedent-index, symbol-index, call-graph) instead of ad-hoc LLM reads |
70
70
  | `execute` / `deterministic_edit` / `validate` / `diagnostics` | `simplicio-dev-cli task` (binary `simplicio-dev-cli`, pkg `simplicio-cli`) | the OPERATOR — applies a decided change via its 6-layer contract (mapper→precedent→prompt→diff→test→verify, ≤3 retries); the AI never hand-writes the diff inside the loop |
71
71
 
72
72
  This is the one place the abstraction is realized by a REQUIRED binding rather than an optional
@@ -24,6 +24,16 @@ DoD per item:
24
24
  ```
25
25
  Done only when fully green. "N/A" on a real AC → mark `partial`, note what's missing.
26
26
 
27
+ **Anchor the ACs — don't re-derive them (anti-deviation).** The acceptance criteria are frozen
28
+ ONCE at intake as the task anchor (`task_anchor.py set`, Step 2b) and re-checked every turn so the
29
+ run cannot silently narrow or wander off the task. Per turn: `task_anchor.py check --goal "<goal
30
+ worked now>" --exit-code` (verdict `DRIFT`/exit 11 ⇒ the goal moved — STOP, re-anchor with `--force`
31
+ only if the task genuinely changed). As each AC is met: `task_anchor.py mark --id ACk --status done
32
+ --evidence "<file:line / command output / screenshot path>"` (a `done` with no receipt is REFUSED).
33
+ The DoD gate is then mechanical: `task_anchor.py gate --exit-code` (exit 12 = criteria still
34
+ pending) MUST pass before "done" or PR-open. This is the loop's durable working memory for SCOPE,
35
+ the sibling of `loop_journal`'s working memory for ATTEMPTS.
36
+
27
37
  ### 4b — WORKS, not just compiles (run-verification, mandatory)
28
38
  "Compiles" ≠ "done". Before done it must RUN:
29
39
  - New/changed command → invoke for real: `--help` returns 0 AND a minimal happy-path produces the
@@ -91,6 +101,19 @@ sections + label vocabulary. Then push, Draft PR, close the item in its source w
91
101
  comment (PR link + verification summary). When the profile is `source=default` (no clear repo
92
102
  history), fall back to Conventional Commits and say so.
93
103
 
104
+ **Every PR carries prints + an item-by-item AC check (the `pr_evidence` worker).** Do NOT hand-write
105
+ the PR body and risk forgetting the proof — assemble it mechanically:
106
+ `python3 scripts/pr_evidence.py build --item <id> --title "<t>" --summary "<s>" --require-evidence
107
+ --out .orchestrator/pr_body.md`. It pulls the item-by-item checklist from the task anchor (one line
108
+ per AC, with its status + the receipt that verified it) AND embeds every screenshot/recording
109
+ captured by `web_verify`/`video_evidence` under `.orchestrator/tee/web`. With `--require-evidence`
110
+ it FAILS CLOSED — exit 3 (`blocked`), never a body — when there is neither a checklist nor a single
111
+ print, so an evidence-less PR cannot be opened by accident. It honors a discovered
112
+ `.github/PULL_REQUEST_TEMPLATE.md` (keeps the maintainer's sections, appends the checklist + prints
113
+ below). `pr_evidence.py comment --item <id> --pr <N>` emits the matching in-source evidence comment
114
+ (PR link + per-AC check + a count of attached prints). Write surrounding comment PROSE in the user's
115
+ language; keep paths/identifiers in English.
116
+
94
117
  **Verify in the workflow, never trust self-report.** When a fan-out drove the run, its FINAL step
95
118
  re-verifies reality: the merged build/test, the `smoke` gate, and a source re-query confirming
96
119
  items are actually closed. The run's status = that measured state, not the sum of agent claims.
@@ -38,7 +38,7 @@ file — no LLM invocation, no re-scan. The full analysis is re-run only intenti
38
38
 
39
39
  | Extension point | Default (`simplicio-mapper`) | Understand Anything adapter |
40
40
  |---|---|---|
41
- | `orient` | `simplicio-mapper index . --json` → `.simplicio/*.json` | Read `.understand-anything/knowledge-graph.json` + `jq` queries |
41
+ | `orient` | `simplicio-mapper scan . --json` (or `index . --json` synchronous) → `.simplicio/*.json` | Read `.understand-anything/knowledge-graph.json` + `jq` queries |
42
42
  | `recall` | `.simplicio/precedent-index.json` (past resolutions) | Guided tours + semantic search over the graph + prior `/understand-diff` snapshots |
43
43
 
44
44
  ### When to use which
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simplicio-loop
3
- Version: 3.11.0
3
+ Version: 3.14.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
@@ -5,6 +5,8 @@ the loop stops on **evidence**, not on a bare promise, and not by accident. We d
5
5
 
6
6
  • promise + evidence → STOP (state cleaned up) ← the success exit
7
7
  • promise WITHOUT evidence → CONTINUE (re-feed, ignored) ← the anti-false-done guard
8
+ • promise + evidence, AC pending → CONTINUE (re-feed, ignored) ← the anti-DRIFT anchor gate
9
+ • promise + evidence, ACs done → STOP ← anchor satisfied
8
10
  • no promise, under cap → CONTINUE (iteration bumped)
9
11
  • iteration >= max_iterations → STOP by cap ← distinct from the evidence exit
10
12
  • .orchestrator/STOP signal → STOP immediately
@@ -79,6 +81,40 @@ def test_bare_promise_without_evidence_continues(tmp_path):
79
81
  assert _iteration(root) == 2
80
82
 
81
83
 
84
+ def _write_anchor(root, criteria):
85
+ loop = os.path.join(root, ".orchestrator", "loop")
86
+ os.makedirs(loop, exist_ok=True)
87
+ with open(os.path.join(loop, "anchor.json"), "w", encoding="utf-8") as f:
88
+ json.dump({"item": "1", "goal": "g", "goal_fp": "x", "criteria": criteria}, f)
89
+
90
+
91
+ def test_promise_with_evidence_but_pending_anchor_continues(tmp_path):
92
+ # The mechanical anti-drift gate: even WITH evidence, a promise must NOT stop the loop while the
93
+ # task anchor still has an unverified acceptance criterion — it re-feeds instead, naming the gap.
94
+ root = str(tmp_path)
95
+ _arm(root, iteration=1, max_iter=5)
96
+ _write_anchor(root, [{"id": "AC1", "status": "done"}, {"id": "AC2", "status": "pending"}])
97
+ r = _tick(root, "Looks done. <promise>SIMPLICIO_DONE</promise> tests pass ✓ "
98
+ "https://github.com/o/r/pull/9")
99
+ assert r.returncode == 0
100
+ assert "followup_message" in r.stdout or "block" in r.stdout, \
101
+ "a promise with an open AC must be ignored, not honored:\n%s" % r.stdout
102
+ assert os.path.exists(_scratchpad(root)), "loop wrongly stopped with an open AC"
103
+ assert "AC2" in r.stdout, "re-feed should name the open acceptance criterion:\n%s" % r.stdout
104
+
105
+
106
+ def test_promise_with_evidence_all_acs_done_stops(tmp_path):
107
+ # Once every anchored AC is verified, the evidence-backed promise stops the loop as before.
108
+ root = str(tmp_path)
109
+ _arm(root, iteration=1, max_iter=5)
110
+ _write_anchor(root, [{"id": "AC1", "status": "done"}, {"id": "AC2", "status": "done"}])
111
+ r = _tick(root, "All green. <promise>SIMPLICIO_DONE</promise> tests pass ✓ "
112
+ "https://github.com/o/r/pull/9")
113
+ assert r.returncode == 0
114
+ assert r.stdout.strip() == "", "expected STOP (every AC verified), got: %s" % r.stdout
115
+ assert not os.path.exists(_scratchpad(root)), "state should be cleaned up on a verified stop"
116
+
117
+
82
118
  def test_no_promise_continues_and_bumps_iteration(tmp_path):
83
119
  root = str(tmp_path)
84
120
  _arm(root, iteration=2, max_iter=5)
@@ -15,6 +15,8 @@ SELFTESTS = [
15
15
  ("scripts/loop_journal.py", "selftest"),
16
16
  ("scripts/billing_aggregator.py", "selftest"),
17
17
  ("scripts/savings_harness.py", "selftest"),
18
+ ("scripts/task_anchor.py", "selftest"),
19
+ ("scripts/pr_evidence.py", "selftest"),
18
20
  ]
19
21
 
20
22
 
@@ -42,6 +44,18 @@ def test_savings_harness_selftest():
42
44
  assert "FAIL" not in r.stdout.upper() or "PASS" in r.stdout.upper(), r.stdout
43
45
 
44
46
 
47
+ def test_task_anchor_selftest():
48
+ r = _run("scripts/task_anchor.py", "selftest")
49
+ assert r.returncode == 0, "task_anchor selftest failed:\n%s%s" % (r.stdout, r.stderr)
50
+ assert "PASS" in r.stdout, r.stdout
51
+
52
+
53
+ def test_pr_evidence_selftest():
54
+ r = _run("scripts/pr_evidence.py", "selftest")
55
+ assert r.returncode == 0, "pr_evidence selftest failed:\n%s%s" % (r.stdout, r.stderr)
56
+ assert "PASS" in r.stdout, r.stdout
57
+
58
+
45
59
  if __name__ == "__main__":
46
60
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
47
61
  from _selfrun import run_module
@@ -97,6 +97,75 @@ def test_repo_conventions_formatters_default():
97
97
  assert c.stdout.strip() == "feat(auth): add SSO", c.stdout
98
98
 
99
99
 
100
+ def _run_anchor(args, env):
101
+ return subprocess.run([sys.executable, os.path.join(REPO, "scripts", "task_anchor.py")] + args,
102
+ capture_output=True, text=True, cwd=REPO, env=env)
103
+
104
+
105
+ def test_task_anchor_gate_and_drift(tmp_path):
106
+ # The anti-deviation guard: a freshly anchored task must BLOCK the done-gate (nothing verified),
107
+ # flag a CHANGED goal as DRIFT, and only go READY once every AC is marked with a receipt.
108
+ env = dict(os.environ, SIMPLICIO_ANCHOR_FILE=str(tmp_path / "anchor.json"))
109
+ s = _run_anchor(["set", "--item", "9", "--goal", "Add SSO login",
110
+ "--ac", "Renders an SSO button", "--ac", "Redirects to the IdP"], env)
111
+ assert s.returncode == 0, s.stdout + s.stderr
112
+
113
+ g = _run_anchor(["gate", "--exit-code"], env)
114
+ assert g.returncode == 12, "expected BLOCKED exit 12, got %d:\n%s" % (g.returncode, g.stdout)
115
+ assert "blocked" in g.stdout.lower(), g.stdout
116
+
117
+ d = _run_anchor(["check", "--goal", "refactor the database layer", "--exit-code"], env)
118
+ assert d.returncode == 11, "expected DRIFT exit 11, got %d:\n%s" % (d.returncode, d.stdout)
119
+ assert "drift" in d.stdout.lower(), d.stdout
120
+
121
+ # marking done WITHOUT a receipt must be refused (no fake "done") and not record progress
122
+ nm = _run_anchor(["mark", "--id", "AC1", "--status", "done"], env)
123
+ assert nm.returncode == 12, nm.stdout
124
+ assert "blocked" in nm.stdout.lower() and "requires --evidence" in nm.stdout.lower(), nm.stdout
125
+ # the refused mark must NOT have advanced the gate
126
+ still = _run_anchor(["gate", "--exit-code"], env)
127
+ assert still.returncode == 12, "refused mark leaked progress:\n%s" % still.stdout
128
+
129
+ _run_anchor(["mark", "--id", "AC1", "--status", "done", "--evidence", "a.png"], env)
130
+ _run_anchor(["mark", "--id", "AC2", "--status", "done", "--evidence", "b.png"], env)
131
+ ok = _run_anchor(["gate", "--exit-code"], env)
132
+ assert ok.returncode == 0, "expected READY, got %d:\n%s" % (ok.returncode, ok.stdout)
133
+ assert "ready" in ok.stdout.lower(), ok.stdout
134
+
135
+
136
+ def test_pr_evidence_blocks_without_evidence(tmp_path):
137
+ # The "PR sem evidência" fix: with --require-evidence and neither a checklist nor a print,
138
+ # building the PR body MUST block (exit 3) and never emit a body / claim done.
139
+ env = dict(os.environ, SIMPLICIO_ANCHOR_FILE=str(tmp_path / "none.json"))
140
+ r = subprocess.run([sys.executable, os.path.join(REPO, "scripts", "pr_evidence.py"), "build",
141
+ "--require-evidence", "--anchor", str(tmp_path / "none.json"),
142
+ "--shots-dir", str(tmp_path / "empty")],
143
+ capture_output=True, text=True, cwd=REPO, env=env)
144
+ assert r.returncode == 3, "expected BLOCKED exit 3, got %d:\n%s" % (r.returncode, r.stdout)
145
+ assert "blocked" in r.stdout.lower(), r.stdout
146
+ assert "# " not in r.stdout, "leaked a PR body while blocked:\n%s" % r.stdout
147
+
148
+
149
+ def test_pr_evidence_builds_with_checklist_and_prints(tmp_path):
150
+ # With an anchor + a captured print, the body MUST contain the item-by-item checklist and embed
151
+ # the screenshot — the two things the client said were missing.
152
+ anchor = str(tmp_path / "anchor.json")
153
+ env = dict(os.environ, SIMPLICIO_ANCHOR_FILE=anchor)
154
+ _run_anchor(["set", "--item", "9", "--goal", "Add SSO login",
155
+ "--ac", "Renders an SSO button"], env)
156
+ _run_anchor(["mark", "--id", "AC1", "--status", "done", "--evidence", "login.png"], env)
157
+ shots = tmp_path / "shots"
158
+ shots.mkdir()
159
+ (shots / "login.png").write_bytes(b"PNG")
160
+ r = subprocess.run([sys.executable, os.path.join(REPO, "scripts", "pr_evidence.py"), "build",
161
+ "--title", "Add SSO login", "--item", "9", "--require-evidence",
162
+ "--anchor", anchor, "--shots-dir", str(shots)],
163
+ capture_output=True, text=True, cwd=REPO, env=env)
164
+ assert r.returncode == 0, r.stdout + r.stderr
165
+ assert "[x] **AC1**" in r.stdout, "missing item-by-item checklist:\n%s" % r.stdout
166
+ assert "login.png" in r.stdout, "missing embedded print:\n%s" % r.stdout
167
+
168
+
100
169
  if __name__ == "__main__":
101
170
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
102
171
  from _selfrun import run_module
File without changes
File without changes