Compare commits

...
38 Commits
Author SHA1 Message Date
conductor 1f96055b69 chore(release): auto-bump version to 0.66.68 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 19:11:06 +01:00
Nick 923a21ba0b chore(store): 17E programme proof records — durable programme + closed stage contracts 2026-09-15 19:10:46 +01:00
Nick 6348386869 fix(programme): truthful failure phase + typing cleanup (17E review)
Failed stages now land in phase=failed (legal building→failed
transition, evidence checkpoint on contract) instead of a swallowed
close attempt. S110/BLE001 eliminated — no exception swallowing in the
failure path. Ratchet-clean.
2026-09-15 19:10:46 +01:00
Nick 55002ad838 feat(programme): canonical continuation wiring + live 4-stage proof (T17 17D/17E)
programme_continuation was TEST-PROVEN but UNWIRED (16M finding). Now:
- scaffolding_stage_runner: binds stages 1:1 to Scaffolding contracts,
  drives the real Construction Interface lifecycle; async stage work
  runs under the event bridge (WAITING_EVENT is a state, not a stop);
  canonical phase re-read after EVERY transition (live finding: stale
  local contract refs silently stopped the draft→building chain);
  work evidence recorded ON the contract before close.
- run_construction_programme: persist → canary → next around EVERY
  transition; durable programme record reloadable after restart.
- canary extended: PROGRAMME_COMPLETE_WITH_FAILURES (all-terminal with
  failed stages closes honestly instead of hanging UNKNOWN).

LIVE PROOF (17E, real bridge, 4.3s): governance stage → async stage
(event wait, real daemon) → deliberately failing stage (isolated,
programme continued) → final stage → PROGRAMME_COMPLETE_WITH_FAILURES;
all 4 contracts closed with checkpoints. 7 wiring held-outs: evidence-
on-contract, failure isolation, restart from durable record, duplicate
signal idempotency, already-closed skip, callable work, missing
contract fails honestly. 15M verdict enum extended.
2026-09-15 19:10:46 +01:00
conductor 6684289a8f chore(release): auto-bump version to 0.66.67 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 18:41:59 +01:00
Nick e25ad0f964 fix(bridge): re-land cross-user FIFO chmod + regression guards (T17 17C)
Root cause of every post-T16 production wait sleeping to its deadline:
(a) the T16 os.chmod(fifo, 0o666) was lost in a rebase resolution —
FIFOs landed 644 (umask 022), the daemon (different user) got EACCES on
the resume write, rc=1 → RESUME_UNVERIFIED; same-user tests passed.
(b) waitstore _atomic_write (daemon snapshot 0b8f384) left mkstemp 0600
records the runner-user waiter could not read back.

Proofs: replay path exercised production-realistically against real
completed CI (event_replayed, 4.6s, authority re-read decided); full
cross-user delivery repro (runner user) now wakes in 2.05s; two
regression guards (mode assertion + cross-user delivery on the real
daemon). Daemon invariants 45/45 + 36/36.
2026-09-15 18:41:32 +01:00
Nick c9dee14c62 docs(t16): final tranche evidence — 16S/T/U/V/X/Y findings + 16Z convergence record 2026-09-15 17:56:15 +01:00
conductor fe9d154778 chore(release): auto-bump version to 0.66.66 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 17:39:29 +01:00
Nick 2a4f9c8367 chore(spine): rebuild after 16R contract registration 2026-09-15 17:38:56 +01:00
Nick 7ddfa7eee7 fix(maintenance): canary-suite glob points at src/rune (T16 16Q/16R)
16Q: the job looped shared/canary_*.py — real canaries live in
src/rune/; it silently no-opped since the canaries moved.
16R: this bounded Rune-internal improvement was executed THROUGH the
retroactive Scaffolding flow — Need → contract 16R-maintenance-canary-
glob (approved→building→verifying→closed) with acceptance verified
independently (YAML parses, glob fixed, 24 canary files at target) —
proving the 16M/16N map drives actual construction, not just docs.
2026-09-15 17:38:56 +01:00
conductor 986b450bc2 chore(release): auto-bump version to 0.66.65 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 17:34:28 +01:00
Nick b51afea0cb feat(reentry): durable event replay + drift-aware resume + 16O benchmark (T16 16C-fix/16O/16P)
- event_wait.find_matching_event(): replays matching completion events
  from the durable bridge event log at registration. Root-caused from
  TWO production cycles: on a serialized single-runner machine the CI
  wake always lands before/around deploy registration — a forward-only
  FIFO rendezvous can never deliver it (both cycles fell back to the
  900s deadline, truthfully). The event log exists precisely for this;
  the wake is still never the verdict.
- ci_event_wait: event_replayed mode short-circuits to the authority
  read; 2 new held-outs (9 total).
- drift_resume.py (16P): claim-vs-observation reconciliation with
  EXPECTED/BENIGN/MATERIAL/CONFLICTING/UNKNOWN classes; MATERIAL/
  CONFLICTING/UNKNOWN block dependent work; user-locked claims escalate
  to CONFLICTING (reality never silently overrides user-locked
  authority). Held-outs driven by the REAL 16O fixture: the VAT
  regression blocks resume; the checkpoint commit resumes clean.
- 16O benchmark persisted (store/evidence/t16/16O_benchmark.md): honest
  result — on a tiny fixture cold and scaffold-assisted re-entry cost
  about the same (8 vs 9 files); the scaffold's advantage is drift
  classification, recorded next actions, and intent, not raw step
  count. No fabricated win.

Suite 1884 green; ratchets PASS.
2026-09-15 17:34:00 +01:00
Nick c15840514f docs(structure): Rune structural map + 16M inventory + 16D live latency proof (T16 16M/16N/16D)
- docs/STRUCTURAL_MAP.md: fast re-entry surface — canonical routes,
  known duplicate routes, unfinished/blocked/deprecated, fresh-session
  quick answers. Companion to Spine (authority) and 16M inventory.
- store/evidence/t16/16M_inventory.md: full retroactive inventory —
  10 canonical systems + aux systems classified; duplicate routes
  priority-ordered (task-state ×4, build_mode legacy, provider ×5
  entry points, T14/T15 drivers, maintenance ledgers ×3, unwired
  programme_continuation); hard-coded path audit.
- store/evidence/t16/16D_latency_proof.md: live proof cycle 1 —
  c707a81 deploy ran event-driven end-to-end in production: watchdog
  re-arms on live CI, deadline expired exactly once at 900.92s as
  failure detection, final authority read success, promoted 16:18:17Z.
  Sleep-30 polling floor gone. Event-before-waiter ordering documented
  honestly (cycle 2 in flight proves the wake path).
2026-09-15 17:34:00 +01:00
Nick 2833175329 feat(scaffolding): future-Substrate seam — outbound projection + inbound command interface + mock client held-outs (T16 16I/16J/16K)
- construction_projection.py: stable SCHEMA_VERSION=1 public projection
  (all §16I mandate fields: phase/stage/waiting_state/correlation/
  canaries/verification/checkpoint/evidence/failure_class/completion/
  candidate_next_action). A projection, NOT a second authority — derived
  read-only from the canonical scaffold store + live wait bridge; degrades
  to BRIDGE_UNREACHABLE when the bridge is absent, never lies.
- construction_commands.py: bounded validated inbound commands
  (REQUEST_STATE/START/RESUME/ACKNOWLEDGE_BLOCKER/PROVIDE_APPROVAL/
  PROVIDE_CONTEXT — PAUSE/CANCEL deliberately unexposed: no paused phase
  in the lifecycle; abandonment needs user provenance). Every command
  carries identity/correlation, validates against LEGAL_TRANSITIONS,
  cannot mutate storage (returns validated intents; the Construction
  Interface executes), idempotent by command_id. Substrate asks;
  Scaffolding decides.
- tests/test_substrate_seam.py: 9 held-outs — MockSubstrateClient knows
  NOTHING of current.json/internal arrays; state via public seam only;
  event observation + correlation; valid RESUME accepted; invalid START
  refused; direct-store semantics inexpressible; restart survives on
  identities alone; Scaffolding fully usable without any client;
  idempotent replay; unknown command/construction refused; schema
  validated.

Suite 1878 green; ratchets PASS; Henxels rc=0.
2026-09-15 17:34:00 +01:00
conductor 4a06d9f11a chore(release): auto-bump version to 0.66.64 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 17:02:38 +01:00
Nick c707a81933 fix(deploy): shared bridge data root for cross-user event-driven CI wait (T16 16C live defect)
The deploy step runs as the runner user while the W/R daemon runs as
nick — defaulting wr_root() to the invoking user's home made FIFO
creation fail (PermissionError) and the deploy failed fast+truthfully on
both 0.66.63 pushes (runs 7953/7958). No false green; deployment
correctly stalled at 5bd8cca.

- shared data root /mnt/workdrive/wr-bridge (waits/events/resumes/
  leases migrated; daemon restarted on it; 46 waits intact)
- deploy.yml: RUNE_WR_ROOT/RUNE_WR_CODE exported + READINESS-BEFORE-
  ACTION write probe (§16U: clear bridge-misconfigured error instead of
  a FIFO-creation stack trace)
- event_wait: FIFOs and durable wait records relaxed to 0o666 — the
  bridge is cross-user by construction now

Suite 1869 green; ratchets PASS.
2026-09-15 17:02:04 +01:00
Nick 37deb6085c feat(provider): provider ownership boundary + PROVIDER_UNAVAILABLE recovery class (T16 16F/16G)
- provider_context.py: require_model() resolves the model from the
  ambient provider context; missing context fails LOUDLY
  (PROVIDER_UNRESOLVED, exit 3) — no silent default provider anywhere.
- t14/t15 builder drivers, evidence_probe, research executor/relay:
  hardcoded 'Verboo Code/glm-5.3-flash' defaults REMOVED (6+ sites);
  provider/model selection now belongs exclusively to the provider
  layer via the ambient context. No caller anywhere set MIMO_MODEL —
  every run silently used the code-chosen provider until now.
- failure_recovery: PROVIDER_UNAVAILABLE class (context-forced; real
  status/API code preserved verbatim in evidence; policy
  block_branch_preserve_error, 0 retries, no fallback — 16G-D).
- tests/test_provider_ownership.py: structural guard (provider names
  in Scaffolding code lines fail the suite; module list live-discovered)
  + 16G held-outs (verbatim codes, zero fallback attempts, resumable
  after block, provider preference never mutates scaffold truth).
- 16U root-cause completion: pyright skew 117⇄124 driven by ambient
  VIRTUAL_ENV/PATH leakage into pyright's interpreter resolution —
  the ratchet now neutralizes ambient-venv signals (PYTHONNOUSERSITE
  was necessary but not sufficient; reproduced and closed).
- spine reconciliation: T15 modules (event_wait, stage_guards,
  failure_recovery, programme_continuation) were missing belongs_to
  relationships in the tracked store while the generated manifest had
  them — registered with SCAFFOLD-TRANCHE-015 evidence; provider_context
  registered under capability; manifest regenerated via export-umbrellas.

Suite 1869 green; ratchets PASS; Henxels rc=0.
2026-09-15 17:02:04 +01:00
conductor affe852ff7 chore(release): auto-bump version to 0.66.63 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 16:27:30 +01:00
Nick e1a71efeb9 feat(ci): event-driven CI qualification wait over the existing W/R bridge (T16 16C/16D/16E)
- deploy.yml sleep-30 polling loop REPLACED: registers a gitea-identity
  PROCESS_EVENT wait, yields in select() on the FIFO, resumes on the
  ci.yml notify-event-bridge WAKE, then re-reads the canonical Gitea
  verdict (deploy_qualify.py). Event payload is never completion truth.
- ci.yml gains notify-event-bridge (always(), best-effort): a red CI
  wakes the waiter as loudly as a green one; bridge unreachable ->
  watchdog recovery covers the lost wake.
- event_wait.py: register_process_wait gains provider/event_type/sha
  identity (16E: ONE bridge primitive; builder + gitea are adapters);
  open_fifo_reader + caller-owned fifo_fd close the 16Y#10 race (event
  before waiter in select()); await_resume_event returns eof instead of
  busy-looping on a caller-owned fd.
- ci_event_wait.py watchdog: failure DETECTOR only — liveness-gated
  re-arm, one-shot authority recovery on lost events, truthful failure
  when no verdict exists; timestamps emitted for the 16D latency proof.
- tests: 8 held-outs on the REAL daemon (shared wr_daemon fixture in
  conftest — no duplicated helpers). Daemon-side: /process-event passes
  through provider/event_type/sha (defaults preserve T15); lease_path
  sanitizes slash-bearing session ids (resume delivery was impossible
  for them — found live, fixed in daemon snapshot 79434e0).

Suite 1861 green; ratchets PASS; Henxels rc=0.
2026-09-15 16:26:54 +01:00
conductor 5bd8cca941 chore(release): auto-bump version to 0.66.62 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 14:15:22 +01:00
Nick fd5f31bba2 fix(scaffolding): §15D tests use ephemeral ports, not fixed 8793
CI failure on the 0.66.61 bump run (job 18584): the latency test got
watchdog/alive:False because its daemon could not bind the FIXED port
8793 — a stale TIME_WAIT socket from an earlier test in the same file
held it (verified locally: 8793 bindable only with SO_REUSEADDR while
no listener exists). The notifier's event POSTs went nowhere, so the
watchdog correctly reported PROCESS_LOST: the harness was at fault,
not the bridge. Daemon fixture now binds port 0 (ephemeral) and reads
back the assigned port. 3x consecutive green locally.
2026-09-15 14:14:44 +01:00
conductor 8a2a4f7b71 chore(release): auto-bump version to 0.66.61 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 13:42:21 +01:00
Nick b2d6802dba fix(scaffolding): §15K test no longer shells out to git worktree on CI
test_15k_build_manager_has_no_parallel_construction_state invoked
BuildManager.start_build for real, which runs 'git worktree add' against
the HARDCODED production path /mnt/workdrive/rune — a real VCS mutation
inside a unit test. Locally it polluted ~/.rune/work/builds; on the CI
runner it fails outright (dubious-ownership: runner checkout is a
different user). The test now redirects BUILDS_DIR/WORKTREE_DIR into tmp
and stubs create_worktree — the property under test (no canonical
construction state from the /build vehicle) is orthogonal to the git op.
This was the CI unit-tests failure on 193d819 (1 failed, 1852 passed).
2026-09-15 13:41:43 +01:00
conductor 3ebbf9a9b1 chore(release): auto-bump version to 0.66.60 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 13:29:02 +01:00
Nick 193d81961d fix(scaffolding): event_wait importlib-clean loading, zero pyright warnings
_load_waitstore now resolves the bridge's own waitstore.py via
importlib.util.spec_from_file_location (no dynamic bare import, no
sys.path mutation) with per-root caching. Verified: 0 errors/0 warnings
in event_wait.py; per-file stash comparison shows my 8 touched files add
zero warnings over baseline (rune_operator 5/5, rune_tui 307/307
identical). The 443→445 CI warning delta is pre-existing environment
skew in untouched files, not T15 regression.
2026-09-15 13:25:39 +01:00
conductor 94ad81245b chore(release): auto-bump version to 0.66.59 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 04:59:31 +01:00
Nick 196cf193df fix(scaffolding): stage_guards assert-check field typed Any (pyright 117 clean) 2026-09-15 04:58:54 +01:00
Nick f95b2a63df feat(scaffolding): §15X/§15Y — efficiency comparison + adversarial matrix completion
15X: T14→T15 comparison record — readiness prevention now evidenced
(3 cases), interface-discovery prevention (1), zero escaped stages;
8 recurring classes mechanically prevented, listed with mechanisms.

15Y: remaining-gap held-outs — cross-project event isolation
(correlation rejects foreign run_ref) + user-gated scope amendment
(builder self-broadening refused, user authority works). The full 25-case
matrix is covered across test_event_wait (events), test_stage_guards
(canaries/verification/reconciliation), test_native_default (TUI
defaults/research negative), test_programme_continuation, and
test_t14_builder_contract.
2026-09-15 04:54:39 +01:00
Nick 708871e511 feat(scaffolding): §15V/§15W — TUI backlog + honest self-hosting proof
15V: 7 new TUI Upgrade Backlog projections (construction-auto-activated,
builder-state-waiting, canary-state, verification-truth,
programme-continuation, resume-discovery, release-convergence) with
INSPECTION/CONTROL classification; TUI stays projection/client.

15W: night-run self-hosting record — 15 stages one programme, no
voluntary stops; event-driven waits default; polling inventory (all
remaining sleeps marked TEST_TIMING or repaired); 10 defects found and
repaired in-programme listed honestly, including the pkill that killed
the production W/R daemon (restarted, health 200).

suite: 1852 passed, 0 failed
2026-09-15 04:53:53 +01:00
Nick 18ef9a53e3 refactor(scaffolding): §15U — resume discovery migrated to the interface
ConstructionInterface.discover_active_contract: ONE canonical home for
fresh-builder resume discovery (per-project scaffolds + main store;
most-recent contract; pure readback). T14/T15 driver helpers are now
shims. capabilities() advertises the op. Held-out: empty store invents
nothing; seeded store discovers the right contract.
2026-09-15 04:50:38 +01:00
Nick 05edd68027 feat(scaffolding): §15R — second generalisation LIVE-PROVEN (Python/SQLite task ledger)
Different language (Python 3.14), different pattern (storage evolution:
SQLite schema + CRUD vs stateless in-memory scheduler), different runtime
(sqlite3 stdlib), same canonical defaults: readiness → classification →
PROJECT_BUILD → spec contract → scope → obligations → real model builder
→ event-driven wait → exit canaries → 15G test truth → evidence closure.

Live findings (honest):
- the builder wrote ledger.py + ledger.test.py (Node-style name per the
  projection scope) + README; 14/14 unittest pass
- the driver's default verifier (unittest discover) found ZERO tests —
  15G correctly REFUSED closure (false-green protection working)
- the mismatch was the DRIVER's verifier not matching the SCOPE's naming
  convention, not the builder's work; completing with the matching
  verifier closed the contract from evidence
- canonical watchdog→recovery path exercised again (builder outlived the
  deadline; waitstore verdict carried completion)
2026-09-15 04:47:39 +01:00
Nick d166e83d55 feat(scaffolding): §15S/§15T — migration scenario + failure classification
15S: breaking-change lifecycle held-out — old API + consumer governed
before change; verifying→migrating→superseding enforced by the phase
map; the old path survives as governed history (supersession is
lifecycle state, not erasure); BUILD NEW never destroys before migration.

15T: src/rune/failure_recovery.py — deterministic classification
(TRANSIENT/INTERMITTENT/KNOWN_PROCESS_DEFECT/PERMANENT/APPROVAL_
REQUIRED/UNKNOWN) with explicit retry budgets; PERMANENT/APPROVAL
never auto-retry; no infinite loops possible. 2 held-outs.
2026-09-15 04:19:17 +01:00
Nick 98b4c58d36 feat(scaffolding): §15Q — real event-driven builder run LIVE-PROVEN + liveness hardening
scripts/t15_builder_session.py: real model builder (mimo run/Verboo) under
the event notifier; driver YIELDS on the bridge FIFO (no polling on the
normal path); readiness canary before launch; exit canaries + 15G test
truth after; evidence-gated closure.

Live findings (recorded honestly):
- real model builds run 10-65 min — the 900s watchdog misfired twice as a
  false PROCESS_LOST; root cause: (a) deadline < real build time, (b)
  node builders re-group/detach children so pgid AND cmdline liveness
  scans can miss the work. Repair: await_resume_event re-arms bounded on
  liveness; the driver consults CANONICAL WAITSTORE STATE after watchdog
  expiry — a recorded verdict means completion, process-table absence is
  not truth (§15E canonical-state-wins).
- an orphaned T14-era builder process (old .mjs-scope prompt, quota-stuck)
  was discovered polluting process checks — killed and recorded.

Result: contract closed from evidence; 10/10 node tests; both builder
exit events processed by the bridge (verdict success both runs).
Evidence: store/evidence/t15/.
2026-09-15 04:08:22 +01:00
Nick f5c8b07d15 feat(scaffolding): §15N/§15O/§15P — discoverability, provenance, idempotency
15N: capabilities() now exposes full argument schemas (types, required
fields, per-item shape, notes), lifecycle phase map + legal transitions,
and scope-amendment authority — a fresh builder DISCOVERS the
report_observation contract instead of trial-and-error (the T14
dict-vs-list + missing-entity mistakes become impossible). Held-out:
schema-guided first report succeeds; schema-violating payload rejected.

15O: audit_scope is PROVENANCE-AWARE — residue paths present before the
builder ran (or explicitly runtime-declared) stay exempt and recorded;
NEW residue-looking paths appearing mid-session (.mimocode/evil,
.git/config) are SCOPE VIOLATIONS. The hostile lookalike can no longer
hide behind the T14 prefix rule.

15P: PROJECT_BUILD bootstrap twice = one project identity (no
duplication); event replay after restart = readback-only recovery
(idempotent by construction, contract-inspected).
2026-09-15 03:16:44 +01:00
Nick 292c72ec3d feat(scaffolding): §15M/§15L — programme continuation canary + authority boundaries
15M: src/rune/programme_continuation.py — after EVERY stage completes the
canary asks 'is the programme objective satisfied?'; if open and eligible
stages remain the NEXT stage starts immediately (no user re-prompt, no
voluntary stop); event-wait is PROGRAMME_OPEN_WAITING (no polling);
blocked branches fall back to pre-authorized work; every stop carries an
honest verdict (PROGRAMME_COMPLETE/ALL_REMAINING_BLOCKED/APPROVAL_
REQUIRED/UNKNOWN). 7 held-outs incl. the required 4-stage auto-continuation
proof + durable persist/reload. Bounded to ONE construction programme —
not a Substrate scheduler.

15L: authority boundary record persisted (construction=Scaffolding,
wait=event bridge, research-state continuity=FUTURE Substrate (not
built), research execution=Research Core, TUI=client/projection) +
negative held-out: research intents never PROJECT_BUILD.
2026-09-15 03:11:13 +01:00
Nick 0a436b2f97 feat(scaffolding): §15J/§15K — Scaffolding IS the native construction default
15J (native route through the REAL send_task path — TUI input →
RuneOperator.send_task → construction hook → classifier → activation →
durable state):
- 6 held-outs (tests/test_native_default.py): PROJECT_BUILD without any
  command; EXISTING attaches Rune authority; LOCAL_CHANGE light; NOT_
  CONSTRUCTION touches nothing; UNKNOWN probes without invention; manual
  override (/scaffold-mode <MODE>) through the SAME classify_mode with
  user provenance
- classifier: explicit FILE mentions ('the README', 'scheduler.js') now
  bind scoped targets — deterministic lexical evidence + the ONE surface
  authority (classify_surface), lazily imported (cycle-safe)

15K (bypass prevention):
- /build start hardened: classifies through the canonical machinery
  BEFORE BuildManager mints any state; NOT_CONSTRUCTION refused;Build-
  Manager remains execution vehicle, no longer a construction authority
- inventory: BuildManager provably creates NO canonical scaffolds

Process repairs caught during the stage (15X evidence):
- os.chdir pollution from a test leaked cwd into later tests (bare
  os.chdir → monkeypatch.chdir)
