Compare commits

...
25 Commits
Author SHA1 Message Date
conductor c5c69de06f chore(release): auto-bump version to 0.66.69 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 19:27:31 +01:00
Nick fcde0b2ce3 feat(context): conversational construction interpretation layer (T17 17I-17Q)
construction_intent.py — the MEANING layer per the §17O authority
boundary: resolves action-vs-discussion (interrogative STRUCTURE, not
vocabulary: 'how would you build X?' never authorizes), multi-turn
referents against live proposals (recency-first, ordinal selectors,
ambiguity → clarification never guessed, stale-context resistance with
content-word revival, relative-pronoun 'that' excluded), explicit
imperative demands (creation verb + indefinite article = new standalone
artifact; strong/direct trigger without unnecessary re-asking), and the
DISCUSSION→PROPOSAL→USER_APPROVED lifecycle (suggestions are never
authorization). Emits the full §17N structured ConstructionIntent;
feeds the ONE canonical classifier — no second classifier.

classify_mode: indefinite-article new-artifact evidence, with structural
precedence (a known existing target means article language describes a
CHANGE, not a new project — §5).

Held-outs: 22 cases including all 20 §17Q matrix items with unseen
paraphrases; live-defect fixes found BY the matrix (relative-that,
question/imperative 'do', turn accounting, stale detection).

