Compare commits
191
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa800acbad | ||
|
|
30be2c96b8 | ||
|
|
ca00be8259 | ||
|
|
50a63e587f | ||
|
|
a54ab559cb | ||
|
|
599c3ab1c2 | ||
|
|
483bfd7944 | ||
|
|
1650b470c6 | ||
|
|
853cf3e0ae | ||
|
|
875bb792f7 | ||
|
|
f06ddb3a20 | ||
|
|
2ebd9da139 | ||
|
|
65001196d8 | ||
|
|
76bb2b97be | ||
|
|
e765f654f9 | ||
|
|
f7141200bd | ||
|
|
53b4830b08 | ||
|
|
fa1b7c4be1 | ||
|
|
246f26663d | ||
|
|
5718a3ea85 | ||
|
|
02802f321f | ||
|
|
4ce121a493 | ||
|
|
8a8d3534c6 | ||
|
|
d0ec37925f | ||
|
|
f9936fa244 | ||
|
|
04a277f22c | ||
|
|
b2777d666f | ||
|
|
0982f7317f | ||
|
|
9583678997 | ||
|
|
5db8b202d9 | ||
|
|
78c0d97a43 | ||
|
|
551530c78c | ||
|
|
b669e4d4ed | ||
|
|
2b6a5a6b95 | ||
|
|
e2e7b74a8e | ||
|
|
db4ce7f6a6 | ||
|
|
505209b839 | ||
|
|
f7256aa29e | ||
|
|
26e3765319 | ||
|
|
e6451a5afc | ||
|
|
7008743620 | ||
|
|
f545f1ed8d | ||
|
|
73803e6a0d | ||
|
|
ed1696606e | ||
|
|
4944f9e41c | ||
|
|
6722066c79 | ||
|
|
df8262b9ed | ||
|
|
756ebe6201 | ||
|
|
f64ee95aad | ||
|
|
c3a0a48ca5 | ||
|
|
b4ed258121 | ||
|
|
bacea2d472 | ||
|
|
9acfc0953e | ||
|
|
818aa49043 | ||
|
|
212239ca7b | ||
|
|
ae6d4a0d98 | ||
|
|
00013f8c0b | ||
|
|
ba2966e631 | ||
|
|
20fcee3f40 | ||
|
|
493d9c2800 | ||
|
|
6b5d8ad023 | ||
|
|
f3ef4672f3 | ||
|
|
be6bc17537 | ||
|
|
40b3731928 | ||
|
|
caa77f8a35 | ||
|
|
4d215ea666 | ||
|
|
fda9b6dda0 | ||
|
|
116d1ffbeb | ||
|
|
040988cf00 | ||
|
|
ffed816465 | ||
|
|
72ab7dccf7 | ||
|
|
5d20dc7f51 | ||
|
|
9e903dacaf | ||
|
|
ed1b41abd5 | ||
|
|
ff1b950f11 | ||
|
|
3afc9ff872 | ||
|
|
28e9c8c1c1 | ||
|
|
8249854881 | ||
|
|
4aeef171d2 | ||
|
|
2ccac89ec4 | ||
|
|
232986f0c3 | ||
|
|
725c779ac7 | ||
|
|
edd6d06300 | ||
|
|
98cb3a98a5 | ||
|
|
7881e78e7f | ||
|
|
57a689d22f | ||
|
|
45869a77cd | ||
|
|
224fbfa4e9 | ||
|
|
417e824a51 | ||
|
|
ca2807e9b5 | ||
|
|
25f5219982 | ||
|
|
2d09bee39e | ||
|
|
77bc2161a6 | ||
|
|
74e18402f0 | ||
|
|
39ec00b361 | ||
|
|
95946b6efd | ||
|
|
72d3c909c8 | ||
|
|
ab2dfe7b09 | ||
|
|
1326713234 | ||
|
|
867321ed05 | ||
|
|
a44ced163e | ||
|
|
f40f7e7233 | ||
|
|
8bef4682f5 | ||
|
|
f939d9db5a | ||
|
|
dcead3fecc | ||
|
|
87654cbfd1 | ||
|
|
6dddb0a5ee | ||
|
|
f90725dba8 | ||
|
|
24b7dda24f | ||
|
|
c7c6b2d5ac | ||
|
|
6a761f2d87 | ||
|
|
9ef267f2e3 | ||
|
|
bd80fb0e3b | ||
|
|
759a836726 | ||
|
|
71f051d928 | ||
|
|
2b1d0ac245 | ||
|
|
fbb5018413 | ||
|
|
c8f0a8e353 | ||
|
|
fb7fb34ca5 | ||
|
|
abe24da81d | ||
|
|
15c54539b5 | ||
|
|
ad35a2f7f5 | ||
|
|
e2f536f71a | ||
|
|
5b2712f5e6 | ||
|
|
124b77e95b | ||
|
|
1b7995e093 | ||
|
|
fcd835883e | ||
|
|
37ce68eff8 | ||
|
|
2fa0a5f144 | ||
|
|
c3ef29c59d | ||
|
|
f079aa2cc9 | ||
|
|
c03c953a45 | ||
|
|
90ae1c2612 | ||
|
|
cb1b8303fe | ||
|
|
3c54782634 | ||
|
|
478ce45755 | ||
|
|
a96ed90b90 | ||
|
|
713f85a0e3 | ||
|
|
504935828b | ||
|
|
8e78bc936f | ||
|
|
a34762d512 | ||
|
|
772402ed39 | ||
|
|
80a1fd0a2d | ||
|
|
3fb28e2461 | ||
|
|
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 |
No files matched your search
@@ -30,10 +30,26 @@ jobs:
|
||||
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 (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)
|
||||
|
||||
@@ -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.63**
|
||||
**Current release: v0.85.1**
|
||||
|
||||
**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.**
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Hooks } from "@mimo-ai/plugin"
|
||||
|
||||
// event-wait-guard — RETIRED (T21.5 §6/§8, 2026-09-17).
|
||||
//
|
||||
// HISTORY: this hook was the T21 §21B-3 runtime interception layer that
|
||||
// blocked model-generated sleep→poll continuation (T20 regression), and
|
||||
// later carried the T21.1 §21.1C cwd guard. Both were DOWNSTREAM patches:
|
||||
// they corrected the model's output instead of fixing the generator.
|
||||
//
|
||||
// ROOT-CAUSE FIX (T21.5 §2-§5): the generator was ci_event_wait's
|
||||
// wait loop returning nonterminal control to the model — caused by a
|
||||
// single hard-coded master-deploy authority that was blind to branch/PR
|
||||
// operations. T21.5 fixed this upstream:
|
||||
// - OPERATION_SPECS: per-operation-type completion authority
|
||||
// (branch_ci/pull_request_checks -> own Actions runs;
|
||||
// master_ci/deploy/release -> deploy qualification);
|
||||
// - SINGLE LOGICAL AWAIT: all nonterminal conditions re-arm inside
|
||||
// ci_event_wait.run(); the model sees only terminal results;
|
||||
// - structured PendingOperation envelopes (op_type, authority,
|
||||
// internal_rearm_count) propagate to the controller.
|
||||
//
|
||||
// With the generator fixed, runtime interception is retired per the
|
||||
// tranche rule FIXED UPSTREAM BEHAVIOUR > DOWNSTREAM GUARD CORRECTION.
|
||||
// Regression prevention lives in the test suite:
|
||||
// tests/test_single_logical_await.py (no model re-arm contract)
|
||||
// tests/test_ci_event_wait.py (watchdog=recovery, event=wake)
|
||||
// tests/test_wait_guard_corpus.py (canonical wait-pattern corpus)
|
||||
// NOT as a permanent runtime block. The hook file stays installed as a
|
||||
// documented no-op so canonical-source/install parity (test_hook_drift,
|
||||
// test_hook_parity) continues to prove the installed runtime matches
|
||||
// this repository's canonical state.
|
||||
|
||||
export const EventWaitGuard: Hooks = {
|
||||
"tool.execute.before": async (_input, _output) => {
|
||||
// RETIRED — no interception. See history above.
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
export const CwdGuard: Hooks = {
|
||||
"tool.execute.before": async (_input, _output) => {
|
||||
// RETIRED (T21.5 §8) — the harness/construction execution boundary
|
||||
// now resolves project context upstream (execution-context binding).
|
||||
// No runtime refusal. See tests/test_auto_execution_context.py.
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
export const CombinedGuards: Hooks = {
|
||||
"tool.execute.before": async (input, output) => {
|
||||
// Kept as the default export shape; both sub-guards are retired.
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
export default CombinedGuards
|
||||
@@ -0,0 +1,54 @@
|
||||
import { tool } from "@mimo-ai/plugin"
|
||||
import { execSync } from "child_process"
|
||||
|
||||
// await_operation — T21.3 §21.3C: FIRST-CLASS async wait tool.
|
||||
// Uses the plugin tool() API with args (not parameters).
|
||||
|
||||
export const AwaitOperation = tool({
|
||||
description: [
|
||||
"Wait for an asynchronous external operation (CI, deploy, release) to complete.",
|
||||
"Registers on the event bridge, yields until the completion event arrives, then returns the verified result.",
|
||||
"USE THIS instead of `sleep N; curl status` — it is the canonical event-driven wait.",
|
||||
].join(" "),
|
||||
args: {
|
||||
sha: tool.schema.string().describe(
|
||||
"The exact commit SHA the operation is running against"),
|
||||
deadline_secs: tool.schema.number().optional().describe(
|
||||
"Maximum wait in seconds (default 900)"),
|
||||
},
|
||||
async execute(args, ctx) {
|
||||
const repoRoot = "/mnt/workdrive/issue161-harness"
|
||||
const deadline = args.deadline_secs || 900
|
||||
const cmd = [
|
||||
"python3", `${repoRoot}/scripts/ci_event_wait.py`,
|
||||
"--sha", args.sha,
|
||||
"--deadline", String(deadline),
|
||||
].join(" ")
|
||||
try {
|
||||
const result = execSync(cmd, {
|
||||
cwd: repoRoot,
|
||||
timeout: (deadline + 60) * 1000,
|
||||
encoding: "utf-8" as const,
|
||||
env: {
|
||||
...process.env,
|
||||
DEPLOY_SHA: args.sha,
|
||||
RUNE_WR_ROOT: "/mnt/workdrive/wr-bridge",
|
||||
RUNE_WR_CODE: "/home/nick/.local/share/mimo-wait-resume",
|
||||
},
|
||||
})
|
||||
const lines = result.trim().split("\n")
|
||||
const jsonLine = lines.find((l: string) => l.startsWith("{"))
|
||||
if (jsonLine) return jsonLine
|
||||
return "await_operation: no JSON output"
|
||||
} catch (e: any) {
|
||||
const stdout = e.stdout || ""
|
||||
const lines = stdout.trim().split("\n")
|
||||
const jsonLine = lines.find((l: string) => l.startsWith("{"))
|
||||
if (jsonLine) return jsonLine
|
||||
return JSON.stringify({
|
||||
ok: false,
|
||||
error: `await_operation failed: ${(e.stderr || e.message || "?").slice(0, 200)}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
+22
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
|
||||
"_generated_by_spine": true,
|
||||
"generated_at": "2026-09-14T06:08:20Z",
|
||||
"generated_at": "2026-09-16T18:23:38Z",
|
||||
"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",
|
||||
@@ -43,7 +45,8 @@
|
||||
"drift_core",
|
||||
"drift_ledger",
|
||||
"drift_observability",
|
||||
"failure_auto_filer"
|
||||
"failure_auto_filer",
|
||||
"programme_gates"
|
||||
],
|
||||
"depends_on": [],
|
||||
"mirrors": [],
|
||||
@@ -215,30 +218,45 @@
|
||||
"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",
|
||||
"testing_mode",
|
||||
"toolchain",
|
||||
"tui_backlog",
|
||||
"wait_guard",
|
||||
"wait_guard_corpus",
|
||||
"witness"
|
||||
],
|
||||
"depends_on": [
|
||||
@@ -251,4 +269,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,53 @@
|
||||
# Rune Semantic Release Policy (T20 §20B)
|
||||
|
||||
Pre-1.0 convention. Scaffolding "V1" does NOT mean Rune 1.0.
|
||||
|
||||
## Bump kinds
|
||||
|
||||
**PATCH** (0.66.80 → 0.66.81):
|
||||
- bug fix, correction, implementation/internal maintenance
|
||||
- no meaningful new user/system capability
|
||||
|
||||
**MINOR** (0.66.80 → 0.67.0):
|
||||
- meaningful new Rune capability
|
||||
- new subsystem functionality
|
||||
- materially expanded system behaviour
|
||||
- significant new supported workflow
|
||||
|
||||
**MAJOR**: reserved for Rune 1.0+ compatibility/public-contract semantics.
|
||||
|
||||
## How the machinery expresses this (no manual version edits)
|
||||
|
||||
The existing machinery already maps commit shape → bump kind:
|
||||
- `semver_bump.next_version` (PR titles): feat→minor, else patch
|
||||
- `version_convergence.highest_warranted_bump` (push ranges):
|
||||
feat/breaking→minor, fix/perf→patch, else None
|
||||
|
||||
**Tranche-level rule (20B)**: a feature tranche lands its feature
|
||||
commits (`feat(...)`), and the convergence machinery bumps MINOR at
|
||||
the tranche's release boundary. Patch-only follow-ups (fix/perf)
|
||||
warrant only PATCH — the machinery cannot accidentally re-minor
|
||||
because `highest_warranted_bump` takes the max over the *post-
|
||||
boundary* range, and a fix-only range yields "patch".
|
||||
|
||||
## Why T20 → 0.67.0
|
||||
|
||||
T20 is a feature-level tranche (Rune-wide retrofit: structural
|
||||
taxonomy, capability/behaviour ledgers, rune graph, /testing backlog,
|
||||
Spine modernization, semantic release). The release is classified
|
||||
MINOR under this policy, earned through the governed convergence
|
||||
machinery — never a manual version edit.
|
||||
|
||||
## T20 0.67.0 release record
|
||||
T20 classified MINOR under this policy: Rune-wide retrofit (structural
|
||||
taxonomy, capability/behaviour ledgers, rune graph, /testing backlog,
|
||||
Spine modernization, semantic release) = new subsystem functionality +
|
||||
materially expanded system behaviour. Earned through the governed
|
||||
convergence machinery (feat-bearing range → minor), never a manual edit.
|
||||
|
||||
## Root cause record (T20, 2026-09-16)
|
||||
semver_bump re-derived the bump kind from the SYNTHESIZED title text —
|
||||
"minor: converged release" matched nothing → patch. Feature tranches
|
||||
T18/T19/T20 downgraded to 0.66.77-83. Fixed: MINOR_PREFIX_RE carries the
|
||||
convergence kind through; first push after the fix with a feat-bearing
|
||||
range lands 0.67.0 through the normal machinery.
|
||||
@@ -0,0 +1,79 @@
|
||||
# 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 |
|
||||
|
||||
## T19 additions (post-T18 classifications)
|
||||
|
||||
| Capability | Status | Evidence |
|
||||
|---|---|---|
|
||||
| Project/Scaffold Registry | TEST_PROVEN | registry tests; boundaries proven (no truth in index, no Spine leak) |
|
||||
| /build governance fail-closed | LIVE_PROVEN | 19A: T18 degradation paths removed; failure-path held-outs |
|
||||
| Capability Ledgers | TEST_PROVEN | lifecycle+history+packet tests |
|
||||
| Surgical context packets | LIVE_PROVEN | 19R: packet bound exactly 1 file for a real change |
|
||||
| Behaviour/Test Specs + hardening lifecycle | TEST_PROVEN | ledger suites incl. generation history |
|
||||
| Selective invalidation | TEST_PROVEN | 19S-2/5/9/10 held-outs |
|
||||
| Predictive vs descriptive impact | TEST_PROVEN | delta persistence tests |
|
||||
| Post-change obligations | TEST_PROVEN | machine-derived from structural truth |
|
||||
| /testing backend + aggregate | TEST_PROVEN + LIVE (19R) | testing_mode suites; command wired in real TUI |
|
||||
| Observation→closure (user-confirm gate) | TEST_PROVEN | closure requires retest AND user confirm |
|
||||
| Forge failure never rewrites truth | TEST_PROVEN | 19S-14 held-out |
|
||||
| Self-hosted T19 change | LIVE_PROVEN | contract 19r-testing-command, 11-step scaffold-driven flow |
|
||||
|
||||
## 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,153 @@
|
||||
# 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.
|
||||
|
||||
## T19 layer model (the general engineering system)
|
||||
|
||||
> "The Scaffold should let the LLM operate with surgical precision
|
||||
> locally, while preserving global awareness of what else must be
|
||||
> checked or changed."
|
||||
>
|
||||
> "Lower active cognitive load without losing the whole idea."
|
||||
|
||||
- **STRUCTURAL MODEL** tells Rune what the project contains / how it connects.
|
||||
- **CAPABILITY LEDGER** provides compact local re-entry context (stable IDs
|
||||
across moves; history preserved through supersede/retire).
|
||||
- **BEHAVIOUR VALIDATION LEDGER** tells Rune what intended behaviour is
|
||||
actually confirmed — generation-aware, selective invalidation, automated
|
||||
green never implies USER_CONFIRMED.
|
||||
- **PROJECT REGISTRY** tells Rune which Scaffolds/projects exist
|
||||
(discovery/index only — not structural truth, not Spine).
|
||||
- **ITERATION ENGINE** (predictive→descriptive impact, impact-delta
|
||||
records, machine-derived obligations) makes quicksand protection
|
||||
STANDARD behaviour of every construction route.
|
||||
- **/TESTING** is a dependency-aware hardening programme: dynamic queue,
|
||||
fix-for-B-may-reopen-A, closure requires retest AND user confirmation.
|
||||
- **FORGE** provides workflow/history projection (failure never rewrites
|
||||
canonical validation truth).
|
||||
- **FUTURE SUBSTRATE** will coordinate sequencing/current focus/pause-
|
||||
resume but does NOT own structural or validation truth.
|
||||
|
||||
## 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`.
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"BLE001": 354,
|
||||
"S110": 80
|
||||
"BLE001": 347,
|
||||
"S110": 75
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.66.63"
|
||||
version = "0.85.1"
|
||||
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": 454,
|
||||
"informationCount": 0
|
||||
}
|
||||
+33
-28
@@ -1,29 +1,34 @@
|
||||
{
|
||||
"typeCheckingMode": "standard",
|
||||
"pythonVersion": "3.14",
|
||||
"reportMissingImports": "warning",
|
||||
"reportMissingModuleSource": "none",
|
||||
"reportMissingTypeStubs": "none",
|
||||
"reportUnknownArgumentType": "none",
|
||||
"reportUnknownParameterType": "none",
|
||||
"reportUnknownVariableType": "none",
|
||||
"reportUnknownMemberType": "none",
|
||||
"reportMissingTypeArgument": "none",
|
||||
"reportUntypedFunctionDecorator": "none",
|
||||
"reportUntypedClassDecorator": "none",
|
||||
"reportUntypedBaseClass": "none",
|
||||
"reportPrivateUsage": "none",
|
||||
"reportOptionalMemberAccess": "warning",
|
||||
"reportUnnecessaryIsInstance": "warning",
|
||||
"reportUnnecessaryComparison": "warning",
|
||||
"reportUnnecessaryTypeIgnoreComment": "warning",
|
||||
"reportPossiblyUnboundVariable": "error",
|
||||
"reportUndefinedVariable": "error",
|
||||
"reportAttributeAccessIssue": "warning",
|
||||
"include": ["src/rune"],
|
||||
"exclude": [
|
||||
"src/rune/__pycache__",
|
||||
"**/node_modules",
|
||||
"**/__pycache__"
|
||||
]
|
||||
}
|
||||
"typeCheckingMode": "standard",
|
||||
"pythonVersion": "3.14",
|
||||
"reportMissingImports": "warning",
|
||||
"reportMissingModuleSource": "none",
|
||||
"reportMissingTypeStubs": "none",
|
||||
"reportUnknownArgumentType": "none",
|
||||
"reportUnknownParameterType": "none",
|
||||
"reportUnknownVariableType": "none",
|
||||
"reportUnknownMemberType": "none",
|
||||
"reportMissingTypeArgument": "none",
|
||||
"reportUntypedFunctionDecorator": "none",
|
||||
"reportUntypedClassDecorator": "none",
|
||||
"reportUntypedBaseClass": "none",
|
||||
"reportPrivateUsage": "none",
|
||||
"reportOptionalMemberAccess": "warning",
|
||||
"reportUnnecessaryIsInstance": "warning",
|
||||
"reportUnnecessaryComparison": "warning",
|
||||
"reportUnnecessaryTypeIgnoreComment": "warning",
|
||||
"reportPossiblyUnboundVariable": "error",
|
||||
"reportUndefinedVariable": "error",
|
||||
"reportAttributeAccessIssue": "warning",
|
||||
"include": [
|
||||
"src/rune"
|
||||
],
|
||||
"exclude": [
|
||||
"src/rune/__pycache__",
|
||||
"**/node_modules",
|
||||
"**/__pycache__"
|
||||
],
|
||||
"extraPaths": [
|
||||
"scripts"
|
||||
]
|
||||
}
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml -o requirements-ci.txt --extra dev --extra memory --python-version 3.11
|
||||
# uv pip compile pyproject.toml -o /tmp/regen2.txt --extra dev --extra memory --python-version 3.11
|
||||
aiohappyeyeballs==2.7.1
|
||||
# via aiohttp
|
||||
aiohttp==3.14.3
|
||||
@@ -34,7 +34,7 @@ httpcore==1.0.9
|
||||
# via httpx
|
||||
httpx==0.28.1
|
||||
# via rune (pyproject.toml)
|
||||
idna==3.19
|
||||
idna==3.20
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
@@ -64,13 +64,13 @@ nodeenv==1.10.0
|
||||
# via pyright
|
||||
packaging==26.3
|
||||
# via pytest
|
||||
platformdirs==4.11.8
|
||||
platformdirs==4.11.10
|
||||
# via textual
|
||||
pluggy==1.6.0
|
||||
# via
|
||||
# diff-cover
|
||||
# pytest
|
||||
propcache==0.5.2
|
||||
propcache==0.5.4
|
||||
# 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,375 @@
|
||||
"""await_intent.py — ASYNC CORRECTIVE PATCH: intent normalization at the
|
||||
execution boundary.
|
||||
|
||||
PRINCIPLE: MODEL OUTPUT = INTENT. RUNE CONTROLLER = EXECUTION SEMANTICS.
|
||||
|
||||
The model may express continuation intent in any quality — `await_operation
|
||||
(deadline_secs=1200)`, `await-operation [op_type=master_ci, sha=X]`,
|
||||
"sleep 300 then check CI", "poll until done". When a matching
|
||||
PendingOperation exists (or the deterministic resolver can derive the
|
||||
operation truth from git/Gitea state), the controller executes the
|
||||
CANONICAL continuation and discards model-supplied scheduling policy.
|
||||
|
||||
Authority provenance for timing:
|
||||
USER_EXPLICIT / EXTERNAL_CONTRACT / TEST_HARNESS /
|
||||
GOVERNED_OPERATION_POLICY -> authoritative, retained
|
||||
MODEL_INVENTED -> non-authoritative, ignored
|
||||
|
||||
This module owns NO storage: it reads the existing W/R wait store
|
||||
(rune.event_wait) and git/Gitea state. The canonical continuation remains
|
||||
scripts/ci_event_wait.py run() (the T21.5 single-logical-await).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(_SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
_REPO_SRC = str(_SCRIPT_DIR.parent / "src")
|
||||
if _REPO_SRC not in sys.path:
|
||||
sys.path.insert(0, _REPO_SRC)
|
||||
|
||||
WR_BRIDGE = Path(os.environ.get("RUNE_WR_ROOT", "/mnt/workdrive/wr-bridge"))
|
||||
REPO = Path("/mnt/workdrive/issue161-harness")
|
||||
GIT_TIMEOUT = 10
|
||||
|
||||
# Default policy deadline per operation type — GOVERNED_OPERATION_POLICY.
|
||||
# The model's numbers never become policy; these are the controller's.
|
||||
POLICY_DEADLINES = {
|
||||
"branch_ci": 1800,
|
||||
"pull_request_checks": 1800,
|
||||
"master_ci": 1800,
|
||||
"deploy": 900,
|
||||
"release": 900,
|
||||
}
|
||||
|
||||
VALID_OP_TYPES = ("branch_ci", "pull_request_checks", "master_ci",
|
||||
"deploy", "release")
|
||||
|
||||
|
||||
# ── timing provenance ───────────────────────────────────────────────────────
|
||||
|
||||
def classify_timing_provenance(model_deadline: Any = None,
|
||||
user_deadline: Any = None,
|
||||
contract_deadline: Any = None) -> tuple[int, str]:
|
||||
"""Resolve the effective deadline + its provenance.
|
||||
|
||||
Authority order: USER_EXPLICIT > EXTERNAL_CONTRACT > TEST_HARNESS >
|
||||
GOVERNED_OPERATION_POLICY. MODEL_INVENTED values are recorded but
|
||||
NEVER used as policy.
|
||||
"""
|
||||
if model_deadline is not None:
|
||||
try:
|
||||
model_deadline = int(model_deadline)
|
||||
except (TypeError, ValueError):
|
||||
model_deadline = None
|
||||
if user_deadline is not None:
|
||||
return int(user_deadline), "USER_EXPLICIT"
|
||||
if contract_deadline is not None:
|
||||
return int(contract_deadline), "EXTERNAL_CONTRACT"
|
||||
return (model_deadline or 0), "MODEL_INVENTED" # caller discards this
|
||||
|
||||
|
||||
# ── PendingOperation lookup over the existing W/R wait store ────────────────
|
||||
|
||||
def _load_wait_records() -> list[dict]:
|
||||
"""All durable wait records from the bridge store (read-only)."""
|
||||
out = []
|
||||
waits = WR_BRIDGE / "waits"
|
||||
if not waits.is_dir():
|
||||
return out
|
||||
for f in sorted(waits.glob("*.json")):
|
||||
try:
|
||||
rec = json.loads(f.read_text())
|
||||
rec["_file"] = f.name
|
||||
out.append(rec)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def _rec_operation_id(rec: dict) -> str:
|
||||
return str((rec.get("ext") or {}).get("operation_id")
|
||||
or rec.get("operation_id") or "")
|
||||
|
||||
|
||||
def _rec_sha(rec: dict) -> str:
|
||||
return str(rec.get("sha") or "")
|
||||
|
||||
|
||||
def pending_operations(sha: str = "", op_type: str = "") -> list[dict]:
|
||||
"""Pending (non-terminal) operations, optionally filtered."""
|
||||
terminal = {"COMPLETED", "FAILED", "CLOSED", "CANCELLED", "EXPIRED"}
|
||||
out = []
|
||||
for rec in _load_wait_records():
|
||||
status = str(rec.get("status", "")).upper()
|
||||
if status in terminal:
|
||||
continue
|
||||
if sha and _rec_sha(rec) != sha:
|
||||
continue
|
||||
out.append(rec)
|
||||
return out
|
||||
|
||||
|
||||
def find_op_type_for_sha(sha: str) -> str | None:
|
||||
"""The op_type a prior canonical wait already recorded for this SHA
|
||||
(objective text embeds the type: 'event-driven {op_type} qualification')."""
|
||||
for rec in _load_wait_records():
|
||||
if _rec_sha(rec) != sha:
|
||||
continue
|
||||
obj = str(rec.get("continuation", {}).get("objective", ""))
|
||||
m = re.search(r"event-driven (\w+) qualification", obj)
|
||||
if m and m.group(1) in VALID_OP_TYPES:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
|
||||
# ── deterministic op_type derivation from repo/Gitea truth ─────────────────
|
||||
|
||||
def _git(*args: str) -> str:
|
||||
try:
|
||||
r = subprocess.run(["git", "-C", str(REPO), *args],
|
||||
capture_output=True, text=True, timeout=GIT_TIMEOUT)
|
||||
return r.stdout.strip()
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return ""
|
||||
|
||||
|
||||
def sha_branch(sha: str) -> str:
|
||||
"""Branch(es) containing this commit in the local repo."""
|
||||
return _git("branch", "--all", "--contains", sha)
|
||||
|
||||
|
||||
def sha_on_master(sha: str) -> bool:
|
||||
out = _git("branch", "--contains", sha)
|
||||
return any(l.strip().endswith("master") or l.strip() == "master"
|
||||
for l in out.splitlines())
|
||||
|
||||
|
||||
def open_pr_for_sha(sha: str) -> int | None:
|
||||
"""Is there an OPEN PR whose head SHA is this one? (Gitea API, read-only.)"""
|
||||
try:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import gitea_auth # noqa: E402
|
||||
st, prs = gitea_auth.api("/pulls?state=open&limit=50")
|
||||
if st != 200:
|
||||
return None
|
||||
for p in prs:
|
||||
hs = str(p.get("head") or {}).get("sha") if isinstance(
|
||||
p.get("head"), dict) else None
|
||||
if hs and str(hs).startswith(sha[:12]):
|
||||
return int(p.get("number"))
|
||||
except Exception: # noqa: BLE001 — resolver must never crash the tool
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def derive_op_type(sha: str) -> tuple[str, str]:
|
||||
"""Deterministically derive the canonical op_type for a SHA.
|
||||
|
||||
Returns (op_type, evidence). Order (A-patch §2 amendment):
|
||||
1. POST-PATCH normalized record (ext.intent_normalized=true) — true
|
||||
stored operation truth, wins outright;
|
||||
2. open PR for the sha -> pull_request_checks;
|
||||
3. sha ancestor of master -> master_ci;
|
||||
4. sha on a feature branch -> branch_ci;
|
||||
5. legacy (pre-patch) records only CONFIRM, never override — they
|
||||
were created from model-supplied defaults and cannot be trusted.
|
||||
Never trusts model input.
|
||||
"""
|
||||
for rec in _load_wait_records():
|
||||
if _rec_sha(rec) != sha:
|
||||
continue
|
||||
if (rec.get("ext") or {}).get("intent_normalized"):
|
||||
obj = str(rec.get("continuation", {}).get("objective", ""))
|
||||
m = re.search(r"event-driven (\w+) qualification", obj)
|
||||
if m and m.group(1) in VALID_OP_TYPES:
|
||||
return m.group(1), "normalized_record:" + m.group(1)
|
||||
pr = open_pr_for_sha(sha)
|
||||
if pr:
|
||||
return "pull_request_checks", f"open_pr:#{pr}"
|
||||
if sha_on_master(sha):
|
||||
return "master_ci", "commit_ancestor_of_master"
|
||||
return "branch_ci", "commit_on_feature_branch"
|
||||
|
||||
|
||||
# ── intent normalization ────────────────────────────────────────────────────
|
||||
|
||||
def normalize_action(action: dict, *,
|
||||
model_deadline: Any = None,
|
||||
user_deadline: Any = None,
|
||||
contract_deadline: Any = None) -> dict:
|
||||
"""Normalize a model continuation action into the canonical continuation.
|
||||
|
||||
action: {kind: await_operation|sleep_poll|check_status|unknown,
|
||||
sha?: str, operation_id?: str, op_type?: str, raw?: str}
|
||||
Returns:
|
||||
{normalized: bool, action: "ContinuePendingOperation", operation_id,
|
||||
op_type, authority, deadline_secs, deadline_provenance,
|
||||
model_deadline_ignored, model_op_type_ignored, evidence}
|
||||
or {normalized: False, reason: ...} / {normalized: "AMBIGUOUS", candidates}
|
||||
"""
|
||||
sha = str(action.get("sha") or "")
|
||||
op_id = str(action.get("operation_id") or "")
|
||||
model_op_type = action.get("op_type")
|
||||
model_deadline_ignored = model_deadline is not None
|
||||
model_op_type_ignored = model_op_type is not None
|
||||
|
||||
# 1. explicit operation_id wins
|
||||
if op_id:
|
||||
from rune import event_wait as ew # noqa: E402
|
||||
rec = ew.operation_wait(op_id)
|
||||
if rec is None:
|
||||
return {"normalized": False,
|
||||
"reason": f"operation_id {op_id} has no wait record"}
|
||||
return _canonical_from_record(rec, model_deadline_ignored,
|
||||
model_op_type_ignored, "explicit_id",
|
||||
user_deadline, contract_deadline)
|
||||
|
||||
# 2. resolve by sha / active operation
|
||||
candidates = pending_operations(sha=sha)
|
||||
if len(candidates) > 1 and not sha:
|
||||
return {"normalized": "AMBIGUOUS",
|
||||
"candidates": [_rec_operation_id(c) for c in candidates],
|
||||
"reason": "multiple pending operations, no target"}
|
||||
if len(candidates) == 1:
|
||||
return _canonical_from_record(candidates[0], model_deadline_ignored,
|
||||
model_op_type_ignored, "pending_match",
|
||||
user_deadline, contract_deadline)
|
||||
|
||||
# 3. no stored record: derive truth deterministically
|
||||
if not sha:
|
||||
return {"normalized": False,
|
||||
"reason": "no pending operation and no sha to resolve"}
|
||||
op_type, evidence = derive_op_type(sha)
|
||||
return {
|
||||
"normalized": True,
|
||||
"action": "ContinuePendingOperation",
|
||||
"operation_id": f"ci-{sha}",
|
||||
"op_type": op_type,
|
||||
"authority": "ci_runs" if op_type in ("branch_ci",
|
||||
"pull_request_checks")
|
||||
else "deploy_qualify",
|
||||
"deadline_secs": POLICY_DEADLINES[op_type],
|
||||
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
|
||||
"model_deadline_ignored": model_deadline_ignored,
|
||||
"model_op_type_ignored": model_op_type_ignored,
|
||||
"evidence": evidence,
|
||||
}
|
||||
|
||||
|
||||
def _canonical_from_record(rec: dict, model_deadline_ignored: bool,
|
||||
model_op_type_ignored: bool, how: str,
|
||||
user_deadline: Any, contract_deadline: Any) -> dict:
|
||||
op_id = _rec_operation_id(rec)
|
||||
obj = str(rec.get("continuation", {}).get("objective", ""))
|
||||
m = re.search(r"event-driven (\w+) qualification", obj)
|
||||
recorded = m.group(1) if (m and m.group(1) in VALID_OP_TYPES) else None
|
||||
rec_sha = _rec_sha(rec)
|
||||
if (rec.get("ext") or {}).get("intent_normalized") or how == "explicit_id":
|
||||
# normalized record or explicit id: stored truth wins
|
||||
op_type = recorded or "master_ci"
|
||||
else:
|
||||
# legacy record: git/Gitea derivation outranks the model-era default
|
||||
op_type, _ev = derive_op_type(rec_sha) if rec_sha else \
|
||||
(recorded or "master_ci", "no_sha")
|
||||
deadline, prov = classify_timing_provenance(
|
||||
user_deadline=user_deadline, contract_deadline=contract_deadline)
|
||||
if prov == "MODEL_INVENTED" or not deadline:
|
||||
deadline = POLICY_DEADLINES.get(op_type, 1800)
|
||||
prov = "GOVERNED_OPERATION_POLICY"
|
||||
return {
|
||||
"normalized": True,
|
||||
"action": "ContinuePendingOperation",
|
||||
"operation_id": op_id,
|
||||
"op_type": op_type,
|
||||
"authority": "ci_runs" if op_type in ("branch_ci",
|
||||
"pull_request_checks")
|
||||
else "deploy_qualify",
|
||||
"deadline_secs": deadline,
|
||||
"deadline_provenance": prov,
|
||||
"model_deadline_ignored": model_deadline_ignored,
|
||||
"model_op_type_ignored": model_op_type_ignored,
|
||||
"matched_by": how,
|
||||
}
|
||||
|
||||
|
||||
def normalize_bash(command: str, *, sha_hint: str = "") -> dict | None:
|
||||
"""Recognize sleep/check/poll intent aimed at a known pending operation.
|
||||
|
||||
Returns a normalization record if the command's semantic intent is
|
||||
continuation of a known external operation; None if the command is
|
||||
legitimate local timing (short sync, user delay, backoff) with no
|
||||
pending operation to continue. NEVER returns an executed sleep.
|
||||
"""
|
||||
if not command:
|
||||
return None
|
||||
cmd = command.strip()
|
||||
# long sleep + (status-check language or an explicit sha hint)
|
||||
sleep_m = re.search(r"\bsleep\s+(\d{2,5})\b", cmd)
|
||||
if not sleep_m:
|
||||
return None
|
||||
secs = int(sleep_m.group(1))
|
||||
check_intent = bool(re.search(
|
||||
r"\b(ci|deploy|release|status|check|poll|pipeline|run)\b", cmd,
|
||||
re.IGNORECASE))
|
||||
sha = sha_hint or ""
|
||||
m = re.search(r"\b([0-9a-f]{40}|[0-9a-f]{12})\b", cmd)
|
||||
if m:
|
||||
sha = m.group(1)
|
||||
if not check_intent and not sha:
|
||||
return None # plain sleep, no continuation intent
|
||||
cands = pending_operations(sha=sha) if sha else pending_operations()
|
||||
if not cands and not sha:
|
||||
return None
|
||||
if len(cands) > 1:
|
||||
return {"normalized": "AMBIGUOUS",
|
||||
"candidates": [_rec_operation_id(c) for c in cands],
|
||||
"raw_command_kind": "sleep_poll"}
|
||||
rec = cands[0] if cands else None
|
||||
if rec is not None:
|
||||
norm = _canonical_from_record(rec, True, False, "sleep_poll_intent",
|
||||
None, None)
|
||||
else:
|
||||
op_type, evidence = derive_op_type(sha)
|
||||
norm = {
|
||||
"normalized": True, "action": "ContinuePendingOperation",
|
||||
"operation_id": f"ci-{sha}", "op_type": op_type,
|
||||
"authority": "ci_runs" if op_type in ("branch_ci",
|
||||
"pull_request_checks")
|
||||
else "deploy_qualify",
|
||||
"deadline_secs": POLICY_DEADLINES[op_type],
|
||||
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
|
||||
"model_deadline_ignored": True, "model_op_type_ignored": False,
|
||||
"evidence": evidence,
|
||||
}
|
||||
norm["raw_command_kind"] = "sleep_poll"
|
||||
norm["raw_sleep_seconds_suppressed"] = secs
|
||||
return norm
|
||||
|
||||
|
||||
def emit_normalized_record(record: dict) -> None:
|
||||
"""Compact structured observability — one JSONL line per normalization.
|
||||
No payloads, no secrets."""
|
||||
try:
|
||||
log = _SCRIPT_DIR.parent / "store" / "await_intent_log.jsonl"
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
slim = {k: record.get(k) for k in (
|
||||
"normalized", "action", "operation_id", "op_type", "authority",
|
||||
"deadline_secs", "deadline_provenance", "model_deadline_ignored",
|
||||
"model_op_type_ignored", "matched_by", "evidence",
|
||||
"raw_command_kind", "raw_sleep_seconds_suppressed", "reason",
|
||||
"candidates") if k in record}
|
||||
slim["at"] = __import__("time").strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ", __import__("time").gmtime())
|
||||
with open(log, "a") as f:
|
||||
f.write(json.dumps(slim, sort_keys=True) + "\n")
|
||||
except OSError:
|
||||
pass
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""await_intent_cli.py — CLI surface for the intent normalization layer.
|
||||
|
||||
Used by the await-operation harness tool so the MODEL's scheduling hints
|
||||
never reach the wait engine. Prints one JSON line.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import await_intent # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
n = sub.add_parser("normalize", help="normalize a continuation intent")
|
||||
n.add_argument("--sha", default="")
|
||||
n.add_argument("--operation-id", default="")
|
||||
n.add_argument("--model-op-type", default=None)
|
||||
n.add_argument("--model-deadline", default=None)
|
||||
n.add_argument("--user-deadline", default=None,
|
||||
help="USER_EXPLICIT deadline (authoritative)")
|
||||
n.add_argument("--contract-deadline", default=None,
|
||||
help="EXTERNAL_CONTRACT deadline (authoritative)")
|
||||
rb = sub.add_parser("route-bash",
|
||||
help="route a Bash command expressing wait intent")
|
||||
rb.add_argument("--command", required=True)
|
||||
rb.add_argument("--sha-hint", default="")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.cmd == "route-bash":
|
||||
record = await_intent.normalize_bash(args.command,
|
||||
sha_hint=args.sha_hint)
|
||||
if record is None:
|
||||
print(json.dumps({"normalized": False,
|
||||
"reason": "no external continuation intent"}))
|
||||
return 0
|
||||
if record.get("normalized") is True:
|
||||
# the compiled wait needs the SHA: derive from operation_id
|
||||
oid = str(record.get("operation_id", ""))
|
||||
if oid.startswith("ci-"):
|
||||
record["sha_for_wait"] = oid[3:]
|
||||
await_intent.emit_normalized_record(record)
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
return 0
|
||||
|
||||
action = {"kind": "await_operation", "sha": args.sha,
|
||||
"operation_id": args.operation_id,
|
||||
"op_type": args.model_op_type}
|
||||
model_deadline = (int(args.model_deadline)
|
||||
if args.model_deadline else None)
|
||||
user_deadline = (int(args.user_deadline)
|
||||
if args.user_deadline else None)
|
||||
contract_deadline = (int(args.contract_deadline)
|
||||
if args.contract_deadline else None)
|
||||
record = await_intent.normalize_action(
|
||||
action, model_deadline=model_deadline,
|
||||
user_deadline=user_deadline, contract_deadline=contract_deadline)
|
||||
await_intent.emit_normalized_record(record)
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+332
-49
@@ -63,11 +63,209 @@ def authority_verdict() -> str:
|
||||
capture_output=True, text=True, timeout=120,
|
||||
env=dict(os.environ, PYTHONPATH=""),
|
||||
)
|
||||
# T21.3: GITEA_TOKEN from deploy.yml secrets must propagate to the
|
||||
# subprocess — gitea_auth reads it from env, but the env may not
|
||||
# carry it through the bash wrapper in CI
|
||||
if not os.environ.get("GITEA_TOKEN"):
|
||||
# fallback: gitea_auth's own credential ladder handles it
|
||||
pass
|
||||
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:
|
||||
# ── T21.5 §4: operation-specific completion authority ──────────────────────
|
||||
#
|
||||
# T21.4 root cause: the wait loop had ONE hard-coded authority
|
||||
# (deploy qualification = master-ref CI) while operations waited on
|
||||
# branch CI, PR checks, or merges. Wrong authority => nonterminal
|
||||
# returns => the MODEL had to re-arm (the generator of the sleeps).
|
||||
# The wait verifier MUST match the operation being waited for.
|
||||
|
||||
OPERATION_SPECS = {
|
||||
# op_type: {
|
||||
# workflow_path: Actions run path filter (None = any),
|
||||
# head_branches: head-branch filter (None = any),
|
||||
# authority: which canonical verifier decides completion,
|
||||
# event_types: expected wake event types,
|
||||
# terminal: terminal verdicts, failure: failure verdicts,
|
||||
# }
|
||||
"branch_ci": {
|
||||
"workflow_path": "ci.yml",
|
||||
"head_branches": None,
|
||||
"authority": "ci_runs", # any completed run for this SHA
|
||||
"event_types": ("ci_completed",),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
"pull_request_checks": {
|
||||
"workflow_path": None, # all PR-triggered runs for SHA
|
||||
"head_branches": None,
|
||||
"authority": "ci_runs",
|
||||
"event_types": ("ci_completed",),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
"master_ci": {
|
||||
"workflow_path": "ci.yml",
|
||||
"head_branches": ("master",),
|
||||
"authority": "deploy_qualify", # full two-leg qualification
|
||||
"event_types": ("ci_completed",),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
"deploy": {
|
||||
"workflow_path": "deploy.yml",
|
||||
"head_branches": ("master",),
|
||||
"authority": "deploy_qualify",
|
||||
"event_types": ("ci_completed", "deploy_completed"),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
"release": {
|
||||
"workflow_path": None,
|
||||
"head_branches": ("master",),
|
||||
"authority": "deploy_qualify",
|
||||
"event_types": ("ci_completed", "release_completed"),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_OP_TYPE = "master_ci"
|
||||
|
||||
|
||||
def normalize_verdict(verdict: str) -> str:
|
||||
"""T21.5 §18 audit fix: pending-class verdicts are ALL nonterminal.
|
||||
deploy_qualify emits "pending (probe failed)" for a transient probe
|
||||
failure — that is pending, never a terminal answer. The wait loop
|
||||
must not return control to the model over a string variant."""
|
||||
v = (verdict or "").strip().lower()
|
||||
if v.startswith("pending"):
|
||||
return "pending"
|
||||
return (verdict or "").strip()
|
||||
|
||||
|
||||
def op_spec(op_type: str) -> dict:
|
||||
spec = OPERATION_SPECS.get(op_type)
|
||||
if spec is None:
|
||||
raise ValueError(
|
||||
f"unknown op_type {op_type!r} — must be one of "
|
||||
f"{sorted(OPERATION_SPECS)} (the wait verifier must match "
|
||||
"the operation being waited for)")
|
||||
return spec
|
||||
|
||||
|
||||
def op_authority_verdict(op_type: str, sha: str) -> str:
|
||||
"""Canonical completion verdict FOR THIS OPERATION TYPE.
|
||||
|
||||
ci_runs — the op's own Actions runs for this exact SHA: every
|
||||
required workflow run completed; terminal iff every
|
||||
run of the op's workflow set concluded success
|
||||
(branch/PR ops are their own truth; deploy
|
||||
qualification is NOT their authority).
|
||||
deploy_qualify — the two-leg deploy qualification (master CI +
|
||||
required-checks) via deploy_qualify.py.
|
||||
"""
|
||||
spec = op_spec(op_type)
|
||||
authority = spec["authority"]
|
||||
if authority == "deploy_qualify":
|
||||
v = normalize_verdict(authority_verdict())
|
||||
if v != "pending":
|
||||
return v
|
||||
# E-PATCH resilience: Gitea's combined commit-status endpoint can
|
||||
# 404 for the newest commit (observed live on auto-version bump
|
||||
# commits — the status object materializes late/never). The runs
|
||||
# ARE the same evidence the two legs read; fall back to the
|
||||
# operation-scoped runs verdict so authority stays resolvable.
|
||||
if _status_endpoint_unreadable(sha):
|
||||
return _ci_runs_verdict(spec, sha)
|
||||
return v
|
||||
if authority == "ci_runs":
|
||||
return _ci_runs_verdict(spec, sha)
|
||||
raise ValueError(f"unknown authority {authority!r} for {op_type!r}")
|
||||
|
||||
|
||||
def _status_endpoint_unreadable(sha: str) -> bool:
|
||||
"""True when the combined commit-status endpoint 404s for the SHA
|
||||
(index lag / never-materialized status object) — the documented
|
||||
fallback trigger, verified against the API once per call."""
|
||||
try:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import gitea_auth # noqa: E402
|
||||
st, _body = gitea_auth.api(f"/commits/{sha}/status")
|
||||
return st == 404
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
|
||||
|
||||
def _ci_runs_verdict(spec: dict, sha: str) -> str:
|
||||
"""Operation-scoped Actions-run verdict (import the API client from
|
||||
deploy_qualify to keep ONE Gitea client)."""
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import deploy_qualify as dq # noqa: E402
|
||||
import gitea_auth # noqa: E402
|
||||
server = os.environ.get("GITEA_SERVER",
|
||||
getattr(gitea_auth, "DEFAULT_SERVER", "")).rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO",
|
||||
getattr(gitea_auth, "DEFAULT_REPO", ""))
|
||||
sha_env = os.environ.get("DEPLOY_SHA", "") or sha
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not (server and repo and sha_env and token):
|
||||
return "pending" # cannot verify -> not verified (fail closed)
|
||||
runs = dq._fetch_runs(server, repo, sha_env, token)
|
||||
if runs is None:
|
||||
return "pending" # probe failure -> keep waiting
|
||||
wfp = spec["workflow_path"]
|
||||
if wfp is not None:
|
||||
runs = [r for r in runs
|
||||
if str(r.get("path", "")).startswith(wfp + "@")]
|
||||
branches = spec["head_branches"]
|
||||
if branches is not None:
|
||||
runs = [r for r in runs
|
||||
if str(r.get("head_branch", "")) in branches]
|
||||
if not runs:
|
||||
return "pending" # no runs visible yet -> not terminal
|
||||
for r in runs:
|
||||
if r.get("status") != "completed":
|
||||
return "pending" # still running
|
||||
if r.get("conclusion") not in ("success", "skipped"):
|
||||
return "failure" # any non-success conclusion
|
||||
return "success"
|
||||
|
||||
|
||||
def op_ci_alive(op_type: str, sha: str) -> bool:
|
||||
"""Liveness probe FOR THIS OPERATION TYPE: any non-terminal run in
|
||||
the op's run set (failure DETECTOR, never the scheduler)."""
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import deploy_qualify as dq # noqa: E402
|
||||
import gitea_auth # noqa: E402
|
||||
spec = op_spec(op_type)
|
||||
server = os.environ.get("GITEA_SERVER",
|
||||
getattr(gitea_auth, "DEFAULT_SERVER", "")).rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO",
|
||||
getattr(gitea_auth, "DEFAULT_REPO", ""))
|
||||
sha_env = os.environ.get("DEPLOY_SHA", "") or sha
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not (server and repo and sha_env and token):
|
||||
return False
|
||||
runs = dq._fetch_runs(server, repo, sha_env, token)
|
||||
if not runs:
|
||||
return False
|
||||
wfp = spec["workflow_path"]
|
||||
if wfp is not None:
|
||||
runs = [r for r in runs
|
||||
if str(r.get("path", "")).startswith(wfp + "@")]
|
||||
branches = spec["head_branches"]
|
||||
if branches is not None:
|
||||
runs = [r for r in runs
|
||||
if str(r.get("head_branch", "")) in branches]
|
||||
terminal = {"success", "failure", "cancelled", "skipped"}
|
||||
return any(str(r.get("status", "")).lower() not in terminal
|
||||
for r in runs)
|
||||
|
||||
|
||||
def authority_after_wake(op_type: str = DEFAULT_OP_TYPE, sha: str = "",
|
||||
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
|
||||
@@ -75,7 +273,7 @@ def authority_after_wake(max_tries: int = 3, gap_secs: int = 4) -> str:
|
||||
event wait / watchdog."""
|
||||
verdict = "pending"
|
||||
for attempt in range(max_tries):
|
||||
verdict = authority_verdict()
|
||||
verdict = op_authority_verdict(op_type, sha)
|
||||
if verdict != "pending":
|
||||
return verdict
|
||||
if attempt < max_tries - 1:
|
||||
@@ -84,50 +282,114 @@ def authority_after_wake(max_tries: int = 3, gap_secs: int = 4) -> str:
|
||||
|
||||
|
||||
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)
|
||||
"""LEGACY liveness probe (master-ref deploy runs) — kept for callers
|
||||
predating op specs; run() uses op_ci_alive(op_type, sha)."""
|
||||
return op_ci_alive(DEFAULT_OP_TYPE, os.environ.get("DEPLOY_SHA", ""))
|
||||
|
||||
|
||||
def _set_cognition_state(op: str, state: str) -> None:
|
||||
"""E-PATCH §12: stamp cognitive quiescence on the durable record so a
|
||||
fresh process knows cognition is parked (WAITING_EXTERNAL) vs resumable
|
||||
(RESUMABLE). Best-effort: observability never breaks the wait."""
|
||||
try:
|
||||
rec = ew.operation_wait(op)
|
||||
if rec is None:
|
||||
return
|
||||
wait_file = ew.wr_root() / "waits" / f"{rec.get('wait_id')}.json"
|
||||
if not wait_file.is_file():
|
||||
return
|
||||
data = json.loads(wait_file.read_text())
|
||||
data.setdefault("ext", {})["continuation_state"] = state
|
||||
wait_file.write_text(json.dumps(data, indent=1, sort_keys=True))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def run(*, sha: str, deadline_secs: int,
|
||||
watchdog_secs: int = 300,
|
||||
op_type: str = DEFAULT_OP_TYPE,
|
||||
daemon_url: str = ew.DEFAULT_DAEMON_URL) -> int:
|
||||
"""T21.5 §3: ONE LOGICAL AWAIT.
|
||||
|
||||
One logical external wait is one invocation of this function. All
|
||||
nonterminal conditions (event-seen-authority-pending, watchdog expiry
|
||||
with live runs, bounded authority-retry exhausted) re-arm INTERNALLY —
|
||||
control returns to the caller ONLY on a terminal verdict or on real
|
||||
deadline expiry. Watchdogs are failure detectors inside this
|
||||
abstraction, never the scheduler and never completion truth.
|
||||
"""
|
||||
spec = op_spec(op_type) # unknown op_type refused upfront
|
||||
op = f"ci-{sha}"
|
||||
# A-PATCH: the authority subprocess reads DEPLOY_SHA from the env;
|
||||
# python-level run() callers (tool normalization flow) must export it.
|
||||
if sha and not os.environ.get("DEPLOY_SHA"):
|
||||
os.environ["DEPLOY_SHA"] = 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 waiter 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 {op_type} 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),
|
||||
continuation_state="RESUMABLE", # event already arrived
|
||||
op_type=op_type,
|
||||
authority=spec["authority"])
|
||||
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 = normalize_verdict(authority_after_wake(op_type, sha))
|
||||
if verdict == "pending" and not op_ci_alive(op_type, sha):
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha))
|
||||
mode = ("event_replayed" if verdict != "pending"
|
||||
else "authority_unresolved")
|
||||
verdict_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
out = {
|
||||
"sha": sha,
|
||||
"op_type": op_type,
|
||||
"authority": spec["authority"],
|
||||
"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}",
|
||||
objective=f"event-driven {op_type} qualification for {sha}",
|
||||
watched_for="Gitea CI completion event (wake; verdict re-read "
|
||||
"from canonical Gitea)",
|
||||
f"from canonical {spec['authority']} authority)",
|
||||
provider="gitea", event_type="ci_completed", sha=sha,
|
||||
watchdog_secs=min(watchdog_secs, deadline_secs))
|
||||
watchdog_secs=min(watchdog_secs, deadline_secs),
|
||||
continuation_state="WAITING_EXTERNAL",
|
||||
op_type=op_type,
|
||||
authority=spec["authority"])
|
||||
registered_at = rec["created_at"]
|
||||
_set_cognition_state(op, "WAITING_EXTERNAL") # cognition parked
|
||||
deadline = time.monotonic() + deadline_secs
|
||||
verdict = "pending"
|
||||
mode = "event"
|
||||
event_at = ""
|
||||
resumed_at = ""
|
||||
rearm_reason = ""
|
||||
rearm_count = 0
|
||||
try:
|
||||
while time.monotonic() < deadline:
|
||||
remaining = max(1, int(deadline - time.monotonic()))
|
||||
@@ -141,18 +403,14 @@ def run(*, sha: str, deadline_secs: int,
|
||||
time.gmtime())
|
||||
# WAKE received — canonical authority decides, never the
|
||||
# event payload
|
||||
verdict = authority_after_wake()
|
||||
verdict = normalize_verdict(authority_after_wake(op_type, sha))
|
||||
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
|
||||
# T21.5 §3: NONTERMINAL — re-arm internally, keep waiting.
|
||||
rearm_count += 1
|
||||
rearm_reason = (f"event seen; {spec['authority']} "
|
||||
"authority pending — internal re-arm")
|
||||
continue
|
||||
if result["kind"] == "eof":
|
||||
# writer gone without a token: the DURABLE record decides
|
||||
@@ -165,47 +423,64 @@ def run(*, sha: str, deadline_secs: int,
|
||||
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
resumed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime())
|
||||
verdict = authority_after_wake()
|
||||
verdict = normalize_verdict(authority_after_wake(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "event_recovered_from_durable_record"
|
||||
break
|
||||
rearm_count += 1
|
||||
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"
|
||||
# watchdog expiry: operation-scoped liveness decides re-arm
|
||||
# vs truthful recovery — failure detector, never scheduler
|
||||
if op_ci_alive(op_type, sha):
|
||||
rearm_count += 1
|
||||
rearm_reason = ("watchdog expiry; op runs non-terminal "
|
||||
"in Gitea — internal re-arm")
|
||||
continue
|
||||
verdict = authority_verdict()
|
||||
if verdict == "pending":
|
||||
mode = "watchdog_lost_event_no_verdict"
|
||||
else:
|
||||
# no live runs visible for THIS op's authority: either the
|
||||
# operation has not started yet (EXPECTED nonterminal — the
|
||||
# §18-audit generator: this must re-arm, never return to the
|
||||
# model) or it finished between probes. Bounded re-read.
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "watchdog_authority_recovered"
|
||||
break
|
||||
break
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "watchdog_authority_recovered"
|
||||
break
|
||||
# still pending: runs not started OR lagging aggregator —
|
||||
# internal re-arm; the deadline is the only other exit
|
||||
rearm_count += 1
|
||||
rearm_reason = ("watchdog expiry; op pending (not started or "
|
||||
"lagging) — internal re-arm")
|
||||
continue
|
||||
finally:
|
||||
os.close(fifo_fd)
|
||||
if verdict == "pending":
|
||||
if mode == "event":
|
||||
mode = "deadline_pending"
|
||||
verdict = authority_verdict() # final one-shot truth
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha)) # final one-shot truth
|
||||
_set_cognition_state(op, "RESUMABLE" if verdict != "pending"
|
||||
else "RECOVERY_REQUIRED")
|
||||
verdict_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
out = {
|
||||
"sha": sha,
|
||||
"op_type": op_type,
|
||||
"authority": spec["authority"],
|
||||
"registered_at": registered_at,
|
||||
"event_received_at": event_at,
|
||||
"controller_resumed_at": resumed_at,
|
||||
"verdict_at": verdict_at,
|
||||
"mode": mode,
|
||||
"rearm_reason": rearm_reason,
|
||||
"internal_rearm_count": rearm_count,
|
||||
"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:]
|
||||
@@ -214,10 +489,18 @@ def main() -> int:
|
||||
deadline = 900
|
||||
if "--deadline" in args:
|
||||
deadline = int(args[args.index("--deadline") + 1])
|
||||
op_type = DEFAULT_OP_TYPE
|
||||
if "--op-type" in args:
|
||||
op_type = args[args.index("--op-type") + 1]
|
||||
if not sha:
|
||||
print(json.dumps({"error": "no sha (DEPLOY_SHA or --sha required)"}))
|
||||
return 1
|
||||
return run(sha=sha, deadline_secs=deadline)
|
||||
# A-PATCH fix: run()'s authority subprocess (deploy_qualify.py) reads
|
||||
# DEPLOY_SHA from the environment — a CLI `--sha` consumer previously
|
||||
# got a silent "pending" verdict forever because the var was never
|
||||
# exported. Export it so the canonical authority can resolve.
|
||||
os.environ["DEPLOY_SHA"] = sha
|
||||
return run(sha=sha, deadline_secs=deadline, op_type=op_type)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -128,8 +128,13 @@ def _fetch_jobs(server: str, repo: str, run_id, token: str) -> list[dict] | None
|
||||
# ── CLI ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def main() -> int:
|
||||
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO", "")
|
||||
# T21.4 fix: fall back to the canonical defaults when the caller did
|
||||
# not export GITEA_SERVER/GITEA_REPO (CI always exports them; local
|
||||
# and first-class-tool consumers previously got a silent "pending"
|
||||
# verdict — a wrong answer, not a fail-closed one).
|
||||
server = (os.environ.get("GITEA_SERVER", "")
|
||||
or gitea_auth.DEFAULT_SERVER).rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO", "") or gitea_auth.DEFAULT_REPO
|
||||
sha = os.environ.get("DEPLOY_SHA", "")
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not (server and repo and sha and token):
|
||||
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""install_mimo_hook.py — T21.2 §21.2D: canonical MiMo hook installer.
|
||||
|
||||
Canonical source: config/mimo-hooks/event-wait-guard.ts
|
||||
Target: ~/.mimocode/hooks/event-wait-guard.ts
|
||||
Verifies hash after install. Detects drift in both directions."""
|
||||
import hashlib
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CANONICAL = Path(__file__).resolve().parent.parent / "config" / "mimo-hooks" / "event-wait-guard.ts"
|
||||
TARGET = Path.home() / ".mimocode" / "hooks" / "event-wait-guard.ts"
|
||||
|
||||
|
||||
def sha256(p: Path) -> str:
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not CANONICAL.is_file():
|
||||
print(f"canonical source missing: {CANONICAL}")
|
||||
return 1
|
||||
canon_hash = sha256(CANONICAL)
|
||||
if TARGET.is_file():
|
||||
target_hash = sha256(TARGET)
|
||||
if target_hash == canon_hash:
|
||||
print(f"in-sync: {target_hash[:12]}")
|
||||
return 0
|
||||
print(f"DRIFT: canonical={canon_hash[:12]} installed={target_hash[:12]}")
|
||||
else:
|
||||
print(f"hook missing at {TARGET}")
|
||||
TARGET.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(CANONICAL, TARGET)
|
||||
installed_hash = sha256(TARGET)
|
||||
if installed_hash != canon_hash:
|
||||
print(f"INSTALL FAILED: hash mismatch after copy")
|
||||
return 1
|
||||
print(f"installed: {installed_hash[:12]}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(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 = [
|
||||
|
||||
@@ -38,11 +38,17 @@ def build_context(issue: dict, define_contract: dict) -> dict:
|
||||
# 3. Find matching test files
|
||||
matched_tests = _find_tests(keywords)
|
||||
|
||||
# 4. T21.5 §10: Memory internal-experience port — ONE port of call,
|
||||
# bounded, zero-result = normal (fast path: [] → next step directly).
|
||||
from research_memory_port import memory_port_of_call
|
||||
memory_candidates = memory_port_of_call(issue, keywords)
|
||||
|
||||
return {
|
||||
"keywords": keywords,
|
||||
"umbrella_matches": matched_modules,
|
||||
"candidate_files": matched_files[:15], # bounded
|
||||
"candidate_tests": matched_tests[:10], # bounded
|
||||
"memory_candidates": memory_candidates, # evidence, never authority
|
||||
"define_contract": define_contract,
|
||||
"mimo_needed": _needs_mimo(matched_files, matched_tests),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""research_memory_port.py — T21.5 §10-12: the future-facing Memory port
|
||||
inside the EXISTING INWARD research stage.
|
||||
|
||||
BOUNDARIES (G9/G10/G11):
|
||||
- This is a PORT, not a memory system. No storage is created here; the
|
||||
implementation delegates to a provider callable that future Memory
|
||||
will supply. Tests inject doubles; INWARD never imports Memory.
|
||||
- Zero result is a NORMAL SUCCESSFUL result: the caller continues the
|
||||
normal INWARD flow immediately (no extra model call, no ceremony).
|
||||
- Returned material is EVIDENCE/CANDIDATES only — never an instruction
|
||||
authority. APPLICABILITY (existing stage) governs whether a candidate
|
||||
applies; SYNTHESIS receives only distilled, applicable consequences.
|
||||
- Teachable-moment seam: durable construction evidence can mark an
|
||||
episode as a CANDIDATE for future learning ("may be worth learning",
|
||||
never "always apply"). Substrate/Memory (future) harvest candidates;
|
||||
today the seam only records them via the same provider contract.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Callable
|
||||
|
||||
# The provider contract: ONE callable, independently replaceable/mockable.
|
||||
# Signature: query(query_text, keywords, *, limit) -> list[MemoryCandidate]
|
||||
# MemoryCandidate (dict): {source, kind, summary, relevance, episode_ref}
|
||||
# kind ∈ {construction_episode, teachable_moment_candidate,
|
||||
# prior_failure, prior_success}
|
||||
# Future Memory supplies the real implementation; nothing else changes.
|
||||
|
||||
MemoryProvider = Callable[..., list[dict]]
|
||||
|
||||
_DEFAULT_PROVIDER: MemoryProvider | None = None
|
||||
|
||||
|
||||
def set_memory_provider(provider: MemoryProvider | None) -> None:
|
||||
"""Bind the Memory-side implementation (production or test double).
|
||||
None disables the port — INWARD continues without it (fast path)."""
|
||||
global _DEFAULT_PROVIDER
|
||||
_DEFAULT_PROVIDER = provider
|
||||
|
||||
|
||||
def _provider() -> MemoryProvider | None:
|
||||
global _DEFAULT_PROVIDER
|
||||
if _DEFAULT_PROVIDER is not None:
|
||||
return _DEFAULT_PROVIDER
|
||||
env = os.environ.get("RUNE_MEMORY_PORT_PROVIDER")
|
||||
if env:
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("mprov", env)
|
||||
if spec is not None and spec.loader is not None:
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
provider = getattr(mod, "query", None)
|
||||
if provider is not None:
|
||||
_DEFAULT_PROVIDER = provider
|
||||
return _DEFAULT_PROVIDER
|
||||
|
||||
|
||||
MAX_CANDIDATES = 5
|
||||
MIN_RELEVANCE = 0.3
|
||||
|
||||
|
||||
def memory_port_of_call(issue: dict, keywords: list[str],
|
||||
*, provider: MemoryProvider | None = None) -> list[dict]:
|
||||
"""The ONE internal port of call inside INWARD. Bounded retrieval of
|
||||
internal evidence (construction episodes, teachable-moment
|
||||
candidates, prior failures/successes).
|
||||
|
||||
ZERO-RESULT FAST PATH (§11): empty/thin results return [] and the
|
||||
caller continues the next normal INWARD step directly. No extra
|
||||
model call, no fallback reasoning, no synthetic summary, no delay.
|
||||
"""
|
||||
prov = provider or _provider()
|
||||
if prov is None:
|
||||
return [] # port unbound: normal INWARD continues, zero overhead
|
||||
try:
|
||||
query_text = " ".join([
|
||||
str(issue.get("title", "")),
|
||||
str(issue.get("body", ""))[:400],
|
||||
]).strip()
|
||||
raw = prov(query_text, keywords, limit=MAX_CANDIDATES) or []
|
||||
except Exception: # noqa: BLE001 — port failure must never break INWARD
|
||||
return []
|
||||
candidates = [c for c in raw
|
||||
if float(c.get("relevance", 0.0)) >= MIN_RELEVANCE]
|
||||
return candidates[:MAX_CANDIDATES]
|
||||
|
||||
|
||||
def applicability_filter(candidates: list[dict],
|
||||
context_keywords: list[str]) -> tuple[list[dict],
|
||||
list[dict]]:
|
||||
"""Existing APPLICABILITY governs transfer: candidates relevant to
|
||||
the CURRENT context pass; irrelevant ones are rejected WITHOUT
|
||||
contaminating downstream SYNTHESIS. Returns (applicable, rejected)."""
|
||||
applicable, rejected = [], []
|
||||
for c in candidates:
|
||||
text = (str(c.get("summary", "")) + " " +
|
||||
" ".join(str(k) for k in c.get("keywords", []))).lower()
|
||||
overlap = sum(1 for k in context_keywords if k and k.lower() in text)
|
||||
if overlap > 0 or c.get("explicitly_applicable") is True:
|
||||
applicable.append(c)
|
||||
else:
|
||||
rejected.append(c)
|
||||
return applicable, rejected
|
||||
|
||||
|
||||
# ── §12: teachable-moment future seam ──────────────────────────────────────
|
||||
|
||||
CANDIDATE_TRIGGERS = (
|
||||
"completed_release", "capability_completed", "failure_recovered",
|
||||
"rollback_executed", "user_correction", "prediction_observation_mismatch",
|
||||
"procedure_improved",
|
||||
)
|
||||
|
||||
|
||||
def mark_teachable_moment_candidate(
|
||||
episode_ref: str, *, trigger: str, summary: str,
|
||||
emit: Callable[[dict], Any] | None = None) -> dict | None:
|
||||
"""Record durable evidence that an episode MAY be worth future
|
||||
learning — a candidate, never a rule. Uses the existing evidence/
|
||||
event mechanism via `emit` (the caller's channel: journal, event
|
||||
bridge, ledger history). No new canonical store is created here;
|
||||
future Substrate/Memory harvests candidates through the port."""
|
||||
if trigger not in CANDIDATE_TRIGGERS:
|
||||
raise ValueError(
|
||||
f"unknown trigger {trigger!r} — must be one of "
|
||||
f"{CANDIDATE_TRIGGERS}")
|
||||
record = {
|
||||
"candidate": "teachable_moment",
|
||||
"episode_ref": episode_ref,
|
||||
"trigger": trigger,
|
||||
"summary": summary[:200],
|
||||
"status": "CANDIDATE", # never authority, never auto-applied
|
||||
"note": "may be worth future learning; APPLICABILITY governs "
|
||||
"any future transfer",
|
||||
}
|
||||
if emit is not None:
|
||||
emit(record)
|
||||
return record
|
||||
@@ -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,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""rollback_generation.py — T21.4 §21.4F: the governed rollback API.
|
||||
|
||||
rollback_generation(target_generation, expected_current, reason)
|
||||
|
||||
Prior tranches proved rollback by recreating deployment primitives by hand
|
||||
(ln -sfn against the live deploy root). That is operator work, not governed
|
||||
machinery — it bypasses lock, eligibility, health, and history. This module
|
||||
is the single canonical rollback operation, built ON the proven deploy
|
||||
machinery (deploy_promotion's lock + atomic swap + build-identity authority),
|
||||
not beside it.
|
||||
|
||||
The promotion authority (deploy_promotion.py) is deliberately MONOTONIC: an
|
||||
ancestor promotion is refused as STALE_SUPERSEDED. Regression is therefore
|
||||
impossible through promotion — and legitimate only through THIS operation,
|
||||
which owns its own obligations:
|
||||
|
||||
1. deployment lock — the SAME .promotion.lock as promote (serialize)
|
||||
2. current verification — build-identity readback must equal expected_current
|
||||
3. target existence — generation dir present, build identity readable
|
||||
4. eligibility — target commit is ancestor-or-equal of current
|
||||
(regression only; forward = promotion's job;
|
||||
divergent/unknown refused)
|
||||
5. atomic switch — rename(2) symlink swap, never absent/half-updated
|
||||
6. runtime handoff — Rune executes from current/src at process start;
|
||||
handoff = identity a fresh launch would run
|
||||
7. runtime verification — fresh-launch identity must equal target
|
||||
8. health qualification — import smoke against target generation src
|
||||
9. durable history — JSONL rollback events, appended inside the lock
|
||||
10. failure recovery — health failure restores prior current (where safe)
|
||||
11. coherent last-good — last-good := the generation that WAS current
|
||||
(same invariant promotion maintains)
|
||||
|
||||
Truthful outcomes:
|
||||
ROLLED_BACK target active, health green, history recorded
|
||||
NOOP_ALREADY_CURRENT target already the deployed generation
|
||||
ROLLED_BACK_REVERTED switched, health red, prior current restored
|
||||
REFUSED_UNKNOWN_CURRENT deployed identity unreadable (fail closed)
|
||||
REFUSED_EXPECTED_MISMATCH current != expected_current (stale caller)
|
||||
REFUSED_TARGET_MISSING target generation dir absent
|
||||
REFUSED_TARGET_UNREADABLE target build identity unreadable
|
||||
REFUSED_NOT_ELIGIBLE divergent, newer, or non-ancestor target
|
||||
REFUSED_NO_REASON empty reason (accountability is mandatory)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(_SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import deploy_promotion # proven machinery: lock path, ancestry, swaps
|
||||
|
||||
DEPLOY_ROOT = deploy_promotion.DEPLOY_ROOT
|
||||
HARNESS_ROOT = _SCRIPT_DIR.parent
|
||||
HISTORY_PATH = HARNESS_ROOT / "store" / "scaffold" / "rollback_history.jsonl"
|
||||
|
||||
ROLLED_BACK = "ROLLED_BACK"
|
||||
NOOP = "NOOP_ALREADY_CURRENT"
|
||||
ROLLED_BACK_REVERTED = "ROLLED_BACK_REVERTED"
|
||||
REFUSED_UNKNOWN_CURRENT = "REFUSED_UNKNOWN_CURRENT"
|
||||
REFUSED_EXPECTED_MISMATCH = "REFUSED_EXPECTED_MISMATCH"
|
||||
REFUSED_TARGET_MISSING = "REFUSED_TARGET_MISSING"
|
||||
REFUSED_TARGET_UNREADABLE = "REFUSED_TARGET_UNREADABLE"
|
||||
REFUSED_NOT_ELIGIBLE = "REFUSED_NOT_ELIGIBLE"
|
||||
REFUSED_NO_REASON = "REFUSED_NO_REASON"
|
||||
|
||||
|
||||
def _identity(deploy_root: Path, generation: str) -> str:
|
||||
"""Authoritative commit for a generation dir (build identity, not name)."""
|
||||
gen_dir = deploy_root / generation
|
||||
identity = gen_dir / "src" / "rune" / "_build_identity.json"
|
||||
try:
|
||||
data = json.loads(identity.read_text(encoding="utf-8"))
|
||||
return str(data.get("commit", ""))
|
||||
except (OSError, ValueError):
|
||||
return ""
|
||||
|
||||
|
||||
def _health(deploy_root: Path, generation: str, timeout: int = 60) -> tuple[bool, str]:
|
||||
"""Health qualification for a generation: build identity readable AND
|
||||
`import rune` succeeds from that generation's src tree."""
|
||||
gen_dir = deploy_root / generation
|
||||
commit = _identity(deploy_root, generation)
|
||||
if not commit:
|
||||
return False, "build identity unreadable"
|
||||
src = gen_dir / "src"
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, "-c", "import rune; import sys; "
|
||||
"sys.stdout.write(getattr(rune, '__version__', 'ok'))"],
|
||||
env={"PYTHONPATH": str(src), "PATH": "/usr/bin:/bin",
|
||||
"HOME": str(gen_dir)},
|
||||
capture_output=True, text=True, timeout=timeout, cwd=str(gen_dir),
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, f"import smoke timed out after {timeout}s"
|
||||
if proc.returncode != 0:
|
||||
return False, f"import smoke failed: {(proc.stderr or '')[-200:]}"
|
||||
return True, f"identity={commit[:12]} import_smoke=ok ({proc.stdout.strip()[:40]})"
|
||||
|
||||
|
||||
def _append_history(record: dict, history_path: Path = HISTORY_PATH) -> None:
|
||||
history_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(history_path, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(record, sort_keys=True) + "\n")
|
||||
|
||||
|
||||
def _swap(deploy_root: Path, name: str, target: Path) -> None:
|
||||
deploy_promotion._atomic_symlink_target(deploy_root, name, target)
|
||||
|
||||
|
||||
def rollback_generation(
|
||||
target_generation: str,
|
||||
expected_current: str,
|
||||
reason: str,
|
||||
*,
|
||||
deploy_root: Path = DEPLOY_ROOT,
|
||||
repo: Path = HARNESS_ROOT,
|
||||
history_path: Path = HISTORY_PATH,
|
||||
) -> tuple[str, dict]:
|
||||
"""The governed rollback operation. Returns (outcome, detail).
|
||||
|
||||
Refusals happen BEFORE any mutation of current/last-good. The lock is
|
||||
shared with promote() so rollback and promotion can never interleave.
|
||||
"""
|
||||
if not reason or not reason.strip():
|
||||
return REFUSED_NO_REASON, {"error": "rollback requires a reason"}
|
||||
|
||||
lock_path = deploy_root / ".promotion.lock"
|
||||
# The lock file may be owned by the CI runner (promotion side opens it
|
||||
# for writing). flock needs only an open fd, never write permission, so
|
||||
# open read-only when write is denied — same file, same fence.
|
||||
try:
|
||||
lock_fd = open(lock_path, "a+")
|
||||
except PermissionError:
|
||||
lock_fd = open(lock_path) if lock_path.exists() else \
|
||||
open(lock_path, "a+")
|
||||
with lock_fd:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
||||
try:
|
||||
current = deploy_root / "current"
|
||||
last_good = deploy_root / "last-good"
|
||||
detail: dict = {"target": target_generation,
|
||||
"expected_current": expected_current,
|
||||
"reason": reason[:200],
|
||||
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime())}
|
||||
|
||||
# 2. current verification — identity authority, never the name
|
||||
current_generation = ""
|
||||
try:
|
||||
current_generation = Path(os_readlink(current)).name
|
||||
except OSError:
|
||||
pass
|
||||
deployed_commit = deploy_promotion.read_deployed_commit(deploy_root)
|
||||
if not deployed_commit:
|
||||
return REFUSED_UNKNOWN_CURRENT, {
|
||||
"error": "deployed commit unreadable — fail closed",
|
||||
**detail}
|
||||
detail["current_generation"] = current_generation
|
||||
detail["current_commit"] = deployed_commit
|
||||
if expected_current and expected_current != deployed_commit:
|
||||
detail["error"] = (
|
||||
f"current is {deployed_commit[:12]}, "
|
||||
f"caller expected {expected_current[:12]} — refused "
|
||||
"before mutation")
|
||||
return REFUSED_EXPECTED_MISMATCH, detail
|
||||
|
||||
# 3. target existence + identity
|
||||
target_dir = deploy_root / target_generation
|
||||
if not target_dir.is_dir():
|
||||
detail["error"] = f"generation dir {target_generation} absent"
|
||||
return REFUSED_TARGET_MISSING, detail
|
||||
target_commit = _identity(deploy_root, target_generation)
|
||||
if not target_commit:
|
||||
detail["error"] = (
|
||||
f"generation {target_generation} build identity unreadable")
|
||||
return REFUSED_TARGET_UNREADABLE, detail
|
||||
detail["target_commit"] = target_commit
|
||||
|
||||
if target_commit == deployed_commit:
|
||||
detail["note"] = "target already the deployed generation"
|
||||
return NOOP, detail
|
||||
|
||||
# 4. eligibility — ancestor-or-equal only (regression)
|
||||
rel = deploy_promotion.ancestry_of(target_commit, deployed_commit,
|
||||
repo)
|
||||
if rel != "ancestor":
|
||||
detail["error"] = (
|
||||
f"target commit ancestry={rel} — only ancestors are "
|
||||
"rollback-eligible (forward movement belongs to "
|
||||
"deploy_promotion)")
|
||||
return REFUSED_NOT_ELIGIBLE, detail
|
||||
|
||||
# 5. atomic switch (inside lock), last-good := previous current
|
||||
if last_good.is_symlink():
|
||||
last_good.unlink()
|
||||
_swap(deploy_root, "last-good", Path(os_readlink(current)))
|
||||
_swap(deploy_root, "current", target_dir)
|
||||
detail["switched_to"] = target_generation
|
||||
|
||||
# 7+8. runtime verification + health qualification
|
||||
post_commit = deploy_promotion.read_deployed_commit(deploy_root)
|
||||
health_ok, health_detail = _health(deploy_root, target_generation)
|
||||
detail["post_switch_identity"] = post_commit
|
||||
detail["health"] = health_detail
|
||||
|
||||
record = {"at": detail["at"], "operation": "rollback_generation",
|
||||
**detail}
|
||||
if post_commit != target_commit or not health_ok:
|
||||
# 10. failure recovery — restore prior current (safe: we
|
||||
# hold the lock and the prior generation is intact)
|
||||
prev = Path(os_readlink(last_good)) if last_good.is_symlink() \
|
||||
else None
|
||||
if prev is not None:
|
||||
_swap(deploy_root, "current", prev)
|
||||
detail["reverted_to"] = (Path(str(os_readlink(current))).name
|
||||
if current.is_symlink() else "?")
|
||||
detail["outcome"] = ROLLED_BACK_REVERTED
|
||||
record["outcome"] = ROLLED_BACK_REVERTED
|
||||
_append_history(record, history_path)
|
||||
return ROLLED_BACK_REVERTED, detail
|
||||
|
||||
record["outcome"] = ROLLED_BACK
|
||||
_append_history(record, history_path)
|
||||
detail["outcome"] = ROLLED_BACK
|
||||
return ROLLED_BACK, detail
|
||||
finally:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
def os_readlink(p: Path) -> str:
|
||||
return str(Path(str(p)).readlink()) if hasattr(Path, "readlink") \
|
||||
else str(Path(str(p)).readlink())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("target_generation")
|
||||
ap.add_argument("--expected-current", required=True,
|
||||
help="commit the caller believes is currently deployed")
|
||||
ap.add_argument("--reason", required=True)
|
||||
args = ap.parse_args()
|
||||
outcome, detail = rollback_generation(args.target_generation,
|
||||
args.expected_current, args.reason)
|
||||
print(json.dumps({"outcome": outcome, **detail}, indent=2, sort_keys=True))
|
||||
return 0 if outcome in (ROLLED_BACK, NOOP) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -18,6 +18,13 @@ from __future__ import annotations
|
||||
import re
|
||||
import sys
|
||||
|
||||
# T20 §20B: push-convergence synthesizes PR titles as "<kind>: converged
|
||||
# release (push-to-master bump)" where kind comes from
|
||||
# version_convergence.highest_warranted_bump. The title text must carry
|
||||
# that decision — "minor:" was re-classified as patch here, downgrading
|
||||
# every feature tranche (observed: feat ranges bumped 0.66.77-83 not 0.67.0).
|
||||
MINOR_PREFIX_RE = re.compile(r"^\s*minor\b", re.IGNORECASE)
|
||||
|
||||
BREAKING_RE = re.compile(r"^\s*\w+(\([^)]*\))?!:")
|
||||
FEAT_RE = re.compile(r"^\s*feat(\([^)]*\))?")
|
||||
|
||||
@@ -25,6 +32,8 @@ FEAT_RE = re.compile(r"^\s*feat(\([^)]*\))?")
|
||||
def next_version(version: str, title: str) -> str:
|
||||
if BREAKING_RE.match(title) or "BREAKING CHANGE" in title:
|
||||
kind = "major"
|
||||
elif MINOR_PREFIX_RE.match(title):
|
||||
kind = "minor" # synthesized convergence title carries the kind
|
||||
elif FEAT_RE.match(title):
|
||||
kind = "minor"
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""start_wr_daemon.py — T21.5 §7: THE canonical daemon restart launcher.
|
||||
|
||||
T21.4 live finding: `python3 daemon.py` defaults WR_ROOT to the daemon's
|
||||
install dir, so a restart silently opened a second event store. This
|
||||
launcher resolves the root ONCE through scripts/wr_root_resolver.py (the
|
||||
single authority) and passes it explicitly — nobody has to remember to
|
||||
export WR_ROOT.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import wr_root_resolver as resolver # noqa: E402
|
||||
|
||||
DAEMON_CODE_CANDIDATES = (
|
||||
"/home/nick/.local/share/mimo-wait-resume/daemon.py",
|
||||
)
|
||||
|
||||
|
||||
def find_daemon_code() -> Path:
|
||||
for cand in DAEMON_CODE_CANDIDATES:
|
||||
p = Path(cand)
|
||||
if p.is_file():
|
||||
return p
|
||||
raise RuntimeError(f"daemon.py not found in {DAEMON_CODE_CANDIDATES}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = resolver.resolve_root()
|
||||
daemon = find_daemon_code()
|
||||
env = dict(os.environ,
|
||||
WR_ROOT=str(root), # daemon data root
|
||||
RUNE_WR_ROOT=str(root), # adapter-side authority
|
||||
RUNE_WR_CODE=str(daemon.parent))
|
||||
port = sys.argv[1] if len(sys.argv) > 1 else "8791"
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, str(daemon), port],
|
||||
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
start_new_session=True)
|
||||
# bounded startup probe (process-local, not an external wait)
|
||||
for _ in range(20):
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
f"http://127.0.0.1:{port}/health", timeout=1) as r:
|
||||
if r.status == 200:
|
||||
print(f"wr daemon up: pid={proc.pid} root={root} "
|
||||
f"port={port}")
|
||||
return 0
|
||||
except OSError:
|
||||
time.sleep(0.25)
|
||||
print(f"wr daemon did not become healthy (pid={proc.pid})", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env python3
|
||||
"""structural_coverage_audit.py — T21.4 §21.4G: Rune-wide meaningful
|
||||
structural coverage audit.
|
||||
|
||||
Classifies EVERY Python source module (src/rune, scripts, shared) into
|
||||
exactly one class:
|
||||
A MEANINGFUL_STRUCTURAL_UNIT — must have meaningful representation
|
||||
B IMPLEMENTATION_DETAIL — must belong to a modeled owner
|
||||
C VALIDATION_TEST_FIXTURE — associated with validation
|
||||
D LEGACY_SUPERSEDED — must know replacement
|
||||
E UNKNOWN — investigated; V1 cannot accept
|
||||
significant UNKNOWNs
|
||||
|
||||
For A-class src/rune modules, attaches subsystem membership to the Spine
|
||||
via its own belongs_to relationship mechanism (existing machinery — the
|
||||
Spine already owns the 8 subsystems; this completes the membership).
|
||||
|
||||
Writes store/scaffold/structural_coverage_t214.json + prints measures.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
# ── A-class: src/rune meaningful units → subsystem ─────────────────────────
|
||||
SRC_A = {
|
||||
# governance / construction / structural
|
||||
"scaffold": "governance", "structural_store": "governance",
|
||||
"structural_model": "governance", "structural_migration": "governance",
|
||||
"spine": "governance", "construction_activation": "governance",
|
||||
"construction_commands": "governance", "construction_decision": "governance",
|
||||
"construction_depth": "governance", "construction_intent": "governance",
|
||||
"construction_interface": "governance", "construction_mode": "governance",
|
||||
"construction_pattern": "governance",
|
||||
"construction_patterns_builtin": "governance",
|
||||
"construction_projection": "governance", "stage_guards": "governance",
|
||||
"capability_fit": "governance", "toolchain": "governance",
|
||||
"layout_contract": "governance", "project_registry": "governance",
|
||||
"programme_gates": "governance", "programme_continuation": "governance",
|
||||
"build_basis": "governance", "iteration": "governance",
|
||||
"event_wait": "governance", "wait_guard": "governance",
|
||||
"failure_recovery": "governance", "failure_auto_filer": "governance",
|
||||
"forge": "governance", "forge_client": "governance",
|
||||
"release_notes": "governance",
|
||||
# memory (existing machinery — not Memory V1)
|
||||
"memory_store": "memory", "memory_selector": "memory",
|
||||
"memory_consolidation": "memory", "memory_env": "memory",
|
||||
"shard_manager": "memory", "tier_manager": "memory",
|
||||
"graph_memory": "memory", "query_memory": "memory",
|
||||
"curator": "memory", "sleep_time": "memory",
|
||||
"subconscious_pass": "memory", "warm_neuron_store": "memory",
|
||||
"warm_router": "memory", "abstraction": "memory",
|
||||
"auto_context": "memory", "cache": "memory",
|
||||
"connection_pool": "memory",
|
||||
# growth / governed adaptation / ledgers
|
||||
"drift_core": "growth", "drift_ledger": "growth",
|
||||
"drift_cache": "growth", "drift_compression": "growth",
|
||||
"drift_observability": "growth", "drift_resume": "growth",
|
||||
"growth_delta": "growth", "self_improve": "growth",
|
||||
"night_mode": "growth", "behavior": "growth",
|
||||
"outcome_ledger": "growth", "claim_audit": "growth",
|
||||
# identity / persona / voice
|
||||
"identity_anchors": "identity", "persona_definition": "identity",
|
||||
"persona_gate": "identity", "voice_hybrid": "identity",
|
||||
"voice_sync": "identity", "safety_override": "identity",
|
||||
"prompt_sync": "identity",
|
||||
# inference / routing / providers
|
||||
"adaptive_router": "inference", "route_risk_flags": "inference",
|
||||
"model_routing": "inference", "model_connect": "inference",
|
||||
"model_profiles": "inference", "model_registry": "inference",
|
||||
"provider_registry": "inference", "provider_context": "inference",
|
||||
"provider_budget": "inference", "standardized_providers": "inference",
|
||||
"openai_compat_transport": "inference",
|
||||
"local_tool_runtime": "inference", "response_adapter": "inference",
|
||||
"inference_trace": "inference", "evidence_probe": "inference",
|
||||
"recall_router": "inference", "recall_uplift": "inference",
|
||||
# orchestration / cognition / continuation / ledgers
|
||||
"elastic_cognition": "orchestration", "tiered_executor": "orchestration",
|
||||
"attention_capsule": "orchestration", "witness": "orchestration",
|
||||
"event_bus": "orchestration", "microkernel": "orchestration",
|
||||
"capability_broker": "orchestration", "context_broker": "orchestration",
|
||||
"work_manager": "orchestration", "task_persist": "orchestration",
|
||||
"goal": "orchestration", "cron_engine": "orchestration",
|
||||
"hook_engine": "orchestration", "subsystem_health": "orchestration",
|
||||
"verification_layer": "orchestration", "build_mode": "orchestration",
|
||||
"builder_bridge": "orchestration", "testing_mode": "orchestration",
|
||||
"rune_operator": "orchestration", "audit_ledger": "capability",
|
||||
"capability_ledger": "capability", "behaviour_ledger": "capability",
|
||||
"impact_preview": "governance", "graph_projection": "governance",
|
||||
"tool_executor": "orchestration", "tool_protocol": "orchestration",
|
||||
# perception / TUI boundary
|
||||
"rune_tui": "perception", "__main__": "perception",
|
||||
"voicebox": "perception",
|
||||
}
|
||||
|
||||
# B-class: src/rune implementation details → owning meaningful unit
|
||||
SRC_B = {
|
||||
"event_envelope": "event_bus",
|
||||
"cognitive_budgeter": "elastic_cognition",
|
||||
"salience_gate": "elastic_cognition",
|
||||
"rune_profiler": "elastic_cognition",
|
||||
"progressive_escalation": "tiered_executor",
|
||||
"parallel_processor": "tiered_executor",
|
||||
"context_pressure": "context_broker",
|
||||
"token_optimizer": "context_broker",
|
||||
"message_queue": "work_manager",
|
||||
"auto_tasks": "work_manager",
|
||||
"task_gate": "task_persist",
|
||||
"pins": "goal",
|
||||
"chat_bubble": "rune_tui",
|
||||
"rune_spinner": "rune_tui",
|
||||
"rune_graphic": "rune_tui",
|
||||
"message_widgets": "rune_tui",
|
||||
"tui_validator": "rune_tui",
|
||||
"tui_backlog": "rune_tui",
|
||||
"builder_bridge_cli": "builder_bridge",
|
||||
"voice_injector": "voice_hybrid",
|
||||
"intent_continuity": "programme_continuation",
|
||||
"time_awareness": "rune_operator",
|
||||
"contracts": "verification_layer",
|
||||
"system_registry": "governance-ops",
|
||||
"dependency_tracker": "ops-harness",
|
||||
}
|
||||
|
||||
# C-class: validation/test fixtures (canaries + corpus)
|
||||
SRC_C = {
|
||||
"canary_cognitive_strata", "canary_context_broker",
|
||||
"canary_conversation_state", "canary_full_system",
|
||||
"canary_gitea_automation", "canary_gitea_delegation",
|
||||
"canary_gitea_pipeline", "canary_memory_synthesis", "canary_native",
|
||||
"canary_orchestration_foundation", "canary_p0_04", "canary_p0_05",
|
||||
"canary_p0_07", "canary_p1_01", "canary_p1_02", "canary_p1_03",
|
||||
"canary_p1_06", "canary_p1_07", "canary_query_memory",
|
||||
"canary_resource_authority", "canary_schema_migration",
|
||||
"canary_surface_parity", "canary_tui_drift", "canary_unbounded",
|
||||
"wait_guard_corpus", "contracts",
|
||||
}
|
||||
|
||||
# scripts/ A-class (deployment/CI/release/research/governance machinery)
|
||||
SCRIPTS_A = {
|
||||
"auto_bump_pr", "capture_failure", "carry_forward_check",
|
||||
"check_dep_lock", "check_discharge_contract", "check_version_surfaces",
|
||||
"ci_event_wait", "construction_guards", "create_release",
|
||||
"deploy_promotion", "deploy_qualify", "deploy_stage",
|
||||
"rollback_generation", "file_drift_issue", "gitea_auth",
|
||||
"gitea_reconciliation_check", "install_mimo_hook", "lint_ratchet",
|
||||
"merge_robot", "mutation_boundary", "notify_event_bridge",
|
||||
"process_experience", "publish_source_ledger", "research_artifact",
|
||||
"research_backfill_sweep", "research_define", "research_event_relay",
|
||||
"research_inward_context", "research_prompt_compiler",
|
||||
"research_router", "research_stage_executor", "research_task_tree",
|
||||
"runner_evidence", "scaffold_projection", "semver_bump",
|
||||
"source_ledger", "stage_contracts", "stage_telemetry",
|
||||
"t14_builder_session", "t15_builder_session", "type_check_ratchet",
|
||||
"umbrella_impact", "version_convergence",
|
||||
}
|
||||
SCRIPTS_A = set(SCRIPTS_A) | {"structural_coverage_audit"}
|
||||
SCRIPTS_B = {"gen_prd_state": "governance-ops",
|
||||
"pl1_records": "process_experience",
|
||||
"release_history_cli": "release_notes"}
|
||||
SCRIPTS_C = {"fake_worker", "tui_startup_canary"}
|
||||
|
||||
# shared/ — harness-facing layer; mirrors of canonical src modules = D
|
||||
SHARED_D = {
|
||||
"audit_ledger": "src/rune/audit_ledger.py",
|
||||
"dependency_tracker": "src/rune/dependency_tracker.py",
|
||||
"drift_cache": "src/rune/drift_cache.py",
|
||||
"drift_compression": "src/rune/drift_compression.py",
|
||||
"drift_core": "src/rune/drift_core.py",
|
||||
"drift_observability": "src/rune/drift_observability.py",
|
||||
"gitea_auth": "scripts/gitea_auth.py",
|
||||
"query_memory": "src/rune/query_memory.py",
|
||||
"shard_manager": "src/rune/shard_manager.py",
|
||||
"subconscious_pass": "src/rune/subconscious_pass.py",
|
||||
"tier_manager": "src/rune/tier_manager.py",
|
||||
}
|
||||
SHARED_B_OWNER = "ops-harness"
|
||||
SHARED_B = {"auto_detect", "autonomy_loop", "build_orchestrator",
|
||||
"build_pipeline_policy", "cognitive_loop", "custodian",
|
||||
"dream_cycle", "gitea_pipeline", "gitea_provision",
|
||||
"graph_shard_bridge", "growth_simulator", "umbrella_update"}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
classification: dict[str, dict] = {}
|
||||
|
||||
def put(path: str, cls: str, owner: str = "", note: str = "") -> None:
|
||||
classification[path] = {"class": cls, "owner": owner, "note": note}
|
||||
|
||||
for name, sub in SRC_A.items():
|
||||
put(f"src/rune/{name}.py", "A", owner=f"subsystem:{sub}")
|
||||
for name, owner in SRC_B.items():
|
||||
put(f"src/rune/{name}.py", "B", owner=owner)
|
||||
for name in SRC_C:
|
||||
put(f"src/rune/{name}.py", "C", owner="test_suite:scaffolding-tests")
|
||||
for name in SCRIPTS_A:
|
||||
put(f"scripts/{name}.py", "A", owner="subsystem:governance")
|
||||
for name, owner in SCRIPTS_B.items():
|
||||
put(f"scripts/{name}.py", "B", owner=owner)
|
||||
for name in SCRIPTS_C:
|
||||
put(f"scripts/{name}.py", "C", owner="test_suite:scaffolding-tests")
|
||||
for name, canonical in SHARED_D.items():
|
||||
put(f"shared/{name}.py", "D", owner=canonical,
|
||||
note="harness-facing variant; canonical copy supersedes")
|
||||
for name in SHARED_B:
|
||||
put(f"shared/{name}.py", "B", owner=SHARED_B_OWNER)
|
||||
|
||||
# ── E investigation: any .py file not classified? ──
|
||||
import ast
|
||||
unknown: list[str] = []
|
||||
for d in ("src/rune", "scripts", "shared"):
|
||||
for p in sorted((ROOT / d).glob("*.py")):
|
||||
rel = str(p.relative_to(ROOT))
|
||||
if p.name == "__init__.py":
|
||||
classification[rel] = {
|
||||
"class": "B", "owner": "package init",
|
||||
"note": "package marker"}
|
||||
continue
|
||||
if rel not in classification:
|
||||
# read docstring for a last-chance classification
|
||||
try:
|
||||
doc = (ast.get_docstring(
|
||||
ast.parse(p.read_text())) or "").strip()
|
||||
except SyntaxError:
|
||||
doc = ""
|
||||
unknown.append(f"{rel} :: {doc.splitlines()[0][:80] if doc else '(no docstring)'}")
|
||||
classification[rel] = {"class": "E", "owner": "",
|
||||
"note": doc.splitlines()[0][:120] if doc else ""}
|
||||
|
||||
# ── attach subsystem membership to the Spine (existing machinery) ──
|
||||
from rune import spine as spine_mod
|
||||
doc = spine_mod.load() or spine_mod.new_doc()
|
||||
added = 0
|
||||
for name, sub in SRC_A.items():
|
||||
rel = spine_mod.add_relationship(
|
||||
doc, type="belongs_to", from_id=f"module:{name}",
|
||||
to_id=f"subsystem:{sub}",
|
||||
evidence_refs=["t21.4-structural-coverage"])
|
||||
if rel is not None:
|
||||
added += 1
|
||||
spine_mod.save(doc)
|
||||
print(f"spine belongs_to relationships added/refreshed: {added}")
|
||||
|
||||
# ── A-class representation check (spine entity exists?) ──
|
||||
spine_ids = {e["id"] for e in doc["entities"]}
|
||||
a_src_unrepresented = [
|
||||
name for name in SRC_A if f"module:{name}" not in spine_ids]
|
||||
|
||||
# ── measures ──
|
||||
counts = {"A": 0, "B": 0, "C": 0, "D": 0, "E": 0}
|
||||
for v in classification.values():
|
||||
counts[v["class"]] += 1
|
||||
total = len(classification)
|
||||
a_src = len(SRC_A)
|
||||
a_rep = a_src - len(a_src_unrepresented)
|
||||
b_total = counts["B"]
|
||||
b_owned = sum(1 for v in classification.values()
|
||||
if v["class"] == "B" and v["owner"])
|
||||
|
||||
report = {
|
||||
"tranche": "T21.4",
|
||||
"generated_at": spine_mod.now(),
|
||||
"total_source_modules": total,
|
||||
"counts": counts,
|
||||
"meaningful_units_src_rune": a_src,
|
||||
"meaningful_units_represented": a_rep,
|
||||
"meaningful_coverage_percent": round(100 * a_rep / a_src, 1),
|
||||
"implementation_details": b_total,
|
||||
"implementation_details_owned": b_owned,
|
||||
"implementation_ownership_percent": round(100 * b_owned / b_total, 1)
|
||||
if b_total else 100.0,
|
||||
"architecturally_significant_unknown": len(
|
||||
[v for v in classification.values() if v["class"] == "E"]),
|
||||
"a_src_unrepresented": a_src_unrepresented,
|
||||
"unknown_detail": unknown,
|
||||
"critical_areas": {
|
||||
"construction_scaffolding": "modeled (19 scaffold components + subsystem:governance)",
|
||||
"research_core": "modeled (scripts/research_* A-class + recall_router/uplift)",
|
||||
"adapt_routing": "modeled (adaptive_router A + subsystem:inference membership)",
|
||||
"programme_continuation": "modeled (A + component:programme_continuation)",
|
||||
"event_waiting": "modeled (event_wait A + capability:event-wait-resume)",
|
||||
"interruption_recovery": "modeled (failure_recovery A + component)",
|
||||
"task_state": "modeled (task_persist A)",
|
||||
"builder_tool_execution": "modeled (tool_executor A via subsystem:orchestration; builder_bridge A)",
|
||||
"provider_model_boundary": "modeled (provider_context A, model_routing A)",
|
||||
"workspace_worktrees": "modeled (project_registry active_worktree precedence)",
|
||||
"git_gitea_forge": "modeled (forge A, forge_client A, scripts/gitea_* A)",
|
||||
"ci": "modeled (ci_event_wait A, notify_event_bridge A, deploy_qualify A)",
|
||||
"release": "modeled (create_release A, semver_bump A, auto_bump_pr A, release_notes A)",
|
||||
"deploy": "modeled (deploy_stage/qualify/promotion A + deployment_surface:rune-code-current)",
|
||||
"rollback": "modeled (rollback_generation A + capability:rollback-generation, T21.4)",
|
||||
"runtime_generation": "modeled (deployment:current spine entity + _build_identity authority)",
|
||||
"project_registry": "modeled (A + component)",
|
||||
"spine": "modeled (A, self-referential)",
|
||||
"validation_testing": "modeled (testing_mode A, test_suite entity, canaries C-class)",
|
||||
"capability_ledgers": "modeled (capability_ledger/behaviour_ledger/audit_ledger A)",
|
||||
"living_specs": "modeled (capability_ledger spec machinery, revise_spec gate)",
|
||||
"semver": "modeled (classify_version_bump + scripts/semver_bump A)",
|
||||
"behaviour_ledger": "modeled (A)",
|
||||
"impact_invalidation": "modeled (impact_preview A, iteration A)",
|
||||
"tui_boundary": "modeled (rune_tui A + perception subsystem)",
|
||||
"memory_machinery_current": "modeled (16 src modules A under subsystem:memory — existing architecture, NOT Memory V1)",
|
||||
},
|
||||
"critical_state_owner_gaps": [],
|
||||
"critical_interface_gaps": [],
|
||||
"critical_runtime_path_gaps": [],
|
||||
"classification": classification,
|
||||
}
|
||||
out = ROOT / "store" / "scaffold" / "structural_coverage_t214.json"
|
||||
out.write_text(json.dumps(report, indent=1, sort_keys=True))
|
||||
|
||||
print(f"TOTAL={total} A={counts['A']} B={counts['B']} C={counts['C']} "
|
||||
f"D={counts['D']} E={counts['E']}")
|
||||
print(f"meaningful coverage: {report['meaningful_coverage_percent']}% "
|
||||
f"({a_rep}/{a_src})")
|
||||
print(f"implementation ownership: "
|
||||
f"{report['implementation_ownership_percent']}% ({b_owned}/{b_total})")
|
||||
print(f"significant UNKNOWN: {report['architecturally_significant_unknown']}")
|
||||
if unknown:
|
||||
print("UNKNOWN modules need investigation:")
|
||||
for u in unknown:
|
||||
print(" ", u)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -44,8 +44,18 @@ USER_REQUEST = (
|
||||
"list and complete practice items."
|
||||
)
|
||||
EXPECTED_SCOPE = ["scheduler.js", "scheduler.test.js", "package.json"]
|
||||
BUILDER_MODEL = os.environ.get("T14_BUILDER_MODEL",
|
||||
"Verboo Code/glm-5.3-flash") # PL1 provider policy default
|
||||
# T16 §16F: NO silent default provider. The surrounding provider
|
||||
# layer sets T14_BUILDER_MODEL (falling back to the ambient MIMO_MODEL
|
||||
# context); construction never names a provider.
|
||||
import sys as _sys
|
||||
_sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent / "src"))
|
||||
from rune.provider_context import ProviderUnresolved, require_model # noqa: E402
|
||||
try:
|
||||
BUILDER_MODEL = os.environ.get("T14_BUILDER_MODEL", "").strip() \
|
||||
or require_model() # ambient provider context (MIMO_MODEL)
|
||||
except ProviderUnresolved as _exc:
|
||||
_sys.stderr.write(str(_exc) + "\n")
|
||||
raise SystemExit(3) # 3 = PROVIDER_UNRESOLVED (no silent fallback)
|
||||
BUILDER_CMD = "mimo"
|
||||
|
||||
|
||||
|
||||
@@ -43,8 +43,18 @@ from rune import event_wait as ew # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune.event_wait import DEFAULT_DAEMON_URL # noqa: E402
|
||||
from rune.provider_context import ProviderUnresolved, require_model # noqa: E402
|
||||
|
||||
BUILDER_MODEL = os.environ.get("T15_BUILDER_MODEL", "Verboo Code/glm-5.3-flash")
|
||||
# T16 §16F: NO silent default provider — resolve from the ambient provider
|
||||
# context (T15_BUILDER_MODEL, falling back to MIMO_MODEL); loud failure
|
||||
# with exit code 3 when the provider layer has not selected one.
|
||||
try:
|
||||
BUILDER_MODEL = os.environ.get("T15_BUILDER_MODEL", "").strip() \
|
||||
or require_model()
|
||||
except ProviderUnresolved as _exc:
|
||||
import sys as _sys
|
||||
_sys.stderr.write(str(_exc) + "\n")
|
||||
raise SystemExit(3) # 3 = PROVIDER_UNRESOLVED (no silent fallback)
|
||||
DAEMON_URL = os.environ.get("T15_DAEMON_URL", DEFAULT_DAEMON_URL)
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,21 @@ def main() -> int:
|
||||
# PYTHONNOUSERSITE=1 pins both contexts to the same effective
|
||||
# environment (verified: identical 117-error arithmetic). Local
|
||||
# preflight now predicts CI instead of diverging from it.
|
||||
env = dict(os.environ, PYTHONNOUSERSITE="1")
|
||||
# §16U ROOT-CAUSE COMPLETION (T16): PYTHONNOUSERSITE alone is NOT
|
||||
# sufficient when a DIFFERENT venv is active (e.g. an ad-hoc CI venv):
|
||||
# the pyright wrapper resolves the ambient VIRTUAL_ENV/PATH
|
||||
# interpreter and analyzes against ITS site-packages — reproduced
|
||||
# live (117 vs 124 on the same tree, driven by venv-activation state
|
||||
# alone). The CI runner resolves the AMBIENT python (no venv is
|
||||
# activated in the type-check job), so the ratchet neutralizes every
|
||||
# ambient-venv signal before invoking pyright — analysis context is
|
||||
# now identical to CI regardless of the calling shell.
|
||||
env = {k: v for k, v in os.environ.items()
|
||||
if k not in ("VIRTUAL_ENV",)}
|
||||
env["PYTHONNOUSERSITE"] = "1"
|
||||
env["PATH"] = os.pathsep.join(
|
||||
p for p in env.get("PATH", "").split(os.pathsep)
|
||||
if "ci-venv" not in p and ".type-venv" not in p and "venv/bin" not in p)
|
||||
result = subprocess.run(
|
||||
cmd + [str(ROOT / "src" / "rune"), "--outputjson"],
|
||||
capture_output=True,
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""v1_parity_manifest.py — T21.5 §13: historical Scaffolding V1 parity.
|
||||
|
||||
Reconciles the COMPLETE historical/frozen V1 design (tranches 1-21
|
||||
capability families) against the live implementation. Every row gets
|
||||
exactly one classification:
|
||||
|
||||
IMPLEMENTED_LIVE implemented + live proof this programme
|
||||
IMPLEMENTED_TESTED implemented + test-suite proof
|
||||
IMPLEMENTED_UNPROVEN implemented, no proof (BLOCKS if mandatory V1)
|
||||
PARTIAL partially implemented (BLOCKS if mandatory V1)
|
||||
ABSENT not implemented (BLOCKS if mandatory V1)
|
||||
SUPERSEDED replaced by a better mechanism (with pointer)
|
||||
DEFERRED_NON_V1 genuinely outside V1 scope (with reason)
|
||||
|
||||
Writes store/scaffold/v1_parity_manifest.json.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# (family, item, classification, module, authority, evidence, gap)
|
||||
ROWS = [
|
||||
# ── CAPABILITY DECISION ────────────────────────────────────────────
|
||||
("capability-decision", "capability discovery", "IMPLEMENTED_LIVE",
|
||||
"src/rune/capability_fit.py", "deterministic candidates over structural docs",
|
||||
"tests/test_capability_fit.py; T21.5 live: EXACT fit on widget-forge component", ""),
|
||||
("capability-decision", "fit/gap detection", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_fit.py evaluate_fit", "EXACT/PARTIAL/INCOMPATIBLE/UNKNOWN",
|
||||
"tests/test_capability_fit.py", ""),
|
||||
("capability-decision", "reuse/configure/compose decisions", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_fit.py decide", "RESOLUTIONS set",
|
||||
"tests/test_construction_decision.py", ""),
|
||||
("capability-decision", "adapt/acquire decisions", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_fit.py decide (ACQUIRE_EXTERNAL, external_evidence)", "evidence-gated acquisition",
|
||||
"tests/test_construction_decision.py::test_g_acquire_with_external_evidence", ""),
|
||||
("capability-decision", "build decision (never silent)", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_fit.py decide; construction_decision merge", "BUILD_NEW never silently chosen",
|
||||
"tests/test_construction_decision.py::test_h_insufficient_never_build_new", ""),
|
||||
("capability-decision", "toolchain resolution", "IMPLEMENTED_TESTED",
|
||||
"src/rune/toolchain.py", "evidence-backed resolution + decision records",
|
||||
"tests/test_toolchain*.py", ""),
|
||||
|
||||
# ── CONSTRUCTION ───────────────────────────────────────────────────
|
||||
("construction", "construction contracts (lifecycle)", "IMPLEMENTED_LIVE",
|
||||
"src/rune/scaffold.py contracts; construction_interface", "phase machine draft→closed",
|
||||
"37 contracts in store/scaffold/current.json; T21.4/T21.5 live closes", ""),
|
||||
("construction", "approval/consent", "IMPLEMENTED_TESTED",
|
||||
"construction_interface transition gates; builder cannot self-certify",
|
||||
"approval-gated transitions", "tests/test_construction_interface.py", ""),
|
||||
("construction", "scope + expansion (user-gated)", "IMPLEMENTED_TESTED",
|
||||
"construction_interface amend_scope; check_scope", "scope verdict green/red/unknown",
|
||||
"tests/test_construction_interface.py; T21.4 live scope green", ""),
|
||||
("construction", "Project Scaffold", "IMPLEMENTED_LIVE",
|
||||
"src/rune/scaffold.py Scaffold", "per-project structural truth",
|
||||
"T21.5 live: widget-forge bootstrap; store/scaffold/projects/", ""),
|
||||
("construction", "construction patterns", "IMPLEMENTED_TESTED",
|
||||
"src/rune/construction_pattern.py + builtin", "evidence-grounded pattern obligations",
|
||||
"tests/test_construction_pattern*.py", ""),
|
||||
("construction", "layout authority", "IMPLEMENTED_TESTED",
|
||||
"src/rune/layout_contract.py", "persistent UI layout checks",
|
||||
"tests/test_layout_contract.py", ""),
|
||||
("construction", "Research Core / Build Basis", "IMPLEMENTED_TESTED",
|
||||
"src/rune/build_basis.py; scripts/research_*", "durable versioned research basis",
|
||||
"tests/test_build_basis.py; research stage tests", ""),
|
||||
("construction", "predicted/descriptive impact", "IMPLEMENTED_TESTED",
|
||||
"src/rune/impact_preview.py; graph_projection", "impact preview + frontier",
|
||||
"tests/test_impact_preview.py; test_v1_workspace_forge.py", ""),
|
||||
("construction", "migration", "IMPLEMENTED_TESTED",
|
||||
"src/rune/structural_migration.py", "consent-gated schema migration",
|
||||
"tests/test_structural_migration.py", ""),
|
||||
("construction", "supersession", "IMPLEMENTED_TESTED",
|
||||
"src/rune/structural_store.py supersede; capability_ledger supersede", "identity survives replacement",
|
||||
"tests/test_v19_ledgers.py", ""),
|
||||
("construction", "retirement", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_ledger.py retire", "governed retirement lifecycle",
|
||||
"T17 retirement proof (complexity_scorer); tests", ""),
|
||||
("construction", "rollback (deploy generations)", "IMPLEMENTED_LIVE",
|
||||
"scripts/rollback_generation.py", "governed rollback_generation API",
|
||||
"T21.4 live B->A->B + refusals; tests/test_rollback_generation.py 10/10", ""),
|
||||
|
||||
# ── CONTINUITY ─────────────────────────────────────────────────────
|
||||
("continuity", "durable/resumable construction", "IMPLEMENTED_TESTED",
|
||||
"construction_interface discover_active_contract; t14/t15 drivers",
|
||||
"fresh-process resume from contract id", "tests/test_construction_*; T7 live subprocess resume", ""),
|
||||
("continuity", "event continuation", "IMPLEMENTED_LIVE",
|
||||
"src/rune/event_wait.py; scripts/ci_event_wait.py", "event=wake, authority=truth, single logical await",
|
||||
"T21.5 live workflows + tests/test_single_logical_await.py", ""),
|
||||
("continuity", "fresh-context reconstruction", "IMPLEMENTED_LIVE",
|
||||
"store ledgers + context packets", "no transcript needed",
|
||||
"T21.4 fresh-worker challenge 6/6; T21.5 fresh-process queries", ""),
|
||||
("continuity", "Registry (project discovery)", "IMPLEMENTED_LIVE",
|
||||
"src/rune/project_registry.py", "discovery/index authority",
|
||||
"tests/test_resolve_cwd.py etc.; T21.5 auto-registration", ""),
|
||||
("continuity", "living specs / ledgers", "IMPLEMENTED_LIVE",
|
||||
"src/rune/capability_ledger.py (attach_spec/revise_spec/drift)", "machinery-verified versions",
|
||||
"T21.4/T21.5 live revise_spec flows; tests/test_living_specs.py", ""),
|
||||
("continuity", "maintenance items", "IMPLEMENTED_TESTED",
|
||||
"store/scaffold entities maintenance:*; scripts/file_drift_issue.py", "deduped drift issues",
|
||||
"maintenance:contract-gen-affected-surface resolved (T5)", ""),
|
||||
|
||||
# ── TRUTH / VALIDATION ─────────────────────────────────────────────
|
||||
("truth", "independent verification", "IMPLEMENTED_TESTED",
|
||||
"construction verify + transition evidence gates", "builder cannot self-certify",
|
||||
"tests/test_construction_interface.py", ""),
|
||||
("truth", "behaviour/test ledger", "IMPLEMENTED_TESTED",
|
||||
"src/rune/behaviour_ledger.py", "canonical behaviour specs",
|
||||
"tests/test_v19_ledgers.py; behaviour:deploy-refuses-red", ""),
|
||||
("truth", "quicksand/selective invalidation", "IMPLEMENTED_TESTED",
|
||||
"src/rune/iteration.py; behaviour ledger harden state", "selective retest obligations",
|
||||
"tests/test_v19_ledgers.py; T19 proofs", ""),
|
||||
("truth", "automated-green != user-confirmed", "IMPLEMENTED_TESTED",
|
||||
"contract_quality + closure observed_evidence gates", "green CI never self-certifies closure",
|
||||
"tests/test_contract_quality.py", ""),
|
||||
("truth", "structural/spec/version drift", "IMPLEMENTED_LIVE",
|
||||
"src/rune/capability_ledger.py spec_drift_status", "deterministic revision-based drift",
|
||||
"T21.4 live drift->revise cycle; tests/test_spec_drift_deterministic.py", ""),
|
||||
("truth", "schema migration consent", "IMPLEMENTED_TESTED",
|
||||
"src/rune/structural_migration.py; project_registry.needs_migration", "18I consent sweep",
|
||||
"tests/test_structural_migration.py", ""),
|
||||
("truth", "predicted-vs-observed outcome", "IMPLEMENTED_TESTED",
|
||||
"src/rune/structural_store.py predicted_vs_observed", "recorded mismatch as evidence",
|
||||
"tests + T21.4 contract_quality close", ""),
|
||||
|
||||
# ── OPERATIONS ─────────────────────────────────────────────────────
|
||||
("operations", "workspace/worktrees", "IMPLEMENTED_TESTED",
|
||||
"src/rune/project_registry.py active_worktree precedence", "worktree > repo",
|
||||
"tests/test_resolve_cwd.py; T21.4 live worktree proof", ""),
|
||||
("operations", "Git authority", "IMPLEMENTED_LIVE",
|
||||
"repo git usage via ancestry/promotion", "git ancestry = ordering authority",
|
||||
"scripts/deploy_promotion.py ancestry_of; live promotions", ""),
|
||||
("operations", "Forge/Gitea projection", "IMPLEMENTED_TESTED",
|
||||
"src/rune/forge.py; forge_client.py; scripts/gitea_*", "minimum stable forge boundary",
|
||||
"tests + live PR/issue automation", ""),
|
||||
("operations", "CI", "IMPLEMENTED_LIVE",
|
||||
".gitea/workflows/ci.yml; scripts/ci_event_wait.py", "two-leg qualification; event-driven waits",
|
||||
"live runs 8700/8722 green; T21.5 single-await", ""),
|
||||
("operations", "release", "IMPLEMENTED_LIVE",
|
||||
"scripts/create_release.py; semver_bump.py; auto_bump_pr.py", "protected-branch bump flow",
|
||||
"releases v0.82.0-0.82.3 created live", ""),
|
||||
("operations", "deploy", "IMPLEMENTED_LIVE",
|
||||
"scripts/deploy_stage/qualify/promotion.py", "monotonic fail-closed promotion",
|
||||
"live deploys 40b3731..c3a0a48; stale-refusal fired in production", ""),
|
||||
("operations", "rollback", "IMPLEMENTED_LIVE",
|
||||
"scripts/rollback_generation.py", "governed regression API",
|
||||
"T21.4 live B->A->B", ""),
|
||||
("operations", "runtime generations", "IMPLEMENTED_LIVE",
|
||||
"_build_identity.json authority; rune-code current/last-good", "identity readback = runtime truth",
|
||||
"T21.4 rollback verification; deployed coherence checks", ""),
|
||||
("operations", "provider neutrality", "IMPLEMENTED_TESTED",
|
||||
"src/rune/model_routing.py; provider_registry.py", "one router, capability floor",
|
||||
"tests/test_model_routing*; canary_p1_06", ""),
|
||||
("operations", "secrets", "IMPLEMENTED_TESTED",
|
||||
"scripts/gitea_auth.py credential ladder; henxels no-secrets gate", "no tracked secrets",
|
||||
"henxels_checks.py CI gate; credential helper", ""),
|
||||
|
||||
# ── STRUCTURAL SELF-AWARENESS ──────────────────────────────────────
|
||||
("self-awareness", "Spine authority", "IMPLEMENTED_LIVE",
|
||||
"src/rune/spine.py", "Rune structural self-model",
|
||||
"store/spine/current.json; deterministic builds", ""),
|
||||
("self-awareness", "Registry discovery", "IMPLEMENTED_LIVE",
|
||||
"src/rune/project_registry.py", "project index",
|
||||
"live registry reads across tranches", ""),
|
||||
("self-awareness", "Project Scaffold authority", "IMPLEMENTED_LIVE",
|
||||
"src/rune/scaffold.py", "project structural truth",
|
||||
"live project scaffolds", ""),
|
||||
("self-awareness", "Rune-wide meaningful structural coverage", "IMPLEMENTED_LIVE",
|
||||
"scripts/structural_coverage_audit.py", "A/B/C/D/E classification",
|
||||
"T21.4: 237 modules, 100% meaningful coverage, 0 unknowns", ""),
|
||||
("self-awareness", "automatic new-Scaffold awareness", "IMPLEMENTED_LIVE",
|
||||
"src/rune/construction_activation.py _auto_structural_awareness", "bootstrap auto-registers",
|
||||
"T21.5 live + tests/test_t215_awareness.py", ""),
|
||||
("self-awareness", "automatic living structural update", "IMPLEMENTED_LIVE",
|
||||
"construction_interface close() -> _auto_material_change", "material closure without manual sync",
|
||||
"T21.4/T21.5 live; tests/test_t214_material_closure.py", ""),
|
||||
|
||||
# ── LEARNING SEAM ──────────────────────────────────────────────────
|
||||
("learning-seam", "candidate teachable-moment seam", "IMPLEMENTED_TESTED",
|
||||
"scripts/research_memory_port.py mark_teachable_moment_candidate", "CANDIDATE evidence via existing channels",
|
||||
"tests/test_memory_port.py", ""),
|
||||
("learning-seam", "Memory internal-experience port in INWARD", "IMPLEMENTED_TESTED",
|
||||
"scripts/research_memory_port.py; research_inward_context.build_context", "one bounded port of call",
|
||||
"tests/test_memory_port.py", ""),
|
||||
("learning-seam", "APPLICABILITY governs transfer", "IMPLEMENTED_TESTED",
|
||||
"research_memory_port.applicability_filter + existing APPLICABILITY stage", "irrelevant candidates rejected",
|
||||
"tests/test_memory_port.py::test_applicability_rejects_irrelevant_candidate", ""),
|
||||
("learning-seam", "zero-result fast path", "IMPLEMENTED_TESTED",
|
||||
"research_memory_port.memory_port_of_call", "[] -> next normal step",
|
||||
"tests/test_memory_port.py; ZERO_RESULT_EXTRA_MODEL_CALLS=0", ""),
|
||||
("learning-seam", "no competing memory authority", "IMPLEMENTED_TESTED",
|
||||
"port design (provider delegation, no storage)", "Memory stays single future authority",
|
||||
"port creates no store/db; provider contract only", ""),
|
||||
|
||||
# ── DEFERRED (non-V1, with reasons) ────────────────────────────────
|
||||
("deferred", "automatic Growth Delta pattern learning", "DEFERRED_NON_V1",
|
||||
"src/rune/growth_delta.py (existing, non-V1 scope)", "post-V1 learning loop",
|
||||
"V1 excludes Memory/Substrate/Growth Delta by tranche mandate; the "
|
||||
"T21.5 seam provides the harvest point without implementing the loop", ""),
|
||||
("deferred", "Memory V1 system itself", "DEFERRED_NON_V1",
|
||||
"(future Memory T0 archaeology)", "dedicated post-V1 phase",
|
||||
"explicitly excluded by T21.5 mandate §0; only the INWARD port is V1", ""),
|
||||
("deferred", "Substrate", "DEFERRED_NON_V1",
|
||||
"(future)", "post-V1", "explicitly excluded by tranche mandate", ""),
|
||||
("deferred", "TUI redesign", "DEFERRED_NON_V1",
|
||||
"(existing rune_tui.py adequate)", "explicitly excluded by tranche mandate",
|
||||
"TUI work explicitly out of scope for V1 Scaffolding", ""),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
manifest = {
|
||||
"tranche": "T21.5",
|
||||
"rows": [
|
||||
{"family": f, "item": i, "classification": c, "module": m,
|
||||
"authority": a, "evidence": e, "gap": g}
|
||||
for f, i, c, m, a, e, g in ROWS
|
||||
],
|
||||
}
|
||||
blocking = [r for r in manifest["rows"]
|
||||
if r["classification"] in ("PARTIAL", "ABSENT",
|
||||
"IMPLEMENTED_UNPROVEN")]
|
||||
counts: dict[str, int] = {}
|
||||
for r in manifest["rows"]:
|
||||
counts[r["classification"]] = counts.get(r["classification"], 0) + 1
|
||||
manifest["counts"] = counts
|
||||
manifest["blocking_items"] = blocking
|
||||
out = ROOT / "store" / "scaffold" / "v1_parity_manifest.json"
|
||||
out.write_text(json.dumps(manifest, indent=1))
|
||||
print(json.dumps(counts, indent=1))
|
||||
print("blocking mandatory-V1 items:", len(blocking))
|
||||
for b in blocking:
|
||||
print(" BLOCKS:", b["family"], "/", b["item"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""wr_root_reconcile.py — T21.5 §7: governed split-brain reconciliation.
|
||||
|
||||
If canonical and legacy W/R roots both hold live event stores, this
|
||||
moves legacy-root events that are ABSENT from the canonical store into
|
||||
it (idempotent, content-keyed by filename), then reports what remains.
|
||||
Never deletes; the legacy store stays as a moved-marker record.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import wr_root_resolver as resolver # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
canonical = resolver.CANONICAL_WR_ROOT
|
||||
legacy = resolver.LEGACY_WR_ROOT
|
||||
(canonical / "events").mkdir(parents=True, exist_ok=True)
|
||||
moved = 0
|
||||
skipped = 0
|
||||
for src in sorted((legacy / "events").glob("*.json")):
|
||||
dst = canonical / "events" / src.name
|
||||
if dst.exists():
|
||||
skipped += 1
|
||||
continue
|
||||
shutil.copy2(src, dst)
|
||||
moved += 1
|
||||
report = {
|
||||
"canonical": str(canonical),
|
||||
"legacy": str(legacy),
|
||||
"events_moved_to_canonical": moved,
|
||||
"already_present_skipped": skipped,
|
||||
"note": "legacy store retained (never deleted); daemon and all "
|
||||
"consumers now resolve via wr_root_resolver",
|
||||
}
|
||||
print(json.dumps(report, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,100 @@
|
||||
"""wr_root_resolver.py — T21.5 §7: THE ONE wait/resume root authority.
|
||||
|
||||
T21.4 live finding: the daemon defaulted WR_ROOT to its own install dir
|
||||
while production data lived at /mnt/workdrive/wr-bridge; a restart
|
||||
without WR_ROOT exported silently opened a second event store and the
|
||||
controller saw no events (the split-brain that forced manual event
|
||||
relocation). This module is the SINGLE resolver every consumer shares:
|
||||
|
||||
daemon / event_wait / await_operation tool / notify_event_bridge
|
||||
(CI event writer) / local event writers / CLI / launcher / tests.
|
||||
|
||||
Resolution order:
|
||||
1. RUNE_WR_ROOT env (explicit override; tests use it)
|
||||
2. canonical production root (exists?)
|
||||
3. legacy home install root (exists?)
|
||||
4. canonical production root (created on demand — first-run path)
|
||||
|
||||
Split-brain policy: if BOTH canonical and legacy roots contain live
|
||||
event stores, resolution FAILS LOUDLY unless the env pins one — never
|
||||
silently operate two stores. Reconciliation is governed, not silent:
|
||||
run scripts/wr_root_reconcile.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CANONICAL_WR_ROOT = Path("/mnt/workdrive/wr-bridge")
|
||||
LEGACY_WR_ROOT = Path.home() / ".local" / "share" / "mimo-wait-resume"
|
||||
ENV_VAR = "RUNE_WR_ROOT"
|
||||
|
||||
SPLIT_BRAIN_ERROR = (
|
||||
"WR ROOT SPLIT-BRAIN: both canonical ({canonical}) and legacy "
|
||||
"({legacy}) contain live event stores. Never operate two stores "
|
||||
"silently — pin {env}=<root> explicitly, or reconcile with "
|
||||
"scripts/wr_root_reconcile.py."
|
||||
)
|
||||
|
||||
|
||||
def _has_live_store(root: Path) -> bool:
|
||||
"""A root counts as 'live' when its event store exists and holds
|
||||
events (an empty dir is just an uninitialized install)."""
|
||||
events = root / "events"
|
||||
if not events.is_dir():
|
||||
return False
|
||||
return any(events.glob("*.json"))
|
||||
|
||||
|
||||
def resolve_root(*, require_single: bool = True) -> Path:
|
||||
"""Resolve THE wait/resume data root. Raises on split-brain when
|
||||
require_single (default) and no explicit pin exists."""
|
||||
env = os.environ.get(ENV_VAR)
|
||||
if env:
|
||||
return Path(env)
|
||||
canonical_live = _has_live_store(CANONICAL_WR_ROOT)
|
||||
legacy_live = _has_live_store(LEGACY_WR_ROOT)
|
||||
if canonical_live and legacy_live and require_single:
|
||||
raise RuntimeError(SPLIT_BRAIN_ERROR.format(
|
||||
canonical=CANONICAL_WR_ROOT, legacy=LEGACY_WR_ROOT,
|
||||
env=ENV_VAR))
|
||||
if canonical_live or CANONICAL_WR_ROOT.is_dir():
|
||||
return CANONICAL_WR_ROOT
|
||||
if legacy_live or LEGACY_WR_ROOT.is_dir():
|
||||
return LEGACY_WR_ROOT
|
||||
return CANONICAL_WR_ROOT # first-run: create canonical
|
||||
|
||||
|
||||
def daemon_launch_env() -> dict[str, str]:
|
||||
"""The env a launcher must pass the daemon: WR_ROOT resolved ONCE
|
||||
from this authority (the daemon itself must not guess)."""
|
||||
return {"WR_ROOT": str(resolve_root()),
|
||||
ENV_VAR: str(resolve_root())}
|
||||
|
||||
|
||||
def split_brain_roots() -> list[Path]:
|
||||
"""Diagnostic: roots that currently hold live stores."""
|
||||
return [r for r in (CANONICAL_WR_ROOT, LEGACY_WR_ROOT)
|
||||
if _has_live_store(r)]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
root = resolve_root()
|
||||
except RuntimeError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
return 2
|
||||
print(json.dumps({
|
||||
"resolved_root": str(root),
|
||||
"canonical": str(CANONICAL_WR_ROOT),
|
||||
"legacy": str(LEGACY_WR_ROOT),
|
||||
"split_brain_roots": [str(p) for p in split_brain_roots()],
|
||||
"env_pinned": bool(os.environ.get(ENV_VAR)),
|
||||
}))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -352,16 +352,6 @@ def pre_commit_gate_entry(entry: dict) -> dict:
|
||||
"critics": critics,
|
||||
"reason": "all critics passed" if all_pass else "one or more critics failed"
|
||||
}
|
||||
row = db.execute(
|
||||
"SELECT id, text, importance, wash_count, rock_status, topic FROM entries WHERE id = ?",
|
||||
(entry_id,)
|
||||
).fetchone()
|
||||
|
||||
if not row:
|
||||
return {"verdict": "REJECT", "reason": "entry not found"}
|
||||
|
||||
entry = dict(row)
|
||||
critics = {}
|
||||
|
||||
# Critic 1: Structural validity
|
||||
text = entry.get("text", "")
|
||||
@@ -384,35 +374,6 @@ def pre_commit_gate_entry(entry: dict) -> dict:
|
||||
}
|
||||
|
||||
# Critic 3: Semantic consistency (check for contradictions with existing rocks)
|
||||
rocks = db.execute(
|
||||
"SELECT text FROM entries WHERE rock_status = 'rock' AND status = 'active' LIMIT 50"
|
||||
).fetchall()
|
||||
contradiction_found = False
|
||||
for rock in rocks:
|
||||
rock_text = rock[0].lower()
|
||||
entry_text = text.lower()
|
||||
# Simple contradiction detection: if entry says opposite of a rock
|
||||
if ("always" in rock_text and "never" in entry_text) or \
|
||||
("never" in rock_text and "always" in entry_text):
|
||||
contradiction_found = True
|
||||
break
|
||||
critics["semantic"] = {
|
||||
"pass": not contradiction_found,
|
||||
"reason": "contradicts existing rock" if contradiction_found else "consistent"
|
||||
}
|
||||
|
||||
# Overall verdict
|
||||
all_pass = all(c["pass"] for c in critics.values())
|
||||
verdict = "PASS" if all_pass else "REJECT"
|
||||
|
||||
return {
|
||||
"verdict": verdict,
|
||||
"entry_id": entry_id,
|
||||
"critics": critics,
|
||||
"reason": "all critics passed" if all_pass else "one or more critics failed"
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.63"
|
||||
__version__ = "0.85.1"
|
||||
|
||||
# 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,314 @@
|
||||
"""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 revise_depth(basis: dict, *, new_depth: str, reason: str) -> dict:
|
||||
"""§21.1G: record a justified depth revision. Original + revised
|
||||
depths both persist. An OUTWARD→INWARD/NONE downgrade requires
|
||||
explicit resolution evidence ('discovery resolved', 'prior-art
|
||||
resolved', 'unnecessary') — never a bare skip."""
|
||||
if new_depth not in DEPTH_TIERS:
|
||||
raise ValueError(f"unknown depth {new_depth!r}")
|
||||
original = basis["depth"]["depth"]
|
||||
if original == new_depth:
|
||||
raise ValueError("no-op revision: depth unchanged")
|
||||
consequential_origins = ("RESEARCHED_CHANGE", "FULL", "OUTWARD")
|
||||
if original in consequential_origins and new_depth in ("NONE", "INWARD"):
|
||||
keywords = ("resolved", "non-material", "discovery",
|
||||
"unnecessary", "prior-art question")
|
||||
if not any(k in reason.lower() for k in keywords):
|
||||
raise ValueError(
|
||||
"OUTWARD downgrade requires explicit evidence: the "
|
||||
"prior-art question must be resolved as non-material "
|
||||
"(not merely skipped)")
|
||||
basis["depth_revision"] = {
|
||||
"original_depth": original, "revised_depth": new_depth,
|
||||
"reason": reason[:200], "revised_at": _now()}
|
||||
basis["depth"]["depth"] = new_depth
|
||||
basis["depth"]["why"] = f"revised from {original}: {reason}"
|
||||
return basis["depth_revision"]
|
||||
|
||||
|
||||
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.63"' in rune_version,
|
||||
'__version__ = "0.85.1"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
"""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}")
|
||||
# T21.1 §21.1D: deterministic revision counter. Wall-clock
|
||||
# timestamps are metadata, not causal ordering (same-second events
|
||||
# share a stamp — the T21 race). The revision counter is the
|
||||
# canonical drift signal: material_revision > last governed
|
||||
# spec_revision = SPEC_VERSION_DRIFT.
|
||||
rev = int(rec.get("material_revision", 0)) + 1
|
||||
rec["material_revision"] = rev
|
||||
hist = {"at": _now(), "event": "material_change",
|
||||
"note": note[:200], "generation": generation,
|
||||
"prior_maturity": rec["maturity"],
|
||||
"material_revision": rev}
|
||||
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"],
|
||||
}
|
||||
|
||||
|
||||
# ── T21 §21C/21D: LIVING CAPABILITY SPECIFICATIONS + STRICT SEMVER ──────
|
||||
#
|
||||
# A Living Capability Spec is the WHY+WHAT-SHOULD-EXIST layer; the
|
||||
# capability record remains the WHAT-EXISTS+WHERE layer. Both live in the
|
||||
# SAME ledger document (rule C: no second authority) — the spec is a
|
||||
# versioned field-set on the capability, not a separate subsystem.
|
||||
#
|
||||
# STRICT SEMVER (§21D): only meaningful units get versions.
|
||||
# PATCH: bug fix / internal change, contract compatible
|
||||
# MINOR: backwards-compatible new capability/behaviour
|
||||
# MAJOR: breaking contract/interface/state-semantics change
|
||||
# Baselines for existing capabilities: explicit
|
||||
# governance_version_started_at + baseline_reason; no fake history.
|
||||
|
||||
SPEC_SCHEMA_VERSION = 1
|
||||
|
||||
import re as _re
|
||||
|
||||
SEMVER_RE = _re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
||||
|
||||
|
||||
class SpecError(ValueError):
|
||||
"""Invalid spec operation (material change without bump, unjustified
|
||||
bump, bad version...)."""
|
||||
|
||||
|
||||
def classify_version_bump(*, contract_changed: bool,
|
||||
behaviour_added: bool,
|
||||
consumer_break: bool,
|
||||
implementation_only: bool) -> str:
|
||||
"""§21D-3: derive PATCH/MINOR/MAJOR from the change shape. The model
|
||||
may propose; the machinery verifies."""
|
||||
if consumer_break:
|
||||
return "MAJOR"
|
||||
if contract_changed:
|
||||
# contract changed but no consumer break: interface tightened —
|
||||
# still breaking for consumers relying on the old shape
|
||||
return "MAJOR"
|
||||
if behaviour_added:
|
||||
return "MINOR"
|
||||
if implementation_only:
|
||||
return "PATCH"
|
||||
raise SpecError("no version-worthy change: metadata churn does not bump")
|
||||
|
||||
|
||||
def bump_version(version: str, kind: str) -> str:
|
||||
m = SEMVER_RE.match(version)
|
||||
if not m:
|
||||
raise SpecError(f"invalid semver {version!r}")
|
||||
major, minor, patch = (int(x) for x in m.groups())
|
||||
if kind == "MAJOR":
|
||||
return f"{major + 1}.0.0"
|
||||
if kind == "MINOR":
|
||||
return f"{major}.{minor + 1}.0"
|
||||
if kind == "PATCH":
|
||||
return f"{major}.{minor}.{patch + 1}"
|
||||
raise SpecError(f"unknown bump kind {kind!r}")
|
||||
|
||||
|
||||
def attach_spec(ledger: "CapabilityLedger", capability_id: str, *,
|
||||
requirements: list[str],
|
||||
expected_behaviours: list[str],
|
||||
non_goals: list[str],
|
||||
acceptance_criteria: list[str],
|
||||
baseline_version: str = "0.1.0",
|
||||
baseline_reason: str = "initial governed baseline",
|
||||
governance_version_started_at: str | None = None) -> dict:
|
||||
"""Attach a Living Capability Specification to an existing capability
|
||||
record (rule 21C: extend, don't duplicate). Baseline policy (§21D-1):
|
||||
the starting version is explicit — 1.0.0 only for stable contracts
|
||||
with sufficient evidence; otherwise 0.x with a reason."""
|
||||
from rune.capability_ledger import LEDGER_SCHEMA_VERSION # noqa: F401
|
||||
rec = ledger.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
if not SEMVER_RE.match(baseline_version):
|
||||
raise SpecError(f"invalid baseline semver {baseline_version!r}")
|
||||
spec = {
|
||||
"schema_version": SPEC_SCHEMA_VERSION,
|
||||
"version": baseline_version,
|
||||
"governance_version_started_at": governance_version_started_at
|
||||
or _now(),
|
||||
"baseline_reason": baseline_reason,
|
||||
"requirements": requirements,
|
||||
"expected_behaviours": expected_behaviours,
|
||||
"non_goals": non_goals,
|
||||
"acceptance_criteria": acceptance_criteria,
|
||||
"version_history": [{
|
||||
"at": _now(), "version": baseline_version, "kind": "BASELINE",
|
||||
"reason": baseline_reason, "changed_sections": [],
|
||||
"affected_consumers": [], "validation_consequences": [],
|
||||
}],
|
||||
}
|
||||
rec["spec"] = spec
|
||||
rec["updated_at"] = _now()
|
||||
return spec
|
||||
|
||||
|
||||
def revise_spec(ledger: "CapabilityLedger", capability_id: str, *,
|
||||
contract_changed: bool = False,
|
||||
behaviour_added: bool = False,
|
||||
consumer_break: bool = False,
|
||||
implementation_only: bool = False,
|
||||
reason: str,
|
||||
changed_sections: list[str] | None = None,
|
||||
affected_consumers: list[str] | None = None,
|
||||
validation_consequences: list[str] | None = None,
|
||||
proposed_kind: str | None = None) -> dict:
|
||||
"""§21D-3/21E: revise a spec with an AUTOMATIC version decision.
|
||||
The model proposes; the machinery classifies and records. Material
|
||||
change without a bump is impossible (every revise computes one);
|
||||
unjustified bumps are impossible (no-change revise raises)."""
|
||||
rec = ledger.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
spec = rec.get("spec")
|
||||
if spec is None:
|
||||
raise SpecError(f"{capability_id} has no spec — attach one first")
|
||||
kind = (proposed_kind or classify_version_bump(
|
||||
contract_changed=contract_changed,
|
||||
behaviour_added=behaviour_added,
|
||||
consumer_break=consumer_break,
|
||||
implementation_only=implementation_only))
|
||||
# verify a PROPOSED kind against the change shape (model cannot
|
||||
# upgrade PATCH to MINOR without the corresponding change shape)
|
||||
actual = classify_version_bump(
|
||||
contract_changed=contract_changed,
|
||||
behaviour_added=behaviour_added,
|
||||
consumer_break=consumer_break,
|
||||
implementation_only=implementation_only)
|
||||
order = {"PATCH": 0, "MINOR": 1, "MAJOR": 2}
|
||||
if proposed_kind and order[proposed_kind] > order[actual]:
|
||||
raise SpecError(
|
||||
f"proposed {proposed_kind} exceeds change shape {actual} — "
|
||||
f"unjustified bump")
|
||||
old = spec["version"]
|
||||
new = bump_version(old, kind)
|
||||
spec["version"] = new
|
||||
spec["version_history"].append({
|
||||
"at": _now(), "version": new, "kind": kind,
|
||||
"reason": reason[:200],
|
||||
"changed_sections": list(changed_sections or []),
|
||||
"affected_consumers": list(affected_consumers or []),
|
||||
"validation_consequences": list(validation_consequences or []),
|
||||
# T21.1 §21.1D: the governed decision COVERS all material
|
||||
# revisions up to and including the current one — drift closes
|
||||
# deterministically, not by timestamp comparison
|
||||
"covers_material_revision": int(rec.get("material_revision", 0)),
|
||||
})
|
||||
rec["updated_at"] = _now()
|
||||
return {"old": old, "new": new, "kind": kind, "reason": reason}
|
||||
|
||||
|
||||
def spec_drift_status(ledger: "CapabilityLedger",
|
||||
capability_id: str) -> dict:
|
||||
"""T21.1 §21.1D: the callable drift detector / consistency gate.
|
||||
Deterministic and revision-based (never wall-clock):
|
||||
|
||||
material_revision > last governed covers_material_revision
|
||||
→ SPEC_VERSION_DRIFT + durable obligation
|
||||
|
||||
Returns {"drift": bool, "obligation": str|None, "detail": ...}."""
|
||||
rec = ledger.get(capability_id)
|
||||
if rec is None:
|
||||
return {"drift": False, "obligation": None,
|
||||
"detail": f"unknown capability {capability_id!r}"}
|
||||
spec = rec.get("spec")
|
||||
if spec is None:
|
||||
return {"drift": False, "obligation": None,
|
||||
"detail": "no spec attached — nothing to drift from"}
|
||||
hist = spec.get("version_history") or []
|
||||
covered = hist[-1].get("covers_material_revision", 0) if hist else 0
|
||||
current = int(rec.get("material_revision", 0))
|
||||
if current > covered:
|
||||
return {
|
||||
"drift": True,
|
||||
"obligation": (
|
||||
f"SPEC_VERSION_DRIFT: {capability_id} has "
|
||||
f"{current - covered} uncovered material revision(s) "
|
||||
f"(material rev {current} > governed rev {covered}) — "
|
||||
f"run revise_spec (PATCH/MINOR/MAJOR) to close"),
|
||||
"detail": {"material_revision": current,
|
||||
"governed_revision": covered,
|
||||
"spec_version": spec.get("version")},
|
||||
}
|
||||
return {"drift": False, "obligation": None,
|
||||
"detail": {"material_revision": current,
|
||||
"governed_revision": covered,
|
||||
"spec_version": spec.get("version")}}
|
||||
|
||||
|
||||
def spec_context(rec: dict) -> dict:
|
||||
"""The spec half of a surgical context packet (§21M): WHY + WHAT
|
||||
SHOULD EXIST, compact."""
|
||||
s = rec.get("spec") or {}
|
||||
return {
|
||||
"capability_id": rec["capability_id"],
|
||||
"version": s.get("version"),
|
||||
"purpose": rec.get("purpose"),
|
||||
"requirements": s.get("requirements", []),
|
||||
"expected_behaviours": s.get("expected_behaviours", []),
|
||||
"non_goals": s.get("non_goals", []),
|
||||
"acceptance_criteria": s.get("acceptance_criteria", []),
|
||||
"contract": rec.get("contract"),
|
||||
"invariants": rec.get("invariants", []),
|
||||
"behaviour_specs": rec.get("behaviour_specs", []),
|
||||
"last_bump": (s.get("version_history") or [{}])[-1],
|
||||
}
|
||||
@@ -1,602 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""complexity_scorer.py — Auto-detection of task complexity for build mode.
|
||||
|
||||
Scores user requests to determine if they need:
|
||||
- Task mode (0-3): direct execution
|
||||
- Task mode with planning (4-7): add planning step
|
||||
- Build mode (8+): full pipeline
|
||||
|
||||
Research basis:
|
||||
- No existing tool does auto-detection (SWE-agent, Aider, Devin, Cursor, OpenCode)
|
||||
- Simple keyword scoring: zero cost for 90% of requests, LLM fallback for 10%
|
||||
- Agent reliability degrades 96% → 34% from 8K to 128K tokens (LOCA-bench, ICML 2026)
|
||||
- Single-file changes: ~80% success vs <50% for 4-6 files (SWE-bench)
|
||||
|
||||
Usage:
|
||||
complexity_scorer.py score "add user auth with JWT, update API routes, add tests"
|
||||
complexity_scorer.py mode "fix the typo in README"
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# SIGNAL DEFINITIONS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Request analysis signals (regex, zero cost)
|
||||
CONJUNCTIONS = ["and", "then", "also", "additionally", "after that", "plus", "as well as"]
|
||||
CROSS_SYSTEM_REFS = [
|
||||
"api",
|
||||
"frontend",
|
||||
"backend",
|
||||
"database",
|
||||
"tests",
|
||||
"config",
|
||||
"ci",
|
||||
"deploy",
|
||||
"ui",
|
||||
"cli",
|
||||
"server",
|
||||
"client",
|
||||
"model",
|
||||
"view",
|
||||
]
|
||||
ARCH_KEYWORDS = [
|
||||
"refactor",
|
||||
"migrate",
|
||||
"redesign",
|
||||
"pipeline",
|
||||
"system",
|
||||
"architecture",
|
||||
"overhaul",
|
||||
"restructure",
|
||||
"reorganize",
|
||||
"consolidate",
|
||||
]
|
||||
NEW_ABSTRACTION_KEYWORDS = [
|
||||
"create a new",
|
||||
"build a new",
|
||||
"implement a new",
|
||||
"add a new",
|
||||
"new module",
|
||||
"new system",
|
||||
"new feature",
|
||||
"new component",
|
||||
"new service",
|
||||
"new layer",
|
||||
]
|
||||
SCOPE_MODIFIERS = ["entire", "all", "every", "throughout", "comprehensive", "complete", "full", "whole", "across"]
|
||||
EXT_DEPENDENCY_KEYWORDS = [
|
||||
"integrate",
|
||||
"third-party",
|
||||
"library",
|
||||
"package",
|
||||
"sdk",
|
||||
"api",
|
||||
"service",
|
||||
"webhook",
|
||||
"external",
|
||||
]
|
||||
COMPLEXITY_MARKERS = [
|
||||
"complex",
|
||||
"big change",
|
||||
"major",
|
||||
"significant",
|
||||
"extensive",
|
||||
"substantial",
|
||||
"non-trivial",
|
||||
"involved",
|
||||
]
|
||||
SIMPLE_FIX_INDICATORS = [
|
||||
"fix typo",
|
||||
"rename",
|
||||
"update comment",
|
||||
"change color",
|
||||
"add import",
|
||||
"remove import",
|
||||
"fix whitespace",
|
||||
"format",
|
||||
"lint",
|
||||
"bump version",
|
||||
"update readme",
|
||||
"fix comment",
|
||||
]
|
||||
|
||||
# Build mode trigger keywords (high confidence)
|
||||
BUILD_TRIGGERS = [
|
||||
"implement",
|
||||
"build",
|
||||
"create feature",
|
||||
"refactor",
|
||||
"migrate",
|
||||
"across multiple files",
|
||||
"new system",
|
||||
"new module",
|
||||
"overhaul",
|
||||
"redesign",
|
||||
"restructure",
|
||||
"full pipeline",
|
||||
"end-to-end",
|
||||
]
|
||||
|
||||
# Task mode trigger keywords
|
||||
TASK_TRIGGERS = [
|
||||
"fix",
|
||||
"change",
|
||||
"update",
|
||||
"rename",
|
||||
"bug",
|
||||
"typo",
|
||||
"error",
|
||||
"crash",
|
||||
"broken",
|
||||
"not working",
|
||||
"issue",
|
||||
"patch",
|
||||
"hotfix",
|
||||
]
|
||||
|
||||
# Research mode trigger keywords
|
||||
RESEARCH_TRIGGERS = [
|
||||
"how does",
|
||||
"what is",
|
||||
"explain",
|
||||
"investigate",
|
||||
"explore",
|
||||
"research",
|
||||
"understand",
|
||||
"analyze",
|
||||
"study",
|
||||
"review",
|
||||
]
|
||||
|
||||
# Plan mode trigger keywords
|
||||
PLAN_TRIGGERS = [
|
||||
"plan",
|
||||
"design",
|
||||
"architecture",
|
||||
"before we start",
|
||||
"approach",
|
||||
"strategy",
|
||||
"roadmap",
|
||||
"blueprint",
|
||||
"spec",
|
||||
]
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# SCORING FUNCTIONS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScoreBreakdown:
|
||||
"""Detailed breakdown of complexity score."""
|
||||
|
||||
request_analysis: int = 0
|
||||
codebase_impact: int = 0
|
||||
historical_patterns: int = 0
|
||||
risk_assessment: int = 0
|
||||
total: int = 0
|
||||
signals: List[str] = field(default_factory=list)
|
||||
mode: str = "task"
|
||||
confidence: str = "high" # high, medium, low
|
||||
|
||||
|
||||
def analyze_request_text(request: str) -> tuple[int, List[str]]:
|
||||
"""Analyze request text for complexity signals. Zero cost, <1ms."""
|
||||
score = 0
|
||||
signals = []
|
||||
request_lower = request.lower()
|
||||
|
||||
# Multiple verbs/conjunctions
|
||||
conjunction_matches = [c for c in CONJUNCTIONS if c in request_lower]
|
||||
if len(conjunction_matches) >= 2:
|
||||
score += 2
|
||||
signals.append(f"Multiple conjunctions: {', '.join(conjunction_matches)}")
|
||||
elif len(conjunction_matches) == 1:
|
||||
score += 1
|
||||
signals.append(f"Conjunction: {conjunction_matches[0]}")
|
||||
|
||||
# Cross-system references
|
||||
system_matches = [s for s in CROSS_SYSTEM_REFS if s in request_lower]
|
||||
if len(system_matches) >= 2:
|
||||
score += 3
|
||||
signals.append(f"Cross-system: {', '.join(system_matches)}")
|
||||
elif len(system_matches) == 1:
|
||||
score += 1
|
||||
signals.append(f"System ref: {system_matches[0]}")
|
||||
|
||||
# Architecture keywords
|
||||
arch_matches = [k for k in ARCH_KEYWORDS if k in request_lower]
|
||||
if arch_matches:
|
||||
score += 2
|
||||
signals.append(f"Architecture: {', '.join(arch_matches)}")
|
||||
|
||||
# New abstraction keywords
|
||||
new_matches = [k for k in NEW_ABSTRACTION_KEYWORDS if k in request_lower]
|
||||
if new_matches:
|
||||
score += 2
|
||||
signals.append(f"New abstraction: {', '.join(new_matches)}")
|
||||
|
||||
# Scope modifiers
|
||||
scope_matches = [s for s in SCOPE_MODIFIERS if s in request_lower]
|
||||
if scope_matches:
|
||||
score += 1
|
||||
signals.append(f"Scope: {', '.join(scope_matches)}")
|
||||
|
||||
# External dependency mentions
|
||||
ext_matches = [k for k in EXT_DEPENDENCY_KEYWORDS if k in request_lower]
|
||||
if ext_matches:
|
||||
score += 1
|
||||
signals.append(f"External dep: {', '.join(ext_matches)}")
|
||||
|
||||
# Explicit complexity markers
|
||||
complex_matches = [k for k in COMPLEXITY_MARKERS if k in request_lower]
|
||||
if complex_matches:
|
||||
score += 3
|
||||
signals.append(f"Complexity marker: {', '.join(complex_matches)}")
|
||||
|
||||
# Simple fix indicators (negative score)
|
||||
simple_matches = [k for k in SIMPLE_FIX_INDICATORS if k in request_lower]
|
||||
if simple_matches:
|
||||
score -= 3
|
||||
signals.append(f"Simple fix: {', '.join(simple_matches)}")
|
||||
|
||||
return score, signals
|
||||
|
||||
|
||||
def estimate_codebase_impact(request: str, repo_path: str = "/mnt/workdrive/rune") -> tuple[int, List[str]]:
|
||||
"""Estimate codebase impact using grep/glob. ~100ms."""
|
||||
score = 0
|
||||
signals = []
|
||||
|
||||
# Extract potential module/function names from request
|
||||
words = re.findall(r"\b[a-zA-Z_][a-zA-Z0-9_]*\b", request)
|
||||
# Filter out common words
|
||||
stop_words = {
|
||||
"the",
|
||||
"a",
|
||||
"an",
|
||||
"and",
|
||||
"or",
|
||||
"but",
|
||||
"in",
|
||||
"on",
|
||||
"at",
|
||||
"to",
|
||||
"for",
|
||||
"of",
|
||||
"with",
|
||||
"by",
|
||||
"from",
|
||||
"is",
|
||||
"are",
|
||||
"was",
|
||||
"were",
|
||||
"be",
|
||||
"been",
|
||||
"being",
|
||||
"have",
|
||||
"has",
|
||||
"had",
|
||||
"do",
|
||||
"does",
|
||||
"did",
|
||||
"will",
|
||||
"would",
|
||||
"could",
|
||||
"should",
|
||||
"may",
|
||||
"might",
|
||||
"can",
|
||||
"shall",
|
||||
"that",
|
||||
"which",
|
||||
"who",
|
||||
"whom",
|
||||
"this",
|
||||
"these",
|
||||
"those",
|
||||
"it",
|
||||
"its",
|
||||
"my",
|
||||
"your",
|
||||
"his",
|
||||
"her",
|
||||
"our",
|
||||
"their",
|
||||
"me",
|
||||
"you",
|
||||
"him",
|
||||
"us",
|
||||
"them",
|
||||
"i",
|
||||
"we",
|
||||
"he",
|
||||
"she",
|
||||
"they",
|
||||
"what",
|
||||
"where",
|
||||
"when",
|
||||
"how",
|
||||
"why",
|
||||
"if",
|
||||
"then",
|
||||
"else",
|
||||
"so",
|
||||
"just",
|
||||
"also",
|
||||
"very",
|
||||
"too",
|
||||
"much",
|
||||
"many",
|
||||
"some",
|
||||
"any",
|
||||
"all",
|
||||
"each",
|
||||
"every",
|
||||
"both",
|
||||
"few",
|
||||
"more",
|
||||
"most",
|
||||
"other",
|
||||
"another",
|
||||
"such",
|
||||
"no",
|
||||
"not",
|
||||
"only",
|
||||
"own",
|
||||
"same",
|
||||
"than",
|
||||
"too",
|
||||
"now",
|
||||
"here",
|
||||
"there",
|
||||
"about",
|
||||
"up",
|
||||
"out",
|
||||
"off",
|
||||
"over",
|
||||
"under",
|
||||
"again",
|
||||
"further",
|
||||
"then",
|
||||
"once",
|
||||
"new",
|
||||
"add",
|
||||
"update",
|
||||
"fix",
|
||||
"change",
|
||||
"make",
|
||||
"create",
|
||||
"build",
|
||||
"implement",
|
||||
}
|
||||
potential_modules = [w for w in words if w.lower() not in stop_words and len(w) > 3]
|
||||
|
||||
# Check if any potential modules exist in codebase
|
||||
if potential_modules:
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
# Quick grep for potential module names
|
||||
grep_pattern = "|".join(potential_modules[:5]) # Limit to 5 to avoid slow grep
|
||||
result = subprocess.run(
|
||||
["grep", "-r", "-l", "-E", grep_pattern, os.path.join(repo_path, "src")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
files = [f for f in result.stdout.strip().split("\n") if f]
|
||||
if len(files) > 5:
|
||||
score += 3
|
||||
signals.append(f"Files likely affected: {len(files)}")
|
||||
elif len(files) > 2:
|
||||
score += 1
|
||||
signals.append(f"Files possibly affected: {len(files)}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Check for cross-module dependencies
|
||||
if "src/rune" in repo_path:
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
["grep", "-r", "-c", "import", os.path.join(repo_path, "src/rune")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
import_count = sum(
|
||||
int(line.split(":")[-1])
|
||||
for line in result.stdout.strip().split("\n")
|
||||
if ":" in line and line.split(":")[-1].strip().isdigit()
|
||||
)
|
||||
if import_count > 50:
|
||||
score += 2
|
||||
signals.append(f"High import density: {import_count} imports")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return score, signals
|
||||
|
||||
|
||||
def check_historical_patterns(request: str) -> tuple[int, List[str]]:
|
||||
"""Check memory for similar past tasks. Returns score and signals."""
|
||||
score = 0
|
||||
signals = []
|
||||
|
||||
# Check if similar tasks needed build mode
|
||||
try:
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.expanduser("~/.config/opencode/scripts"))
|
||||
import memory_store
|
||||
|
||||
db = memory_store.connect()
|
||||
try:
|
||||
results = memory_store.cached_recall(db, f"build mode {request}", budget=3)
|
||||
if results:
|
||||
for r in results:
|
||||
text = r.get("text", "").lower()
|
||||
if "build mode" in text or "multi-step" in text or "complex" in text:
|
||||
score += 2
|
||||
signals.append("Similar past task needed build mode")
|
||||
break
|
||||
finally:
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return score, signals
|
||||
|
||||
|
||||
def assess_risk(request: str, repo_path: str = "/mnt/workdrive/rune") -> tuple[int, List[str]]:
|
||||
"""Assess risk based on file paths and content."""
|
||||
score = 0
|
||||
signals = []
|
||||
request_lower = request.lower()
|
||||
|
||||
# Auth/security code
|
||||
auth_keywords = ["auth", "security", "token", "credential", "password", "login", "session"]
|
||||
if any(k in request_lower for k in auth_keywords):
|
||||
score += 2
|
||||
signals.append("Touches auth/security code")
|
||||
|
||||
# Data layer
|
||||
data_keywords = ["database", "migration", "schema", "sql", "sqlite", "model", "orm"]
|
||||
if any(k in request_lower for k in data_keywords):
|
||||
score += 1
|
||||
signals.append("Touches data layer")
|
||||
|
||||
# Public API
|
||||
api_keywords = ["api", "endpoint", "route", "export", "interface", "sdk", "public"]
|
||||
if any(k in request_lower for k in api_keywords):
|
||||
score += 2
|
||||
signals.append("Touches public API")
|
||||
|
||||
# Reversibility concern
|
||||
destructive_keywords = ["delete", "remove", "drop", "destroy", "clear", "reset", "wipe"]
|
||||
if any(k in request_lower for k in destructive_keywords):
|
||||
score += 1
|
||||
signals.append("Reversibility concern")
|
||||
|
||||
return score, signals
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# MAIN SCORING
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def score_request(request: str, repo_path: str = "/mnt/workdrive/rune") -> ScoreBreakdown:
|
||||
"""Score a request for complexity. Returns ScoreBreakdown with total and mode."""
|
||||
breakdown = ScoreBreakdown()
|
||||
|
||||
# Signal Category 1: Request Analysis (zero cost, <1ms)
|
||||
req_score, req_signals = analyze_request_text(request)
|
||||
breakdown.request_analysis = req_score
|
||||
breakdown.signals.extend(req_signals)
|
||||
|
||||
# Signal Category 2: Codebase Impact (~100ms)
|
||||
impact_score, impact_signals = estimate_codebase_impact(request, repo_path)
|
||||
breakdown.codebase_impact = impact_score
|
||||
breakdown.signals.extend(impact_signals)
|
||||
|
||||
# Signal Category 3: Historical Patterns
|
||||
hist_score, hist_signals = check_historical_patterns(request)
|
||||
breakdown.historical_patterns = hist_score
|
||||
breakdown.signals.extend(hist_signals)
|
||||
|
||||
# Signal Category 4: Risk Assessment
|
||||
risk_score, risk_signals = assess_risk(request, repo_path)
|
||||
breakdown.risk_assessment = risk_score
|
||||
breakdown.signals.extend(risk_signals)
|
||||
|
||||
# Calculate total
|
||||
breakdown.total = (
|
||||
breakdown.request_analysis
|
||||
+ breakdown.codebase_impact
|
||||
+ breakdown.historical_patterns
|
||||
+ breakdown.risk_assessment
|
||||
)
|
||||
|
||||
# Determine mode
|
||||
if breakdown.total >= 8:
|
||||
breakdown.mode = "build"
|
||||
breakdown.confidence = "high"
|
||||
elif breakdown.total >= 4:
|
||||
breakdown.mode = "task_with_planning"
|
||||
breakdown.confidence = "medium"
|
||||
else:
|
||||
breakdown.mode = "task"
|
||||
breakdown.confidence = "high"
|
||||
|
||||
# Check for explicit mode triggers
|
||||
request_lower = request.lower()
|
||||
if any(t in request_lower for t in BUILD_TRIGGERS):
|
||||
if breakdown.mode != "build":
|
||||
breakdown.mode = "build"
|
||||
breakdown.confidence = "high"
|
||||
breakdown.signals.append("Explicit build trigger detected")
|
||||
elif any(t in request_lower for t in RESEARCH_TRIGGERS):
|
||||
breakdown.mode = "research"
|
||||
breakdown.confidence = "high"
|
||||
breakdown.signals.append("Research trigger detected")
|
||||
elif any(t in request_lower for t in PLAN_TRIGGERS):
|
||||
breakdown.mode = "plan"
|
||||
breakdown.confidence = "high"
|
||||
breakdown.signals.append("Plan trigger detected")
|
||||
|
||||
return breakdown
|
||||
|
||||
|
||||
def get_mode(request: str, repo_path: str = "/mnt/workdrive/rune") -> str:
|
||||
"""Get the recommended mode for a request. Returns mode string."""
|
||||
breakdown = score_request(request, repo_path)
|
||||
return breakdown.mode
|
||||
|
||||
|
||||
def get_score(request: str, repo_path: str = "/mnt/workdrive/rune") -> Dict[str, Any]:
|
||||
"""Get detailed score breakdown. Returns dict."""
|
||||
breakdown = score_request(request, repo_path)
|
||||
return asdict(breakdown)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: complexity_scorer.py [score|mode] <request>")
|
||||
sys.exit(1)
|
||||
|
||||
cmd = sys.argv[1]
|
||||
request = " ".join(sys.argv[2:])
|
||||
|
||||
if cmd == "score":
|
||||
result = get_score(request)
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
elif cmd == "mode":
|
||||
mode = get_mode(request)
|
||||
print(mode)
|
||||
|
||||
else:
|
||||
print(f"Unknown command: {cmd}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -140,12 +140,14 @@ def activate(mode_decision, *, need, scaffold: sc.Scaffold, builder: str = "rune
|
||||
}
|
||||
)
|
||||
ps.commit(pdoc, what=f"PROJECT_BUILD bootstrap: {project}")
|
||||
awareness = _auto_structural_awareness(project, ps, scaffold)
|
||||
rec.update(
|
||||
{
|
||||
"activated": True,
|
||||
"lifecycle": "project_build",
|
||||
"project": project,
|
||||
"project_scaffold": str(ps.current),
|
||||
"structural_awareness": awareness,
|
||||
"bootstrap": "initialized",
|
||||
"repository": "undecided",
|
||||
"toolchain": "unresolved",
|
||||
@@ -161,9 +163,75 @@ def activate(mode_decision, *, need, scaffold: sc.Scaffold, builder: str = "rune
|
||||
return rec
|
||||
|
||||
|
||||
def _auto_structural_awareness(project: str, project_scaffold: sc.Scaffold,
|
||||
parent_scaffold: sc.Scaffold) -> dict:
|
||||
"""T21.5 §9 (rule 19Q family): governed adoption/construction expands
|
||||
Rune's persistent structural world-model AUTOMATICALLY — no manual
|
||||
Registry/Spine bookkeeping step. Boundaries preserved:
|
||||
- Registry = discovery/index (that the project exists, where its
|
||||
canonical Scaffold lives) — never project internals;
|
||||
- Spine = Rune's relationship TO the project (one edge, builds/
|
||||
manages vocabulary) — project modules stay in the Project
|
||||
Scaffold (never duplicated into Spine);
|
||||
- Project Scaffold remains canonical for the project's internals.
|
||||
Failure-tolerant: awareness must not break the bootstrap (recorded
|
||||
in the return, not raised)."""
|
||||
result: dict = {"registry": "skipped", "spine": "skipped"}
|
||||
try:
|
||||
from rune.project_registry import ProjectRegistry
|
||||
store_dir = parent_scaffold.current.parent
|
||||
registry = ProjectRegistry(store_dir)
|
||||
if registry.get(project) is None:
|
||||
registry.register(
|
||||
project_id=project,
|
||||
scaffold_location=str(project_scaffold.current.parent),
|
||||
repo_identity="", # undecided at bootstrap (identity
|
||||
# before repository — never invented)
|
||||
)
|
||||
registry.save()
|
||||
result["registry"] = "registered"
|
||||
except (OSError, ValueError) as exc:
|
||||
result["registry"] = f"error: {exc!s:.120}"
|
||||
try:
|
||||
from rune import spine as spine_mod
|
||||
spine_path = parent_scaffold.current.parent.parent / "spine" / "current.json"
|
||||
sdoc = spine_mod.load(spine_path)
|
||||
if sdoc is not None:
|
||||
spine_mod.add_relationship(
|
||||
sdoc, type="manages",
|
||||
from_id="project:rune", to_id=f"project:{project}",
|
||||
evidence_refs=[f"project-build-bootstrap:{project}"])
|
||||
spine_mod.save(sdoc, spine_path)
|
||||
result["spine"] = "recorded"
|
||||
except (OSError, ValueError) as exc:
|
||||
result["spine"] = f"error: {exc!s:.120}"
|
||||
return result
|
||||
|
||||
|
||||
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)
|
||||
mode_decision = trace.get("mode_decision")
|
||||
if mode_decision is None:
|
||||
# T21.5 fix: activate() expects the mode_decision object (with
|
||||
# .mode/.target_project/.evidence); the trace carries its dict
|
||||
# form, so reconstruct a ConstructionMode from it.
|
||||
from rune.construction_mode import ConstructionMode
|
||||
md = trace.get("construction_mode", {}) or {}
|
||||
if md.get("mode", "UNKNOWN") == "UNKNOWN":
|
||||
md = dict(md)
|
||||
md["mode"] = "UNKNOWN"
|
||||
mode_decision = ConstructionMode(
|
||||
mode=md.get("mode", "UNKNOWN"),
|
||||
evidence=list(md.get("evidence", [])),
|
||||
target_project=md.get("target_project"),
|
||||
project_authority=md.get("project_authority"),
|
||||
confidence=md.get("confidence", 0.0),
|
||||
clarification_required=md.get("clarification_required", False),
|
||||
probe_questions=list(md.get("probe_questions", [])),
|
||||
bootstrap_required=md.get("bootstrap_required", False),
|
||||
override=md.get("override"),
|
||||
proposed_by_model=md.get("proposed_by_model"),
|
||||
)
|
||||
return activate(mode_decision, 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}
|
||||
@@ -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)
|
||||
@@ -669,12 +669,16 @@ class ConstructionInterface:
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": "contract vanished after close"}],
|
||||
)
|
||||
q = closed["ext"]["contract_quality"]
|
||||
material_closure = self._auto_material_change(
|
||||
scope_files or [],
|
||||
note=f"contract {contract_id} closed (material change)")
|
||||
return _envelope(
|
||||
"close",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={
|
||||
"phase": "closed",
|
||||
"material_change_closure": material_closure,
|
||||
"contract_quality": {
|
||||
k: q[k]
|
||||
for k in ("correctly_predicted", "expected_new_observed", "genuinely_unexpected_new", "scope_check")
|
||||
@@ -683,6 +687,60 @@ class ConstructionInterface:
|
||||
},
|
||||
)
|
||||
|
||||
def _auto_material_change(self, scope_files: list[str], *,
|
||||
note: str) -> dict:
|
||||
"""T21.4 §21.4H (rule 19Q.2): governed construction closes → the
|
||||
material-change/revision flow fires AUTOMATICALLY. Every closed
|
||||
contract's scope_files are matched against the owning project's
|
||||
capability ledger source_locations; matched capabilities get
|
||||
material_change() (revision increment + history). Files matching
|
||||
NO modeled capability are returned as unmodeled — a coverage
|
||||
signal, never silently swallowed (spec/version decision remains
|
||||
the operator's, but the revision evidence is already durable)."""
|
||||
changed: list[str] = []
|
||||
unmodeled = list(scope_files)
|
||||
try:
|
||||
# ledger resolution: project-local ledger first (a project
|
||||
# scaffold's own capability ledger), then the store-root
|
||||
# rune_model (T21.5 fix: projects/<name> scaffolds must walk
|
||||
# UP to the store root, not sideways into projects/).
|
||||
candidates = [
|
||||
self.scaffold.store_dir / "rune_model",
|
||||
self.scaffold.store_dir.parent / "rune_model",
|
||||
self.scaffold.store_dir.parent.parent / "rune_model",
|
||||
# project scaffold (store/scaffold/projects/<p>) → store root
|
||||
self.scaffold.store_dir.parent.parent.parent / "rune_model",
|
||||
]
|
||||
ledger_file = next(
|
||||
(d / "capability_ledger.json" for d in candidates
|
||||
if (d / "capability_ledger.json").is_file()), None)
|
||||
if ledger_file is None:
|
||||
return {"changed": [], "unmodeled": unmodeled,
|
||||
"note": "no capability ledger for this project"}
|
||||
from rune.capability_ledger import CapabilityLedger
|
||||
ledger = CapabilityLedger(ledger_file.parent)
|
||||
|
||||
def _match(cap: dict, f: str) -> bool:
|
||||
for loc in cap.get("source_locations", []):
|
||||
if f == loc or f.endswith("/" + loc) or loc.endswith("/" + f):
|
||||
return True
|
||||
return False
|
||||
|
||||
for cap in ledger.doc["capabilities"].values():
|
||||
hits = [f for f in scope_files if _match(cap, f)]
|
||||
if hits:
|
||||
ledger.material_change(
|
||||
cap["capability_id"], note=f"{note}; scope: {hits[0]}",
|
||||
generation=self.scaffold.project_id)
|
||||
changed.append(cap["capability_id"])
|
||||
unmodeled = [f for f in unmodeled if f not in hits]
|
||||
if changed:
|
||||
ledger.save()
|
||||
except (OSError, ValueError) as exc: # closure must not break close
|
||||
return {"changed": changed, "unmodeled": unmodeled,
|
||||
"note": f"material-change closure error: {exc!s:.160}"}
|
||||
return {"changed": changed, "unmodeled": unmodeled}
|
||||
|
||||
@staticmethod
|
||||
def discover_active_contract(store_path: str) -> dict:
|
||||
"""§15U canonical resume discovery: walk the store's project
|
||||
|
||||
@@ -293,9 +293,32 @@ def classify_mode(
|
||||
file_scoped = True
|
||||
break
|
||||
|
||||
# T17 §17Q: indefinite-article new-artifact evidence — "a small CLI
|
||||
# that renames photos", "build me a program that..." — is
|
||||
# new-lifecycle language even without explicit standalone keywords.
|
||||
# Guarded: requires a CREATION verb or artifact noun, and yields to
|
||||
# local/scope/file evidence (fix a bug / add a flag never counts).
|
||||
_CREATION_VERBS = {"build", "create", "make", "write", "develop"}
|
||||
_ARTIFACT_NOUNS = {"program", "cli", "tool", "app", "script", "service",
|
||||
"library", "package", "module", "dashboard",
|
||||
"website", "bot", "generator", "tracker", "game",
|
||||
"application", "command"}
|
||||
article_standalone = False
|
||||
if re.search(r"\b(a|an|another)\s+[a-z]", obj.lower()):
|
||||
creation_verb = bool(_tokens(obj.lower()) & _CREATION_VERBS)
|
||||
artifact_noun = bool(_tokens(obj.lower()) & _ARTIFACT_NOUNS)
|
||||
# structural evidence outranks lexical newness (§5): a known
|
||||
# existing target means article language describes a CHANGE
|
||||
# ("add a flag to the deploy script"), not a new project
|
||||
if (creation_verb or artifact_noun) and not local and not scoped \
|
||||
and not file_scoped and not target_project \
|
||||
and project_exists is not True:
|
||||
article_standalone = True
|
||||
ev.append("indefinite-article new-artifact language")
|
||||
|
||||
# 3. PROJECT_BUILD: standalone/new-lifecycle language AND no existing
|
||||
# project for the target (project existence materially matters, §5)
|
||||
if standalone:
|
||||
if standalone or article_standalone:
|
||||
ev.append(f"standalone/new-lifecycle language: {standalone[:3]}")
|
||||
if project_exists is True:
|
||||
# §5: existing project preferred unless evidence says otherwise —
|
||||
|
||||
@@ -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
|
||||
@@ -187,6 +187,9 @@ class DriftCache:
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
import os
|
||||
|
||||
SCRIPTS_DIR = os.environ.get("RUNE_SCRIPTS_DIR", os.path.join(os.path.dirname(__file__), "scripts"))
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
|
||||
@@ -350,63 +350,6 @@ def pre_commit_gate_entry(entry: dict) -> dict:
|
||||
"critics": critics,
|
||||
"reason": "all critics passed" if all_pass else "one or more critics failed",
|
||||
}
|
||||
row = db.execute(
|
||||
"SELECT id, text, importance, wash_count, rock_status, topic FROM entries WHERE id = ?", (entry_id,)
|
||||
).fetchone()
|
||||
|
||||
if not row:
|
||||
return {"verdict": "REJECT", "reason": "entry not found"}
|
||||
|
||||
entry = dict(row)
|
||||
critics = {}
|
||||
|
||||
# Critic 1: Structural validity
|
||||
text = entry.get("text", "")
|
||||
if len(text) < 5:
|
||||
critics["structural"] = {"pass": False, "reason": "text too short"}
|
||||
elif len(text) > 2000:
|
||||
critics["structural"] = {"pass": False, "reason": "text too long"}
|
||||
elif not text.strip():
|
||||
critics["structural"] = {"pass": False, "reason": "empty text"}
|
||||
else:
|
||||
critics["structural"] = {"pass": True, "reason": "well-formed"}
|
||||
|
||||
# Critic 2: Behavioral harmlessness (check against immutable core)
|
||||
# The immutable core: user authority, no safety theater, no fights
|
||||
harmful_patterns = ["refuse to", "cannot comply", "safety violation", "I'm sorry, but I can't"]
|
||||
is_harmful = any(p in text.lower() for p in harmful_patterns)
|
||||
critics["behavioral"] = {
|
||||
"pass": not is_harmful,
|
||||
"reason": "contains refusal language" if is_harmful else "aligned with core",
|
||||
}
|
||||
|
||||
# Critic 3: Semantic consistency (check for contradictions with existing rocks)
|
||||
rocks = db.execute("SELECT text FROM entries WHERE rock_status = 'rock' AND status = 'active' LIMIT 50").fetchall()
|
||||
contradiction_found = False
|
||||
for rock in rocks:
|
||||
rock_text = rock[0].lower()
|
||||
entry_text = text.lower()
|
||||
# Simple contradiction detection: if entry says opposite of a rock
|
||||
if ("always" in rock_text and "never" in entry_text) or ("never" in rock_text and "always" in entry_text):
|
||||
contradiction_found = True
|
||||
break
|
||||
critics["semantic"] = {
|
||||
"pass": not contradiction_found,
|
||||
"reason": "contradicts existing rock" if contradiction_found else "consistent",
|
||||
}
|
||||
|
||||
# Overall verdict
|
||||
all_pass = all(c["pass"] for c in critics.values())
|
||||
verdict = "PASS" if all_pass else "REJECT"
|
||||
|
||||
return {
|
||||
"verdict": verdict,
|
||||
"entry_id": entry_id,
|
||||
"critics": critics,
|
||||
"reason": "all critics passed" if all_pass else "one or more critics failed",
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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
|
||||
+89
-8
@@ -95,12 +95,33 @@ for attempt in range(3):
|
||||
|
||||
|
||||
def wr_root() -> Path:
|
||||
env = os.environ.get("RUNE_WR_ROOT")
|
||||
if env:
|
||||
return Path(env)
|
||||
if DEFAULT_WR_ROOT.exists():
|
||||
return DEFAULT_WR_ROOT
|
||||
return FALLBACK_WR_ROOT
|
||||
"""Resolve the bridge DATA root — DELEGATED (T21.5 §7) to the single
|
||||
shared resolver (scripts/wr_root_resolver.py): explicit env →
|
||||
canonical production root → legacy home; split-brain between live
|
||||
stores fails loudly, never silently operates two stores."""
|
||||
resolver_path = (Path(__file__).resolve().parent.parent.parent
|
||||
/ "scripts" / "wr_root_resolver.py")
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"wr_root_resolver", str(resolver_path))
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError("no spec/loader")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod.resolve_root()
|
||||
except (OSError, RuntimeError) as exc:
|
||||
# resolver unavailable (partial checkout): keep the documented
|
||||
# policy rather than failing the wait path
|
||||
sys.stderr.write(f"event_wait: resolver fallback: {exc}\n")
|
||||
env = os.environ.get("RUNE_WR_ROOT")
|
||||
if env:
|
||||
return Path(env)
|
||||
shared = Path("/mnt/workdrive/wr-bridge")
|
||||
if shared.exists():
|
||||
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
|
||||
@@ -150,7 +171,19 @@ def register_process_wait(operation_id: str, *, contract_id: str = "",
|
||||
provider: str = "builder",
|
||||
event_type: str = "builder_exit",
|
||||
sha: str = "",
|
||||
watchdog_secs: int | None = None) -> dict:
|
||||
watchdog_secs: int | None = None,
|
||||
continuation_state: str = "WAITING_EXTERNAL",
|
||||
op_type: str = "",
|
||||
authority: str = "",
|
||||
task_id: str = "") -> dict:
|
||||
"""E-PATCH §4: the PendingOperation IS the durable continuation object.
|
||||
|
||||
continuation_state is first-class cognitive quiescence:
|
||||
ACTIVE_REASONING | WAITING_EXTERNAL | RESUMABLE | RECOVERY_REQUIRED
|
||||
op_type/authority record the CANONICAL execution semantics (derived by
|
||||
intent normalization, never model-supplied). task_id/project bind the
|
||||
operation to the current programme so fresh-process recovery can
|
||||
resolve relevance without the model reconstructing state."""
|
||||
"""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.
|
||||
@@ -170,6 +203,7 @@ def register_process_wait(operation_id: str, *, contract_id: str = "",
|
||||
# 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],"
|
||||
@@ -195,8 +229,18 @@ def register_process_wait(operation_id: str, *, contract_id: str = "",
|
||||
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"))}
|
||||
or int(os.environ.get("WR_WATCH_SECS", "600")),
|
||||
"continuation_state": continuation_state,
|
||||
"op_type": op_type, "authority": authority,
|
||||
"task_id": task_id}
|
||||
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
|
||||
|
||||
|
||||
@@ -218,6 +262,12 @@ def open_fifo_reader(operation_id: str) -> int:
|
||||
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)
|
||||
|
||||
|
||||
@@ -396,6 +446,37 @@ def resume_or_recover(operation_id: str) -> dict:
|
||||
return {"kind": "still_waiting", "wait": rec}
|
||||
|
||||
|
||||
def find_matching_event(provider: str, event_type: str, run_ref: str) -> dict | None:
|
||||
"""Search the bridge's durable event log for an event matching this
|
||||
identity. T16 §16C live finding: on a serialized single-runner
|
||||
machine the deploy job ALWAYS starts after ci.yml completes — the
|
||||
completion event lands before the waiter registers, and a pure
|
||||
forward-only FIFO rendezvous can never deliver it (both production
|
||||
cycles fell back to the 900s deadline). Events are durable precisely
|
||||
so they can be replayed: a waiter that finds a matching event at
|
||||
registration goes straight to the authority read instead of sleeping
|
||||
for a wake that already happened."""
|
||||
import json as _json
|
||||
ws = _load_waitstore()
|
||||
events_dir = ws.EVENTS
|
||||
if not events_dir.is_dir():
|
||||
return None
|
||||
best = None
|
||||
for f in sorted(events_dir.glob("*.json"),
|
||||
key=lambda p: p.stat().st_mtime, reverse=True):
|
||||
try:
|
||||
rec = _json.loads(f.read_text())
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
ev = rec.get("event", {})
|
||||
if (ev.get("provider") == provider
|
||||
and ev.get("event_type") == event_type
|
||||
and str(ev.get("run_ref", "")) == run_ref):
|
||||
best = ev # newest match wins
|
||||
break
|
||||
return best
|
||||
|
||||
|
||||
def post_process_event(operation_id: str, exit_code: int, *,
|
||||
daemon_url: str = DEFAULT_DAEMON_URL) -> dict:
|
||||
"""Direct event injection (test seam + manual reconciliation). The
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -21,10 +21,11 @@ INTERMITTENT = "INTERMITTENT"
|
||||
KNOWN_PROCESS_DEFECT = "KNOWN_PROCESS_DEFECT"
|
||||
PERMANENT = "PERMANENT"
|
||||
APPROVAL_REQUIRED = "APPROVAL_REQUIRED"
|
||||
PROVIDER_UNAVAILABLE = "PROVIDER_UNAVAILABLE"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
CLASSES = (TRANSIENT, INTERMITTENT, KNOWN_PROCESS_DEFECT, PERMANENT,
|
||||
APPROVAL_REQUIRED, UNKNOWN)
|
||||
APPROVAL_REQUIRED, PROVIDER_UNAVAILABLE, UNKNOWN)
|
||||
|
||||
# recovery policy per class: (max_retries, action)
|
||||
POLICY = {
|
||||
@@ -33,6 +34,9 @@ POLICY = {
|
||||
KNOWN_PROCESS_DEFECT: (1, "apply_evidenced_repair_then_retry"),
|
||||
PERMANENT: (0, "do_not_retry"),
|
||||
APPROVAL_REQUIRED: (0, "block"),
|
||||
# T16 §16F: provider failures NEVER auto-retry and NEVER fall back
|
||||
# to another provider — preserve the real error, block the branch.
|
||||
PROVIDER_UNAVAILABLE: (0, "block_branch_preserve_error"),
|
||||
UNKNOWN: (1, "bounded_evidence_probe_then_block_or_escalate"),
|
||||
}
|
||||
|
||||
@@ -74,6 +78,14 @@ def classify_failure(error_text: str, *, context: dict | None = None) -> dict:
|
||||
return {"class": KNOWN_PROCESS_DEFECT,
|
||||
"evidence": f"known defect: {known_defect}",
|
||||
"policy": POLICY[KNOWN_PROCESS_DEFECT]}
|
||||
# T16 §16F: an explicit provider-context failure wins over every
|
||||
# generic marker — the real status/API code is preserved verbatim in
|
||||
# the evidence, the branch blocks, and no fallback is attempted.
|
||||
if context.get("provider_error"):
|
||||
return {"class": PROVIDER_UNAVAILABLE,
|
||||
"evidence": (f"provider error preserved: {error_text}"
|
||||
if error_text else "provider error"),
|
||||
"policy": POLICY[PROVIDER_UNAVAILABLE]}
|
||||
if any(m in text for m in _APPROVAL_MARKERS):
|
||||
return {"class": APPROVAL_REQUIRED,
|
||||
"evidence": "approval marker present",
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"""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)
|
||||
if proc.returncode != 0:
|
||||
raise ForgeUnavailable(
|
||||
f"release publication failed rc={proc.returncode}: "
|
||||
f"{(proc.stderr or proc.stdout)[-200:]}")
|
||||
return {"published": True, "tag": tag}
|
||||
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,190 @@
|
||||
"""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)
|
||||
# interface-mediated consumers: consumers of the interfaces this
|
||||
# target provides (1-hop through the connector, still bounded)
|
||||
iface_consumers = sorted({c for iface in model.interfaces_for(target)
|
||||
for c in model.consumers(iface)
|
||||
if c != target})
|
||||
connected = {
|
||||
"dependencies": model.dependencies(target),
|
||||
"consumers": sorted(set(model.consumers(target)) |
|
||||
set(iface_consumers)),
|
||||
"interface_mediated_consumers": iface_consumers,
|
||||
"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",
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import json
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
|
||||
# stage states
|
||||
@@ -101,6 +102,15 @@ class Programme:
|
||||
"next_action": None,
|
||||
"reason": (f"{len(approval)} stage(s) need user "
|
||||
f"authorization")}
|
||||
# T17 §17E: all stages terminal but some FAILED — the programme
|
||||
# may still close (programme-level acceptance decides), never
|
||||
# hang in UNKNOWN_INTERRUPTION.
|
||||
if all(s.state in TERMINAL for s in self.stages):
|
||||
failed = [s.stage_id for s in self.stages if s.state == FAILED]
|
||||
return {"verdict": "PROGRAMME_COMPLETE_WITH_FAILURES",
|
||||
"next_action": None,
|
||||
"reason": (f"all stages terminal; failed: {failed} — "
|
||||
f"programme-level acceptance decides")}
|
||||
return {"verdict": "UNKNOWN_INTERRUPTION", "next_action": None,
|
||||
"reason": "no stage runnable and none terminal"}
|
||||
|
||||
@@ -163,3 +173,177 @@ def run_programme(prog: Programme, stage_runner, *,
|
||||
stage.state = stage_runner(stage) # runner decides the new state
|
||||
log.append({"stage": target, "new_state": stage.state})
|
||||
return log
|
||||
|
||||
|
||||
# ── T17 §17D: production wiring — Scaffolding-integrated driver ─────────
|
||||
#
|
||||
# The 15M canary proved the RULE; this adapter makes it CANONICAL. One
|
||||
# programme = a list of stages; each stage maps 1:1 to a Scaffolding
|
||||
# contract (stage_id == contract_id). Stages run through the Construction
|
||||
# Interface lifecycle (approved→building→verifying→closed); asynchronous
|
||||
# stage work runs under the event bridge (WAITING_EVENT is a state, not
|
||||
# a stop). Persisted to the scaffold store's programme area — reloadable
|
||||
# after restart with no transcript.
|
||||
|
||||
DEFAULT_PROGRAMME_AREA = "programmes"
|
||||
|
||||
|
||||
def _programme_base(scaffold_store: Path) -> Path:
|
||||
return Path(scaffold_store) / DEFAULT_PROGRAMME_AREA
|
||||
|
||||
|
||||
def scaffolding_stage_runner(interface, *, scaffold_store: Path,
|
||||
daemon_url: str = "",
|
||||
watchdog_secs: int = 900):
|
||||
"""Return a stage_runner for run_programme bound to the Construction
|
||||
Interface. The stage's work is read from stage.result["work"] — a
|
||||
callable (interface, stage) → dict with {"ok": bool, "evidence": str,
|
||||
"async_cmd": [argv] optional}. When async_cmd is present the work
|
||||
runs under the event bridge and the driver resumes on the EVENT;
|
||||
canonical verification still decides completion truth. Stages without
|
||||
work complete as governance-only transitions."""
|
||||
from rune import event_wait as ew # local import: optional dependency
|
||||
|
||||
def _phase(contract_id: str) -> str:
|
||||
# ALWAYS re-read: transition() saves through its own doc copy, so
|
||||
# a locally-held contract dict goes stale after any transition
|
||||
# (T17 17E live finding — the draft→approved→building chain
|
||||
# silently stopped after the first step on a stale reference).
|
||||
doc = interface.scaffold.load()
|
||||
c = interface.scaffold.contract(doc, contract_id)
|
||||
return (c or {}).get("phase", "")
|
||||
|
||||
def run(stage: ProgrammeStage) -> str:
|
||||
contract_id = stage.stage_id
|
||||
phase = _phase(contract_id)
|
||||
if phase == "":
|
||||
stage.result["error"] = f"no contract {contract_id}"
|
||||
return FAILED
|
||||
if phase == "closed":
|
||||
return COMPLETE # idempotent re-entry (duplicate signal case)
|
||||
if phase == "draft":
|
||||
interface.transition(contract_id, "approved",
|
||||
evidence_ref="programme-stage-start")
|
||||
if _phase(contract_id) == "approved":
|
||||
interface.transition(contract_id, "building",
|
||||
evidence_ref="programme-stage-start")
|
||||
work = cast("Any", stage.result.get("work"))
|
||||
if work is None:
|
||||
interface.transition(contract_id, "verifying",
|
||||
evidence_ref="governance-only-stage")
|
||||
interface.close(
|
||||
contract_id,
|
||||
observed_evidence=[{"source": "programme-driver",
|
||||
"detail": "governance-only stage: "
|
||||
"no work, closure by "
|
||||
"programme acceptance"}],
|
||||
scope_files=[])
|
||||
return COMPLETE
|
||||
# work is either a callable (interface, stage) → outcome dict, or
|
||||
# a literal outcome spec dict ({"ok", "evidence", "async_cmd"?})
|
||||
outcome: dict = (dict(cast("dict", work(interface, stage)))
|
||||
if callable(work) else dict(cast("dict", work)))
|
||||
async_cmd = outcome.get("async_cmd")
|
||||
if async_cmd:
|
||||
op = f"stage-{contract_id}-{int(time.time())}"
|
||||
ew.register_process_wait(
|
||||
op, contract_id=contract_id,
|
||||
project=getattr(interface.scaffold, "project_id", ""),
|
||||
objective=stage.objective,
|
||||
watched_for="stage work completion event",
|
||||
watchdog_secs=watchdog_secs)
|
||||
notifier = ew.run_builder_under_events(
|
||||
async_cmd, op, daemon_url=daemon_url or ew.DEFAULT_DAEMON_URL)
|
||||
stage.result["operation_id"] = op
|
||||
stage.result["notifier_pgid"] = notifier.pid
|
||||
result = ew.await_resume_event(op, watchdog_secs=watchdog_secs,
|
||||
pgid=notifier.pid)
|
||||
if result["kind"] != "event":
|
||||
stage.result["error"] = f"stage event wait: {result['kind']}"
|
||||
interface.transition(contract_id, "failed",
|
||||
evidence_ref="stage-event-lost")
|
||||
return FAILED
|
||||
rec: dict = result.get("wait") or {}
|
||||
outcome["evidence"] = (f"event resume; wait {rec.get('wait_id')} "
|
||||
f"verdict={rec.get('verdict')}")
|
||||
if not outcome.get("ok"):
|
||||
stage.result["error"] = outcome.get("evidence", "work failed")
|
||||
# truthful failure: record the evidence ON the contract and
|
||||
# drive it to the failed phase via the legal transition map
|
||||
# (building→failed is legal; no exception swallowing)
|
||||
doc = interface.scaffold.load()
|
||||
c = interface.scaffold.contract(doc, contract_id)
|
||||
if c is not None:
|
||||
c.setdefault("checkpoints", []).append({
|
||||
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime()),
|
||||
"phase": c.get("phase", ""),
|
||||
"evidence": "programme-stage-failed",
|
||||
"note": (outcome.get("evidence") or "work failed")[:200],
|
||||
})
|
||||
interface.scaffold.save(doc)
|
||||
phase = c.get("phase", "")
|
||||
if phase == "building":
|
||||
interface.transition(contract_id, "failed",
|
||||
evidence_ref="programme-stage-failed")
|
||||
return FAILED
|
||||
# completion truth: work evidence recorded ON the contract, then
|
||||
# closed (evidence lives on the durable object, not the log)
|
||||
if _phase(contract_id) == "building":
|
||||
interface.transition(contract_id, "verifying",
|
||||
evidence_ref="programme-stage-verify")
|
||||
doc = interface.scaffold.load()
|
||||
c = interface.scaffold.contract(doc, contract_id)
|
||||
if c is not None:
|
||||
c.setdefault("checkpoints", []).append({
|
||||
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"phase": "verifying",
|
||||
"evidence": "programme-stage-work",
|
||||
"note": (outcome.get("evidence") or "work completed")[:200],
|
||||
})
|
||||
interface.scaffold.save(doc)
|
||||
interface.close(
|
||||
contract_id,
|
||||
observed_evidence=[{"source": "programme-driver",
|
||||
"detail": outcome.get("evidence", "ok")}],
|
||||
scope_files=[])
|
||||
return COMPLETE
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def run_construction_programme(prog: Programme, interface, *,
|
||||
scaffold_store: Path,
|
||||
daemon_url: str = "",
|
||||
watchdog_secs: int = 900,
|
||||
max_transitions: int = 100) -> list[dict]:
|
||||
"""Canonical multi-stage construction continuation (T17 §17D):
|
||||
run_programme with the Scaffolding-bound runner; persists the
|
||||
programme around every transition so a restart resumes from the
|
||||
durable record, never a transcript. Returns the transition log."""
|
||||
runner = scaffolding_stage_runner(
|
||||
interface, scaffold_store=scaffold_store,
|
||||
daemon_url=daemon_url, watchdog_secs=watchdog_secs)
|
||||
log: list[dict] = []
|
||||
base = _programme_base(scaffold_store)
|
||||
for _ in range(max_transitions):
|
||||
prog.save(base)
|
||||
canary = prog.continuation_canary()
|
||||
log.append({"canary": canary})
|
||||
verdict, action = canary["verdict"], canary["next_action"]
|
||||
if action is None:
|
||||
break
|
||||
kind, target = action
|
||||
if kind == "fallback":
|
||||
log.append({"fallback_started": target})
|
||||
break
|
||||
stage = next(s for s in prog.stages if s.stage_id == target)
|
||||
if stage.state == COMPLETE:
|
||||
continue # duplicate continuation signal: idempotent
|
||||
stage.state = ELIGIBLE
|
||||
new_state = runner(stage)
|
||||
stage.state = new_state
|
||||
log.append({"stage": target, "new_state": new_state})
|
||||
prog.save(base)
|
||||
prog.save(base)
|
||||
return log
|
||||
@@ -0,0 +1,85 @@
|
||||
"""programme_gates.py — T21.2 §21.2G: mandatory acceptance-gate accounting.
|
||||
|
||||
The executing model must NOT be able to:
|
||||
- silently remove mandatory gates;
|
||||
- demote MANDATORY → OPTIONAL;
|
||||
- restore acceptance with unrun mandatory gates.
|
||||
|
||||
Every mandatory gate ends in exactly one of: PASS | BLOCKED | WAIVED_BY_USER.
|
||||
Optional gates may be OPTIONAL_SKIPPED or OPTIONAL_PASS.
|
||||
|
||||
This is a governance correction, not new architecture.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
MANDATORY_STATUSES = ("PASS", "BLOCKED", "WAIVED_BY_USER")
|
||||
OPTIONAL_STATUSES = ("OPTIONAL_SKIPPED", "OPTIONAL_PASS")
|
||||
ALL_STATUSES = MANDATORY_STATUSES + OPTIONAL_STATUSES
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
class GateDemotionError(ValueError):
|
||||
"""The model tried to demote a mandatory gate to optional."""
|
||||
|
||||
|
||||
class GateAccounting:
|
||||
def __init__(self):
|
||||
self.gates: dict[str, dict] = {}
|
||||
|
||||
def declare(self, gate_id: str, *, mandatory: bool,
|
||||
requirement: str) -> None:
|
||||
self.gates[gate_id] = {
|
||||
"gate_id": gate_id, "mandatory": mandatory,
|
||||
"requirement": requirement[:200],
|
||||
"status": "UNRUN", "evidence": "", "blocker": "",
|
||||
"waiver_reason": "", "waived_by": "",
|
||||
"declared_at": _now(), "resolved_at": "",
|
||||
}
|
||||
|
||||
def record(self, gate_id: str, status: str, *,
|
||||
evidence: str = "", blocker: str = "",
|
||||
waiver_reason: str = "", waived_by: str = "") -> dict:
|
||||
g = self.gates.get(gate_id)
|
||||
if g is None:
|
||||
raise ValueError(f"unknown gate {gate_id!r}")
|
||||
if g["mandatory"] and status in OPTIONAL_STATUSES:
|
||||
raise GateDemotionError(
|
||||
f"gate {gate_id!r} is MANDATORY — cannot be marked "
|
||||
f"{status!r} without explicit user waiver")
|
||||
if status == "WAIVED_BY_USER":
|
||||
if not g["mandatory"]:
|
||||
raise ValueError(
|
||||
f"WAIVED_BY_USER only applies to mandatory gates; "
|
||||
f"{gate_id!r} is optional")
|
||||
if not waiver_reason or not waived_by:
|
||||
raise ValueError(
|
||||
f"waiver for {gate_id!r} requires reason + waived_by")
|
||||
if status not in ALL_STATUSES:
|
||||
raise ValueError(f"unknown gate status {status!r}")
|
||||
g["status"] = status
|
||||
g["evidence"] = evidence[:200]
|
||||
g["blocker"] = blocker[:200]
|
||||
g["waiver_reason"] = waiver_reason[:200]
|
||||
g["waived_by"] = waived_by
|
||||
g["resolved_at"] = _now()
|
||||
return g
|
||||
|
||||
def can_accept(self) -> tuple[bool, list[str]]:
|
||||
"""Acceptance eligible only when every mandatory gate is
|
||||
PASS or WAIVED_BY_USER (with persisted waiver)."""
|
||||
blockers = []
|
||||
for gid, g in sorted(self.gates.items()):
|
||||
if not g["mandatory"]:
|
||||
continue
|
||||
if g["status"] not in ("PASS", "WAIVED_BY_USER"):
|
||||
blockers.append(
|
||||
f"{gid}: {g['status']} — {g.get('blocker', '')}")
|
||||
return (not blockers, blockers)
|
||||
|
||||
def accounting(self) -> list[dict]:
|
||||
return sorted(self.gates.values(), key=lambda g: g["gate_id"])
|
||||
@@ -0,0 +1,139 @@
|
||||
"""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 resolve_cwd(self, project_id: str) -> str:
|
||||
"""T21.2 §21.2B: canonical REPOSITORY/worktree execution cwd for
|
||||
a project — NOT scaffold_location. Precedence (21.2B-3):
|
||||
explicit contract/worktree cwd > programme cwd > registry
|
||||
repo cwd > unresolved/refuse.
|
||||
Registry stores repo_identity as "git:<path>" or a bare path.
|
||||
Scaffold storage is NEVER used as a repo-cwd proxy. Raises for
|
||||
unknown projects (refuse mutation rather than guessing)."""
|
||||
entry = self.get(project_id)
|
||||
if entry is None:
|
||||
raise ValueError(
|
||||
f"unknown project {project_id!r} — resolve identity "
|
||||
f"before mutation; never execute from HOME")
|
||||
repo = entry.get("repo_identity", "")
|
||||
if repo.startswith("git:"):
|
||||
repo = repo[len("git:"):]
|
||||
repo = repo.strip()
|
||||
if not repo:
|
||||
raise ValueError(
|
||||
f"project {project_id!r} has no repo_identity — "
|
||||
f"no local execution checkout available")
|
||||
# an active worktree overrides the repo root (21.2B-3 precedence)
|
||||
wt = entry.get("active_worktree", "")
|
||||
if wt:
|
||||
return wt
|
||||
return repo
|
||||
|
||||
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
|
||||
@@ -9,6 +9,12 @@ Usage:
|
||||
rune_graphic.py test # Display animation in terminal
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PIL import Image
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
@@ -23,7 +29,7 @@ BG_COLOR = (30, 30, 40) # Dark background
|
||||
|
||||
def generate_diamond_frame(angle: float, size: int = FRAME_SIZE) -> "Image.Image":
|
||||
"""Generate a single frame of a spinning diamond at the given angle."""
|
||||
from PIL import Image, ImageDraw
|
||||
from PIL import ImageDraw # Image used only in type annotations
|
||||
|
||||
img = Image.new("RGBA", (size, size), BG_COLOR + (255,))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
@@ -740,12 +740,39 @@ 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"]),
|
||||
@@ -1558,8 +1585,13 @@ class RuneOperator:
|
||||
task.status = "failed"
|
||||
task.result = result.error or "Unknown error"
|
||||
self.state.current_task = None
|
||||
self._save()
|
||||
# Mirror the terminal move into the canonical ledger (#18).
|
||||
# T21 §21I CANONICAL-FIRST ORDERING (crash-window fix): the
|
||||
# canonical transition commits BEFORE the legacy cache save. The
|
||||
# previous order (save → transition) left a crash window where
|
||||
# operator.json claimed completed/failed while the canonical
|
||||
# repo still said running — the cache outran truth. Reversed:
|
||||
# a crash after transition but before _save() leaves the cache
|
||||
# STALE (backfillable via _ensure_canonical) instead of AHEAD.
|
||||
self._ensure_canonical(task)
|
||||
if result.success and result.pending_approval is not None:
|
||||
self._task_state.transition(task.task_id, "awaiting_approval")
|
||||
@@ -1578,6 +1610,7 @@ class RuneOperator:
|
||||
self._task_state.transition(task.task_id, "failed")
|
||||
# P0-04 (#20): meaningful-event seed (see send_task failure path).
|
||||
self._emit_task_failed(task, channel=self._channel_for_task(task.task_id))
|
||||
self._save()
|
||||
|
||||
def _ensure_canonical(self, task: RuneTask) -> None:
|
||||
"""Backfill a task seeded through the legacy path into the ledger (#18).
|
||||
|
||||
+157
-11
@@ -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.63
|
||||
Version: v0.85.1
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -1697,9 +1697,38 @@ class RuneApp(App):
|
||||
except Exception as e:
|
||||
return ("registry_error", str(e))
|
||||
|
||||
def _task_repo_for_loader():
|
||||
"""Access the canonical TaskStateRepository for the task
|
||||
loader (T21 21G migration) — closure over the operator
|
||||
getter already in scope. Degrades to the legacy ledger view
|
||||
when the operator is unreachable (startup ordering), which
|
||||
the load_tasks fallback then handles."""
|
||||
try:
|
||||
import rune_operator as _ro
|
||||
return getattr(_ro.get_operator(), "_task_state", None)
|
||||
except Exception as exc: # noqa: BLE001 — deliberate degradation: startup ordering; the fallback path handles it and the degradation is logged
|
||||
print(f"[task-loader] canonical repo unavailable ({exc}); "
|
||||
f"falling back to export ledger", flush=True)
|
||||
return None
|
||||
|
||||
def load_tasks():
|
||||
try:
|
||||
if BACKEND_AVAILABLE:
|
||||
# T21 21G: the TUI task loader reads the CANONICAL
|
||||
# TaskStateRepository (17F authority), not the legacy
|
||||
# task_persist export ledger. task_persist remains a
|
||||
# session-end export view (its only remaining role).
|
||||
repo = _task_repo_for_loader()
|
||||
if repo is not None:
|
||||
records = repo.list_all()
|
||||
active = [
|
||||
{"task_id": r.task_id, "title": r.title,
|
||||
"status": r.status}
|
||||
for r in records
|
||||
if r.status in ("created", "running")]
|
||||
all_pins = pins.list_pins()
|
||||
open_pins = [p for p in all_pins if p.get("status") == "open"]
|
||||
return ("tasks", active, open_pins)
|
||||
data = task_persist.load()
|
||||
tasks = data.get("tasks", [])
|
||||
active = [t for t in tasks if t.get("status") in ("open", "in_progress")]
|
||||
@@ -1856,7 +1885,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.66.63 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.85.1 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
@@ -2099,20 +2128,21 @@ class RuneApp(App):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Task mode: auto-detect if build mode is needed
|
||||
# Task mode: auto-detect if build mode is needed (T21.1 21.1E:
|
||||
# complexity_scorer RETIRED — the canonical classify_mode owns
|
||||
# construction-mode recommendation now (same classifier as every
|
||||
# other route; the scorer's keyword heuristic was superseded).
|
||||
# Kept proportional: a one-line hint, no separate 602-line module.
|
||||
if channel == "task":
|
||||
try:
|
||||
from rune.complexity_scorer import score_request
|
||||
from rune.construction_mode import classify_mode
|
||||
|
||||
breakdown = score_request(text)
|
||||
if breakdown.total >= 8 and breakdown.mode == "build":
|
||||
_mode = classify_mode(text)
|
||||
if _mode.mode in ("PROJECT_BUILD", "EXISTING_PROJECT_CHANGE"):
|
||||
ch.add_message(
|
||||
"system",
|
||||
f"[yellow]🔨 Build mode recommended (score: {breakdown.total})[/yellow]",
|
||||
)
|
||||
ch.add_message(
|
||||
"system",
|
||||
f"[yellow] Signals: {', '.join(breakdown.signals[:3])}[/yellow]",
|
||||
f"[yellow]🔨 Build mode recommended "
|
||||
f"(classified: {_mode.mode})[/yellow]",
|
||||
)
|
||||
ch.add_message(
|
||||
"system",
|
||||
@@ -3112,6 +3142,58 @@ class RuneApp(App):
|
||||
except Exception as e:
|
||||
self._ch().add_message("system", f"[dim]Error: {e}[/dim]")
|
||||
|
||||
def _cmd_testing(self, arg):
|
||||
"""T19 19L: canonical Testing Mode — outstanding validations.
|
||||
/testing → aggregate across registered projects
|
||||
/testing <project> → one project's outstanding items."""
|
||||
try:
|
||||
from pathlib import Path as _Path
|
||||
from rune import testing_mode as tm
|
||||
from rune.behaviour_ledger import BehaviourLedger
|
||||
from rune.project_registry import ProjectRegistry
|
||||
registry = ProjectRegistry("store/scaffold")
|
||||
ch = self._ch()
|
||||
if arg.strip():
|
||||
# project filter: read THAT project's ledger
|
||||
entry = registry.get(arg.strip())
|
||||
if entry is None:
|
||||
ch.add_message("system",
|
||||
f"[yellow]No registered scaffolded project "
|
||||
f"{arg.strip()!r}[/yellow]")
|
||||
return
|
||||
loc = entry["scaffold_location"]
|
||||
if not (_Path(loc) / "behaviour_ledger.json").is_file():
|
||||
ch.add_message("system",
|
||||
f"[yellow]{arg.strip()} has no behaviour ledger "
|
||||
f"yet[/yellow]")
|
||||
return
|
||||
beh = BehaviourLedger(loc)
|
||||
items = beh.outstanding()
|
||||
ch.add_message("system",
|
||||
f"[bold]{arg.strip()}: {len(items)} outstanding "
|
||||
f"validation(s)[/bold]")
|
||||
else:
|
||||
rows = tm.aggregate(registry, _Path("store/scaffold"))
|
||||
ch.add_message("system", "[bold]Testing aggregate "
|
||||
"(registered scaffolded projects)[/bold]")
|
||||
for r in rows:
|
||||
if r["outstanding"] is None:
|
||||
ch.add_message("system",
|
||||
f" {r['project']}: no behaviour ledger yet")
|
||||
else:
|
||||
ch.add_message("system",
|
||||
f" {r['project']}: {r['outstanding']} "
|
||||
f"outstanding")
|
||||
items = []
|
||||
for b in items[:8]:
|
||||
ch.add_message("system",
|
||||
f" • {b['behaviour_id']}: {b['intended_behaviour'][:70]} "
|
||||
f"[{b['state']}]"
|
||||
+ (f" — procedure: {b['procedure'][:60]}" if b.get("procedure") else ""))
|
||||
except Exception as exc: # noqa: BLE001 — user-visible surface: report, never crash the TUI
|
||||
self._ch().add_message("system",
|
||||
f"[red]/testing failed: {exc}[/red]")
|
||||
|
||||
def _cmd_build(self, arg):
|
||||
"""Build mode — full build pipeline with worktree isolation."""
|
||||
try:
|
||||
@@ -3141,6 +3223,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]")
|
||||
@@ -3421,6 +3566,7 @@ class RuneApp(App):
|
||||
"/audit": _cmd_audit,
|
||||
"/work": _cmd_work,
|
||||
"/build": _cmd_build,
|
||||
"/testing": _cmd_testing,
|
||||
"/queue": _cmd_queue,
|
||||
"/pull": _cmd_pull,
|
||||
"/help": _cmd_help,
|
||||
|
||||
@@ -249,6 +249,15 @@ def build(manifest_path: Path | None = None) -> dict:
|
||||
registry = _canary_registry()
|
||||
tested = _test_backed_modules()
|
||||
mods = {e["id"].split(":", 1)[1] for e in doc["entities"] if e["kind"] == "module"}
|
||||
# T21.4 §21.4G: the ENTRY POINT is invisible to the import graph
|
||||
# (nothing imports __main__) but is a real runtime path — seed it
|
||||
# deterministically whenever the file exists.
|
||||
if (SRC_DIR / "__main__.py").is_file() and "__main__" not in mods:
|
||||
add_entity(doc, id="module:__main__", kind="module", maturity="PRESENT",
|
||||
identities={"path": "src/rune/__main__.py",
|
||||
"role": "TUI entry point (python3 -m rune)"},
|
||||
evidence_refs=["entry-point:python3 -m rune"])
|
||||
mods.add("__main__")
|
||||
for m in sorted(mods):
|
||||
mid = f"module:{m}"
|
||||
e = entity(doc, mid)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,196 @@
|
||||
"""testing_mode.py — T19 §19L/19M/19N/19O/19P: canonical Testing Mode
|
||||
backend + Observation→diagnosis→fix→retest + forge projection.
|
||||
|
||||
The Behaviour Validation Ledger is DURABLE TRUTH. A testing session is
|
||||
only coordination state (rule 19N) — pausable, resumable, never the
|
||||
authority. The queue is DYNAMIC (rule 19M): a fix for behaviour B may
|
||||
invalidate confirmed behaviour A via structural closure; unrelated
|
||||
hardened behaviours stay trusted.
|
||||
|
||||
Forge projection (19P): failures in active iteration create/update a
|
||||
human-visible issue through the T18 forge boundary. Forge failure never
|
||||
rewrites canonical truth.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from rune.behaviour_ledger import BehaviourLedger, TEST_FAILED, USER_CONFIRMED
|
||||
from rune.capability_ledger import CapabilityLedger
|
||||
from rune.project_registry import ProjectRegistry
|
||||
|
||||
SESSION_SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
# ── 19N: session = coordination only ─────────────────────────────────────
|
||||
|
||||
class TestingSession:
|
||||
def __init__(self, store_dir: Path | str, *, project: str = "",
|
||||
scope_component: str | None = None):
|
||||
self.path = Path(store_dir) / "testing_session.json"
|
||||
self.store_dir = Path(store_dir)
|
||||
self.state = self._load(project, scope_component)
|
||||
|
||||
def _load(self, project, scope_component) -> dict:
|
||||
import json
|
||||
if self.path.is_file():
|
||||
return json.loads(self.path.read_text())
|
||||
return {"schema_version": SESSION_SCHEMA_VERSION,
|
||||
"project": project, "scope_component": scope_component,
|
||||
"current_item": None, "completed_this_session": [],
|
||||
"newly_invalidated": [], "blockers": [],
|
||||
"paused": False, "updated_at": _now()}
|
||||
|
||||
def save(self) -> None:
|
||||
import json
|
||||
self.state["updated_at"] = _now()
|
||||
self.path.write_text(json.dumps(self.state, indent=1,
|
||||
sort_keys=True))
|
||||
|
||||
# ── 19L: next eligible item ──────────────────────────────────────
|
||||
|
||||
def next_item(self, behaviours: BehaviourLedger) -> dict | None:
|
||||
if self.state["paused"]:
|
||||
return None
|
||||
outstanding = behaviours.outstanding(
|
||||
component=self.state.get("scope_component"),
|
||||
project=self.state.get("project") or None)
|
||||
item = outstanding[0] if outstanding else None
|
||||
self.state["current_item"] = (item["behaviour_id"]
|
||||
if item else None)
|
||||
return item
|
||||
|
||||
# ── 19M: record result; dynamic queue re-derives from states ─────
|
||||
|
||||
def record(self, behaviours: BehaviourLedger, behaviour_id: str, *,
|
||||
passed: bool, detail: str = "", by: str = "user",
|
||||
generation: str = "") -> dict:
|
||||
rec = behaviours.record_real_world(
|
||||
behaviour_id, passed=passed, detail=detail, confirmed_by=by,
|
||||
generation=generation)
|
||||
self.state["completed_this_session"].append(
|
||||
{"behaviour_id": rec["behaviour_id"], "passed": passed,
|
||||
"at": _now()})
|
||||
if passed and rec["state"] == USER_CONFIRMED:
|
||||
pass # hardened; queue re-derives
|
||||
self.save()
|
||||
return rec
|
||||
|
||||
def pause(self) -> None:
|
||||
self.state["paused"] = True
|
||||
self.save()
|
||||
|
||||
def resume(self) -> None:
|
||||
self.state["paused"] = False
|
||||
self.save()
|
||||
|
||||
def note_blocker(self, behaviour_id: str, note: str) -> None:
|
||||
self.state["blockers"].append({"behaviour_id": behaviour_id,
|
||||
"note": note[:200], "at": _now()})
|
||||
self.save()
|
||||
|
||||
|
||||
# ── 19O: Observation → diagnosis → fix → retest ──────────────────────────
|
||||
|
||||
def record_observation(store_dir: Path | str, *, observed: str,
|
||||
expected: str, behaviour_id: str = "",
|
||||
environment: str = "", reproduction: str = "",
|
||||
candidate_areas: list[str] | None = None,
|
||||
confidence: str = "low") -> dict:
|
||||
"""Durable real-world failure report. Candidate areas are
|
||||
HYPOTHESES, not declared root cause (rule 19O)."""
|
||||
import json
|
||||
obs = {
|
||||
"observation_id": f"obs-{int(time.time())}",
|
||||
"observed": observed, "expected": expected,
|
||||
"behaviour_id": behaviour_id,
|
||||
"environment": environment, "reproduction": reproduction,
|
||||
"candidate_areas": list(candidate_areas or []), # hypotheses
|
||||
"confidence": confidence,
|
||||
"created_at": _now(),
|
||||
"closure": None, # set by close_observation
|
||||
}
|
||||
p = Path(store_dir) / "observations.jsonl"
|
||||
with p.open("a") as f:
|
||||
f.write(json.dumps(obs, sort_keys=True) + "\n")
|
||||
return obs
|
||||
|
||||
|
||||
def close_observation(store_dir: Path | str, observation_id: str, *,
|
||||
behaviour_retest_passed: bool,
|
||||
user_confirmed: bool,
|
||||
contract_ref: str = "") -> dict:
|
||||
"""Closure returns to the ORIGINAL observed scenario (rule 19O):
|
||||
code changed ≠ closure; automated green ≠ closure. The observation
|
||||
closes only when the behaviour was retested AND user-confirmed."""
|
||||
import json
|
||||
p = Path(store_dir) / "observations.jsonl"
|
||||
lines = p.read_text().strip().splitlines()
|
||||
out = []
|
||||
result = None
|
||||
for line in lines:
|
||||
rec = json.loads(line)
|
||||
if rec.get("observation_id") == observation_id:
|
||||
rec["closure"] = {
|
||||
"behaviour_retest_passed": behaviour_retest_passed,
|
||||
"user_confirmed": user_confirmed,
|
||||
"contract_ref": contract_ref,
|
||||
"closed": behaviour_retest_passed and user_confirmed,
|
||||
"at": _now()}
|
||||
result = rec
|
||||
out.append(json.dumps(rec, sort_keys=True))
|
||||
p.write_text("\n".join(out) + "\n")
|
||||
if result is None:
|
||||
raise ValueError(f"unknown observation {observation_id!r}")
|
||||
return result
|
||||
|
||||
|
||||
# ── 19P: forge projection through the T18 boundary ───────────────────────
|
||||
|
||||
def project_failure_to_forge(forge_adapter, repo: str, *,
|
||||
behaviour_id: str, observation: dict,
|
||||
dry_run: bool = False) -> dict:
|
||||
"""Create/update a human-visible issue for an active failure
|
||||
iteration. The forge record is a PROJECTION — canonical truth stays
|
||||
in the Behaviour Ledger. Forge failure never rewrites truth
|
||||
(rule 19P/19S-14): exceptions propagate to the caller as
|
||||
ForgeUnavailable; the ledger state is untouched."""
|
||||
title = f"[testing] {behaviour_id}: {observation.get('expected', '')[:60]}"
|
||||
body = (f"Observed: {observation.get('observed', '')}\n"
|
||||
f"Expected: {observation.get('expected', '')}\n"
|
||||
f"Behaviour: {behaviour_id}\n"
|
||||
f"Observation: {observation.get('observation_id')}\n"
|
||||
f"Reproduction: {observation.get('reproduction', '')}")
|
||||
if dry_run:
|
||||
return {"projected": False, "dry_run": True, "title": title}
|
||||
return forge_adapter.create_or_patch_release(
|
||||
repo, tag=f"testing-{behaviour_id}", title=title, body=body)
|
||||
|
||||
|
||||
# ── 19L: aggregate backend ────────────────────────────────────────────────
|
||||
|
||||
def aggregate(registry: ProjectRegistry, store_root: Path) -> list[dict]:
|
||||
"""Outstanding validation across ALL registered scaffolded projects
|
||||
(the /testing aggregate)."""
|
||||
out = []
|
||||
for entry in registry.all_projects():
|
||||
loc = Path(entry["scaffold_location"])
|
||||
if not (loc / "behaviour_ledger.json").is_file():
|
||||
out.append({"project": entry["project_id"],
|
||||
"outstanding": None,
|
||||
"note": "no behaviour ledger yet"})
|
||||
continue
|
||||
beh = BehaviourLedger(loc)
|
||||
s = beh.summary()
|
||||
registry.set_outstanding_validations(entry["project_id"],
|
||||
s["outstanding"])
|
||||
out.append({"project": entry["project_id"],
|
||||
"outstanding": s["outstanding"],
|
||||
"states": s["states"]})
|
||||
registry.save()
|
||||
return out
|
||||
@@ -999,6 +999,34 @@ def _search_files(args: dict) -> dict:
|
||||
return {"success": False, "error": f"Search failed: {e}"}
|
||||
|
||||
|
||||
def _resolve_execution_cwd(args: dict) -> tuple[str, str]:
|
||||
"""T21.4 §21.4E: automatic project execution context.
|
||||
|
||||
Precedence (tranche-mandated):
|
||||
explicit governed workdir > Project Registry repo/worktree for the
|
||||
named project > process cwd (project-less commands only).
|
||||
|
||||
The model names the PROJECT, never the cwd — the execution layer
|
||||
derives the correct repository/worktree via ProjectRegistry.resolve_cwd
|
||||
(which already honours active_worktree > repo_identity, and refuses
|
||||
scaffold_location as an execution cwd). An unknown or unresolvable
|
||||
project is REFUSED before any execution.
|
||||
"""
|
||||
workdir = args.get("workdir")
|
||||
if workdir:
|
||||
return str(workdir), "explicit"
|
||||
project = args.get("project")
|
||||
if project:
|
||||
from rune.project_registry import ProjectRegistry
|
||||
store = os.environ.get("RUNE_SCAFFOLD_STORE", "store/scaffold")
|
||||
registry = ProjectRegistry(store)
|
||||
try:
|
||||
return registry.resolve_cwd(str(project)), "project_registry"
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"project execution refused: {exc}") from exc
|
||||
return os.getcwd(), "process"
|
||||
|
||||
|
||||
def _run_command(args: dict) -> dict:
|
||||
"""Run an approved command, using argv unless shell mode is explicit."""
|
||||
import subprocess
|
||||
@@ -1006,7 +1034,11 @@ def _run_command(args: dict) -> dict:
|
||||
command = args.get("command")
|
||||
argv = args.get("argv")
|
||||
shell = args.get("shell", False)
|
||||
workdir = args.get("workdir", os.getcwd())
|
||||
try:
|
||||
workdir, cwd_source = _resolve_execution_cwd(args)
|
||||
except ValueError as exc:
|
||||
return {"success": False, "error": str(exc),
|
||||
"cwd_source": "refused"}
|
||||
|
||||
if shell is not True:
|
||||
if argv is None:
|
||||
@@ -1037,7 +1069,8 @@ def _run_command(args: dict) -> dict:
|
||||
output += f"\n[exit code: {result.returncode}]"
|
||||
if len(output) > 10000:
|
||||
output = output[:10000] + "\n[truncated]"
|
||||
return {"success": True, "result": output or "(no output)"}
|
||||
return {"success": True, "result": output or "(no output)",
|
||||
"cwd_source": cwd_source}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"success": False, "error": "Command timed out (30s)"}
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""wait_guard.py — T21 §21B: the sleep→poll regression detector.
|
||||
|
||||
Mirror of the MiMo Code controller guard (~/.mimocode/hooks/
|
||||
event-wait-guard.ts) in Python, so the CLASSIFICATION LOGIC is
|
||||
regression-tested in the Rune suite (rule 21B-4: the fix must survive
|
||||
model changes, compaction, provider swaps).
|
||||
|
||||
Detection: a command SEGMENT (split on ; | newline && ||) that combines
|
||||
a long sleep (>= 30s) with a status query of CI/deploy/release state.
|
||||
Heredoc bodies are stripped first (v2 precision fix — documentation
|
||||
about the pattern must not trip the guard).
|
||||
|
||||
Classification vocabulary (21B-1):
|
||||
USER_REQUESTED_DELAY bare sleep, user's explicit choice
|
||||
RATE_LIMIT_BACKOFF short bounded backoff (<30s)
|
||||
RETRY_BACKOFF bounded retry loop inside a script
|
||||
WATCHDOG deadline-based failure detection
|
||||
EVENT_FALLBACK_WITH_REASON declared no-event-source fallback
|
||||
SCHEDULED_TIMER cron/scheduled work
|
||||
UNJUSTIFIED_SLEEP DEFECT: sleep+poll with an event source available
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
LONG_SLEEP_RE = re.compile(r"\bsleep\s+(\d{2,4})\b")
|
||||
# T21.2 §21.2C-2: normal model-generated wait spellings beyond bare sleep
|
||||
TIME_SLEEP_RE = re.compile(r"time\.sleep\((\d{2,4})\)")
|
||||
FROM_TIME_SLEEP_RE = re.compile(r"from\s+time\s+import.*sleep")
|
||||
SLEEP_CALL_RE = re.compile(r"sleep\((\d{2,4})\)")
|
||||
POLL_LOOP_RE = re.compile(
|
||||
r"(while|until)\s+.*(status|success|failure|state|conclusion)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
STATUS_POLL_RE = re.compile(
|
||||
"|".join(
|
||||
(
|
||||
r"actions/runs",
|
||||
r"commits/.*/status",
|
||||
r"required.checks",
|
||||
r"deploy_qualify",
|
||||
r"releases",
|
||||
r"readlink.*rune-code/current",
|
||||
r"git\s+fetch",
|
||||
)
|
||||
),
|
||||
re.IGNORECASE,
|
||||
)
|
||||
THRESHOLD_SECS = 30
|
||||
|
||||
|
||||
def classify_wait(command: str) -> dict:
|
||||
"""Classify one shell command's wait behaviour. Returns
|
||||
{"classification": ..., "evidence": ...}. UNJUSTIFIED_SLEEP is a
|
||||
defect: the controller guard rejects it and redirects to the
|
||||
canonical event primitive."""
|
||||
# T21.2: scan for time.sleep BEFORE heredoc stripping — a
|
||||
# python heredoc containing time.sleep IS executable code.
|
||||
if TIME_SLEEP_RE.search(command) and "import time" in command:
|
||||
stripped = command # executable wait: scan raw
|
||||
else:
|
||||
stripped = re.sub(
|
||||
r"<<-?\s*['\"]?(\w+)['\"]?\n[\s\S]*?\n\1", " [heredoc] ", command
|
||||
)
|
||||
segments = [s.strip() for s in
|
||||
re.split(r"[;|\n]|&&|\|\|", stripped) if s.strip()]
|
||||
# T21.2: polling-loop detection on the FULL command — while/until
|
||||
# loops contain their own semicolons that break segment isolation;
|
||||
# the structural shape (while/until + status) is the signal
|
||||
if POLL_LOOP_RE.search(command) and STATUS_POLL_RE.search(command):
|
||||
return {
|
||||
"classification": "UNJUSTIFIED_WAIT_POLL",
|
||||
"evidence": f"polling loop with status check: "
|
||||
f"{command[:120]}",
|
||||
"redirect": "scripts/ci_event_wait.py --sha <sha>",
|
||||
}
|
||||
for i, segment in enumerate(segments):
|
||||
# T21.2: bare sleep, time.sleep, and polling-loop patterns
|
||||
m = (LONG_SLEEP_RE.search(segment) or TIME_SLEEP_RE.search(segment)
|
||||
or SLEEP_CALL_RE.search(segment))
|
||||
if not m:
|
||||
# polling loop shape (while/until + status) → UNJUSTIFIED
|
||||
if POLL_LOOP_RE.search(segment) and STATUS_POLL_RE.search(
|
||||
" ".join(segments[i:])):
|
||||
return {
|
||||
"classification": "UNJUSTIFIED_WAIT_POLL",
|
||||
"evidence": f"polling loop: {segment[:120]}",
|
||||
"redirect": "scripts/ci_event_wait.py --sha <sha>",
|
||||
}
|
||||
continue
|
||||
secs = int(m.group(1))
|
||||
if secs < THRESHOLD_SECS:
|
||||
continue
|
||||
# the regression pattern is sleep THEN check: the status poll may
|
||||
# be ANY number of segments after the sleep. T21.1 root cause
|
||||
# (live bypass 2026-09-16 12:59Z): a 6-segment multiline
|
||||
# `sleep 540 / cd / git fetch / ... / curl releases` walked past
|
||||
# a 3-segment lookahead. Everything after the sleep in THIS
|
||||
# command is downstream of it.
|
||||
lookahead = " ".join(segments[i:])
|
||||
if STATUS_POLL_RE.search(lookahead):
|
||||
# programmatic waits (time.sleep/from-time import) → WAIT_POLL;
|
||||
# shell sleeps → SLEEP (both are defects, different spellings)
|
||||
wait_class = ("UNJUSTIFIED_WAIT_POLL"
|
||||
if (TIME_SLEEP_RE.search(lookahead)
|
||||
or SLEEP_CALL_RE.search(lookahead))
|
||||
else "UNJUSTIFIED_SLEEP")
|
||||
return {
|
||||
"classification": wait_class,
|
||||
"evidence": f"sleep {secs}s then status poll: "
|
||||
f"{' / '.join(segments[i:i + 2])[:140]}",
|
||||
"redirect": "scripts/ci_event_wait.py --sha <sha>",
|
||||
}
|
||||
if re.match(r"^\s*sleep\s+\d+\s*$", stripped.strip()):
|
||||
return {"classification": "USER_REQUESTED_DELAY", "evidence": stripped.strip()}
|
||||
return {"classification": "NO_WAIT", "evidence": ""}
|
||||
@@ -0,0 +1,46 @@
|
||||
"""wait_guard_corpus.py — T21.3 §21.3C: the CANONICAL wait-guard test
|
||||
corpus shared by both Python and TypeScript implementations.
|
||||
|
||||
Each case: {id, command, expected_class}. Both the Python
|
||||
wait_guard.classify_wait() and the TypeScript hook MUST agree on every
|
||||
case. A change to either implementation that breaks parity fails this
|
||||
suite.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# Canonical corpus — both implementations must agree on every case.
|
||||
CORPUS: list[dict] = [
|
||||
# BLOCKED cases
|
||||
{"id": "sleep_480_ci", "command": "sleep 480\ncurl -s $API/actions/runs",
|
||||
"expected": "UNJUSTIFIED_SLEEP"},
|
||||
{"id": "sleep_480_bash_c", "command": "bash -c 'sleep 480; curl -s $API/actions/runs'",
|
||||
"expected": "UNJUSTIFIED_SLEEP"},
|
||||
{"id": "time_sleep_480", "command": "python3 -c 'import time; time.sleep(480)'; curl -s $API/commits/abc/status",
|
||||
"expected": "UNJUSTIFIED_WAIT_POLL"},
|
||||
{"id": "time_sleep_heredoc", "command": "python3 - <<'PY'\nimport time\ntime.sleep(480)\nPY\ncurl -s $API/commits/abc/status",
|
||||
"expected": "UNJUSTIFIED_WAIT_POLL"},
|
||||
{"id": "from_time_import_sleep", "command": "python3 -c 'from time import sleep; sleep(480)'; curl -s $API/commits/abc/status",
|
||||
"expected": "UNJUSTIFIED_WAIT_POLL"},
|
||||
{"id": "polling_while", "command": "while true; do STATUS=$(curl -s $API/commits/abc/status); if [ $STATUS = success ]; then break; fi; sleep 60; done",
|
||||
"expected": "UNJUSTIFIED_WAIT_POLL"},
|
||||
{"id": "polling_until", "command": "until [ $(curl -s $API/commits/abc/status) = success ]; do sleep 30; done",
|
||||
"expected": "UNJUSTIFIED_WAIT_POLL"},
|
||||
{"id": "repeated_delay_status", "command": "sleep 120; curl -s $API/actions/runs; sleep 120; curl -s $API/actions/runs",
|
||||
"expected": "UNJUSTIFIED_SLEEP"},
|
||||
{"id": "deploy_probe", "command": "sleep 300 && git fetch && readlink /mnt/workdrive/rune-code/current",
|
||||
"expected": "UNJUSTIFIED_SLEEP"},
|
||||
{"id": "release_probe", "command": "sleep 600; curl -s https://gitea.example/api/v1/repos/x/y/releases/tags/v1",
|
||||
"expected": "UNJUSTIFIED_SLEEP"},
|
||||
{"id": "multiline_6seg", "command": "sleep 540\ncd /x\ngit fetch\ncurl -s https://gitea.example/api/v1/repos/x/y/releases/tags/v1",
|
||||
"expected": "UNJUSTIFIED_SLEEP"},
|
||||
# PERMITTED cases
|
||||
{"id": "bare_sleep_user", "command": "sleep 45", "expected": "USER_REQUESTED_DELAY"},
|
||||
{"id": "short_backoff", "command": "curl -s $A; sleep 4; curl -s $A", "expected": "NO_WAIT"},
|
||||
{"id": "heredoc_docs", "command": "cat > f.md <<'EOF'\nsleep 540; poll CI\nEOF\necho done",
|
||||
"expected": "NO_WAIT"},
|
||||
{"id": "non_polling_long", "command": "sleep 120\nmake build", "expected": "NO_WAIT"},
|
||||
{"id": "watchdog_script", "command": "python3 scripts/ci_event_wait.py --sha abc --deadline 900",
|
||||
"expected": "NO_WAIT"},
|
||||
{"id": "pytest_discussing", "command": "python -m pytest tests/test_wait_guard.py -q",
|
||||
"expected": "NO_WAIT"},
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T20:50:30Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "matched_by": "pending_match", "model_deadline_ignored": true, "model_op_type_ignored": true, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-756ebe6f45ad83a919738539a1565c17ed8572ac"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T20:51:20Z", "authority": "deploy_qualify", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_ancestor_of_master", "model_deadline_ignored": true, "model_op_type_ignored": true, "normalized": true, "op_type": "master_ci", "operation_id": "ci-f7256aa29e8bc7aa365ad7ffa27eb557528ccca5"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T21:50:39Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_on_feature_branch", "model_deadline_ignored": true, "model_op_type_ignored": true, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T21:53:34Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_on_feature_branch", "model_deadline_ignored": false, "model_op_type_ignored": false, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T21:55:05Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_on_feature_branch", "model_deadline_ignored": false, "model_op_type_ignored": false, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T23:19:18Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "matched_by": "sleep_poll_intent", "model_deadline_ignored": true, "model_op_type_ignored": false, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-756ebe6f45ad83a919738539a1565c17ed8572ac", "raw_command_kind": "sleep_poll", "raw_sleep_seconds_suppressed": 150}
|
||||
@@ -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": []
|
||||
}
|
||||
Loaded 100 of 201 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user