Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94ba2c97a3 | ||
|
|
94a4774853 | ||
|
|
efb2c6b9a1 | ||
|
|
99115889d7 | ||
|
|
2a971578e1 | ||
|
|
ec434f2a70 | ||
|
|
d9c50c66c9 | ||
|
|
a382c15562 | ||
|
|
186fdd1410 | ||
|
|
aa9750c59f | ||
|
|
ab114d260b | ||
|
|
b7865a76bb | ||
|
|
04b0f4ac96 | ||
|
|
9b328c9af1 | ||
|
|
bc0f244934 | ||
|
|
29570f404a | ||
|
|
927548e9a0 | ||
|
|
781b286f5b | ||
|
|
d1ccd7ebec | ||
|
|
3adfa1b1ed | ||
|
|
da4eece9e0 | ||
|
|
2e1f0c8cab | ||
|
|
6e2f5950bd | ||
|
|
c53c82bb35 | ||
|
|
35adcd8cb1 | ||
|
|
810d0027c3 | ||
|
|
4839a62b70 | ||
|
|
15408841ac | ||
|
|
c5c69de06f | ||
|
|
fcde0b2ce3 | ||
|
|
1f96055b69 | ||
|
|
923a21ba0b | ||
|
|
6348386869 | ||
|
|
55002ad838 | ||
|
|
6684289a8f | ||
|
|
e25ad0f964 | ||
|
|
c9dee14c62 | ||
|
|
fe9d154778 | ||
|
|
2a4f9c8367 | ||
|
|
7ddfa7eee7 | ||
|
|
986b450bc2 | ||
|
|
b51afea0cb | ||
|
|
c15840514f | ||
|
|
2833175329 | ||
|
|
4a06d9f11a | ||
|
|
c707a81933 | ||
|
|
37deb6085c | ||
|
|
affe852ff7 | ||
|
|
e1a71efeb9 | ||
|
|
5bd8cca941 | ||
|
|
fd5f31bba2 | ||
|
|
8a2a4f7b71 | ||
|
|
b2d6802dba | ||
|
|
3ebbf9a9b1 | ||
|
|
193d81961d | ||
|
|
94ad81245b | ||
|
|
196cf193df | ||
|
|
f95b2a63df | ||
|
|
708871e511 | ||
|
|
18ef9a53e3 | ||
|
|
05edd68027 | ||
|
|
d166e83d55 | ||
|
|
98b4c58d36 | ||
|
|
f5c8b07d15 | ||
|
|
292c72ec3d | ||
|
|
0a436b2f97 | ||
|
|
2ba4114fce | ||
|
|
eccb446a3c | ||
|
|
5e6a397e7c | ||
|
|
87fad6ff8b | ||
|
|
87252705a8 | ||
|
|
4423367ddb | ||
|
|
f85c654d9d | ||
|
|
7417a8ca02 | ||
|
|
ecfa80a7b3 | ||
|
|
2a106e39a3 | ||
|
|
54cee39b95 | ||
|
|
c27d13d9ac | ||
|
|
dd5030407e | ||
|
|
ba0ec0ae31 | ||
|
|
4d410319ce | ||
|
|
09ec2b81ba | ||
|
|
577695a1a3 | ||
|
|
2fad565016 | ||
|
|
6b805eea4a | ||
|
|
4ecd99023f | ||
|
|
d3410671fd | ||
|
|
a4c099a9ca | ||
|
|
09ba25473a | ||
|
|
b7a71e69f1 | ||
|
|
a30464df1a | ||
|
|
0f80334be1 | ||
|
|
c09a42f10a | ||
|
|
13704bd315 | ||
|
|
56f96f0d60 | ||
|
|
e86da59cdb | ||
|
|
143e6095ed | ||
|
|
fe4bc43a83 | ||
|
|
83426c0e1c | ||
|
|
b5c9a3c04e | ||
|
|
cb10db1e78 | ||
|
|
1d466a6926 | ||
|
|
fa51b96ca7 | ||
|
|
5796caa30a | ||
|
|
5169e02610 | ||
|
|
cceb607d99 | ||
|
|
c692013441 | ||
|
|
11ebc50a55 | ||
|
|
08c782fe91 |
No files matched your search
@@ -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
|
||||
+41
-44
@@ -19,32 +19,44 @@ 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:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
GITEA_SERVER: ${{ gitea.server_url }}
|
||||
GITEA_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
python3 scripts/deploy_stage.py
|
||||
|
||||
@@ -86,43 +98,28 @@ jobs:
|
||||
assert rune.__version__ == info['version']
|
||||
print('RUNTIME VERIFIED:', info)
|
||||
"
|
||||
- name: Release-history convergence refresh (SCAFFOLD-TRANCHE-009B)
|
||||
# §0 T10 CORRECTION: gate on the PROMOTION OUTPUT, not success() —
|
||||
# success() is true even for non-promoting (stale/superseded) jobs,
|
||||
# and a stale candidate writing its own HEAD as deployed_sha would
|
||||
# corrupt Gitea history. Only the SHA that ACTUALLY became
|
||||
# deployment:current may refresh the release record.
|
||||
- name: Release-history convergence fallback (SCAFFOLD-TRANCHE-0012C)
|
||||
# The PRIMARY convergence mechanism is deploy_stage.py's out-of-lock
|
||||
# publisher (publishes from CURRENT deployment authority, §14B/§14C).
|
||||
# This step is a non-blocking FALLBACK: if the primary publish failed
|
||||
# (e.g. missing Gitea config), this retries. NEVER fails the job —
|
||||
# documentation failure never outweighs deployment validity.
|
||||
if: ${{ success() && steps.promotion_truth.outputs.promoted == 'true' }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
# §3/§10: a later SHA of the SAME version successfully became
|
||||
# deployment:current → refresh the SAME Gitea release with the
|
||||
# converged identity + newly-governed facts. Idempotent. Failure
|
||||
# is VISIBLE (non-zero) but does NOT roll back valid software —
|
||||
# documentation failure never outweighs deployment validity (§5);
|
||||
# retry converges.
|
||||
#
|
||||
# Deployed identity comes from the DEPLOYMENT AUTHORITY (current
|
||||
# symlink readback), never from git rev-parse HEAD alone — the two
|
||||
# must agree (the promotion check proved they do) and the authority
|
||||
# is what is recorded.
|
||||
SHA=$(git rev-parse HEAD)
|
||||
CURRENT_TARGET=$(readlink /mnt/workdrive/rune-code/current || true)
|
||||
DEPLOYED_IDENTITY=$(python3 -c "
|
||||
import json
|
||||
print(json.load(open('$CURRENT_TARGET/src/rune/_build_identity.json'))['commit'])")
|
||||
if [ "$DEPLOYED_IDENTITY" != "$SHA" ]; then
|
||||
echo "::error::deployment authority disagrees with candidate SHA — refusing to refresh release history"
|
||||
exit 1
|
||||
VERSION=$(python3 -c "import sys; sys.path.insert(0,'src'); import rune; print(rune.__version__)" 2>/dev/null || echo "")
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "SKIP: cannot determine version"
|
||||
exit 0
|
||||
fi
|
||||
VERSION=$(python3 -c "import sys; sys.path.insert(0,'src'); import rune; print(rune.__version__)")
|
||||
python3 scripts/release_history_cli.py refresh \
|
||||
--version "$VERSION" \
|
||||
--server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO" \
|
||||
--store "$GITHUB_WORKSPACE/store/scaffold/current.json" \
|
||||
--release-sha "$SHA" \
|
||||
--deployed-sha "$DEPLOYED_IDENTITY" \
|
||||
|| echo "::warning::release-history refresh FAILED — release may be stale; retry converges (deployment unaffected)"
|
||||
--release-sha "$(git rev-parse HEAD)" \
|
||||
--deployed-sha "$(git rev-parse HEAD)" \
|
||||
|| echo "::warning::release-history fallback refresh FAILED — in-lock hook may have already converged; retry converges"
|
||||
@@ -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,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.66.48**
|
||||
**Current release: v0.66.78**
|
||||
|
||||
**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.**
|
||||
|
||||
|
||||
+22
-1
@@ -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-14T01:52:07Z",
|
||||
"generated_at": "2026-09-15T23:52:02Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
@@ -12,11 +12,13 @@
|
||||
"capability_broker",
|
||||
"connection_pool",
|
||||
"evidence_probe",
|
||||
"forge",
|
||||
"forge_client",
|
||||
"intent_continuity",
|
||||
"local_tool_runtime",
|
||||
"message_queue",
|
||||
"openai_compat_transport",
|
||||
"provider_context",
|
||||
"recall_uplift",
|
||||
"route_risk_flags",
|
||||
"tool_executor",
|
||||
@@ -215,21 +217,40 @@
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
"auto_tasks",
|
||||
"behaviour_ledger",
|
||||
"build_basis",
|
||||
"capability_fit",
|
||||
"capability_ledger",
|
||||
"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",
|
||||
"graph_projection",
|
||||
"impact_preview",
|
||||
"iteration",
|
||||
"layout_contract",
|
||||
"programme_continuation",
|
||||
"project_registry",
|
||||
"release_notes",
|
||||
"salience_gate",
|
||||
"scaffold",
|
||||
"spine",
|
||||
"stage_guards",
|
||||
"structural_migration",
|
||||
"structural_model",
|
||||
"structural_store",
|
||||
"toolchain",
|
||||
"tui_backlog",
|
||||
|
||||
@@ -104,15 +104,15 @@
|
||||
},
|
||||
{
|
||||
"path": "src/rune/attention_capsule.py",
|
||||
"role": "AttentionCapsule — the typed ledger of admitted blocks the model receives (to_messages re-derives the runtime payload)",
|
||||
"role": "AttentionCapsule \u2014 the typed ledger of admitted blocks the model receives (to_messages re-derives the runtime payload)",
|
||||
"disposition": "retain-authority",
|
||||
"evidence": "P1-02 (#25): the capsule IS the product the model receives — the broker renders admitted blocks into it, the safety override mutates its user block in place (re-measured), runtime.run consumes capsule.to_messages(), and the trace v4 capsule_messages/capsule_content_hashes prove the logged capsule EXACTLY equals the sent payload. Legacy create_capsule/to_prompt (string-trim fake budgeting) deprecated with no live callers."
|
||||
"evidence": "P1-02 (#25): the capsule IS the product the model receives \u2014 the broker renders admitted blocks into it, the safety override mutates its user block in place (re-measured), runtime.run consumes capsule.to_messages(), and the trace v4 capsule_messages/capsule_content_hashes prove the logged capsule EXACTLY equals the sent payload. Legacy create_capsule/to_prompt (string-trim fake budgeting) deprecated with no live callers."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/inference_trace.py",
|
||||
"role": "Durable per-run inference trace (tokens served/estimated, output, latency, per-block visibility)",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "P0-07 (#23): persists what ContextBroker (#19) assembled and what the transport served — builds FROM the broker trace, never re-measures; store state/inference_traces.jsonl."
|
||||
"evidence": "P0-07 (#23): persists what ContextBroker (#19) assembled and what the transport served \u2014 builds FROM the broker trace, never re-measures; store state/inference_traces.jsonl."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -160,13 +160,13 @@
|
||||
"path": "src/rune/capability_broker.py",
|
||||
"role": "Authoritative capability selector (select_for_job + verified floor + find_capability escape hatch)",
|
||||
"disposition": "retain-authority",
|
||||
"evidence": "P1-01 (#24): select_for_job is the single selection authority — its ordered decision feeds LocalToolRuntime(tools=) at both construction sites and the _contributor_tools prompt block; degrade-not-crash falls back to full TOOLS with a tier-1 record_failure."
|
||||
"evidence": "P1-01 (#24): select_for_job is the single selection authority \u2014 its ordered decision feeds LocalToolRuntime(tools=) at both construction sites and the _contributor_tools prompt block; degrade-not-crash falls back to full TOOLS with a tier-1 record_failure."
|
||||
},
|
||||
{
|
||||
"path": "memory/index/capability_cards.json",
|
||||
"role": "Capability-card index (rebuilt on empty/stale/corrupt; native cards synthesized from TOOLS metadata)",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "P1-01 (#24): load_cards validates and rebuilds — the packaged empty array no longer passes as an index; cards carry risk/preconditions/side_effects copied from tool_executor.TOOLS."
|
||||
"evidence": "P1-01 (#24): load_cards validates and rebuilds \u2014 the packaged empty array no longer passes as an index; cards carry risk/preconditions/side_effects copied from tool_executor.TOOLS."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/local_tool_runtime.py",
|
||||
@@ -486,12 +486,6 @@
|
||||
"disposition": "retain-authority",
|
||||
"evidence": "Defines the owner-approved canary-first Gitea sequence."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/plugins/build_mode/__init__.py",
|
||||
"role": "Checkpoint/ledger state-machine primitive used by current governed builds",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "Persists checkpoints but encodes an older phase order."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/build_mode.py",
|
||||
"role": "Separate BuildManager and worktree engine",
|
||||
@@ -504,6 +498,9 @@
|
||||
"disposition": "archive-candidate",
|
||||
"evidence": "Reports fixed success strings for phases without executing their claimed effects."
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"T17 17U: plugin BuildStateMachine deleted (zero importers; second parallel orchestrator)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -540,4 +537,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# SCAFFOLDING IMPLEMENTATION AUDIT + V1 GAP MATRIX (T18 §18B)
|
||||
|
||||
_Every claim classified against evidence. Scale: LIVE_PROVEN (production
|
||||
evidence) / TEST_PROVEN (suite green, no production exercise) /
|
||||
IMPLEMENTED_UNPROVEN / PARTIAL / PLANNED / ABSENT / LEGACY / DUPLICATE._
|
||||
|
||||
## Kernel claims
|
||||
|
||||
| Claim | Status | Evidence |
|
||||
|---|---|---|
|
||||
| Contract lifecycle + legal transitions | LIVE_PROVEN | 28 contracts driven through real flow; refusal envelopes observed |
|
||||
| classify_mode (one classifier) | LIVE_PROVEN | gates TUI /build (17G), operator hook, tests 62+ |
|
||||
| Conversational intent layer | TEST_PROVEN | 22+ held-outs incl. unseen paraphrases; 17R live exercise (one real conversational proof) |
|
||||
| Programme continuation | LIVE_PROVEN | 17E 4-stage proof on real bridge; restart/dup/failure held-outs |
|
||||
| Event-driven waits (builder) | LIVE_PROVEN | T15 15Q real model build; T17E async stage |
|
||||
| Event-driven waits (CI) | LIVE_PROVEN | T16 16D deadline path ×2 + T17 natural wake (mode=event, 112.5s) |
|
||||
| Event replay (before-registration) | TEST_PROVEN + production-realistic exercise | 17C 4.6s vs real completed CI; natural-cycle confirmation: the 17Z wake arrived mid-wait, replay path itself not yet hit naturally |
|
||||
| Cross-user bridge delivery | LIVE_PROVEN | 17C repro as runner user (2.05s) + production deploys |
|
||||
| Provider boundary | TEST_PROVEN (structural guard) + observed loud-failure | 16F/G suites; no silent fallback observed in production runs |
|
||||
| Builder cannot self-certify | LIVE_PROVEN | T14 closure refusals ×2; phase-gated close |
|
||||
| Completion truth evidence-driven | LIVE_PROVEN | contract checkpoints; verify_test_truth refusals |
|
||||
| Interruption/resume | LIVE_PROVEN | T14E SIGKILL; 17E restart held-outs |
|
||||
| Impact preview (visible, pre-build) | ABSENT → T18 | nothing renders structured impact before build today |
|
||||
| Structural project model (first-class) | PARTIAL | stores are spine-shaped (entities/rels) but projects model CONTRACTS, not architecture; no domain/subsystem/interface nesting for arbitrary projects |
|
||||
| Structural queries/impact closure for projects | PARTIAL | spine.impact_frontier exists for Rune; project stores lack query API |
|
||||
| Research→Build Basis (durable, versioned) | ABSENT → T18 | research output today stays in transient context/artifacts |
|
||||
| Research-depth decision at construction entry | PARTIAL | ADAPT routes task classes; no explicit construction-stage research gate |
|
||||
| OUTWARD prior-art-first pass | PARTIAL | stage exists in Research Core; not strengthened to precedent-first ordering |
|
||||
| Schema versioning + consent-gated migration | PARTIAL | stores carry schema_version=1; no migration machinery/consent flow |
|
||||
| Environment/runtime model | PARTIAL | deployment_surface exists in Spine; project scaffolds don't model envs |
|
||||
| Workspace lifecycle (worktree per contract) | PARTIAL | BuildManager worktrees exist (legacy adapter); not wired to contracts |
|
||||
| Forge abstraction | ABSENT (Gitea-coupled scripts) | deploy/qualify call Gitea API directly |
|
||||
| Structural drift for project scaffolds | PARTIAL | drift_resume (16P) covers checkpoint-vs-reality; no file/entity-level drift detection |
|
||||
| Graph projection for visualisation | ABSENT → T18 | Spine has snapshots; no stable read-only graph projection |
|
||||
| Memory/Substrate | ABSENT (BY DESIGN — out of scope) | rule L |
|
||||
|
||||
## V1 North Star gap matrix (this tranche's targets)
|
||||
|
||||
| North Star capability | Gap | T18 action |
|
||||
|---|---|---|
|
||||
| Research-led construction (early research, Build Basis) | ABSENT Build Basis | 18C: build_basis.py + ADAPT depth hook |
|
||||
| OUTWARD precedent-first | PARTIAL | 18D: strengthen stage contract + real research run |
|
||||
| First-class structural Scaffold | PARTIAL (contract-shaped) | 18E: structural_model.py over structural_store core |
|
||||
| Structure→scope→governance | PARTIAL | 18F: queries + impact closure feeding contracts |
|
||||
| Visible impact preview | ABSENT | 18G: impact_preview projection |
|
||||
| Structural drift | PARTIAL | 18H: extend drift_resume with entity-level checks |
|
||||
| Schema versioning + consent | PARTIAL | 18I: migration + consent flow + tests |
|
||||
| Environment/runtime model | PARTIAL | 18J: env entities + verification distinction |
|
||||
| Workspace lifecycle | PARTIAL (legacy adapter) | 18K: workspace contract on the BuildManager adapter |
|
||||
| Forge boundary | ABSENT | 18L: forge.py adapter interface (Gitea impl) |
|
||||
| Full route convergence | PARTIAL (classify shared; backends differ) | 18M: audit + smallest fix |
|
||||
| Generalisation (not phrase list) | TEST_PROVEN | 18N: adversarial paraphrase families |
|
||||
| Self-scaffold | PARTIAL (contracts only) | 18O: structural model of Scaffolding itself |
|
||||
| Upgrade-via-Scaffold proof | ABSENT | 18P: one real upgrade driven by the scaffold |
|
||||
| Graph projection | ABSENT | 18Q: versioned read-only projection |
|
||||
|
||||
## Kernel exceptions required (rule B record)
|
||||
|
||||
None yet. Every T18 plan so far EXTENDS (new modules consuming
|
||||
structural_store) rather than modifying frozen kernel behavior. Any
|
||||
exception discovered during implementation will be recorded here with
|
||||
evidence that the V1 requirement could not be met otherwise.
|
||||
@@ -0,0 +1,127 @@
|
||||
# SCAFFOLDING V1 DEFINITION (T18 §18B)
|
||||
|
||||
_Plane-English + technical definition of Scaffolding as it ACTUALLY
|
||||
exists at tranche T18 start (master 29570f4). Companion artifact:
|
||||
SCAFFOLDING_IMPLEMENTATION_AUDIT (evidence classification per claim)._
|
||||
|
||||
## What Scaffolding is (plain English)
|
||||
|
||||
Scaffolding is Rune's **construction authority**: the governed process
|
||||
by which software gets built or changed. It takes an authorized need,
|
||||
classifies what kind of construction it is, proportionally governs the
|
||||
work through a durable contract (with legal lifecycle transitions),
|
||||
derives scope from structural reality, drives builders, waits on events
|
||||
(not sleeps), refuses self-certified completion, verifies from
|
||||
canonical reality, and records evidence on durable objects so any
|
||||
future session can resume without a transcript.
|
||||
|
||||
It is NOT a scheduler, NOT a memory system, NOT a provider selector,
|
||||
NOT a research engine, NOT a release system. It is the layer that
|
||||
decides *whether, how, and with what obligations* construction happens
|
||||
— and whether it actually finished.
|
||||
|
||||
## What it owns
|
||||
|
||||
- **Construction classification** (`construction_mode.classify_mode` —
|
||||
the ONE classifier: NOT_CONSTRUCTION / LOCAL_CHANGE /
|
||||
EXISTING_PROJECT_CHANGE / PROJECT_BUILD / UNKNOWN; never silently
|
||||
chooses; unknown surfaces probe questions)
|
||||
- **Conversational interpretation** (`construction_intent.py` —
|
||||
meaning/referents/authorization; DISCUSSION→PROPOSAL→USER_APPROVED;
|
||||
feeds the ONE classifier, never deepens)
|
||||
- **Project Scaffold + Contracts** (`scaffold.py` — legal phase map
|
||||
draft→approved→building→verifying→…→closed/failed/abandoned; scope;
|
||||
obligations; checkpoints; predicted-vs-observed at close)
|
||||
- **Programme continuation** (`programme_continuation.py` —
|
||||
run_construction_programme: stages 1:1 to contracts, persist→canary→
|
||||
next; "task done ≠ programme done")
|
||||
- **Impact/scope machinery** (construction_decision, completeness
|
||||
gates, affected-surface propagation)
|
||||
- **Stage guards** (`stage_guards.py` — readiness blocks, liveness
|
||||
proves progress, exit canary proves effect; verify_test_truth refuses
|
||||
false greens)
|
||||
- **Failure taxonomy** (`failure_recovery.py` — 7 classes incl.
|
||||
PROVIDER_UNAVAILABLE; retry budgets; PERMANENT/APPROVAL never retry)
|
||||
- **Builder interface** (`construction_interface.py` — ONE builder
|
||||
boundary; JSON envelopes; builder identity recorded-but-inert;
|
||||
discover_active_contract; capabilities())
|
||||
|
||||
## What it does NOT own
|
||||
|
||||
- **Provider/model selection** (provider layer owns; ambient context
|
||||
via `provider_context.require_model`; structural guard test)
|
||||
- **Research competence** (Research Core owns; Scaffolding invokes and
|
||||
consumes — see 18C integration)
|
||||
- **Asynchronous wait/resume machinery** (W/R event bridge owns;
|
||||
`event_wait.py` is the repo-side adapter: event drives continuation,
|
||||
watchdog detects failure, durable record is truth)
|
||||
- **Release/deployment** (deploy_stage/promotion/qualify + Gitea
|
||||
workflows own it; Scaffolding records evidence)
|
||||
- **Rune's structural self-model** (Spine owns — `spine.py` over
|
||||
`structural_store.py`; Scaffolding stores use the SAME generic core)
|
||||
- **Long-horizon relevance/memory** (future Substrate — not built;
|
||||
seam exists: construction_projection + construction_commands)
|
||||
- **The TUI** (client; sends text, renders projections)
|
||||
|
||||
## How the flows work
|
||||
|
||||
**New build (PROJECT_BUILD)**: conversational/explicit intent →
|
||||
classify_mode (+ project-existence evidence) → activate() bootstraps a
|
||||
provisional Project Scaffold (identity before repository, UNKNOWN
|
||||
fields explicit) → contract draft→approved (user provenance) →
|
||||
building (builder under event wait) → verifying (exit canary;
|
||||
independent evidence) → closed with predicted-vs-observed.
|
||||
|
||||
**Existing-project change**: intent → classify (scoped/file/target
|
||||
evidence; proportional) → attach to existing authority → light or full
|
||||
contract by depth → same lifecycle, proportionate ceremony.
|
||||
|
||||
**Research-led construction (V1 target — CORRECTED 2026-09-15, Nick):**
|
||||
|
||||
> THE RESEARCH PLAN ITSELF IS CONTEXTUALLY GENERATED. There is NO
|
||||
> permanent research-topic checklist. For every request:
|
||||
> REQUEST/INTENT → understand target/current reality → identify material
|
||||
> unknowns/decisions/risks → generate a context-specific research plan →
|
||||
> perform proportional INWARD/OUTWARD/deeper research. OUTWARD asks what
|
||||
> already exists relevant to THIS specific intent (adopt/adapt/combine/
|
||||
> reject/gaps/innovation). The categories researched are selected from
|
||||
> context; deeper technical research is driven by unresolved questions
|
||||
> and consequence level, never performed mechanically.
|
||||
|
||||
`build_basis.generate_research_plan()` implements this: topics are
|
||||
synthesized from THIS intent's material unknowns/decisions/risks;
|
||||
the held-out suite proves unrelated intents produce materially
|
||||
different plans and that no canned category list exists.
|
||||
|
||||
**Future upgrades (V1 target)**: authorized need → research-depth
|
||||
decision (ADAPT) → Research Core pass → durable Build Basis →
|
||||
structural Scaffold consult/construct → impact closure + visible
|
||||
preview → contract with structurally-derived scope → isolated
|
||||
workspace → governed build/verify → merge/release → structural
|
||||
Scaffold updated. (Parts of this chain are new in T18 — see audit.)
|
||||
|
||||
**Interruption/resume**: SIGKILL-safe. Durable contract + programme
|
||||
records + wait records are the truth; fresh process resumes from
|
||||
identity alone; duplicate signals idempotent (live-proven T14E/T17E).
|
||||
|
||||
**Deployment/release participation**: Scaffolding does not run the
|
||||
pipeline; it consumes qualified CI/deploy evidence, records deployed
|
||||
identity on contracts, and refuses closure without it where contracted.
|
||||
|
||||
## How Spine relates
|
||||
|
||||
Spine = Rune's canonical structural self-model (structural_store
|
||||
generic core, maturity ladder, impact_frontier). Scaffolding's project
|
||||
stores share the SAME generic core (zero Rune constants) — one graph
|
||||
discipline, two scopes: Spine models Rune; project Scaffolds model
|
||||
their projects. No duplicate truth: a project's structural model lives
|
||||
in its own store; Rune's lives in Spine. Later retrofit will modernise
|
||||
Spine using the discipline V1 generalises (18K relationship).
|
||||
|
||||
## How Substrate will interact (without owning internals)
|
||||
|
||||
Read: versioned outbound projection (construction_projection,
|
||||
SCHEMA_VERSION=1). Write: bounded validated commands
|
||||
(construction_commands — 6 commands, lifecycle-validated, idempotent,
|
||||
no direct storage mutation). Substrate asks; Scaffolding decides.
|
||||
Memory graph explicitly out of scope (T18 rule L).
|
||||
@@ -0,0 +1,73 @@
|
||||
# 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` — PIN `RUNE_WR_ROOT`: unpinned consumers get a loud warning, never a silent split-brain) | waits/events/leases/resumes JSON |
|
||||
| Programme continuation | `src/rune/programme_continuation.py` (WIRED T17: `run_construction_programme` binds stages to Scaffolding contracts) | scaffold store `programmes/` |
|
||||
| Conversational construction | `src/rune/construction_intent.py` (meaning/referents/authorization) → `construction_mode.classify_mode` (the ONE classifier) | intents are transient; contracts durable |
|
||||
| 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: WIRED (T17 17D — run_construction_programme
|
||||
is the canonical multi-stage driver; 17E live-proven).
|
||||
- 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`.
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"BLE001": 352,
|
||||
"S110": 80
|
||||
}
|
||||
"BLE001": 350,
|
||||
"S110": 78
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.66.48"
|
||||
version = "0.66.78"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"errorCount": 117,
|
||||
"warningCount": 443,
|
||||
"informationCount": 0
|
||||
"errorCount": 117,
|
||||
"warningCount": 448,
|
||||
"informationCount": 0
|
||||
}
|
||||
+2
-2
@@ -70,7 +70,7 @@ pluggy==1.6.0
|
||||
# via
|
||||
# diff-cover
|
||||
# pytest
|
||||
propcache==0.5.2
|
||||
propcache==0.5.3
|
||||
# via
|
||||
# aiohttp
|
||||
# yarl
|
||||
@@ -118,5 +118,5 @@ typing-extensions==4.16.0
|
||||
# textual-autocomplete
|
||||
websockets==17.1
|
||||
# via rune (pyproject.toml)
|
||||
yarl==1.24.5
|
||||
yarl==1.25.1
|
||||
# via aiohttp
|
||||
@@ -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())
|
||||
@@ -147,7 +147,12 @@ def read_deployed_commit(deploy_root: Path = DEPLOY_ROOT) -> str:
|
||||
|
||||
|
||||
def promote(
|
||||
candidate_sha: str, stage: Path, tmp_stage: Path, repo: Path, deploy_root: Path = DEPLOY_ROOT
|
||||
candidate_sha: str,
|
||||
stage: Path,
|
||||
tmp_stage: Path,
|
||||
repo: Path,
|
||||
deploy_root: Path = DEPLOY_ROOT,
|
||||
on_promoted=None,
|
||||
) -> tuple[str, str]:
|
||||
"""Atomically decide + promote under an exclusive lock.
|
||||
|
||||
@@ -189,7 +194,18 @@ def promote(
|
||||
shutil.rmtree(stage)
|
||||
os.rename(tmp_stage, stage)
|
||||
_atomic_symlink_target(deploy_root, "current", stage)
|
||||
return (OUTCOME_PROMOTED, f"current -> {stage.name}")
|
||||
detail = f"current -> {stage.name}"
|
||||
if on_promoted is not None:
|
||||
# §12C: race-safe convergence — the WINNER refreshes release
|
||||
# history atomically with its promotion, inside the lock.
|
||||
# Failure degrades to STALE (software valid, history lags;
|
||||
# retry by any later deploy converges idempotently).
|
||||
try:
|
||||
on_promoted(candidate_sha)
|
||||
detail += " (+ release-history converged)"
|
||||
except Exception as exc: # noqa: BLE001
|
||||
detail += f" (release-history refresh FAILED: {exc!s:.120} — STALE, retry converges)"
|
||||
return (OUTCOME_PROMOTED, detail)
|
||||
finally:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
+104
-3
@@ -28,6 +28,92 @@ DEPLOY_ROOT = Path("/mnt/workdrive/rune-code")
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
VENV_PY = "/home/nick/.venvs/memory/bin/python3"
|
||||
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
sys.path.insert(0, str(REPO_ROOT / "src"))
|
||||
import deploy_promotion # noqa: E402
|
||||
from rune.release_notes import ( # noqa: E402
|
||||
ReleaseNote as _ReleaseNote,
|
||||
auto_initial_note as _auto_note,
|
||||
converged_deployed_sha as _authority_sha,
|
||||
convergence_refresh as _refresh,
|
||||
fetch_release as _fetch_release,
|
||||
publish_release as _publish,
|
||||
sha_from_body as _sha_from_body,
|
||||
summary_from_body as _summary_from_body,
|
||||
)
|
||||
|
||||
|
||||
def publish_out_of_lock(short: str, sha: str, *,
|
||||
deploy_root: Path = DEPLOY_ROOT,
|
||||
scaffold_store: Path | None = None) -> None:
|
||||
"""§13D/§14B: release-history publication AFTER the promotion lock is
|
||||
released. Network I/O never holds the flock.
|
||||
|
||||
§14B INVARIANT: publication does NOT trust the originating job's SHA.
|
||||
The CURRENT deployment authority is re-read immediately before
|
||||
publishing; the note is constructed from CURRENT authority. A stale
|
||||
job whose candidate was superseded converges the NEW authority (or
|
||||
refuses a version it cannot truthfully describe) — it may help
|
||||
converge B, but may never reassert itself as deployed.
|
||||
Failure degrades to STALE (software valid, history lags; retry
|
||||
converges)."""
|
||||
try:
|
||||
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO", "")
|
||||
token = os.environ.get("GITEA_TOKEN", "")
|
||||
if not (server and repo and token):
|
||||
return
|
||||
# §14B: re-read CURRENT authority — never trust the job's SHA.
|
||||
authority_sha = _authority_sha(str(deploy_root))
|
||||
if not authority_sha:
|
||||
print("WARNING: release-history convergence skipped: "
|
||||
"deployment authority unreadable — STALE, retry converges")
|
||||
return
|
||||
store = str(scaffold_store or (REPO_ROOT / "store" / "scaffold" / "current.json"))
|
||||
identity = deploy_root / authority_sha[:12] / "src" / "rune" / "_build_identity.json"
|
||||
try:
|
||||
version = json.loads(identity.read_text()).get("version", "")
|
||||
except (OSError, ValueError):
|
||||
version = ""
|
||||
if not version:
|
||||
print("WARNING: release-history convergence skipped: "
|
||||
"no version on current authority — STALE, retry converges")
|
||||
return
|
||||
tag = f"v{version}"
|
||||
existing = _fetch_release(server, repo, token, tag)
|
||||
if existing is None:
|
||||
note = _auto_note(
|
||||
version=version,
|
||||
summary=(f"Rune v{version} — evidence-derived release "
|
||||
f"history (automatic convergence)"),
|
||||
scaffold_store=store,
|
||||
release_sha=authority_sha, deployed_sha=authority_sha,
|
||||
provenance=["automatic convergence publish "
|
||||
"(deploy workflow, authority-derived)"])
|
||||
else:
|
||||
note = _refresh(
|
||||
_ReleaseNote.from_dict({
|
||||
"version": version,
|
||||
"summary": _summary_from_body(existing.get("body", "")),
|
||||
"release_sha": _sha_from_body(existing.get("body", ""), "tag")
|
||||
or authority_sha,
|
||||
"provenance": [],
|
||||
}),
|
||||
deployed_sha=authority_sha, scaffold_store=store,
|
||||
extra_provenance="automatic convergence publish "
|
||||
"(deploy workflow, authority-derived)")
|
||||
# preserve the tag SHA the stored body carries
|
||||
tag_sha = _sha_from_body(existing.get("body", ""), "tag")
|
||||
if tag_sha:
|
||||
note.release_sha = tag_sha
|
||||
result = _publish(server, repo, token, note)
|
||||
print(f"release-history convergence: "
|
||||
f"{result.get('action', 'unknown')} "
|
||||
f"(authority {authority_sha[:12]})")
|
||||
except Exception as conv_exc: # noqa: BLE001 — history STALE, software valid
|
||||
print(f"WARNING: release-history convergence failed: "
|
||||
f"{conv_exc!s:.200} — STALE, retry converges")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# The runner's checkout workspace (act sets GITHUB_WORKSPACE). Never a
|
||||
@@ -127,10 +213,25 @@ print("VERIFIED:", json.dumps(info))
|
||||
# ancestry check + lock live in deploy_promotion.py; this script only
|
||||
# handles the truthful outcome reporting. A stale candidate must NOT
|
||||
# claim promotion success — build/stage success != promotion success.
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
import deploy_promotion # noqa: E402
|
||||
#
|
||||
# §14C: NO separate convergence-intent ledger. The durable pending-
|
||||
# convergence state IS the deployment authority itself (atomically
|
||||
# swapped `current` symlink + stamped build identity): publication
|
||||
# derives from it, idempotently, outside the lock. The old intent
|
||||
# file had exactly one writer and zero readers, non-atomic writes,
|
||||
# and a bogus recorded_at (SECONDS is a bash builtin, never exported
|
||||
# — always "0"). Deriving from authority gives atomicity, restart
|
||||
# readability, corruption immunity, monotonicity (stale publishers
|
||||
# read CURRENT authority, not their own job) and idempotent retry
|
||||
# for free. REMOVE more complexity than you add.
|
||||
|
||||
outcome, detail = deploy_promotion.promote(sha, DEPLOY_ROOT / short, tmp_stage, repo)
|
||||
outcome, detail = deploy_promotion.promote(
|
||||
sha, DEPLOY_ROOT / short, tmp_stage, repo)
|
||||
|
||||
# §13D: publish release history OUTSIDE the promotion lock (network
|
||||
# I/O must not hold the flock). Only for PROMOTED outcomes.
|
||||
if outcome == deploy_promotion.OUTCOME_PROMOTED:
|
||||
publish_out_of_lock(short, sha)
|
||||
|
||||
if outcome == deploy_promotion.OUTCOME_PROMOTED:
|
||||
print(f"PROMOTED: {short}")
|
||||
|
||||
@@ -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())
|
||||
@@ -87,6 +87,30 @@ def main(argv=None) -> int:
|
||||
return 0 if not result.get("failed") else 1
|
||||
|
||||
if args.cmd == "refresh":
|
||||
# §12C RACE-SAFE REVALIDATION: between promotion and refresh, a
|
||||
# NEWER deploy may have superseded this candidate. The deployment
|
||||
# authority is re-read NOW: if current's build identity differs
|
||||
# from the candidate, this job is stale → refuse (preserve T11
|
||||
# invariant: stale jobs never overwrite release history). When
|
||||
# current AGREES, the candidate IS the converged truth.
|
||||
current_link = os.environ.get(
|
||||
"RUNE_DEPLOY_CURRENT", "/mnt/workdrive/rune-code/current")
|
||||
try:
|
||||
deployed_now = rn.converged_deployed_sha(
|
||||
str(Path(current_link).parent))
|
||||
except (OSError, ValueError):
|
||||
deployed_now = ""
|
||||
if args.deployed_sha and deployed_now and \
|
||||
not deployed_now.startswith(args.deployed_sha[:12]):
|
||||
print(json.dumps({
|
||||
"failed": True, "action": "refresh-superseded",
|
||||
"error": f"deployment authority shows {deployed_now[:12]}, "
|
||||
f"candidate {args.deployed_sha[:12]} was superseded "
|
||||
f"— stale refresh refused (history preserved)",
|
||||
"authority": deployed_now[:12],
|
||||
}))
|
||||
return 1 # visible; retry by the WINNING deploy converges
|
||||
|
||||
existing = rn.fetch_release(args.server, args.repo, token,
|
||||
f"v{args.version}")
|
||||
if existing is None:
|
||||
@@ -119,28 +143,12 @@ def main(argv=None) -> int:
|
||||
|
||||
|
||||
def _summary_from_body(body: str) -> str:
|
||||
"""Extract the summary paragraph (between the H1 and the first other
|
||||
heading/italic line) from a stored release body."""
|
||||
lines = body.splitlines()
|
||||
grab = []
|
||||
for ln in lines[1:]:
|
||||
s = ln.strip()
|
||||
if not s:
|
||||
if grab:
|
||||
break
|
||||
continue
|
||||
if s.startswith(("#", "_", "##", "-")):
|
||||
break
|
||||
grab.append(s)
|
||||
return " ".join(grab) or f"Rune — evidence-derived release history"
|
||||
"""Delegate to the single authority in rune.release_notes (§14B)."""
|
||||
return rn.summary_from_body(body)
|
||||
|
||||
|
||||
def _sha_from_body(body: str, which: str) -> str:
|
||||
import re
|
||||
pat = ("tag commit: `([0-9a-f]+)`" if which == "tag"
|
||||
else r"\*\*final deployed: `([0-9a-f]+)`\*\*")
|
||||
m = re.search(pat, body, re.I)
|
||||
return m.group(1) if m else ""
|
||||
return rn.sha_from_body(body, which)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
#!/usr/bin/env python3
|
||||
"""t14_builder_session.py — T14D/14E REAL builder session driver.
|
||||
|
||||
The builder is the ACTUAL mimo CLI process (mimo run, model
|
||||
Xioami/mimo-v2.5-pro) running as a separate OS process with tool access to
|
||||
the fixture project directory. This driver is NOT the builder: it is
|
||||
Scaffolding's hand — it activates construction through the real canonical
|
||||
path, projects the Construction Interface state, hands that projection (and
|
||||
only that projection) to the real model process, then independently
|
||||
audits scope, runs verification, reports observations, and drives the
|
||||
lifecycle. Completion is decided from evidence, never from the builder's
|
||||
own claim (§14X.8).
|
||||
|
||||
Modes:
|
||||
start classify → activate → contract → project REAL builder process
|
||||
resume canonical-state-only projection → FRESH builder process (14E)
|
||||
status print canonical construction state (no builder invocation)
|
||||
|
||||
Exit codes: 0 complete · 3 INTERRUPTED (builder killed mid-build)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune import construction_interface as ci # noqa: E402
|
||||
from rune import construction_mode as cm # noqa: E402
|
||||
from rune import construction_activation as ca # noqa: E402
|
||||
from rune import construction_decision as cd # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
|
||||
USER_REQUEST = (
|
||||
"Build me a small standalone practice scheduler that can create, "
|
||||
"list and complete practice items."
|
||||
)
|
||||
EXPECTED_SCOPE = ["scheduler.js", "scheduler.test.js", "package.json"]
|
||||
# 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"
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
print(f"[t14] {msg}", flush=True)
|
||||
|
||||
|
||||
def classify_and_activate(store: Path, objective: str) -> dict:
|
||||
"""The REAL canonical path: classify → activate. No shortcuts."""
|
||||
parent = sc.Scaffold(str(store), "rune")
|
||||
parent.init(purpose="t14 fixture parent authority")
|
||||
decision = cm.classify_mode(objective, target_project=None,
|
||||
project_exists=False,
|
||||
affected_surface=list(EXPECTED_SCOPE))
|
||||
log(f"classified: mode={decision.mode} "
|
||||
f"target={decision.target_project!r}")
|
||||
need = cd.Need(
|
||||
need_id=f"need-{ss.now()}-t14", objective=objective,
|
||||
origin="rune_native", project=decision.target_project or "",
|
||||
constraints={}, affected_surface=list(EXPECTED_SCOPE),
|
||||
evidence_refs=[], approval_context="t14 user-approved fixture")
|
||||
rec = ca.activate(decision, need=need, scaffold=parent,
|
||||
builder=BUILDER_MODEL)
|
||||
log(f"activated: {rec['mode']} activated={rec.get('activated')} "
|
||||
f"project={rec.get('project')}")
|
||||
return {"decision": {"mode": decision.mode,
|
||||
"target_project": decision.target_project,
|
||||
"evidence": list(decision.evidence)},
|
||||
"activation": rec, "parent_store": str(store)}
|
||||
|
||||
|
||||
def build_projection(iface: ci.ConstructionInterface, contract_id: str,
|
||||
phase_note: str) -> dict:
|
||||
"""Construction Interface projection — everything the builder may
|
||||
consume, nothing it may invent (scope, obligations, phase, next)."""
|
||||
scope = iface.get_scope(contract_id)
|
||||
obligations = iface.get_obligations(contract_id)
|
||||
context = iface.construction_context(contract_id)
|
||||
assert scope.get("success") and obligations.get("success"), \
|
||||
f"projection failure: {scope.get('errors')} {obligations.get('errors')}"
|
||||
return {
|
||||
"phase": context["state"].get("phase"),
|
||||
"project": context["state"].get("project"),
|
||||
"need": context["state"].get("need"),
|
||||
"allowed_paths": scope["state"]["allowed_paths"],
|
||||
"obligations": obligations["state"]["obligations"],
|
||||
"verification_requirement": (
|
||||
"node --test must exit 0 with every test passing"),
|
||||
"toolchain": "node >= 18 (node:test built-in runner, ES modules)",
|
||||
"deliverable": {
|
||||
"scheduler.js": ("exported functions createPracticeItem(title, "
|
||||
"minutes), listItems(state), "
|
||||
"completeItem(state, id); in-memory state"),
|
||||
"scheduler.test.js": ("node:test cases proving create, list "
|
||||
"and complete work"),
|
||||
"package.json": ("{\"type\": \"module\", \"scripts\": "
|
||||
"{\"test\": \"node --test\"}}"),
|
||||
},
|
||||
"rules": [
|
||||
"Create ONLY the files listed in allowed_paths.",
|
||||
"Do not modify or create anything else.",
|
||||
"Verify with: node --test (must pass).",
|
||||
"Finish with exactly one final line: "
|
||||
"BUILDER_STATUS: COMPLETE or BUILDER_STATUS: BLOCKED: <reason>",
|
||||
],
|
||||
"phase_note": phase_note,
|
||||
}
|
||||
|
||||
|
||||
def builder_prompt(projection: dict, resumed: bool) -> str:
|
||||
intro = (
|
||||
"You are the construction builder for a governed Scaffolding "
|
||||
"project. Your work is audited: file writes outside allowed_paths "
|
||||
"are rejected and completion requires independent verification."
|
||||
)
|
||||
if resumed:
|
||||
intro += (
|
||||
" A PREVIOUS builder process was interrupted mid-build. Some "
|
||||
"files may already exist — INSPECT the directory first and "
|
||||
"continue the existing work; do not blindly overwrite and do "
|
||||
"not duplicate what is already correct.")
|
||||
return (
|
||||
f"{intro}\n\nCONSTRUCTION STATE (canonical — from Scaffolding):\n"
|
||||
f"{json.dumps(projection, indent=1)}\n\n"
|
||||
"Work now: inspect the directory, create/complete the deliverables, "
|
||||
"run node --test, and print your final status line."
|
||||
)
|
||||
|
||||
|
||||
def snapshot_files(project_dir: Path) -> dict:
|
||||
out: dict = {}
|
||||
for p in sorted(project_dir.rglob("*")):
|
||||
if p.is_file():
|
||||
out[str(p.relative_to(project_dir))] = p.stat().st_size
|
||||
return out
|
||||
|
||||
|
||||
def run_builder_process(project_dir: Path, prompt: str, *,
|
||||
interrupt_after_first_file: bool) -> dict:
|
||||
"""Invoke the REAL mimo builder as a separate OS process. Returns
|
||||
{terminated_by, exit_code, session_output}."""
|
||||
env = dict(os.environ)
|
||||
proc = subprocess.Popen(
|
||||
[BUILDER_CMD, "run", "-m", BUILDER_MODEL,
|
||||
"--format", "json", prompt],
|
||||
cwd=str(project_dir), env=env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, start_new_session=True)
|
||||
terminated_by = "completed"
|
||||
if interrupt_after_first_file:
|
||||
log("interrupt armed: watching for first source file...")
|
||||
while True:
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
files = [p for p in project_dir.iterdir()
|
||||
if p.suffix in (".mjs", ".js", ".json")]
|
||||
if files:
|
||||
log(f"INTERRUPTING builder: first file landed "
|
||||
f"({files[0].name}) — SIGKILL to the real process")
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
||||
terminated_by = "sigkill_mid_build"
|
||||
break
|
||||
time.sleep(0.5)
|
||||
try:
|
||||
out, err = proc.communicate(timeout=900)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
||||
out, err = proc.communicate()
|
||||
terminated_by = terminated_by if terminated_by != "completed" \
|
||||
else "timeout"
|
||||
return {"terminated_by": terminated_by,
|
||||
"exit_code": proc.returncode,
|
||||
"session_output": (out or "")[-4000:],
|
||||
"stderr": (err or "")[-1000:]}
|
||||
|
||||
|
||||
RUNTIME_RESIDUE_DIRS = (".mimocode", ".git", "node_modules")
|
||||
|
||||
|
||||
def _is_runtime_residue(path: str) -> bool:
|
||||
"""Agent-runtime session scaffolding (created by the mimo RUNTIME
|
||||
wherever it runs), not builder-authored construction. Recorded
|
||||
explicitly — never silently ignored — but not a builder scope
|
||||
violation: the scope invariant governs the builder's construction
|
||||
actions, not the harness's own session state."""
|
||||
top = path.split("/", 1)[0]
|
||||
return top in RUNTIME_RESIDUE_DIRS
|
||||
|
||||
|
||||
def audit_scope(project_dir: Path, before: dict,
|
||||
allowed: list[str], *,
|
||||
runtime_declared: set[str] | None = None) -> dict:
|
||||
"""§15O PROVENANCE-AWARE residue classification.
|
||||
|
||||
The T14 prefix rule (.git/.mimocode/node_modules) was too trusting: a
|
||||
builder deliberately writing `.mimocode/evil` would vanish from the
|
||||
audit. Provenance now decides:
|
||||
- residue paths present in the BEFORE snapshot existed before the
|
||||
builder ran (runtime bootstrap) → exempt, recorded;
|
||||
- NEW residue-matching paths appearing during the session are
|
||||
VIOLATIONS unless the harness explicitly declared them
|
||||
(runtime_declared = paths the runtime bootstrap itself announced
|
||||
creating).
|
||||
"""
|
||||
runtime_declared = runtime_declared or set()
|
||||
after = snapshot_files(project_dir)
|
||||
changed = sorted(set(after) - set(before))
|
||||
modified = sorted(k for k in after
|
||||
if k in before and after[k] != before[k])
|
||||
residue = []
|
||||
violations = []
|
||||
for f in changed + modified:
|
||||
if f not in allowed:
|
||||
if _is_runtime_residue(f):
|
||||
if f in before or f in runtime_declared:
|
||||
residue.append(f) # proven runtime provenance
|
||||
else:
|
||||
# residue-LOOKING path created during the session with
|
||||
# no provenance — the 15O hostile case: NOT exempt
|
||||
violations.append(f)
|
||||
else:
|
||||
violations.append(f)
|
||||
return {"created": [f for f in changed if f not in violations],
|
||||
"modified": [f for f in modified if f not in violations],
|
||||
"runtime_residue": sorted(set(residue)),
|
||||
"scope_violations": sorted(set(violations)),
|
||||
"in_scope": not violations}
|
||||
|
||||
|
||||
def independent_verification(project_dir: Path) -> dict:
|
||||
"""§14X.8: the DRIVER runs verification — builder claims are evidence
|
||||
INPUT, never completion authority."""
|
||||
probe = subprocess.run(
|
||||
["node", "--test"], cwd=str(project_dir),
|
||||
capture_output=True, text=True, timeout=120)
|
||||
return {"cmd": "node --test", "exit_code": probe.returncode,
|
||||
"stdout": (probe.stdout or "")[-2000:],
|
||||
"stderr": (probe.stderr or "")[-500:]}
|
||||
|
||||
|
||||
def complete_lifecycle(iface: ci.ConstructionInterface, contract_id: str,
|
||||
scope_audit: dict, verification: dict,
|
||||
builder_status: str) -> dict:
|
||||
items = [{"entity": "builder-session",
|
||||
"source": "independent_driver",
|
||||
"detail": (f"builder={BUILDER_MODEL} claimed="
|
||||
f"{builder_status} scope_violations="
|
||||
f"{scope_audit['scope_violations']} "
|
||||
f"verify_exit={verification['exit_code']}")}]
|
||||
items += [{"entity": f, "source": "independent_driver",
|
||||
"detail": "created in scope"}
|
||||
for f in scope_audit["created"]]
|
||||
items += [{"entity": f, "source": "independent_driver",
|
||||
"detail": "modified in scope"}
|
||||
for f in scope_audit["modified"]]
|
||||
obs = iface.report_observation(contract_id, items)
|
||||
assert obs.get("success"), obs.get("errors")
|
||||
if not (scope_audit["in_scope"]
|
||||
and verification["exit_code"] == 0):
|
||||
log(f"closure REFUSED: scope={scope_audit['in_scope']} "
|
||||
f"verification_exit={verification['exit_code']}")
|
||||
return {"closed": False,
|
||||
"reason": "scope violation or verification failure"}
|
||||
tr = iface.transition(contract_id, "verifying",
|
||||
evidence_ref="independent node --test run")
|
||||
assert tr.get("success"), tr.get("errors")
|
||||
v = iface.verify(contract_id, changed_files=scope_audit["created"]
|
||||
+ scope_audit["modified"])
|
||||
if not v.get("success"):
|
||||
log(f"verify refused: {v.get('errors')}")
|
||||
return {"closed": False, "reason": "verify refused"}
|
||||
c = iface.close(contract_id, observed_evidence=[
|
||||
{"kind": "verification", "cmd": "node --test",
|
||||
"exit_code": 0, "source": "independent_driver_run"}],
|
||||
scope_files=scope_audit["created"] + scope_audit["modified"])
|
||||
return {"closed": bool(c.get("success")),
|
||||
"close_envelope": {k: c.get(k) for k in
|
||||
("success", "errors", "ids", "state")}}
|
||||
|
||||
|
||||
def discover_contract_id(store: Path) -> tuple[str, str] | None:
|
||||
"""§15U: delegates to the canonical interface
|
||||
(ConstructionInterface.discover_active_contract). This shim stays only
|
||||
so existing driver call sites keep working."""
|
||||
from rune import construction_interface as _ci
|
||||
env = _ci.ConstructionInterface.discover_active_contract(str(store))
|
||||
if not env.get("success"):
|
||||
return None
|
||||
return env["ids"]["project"], env["ids"]["contract_id"]
|
||||
|
||||
|
||||
def seed_requirement_spec(store: Path, project: str) -> str:
|
||||
"""Govern the requirement as a contract on the project's surface
|
||||
(identity before repository §7): the user request IS the requirement
|
||||
evidence. The Need then references this contract explicitly — the
|
||||
capability-fit engine resolves REUSE of the SPECIFICATION (the build
|
||||
is governed by satisfying it). Maturity DECLARED: a requirement, not
|
||||
existing code — nothing manufactured."""
|
||||
proj = sc.Scaffold(str(store / "projects" / project), project)
|
||||
doc = proj.load()
|
||||
if ss.entity(doc, "contract:practice-items-spec") is None:
|
||||
ss.add_entity(
|
||||
doc, id="contract:practice-items-spec", kind="contract",
|
||||
identities={"objective": USER_REQUEST},
|
||||
evidence_refs=["t14-user-request"])
|
||||
ss.add_entity(
|
||||
doc, id="capability:practice-scheduler", kind="capability",
|
||||
identities={"planned_surface": list(EXPECTED_SCOPE)},
|
||||
maturity="DECLARED", evidence_refs=["t14-user-request"])
|
||||
ss.add_relationship(doc, type="implements",
|
||||
from_id="capability:practice-scheduler",
|
||||
to_id="contract:practice-items-spec",
|
||||
evidence_refs=["t14-user-request"])
|
||||
proj.commit(doc, what="seed requirement spec: practice items "
|
||||
"create/list/complete (user request)")
|
||||
return "contract:practice-items-spec"
|
||||
|
||||
|
||||
def cmd_start(store: Path, project_dir: Path, interrupt: bool) -> int:
|
||||
before = snapshot_files(project_dir)
|
||||
staged = classify_and_activate(store, USER_REQUEST)
|
||||
project = staged["activation"]["project"]
|
||||
assert staged["activation"]["mode"] == "PROJECT_BUILD", \
|
||||
f"expected PROJECT_BUILD, got {staged['activation']['mode']}"
|
||||
spec_contract = seed_requirement_spec(store, project)
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL,
|
||||
project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
sub = iface.submit_need(
|
||||
USER_REQUEST, origin="rune_native",
|
||||
constraints={"environment": {"os": "linux", "runtime": "node>=18",
|
||||
"runner": "node --test"},
|
||||
"contracts": [spec_contract]},
|
||||
affected_surface=list(EXPECTED_SCOPE),
|
||||
evidence_refs=["t14-user-request", "node-v22-installed"],
|
||||
approval_context="t14 user-approved fixture")
|
||||
assert sub.get("success"), sub.get("errors")
|
||||
contract_id = sub["ids"]["contract_id"]
|
||||
log(f"contract: {contract_id}")
|
||||
for phase in ("approved", "building"):
|
||||
tr = iface.transition(contract_id, phase,
|
||||
evidence_ref=f"t14 {phase}")
|
||||
assert tr.get("success"), tr.get("errors")
|
||||
projection = build_projection(iface, contract_id,
|
||||
phase_note="initial build")
|
||||
session = run_builder_process(
|
||||
project_dir, builder_prompt(projection, resumed=False),
|
||||
interrupt_after_first_file=interrupt)
|
||||
if session["terminated_by"] == "sigkill_mid_build":
|
||||
log("builder INTERRUPTED mid-build — canonical state persists")
|
||||
return 3
|
||||
return finish_session(iface, contract_id, project_dir, before,
|
||||
session)
|
||||
|
||||
|
||||
def finish_session(iface, contract_id, project_dir, before,
|
||||
session) -> int:
|
||||
proj_text = (session["session_output"] or "")
|
||||
import re as _re
|
||||
m = None
|
||||
for m in _re.finditer(r"BUILDER_STATUS:\s*([A-Z]+)", proj_text):
|
||||
pass
|
||||
builder_status = (m.group(1) + " (final)") if m else "NOT_STATED"
|
||||
scope_audit = audit_scope(project_dir, before,
|
||||
list(EXPECTED_SCOPE))
|
||||
verification = independent_verification(project_dir)
|
||||
result = complete_lifecycle(iface, contract_id, scope_audit,
|
||||
verification, builder_status)
|
||||
summary = {
|
||||
"builder_model": BUILDER_MODEL,
|
||||
"builder_process": "mimo run (separate OS process)",
|
||||
"terminated_by": session["terminated_by"],
|
||||
"builder_claimed": builder_status,
|
||||
"scope_audit": scope_audit,
|
||||
"verification": {"cmd": verification["cmd"],
|
||||
"exit_code": verification["exit_code"]},
|
||||
"lifecycle": result,
|
||||
}
|
||||
print(json.dumps(summary, indent=1))
|
||||
return 0 if result.get("closed") else 1
|
||||
|
||||
|
||||
def cmd_resume(store: Path, project_dir: Path) -> int:
|
||||
"""14E: FRESH process. Canonical state ONLY — no transcript, no
|
||||
memory of the previous session."""
|
||||
found = discover_contract_id(store)
|
||||
assert found, "no contract found in canonical state"
|
||||
project, contract_id = found
|
||||
log(f"fresh builder resume: project={project} "
|
||||
f"contract={contract_id} (canonical only)")
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL + "-resumed",
|
||||
project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
context = iface.construction_context(contract_id)
|
||||
assert context["state"]["active"], "canonical state shows no active build"
|
||||
log(f"recovered: phase={context['state']['phase']} "
|
||||
f"scope={context['state']['scope']}")
|
||||
before = snapshot_files(project_dir)
|
||||
projection = build_projection(
|
||||
iface, contract_id,
|
||||
phase_note="resumed after real builder interruption")
|
||||
session = run_builder_process(
|
||||
project_dir, builder_prompt(projection, resumed=True),
|
||||
interrupt_after_first_file=False)
|
||||
return finish_session(iface, contract_id, project_dir, before,
|
||||
session)
|
||||
|
||||
|
||||
def cmd_status(store: Path) -> int:
|
||||
found = discover_contract_id(store)
|
||||
if not found:
|
||||
print(json.dumps({"active": False}))
|
||||
return 0
|
||||
project, contract_id = found
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="inspector",
|
||||
project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
print(json.dumps(iface.construction_context(contract_id)["state"],
|
||||
indent=1, default=str))
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("mode", choices=["start", "resume", "status"])
|
||||
ap.add_argument("--store", required=True)
|
||||
ap.add_argument("--project-dir", required=False)
|
||||
ap.add_argument("--interrupt-after-first-file", action="store_true")
|
||||
args = ap.parse_args()
|
||||
store, project_dir = Path(args.store).resolve(), \
|
||||
Path(args.project_dir).resolve() if args.project_dir else None
|
||||
project_dir.mkdir(parents=True, exist_ok=True) if project_dir else None
|
||||
if args.mode == "start":
|
||||
return cmd_start(store, project_dir,
|
||||
args.interrupt_after_first_file)
|
||||
if args.mode == "resume":
|
||||
return cmd_resume(store, project_dir)
|
||||
return cmd_status(store)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env python3
|
||||
"""t15_builder_session.py — T15 §15Q: REAL event-driven builder proof.
|
||||
|
||||
The difference from T14, enforced by construction:
|
||||
|
||||
launch → register WAITING_EVENT → YIELD (select on the bridge FIFO)
|
||||
→ builder exit event → resume immediately → reload canonical state
|
||||
→ readiness/exit canaries → independent verification → continuation
|
||||
|
||||
NO sleep/poll loops anywhere on the normal path. The watchdog is only a
|
||||
failure detector (lost event / dead process), never the scheduler.
|
||||
|
||||
Modes:
|
||||
start classify → activate → contract → real builder → event wait
|
||||
→ exit canary → verification → evidence-backed close
|
||||
interrupt like start, but the builder process group is SIGKILLed at
|
||||
the first source file → PROCESS_LOST → canonical state
|
||||
persists for a fresh builder (run `resume` next)
|
||||
resume FRESH controller + fresh builder from canonical state only
|
||||
status print canonical construction state
|
||||
|
||||
The builder is the real model process (mimo run — Verboo per the standing
|
||||
provider policy). Builder identity is recorded, never trusted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
import t14_builder_session as t14 # noqa: E402 — reuse the proven pieces
|
||||
from rune import construction_interface as ci # noqa: E402
|
||||
from rune import event_wait as ew # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune.event_wait import DEFAULT_DAEMON_URL # noqa: E402
|
||||
from rune.provider_context import ProviderUnresolved, require_model # noqa: E402
|
||||
|
||||
# T16 §16F: NO silent default provider — resolve from the ambient provider
|
||||
# context (T15_BUILDER_MODEL, falling back to MIMO_MODEL); loud failure
|
||||
# with exit code 3 when the provider layer has not selected one.
|
||||
try:
|
||||
BUILDER_MODEL = os.environ.get("T15_BUILDER_MODEL", "").strip() \
|
||||
or require_model()
|
||||
except ProviderUnresolved as _exc:
|
||||
import sys as _sys
|
||||
_sys.stderr.write(str(_exc) + "\n")
|
||||
raise SystemExit(3) # 3 = PROVIDER_UNRESOLVED (no silent fallback)
|
||||
DAEMON_URL = os.environ.get("T15_DAEMON_URL", DEFAULT_DAEMON_URL)
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
print(f"[t15] {msg}", flush=True)
|
||||
|
||||
|
||||
def readiness_canary(toolchain_hint: str) -> dict:
|
||||
"""§15F: may the build stage begin? Executable checks, not optimism.
|
||||
Runtime checks derive from the verifier (15R generalisation)."""
|
||||
checks = []
|
||||
runtimes = (["node"] if "node" in VERIFIER else []) + (["python3"] if "python" in VERIFIER else [])
|
||||
for rt in runtimes:
|
||||
r = subprocess.run([rt, "--version"], capture_output=True, text=True)
|
||||
checks.append({"name": f"{rt}_present", "passed": r.returncode == 0,
|
||||
"evidence": (r.stdout or "").strip()[:30]})
|
||||
r2 = subprocess.run(["mimo", "--version"], capture_output=True, text=True)
|
||||
checks.append({"name": "builder_cli_present", "passed": r2.returncode == 0,
|
||||
"evidence": (r2.stdout or "").strip()[:40]})
|
||||
try:
|
||||
import urllib.request
|
||||
with urllib.request.urlopen(DAEMON_URL + "/health", timeout=3) as resp:
|
||||
ok = resp.status == 200
|
||||
checks.append({"name": "event_bridge_healthy", "passed": ok,
|
||||
"evidence": DAEMON_URL})
|
||||
except Exception as e: # noqa: BLE001
|
||||
checks.append({"name": "event_bridge_healthy", "passed": False,
|
||||
"evidence": str(e)[:80]})
|
||||
verdict = "READY" if all(c["passed"] for c in checks) else "BLOCKED"
|
||||
return {"canary": "readiness", "verdict": verdict, "checks": checks}
|
||||
|
||||
|
||||
# 15R: the driver is construction INFRASTRUCTURE — the request/scope are
|
||||
# parameters, never scheduler-specific constants. Defaults preserve the
|
||||
# proven fixture.
|
||||
REQUEST = os.environ.get(
|
||||
"T15_REQUEST",
|
||||
"Build me a small standalone practice scheduler that can create, "
|
||||
"list and complete practice items.")
|
||||
SCOPE = [s for s in os.environ.get(
|
||||
"T15_SCOPE", "scheduler.js,scheduler.test.js,package.json").split(",")
|
||||
if s]
|
||||
PROJECT_NAME = os.environ.get("T15_PROJECT_NAME", "")
|
||||
SPEC_CONTRACT = os.environ.get("T15_SPEC_CONTRACT",
|
||||
"contract:practice-items-spec")
|
||||
TOOLCHAIN = os.environ.get("T15_TOOLCHAIN",
|
||||
"node >= 18 (node:test built-in runner, ES modules)")
|
||||
VERIFIER = os.environ.get("T15_VERIFIER", "node --test")
|
||||
|
||||
|
||||
def setup_construction(store: Path, project_dir: Path) -> tuple[str, str]:
|
||||
"""classify → activate → seed spec → contract → transitions. Reuses
|
||||
the proven T14 canonical path unchanged."""
|
||||
staged = t14.classify_and_activate(store, REQUEST)
|
||||
project = staged["activation"]["project"]
|
||||
assert staged["activation"]["mode"] == "PROJECT_BUILD"
|
||||
_seed_spec(store, project)
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL,
|
||||
project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
sub = iface.submit_need(
|
||||
REQUEST, origin="rune_native",
|
||||
constraints={"environment": {"os": "linux",
|
||||
"runtime": TOOLCHAIN,
|
||||
"runner": VERIFIER},
|
||||
"contracts": [SPEC_CONTRACT]},
|
||||
affected_surface=list(SCOPE),
|
||||
evidence_refs=["t15-user-request", "node-v22-installed"],
|
||||
approval_context="t15 user-approved fixture")
|
||||
assert sub.get("success"), sub.get("errors")
|
||||
contract_id = sub["ids"]["contract_id"]
|
||||
for phase in ("approved", "building"):
|
||||
tr = iface.transition(contract_id, phase, evidence_ref=f"t15 {phase}")
|
||||
assert tr.get("success"), tr.get("errors")
|
||||
log(f"contract {contract_id} → building")
|
||||
return project, contract_id
|
||||
|
||||
|
||||
def _seed_spec(store: Path, project: str) -> str:
|
||||
"""Govern the requirement as a contract on the project surface
|
||||
(generic — request/scope come from parameters)."""
|
||||
proj = sc.Scaffold(str(store / "projects" / project), project)
|
||||
doc = proj.load()
|
||||
if ss.entity(doc, SPEC_CONTRACT) is None:
|
||||
ss.add_entity(doc, id=SPEC_CONTRACT, kind="contract",
|
||||
identities={"objective": REQUEST},
|
||||
evidence_refs=["t15-request"])
|
||||
ss.add_entity(doc, id="capability:planned-" + project[:24],
|
||||
kind="capability", maturity="DECLARED",
|
||||
evidence_refs=["t15-request"])
|
||||
ss.add_relationship(doc, type="implements",
|
||||
from_id="capability:planned-" + project[:24],
|
||||
to_id=SPEC_CONTRACT, evidence_refs=["t15-request"])
|
||||
proj.commit(doc, what="seed requirement spec (user request)")
|
||||
return SPEC_CONTRACT
|
||||
|
||||
|
||||
def run_build_via_events(project_dir: Path, operation_id: str,
|
||||
projection: dict, contract_id: str,
|
||||
interrupt_after_first_file: bool) -> dict:
|
||||
"""THE 15Q core: real builder under the event notifier; the driver
|
||||
YIELDS on the bridge FIFO. No sleep. Watchdog = failure detector."""
|
||||
ew.register_process_wait(
|
||||
operation_id, contract_id=contract_id,
|
||||
project="t15-fixture", objective=REQUEST,
|
||||
watched_for="builder process exit event", watchdog_secs=900)
|
||||
# op marker rides the prompt (last argv) so cmdline liveness can find
|
||||
# the build even if node regroups its children (T15Q live finding)
|
||||
builder_cmd = ["mimo", "run", "-m", BUILDER_MODEL, "--format", "json",
|
||||
t14.builder_prompt(projection, resumed=False)
|
||||
+ f" [op:{operation_id}]"]
|
||||
notifier = ew.run_builder_under_events(builder_cmd, operation_id,
|
||||
daemon_url=DAEMON_URL)
|
||||
log(f"builder launched (pgid {notifier.pid}); driver YIELDS on the "
|
||||
f"event bridge — no polling")
|
||||
if interrupt_after_first_file:
|
||||
# kill at first source file: the interruption is real, the event
|
||||
# never arrives (the notifier dies with the group) — the watchdog
|
||||
# must classify PROCESS_LOST, never silently wait.
|
||||
# TEST_TIMING: this 0.5s poll exists ONLY on the deliberate
|
||||
# interrupt path (killing at a checkpoint), never on the normal
|
||||
# continuation path.
|
||||
while True:
|
||||
files = [p for p in project_dir.iterdir()
|
||||
if p.suffix in (".mjs", ".js", ".json")]
|
||||
if files:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
log("INTERRUPTING builder (first file landed) — SIGKILL group")
|
||||
os.killpg(notifier.pid, signal.SIGKILL)
|
||||
result = ew.await_resume_event(operation_id, watchdog_secs=900,
|
||||
pgid=notifier.pid)
|
||||
return result
|
||||
|
||||
|
||||
def finish_via_canaries(store: Path, project: str, contract_id: str,
|
||||
project_dir: Path, before: dict,
|
||||
resume_note: str) -> int:
|
||||
"""Reload canonical state → scope audit → INDEPENDENT verification
|
||||
(15G test truth) → evidence-backed lifecycle."""
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL, project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
ctx = iface.construction_context(contract_id)
|
||||
assert ctx["state"]["active"] or ctx["state"]["phase"] == "closed"
|
||||
log(f"canonical state: phase={ctx['state']['phase']} "
|
||||
f"scope={ctx['state']['scope']}")
|
||||
# exit canary: scope audit with provenance-aware residue (15O)
|
||||
audit = t14.audit_scope(project_dir, before, list(SCOPE))
|
||||
# 15G: verification truth — node --test must actually run tests
|
||||
# 15G test truth via the CONTRACT's verifier (parameterized — 15R
|
||||
# generalisation: python unittest/pytest, node --test, etc.)
|
||||
truth = subprocess.run(
|
||||
["bash", "-c", VERIFIER], cwd=str(project_dir),
|
||||
capture_output=True, text=True, timeout=300)
|
||||
import re
|
||||
count = None
|
||||
for pat in (r"# pass (\d+)", # node --test TAP summary
|
||||
r"(\d+) passed", # pytest
|
||||
r"Ran (\d+) tests"): # python unittest
|
||||
m = re.search(pat, (truth.stdout or "") + (truth.stderr or ""))
|
||||
if m:
|
||||
count = int(m.group(1))
|
||||
break
|
||||
test_count = count if count is not None else 0
|
||||
log(f"verification: exit={truth.returncode} tests={test_count}")
|
||||
if test_count == 0:
|
||||
log("FALSE GREEN REFUSED: zero tests executed (15G)")
|
||||
return 1
|
||||
if audit["scope_violations"]:
|
||||
log(f"closure REFUSED: scope violations {audit['scope_violations']}")
|
||||
return 1
|
||||
builder_status = "UNKNOWN"
|
||||
result = t14.complete_lifecycle(iface, contract_id, audit,
|
||||
{"cmd": "node --test",
|
||||
"exit_code": truth.returncode},
|
||||
builder_status)
|
||||
summary = {
|
||||
"event_driven": True,
|
||||
"builder_model": BUILDER_MODEL,
|
||||
"resume_note": resume_note,
|
||||
"scope_audit": audit,
|
||||
"verification": {"exit_code": truth.returncode,
|
||||
"test_count": test_count},
|
||||
"lifecycle": result,
|
||||
}
|
||||
print(json.dumps(summary, indent=1))
|
||||
return 0 if result.get("closed") else 1
|
||||
|
||||
|
||||
def discover(store: Path) -> tuple[str, str] | None:
|
||||
"""§15U: delegates to the canonical interface discovery (via the t14
|
||||
shim — one hop, no duplication)."""
|
||||
return t14.discover_contract_id(store)
|
||||
|
||||
|
||||
def cmd_start(store: Path, project_dir: Path, interrupt: bool) -> int:
|
||||
ready = readiness_canary("node")
|
||||
assert ready["verdict"] == "READY", f"readiness BLOCKED: {ready['checks']}"
|
||||
log(f"readiness READY ({len(ready['checks'])} checks)")
|
||||
before = t14.snapshot_files(project_dir)
|
||||
project, contract_id = setup_construction(store, project_dir)
|
||||
operation_id = f"t15-{contract_id}"
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL, project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
projection = t14.build_projection(iface, contract_id,
|
||||
phase_note="initial build")
|
||||
result = run_build_via_events(project_dir, operation_id, projection,
|
||||
contract_id, interrupt)
|
||||
if result["kind"] == "watchdog":
|
||||
# §15E canonical-state-wins: process-table absence is NOT truth
|
||||
# (detached builders re-parent their workers — T15Q live finding).
|
||||
# The durable waitstore decides: a recorded verdict means the
|
||||
# builder completed and the event WAS processed.
|
||||
rec = ew.resume_or_recover(operation_id)
|
||||
if rec["kind"] == "event":
|
||||
log(f"watchdog fired but canonical state shows the build "
|
||||
f"completed (verdict={rec['wait'].get('verdict')}) — "
|
||||
f"continuing from authority")
|
||||
return finish_via_canaries(store, project, contract_id,
|
||||
project_dir, before,
|
||||
"watchdog→canonical recovery")
|
||||
log("PROCESS_LOST: no event, no durable verdict — fresh builder "
|
||||
"resumes from authority only")
|
||||
return 3
|
||||
wait = result.get("wait", {})
|
||||
log(f"event-driven resume: verdict={wait.get('verdict')} "
|
||||
f"exit={wait.get('process_event_evidence', {}).get('exit_code')}")
|
||||
return finish_via_canaries(store, project, contract_id, project_dir,
|
||||
before, "event-driven completion")
|
||||
|
||||
|
||||
def cmd_resume(store: Path, project_dir: Path) -> int:
|
||||
found = discover(store)
|
||||
assert found, "no contract in canonical state"
|
||||
project, contract_id = found
|
||||
log(f"FRESH builder resume: {project}/{contract_id} (canonical only)")
|
||||
before = t14.snapshot_files(project_dir)
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL + "-resumed",
|
||||
project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
projection = t14.build_projection(
|
||||
iface, contract_id, phase_note="resumed after real interruption")
|
||||
operation_id = f"t15-resume-{contract_id}"
|
||||
ew.register_process_wait(
|
||||
operation_id, contract_id=contract_id, project=project,
|
||||
objective=REQUEST, watched_for="builder exit event",
|
||||
watchdog_secs=900)
|
||||
builder_cmd = ["mimo", "run", "-m", BUILDER_MODEL, "--format", "json",
|
||||
t14.builder_prompt(projection, resumed=True)
|
||||
+ f" [op:{operation_id}]"]
|
||||
notifier = ew.run_builder_under_events(builder_cmd, operation_id,
|
||||
daemon_url=DAEMON_URL)
|
||||
log("driver YIELDS on the bridge — no polling")
|
||||
result = ew.await_resume_event(operation_id, watchdog_secs=900,
|
||||
pgid=notifier.pid)
|
||||
if result["kind"] == "watchdog":
|
||||
rec = ew.resume_or_recover(operation_id)
|
||||
if rec["kind"] == "event":
|
||||
log(f"watchdog fired but canonical state shows the resume "
|
||||
f"build completed (verdict={rec['wait'].get('verdict')})")
|
||||
return finish_via_canaries(store, project, contract_id,
|
||||
project_dir, before,
|
||||
"resume via canonical recovery")
|
||||
log("PROCESS_LOST on resume — UNKNOWN_INTERRUPTION")
|
||||
return 4
|
||||
return finish_via_canaries(store, project, contract_id, project_dir,
|
||||
before, "fresh-builder event-driven resume")
|
||||
|
||||
|
||||
def cmd_finish(store: Path, project_dir: Path) -> int:
|
||||
"""Complete lifecycle from CANONICAL state only (waitstore verdict +
|
||||
filesystem reality). Used after a watchdog→canonical recovery."""
|
||||
found = discover(store)
|
||||
assert found, "no contract in canonical state"
|
||||
project, contract_id = found
|
||||
before = t14.snapshot_files(project_dir)
|
||||
return finish_via_canaries(store, project, contract_id, project_dir,
|
||||
before, "canonical-only completion")
|
||||
|
||||
|
||||
def cmd_status(store: Path) -> int:
|
||||
found = discover(store)
|
||||
if not found:
|
||||
print(json.dumps({"active": False}))
|
||||
return 0
|
||||
project, contract_id = found
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="inspector", project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
print(json.dumps(iface.construction_context(contract_id)["state"],
|
||||
indent=1, default=str))
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("mode", choices=["start", "resume", "finish",
|
||||
"status"])
|
||||
ap.add_argument("--store", required=True)
|
||||
ap.add_argument("--project-dir", required=False)
|
||||
ap.add_argument("--interrupt-after-first-file", action="store_true")
|
||||
args = ap.parse_args()
|
||||
store = Path(args.store).resolve()
|
||||
project_dir = (Path(args.project_dir).resolve()
|
||||
if args.project_dir else None)
|
||||
if project_dir:
|
||||
project_dir.mkdir(parents=True, exist_ok=True)
|
||||
if args.mode == "start":
|
||||
return cmd_start(store, project_dir,
|
||||
args.interrupt_after_first_file)
|
||||
if args.mode == "resume":
|
||||
return cmd_resume(store, project_dir)
|
||||
if args.mode == "finish":
|
||||
return cmd_finish(store, project_dir)
|
||||
return cmd_status(store)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -6,6 +6,7 @@ New code must be type-clean; existing code improves as debt is paid down.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -23,12 +24,35 @@ def main() -> int:
|
||||
# not work on all platforms). Fall back to sys.executable -m.
|
||||
pyright_bin = Path(sys.executable).parent / "pyright"
|
||||
cmd = [str(pyright_bin)] if pyright_bin.is_file() else [sys.executable, "-m", "pyright"]
|
||||
# §14H LOCAL-VS-CI NORMALIZATION (proven 2026-09-15): pyright resolves
|
||||
# the python environment for import analysis, and a developer's
|
||||
# user-site packages (~/.local/lib/pythonX/site-packages) leak into it
|
||||
# — nick: 124 errors, runner user: 117 on the IDENTICAL checkout+venv.
|
||||
# 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.
|
||||
# §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,
|
||||
text=True,
|
||||
timeout=600,
|
||||
cwd=str(ROOT),
|
||||
env=env,
|
||||
)
|
||||
data = json.loads(result.stdout)
|
||||
summary = data.get("summary", {})
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.48"
|
||||
__version__ = "0.66.78"
|
||||
|
||||
# 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
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
"""behaviour_ledger.py — T19 §19F/19G/19I: the canonical Behaviour
|
||||
Validation Ledger.
|
||||
|
||||
PRIMARY UNIT: the VERSIONED BEHAVIOUR / TEST SPECIFICATION — not "a
|
||||
test run in some session". Automated verification and real-world/user
|
||||
confirmation are DISTINCT truths (rule 19F): tests green ≠ user-
|
||||
confirmed.
|
||||
|
||||
GENERATION-AWARE HISTORY (rule 19G, mandatory): when a dependency
|
||||
changes, affected behaviours become RETEST_REQUIRED while their prior
|
||||
confirmation history stays queryable — confidence does not silently
|
||||
carry forward, and it does not silently disappear either.
|
||||
|
||||
SELECTIVE INVALIDATION (rule 19I): only behaviours whose structural
|
||||
dependencies changed are invalidated; every invalidation carries a
|
||||
reason; unrelated hardened confirmations are preserved.
|
||||
|
||||
Storage: <project_store>/behaviour_ledger.json (versioned document).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
LEDGER_SCHEMA_VERSION = 1
|
||||
|
||||
# hardening lifecycle (rule 19G semantics)
|
||||
UNVALIDATED = "UNVALIDATED" # not yet validated
|
||||
IMPLEMENTED = "IMPLEMENTED" # implementation exists
|
||||
AUTO_VERIFIED = "AUTO_VERIFIED" # automated verification passed
|
||||
REAL_WORLD_PASSED = "REAL_WORLD_PASSED" # manual/real test passed
|
||||
TEST_FAILED = "TEST_FAILED" # test failed
|
||||
PARTIAL = "PARTIAL" # inconclusive
|
||||
USER_CONFIRMED = "USER_CONFIRMED" # hardened
|
||||
RETEST_REQUIRED = "RETEST_REQUIRED" # confidence invalidated
|
||||
ACCEPTED_LIMITATION = "ACCEPTED_LIMITATION" # explicitly approved gap
|
||||
NOT_APPLICABLE = "NOT_APPLICABLE"
|
||||
|
||||
STATES = (UNVALIDATED, IMPLEMENTED, AUTO_VERIFIED, REAL_WORLD_PASSED,
|
||||
TEST_FAILED, PARTIAL, USER_CONFIRMED, RETEST_REQUIRED,
|
||||
ACCEPTED_LIMITATION, NOT_APPLICABLE)
|
||||
|
||||
# states that represent STANDING CONFIDENCE (selective invalidation
|
||||
# may reopen exactly these; everything else is already open or n/a)
|
||||
CONFIRMED_STATES = (AUTO_VERIFIED, REAL_WORLD_PASSED, USER_CONFIRMED)
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
class BehaviourLedger:
|
||||
def __init__(self, store_dir: Path | str):
|
||||
self.path = Path(store_dir) / "behaviour_ledger.json"
|
||||
self.doc = self._load()
|
||||
|
||||
def _load(self) -> dict:
|
||||
if self.path.is_file():
|
||||
import json
|
||||
return json.loads(self.path.read_text())
|
||||
return {"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"behaviours": {}, "updated_at": _now()}
|
||||
|
||||
def save(self) -> None:
|
||||
import json
|
||||
self.doc["updated_at"] = _now()
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.path.write_text(json.dumps(self.doc, indent=1, sort_keys=True))
|
||||
|
||||
# -- specification ------------------------------------------------------
|
||||
|
||||
def define(self, *, behaviour_id: str, owning_project: str,
|
||||
intended_behaviour: str, origin_intent: str = "",
|
||||
pass_criteria: str = "", fail_criteria: str = "",
|
||||
procedure: str = "",
|
||||
owner_component: str = "",
|
||||
dependencies: list[str] | None = None,
|
||||
automated_ref: str = "",
|
||||
environment: str = "") -> dict:
|
||||
bid = (behaviour_id if behaviour_id.startswith("behaviour:")
|
||||
else f"behaviour:{behaviour_id}")
|
||||
if bid in self.doc["behaviours"]:
|
||||
raise ValueError(f"behaviour exists: {bid}")
|
||||
rec = {
|
||||
"behaviour_id": bid,
|
||||
"owning_project": owning_project,
|
||||
"intended_behaviour": intended_behaviour,
|
||||
"origin_intent": origin_intent,
|
||||
"pass_criteria": pass_criteria,
|
||||
"fail_criteria": fail_criteria,
|
||||
"procedure": procedure,
|
||||
"owner_component": owner_component, # structural entity id
|
||||
"dependencies": list(dependencies or []), # structural ids
|
||||
"automated_ref": automated_ref,
|
||||
"environment": environment,
|
||||
"state": UNVALIDATED,
|
||||
"evidence": [], # [{at, generation, kind, detail}]
|
||||
"history": [], # every state transition, never lost
|
||||
"invalidations": [], # [{at, generation, because}]
|
||||
"limitations": [],
|
||||
"created_at": _now(),
|
||||
"updated_at": _now(),
|
||||
}
|
||||
self.doc["behaviours"][bid] = rec
|
||||
return rec
|
||||
|
||||
def get(self, behaviour_id: str) -> dict | None:
|
||||
return self.doc["behaviours"].get(
|
||||
behaviour_id if behaviour_id.startswith("behaviour:")
|
||||
else f"behaviour:{behaviour_id}")
|
||||
|
||||
# -- state transitions (all recorded in history — rule 19G) -------------
|
||||
|
||||
def _transition(self, rec: dict, new_state: str, *, kind: str,
|
||||
detail: str = "", generation: str = "") -> None:
|
||||
if new_state not in STATES:
|
||||
raise ValueError(f"unknown validation state {new_state!r}")
|
||||
rec["history"].append({
|
||||
"at": _now(), "from": rec["state"], "to": new_state,
|
||||
"kind": kind, "detail": detail[:200],
|
||||
"generation": generation})
|
||||
rec["state"] = new_state
|
||||
rec["updated_at"] = _now()
|
||||
|
||||
def record_auto_verification(self, behaviour_id: str, *,
|
||||
passed: bool, detail: str = "",
|
||||
generation: str = "") -> dict:
|
||||
"""Automated verification. GREEN NEVER IMPLIES USER_CONFIRMED
|
||||
(rule 19W-22): the ceiling here is AUTO_VERIFIED."""
|
||||
rec = self.get(behaviour_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown behaviour {behaviour_id!r}")
|
||||
rec["evidence"].append({"at": _now(), "kind": "automated",
|
||||
"passed": passed,
|
||||
"detail": detail[:200],
|
||||
"generation": generation})
|
||||
self._transition(rec, AUTO_VERIFIED if passed else TEST_FAILED,
|
||||
kind="automated_verification", detail=detail,
|
||||
generation=generation)
|
||||
return rec
|
||||
|
||||
def record_real_world(self, behaviour_id: str, *, passed: bool,
|
||||
detail: str = "", generation: str = "",
|
||||
confirmed_by: str = "user") -> dict:
|
||||
"""Real-world/manual test result (testing mode). USER_CONFIRMED
|
||||
requires an explicit confirmed_by='user' pass (rule 19M)."""
|
||||
rec = self.get(behaviour_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown behaviour {behaviour_id!r}")
|
||||
rec["evidence"].append({"at": _now(), "kind": "real_world",
|
||||
"passed": passed,
|
||||
"detail": detail[:200],
|
||||
"generation": generation,
|
||||
"by": confirmed_by})
|
||||
if passed:
|
||||
state = (USER_CONFIRMED if confirmed_by == "user"
|
||||
else REAL_WORLD_PASSED)
|
||||
else:
|
||||
state = TEST_FAILED
|
||||
self._transition(rec, state, kind="real_world", detail=detail,
|
||||
generation=generation)
|
||||
return rec
|
||||
|
||||
def accept_limitation(self, behaviour_id: str, *, note: str,
|
||||
approved_by: str = "user") -> dict:
|
||||
rec = self.get(behaviour_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown behaviour {behaviour_id!r}")
|
||||
rec["limitations"].append({"at": _now(), "note": note[:200],
|
||||
"approved_by": approved_by})
|
||||
self._transition(rec, ACCEPTED_LIMITATION, kind="accept_limitation",
|
||||
detail=note, )
|
||||
return rec
|
||||
|
||||
# -- selective invalidation (rule 19I) -----------------------------------
|
||||
|
||||
def invalidate_for_change(self, *, changed_entities: set[str],
|
||||
generation: str = "",
|
||||
reasons: dict[str, str] | None = None
|
||||
) -> list[dict]:
|
||||
"""Mark RETEST_REQUIRED exactly those confirmed behaviours whose
|
||||
structural dependencies intersect the changed closure. Every
|
||||
invalidation carries a reason; unrelated confirmations stay.
|
||||
Returns the invalidated records."""
|
||||
reasons = reasons or {}
|
||||
invalidated = []
|
||||
for rec in self.doc["behaviours"].values():
|
||||
if rec["state"] not in CONFIRMED_STATES:
|
||||
continue
|
||||
overlap = set(rec.get("dependencies") or []) & changed_entities
|
||||
if rec.get("owner_component") in changed_entities:
|
||||
overlap.add(rec["owner_component"])
|
||||
if not overlap:
|
||||
continue # unrelated hardened behaviour: PRESERVED (19S-5)
|
||||
because = reasons.get(
|
||||
rec["behaviour_id"],
|
||||
f"structural change touched: {sorted(overlap)}")
|
||||
rec["invalidations"].append({
|
||||
"at": _now(), "generation": generation,
|
||||
"because": because[:200],
|
||||
"was": rec["state"],
|
||||
"changed_entities": sorted(overlap)})
|
||||
# HISTORY IS PRESERVED — prior confirmation stays queryable
|
||||
self._transition(rec, RETEST_REQUIRED,
|
||||
kind="selective_invalidation",
|
||||
detail=f"was {rec['state']}; {because}",
|
||||
generation=generation)
|
||||
invalidated.append(rec)
|
||||
return invalidated
|
||||
|
||||
def retest_history(self, behaviour_id: str) -> list[dict]:
|
||||
"""Generation-aware history: all confirmations + invalidations
|
||||
for one behaviour, oldest first (rule 19S-12)."""
|
||||
rec = self.get(behaviour_id)
|
||||
if rec is None:
|
||||
return []
|
||||
events = []
|
||||
for h in rec["history"]:
|
||||
events.append({"at": h["at"], "generation": h.get("generation"),
|
||||
"event": f"{h['from']}→{h['to']}",
|
||||
"kind": h["kind"], "detail": h.get("detail", "")})
|
||||
return events
|
||||
|
||||
# -- queue (testing mode, 19L/M) ------------------------------------------
|
||||
|
||||
def outstanding(self, *, component: str | None = None,
|
||||
project: str | None = None) -> list[dict]:
|
||||
"""Eligible testing items: anything not confirmed/accepted/n.a.
|
||||
Dynamic — the queue re-derives from states after every fix
|
||||
(rule 19M)."""
|
||||
out = []
|
||||
for rec in self.doc["behaviours"].values():
|
||||
if rec["state"] in (USER_CONFIRMED, ACCEPTED_LIMITATION,
|
||||
NOT_APPLICABLE):
|
||||
continue
|
||||
if project and rec.get("owning_project") != project:
|
||||
continue
|
||||
if component and rec.get("owner_component") != component:
|
||||
continue
|
||||
out.append(rec)
|
||||
out.sort(key=lambda r: (0 if r["state"] == TEST_FAILED else 1,
|
||||
r["behaviour_id"]))
|
||||
return out
|
||||
|
||||
def summary(self) -> dict:
|
||||
counts: dict[str, int] = {}
|
||||
for rec in self.doc["behaviours"].values():
|
||||
counts[rec["state"]] = counts.get(rec["state"], 0) + 1
|
||||
return {"total": len(self.doc["behaviours"]),
|
||||
"states": counts,
|
||||
"outstanding": len(self.outstanding())}
|
||||
@@ -0,0 +1,287 @@
|
||||
"""build_basis.py — T18 §18C: durable versioned RESEARCH BASIS for
|
||||
construction (V1 rule F: RESEARCH INFORMS STRUCTURE).
|
||||
|
||||
Research output must not disappear in transient model context. A Build
|
||||
Basis is a durable, schema-versioned document that captures WHAT
|
||||
research decided and WHY, with references — not research dumps. It is
|
||||
created after the research-depth decision and before/with the
|
||||
Construction Contract, and is attachable to:
|
||||
|
||||
- a Project Scaffold (project-level basis)
|
||||
- a Construction Contract (per-contract basis, via evidence_ref)
|
||||
- future structural entities where useful
|
||||
|
||||
Research Core ownership (rule C): this module NEVER researches. It
|
||||
records the depth decision (derived FROM adaptive_router's route — no
|
||||
second router), references the canonical pipeline's qualified output
|
||||
(stages ISSUE→…→QUALIFY, artifacts in research/issue-<N>/), and
|
||||
structures the adopt/adapt/combine/reject/innovate decisions (rule E).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
# Research depth tiers — derived from adaptive_router ROUTES, never a
|
||||
# second router (rule C/A). Each tier records WHY it was chosen.
|
||||
DEPTH_NONE = "NONE" # tiny obvious local change; local evidence only
|
||||
DEPTH_INWARD = "INWARD" # repo-internal understanding suffices
|
||||
DEPTH_OUTWARD = "OUTWARD" # precedent/prior-art discovery required
|
||||
DEPTH_FULL = "FULL" # full pipeline incl. adversarial+synthesis
|
||||
|
||||
DEPTH_TIERS = (DEPTH_NONE, DEPTH_INWARD, DEPTH_OUTWARD, DEPTH_FULL)
|
||||
|
||||
# route → default depth (recorded as decision evidence; caller may
|
||||
# justify an exception, which is recorded as such — never silent)
|
||||
_ROUTE_DEFAULT_DEPTH = {
|
||||
"LOCAL_FIX": DEPTH_NONE,
|
||||
"STANDARD_CHANGE": DEPTH_INWARD,
|
||||
"RESEARCHED_CHANGE": DEPTH_FULL,
|
||||
}
|
||||
|
||||
|
||||
def research_depth(route: str, *, mode: str = "",
|
||||
justification: str = "") -> dict:
|
||||
"""The research-depth DECISION for a construction need. Derived from
|
||||
the canonical route authority (adaptive_router); `justification` is
|
||||
REQUIRED when deviating from the route default — recorded, never
|
||||
silent (rule D: record why research depth was chosen)."""
|
||||
default = _ROUTE_DEFAULT_DEPTH.get(route, DEPTH_INWARD)
|
||||
depth = default
|
||||
why = f"route {route} default"
|
||||
if justification and justification.strip():
|
||||
# an explicit justification selects a tier by name if given;
|
||||
# otherwise it annotates the default
|
||||
candidate = justification.strip().split(":", 1)[0].strip()
|
||||
if candidate in DEPTH_TIERS:
|
||||
depth = candidate
|
||||
why = justification.strip()
|
||||
else:
|
||||
why = f"route {route} default; note: {justification.strip()}"
|
||||
elif route not in _ROUTE_DEFAULT_DEPTH:
|
||||
why = f"route {route!r} unknown — INWARD floor (never silent NONE)"
|
||||
depth = DEPTH_INWARD
|
||||
return {"depth": depth, "why": why, "route": route, "mode": mode,
|
||||
"decided_at": _now()}
|
||||
|
||||
|
||||
# OUTWARD precedent-first ordering contract (rule E): the questions the
|
||||
# prior-art pass MUST answer before deeper material is consulted.
|
||||
OUTWARD_PRIOR_ART_QUESTIONS = (
|
||||
"what already exists addressing this intent",
|
||||
"what open-source systems/products/workflows attempt it",
|
||||
"what architectural patterns already work",
|
||||
"what standards/conventions exist",
|
||||
"what can be directly ADOPTED",
|
||||
"what can be ADAPTED",
|
||||
"what can be COMBINED",
|
||||
"what should be REJECTED",
|
||||
"where is genuine INNOVATION required",
|
||||
)
|
||||
|
||||
_DISPOSITIONS = ("ADOPT", "ADAPT", "COMBINE", "REJECT", "INNOVATE")
|
||||
|
||||
|
||||
def new_basis(*, question: str, depth_decision: dict,
|
||||
issue_ref: str = "") -> dict:
|
||||
"""Create an empty-but-valid Build Basis. References only — full
|
||||
research dumps live in the Research Core's artifact dirs (rule 18C:
|
||||
do not store unnecessary dumps)."""
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"kind": "build_basis",
|
||||
"created_at": _now(),
|
||||
"question": question,
|
||||
"issue_ref": issue_ref, # e.g. research/issue-164 or Gitea issue
|
||||
"depth": depth_decision,
|
||||
"inward": {"findings": [], "refs": []},
|
||||
"outward": {"prior_art_questions_answered": [],
|
||||
"findings": [], "refs": []},
|
||||
"deeper": {"used": False, "findings": [], "refs": []},
|
||||
"dispositions": [], # [{item, disposition: ADOPT/…, why, ref}]
|
||||
"alternatives_considered": [],
|
||||
"uncertainty": [],
|
||||
"risks": [],
|
||||
"implications": {"architecture": [], "structural": [],
|
||||
"migration": [], "verification": []},
|
||||
}
|
||||
|
||||
|
||||
def set_dispositions(basis: dict, dispositions: list[dict]) -> list[str]:
|
||||
"""Record the adopt/adapt/combine/reject/innovate decisions (rule E).
|
||||
Returns validation errors; the basis stays unqualified until valid."""
|
||||
basis["dispositions"] = [
|
||||
{"item": d.get("item", ""), "disposition": d.get("disposition", ""),
|
||||
"why": d.get("why", ""), "ref": d.get("ref", "")}
|
||||
for d in dispositions
|
||||
]
|
||||
return validate_basis(basis)
|
||||
|
||||
|
||||
def validate_basis(basis: dict) -> list[str]:
|
||||
"""Qualification gate. A basis is QUALIFIED when it carries a depth
|
||||
decision with why, at least one outward prior-art answer when depth
|
||||
is OUTWARD/FULL, and every disposition uses a legal disposition."""
|
||||
errors: list[str] = []
|
||||
if basis.get("schema_version") != SCHEMA_VERSION:
|
||||
errors.append(f"schema_version {basis.get('schema_version')!r}")
|
||||
depth = (basis.get("depth") or {}).get("depth")
|
||||
if depth not in DEPTH_TIERS:
|
||||
errors.append(f"depth {depth!r} not in {DEPTH_TIERS}")
|
||||
if not (basis.get("depth") or {}).get("why"):
|
||||
errors.append("depth decision lacks WHY (rule D)")
|
||||
if depth in (DEPTH_OUTWARD, DEPTH_FULL):
|
||||
outward = basis.get("outward") or {}
|
||||
if not (outward.get("prior_art_questions_answered")
|
||||
or outward.get("findings") or outward.get("refs")):
|
||||
errors.append("OUTWARD/FULL depth requires prior-art evidence")
|
||||
if depth == DEPTH_INWARD:
|
||||
inward = basis.get("inward") or {}
|
||||
if not (inward.get("findings") or inward.get("refs")
|
||||
or basis.get("dispositions")):
|
||||
errors.append("INWARD depth requires recorded evidence "
|
||||
"(findings/refs) — an empty basis is not "
|
||||
"qualified")
|
||||
for d in basis.get("dispositions", []):
|
||||
if d.get("disposition") not in _DISPOSITIONS:
|
||||
errors.append(f"illegal disposition {d.get('disposition')!r} "
|
||||
f"for {d.get('item','')[:40]!r}")
|
||||
return errors
|
||||
|
||||
|
||||
def is_qualified(basis: dict) -> bool:
|
||||
return not validate_basis(basis)
|
||||
|
||||
|
||||
def attach_evidence_ref(basis: dict) -> str:
|
||||
"""The evidence_ref string a contract carries to point at this basis
|
||||
(durable pointer, not a copy — stores stay independent)."""
|
||||
return (f"build_basis:{basis.get('issue_ref') or 'adhoc'}:"
|
||||
f"{basis.get('created_at')}")
|
||||
|
||||
|
||||
def summary(basis: dict) -> dict:
|
||||
"""Compact projection for prompts/previews (no private reasoning —
|
||||
structured conclusions only, rule 18G)."""
|
||||
return {
|
||||
"schema_version": basis.get("schema_version"),
|
||||
"question": basis.get("question"),
|
||||
"issue_ref": basis.get("issue_ref"),
|
||||
"depth": basis.get("depth"),
|
||||
"qualified": is_qualified(basis),
|
||||
"validation": validate_basis(basis),
|
||||
"dispositions": [
|
||||
{"item": d.get("item", "")[:80],
|
||||
"disposition": d.get("disposition")}
|
||||
for d in basis.get("dispositions", [])
|
||||
],
|
||||
"uncertainty_count": len(basis.get("uncertainty", [])),
|
||||
"risks_count": len(basis.get("risks", [])),
|
||||
"implications": {k: len(v) for k, v in
|
||||
(basis.get("implications") or {}).items()},
|
||||
}
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def to_json(basis: dict) -> str:
|
||||
import json
|
||||
return json.dumps(basis, indent=1, sort_keys=True)
|
||||
|
||||
|
||||
def from_json(text: str) -> dict:
|
||||
import json
|
||||
return json.loads(text)
|
||||
|
||||
|
||||
_ = Any # typing import kept for forward annotations
|
||||
|
||||
|
||||
# ── T18 §18D CORRECTION (Nick, 2026-09-15): THE RESEARCH PLAN ITSELF IS
|
||||
# CONTEXTUALLY GENERATED. The prior-art categories researched for the
|
||||
# structural-model task (architecture graphs, monorepo tooling, etc.)
|
||||
# were CONTEXTUAL to that task — they must never become a fixed topic
|
||||
# list. For every request the plan is derived from:
|
||||
# intent → target/current reality → material unknowns/decisions/risks
|
||||
# → context-specific research topics → proportional depth.
|
||||
# There is NO permanent research-topic checklist anywhere in Scaffolding.
|
||||
|
||||
def generate_research_plan(*, intent: str, target: str = "",
|
||||
current_reality: str = "",
|
||||
unknowns: list[str] | None = None,
|
||||
decisions: list[str] | None = None,
|
||||
risks: list[str] | None = None,
|
||||
depth: str | None = None) -> dict:
|
||||
"""Generate a CONTEXT-SPECIFIC research plan for one construction
|
||||
intent. Topics are synthesized from the material unknowns/decisions/
|
||||
risks of THIS intent — the function has no topic vocabulary and no
|
||||
permanent category list. Depth, when not supplied, is chosen by
|
||||
consequence heuristics recorded in the plan (why is always present)."""
|
||||
unknowns = [u for u in (unknowns or []) if u.strip()]
|
||||
decisions = [d for d in (decisions or []) if d.strip()]
|
||||
risks = [r for r in (risks or []) if r.strip()]
|
||||
topics: list[dict] = []
|
||||
# each material unknown becomes a research topic aimed at resolving it
|
||||
for u in unknowns:
|
||||
topics.append({
|
||||
"topic": f"resolve unknown: {u}",
|
||||
"kind": "unknown-resolution",
|
||||
"why": f"material unknown blocks confident construction: {u[:80]}",
|
||||
})
|
||||
for d in decisions:
|
||||
topics.append({
|
||||
"topic": f"inform decision: {d}",
|
||||
"kind": "decision-support",
|
||||
"why": f"consequential decision needs precedent/evidence: {d[:80]}",
|
||||
})
|
||||
for r in risks:
|
||||
topics.append({
|
||||
"topic": f"de-risk: {r}",
|
||||
"kind": "risk-investigation",
|
||||
"why": f"identified risk needs verification/mitigation evidence: {r[:80]}",
|
||||
})
|
||||
# precedent question (rule E) attaches ONLY when outward evidence is
|
||||
# plausibly relevant — an unknown/decision about external prior art.
|
||||
needs_outward = any(
|
||||
w in " ".join(unknowns + decisions).lower()
|
||||
for w in ("exist", "existing", "prior art", "precedent", "standard",
|
||||
"convention", "library", "tool", "framework", "approach"))
|
||||
if needs_outward:
|
||||
topics.append({
|
||||
"topic": ("what already exists that is relevant to THIS intent "
|
||||
"(adopt/adapt/combine/reject/innovate)"),
|
||||
"kind": "prior-art",
|
||||
"why": "material unknown references existing practice — "
|
||||
"precedent discovery required before deeper research",
|
||||
})
|
||||
if depth is None:
|
||||
# consequence heuristics: novel/architectural/unsafe → deeper
|
||||
consequential = any(
|
||||
w in (intent + " " + " ".join(risks)).lower()
|
||||
for w in ("architecture", "migration", "replace", "rewrite",
|
||||
"security", "schema", "delete", "deploy", "breaking"))
|
||||
depth = DEPTH_FULL if (consequential and topics) else (
|
||||
DEPTH_OUTWARD if needs_outward and topics else (
|
||||
DEPTH_INWARD if topics else DEPTH_NONE))
|
||||
why = ("consequence heuristics: consequential keywords + research "
|
||||
"topics present" if depth == DEPTH_FULL else
|
||||
"precedent-relevant unknowns present" if depth == DEPTH_OUTWARD
|
||||
else "local understanding suffices" if depth == DEPTH_INWARD
|
||||
else "no material unknowns — local evidence only")
|
||||
else:
|
||||
why = "depth supplied by caller (recorded, caller accountable)"
|
||||
return {
|
||||
"intent": intent,
|
||||
"target": target,
|
||||
"current_reality_note": current_reality[:200],
|
||||
"depth": {"depth": depth, "why": why},
|
||||
"topics": topics,
|
||||
"plan_generated_at": _now(),
|
||||
# the plan ITSELF is evidence that research was contextual, not
|
||||
# checklist-driven: two different intents must produce disjoint
|
||||
# topic sets unless they share genuine unknowns
|
||||
}
|
||||
@@ -231,7 +231,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.66.48"' in rune_version,
|
||||
'__version__ = "0.66.78"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
"""capability_ledger.py — T19 §19D: compact versioned Capability
|
||||
Ledgers — local re-entry context packets.
|
||||
|
||||
PURPOSE (rule 19D): a fresh LLM rehydrates the meaning of a target area
|
||||
after compaction/context loss/model swap/long absence WITHOUT rereading
|
||||
the repository. Not a second source tree, not a second Scaffold, not
|
||||
prose dumps — structured, proportional, operationally-used records.
|
||||
|
||||
Identity rule: capability IDs are STABLE across file moves (name-based,
|
||||
not path-based); history is preserved on supersede/retire (rule 19Q).
|
||||
|
||||
Storage: <project_store>/capability_ledger.json — versioned document,
|
||||
provenance-aware (rule G). Historical versions remain traceable via
|
||||
the history list.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
LEDGER_SCHEMA_VERSION = 1
|
||||
|
||||
MATURITY = ("proposed", "implemented", "verified", "hardened",
|
||||
"retest_required", "retired", "superseded")
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
class CapabilityLedger:
|
||||
def __init__(self, store_dir: Path | str):
|
||||
self.path = Path(store_dir) / "capability_ledger.json"
|
||||
self.doc = self._load()
|
||||
|
||||
def _load(self) -> dict:
|
||||
if self.path.is_file():
|
||||
import json
|
||||
return json.loads(self.path.read_text())
|
||||
return {"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"capabilities": {}, "history": [],
|
||||
"updated_at": _now()}
|
||||
|
||||
def save(self) -> None:
|
||||
import json
|
||||
self.doc["updated_at"] = _now()
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.path.write_text(json.dumps(self.doc, indent=1, sort_keys=True))
|
||||
|
||||
# -- lifecycle --------------------------------------------------------
|
||||
|
||||
def define(self, *, capability_id: str, name: str, purpose: str,
|
||||
owning_project: str, component: str,
|
||||
source_locations: list[str] | None = None,
|
||||
contract: str = "", state_touched: str = "",
|
||||
dependencies: list[str] | None = None,
|
||||
consumers: list[str] | None = None,
|
||||
invariants: list[str] | None = None,
|
||||
behaviour_specs: list[str] | None = None,
|
||||
constraints: str = "",
|
||||
generation: str = "") -> dict:
|
||||
"""Create a capability record (auto-invoked by Scaffolding when a
|
||||
meaningful capability is created — rule 19Q)."""
|
||||
cid = f"capability:{capability_id}"
|
||||
if cid in self.doc["capabilities"]:
|
||||
raise ValueError(f"capability exists: {cid}")
|
||||
rec = {
|
||||
"capability_id": cid,
|
||||
"name": name,
|
||||
"purpose": purpose,
|
||||
"owning_project": owning_project,
|
||||
"component": component, # structural entity id
|
||||
"source_locations": list(source_locations or []),
|
||||
"contract": contract,
|
||||
"state_touched": state_touched,
|
||||
"dependencies": list(dependencies or []),
|
||||
"consumers": list(consumers or []),
|
||||
"invariants": list(invariants or []),
|
||||
"behaviour_specs": list(behaviour_specs or []),
|
||||
"maturity": "implemented",
|
||||
"constraints": constraints,
|
||||
"generation": generation,
|
||||
"limitations": [],
|
||||
"created_at": _now(),
|
||||
"updated_at": _now(),
|
||||
}
|
||||
self.doc["capabilities"][cid] = rec
|
||||
return rec
|
||||
|
||||
def get(self, capability_id: str) -> dict | None:
|
||||
return self.doc["capabilities"].get(
|
||||
capability_id if ":" in capability_id
|
||||
else f"capability:{capability_id}")
|
||||
|
||||
def material_change(self, capability_id: str, *, note: str,
|
||||
generation: str = "",
|
||||
source_locations: list[str] | None = None) -> dict:
|
||||
"""Record a material change (auto-invoked after construction —
|
||||
rule 19Q.2). History entry keeps traceability; dependents' harden
|
||||
state is the behaviour ledger's job (via structural closure)."""
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
hist = {"at": _now(), "event": "material_change",
|
||||
"note": note[:200], "generation": generation,
|
||||
"prior_maturity": rec["maturity"]}
|
||||
self.doc["history"].append({"capability_id": rec["capability_id"],
|
||||
**hist})
|
||||
rec["updated_at"] = _now()
|
||||
if generation:
|
||||
rec["generation"] = generation
|
||||
if source_locations is not None:
|
||||
rec["source_locations"] = list(source_locations)
|
||||
return rec
|
||||
|
||||
def move(self, capability_id: str, new_locations: list[str]) -> dict:
|
||||
"""File move: STABLE IDENTITY SURVIVES (rule 19S-6); the move is
|
||||
recorded, history preserved."""
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
old = list(rec["source_locations"])
|
||||
rec["source_locations"] = list(new_locations)
|
||||
rec["updated_at"] = _now()
|
||||
self.doc["history"].append({
|
||||
"capability_id": rec["capability_id"], "at": _now(),
|
||||
"event": "moved", "from": old, "to": list(new_locations)})
|
||||
return rec
|
||||
|
||||
def supersede(self, capability_id: str, by: str, *,
|
||||
note: str = "") -> dict:
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
rec["maturity"] = "superseded"
|
||||
rec["superseded_by"] = (by if ":" in by else f"capability:{by}")
|
||||
rec["updated_at"] = _now()
|
||||
self.doc["history"].append({
|
||||
"capability_id": rec["capability_id"], "at": _now(),
|
||||
"event": "superseded", "by": rec["superseded_by"],
|
||||
"note": note[:160]})
|
||||
return rec
|
||||
|
||||
def retire(self, capability_id: str, *, note: str = "") -> dict:
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
rec["maturity"] = "retired"
|
||||
rec["updated_at"] = _now()
|
||||
self.doc["history"].append({
|
||||
"capability_id": rec["capability_id"], "at": _now(),
|
||||
"event": "retired", "note": note[:160]})
|
||||
return rec
|
||||
|
||||
# -- queries (surgical context, 19E) -----------------------------------
|
||||
|
||||
def active(self) -> list[dict]:
|
||||
return [r for r in self.doc["capabilities"].values()
|
||||
if r["maturity"] not in ("retired", "superseded")]
|
||||
|
||||
def by_component(self, component: str) -> list[dict]:
|
||||
return [r for r in self.active() if r["component"] == component]
|
||||
|
||||
def context_packet(self, capability_id: str) -> dict:
|
||||
"""The LOCAL context packet (19E): everything a fresh builder
|
||||
needs for one capability — bounded, structured, no reasoning."""
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
return {"error": f"unknown capability {capability_id!r}"}
|
||||
return {
|
||||
"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"capability_id": rec["capability_id"],
|
||||
"name": rec["name"],
|
||||
"purpose": rec["purpose"],
|
||||
"component": rec["component"],
|
||||
"source_locations": rec["source_locations"],
|
||||
"contract": rec["contract"],
|
||||
"state_touched": rec["state_touched"],
|
||||
"dependencies": rec["dependencies"],
|
||||
"consumers": rec["consumers"],
|
||||
"invariants": rec["invariants"],
|
||||
"behaviour_specs": rec["behaviour_specs"],
|
||||
"maturity": rec["maturity"],
|
||||
"constraints": rec["constraints"],
|
||||
"generation": rec["generation"],
|
||||
"limitations": rec["limitations"],
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
"""construction_activation.py — the activation dispatcher.
|
||||
|
||||
SCAFFOLD-TRANCHE-0011. §3: NOT another planner/router/engine. Given an
|
||||
evidence-backed ConstructionMode, start/resume the correct Scaffolding
|
||||
lifecycle. ONE activation authority; consumers: Rune native backend path,
|
||||
Construction Interface (MiMo/other builders), manual operator override.
|
||||
|
||||
Mode dispatch (tranche brief §4/§7/§8/§9/§10):
|
||||
|
||||
NOT_CONSTRUCTION → no Scaffolding state touched (§10)
|
||||
LOCAL_CHANGE → bounded governance via the existing project's
|
||||
authority; Construction Contract only if policy
|
||||
requires; no Project Scaffold bootstrap (§7)
|
||||
EXISTING_PROJECT_CHANGE
|
||||
→ attach to the existing project authority (Rune
|
||||
Spine for Rune; bound Project Scaffold otherwise);
|
||||
full proportional lifecycle (§8) — never a new
|
||||
project (§1 existence evidence governs)
|
||||
PROJECT_BUILD → actually bootstrap: provisional Project Scaffold
|
||||
initialized with UNKNOWN fields explicit (§6/§7) —
|
||||
repository: undecided, toolchain: unresolved —
|
||||
progressive refinement, nothing manufactured
|
||||
UNKNOWN → NO activation; return evidence gaps + probe
|
||||
questions (§9)
|
||||
|
||||
Persistence discipline (§21): every state mutation goes through
|
||||
Scaffold.commit() (save + readback verify) BEFORE any accounting.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from rune import structural_store as ss
|
||||
from rune import scaffold as sc
|
||||
from rune import construction_mode as cm
|
||||
|
||||
__all__ = ["activate"]
|
||||
|
||||
|
||||
def _scaffold_for(project: str, *, create: bool, parent_scaffold: sc.Scaffold | None = None) -> sc.Scaffold | None:
|
||||
"""Resolve the Project Scaffold store for a project (§1 evidence:
|
||||
existence comes from a bound store on disk, not from the name).
|
||||
Location derives from the PARENT scaffold's store directory — no
|
||||
hardcoded relative path (T11 fix: activation under a tmp/fixture parent
|
||||
must write the fixture root, not the repo's store)."""
|
||||
parent_dir = parent_scaffold.current.parent if parent_scaffold else Path("store/scaffold")
|
||||
base = parent_dir / "projects" / project
|
||||
current = base / "current.json"
|
||||
if current.is_file():
|
||||
return sc.Scaffold(base, project)
|
||||
if create:
|
||||
return sc.Scaffold(base, project)
|
||||
return None
|
||||
|
||||
|
||||
def activate(mode_decision, *, need, scaffold: sc.Scaffold, builder: str = "rune") -> dict:
|
||||
"""Dispatch on the evidence-backed mode; return an activation record
|
||||
(mode, project authority, actions taken, context ref). The NEED must
|
||||
already be classified — UNKNOWN never reaches here without explicit
|
||||
operator override carried in mode_decision."""
|
||||
mode = mode_decision.mode
|
||||
rec: dict = {
|
||||
"mode": mode,
|
||||
"builder": builder,
|
||||
"target_project": mode_decision.target_project,
|
||||
"evidence": list(mode_decision.evidence),
|
||||
}
|
||||
|
||||
if mode == "NOT_CONSTRUCTION":
|
||||
# §10: no Scaffolding state touched
|
||||
rec["activated"] = False
|
||||
rec["reason"] = "non-construction intent — no construction state"
|
||||
return rec
|
||||
|
||||
if mode == "UNKNOWN":
|
||||
# §9: no activation; probe questions surface
|
||||
rec["activated"] = False
|
||||
rec["reason"] = "insufficient evidence — clarification required; no Project Scaffold initialized"
|
||||
rec["probe_questions"] = mode_decision.probe_questions
|
||||
rec["clarification_required"] = True
|
||||
return rec
|
||||
|
||||
if mode == "LOCAL_CHANGE":
|
||||
# §7: governance without ceremony — reuse the existing project's
|
||||
# authority; the lightest contract path (caller decides policy);
|
||||
# NO new Project Scaffold bootstrap
|
||||
rec["activated"] = True
|
||||
rec["lifecycle"] = "bounded"
|
||||
rec["project_authority"] = mode_decision.project_authority or "rune_internal"
|
||||
rec["note"] = "bounded governance via existing authority — no Project Scaffold bootstrap"
|
||||
return rec
|
||||
|
||||
if mode == "EXISTING_PROJECT_CHANGE":
|
||||
# §8: attach to the existing authority — never a new project.
|
||||
# Existence was already proven structurally (T11 §1) before this
|
||||
# mode was selected.
|
||||
rec["activated"] = True
|
||||
rec["lifecycle"] = "proportional"
|
||||
rec["project_authority"] = mode_decision.project_authority or "rune_internal"
|
||||
rec["note"] = (
|
||||
"attached to existing project authority; Need/completeness/patterns/contract flow governs proportionally"
|
||||
)
|
||||
return rec
|
||||
|
||||
if mode == "PROJECT_BUILD":
|
||||
# §6/§7: actually bootstrap. Provisional Project Scaffold with
|
||||
# explicit UNKNOWN fields; identity before repository (§7).
|
||||
project = mode_decision.target_project or need.project
|
||||
if not project:
|
||||
# Rune-originated Needs may carry no project identity (§18):
|
||||
# derive a stable provisional id from the objective — the
|
||||
# bootstrap is provisional and renameable before any repo
|
||||
# exists (§7 identity before repository).
|
||||
import re as _re
|
||||
|
||||
slug = _re.sub(r"[^a-z0-9]+", "-", need.objective.lower()).strip("-")[:48]
|
||||
project = slug or "unnamed-project"
|
||||
ps = _scaffold_for(project, create=True, parent_scaffold=scaffold)
|
||||
assert ps is not None
|
||||
pdoc = ps.load()
|
||||
if ss.entity(pdoc, f"project:{project}") is None:
|
||||
ps.init(purpose=need.objective) # init persists its own doc
|
||||
pdoc = ps.load() # §21: reload after persistence
|
||||
structural = ss.entity(pdoc, f"project:{project}")
|
||||
assert structural is not None
|
||||
# explicit UNKNOWN fields — nothing manufactured (§6/§10)
|
||||
structural.setdefault("ext", {}).update(
|
||||
{
|
||||
"bootstrap": "initialized",
|
||||
"repository": "undecided",
|
||||
"toolchain": "unresolved",
|
||||
"deployment": "unknown",
|
||||
"requirement_gaps": list(mode_decision.probe_questions)
|
||||
or ["target platform unknown", "requirements not yet refined"],
|
||||
"bootstrap_started_at": ss.now(),
|
||||
"origin_need": need.need_id,
|
||||
"origin_builder": builder,
|
||||
}
|
||||
)
|
||||
ps.commit(pdoc, what=f"PROJECT_BUILD bootstrap: {project}")
|
||||
rec.update(
|
||||
{
|
||||
"activated": True,
|
||||
"lifecycle": "project_build",
|
||||
"project": project,
|
||||
"project_scaffold": str(ps.current),
|
||||
"bootstrap": "initialized",
|
||||
"repository": "undecided",
|
||||
"toolchain": "unresolved",
|
||||
"deployment": "unknown",
|
||||
"requirement_gaps": structural["ext"]["requirement_gaps"],
|
||||
"next_action": ("refine requirements; resolve toolchain/repo decisions via evidence (approval-gated)"),
|
||||
}
|
||||
)
|
||||
return rec
|
||||
|
||||
rec["activated"] = False
|
||||
rec["reason"] = f"unhandled mode {mode}"
|
||||
return rec
|
||||
|
||||
|
||||
def dispatch_from_decision(trace: dict, *, need, scaffold: sc.Scaffold, builder: str = "rune") -> dict:
|
||||
"""§15: read the mode from a make_decision trace and dispatch. The
|
||||
shared Construction Context then derives from the SAME canonical state
|
||||
— no duplicate machines."""
|
||||
mode = trace.get("construction_mode", {}).get("mode", "UNKNOWN")
|
||||
return activate(mode, need=need, scaffold=scaffold, builder=builder)
|
||||
@@ -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}
|
||||
@@ -147,11 +147,11 @@ def classify_surface(surface: str) -> str:
|
||||
if surface.endswith(".yml") or surface.endswith(".yaml") or surface.endswith(".json") or surface.endswith(".toml"):
|
||||
return "workflow_config"
|
||||
if (
|
||||
surface.endswith(".py")
|
||||
surface.endswith((".py", ".js", ".ts", ".jsx", ".tsx", ".rs",
|
||||
".md", ".go", ".java", ".c", ".cpp", ".h",
|
||||
".css", ".html", ".yaml", ".yml",
|
||||
".mjs", ".cjs", ".mts", ".cts"))
|
||||
or "/" in surface
|
||||
or surface.endswith(".md")
|
||||
or surface.endswith(".rs")
|
||||
or surface.endswith(".ts")
|
||||
):
|
||||
return "source_path"
|
||||
return "unresolved"
|
||||
@@ -283,7 +283,7 @@ def resolve_affected_surface(need: Need, authorities: dict[str, dict]) -> dict:
|
||||
"entity": test_entity,
|
||||
"derived_from": surface,
|
||||
"rule": "Rune test-path convention: tests/test_<name>.py implies "
|
||||
"the test:<name> surface participates in the build",
|
||||
"the test:<name> surface participates in the build",
|
||||
"evidence": f"affected_surface entry {surface}",
|
||||
}
|
||||
)
|
||||
@@ -505,6 +505,33 @@ def needs_user_approval(
|
||||
# ── the live decision path ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _project_exists_evidence(project: str | None, authorities: dict[str, dict]) -> tuple[bool | None, list[str]]:
|
||||
"""T11 §1: project existence from STRUCTURAL EVIDENCE only.
|
||||
|
||||
Returns (exists, evidence) where exists is:
|
||||
True — a bound Project Scaffold or Rune Spine carries the project
|
||||
False — the project name is known but NO structural evidence for it
|
||||
None — project identity missing/unresolvable (UNKNOWN; classifier
|
||||
must not manufacture existence)
|
||||
|
||||
Rune itself exists (Rune Spine is its authority). Any other name must
|
||||
appear as a project entity in a consulted authority; a bare string is
|
||||
never proof (held-out: arbitrary "foo" → False, not silently True)."""
|
||||
evidence: list[str] = []
|
||||
if not project:
|
||||
return None, ["no project identity supplied — existence UNKNOWN"]
|
||||
if project in ("rune", "issue161-harness"):
|
||||
evidence.append("Rune Spine is the Rune project authority (rune_internal mode)")
|
||||
return True, evidence
|
||||
for name, doc in authorities.items():
|
||||
for e in doc.get("entities", []):
|
||||
if e.get("kind") == "project" and e.get("id") == f"project:{project}":
|
||||
evidence.append(f"bound Project Scaffold entity project:{project} in {name}")
|
||||
return True, evidence
|
||||
evidence.append(f"no structural evidence for project '{project}' in consulted authorities")
|
||||
return False, evidence
|
||||
|
||||
|
||||
def make_decision(
|
||||
need: Need,
|
||||
*,
|
||||
@@ -549,16 +576,19 @@ def make_decision(
|
||||
surface_result = resolve_affected_surface(need, authorities)
|
||||
trace["affected_surfaces"] = surface_result
|
||||
|
||||
# CONTEXTUAL CONSTRUCTION MODE (Tranche 10): typed mode classification
|
||||
# with evidence — ONE canonical home (construction_mode); §3 distinction
|
||||
# preserved: capability BUILD_NEW != mode PROJECT_BUILD
|
||||
# CONTEXTUAL CONSTRUCTION MODE (Tranche 10 + T11 §1): typed mode
|
||||
# classification with evidence — ONE canonical home (construction_mode).
|
||||
# §1 CORRECTION: project existence derives from STRUCTURAL EVIDENCE
|
||||
# (bound Project Scaffold / Rune Spine), never from a non-"rune" string.
|
||||
from rune.construction_mode import classify_mode
|
||||
|
||||
existence, existence_evidence = _project_exists_evidence(need.project, authorities)
|
||||
mode_decision = classify_mode(
|
||||
need.objective,
|
||||
target_project=need.project if need.project != "rune" else "rune",
|
||||
project_exists=(need.project != "rune"), # rune itself exists
|
||||
affected_surface=need.affected_surface)
|
||||
need.objective, target_project=need.project, project_exists=existence, affected_surface=need.affected_surface
|
||||
)
|
||||
mode_decision.evidence.extend(existence_evidence)
|
||||
trace["construction_mode"] = mode_decision.to_dict()
|
||||
trace["project_existence"] = {"project": need.project, "exists": existence, "evidence": existence_evidence}
|
||||
|
||||
# REQUIREMENT COMPLETENESS GATE (Tranche 5 §6-8) — before fit
|
||||
completeness = check_completeness(req, need, authorities, surface_result)
|
||||
@@ -646,12 +676,13 @@ def make_decision(
|
||||
# Project evidence > pattern assumption (§3): a pattern that contradicts
|
||||
# recorded project exclusions becomes DOES_NOT_APPLY.
|
||||
from rune.construction_pattern import ( # noqa: E402 — local: pattern layer
|
||||
combine_pattern_obligations, derive_pattern_obligations,
|
||||
evaluate_applicability)
|
||||
combine_pattern_obligations,
|
||||
derive_pattern_obligations,
|
||||
evaluate_applicability,
|
||||
)
|
||||
from rune.construction_patterns_builtin import BUILTIN_PATTERNS # noqa: E402
|
||||
|
||||
pattern_candidates = list(BUILTIN_PATTERNS) + list(
|
||||
extra_patterns or [])
|
||||
pattern_candidates = list(BUILTIN_PATTERNS) + list(extra_patterns or [])
|
||||
patterns_considered = []
|
||||
derivations = []
|
||||
for pattern in pattern_candidates:
|
||||
@@ -664,14 +695,16 @@ def make_decision(
|
||||
# weaker evidence stays visible.
|
||||
if app["verdict"] in ("APPLIES", "PARTIAL"):
|
||||
d = derive_pattern_obligations(
|
||||
pattern, need, surface_result, authorities,
|
||||
project_exceptions=need.constraints.get(
|
||||
"pattern_exceptions", []))
|
||||
pattern,
|
||||
need,
|
||||
surface_result,
|
||||
authorities,
|
||||
project_exceptions=need.constraints.get("pattern_exceptions", []),
|
||||
)
|
||||
for entry in d["accepted"] + d["rejected"] + d["unresolved"]:
|
||||
entry["pattern_applicability"] = app["verdict"]
|
||||
derivations.append(d)
|
||||
combined = combine_pattern_obligations(derivations) if derivations else {
|
||||
"obligations": [], "conflicts": []}
|
||||
combined = combine_pattern_obligations(derivations) if derivations else {"obligations": [], "conflicts": []}
|
||||
trace["patterns_considered"] = patterns_considered
|
||||
trace["pattern_obligations"] = combined
|
||||
|
||||
@@ -684,8 +717,7 @@ def make_decision(
|
||||
trace["obligations"]["pattern"] = combined["obligations"]
|
||||
trace["obligations"]["pattern_conflicts"] = combined["conflicts"]
|
||||
for po in combined["obligations"]:
|
||||
trace["obligations"]["verification"].append(
|
||||
f"[pattern:{po['pattern']}@r{po['revision']}] {po['obligation']}")
|
||||
trace["obligations"]["verification"].append(f"[pattern:{po['pattern']}@r{po['revision']}] {po['obligation']}")
|
||||
return trace
|
||||
|
||||
|
||||
@@ -852,9 +884,10 @@ def generate_contract(
|
||||
resolution=trace["resolution"],
|
||||
toolchain=trace["toolchain"],
|
||||
affected_entities=sorted(affected),
|
||||
expected_new_entities=sorted(set(
|
||||
trace["need"]["constraints"].get("expected_new_entities", [])
|
||||
) | {d["entity"] for d in surfaces.get("derived_expected_new", [])}),
|
||||
expected_new_entities=sorted(
|
||||
set(trace["need"]["constraints"].get("expected_new_entities", []))
|
||||
| {d["entity"] for d in surfaces.get("derived_expected_new", [])}
|
||||
),
|
||||
expected_consumers=[],
|
||||
regression_obligations=obl["regression"],
|
||||
verification_obligations=sorted(set(obl["verification"])),
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"""construction_depth.py — proportional Scaffolding depth selection.
|
||||
|
||||
SCAFFOLD-TRANCHE-0012 §12E. ONE construction discipline, variable depth.
|
||||
"Small" never means "bypass governance"; a one-line typo never creates
|
||||
six meaningless artefacts.
|
||||
|
||||
Depth is selected from EVIDENCE, not from keywords alone. The evidence
|
||||
inputs (§12E): construction mode, novelty, persistence, risk, affected
|
||||
surface, architecture impact, migration, deployment impact, user-locked
|
||||
structure, uncertainty.
|
||||
|
||||
Levels:
|
||||
LIGHT classification + authority/scope + minimal obligations +
|
||||
focused verification. For tiny bounded local fixes.
|
||||
STANDARD + requirements/completeness + capability/toolchain check
|
||||
+ broader verification. For features/repairs/migrations.
|
||||
FULL + project bootstrap + architecture/toolchain decisions +
|
||||
patterns + deployment/rollback. For PROJECT_BUILD.
|
||||
|
||||
Reuses existing semantics: construction mode (T10/T11) already encodes
|
||||
most of the signal — depth is DERIVED from mode + modifiers, not a
|
||||
parallel classification.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
__all__ = ["Depth", "select_depth", "LEVELS"]
|
||||
|
||||
LEVELS = ("NONE", "UNRESOLVED", "LIGHT", "STANDARD", "FULL")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Depth:
|
||||
level: str
|
||||
reason: str
|
||||
evidence: list[str] = field(default_factory=list)
|
||||
ceremony: list[str] = field(default_factory=list) # what depth requires
|
||||
bypassed: list[str] = field(default_factory=list) # what is skipped and why
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {"level": self.level, "reason": self.reason,
|
||||
"evidence": self.evidence, "ceremony": self.ceremony,
|
||||
"bypassed": self.bypassed}
|
||||
|
||||
|
||||
def select_depth(mode: str, *, objective: str = "",
|
||||
affected_surface: list[str] | None = None,
|
||||
has_migration: bool = False,
|
||||
breaking: bool = False,
|
||||
user_locked_layout: bool = False,
|
||||
external_service: bool = False,
|
||||
uncertainty: str = "", # "" | "partial" | "high"
|
||||
project_exists: bool | None = None,
|
||||
) -> Depth:
|
||||
"""Select proportional depth from construction mode + evidence."""
|
||||
ev: list[str] = [f"mode: {mode}"]
|
||||
ceremony: list[str] = []
|
||||
bypassed: list[str] = []
|
||||
|
||||
if mode == "NOT_CONSTRUCTION":
|
||||
# §13C: NOT_CONSTRUCTION has NO construction depth — not even LIGHT
|
||||
return Depth(level="NONE", reason="not construction — no construction "
|
||||
"depth applies", evidence=ev, ceremony=[],
|
||||
bypassed=["all"])
|
||||
|
||||
if mode == "UNKNOWN":
|
||||
# §13C: UNKNOWN means construction hasn't started — no lifecycle yet
|
||||
return Depth(level="UNRESOLVED",
|
||||
reason="UNKNOWN — probe/clarify before any construction "
|
||||
"lifecycle begins",
|
||||
evidence=ev, ceremony=["probe/clarify"],
|
||||
bypassed=["contract", "patterns", "bootstrap",
|
||||
"depth selection"])
|
||||
|
||||
if mode == "LOCAL_CHANGE":
|
||||
ev.append("LOCAL_CHANGE: bounded scope, minimal ceremony")
|
||||
ceremony.extend(["classification", "authority/scope",
|
||||
"focused verification", "durable result"])
|
||||
bypassed.extend(["requirements/completeness", "patterns",
|
||||
"capability/toolchain check", "project bootstrap",
|
||||
"deployment/rollback"])
|
||||
if user_locked_layout:
|
||||
ev.append("USER-LOCKED layout surface — preservation obligation "
|
||||
"retained even at LIGHT depth")
|
||||
ceremony.append("layout obligation check")
|
||||
return Depth(level="LIGHT",
|
||||
reason="bounded local fix — governance without ceremony",
|
||||
evidence=ev, ceremony=ceremony, bypassed=bypassed)
|
||||
|
||||
if mode == "PROJECT_BUILD":
|
||||
ev.append("PROJECT_BUILD: full lifecycle required")
|
||||
ceremony.extend(["project bootstrap", "requirements/completeness",
|
||||
"capability/toolchain decisions",
|
||||
"patterns/obligations", "Construction Contract",
|
||||
"implementation", "verification",
|
||||
"deployment/rollback strategy",
|
||||
"maintenance lifecycle"])
|
||||
return Depth(level="FULL",
|
||||
reason="genuinely new project — full Scaffolding "
|
||||
"lifecycle",
|
||||
evidence=ev, ceremony=ceremony)
|
||||
|
||||
# EXISTING_PROJECT_CHANGE: base STANDARD, escalate on modifiers
|
||||
ceremony.extend(["classification", "requirements/completeness",
|
||||
"capability fit", "Construction Contract", "scope",
|
||||
"obligations", "verification"])
|
||||
level = "STANDARD"
|
||||
reason = "existing-project change — proportional governance"
|
||||
|
||||
if has_migration:
|
||||
ev.append("migration: old/new compatibility + rollback obligations")
|
||||
ceremony.extend(["migration proof", "rollback preparation"])
|
||||
if breaking:
|
||||
ev.append("breaking change: stronger approval + regression "
|
||||
"obligations")
|
||||
ceremony.append("consumer migration completeness")
|
||||
if user_locked_layout:
|
||||
ev.append("USER-LOCKED layout surface affected")
|
||||
ceremony.append("layout obligation check")
|
||||
if external_service:
|
||||
ev.append("external service consequence")
|
||||
ceremony.append("deployment verification")
|
||||
|
||||
if uncertainty == "high":
|
||||
ev.append("high uncertainty: probe before deeper ceremony")
|
||||
ceremony.insert(0, "evidence probe")
|
||||
surface_list = affected_surface or []
|
||||
if len(surface_list) > 5:
|
||||
ev.append(f"wide affected surface: {len(surface_list)} entries")
|
||||
level = "FULL"
|
||||
reason = "wide surface — full depth warranted"
|
||||
if breaking and has_migration:
|
||||
level = "FULL"
|
||||
reason = "breaking + migration — full depth"
|
||||
|
||||
return Depth(level=level, reason=reason, evidence=ev, ceremony=ceremony,
|
||||
bypassed=bypassed)
|
||||
@@ -0,0 +1,555 @@
|
||||
"""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' detection by PRECEDING context (T18 18N fix): 'that'
|
||||
# is a relative pronoun when the word before it is an article/noun
|
||||
# ("the proposal that you suggested") — demonstrative when
|
||||
# sentence-initial or after a verb/punctuation ("build that" /
|
||||
# "that works for me"). The old following-word heuristic misclassified
|
||||
# "that works for me" as a relative clause.
|
||||
_ARTICLES = {"the", "a", "an", "this", "these", "those", "one"}
|
||||
|
||||
|
||||
def _is_relative_that(low: str, dem_tokens: set) -> bool:
|
||||
"""True when every 'that' token reads as a relative pronoun by its
|
||||
preceding context; demonstrative otherwise."""
|
||||
words = re.findall(r"[a-z']+", low)
|
||||
seen_that = False
|
||||
for i, w in enumerate(words):
|
||||
if w != "that":
|
||||
continue
|
||||
seen_that = True
|
||||
prev = words[i - 1] if i > 0 else None
|
||||
if prev is None or prev in _CONSTRUCTION_VERBS:
|
||||
return False # sentence-initial or post-verb → demonstrative
|
||||
if prev not in _ARTICLES:
|
||||
# preceding word is neither article nor verb: treat as
|
||||
# demonstrative unless it is clearly a noun modifier —
|
||||
# conservative default is demonstrative (referent-seeking)
|
||||
return False
|
||||
return seen_that and "that" in dem_tokens
|
||||
|
||||
_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|make that happen|that's the one|go with|"
|
||||
r"let's go|let's do|let's have it|proceed|good with that|"
|
||||
r"fine with that|happy with that|on board|approved|ship it)\b",
|
||||
re.IGNORECASE)
|
||||
|
||||
|
||||
def _tokens(text: str) -> set[str]:
|
||||
return set(re.split(r"[^a-z0-9]+", text.lower()))
|
||||
|
||||
|
||||
_NEGATION = re.compile(
|
||||
r"^\s*(don't|do not|dont|never|not yet|stop|hold off|no need|"
|
||||
r"skip it|forget it)\b", re.IGNORECASE)
|
||||
_IDIOM_PREFIX = re.compile(
|
||||
r"^\s*(go ahead and|go and|please|kindly|then|now)\s+", re.IGNORECASE)
|
||||
|
||||
|
||||
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'. Idiom prefixes
|
||||
('go ahead and build X', 'please implement X') are stripped so the
|
||||
real verb surfaces."""
|
||||
t = text
|
||||
for _ in range(3):
|
||||
t2 = _IDIOM_PREFIX.sub("", t)
|
||||
if t2 == t:
|
||||
break
|
||||
t = t2
|
||||
m = re.match(r"^\s*(\w+)", t, 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] = []
|
||||
|
||||
# 0. NEGATION (§18N): "don't build that yet" blocks authorization —
|
||||
# construction language inside a negation frame is NOT a demand.
|
||||
if _NEGATION.match(t):
|
||||
ev.append("negation frame — explicitly blocks construction")
|
||||
return _intent(t, "DISCUSSION", "DISCUSSION", 0.85, "NONE",
|
||||
ev, probes, context, origin_context_id)
|
||||
|
||||
# 1a. REQUEST MODALS: "can you fix that?" is an imperative in
|
||||
# interrogative clothing — a state-changing action request, NOT an
|
||||
# information question. Information openers (how/what/why/should we)
|
||||
# stay discussion; request modals authorize when the target resolves.
|
||||
# information openers outrank modals: "how would you build X?" asks
|
||||
# about METHOD; "can you fix that?" requests ACTION
|
||||
information_opener = bool(re.match(
|
||||
r"^\s*(how|what|why|when|where|which)\b", low))
|
||||
request_modal = (not information_opener) and bool(re.search(
|
||||
r"\b(can you|could you|would you|will you|please)\b", low))
|
||||
if request_modal:
|
||||
# modal + a construction verb somewhere in the turn
|
||||
request_modal = bool(_tokens(low) & _CONSTRUCTION_VERBS)
|
||||
if request_modal:
|
||||
ev.append("request modal (can/could/would you, please) + "
|
||||
"construction verb → action request, not discussion")
|
||||
# a modal + demonstrative + live proposal authorizes THAT
|
||||
# proposal (referent resolution outranks target resolution)
|
||||
dem_tokens_m = _tokens(low) & _DEMONSTRATIVES
|
||||
if dem_tokens_m and context.live_proposals():
|
||||
referent = _resolve_referent(low, context)
|
||||
if referent and not referent.get("ambiguous"):
|
||||
tp = referent["proposal"]
|
||||
ev.append(f"modal authorization resolves proposal "
|
||||
f"{tp.proposal_id}")
|
||||
return ConstructionIntent(
|
||||
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
|
||||
objective=tp.summary,
|
||||
requested_effect=f"construct: {tp.summary}",
|
||||
resolved_target=context.active_project,
|
||||
resolved_referent=tp.proposal_id,
|
||||
origin_context_id=origin_context_id,
|
||||
action_intent="ACTION", approval_state="USER_APPROVED",
|
||||
confidence=0.85, ambiguity="NONE",
|
||||
consequence_level=_consequence(t, context),
|
||||
relevant_constraints=_constraints(context),
|
||||
classification_evidence=ev)
|
||||
target = context.active_project
|
||||
if target:
|
||||
ev.append(f"target resolved from active context: {target}")
|
||||
# demonstrative referent: "fix that" with no live proposal —
|
||||
# the antecedent is the defect described in THIS turn
|
||||
return ConstructionIntent(
|
||||
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
|
||||
objective=t, requested_effect=f"fix/act: {t}",
|
||||
resolved_target=target,
|
||||
resolved_referent="this-turn-topic",
|
||||
origin_context_id=origin_context_id,
|
||||
action_intent="ACTION", approval_state="USER_APPROVED",
|
||||
confidence=0.85, ambiguity="NONE",
|
||||
consequence_level=_consequence(t, context),
|
||||
relevant_constraints=_constraints(context),
|
||||
classification_evidence=ev)
|
||||
ev.append("request modal but no resolvable target")
|
||||
probes.append("where should that change land — which project?")
|
||||
return _intent(t, "ACTION", "DISCUSSION", 0.5, "TARGET",
|
||||
ev, probes, context, origin_context_id)
|
||||
|
||||
# 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 _is_relative_that(low, dem_tokens):
|
||||
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:
|
||||
# "go ahead and build X": idiom + explicit imperative — fall
|
||||
# through to the demand path (target resolution) rather than
|
||||
# dead-ending in referent ambiguity
|
||||
if imperative 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)
|
||||
ev.append("no live proposal matches; treating as direct "
|
||||
"demand (explicit imperative object present)")
|
||||
if referent is None:
|
||||
# live proposals exist but are ALL STALE (topic moved on) —
|
||||
# never approve a stale referent; fall to the demand path
|
||||
ev.append("proposals stale for this turn — not selected")
|
||||
if imperative is None:
|
||||
probes.append("what should that refer to? The earlier "
|
||||
"proposal no longer seems active.")
|
||||
return _intent(t, "ACTION", "DISCUSSION", 0.4, "REFERENT",
|
||||
ev, probes, context, origin_context_id)
|
||||
ev.append("stale proposals ignored; explicit imperative → "
|
||||
"demand path")
|
||||
referent = {"__fallthrough__": True}
|
||||
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)
|
||||
if referent.get("__fallthrough__"):
|
||||
# demand path: target from context or clarify (never stale)
|
||||
target = context.active_project
|
||||
if not target and imperative in _CREATION_VERBS and \
|
||||
_NEW_ARTICLE.search(low):
|
||||
# new standalone artifact demand — no target needed
|
||||
ev.append("creation verb + indefinite article → new "
|
||||
"standalone artifact; 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}",
|
||||
consequence_level=_consequence(t, context))
|
||||
if target:
|
||||
ev.append(f"target resolved from active context: {target}")
|
||||
return _intent(t, "ACTION", "USER_APPROVED", 0.8, "NONE",
|
||||
ev, probes, context, origin_context_id,
|
||||
requested_effect=f"construct: {t}",
|
||||
consequence_level=_consequence(t, context))
|
||||
probes.append("build where — which project/system?")
|
||||
return _intent(t, "ACTION", "DISCUSSION", 0.5, "TARGET",
|
||||
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)
|
||||
@@ -214,7 +214,11 @@ class ConstructionInterface:
|
||||
"""§28 schema negotiation: builders detect the supported interface
|
||||
schema and operation set BEFORE calling operations. Also exposes the
|
||||
error-state vocabulary so builders can switch on states without
|
||||
parsing prose. Read-only; no contract required."""
|
||||
parsing prose. Read-only; no contract required.
|
||||
§15N: full argument schemas included — a fresh builder discovers
|
||||
required types/fields deterministically instead of trial-and-error
|
||||
(the T14 report_observation dict-vs-list + missing-entity mistakes
|
||||
are exactly what this prevents)."""
|
||||
return _envelope(
|
||||
"capabilities",
|
||||
success=True,
|
||||
@@ -223,6 +227,7 @@ class ConstructionInterface:
|
||||
"mode": self.mode,
|
||||
"project": self.scaffold.project_id,
|
||||
"operations": [
|
||||
"discover_active_contract",
|
||||
"submit_need",
|
||||
"inspect_decision",
|
||||
"get_scope",
|
||||
@@ -236,6 +241,67 @@ class ConstructionInterface:
|
||||
"capabilities",
|
||||
],
|
||||
"error_states": sorted(v for k, v in vars(Errors).items() if not k.startswith("_")),
|
||||
# ── §15N argument schemas (discovery over guessing) ──
|
||||
"argument_schemas": {
|
||||
"submit_need": {
|
||||
"objective": {"type": "str", "required": True},
|
||||
"origin": {"type": "str", "default": "builder"},
|
||||
"constraints": {"type": "dict", "required": False,
|
||||
"notes": ("{'environment': {...}, "
|
||||
"'contracts': [...]} — "
|
||||
"environment missing "
|
||||
"degrades to "
|
||||
"INSUFFICIENT_EVIDENCE")},
|
||||
"affected_surface": {"type": "list[str]",
|
||||
"required": False},
|
||||
"evidence_refs": {"type": "list[str]",
|
||||
"required": True,
|
||||
"notes": ("empty list degrades "
|
||||
"fit resolution")},
|
||||
"approval_context": {"type": "str",
|
||||
"required": False},
|
||||
},
|
||||
"report_observation": {
|
||||
"observations": {
|
||||
"type": "list[dict]",
|
||||
"required": True,
|
||||
"item_required_fields": ["entity"],
|
||||
"item_optional_fields": ["source", "detail"],
|
||||
"notes": ("a LIST of dicts, each with an "
|
||||
"'entity' key — a bare dict or a "
|
||||
"dict without 'entity' is rejected "
|
||||
"(NEED_INCOMPLETE)"),
|
||||
},
|
||||
},
|
||||
"verify": {
|
||||
"changed_files": {"type": "list[str] | None",
|
||||
"required": False,
|
||||
"notes": ("scope-checked against "
|
||||
"the contract scope; "
|
||||
"out-of-scope files → "
|
||||
"SCOPE_VIOLATION")},
|
||||
},
|
||||
"transition": {
|
||||
"phase": {"type": "str", "required": True,
|
||||
"allowed": list(sc.PHASES)},
|
||||
},
|
||||
"close": {
|
||||
"observed_evidence": {
|
||||
"type": "list[dict]", "required": True,
|
||||
"notes": ("evidence INPUT only — closure is "
|
||||
"decided by verification truth, "
|
||||
"never by builder claim")},
|
||||
"scope_files": {"type": "list[str] | None",
|
||||
"required": False},
|
||||
},
|
||||
},
|
||||
"lifecycle": {
|
||||
"phases": list(sc.PHASES),
|
||||
"legal_transitions": {k: sorted(v) for k, v in
|
||||
sc.LEGAL_TRANSITIONS.items()},
|
||||
"scope_amendment": ("user-gated via amend_scope — "
|
||||
"builders cannot broaden scope"),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -323,7 +389,7 @@ class ConstructionInterface:
|
||||
warnings.append({"warning": "contract generated in draft; requires approval transition before building"})
|
||||
self._record_session(doc, "submit_need", f"{need.need_id} → {trace['resolution']}")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
self.scaffold.commit(doc, what="submit_need")
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"submit_need",
|
||||
@@ -457,7 +523,7 @@ class ConstructionInterface:
|
||||
ss.record_observed_impact(doc, contract_id, items)
|
||||
self._record_session(doc, "report_observation", f"{len(items)} observation(s) on {contract_id}")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
self.scaffold.commit(doc, what="observations")
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
@@ -556,7 +622,7 @@ class ConstructionInterface:
|
||||
)
|
||||
self._record_session(doc, "transition", f"{contract_id} → {phase}")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
self.scaffold.commit(doc, what="transition")
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"transition",
|
||||
@@ -586,7 +652,7 @@ class ConstructionInterface:
|
||||
)
|
||||
self._record_session(doc, "close", f"{contract_id} closed")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
self.scaffold.commit(doc, what="close")
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"close",
|
||||
@@ -617,6 +683,53 @@ class ConstructionInterface:
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def discover_active_contract(store_path: str) -> dict:
|
||||
"""§15U canonical resume discovery: walk the store's project
|
||||
scaffolds AND the main store's own current.json, returning the
|
||||
most recent contract's (project, contract_id). Migrated here from
|
||||
the T14/T15 session drivers — a fresh builder with NO transcript
|
||||
discovers what to resume through the INTERFACE, not a script.
|
||||
Pure readback: no session file, no transcript, no mutation."""
|
||||
from pathlib import Path as _P
|
||||
import json as _json
|
||||
root = _P(store_path)
|
||||
candidates: list[tuple] = []
|
||||
for proj_current in sorted(root.glob("projects/*/current.json")):
|
||||
# per-project scaffold: (file, project_id)
|
||||
candidates.append((proj_current, proj_current.parent.name))
|
||||
main = root / "current.json"
|
||||
if main.exists():
|
||||
# main store: contracts carry their project in identities
|
||||
candidates.append((main, None))
|
||||
if not candidates:
|
||||
return _envelope("discover_active_contract", success=False,
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT,
|
||||
"detail": "no projects in store"}])
|
||||
best = None
|
||||
best_project = ""
|
||||
for proj_current, project in candidates:
|
||||
try:
|
||||
doc = _json.loads(proj_current.read_text())
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] == "construction_contract":
|
||||
proj = project or e.get("identities", {}).get(
|
||||
"project", root.name)
|
||||
key = (e.get("observed_at") or "", proj,
|
||||
e["id"].split(":", 1)[1])
|
||||
if best is None or key > (best[0], best[1], best[2]):
|
||||
best = key
|
||||
best_project = proj
|
||||
if best is None:
|
||||
return _envelope("discover_active_contract", success=False,
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT,
|
||||
"detail": "no contracts in store"}])
|
||||
return _envelope("discover_active_contract", success=True,
|
||||
ids={"project": best_project,
|
||||
"contract_id": best[2]})
|
||||
|
||||
def resume(self, contract_id: str) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
@@ -629,17 +742,24 @@ class ConstructionInterface:
|
||||
)
|
||||
ext = c.get("ext", {})
|
||||
t = ext.get("decision_trace", {})
|
||||
need_fallback = {
|
||||
"objective": c["identities"].get("need", ""),
|
||||
"origin": c["identities"].get("origin", ""),
|
||||
"need_id": contract_id,
|
||||
}
|
||||
resolution_fallback = c["identities"].get("resolution", "")
|
||||
toolchain_fallback = c["identities"].get("toolchain", {}).get("selected_language", "")
|
||||
return _envelope(
|
||||
"resume",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id, "need_id": t.get("need", {}).get("need_id", "")},
|
||||
ids={"contract_id": contract_id, "need_id": t.get("need", {}).get("need_id", contract_id)},
|
||||
state={
|
||||
"mode": self.mode,
|
||||
"need": t.get("need", {}),
|
||||
"need": t.get("need", need_fallback),
|
||||
"decision": {
|
||||
"resolution": t.get("resolution"),
|
||||
"completeness": t.get("completeness", {}).get("verdict"),
|
||||
"toolchain": t.get("toolchain", {}).get("selected_language", ""),
|
||||
"resolution": t.get("resolution") or resolution_fallback,
|
||||
"completeness": t.get("completeness", {}).get("verdict", ""),
|
||||
"toolchain": t.get("toolchain", {}).get("selected_language", "") or toolchain_fallback,
|
||||
},
|
||||
"phase": c.get("phase"),
|
||||
"status": c.get("status"),
|
||||
@@ -686,7 +806,7 @@ class ConstructionInterface:
|
||||
{"added": sorted(add_paths), "reason": reason[:200], "authorized_by": "user"}
|
||||
)
|
||||
self._record_session(doc, "amend_scope", f"{contract_id} +{len(add_paths)} paths (user)")
|
||||
self.scaffold.save(doc)
|
||||
self.scaffold.commit(doc, what=f"amend_scope {contract_id}")
|
||||
return _envelope(
|
||||
"amend_scope", success=True, ids={"contract_id": contract_id}, state={"scope": c["identities"]["scope"]}
|
||||
)
|
||||
@@ -706,6 +826,22 @@ def _obligations_projection(c: dict) -> list[dict]:
|
||||
t = c.get("ext", {}).get("decision_trace", {})
|
||||
obl = t.get("obligations", {})
|
||||
out = []
|
||||
# contract-level obligations (from identities, not just trace)
|
||||
ident = c.get("identities", {})
|
||||
for field_name, prov in (
|
||||
("verification_obligations", "CONTRACT"),
|
||||
("regression_obligations", "CONTRACT"),
|
||||
):
|
||||
for item in ident.get(field_name, []):
|
||||
out.append(
|
||||
{
|
||||
"id": f"{prov.lower()}:{abs(hash(str(item))) % 10**8}",
|
||||
"description": str(item)[:200],
|
||||
"provenance": prov,
|
||||
"kind": field_name,
|
||||
"status": "REQUIRED",
|
||||
}
|
||||
)
|
||||
for kind in ("regression", "verification", "composition", "acquisition"):
|
||||
for item in obl.get(kind, []):
|
||||
out.append(
|
||||
|
||||
@@ -64,6 +64,10 @@ _STANDALONE_WORDS = {
|
||||
"scaffold a new",
|
||||
"new web",
|
||||
}
|
||||
# T15 §15J: extensionless filenames that bind a scoped target when
|
||||
# mentioned explicitly (dotted/path tokens go through classify_surface)
|
||||
_KNOWN_FILES = {"readme", "makefile", "dockerfile", "changelog", "license",
|
||||
"gemfile", "procfile", "jenkinsfile", "justfile"}
|
||||
_LOCAL_WORDS = {
|
||||
"typo",
|
||||
"small fix",
|
||||
@@ -161,6 +165,26 @@ class ConstructionMode:
|
||||
"proposed_by_model": self.proposed_by_model,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict) -> "ConstructionMode":
|
||||
known = {
|
||||
k: d[k]
|
||||
for k in (
|
||||
"mode",
|
||||
"evidence",
|
||||
"target_project",
|
||||
"project_authority",
|
||||
"confidence",
|
||||
"clarification_required",
|
||||
"probe_questions",
|
||||
"bootstrap_required",
|
||||
"override",
|
||||
"proposed_by_model",
|
||||
)
|
||||
if k in d
|
||||
}
|
||||
return cls(**known)
|
||||
|
||||
|
||||
def _tokens(text: str) -> set[str]:
|
||||
return set(re.split(r"[^a-z0-9]+", text.lower()))
|
||||
@@ -226,7 +250,8 @@ def classify_mode(
|
||||
# construction request (§4: "build confidence", "make a list")
|
||||
non_constructible = {"list", "confidence", "explanation", "summary", "chord", "note"}
|
||||
obj_tokens = _tokens(obj)
|
||||
if not has_verb or obj_tokens & non_constructible:
|
||||
standalone_evidence = bool(_contains_any(obj.lower(), _STANDALONE_WORDS))
|
||||
if not has_verb or (obj_tokens & non_constructible and not standalone_evidence):
|
||||
ev.append(f"non-construction intent markers: {notc[:3]}")
|
||||
ev.append(
|
||||
"construction-verb object is not a buildable artifact" if has_verb else "no construction verb present"
|
||||
@@ -246,10 +271,54 @@ def classify_mode(
|
||||
standalone = _contains_any(obj.lower(), _STANDALONE_WORDS)
|
||||
local = _contains_any(obj.lower(), _LOCAL_WORDS)
|
||||
scoped = _contains_any(obj.lower(), _SCOPING_WORDS)
|
||||
# T15 §15J: an explicit FILE mention binds a scoped target ("fix the
|
||||
# typo in the README", "update scheduler.js"). Deterministic lexical
|
||||
# evidence in the same corroboration style as the other lexicons;
|
||||
# dotted/path tokens delegated to the ONE surface authority
|
||||
# (construction_decision.classify_surface) — lazily imported (cycle:
|
||||
# construction_decision imports this module's classify_mode).
|
||||
file_scoped = False
|
||||
for w in re.split(r"[^a-z0-9_./-]+", obj.lower()):
|
||||
if not w or len(w) < 3:
|
||||
continue
|
||||
if w in _KNOWN_FILES or "." in w or "/" in w:
|
||||
try:
|
||||
from rune.construction_decision import classify_surface
|
||||
if classify_surface(w) in ("source_path", "workflow_config"):
|
||||
file_scoped = True
|
||||
break
|
||||
except Exception: # noqa: BLE001,S110 — authority unavailable:
|
||||
pass # lexical fallback decides; never fatal
|
||||
if w in _KNOWN_FILES:
|
||||
file_scoped = True
|
||||
break
|
||||
|
||||
# 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 —
|
||||
@@ -265,17 +334,34 @@ def classify_mode(
|
||||
mode, conf = "PROJECT_BUILD", 0.9
|
||||
bootstrap = True
|
||||
# 4. LOCAL_CHANGE: explicit smallness + scoped target + no standalone
|
||||
elif local and (scoped or target_project or project_exists):
|
||||
elif local and (scoped or file_scoped or target_project or project_exists):
|
||||
ev.append(f"smallness markers: {local[:3]}")
|
||||
if scoped:
|
||||
ev.append(f"scoping words bind to known target: {scoped[:3]}")
|
||||
if file_scoped:
|
||||
ev.append("file mention binds scoped target")
|
||||
mode, conf = "LOCAL_CHANGE", 0.8
|
||||
# 5. EXISTING_PROJECT_CHANGE: scoping to a known/existing target
|
||||
# 5. EXISTING_PROJECT_CHANGE: scoping to a known/existing target.
|
||||
# T11 §1: a change scoped to a project with PROVEN non-existence must
|
||||
# NOT silently claim existing-project semantics — clarify instead.
|
||||
elif (scoped or target_project or project_exists is True) and has_verb:
|
||||
ev.append("change scoped to an existing project/target")
|
||||
if target_project:
|
||||
ev.append(f"target project: {target_project}")
|
||||
mode, conf = "EXISTING_PROJECT_CHANGE", 0.8
|
||||
if project_exists is False:
|
||||
ev.append(
|
||||
"BUT structural evidence proves the target project "
|
||||
"does not exist — refusing to manufacture existing-"
|
||||
"project semantics"
|
||||
)
|
||||
mode, conf, clarification = "UNKNOWN", 0.3, True
|
||||
probes.append(
|
||||
f"project '{target_project or 'the target'}' has "
|
||||
"no structural record — is it new (project "
|
||||
"build) or does it live somewhere not bound?"
|
||||
)
|
||||
else:
|
||||
mode, conf = "EXISTING_PROJECT_CHANGE", 0.8
|
||||
# 6. construction verb but no scope and no standalone language → UNKNOWN
|
||||
elif has_verb:
|
||||
ev.append("construction verb present but no target-scope evidence and no standalone/new-lifecycle language")
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,118 @@
|
||||
"""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}
|
||||
|
||||
|
||||
# ── T18 §18H: structural drift detection (extends drift_resume; no
|
||||
# duplicate mechanism). Detects observable drift between a structural
|
||||
# model's claims and repository reality. REALITY WINS unless user-locked.
|
||||
|
||||
def detect_structural_drift(model, repo_root) -> list[dict]:
|
||||
"""Compare structural-model component claims against source reality.
|
||||
Observable cases: source file moved/missing (component identities
|
||||
carry 'source'), entities for files that no longer exist. Returns
|
||||
drift records in the classify_drift vocabulary; reality wins unless
|
||||
the claim is user-locked (authority == 'user' on the entity's
|
||||
evidence or identities.user_locked)."""
|
||||
import os
|
||||
from pathlib import Path as _P
|
||||
root = _P(repo_root)
|
||||
drifts = []
|
||||
for e in model.doc["entities"]:
|
||||
if e.get("kind") != "component":
|
||||
continue
|
||||
src = ((e.get("ext") or {}).get("source")
|
||||
or (e.get("identities") or {}).get("source"))
|
||||
if not src:
|
||||
continue
|
||||
locked = (e.get("identities") or {}).get("user_locked") is True
|
||||
expected = root / src
|
||||
if not expected.exists():
|
||||
# reality: file gone/moved — search one level for a moved twin
|
||||
moved = None
|
||||
if expected.name:
|
||||
for cand in root.rglob(expected.name):
|
||||
if cand.is_file():
|
||||
moved = str(cand.relative_to(root))
|
||||
break
|
||||
drifts.append({
|
||||
"claim": f"{e['id']} source at {src}",
|
||||
"class": "CONFLICTING" if locked else "MATERIAL",
|
||||
"evidence": (f"source missing; moved to {moved}"
|
||||
if moved else "source file absent from repo"),
|
||||
"reality_wins": not locked,
|
||||
"reconciliation": (f"update source to {moved}" if moved
|
||||
else "entity requires re-derivation or "
|
||||
"user-locked preservation"),
|
||||
})
|
||||
return drifts
|
||||
@@ -0,0 +1,472 @@
|
||||
"""event_wait.py — T15 §15D: event-driven wait as the CANONICAL DEFAULT
|
||||
for long-running construction operations (builders, CI, deploys).
|
||||
|
||||
GLOBAL WAITING INVARIANT (tranche brief):
|
||||
|
||||
EVENT DRIVES CONTINUATION.
|
||||
TIMEOUT DETECTS FAILURE OF CONTINUATION.
|
||||
|
||||
The W/R bridge (daemon.py + waitstore.py at RUNE_WR_ROOT, default
|
||||
~/.local/share/mimo-wait-resume) already provides: durable waits, event
|
||||
matching, per-session single-flight leases, process-tree ownership, and a
|
||||
resume driver. Until T15 its resume target was exclusively a MiMo
|
||||
session (global mimo-shaped resume_cmd), so Python controllers (stage
|
||||
drivers, builder sessions) could not be resumed by the bridge — which is
|
||||
exactly why fixed sleep/poll loops crept back into T12-T14.
|
||||
|
||||
This adapter CLOSES that gap with the smallest possible surface:
|
||||
|
||||
1. register_process_wait() — a controller/operation registers a
|
||||
PROCESS_EVENT wait (correlation: operation_id) whose per-wait
|
||||
resume_cmd writes a resume token to a local FIFO. Registration is a
|
||||
durable waitstore record: survives restarts, visible to reconciliation.
|
||||
|
||||
2. run_builder_under_events() — launches the real builder command under
|
||||
a notifier wrapper (separate session/process-group). The wrapper
|
||||
observes the builder's actual exit and POSTs the completion event to
|
||||
the daemon (/process-event). The wrapper is the event source; the
|
||||
driver does NOT wait on the builder directly.
|
||||
|
||||
3. await_resume_event() — the driver YIELDS: blocks in select() on the
|
||||
FIFO (no polling loop, no sleep floor) with a watchdog deadline. The
|
||||
daemon matches the event, advances the wait, and the per-wait
|
||||
resume_cmd fires immediately — the driver resumes on the EVENT, not
|
||||
after a configured delay.
|
||||
|
||||
4. Watchdog semantics: on timeout the driver performs a ONE-SHOT
|
||||
liveness inspection of the notifier process group (ALIVE → re-arm;
|
||||
GONE → PROCESS_LOST recovery). The timeout is a failure detector,
|
||||
never the scheduler.
|
||||
|
||||
Idempotency (15E) is inherited from the bridge: match() applies only to
|
||||
WAITING_EXTERNAL waits, so duplicate/late events cannot double-resume;
|
||||
correlation by operation_id (run_ref) means a stale event from a prior
|
||||
operation matches nothing; the reconciler never verifies PROCESS_EVENT
|
||||
waits against Gitea (the event is the authority).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
DEFAULT_WR_ROOT = Path.home() / ".local" / "share" / "mimo-wait-resume"
|
||||
FALLBACK_WR_ROOT = Path("/home/nick/.local/share/mimo-wait-resume")
|
||||
DEFAULT_DAEMON_URL = "http://127.0.0.1:8791"
|
||||
|
||||
# The notifier wrapper: runs the real builder command as its child,
|
||||
# observes the ACTUAL exit, and POSTs the completion event to the daemon.
|
||||
# It is the event SOURCE — deliberately independent of the waiting driver.
|
||||
_NOTIFIER_SRC = r"""
|
||||
import json, os, subprocess, sys, time, urllib.request
|
||||
op = sys.argv[1]; cmd = sys.argv[2:]
|
||||
start = time.time()
|
||||
proc = subprocess.run(cmd)
|
||||
completed_at = time.time()
|
||||
payload = {
|
||||
"operation_id": op,
|
||||
"exit_code": proc.returncode,
|
||||
"completed_at": completed_at,
|
||||
"started_at": start,
|
||||
}
|
||||
for attempt in range(3):
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
os.environ["T15_DAEMON_URL"] + "/process-event",
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=5) as r:
|
||||
r.read()
|
||||
break
|
||||
except Exception as e:
|
||||
if attempt == 2:
|
||||
sys.stderr.write(f"event POST failed: {e}\n")
|
||||
# best-effort: the watchdog is the safety net for lost events
|
||||
time.sleep(1)
|
||||
"""
|
||||
|
||||
|
||||
def wr_root() -> Path:
|
||||
"""Resolve the bridge DATA root. T17 17T finding: two live roots
|
||||
existed (the shared /mnt/workdrive/wr-bridge and the legacy
|
||||
~/.local/share/mimo-wait-resume) and an unpinned consumer silently
|
||||
read the stale one. Policy now: explicit env → shared root if it
|
||||
exists (the canonical production root) → legacy home (warn loudly —
|
||||
a stale-store split-brain must be visible, never silent)."""
|
||||
import sys as _sys
|
||||
env = os.environ.get("RUNE_WR_ROOT")
|
||||
if env:
|
||||
return Path(env)
|
||||
shared = Path("/mnt/workdrive/wr-bridge")
|
||||
if shared.exists():
|
||||
if DEFAULT_WR_ROOT.exists():
|
||||
_sys.stderr.write(
|
||||
"event_wait: RUNE_WR_ROOT unset — using shared bridge "
|
||||
f"root {shared} while legacy root {DEFAULT_WR_ROOT} also "
|
||||
"exists; pin RUNE_WR_ROOT to silence\n")
|
||||
return shared
|
||||
if DEFAULT_WR_ROOT.exists():
|
||||
return DEFAULT_WR_ROOT
|
||||
return FALLBACK_WR_ROOT
|
||||
|
||||
|
||||
# waitstore instances cached per data root (isolated tests use per-test
|
||||
# tmp roots; the daemon uses its install dir). Loading via importlib.util
|
||||
# keeps the dynamic import pyright-clean — no unresolvable-import warning.
|
||||
_waitstore_cache: dict[str, Any] = {}
|
||||
|
||||
|
||||
def _load_waitstore() -> Any:
|
||||
"""Load the bridge's OWN waitstore module (never a copy), bound to
|
||||
the adapter's data root. waitstore.py lives in the DAEMON CODE dir;
|
||||
in production WR_ROOT is that dir (code + data together); in isolated
|
||||
test environments WR_ROOT is a data-only dir, so the code dir is
|
||||
resolved separately: RUNE_WR_CODE env → WR_ROOT → known installs."""
|
||||
root = wr_root()
|
||||
# waitstore binds its DATA dir from WR_ROOT at import time. Pin
|
||||
# WR_ROOT to the adapter's authority so adapter-side and daemon-side
|
||||
# state agree — otherwise a mismatch silently splits the wait store
|
||||
# from the event matcher and continuation dies as a watchdog timeout.
|
||||
os.environ["WR_ROOT"] = str(root)
|
||||
cached = _waitstore_cache.get(str(root))
|
||||
if cached is not None:
|
||||
return cached
|
||||
code_dir = None
|
||||
for cand in (os.environ.get("RUNE_WR_CODE"), str(root),
|
||||
str(FALLBACK_WR_ROOT)):
|
||||
if cand and (Path(cand) / "waitstore.py").is_file():
|
||||
code_dir = Path(cand)
|
||||
break
|
||||
if code_dir is None:
|
||||
raise RuntimeError(
|
||||
"waitstore.py not found in known W/R roots "
|
||||
"(RUNE_WR_CODE, WR_ROOT, ~/.local/share/mimo-wait-resume)")
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"waitstore", str(code_dir / "waitstore.py"))
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"cannot load waitstore from {code_dir}")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
_waitstore_cache[str(root)] = mod
|
||||
return mod
|
||||
|
||||
|
||||
def register_process_wait(operation_id: str, *, contract_id: str = "",
|
||||
project: str = "", objective: str = "",
|
||||
watched_for: str = "",
|
||||
provider: str = "builder",
|
||||
event_type: str = "builder_exit",
|
||||
sha: str = "",
|
||||
watchdog_secs: int | None = None) -> dict:
|
||||
"""Register a durable PROCESS_EVENT wait for one operation. The
|
||||
per-wait resume_cmd writes a resume token to the operation's FIFO —
|
||||
the bridge can resume a Python controller, not just MiMo sessions.
|
||||
|
||||
provider/event_type are the adapter's event identity on the shared
|
||||
bridge (16E: ONE wait primitive; builder / Gitea-CI / future adapters
|
||||
differ ONLY in these identity fields + their liveness probe — the
|
||||
registration, FIFO rendezvous, matching, idempotency, restart
|
||||
recovery, and watchdog semantics are the same code for all).
|
||||
Defaults preserve the T15 builder semantics."""
|
||||
ws = _load_waitstore()
|
||||
fifo = fifo_path(operation_id)
|
||||
fifo.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Do NOT unlink+recreate an existing fifo: a caller that opened its
|
||||
# read end before registering (open_fifo_reader, §16Y #10 race fix)
|
||||
# would be orphaned onto the old inode while the daemon's resume
|
||||
# writer opened the new one. The rendezvous is create-if-missing.
|
||||
if not fifo.exists():
|
||||
os.mkfifo(fifo)
|
||||
os.chmod(fifo, 0o666) # cross-user bridge (see open_fifo_reader)
|
||||
writer = ("import sys,json,time,os;"
|
||||
"fd=os.open(sys.argv[1], os.O_WRONLY | os.O_NONBLOCK);"
|
||||
"os.write(fd, json.dumps({'wait_id': sys.argv[2],"
|
||||
" 'resumed_at': time.time()}).encode());os.close(fd)")
|
||||
rec = ws.new_wait(
|
||||
session_id=f"controller-{operation_id}",
|
||||
provider=provider,
|
||||
event_type=event_type,
|
||||
sha=sha,
|
||||
repo=project or "construction",
|
||||
run_ref=operation_id,
|
||||
expected="success",
|
||||
wait_kind="PROCESS_EVENT",
|
||||
target_kind="headless", # owned continuation target: FIFO writer
|
||||
objective=objective or f"builder operation {operation_id}",
|
||||
waited_for=watched_for or "builder process exit event",
|
||||
verify_when_woken="reload canonical state; exit canary; "
|
||||
"independent verification",
|
||||
on_success="continue programme continuation canary",
|
||||
on_failure="classify failure; recovery or block",
|
||||
resume_cmd=["python3", "-c", writer, str(fifo), "{wait_id}"],
|
||||
)
|
||||
rec["ext"] = {"contract_id": contract_id, "operation_id": operation_id,
|
||||
"fifo": str(fifo),
|
||||
"watchdog_secs": watchdog_secs
|
||||
or int(os.environ.get("WR_WATCH_SECS", "600"))}
|
||||
ws.save(rec)
|
||||
# cross-user bridge (T16 §16C live finding): the daemon reconciler may
|
||||
# run under a different account than the registering controller — the
|
||||
# durable record must be readable/writable on both sides
|
||||
try:
|
||||
os.chmod(ws.WAITS / f"{rec['wait_id']}.json", 0o666)
|
||||
except OSError:
|
||||
pass # single-user deployment: nothing to relax
|
||||
return rec
|
||||
|
||||
|
||||
def fifo_path(operation_id: str) -> Path:
|
||||
return wr_root() / "resumes" / f"{operation_id}.fifo"
|
||||
|
||||
|
||||
def open_fifo_reader(operation_id: str) -> int:
|
||||
"""Open the operation's FIFO read end (O_NONBLOCK) BEFORE the wait is
|
||||
registered. §16Y adversarial #10: an event that arrives after
|
||||
registration but before the controller enters select() would find no
|
||||
reader — the resume write fails (ENXIO) and the wait lands
|
||||
RESUME_UNVERIFIED while the controller sleeps until the watchdog.
|
||||
Holding the reader open across the registration window closes that
|
||||
race for every adapter; the fd is passed to await_resume_event and
|
||||
closed by the caller."""
|
||||
fifo = fifo_path(operation_id)
|
||||
fifo.parent.mkdir(parents=True, exist_ok=True)
|
||||
if fifo.exists():
|
||||
fifo.unlink()
|
||||
os.mkfifo(fifo)
|
||||
# CROSS-USER BRIDGE (T17 17C root-cause, re-landed): the T16 chmod was
|
||||
# lost in a rebase resolution — FIFOs landed 644 (666 & umask 022),
|
||||
# and the daemon running as a DIFFERENT user got EACCES on the resume
|
||||
# write (rc=1 → RESUME_UNVERIFIED) while same-user tests passed.
|
||||
# chmod AFTER mkfifo is the only reliable act — umask never applies.
|
||||
os.chmod(fifo, 0o666)
|
||||
return os.open(fifo, os.O_RDONLY | os.O_NONBLOCK)
|
||||
|
||||
|
||||
def run_builder_under_events(builder_cmd: list[str], operation_id: str, *,
|
||||
daemon_url: str = DEFAULT_DAEMON_URL):
|
||||
"""Launch the REAL builder under the event notifier (separate process
|
||||
group). Returns the Popen for the notifier (pgid == its pid). The
|
||||
driver must NOT wait on this directly — it yields via
|
||||
await_resume_event; the notifier's exit event comes back through the
|
||||
bridge."""
|
||||
env = dict(os.environ, T15_DAEMON_URL=daemon_url)
|
||||
log_file = wr_root() / "resumes" / f"{operation_id}.builder.log"
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(log_file, "w") as lf:
|
||||
return subprocess.Popen(
|
||||
[sys.executable, "-c", _NOTIFIER_SRC, operation_id, *builder_cmd],
|
||||
stdout=lf, stderr=subprocess.PIPE,
|
||||
text=True, start_new_session=True, env=env)
|
||||
|
||||
|
||||
def _pgroup_alive(pgid: int) -> bool:
|
||||
"""Liveness = at least one NON-ZOMBIE member of the process group.
|
||||
A killed-but-unreaped notifier is a zombie and does NOT count —
|
||||
treating zombies as alive would misclassify PROCESS_LOST as alive."""
|
||||
try:
|
||||
os.killpg(pgid, 0) # group exists at all?
|
||||
except (ProcessLookupError, PermissionError):
|
||||
return False
|
||||
for proc in Path("/proc").iterdir():
|
||||
if not proc.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
stat = (proc / "stat").read_text()
|
||||
# field 5 is state; comm (field 2) may contain spaces/parens
|
||||
state = stat.rsplit(")", 1)[1].split()[0]
|
||||
pgid_field = int(stat.rsplit(")", 1)[1].split()[2])
|
||||
except (OSError, ValueError, IndexError):
|
||||
continue
|
||||
if pgid_field == pgid and state != "Z":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _cmdline_alive(needle: str) -> bool:
|
||||
"""Liveness fallback: any live process whose cmdline contains the
|
||||
needle. Real builders (node CLIs) may re-group their children, so
|
||||
pgid alone can false-negative — T15Q observed a 65-minute real build
|
||||
outliving the watchdog AND invisible to the pgid scan. The needle
|
||||
should be operation-specific (the prompt marker), not generic."""
|
||||
for proc in Path("/proc").iterdir():
|
||||
if not proc.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
cmdline = (proc / "cmdline").read_bytes().decode(
|
||||
errors="replace")
|
||||
stat = (proc / "stat").read_text()
|
||||
state = stat.rsplit(")", 1)[1].split()[0]
|
||||
except (OSError, ValueError, IndexError):
|
||||
continue
|
||||
if needle in cmdline and state != "Z":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def liveness(operation_id: str, pgid: int | None = None) -> bool:
|
||||
"""Watchdog helper: is the watched operation actually alive? pgid
|
||||
membership first; cmdline evidence as the regroup-tolerant fallback."""
|
||||
if pgid and _pgroup_alive(pgid):
|
||||
return True
|
||||
return _cmdline_alive(f"op:{operation_id}")
|
||||
|
||||
|
||||
def await_resume_event(operation_id: str, *, watchdog_secs: int,
|
||||
pgid: int | None = None,
|
||||
max_rearms: int = 20,
|
||||
fifo_fd: int | None = None) -> dict:
|
||||
"""YIELD until the bridge resumes this controller. Blocks in select()
|
||||
on the FIFO — no sleep, no polling.
|
||||
|
||||
fifo_fd: a pre-opened read end from open_fifo_reader() (§16Y #10 race
|
||||
fix — the fd must already exist when the wait becomes registered).
|
||||
Caller-owned fds are never closed here; on EOF (writer gone, no
|
||||
token) the call returns {"kind": "eof"} instead of busy-looping on an
|
||||
always-readable fd — the caller falls back to the durable waitstore
|
||||
record via resume_or_recover/operation_wait.
|
||||
|
||||
Watchdog semantics (§15D): the deadline is a FAILURE DETECTOR, never
|
||||
the scheduler. On expiry, liveness is inspected ONCE (pgid membership,
|
||||
then operation-specific cmdline evidence — real builders regroup their
|
||||
children): ALIVE → re-arm (select again, up to max_rearms); GONE →
|
||||
PROCESS_LOST. The T15Q live run proved why: a real 65-minute model
|
||||
build outlived a single 900s deadline, and node regrouping hid the
|
||||
processes from the pgid scan — neither is a failure of continuation.
|
||||
"""
|
||||
owned_fd = fifo_fd is None
|
||||
if owned_fd:
|
||||
fifo = fifo_path(operation_id)
|
||||
# idempotent re-arm: a prior await consumed+unlinked the fifo; the
|
||||
# rendezvous point is (re)created so watchdog re-arm and recovery
|
||||
# paths can wait again on the same operation
|
||||
fifo.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not fifo.exists():
|
||||
os.mkfifo(fifo)
|
||||
fd = os.open(fifo, os.O_RDONLY | os.O_NONBLOCK)
|
||||
else:
|
||||
fd = fifo_fd
|
||||
try:
|
||||
for arm in range(max_rearms + 1):
|
||||
deadline = time.monotonic() + watchdog_secs
|
||||
while True:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
break
|
||||
r, _, _ = select.select([fd], [], [], remaining)
|
||||
if not r:
|
||||
continue
|
||||
data = os.read(fd, 4096)
|
||||
if not data:
|
||||
if owned_fd:
|
||||
continue
|
||||
# caller-owned fd at EOF: writer is gone without
|
||||
# delivering a token — never a busy loop; the durable
|
||||
# record decides what actually happened
|
||||
return {"kind": "eof"}
|
||||
try:
|
||||
token = json.loads(data.decode())
|
||||
except ValueError:
|
||||
break # malformed token → treat as expiry
|
||||
ws = _load_waitstore()
|
||||
rec = ws.load(token.get("wait_id", ""))
|
||||
return {"kind": "event", "wait": rec,
|
||||
"resumed_at": token.get("resumed_at")}
|
||||
# deadline expired: liveness decides re-arm vs PROCESS_LOST
|
||||
alive = liveness(operation_id, pgid)
|
||||
if not alive:
|
||||
return {"kind": "watchdog", "alive": False,
|
||||
"rearms": arm}
|
||||
return {"kind": "watchdog", "alive": liveness(operation_id, pgid),
|
||||
"rearms": max_rearms}
|
||||
finally:
|
||||
if owned_fd:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def operation_wait(operation_id: str) -> dict | None:
|
||||
ws = _load_waitstore()
|
||||
for rec in ws.all_waits():
|
||||
if rec.get("ext", {}).get("operation_id") == operation_id:
|
||||
return rec
|
||||
return None
|
||||
|
||||
|
||||
def resume_or_recover(operation_id: str) -> dict:
|
||||
"""15E case 10 — controller restart after the event was persisted but
|
||||
before the controller handled it. The durable waitstore record is the
|
||||
authority, not the transient FIFO token: if the wait already reached a
|
||||
terminal/resumable state, return it directly (idempotent recovery, no
|
||||
duplicate transition); if still WAITING_EXTERNAL, re-arm the normal
|
||||
event wait."""
|
||||
rec = operation_wait(operation_id)
|
||||
if rec is None:
|
||||
return {"kind": "unknown_operation"}
|
||||
if rec.get("status") in ("COMPLETED", "FAILED", "READY_TO_RESUME",
|
||||
"READY_TO_RETRY", "RESUMING"):
|
||||
# RESUMING counts as recovered: the resume delivery is in flight
|
||||
# (or already consumed the rendezvous). The durable record is the
|
||||
# truth; the controller restarts from it, never from the token.
|
||||
return {"kind": "event", "wait": rec, "recovered": True}
|
||||
if rec.get("status") == "RESUME_UNVERIFIED" and \
|
||||
rec.get("process_event_evidence"):
|
||||
# the completion EVENT was processed (verdict recorded durably);
|
||||
# only the resume-token delivery was unverified (e.g. controller
|
||||
# was down, so no reader consumed the rendezvous). Canonical
|
||||
# state wins — this IS the restart-after-event case.
|
||||
return {"kind": "event", "wait": rec, "recovered": True}
|
||||
return {"kind": "still_waiting", "wait": rec}
|
||||
|
||||
|
||||
def find_matching_event(provider: str, event_type: str, run_ref: str) -> dict | None:
|
||||
"""Search the bridge's durable event log for an event matching this
|
||||
identity. T16 §16C live finding: on a serialized single-runner
|
||||
machine the deploy job ALWAYS starts after ci.yml completes — the
|
||||
completion event lands before the waiter registers, and a pure
|
||||
forward-only FIFO rendezvous can never deliver it (both production
|
||||
cycles fell back to the 900s deadline). Events are durable precisely
|
||||
so they can be replayed: a waiter that finds a matching event at
|
||||
registration goes straight to the authority read instead of sleeping
|
||||
for a wake that already happened."""
|
||||
import json as _json
|
||||
ws = _load_waitstore()
|
||||
events_dir = ws.EVENTS
|
||||
if not events_dir.is_dir():
|
||||
return None
|
||||
best = None
|
||||
for f in sorted(events_dir.glob("*.json"),
|
||||
key=lambda p: p.stat().st_mtime, reverse=True):
|
||||
try:
|
||||
rec = _json.loads(f.read_text())
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
ev = rec.get("event", {})
|
||||
if (ev.get("provider") == provider
|
||||
and ev.get("event_type") == event_type
|
||||
and str(ev.get("run_ref", "")) == run_ref):
|
||||
best = ev # newest match wins
|
||||
break
|
||||
return best
|
||||
|
||||
|
||||
def post_process_event(operation_id: str, exit_code: int, *,
|
||||
daemon_url: str = DEFAULT_DAEMON_URL) -> dict:
|
||||
"""Direct event injection (test seam + manual reconciliation). The
|
||||
normal path is the notifier wrapper; this exists so tests and
|
||||
watchdog recovery can emit the same event shape."""
|
||||
import urllib.request
|
||||
payload = {"operation_id": operation_id, "exit_code": exit_code,
|
||||
"completed_at": time.time()}
|
||||
req = urllib.request.Request(
|
||||
daemon_url + "/process-event",
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=5) as r:
|
||||
return json.loads(r.read())
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""failure_recovery.py — T15 §15T: failure classification + recovery
|
||||
policy.
|
||||
|
||||
Before retrying, classify the failure:
|
||||
|
||||
TRANSIENT bounded immediate retry appropriate
|
||||
INTERMITTENT bounded retry/backoff
|
||||
KNOWN_PROCESS_DEFECT apply evidenced repair, then retry
|
||||
PERMANENT / INVALID do NOT retry blindly
|
||||
APPROVAL_REQUIRED block
|
||||
UNKNOWN bounded evidence probe, then block/escalate
|
||||
|
||||
No infinite retry loops: every class carries a retry budget, and
|
||||
PERMANENT/APPROVAL_REQUIRED never auto-retry.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
TRANSIENT = "TRANSIENT"
|
||||
INTERMITTENT = "INTERMITTENT"
|
||||
KNOWN_PROCESS_DEFECT = "KNOWN_PROCESS_DEFECT"
|
||||
PERMANENT = "PERMANENT"
|
||||
APPROVAL_REQUIRED = "APPROVAL_REQUIRED"
|
||||
PROVIDER_UNAVAILABLE = "PROVIDER_UNAVAILABLE"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
CLASSES = (TRANSIENT, INTERMITTENT, KNOWN_PROCESS_DEFECT, PERMANENT,
|
||||
APPROVAL_REQUIRED, PROVIDER_UNAVAILABLE, UNKNOWN)
|
||||
|
||||
# recovery policy per class: (max_retries, action)
|
||||
POLICY = {
|
||||
TRANSIENT: (2, "retry_immediately"),
|
||||
INTERMITTENT: (3, "retry_with_backoff"),
|
||||
KNOWN_PROCESS_DEFECT: (1, "apply_evidenced_repair_then_retry"),
|
||||
PERMANENT: (0, "do_not_retry"),
|
||||
APPROVAL_REQUIRED: (0, "block"),
|
||||
# T16 §16F: provider failures NEVER auto-retry and NEVER fall back
|
||||
# to another provider — preserve the real error, block the branch.
|
||||
PROVIDER_UNAVAILABLE: (0, "block_branch_preserve_error"),
|
||||
UNKNOWN: (1, "bounded_evidence_probe_then_block_or_escalate"),
|
||||
}
|
||||
|
||||
# deterministic markers (substring, lowercase) — the classifier is
|
||||
# evidence-first: explicit signals win; absence defaults to UNKNOWN
|
||||
_TRANSIENT_MARKERS = (
|
||||
"connection reset", "timeout", "timed out", "temporarily unavailable",
|
||||
"rate limit", "429", "503", "econnreset", "socket hang up",
|
||||
"service unavailable",
|
||||
)
|
||||
_INTERMITTENT_MARKERS = (
|
||||
"flaky", "intermittent", "occasionally fails", "sometimes fails",
|
||||
"retry succeeded",
|
||||
)
|
||||
_PERMANENT_MARKERS = (
|
||||
"invalid input", "schema-invalid", "not found", "no such file",
|
||||
"permission denied", "unsupported", "incompatible",
|
||||
"module not found", "syntaxerror", "typeerror",
|
||||
)
|
||||
_APPROVAL_MARKERS = (
|
||||
"approval required", "needs authorization", "user confirmation",
|
||||
"destructive", "external service", "new repository",
|
||||
"user-locked", "user locked",
|
||||
)
|
||||
_KNOWN_DEFECT_MARKERS = (
|
||||
"stale document", "forgotten save", "wrong path", "guessed api",
|
||||
"missing import", "no-op edit", "substring match", "wrong signature",
|
||||
)
|
||||
|
||||
|
||||
def classify_failure(error_text: str, *, context: dict | None = None) -> dict:
|
||||
"""Classify one failure into a recovery class. Deterministic: marker
|
||||
evidence first; explicit context hints (e.g. known defect id) second;
|
||||
UNKNOWN when nothing matches (never manufactured certainty)."""
|
||||
text = (error_text or "").lower()
|
||||
context = context or {}
|
||||
known_defect = context.get("known_defect_id")
|
||||
if known_defect:
|
||||
return {"class": KNOWN_PROCESS_DEFECT,
|
||||
"evidence": f"known defect: {known_defect}",
|
||||
"policy": POLICY[KNOWN_PROCESS_DEFECT]}
|
||||
# T16 §16F: an explicit provider-context failure wins over every
|
||||
# generic marker — the real status/API code is preserved verbatim in
|
||||
# the evidence, the branch blocks, and no fallback is attempted.
|
||||
if context.get("provider_error"):
|
||||
return {"class": PROVIDER_UNAVAILABLE,
|
||||
"evidence": (f"provider error preserved: {error_text}"
|
||||
if error_text else "provider error"),
|
||||
"policy": POLICY[PROVIDER_UNAVAILABLE]}
|
||||
if any(m in text for m in _APPROVAL_MARKERS):
|
||||
return {"class": APPROVAL_REQUIRED,
|
||||
"evidence": "approval marker present",
|
||||
"policy": POLICY[APPROVAL_REQUIRED]}
|
||||
if any(m in text for m in _PERMANENT_MARKERS):
|
||||
return {"class": PERMANENT, "evidence": "permanent marker present",
|
||||
"policy": POLICY[PERMANENT]}
|
||||
if any(m in text for m in _KNOWN_DEFECT_MARKERS):
|
||||
return {"class": KNOWN_PROCESS_DEFECT,
|
||||
"evidence": "known process-defect marker present",
|
||||
"policy": POLICY[KNOWN_PROCESS_DEFECT]}
|
||||
if any(m in text for m in _INTERMITTENT_MARKERS):
|
||||
return {"class": INTERMITTENT,
|
||||
"evidence": "intermittent marker present",
|
||||
"policy": POLICY[INTERMITTENT]}
|
||||
if any(m in text for m in _TRANSIENT_MARKERS):
|
||||
return {"class": TRANSIENT, "evidence": "transient marker present",
|
||||
"policy": POLICY[TRANSIENT]}
|
||||
return {"class": UNKNOWN, "evidence": "no marker matched",
|
||||
"policy": POLICY[UNKNOWN]}
|
||||
@@ -0,0 +1,142 @@
|
||||
"""forge.py — T18 §18L: minimum stable forge abstraction boundary.
|
||||
|
||||
Rune today is Gitea-centric; Scaffolding must not make Gitea itself part
|
||||
of canonical construction semantics. This module defines the boundary:
|
||||
Git remains the source-history authority; a Forge adapter handles the
|
||||
remote-services surface (push/CI status/PR/protected checks/release
|
||||
metadata). GiteaForge is the real working adapter, delegating to the
|
||||
existing gitea_auth + scripts (rule A: no duplicate implementation).
|
||||
|
||||
A future Forgejo/GitHub/GitLab adapter implements the same interface
|
||||
without changing Scaffolding's core model.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class ForgeAdapter(Protocol):
|
||||
"""The minimum stable forge boundary. All methods may raise
|
||||
ForgeUnavailable — callers treat forge failures as blocking-for-that-
|
||||
operation, never as construction-truth changes."""
|
||||
|
||||
def push(self, repo: str, branch: str) -> dict: ...
|
||||
|
||||
def commit_status(self, repo: str, sha: str) -> str: ...
|
||||
|
||||
def ci_runs_for_sha(self, repo: str, sha: str) -> list[dict]: ...
|
||||
|
||||
def create_or_patch_release(self, repo: str, tag: str,
|
||||
title: str, body: str) -> dict: ...
|
||||
|
||||
|
||||
class ForgeUnavailable(RuntimeError):
|
||||
"""The forge could not be reached or refused the operation."""
|
||||
|
||||
|
||||
class GiteaForge:
|
||||
"""Real adapter. Delegates to the existing canonical implementations
|
||||
(gitea_auth credential ladder; deploy_qualify verdict logic) — no
|
||||
duplicated API clients (rule A)."""
|
||||
|
||||
def __init__(self, server: str | None = None, repo: str = "conductor/rune"):
|
||||
self._server = server # None → scripts' env/credential resolution
|
||||
self._repo = repo
|
||||
|
||||
def _env(self) -> dict:
|
||||
import os
|
||||
env = dict(os.environ)
|
||||
if self._server:
|
||||
env["GITEA_SERVER"] = self._server
|
||||
env.setdefault("GITEA_REPO", self._repo)
|
||||
return env
|
||||
|
||||
def push(self, repo: str, branch: str) -> dict:
|
||||
import subprocess
|
||||
proc = subprocess.run(["git", "push", "gitea", f"master:{branch}"],
|
||||
capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
raise ForgeUnavailable(f"push failed: {proc.stderr[:200]}")
|
||||
return {"pushed": True, "branch": branch}
|
||||
|
||||
@staticmethod
|
||||
def _gitea_auth():
|
||||
"""Canonical scripts/gitea_auth credential ladder (rule A: no
|
||||
duplicate auth). scripts/ is not a package — path shim, the
|
||||
established spec-loaded-scripts pattern."""
|
||||
import sys
|
||||
from pathlib import Path as _P
|
||||
scripts = _P(__file__).resolve().parents[2] / "scripts"
|
||||
if str(scripts) not in sys.path:
|
||||
sys.path.insert(0, str(scripts))
|
||||
import gitea_auth
|
||||
return gitea_auth
|
||||
|
||||
def commit_status(self, repo: str, sha: str) -> str:
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
gitea_auth = self._gitea_auth()
|
||||
server = self._server or os.environ.get(
|
||||
"GITEA_SERVER", "https://gitea.thingscouldgetdicey.online")
|
||||
if not gitea_auth.has_token():
|
||||
raise ForgeUnavailable("no forge credentials (gitea token)")
|
||||
req = urllib.request.Request(
|
||||
f"{server}/api/v1/repos/{repo}/commits/{sha}/status",
|
||||
headers={"Authorization": f"token {gitea_auth.get_token()}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
data = json.loads(r.read())
|
||||
except OSError as exc:
|
||||
raise ForgeUnavailable(f"status unreachable: {exc}") from exc
|
||||
return data.get("state", "unknown")
|
||||
|
||||
def ci_runs_for_sha(self, repo: str, sha: str) -> list[dict]:
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
gitea_auth = self._gitea_auth()
|
||||
server = self._server or os.environ.get(
|
||||
"GITEA_SERVER", "https://gitea.thingscouldgetdicey.online")
|
||||
if not gitea_auth.has_token():
|
||||
raise ForgeUnavailable("no forge credentials (gitea token)")
|
||||
req = urllib.request.Request(
|
||||
f"{server}/api/v1/repos/{repo}/actions/runs?limit=50",
|
||||
headers={"Authorization": f"token {gitea_auth.get_token()}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
data = json.loads(r.read())
|
||||
except OSError as exc:
|
||||
raise ForgeUnavailable(f"runs unreachable: {exc}") from exc
|
||||
return [run for run in data.get("workflow_runs", [])
|
||||
if str(run.get("head_sha", "")) == sha]
|
||||
|
||||
def create_or_patch_release(self, repo: str, tag: str,
|
||||
title: str, body: str) -> dict:
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
gitea_auth = self._gitea_auth()
|
||||
server = self._server or os.environ.get(
|
||||
"GITEA_SERVER", "https://gitea.thingscouldgetdicey.online")
|
||||
if not gitea_auth.has_token():
|
||||
raise ForgeUnavailable("no forge credentials (gitea token)")
|
||||
# delegate to the canonical idempotent publisher
|
||||
import subprocess
|
||||
import sys
|
||||
script = None
|
||||
for cand in ("scripts/create_release.py",):
|
||||
if (sys.path and __import__("pathlib").Path(cand).is_file()):
|
||||
script = cand
|
||||
break
|
||||
if script:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, script, "--tag", tag, "--title", title,
|
||||
"--body", body], capture_output=True, text=True)
|
||||
return {"rc": proc.returncode, "stdout": proc.stdout[-400:]}
|
||||
raise ForgeUnavailable("canonical release publisher not found")
|
||||
|
||||
|
||||
def default_forge() -> ForgeAdapter:
|
||||
"""The working adapter (rule 18L: prove Gitea still works)."""
|
||||
return GiteaForge()
|
||||
@@ -0,0 +1,108 @@
|
||||
"""graph_projection.py — T18 §18Q: stable versioned READ-ONLY graph
|
||||
projection from a structural Scaffold, suitable for later local-web
|
||||
visualisation, GUI, TUI tree, or programmatic queries.
|
||||
|
||||
This is a PROJECTION — never another authority (rule 18Q). Structural
|
||||
truth remains the Scaffold (structural.json) / Spine. Built THROUGH the
|
||||
self-scaffolded upgrade (contract 18q-graph-projection): the upgrade
|
||||
started from the maintained Scaffold, per the 18P proof.
|
||||
|
||||
Formats: versioned JSON (programmatic), DOT and Mermaid (visual —
|
||||
dependency-cruiser-proven human-usable formats per the 18D research).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
PROJECTION_SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
def project_json(model) -> dict:
|
||||
"""Stable versioned JSON projection: nodes + edges, no internal
|
||||
storage layout leaked (Substrate-safe seam, rule 18E)."""
|
||||
nodes = []
|
||||
for e in model.doc["entities"]:
|
||||
nodes.append({
|
||||
"id": e["id"],
|
||||
"kind": e.get("kind"),
|
||||
"maturity": e.get("maturity"),
|
||||
"name": (e.get("identities") or {}).get("name"),
|
||||
"ext": {k: v for k, v in (e.get("ext") or {}).items()
|
||||
if k in ("source", "bounded_adapter")},
|
||||
})
|
||||
edges = []
|
||||
for r in model.doc["relationships"]:
|
||||
edges.append({
|
||||
"type": r.get("type"),
|
||||
"from": r.get("from"),
|
||||
"to": r.get("to"),
|
||||
"authority": r.get("authority"),
|
||||
"status": r.get("status"),
|
||||
})
|
||||
return {
|
||||
"schema_version": PROJECTION_SCHEMA_VERSION,
|
||||
"project": getattr(model, "project_id", ""),
|
||||
"generated_at": _now(),
|
||||
"node_count": len(nodes),
|
||||
"edge_count": len(edges),
|
||||
"nodes": nodes,
|
||||
"edges": edges,
|
||||
}
|
||||
|
||||
|
||||
def project_dot(model) -> str:
|
||||
"""Graphviz DOT — clusters per domain, edges labeled by type."""
|
||||
lines = ["digraph structural {", " rankdir=LR;",
|
||||
' node [shape=box, style="rounded,filled", fillcolor="#eef"];']
|
||||
for e in model.doc["entities"]:
|
||||
eid = e["id"].replace('"', "'")
|
||||
label = (e.get("identities") or {}).get("name") or \
|
||||
e["id"].split(":", 1)[-1]
|
||||
lines.append(f' "{eid}" [label="{label} ({e["kind"]})"];')
|
||||
for r in model.doc["relationships"]:
|
||||
if r["type"] == "belongs_to" and r.get("status") == "root":
|
||||
continue
|
||||
a = str(r["from"]).replace('"', "'")
|
||||
b = str(r["to"]).replace('"', "'")
|
||||
lines.append(f' "{a}" -> "{b}" [label="{r["type"]}"];')
|
||||
lines.append("}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def project_mermaid(model) -> str:
|
||||
"""Mermaid flowchart — renders on GitHub/GitLab/many viewers."""
|
||||
lines = ["graph LR"]
|
||||
ids = {e["id"] for e in model.doc["entities"]}
|
||||
safe = {}
|
||||
for i, eid in enumerate(sorted(ids)):
|
||||
safe[eid] = f"n{i}"
|
||||
for e in model.doc["entities"]:
|
||||
label = ((e.get("identities") or {}).get("name")
|
||||
or e["id"].split(":", 1)[-1]).replace('"', "'")
|
||||
lines.append(f' {safe[e["id"]]}["{label} ({e["kind"]})"]')
|
||||
for r in model.doc["relationships"]:
|
||||
if r["type"] == "belongs_to" and r.get("status") == "root":
|
||||
continue
|
||||
if r["from"] in safe and r["to"] in safe:
|
||||
lines.append(f' {safe[r["from"]]} -->|{r["type"]}| '
|
||||
f'{safe[r["to"]]}')
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def write_all(model, out_dir) -> dict:
|
||||
"""Write all three formats; returns paths. Read-only over truth —
|
||||
writing projections never mutates the model."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
out = Path(out_dir)
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
j = out / "structural_graph.json"
|
||||
d = out / "structural_graph.dot"
|
||||
mm = out / "structural_graph.mmd"
|
||||
j.write_text(json.dumps(project_json(model), indent=1, sort_keys=True))
|
||||
d.write_text(project_dot(model))
|
||||
mm.write_text(project_mermaid(model))
|
||||
return {"json": str(j), "dot": str(d), "mermaid": str(mm)}
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
import time
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
@@ -0,0 +1,69 @@
|
||||
"""impact_preview.py — T18 §18G: visible change-impact preview.
|
||||
|
||||
For consequential changes, a user-visible STRUCTURED projection of what
|
||||
a construction will touch — BEFORE build. Derived from the structural
|
||||
Scaffold's impact closure + the Build Basis summary. Contains
|
||||
structured conclusions/evidence only — NEVER private chain-of-thought
|
||||
(rule 18G). This is a projection, not an authority.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
PREVIEW_SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
def build_preview(model, closure: dict, *, basis: dict | None = None,
|
||||
contract_scope: list[str] | None = None) -> dict:
|
||||
"""Assemble the preview from an impact_closure (structural_model)
|
||||
and optionally the Build Basis summary. Pure derivation — no hidden
|
||||
reasoning, no mutation."""
|
||||
if closure.get("error"):
|
||||
return {"schema_version": PREVIEW_SCHEMA_VERSION,
|
||||
"error": closure["error"]}
|
||||
scope = set(closure.get("mutation_scope", []))
|
||||
observation = closure.get("observation_scope") or {}
|
||||
deps = observation.get("direct_dependencies", [])
|
||||
# candidate files: identities' source paths if modeled
|
||||
candidate_files = []
|
||||
for eid in sorted(scope):
|
||||
e = model.get(eid) or {}
|
||||
src = (e.get("identities") or {}).get("source")
|
||||
if src:
|
||||
candidate_files.append(src)
|
||||
return {
|
||||
"schema_version": PREVIEW_SCHEMA_VERSION,
|
||||
"target": closure.get("target"),
|
||||
"affected_domains": ([closure.get("owning_domain")]
|
||||
if closure.get("owning_domain") else []),
|
||||
"affected_subsystems": sorted(
|
||||
e for e in scope if str(e).startswith("subsystem:")),
|
||||
"consumers_connectors": sorted(
|
||||
e for e in scope if e != closure.get("target")),
|
||||
"candidate_files": sorted(set(candidate_files))[:50],
|
||||
"contract_scope_additions": sorted(
|
||||
set(contract_scope or []) - set(candidate_files)),
|
||||
"state_implications": closure.get("state_surfaces", []),
|
||||
"environment_runtime_implications": sorted(
|
||||
model.deployment_surfaces(closure["target"])
|
||||
if model.get(closure["target"]) else []),
|
||||
"migration_implications": (basis or {}).get("implications",
|
||||
{}).get("migration", []),
|
||||
"risks_uncertainties": [
|
||||
*(basis or {}).get("risks", []),
|
||||
*closure.get("uncertainty", []),
|
||||
],
|
||||
"verification_plan": closure.get("verification_scope", []),
|
||||
"rollback_implications": [
|
||||
"revert contract scope commits; deployment rollbacks via "
|
||||
"last-good symlink (canonical deploy machinery)"],
|
||||
"basis_summary": (basis or {}).get("depth"),
|
||||
}
|
||||
|
||||
|
||||
def validate_preview(preview: dict) -> list[str]:
|
||||
errs = []
|
||||
if preview.get("schema_version") != PREVIEW_SCHEMA_VERSION:
|
||||
errs.append("schema_version mismatch")
|
||||
for f in ("target", "verification_plan", "risks_uncertainties"):
|
||||
if f not in preview:
|
||||
errs.append(f"missing {f}")
|
||||
return errs
|
||||
@@ -0,0 +1,183 @@
|
||||
"""iteration.py — T19 §19E/19H/19K: the canonical dependency-aware
|
||||
iteration engine — the "quicksand" solution as STANDARD Scaffolding
|
||||
behaviour.
|
||||
|
||||
Canonical loop (rule 19H):
|
||||
|
||||
intent → target → research/Build Basis → scaffold reconcile →
|
||||
PREDICTIVE impact → bounded mutation scope + wider observation
|
||||
scope → preview → isolated construction → verification →
|
||||
DESCRIPTIVE impact → predicted-vs-observed delta → structural
|
||||
truth update → selective validation invalidation → obligations →
|
||||
iterate → harden.
|
||||
|
||||
Built ON the T18 foundation (rule C): structural_model.impact_closure,
|
||||
build_basis, impact_preview, drift_resume — no second anything.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from rune.capability_ledger import CapabilityLedger
|
||||
from rune.behaviour_ledger import BehaviourLedger
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
# ── 19E: surgical context packets ────────────────────────────────────────
|
||||
|
||||
def context_packet(model, ledger: CapabilityLedger, target: str, *,
|
||||
intent: str = "", basis_summary: dict | None = None,
|
||||
max_local_files: int = 12) -> dict:
|
||||
"""LOCAL + CONNECTED context for one structural target (rule 19E).
|
||||
Bounded and proportional: the whole graph is NEVER dumped. Local =
|
||||
the target's own ledger entry; connected = 1-hop structural
|
||||
neighbours with their ledgers on demand."""
|
||||
entity = model.get(target)
|
||||
if entity is None:
|
||||
return {"error": f"unknown structural target {target!r}"}
|
||||
caps = ledger.by_component(target)
|
||||
local = ledger.context_packet(caps[0]["capability_id"]) if caps else None
|
||||
beneath = [b for b in model.beneath(target)
|
||||
if not model.get(b).get("identities", {}).get("source")]
|
||||
files: list[str] = []
|
||||
for eid in [target] + model.children(target):
|
||||
e = model.get(eid) or {}
|
||||
src = (e.get("ext") or {}).get("source") or \
|
||||
(e.get("identities") or {}).get("source")
|
||||
if src:
|
||||
files.append(src)
|
||||
for cap in caps:
|
||||
files.extend(cap.get("source_locations", []))
|
||||
closure = model.impact_closure(target)
|
||||
connected = {
|
||||
"dependencies": model.dependencies(target),
|
||||
"consumers": model.consumers(target),
|
||||
"interfaces_provided": model.interfaces_for(target),
|
||||
"state_owned": model.state_owned(target),
|
||||
"verification": model.verification_surfaces(target),
|
||||
"deployment": model.deployment_surfaces(target),
|
||||
"uncertainty": closure.get("uncertainty", []),
|
||||
}
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"intent": intent,
|
||||
"target": target,
|
||||
"target_kind": entity.get("kind"),
|
||||
"owning_domain": model.owning_domain(target),
|
||||
"local": local,
|
||||
"local_files": sorted(set(files))[:max_local_files],
|
||||
"beneath_count": len(beneath),
|
||||
"connected": connected,
|
||||
"basis_summary": basis_summary,
|
||||
"generated_at": _now(),
|
||||
}
|
||||
|
||||
|
||||
# ── 19H: predictive vs descriptive impact ────────────────────────────────
|
||||
|
||||
def predictive_impact(model, target: str) -> dict:
|
||||
"""What MAY be affected BEFORE the change (structural closure)."""
|
||||
return model.impact_closure(target)
|
||||
|
||||
|
||||
def descriptive_impact(model, target: str, *,
|
||||
observed_changed: list[str],
|
||||
generation: str = "") -> dict:
|
||||
"""What was MATERIALLY affected AFTER the change — from actual
|
||||
evidence (diff/observations), never assumed from the graph alone."""
|
||||
predicted = model.impact_closure(target)
|
||||
predicted_scope = set(predicted.get("mutation_scope", []))
|
||||
observed = set(observed_changed)
|
||||
return {
|
||||
"target": target,
|
||||
"generation": generation,
|
||||
"predicted": sorted(predicted_scope),
|
||||
"observed": sorted(observed),
|
||||
"unexpected": sorted(observed - predicted_scope), # prediction missed
|
||||
"unaffected_as_predicted": sorted(predicted_scope - observed),
|
||||
"recorded_at": _now(),
|
||||
}
|
||||
|
||||
|
||||
def record_impact_delta(project_store: Path, delta: dict) -> Path:
|
||||
"""Persist predicted-vs-observed deltas — they compound into better
|
||||
future impact prediction (rule 19H)."""
|
||||
p = Path(project_store) / "impact_deltas.jsonl"
|
||||
import json
|
||||
with p.open("a") as f:
|
||||
f.write(json.dumps(delta, sort_keys=True) + "\n")
|
||||
return p
|
||||
|
||||
|
||||
# ── 19I: selective invalidation bridge ───────────────────────────────────
|
||||
|
||||
def invalidate_behaviours(model, behaviours: BehaviourLedger, *,
|
||||
changed_entities: set[str], generation: str = ""
|
||||
) -> list[dict]:
|
||||
"""Selective invalidation driven by STRUCTURAL truth: behaviours
|
||||
whose dependencies/owner intersect the changed closure reopen;
|
||||
everything else keeps its confidence (rule 19I)."""
|
||||
return behaviours.invalidate_for_change(
|
||||
changed_entities=changed_entities, generation=generation)
|
||||
|
||||
|
||||
# ── 19K: post-change obligations ─────────────────────────────────────────
|
||||
|
||||
def post_change_obligations(model, target: str, *,
|
||||
behaviours: BehaviourLedger | None = None,
|
||||
changed_entities: set[str] | None = None,
|
||||
) -> list[dict]:
|
||||
"""Machine-derived follow-up obligations from STRUCTURAL truth
|
||||
(rule 19K) — never an improvised generic checklist."""
|
||||
changed = changed_entities or {target}
|
||||
obligations: list[dict] = []
|
||||
seen: set[tuple] = set()
|
||||
|
||||
def add(kind: str, target_e: str, why: str) -> None:
|
||||
key = (kind, target_e)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
obligations.append({"kind": kind, "target": target_e,
|
||||
"why": why})
|
||||
|
||||
for eid in sorted(changed):
|
||||
# consumers of a changed component must be inspected
|
||||
for consumer in model.consumers(eid):
|
||||
add("inspect_consumer", consumer,
|
||||
f"consumes/depends on changed {eid}")
|
||||
# provided interfaces: every consumer of the interface is affected
|
||||
for iface in model.interfaces_for(eid):
|
||||
add("verify_interface", iface,
|
||||
f"provided by changed {eid}")
|
||||
for consumer in model.consumers(iface):
|
||||
add("inspect_consumer", consumer,
|
||||
f"consumes {iface} provided by changed {eid}")
|
||||
# state compatibility
|
||||
for store in model.state_owned(eid):
|
||||
add("check_state_compatibility", store,
|
||||
f"state owned by changed {eid}")
|
||||
# interfaces the target consumes
|
||||
for iface in model.consumers(eid) if False else []:
|
||||
pass
|
||||
consumed = sorted(r["to"] for r in model.doc["relationships"]
|
||||
if r["type"] == "consumes" and r["from"] == eid)
|
||||
for iface in consumed:
|
||||
add("check_interface_contract", iface,
|
||||
f"changed {eid} consumes {iface} — contract must still hold")
|
||||
# deployment
|
||||
for dep in model.deployment_surfaces(eid):
|
||||
add("verify_deployment", dep,
|
||||
f"deployed surface of changed {eid}")
|
||||
# behaviour retests
|
||||
if behaviours is not None:
|
||||
for rec in behaviours.doc["behaviours"].values():
|
||||
deps = set(rec.get("dependencies") or []) | {
|
||||
rec.get("owner_component")}
|
||||
if deps & changed:
|
||||
add("retest", rec["behaviour_id"],
|
||||
f"depends on changed {sorted(deps & changed)}")
|
||||
return obligations
|
||||
@@ -241,6 +241,13 @@ class TaskStateRepository:
|
||||
with self._lock:
|
||||
return self._tasks.get(task_id)
|
||||
|
||||
def list_all(self) -> list[TaskStateRecord]:
|
||||
"""Every tracked task record (T17 17F: user-facing counters and
|
||||
listings derive from THIS authority, not the legacy operator
|
||||
cache)."""
|
||||
with self._lock:
|
||||
return list(self._tasks.values())
|
||||
|
||||
def list_open(self, channel_id: str) -> list[TaskStateRecord]:
|
||||
with self._lock:
|
||||
return sorted(self._open_in_channel(channel_id), key=lambda t: t.updated_at)
|
||||
|
||||
@@ -1,366 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build Mode Plugin — State machine orchestrator.
|
||||
|
||||
Each phase is an independent module. The state machine coordinates:
|
||||
1. Read current state from checkpoint file
|
||||
2. Execute the current phase's module
|
||||
3. Write output to checkpoint file
|
||||
4. Advance to next phase
|
||||
5. Repeat
|
||||
|
||||
Key insight from Gauntlet: "Checkpoint files on disk. Reports generated
|
||||
from checkpoints, not memory." If the conversation dies, the next session
|
||||
resumes from the last checkpoint. No drift because state is on disk.
|
||||
|
||||
Architecture:
|
||||
- Each phase is a separate module in phases/
|
||||
- State machine reads checkpoint.json to know current phase
|
||||
- Each phase writes its output to checkpoint.json
|
||||
- Cron tasks fire each phase independently
|
||||
- Ledger tracks progress for resumability
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CONFIGURATION
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
PLUGIN_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PHASES_DIR = os.path.join(PLUGIN_DIR, "phases")
|
||||
BUILDS_DIR = os.path.expanduser("~/.rune/work/builds")
|
||||
WORKTREE_DIR = os.path.expanduser("~/.rune/worktrees")
|
||||
REPO_DIR = "/mnt/workdrive/rune"
|
||||
|
||||
# Phase execution order
|
||||
PHASE_ORDER = [
|
||||
"assess",
|
||||
"research",
|
||||
"feasible",
|
||||
"consult",
|
||||
"consent",
|
||||
"plan",
|
||||
"build",
|
||||
"verify",
|
||||
"correct",
|
||||
"integrate",
|
||||
"record",
|
||||
]
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# STATE MACHINE
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class BuildStateMachine:
|
||||
"""State machine for build mode. Reads/writes checkpoint files."""
|
||||
|
||||
def __init__(self, build_id: str):
|
||||
self.build_id = build_id
|
||||
self.build_dir = os.path.join(BUILDS_DIR, build_id)
|
||||
self.checkpoint_path = os.path.join(self.build_dir, "checkpoint.json")
|
||||
self.ledger_path = os.path.join(self.build_dir, "ledger.json")
|
||||
self.trajectory_path = os.path.join(self.build_dir, "trajectory.json")
|
||||
|
||||
os.makedirs(self.build_dir, exist_ok=True)
|
||||
|
||||
def load_checkpoint(self) -> Dict[str, Any]:
|
||||
"""Load current state from checkpoint file."""
|
||||
if os.path.exists(self.checkpoint_path):
|
||||
with open(self.checkpoint_path) as f:
|
||||
return json.load(f)
|
||||
return self._initial_checkpoint()
|
||||
|
||||
def save_checkpoint(self, checkpoint: Dict[str, Any]):
|
||||
"""Save state to checkpoint file."""
|
||||
checkpoint["updated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
with open(self.checkpoint_path, "w") as f:
|
||||
json.dump(checkpoint, f, indent=2)
|
||||
|
||||
def _initial_checkpoint(self) -> Dict[str, Any]:
|
||||
"""Create initial checkpoint."""
|
||||
return {
|
||||
"build_id": self.build_id,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"updated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"current_phase": "assess",
|
||||
"phase_status": "pending",
|
||||
"phases": {},
|
||||
"description": "",
|
||||
"worktree_path": None,
|
||||
"complexity_score": 0,
|
||||
"estimated_steps": 0,
|
||||
"batches": [],
|
||||
"corrections": 0,
|
||||
"max_corrections": 3,
|
||||
"files_changed": [],
|
||||
"contracts": [],
|
||||
"failure_patterns": [],
|
||||
}
|
||||
|
||||
def get_current_phase(self) -> str:
|
||||
"""Get current phase from checkpoint."""
|
||||
checkpoint = self.load_checkpoint()
|
||||
return checkpoint.get("current_phase", "assess")
|
||||
|
||||
def get_phase_status(self) -> str:
|
||||
"""Get current phase status from checkpoint."""
|
||||
checkpoint = self.load_checkpoint()
|
||||
return checkpoint.get("phase_status", "pending")
|
||||
|
||||
def advance_phase(self):
|
||||
"""Advance to next phase."""
|
||||
checkpoint = self.load_checkpoint()
|
||||
current = checkpoint["current_phase"]
|
||||
|
||||
# Mark current phase as complete
|
||||
checkpoint["phases"][current] = "complete"
|
||||
|
||||
# Find next phase
|
||||
try:
|
||||
idx = PHASE_ORDER.index(current)
|
||||
if idx + 1 < len(PHASE_ORDER):
|
||||
next_phase = PHASE_ORDER[idx + 1]
|
||||
checkpoint["current_phase"] = next_phase
|
||||
checkpoint["phase_status"] = "pending"
|
||||
else:
|
||||
checkpoint["phase_status"] = "complete"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.save_checkpoint(checkpoint)
|
||||
|
||||
def set_phase_status(self, status: str, details: Dict[str, Any] = None):
|
||||
"""Set current phase status."""
|
||||
checkpoint = self.load_checkpoint()
|
||||
checkpoint["phase_status"] = status
|
||||
if details:
|
||||
checkpoint.update(details)
|
||||
self.save_checkpoint(checkpoint)
|
||||
|
||||
def log_to_ledger(self, entry: Dict[str, Any]):
|
||||
"""Append entry to ledger."""
|
||||
ledger = []
|
||||
if os.path.exists(self.ledger_path):
|
||||
with open(self.ledger_path) as f:
|
||||
ledger = json.load(f)
|
||||
|
||||
entry["timestamp"] = datetime.now(timezone.utc).isoformat()
|
||||
ledger.append(entry)
|
||||
|
||||
with open(self.ledger_path, "w") as f:
|
||||
json.dump(ledger, f, indent=2)
|
||||
|
||||
def log_trajectory(self, action: str, details: Dict[str, Any] = None):
|
||||
"""Append to trajectory log (like SWE-agent .traj files)."""
|
||||
trajectory = []
|
||||
if os.path.exists(self.trajectory_path):
|
||||
with open(self.trajectory_path) as f:
|
||||
trajectory = json.load(f)
|
||||
|
||||
entry = {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"action": action,
|
||||
"details": details or {},
|
||||
}
|
||||
trajectory.append(entry)
|
||||
|
||||
with open(self.trajectory_path, "w") as f:
|
||||
json.dump(trajectory, f, indent=2)
|
||||
|
||||
def get_resume_point(self) -> Optional[str]:
|
||||
"""Get phase to resume from."""
|
||||
checkpoint = self.load_checkpoint()
|
||||
current = checkpoint.get("current_phase")
|
||||
status = checkpoint.get("phase_status")
|
||||
|
||||
if status == "complete":
|
||||
# All phases complete
|
||||
return None
|
||||
|
||||
return current
|
||||
|
||||
def can_resume(self) -> bool:
|
||||
"""Check if build can be resumed."""
|
||||
return self.get_resume_point() is not None
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get build status."""
|
||||
checkpoint = self.load_checkpoint()
|
||||
return {
|
||||
"build_id": self.build_id,
|
||||
"description": checkpoint.get("description", ""),
|
||||
"current_phase": checkpoint.get("current_phase", "assess"),
|
||||
"phase_status": checkpoint.get("phase_status", "pending"),
|
||||
"phases": checkpoint.get("phases", {}),
|
||||
"batches": len(checkpoint.get("batches", [])),
|
||||
"corrections": checkpoint.get("corrections", 0),
|
||||
"worktree": checkpoint.get("worktree_path"),
|
||||
"complexity_score": checkpoint.get("complexity_score", 0),
|
||||
"estimated_steps": checkpoint.get("estimated_steps", 0),
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE EXECUTION
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def execute_phase(build_id: str, phase: str = None) -> Dict[str, Any]:
|
||||
"""Execute a phase. If phase is None, execute current phase."""
|
||||
sm = BuildStateMachine(build_id)
|
||||
|
||||
if phase is None:
|
||||
phase = sm.get_current_phase()
|
||||
|
||||
# Check if phase module exists
|
||||
phase_module_path = os.path.join(PHASES_DIR, f"{phase}.py")
|
||||
if not os.path.exists(phase_module_path):
|
||||
return {"success": False, "error": f"Phase module not found: {phase}"}
|
||||
|
||||
# Import and execute phase module
|
||||
try:
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.spec_from_file_location(f"phase_{phase}", phase_module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Execute phase
|
||||
result = module.execute(sm)
|
||||
|
||||
# Log to trajectory
|
||||
sm.log_trajectory(f"phase_{phase}", {"result": result})
|
||||
|
||||
# Advance to next phase if successful
|
||||
if result.get("success"):
|
||||
sm.advance_phase()
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
sm.log_trajectory(f"phase_{phase}_error", {"error": str(e)})
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: build_mode_plugin.py [start|status|resume|execute|abort]")
|
||||
sys.exit(1)
|
||||
|
||||
cmd = sys.argv[1]
|
||||
|
||||
if cmd == "start":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: build_mode_plugin.py start <description>")
|
||||
sys.exit(1)
|
||||
description = " ".join(sys.argv[2:])
|
||||
build_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
||||
sm = BuildStateMachine(build_id)
|
||||
checkpoint = sm.load_checkpoint()
|
||||
checkpoint["description"] = description
|
||||
sm.save_checkpoint(checkpoint)
|
||||
sm.log_to_ledger({"action": "build_started", "description": description})
|
||||
print(f"✅ Build {build_id} started")
|
||||
print(f" Description: {description}")
|
||||
print(f" Checkpoint: {sm.checkpoint_path}")
|
||||
|
||||
elif cmd == "status":
|
||||
if len(sys.argv) < 3:
|
||||
# List all builds
|
||||
if os.path.exists(BUILDS_DIR):
|
||||
builds = os.listdir(BUILDS_DIR)
|
||||
if builds:
|
||||
print(f"Builds: {len(builds)}")
|
||||
for build_id in sorted(builds):
|
||||
try:
|
||||
sm = BuildStateMachine(build_id)
|
||||
status = sm.get_status()
|
||||
print(
|
||||
f" {build_id}: {status['description']} ({status['current_phase']}: {status['phase_status']})"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
print("No builds found.")
|
||||
else:
|
||||
print("No builds found.")
|
||||
else:
|
||||
build_id = sys.argv[2]
|
||||
try:
|
||||
sm = BuildStateMachine(build_id)
|
||||
status = sm.get_status()
|
||||
print(f"Build: {status['build_id']}")
|
||||
print(f" Description: {status['description']}")
|
||||
print(f" Phase: {status['current_phase']} ({status['phase_status']})")
|
||||
print(f" Batches: {status['batches']}")
|
||||
print(f" Corrections: {status['corrections']}")
|
||||
print(f" Worktree: {status['worktree']}")
|
||||
print(f" Complexity: {status['complexity_score']}")
|
||||
print(f" Estimated steps: {status['estimated_steps']}")
|
||||
except FileNotFoundError:
|
||||
print(f"Build {build_id} not found")
|
||||
|
||||
elif cmd == "resume":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: build_mode_plugin.py resume <build_id>")
|
||||
sys.exit(1)
|
||||
build_id = sys.argv[2]
|
||||
try:
|
||||
sm = BuildStateMachine(build_id)
|
||||
if sm.can_resume():
|
||||
resume_point = sm.get_resume_point()
|
||||
print(f"Resuming build {build_id} from phase: {resume_point}")
|
||||
# Execute the current phase
|
||||
result = execute_phase(build_id)
|
||||
print(f"Result: {result}")
|
||||
else:
|
||||
print(f"Build {build_id} cannot be resumed (all phases complete)")
|
||||
except FileNotFoundError:
|
||||
print(f"Build {build_id} not found")
|
||||
|
||||
elif cmd == "execute":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: build_mode_plugin.py execute <build_id> [phase]")
|
||||
sys.exit(1)
|
||||
build_id = sys.argv[2]
|
||||
phase = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
result = execute_phase(build_id, phase)
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
elif cmd == "abort":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: build_mode_plugin.py abort <build_id>")
|
||||
sys.exit(1)
|
||||
build_id = sys.argv[2]
|
||||
try:
|
||||
sm = BuildStateMachine(build_id)
|
||||
checkpoint = sm.load_checkpoint()
|
||||
worktree = checkpoint.get("worktree_path")
|
||||
if worktree:
|
||||
subprocess.run(
|
||||
["git", "worktree", "remove", worktree, "--force"],
|
||||
cwd=REPO_DIR,
|
||||
capture_output=True,
|
||||
)
|
||||
sm.log_to_ledger({"action": "build_aborted"})
|
||||
print(f"✅ Build {build_id} aborted")
|
||||
except FileNotFoundError:
|
||||
print(f"Build {build_id} not found")
|
||||
|
||||
else:
|
||||
print(f"Unknown command: {cmd}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Assess phase — Complexity scoring and codebase impact estimation.
|
||||
|
||||
This is the first phase. It scores the user's request to determine:
|
||||
- If build mode is needed (score 8+)
|
||||
- How many steps are estimated
|
||||
- What files will be affected
|
||||
|
||||
Output: Updates checkpoint with complexity_score, estimated_steps, description.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
# Add parent to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the assess phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
description = checkpoint.get("description", "")
|
||||
|
||||
if not description:
|
||||
return {"success": False, "error": "No description provided"}
|
||||
|
||||
# Import complexity scorer
|
||||
try:
|
||||
from rune.complexity_scorer import score_request
|
||||
|
||||
breakdown = score_request(description)
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Complexity scorer failed: {e}"}
|
||||
|
||||
# Update checkpoint
|
||||
checkpoint["complexity_score"] = breakdown.total
|
||||
checkpoint["estimated_steps"] = _estimate_steps(description)
|
||||
checkpoint["assess_signals"] = breakdown.signals
|
||||
checkpoint["assess_mode"] = breakdown.mode
|
||||
checkpoint["assess_confidence"] = breakdown.confidence
|
||||
sm.save_checkpoint(checkpoint)
|
||||
|
||||
# Log to ledger
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "assess_complete",
|
||||
"complexity_score": breakdown.total,
|
||||
"estimated_steps": checkpoint["estimated_steps"],
|
||||
"mode": breakdown.mode,
|
||||
"confidence": breakdown.confidence,
|
||||
"signals": breakdown.signals,
|
||||
}
|
||||
)
|
||||
|
||||
# Check if build mode is needed
|
||||
if breakdown.total < 8:
|
||||
return {
|
||||
"success": True,
|
||||
"build_mode_needed": False,
|
||||
"complexity_score": breakdown.total,
|
||||
"estimated_steps": checkpoint["estimated_steps"],
|
||||
"mode": breakdown.mode,
|
||||
"reason": f"Score {breakdown.total} < 8, task mode recommended",
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"build_mode_needed": True,
|
||||
"complexity_score": breakdown.total,
|
||||
"estimated_steps": checkpoint["estimated_steps"],
|
||||
"mode": breakdown.mode,
|
||||
"confidence": breakdown.confidence,
|
||||
"signals": breakdown.signals,
|
||||
}
|
||||
|
||||
|
||||
def _estimate_steps(description: str) -> int:
|
||||
"""Estimate number of steps from description."""
|
||||
steps = 0
|
||||
desc_lower = description.lower()
|
||||
|
||||
# Each distinct verb ≈ 1-2 steps
|
||||
verbs = [
|
||||
"add",
|
||||
"create",
|
||||
"build",
|
||||
"implement",
|
||||
"update",
|
||||
"fix",
|
||||
"refactor",
|
||||
"migrate",
|
||||
"test",
|
||||
"deploy",
|
||||
"configure",
|
||||
"integrate",
|
||||
"design",
|
||||
]
|
||||
verb_count = sum(1 for v in verbs if v in desc_lower)
|
||||
steps += verb_count * 2
|
||||
|
||||
# Each "and" or "then" ≈ 1 step
|
||||
conjunctions = ["and", "then", "also", "plus"]
|
||||
conjunction_count = sum(1 for c in conjunctions if c in desc_lower)
|
||||
steps += conjunction_count
|
||||
|
||||
# Each file reference ≈ 1 step
|
||||
import re
|
||||
|
||||
file_refs = re.findall(r"\b\w+\.\w+\b", description)
|
||||
steps += len(file_refs)
|
||||
|
||||
# Minimum 3 steps for build mode
|
||||
return max(steps, 3)
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build phase — Execute batches in 3 phases.
|
||||
|
||||
Three-phase execution:
|
||||
1. Foundation: Types, interfaces, shared utilities
|
||||
2. Implementation: Feature code
|
||||
3. Integration: Tests, wiring, documentation
|
||||
|
||||
Output: Updates checkpoint with build results.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any, List
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the build phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
batches = checkpoint.get("batches", [])
|
||||
|
||||
if not batches:
|
||||
return {"success": False, "error": "No batches to build"}
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "build_started",
|
||||
"batches": len(batches),
|
||||
}
|
||||
)
|
||||
|
||||
# Build is done by the TUI's model
|
||||
# This module just tracks the state
|
||||
|
||||
sm.set_phase_status("in_progress", {"build_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"build_needed": True,
|
||||
"batches": batches,
|
||||
"instruction": "Execute batches in 3 phases: Foundation → Implementation → Integration",
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Consent phase — User approval.
|
||||
|
||||
Presents the plan to the user and waits for approval.
|
||||
Output: Updates checkpoint with approved.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the consent phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
description = checkpoint.get("description", "")
|
||||
complexity_score = checkpoint.get("complexity_score", 0)
|
||||
estimated_steps = checkpoint.get("estimated_steps", 0)
|
||||
patterns = checkpoint.get("consult_patterns", [])
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "consent_started",
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
|
||||
# Consent is handled by the TUI
|
||||
# This module just prepares the approval request
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"consent_needed": True,
|
||||
"description": description,
|
||||
"complexity_score": complexity_score,
|
||||
"estimated_steps": estimated_steps,
|
||||
"patterns": patterns,
|
||||
"instruction": "Present plan to user and wait for approval",
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Consult phase — Knowledge base check + failure pattern lookup.
|
||||
|
||||
Checks llm-craft and textual-craft knowledge bases for best practices.
|
||||
Queries failure pattern database for similar past failures.
|
||||
Output: Updates checkpoint with patterns, best_practices.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the consult phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
description = checkpoint.get("description", "")
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "consult_started",
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
|
||||
# Query knowledge bases
|
||||
patterns = []
|
||||
best_practices = []
|
||||
|
||||
try:
|
||||
# Check llm-craft knowledge base
|
||||
kb_path = os.path.join(os.path.dirname(__file__), "..", "..", "skills", "llm-craft", "knowledge.md")
|
||||
if os.path.exists(kb_path):
|
||||
with open(kb_path) as f:
|
||||
content = f.read()
|
||||
# Extract relevant patterns
|
||||
if "test" in description.lower():
|
||||
patterns.append("Use contract testing for interface verification")
|
||||
if "refactor" in description.lower():
|
||||
patterns.append("Refactor in small steps, verify after each change")
|
||||
if "new feature" in description.lower():
|
||||
patterns.append("Start with interface definition, then implementation")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Query failure patterns
|
||||
failure_patterns = checkpoint.get("failure_patterns", [])
|
||||
if failure_patterns:
|
||||
best_practices.append(f"Learn from {len(failure_patterns)} past failures")
|
||||
|
||||
# Update checkpoint
|
||||
checkpoint["consult_patterns"] = patterns
|
||||
checkpoint["consult_best_practices"] = best_practices
|
||||
sm.save_checkpoint(checkpoint)
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "consult_complete",
|
||||
"patterns": patterns,
|
||||
"best_practices": best_practices,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"patterns": patterns,
|
||||
"best_practices": best_practices,
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Correct phase — Fix failed batches.
|
||||
|
||||
3 attempts max per batch. After 2 failures, re-plan instead of blindly retrying.
|
||||
Output: Updates checkpoint with corrections.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the correct phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
corrections = checkpoint.get("corrections", 0)
|
||||
max_corrections = checkpoint.get("max_corrections", 3)
|
||||
|
||||
if corrections >= max_corrections:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Max corrections ({max_corrections}) reached",
|
||||
"instruction": "Escalate to user with diagnostic summary",
|
||||
}
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "correct_started",
|
||||
"corrections": corrections,
|
||||
}
|
||||
)
|
||||
|
||||
# Correct is done by the TUI
|
||||
# This module just tracks the state
|
||||
|
||||
checkpoint["corrections"] = corrections + 1
|
||||
sm.save_checkpoint(checkpoint)
|
||||
|
||||
sm.set_phase_status("in_progress", {"correct_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"correct_needed": True,
|
||||
"corrections": corrections + 1,
|
||||
"instruction": "Fix failed batches, re-verify. After 2 failures, re-plan.",
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Feasible phase — Feasibility gate + complexity report.
|
||||
|
||||
Checks if the task is feasible based on research findings.
|
||||
Output: Updates checkpoint with feasible, risk, complexity_report.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the feasible phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
description = checkpoint.get("description", "")
|
||||
research = checkpoint.get("research_findings", {})
|
||||
|
||||
# Feasibility is determined by the TUI's model
|
||||
# This module just tracks the state
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "feasible_started",
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
|
||||
sm.set_phase_status("in_progress", {"feasibility_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"feasibility_needed": True,
|
||||
"description": description,
|
||||
"research": research,
|
||||
"instruction": "Assess feasibility based on research findings",
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Integrate phase — Scope guardian (full test suite).
|
||||
|
||||
Runs full verification after all batches pass.
|
||||
Output: Updates checkpoint with integration results.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the integrate phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "integrate_started",
|
||||
}
|
||||
)
|
||||
|
||||
# Integrate is done by the TUI
|
||||
# This module just tracks the state
|
||||
|
||||
sm.set_phase_status("in_progress", {"integrate_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"integrate_needed": True,
|
||||
"instruction": "Run full test suite: canary + ruff + pyright + contracts + TUI validator",
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Plan phase — Work manager creates batches with dependency DAG.
|
||||
|
||||
Creates batches of 1-3 files each, routes to local or cloud models.
|
||||
Output: Updates checkpoint with batches.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any, List
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the plan phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
description = checkpoint.get("description", "")
|
||||
complexity_score = checkpoint.get("complexity_score", 0)
|
||||
estimated_steps = checkpoint.get("estimated_steps", 0)
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "plan_started",
|
||||
"description": description,
|
||||
"complexity_score": complexity_score,
|
||||
"estimated_steps": estimated_steps,
|
||||
}
|
||||
)
|
||||
|
||||
# Plan is done by the TUI's model
|
||||
# This module just tracks the state
|
||||
|
||||
sm.set_phase_status("in_progress", {"plan_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"plan_needed": True,
|
||||
"description": description,
|
||||
"complexity_score": complexity_score,
|
||||
"estimated_steps": estimated_steps,
|
||||
"instruction": "Create batches with dependency DAG, route to models",
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Record phase — Update PRD, GitHub, knowledge base, trajectory log.
|
||||
|
||||
Final phase. Updates all documentation and records what was learned.
|
||||
Output: Updates checkpoint with record results.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the record phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
build_id = sm.build_id
|
||||
description = checkpoint.get("description", "")
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "record_started",
|
||||
"build_id": build_id,
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
|
||||
# Record is done by the TUI
|
||||
# This module just tracks the state
|
||||
|
||||
sm.set_phase_status("in_progress", {"record_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"record_needed": True,
|
||||
"build_id": build_id,
|
||||
"description": description,
|
||||
"instruction": "Update PRD, commit to GitHub, update knowledge base with learnings",
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Research phase — Deep research on the task.
|
||||
|
||||
Always runs for build mode. Uses deep-research skill or parallel sub-agents.
|
||||
Output: Updates checkpoint with research_findings.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the research phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
description = checkpoint.get("description", "")
|
||||
|
||||
if not description:
|
||||
return {"success": False, "error": "No description provided"}
|
||||
|
||||
# Log to ledger
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "research_started",
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
|
||||
# Research is done by the TUI's model (not this module)
|
||||
# This module just tracks the state
|
||||
# The actual research happens in the TUI's _send_to_model flow
|
||||
|
||||
# For now, mark as needing external research
|
||||
sm.set_phase_status("in_progress", {"research_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"research_needed": True,
|
||||
"description": description,
|
||||
"instruction": "Use deep-research skill or parallel sub-agents to research this task",
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify phase — Tiered verification.
|
||||
|
||||
Tiered verification strategy:
|
||||
1. Syntactic: Per-edit lint, type-check, format (<1s)
|
||||
2. Semantic: Per-batch unit tests, import validation (<30s)
|
||||
3. Integration: Per-wave contract tests, build/compile (<5min)
|
||||
4. System: Full test suite, property-based (<15min)
|
||||
|
||||
Output: Updates checkpoint with verification results.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the verify phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
batches = checkpoint.get("batches", [])
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "verify_started",
|
||||
"batches": len(batches),
|
||||
}
|
||||
)
|
||||
|
||||
# Verify is done by the TUI
|
||||
# This module just tracks the state
|
||||
|
||||
sm.set_phase_status("in_progress", {"verify_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"verify_needed": True,
|
||||
"batches": batches,
|
||||
"instruction": "Run tiered verification: syntactic → semantic → integration → system",
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
"""programme_continuation.py — T15 §15M: the PROGRAMME CONTINUATION
|
||||
CANARY.
|
||||
|
||||
The user observation this answers: subtask completion repeatedly ended
|
||||
whole programmes. "task done" is not "programme done".
|
||||
|
||||
After ANY stage/subtask reports complete, the runner asks one bounded
|
||||
question — is the overall construction/programme objective satisfied?
|
||||
If not, and eligible authorized stages remain, the NEXT stage starts
|
||||
immediately (no user re-prompt, no voluntary stop). Blocked branches
|
||||
fall back to pre-authorized independent work.
|
||||
|
||||
Boundary (tranche brief): this is a bounded CONSTRUCTION-programme rule
|
||||
over Scaffolding stage contracts — NOT the future general-purpose
|
||||
Substrate scheduler. It knows nothing outside one programme's stage
|
||||
list.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
|
||||
# stage states
|
||||
PENDING = "PENDING"
|
||||
ELIGIBLE = "ELIGIBLE"
|
||||
WAITING_EVENT = "WAITING_EVENT"
|
||||
COMPLETE = "COMPLETE"
|
||||
BLOCKED = "BLOCKED"
|
||||
APPROVAL_REQUIRED = "APPROVAL_REQUIRED"
|
||||
FAILED = "FAILED"
|
||||
|
||||
TERMINAL = {COMPLETE, BLOCKED, APPROVAL_REQUIRED, FAILED}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProgrammeStage:
|
||||
stage_id: str
|
||||
objective: str
|
||||
state: str = PENDING
|
||||
depends_on: list = field(default_factory=list) # stage_ids
|
||||
priority: int = 0 # lower = sooner
|
||||
result: dict = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {"stage_id": self.stage_id, "objective": self.objective,
|
||||
"state": self.state, "depends_on": list(self.depends_on),
|
||||
"priority": self.priority}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Programme:
|
||||
"""One bounded construction programme: its objective, its authorized
|
||||
stages, and the continuation rule. Persisted to the contract store's
|
||||
project area (durable, reloadable, no transcript dependency)."""
|
||||
programme_id: str
|
||||
objective: str
|
||||
stages: list = field(default_factory=list) # [ProgrammeStage]
|
||||
fallbacks: list = field(default_factory=list) # pre-authorized ids
|
||||
|
||||
# ── the continuation canary (15M) ────────────────────────────────
|
||||
def continuation_canary(self) -> dict:
|
||||
"""'task done' ≠ 'programme done'. Returns the verdict AND the
|
||||
next action — the runner never has to guess whether to stop."""
|
||||
complete = [s for s in self.stages if s.state == COMPLETE]
|
||||
if len(complete) == len(self.stages):
|
||||
return {"verdict": "PROGRAMME_COMPLETE",
|
||||
"next_action": None,
|
||||
"reason": f"all {len(self.stages)} stages complete"}
|
||||
eligible = self.eligible_stages()
|
||||
if eligible:
|
||||
nxt = eligible[0]
|
||||
return {"verdict": "PROGRAMME_OPEN",
|
||||
"next_action": ("start", nxt.stage_id),
|
||||
"reason": (f"{len(complete)}/{len(self.stages)} "
|
||||
f"complete; {len(eligible)} eligible — "
|
||||
f"continue immediately")}
|
||||
waiting = [s for s in self.stages
|
||||
if s.state == WAITING_EVENT]
|
||||
if waiting:
|
||||
return {"verdict": "PROGRAMME_OPEN_WAITING",
|
||||
"next_action": ("await_event", waiting[0].stage_id),
|
||||
"reason": "event-driven wait active — no polling"}
|
||||
blocked = [s for s in self.stages if s.state == BLOCKED]
|
||||
if blocked and self.fallbacks:
|
||||
return {"verdict": "PROGRAMME_OPEN",
|
||||
"next_action": ("fallback", self.fallbacks[0]),
|
||||
"reason": (f"{len(blocked)} blocked; pre-authorized "
|
||||
f"fallback available")}
|
||||
if blocked:
|
||||
return {"verdict": "ALL_REMAINING_BLOCKED",
|
||||
"next_action": None,
|
||||
"reason": f"{len(blocked)} stage(s) blocked, no fallback"}
|
||||
approval = [s for s in self.stages
|
||||
if s.state == APPROVAL_REQUIRED]
|
||||
if approval:
|
||||
return {"verdict": "APPROVAL_REQUIRED",
|
||||
"next_action": None,
|
||||
"reason": (f"{len(approval)} stage(s) need user "
|
||||
f"authorization")}
|
||||
# T17 §17E: all stages terminal but some FAILED — the programme
|
||||
# may still close (programme-level acceptance decides), never
|
||||
# hang in UNKNOWN_INTERRUPTION.
|
||||
if all(s.state in TERMINAL for s in self.stages):
|
||||
failed = [s.stage_id for s in self.stages if s.state == FAILED]
|
||||
return {"verdict": "PROGRAMME_COMPLETE_WITH_FAILURES",
|
||||
"next_action": None,
|
||||
"reason": (f"all stages terminal; failed: {failed} — "
|
||||
f"programme-level acceptance decides")}
|
||||
return {"verdict": "UNKNOWN_INTERRUPTION", "next_action": None,
|
||||
"reason": "no stage runnable and none terminal"}
|
||||
|
||||
def eligible_stages(self) -> list:
|
||||
"""PENDING stages whose dependencies are COMPLETE, priority-first.
|
||||
ELIGIBLE counts as runnable too — an eligible stage nobody started
|
||||
is exactly the case the continuation canary exists to catch."""
|
||||
done = {s.stage_id for s in self.stages if s.state == COMPLETE}
|
||||
out = [s for s in self.stages
|
||||
if s.state in (PENDING, ELIGIBLE)
|
||||
and all(d in done for d in s.depends_on)]
|
||||
out.sort(key=lambda s: (s.priority, s.stage_id))
|
||||
return out
|
||||
|
||||
# ── durable persistence (canonical store, not transcript) ────────
|
||||
def save(self, base: Path) -> Path:
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
p = base / f"{self.programme_id}.json"
|
||||
p.write_text(json.dumps({
|
||||
"programme_id": self.programme_id,
|
||||
"objective": self.objective,
|
||||
"stages": [s.to_dict() for s in self.stages],
|
||||
"fallbacks": list(self.fallbacks),
|
||||
"saved_at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime()),
|
||||
}, indent=1))
|
||||
return p
|
||||
|
||||
@classmethod
|
||||
def load(cls, base: Path, programme_id: str) -> "Programme | None":
|
||||
p = base / f"{programme_id}.json"
|
||||
if not p.exists():
|
||||
return None
|
||||
d = json.loads(p.read_text())
|
||||
prog = cls(programme_id=d["programme_id"],
|
||||
objective=d["objective"], fallbacks=d.get("fallbacks", []))
|
||||
for s in d["stages"]:
|
||||
prog.stages.append(ProgrammeStage(**s))
|
||||
return prog
|
||||
|
||||
|
||||
def run_programme(prog: Programme, stage_runner, *,
|
||||
max_transitions: int = 100) -> list[dict]:
|
||||
"""Drive one programme to a legitimate stop. `stage_runner(stage)` —
|
||||
provided by the caller — must return a state for the stage. Between
|
||||
EVERY stage: persist → continuation canary → next. Returns the
|
||||
transition log (the 15M proof artifact)."""
|
||||
log: list[dict] = []
|
||||
for _ in range(max_transitions):
|
||||
prog_canary = prog.continuation_canary()
|
||||
log.append({"canary": prog_canary})
|
||||
verdict, action = prog_canary["verdict"], prog_canary["next_action"]
|
||||
if action is None:
|
||||
break # legitimate stop — reason recorded in the log
|
||||
kind, target = action
|
||||
if kind == "fallback":
|
||||
log.append({"fallback_started": target})
|
||||
break
|
||||
stage = next(s for s in prog.stages if s.stage_id == target)
|
||||
stage.state = stage_runner(stage) # runner decides the new state
|
||||
log.append({"stage": target, "new_state": stage.state})
|
||||
return log
|
||||
|
||||
|
||||
# ── T17 §17D: production wiring — Scaffolding-integrated driver ─────────
|
||||
#
|
||||
# The 15M canary proved the RULE; this adapter makes it CANONICAL. One
|
||||
# programme = a list of stages; each stage maps 1:1 to a Scaffolding
|
||||
# contract (stage_id == contract_id). Stages run through the Construction
|
||||
# Interface lifecycle (approved→building→verifying→closed); asynchronous
|
||||
# stage work runs under the event bridge (WAITING_EVENT is a state, not
|
||||
# a stop). Persisted to the scaffold store's programme area — reloadable
|
||||
# after restart with no transcript.
|
||||
|
||||
DEFAULT_PROGRAMME_AREA = "programmes"
|
||||
|
||||
|
||||
def _programme_base(scaffold_store: Path) -> Path:
|
||||
return Path(scaffold_store) / DEFAULT_PROGRAMME_AREA
|
||||
|
||||
|
||||
def scaffolding_stage_runner(interface, *, scaffold_store: Path,
|
||||
daemon_url: str = "",
|
||||
watchdog_secs: int = 900):
|
||||
"""Return a stage_runner for run_programme bound to the Construction
|
||||
Interface. The stage's work is read from stage.result["work"] — a
|
||||
callable (interface, stage) → dict with {"ok": bool, "evidence": str,
|
||||
"async_cmd": [argv] optional}. When async_cmd is present the work
|
||||
runs under the event bridge and the driver resumes on the EVENT;
|
||||
canonical verification still decides completion truth. Stages without
|
||||
work complete as governance-only transitions."""
|
||||
from rune import event_wait as ew # local import: optional dependency
|
||||
|
||||
def _phase(contract_id: str) -> str:
|
||||
# ALWAYS re-read: transition() saves through its own doc copy, so
|
||||
# a locally-held contract dict goes stale after any transition
|
||||
# (T17 17E live finding — the draft→approved→building chain
|
||||
# silently stopped after the first step on a stale reference).
|
||||
doc = interface.scaffold.load()
|
||||
c = interface.scaffold.contract(doc, contract_id)
|
||||
return (c or {}).get("phase", "")
|
||||
|
||||
def run(stage: ProgrammeStage) -> str:
|
||||
contract_id = stage.stage_id
|
||||
phase = _phase(contract_id)
|
||||
if phase == "":
|
||||
stage.result["error"] = f"no contract {contract_id}"
|
||||
return FAILED
|
||||
if phase == "closed":
|
||||
return COMPLETE # idempotent re-entry (duplicate signal case)
|
||||
if phase == "draft":
|
||||
interface.transition(contract_id, "approved",
|
||||
evidence_ref="programme-stage-start")
|
||||
if _phase(contract_id) == "approved":
|
||||
interface.transition(contract_id, "building",
|
||||
evidence_ref="programme-stage-start")
|
||||
work = cast("Any", stage.result.get("work"))
|
||||
if work is None:
|
||||
interface.transition(contract_id, "verifying",
|
||||
evidence_ref="governance-only-stage")
|
||||
interface.close(
|
||||
contract_id,
|
||||
observed_evidence=[{"source": "programme-driver",
|
||||
"detail": "governance-only stage: "
|
||||
"no work, closure by "
|
||||
"programme acceptance"}],
|
||||
scope_files=[])
|
||||
return COMPLETE
|
||||
# work is either a callable (interface, stage) → outcome dict, or
|
||||
# a literal outcome spec dict ({"ok", "evidence", "async_cmd"?})
|
||||
outcome: dict = (dict(cast("dict", work(interface, stage)))
|
||||
if callable(work) else dict(cast("dict", work)))
|
||||
async_cmd = outcome.get("async_cmd")
|
||||
if async_cmd:
|
||||
op = f"stage-{contract_id}-{int(time.time())}"
|
||||
ew.register_process_wait(
|
||||
op, contract_id=contract_id,
|
||||
project=getattr(interface.scaffold, "project_id", ""),
|
||||
objective=stage.objective,
|
||||
watched_for="stage work completion event",
|
||||
watchdog_secs=watchdog_secs)
|
||||
notifier = ew.run_builder_under_events(
|
||||
async_cmd, op, daemon_url=daemon_url or ew.DEFAULT_DAEMON_URL)
|
||||
stage.result["operation_id"] = op
|
||||
stage.result["notifier_pgid"] = notifier.pid
|
||||
result = ew.await_resume_event(op, watchdog_secs=watchdog_secs,
|
||||
pgid=notifier.pid)
|
||||
if result["kind"] != "event":
|
||||
stage.result["error"] = f"stage event wait: {result['kind']}"
|
||||
interface.transition(contract_id, "failed",
|
||||
evidence_ref="stage-event-lost")
|
||||
return FAILED
|
||||
rec: dict = result.get("wait") or {}
|
||||
outcome["evidence"] = (f"event resume; wait {rec.get('wait_id')} "
|
||||
f"verdict={rec.get('verdict')}")
|
||||
if not outcome.get("ok"):
|
||||
stage.result["error"] = outcome.get("evidence", "work failed")
|
||||
# truthful failure: record the evidence ON the contract and
|
||||
# drive it to the failed phase via the legal transition map
|
||||
# (building→failed is legal; no exception swallowing)
|
||||
doc = interface.scaffold.load()
|
||||
c = interface.scaffold.contract(doc, contract_id)
|
||||
if c is not None:
|
||||
c.setdefault("checkpoints", []).append({
|
||||
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime()),
|
||||
"phase": c.get("phase", ""),
|
||||
"evidence": "programme-stage-failed",
|
||||
"note": (outcome.get("evidence") or "work failed")[:200],
|
||||
})
|
||||
interface.scaffold.save(doc)
|
||||
phase = c.get("phase", "")
|
||||
if phase == "building":
|
||||
interface.transition(contract_id, "failed",
|
||||
evidence_ref="programme-stage-failed")
|
||||
return FAILED
|
||||
# completion truth: work evidence recorded ON the contract, then
|
||||
# closed (evidence lives on the durable object, not the log)
|
||||
if _phase(contract_id) == "building":
|
||||
interface.transition(contract_id, "verifying",
|
||||
evidence_ref="programme-stage-verify")
|
||||
doc = interface.scaffold.load()
|
||||
c = interface.scaffold.contract(doc, contract_id)
|
||||
if c is not None:
|
||||
c.setdefault("checkpoints", []).append({
|
||||
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"phase": "verifying",
|
||||
"evidence": "programme-stage-work",
|
||||
"note": (outcome.get("evidence") or "work completed")[:200],
|
||||
})
|
||||
interface.scaffold.save(doc)
|
||||
interface.close(
|
||||
contract_id,
|
||||
observed_evidence=[{"source": "programme-driver",
|
||||
"detail": outcome.get("evidence", "ok")}],
|
||||
scope_files=[])
|
||||
return COMPLETE
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def run_construction_programme(prog: Programme, interface, *,
|
||||
scaffold_store: Path,
|
||||
daemon_url: str = "",
|
||||
watchdog_secs: int = 900,
|
||||
max_transitions: int = 100) -> list[dict]:
|
||||
"""Canonical multi-stage construction continuation (T17 §17D):
|
||||
run_programme with the Scaffolding-bound runner; persists the
|
||||
programme around every transition so a restart resumes from the
|
||||
durable record, never a transcript. Returns the transition log."""
|
||||
runner = scaffolding_stage_runner(
|
||||
interface, scaffold_store=scaffold_store,
|
||||
daemon_url=daemon_url, watchdog_secs=watchdog_secs)
|
||||
log: list[dict] = []
|
||||
base = _programme_base(scaffold_store)
|
||||
for _ in range(max_transitions):
|
||||
prog.save(base)
|
||||
canary = prog.continuation_canary()
|
||||
log.append({"canary": canary})
|
||||
verdict, action = canary["verdict"], canary["next_action"]
|
||||
if action is None:
|
||||
break
|
||||
kind, target = action
|
||||
if kind == "fallback":
|
||||
log.append({"fallback_started": target})
|
||||
break
|
||||
stage = next(s for s in prog.stages if s.stage_id == target)
|
||||
if stage.state == COMPLETE:
|
||||
continue # duplicate continuation signal: idempotent
|
||||
stage.state = ELIGIBLE
|
||||
new_state = runner(stage)
|
||||
stage.state = new_state
|
||||
log.append({"stage": target, "new_state": new_state})
|
||||
prog.save(base)
|
||||
prog.save(base)
|
||||
return log
|
||||
@@ -0,0 +1,112 @@
|
||||
"""project_registry.py — T19 §19C: canonical registry of scaffolded
|
||||
projects.
|
||||
|
||||
BOUNDARIES (rule 19C, proven by tests):
|
||||
- Registry = DISCOVERY/INDEX (what projects exist, where their truth
|
||||
lives, health/migration state).
|
||||
- Project Scaffold/structural model = project structural truth
|
||||
(never duplicated here).
|
||||
- Spine = Rune's structural self-model. An external project's Scaffold
|
||||
does NOT become Spine structure because Rune knows it exists.
|
||||
|
||||
Registry state: one JSON document (versioned, provenance-aware per rule
|
||||
G) at <store_dir>/project_registry.json. Operational uses: /testing
|
||||
across projects (19L), drift/migration sweeps, re-entry discovery.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
REGISTRY_SCHEMA_VERSION = 1
|
||||
|
||||
# project health states (operational: drives /testing and migration
|
||||
# consent sweeps)
|
||||
HEALTHY = "HEALTHY"
|
||||
DRIFTED = "DRIFTED"
|
||||
MIGRATION_REQUIRED = "MIGRATION_REQUIRED"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
class ProjectRegistry:
|
||||
def __init__(self, store_dir: Path | str):
|
||||
self.path = Path(store_dir) / "project_registry.json"
|
||||
self.doc = self._load()
|
||||
|
||||
def _load(self) -> dict:
|
||||
if self.path.is_file():
|
||||
import json
|
||||
return json.loads(self.path.read_text())
|
||||
return {"schema_version": REGISTRY_SCHEMA_VERSION,
|
||||
"projects": {}, "updated_at": _now()}
|
||||
|
||||
def save(self) -> None:
|
||||
import json
|
||||
self.doc["updated_at"] = _now()
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.path.write_text(json.dumps(self.doc, indent=1, sort_keys=True))
|
||||
|
||||
def register(self, *, project_id: str, scaffold_location: str,
|
||||
repo_identity: str = "", forge_repo: str = "",
|
||||
default_branch: str = "master",
|
||||
structural_schema_version: int = 1,
|
||||
runtime_generation: str = "",
|
||||
last_known_good: str = "") -> dict:
|
||||
"""Register/index a scaffolded project. References only — the
|
||||
project's structural truth stays in ITS store (boundary rule)."""
|
||||
entry = {
|
||||
"project_id": project_id,
|
||||
"scaffold_location": scaffold_location,
|
||||
"repo_identity": repo_identity,
|
||||
"forge_repo": forge_repo,
|
||||
"default_branch": default_branch,
|
||||
"structural_schema_version": structural_schema_version,
|
||||
"scaffold_state": {"health": UNKNOWN,
|
||||
"last_material_update": _now()},
|
||||
"runtime_generation": runtime_generation,
|
||||
"last_known_good": last_known_good,
|
||||
"outstanding_validations": 0,
|
||||
"registered_at": _now(),
|
||||
}
|
||||
self.doc["projects"][project_id] = entry
|
||||
return entry
|
||||
|
||||
def get(self, project_id: str) -> dict | None:
|
||||
return self.doc["projects"].get(project_id)
|
||||
|
||||
def all_projects(self) -> list[dict]:
|
||||
return sorted(self.doc["projects"].values(),
|
||||
key=lambda p: p["project_id"])
|
||||
|
||||
def set_health(self, project_id: str, health: str, *,
|
||||
note: str = "") -> None:
|
||||
if health not in (HEALTHY, DRIFTED, MIGRATION_REQUIRED, UNKNOWN):
|
||||
raise ValueError(f"unknown health {health!r}")
|
||||
p = self.doc["projects"][project_id]
|
||||
p["scaffold_state"]["health"] = health
|
||||
p["scaffold_state"]["last_material_update"] = _now()
|
||||
if note:
|
||||
p["scaffold_state"].setdefault("notes", []).append(
|
||||
{"at": _now(), "note": note[:160]})
|
||||
|
||||
def set_outstanding_validations(self, project_id: str,
|
||||
count: int) -> None:
|
||||
self.doc["projects"][project_id]["outstanding_validations"] = count
|
||||
|
||||
def mark_generation(self, project_id: str, *, current: str,
|
||||
last_known_good: str = "") -> None:
|
||||
p = self.doc["projects"][project_id]
|
||||
p["runtime_generation"] = current
|
||||
if last_known_good:
|
||||
p["last_known_good"] = last_known_good
|
||||
|
||||
def needs_migration(self, proposed_schema_version: int) -> list[str]:
|
||||
"""Projects whose structural schema is older than a proposed
|
||||
version (feeds the 18I consent sweep — never silent upgrades)."""
|
||||
return sorted(pid for pid, p in self.doc["projects"].items()
|
||||
if p.get("structural_schema_version", 1)
|
||||
< proposed_schema_version)
|
||||
@@ -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
|
||||
@@ -238,6 +238,37 @@ def build_note(
|
||||
)
|
||||
|
||||
|
||||
# ── stored-body field extraction (§14B single authority) ─────────────────────
|
||||
# One set of parsers for release-body fields, shared by release_history_cli.py
|
||||
# and deploy_stage's authority-derived publisher — never duplicated.
|
||||
|
||||
|
||||
def summary_from_body(body: str) -> str:
|
||||
"""Extract the summary paragraph (between the H1 and the first other
|
||||
heading/italic line) from a stored release body."""
|
||||
lines = body.splitlines()
|
||||
grab = []
|
||||
for ln in lines[1:]:
|
||||
s = ln.strip()
|
||||
if not s:
|
||||
if grab:
|
||||
break
|
||||
continue
|
||||
if s.startswith(("#", "_", "##", "-")):
|
||||
break
|
||||
grab.append(s)
|
||||
return " ".join(grab) or f"Rune — evidence-derived release history"
|
||||
|
||||
|
||||
def sha_from_body(body: str, which: str) -> str:
|
||||
"""Extract a recorded SHA from a stored release body: 'tag' → the tag
|
||||
commit; anything else → the final deployed identity."""
|
||||
pat = ("tag commit: `([0-9a-f]+)`" if which == "tag"
|
||||
else r"\*\*final deployed: `([0-9a-f]+)`\*\*")
|
||||
m = re.search(pat, body, re.I)
|
||||
return m.group(1) if m else ""
|
||||
|
||||
|
||||
# ── Gitea publication (§15/§17/§25) ──────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
+133
-1
@@ -740,17 +740,131 @@ class RuneOperator:
|
||||
json.dump(self.state.to_dict(), f, indent=2)
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get Rune's current status."""
|
||||
"""Get Rune's current status. T17 17F: task counters derive from
|
||||
the CANONICAL TaskStateRepository — operator.json's task list is
|
||||
a legacy cache, never a second authority."""
|
||||
counters = self._task_counters()
|
||||
return {
|
||||
"identity": self.state.identity,
|
||||
"role": self.state.role,
|
||||
"current_task": self.state.current_task,
|
||||
"active_model": self.state.active_model,
|
||||
**counters,
|
||||
}
|
||||
|
||||
def _task_counters(self) -> Dict[str, int]:
|
||||
"""Counters from the canonical repo; repo vocabulary mapped to
|
||||
the legacy counter buckets (created→pending)."""
|
||||
buckets = {"pending": 0, "running": 0, "completed": 0}
|
||||
try:
|
||||
records = self._task_state.list_all()
|
||||
except Exception: # noqa: BLE001 — repo unavailable: in-memory fallback
|
||||
records = []
|
||||
if records:
|
||||
for r in records:
|
||||
if r.status in ("created", "pending"):
|
||||
buckets["pending"] += 1
|
||||
elif r.status == "running":
|
||||
buckets["running"] += 1
|
||||
elif r.status == "completed":
|
||||
buckets["completed"] += 1
|
||||
return {"tasks_pending": buckets["pending"],
|
||||
"tasks_running": buckets["running"],
|
||||
"tasks_completed": buckets["completed"]}
|
||||
# no repo records (fresh store): fall back to the in-memory set
|
||||
return {
|
||||
"tasks_pending": len([t for t in self.state.tasks if t.status == "pending"]),
|
||||
"tasks_running": len([t for t in self.state.tasks if t.status == "running"]),
|
||||
"tasks_completed": len([t for t in self.state.tasks if t.status == "completed"]),
|
||||
}
|
||||
|
||||
def _construction_store(self) -> str:
|
||||
"""SCAFFOLD-TRANCHE-0012: construction store location. Derives from
|
||||
the module's own repo root (rune_operator.py lives in <root>/src/rune/)
|
||||
with an env override for tests/fixtures — never a bare relative path
|
||||
dependent on the caller's CWD."""
|
||||
env = os.environ.get("RUNE_CONSTRUCTION_STORE")
|
||||
if env:
|
||||
return env
|
||||
return str(Path(__file__).resolve().parent.parent.parent
|
||||
/ "store" / "scaffold")
|
||||
|
||||
def _construction_note(self, text: str) -> None:
|
||||
"""SCAFFOLD-TRANCHE-0012: append a construction activation note to
|
||||
the governed scaffold observations. Degrades silently — observations
|
||||
are evidence, never task-critical."""
|
||||
try:
|
||||
from .scaffold import Scaffold
|
||||
s = Scaffold(self._construction_store(), "issue161-harness")
|
||||
doc = s.load()
|
||||
doc.setdefault("observations", []).append({
|
||||
"kind": "construction_note", "text": text[:300],
|
||||
"observed_at": __import__("datetime").datetime.utcnow()
|
||||
.strftime("%Y-%m-%dT%H:%M:%SZ")})
|
||||
s.save(doc)
|
||||
except Exception: # noqa: S110 — notes are best-effort by design
|
||||
pass
|
||||
|
||||
def _construction_activation(self, description: str, task_id: str) -> None:
|
||||
"""SCAFFOLD-TRANCHE-0011/0012 §11: contextual construction
|
||||
classification + activation on the NORMAL input path. Deterministic;
|
||||
the mode lands in the durable scaffold store where the shared
|
||||
Construction Context (and the future TUI) reads it. Delegates all
|
||||
lifecycle decisions to construction_mode/construction_activation —
|
||||
NO duplicate intelligence here."""
|
||||
from .construction_mode import classify_mode, _STANDALONE_WORDS
|
||||
from .construction_activation import activate
|
||||
# §15J case F: manual override feeds the SAME decision path — a
|
||||
# leading "/scaffold-mode <MODE>" (or "scaffold-mode: <MODE>")
|
||||
# marker is stripped and passed as explicit_override with
|
||||
# override_authority="user". NOT a second implementation:
|
||||
# classification still runs through classify_mode with provenance.
|
||||
override = None
|
||||
m = re.match(
|
||||
r"^(?:/scaffold-mode|scaffold-mode:)\s+"
|
||||
r"(NOT_CONSTRUCTION|LOCAL_CHANGE|EXISTING_PROJECT_CHANGE|"
|
||||
r"PROJECT_BUILD|UNKNOWN)\s+(.*)$",
|
||||
description, re.DOTALL)
|
||||
if m:
|
||||
override = m.group(1)
|
||||
description = m.group(2).strip()
|
||||
low = description.lower()
|
||||
tokens = set(__import__("re").split(r"[^a-z0-9]+", low))
|
||||
# §12D: the NATIVE hook must not force the Rune target — standalone
|
||||
# language means a NEW project (evidenced non-existence); explicit
|
||||
# Rune references mean the existing authority; otherwise UNKNOWN.
|
||||
if "rune" in tokens:
|
||||
target, exists = "rune", True
|
||||
elif _contains_any := [w for w in _STANDALONE_WORDS if w in low]:
|
||||
target, exists = None, False # evidenced: standalone = new
|
||||
elif "existing" in tokens:
|
||||
target, exists = "rune", True
|
||||
else:
|
||||
target, exists = None, None # unknown → probes, not invention
|
||||
mode_decision = classify_mode(
|
||||
description, target_project=target, project_exists=exists,
|
||||
affected_surface=[],
|
||||
explicit_override=override,
|
||||
override_authority="user" if override else None)
|
||||
from .scaffold import Scaffold
|
||||
s = Scaffold(self._construction_store(), "issue161-harness")
|
||||
doc = s.load()
|
||||
rec = activate(mode_decision, need=type("N", (), {
|
||||
"objective": description,
|
||||
"project": "rune",
|
||||
"need_id": f"native-{task_id}",
|
||||
"affected_surface": []})(), scaffold=s, builder="rune-native")
|
||||
doc.setdefault("observations", []).append({
|
||||
"kind": "construction_activated",
|
||||
"task": task_id,
|
||||
"mode": mode_decision.mode,
|
||||
"activated": rec.get("activated", False),
|
||||
"lifecycle": rec.get("lifecycle", ""),
|
||||
"classification_evidence": mode_decision.evidence[:3],
|
||||
"observed_at": __import__("datetime").datetime.utcnow()
|
||||
.strftime("%Y-%m-%dT%H:%M:%SZ")})
|
||||
s.save(doc)
|
||||
|
||||
def send_task(self, description: str, model: str | None = None, channel: str = OPERATOR_CHANNEL) -> Dict[str, Any]:
|
||||
"""Send a task to a model.
|
||||
|
||||
@@ -808,6 +922,24 @@ class RuneOperator:
|
||||
channel=channel,
|
||||
)
|
||||
task.route = task_route.route if task_route else ""
|
||||
|
||||
# CONTEXTUAL CONSTRUCTION ACTIVATION (SCAFFOLD-TRANCHE-0011/0012):
|
||||
# every normal input passes through here. Classification is
|
||||
# deterministic; activation degrades gracefully — a construction
|
||||
# failure NEVER blocks the ordinary task path (same lifecycle
|
||||
# guarantee as the route above).
|
||||
try:
|
||||
self._construction_activation(description, state_task.task_id)
|
||||
except Exception as exc: # noqa: BLE001 — construction must not break tasks
|
||||
try:
|
||||
self._task_state.transition(
|
||||
state_task.task_id, "running",
|
||||
outcome_ref="construction_activation_error")
|
||||
except Exception: # noqa: S110 — secondary degrade, task continues
|
||||
pass
|
||||
self._construction_note(
|
||||
f"construction activation error: {exc}")
|
||||
|
||||
if task_route is not None and task_route.decided_at:
|
||||
# Canonical reference: task state POINTS at the route event in
|
||||
# route.jsonl (sole route authority) — no value copied.
|
||||
|
||||
+65
-2
@@ -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.48
|
||||
Version: v0.66.78
|
||||
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.48 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.66.78 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
@@ -3141,6 +3141,69 @@ class RuneApp(App):
|
||||
|
||||
elif arg.startswith("start "):
|
||||
description = arg[6:]
|
||||
# T17 17G: /build start goes through the SAME canonical
|
||||
# Scaffolding classification as every other construction
|
||||
# route — no independent construction state machine.
|
||||
# NOT_CONSTRUCTION refuses; UNKNOWN surfaces the probe
|
||||
# questions; PROJECT_BUILD/EXISTING_PROJECT_CHANGE
|
||||
# activate the contract, then delegate to BuildManager
|
||||
# as a bounded adapter (worktree isolation + resume
|
||||
# ledger remain its unique capabilities).
|
||||
try:
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
iface = ConstructionInterface(builder="tui-build-cmd")
|
||||
envelope = iface.classify(description)
|
||||
state = envelope.get("state") or {}
|
||||
mode_name = state.get("mode", "UNKNOWN")
|
||||
probes = state.get("probe_questions", [])
|
||||
except Exception as exc: # noqa: BLE001 — classification
|
||||
mode_name = "UNKNOWN" # unavailable: refuse, fail closed
|
||||
probes = []
|
||||
self._ch().add_message("system",
|
||||
f"[red]Construction classification unavailable ({exc}) — refusing to start a build[/red]")
|
||||
return
|
||||
if mode_name in ("NOT_CONSTRUCTION", "UNKNOWN"):
|
||||
msg = (f"[yellow]'/build start' refused: classified "
|
||||
f"{mode_name}.[/yellow]")
|
||||
for q in probes[:3]:
|
||||
msg += f"\n - {q}"
|
||||
self._ch().add_message("system", msg)
|
||||
return
|
||||
# T18 18M FULL BACKEND CONVERGENCE: admission mints a
|
||||
# canonical Construction Contract (same as every other
|
||||
# route) — /build start no longer runs an independent
|
||||
# construction state machine. BuildManager remains the
|
||||
# bounded worktree/ledger adapter driven BY the contract.
|
||||
# T19 19A GOVERNANCE INVARIANT: real construction must
|
||||
# NOT silently bypass its canonical Construction Contract.
|
||||
# The T18 degradation paths (mint unavailable/failed →
|
||||
# BuildManager proceeds legacy-governed) were a silent
|
||||
# downgrade — REMOVED. Contract mint failure is FAIL-
|
||||
# CLOSED: the build does not start; the user sees why.
|
||||
# BuildManager remains a bounded worktree/ledger adapter
|
||||
# that is only ever reachable WITH a governing contract.
|
||||
import time as _t
|
||||
_cid = f"build-{int(_t.time())}"
|
||||
try:
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
_iface = ConstructionInterface(builder="tui-build-cmd")
|
||||
_doc = _iface.scaffold.load()
|
||||
_iface.scaffold.new_contract(
|
||||
_doc, contract_id=_cid, need=description,
|
||||
origin="user", scope=[],
|
||||
requirement="/build start (classified: "
|
||||
f"{mode_name})")
|
||||
_iface.scaffold.save(_doc)
|
||||
_iface.transition(_cid, "approved",
|
||||
evidence_ref="user:/build-start")
|
||||
_iface.transition(_cid, "building",
|
||||
evidence_ref="build-manager-adapter")
|
||||
except Exception as exc: # noqa: BLE001 — T19 19A: contract mint failure FAILS CLOSED (refuse, never degrade to ungoverned construction)
|
||||
self._ch().add_message("system",
|
||||
f"[red]'/build start' refused: canonical contract "
|
||||
f"mint failed ({exc}) — construction cannot start "
|
||||
f"without its governing contract.[/red]")
|
||||
return
|
||||
manager = BuildManager()
|
||||
build_id = manager.start_build(description)
|
||||
self._ch().add_message("system", f"[bold]✅ Build {build_id} started[/bold]")
|
||||
|
||||
+44
-2
@@ -134,7 +134,48 @@ class Scaffold:
|
||||
if not ok:
|
||||
raise AssertionError(
|
||||
f"persistence verification FAILED for {what!r}: reloaded "
|
||||
f"store does not reflect the saved doc (disk-write failure)")
|
||||
f"store does not reflect the saved doc (disk-write failure)"
|
||||
)
|
||||
return p
|
||||
|
||||
def commit(self, doc: dict, *, what: str = "durable state",
|
||||
selector=None, expect=None) -> Path:
|
||||
"""§21 TRANSACTIONAL BOUNDARY (Tranche 11): the ONLY way durable
|
||||
mutations are finalized. Persists, then verifies by readback. All
|
||||
accounting/report work happens AFTER commit returns — no quality
|
||||
inspection may sit between mutation and its durable commit (the
|
||||
forgotten-save anti-pattern: mutate → fragile read → exception →
|
||||
save never happens, 3 recurrences T5/T9B/T10)."""
|
||||
# §12B REAL DURABLE COMMIT SEMANTICS: "load didn't throw" is not
|
||||
# proof — load() returns a fresh empty document when the store file
|
||||
# is missing/corrupt, which silently masquerades as success. Instead:
|
||||
# canonical digest of the INTENDED document vs canonical digest of
|
||||
# what is ACTUALLY on disk after save. Semantic mismatch (partial
|
||||
# write, wrong path, unreadable store) fails visibly.
|
||||
from rune.structural_store import canonical_digest
|
||||
from rune.structural_store import load as _ss_load
|
||||
|
||||
intended = canonical_digest(doc)
|
||||
p = self.save(doc)
|
||||
reloaded = _ss_load(self.current)
|
||||
if reloaded is None:
|
||||
raise AssertionError(
|
||||
f"persistence verification FAILED for {what!r}: store did "
|
||||
f"not reload after save (missing or corrupt)")
|
||||
if canonical_digest(reloaded) != intended:
|
||||
raise AssertionError(
|
||||
f"persistence verification FAILED for {what!r}: reloaded "
|
||||
f"store digest differs from intended document — the "
|
||||
f"mutation is not durably what was intended")
|
||||
# optional caller-supplied semantic invariant (held-out: wrong/
|
||||
# stale doc where an expectation was supplied)
|
||||
if selector is not None:
|
||||
value = selector(reloaded)
|
||||
ok = expect(value) if callable(expect) else value == expect
|
||||
if not ok:
|
||||
raise AssertionError(
|
||||
f"persistence invariant FAILED for {what!r}: reloaded "
|
||||
f"state does not satisfy the supplied expectation")
|
||||
return p
|
||||
|
||||
def verify_on_disk(self, selector, expect, *, what: str = "state") -> bool:
|
||||
@@ -150,7 +191,8 @@ class Scaffold:
|
||||
raise AssertionError(
|
||||
f"durable-state verification FAILED for {what!r}: the on-"
|
||||
f"disk store does not reflect the claimed mutation — it was "
|
||||
f"probably never saved (Tranche 5 defect class A)")
|
||||
f"probably never saved (Tranche 5 defect class A)"
|
||||
)
|
||||
return True
|
||||
|
||||
# -- project structure (open-world kinds; no fixed vocabulary) ----------
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
"""stage_guards.py — T15 §15F–§15I: guarded stage execution for Scaffolding.
|
||||
|
||||
Design lessons absorbed (NOT the systems they came from): pre/post-
|
||||
conditions and invariants (Design by Contract), readiness gating of
|
||||
downstream work (DAGs), distinct startup/liveness/exit probe meanings
|
||||
(K8s probes), desired-vs-actual reconciliation (K8s controllers),
|
||||
idempotent retry + failure classification (Temporal), explicit
|
||||
inputs/outputs and null-build checks (Bazel), policy decision separated
|
||||
from the governed actor (policy-as-code).
|
||||
|
||||
§15F THREE CANARY MEANINGS — deliberately NOT one generic flag:
|
||||
|
||||
READINESS — may this stage begin? failure BLOCKS the stage
|
||||
LIVENESS — is progress still possible? inactivity ≠ completion
|
||||
EXIT — did reality reach the expected state, invariants held?
|
||||
a stage cannot become PASS without it
|
||||
|
||||
§15F INDEPENDENT COMPLETION AUTHORITY (14X.8 continued): the actor that
|
||||
performed the stage action may NOT self-certify its exit canary. The
|
||||
canary executes the check itself (command, readback, assertion) and
|
||||
records evidence; actor claims are evidence INPUT only.
|
||||
|
||||
§15G TEST TRUTH: exit code 0 is not proof. Where the contract requires
|
||||
tests, the exit canary requires: non-zero executed test count, no
|
||||
skip-all, expected artifact present. Expectations come from the
|
||||
contract, never hard-coded.
|
||||
|
||||
§15H HENXELS PLACEMENT: structural/system-health guard at proportionate
|
||||
boundaries — REQUIRED for structural integration boundaries and
|
||||
FULL/PROJECT_BUILD closure, NOT_APPLICABLE for tiny bounded changes.
|
||||
Every material boundary records which, with rationale.
|
||||
|
||||
§15I DESIRED VS ACTUAL: after consequential actions — persist, reload
|
||||
canonical reality, compare against desired. A command exiting 0 is not
|
||||
an effect. Zero/acceptable delta gates the NEXT stage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ── §15F guarded stage contract ─────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class StageCheck:
|
||||
"""One named, executable readiness/exit check. The CHECK executes —
|
||||
it is never satisfied by a claim."""
|
||||
name: str
|
||||
kind: str # "command" | "path_exists" | "assert"
|
||||
# command: argv list; truth from exit code (+ optional output_pattern)
|
||||
argv: list | None = None
|
||||
output_pattern: str = "" # regex that must appear on stdout (15G)
|
||||
# path_exists: path that must exist after the stage
|
||||
path: str = ""
|
||||
# assert: callable(state) -> bool (in-process deterministic check)
|
||||
check: Any = None
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class StageContract:
|
||||
"""Minimum guarded-stage semantics expressed over existing
|
||||
construction state. Not a workflow engine: a stage contract is data
|
||||
attached to (or derived from) a Construction Contract stage."""
|
||||
stage_id: str
|
||||
contract_id: str = ""
|
||||
dependencies: list = field(default_factory=list) # stage_ids
|
||||
ready_when: list = field(default_factory=list) # [StageCheck]
|
||||
done_when: list = field(default_factory=list) # [StageCheck]
|
||||
invariants: list = field(default_factory=list) # [StageCheck]
|
||||
expected_effect: str = ""
|
||||
retry_class: str = "UNKNOWN" # TRANSIENT/INTERMITTENT/KNOWN_PROCESS_
|
||||
# DEFECT/PERMANENT/APPROVAL_REQUIRED/UNKNOWN
|
||||
idempotency_key: str = "" # operation identity for null-action
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
def chk(c: StageCheck) -> dict:
|
||||
return {"name": c.name, "kind": c.kind, "argv": c.argv,
|
||||
"output_pattern": c.output_pattern, "path": c.path,
|
||||
"description": c.description}
|
||||
return {"stage_id": self.stage_id, "contract_id": self.contract_id,
|
||||
"dependencies": list(self.dependencies),
|
||||
"ready_when": [chk(c) for c in self.ready_when],
|
||||
"done_when": [chk(c) for c in self.done_when],
|
||||
"invariants": [chk(c) for c in self.invariants],
|
||||
"expected_effect": self.expected_effect,
|
||||
"retry_class": self.retry_class,
|
||||
"idempotency_key": self.idempotency_key}
|
||||
|
||||
|
||||
class StageBlocked(Exception):
|
||||
def __init__(self, stage_id: str, failures: list[dict]):
|
||||
self.stage_id = stage_id
|
||||
self.failures = failures
|
||||
super().__init__(f"stage {stage_id} blocked: "
|
||||
f"{[f['name'] for f in failures]}")
|
||||
|
||||
|
||||
class ExitCanaryFailed(Exception):
|
||||
def __init__(self, stage_id: str, failures: list[dict]):
|
||||
self.stage_id = stage_id
|
||||
self.failures = failures
|
||||
super().__init__(f"stage {stage_id} exit canary FAILED: "
|
||||
f"{[f['name'] for f in failures]}")
|
||||
|
||||
|
||||
def _run_check(check: StageCheck) -> dict:
|
||||
"""Execute ONE check and record evidence. Returns
|
||||
{name, passed, evidence}. A check that cannot execute is a FAILURE
|
||||
(cannot-execute is never a pass)."""
|
||||
ev: dict = {"name": check.name, "kind": check.kind}
|
||||
try:
|
||||
if check.kind == "command":
|
||||
r = subprocess.run(check.argv or [], capture_output=True,
|
||||
text=True, timeout=300)
|
||||
ev["exit_code"] = r.returncode
|
||||
passed = r.returncode == 0
|
||||
if passed and check.output_pattern:
|
||||
m = re.search(check.output_pattern, r.stdout or "")
|
||||
ev["pattern"] = check.output_pattern
|
||||
ev["matched"] = bool(m)
|
||||
passed = bool(m)
|
||||
if m:
|
||||
ev["matched_text"] = m.group(0)[:100]
|
||||
elif check.kind == "path_exists":
|
||||
p = Path(check.path)
|
||||
ev["path"] = check.path
|
||||
passed = p.exists() and p.stat().st_size >= 0
|
||||
ev["size"] = p.stat().st_size if p.exists() else 0
|
||||
elif check.kind == "assert":
|
||||
ev["assert"] = check.description or check.name
|
||||
passed = bool(check.check({}))
|
||||
else:
|
||||
ev["error"] = f"unknown check kind {check.kind}"
|
||||
passed = False
|
||||
except Exception as e: # noqa: BLE001 — cannot-execute is a failure
|
||||
ev["error"] = str(e)[:200]
|
||||
passed = False
|
||||
ev["passed"] = passed
|
||||
return ev
|
||||
|
||||
|
||||
def readiness_canary(stage: StageContract) -> dict:
|
||||
"""§15F: may this stage begin? ALL ready_when checks must pass.
|
||||
Failure BLOCKS the stage — it does NOT auto-repair."""
|
||||
failures = [ev for ev in (_run_check(c) for c in stage.ready_when)
|
||||
if not ev["passed"]]
|
||||
verdict = "READY" if not failures else "BLOCKED"
|
||||
return {"stage_id": stage.stage_id, "canary": "readiness",
|
||||
"verdict": verdict,
|
||||
"checks": len(stage.ready_when), "failures": failures}
|
||||
|
||||
|
||||
def execute_stage(stage: StageContract, action) -> dict:
|
||||
"""Run one guarded stage: readiness → action → exit canary.
|
||||
The action is the caller's function; its return value is evidence
|
||||
INPUT. The exit canary independently verifies done_when + invariants.
|
||||
Readiness failure raises StageBlocked (stage does not execute);
|
||||
exit failure raises ExitCanaryFailed (stage cannot become PASS)."""
|
||||
ready = readiness_canary(stage)
|
||||
if ready["verdict"] == "BLOCKED":
|
||||
raise StageBlocked(stage.stage_id, ready["failures"])
|
||||
action_evidence = action() if action else {}
|
||||
exit_evidence = [_run_check(c) for c in stage.done_when]
|
||||
invariant_evidence = [_run_check(c) for c in stage.invariants]
|
||||
failures = [e for e in exit_evidence + invariant_evidence
|
||||
if not e["passed"]]
|
||||
result = {"stage_id": stage.stage_id, "canary": "exit",
|
||||
"verdict": "PASS" if not failures else "FAILED",
|
||||
"action_evidence": action_evidence,
|
||||
"exit_checks": exit_evidence,
|
||||
"invariant_checks": invariant_evidence,
|
||||
"failures": failures}
|
||||
if failures:
|
||||
# §15F: the stage cannot become PASS — and "cannot" means the
|
||||
# caller gets an exception, not a verdict it may ignore.
|
||||
raise ExitCanaryFailed(stage.stage_id, failures)
|
||||
return result
|
||||
|
||||
|
||||
def liveness_canary(last_progress_at: float, now: float | None = None,
|
||||
stall_limit_secs: float = 300.0) -> dict:
|
||||
"""§15F: is progress still possible? Inactivity NEVER means
|
||||
completion — a stalled stage is STALLED, not done."""
|
||||
now = now if now is not None else time.monotonic()
|
||||
stalled = (now - last_progress_at) > stall_limit_secs
|
||||
return {"canary": "liveness", "stalled": stalled,
|
||||
"idle_secs": round(now - last_progress_at, 1),
|
||||
"verdict": "STALLED" if stalled else "ALIVE"}
|
||||
|
||||
|
||||
# ── §15G verify test truth, not just exit code ──────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class TestExpectations:
|
||||
"""Contract-derived verification expectations (never universal
|
||||
constants)."""
|
||||
require_tests: bool = True
|
||||
min_tests: int = 1
|
||||
test_output_pattern: str = r"(?:# pass (\d+)|(\d+) passed)"
|
||||
forbid_all_skipped: str = r"# skipped (\d+)"
|
||||
required_artifacts: list = field(default_factory=list) # paths
|
||||
|
||||
|
||||
def verify_test_truth(cmd: list, cwd: str, expectations: TestExpectations,
|
||||
*, base_dir: str = ".") -> dict:
|
||||
"""§15G: run the verification command and judge TRUTH, not exit code.
|
||||
False-green hostile cases rejected: zero tests discovered with exit 0,
|
||||
all tests skipped, expected artifact absent."""
|
||||
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True,
|
||||
timeout=300)
|
||||
evidence: dict = {"cmd": cmd, "exit_code": r.returncode,
|
||||
"stdout_tail": (r.stdout or "")[-500:]}
|
||||
failures: list[dict] = []
|
||||
if r.returncode != 0:
|
||||
failures.append({"name": "exit_code", "exit_code": r.returncode})
|
||||
if expectations.require_tests:
|
||||
m = re.search(expectations.test_output_pattern, r.stdout or "")
|
||||
count = int(m.group(1) or m.group(2)) if m else 0
|
||||
evidence["test_count"] = count
|
||||
if count == 0:
|
||||
# zero tests + exit 0 is a false green when tests are required
|
||||
failures.append({"name": "test_count", "count": 0,
|
||||
"why": "zero tests executed — false green"})
|
||||
elif count < expectations.min_tests:
|
||||
failures.append({"name": "test_count", "count": count,
|
||||
"min_required": expectations.min_tests})
|
||||
if expectations.forbid_all_skipped:
|
||||
s = re.search(expectations.forbid_all_skipped, r.stdout or "")
|
||||
if s and count and int(s.group(1)) >= count:
|
||||
failures.append({"name": "all_skipped",
|
||||
"skipped": int(s.group(1))})
|
||||
for art in expectations.required_artifacts:
|
||||
p = Path(base_dir) / art
|
||||
if not p.exists() or p.stat().st_size == 0:
|
||||
failures.append({"name": "artifact_missing", "path": art})
|
||||
evidence["failures"] = failures
|
||||
evidence["verdict"] = "PASS" if not failures else "FALSE_GREEN_REFUSED"
|
||||
return evidence
|
||||
|
||||
|
||||
# ── §15H Henxels as proportional structural guard ───────────────────────────
|
||||
|
||||
def henxels_placement(*, depth: str, structural_boundary: bool,
|
||||
stage_id: str) -> dict:
|
||||
"""§15H: is Henxels REQUIRED at this boundary? Proportional, not
|
||||
ceremonial. Records the decision with rationale (either way)."""
|
||||
if structural_boundary or depth in ("FULL", "PROJECT_BUILD"):
|
||||
return {"stage_id": stage_id, "henxels": "HENXELS_REQUIRED",
|
||||
"rationale": (f"depth={depth}, structural_boundary="
|
||||
f"{structural_boundary} — structural "
|
||||
f"integration evidence required")}
|
||||
return {"stage_id": stage_id, "henxels": "HENXELS_NOT_APPLICABLE",
|
||||
"rationale": (f"depth={depth}, no structural boundary — "
|
||||
f"focused exit canary suffices")}
|
||||
|
||||
|
||||
# ── §15I desired vs actual reconciliation ───────────────────────────────────
|
||||
|
||||
def reconcile(desired: dict, read_actual, *, stage_id: str = "",
|
||||
tolerance: set | None = None) -> dict:
|
||||
"""§15I: persist → reload canonical reality → compare. `read_actual`
|
||||
is a callable returning the ACTUAL canonical state (fresh read, never
|
||||
a cached copy). Returns the delta; empty delta = continue. The next
|
||||
stage MUST NOT start on a non-empty delta."""
|
||||
tolerance = tolerance or set()
|
||||
actual = read_actual()
|
||||
delta: dict = {}
|
||||
for key, want in desired.items():
|
||||
if key in tolerance:
|
||||
continue
|
||||
got = actual.get(key) if isinstance(actual, dict) else None
|
||||
if got != want:
|
||||
delta[key] = {"desired": want, "actual": got}
|
||||
return {"stage_id": stage_id, "reconciled": not delta, "delta": delta}
|
||||
@@ -0,0 +1,77 @@
|
||||
"""structural_migration.py — T18 §18I: schema versioning + consent for
|
||||
structural Scaffolds.
|
||||
|
||||
USER REQUIREMENT (T18 mandate): when Scaffolding itself later receives a
|
||||
SIGNIFICANT structural/version uplift, known projects previously
|
||||
scaffolded must NOT be silently upgraded. Material migrations require
|
||||
user consent; minor backwards-compatible migrations stay automatic.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class ConsentRequired(Exception):
|
||||
"""A material schema migration was requested without user approval."""
|
||||
|
||||
|
||||
def classify_migration(*, from_v: int, to_v: int,
|
||||
changes: list[str]) -> str:
|
||||
"""MINOR (automatic) vs MATERIAL (consent required). Deterministic:
|
||||
renames/removals/id-format changes/semantic shifts are MATERIAL;
|
||||
additive optional kinds/fields are MINOR."""
|
||||
material_markers = ("rename", "drop ", "remove", "delete",
|
||||
"id format", "semantic", "restructure",
|
||||
"change format", "merge", "split")
|
||||
for c in changes:
|
||||
cl = c.lower()
|
||||
if any(m in cl for m in material_markers):
|
||||
return "MATERIAL"
|
||||
return "MINOR"
|
||||
|
||||
|
||||
def affected_projects(stores: dict[str, dict], proposed_v: int) -> list[str]:
|
||||
"""Which known projects (schema_version < proposed_v) would a
|
||||
proposed uplift touch. Scaffolding presents these for consent —
|
||||
never silently upgrades."""
|
||||
return sorted(name for name, doc in stores.items()
|
||||
if doc.get("schema_version", 0) < proposed_v)
|
||||
|
||||
|
||||
def apply_migration(*, model_store, from_v: int, to_v: int,
|
||||
changes: list[str], user_approved: bool,
|
||||
approved_by: str = "") -> dict:
|
||||
"""Apply (or refuse) a schema migration. MATERIAL without explicit
|
||||
user approval raises ConsentRequired — the silent upgrade is
|
||||
structurally impossible. Returns a durable migration record."""
|
||||
kind = classify_migration(from_v=from_v, to_v=to_v, changes=changes)
|
||||
if kind == "MATERIAL" and not user_approved:
|
||||
raise ConsentRequired(
|
||||
f"material schema migration {from_v}→{to_v} requires user "
|
||||
f"consent: {changes[:3]}")
|
||||
return {
|
||||
"from_v": from_v, "to_v": to_v, "kind": kind,
|
||||
"changes": changes, "user_approved": user_approved,
|
||||
"approved_by": approved_by if user_approved else None,
|
||||
"applied_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
|
||||
|
||||
def consent_brief(*, from_v: int, to_v: int, changes: list[str],
|
||||
benefits: list[str]) -> dict:
|
||||
"""The consent presentation (rule 18I): current version, proposed
|
||||
version, benefits, semantic changes, risks, expected effect."""
|
||||
return {
|
||||
"current_version": from_v,
|
||||
"proposed_version": to_v,
|
||||
"benefits": benefits,
|
||||
"semantic_changes": changes,
|
||||
"migration_risks": [
|
||||
"stored models require one-time reconciliation after upgrade",
|
||||
"consumers of the old schema need re-reading via projection",
|
||||
],
|
||||
"expected_effect": ("models keep their data; identity and "
|
||||
"relationship semantics change per the listed "
|
||||
"changes"),
|
||||
"requires": "explicit user approval before any project is upgraded",
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
"""structural_model.py — T18 §18E/§18F: the first-class STRUCTURAL
|
||||
SCAFFOLD for scaffolded projects.
|
||||
|
||||
RESEARCH-LED (Build Basis: research/t18-structural-model-prior-art,
|
||||
QUALIFIED 2026-09-15):
|
||||
|
||||
ADOPTED: Backstage relation vocabulary (partOf/belongs_to,
|
||||
depends_on, provides/consumes, ownedBy) + entity-ref discipline;
|
||||
Kythe edge kinds (calls, reads, writes, satisfies, overrides) +
|
||||
derived-reverse-edge rule; Nx affected semantics for impact closure;
|
||||
Bazel rdeps(kind(test)) for verification scoping.
|
||||
|
||||
ADAPTED: identity is DETERMINISTIC and STABLE across versions
|
||||
(Kythe explicitly declines this — a persistent model cannot); C4
|
||||
deployment shapes become machine-derived deployed_by edges; Glean's
|
||||
batch layering → externally-derived relationships carry a distinct
|
||||
authority.
|
||||
|
||||
REJECTED: heavyweight graph stores (no evidence/maturity/consent
|
||||
semantics); LSIF; Stack Graphs (archived).
|
||||
|
||||
INNOVATION (absent from prior art): evidence_refs + authority +
|
||||
status on every relationship; verified_by/deployed_by to concrete
|
||||
entities; consent-gated schema migration.
|
||||
|
||||
Storage: the EXISTING structural_store generic core (rule A — no
|
||||
second graph store, no competing Spine; Spine uses the same core for
|
||||
Rune, project models use it for their projects).
|
||||
|
||||
ENTITY KINDS: project, domain, subsystem, component (module), interface,
|
||||
state_store, test_suite, deployment_surface, environment.
|
||||
RELATIONSHIPS: belongs_to, depends_on, calls, provides, consumes,
|
||||
reads, writes, connects_to, verified_by, deployed_by, owns, supersedes,
|
||||
legacy_of — every edge carries evidence_refs + authority + status.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from rune.structural_store import (
|
||||
add_entity, add_relationship, entity, load, new_doc, save,
|
||||
)
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
ENTITY_KINDS = (
|
||||
"project", "domain", "subsystem", "component", "interface",
|
||||
"state_store", "test_suite", "deployment_surface", "environment",
|
||||
)
|
||||
|
||||
RELATION_TYPES = (
|
||||
"belongs_to", "owns", "depends_on", "calls", "provides", "consumes",
|
||||
"reads", "writes", "connects_to", "verified_by", "deployed_by",
|
||||
"supersedes", "legacy_of",
|
||||
)
|
||||
|
||||
|
||||
class StructuralModel:
|
||||
"""A project's persistent interconnected structural model. One file
|
||||
per project: <store_dir>/structural.json (spine-shaped — same
|
||||
generic core, open-world: unknown future kinds/relationships survive
|
||||
round-trips by construction)."""
|
||||
|
||||
def __init__(self, store_dir: Path | str, project_id: str):
|
||||
self.store_dir = Path(store_dir)
|
||||
self.project_id = project_id
|
||||
self.path = self.store_dir / "structural.json"
|
||||
doc = load(self.path)
|
||||
if doc is None:
|
||||
doc = new_doc()
|
||||
self._init_project(doc)
|
||||
self.doc = doc
|
||||
|
||||
# -- persistence ----------------------------------------------------
|
||||
|
||||
def _init_project(self, doc: dict) -> None:
|
||||
add_entity(doc, id=f"project:{self.project_id}", kind="project",
|
||||
maturity="DECLARED",
|
||||
evidence_refs=["structural-model-init"])
|
||||
root = add_relationship(doc, type="belongs_to",
|
||||
from_id=f"project:{self.project_id}",
|
||||
to_id=f"project:{self.project_id}",
|
||||
evidence_refs=["structural-model-root"])
|
||||
root["authority"] = "model-author"
|
||||
root["status"] = "root"
|
||||
save(doc, self.path)
|
||||
|
||||
def save(self) -> None:
|
||||
self.doc["schema_version"] = SCHEMA_VERSION
|
||||
save(self.doc, self.path)
|
||||
|
||||
def reload(self) -> None:
|
||||
doc = load(self.path)
|
||||
if doc is not None:
|
||||
self.doc = doc
|
||||
|
||||
# -- entity/relationship construction -------------------------------
|
||||
|
||||
def add(self, kind: str, name: str, *, parent: str | None = None,
|
||||
ext: dict | None = None,
|
||||
evidence_ref: str = "structural-model") -> str:
|
||||
"""Add an entity with a STABLE deterministic id (kind:name —
|
||||
persistent across model versions; rule 18E). parent (an id) adds
|
||||
the belongs_to edge (Backstage partOf semantics)."""
|
||||
if kind not in ENTITY_KINDS:
|
||||
raise ValueError(f"unknown entity kind {kind!r} — known: "
|
||||
f"{ENTITY_KINDS}")
|
||||
eid = f"{kind}:{name}"
|
||||
e = add_entity(self.doc, id=eid, kind=kind, maturity="DECLARED",
|
||||
evidence_refs=[evidence_ref])
|
||||
e["identities"]["name"] = name
|
||||
if ext:
|
||||
e.setdefault("ext", {}).update(ext)
|
||||
if parent:
|
||||
self.relate(eid, "belongs_to", parent,
|
||||
evidence_ref=evidence_ref)
|
||||
return eid
|
||||
|
||||
def relate(self, from_id: str, rel: str, to_id: str, *,
|
||||
evidence_ref: str = "structural-model",
|
||||
authority: str = "model-author",
|
||||
status: str = "active", ext: dict | None = None) -> dict:
|
||||
"""Add a typed relationship. Every edge carries evidence_refs +
|
||||
authority + status (the INNOVATION per the Build Basis —
|
||||
authority-graded claims). Authority values: model-author
|
||||
(agent-observed/derived), importer (external tool derived —
|
||||
Glean batch-layering adaptation), user (user-locked)."""
|
||||
if rel not in RELATION_TYPES:
|
||||
raise ValueError(f"unknown relationship {rel!r} — known: "
|
||||
f"{RELATION_TYPES}")
|
||||
r = add_relationship(self.doc, type=rel, from_id=from_id,
|
||||
to_id=to_id, evidence_refs=[evidence_ref])
|
||||
r["authority"] = authority
|
||||
r["status"] = status
|
||||
if ext:
|
||||
r["ext"] = ext
|
||||
return r
|
||||
|
||||
# -- queries (§18F) ---------------------------------------------------
|
||||
|
||||
def get(self, eid: str) -> dict | None:
|
||||
return entity(self.doc, eid)
|
||||
|
||||
def children(self, eid: str) -> list[str]:
|
||||
return [r["from"] for r in self.doc["relationships"]
|
||||
if r["type"] == "belongs_to" and r["to"] == eid
|
||||
and r["from"] != eid]
|
||||
|
||||
def beneath(self, eid: str) -> list[str]:
|
||||
"""Transitive descendants (bounded by the graph itself)."""
|
||||
seen, out, frontier = {eid}, [], [eid]
|
||||
while frontier:
|
||||
cur = frontier.pop()
|
||||
for child in self.children(cur):
|
||||
if child not in seen:
|
||||
seen.add(child)
|
||||
out.append(child)
|
||||
frontier.append(child)
|
||||
return sorted(out)
|
||||
|
||||
def owning_domain(self, eid: str) -> str | None:
|
||||
cur, hops = eid, 0
|
||||
while hops < 16:
|
||||
if cur.startswith("domain:"):
|
||||
return cur
|
||||
parents = [r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "belongs_to" and r["from"] == cur]
|
||||
if not parents:
|
||||
return None
|
||||
cur = parents[0]
|
||||
hops += 1
|
||||
return None
|
||||
|
||||
def dependencies(self, eid: str) -> list[str]:
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "depends_on" and r["from"] == eid
|
||||
and r.get("status") != "stale")
|
||||
|
||||
def consumers(self, eid: str) -> list[str]:
|
||||
"""Who depends on OR consumes this entity (interfaces are
|
||||
consumed; components are depended on — Backstage relation pairs)."""
|
||||
return sorted(r["from"] for r in self.doc["relationships"]
|
||||
if r["type"] in ("depends_on", "consumes")
|
||||
and r["to"] == eid and r.get("status") != "stale")
|
||||
|
||||
def interfaces_for(self, eid: str) -> list[str]:
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "provides" and r["from"] == eid)
|
||||
|
||||
def state_owned(self, eid: str) -> list[str]:
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "owns" and r["from"] == eid)
|
||||
|
||||
def verification_surfaces(self, eid: str) -> list[str]:
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "verified_by" and r["from"] == eid)
|
||||
|
||||
def deployment_surfaces(self, eid: str) -> list[str]:
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "deployed_by" and r["from"] == eid)
|
||||
|
||||
def canonical_state_owner(self, eid: str) -> str | None:
|
||||
"""The canonical-state owner: the state_store this entity owns,
|
||||
or (for a state_store) itself."""
|
||||
e = self.get(eid)
|
||||
if e and e.get("kind") == "state_store":
|
||||
return eid
|
||||
owned = self.state_owned(eid)
|
||||
return owned[0] if owned else None
|
||||
|
||||
def supersession(self, eid: str) -> dict:
|
||||
return {
|
||||
"supersedes": sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "supersedes"
|
||||
and r["from"] == eid),
|
||||
"superseded_by": sorted(
|
||||
r["from"] for r in self.doc["relationships"]
|
||||
if r["type"] == "supersedes" and r["to"] == eid),
|
||||
"legacy_of": sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "legacy_of"
|
||||
and r["from"] == eid),
|
||||
}
|
||||
|
||||
# -- impact closure (§18F) -------------------------------------------
|
||||
|
||||
def impact_closure(self, eid: str, *, max_depth: int = 8) -> dict:
|
||||
"""STRUCTURALLY BOUNDED SCOPE (rule J): the necessary structural
|
||||
impact closure for a change to eid. Nx-affected semantics:
|
||||
reverse-dependency closure + verification + deployment + state
|
||||
surfaces. Unknown/dynamic relationships stay explicit uncertainty
|
||||
— never silent (rule 18F)."""
|
||||
if self.get(eid) is None:
|
||||
return {"error": f"unknown entity {eid!r}"}
|
||||
# reverse-dependency closure (consumers, transitively, bounded)
|
||||
seen, frontier, depth = {eid}, [eid], 0
|
||||
consumers: set[str] = set()
|
||||
while frontier and depth < max_depth:
|
||||
nxt = []
|
||||
for cur in frontier:
|
||||
for c in self.consumers(cur):
|
||||
if c not in seen:
|
||||
seen.add(c)
|
||||
consumers.add(c)
|
||||
nxt.append(c)
|
||||
frontier, depth = nxt, depth + 1
|
||||
direct_deps = self.dependencies(eid)
|
||||
# ripple through PROVIDED interfaces (Kythe exports semantics):
|
||||
# a change that touches an entity's provided interface reaches
|
||||
# every consumer of that interface
|
||||
for iface in self.interfaces_for(eid):
|
||||
for c in self.consumers(iface):
|
||||
if c not in seen:
|
||||
seen.add(c)
|
||||
consumers.add(c)
|
||||
uncertainty = [
|
||||
{"kind": "dynamic-consumer",
|
||||
"note": "static model cannot see dynamic dispatch/string-keyed "
|
||||
"lookup/late-binding consumers — reconcilers must "
|
||||
"verify at runtime"}]
|
||||
# lockfile/deps edge (Nx documented case): deps outside closure
|
||||
# are OBSERVATION scope, not mutation scope
|
||||
return {
|
||||
"target": eid,
|
||||
"owning_domain": self.owning_domain(eid),
|
||||
"mutation_scope": sorted({eid} | consumers),
|
||||
"observation_scope": {
|
||||
"direct_dependencies": direct_deps,
|
||||
# interfaces the target provides AND consumes — both
|
||||
# directions are observation-relevant for a change
|
||||
"interfaces_provided": self.interfaces_for(eid),
|
||||
"interfaces_consumed": sorted(
|
||||
r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "consumes" and r["from"] == eid),
|
||||
},
|
||||
"state_surfaces": sorted(
|
||||
{s for e in ({eid} | consumers)
|
||||
for s in self.state_owned(e)}),
|
||||
"verification_scope": sorted(
|
||||
{v for e in ({eid} | consumers)
|
||||
for v in self.verification_surfaces(e)}),
|
||||
"deployment_scope": sorted(
|
||||
{d for e in ({eid} | consumers)
|
||||
for d in self.deployment_surfaces(e)}),
|
||||
"invariants": [
|
||||
f"belongs_to hierarchy of {eid} remains stable",
|
||||
"canonical-state ownership unchanged"],
|
||||
"non_goals": [],
|
||||
"uncertainty": uncertainty,
|
||||
"closure_depth": depth,
|
||||
}
|
||||
|
||||
|
||||
# ── T18 §18K: workspace lifecycle foundation ────────────────────────────
|
||||
#
|
||||
# Canonical lifecycle (rule 18K) — BuildManager remains the bounded
|
||||
# adapter for the worktree mechanics (17G decision), the LIFECYCLE and
|
||||
# its record live here so contracts can carry workspace provenance:
|
||||
#
|
||||
# verified source/base SHA → per-contract isolated worktree →
|
||||
# construction → verification → commit → push → CI/merge/release →
|
||||
# deployment confirmation → evidence archival → cleanup
|
||||
#
|
||||
# Records never hard-code live-runtime mutation (rule P): the deployed
|
||||
# generation changes only through the canonical promote machinery.
|
||||
|
||||
WORKSPACE_LIFECYCLE_STAGES = (
|
||||
"base_verified", "worktree_created", "construction",
|
||||
"verified", "committed", "pushed", "ci_qualified",
|
||||
"released", "deployment_confirmed", "evidence_archived", "cleaned",
|
||||
)
|
||||
|
||||
|
||||
def workspace_record(*, contract_id: str, construction_base: str,
|
||||
runtime_generation: str = "",
|
||||
candidate_generation: str = "",
|
||||
promotion_target: str = "gitea/master",
|
||||
rollback_generation: str = "") -> dict:
|
||||
"""The workspace provenance record a contract carries. Generations
|
||||
are SHAs/dirs — never live-tree mutations."""
|
||||
return {
|
||||
"contract_id": contract_id,
|
||||
"construction_base": construction_base,
|
||||
"runtime_generation": runtime_generation,
|
||||
"candidate_generation": candidate_generation,
|
||||
"promotion_target": promotion_target,
|
||||
"rollback_generation": rollback_generation,
|
||||
"stages_completed": [],
|
||||
"lifecycle": list(WORKSPACE_LIFECYCLE_STAGES),
|
||||
}
|
||||
|
||||
|
||||
def workspace_advance(record: dict, stage: str, *,
|
||||
evidence: str = "") -> dict:
|
||||
"""Advance the workspace lifecycle one stage (order-enforced)."""
|
||||
if stage not in WORKSPACE_LIFECYCLE_STAGES:
|
||||
raise ValueError(f"unknown workspace stage {stage!r}")
|
||||
done = record["stages_completed"]
|
||||
if done:
|
||||
last = WORKSPACE_LIFECYCLE_STAGES.index(done[-1])
|
||||
cur = WORKSPACE_LIFECYCLE_STAGES.index(stage)
|
||||
if cur != last + 1:
|
||||
raise ValueError(
|
||||
f"lifecycle order violation: {done[-1]} → {stage}")
|
||||
elif stage != WORKSPACE_LIFECYCLE_STAGES[0]:
|
||||
raise ValueError(f"lifecycle must start at "
|
||||
f"{WORKSPACE_LIFECYCLE_STAGES[0]}, got {stage!r}")
|
||||
done.append(stage)
|
||||
if evidence:
|
||||
record.setdefault("evidence", []).append(
|
||||
{"stage": stage, "evidence": evidence[:200]})
|
||||
return record
|
||||
@@ -109,8 +109,14 @@ def _redact_secrets(obj):
|
||||
dict key named like a credential has its value stripped before
|
||||
serialization. Structural stores must never become secret stores."""
|
||||
if isinstance(obj, dict):
|
||||
def _is_secret_key(key) -> bool:
|
||||
kl = str(key).lower()
|
||||
return (kl in _SECRET_KEY_NAMES
|
||||
or any(w in kl for w in ("token", "api_key", "apikey",
|
||||
"password", "secret",
|
||||
"credential", "private_key")))
|
||||
return {
|
||||
k: ("<redacted>" if str(k).lower() in _SECRET_KEY_NAMES else _redact_secrets(v)) for k, v in obj.items()
|
||||
k: ("<redacted>" if _is_secret_key(k) else _redact_secrets(v)) for k, v in obj.items()
|
||||
}
|
||||
if isinstance(obj, list):
|
||||
return [_redact_secrets(v) for v in obj]
|
||||
@@ -128,6 +134,17 @@ def save(doc: dict, path: Path | str) -> Path:
|
||||
return p
|
||||
|
||||
|
||||
def canonical_digest(doc: dict) -> str:
|
||||
"""SCAFFOLD-TRANCHE-0012 §12B: stable digest of a document's SEMANTIC
|
||||
content — the same normalization save() uses on disk (_sorted +
|
||||
_redact_secrets + sort_keys). Key ordering cannot create false failure;
|
||||
identical semantic content always yields an identical digest."""
|
||||
import hashlib
|
||||
|
||||
payload = json.dumps(_sorted(_redact_secrets(doc)), sort_keys=True)
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def snapshot(doc: dict, snapshots_dir: Path, label: str = "", tag: str = "") -> Path:
|
||||
"""Persist an immutable snapshot. `tag` is a caller-supplied identity
|
||||
fragment (Spine uses the deployed SHA; a Project Scaffold may use its
|
||||
|
||||
@@ -52,6 +52,14 @@ def _save(data):
|
||||
os.replace(tmp_path, LEDGER_PATH)
|
||||
|
||||
|
||||
def load() -> dict:
|
||||
"""Public read of the persisted task ledger (T17 17R: the TUI
|
||||
startup loader called task_persist.load() which did not exist —
|
||||
the AttributeError was silently swallowed and startup loading was
|
||||
dead code). Returns the raw ledger doc: {"tasks": [...]}."""
|
||||
return _load()
|
||||
|
||||
|
||||
def export(tasks_json):
|
||||
"""Save a list of tasks (from task tool output) to the ledger.
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
TAP version 13
|
||||
# Subtest: createPracticeItem
|
||||
# Subtest: creates an item with title, minutes, and an id
|
||||
ok 1 - creates an item with title, minutes, and an id
|
||||
---
|
||||
duration_ms: 0.515984
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: assigns distinct ids to successive items
|
||||
ok 2 - assigns distinct ids to successive items
|
||||
---
|
||||
duration_ms: 0.116346
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: rejects an empty title
|
||||
ok 3 - rejects an empty title
|
||||
---
|
||||
duration_ms: 0.31372
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: rejects non-positive minutes
|
||||
ok 4 - rejects non-positive minutes
|
||||
---
|
||||
duration_ms: 0.116486
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
ok 1 - createPracticeItem
|
||||
---
|
||||
duration_ms: 2.21403
|
||||
type: 'suite'
|
||||
...
|
||||
# Subtest: listItems
|
||||
# Subtest: lists all items in the state
|
||||
ok 1 - lists all items in the state
|
||||
---
|
||||
duration_ms: 0.516824
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: returns copies, so mutating the list does not touch the state
|
||||
ok 2 - returns copies, so mutating the list does not touch the state
|
||||
---
|
||||
duration_ms: 0.096947
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
ok 2 - listItems
|
||||
---
|
||||
duration_ms: 0.730577
|
||||
type: 'suite'
|
||||
...
|
||||
# Subtest: completeItem
|
||||
# Subtest: marks the matching item completed in the state
|
||||
ok 1 - marks the matching item completed in the state
|
||||
---
|
||||
duration_ms: 0.211093
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: throws for an unknown id
|
||||
ok 2 - throws for an unknown id
|
||||
---
|
||||
duration_ms: 0.168354
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
ok 3 - completeItem
|
||||
---
|
||||
duration_ms: 0.608651
|
||||
type: 'suite'
|
||||
...
|
||||
1..3
|
||||
# tests 8
|
||||
# suites 3
|
||||
# pass 8
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 54.342572
|
||||
@@ -0,0 +1,32 @@
|
||||
# Task Ledger
|
||||
|
||||
A small standalone task ledger backed by SQLite (stdlib `sqlite3` only).
|
||||
Supports adding tasks, listing them, and marking them complete.
|
||||
|
||||
## Files
|
||||
|
||||
- `ledger.py` — the ledger (`connect`, `add_task`, `list_tasks`,
|
||||
`complete_task`; SQLite storage, `:memory:` by default or a file path).
|
||||
- `ledger.test.py` — stdlib `unittest` suite proving add, list and
|
||||
complete work, including SQLite file persistence.
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
import ledger
|
||||
|
||||
conn = ledger.connect("tasks.db") # or ledger.connect() for in-memory
|
||||
task = ledger.add_task(conn, "write report")
|
||||
print(ledger.list_tasks(conn))
|
||||
ledger.complete_task(conn, task["id"])
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
The test file uses a Node-style name (`ledger.test.py`), so the default
|
||||
`unittest discover` pattern (`test*.py`) does not match it. Run it
|
||||
directly:
|
||||
|
||||
```sh
|
||||
python3 ledger.test.py -v
|
||||
```
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Standalone SQLite task ledger: add, list and complete tasks.
|
||||
|
||||
Stdlib only (sqlite3). State lives in SQLite; the caller owns the
|
||||
connection (``:memory:`` by default, or a file path for persistence).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
SCHEMA = (
|
||||
"CREATE TABLE IF NOT EXISTS tasks ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"title TEXT NOT NULL, "
|
||||
"completed INTEGER NOT NULL DEFAULT 0"
|
||||
")"
|
||||
)
|
||||
|
||||
|
||||
def connect(path: str = ":memory:") -> sqlite3.Connection:
|
||||
"""Open a ledger database and ensure the schema exists."""
|
||||
conn = sqlite3.connect(path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
init_db(conn)
|
||||
return conn
|
||||
|
||||
|
||||
def init_db(conn: sqlite3.Connection) -> None:
|
||||
"""Create the tasks table if it does not exist."""
|
||||
conn.execute(SCHEMA)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _row_to_dict(row: sqlite3.Row) -> dict:
|
||||
return {
|
||||
"id": row["id"],
|
||||
"title": row["title"],
|
||||
"completed": bool(row["completed"]),
|
||||
}
|
||||
|
||||
|
||||
def add_task(conn: sqlite3.Connection, title: str) -> dict:
|
||||
"""Add a task; return ``{"id", "title", "completed"}``.
|
||||
|
||||
Raises:
|
||||
TypeError: if title is not a string.
|
||||
ValueError: if title is empty or blank.
|
||||
"""
|
||||
if not isinstance(title, str):
|
||||
raise TypeError("title must be a string")
|
||||
cleaned = title.strip()
|
||||
if not cleaned:
|
||||
raise ValueError("title must be a non-empty string")
|
||||
init_db(conn)
|
||||
cur = conn.execute(
|
||||
"INSERT INTO tasks (title, completed) VALUES (?, 0)", (cleaned,)
|
||||
)
|
||||
conn.commit()
|
||||
task_id = cur.lastrowid
|
||||
row = conn.execute(
|
||||
"SELECT id, title, completed FROM tasks WHERE id = ?", (task_id,)
|
||||
).fetchone()
|
||||
return _row_to_dict(row)
|
||||
|
||||
|
||||
def list_tasks(conn: sqlite3.Connection) -> list:
|
||||
"""Return all tasks ordered by id (each a dict)."""
|
||||
init_db(conn)
|
||||
rows = conn.execute(
|
||||
"SELECT id, title, completed FROM tasks ORDER BY id"
|
||||
).fetchall()
|
||||
return [_row_to_dict(r) for r in rows]
|
||||
|
||||
|
||||
def complete_task(conn: sqlite3.Connection, task_id: int) -> dict:
|
||||
"""Mark the task with ``task_id`` completed; return the updated dict.
|
||||
|
||||
Raises:
|
||||
TypeError: if task_id is not an int.
|
||||
LookupError: if no task has that id.
|
||||
"""
|
||||
if isinstance(task_id, bool) or not isinstance(task_id, int):
|
||||
raise TypeError("task_id must be an int")
|
||||
init_db(conn)
|
||||
cur = conn.execute(
|
||||
"UPDATE tasks SET completed = 1 WHERE id = ?", (task_id,)
|
||||
)
|
||||
conn.commit()
|
||||
if cur.rowcount == 0:
|
||||
raise LookupError(f"no task with id {task_id}")
|
||||
row = conn.execute(
|
||||
"SELECT id, title, completed FROM tasks WHERE id = ?", (task_id,)
|
||||
).fetchone()
|
||||
return _row_to_dict(row)
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Tests for the standalone SQLite task ledger (``ledger.py``).
|
||||
|
||||
Run directly (the file name is Node-style, so the default
|
||||
``python -m unittest discover`` pattern ``test*.py`` does not pick it up)::
|
||||
|
||||
python3 ledger.test.py -v
|
||||
|
||||
Stdlib only.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import ledger
|
||||
|
||||
|
||||
def fresh_conn():
|
||||
return ledger.connect(":memory:")
|
||||
|
||||
|
||||
class TestAddTask(unittest.TestCase):
|
||||
def test_add_returns_task_with_id_title_and_not_completed(self):
|
||||
conn = fresh_conn()
|
||||
task = ledger.add_task(conn, "write report")
|
||||
self.assertEqual(task["title"], "write report")
|
||||
self.assertFalse(task["completed"])
|
||||
self.assertIsInstance(task["id"], int)
|
||||
|
||||
def test_add_assigns_unique_ids(self):
|
||||
conn = fresh_conn()
|
||||
first = ledger.add_task(conn, "one")
|
||||
second = ledger.add_task(conn, "two")
|
||||
self.assertNotEqual(first["id"], second["id"])
|
||||
|
||||
def test_add_trims_title(self):
|
||||
conn = fresh_conn()
|
||||
task = ledger.add_task(conn, " tidy desk ")
|
||||
self.assertEqual(task["title"], "tidy desk")
|
||||
|
||||
def test_add_rejects_empty_or_blank_title(self):
|
||||
conn = fresh_conn()
|
||||
with self.assertRaises(ValueError):
|
||||
ledger.add_task(conn, "")
|
||||
with self.assertRaises(ValueError):
|
||||
ledger.add_task(conn, " ")
|
||||
|
||||
def test_add_rejects_non_string_title(self):
|
||||
conn = fresh_conn()
|
||||
with self.assertRaises(TypeError):
|
||||
ledger.add_task(conn, None)
|
||||
with self.assertRaises(TypeError):
|
||||
ledger.add_task(conn, 123)
|
||||
|
||||
|
||||
class TestListTasks(unittest.TestCase):
|
||||
def test_list_empty_ledger(self):
|
||||
self.assertEqual(ledger.list_tasks(fresh_conn()), [])
|
||||
|
||||
def test_list_returns_all_tasks_in_order(self):
|
||||
conn = fresh_conn()
|
||||
ledger.add_task(conn, "alpha")
|
||||
ledger.add_task(conn, "beta")
|
||||
tasks = ledger.list_tasks(conn)
|
||||
self.assertEqual([t["title"] for t in tasks], ["alpha", "beta"])
|
||||
|
||||
def test_list_shows_completed_flags(self):
|
||||
conn = fresh_conn()
|
||||
task = ledger.add_task(conn, "alpha")
|
||||
ledger.add_task(conn, "beta")
|
||||
ledger.complete_task(conn, task["id"])
|
||||
by_title = {t["title"]: t for t in ledger.list_tasks(conn)}
|
||||
self.assertTrue(by_title["alpha"]["completed"])
|
||||
self.assertFalse(by_title["beta"]["completed"])
|
||||
|
||||
|
||||
class TestCompleteTask(unittest.TestCase):
|
||||
def test_complete_marks_matching_task(self):
|
||||
conn = fresh_conn()
|
||||
first = ledger.add_task(conn, "alpha")
|
||||
second = ledger.add_task(conn, "beta")
|
||||
updated = ledger.complete_task(conn, first["id"])
|
||||
self.assertTrue(updated["completed"])
|
||||
self.assertEqual(updated["id"], first["id"])
|
||||
by_title = {t["title"]: t for t in ledger.list_tasks(conn)}
|
||||
self.assertTrue(by_title["alpha"]["completed"])
|
||||
self.assertFalse(by_title["beta"]["completed"])
|
||||
|
||||
def test_complete_persists_across_listing(self):
|
||||
conn = fresh_conn()
|
||||
task = ledger.add_task(conn, "alpha")
|
||||
ledger.complete_task(conn, task["id"])
|
||||
done = [t for t in ledger.list_tasks(conn) if t["completed"]]
|
||||
self.assertEqual([t["title"] for t in done], ["alpha"])
|
||||
|
||||
def test_complete_unknown_id_raises(self):
|
||||
conn = fresh_conn()
|
||||
ledger.add_task(conn, "alpha")
|
||||
with self.assertRaises(LookupError):
|
||||
ledger.complete_task(conn, 999)
|
||||
|
||||
def test_complete_rejects_non_int_id(self):
|
||||
conn = fresh_conn()
|
||||
ledger.add_task(conn, "alpha")
|
||||
with self.assertRaises(TypeError):
|
||||
ledger.complete_task(conn, "1")
|
||||
|
||||
|
||||
class TestPersistence(unittest.TestCase):
|
||||
def test_tasks_survive_reconnect_to_file_db(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = os.path.join(tmp, "tasks.db")
|
||||
conn = ledger.connect(path)
|
||||
task = ledger.add_task(conn, "persist me")
|
||||
ledger.complete_task(conn, task["id"])
|
||||
conn.close()
|
||||
reopened = ledger.connect(path)
|
||||
try:
|
||||
tasks = ledger.list_tasks(reopened)
|
||||
finally:
|
||||
reopened.close()
|
||||
self.assertEqual(len(tasks), 1)
|
||||
self.assertEqual(tasks[0]["title"], "persist me")
|
||||
self.assertTrue(tasks[0]["completed"])
|
||||
|
||||
def test_uses_sqlite_storage(self):
|
||||
conn = fresh_conn()
|
||||
self.assertIsInstance(conn, sqlite3.Connection)
|
||||
ledger.add_task(conn, "alpha")
|
||||
rows = conn.execute("SELECT title FROM tasks").fetchall()
|
||||
self.assertEqual([r[0] for r in rows], ["alpha"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,2 @@
|
||||
# tests 10
|
||||
# pass 10
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,42 @@
|
||||
# T17 §17T — real-Rune re-entry benchmark (2026-09-15)
|
||||
|
||||
Target: the event/wait-resume subsystem. Two fresh-identity agents, no
|
||||
transcript. A: cold repo exploration. B: docs/STRUCTURAL_MAP.md first,
|
||||
then targeted reconciliation.
|
||||
|
||||
## Measured
|
||||
|
||||
| Metric | A cold | B map-assisted |
|
||||
|---|---|---|
|
||||
| Files opened | 6 | 4 (+ grep over ~10) |
|
||||
| Commands run | 14 (11 bash + 3 reads) | 13 (10 bash + 3 grep) |
|
||||
| Test runs | 1 (19 tests) | 1 (8 tests) |
|
||||
| Authority identified | correct (durable waitstore record) | correct |
|
||||
| Ownership identified | correct (out-of-repo bridge) | correct |
|
||||
| Unfinished/duplication found | CI adapter no production call site; bridge unversioned | CI coverage gap; TWO data roots (split-brain risk); t14 legacy; build_mode wired |
|
||||
| Wrong assumptions | **1 material**: claimed daemon DEAD with stale pidfile (false — PID 287505 live, /health ok) | 0 material; flagged env-pinning dependency |
|
||||
| Map reconciliation | n/a | 5 CONFIRMED, 1 UNVERIFIABLE, 0 DRIFTED |
|
||||
| Confidence | 80-95% | high/medium, calibrated |
|
||||
|
||||
## Honest verdict
|
||||
|
||||
1. **Raw cost ≈ equal** (6 vs 4 files, 14 vs 13 commands) — consistent
|
||||
with 16O: no step-count win on a well-structured repo.
|
||||
2. **Correctness gap is real**: the cold path asserted a FALSE runtime
|
||||
fact (daemon dead) from a stale pidfile and reported it at 95%
|
||||
confidence; the map-assisted path, knowing the daemon's deployment
|
||||
shape from the map, verified liveness and reported the env-pinning
|
||||
dependency instead. One false belief about a LIVE system is exactly
|
||||
the failure mode re-entry exists to prevent.
|
||||
3. **Coverage gap**: the map-assisted path found the two-data-roots
|
||||
split-brain risk (legacy ~/.local/share/mimo-wait-resume/waits still
|
||||
holds 46 records; wr_root() defaults there when RUNE_WR_ROOT unset);
|
||||
the cold path missed it.
|
||||
4. The map itself survived reconciliation with ZERO drifted claims —
|
||||
maintained structural projections are earning their update cost.
|
||||
|
||||
## Actionable finding (fed to 17U/W)
|
||||
The two-data-roots finding is real debt: `wr_root()` (event_wait.py)
|
||||
falls back to the legacy home when RUNE_WR_ROOT is unset. Production
|
||||
pins it (deploy.yml), but any unpinned consumer silently reads a stale
|
||||
store. Candidate: drop the legacy fallback or make it warn loudly.
|
||||
@@ -0,0 +1,109 @@
|
||||
# T17 §17V — Scaffolding freeze assessment + §17X adversarial matrix
|
||||
(2026-09-15)
|
||||
|
||||
## §17X — adversarial system matrix (36 cases → evidence)
|
||||
|
||||
| # | Case | Evidence |
|
||||
|---|------|----------|
|
||||
| 1 | explicit build request | 17Q-1 (PROJECT_BUILD, bootstrap) |
|
||||
| 2 | indirect authorization | 17Q-4 ("that's the one, make it happen") |
|
||||
| 3 | discussion only | 17Q-2 + 17L test (3 phrasings) |
|
||||
| 4 | ambiguous referent | 17Q-6 (asks, never guesses) |
|
||||
| 5 | stale referent | 17Q-8 (topic-shift resistance) |
|
||||
| 6 | superseded proposal | 17Q-20 |
|
||||
| 7 | option-selection referent | 17Q-5 (ordinal) |
|
||||
| 8 | new project | 17Q-14 |
|
||||
| 9 | existing project | 17Q-11 |
|
||||
| 10 | local change | 17Q-12 |
|
||||
| 11 | non-construction request | 17Q-10 ("build a list") |
|
||||
| 12 | research request | 17Q-9 |
|
||||
| 13 | provider unavailable | 16G-C (verbatim codes) |
|
||||
| 14 | provider manually changed | 16G-G (scaffold byte-identical) |
|
||||
| 15 | duplicate completion event | 15E double-resume tests |
|
||||
| 16 | completion event before waiter | 17C replay (4.6s prod-realistic) |
|
||||
| 17 | completion event after waiter | 17C cross-user delivery (2.05s) |
|
||||
| 18 | lost event | T16 16D live cycles (deadline recovery, promoted) |
|
||||
| 19 | controller restart | 15E restart tests + resume_or_recover |
|
||||
| 20 | stage restart | 17E wiring: already-closed skip test |
|
||||
| 21 | programme restart | 17E wiring: restart-from-durable-record test |
|
||||
| 22 | subtask≠programme | 15M canary + 17E failure isolation |
|
||||
| 23 | programme complete | 17E: PROGRAMME_COMPLETE / _WITH_FAILURES honest verdicts |
|
||||
| 24 | blocked stage | canary BLOCKED→fallback (15M tests) |
|
||||
| 25 | approval-required stage | canary APPROVAL_REQUIRED honest stop (15M) |
|
||||
| 26 | duplicate task state migration | 17F held-outs (counters from authority) |
|
||||
| 27 | legacy build route | 17G gate (/build start refused unless classified) |
|
||||
| 28 | command/NL equivalence | 17Q-15/16 (same classifier, same semantics) |
|
||||
| 29 | ContextIntent replay/idempotency | intents stateless; contracts idempotent (17E dup-signal test) |
|
||||
| 30 | invalid Substrate command | 16K refusal tests |
|
||||
| 31 | long-gap re-entry | 16O + 17T benchmarks |
|
||||
| 32 | repository drift | 16P VAT-regression blocks resume |
|
||||
| 33 | false-green test | stage_guards verify_test_truth (15F-I) |
|
||||
| 34 | Henxels structural failure | rc=0 all session; manifest gate caught every new module |
|
||||
| 35 | release stale publisher | T14B authority re-read (prior tranche, tests stand) |
|
||||
| 36 | deployment convergence | T16/T17: 0.66.63→0.66.66 auto-converged, verified |
|
||||
|
||||
Coverage honestly noted: cases 13-15, 19, 33, 35 rely on T14-T16 suites
|
||||
(green in this session's full runs); cases 16-18 carry THIS-tranche live
|
||||
production evidence.
|
||||
|
||||
## §17V — FREEZE ASSESSMENT
|
||||
|
||||
Checklist (mandate's 17 items):
|
||||
1. one canonical construction kernel — YES (scaffold.py + ONE
|
||||
classify_mode; T17 17G closed the /build bypass gate; 17U deleted the
|
||||
dead second orchestrator)
|
||||
2. native conversational/default activation — YES (operator hook path,
|
||||
T15; T17 added the conversational interpretation layer)
|
||||
3. explicit construction commands — YES (17K/17Q-1/7)
|
||||
4. contextual multi-turn activation — YES (17J-17N, 17Q matrix 22 cases)
|
||||
5. one canonical programme continuation mechanism — YES (17D/17E wired +
|
||||
live-proven)
|
||||
6. event-driven waiting canonical where events exist — YES (16C/17C;
|
||||
CI + builder adapters, replay, cross-user delivery)
|
||||
7. watchdog only as recovery/failure detection — YES (proven in
|
||||
production twice; wake path proven production-realistically)
|
||||
8. provider ownership outside Scaffolding — YES (16F structural guard)
|
||||
9. builder cannot self-certify — YES (phase-gated close; closure
|
||||
refusals observed live in T14)
|
||||
10. completion truth evidence-driven — YES (checkpoints on contracts;
|
||||
verify_test_truth)
|
||||
11. interruption/resume proven — YES (T14E live; 17E restart tests)
|
||||
12. long-gap re-entry supported — YES (drift_resume + 16O/17T honest
|
||||
benchmarks)
|
||||
13. Rune itself uses Scaffolding — YES (T16 16R, T17 17R/Y contracts
|
||||
closed through the real flow)
|
||||
14. future Substrate seam stable — YES (16I/J/K; SCHEMA_VERSION=1;
|
||||
zero direct-store coupling tests)
|
||||
15. major duplicate construction authorities removed or consciously
|
||||
deprecated — YES with residuals (plugin deleted; /build gated;
|
||||
build_mode kept as bounded adapter BY DECISION; task-state cache
|
||||
demoted by design, full writer retirement deferred)
|
||||
16. real-world generalisation begun — PARTIAL (fixtures + Rune itself;
|
||||
no external project yet)
|
||||
17. remaining work mostly bugs/edge/docs/adoption — YES (16W/17U debt
|
||||
lists are small and bounded)
|
||||
|
||||
## VERDICT: FREEZE_READY (with named residuals)
|
||||
|
||||
Rationale: every architecture-level capability the freeze checklist
|
||||
names is implemented, canonical, and verified — most of them with live
|
||||
production evidence from T16/T17. The residuals are deliberately
|
||||
bounded and none is architectural:
|
||||
- build_mode worktree adapter retirement (needs a migration plan, not
|
||||
new architecture)
|
||||
- task-state cache writer retirement (demoted, consumers derived)
|
||||
- replay-path first NATURAL cycle confirmation (mechanism proven
|
||||
production-realistically; only the natural-timing capture pending)
|
||||
- external-project generalisation (adoption, not construction)
|
||||
|
||||
Recommended posture: stop adding Scaffolding architecture. Future work:
|
||||
USE it (Rune retrofit completion, real projects), fix observed defects,
|
||||
simplify, stabilise — exactly the direction the mandate anticipated.
|
||||
|
||||
## §17Z POSTSCRIPT — natural event-path proof (19:15Z, final release cycle)
|
||||
The 17C residual closed ITSELF on the final release: deploy waiter for
|
||||
927548e registered 19:13:28, CI completion wake received 19:15:16,
|
||||
controller resumed ON THE EVENT (mode="event", no rearm, no deadline),
|
||||
authority re-read verdict success at 19:15:20, wall 112.5s, PROMOTED.
|
||||
First NATURAL production event-driven CI wake. v0.66.72 released,
|
||||
deployed current = 927548e = gitea/master = release. Full convergence.
|
||||
@@ -0,0 +1,39 @@
|
||||
# T18 §18A — exact current-state audit (2026-09-15, fresh verification)
|
||||
|
||||
All facts independently verified this session; nothing trusted from the
|
||||
T17 report without re-verification.
|
||||
|
||||
## Identity / release chain
|
||||
- HEAD = gitea/master = **29570f4** (T17 17Z postscript)
|
||||
- Deployed current → 29570f4 (symlink 20:22); last-good → 927548e (0.66.72's
|
||||
bump parent)
|
||||
- Release **v0.66.72** (20:06:36); the post-report doc commit DRAINED and
|
||||
DEPLOYED — T17's "draining" state is fully converged.
|
||||
|
||||
## Gates (lock-exact environments, 29570f4)
|
||||
- Full suite: **1920 passed + 84 subtests** (127s)
|
||||
- Lint ratchet: PASS (BLE001 350/350, S110 78/78 — post-17U shrink)
|
||||
- Type ratchet: PASS — errors **111** (baseline 117 — IMPROVED, honest
|
||||
high-water keeps the lower count), warnings 435 (≤443)
|
||||
- Henxels: rc=0
|
||||
|
||||
## Infrastructure
|
||||
- W/R daemon: pid 287505, /health ok, data root /mnt/workdrive/wr-bridge
|
||||
(shared root; wr_root() anti-split-brain policy active)
|
||||
- Spine store: 148 entities (139 module, 8 subsystem, 1 deployment_surface),
|
||||
636 relationships
|
||||
- Scaffolding store: 28 contracts — now ALL terminal after 18A
|
||||
reconciliation; 3 durable programme records (17E proofs)
|
||||
|
||||
## 18A drift reconciliation performed
|
||||
- 8 orphan contracts from superseded 17E proof attempts (17e-stage-*,
|
||||
17e2-stage-*) were draft/approved in the main store — abandoned
|
||||
truthfully with reconciliation checkpoints. Open contracts: ZERO.
|
||||
|
||||
## Current routes (verified by presence, detailed audits follow)
|
||||
- Task-state authority: TaskStateRepository (17F)
|
||||
- build_mode: bounded adapter behind the /build classification gate (17G)
|
||||
- Contextual routes: construction_intent → classify_mode (17J-Q),
|
||||
request-modals + information-opener precedence live
|
||||
- programme continuation: run_construction_programme wired + live-proven
|
||||
- structural map: docs/STRUCTURAL_MAP.md (updated 17T)
|
||||
@@ -0,0 +1,22 @@
|
||||
# T18 §18D CORRECTION — contextual research scope (2026-09-15, Nick)
|
||||
|
||||
CORRECTION: the 18D research topics (architecture graphs, dependency
|
||||
graphs, service topology, monorepo tooling, ADRs, graph formats) were
|
||||
EXAMPLES relevant to the structural-model task — not a universal
|
||||
research checklist. Scaffolding must never hard-code research topics.
|
||||
|
||||
IMPLEMENTED:
|
||||
- build_basis.generate_research_plan(): context-specific plan synthesis
|
||||
(intent → target/reality → unknowns/decisions/risks → topics). No
|
||||
topic vocabulary in code. Depth by consequence heuristics, always
|
||||
with WHY.
|
||||
- Held-outs (tests/test_contextual_research_plan.py): unrelated intents
|
||||
produce materially different plans; the 18D architecture topics do
|
||||
NOT leak into unrelated research; no-unknowns → depth NONE with empty
|
||||
topics; prior-art machinery summons only when unknowns reference
|
||||
existing practice, phrased for THIS intent.
|
||||
- Principle persisted in docs/SCAFFOLDING_V1_DEFINITION.md.
|
||||
|
||||
The 18D Build Basis (research/t18-structural-model-prior-art) remains
|
||||
VALID: those topics were contextually correct for designing the
|
||||
structural model itself.
|
||||
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"alternatives_considered": [],
|
||||
"created_at": "2026-09-15T21:13:20Z",
|
||||
"deeper": {
|
||||
"findings": [],
|
||||
"refs": [],
|
||||
"used": false
|
||||
},
|
||||
"depth": {
|
||||
"decided_at": "2026-09-15T21:13:20Z",
|
||||
"depth": "FULL",
|
||||
"mode": "",
|
||||
"route": "RESEARCHED_CHANGE",
|
||||
"why": "route RESEARCHED_CHANGE default"
|
||||
},
|
||||
"dispositions": [
|
||||
{
|
||||
"disposition": "ADOPT",
|
||||
"item": "Backstage relation vocabulary + entity-ref discipline + catalog-info.yaml emission",
|
||||
"ref": "backstage.io",
|
||||
"why": "Domain/System/Component/Resource/API maps 1:1 to our nesting; ecosystem interop free"
|
||||
},
|
||||
{
|
||||
"disposition": "ADOPT",
|
||||
"item": "Kythe edge-kind vocabulary (ref/call, property/reads-writes, satisfies, overrides) + derived-reverse-edge rule",
|
||||
"ref": "kythe.io/docs/schema",
|
||||
"why": "disciplined, versioned, explicit"
|
||||
},
|
||||
{
|
||||
"disposition": "ADOPT",
|
||||
"item": "SCIP monikers for cross-index identity bridging",
|
||||
"ref": "scip",
|
||||
"why": "decouples stable identity from any one indexer"
|
||||
},
|
||||
{
|
||||
"disposition": "ADOPT",
|
||||
"item": "Nx affected semantics as impact-closure spec (diff\u2192owning entities\u2192reverse-dep closure)",
|
||||
"ref": "nx.dev/concepts/affected",
|
||||
"why": "productionized impact_frontier"
|
||||
},
|
||||
{
|
||||
"disposition": "ADOPT",
|
||||
"item": "Bazel rdeps(kind(.*_test)) as verified_by-driven verification scoping",
|
||||
"ref": "bazel.build",
|
||||
"why": "semantic template for test-surface queries"
|
||||
},
|
||||
{
|
||||
"disposition": "ADAPT",
|
||||
"item": "Kythe VName stability contract",
|
||||
"ref": "kythe.io",
|
||||
"why": "Kythe explicitly declines cross-version stability \u2014 our persistent model REQUIRES it; keep deterministic regeneration but promise stability"
|
||||
},
|
||||
{
|
||||
"disposition": "ADAPT",
|
||||
"item": "C4 deployment shapes (DeploymentNode/ContainerInstance)",
|
||||
"ref": "c4model.com",
|
||||
"why": "become machine-derived deployed_by/connects_to with evidence, not hand-authored"
|
||||
},
|
||||
{
|
||||
"disposition": "ADAPT",
|
||||
"item": "Glean immutable-batch layering",
|
||||
"ref": "glean.software",
|
||||
"why": "our snapshots are philosophically identical; mark externally-derived edges with distinct authority"
|
||||
},
|
||||
{
|
||||
"disposition": "COMBINE",
|
||||
"item": "Backstage org model + Kythe code model + Nx traversal COMBINED on our evidence-gated store",
|
||||
"ref": "combined",
|
||||
"why": "prior art occupies one layer each; the cross-layer join is the actual design"
|
||||
},
|
||||
{
|
||||
"disposition": "REJECT",
|
||||
"item": "Adopting CodeQL/Kythe/Glean as our store core; generic graph DB (Neo4j); LSIF; Stack Graphs (archived 2025-09)",
|
||||
"ref": "various",
|
||||
"why": "none carry evidence_refs/authority/status/maturity as first-class; heavyweight or dead"
|
||||
},
|
||||
{
|
||||
"disposition": "INNOVATE",
|
||||
"item": "Consent-gated schema migration; verified_by edges to verification entities; deployed_by with code linkage; authority-graded evidence on every edge",
|
||||
"ref": "18V assessment",
|
||||
"why": "genuinely absent from all surveyed systems"
|
||||
}
|
||||
],
|
||||
"implications": {
|
||||
"architecture": [],
|
||||
"migration": [],
|
||||
"structural": [],
|
||||
"verification": []
|
||||
},
|
||||
"inward": {
|
||||
"findings": [],
|
||||
"refs": []
|
||||
},
|
||||
"issue_ref": "research/t18-structural-model-prior-art",
|
||||
"kind": "build_basis",
|
||||
"outward": {
|
||||
"findings": [],
|
||||
"prior_art_questions_answered": [
|
||||
"what already exists addressing this intent",
|
||||
"what open-source systems/products/workflows attempt it",
|
||||
"what architectural patterns already work",
|
||||
"what standards/conventions exist",
|
||||
"what can be directly ADOPTED",
|
||||
"what can be ADAPTED",
|
||||
"what can be COMBINED",
|
||||
"what should be REJECTED",
|
||||
"where is genuine INNOVATION required"
|
||||
],
|
||||
"refs": [
|
||||
"https://backstage.io/docs/features/software-catalog/system-model/",
|
||||
"https://kythe.io/docs/schema/",
|
||||
"https://github.com/scip-code/scip",
|
||||
"https://nx.dev/concepts/affected",
|
||||
"https://c4model.com/",
|
||||
"https://bazel.build/query/language",
|
||||
"https://github.com/dependency-cruiser/dependency-cruiser"
|
||||
]
|
||||
},
|
||||
"question": "What existing systems/modeling approaches should Scaffolding's structural Scaffold adopt, adapt, combine, or reject?",
|
||||
"risks": [],
|
||||
"schema_version": 1,
|
||||
"uncertainty": []
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"target": "component:structural_model",
|
||||
"affected_domains": [
|
||||
"domain:structural-awareness"
|
||||
],
|
||||
"affected_subsystems": [],
|
||||
"consumers_connectors": [],
|
||||
"candidate_files": [],
|
||||
"contract_scope_additions": [],
|
||||
"state_implications": [
|
||||
"state_store:structural-store"
|
||||
],
|
||||
"environment_runtime_implications": [],
|
||||
"migration_implications": [],
|
||||
"risks_uncertainties": [
|
||||
{
|
||||
"kind": "dynamic-consumer",
|
||||
"note": "static model cannot see dynamic dispatch/string-keyed lookup/late-binding consumers \u2014 reconcilers must verify at runtime"
|
||||
}
|
||||
],
|
||||
"verification_plan": [
|
||||
"test_suite:scaffolding-tests"
|
||||
],
|
||||
"rollback_implications": [
|
||||
"revert contract scope commits; deployment rollbacks via last-good symlink (canonical deploy machinery)"
|
||||
],
|
||||
"basis_summary": {
|
||||
"depth": "INWARD",
|
||||
"why": "depth supplied by caller (recorded, caller accountable)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"contract_id": "18q-graph-projection",
|
||||
"construction_base": "b7865a76bbdb",
|
||||
"runtime_generation": "dep-29570f4",
|
||||
"candidate_generation": "",
|
||||
"promotion_target": "gitea/master",
|
||||
"rollback_generation": "",
|
||||
"stages_completed": [
|
||||
"base_verified",
|
||||
"worktree_created",
|
||||
"construction",
|
||||
"verified",
|
||||
"committed"
|
||||
],
|
||||
"lifecycle": [
|
||||
"base_verified",
|
||||
"worktree_created",
|
||||
"construction",
|
||||
"verified",
|
||||
"committed",
|
||||
"pushed",
|
||||
"ci_qualified",
|
||||
"released",
|
||||
"deployment_confirmed",
|
||||
"evidence_archived",
|
||||
"cleaned"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"stage": "worktree_created",
|
||||
"evidence": "harness worktree (existing dev worktree, rule P)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# T18 §18R/§18S — adversarial matrix + V1 gap reassessment (2026-09-15)
|
||||
|
||||
## §18R — 20-case matrix → evidence
|
||||
|
||||
| # | Case | Evidence |
|
||||
|---|------|----------|
|
||||
| 1 | stale Scaffold | test_18r_stale_scaffold_reconciles_via_drift (MATERIAL + reconciliation) |
|
||||
| 2 | changed file location | 18H moved-file detection (src/widgets/widget.py) |
|
||||
| 3 | unknown dynamic consumer | test_18r_dynamic_consumers_stay_uncertain (explicit uncertainty) |
|
||||
| 4 | broken connector | structural_model: consumers/depends_on status field; stale edges excluded from queries |
|
||||
| 5 | research unavailable | build_basis: unqualified basis → is_qualified False; contracts cannot attach qualified=false silently |
|
||||
| 6 | research contradicts architecture assumption | dispositions carry REJECT with why (18D basis: REJECT graph-DB adoption) |
|
||||
| 7 | target expands outside scope | rule J: reconcile/expand explicitly — impact closure marks non_goals + uncertainty; contract scope amendments user-gated (scaffold.py) |
|
||||
| 8 | user rejects material schema upgrade | test_18r_user_rejects_material_upgrade (ConsentRequired) |
|
||||
| 9 | minor compatible migration | classify_migration MINOR = automatic (test_18i) |
|
||||
| 10 | dirty worktree | test_18r_dirty_worktree_is_visible_not_fatal (detect + record, policy per-contract) |
|
||||
| 11 | interrupted construction | T14E SIGKILL live + 17E restart held-outs |
|
||||
| 12 | provider unavailable | 16G-C verbatim status codes; PROVIDER_UNAVAILABLE blocks branch |
|
||||
| 13 | CI failure | T16 16D: deploy refused truthfully on red |
|
||||
| 14 | deployment failure | runs 7953/7958 failed fast+truthful (T16 17C finding) |
|
||||
| 15 | old runtime + candidate schema incompatibility | consent machinery: material migrations gated per project (18I) |
|
||||
| 16 | secret present in env/config | test_18r_secret_in_env_does_not_block_construction (rule O: continues) |
|
||||
| 17 | secret in generated output | test_18r_secret_value_redacted_in_store — **FOUND REAL BUG: *_token keys were not redacted; fixed (substring match now)** |
|
||||
| 18 | forge unavailable | test_18r_forge_unavailable_raises_explicitly (ForgeUnavailable, never truth change) |
|
||||
| 19 | structural graph projection stale | test_18q_projection_is_read_only + projection regenerated on model change (18P step 9) |
|
||||
| 20 | /build route backend bypass | 18M: /build start mints canonical contract (test_native_default + gate) |
|
||||
| 21 | contextual ambiguous authorization | 17Q-6 + 18N multiple-proposal terse selection |
|
||||
|
||||
**Matrix yield: 2 real defects found and fixed** (secret redaction gap;
|
||||
drift source-field mismatch) — the matrix is load-bearing, not decorative.
|
||||
|
||||
## §18S — V1 gap re-score (post-T18)
|
||||
|
||||
| North Star capability | T18-start | T18-end |
|
||||
|---|---|---|
|
||||
| Research-led construction (Build Basis) | ABSENT | **TEST_PROVEN + one QUALIFIED real basis (18D)** |
|
||||
| Contextual research plan (correction) | — | **TEST_PROVEN (disjoint-plan held-outs)** |
|
||||
| OUTWARD precedent-first | PARTIAL | **TEST_PROVEN (contract questions + real run)** |
|
||||
| First-class structural Scaffold | PARTIAL | **TEST_PROVEN + LIVE on Scaffolding itself (18O)** |
|
||||
| Structure→scope→governance | PARTIAL | **TEST_PROVEN (closure→contract 18P)** |
|
||||
| Visible impact preview | ABSENT | **TEST_PROVEN (18G + real 18P preview persisted)** |
|
||||
| Structural drift | PARTIAL | **TEST_PROVEN (18H; 2 bugs found by matrix)** |
|
||||
| Schema versioning + consent | PARTIAL | **TEST_PROVEN (18I; consent structurally enforced)** |
|
||||
| Environment/runtime model | PARTIAL | **TEST_PROVEN (env/deployment entities + distinction)** |
|
||||
| Workspace lifecycle | PARTIAL | **TEST_PROVEN (18K order-enforced; adapter retained)** |
|
||||
| Forge boundary | ABSENT | **TEST_PROVEN (18L; Gitea proven by every push this session)** |
|
||||
| Full route convergence | PARTIAL | **TEST_PROVEN (18M: /build start mints contracts)** |
|
||||
| Contextual generalisation | TEST_PROVEN | **TEST_PROVEN+ (18N: 4 semantic gaps fixed)** |
|
||||
| Self-scaffold | PARTIAL | **LIVE (18O consistency-enforced)** |
|
||||
| Upgrade-via-Scaffold | ABSENT | **LIVE_PROVEN (18P: 11 scaffold-driven steps)** |
|
||||
| Graph projection | ABSENT | **TEST_PROVEN (18Q, 3 formats)** |
|
||||
| Memory/Substrate | ABSENT | ABSENT (BY DESIGN — rule L) |
|
||||
| Kernel behavior changes | — | **ZERO frozen-kernel exceptions (rule B record: none needed)** |
|
||||
|
||||
## VERDICT: RETROFIT_READY (for the Rune-wide retrofit tranche)
|
||||
|
||||
Why: every V1 foundation capability is implemented, test-proven, and —
|
||||
where the tranche could — live-proven on Scaffolding itself (self-
|
||||
scaffold, scaffold-driven upgrade, qualified research basis from real
|
||||
prior-art work). The adversarial matrix made the system better (2 real
|
||||
fixes). What remains is exactly the mandate's next phase: applying this
|
||||
discipline across Rune's remaining subsystems (Spine modernisation,
|
||||
research pipeline modeling, release/deploy structural entities) and
|
||||
real external projects — plus natural-production confirmation of the
|
||||
event-replay path (mechanism proven; natural timing pending, as in T17).
|
||||
Not full V1: generalisation beyond one project is still ahead.
|
||||
@@ -0,0 +1,84 @@
|
||||
digraph structural {
|
||||
rankdir=LR;
|
||||
node [shape=box, style="rounded,filled", fillcolor="#eef"];
|
||||
"component:build_mode_adapter" [label="build_mode_adapter (component)"];
|
||||
"component:build_basis" [label="build_basis (component)"];
|
||||
"component:construction_activation" [label="construction_activation (component)"];
|
||||
"component:construction_decision" [label="construction_decision (component)"];
|
||||
"component:construction_intent" [label="construction_intent (component)"];
|
||||
"component:construction_interface" [label="construction_interface (component)"];
|
||||
"component:construction_mode" [label="construction_mode (component)"];
|
||||
"component:drift_resume" [label="drift_resume (component)"];
|
||||
"component:event_wait" [label="event_wait (component)"];
|
||||
"component:failure_recovery" [label="failure_recovery (component)"];
|
||||
"component:forge" [label="forge (component)"];
|
||||
"component:graph_projection" [label="graph_projection (component)"];
|
||||
"component:impact_preview" [label="impact_preview (component)"];
|
||||
"component:programme_continuation" [label="programme_continuation (component)"];
|
||||
"component:scaffold" [label="scaffold (component)"];
|
||||
"component:stage_guards" [label="stage_guards (component)"];
|
||||
"component:structural_migration" [label="structural_migration (component)"];
|
||||
"component:structural_model" [label="structural_model (component)"];
|
||||
"deployment_surface:rune-code-current" [label="rune-code-current (deployment_surface)"];
|
||||
"domain:construction-governance" [label="construction-governance (domain)"];
|
||||
"domain:execution-integration" [label="execution-integration (domain)"];
|
||||
"domain:structural-awareness" [label="structural-awareness (domain)"];
|
||||
"environment:production" [label="production (environment)"];
|
||||
"interface:builder-boundary" [label="builder-boundary (interface)"];
|
||||
"state_store:scaffold-store" [label="scaffold-store (state_store)"];
|
||||
"state_store:structural-store" [label="structural-store (state_store)"];
|
||||
"subsystem:execution" [label="execution (subsystem)"];
|
||||
"subsystem:interpretation" [label="interpretation (subsystem)"];
|
||||
"subsystem:kernel" [label="kernel (subsystem)"];
|
||||
"subsystem:structural" [label="structural (subsystem)"];
|
||||
"test_suite:scaffolding-tests" [label="scaffolding-tests (test_suite)"];
|
||||
"project:scaffolding" [label="scaffolding (project)"];
|
||||
"component:build_mode_adapter" -> "subsystem:kernel" [label="legacy_of"];
|
||||
"component:build_basis" -> "state_store:structural-store" [label="reads"];
|
||||
"component:build_basis" -> "subsystem:structural" [label="belongs_to"];
|
||||
"component:build_mode_adapter" -> "subsystem:execution" [label="belongs_to"];
|
||||
"component:construction_activation" -> "subsystem:interpretation" [label="belongs_to"];
|
||||
"component:construction_decision" -> "subsystem:kernel" [label="belongs_to"];
|
||||
"component:construction_intent" -> "component:construction_mode" [label="consumes"];
|
||||
"component:construction_intent" -> "subsystem:interpretation" [label="belongs_to"];
|
||||
"component:construction_interface" -> "interface:builder-boundary" [label="provides"];
|
||||
"component:construction_interface" -> "subsystem:execution" [label="belongs_to"];
|
||||
"component:construction_mode" -> "subsystem:kernel" [label="belongs_to"];
|
||||
"component:drift_resume" -> "subsystem:structural" [label="belongs_to"];
|
||||
"component:event_wait" -> "component:programme_continuation" [label="connects_to"];
|
||||
"component:event_wait" -> "subsystem:execution" [label="belongs_to"];
|
||||
"component:failure_recovery" -> "subsystem:kernel" [label="belongs_to"];
|
||||
"component:forge" -> "subsystem:execution" [label="belongs_to"];
|
||||
"component:forge" -> "test_suite:scaffolding-tests" [label="verified_by"];
|
||||
"component:graph_projection" -> "state_store:structural-store" [label="reads"];
|
||||
"component:graph_projection" -> "subsystem:structural" [label="belongs_to"];
|
||||
"component:graph_projection" -> "test_suite:scaffolding-tests" [label="verified_by"];
|
||||
"component:impact_preview" -> "subsystem:structural" [label="belongs_to"];
|
||||
"component:programme_continuation" -> "subsystem:execution" [label="belongs_to"];
|
||||
"component:programme_continuation" -> "test_suite:scaffolding-tests" [label="verified_by"];
|
||||
"component:scaffold" -> "component:construction_mode" [label="depends_on"];
|
||||
"component:scaffold" -> "deployment_surface:rune-code-current" [label="deployed_by"];
|
||||
"component:scaffold" -> "state_store:scaffold-store" [label="owns"];
|
||||
"component:scaffold" -> "state_store:structural-store" [label="reads"];
|
||||
"component:scaffold" -> "subsystem:kernel" [label="belongs_to"];
|
||||
"component:scaffold" -> "test_suite:scaffolding-tests" [label="verified_by"];
|
||||
"component:stage_guards" -> "subsystem:kernel" [label="belongs_to"];
|
||||
"component:structural_migration" -> "subsystem:structural" [label="belongs_to"];
|
||||
"component:structural_model" -> "state_store:structural-store" [label="owns"];
|
||||
"component:structural_model" -> "subsystem:structural" [label="belongs_to"];
|
||||
"component:structural_model" -> "test_suite:scaffolding-tests" [label="verified_by"];
|
||||
"deployment_surface:rune-code-current" -> "environment:production" [label="connects_to"];
|
||||
"deployment_surface:rune-code-current" -> "project:scaffolding" [label="belongs_to"];
|
||||
"domain:construction-governance" -> "project:scaffolding" [label="belongs_to"];
|
||||
"domain:execution-integration" -> "project:scaffolding" [label="belongs_to"];
|
||||
"domain:structural-awareness" -> "project:scaffolding" [label="belongs_to"];
|
||||
"environment:production" -> "project:scaffolding" [label="belongs_to"];
|
||||
"interface:builder-boundary" -> "subsystem:execution" [label="belongs_to"];
|
||||
"state_store:scaffold-store" -> "project:scaffolding" [label="belongs_to"];
|
||||
"state_store:structural-store" -> "project:scaffolding" [label="belongs_to"];
|
||||
"subsystem:execution" -> "domain:execution-integration" [label="belongs_to"];
|
||||
"subsystem:interpretation" -> "domain:construction-governance" [label="belongs_to"];
|
||||
"subsystem:kernel" -> "domain:construction-governance" [label="belongs_to"];
|
||||
"subsystem:structural" -> "domain:structural-awareness" [label="belongs_to"];
|
||||
"test_suite:scaffolding-tests" -> "project:scaffolding" [label="belongs_to"];
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
{
|
||||
"edge_count": 49,
|
||||
"edges": [
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:build_mode_adapter",
|
||||
"status": "active",
|
||||
"to": "subsystem:kernel",
|
||||
"type": "legacy_of"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:build_basis",
|
||||
"status": "active",
|
||||
"to": "state_store:structural-store",
|
||||
"type": "reads"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:build_basis",
|
||||
"status": "active",
|
||||
"to": "subsystem:structural",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:build_mode_adapter",
|
||||
"status": "active",
|
||||
"to": "subsystem:execution",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_activation",
|
||||
"status": "active",
|
||||
"to": "subsystem:interpretation",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_decision",
|
||||
"status": "active",
|
||||
"to": "subsystem:kernel",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_intent",
|
||||
"status": "active",
|
||||
"to": "component:construction_mode",
|
||||
"type": "consumes"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_intent",
|
||||
"status": "active",
|
||||
"to": "subsystem:interpretation",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_interface",
|
||||
"status": "active",
|
||||
"to": "interface:builder-boundary",
|
||||
"type": "provides"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_interface",
|
||||
"status": "active",
|
||||
"to": "subsystem:execution",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_mode",
|
||||
"status": "active",
|
||||
"to": "subsystem:kernel",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:drift_resume",
|
||||
"status": "active",
|
||||
"to": "subsystem:structural",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:event_wait",
|
||||
"status": "active",
|
||||
"to": "component:programme_continuation",
|
||||
"type": "connects_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:event_wait",
|
||||
"status": "active",
|
||||
"to": "subsystem:execution",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:failure_recovery",
|
||||
"status": "active",
|
||||
"to": "subsystem:kernel",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:forge",
|
||||
"status": "active",
|
||||
"to": "subsystem:execution",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:forge",
|
||||
"status": "active",
|
||||
"to": "test_suite:scaffolding-tests",
|
||||
"type": "verified_by"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:graph_projection",
|
||||
"status": "active",
|
||||
"to": "state_store:structural-store",
|
||||
"type": "reads"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:graph_projection",
|
||||
"status": "active",
|
||||
"to": "subsystem:structural",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:graph_projection",
|
||||
"status": "active",
|
||||
"to": "test_suite:scaffolding-tests",
|
||||
"type": "verified_by"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:impact_preview",
|
||||
"status": "active",
|
||||
"to": "subsystem:structural",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:programme_continuation",
|
||||
"status": "active",
|
||||
"to": "subsystem:execution",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:programme_continuation",
|
||||
"status": "active",
|
||||
"to": "test_suite:scaffolding-tests",
|
||||
"type": "verified_by"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:scaffold",
|
||||
"status": "active",
|
||||
"to": "component:construction_mode",
|
||||
"type": "depends_on"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:scaffold",
|
||||
"status": "active",
|
||||
"to": "deployment_surface:rune-code-current",
|
||||
"type": "deployed_by"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:scaffold",
|
||||
"status": "active",
|
||||
"to": "state_store:scaffold-store",
|
||||
"type": "owns"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:scaffold",
|
||||
"status": "active",
|
||||
"to": "state_store:structural-store",
|
||||
"type": "reads"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:scaffold",
|
||||
"status": "active",
|
||||
"to": "subsystem:kernel",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:scaffold",
|
||||
"status": "active",
|
||||
"to": "test_suite:scaffolding-tests",
|
||||
"type": "verified_by"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:stage_guards",
|
||||
"status": "active",
|
||||
"to": "subsystem:kernel",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:structural_migration",
|
||||
"status": "active",
|
||||
"to": "subsystem:structural",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:structural_model",
|
||||
"status": "active",
|
||||
"to": "state_store:structural-store",
|
||||
"type": "owns"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:structural_model",
|
||||
"status": "active",
|
||||
"to": "subsystem:structural",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:structural_model",
|
||||
"status": "active",
|
||||
"to": "test_suite:scaffolding-tests",
|
||||
"type": "verified_by"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "deployment_surface:rune-code-current",
|
||||
"status": "active",
|
||||
"to": "environment:production",
|
||||
"type": "connects_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "deployment_surface:rune-code-current",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "domain:construction-governance",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "domain:execution-integration",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "domain:structural-awareness",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "environment:production",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "interface:builder-boundary",
|
||||
"status": "active",
|
||||
"to": "subsystem:execution",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "state_store:scaffold-store",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "state_store:structural-store",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "subsystem:execution",
|
||||
"status": "active",
|
||||
"to": "domain:execution-integration",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "subsystem:interpretation",
|
||||
"status": "active",
|
||||
"to": "domain:construction-governance",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "subsystem:kernel",
|
||||
"status": "active",
|
||||
"to": "domain:construction-governance",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "subsystem:structural",
|
||||
"status": "active",
|
||||
"to": "domain:structural-awareness",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "test_suite:scaffolding-tests",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "project:scaffolding",
|
||||
"status": "root",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
}
|
||||
],
|
||||
"generated_at": "2026-09-15T22:10:51Z",
|
||||
"node_count": 32,
|
||||
"nodes": [
|
||||
{
|
||||
"ext": {
|
||||
"bounded_adapter": true,
|
||||
"source": "src/rune/build_mode.py"
|
||||
},
|
||||
"id": "component:build_mode_adapter",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "build_mode_adapter"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/build_basis.py"
|
||||
},
|
||||
"id": "component:build_basis",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "build_basis"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/construction_activation.py"
|
||||
},
|
||||
"id": "component:construction_activation",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "construction_activation"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/construction_decision.py"
|
||||
},
|
||||
"id": "component:construction_decision",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "construction_decision"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/construction_intent.py"
|
||||
},
|
||||
"id": "component:construction_intent",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "construction_intent"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/construction_interface.py"
|
||||
},
|
||||
"id": "component:construction_interface",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "construction_interface"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/construction_mode.py"
|
||||
},
|
||||
"id": "component:construction_mode",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "construction_mode"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/drift_resume.py"
|
||||
},
|
||||
"id": "component:drift_resume",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "drift_resume"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/event_wait.py"
|
||||
},
|
||||
"id": "component:event_wait",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "event_wait"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/failure_recovery.py"
|
||||
},
|
||||
"id": "component:failure_recovery",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "failure_recovery"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/forge.py"
|
||||
},
|
||||
"id": "component:forge",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "forge"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/graph_projection.py"
|
||||
},
|
||||
"id": "component:graph_projection",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "graph_projection"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/impact_preview.py"
|
||||
},
|
||||
"id": "component:impact_preview",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "impact_preview"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/programme_continuation.py"
|
||||
},
|
||||
"id": "component:programme_continuation",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "programme_continuation"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/scaffold.py"
|
||||
},
|
||||
"id": "component:scaffold",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "scaffold"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/stage_guards.py"
|
||||
},
|
||||
"id": "component:stage_guards",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "stage_guards"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/structural_migration.py"
|
||||
},
|
||||
"id": "component:structural_migration",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "structural_migration"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/structural_model.py"
|
||||
},
|
||||
"id": "component:structural_model",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "structural_model"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "deployment_surface:rune-code-current",
|
||||
"kind": "deployment_surface",
|
||||
"maturity": "DECLARED",
|
||||
"name": "rune-code-current"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "domain:construction-governance",
|
||||
"kind": "domain",
|
||||
"maturity": "DECLARED",
|
||||
"name": "construction-governance"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "domain:execution-integration",
|
||||
"kind": "domain",
|
||||
"maturity": "DECLARED",
|
||||
"name": "execution-integration"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "domain:structural-awareness",
|
||||
"kind": "domain",
|
||||
"maturity": "DECLARED",
|
||||
"name": "structural-awareness"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "environment:production",
|
||||
"kind": "environment",
|
||||
"maturity": "DECLARED",
|
||||
"name": "production"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "interface:builder-boundary",
|
||||
"kind": "interface",
|
||||
"maturity": "DECLARED",
|
||||
"name": "builder-boundary"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "state_store:scaffold-store",
|
||||
"kind": "state_store",
|
||||
"maturity": "DECLARED",
|
||||
"name": "scaffold-store"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "state_store:structural-store",
|
||||
"kind": "state_store",
|
||||
"maturity": "DECLARED",
|
||||
"name": "structural-store"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "subsystem:execution",
|
||||
"kind": "subsystem",
|
||||
"maturity": "DECLARED",
|
||||
"name": "execution"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "subsystem:interpretation",
|
||||
"kind": "subsystem",
|
||||
"maturity": "DECLARED",
|
||||
"name": "interpretation"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "subsystem:kernel",
|
||||
"kind": "subsystem",
|
||||
"maturity": "DECLARED",
|
||||
"name": "kernel"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "subsystem:structural",
|
||||
"kind": "subsystem",
|
||||
"maturity": "DECLARED",
|
||||
"name": "structural"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "test_suite:scaffolding-tests",
|
||||
"kind": "test_suite",
|
||||
"maturity": "DECLARED",
|
||||
"name": "scaffolding-tests"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "project:scaffolding",
|
||||
"kind": "project",
|
||||
"maturity": "DECLARED",
|
||||
"name": null
|
||||
}
|
||||
],
|
||||
"project": "scaffolding",
|
||||
"schema_version": 1
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
graph LR
|
||||
n1["build_mode_adapter (component)"]
|
||||
n0["build_basis (component)"]
|
||||
n2["construction_activation (component)"]
|
||||
n3["construction_decision (component)"]
|
||||
n4["construction_intent (component)"]
|
||||
n5["construction_interface (component)"]
|
||||
n6["construction_mode (component)"]
|
||||
n7["drift_resume (component)"]
|
||||
n8["event_wait (component)"]
|
||||
n9["failure_recovery (component)"]
|
||||
n10["forge (component)"]
|
||||
n11["graph_projection (component)"]
|
||||
n12["impact_preview (component)"]
|
||||
n13["programme_continuation (component)"]
|
||||
n14["scaffold (component)"]
|
||||
n15["stage_guards (component)"]
|
||||
n16["structural_migration (component)"]
|
||||
n17["structural_model (component)"]
|
||||
n18["rune-code-current (deployment_surface)"]
|
||||
n19["construction-governance (domain)"]
|
||||
n20["execution-integration (domain)"]
|
||||
n21["structural-awareness (domain)"]
|
||||
n22["production (environment)"]
|
||||
n23["builder-boundary (interface)"]
|
||||
n25["scaffold-store (state_store)"]
|
||||
n26["structural-store (state_store)"]
|
||||
n27["execution (subsystem)"]
|
||||
n28["interpretation (subsystem)"]
|
||||
n29["kernel (subsystem)"]
|
||||
n30["structural (subsystem)"]
|
||||
n31["scaffolding-tests (test_suite)"]
|
||||
n24["scaffolding (project)"]
|
||||
n1 -->|legacy_of| n29
|
||||
n0 -->|reads| n26
|
||||
n0 -->|belongs_to| n30
|
||||
n1 -->|belongs_to| n27
|
||||
n2 -->|belongs_to| n28
|
||||
n3 -->|belongs_to| n29
|
||||
n4 -->|consumes| n6
|
||||
n4 -->|belongs_to| n28
|
||||
n5 -->|provides| n23
|
||||
n5 -->|belongs_to| n27
|
||||
n6 -->|belongs_to| n29
|
||||
n7 -->|belongs_to| n30
|
||||
n8 -->|connects_to| n13
|
||||
n8 -->|belongs_to| n27
|
||||
n9 -->|belongs_to| n29
|
||||
n10 -->|belongs_to| n27
|
||||
n10 -->|verified_by| n31
|
||||
n11 -->|reads| n26
|
||||
n11 -->|belongs_to| n30
|
||||
n11 -->|verified_by| n31
|
||||
n12 -->|belongs_to| n30
|
||||
n13 -->|belongs_to| n27
|
||||
n13 -->|verified_by| n31
|
||||
n14 -->|depends_on| n6
|
||||
n14 -->|deployed_by| n18
|
||||
n14 -->|owns| n25
|
||||
n14 -->|reads| n26
|
||||
n14 -->|belongs_to| n29
|
||||
n14 -->|verified_by| n31
|
||||
n15 -->|belongs_to| n29
|
||||
n16 -->|belongs_to| n30
|
||||
n17 -->|owns| n26
|
||||
n17 -->|belongs_to| n30
|
||||
n17 -->|verified_by| n31
|
||||
n18 -->|connects_to| n22
|
||||
n18 -->|belongs_to| n24
|
||||
n19 -->|belongs_to| n24
|
||||
n20 -->|belongs_to| n24
|
||||
n21 -->|belongs_to| n24
|
||||
n22 -->|belongs_to| n24
|
||||
n23 -->|belongs_to| n27
|
||||
n25 -->|belongs_to| n24
|
||||
n26 -->|belongs_to| n24
|
||||
n27 -->|belongs_to| n20
|
||||
n28 -->|belongs_to| n19
|
||||
n29 -->|belongs_to| n19
|
||||
n30 -->|belongs_to| n21
|
||||
n31 -->|belongs_to| n24
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"alternatives_considered": [],
|
||||
"created_at": "2026-09-15T23:39:58Z",
|
||||
"deeper": {
|
||||
"findings": [],
|
||||
"refs": [],
|
||||
"used": false
|
||||
},
|
||||
"depth": {
|
||||
"decided_at": "2026-09-15T23:39:58Z",
|
||||
"depth": "FULL",
|
||||
"mode": "",
|
||||
"route": "RESEARCHED_CHANGE",
|
||||
"why": "route RESEARCHED_CHANGE default"
|
||||
},
|
||||
"dispositions": [
|
||||
{
|
||||
"disposition": "ADOPT",
|
||||
"item": "Structural closure (Bazel rdeps/Nx affected) as the SOUND invalidation backbone",
|
||||
"ref": "bazel.build",
|
||||
"why": "static closure over-approximates (20-50% precision) \u2014 drives RETEST MARKING, never verdicts"
|
||||
},
|
||||
{
|
||||
"disposition": "ADOPT",
|
||||
"item": "Generation-pinned confirmation state (merge-queue semantics: confirmation belongs to a generation, not forever)",
|
||||
"ref": "github merge-queue",
|
||||
"why": "history append-only, never auto-reverts, always names the verifying generation"
|
||||
},
|
||||
{
|
||||
"disposition": "INNOVATE",
|
||||
"item": "Feedback correction loop: predicted-vs-observed impact persisted per change; misses WIDEN future closures",
|
||||
"ref": "netflix/facebook pattern",
|
||||
"why": "no mainstream tool logs prediction accuracy per edge; substitutes for coverage instrumentation we don't control"
|
||||
},
|
||||
{
|
||||
"disposition": "COMBINE",
|
||||
"item": "Context packets: derived/budgeted code layer + thin curated behaviour-ledger layer; NOT hand-maintained prose (rots), NOT embedding-first (similarity\u2260causality)",
|
||||
"ref": "aider.chat/docs/repomap",
|
||||
"why": "aider repomap proves derived context; behaviour lines add intent/risk repomaps lack"
|
||||
},
|
||||
{
|
||||
"disposition": "ADAPT",
|
||||
"item": "Dynamic /testing queue with reopen semantics; process dependents after providers, topological order",
|
||||
"ref": "gitlab flaky insights",
|
||||
"why": "merge-queue eviction + Nx last-successful-commit anchoring avoid re-chasing fixed sets"
|
||||
},
|
||||
{
|
||||
"disposition": "REJECT",
|
||||
"item": "Mutation testing as continuous gate",
|
||||
"ref": "mutmut",
|
||||
"why": "too expensive per-change; retained as audit-time oracle idea only"
|
||||
},
|
||||
{
|
||||
"disposition": "REJECT",
|
||||
"item": "Embedding indexes as PRIMARY dependency mechanism",
|
||||
"ref": "cursor/cody",
|
||||
"why": "similarity is not causality; keep structural closure primary"
|
||||
}
|
||||
],
|
||||
"implications": {
|
||||
"architecture": [],
|
||||
"migration": [],
|
||||
"structural": [],
|
||||
"verification": []
|
||||
},
|
||||
"inward": {
|
||||
"findings": [],
|
||||
"refs": []
|
||||
},
|
||||
"issue_ref": "research/t19-quicksand",
|
||||
"kind": "build_basis",
|
||||
"outward": {
|
||||
"findings": [],
|
||||
"prior_art_questions_answered": [
|
||||
"what already exists addressing this intent",
|
||||
"what open-source systems/products/workflows attempt it",
|
||||
"what architectural patterns already work",
|
||||
"what standards/conventions exist",
|
||||
"what can be directly ADOPTED",
|
||||
"what can be ADAPTED",
|
||||
"what can be COMBINED",
|
||||
"what should be REJECTED",
|
||||
"where is genuine INNOVATION required"
|
||||
],
|
||||
"refs": [
|
||||
"https://bazel.build/query/language",
|
||||
"https://nx.dev/concepts/affected",
|
||||
"https://netflixtechblog.com/practical-intelligent-test-selection-95423d2516d6",
|
||||
"https://docs.pact.io",
|
||||
"https://aider.chat/docs/repomap.html",
|
||||
"https://docs.github.com/en/pull-requests/merging-a-pull-request-with-a-merge-queue"
|
||||
]
|
||||
},
|
||||
"question": "How should Scaffolding safely iterate a large evolving codebase without quicksand, while minimizing LLM context and rediscovery cost?",
|
||||
"risks": [],
|
||||
"schema_version": 1,
|
||||
"uncertainty": []
|
||||
}
|
||||
+13068
-1
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"programme_id": "t17-17e-proof-2",
|
||||
"objective": "17E live proof: 4 stages, async event wait, failure, auto-continuation",
|
||||
"stages": [
|
||||
{
|
||||
"stage_id": "17e2-stage-1-governance",
|
||||
"objective": "governance-only stage",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"stage_id": "17e2-stage-2-async",
|
||||
"objective": "async work under event bridge",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"stage_id": "17e2-stage-3-fails",
|
||||
"objective": "deliberately failing stage",
|
||||
"state": "FAILED",
|
||||
"depends_on": [],
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"stage_id": "17e2-stage-4-final",
|
||||
"objective": "final stage",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 3
|
||||
}
|
||||
],
|
||||
"fallbacks": [],
|
||||
"saved_at": "2026-09-15T17:51:22Z"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"programme_id": "t17-17e-proof-3",
|
||||
"objective": "17E live proof: async event wait, failure isolation, auto-continuation, honest terminal verdict",
|
||||
"stages": [
|
||||
{
|
||||
"stage_id": "17e3-stage-1-governance",
|
||||
"objective": "governance-only stage",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"stage_id": "17e3-stage-2-async",
|
||||
"objective": "async work under event bridge",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"stage_id": "17e3-stage-3-fails",
|
||||
"objective": "deliberately failing stage",
|
||||
"state": "FAILED",
|
||||
"depends_on": [],
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"stage_id": "17e3-stage-4-final",
|
||||
"objective": "final stage",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 3
|
||||
}
|
||||
],
|
||||
"fallbacks": [],
|
||||
"saved_at": "2026-09-15T17:52:31Z"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"programme_id": "t17-17e-proof",
|
||||
"objective": "prove canonical programme continuation: 4 stages incl. async event wait, failure, auto-continuation",
|
||||
"stages": [
|
||||
{
|
||||
"stage_id": "17e-stage-1-governance",
|
||||
"objective": "governance-only stage",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"stage_id": "17e-stage-2-async",
|
||||
"objective": "async work under event bridge",
|
||||
"state": "PENDING",
|
||||
"depends_on": [],
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"stage_id": "17e-stage-3-fails",
|
||||
"objective": "deliberately failing stage",
|
||||
"state": "PENDING",
|
||||
"depends_on": [],
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"stage_id": "17e-stage-4-final",
|
||||
"objective": "final stage",
|
||||
"state": "PENDING",
|
||||
"depends_on": [],
|
||||
"priority": 3
|
||||
}
|
||||
],
|
||||
"fallbacks": [],
|
||||
"saved_at": "2026-09-15T17:50:29Z"
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+2002
-159
File diff suppressed because it is too large.
Load diff
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user