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.
Files changed (319) hide show
  1. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.claude-plugin/plugin.json +1 -1
  2. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/CHANGELOG.md +85 -1
  3. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/PKG-INFO +10 -6
  4. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/README.md +9 -5
  5. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/action.yml +42 -1
  6. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/__main__.py +40 -5
  7. loop_engineer-0.10.0/loop/chain.py +88 -0
  8. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/contract.py +4 -2
  9. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/events.py +86 -5
  10. loop_engineer-0.10.0/loop/migrate.py +45 -0
  11. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/reducer.py +18 -2
  12. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/runcontrol.py +9 -1
  13. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/runner.py +32 -28
  14. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/runtime.py +109 -39
  15. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/pyproject.toml +1 -1
  16. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/repo-os-contract.md +272 -9
  17. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/event.schema.json +3 -1
  18. loop_engineer-0.10.0/scripts/chain_fixtures.py +66 -0
  19. loop_engineer-0.10.0/scripts/test_adversarial_chain.py +345 -0
  20. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_docs_version.py +12 -2
  21. loop_engineer-0.10.0/scripts/test_doctor_eventstore.py +420 -0
  22. loop_engineer-0.10.0/scripts/test_event_chain.py +487 -0
  23. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_cli.py +1 -1
  24. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_cli_status_replay.py +10 -3
  25. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_simulate_zero_writes.py +28 -3
  26. loop_engineer-0.10.0/scripts/test_migrate_cli.py +128 -0
  27. loop_engineer-0.9.0/scripts/test_doctor_eventstore.py +0 -166
  28. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.claude-plugin/marketplace.json +0 -0
  29. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  30. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  31. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  32. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  33. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/dependabot.yml +0 -0
  34. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/workflows/ci.yml +0 -0
  35. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.github/workflows/publish.yml +0 -0
  36. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.gitignore +0 -0
  37. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/.pre-commit-hooks.yaml +0 -0
  38. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/CODE_OF_CONDUCT.md +0 -0
  39. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/CONTRIBUTING.md +0 -0
  40. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/GLOSSARY.md +0 -0
  41. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/LICENSE +0 -0
  42. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/SECURITY.md +0 -0
  43. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/ROADMAP-v1.0.md +0 -0
  44. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/adr/0001-proof-kernel-and-runtime.md +0 -0
  45. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/01-good-first-qw9-trigger-phrases.md +0 -0
  46. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/02-good-first-qw10-self-eval-labels.md +0 -0
  47. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/03-good-first-emit-metrics-vocabulary.md +0 -0
  48. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/04-help-wanted-openhands-recipe.md +0 -0
  49. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/05-help-wanted-ruflo-recipe.md +0 -0
  50. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/06-help-wanted-gap-reports.md +0 -0
  51. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/contributing/issues/07-help-wanted-emit-scaffold-runlog-seed.md +0 -0
  52. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/demo.cast +0 -0
  53. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/demo.gif +0 -0
  54. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/gap-reports/scoreboard.md +0 -0
  55. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/gap-reports/superpowers.md +0 -0
  56. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/integrations/langgraph.md +0 -0
  57. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/integrations/temporal.md +0 -0
  58. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/metrics-baseline.json +0 -0
  59. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/social-card.png +0 -0
  60. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/plans/2026-06-20-loop-engineer-v1.md +0 -0
  61. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/plans/2026-06-30-loop-engineer-v1.0-roadmap.md +0 -0
  62. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/plans/2026-07-03-adoption-slices.md +0 -0
  63. {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
  64. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/plans/2026-07-09-st5-harness-scoreboard.md +0 -0
  65. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-06-20-loop-engineer-design.md +0 -0
  66. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-06-30-st1-metrics-baseline.md +0 -0
  67. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-06-30-st2-portable-contract-spec.md +0 -0
  68. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-06-30-st3-integration-adapters.md +0 -0
  69. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-06-30-v04-credibility-enforcement.md +0 -0
  70. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-07-03-adoption-slices-design.md +0 -0
  71. {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
  72. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/docs/superpowers/specs/2026-07-09-st5-harness-scoreboard-design.md +0 -0
  73. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/evals/cases/structural.json +0 -0
  74. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/evals/rubric.md +0 -0
  75. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/README.md +0 -0
  76. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/agent-os/product/mission.md +0 -0
  77. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/agent-os/product/roadmap.md +0 -0
  78. {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
  79. {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
  80. {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
  81. {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
  82. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/agent-os/standards/backend/data-imports.md +0 -0
  83. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/agent-os-run/agent-os/standards/index.yml +0 -0
  84. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/README.md +0 -0
  85. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/_bmad/bmm/config.yaml +0 -0
  86. {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
  87. {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
  88. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/_bmad-output/implementation-artifacts/sprint-status.yaml +0 -0
  89. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/_bmad-output/planning-artifacts/epics.md +0 -0
  90. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/_bmad-output/planning-artifacts/prd.md +0 -0
  91. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/src/import_contacts.py +0 -0
  92. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/bmad-run/tests/test_import_contacts.py +0 -0
  93. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/1235-analysis.md +0 -0
  94. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/1235.md +0 -0
  95. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/epic.md +0 -0
  96. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/execution-status.md +0 -0
  97. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/github-mapping.md +0 -0
  98. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/updates/1235/progress.md +0 -0
  99. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/epics/csv-dedupe/updates/1235/stream-A.md +0 -0
  100. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/.claude/prds/csv-dedupe.md +0 -0
  101. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ccpm-run/README.md +0 -0
  102. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/approvals/.gitkeep +0 -0
  103. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/artifacts/holdout-run.txt +0 -0
  104. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/artifacts/holdout-verdict.json +0 -0
  105. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/artifacts/verify-T1.json +0 -0
  106. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/artifacts/verify-T2-iter1.json +0 -0
  107. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/artifacts/verify-T2.json +0 -0
  108. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/checkpoints/.gitkeep +0 -0
  109. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/manifest.yaml +0 -0
  110. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/memory/.gitkeep +0 -0
  111. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/repair/iter-002.json +0 -0
  112. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/.loop/state.json +0 -0
  113. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/ADR.md +0 -0
  114. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/README.md +0 -0
  115. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/RUNLOG.md +0 -0
  116. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/SPEC.md +0 -0
  117. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/TASKS.json +0 -0
  118. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/WORKFLOW.md +0 -0
  119. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/scripts/run-example +0 -0
  120. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/scripts/verify-fast +0 -0
  121. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/scripts/verify-full +0 -0
  122. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/target/manifest.json +0 -0
  123. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/target/measure_coverage.py +0 -0
  124. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/target/pricing.py +0 -0
  125. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/target/test_holdout.py +0 -0
  126. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/target/test_visible.py +0 -0
  127. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/coverage-repair/terminal_state.json +0 -0
  128. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/evidence/invalid/absolute-uri.json +0 -0
  129. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/evidence/invalid/bad-sha256-pattern.json +0 -0
  130. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/evidence/invalid/missing-required-field.json +0 -0
  131. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/evidence/invalid/scheme-uri.json +0 -0
  132. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/evidence/valid-evidence.json +0 -0
  133. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/artifacts/holdout-verdict.json +0 -0
  134. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/artifacts/verify-T1-iter1.json +0 -0
  135. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/artifacts/verify-T1.json +0 -0
  136. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/manifest.yaml +0 -0
  137. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/repair/iter-002.json +0 -0
  138. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/state.json +0 -0
  139. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/.loop/terminal_state.json +0 -0
  140. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/README.md +0 -0
  141. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/RUNLOG.md +0 -0
  142. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/SPEC.md +0 -0
  143. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/TASKS.json +0 -0
  144. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/WORKFLOW.md +0 -0
  145. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/scripts/run-example +0 -0
  146. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/scripts/verify-fast +0 -0
  147. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/scripts/verify-full +0 -0
  148. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/target/jobs.py +0 -0
  149. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/target/manifest.json +0 -0
  150. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/target/measure_stability.py +0 -0
  151. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/target/test_holdout.py +0 -0
  152. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/flaky-test-triage/target/test_visible.py +0 -0
  153. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/langgraph-emit/README.md +0 -0
  154. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/langgraph-emit/graph_example.py +0 -0
  155. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/naive-loop/.loop/state.json +0 -0
  156. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/naive-loop/README.md +0 -0
  157. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/openspec-run/README.md +0 -0
  158. {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
  159. {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
  160. {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
  161. {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
  162. {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
  163. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/openspec-run/openspec/config.yaml +0 -0
  164. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/openspec-run/openspec/specs/csv-dedupe/spec.md +0 -0
  165. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/plans/coverage-repair.plan.json +0 -0
  166. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/plans/invalid/cyclic-dependency.plan.json +0 -0
  167. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/plans/invalid/missing-goal.plan.json +0 -0
  168. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/PRPs/plans/completed/csv-dedupe.plan.md +0 -0
  169. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/PRPs/prds/csv-dedupe.prd.md +0 -0
  170. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/PRPs/reports/csv-dedupe-report.md +0 -0
  171. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/PRPs/reviews/pr-42-cycle-0.verdict.json +0 -0
  172. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/PRPs/reviews/pr-42-review.md +0 -0
  173. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/prp-loop.run.log +0 -0
  174. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/.claude/prp-loop.state.json +0 -0
  175. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/prp-run/README.md +0 -0
  176. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ruflo-run/.claude-flow/metrics/swarm-activity.json +0 -0
  177. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ruflo-run/.claude-flow/metrics/v3-progress.json +0 -0
  178. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ruflo-run/.claude-flow/security/audit-status.json +0 -0
  179. {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
  180. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ruflo-run/.mcp.json +0 -0
  181. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ruflo-run/.swarm/memory-export.json +0 -0
  182. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/ruflo-run/README.md +0 -0
  183. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/.specify/feature.json +0 -0
  184. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/.specify/memory/constitution.md +0 -0
  185. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/README.md +0 -0
  186. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/specs/001-csv-dedupe/checklists/requirements.md +0 -0
  187. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/specs/001-csv-dedupe/plan.md +0 -0
  188. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/specs/001-csv-dedupe/quickstart.md +0 -0
  189. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/specs/001-csv-dedupe/research.md +0 -0
  190. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/specs/001-csv-dedupe/spec.md +0 -0
  191. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/spec-kit-run/specs/001-csv-dedupe/tasks.md +0 -0
  192. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/superpowers-run/.superpowers/sdd/progress.md +0 -0
  193. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/superpowers-run/README.md +0 -0
  194. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/superpowers-run/docs/superpowers/plans/2026-07-08-csv-dedupe.md +0 -0
  195. {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
  196. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/config.json +0 -0
  197. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/docs/prd.txt +0 -0
  198. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/reports/task-complexity-report.json +0 -0
  199. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/state.json +0 -0
  200. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/tasks/task_001.txt +0 -0
  201. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/tasks/tasks.json +0 -0
  202. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/.taskmaster/templates/example_prd.txt +0 -0
  203. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/CLAUDE.md +0 -0
  204. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/task-master-run/README.md +0 -0
  205. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/temporal-certify/README.md +0 -0
  206. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/examples/temporal-certify/workflow_example.py +0 -0
  207. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/hooks/stop_firewall.py +0 -0
  208. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/__init__.py +0 -0
  209. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/_resources.py +0 -0
  210. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/architect.py +0 -0
  211. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/completion.py +0 -0
  212. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/emit.py +0 -0
  213. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/evidence.py +0 -0
  214. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/foreign.py +0 -0
  215. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/fsm.py +0 -0
  216. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/integrations.py +0 -0
  217. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/paths.py +0 -0
  218. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/plan.py +0 -0
  219. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/scaffold.py +0 -0
  220. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/loop/simulate.py +0 -0
  221. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/architecture-matrix.md +0 -0
  222. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/eval-suite.md +0 -0
  223. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/loop-patterns.md +0 -0
  224. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/model-routing.md +0 -0
  225. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/platform-map.md +0 -0
  226. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/prompt-templates.md +0 -0
  227. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/reference/safety-and-approvals.md +0 -0
  228. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/evidence.schema.json +0 -0
  229. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/manifest.schema.json +0 -0
  230. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/plan.schema.json +0 -0
  231. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/receipt.schema.json +0 -0
  232. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/repair-record.schema.json +0 -0
  233. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/rollout-record.schema.json +0 -0
  234. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/state.schema.json +0 -0
  235. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/tasks.schema.json +0 -0
  236. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/schemas/terminal.schema.json +0 -0
  237. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/action_scorecard.py +0 -0
  238. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/anticheat_scan.py +0 -0
  239. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/benchmark_harness.py +0 -0
  240. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/holdout_gate.py +0 -0
  241. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/inspect_loop.py +0 -0
  242. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/metrics.py +0 -0
  243. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/rollout_ledger.py +0 -0
  244. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/runtime_monitor.py +0 -0
  245. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/self_eval.py +0 -0
  246. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_action_scorecard.py +0 -0
  247. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_adversarial_kernel.py +0 -0
  248. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_adversarial_process.py +0 -0
  249. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_anticheat_scan.py +0 -0
  250. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_benchmark_harness.py +0 -0
  251. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_ci_release_mode.py +0 -0
  252. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_completion_policy.py +0 -0
  253. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_conformance.py +0 -0
  254. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_contract_records.py +0 -0
  255. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_docs_adoption.py +0 -0
  256. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_docs_baseline.py +0 -0
  257. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_docs_claims.py +0 -0
  258. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_emit.py +0 -0
  259. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_eventstore.py +0 -0
  260. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_evidence.py +0 -0
  261. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_example_runnable.py +0 -0
  262. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_flaky_example.py +0 -0
  263. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_foreign_inspect.py +0 -0
  264. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_fsm.py +0 -0
  265. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_holdout_gate.py +0 -0
  266. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_inspect_loop.py +0 -0
  267. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_integrations.py +0 -0
  268. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_langgraph_recipe.py +0 -0
  269. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_langgraph_recipe_st3.py +0 -0
  270. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_architect_cli.py +0 -0
  271. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_contract_core.py +0 -0
  272. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_runcontrol_cli.py +0 -0
  273. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_loop_simulate_cli.py +0 -0
  274. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_manifest_extra_states_schema.py +0 -0
  275. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_metrics.py +0 -0
  276. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_metrics_cli.py +0 -0
  277. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_plan_schema.py +0 -0
  278. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_precommit_hook.py +0 -0
  279. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_reducer.py +0 -0
  280. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_resources.py +0 -0
  281. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_rollout_ledger.py +0 -0
  282. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_runner_dispatch.py +0 -0
  283. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_runner_verifier.py +0 -0
  284. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_runtime_monitor.py +0 -0
  285. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_scaffold.py +0 -0
  286. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_schemas_metrics.py +0 -0
  287. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_self_eval.py +0 -0
  288. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_state_vocabulary.py +0 -0
  289. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_stop_firewall.py +0 -0
  290. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_template_roundtrip.py +0 -0
  291. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_temporal_recipe.py +0 -0
  292. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_validate_frontmatter.py +0 -0
  293. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/test_wheel_selfcontained.py +0 -0
  294. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/scripts/validate_frontmatter.py +0 -0
  295. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-architect/SKILL.md +0 -0
  296. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-contract/SKILL.md +0 -0
  297. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-engineer/SKILL.md +0 -0
  298. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-evals/SKILL.md +0 -0
  299. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-flywheel/SKILL.md +0 -0
  300. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-inspector/SKILL.md +0 -0
  301. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-inspector/reference/patterns.md +0 -0
  302. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-repair/SKILL.md +0 -0
  303. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-run/SKILL.md +0 -0
  304. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-runtime-monitor/SKILL.md +0 -0
  305. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/skills/loop-runtime-monitor/reference/patterns.md +0 -0
  306. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/AGENTS.md.tmpl +0 -0
  307. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/EVALS-rubric.md.tmpl +0 -0
  308. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/RUNLOG.md.tmpl +0 -0
  309. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/SPEC.md.tmpl +0 -0
  310. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/TASKS.json.tmpl +0 -0
  311. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/WORKFLOW.md.tmpl +0 -0
  312. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/extract-trace-metrics.sh +0 -0
  313. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/judge-rubric.sh +0 -0
  314. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/manifest.yaml.tmpl +0 -0
  315. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/state.json.tmpl +0 -0
  316. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/terminal_state.json.tmpl +0 -0
  317. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/verify-fast.sh +0 -0
  318. {loop_engineer-0.9.0 → loop_engineer-0.10.0}/templates/verify-full.sh +0 -0
  319. {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.9.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.9.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
  [![CI](https://github.com/SollanSystems/loop-engineer/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/SollanSystems/loop-engineer/actions/workflows/ci.yml)
45
45
  [![Python 3.10–3.12](https://img.shields.io/badge/python-3.10%E2%80%933.12-blue)](https://www.python.org/downloads/)
46
46
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
47
- [![Release](https://img.shields.io/badge/release-0.9.0-blue)](https://github.com/SollanSystems/loop-engineer/tags)
47
+ [![Release](https://img.shields.io/badge/release-0.10.0-blue)](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
  ![The inspector scores a self-asserted DIY loop 0/weak, then the gate-backed example 90/strong — both runs live](docs/demo.gif)
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.9.0
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.9.0`
464
- - Release tag: `v0.9.0` (PyPI publish trigger; plugin tags through 0.6.0 used `loop-engineer--v<version>`)
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
  [![CI](https://github.com/SollanSystems/loop-engineer/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/SollanSystems/loop-engineer/actions/workflows/ci.yml)
6
6
  [![Python 3.10–3.12](https://img.shields.io/badge/python-3.10%E2%80%933.12-blue)](https://www.python.org/downloads/)
7
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
8
- [![Release](https://img.shields.io/badge/release-0.9.0-blue)](https://github.com/SollanSystems/loop-engineer/tags)
8
+ [![Release](https://img.shields.io/badge/release-0.10.0-blue)](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
  ![The inspector scores a self-asserted DIY loop 0/weak, then the gate-backed example 90/strong — both runs live](docs/demo.gif)
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.9.0
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.9.0`
425
- - Release tag: `v0.9.0` (PyPI publish trigger; plugin tags through 0.6.0 used `loop-engineer--v<version>`)
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
- loop doctor "$LOOP_PATH" | tee "${RUNNER_TEMP}/doctor.json"
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] <workspace-or-.loop>
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) -> dict[str, Any]:
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(target, mode=mode)
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
- conn.execute("INSERT INTO events (run_id, sequence, event_id, type, actor, causation_id, correlation_id, ts, payload, artifact_hashes) VALUES (?,?,?,?,?,?,?,?,?,?)",
282
- (record["run_id"], record["sequence"], record["event_id"], record["type"], record["actor"], record["causation_id"], record["correlation_id"], record["ts"], payload_json, hashes_json))
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
- operator, cursor = (">=", 0) if since_sequence is None else (">", since_sequence)
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()