simplicio-loop 3.23.0__tar.gz → 3.24.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simplicio_loop-3.23.0/simplicio_loop.egg-info → simplicio_loop-3.24.0}/PKG-INFO +3 -3
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/pyproject.toml +3 -3
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/action_gate.py +43 -3
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/loop_stop.py +78 -17
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/simplicio_dashboard.py +19 -1
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/simplicio_watch.py +20 -3
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0/simplicio_loop.egg-info}/PKG-INFO +3 -3
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop.egg-info/SOURCES.txt +2 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop.egg-info/requires.txt +2 -2
- simplicio_loop-3.24.0/tests/test_hbp_topics.py +299 -0
- simplicio_loop-3.24.0/tests/test_locked_append.py +168 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/LICENSE +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/MANIFEST.in +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/PYPI.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/README.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/setup.cfg +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/__init__.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/README.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/hooks.claude.json +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/hooks.json +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/loop_capture.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/orient_clamp.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/orient_rewrite.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/pre-commit.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/scripts/cross_agent_wiki.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/scripts/hierarchical_planner.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-autoresearch/SKILL.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-compress/SKILL.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-learn/SKILL.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/SKILL.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/agentsview-adapter.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/azure-devops-adapter.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/bound-operators.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/claims-gate.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/cross-agent-wiki.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/cross-repo-integration.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/extension-points.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/hierarchical-planner.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/lmcache-adapter.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/orchestration.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/quality-safety-delivery.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/run-journal-stall-detector.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/spindle-handoff.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/standing-loop-247.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/token-capture.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/token-economy.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/triage-verify-detail.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/understand-anything-adapter.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/video-evidence.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-loop/references/web-evidence.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-orient/SKILL.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-review/SKILL.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-tasks/SKILL.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/agentsview-adapter.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/azure-devops-adapter.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/extension-points.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/lmcache-adapter.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/orchestration.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/quality-safety-delivery.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/standing-loop-247.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-capture.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-economy.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/understand-anything-adapter.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/video-evidence.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/web-evidence.md +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/tests/_selfrun.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/tests/test_cross_agent_wiki.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/tests/test_loop_e2e.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/cli.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop.egg-info/dependency_links.txt +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop.egg-info/entry_points.txt +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop.egg-info/top_level.txt +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_action_gate.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_agentsview_adapter.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_autoresearch.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_az_boards_adapter.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_claims_audit.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_claims_manifest_unit.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_cross_agent_wiki.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_dashboard_hook.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_doctor_smoke.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_evidence_chain.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_fan_out_flow.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_fan_out_unit.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_flow_audit.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_hierarchical_planner.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_hooks_coverage.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_impact_audit.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_install_lib.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_learn_pipeline_removed.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_loop_e2e.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_precommit_bundle_sync.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_repo_conventions_architecture.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_schema_verify_integration.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_schema_verify_unit.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_system_check.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_task_backlog.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_token_budget.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_toon_codec.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_verify_adapters_integration.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_worker_cli_contract.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_worker_selftests.py +0 -0
- {simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/tests/test_worker_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simplicio-loop
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.24.0
|
|
4
4
|
Summary: The Universal Looping AI Orchestrator — a runtime-agnostic super-plugin (7 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
|
|
@@ -19,8 +19,8 @@ Classifier: Operating System :: OS Independent
|
|
|
19
19
|
Requires-Python: >=3.8
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
|
-
Requires-Dist: simplicio-mapper>=0.
|
|
23
|
-
Requires-Dist: simplicio-cli>=0.
|
|
22
|
+
Requires-Dist: simplicio-mapper>=0.19.0
|
|
23
|
+
Requires-Dist: simplicio-cli>=0.11.0
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=7; extra == "dev"
|
|
26
26
|
Provides-Extra: ml
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "simplicio-loop"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.24.0"
|
|
8
8
|
description = "The Universal Looping AI Orchestrator — a runtime-agnostic super-plugin (7 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"
|
|
@@ -25,8 +25,8 @@ classifiers = [
|
|
|
25
25
|
# simplicio-mapper -> the repo-survey step (binds `orient`)
|
|
26
26
|
# simplicio-cli -> the operator that applies+verifies changes (binds `execute`/`deterministic_edit`)
|
|
27
27
|
dependencies = [
|
|
28
|
-
"simplicio-mapper>=0.
|
|
29
|
-
"simplicio-cli>=0.
|
|
28
|
+
"simplicio-mapper>=0.19.0",
|
|
29
|
+
"simplicio-cli>=0.11.0",
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
# Optional: the real embedding backend for `simplicio-cli semantic --ml` / `simplicio-cli rag --ml`.
|
|
@@ -235,8 +235,46 @@ def gate_command(cmd, staged=False):
|
|
|
235
235
|
|
|
236
236
|
|
|
237
237
|
# ---------------------------------------------------------------------------------------------
|
|
238
|
-
def
|
|
238
|
+
def _hbp_append_gate_blocked(reason, cmd=""):
|
|
239
|
+
"""Record a gate BLOCK into the runtime's HBP tamper-evident hash chain (#128) — fail-open,
|
|
240
|
+
`simplicio`-only, mirrors `hooks/loop_stop.py`'s `_call_simplicio_hbp_append*` family. A
|
|
241
|
+
blocked mutation is exactly the kind of decision that should be provable later (a later
|
|
242
|
+
`simplicio hbp verify` can show this exact command/reason was recorded in order), not just
|
|
243
|
+
printed to stderr and forgotten. Absent binary or any failure: silent no-op, never blocks
|
|
244
|
+
the gate's own allow/block decision (that decision already happened before this is called).
|
|
245
|
+
"""
|
|
246
|
+
binary = shutil.which("simplicio")
|
|
247
|
+
if not binary:
|
|
248
|
+
return
|
|
249
|
+
try:
|
|
250
|
+
import hashlib
|
|
251
|
+
# minimal payload contract shared by the #128 topics: fingerprint + attempt id.
|
|
252
|
+
fp = hashlib.sha1((reason or "").encode("utf-8", "replace")).hexdigest()[:12]
|
|
253
|
+
attempt = None
|
|
254
|
+
try: # attempt id = live loop iteration from the scratchpad frontmatter, when armed
|
|
255
|
+
with open(os.path.join(".orchestrator", "loop", "scratchpad.md"),
|
|
256
|
+
encoding="utf-8") as f:
|
|
257
|
+
m = re.search(r"^iteration:\s*(\d+)", f.read(), re.M)
|
|
258
|
+
attempt = int(m.group(1)) if m else None
|
|
259
|
+
except Exception:
|
|
260
|
+
attempt = None
|
|
261
|
+
payload = json.dumps({"fingerprint": fp, "attempt": attempt,
|
|
262
|
+
"reason": (reason or "")[:200], "command": (cmd or "")[:200]})
|
|
263
|
+
subprocess.run(
|
|
264
|
+
[binary, "hbp", "append",
|
|
265
|
+
"--topic", "loop-gate-blocked",
|
|
266
|
+
"--payload", payload,
|
|
267
|
+
"--provenance", "simplicio-loop",
|
|
268
|
+
"--json"],
|
|
269
|
+
capture_output=True, timeout=15,
|
|
270
|
+
)
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _emit_and_exit(verdict, pretooluse=False, cmd=""):
|
|
239
276
|
if verdict["action"] == "block":
|
|
277
|
+
_hbp_append_gate_blocked(verdict.get("reason", ""), cmd)
|
|
240
278
|
if pretooluse:
|
|
241
279
|
# Claude PreToolUse: exit 2 blocks the call; reason on stderr is fed back to the model.
|
|
242
280
|
sys.stderr.write("action_gate BLOCK — " + verdict["reason"] + "\n")
|
|
@@ -251,7 +289,7 @@ def _emit_and_exit(verdict, pretooluse=False):
|
|
|
251
289
|
|
|
252
290
|
def cmd_check(opts):
|
|
253
291
|
cmd = opts.get("command", "")
|
|
254
|
-
_emit_and_exit(gate_command(cmd, staged=bool(opts.get("staged"))))
|
|
292
|
+
_emit_and_exit(gate_command(cmd, staged=bool(opts.get("staged"))), cmd=cmd)
|
|
255
293
|
|
|
256
294
|
|
|
257
295
|
def cmd_scan_diff(opts):
|
|
@@ -271,6 +309,8 @@ def cmd_scan_diff(opts):
|
|
|
271
309
|
for label, n in hits:
|
|
272
310
|
print(" secret: %s (line %d)" % (label, n))
|
|
273
311
|
print("block")
|
|
312
|
+
_hbp_append_gate_blocked("secret in diff (%s)" % ", ".join(sorted({h[0] for h in hits})),
|
|
313
|
+
"scan-diff %s" % (src or "-"))
|
|
274
314
|
sys.exit(2)
|
|
275
315
|
print("allow")
|
|
276
316
|
sys.exit(0)
|
|
@@ -292,7 +332,7 @@ def from_pretooluse():
|
|
|
292
332
|
cmd = (data.get("tool_input", {}) or {}).get("command", "")
|
|
293
333
|
if not cmd:
|
|
294
334
|
sys.exit(0)
|
|
295
|
-
_emit_and_exit(gate_command(cmd), pretooluse=True)
|
|
335
|
+
_emit_and_exit(gate_command(cmd), pretooluse=True, cmd=cmd)
|
|
296
336
|
|
|
297
337
|
|
|
298
338
|
def cmd_selftest(_opts):
|
|
@@ -326,9 +326,12 @@ def _changed_files():
|
|
|
326
326
|
commit). A heuristic, not a precise "since loop start" diff — fail-open: {} on any error.
|
|
327
327
|
|
|
328
328
|
Excludes `.orchestrator/` (the loop's own state files — never source, and would otherwise
|
|
329
|
-
make the receipt's own write, or a sibling state write, look like a "later" source change)
|
|
330
|
-
|
|
331
|
-
|
|
329
|
+
make the receipt's own write, or a sibling state write, look like a "later" source change),
|
|
330
|
+
`.simplicio/` (the simplicio runtime/dev-cli's own state — checkpoints, events.jsonl, survey
|
|
331
|
+
artifacts — written by this very hook's fire-and-forget CLI callouts mid-turn, the same
|
|
332
|
+
self-inflicted false-positive class), and build/cache noise (`__pycache__`, `.pyc`) that this
|
|
333
|
+
very check's own module import can create — those false-positives are exactly why all three
|
|
334
|
+
are filtered here.
|
|
332
335
|
"""
|
|
333
336
|
out = set()
|
|
334
337
|
for args in (
|
|
@@ -344,7 +347,8 @@ def _changed_files():
|
|
|
344
347
|
continue
|
|
345
348
|
return {
|
|
346
349
|
f for f in out
|
|
347
|
-
if not f.startswith(".orchestrator/"
|
|
350
|
+
if not f.startswith((".orchestrator/", ".simplicio/"))
|
|
351
|
+
and "__pycache__" not in f and not f.endswith((".pyc", ".pyo"))
|
|
348
352
|
}
|
|
349
353
|
|
|
350
354
|
|
|
@@ -518,6 +522,31 @@ def _call_simplicio_checkpoint(iteration):
|
|
|
518
522
|
pass
|
|
519
523
|
|
|
520
524
|
|
|
525
|
+
def _call_simplicio_hbp_append_topic(topic, payload_dict):
|
|
526
|
+
"""Generic fail-open append into the runtime's HBP tamper-evident hash chain
|
|
527
|
+
(`simplicio hbp append`), when the native `simplicio` binary is on PATH (#128).
|
|
528
|
+
|
|
529
|
+
Same contract as the promise-verified append below: `simplicio`-only, best-effort — absent
|
|
530
|
+
binary or any failure is a silent no-op that never blocks the caller's own decision. Every
|
|
531
|
+
new HBP point in this hook family (stall detected, gate blocked, run blocked) goes through
|
|
532
|
+
here so the topics all carry the same provenance and failure discipline.
|
|
533
|
+
"""
|
|
534
|
+
binary = shutil.which("simplicio")
|
|
535
|
+
if not binary:
|
|
536
|
+
return
|
|
537
|
+
try:
|
|
538
|
+
subprocess.run(
|
|
539
|
+
[binary, "hbp", "append",
|
|
540
|
+
"--topic", topic,
|
|
541
|
+
"--payload", json.dumps(payload_dict),
|
|
542
|
+
"--provenance", "simplicio-loop",
|
|
543
|
+
"--json"],
|
|
544
|
+
capture_output=True, timeout=15,
|
|
545
|
+
)
|
|
546
|
+
except Exception:
|
|
547
|
+
pass
|
|
548
|
+
|
|
549
|
+
|
|
521
550
|
def _call_simplicio_hbp_append(iteration, promise, watcher_tag):
|
|
522
551
|
"""Record the promise-verification decision into the runtime's HBP
|
|
523
552
|
tamper-evident hash chain (`simplicio hbp append`), when the native
|
|
@@ -531,29 +560,44 @@ def _call_simplicio_hbp_append(iteration, promise, watcher_tag):
|
|
|
531
560
|
recorded in order, without trusting this process's own say-so after the
|
|
532
561
|
fact. Absent binary or any failure: silent no-op, never blocks the stop.
|
|
533
562
|
"""
|
|
534
|
-
binary = shutil.which("simplicio")
|
|
535
|
-
if not binary:
|
|
536
|
-
return
|
|
537
563
|
try:
|
|
538
564
|
anchor = read_anchor() or {}
|
|
539
|
-
|
|
565
|
+
_call_simplicio_hbp_append_topic("loop-promise-verified", {
|
|
540
566
|
"iteration": iteration,
|
|
541
567
|
"promise": promise,
|
|
542
568
|
"watcher_tag": watcher_tag,
|
|
543
569
|
"goal_fp": anchor.get("goal_fp", ""),
|
|
544
570
|
})
|
|
545
|
-
subprocess.run(
|
|
546
|
-
[binary, "hbp", "append",
|
|
547
|
-
"--topic", "loop-promise-verified",
|
|
548
|
-
"--payload", payload,
|
|
549
|
-
"--provenance", "simplicio-loop",
|
|
550
|
-
"--json"],
|
|
551
|
-
capture_output=True, timeout=15,
|
|
552
|
-
)
|
|
553
571
|
except Exception:
|
|
554
572
|
pass
|
|
555
573
|
|
|
556
574
|
|
|
575
|
+
def _journal_stall(k=3):
|
|
576
|
+
"""Trailing same-fingerprint failure streak from the journal — (fingerprint, streak).
|
|
577
|
+
|
|
578
|
+
Mirrors `scripts/loop_journal.py`'s `analyze()` trailing-streak math over the tail of the
|
|
579
|
+
journal, locally and fail-open (("", 0) on any error) — the hook must never import the
|
|
580
|
+
worker to decide whether to emit the `loop-stall-detected` HBP record (#128).
|
|
581
|
+
"""
|
|
582
|
+
try:
|
|
583
|
+
rows = tail_journal(n=max(k * 3, 12))
|
|
584
|
+
if not rows:
|
|
585
|
+
return "", 0
|
|
586
|
+
last = rows[-1]
|
|
587
|
+
fp = last.get("fingerprint", "")
|
|
588
|
+
if last.get("gate") == "pass" or not fp:
|
|
589
|
+
return "", 0
|
|
590
|
+
streak = 0
|
|
591
|
+
for r in reversed(rows):
|
|
592
|
+
if r.get("gate") != "pass" and r.get("fingerprint") == fp:
|
|
593
|
+
streak += 1
|
|
594
|
+
else:
|
|
595
|
+
break
|
|
596
|
+
return fp, streak
|
|
597
|
+
except Exception:
|
|
598
|
+
return "", 0
|
|
599
|
+
|
|
600
|
+
|
|
557
601
|
def read_watcher_challenge():
|
|
558
602
|
"""Return the current per-iteration watcher challenge dict, or None. Fail-open."""
|
|
559
603
|
try:
|
|
@@ -781,7 +825,11 @@ def main():
|
|
|
781
825
|
# hand-survey/hand-edit.
|
|
782
826
|
missing_ops = missing_bound_operators()
|
|
783
827
|
if missing_ops:
|
|
784
|
-
|
|
828
|
+
reason = "bound operator missing: %s" % ", ".join(missing_ops)
|
|
829
|
+
_call_simplicio_hbp_append_topic("loop-run-blocked", {
|
|
830
|
+
"fingerprint": _journal_stall()[0], "attempt": iteration, "reason": reason,
|
|
831
|
+
})
|
|
832
|
+
write_handoff(reason, meta, body)
|
|
785
833
|
cleanup_and_stop()
|
|
786
834
|
|
|
787
835
|
stdin = read_stdin_json()
|
|
@@ -792,6 +840,15 @@ def main():
|
|
|
792
840
|
# this turn even if the agent forgot the manual `loop_journal.py record` call.
|
|
793
841
|
auto_record_journal(iteration, has_evidence)
|
|
794
842
|
|
|
843
|
+
# HBP point: stall detected (#128) — when the trailing journal streak crosses the same
|
|
844
|
+
# K=3 threshold the stall detector/planner use, record it into the tamper-evident chain
|
|
845
|
+
# so a later `simplicio hbp verify` can prove WHEN the loop knew it was stuck. Fail-open.
|
|
846
|
+
stall_fp, stall_streak = _journal_stall()
|
|
847
|
+
if stall_streak >= 3:
|
|
848
|
+
_call_simplicio_hbp_append_topic("loop-stall-detected", {
|
|
849
|
+
"fingerprint": stall_fp, "attempt": iteration, "streak": stall_streak,
|
|
850
|
+
})
|
|
851
|
+
|
|
795
852
|
# HRM-style hierarchical planner: re-assess phase on stall or every N iterations.
|
|
796
853
|
# Runs BEFORE the promise gate so the phase context is available.
|
|
797
854
|
_call_hierarchical_planner()
|
|
@@ -824,6 +881,10 @@ def main():
|
|
|
824
881
|
cleanup_and_stop()
|
|
825
882
|
# (4) Iteration cap — incomplete stop, hand off.
|
|
826
883
|
if max_iter > 0 and iteration >= max_iter:
|
|
884
|
+
_call_simplicio_hbp_append_topic("loop-run-blocked", {
|
|
885
|
+
"fingerprint": _journal_stall()[0], "attempt": iteration,
|
|
886
|
+
"reason": "max_iterations cap reached",
|
|
887
|
+
})
|
|
827
888
|
write_handoff("max_iterations cap reached", meta, body)
|
|
828
889
|
cleanup_and_stop()
|
|
829
890
|
# (5) Spindle handoff — latched handoff overrides re-feed.
|
{simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/simplicio_dashboard.py
RENAMED
|
@@ -27,6 +27,15 @@ from pathlib import Path
|
|
|
27
27
|
|
|
28
28
|
HOME = os.path.expanduser("~")
|
|
29
29
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
30
|
+
_SCRIPTS_DIR = str(REPO_ROOT / "scripts")
|
|
31
|
+
if _SCRIPTS_DIR not in sys.path:
|
|
32
|
+
sys.path.insert(0, _SCRIPTS_DIR)
|
|
33
|
+
try:
|
|
34
|
+
# tolerant JSONL reader (#127) — see hooks/simplicio_watch.py for the same rationale: the
|
|
35
|
+
# savings ledger is written by the external `simplicio` runtime, never by this repo.
|
|
36
|
+
from _locked_append import count_jsonl_lines
|
|
37
|
+
except Exception:
|
|
38
|
+
count_jsonl_lines = None
|
|
30
39
|
# Structured savings file written by the capture engine — the primary data source.
|
|
31
40
|
SAVINGS_JSON_CANDIDATES = [
|
|
32
41
|
Path(HOME) / ".simplicio" / "proxy_savings.json",
|
|
@@ -706,8 +715,16 @@ def get_status():
|
|
|
706
715
|
except (IndexError, ValueError):
|
|
707
716
|
pass
|
|
708
717
|
|
|
718
|
+
# Tolerant count (#127): a truncated/illegible line is counted separately, not folded into
|
|
719
|
+
# the event total as if it were real data.
|
|
709
720
|
ledger = REPO_ROOT / ".simplicio" / "ledger" / "savings-events.jsonl"
|
|
710
|
-
|
|
721
|
+
if ledger.exists() and count_jsonl_lines is not None:
|
|
722
|
+
lc, ledger_corrupt = count_jsonl_lines(str(ledger))
|
|
723
|
+
elif ledger.exists():
|
|
724
|
+
lc = sum(1 for _ in ledger.open(errors="replace"))
|
|
725
|
+
ledger_corrupt = 0
|
|
726
|
+
else:
|
|
727
|
+
lc = ledger_corrupt = 0
|
|
711
728
|
|
|
712
729
|
savings_pct = round((tok_saved / max(tok_before, 1)) * 100, 1)
|
|
713
730
|
|
|
@@ -751,6 +768,7 @@ def get_status():
|
|
|
751
768
|
"cache_hit_pct": cache_hit,
|
|
752
769
|
"memories": mem,
|
|
753
770
|
"ledger_events": lc,
|
|
771
|
+
"ledger_corrupt_lines": ledger_corrupt,
|
|
754
772
|
"series": series,
|
|
755
773
|
"models_seen": models_seen[:8],
|
|
756
774
|
"active_model": active_model,
|
{simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/simplicio_watch.py
RENAMED
|
@@ -18,6 +18,17 @@ from pathlib import Path
|
|
|
18
18
|
HOME = os.path.expanduser("~")
|
|
19
19
|
LOGS = os.path.join(HOME, ".simplicio", "logs")
|
|
20
20
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
21
|
+
_SCRIPTS_DIR = str(REPO_ROOT / "scripts")
|
|
22
|
+
if _SCRIPTS_DIR not in sys.path:
|
|
23
|
+
sys.path.insert(0, _SCRIPTS_DIR)
|
|
24
|
+
try:
|
|
25
|
+
# tolerant JSONL reader (#127) — the savings ledger is written by the external `simplicio`
|
|
26
|
+
# runtime, never by this repo; we only ever READ it, so hardening here means counting valid
|
|
27
|
+
# vs. corrupt/truncated lines instead of a naive `sum(1 for _ in file)` that treats a torn
|
|
28
|
+
# line the same as a real event. Fail-open: missing helper -> fall back to the raw count.
|
|
29
|
+
from _locked_append import count_jsonl_lines
|
|
30
|
+
except Exception:
|
|
31
|
+
count_jsonl_lines = None
|
|
21
32
|
# Native engine: invoked cross-platform via this interpreter (no external binary).
|
|
22
33
|
ENGINE_CMD = [sys.executable or "python3", str(REPO_ROOT / "engine" / "simplicio_engine.py")]
|
|
23
34
|
PROXY_PORT = os.environ.get("SIMPLICIO_PROXY_PORT", "8788")
|
|
@@ -50,11 +61,17 @@ def status():
|
|
|
50
61
|
for line in out2.split("\n"):
|
|
51
62
|
if "Total" in line or "Database" in line:
|
|
52
63
|
log(f" {line.strip()}")
|
|
53
|
-
# Savings ledger
|
|
64
|
+
# Savings ledger — tolerant count (#127): a truncated/illegible line is counted, not silently
|
|
65
|
+
# folded into the total as if it were a real event.
|
|
54
66
|
ledger = REPO_ROOT / ".simplicio" / "ledger" / "savings-events.jsonl"
|
|
55
67
|
if ledger.is_file():
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
if count_jsonl_lines is not None:
|
|
69
|
+
valid, corrupt = count_jsonl_lines(str(ledger))
|
|
70
|
+
suffix = f" ({corrupt} corrupt line(s) skipped)" if corrupt else ""
|
|
71
|
+
log(f" 💰 Savings ledger: {valid} events{suffix}")
|
|
72
|
+
else:
|
|
73
|
+
total = sum(1 for _ in ledger.open(errors="replace"))
|
|
74
|
+
log(f" 💰 Savings ledger: {total} events")
|
|
58
75
|
log(f" 🪵 Logs: {LOGS}/proxy.log")
|
|
59
76
|
return 0 if running else 1
|
|
60
77
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simplicio-loop
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.24.0
|
|
4
4
|
Summary: The Universal Looping AI Orchestrator — a runtime-agnostic super-plugin (7 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
|
|
@@ -19,8 +19,8 @@ Classifier: Operating System :: OS Independent
|
|
|
19
19
|
Requires-Python: >=3.8
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
|
-
Requires-Dist: simplicio-mapper>=0.
|
|
23
|
-
Requires-Dist: simplicio-cli>=0.
|
|
22
|
+
Requires-Dist: simplicio-mapper>=0.19.0
|
|
23
|
+
Requires-Dist: simplicio-cli>=0.11.0
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=7; extra == "dev"
|
|
26
26
|
Provides-Extra: ml
|
|
@@ -79,11 +79,13 @@ tests/test_evidence_chain.py
|
|
|
79
79
|
tests/test_fan_out_flow.py
|
|
80
80
|
tests/test_fan_out_unit.py
|
|
81
81
|
tests/test_flow_audit.py
|
|
82
|
+
tests/test_hbp_topics.py
|
|
82
83
|
tests/test_hierarchical_planner.py
|
|
83
84
|
tests/test_hooks_coverage.py
|
|
84
85
|
tests/test_impact_audit.py
|
|
85
86
|
tests/test_install_lib.py
|
|
86
87
|
tests/test_learn_pipeline_removed.py
|
|
88
|
+
tests/test_locked_append.py
|
|
87
89
|
tests/test_loop_e2e.py
|
|
88
90
|
tests/test_precommit_bundle_sync.py
|
|
89
91
|
tests/test_repo_conventions_architecture.py
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""#128: journal consumes dev-cli events + the new HBP evidence-chain points.
|
|
2
|
+
|
|
3
|
+
Two halves, both deterministic:
|
|
4
|
+
|
|
5
|
+
1. `scripts/loop_journal.py` optionally folds the dev-cli's OWN event log
|
|
6
|
+
(`<repo>/.simplicio/events.jsonl`, schema `simplicio.dev-cli-event/v1` — the documented
|
|
7
|
+
contract from simplicio-dev-cli's `observability.emit_event`, read by shape, never imported)
|
|
8
|
+
into the attempt fingerprint + stall detector: a repeated `validation_fail` on the same
|
|
9
|
+
target streaks to STALLED exactly like a repeated journal failure; `edit_applied`/
|
|
10
|
+
`task_complete` reset the streak; a missing/corrupt file changes nothing (fail-open).
|
|
11
|
+
|
|
12
|
+
2. The three new HBP producers append to the runtime's tamper-evident chain via
|
|
13
|
+
`simplicio hbp append` and are FAIL-OPEN like the pre-existing promise-verified append
|
|
14
|
+
(`hooks/loop_stop.py::_call_simplicio_hbp_append`): with a FAKE `simplicio` binary on PATH the
|
|
15
|
+
exact topic + payload go through; with the binary ABSENT the caller degrades silently — never
|
|
16
|
+
a crash, never a changed decision. (Producer discipline per tests/test_evidence_chain.py:
|
|
17
|
+
prove the positive chain with fixtures shaped like the real producer's output, and prove the
|
|
18
|
+
absent-toolchain path degrades rather than fakes.)
|
|
19
|
+
"""
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
27
|
+
sys.path.insert(0, os.path.join(REPO, "scripts"))
|
|
28
|
+
import loop_journal as lj # noqa: E402
|
|
29
|
+
|
|
30
|
+
HOOK = os.path.join(REPO, "hooks", "loop_stop.py")
|
|
31
|
+
GATE = os.path.join(REPO, "hooks", "action_gate.py")
|
|
32
|
+
|
|
33
|
+
SCHEMA = "simplicio.dev-cli-event/v1"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _event(event, target="src/app.py", ts="2026-07-09T00:00:00Z", warnings=None, attempt=1):
|
|
37
|
+
"""A record shaped exactly like observability.emit_event()'s documented on-disk contract."""
|
|
38
|
+
return {"schema": SCHEMA, "ts": ts, "event": event, "level": "info",
|
|
39
|
+
"payload": {"target": target, "attempt": attempt, "warnings": warnings or []}}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _write_events(root, records, extra_lines=()):
|
|
43
|
+
d = os.path.join(root, ".simplicio")
|
|
44
|
+
os.makedirs(d, exist_ok=True)
|
|
45
|
+
with open(os.path.join(d, "events.jsonl"), "w", encoding="utf-8") as f:
|
|
46
|
+
for rec in records:
|
|
47
|
+
f.write(json.dumps(rec) + "\n")
|
|
48
|
+
for ln in extra_lines:
|
|
49
|
+
f.write(ln + "\n")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── half 1: dev-cli events -> stall detector ────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_missing_events_file_changes_nothing(tmp_path):
|
|
56
|
+
events, corrupt = lj.load_dev_cli_events(str(tmp_path))
|
|
57
|
+
assert events == [] and corrupt == 0
|
|
58
|
+
rows = [{"gate": "fail", "fingerprint": "aaa", "ts": "t", "action": "x"}]
|
|
59
|
+
assert lj.merge_dev_cli(rows, events) == rows # identical behavior to today
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_repeated_validation_fail_same_target_streaks_to_stalled(tmp_path):
|
|
63
|
+
root = str(tmp_path)
|
|
64
|
+
_write_events(root, [
|
|
65
|
+
_event("validation_fail", ts="2026-07-09T00:00:0%dZ" % i, warnings=["pytest failed"])
|
|
66
|
+
for i in (1, 2, 3)
|
|
67
|
+
])
|
|
68
|
+
events, corrupt = lj.load_dev_cli_events(root)
|
|
69
|
+
assert len(events) == 3 and corrupt == 0
|
|
70
|
+
verdict = lj.analyze(lj.merge_dev_cli([], events), 3)
|
|
71
|
+
assert verdict["verdict"] == "STALLED", verdict
|
|
72
|
+
assert verdict["stall_count"] == 3
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_different_targets_do_not_streak(tmp_path):
|
|
76
|
+
root = str(tmp_path)
|
|
77
|
+
_write_events(root, [
|
|
78
|
+
_event("validation_fail", target="a.py", ts="2026-07-09T00:00:01Z", warnings=["boom"]),
|
|
79
|
+
_event("validation_fail", target="b.py", ts="2026-07-09T00:00:02Z", warnings=["boom"]),
|
|
80
|
+
_event("validation_fail", target="c.py", ts="2026-07-09T00:00:03Z", warnings=["boom"]),
|
|
81
|
+
])
|
|
82
|
+
events, _ = lj.load_dev_cli_events(root)
|
|
83
|
+
assert lj.analyze(lj.merge_dev_cli([], events), 3)["verdict"] == "PROGRESS"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_task_complete_resets_the_streak(tmp_path):
|
|
87
|
+
root = str(tmp_path)
|
|
88
|
+
_write_events(root, [
|
|
89
|
+
_event("validation_fail", ts="2026-07-09T00:00:01Z", warnings=["boom"]),
|
|
90
|
+
_event("validation_fail", ts="2026-07-09T00:00:02Z", warnings=["boom"]),
|
|
91
|
+
_event("validation_fail", ts="2026-07-09T00:00:03Z", warnings=["boom"]),
|
|
92
|
+
_event("task_complete", ts="2026-07-09T00:00:04Z"),
|
|
93
|
+
])
|
|
94
|
+
events, _ = lj.load_dev_cli_events(root)
|
|
95
|
+
assert lj.analyze(lj.merge_dev_cli([], events), 3)["verdict"] == "PROGRESS"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_events_merge_chronologically_with_journal_rows(tmp_path):
|
|
99
|
+
# A journal fail + two dev-cli fails on the same signature must form ONE trailing streak.
|
|
100
|
+
root = str(tmp_path)
|
|
101
|
+
ev = [_event("validation_fail", ts="2026-07-09T00:00:0%dZ" % i, warnings=["boom"])
|
|
102
|
+
for i in (2, 3)]
|
|
103
|
+
_write_events(root, ev)
|
|
104
|
+
events, _ = lj.load_dev_cli_events(root)
|
|
105
|
+
fp = lj._dev_cli_fingerprint(ev[0])
|
|
106
|
+
rows = [{"gate": "fail", "fingerprint": fp, "ts": "2026-07-09T00:00:01Z", "action": "manual"}]
|
|
107
|
+
verdict = lj.analyze(lj.merge_dev_cli(rows, events), 3)
|
|
108
|
+
assert verdict["verdict"] == "STALLED", verdict
|
|
109
|
+
assert verdict["stall_count"] == 3
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_corrupt_and_foreign_lines_are_tolerated(tmp_path):
|
|
113
|
+
root = str(tmp_path)
|
|
114
|
+
_write_events(root, [_event("validation_fail", warnings=["boom"])],
|
|
115
|
+
extra_lines=['{"torn json', json.dumps({"schema": "other/v9", "event": "x"}),
|
|
116
|
+
json.dumps(["not", "a", "dict"])])
|
|
117
|
+
events, corrupt = lj.load_dev_cli_events(root)
|
|
118
|
+
assert len(events) == 1 # only the real dev-cli event
|
|
119
|
+
assert corrupt == 1 # only the torn line counts as corrupt; foreign schema is skipped
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_stall_cli_consumes_events_via_events_root(tmp_path):
|
|
123
|
+
# End-to-end through the real CLI: copy the worker (+ its sibling imports) into an isolated
|
|
124
|
+
# repo dir, journal EMPTY, only dev-cli events present -> `stall --format json` = STALLED.
|
|
125
|
+
root = str(tmp_path)
|
|
126
|
+
scripts = os.path.join(root, "scripts")
|
|
127
|
+
os.makedirs(scripts)
|
|
128
|
+
for mod in ("loop_journal.py", "toon_codec.py", "_locked_append.py"):
|
|
129
|
+
shutil.copy2(os.path.join(REPO, "scripts", mod), os.path.join(scripts, mod))
|
|
130
|
+
_write_events(root, [
|
|
131
|
+
_event("validation_fail", ts="2026-07-09T00:00:0%dZ" % i, warnings=["pytest failed"])
|
|
132
|
+
for i in (1, 2, 3)
|
|
133
|
+
])
|
|
134
|
+
r = subprocess.run([sys.executable, os.path.join(scripts, "loop_journal.py"),
|
|
135
|
+
"stall", "--format", "json"],
|
|
136
|
+
capture_output=True, text=True, cwd=root)
|
|
137
|
+
assert r.returncode == 0, r.stdout + r.stderr
|
|
138
|
+
verdict = json.loads(r.stdout)
|
|
139
|
+
assert verdict["verdict"] == "STALLED", verdict
|
|
140
|
+
# and --events-root pointing somewhere empty restores today's behavior
|
|
141
|
+
empty = os.path.join(root, "elsewhere")
|
|
142
|
+
os.makedirs(empty)
|
|
143
|
+
r2 = subprocess.run([sys.executable, os.path.join(scripts, "loop_journal.py"),
|
|
144
|
+
"stall", "--format", "json", "--events-root", empty],
|
|
145
|
+
capture_output=True, text=True, cwd=root)
|
|
146
|
+
assert json.loads(r2.stdout)["verdict"] == "PROGRESS", r2.stdout
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── half 2: HBP producers (fake / absent binary) ────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _fake_simplicio(bin_dir, log_path):
|
|
153
|
+
"""A fake `simplicio` binary that appends its argv (one call per line) to log_path."""
|
|
154
|
+
os.makedirs(bin_dir, exist_ok=True)
|
|
155
|
+
if os.name == "nt": # pragma: no cover — POSIX CI
|
|
156
|
+
path = os.path.join(bin_dir, "simplicio.bat")
|
|
157
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
158
|
+
f.write('@echo off\r\necho %* >> "%s"\r\n' % log_path)
|
|
159
|
+
else:
|
|
160
|
+
path = os.path.join(bin_dir, "simplicio")
|
|
161
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
162
|
+
f.write('#!/bin/sh\nprintf \'%s\\n\' "$*" >> "{}"\n'.format(log_path))
|
|
163
|
+
os.chmod(path, 0o755)
|
|
164
|
+
return path
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _calls(log_path):
|
|
168
|
+
if not os.path.exists(log_path):
|
|
169
|
+
return []
|
|
170
|
+
with open(log_path, encoding="utf-8") as f:
|
|
171
|
+
return [ln.rstrip("\n") for ln in f if ln.strip()]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
SCRATCHPAD = """---
|
|
175
|
+
iteration: {iteration}
|
|
176
|
+
max_iterations: {max_iter}
|
|
177
|
+
completion_promise: "SIMPLICIO_DONE"
|
|
178
|
+
evidence_required: true
|
|
179
|
+
started_at: "2026-06-24T00:00:00Z"
|
|
180
|
+
---
|
|
181
|
+
Implement the thing and prove it works.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _arm(root, iteration=1, max_iter=5):
|
|
186
|
+
loop = os.path.join(root, ".orchestrator", "loop")
|
|
187
|
+
os.makedirs(loop, exist_ok=True)
|
|
188
|
+
with open(os.path.join(loop, "scratchpad.md"), "w", encoding="utf-8") as f:
|
|
189
|
+
f.write(SCRATCHPAD.format(iteration=iteration, max_iter=max_iter))
|
|
190
|
+
return loop
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _write_stalled_journal(root, fp="deadbeef0001", n=3):
|
|
194
|
+
loop = os.path.join(root, ".orchestrator", "loop")
|
|
195
|
+
os.makedirs(loop, exist_ok=True)
|
|
196
|
+
with open(os.path.join(loop, "journal.jsonl"), "w", encoding="utf-8") as f:
|
|
197
|
+
for i in range(1, n + 1):
|
|
198
|
+
f.write(json.dumps({"iteration": i, "action": "retry fetch", "gate": "fail",
|
|
199
|
+
"fingerprint": fp, "ts": "2026-07-09T00:00:0%dZ" % i}) + "\n")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _tick(root, text, env):
|
|
203
|
+
return subprocess.run([sys.executable, HOOK], input=json.dumps({"text": text}),
|
|
204
|
+
capture_output=True, text=True, cwd=root, env=env)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_stall_detected_appends_hbp_topic(tmp_path):
|
|
208
|
+
root = str(tmp_path)
|
|
209
|
+
_arm(root, iteration=3, max_iter=10)
|
|
210
|
+
_write_stalled_journal(root)
|
|
211
|
+
log = os.path.join(root, "hbp_calls.log")
|
|
212
|
+
bin_dir = os.path.join(root, "bin")
|
|
213
|
+
_fake_simplicio(bin_dir, log)
|
|
214
|
+
env = dict(os.environ, PATH=bin_dir + os.pathsep + os.environ.get("PATH", ""))
|
|
215
|
+
_tick(root, "still failing, no promise", env)
|
|
216
|
+
calls = _calls(log)
|
|
217
|
+
stall = [c for c in calls if "loop-stall-detected" in c]
|
|
218
|
+
assert stall, "a 3-deep same-fingerprint streak must append loop-stall-detected: %s" % calls
|
|
219
|
+
assert "deadbeef0001" in stall[0], stall[0]
|
|
220
|
+
assert "hbp append" in stall[0]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_run_blocked_on_cap_appends_hbp_topic(tmp_path):
|
|
224
|
+
root = str(tmp_path)
|
|
225
|
+
_arm(root, iteration=5, max_iter=5) # at the cap -> run blocked
|
|
226
|
+
log = os.path.join(root, "hbp_calls.log")
|
|
227
|
+
bin_dir = os.path.join(root, "bin")
|
|
228
|
+
_fake_simplicio(bin_dir, log)
|
|
229
|
+
env = dict(os.environ, PATH=bin_dir + os.pathsep + os.environ.get("PATH", ""))
|
|
230
|
+
r = _tick(root, "still working", env)
|
|
231
|
+
assert r.stdout.strip() == "", "cap must stop the loop:\n%s" % r.stdout
|
|
232
|
+
blocked = [c for c in _calls(log) if "loop-run-blocked" in c]
|
|
233
|
+
assert blocked, "cap-reached stop must append loop-run-blocked: %s" % _calls(log)
|
|
234
|
+
assert "max_iterations cap reached" in blocked[0]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_run_blocked_on_missing_operator_appends_hbp_topic(tmp_path):
|
|
238
|
+
root = str(tmp_path)
|
|
239
|
+
_arm(root, iteration=1, max_iter=9)
|
|
240
|
+
skill_dir = os.path.join(root, ".claude", "skills", "simplicio-loop")
|
|
241
|
+
os.makedirs(skill_dir, exist_ok=True)
|
|
242
|
+
open(os.path.join(skill_dir, "SKILL.md"), "w").close()
|
|
243
|
+
log = os.path.join(root, "hbp_calls.log")
|
|
244
|
+
bin_dir = os.path.join(root, "bin")
|
|
245
|
+
_fake_simplicio(bin_dir, log) # simplicio IS on PATH; mapper/dev-cli are NOT
|
|
246
|
+
env = dict(os.environ, PATH=bin_dir)
|
|
247
|
+
r = _tick(root, "no promise yet", env)
|
|
248
|
+
assert r.stdout.strip() == "", r.stdout
|
|
249
|
+
blocked = [c for c in _calls(log) if "loop-run-blocked" in c]
|
|
250
|
+
assert blocked, "operator-missing block must append loop-run-blocked: %s" % _calls(log)
|
|
251
|
+
assert "bound operator missing" in blocked[0]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_gate_blocked_appends_hbp_topic(tmp_path):
|
|
255
|
+
root = str(tmp_path)
|
|
256
|
+
os.makedirs(os.path.join(root, ".orchestrator"), exist_ok=True) # project-relevance marker
|
|
257
|
+
log = os.path.join(root, "hbp_calls.log")
|
|
258
|
+
bin_dir = os.path.join(root, "bin")
|
|
259
|
+
_fake_simplicio(bin_dir, log)
|
|
260
|
+
env = dict(os.environ, PATH=bin_dir + os.pathsep + os.environ.get("PATH", ""))
|
|
261
|
+
r = subprocess.run([sys.executable, GATE, "check", "--command",
|
|
262
|
+
"git push --force origin main"],
|
|
263
|
+
capture_output=True, text=True, cwd=root, env=env)
|
|
264
|
+
assert r.returncode == 2, "force-push must still be blocked (exit 2)"
|
|
265
|
+
blocked = [c for c in _calls(log) if "loop-gate-blocked" in c]
|
|
266
|
+
assert blocked, "a gate BLOCK must append loop-gate-blocked: %s" % _calls(log)
|
|
267
|
+
assert "fingerprint" in blocked[0]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_all_producers_degrade_silently_without_binary(tmp_path):
|
|
271
|
+
# Absent `simplicio`: every path still reaches its own decision — never a crash, no append.
|
|
272
|
+
root = str(tmp_path)
|
|
273
|
+
empty = os.path.join(root, "empty-bin")
|
|
274
|
+
os.makedirs(empty)
|
|
275
|
+
env = dict(os.environ, PATH=empty)
|
|
276
|
+
|
|
277
|
+
# stall path (journal stalled, no binary): hook still re-feeds normally
|
|
278
|
+
_arm(root, iteration=3, max_iter=10)
|
|
279
|
+
_write_stalled_journal(root)
|
|
280
|
+
r = _tick(root, "still failing, no promise", env)
|
|
281
|
+
assert r.returncode == 0
|
|
282
|
+
assert "followup_message" in r.stdout, "stall append absent-binary must not break the re-feed"
|
|
283
|
+
|
|
284
|
+
# cap path: still a clean stop
|
|
285
|
+
_arm(root, iteration=10, max_iter=10)
|
|
286
|
+
r = _tick(root, "still working", env)
|
|
287
|
+
assert r.returncode == 0 and r.stdout.strip() == ""
|
|
288
|
+
|
|
289
|
+
# gate path: still blocks with exit 2 (PATH needs git for the classify-only path — none used)
|
|
290
|
+
r = subprocess.run([sys.executable, GATE, "check", "--command",
|
|
291
|
+
"git push --force origin main"],
|
|
292
|
+
capture_output=True, text=True, cwd=root, env=env)
|
|
293
|
+
assert r.returncode == 2, "gate must still block without the runtime binary"
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
if __name__ == "__main__":
|
|
297
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
298
|
+
from _selfrun import run_module
|
|
299
|
+
run_module(globals(), "test_hbp_topics")
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""scripts/_locked_append.py — locked JSONL append + tolerant-count helpers (issue #127).
|
|
2
|
+
|
|
3
|
+
Two layers of proof:
|
|
4
|
+
1. Unit-level: the helper appends valid JSON, creates parent dirs, and — critically — FAILS
|
|
5
|
+
OPEN (skips the write, never writes unlocked/partial) when the lock can't be acquired within
|
|
6
|
+
its timeout.
|
|
7
|
+
2. Concurrency: K REAL OS processes (`multiprocessing.Process`, not threads — threads never
|
|
8
|
+
exercise actual cross-process file locking) each append M lines to the SAME journal-shaped
|
|
9
|
+
file at once. Post-condition: exactly K*M lines, every single one valid JSON. This is the
|
|
10
|
+
regression guard for the exact bug class #127 fixes: two writers racing an unlocked
|
|
11
|
+
`open(path, "a").write()` interleaving into a torn/unparseable line.
|
|
12
|
+
"""
|
|
13
|
+
import json
|
|
14
|
+
import multiprocessing
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
20
|
+
sys.path.insert(0, os.path.join(REPO, "scripts"))
|
|
21
|
+
import _locked_append as la # noqa: E402
|
|
22
|
+
|
|
23
|
+
K_WORKERS = 8
|
|
24
|
+
M_LINES = 40
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_appends_valid_json_line(tmp_path):
|
|
28
|
+
path = str(tmp_path / "journal.jsonl")
|
|
29
|
+
assert la.locked_append_line(path, json.dumps({"a": 1})) is True
|
|
30
|
+
assert la.locked_append_line(path, json.dumps({"a": 2})) is True
|
|
31
|
+
with open(path, encoding="utf-8") as f:
|
|
32
|
+
lines = [ln for ln in f if ln.strip()]
|
|
33
|
+
assert len(lines) == 2
|
|
34
|
+
assert json.loads(lines[0]) == {"a": 1}
|
|
35
|
+
assert json.loads(lines[1]) == {"a": 2}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_adds_trailing_newline_when_missing(tmp_path):
|
|
39
|
+
path = str(tmp_path / "j.jsonl")
|
|
40
|
+
la.locked_append_line(path, '{"x":1}') # no trailing \n supplied
|
|
41
|
+
la.locked_append_line(path, '{"x":2}')
|
|
42
|
+
with open(path, encoding="utf-8") as f:
|
|
43
|
+
text = f.read()
|
|
44
|
+
assert text == '{"x":1}\n{"x":2}\n'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_creates_parent_dirs(tmp_path):
|
|
48
|
+
path = str(tmp_path / "nested" / "deep" / "journal.jsonl")
|
|
49
|
+
assert la.locked_append_line(path, json.dumps({"ok": True})) is True
|
|
50
|
+
assert os.path.exists(path)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_flush_and_fsync_survive_before_lock_release(tmp_path):
|
|
54
|
+
# Not directly observable from the outside, but a write that returns True must be durably on
|
|
55
|
+
# disk (readable) immediately after the call returns — this is the contract fsync buys us.
|
|
56
|
+
path = str(tmp_path / "durable.jsonl")
|
|
57
|
+
la.locked_append_line(path, json.dumps({"durable": True}))
|
|
58
|
+
with open(path, encoding="utf-8") as f:
|
|
59
|
+
assert json.loads(f.read().strip()) == {"durable": True}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_timeout_skips_write_never_writes_unlocked(tmp_path):
|
|
63
|
+
"""Fail-open contract: if the lock is already held (by THIS process, a different fd — POSIX
|
|
64
|
+
flock is per-open-file-description, so a second fd contends genuinely), a short-timeout call
|
|
65
|
+
must return False and must NOT append anything — never a partial/unlocked write."""
|
|
66
|
+
if la.fcntl is None:
|
|
67
|
+
return # POSIX-only path exercised here; Windows CI (if any) skips this specific check
|
|
68
|
+
path = str(tmp_path / "contended.jsonl")
|
|
69
|
+
lock_path = la._lock_path(path)
|
|
70
|
+
la._ensure_lock_file(lock_path)
|
|
71
|
+
held_fh = open(lock_path, "a+b")
|
|
72
|
+
la.fcntl.flock(held_fh.fileno(), la.fcntl.LOCK_EX)
|
|
73
|
+
try:
|
|
74
|
+
ok = la.locked_append_line(path, json.dumps({"should": "not-write"}), timeout_ms=80)
|
|
75
|
+
assert ok is False
|
|
76
|
+
assert not os.path.exists(path), "a timed-out lock must never write, not even partially"
|
|
77
|
+
finally:
|
|
78
|
+
la.fcntl.flock(held_fh.fileno(), la.fcntl.LOCK_UN)
|
|
79
|
+
held_fh.close()
|
|
80
|
+
# once the lock is free again, a normal call succeeds
|
|
81
|
+
assert la.locked_append_line(path, json.dumps({"now": "ok"})) is True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_count_jsonl_lines_valid_only(tmp_path):
|
|
85
|
+
path = tmp_path / "clean.jsonl"
|
|
86
|
+
path.write_text('{"a":1}\n{"a":2}\n{"a":3}\n', encoding="utf-8")
|
|
87
|
+
valid, corrupt = la.count_jsonl_lines(str(path))
|
|
88
|
+
assert (valid, corrupt) == (3, 0)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_count_jsonl_lines_counts_corrupt_without_raising(tmp_path):
|
|
92
|
+
path = tmp_path / "dirty.jsonl"
|
|
93
|
+
path.write_text('{"a":1}\n{"a":2\nnot json at all\n{"a":3}\n', encoding="utf-8")
|
|
94
|
+
valid, corrupt = la.count_jsonl_lines(str(path))
|
|
95
|
+
assert valid == 2
|
|
96
|
+
assert corrupt == 2
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_count_jsonl_lines_missing_file_is_zero_zero(tmp_path):
|
|
100
|
+
assert la.count_jsonl_lines(str(tmp_path / "nope.jsonl")) == (0, 0)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_count_jsonl_lines_ignores_blank_lines(tmp_path):
|
|
104
|
+
path = tmp_path / "blanks.jsonl"
|
|
105
|
+
path.write_text('{"a":1}\n\n\n{"a":2}\n', encoding="utf-8")
|
|
106
|
+
assert la.count_jsonl_lines(str(path)) == (2, 0)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ── concurrency (K real processes) ──────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _worker(path, worker_id, m):
|
|
113
|
+
"""Top-level, picklable worker body — runs in its OWN OS process."""
|
|
114
|
+
scripts_dir = os.path.join(REPO, "scripts")
|
|
115
|
+
if scripts_dir not in sys.path:
|
|
116
|
+
sys.path.insert(0, scripts_dir)
|
|
117
|
+
from _locked_append import locked_append_line
|
|
118
|
+
|
|
119
|
+
for i in range(m):
|
|
120
|
+
rec = {
|
|
121
|
+
"worker": worker_id,
|
|
122
|
+
"seq": i,
|
|
123
|
+
"iteration": worker_id * 1000 + i,
|
|
124
|
+
"action": "concurrent-write",
|
|
125
|
+
"gate": "pass",
|
|
126
|
+
"ts": time.time(),
|
|
127
|
+
}
|
|
128
|
+
ok = locked_append_line(path, json.dumps(rec))
|
|
129
|
+
if not ok:
|
|
130
|
+
raise RuntimeError("worker %d: lock acquisition failed on write %d" % (worker_id, i))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_k_processes_m_lines_all_valid_json_no_loss(tmp_path):
|
|
134
|
+
journal = str(tmp_path / "journal.jsonl")
|
|
135
|
+
procs = []
|
|
136
|
+
for w in range(K_WORKERS):
|
|
137
|
+
p = multiprocessing.Process(target=_worker, args=(journal, w, M_LINES))
|
|
138
|
+
p.start()
|
|
139
|
+
procs.append(p)
|
|
140
|
+
for p in procs:
|
|
141
|
+
p.join(timeout=90)
|
|
142
|
+
assert p.exitcode == 0, (
|
|
143
|
+
"worker process %s failed or timed out (exitcode=%s)" % (p.pid, p.exitcode)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
with open(journal, encoding="utf-8") as f:
|
|
147
|
+
lines = [ln for ln in f if ln.strip()]
|
|
148
|
+
|
|
149
|
+
expected = K_WORKERS * M_LINES
|
|
150
|
+
assert len(lines) == expected, (
|
|
151
|
+
"expected %d lines, got %d — a write was lost under concurrency" % (expected, len(lines))
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
seen = set()
|
|
155
|
+
for ln in lines:
|
|
156
|
+
rec = json.loads(ln) # raises ValueError if this line is torn/interleaved — that IS the
|
|
157
|
+
# regression this test guards against (#127): an unlocked writer can corrupt a line.
|
|
158
|
+
seen.add((rec["worker"], rec["seq"]))
|
|
159
|
+
assert len(seen) == expected, "duplicate or garbled records survived under concurrency"
|
|
160
|
+
|
|
161
|
+
valid, corrupt = la.count_jsonl_lines(journal)
|
|
162
|
+
assert (valid, corrupt) == (expected, 0)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
167
|
+
from _selfrun import run_module
|
|
168
|
+
run_module(globals(), "test_locked_append")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/hooks.claude.json
RENAMED
|
File without changes
|
|
File without changes
|
{simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/loop_capture.py
RENAMED
|
File without changes
|
{simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/orient_clamp.py
RENAMED
|
File without changes
|
{simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/hooks/orient_rewrite.py
RENAMED
|
File without changes
|
|
File without changes
|
{simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/scripts/cross_agent_wiki.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop/_bundle/tests/test_loop_e2e.py
RENAMED
|
File without changes
|
|
File without changes
|
{simplicio_loop-3.23.0 → simplicio_loop-3.24.0}/simplicio_loop.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|