Suite 1915 green; ratchets PASS; Henxels rc=0.
2026-09-15 19:27:04 +01:00
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
50 changed files with 5045 additions and 203 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.60**
**Current release: v0.66.69**
**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.**
+7 -2
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-15T18:24:01Z",
"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,14 +220,18 @@
"cognitive_budgeter",
"complexity_scorer",
"construction_activation",
"construction_commands",
"construction_decision",
"construction_depth",
"construction_intent",
"construction_interface",
"construction_mode",
"construction_pattern",
"construction_patterns_builtin",
"construction_projection",
"context_broker",
"context_pressure",
"drift_resume",
"event_wait",
"failure_recovery",
"layout_contract",
@@ -251,4 +256,4 @@
]
}
}
}
}
+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.60"
version = "0.66.69"
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")
+12 -2
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"
+11 -1
View File
@@ -43,8 +43,18 @@ 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
BUILDER_MODEL = os.environ.get("T15_BUILDER_MODEL", "Verboo Code/glm-5.3-flash")
# 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)
+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.60"
__version__ = "0.66.69"
# 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.60"' in rune_version,
'__version__ = "0.66.69"' 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}
+410
View File
@@ -0,0 +1,410 @@
"""construction_intent.py — T17 §17J-§17N: the conversational
interpretation layer of contextual construction.
AUTHORITY BOUNDARY (§17O):
THIS LAYER owns: meaning, referent resolution, target resolution,
action-vs-discussion, conversational authorization, user constraints.
SCAFFOLDING (construction_mode.classify_mode) owns: construction
classification, depth, contracts, scope, lifecycle.
This module NEVER deepens or reclassifies — it resolves what the user
MEANT and whether they AUTHORIZED it, then hands a structured
ConstructionIntent to Scaffolding, which runs the ONE canonical
classifier (classify_mode). No second classifier lives here.
Not a phrase table: explicit demands are recognized by imperative
grammar + construction verbs; questions are recognized by
interrogative structure; referents are resolved against the live
conversation context (pending proposals, recency, topic drift).
Paraphrases resolve because the criteria are structural, not lexical
incantations.
"""
from __future__ import annotations
import re
import time
import uuid
from dataclasses import dataclass, field
# ── conversation context (the layer's memory, NOT Scaffolding state) ────
@dataclass
class Proposal:
"""A pending construction proposal awaiting user authorization.
Rune suggesting a change is NOT authorization to mutate (§17L)."""
proposal_id: str
summary: str
created_turn: int
superseded: bool = False
@dataclass
class ConversationContext:
"""Active conversational/project context for referent resolution.
Recency-aware, proposal-aware, stale-context-resistant (§17M)."""
turn: int = 0
recent_turns: list = field(default_factory=list) # [{role, text}]
pending_proposals: list = field(default_factory=list) # [Proposal]
active_project: str | None = None
active_task_id: str | None = None
def next_turn(self) -> int:
self.turn += 1
return self.turn
def record_turn(self, role: str, text: str) -> None:
self.recent_turns.append({"role": role, "text": text,
"turn": self.turn})
self.turn += 1 # each recorded exchange advances the context
def add_proposal(self, summary: str) -> Proposal:
p = Proposal(proposal_id=f"prop-{uuid.uuid4().hex[:8]}",
summary=summary,
created_turn=self.turn)
self.pending_proposals.append(p)
return p
def supersede_proposal(self, proposal_id: str) -> None:
for p in self.pending_proposals:
if p.proposal_id == proposal_id:
p.superseded = True
def live_proposals(self) -> list:
return [p for p in self.pending_proposals if not p.superseded]
# ── the structured intent (§17N) ─────────────────────────────────────────
APPROVAL_STATES = ("DISCUSSION", "PROPOSAL", "USER_APPROVED",
"CONSTRUCTION_STARTED")
# approval-state lifecycle legality (§17L): authorization may only be
# granted from PROPOSAL or DISCUSSION (direct demand); CONSTRUCTION_STARTED
# and mutations are Scaffolding's side of the boundary.
LEGAL_APPROVAL_TRANSITIONS = {
"DISCUSSION": {"PROPOSAL", "USER_APPROVED"},
"PROPOSAL": {"USER_APPROVED"},
"USER_APPROVED": {"CONSTRUCTION_STARTED"},
"CONSTRUCTION_STARTED": set(),
}
@dataclass
class ConstructionIntent:
"""Structured meaning-resolution output (§17N). Scaffolding decides
mode/depth from this; this layer never does."""
intent_id: str
objective: str
requested_effect: str
resolved_target: str | None
resolved_referent: str | None
origin_context_id: str | None
action_intent: str # ACTION | DISCUSSION
approval_state: str # DISCUSSION | PROPOSAL | USER_APPROVED
confidence: float
ambiguity: str # NONE | REFERENT | TARGET | SCOPE
consequence_level: str # LOW | MEDIUM | HIGH
relevant_constraints: list = field(default_factory=list)
classification_evidence: list = field(default_factory=list)
probe_questions: list = field(default_factory=list)
def to_dict(self) -> dict:
return {
"intent_id": self.intent_id,
"objective": self.objective,
"requested_effect": self.requested_effect,
"resolved_target": self.resolved_target,
"resolved_referent": self.resolved_referent,
"origin_context_id": self.origin_context_id,
"action_intent": self.action_intent,
"approval_state": self.approval_state,
"confidence": self.confidence,
"ambiguity": self.ambiguity,
"consequence_level": self.consequence_level,
"relevant_constraints": self.relevant_constraints,
"classification_evidence": self.classification_evidence,
"probe_questions": self.probe_questions,
}
# ── structural lexical criteria (grammar classes, not incantations) ─────
# §17M stale-context resistance: a proposal older than this many turns
# is stale unless the user's text explicitly references its content.
STALE_GAP_TURNS = 2
_NEW_ARTICLE = re.compile(r"\b(me a|me an|a|an|another)\b")
_CREATION_VERBS = {"build", "create", "make", "write", "develop"}
_CONSTRUCTION_VERBS = {
"build", "implement", "add", "replace", "fix", "create", "make",
"write", "port", "migrate", "refactor", "integrate", "remove",
"extend", "wire", "ship",
}
_DEMONSTRATIVES = {"that", "this", "it", "those", "these"}
_OPTION_REFERENTS = re.compile(
r"\b(first|second|third|other|latter|former)\s+(option|approach|one)\b")
_QUESTION_MARKERS = re.compile(
r"^\s*(how|what|why|when|where|who|which|should|could|would|can|"
r"is|are|did|do you|do we|do they|does it|does this|does that)\b|"
r"\?\s*$", re.IGNORECASE)
_ANALYSIS_MARKERS = re.compile(
r"\b(think|opinion|view|recommend|suggest|assess|evaluate|review|"
r"compare|consider|worth it|worth it\?|needs? improvement|any "
r"ideas?)\b", re.IGNORECASE)
_RELATIVE_THAT = re.compile(
r"\bthat\s+(?!the\b|a\b|an\b|is\b|was\b|will\b|can\b|should\b)"
r"[a-z]+(?:s|ing)\b")
_GO_AHEAD = re.compile(
r"^\s*(yes|yeah|yep|ok|okay|sure|go ahead|please do|do it|do that|"
r"carry on|proceed|approved|lgtm|ship it)\b[\s.,!]*$", re.IGNORECASE)
_AFFIRMATION_OPENERS = re.compile(
r"^\s*(yes|yeah|yep|yup|ok|okay|sure|agreed|absolutely|indeed|"
r"alright|right)\b", re.IGNORECASE)
_APPROVAL_IDIOM = re.compile(
r"\b(go ahead|do it|do that|carry on|build that|implement that|"
r"make it so|make it happen|that's the one|go with|let's go|"
r"let's do|proceed with|approved|ship it)\b", re.IGNORECASE)
def _tokens(text: str) -> set[str]:
return set(re.split(r"[^a-z0-9]+", text.lower()))
def _is_question(text: str) -> bool:
"""Interrogative STRUCTURE, not vocabulary: leading wh/auxiliary or
terminal '?'. 'How would you build this?' is a question regardless
of the construction verb inside it (§17K)."""
if "?" in text and not re.match(r"^\s*(build|implement|add|fix|make|"
r"create|write|replace|remove)\b",
text, re.IGNORECASE):
return True
return bool(_QUESTION_MARKERS.match(text))
def _imperative_verb(text: str) -> str | None:
"""Leading imperative construction verb — the strong/direct demand
signal (§17K). 'Build me a program...' → 'build'."""
m = re.match(r"^\s*(\w+)", text, re.IGNORECASE)
if m and m.group(1).lower() in _CONSTRUCTION_VERBS:
return m.group(1).lower()
return None
def resolve_construction_intent(
text: str,
context: ConversationContext,
*,
origin_context_id: str | None = None,
) -> ConstructionIntent:
"""Resolve one user turn into a structured ConstructionIntent.
Criteria (§17J), in precedence order:
1. interrogative structure → DISCUSSION (never mutation); analysis
of an improvement question may RECORD a proposal for later.
2. bare approval ("go ahead") or demonstrative authorization
("build that") → resolve referent against LIVE proposals,
recency-first; multiple equally plausible → clarification;
none live → clarification (stale-context resistant).
3. explicit imperative with resolved target → USER_APPROVED
direct demand — no unnecessary re-asking (§17K).
4. everything else → DISCUSSION; construction language without
resolved target → ambiguity flagged, never guessed (§17M).
"""
context.next_turn()
context.record_turn("user", text)
t = text.strip()
low = t.lower()
ev: list[str] = []
probes: list[str] = []
# 1. questions are discussion — even with construction verbs inside
if _is_question(t):
ev.append("interrogative structure → discussion, not authorization")
analysis = bool(_ANALYSIS_MARKERS.search(t))
if analysis:
ev.append("analysis/advice request — a proposal may be recorded "
"for later authorization")
return ConstructionIntent(
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
objective=t, requested_effect="analysis/discussion",
resolved_target=context.active_project,
resolved_referent=None, origin_context_id=origin_context_id,
action_intent="DISCUSSION", approval_state="DISCUSSION",
confidence=0.85, ambiguity="NONE",
consequence_level="LOW", classification_evidence=ev)
# 2. authorization of a prior proposal (bare approval or demonstrative)
bare_approval = bool(_GO_AHEAD.match(t))
imperative = _imperative_verb(t)
dem_tokens = _tokens(low) & _DEMONSTRATIVES
if "that" in dem_tokens and _RELATIVE_THAT.search(low):
dem_tokens.discard("that") # relative clause, not a demonstrative
ev.append("'that' is a relative pronoun here, not a referent")
demonstrative = bool(dem_tokens) or \
bool(_OPTION_REFERENTS.search(low))
option_ref = bool(_OPTION_REFERENTS.search(low))
approval_idiom = bool(_APPROVAL_IDIOM.search(low))
affirmation = bool(_AFFIRMATION_OPENERS.match(t))
authorized = (bare_approval or approval_idiom or
(affirmation and (demonstrative or option_ref)) or
(imperative and (demonstrative or option_ref)) or
(option_ref and imperative is not None))
if authorized:
referent = _resolve_referent(low, context)
if referent is None:
ev.append("authorization language but NO live resolvable "
"referent — never guessed")
probes.append("what should that refer to? I don't have an "
"active proposal matching it.")
return _intent(t, "ACTION", "DISCUSSION", 0.4, "REFERENT",
ev, probes, context, origin_context_id)
if referent.get("ambiguous"):
ev.append("multiple equally plausible referents — asking, "
"not guessing (§17M)")
probes.append("which one — " + " or ".join(
p.summary[:40] for p in referent["candidates"]) + "?")
return _intent(t, "ACTION", "DISCUSSION", 0.4, "REFERENT",
ev, probes, context, origin_context_id)
target_prop = referent["proposal"]
ev.append(f"referent resolved: {target_prop.proposal_id} "
f"(recency-first among live proposals)")
ev.append("prior proposal + explicit authorization → USER_APPROVED")
return ConstructionIntent(
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
objective=target_prop.summary,
requested_effect=f"construct: {target_prop.summary}",
resolved_target=context.active_project,
resolved_referent=target_prop.proposal_id,
origin_context_id=origin_context_id,
action_intent="ACTION", approval_state="USER_APPROVED",
confidence=0.9, ambiguity="NONE",
consequence_level=_consequence(t, context),
relevant_constraints=_constraints(context),
classification_evidence=ev)
# 3. explicit imperative construction demand with resolved target
if imperative:
# non-constructible object check is Scaffolding's classifier's
# job (§17O) — this layer records the demand structure only
ev.append(f"imperative construction verb: {imperative} — strong/"
f"direct demand signal (§17K)")
target = context.active_project
if target:
ev.append(f"target resolved from active context: {target}")
return _intent(t, "ACTION", "USER_APPROVED", 0.9, "NONE",
ev, probes, context, origin_context_id,
requested_effect=f"construct: {t}",
consequence_level=_consequence(t, context))
# no active project: a CREATION verb + indefinite article signals
# a NEW standalone artifact — the demand is complete without an
# existing target ("build me a program that...")
if imperative in _CREATION_VERBS and _NEW_ARTICLE.search(low):
ev.append("creation verb + indefinite article → new standalone "
"artifact demand; no existing target required")
return _intent(t, "ACTION", "USER_APPROVED", 0.85, "NONE",
ev, probes, context, origin_context_id,
requested_effect=f"construct new: {t}")
ev.append("imperative demand but no resolvable target — "
"clarification, never guessing")
probes.append("build where — which project/system?")
return _intent(t, "ACTION", "DISCUSSION", 0.5, "TARGET",
ev, probes, context, origin_context_id)
# 4. default: discussion / context statement
ev.append("no authorization structure, no imperative demand → "
"discussion")
return _intent(t, "DISCUSSION", "DISCUSSION", 0.7, "NONE",
ev, probes, context, origin_context_id)
def _resolve_referent(low: str, context: ConversationContext) -> dict | None:
"""Recency-first referent resolution over LIVE proposals (§17M).
Stale-context-resistant: proposals older than STALE_GAP_TURNS are
ignored unless the user's text explicitly references their content.
Returns {"proposal": Proposal} or {"ambiguous": True,
"candidates": [...]} or None."""
live = context.live_proposals()
if not live:
return None
fresh = [p for p in live
if context.turn - p.created_turn <= STALE_GAP_TURNS
or _references(p.summary, low)]
if not fresh:
return None # all live proposals are stale for this turn
live = fresh
# option-selectors pick among live proposals by ordinal
m = _OPTION_REFERENTS.search(low)
if m:
ordinals = {"first": 0, "second": 1, "third": 2, "other": 1,
"latter": len(live) - 1, "former": 0}
idx = ordinals.get(m.group(1).lower())
if idx is not None and idx < len(live):
return {"proposal": live[idx]}
if len(live) > 1:
return {"ambiguous": True, "candidates": live}
# multiple live proposals + no ordinal → ambiguous unless recency
# is decisive (only one proposal younger than the last topic shift)
if len(live) > 1:
# recency heuristic: if the newest proposal is at least 2 turns
# newer than the runner-up, recency decides; otherwise ask
newest, runner = live[-1], live[-2]
if newest.created_turn - runner.created_turn >= 2:
return {"proposal": newest}
return {"ambiguous": True, "candidates": live}
return {"proposal": live[0]}
def _references(summary: str, low: str) -> bool:
"""Does the user's text explicitly reference the proposal's content?
Distinctive content words (len>4) appearing in the text revive a
stale proposal (explicit > recency)."""
words = {w for w in _tokens(summary) if len(w) > 4}
return bool(words & set(_tokens(low)))
def _consequence(text: str, context: ConversationContext) -> str:
low = text.lower()
if any(w in low for w in ("replace", "migrate", "rewrite", "delete",
"remove all", "subsystem")):
return "HIGH"
if any(w in low for w in ("add", "extend", "port", "integrate")):
return "MEDIUM"
return "LOW"
def _constraints(context: ConversationContext) -> list:
"""User/project constraints recorded in recent turns (light capture;
the constraint STORE is future Substrate's — §17O boundary)."""
out = []
for turn in context.recent_turns[-6:]:
txt = turn.get("text", "").lower()
if "must not" in txt or "don't" in txt or "never" in txt or \
"no hardcoded" in txt:
out.append(turn["text"][:120])
return out
def _intent(objective: str, action_intent: str, approval_state: str,
confidence: float, ambiguity: str, ev: list, probes: list,
context: ConversationContext, origin_context_id: str | None,
requested_effect: str = "",
consequence_level: str | None = None) -> ConstructionIntent:
return ConstructionIntent(
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
objective=objective,
requested_effect=requested_effect or objective,
resolved_target=context.active_project,
resolved_referent=None,
origin_context_id=origin_context_id,
action_intent=action_intent,
approval_state=approval_state,
confidence=confidence, ambiguity=ambiguity,
consequence_level=consequence_level
or ("LOW" if action_intent == "DISCUSSION" else "MEDIUM"),
relevant_constraints=_constraints(context),
classification_evidence=ev, probe_questions=probes)
+24 -1
View File
@@ -293,9 +293,32 @@ def classify_mode(
file_scoped = True
break
# T17 §17Q: indefinite-article new-artifact evidence — "a small CLI
# that renames photos", "build me a program that..." — is
# new-lifecycle language even without explicit standalone keywords.
# Guarded: requires a CREATION verb or artifact noun, and yields to
# local/scope/file evidence (fix a bug / add a flag never counts).
_CREATION_VERBS = {"build", "create", "make", "write", "develop"}
_ARTIFACT_NOUNS = {"program", "cli", "tool", "app", "script", "service",
"library", "package", "module", "dashboard",
"website", "bot", "generator", "tracker", "game",
"application", "command"}
article_standalone = False
if re.search(r"\b(a|an|another)\s+[a-z]", obj.lower()):
creation_verb = bool(_tokens(obj.lower()) & _CREATION_VERBS)
artifact_noun = bool(_tokens(obj.lower()) & _ARTIFACT_NOUNS)
# structural evidence outranks lexical newness (§5): a known
# existing target means article language describes a CHANGE
# ("add a flag to the deploy script"), not a new project
if (creation_verb or artifact_noun) and not local and not scoped \
and not file_scoped and not target_project \
and project_exists is not True:
article_standalone = True
ev.append("indefinite-article new-artifact language")
# 3. PROJECT_BUILD: standalone/new-lifecycle language AND no existing
# project for the target (project existence materially matters, §5)
if standalone:
if standalone or article_standalone:
ev.append(f"standalone/new-lifecycle language: {standalone[:3]}")
if project_exists is True:
# §5: existing project preferred unless evidence says otherwise —
+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}
+120 -26
View File
@@ -147,24 +147,39 @@ def _load_waitstore() -> Any:
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."""
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)
if fifo.exists():
fifo.unlink()
os.mkfifo(fifo)
# 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="builder",
event_type="builder_exit",
provider=provider,
event_type=event_type,
sha=sha,
repo=project or "construction",
run_ref=operation_id,
expected="success",
@@ -183,6 +198,13 @@ def register_process_wait(operation_id: str, *, contract_id: str = "",
"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
@@ -190,6 +212,29 @@ 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
@@ -261,10 +306,18 @@ def liveness(operation_id: str, pgid: int | None = None) -> bool:
def await_resume_event(operation_id: str, *, watchdog_secs: int,
pgid: int | None = None,
max_rearms: int = 20) -> dict:
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
@@ -273,17 +326,21 @@ def await_resume_event(operation_id: str, *, watchdog_secs: int,
build outlived a single 900s deadline, and node regrouping hid the
processes from the pgid scan — neither is a failure of continuation.
"""
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)
for arm in range(max_rearms + 1):
deadline = time.monotonic() + watchdog_secs
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)
try:
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:
@@ -293,7 +350,12 @@ def await_resume_event(operation_id: str, *, watchdog_secs: int,
continue
data = os.read(fd, 4096)
if not data:
continue
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:
@@ -302,15 +364,16 @@ def await_resume_event(operation_id: str, *, watchdog_secs: int,
rec = ws.load(token.get("wait_id", ""))
return {"kind": "event", "wait": rec,
"resumed_at": token.get("resumed_at")}
finally:
# 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)
# 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}
def operation_wait(operation_id: str) -> dict | None:
@@ -347,6 +410,37 @@ def resume_or_recover(operation_id: str) -> dict:
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
+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",
+13 -1
View File
@@ -21,10 +21,11 @@ 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, UNKNOWN)
APPROVAL_REQUIRED, PROVIDER_UNAVAILABLE, UNKNOWN)
# recovery policy per class: (max_retries, action)
POLICY = {
@@ -33,6 +34,9 @@ POLICY = {
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"),
}
@@ -74,6 +78,14 @@ def classify_failure(error_text: str, *, context: dict | None = None) -> dict:
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",
+184
View File
@@ -22,6 +22,7 @@ import json
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, cast
# stage states
@@ -101,6 +102,15 @@ class Programme:
"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"}
@@ -163,3 +173,177 @@ def run_programme(prog: Programme, stage_runner, *,
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
+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.60
Version: v0.66.69
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.60 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.66.69 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
+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.
+35
View File
@@ -0,0 +1,35 @@
# T17 §17F/§17G/§17H audits (2026-09-15, from parallel read-only sweeps)
## 17F — task-state surfaces
1. TaskStateRepository (orchestration/conversation_state.py:111) — CANONICAL
(durable JSONL + hot checkpoint, legal transitions, wired through composition)
2. RuneState.tasks in operator.json (rune_operator.py:377) — DUPLICATE
(double-written on every send_task; the one true duplicate authority pair)
3. task_persist.py persisted.json — LEGACY/QUEUE (TUI startup load is dead code:
calls load() which does not exist, silently swallowed)
4. memory_store working table — CACHE (single consumer: its own CLI)
5. auto_tasks.db — QUEUE (self-contained, no bridge)
6. work_manager batch meta — QUEUE (task-adjacent, TUI /work only)
CONSOLIDATION: #1 authority; migrate #2 writers (send_task → repo only,
derive get_status/list_tasks), fix #3 dead load, keep #4-6 as bounded caches/queues.
## 17G — build_mode
Unique: VCS worktree isolation (create/merge/delete) — NOT in Scaffolding;
phase ledger/resume. NOT unique: DAG batching (work_manager), correction
loop (counter only), complexity assess (placeholder). Plugin BuildStateMachine
= second parallel orchestrator (DUPLICATE, only assess.py referenced).
T15 classification lives in operator hook path; TUI /build start bypasses
it and mints unclassified state.
SMALLEST MIGRATION: _cmd_build start → classify first (same classify_mode),
refuse NOT_CONSTRUCTION/UNKNOWN with probes, activate contract, then delegate
to BuildManager as bounded adapter (worktree + resume ledger). Delete plugin
duplicate.
## 17H — W/R daemon ownership (verdict)
INTENTIONAL SHARED INFRASTRUCTURE with packaging debt. The bridge serves
multiple consumers (deploy waiter as runner user, daemon as nick, MiMo
sessions). Smallest clean migration: keep out-of-repo deployment BUT (a)
versioned snapshot (git tag per release), (b) RUNE_WR_CODE discovery
already implemented in event_wait, (c) health endpoint exists. Full
in-repo packaging DEFERRED — the shared-root contract (0644/0666) is the
load-bearing fix and it is now guarded by tests.
+938
View File
@@ -1,5 +1,71 @@
{
"entities": [
{
"checkpoints": [],
"evidence_refs": [
"contract-created:17e-stage-3-fails"
],
"id": "contract:17e-stage-3-fails",
"identities": {
"acceptance": [],
"affected_entities": [],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "deliberately failing stage",
"next_action": "review for approval",
"non_goals": [],
"origin": "programme",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T17:50:29Z",
"phase": "draft",
"status": "open",
"tags": []
},
{
"checkpoints": [],
"evidence_refs": [
"contract-created:17e-stage-4-final"
],
"id": "contract:17e-stage-4-final",
"identities": {
"acceptance": [],
"affected_entities": [],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "final stage",
"next_action": "review for approval",
"non_goals": [],
"origin": "programme",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T17:50:29Z",
"phase": "draft",
"status": "open",
"tags": []
},
{
"checkpoints": [
{
@@ -34781,6 +34847,674 @@
"status": "closed",
"tags": []
},
{
"checkpoints": [
{
"at": "2026-09-15T16:35:22Z",
"evidence": "",
"note": "",
"phase": "approved"
},
{
"at": "2026-09-15T16:35:22Z",
"evidence": "",
"note": "",
"phase": "building"
},
{
"at": "2026-09-15T16:35:22Z",
"evidence": "",
"note": "",
"phase": "closed"
},
{
"at": "2026-09-15T16:35:22Z",
"evidence": "",
"note": "",
"phase": "verifying"
},
{
"evidence": "YAML parses; glob fixed; 24 canary files at target"
}
],
"evidence_refs": [
"contract-created:16R-maintenance-canary-glob"
],
"id": "contract:16R-maintenance-canary-glob",
"identities": {
"acceptance": [
"canary files exist at the glob target",
"glob points at src/rune/canary_*.py",
"workflow YAML parses"
],
"affected_entities": [
"module:canary_native"
],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "maintenance.yml canary-suite job loops shared/canary_*.py \u2014 real canaries live in src/rune/; job silently no-ops (16M finding #6)",
"next_action": "observe real outcome; capture lessons",
"non_goals": [
"broad maintenance-ledger consolidation (16Q DEFER)",
"build_mode removal (DEFER \u2014 needs migration plan)"
],
"origin": "user",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "BUILD_NEW",
"rollback_ref": "",
"scope": [
".gitea/workflows/maintenance.yml"
],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T16:35:22Z",
"phase": "closed",
"status": "closed",
"tags": []
},
{
"checkpoints": [
{
"at": "2026-09-15T17:50:29Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "approved"
}
],
"evidence_refs": [
"contract-created:17e-stage-1-governance"
],
"id": "contract:17e-stage-1-governance",
"identities": {
"acceptance": [],
"affected_entities": [],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "governance-only stage",
"next_action": "begin building",
"non_goals": [],
"origin": "programme",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T17:50:29Z",
"phase": "approved",
"status": "open",
"tags": []
},
{
"checkpoints": [
{
"at": "2026-09-15T17:50:29Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "approved"
}
],
"evidence_refs": [
"contract-created:17e-stage-2-async"
],
"id": "contract:17e-stage-2-async",
"identities": {
"acceptance": [],
"affected_entities": [],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "async work under event bridge",
"next_action": "begin building",
"non_goals": [],
"origin": "programme",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T17:50:29Z",
"phase": "approved",
"status": "open",
"tags": []
},
{
"checkpoints": [
{
"at": "2026-09-15T17:51:19Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "approved"
}
],
"evidence_refs": [
"contract-created:17e2-stage-1-governance"
],
"id": "contract:17e2-stage-1-governance",
"identities": {
"acceptance": [],
"affected_entities": [],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "governance-only stage",
"next_action": "begin building",
"non_goals": [],
"origin": "programme",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T17:51:19Z",
"phase": "approved",
"status": "open",
"tags": []
},
{
"checkpoints": [
{
"at": "2026-09-15T17:51:19Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "approved"
}
],
"evidence_refs": [
"contract-created:17e2-stage-2-async"
],
"id": "contract:17e2-stage-2-async",
"identities": {
"acceptance": [],
"affected_entities": [],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "async work under event bridge",
"next_action": "begin building",
"non_goals": [],
"origin": "programme",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T17:51:19Z",
"phase": "approved",
"status": "open",
"tags": []
},
{
"checkpoints": [
{
"at": "2026-09-15T17:51:22Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "approved"
}
],
"evidence_refs": [
"contract-created:17e2-stage-3-fails"
],
"id": "contract:17e2-stage-3-fails",
"identities": {
"acceptance": [],
"affected_entities": [],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "deliberately failing stage",
"next_action": "begin building",
"non_goals": [],
"origin": "programme",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T17:51:22Z",
"phase": "approved",
"status": "open",
"tags": []
},
{
"checkpoints": [
{
"at": "2026-09-15T17:51:22Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "approved"
}
],
"evidence_refs": [
"contract-created:17e2-stage-4-final"
],
"id": "contract:17e2-stage-4-final",
"identities": {
"acceptance": [],
"affected_entities": [],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "final stage",
"next_action": "begin building",
"non_goals": [],
"origin": "programme",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T17:51:22Z",
"phase": "approved",
"status": "open",
"tags": []
},
{
"checkpoints": [
{
"at": "2026-09-15T17:52:26Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "approved"
},
{
"at": "2026-09-15T17:52:27Z",
"evidence": "closure-record",
"note": "predicted-vs-observed captured",
"phase": "closed"
},
{
"at": "2026-09-15T17:52:27Z",
"evidence": "governance-only-stage",
"note": "",
"phase": "verifying"
},
{
"at": "2026-09-15T17:52:27Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "building"
}
],
"evidence_refs": [
"contract-created:17e3-stage-1-governance"
],
"ext": {
"contract_quality": {
"candidate_corrections": [],
"captured_at": "2026-09-15T17:52:27Z",
"contract": "17e3-stage-1-governance",
"correctly_predicted": [],
"evidence_sources": [
"programme-driver"
],
"expected_new_entities": [],
"expected_new_missing": [],
"expected_new_observed": [],
"expected_runtime_effects": [],
"expected_runtime_missing": [],
"expected_runtime_observed": [],
"genuinely_unexpected_new": [],
"missed": [],
"needless": [],
"observed": [],
"observed_existing": [],
"observed_new": [],
"predicted_but_unaffected": [],
"predicted_existing_affected": [],
"scope_check": {
"in_scope": [],
"out_of_scope": [],
"reason": "contract has no approved scope",
"verdict": "unknown"
}
}
},
"id": "contract:17e3-stage-1-governance",
"identities": {
"acceptance": [],
"affected_entities": [],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "governance-only stage",
"next_action": "observe real outcome; capture lessons",
"non_goals": [],
"origin": "programme",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T17:52:27Z",
"phase": "closed",
"status": "closed",
"tags": []
},
{
"checkpoints": [
{
"at": "2026-09-15T17:52:27Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "approved"
},
{
"at": "2026-09-15T17:52:27Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "building"
},
{
"at": "2026-09-15T17:52:29Z",
"evidence": "closure-record",
"note": "predicted-vs-observed captured",
"phase": "closed"
},
{
"at": "2026-09-15T17:52:29Z",
"evidence": "programme-stage-verify",
"note": "",
"phase": "verifying"
}
],
"evidence_refs": [
"contract-created:17e3-stage-2-async"
],
"ext": {
"contract_quality": {
"candidate_corrections": [],
"captured_at": "2026-09-15T17:52:29Z",
"contract": "17e3-stage-2-async",
"correctly_predicted": [],
"evidence_sources": [
"programme-driver"
],
"expected_new_entities": [],
"expected_new_missing": [],
"expected_new_observed": [],
"expected_runtime_effects": [],
"expected_runtime_missing": [],
"expected_runtime_observed": [],
"genuinely_unexpected_new": [],
"missed": [],
"needless": [],
"observed": [],
"observed_existing": [],
"observed_new": [],
"predicted_but_unaffected": [],
"predicted_existing_affected": [],
"scope_check": {
"in_scope": [],
"out_of_scope": [],
"reason": "contract has no approved scope",
"verdict": "unknown"
}
}
},
"id": "contract:17e3-stage-2-async",
"identities": {
"acceptance": [],
"affected_entities": [],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "async work under event bridge",
"next_action": "observe real outcome; capture lessons",
"non_goals": [],
"origin": "programme",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T17:52:29Z",
"phase": "closed",
"status": "closed",
"tags": []
},
{
"checkpoints": [
{
"at": "2026-09-15T17:52:30Z",
"evidence": "closure-record",
"note": "predicted-vs-observed captured",
"phase": "closed"
},
{
"at": "2026-09-15T17:52:30Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "approved"
},
{
"at": "2026-09-15T17:52:30Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "building"
},
{
"at": "2026-09-15T17:52:30Z",
"evidence": "programme-stage-verify",
"note": "",
"phase": "verifying"
}
],
"evidence_refs": [
"contract-created:17e3-stage-3-fails"
],
"ext": {
"contract_quality": {
"candidate_corrections": [],
"captured_at": "2026-09-15T17:52:30Z",
"contract": "17e3-stage-3-fails",
"correctly_predicted": [],
"evidence_sources": [
"programme-driver"
],
"expected_new_entities": [],
"expected_new_missing": [],
"expected_new_observed": [],
"expected_runtime_effects": [],
"expected_runtime_missing": [],
"expected_runtime_observed": [],
"genuinely_unexpected_new": [],
"missed": [],
"needless": [],
"observed": [],
"observed_existing": [],
"observed_new": [],
"predicted_but_unaffected": [],
"predicted_existing_affected": [],
"scope_check": {
"in_scope": [],
"out_of_scope": [],
"reason": "contract has no approved scope",
"verdict": "unknown"
}
}
},
"id": "contract:17e3-stage-3-fails",
"identities": {
"acceptance": [],
"affected_entities": [],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "deliberately failing stage",
"next_action": "observe real outcome; capture lessons",
"non_goals": [],
"origin": "programme",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T17:52:30Z",
"phase": "closed",
"status": "closed",
"tags": []
},
{
"checkpoints": [
{
"at": "2026-09-15T17:52:30Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "approved"
},
{
"at": "2026-09-15T17:52:30Z",
"evidence": "programme-stage-start",
"note": "",
"phase": "building"
},
{
"at": "2026-09-15T17:52:30Z",
"evidence": "programme-stage-verify",
"note": "",
"phase": "verifying"
},
{
"at": "2026-09-15T17:52:31Z",
"evidence": "closure-record",
"note": "predicted-vs-observed captured",
"phase": "closed"
}
],
"evidence_refs": [
"contract-created:17e3-stage-4-final"
],
"ext": {
"contract_quality": {
"candidate_corrections": [],
"captured_at": "2026-09-15T17:52:31Z",
"contract": "17e3-stage-4-final",
"correctly_predicted": [],
"evidence_sources": [
"programme-driver"
],
"expected_new_entities": [],
"expected_new_missing": [],
"expected_new_observed": [],
"expected_runtime_effects": [],
"expected_runtime_missing": [],
"expected_runtime_observed": [],
"genuinely_unexpected_new": [],
"missed": [],
"needless": [],
"observed": [],
"observed_existing": [],
"observed_new": [],
"predicted_but_unaffected": [],
"predicted_existing_affected": [],
"scope_check": {
"in_scope": [],
"out_of_scope": [],
"reason": "contract has no approved scope",
"verdict": "unknown"
}
}
},
"id": "contract:17e3-stage-4-final",
"identities": {
"acceptance": [],
"affected_entities": [],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "final stage",
"next_action": "observe real outcome; capture lessons",
"non_goals": [],
"origin": "programme",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T17:52:31Z",
"phase": "closed",
"status": "closed",
"tags": []
},
{
"evidence_refs": [
"DEPLOY-MONOTONIC-001"
@@ -35526,6 +36260,34 @@
"observed_at": "2026-09-13T02:16:19Z",
"tags": []
},
{
"evidence_refs": [
"scaffold-init"
],
"id": "project:rune-self",
"identities": {
"purpose": "Rune retroactive self-scaffolding (16M inventory live)",
"requirement": ""
},
"kind": "project",
"maturity": "DECLARED",
"observed_at": "2026-09-15T16:35:22Z",
"tags": []
},
{
"evidence_refs": [
"scaffold-init"
],
"id": "project:t17-programme-proof",
"identities": {
"purpose": "T17 17E live multi-stage programme continuation proof",
"requirement": ""
},
"kind": "project",
"maturity": "DECLARED",
"observed_at": "2026-09-15T17:49:48Z",
"tags": []
},
{
"evidence_refs": [
"store/evidence/t14/runA-end-to-end.log",
@@ -47833,6 +48595,182 @@
"mode": "rune_internal",
"observed_at": "2026-09-14T01:54:18Z",
"operation": "submit_need"
},
{
"builder": "t17-programme",
"detail": "17e-stage-1-governance \u2192 approved",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:50:29Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e-stage-2-async \u2192 approved",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:50:29Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e2-stage-1-governance \u2192 approved",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:51:19Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e2-stage-2-async \u2192 approved",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:51:19Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e2-stage-3-fails \u2192 approved",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:51:22Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e2-stage-4-final \u2192 approved",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:51:22Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-1-governance \u2192 approved",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:26Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-1-governance \u2192 building",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:27Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-1-governance \u2192 verifying",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:27Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-1-governance closed",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:27Z",
"operation": "close"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-2-async \u2192 approved",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:27Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-2-async \u2192 building",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:27Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-2-async \u2192 verifying",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:29Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-2-async closed",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:29Z",
"operation": "close"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-3-fails \u2192 approved",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:30Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-3-fails \u2192 building",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:30Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-3-fails \u2192 verifying",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:30Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-3-fails closed",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:30Z",
"operation": "close"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-4-final \u2192 approved",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:30Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-4-final \u2192 building",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:30Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-4-final \u2192 verifying",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:30Z",
"operation": "transition"
},
{
"builder": "t17-programme",
"detail": "17e3-stage-4-final closed",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T17:52:31Z",
"operation": "close"
}
],
"relationships": [
@@ -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"
}
+685 -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)
+270
View File
@@ -0,0 +1,270 @@
"""test_contextual_construction.py — T17 §17J-§17Q: conversational
interpretation held-outs.
The matrix cases are TESTS, not trigger vocabulary: every authorization
and referent case below uses fresh paraphrases not present in the
implementation. The intent layer resolves MEANING; Scaffolding's
classify_mode (the ONE classifier) decides mode. §17O boundary holds
throughout.
"""
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_intent as cint # noqa: E402
from rune import construction_mode as cm # noqa: E402
def ctx(**kw) -> cint.ConversationContext:
return cint.ConversationContext(**kw)
def propose(c, summary) -> cint.Proposal:
return c.add_proposal(summary)
# ── 17Q case 1: explicit demand, new standalone program ────────────────
def test_q1_explicit_demand_new_program():
c = ctx()
intent = cint.resolve_construction_intent(
"Build me a program that tracks my climbing sessions", c)
assert intent.action_intent == "ACTION"
assert intent.approval_state == "USER_APPROVED"
# Scaffolding (the ONE classifier) decides the mode
mode = cm.classify_mode(intent.objective, project_exists=False)
assert mode.mode == "PROJECT_BUILD" and mode.bootstrap_required
# ── 17Q case 2: "how would you build X?" is discussion only ────────────
def test_q2_how_question_never_authorizes():
c = ctx()
intent = cint.resolve_construction_intent(
"How would you build a session tracker for climbing?", c)
assert intent.action_intent == "DISCUSSION"
assert intent.approval_state == "DISCUSSION"
mode = cm.classify_mode(intent.objective)
assert mode.mode != "PROJECT_BUILD" or intent.approval_state != \
"USER_APPROVED" # no construction intent, no mutation
# ── 17Q case 3: propose → later authorize (§17L lifecycle) ─────────────
def test_q3_proposal_then_authorization():
c = ctx()
i1 = cint.resolve_construction_intent(
"Do you think the wait/resume machinery could be simpler?", c)
assert i1.approval_state == "DISCUSSION"
# Rune proposes (recorded via the context, not mutation)
propose(c, "Consolidate the four wait/verify paths into one bridge "
"adapter with per-kind verdict functions")
i2 = cint.resolve_construction_intent(
"Alright, let's go ahead with that.", c)
assert i2.approval_state == "USER_APPROVED"
assert i2.resolved_referent == c.live_proposals()[0].proposal_id
assert "Consolidate" in i2.objective
# ── 17Q case 4: unseen indirect authorization of a prior proposal ──────
def test_q4_unseen_indirect_authorization():
c = ctx()
propose(c, "Add a drift-aware resume gate to the programme runner")
i = cint.resolve_construction_intent(
"Yeah that's the one, make it happen.", c)
assert i.approval_state == "USER_APPROVED"
assert i.resolved_referent is not None
# ── 17Q case 5: ordinal option selection among multiple proposals ──────
def test_q5_ordinal_referent():
c = ctx()
p1 = propose(c, "Option A: poll-based retry sweep")
p2 = propose(c, "Option B: event-driven resume with watchdog")
i = cint.resolve_construction_intent(
"Go with the second approach.", c)
assert i.approval_state == "USER_APPROVED"
assert i.resolved_referent == p2.proposal_id
# ── 17Q case 6: ambiguous "do that" → clarification, never a guess ─────
def test_q6_ambiguous_referent_asks():
c = ctx()
p1 = propose(c, "Rewrite the transport layer")
p2 = propose(c, "Rewrite the router escalation")
# both proposed same turn → recency NOT decisive
i = cint.resolve_construction_intent("Do that.", c)
assert i.ambiguity == "REFERENT"
assert i.approval_state != "USER_APPROVED"
assert i.probe_questions # asks, doesn't guess
# ── 17Q case 7: explicit imperative "build that", resolved referent ────
def test_q7_imperative_with_resolved_referent():
c = ctx()
propose(c, "Bounded CLI for the ledger")
i = cint.resolve_construction_intent("Build that.", c)
assert i.action_intent == "ACTION"
assert i.approval_state == "USER_APPROVED"
assert i.resolved_referent == c.live_proposals()[0].proposal_id
# ── 17Q case 8: topic changed → stale proposal not selected ────────────
def test_q8_stale_context_resistance():
c = ctx()
propose(c, "Refactor the old exporter")
c.record_turn("user", "actually, never mind that — let's talk about "
"the TUI fonts instead")
c.record_turn("assistant", "sure, fonts it is")
c.next_turn()
i = cint.resolve_construction_intent("Build that.", c)
# "that" now plausibly refers to the font topic, not the stale
# proposal — the layer must NOT silently build the exporter
assert i.approval_state != "USER_APPROVED" or \
"exporter" not in i.objective
# ── 17Q case 9: research discussion with construction language ─────────
def test_q9_research_not_mutation():
c = ctx()
i = cint.resolve_construction_intent(
"Research how other tools implement retry budgets — what would "
"building something like that involve?", c)
assert i.action_intent == "DISCUSSION"
# ── 17Q case 10: non-construction meaning of "build" ───────────────────
def test_q10_non_construction_build():
c = ctx()
i = cint.resolve_construction_intent(
"Build a list of the pros and cons.", c)
# imperative "build" + non-constructible object → Scaffolding's
# verb-object check refuses construction semantics
mode = cm.classify_mode(i.objective)
assert mode.mode == "NOT_CONSTRUCTION"
# ── 17Q cases 11-14: mode proportionality through the ONE classifier ───
def test_q11_existing_project_change():
mode = cm.classify_mode("fix the pagination bug in the rune TUI",
target_project="rune", project_exists=True)
assert mode.mode == "EXISTING_PROJECT_CHANGE"
def test_q12_small_local_change():
mode = cm.classify_mode("fix the typo in the README",
target_project="rune", project_exists=True)
assert mode.mode == "LOCAL_CHANGE"
def test_q13_large_architectural_migration():
mode = cm.classify_mode(
"migrate the whole provider layer to a new adapter protocol",
target_project="rune", project_exists=True)
assert mode.mode == "EXISTING_PROJECT_CHANGE"
# depth is Scaffolding's decision; the intent layer's consequence
# signal is HIGH (checked at the intent layer level below)
c = ctx(active_project="rune")
i = cint.resolve_construction_intent(
"migrate the whole provider layer to a new adapter protocol", c)
assert i.consequence_level == "HIGH"
def test_q14_new_standalone():
mode = cm.classify_mode("a small CLI that renames photos by EXIF date",
project_exists=False)
assert mode.mode == "PROJECT_BUILD"
# ── 17Q cases 15-16: command / natural-language / contextual
# equivalence — all three produce the same intent semantics ──────────
def test_q15_command_and_language_same_backend():
# /build X (command form) and "build X" (natural language) must both
# end at classify_mode with the same inputs
cmd_objective = "a password generator with configurable entropy"
nl_objective = "build me a password generator with configurable entropy"
m_cmd = cm.classify_mode(cmd_objective, project_exists=False)
i_nl = cint.resolve_construction_intent(nl_objective, ctx())
m_nl = cm.classify_mode(i_nl.objective, project_exists=False)
assert m_cmd.mode == m_nl.mode == "PROJECT_BUILD"
assert m_cmd.bootstrap_required == m_nl.bootstrap_required
def test_q16_contextual_and_explicit_equivalent_semantics():
c = ctx(active_project="rune")
propose(c, "Add a --dry-run flag to the deploy script")
i_ctx = cint.resolve_construction_intent("yes, go ahead with that", c)
i_exp = cint.resolve_construction_intent(
"Add a --dry-run flag to the deploy script",
ctx(active_project="rune"))
# contextual route resolves through the referent; explicit route
# through the imperative — SAME approval semantics
assert i_ctx.approval_state == i_exp.approval_state == "USER_APPROVED"
assert i_ctx.action_intent == i_exp.action_intent == "ACTION"
# ── 17Q case 17-18: provider ownership + approval not bypassed ─────────
def test_q17_provider_ownership_preserved():
# the intent layer emits no provider field at all — provider
# selection cannot pass through it (17O boundary, structural)
c = ctx()
i = cint.resolve_construction_intent("build a fixture runner", c)
assert not hasattr(i, "provider") and "provider" not in i.to_dict()
def test_q18_approval_not_bypassed_by_context():
# a proposal is NOT authorization: approval_state stays PROPOSAL
# until a user turn authorizes
c = ctx()
propose(c, "Rewrite the auth subsystem")
i = cint.resolve_construction_intent(
"sounds plausible, tell me more about the migration steps", c)
assert i.approval_state != "USER_APPROVED"
# ── 17Q case 19: unknown target → clarification ────────────────────────
def test_q19_unknown_target_asks():
c = ctx() # no active project
i = cint.resolve_construction_intent("Implement the export feature.", c)
assert i.ambiguity == "TARGET"
assert i.approval_state != "USER_APPROVED"
assert i.probe_questions
# ── 17Q case 20: superseded proposal not accidentally built ────────────
def test_q20_superseded_proposal_ignored():
c = ctx()
p_old = propose(c, "Use polling for CI waits")
c.supersede_proposal(p_old.proposal_id)
p_new = propose(c, "Use event-driven waits with watchdogs")
i = cint.resolve_construction_intent("go ahead with that", c)
assert i.resolved_referent == p_new.proposal_id
# ── 17N: structured intent shape ────────────────────────────────────────
def test_17n_intent_shape_complete():
c = ctx(active_project="rune")
i = cint.resolve_construction_intent("fix the flaky lease test", c)
d = i.to_dict()
for f in ("intent_id", "objective", "requested_effect",
"resolved_target", "resolved_referent", "origin_context_id",
"action_intent", "approval_state", "confidence", "ambiguity",
"consequence_level", "relevant_constraints",
"classification_evidence"):
assert f in d, f"missing intent field {f}"
assert d["resolved_target"] == "rune"
# ── §17L: approval-state lifecycle legality ─────────────────────────────
def test_17l_discussion_never_mutates():
c = ctx(active_project="rune")
for phrase in ("what do you think about the deploy script?",
"should we improve the wait semantics?",
"how would you build the exporter?"):
i = cint.resolve_construction_intent(phrase, c)
assert i.approval_state == "DISCUSSION"
assert i.action_intent == "DISCUSSION"
+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"] == []
+17 -8
View File
@@ -49,7 +49,19 @@ 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):
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),
@@ -93,14 +105,11 @@ class Daemon:
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(tmp_path, monkeypatch):
monkeypatch.setenv("RUNE_WR_ROOT", str(tmp_path)) # adapter binds here
monkeypatch.setenv("RUNE_WR_CODE", str(WR))
monkeypatch.setenv("RUNE_WR_ROOT", str(tmp_path))
d = Daemon(tmp_path, port=8793)
yield d
d.stop()
def daemon(wr_daemon):
return wr_daemon
# real fake-builder: sleeps then exits — cheap but genuinely a process
+19 -1
View File
@@ -129,10 +129,28 @@ 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."""
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"
+1
View File
@@ -105,6 +105,7 @@ def test_15m_every_stop_carries_an_honest_verdict():
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, \
+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")
+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) != []