simplicio-loop 3.25.1__tar.gz → 3.31.1__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.25.1/simplicio_loop.egg-info → simplicio_loop-3.31.1}/PKG-INFO +1 -1
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/README.md +20 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/pyproject.toml +1 -1
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/__init__.py +2 -2
- simplicio_loop-3.31.1/simplicio_loop/agent_contract.py +79 -0
- simplicio_loop-3.31.1/simplicio_loop/attempt_journal.py +251 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/cli.py +68 -2
- simplicio_loop-3.31.1/simplicio_loop/driver_contract.py +101 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/evidence.py +6 -2
- simplicio_loop-3.31.1/simplicio_loop/execution_board.py +167 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/oracle.py +32 -0
- simplicio_loop-3.31.1/simplicio_loop/plan_contract.py +95 -0
- simplicio_loop-3.31.1/simplicio_loop/progress.py +227 -0
- simplicio_loop-3.31.1/simplicio_loop/recovery.py +393 -0
- simplicio_loop-3.31.1/simplicio_loop/remote_queue.py +426 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/runner.py +137 -9
- simplicio_loop-3.31.1/simplicio_loop/runtime_adapter.py +235 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1/simplicio_loop.egg-info}/PKG-INFO +1 -1
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop.egg-info/SOURCES.txt +23 -0
- simplicio_loop-3.31.1/tests/test_agent_contract.py +33 -0
- simplicio_loop-3.31.1/tests/test_agent_identity.py +46 -0
- simplicio_loop-3.31.1/tests/test_attempt_journal.py +72 -0
- simplicio_loop-3.31.1/tests/test_cli_oracle.py +48 -0
- simplicio_loop-3.31.1/tests/test_driver_contract.py +34 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_evidence_receipt.py +28 -0
- simplicio_loop-3.31.1/tests/test_execution_board_e2e.py +42 -0
- simplicio_loop-3.31.1/tests/test_independent_watcher.py +66 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_operator_worktree_batch.py +55 -0
- simplicio_loop-3.31.1/tests/test_plan_contract.py +56 -0
- simplicio_loop-3.31.1/tests/test_progress.py +101 -0
- simplicio_loop-3.31.1/tests/test_recovery.py +155 -0
- simplicio_loop-3.31.1/tests/test_remote_queue.py +92 -0
- simplicio_loop-3.31.1/tests/test_remote_queue_live.py +105 -0
- simplicio_loop-3.31.1/tests/test_runtime_adapter.py +115 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_task_backlog.py +40 -0
- simplicio_loop-3.31.1/tests/test_task_to_delivery_golden_corpus.py +32 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/LICENSE +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/MANIFEST.in +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/PYPI.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/setup.cfg +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/hooks/README.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/hooks/action_gate.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/hooks/hooks.claude.json +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/hooks/hooks.json +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/hooks/loop_capture.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/hooks/loop_stop.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/hooks/orient_clamp.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/hooks/orient_rewrite.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/hooks/pre-commit.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/hooks/simplicio_dashboard.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/hooks/simplicio_watch.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/scripts/cross_agent_wiki.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/scripts/hierarchical_planner.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-autoresearch/SKILL.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-compress/SKILL.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-learn/SKILL.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/SKILL.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/agentsview-adapter.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/azure-devops-adapter.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/bound-operators.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/claims-gate.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/cross-agent-wiki.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/cross-repo-integration.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/extension-points.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/hierarchical-planner.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/lmcache-adapter.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/orchestration.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/quality-safety-delivery.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/run-journal-stall-detector.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/spindle-handoff.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/standing-loop-247.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/token-capture.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/token-economy.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/triage-verify-detail.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/understand-anything-adapter.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/video-evidence.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-loop/references/web-evidence.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-orient/SKILL.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-review/SKILL.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-tasks/SKILL.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-tasks/references/agentsview-adapter.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-tasks/references/azure-devops-adapter.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-tasks/references/extension-points.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-tasks/references/lmcache-adapter.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-tasks/references/orchestration.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-tasks/references/quality-safety-delivery.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-tasks/references/standing-loop-247.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-capture.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-economy.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-tasks/references/understand-anything-adapter.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-tasks/references/video-evidence.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/skills/simplicio-tasks/references/web-evidence.md +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/tests/_selfrun.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/tests/test_cross_agent_wiki.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/_bundle/tests/test_loop_e2e.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/delivery.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/drain.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/ops_ledger.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/phase_events.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/source_state.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop/task_contract.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop.egg-info/dependency_links.txt +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop.egg-info/entry_points.txt +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop.egg-info/requires.txt +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/simplicio_loop.egg-info/top_level.txt +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_action_gate.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_agentsview_adapter.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_autoresearch.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_az_boards_adapter.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_check_e2e_demo_contract.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_check_e2e_installed.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_claims_audit.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_claims_manifest_unit.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_clean_env_contract.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_completion_oracle.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_completion_oracle_matrix.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_cross_agent_wiki.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_dashboard_hook.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_delivery_source_identity.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_doctor_smoke.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_drain.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_drain_cli.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_e2e_demo_audit.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_evidence_chain.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_fan_out_flow.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_fan_out_scheduler.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_fan_out_unit.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_flow_audit.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_hbp_topics.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_hierarchical_planner.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_hooks_coverage.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_impact_audit.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_install_lib.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_learn_pipeline_removed.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_ledger_cli.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_locked_append.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_loop_e2e.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_mirror_parity.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_operator_batch.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_ops_ledger.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_phase_events.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_planes_golden_eval.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_precommit_bundle_sync.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_repo_conventions_architecture.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_runner_cli.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_schema_verify_integration.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_schema_verify_unit.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_system_check.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_task_contract_cli.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_task_contract_unit.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_token_budget.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_toon_codec.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_verify_adapters_integration.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_watcher_binding.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_worker_cli_contract.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_worker_selftests.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_worker_smoke.py +0 -0
- {simplicio_loop-3.25.1 → simplicio_loop-3.31.1}/tests/test_worktree_queue.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simplicio-loop
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.31.1
|
|
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
|
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
|
|
56
56
|
- **Contract first** — acceptance criteria, dependencies, risks, source state, and the completion oracle are explicit before execution.
|
|
57
57
|
- **Parallel without corruption** — ready tasks run in isolated lanes/worktrees and converge through an operational ledger.
|
|
58
|
+
- **Automatic fan-out by default** — `batch` provisions one owned worktree per independent,
|
|
59
|
+
authorized task; overlap, missing evidence, or unavailable isolation falls back to a
|
|
60
|
+
visible serial lane. See [`docs/AUTO_FAN_OUT.md`](docs/AUTO_FAN_OUT.md).
|
|
58
61
|
- **Proof before completion** — tests, impact/flow checks, watcher challenges, delivery receipts, and HBP evidence reject false done states.
|
|
59
62
|
- **Memory that changes behavior** — the journal, stall detector, checkpoints, and cross-agent wiki prevent oscillation and make handoffs durable.
|
|
60
63
|
|
|
@@ -124,6 +127,23 @@ company work. `simplicio-runtime` is the unified entrypoint going forward, but i
|
|
|
124
127
|
reuse this loop's evidence-gated converge/drain discipline, durable attempt journal, and worker
|
|
125
128
|
coordination patterns instead of creating a separate task semantics.
|
|
126
129
|
|
|
130
|
+
### 👁️ Progresso visual, honesto e portátil
|
|
131
|
+
|
|
132
|
+
Cada execução pode ser acompanhada por texto, Markdown, JSON ou uma animação ANSI. Todas as
|
|
133
|
+
superfícies consomem o mesmo evento `simplicio.progress/v1`, incluindo ícones de etapa, gates de
|
|
134
|
+
evidence/watcher/oracle, lanes de worktrees e eventos de `worker_claimed` até `delivery_reconciled`.
|
|
135
|
+
|
|
136
|
+
```powershell
|
|
137
|
+
simplicio-loop progress <run-id> --format text --once
|
|
138
|
+
simplicio-loop progress <run-id> --format markdown --once # LLM/chat
|
|
139
|
+
simplicio-loop progress <run-id> --format json --once # dashboard/adapters
|
|
140
|
+
simplicio-loop progress <run-id> --format text --ascii --no-animation # log/PowerShell
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`100%` só aparece com receipt do oracle pronto (`COMPLETE`/`DRAINED`); uma fase `done` sem prova
|
|
144
|
+
fica quase concluída e mostra o blocker. Consulte o contrato completo em
|
|
145
|
+
[`docs/PROGRESS_PROTOCOL.md`](docs/PROGRESS_PROTOCOL.md).
|
|
146
|
+
|
|
127
147
|
---
|
|
128
148
|
|
|
129
149
|
## 🤖 LLM front door
|
|
@@ -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.31.1"
|
|
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"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Identity, context-isolation and receipt bindings for distributed workers.
|
|
2
|
+
|
|
3
|
+
The queue identifies *who* owns a lease; this module defines the smaller
|
|
4
|
+
contract that may cross a device boundary. A context pack contains only the
|
|
5
|
+
assigned work item and allow-listed source references. Prompts, transcripts,
|
|
6
|
+
environment variables and another worker's private state are never copied.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from copy import deepcopy
|
|
11
|
+
from typing import Any, Mapping, Sequence
|
|
12
|
+
|
|
13
|
+
SCHEMA = "simplicio.agent-context/v1"
|
|
14
|
+
RECEIPT_SCHEMA = "simplicio.agent-receipt/v1"
|
|
15
|
+
IDENTITY_FIELDS = ("agent_id", "runtime", "device_id", "session_id")
|
|
16
|
+
CAPABILITIES = frozenset(("claim", "heartbeat", "fencing", "receipts", "events", "evidence", "completion"))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentContractError(ValueError):
|
|
20
|
+
"""Raised when an identity, capability set, or context pack is unsafe."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_identity(identity: Mapping[str, Any], *, capabilities: Sequence[str] | None = None) -> dict[str, Any]:
|
|
24
|
+
if not isinstance(identity, Mapping):
|
|
25
|
+
raise AgentContractError("agent identity must be an object")
|
|
26
|
+
result = {field: str(identity.get(field) or "").strip() for field in IDENTITY_FIELDS}
|
|
27
|
+
missing = [field for field, value in result.items() if not value]
|
|
28
|
+
if missing:
|
|
29
|
+
raise AgentContractError("agent identity missing: " + ", ".join(missing))
|
|
30
|
+
raw_caps = list(capabilities if capabilities is not None else (identity.get("capabilities") or []))
|
|
31
|
+
if any(not isinstance(cap, str) or not cap.strip() for cap in raw_caps):
|
|
32
|
+
raise AgentContractError("capabilities must contain non-empty strings")
|
|
33
|
+
normalized = [cap.strip() for cap in raw_caps]
|
|
34
|
+
if len(set(normalized)) != len(normalized):
|
|
35
|
+
raise AgentContractError("duplicate capabilities are not allowed")
|
|
36
|
+
unknown = sorted(set(normalized).difference(CAPABILITIES))
|
|
37
|
+
if unknown:
|
|
38
|
+
raise AgentContractError("unsupported capabilities: " + ", ".join(unknown))
|
|
39
|
+
result["capabilities"] = normalized
|
|
40
|
+
result["protocol"] = str(identity.get("protocol") or "simplicio-distributed/v1")
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_context_pack(*, task_id: str, goal: str, identity: Mapping[str, Any],
|
|
45
|
+
acs: Sequence[str] = (), source_refs: Sequence[str] = (),
|
|
46
|
+
depends_on: Sequence[str] = (), allowed_paths: Sequence[str] = ()) -> dict[str, Any]:
|
|
47
|
+
"""Build a deterministic, minimum-necessary context for one worker."""
|
|
48
|
+
normalized = validate_identity(identity)
|
|
49
|
+
allow = {str(path).strip() for path in allowed_paths if str(path).strip()}
|
|
50
|
+
refs = [str(path).strip() for path in source_refs if str(path).strip() and str(path).strip() in allow]
|
|
51
|
+
return {
|
|
52
|
+
"schema": SCHEMA,
|
|
53
|
+
"task_id": str(task_id).strip(),
|
|
54
|
+
"goal": str(goal).strip(),
|
|
55
|
+
"acs": [str(ac).strip() for ac in acs if str(ac).strip()],
|
|
56
|
+
"source_refs": refs,
|
|
57
|
+
"depends_on": [str(dep).strip() for dep in depends_on if str(dep).strip()],
|
|
58
|
+
"assigned_to": {field: normalized[field] for field in IDENTITY_FIELDS},
|
|
59
|
+
"capabilities": list(normalized["capabilities"]),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def bind_receipt(receipt: Mapping[str, Any], identity: Mapping[str, Any], *, context_pack: Mapping[str, Any] | None = None) -> dict[str, Any]:
|
|
64
|
+
"""Return a receipt carrying immutable agent identity and isolated context."""
|
|
65
|
+
normalized = validate_identity(identity)
|
|
66
|
+
result = deepcopy(dict(receipt))
|
|
67
|
+
existing = result.get("agent")
|
|
68
|
+
if existing is not None and validate_identity(existing) != normalized:
|
|
69
|
+
raise AgentContractError("receipt agent identity mismatch")
|
|
70
|
+
result["schema"] = result.get("schema") or RECEIPT_SCHEMA
|
|
71
|
+
result["agent"] = normalized
|
|
72
|
+
if context_pack is not None:
|
|
73
|
+
if context_pack.get("assigned_to") != {field: normalized[field] for field in IDENTITY_FIELDS}:
|
|
74
|
+
raise AgentContractError("context pack is assigned to another agent")
|
|
75
|
+
result["context"] = deepcopy(dict(context_pack))
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["AgentContractError", "CAPABILITIES", "IDENTITY_FIELDS", "RECEIPT_SCHEMA", "SCHEMA", "bind_receipt", "build_context_pack", "validate_identity"]
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Typed, append-only attempt observations for Loop/Runtime reconciliation.
|
|
2
|
+
|
|
3
|
+
``loop_journal.py`` is intentionally human-friendly and accepts legacy rows. This
|
|
4
|
+
module is the machine-facing companion: every row has stable identity, causal
|
|
5
|
+
lineage, a hash-chain link and an explicit claim class. It is transport agnostic
|
|
6
|
+
and can therefore be exported to Runtime or replayed after a provider handoff.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional
|
|
16
|
+
|
|
17
|
+
from .ops_ledger import _locks
|
|
18
|
+
|
|
19
|
+
SCHEMA = "simplicio.loop-observation/v1"
|
|
20
|
+
EVENT_KINDS = frozenset((
|
|
21
|
+
"hypothesis", "action", "tool_execution", "validation", "failure",
|
|
22
|
+
"observation", "decision",
|
|
23
|
+
))
|
|
24
|
+
CLAIM_TYPES = frozenset(("MEASURED", "UNVERIFIED", "ESTIMATED"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AttemptJournalError(ValueError):
|
|
28
|
+
"""Raised when journal data cannot be safely appended or replayed."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _canonical(value: Mapping[str, Any]) -> str:
|
|
32
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _hash(value: Mapping[str, Any]) -> str:
|
|
36
|
+
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _text(value: Any, name: str) -> str:
|
|
40
|
+
if not isinstance(value, str) or not value.strip():
|
|
41
|
+
raise AttemptJournalError("%s must be a non-empty string" % name)
|
|
42
|
+
return value.strip()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_observation(*, run_id: str, work_item_id: str, attempt_id: str,
|
|
46
|
+
actor: str, kind: str, payload: Mapping[str, Any],
|
|
47
|
+
sequence: int, event_id: str, causation_id: Optional[str] = None,
|
|
48
|
+
claim_type: str = "UNVERIFIED", ac_ids: Optional[Iterable[str]] = None,
|
|
49
|
+
observed_at: Optional[str] = None) -> Dict[str, Any]:
|
|
50
|
+
"""Build one validated typed observation envelope.
|
|
51
|
+
|
|
52
|
+
``event_id`` is caller-owned so retries can reuse it. Reusing an ID with a
|
|
53
|
+
changed envelope is rejected by :class:`AttemptJournal` rather than silently
|
|
54
|
+
creating a second attempt.
|
|
55
|
+
"""
|
|
56
|
+
if isinstance(sequence, bool) or not isinstance(sequence, int) or sequence < 1:
|
|
57
|
+
raise AttemptJournalError("sequence must be a positive integer")
|
|
58
|
+
kind = _text(kind, "kind")
|
|
59
|
+
if kind not in EVENT_KINDS:
|
|
60
|
+
raise AttemptJournalError("unsupported observation kind: %s" % kind)
|
|
61
|
+
if not isinstance(payload, Mapping):
|
|
62
|
+
raise AttemptJournalError("payload must be an object")
|
|
63
|
+
claim_type = _text(claim_type, "claim_type").upper()
|
|
64
|
+
if claim_type not in CLAIM_TYPES:
|
|
65
|
+
raise AttemptJournalError("unsupported claim_type: %s" % claim_type)
|
|
66
|
+
criteria = list(ac_ids or ())
|
|
67
|
+
if any(not isinstance(item, str) or not item.strip() for item in criteria):
|
|
68
|
+
raise AttemptJournalError("ac_ids must contain non-empty strings")
|
|
69
|
+
event: Dict[str, Any] = {
|
|
70
|
+
"schema": SCHEMA,
|
|
71
|
+
"sequence": sequence,
|
|
72
|
+
"event_id": _text(event_id, "event_id"),
|
|
73
|
+
"run_id": _text(run_id, "run_id"),
|
|
74
|
+
"work_item_id": _text(work_item_id, "work_item_id"),
|
|
75
|
+
"attempt_id": _text(attempt_id, "attempt_id"),
|
|
76
|
+
"actor": _text(actor, "actor"),
|
|
77
|
+
"kind": kind,
|
|
78
|
+
"causation_id": _text(causation_id or event_id, "causation_id"),
|
|
79
|
+
"claim_type": claim_type,
|
|
80
|
+
"ac_ids": criteria,
|
|
81
|
+
"payload": dict(payload),
|
|
82
|
+
}
|
|
83
|
+
if kind == "failure":
|
|
84
|
+
# Providers may supply the loop_journal fingerprint; otherwise derive a
|
|
85
|
+
# provider-independent digest from the failure payload. This is the
|
|
86
|
+
# identity used to decide whether a resumed retry is genuinely new.
|
|
87
|
+
supplied = payload.get("failure_fingerprint", payload.get("fingerprint"))
|
|
88
|
+
event["failure_fingerprint"] = _text(supplied, "failure_fingerprint") if supplied else _hash(payload)[:12]
|
|
89
|
+
if observed_at is not None:
|
|
90
|
+
event["observed_at"] = _text(observed_at, "observed_at")
|
|
91
|
+
return event
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def validate_observation(event: Mapping[str, Any]) -> Dict[str, Any]:
|
|
95
|
+
if not isinstance(event, Mapping) or event.get("schema") != SCHEMA:
|
|
96
|
+
raise AttemptJournalError("unsupported observation schema")
|
|
97
|
+
required = ("event_id", "run_id", "work_item_id", "attempt_id", "actor",
|
|
98
|
+
"kind", "causation_id", "claim_type", "ac_ids", "payload")
|
|
99
|
+
for field in required:
|
|
100
|
+
if field not in event:
|
|
101
|
+
raise AttemptJournalError("observation missing %s" % field)
|
|
102
|
+
checked = build_observation(
|
|
103
|
+
run_id=event["run_id"], work_item_id=event["work_item_id"],
|
|
104
|
+
attempt_id=event["attempt_id"], actor=event["actor"], kind=event["kind"],
|
|
105
|
+
payload=event["payload"], sequence=event["sequence"], event_id=event["event_id"],
|
|
106
|
+
causation_id=event.get("causation_id"), claim_type=event["claim_type"],
|
|
107
|
+
ac_ids=event.get("ac_ids"), observed_at=event.get("observed_at"),
|
|
108
|
+
)
|
|
109
|
+
if event.get("kind") == "failure" and event.get("failure_fingerprint") != checked.get("failure_fingerprint"):
|
|
110
|
+
raise AttemptJournalError("failure fingerprint mismatch")
|
|
111
|
+
if set(event) - set(checked):
|
|
112
|
+
# Forward-compatible fields are allowed, but are included in the hash and
|
|
113
|
+
# therefore cannot be changed during replay.
|
|
114
|
+
checked.update({key: event[key] for key in set(event) - set(checked)})
|
|
115
|
+
return checked
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class AttemptJournal:
|
|
119
|
+
"""Hash-chained JSONL journal with idempotent append and deterministic import."""
|
|
120
|
+
|
|
121
|
+
def __init__(self, path: str | os.PathLike[str]):
|
|
122
|
+
self.path = Path(path)
|
|
123
|
+
self.lock_path = Path(str(self.path) + ".lock")
|
|
124
|
+
|
|
125
|
+
def _lock(self):
|
|
126
|
+
if _locks is None:
|
|
127
|
+
raise AttemptJournalError("cross-process locking helper is unavailable")
|
|
128
|
+
_locks._ensure_lock_file(str(self.lock_path))
|
|
129
|
+
if _locks.fcntl is not None:
|
|
130
|
+
handle = self.lock_path.open("a+b")
|
|
131
|
+
if not _locks._acquire_posix(handle, _locks.DEFAULT_TIMEOUT_MS):
|
|
132
|
+
handle.close()
|
|
133
|
+
raise AttemptJournalError("journal lock acquisition timed out")
|
|
134
|
+
return handle, _locks._release_posix
|
|
135
|
+
if _locks.msvcrt is not None: # pragma: no cover - exercised on Windows CI
|
|
136
|
+
handle = self.lock_path.open("r+b")
|
|
137
|
+
if not _locks._acquire_windows(handle, _locks.DEFAULT_TIMEOUT_MS):
|
|
138
|
+
handle.close()
|
|
139
|
+
raise AttemptJournalError("journal lock acquisition timed out")
|
|
140
|
+
return handle, _locks._release_windows
|
|
141
|
+
raise AttemptJournalError("no cross-process locking primitive is available")
|
|
142
|
+
|
|
143
|
+
def _read(self) -> List[Dict[str, Any]]:
|
|
144
|
+
if not self.path.exists():
|
|
145
|
+
return []
|
|
146
|
+
rows: List[Dict[str, Any]] = []
|
|
147
|
+
for n, line in enumerate(self.path.read_text(encoding="utf-8").splitlines(), 1):
|
|
148
|
+
if not line.strip():
|
|
149
|
+
continue
|
|
150
|
+
try:
|
|
151
|
+
row = json.loads(line)
|
|
152
|
+
except ValueError as exc:
|
|
153
|
+
raise AttemptJournalError("invalid JSON at journal line %d" % n) from exc
|
|
154
|
+
if not isinstance(row, dict):
|
|
155
|
+
raise AttemptJournalError("journal line %d is not an object" % n)
|
|
156
|
+
rows.append(row)
|
|
157
|
+
return rows
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _verify(rows: List[Dict[str, Any]]) -> None:
|
|
161
|
+
previous = ""
|
|
162
|
+
for expected, row in enumerate(rows, 1):
|
|
163
|
+
validate_observation(row)
|
|
164
|
+
if row.get("sequence") != expected or row.get("prev_hash", "") != previous:
|
|
165
|
+
raise AttemptJournalError("journal sequence/hash gap at %s" % row.get("event_id", "?"))
|
|
166
|
+
body = dict(row)
|
|
167
|
+
recorded = body.pop("hash", None)
|
|
168
|
+
if recorded != _hash(body):
|
|
169
|
+
raise AttemptJournalError("journal hash mismatch at %s" % row.get("event_id", "?"))
|
|
170
|
+
previous = recorded
|
|
171
|
+
|
|
172
|
+
def replay(self) -> List[Dict[str, Any]]:
|
|
173
|
+
handle, release = self._lock()
|
|
174
|
+
try:
|
|
175
|
+
rows = self._read()
|
|
176
|
+
self._verify(rows)
|
|
177
|
+
return rows
|
|
178
|
+
finally:
|
|
179
|
+
release(handle)
|
|
180
|
+
handle.close()
|
|
181
|
+
|
|
182
|
+
def append(self, event: Mapping[str, Any]) -> Dict[str, Any]:
|
|
183
|
+
normalized = validate_observation(event)
|
|
184
|
+
handle, release = self._lock()
|
|
185
|
+
try:
|
|
186
|
+
rows = self._read()
|
|
187
|
+
self._verify(rows)
|
|
188
|
+
for prior in rows:
|
|
189
|
+
if prior.get("event_id") == normalized["event_id"]:
|
|
190
|
+
prior_core = {key: value for key, value in prior.items()
|
|
191
|
+
if key not in ("sequence", "prev_hash", "hash")}
|
|
192
|
+
normalized_core = {key: value for key, value in normalized.items()
|
|
193
|
+
if key not in ("sequence", "prev_hash", "hash")}
|
|
194
|
+
if prior_core == normalized_core:
|
|
195
|
+
return prior
|
|
196
|
+
raise AttemptJournalError("event_id already exists with a different envelope")
|
|
197
|
+
body = dict(normalized)
|
|
198
|
+
# Imported envelopes carry their source chain metadata. Recompute
|
|
199
|
+
# local sequence/link/hash instead of hashing the old hash field.
|
|
200
|
+
body.pop("hash", None)
|
|
201
|
+
body.pop("prev_hash", None)
|
|
202
|
+
body["sequence"] = len(rows) + 1
|
|
203
|
+
body["prev_hash"] = rows[-1].get("hash", "") if rows else ""
|
|
204
|
+
body["hash"] = _hash(body)
|
|
205
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
206
|
+
with self.path.open("a", encoding="utf-8", newline="\n") as stream:
|
|
207
|
+
stream.write(json.dumps(body, ensure_ascii=False, sort_keys=True) + "\n")
|
|
208
|
+
stream.flush()
|
|
209
|
+
os.fsync(stream.fileno())
|
|
210
|
+
return body
|
|
211
|
+
finally:
|
|
212
|
+
release(handle)
|
|
213
|
+
handle.close()
|
|
214
|
+
|
|
215
|
+
def import_legacy(self, path: str | os.PathLike[str], *, run_id: str,
|
|
216
|
+
work_item_id: str, attempt_id: str, actor: str) -> List[Dict[str, Any]]:
|
|
217
|
+
"""Migrate human journal rows deterministically; source is never modified."""
|
|
218
|
+
source = Path(path)
|
|
219
|
+
if not source.exists():
|
|
220
|
+
raise AttemptJournalError("legacy journal does not exist")
|
|
221
|
+
imported: List[Dict[str, Any]] = []
|
|
222
|
+
for index, raw in enumerate(source.read_text(encoding="utf-8").splitlines(), 1):
|
|
223
|
+
if not raw.strip():
|
|
224
|
+
continue
|
|
225
|
+
try:
|
|
226
|
+
old = json.loads(raw)
|
|
227
|
+
except ValueError as exc:
|
|
228
|
+
raise AttemptJournalError("invalid legacy JSON at line %d" % index) from exc
|
|
229
|
+
gate = str(old.get("gate", "blocked")).lower()
|
|
230
|
+
kind = "validation" if gate == "pass" else ("failure" if gate == "fail" else "observation")
|
|
231
|
+
event = build_observation(
|
|
232
|
+
run_id=run_id, work_item_id=work_item_id, attempt_id=attempt_id,
|
|
233
|
+
actor=actor, kind=kind, sequence=index, event_id="legacy-%d" % index,
|
|
234
|
+
causation_id="legacy-%d" % index,
|
|
235
|
+
claim_type="MEASURED" if gate == "pass" else "UNVERIFIED",
|
|
236
|
+
payload={"legacy": old},
|
|
237
|
+
)
|
|
238
|
+
imported.append(self.append(event))
|
|
239
|
+
return imported
|
|
240
|
+
|
|
241
|
+
def export(self) -> List[Dict[str, Any]]:
|
|
242
|
+
"""Return the verified canonical envelopes for Runtime ingestion."""
|
|
243
|
+
return self.replay()
|
|
244
|
+
|
|
245
|
+
def import_events(self, events: Iterable[Mapping[str, Any]]) -> List[Dict[str, Any]]:
|
|
246
|
+
"""Validate and idempotently ingest envelopes from Runtime or another host."""
|
|
247
|
+
return [self.append(event) for event in events]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
__all__ = ["AttemptJournal", "AttemptJournalError", "CLAIM_TYPES", "EVENT_KINDS",
|
|
251
|
+
"SCHEMA", "build_observation", "validate_observation"]
|
|
@@ -42,6 +42,8 @@ from .ops_ledger import (
|
|
|
42
42
|
LedgerError,
|
|
43
43
|
validate_handshake,
|
|
44
44
|
)
|
|
45
|
+
from .progress import stream as stream_progress
|
|
46
|
+
from .oracle import evaluate_matrix, persist_completion_receipt
|
|
45
47
|
|
|
46
48
|
BUNDLE = Path(__file__).resolve().parent / "_bundle"
|
|
47
49
|
DASHBOARD = BUNDLE / "hooks" / "simplicio_dashboard.py"
|
|
@@ -208,7 +210,8 @@ def tick(repo: str, run_id: str, task_index: int) -> int:
|
|
|
208
210
|
return 0
|
|
209
211
|
|
|
210
212
|
|
|
211
|
-
def batch(repo: str, run_id: str, task_indices: str, max_workers: int, retry_budget: int
|
|
213
|
+
def batch(repo: str, run_id: str, task_indices: str, max_workers: int, retry_budget: int,
|
|
214
|
+
serial: bool = False) -> int:
|
|
212
215
|
"""Run selected ready tasks through the durable real-operator pool."""
|
|
213
216
|
indices = None
|
|
214
217
|
if task_indices.strip():
|
|
@@ -222,6 +225,7 @@ def batch(repo: str, run_id: str, task_indices: str, max_workers: int, retry_bud
|
|
|
222
225
|
indices,
|
|
223
226
|
max_workers=max_workers or None,
|
|
224
227
|
retry_budget=retry_budget,
|
|
228
|
+
auto_fan_out=not serial,
|
|
225
229
|
)
|
|
226
230
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
227
231
|
return 0
|
|
@@ -233,6 +237,38 @@ def cancel(repo: str, run_id: str) -> int:
|
|
|
233
237
|
return 0
|
|
234
238
|
|
|
235
239
|
|
|
240
|
+
def oracle(loop_dir: str, run_dir: str, response_text: str, flow_gap: str,
|
|
241
|
+
write_receipt: bool) -> int:
|
|
242
|
+
"""Evaluate the shared completion oracle and its cross-runtime parity."""
|
|
243
|
+
payload = evaluate_matrix(loop_dir, run_dir, response_text=response_text, flow_gap=flow_gap)
|
|
244
|
+
if write_receipt:
|
|
245
|
+
# Persist the canonical verdict once; the matrix is only the parity proof.
|
|
246
|
+
from .oracle import evaluate_completion
|
|
247
|
+
verdict = evaluate_completion(loop_dir, run_dir, response_text=response_text,
|
|
248
|
+
flow_gap=flow_gap)
|
|
249
|
+
payload["receipt_path"] = persist_completion_receipt(verdict, loop_dir, run_dir)
|
|
250
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
251
|
+
return 0 if payload.get("parity") and payload.get("signature", [False])[0] else 1
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def progress(repo: str, run_id: str, fmt: str, once: bool, interval: float,
|
|
255
|
+
no_animation: bool = False, ascii_only: bool = False) -> int:
|
|
256
|
+
"""Render a run's portable progress event for an LLM, terminal, or dashboard."""
|
|
257
|
+
try:
|
|
258
|
+
status = read_status(repo, run_id)
|
|
259
|
+
except (FileNotFoundError, OSError, ValueError, KeyError) as exc:
|
|
260
|
+
print(json.dumps({"schema": "simplicio.progress/v1", "status": "UNVERIFIED",
|
|
261
|
+
"reason_code": "run_missing", "error": str(exc)}, ensure_ascii=False))
|
|
262
|
+
return 2
|
|
263
|
+
if not status or not status.get("run_dir"):
|
|
264
|
+
print(json.dumps({"schema": "simplicio.progress/v1", "status": "UNVERIFIED",
|
|
265
|
+
"reason_code": "run_missing"}, ensure_ascii=False))
|
|
266
|
+
return 2
|
|
267
|
+
stream_progress(status["run_dir"], fmt=fmt, once=once, interval=interval,
|
|
268
|
+
no_animation=no_animation, ascii_only=ascii_only)
|
|
269
|
+
return 0
|
|
270
|
+
|
|
271
|
+
|
|
236
272
|
def maintenance_deferred(repo: str, run_id: str, mode: str, disposition: str,
|
|
237
273
|
correction_summary: str, deferral_reason: str,
|
|
238
274
|
resume_instructions: list[str], evidence_status: str) -> int:
|
|
@@ -484,10 +520,30 @@ def main(argv=None) -> int:
|
|
|
484
520
|
p_run.add_argument("--delivery", default="verified", help="requested delivery target")
|
|
485
521
|
p_run.add_argument("--max-iterations", type=int, default=12, help="safety cap")
|
|
486
522
|
|
|
523
|
+
p_oracle = sub.add_parser("oracle", help="evaluate completion and cross-runtime parity")
|
|
524
|
+
p_oracle.add_argument("--loop-dir", default=os.path.join(".orchestrator", "loop"))
|
|
525
|
+
p_oracle.add_argument("--run-dir", default=os.environ.get("SIMPLICIO_RUN_DIR", ""))
|
|
526
|
+
p_oracle.add_argument("--response-text", default="")
|
|
527
|
+
p_oracle.add_argument("--flow-gap", default="")
|
|
528
|
+
p_oracle.add_argument("--write-receipt", action="store_true")
|
|
529
|
+
|
|
487
530
|
p_status = sub.add_parser("status", help="show the latest run state or a specific run")
|
|
488
531
|
p_status.add_argument("--repo", default=".", help="repository root")
|
|
489
532
|
p_status.add_argument("--run-id", default="", help="run id to inspect")
|
|
490
533
|
|
|
534
|
+
p_progress = sub.add_parser("progress", help="render visual progress for a run")
|
|
535
|
+
p_progress.add_argument("--repo", default=".", help="repository root")
|
|
536
|
+
p_progress.add_argument("run_id", help="run id to render")
|
|
537
|
+
p_progress.add_argument("--format", choices=("text", "json", "markdown", "ansi"),
|
|
538
|
+
default="text", dest="fmt", help="output format")
|
|
539
|
+
p_progress.add_argument("--once", action="store_true", help="render one snapshot")
|
|
540
|
+
p_progress.add_argument("--no-animation", action="store_true",
|
|
541
|
+
help="emit one static snapshot without spinner/ANSI control codes")
|
|
542
|
+
p_progress.add_argument("--ascii", action="store_true", dest="ascii_only",
|
|
543
|
+
help="use ASCII icons/bar for terminals without Unicode support")
|
|
544
|
+
p_progress.add_argument("--interval", type=float, default=0.25,
|
|
545
|
+
help="animation polling interval in seconds")
|
|
546
|
+
|
|
491
547
|
p_resume = sub.add_parser("resume", help="resume a non-terminal run")
|
|
492
548
|
p_resume.add_argument("--repo", default=".", help="repository root")
|
|
493
549
|
p_resume.add_argument("run_id", help="run id to resume")
|
|
@@ -512,6 +568,10 @@ def main(argv=None) -> int:
|
|
|
512
568
|
help="maximum live operator workers (default: SIMPLICIO_LOOP_OPERATOR_WORKERS/6)",
|
|
513
569
|
)
|
|
514
570
|
p_batch.add_argument("--retry-budget", type=int, default=3, help="retries after the first attempt")
|
|
571
|
+
p_batch.add_argument(
|
|
572
|
+
"--serial", action="store_true",
|
|
573
|
+
help="disable the default isolated fan-out and force the shared-run serial lane",
|
|
574
|
+
)
|
|
515
575
|
|
|
516
576
|
p_cancel = sub.add_parser("cancel", help="cancel a non-terminal run")
|
|
517
577
|
p_cancel.add_argument("--repo", default=".", help="repository root")
|
|
@@ -611,14 +671,20 @@ def main(argv=None) -> int:
|
|
|
611
671
|
return plan(args.task, args.out)
|
|
612
672
|
if command == "run":
|
|
613
673
|
return run(args.repo, args.task, args.delivery, args.max_iterations)
|
|
674
|
+
if command == "oracle":
|
|
675
|
+
return oracle(args.loop_dir, args.run_dir, args.response_text, args.flow_gap,
|
|
676
|
+
args.write_receipt)
|
|
614
677
|
if command == "status":
|
|
615
678
|
return status(args.repo, args.run_id)
|
|
679
|
+
if command == "progress":
|
|
680
|
+
return progress(args.repo, args.run_id, args.fmt, args.once, args.interval,
|
|
681
|
+
args.no_animation, args.ascii_only)
|
|
616
682
|
if command == "resume":
|
|
617
683
|
return resume(args.repo, args.run_id)
|
|
618
684
|
if command == "tick":
|
|
619
685
|
return tick(args.repo, args.run_id, args.task_index)
|
|
620
686
|
if command == "batch":
|
|
621
|
-
return batch(args.repo, args.run_id, args.task_indices, args.max_workers, args.retry_budget)
|
|
687
|
+
return batch(args.repo, args.run_id, args.task_indices, args.max_workers, args.retry_budget, args.serial)
|
|
622
688
|
if command == "cancel":
|
|
623
689
|
return cancel(args.repo, args.run_id)
|
|
624
690
|
if command in {"maintenance-deferred", "defer-maintenance"}:
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Host-neutral loop driver contract.
|
|
2
|
+
|
|
3
|
+
Hook callbacks and scheduler ticks are transports, not separate state machines. This
|
|
4
|
+
module normalizes either transport into one deterministic decision and canonical event
|
|
5
|
+
stream. It is intentionally stdlib-only so runtimes can import it without the CLI.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any, Dict, Iterable, List, Mapping
|
|
12
|
+
|
|
13
|
+
SCHEMA = "simplicio.loop-driver/v1"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DriverContractError(ValueError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _canon(value: Any) -> str:
|
|
21
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def event_id(event: Mapping[str, Any]) -> str:
|
|
25
|
+
"""Return a stable id when a transport did not provide one."""
|
|
26
|
+
explicit = str(event.get("event_id") or "").strip()
|
|
27
|
+
if explicit:
|
|
28
|
+
return explicit
|
|
29
|
+
seed = {k: event.get(k) for k in ("run_id", "iteration", "phase", "decision", "reason_code")}
|
|
30
|
+
return "evt-" + hashlib.sha256(_canon(seed).encode("utf-8")).hexdigest()[:20]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def normalize_event(raw: Mapping[str, Any], source: str) -> Dict[str, Any]:
|
|
34
|
+
"""Normalize hook/self-paced observations into the same envelope."""
|
|
35
|
+
if not isinstance(raw, Mapping):
|
|
36
|
+
raise DriverContractError("driver event must be an object")
|
|
37
|
+
source = str(source or raw.get("source") or "").strip().lower()
|
|
38
|
+
if source not in {"hook", "self-paced"}:
|
|
39
|
+
raise DriverContractError("source must be hook or self-paced")
|
|
40
|
+
event = {
|
|
41
|
+
"schema": SCHEMA,
|
|
42
|
+
"event_id": event_id(raw),
|
|
43
|
+
"source": source,
|
|
44
|
+
"run_id": str(raw.get("run_id") or "").strip(),
|
|
45
|
+
"iteration": int(raw.get("iteration") or 0),
|
|
46
|
+
"phase": str(raw.get("phase") or "tick").strip(),
|
|
47
|
+
"decision": str(raw.get("decision") or "continue").strip().lower(),
|
|
48
|
+
"reason_code": str(raw.get("reason_code") or "").strip(),
|
|
49
|
+
"gates": dict(raw.get("gates") or {}),
|
|
50
|
+
}
|
|
51
|
+
if event["iteration"] < 0:
|
|
52
|
+
raise DriverContractError("iteration must be non-negative")
|
|
53
|
+
if event["decision"] not in {"continue", "stop", "block"}:
|
|
54
|
+
raise DriverContractError("invalid driver decision")
|
|
55
|
+
return event
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def reconcile_events(events: Iterable[Mapping[str, Any]]) -> List[Dict[str, Any]]:
|
|
59
|
+
"""Deduplicate identical callbacks and reject conflicting duplicate ids."""
|
|
60
|
+
seen: Dict[str, Dict[str, Any]] = {}
|
|
61
|
+
for raw in events:
|
|
62
|
+
source = str(raw.get("source") or "").strip().lower()
|
|
63
|
+
event = normalize_event(raw, source)
|
|
64
|
+
key = event["event_id"]
|
|
65
|
+
prior = seen.get(key)
|
|
66
|
+
if prior is not None:
|
|
67
|
+
# Hook and scheduler may deliver the same logical tick. Transport
|
|
68
|
+
# identity is deliberately excluded from the conflict comparison.
|
|
69
|
+
left = dict(prior); right = dict(event)
|
|
70
|
+
left.pop("source", None); right.pop("source", None)
|
|
71
|
+
if _canon(left) != _canon(right):
|
|
72
|
+
raise DriverContractError("conflicting duplicate event: %s" % key)
|
|
73
|
+
continue
|
|
74
|
+
seen[key] = event
|
|
75
|
+
return sorted(seen.values(), key=lambda item: (item["iteration"], item["event_id"]))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def evaluate_tick(snapshot: Mapping[str, Any]) -> Dict[str, Any]:
|
|
79
|
+
"""Apply shared cap/STOP/evidence gates; absent hook delivery never completes."""
|
|
80
|
+
if not isinstance(snapshot, Mapping):
|
|
81
|
+
raise DriverContractError("snapshot must be an object")
|
|
82
|
+
mode = str(snapshot.get("mode") or "self-paced").strip().lower()
|
|
83
|
+
if mode not in {"hook", "self-paced"}:
|
|
84
|
+
raise DriverContractError("mode must be hook or self-paced")
|
|
85
|
+
iteration = int(snapshot.get("iteration") or 0)
|
|
86
|
+
maximum = int(snapshot.get("max_iterations") or 1)
|
|
87
|
+
if iteration < 0 or maximum < 1:
|
|
88
|
+
raise DriverContractError("invalid iteration cap")
|
|
89
|
+
gates = dict(snapshot.get("gates") or {})
|
|
90
|
+
if snapshot.get("hook_delivered") is False:
|
|
91
|
+
return {"schema": SCHEMA, "decision": "block", "reason_code": "hook_missing", "tag": "UNVERIFIED", "mode": mode}
|
|
92
|
+
if snapshot.get("stop_requested") is True:
|
|
93
|
+
return {"schema": SCHEMA, "decision": "block", "reason_code": "stop_requested", "tag": "UNVERIFIED", "mode": mode}
|
|
94
|
+
if iteration >= maximum:
|
|
95
|
+
return {"schema": SCHEMA, "decision": "block", "reason_code": "iteration_cap", "tag": "UNVERIFIED", "mode": mode}
|
|
96
|
+
if snapshot.get("promise_exact") is True and all(bool(gates.get(name)) for name in ("watcher", "evidence", "oracle")):
|
|
97
|
+
return {"schema": SCHEMA, "decision": "stop", "reason_code": "completion_verified", "tag": "MEASURED", "mode": mode}
|
|
98
|
+
return {"schema": SCHEMA, "decision": "continue", "reason_code": "gates_pending", "tag": "UNVERIFIED", "mode": mode}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
__all__ = ["SCHEMA", "DriverContractError", "event_id", "normalize_event", "reconcile_events", "evaluate_tick"]
|
|
@@ -79,8 +79,12 @@ def _command_policy(check: Dict[str, Any]) -> tuple[List[str] | None, str]:
|
|
|
79
79
|
return None, "no argv resolved"
|
|
80
80
|
exe = Path(normalized[0]).name.lower()
|
|
81
81
|
if exe.endswith(".exe"):
|
|
82
|
-
exe = exe
|
|
83
|
-
|
|
82
|
+
exe = exe[:-4]
|
|
83
|
+
# Test runners and receipts often pass an absolute interpreter path
|
|
84
|
+
# (python3.11, python3.14, ...). Treat versioned Python executables as
|
|
85
|
+
# equivalent to the explicitly allowlisted python/python3 commands.
|
|
86
|
+
python_executable = exe == "python" or exe.startswith("python3.")
|
|
87
|
+
if exe not in SAFE_EXECUTABLES and not python_executable and not normalized[0].lower().endswith(".py"):
|
|
84
88
|
return None, f"command executable not allowlisted: {normalized[0]}"
|
|
85
89
|
return normalized, "allowed"
|
|
86
90
|
|