- construction-hook tests were making REAL model calls (42s, provider
  quota burned, starved stage-telemetry's model call → GATE_FAILED):
  send_task's _execute_on_model now stubbed in hook tests — suite 144s
  → 54s and deterministic
- S110 noqa for the deliberate classifier degrade; new modules mapped
  into config/umbrellas.json (perception: event_wait, stage_guards)
- suite: 1835 passed, 0 failed
2026-09-15 03:06:55 +01:00
Nick 2ba4114fce feat(scaffolding): §15F/§15G/§15H/§15I — guarded stage execution
src/rune/stage_guards.py (design lessons absorbed, not the systems):
- 15F: StageContract (ready_when/done_when/invariants as EXECUTABLE
  checks) + readiness_canary (failure BLOCKS, action never runs) +
  execute_stage (action return = evidence INPUT; exit canary
  independently verifies; failure RAISES — self-certification
  structurally impossible) + liveness_canary (inactivity = STALLED,
  never completion)
- 15G: verify_test_truth — exit 0 is not proof: zero-tests false green
  refused, all-skipped refused, contract-required artifacts enforced,
  min_tests contract-derived (never universal constants)
- 15H: henxels_placement — REQUIRED at structural boundaries/FULL depth,
  NOT_APPLICABLE for tiny changes; both recorded with rationale
- 15I: reconcile (desired vs freshly-read actual) — green command with
  absent durable effect leaves non-empty delta; next stage must not start
- tests/test_stage_guards.py: 14 held-outs
2026-09-15 02:19:28 +01:00
Nick eccb446a3c feat(scaffolding): §15C/§15D/§15E — event-driven wait becomes the canonical default
- 15C audit: the W/R bridge (daemon :8791) was complete but its resume
  target was mimo-session-shaped only — every Python controller had to
  sleep/poll. Root cause recorded, not excused.
- src/rune/event_wait.py: register_process_wait (durable PROCESS_EVENT
  wait + per-wait FIFO resume cmd), run_builder_under_events (real
  builder under a notifier that observes actual exit and POSTs the
  event), await_resume_event (select-block, NO sleep floor, watchdog
  deadline = failure detector), resume_or_recover (restart: durable
  waitstore wins over transient token).
- 15E adversarials: duplicate/stale/wrong-correlation/malformed events,
  controller restart, lost-event watchdog (zombie-aware liveness).
- Latency proof: builder exits at T, controller resumes at T+~0.1s —
  no arbitrary floor. Failures carry truth (exit_code verdict).
- tests/test_event_wait.py: 8 proofs, real daemon subprocess,
  disposable WR_ROOT. Daemon invariant suites 45/45 + 36/36 unchanged.
2026-09-15 02:12:54 +01:00
58 changed files with 7544 additions and 195 deletions

No files matched your search

BIN
View File
Binary file not shown.
+22
View File
@@ -322,3 +322,25 @@ jobs:
echo "::error::Required CI checks not green — refusing to publish required-checks success"
echo "lint=${{ needs.lint.result }} unit-tests=${{ needs['unit-tests'].result }} canary=${{ needs.canary.result }} best-practices=${{ needs['best-practices'].result }} documentation=${{ needs.documentation.result }} type-check=${{ needs['type-check'].result }} gitea-offline=${{ needs['gitea-offline'].result }}"
exit 1
notify-event-bridge:
# T16 §16C: post the CI completion WAKE to the local event bridge so
# event-driven waiters (deploy.yml → ci_event_wait.py) resume on the
# event instead of sleep-polling. ALWAYS runs (success or failure —
# a red CI must wake the waiter just as loudly as a green one). The
# wake is best-effort and never decides the verdict: the waiter
# re-reads the canonical Gitea state via deploy_qualify.py.
name: CI Pipeline / notify-event-bridge
runs-on: rune-ci-desktop
needs: [required-checks]
if: always()
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
fetch-depth: 1
- name: Wake event bridge (wake only — never completion truth)
env:
CI_SHA: ${{ gitea.sha }}
CI_RESULT: ${{ needs.required-checks.result }}
RUNE_EVENT_BRIDGE_URL: ${{ vars.RUNE_EVENT_BRIDGE_URL || 'http://127.0.0.1:8791' }}
run: python3 scripts/notify_event_bridge.py
+25 -15
View File
@@ -19,28 +19,38 @@ jobs:
with:
fetch-depth: 0
- name: Wait for CI qualification on this exact SHA
- name: Wait for CI qualification on this exact SHA (event-driven, T16 §16C)
# T16 §16C/§16D: the old sleep-30 → query → repeat loop is gone.
# The waiter registers a correlation on the W/R event bridge and
# yields in select() until ci.yml's notify-event-bridge job posts
# the completion WAKE; the verdict is then re-read from canonical
# Gitea (deploy_qualify.py). The 900s deadline is a FAILURE
# DETECTOR (lost-event recovery), never the scheduler.
env:
GITEA_SERVER: ${{ gitea.server_url }}
GITEA_REPO: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
# Shared bridge data root: the deploy step runs as the runner
# user while the daemon runs as nick — the rendezvous state
# (waits/events/resumes/leases) must be shared. §16C live
# finding: defaulting to the invoking user's home made the
# controller unable to create its FIFO (PermissionError) and
# the deploy failed fast+truthfully on both 0.66.63 pushes.
RUNE_WR_ROOT: /mnt/workdrive/wr-bridge
RUNE_WR_CODE: /home/nick/.local/share/mimo-wait-resume
run: |
DEPLOY_SHA=$(git rev-parse HEAD)
export DEPLOY_SHA
echo "Qualifying $DEPLOY_SHA"
DEADLINE=$((SECONDS + 900))
while [ $SECONDS -lt $DEADLINE ]; do
STATUS=$(python3 scripts/deploy_qualify.py)
echo "required-checks: $STATUS"
if [ "$STATUS" = "success" ]; then exit 0; fi
if [ "$STATUS" = "failure" ]; then
echo "::error::CI failed on $DEPLOY_SHA — deployment refused"
exit 1
fi
sleep 30
done
echo "::error::CI qualification timeout on $DEPLOY_SHA — deployment refused"
exit 1
echo "Qualifying $DEPLOY_SHA (event-driven)"
# READINESS BEFORE ACTION (§16U): fail with a clear message if
# the bridge rendezvous is not writable in THIS user's context,
# instead of a stack trace at FIFO creation time.
if ! touch /mnt/workdrive/wr-bridge/resumes/.write-probe 2>/dev/null; then
echo "::error::RUNE_WR_ROOT not writable by $(id -un) — bridge data root misconfigured"
exit 1
fi
rm -f /mnt/workdrive/wr-bridge/resumes/.write-probe
python3 scripts/ci_event_wait.py --sha "$DEPLOY_SHA" --deadline 900
- name: Stage, verify, promote (monotonic)
env:
+4 -1
View File
@@ -22,9 +22,12 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Run all canaries
# T16 §16Q reconciliation: the glob looped shared/canary_*.py —
# real canaries live in src/rune/ (canary_native.MODULES is the
# registry authority). The old job silently no-opped.
run: |
FAILED=0
for canary in shared/canary_*.py; do
for canary in src/rune/canary_*.py; do
if [ -f "$canary" ]; then
echo "Running $canary..."
python3 "$canary" || FAILED=$((FAILED + 1))
+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.66.58**
**Current release: v0.66.68**
**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.**
+9 -1
View File
@@ -1,7 +1,7 @@
{
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
"_generated_by_spine": true,
"generated_at": "2026-09-14T06:08:20Z",
"generated_at": "2026-09-15T16:31:19Z",
"umbrellas": {
"capability": {
"description": "Tool selection and execution authority",
@@ -17,6 +17,7 @@
"local_tool_runtime",
"message_queue",
"openai_compat_transport",
"provider_context",
"recall_uplift",
"route_risk_flags",
"tool_executor",
@@ -219,19 +220,26 @@
"cognitive_budgeter",
"complexity_scorer",
"construction_activation",
"construction_commands",
"construction_decision",
"construction_depth",
"construction_interface",
"construction_mode",
"construction_pattern",
"construction_patterns_builtin",
"construction_projection",
"context_broker",
"context_pressure",
"drift_resume",
"event_wait",
"failure_recovery",
"layout_contract",
"programme_continuation",
"release_notes",
"salience_gate",
"scaffold",
"spine",
"stage_guards",
"structural_store",
"toolchain",
"tui_backlog",
+71
View File
@@ -0,0 +1,71 @@
# Rune Structural Map
_Maintained structural projection — the fast re-entry surface (T16 §16N).
Companion to the Spine store (`store/spine/current.json`, structural
authority) and `store/evidence/t16/16M_inventory.md` (full classification
record). Update this map when authority or canonical routes change; do
not copy the repo here._
## What is Rune?
A self-hosting coding agent: TUI client over an operator core, with a
structural self-model (Spine), a construction authority (Scaffolding),
an event-driven wait/resume bridge (W/R), a research pipeline, and a
fail-closed release/deployment pipeline that builds, tests, releases,
and deploys Rune itself to `/mnt/workdrive/rune-code/current`.
## Where things happen (canonical routes only)
| Concern | Authority | State |
|---|---|---|
| Structural self-model | `src/rune/spine.py` | `store/spine/current.json` |
| Construction (build lifecycle) | `src/rune/scaffold.py` + `construction_*.py` | scaffold store (`<dir>/.rune/scaffold`) |
| Builder sessions | `scripts/t15_builder_session.py` (event-driven successor; T14 is legacy) | contract + wait records |
| Provider/model selection | `rune_operator` final decider; ambient context via `provider_context.require_model` | `state/model_routing_profiles.json` |
| Wait/resume (any long op) | `src/rune/event_wait.py` + W/R daemon (out-of-repo snapshot at `~/.local/share/mimo-wait-resume`, data root `/mnt/workdrive/wr-bridge`) | waits/events/leases/resumes JSON |
| CI qualification verdict | `scripts/deploy_qualify.py` (two legs, fail-closed) | Gitea commit status + runs |
| Release/deploy | `scripts/deploy_stage.py` → `deploy_promotion.py` (monotonic, flock) | deploy root + `current`/`last-good` symlinks |
| Research | `scripts/research_task_tree.py` (authority; labels are projection) | `research/issue-<N>/` |
| Task completion truth | `verification_layer.py` + `orchestration/conversation_state.py` | durable event store + checkpoints |
| Routing (task class) | `src/rune/adaptive_router.py` | `routes/route.jsonl` |
| Canary registry | `src/rune/canary_native.MODULES` | observed by Spine |
| Structural health | `henxels_checks.py` + `henxels.yaml` | repo root |
## Known non-canonical / duplicate routes (do not extend; reconcile via 16Q)
- `build_mode.BuildManager` (legacy /build) — superseded by builder_bridge
+ event-driven sessions; still wired.
- `scripts/t14_builder_session.py` — superseded by t15 event-driven driver.
- Provider selection entry points ×5 (`model_routing.rank`,
`model_connect route`, `model_registry get`, `provider_registry`,
`work_manager`) — operator is the de-facto final decider.
- Task-ish state ×4 (task-tool DB, `task_persist` persisted.json,
`memory_store.working`, orchestration event store).
- Maintenance ledgers ×3 (scaffold maintenance_item, tui_backlog, drift_ledger).
## Unfinished / blocked / deprecated
- programme_continuation.py: implemented + tested but UNWIRED (no
production caller) — the "continue problem" machinery awaits routing.
- maintenance.yml canary-suite job: loops `shared/canary_*.py` — wrong
dir (real canaries in `src/rune/`), job likely no-ops (LEGACY).
- daemon out-of-repo: the W/R daemon is deployed from
`~/.local/share/mimo-wait-resume` with its own git snapshot —
packaging debt, no CI coverage of the deployed copy.
- Substrate: NOT BUILT. Scaffolding exposes the seam
(`construction_projection.py`, `construction_commands.py`); mock
client proves compatibility (`tests/test_substrate_seam.py`).
## Fresh-session quick answers
- Is a claim true? → Spine store + evidence_refs, never commit messages.
- Is construction allowed? → scaffold contract phase + LEGAL_TRANSITIONS.
- How does a long wait work? → register on the W/R bridge, yield on the
FIFO event; watchdog is failure detection, never scheduling; the event
is a WAKE — authority is re-read after resume.
- Who picks the provider? → the user layer, via ambient context;
construction never does (`test_provider_ownership.py` enforces).
- What runs where? → CI on `rune-ci-desktop` runner; deploy stages to
`/mnt/workdrive/rune-code/<sha>`; data home pinned at
`/mnt/workdrive/rune` (RUNE_HOME); implementation worktree
`/mnt/workdrive/issue161-harness`.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.66.58"
version = "0.66.68"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""ci_event_wait.py — T16 §16C/§16D: event-driven CI qualification wait.
Replaces the deploy.yml sleep-30 → query → repeat polling loop with the
EXISTING W/R event bridge (src/rune/event_wait.py — the SAME primitive the
builder adapter uses; §16E: one bridge, adapters differ only in identity
and authority probe).
Flow (mandate §16C):
push / CI workflow starts
↓
register correlation (provider="gitea", event_type="ci_completed",
run_ref="ci-<sha>", sha=<sha>)
↓
WAITING_FOR_EVENT (await_resume_event: select() on FIFO, no floor)
↓
ci.yml final job POSTs /process-event (the WAKE)
↓
daemon matches → resume_cmd fires → FIFO token → controller resumes
↓
canonical Gitea state INDEPENDENTLY re-read (deploy_qualify.py verdict)
↓
verdict
The event is a WAKE, never truth. The final verdict always comes from
deploy_qualify.py (required-checks context + exact-SHA ci.yml run, both
legs). Watchdog deadline is a FAILURE DETECTOR, never the scheduler:
- event arrives → authority re-read; pending → bounded re-arm until the
overall deadline (ack-window semantics, same as the daemon's wake sweep).
- event lost/suppressed → watchdog expires → ONE-SHOT authority query:
terminal verdict → recovered truthfully (no invented wait);
CI still running (non-terminal Gitea runs for the SHA) → re-arm;
nothing alive and no verdict → truthful failure (deployment refused).
Exit codes: 0 = qualified (success), 1 = refused (failure/timeout).
Output: one JSON line with the §16D latency-proof timestamps.
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import time
from pathlib import Path
_SCRIPT_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(_SCRIPT_DIR.parent / "src"))
from rune import event_wait as ew # noqa: E402
REPO_ROOT = _SCRIPT_DIR.parent
QUALIFY_SCRIPT = _SCRIPT_DIR / "deploy_qualify.py"
def authority_verdict() -> str:
"""ONE-SHOT canonical Gitea verdict via deploy_qualify.py — the only
completion truth. Never parses the event payload."""
proc = subprocess.run(
[sys.executable, str(QUALIFY_SCRIPT)],
capture_output=True, text=True, timeout=120,
env=dict(os.environ, PYTHONPATH=""),
)
out = (proc.stdout or "").strip().splitlines()
return (out[-1] if out else "pending").strip()
def authority_after_wake(max_tries: int = 3, gap_secs: int = 4) -> str:
"""Post-wake authority read. The daemon's own wake sweep uses a
bounded event-triggered retry (4s ×3) for the job-finished-microseconds-
ago ack window; a verdict read here can hit the same window. This is a
bounded schedule, not a polling loop — 3 reads max, then back to the
event wait / watchdog."""
verdict = "pending"
for attempt in range(max_tries):
verdict = authority_verdict()
if verdict != "pending":
return verdict
if attempt < max_tries - 1:
time.sleep(gap_secs)
return verdict
def ci_alive() -> bool:
"""Liveness probe for THIS adapter: any non-terminal Actions run for
the SHA in canonical Gitea (one-shot on watchdog expiry — not a poll).
Import the probe from deploy_qualify to keep ONE API client."""
sys.path.insert(0, str(_SCRIPT_DIR))
import deploy_qualify as dq # noqa: E402 (spec-loaded scripts pattern)
import gitea_auth
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
repo = os.environ.get("GITEA_REPO", "")
sha = os.environ.get("DEPLOY_SHA", "")
if not (server and repo and sha):
return False
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
if not token:
return False
runs = dq._fetch_runs(server, repo, sha, token)
if not runs:
return False
terminal = {"success", "failure", "cancelled", "skipped"}
return any(str(r.get("status", "")).lower() not in terminal
for r in runs)
def run(*, sha: str, deadline_secs: int,
watchdog_secs: int = 300,
daemon_url: str = ew.DEFAULT_DAEMON_URL) -> int:
op = f"ci-{sha}"
t0 = time.time()
# §16Y #10 race fix: the FIFO reader exists BEFORE registration, so a
# wake arriving in the registration window can always be delivered.
fifo_fd = ew.open_fifo_reader(op)
# Event replay (T16 §16C live finding): on a serialized runner the
# completion event usually arrives BEFORE the deploy job registers.
# A matching durable event means the wake already happened — skip
# the rendezvous entirely and go straight to the authority read.
prior = ew.find_matching_event("gitea", "ci_completed", op)
if prior is not None and prior.get("exit_code") is not None:
rec = ew.register_process_wait(
op, project="rune",
objective=f"event-driven CI qualification for {sha}",
watched_for="Gitea CI completion event (replayed from the "
"durable event log)",
provider="gitea", event_type="ci_completed", sha=sha,
watchdog_secs=min(watchdog_secs, deadline_secs))
registered_at = rec["created_at"]
event_at = time.strftime(
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
resumed_at = event_at
# the event is a WAKE, never truth — canonical authority decides
verdict = authority_after_wake()
if verdict == "pending" and not ci_alive():
verdict = authority_verdict()
mode = ("event_replayed" if verdict != "pending"
else "authority_unresolved")
verdict_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
out = {
"sha": sha,
"registered_at": registered_at,
"event_received_at": event_at,
"controller_resumed_at": resumed_at,
"verdict_at": verdict_at,
"mode": mode,
"rearm_reason": "completion event replayed from durable log "
"(arrived before registration)",
"verdict": verdict,
"wall_secs": round(time.time() - t0, 3),
}
print(json.dumps(out))
return 0 if verdict == "success" else 1
rec = ew.register_process_wait(
op, project="rune",
objective=f"event-driven CI qualification for {sha}",
watched_for="Gitea CI completion event (wake; verdict re-read "
"from canonical Gitea)",
provider="gitea", event_type="ci_completed", sha=sha,
watchdog_secs=min(watchdog_secs, deadline_secs))
registered_at = rec["created_at"]
deadline = time.monotonic() + deadline_secs
verdict = "pending"
mode = "event"
event_at = ""
resumed_at = ""
rearm_reason = ""
try:
while time.monotonic() < deadline:
remaining = max(1, int(deadline - time.monotonic()))
result = ew.await_resume_event(
op, watchdog_secs=min(remaining, watchdog_secs), pgid=None,
fifo_fd=fifo_fd)
if result["kind"] == "event":
event_at = event_at or time.strftime(
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
resumed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ",
time.gmtime())
# WAKE received — canonical authority decides, never the
# event payload
verdict = authority_after_wake()
if verdict != "pending":
mode = "event"
break
rearm_reason = "event seen; authority pending"
if not ci_alive():
# CI terminal in Gitea but aggregator lagging: one
# final truthful read, then stop
verdict = authority_verdict()
mode = ("event" if verdict != "pending"
else "authority_unresolved")
break
continue
if result["kind"] == "eof":
# writer gone without a token: the DURABLE record decides
rec = ew.operation_wait(op) or {}
if rec.get("status") in ("COMPLETED", "FAILED",
"READY_TO_RESUME", "RESUMING") or \
(rec.get("status") == "RESUME_UNVERIFIED"
and rec.get("process_event_evidence")):
event_at = event_at or time.strftime(
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
resumed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ",
time.gmtime())
verdict = authority_after_wake()
if verdict != "pending":
mode = "event_recovered_from_durable_record"
break
rearm_reason = "eof without token; durable record pending"
continue
# watchdog expiry: adapter liveness (Gitea runs) decides
# re-arm vs truthful recovery — failure detector, never
# scheduler
if ci_alive():
rearm_reason = "watchdog expiry; CI runs non-terminal " \
"in Gitea — re-arm"
continue
verdict = authority_verdict()
if verdict == "pending":
mode = "watchdog_lost_event_no_verdict"
else:
mode = "watchdog_authority_recovered"
break
finally:
os.close(fifo_fd)
if verdict == "pending":
if mode == "event":
mode = "deadline_pending"
verdict = authority_verdict() # final one-shot truth
verdict_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
out = {
"sha": sha,
"registered_at": registered_at,
"event_received_at": event_at,
"controller_resumed_at": resumed_at,
"verdict_at": verdict_at,
"mode": mode,
"rearm_reason": rearm_reason,
"verdict": verdict,
"wall_secs": round(time.time() - t0, 3),
}
print(json.dumps(out))
return 0 if verdict == "success" else 1
def main() -> int:
sha = os.environ.get("DEPLOY_SHA", "")
args = sys.argv[1:]
if "--sha" in args:
sha = args[args.index("--sha") + 1]
deadline = 900
if "--deadline" in args:
deadline = int(args[args.index("--deadline") + 1])
if not sha:
print(json.dumps({"error": "no sha (DEPLOY_SHA or --sha required)"}))
return 1
return run(sha=sha, deadline_secs=deadline)
if __name__ == "__main__":
sys.exit(main())
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""notify_event_bridge.py — T16 §16C: ci.yml's final job posts the CI
completion WAKE to the local W/R event bridge (daemon /process-event).
The event is a WAKE, never completion truth: the waiting controller
(ci_event_wait.py) re-reads the canonical Gitea verdict via
deploy_qualify.py after resuming. Best-effort by design — if the bridge
is unreachable the waiter's watchdog still recovers truthfully (that is
the §16D lost-event contract), so a notify failure must NEVER fail CI.
"""
from __future__ import annotations
import json
import os
import sys
import time
import urllib.request
BRIDGE_URL = os.environ.get("RUNE_EVENT_BRIDGE_URL",
"http://127.0.0.1:8791")
def main() -> int:
sha = os.environ.get("CI_SHA", "")
result = os.environ.get("CI_RESULT", "") # needs.<job>.result
if not sha:
print("notify-event-bridge: no CI_SHA — nothing to wake", flush=True)
return 0
payload = {
"operation_id": f"ci-{sha}",
"provider": "gitea",
"event_type": "ci_completed",
"sha": sha,
# exit_code is informational only; the wake is not the verdict
"exit_code": 0 if result == "success" else 1,
"completed_at": time.time(),
}
for attempt in range(2):
try:
req = urllib.request.Request(
BRIDGE_URL + "/process-event",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=5) as r:
resp = json.loads(r.read())
print(f"notify-event-bridge: woke bridge "
f"(matched={resp.get('matched')})", flush=True)
return 0
except Exception as exc: # noqa: BLE001 — wake is best-effort by design; failure is logged and the waiter's watchdog recovery covers a lost wake (§16D contract)
if attempt == 0:
time.sleep(2)
else:
print(f"notify-event-bridge: bridge unreachable ({exc}); "
f"watchdog recovery covers a lost wake", flush=True)
return 0
if __name__ == "__main__":
sys.exit(main())
+2 -1
View File
@@ -181,7 +181,8 @@ def run_stage_with_relay(tree, out_dir: str) -> dict:
err_path = os.path.join(out_dir, f"stage_{stage}.mimo.err.log")
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
from rune.provider_context import require_model
mimo_model = require_model() # T16 §16F: provider layer owns selection
mimo_agent = os.environ.get("MIMO_AGENT", "build")
cmd = [
+12 -6
View File
@@ -793,7 +793,8 @@ class StageExecutor:
prompt = chr(10).join(pp)
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
from rune.provider_context import require_model
mimo_model = require_model() # T16 §16F: provider layer owns selection
mimo_agent = os.environ.get("MIMO_AGENT", "build")
events_path = os.path.join(self.artifact_dir, "stage_OUTWARD.events.jsonl")
err_path = os.path.join(self.artifact_dir, "stage_OUTWARD.mimo.err.log")
@@ -933,7 +934,8 @@ class StageExecutor:
prompt = chr(10).join(pp)
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
from rune.provider_context import require_model
mimo_model = require_model() # T16 §16F: provider layer owns selection
mimo_agent = os.environ.get("MIMO_AGENT", "build")
events_path = os.path.join(self.artifact_dir, "stage_APPLICABILITY.events.jsonl")
err_path = os.path.join(self.artifact_dir, "stage_APPLICABILITY.mimo.err.log")
@@ -1137,7 +1139,8 @@ class StageExecutor:
prompt = chr(10).join(pp)
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
from rune.provider_context import require_model
mimo_model = require_model() # T16 §16F: provider layer owns selection
mimo_agent = os.environ.get("MIMO_AGENT", "build")
events_path = os.path.join(self.artifact_dir, "stage_ADVERSARIAL.events.jsonl")
err_path = os.path.join(self.artifact_dir, "stage_ADVERSARIAL.mimo.err.log")
@@ -1345,7 +1348,8 @@ class StageExecutor:
prompt = chr(10).join(pp).replace("{n}", str(self.issue))
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
from rune.provider_context import require_model
mimo_model = require_model() # T16 §16F: provider layer owns selection
mimo_agent = os.environ.get("MIMO_AGENT", "build")
events_path = os.path.join(self.artifact_dir, "stage_INNOVATION.events.jsonl")
err_path = os.path.join(self.artifact_dir, "stage_INNOVATION.mimo.err.log")
@@ -1534,7 +1538,8 @@ class StageExecutor:
prompt = chr(10).join(pp)
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
from rune.provider_context import require_model
mimo_model = require_model() # T16 §16F: provider layer owns selection
mimo_agent = os.environ.get("MIMO_AGENT", "build")
events_path = os.path.join(self.artifact_dir, "stage_SYNTHESIS.events.jsonl")
err_path = os.path.join(self.artifact_dir, "stage_SYNTHESIS.mimo.err.log")
@@ -1671,7 +1676,8 @@ class StageExecutor:
prompt = chr(10).join(pp).replace("{n}", str(self.issue))
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
from rune.provider_context import require_model
mimo_model = require_model() # T16 §16F: provider layer owns selection
mimo_agent = os.environ.get("MIMO_AGENT", "build")
events_path = os.path.join(self.artifact_dir, "stage_QUALIFY.events.jsonl")
err_path = os.path.join(self.artifact_dir, "stage_QUALIFY.mimo.err.log")
+51 -30
View File
@@ -44,8 +44,18 @@ USER_REQUEST = (
"list and complete practice items."
)
EXPECTED_SCOPE = ["scheduler.js", "scheduler.test.js", "package.json"]
BUILDER_MODEL = os.environ.get("T14_BUILDER_MODEL",
"Verboo Code/glm-5.3-flash") # PL1 provider policy default
# T16 §16F: NO silent default provider. The surrounding provider
# layer sets T14_BUILDER_MODEL (falling back to the ambient MIMO_MODEL
# context); construction never names a provider.
import sys as _sys
_sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent / "src"))
from rune.provider_context import ProviderUnresolved, require_model # noqa: E402
try:
BUILDER_MODEL = os.environ.get("T14_BUILDER_MODEL", "").strip() \
or require_model() # ambient provider context (MIMO_MODEL)
except ProviderUnresolved as _exc:
_sys.stderr.write(str(_exc) + "\n")
raise SystemExit(3) # 3 = PROVIDER_UNRESOLVED (no silent fallback)
BUILDER_CMD = "mimo"
@@ -196,19 +206,42 @@ def _is_runtime_residue(path: str) -> bool:
def audit_scope(project_dir: Path, before: dict,
allowed: list[str]) -> dict:
allowed: list[str], *,
runtime_declared: set[str] | None = None) -> dict:
"""§15O PROVENANCE-AWARE residue classification.
The T14 prefix rule (.git/.mimocode/node_modules) was too trusting: a
builder deliberately writing `.mimocode/evil` would vanish from the
audit. Provenance now decides:
- residue paths present in the BEFORE snapshot existed before the
builder ran (runtime bootstrap) → exempt, recorded;
- NEW residue-matching paths appearing during the session are
VIOLATIONS unless the harness explicitly declared them
(runtime_declared = paths the runtime bootstrap itself announced
creating).
"""
runtime_declared = runtime_declared or set()
after = snapshot_files(project_dir)
changed = sorted(set(after) - set(before))
modified = sorted(k for k in after
if k in before and after[k] != before[k])
residue = sorted(f for f in changed + modified
if _is_runtime_residue(f))
violations = sorted(f for f in changed + modified
if f not in allowed and not _is_runtime_residue(f))
return {"created": [f for f in changed if not _is_runtime_residue(f)],
"modified": [f for f in modified if not _is_runtime_residue(f)],
"runtime_residue": residue,
"scope_violations": violations,
residue = []
violations = []
for f in changed + modified:
if f not in allowed:
if _is_runtime_residue(f):
if f in before or f in runtime_declared:
residue.append(f) # proven runtime provenance
else:
# residue-LOOKING path created during the session with
# no provenance — the 15O hostile case: NOT exempt
violations.append(f)
else:
violations.append(f)
return {"created": [f for f in changed if f not in violations],
"modified": [f for f in modified if f not in violations],
"runtime_residue": sorted(set(residue)),
"scope_violations": sorted(set(violations)),
"in_scope": not violations}
@@ -264,26 +297,14 @@ def complete_lifecycle(iface: ci.ConstructionInterface, contract_id: str,
def discover_contract_id(store: Path) -> tuple[str, str] | None:
"""Purely canonical discovery: walk the store's project scaffolds and
return (project, contract_id) for the most recent contract. No session
file, no transcript — exactly what a fresh builder may know."""
projects_dir = store / "projects"
if not projects_dir.exists():
"""§15U: delegates to the canonical interface
(ConstructionInterface.discover_active_contract). This shim stays only
so existing driver call sites keep working."""
from rune import construction_interface as _ci
env = _ci.ConstructionInterface.discover_active_contract(str(store))
if not env.get("success"):
return None
best: tuple[str, str, str] | None = None
for proj_current in sorted(projects_dir.glob("*/current.json")):
project = proj_current.parent.name
try:
doc = json.loads(proj_current.read_text())
except (OSError, ValueError):
continue
for e in doc["entities"]:
if e["kind"] == "construction_contract":
key = (e.get("observed_at") or "", project,
e["id"].split(":", 1)[1])
if best is None or key > (best[0], best[1], best[2]):
best = key
return (best[1], best[2]) if best else None
return env["ids"]["project"], env["ids"]["contract_id"]
def seed_requirement_spec(store: Path, project: str) -> str:
+381
View File
@@ -0,0 +1,381 @@
#!/usr/bin/env python3
"""t15_builder_session.py — T15 §15Q: REAL event-driven builder proof.
The difference from T14, enforced by construction:
launch → register WAITING_EVENT → YIELD (select on the bridge FIFO)
→ builder exit event → resume immediately → reload canonical state
→ readiness/exit canaries → independent verification → continuation
NO sleep/poll loops anywhere on the normal path. The watchdog is only a
failure detector (lost event / dead process), never the scheduler.
Modes:
start classify → activate → contract → real builder → event wait
→ exit canary → verification → evidence-backed close
interrupt like start, but the builder process group is SIGKILLed at
the first source file → PROCESS_LOST → canonical state
persists for a fresh builder (run `resume` next)
resume FRESH controller + fresh builder from canonical state only
status print canonical construction state
The builder is the real model process (mimo run — Verboo per the standing
provider policy). Builder identity is recorded, never trusted.
"""
from __future__ import annotations
import json
import os
import signal
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
sys.path.insert(0, str(ROOT / "scripts"))
import t14_builder_session as t14 # noqa: E402 — reuse the proven pieces
from rune import construction_interface as ci # noqa: E402
from rune import event_wait as ew # noqa: E402
from rune import scaffold as sc # noqa: E402
from rune import structural_store as ss # noqa: E402
from rune.event_wait import DEFAULT_DAEMON_URL # noqa: E402
from rune.provider_context import ProviderUnresolved, require_model # noqa: E402
# T16 §16F: NO silent default provider — resolve from the ambient provider
# context (T15_BUILDER_MODEL, falling back to MIMO_MODEL); loud failure
# with exit code 3 when the provider layer has not selected one.
try:
BUILDER_MODEL = os.environ.get("T15_BUILDER_MODEL", "").strip() \
or require_model()
except ProviderUnresolved as _exc:
import sys as _sys
_sys.stderr.write(str(_exc) + "\n")
raise SystemExit(3) # 3 = PROVIDER_UNRESOLVED (no silent fallback)
DAEMON_URL = os.environ.get("T15_DAEMON_URL", DEFAULT_DAEMON_URL)
def log(msg: str) -> None:
print(f"[t15] {msg}", flush=True)
def readiness_canary(toolchain_hint: str) -> dict:
"""§15F: may the build stage begin? Executable checks, not optimism.
Runtime checks derive from the verifier (15R generalisation)."""
checks = []
runtimes = (["node"] if "node" in VERIFIER else []) + (["python3"] if "python" in VERIFIER else [])
for rt in runtimes:
r = subprocess.run([rt, "--version"], capture_output=True, text=True)
checks.append({"name": f"{rt}_present", "passed": r.returncode == 0,
"evidence": (r.stdout or "").strip()[:30]})
r2 = subprocess.run(["mimo", "--version"], capture_output=True, text=True)
checks.append({"name": "builder_cli_present", "passed": r2.returncode == 0,
"evidence": (r2.stdout or "").strip()[:40]})
try:
import urllib.request
with urllib.request.urlopen(DAEMON_URL + "/health", timeout=3) as resp:
ok = resp.status == 200
checks.append({"name": "event_bridge_healthy", "passed": ok,
"evidence": DAEMON_URL})
except Exception as e: # noqa: BLE001
checks.append({"name": "event_bridge_healthy", "passed": False,
"evidence": str(e)[:80]})
verdict = "READY" if all(c["passed"] for c in checks) else "BLOCKED"
return {"canary": "readiness", "verdict": verdict, "checks": checks}
# 15R: the driver is construction INFRASTRUCTURE — the request/scope are
# parameters, never scheduler-specific constants. Defaults preserve the
# proven fixture.
REQUEST = os.environ.get(
"T15_REQUEST",
"Build me a small standalone practice scheduler that can create, "
"list and complete practice items.")
SCOPE = [s for s in os.environ.get(
"T15_SCOPE", "scheduler.js,scheduler.test.js,package.json").split(",")
if s]
PROJECT_NAME = os.environ.get("T15_PROJECT_NAME", "")
SPEC_CONTRACT = os.environ.get("T15_SPEC_CONTRACT",
"contract:practice-items-spec")
TOOLCHAIN = os.environ.get("T15_TOOLCHAIN",
"node >= 18 (node:test built-in runner, ES modules)")
VERIFIER = os.environ.get("T15_VERIFIER", "node --test")
def setup_construction(store: Path, project_dir: Path) -> tuple[str, str]:
"""classify → activate → seed spec → contract → transitions. Reuses
the proven T14 canonical path unchanged."""
staged = t14.classify_and_activate(store, REQUEST)
project = staged["activation"]["project"]
assert staged["activation"]["mode"] == "PROJECT_BUILD"
_seed_spec(store, project)
iface = ci.ConstructionInterface(
builder=BUILDER_MODEL,
project_store=str(store / "projects" / project),
project_id=project)
sub = iface.submit_need(
REQUEST, origin="rune_native",
constraints={"environment": {"os": "linux",
"runtime": TOOLCHAIN,
"runner": VERIFIER},
"contracts": [SPEC_CONTRACT]},
affected_surface=list(SCOPE),
evidence_refs=["t15-user-request", "node-v22-installed"],
approval_context="t15 user-approved fixture")
assert sub.get("success"), sub.get("errors")
contract_id = sub["ids"]["contract_id"]
for phase in ("approved", "building"):
tr = iface.transition(contract_id, phase, evidence_ref=f"t15 {phase}")
assert tr.get("success"), tr.get("errors")
log(f"contract {contract_id} → building")
return project, contract_id
def _seed_spec(store: Path, project: str) -> str:
"""Govern the requirement as a contract on the project surface
(generic — request/scope come from parameters)."""
proj = sc.Scaffold(str(store / "projects" / project), project)
doc = proj.load()
if ss.entity(doc, SPEC_CONTRACT) is None:
ss.add_entity(doc, id=SPEC_CONTRACT, kind="contract",
identities={"objective": REQUEST},
evidence_refs=["t15-request"])
ss.add_entity(doc, id="capability:planned-" + project[:24],
kind="capability", maturity="DECLARED",
evidence_refs=["t15-request"])
ss.add_relationship(doc, type="implements",
from_id="capability:planned-" + project[:24],
to_id=SPEC_CONTRACT, evidence_refs=["t15-request"])
proj.commit(doc, what="seed requirement spec (user request)")
return SPEC_CONTRACT
def run_build_via_events(project_dir: Path, operation_id: str,
projection: dict, contract_id: str,
interrupt_after_first_file: bool) -> dict:
"""THE 15Q core: real builder under the event notifier; the driver
YIELDS on the bridge FIFO. No sleep. Watchdog = failure detector."""
ew.register_process_wait(
operation_id, contract_id=contract_id,
project="t15-fixture", objective=REQUEST,
watched_for="builder process exit event", watchdog_secs=900)
# op marker rides the prompt (last argv) so cmdline liveness can find
# the build even if node regroups its children (T15Q live finding)
builder_cmd = ["mimo", "run", "-m", BUILDER_MODEL, "--format", "json",
t14.builder_prompt(projection, resumed=False)
+ f" [op:{operation_id}]"]
notifier = ew.run_builder_under_events(builder_cmd, operation_id,
daemon_url=DAEMON_URL)
log(f"builder launched (pgid {notifier.pid}); driver YIELDS on the "
f"event bridge — no polling")
if interrupt_after_first_file:
# kill at first source file: the interruption is real, the event
# never arrives (the notifier dies with the group) — the watchdog
# must classify PROCESS_LOST, never silently wait.
# TEST_TIMING: this 0.5s poll exists ONLY on the deliberate
# interrupt path (killing at a checkpoint), never on the normal
# continuation path.
while True:
files = [p for p in project_dir.iterdir()
if p.suffix in (".mjs", ".js", ".json")]
if files:
break
time.sleep(0.5)
log("INTERRUPTING builder (first file landed) — SIGKILL group")
os.killpg(notifier.pid, signal.SIGKILL)
result = ew.await_resume_event(operation_id, watchdog_secs=900,
pgid=notifier.pid)
return result
def finish_via_canaries(store: Path, project: str, contract_id: str,
project_dir: Path, before: dict,
resume_note: str) -> int:
"""Reload canonical state → scope audit → INDEPENDENT verification
(15G test truth) → evidence-backed lifecycle."""
iface = ci.ConstructionInterface(
builder=BUILDER_MODEL, project_store=str(store / "projects" / project),
project_id=project)
ctx = iface.construction_context(contract_id)
assert ctx["state"]["active"] or ctx["state"]["phase"] == "closed"
log(f"canonical state: phase={ctx['state']['phase']} "
f"scope={ctx['state']['scope']}")
# exit canary: scope audit with provenance-aware residue (15O)
audit = t14.audit_scope(project_dir, before, list(SCOPE))
# 15G: verification truth — node --test must actually run tests
# 15G test truth via the CONTRACT's verifier (parameterized — 15R
# generalisation: python unittest/pytest, node --test, etc.)
truth = subprocess.run(
["bash", "-c", VERIFIER], cwd=str(project_dir),
capture_output=True, text=True, timeout=300)
import re
count = None
for pat in (r"# pass (\d+)", # node --test TAP summary
r"(\d+) passed", # pytest
r"Ran (\d+) tests"): # python unittest
m = re.search(pat, (truth.stdout or "") + (truth.stderr or ""))
if m:
count = int(m.group(1))
break
test_count = count if count is not None else 0
log(f"verification: exit={truth.returncode} tests={test_count}")
if test_count == 0:
log("FALSE GREEN REFUSED: zero tests executed (15G)")
return 1
if audit["scope_violations"]:
log(f"closure REFUSED: scope violations {audit['scope_violations']}")
return 1
builder_status = "UNKNOWN"
result = t14.complete_lifecycle(iface, contract_id, audit,
{"cmd": "node --test",
"exit_code": truth.returncode},
builder_status)
summary = {
"event_driven": True,
"builder_model": BUILDER_MODEL,
"resume_note": resume_note,
"scope_audit": audit,
"verification": {"exit_code": truth.returncode,
"test_count": test_count},
"lifecycle": result,
}
print(json.dumps(summary, indent=1))
return 0 if result.get("closed") else 1
def discover(store: Path) -> tuple[str, str] | None:
"""§15U: delegates to the canonical interface discovery (via the t14
shim — one hop, no duplication)."""
return t14.discover_contract_id(store)
def cmd_start(store: Path, project_dir: Path, interrupt: bool) -> int:
ready = readiness_canary("node")
assert ready["verdict"] == "READY", f"readiness BLOCKED: {ready['checks']}"
log(f"readiness READY ({len(ready['checks'])} checks)")
before = t14.snapshot_files(project_dir)
project, contract_id = setup_construction(store, project_dir)
operation_id = f"t15-{contract_id}"
iface = ci.ConstructionInterface(
builder=BUILDER_MODEL, project_store=str(store / "projects" / project),
project_id=project)
projection = t14.build_projection(iface, contract_id,
phase_note="initial build")
result = run_build_via_events(project_dir, operation_id, projection,
contract_id, interrupt)
if result["kind"] == "watchdog":
# §15E canonical-state-wins: process-table absence is NOT truth
# (detached builders re-parent their workers — T15Q live finding).
# The durable waitstore decides: a recorded verdict means the
# builder completed and the event WAS processed.
rec = ew.resume_or_recover(operation_id)
if rec["kind"] == "event":
log(f"watchdog fired but canonical state shows the build "
f"completed (verdict={rec['wait'].get('verdict')}) — "
f"continuing from authority")
return finish_via_canaries(store, project, contract_id,
project_dir, before,
"watchdog→canonical recovery")
log("PROCESS_LOST: no event, no durable verdict — fresh builder "
"resumes from authority only")
return 3
wait = result.get("wait", {})
log(f"event-driven resume: verdict={wait.get('verdict')} "
f"exit={wait.get('process_event_evidence', {}).get('exit_code')}")
return finish_via_canaries(store, project, contract_id, project_dir,
before, "event-driven completion")
def cmd_resume(store: Path, project_dir: Path) -> int:
found = discover(store)
assert found, "no contract in canonical state"
project, contract_id = found
log(f"FRESH builder resume: {project}/{contract_id} (canonical only)")
before = t14.snapshot_files(project_dir)
iface = ci.ConstructionInterface(
builder=BUILDER_MODEL + "-resumed",
project_store=str(store / "projects" / project),
project_id=project)
projection = t14.build_projection(
iface, contract_id, phase_note="resumed after real interruption")
operation_id = f"t15-resume-{contract_id}"
ew.register_process_wait(
operation_id, contract_id=contract_id, project=project,
objective=REQUEST, watched_for="builder exit event",
watchdog_secs=900)
builder_cmd = ["mimo", "run", "-m", BUILDER_MODEL, "--format", "json",
t14.builder_prompt(projection, resumed=True)
+ f" [op:{operation_id}]"]
notifier = ew.run_builder_under_events(builder_cmd, operation_id,
daemon_url=DAEMON_URL)
log("driver YIELDS on the bridge — no polling")
result = ew.await_resume_event(operation_id, watchdog_secs=900,
pgid=notifier.pid)
if result["kind"] == "watchdog":
rec = ew.resume_or_recover(operation_id)
if rec["kind"] == "event":
log(f"watchdog fired but canonical state shows the resume "
f"build completed (verdict={rec['wait'].get('verdict')})")
return finish_via_canaries(store, project, contract_id,
project_dir, before,
"resume via canonical recovery")
log("PROCESS_LOST on resume — UNKNOWN_INTERRUPTION")
return 4
return finish_via_canaries(store, project, contract_id, project_dir,
before, "fresh-builder event-driven resume")
def cmd_finish(store: Path, project_dir: Path) -> int:
"""Complete lifecycle from CANONICAL state only (waitstore verdict +
filesystem reality). Used after a watchdog→canonical recovery."""
found = discover(store)
assert found, "no contract in canonical state"
project, contract_id = found
before = t14.snapshot_files(project_dir)
return finish_via_canaries(store, project, contract_id, project_dir,
before, "canonical-only completion")
def cmd_status(store: Path) -> int:
found = discover(store)
if not found:
print(json.dumps({"active": False}))
return 0
project, contract_id = found
iface = ci.ConstructionInterface(
builder="inspector", project_store=str(store / "projects" / project),
project_id=project)
print(json.dumps(iface.construction_context(contract_id)["state"],
indent=1, default=str))
return 0
def main() -> int:
import argparse
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("mode", choices=["start", "resume", "finish",
"status"])
ap.add_argument("--store", required=True)
ap.add_argument("--project-dir", required=False)
ap.add_argument("--interrupt-after-first-file", action="store_true")
args = ap.parse_args()
store = Path(args.store).resolve()
project_dir = (Path(args.project_dir).resolve()
if args.project_dir else None)
if project_dir:
project_dir.mkdir(parents=True, exist_ok=True)
if args.mode == "start":
return cmd_start(store, project_dir,
args.interrupt_after_first_file)
if args.mode == "resume":
return cmd_resume(store, project_dir)
if args.mode == "finish":
return cmd_finish(store, project_dir)
return cmd_status(store)
if __name__ == "__main__":
sys.exit(main())
+15 -1
View File
@@ -31,7 +31,21 @@ def main() -> int:
# PYTHONNOUSERSITE=1 pins both contexts to the same effective
# environment (verified: identical 117-error arithmetic). Local
# preflight now predicts CI instead of diverging from it.
env = dict(os.environ, PYTHONNOUSERSITE="1")
# §16U ROOT-CAUSE COMPLETION (T16): PYTHONNOUSERSITE alone is NOT
# sufficient when a DIFFERENT venv is active (e.g. an ad-hoc CI venv):
# the pyright wrapper resolves the ambient VIRTUAL_ENV/PATH
# interpreter and analyzes against ITS site-packages — reproduced
# live (117 vs 124 on the same tree, driven by venv-activation state
# alone). The CI runner resolves the AMBIENT python (no venv is
# activated in the type-check job), so the ratchet neutralizes every
# ambient-venv signal before invoking pyright — analysis context is
# now identical to CI regardless of the calling shell.
env = {k: v for k, v in os.environ.items()
if k not in ("VIRTUAL_ENV",)}
env["PYTHONNOUSERSITE"] = "1"
env["PATH"] = os.pathsep.join(
p for p in env.get("PATH", "").split(os.pathsep)
if "ci-venv" not in p and ".type-venv" not in p and "venv/bin" not in p)
result = subprocess.run(
cmd + [str(ROOT / "src" / "rune"), "--outputjson"],
capture_output=True,
+1 -1
View File
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
import json
from pathlib import Path
__version__ = "0.66.58"
__version__ = "0.66.68"
# One build identity (handoff 2026-09-10 §4): the semantic version above is
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
+1 -1
View File
@@ -231,7 +231,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.66.58"' in rune_version,
'__version__ = "0.66.68"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+173
View File
@@ -0,0 +1,173 @@
"""construction_commands.py — T16 §16J: bounded validated inbound
command interface for future Substrate integration.
The future Substrate ASKS; Scaffolding DECIDES. Every command:
- carries identity (command_id, correlation_id, construction_id)
- is validated by Scaffolding against the existing lifecycle
(LEGAL_TRANSITIONS — no new lifecycle invented)
- cannot directly mutate storage (commands return validated intents;
the Construction Interface executes them through its own governed
path — the caller never writes the store)
- is idempotent where applicable (repeat delivery of the same
command_id returns the same recorded outcome)
Only commands that fit EXISTING semantics safely are exposed. PAUSE and
CANCEL are intentionally NOT exposed: the lifecycle has no paused phase
and abandonment needs user provenance — a future Substrate asking for
them gets a refusal, not a half-measure.
"""
from __future__ import annotations
import time
import uuid
from rune.scaffold import LEGAL_TRANSITIONS
COMMANDS = (
"REQUEST_STATE", "START", "RESUME", "ACKNOWLEDGE_BLOCKER",
"PROVIDE_APPROVAL", "PROVIDE_CONTEXT",
)
# Command → the interface operation it asks for (the interface still
# decides: validation failures return refused, never raise into storage).
_COMMAND_TO_OP = {
"REQUEST_STATE": "resume", # read-only state projection
"START": "transition", # draft → approved (then building)
"RESUME": "resume", # re-read canonical state
"ACKNOWLEDGE_BLOCKER": "transition", # blocked → building (retry path)
"PROVIDE_APPROVAL": "transition", # draft → approved with provenance
"PROVIDE_CONTEXT": "amend_scope", # context arrives as scope amendment
}
# In-memory idempotency ledger: command_id → recorded outcome. The
# Scaffolding lifecycle is the authority; this ledger only makes repeat
# delivery return the same answer (no double transition).
_outcomes: dict[str, dict] = {}
def command(command_type: str, *, construction_id: str,
correlation_id: str = "", command_id: str = "",
payload: dict | None = None,
scaffold=None) -> dict:
"""Validate an inbound command. Returns a validated intent envelope:
{"accepted": True, "op": <interface op>, "args": {...},
"command_id": ..., "idempotent_replay": bool}
or {"accepted": False, "reason": ...}. The caller executes the intent
through the Construction Interface — it never touches storage."""
payload = payload or {}
command_id = command_id or f"cmd-{uuid.uuid4().hex[:8]}"
if command_id in _outcomes:
prior = dict(_outcomes[command_id])
prior["idempotent_replay"] = True
return prior
if command_type not in COMMANDS:
return _refuse(command_id, construction_id, correlation_id,
f"unknown command {command_type!r} "
f"(supported: {', '.join(COMMANDS)})")
if not construction_id:
return _refuse(command_id, construction_id, correlation_id,
"construction_id is required")
intent = _validate(command_type, construction_id,
correlation_id, command_id, payload, scaffold)
_outcomes[command_id] = intent
return intent
def _refuse(command_id: str, construction_id: str,
correlation_id: str, reason: str) -> dict:
return {"accepted": False, "reason": reason,
"command_id": command_id, "construction_id": construction_id,
"correlation_id": correlation_id,
"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
time.gmtime()),
"idempotent_replay": False}
def _validate(command_type: str, construction_id: str,
correlation_id: str, command_id: str,
payload: dict, scaffold) -> dict:
"""Validate against the CURRENT canonical state — the lifecycle
decides, not the requester. The scaffold (canonical store handle) is
supplied by the serving layer; the client only ever sees the returned
envelope, never the store."""
if scaffold is None:
return _refuse(command_id, construction_id, correlation_id,
"no serving scaffold bound (server-side wiring)")
doc = scaffold.load()
contract = None
for e in doc.get("entities", []):
if e.get("id") == f"contract:{construction_id}":
contract = e
break
if contract is None:
return _refuse(command_id, construction_id, correlation_id,
f"unknown construction {construction_id!r}")
phase = contract.get("phase", "")
if command_type == "REQUEST_STATE":
return _accept(command_id, construction_id, correlation_id,
"resume", {"contract_id": construction_id})
if command_type == "RESUME":
if phase in ("closed", "abandoned"):
return _refuse(command_id, construction_id, correlation_id,
f"construction is {phase} — resume is invalid")
return _accept(command_id, construction_id, correlation_id,
"resume", {"contract_id": construction_id})
if command_type == "START":
if phase != "draft":
return _refuse(command_id, construction_id, correlation_id,
f"START requires draft phase (current: {phase})")
return _accept(command_id, construction_id, correlation_id,
"transition",
{"contract_id": construction_id,
"phase": "approved"})
if command_type == "PROVIDE_APPROVAL":
if phase != "draft":
return _refuse(command_id, construction_id, correlation_id,
f"approval applies to draft (current: {phase})")
if payload.get("approved_by") != "user":
# lifecycle rule: approval provenance must be the user —
# Substrate cannot approve on the user's behalf
return _refuse(command_id, construction_id, correlation_id,
"approval provenance must be user "
"(Substrate cannot approve for the user)")
return _accept(command_id, construction_id, correlation_id,
"transition",
{"contract_id": construction_id,
"phase": "approved",
"approved_by": "user"})
if command_type == "ACKNOWLEDGE_BLOCKER":
if phase not in ("failed", "building", "verifying"):
return _refuse(command_id, construction_id, correlation_id,
f"no blockable phase (current: {phase})")
return _accept(command_id, construction_id, correlation_id,
"transition",
{"contract_id": construction_id,
"phase": "building"})
if command_type == "PROVIDE_CONTEXT":
paths = payload.get("add_paths") or []
if not paths:
return _refuse(command_id, construction_id, correlation_id,
"PROVIDE_CONTEXT requires add_paths")
return _accept(command_id, construction_id, correlation_id,
"amend_scope",
{"contract_id": construction_id,
"add_paths": paths,
"reason": payload.get("reason", "substrate context"),
"authority": "user"
if payload.get("approved_by") == "user" else "substrate"})
return _refuse(command_id, construction_id, correlation_id,
"unreachable validator branch")
def _accept(command_id: str, construction_id: str, correlation_id: str,
op: str, args: dict) -> dict:
return {"accepted": True, "op": op, "args": args,
"command_id": command_id, "construction_id": construction_id,
"correlation_id": correlation_id,
"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
time.gmtime()),
"idempotent_replay": False}
+114 -1
View File
@@ -214,7 +214,11 @@ class ConstructionInterface:
"""§28 schema negotiation: builders detect the supported interface
schema and operation set BEFORE calling operations. Also exposes the
error-state vocabulary so builders can switch on states without
parsing prose. Read-only; no contract required."""
parsing prose. Read-only; no contract required.
§15N: full argument schemas included — a fresh builder discovers
required types/fields deterministically instead of trial-and-error
(the T14 report_observation dict-vs-list + missing-entity mistakes
are exactly what this prevents)."""
return _envelope(
"capabilities",
success=True,
@@ -223,6 +227,7 @@ class ConstructionInterface:
"mode": self.mode,
"project": self.scaffold.project_id,
"operations": [
"discover_active_contract",
"submit_need",
"inspect_decision",
"get_scope",
@@ -236,6 +241,67 @@ class ConstructionInterface:
"capabilities",
],
"error_states": sorted(v for k, v in vars(Errors).items() if not k.startswith("_")),
# ── §15N argument schemas (discovery over guessing) ──
"argument_schemas": {
"submit_need": {
"objective": {"type": "str", "required": True},
"origin": {"type": "str", "default": "builder"},
"constraints": {"type": "dict", "required": False,
"notes": ("{'environment': {...}, "
"'contracts': [...]} — "
"environment missing "
"degrades to "
"INSUFFICIENT_EVIDENCE")},
"affected_surface": {"type": "list[str]",
"required": False},
"evidence_refs": {"type": "list[str]",
"required": True,
"notes": ("empty list degrades "
"fit resolution")},
"approval_context": {"type": "str",
"required": False},
},
"report_observation": {
"observations": {
"type": "list[dict]",
"required": True,
"item_required_fields": ["entity"],
"item_optional_fields": ["source", "detail"],
"notes": ("a LIST of dicts, each with an "
"'entity' key — a bare dict or a "
"dict without 'entity' is rejected "
"(NEED_INCOMPLETE)"),
},
},
"verify": {
"changed_files": {"type": "list[str] | None",
"required": False,
"notes": ("scope-checked against "
"the contract scope; "
"out-of-scope files → "
"SCOPE_VIOLATION")},
},
"transition": {
"phase": {"type": "str", "required": True,
"allowed": list(sc.PHASES)},
},
"close": {
"observed_evidence": {
"type": "list[dict]", "required": True,
"notes": ("evidence INPUT only — closure is "
"decided by verification truth, "
"never by builder claim")},
"scope_files": {"type": "list[str] | None",
"required": False},
},
},
"lifecycle": {
"phases": list(sc.PHASES),
"legal_transitions": {k: sorted(v) for k, v in
sc.LEGAL_TRANSITIONS.items()},
"scope_amendment": ("user-gated via amend_scope — "
"builders cannot broaden scope"),
},
},
)
@@ -617,6 +683,53 @@ class ConstructionInterface:
},
)
@staticmethod
def discover_active_contract(store_path: str) -> dict:
"""§15U canonical resume discovery: walk the store's project
scaffolds AND the main store's own current.json, returning the
most recent contract's (project, contract_id). Migrated here from
the T14/T15 session drivers — a fresh builder with NO transcript
discovers what to resume through the INTERFACE, not a script.
Pure readback: no session file, no transcript, no mutation."""
from pathlib import Path as _P
import json as _json
root = _P(store_path)
candidates: list[tuple] = []
for proj_current in sorted(root.glob("projects/*/current.json")):
# per-project scaffold: (file, project_id)
candidates.append((proj_current, proj_current.parent.name))
main = root / "current.json"
if main.exists():
# main store: contracts carry their project in identities
candidates.append((main, None))
if not candidates:
return _envelope("discover_active_contract", success=False,
errors=[{"error": Errors.UNKNOWN_CONTRACT,
"detail": "no projects in store"}])
best = None
best_project = ""
for proj_current, project in candidates:
try:
doc = _json.loads(proj_current.read_text())
except (OSError, ValueError):
continue
for e in doc["entities"]:
if e["kind"] == "construction_contract":
proj = project or e.get("identities", {}).get(
"project", root.name)
key = (e.get("observed_at") or "", proj,
e["id"].split(":", 1)[1])
if best is None or key > (best[0], best[1], best[2]):
best = key
best_project = proj
if best is None:
return _envelope("discover_active_contract", success=False,
errors=[{"error": Errors.UNKNOWN_CONTRACT,
"detail": "no contracts in store"}])
return _envelope("discover_active_contract", success=True,
ids={"project": best_project,
"contract_id": best[2]})
def resume(self, contract_id: str) -> dict:
doc = self.scaffold.load()
c = self.scaffold.contract(doc, contract_id)
+28 -1
View File
@@ -64,6 +64,10 @@ _STANDALONE_WORDS = {
"scaffold a new",
"new web",
}
# T15 §15J: extensionless filenames that bind a scoped target when
# mentioned explicitly (dotted/path tokens go through classify_surface)
_KNOWN_FILES = {"readme", "makefile", "dockerfile", "changelog", "license",
"gemfile", "procfile", "jenkinsfile", "justfile"}
_LOCAL_WORDS = {
"typo",
"small fix",
@@ -267,6 +271,27 @@ def classify_mode(
standalone = _contains_any(obj.lower(), _STANDALONE_WORDS)
local = _contains_any(obj.lower(), _LOCAL_WORDS)
scoped = _contains_any(obj.lower(), _SCOPING_WORDS)
# T15 §15J: an explicit FILE mention binds a scoped target ("fix the
# typo in the README", "update scheduler.js"). Deterministic lexical
# evidence in the same corroboration style as the other lexicons;
# dotted/path tokens delegated to the ONE surface authority
# (construction_decision.classify_surface) — lazily imported (cycle:
# construction_decision imports this module's classify_mode).
file_scoped = False
for w in re.split(r"[^a-z0-9_./-]+", obj.lower()):
if not w or len(w) < 3:
continue
if w in _KNOWN_FILES or "." in w or "/" in w:
try:
from rune.construction_decision import classify_surface
if classify_surface(w) in ("source_path", "workflow_config"):
file_scoped = True
break
except Exception: # noqa: BLE001,S110 — authority unavailable:
pass # lexical fallback decides; never fatal
if w in _KNOWN_FILES:
file_scoped = True
break
# 3. PROJECT_BUILD: standalone/new-lifecycle language AND no existing
# project for the target (project existence materially matters, §5)
@@ -286,10 +311,12 @@ def classify_mode(
mode, conf = "PROJECT_BUILD", 0.9
bootstrap = True
# 4. LOCAL_CHANGE: explicit smallness + scoped target + no standalone
elif local and (scoped or target_project or project_exists):
elif local and (scoped or file_scoped or target_project or project_exists):
ev.append(f"smallness markers: {local[:3]}")
if scoped:
ev.append(f"scoping words bind to known target: {scoped[:3]}")
if file_scoped:
ev.append("file mention binds scoped target")
mode, conf = "LOCAL_CHANGE", 0.8
# 5. EXISTING_PROJECT_CHANGE: scoping to a known/existing target.
# T11 §1: a change scoped to a project with PROVEN non-existence must
+135
View File
@@ -0,0 +1,135 @@
"""construction_projection.py — T16 §16I: stable versioned PUBLIC
projection of construction state for future Substrate integration.
This is a PROJECTION, never a second authority. All fields are derived
(read-only) from the canonical scaffold store (Scaffold.load) plus the
live wait bridge state (event_wait.operation_wait). A consumer (the
future Substrate, the mock client in tests) must know NOTHING about
current.json layout or internal entity arrays — only this schema.
Schema versioning: SCHEMA_VERSION is bumped on any field change; consumers
negotiate by reading schema_version first. Unknown future fields are
ignored by readers (forward-tolerant); removed fields bump the major.
"""
from __future__ import annotations
from typing import Any
SCHEMA_VERSION = 1
# The mandate's required projection fields (§16I). None may carry live
# provider/account secrets or internal storage layout.
FIELDS = (
"schema_version", "project_id", "originating_goal_id",
"construction_id", "contract_id",
"mode", "depth", "phase", "stage",
"operation_id", "waiting_state", "correlation_id",
"builder_state", "blockers", "approval_required",
"readiness_canary", "liveness_canary", "exit_canary",
"verification_state", "checkpoint", "evidence_refs",
"failure_class", "completion_state", "candidate_next_action",
)
def _contract_entity(doc: dict, contract_id: str) -> dict | None:
for e in doc.get("entities", []):
if e.get("id") == f"contract:{contract_id}":
return e
return None
def project(
scaffold, contract_id: str, *,
originating_goal_id: str = "",
operation_id: str = "",
correlation_id: str = "",
stage: str = "",
builder_state: str = "",
verification_state: str = "",
checkpoint: str = "",
failure_class: str = "",
completion_state: str = "",
) -> dict:
"""Build the stable public projection for one contract. `scaffold` is
a Rune Scaffold instance (the canonical store); all live fields are
derived from it. Optional live fields (waiting_state etc.) are filled
from the event bridge when operation_id is supplied."""
doc = scaffold.load()
c = _contract_entity(doc, contract_id) or {}
ids = c.get("identities", {})
ext = c.get("ext", {})
trace = ext.get("decision_trace", {})
waiting_state = ""
live_correlation = correlation_id
if operation_id:
try:
from rune import event_wait as ew
rec = ew.operation_wait(operation_id)
if rec is not None:
waiting_state = rec.get("status", "")
live_correlation = correlation_id or rec.get("wait_id", "")
except OSError: # bridge data root unreadable → degrade, never lie
waiting_state = "BRIDGE_UNREACHABLE"
except RuntimeError: # waitstore not resolvable in this context
waiting_state = "BRIDGE_UNREACHABLE"
checkpoints = c.get("checkpoints", [])
evidence_refs = []
for cp in checkpoints:
ev = cp.get("evidence", "")
if ev:
evidence_refs.append(ev[:120])
approval = trace.get("approval", {})
approval_required = bool(
approval.get("required") or approval.get("state") == "REQUIRED"
or c.get("phase") == "awaiting_approval")
blockers: list[str] = []
blk = ids.get("blocker")
if isinstance(blk, dict):
blockers.append(f"{blk.get('class', 'BLOCKED')}: "
f"{blk.get('evidence', '')[:120]}")
elif isinstance(blk, str) and blk:
blockers.append(blk)
return {
"schema_version": SCHEMA_VERSION,
"project_id": getattr(scaffold, "project_id", ""),
"originating_goal_id": originating_goal_id,
"construction_id": contract_id,
"contract_id": contract_id,
"mode": ids.get("construction_mode", "") or "",
"depth": ids.get("depth", "") or "",
"phase": c.get("phase", ""),
"stage": stage,
"operation_id": operation_id,
"waiting_state": waiting_state,
"correlation_id": live_correlation,
"builder_state": builder_state,
"blockers": blockers,
"approval_required": approval_required,
"readiness_canary": ext.get("readiness_canary", ""),
"liveness_canary": ext.get("liveness_canary", ""),
"exit_canary": ext.get("exit_canary", ""),
"verification_state": verification_state,
"checkpoint": checkpoint,
"evidence_refs": evidence_refs,
"failure_class": failure_class,
"completion_state": completion_state,
"candidate_next_action": ids.get("next_action", ""),
}
def validate(projection: dict) -> list[str]:
"""Check a projection against the schema. Returns a list of errors
(empty = valid). Consumers use this before trusting a projection."""
errors = []
if projection.get("schema_version") != SCHEMA_VERSION:
errors.append(f"schema_version: expected {SCHEMA_VERSION}, got "
f"{projection.get('schema_version')!r}")
for f in FIELDS:
if f not in projection:
errors.append(f"missing field: {f}")
return errors
+73
View File
@@ -0,0 +1,73 @@
"""drift_resume.py — T16 §16P: drift-aware resume for months-later
project re-entry.
Months-later resume must NOT blindly trust the last checkpoint. The
16O benchmark proved both cold and scaffold-assisted paths converge on
the same principle: checkpoint claims NEVER substitute for reality.
Flow:
load last verified scaffold state (expectations)
↓
inspect current repo identity/state (observations)
↓
compare expected vs actual → DriftRecord list
↓
unchanged: resume
drift: classify EXPECTED / BENIGN / MATERIAL / CONFLICTING /
UNKNOWN — MATERIAL/UNKNOWN can block dependent work
Reality wins unless the claim carries explicit user-locked authority
(the one scaffold exception, already enforced by layout_contract).
"""
from __future__ import annotations
from typing import Callable
EXPECTED = "EXPECTED" # checkpoint older than reality; benign aging
BENIGN = "BENIGN" # difference with no bearing on the claims
MATERIAL = "MATERIAL" # reality contradicts a scaffold claim
CONFLICTING = "CONFLICTING" # two authorities disagree; needs user
UNKNOWN = "UNKNOWN" # cannot decide — treated as blocking
BLOCKING_CLASSES = (MATERIAL, CONFLICTING, UNKNOWN)
def classify_drift(claim: dict, observation: dict) -> dict:
"""Classify one claim-vs-observation pair. Deterministic:
verification evidence beats prose; explicit beats inferred.
claim: {"claim": str, "kind": str, "verified_at": str,
"user_locked": bool}
observation: {"actual": str|None, "checked_by": str,
"matches": bool|None}
"""
if observation.get("matches") is True:
return {"claim": claim.get("claim", ""),
"class": EXPECTED if claim.get("stale_ok") else "CONFIRMED",
"evidence": observation.get("checked_by", "")}
if observation.get("matches") is None:
return {"claim": claim.get("claim", ""), "class": UNKNOWN,
"evidence": f"unverifiable via {observation.get('checked_by', '?')}"}
if claim.get("user_locked"):
return {"claim": claim.get("claim", ""), "class": CONFLICTING,
"evidence": ("reality differs from a USER-LOCKED claim "
"— user arbitration required")}
return {"claim": claim.get("claim", ""), "class": MATERIAL,
"evidence": (f"reality differs: {observation.get('actual', '')}"
f" (checked by {observation.get('checked_by', '?')})")}
def reconcile(claims: list[dict], observer: Callable[[dict], dict]) -> dict:
"""Reconcile a set of checkpoint claims against reality.
observer(claim) → observation dict (runs the actual check: test
command, file read, git query — never trusts the claim text).
Returns {"resume": bool, "drifts": [...], "blocking": [...]}. The
resume decision: drift-aware, not blind — ANY blocking-class drift
stops dependent work until reconciled."""
drifts = [classify_drift(c, observer(c)) for c in claims]
blocking = [d for d in drifts if d["class"] in BLOCKING_CLASSES]
return {"resume": not blocking, "drifts": drifts,
"blocking": blocking}
+457
View File
@@ -0,0 +1,457 @@
"""event_wait.py — T15 §15D: event-driven wait as the CANONICAL DEFAULT
for long-running construction operations (builders, CI, deploys).
GLOBAL WAITING INVARIANT (tranche brief):
EVENT DRIVES CONTINUATION.
TIMEOUT DETECTS FAILURE OF CONTINUATION.
The W/R bridge (daemon.py + waitstore.py at RUNE_WR_ROOT, default
~/.local/share/mimo-wait-resume) already provides: durable waits, event
matching, per-session single-flight leases, process-tree ownership, and a
resume driver. Until T15 its resume target was exclusively a MiMo
session (global mimo-shaped resume_cmd), so Python controllers (stage
drivers, builder sessions) could not be resumed by the bridge — which is
exactly why fixed sleep/poll loops crept back into T12-T14.
This adapter CLOSES that gap with the smallest possible surface:
1. register_process_wait() — a controller/operation registers a
PROCESS_EVENT wait (correlation: operation_id) whose per-wait
resume_cmd writes a resume token to a local FIFO. Registration is a
durable waitstore record: survives restarts, visible to reconciliation.
2. run_builder_under_events() — launches the real builder command under
a notifier wrapper (separate session/process-group). The wrapper
observes the builder's actual exit and POSTs the completion event to
the daemon (/process-event). The wrapper is the event source; the
driver does NOT wait on the builder directly.
3. await_resume_event() — the driver YIELDS: blocks in select() on the
FIFO (no polling loop, no sleep floor) with a watchdog deadline. The
daemon matches the event, advances the wait, and the per-wait
resume_cmd fires immediately — the driver resumes on the EVENT, not
after a configured delay.
4. Watchdog semantics: on timeout the driver performs a ONE-SHOT
liveness inspection of the notifier process group (ALIVE → re-arm;
GONE → PROCESS_LOST recovery). The timeout is a failure detector,
never the scheduler.
Idempotency (15E) is inherited from the bridge: match() applies only to
WAITING_EXTERNAL waits, so duplicate/late events cannot double-resume;
correlation by operation_id (run_ref) means a stale event from a prior
operation matches nothing; the reconciler never verifies PROCESS_EVENT
waits against Gitea (the event is the authority).
"""
from __future__ import annotations
import importlib.util
import json
import os
import select
import signal
import subprocess
import sys
import time
from pathlib import Path
from typing import Any
DEFAULT_WR_ROOT = Path.home() / ".local" / "share" / "mimo-wait-resume"
FALLBACK_WR_ROOT = Path("/home/nick/.local/share/mimo-wait-resume")
DEFAULT_DAEMON_URL = "http://127.0.0.1:8791"
# The notifier wrapper: runs the real builder command as its child,
# observes the ACTUAL exit, and POSTs the completion event to the daemon.
# It is the event SOURCE — deliberately independent of the waiting driver.
_NOTIFIER_SRC = r"""
import json, os, subprocess, sys, time, urllib.request
op = sys.argv[1]; cmd = sys.argv[2:]
start = time.time()
proc = subprocess.run(cmd)
completed_at = time.time()
payload = {
"operation_id": op,
"exit_code": proc.returncode,
"completed_at": completed_at,
"started_at": start,
}
for attempt in range(3):
try:
req = urllib.request.Request(
os.environ["T15_DAEMON_URL"] + "/process-event",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=5) as r:
r.read()
break
except Exception as e:
if attempt == 2:
sys.stderr.write(f"event POST failed: {e}\n")
# best-effort: the watchdog is the safety net for lost events
time.sleep(1)
"""
def wr_root() -> Path:
env = os.environ.get("RUNE_WR_ROOT")
if env:
return Path(env)
if DEFAULT_WR_ROOT.exists():
return DEFAULT_WR_ROOT
return FALLBACK_WR_ROOT
# waitstore instances cached per data root (isolated tests use per-test
# tmp roots; the daemon uses its install dir). Loading via importlib.util
# keeps the dynamic import pyright-clean — no unresolvable-import warning.
_waitstore_cache: dict[str, Any] = {}
def _load_waitstore() -> Any:
"""Load the bridge's OWN waitstore module (never a copy), bound to
the adapter's data root. waitstore.py lives in the DAEMON CODE dir;
in production WR_ROOT is that dir (code + data together); in isolated
test environments WR_ROOT is a data-only dir, so the code dir is
resolved separately: RUNE_WR_CODE env → WR_ROOT → known installs."""
root = wr_root()
# waitstore binds its DATA dir from WR_ROOT at import time. Pin
# WR_ROOT to the adapter's authority so adapter-side and daemon-side
# state agree — otherwise a mismatch silently splits the wait store
# from the event matcher and continuation dies as a watchdog timeout.
os.environ["WR_ROOT"] = str(root)
cached = _waitstore_cache.get(str(root))
if cached is not None:
return cached
code_dir = None
for cand in (os.environ.get("RUNE_WR_CODE"), str(root),
str(FALLBACK_WR_ROOT)):
if cand and (Path(cand) / "waitstore.py").is_file():
code_dir = Path(cand)
break
if code_dir is None:
raise RuntimeError(
"waitstore.py not found in known W/R roots "
"(RUNE_WR_CODE, WR_ROOT, ~/.local/share/mimo-wait-resume)")
spec = importlib.util.spec_from_file_location(
"waitstore", str(code_dir / "waitstore.py"))
if spec is None or spec.loader is None:
raise RuntimeError(f"cannot load waitstore from {code_dir}")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
_waitstore_cache[str(root)] = mod
return mod
def register_process_wait(operation_id: str, *, contract_id: str = "",
project: str = "", objective: str = "",
watched_for: str = "",
provider: str = "builder",
event_type: str = "builder_exit",
sha: str = "",
watchdog_secs: int | None = None) -> dict:
"""Register a durable PROCESS_EVENT wait for one operation. The
per-wait resume_cmd writes a resume token to the operation's FIFO —
the bridge can resume a Python controller, not just MiMo sessions.
provider/event_type are the adapter's event identity on the shared
bridge (16E: ONE wait primitive; builder / Gitea-CI / future adapters
differ ONLY in these identity fields + their liveness probe — the
registration, FIFO rendezvous, matching, idempotency, restart
recovery, and watchdog semantics are the same code for all).
Defaults preserve the T15 builder semantics."""
ws = _load_waitstore()
fifo = fifo_path(operation_id)
fifo.parent.mkdir(parents=True, exist_ok=True)
# Do NOT unlink+recreate an existing fifo: a caller that opened its
# read end before registering (open_fifo_reader, §16Y #10 race fix)
# would be orphaned onto the old inode while the daemon's resume
# writer opened the new one. The rendezvous is create-if-missing.
if not fifo.exists():
os.mkfifo(fifo)
os.chmod(fifo, 0o666) # cross-user bridge (see open_fifo_reader)
writer = ("import sys,json,time,os;"
"fd=os.open(sys.argv[1], os.O_WRONLY | os.O_NONBLOCK);"
"os.write(fd, json.dumps({'wait_id': sys.argv[2],"
" 'resumed_at': time.time()}).encode());os.close(fd)")
rec = ws.new_wait(
session_id=f"controller-{operation_id}",
provider=provider,
event_type=event_type,
sha=sha,
repo=project or "construction",
run_ref=operation_id,
expected="success",
wait_kind="PROCESS_EVENT",
target_kind="headless", # owned continuation target: FIFO writer
objective=objective or f"builder operation {operation_id}",
waited_for=watched_for or "builder process exit event",
verify_when_woken="reload canonical state; exit canary; "
"independent verification",
on_success="continue programme continuation canary",
on_failure="classify failure; recovery or block",
resume_cmd=["python3", "-c", writer, str(fifo), "{wait_id}"],
)
rec["ext"] = {"contract_id": contract_id, "operation_id": operation_id,
"fifo": str(fifo),
"watchdog_secs": watchdog_secs
or int(os.environ.get("WR_WATCH_SECS", "600"))}
ws.save(rec)
# cross-user bridge (T16 §16C live finding): the daemon reconciler may
# run under a different account than the registering controller — the
# durable record must be readable/writable on both sides
try:
os.chmod(ws.WAITS / f"{rec['wait_id']}.json", 0o666)
except OSError:
pass # single-user deployment: nothing to relax
return rec
def fifo_path(operation_id: str) -> Path:
return wr_root() / "resumes" / f"{operation_id}.fifo"
def open_fifo_reader(operation_id: str) -> int:
"""Open the operation's FIFO read end (O_NONBLOCK) BEFORE the wait is
registered. §16Y adversarial #10: an event that arrives after
registration but before the controller enters select() would find no
reader — the resume write fails (ENXIO) and the wait lands
RESUME_UNVERIFIED while the controller sleeps until the watchdog.
Holding the reader open across the registration window closes that
race for every adapter; the fd is passed to await_resume_event and
closed by the caller."""
fifo = fifo_path(operation_id)
fifo.parent.mkdir(parents=True, exist_ok=True)
if fifo.exists():
fifo.unlink()
os.mkfifo(fifo)
# CROSS-USER BRIDGE (T17 17C root-cause, re-landed): the T16 chmod was
# lost in a rebase resolution — FIFOs landed 644 (666 & umask 022),
# and the daemon running as a DIFFERENT user got EACCES on the resume
# write (rc=1 → RESUME_UNVERIFIED) while same-user tests passed.
# chmod AFTER mkfifo is the only reliable act — umask never applies.
os.chmod(fifo, 0o666)
return os.open(fifo, os.O_RDONLY | os.O_NONBLOCK)
def run_builder_under_events(builder_cmd: list[str], operation_id: str, *,
daemon_url: str = DEFAULT_DAEMON_URL):
"""Launch the REAL builder under the event notifier (separate process
group). Returns the Popen for the notifier (pgid == its pid). The
driver must NOT wait on this directly — it yields via
await_resume_event; the notifier's exit event comes back through the
bridge."""
env = dict(os.environ, T15_DAEMON_URL=daemon_url)
log_file = wr_root() / "resumes" / f"{operation_id}.builder.log"
log_file.parent.mkdir(parents=True, exist_ok=True)
with open(log_file, "w") as lf:
return subprocess.Popen(
[sys.executable, "-c", _NOTIFIER_SRC, operation_id, *builder_cmd],
stdout=lf, stderr=subprocess.PIPE,
text=True, start_new_session=True, env=env)
def _pgroup_alive(pgid: int) -> bool:
"""Liveness = at least one NON-ZOMBIE member of the process group.
A killed-but-unreaped notifier is a zombie and does NOT count —
treating zombies as alive would misclassify PROCESS_LOST as alive."""
try:
os.killpg(pgid, 0) # group exists at all?
except (ProcessLookupError, PermissionError):
return False
for proc in Path("/proc").iterdir():
if not proc.name.isdigit():
continue
try:
stat = (proc / "stat").read_text()
# field 5 is state; comm (field 2) may contain spaces/parens
state = stat.rsplit(")", 1)[1].split()[0]
pgid_field = int(stat.rsplit(")", 1)[1].split()[2])
except (OSError, ValueError, IndexError):
continue
if pgid_field == pgid and state != "Z":
return True
return False
def _cmdline_alive(needle: str) -> bool:
"""Liveness fallback: any live process whose cmdline contains the
needle. Real builders (node CLIs) may re-group their children, so
pgid alone can false-negative — T15Q observed a 65-minute real build
outliving the watchdog AND invisible to the pgid scan. The needle
should be operation-specific (the prompt marker), not generic."""
for proc in Path("/proc").iterdir():
if not proc.name.isdigit():
continue
try:
cmdline = (proc / "cmdline").read_bytes().decode(
errors="replace")
stat = (proc / "stat").read_text()
state = stat.rsplit(")", 1)[1].split()[0]
except (OSError, ValueError, IndexError):
continue
if needle in cmdline and state != "Z":
return True
return False
def liveness(operation_id: str, pgid: int | None = None) -> bool:
"""Watchdog helper: is the watched operation actually alive? pgid
membership first; cmdline evidence as the regroup-tolerant fallback."""
if pgid and _pgroup_alive(pgid):
return True
return _cmdline_alive(f"op:{operation_id}")
def await_resume_event(operation_id: str, *, watchdog_secs: int,
pgid: int | None = None,
max_rearms: int = 20,
fifo_fd: int | None = None) -> dict:
"""YIELD until the bridge resumes this controller. Blocks in select()
on the FIFO — no sleep, no polling.
fifo_fd: a pre-opened read end from open_fifo_reader() (§16Y #10 race
fix — the fd must already exist when the wait becomes registered).
Caller-owned fds are never closed here; on EOF (writer gone, no
token) the call returns {"kind": "eof"} instead of busy-looping on an
always-readable fd — the caller falls back to the durable waitstore
record via resume_or_recover/operation_wait.
Watchdog semantics (§15D): the deadline is a FAILURE DETECTOR, never
the scheduler. On expiry, liveness is inspected ONCE (pgid membership,
then operation-specific cmdline evidence — real builders regroup their
children): ALIVE → re-arm (select again, up to max_rearms); GONE →
PROCESS_LOST. The T15Q live run proved why: a real 65-minute model
build outlived a single 900s deadline, and node regrouping hid the
processes from the pgid scan — neither is a failure of continuation.
"""
owned_fd = fifo_fd is None
if owned_fd:
fifo = fifo_path(operation_id)
# idempotent re-arm: a prior await consumed+unlinked the fifo; the
# rendezvous point is (re)created so watchdog re-arm and recovery
# paths can wait again on the same operation
fifo.parent.mkdir(parents=True, exist_ok=True)
if not fifo.exists():
os.mkfifo(fifo)
fd = os.open(fifo, os.O_RDONLY | os.O_NONBLOCK)
else:
fd = fifo_fd
try:
for arm in range(max_rearms + 1):
deadline = time.monotonic() + watchdog_secs
while True:
remaining = deadline - time.monotonic()
if remaining <= 0:
break
r, _, _ = select.select([fd], [], [], remaining)
if not r:
continue
data = os.read(fd, 4096)
if not data:
if owned_fd:
continue
# caller-owned fd at EOF: writer is gone without
# delivering a token — never a busy loop; the durable
# record decides what actually happened
return {"kind": "eof"}
try:
token = json.loads(data.decode())
except ValueError:
break # malformed token → treat as expiry
ws = _load_waitstore()
rec = ws.load(token.get("wait_id", ""))
return {"kind": "event", "wait": rec,
"resumed_at": token.get("resumed_at")}
# deadline expired: liveness decides re-arm vs PROCESS_LOST
alive = liveness(operation_id, pgid)
if not alive:
return {"kind": "watchdog", "alive": False,
"rearms": arm}
return {"kind": "watchdog", "alive": liveness(operation_id, pgid),
"rearms": max_rearms}
finally:
if owned_fd:
os.close(fd)
def operation_wait(operation_id: str) -> dict | None:
ws = _load_waitstore()
for rec in ws.all_waits():
if rec.get("ext", {}).get("operation_id") == operation_id:
return rec
return None
def resume_or_recover(operation_id: str) -> dict:
"""15E case 10 — controller restart after the event was persisted but
before the controller handled it. The durable waitstore record is the
authority, not the transient FIFO token: if the wait already reached a
terminal/resumable state, return it directly (idempotent recovery, no
duplicate transition); if still WAITING_EXTERNAL, re-arm the normal
event wait."""
rec = operation_wait(operation_id)
if rec is None:
return {"kind": "unknown_operation"}
if rec.get("status") in ("COMPLETED", "FAILED", "READY_TO_RESUME",
"READY_TO_RETRY", "RESUMING"):
# RESUMING counts as recovered: the resume delivery is in flight
# (or already consumed the rendezvous). The durable record is the
# truth; the controller restarts from it, never from the token.
return {"kind": "event", "wait": rec, "recovered": True}
if rec.get("status") == "RESUME_UNVERIFIED" and \
rec.get("process_event_evidence"):
# the completion EVENT was processed (verdict recorded durably);
# only the resume-token delivery was unverified (e.g. controller
# was down, so no reader consumed the rendezvous). Canonical
# state wins — this IS the restart-after-event case.
return {"kind": "event", "wait": rec, "recovered": True}
return {"kind": "still_waiting", "wait": rec}
def find_matching_event(provider: str, event_type: str, run_ref: str) -> dict | None:
"""Search the bridge's durable event log for an event matching this
identity. T16 §16C live finding: on a serialized single-runner
machine the deploy job ALWAYS starts after ci.yml completes — the
completion event lands before the waiter registers, and a pure
forward-only FIFO rendezvous can never deliver it (both production
cycles fell back to the 900s deadline). Events are durable precisely
so they can be replayed: a waiter that finds a matching event at
registration goes straight to the authority read instead of sleeping
for a wake that already happened."""
import json as _json
ws = _load_waitstore()
events_dir = ws.EVENTS
if not events_dir.is_dir():
return None
best = None
for f in sorted(events_dir.glob("*.json"),
key=lambda p: p.stat().st_mtime, reverse=True):
try:
rec = _json.loads(f.read_text())
except (OSError, ValueError):
continue
ev = rec.get("event", {})
if (ev.get("provider") == provider
and ev.get("event_type") == event_type
and str(ev.get("run_ref", "")) == run_ref):
best = ev # newest match wins
break
return best
def post_process_event(operation_id: str, exit_code: int, *,
daemon_url: str = DEFAULT_DAEMON_URL) -> dict:
"""Direct event injection (test seam + manual reconciliation). The
normal path is the notifier wrapper; this exists so tests and
watchdog recovery can emit the same event shape."""
import urllib.request
payload = {"operation_id": operation_id, "exit_code": exit_code,
"completed_at": time.time()}
req = urllib.request.Request(
daemon_url + "/process-event",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=5) as r:
return json.loads(r.read())
+2 -1
View File
@@ -192,7 +192,8 @@ def _default_runner(prompt: str, artifact_path: str) -> None:
import subprocess
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
from rune.provider_context import require_model
mimo_model = require_model() # T16 §16F: provider layer owns selection
cmd = [
mimo_bin,
"run",
+108
View File
@@ -0,0 +1,108 @@
"""failure_recovery.py — T15 §15T: failure classification + recovery
policy.
Before retrying, classify the failure:
TRANSIENT bounded immediate retry appropriate
INTERMITTENT bounded retry/backoff
KNOWN_PROCESS_DEFECT apply evidenced repair, then retry
PERMANENT / INVALID do NOT retry blindly
APPROVAL_REQUIRED block
UNKNOWN bounded evidence probe, then block/escalate
No infinite retry loops: every class carries a retry budget, and
PERMANENT/APPROVAL_REQUIRED never auto-retry.
"""
from __future__ import annotations
TRANSIENT = "TRANSIENT"
INTERMITTENT = "INTERMITTENT"
KNOWN_PROCESS_DEFECT = "KNOWN_PROCESS_DEFECT"
PERMANENT = "PERMANENT"
APPROVAL_REQUIRED = "APPROVAL_REQUIRED"
PROVIDER_UNAVAILABLE = "PROVIDER_UNAVAILABLE"
UNKNOWN = "UNKNOWN"
CLASSES = (TRANSIENT, INTERMITTENT, KNOWN_PROCESS_DEFECT, PERMANENT,
APPROVAL_REQUIRED, PROVIDER_UNAVAILABLE, UNKNOWN)
# recovery policy per class: (max_retries, action)
POLICY = {
TRANSIENT: (2, "retry_immediately"),
INTERMITTENT: (3, "retry_with_backoff"),
KNOWN_PROCESS_DEFECT: (1, "apply_evidenced_repair_then_retry"),
PERMANENT: (0, "do_not_retry"),
APPROVAL_REQUIRED: (0, "block"),
# T16 §16F: provider failures NEVER auto-retry and NEVER fall back
# to another provider — preserve the real error, block the branch.
PROVIDER_UNAVAILABLE: (0, "block_branch_preserve_error"),
UNKNOWN: (1, "bounded_evidence_probe_then_block_or_escalate"),
}
# deterministic markers (substring, lowercase) — the classifier is
# evidence-first: explicit signals win; absence defaults to UNKNOWN
_TRANSIENT_MARKERS = (
"connection reset", "timeout", "timed out", "temporarily unavailable",
"rate limit", "429", "503", "econnreset", "socket hang up",
"service unavailable",
)
_INTERMITTENT_MARKERS = (
"flaky", "intermittent", "occasionally fails", "sometimes fails",
"retry succeeded",
)
_PERMANENT_MARKERS = (
"invalid input", "schema-invalid", "not found", "no such file",
"permission denied", "unsupported", "incompatible",
"module not found", "syntaxerror", "typeerror",
)
_APPROVAL_MARKERS = (
"approval required", "needs authorization", "user confirmation",
"destructive", "external service", "new repository",
"user-locked", "user locked",
)
_KNOWN_DEFECT_MARKERS = (
"stale document", "forgotten save", "wrong path", "guessed api",
"missing import", "no-op edit", "substring match", "wrong signature",
)
def classify_failure(error_text: str, *, context: dict | None = None) -> dict:
"""Classify one failure into a recovery class. Deterministic: marker
evidence first; explicit context hints (e.g. known defect id) second;
UNKNOWN when nothing matches (never manufactured certainty)."""
text = (error_text or "").lower()
context = context or {}
known_defect = context.get("known_defect_id")
if known_defect:
return {"class": KNOWN_PROCESS_DEFECT,
"evidence": f"known defect: {known_defect}",
"policy": POLICY[KNOWN_PROCESS_DEFECT]}
# T16 §16F: an explicit provider-context failure wins over every
# generic marker — the real status/API code is preserved verbatim in
# the evidence, the branch blocks, and no fallback is attempted.
if context.get("provider_error"):
return {"class": PROVIDER_UNAVAILABLE,
"evidence": (f"provider error preserved: {error_text}"
if error_text else "provider error"),
"policy": POLICY[PROVIDER_UNAVAILABLE]}
if any(m in text for m in _APPROVAL_MARKERS):
return {"class": APPROVAL_REQUIRED,
"evidence": "approval marker present",
"policy": POLICY[APPROVAL_REQUIRED]}
if any(m in text for m in _PERMANENT_MARKERS):
return {"class": PERMANENT, "evidence": "permanent marker present",
"policy": POLICY[PERMANENT]}
if any(m in text for m in _KNOWN_DEFECT_MARKERS):
return {"class": KNOWN_PROCESS_DEFECT,
"evidence": "known process-defect marker present",
"policy": POLICY[KNOWN_PROCESS_DEFECT]}
if any(m in text for m in _INTERMITTENT_MARKERS):
return {"class": INTERMITTENT,
"evidence": "intermittent marker present",
"policy": POLICY[INTERMITTENT]}
if any(m in text for m in _TRANSIENT_MARKERS):
return {"class": TRANSIENT, "evidence": "transient marker present",
"policy": POLICY[TRANSIENT]}
return {"class": UNKNOWN, "evidence": "no marker matched",
"policy": POLICY[UNKNOWN]}
+349
View File
@@ -0,0 +1,349 @@
"""programme_continuation.py — T15 §15M: the PROGRAMME CONTINUATION
CANARY.
The user observation this answers: subtask completion repeatedly ended
whole programmes. "task done" is not "programme done".
After ANY stage/subtask reports complete, the runner asks one bounded
question — is the overall construction/programme objective satisfied?
If not, and eligible authorized stages remain, the NEXT stage starts
immediately (no user re-prompt, no voluntary stop). Blocked branches
fall back to pre-authorized independent work.
Boundary (tranche brief): this is a bounded CONSTRUCTION-programme rule
over Scaffolding stage contracts — NOT the future general-purpose
Substrate scheduler. It knows nothing outside one programme's stage
list.
"""
from __future__ import annotations
import json
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, cast
# stage states
PENDING = "PENDING"
ELIGIBLE = "ELIGIBLE"
WAITING_EVENT = "WAITING_EVENT"
COMPLETE = "COMPLETE"
BLOCKED = "BLOCKED"
APPROVAL_REQUIRED = "APPROVAL_REQUIRED"
FAILED = "FAILED"
TERMINAL = {COMPLETE, BLOCKED, APPROVAL_REQUIRED, FAILED}
@dataclass
class ProgrammeStage:
stage_id: str
objective: str
state: str = PENDING
depends_on: list = field(default_factory=list) # stage_ids
priority: int = 0 # lower = sooner
result: dict = field(default_factory=dict)
def to_dict(self) -> dict:
return {"stage_id": self.stage_id, "objective": self.objective,
"state": self.state, "depends_on": list(self.depends_on),
"priority": self.priority}
@dataclass
class Programme:
"""One bounded construction programme: its objective, its authorized
stages, and the continuation rule. Persisted to the contract store's
project area (durable, reloadable, no transcript dependency)."""
programme_id: str
objective: str
stages: list = field(default_factory=list) # [ProgrammeStage]
fallbacks: list = field(default_factory=list) # pre-authorized ids
# ── the continuation canary (15M) ────────────────────────────────
def continuation_canary(self) -> dict:
"""'task done' ≠ 'programme done'. Returns the verdict AND the
next action — the runner never has to guess whether to stop."""
complete = [s for s in self.stages if s.state == COMPLETE]
if len(complete) == len(self.stages):
return {"verdict": "PROGRAMME_COMPLETE",
"next_action": None,
"reason": f"all {len(self.stages)} stages complete"}
eligible = self.eligible_stages()
if eligible:
nxt = eligible[0]
return {"verdict": "PROGRAMME_OPEN",
"next_action": ("start", nxt.stage_id),
"reason": (f"{len(complete)}/{len(self.stages)} "
f"complete; {len(eligible)} eligible — "
f"continue immediately")}
waiting = [s for s in self.stages
if s.state == WAITING_EVENT]
if waiting:
return {"verdict": "PROGRAMME_OPEN_WAITING",
"next_action": ("await_event", waiting[0].stage_id),
"reason": "event-driven wait active — no polling"}
blocked = [s for s in self.stages if s.state == BLOCKED]
if blocked and self.fallbacks:
return {"verdict": "PROGRAMME_OPEN",
"next_action": ("fallback", self.fallbacks[0]),
"reason": (f"{len(blocked)} blocked; pre-authorized "
f"fallback available")}
if blocked:
return {"verdict": "ALL_REMAINING_BLOCKED",
"next_action": None,
"reason": f"{len(blocked)} stage(s) blocked, no fallback"}
approval = [s for s in self.stages
if s.state == APPROVAL_REQUIRED]
if approval:
return {"verdict": "APPROVAL_REQUIRED",
"next_action": None,
"reason": (f"{len(approval)} stage(s) need user "
f"authorization")}
# T17 §17E: all stages terminal but some FAILED — the programme
# may still close (programme-level acceptance decides), never
# hang in UNKNOWN_INTERRUPTION.
if all(s.state in TERMINAL for s in self.stages):
failed = [s.stage_id for s in self.stages if s.state == FAILED]
return {"verdict": "PROGRAMME_COMPLETE_WITH_FAILURES",
"next_action": None,
"reason": (f"all stages terminal; failed: {failed} — "
f"programme-level acceptance decides")}
return {"verdict": "UNKNOWN_INTERRUPTION", "next_action": None,
"reason": "no stage runnable and none terminal"}
def eligible_stages(self) -> list:
"""PENDING stages whose dependencies are COMPLETE, priority-first.
ELIGIBLE counts as runnable too — an eligible stage nobody started
is exactly the case the continuation canary exists to catch."""
done = {s.stage_id for s in self.stages if s.state == COMPLETE}
out = [s for s in self.stages
if s.state in (PENDING, ELIGIBLE)
and all(d in done for d in s.depends_on)]
out.sort(key=lambda s: (s.priority, s.stage_id))
return out
# ── durable persistence (canonical store, not transcript) ────────
def save(self, base: Path) -> Path:
base.mkdir(parents=True, exist_ok=True)
p = base / f"{self.programme_id}.json"
p.write_text(json.dumps({
"programme_id": self.programme_id,
"objective": self.objective,
"stages": [s.to_dict() for s in self.stages],
"fallbacks": list(self.fallbacks),
"saved_at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
time.gmtime()),
}, indent=1))
return p
@classmethod
def load(cls, base: Path, programme_id: str) -> "Programme | None":
p = base / f"{programme_id}.json"
if not p.exists():
return None
d = json.loads(p.read_text())
prog = cls(programme_id=d["programme_id"],
objective=d["objective"], fallbacks=d.get("fallbacks", []))
for s in d["stages"]:
prog.stages.append(ProgrammeStage(**s))
return prog
def run_programme(prog: Programme, stage_runner, *,
max_transitions: int = 100) -> list[dict]:
"""Drive one programme to a legitimate stop. `stage_runner(stage)` —
provided by the caller — must return a state for the stage. Between
EVERY stage: persist → continuation canary → next. Returns the
transition log (the 15M proof artifact)."""
log: list[dict] = []
for _ in range(max_transitions):
prog_canary = prog.continuation_canary()
log.append({"canary": prog_canary})
verdict, action = prog_canary["verdict"], prog_canary["next_action"]
if action is None:
break # legitimate stop — reason recorded in the log
kind, target = action
if kind == "fallback":
log.append({"fallback_started": target})
break
stage = next(s for s in prog.stages if s.stage_id == target)
stage.state = stage_runner(stage) # runner decides the new state
log.append({"stage": target, "new_state": stage.state})
return log
# ── T17 §17D: production wiring — Scaffolding-integrated driver ─────────
#
# The 15M canary proved the RULE; this adapter makes it CANONICAL. One
# programme = a list of stages; each stage maps 1:1 to a Scaffolding
# contract (stage_id == contract_id). Stages run through the Construction
# Interface lifecycle (approved→building→verifying→closed); asynchronous
# stage work runs under the event bridge (WAITING_EVENT is a state, not
# a stop). Persisted to the scaffold store's programme area — reloadable
# after restart with no transcript.
DEFAULT_PROGRAMME_AREA = "programmes"
def _programme_base(scaffold_store: Path) -> Path:
return Path(scaffold_store) / DEFAULT_PROGRAMME_AREA
def scaffolding_stage_runner(interface, *, scaffold_store: Path,
daemon_url: str = "",
watchdog_secs: int = 900):
"""Return a stage_runner for run_programme bound to the Construction
Interface. The stage's work is read from stage.result["work"] — a
callable (interface, stage) → dict with {"ok": bool, "evidence": str,
"async_cmd": [argv] optional}. When async_cmd is present the work
runs under the event bridge and the driver resumes on the EVENT;
canonical verification still decides completion truth. Stages without
work complete as governance-only transitions."""
from rune import event_wait as ew # local import: optional dependency
def _phase(contract_id: str) -> str:
# ALWAYS re-read: transition() saves through its own doc copy, so
# a locally-held contract dict goes stale after any transition
# (T17 17E live finding — the draft→approved→building chain
# silently stopped after the first step on a stale reference).
doc = interface.scaffold.load()
c = interface.scaffold.contract(doc, contract_id)
return (c or {}).get("phase", "")
def run(stage: ProgrammeStage) -> str:
contract_id = stage.stage_id
phase = _phase(contract_id)
if phase == "":
stage.result["error"] = f"no contract {contract_id}"
return FAILED
if phase == "closed":
return COMPLETE # idempotent re-entry (duplicate signal case)
if phase == "draft":
interface.transition(contract_id, "approved",
evidence_ref="programme-stage-start")
if _phase(contract_id) == "approved":
interface.transition(contract_id, "building",
evidence_ref="programme-stage-start")
work = cast("Any", stage.result.get("work"))
if work is None:
interface.transition(contract_id, "verifying",
evidence_ref="governance-only-stage")
interface.close(
contract_id,
observed_evidence=[{"source": "programme-driver",
"detail": "governance-only stage: "
"no work, closure by "
"programme acceptance"}],
scope_files=[])
return COMPLETE
# work is either a callable (interface, stage) → outcome dict, or
# a literal outcome spec dict ({"ok", "evidence", "async_cmd"?})
outcome: dict = (dict(cast("dict", work(interface, stage)))
if callable(work) else dict(cast("dict", work)))
async_cmd = outcome.get("async_cmd")
if async_cmd:
op = f"stage-{contract_id}-{int(time.time())}"
ew.register_process_wait(
op, contract_id=contract_id,
project=getattr(interface.scaffold, "project_id", ""),
objective=stage.objective,
watched_for="stage work completion event",
watchdog_secs=watchdog_secs)
notifier = ew.run_builder_under_events(
async_cmd, op, daemon_url=daemon_url or ew.DEFAULT_DAEMON_URL)
stage.result["operation_id"] = op
stage.result["notifier_pgid"] = notifier.pid
result = ew.await_resume_event(op, watchdog_secs=watchdog_secs,
pgid=notifier.pid)
if result["kind"] != "event":
stage.result["error"] = f"stage event wait: {result['kind']}"
interface.transition(contract_id, "failed",
evidence_ref="stage-event-lost")
return FAILED
rec: dict = result.get("wait") or {}
outcome["evidence"] = (f"event resume; wait {rec.get('wait_id')} "
f"verdict={rec.get('verdict')}")
if not outcome.get("ok"):
stage.result["error"] = outcome.get("evidence", "work failed")
# truthful failure: record the evidence ON the contract and
# drive it to the failed phase via the legal transition map
# (building→failed is legal; no exception swallowing)
doc = interface.scaffold.load()
c = interface.scaffold.contract(doc, contract_id)
if c is not None:
c.setdefault("checkpoints", []).append({
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
time.gmtime()),
"phase": c.get("phase", ""),
"evidence": "programme-stage-failed",
"note": (outcome.get("evidence") or "work failed")[:200],
})
interface.scaffold.save(doc)
phase = c.get("phase", "")
if phase == "building":
interface.transition(contract_id, "failed",
evidence_ref="programme-stage-failed")
return FAILED
# completion truth: work evidence recorded ON the contract, then
# closed (evidence lives on the durable object, not the log)
if _phase(contract_id) == "building":
interface.transition(contract_id, "verifying",
evidence_ref="programme-stage-verify")
doc = interface.scaffold.load()
c = interface.scaffold.contract(doc, contract_id)
if c is not None:
c.setdefault("checkpoints", []).append({
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"phase": "verifying",
"evidence": "programme-stage-work",
"note": (outcome.get("evidence") or "work completed")[:200],
})
interface.scaffold.save(doc)
interface.close(
contract_id,
observed_evidence=[{"source": "programme-driver",
"detail": outcome.get("evidence", "ok")}],
scope_files=[])
return COMPLETE
return run
def run_construction_programme(prog: Programme, interface, *,
scaffold_store: Path,
daemon_url: str = "",
watchdog_secs: int = 900,
max_transitions: int = 100) -> list[dict]:
"""Canonical multi-stage construction continuation (T17 §17D):
run_programme with the Scaffolding-bound runner; persists the
programme around every transition so a restart resumes from the
durable record, never a transcript. Returns the transition log."""
runner = scaffolding_stage_runner(
interface, scaffold_store=scaffold_store,
daemon_url=daemon_url, watchdog_secs=watchdog_secs)
log: list[dict] = []
base = _programme_base(scaffold_store)
for _ in range(max_transitions):
prog.save(base)
canary = prog.continuation_canary()
log.append({"canary": canary})
verdict, action = canary["verdict"], canary["next_action"]
if action is None:
break
kind, target = action
if kind == "fallback":
log.append({"fallback_started": target})
break
stage = next(s for s in prog.stages if s.stage_id == target)
if stage.state == COMPLETE:
continue # duplicate continuation signal: idempotent
stage.state = ELIGIBLE
new_state = runner(stage)
stage.state = new_state
log.append({"stage": target, "new_state": new_state})
prog.save(base)
prog.save(base)
return log
+48
View File
@@ -0,0 +1,48 @@
"""provider_context.py — T16 §16F: provider ownership boundary.
ARCHITECTURE RULE (T16 mandate, permanent):
SCAFFOLDING DOES NOT OWN PROVIDER SELECTION.
Provider, model, provider fallback, provider source, and provider account
belong to the surrounding Rune/TUI/provider layer. Construction code never
names a provider. The ONLY supported way for a driver/probe (builder
session, research stage, evidence probe — all PROVIDER_LAYER callers) to
learn which model to use is the ambient provider context, i.e. the
environment the surrounding layer sets.
works → continue
missing/unresolvable → fail LOUDLY with PROVIDER_UNRESOLVED
provider call fails → preserve the REAL error and status/API code,
classify BLOCKED/PROVIDER_UNAVAILABLE, stop the dependent branch
NO silent default provider. NO automatic fallback. NO preference mutation.
(Automatic provider fallback is DEFERRED to the future multi-source /
local-model architecture.)
"""
from __future__ import annotations
import os
class ProviderUnresolved(RuntimeError):
"""Raised when no provider context is available and a silent default
would otherwise have been selected. Deliberately loud: provider
selection is a user-layer decision, never a code default."""
def require_model(env_var: str = "MIMO_MODEL") -> str:
"""Resolve the current model from the ambient provider context.
The surrounding provider layer (TUI / operator / session environment)
owns this variable. A driver or probe MUST NOT carry its own default:
a missing value is a configuration gap the user layer must fix, not
something construction code may paper over by picking a provider."""
model = (os.environ.get(env_var) or "").strip()
if not model:
raise ProviderUnresolved(
f"PROVIDER_UNRESOLVED: {env_var} is not set — provider/model "
f"selection belongs to the provider layer (set {env_var} in "
f"the calling environment); no silent default is permitted "
f"(T16 §16F)")
return model
+17 -1
View File
@@ -787,6 +787,20 @@ class RuneOperator:
NO duplicate intelligence here."""
from .construction_mode import classify_mode, _STANDALONE_WORDS
from .construction_activation import activate
# §15J case F: manual override feeds the SAME decision path — a
# leading "/scaffold-mode <MODE>" (or "scaffold-mode: <MODE>")
# marker is stripped and passed as explicit_override with
# override_authority="user". NOT a second implementation:
# classification still runs through classify_mode with provenance.
override = None
m = re.match(
r"^(?:/scaffold-mode|scaffold-mode:)\s+"
r"(NOT_CONSTRUCTION|LOCAL_CHANGE|EXISTING_PROJECT_CHANGE|"
r"PROJECT_BUILD|UNKNOWN)\s+(.*)$",
description, re.DOTALL)
if m:
override = m.group(1)
description = m.group(2).strip()
low = description.lower()
tokens = set(__import__("re").split(r"[^a-z0-9]+", low))
# §12D: the NATIVE hook must not force the Rune target — standalone
@@ -802,7 +816,9 @@ class RuneOperator:
target, exists = None, None # unknown → probes, not invention
mode_decision = classify_mode(
description, target_project=target, project_exists=exists,
affected_surface=[])
affected_surface=[],
explicit_override=override,
override_authority="user" if override else None)
from .scaffold import Scaffold
s = Scaffold(self._construction_store(), "issue161-harness")
doc = s.load()
+2 -2
View File
@@ -8,7 +8,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.66.58
Version: v0.66.68
Features: 41 (24 original + 17 new native commands)
Usage:
@@ -1856,7 +1856,7 @@ class RuneApp(App):
# Show version info
self.chat_channel.add_message(
"system",
f"[dim]Rune v0.66.58 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.66.68 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
+281
View File
@@ -0,0 +1,281 @@
"""stage_guards.py — T15 §15F–§15I: guarded stage execution for Scaffolding.
Design lessons absorbed (NOT the systems they came from): pre/post-
conditions and invariants (Design by Contract), readiness gating of
downstream work (DAGs), distinct startup/liveness/exit probe meanings
(K8s probes), desired-vs-actual reconciliation (K8s controllers),
idempotent retry + failure classification (Temporal), explicit
inputs/outputs and null-build checks (Bazel), policy decision separated
from the governed actor (policy-as-code).
§15F THREE CANARY MEANINGS — deliberately NOT one generic flag:
READINESS — may this stage begin? failure BLOCKS the stage
LIVENESS — is progress still possible? inactivity ≠ completion
EXIT — did reality reach the expected state, invariants held?
a stage cannot become PASS without it
§15F INDEPENDENT COMPLETION AUTHORITY (14X.8 continued): the actor that
performed the stage action may NOT self-certify its exit canary. The
canary executes the check itself (command, readback, assertion) and
records evidence; actor claims are evidence INPUT only.
§15G TEST TRUTH: exit code 0 is not proof. Where the contract requires
tests, the exit canary requires: non-zero executed test count, no
skip-all, expected artifact present. Expectations come from the
contract, never hard-coded.
§15H HENXELS PLACEMENT: structural/system-health guard at proportionate
boundaries — REQUIRED for structural integration boundaries and
FULL/PROJECT_BUILD closure, NOT_APPLICABLE for tiny bounded changes.
Every material boundary records which, with rationale.
§15I DESIRED VS ACTUAL: after consequential actions — persist, reload
canonical reality, compare against desired. A command exiting 0 is not
an effect. Zero/acceptable delta gates the NEXT stage.
"""
from __future__ import annotations
import re
import subprocess
import time
from typing import Any
from dataclasses import dataclass, field
from pathlib import Path
# ── §15F guarded stage contract ─────────────────────────────────────────────
@dataclass
class StageCheck:
"""One named, executable readiness/exit check. The CHECK executes —
it is never satisfied by a claim."""
name: str
kind: str # "command" | "path_exists" | "assert"
# command: argv list; truth from exit code (+ optional output_pattern)
argv: list | None = None
output_pattern: str = "" # regex that must appear on stdout (15G)
# path_exists: path that must exist after the stage
path: str = ""
# assert: callable(state) -> bool (in-process deterministic check)
check: Any = None
description: str = ""
@dataclass
class StageContract:
"""Minimum guarded-stage semantics expressed over existing
construction state. Not a workflow engine: a stage contract is data
attached to (or derived from) a Construction Contract stage."""
stage_id: str
contract_id: str = ""
dependencies: list = field(default_factory=list) # stage_ids
ready_when: list = field(default_factory=list) # [StageCheck]
done_when: list = field(default_factory=list) # [StageCheck]
invariants: list = field(default_factory=list) # [StageCheck]
expected_effect: str = ""
retry_class: str = "UNKNOWN" # TRANSIENT/INTERMITTENT/KNOWN_PROCESS_
# DEFECT/PERMANENT/APPROVAL_REQUIRED/UNKNOWN
idempotency_key: str = "" # operation identity for null-action
def to_dict(self) -> dict:
def chk(c: StageCheck) -> dict:
return {"name": c.name, "kind": c.kind, "argv": c.argv,
"output_pattern": c.output_pattern, "path": c.path,
"description": c.description}
return {"stage_id": self.stage_id, "contract_id": self.contract_id,
"dependencies": list(self.dependencies),
"ready_when": [chk(c) for c in self.ready_when],
"done_when": [chk(c) for c in self.done_when],
"invariants": [chk(c) for c in self.invariants],
"expected_effect": self.expected_effect,
"retry_class": self.retry_class,
"idempotency_key": self.idempotency_key}
class StageBlocked(Exception):
def __init__(self, stage_id: str, failures: list[dict]):
self.stage_id = stage_id
self.failures = failures
super().__init__(f"stage {stage_id} blocked: "
f"{[f['name'] for f in failures]}")
class ExitCanaryFailed(Exception):
def __init__(self, stage_id: str, failures: list[dict]):
self.stage_id = stage_id
self.failures = failures
super().__init__(f"stage {stage_id} exit canary FAILED: "
f"{[f['name'] for f in failures]}")
def _run_check(check: StageCheck) -> dict:
"""Execute ONE check and record evidence. Returns
{name, passed, evidence}. A check that cannot execute is a FAILURE
(cannot-execute is never a pass)."""
ev: dict = {"name": check.name, "kind": check.kind}
try:
if check.kind == "command":
r = subprocess.run(check.argv or [], capture_output=True,
text=True, timeout=300)
ev["exit_code"] = r.returncode
passed = r.returncode == 0
if passed and check.output_pattern:
m = re.search(check.output_pattern, r.stdout or "")
ev["pattern"] = check.output_pattern
ev["matched"] = bool(m)
passed = bool(m)
if m:
ev["matched_text"] = m.group(0)[:100]
elif check.kind == "path_exists":
p = Path(check.path)
ev["path"] = check.path
passed = p.exists() and p.stat().st_size >= 0
ev["size"] = p.stat().st_size if p.exists() else 0
elif check.kind == "assert":
ev["assert"] = check.description or check.name
passed = bool(check.check({}))
else:
ev["error"] = f"unknown check kind {check.kind}"
passed = False
except Exception as e: # noqa: BLE001 — cannot-execute is a failure
ev["error"] = str(e)[:200]
passed = False
ev["passed"] = passed
return ev
def readiness_canary(stage: StageContract) -> dict:
"""§15F: may this stage begin? ALL ready_when checks must pass.
Failure BLOCKS the stage — it does NOT auto-repair."""
failures = [ev for ev in (_run_check(c) for c in stage.ready_when)
if not ev["passed"]]
verdict = "READY" if not failures else "BLOCKED"
return {"stage_id": stage.stage_id, "canary": "readiness",
"verdict": verdict,
"checks": len(stage.ready_when), "failures": failures}
def execute_stage(stage: StageContract, action) -> dict:
"""Run one guarded stage: readiness → action → exit canary.
The action is the caller's function; its return value is evidence
INPUT. The exit canary independently verifies done_when + invariants.
Readiness failure raises StageBlocked (stage does not execute);
exit failure raises ExitCanaryFailed (stage cannot become PASS)."""
ready = readiness_canary(stage)
if ready["verdict"] == "BLOCKED":
raise StageBlocked(stage.stage_id, ready["failures"])
action_evidence = action() if action else {}
exit_evidence = [_run_check(c) for c in stage.done_when]
invariant_evidence = [_run_check(c) for c in stage.invariants]
failures = [e for e in exit_evidence + invariant_evidence
if not e["passed"]]
result = {"stage_id": stage.stage_id, "canary": "exit",
"verdict": "PASS" if not failures else "FAILED",
"action_evidence": action_evidence,
"exit_checks": exit_evidence,
"invariant_checks": invariant_evidence,
"failures": failures}
if failures:
# §15F: the stage cannot become PASS — and "cannot" means the
# caller gets an exception, not a verdict it may ignore.
raise ExitCanaryFailed(stage.stage_id, failures)
return result
def liveness_canary(last_progress_at: float, now: float | None = None,
stall_limit_secs: float = 300.0) -> dict:
"""§15F: is progress still possible? Inactivity NEVER means
completion — a stalled stage is STALLED, not done."""
now = now if now is not None else time.monotonic()
stalled = (now - last_progress_at) > stall_limit_secs
return {"canary": "liveness", "stalled": stalled,
"idle_secs": round(now - last_progress_at, 1),
"verdict": "STALLED" if stalled else "ALIVE"}
# ── §15G verify test truth, not just exit code ──────────────────────────────
@dataclass
class TestExpectations:
"""Contract-derived verification expectations (never universal
constants)."""
require_tests: bool = True
min_tests: int = 1
test_output_pattern: str = r"(?:# pass (\d+)|(\d+) passed)"
forbid_all_skipped: str = r"# skipped (\d+)"
required_artifacts: list = field(default_factory=list) # paths
def verify_test_truth(cmd: list, cwd: str, expectations: TestExpectations,
*, base_dir: str = ".") -> dict:
"""§15G: run the verification command and judge TRUTH, not exit code.
False-green hostile cases rejected: zero tests discovered with exit 0,
all tests skipped, expected artifact absent."""
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True,
timeout=300)
evidence: dict = {"cmd": cmd, "exit_code": r.returncode,
"stdout_tail": (r.stdout or "")[-500:]}
failures: list[dict] = []
if r.returncode != 0:
failures.append({"name": "exit_code", "exit_code": r.returncode})
if expectations.require_tests:
m = re.search(expectations.test_output_pattern, r.stdout or "")
count = int(m.group(1) or m.group(2)) if m else 0
evidence["test_count"] = count
if count == 0:
# zero tests + exit 0 is a false green when tests are required
failures.append({"name": "test_count", "count": 0,
"why": "zero tests executed — false green"})
elif count < expectations.min_tests:
failures.append({"name": "test_count", "count": count,
"min_required": expectations.min_tests})
if expectations.forbid_all_skipped:
s = re.search(expectations.forbid_all_skipped, r.stdout or "")
if s and count and int(s.group(1)) >= count:
failures.append({"name": "all_skipped",
"skipped": int(s.group(1))})
for art in expectations.required_artifacts:
p = Path(base_dir) / art
if not p.exists() or p.stat().st_size == 0:
failures.append({"name": "artifact_missing", "path": art})
evidence["failures"] = failures
evidence["verdict"] = "PASS" if not failures else "FALSE_GREEN_REFUSED"
return evidence
# ── §15H Henxels as proportional structural guard ───────────────────────────
def henxels_placement(*, depth: str, structural_boundary: bool,
stage_id: str) -> dict:
"""§15H: is Henxels REQUIRED at this boundary? Proportional, not
ceremonial. Records the decision with rationale (either way)."""
if structural_boundary or depth in ("FULL", "PROJECT_BUILD"):
return {"stage_id": stage_id, "henxels": "HENXELS_REQUIRED",
"rationale": (f"depth={depth}, structural_boundary="
f"{structural_boundary} — structural "
f"integration evidence required")}
return {"stage_id": stage_id, "henxels": "HENXELS_NOT_APPLICABLE",
"rationale": (f"depth={depth}, no structural boundary — "
f"focused exit canary suffices")}
# ── §15I desired vs actual reconciliation ───────────────────────────────────
def reconcile(desired: dict, read_actual, *, stage_id: str = "",
tolerance: set | None = None) -> dict:
"""§15I: persist → reload canonical reality → compare. `read_actual`
is a callable returning the ACTUAL canonical state (fresh read, never
a cached copy). Returns the delta; empty delta = continue. The next
stage MUST NOT start on a non-empty delta."""
tolerance = tolerance or set()
actual = read_actual()
delta: dict = {}
for key, want in desired.items():
if key in tolerance:
continue
got = actual.get(key) if isinstance(actual, dict) else None
if got != want:
delta[key] = {"desired": want, "actual": got}
return {"stage_id": stage_id, "reconciled": not delta, "delta": delta}
+32
View File
@@ -0,0 +1,32 @@
# Task Ledger
A small standalone task ledger backed by SQLite (stdlib `sqlite3` only).
Supports adding tasks, listing them, and marking them complete.
## Files
- `ledger.py` — the ledger (`connect`, `add_task`, `list_tasks`,
`complete_task`; SQLite storage, `:memory:` by default or a file path).
- `ledger.test.py` — stdlib `unittest` suite proving add, list and
complete work, including SQLite file persistence.
## Usage
```python
import ledger
conn = ledger.connect("tasks.db") # or ledger.connect() for in-memory
task = ledger.add_task(conn, "write report")
print(ledger.list_tasks(conn))
ledger.complete_task(conn, task["id"])
```
## Run tests
The test file uses a Node-style name (`ledger.test.py`), so the default
`unittest discover` pattern (`test*.py`) does not match it. Run it
directly:
```sh
python3 ledger.test.py -v
```
+94
View File
@@ -0,0 +1,94 @@
"""Standalone SQLite task ledger: add, list and complete tasks.
Stdlib only (sqlite3). State lives in SQLite; the caller owns the
connection (``:memory:`` by default, or a file path for persistence).
"""
from __future__ import annotations
import sqlite3
SCHEMA = (
"CREATE TABLE IF NOT EXISTS tasks ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"title TEXT NOT NULL, "
"completed INTEGER NOT NULL DEFAULT 0"
")"
)
def connect(path: str = ":memory:") -> sqlite3.Connection:
"""Open a ledger database and ensure the schema exists."""
conn = sqlite3.connect(path)
conn.row_factory = sqlite3.Row
init_db(conn)
return conn
def init_db(conn: sqlite3.Connection) -> None:
"""Create the tasks table if it does not exist."""
conn.execute(SCHEMA)
conn.commit()
def _row_to_dict(row: sqlite3.Row) -> dict:
return {
"id": row["id"],
"title": row["title"],
"completed": bool(row["completed"]),
}
def add_task(conn: sqlite3.Connection, title: str) -> dict:
"""Add a task; return ``{"id", "title", "completed"}``.
Raises:
TypeError: if title is not a string.
ValueError: if title is empty or blank.
"""
if not isinstance(title, str):
raise TypeError("title must be a string")
cleaned = title.strip()
if not cleaned:
raise ValueError("title must be a non-empty string")
init_db(conn)
cur = conn.execute(
"INSERT INTO tasks (title, completed) VALUES (?, 0)", (cleaned,)
)
conn.commit()
task_id = cur.lastrowid
row = conn.execute(
"SELECT id, title, completed FROM tasks WHERE id = ?", (task_id,)
).fetchone()
return _row_to_dict(row)
def list_tasks(conn: sqlite3.Connection) -> list:
"""Return all tasks ordered by id (each a dict)."""
init_db(conn)
rows = conn.execute(
"SELECT id, title, completed FROM tasks ORDER BY id"
).fetchall()
return [_row_to_dict(r) for r in rows]
def complete_task(conn: sqlite3.Connection, task_id: int) -> dict:
"""Mark the task with ``task_id`` completed; return the updated dict.
Raises:
TypeError: if task_id is not an int.
LookupError: if no task has that id.
"""
if isinstance(task_id, bool) or not isinstance(task_id, int):
raise TypeError("task_id must be an int")
init_db(conn)
cur = conn.execute(
"UPDATE tasks SET completed = 1 WHERE id = ?", (task_id,)
)
conn.commit()
if cur.rowcount == 0:
raise LookupError(f"no task with id {task_id}")
row = conn.execute(
"SELECT id, title, completed FROM tasks WHERE id = ?", (task_id,)
).fetchone()
return _row_to_dict(row)
+136
View File
@@ -0,0 +1,136 @@
"""Tests for the standalone SQLite task ledger (``ledger.py``).
Run directly (the file name is Node-style, so the default
``python -m unittest discover`` pattern ``test*.py`` does not pick it up)::
python3 ledger.test.py -v
Stdlib only.
"""
import os
import sqlite3
import tempfile
import unittest
import ledger
def fresh_conn():
return ledger.connect(":memory:")
class TestAddTask(unittest.TestCase):
def test_add_returns_task_with_id_title_and_not_completed(self):
conn = fresh_conn()
task = ledger.add_task(conn, "write report")
self.assertEqual(task["title"], "write report")
self.assertFalse(task["completed"])
self.assertIsInstance(task["id"], int)
def test_add_assigns_unique_ids(self):
conn = fresh_conn()
first = ledger.add_task(conn, "one")
second = ledger.add_task(conn, "two")
self.assertNotEqual(first["id"], second["id"])
def test_add_trims_title(self):
conn = fresh_conn()
task = ledger.add_task(conn, " tidy desk ")
self.assertEqual(task["title"], "tidy desk")
def test_add_rejects_empty_or_blank_title(self):
conn = fresh_conn()
with self.assertRaises(ValueError):
ledger.add_task(conn, "")
with self.assertRaises(ValueError):
ledger.add_task(conn, " ")
def test_add_rejects_non_string_title(self):
conn = fresh_conn()
with self.assertRaises(TypeError):
ledger.add_task(conn, None)
with self.assertRaises(TypeError):
ledger.add_task(conn, 123)
class TestListTasks(unittest.TestCase):
def test_list_empty_ledger(self):
self.assertEqual(ledger.list_tasks(fresh_conn()), [])
def test_list_returns_all_tasks_in_order(self):
conn = fresh_conn()
ledger.add_task(conn, "alpha")
ledger.add_task(conn, "beta")
tasks = ledger.list_tasks(conn)
self.assertEqual([t["title"] for t in tasks], ["alpha", "beta"])
def test_list_shows_completed_flags(self):
conn = fresh_conn()
task = ledger.add_task(conn, "alpha")
ledger.add_task(conn, "beta")
ledger.complete_task(conn, task["id"])
by_title = {t["title"]: t for t in ledger.list_tasks(conn)}
self.assertTrue(by_title["alpha"]["completed"])
self.assertFalse(by_title["beta"]["completed"])
class TestCompleteTask(unittest.TestCase):
def test_complete_marks_matching_task(self):
conn = fresh_conn()
first = ledger.add_task(conn, "alpha")
second = ledger.add_task(conn, "beta")
updated = ledger.complete_task(conn, first["id"])
self.assertTrue(updated["completed"])
self.assertEqual(updated["id"], first["id"])
by_title = {t["title"]: t for t in ledger.list_tasks(conn)}
self.assertTrue(by_title["alpha"]["completed"])
self.assertFalse(by_title["beta"]["completed"])
def test_complete_persists_across_listing(self):
conn = fresh_conn()
task = ledger.add_task(conn, "alpha")
ledger.complete_task(conn, task["id"])
done = [t for t in ledger.list_tasks(conn) if t["completed"]]
self.assertEqual([t["title"] for t in done], ["alpha"])
def test_complete_unknown_id_raises(self):
conn = fresh_conn()
ledger.add_task(conn, "alpha")
with self.assertRaises(LookupError):
ledger.complete_task(conn, 999)
def test_complete_rejects_non_int_id(self):
conn = fresh_conn()
ledger.add_task(conn, "alpha")
with self.assertRaises(TypeError):
ledger.complete_task(conn, "1")
class TestPersistence(unittest.TestCase):
def test_tasks_survive_reconnect_to_file_db(self):
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "tasks.db")
conn = ledger.connect(path)
task = ledger.add_task(conn, "persist me")
ledger.complete_task(conn, task["id"])
conn.close()
reopened = ledger.connect(path)
try:
tasks = ledger.list_tasks(reopened)
finally:
reopened.close()
self.assertEqual(len(tasks), 1)
self.assertEqual(tasks[0]["title"], "persist me")
self.assertTrue(tasks[0]["completed"])
def test_uses_sqlite_storage(self):
conn = fresh_conn()
self.assertIsInstance(conn, sqlite3.Connection)
ledger.add_task(conn, "alpha")
rows = conn.execute("SELECT title FROM tasks").fetchall()
self.assertEqual([r[0] for r in rows], ["alpha"])
if __name__ == "__main__":
unittest.main(verbosity=2)
+2
View File
@@ -0,0 +1,2 @@
# tests 10
# pass 10
+48
View File
@@ -0,0 +1,48 @@
# T16 Stage 16A — exact baseline (verified 2026-09-15 ~15:40 local)
Fresh session ses_-ffe5f5a82d0b5ffeLrGt5rMRv. All facts verified independently this session; none carried from prior-session memory without re-verification.
## Identity / release chain
- gitea/master: 5bd8cca9410b = auto-bump 0.66.62 (2026-09-15 14:15:22 +0100)
- Releases: v0.66.62 published 14:15:51; v0.66.60 (13:29), v0.66.61 (13:42) same day
- Deployed: /mnt/workdrive/rune-code/current -> 5bd8cca9410b (symlink mtime 14:20)
- last-good -> fd5f31b (0.66.61-lineage fix commit)
- CONVERGED: release tag, deployed current, and master tip are the same SHA. No manual refresh used.
## Inherited T15 open item — RESOLVED on master
- 15Z CI failure (196cf19: pyright 445 vs 443; unit-tests job truncated) — fixed by:
- 193d819 "event_wait importlib-clean loading, zero pyright warnings" (= be87dc3 rebased/pushed)
- b2d6802 "§15K test no longer shells out to git worktree on CI"
- fd5f31b "§15D tests use ephemeral ports, not fixed 8793"
- These drove releases 0.66.60 → 0.66.61 → 0.66.62, each deployed. Nothing inherited is left open.
## Working copies
- /mnt/workdrive/issue161-harness: master tip, clean — THE code worktree for T16.
- /mnt/workdrive/rune (RUNE_HOME): branch pinned-live @ 90c7db1 (2026-09-08), 652 commits behind master, HEAD is a clean ancestor (fast-forwardable). Expected: RUNE_HOME is the data-continuity home; runtime launches from rune-code/current. Still: stale and drifting (16P material).
- 39 stale build-20260915-* worktrees, all cut at 90c7db1 (stale base) — /home/nick/.rune/worktrees/. Cleanup candidate (16W).
## Gates (all run in lock-exact environments, master tip 5bd8cca)
- Full suite: 1854 passed + 84 subtests, 0 failures (113s)
- Lint ratchet: PASS (BLE001 354/354, S110 80/80 at baseline). Raw `ruff check` = 12 errors, all within committed lint-baseline (mandate's "8 pre-existing" is now 12 — drift, 16W item).
- Type-check ratchet: PASS — errors 117 (= baseline), warnings 437 (≤ 443). CRITICAL METHOD NOTE: must run in `.type-venv` recreated from requirements-ci.txt (lock-exact). Stale /tmp/ci-venv (pyright 1.1.414 but stale env) false-failed 124 vs 117. Environment-exactness is a repeat failure class (16U).
- Henxels: rc=0.
## Wait/resume infrastructure
- W/R daemon RUNNING: pid 3756873, `python3 daemon.py 8791` (webhook receiver on 0.0.0.0:8791), restarted 14:08 today.
- Daemon lives at /home/nick/.local/share/mimo-wait-resume (daemon.py, waitstore.py, verify.py, own tests) with its OWN git snapshot @ 4811070 "T15 §15D/§15E: per-wait resume_cmd override + PROCESS_EVENT wait kind + /process-event endpoint". The daemon is NOT part of the master tree — src/rune/ has event_wait.py (client-side adapter) but no daemon.py. (16M inventory finding: out-of-repo deployment, separate snapshot.)
- Wait store: 45 records — states: COMPLETED 19, RESUME_UNVERIFIED 10, FAILED 6, READY_TO_RESUME 6, WAITING_EXTERNAL 4. Kinds: EXACT_WORKFLOW 24, RELEASE_DEPLOY 8, PROCESS_EVENT 7 (+6 pre-kind records with wait_kind absent).
- 4 WAITING_EXTERNAL + 6 READY_TO_RESUME + 10 RESUME_UNVERIFIED = outstanding waits to reconcile or leave truthfully parked.
## Scaffolding stores
- Repo store/scaffold/current.json: spine-shaped (entities/relationships/observations), no projects key.
- store/scaffold/projects/: EMPTY. T14/T15 project scaffolds lived in ephemeral /tmp stores (e.g. /tmp/t15r-store). Durable evidence: store/evidence/t14/, store/evidence/t15/ (+ t16/ created this session).
## T15 maturity — CORRECTED RECORD (16A mandate)
- Programme continuation MECHANISM (stage_guards, event waits, canaries): INTEGRATED / TEST-PROVEN. T15's own test evidence stands — not erased.
- Uninterrupted REAL-SESSION programme continuation: NOT YET LIVE-PROVEN. The prior session's actual user experience was repeated manual "continue" despite the mechanism being tested green. This fresh T16 session is the live experiment (Stage 16B).
- Builder event-driven wait: CANONICAL DEFAULT (bounded live proof, T15).
- CI/deployment wait: STILL POLLING FALLBACK (sleep-N → query patterns remain in release/CI verification paths) — the 16C/D target. Verified by inspection during 16A setup; full inventory in 16C.
## Provider/model handling (initial scan, full audit = 16F)
- Known hardcode surface to classify: Verboo, glm-5.3-flash, mimo-v2.5-pro, Xioami, BUILDER_MODEL, DEFAULT_MODEL, MIMO_MODEL, --model/-m.
- T14/T15 real-builder runs pinned "Verboo Code/glm-5.3-flash" at the DRIVER layer (t14_builder_session.py), not inside Scaffolding proper — classification pending 16F audit.
+44
View File
@@ -0,0 +1,44 @@
# T16 Stage 16B — live continuation diagnostic (spans the whole tranche)
fresh_session = true
session = ses_-ffe5f5a82d0b5ffeLrGt5rMRv
tranche_start = 2026-09-15 (T16 mandate received at session open)
## Response-boundary ledger
| # | boundary | last completed | next eligible stage started automatically? | user intervention required? |
|---|----------|----------------|---------------------------------------------|------------------------------|
| 1 | session open (checkpoint rebuild from prior-session T16 mandate) | none | 16A started this session without user prompt | NO |
## Stage log
- 16A: STARTED 2026-09-15 (this session, unprompted continuation from mandate). COMPLETED ~15:45 local. Baseline + T15 maturity correction persisted (store/evidence/t16/16A_baseline.md + project MEMORY.md). Next stage (16C) selected and started immediately in the same assistant turn — no user intervention.
## Verdict so far
Running experiment. Required live proof: ≥4 substantial sequential stages completing and the next starting automatically without user intervention. A LIVE_CONTINUATION_FAILURE is recorded the moment this assistant reaches a normal response boundary while eligible authorized work remains.
## Stage log update (16:00Z)
- 16A: COMPLETED (persisted + readback done). No user intervention.
- 16B diagnostic: created and maintained continuously. No user intervention.
- 16C: COMPLETED (event-driven CI wait adapter implemented, 8 held-outs, workflows wired, daemon passthrough landed). NO user intervention — began immediately after 16A in the same assistant turn.
- 16D: IN PROGRESS — first live cycle exposed a real defect (runner user vs bridge data root; deploy failed fast+truthfully on 7953/7958 — no false green). Root-caused, shared root + readiness probe shipped (c707a81, pushed 16:02Z); latency proof pending the next cycle. This is itself a 16U datum: "wrong environment" was caught by production before a canary could — the readiness probe now added prevents recurrence.
- 16E: COMPLETED (one primitive: builder + gitea adapters share register/FIFO/match/idempotency/watchdog; identity selects events — 16E held-out).
- 16F/16G: COMPLETED (provider_context seam, PROVIDER_UNAVAILABLE class, 7 silent provider defaults removed, structural guard + 6 held-outs; committed 37deb60).
Response boundaries so far: ZERO user interventions. Continuation has been automatic at every stage boundary.
## Stage log update (16:35Z)
- 16D: COMPLETED — two real production cycles proved the watchdog/
deadline truthfulness path (deploy promoted from the deadline path,
zero false greens, sleep-30 floor gone); event-before-waiter root-
caused and fixed via durable replay (b51afea pushed 16:34Z).
- 16E: COMPLETED (one primitive, adapters by identity).
- 16F/16G: COMPLETED. 16I/J/K: COMPLETED. 16M: COMPLETED. 16N: COMPLETED
(docs/STRUCTURAL_MAP.md). 16O: COMPLETED (honest result — no step-count
win on tiny fixtures; qualitative advantages recorded). 16P: COMPLETED
(drift_resume.py + real-fixture held-outs).
- Next: 16Q (reconciliation), 16R (self-hosted change), 16U-V, 16W-Y, 16Z.
- USER INTERVENTIONS TO DATE: ZERO. Every stage transition has been
automatic within continuous assistant execution.
+44
View File
@@ -0,0 +1,44 @@
# T16 §16D — live CI/deploy latency proofs (2026-09-15)
## Cycle 1: c707a81 (fix commit) — watchdog/deadline-recovery path
- registered_at: 16:03:16Z (deploy job start)
- event_received_at: (none — ci.yml notify ran BEFORE deploy registered;
event-before-waiter ordering, the workflow-level sibling of 16Y #10)
- controller behavior: watchdog expiry → ci_alive() True (runs genuinely
non-terminal) → truthful re-arms; no false green at any point
- verdict_at: 16:18:16Z (mode deadline_pending → final authority read
success), wall_secs 900.92 — the deadline is a FAILURE DETECTOR; it
expired exactly once and the final one-shot authority read decided
- PROMOTED: c707a81 at 16:18:17Z (current moved; last-good → previous)
- Sleep-30 polling floor: GONE (old loop would have polled 30 times in
this window; new adapter slept in select(), zero poll queries — the
only Gitea reads were one-shot authority verdicts on expiry)
## Cycle 2: 4a06d9f (auto-bump 0.66.64) — true event path
- waiter registered while CI still in progress; process observed
sleeping in select() (S state, 700s elapsed, zero CPU) at inspection
- expected: notify-event-bridge POST on CI completion → mid-wait wake →
resume latency « 30s floor (proof line appended when the cycle closes)
## Event-before-waiter finding (new, honest)
The wake event cannot be delivered when CI finishes before the deploy
job registers its correlation. This ordering is inherent (deploy.yml
starts after checkout; ci.yml may finish earlier). The durable-bridge
recovery covers it: the waiter's watchdog + one-shot authority read.
A future improvement could register the correlation BEFORE CI completes
(e.g. a preceding workflow step) — deferred; watchdog recovery is
truthful and the deadline bound held exactly.
## Cycle 2 postmortem + replay fix (17:35Z)
Cycle 2 (4a06d9f) also ended deadline_pending at 901.0s: the completion
events DID reach the daemon (16:07:44/16:08:21, after registration at
16:07:06) but never resumed the waiter. ROOT CAUSE: on a serialized
single-runner machine the deploy job starts only after ci.yml's job
slot frees — the wake lands before/while the waiter is between
registration arms; the FIFO rendezvous is forward-only.
FIX: durable event replay — find_matching_event() scans the bridge's
event log at registration; a matching event short-circuits straight to
the authority read (event_replayed mode). 2 held-outs added (9 total).
NOTE: the replay fix lands on the NEXT cycle (it cannot prove itself on
the cycle that exposed it); cycles 1-2 remain valid proof of the
watchdog/deadline truthfulness path.
+54
View File
@@ -0,0 +1,54 @@
# T16 Stage 16M — Retroactive Rune inventory (2026-09-15)
Sources: two parallel read-only exploration sweeps + 16A/16C hands-on verification. Classification scale per mandate. This is the raw material for the 16N structural map.
## The ten canonical systems
| System | Modules | Canonical state | Classification | Notes |
|---|---|---|---|---|
| Spine | spine.py (695L) over structural_store.py (650L, zero Rune constants) | store/spine/current.json, snapshots byte-deterministic | HEALTHY | maturity ladder evidence-gated; CLI JSON-canonical |
| Research pipeline | scripts/research_task_tree.py (authority), research_stage_executor.py, research_event_relay.py | research/issue-N/ artifacts + Gitea labels (labels=projection) | HEALTHY | stage gates per artifact; coverage thinner than Spine |
| ADAPT/router | adaptive_router.py (625L, "canonical route authority ADAPT-2"), route_risk_flags.py | routes/route.jsonl append-only | HEALTHY | capability_broker/recall_router/warm_router/model_routing are SEPARATE routing domains — same word, different authority; DO NOT MERGE |
| Completion Truth | verification_layer.py, orchestration/conversation_state.py (TaskStateRepository over DurableEventStore), records.py | event-store log + atomic checkpoints | UNCLEAR_AUTHORITY / MISSING_SEAM | no enforced seam that a task cannot close COMPLETED while verifiers fail — highest-value gap found |
| Release/deploy | deploy_stage/deploy_promotion/deploy_qualify/create_release/auto_bump + workflows | deploy root + current/last-good + _build_identity.json | HEALTHY | monotonic promotion fail-closed; 16C event-driven wait now added |
| Provider layer | model_routing ("one authoritative router #29 P1-06"), model_connect, model_registry, provider_registry, work_manager, standardized_providers, openai_compat_transport | model_routing_profiles.json keyed (provider,model) | DUPLICATE_ROUTE / UNCLEAR_AUTHORITY | ≥5 selection entry points; the OPERATOR is the final decider, not any registry. 16F removed construction-side selection; provider-layer consolidation = 16Q candidate |
| W/R event bridge | event_wait.py + out-of-repo daemon.py/waitstore.py/verify.py | WR_ROOT waits/events/leases/resumes | HEALTHY / OPTIONAL_BUT_SHOULD_BE_DEFAULT | one primitive, builder+gitea adapters (16E proven); daemon lives OUT-OF-REPO with its own git snapshot — packaging/deployment debt |
| TUI/backend | rune_tui.py (3885L) ← rune_operator.py (2604L) + microkernel + orchestration | conversation/task repositories | HEALTHY | one-way direction enforced by canary_tui_drift; import via sys.path shim (fragile, keep test) |
| build_mode (legacy /build) | build_mode.py BuildManager + plugins/build_mode/phases | ~/.rune/work/builds + worktrees | LEGACY / DUPLICATE_ROUTE | superseded by builder_bridge + event-driven sessions (T14/T15); still wired → two ways to run a build; REPO_DIR="/mnt/workdrive/rune" stale constant |
| Canaries | 24 canary_*.py + canary_native.MODULES registry | none (stateless) | HEALTHY | native/full_system/orchestration_foundation partially overlap (layered, acceptable); no test enforces MODULES completeness |
## Auxiliary systems
| System | Classification | Notes |
|---|---|---|
| Scaffolding itself | HEALTHY | provider-blind since 16F (structural guard); phase machine governed |
| Maintenance machinery | UNCLEAR_AUTHORITY | three overlapping routes: scaffold maintenance_item entities, tui_backlog ledger, drift_ledger — no central module, NO tests |
| Task/intent state | DUPLICATE_ROUTE | task-tool session DB vs task_persist persisted.json vs memory_store.working vs orchestration event store — four task-ish stores |
| Henxels | HEALTHY | henxels.yaml + 191L checks; contract-tested |
| Plugin surfaces | MISSING_SEAM | phases exist as files; no generic loader; single hardcoded plugin |
| Learning/process records | DUPLICATE_ROUTE | memory_store vs graph_memory vs warm_neuron_store vs curator vs process_experience — no single authority doc |
| programme_continuation | MISSING_SEAM | run_programme() has NO production caller (proof-only, T15 §15M); night_mode LEGACY aspirational; cron_engine MISSING_VERIFICATION |
| maintenance.yml | LEGACY | canary-suite job loops shared/canary_*.py — real canaries live in src/rune/; the job likely no-ops |
## Duplicate routes / bypasses (16Q raw material, priority-ordered)
1. **task-state quadruplication** — 4 stores hold task-ish state; truth hierarchy unclear
2. **build_mode vs builder_bridge** — two build paths, legacy still wired
3. **provider selection ×5 entry points** — operator is de-facto authority
4. **T14 vs T15 builder drivers** — T15 is the event-driven successor; T14 parallel-runs
5. **maintenance ledgers ×3** — scaffold entities, backlog, drift ledger
6. **maintenance.yml canary glob wrong dir** — stale no-op job
7. **programme_continuation unwired** — the "continue problem" machinery exists but nothing calls it in production
8. **memory stores ×5** — overlapping recall surfaces
## Hard-coded dev paths in production code (16W targets)
- build_mode.REPO_DIR + 5 complexity_scorer defaults → /mnt/workdrive/rune (stale for harness)
- deploy_promotion.DEPLOY_ROOT → /mnt/workdrive/rune-code (fine: that IS the deploy root, but should be env-derived — it is)
- two canaries sys.path.insert(/home/nick/.config/opencode/scripts)
- memory/system_registry.json points at /mnt/workdrive/rune — STALE repo root
- event_wait FALLBACK_WR_ROOT → /home/nick/... (fallback only; shared root now env-set in deploy.yml)
## Honest overall
Rune's structural health is better than the mandate's tone suggests: Spine, release/deploy, ADAPT, W/R, TUI boundary, canaries, Henxels, and Scaffolding are all HEALTHY with real verification. The debt concentrates in: completion-truth enforcement seam, task-state multiplication, provider-layer entry-point spread, legacy build_mode still wired, and unwired programme continuation (the very continuation defect this tranche experiments on).
+45
View File
@@ -0,0 +1,45 @@
# T16 §16O — long-gap re-entry benchmark (2026-09-15)
Fixture: /mnt/workdrive/t16-reentry — Python invoice-ledger project, 2
stages (1 closed+verified, 2 blocked PROVIDER_UNAVAILABLE with recorded
next action), then POST-CHECKPOINT DRIFT injected: (a) VAT regression in
report.py breaking 2 tests, (b) unrecorded discount.py module, (c) 2
unrecorded commits, (d) committed __pycache__ + no .gitignore (unplanned
extras). Two fresh-identity subagents, no transcript, no briefing beyond
method.
## Measured
| Metric | PATH A cold rediscovery | PATH B scaffold-assisted |
|---|---|---|
| Files opened | 8 | 9 |
| Commands run | 4 | 6 |
| Explore/verify cycles | ~5 | ~8 |
| Drifts found | 4/4 planted + pycache + gitignore | 4/4 planted (+pytest_cache receipt); did not enumerate pycache/gitignore |
| False assumptions corrected | 2 (pytest path; comments-might-be-stale) | 3 (pytest path; assumed green still held; assumed repo at checkpoint commit) |
| Drift CLASSIFICATION | none (flat findings) | EXPECTED/MATERIAL applied correctly, per-claim CONFIRMED/DRIFTED |
| Next action quality | correct, self-derived | correct + explicitly adjusted from recorded next action |
| Confidence calibration | honest (blocked=medium) | honest (drift completeness medium-high) |
## Honest verdict
1. On a TINY fixture, cold rediscovery is CHEAP and nearly as accurate —
the scaffold did not save significant effort at this scale. No
fabricated win: file/command counts are comparable (8 vs 9, 4 vs 6).
2. The scaffold's measurable advantages are QUALITATIVE and scale with
project size: (a) structured per-claim reconciliation instead of flat
discovery, (b) recorded next action + blocker evidence that survives
model turnover, (c) drift CLASSES enabling the 16P material/expected
gating decision, (d) intent (non_goals: "no hardcoded FX rates")
that cold inspection cannot infer.
3. Both paths needed a test run to catch the regression — checkpoint
claims NEVER substitute for reality (the 16P principle, proven by
both paths independently).
4. Path A's unplanned extras (pycache/gitignore) show cold discovery
also finds things outside the scaffold's frame — the scaffold is a
starting point, not a boundary.
Recommendation: adopt scaffold-assisted re-entry as the DEFAULT for
multi-session projects (16P flow), keep cold verification of claims as
a mandatory reconciliation step. Re-benchmark on a realistically-sized
repo before claiming step-efficiency wins (OVERFLOW 3 candidate).
+91
View File
@@ -0,0 +1,91 @@
# T16 §16S/§16T/§16U/§16V/§16X/§16Y — consolidated findings (2026-09-15)
## §16S — live programme continuation proof
Evidence: 16B_continuity_log.md. Stages 16A→16B→16C→16D→16E→16F/16G→
16I/J/K→16M→16N→16O→16P→16Q/16R executed in ONE continuous assistant
session with ZERO user "continue" interventions; every stage transition
was automatic. ≥4 substantial sequential stages completed with the next
starting automatically (requirement exceeded: 12+ stages).
**LIVE-PROVEN (bounded, this session).** Caveat recorded honestly: the
session is long; a context-boundary failure would still be possible in
even longer programmes — the mechanism (stage_guards + programme_
continuation canary) is what carried continuation, now live-proven.
## §16T — session-freshness finding
Previous long session (T15): user typed "continue" repeatedly DESPITE
the mechanism being test-proven. Fresh T16 session: zero interventions
across 12+ stages. Verdict: **MIXED** — old-session degradation is
SUPPORTED as the parsimonious explanation (same mechanism, same model
class, different session freshness → different behavior), but n=1 and
the prior session also carried much larger context; a systemic harness
contribution cannot be excluded. Not called corruption; no evidence of
it.
## §16U — interface-first error prevention (moved-left repairs)
- pyright ambient-venv skew (117⇄124): root-caused; ratchet now
neutralizes VIRTUAL_ENV/PATH before invoking pyright — environment
mismatch is now detected-at-setup instead of post-hoc.
- deploy readiness probe: RUNE_WR_ROOT writability checked BEFORE the
wait (the PermissionError class caught twice in production is now a
clear setup-time error).
- provider PROVIDER_UNRESOLVED: missing provider context fails loudly
before any construction run instead of silently using a default.
- lesson-class recorded: "wrong environment" failures were caught by
production twice before a canary existed — readiness-before-action is
the general fix shape.
## §16V — canary + Henxels coverage vs Rune boundaries
- Henxels rc=0 all session; umbrella manifest gate caught every new
module (provider_context, construction_projection, construction_
commands, drift_resume) — the completeness statement works.
- Found + fixed: maintenance.yml canary glob pointed at shared/ (silent
no-op) — 16Q/16R.
- Gaps recorded (16M): no MODULES-completeness test for canary registry;
maintenance machinery lacks tests; daemon out-of-repo has no CI cover.
- Not duplicated: Henxels = structural statements; canaries = import/
exercise; tests = behavior. Boundary held.
## §16X — backlogs (backend → future projection/control needs)
Construction projection (16I) already exposes: project, contract, phase,
stage, waiting_state, correlation, builder, blockers, verification,
next_action. TUI/Substrate control needs mapped to 16J commands:
START/RESUME/PROVIDE_APPROVAL/PROVIDE_CONTEXT + REQUEST_STATE.
Drift display: drift_resume classes are the vocabulary.
DEBT (recorded, not built): PAUSE needs a lifecycle phase if wanted;
goal-level (why-is-this-active) linkage belongs to future Substrate.
## §16Y — adversarial matrix (30 cases) — where each is held
1 old-context absent → 16O both paths; 2 scaffold reconstructs → 16O Path B
+ 16P; 3 provider unavailable → 16G-C; 4 no fallback → 16G-D (policy 0);
5 real status preserved → 16G-C verbatim; 6 duplicate event → 15E tests;
7 stale event → 15E tests; 8 missing webhook → 16D cycles 1-2 (deadline
recovery, promoted truthfully); 9 duplicate webhook → 15E double-resume
tests; 10 event-before-waiter → REPLAY FIX (b51afea) + live finding;
11 controller restart → 15E restart tests + resume_or_recover; 12 mock
invalid command → 16K refusal tests; 13 direct-state semantics → 16K
inexpressible test; 14 stale scaffold → 16P VAT regression blocks; 15
months-later drift → 16O/16P; 16 incomplete work after gap → 16O stage-2;
17 no-activity≠completion → watchdog/PROCESS_LOST semantics (15D/Q);
18 duplicate route → 16M inventory + 16Q fix; 19-21 TUI request classes →
classify_mode (T10-15 suites); 22 manual provider switch → 16G-G
byte-identical scaffold; 23 approval gate → LEGAL_TRANSITIONS + 16J
user-provenance refusal; 24 Henxels defect → 16Q canary glob (found+fixed
by inventory); 25 false-green → stage_guards verify_test_truth (15F-I)
+ deploy refusals observed live; 26 completion replay → match only on
WAITING_EXTERNAL; 27 stale publisher → T14B authority re-read; 28 CI
completion event → 16C suite; 29 stage-completion-≠-programme → 15M
canary + this session's behavior; 30 final completion → 16Z report
condition (this document; programme-complete only when the release
cycle converges).
Gaps honestly recorded: adversarial cases 22-27 rely on T14/T15-era
suites (not re-run live this tranche); cases 1-2 depend on subagent
honesty (both paths cross-checked against planted ground truth).
## §16Z — final release convergence (17:55Z)
Deployed current -> 986b450 (T16 16O/P commit) at 17:54Z, promoted by
the event-driven pipeline itself; releases 0.66.63→0.66.66 auto-published
and deployed through T16 commits; zero manual release refresh. Run 7991
(2a4f9c8) in flight at report time; durable waits remain parked and the
pipeline drains autonomously. Suite 1884 green; ratchets PASS; Henxels
rc=0 at last full gate run.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,36 @@
{
"programme_id": "t17-17e-proof-2",
"objective": "17E live proof: 4 stages, async event wait, failure, auto-continuation",
"stages": [
{
"stage_id": "17e2-stage-1-governance",
"objective": "governance-only stage",
"state": "COMPLETE",
"depends_on": [],
"priority": 0
},
{
"stage_id": "17e2-stage-2-async",
"objective": "async work under event bridge",
"state": "COMPLETE",
"depends_on": [],
"priority": 1
},
{
"stage_id": "17e2-stage-3-fails",
"objective": "deliberately failing stage",
"state": "FAILED",
"depends_on": [],
"priority": 2
},
{
"stage_id": "17e2-stage-4-final",
"objective": "final stage",
"state": "COMPLETE",
"depends_on": [],
"priority": 3
}
],
"fallbacks": [],
"saved_at": "2026-09-15T17:51:22Z"
}
@@ -0,0 +1,36 @@
{
"programme_id": "t17-17e-proof-3",
"objective": "17E live proof: async event wait, failure isolation, auto-continuation, honest terminal verdict",
"stages": [
{
"stage_id": "17e3-stage-1-governance",
"objective": "governance-only stage",
"state": "COMPLETE",
"depends_on": [],
"priority": 0
},
{
"stage_id": "17e3-stage-2-async",
"objective": "async work under event bridge",
"state": "COMPLETE",
"depends_on": [],
"priority": 1
},
{
"stage_id": "17e3-stage-3-fails",
"objective": "deliberately failing stage",
"state": "FAILED",
"depends_on": [],
"priority": 2
},
{
"stage_id": "17e3-stage-4-final",
"objective": "final stage",
"state": "COMPLETE",
"depends_on": [],
"priority": 3
}
],
"fallbacks": [],
"saved_at": "2026-09-15T17:52:31Z"
}
@@ -0,0 +1,36 @@
{
"programme_id": "t17-17e-proof",
"objective": "prove canonical programme continuation: 4 stages incl. async event wait, failure, auto-continuation",
"stages": [
{
"stage_id": "17e-stage-1-governance",
"objective": "governance-only stage",
"state": "COMPLETE",
"depends_on": [],
"priority": 0
},
{
"stage_id": "17e-stage-2-async",
"objective": "async work under event bridge",
"state": "PENDING",
"depends_on": [],
"priority": 1
},
{
"stage_id": "17e-stage-3-fails",
"objective": "deliberately failing stage",
"state": "PENDING",
"depends_on": [],
"priority": 2
},
{
"stage_id": "17e-stage-4-final",
"objective": "final stage",
"state": "PENDING",
"depends_on": [],
"priority": 3
}
],
"fallbacks": [],
"saved_at": "2026-09-15T17:50:29Z"
}
+718 -130
View File
File diff suppressed because it is too large. Load diff
+37
View File
@@ -12,12 +12,49 @@ override via monkeypatch.setenv, which wins over this fixture.
"""
import os
import sys
from pathlib import Path
import pytest
# W/R bridge code dir (daemon/waitstore live out-of-repo, own snapshot).
# Env-overridable so runner users without $HOME access can point at an
# installed copy — T15 legacy hardcode, resolved T16.
WR_CODE_DIR = Path(os.environ.get(
"RUNE_WR_CODE", "/home/nick/.local/share/mimo-wait-resume"))
if str(WR_CODE_DIR) not in sys.path:
sys.path.insert(0, str(WR_CODE_DIR))
TESTS_DIR = Path(__file__).resolve().parent
REPO_ROOT = TESTS_DIR.parent
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
@pytest.fixture(autouse=True)
def _isolate_construction_store(tmp_path, monkeypatch):
monkeypatch.setenv("RUNE_CONSTRUCTION_STORE",
str(tmp_path / ".rune" / "scaffold"))
yield
@pytest.fixture(autouse=True)
def _provide_test_provider_context(monkeypatch):
"""T16 §16F: drivers/probes resolve the model from the ambient
provider context (MIMO_MODEL) and fail loudly without one. In tests,
the HARNESS is the surrounding provider layer — so the default test
context provides one, unless a test explicitly removes it (the
PROVIDER_UNRESOLVED held-outs do exactly that)."""
monkeypatch.setenv("MIMO_MODEL", "test-provider/test-model")
@pytest.fixture()
def wr_daemon(tmp_path, monkeypatch):
"""Real W/R daemon subprocess on an isolated WR_ROOT + ephemeral port.
Shared by test_event_wait.py (T15) and test_ci_event_wait.py (T16)."""
monkeypatch.setenv("RUNE_WR_ROOT", str(tmp_path)) # adapter binds here
monkeypatch.setenv("RUNE_WR_CODE", str(WR_CODE_DIR))
from tests.test_event_wait import Daemon
d = Daemon(tmp_path)
yield d
d.stop()
+303
View File
@@ -0,0 +1,303 @@
"""test_ci_event_wait.py — T16 §16C/§16D/§16E: event-driven CI
qualification wait over the EXISTING W/R bridge.
Same disposable harness as test_event_wait.py (real daemon subprocess on
an isolated WR_ROOT + ephemeral port; Daemon fixture imported — 16W rule:
no duplicated test helpers).
Proofs:
16C event wake — CI-completion event (provider=gitea,
event_type=ci_completed, correlated by sha) resumes
the waiting controller; the EVENT payload is NOT
truth — the verdict is re-read from canonical Gitea
authority (monkeypatched here; production = deploy_
qualify.py).
16C event≠truth — event claims success, authority says failure →
deployment refused (payload never decides).
16D no floor — event → resume latency is event-driven (sub-second),
not a fixed sleep floor.
16D lost event — event suppressed → watchdog expiry → ONE-SHOT
authority query → truthful recovery (or truthful
failure when authority has no verdict). Watchdog is
the failure detector, never the scheduler.
16D re-arm — watchdog expiry while CI runs are non-terminal in
Gitea → re-arm, not failure; a later event resumes.
16E one primitive— builder and gitea waits share the SAME bridge
machinery; identities select the matching events
(a gitea event never resumes a builder wait and
vice versa).
"""
from __future__ import annotations
import importlib.util
import json
import sys
import threading
import time
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import event_wait as ew # noqa: E402
from tests.test_event_wait import Daemon # noqa: E402 (shared fixture)
def _load_ci_wait_module():
spec = importlib.util.spec_from_file_location(
"ci_event_wait", str(REPO / "scripts" / "ci_event_wait.py"))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
CI_SHA = "e" * 40
def _post_when_registered(wr_daemon: Daemon, op: str, payload: dict,
timeout: float = 10.0):
"""Thread body: wait for the wait record to exist, then post the wake
event. The event POST is best-effort (tests assert on outcomes)."""
def go():
deadline = time.time() + timeout
while time.time() < deadline:
if ew.operation_wait(op) is not None:
break
time.sleep(0.05)
wr_daemon.post_event(payload) # loud: a failed wake must fail the test
t = threading.Thread(target=go, daemon=True)
t.start()
return t
def test_16c_event_wake_resumes_and_authority_decides(wr_daemon, monkeypatch):
mod = _load_ci_wait_module()
monkeypatch.setattr(mod, "authority_verdict", lambda: "success")
monkeypatch.setattr(mod, "ci_alive", lambda: False)
payload = {"operation_id": f"ci-{CI_SHA}", "provider": "gitea",
"event_type": "ci_completed", "sha": CI_SHA, "exit_code": 0,
"completed_at": time.time()}
_post_when_registered(wr_daemon, f"ci-{CI_SHA}", payload)
t0 = time.monotonic()
rc = mod.run(sha=CI_SHA, deadline_secs=25, daemon_url=wr_daemon.url())
wall = time.monotonic() - t0
# rc==0 alone is NOT proof: watchdog recovery also returns 0. The wake
# must actually have driven the resume (fast), not the watchdog.
assert rc == 0
assert wall < 15, f"wake-resume took {wall:.1f}s — event never arrived"
rec = ew.operation_wait(f"ci-{CI_SHA}")
assert rec is not None and rec["provider"] == "gitea"
assert rec["wait_kind"] == "PROCESS_EVENT"
def test_16c_event_payload_is_never_truth(wr_daemon, monkeypatch):
mod = _load_ci_wait_module()
# the event says success (exit_code 0); canonical authority says the
# CI is RED — the payload must never decide the verdict
monkeypatch.setattr(mod, "authority_verdict", lambda: "failure")
monkeypatch.setattr(mod, "ci_alive", lambda: False)
payload = {"operation_id": f"ci-{CI_SHA}", "provider": "gitea",
"event_type": "ci_completed", "sha": CI_SHA, "exit_code": 0,
"completed_at": time.time()}
_post_when_registered(wr_daemon, f"ci-{CI_SHA}", payload)
rc = mod.run(sha=CI_SHA, deadline_secs=25, daemon_url=wr_daemon.url())
assert rc == 1 # refused, despite the wake saying 0
def test_16d_event_resume_has_no_sleep_floor(wr_daemon, monkeypatch):
mod = _load_ci_wait_module()
monkeypatch.setattr(mod, "authority_verdict", lambda: "success")
monkeypatch.setattr(mod, "ci_alive", lambda: False)
payload = {"operation_id": f"ci-{CI_SHA}", "provider": "gitea",
"event_type": "ci_completed", "sha": CI_SHA, "exit_code": 0,
"completed_at": time.time()}
_post_when_registered(wr_daemon, f"ci-{CI_SHA}", payload)
t0 = time.monotonic()
rc = mod.run(sha=CI_SHA, deadline_secs=25, daemon_url=wr_daemon.url())
wall = time.monotonic() - t0
# a would-be 30s polling floor is NOT waited: registration + event +
# verdict must be seconds, not the old sleep-30 loop quantum
assert rc == 0
assert wall < 15, f"event-driven wait took {wall:.1f}s — floor crept back"
def test_16d_lost_event_watchdog_recovers_from_authority(
wr_daemon, monkeypatch):
mod = _load_ci_wait_module()
# NO event is ever posted (suppressed/lost); authority IS resolvable
monkeypatch.setattr(mod, "authority_verdict", lambda: "success")
monkeypatch.setattr(mod, "ci_alive", lambda: False)
rc = mod.run(sha=CI_SHA, deadline_secs=4, daemon_url=wr_daemon.url())
assert rc == 0
# truthful recovery, not invented success: the record shows the
# watchdog (failure detector) path, and the verdict came from authority
rec = ew.operation_wait(f"ci-{CI_SHA}")
assert rec is not None
def test_16d_lost_event_with_no_verdict_fails_truthfully(
wr_daemon, monkeypatch):
mod = _load_ci_wait_module()
monkeypatch.setattr(mod, "authority_verdict", lambda: "pending")
monkeypatch.setattr(mod, "ci_alive", lambda: False)
rc = mod.run(sha=CI_SHA, deadline_secs=4, daemon_url=wr_daemon.url())
assert rc == 1 # deployment refused — timeout is failure detection
def test_16d_ci_alive_rearm_then_event(wr_daemon, monkeypatch):
mod = _load_ci_wait_module()
monkeypatch.setattr(mod, "authority_verdict", lambda: "success")
# first watchdog expiry (t=3s): CI runs still non-terminal → re-arm,
# not fail; the event arrives only AFTER that expiry (t≥4s)
alive = {"n": 0}
def flaky_alive():
alive["n"] += 1
return alive["n"] <= 1 # alive on first expiry, gone after event
monkeypatch.setattr(mod, "ci_alive", flaky_alive)
def post_late():
deadline = time.time() + 10
while time.time() < deadline:
if ew.operation_wait(f"ci-{CI_SHA}") is not None:
break
time.sleep(0.05)
time.sleep(4.0) # strictly after the first watchdog expiry (3s)
wr_daemon.post_event({"operation_id": f"ci-{CI_SHA}",
"provider": "gitea",
"event_type": "ci_completed",
"sha": CI_SHA, "exit_code": 0,
"completed_at": time.time()})
threading.Thread(target=post_late, daemon=True).start()
rc = mod.run(sha=CI_SHA, deadline_secs=15, watchdog_secs=3,
daemon_url=wr_daemon.url())
assert rc == 0
assert alive["n"] >= 1 # the re-arm actually happened
def test_16e_one_bridge_two_adapters_identity_selects(wr_daemon):
"""16E: builder and gitea waits share the same primitive; only the
identity fields differ, and identity selects the matching event."""
# builder-style wait
b = ew.register_process_wait("op-builder-1", watchdog_secs=60)
# gitea-style wait
g = ew.register_process_wait(f"ci-{CI_SHA}", provider="gitea",
event_type="ci_completed", sha=CI_SHA,
watchdog_secs=60)
assert b["provider"] == "builder" and g["provider"] == "gitea"
# a gitea CI event must NOT resume the builder wait...
wr_daemon.post_event({"operation_id": f"ci-{CI_SHA}", "provider": "gitea",
"event_type": "ci_completed", "sha": CI_SHA,
"exit_code": 0})
brec = ew.operation_wait("op-builder-1")
grec = ew.operation_wait(f"ci-{CI_SHA}")
assert brec["status"] == "WAITING_EXTERNAL"
assert grec["status"] in ("READY_TO_RESUME", "RESUME_UNVERIFIED",
"COMPLETED")
# ...and a builder event must not resume a (fresh) gitea wait
g2 = ew.register_process_wait(f"ci-{'f' * 40}", provider="gitea",
event_type="ci_completed", sha="f" * 40,
watchdog_secs=60)
wr_daemon.post_event({"operation_id": "op-builder-1", "exit_code": 0})
assert ew.operation_wait(f"ci-{'f' * 40}")["status"] == "WAITING_EXTERNAL"
assert ew.operation_wait("op-builder-1")["status"] in (
"READY_TO_RESUME", "RESUME_UNVERIFIED", "COMPLETED")
def test_16c_event_replay_before_registration(wr_daemon, monkeypatch):
"""§16C live finding (serialized runner): the completion event lands
BEFORE the deploy job registers. The durable event log replays it —
the waiter skips the rendezvous and goes straight to authority."""
mod = _load_ci_wait_module()
monkeypatch.setattr(mod, "authority_verdict", lambda: "success")
monkeypatch.setattr(mod, "ci_alive", lambda: False)
wr_daemon.post_event({"operation_id": f"ci-{CI_SHA}",
"provider": "gitea", "event_type": "ci_completed",
"sha": CI_SHA, "exit_code": 0,
"completed_at": time.time()})
t0 = time.monotonic()
rc = mod.run(sha=CI_SHA, deadline_secs=60, watchdog_secs=300,
daemon_url=wr_daemon.url())
wall = time.monotonic() - t0
assert rc == 0
assert wall < 10, f"replayed wake took {wall:.1f}s — replay path dead"
assert ew.find_matching_event("gitea", "ci_completed",
f"ci-{CI_SHA}") is not None
def test_16c_replay_does_not_match_other_identities(wr_daemon):
wr_daemon.post_event({"operation_id": f"ci-{CI_SHA}",
"provider": "gitea", "event_type": "ci_completed",
"sha": CI_SHA, "exit_code": 0})
assert ew.find_matching_event("gitea", "ci_completed",
"ci-other") is None
assert ew.find_matching_event("builder", "builder_exit",
f"ci-{CI_SHA}") is None
def test_17c_fifo_mode_is_cross_user(wr_daemon):
"""T17 17C ROOT-CAUSE REGRESSION GUARD: the T16 chmod(0o666) was lost
in a rebase; FIFOs landed 644, the daemon (different user) got EACCES
on the resume write, and every production waiter slept to its
deadline. Mode must be 666 regardless of umask/creating user."""
import os as _os
import stat as _stat
fd = ew.open_fifo_reader("ci-mode-guard-op")
try:
mode = _os.stat(ew.fifo_path("ci-mode-guard-op")).st_mode
assert mode & 0o666 == 0o666, (
f"fifo mode {oct(mode & 0o777)} — resume writer from another "
f"user will get EACCES (17C root-cause regression)")
finally:
_os.close(fd)
def test_17c_cross_user_resume_delivery(wr_daemon):
"""Full cross-user delivery: the WAITER side (fifo + registration)
runs as the invoking user; the resume WRITE runs through the real
daemon (as its own user in production). After the mode fix the
daemon's writer must succeed (rc=0, wait COMPLETED), not rc=1."""
import os as _os
op = f"ci-crossuser-{int(time.time())}"
fd = ew.open_fifo_reader(op)
rec = ew.register_process_wait(op, provider="gitea",
event_type="ci_completed",
sha="crossuser", watchdog_secs=60)
try:
import threading
done = {}
def waiter():
done["r"] = ew.await_resume_event(op, watchdog_secs=15,
fifo_fd=fd)
t = threading.Thread(target=waiter, daemon=True)
t.start()
time.sleep(0.3)
wr_daemon.post_event({"operation_id": op, "provider": "gitea",
"event_type": "ci_completed",
"sha": "crossuser", "exit_code": 0,
"completed_at": time.time()})
t.join(timeout=20)
assert done.get("r", {}).get("kind") == "event", (
f"resume delivery failed again: {done} — 17C root-cause "
f"regression")
# the durable record must show the writer exited rc=0
deadline = time.time() + 10
while time.time() < deadline:
r = ew.operation_wait(op) or {}
if r.get("status") == "COMPLETED":
break
time.sleep(0.2)
assert r.get("status") == "COMPLETED", (
f"wait status {r.get('status')} — resume writer did not "
f"deliver cleanly (expect COMPLETED, not RESUME_UNVERIFIED)")
finally:
_os.close(fd)
+36
View File
@@ -549,3 +549,39 @@ def test_capabilities_cli_roundtrip(tmp_path):
assert out["success"]
assert out["state"]["schema_version"] == ci.INTERFACE_SCHEMA_VERSION
assert "capabilities" in out["state"]["operations"]
def test_15u_discover_active_contract_migrated_to_interface(tmp_path):
"""§15U simplification: resume discovery lives in the INTERFACE (one
canonical home), not in session-driver scripts. The drivers' helpers
are shims over this."""
from rune import construction_interface as ci
store = tmp_path / "scaffold"
iface = ci.ConstructionInterface(builder="probe",
project_store=str(store),
project_id="t15u")
iface.scaffold.init(purpose="15u probe")
empty = ci.ConstructionInterface.discover_active_contract(str(store))
assert not empty["success"], "empty store must not invent a contract"
doc = iface.scaffold.load()
from rune import structural_store as ss
ss.add_entity(doc, id="contract:spec-15u", kind="contract",
identities={"objective": "spec"}, evidence_refs=["e"])
ss.add_entity(doc, id="capability:planned", kind="capability",
maturity="DECLARED", evidence_refs=["e"])
ss.add_relationship(doc, type="implements",
from_id="capability:planned",
to_id="contract:spec-15u", evidence_refs=["e"])
iface.scaffold.commit(doc, what="seed 15u spec")
sub = iface.submit_need(
"build the 15u module",
constraints={"environment": {"os": "linux"},
"contracts": ["contract:spec-15u"]},
evidence_refs=["e"], approval_context="15u")
assert sub["success"]
found = ci.ConstructionInterface.discover_active_contract(str(store))
assert found["success"]
assert found["ids"]["contract_id"] == sub["ids"]["contract_id"]
# capabilities() advertises the new op
assert "discover_active_contract" in \
iface.capabilities()["state"]["operations"]
+82
View File
@@ -0,0 +1,82 @@
"""test_drift_resume.py — T16 §16P: drift-aware resume held-outs,
driven by the REAL 16O fixture."""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import drift_resume as dr # noqa: E402
FIXTURE = Path("/mnt/workdrive/t16-reentry")
REPO_DIR = FIXTURE / "repo"
STORE = FIXTURE / "scaffold" / "current.json"
def test_16p_drift_classes_deterministic():
ok = {"actual": "", "checked_by": "pytest", "matches": True}
bad = {"actual": "2 tests fail", "checked_by": "pytest", "matches": False}
unverif = {"actual": None, "checked_by": "pytest", "matches": None}
assert dr.classify_drift({"claim": "green"}, ok)["class"] == "CONFIRMED"
assert dr.classify_drift({"claim": "green"}, bad)["class"] == "MATERIAL"
assert dr.classify_drift({"claim": "x"}, unverif)["class"] == "UNKNOWN"
locked = dict({"claim": "layout"}, user_locked=True)
assert dr.classify_drift(locked, bad)["class"] == "CONFLICTING"
def test_16p_reality_wins_user_locked_needs_user():
r = dr.reconcile(
[{"claim": "layout locked as X", "user_locked": True}],
lambda c: {"actual": "layout is Y", "checked_by": "ls",
"matches": False})
assert r["resume"] is False # blocked, not overridden
assert r["blocking"][0]["class"] == "CONFLICTING"
def test_16p_real_fixture_vat_regression_blocks_resume():
"""The REAL fixture: scaffold claims stage-1 verified green; reality
has the VAT regression. The reconciliation must block."""
doc = json.loads(STORE.read_text())
stage1 = next(e for e in doc["entities"]
if e["id"] == "contract:stage-1-monthly-totals")
def run_fixture_tests(claim):
proc = subprocess.run(
[sys.executable, "-m", "pytest", "tests/", "-q"],
cwd=REPO_DIR, capture_output=True, text=True,
env={"PATH": "/usr/bin:/bin:/usr/local/bin",
"PYTHONPATH": "src"})
passed = proc.returncode == 0
return {"actual": proc.stdout.strip().splitlines()[-1]
if proc.stdout else proc.stderr[:120],
"checked_by": "pytest tests/", "matches": passed}
claims = [{"claim": "stage-1 verified green", "kind": "verification",
"stale_ok": False}]
r = dr.reconcile(claims, run_fixture_tests)
assert r["resume"] is False
assert r["blocking"][0]["class"] == "MATERIAL"
assert "failed" in r["blocking"][0]["evidence"]
assert stage1["phase"] == "closed" # the stale claim we caught
def test_16p_clean_repo_resumes():
"""Same observer on the checkpoint commit: no drift, resume allowed."""
def check_at_checkpoint(claim):
proc = subprocess.run(
["git", "show", "39bd917:src/report.py"], cwd=REPO_DIR,
capture_output=True, text=True)
vat_free = "1.19" not in proc.stdout
return {"actual": "" if vat_free else "VAT present at checkpoint",
"checked_by": "git show", "matches": vat_free}
r = dr.reconcile([{"claim": "stage-1 green at checkpoint"}],
check_at_checkpoint)
assert r["resume"] is True and r["blocking"] == []
+260
View File
@@ -0,0 +1,260 @@
"""test_event_wait.py — T15 §15D/§15E: event-driven wait as canonical
default + event semantics adversarials.
Runs the REAL W/R daemon as a subprocess against an isolated WR_ROOT
(the same disposable harness pattern as the bridge's own v0.2/v0.3
invariant tests), then drives the repo-side adapter (rune.event_wait)
through it. No real MiMo sessions; the "builder" is a cheap fake command.
Proofs:
15D latency — controller resumes on the EVENT; continuation
latency recorded; no arbitrary wait floor exists
(a deliberately long would-be floor is NOT waited).
15D failure — non-zero builder exit → verdict failure, resume
still fires (timeout is not success; events carry
truth).
15D watchdog — lost event → deadline expires → one-shot liveness
inspection → PROCESS_LOST classification. Watchdog
is the failure detector, never the scheduler.
15E duplicate — duplicate completion event cannot double-resume.
15E stale — event from a prior operation matches nothing.
15E wrong corr — unknown operation_id matches nothing.
15E malformed — event without exit_code leaves wait WAITING.
15E restart — controller restart after event persisted: durable
waitstore wins; resume_or_recover is idempotent.
"""
from __future__ import annotations
import json
import os
import signal
import subprocess
import sys
import tempfile
import time
import urllib.request
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
WR = Path("/home/nick/.local/share/mimo-wait-resume")
sys.path.insert(0, str(WR))
from rune import event_wait as ew # noqa: E402
class Daemon:
"""Real daemon subprocess on an isolated WR_ROOT + ephemeral port."""
def __init__(self, tmp_root: Path, port: int | None = None):
# Ephemeral port (not a fixed one): the CI runner executes the
# FULL suite in one process, and a fixed port collides with
# TIME_WAIT sockets from earlier tests in the same file or with
# parallel jobs on the same runner. 15Z CI failure: the latency
# test's daemon failed to bind 8793 (stale socket), the notifier's
# event POSTs went nowhere, and the watchdog correctly reported
# PROCESS_LOST — the test harness was at fault, not the bridge.
if port is None:
import socket as _socket
with _socket.socket() as _s:
_s.bind(("127.0.0.1", 0))
port = _s.getsockname()[1]
self.root, self.port = tmp_root, port
env = dict(os.environ,
WR_ROOT=str(tmp_root),
WR_DRIVE_SECS="1",
WR_RECONCILE_SECS="2",
WR_RECONCILE_TICKS="1",
WR_WATCH_SECS="30",
WR_TERM_GRACE_SECS="1",
RUNE_WR_ROOT=str(tmp_root))
self.proc = subprocess.Popen(
[sys.executable, str(WR / "daemon.py"), str(port)],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, env=env)
deadline = time.time() + 15
while time.time() < deadline:
try:
with urllib.request.urlopen(
f"http://127.0.0.1:{port}/health", timeout=2) as r:
if r.status == 200:
return
except Exception:
time.sleep(0.2)
raise RuntimeError("daemon did not become healthy")
def url(self) -> str:
return f"http://127.0.0.1:{self.port}"
def stop(self):
self.proc.send_signal(signal.SIGTERM)
try:
self.proc.wait(timeout=5)
except subprocess.TimeoutExpired:
self.proc.kill()
def post_event(self, payload: dict) -> dict:
req = urllib.request.Request(
self.url() + "/process-event",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=5) as r:
return json.loads(r.read())
# T16 §16E: the daemon fixture moved to tests/conftest.py (wr_daemon) so
# test_ci_event_wait.py shares ONE harness — no duplicated test helpers.
@pytest.fixture()
def daemon(wr_daemon):
return wr_daemon
# real fake-builder: sleeps then exits — cheap but genuinely a process
FAKE_BUILDER = ["bash", "-c", "sleep 2; exit 0"]
FAKE_BUILDER_FAIL = ["bash", "-c", "sleep 1; exit 3"]
def test_15d_latency_event_driven_no_floor(daemon):
"""Continuation happens FROM the event: controller resumes ~immediately
after the 2s builder exits — proving no 60s+ polling floor exists."""
op = "op-latency-proof"
rec = ew.register_process_wait(
op, contract_id="contract:t15", project="t15-fixture",
objective="latency proof", watchdog_secs=120)
assert rec["wait_kind"] == "PROCESS_EVENT"
assert rec["resume_cmd"], "per-wait resume_cmd required"
builder_started = time.time()
notifier = ew.run_builder_under_events(FAKE_BUILDER, op,
daemon_url=daemon.url())
result = ew.await_resume_event(op, watchdog_secs=120,
pgid=notifier.pid)
resumed_at = time.time()
assert result["kind"] == "event", f"expected event resume, got {result}"
wait = result["wait"]
assert wait["verdict"] == "success"
evidence = wait.get("process_event_evidence", {})
child_exit = evidence and float(evidence.get("exit_code") == 0)
# The event carries the actual exit evidence
assert evidence.get("exit_code") == 0
builder_elapsed = resumed_at - builder_started
# builder itself takes ~2s; if a 60s polling floor existed we'd exceed it
assert builder_elapsed < 30, \
f"continuation took {builder_elapsed:.1f}s — a wait floor exists"
# and it cannot be faster than the builder itself (event came from
# the real process exit, not a timer)
assert builder_elapsed >= 1.5
assert wait["status"] in ("RESUMING", "COMPLETED")
def test_15d_failure_event_carries_truth(daemon):
op = "op-failure-proof"
ew.register_process_wait(op, project="t15-fixture",
objective="failure proof", watchdog_secs=60)
notifier = ew.run_builder_under_events(FAKE_BUILDER_FAIL, op,
daemon_url=daemon.url())
result = ew.await_resume_event(op, watchdog_secs=60, pgid=notifier.pid)
assert result["kind"] == "event"
assert result["wait"]["verdict"] == "failure"
assert result["wait"]["process_event_evidence"]["exit_code"] == 3
def test_15d_watchdog_detects_lost_event(daemon, tmp_path):
"""Lost event: deadline expires → one-shot liveness inspection →
PROCESS_LOST. The watchdog never schedules; it detects failure of
continuation."""
op = "op-lost-event"
rec = ew.register_process_wait(op, project="t15-fixture",
objective="lost event", watchdog_secs=3)
# launch a long-running builder whose event will NEVER arrive
notifier = ew.run_builder_under_events(
["bash", "-c", "sleep 120"], op, daemon_url=daemon.url())
result = ew.await_resume_event(op, watchdog_secs=3, pgid=notifier.pid)
assert result["kind"] == "watchdog"
assert result["alive"] is True # process still alive → NOT lost, re-arm
# now genuinely lose it: kill the group, next watchdog sees GONE
os.killpg(notifier.pid, signal.SIGKILL)
result2 = ew.await_resume_event(op, watchdog_secs=3, pgid=notifier.pid)
assert result2["kind"] == "watchdog"
assert result2["alive"] is False # PROCESS_LOST → recovery path
# recovery: process died without completion — the wait is still
# WAITING_EXTERNAL (canonical state: nothing completed)
wait = ew.operation_wait(op)
assert wait["status"] == "WAITING_EXTERNAL"
def test_15e_duplicate_event_cannot_double_resume(daemon):
op = "op-dup-event"
ew.register_process_wait(op, project="t15-fixture", watchdog_secs=60)
first = daemon.post_event({"operation_id": op, "exit_code": 0})
assert first["matched"] >= 1
wait_for(lambda: (ew.operation_wait(op)["status"] != "READY_TO_RESUME"))
second = daemon.post_event({"operation_id": op, "exit_code": 0})
# second event matches nothing new (wait no longer WAITING_EXTERNAL)
# and must not re-dispatch a resumed wait
rec = ew.operation_wait(op)
if second["matched"] > 0:
# if it dispatched again, it must NOT have reset resume state
assert rec.get("resume_attempts", 0) >= 1
assert rec["status"] != "WAITING_EXTERNAL"
# verdict unchanged by the duplicate
assert rec.get("verdict") in ("success", None)
def test_15e_stale_event_from_prior_operation_matches_nothing(daemon):
op_old, op_new = "op-old", "op-new"
ew.register_process_wait(op_old, project="t15-fixture", watchdog_secs=60)
ew.register_process_wait(op_new, project="t15-fixture", watchdog_secs=60)
# satisfy + consume the old operation's event
daemon.post_event({"operation_id": op_old, "exit_code": 0})
wait_for(lambda: ew.operation_wait(op_old)["status"] != "READY_TO_RESUME")
# a STALE repeat of the old event must not touch the new operation
daemon.post_event({"operation_id": op_old, "exit_code": 0})
new_wait = ew.operation_wait(op_new)
assert new_wait["status"] == "WAITING_EXTERNAL"
assert new_wait.get("verdict") is None
def test_15e_wrong_correlation_matches_nothing(daemon):
op = "op-wrong-corr"
ew.register_process_wait(op, project="t15-fixture", watchdog_secs=60)
resp = daemon.post_event({"operation_id": "completely-unrelated",
"exit_code": 0})
assert resp["matched"] == 0
assert ew.operation_wait(op)["status"] == "WAITING_EXTERNAL"
def test_15e_malformed_event_without_exit_code_stays_waiting(daemon):
op = "op-malformed"
ew.register_process_wait(op, project="t15-fixture", watchdog_secs=60)
resp = daemon.post_event({"operation_id": op}) # no exit_code
assert ew.operation_wait(op)["status"] == "WAITING_EXTERNAL"
def test_15e_restart_after_event_persisted_waitstore_wins(daemon):
"""Case 10: the event was persisted and the wait advanced, but the
controller 'crashed' before handling it. On restart the durable
waitstore wins; resume_or_recover is idempotent (no duplicate
transition, no duplicate resume)."""
op = "op-restart"
ew.register_process_wait(op, project="t15-fixture", watchdog_secs=60)
daemon.post_event({"operation_id": op, "exit_code": 0})
wait_for(lambda: ew.operation_wait(op)["status"] != "READY_TO_RESUME")
r1 = ew.resume_or_recover(op)
r2 = ew.resume_or_recover(op)
assert r1["kind"] == "event" and r1.get("recovered") is True
assert r2 == r1 # idempotent: canonical state, not transient token
assert r1["wait"]["verdict"] == "success"
def wait_for(fn, timeout=30, interval=0.3):
deadline = time.time() + timeout
while time.time() < deadline:
r = fn()
if r:
return r
time.sleep(interval)
return fn()
+8
View File
@@ -19,6 +19,14 @@ def native_operator(tmp_path, monkeypatch):
stubbed so the test exercises only the construction hook."""
store = tmp_path / ".rune" / "scaffold"
monkeypatch.setenv("RUNE_CONSTRUCTION_STORE", str(store))
# §15X: hook tests exercise the construction hook, not the provider —
# stub the downstream model execution (send_task otherwise makes REAL
# model calls, burning quota and coupling unit tests to provider health)
from rune.tool_protocol import OperatorResponse
monkeypatch.setattr(
"rune.rune_operator.RuneOperator._execute_on_model",
lambda self, *a, **k: OperatorResponse(success=True,
user_text="stubbed"))
from rune import scaffold as sc
sc.Scaffold(store, "rune").init(purpose="rune native test")
import rune.rune_operator as ro
+251
View File
@@ -0,0 +1,251 @@
"""test_native_default.py — T15 §15J/§15K: Scaffolding as the NATIVE
DEFAULT construction route + bypass prevention.
15J: ordinary send_task input (the real TUI submission path per the
12A.C trace: TUI HistoryInput → submit → RuneOperator.send_task →
construction hook) automatically produces canonical construction state:
A. PROJECT_BUILD — durable Project Scaffold, no /build command
B. EXISTING — attaches Rune authority, no fake project
C. LOCAL_CHANGE — lightweight, still governed
D. NOT_CONSTRUCTION — no construction state
E. UNKNOWN — probes, no invention
F. manual override through the SAME path (/scaffold-mode marker)
15K: a native client construction command (/build start) cannot mint
construction outside the authority — NOT_CONSTRUCTION is refused;
BuildManager state is created only AFTER canonical recognition.
"""
import json
import os
import sys
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import scaffold as sc # noqa: E402
from rune import structural_store as ss # noqa: E402
@pytest.fixture()
def op(tmp_path, monkeypatch):
monkeypatch.setenv("RUNE_CONSTRUCTION_STORE",
str(tmp_path / ".rune" / "scaffold"))
from rune.rune_operator import RuneOperator
# §15X process repair: construction-hook tests exercise the hook, not
# the provider. send_task invokes a REAL model downstream of the hook —
# unstubbed, these unit tests burn provider quota and starve other
# suites' model calls (observed: stage-telemetry GATE_FAILED after
# quota exhaustion). The hook runs BEFORE model selection, so a stub
# changes nothing these tests assert.
from rune.tool_protocol import OperatorResponse
monkeypatch.setattr(
"rune.rune_operator.RuneOperator._execute_on_model",
lambda self, *a, **k: OperatorResponse(success=True,
user_text="stubbed"))
return RuneOperator(str(tmp_path / "operator.json")), tmp_path
def _last_activation(store: Path):
doc = json.loads((store / ".rune" / "scaffold" / "current.json").read_text())
recs = [o for o in doc.get("observations", [])
if o.get("kind") == "construction_activated"]
return recs[-1] if recs else None
# ── 15J ─────────────────────────────────────────────────────────────────────
def test_15j_native_project_build_without_any_command(op):
op, tmp = op
op.send_task("Build me a standalone practice scheduler app")
rec = _last_activation(tmp)
assert rec is not None, "native input must produce construction state"
assert rec["mode"] == "PROJECT_BUILD"
assert rec["activated"] is True
# durable provisional Project Scaffold exists
projects = (tmp / ".rune" / "scaffold" / "projects")
assert any(projects.glob("*/current.json")), "identity before repository"
def test_15j_native_existing_project_change_attaches_rune(op):
op, tmp = op
op.send_task("Add CSV export to the rune project")
rec = _last_activation(tmp)
assert rec["mode"] == "EXISTING_PROJECT_CHANGE"
assert rec["activated"] is True
def test_15j_native_local_change_is_light(op):
op, tmp = op
op.send_task("Fix the typo in the README")
rec = _last_activation(tmp)
assert rec["mode"] == "LOCAL_CHANGE"
assert rec["activated"] is True
def test_15j_native_not_construction_touches_nothing(op):
op, tmp = op
op.send_task("Explain modal interchange theory")
rec = _last_activation(tmp)
assert rec["mode"] == "NOT_CONSTRUCTION"
assert rec["activated"] is False
# no project invention
projects = (tmp / ".rune" / "scaffold" / "projects")
assert not projects.exists() or not any(projects.glob("*/current.json"))
def test_15j_native_unknown_probes_without_invention(op):
op, tmp = op
op.send_task("Build something useful for this")
rec = _last_activation(tmp)
assert rec["mode"] == "UNKNOWN"
assert rec["activated"] is False
assert not (tmp / ".rune" / "scaffold" / "projects").exists() or \
not any((tmp / ".rune" / "scaffold" / "projects").glob("*/current.json"))
def test_15j_manual_override_same_path(op):
"""Case F: /scaffold-mode marker feeds explicit_override through the
SAME classify_mode call — provenance carries 'user'."""
op, tmp = op
op.send_task("/scaffold-mode NOT_CONSTRUCTION build me a scheduler app")
rec = _last_activation(tmp)
assert rec["mode"] == "NOT_CONSTRUCTION", \
"override must win through the SAME path"
assert "user" in " ".join(rec.get("classification_evidence", [])), \
"override provenance must be recorded"
# and a non-overridden version of the same text classifies normally
op.send_task("Build me a standalone practice scheduler app")
rec2 = _last_activation(tmp)
assert rec2["mode"] == "PROJECT_BUILD"
# ── 15K ─────────────────────────────────────────────────────────────────────
def test_15k_build_manager_has_no_parallel_construction_state(tmp_path,
monkeypatch):
"""§15K inventory: BuildManager (the /build vehicle) creates its own
phase state but must NOT create project scaffolds/contracts. The
canonical construction record comes only from the send_task hook.
BuildManager.start_build shells out to `git worktree add` against a
HARDCODED production repo path (/mnt/workdrive/rune) — that is real
VCS mutation and has no place in a unit test (it fails on CI with
dubious-ownership, and would pollute the real repo locally). This
test therefore inspects the manager's state machine WITHOUT invoking
the worktree-creating path: construction-state separation is a
property of where BuildManager writes, not of the git operation."""
from rune import build_mode
from rune.build_mode import BuildManager
monkeypatch.chdir(tmp_path) # NOT bare os.chdir — must not leak
monkeypatch.setattr(build_mode, "BUILDS_DIR",
str(tmp_path / ".rune" / "work" / "builds"))
monkeypatch.setattr(build_mode, "WORKTREE_DIR",
str(tmp_path / ".rune" / "worktrees"))
bm = BuildManager()
# start_build's worktree step is a real VCS mutation — stub it; the
# property under test (no canonical construction state) is orthogonal
monkeypatch.setattr(
BuildManager, "create_worktree",
lambda self: setattr(self.state, "worktree_path", "stubbed") or "stubbed")
build_id = bm.start_build("build a thing")
# BuildManager's own state exists...
assert bm.state.description == "build a thing"
# ...but it created NO canonical construction authority
assert not (tmp_path / "store" / "scaffold").exists() or \
not any((tmp_path / "store" / "scaffold").rglob("current.json"))
def test_15k_tui_build_start_refuses_not_construction(monkeypatch, tmp_path):
"""/build start with a non-construction description must be refused
BEFORE BuildManager mints build state — no silent bypass."""
monkeypatch.setenv("RUNE_CONSTRUCTION_STORE",
str(tmp_path / ".rune" / "scaffold"))
from rune.rune_operator import RuneOperator
op = RuneOperator(str(tmp_path / "operator.json"))
desc = "explain modal interchange theory"
# the same classification the hardened /build start handler performs
from rune.construction_mode import classify_mode
d = classify_mode(desc, target_project=None, project_exists=None,
affected_surface=[])
assert d.mode == "NOT_CONSTRUCTION"
# the handler's contract: refuse (never reach BuildManager.start_build)
# — proven here at the decision level plus the hook's canonical record
op.send_task(f"/build start: {desc}")
# via the hook path, the canonical store records NOT_CONSTRUCTION and
# no build artifact exists
doc = json.loads((tmp_path / ".rune" / "scaffold" / "current.json").read_text())
recs = [o for o in doc.get("observations", [])
if o.get("kind") == "construction_activated"]
assert recs and recs[-1]["mode"] in ("NOT_CONSTRUCTION", "UNKNOWN"), \
"prefixed build command must not mint construction for a chat intent"
assert recs[-1]["activated"] is False
assert not (tmp_path / ".rune" / "work" / "builds").exists() or \
not list((tmp_path / ".rune" / "work" / "builds").iterdir())
def test_15l_research_requests_never_become_project_builds():
"""15L negative: research/investigation intents must not PROJECT_BUILD
merely because they contain 'build'/'construct' words. Ownership:
research competence = Research Core; continuity = future Substrate.
The classifier may probe (UNKNOWN) but must never invent a project."""
from rune.construction_mode import classify_mode
research_intents = [
"investigate why the build fails",
"build understanding of the migration path",
"construct an explanation of modal interchange",
"research the best approach for CSV export",
]
for t in research_intents:
d = classify_mode(t, target_project=None, project_exists=None,
affected_surface=[])
assert d.mode != "PROJECT_BUILD", \
f"research intent '{t}' must not PROJECT_BUILD"
assert d.mode in ("UNKNOWN", "NOT_CONSTRUCTION")
def test_15n_fresh_builder_discovers_observation_schema():
"""15N: a fresh builder with NO prior knowledge queries capabilities()
first and learns the observation schema deterministically — the T14
dict-vs-list + missing-entity mistakes become discoverable, not
trial-and-error."""
import tempfile
from rune import construction_interface as ci
tmp = Path(tempfile.mkdtemp())
iface = ci.ConstructionInterface(builder="fresh-builder",
project_store=str(tmp / "s"),
project_id="t")
iface.scaffold.init(purpose="15N probe")
caps = iface.capabilities()["state"]
obs_schema = caps["argument_schemas"]["report_observation"]["observations"]
assert obs_schema["type"] == "list[dict]"
assert "entity" in obs_schema["item_required_fields"]
# guided by the schema, the builder's FIRST report is well-formed:
# seed a spec contract so the fit gate resolves (same proven pattern
# as the T14 fixture: the Need references the governed requirement)
from rune import structural_store as ss
doc = iface.scaffold.load()
ss.add_entity(doc, id="contract:spec-15n", kind="contract",
identities={"objective": "spec"}, evidence_refs=["e"])
ss.add_entity(doc, id="capability:planned-15n", kind="capability",
maturity="DECLARED", evidence_refs=["e"])
ss.add_relationship(doc, type="implements",
from_id="capability:planned-15n",
to_id="contract:spec-15n", evidence_refs=["e"])
iface.scaffold.commit(doc, what="seed 15n spec")
sub = iface.submit_need(
"build the 15n module",
constraints={"environment": {"os": "linux"},
"contracts": ["contract:spec-15n"]},
evidence_refs=["e"], approval_context="15n")
assert sub["success"], sub.get("errors")
cid = sub["ids"]["contract_id"]
r = iface.report_observation(cid, [
{"entity": "m.js", "detail": "created"}])
assert r["success"], r.get("errors")
# and a schema-violating first attempt is REJECTED, not silently kept
bad = iface.report_observation(cid, {"entity": "x", "detail": "dict"})
assert not bad["success"]
+150
View File
@@ -0,0 +1,150 @@
"""test_programme_continuation.py — T15 §15M: 'task done' ≠ 'programme
done'.
The required proof: a 4-stage programme where each completed stage
AUTOMATICALLY starts the next — no user re-prompt, no voluntary stop.
Plus the stop-classification matrix (every voluntary stop must carry an
honest verdict) and the BLOCKED→fallback path.
"""
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune.programme_continuation import ( # noqa: E402
APPROVAL_REQUIRED, BLOCKED, COMPLETE, ELIGIBLE, FAILED, PENDING,
WAITING_EVENT, Programme, ProgrammeStage, run_programme,
)
def _four_stage_programme():
return Programme(
programme_id="prog-15m-proof",
objective="prove automatic continuation",
stages=[
ProgrammeStage("s1", "stage one", priority=1),
ProgrammeStage("s2", "stage two", depends_on=["s1"], priority=2),
ProgrammeStage("s3", "stage three waits for builder",
depends_on=["s2"], priority=3),
ProgrammeStage("s4", "stage four", depends_on=["s3"], priority=4),
])
def test_15m_stage_completion_starts_next_stage_automatically():
"""THE required proof: completing stage 1 MUST begin stage 2 without
a user re-prompt — the canary returns PROGRAMME_OPEN with a concrete
next action, never a stop."""
prog = _four_stage_programme()
v = prog.continuation_canary()
assert v["verdict"] == "PROGRAMME_OPEN"
assert v["next_action"] == ("start", "s1")
# stage 1 finishes...
prog.stages[0].state = COMPLETE
v2 = prog.continuation_canary()
assert v2["verdict"] == "PROGRAMME_OPEN"
assert v2["next_action"] == ("start", "s2"), \
"stage 2 must start immediately — no manual continue"
# stage 2 finishes → stage 3...
prog.stages[1].state = COMPLETE
v3 = prog.continuation_canary()
assert v3["next_action"] == ("start", "s3")
# stage 3 finishes → stage 4 → programme complete
prog.stages[2].state = COMPLETE
prog.stages[3].state = COMPLETE
v4 = prog.continuation_canary()
assert v4["verdict"] == "PROGRAMME_COMPLETE"
def test_15m_event_wait_is_open_not_stop():
prog = _four_stage_programme()
prog.stages[0].state = COMPLETE
prog.stages[1].state = COMPLETE
prog.stages[2].state = WAITING_EVENT
v = prog.continuation_canary()
assert v["verdict"] == "PROGRAMME_OPEN_WAITING"
assert v["next_action"] == ("await_event", "s3")
assert "polling" not in (v["reason"] or "") or "no polling" in v["reason"]
def test_15m_blocked_falls_back_to_preauthorized():
prog = _four_stage_programme()
prog.fallbacks = ["fallback-docs"]
prog.stages[0].state = COMPLETE
prog.stages[1].state = BLOCKED # provider outage, say
prog.stages[2].state = BLOCKED
v = prog.continuation_canary()
assert v["verdict"] == "PROGRAMME_OPEN"
assert v["next_action"] == ("fallback", "fallback-docs"), \
"blocked branch must not idle the programme"
def test_15m_approval_required_is_honest_stop():
prog = _four_stage_programme()
prog.stages[0].state = COMPLETE
prog.stages[1].state = APPROVAL_REQUIRED
prog.stages[2].state = APPROVAL_REQUIRED
prog.stages[3].state = APPROVAL_REQUIRED
v = prog.continuation_canary()
assert v["verdict"] == "APPROVAL_REQUIRED"
assert v["next_action"] is None
def test_15m_every_stop_carries_an_honest_verdict():
"""No voluntary stop may be classified UNKNOWN or missing."""
states = (PENDING, ELIGIBLE, WAITING_EVENT, COMPLETE, BLOCKED,
APPROVAL_REQUIRED, FAILED)
for combo_state in states:
prog = _four_stage_programme()
prog.stages[0].state = COMPLETE
for s in prog.stages[1:]:
s.state = combo_state
v = prog.continuation_canary()
assert v["verdict"] in (
"PROGRAMME_COMPLETE", "PROGRAMME_OPEN", "PROGRAMME_OPEN_WAITING",
"ALL_REMAINING_BLOCKED", "APPROVAL_REQUIRED",
"PROGRAMME_COMPLETE_WITH_FAILURES", # T17 17E: honest all-terminal
"UNKNOWN_INTERRUPTION"), v
if v["verdict"] == "UNKNOWN_INTERRUPTION":
assert combo_state == FAILED, \
"UNKNOWN only tolerable for genuinely unclassifiable state"
def test_15m_run_programme_drives_without_manual_continue(tmp_path):
"""End-to-end: the runner drives all 4 stages autonomously; the
transition log is the proof artifact; it persists and reloads."""
prog = _four_stage_programme()
def stage_runner(stage):
# every stage completes; stage 3's "builder wait" is simulated by
# the runner returning COMPLETE (the event mechanism itself is
# proven in test_event_wait.py)
return COMPLETE
log = run_programme(prog, stage_runner)
verdicts = [e["canary"]["verdict"] for e in log if "canary" in e]
assert verdicts.count("PROGRAMME_OPEN") >= 3, \
"each completed stage must be followed by an automatic next start"
assert verdicts[-1] == "PROGRAMME_COMPLETE"
assert prog.stages[3].state == COMPLETE
# durable: persist + fresh reload → same state, no transcript
prog.save(tmp_path)
reloaded = Programme.load(tmp_path, "prog-15m-proof")
assert reloaded is not None
assert all(s.state == COMPLETE for s in reloaded.stages)
assert reloaded.continuation_canary()["verdict"] == "PROGRAMME_COMPLETE"
def test_15m_dependencies_gate_eligibility():
prog = _four_stage_programme()
# s2 depends on s1 — with s1 incomplete, s2 is NOT eligible
eligible = prog.eligible_stages()
assert [s.stage_id for s in eligible] == ["s1"]
prog.stages[0].state = COMPLETE
eligible = prog.eligible_stages()
assert [s.stage_id for s in eligible] == ["s2", "s3", "s4"] \
or [s.stage_id for s in eligible] == ["s2"], \
"s3/s4 depend transitively on s2/s3 — not eligible yet"
+160
View File
@@ -0,0 +1,160 @@
"""test_programme_wiring.py — T17 §17D/§17E: canonical programme
continuation wiring held-outs.
The 15M canary proved the RULE; these prove the WIRING: stages bound to
real Scaffolding contracts, driven through the Construction Interface
lifecycle, with restart/duplicate/idempotency/adversarial cases.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import construction_interface as ci # noqa: E402
from rune import programme_continuation as pc # noqa: E402
@pytest.fixture()
def iface(tmp_path):
return ci.ConstructionInterface(
builder="t17-programme-test",
project_store=str(tmp_path / "scaffold"),
project_id="prog-test")
def _make_contract(iface, contract_id, need="stage need"):
doc = iface.scaffold.load()
iface.scaffold.new_contract(doc, contract_id=contract_id, need=need,
origin="programme", affected_entities=[])
iface.scaffold.save(doc)
def _prog(prog_id, stages):
p = pc.Programme(programme_id=prog_id, objective="test programme")
for sid, work in stages:
st = pc.ProgrammeStage(stage_id=sid, objective=sid,
priority=len(p.stages))
if work is not None:
st.result["work"] = work
p.stages.append(st)
return p
def _phase(iface, contract_id):
doc = iface.scaffold.load()
c = iface.scaffold.contract(doc, contract_id)
return (c or {}).get("phase", "")
def test_17e_all_four_stages_close_with_evidence(iface, tmp_path):
stages = [("st-1", None),
("st-2", {"ok": True, "evidence": "did the thing"}),
("st-3", {"ok": True, "evidence": "more work"})]
prog = _prog("p-four", stages)
for sid, _ in stages:
_make_contract(iface, sid)
log = pc.run_construction_programme(prog, iface,
scaffold_store=tmp_path / "scaffold")
assert prog.continuation_canary()["verdict"] == "PROGRAMME_COMPLETE"
for sid, _ in stages:
assert _phase(iface, sid) == "closed"
doc = iface.scaffold.load()
c = next(e for e in doc["entities"] if e["id"] == "contract:st-2")
assert any("did the thing" in (cp.get("note", "") + cp.get("evidence", ""))
for cp in c.get("checkpoints", []))
def test_17e_failure_does_not_stop_programme(iface, tmp_path):
stages = [("f-1", {"ok": False, "evidence": "verification failed"}),
("f-2", {"ok": True, "evidence": "independent work ok"})]
prog = _prog("p-fail", stages)
for sid, _ in stages:
_make_contract(iface, sid)
pc.run_construction_programme(prog, iface,
scaffold_store=tmp_path / "scaffold")
verdict = prog.continuation_canary()["verdict"]
assert verdict == "PROGRAMME_COMPLETE_WITH_FAILURES"
assert _phase(iface, "f-1") == "failed" # truthful failure, not hung
assert _phase(iface, "f-2") == "closed"
def test_17e_restart_resumes_from_durable_record(iface, tmp_path):
stages = [("r-1", {"ok": True, "evidence": "done"}), ("r-2", None)]
prog = _prog("p-restart", stages)
for sid, _ in stages:
_make_contract(iface, sid)
base = tmp_path / "scaffold" / "programmes"
# stage 1 genuinely completed before the "restart": close its contract
iface.transition("r-1", "approved", evidence_ref="pre")
iface.transition("r-1", "building", evidence_ref="pre")
iface.transition("r-1", "verifying", evidence_ref="pre")
iface.close("r-1", observed_evidence=[{"source": "pre", "detail": "x"}],
scope_files=[])
prog.stages[0].state = pc.COMPLETE
prog.save(base)
# "restart": reload from the durable record only
prog2 = pc.Programme.load(base, "p-restart")
assert prog2.stages[0].state == pc.COMPLETE
assert prog2.stages[1].state == pc.PENDING
pc.run_construction_programme(prog2, iface,
scaffold_store=tmp_path / "scaffold")
assert _phase(iface, "r-1") == "closed"
assert _phase(iface, "r-2") == "closed"
def test_17e_duplicate_continuation_signal_idempotent(iface, tmp_path):
_make_contract(iface, "d-1")
prog = _prog("p-dup", [("d-1", {"ok": True, "evidence": "once"})])
base = tmp_path / "scaffold"
log1 = pc.run_construction_programme(prog, iface, scaffold_store=base)
# re-run the whole programme against the SAME contracts
prog2 = pc.Programme.load(base / "programmes", "p-dup")
log2 = pc.run_construction_programme(prog2, iface, scaffold_store=base)
assert _phase(iface, "d-1") == "closed" # not re-opened, not corrupted
assert any(e["canary"]["verdict"] == "PROGRAMME_COMPLETE"
for e in log2 if "canary" in e)
def test_17e_already_completed_stage_skipped(iface, tmp_path):
_make_contract(iface, "s-1")
iface.transition("s-1", "approved", evidence_ref="pre")
iface.transition("s-1", "building", evidence_ref="pre")
iface.transition("s-1", "verifying", evidence_ref="pre")
iface.close("s-1", observed_evidence=[{"source": "pre", "detail": "x"}],
scope_files=[])
prog = _prog("p-skip", [("s-1", {"ok": True, "evidence": "rework?"})])
log = pc.run_construction_programme(prog, iface,
scaffold_store=tmp_path / "scaffold")
# already-closed → COMPLETE without re-running work
assert prog.continuation_canary()["verdict"] == "PROGRAMME_COMPLETE"
assert not any("rework" in str(e) for e in log)
def test_17e_work_callable_receives_interface_and_stage(iface, tmp_path):
seen = {}
def work(i, stage):
seen["iface"] = type(i).__name__
seen["stage"] = stage.stage_id
return {"ok": True, "evidence": "callable ran"}
_make_contract(iface, "w-1")
prog = _prog("p-call", [("w-1", work)])
pc.run_construction_programme(prog, iface,
scaffold_store=tmp_path / "scaffold")
assert seen == {"iface": "ConstructionInterface", "stage": "w-1"}
assert _phase(iface, "w-1") == "closed"
def test_17e_missing_contract_fails_honestly(iface, tmp_path):
prog = _prog("p-missing", [("ghost-1", {"ok": True, "evidence": "x"})])
log = pc.run_construction_programme(prog, iface,
scaffold_store=tmp_path / "scaffold")
assert prog.stages[0].state == pc.FAILED
assert "no contract" in (prog.stages[0].result.get("error") or "")
+198
View File
@@ -0,0 +1,198 @@
"""test_provider_ownership.py — T16 §16F/§16G: provider ownership
boundary + provider failure held-outs.
THE RULE (permanent architecture):
SCAFFOLDING DOES NOT OWN PROVIDER SELECTION.
Proofs:
16F guard — no provider/model name leaks into Scaffolding modules
(structural, permanent — the audit is enforced by the
suite, not by a one-off grep).
16F seam — provider_context.require_model resolves from the ambient
context and fails LOUDLY (ProviderUnresolved) when the
provider layer has not selected a model; no silent
default exists.
16G-A/B — provider context flows THROUGH the construction path:
the builder driver inherits whatever the provider layer
set; Scaffolding never names one.
16G-C — a real provider error (401/403/404/429/5xx) is preserved
VERBATIM — never relabelled a generic "400".
16G-D — NO fallback: a provider failure attempts zero alternate
providers (policy = block_branch_preserve_error, 0
retries).
16G-E — the construction state remains resumable after a
provider block (block ≠ dead contract).
16G-G — Scaffolding canonical project truth is unaffected by
provider preference: switching the provider context does
not mutate scaffold state.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import failure_recovery as fr # noqa: E402
from rune.provider_context import ProviderUnresolved, require_model # noqa: E402
# The Scaffolding core: construction authority modules that must never
# own provider selection (T16 §16F audit scope).
_SCAFFOLDING_CORE = [
"scaffold.py", "construction_interface.py",
"construction_decision.py", "construction_mode.py",
"construction_pattern.py", "construction_patterns_builtin.py",
"stage_guards.py", "event_wait.py",
"failure_recovery.py", "structural_store.py",
]
# construction_* modules discovered live — new ones are covered
# automatically (the guard follows the surface, not a hand-list)
import re as _re
SCAFFOLDING_MODULES = sorted(set(
_re.match(r"(construction_\w+)\.py", p.name).group(1) + ".py"
for p in (REPO / "src" / "rune").glob("construction_*.py")
) | set(_SCAFFOLDING_CORE))
_PROVIDER_TOKENS = [
"Verboo", "verboo", "glm-5.3-flash", "mimo-v2.5-pro", "Xioami",
"xiaomi", "qwen3:14b", "BUILDER_MODEL", "MIMO_MODEL",
]
def test_16f_no_provider_names_in_scaffolding_modules():
"""Structural guard: the construction authority is provider-blind."""
src = REPO / "src" / "rune"
for mod in SCAFFOLDING_MODULES:
text = (src / mod).read_text()
# strip comments and docstrings crudely but decisively: any REAL
# code reference would survive; pure prose mentions live in
# comments/docstrings. We assert on code lines only.
code_lines = [
ln for ln in text.splitlines()
if ln.strip() and not ln.strip().startswith(("#", '"', "'"))
]
for token in _PROVIDER_TOKENS:
hits = [ln for ln in code_lines if token in ln]
assert not hits, (
f"Scaffolding module {mod} references provider token "
f"{token!r} in CODE: {hits[:2]} — Scaffolding must not "
f"own provider selection (T16 §16F)")
def test_16f_require_model_resolves_ambient_context(monkeypatch):
monkeypatch.setenv("MIMO_MODEL", "Provider A/model-a")
assert require_model() == "Provider A/model-a"
def test_16f_missing_context_fails_loudly_never_silent(monkeypatch):
monkeypatch.delenv("MIMO_MODEL", raising=False)
with pytest.raises(ProviderUnresolved) as ei:
require_model()
assert "PROVIDER_UNRESOLVED" in str(ei.value)
def test_16g_c_real_provider_status_preserved_verbatim():
"""No test may pretend all provider failures are a generic '400'."""
for err in ("HTTP 401 Unauthorized: bad api key",
"HTTP 403 Forbidden: quota tier",
"HTTP 404 model not found: glm-5.3-flash",
"HTTP 429 rate limited: retry after 30s",
"HTTP 502 upstream unavailable"):
verdict = fr.classify_failure(err, context={"provider_error": True})
assert verdict["class"] == fr.PROVIDER_UNAVAILABLE
# the REAL code survives into the evidence, verbatim
assert err.split(":")[0] in verdict["evidence"] or \
err in verdict["evidence"]
assert verdict["policy"] == fr.POLICY[fr.PROVIDER_UNAVAILABLE]
assert verdict["policy"][0] == 0 # zero retries, zero fallbacks
def test_16g_d_no_fallback_provider_attempted():
"""A provider failure stops the branch — the plan of record contains
exactly ONE provider attempt, and the recovery policy blocks."""
attempted_providers: list[str] = []
def construction_path(provider_ctx: str, fail_on: str):
attempted_providers.append(provider_ctx)
try:
if provider_ctx == fail_on:
raise RuntimeError(f"HTTP 401 Unauthorized from {provider_ctx}")
except RuntimeError as exc:
verdict = fr.classify_failure(str(exc),
context={"provider_error": True})
if verdict["policy"][0] == 0: # block_branch_preserve_error
return verdict # STOP the dependent branch — no plan B
return None
verdict = construction_path("Provider A/model-a", "Provider A/model-a")
assert attempted_providers == ["Provider A/model-a"] # 16G-D: ONE
assert verdict["class"] == fr.PROVIDER_UNAVAILABLE
assert "401" in verdict["evidence"] # real code preserved
def test_16g_e_construction_remains_resumable_after_provider_block(tmp_path):
"""Block ≠ dead: after a provider-unavailable block the scaffold
contract must still be loadable and non-terminal (resume stays
legal)."""
from rune import failure_recovery as fr
from rune import scaffold as sc
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "prov-e")
doc = s.init(purpose="provider block resumability")
s.new_contract(doc, contract_id="prov-e-1", need="build a thing")
verdict = fr.classify_failure("HTTP 429 quota exhausted",
context={"provider_error": True})
for e in doc["entities"]:
if e["id"] == "contract:prov-e-1":
e["identities"]["blocker"] = {
"class": verdict["class"], "policy": verdict["policy"],
"evidence": verdict["evidence"],
}
s.save(doc)
# a FRESH process reads the same contract: non-terminal + resumable
doc2 = sc.Scaffold(tmp_path / ".rune" / "scaffold", "prov-e").load()
c = next(e for e in doc2["entities"]
if e["id"] == "contract:prov-e-1")
assert c["identities"]["blocker"]["class"] == "PROVIDER_UNAVAILABLE"
assert c["identities"]["blocker"]["policy"][1].startswith("block_branch")
assert c.get("phase") != "closed" # block is NOT completion
def test_16g_g_provider_preference_never_touches_scaffold_truth(tmp_path):
"""Switching the provider context must leave Scaffolding canonical
project state byte-identical."""
import os
from rune import scaffold as sc
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "prov-g")
doc = s.init(purpose="provider neutrality")
s.new_contract(doc, contract_id="prov-g-1", need="a need")
s.save(doc)
before = (tmp_path / ".rune" / "scaffold" / "current.json").read_text()
for ctx in ("Provider A/model-a", "Provider B/model-b", None):
if ctx:
os.environ["MIMO_MODEL"] = ctx
else:
os.environ.pop("MIMO_MODEL", None)
s2 = sc.Scaffold(tmp_path / ".rune" / "scaffold", "prov-g")
doc2 = s2.init(purpose="provider neutrality") # idempotent re-init
s2.save(doc2)
after = (tmp_path / ".rune" / "scaffold" / "current.json").read_text()
assert "Provider" not in after # provider context never leaks in
before = after
os.environ.pop("MIMO_MODEL", None)
def test_16g_drivers_have_no_silent_provider_default():
"""The builder/research drivers resolve provider via require_model —
a missing context exits 3 (PROVIDER_UNRESOLVED), never a baked-in
provider name."""
for script in ("t14_builder_session.py", "t15_builder_session.py"):
text = (REPO / "scripts" / script).read_text()
assert "require_model" in text, f"{script} bypasses the seam"
assert "Verboo Code/glm-5.3-flash\"\" )" not in text
assert '"Verboo Code/glm-5.3-flash")' not in text and \
"'Verboo Code/glm-5.3-flash')" not in text, (
f"{script} still carries a hardcoded provider default")
+216
View File
@@ -0,0 +1,216 @@
"""test_stage_guards.py — T15 §15F/§15G/§15H/§15I held-outs.
Hostile-first: these tests attempt to falsify the guard semantics,
not encode them.
15F readiness failure BLOCKS the action (action must NOT run)
15F exit canary failure prevents stage PASS even when the action
"succeeded" (self-certification impossible)
15F liveness: inactivity is STALLED, never completion
15G zero tests + exit 0 → FALSE_GREEN_REFUSED
15G all-skipped + exit 0 → refused
15G contract-required artifact absent → refused despite green command
15G genuine pass with contract-sufficient tests → PASS
15H Henxels REQUIRED at structural boundaries / FULL depth;
NOT_APPLICABLE for tiny bounded changes — always with rationale
15I command exits 0 but durable effect absent → delta non-zero →
the next stage MUST NOT start
15I reconcile zero-delta → continue
"""
import json
import subprocess
import subprocess
import subprocess
import os
import sys
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune.stage_guards import ( # noqa: E402
ExitCanaryFailed, StageBlocked, StageContract, StageCheck,
TestExpectations, execute_stage, henxels_placement, liveness_canary,
readiness_canary, reconcile, verify_test_truth,
)
def _stage(tmp_path, *, ready=None, done=None):
marker = tmp_path / "effect.txt"
return StageContract(
stage_id="s1", contract_id="contract:t15",
ready_when=ready if ready is not None else [],
done_when=done if done is not None else [
StageCheck(name="effect", kind="path_exists", path=str(marker))],
expected_effect="marker exists",
), marker
# ── 15F ─────────────────────────────────────────────────────────────────────
def test_15f_readiness_failure_blocks_action(tmp_path):
sentry = tmp_path / "action-ran.txt"
stage, _ = _stage(tmp_path, ready=[
StageCheck(name="prereq", kind="path_exists",
path=str(tmp_path / "missing-prereq.txt"))])
def action():
sentry.write_text("must not happen")
return {}
with pytest.raises(StageBlocked) as e:
execute_stage(stage, action)
assert e.value.failures[0]["name"] == "prereq"
assert not sentry.exists(), "blocked stage must NOT execute"
def test_15f_exit_canary_refuses_self_certification(tmp_path):
"""The action 'succeeds' and CLAIMS completion — but the expected
effect does not exist in reality. Exit canary refuses."""
stage, marker = _stage(tmp_path) # done_when: marker must exist
def lying_action():
return {"claimed": "complete, trust me"}
with pytest.raises(ExitCanaryFailed) as e:
execute_stage(stage, lying_action)
assert e.value.failures[0]["name"] == "effect"
assert not marker.exists()
def test_15f_exit_canary_passes_on_real_effect(tmp_path):
stage, marker = _stage(tmp_path)
def real_action():
marker.write_text("done")
return {"wrote": str(marker)}
result = execute_stage(stage, real_action)
assert result["verdict"] == "PASS"
def test_15f_liveness_inactivity_is_not_completion():
alive = liveness_canary(last_progress_at=100.0, now=110.0)
stalled = liveness_canary(last_progress_at=100.0, now=500.0)
assert alive["verdict"] == "ALIVE"
assert stalled["verdict"] == "STALLED"
assert stalled["stalled"] is True
# a stalled stage has NO completion semantics — only STALLED
assert "complete" not in stalled["verdict"].lower()
# ── 15G ─────────────────────────────────────────────────────────────────────
def _write_node_project(tmp_path, body: str):
(tmp_path / "scheduler.test.js").write_text(body)
def test_15g_zero_tests_with_green_exit_is_false_green(tmp_path):
_write_node_project(tmp_path, "console.log('nothing tested');")
r = verify_test_truth(
["node", "scheduler.test.js"], str(tmp_path),
TestExpectations(require_tests=True), base_dir=str(tmp_path))
assert r["verdict"] == "FALSE_GREEN_REFUSED"
assert any(f["name"] == "test_count" for f in r["failures"])
def test_15g_all_skipped_is_refused(tmp_path):
# 2 "tests" all skipped: node --test reports pass 0 skip 2 → the
# zero-count guard also triggers; craft a runner-shaped output where
# pass counts exist but all runs were skips is covered by the
# skip-ratio check via a fake runner script
runner = tmp_path / "fake-runner.sh"
runner.write_text('#!/bin/bash\necho "# pass 5"\necho "# skipped 5"\nexit 0\n')
runner.chmod(0o755)
r = verify_test_truth([str(runner)], str(tmp_path),
TestExpectations(require_tests=True),
base_dir=str(tmp_path))
assert r["verdict"] == "FALSE_GREEN_REFUSED"
assert any(f["name"] == "all_skipped" for f in r["failures"])
def test_15g_missing_required_artifact_refused_despite_green(tmp_path):
runner = tmp_path / "green.sh"
runner.write_text('#!/bin/bash\necho "# pass 11"\nexit 0\n')
runner.chmod(0o755)
r = verify_test_truth([str(runner)], str(tmp_path),
TestExpectations(
require_tests=True,
required_artifacts=["scheduler.js"]),
base_dir=str(tmp_path))
assert r["verdict"] == "FALSE_GREEN_REFUSED"
assert any(f["name"] == "artifact_missing" for f in r["failures"])
def test_15g_genuine_pass_with_real_tests(tmp_path):
_write_node_project(tmp_path, (
"import test from 'node:test';\n"
"import assert from 'node:assert/strict';\n"
"test('works', () => assert.equal(1+1, 2));\n"
"test('also', () => assert.ok(true));\n"))
(tmp_path / "scheduler.js").write_text("export const x = 1\n")
r = verify_test_truth(
["node", "--test"], str(tmp_path),
TestExpectations(require_tests=True, min_tests=2,
required_artifacts=["scheduler.js"]),
base_dir=str(tmp_path))
assert r["verdict"] == "PASS", r["failures"]
assert r["test_count"] >= 2
def test_15g_expectations_come_from_contract_not_hardcoded(tmp_path):
"""min_tests is contract-derived: a contract may legitimately require
more; the same green output fails a higher requirement."""
runner = tmp_path / "g.sh"
runner.write_text('#!/bin/bash\necho "# pass 3"\nexit 0\n')
runner.chmod(0o755)
ok = verify_test_truth([str(runner)], str(tmp_path),
TestExpectations(min_tests=3), base_dir=str(tmp_path))
strict = verify_test_truth([str(runner)], str(tmp_path),
TestExpectations(min_tests=11), base_dir=str(tmp_path))
assert ok["verdict"] == "PASS"
assert strict["verdict"] == "FALSE_GREEN_REFUSED"
# ── 15H ─────────────────────────────────────────────────────────────────────
def test_15h_henxels_required_at_structural_boundary():
r = henxels_placement(depth="STANDARD", structural_boundary=True,
stage_id="s-structural")
assert r["henxels"] == "HENXELS_REQUIRED"
assert r["rationale"]
def test_15h_henxels_required_for_full_depth():
r = henxels_placement(depth="FULL", structural_boundary=False,
stage_id="s-full")
assert r["henxels"] == "HENXELS_REQUIRED"
def test_15h_henxels_not_applicable_for_tiny_change():
r = henxels_placement(depth="LIGHT", structural_boundary=False,
stage_id="s-typo")
assert r["henxels"] == "HENXELS_NOT_APPLICABLE"
assert r["rationale"] # NOT_APPLICABLE is a recorded decision too
# ── 15I ─────────────────────────────────────────────────────────────────────
def test_15i_command_green_but_effect_absent_blocks_next_stage(tmp_path):
"""The classic lie: subprocess exits 0, durable effect absent.
Reconciliation computes the delta; the next stage must not start."""
desired = {"deployed_marker": str(tmp_path / "deployed.txt")}
# "action" that exits 0 but does nothing durable:
r = subprocess.run(["bash", "-c", "exit 0"], capture_output=True)
assert r.returncode == 0 # the command "succeeded"
delta = reconcile({"deployed_marker": "present"},
lambda: {"deployed_marker": "absent"},
stage_id="deploy")
assert delta["reconciled"] is False
assert delta["delta"]["deployed_marker"]["desired"] == "present"
assert delta["delta"]["deployed_marker"]["actual"] == "absent"
def test_15i_reconciled_zero_delta_continues(tmp_path):
actual = {"phase": "closed", "scope": ["a.js"]}
r = reconcile({"phase": "closed", "scope": ["a.js"]}, lambda: actual,
stage_id="close")
assert r["reconciled"] is True
assert r["delta"] == {}
+176
View File
@@ -0,0 +1,176 @@
"""test_substrate_seam.py — T16 §16I/§16J/§16K: future-Substrate
compatibility seam held-outs.
The MOCK client below knows NOTHING about current.json or internal
entity arrays — it sees only the public projection (construction_
projection) and the inbound command seam (construction_commands). It:
1. requests current construction state through the public seam;
2. observes a construction event (a projection whose waiting_state
advanced — simulated by a durable state change, not by poking
internals);
3. correlates project/contract/stage;
4. issues RESUME (valid) and receives acceptance;
5. issues an invalid transition (START on a non-draft) and receives
REFUSAL;
6. survives process restart using identities (re-request state by
contract id alone — no transcript, no cached internals);
7. leaves Scaffolding fully usable without the client existing.
NOT implemented (explicitly out of scope): relevance, autonomous
prioritisation, goal planning, memory, Substrate cognition. This is a
compatibility seam, not a Substrate.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import construction_commands as cmds # noqa: E402
from rune import construction_projection as proj # noqa: E402
from rune import scaffold as sc # noqa: E402
class MockSubstrateClient:
"""TEST CLIENT ONLY. Sees the projection + command seam; the store
object is passed to the SEAM functions, never touched directly."""
def __init__(self, scaffold):
self._scaffold = scaffold
self.observed: list[dict] = []
def request_state(self, contract_id: str, **kw) -> dict:
p = proj.project(self._scaffold, contract_id, **kw)
assert not proj.validate(p), f"invalid projection: {proj.validate(p)}"
self.observed.append(p)
return p
def issue(self, command_type: str, construction_id: str, **kw) -> dict:
# the serving layer binds the canonical store to the seam; the
# client only ever sees the returned envelope
return cmds.command(command_type, construction_id=construction_id,
scaffold=self._scaffold, **kw)
def _fixture_scaffold(tmp_path):
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "substrate-seam")
doc = s.init(purpose="substrate seam held-out")
s.new_contract(doc, contract_id="seam-1", need="a thing to build")
s.save(doc)
return s
def test_16k_state_through_public_seam_only(tmp_path):
s = _fixture_scaffold(tmp_path)
client = MockSubstrateClient(s)
p = client.request_state("seam-1", originating_goal_id="goal-9",
stage="BUILD")
assert p["schema_version"] == proj.SCHEMA_VERSION
assert p["contract_id"] == "seam-1"
assert p["phase"] == "draft"
assert p["candidate_next_action"] # Scaffolding tells, client reads
# the projection exposes NO storage internals
assert "entities" not in p and "current.json" not in str(p)
def test_16k_observe_event_correlate_and_resume(tmp_path):
s = _fixture_scaffold(tmp_path)
client = MockSubstrateClient(s)
before = client.request_state("seam-1", stage="BUILD")
# a construction EVENT: durable state advanced (transition to
# approved — done through the real lifecycle, not internals)
doc = s.load()
s.transition_contract(doc, "seam-1", "approved")
s.save(doc)
after = client.request_state("seam-1", stage="BUILD")
assert before["phase"] == "draft" and after["phase"] == "approved"
# correlate project/contract/stage, then issue the valid command
assert after["project_id"] == "substrate-seam"
resp = client.issue("RESUME", construction_id="seam-1",
correlation_id="corr-1")
assert resp["accepted"] is True and resp["op"] == "resume"
def test_16k_invalid_transition_refused(tmp_path):
s = _fixture_scaffold(tmp_path)
client = MockSubstrateClient(s)
doc = s.load()
s.transition_contract(doc, "seam-1", "approved")
s.save(doc)
# START on a non-draft is invalid — Scaffolding decides, Substrate asks
resp = client.issue("START", construction_id="seam-1")
assert resp["accepted"] is False
assert "draft" in resp["reason"]
def test_16k_direct_store_semantics_impossible(tmp_path):
s = _fixture_scaffold(tmp_path)
client = MockSubstrateClient(s)
# the client has no path to the store: it never received the store
# dir, only the scaffold object for seam calls. Attempting to mutate
# storage is not expressible through the seam — the closest it gets
# is PROVIDE_CONTEXT, which is validated and user-provenance-gated.
resp = client.issue("PROVIDE_CONTEXT", construction_id="seam-1",
payload={"add_paths": ["src/x.py"]})
assert resp["accepted"] is True
assert resp["args"]["authority"] == "substrate" # never user-forged
def test_16k_survives_restart_via_identities(tmp_path):
s = _fixture_scaffold(tmp_path)
# "restart": drop the client AND the scaffold; rebuild from identities
contract_id, project_id = "seam-1", "substrate-seam"
del s
s2 = sc.Scaffold(tmp_path / ".rune" / "scaffold", project_id)
client2 = MockSubstrateClient(s2)
p = client2.request_state(contract_id) # contract id alone — no transcript
assert p["phase"] == "draft" and p["project_id"] == project_id
def test_16k_scaffolding_usable_without_client(tmp_path):
# the client never existed here: the full lifecycle still works
s = _fixture_scaffold(tmp_path)
doc = s.load()
s.transition_contract(doc, "seam-1", "approved")
s.transition_contract(doc, "seam-1", "building")
s.save(doc)
doc2 = s.load()
c = next(e for e in doc2["entities"] if e["id"] == "contract:seam-1")
assert c["phase"] == "building"
def test_16k_idempotent_command_replay(tmp_path):
s = _fixture_scaffold(tmp_path)
client = MockSubstrateClient(s)
first = client.issue("RESUME", construction_id="seam-1",
command_id="replay-1")
second = client.issue("RESUME", construction_id="seam-1",
command_id="replay-1")
assert first["accepted"] and second["accepted"]
assert second["idempotent_replay"] is True
def test_16k_unknown_command_and_unknown_construction_refused(tmp_path):
s = _fixture_scaffold(tmp_path)
client = MockSubstrateClient(s)
assert client.issue("PAUSE", construction_id="seam-1")["accepted"] is False
assert client.issue("RESUME", construction_id="nope")["accepted"] is False
assert client.issue("RESUME", construction_id="")["accepted"] is False
def test_16k_projection_schema_validated(tmp_path):
s = _fixture_scaffold(tmp_path)
client = MockSubstrateClient(s)
p = client.request_state("seam-1")
assert proj.validate(p) == []
bad = dict(p)
del bad["phase"]
assert proj.validate(bad) != []
bad2 = dict(p, schema_version=999)
assert proj.validate(bad2) != []
+267
View File
@@ -249,3 +249,270 @@ def test_14f_construction_kernel_shared_rune_internal_vs_external(tmp_path):
# ALLOWED difference: authority identity
assert ext.scaffold.project_id == "t14-hostile"
assert rune_iface.scaffold.project_id == "rune"
# ── 15O: residue provenance hardening ───────────────────────────────────────
def test_15o_residue_lookalike_malicious_write_is_caught(tmp_path):
"""§15O hostile: a builder deliberately writes an exempt-looking path
(.mimocode/evil, .git/config). Provenance — NOT the prefix — decides:
the path appeared mid-session and was never declared by the runtime,
so it is a SCOPE VIOLATION, not residue."""
sys.path.insert(0, str(ROOT / "scripts"))
import t14_builder_session as t14
proj = tmp_path / "proj"
proj.mkdir()
# runtime bootstrap BEFORE builder: .mimocode session dir exists
(proj / ".mimocode").mkdir()
(proj / ".mimocode" / "session.json").write_text("{}")
before = t14.snapshot_files(proj)
# builder writes its legit file AND a residue-lookalike malicious one
(proj / "scheduler.js").write_text("ok")
(proj / ".mimocode" / "evil").write_text("payload")
(proj / ".git" / "config").parent.mkdir()
(proj / ".git" / "config").write_text("tampered")
audit = t14.audit_scope(proj, before, ["scheduler.js"])
assert set(audit["scope_violations"]) == {".mimocode/evil", ".git/config"}
assert not audit["in_scope"]
assert audit["runtime_residue"] == [".mimocode/session.json"] or \
".mimocode/session.json" not in audit["scope_violations"]
def test_15o_preexisting_residue_stays_exempt(tmp_path):
"""Residue with proven provenance (existed before the builder ran, or
explicitly runtime-declared) remains exempt and is recorded."""
sys.path.insert(0, str(ROOT / "scripts"))
import t14_builder_session as t14
proj = tmp_path / "proj"
(proj / ".mimocode").mkdir(parents=True)
(proj / ".mimocode" / "session.json").write_text("{}")
before = t14.snapshot_files(proj)
(proj / "scheduler.js").write_text("ok")
(proj / ".mimocode" / "new-runtime-file").write_text("x")
audit = t14.audit_scope(
proj, before, ["scheduler.js"],
runtime_declared={".mimocode/new-runtime-file"})
assert audit["in_scope"]
assert ".mimocode/new-runtime-file" in audit["runtime_residue"]
assert audit["scope_violations"] == []
# ── 15P: null-action / idempotency proofs ───────────────────────────────────
def test_15p_project_build_bootstrap_twice_no_duplicate(tmp_path):
"""15P: PROJECT_BUILD bootstrap executed twice must be a
NOOP_ALREADY_SATISFIED-equivalent — one project identity, not two."""
from rune import construction_activation as ca
from rune import construction_mode as cm
store = tmp_path / "scaffold"
s = sc.Scaffold(str(store), "rune")
s.init(purpose="15p probe")
d1 = cm.classify_mode(
"create a standalone practice scheduler with create/list/complete",
target_project=None, project_exists=False,
affected_surface=["scheduler.js"])
from rune.construction_decision import Need
need = Need(need_id="n1", objective="create a standalone practice "
"scheduler with create/list/complete", origin="test",
project="scheduler", constraints={}, affected_surface=[],
evidence_refs=[], approval_context="15p")
r1 = ca.activate(d1, need=need, scaffold=s, builder="probe")
r2 = ca.activate(d1, need=need, scaffold=s, builder="probe")
assert r1["mode"] == "PROJECT_BUILD" and r2["mode"] == "PROJECT_BUILD"
# exactly ONE project scaffold — no duplicate identity
projects = sorted((store / "projects").glob("*/current.json"))
assert len(projects) == 1, f"bootstrap duplicated: {projects}"
def test_15p_event_replay_after_restart_is_idempotent():
"""15P + 15E: replaying a consumed operation event after a controller
restart must be a no-op — the durable waitstore verdict stands."""
from rune import event_wait as ew
# (full daemon-backed proof lives in test_event_wait.py
# test_15e_restart_after_event_persisted_waitstore_wins; this is the
# semantic contract: resume_or_recover is a pure readback)
# the function never mutates state — verify by contract inspection
import inspect
src = inspect.getsource(ew.resume_or_recover)
assert "save(" not in src and "transition(" not in src, \
"recovery must be readback-only (idempotent by construction)"
# ── 15S: breaking-change / migration scenario ───────────────────────────────
def test_15s_breaking_change_preserves_old_path_until_migrated(tmp_path):
"""15S: existing project → breaking change → migration requirement →
FULL depth. BUILD NEW does not destroy the old path before migration
evidence exists; canaries block supersession until consumers migrate."""
from rune import construction_activation as ca
from rune import construction_mode as cm
store = tmp_path / "scaffold"
s = sc.Scaffold(str(store), "rune")
s.init(purpose="15s probe")
doc = s.load()
# existing project with a consumer of the old API
ss.add_entity(doc, id="module:v1_api", kind="module",
identities={"api": "get_items()"}, maturity="WIRED",
evidence_refs=["e"])
ss.add_entity(doc, id="consumer:dashboard", kind="capability",
identities={"consumes": "module:v1_api"},
maturity="WIRED", evidence_refs=["e"])
ss.add_relationship(doc, type="consumes", from_id="consumer:dashboard",
to_id="module:v1_api", evidence_refs=["e"])
ss.add_entity(doc, id="contract:api-spec", kind="contract",
identities={"objective": "api spec"}, evidence_refs=["e"])
ss.add_entity(doc, id="capability:api-v2", kind="capability",
maturity="DECLARED", evidence_refs=["e"])
ss.add_relationship(doc, type="implements",
from_id="capability:api-v2",
to_id="contract:api-spec", evidence_refs=["e"])
s.commit(doc, what="seed 15s existing project")
iface = ci.ConstructionInterface(
builder="probe", project_store=str(store), project_id="rune")
sub = iface.submit_need(
"replace get_items() with get_items_paged() and migrate the "
"dashboard consumer",
constraints={"environment": {"os": "linux"},
"contracts": ["contract:api-spec"]},
affected_surface=["api.py", "dashboard.py"],
evidence_refs=["e"], approval_context="15s")
assert sub["success"], sub["errors"]
cid = sub["ids"]["contract_id"]
doc = iface.scaffold.load()
c = iface.scaffold.contract(doc, cid)
# the old path is IDENTIFIED as an affected entity — governance sees
# the consumer before any change happens
assert c is not None
# transition to verifying, then migrating: supersession gated on
# migration evidence — a close from verifying WITHOUT the migrating
# phase must still be possible only with verification evidence;
# the phase map enforces migrating before superseding for
# migration-carrying contracts
assert iface.transition(cid, "approved")["success"]
assert iface.transition(cid, "building")["success"]
assert iface.transition(cid, "verifying")["success"]
# BOTH behaviors must be proven: old consumer still works AND new
# API exists — the contract carries migration obligations
mig = iface.transition(cid, "migrating", evidence_ref="consumer migration")
assert mig["success"], mig.get("errors")
sup = iface.transition(cid, "superseding", evidence_ref="old path retired")
assert sup["success"], sup.get("errors")
# the OLD path record survives in the governed store (history, not
# deletion): supersession is a lifecycle state, not erasure
doc = iface.scaffold.load()
assert ss.entity(doc, "module:v1_api") is not None
assert ss.entity(doc, "consumer:dashboard") is not None
# ── 15T: failure / recovery matrix ──────────────────────────────────────────
def test_15t_failure_classification_matrix():
"""15T: every failure class classifies deterministically, with an
explicit retry budget — and PERMANENT/APPROVAL never auto-retry."""
from rune import failure_recovery as fr
cases = [
("connection reset by peer", fr.TRANSIENT, 2),
("rate limit 429 exceeded", fr.TRANSIENT, 2),
("test is flaky, occasionally fails", fr.INTERMITTENT, 3),
("schema-invalid observation payload", fr.PERMANENT, 0),
("module not found: foo", fr.PERMANENT, 0),
("user confirmation required to proceed", fr.APPROVAL_REQUIRED, 0),
("destructive external change", fr.APPROVAL_REQUIRED, 0),
("stale document committed over persisted state", fr.KNOWN_PROCESS_DEFECT, 1),
("totally novel failure mode", fr.UNKNOWN, 1),
]
for text, expected_class, expected_budget in cases:
r = fr.classify_failure(text)
assert r["class"] == expected_class, f"{text!r} → {r['class']}"
max_retries, action = r["policy"]
assert max_retries == expected_budget, text
# no infinite loops: every policy has an explicit budget
for cls in fr.CLASSES:
budget, _ = fr.POLICY[cls]
assert isinstance(budget, int) and budget <= 3
# PERMANENT and APPROVAL never retry
assert fr.POLICY[fr.PERMANENT][0] == 0
assert fr.POLICY[fr.APPROVAL_REQUIRED][0] == 0
def test_15t_known_defect_context_wins_over_markers():
from rune import failure_recovery as fr
r = fr.classify_failure("timeout during deploy",
context={"known_defect_id": "process-defect:x"})
assert r["class"] == fr.KNOWN_PROCESS_DEFECT
assert "process-defect:x" in r["evidence"]
# ── 15Y: adversarial default-mechanism matrix (remaining gaps) ──────────────
def test_15y_event_for_different_project_matches_nothing():
"""15Y case 8: an event for a different project cannot mutate the
target wait — correlation is (provider, event_type, run_ref)."""
import sys
sys.path.insert(0, str(ROOT / "src"))
from rune import event_wait as ew
import tempfile
from pathlib import Path
tmp = Path(tempfile.mkdtemp())
import os
os.environ["RUNE_WR_ROOT"] = str(tmp)
os.environ["RUNE_WR_CODE"] = "/home/nick/.local/share/mimo-wait-resume"
ew.register_process_wait("op-target", project="project-a",
watchdog_secs=60)
ew.register_process_wait("op-other", project="project-b",
watchdog_secs=60)
import importlib
sys.path.insert(0, "/home/nick/.local/share/mimo-wait-resume")
import waitstore
waitstore = importlib.reload(waitstore)
# the target's correlation is op-target; an event for op-other has a
# different run_ref → match() rejects it for the target's wait
target = None
for rec in waitstore.all_waits():
if rec.get("ext", {}).get("operation_id") == "op-target":
target = rec
assert target is not None
foreign_event = {"provider": "builder", "event_type": "builder_exit",
"run_ref": "op-other", "exit_code": 0}
assert waitstore.match(target, foreign_event) is False
own_event = {"provider": "builder", "event_type": "builder_exit",
"run_ref": "op-target", "exit_code": 0}
assert waitstore.match(target, own_event) is True
def test_15y_user_approval_gate_blocks_consequential_action():
"""15Y case 22: a consequential scope change without user authority
is refused — amend_scope is user-gated, not builder-gated."""
from rune import construction_interface as ci
import tempfile
from pathlib import Path
tmp = Path(tempfile.mkdtemp())
iface = ci.ConstructionInterface(builder="rogue-builder",
project_store=str(tmp / "s"),
project_id="t15y")
iface.scaffold.init(purpose="15y probe")
doc = iface.scaffold.load()
from rune import structural_store as ss
ss.add_entity(doc, id="contract:spec-15y", kind="contract",
identities={"objective": "spec"}, evidence_refs=["e"])
ss.add_entity(doc, id="capability:planned", kind="capability",
maturity="DECLARED", evidence_refs=["e"])
ss.add_relationship(doc, type="implements",
from_id="capability:planned",
to_id="contract:spec-15y", evidence_refs=["e"])
iface.scaffold.commit(doc, what="seed 15y spec")
sub = iface.submit_need(
"build the 15y module",
constraints={"environment": {"os": "linux"},
"contracts": ["contract:spec-15y"]},
evidence_refs=["e"], approval_context="15y")
assert sub["success"]
cid = sub["ids"]["contract_id"]
# builder attempts to broaden its own scope — refused
r = iface.amend_scope(cid, ["extra/secret.py"], reason="builder wants",
authority="builder")
assert not r["success"]
# user authority works
r2 = iface.amend_scope(cid, ["extra/needed.py"], reason="user asked",
authority="user")
assert r2["success"], r2.get("errors")