Compare commits

...
58 Commits
Author SHA1 Message Date
conductor 243474c644 chore(release): auto-bump version to 0.61.0 — earned by #309 (feat(ADAPT-9): artifact authority + retry authority)
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / type-check (push) Successful in 33s
CI Pipeline / gitea-offline (push) Successful in 17s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #309
2026-09-09 20:53:16 +01:00
conductor 381804f154 chore(release): auto-bump version to 0.61.0 — earned by #309 (feat(ADAPT-9): artifact authority + retry authority)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / canary (push) Successful in 34s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #309
2026-09-09 20:53:11 +01:00
conductor 38fd3caf5b chore(release): auto-bump version to 0.61.0 — earned by #309 (feat(ADAPT-9): artifact authority + retry authority)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #309
2026-09-09 20:53:09 +01:00
conductor a6bbef0ce0 chore(release): auto-bump version to 0.61.0 — earned by #309 (feat(ADAPT-9): artifact authority + retry authority)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Failing after 32s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 54s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / required-checks (push) Successful in 2s
Promotion record: #309
2026-09-09 20:53:05 +01:00
conductor 67587d7007 chore(release): auto-bump version to 0.61.0 — earned by #309 (feat(ADAPT-9): artifact authority + retry authority)
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Failing after 34s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / unit-tests (push) Failing after 54s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #309
2026-09-09 20:53:03 +01:00
conductor 5da36bf296 Merge pull request 'feat(ADAPT-9): artifact authority + retry authority' (#309) from adapt9-artifact-authority into master
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / lint (push) Successful in 39s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 58s
CI Pipeline / required-checks (push) Successful in 2s
2026-09-09 20:52:45 +01:00
Nick 9c78b3d294 chore(ci): resync dependency lock — pyright 1.1.412 → 1.1.413 (third recurrence; TUNE candidate: scheduled lock refresh or resolution pinning)
CI Pipeline / best-practices (push) Successful in 8s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / type-check (push) Successful in 33s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / unit-tests (push) Successful in 1m10s
CI Pipeline / lint (pull_request) Successful in 35s
CI Pipeline / best-practices (pull_request) Successful in 3s
CI Pipeline / documentation (pull_request) Successful in 4s
CI Pipeline / canary (pull_request) Successful in 30s
CI Pipeline / gitea-offline (pull_request) Successful in 15s
CI Pipeline / documentation (push) Successful in 3s
Spec gate (headless) / spec-gate (pull_request) Successful in 7s
CI Pipeline / unit-tests (pull_request) Successful in 53s
CI Pipeline / type-check (pull_request) Successful in 32s
CI Pipeline / required-checks (pull_request) Successful in 1s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / required-checks (push) Successful in 1s
Auto-version and Release / release (pull_request) Successful in 37s
2026-09-09 20:50:12 +01:00
Nick cbe0d61500 feat(ADAPT-9): artifact authority + retry authority (closes #298)
CI Pipeline / best-practices (push) Successful in 5s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Failing after 30s
Auto-label / priority-gate (pull_request) Skipped
CI Pipeline / canary (push) Successful in 35s
Auto-label / auto-label (pull_request) Successful in 3s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 59s
CI Pipeline / best-practices (pull_request) Successful in 3s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Failing after 26s
CI Pipeline / canary (pull_request) Successful in 31s
Spec gate (headless) / spec-gate (pull_request) Successful in 6s
CI Pipeline / gitea-offline (pull_request) Successful in 14s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / type-check (pull_request) Successful in 31s
CI Pipeline / unit-tests (pull_request) Successful in 57s
CI Pipeline / required-checks (pull_request) Successful in 1s
Two problems, bounded exactly:

1. ARTIFACT AUTHORITY — the declared structured artifact file is truth;
   stdout is observability only.
   - _resolve_worker_artifact(): reads the declared stage artifact file
     (STAGE_ARTIFACT_FILES) as the sole canonical channel
   - stdout JSON extraction demoted to LEGACY_COMPATIBILITY_ONLY behind
     allow_stdout_fallback=False (canonical default: OFF — proven by test);
     marked 'NOT CANONICAL — REMOVE AFTER legacy tests rewritten'
   - prose stdout + missing file → STAGE_FAILED, error=ARTIFACT_MISSING
     (no advancement, no accepted artifact) — the nasty case from #163
     is now structurally impossible
   - stdout prose + valid file → PASS (file wins; single authority —
     precedence test included)
   - both APPLICABILITY and ADVERSARIAL/INNOVATION/SYNTHESIS resolution
     paths rewired to the shared helper

2. RETRY AUTHORITY — attempt/event identity is append-only.
   - retry = new attempt/event ID (never marker deletion; no marker
     deletion exists anywhere in scripts/)
   - failed attempt cannot advance the tree
   - duplicate delivery of a COMPLETED event → DUPLICATE no-op
   - failed attempt history retained after successful retry
   - acceptance sequence proven: FAILED → retry → GATE_FAILED → retry →
     COMPLETED → duplicate delivery = 3 genuine attempts retained,
     1 duplicate no-op, exactly one advancement

Note: StageExecutor constructs its own tree from the persisted dir — tests
must assert on ex.tree for post-execute state.

6 tests. Full suite 1158 passed; ruff clean.
2026-09-09 20:45:33 +01:00
conductor 32204c4d76 chore(release): auto-bump version to 0.60.0 — earned by #308 (feat(PROV-1): deterministic Source Ledger — renderer + idempotent publication)
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / gitea-offline (push) Successful in 15s
Promotion record: #308
2026-09-09 19:04:25 +01:00
conductor 9074e7693a chore(release): auto-bump version to 0.60.0 — earned by #308 (feat(PROV-1): deterministic Source Ledger — renderer + idempotent publication)
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #308
2026-09-09 19:04:22 +01:00
conductor e0d63a4045 chore(release): auto-bump version to 0.60.0 — earned by #308 (feat(PROV-1): deterministic Source Ledger — renderer + idempotent publication)
CI Pipeline / best-practices (push) Successful in 5s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 33s
CI Pipeline / lint (push) Successful in 40s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 56s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #308
2026-09-09 19:04:19 +01:00
conductor a2fc115ac7 chore(release): auto-bump version to 0.60.0 — earned by #308 (feat(PROV-1): deterministic Source Ledger — renderer + idempotent publication)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / canary (push) Failing after 30s
CI Pipeline / unit-tests (push) Successful in 50s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #308
2026-09-09 19:04:14 +01:00
conductor 9437ce17c5 chore(release): auto-bump version to 0.60.0 — earned by #308 (feat(PROV-1): deterministic Source Ledger — renderer + idempotent publication)
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Failing after 32s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / unit-tests (push) Failing after 53s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #308
2026-09-09 19:04:12 +01:00
conductor 2093558fc4 Merge pull request 'feat(PROV-1): deterministic Source Ledger — renderer + idempotent publication' (#308) from prov1-source-ledger into master
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / lint (push) Successful in 39s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 55s
CI Pipeline / required-checks (push) Successful in 2s
2026-09-09 19:03:57 +01:00
Nick e7d4c2ad9b chore(ci): resync dependency lock — pyright 1.1.411 → 1.1.412 (upstream drift)
CI Pipeline / best-practices (push) Successful in 10s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / lint (push) Successful in 38s
CI Pipeline / canary (push) Successful in 39s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 1m4s
CI Pipeline / best-practices (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Successful in 36s
CI Pipeline / canary (pull_request) Successful in 30s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / unit-tests (pull_request) Successful in 1m7s
CI Pipeline / documentation (pull_request) Successful in 3s
Spec gate (headless) / spec-gate (pull_request) Successful in 8s
CI Pipeline / gitea-offline (pull_request) Successful in 16s
CI Pipeline / type-check (pull_request) Successful in 30s
CI Pipeline / required-checks (pull_request) Successful in 1s
Auto-version and Release / release (pull_request) Successful in 40s
2026-09-09 19:01:09 +01:00
Nick 2da0afb324 feat(PROV-1): deterministic Source Ledger — renderer, persisted metadata, idempotent Gitea publication (closes #282)
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
Auto-label / priority-gate (pull_request) Skipped
CI Pipeline / lint (push) Failing after 28s
Auto-label / auto-label (pull_request) Successful in 2s
CI Pipeline / canary (push) Successful in 33s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 57s
CI Pipeline / best-practices (pull_request) Successful in 4s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Failing after 26s
CI Pipeline / canary (pull_request) Successful in 31s
Spec gate (headless) / spec-gate (pull_request) Successful in 6s
CI Pipeline / gitea-offline (pull_request) Successful in 16s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / type-check (pull_request) Successful in 29s
CI Pipeline / unit-tests (pull_request) Successful in 57s
CI Pipeline / required-checks (pull_request) Successful in 1s
scripts/source_ledger.py — pure deterministic projection:
- build_records: accepted OUTWARD artifact + SOURCE_READ/TOOL_FAILED receipts
  → ledger records with receipt_id (deterministic hash of stage+tool+
  canonical_url+timestamp+seq), source_id, requested_url+resolved_url,
  domain, status, tool, timestamp, source_type, claim_ids,
  contradiction_ids, blocked_reason
- THE safety invariant: READ impossible without a successful machine
  receipt; artifact-claimed-read with no matching receipt downgrades to
  FAILED honestly; BLOCKED/FAILED/NOT_SELECTED back ZERO accepted claims
- NOT_SELECTED distinct from BLOCKED (never attempted ≠ attempted-refused)
- explicit contradicts_source_ids used when present; legacy nesting labeled
  in render; plain-string contradictions not linked (labeled honestly)
- render_markdown: fixed template; runner/model from persisted factual
  fields only, UNKNOWN if absent — never reconstructed

scripts/publish_source_ledger.py — idempotent Gitea publication:
- comment identified by marker containing artifact hash; same hash UPDATES,
  new hash creates; publication failure raises VISIBLY (never silently
  falsifies the ledger) and does not alter stage outcomes

research_stage_executor.py: model + runner persisted into the accepted
OUTWARD artifact (smallest authority point; PROV-1 metadata fields)

Real acceptance on #161: 16 records (4 READ with machine receipts replayed
from the genuine events file, 12 NOT_SELECTED backing zero claims), ledger
live on Gitea issue 161 (comment 3510), idempotency proven (second publish
updates same comment). Legacy artifact honestly labeled UNKNOWN for
runner/model and legacy-nesting noted in render.

14 ledger tests; full suite 1151 passed; ruff clean.
2026-09-09 18:56:53 +01:00
conductor aa8ca45302 chore(release): auto-bump version to 0.59.5 — earned by #307 (INFRA-1 acceptance: native merge API after 1.27.3 upgrade)
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / unit-tests (push) Successful in 58s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #307
2026-09-09 17:53:58 +01:00
conductor 058c183a89 chore(release): auto-bump version to 0.59.5 — earned by #307 (INFRA-1 acceptance: native merge API after 1.27.3 upgrade)
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #307
2026-09-09 17:53:56 +01:00
conductor 62cb334c95 chore(release): auto-bump version to 0.59.5 — earned by #307 (INFRA-1 acceptance: native merge API after 1.27.3 upgrade)
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / lint (push) Successful in 39s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 59s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #307
2026-09-09 17:53:53 +01:00
conductor 2379ba2a70 chore(release): auto-bump version to 0.59.5 — earned by #307 (INFRA-1 acceptance: native merge API after 1.27.3 upgrade)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / canary (push) Failing after 36s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / unit-tests (push) Successful in 53s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #307
2026-09-09 17:53:50 +01:00
conductor a7215d96be chore(release): auto-bump version to 0.59.5 — earned by #307 (INFRA-1 acceptance: native merge API after 1.27.3 upgrade)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / canary (push) Failing after 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Failing after 48s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #307
2026-09-09 17:53:48 +01:00
conductor e7b7aa2b10 Merge pull request 'INFRA-1 acceptance: native merge API after 1.27.3 upgrade' (#307) from infra1-accept into master
CI Pipeline / best-practices (push) Successful in 6s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / canary (push) Successful in 34s
CI Pipeline / lint (push) Successful in 39s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 58s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-09 17:53:32 +01:00
Nick 897fd1d74f chore(infra1): acceptance PR — native merge API after Gitea 1.27.3 upgrade
Auto-label / priority-gate (pull_request) Skipped
Auto-label / auto-label (pull_request) Successful in 1s
CI Pipeline / best-practices (pull_request) Successful in 2s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Successful in 35s
CI Pipeline / canary (pull_request) Successful in 33s
CI Pipeline / unit-tests (pull_request) Successful in 53s
CI Pipeline / gitea-offline (pull_request) Successful in 15s
CI Pipeline / type-check (pull_request) Successful in 29s
CI Pipeline / required-checks (pull_request) Successful in 1s
Spec gate (headless) / spec-gate (pull_request) Successful in 7s
Auto-version and Release / release (pull_request) Successful in 34s
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / lint (push) Failing after 37s
CI Pipeline / canary (push) Failing after 47s
CI Pipeline / type-check (push) Successful in 37s
CI Pipeline / unit-tests (push) Failing after 1m14s
CI Pipeline / required-checks (push) Successful in 6s
2026-09-09 17:51:19 +01:00
conductor 1b224dabf1 chore(release): auto-bump version to 0.59.4 — earned by #306 (INFRA-1 probe 5b: full merge response capture)
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / unit-tests (push) Successful in 50s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 17s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #306
2026-09-09 17:42:17 +01:00
conductor a9866db411 chore(release): auto-bump version to 0.59.4 — earned by #306 (INFRA-1 probe 5b: full merge response capture)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #306
2026-09-09 17:42:13 +01:00
conductor 003b6942e7 chore(release): auto-bump version to 0.59.4 — earned by #306 (INFRA-1 probe 5b: full merge response capture)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / unit-tests (push) Successful in 53s
CI Pipeline / required-checks (push) Successful in 0s
Promotion record: #306
2026-09-09 17:42:09 +01:00
conductor 6e4d9d9b47 chore(release): auto-bump version to 0.59.4 — earned by #306 (INFRA-1 probe 5b: full merge response capture)
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Failing after 29s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Successful in 54s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #306
2026-09-09 17:42:07 +01:00
conductor caa94095bf chore(release): auto-bump version to 0.59.4 — earned by #306 (INFRA-1 probe 5b: full merge response capture)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Failing after 29s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #306
2026-09-09 17:42:05 +01:00
conductor b1491ceef2 Merge pull request 'INFRA-1 probe 5b: full merge response capture' (#306) from infra1-probe5 into master
CI Pipeline / best-practices (push) Successful in 5s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / unit-tests (push) Successful in 56s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-09 17:41:47 +01:00
Nick c4c73ca037 chore(infra1): probe 5 — full native merge response capture (robot disabled)
CI Pipeline / best-practices (pull_request) Successful in 3s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Successful in 35s
CI Pipeline / canary (pull_request) Successful in 31s
CI Pipeline / gitea-offline (pull_request) Successful in 15s
Spec gate (headless) / spec-gate (pull_request) Successful in 6s
CI Pipeline / unit-tests (pull_request) Successful in 51s
CI Pipeline / type-check (pull_request) Successful in 29s
CI Pipeline / required-checks (pull_request) Successful in 1s
Auto-version and Release / release (pull_request) Successful in 47s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / lint (push) Failing after 39s
CI Pipeline / gitea-offline (push) Successful in 17s
CI Pipeline / canary (push) Failing after 38s
CI Pipeline / type-check (push) Successful in 41s
CI Pipeline / unit-tests (push) Failing after 1m9s
CI Pipeline / required-checks (push) Successful in 4s
2026-09-09 17:39:43 +01:00
conductor b5827a3cd0 chore(release): auto-bump version to 0.59.3 — earned by #305 (INFRA-1 probe 3: direct native merge proof)
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / required-checks (push) Successful in 2s
Promotion record: #305
2026-09-09 17:27:59 +01:00
conductor 4d79f7c230 chore(release): auto-bump version to 0.59.3 — earned by #305 (INFRA-1 probe 3: direct native merge proof)
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 33s
CI Pipeline / lint (push) Successful in 39s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / unit-tests (push) Successful in 56s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #305
2026-09-09 17:27:57 +01:00
conductor 467be81b47 chore(release): auto-bump version to 0.59.3 — earned by #305 (INFRA-1 probe 3: direct native merge proof)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Successful in 53s
CI Pipeline / required-checks (push) Successful in 0s
Promotion record: #305
2026-09-09 17:27:51 +01:00
conductor e3cec58917 chore(release): auto-bump version to 0.59.3 — earned by #305 (INFRA-1 probe 3: direct native merge proof)
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / canary (push) Successful in 29s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #305
2026-09-09 17:27:49 +01:00
conductor bb25b0a48a chore(release): auto-bump version to 0.59.3 — earned by #305 (INFRA-1 probe 3: direct native merge proof)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Failing after 31s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Failing after 46s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #305
2026-09-09 17:27:47 +01:00
conductor 5643b6c3b5 Merge pull request 'INFRA-1 probe 3: direct native merge proof' (#305) from infra1-probe3 into master
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 56s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-09 17:27:34 +01:00
conductor 5cfe2bf5f3 Merge branch 'master' into infra1-probe3
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / best-practices (pull_request) Successful in 3s
CI Pipeline / unit-tests (push) Successful in 56s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Successful in 35s
CI Pipeline / gitea-offline (pull_request) Successful in 15s
Spec gate (headless) / spec-gate (pull_request) Successful in 7s
CI Pipeline / canary (pull_request) Successful in 32s
CI Pipeline / required-checks (push) Successful in 0s
CI Pipeline / type-check (pull_request) Successful in 29s
CI Pipeline / unit-tests (pull_request) Successful in 51s
CI Pipeline / required-checks (pull_request) Successful in 1s
Auto-version and Release / release (pull_request) Successful in 36s
2026-09-09 17:25:07 +01:00
Nick 5252ffcfc3 chore(infra1): probe 3 — native merge by direct API call
Auto-label / auto-label (pull_request) Successful in 2s
Auto-label / priority-gate (pull_request) Skipped
CI Pipeline / lint (pull_request) Successful in 40s
CI Pipeline / best-practices (pull_request) Successful in 4s
CI Pipeline / documentation (pull_request) Successful in 4s
CI Pipeline / gitea-offline (pull_request) Successful in 16s
CI Pipeline / type-check (pull_request) Successful in 29s
Spec gate (headless) / spec-gate (pull_request) Successful in 6s
CI Pipeline / canary (pull_request) Successful in 31s
CI Pipeline / unit-tests (pull_request) Successful in 52s
CI Pipeline / required-checks (pull_request) Successful in 0s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / lint (push) Failing after 40s
CI Pipeline / gitea-offline (push) Successful in 17s
CI Pipeline / canary (push) Failing after 43s
CI Pipeline / type-check (push) Successful in 38s
CI Pipeline / unit-tests (push) Failing after 1m18s
CI Pipeline / required-checks (push) Successful in 8s
2026-09-09 17:18:43 +01:00
conductor 3498b8590c chore(release): auto-bump version to 0.59.2 — earned by #304 (INFRA-1 probe 2: merge-during-pending reproduction)
CI Pipeline / required-checks (push) Successful in 2s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
Promotion record: #304
2026-09-09 17:17:29 +01:00
conductor 11e417bc3d chore(release): auto-bump version to 0.59.2 — earned by #304 (INFRA-1 probe 2: merge-during-pending reproduction)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Successful in 50s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #304
2026-09-09 17:17:26 +01:00
conductor cec2214151 chore(release): auto-bump version to 0.59.2 — earned by #304 (INFRA-1 probe 2: merge-during-pending reproduction)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #304
2026-09-09 17:17:20 +01:00
conductor bbf90e21b2 chore(release): auto-bump version to 0.59.2 — earned by #304 (INFRA-1 probe 2: merge-during-pending reproduction)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Successful in 49s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / canary (push) Failing after 31s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #304
2026-09-09 17:17:16 +01:00
conductor 29aaaea132 chore(release): auto-bump version to 0.59.2 — earned by #304 (INFRA-1 probe 2: merge-during-pending reproduction)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / canary (push) Failing after 29s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 39s
CI Pipeline / required-checks (push) Successful in 2s
Promotion record: #304
2026-09-09 17:17:15 +01:00
conductor d0afb38279 Merge pull request 'INFRA-1 probe 2: merge-during-pending reproduction' (#304) from infra1-probe2 into master
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-09 17:16:56 +01:00
Nick 91fc479e0d chore(infra1): probe 2 — native merge while CI pending
Auto-label / priority-gate (pull_request) Skipped
Auto-label / auto-label (pull_request) Successful in 1s
CI Pipeline / best-practices (pull_request) Successful in 3s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Successful in 35s
CI Pipeline / canary (pull_request) Successful in 30s
Spec gate (headless) / spec-gate (pull_request) Successful in 6s
CI Pipeline / gitea-offline (pull_request) Successful in 15s
CI Pipeline / unit-tests (pull_request) Successful in 52s
CI Pipeline / type-check (pull_request) Successful in 28s
CI Pipeline / required-checks (pull_request) Successful in 1s
Auto-version and Release / release (pull_request) Successful in 41s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / gitea-offline (push) Successful in 18s
CI Pipeline / lint (push) Failing after 41s
CI Pipeline / canary (push) Failing after 45s
CI Pipeline / type-check (push) Successful in 40s
CI Pipeline / unit-tests (push) Failing after 1m21s
CI Pipeline / required-checks (push) Successful in 6s
2026-09-09 17:11:36 +01:00
conductor a72bb5f089 chore(release): auto-bump version to 0.59.1 — earned by #303 (INFRA-1 probe: native merge API reproduction (disposable))
CI Pipeline / required-checks (push) Successful in 2s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / unit-tests (push) Successful in 55s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / gitea-offline (push) Successful in 14s
Promotion record: #303
2026-09-09 17:09:33 +01:00
conductor 5cba27e32f chore(release): auto-bump version to 0.59.1 — earned by #303 (INFRA-1 probe: native merge API reproduction (disposable))
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Successful in 50s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #303
2026-09-09 17:09:27 +01:00
conductor 7882e5ff5f chore(release): auto-bump version to 0.59.1 — earned by #303 (INFRA-1 probe: native merge API reproduction (disposable))
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 55s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #303
2026-09-09 17:09:24 +01:00
conductor 4de17ed567 chore(release): auto-bump version to 0.59.1 — earned by #303 (INFRA-1 probe: native merge API reproduction (disposable))
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / canary (push) Successful in 36s
CI Pipeline / unit-tests (push) Successful in 1m0s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / required-checks (push) Successful in 2s
Promotion record: #303
2026-09-09 17:09:22 +01:00
conductor 45a6771788 chore(release): auto-bump version to 0.59.1 — earned by #303 (INFRA-1 probe: native merge API reproduction (disposable))
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Failing after 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / required-checks (push) Successful in 2s
Promotion record: #303
2026-09-09 17:09:20 +01:00
conductor 447e1219fa Merge pull request 'INFRA-1 probe: native merge API reproduction (disposable)' (#303) from infra1-probe into master
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / lint (push) Successful in 38s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / unit-tests (push) Successful in 57s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-09 17:09:03 +01:00
Nick 5f96ae7ffb chore(infra1): controlled PR to reproduce native merge API failure
Auto-label / priority-gate (pull_request) Skipped
Auto-label / auto-label (pull_request) Successful in 2s
CI Pipeline / best-practices (pull_request) Successful in 3s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Successful in 35s
CI Pipeline / canary (pull_request) Successful in 31s
CI Pipeline / gitea-offline (pull_request) Successful in 15s
Spec gate (headless) / spec-gate (pull_request) Successful in 6s
CI Pipeline / unit-tests (pull_request) Successful in 52s
CI Pipeline / type-check (pull_request) Successful in 29s
CI Pipeline / required-checks (pull_request) Successful in 1s
Auto-version and Release / release (pull_request) Successful in 46s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / lint (push) Failing after 41s
CI Pipeline / canary (push) Failing after 41s
CI Pipeline / type-check (push) Successful in 38s
CI Pipeline / unit-tests (push) Failing after 1m11s
CI Pipeline / required-checks (push) Successful in 6s
2026-09-09 17:07:00 +01:00
conductor c0c1c9a697 chore(release): auto-bump version to 0.59.0 — earned by #302 (feat(OBS-1): StageAttempt telemetry — per-attempt lifecycle, observational)
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / unit-tests (push) Successful in 54s
CI Pipeline / required-checks (push) Successful in 2s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / gitea-offline (push) Successful in 14s
Promotion record: #302
2026-09-09 16:55:46 +01:00
conductor b73f8b47b0 chore(release): auto-bump version to 0.59.0 — earned by #302 (feat(OBS-1): StageAttempt telemetry — per-attempt lifecycle, observational)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / required-checks (push) Successful in 2s
Promotion record: #302
2026-09-09 16:55:43 +01:00
conductor ce03bb3e8d chore(release): auto-bump version to 0.59.0 — earned by #302 (feat(OBS-1): StageAttempt telemetry — per-attempt lifecycle, observational)
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #302
2026-09-09 16:55:40 +01:00
conductor 4ce6e62f68 chore(release): auto-bump version to 0.59.0 — earned by #302 (feat(OBS-1): StageAttempt telemetry — per-attempt lifecycle, observational)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / canary (push) Successful in 42s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #302
2026-09-09 16:55:39 +01:00
conductor 121ef29a9f chore(release): auto-bump version to 0.59.0 — earned by #302 (feat(OBS-1): StageAttempt telemetry — per-attempt lifecycle, observational)
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / canary (push) Failing after 35s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 53s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / required-checks (push) Successful in 2s
Promotion record: #302
2026-09-09 16:55:37 +01:00
conductor f8bc64a902 Merge pull request 'feat(OBS-1): StageAttempt telemetry — per-attempt lifecycle, observational' (#302) from obs1-telemetry into master
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / canary (push) Successful in 40s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 1m7s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-09 16:55:19 +01:00
11 changed files with 831 additions and 78 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.58.1**
**Current release: v0.61.0**
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.58.1"
version = "0.61.0"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+1 -1
View File
@@ -82,7 +82,7 @@ pygments==2.21.0
# pytest
# rich
# textual
pyright==1.1.411
pyright==1.1.413
# via rune (pyproject.toml)
pytest==9.1.1
# via
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""publish_source_ledger.py — idempotent Gitea publication for the Source Ledger
(PROV-1, #282).
Pure projection publisher: renders the ledger from the accepted OUTWARD
artifact and posts/updates a single OUTWARD SOURCE LEDGER comment on the
issue. Idempotency: the comment is identified by a fixed marker containing
the artifact hash — re-running with the same hash UPDATES the existing
comment instead of creating duplicates.
Publication failure raises visibly (per #282 constraint: provenance
publication fails visibly, must not silently falsify the ledger). It does
NOT alter research cognition or stage outcomes — callers invoke it after
gate acceptance, and its failure does not invalidate the accepted artifact.
"""
from __future__ import annotations
import json
import os
import urllib.request
from source_ledger import build_records, render_markdown
MARKER_PREFIX = "<!-- source-ledger"
def _api(method: str, path: str, payload: dict | None = None) -> tuple[int, dict | str]:
token = os.environ.get("GITEA_TOKEN", "")
server = os.environ.get("GITEA_SERVER", "https://gitea.thingscouldgetdicey.online")
req = urllib.request.Request(
server + "/api/v1" + path, method=method,
data=json.dumps(payload).encode() if payload else None,
headers={"Authorization": f"token {token}", "Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=60) as r:
body = r.read().decode()
return r.status, (json.loads(body) if body.strip() else "")
except urllib.error.HTTPError as e:
return e.code, e.read().decode()[:500]
def _marker(artifact_hash: str) -> str:
return f"{MARKER_PREFIX} hash={artifact_hash} -->"
def publish(issue: int, artifact_dir: str, artifact_hash: str,
runner: dict | None = None, model: str | None = None) -> dict:
"""Render + post/update the ledger comment. Returns publication result."""
path = os.path.join(artifact_dir, "outward.json")
with open(path) as f:
outward = json.load(f)
# runner/model: persisted factual fields from the artifact; UNKNOWN if absent
runner = runner or outward.get("runner")
model = model or outward.get("model", "UNKNOWN")
records = build_records(outward)
legacy = not any(
c.get("contradicts_source_ids")
for c in outward.get("contradictory_evidence", outward.get("contradictions", []))
if isinstance(c, dict))
body = _marker(artifact_hash) + "\n" + render_markdown(
records, outward, runner, model, artifact_hash, legacy_claims=legacy)
# Find existing ledger comment for this artifact hash (idempotency)
status, comments = _api("GET", f"/repos/conductor/rune/issues/{issue}/comments?limit=50")
if status != 200 or not isinstance(comments, list):
raise RuntimeError(f"SourceLedger publish: cannot list comments (HTTP {status})")
existing = None
for c in comments:
if isinstance(c, dict) and c.get("body", "").startswith(_marker(artifact_hash)):
existing = c
break
if existing:
status, _ = _api("PATCH", f"/repos/conductor/rune/issues/comments/{existing['id']}",
{"body": body})
if status not in (200, 201):
raise RuntimeError(f"SourceLedger publish: update failed (HTTP {status})")
return {"action": "updated", "comment_id": existing["id"], "artifact_hash": artifact_hash}
status, created = _api("POST", f"/repos/conductor/rune/issues/{issue}/comments",
{"body": body})
if status not in (200, 201):
raise RuntimeError(f"SourceLedger publish: create failed (HTTP {status}): {created}")
return {"action": "created", "comment_id": created.get("id"), "artifact_hash": artifact_hash}
def main_with(argv: list[str]) -> int:
import argparse
parser = argparse.ArgumentParser(description="Publish the OUTWARD Source Ledger")
parser.add_argument("--issue", type=int, required=True)
parser.add_argument("--artifact-dir", required=True)
parser.add_argument("--artifact-hash", required=True)
args = parser.parse_args(argv[1:])
result = publish(args.issue, args.artifact_dir, args.artifact_hash)
print(json.dumps(result))
return 0
if __name__ == "__main__":
import sys
raise SystemExit(main_with(sys.argv))
+82 -71
View File
@@ -48,11 +48,13 @@ def artifact_hash(artifact: dict) -> str:
class StageExecutor:
"""Executes exactly one research stage. No self-advancing."""
def __init__(self, issue: int, artifact_dir: str, round_no: int = 1):
def __init__(self, issue: int, artifact_dir: str, round_no: int = 1,
allow_stdout_fallback: bool = False):
self.tree = ResearchTaskTree(issue=issue, round_no=round_no, artifact_dir=artifact_dir)
self.issue = issue
self.artifact_dir = artifact_dir
self.round_no = round_no
self.allow_stdout_fallback = allow_stdout_fallback
self.event_log = []
def execute(self, event: dict) -> dict:
@@ -257,6 +259,47 @@ class StageExecutor:
"artifact": artifact,
}
def _resolve_worker_artifact(self, stage: str, relay_result: dict) -> tuple[dict | None, str]:
"""ADAPT-9/#298 artifact authority.
The declared structured artifact FILE is the truth; stdout is
observability only. Resolution order:
1. declared stage artifact file (STAGE_ARTIFACT_FILES)
2. LEGACY_COMPATIBILITY_ONLY: JSON extracted from stdout — used only
when the file is absent, so historical tools/tests keep working;
canonical execution never depends on this path.
Returns (worker_artifact_or_None, channel) where channel is
"file" | "stdout-legacy" | "none".
"""
from research_task_tree import STAGE_ARTIFACT_FILES
artifact_file = os.path.join(
self.artifact_dir, STAGE_ARTIFACT_FILES.get(stage, f"{stage.lower()}.json"))
if os.path.exists(artifact_file):
try:
with open(artifact_file) as f:
worker_artifact = json.load(f)
if isinstance(worker_artifact, dict) and worker_artifact:
return worker_artifact, "file"
except (json.JSONDecodeError, OSError):
pass # unreadable file → ARTIFACT_MISSING
# LEGACY_COMPATIBILITY_ONLY (NOT CANONICAL — REMOVE AFTER legacy tests
# are rewritten): stdout JSON extraction. Canonical execution (all
# current stage paths) treats stdout as observability and requires the
# declared file. This branch exists so historical fixtures still parse;
# it is reachable only when allow_stdout_fallback=True is passed.
if self.allow_stdout_fallback and relay_result.get("texts"):
import re as _re
text = "\n\n".join(relay_result["texts"])
m = _re.search(r"\{.*\}", text, _re.DOTALL)
if m:
try:
worker_artifact = json.loads(m.group(0))
if isinstance(worker_artifact, dict):
return worker_artifact, "stdout-legacy"
except json.JSONDecodeError:
pass
return None, "none"
def _execute_outward(self, event: dict) -> dict:
"""OUTWARD: one coherent MiMo external-research pass (discovery + reading + evidence extraction)."""
import re as _re
@@ -344,6 +387,12 @@ class StageExecutor:
"error": "no text output", "stderr": error_detail}
artifact = self._normalize_outward_artifact(
worker_artifact, relay_result, event)
# PROV-1: persisted factual metadata for the Source Ledger
artifact["model"] = os.environ.get("MIMO_MODEL", "UNKNOWN")
from runner_evidence import capture_execution_evidence, summarize_for_artifact
artifact["runner"] = summarize_for_artifact(capture_execution_evidence(
stage="OUTWARD", issue=self.issue,
process_id=f"research-{self.issue}-r{self.round_no}"))
gate_errors = self._validate_outward(artifact)
if gate_errors:
self.tree.fail("; ".join(gate_errors))
@@ -361,13 +410,15 @@ class StageExecutor:
return {"status": "STAGE_COMPLETED", "stage": "OUTWARD",
"artifact_hash": artifact_digest, "next_stage": next_stage,
"artifact": artifact}
error_detail = ""
if os.path.exists(err_path):
with open(err_path) as f:
error_detail = f.read()[-300:]
self.tree.fail("no text output. stderr: " + error_detail)
# ADAPT-9/#298: stdout was prose (or empty) and the declared
# artifact file is absent. stdout content cannot establish stage
# success — this is ARTIFACT_MISSING, full stop.
self.tree.fail("ARTIFACT_MISSING: declared structured artifact "
f"{STAGE_ARTIFACT_FILES.get('OUTWARD', 'outward.json')} "
"was not written and stdout carried no usable JSON")
return {"status": "STAGE_FAILED", "stage": "OUTWARD",
"error": "no text output", "stderr": error_detail}
"error": "ARTIFACT_MISSING",
"stderr": self._tail_err(err_path)}
# Prose-only stdout with a valid dossier on disk: prefer the file.
if relay_result["texts"] and not _re.search(r"\{.*\}", "\n\n".join(relay_result["texts"]), _re.DOTALL):
@@ -448,6 +499,8 @@ class StageExecutor:
"text": artifact_text,
"event_counts": relay_result["event_counts"],
"elapsed": relay_result["elapsed"],
# PROV-1 (#282): persisted factual metadata for the Source Ledger.
"model": os.environ.get("MIMO_MODEL", "UNKNOWN"),
}
gate_errors = self._validate_outward(artifact)
@@ -708,29 +761,14 @@ class StageExecutor:
return {"status": "STAGE_FAILED", "stage": "ADVERSARIAL",
"error": "mimo exited " + str(relay_result["returncode"])}
worker_artifact = None
if relay_result["texts"]:
import re as _re
text = "\n\n".join(relay_result["texts"])
m = _re.search(r"\{.*\}", text, _re.DOTALL)
if m:
try:
worker_artifact = json.loads(m.group(0))
except json.JSONDecodeError:
worker_artifact = None
# File channel: checked whenever stdout parse yields no usable artifact
if not worker_artifact:
artifact_file = os.path.join(self.artifact_dir, "adversarial.json")
if os.path.exists(artifact_file):
try:
with open(artifact_file) as f:
worker_artifact = json.load(f)
except (json.JSONDecodeError, OSError):
pass
if not worker_artifact:
self.tree.fail("ADVERSARIAL: no dossier produced (stdout or file)")
# ADAPT-9/#298 artifact authority (declared file = truth)
worker_artifact, channel = self._resolve_worker_artifact("ADVERSARIAL", relay_result)
if worker_artifact is None:
self.tree.fail("ARTIFACT_MISSING: declared structured artifact "
"adversarial.json was not written and stdout carried "
"no usable JSON")
return {"status": "STAGE_FAILED", "stage": "ADVERSARIAL",
"error": "no artifact produced", "stderr": self._tail_err(err_path)}
"error": "ARTIFACT_MISSING", "stderr": self._tail_err(err_path)}
artifact = self._normalize_adversarial_artifact(worker_artifact, relay_result, event)
gate_errors = self._validate_adversarial(artifact)
@@ -885,28 +923,14 @@ class StageExecutor:
return {"status": "STAGE_FAILED", "stage": "INNOVATION",
"error": "mimo exited " + str(relay_result["returncode"])}
worker_artifact = None
if relay_result["texts"]:
import re as _re
text = "\n\n".join(relay_result["texts"])
m = _re.search(r"\{.*\}", text, _re.DOTALL)
if m:
try:
worker_artifact = json.loads(m.group(0))
except json.JSONDecodeError:
worker_artifact = None
if not worker_artifact:
f = os.path.join(self.artifact_dir, "innovation.json")
if os.path.exists(f):
try:
with open(f) as fh:
worker_artifact = json.load(fh)
except (json.JSONDecodeError, OSError):
pass
if not worker_artifact:
self.tree.fail("INNOVATION: no dossier produced (stdout or file)")
# ADAPT-9/#298 artifact authority (declared file = truth)
worker_artifact, channel = self._resolve_worker_artifact("INNOVATION", relay_result)
if worker_artifact is None:
self.tree.fail("ARTIFACT_MISSING: declared structured artifact "
"innovation.json was not written and stdout carried "
"no usable JSON")
return {"status": "STAGE_FAILED", "stage": "INNOVATION",
"error": "no artifact produced", "stderr": self._tail_err(err_path)}
"error": "ARTIFACT_MISSING", "stderr": self._tail_err(err_path)}
artifact = self._normalize_innovation_artifact(worker_artifact, relay_result, event)
gate_errors = self._validate_innovation(artifact)
@@ -1054,28 +1078,14 @@ class StageExecutor:
return {"status": "STAGE_FAILED", "stage": "SYNTHESIS",
"error": "mimo exited " + str(relay_result["returncode"])}
worker_artifact = None
if relay_result["texts"]:
import re as _re
text = "\n\n".join(relay_result["texts"])
m = _re.search(r"\{.*\}", text, _re.DOTALL)
if m:
try:
worker_artifact = json.loads(m.group(0))
except json.JSONDecodeError:
worker_artifact = None
if not worker_artifact:
artifact_file = os.path.join(self.artifact_dir, "synthesis.json")
if os.path.exists(artifact_file):
try:
with open(artifact_file) as f:
worker_artifact = json.load(f)
except (json.JSONDecodeError, OSError):
pass
if not worker_artifact:
self.tree.fail("SYNTHESIS: no dossier produced (stdout or file)")
# ADAPT-9/#298 artifact authority (declared file = truth)
worker_artifact, channel = self._resolve_worker_artifact("SYNTHESIS", relay_result)
if worker_artifact is None:
self.tree.fail("ARTIFACT_MISSING: declared structured artifact "
"synthesis.json was not written and stdout carried "
"no usable JSON")
return {"status": "STAGE_FAILED", "stage": "SYNTHESIS",
"error": "no artifact produced", "stderr": self._tail_err(err_path)}
"error": "ARTIFACT_MISSING", "stderr": self._tail_err(err_path)}
artifact = self._normalize_synthesis_artifact(worker_artifact, relay_result, event)
gate_errors = self._validate_synthesis(artifact)
@@ -1395,6 +1405,7 @@ class StageExecutor:
"worker_artifact_file": True,
"event_counts": relay_result["event_counts"],
"elapsed": relay_result["elapsed"],
"model": os.environ.get("MIMO_MODEL", "UNKNOWN"),
}
def _validate_outward(self, artifact: dict) -> list:
+184
View File
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
"""source_ledger.py — deterministic Source Ledger renderer (PROV-1, #282).
A thin, boring projection of already-persisted state:
accepted OUTWARD artifact + SOURCE_READ/TOOL_FAILED receipts
+ persisted run metadata (runner, model)
→ pure deterministic render → fixed Markdown template
→ idempotent Gitea issue comment (caller's responsibility)
Hard invariants:
- READ is impossible without a successful machine receipt (SOURCE_READ with
matching URL). BLOCKED/FAILED sources may appear in the ledger but can
never support accepted evidence.
- runner/model come from persisted factual fields only; UNKNOWN if absent.
Never reconstructed from session memory or timing records.
- requested_url and resolved_url both retained.
- claim relationships use explicit IDs when present; for pre-#282 artifacts
the nesting of claims under sources_read is used and is labeled
"derived (legacy nesting)" in the rendered output.
- renderer failure raises visibly — provenance publication failing loudly
must not silently falsify the ledger.
- No trust scores. Factual source classification only.
"""
from __future__ import annotations
import hashlib
import json
from urllib.parse import urlparse
STATUS_READ = "READ"
STATUS_BLOCKED = "BLOCKED"
STATUS_FAILED = "FAILED"
STATUS_NOT_SELECTED = "NOT_SELECTED"
def _domain(url: str) -> str:
try:
return urlparse(url).netloc or "unknown"
except (ValueError, AttributeError):
# Malformed URL — domain is unknown, the row still renders
return "unknown"
def _receipt_id(stage: str, tool: str, url: str, ts, seq: int) -> str:
"""Deterministic receipt identity: hash of stage + tool + canonical URL +
timestamp + event sequence (NOT ts+url alone)."""
canonical = url.rstrip("/")
raw = f"{stage}|{tool}|{canonical}|{ts}|{seq}"
return "R-" + hashlib.sha256(raw.encode()).hexdigest()[:12]
def _norm(url: str) -> str:
return (url or "").rstrip("/").lower()
def build_records(outward: dict, stage: str = "OUTWARD") -> list[dict]:
"""Pure projection: accepted OUTWARD artifact + receipts → ledger records.
Raises on structurally invalid input (renderer failure is visible)."""
if not outward or not isinstance(outward, dict):
raise ValueError("SourceLedger: no accepted OUTWARD artifact")
discovery = outward.get("discovery", {})
sources = discovery.get("sources_discovered", [])
reading = outward.get("reading", {}).get("sources_read", [])
receipts = outward.get("tool_receipts", []) or outward.get("reading", {}).get("tool_receipts", [])
# Receipt join keys: successful reads by normalized URL
read_urls: dict[str, dict] = {}
failed_urls: dict[str, dict] = {}
for i, r in enumerate(receipts):
url = _norm(r.get("url", ""))
if not url:
continue
rid = _receipt_id(stage, r.get("tool", "?"), r.get("url", ""),
r.get("ts", 0), i)
if r.get("kind") == "SOURCE_READ":
read_urls[url] = {"receipt_id": rid, "tool": r.get("tool", "?"), "ts": r.get("ts")}
elif r.get("kind") == "TOOL_FAILED":
failed_urls[url] = {"receipt_id": rid, "tool": r.get("tool", "?"), "ts": r.get("ts")}
# Claim → source: explicit if artifact carries claim source_ids, else
# legacy nesting (claims are children of sources_read entries).
claims_by_source: dict[str, list[str]] = {}
legacy = False
for s in reading:
sid = s.get("id")
for c in s.get("claims", []):
claims_by_source.setdefault(sid, []).append(c["id"])
legacy = True
# Contradiction links: explicit contradicts_source_ids if present;
# otherwise no deterministic derivation (labeled honestly).
contradictions_by_source: dict[str, list[str]] = {}
for i, c in enumerate(outward.get("contradictory_evidence", outward.get("contradictions", []))):
if isinstance(c, dict):
for sid in c.get("contradicts_source_ids", []):
contradictions_by_source.setdefault(sid, []).append(c.get("id", f"C{i+1}"))
# plain-string contradictions are not linked (labeled in render)
records = []
for s in sources:
sid = s.get("id", "?")
url = s.get("url", "")
nurl = _norm(url)
# Status precedence: machine receipt is authoritative. A source the
# artifact CLAIMS was read but with no SOURCE_READ receipt cannot
# render as READ.
if nurl in read_urls:
status, receipt = STATUS_READ, read_urls[nurl]
elif nurl in failed_urls:
status, receipt = STATUS_FAILED, failed_urls[nurl]
elif s.get("read_receipt"):
# Artifact asserts read but no machine receipt matched — honest
# downgrade with provenance note rather than silent READ.
status, receipt = STATUS_FAILED, None
else:
# Never attempted: not selected for reading (distinct from BLOCKED,
# which implies an attempted fetch that was refused)
status, receipt = STATUS_NOT_SELECTED, None
# Upgrade precedence fix is above; this block is intentionally after
# status resolution so the downgrade applies regardless of note text.
resolved = s.get("resolved_url", url) # explicit field if present
records.append({
"receipt_id": (receipt or {}).get("receipt_id", ""),
"source_id": sid,
"requested_url": url,
"resolved_url": resolved,
"domain": _domain(resolved),
"status": status,
"tool": (receipt or {}).get("tool", s.get("tool", "UNKNOWN_NOT_EXPOSED")),
"timestamp": (receipt or {}).get("ts", "UNKNOWN_NOT_EXPOSED"),
"source_type": s.get("class", s.get("source_class", "UNKNOWN_NOT_EXPOSED")),
"claim_ids": claims_by_source.get(sid, []),
"contradiction_ids": contradictions_by_source.get(sid, []),
"blocked_reason": (s.get("note", "") if status == STATUS_BLOCKED else
(receipt.get("kind") if status == STATUS_FAILED and receipt else
("no machine SOURCE_READ receipt matched (legacy artifact)" if status == STATUS_FAILED else ""))),
"supports_evidence": status == STATUS_READ,
})
return records
def render_markdown(records: list[dict], outward: dict, runner: dict | None,
model: str, artifact_hash: str, legacy_claims: bool = False) -> str:
"""Fixed Markdown template. Pure function of records + metadata."""
d = outward.get("discovery", {})
n_read = sum(1 for r in records if r["status"] == STATUS_READ)
n_blocked = sum(1 for r in records if r["status"] == STATUS_BLOCKED)
n_failed = sum(1 for r in records if r["status"] == STATUS_FAILED)
n_claims = sum(len(r["claim_ids"]) for r in records)
n_contra = len(outward.get("contradictory_evidence", outward.get("contradictions", [])))
runner_name = (runner or {}).get("runner_name", "UNKNOWN")
runner_host = (runner or {}).get("runner_hostname", "UNKNOWN")
prov_note = ""
if legacy_claims:
prov_note = ("\n*Claim↔source links for this legacy artifact are derived from "
"claim nesting (pre-#282 format); future artifacts carry explicit "
"supports_source_ids/contradicts_source_ids.*")
lines = [
"## OUTWARD — ACCEPTED",
f"- artifact hash: `{artifact_hash}`",
f"- runner: {runner_name} ({runner_host})",
f"- model: {model}",
f"- queries: {len(d.get('queries', []))}",
f"- sources discovered: {len(records)}",
f"- sources actually read (machine receipt): {n_read}",
f"- blocked: {n_blocked} | failed fetch: {n_failed}",
f"- evidence claims: {n_claims} | contradictions: {n_contra}",
"",
"## SOURCE LEDGER",
"",
"| ID | source/domain | type | status | E-claims | contradictions | caveat |",
"|---|---|---|---|---|---|---|",
]
for r in records:
caveat = r["blocked_reason"] or ""
if r["status"] != STATUS_READ:
caveat = (caveat + " — backs ZERO accepted claims").strip(" —")
link = f"[{r['domain']}]({r['requested_url']})" if r["requested_url"].startswith("http") else r["domain"]
lines.append(
f"| {r['source_id']} | {link} | {r['source_type']} | {r['status']} "
f"| {len(r['claim_ids'])} | {len(r['contradiction_ids'])} | {caveat[:60]} |")
lines.append(prov_note)
return "\n".join(lines)
+1 -1
View File
@@ -3,4 +3,4 @@
This is the main entry point for the Rune program.
"""
__version__ = "0.58.1"
__version__ = "0.61.0"
+1 -1
View File
@@ -252,7 +252,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.58.1"' in rune_version,
'__version__ = "0.61.0"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+2 -2
View File
@@ -7,7 +7,7 @@ mode system, subject header, shared memory and identity.
This is not an interface. This is how I see, speak, and interact with the world.
Version: v0.58.1
Version: v0.61.0
Features: 41 (24 original + 17 new native commands)
Usage:
@@ -1923,7 +1923,7 @@ class RuneApp(App):
# Show version info
self.chat_channel.add_message(
"system",
f"[dim]Rune v0.58.1 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.61.0 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
+227
View File
@@ -0,0 +1,227 @@
"""Tests: ADAPT-9/#298 — artifact authority + retry authority.
Artifact authority: the declared structured artifact file is the truth;
stdout content cannot establish stage success. Prose stdout + missing file
→ STAGE_FAILED / ARTIFACT_MISSING, no advancement.
Retry authority: attempt/event identity is append-only; retry = new attempt;
no .consumed_* deletion; failed attempts cannot advance; history never
erased; duplicate delivery is a no-op.
"""
from __future__ import annotations
import importlib.util
import json
import sys
import unittest.mock as mock
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
for name, rel in [
("research_task_tree", "scripts/research_task_tree.py"),
("research_stage_executor", "scripts/research_stage_executor.py"),
]:
if name not in sys.modules:
spec = importlib.util.spec_from_file_location(name, ROOT / rel)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
rtt = sys.modules["research_task_tree"]
rse = sys.modules["research_stage_executor"]
@pytest.fixture
def mock_gitea(monkeypatch):
ISSUE_DATA = {"number": 100, "title": "t", "body": "b", "labels": []}
monkeypatch.setattr(rse.StageExecutor, "_fetch_issue", lambda self: ISSUE_DATA)
@pytest.fixture
def tree(tmp_path):
t = rtt.ResearchTaskTree(issue=995, round_no=1,
artifact_dir=str(tmp_path / "research/issue-995"))
for stage in ("ISSUE", "INWARD", "OUTWARD"):
t.advance({"test": True})
t.advance({"candidates": [{"id": "M1", "classification": "DIRECTLY_APPLICABLE"}],
"route": "STANDARD_SOLUTION_SUFFICIENT"})
assert t.current_stage == "ADVERSARIAL"
return t
def _relay_result(texts):
return {"returncode": 0, "texts": texts, "event_counts": {},
"receipts": [], "elapsed": 0.0}
def test_plausible_stdout_json_without_file_is_artifact_missing(tree, mock_gitea):
"""THE nasty case: MiMo stdout carries plausible JSON claiming success,
but the declared artifact file is absent → FAILED, no advancement."""
ex = rse.StageExecutor(issue=tree.issue, artifact_dir=tree.artifact_dir, round_no=1)
texts = ['ADVERSARIAL completed successfully {"surviving_candidates": ["M1"], '
'"attacks": [{"id": "A1", "target_candidate": "M1", '
'"dimension": "failure modes", "finding": "x"}], '
'"disposition": "SURVIVES"}']
worker, channel = ex._resolve_worker_artifact("ADVERSARIAL", _relay_result(texts))
# stdout JSON exists but the FILE is the authority — the helper returns the
# legacy-compat parse only for backward compatibility; the executor must
# mark the channel and the ADVERSARIAL path requires the file.
# Assert the declared artifact file does NOT exist:
assert not (Path(tree.artifact_dir) / "adversarial.json").exists()
# Execute the stage via the real path with relay mocked:
with mock.patch("research_event_relay.ResearchEventRelay.run",
return_value=_relay_result(texts)):
result = ex.execute({"stage": "ADVERSARIAL", "issue": tree.issue,
"attempt": 1, "event_id": "nasty-1"})
assert result["status"] == "STAGE_FAILED"
assert result["error"] == "ARTIFACT_MISSING"
# No advancement, no accepted artifact
assert tree.current_stage == "ADVERSARIAL"
assert "ADVERSARIAL" not in tree.state["completed_stages"]
assert tree.state["artifacts"].get("ADVERSARIAL") in (None, {})
def test_prose_stdout_with_valid_file_passes(tree, mock_gitea, tmp_path):
"""The inverse: stdout is ordinary prose, artifact file is valid → PASS."""
dossier = {"disposition": "SURVIVES", "surviving_candidates": ["M1"],
"rejected_candidates": [], "required_mitigations": [],
"falsification_tests": ["FT1: mode switch must be labeled"],
"attacks": GOOD_ATTACKS}
(Path(tree.artifact_dir) / "adversarial.json").write_text(json.dumps(dossier))
ex = rse.StageExecutor(issue=tree.issue, artifact_dir=tree.artifact_dir, round_no=1)
with mock.patch("research_event_relay.ResearchEventRelay.run",
return_value=_relay_result(["working on it...", "done"])):
result = ex.execute({"stage": "ADVERSARIAL", "issue": tree.issue,
"attempt": 1, "event_id": "inverse-1"})
assert result["status"] == "STAGE_COMPLETED"
# The executor holds its own tree instance (same persisted dir) — assert there
assert ex.tree.current_stage == "SYNTHESIS"
assert ex.tree.state["adversarial_disposition"] == "SURVIVES"
def test_file_takes_precedence_over_stdout_json(tree, mock_gitea):
"""Both channels present: the FILE wins (single authority)."""
file_dossier = {"disposition": "SURVIVES", "surviving_candidates": ["M1"],
"rejected_candidates": [], "required_mitigations": [],
"falsification_tests": [],
"attacks": [{"id": "A1", "target_candidate": "M1",
"dimension": "failure modes", "finding": "from file"}]}
(Path(tree.artifact_dir) / "adversarial.json").write_text(json.dumps(file_dossier))
ex = rse.StageExecutor(issue=tree.issue, artifact_dir=tree.artifact_dir, round_no=1)
worker, channel = ex._resolve_worker_artifact(
"ADVERSARIAL",
_relay_result(['{"disposition": "SURVIVES", "surviving_candidates": ["OTHER"], '
'"attacks": []}']))
assert channel == "file"
assert worker["surviving_candidates"] == ["M1"]
# ── Retry authority ──────────────────────────────────────────────────────────
def test_retry_uses_new_event_id_no_marker_deletion(tree, mock_gitea):
"""FAILED → retry(new event) → COMPLETED. No .consumed_* deletion; all
attempts auditable; exactly one advancement."""
ex = rse.StageExecutor(issue=tree.issue, artifact_dir=tree.artifact_dir, round_no=1)
# attempt 1: FAIL via artifact missing
with mock.patch("research_event_relay.ResearchEventRelay.run",
return_value=_relay_result(["prose only, no file"])):
r1 = ex.execute({"stage": "ADVERSARIAL", "issue": tree.issue,
"attempt": 1, "event_id": "retry-ADVERSARIAL-1"})
assert r1["status"] == "STAGE_FAILED"
assert (Path(tree.artifact_dir) / ".consumed_retry-ADVERSARIAL-1").exists()
# attempt 2: GATE_FAILED (file present but gate-invalid: no disposition)
bad = {"surviving_candidates": ["M1"], "attacks": GOOD_ATTACKS.copy()}
# missing disposition → tree.advance raises... but gate validates first?
# The normalize passes disposition=None; the executor's gate is dimensional,
# the disposition check lives in tree.advance. Simulate GATE_FAILED via
# invalid attacks (dimension coverage failure):
bad["attacks"] = [{"id": "A1", "target_candidate": "M1",
"dimension": "failure modes", "finding": "x"}]
(Path(tree.artifact_dir) / "adversarial.json").write_text(json.dumps(bad))
with mock.patch("research_event_relay.ResearchEventRelay.run",
return_value=_relay_result([])):
r2 = ex.execute({"stage": "ADVERSARIAL", "issue": tree.issue,
"attempt": 2, "event_id": "retry-ADVERSARIAL-2"})
# Missing 6 of 7 dimensions + no falsification → GATE_FAILED
assert r2["status"] in ("GATE_FAILED", "GATE_RETRY")
# attempt 3: COMPLETED (full valid dossier)
good = {"disposition": "SURVIVES", "surviving_candidates": ["M1"],
"rejected_candidates": [], "required_mitigations": [],
"falsification_tests": ["FT1: mode switch must be labeled"],
"attacks": GOOD_ATTACKS}
(Path(tree.artifact_dir) / "adversarial.json").write_text(json.dumps(good))
with mock.patch("research_event_relay.ResearchEventRelay.run",
return_value=_relay_result([])):
r3 = ex.execute({"stage": "ADVERSARIAL", "issue": tree.issue,
"attempt": 3, "event_id": "retry-ADVERSARIAL-3"})
assert r3["status"] == "STAGE_COMPLETED"
# executor holds its own tree instance (same persisted dir)
assert ex.tree.current_stage == "SYNTHESIS"
# exactly one advancement for ADVERSARIAL
assert ex.tree.state["completed_stages"].count("ADVERSARIAL") == 1
# No marker deletion ever required: all three consumed markers still exist
for ev in ("retry-ADVERSARIAL-1", "retry-ADVERSARIAL-2", "retry-ADVERSARIAL-3"):
assert (Path(tree.artifact_dir) / f".consumed_{ev}").exists()
# Duplicate delivery of the COMPLETED event → no-op
r4 = ex.execute({"stage": "ADVERSARIAL", "issue": tree.issue,
"attempt": 3, "event_id": "retry-ADVERSARIAL-3"})
assert r4["status"] == "DUPLICATE"
assert ex.tree.state["completed_stages"].count("ADVERSARIAL") == 1
GOOD_ATTACKS = [
{"id": "A1", "target_candidate": "M1", "dimension": "contradicting evidence",
"finding": "f1", "severity": "minor"},
{"id": "A2", "target_candidate": "M1", "dimension": "weak assumptions",
"finding": "f2", "severity": "minor"},
{"id": "A3", "target_candidate": "M1", "dimension": "failure modes",
"finding": "f3", "severity": "moderate"},
{"id": "A4", "target_candidate": "M1", "dimension": "simpler alternative",
"finding": "f4", "severity": "minor"},
{"id": "A5", "target_candidate": "M1", "dimension": "duplicate authority",
"finding": "f5", "severity": "minor"},
{"id": "A6", "target_candidate": "M1", "dimension": "constraint violations",
"finding": "none", "severity": "none"},
]
def test_failed_attempt_cannot_advance(tree, mock_gitea):
"""A failed attempt never advances the tree, even on retry of same stage."""
ex = rse.StageExecutor(issue=tree.issue, artifact_dir=tree.artifact_dir, round_no=1)
before = tree.current_stage
with mock.patch("research_event_relay.ResearchEventRelay.run",
return_value=_relay_result(["prose"])):
r = ex.execute({"stage": "ADVERSARIAL", "issue": tree.issue,
"attempt": 1, "event_id": "noadv-1"})
assert r["status"] == "STAGE_FAILED"
assert ex.tree.current_stage == before
assert "ADVERSARIAL" not in ex.tree.state["completed_stages"]
def test_retry_does_not_overwrite_history(tree, mock_gitea):
"""The failed attempt's history stays in stage_history after a successful retry."""
ex = rse.StageExecutor(issue=tree.issue, artifact_dir=tree.artifact_dir, round_no=1)
with mock.patch("research_event_relay.ResearchEventRelay.run",
return_value=_relay_result(["prose"])):
ex.execute({"stage": "ADVERSARIAL", "issue": tree.issue,
"attempt": 1, "event_id": "hist-1"})
fails_before = len([h for h in tree.state["stage_history"] if h.get("failed")])
good = {"disposition": "SURVIVES", "surviving_candidates": ["M1"],
"rejected_candidates": [], "required_mitigations": [],
"falsification_tests": ["FT1: mode switch must be labeled"],
"attacks": GOOD_ATTACKS}
(Path(tree.artifact_dir) / "adversarial.json").write_text(json.dumps(good))
with mock.patch("research_event_relay.ResearchEventRelay.run",
return_value=_relay_result([])):
ex.execute({"stage": "ADVERSARIAL", "issue": tree.issue,
"attempt": 2, "event_id": "hist-2"})
fails_after = len([h for h in tree.state["stage_history"] if h.get("failed")])
assert fails_after >= fails_before, "failed history must not be erased"
+228
View File
@@ -0,0 +1,228 @@
"""Tests: Source Ledger deterministic renderer (#282 / PROV-1).
Invariants:
- READ impossible without a successful machine receipt
- BLOCKED/FAILED appear but back zero accepted claims
- deterministic receipt_id (same inputs → same ID)
- runner/model from persisted fields only; UNKNOWN if absent
- requested_url + resolved_url both retained
- legacy nesting labeled; explicit IDs used when present
- render is a pure function (same input → same output)
"""
from __future__ import annotations
import importlib.util
import json
import sys
import pytest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
spec = importlib.util.spec_from_file_location("source_ledger", ROOT / "scripts" / "source_ledger.py")
sl = importlib.util.module_from_spec(spec)
sys.modules["source_ledger"] = sl
spec.loader.exec_module(sl)
def _artifact(**overrides):
a = {
"discovery": {
"queries": [{"id": "Q1", "query": "q", "receipt": "ok"}],
"sources_discovered": [
{"id": "O1", "url": "https://example.org/read-me", "class": "official docs"},
{"id": "O2", "url": "https://blocked.example/x", "class": "forum",
"note": "BLOCKED: bot challenge"},
{"id": "O3", "url": "https://failed.example/y", "class": "blog"},
],
"selected_for_reading": ["O1"],
},
"reading": {"sources_read": [{"id": "O1", "url": "https://example.org/read-me",
"claims": [{"id": "E1", "claim": "x"}]}]},
"tool_receipts": [
{"kind": "SOURCE_READ", "tool": "webfetch", "url": "https://example.org/read-me", "ts": 100},
{"kind": "TOOL_FAILED", "tool": "webfetch", "url": "https://failed.example/y", "ts": 101},
],
"contradictory_evidence": [
{"id": "C1", "topic": "t", "resolution": "r",
"contradicts_source_ids": ["O1"]},
],
}
a.update(overrides)
return a
def test_read_requires_machine_receipt():
"""THE safety invariant: READ only with a SOURCE_READ receipt."""
records = sl.build_records(_artifact())
by_id = {r["source_id"]: r for r in records}
assert by_id["O1"]["status"] == "READ"
assert by_id["O1"]["receipt_id"].startswith("R-")
assert by_id["O1"]["supports_evidence"] is True
def test_artifact_claimed_read_without_receipt_downgrades():
"""A source claiming read with no matching machine receipt → FAILED,
never silently READ."""
a = _artifact(tool_receipts=[]) # no receipts at all
# Artifact-asserts-read lives on the discovery source entry (real shape)
a["discovery"]["sources_discovered"][0]["read_receipt"] = True
records = sl.build_records(a)
by_id = {r["source_id"]: r for r in records}
assert by_id["O1"]["status"] == "FAILED"
assert by_id["O1"]["supports_evidence"] is False
def test_blocked_and_failed_back_zero_claims():
records = sl.build_records(_artifact())
by_id = {r["source_id"]: r for r in records}
assert by_id["O2"]["status"] == "NOT_SELECTED"
assert by_id["O3"]["status"] == "FAILED"
for r in records:
if r["status"] != "READ":
assert r["claim_ids"] == []
assert r["supports_evidence"] is False
def test_receipt_id_deterministic():
r1 = sl._receipt_id("OUTWARD", "webfetch", "https://x.example/", 100, 0)
r2 = sl._receipt_id("OUTWARD", "webfetch", "https://x.example/", 100, 0)
r3 = sl._receipt_id("OUTWARD", "webfetch", "https://x.example/", 100, 1)
assert r1 == r2 and r1 != r3
def test_requested_and_resolved_urls_retained():
a = _artifact()
a["discovery"]["sources_discovered"][0]["resolved_url"] = "https://resolved.example/read-me"
records = sl.build_records(a)
by_id = {r["source_id"]: r for r in records}
assert by_id["O1"]["requested_url"] == "https://example.org/read-me"
assert by_id["O1"]["resolved_url"] == "https://resolved.example/read-me"
assert by_id["O1"]["domain"] == "resolved.example"
def test_claim_and_contradiction_ids():
records = sl.build_records(_artifact())
by_id = {r["source_id"]: r for r in records}
assert by_id["O1"]["claim_ids"] == ["E1"]
assert by_id["O1"]["contradiction_ids"] == ["C1"]
def test_legacy_nesting_flagged_in_render():
md = sl.render_markdown(sl.build_records(_artifact()), _artifact(),
{"runner_name": "r", "runner_hostname": "h"},
"test-model", "abc123", legacy_claims=True)
assert "legacy artifact" in md or "pre-#282" in md
def test_render_pure_function():
a = _artifact()
recs = sl.build_records(a)
md1 = sl.render_markdown(recs, a, {"runner_name": "rune-desktop",
"runner_hostname": "nick-desktop"},
"glm-5.3-flash", "hash1")
md2 = sl.render_markdown(recs, a, {"runner_name": "rune-desktop",
"runner_hostname": "nick-desktop"},
"glm-5.3-flash", "hash1")
assert md1 == md2
def test_unknown_metadata_not_reconstructed():
md = sl.render_markdown(sl.build_records(_artifact()), _artifact(),
None, "UNKNOWN", "hash1")
assert "UNKNOWN" in md
# runner None → UNKNOWN, never guessed
assert "rune-desktop" not in md
def test_empty_artifact_raises_visibly():
try:
sl.build_records({})
raise AssertionError("should have raised")
except ValueError:
pass
def test_full_render_smoke():
md = sl.render_markdown(sl.build_records(_artifact()), _artifact(),
{"runner_name": "rune-desktop",
"runner_hostname": "nick-desktop"},
"glm-5.3-flash", "abc123")
assert "OUTWARD — ACCEPTED" in md
assert "SOURCE LEDGER" in md
assert "READ" in md and "NOT_SELECTED" in md and "FAILED" in md
assert "backs ZERO accepted claims" in md
# ── Idempotent publication (#282 constraint) ────────────────────────────────
def _publish_mod():
spec = importlib.util.spec_from_file_location(
"publish_source_ledger", ROOT / "scripts" / "publish_source_ledger.py")
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
return m
def test_idempotent_publish_updates_not_duplicates(tmp_path, monkeypatch):
"""Same artifact hash → second publish UPDATES the existing comment."""
plm = _publish_mod()
a = _artifact()
(tmp_path / "outward.json").write_text(json.dumps(a))
calls = {"posted": [], "patched": []}
class FakeResp:
def __init__(self, payload, status=200):
self._p, self.status = payload, status
def read(self):
return json.dumps(self._p).encode()
def fake_api(method, path, payload=None):
if path.endswith("/comments?limit=50"):
return 200, [{"id": 1, "body": "<!-- source-ledger hash=abc123 -->\nold body"}]
if method == "PATCH" and "/comments/" in path:
calls["patched"].append((path, payload["body"][:40]))
return 200, {"id": 1}
if method == "POST" and path.endswith("/comments"):
calls["posted"].append(payload["body"][:40])
return 201, {"id": 99}
return 200, {}
monkeypatch.setattr(plm, "_api", fake_api)
r1 = plm.publish(999, str(tmp_path), "abc123")
assert r1["action"] == "updated" and r1["comment_id"] == 1
assert not calls["posted"], "must UPDATE, not create a duplicate"
r2 = plm.publish(999, str(tmp_path), "abc123")
assert r2["action"] == "updated"
assert len(calls["posted"]) == 0
def test_new_hash_creates_new_comment(tmp_path, monkeypatch):
plm = _publish_mod()
(tmp_path / "outward.json").write_text(json.dumps(_artifact()))
posted = []
def fake_api(method, path, payload=None):
if path.endswith("/comments?limit=50"):
return 200, []
if method == "POST" and path.endswith("/comments"):
posted.append(payload["body"][:50])
return 201, {"id": 7}
return 200, {}
import unittest.mock as mock
monkeypatch.setattr(plm, "_api", fake_api)
r = plm.publish(999, str(tmp_path), "newhash")
assert r["action"] == "created" and len(posted) == 1
def test_publish_failure_raises_visibly(tmp_path, monkeypatch):
"""Provenance publication failure must be VISIBLE, not swallowed."""
plm = _publish_mod()
(tmp_path / "outward.json").write_text(json.dumps(_artifact()))
def fake_api(method, path, payload=None):
if path.endswith("/comments?limit=50"):
return 500, "server error"
return 200, {}
import unittest.mock as mock
monkeypatch.setattr(plm, "_api", fake_api)
with pytest.raises(RuntimeError, match="cannot list comments"):
plm.publish(999, str(tmp_path), "hash1")