loop-engineer 0.9.0__tar.gz → 0.10.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.
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.claude-plugin/plugin.json +1 -1
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/CHANGELOG.md +85 -1
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/PKG-INFO +10 -6
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/README.md +9 -5
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/action.yml +42 -1
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/__main__.py +40 -5
- loop_engineer-0.10.0/loop/chain.py +88 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/contract.py +4 -2
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/events.py +86 -5
- loop_engineer-0.10.0/loop/migrate.py +45 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/reducer.py +18 -2
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/runcontrol.py +9 -1
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/runner.py +32 -28
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/runtime.py +109 -39
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/pyproject.toml +1 -1
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/repo-os-contract.md +272 -9
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/event.schema.json +3 -1
- loop_engineer-0.10.0/scripts/chain_fixtures.py +66 -0
- loop_engineer-0.10.0/scripts/test_adversarial_chain.py +345 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_docs_version.py +12 -2
- loop_engineer-0.10.0/scripts/test_doctor_eventstore.py +420 -0
- loop_engineer-0.10.0/scripts/test_event_chain.py +487 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_cli.py +1 -1
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_cli_status_replay.py +10 -3
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_simulate_zero_writes.py +28 -3
- loop_engineer-0.10.0/scripts/test_migrate_cli.py +128 -0
- loop_engineer-0.9.0/scripts/test_doctor_eventstore.py +0 -166
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.claude-plugin/marketplace.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/dependabot.yml +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/workflows/ci.yml +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/workflows/publish.yml +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.gitignore +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.pre-commit-hooks.yaml +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/CODE_OF_CONDUCT.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/CONTRIBUTING.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/GLOSSARY.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/LICENSE +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/SECURITY.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/ROADMAP-v1.0.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/adr/0001-proof-kernel-and-runtime.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/01-good-first-qw9-trigger-phrases.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/02-good-first-qw10-self-eval-labels.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/03-good-first-emit-metrics-vocabulary.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/04-help-wanted-openhands-recipe.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/05-help-wanted-ruflo-recipe.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/06-help-wanted-gap-reports.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/07-help-wanted-emit-scaffold-runlog-seed.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/demo.cast +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/demo.gif +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/gap-reports/scoreboard.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/gap-reports/superpowers.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/integrations/langgraph.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/integrations/temporal.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/metrics-baseline.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/social-card.png +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/plans/2026-06-20-loop-engineer-v1.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/plans/2026-06-30-loop-engineer-v1.0-roadmap.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/plans/2026-07-03-adoption-slices.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/plans/2026-07-08-v0.8.0-composes-the-field.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/plans/2026-07-09-st5-harness-scoreboard.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-06-20-loop-engineer-design.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-06-30-st1-metrics-baseline.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-06-30-st2-portable-contract-spec.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-06-30-st3-integration-adapters.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-06-30-v04-credibility-enforcement.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-07-03-adoption-slices-design.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-07-08-v0.8.0-composes-the-field-design.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-07-09-st5-harness-scoreboard-design.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/evals/cases/structural.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/evals/rubric.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/agent-os/product/mission.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/agent-os/product/roadmap.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/agent-os/specs/2026-07-09-1030-csv-dedupe/plan.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/agent-os/specs/2026-07-09-1030-csv-dedupe/references.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/agent-os/specs/2026-07-09-1030-csv-dedupe/shape.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/agent-os/specs/2026-07-09-1030-csv-dedupe/standards.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/agent-os/standards/backend/data-imports.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/agent-os/standards/index.yml +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/_bmad/bmm/config.yaml +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/_bmad-output/implementation-artifacts/1-1-deduplicate-csv-rows.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/_bmad-output/implementation-artifacts/epic-1-retro-2026-07-09.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/_bmad-output/implementation-artifacts/sprint-status.yaml +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/_bmad-output/planning-artifacts/epics.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/_bmad-output/planning-artifacts/prd.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/src/import_contacts.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/tests/test_import_contacts.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/1235-analysis.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/1235.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/epic.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/execution-status.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/github-mapping.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/updates/1235/progress.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/updates/1235/stream-A.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/prds/csv-dedupe.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/approvals/.gitkeep +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/artifacts/holdout-run.txt +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/artifacts/holdout-verdict.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/artifacts/verify-T1.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/artifacts/verify-T2-iter1.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/artifacts/verify-T2.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/checkpoints/.gitkeep +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/manifest.yaml +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/memory/.gitkeep +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/repair/iter-002.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/state.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/ADR.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/RUNLOG.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/SPEC.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/TASKS.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/WORKFLOW.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/scripts/run-example +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/scripts/verify-fast +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/scripts/verify-full +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/target/manifest.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/target/measure_coverage.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/target/pricing.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/target/test_holdout.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/target/test_visible.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/terminal_state.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/evidence/invalid/absolute-uri.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/evidence/invalid/bad-sha256-pattern.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/evidence/invalid/missing-required-field.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/evidence/invalid/scheme-uri.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/evidence/valid-evidence.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/artifacts/holdout-verdict.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/artifacts/verify-T1-iter1.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/artifacts/verify-T1.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/manifest.yaml +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/repair/iter-002.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/state.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/terminal_state.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/RUNLOG.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/SPEC.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/TASKS.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/WORKFLOW.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/scripts/run-example +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/scripts/verify-fast +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/scripts/verify-full +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/target/jobs.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/target/manifest.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/target/measure_stability.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/target/test_holdout.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/target/test_visible.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/langgraph-emit/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/langgraph-emit/graph_example.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/naive-loop/.loop/state.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/naive-loop/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/openspec-run/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/openspec-run/openspec/changes/archive/2026-07-09-dedupe-csv-rows/.openspec.yaml +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/openspec-run/openspec/changes/archive/2026-07-09-dedupe-csv-rows/design.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/openspec-run/openspec/changes/archive/2026-07-09-dedupe-csv-rows/proposal.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/openspec-run/openspec/changes/archive/2026-07-09-dedupe-csv-rows/specs/csv-dedupe/spec.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/openspec-run/openspec/changes/archive/2026-07-09-dedupe-csv-rows/tasks.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/openspec-run/openspec/config.yaml +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/openspec-run/openspec/specs/csv-dedupe/spec.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/plans/coverage-repair.plan.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/plans/invalid/cyclic-dependency.plan.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/plans/invalid/missing-goal.plan.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/PRPs/plans/completed/csv-dedupe.plan.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/PRPs/prds/csv-dedupe.prd.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/PRPs/reports/csv-dedupe-report.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/PRPs/reviews/pr-42-cycle-0.verdict.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/PRPs/reviews/pr-42-review.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/prp-loop.run.log +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/prp-loop.state.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ruflo-run/.claude-flow/metrics/swarm-activity.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ruflo-run/.claude-flow/metrics/v3-progress.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ruflo-run/.claude-flow/security/audit-status.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ruflo-run/.hive-mind/sessions/hive-mind-prompt-swarm-2026-07-09.txt +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ruflo-run/.mcp.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ruflo-run/.swarm/memory-export.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ruflo-run/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/.specify/feature.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/.specify/memory/constitution.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/specs/001-csv-dedupe/checklists/requirements.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/specs/001-csv-dedupe/plan.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/specs/001-csv-dedupe/quickstart.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/specs/001-csv-dedupe/research.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/specs/001-csv-dedupe/spec.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/specs/001-csv-dedupe/tasks.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/superpowers-run/.superpowers/sdd/progress.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/superpowers-run/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/superpowers-run/docs/superpowers/plans/2026-07-08-csv-dedupe.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/superpowers-run/docs/superpowers/specs/2026-07-08-csv-dedupe-design.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/config.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/docs/prd.txt +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/reports/task-complexity-report.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/state.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/tasks/task_001.txt +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/tasks/tasks.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/templates/example_prd.txt +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/CLAUDE.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/temporal-certify/README.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/temporal-certify/workflow_example.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/hooks/stop_firewall.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/__init__.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/_resources.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/architect.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/completion.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/emit.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/evidence.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/foreign.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/fsm.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/integrations.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/paths.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/plan.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/scaffold.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/simulate.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/architecture-matrix.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/eval-suite.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/loop-patterns.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/model-routing.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/platform-map.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/prompt-templates.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/safety-and-approvals.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/evidence.schema.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/manifest.schema.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/plan.schema.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/receipt.schema.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/repair-record.schema.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/rollout-record.schema.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/state.schema.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/tasks.schema.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/terminal.schema.json +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/action_scorecard.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/anticheat_scan.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/benchmark_harness.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/holdout_gate.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/inspect_loop.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/metrics.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/rollout_ledger.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/runtime_monitor.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/self_eval.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_action_scorecard.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_adversarial_kernel.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_adversarial_process.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_anticheat_scan.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_benchmark_harness.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_ci_release_mode.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_completion_policy.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_conformance.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_contract_records.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_docs_adoption.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_docs_baseline.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_docs_claims.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_emit.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_eventstore.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_evidence.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_example_runnable.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_flaky_example.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_foreign_inspect.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_fsm.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_holdout_gate.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_inspect_loop.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_integrations.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_langgraph_recipe.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_langgraph_recipe_st3.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_architect_cli.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_contract_core.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_runcontrol_cli.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_simulate_cli.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_manifest_extra_states_schema.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_metrics.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_metrics_cli.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_plan_schema.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_precommit_hook.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_reducer.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_resources.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_rollout_ledger.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_runner_dispatch.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_runner_verifier.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_runtime_monitor.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_scaffold.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_schemas_metrics.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_self_eval.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_state_vocabulary.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_stop_firewall.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_template_roundtrip.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_temporal_recipe.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_validate_frontmatter.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_wheel_selfcontained.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/validate_frontmatter.py +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-architect/SKILL.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-contract/SKILL.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-engineer/SKILL.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-evals/SKILL.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-flywheel/SKILL.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-inspector/SKILL.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-inspector/reference/patterns.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-repair/SKILL.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-run/SKILL.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-runtime-monitor/SKILL.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-runtime-monitor/reference/patterns.md +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/AGENTS.md.tmpl +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/EVALS-rubric.md.tmpl +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/RUNLOG.md.tmpl +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/SPEC.md.tmpl +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/TASKS.json.tmpl +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/WORKFLOW.md.tmpl +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/extract-trace-metrics.sh +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/judge-rubric.sh +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/manifest.yaml.tmpl +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/state.json.tmpl +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/terminal_state.json.tmpl +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/verify-fast.sh +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/verify-full.sh +0 -0
- {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/verify-safety.sh +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop-engineer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Design, launch, verify, repair, and improve agent loops. A Claude-Code-native architect+operator for long-running, verifiable, self-improving agentic-coding systems.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Sollan Systems",
|
|
@@ -14,6 +14,90 @@ All notable changes to `loop-engineer` are documented here.
|
|
|
14
14
|
`WORKFLOW.md` and `README.md` are reworded to describe the mechanism; the 0.3.4
|
|
15
15
|
history is left intact.
|
|
16
16
|
|
|
17
|
+
## 0.10.0 — 2026-07-25
|
|
18
|
+
|
|
19
|
+
**The hash-linked event chain.** Every `loop-engineer/event@1` row now carries
|
|
20
|
+
`prev_event_hash` and `event_hash` — a sha256 over a canonical JSON preimage of
|
|
21
|
+
the event's hashed fields, `prev_event_hash` among them. Canonical form is
|
|
22
|
+
`json.dumps(sort_keys=True, separators=(",", ":"), ensure_ascii=False,
|
|
23
|
+
allow_nan=False)` encoded UTF-8, pinned normatively with conformance vectors in
|
|
24
|
+
`reference/repo-os-contract.md` §16. The digest is computed **inside the store on
|
|
25
|
+
append**, never supplied by a caller, and the deterministic reducer re-verifies
|
|
26
|
+
each link as it folds: a spliced, reordered, or edited row raises
|
|
27
|
+
`ChainBreakError` and stops the fold instead of projecting a plausible state.
|
|
28
|
+
`loop.chain` is stdlib-only and imports no other module of this package, so
|
|
29
|
+
`verify_chain()` re-verifies an exported event stream without the store code
|
|
30
|
+
(#82).
|
|
31
|
+
|
|
32
|
+
**Store generations and `loop migrate`.** A fresh store stamps `PRAGMA
|
|
33
|
+
user_version = 2` and widens the `events` DDL with `event_hash TEXT NOT NULL`.
|
|
34
|
+
`loop migrate` is the only store-upgrade path — explicit, idempotent, and
|
|
35
|
+
non-rewriting: it adds the two columns and stamps `user_version = 2`, but never
|
|
36
|
+
backfills hashes onto existing rows, because the append-only `BEFORE UPDATE`
|
|
37
|
+
trigger forbids it. Pre-migration rows therefore stay an **unchained prefix**
|
|
38
|
+
that doctor reports rather than elides, and the first post-migration append is a
|
|
39
|
+
chain genesis.
|
|
40
|
+
|
|
41
|
+
**Anchored doctor gate.** `loop doctor` gains an `event_store.chain` block
|
|
42
|
+
(`head`, `unchained_prefix`) and a `--expect-chain-head SHA256` flag — also
|
|
43
|
+
accepted by `validate` and `verify` — that fails the gate unless the store's head
|
|
44
|
+
equals an externally remembered anchor. Four new issue codes: `event_chain_broken`
|
|
45
|
+
(a link does not verify), `chain_anchor_mismatch` (the head differs from the
|
|
46
|
+
supplied anchor, or an anchor was supplied with no readable store),
|
|
47
|
+
`missing_event_store` (`events.db` is absent while SQLite sidecars remain — the
|
|
48
|
+
store was deleted), and `chain_columns_missing` (the store still declares
|
|
49
|
+
generation 2 with its chain columns gone — the lazy downgrade). The composite
|
|
50
|
+
action publishes the observed head as a `chain-head` output on every run and
|
|
51
|
+
optionally enforces one through an `expect-chain-head` input.
|
|
52
|
+
|
|
53
|
+
**Integrity boundary.** The chain is **tamper-evident relative to an anchored
|
|
54
|
+
head** — a detection property, not a prevention one, and scoped to the anchor.
|
|
55
|
+
It detects splicing, reordering, an edit that does not recompute every downstream
|
|
56
|
+
digest, and byte corruption of any hashed field; given an anchor it detects *any*
|
|
57
|
+
divergence from the head that anchor names, including tail truncation, which is
|
|
58
|
+
otherwise invisible because deleting trailing events leaves a shorter but
|
|
59
|
+
internally valid chain. It does **not** detect a full in-workspace recompute, a
|
|
60
|
+
chain-column downgrade that also resets `user_version`, deletion of the store when
|
|
61
|
+
no sidecars remain and no anchor is supplied, well-formed lies (nothing in the
|
|
62
|
+
chain judges whether a payload is true), or anything in a never-migrated prefix.
|
|
63
|
+
And the window stays open at the head: "An anchor certifies the log only up to the
|
|
64
|
+
anchored head. Everything appended after the last externally-read anchor —
|
|
65
|
+
including a rewrite of the suffix — is unverified until the next anchor is read
|
|
66
|
+
and remembered outside the workspace. The chain narrows the tampering window; it
|
|
67
|
+
does not close it." `scripts/test_adversarial_chain.py` pins both sides — the
|
|
68
|
+
attacks that are caught and four `PINNED LIMITATION` cases that are not. The full
|
|
69
|
+
boundary, with the anchor's trust assumptions, is normative in
|
|
70
|
+
`reference/repo-os-contract.md` §16.
|
|
71
|
+
|
|
72
|
+
**Stricter reads.** `status`, `replay`, and `doctor` now **reject** a store
|
|
73
|
+
containing any schema-invalid event, raising `invalid_event` instead of silently
|
|
74
|
+
folding past it as they did in 0.9.0. Validation runs before the fold, so a tamper
|
|
75
|
+
that also violates `event@1` surfaces as `invalid_event` rather than
|
|
76
|
+
`event_chain_broken`.
|
|
77
|
+
|
|
78
|
+
**Sidecars resolved.** Read verbs no longer leave `-wal`/`-shm` files beside a
|
|
79
|
+
clean `.loop/events.db` — the 0.9.0 known limitation recorded below. #80 landed
|
|
80
|
+
the first half, opening read-only connections with `immutable=1` when no WAL
|
|
81
|
+
sidecar exists; this release completes it with a two-stage retry so that a lost
|
|
82
|
+
`immutable=1` race against a live writer retries plainly as `mode=ro` before
|
|
83
|
+
anything may be called corruption, the same read path in the runner, and a
|
|
84
|
+
zero-carve-out tripwire proving every read verb leaves a clean store
|
|
85
|
+
byte-identical on both store generations.
|
|
86
|
+
|
|
87
|
+
**Compatibility, both directions.** Pre-0.10.0 *readers* can read a v2 store:
|
|
88
|
+
their explicit ten-column `SELECT` is unaffected by the two added columns.
|
|
89
|
+
Pre-0.10.0 *writers* must not append to a chained store. A fresh v0.10.0 store
|
|
90
|
+
refuses such an append at the database, because `event_hash` is `NOT NULL`; a
|
|
91
|
+
*migrated* store keeps its columns nullable, so the append succeeds and produces a
|
|
92
|
+
permanent, unrepairable `event_chain_broken` — the row cannot be re-linked
|
|
93
|
+
afterwards, since `UPDATE` is trigger-blocked. Pin your `loop-engineer` and action
|
|
94
|
+
version per store.
|
|
95
|
+
|
|
96
|
+
Test baseline: 1021 passed / 16 skipped with the `yaml`+`schemas` extras;
|
|
97
|
+
951 / 86 in structural-fallback mode (PyYAML only). Both measured in a fresh
|
|
98
|
+
worktree; a live checkout reads +2 passed / −2 skipped through two
|
|
99
|
+
checked-when-present tests.
|
|
100
|
+
|
|
17
101
|
## 0.9.0 — 2026-07-17
|
|
18
102
|
|
|
19
103
|
**The event-sourced kernel.** The contract gains a durable runtime substrate:
|
|
@@ -71,7 +155,7 @@ least-privilege workflow permissions (#46, #47).
|
|
|
71
155
|
`-wal`/`-shm` sidecars next to `.loop/events.db` (read-only connections
|
|
72
156
|
recreate them and cannot checkpoint on close). Store content is never
|
|
73
157
|
mutated, but tree-byte-identity checks over a store-backed workspace will
|
|
74
|
-
notice them.
|
|
158
|
+
notice them. *(Resolved in 0.10.0 — see "Sidecars resolved" above.)*
|
|
75
159
|
|
|
76
160
|
Test baseline: 933 passed / 16 skipped with the `yaml`+`schemas` extras;
|
|
77
161
|
864 / 85 in structural-fallback mode (PyYAML only).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loop-engineer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Portable Loop Contract Core: validate and inspect repo-native operating contracts for agent loops.
|
|
5
5
|
Project-URL: Homepage, https://github.com/SollanSystems/loop-engineer
|
|
6
6
|
Project-URL: Repository, https://github.com/SollanSystems/loop-engineer
|
|
@@ -44,7 +44,7 @@ Description-Content-Type: text/markdown
|
|
|
44
44
|
[](https://github.com/SollanSystems/loop-engineer/actions/workflows/ci.yml)
|
|
45
45
|
[](https://www.python.org/downloads/)
|
|
46
46
|
[](LICENSE)
|
|
47
|
-
[](https://github.com/SollanSystems/loop-engineer/tags)
|
|
48
48
|
|
|
49
49
|
Long-running agents commit **false completion**. After context compaction they
|
|
50
50
|
forget what "done" meant, optimize to the visible test, patch in circles, and
|
|
@@ -63,7 +63,11 @@ ships, on disk and runnable today:
|
|
|
63
63
|
- an **event-sourced runtime** — `run`, `status`, `replay`, `simulate`, and
|
|
64
64
|
approve/pause/resume/cancel over an append-only SQLite event log
|
|
65
65
|
(`.loop/events.db`), folded by a deterministic reducer that enforces the same
|
|
66
|
-
completion gate as the writers, with crash-safe single-step resume.
|
|
66
|
+
completion gate as the writers, with crash-safe single-step resume. Events are
|
|
67
|
+
hash-chained; `loop doctor --expect-chain-head` verifies the log against an
|
|
68
|
+
externally anchored head. The chain is tamper-evident **relative to an
|
|
69
|
+
anchor** — an adversary with workspace write access can rewrite an unanchored
|
|
70
|
+
log.
|
|
67
71
|
|
|
68
72
|

|
|
69
73
|
|
|
@@ -367,7 +371,7 @@ present. Recipe:
|
|
|
367
371
|
**CI** — one workflow step validates the contract and publishes a scorecard:
|
|
368
372
|
|
|
369
373
|
```yaml
|
|
370
|
-
- uses: SollanSystems/loop-engineer@v0.
|
|
374
|
+
- uses: SollanSystems/loop-engineer@v0.10.0
|
|
371
375
|
with:
|
|
372
376
|
path: "."
|
|
373
377
|
```
|
|
@@ -460,8 +464,8 @@ doctor` and the contract's own `verify-*` scripts).
|
|
|
460
464
|
|
|
461
465
|
## Status
|
|
462
466
|
|
|
463
|
-
- Version: `0.
|
|
464
|
-
- Release tag: `v0.
|
|
467
|
+
- Version: `0.10.0`
|
|
468
|
+
- Release tag: `v0.10.0` (PyPI publish trigger; plugin tags through 0.6.0 used `loop-engineer--v<version>`)
|
|
465
469
|
- License: MIT
|
|
466
470
|
- Primary interface: Claude Code plugin
|
|
467
471
|
- Portable core: Python CLI + JSON schemas
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/SollanSystems/loop-engineer/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.python.org/downloads/)
|
|
7
7
|
[](LICENSE)
|
|
8
|
-
[](https://github.com/SollanSystems/loop-engineer/tags)
|
|
9
9
|
|
|
10
10
|
Long-running agents commit **false completion**. After context compaction they
|
|
11
11
|
forget what "done" meant, optimize to the visible test, patch in circles, and
|
|
@@ -24,7 +24,11 @@ ships, on disk and runnable today:
|
|
|
24
24
|
- an **event-sourced runtime** — `run`, `status`, `replay`, `simulate`, and
|
|
25
25
|
approve/pause/resume/cancel over an append-only SQLite event log
|
|
26
26
|
(`.loop/events.db`), folded by a deterministic reducer that enforces the same
|
|
27
|
-
completion gate as the writers, with crash-safe single-step resume.
|
|
27
|
+
completion gate as the writers, with crash-safe single-step resume. Events are
|
|
28
|
+
hash-chained; `loop doctor --expect-chain-head` verifies the log against an
|
|
29
|
+
externally anchored head. The chain is tamper-evident **relative to an
|
|
30
|
+
anchor** — an adversary with workspace write access can rewrite an unanchored
|
|
31
|
+
log.
|
|
28
32
|
|
|
29
33
|

|
|
30
34
|
|
|
@@ -328,7 +332,7 @@ present. Recipe:
|
|
|
328
332
|
**CI** — one workflow step validates the contract and publishes a scorecard:
|
|
329
333
|
|
|
330
334
|
```yaml
|
|
331
|
-
- uses: SollanSystems/loop-engineer@v0.
|
|
335
|
+
- uses: SollanSystems/loop-engineer@v0.10.0
|
|
332
336
|
with:
|
|
333
337
|
path: "."
|
|
334
338
|
```
|
|
@@ -421,8 +425,8 @@ doctor` and the contract's own `verify-*` scripts).
|
|
|
421
425
|
|
|
422
426
|
## Status
|
|
423
427
|
|
|
424
|
-
- Version: `0.
|
|
425
|
-
- Release tag: `v0.
|
|
428
|
+
- Version: `0.10.0`
|
|
429
|
+
- Release tag: `v0.10.0` (PyPI publish trigger; plugin tags through 0.6.0 used `loop-engineer--v<version>`)
|
|
426
430
|
- License: MIT
|
|
427
431
|
- Primary interface: Claude Code plugin
|
|
428
432
|
- Portable core: Python CLI + JSON schemas
|
|
@@ -25,6 +25,18 @@ inputs:
|
|
|
25
25
|
description: "Token for the optional PR scorecard comment. Empty skips the comment."
|
|
26
26
|
required: false
|
|
27
27
|
default: ""
|
|
28
|
+
expect-chain-head:
|
|
29
|
+
description: >-
|
|
30
|
+
Fail the gate unless the store's chain head equals this 64-hex value (an
|
|
31
|
+
externally remembered anchor). Empty performs NO cross-run tamper
|
|
32
|
+
detection — the gate then only records the head for a later comparison.
|
|
33
|
+
required: false
|
|
34
|
+
default: ""
|
|
35
|
+
|
|
36
|
+
outputs:
|
|
37
|
+
chain-head:
|
|
38
|
+
description: "Chain head event_hash observed by this gate run ('' when the store has no chained events)."
|
|
39
|
+
value: ${{ steps.chain-head.outputs.chain-head }}
|
|
28
40
|
|
|
29
41
|
runs:
|
|
30
42
|
using: "composite"
|
|
@@ -50,12 +62,17 @@ runs:
|
|
|
50
62
|
shell: bash
|
|
51
63
|
env:
|
|
52
64
|
LOOP_PATH: "${{ inputs.path }}"
|
|
65
|
+
LOOP_EXPECT_HEAD: "${{ inputs.expect-chain-head }}"
|
|
53
66
|
run: |
|
|
54
67
|
# -eo pipefail (GitHub's bash default) makes a doctor failure fail the step
|
|
55
68
|
# despite the tee. Then assert the strict validation path actually ran, so a
|
|
56
69
|
# future packaging regression that drops the extras fails loudly here. The
|
|
57
70
|
# check reads one fixed field from doctor's own JSON — no fragile parsing.
|
|
58
|
-
|
|
71
|
+
if [ -n "$LOOP_EXPECT_HEAD" ]; then
|
|
72
|
+
loop doctor --expect-chain-head "$LOOP_EXPECT_HEAD" "$LOOP_PATH" | tee "${RUNNER_TEMP}/doctor.json"
|
|
73
|
+
else
|
|
74
|
+
loop doctor "$LOOP_PATH" | tee "${RUNNER_TEMP}/doctor.json"
|
|
75
|
+
fi
|
|
59
76
|
python - "${RUNNER_TEMP}/doctor.json" <<'PY'
|
|
60
77
|
import json, sys
|
|
61
78
|
mode = json.load(open(sys.argv[1])).get("validation_mode")
|
|
@@ -65,6 +82,30 @@ runs:
|
|
|
65
82
|
raise SystemExit(1)
|
|
66
83
|
PY
|
|
67
84
|
|
|
85
|
+
- name: chain head (anchor surface)
|
|
86
|
+
id: chain-head
|
|
87
|
+
if: always()
|
|
88
|
+
shell: bash
|
|
89
|
+
run: |
|
|
90
|
+
# Runs even when doctor failed: an anchor MISMATCH is exactly the run whose
|
|
91
|
+
# observed head an operator needs recorded. Absent/empty doctor.json (doctor
|
|
92
|
+
# never got to write one) is a silent no-op, not an error.
|
|
93
|
+
[ -s "${RUNNER_TEMP}/doctor.json" ] || exit 0
|
|
94
|
+
python - "${RUNNER_TEMP}/doctor.json" "$GITHUB_STEP_SUMMARY" "$GITHUB_OUTPUT" <<'PY'
|
|
95
|
+
import json, sys
|
|
96
|
+
try:
|
|
97
|
+
doctor = json.load(open(sys.argv[1]))
|
|
98
|
+
except (OSError, json.JSONDecodeError):
|
|
99
|
+
doctor = {}
|
|
100
|
+
chain = (doctor.get("event_store") or {}).get("chain") or {}
|
|
101
|
+
head = chain.get("head") or {}
|
|
102
|
+
value = head.get("event_hash") or ""
|
|
103
|
+
line = (f"**loop-engineer chain head:** `{value}` (sequence {head.get('sequence')})"
|
|
104
|
+
if value else "**loop-engineer chain head:** none (no chained events)")
|
|
105
|
+
open(sys.argv[2], "a").write(line + "\n")
|
|
106
|
+
open(sys.argv[3], "a").write(f"chain-head={value}\n")
|
|
107
|
+
PY
|
|
108
|
+
|
|
68
109
|
- name: loop inspect (scorecard)
|
|
69
110
|
shell: bash
|
|
70
111
|
env:
|
|
@@ -12,19 +12,20 @@ from .runcontrol import RunControlError
|
|
|
12
12
|
|
|
13
13
|
_PROG = "python3 -m loop"
|
|
14
14
|
|
|
15
|
-
_COMMANDS = ("scaffold", "doctor", "validate", "verify", "inspect", "metrics", "plan-lint", "status", "replay", "simulate", "run", "approve", "pause", "resume", "cancel", "architect")
|
|
15
|
+
_COMMANDS = ("scaffold", "doctor", "validate", "verify", "inspect", "metrics", "plan-lint", "status", "replay", "simulate", "run", "approve", "pause", "resume", "cancel", "migrate", "architect")
|
|
16
16
|
|
|
17
17
|
# Read commands operate on an EXISTING contract dir; scaffold CREATES one, so it
|
|
18
18
|
# is exempt from the "target must exist" guard.
|
|
19
|
-
_READ_COMMANDS = ("doctor", "validate", "verify", "inspect", "metrics", "plan-lint", "status", "replay", "simulate", "run", "approve", "pause", "resume", "cancel")
|
|
19
|
+
_READ_COMMANDS = ("doctor", "validate", "verify", "inspect", "metrics", "plan-lint", "status", "replay", "simulate", "run", "approve", "pause", "resume", "cancel", "migrate")
|
|
20
20
|
|
|
21
|
-
_USAGE = f"usage: {_PROG} <scaffold|doctor|validate|verify|inspect|metrics|plan-lint|status|replay|simulate|run|approve|pause|resume|cancel|architect> <target>"
|
|
21
|
+
_USAGE = f"usage: {_PROG} <scaffold|doctor|validate|verify|inspect|metrics|plan-lint|status|replay|simulate|run|approve|pause|resume|cancel|migrate|architect> <target>"
|
|
22
22
|
|
|
23
23
|
_HELP = f"""{_PROG} — validate, inspect, and measure a portable repo-OS loop contract.
|
|
24
24
|
|
|
25
25
|
{_USAGE}
|
|
26
26
|
{_PROG} metrics [--baseline] <workspace-or-.loop>
|
|
27
|
-
{_PROG} doctor|validate|verify [--mode basic|strict|release]
|
|
27
|
+
{_PROG} doctor|validate|verify [--mode basic|strict|release]
|
|
28
|
+
[--expect-chain-head SHA256] <workspace-or-.loop>
|
|
28
29
|
{_PROG} status [--mode basic|strict|release] <workspace>
|
|
29
30
|
{_PROG} replay [--mode basic|strict|release] <workspace>
|
|
30
31
|
{_PROG} simulate [--mode basic|strict|release] <workspace>
|
|
@@ -33,6 +34,7 @@ _HELP = f"""{_PROG} — validate, inspect, and measure a portable repo-OS loop c
|
|
|
33
34
|
{_PROG} pause --reason REASON [--mode basic|strict|release] <workspace>
|
|
34
35
|
{_PROG} resume [--note NOTE] [--mode basic|strict|release] <workspace>
|
|
35
36
|
{_PROG} cancel [--reason REASON] [--mode basic|strict|release] <workspace>
|
|
37
|
+
{_PROG} migrate <workspace>
|
|
36
38
|
{_PROG} plan-lint [--mode basic|strict|release] <plan-file>
|
|
37
39
|
|
|
38
40
|
commands:
|
|
@@ -57,6 +59,7 @@ commands:
|
|
|
57
59
|
pause Pause a non-terminal run.
|
|
58
60
|
resume Resume a paused run.
|
|
59
61
|
cancel Terminate a non-terminal run as AbortedByHuman.
|
|
62
|
+
migrate Add hash-chain columns to a legacy events.db (explicit, idempotent; the only store-upgrade path).
|
|
60
63
|
architect Not implemented by this CLI: architecture classification and ADR
|
|
61
64
|
authorship require agentic judgment, not deterministic code. See
|
|
62
65
|
the loop-architect skill.
|
|
@@ -69,6 +72,10 @@ options:
|
|
|
69
72
|
--mode {{basic,strict,release}}
|
|
70
73
|
(doctor/validate/verify/plan-lint/status/replay/simulate/run) basic forces structural
|
|
71
74
|
checks; strict/release require jsonschema. Default: auto-detect.
|
|
75
|
+
--expect-chain-head SHA256
|
|
76
|
+
(doctor/validate/verify) fail unless the event store's chain head
|
|
77
|
+
is exactly this 64-character lowercase hex hash. A missing,
|
|
78
|
+
unreadable, unchained, or diverged store fails the gate.
|
|
72
79
|
--baseline (metrics only) write docs/metrics-baseline.json over a gate-backed
|
|
73
80
|
run; exits non-zero and writes nothing otherwise.
|
|
74
81
|
-h, --help Show this help and exit.
|
|
@@ -238,6 +245,26 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
238
245
|
print(_USAGE, file=sys.stderr)
|
|
239
246
|
return 2
|
|
240
247
|
|
|
248
|
+
expect_chain_head = None
|
|
249
|
+
if command in {"doctor", "validate", "verify"}:
|
|
250
|
+
try:
|
|
251
|
+
expect_chain_head, argv = _extract_value_flag(argv, "--expect-chain-head")
|
|
252
|
+
except ValueError as exc:
|
|
253
|
+
print(f"{command}: {exc}", file=sys.stderr)
|
|
254
|
+
print(_USAGE, file=sys.stderr)
|
|
255
|
+
return 2
|
|
256
|
+
if expect_chain_head is not None and re.fullmatch(r"[0-9a-f]{64}", expect_chain_head) is None:
|
|
257
|
+
print(f"{command}: --expect-chain-head must be a 64-character lowercase hex sha256",
|
|
258
|
+
file=sys.stderr)
|
|
259
|
+
return 2
|
|
260
|
+
elif any(a == "--expect-chain-head" or a.startswith("--expect-chain-head=") for a in argv):
|
|
261
|
+
# No generic unknown-flag guard exists for the other commands, and scaffold
|
|
262
|
+
# would otherwise CREATE a directory named after the flag.
|
|
263
|
+
print(f"{command}: --expect-chain-head is only valid for doctor/validate/verify",
|
|
264
|
+
file=sys.stderr)
|
|
265
|
+
print(_USAGE, file=sys.stderr)
|
|
266
|
+
return 2
|
|
267
|
+
|
|
241
268
|
decision = resume_target = reason = note = None
|
|
242
269
|
if command in {"approve", "pause", "resume", "cancel"}:
|
|
243
270
|
try:
|
|
@@ -308,7 +335,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
308
335
|
|
|
309
336
|
if command in {"doctor", "validate", "verify"}:
|
|
310
337
|
try:
|
|
311
|
-
return _print_json(doctor_report(target, mode=mode))
|
|
338
|
+
return _print_json(doctor_report(target, mode=mode, expect_chain_head=expect_chain_head))
|
|
312
339
|
except ValidationModeError as exc:
|
|
313
340
|
print(f"{command}: {exc}", file=sys.stderr)
|
|
314
341
|
return 2
|
|
@@ -320,6 +347,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
320
347
|
print(f"{command}: {exc}", file=sys.stderr)
|
|
321
348
|
return 2
|
|
322
349
|
|
|
350
|
+
if command == "migrate":
|
|
351
|
+
from .migrate import migrate_store
|
|
352
|
+
try:
|
|
353
|
+
return _print_json(migrate_store(target))
|
|
354
|
+
except RuntimeStoreError as exc:
|
|
355
|
+
print(f"migrate: {exc}", file=sys.stderr)
|
|
356
|
+
return 2
|
|
357
|
+
|
|
323
358
|
if command in {"status", "replay", "simulate"}:
|
|
324
359
|
from .runner import RunnerError
|
|
325
360
|
try:
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Pure hash-chain canonicalization and verification for event@1 records.
|
|
2
|
+
|
|
3
|
+
Stdlib-only and import-free of other loop modules: verify_chain() must work over
|
|
4
|
+
any ordered event list (a SQLite read or a JSONL export) so third parties can
|
|
5
|
+
re-verify a chain without this package's store code. Canonical form is
|
|
6
|
+
json.dumps(sort_keys, separators=(",",":"), ensure_ascii=False, allow_nan=False)
|
|
7
|
+
encoded UTF-8 — pinned normatively in reference/repo-os-contract.md #16.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
from typing import Any, Iterable, Mapping
|
|
15
|
+
|
|
16
|
+
_PREIMAGE_FIELDS = (
|
|
17
|
+
"schema", "run_id", "sequence", "event_id", "type", "actor", "ts",
|
|
18
|
+
"causation_id", "correlation_id", "payload", "artifact_hashes",
|
|
19
|
+
"prev_event_hash",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChainHashError(ValueError):
|
|
24
|
+
"""A value cannot be canonically hashed (non-JSON type, non-finite float, lone surrogate)."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def canonical_json(value: Any) -> str:
|
|
28
|
+
try:
|
|
29
|
+
text = json.dumps(value, sort_keys=True, separators=(",", ":"),
|
|
30
|
+
ensure_ascii=False, allow_nan=False)
|
|
31
|
+
except (TypeError, ValueError) as exc:
|
|
32
|
+
raise ChainHashError(f"value is not canonically serializable: {exc}") from exc
|
|
33
|
+
try:
|
|
34
|
+
text.encode("utf-8")
|
|
35
|
+
except UnicodeEncodeError as exc:
|
|
36
|
+
raise ChainHashError(f"value contains a lone surrogate: {exc}") from exc
|
|
37
|
+
return text
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def compute_event_hash(record: Mapping[str, Any]) -> str:
|
|
41
|
+
preimage = {field: record.get(field) for field in _PREIMAGE_FIELDS}
|
|
42
|
+
return hashlib.sha256(canonical_json(preimage).encode("utf-8")).hexdigest()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def link_issue(record: Mapping[str, Any], prev_head: Mapping[str, Any] | None) -> str | None:
|
|
46
|
+
"""One incremental chain check; None means record legally extends prev_head."""
|
|
47
|
+
sequence = record.get("sequence")
|
|
48
|
+
stored = record.get("event_hash")
|
|
49
|
+
if stored is None:
|
|
50
|
+
if prev_head is None:
|
|
51
|
+
return None
|
|
52
|
+
return (f"unchained event after chained prefix at sequence {sequence!r} "
|
|
53
|
+
"(a pre-0.10.0 writer appended to a chained store, or the row was tampered)")
|
|
54
|
+
expected_prev = prev_head["event_hash"] if prev_head is not None else None
|
|
55
|
+
if record.get("prev_event_hash") != expected_prev:
|
|
56
|
+
return f"prev_event_hash mismatch at sequence {sequence!r}"
|
|
57
|
+
try:
|
|
58
|
+
recomputed = compute_event_hash(record)
|
|
59
|
+
except ChainHashError as exc:
|
|
60
|
+
return f"unhashable record at sequence {sequence!r}: {exc}"
|
|
61
|
+
if recomputed != stored:
|
|
62
|
+
return f"event_hash mismatch at sequence {sequence!r}"
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def verify_chain(events: Iterable[Mapping[str, Any]], *, expected_head: str | None = None) -> dict[str, Any]:
|
|
67
|
+
"""Verify a COMPLETE run stream's hash chain (sequence 0 onward); pure, I/O-free."""
|
|
68
|
+
issues: list[str] = []
|
|
69
|
+
unchained_prefix = 0
|
|
70
|
+
chained_events = 0
|
|
71
|
+
head: dict[str, Any] | None = None
|
|
72
|
+
for record in events:
|
|
73
|
+
issue = link_issue(record, head)
|
|
74
|
+
if issue is not None:
|
|
75
|
+
issues.append(issue)
|
|
76
|
+
break
|
|
77
|
+
if record.get("event_hash") is None:
|
|
78
|
+
unchained_prefix += 1
|
|
79
|
+
continue
|
|
80
|
+
chained_events += 1
|
|
81
|
+
head = {"sequence": record.get("sequence"), "event_hash": record["event_hash"]}
|
|
82
|
+
if expected_head is not None and not issues:
|
|
83
|
+
if head is None:
|
|
84
|
+
issues.append("expected chain head, but the stream has no chained events")
|
|
85
|
+
elif head["event_hash"] != expected_head:
|
|
86
|
+
issues.append(f"chain head {head['event_hash']} does not match expected {expected_head}")
|
|
87
|
+
return {"ok": not issues, "issues": issues, "chained_events": chained_events,
|
|
88
|
+
"unchained_prefix": unchained_prefix, "head": head}
|
|
@@ -749,11 +749,13 @@ def validate_contract(target: str | Path, *, mode: str | None = None) -> dict[st
|
|
|
749
749
|
}
|
|
750
750
|
|
|
751
751
|
|
|
752
|
-
def doctor_report(target: str | Path, *, mode: str | None = None
|
|
752
|
+
def doctor_report(target: str | Path, *, mode: str | None = None,
|
|
753
|
+
expect_chain_head: str | None = None) -> dict[str, Any]:
|
|
753
754
|
report = validate_contract(target, mode=mode)
|
|
754
755
|
from .runtime import event_consistency_issues
|
|
755
756
|
|
|
756
|
-
event_store, event_issues = event_consistency_issues(
|
|
757
|
+
event_store, event_issues = event_consistency_issues(
|
|
758
|
+
target, mode=mode, expect_chain_head=expect_chain_head)
|
|
757
759
|
issues = report["issues"] + list(event_issues) if event_issues else report["issues"]
|
|
758
760
|
return {**report, "event_store": event_store, "issues": issues,
|
|
759
761
|
"ok": report["ok"] and not event_issues}
|
|
@@ -10,6 +10,7 @@ from datetime import datetime, timezone
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Mapping, Protocol, Sequence, runtime_checkable
|
|
12
12
|
|
|
13
|
+
from . import chain
|
|
13
14
|
from .contract import ContractIssue, _resolve_requested_mode, _schemas_dir
|
|
14
15
|
from .emit import _ITERATION_OUTCOMES, _RECEIPT_OUTCOMES, _RECEIPT_ROLES
|
|
15
16
|
|
|
@@ -41,6 +42,8 @@ CREATE TABLE IF NOT EXISTS events (
|
|
|
41
42
|
ts TEXT NOT NULL,
|
|
42
43
|
payload TEXT NOT NULL,
|
|
43
44
|
artifact_hashes TEXT NOT NULL,
|
|
45
|
+
prev_event_hash TEXT,
|
|
46
|
+
event_hash TEXT NOT NULL,
|
|
44
47
|
PRIMARY KEY (run_id, sequence)
|
|
45
48
|
)
|
|
46
49
|
"""
|
|
@@ -66,6 +69,14 @@ class SequenceConflictError(ValueError):
|
|
|
66
69
|
"""expected_sequence differed from the atomically assigned next sequence."""
|
|
67
70
|
|
|
68
71
|
|
|
72
|
+
class EventRowDecodeError(ValueError):
|
|
73
|
+
"""A stored payload/artifact_hashes column is not valid JSON."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class EventStoreOperationalError(RuntimeError):
|
|
77
|
+
"""The events table exists but cannot service this operation (schema drift, lock)."""
|
|
78
|
+
|
|
79
|
+
|
|
69
80
|
@runtime_checkable
|
|
70
81
|
class EventStore(Protocol):
|
|
71
82
|
def append(self, run_id: str, event_type: str, payload: Mapping[str, Any], *, actor: str,
|
|
@@ -191,6 +202,11 @@ def _structural_validate_event(data: dict[str, Any]) -> list[str]:
|
|
|
191
202
|
for field in ("causation_id", "correlation_id"):
|
|
192
203
|
if field in data and data[field] is not None and not isinstance(data[field], str):
|
|
193
204
|
issues.append(f"{field} must be a string or null")
|
|
205
|
+
for field in ("prev_event_hash", "event_hash"):
|
|
206
|
+
if field in data and data[field] is not None and (
|
|
207
|
+
not isinstance(data[field], str)
|
|
208
|
+
or re.search(r"^[0-9a-f]{64}$", data[field]) is None):
|
|
209
|
+
issues.append(f"{field} must be null or a 64-character lowercase hex sha256")
|
|
194
210
|
hashes = data.get("artifact_hashes", [])
|
|
195
211
|
if not isinstance(hashes, list):
|
|
196
212
|
issues.append("artifact_hashes must be an array")
|
|
@@ -234,6 +250,45 @@ def validate_event(data: dict[str, Any], *, mode: str | None = None) -> dict[str
|
|
|
234
250
|
return _validate_event_dict(data, mode=mode)
|
|
235
251
|
|
|
236
252
|
|
|
253
|
+
def has_chain_columns(conn: sqlite3.Connection) -> bool:
|
|
254
|
+
return any(row[1] == "event_hash" for row in conn.execute("PRAGMA table_info(events)"))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def store_user_version(conn: sqlite3.Connection) -> int:
|
|
258
|
+
return int(conn.execute("PRAGMA user_version").fetchone()[0])
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
_BASE_COLUMNS = ("run_id", "sequence", "event_id", "type", "actor", "causation_id",
|
|
262
|
+
"correlation_id", "ts", "payload", "artifact_hashes")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def read_event_rows(conn: sqlite3.Connection, run_id: str, *,
|
|
266
|
+
since_sequence: int | None = None) -> list[dict[str, Any]]:
|
|
267
|
+
"""The single event-row projection shared by store, runtime, and runner reads.
|
|
268
|
+
|
|
269
|
+
Records always carry prev_event_hash/event_hash keys; legacy stores project
|
|
270
|
+
None. Owns the JSON-decode translation every call site used to repeat.
|
|
271
|
+
"""
|
|
272
|
+
chained = has_chain_columns(conn)
|
|
273
|
+
columns = _BASE_COLUMNS + (("prev_event_hash", "event_hash") if chained else ())
|
|
274
|
+
operator, cursor = (">=", 0) if since_sequence is None else (">", since_sequence)
|
|
275
|
+
rows = conn.execute(
|
|
276
|
+
f"SELECT {', '.join(columns)} FROM events WHERE run_id = ? AND sequence {operator} ? "
|
|
277
|
+
"ORDER BY sequence ASC", (run_id, cursor)).fetchall()
|
|
278
|
+
records: list[dict[str, Any]] = []
|
|
279
|
+
try:
|
|
280
|
+
for row in rows:
|
|
281
|
+
records.append({
|
|
282
|
+
"schema": EVENT_SCHEMA_ID, "run_id": row[0], "sequence": row[1], "event_id": row[2],
|
|
283
|
+
"type": row[3], "actor": row[4], "causation_id": row[5], "correlation_id": row[6],
|
|
284
|
+
"ts": row[7], "payload": json.loads(row[8]), "artifact_hashes": json.loads(row[9]),
|
|
285
|
+
"prev_event_hash": row[10] if chained else None,
|
|
286
|
+
"event_hash": row[11] if chained else None})
|
|
287
|
+
except (TypeError, json.JSONDecodeError) as exc:
|
|
288
|
+
raise EventRowDecodeError(f"event row is not decodable: {exc}") from exc
|
|
289
|
+
return records
|
|
290
|
+
|
|
291
|
+
|
|
237
292
|
class SQLiteEventStore:
|
|
238
293
|
"""A transactional SQLite/WAL event store with DB-enforced append-only rows."""
|
|
239
294
|
|
|
@@ -245,9 +300,13 @@ class SQLiteEventStore:
|
|
|
245
300
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
246
301
|
conn.execute("PRAGMA synchronous=FULL")
|
|
247
302
|
conn.execute("PRAGMA busy_timeout=5000")
|
|
303
|
+
fresh = conn.execute(
|
|
304
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='events'").fetchone() is None
|
|
248
305
|
conn.execute(_CREATE_EVENTS_TABLE)
|
|
249
306
|
conn.execute(_CREATE_NO_UPDATE_TRIGGER)
|
|
250
307
|
conn.execute(_CREATE_NO_DELETE_TRIGGER)
|
|
308
|
+
if fresh:
|
|
309
|
+
conn.execute("PRAGMA user_version = 2")
|
|
251
310
|
return conn
|
|
252
311
|
|
|
253
312
|
def append(self, run_id: str, event_type: str, payload: Mapping[str, Any], *, actor: str,
|
|
@@ -275,15 +334,39 @@ class SQLiteEventStore:
|
|
|
275
334
|
conn.execute("ROLLBACK")
|
|
276
335
|
raise SequenceConflictError(f"expected next sequence {next_sequence} for run_id {run_id!r}, caller supplied {expected_sequence}")
|
|
277
336
|
record["sequence"] = next_sequence
|
|
337
|
+
chained = has_chain_columns(conn)
|
|
338
|
+
if chained:
|
|
339
|
+
prev_row = conn.execute(
|
|
340
|
+
"SELECT event_hash FROM events WHERE run_id = ? AND sequence = ?",
|
|
341
|
+
(run_id, next_sequence - 1)).fetchone() if next_sequence else None
|
|
342
|
+
record["prev_event_hash"] = prev_row[0] if prev_row else None
|
|
343
|
+
try:
|
|
344
|
+
record["event_hash"] = chain.compute_event_hash(record)
|
|
345
|
+
except chain.ChainHashError as exc:
|
|
346
|
+
conn.execute("ROLLBACK")
|
|
347
|
+
raise EventValidationError(str(exc)) from exc
|
|
348
|
+
else:
|
|
349
|
+
record["prev_event_hash"] = None
|
|
350
|
+
record["event_hash"] = None
|
|
278
351
|
payload_json = json.dumps(record["payload"], sort_keys=True)
|
|
279
352
|
hashes_json = json.dumps([dict(item) for item in record["artifact_hashes"]], sort_keys=True)
|
|
280
353
|
try:
|
|
281
|
-
|
|
282
|
-
|
|
354
|
+
if chained:
|
|
355
|
+
conn.execute("INSERT INTO events (run_id, sequence, event_id, type, actor, causation_id, correlation_id, ts, payload, artifact_hashes, prev_event_hash, event_hash) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
356
|
+
(record["run_id"], record["sequence"], record["event_id"], record["type"], record["actor"], record["causation_id"], record["correlation_id"], record["ts"], payload_json, hashes_json, record["prev_event_hash"], record["event_hash"]))
|
|
357
|
+
else:
|
|
358
|
+
conn.execute("INSERT INTO events (run_id, sequence, event_id, type, actor, causation_id, correlation_id, ts, payload, artifact_hashes) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
|
359
|
+
(record["run_id"], record["sequence"], record["event_id"], record["type"], record["actor"], record["causation_id"], record["correlation_id"], record["ts"], payload_json, hashes_json))
|
|
283
360
|
except sqlite3.IntegrityError as exc:
|
|
284
361
|
conn.execute("ROLLBACK")
|
|
285
362
|
raise DuplicateEventError(record["event_id"]) from exc
|
|
286
363
|
conn.execute("COMMIT")
|
|
364
|
+
except sqlite3.OperationalError as exc:
|
|
365
|
+
try:
|
|
366
|
+
conn.execute("ROLLBACK")
|
|
367
|
+
except sqlite3.Error:
|
|
368
|
+
pass
|
|
369
|
+
raise EventStoreOperationalError(f"event store cannot accept appends: {exc}") from exc
|
|
287
370
|
finally:
|
|
288
371
|
conn.close()
|
|
289
372
|
return record
|
|
@@ -292,11 +375,9 @@ class SQLiteEventStore:
|
|
|
292
375
|
"""Read the full stream for None, or events strictly after an integer cursor."""
|
|
293
376
|
conn = self._connect()
|
|
294
377
|
try:
|
|
295
|
-
|
|
296
|
-
rows = conn.execute(f"SELECT run_id, sequence, event_id, type, actor, causation_id, correlation_id, ts, payload, artifact_hashes FROM events WHERE run_id = ? AND sequence {operator} ? ORDER BY sequence ASC", (run_id, cursor)).fetchall()
|
|
378
|
+
return read_event_rows(conn, run_id, since_sequence=since_sequence)
|
|
297
379
|
finally:
|
|
298
380
|
conn.close()
|
|
299
|
-
return [{"schema": EVENT_SCHEMA_ID, "run_id": row[0], "sequence": row[1], "event_id": row[2], "type": row[3], "actor": row[4], "causation_id": row[5], "correlation_id": row[6], "ts": row[7], "payload": json.loads(row[8]), "artifact_hashes": json.loads(row[9])} for row in rows]
|
|
300
381
|
|
|
301
382
|
def latest_sequence(self, run_id: str) -> int | None:
|
|
302
383
|
conn = self._connect()
|