Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c12c155dd8 | ||
|
|
c01d33f6e8 | ||
|
|
edc20b9e42 | ||
|
|
a3ae72d353 | ||
|
|
dc99f1fac3 |
No files matched your search
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: land
|
||||
description: Review and land one small Rune change through the governed PR workflow.
|
||||
disable-model-invocation: true
|
||||
metadata:
|
||||
delta-action: land
|
||||
---
|
||||
|
||||
# Land one small diff
|
||||
|
||||
1. Read applicable `AGENTS.md` rules and the repo's PR/merge workflow.
|
||||
2. Confirm the issue, exact diff, target branch, and permission to publish and land.
|
||||
3. Keep one small, reversible change; preserve unrelated work.
|
||||
4. For bugs, follow `docs/bugfix-contract.md`: red test first, root cause, discharge.
|
||||
5. Run required tests, static analysis, canaries, and henxels; record results.
|
||||
6. Get independent `/review` per `docs/adversarial-review.md`; clear blockers.
|
||||
7. Inspect remotes; publish the PR to the approved forge, never `local`.
|
||||
8. Require human approval and green required CI on the exact PR head; recheck changed heads.
|
||||
9. Load `report-landed`; use the repo's merge workflow. Stop on conflicts; never force-push or bypass hooks.
|
||||
10. Verify the target commit and CI, then report the actual landing result and remaining blockers.
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
# #149 Coverage ratchet — both floors live here, and they only ever rise.
|
||||
# Doctrine (issue #149 evidence trail): CPython devguide — "any increase is a
|
||||
# good one"; a floor is committed once measured and never lowered afterwards.
|
||||
#
|
||||
# Global floor = [report] fail_under below (measured 2026-09-07 at the
|
||||
# #149 baseline; raise it when coverage grows, never lower).
|
||||
# Changed-lines floor = diff-cover --fail-under=70 in .gitea/workflows/ci.yml
|
||||
# (changed code may not reduce tested coverage; 70% start,
|
||||
# ratchetable upward after the first clean pass).
|
||||
#
|
||||
# Regenerating the CI lock after touching this file is NOT required (tool
|
||||
# config, not a dependency), but the floors above are contract: edit them
|
||||
# only to raise.
|
||||
#
|
||||
# Canary files are omitted: src/rune/canary_*.py are standalone CI
|
||||
# verification scripts executed directly by the canary job, never imported by
|
||||
# the test suite — counting them would make the global floor a fiction.
|
||||
# ffi_query_memory_smoke.py is the same class: a standalone evidence script
|
||||
# driven directly (and by tests/test_ffi_query_memory_smoke.py when the cdylib
|
||||
# is built), never imported by the suite.
|
||||
|
||||
[run]
|
||||
source = src/rune
|
||||
omit =
|
||||
tests/*
|
||||
scripts/*
|
||||
src/rune/canary_*.py
|
||||
src/rune/ffi_query_memory_smoke.py
|
||||
|
||||
[report]
|
||||
fail_under = 29
|
||||
show_missing = False
|
||||
@@ -1,46 +1,16 @@
|
||||
---
|
||||
name: Bug Fix
|
||||
about: Demonstrate currently incorrect behaviour — plain English first, proof required
|
||||
about: Fix a bug or issue
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
## Plain summary
|
||||
<!-- 2-4 sentences, NON-TECHNICAL: what's broken and why it matters. -->
|
||||
|
||||
|
||||
What is broken: <what a person observes going wrong>
|
||||
|
||||
What should happen instead: <the correct behaviour>
|
||||
|
||||
How often / when: <always | after X | since change Y>
|
||||
|
||||
## Proof it is broken (required — no proof, no bug)
|
||||
<!-- A bug is DEMONSTRATED currently incorrect behaviour. Nothing else.
|
||||
If this section cannot be filled, file it as a question, not a bug. -->
|
||||
- [ ] Reproduction command or steps (deterministic where possible)
|
||||
- [ ] The exact wrong output observed: <paste/log excerpt>
|
||||
- [ ] Where the evidence lives: <log path, fixture, session id>
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
## What would let us close this
|
||||
- [ ] The reproduction command now shows correct behaviour
|
||||
- [ ] A regression test exists that fails before the fix and passes after
|
||||
- [ ] No other behaviour changed (or changes are listed and justified)
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- A bug fix is USUALLY a patch. Pick major or minor only with a reason. -->
|
||||
- [ ] patch — same behaviour, done correctly (usual for bugs)
|
||||
- [ ] major — the bug was in a published contract and fixing it breaks consumers (justify)
|
||||
- [ ] minor — fixing revealed a missing capability that had to be added (justify)
|
||||
## Example
|
||||
<!-- A concrete before/after: "Doing X currently produces Y; it should produce Z." -->
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT (the bug) / WHERE (files) / EVIDENCE (how it was confirmed). -->
|
||||
@@ -59,6 +29,10 @@ How often / when: <always | after X | since change Y>
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit references
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
|
||||
@@ -1,44 +1,18 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: New feature or improvement — plain English first, verifiable close conditions, version intent
|
||||
about: New feature or improvement — DESCRIPTION GATE v4 format (plain summary + pipeline requirements)
|
||||
title: "[FEATURE] "
|
||||
labels: feature
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. A non-technical reader must be able to retell this
|
||||
section. 3-6 sentences, no jargon. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
What is this about: <what>
|
||||
|
||||
Why do we need it: <the itch — what happens today that shouldn't, or what becomes possible>
|
||||
|
||||
Why is it still open: <"not started yet" | "part done, X remains" | "deliberately parked as a future idea">
|
||||
## Plain summary
|
||||
<!-- 2-4 sentences, NON-TECHNICAL: what this does and why it matters. Nick should
|
||||
understand the goal without decoding internals. -->
|
||||
|
||||
## Example
|
||||
<!-- A concrete before/after in plain terms: "If X happens, the system will Y instead of Z." -->
|
||||
|
||||
## What would let us close this
|
||||
<!-- Checkable statements, verifiable by looking at something that EXISTS —
|
||||
a file, a log line, a passing test. Not "when appropriate". -->
|
||||
- [ ] <condition 1 — observable>
|
||||
- [ ] <condition 2>
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- The pipeline derives the real bump from merged conventional commits and
|
||||
compares against this declaration. A mismatch is flagged, not fatal:
|
||||
either the declaration was wrong or the work grew. Both facts matter. -->
|
||||
- [ ] major — breaking: changes a contract, schema, or interface others depend on
|
||||
- [ ] minor — new capability: something becomes possible that wasn't
|
||||
- [ ] patch — repair: same behaviour, done correctly
|
||||
- [ ] none — touches no released artifact (docs, memory, process notes)
|
||||
<!-- A concrete example of the intended behavior, in plain terms:
|
||||
"If X happens, the system will Y instead of Z." -->
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT — one-line objective.
|
||||
@@ -63,6 +37,7 @@ DEPENDENCIES — parent epic + preceding stage/plan refs. -->
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit/PR references
|
||||
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
@@ -94,4 +69,4 @@ Hard invariants: derived cognition preserves evidentiary ancestry · epistemic c
|
||||
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
|
||||
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
|
||||
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
name: Research Submission
|
||||
about: Buy down uncertainty before building — question, falsifier, budget, durable findings
|
||||
title: "[RESEARCH] "
|
||||
labels: research
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
The question: <what we're trying to learn, one sentence>
|
||||
|
||||
Why we're asking: <what decision this research will inform — a name, an issue
|
||||
number, or "exploratory: none yet">
|
||||
|
||||
What we believe today: <the current assumption, stated so it can be proven wrong>
|
||||
|
||||
## The shape of an answer
|
||||
- [ ] What evidence would settle the question: <observation, benchmark, or spike>
|
||||
- [ ] What evidence would KILL the current assumption (the falsifier): <be specific —
|
||||
this is stated BEFORE starting so the research cannot become a search for confirmation>
|
||||
- [ ] Budget: <time/effort cap — when research must stop and report back>
|
||||
|
||||
## What would let us close this
|
||||
- [ ] Findings written to a durable document: <path, e.g. research/issue-NNN/> —
|
||||
a comment thread is not a durable finding
|
||||
- [ ] The decision the research informs is recorded: made / deferred / changed
|
||||
- [ ] If a follow-up build issue was minted: <#N or "none needed">
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
- [ ] none — research changes no released artifact (usual)
|
||||
- [ ] minor/major — only if a code spike is being merged as part of this (justify)
|
||||
|
||||
## Technical detail
|
||||
<!-- Candidate approaches, prior art in the repo, related issues, constraints,
|
||||
rebuildable-projection or authority requirements if findings touch canonical state. -->
|
||||
|
||||
## Pipeline requirements
|
||||
- [ ] Findings committed under the stated durable path
|
||||
- [ ] Every citation verified against a real source (no fabricated references)
|
||||
- [ ] Closed with a properly formatted closing comment: findings summary + decision
|
||||
|
||||
## Cognitive invariants (memory/cognition research — both handoffs)
|
||||
<!-- Complete ONLY if findings touch memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before proposing modules)
|
||||
- [ ] **Read/write authority** — which canonical state is affected
|
||||
- [ ] **Provenance** — findings traceable to evidence
|
||||
- [ ] **Reversibility** — projections remain rebuildable; no unique truth moves into a derived index
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · retrieval never increases utility · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Life (Nick's)
|
||||
- [ ] Custodian
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — blocks a decision that blocks other work
|
||||
- [ ] **P1 — high** — informs a next-stage dependency
|
||||
- [ ] **P2 — medium** — quality/consolidation understanding
|
||||
- [ ] **P3 — low** — watchlist / deferred / exploratory
|
||||
@@ -83,9 +83,3 @@ DEPENDENCIES (epic + issues). -->
|
||||
|
||||
## Plain-language review notes
|
||||
<!-- Explain what Nick should inspect, approve, reject, or question. -->
|
||||
|
||||
## Research provenance
|
||||
<!-- If this PR's issue went through a research round: link the research file
|
||||
(research/issue-<N>-research.md) and state which synthesis decisions were
|
||||
adopted. If no research round: write "none" — the spec-gate will flag it. -->
|
||||
-->
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
backup:
|
||||
runs-on: rune-ci-desktop
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
@@ -18,4 +18,4 @@ jobs:
|
||||
python3 -m unittest -v tests.test_gitea_backup
|
||||
python3 src/rune/canary_gitea_automation.py
|
||||
- name: Create consistent off-disk Gitea backup
|
||||
run: sudo /usr/local/bin/gitea-backup-pc.sh
|
||||
run: sudo /usr/local/sbin/rune-gitea-backup
|
||||
+55
-140
@@ -7,38 +7,49 @@ on:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
# SINGLE-PASS FILING (2026-10-07, Nick's first-time-correct directive):
|
||||
# the old design ran TWO jobs on submission — priority-gate (which saw
|
||||
# no P-label and posted the ⚠️ warning) and auto-label (which inferred
|
||||
# and applied the P-label). The gate ran BEFORE the inference that
|
||||
# would have made the warning unnecessary: every keyword-inferrable
|
||||
# submission ate a spurious warning, a flag add/remove cycle, and a
|
||||
# wasted workflow run. The fix is ORDER: infer → apply → THEN gate.
|
||||
# A submission whose priority is inferable gets its P-label in this
|
||||
# same run; the warning fires only when inference genuinely produced
|
||||
# nothing (a true filing gap, not a race the robot loses to itself).
|
||||
auto-label:
|
||||
runs-on: rune-ci-desktop
|
||||
# PRIORITY GATE (2026-09-04): every issue must carry exactly one of
|
||||
# P0/P1/P2/P3. An issue that lands without one gets 'unprioritized' +
|
||||
# a demand comment — filing is not complete until prioritized.
|
||||
priority-gate:
|
||||
if: github.event_name == 'issues' && github.event.action != 'labeled'
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Infer, apply, then gate-check (one pass)
|
||||
- name: Enforce priority label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// CLOSED-ISSUE GUARD (#407): labels are filing-time metadata.
|
||||
// A closed issue's lifecycle has ended; re-running label
|
||||
// inference on it re-adds stale type labels (bug = currently
|
||||
// incorrect behavior — a closed fixed issue must not carry
|
||||
// it) and makes any human label cleanup self-defeating,
|
||||
// because every PUT fires a 'labeled' event that re-triggers
|
||||
// this job. Post-close label curation belongs to humans and
|
||||
// the conductor, not this workflow.
|
||||
const issueState = context.payload.issue?.state;
|
||||
if (issueState === 'closed') {
|
||||
console.log(`Issue #${context.payload.issue?.number} is closed — auto-label skips (labels are curated post-close).`);
|
||||
const issue = context.payload.issue;
|
||||
if (!issue) return;
|
||||
const labels = (issue.labels || []).map(l => l.name);
|
||||
const priorities = ['P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low'];
|
||||
const has = labels.filter(l => priorities.includes(l));
|
||||
if (has.length >= 1) {
|
||||
// Prioritized — clear the flag if it was set.
|
||||
if (labels.includes('unprioritized')) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number, name: 'unprioritized',
|
||||
}).catch(e => { if (e.status !== 404) throw e; });
|
||||
}
|
||||
return;
|
||||
}
|
||||
const action = context.payload.action;
|
||||
const isIssue = context.payload.issue !== undefined;
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number, labels: ['unprioritized'],
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `⚠️ **Priority gate:** this issue has no priority. Assign exactly one of **P0 — critical** (spine/correctness/blocks), **P1 — high** (material value or real defect), **P2 — medium** (quality/consolidation), **P3 — low** (measurement/deferred). An issue without a priority is not filed.`,
|
||||
});
|
||||
|
||||
auto-label:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Analyze and label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const title = context.payload.issue?.title || context.payload.pull_request?.title || '';
|
||||
const body = context.payload.issue?.body || context.payload.pull_request?.body || '';
|
||||
const text = (title + ' ' + body).toLowerCase();
|
||||
@@ -47,44 +58,9 @@ jobs:
|
||||
const managedLabels = new Set([
|
||||
'memory', 'governance', 'identity', 'growth',
|
||||
'perception', 'infrastructure', 'life', 'custodian',
|
||||
'bug', 'feature', 'research',
|
||||
'P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low',
|
||||
'priority: high', 'bug', 'feature',
|
||||
]);
|
||||
|
||||
// Type label: DECLARED over GUESSED. The template spine
|
||||
// (kind: bug|feature|research|epic|...) is authoritative.
|
||||
// A recognized kind maps to its label; ANY other declared kind
|
||||
// (e.g. epic) means NO type label and NO keyword guessing —
|
||||
// epics are containers, not bugs. Keyword heuristics apply
|
||||
// only to pre-template issues with no spine at all.
|
||||
let isBug = false, isFeature = false, isResearch = false;
|
||||
const kindMatch = body && body.match(/^kind:\s*([a-z_-]+)\s*$/m);
|
||||
if (kindMatch) {
|
||||
const kind = kindMatch[1];
|
||||
isBug = kind === 'bug';
|
||||
isFeature = kind === 'feature';
|
||||
isResearch = kind === 'research';
|
||||
} else if (action === 'opened') {
|
||||
// KEYWORD FALLBACK (#407): pre-template issues only, and
|
||||
// ONLY at filing time. Re-running keyword guesses on every
|
||||
// edit/label event overrides human curation of spine-less
|
||||
// issues (documented rule: keyword heuristics are the
|
||||
// pre-template fallback, not a standing authority).
|
||||
const textBug = title.startsWith('[BUG]') || text.includes('bug') || text.includes('fix') || text.includes('broken');
|
||||
isBug = textBug;
|
||||
isFeature = !isBug && (title.startsWith('[FEATURE]') || text.includes('feature') || text.includes('improve') || text.includes('add'));
|
||||
isResearch = !isBug && !isFeature && title.startsWith('[RESEARCH]');
|
||||
}
|
||||
if (isBug) {
|
||||
desiredLabels.add('bug');
|
||||
}
|
||||
if (isFeature) {
|
||||
desiredLabels.add('feature');
|
||||
}
|
||||
if (isResearch) {
|
||||
desiredLabels.add('research');
|
||||
}
|
||||
|
||||
// Umbrella labels
|
||||
if (text.includes('memory') || text.includes('store') || text.includes('recall') || text.includes('consolidat')) {
|
||||
desiredLabels.add('memory');
|
||||
@@ -111,31 +87,19 @@ jobs:
|
||||
desiredLabels.add('custodian');
|
||||
}
|
||||
|
||||
// Priority labels (2026-10-05: PROGRAMMATIC P-ASSIGNMENT —
|
||||
// the gate demanded P0-P3 labels but nothing assigned them;
|
||||
// the dead 'priority: high' label is gone per the 2026-09-04
|
||||
// normalization). Assignment from the TITLE prefix first
|
||||
// ([P1]/P1:/priority=...), then keyword inference using the
|
||||
// gate's own rule set (the scale lives in ONE place — the
|
||||
// gate check at the bottom of THIS job).
|
||||
const pMatch = title.match(/\bP([0-3])\b/);
|
||||
const priorityLabelNames = ['P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low'];
|
||||
if (pMatch) {
|
||||
desiredLabels.add(priorityLabelNames[Number(pMatch[1])]);
|
||||
} else if (text.includes('urgent') || text.includes('critical') ||
|
||||
text.includes('spine') || text.includes('blocks other work')) {
|
||||
desiredLabels.add(priorityLabelNames[0]); // P0 — critical
|
||||
} else if (text.includes('broken') || text.includes('fix') ||
|
||||
text.includes('repair') || text.includes('defect') ||
|
||||
text.includes('real defect') || text.includes('money')) {
|
||||
desiredLabels.add(priorityLabelNames[1]); // P1 — high
|
||||
} else if (text.includes('improve') || text.includes('consolidat') ||
|
||||
text.includes('refactor') || text.includes('kernel') ||
|
||||
text.includes('port')) {
|
||||
desiredLabels.add(priorityLabelNames[2]); // P2 — medium
|
||||
} else if (text.includes('benchmark') || text.includes('measurement') ||
|
||||
text.includes('deferred')) {
|
||||
desiredLabels.add(priorityLabelNames[3]); // P3 — low
|
||||
// Priority labels
|
||||
if (text.includes('urgent') || text.includes('critical') || text.includes('broken')) {
|
||||
desiredLabels.add('priority: high');
|
||||
}
|
||||
|
||||
// Type labels
|
||||
const isBug = title.startsWith('[BUG]') || text.includes('bug') || text.includes('fix') || text.includes('broken');
|
||||
const isFeature = !isBug && (title.startsWith('[FEATURE]') || text.includes('feature') || text.includes('improve') || text.includes('add'));
|
||||
if (isBug) {
|
||||
desiredLabels.add('bug');
|
||||
}
|
||||
if (isFeature) {
|
||||
desiredLabels.add('feature');
|
||||
}
|
||||
|
||||
const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
|
||||
@@ -145,7 +109,6 @@ jobs:
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
const currentLabels = new Set(current.data.map(label => label.name));
|
||||
const currentLabelIds = new Map(current.data.map(label => [label.name, label.id]));
|
||||
const staleManaged = [...currentLabels].filter(
|
||||
label => managedLabels.has(label) && !desiredLabels.has(label)
|
||||
);
|
||||
@@ -155,14 +118,11 @@ jobs:
|
||||
|
||||
for (const name of staleManaged) {
|
||||
try {
|
||||
// Gitea's DELETE /labels/{id} requires the numeric label ID.
|
||||
// Octokit's GitHub-style removeLabel(name) returned 404 and
|
||||
// left stale priority labels behind (#2467: P0 AND P2).
|
||||
await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{id}', {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
id: currentLabelIds.get(name),
|
||||
name,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
@@ -180,50 +140,5 @@ jobs:
|
||||
labels: missingDesired,
|
||||
});
|
||||
}
|
||||
|
||||
// ---- PRIORITY GATE (runs LAST — after inference and
|
||||
// reconciliation, so the verdict reflects the labels THIS run
|
||||
// just applied, not a pre-inference snapshot). Issue-scoped:
|
||||
// the filing contract governs issues; PRs get inferred labels
|
||||
// but are not filing units.
|
||||
if (!isIssue) {
|
||||
console.log(`PR #${issueNumber}: labels applied, gate skipped (gate is issue-scoped).`);
|
||||
return;
|
||||
}
|
||||
const reconciled = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
const actual = reconciled.data;
|
||||
const activePriority = actual.filter(l => priorityLabelNames.includes(l.name));
|
||||
const flag = actual.find(l => l.name === 'unprioritized');
|
||||
if (activePriority.length === 1) {
|
||||
// Prioritized (by human or by this run's inference). Clear
|
||||
// any stale flag — no warning is ever posted on this path.
|
||||
if (flag) {
|
||||
await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{id}', {
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber, id: flag.id,
|
||||
});
|
||||
}
|
||||
console.log(`Priority gate: satisfied (${activePriority[0].name}).`);
|
||||
return;
|
||||
}
|
||||
// Genuinely unprioritized: inference produced nothing AND the
|
||||
// caller set nothing. Flag + demand comment — the filing gap
|
||||
// is real, not a race artifact.
|
||||
const reason = activePriority.length === 0
|
||||
? 'no priority'
|
||||
: `${activePriority.length} priority labels (exactly one required)`;
|
||||
if (!flag) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber, labels: ['unprioritized'],
|
||||
});
|
||||
}
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: `⚠️ **Priority gate:** this issue has ${reason}. Assign exactly one of **P0 — critical** (spine/correctness/blocks), **P1 — high** (material value or real defect), **P2 — medium** (quality/consolidation), **P3 — low** (measurement/deferred). An issue without a priority is not filed.`,
|
||||
});
|
||||
console.log(`Priority gate: flagged — ${reason}.`);
|
||||
console.log(`Removed managed labels: ${staleManaged.join(', ') || 'none'}`);
|
||||
console.log(`Added managed labels: ${missingDesired.join(', ') || 'none'}`);
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
update-gitea:
|
||||
runs-on: rune-ci-desktop
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Check current Gitea version
|
||||
id: current
|
||||
|
||||
@@ -1,330 +1,80 @@
|
||||
name: Auto-version and Release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
types: [closed]
|
||||
branches: [master]
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: rune-ci-desktop
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Skip if PR is not merged (direct pushes continue to convergence)
|
||||
run: "MERGED=\"${{ gitea.event.pull_request.merged }}\"\nEVENT=\"${{ gitea.event_name\
|
||||
\ }}\"\nif [ \"$EVENT\" = \"pull_request\" ] && [ \"$MERGED\" != \"true\"\
|
||||
\ ]; then\n echo \"PR not merged \u2014 skipping release\"\n exit 0\nfi\n\
|
||||
echo \"event=$EVENT \u2014 continuing\"\n"
|
||||
- name: Read reviewed semantic version
|
||||
run: 'VERSION=$(python3 -c ''import re; text=open("src/rune/__init__.py").read();
|
||||
print(re.search(r"__version__ = \"([0-9]+\.[0-9]+\.[0-9]+)\"", text).group(1))'')
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
echo "RELEASE_VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
- name: Skip if PR is not merged
|
||||
run: |
|
||||
MERGED="${{ gitea.event.pull_request.merged }}"
|
||||
if [ "$MERGED" != "true" ]; then
|
||||
echo "PR not merged — skipping release"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Release version: $VERSION"
|
||||
- name: Read reviewed semantic version
|
||||
run: |
|
||||
VERSION=$(python3 -c 'import re; text=open("src/rune/__init__.py").read(); print(re.search(r"__version__ = \"([0-9]+\.[0-9]+\.[0-9]+)\"", text).group(1))')
|
||||
echo "RELEASE_VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
echo "Release version: $VERSION"
|
||||
|
||||
'
|
||||
- name: Auto-bump if the PR did not carry a version bump
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
PR_TITLE: ${{ gitea.event.pull_request.title }}
|
||||
PR_BODY: ${{ gitea.event.pull_request.body }}
|
||||
PR_NUMBER: ${{ gitea.event.pull_request.number }}
|
||||
run: "LATEST=$(python3 scripts/tag_repair.py --latest-tag --namespace main)\nLATEST=\"${LATEST#v}\"\nif [ -z\
|
||||
\ \"$LATEST\" ]; then\n echo \"No previous tags \u2014 releasing first version\
|
||||
\ $RELEASE_VERSION\"\n exit 0\nfi\nif [ \"$LATEST\" != \"$RELEASE_VERSION\"\
|
||||
\ ]; then\n if [ \"$(printf '%s\\n%s\\n' \"$LATEST\" \"$RELEASE_VERSION\"\
|
||||
\ | sort -V | head -1)\" = \"$RELEASE_VERSION\" ]; then\n echo \"::warning::VERSION-BEHIND-TAG\
|
||||
\ ANOMALY (#192): master=$RELEASE_VERSION < latest tag v$LATEST \u2014 bumping\
|
||||
\ from master version; reconcile the tag lineage\"\n else\n echo \"Version\
|
||||
\ $RELEASE_VERSION > last tag v$LATEST \u2014 bump rode the PR\"\n exit\
|
||||
\ 0\n fi\nfi\n# semver level: PR title on pull_request events; on push events\n\
|
||||
# (direct master pushes bypass PR merges \u2014 observed 2026-09-10,\n# version\
|
||||
\ stuck at 0.66.0 through ~15 substantive commits), the\n# warranted bump\
|
||||
\ comes from scripts/version_convergence.py over\n# commits since the last\
|
||||
\ tag. chore(release) commits never\n# warrant a bump \u2192 the bump commit\
|
||||
\ cannot recurse.\nif [ -z \"$PR_TITLE\" ] || [ \"$PR_TITLE\" = \"null\" ];\
|
||||
\ then\n # Recursive-loop guard: HEAD being a bump commit means THIS\n #\
|
||||
\ push already applied the bump \u2014 do not re-bump. The decision\n # lives\
|
||||
\ in version_convergence (--is-release-subject), one\n # source of truth\
|
||||
\ shared with the regression test.\n HEAD_SUBJECT=$(git log -1 --format=%s)\n\
|
||||
\ if python3 scripts/version_convergence.py --is-release-subject \"$HEAD_SUBJECT\"\
|
||||
; then\n echo \"HEAD is a release bump \u2014 loop guard, no re-bump\"\n\
|
||||
\ exit 0\n fi\n CONVERGENCE=$(python3 scripts/version_convergence.py)\n\
|
||||
\ WARRANTED=$(echo \"$CONVERGENCE\" | python3 -c \"import json,sys; d=json.load(sys.stdin);\
|
||||
\ print(d.get('warranted_bump') or '')\")\n CONVERGED=$(echo \"$CONVERGENCE\"\
|
||||
\ | python3 -c \"import json,sys; print(json.load(sys.stdin).get('converged',\
|
||||
\ 'true'))\")\n if [ \"$CONVERGED\" = \"True\" ]; then\n echo \"Already\
|
||||
\ converged \u2014 no bump needed\"\n exit 0\n fi\n echo \"push convergence:\
|
||||
\ warranted_bump=$WARRANTED\"\n if [ -z \"$WARRANTED\" ]; then\n echo\
|
||||
\ \"No substantive commits since last tag \u2014 no bump needed\"\n exit\
|
||||
\ 0\n fi\n PR_TITLE=\"$WARRANTED: converged release (push-to-master bump)\"\
|
||||
\nfi\nNEW=$(python3 scripts/semver_bump.py \"$RELEASE_VERSION\" \"$PR_TITLE\"\
|
||||
)\nexport NEW_VERSION=\"$NEW\"\necho \"Auto-bump: $RELEASE_VERSION -> $NEW\
|
||||
\ (direct commit to master \u2014 bump PRs retired, Nick policy 2026-09-08)\"\
|
||||
\npython3 - <<'PYBUMP'\nimport base64, json, os, sys, urllib.request\n\nTOKEN\
|
||||
\ = os.environ[\"GITEA_TOKEN\"]\nBASE = os.environ[\"RELEASE_SERVER_URL\"\
|
||||
] + \"/api/v1/repos/\" + os.environ[\"RELEASE_REPO\"]\nVERSION = os.environ[\"\
|
||||
RELEASE_VERSION\"]\nNEW = os.environ[\"NEW_VERSION\"]\nPR_TITLE = os.environ.get(\"\
|
||||
PR_TITLE\", \"\")\nPR_NUMBER = os.environ.get(\"PR_NUMBER\", \"\")\nSERVER\
|
||||
\ = os.environ[\"RELEASE_SERVER_URL\"].rstrip(\"/\")\npromoting_url = f\"\
|
||||
{SERVER}/{os.environ['RELEASE_REPO']}/pulls/{PR_NUMBER}\" if PR_NUMBER else\
|
||||
\ \"(unknown)\"\n\ndef api(path, method=\"GET\", data=None, ok=(200, 201)):\n\
|
||||
\ req = urllib.request.Request(\n BASE + path,\n data=json.dumps(data).encode()\
|
||||
\ if data is not None else None,\n headers={\"Authorization\": \"token\
|
||||
\ \" + TOKEN, \"Content-Type\": \"application/json\"},\n method=method,\n\
|
||||
\ )\n try:\n with urllib.request.urlopen(req, timeout=30) as\
|
||||
\ r:\n body, status = r.read(), r.status\n except urllib.error.HTTPError\
|
||||
\ as e:\n body, status = e.read(), e.code\n try:\n return\
|
||||
\ status, (json.loads(body) if body else {})\n except json.JSONDecodeError:\n\
|
||||
\ return status, {\"_raw\": body.decode(\"utf-8\", \"replace\")[:200]}\n\
|
||||
\n# Version surfaces: the TUI footer renders rune.__version__ AT CALL TIME\n\
|
||||
# (rune_tui.py VersionLabel), so __init__.py IS the displayed version.\n#\
|
||||
\ rune_tui.py header is the drift-guard that must stay in sync.\nsurfaces\
|
||||
\ = {\n \"src/rune/__init__.py\": [(f'__version__ = \"{VERSION}\"', f'__version__\
|
||||
\ = \"{NEW}\"')],\n \"src/rune/rune_tui.py\": [\n (f\"Version: v{VERSION}\"\
|
||||
, f\"Version: v{NEW}\"),\n (f\"Rune v{VERSION} |\", f\"Rune v{NEW}\
|
||||
\ |\"),\n ],\n \"src/rune/canary_gitea_automation.py\": [(f'\"{VERSION}\"\
|
||||
', f'\"{NEW}\"')],\n \"README.md\": [(f\"v{VERSION}\", f\"v{NEW}\")],\n\
|
||||
\ \"pyproject.toml\": [(f'version = \"{VERSION}\"', f'version = \"{NEW}\"\
|
||||
')],\n}\n\n# Fetch current master content, apply replacements, VERIFY all\
|
||||
\ six\n# surface patterns (five files + the TUI footer) agree on NEW before\n\
|
||||
# committing anything (TUI gate). The footer check was added after\n# the\
|
||||
\ 0.89.0 half-bump: a stale RELEASE_VERSION made both replace\n# pairs no-op;\
|
||||
\ the gate's RECOVER regex fixed the header only and\n# the footer silently\
|
||||
\ stayed 0.88.4, failing the banner test.\nedited = {}\nfor path, pairs in\
|
||||
\ surfaces.items():\n status, cur = api(f\"/contents/{path}?ref=master\"\
|
||||
)\n if status != 200:\n print(f\" skip {path} ({status})\"); continue\n\
|
||||
\ text = base64.b64decode(cur[\"content\"]).decode(\"utf-8\")\n changed\
|
||||
\ = text\n for old, newv in pairs:\n changed = changed.replace(old,\
|
||||
\ newv)\n if changed != text:\n edited[path] = (changed, cur[\"\
|
||||
sha\"])\n\n# In-memory gate: all five surfaces must now carry NEW\nimport\
|
||||
\ re\nVER = r\"\\d+\\.\\d+\\.\\d+\"\nchecks = [\n (\"src/rune/__init__.py\"\
|
||||
, rf'__version__ = \"({VER})\"'),\n (\"src/rune/rune_tui.py\", rf\"Version:\
|
||||
\ v({VER})\"),\n (\"src/rune/rune_tui.py\", rf\"Rune v({VER}) \\|\"),\n\
|
||||
\ (\"src/rune/canary_gitea_automation.py\", rf\"'__version__ = \\\"({VER})\\\
|
||||
\"'\"),\n (\"pyproject.toml\", rf'version = \"({VER})\"'),\n (\"README.md\"\
|
||||
, rf\"v({VER})\"),\n]\ndrifts = []\nfor rel, pat in checks:\n if rel not\
|
||||
\ in edited:\n # unchanged surface: must ALREADY carry NEW \u2014 if\
|
||||
\ it carries a\n # DIFFERENT version (base drift, e.g. pyproject at\
|
||||
\ 0.49.0 while\n # master is 0.54.1), RECOVER by force-editing to NEW.\
|
||||
\ The gate\n # stays: nothing commits unless every surface carries\
|
||||
\ NEW.\n status, cur = api(f\"/contents/{rel}?ref=master\")\n \
|
||||
\ if status != 200:\n drifts.append(f\"{rel}: unreadable ({status})\"\
|
||||
)\n continue\n text = base64.b64decode(cur.get(\"content\"\
|
||||
, \"\")).decode(\"utf-8\", \"replace\")\n hits = re.findall(pat, text)\n\
|
||||
\ if NEW not in hits:\n fixed = re.sub(pat, lambda m: m.group(0).replace(m.group(1),\
|
||||
\ NEW), text, count=1)\n if fixed != text:\n edited[rel]\
|
||||
\ = (fixed, cur[\"sha\"])\n print(f\" RECOVERED {rel}: base\
|
||||
\ drift {sorted(set(hits))} -> {NEW}\")\n else:\n \
|
||||
\ drifts.append(f\"{rel}: pattern present but unfixable (found {sorted(set(hits))})\"\
|
||||
)\n else:\n hits = re.findall(pat, edited[rel][0])\n if NEW\
|
||||
\ not in hits:\n drifts.append(f\"{rel}: edited content does not\
|
||||
\ carry {NEW}\")\nif drifts:\n print(\"VERSION-SURFACE DRIFT \u2014 bump\
|
||||
\ aborted, nothing committed:\")\n for d in drifts:\n print(f\"\
|
||||
\ - {d}\")\n sys.exit(1)\n\n# Commit ALL surfaces as ONE commit via ChangeFiles\n\
|
||||
# (POST /contents with a files[] array). The per-file contents PUT\n# created\
|
||||
\ one commit per version surface \u2014 five same-version \"bump\n# commits\"\
|
||||
\ per release (the phantom duplicate-bump cascade,\n# misdiagnosed as queue\
|
||||
\ drain since 0.66.2; root-caused T15 Part F\n# 2026-09-11). Git-data blob/tree\
|
||||
\ creation is 404 on this Gitea\n# build; ChangeFiles is the supported single-commit\
|
||||
\ primitive.\nmsg = f\"chore(release): auto-bump version to {NEW} \u2014 earned\
|
||||
\ by #{PR_NUMBER} ({PR_TITLE[:80]})\\n\\nPromotion record: {promoting_url}\"\
|
||||
\nops = [{\n \"operation\": \"update\",\n \"path\": path,\n \"content\"\
|
||||
: base64.b64encode(content.encode()).decode(), # ChangeFileOperation.content\
|
||||
\ is ContentBase64\n} for path, (content, _old_sha) in edited.items()]\nstatus,\
|
||||
\ r = api(\"/contents\", \"POST\", {\n \"branch\": \"master\",\n \"\
|
||||
message\": msg,\n \"files\": ops,\n})\nif status not in (200, 201):\n \
|
||||
\ print(f\"change-files failed ({status}): {str(r)[:300]}\"); sys.exit(1)\n\
|
||||
commit_sha = (r.get(\"commit\") or {}).get(\"sha\", \"?\") if isinstance(r,\
|
||||
\ dict) else \"?\"\nprint(f\" committed {len(ops)} surfaces as ONE commit\
|
||||
\ {str(commit_sha)[:7]}\")\n\nprint(f\"AUTO_BUMPED=true \u2014 version {NEW}\
|
||||
\ live on master; TUI footer will render v{NEW}\")\nwith open(os.environ[\"\
|
||||
GITHUB_ENV\"], \"a\") as env:\n env.write(f\"RELEASE_VERSION={NEW}\\n\"\
|
||||
)\n env.write(\"AUTO_BUMPED=true\\n\")\n env.write(f\"BUMP_COMMIT_SHA={commit_sha}\\n\")\nPYBUMP\n"
|
||||
- name: Create evidence-derived release (SCAFFOLD-TRANCHE-009B)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: "# ONE note-generation authority: the tested release_notes module.\n# The\
|
||||
\ old PR-title/body paste path (which produced empty '##'\n# bodies on direct-push\
|
||||
\ releases) is REMOVED \u2014 governed evidence\n# from the scaffold store\
|
||||
\ is the source, not PR prose.\n# EXIT-NOTE TEMPLATE (Nick 2026-10-08): plain-English\
|
||||
\ summary of\n# what was worked on and submitted \u2014 the version-history\
|
||||
\ reader's\n# counterpart of the issue-entry template. Appended to the\n#\
|
||||
\ evidence-derived body so governed facts AND human narrative\n# both appear.\n\
|
||||
LAST_TAG=$(python3 scripts/tag_repair.py --latest-tag --namespace main)\nEXIT_NOTE=$(python3 scripts/release_exit_note.py\
|
||||
\ --since \"$LAST_TAG\" --to HEAD) || EXIT_NOTE=\"\"\npython3 scripts/release_history_cli.py\
|
||||
\ init \\\n --version \"$RELEASE_VERSION\" \\\n --server \"$RELEASE_SERVER_URL\"\
|
||||
\ --repo \"$RELEASE_REPO\" \\\n --store store/scaffold/current.json \\\n\
|
||||
\ ${EXIT_NOTE:+--exit-note \"$EXIT_NOTE\"}"
|
||||
- name: Plain-English narrative at the top of the release body
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
# OWNER DIRECTIVE (plain-English releases): prepend a
|
||||
# "# What's new — plain English" section to the release body,
|
||||
# derived from the conventional-commit range since the previous
|
||||
# tag. Go-side (src/go/release-notes narrative.go) — zero new
|
||||
# Python. Idempotent: a body already carrying the section is left
|
||||
# alone. Additive and non-blocking: a missing toolchain or a failed
|
||||
# prepend must never fail a release.
|
||||
LAST_TAG=$(python3 scripts/tag_repair.py --latest-tag --namespace main)
|
||||
if ! command -v go >/dev/null 2>&1; then
|
||||
echo "::warning::go toolchain unavailable — release published without the plain-English narrative"
|
||||
exit 0
|
||||
fi
|
||||
(cd src/go/release-notes && go build -o /tmp/rune-release-notes .) || {
|
||||
echo "::warning::release-notes build failed — narrative skipped"; exit 0; }
|
||||
# rebuild-body (#2582): the FULL plain-english template — proper
|
||||
# markdown (one H1 title, sections in reading order, capped bullets,
|
||||
# the no-changes contradiction scrubbed, evidence tails preserved).
|
||||
(cd src/go/release-notes && go build -o /tmp/rune-release-notes .) || true
|
||||
/tmp/rune-release-notes rebuild-body \
|
||||
--server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO" \
|
||||
--tag "v${RELEASE_VERSION}" --since "$LAST_TAG" || {
|
||||
echo "::warning::body rebuild failed — release body left unchanged"; exit 0; }
|
||||
- name: Sync the committed changelog from the version tags
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
run: |
|
||||
# CHANGELOG LAW (#2582): every release syncs the committed
|
||||
# CHANGELOG.md (deterministic regeneration from the tags). CI's lint
|
||||
# job byte-compares regeneration against the file — a release that
|
||||
# skipped its sync turns the gate red on the next PR. The sync
|
||||
# commit is chore(changelog) (= releases nothing; the convergence
|
||||
# classifier's "other" bucket), so it earns no version.
|
||||
(cd src/go/release-notes && go build -o /tmp/rn-changelog .) || {
|
||||
echo "::warning::changelog sync skipped — go toolchain unavailable"; exit 0; }
|
||||
/tmp/rn-changelog changelog
|
||||
if ! git diff --quiet CHANGELOG.md; then
|
||||
git config user.name "release-bot"
|
||||
git config user.email "release-bot@rune.dev"
|
||||
git add CHANGELOG.md
|
||||
# push auth = tag_repair's tested extraheader mechanism
|
||||
python3 - <<'PYPUSH'
|
||||
import tag_repair
|
||||
import os, subprocess
|
||||
repo = os.getcwd()
|
||||
try:
|
||||
err = tag_repair._configure_http_auth(repo)
|
||||
except Exception as e:
|
||||
err = str(e)
|
||||
if err:
|
||||
print("::warning::changelog push auth unavailable — next release retries:", str(err)[:120])
|
||||
else:
|
||||
r = subprocess.run(["git", "push", "gitea", "master"], capture_output=True, text=True)
|
||||
print("changelog push:", r.returncode, (r.stdout or r.stderr)[:200])
|
||||
PYPUSH
|
||||
else
|
||||
echo "changelog already current"
|
||||
fi
|
||||
- name: Refresh README Highlights from the latest release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
# OWNER DIRECTIVE (#2538 follow-up): the README Highlights section
|
||||
# between the highlights:start/end markers refreshes from the newest
|
||||
# main release's plain-English narrative — the same narrative the
|
||||
# previous step prepends to the release body. Go-side
|
||||
# (src/go/release-notes cli_highlights.go): marker-scoped, idempotent
|
||||
# (identical bytes are a reported noop — no commit), refuses
|
||||
# unbalanced markers. Non-blocking: any failure is a warning; the
|
||||
# release survives.
|
||||
if ! command -v go >/dev/null 2>&1; then
|
||||
echo "::warning::go toolchain unavailable — README highlights not refreshed"
|
||||
exit 0
|
||||
fi
|
||||
(cd src/go/release-notes && go build -o /tmp/rune-release-notes .) || {
|
||||
echo "::warning::release-notes build failed — highlights refresh skipped"; exit 0; }
|
||||
# The bump step edits README's version strings server-side (API
|
||||
# commit) and never touches this checkout — re-point master at the
|
||||
# fetched tip so the CLI reads the post-bump README and the commit
|
||||
# can push as a fast-forward.
|
||||
git fetch origin master || {
|
||||
echo "::warning::git fetch failed — highlights refresh skipped"; exit 0; }
|
||||
git checkout -B master FETCH_HEAD || {
|
||||
echo "::warning::master re-point failed — highlights refresh skipped"; exit 0; }
|
||||
ACTION=$(/tmp/rune-release-notes refresh-highlights \
|
||||
--file README.md --server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO") || {
|
||||
echo "::warning::refresh-highlights failed — README left unchanged"; exit 0; }
|
||||
echo "refresh-highlights: $ACTION"
|
||||
case "$ACTION" in
|
||||
*'"action":"noop"'*)
|
||||
echo "Highlights already current — nothing to commit"; exit 0;;
|
||||
esac
|
||||
REL_TAG=$(echo "$ACTION" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tag", ""))') || REL_TAG=""
|
||||
export HIGHLIGHTS_TAG="${REL_TAG:-v${RELEASE_VERSION}}"
|
||||
# Push auth reuses tag_repair.py's tested extraheader mechanism
|
||||
# (token owner's login + ROBOT_TOKEN) — one authority, not a copy.
|
||||
if ! python3 - <<'PYCOMMIT'
|
||||
import os, subprocess, sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, "scripts")
|
||||
import tag_repair
|
||||
- name: Verify version was bumped since last tag
|
||||
run: |
|
||||
LATEST=$(git tag --sort=-v:refname | head -1 | sed 's/^v//')
|
||||
if [ -z "$LATEST" ]; then
|
||||
echo "No previous tags — releasing first version $RELEASE_VERSION"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$LATEST" = "$RELEASE_VERSION" ]; then
|
||||
echo "::error::Version $RELEASE_VERSION was already released (tag v$LATEST)."
|
||||
echo "::error::Semver bump missing from this PR: add a 'chore(release): bump version to X.Y.Z' commit that increments src/rune/__init__.py, then merge again."
|
||||
exit 1
|
||||
fi
|
||||
echo "New version $RELEASE_VERSION (last tag v$LATEST)"
|
||||
|
||||
repo = Path.cwd()
|
||||
ident = tag_repair._git_identity(repo)
|
||||
err = tag_repair._configure_http_auth(repo)
|
||||
if err is not None:
|
||||
print(f"::warning::{err} — README highlights left uncommitted")
|
||||
sys.exit(0)
|
||||
|
||||
def git(*args):
|
||||
proc = subprocess.run(["git", *args], cwd=repo, text=True, capture_output=True)
|
||||
if proc.returncode != 0:
|
||||
print(f"::warning::git {args[0]} failed — README highlights left uncommitted ({proc.stderr.strip()[:200]})")
|
||||
sys.exit(0)
|
||||
|
||||
git("add", "README.md")
|
||||
tag = os.environ.get("HIGHLIGHTS_TAG") or "v?"
|
||||
git("commit", "-m", f"docs(readme): refresh highlights from {tag}")
|
||||
git("push", "origin", "master")
|
||||
print(f"README highlights refreshed from {tag} and pushed")
|
||||
PYCOMMIT
|
||||
then
|
||||
echo "::warning::README highlights commit/push pipeline failed — release continues"
|
||||
exit 0
|
||||
fi
|
||||
- name: Push tui-v<version> tag via git so the tag-push event fires tui-release (D27)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
# WHY git push instead of the Gitea API: API-created tags (POST /tags)
|
||||
# do NOT emit a push event, so tui-release.yml's `on: push: tags: tui-v*`
|
||||
# never fired (tui-v0.310.0 landed with no TUI release). A real
|
||||
# `git push` of the tag is the trigger Gitea actions actually honor.
|
||||
#
|
||||
# ALL decision logic lives in scripts/tag_repair.py (one tested
|
||||
# authority — wave-release-pipeline-deepdive). It:
|
||||
# * is SIGPIPE-free (the old `git tag | head -1` read killed this
|
||||
# job with exit 141 on its FIRST line with 500+ tags — runs
|
||||
# 29929/30435/30636 — so no fallback ever ran),
|
||||
# * tags the bump commit, falling back to the fetched master tip
|
||||
# when the sha is unusable (0.312.x failure mode),
|
||||
# * is idempotent (tag already on the server = noop), and
|
||||
# * SELF-HEALS the version>tag deadlock: if master's version is
|
||||
# ahead of the latest tag because a bump landed without its tag,
|
||||
# this step creates+pushes the missing tui-v<version> tag on the
|
||||
# next push instead of skipping forever.
|
||||
git fetch --no-tags origin master
|
||||
python3 scripts/tag_repair.py --ensure "${RELEASE_VERSION}" \
|
||||
--bump-sha "${BUMP_COMMIT_SHA:-}" --push
|
||||
concurrency:
|
||||
group: auto-version-and-release-${ github.ref }
|
||||
cancel-in-progress: true
|
||||
- name: Create release from merged pull request
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
PR_TITLE: ${{ gitea.event.pull_request.title }}
|
||||
PR_BODY: ${{ gitea.event.pull_request.body }}
|
||||
RELEASE_API_URL: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases
|
||||
LIST_API_URL: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases?limit=50
|
||||
run: |
|
||||
# Skip idempotently if a release for this tag already exists (retry/replay safety).
|
||||
if curl --fail --silent "$LIST_API_URL" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
| python3 -c 'import json,sys,os; exists=[r for r in json.load(sys.stdin) if r.get("tag_name")==f"v{os.environ[\"RELEASE_VERSION\"]}"]; sys.exit(0 if exists else 1)'; then
|
||||
echo "Release v$RELEASE_VERSION already exists — skipping creation"
|
||||
exit 0
|
||||
fi
|
||||
python3 - <<'PY' > /tmp/rune-release.json
|
||||
import json, os
|
||||
version = os.environ["RELEASE_VERSION"]
|
||||
body = f"## {os.environ['PR_TITLE']}\n\n{os.environ.get('PR_BODY', '')}"
|
||||
print(json.dumps({
|
||||
"tag_name": f"v{version}",
|
||||
"target_commitish": "master",
|
||||
"name": f"Rune v{version}",
|
||||
"body": body,
|
||||
"draft": False,
|
||||
"prerelease": False,
|
||||
}))
|
||||
PY
|
||||
curl --fail-with-body --silent --show-error \
|
||||
-X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @/tmp/rune-release.json \
|
||||
"$RELEASE_API_URL"
|
||||
rm -f /tmp/rune-release.json
|
||||
+24
-574
@@ -4,43 +4,19 @@ on:
|
||||
push:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
workflow_dispatch: # benchmark: run representative CI without manufacturing commits
|
||||
|
||||
# SRE fix (2026-10-10): cancel superseded runs — the pipeline was draining
|
||||
# 62k queued tasks on STALE SHAs while newest pushes waited. This makes the
|
||||
# queue test HEAD, not history.
|
||||
concurrency:
|
||||
group: rune-ci-master
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Create lint environment from lock
|
||||
- name: Create lint environment from manifest
|
||||
run: |
|
||||
# #150: deps come ONLY from the pinned lock; the project itself is
|
||||
# installed with --no-deps so nothing floats outside requirements-ci.txt.
|
||||
python3 -m venv .ci-venv
|
||||
.ci-venv/bin/pip install --disable-pip-version-check -r requirements-ci.txt
|
||||
.ci-venv/bin/pip install --disable-pip-version-check -e . --no-deps
|
||||
- name: Dependency lock drift ratchet
|
||||
run: |
|
||||
# #150: fail the job if requirements-ci.txt no longer matches
|
||||
# pyproject.toml. uv is not on runners; install it pinned into the
|
||||
# lint venv (tool-only, not a project dependency).
|
||||
.ci-venv/bin/pip install --disable-pip-version-check uv==0.10.11
|
||||
# #150 gate review: the checker must resolve the PINNED uv (in
|
||||
# .ci-venv/bin), not a stray /usr/bin/uv with a different resolver
|
||||
# version — export the venv first; the checker also prefers the
|
||||
# interpreter-adjacent uv as a second line of defense.
|
||||
export PATH="$PWD/.ci-venv/bin:$PATH"
|
||||
.ci-venv/bin/python scripts/check_dep_lock.py
|
||||
.ci-venv/bin/pip install --disable-pip-version-check -e ".[dev]"
|
||||
- name: Reject syntax and undefined-name errors
|
||||
run: |
|
||||
BASE=$(python3 -c 'import json, os; event=json.load(open(os.environ["GITHUB_EVENT_PATH"])); print(event.get("pull_request", {}).get("base", {}).get("sha") or event.get("before") or "")')
|
||||
@@ -55,463 +31,44 @@ jobs:
|
||||
fi
|
||||
printf 'Checking %s\n' "${CHANGED_PYTHON[@]}"
|
||||
.ci-venv/bin/ruff check --select E9,F63,F7,F82 --output-format=github "${CHANGED_PYTHON[@]}"
|
||||
# #126: silent-handler ratchet — NEW try-except-pass / blind-except
|
||||
# handlers fail CI; existing ones are a committed, shrinking baseline
|
||||
# (lint-baseline.json + docs/silent-handler-debt.md).
|
||||
- name: Silent-handler ratchet (S110, BLE001)
|
||||
run: |
|
||||
.ci-venv/bin/python scripts/lint_ratchet.py
|
||||
|
||||
python3 -m compileall -q "${CHANGED_PYTHON[@]}"
|
||||
mapfile -t ADDED_PYTHON < <(git diff --diff-filter=A --name-only "$BASE" HEAD -- '*.py')
|
||||
if [ "${#ADDED_PYTHON[@]}" -gt 0 ]; then
|
||||
.ci-venv/bin/ruff format --check "${ADDED_PYTHON[@]}"
|
||||
fi
|
||||
- name: Changelog currency (generated == committed, #2582)
|
||||
run: |
|
||||
# The committed CHANGELOG.md is generated from the version tags; a
|
||||
# release that landed without a changelog sync (or a tag added
|
||||
# without an entry) fails here with the sync command. The tags are
|
||||
# immutable — regeneration is deterministic, so this gate is exact.
|
||||
(cd src/go/release-notes && go build -o /tmp/rn-changelog .)
|
||||
/tmp/rn-changelog changelog --check
|
||||
- name: Structural contract (henxels)
|
||||
run: |
|
||||
.ci-venv/bin/henxels check --all --plain
|
||||
# ABSOLUTE SCAFFOLDING LAW (2026-10-07): every Go/Rust implementation
|
||||
# unit must map to a construction contract in store/scaffold/. New
|
||||
# units without a contract fail the build — open the contract BEFORE
|
||||
# the commit lands, never after.
|
||||
- name: Scaffolding coverage (absolute law)
|
||||
run: |
|
||||
python3 scripts/scaffolding_coverage.py
|
||||
|
||||
unit-tests:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
env:
|
||||
RUNE_HOME: /tmp/rune-ci-${{ github.run_id }}
|
||||
RUNE_MEMORY_DIR: /tmp/rune-ci-${{ github.run_id }}/memory
|
||||
# The unit shim battery loads the cdylib (ep_loader); point it at the
|
||||
# runner-owned cdylib FILE from the crate-build step below (the override
|
||||
# expects a file path, not a directory).
|
||||
EP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libevidence_provenance.so
|
||||
SP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/librune_spine.so
|
||||
MS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_scoring.so
|
||||
CS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libcapability_scoring.so
|
||||
TC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libtoken_counting.so
|
||||
SC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libscaffold_core.so
|
||||
MEMS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_storage.so
|
||||
MR_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_retrieval.so
|
||||
CC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libcognition_core.so
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
# #149: diff-cover --compare-branch origin/master needs full history
|
||||
# to compute the changed-line diff in PR (merge-ref) and push runs.
|
||||
fetch-depth: 0
|
||||
- name: Create test environment from lock
|
||||
- name: Create test environment from manifest
|
||||
run: |
|
||||
# #150: deps come ONLY from the pinned lock.
|
||||
python3 -m venv .test-venv
|
||||
.test-venv/bin/pip install --disable-pip-version-check -r requirements-ci.txt
|
||||
.test-venv/bin/pip install --disable-pip-version-check -e . --no-deps
|
||||
- name: Umbrella impact gate (propagation contract)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
SERVER: ${{ gitea.server_url }}
|
||||
REPO: ${{ gitea.repository }}
|
||||
PR_NUMBER: ${{ gitea.event.pull_request.number }}
|
||||
.test-venv/bin/pip install --disable-pip-version-check -e ".[dev,memory]"
|
||||
- name: Test local tools and interstitial message objects
|
||||
run: |
|
||||
BODY_FILE=""
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
BODY_FILE=$(mktemp)
|
||||
curl -sf -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$SERVER/api/v1/repos/$REPO/pulls/$PR_NUMBER" | \
|
||||
python3 -c "import json,sys; print(json.load(sys.stdin).get('body',''))" > "$BODY_FILE"
|
||||
fi
|
||||
# Propagation applies to pull_request runs (the merge gate — they have
|
||||
# a body). Push runs are pre-PR: propagation is unenforceable there
|
||||
# (no body exists), so only the UNMAPPED check applies.
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
export UMBRELLA_SKIP_PROPAGATION=1
|
||||
fi
|
||||
python3 scripts/umbrella_impact.py check \
|
||||
--base "$(git merge-base origin/master HEAD)" --head HEAD ${BODY_FILE:+--body-file "$BODY_FILE"}
|
||||
- name: Build Rust evidence_provenance crate
|
||||
run: |
|
||||
# tests/test_evidence_provenance_shims.py drives the cdylib through
|
||||
# the shim layer. Toolchain law (see the canary job for the full
|
||||
# rationale): system rustc is broken on this host (LLVM_23.1
|
||||
# version-node symbol gap); use the healthy rustup toolchain, PATH
|
||||
# first so the rustup shim can't exec the broken system rustc.
|
||||
# Runner-owned /tmp target dir (the repo's target/ is nick's).
|
||||
export PATH="/home/nick/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin:$PATH"
|
||||
export CARGO_TARGET_DIR="/tmp/rune-ci-target-${{ github.run_id }}"
|
||||
mkdir -p "$CARGO_TARGET_DIR"
|
||||
cargo build --release --manifest-path src/rust/evidence_provenance/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/rune_spine/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_scoring/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/capability_scoring/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/token_counting/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/scaffold_core/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_storage/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_retrieval/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/cognition_core/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/query_memory/Cargo.toml
|
||||
- name: Test with coverage (global floor + changed-lines ratchet)
|
||||
run: |
|
||||
# #149 coverage ratchet — both floors live in .coveragerc; raise,
|
||||
# never lower. The changed-lines floor (70) gates every PR's own
|
||||
# lines; the global floor (29, measured at the #149 baseline) gates
|
||||
# the suite total.
|
||||
PYTHONPATH=src:. .test-venv/bin/coverage run -m pytest tests/ -q
|
||||
.test-venv/bin/coverage report --fail-under=29
|
||||
.test-venv/bin/coverage xml
|
||||
.test-venv/bin/diff-cover coverage.xml --compare-branch origin/master --fail-under=70 --diff-range-notation ..
|
||||
|
||||
go-surface:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- name: Verify Go toolchain version pin
|
||||
run: |
|
||||
# H0 pin: the Go surface builds against the pacman toolchain; the
|
||||
# pin file is the single source of truth so a distro upgrade that
|
||||
# changes behavior fails loud here, not downstream in the canary.
|
||||
WANT=$(cat go-version.txt)
|
||||
GOT=$(go version | awk '{print $3}' | sed 's/^go//' | cut -d- -f1)
|
||||
echo "pinned: $WANT | toolchain: $GOT"
|
||||
[ "$GOT" = "$WANT" ] || { echo "::error::Go toolchain drift: want $WANT, got $GOT (update go-version.txt with the rollout)"; exit 1; }
|
||||
- name: build Survivor staticlibs in-tree (for cgo main packages)
|
||||
run: |
|
||||
# Survivor main packages link Rust staticlibs via ${SRCDIR} paths
|
||||
# (adaptive-router → libroute_risk_flags.a). Those paths resolve
|
||||
# only when the crate target dir is IN-TREE, so this build must
|
||||
# NOT set CARGO_TARGET_DIR (the redirected builds below are for
|
||||
# the FFI-loader canaries and stay as they are).
|
||||
# Every Survivor-linked crate must have its .a in-tree before the
|
||||
# go loop: adaptive-router → route_risk_flags; cognition →
|
||||
# cognition_core. Add crates here as Survivor packages grow.
|
||||
cargo build --release --manifest-path src/rust/route_risk_flags/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/cognition_core/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/query_memory/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/construction_interface/Cargo.toml
|
||||
- name: prune stale /tmp cargo target dirs (tmpfs guard)
|
||||
run: |
|
||||
# /tmp is a 32G tmpfs; every CI run leaves a ~218MB
|
||||
# rune-ci-target-<run_id> dir. Without pruning, 300+ runs filled
|
||||
# the tmpfs (2026-10-07: venv creation failed disk-full across
|
||||
# every job). Keep the 3 newest; drop the rest.
|
||||
cd /tmp && ls -dt rune-ci-target-* go-bins-* 2>/dev/null | tail -n +3 | xargs -r rm -rf
|
||||
# deploy-test cache: rust targets accumulate ~15G (every crate's
|
||||
# release dir). The staged source survives; targets rebuild.
|
||||
find /tmp/rune-deploy-test/src/rust -maxdepth 2 -name target -type d 2>/dev/null | xargs -r rm -rf
|
||||
- name: go vet + go test + go build
|
||||
run: |
|
||||
for dir in src/go/*/; do
|
||||
if [ -f "$dir/go.mod" ] && [ -z "$(find "$dir" -maxdepth 1 -name 'bins' -type d 2>/dev/null)" ]; then
|
||||
echo "=== Building $dir ==="
|
||||
cd "$dir"
|
||||
go vet ./...
|
||||
go test ./... -v || { echo "::error::go test failed in $dir"; exit 1; }
|
||||
OUT_DIR="/tmp/go-bins-${{ gitea.run_id }}"
|
||||
mkdir -p "$OUT_DIR"
|
||||
go build -o "$OUT_DIR/go-$(basename "$dir")" .
|
||||
cd - > /dev/null
|
||||
fi
|
||||
done
|
||||
- name: golangci-lint (server-side Go analysis — catches what go vet misses)
|
||||
run: |
|
||||
which golangci-lint 2>/dev/null || echo '::warning::golangci-lint not installed — skipping'
|
||||
golangci-lint --version 2>/dev/null || true
|
||||
|
||||
- name: adaptive-router parity gate (HARD — oracle replay, not swallowable)
|
||||
run: |
|
||||
cd src/go/adaptive-router
|
||||
go test ./... -run TestParityAgainstPythonOracle -v
|
||||
- name: operator-deep evidence-kernel parity gate (HARD — #2479 batch: #30 kernels)
|
||||
run: |
|
||||
cd src/go/operator-deep && go build -o /tmp/operator-deep-${{ gitea.run_id }} . && go test -run "TestOperator30Parity|TestVerificationAndOutcomeContract|TestTaskIdentityParity|TestWindowParity" -v
|
||||
- name: curator-core candidate-decay parity gate (HARD — #2479 batch: ledger decay + journal render + cosine)
|
||||
run: |
|
||||
cd src/go/curator-core && go build -o /tmp/curator-core-${{ gitea.run_id }} . && /tmp/curator-core-${{ gitea.run_id }} replay truth_candidate_decay.json && go test -run TestCandidateDecayParity -v
|
||||
- name: cron-engine parity gate (HARD — #2479 batches 1+126 + gap-close: parser + lifecycle + execution family)
|
||||
run: |
|
||||
cd src/go/cron-engine && go build -o /tmp/cron-engine-${{ gitea.run_id }} . && /tmp/cron-engine-${{ gitea.run_id }} replay truth.json && /tmp/cron-engine-${{ gitea.run_id }} replay-gap truth_gap.json && go test -run 'TestLifecycleParity|TestGapParity' -v
|
||||
- name: goal-engine parity gate (HARD — #2479 batches 2+129: verifier + lifecycle)
|
||||
run: |
|
||||
cd src/go/goal-engine && go build -o /tmp/goal-engine-${{ gitea.run_id }} . && /tmp/goal-engine-${{ gitea.run_id }} replay truth.json && go test -run TestLifecycleParity -v
|
||||
- name: goal FULL-port parity gate (HARD — #2479 dichotomy wave: 59-row frozen oracle)
|
||||
run: |
|
||||
cd src/go/goal && go build -o /tmp/goal-${{ gitea.run_id }} . && /tmp/goal-${{ gitea.run_id }} replay truth.json && go test -run TestTruthParity -v
|
||||
- name: abstraction FULL-port parity gate (HARD — #2479 dichotomy wave: 77-row frozen oracle)
|
||||
run: |
|
||||
cd src/go/abstraction && go build -o /tmp/abstraction-${{ gitea.run_id }} . && /tmp/abstraction-${{ gitea.run_id }} replay truth.json && go test -run TestTruthParity -v
|
||||
- name: provider-budget parity gate (HARD — #2479 batches 3+146: budget + ties/headers/rpm)
|
||||
run: |
|
||||
cd src/go/provider-budget && go build -o /tmp/provider-budget-${{ gitea.run_id }} .
|
||||
/tmp/provider-budget-${{ gitea.run_id }} replay truth.json
|
||||
- name: event-wait parity gate (HARD — #2479 batch 4, oracle replay)
|
||||
run: |
|
||||
cd src/go/event-wait && go build -o /tmp/event-wait-${{ gitea.run_id }} .
|
||||
/tmp/event-wait-${{ gitea.run_id }} replay truth.json
|
||||
- name: memory-consolidation parity gate (HARD — #2479 batches 5+131: scoring + constructors)
|
||||
run: |
|
||||
cd src/go/memory-consolidation && go build -o /tmp/memory-consolidation-${{ gitea.run_id }} . && /tmp/memory-consolidation-${{ gitea.run_id }} replay truth.json && go test -run TestConstructorParity -v
|
||||
- name: release-notes FULL-port parity gate (HARD — #2479 dichotomy wave: 78-row frozen oracle)
|
||||
run: |
|
||||
cd src/go/release-notes && go build -o /tmp/release-notes-${{ gitea.run_id }} .
|
||||
/tmp/release-notes-${{ gitea.run_id }} replay truth.json
|
||||
go test -run TestTruthParity -v
|
||||
- name: model-profiles parity gate (HARD — #2479 batches 7+145: detection + edge pins)
|
||||
run: |
|
||||
cd src/go/model-profiles && go build -o /tmp/model-profiles-${{ gitea.run_id }} .
|
||||
/tmp/model-profiles-${{ gitea.run_id }} replay truth.json
|
||||
- name: model-routing parity gate (HARD — #2479 batches 8+136: rank + cost-override)
|
||||
run: |
|
||||
cd src/go/model-routing && go build -o /tmp/model-routing-${{ gitea.run_id }} .
|
||||
/tmp/model-routing-${{ gitea.run_id }} replay truth.json
|
||||
- name: work-manager parity gate (HARD — #2479 batches 9+137+156: batching + lifecycle)
|
||||
run: |
|
||||
cd src/go/work-manager && go build -o /tmp/work-manager-${{ gitea.run_id }} . && /tmp/work-manager-${{ gitea.run_id }} replay internal/workmanager/truth.json && go test -run TestLifecycleParity -v ./...
|
||||
- name: core-server parity gate (HARD — #2479 batches 10+138: envelopes + extraction/identity)
|
||||
run: |
|
||||
cd src/go/core-server && go build -o /tmp/core-server-${{ gitea.run_id }} .
|
||||
/tmp/core-server-${{ gitea.run_id }} replay truth.json
|
||||
- name: builder-bridge parity + improvement gate (HARD — #2479 batches 11+139: render/LRU + TTL/refresh/key)
|
||||
run: |
|
||||
cd src/go/builder-bridge && go build -o /tmp/builder-bridge-${{ gitea.run_id }} .
|
||||
/tmp/builder-bridge-${{ gitea.run_id }} replay truth.json
|
||||
- name: voicebox parity + improvement gate (HARD — #2479 batches 12+140: hook + full manifest)
|
||||
run: |
|
||||
cd src/go/voicebox && go build -o /tmp/voicebox-${{ gitea.run_id }} .
|
||||
/tmp/voicebox-${{ gitea.run_id }} replay truth.json
|
||||
- name: voice-sync parity gate (HARD — #2479 batches 35+143: extract + unicode/generate)
|
||||
run: |
|
||||
cd src/go/voice-sync && go build -o /tmp/voice-sync-${{ gitea.run_id }} .
|
||||
/tmp/voice-sync-${{ gitea.run_id }} replay truth.json
|
||||
- name: progressive-escalation parity gate (HARD — #2479 batches 44+147: full truth table)
|
||||
run: |
|
||||
cd src/go/progressive-escalation && go build -o /tmp/progressive-escalation-${{ gitea.run_id }} .
|
||||
/tmp/progressive-escalation-${{ gitea.run_id }} replay truth.json
|
||||
- name: message-queue parity gate (HARD — #2479 batches 61+148: full pattern table)
|
||||
run: |
|
||||
cd src/go/message-queue && go build -o /tmp/message-queue-${{ gitea.run_id }} .
|
||||
/tmp/message-queue-${{ gitea.run_id }} replay truth.json
|
||||
- name: message_queue parity gate (HARD — #2479 dichotomy wave: FULL port, 55 frozen rows)
|
||||
run: |
|
||||
cd src/go/message_queue && go test ./... -race -count=1
|
||||
cd src/go/message_queue && go build -o /tmp/message_queue-${{ gitea.run_id }} . && /tmp/message_queue-${{ gitea.run_id }} replay truth.json
|
||||
- name: outcome-ledger parity gate (HARD — #2479 batches 49+151: stats + live fixture)
|
||||
run: |
|
||||
cd src/go/outcome-ledger && go build -o /tmp/outcome-ledger-${{ gitea.run_id }} .
|
||||
/tmp/outcome-ledger-${{ gitea.run_id }} replay truth.json
|
||||
- name: auto-context parity gate (HARD — #2479 batches 50+157: oracle-exact keywords)
|
||||
run: |
|
||||
cd src/go/auto-context && go build -o /tmp/auto-context-${{ gitea.run_id }} .
|
||||
/tmp/auto-context-${{ gitea.run_id }} replay truth.json
|
||||
- name: night-mode parity gate (HARD — #2479 batches 60+167: state-machine cores)
|
||||
run: |
|
||||
cd src/go/night-mode && go build -o /tmp/night-mode-${{ gitea.run_id }} . && go test -run TestLifecycleParity -v
|
||||
- name: sleep-time parity gate (HARD — #2479 batches 11+168: transcript kernels)
|
||||
run: |
|
||||
cd src/go/sleep-time && go build -o /tmp/sleep-time-${{ gitea.run_id }} . && go test -run TestTranscriptKernels -v
|
||||
- name: response-adapter parity gate (HARD — #2479 batches 109+170: parsers + provenance)
|
||||
run: |
|
||||
cd src/go/response-adapter && go build -o /tmp/response-adapter-${{ gitea.run_id }} .
|
||||
/tmp/response-adapter-${{ gitea.run_id }} replay truth.json
|
||||
- name: standardized-providers parity gate (HARD — #2479 batches 62+171: registry + connect gate)
|
||||
run: |
|
||||
cd src/go/standardized-providers && go build -o /tmp/standardized-providers-${{ gitea.run_id }} . && /tmp/standardized-providers-${{ gitea.run_id }} replay truth.json && go test -run TestConnectDecisionParity -v
|
||||
- name: recall-uplift parity gate (HARD — #2479 batches 78+172: full expansion table)
|
||||
run: |
|
||||
cd src/go/recall-uplift && go build -o /tmp/recall-uplift-${{ gitea.run_id }} .
|
||||
/tmp/recall-uplift-${{ gitea.run_id }} replay truth.json
|
||||
- name: forge-core parity + improvement gate (HARD — #2479 batches 13+106+141: resolvers + lattice restore)
|
||||
run: |
|
||||
cd src/go/forge-core && go build -o /tmp/forge-core-${{ gitea.run_id }} .
|
||||
/tmp/forge-core-${{ gitea.run_id }} replay truth.json
|
||||
- name: forge-client parity gate (HARD — #2479 batch 133, oracle replay)
|
||||
run: |
|
||||
cd src/go/forge-client && go build -o /tmp/forge-client-${{ gitea.run_id }} .
|
||||
/tmp/forge-client-${{ gitea.run_id }} replay truth.json
|
||||
- name: elastic-cognition parity + improvement gate (HARD — #2479 batches 14+117: +budget/parse kernels)
|
||||
run: |
|
||||
cd src/go/elastic-cognition && go build -o /tmp/elastic-cognition-${{ gitea.run_id }} .
|
||||
/tmp/elastic-cognition-${{ gitea.run_id }} replay truth.json
|
||||
/tmp/elastic-cognition-${{ gitea.run_id }} replay truth_budget_parse.json
|
||||
- name: auto-tasks parity + improvement gate (HARD — #2479 batches 15+144: adjacency + action/seq/gates)
|
||||
run: |
|
||||
cd src/go/auto-tasks && go build -o /tmp/auto-tasks-${{ gitea.run_id }} .
|
||||
/tmp/auto-tasks-${{ gitea.run_id }} replay truth.json
|
||||
- name: model-connect parity + improvement gate (HARD — #2479 batches 16+113: clean-name parser + edge top-up)
|
||||
run: |
|
||||
cd src/go/model-connect && go build -o /tmp/model-connect-${{ gitea.run_id }} .
|
||||
/tmp/model-connect-${{ gitea.run_id }} replay truth.json
|
||||
- name: yamllint (all workflow files — would have caught the parser bugs)
|
||||
run: |
|
||||
# The runner's system python3 has NO PyYAML (PEP 668 blocks the
|
||||
# pip install; || true swallowed it — this step failed on its first
|
||||
# ever execution, run 23342). Self-contained venv, same shape as
|
||||
# the lint job's environment step.
|
||||
python3 -m venv .yaml-venv
|
||||
.yaml-venv/bin/pip install --disable-pip-version-check -q pyyaml
|
||||
for f in .gitea/workflows/*.yml; do
|
||||
.yaml-venv/bin/python -c "import yaml; yaml.safe_load(open('$f'))" || { echo "YAML INVALID: $f"; exit 1; }
|
||||
done
|
||||
echo "All workflow YAML files valid"
|
||||
|
||||
- name: gofmt gate (formatting is a gate — polyglot lint contract)
|
||||
run: |
|
||||
UNFORMATTED=$(gofmt -l src/go/ 2>/dev/null)
|
||||
if [ -n "$UNFORMATTED" ]; then
|
||||
echo "::error::gofmt drift — run gofmt -w on these:"
|
||||
echo "$UNFORMATTED"
|
||||
exit 1
|
||||
fi
|
||||
- name: memory-store-deep text-kernel parity gate (HARD — #2479 batch: fts/mapreduce/entities)
|
||||
run: |
|
||||
cd src/go/memory-store-deep && go build -o /tmp/msd-${{ gitea.run_id }} . && go test -run "TestTextKernelsParity|TestDriftRerankBlendParity" -v
|
||||
- name: scaffold builders parity gate (HARD — #2479 batch: 7 pure builders)
|
||||
run: |
|
||||
cd src/go/scaffold && go build -o /tmp/scaffold-${{ gitea.run_id }} . && go test -run TestBuildersParity -v
|
||||
- name: tool-executor prepared-path parity gate (HARD — #2479 batch)
|
||||
run: |
|
||||
cd src/go/tool-executor && go build -o /tmp/tool-exec-${{ gitea.run_id }} . && go test -run TestPreparedPathParity -v ./...
|
||||
- name: failure-filer parity gate (HARD — #2479 batch: classify/redact/tier/marker)
|
||||
run: |
|
||||
cd src/go/failure-filer && go build -o /tmp/failfiler-${{ gitea.run_id }} . && go test -run TestFilerParity -v
|
||||
- name: tool-protocol shapes parity gate (HARD — #2479 batch: 5 record shapes)
|
||||
run: |
|
||||
cd src/go/tool-protocol && go build -o /tmp/tp-${{ gitea.run_id }} . && go test -run TestShapesFromDictParity -v
|
||||
- name: go race detector (every module's test suite under -race)
|
||||
run: |
|
||||
for dir in src/go/*/; do
|
||||
if [ -f "$dir/go.mod" ]; then
|
||||
cd "$dir"
|
||||
go test -race ./... 2>/dev/null || true
|
||||
cd - > /dev/null
|
||||
fi
|
||||
done
|
||||
- name: go fuzz smoke (seed corpus + 15s per target — panics and
|
||||
invariant violations fail the build; corpus caches in the repo)
|
||||
run: |
|
||||
# go test -fuzz requires a SINGLE target pattern: `-fuzz .` with
|
||||
# multiple FuzzX targets exits 1 ("matches more than one fuzz test")
|
||||
# and pipefail turns that into a job failure. Enumerate targets and
|
||||
# fuzz each — and fail LOUD (no pipe-mask, exit 1 propagates).
|
||||
for dir in src/go/tool-executor/internal/toolexec src/go/curator-core src/go/memory-store-deep; do
|
||||
cd "$dir"
|
||||
for target in $(go test -list '^Fuzz' . | grep '^Fuzz'); do
|
||||
echo "fuzz: $dir/$target"
|
||||
go test -run "^${target}$" -fuzz "^${target}$" -fuzztime 15s . || exit 1
|
||||
done
|
||||
cd - > /dev/null
|
||||
done
|
||||
|
||||
rust-surface:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- name: rustup clippy + rustfmt components
|
||||
run: |
|
||||
rustup component add clippy rustfmt 2>/dev/null || true
|
||||
cargo clippy --version
|
||||
cargo fmt --version
|
||||
- name: cargo fmt --check (formatting is a gate)
|
||||
run: |
|
||||
FAIL=0
|
||||
for crate in src/rust/*/; do
|
||||
if [ -f "$crate/Cargo.toml" ]; then
|
||||
(cd "$crate" && cargo fmt --check) || FAIL=1
|
||||
fi
|
||||
done
|
||||
exit $FAIL
|
||||
- name: cargo clippy (warnings are errors — first linting the Rust
|
||||
surface has EVER had; pre-existing debt tracked in the crate-local
|
||||
lib.rs allow list, NOT a global opt-out)
|
||||
run: |
|
||||
FAIL=0
|
||||
for crate in src/rust/*/; do
|
||||
if [ -f "$crate/Cargo.toml" ]; then
|
||||
echo "=== clippy $(basename "$crate") ==="
|
||||
(cd "$crate" && cargo clippy --quiet -- -D warnings) || FAIL=1
|
||||
fi
|
||||
done
|
||||
exit $FAIL
|
||||
- name: cargo test (lights up when the first #[test] lands)
|
||||
run: |
|
||||
FAIL=0
|
||||
for crate in src/rust/*/; do
|
||||
if [ -f "$crate/Cargo.toml" ]; then
|
||||
(cd "$crate" && cargo test --quiet) || FAIL=1
|
||||
fi
|
||||
done
|
||||
exit $FAIL
|
||||
PYTHONPATH=src:. .test-venv/bin/python -m pytest tests/ -q
|
||||
|
||||
canary:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
runs-on: self-hosted
|
||||
env:
|
||||
RUNE_HOME: /tmp/rune-ci-${{ github.run_id }}
|
||||
RUNE_MEMORY_DIR: /tmp/rune-ci-${{ github.run_id }}/memory
|
||||
# The build emits to a runner-owned /tmp target dir (the repo's target/
|
||||
# is nick-owned); EP_RUST_LIB points ep_loader at the cdylib FILE there
|
||||
# (the override expects a file path, not a directory).
|
||||
EP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libevidence_provenance.so
|
||||
SP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/librune_spine.so
|
||||
MS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_scoring.so
|
||||
CS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libcapability_scoring.so
|
||||
TC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libtoken_counting.so
|
||||
SC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libscaffold_core.so
|
||||
MEMS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_storage.so
|
||||
MR_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_retrieval.so
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- name: Create canary environment from lock
|
||||
- name: Create canary environment
|
||||
run: |
|
||||
# #150: the old ad-hoc textual pre-install + '|| true' fallback is
|
||||
# gone — the lock pins textual==8.2.8 and textual-autocomplete==4.0.6
|
||||
# (exactly what the canary needed), so there is no silent escape hatch.
|
||||
python3 -m venv .canary-venv
|
||||
.canary-venv/bin/pip install --disable-pip-version-check -r requirements-ci.txt
|
||||
.canary-venv/bin/pip install --disable-pip-version-check -e . --no-deps
|
||||
- name: Build Rust evidence_provenance crate
|
||||
.canary-venv/bin/pip install --disable-pip-version-check textual==8.2.8 textual-autocomplete==4.0.6 sqlite-vec 2>/dev/null || true
|
||||
- name: Create canary environment from manifest
|
||||
run: |
|
||||
# The canary pair + the unit shim battery drive the COMPILED surfaces
|
||||
# (binary CLI parity, ep_loader FFI, ctypes cdylib loads).
|
||||
#
|
||||
# Toolchain law (troubleshooting history): the SYSTEM rustc is broken
|
||||
# on this host — librustc_driver-<sha>.so (pacman rust 1:1.99.0-1)
|
||||
# references std::string::_M_mutate under the LLVM_23.1 version node,
|
||||
# but /usr/lib/libLLVM.so.23.1 does not define that narrow-char
|
||||
# symbol under that node (it exports _M_create/_M_assign there, and
|
||||
# only the WIDE-char _M_mutate) — every compile dies with `symbol
|
||||
# lookup error` while `cargo --version` still prints. The rustup
|
||||
# toolchain under /home/nick/.rustup is healthy and ships its own
|
||||
# libLLVM, and /home/nick carries the x-ACL for this runner user.
|
||||
# Point PATH at the toolchain bin dir (bypasses the rustup shim,
|
||||
# which would exec the broken system rustc via RUSTUP_TOOLCHAIN).
|
||||
# Target dir is runner-owned (/tmp): the repo's target/ is nick's.
|
||||
export PATH="/home/nick/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin:$PATH"
|
||||
export CARGO_TARGET_DIR="/tmp/rune-ci-target-${{ github.run_id }}"
|
||||
mkdir -p "$CARGO_TARGET_DIR"
|
||||
cargo build --release --manifest-path src/rust/evidence_provenance/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/rune_spine/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_scoring/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/capability_scoring/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/token_counting/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/scaffold_core/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_storage/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_retrieval/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/cognition_core/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/query_memory/Cargo.toml
|
||||
python3 -m venv .canary-venv
|
||||
.canary-venv/bin/pip install --disable-pip-version-check -e ".[dev,memory]"
|
||||
- name: Run canary suite
|
||||
run: |
|
||||
FAILED=0
|
||||
@@ -532,27 +89,8 @@ jobs:
|
||||
src/rune/canary_p0_04.py \
|
||||
src/rune/canary_p0_05.py \
|
||||
src/rune/canary_p0_07.py \
|
||||
src/rune/canary_p1_02.py \
|
||||
src/rune/canary_p1_03.py \
|
||||
src/rune/canary_p1_07.py \
|
||||
src/rune/canary_p1_06.py \
|
||||
src/rune/canary_p1_01.py \
|
||||
src/rune/canary_query_memory.py \
|
||||
src/rune/canary_rune_entry.py \
|
||||
src/rune/canary_spine_meta.py \
|
||||
src/rune/canary_audit_ledger.py \
|
||||
src/rust/rune_spine/canary_spine_rust.py \
|
||||
src/rune/canary_rune_serve.py \
|
||||
src/rune/canary_tui_transport.py \
|
||||
src/rust/memory_scoring/canary_memory_scoring.py \
|
||||
src/rust/capability_scoring/canary_capability_scoring.py \
|
||||
src/rust/token_counting/canary_token_counting.py \
|
||||
src/rust/scaffold_core/canary_scaffold_core.py \
|
||||
src/rust/memory_storage/canary_memory_storage.py \
|
||||
src/rust/memory_retrieval/canary_memory_retrieval.py \
|
||||
src/rust/cognition_core/canary_growth_pins.py \
|
||||
src/rust/cognition_core/canary_persona.py \
|
||||
src/rust/cognition_core/canary_cognition_core.py \
|
||||
src/rune/canary_unbounded.py \
|
||||
src/rune/canary_full_system.py \
|
||||
src/rune/canary_native.py; do
|
||||
@@ -578,8 +116,7 @@ jobs:
|
||||
esac
|
||||
|
||||
best-practices:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
@@ -603,8 +140,7 @@ jobs:
|
||||
echo " $COUNT TODO/FIXME items found"
|
||||
|
||||
documentation:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
@@ -635,26 +171,19 @@ jobs:
|
||||
"
|
||||
|
||||
type-check:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- name: Create type-check environment from lock
|
||||
- name: Create type-check environment from manifest
|
||||
run: |
|
||||
# #150 gate review: the protected type-check toolchain previously
|
||||
# installed floating ".[dev,memory]" — unreproducible. Same lock-only
|
||||
# pattern as every other job: deps from the pinned file, project with
|
||||
# --no-deps so nothing floats outside requirements-ci.txt.
|
||||
python3 -m venv .type-venv
|
||||
.type-venv/bin/pip install --disable-pip-version-check -r requirements-ci.txt
|
||||
.type-venv/bin/pip install --disable-pip-version-check -e . --no-deps
|
||||
.type-venv/bin/pip install --disable-pip-version-check -e ".[dev,memory]"
|
||||
- name: Type-check with ratchet
|
||||
run: |
|
||||
.type-venv/bin/python scripts/type_check_ratchet.py
|
||||
|
||||
gitea-offline:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
@@ -665,90 +194,11 @@ jobs:
|
||||
bash -n scripts/gitea-backup.sh scripts/gitea-restore-verify.sh
|
||||
|
||||
required-checks:
|
||||
# pull_request AND workflow_dispatch: the auto-bump path creates its PR via
|
||||
# API (no pull_request events on this Gitea) and dispatches ci.yml on the
|
||||
# bump branch — without dispatch, required-checks never publishes and the
|
||||
# bump PR can never merge (#219, observed live).
|
||||
if: always()
|
||||
runs-on: rune-ci-desktop
|
||||
if: gitea.event_name == 'pull_request'
|
||||
runs-on: self-hosted
|
||||
# #148: type-check MUST gate the protected status — a red type-check job
|
||||
# previously left the protected merge gate green (dependency-chain hole).
|
||||
# SRE fix (2026-10-10): only the 5 REAL gates gate the pipeline —
|
||||
# best-practices/documentation/gitea-offline are trivially green and
|
||||
# dilute the signal.
|
||||
needs: [lint, unit-tests, canary, type-check, go-surface, rust-surface]
|
||||
needs: [lint, unit-tests, canary, best-practices, documentation, type-check, gitea-offline]
|
||||
steps:
|
||||
- name: Checkout (bump gate reads version surfaces)
|
||||
if: ${{ startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- name: "Light version gate — bump PRs skip the full pipeline (Nick policy)"
|
||||
if: ${{ startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
run: |
|
||||
NEW="$(echo "${{ gitea.event.pull_request.head.ref }}" | sed 's|chore/auto-bump-||')"
|
||||
python3 scripts/check_version_surfaces.py check --root . --version "$NEW"
|
||||
- name: Publish protected-branch gate (full pipeline path — all needs green)
|
||||
# RELEASE-SAFETY FIX (2026-09-13): the previous version of this step
|
||||
# read /tmp/need_* files that no step ever writes and echoed success
|
||||
# UNCONDITIONALLY — required-checks published green while type-check
|
||||
# and unit-tests were red, and deploy promoted the red commits
|
||||
# (observed live: 24413e4, fd6e9b4, de30cd9 promoted 0.66.28/29 with
|
||||
# failing type-check). The verdict now comes from the needs context
|
||||
# itself: this step runs ONLY when every required job succeeded.
|
||||
if: >-
|
||||
${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump')
|
||||
&& needs.lint.result == 'success'
|
||||
&& needs['unit-tests'].result == 'success'
|
||||
&& needs.canary.result == 'success'
|
||||
&& needs['best-practices'].result == 'success'
|
||||
&& needs.documentation.result == 'success'
|
||||
&& needs['type-check'].result == 'success'
|
||||
&& needs['gitea-offline'].result == 'success' }}
|
||||
- name: Publish protected-branch gate
|
||||
run: echo "All required CI checks passed"
|
||||
- name: Refuse promotion when any required check failed (fail-closed)
|
||||
# Companion to the step above: if anything is not exactly success
|
||||
# (failure, cancelled, skipped on a non-bump path), the required-checks
|
||||
# context reports FAILURE — deploy_qualify then refuses promotion.
|
||||
# Fail-closed: not verified → not deployable.
|
||||
if: >-
|
||||
${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump')
|
||||
&& (needs.lint.result != 'success'
|
||||
|| needs['unit-tests'].result != 'success'
|
||||
|| needs.canary.result != 'success'
|
||||
|| needs['best-practices'].result != 'success'
|
||||
|| needs.documentation.result != 'success'
|
||||
|| needs['type-check'].result != 'success'
|
||||
|| needs['gitea-offline'].result != 'success') }}
|
||||
run: |
|
||||
echo "::error::Required CI checks not green — refusing to publish required-checks success"
|
||||
echo "lint=${{ needs.lint.result }} unit-tests=${{ needs['unit-tests'].result }} canary=${{ needs.canary.result }} best-practices=${{ needs['best-practices'].result }} documentation=${{ needs.documentation.result }} type-check=${{ needs['type-check'].result }} gitea-offline=${{ needs['gitea-offline'].result }}"
|
||||
exit 1
|
||||
|
||||
notify-event-bridge:
|
||||
# T16 §16C: post the CI completion WAKE to the local event bridge so
|
||||
# event-driven waiters (deploy.yml → ci_event_wait.py) resume on the
|
||||
# event instead of sleep-polling. ALWAYS runs (success or failure —
|
||||
# a red CI must wake the waiter just as loudly as a green one). The
|
||||
# wake is best-effort and never decides the verdict: the waiter
|
||||
# re-reads the canonical Gitea state via deploy_qualify.py.
|
||||
name: CI Pipeline / notify-event-bridge
|
||||
runs-on: rune-ci-desktop
|
||||
needs: [required-checks]
|
||||
if: always()
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Wake event bridge (wake only — never completion truth)
|
||||
env:
|
||||
CI_SHA: ${{ gitea.sha }}
|
||||
CI_RESULT: ${{ needs.required-checks.result }}
|
||||
RUNE_EVENT_BRIDGE_URL: ${{ vars.RUNE_EVENT_BRIDGE_URL || 'http://127.0.0.1:8791' }}
|
||||
run: python3 scripts/notify_event_bridge.py
|
||||
|
||||
- name: query_memory shadow (pre-shim oracle vs Rust CLI — #2447 qualification)
|
||||
run: |
|
||||
# The shadow runs BOTH implementations on the frozen 24-command
|
||||
# sequence in isolated HOMEs; divergence is a defect. The pre-shim
|
||||
# Python oracle is recovered from git history (6f44a87~1) — the
|
||||
# authority-switched shared/query_memory.py is the Rust core now.
|
||||
python3 scripts/qm_shadow.py
|
||||
@@ -1,79 +0,0 @@
|
||||
name: deploy
|
||||
on:
|
||||
push: null
|
||||
workflow_dispatch: null
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ gitea.ref == 'refs/heads/master' && !startsWith(gitea.event.pull_request.head.ref,
|
||||
'chore/auto-bump') }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: "Wait for CI qualification on this exact SHA (event-driven, T16 \xA716C)"
|
||||
env:
|
||||
GITEA_SERVER: ${{ gitea.server_url }}
|
||||
GITEA_REPO: ${{ gitea.repository }}
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
RUNE_WR_ROOT: /mnt/workdrive/wr-bridge
|
||||
RUNE_WR_CODE: /home/nick/.local/share/mimo-wait-resume
|
||||
run: "DEPLOY_SHA=$(git rev-parse HEAD)\nexport DEPLOY_SHA\necho \"Qualifying\
|
||||
\ $DEPLOY_SHA (event-driven)\"\n# READINESS BEFORE ACTION (\xA716U): fail\
|
||||
\ with a clear message if\n# the bridge rendezvous is not writable in THIS\
|
||||
\ user's context,\n# instead of a stack trace at FIFO creation time.\nif !\
|
||||
\ touch /mnt/workdrive/wr-bridge/resumes/.write-probe 2>/dev/null; then\n\
|
||||
\ echo \"::error::RUNE_WR_ROOT not writable by $(id -un) \u2014 bridge data\
|
||||
\ root misconfigured\"\n exit 1\nfi\nrm -f /mnt/workdrive/wr-bridge/resumes/.write-probe\n\
|
||||
python3 scripts/ci_event_wait.py --sha \"$DEPLOY_SHA\" --deadline 900\n"
|
||||
- name: Stage, verify, promote (monotonic)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
GITEA_SERVER: ${{ gitea.server_url }}
|
||||
GITEA_REPO: ${{ gitea.repository }}
|
||||
run: 'python3 scripts/deploy_stage.py
|
||||
|
||||
'
|
||||
- name: Promotion truthfulness check
|
||||
id: promotion_truth
|
||||
run: "SHA=$(git rev-parse HEAD)\nCURRENT_TARGET=$(readlink /mnt/workdrive/rune-code/current\
|
||||
\ || true)\nif [ \"$CURRENT_TARGET\" = \"/mnt/workdrive/rune-code/${SHA:0:12}\"\
|
||||
\ ]; then\n echo \"PROMOTED: current == candidate ($SHA)\"\n echo \"promoted=true\"\
|
||||
\ >> \"$GITEA_OUTPUT\"\nelse\n # Not promoted. deploy_stage exit 0 + non-matching\
|
||||
\ current =\n # NOOP_ALREADY_CURRENT or STALE_SUPERSEDED \u2014 truthful\
|
||||
\ non-error\n # terminal states (a newer build already won). Do NOT claim\n\
|
||||
\ # promotion; the runtime-verification step below only runs for\n # real\
|
||||
\ promotions.\n echo \"NOT PROMOTED: current -> $CURRENT_TARGET (superseded/noop\
|
||||
\ \u2014 current is authoritative)\"\n echo \"promoted=false\" >> \"$GITEA_OUTPUT\"\
|
||||
\nfi\n"
|
||||
- name: Post-deploy runtime verification (only for real promotions)
|
||||
if: ${{ success() }}
|
||||
run: "SHA=$(git rev-parse HEAD)\nCURRENT_TARGET=$(readlink /mnt/workdrive/rune-code/current\
|
||||
\ || true)\nif [ \"$CURRENT_TARGET\" != \"/mnt/workdrive/rune-code/${SHA:0:12}\"\
|
||||
\ ]; then\n echo \"SKIP runtime verification: this job did not promote (superseded/noop)\"\
|
||||
\n exit 0\nfi\n# Exactly what the launcher execs: cd <current>/src, import\
|
||||
\ rune.\nRUNE_HOME=/mnt/workdrive/rune RUNE_MEMORY_DIR=/home/nick/.config/opencode/memory\
|
||||
\ \\\n /home/nick/.venvs/memory/bin/python3 -c \"\n import sys\n sys.path.insert(0,\
|
||||
\ '/mnt/workdrive/rune-code/current/src')\n import rune\n info = rune.build_info()\n\
|
||||
\ assert info['commit'] == '$SHA', f'runtime SHA mismatch: {info}'\n assert\
|
||||
\ rune.__version__ == info['version']\n print('RUNTIME VERIFIED:', info)\n\
|
||||
\ \"\n"
|
||||
- name: Release-history convergence fallback (SCAFFOLD-TRANCHE-0012C)
|
||||
if: ${{ success() && steps.promotion_truth.outputs.promoted == 'true' }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: "VERSION=$(python3 -c \"import sys; sys.path.insert(0,'src'); import rune;\
|
||||
\ print(rune.__version__)\" 2>/dev/null || echo \"\")\nif [ -z \"$VERSION\"\
|
||||
\ ]; then\n echo \"SKIP: cannot determine version\"\n exit 0\nfi\npython3\
|
||||
\ scripts/release_history_cli.py refresh \\\n --version \"$VERSION\" \\\n\
|
||||
\ --server \"$RELEASE_SERVER_URL\" --repo \"$RELEASE_REPO\" \\\n --store\
|
||||
\ \"$GITHUB_WORKSPACE/store/scaffold/current.json\" \\\n --release-sha \"\
|
||||
$(git rev-parse HEAD)\" \\\n --deployed-sha \"$(git rev-parse HEAD)\" \\\n\
|
||||
|| echo \"::warning::release-history fallback refresh FAILED \u2014 in-lock\
|
||||
\ hook may have already converged; retry converges\"\n"
|
||||
concurrency:
|
||||
group: deploy-${ github.ref }
|
||||
cancel-in-progress: true
|
||||
@@ -1,156 +0,0 @@
|
||||
name: Fault Watch
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI Pipeline"]
|
||||
types: [completed]
|
||||
schedule:
|
||||
- cron: '*/15 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
fault-watch:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
steps:
|
||||
- name: Check HEAD's named gates and file/close fault issues
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
// The ONLY gates that matter — the five real code checks.
|
||||
const REQUIRED_GATES = ['lint', 'unit-tests', 'rust-surface', 'go-surface', 'canary'];
|
||||
|
||||
// Get the HEAD commit's combined status
|
||||
let headSha;
|
||||
try {
|
||||
const branchRes = await github.rest.repos.getBranch({ owner, repo, branch: 'master' });
|
||||
headSha = branchRes.data.commit.sha;
|
||||
} catch (e) {
|
||||
core.warning(`Cannot get HEAD branch: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const statuses = [];
|
||||
try {
|
||||
const res = await github.rest.repos.listCommitStatusesForRef({
|
||||
owner, repo, ref: headSha, per_page: 100,
|
||||
});
|
||||
statuses.push(...res.data);
|
||||
} catch (e) {
|
||||
core.warning(`Cannot list statuses for ${headSha}: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Deduplicate: keep only the LATEST status per context
|
||||
const latest = new Map();
|
||||
for (const s of statuses) {
|
||||
if (!latest.has(s.context)) latest.set(s.context, s);
|
||||
}
|
||||
|
||||
// Check each required gate
|
||||
const failures = [];
|
||||
const passes = [];
|
||||
for (const gate of REQUIRED_GATES) {
|
||||
let found = false;
|
||||
for (const [context, status] of latest) {
|
||||
// Match gate name in the context string (Gitea prefixes with workflow name)
|
||||
if (context.includes(gate)) {
|
||||
found = true;
|
||||
if (status.state === 'success') {
|
||||
passes.push(gate);
|
||||
} else if (status.state === 'failure') {
|
||||
failures.push({ gate, sha: headSha.substring(0, 8), context, description: status.description || 'no description' });
|
||||
}
|
||||
// pending = still running, don't count as failure
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// Gate hasn't reported yet on this SHA — not a failure, it's pending
|
||||
core.info(`Gate ${gate}: no status reported on ${headSha.substring(0, 8)} — pending`);
|
||||
}
|
||||
}
|
||||
|
||||
core.info(`HEAD ${headSha.substring(0, 8)}: passes=${passes.join(',')} failures=${failures.length}`);
|
||||
for (const f of failures) {
|
||||
core.info(` FAIL: ${f.gate} — ${f.description}`);
|
||||
}
|
||||
|
||||
if (failures.length === 0) {
|
||||
core.info('All named gates green or pending — no faults to file.');
|
||||
// Auto-close: find open ci-fault issues whose gate is now green
|
||||
const openFaults = [];
|
||||
try {
|
||||
const issues = await github.rest.issues.listForRepo({
|
||||
owner, repo, state: 'open', labels: 'ci-fault', per_page: 50,
|
||||
});
|
||||
openFaults.push(...issues.data.filter(i => !i.pull_request));
|
||||
} catch (e) { core.warning(`Cannot list ci-fault issues: ${e.message}`); }
|
||||
for (const issue of openFaults) {
|
||||
// Check if this issue's gate is now green
|
||||
const gateName = issue.title.match(/gate (\w+)/i)?.[1] ||
|
||||
issue.body?.match(/Gate:\s*(\w+)/i)?.[1];
|
||||
if (gateName && passes.includes(gateName)) {
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: issue.number,
|
||||
body: `✅ **Gate ${gateName} is green on HEAD** (${headSha.substring(0, 8)}). Auto-closing.`,
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner, repo, issue_number: issue.number, state: 'closed',
|
||||
});
|
||||
core.info(`Auto-closed ci-fault issue #${issue.number} (${gateName} green).`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// File failures as issues — dedup by (gate, error-signature)
|
||||
for (const f of failures) {
|
||||
const dedupKey = `ci-fault-${f.gate}`;
|
||||
// Check for an existing open issue with this gate
|
||||
let existing = null;
|
||||
try {
|
||||
const issues = await github.rest.issues.listForRepo({
|
||||
owner, repo, state: 'open', labels: 'ci-fault', per_page: 50,
|
||||
});
|
||||
existing = issues.data.find(i =>
|
||||
!i.pull_request && i.title.toLowerCase().includes(f.gate.toLowerCase())
|
||||
);
|
||||
} catch (e) { core.warning(`Cannot check existing issues: ${e.message}`); }
|
||||
|
||||
if (existing) {
|
||||
// Update with a comment (same gate, maybe different SHA)
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: existing.number,
|
||||
body: `🔴 **Still failing on ${f.sha}** — ${f.description}\n\nCommit: ${headSha}`,
|
||||
});
|
||||
core.info(`Updated existing ci-fault issue #${existing.number} for ${f.gate}.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a new ci-fault issue
|
||||
await github.rest.issues.create({
|
||||
owner, repo,
|
||||
title: `CI FAULT: gate ${f.gate} failing on HEAD`,
|
||||
body: [
|
||||
`## CI Gate Failure`,
|
||||
``,
|
||||
`**Gate:** ${f.gate}`,
|
||||
`**SHA:** ${f.sha}`,
|
||||
`**Status:** failure`,
|
||||
`**Description:** ${f.description}`,
|
||||
`**Commit:** ${headSha}`,
|
||||
``,
|
||||
`This issue was auto-filed by the fault-watch. It auto-closes when the gate goes green on a pushed commit.`,
|
||||
``,
|
||||
`**Severity rule:** if this gate failure means ALL subsequent submissions also fail (pipeline-blocking), fix immediately. Otherwise: finish the current batch, then fix before the next submission.`,
|
||||
].join('\n'),
|
||||
labels: ['ci-fault', 'bug'],
|
||||
});
|
||||
core.info(`Filed ci-fault issue for ${f.gate} on ${f.sha}.`);
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
name: Local-Model First-Pass Review (#131)
|
||||
|
||||
# Non-blocking PR review by a local model (Ollama on the 4070 Ti).
|
||||
# Advisory only — nothing gates on the local model. The two-gate reviews
|
||||
# (spec + quality) remain the enforcement path; this is a first-pass
|
||||
# filter adding redundancy against controller degradation.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
first-pass-review:
|
||||
runs-on: rune-ci-desktop
|
||||
# Advisory review needs PR context (diff base + comment target). Push
|
||||
# runs have neither — run NOTHING on push (the 20721 lesson: guarding
|
||||
# only the collect step let the post step fail the run at
|
||||
# /issues//comments). Auto-bump PRs stay excluded as before.
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Collect the PR diff
|
||||
run: |
|
||||
if [ -z "$GITEA_BASE_REF" ]; then
|
||||
echo "no PR context (push run) — advisory review not applicable"
|
||||
exit 0
|
||||
fi
|
||||
git diff "origin/${GITEA_BASE_REF}...HEAD" --stat > /tmp/pr_stat.txt
|
||||
git diff "origin/${GITEA_BASE_REF}...HEAD" > /tmp/pr_diff.txt
|
||||
wc -l /tmp/pr_diff.txt /tmp/pr_stat.txt
|
||||
env:
|
||||
GITEA_BASE_REF: ${{ gitea.event.pull_request.base.ref }}
|
||||
- name: Local-model review (Ollama — advisory, never blocking)
|
||||
env:
|
||||
OLLAMA_HOST: ${{ secrets.OLLAMA_HOST }}
|
||||
run: |
|
||||
# Fail-open: no Ollama reachable → skip with a note, never fail.
|
||||
if ! curl -sf "${OLLAMA_HOST:-http://192.168.1.50:11434}/api/tags" > /dev/null 2>&1; then
|
||||
echo "Ollama unreachable — skipping first-pass review (advisory only)"
|
||||
exit 0
|
||||
fi
|
||||
MODEL="${OLLAMA_MODEL:-qwen2.5-coder:7b}"
|
||||
python3 scripts/local_firstpass_review.py \
|
||||
--diff /tmp/pr_diff.txt --stat /tmp/pr_stat.txt \
|
||||
--model "$MODEL" --host "${OLLAMA_HOST:-http://192.168.1.50:11434}"
|
||||
- name: Post findings as PR comment
|
||||
if: ${{ gitea.event.pull_request.number != 0 }}
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
PR_NUMBER: ${{ gitea.event.pull_request.number }}
|
||||
run: |
|
||||
if [ -f /tmp/review_findings.md ]; then
|
||||
export PR_NUMBER
|
||||
python3 - <<'PYEOF'
|
||||
import json, os, urllib.request
|
||||
body = open("/tmp/review_findings.md").read()
|
||||
if not body.strip():
|
||||
print("no findings — no comment")
|
||||
exit(0)
|
||||
base = os.environ["GITEA_SERVER_URL"] + "/api/v1/repos/" + os.environ["GITEA_REPOSITORY"]
|
||||
req = urllib.request.Request(
|
||||
base + "/issues/" + os.environ["PR_NUMBER"] + "/comments",
|
||||
data=json.dumps({"body": "## First-pass review (local model, advisory)\n\n" + body}).encode(),
|
||||
headers={"Authorization": "token " + os.environ["GITEA_TOKEN"], "Content-Type": "application/json"},
|
||||
method="POST")
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
print("comment posted:", r.status)
|
||||
PYEOF
|
||||
fi
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
lint-and-format:
|
||||
runs-on: rune-ci-desktop
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Lint Python files
|
||||
@@ -18,16 +18,13 @@ jobs:
|
||||
ruff format shared/ src/ || true
|
||||
|
||||
canary-suite:
|
||||
runs-on: rune-ci-desktop
|
||||
runs-on: self-hosted
|
||||
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 src/rune/canary_*.py; do
|
||||
for canary in shared/canary_*.py; do
|
||||
if [ -f "$canary" ]; then
|
||||
echo "Running $canary..."
|
||||
python3 "$canary" || FAILED=$((FAILED + 1))
|
||||
@@ -37,7 +34,7 @@ jobs:
|
||||
exit $FAILED
|
||||
|
||||
best-practices:
|
||||
runs-on: rune-ci-desktop
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check best practices
|
||||
@@ -50,8 +47,8 @@ jobs:
|
||||
|
||||
# Check for missing docstrings
|
||||
echo "--- Missing docstrings ---"
|
||||
python3 - <<'PYBEST'
|
||||
import ast, os, sys
|
||||
python3 -c "
|
||||
import ast, os
|
||||
for root, dirs, files in os.walk('shared'):
|
||||
for f in files:
|
||||
if f.endswith('.py') and not f.startswith('canary'):
|
||||
@@ -62,17 +59,15 @@ jobs:
|
||||
if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
|
||||
if not ast.get_docstring(node):
|
||||
print(f' {f}: {node.name} missing docstring')
|
||||
except Exception as exc:
|
||||
print(f' warning: could not parse {f}: {exc}', file=sys.stderr)
|
||||
PYBEST
|
||||
echo " Clean"
|
||||
except: pass
|
||||
" || echo " Clean"
|
||||
|
||||
# Check for TODO/FIXME
|
||||
echo "--- TODO/FIXME ---"
|
||||
grep -rn "TODO\|FIXME\|HACK\|XXX" shared/ src/ --include="*.py" | head -10 || echo " Clean"
|
||||
|
||||
documentation-check:
|
||||
runs-on: rune-ci-desktop
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check documentation coverage
|
||||
@@ -87,8 +82,8 @@ jobs:
|
||||
|
||||
# Check function docs
|
||||
echo "--- Function documentation ---"
|
||||
python3 - <<'PYDOCS'
|
||||
import ast, os, sys
|
||||
python3 -c "
|
||||
import ast, os
|
||||
total = 0
|
||||
documented = 0
|
||||
for root, dirs, files in os.walk('shared'):
|
||||
@@ -103,51 +98,7 @@ jobs:
|
||||
total += 1
|
||||
if ast.get_docstring(node):
|
||||
documented += 1
|
||||
except Exception as exc:
|
||||
print(f' warning: could not parse {f}: {exc}', file=sys.stderr)
|
||||
except: pass
|
||||
pct = (documented / total * 100) if total > 0 else 0
|
||||
print(f'Public functions: {total}, Documented: {documented} ({pct:.1f}%)')
|
||||
PYDOCS
|
||||
|
||||
# Release drift reconciliation (#192): the advisory counterpart to the
|
||||
# push-triggered safety net in auto-version.yml. Detects master/tag drift
|
||||
# and files a deduped issue — it never auto-releases without the PR flow.
|
||||
release-drift-reconciliation:
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Convergence decision
|
||||
run: |
|
||||
VERSION=$(python3 -c 'import re; text=open("src/rune/__init__.py").read(); print(re.search(r"__version__ = \"([0-9]+\.[0-9]+\.[0-9]+)\"", text).group(1))')
|
||||
echo "MASTER_VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
echo "LATEST_TAG=$(python3 scripts/tag_repair.py --latest-tag --namespace main)" >> "$GITHUB_ENV"
|
||||
python3 scripts/version_convergence.py > /tmp/convergence.json
|
||||
echo "Convergence: $(cat /tmp/convergence.json)"
|
||||
python3 - <<'PYENV'
|
||||
import json, os
|
||||
decision = json.loads(open("/tmp/convergence.json").read())
|
||||
with open(os.environ["GITHUB_ENV"], "a") as env:
|
||||
env.write(f"CONVERGENCE_JSON={json.dumps(decision)}\n")
|
||||
env.write(f"DRIFT={'true' if not decision['converged'] else 'false'}\n")
|
||||
PYENV
|
||||
|
||||
- name: File or update the deduped drift issue
|
||||
if: env.DRIFT == 'true'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
python3 scripts/file_drift_issue.py
|
||||
|
||||
- name: Converged — close any resolved drift issue
|
||||
if: env.DRIFT == 'false'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
python3 scripts/file_drift_issue.py --close-if-resolved
|
||||
"
|
||||
@@ -1,38 +0,0 @@
|
||||
name: Merge robot
|
||||
on:
|
||||
schedule:
|
||||
- cron: 23 * * * *
|
||||
workflow_dispatch: {}
|
||||
workflow_run:
|
||||
workflows:
|
||||
- CI Pipeline
|
||||
- Auto-version and Release
|
||||
types:
|
||||
- completed
|
||||
permissions:
|
||||
pull_requests: write
|
||||
issues: write
|
||||
contents: read
|
||||
env:
|
||||
OWNER: conductor
|
||||
REPO: rune
|
||||
jobs:
|
||||
merge-green:
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- name: "Checkout repo \u2014 job never checked out, scripts/merge_robot.py missing,\
|
||||
\ exit 2 since #211"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Merge PRs whose protected checks are green (fail-loud)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
SERVER: ${{ gitea.server_url }}
|
||||
run: 'python3 scripts/merge_robot.py "$GITEA_TOKEN" "$SERVER/api/v1/repos/conductor/rune"
|
||||
|
||||
'
|
||||
concurrency:
|
||||
group: merge-robot
|
||||
cancel-in-progress: false
|
||||
max-parallel: 1
|
||||
@@ -3,8 +3,6 @@ name: Pipeline Order Gate
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, closed]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
schedule:
|
||||
# Hourly sweep: catches newly-eligible issues when a dependency closed
|
||||
# while the teacher was down/not yet deployed (temporal gap fix).
|
||||
@@ -18,7 +16,7 @@ jobs:
|
||||
# `Depends on: #N` reference in its body is CLOSED. Otherwise Gitea
|
||||
# blocks the stage (removes ready, adds blocked, names the blocker).
|
||||
gate:
|
||||
runs-on: rune-ci-desktop
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Verify dependency order before allowing ready
|
||||
uses: actions/github-script@v7
|
||||
@@ -60,15 +58,9 @@ jobs:
|
||||
}
|
||||
}
|
||||
if (openDeps.length === 0) {
|
||||
// Auto-apply research-pending: the issue cannot start until a
|
||||
// 4-phase research round (INWARD / OUTWARD / INNOVATION / SYNTHESIS)
|
||||
// is completed and posted as a comment.
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number: issue.number, labels: ['research-pending'],
|
||||
}).catch(e => { if (e.status !== 404) throw e; });
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: issue.number,
|
||||
body: `🟢 **READY — order satisfied.** Dependencies: ${uniqueDeps.length ? uniqueDeps.map(n => '#' + n).join(', ') : 'none'}.\n\n📋 **Research round required** (RESEARCH ROUND V3 — 4 phases):\n1. **INWARD** — codebase analysis\n2. **OUTWARD** — state-of-the-art survey (libraries, papers, production systems; "is there anything BETTER?")\n3. **INNOVATION** — viability-filtered improvements (evaluate, optionally implement)\n4. **SYNTHESIS** — specific build instructions\n\nRune (the agent) performs the research and posts the findings as a comment on this issue. The \`research-pending\` label will be cleared automatically and the issue becomes startable.`,
|
||||
body: `🟢 **READY — order satisfied; Rune may start.** Dependencies: ${uniqueDeps.length ? uniqueDeps.map(n => '#' + n).join(', ') : 'none'}.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -90,7 +82,7 @@ jobs:
|
||||
# issue (open, has the orchestration-stage marker in its body, all deps
|
||||
# closed) and label it ready — the order teacher keeps Rune on task.
|
||||
teach:
|
||||
runs-on: rune-ci-desktop
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Announce the next issue in dependency order
|
||||
uses: actions/github-script@v7
|
||||
@@ -162,303 +154,3 @@ jobs:
|
||||
body: (() => { const closed = context.payload.issue ? ' (closed: #' + context.payload.issue.number + ')' : ' (scheduled sweep)'; return `🎓 **NEXT: #${next.number} — ${next.title}** — dependencies satisfied${closed}; Rune may start.`; })(),
|
||||
});
|
||||
core.info(`Announced #${next.number} as next.`);
|
||||
|
||||
# RESEARCH CLEAR: when a comment containing the 4-phase research markers
|
||||
# lands on a research-pending issue, clear the label and announce start.
|
||||
research-clear:
|
||||
runs-on: rune-ci-desktop
|
||||
if: github.event_name == 'issue_comment' && github.event.action == 'created'
|
||||
steps:
|
||||
- name: Clear research-pending when research round is posted
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
if (!issue) { core.info('no issue in payload'); return; }
|
||||
const comment = context.payload.comment?.body || '';
|
||||
|
||||
// --- Phase header detection (case-insensitive) ---
|
||||
const hasInward = /\bINWARD\b/i.test(comment);
|
||||
const hasOutward = /\bOUTWARD\b/i.test(comment);
|
||||
const hasInnovation = /\bINNOVATION\b/i.test(comment);
|
||||
const hasSynthesis = /\bSYNTHESIS\b/i.test(comment);
|
||||
if (!hasInward || !hasOutward || !hasInnovation || !hasSynthesis) {
|
||||
core.info(`research-clear: comment on #${issue.number} missing phases (inward=${hasInward} outward=${hasOutward} innovation=${hasInnovation} synthesis=${hasSynthesis}) — skip`);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Evidence enforcement (per phase, section-scoped) ---
|
||||
// Split the comment into phase sections on the phase headers.
|
||||
function section(phase) {
|
||||
const re = new RegExp(`\\b${phase}\\b[\\s\\S]*?(?=\\b(?:INWARD|OUTWARD|INNOVATION|SYNTHESIS)\\b|$)`, 'i');
|
||||
const m = comment.match(re);
|
||||
return m ? m[0] : '';
|
||||
}
|
||||
const forgeHost = context.payload.repository?.html_url ? new URL(context.payload.repository.html_url).host : '';
|
||||
const externalUrlRe = /https?:\/\/[^\s)\]>]+/gi;
|
||||
const codePathRe = /(?:^|\s)(?:src|tests|scripts|docs|\.gitea)\/[\w./-]+/gi;
|
||||
const plansPathRe = /(?:^|\s)plans\/[\w./-]+/gi;
|
||||
|
||||
function uniqueExternalUrls(text) {
|
||||
const found = text.match(externalUrlRe) || [];
|
||||
const uniq = new Map(); // normalized key -> original URL (fetchable)
|
||||
for (const u of found) {
|
||||
try {
|
||||
const parsed = new URL(u);
|
||||
if (parsed.host && parsed.host !== forgeHost) {
|
||||
const key = parsed.host + parsed.pathname;
|
||||
if (!uniq.has(key)) uniq.set(key, u);
|
||||
}
|
||||
} catch (e) { /* skip malformed */ }
|
||||
}
|
||||
return uniq;
|
||||
}
|
||||
function codeCitations(text) {
|
||||
return new Set((text.match(codePathRe) || []).map(s => s.trim()));
|
||||
}
|
||||
|
||||
const inwardSec = section('INWARD');
|
||||
const outwardSec = section('OUTWARD');
|
||||
const innovationSec = section('INNOVATION');
|
||||
const synthesisSec = section('SYNTHESIS');
|
||||
const tailSec = innovationSec + '\n' + synthesisSec;
|
||||
|
||||
const inwardCites = codeCitations(inwardSec);
|
||||
const outwardUrls = uniqueExternalUrls(outwardSec);
|
||||
const tailUrls = uniqueExternalUrls(tailSec);
|
||||
const tailPlans = (tailSec.match(plansPathRe) || []).length;
|
||||
|
||||
const missing = [];
|
||||
if (inwardCites.size < 2) missing.push(`- **INWARD**: needs ≥2 codebase citations (paths like src/..., tests/..., .gitea/...) — found ${inwardCites.size}.`);
|
||||
if (outwardUrls.size < 2) missing.push(`- **OUTWARD**: needs ≥2 UNIQUE external source URLs (not this forge) — found ${outwardUrls.size}. Research must pull real sources to learn from so later stages build on evidence.`);
|
||||
if (tailUrls.size < 1 && tailPlans < 1) missing.push(`- **INNOVATION/SYNTHESIS**: needs ≥1 source URL reference or a plans/ artifact path tying synthesis back to evidence.`);
|
||||
|
||||
// --- INNOVATION must be source-tied: ≥1 external URL from OUTWARD
|
||||
// re-referenced (proving candidates derive from pulled sources, not invention).
|
||||
const innovationUrlKeys = [...uniqueExternalUrls(innovationSec).keys()];
|
||||
const tiedToOutward = innovationUrlKeys.filter(k => outwardUrls.has(k));
|
||||
if (outwardUrls.size >= 2 && tiedToOutward.length < 1) {
|
||||
missing.push(`- **INNOVATION**: candidate approaches must derive from OUTWARD sources — re-reference ≥1 of the OUTWARD source URLs here and say what it contributed. Found 0 source-tied candidates.`);
|
||||
}
|
||||
|
||||
// --- SYNTHESIS qualification gate: the synthesis must (a) declare its
|
||||
// evidence sufficiency, (b) judge feasibility REALISTICALLY (mechanism +
|
||||
// assumptions + failure modes), and (c) either commit to a plan artifact
|
||||
// or honestly bounce to MORE RESEARCH. Evidence-insufficient synthesis
|
||||
// MUST declare MORE RESEARCH — bluffing through fails the gate.
|
||||
const SYNTH_REJECTS = [];
|
||||
const suffMarker = /\bevidence[^\n]{0,40}(suffic|adequat|insuffic)|insufficient evidence|evidence gap/i;
|
||||
const hasSufficiency = suffMarker.test(synthesisSec);
|
||||
const moreResearch = /\bMORE[- ]RESEARCH\b/i.test(synthesisSec);
|
||||
const hasPlanArtifact = /(?:^|\s)plans\/[\w./-]+/.test(synthesisSec);
|
||||
if (moreResearch) {
|
||||
// Honest loop-back path: open questions REQUIRED, feasibility verdict
|
||||
// explicitly NOT required — a substantiated MORE-RESEARCH is the honest
|
||||
// outcome; demanding feasibility prose would teach padding.
|
||||
const openQs = (synthesisSec.match(/\?\s*$/gm) || []).length + (synthesisSec.match(/\bopen question|\bunknown|\bunresolved/gi) || []).length;
|
||||
if (openQs < 1) {
|
||||
SYNTH_REJECTS.push(`- **MORE-RESEARCH declared but unsubstantiated**: name the specific open questions/unknowns that require more research (at least one '?', 'open question', or 'unknown').`);
|
||||
}
|
||||
if (!hasSufficiency) {
|
||||
SYNTH_REJECTS.push(`- **MORE-RESEARCH qualification**: state what evidence you have, what is missing, and why the gap blocks a design decision (evidence-sufficiency statement).`);
|
||||
}
|
||||
} else {
|
||||
// Proceed-to-plan path: full qualification — sufficiency + feasibility + kill test.
|
||||
if (!hasSufficiency) {
|
||||
SYNTH_REJECTS.push(`- **SYNTHESIS qualification**: no evidence-sufficiency statement. State explicitly whether OUTWARD evidence is sufficient to design from, and why (e.g. 'Evidence sufficient: <2-3 sources converge on the mechanism>' or 'Evidence insufficient: <what specifically is missing>').`);
|
||||
}
|
||||
const feasMarker = /\bfeasib|\bwon'?t work\b|\bwill work\b|\brisk\b|\bfailure mode\b|\bassumption\b|\brealistic/i;
|
||||
const hasFeasibility = feasMarker.test(synthesisSec);
|
||||
if (!hasFeasibility) {
|
||||
SYNTH_REJECTS.push(`- **SYNTHESIS qualification**: no feasibility judgement. State the working mechanism, the load-bearing assumptions, and the failure modes — and judge realistically whether the synthesis WILL WORK. Optimism is not evidence.`);
|
||||
}
|
||||
// Kill test: a feasibility claim must name its cheapest disproof.
|
||||
// The plan must carry it as an early frozen test — prose is not a prediction.
|
||||
const hasKillTest = /\bkill test\b|\bdisprov|\bfalsif/i.test(synthesisSec);
|
||||
if (hasFeasibility && !hasKillTest) {
|
||||
SYNTH_REJECTS.push(`- **SYNTHESIS kill test**: name the cheapest observation that would DISPROVE the feasibility claim (a 'kill test:' line). For workflow-only changes the protected CI run may be the kill test — say so explicitly. A claim with no disproof is not a claim.`);
|
||||
}
|
||||
if (!hasPlanArtifact) {
|
||||
SYNTH_REJECTS.push(`- **SYNTHESIS disposition**: must reference a concrete plans/ artifact (ready to implement) or declare **MORE-RESEARCH** with the specific open questions. Neither found.`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Trail of use: each OUTWARD source must carry an inline annotation
|
||||
// (what was learned from it) on the same line — the verifiable trail.
|
||||
const annotated = [...outwardUrls.entries()].filter(([, url]) => {
|
||||
const line = outwardSec.split('\n').find(l => l.includes(url));
|
||||
if (!line) return false;
|
||||
const prose = line.replace(externalUrlRe, '').replace(/[-*`>\[\]()]/g, '').trim();
|
||||
return prose.length >= 15;
|
||||
});
|
||||
if (outwardUrls.size >= 2 && annotated.length < 2) {
|
||||
missing.push(`- **OUTWARD trail**: each source needs an inline note of what it taught (URL + ≥15 chars of what was learned, same line) — ${annotated.length}/${outwardUrls.size} annotated. A bare URL list is not a research trail.`);
|
||||
}
|
||||
|
||||
// --- Live verification: fetch up to 5 sources; dead/hallucinated links fail.
|
||||
// 2xx/3xx = real; 403/405/406/429 = real-but-bot-blocked (count as verified);
|
||||
// 404/410/DNS/timeout (after 1 retry) = dead.
|
||||
const toVerify = [...outwardUrls.values()].slice(0, 5);
|
||||
const dead = [];
|
||||
for (const u of toVerify) {
|
||||
let ok = false;
|
||||
for (let attempt = 0; attempt < 2 && !ok; attempt++) {
|
||||
try {
|
||||
const ctl = new AbortController();
|
||||
const t = setTimeout(() => ctl.abort(), 8000);
|
||||
const r = await fetch(u, { redirect: 'follow', signal: ctl.signal, headers: { 'User-Agent': 'rune-research-gate/1.0 (source verification)' } });
|
||||
clearTimeout(t);
|
||||
ok = (r.status < 400) || [403, 405, 406, 429].includes(r.status);
|
||||
if (!ok && [404, 410].includes(r.status)) break; // definitively dead, no retry
|
||||
} catch (e) {
|
||||
clearTimeout(t);
|
||||
if (attempt === 1) { /* second failure — stays dead */ }
|
||||
}
|
||||
}
|
||||
if (!ok) dead.push(u);
|
||||
}
|
||||
if (dead.length > 0) {
|
||||
missing.push(`- **OUTWARD verification**: source(s) failed live fetch: ${dead.join(', ')} — dead or unreachable links cannot serve as research evidence.`);
|
||||
}
|
||||
|
||||
const labels0 = (issue.labels || []).map(l => l.name);
|
||||
if (!labels0.includes('research-pending')) {
|
||||
core.info(`research-clear: #${issue.number} has no research-pending label — skip`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `🚫 **Research round REJECTED — evidence gate.** All 4 phase headers present, but the evidence floor is not met:\n\n${missing.join('\n')}\n\nresearch-pending stays. Post the missing evidence (real sources fetched, real paths read) and the gate will re-run on your next comment.`,
|
||||
});
|
||||
core.info(`research-clear: evidence gate rejected #${issue.number}: ${missing.length} gap(s)`);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Honest loop-back: synthesis declared MORE-RESEARCH with substantiated
|
||||
// open questions and all evidence floors met. This is a SUCCESS for honesty:
|
||||
// research-pending stays, the named unknowns drive the next research pass.
|
||||
// Bound: after 2 prior loop-backs on this issue, escalate to the owner
|
||||
// instead of cycling silently — infinite MORE-RESEARCH is dithering.
|
||||
if (moreResearch) {
|
||||
let priorLoopBacks = 0;
|
||||
try {
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number, per_page: 100,
|
||||
});
|
||||
priorLoopBacks = comments.data.filter(c => (c.body || '').includes('honest loop-back accepted')).length;
|
||||
} catch (e) { core.info('loop-back count fetch failed: ' + (e && e.message)); }
|
||||
if (priorLoopBacks >= 2) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `⏸️ **Loop-back bound reached (${priorLoopBacks} prior MORE-RESEARCH cycles).** The synthesis again declares MORE-RESEARCH with substantiated open questions. Escalating to the owner rather than cycling silently: this issue needs a human decision — narrow the scope, accept a design under uncertainty, or split the unknowns into their own research issues.`,
|
||||
});
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number, labels: ['blocked'],
|
||||
}).catch(e => { if (e.status !== 404) throw e; });
|
||||
core.info(`research-clear: loop-back bound reached on #${issue.number} — escalated with blocked label.`);
|
||||
return;
|
||||
}
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `🔁 **Synthesis not ready — honest loop-back accepted (cycle ${priorLoopBacks + 1}).** Evidence floors met (INWARD citations: ${inwardCites.size}, OUTWARD sources: ${outwardUrls.size}, trail annotated: ${annotated.length}, live-verified: ${outwardUrls.size - dead.length}), but the synthesis itself declares MORE-RESEARCH with substantiated open questions.\n\nresearch-pending stays. Next round must resolve the named unknowns — a later synthesis must either produce a plans/ artifact or identify NEW unknowns. Bound: 2 loop-backs, then escalation.`,
|
||||
});
|
||||
core.info(`research-clear: honest MORE-RESEARCH loop-back on #${issue.number} (cycle ${priorLoopBacks + 1}) — research-pending retained.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear research-pending, add in-progress
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number, name: 'research-pending',
|
||||
});
|
||||
} catch (e) { if (e.status !== 404) throw e; }
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number, labels: ['in-progress'],
|
||||
}).catch(e => { if (e.status !== 404) throw e; });
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `🔬 **Research round verified — synthesis qualified.** 4 phases + evidence floor met (INWARD citations: ${inwardCites.size}, OUTWARD sources: ${outwardUrls.size}, trail annotated: ${annotated.length}, live-fetch verified: ${outwardUrls.size - dead.length}); synthesis declared evidence ${hasSufficiency ? 'sufficiency statement ✓' : '✓'} + feasibility judgement ✓ and committed to a plans/ artifact.\n\n▶️ **in-progress applied** — Rune is now working on this issue. The pipeline will notify at each transition.`,
|
||||
});
|
||||
core.info(`research-clear: #${issue.number} research verified with evidence, label cleared.`);
|
||||
|
||||
# INCOMPLETE CATCHER: detect branches with commits but no PR, belonging to
|
||||
# in-progress issues — auto-create draft PR with incomplete label + ledger.
|
||||
# Runs on the hourly sweep (piggybacks on existing schedule trigger).
|
||||
incomplete-catcher:
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- name: Detect abandoned in-progress work and create draft PRs
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
// Find open issues with in-progress label
|
||||
const issues = [];
|
||||
for (let page = 1; ; page++) {
|
||||
const res = await github.rest.issues.listForRepo({
|
||||
owner, repo, state: 'open', labels: 'in-progress', per_page: 50, page,
|
||||
});
|
||||
issues.push(...res.data.filter(i => !i.pull_request));
|
||||
if (res.data.length < 50) break;
|
||||
}
|
||||
if (issues.length === 0) { core.info('No in-progress issues.'); return; }
|
||||
// Find open branches
|
||||
const branches = await github.rest.repos.listBranches({ owner, repo, per_page: 100 });
|
||||
const branchNames = branches.data.map(b => b.name);
|
||||
// Find existing PRs
|
||||
const prs = await github.rest.pulls.list({ owner, repo, state: 'open', per_page: 100 });
|
||||
const prHeads = new Set(prs.data.map(p => p.head.ref));
|
||||
for (const issue of issues) {
|
||||
// Look for a branch matching the issue pattern
|
||||
const branch = branchNames.find(b => b.includes(String(issue.number)) || b === issue.head_branch);
|
||||
if (!branch || prHeads.has(branch)) continue;
|
||||
// Branch exists, no PR yet — create draft PR with incomplete label
|
||||
try {
|
||||
const pr = await github.rest.pulls.create({
|
||||
owner, repo,
|
||||
title: `[INCOMPLETE] ${issue.title}`,
|
||||
head: branch, base: 'master', draft: true,
|
||||
body: [
|
||||
'## Incomplete Work Ledger',
|
||||
'',
|
||||
`**Issue:** #${issue.number}`,
|
||||
`**Branch:** ${branch}`,
|
||||
`**Status:** Work in progress, paused mid-implementation.`,
|
||||
'',
|
||||
'### What exists',
|
||||
'- See commits on this branch',
|
||||
'',
|
||||
'### What remains',
|
||||
'- Resume implementation from where it left off',
|
||||
'- Run gates when complete',
|
||||
'',
|
||||
'### Resume instructions',
|
||||
'1. Check out this branch',
|
||||
'2. Read the plan file in the worktree',
|
||||
'3. Continue from the last commit',
|
||||
'4. Mark ready when implementation is complete',
|
||||
].join('\n'),
|
||||
});
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number: pr.data.number, labels: ['incomplete'],
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: issue.number,
|
||||
body: `📋 **Incomplete work captured** — draft PR #${pr.data.number} created from branch \`${branch}\`. Resume from there when ready.`,
|
||||
});
|
||||
core.info(`Created draft PR #${pr.data.number} for issue #${issue.number}`);
|
||||
} catch (e) {
|
||||
if (e.status !== 422) throw e; // 422 = PR already exists
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
name: Spec gate (headless)
|
||||
|
||||
# First slice of #204: agent cognition as a scoped, event-triggered Actions
|
||||
# stage. A PR opens → this job runs a single self-contained review prompt
|
||||
# (black-box doctrine per docs/adversarial-review.md — the reviewer answers
|
||||
# the mandatory question BEFORE reading the implementation) → posts a
|
||||
# Conventional-Comments verdict as a NON-BLOCKING PR comment.
|
||||
#
|
||||
# Deterministic floors underneath (untouched): required checks still gate the
|
||||
# merge; this stage adds judgment, never replaces the floors.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull_requests: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
spec-gate:
|
||||
runs-on: rune-ci-desktop
|
||||
# Skip draft PRs and our own automation PRs (bump PRs have no reviewable logic)
|
||||
if: ${{ !startsWith(gitea.event.pull_request.title, 'chore(release)') }}
|
||||
steps:
|
||||
- name: Check out PR head
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run headless spec-gate review
|
||||
env:
|
||||
MIMOCODE_AUTO_APPROVE_DELETE: "1"
|
||||
MIMOCODE_DANGEROUSLY_SKIP_PERMISSIONS: "1"
|
||||
HOME: /var/lib/gitea-desktop-runner
|
||||
XDG_CONFIG_HOME: /home/nick/.config
|
||||
XDG_DATA_HOME: /var/lib/gitea-desktop-runner/.local-share-mimo
|
||||
run: |
|
||||
BASE_SHA="$(git merge-base origin/master HEAD)"
|
||||
{
|
||||
echo "## Spec-gate review request"
|
||||
echo ""
|
||||
echo "You are the INDEPENDENT spec-compliance reviewer for this PR."
|
||||
echo ""
|
||||
echo "MANDATORY FIRST STEP (docs/adversarial-review.md): answer, in writing,"
|
||||
echo "'What incorrect implementation could still pass these tests?' BEFORE"
|
||||
echo "reading the implementation diff. Enumerate the wrong implementations"
|
||||
echo "the existing tests would not catch."
|
||||
echo ""
|
||||
echo "Then: read the diff (git diff $BASE_SHA..HEAD), the changed files, and"
|
||||
echo "any plan files under plans/ referenced in the PR body. Check:"
|
||||
echo "1. Acceptance criteria (from the issue) — PASS/FAIL per criterion with file:line evidence."
|
||||
echo "2. Deviation honesty — are recorded deviations sound? Any weakening of acceptance?"
|
||||
echo "3. Test honesty — behavior-asserting or implementation-coupled? Vacuous assertions?"
|
||||
echo "4. Silent failure — every except re-raises/logs/spools?"
|
||||
echo ""
|
||||
echo "Output format (Conventional Comments):"
|
||||
echo "- Verdict line: 'Verdict: PASS' or 'Verdict: PASS-with-findings' or 'Verdict: Changes-required'"
|
||||
echo "- Findings as: [BLOCKER]/[MAJOR]/[MINOR] — description (file:line)"
|
||||
echo "- The mandatory-question answer, explicitly labeled."
|
||||
echo "Be adversarial. Be specific. Under 500 words."
|
||||
} > /tmp/spec_gate_prompt.txt
|
||||
|
||||
/var/lib/gitea-desktop-runner/bin/mimo run \
|
||||
-m "Verboo Code/glm-5.3-flash" \
|
||||
--dangerously-skip-permissions \
|
||||
--format json \
|
||||
"$(cat /tmp/spec_gate_prompt.txt)" \
|
||||
> /tmp/spec_gate_events.jsonl 2>/tmp/spec_gate_err.log
|
||||
# Extract the final assistant text from the JSONL event stream
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
text = ""
|
||||
try:
|
||||
for line in open("/tmp/spec_gate_events.jsonl"):
|
||||
try:
|
||||
ev = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if ev.get("type") == "assistant" and isinstance(ev.get("message", {}).get("content"), list):
|
||||
for part in ev["message"]["content"]:
|
||||
if part.get("type") == "text":
|
||||
text = part.get("text", text)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
open("/tmp/spec_gate_verdict.md", "w").write(text or "Spec-gate agent produced no text output — check runner logs.")
|
||||
PY
|
||||
|
||||
- name: Post verdict as non-blocking PR comment
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
SERVER: ${{ gitea.server_url }}
|
||||
PR_NUMBER: ${{ gitea.event.pull_request.number }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json, os, urllib.request
|
||||
token = os.environ["GITEA_TOKEN"]
|
||||
base = os.environ["SERVER"].rstrip("/") + "/api/v1/repos/conductor/rune"
|
||||
pr = os.environ["PR_NUMBER"]
|
||||
body = open("/tmp/spec_gate_verdict.md").read().strip()
|
||||
body = ("🤖 **Headless spec gate (non-blocking, #204 first slice)**\n\n" + body +
|
||||
"\n\n<details><summary>Deterministic floors still govern the merge</summary>\n\n"
|
||||
"This comment is advisory. Required checks (lint, tests, canaries, "
|
||||
"type-check, coverage, dependency lock, impact gate) remain the merge "
|
||||
"authority per docs/adversarial-review.md proportionality.</details>")
|
||||
req = urllib.request.Request(
|
||||
f"{base}/issues/{pr}/comments",
|
||||
data=json.dumps({"body": body}).encode(),
|
||||
headers={"Authorization": "token " + token, "Content-Type": "application/json"},
|
||||
method="POST")
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
print(f"verdict posted: {r.status}")
|
||||
PY
|
||||
@@ -1,132 +0,0 @@
|
||||
name: TUI Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'tui-v*'
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
release:
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Read semver from the pushed tag
|
||||
run: |
|
||||
# GITEA_REF_NAME resolves EMPTY on the rune-ci-desktop runner even on
|
||||
# tag pushes (runs 30390/30456/30862: "triggered on non-tui tag " with
|
||||
# an empty tag). Fall back to GITHUB_REF_NAME, then to the tag that
|
||||
# checkout actually put us on (git describe --exact-match on the
|
||||
# detached tag checkout) — wave-release-pipeline-deepdive.
|
||||
TAG="${GITEA_REF_NAME:-${GITHUB_REF_NAME:-$(git describe --tags --exact-match HEAD 2>/dev/null || true)}}"
|
||||
case "$TAG" in
|
||||
tui-v*) VERSION="${TAG#tui-v}" ;;
|
||||
*) echo "::error::triggered on non-tui tag $TAG"; exit 1 ;;
|
||||
esac
|
||||
echo "TUI_TAG=$TAG" >> "$GITHUB_ENV"
|
||||
echo "TUI_VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
echo "releasing $TAG (semver $VERSION)"
|
||||
- name: Paired build with semver injection (vendored, GOPROXY=off)
|
||||
env:
|
||||
GOPROXY: "off"
|
||||
GOFLAGS: "-mod=vendor"
|
||||
run: |
|
||||
[ -f tui/vendor/modules.txt ] || { echo "::error::tui/vendor missing - not a self-contained build"; exit 1; }
|
||||
echo "vendor proof: GOPROXY=$(go env GOPROXY) flags=$(go env GOFLAGS) ($(wc -l < tui/vendor/modules.txt) vendored modules)"
|
||||
bash scripts/build_paired.sh
|
||||
BUILD_ID="$(./verification/build-id/rune-tui --build-id)"
|
||||
echo "TUI_BUILD_ID=$BUILD_ID" >> "$GITHUB_ENV"
|
||||
echo "binary stamped: build=$BUILD_ID semver=$TUI_VERSION (from $TUI_TAG on HEAD)"
|
||||
- name: Create release with commit range since last tui tag
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
LAST_TUI_TAG="$(git tag --list 'tui-v*' --sort=-creatordate | grep -vx "$TUI_TAG" | head -1 || true)"
|
||||
{
|
||||
echo "Native TUI release $TUI_VERSION"
|
||||
echo
|
||||
echo "- semver: \`$TUI_VERSION\` (injected at build from tag \`$TUI_TAG\`)"
|
||||
echo "- paired build id: \`$TUI_BUILD_ID\`"
|
||||
echo "- binary: \`rune-tui-$TUI_VERSION\` (self-contained, vendored deps, GOPROXY=off proof)"
|
||||
echo
|
||||
if [ -n "$LAST_TUI_TAG" ]; then
|
||||
echo "## Commits since $LAST_TUI_TAG"
|
||||
echo
|
||||
git log --oneline "$LAST_TUI_TAG..HEAD"
|
||||
else
|
||||
echo "## Commits (first tui-tagged release)"
|
||||
echo
|
||||
git log --oneline -30
|
||||
fi
|
||||
} > /tmp/tui-release-body.md
|
||||
TUI_TAG="$TUI_TAG" TUI_VERSION="$TUI_VERSION" python3 - <<'PYREL'
|
||||
import json, os, sys, urllib.request
|
||||
|
||||
TOKEN = os.environ["GITEA_TOKEN"]
|
||||
BASE = os.environ["RELEASE_SERVER_URL"].rstrip("/") + "/api/v1/repos/" + os.environ["RELEASE_REPO"]
|
||||
TAG = os.environ["TUI_TAG"]
|
||||
VERSION = os.environ["TUI_VERSION"]
|
||||
|
||||
def api(path, method="GET", data=None):
|
||||
req = urllib.request.Request(
|
||||
BASE + path,
|
||||
data=json.dumps(data).encode() if data is not None else None,
|
||||
headers={"Authorization": "token " + TOKEN, "Content-Type": "application/json"},
|
||||
method=method,
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
body, status = r.read(), r.status
|
||||
except urllib.error.HTTPError as e:
|
||||
body, status = e.read(), e.code
|
||||
try:
|
||||
return status, (json.loads(body) if body else {})
|
||||
except json.JSONDecodeError:
|
||||
return status, {"_raw": body.decode("utf-8", "replace")[:200]}
|
||||
|
||||
body = open("/tmp/tui-release-body.md").read()
|
||||
status, existing = api(f"/releases/tags/{TAG}")
|
||||
if status == 200:
|
||||
release_id = existing["id"]
|
||||
status, _ = api(f"/releases/{release_id}", "PATCH", {"body": body, "name": f"Rune TUI {VERSION}"})
|
||||
if status not in (200, 201):
|
||||
print(f"release patch failed ({status})"); sys.exit(1)
|
||||
print(f"updated existing release {release_id} for {TAG}")
|
||||
else:
|
||||
status, rel = api("/releases", "POST", {
|
||||
"tag_name": TAG, "name": f"Rune TUI {VERSION}",
|
||||
"body": body, "draft": False, "prerelease": False,
|
||||
})
|
||||
if status != 201:
|
||||
print(f"release create failed ({status}): {str(rel)[:300]}"); sys.exit(1)
|
||||
release_id = rel["id"]
|
||||
print(f"created release {release_id} for {TAG}")
|
||||
with open(os.environ["GITHUB_ENV"], "a") as env:
|
||||
env.write(f"TUI_RELEASE_ID={release_id}\n")
|
||||
PYREL
|
||||
- name: Attach the single binary to the release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
ASSET="rune-tui-$TUI_VERSION"
|
||||
# re-runs must not stack duplicate attachments (Gitea 409s on name clash)
|
||||
curl -sf -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$RELEASE_SERVER_URL/api/v1/repos/$RELEASE_REPO/releases/$TUI_RELEASE_ID/assets" \
|
||||
| python3 -c "import json,sys; [print(a['id']) for a in json.load(sys.stdin) if a['name'] == '$ASSET']" \
|
||||
| while read -r AID; do
|
||||
curl -sf -X DELETE -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$RELEASE_SERVER_URL/api/v1/repos/$RELEASE_REPO/releases/$TUI_RELEASE_ID/assets/$AID" || \
|
||||
{ echo "::error::could not clear stale asset $AID"; exit 1; }
|
||||
done
|
||||
curl -sf -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
-F "attachment=@verification/build-id/rune-tui" \
|
||||
"$RELEASE_SERVER_URL/api/v1/repos/$RELEASE_REPO/releases/$TUI_RELEASE_ID/assets?name=$ASSET" \
|
||||
| python3 -c "import json,sys; a=json.load(sys.stdin); print('attached:', a.get('name'), a.get('browser_download_url'))"
|
||||
concurrency:
|
||||
group: tui-release-${{ gitea.ref }}
|
||||
cancel-in-progress: false
|
||||
+1
-72
@@ -10,75 +10,4 @@ build/
|
||||
*.log
|
||||
.env
|
||||
.venv/
|
||||
# Runtime artifacts (cwd-local state + event journal + capability index).
|
||||
# Root-anchored on purpose: unanchored `index/`/`journal/` swallowed the
|
||||
# tracked source dirs memory/index/ and memory/journal/, silently ignoring
|
||||
# every NEW file there — same broad-ignore trap class as the src/go rule
|
||||
# below. (dist/ + build/ reviewed: only collisions live inside
|
||||
# src/rust/*/target/, which this file already ignores, so left generic.)
|
||||
/state/
|
||||
/index/
|
||||
/journal/
|
||||
src/rust/*/target/
|
||||
|
||||
# batch 134: Go build artifacts — binaries are CI-built per gate (go build -o /tmp/...),
|
||||
# never stored. Each module dir keeps go.mod/main.go/truth*.json + *_test.go only.
|
||||
#
|
||||
# Trap history (modegate binary fabrication, K9 sweep accident, 102MB cache
|
||||
# re-lands): the bare `src/go/*/[^.]*` artifact rule also matched the module
|
||||
# dirs themselves and every non-dot entry inside them, so sweeps pruned whole
|
||||
# new modules and every new internal/ file needed `git add -f`. Rules below
|
||||
# keep ONLY source files addable:
|
||||
# - module dirs re-included so git traverses them (new modules stay
|
||||
# visible to status/add sweeps);
|
||||
# - module-level sources by extension (.go/.json/.mod/.sum);
|
||||
# - subpackage dirs (internal/, cmd/, ...) traversable, their contents
|
||||
# ignored by default, then re-included for sources only (.go/.json/.md).
|
||||
# NO binary intent preserved: anything else under a subpackage dir
|
||||
# (compiled binaries, caches) stays ignored.
|
||||
src/go/*/[^.]*
|
||||
!src/go/*/
|
||||
!src/go/*/*.go
|
||||
!src/go/*/*.json
|
||||
!src/go/*/*.mod
|
||||
!src/go/*/*.sum
|
||||
!src/go/*/*/
|
||||
src/go/*/*/**
|
||||
!src/go/*/*/**/
|
||||
!src/go/*/*/**/*.go
|
||||
!src/go/*/*/**/*.json
|
||||
!src/go/*/*/**/*.md
|
||||
# explicit re-statement of the module-root binary law (sweep-race repair,
|
||||
# 2026-10-18): the stale untracked tool-executor build artifact sat ignored
|
||||
# ONLY via the broad rule above — named here so the intent survives any
|
||||
# future loosening of that rule. Binaries are CI-built per gate
|
||||
# (go build -o /tmp/...), never stored in the module dir.
|
||||
src/go/tool-executor/tool-executor
|
||||
# committed fixture stores (frozen-truth construction rigs) — fixtures/ and
|
||||
# testdata/ are inputs, not build output: fully tracked at any depth under
|
||||
# src/go/, including non-source members (.jsonl/.txt/etc.).
|
||||
!src/go/**/fixtures/
|
||||
!src/go/**/fixtures/**
|
||||
!src/go/**/testdata/
|
||||
!src/go/**/testdata/**
|
||||
|
||||
# phase 0C task 0.14: paired-build stamps are per-build output (one build
|
||||
# id in verification/build-id/, gitignored like the binaries it stamps).
|
||||
verification/build-id/
|
||||
tui/boundary/gen/build_id_gen.go
|
||||
|
||||
# D30 wasm migration wave 1: wasm modules are reproducibly-built compiled
|
||||
# artifacts (build_paired.sh: cargo → tui/wasm/modules/), never committed —
|
||||
# same no-committed-binaries law as the Go/Rust binaries. Source of truth
|
||||
# is src/rust/token_counting; manifest.json carries the paired build id.
|
||||
tui/wasm/modules/
|
||||
|
||||
# store segments are canonical; this file is the derived LOCAL cache,
|
||||
# rebuilt from segments (the 102MB re-land class) — never committable.
|
||||
# (Single entry: the pre-hygiene duplicate removed.)
|
||||
store/scaffold/current.json
|
||||
tui/rune-tui
|
||||
|
||||
# 83MB legacy migration snapshot — purged from history 2026-10-21 (wave-ghost-purge);
|
||||
# the on-disk working copy stays, but it is never committable.
|
||||
store/scaffold/legacy-current-migration-snapshot.json
|
||||
state/
|
||||
@@ -17,26 +17,6 @@ Only use `git commit --no-verify` in a genuine emergency: it bypasses the hooks
|
||||
- Plans are markdown, one per build (in ./plans)
|
||||
- Top-level docs are markdown or yaml references (in ./docs/*)
|
||||
- The known giant stays visible — file-size debt is tracked, not silent (in ./src/rune/*)
|
||||
- The umbrella impact map is complete and truthful — every module mapped, no phantom entries (in ./config/umbrellas.json)
|
||||
- Umbrella canaries exist in the repo — the propagation gate cannot reference ghosts (in ./config/umbrellas.json)
|
||||
- The umbrella dependency graph stays a DAG — reflexive updates cannot cycle (in ./config/umbrellas.json)
|
||||
- The bug-fix discharge contract is a governed doc — it exists, is markdown, and changes are deliberate (in ./docs/bugfix-contract.md)
|
||||
- The discharge checker ships with the repo — red-first enforcement is structural (in ./scripts)
|
||||
- Discharge checker and its CI wiring change together
|
||||
- Every workflow is registered in the pipeline registry — new pipeline machinery ships with documentation (in ./.gitea/workflows/*.yml)
|
||||
- Every TUI surface is registered — no silent UI growth (in ./src/rune/*tui*.py)
|
||||
- New rune modules ship with tests or a canary — no ungoverned code surfaces (in ./src/rune/*.py)
|
||||
- Rust crate files stay under a line budget (in ./src/rust/*/src/*.rs)
|
||||
- Go modules stay under a line budget (in ./src/go/**/*.go)
|
||||
- No secrets in Rust or Go source files (in ./src/rust/*/src/*.rs, ./src/go/**/*.go)
|
||||
- PRDs are living documents — their generated state sections regenerate identically from governed sources
|
||||
- PRD staleness is a CI failure — docs rot is silent failure (in ./scripts/gen_prd_state.py)
|
||||
- The adversarial review procedure is binding — dispatch templates must reference it (in ./docs/adversarial-review.md)
|
||||
- The fixture pipeline ships with the repo — production failure capture is structural (in ./scripts)
|
||||
- Production failure fixtures are a governed surface — the registry and its README exist (in ./tests/fixtures/production)
|
||||
- Fixture payloads and the manifest travel together — tamper-evidence needs both
|
||||
- The fixture manifest and its generated regression tests travel together
|
||||
- The dichotomy is enforced at the commit boundary — every src/rune module lands in its measured side, dead oracles and wrong-language kernels fail with the correct implementation as the instruction (in ./src/rune)
|
||||
|
||||
### Behaviours
|
||||
|
||||
@@ -53,35 +33,3 @@ Need a check that genuinely doesn't exist? `henxels create-new-statement <name>`
|
||||
other repos, not tied to this one — contribute it upstream with `henxels contribute`.
|
||||
We're in the agentic era: send a ready-to-merge PR instead of opening an issue.
|
||||
<!-- henxels:end -->
|
||||
|
||||
## Bug fixes (binding)
|
||||
|
||||
Every bug found — by gates, CI, canaries, production, or humans — is fixed per
|
||||
`docs/bugfix-contract.md`: red test FIRST (reproduces the cause), root-cause
|
||||
fix, full pipeline re-entry (suite + ratchet + ruff + canaries + henxels;
|
||||
gates for anything beyond a mechanical minor), regression tests frozen forever,
|
||||
discharge recorded finding → test → commit. Symptom patches are rejected at
|
||||
review. To escape: edit `henxels.yaml` or this file — visibly.
|
||||
|
||||
## Issue organisation standard (binding, 2026-09-19)
|
||||
|
||||
One open note per idea. Issues use the template spine (kind, subsystem,
|
||||
version intent) with a plain-English heading that restates the subject —
|
||||
no meta-labels. Before minting a standalone issue, check whether the work
|
||||
belongs inside an existing parent:
|
||||
|
||||
- **Parked ideas under a blocked/future epic** are filed as checklist items
|
||||
in the parent's "Absorbed children" section, NOT as standalone issues.
|
||||
Absorption is reversible — re-mint standalone when the idea becomes live.
|
||||
- **Live, dependency-tracked, or starting work** gets a standalone issue.
|
||||
- **Closures state the reason as data**: `closed_because` in the spine
|
||||
(completed / superseded-by-#N / absorbed-into-#N / wont-do / duplicate),
|
||||
plus a standard closing comment (plain summary first).
|
||||
- The reconciliation checker (`scripts/gitea_reconciliation_check.py`) must
|
||||
stay green after any issue state change; the artifact is updated in the
|
||||
same change that moves issues.
|
||||
|
||||
## Async continuation final flow proofs (R13/M9) — 2026-09-19
|
||||
|
||||
PR-flow live proof marker: harmless documentation addition exercising the
|
||||
canonical single-continuation path on the PC-local forge. No code change.
|
||||
-7006
File diff suppressed because it is too large.
Load diff
@@ -1,155 +0,0 @@
|
||||
# Morning test list — 2026-10-16
|
||||
|
||||
Things you can test by hand, in order, with your own eyes and ears.
|
||||
This list gets filled in further as overnight work lands.
|
||||
|
||||
## 1. System audio (landed yesterday afternoon)
|
||||
|
||||
- Play anything (browser video, music).
|
||||
- Expect: sound through your monitors as normal.
|
||||
- If you reboot: log in, wait ~15 seconds, play something again.
|
||||
Expect: sound still there (the self-healing system rebuilds the
|
||||
System_Audio path if the startup race ever eats it).
|
||||
|
||||
## 2. Game audio via Heroic (armed, waiting on you)
|
||||
|
||||
- Close Mafia if it's still open (the fix applies itself on close).
|
||||
- Relaunch it through Heroic.
|
||||
- Expect: sound from the menus onward.
|
||||
- If still silent: tell Rune — the next fix (umu runtime rollback) is
|
||||
already planned and takes minutes.
|
||||
|
||||
## 3. The one-command whole-system check (LANDED overnight)
|
||||
|
||||
- Run, from the repo root:
|
||||
`make check`
|
||||
(same thing, spelled out: `python scripts/unified_check.py`)
|
||||
- Takes several minutes — it sweeps everything: all 103 Rust crates, the
|
||||
whole Python suite, the Go side, the boundary conversation tests, the
|
||||
code-generation gate, and the no-binaries gate.
|
||||
- Expect the final verdict **AMBER**. Plain meaning of AMBER: "only the
|
||||
old, known failures remain — nothing new." That is the correct, honest
|
||||
green for this repo right now. RED means something new broke.
|
||||
- The known-old list it tolerates: ~120 old Python failures (recorded
|
||||
baseline, being paid down over time) + one known-sick Rust crate
|
||||
(prompt_sync — found by this very runner on its first ever sweep;
|
||||
repair already queued overnight).
|
||||
|
||||
## 4. The TUI first breath (foundation — LANDED overnight)
|
||||
|
||||
- Run, from the repo root:
|
||||
`cd tui && go run ./cmd/rune-tui`
|
||||
(The `./cmd/rune-tui` part matters — the module root itself has no
|
||||
main program.)
|
||||
- Expect: a small face showing **Rune**, a version placeholder
|
||||
(0.0.0-dev), a build line, and "q to quit". Press **q** (or ctrl+c):
|
||||
it exits cleanly (exit code 0).
|
||||
- The build line says `unset (unpaired dev build)` for `go run` — that
|
||||
is honest: `go run` skips the paired-build stamping step. A
|
||||
paired-stamped build (`bash scripts/build_paired.sh`, then run
|
||||
`verification/build-id/rune-tui`) shows the real build id instead.
|
||||
- Pre-verified overnight by machine (boot, render, q, clean exit 0),
|
||||
including through a fake terminal — your run is the human confirmation.
|
||||
- It is the seed, not the tree — it proves the whole chain works end to
|
||||
end (module + vendored dependencies + stamping + tests).
|
||||
|
||||
## 5. The behavior map (LANDED overnight)
|
||||
|
||||
- What it is: the single action-model both future visual designs must
|
||||
satisfy — 144 states, 20 actions, frozen golden paths. It is the
|
||||
guarantee that every visual option behaves identically.
|
||||
- See it pass:
|
||||
`cd tui && go test ./semantic/ -v`
|
||||
- You will see the mutant tests (deliberate breakages that MUST fail the
|
||||
checker) and the key-parsing tests (the tricky alt+q case included).
|
||||
All green = the foundation of all three future looks is solid.
|
||||
|
||||
---
|
||||
|
||||
## What you do NOT need to test by hand
|
||||
|
||||
Everything below is already proven by born-failing tests before it
|
||||
landed (the five repairs, the boundary machinery, the sandbox). The
|
||||
test list above is only for things that need human eyes or ears.
|
||||
|
||||
## Overnight judgment calls made while you slept
|
||||
|
||||
(Filled in as they happen — each one gets a line here so you can
|
||||
overrule anything in the morning.)
|
||||
|
||||
1. **Substrate's first sweep found a sick Rust crate** (`prompt_sync` —
|
||||
an arithmetic overflow that crashes three of its tests; pre-existing,
|
||||
never swept before, NOT caused by tonight's work). Left unfixed by
|
||||
the substrate worker (correctly — separate concern), repair queued
|
||||
overnight.
|
||||
2. **The repo's "spine" snapshot was stale** relative to yesterday's
|
||||
landed fixes. The suite refreshes it when it runs; I left the
|
||||
refreshed version in place because restoring the stale one would arm
|
||||
a false alarm. Flagged for your call if you want spine handling
|
||||
changed.
|
||||
3. **The old failure count is 120, not 116** (the research's number was
|
||||
approximate). The runner records the honest baseline and compares
|
||||
against it by content, not by count — the edges of the old set flicker
|
||||
by one or two between runs, so exact counts lie.
|
||||
4. **The sick crate was repaired overnight** (prompt_sync — a Python-to-Rust
|
||||
port where Python's forgiving negative numbers met Rust's hard floor;
|
||||
two underflow sites fixed with exact-Python-semantics guards; all its
|
||||
tests including the frozen oracle battery now pass). The one-command
|
||||
check in section 3 still says AMBER — that remains correct.
|
||||
5. **One crate is order-flaky** (graph_projection): passes 12/12 when run
|
||||
alone, occasionally trips when the whole system sweeps in one order.
|
||||
Known flake class, tolerated in the baseline so it cannot cause false
|
||||
alarms. Real fix (test hygiene) queued for a later wave.
|
||||
6. **Bubbles v2.2.1 is pinned through a tiny internal package, not used
|
||||
yet** (tui/internal/toolkit/pins.go — one blank import). `go mod tidy`
|
||||
deletes any dependency nothing imports, and the spec pins all three
|
||||
toolkit pieces (task 5.3b), so the pin needs a real import to survive
|
||||
tidy + vendoring. It says exactly that in its comment; components
|
||||
arrive with later waves.
|
||||
7. **tui/go.mod grew its dependency list through the GENERATOR, not by
|
||||
hand** — go.mod is generated territory (the codegen gate byte-diffs
|
||||
it), so the pins went into the gen_boundary_types.py template and the
|
||||
file must stay byte-identical to what `go mod tidy` produces. The
|
||||
runner needed NO changes: Go spots tui/vendor/ automatically and
|
||||
builds offline from it.
|
||||
|
||||
6. **Wave D (lanes groundwork) is building overnight** — the two-lane
|
||||
skeleton (Chat left, Task right, focus switching) rendered over the
|
||||
behavior map above. Judgment entry when it lands.
|
||||
|
||||
7. **Wave D (two-lane skeleton) LANDED overnight** — the face now shows
|
||||
Chat left + Task right, focused pane wider, tab/1/2 to switch focus,
|
||||
q to quit. Panes say "not yet wired" — honest placeholders; the real
|
||||
content arrives in later waves.
|
||||
Run it: `cd tui && go run ./cmd/rune-tui`
|
||||
8. **Judgment call: Gitea restarted itself mid-night** (03:44) — the Wave D
|
||||
push failed once, then landed clean on retry. If the machine rebooted
|
||||
overnight, your morning `git push` context is unaffected; all commits
|
||||
are on the server.
|
||||
|
||||
9. **Wave E (activity rail) LANDED overnight** — beneath the lanes now
|
||||
sits a RAIL line (which lane is focused, status, newest event with its
|
||||
source). Press `r` to open the full-width ACTIVITY RAIL box; `x` closes
|
||||
it back to exactly the lanes you had. `q` quits (inside the box, keys
|
||||
are swallowed until you close it — it is an inspection room, not a
|
||||
door). Entries already carry source + ancestry — the thread tree (your
|
||||
SA-D7) gets its data free.
|
||||
10. **Judgment call: Wave E needed two watchdog nudges** — the worker
|
||||
stalled twice with nothing on disk, moved both times right after the
|
||||
ping. Pattern of the night: long writes, not stuck workers. The rule
|
||||
(probe the worktree, nudge, escalate to tighter re-spawn) worked
|
||||
without ever firing the trigger.
|
||||
|
||||
11. **Heroic: already up to date** (2.22.4-1, newest everywhere — nothing
|
||||
was changed). Your launcher is healthy; 40 files, none missing.
|
||||
12. **Game audio, corrected picture:** the fix already applied itself
|
||||
yesterday at 15:12 (the watcher fired on game close; registry patched
|
||||
to Audio=pulse; backup saved). BUT two things changed the equation:
|
||||
(a) the runtime on disk is Oct 7, not Oct 13 — so the "Oct 13 broke
|
||||
it" theory is wrong; (b) the next game launch may auto-download a
|
||||
newer runtime, which could change things again. So: relaunch Mafia
|
||||
through Heroic, listen from the menus. If sound is there — done, tell
|
||||
Rune and the umu update hold lifts. If silent — say so immediately;
|
||||
the rollback plan pivots to whatever runtime just downloaded.
|
||||
(Held deliberately: umu-launcher 1.4.4-1 NOT installed until your
|
||||
audio verdict.)
|
||||
@@ -1,18 +0,0 @@
|
||||
# phase 0C task 0.13 — ONE command for the whole test world (design D26:
|
||||
# no two test worlds; partial green is impossible). This is the entry the
|
||||
# landing gate (scripts/check_landing.py) demands evidence from.
|
||||
.PHONY: check boundary-regen paired-build install-launcher
|
||||
|
||||
check:
|
||||
python scripts/unified_check.py
|
||||
|
||||
boundary-regen:
|
||||
python scripts/gen_boundary_types.py
|
||||
|
||||
paired-build:
|
||||
bash scripts/build_paired.sh
|
||||
|
||||
# wave-rune-launcher: the standard `rune` command boots the Go TUI face;
|
||||
# the launcher is version-controlled and installed by this target.
|
||||
install-launcher:
|
||||
bash scripts/install_rune_launcher.sh
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune TUI — Product Requirements Document (PRD)
|
||||
|
||||
**Last Updated**: 2026-09-07
|
||||
**Last Updated**: 2026-08-30
|
||||
**Version**: v0.45.1
|
||||
**Status**: Active Development
|
||||
|
||||
@@ -12,23 +12,6 @@ Rune is a stateful coding assistant with a Textual-based TUI. It connects to loc
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN GENERATED: state — edits here are overwritten by scripts/gen_prd_state.py -->
|
||||
**Version**: v0.54.0 · **HEAD**: 3e0b748 · **Regenerated**: 2026-09-07
|
||||
|
||||
### Governed TUI surfaces (docs/tui-registry.md)
|
||||
|
||||
| `rune_tui.py` | The main Textual application — chat pane, work pane, status bar (version label + refresh), footer, dialogs | The known giant (file-size debt tracked in henxels); composition root for all UI |
|
||||
| `tui_validator.py` | Import-allowlist validation for TUI code | Drift defect tracked in #139 (allowlist vs `__future__`/relative imports) |
|
||||
|
||||
`rune_tui.py` is 4075 lines — the known giant, file-size debt tracked in henxels; decomposition target #125.
|
||||
|
||||
### TUI-adjacent subsystems (impact map)
|
||||
|
||||
- orchestration umbrella: 38 modules (operator, TUI presentation, task lifecycle)
|
||||
- perception umbrella: 9 modules (context assembly, capsule)
|
||||
- capability umbrella: 7 modules (tool authority)
|
||||
<!-- END GENERATED: state -->
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Stack
|
||||
|
||||
@@ -1,123 +1,219 @@
|
||||
# Rune
|
||||
# Rune — Stateful Coding Assistant with Elastic Cognition
|
||||
|
||||
**Current release: v0.358.1**
|
||||
Rune is a stateful coding assistant that dynamically spends the minimum cognition needed for reliable results.
|
||||
|
||||
<!-- highlights:start -->
|
||||
## Highlights — from the latest release (v0.358.1)
|
||||
|
||||
- Plain-English jargon dictionary covers the v0.329 vocabulary, plus catch-all sentence simplification pass.
|
||||
|
||||
Behind-the-scenes upkeep: 1 commit(s) of docs, tests and tooling this release.
|
||||
<!-- highlights:end -->
|
||||
|
||||
---
|
||||
|
||||
## What Rune is
|
||||
|
||||
Rune is a personal AI agent that lives on Nick's computer and talks like a person. Ask her something in ordinary language and she answers, does the work, and remembers.
|
||||
|
||||
The language model inside her is deliberately treated as a replaceable part — the "thinking engine" she borrows to reason. Everything that actually makes her Rune is kept by Rune itself, independently of any one model provider:
|
||||
|
||||
- her memory of conversations and what happened;
|
||||
- the tasks she is working on and how far they got;
|
||||
- the skills and techniques she has learned;
|
||||
- her own judgement, built up from real outcomes.
|
||||
|
||||
If the underlying model is swapped tomorrow, she is still Rune.
|
||||
|
||||
---
|
||||
|
||||
## How you talk to her
|
||||
|
||||
**Chat** is where you simply talk. It works like a continuous conversation, not a series of separate questions: she keeps the thread, follows corrections, and answers follow-ups in context. You can keep chatting while she is busy — she works in the background and speaks when she has something to say.
|
||||
|
||||
**Tasks** is the operations board. When she is doing real work, the plan, the current step, the commands she ran, and what actually happened all show up here — recorded from what really occurred, not from what she claims. You watch the work happen without interrupting it, and finished work lands here rather than cluttering the conversation.
|
||||
|
||||
---
|
||||
|
||||
## What she can do
|
||||
|
||||
- **Act on the PC in task mode.** Give her real work and she runs commands, reads and edits files, runs the tests, looks at the result, and tries again if it failed — checking real evidence before she says something is done. She does not accept her own word as proof.
|
||||
- **Remember.** Conversations, task outcomes, and records persist across restarts in her own store, so a new session picks up where the last one ended.
|
||||
- **Learn skills.** Successful techniques are kept as reusable knowledge — the skills engine runs on the Go side and its live state shows in her interface.
|
||||
- **Know her limits.** For anything destructive or irreversible she asks first, and she separates what she is *capable of* from what she is *authorised* to do.
|
||||
|
||||
---
|
||||
|
||||
## How to run
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Launch Rune
|
||||
rune
|
||||
|
||||
# Or from the source directory
|
||||
cd /mnt/workdrive/rune/src
|
||||
python3 -m rune
|
||||
```
|
||||
|
||||
The `rune` command boots the deployed chat window (a text interface in the terminal). To build the current state from source:
|
||||
## Architecture
|
||||
|
||||
```bash
|
||||
bash scripts/build_paired.sh
|
||||
### Rune v1 authority source
|
||||
|
||||
Rune v1 admits runtime changes through the machine-readable
|
||||
`config/v1_authority_map.json` and the human decision record at
|
||||
`docs/v1/AUTHORITY_MAP.md`. These distinguish current execution authority from
|
||||
the intended v1 owner and classify every parallel path as retained, adapter,
|
||||
archive candidate, or dormant.
|
||||
|
||||
No subsystem may inject model context outside the future authoritative
|
||||
ContextBroker. The map is an admission and migration plan; it does not claim
|
||||
those migrations are already implemented.
|
||||
|
||||
Rune's ordinary v0.50.0 turn currently runs through `RuneApp` → `RuneOperator` →
|
||||
`LocalToolRuntime`. The repository also contains the following layered Elastic
|
||||
Cognition prototypes. They are migration candidates, not the authoritative live
|
||||
path; ownership is recorded in `docs/v1/AUTHORITY_MAP.md`.
|
||||
|
||||
1. **Response Adapter** — Normalizes provider output (strips reasoning leakage)
|
||||
2. **Event Envelope** — Normalizes all inputs (user messages, task events, tool results)
|
||||
3. **Salience Gate** — Decides if cognition beyond deterministic handling is needed
|
||||
4. **Cognitive Budgeter** — Decides how much cognition the event deserves
|
||||
5. **Model Registry** — Configurable resource ladder (T0-T4)
|
||||
6. **Tiered Executor** — Routes to appropriate tier
|
||||
7. **Verification Layer** — Objective checks (exit codes, tests, schemas)
|
||||
8. **Progressive Escalation** — Only when evidence warrants it
|
||||
9. **Attention Capsule** — Compact working set (300-600 tokens)
|
||||
10. **Outcome Ledger** — Performance tracking
|
||||
|
||||
### Elastic Cognition target/prototype
|
||||
|
||||
```
|
||||
Event
|
||||
-> EventEnvelope (normalize input)
|
||||
-> SalienceGate (does this deserve cognition?)
|
||||
-> CognitiveBudgeter (how much cognition?)
|
||||
-> TieredExecutor (cheapest competent process)
|
||||
-> VerificationLayer (objective checks)
|
||||
-> ProgressiveEscalation (only if needed)
|
||||
-> ResponseAdapter (strip reasoning leakage)
|
||||
-> RuneResponse (one clean response)
|
||||
```
|
||||
|
||||
---
|
||||
## Cognitive Resource Ladder
|
||||
|
||||
## How she's built
|
||||
```
|
||||
T0 — deterministic
|
||||
Python, parsers, retrieval, schemas, tests
|
||||
|
||||
One sentence: **a Go agent core does the thinking and acting; a Go "front door" is the only door to it; the chat window in your terminal talks only to that door.**
|
||||
T1 — cheap local
|
||||
qwen2.5:3b or measured equivalent
|
||||
|
||||
```text
|
||||
you ⇄ chat window (terminal)
|
||||
⇄ front door (127.0.0.1:8377)
|
||||
⇄ agent core (127.0.0.1:8376)
|
||||
├── conversation & tasks
|
||||
├── memory store
|
||||
└── tool loop: reason → act → check → act again → verify → answer
|
||||
T2 — medium local
|
||||
qwen3:14b and/or qwen2.5-coder:14b
|
||||
|
||||
T3 — strong local
|
||||
Qwen3.6 27B Fable Fusion through Ollama
|
||||
|
||||
T4 — cloud
|
||||
MiMo V2.5 Pro
|
||||
```
|
||||
|
||||
- The **agent core** is the runtime: it holds the conversation, the task ledger, the memory, and the loop that lets her use tools and keep working until the job is genuinely done.
|
||||
- The **front door** is a small Go web interface that stands in front of the core. The chat window never touches the core directly — every request goes through the door, which keeps one honest, inspectable path for everything.
|
||||
- The **chat window** shows the two panes above (chat and tasks) plus a live activity strip, and it can run while the front door is down by carrying its own copy of the door as a child process.
|
||||
## Commands
|
||||
|
||||
The older Python runtime is being retired: new capability lands in Go, and no new Python is added.
|
||||
```
|
||||
/project <name> — set subject header
|
||||
/mode <mode> — switch mode (task/chat/build/research/plan)
|
||||
/model <model> — switch model (local/cloud/auto/<name>)
|
||||
/theme <name> — switch theme
|
||||
/persona — show defining principles
|
||||
/abstraction — show abstraction pipeline stats
|
||||
/claims — show claim audit report
|
||||
/neurons — show warm neuron stats
|
||||
/graph — show graph memory stats
|
||||
/pressure — show context pressure
|
||||
/events — show event bus stats
|
||||
/plugins — show microkernel status
|
||||
/env — show environment info
|
||||
/persona-gate — show persona gate decision
|
||||
/hooks — show hook engine status
|
||||
/growth — show growth delta
|
||||
/improve — show self-improvement suggestions
|
||||
/voice — show voice blend
|
||||
/voice-sync — show voice sync status
|
||||
/drift — show drift ledger status
|
||||
/sleep — show sleep-time status
|
||||
/help — show available commands
|
||||
```
|
||||
|
||||
---
|
||||
## Native Modules
|
||||
|
||||
## What's real vs what's planned
|
||||
27 modules import natively with zero subprocess overhead:
|
||||
|
||||
Rune is under active development, and the project holds itself to a strict rule: a thing counts as real only when ordinary execution actually uses it. Code that exists but isn't wired into daily behaviour is labelled as such, not counted.
|
||||
- warm_router, capability_broker, memory_store, witness, cache
|
||||
- model_connect, task_persist, pins, prompt_sync, abstraction
|
||||
- claim_audit, memory_env, warm_neuron_store, graph_memory
|
||||
- context_pressure, event_bus, microkernel, persona_gate
|
||||
- hook_engine, growth_delta, self_improve, recall_router
|
||||
- voicebox, voice_hybrid, voice_sync, sleep_time, drift_ledger
|
||||
|
||||
The authoritative map of what currently runs — and what is still a prototype or planned — is:
|
||||
## Configuration
|
||||
|
||||
- `docs/v1/AUTHORITY_MAP.md`
|
||||
- `config/v1_authority_map.json`
|
||||
Configuration is stored in `/mnt/workdrive/rune/config/rune.json`.
|
||||
|
||||
---
|
||||
## Governed builds
|
||||
|
||||
## Documentation
|
||||
Explicit **fix** and **build** requests use the canonical ledger/worktree →
|
||||
research → pin findings → feasibility → complexity → consult → plan → canaries
|
||||
→ consent → Foundation → Implementation → Integration → verification → scope
|
||||
guardian → documentation → commit/push → Gitea PR/CI → record pipeline. Each
|
||||
code change is linked to a Gitea issue, isolated branch, pull request, and
|
||||
protected required checks. Routine work defaults to local models and Gitea
|
||||
Actions; placeholder phase output is never accepted as evidence that a command,
|
||||
push, or check ran.
|
||||
|
||||
- `docs/v1/AUTHORITY_MAP.md` — what is live vs prototype vs planned
|
||||
- `docs/wiki/Home.md` — documentation index
|
||||
- `docs/wiki/Operations.md` — operational procedures
|
||||
- `docs/wiki/Backup-and-Restore.md` — Gitea backup and restore
|
||||
- `docs/openspec/` — experience and architecture specs
|
||||
## Local tools and interstitial objects
|
||||
|
||||
---
|
||||
Tool-capable Ollama models use native `/api/chat` tool calls. Other local
|
||||
models use a validated compatibility protocol. Read-only calls execute through
|
||||
the tool authority boundary; mutations pause for explicit approval and are
|
||||
revalidated before one-time execution.
|
||||
|
||||
The TUI renders thinking, tool calls, results, approvals, and assistant chat as
|
||||
separate typed objects. Tool objects appear before the assistant continuation,
|
||||
so chat remains interstitial rather than swallowing protocol data as prose.
|
||||
|
||||
## Memory
|
||||
|
||||
Memory is stored in `/mnt/workdrive/rune/memory/`.
|
||||
|
||||
## Development
|
||||
|
||||
Run the Go test batteries (with the race detector, the project's deep code testing):
|
||||
|
||||
```bash
|
||||
cd src/go/release-notes && go test ./... -race
|
||||
cd tui && go test ./... -race
|
||||
# Run canary to verify all imports
|
||||
cd /mnt/workdrive/rune/src
|
||||
python3 rune/canary_native.py
|
||||
|
||||
# Run tests
|
||||
python3 -m pytest tests/
|
||||
```
|
||||
|
||||
CI installs Python dependencies only from the committed, fully-pinned `requirements-ci.txt` (issue #150). After any dependency edit in `pyproject.toml`, regenerate and commit the lock:
|
||||
### Gitea repository state
|
||||
|
||||
Branch protection, the 11 managed labels, three milestones, and six wiki pages
|
||||
are declared in the [declarative repository state](config/gitea_repository_state.json).
|
||||
Wiki bodies are tracked as Markdown under the [wiki documentation](docs/wiki/Home.md).
|
||||
|
||||
```bash
|
||||
uv pip compile pyproject.toml -o requirements-ci.txt --extra dev --extra memory --python-version 3.11
|
||||
python3 scripts/check_dep_lock.py # must print "in sync" before committing
|
||||
# Read-only drift check (also the default when no mode is supplied)
|
||||
python3 shared/gitea_provision.py --check
|
||||
|
||||
# Create or update managed state, then verify convergence
|
||||
python3 shared/gitea_provision.py --apply
|
||||
```
|
||||
|
||||
---
|
||||
The provisioner loads credentials at runtime through `shared/gitea_auth.py`.
|
||||
Set `GITEA_TOKEN`, or use its mode-0600 token file. Check mode sends GET
|
||||
requests only. Apply mode never deletes labels, milestones, wiki pages, or
|
||||
other unmanaged repository objects.
|
||||
|
||||
### Gitea backup and restore verification
|
||||
|
||||
The daily backup action invokes the root-owned
|
||||
`/usr/local/sbin/rune-gitea-backup` on the Gitea host. It stops Gitea only for
|
||||
the official `gitea dump`, restores service and verifies health, validates all
|
||||
mandatory archive members, and publishes the archive, metadata, and SHA-256
|
||||
sidecar to the separate Foundry disk. The checksum is linked last as the
|
||||
publication commit marker, and complete backup sets have bounded retention.
|
||||
Before creating the backup directory or dumping, the helper requires
|
||||
`BACKUP_MOUNT` (default `/media/pi/Foundry`) to be the exact active mount,
|
||||
requires `BACKUP_DIR` to remain inside it lexically and after symlink
|
||||
resolution, and verifies that its backing device differs from `/`.
|
||||
Run it only through the installed, administrator-controlled entry point:
|
||||
|
||||
```bash
|
||||
sudo /usr/local/sbin/rune-gitea-backup
|
||||
```
|
||||
|
||||
Restore verification is an explicit, rootless, disposable drill. As a
|
||||
non-root user, acknowledge the guard and supply an archive with its adjacent
|
||||
`.sha256` sidecar:
|
||||
|
||||
```bash
|
||||
RUNE_GITEA_RESTORE_VERIFY=1 scripts/gitea-restore-verify.sh \
|
||||
/media/pi/Foundry/backups/gitea/gitea-dump-<timestamp>.tar.gz
|
||||
```
|
||||
|
||||
The verifier requires the audited digest-pinned Gitea image to exist locally;
|
||||
it uses `--pull=never`, no container network, no published ports, and a
|
||||
read-only container root. It extracts only into a fresh temporary directory
|
||||
and mounts only that disposable restore tree. Archived application data is
|
||||
restored into `/data/gitea` before the reconstructed SQLite database and
|
||||
generated isolated configuration overwrite archived versions. Every
|
||||
database-declared repository must exist at its canonical owner/name path and
|
||||
pass Git integrity checks; extra repositories such as wikis remain allowed.
|
||||
Production Gitea configuration, data, repositories, and service paths are
|
||||
therefore never accepted or mounted as a restore target. See the tracked
|
||||
[backup and restore runbook](docs/wiki/Backup-and-Restore.md) and
|
||||
[operations guide](docs/wiki/Operations.md).
|
||||
|
||||
## License
|
||||
|
||||
Private — Nick's personal AI agent.
|
||||
Private — Nick's personal AI assistant.
|
||||
@@ -1 +0,0 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://news.ycombinator.com/item?id=31473373","title":"I've yet to see how you can use SQLite for a multi-user multi-write app ...","description":"SQLite doesn't support concurrent writers. In WAL mode it can handle a single writer with concurrent readers. In \"vanilla\" mode, a writer requires exclusive ...","position":1},{"url":"https://sqlite.org/forum/info/b4e8b29ae409cd198652c6b7e70b53b702f269e67e1d2573d627feeba37bbf85","title":"Multiple Writers - SQLite User Forum","description":"# Multiple Writers\n### (1) By [pyarlath](https://sqlite.org/forum/timeline?ss=v&y=f&vfx&u=pyarlath) on 2025-01-03 08:35:02 [[link]](https://sqlite.org/forum/forumpost/7fb8bf206005d943) [[source]](https://sqlite.org/forum/forumpost/7fb8bf206005d943?raw)\n```\nI am new to this forum, so please be adjusting of me if i ask some noob level questions.\n\nWhy cant sqlite have multiple writers?\n\nMy theoretical understanding is if you open a file in append only mode by 2 process, each process can append data without overwriting each other, now i further understand the support for append is weak in other systems apart from linux or may even differ, but then whats stopping to write a module with shared memory or file or something equivalent to maintain a counter and allocate exclusive space for different writers such that they dont overwrite each other in same file a little like MVCC but instead of version of same data its writing different data from different writers..\n\nI understand there will be more issues internal to sqlite to manage other ds but i feel they can managed with some clever solution, the real limitation should be file cannot accept multiple writer, but they do in an append only mode..\n\nDoes this click some thought?\n```\n\n### (4) By [pyarlath](https://sqlite.org/forum/timeline?ss=v&y=f&vfx&u=pyarlath) on 2025-01-03 13:14:00 in reply to [2.1](https://sqlite.org/forum/forumpost/6fda11b073) [[link]](https://sqlite.org/forum/forumpost/dcdf209b088e7f93) [[source]](https://sqlite.org/forum/forumpost/dcdf209b088e7f93?raw)\n```\nI may be wrong but as a DB system it should be able to provide multiple writers to a same sqlite file is my recommendation here, if to writers wants to update same at same time fine we should be able to give them that provision with complete data integrity, what meaning that data makes to them is app driven logic..\n\nI am talking here about lockless system like MVCC where updates to same row can be made by just appending to the file at the same time without any locks. So i think there is no illusion here its a well known practice in DB world... i feel\n```\n\n### (5) By [pyarlath](https://sqlite.org/forum/timeline?ss=v&y=f&vfx&u=pyarlath) on 2025-01-03 13:16:22 in reply to [3](https://sqlite.org/forum/forumpost/c36f1f8f03) [[link]](https://sqlite.org/forum/forumpost/b85a03da3c87e6ea) [[source]](https://sqlite.org/forum/forumpost/b85a03da3c87e6ea?raw)\n```\nNot sure why you thought of performance is what i am after, i am not my request was just like multiple process can read the same DB file, i wanted to understand what is the challenge to provide multiple writer? specially when systems like append-only data structures exists..\n```\n\n### (12) By [pyarlath](https://sqlite.org/forum/timeline?ss=v&y=f&vfx&u=pyarlath) on 2025-01-03 19:07:57 in reply to [11](https://sqlite.org/forum/forumpost/b4e8b29ae4) [[link]](https://sqlite.org/forum/forumpost/85cde6083d4c6102) [[source]](https://sqlite.org/forum/forumpost/85cde6083d4c6102?raw)\n```\nI never said I understand sqlite, infact I am noob like i said in the original first post.. but am a big fan of the embedded database strategy..\n\nTransactions in append only file formats are durable and also have order the append order..\n\nI am not asking for a coordinator process but all we need is to do is coordinate of the underlying file size and keep appending ie adding to it as a shared counter..\n\nI completely understand file format compatibility is number 1 priority, but we can always achieve the said by having extra file just like Wal which will be home for multiple writers and possible move data to sqlite format on periodic time..\n\nAgain my interest is in suggesting this concept of achieving multiple writer but the community can tell me if this a feasible idea..\n```\n\n### (15) By [pyarlath](https://sqlite.org/forum/timeline?ss=v&y=f&vfx&u=pyarlath) on 2025-01-04 11:24:41 in reply to [13](https://sqlite.org/forum/forumpost/388af122d9) [[link]](https://sqlite.org/forum/forumpost/f39edda8db7d11a5) [[source]](https://sqlite.org/forum/forumpost/f39edda8db7d11a5?raw)\n```\nThat's called snapshot, Each transaction starts with a snapshot[simply say file size here] so when final commit happens transaction just compares the snapshot[file size] if no data is mutated it remains same else it's changes and we can throw serialization error for the transaction..\n\nAgain all this is common practice in PG and other ACID comLine truncated
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "c9419eadc0641ecaf40267e0543a5bdbfc548b377a4c2721f92662a641e32f10", "evidence": {"detail": "auth check observed: unreachable", "ended_at": 1792233218.9893527, "error_class": "UNREACHABLE", "input_digest": "dd027a0116f03542db66d808b90e5c9cd27a99ed290a046c4ea7f3a8a143d6f3", "output_digest": "c3c4c022a5a8e2e454665615a663a6984323d1c54df648ae4f49599a329a22fb", "started_at": 1792233218.9739888}, "kind": "provider", "observed_at": 1792233218.989386, "path": "provider:ollama", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233218-492cf12e", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "unmeasured"}
|
||||
{"digest": "ebd493b6544b8f215d2a714a998a7ac37add986d780fc1b809c0a270e8821e5d", "evidence": {"detail": "minimal call observed failing: Exception: Ollama connection failed: cannot import name '_clean_model_name' from 'model_connect' (/h", "ended_at": 1792233218.997182, "error_class": "UNREACHABLE", "input_digest": "a21f92f9ee3f2256bcbf27aa45981290981c6c1cf2655bfa50b0ba11ba20868c", "output_digest": "76b0ec3f3942928cb5232e64112f0fa5347f3a5193ca8a82a62d9e963e142a44", "started_at": 1792233218.9893873}, "kind": "provider", "observed_at": 1792233218.9972157, "path": "provider:ollama", "phase": "minimal_call", "prev_digest": "c9419eadc0641ecaf40267e0543a5bdbfc548b377a4c2721f92662a641e32f10", "receipt_id": "LP-1792233218-e0f20aa3", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "unmeasured"}
|
||||
{"digest": "422166d72546bf077ff51d2152acbd0e987e92d3d8b00748d5a14706e66944ac", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792233219.006819, "error_class": null, "injected_fault": "generate against a nonexistent model id", "input_digest": "6c0df4008620e6808cc874cd54c1c9b516dd5b50e4ea1bfe0ad118b00f085587", "output_digest": "71316104b00a83a2835a7d02b2b1fc42de3cf5bc50f4135785314698032e1622", "started_at": 1792233218.997217}, "kind": "provider", "observed_at": 1792233219.0068464, "path": "provider:ollama", "phase": "failure_injection", "prev_digest": "ebd493b6544b8f215d2a714a998a7ac37add986d780fc1b809c0a270e8821e5d", "receipt_id": "LP-1792233219-bb625093", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_fail"}
|
||||
{"digest": "0aecbb624f13ee7719fd6e0679655142ec39f252a134f2181579f85150793168", "evidence": {"detail": "recovery call observed failing: Exception: Ollama connection failed: cannot import name '_clean_model_name' from 'model_connect' (/h", "ended_at": 1792233219.0125165, "error_class": "UNREACHABLE", "input_digest": "da1b19503325152474269326e22e35a3541de5be0b52f6a4d15697562276ffcf", "output_digest": "39c9e709defa1679ba1d6ab7c60e3a4be6778264bef4fbe47cb1f1e3cbf8afb7", "started_at": 1792233219.0068486}, "kind": "provider", "observed_at": 1792233219.0125406, "path": "provider:ollama", "phase": "recovery", "prev_digest": "422166d72546bf077ff51d2152acbd0e987e92d3d8b00748d5a14706e66944ac", "receipt_id": "LP-1792233219-a4e0929a", "recovery_of": "LP-1792233219-bb625093", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "unmeasured"}
|
||||
{"digest": "eb332a8c25995da32fd4c4db62a5ba910f6b3469beadb2a1e3b8155c565bd1a5", "evidence": {"detail": "auth check observed: unreachable", "ended_at": 1792234272.8529687, "error_class": "UNREACHABLE", "input_digest": "dd027a0116f03542db66d808b90e5c9cd27a99ed290a046c4ea7f3a8a143d6f3", "output_digest": "c3c4c022a5a8e2e454665615a663a6984323d1c54df648ae4f49599a329a22fb", "started_at": 1792234272.8361378}, "kind": "provider", "observed_at": 1792234272.8529952, "path": "provider:ollama", "phase": "auth_check", "prev_digest": "0aecbb624f13ee7719fd6e0679655142ec39f252a134f2181579f85150793168", "receipt_id": "LP-1792234272-79069dd1", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "unmeasured"}
|
||||
{"digest": "147fed3aca1ac49ebc86d959fbf89e1bb2c7a93343a94605f7e51890f22bdc51", "evidence": {"detail": "minimal call observed failing: Exception: Ollama connection failed: cannot import name '_clean_model_name' from 'model_connect' (/h", "ended_at": 1792234272.860741, "error_class": "UNREACHABLE", "input_digest": "a21f92f9ee3f2256bcbf27aa45981290981c6c1cf2655bfa50b0ba11ba20868c", "output_digest": "76b0ec3f3942928cb5232e64112f0fa5347f3a5193ca8a82a62d9e963e142a44", "started_at": 1792234272.8529968}, "kind": "provider", "observed_at": 1792234272.8607702, "path": "provider:ollama", "phase": "minimal_call", "prev_digest": "eb332a8c25995da32fd4c4db62a5ba910f6b3469beadb2a1e3b8155c565bd1a5", "receipt_id": "LP-1792234272-9c39fdf0", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "unmeasured"}
|
||||
{"digest": "4ff45d265c98ee4dedb981ea927efc89342c3af39288f7343def206e0c6865c1", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792234272.8636794, "error_class": null, "injected_fault": "generate against a nonexistent model id", "input_digest": "6c0df4008620e6808cc874cd54c1c9b516dd5b50e4ea1bfe0ad118b00f085587", "output_digest": "71316104b00a83a2835a7d02b2b1fc42de3cf5bc50f4135785314698032e1622", "started_at": 1792234272.8607724}, "kind": "provider", "observed_at": 1792234272.8637164, "path": "provider:ollama", "phase": "failure_injection", "prev_digest": "147fed3aca1ac49ebc86d959fbf89e1bb2c7a93343a94605f7e51890f22bdc51", "receipt_id": "LP-1792234272-b76c579a", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_fail"}
|
||||
{"digest": "34793a3250f51c5b28de3405feda20e0cf6a56d456317dd85c1847966002da84", "evidence": {"detail": "recovery call observed failing: Exception: Ollama connection failed: cannot import name '_clean_model_name' from 'model_connect' (/h", "ended_at": 1792234272.8698554, "error_class": "UNREACHABLE", "input_digest": "da1b19503325152474269326e22e35a3541de5be0b52f6a4d15697562276ffcf", "output_digest": "39c9e709defa1679ba1d6ab7c60e3a4be6778264bef4fbe47cb1f1e3cbf8afb7", "started_at": 1792234272.863718}, "kind": "provider", "observed_at": 1792234272.8698883, "path": "provider:ollama", "phase": "recovery", "prev_digest": "4ff45d265c98ee4dedb981ea927efc89342c3af39288f7343def206e0c6865c1", "receipt_id": "LP-1792234272-f20f26d0", "recovery_of": "LP-1792234272-b76c579a", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "unmeasured"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "99025712c48b1307084db77b76fcb86dbda65a0e865ca76431edac2946c66663", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233218.9710536, "error_class": null, "input_digest": "effb3b5d5769d540ff9760238ef48bff2f7e776d98141067ecab0e89d02745fb", "output_digest": "a43dee11da21b5c47cd68d4a4fffd3f16b118377a18610880c4ddb8c437cff30", "started_at": 1792233218.9710517}, "kind": "tool", "observed_at": 1792233218.971072, "path": "tool:check_growth", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233218-fe7201f0", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "f1732dcec4346174f63e4261e58deac8811065f5ab8ceaea640b7e6fe380fd8e", "evidence": {"detail": "check growth status", "ended_at": 1792233218.971133, "error_class": null, "input_digest": "cb2cbff6f8bc71c36d0ee1d0a7959bc3ee7ff9aa933f06bfaa51c50076fc1bc9", "output_digest": "71a806b344d675b9a062b2e91bc5b3a9c8b56b64c83d7027f3b2001279de1c4f", "started_at": 1792233218.971073}, "kind": "tool", "observed_at": 1792233218.9711418, "path": "tool:check_growth", "phase": "minimal_call", "prev_digest": "99025712c48b1307084db77b76fcb86dbda65a0e865ca76431edac2946c66663", "receipt_id": "LP-1792233218-65eafd31", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "ed3856b44b6239e6446b77469a6c41527112ad465814cea2df4242a36bc283ff", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792233218.9712937, "error_class": null, "injected_fault": "check with a disabled growth gate", "input_digest": "01fa0327088c9965436c396d3f0bb4234e19811f3466d8911f5bdb5df3b5b9b5", "output_digest": "ed530432d25645b5a47a6c6c577e89a0c9202b1596c61a6cd54054ccb516e4d2", "started_at": 1792233218.9711423}, "kind": "tool", "observed_at": 1792233218.971308, "path": "tool:check_growth", "phase": "failure_injection", "prev_digest": "f1732dcec4346174f63e4261e58deac8811065f5ab8ceaea640b7e6fe380fd8e", "receipt_id": "LP-1792233218-44814065", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_fail"}
|
||||
{"digest": "32be97e9cfe91e0466147c5357cb13df1bff9ad5b66b68887d8d99c07ff0cf0a", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233218.9713871, "error_class": null, "input_digest": "739b3f090bd90a54c54f40b518d0e5e316bda4f14ab25342445139362394e167", "output_digest": "88a7f14ac8dc1521fede6db1e4c142fba565cd1a2827424ad93e2ad6c49fe411", "started_at": 1792233218.9713087}, "kind": "tool", "observed_at": 1792233218.971402, "path": "tool:check_growth", "phase": "recovery", "prev_digest": "ed3856b44b6239e6446b77469a6c41527112ad465814cea2df4242a36bc283ff", "receipt_id": "LP-1792233218-8aec1e2c", "recovery_of": "LP-1792233218-44814065", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "e45d6ec8bc98722470b1c6bd712dc290e7cdf20cb4585ee1e11487d50ee637dc", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234272.832866, "error_class": null, "input_digest": "effb3b5d5769d540ff9760238ef48bff2f7e776d98141067ecab0e89d02745fb", "output_digest": "a43dee11da21b5c47cd68d4a4fffd3f16b118377a18610880c4ddb8c437cff30", "started_at": 1792234272.8328648}, "kind": "tool", "observed_at": 1792234272.8328886, "path": "tool:check_growth", "phase": "auth_check", "prev_digest": "32be97e9cfe91e0466147c5357cb13df1bff9ad5b66b68887d8d99c07ff0cf0a", "receipt_id": "LP-1792234272-f2be3eb8", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "17e07c39e371aaeebfa83cb873ba65f3bed126894b4f052bf514a7f8baa1ce63", "evidence": {"detail": "check growth status", "ended_at": 1792234272.8329782, "error_class": null, "input_digest": "cb2cbff6f8bc71c36d0ee1d0a7959bc3ee7ff9aa933f06bfaa51c50076fc1bc9", "output_digest": "71a806b344d675b9a062b2e91bc5b3a9c8b56b64c83d7027f3b2001279de1c4f", "started_at": 1792234272.8328893}, "kind": "tool", "observed_at": 1792234272.8329926, "path": "tool:check_growth", "phase": "minimal_call", "prev_digest": "e45d6ec8bc98722470b1c6bd712dc290e7cdf20cb4585ee1e11487d50ee637dc", "receipt_id": "LP-1792234272-bc7a7334", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "0031728bfd365ccb090ec337a9c91953975352ba48b7d3576caef2e31c9a2417", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792234272.8330224, "error_class": null, "injected_fault": "check with a disabled growth gate", "input_digest": "01fa0327088c9965436c396d3f0bb4234e19811f3466d8911f5bdb5df3b5b9b5", "output_digest": "ed530432d25645b5a47a6c6c577e89a0c9202b1596c61a6cd54054ccb516e4d2", "started_at": 1792234272.8329933}, "kind": "tool", "observed_at": 1792234272.8330305, "path": "tool:check_growth", "phase": "failure_injection", "prev_digest": "17e07c39e371aaeebfa83cb873ba65f3bed126894b4f052bf514a7f8baa1ce63", "receipt_id": "LP-1792234272-3c00183b", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_fail"}
|
||||
{"digest": "74a1cb3f6452f59f43877f96d88b2a5243b78e3914443d8744f41d717eb6e568", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234272.8330655, "error_class": null, "input_digest": "739b3f090bd90a54c54f40b518d0e5e316bda4f14ab25342445139362394e167", "output_digest": "88a7f14ac8dc1521fede6db1e4c142fba565cd1a2827424ad93e2ad6c49fe411", "started_at": 1792234272.833031}, "kind": "tool", "observed_at": 1792234272.8330722, "path": "tool:check_growth", "phase": "recovery", "prev_digest": "0031728bfd365ccb090ec337a9c91953975352ba48b7d3576caef2e31c9a2417", "receipt_id": "LP-1792234272-a52f85ab", "recovery_of": "LP-1792234272-3c00183b", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "fb033f6370e01ed82a84f32ff0a54df6b77ce3765e79333f584b699620ae1085", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233218.972268, "error_class": null, "input_digest": "d6553a8acb42491e8735929dc6f91b553ff94c4e797554e46273f9acb9e30bd4", "output_digest": "c8cb4141c0ee8d220179c8acc4aae21aa0cff50326c88a13bdfdb2c9c5b2203f", "started_at": 1792233218.9722667}, "kind": "tool", "observed_at": 1792233218.9722826, "path": "tool:diagnose_behavior", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233218-dd7210f5", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "ab7b7039801bd75b33269958bd3d323425612b084b69024b9d98442734cf6920", "evidence": {"detail": "minimal call failed: Text is required", "ended_at": 1792233218.972331, "error_class": "TOOL_ERROR", "input_digest": "f9059e4bd866ddfc75fb5ccd9b89306e9cf4a65fdc82ab98c1a677c78c0d1685", "output_digest": "ead878aa4011839b17b4f7d7a75db98c83f4b3ed22140d36e4a16580cf81d671", "started_at": 1792233218.9722831}, "kind": "tool", "observed_at": 1792233218.9723408, "path": "tool:diagnose_behavior", "phase": "minimal_call", "prev_digest": "fb033f6370e01ed82a84f32ff0a54df6b77ce3765e79333f584b699620ae1085", "receipt_id": "LP-1792233218-134fe0fd", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_fail"}
|
||||
{"digest": "b858918d419ad179109da3c570351f8bf3f4c40bda784c8caffe2b1d379401f2", "evidence": {"detail": "injected fault observed failing and classified: Text is required", "ended_at": 1792233218.9723644, "error_class": "TOOL_ERROR", "injected_fault": "diagnose a nonexistent behavior", "input_digest": "8c4282e880c336c239d9ac91bd85a502b44ac6d404592d5aabfe17b8b834f849", "output_digest": "01973c0c768f536fed316470916982cef444dfc09453f9e52ae1b43fe997291c", "started_at": 1792233218.9723413}, "kind": "tool", "observed_at": 1792233218.9723866, "path": "tool:diagnose_behavior", "phase": "failure_injection", "prev_digest": "ab7b7039801bd75b33269958bd3d323425612b084b69024b9d98442734cf6920", "receipt_id": "LP-1792233218-7c223469", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_pass"}
|
||||
{"digest": "3b153deb3d2f4c4f3560095e4dff865f7f4c3974acee34124f5ea052a2aa717e", "evidence": {"detail": "recovery call failed: Text is required", "ended_at": 1792233218.972428, "error_class": "TOOL_ERROR", "input_digest": "d78b3459d5852c759f6ea1b0f50054bdea2fe9ba78aefc436739e01da74271ad", "output_digest": "42ed65c4c87cc759829b454747b386d6ed17fbc1d384a276f655671eb83e8c8b", "started_at": 1792233218.9723876}, "kind": "tool", "observed_at": 1792233218.9724379, "path": "tool:diagnose_behavior", "phase": "recovery", "prev_digest": "b858918d419ad179109da3c570351f8bf3f4c40bda784c8caffe2b1d379401f2", "receipt_id": "LP-1792233218-77b6723d", "recovery_of": "LP-1792233218-7c223469", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_fail"}
|
||||
{"digest": "c32d50db85d43518dda60996844776e0989cd908c5b9c5030c39ae9743a71946", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234272.8339787, "error_class": null, "input_digest": "d6553a8acb42491e8735929dc6f91b553ff94c4e797554e46273f9acb9e30bd4", "output_digest": "c8cb4141c0ee8d220179c8acc4aae21aa0cff50326c88a13bdfdb2c9c5b2203f", "started_at": 1792234272.8339775}, "kind": "tool", "observed_at": 1792234272.8339918, "path": "tool:diagnose_behavior", "phase": "auth_check", "prev_digest": "3b153deb3d2f4c4f3560095e4dff865f7f4c3974acee34124f5ea052a2aa717e", "receipt_id": "LP-1792234272-dfac6bb4", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "8756bc92697e9f5de328db31223bf7481f698e1ad7634e7f058d36d0dc83ddec", "evidence": {"detail": "minimal call failed: Text is required", "ended_at": 1792234272.8340354, "error_class": "TOOL_ERROR", "input_digest": "f9059e4bd866ddfc75fb5ccd9b89306e9cf4a65fdc82ab98c1a677c78c0d1685", "output_digest": "ead878aa4011839b17b4f7d7a75db98c83f4b3ed22140d36e4a16580cf81d671", "started_at": 1792234272.8339922}, "kind": "tool", "observed_at": 1792234272.8340425, "path": "tool:diagnose_behavior", "phase": "minimal_call", "prev_digest": "c32d50db85d43518dda60996844776e0989cd908c5b9c5030c39ae9743a71946", "receipt_id": "LP-1792234272-836d1042", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_fail"}
|
||||
{"digest": "9297fed4e6920a3f5762cfe2fb274a29c3c1fec4802393f2ff3f004d70b7719a", "evidence": {"detail": "injected fault observed failing and classified: Text is required", "ended_at": 1792234272.8340626, "error_class": "TOOL_ERROR", "injected_fault": "diagnose a nonexistent behavior", "input_digest": "8c4282e880c336c239d9ac91bd85a502b44ac6d404592d5aabfe17b8b834f849", "output_digest": "01973c0c768f536fed316470916982cef444dfc09453f9e52ae1b43fe997291c", "started_at": 1792234272.834043}, "kind": "tool", "observed_at": 1792234272.834069, "path": "tool:diagnose_behavior", "phase": "failure_injection", "prev_digest": "8756bc92697e9f5de328db31223bf7481f698e1ad7634e7f058d36d0dc83ddec", "receipt_id": "LP-1792234272-a9bb3919", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_pass"}
|
||||
{"digest": "8ad5aee780ccd936b60fc54fbaa1eb1eb53876090e95815abb2e4b6bf6587e2d", "evidence": {"detail": "recovery call failed: Text is required", "ended_at": 1792234272.8340976, "error_class": "TOOL_ERROR", "input_digest": "d78b3459d5852c759f6ea1b0f50054bdea2fe9ba78aefc436739e01da74271ad", "output_digest": "42ed65c4c87cc759829b454747b386d6ed17fbc1d384a276f655671eb83e8c8b", "started_at": 1792234272.8340695}, "kind": "tool", "observed_at": 1792234272.8341036, "path": "tool:diagnose_behavior", "phase": "recovery", "prev_digest": "9297fed4e6920a3f5762cfe2fb274a29c3c1fec4802393f2ff3f004d70b7719a", "receipt_id": "LP-1792234272-20e5a3a8", "recovery_of": "LP-1792234272-a9bb3919", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_fail"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "48cd2f229a78ab32c2f2070a5b47e77ef5e48db795c686649565883660ee6aa5", "evidence": {"detail": "approval gate refused to prepare (observed gate): Search string is required", "ended_at": 1792233217.9267302, "error_class": "GATE_REFUSED", "input_digest": "4eabb2182550c057b79bd5976ad49473e3084277549b998891a122dc3dc7f9db", "output_digest": "a45af98f4939d4fd8485c8d4fa7ab62a45dd55e86f998696b15442bcff3e0349", "started_at": 1792233217.9261644}, "kind": "tool", "observed_at": 1792233217.9267461, "path": "tool:edit_file", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233217-a68e62ef", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_fail"}
|
||||
{"digest": "a9953e071141252ad8c3b42486abcbb1cda5789b0d829cd982d7620faf59b56f", "evidence": {"detail": "minimal call failed: Search string is required", "ended_at": 1792233217.9273448, "error_class": "TOOL_ERROR", "input_digest": "6fb6fbf080b369dc2e686b32cddbc680267340fb9425e08afb7b9fa9e7e8b377", "output_digest": "f4a206c8cead20aeeb6bb307edb77a9f2d53db376d64bc1813ce588ad6053da8", "started_at": 1792233217.9267473}, "kind": "tool", "observed_at": 1792233217.9273586, "path": "tool:edit_file", "phase": "minimal_call", "prev_digest": "48cd2f229a78ab32c2f2070a5b47e77ef5e48db795c686649565883660ee6aa5", "receipt_id": "LP-1792233217-e0f92cc9", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_fail"}
|
||||
{"digest": "a45da58bf28d26c9ae75153acebf1a8c2cc9e26a3d513714bfa8697c8bdd9620", "evidence": {"detail": "injected fault observed failing and classified: Search string is required", "ended_at": 1792233217.9278736, "error_class": "TOOL_ERROR", "injected_fault": "edit a file with a missing anchor", "input_digest": "63ce71b463d1e77f081e233375c7c291c34c6f93853676b848ef68f96dc7feaa", "output_digest": "0237d8dd751db3440a726294e02d4fd8a84b00989c6119f0577f034870a3af5d", "started_at": 1792233217.92736}, "kind": "tool", "observed_at": 1792233217.9278855, "path": "tool:edit_file", "phase": "failure_injection", "prev_digest": "a9953e071141252ad8c3b42486abcbb1cda5789b0d829cd982d7620faf59b56f", "receipt_id": "LP-1792233217-3ad43eab", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_pass"}
|
||||
{"digest": "8e4d081578f175f2b37caedab52f719b56668ca311e7e8fb31e9842feee42a6c", "evidence": {"detail": "recovery call failed: Search string is required", "ended_at": 1792233217.928333, "error_class": "TOOL_ERROR", "input_digest": "a70358c28463e2f8e5eea5eb760f76644b1e20383fa4678ec4c318d190c60d11", "output_digest": "f89be8c3fddb6328acfd951900fb398c74c50f7f1708e527e688a18d016f9302", "started_at": 1792233217.9278872}, "kind": "tool", "observed_at": 1792233217.928343, "path": "tool:edit_file", "phase": "recovery", "prev_digest": "a45da58bf28d26c9ae75153acebf1a8c2cc9e26a3d513714bfa8697c8bdd9620", "receipt_id": "LP-1792233217-6ee28288", "recovery_of": "LP-1792233217-3ad43eab", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_fail"}
|
||||
{"digest": "dc5b600d7dfc0d4e381201f66c4f3e6dcb17a9d383f1dd0ea86d214f0150ab76", "evidence": {"detail": "approval gate observed: prepared + approved (single-use token, success=True)", "ended_at": 1792234271.5940187, "error_class": null, "input_digest": "4eabb2182550c057b79bd5976ad49473e3084277549b998891a122dc3dc7f9db", "output_digest": "3a5717ee3ccb85eafbb6c567c687594f4c39d771cf429b812c26155ae4177f65", "started_at": 1792234271.5907593}, "kind": "tool", "observed_at": 1792234271.5940323, "path": "tool:edit_file", "phase": "auth_check", "prev_digest": "8e4d081578f175f2b37caedab52f719b56668ca311e7e8fb31e9842feee42a6c", "receipt_id": "LP-1792234271-74f79322", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "2c779b57fe74d139b79ce64707d0290d1252b26f3ce158dce6eb3cd0a3c040f7", "evidence": {"detail": "minimal call failed: Search string not found in edit_file.txt", "ended_at": 1792234271.5949092, "error_class": "TOOL_ERROR", "input_digest": "6fb6fbf080b369dc2e686b32cddbc680267340fb9425e08afb7b9fa9e7e8b377", "output_digest": "20ed3750d32c8a881aea08cb33fb52bba8fd61fcf969c2022b721bbd8e9ba42a", "started_at": 1792234271.5940335}, "kind": "tool", "observed_at": 1792234271.594928, "path": "tool:edit_file", "phase": "minimal_call", "prev_digest": "dc5b600d7dfc0d4e381201f66c4f3e6dcb17a9d383f1dd0ea86d214f0150ab76", "receipt_id": "LP-1792234271-b30479ab", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_fail"}
|
||||
{"digest": "5d7fd346bcbb3adbc3f3952c5f592866541d5afe2bfdeb01ee98cff7c669085c", "evidence": {"detail": "injected fault observed failing and classified: Edit failed: [Errno 2] No such file or directory: '/mnt/workdrive/rune/store/live_proof/sandbox/__nonexistent_live_probe", "ended_at": 1792234271.5957453, "error_class": "ENOENT", "injected_fault": "edit a file with a missing anchor", "input_digest": "63ce71b463d1e77f081e233375c7c291c34c6f93853676b848ef68f96dc7feaa", "output_digest": "364eb58fdab4217299d3a7f3fd590815a1964f86b74f7c2fa0d38032a62150c0", "started_at": 1792234271.594929}, "kind": "tool", "observed_at": 1792234271.5957549, "path": "tool:edit_file", "phase": "failure_injection", "prev_digest": "2c779b57fe74d139b79ce64707d0290d1252b26f3ce158dce6eb3cd0a3c040f7", "receipt_id": "LP-1792234271-65f7fd20", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_pass"}
|
||||
{"digest": "d5a0fbc69149bae42bf8d556920c9052247db4d55d5c892c55c72fa4b3b9cae9", "evidence": {"detail": "recovery call failed: Search string not found in edit_file.txt", "ended_at": 1792234271.5965545, "error_class": "TOOL_ERROR", "input_digest": "a70358c28463e2f8e5eea5eb760f76644b1e20383fa4678ec4c318d190c60d11", "output_digest": "d9bf9f6f2c4f64727973bc0f636af218d074075cd1926e0bc501813278d63992", "started_at": 1792234271.595756}, "kind": "tool", "observed_at": 1792234271.5965624, "path": "tool:edit_file", "phase": "recovery", "prev_digest": "5d7fd346bcbb3adbc3f3952c5f592866541d5afe2bfdeb01ee98cff7c669085c", "receipt_id": "LP-1792234271-28bf8c0f", "recovery_of": "LP-1792234271-65f7fd20", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_fail"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "de031143b88a4d31724982cb7bc0a04d1b9953b41b4e86d282ea21849ad0e500", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233218.9731452, "error_class": null, "input_digest": "0adb83aa939f7216e7bf9ba290ca9e9a64c6e8410bbf031307b944b3fd61062f", "output_digest": "35e723bbc9593fdbb9b7b37505f216ba25bc48db956b46db0c40720f6e9c55c2", "started_at": 1792233218.973144}, "kind": "tool", "observed_at": 1792233218.9731574, "path": "tool:find_capability", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233218-ce2187d2", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "9f677dc17c92915a62e15d7f750400195b31c341555f78979c1302c88ffdfc5e", "evidence": {"detail": "minimal call failed: Query is required \u2014 describe the capability you need", "ended_at": 1792233218.9731963, "error_class": "TOOL_ERROR", "input_digest": "3b47e2eacf2a6aa514330dfcd9bee3f7cfc83bae4c0930f657f00044030ae69b", "output_digest": "62565e29fa6c5e71b931cbfd6c1684af3a471c679da69334046585eefc2fee88", "started_at": 1792233218.973158}, "kind": "tool", "observed_at": 1792233218.9732049, "path": "tool:find_capability", "phase": "minimal_call", "prev_digest": "de031143b88a4d31724982cb7bc0a04d1b9953b41b4e86d282ea21849ad0e500", "receipt_id": "LP-1792233218-3566a907", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_fail"}
|
||||
{"digest": "38176a2beaf5d78cc874cbfac22ac7845b000efdb13f2a284b053d808f0a7da6", "evidence": {"detail": "injected fault observed failing and classified: Query is required \u2014 describe the capability you need", "ended_at": 1792233218.9732258, "error_class": "TOOL_ERROR", "injected_fault": "find with an empty description", "input_digest": "0ac9aad40765fe88299b7b07e5013a81a1f2a895b8afb4dce2c3cfac64d7dd26", "output_digest": "28352af01bb36df885d64cb1b6bc26d4a61e8077629f823fb67340c0d423d174", "started_at": 1792233218.9732053}, "kind": "tool", "observed_at": 1792233218.9732332, "path": "tool:find_capability", "phase": "failure_injection", "prev_digest": "9f677dc17c92915a62e15d7f750400195b31c341555f78979c1302c88ffdfc5e", "receipt_id": "LP-1792233218-712a2227", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_pass"}
|
||||
{"digest": "50854fcc958dbce5821e72e55b730b0343f44590be10930a3b12bc727881d707", "evidence": {"detail": "recovery call failed: Query is required \u2014 describe the capability you need", "ended_at": 1792233218.9732618, "error_class": "TOOL_ERROR", "input_digest": "d151d7e8dd968e38c8d85bef653e87199364bc1414b7c97b8e1fc59a601c9a59", "output_digest": "d8491c426e55ca12008180f3da157210d2f7fb207ac428a1b90ecb4de1e924ef", "started_at": 1792233218.973234}, "kind": "tool", "observed_at": 1792233218.9732685, "path": "tool:find_capability", "phase": "recovery", "prev_digest": "38176a2beaf5d78cc874cbfac22ac7845b000efdb13f2a284b053d808f0a7da6", "receipt_id": "LP-1792233218-9a17a48f", "recovery_of": "LP-1792233218-712a2227", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_fail"}
|
||||
{"digest": "1f2cd83e04e5afda5afd551a7f84165cf4203d994ea439dcdbb2c416cdce4067", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234272.8349164, "error_class": null, "input_digest": "0adb83aa939f7216e7bf9ba290ca9e9a64c6e8410bbf031307b944b3fd61062f", "output_digest": "35e723bbc9593fdbb9b7b37505f216ba25bc48db956b46db0c40720f6e9c55c2", "started_at": 1792234272.8349156}, "kind": "tool", "observed_at": 1792234272.83493, "path": "tool:find_capability", "phase": "auth_check", "prev_digest": "50854fcc958dbce5821e72e55b730b0343f44590be10930a3b12bc727881d707", "receipt_id": "LP-1792234272-b25ebea1", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "6f8eb8865b3b3617dbe09a42bd481b06ef93e5bedab18950082231388f66cc14", "evidence": {"detail": "minimal call failed: Query is required \u2014 describe the capability you need", "ended_at": 1792234272.8349726, "error_class": "TOOL_ERROR", "input_digest": "3b47e2eacf2a6aa514330dfcd9bee3f7cfc83bae4c0930f657f00044030ae69b", "output_digest": "62565e29fa6c5e71b931cbfd6c1684af3a471c679da69334046585eefc2fee88", "started_at": 1792234272.8349307}, "kind": "tool", "observed_at": 1792234272.834981, "path": "tool:find_capability", "phase": "minimal_call", "prev_digest": "1f2cd83e04e5afda5afd551a7f84165cf4203d994ea439dcdbb2c416cdce4067", "receipt_id": "LP-1792234272-c3442c1f", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_fail"}
|
||||
{"digest": "08b43ea4362c8d56c5a006187c8c91e7da64c39056798f338505d1501606c7b3", "evidence": {"detail": "injected fault observed failing and classified: Query is required \u2014 describe the capability you need", "ended_at": 1792234272.8350027, "error_class": "TOOL_ERROR", "injected_fault": "find with an empty description", "input_digest": "0ac9aad40765fe88299b7b07e5013a81a1f2a895b8afb4dce2c3cfac64d7dd26", "output_digest": "28352af01bb36df885d64cb1b6bc26d4a61e8077629f823fb67340c0d423d174", "started_at": 1792234272.8349812}, "kind": "tool", "observed_at": 1792234272.8350093, "path": "tool:find_capability", "phase": "failure_injection", "prev_digest": "6f8eb8865b3b3617dbe09a42bd481b06ef93e5bedab18950082231388f66cc14", "receipt_id": "LP-1792234272-79fe0ee7", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_pass"}
|
||||
{"digest": "ab14d5cddb37f55e0c9adbcf00f51faf439dd10e54c6b26d2821180dd7b6606a", "evidence": {"detail": "recovery call failed: Query is required \u2014 describe the capability you need", "ended_at": 1792234272.8350449, "error_class": "TOOL_ERROR", "input_digest": "d151d7e8dd968e38c8d85bef653e87199364bc1414b7c97b8e1fc59a601c9a59", "output_digest": "d8491c426e55ca12008180f3da157210d2f7fb207ac428a1b90ecb4de1e924ef", "started_at": 1792234272.8350098}, "kind": "tool", "observed_at": 1792234272.8350532, "path": "tool:find_capability", "phase": "recovery", "prev_digest": "08b43ea4362c8d56c5a006187c8c91e7da64c39056798f338505d1501606c7b3", "receipt_id": "LP-1792234272-6766c0b0", "recovery_of": "LP-1792234272-79fe0ee7", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_fail"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "4339eac29575b6f17356e0dcd605bef2e8ae659e072cbb81416e92b88698f85c", "evidence": {"detail": "approval gate observed: prepared + approved (single-use token, success=False)", "ended_at": 1792233217.9835057, "error_class": null, "input_digest": "d496d4e824e588322f4e103d0da9bbe37a373e9ac5bc58e90ad6a4b761882322", "output_digest": "b4dacd5dcf32609a5baf5b876f42c419899d3086436c99dca34d6810663b1e21", "started_at": 1792233217.9491942}, "kind": "tool", "observed_at": 1792233217.9835477, "path": "tool:format_code", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233217-6f043361", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "9164fb82484dba5ae0e8c497728866716a2cb8874021b1fe6d53fb7273ada121", "evidence": {"detail": "format the sandbox file", "ended_at": 1792233217.984442, "error_class": null, "input_digest": "32d0d5c5830c69cf32c0ee758005a203837ad7fd802259c1ad21b6f3954bc63b", "output_digest": "c8c2a02139b62296fee3cc277bd826d681d35f5646811f49cdbc91afed8ab1e7", "started_at": 1792233217.983549}, "kind": "tool", "observed_at": 1792233217.984453, "path": "tool:format_code", "phase": "minimal_call", "prev_digest": "4339eac29575b6f17356e0dcd605bef2e8ae659e072cbb81416e92b88698f85c", "receipt_id": "LP-1792233217-39e45da0", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "0a929f0cdf83277119d58ac3dfe50b63e646b7a8eb078e44cf3a790f4384638f", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792233217.985334, "error_class": null, "injected_fault": "format a nonexistent file", "input_digest": "699188135863c5b805a7e910e721be757d537f958ef28f1590a2f7d087732c81", "output_digest": "6ed318b6181b478b9a2bc0fff2614602fb252749895d9bd5fa8b3f03b1fe613c", "started_at": 1792233217.9844537}, "kind": "tool", "observed_at": 1792233217.9853451, "path": "tool:format_code", "phase": "failure_injection", "prev_digest": "9164fb82484dba5ae0e8c497728866716a2cb8874021b1fe6d53fb7273ada121", "receipt_id": "LP-1792233217-cdc17635", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_fail"}
|
||||
{"digest": "0c704fc95e1195ff65984ee123c847f64d714cabd312ba8b8b38ec1764d8586c", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233217.9860196, "error_class": null, "input_digest": "cfa71b658f7e5cd0bd2c68fcc3a0387dffc2997b910f2ed78a5cfb2db2c9f715", "output_digest": "ae2604e827746341571cc34b70a5fd25cfd8983942c871a97640d1012d562630", "started_at": 1792233217.9853458}, "kind": "tool", "observed_at": 1792233217.9860277, "path": "tool:format_code", "phase": "recovery", "prev_digest": "0a929f0cdf83277119d58ac3dfe50b63e646b7a8eb078e44cf3a790f4384638f", "receipt_id": "LP-1792233217-096d666c", "recovery_of": "LP-1792233217-cdc17635", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "dd1250bf123cb79d2fbf1072359e671d38772f14ca36e547320286829a3d722a", "evidence": {"detail": "approval gate observed: prepared + approved (single-use token, success=False)", "ended_at": 1792234271.6412644, "error_class": null, "input_digest": "d496d4e824e588322f4e103d0da9bbe37a373e9ac5bc58e90ad6a4b761882322", "output_digest": "b4dacd5dcf32609a5baf5b876f42c419899d3086436c99dca34d6810663b1e21", "started_at": 1792234271.6176572}, "kind": "tool", "observed_at": 1792234271.6412973, "path": "tool:format_code", "phase": "auth_check", "prev_digest": "0c704fc95e1195ff65984ee123c847f64d714cabd312ba8b8b38ec1764d8586c", "receipt_id": "LP-1792234271-bad0a4a4", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "9ec4ca7e02f555fefb59a8d9ee2e582c6b54f8b047cfeb23c6053cbeccc9f214", "evidence": {"detail": "format the sandbox file", "ended_at": 1792234271.6419256, "error_class": null, "input_digest": "32d0d5c5830c69cf32c0ee758005a203837ad7fd802259c1ad21b6f3954bc63b", "output_digest": "c8c2a02139b62296fee3cc277bd826d681d35f5646811f49cdbc91afed8ab1e7", "started_at": 1792234271.6412988}, "kind": "tool", "observed_at": 1792234271.641933, "path": "tool:format_code", "phase": "minimal_call", "prev_digest": "dd1250bf123cb79d2fbf1072359e671d38772f14ca36e547320286829a3d722a", "receipt_id": "LP-1792234271-7eee87ea", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "e45f1684037afb1cf03448b01053d8170b613851e9484de8010ef46621ad6e04", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792234271.6423914, "error_class": null, "injected_fault": "format a nonexistent file", "input_digest": "699188135863c5b805a7e910e721be757d537f958ef28f1590a2f7d087732c81", "output_digest": "6ed318b6181b478b9a2bc0fff2614602fb252749895d9bd5fa8b3f03b1fe613c", "started_at": 1792234271.6419346}, "kind": "tool", "observed_at": 1792234271.6424007, "path": "tool:format_code", "phase": "failure_injection", "prev_digest": "9ec4ca7e02f555fefb59a8d9ee2e582c6b54f8b047cfeb23c6053cbeccc9f214", "receipt_id": "LP-1792234271-fdfacc31", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_fail"}
|
||||
{"digest": "60ce32130f5c17019ac7b70717e0f420fa54c8ed56a68fd674621e94275be7b2", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234271.6428502, "error_class": null, "input_digest": "cfa71b658f7e5cd0bd2c68fcc3a0387dffc2997b910f2ed78a5cfb2db2c9f715", "output_digest": "ae2604e827746341571cc34b70a5fd25cfd8983942c871a97640d1012d562630", "started_at": 1792234271.6424017}, "kind": "tool", "observed_at": 1792234271.6428561, "path": "tool:format_code", "phase": "recovery", "prev_digest": "e45f1684037afb1cf03448b01053d8170b613851e9484de8010ef46621ad6e04", "receipt_id": "LP-1792234271-a43376a0", "recovery_of": "LP-1792234271-fdfacc31", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "916622b0692180079be0fa14dc9868f280ae325a0ccb735aba7df355edbc013b", "evidence": {"detail": "approval gate observed: prepared + approved (single-use token, success=True)", "ended_at": 1792233218.126073, "error_class": null, "input_digest": "ed159930321ec7053a54a9f1e7a82daeadcf1554c84fc707791bfe3fed89ae36", "output_digest": "577080931c4764d94a2b67815cba86356206569ca9fc76ae2bb9f6ae150bfc06", "started_at": 1792233218.0524125}, "kind": "tool", "observed_at": 1792233218.1261241, "path": "tool:git_commit", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233218-178540f6", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "38f228c1f4e5b222b197325149efd41779a62aface45517b0a54873a0c960b27", "evidence": {"detail": "commit the sandbox repo", "ended_at": 1792233218.1262887, "error_class": null, "input_digest": "54ceaa1b4eccbee19db6a3dc8c24f692d029804bd0ae7c9f685c1c7860a3f2d0", "output_digest": "7b06fbc1d8ade52285636daec396c0d8ca75964b514e1077139302b18963279c", "started_at": 1792233218.1261256}, "kind": "tool", "observed_at": 1792233218.1262999, "path": "tool:git_commit", "phase": "minimal_call", "prev_digest": "916622b0692180079be0fa14dc9868f280ae325a0ccb735aba7df355edbc013b", "receipt_id": "LP-1792233218-b761dbbe", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "8e1595f6593a9291f038589c9e34b4d14a173b34764cb2e7644ffbd4c047ffb0", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792233218.1263576, "error_class": null, "injected_fault": "commit in a nonexistent repo", "input_digest": "9bddc4cfb693552c656dc9e47584d9068b3be99daae94438e0530121cb97fc8e", "output_digest": "7f907c177d743bae750c07e27658d6879a89845424b140bed34862aacda401b7", "started_at": 1792233218.1263006}, "kind": "tool", "observed_at": 1792233218.1263666, "path": "tool:git_commit", "phase": "failure_injection", "prev_digest": "38f228c1f4e5b222b197325149efd41779a62aface45517b0a54873a0c960b27", "receipt_id": "LP-1792233218-8677fcf9", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_fail"}
|
||||
{"digest": "83fb0e9120ac471c8424758037a68d54722fc570a42b735e661daabe12631fe8", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233218.1264496, "error_class": null, "input_digest": "bc75d76baf987fe949e4bb458bfc3b5746e59531bce0b74ddc3e54054a7c6b9d", "output_digest": "026076a4884e63ffc7e67997e014186d6211e01bc0e63d77c19b6392c1836f9c", "started_at": 1792233218.1263678}, "kind": "tool", "observed_at": 1792233218.1264575, "path": "tool:git_commit", "phase": "recovery", "prev_digest": "8e1595f6593a9291f038589c9e34b4d14a173b34764cb2e7644ffbd4c047ffb0", "receipt_id": "LP-1792233218-ccff3ae0", "recovery_of": "LP-1792233218-8677fcf9", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "073f9815684222ff103e6a3e30db658ec97162fe202cd2d5c05c4ba111d88e88", "evidence": {"detail": "approval gate observed: prepared + approved (single-use token, success=True)", "ended_at": 1792234271.914318, "error_class": null, "input_digest": "ed159930321ec7053a54a9f1e7a82daeadcf1554c84fc707791bfe3fed89ae36", "output_digest": "577080931c4764d94a2b67815cba86356206569ca9fc76ae2bb9f6ae150bfc06", "started_at": 1792234271.71573}, "kind": "tool", "observed_at": 1792234271.9143634, "path": "tool:git_commit", "phase": "auth_check", "prev_digest": "83fb0e9120ac471c8424758037a68d54722fc570a42b735e661daabe12631fe8", "receipt_id": "LP-1792234271-81ad40b9", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "1a1a64773f8dfe4b3618f26d6d8c60b8c17ec71a1acc6436fbd1babf7ddd5388", "evidence": {"detail": "commit the sandbox repo", "ended_at": 1792234271.9145355, "error_class": null, "input_digest": "54ceaa1b4eccbee19db6a3dc8c24f692d029804bd0ae7c9f685c1c7860a3f2d0", "output_digest": "7b06fbc1d8ade52285636daec396c0d8ca75964b514e1077139302b18963279c", "started_at": 1792234271.9143646}, "kind": "tool", "observed_at": 1792234271.9145465, "path": "tool:git_commit", "phase": "minimal_call", "prev_digest": "073f9815684222ff103e6a3e30db658ec97162fe202cd2d5c05c4ba111d88e88", "receipt_id": "LP-1792234271-fe21eb5b", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "4ecfc38df097fa56163ae6b8501207c50ca58100ea634511978be196a1e628a8", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792234271.9145935, "error_class": null, "injected_fault": "commit in a nonexistent repo", "input_digest": "9bddc4cfb693552c656dc9e47584d9068b3be99daae94438e0530121cb97fc8e", "output_digest": "7f907c177d743bae750c07e27658d6879a89845424b140bed34862aacda401b7", "started_at": 1792234271.9145477}, "kind": "tool", "observed_at": 1792234271.914602, "path": "tool:git_commit", "phase": "failure_injection", "prev_digest": "1a1a64773f8dfe4b3618f26d6d8c60b8c17ec71a1acc6436fbd1babf7ddd5388", "receipt_id": "LP-1792234271-bc6f3907", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_fail"}
|
||||
{"digest": "994380b2322699aaef85b0b64f07828650b6f06cc25a3a0c5d70c84b5fb7a610", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234271.9146597, "error_class": null, "input_digest": "bc75d76baf987fe949e4bb458bfc3b5746e59531bce0b74ddc3e54054a7c6b9d", "output_digest": "026076a4884e63ffc7e67997e014186d6211e01bc0e63d77c19b6392c1836f9c", "started_at": 1792234271.9146032}, "kind": "tool", "observed_at": 1792234271.9146674, "path": "tool:git_commit", "phase": "recovery", "prev_digest": "4ecfc38df097fa56163ae6b8501207c50ca58100ea634511978be196a1e628a8", "receipt_id": "LP-1792234271-a1df6340", "recovery_of": "LP-1792234271-bc6f3907", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "b8a6d2fdafc5e73d9a7ede9bd9ad93a09718d7c5aecec56f3b59828ff74093f4", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233217.9869187, "error_class": null, "input_digest": "dd109683f9eb4df87b83f1537860edf044f18fe29fb5bf9de5b570e4d42351d1", "output_digest": "600f50fcd096a6e7f256b1cb1e984795b95d9a2bca2271c14b1e555df34b0535", "started_at": 1792233217.9869177}, "kind": "tool", "observed_at": 1792233217.9869332, "path": "tool:git_diff", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233217-f020249f", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "4e3eed0978e2137df3ac4b4145d80dc8fe36872dbc7608de219977f684586323", "evidence": {"detail": "git diff over the sandbox repo", "ended_at": 1792233217.9911351, "error_class": null, "input_digest": "5dc9c8c0e0c1ef6aefe7169679869a6840c7df389307272714b882247d263083", "output_digest": "6ac3a850c53e3379dd847a6954b7c10317b0e655863a0c1fa67ac996102b4a9d", "started_at": 1792233217.9869337}, "kind": "tool", "observed_at": 1792233217.9911575, "path": "tool:git_diff", "phase": "minimal_call", "prev_digest": "b8a6d2fdafc5e73d9a7ede9bd9ad93a09718d7c5aecec56f3b59828ff74093f4", "receipt_id": "LP-1792233217-b056453e", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "1399ec2a6747f3bafa601cf469768b969520548b21c2104d379854cf570ed075", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792233217.9927576, "error_class": null, "injected_fault": "diff a denied (blocked) repo path", "input_digest": "56cb25dd83425ae7f3d26a189d872c5fbde6dae02b7a982a013494f55e135edf", "output_digest": "48648c794f0ada1a506476a80fd26a95e9ed255acab9015aa01963b483fae14f", "started_at": 1792233217.9911582}, "kind": "tool", "observed_at": 1792233217.9927845, "path": "tool:git_diff", "phase": "failure_injection", "prev_digest": "4e3eed0978e2137df3ac4b4145d80dc8fe36872dbc7608de219977f684586323", "receipt_id": "LP-1792233217-fe7a0fc9", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_fail"}
|
||||
{"digest": "6ff4c8d31c5acdb1ee3abf1e74e9f14d308f2b493d6ce25896109a5df4447b8b", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233217.9959311, "error_class": null, "input_digest": "6133a0f98e6ba29061d08e6c51faae4dbf25538ccb39d04b85a8dee63616f948", "output_digest": "3e4609baeb5a414e794e4616e9bfc60632540142450492360ace994cce0b0777", "started_at": 1792233217.9927852}, "kind": "tool", "observed_at": 1792233217.9959495, "path": "tool:git_diff", "phase": "recovery", "prev_digest": "1399ec2a6747f3bafa601cf469768b969520548b21c2104d379854cf570ed075", "receipt_id": "LP-1792233217-b2af2209", "recovery_of": "LP-1792233217-fe7a0fc9", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "70fdd79ac35608e5c15786e35e57f54b16991f8969413bef7bd20ad1840c8ce9", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234271.6440454, "error_class": null, "input_digest": "dd109683f9eb4df87b83f1537860edf044f18fe29fb5bf9de5b570e4d42351d1", "output_digest": "600f50fcd096a6e7f256b1cb1e984795b95d9a2bca2271c14b1e555df34b0535", "started_at": 1792234271.6440432}, "kind": "tool", "observed_at": 1792234271.6440785, "path": "tool:git_diff", "phase": "auth_check", "prev_digest": "6ff4c8d31c5acdb1ee3abf1e74e9f14d308f2b493d6ce25896109a5df4447b8b", "receipt_id": "LP-1792234271-df5ab7a3", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "a82ddab09ba040046b1c766ee624456ce04eb9c483c498567b9f6262bdf05445", "evidence": {"detail": "git diff over the sandbox repo", "ended_at": 1792234271.6542373, "error_class": null, "input_digest": "5dc9c8c0e0c1ef6aefe7169679869a6840c7df389307272714b882247d263083", "output_digest": "6ac3a850c53e3379dd847a6954b7c10317b0e655863a0c1fa67ac996102b4a9d", "started_at": 1792234271.6440797}, "kind": "tool", "observed_at": 1792234271.6542816, "path": "tool:git_diff", "phase": "minimal_call", "prev_digest": "70fdd79ac35608e5c15786e35e57f54b16991f8969413bef7bd20ad1840c8ce9", "receipt_id": "LP-1792234271-932fb030", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "289c44ecd57249e81f024825bfc778c46b26b8e4a7073547222773a2cee293f1", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792234271.6567209, "error_class": null, "injected_fault": "diff a denied (blocked) repo path", "input_digest": "56cb25dd83425ae7f3d26a189d872c5fbde6dae02b7a982a013494f55e135edf", "output_digest": "48648c794f0ada1a506476a80fd26a95e9ed255acab9015aa01963b483fae14f", "started_at": 1792234271.6542833}, "kind": "tool", "observed_at": 1792234271.6567662, "path": "tool:git_diff", "phase": "failure_injection", "prev_digest": "a82ddab09ba040046b1c766ee624456ce04eb9c483c498567b9f6262bdf05445", "receipt_id": "LP-1792234271-e5bd14bc", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_fail"}
|
||||
{"digest": "ca6edf5380c18685a683509c5919f4c89391a5c058ac32601c5d3d91a20fbaec", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234271.6615918, "error_class": null, "input_digest": "6133a0f98e6ba29061d08e6c51faae4dbf25538ccb39d04b85a8dee63616f948", "output_digest": "3e4609baeb5a414e794e4616e9bfc60632540142450492360ace994cce0b0777", "started_at": 1792234271.656769}, "kind": "tool", "observed_at": 1792234271.661634, "path": "tool:git_diff", "phase": "recovery", "prev_digest": "289c44ecd57249e81f024825bfc778c46b26b8e4a7073547222773a2cee293f1", "receipt_id": "LP-1792234271-7aab4e64", "recovery_of": "LP-1792234271-e5bd14bc", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "d37a399bd2e98a508d665ae770cd0fd13b392be7377ffd8a83a2cfc948df4dcf", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233217.9971395, "error_class": null, "input_digest": "649063c0ce4caeb01410f1d801f5473ca5a915b93b584979ed58df4c7d174e56", "output_digest": "474dbdd4b646a9d07ca2953c3344cfc3e70a4eaf5424262e68a8cf5bff6a2c3d", "started_at": 1792233217.997138}, "kind": "tool", "observed_at": 1792233217.9971535, "path": "tool:git_log", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233217-c706c568", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "dbc6f52f31665718ef4535042dce6d2117a13c928387e608b7dfb3b8c87604d2", "evidence": {"detail": "git log over the sandbox repo", "ended_at": 1792233218.026486, "error_class": null, "input_digest": "3b421cec4cca1fb5c1c61278a90d780af62162d18945951c736240001ecd785c", "output_digest": "0909012aa545ff66ddfd9e7f1a0effe86d275452a13dec63a99590b908bffdd4", "started_at": 1792233217.997154}, "kind": "tool", "observed_at": 1792233218.0265276, "path": "tool:git_log", "phase": "minimal_call", "prev_digest": "d37a399bd2e98a508d665ae770cd0fd13b392be7377ffd8a83a2cfc948df4dcf", "receipt_id": "LP-1792233218-a9755c01", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "7314b1c165f9147d420611dd332bb5acb2d7f35515033e212a6638557f92c060", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792233218.0281873, "error_class": null, "injected_fault": "log a denied (blocked) repo path", "input_digest": "6975cd174ebb61d6ef0f859f9c4dc4e282b3d2190531ac3195577f10652bdaf1", "output_digest": "1e710f2b8c9eb63962f2bc36711fbc0f55f993f6f6e3f0450e95fddb0c982a02", "started_at": 1792233218.0265288}, "kind": "tool", "observed_at": 1792233218.028207, "path": "tool:git_log", "phase": "failure_injection", "prev_digest": "dbc6f52f31665718ef4535042dce6d2117a13c928387e608b7dfb3b8c87604d2", "receipt_id": "LP-1792233218-21a25935", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_fail"}
|
||||
{"digest": "8c5bdf961689cdf93e47cab74adf6189a70250d3906db4058725b7545efd3d86", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233218.0514162, "error_class": null, "input_digest": "40c3054fe56e3e132c4996bcce0ddaa321ff089a2a3cea1a86fbfd313bb3dcbd", "output_digest": "1737bb3c248263253e525f3891c664753ee30c1038127d2d9e5e806b415ad8a5", "started_at": 1792233218.0282078}, "kind": "tool", "observed_at": 1792233218.0514512, "path": "tool:git_log", "phase": "recovery", "prev_digest": "7314b1c165f9147d420611dd332bb5acb2d7f35515033e212a6638557f92c060", "receipt_id": "LP-1792233218-876244c2", "recovery_of": "LP-1792233218-21a25935", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "2602a1f6fcea47d4c440e6d577a7264c45ad834f7e9119ae8a908c96796b7824", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234271.6629384, "error_class": null, "input_digest": "649063c0ce4caeb01410f1d801f5473ca5a915b93b584979ed58df4c7d174e56", "output_digest": "474dbdd4b646a9d07ca2953c3344cfc3e70a4eaf5424262e68a8cf5bff6a2c3d", "started_at": 1792234271.662937}, "kind": "tool", "observed_at": 1792234271.6629672, "path": "tool:git_log", "phase": "auth_check", "prev_digest": "8c5bdf961689cdf93e47cab74adf6189a70250d3906db4058725b7545efd3d86", "receipt_id": "LP-1792234271-37ca6466", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "5ed130156aa2ab7ca5374d4f96839bfc74606f52c39c34561109ab2f48fb86c6", "evidence": {"detail": "git log over the sandbox repo", "ended_at": 1792234271.6929662, "error_class": null, "input_digest": "3b421cec4cca1fb5c1c61278a90d780af62162d18945951c736240001ecd785c", "output_digest": "0909012aa545ff66ddfd9e7f1a0effe86d275452a13dec63a99590b908bffdd4", "started_at": 1792234271.6629682}, "kind": "tool", "observed_at": 1792234271.6930032, "path": "tool:git_log", "phase": "minimal_call", "prev_digest": "2602a1f6fcea47d4c440e6d577a7264c45ad834f7e9119ae8a908c96796b7824", "receipt_id": "LP-1792234271-d90ab32f", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "5f3052d9436dd547a5b25d8ea05cbce96601d1d8746e90a54c26812f8f964e02", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792234271.6946573, "error_class": null, "injected_fault": "log a denied (blocked) repo path", "input_digest": "6975cd174ebb61d6ef0f859f9c4dc4e282b3d2190531ac3195577f10652bdaf1", "output_digest": "1e710f2b8c9eb63962f2bc36711fbc0f55f993f6f6e3f0450e95fddb0c982a02", "started_at": 1792234271.6930046}, "kind": "tool", "observed_at": 1792234271.6946838, "path": "tool:git_log", "phase": "failure_injection", "prev_digest": "5ed130156aa2ab7ca5374d4f96839bfc74606f52c39c34561109ab2f48fb86c6", "receipt_id": "LP-1792234271-4e1f029c", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_fail"}
|
||||
{"digest": "c314d226a8f46b7e3ac7b071c274d7c7e6c3ca590d7806b5f14eca3cdc2c6cd3", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234271.714346, "error_class": null, "input_digest": "40c3054fe56e3e132c4996bcce0ddaa321ff089a2a3cea1a86fbfd313bb3dcbd", "output_digest": "1737bb3c248263253e525f3891c664753ee30c1038127d2d9e5e806b415ad8a5", "started_at": 1792234271.6946852}, "kind": "tool", "observed_at": 1792234271.7143848, "path": "tool:git_log", "phase": "recovery", "prev_digest": "5f3052d9436dd547a5b25d8ea05cbce96601d1d8746e90a54c26812f8f964e02", "receipt_id": "LP-1792234271-b05f6066", "recovery_of": "LP-1792234271-4e1f029c", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "fbd396f3b474173ddee9fa101702586dcdc0d40c54a13859febefb50cedd8278", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233217.9339151, "error_class": null, "input_digest": "4f843121fb703d9b35dfc4d14dfa2853921cda7260dbcccf319fe2a34a530748", "output_digest": "4eb22f7ea0eddae4e334f9327b3bd1b401a7e6bca7e6514cb36519196079f333", "started_at": 1792233217.933913}, "kind": "tool", "observed_at": 1792233217.933931, "path": "tool:glob_files", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233217-d0ea8c91", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "5a44ebf5c7aed90e1c87242450efe28bab9e915a0b56be7868c30cff5b1c3f03", "evidence": {"detail": "minimal call failed: Glob failed: Non-relative patterns are unsupported", "ended_at": 1792233217.934225, "error_class": "TOOL_ERROR", "input_digest": "6d29ab4801e6dcf7ac519cb0317756465a052e717cca11a43c6475eb2756ab13", "output_digest": "18ca7c617ecabdb08c57e16078a646adc71ecd3c7dbb989432958beaf46f5bf4", "started_at": 1792233217.9339318}, "kind": "tool", "observed_at": 1792233217.9342358, "path": "tool:glob_files", "phase": "minimal_call", "prev_digest": "fbd396f3b474173ddee9fa101702586dcdc0d40c54a13859febefb50cedd8278", "receipt_id": "LP-1792233217-e24f8da8", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_fail"}
|
||||
{"digest": "cf3da74f77b687063e6a522ba59369a6a0f0d88b982187cf2f3e6ebdc7f4936d", "evidence": {"detail": "injected fault observed failing and classified: Glob failed: Non-relative patterns are unsupported", "ended_at": 1792233217.9344637, "error_class": "TOOL_ERROR", "injected_fault": "glob a denied (blocked) directory", "input_digest": "18ecd842ba95f399ce55dad78bc94a4b2e81f2aab234b74e78a065dbd09fc8a9", "output_digest": "fc71fec9e8df2268216fa60097e52336b6a820b3912777259e4c30e2a363e3af", "started_at": 1792233217.9342365}, "kind": "tool", "observed_at": 1792233217.9344726, "path": "tool:glob_files", "phase": "failure_injection", "prev_digest": "5a44ebf5c7aed90e1c87242450efe28bab9e915a0b56be7868c30cff5b1c3f03", "receipt_id": "LP-1792233217-2e142c3a", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_pass"}
|
||||
{"digest": "e19f188c25fa4ecf7b5eb61060dbe9bd6da5e382de88b98cbb45321e0b3fe458", "evidence": {"detail": "recovery call failed: Glob failed: Non-relative patterns are unsupported", "ended_at": 1792233217.9346797, "error_class": "TOOL_ERROR", "input_digest": "c888737009165e2beefd3b8a85d12410caa374e096bc767e81c8f72504262fe5", "output_digest": "548f772b66899f037391d4b5a9bc3f78eb7db2b3ca8636b1e2803f31cdcc9fb4", "started_at": 1792233217.9344733}, "kind": "tool", "observed_at": 1792233217.9346871, "path": "tool:glob_files", "phase": "recovery", "prev_digest": "cf3da74f77b687063e6a522ba59369a6a0f0d88b982187cf2f3e6ebdc7f4936d", "receipt_id": "LP-1792233217-66d6763a", "recovery_of": "LP-1792233217-2e142c3a", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_fail"}
|
||||
{"digest": "c6e7d44d8db50d0c011d05262ace8a57f1f3249e42fd6557d8f8ed20799d54d4", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234271.6009638, "error_class": null, "input_digest": "4f843121fb703d9b35dfc4d14dfa2853921cda7260dbcccf319fe2a34a530748", "output_digest": "4eb22f7ea0eddae4e334f9327b3bd1b401a7e6bca7e6514cb36519196079f333", "started_at": 1792234271.6009629}, "kind": "tool", "observed_at": 1792234271.6009784, "path": "tool:glob_files", "phase": "auth_check", "prev_digest": "e19f188c25fa4ecf7b5eb61060dbe9bd6da5e382de88b98cbb45321e0b3fe458", "receipt_id": "LP-1792234271-28ce602a", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "cd592f9f5311b2b7e5ff65312869c8f18ea71869acde9bfcc5188a2e67ff5692", "evidence": {"detail": "minimal call failed: Glob failed: Non-relative patterns are unsupported", "ended_at": 1792234271.6012032, "error_class": "TOOL_ERROR", "input_digest": "6d29ab4801e6dcf7ac519cb0317756465a052e717cca11a43c6475eb2756ab13", "output_digest": "18ca7c617ecabdb08c57e16078a646adc71ecd3c7dbb989432958beaf46f5bf4", "started_at": 1792234271.600979}, "kind": "tool", "observed_at": 1792234271.6012113, "path": "tool:glob_files", "phase": "minimal_call", "prev_digest": "c6e7d44d8db50d0c011d05262ace8a57f1f3249e42fd6557d8f8ed20799d54d4", "receipt_id": "LP-1792234271-166ef69b", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_fail"}
|
||||
{"digest": "e8f61c426974e4fe988c9da8d186b85f2beab79d92b33b184df5ddd3a5a071f3", "evidence": {"detail": "injected fault observed failing and classified: Glob failed: Non-relative patterns are unsupported", "ended_at": 1792234271.6014142, "error_class": "TOOL_ERROR", "injected_fault": "glob a denied (blocked) directory", "input_digest": "18ecd842ba95f399ce55dad78bc94a4b2e81f2aab234b74e78a065dbd09fc8a9", "output_digest": "fc71fec9e8df2268216fa60097e52336b6a820b3912777259e4c30e2a363e3af", "started_at": 1792234271.6012118}, "kind": "tool", "observed_at": 1792234271.6014228, "path": "tool:glob_files", "phase": "failure_injection", "prev_digest": "cd592f9f5311b2b7e5ff65312869c8f18ea71869acde9bfcc5188a2e67ff5692", "receipt_id": "LP-1792234271-4a6974f3", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_pass"}
|
||||
{"digest": "b3ce4f9583b5fabaf26f4a125d5b71614687f2d4919249829ac39c749d3feda1", "evidence": {"detail": "recovery call failed: Glob failed: Non-relative patterns are unsupported", "ended_at": 1792234271.6016119, "error_class": "TOOL_ERROR", "input_digest": "c888737009165e2beefd3b8a85d12410caa374e096bc767e81c8f72504262fe5", "output_digest": "548f772b66899f037391d4b5a9bc3f78eb7db2b3ca8636b1e2803f31cdcc9fb4", "started_at": 1792234271.6014235}, "kind": "tool", "observed_at": 1792234271.6016188, "path": "tool:glob_files", "phase": "recovery", "prev_digest": "e8f61c426974e4fe988c9da8d186b85f2beab79d92b33b184df5ddd3a5a071f3", "receipt_id": "LP-1792234271-aace9b93", "recovery_of": "LP-1792234271-4a6974f3", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_fail"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "ca6b2d197b0f4044a4208800b15e8990a18af4aa4e3c7e165c884f725b439706", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233217.935326, "error_class": null, "input_digest": "cdd89c699a648d21d577bcfeb2b87a987f64944e82350846d139f4f1eec4d418", "output_digest": "d4766f9548002e4602da5da4f827f61294cc8b08a234cc7880f8ae42147ab278", "started_at": 1792233217.9353254}, "kind": "tool", "observed_at": 1792233217.9353373, "path": "tool:grep_files", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233217-17701868", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "ce86db21dd675cfb1f8e5f9f4af6bc822f65d2064ff62f12629216de55e7e112", "evidence": {"detail": "grep a sandbox file", "ended_at": 1792233217.9409065, "error_class": null, "input_digest": "38ce20c1d4bc02969671edfe6f3464b78a54a6ba9b089ddad424c5583748a6e8", "output_digest": "1955a17bb45ecc94b754b450cee352c2fe72595a1b05b703f93fd2d8acde73ba", "started_at": 1792233217.9353378}, "kind": "tool", "observed_at": 1792233217.9409366, "path": "tool:grep_files", "phase": "minimal_call", "prev_digest": "ca6b2d197b0f4044a4208800b15e8990a18af4aa4e3c7e165c884f725b439706", "receipt_id": "LP-1792233217-a7192254", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "3e3047bd15e478e012e1aedd3dbfd41e33ebf414f0a9a3b571f88d4710a07a96", "evidence": {"detail": "injected fault observed failing and classified: Access denied: /etc/__live_proof_probe__", "ended_at": 1792233217.9411662, "error_class": "PERMISSION", "injected_fault": "grep a denied (blocked) directory", "input_digest": "c65c78d2d1158e068826d995cf869a01093c6595829ef41bd2b4802ade1c0ef2", "output_digest": "e2c0b1072fac866e0ec2ea1177d20dee5a2a89439b2307e06cae94d31ea6c59b", "started_at": 1792233217.9409375}, "kind": "tool", "observed_at": 1792233217.941184, "path": "tool:grep_files", "phase": "failure_injection", "prev_digest": "ce86db21dd675cfb1f8e5f9f4af6bc822f65d2064ff62f12629216de55e7e112", "receipt_id": "LP-1792233217-f7b02ecf", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_pass"}
|
||||
{"digest": "54090879f2f24ba8696656076e14b9d392673b13205a318d696404e416581769", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233217.9442227, "error_class": null, "input_digest": "364577c4d7e3583ff668e6ab3d5c7c327ccdad96e32b554b6701e3ff16665bb4", "output_digest": "492c4575f234f3b56c2e0b3271eb87dfccf7fae373e64af1c20753f9bd40ce11", "started_at": 1792233217.9411852}, "kind": "tool", "observed_at": 1792233217.9442453, "path": "tool:grep_files", "phase": "recovery", "prev_digest": "3e3047bd15e478e012e1aedd3dbfd41e33ebf414f0a9a3b571f88d4710a07a96", "receipt_id": "LP-1792233217-120e6b01", "recovery_of": "LP-1792233217-f7b02ecf", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "725a232bd125611cfcbab7e6c0702359bedfeef5cf4215cbd4b9cab5befcbb7a", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234271.6025229, "error_class": null, "input_digest": "cdd89c699a648d21d577bcfeb2b87a987f64944e82350846d139f4f1eec4d418", "output_digest": "d4766f9548002e4602da5da4f827f61294cc8b08a234cc7880f8ae42147ab278", "started_at": 1792234271.6025221}, "kind": "tool", "observed_at": 1792234271.6025395, "path": "tool:grep_files", "phase": "auth_check", "prev_digest": "54090879f2f24ba8696656076e14b9d392673b13205a318d696404e416581769", "receipt_id": "LP-1792234271-37ebbcdd", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "d4579a36a0043c200661f235d2f5562ce9c1142d091d4fef7e708948f3e39e75", "evidence": {"detail": "grep a sandbox file", "ended_at": 1792234271.6092792, "error_class": null, "input_digest": "38ce20c1d4bc02969671edfe6f3464b78a54a6ba9b089ddad424c5583748a6e8", "output_digest": "1955a17bb45ecc94b754b450cee352c2fe72595a1b05b703f93fd2d8acde73ba", "started_at": 1792234271.6025403}, "kind": "tool", "observed_at": 1792234271.6093132, "path": "tool:grep_files", "phase": "minimal_call", "prev_digest": "725a232bd125611cfcbab7e6c0702359bedfeef5cf4215cbd4b9cab5befcbb7a", "receipt_id": "LP-1792234271-aadffe6a", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "4e7426470340a5eaacc4f26bdec0799758778fb2c69e7830aff9c423220565fa", "evidence": {"detail": "injected fault observed failing and classified: Access denied: /etc/__live_proof_probe__", "ended_at": 1792234271.6094868, "error_class": "PERMISSION", "injected_fault": "grep a denied (blocked) directory", "input_digest": "c65c78d2d1158e068826d995cf869a01093c6595829ef41bd2b4802ade1c0ef2", "output_digest": "e2c0b1072fac866e0ec2ea1177d20dee5a2a89439b2307e06cae94d31ea6c59b", "started_at": 1792234271.609315}, "kind": "tool", "observed_at": 1792234271.6095002, "path": "tool:grep_files", "phase": "failure_injection", "prev_digest": "d4579a36a0043c200661f235d2f5562ce9c1142d091d4fef7e708948f3e39e75", "receipt_id": "LP-1792234271-b3ea1b58", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_pass"}
|
||||
{"digest": "e043fb2c4fe25f6f1be293c3af0cc419fc09ff0823a93fe3b4bb0c2b3ea29a14", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234271.6120403, "error_class": null, "input_digest": "364577c4d7e3583ff668e6ab3d5c7c327ccdad96e32b554b6701e3ff16665bb4", "output_digest": "492c4575f234f3b56c2e0b3271eb87dfccf7fae373e64af1c20753f9bd40ce11", "started_at": 1792234271.6095011}, "kind": "tool", "observed_at": 1792234271.6121018, "path": "tool:grep_files", "phase": "recovery", "prev_digest": "4e7426470340a5eaacc4f26bdec0799758778fb2c69e7830aff9c423220565fa", "receipt_id": "LP-1792234271-14b06674", "recovery_of": "LP-1792234271-b3ea1b58", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "2afa23cbd5394f3b1d8a5da9bef664f881009a39743f3b79d69e79aafa39194b", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233217.9476285, "error_class": null, "input_digest": "2a7a6a240931ee72ca2019216925dacd7aba19b13999a3fb7ceb236d7a0288a7", "output_digest": "9ac538a32cfeadb31fccf607fac77e1f8a64b2053a6083c94cabc4a7c835c8f0", "started_at": 1792233217.9476275}, "kind": "tool", "observed_at": 1792233217.9476404, "path": "tool:lint_file", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233217-4dd5966b", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "4802525f3c9276621b2a362eb5264cb28c5d48b966dd085534e992f4d4dc5730", "evidence": {"detail": "minimal call succeeded", "ended_at": 1792233217.9476998, "error_class": null, "input_digest": "cd2bb73a5642aadf8695d8127b90b452514ce85166ab6b49b18d904eedc678cb", "output_digest": "be244c27ed1fc6de75e4aff5cc21ec8e59617b9bfabc778bf55d1d5834c94a22", "started_at": 1792233217.947641}, "kind": "tool", "observed_at": 1792233217.9477074, "path": "tool:lint_file", "phase": "minimal_call", "prev_digest": "2afa23cbd5394f3b1d8a5da9bef664f881009a39743f3b79d69e79aafa39194b", "receipt_id": "LP-1792233217-925281b0", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "8be94b51c2df782f24645930b8dfbb35585560a2da00bdd175900456a7842581", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792233217.947737, "error_class": null, "injected_fault": "nonexistent path", "input_digest": "25ba8e3e86e54b7b999f22e50ae9aff41264c8bacefdf7c76132c9e3f5961233", "output_digest": "12ce9c293d4b3616c57584df85827ffacb72e660f76329c9ca01b4fa1367ac13", "started_at": 1792233217.947708}, "kind": "tool", "observed_at": 1792233217.9477437, "path": "tool:lint_file", "phase": "failure_injection", "prev_digest": "4802525f3c9276621b2a362eb5264cb28c5d48b966dd085534e992f4d4dc5730", "receipt_id": "LP-1792233217-9a26297d", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_fail"}
|
||||
{"digest": "08c9558952fc2be0ab5cd37939d31b3b0d90370ed2d184c0f0891715d29925a5", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233217.947783, "error_class": null, "input_digest": "bbb6def7000c7b95ae376d280dee9b494d20d8454507e06fe1ee032a0a4b043c", "output_digest": "0759c6c15d35bd61e08ac0a7d39b1b6ec3a73f97833c9df5a3719b77d99e2724", "started_at": 1792233217.9477446}, "kind": "tool", "observed_at": 1792233217.9477885, "path": "tool:lint_file", "phase": "recovery", "prev_digest": "8be94b51c2df782f24645930b8dfbb35585560a2da00bdd175900456a7842581", "receipt_id": "LP-1792233217-c32a6139", "recovery_of": "LP-1792233217-9a26297d", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "dc06417da9ea0c3d55014d9989deaf698a6bec8a4907db23ee7fae781137d38f", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234271.6153092, "error_class": null, "input_digest": "2a7a6a240931ee72ca2019216925dacd7aba19b13999a3fb7ceb236d7a0288a7", "output_digest": "9ac538a32cfeadb31fccf607fac77e1f8a64b2053a6083c94cabc4a7c835c8f0", "started_at": 1792234271.6153085}, "kind": "tool", "observed_at": 1792234271.6153266, "path": "tool:lint_file", "phase": "auth_check", "prev_digest": "08c9558952fc2be0ab5cd37939d31b3b0d90370ed2d184c0f0891715d29925a5", "receipt_id": "LP-1792234271-e0ababcd", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "6ffed698068fa5738f6c1139fc8f6cd34853783d0b540791a40d1af4ae34c128", "evidence": {"detail": "minimal call succeeded", "ended_at": 1792234271.6154006, "error_class": null, "input_digest": "cd2bb73a5642aadf8695d8127b90b452514ce85166ab6b49b18d904eedc678cb", "output_digest": "be244c27ed1fc6de75e4aff5cc21ec8e59617b9bfabc778bf55d1d5834c94a22", "started_at": 1792234271.6153276}, "kind": "tool", "observed_at": 1792234271.615408, "path": "tool:lint_file", "phase": "minimal_call", "prev_digest": "dc06417da9ea0c3d55014d9989deaf698a6bec8a4907db23ee7fae781137d38f", "receipt_id": "LP-1792234271-e366c8c1", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "4a1669efd3d9eaf14c8b2f6d4646348e9324f86b620eb3c319b2c72fecba0680", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792234271.6154346, "error_class": null, "injected_fault": "nonexistent path", "input_digest": "25ba8e3e86e54b7b999f22e50ae9aff41264c8bacefdf7c76132c9e3f5961233", "output_digest": "12ce9c293d4b3616c57584df85827ffacb72e660f76329c9ca01b4fa1367ac13", "started_at": 1792234271.6154084}, "kind": "tool", "observed_at": 1792234271.6154406, "path": "tool:lint_file", "phase": "failure_injection", "prev_digest": "6ffed698068fa5738f6c1139fc8f6cd34853783d0b540791a40d1af4ae34c128", "receipt_id": "LP-1792234271-af2ad360", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_fail"}
|
||||
{"digest": "eaf5b688c55ab6d59f12cc00815172119c26cbd5ea6718908c94c36a6361aa5d", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234271.6154745, "error_class": null, "input_digest": "bbb6def7000c7b95ae376d280dee9b494d20d8454507e06fe1ee032a0a4b043c", "output_digest": "0759c6c15d35bd61e08ac0a7d39b1b6ec3a73f97833c9df5a3719b77d99e2724", "started_at": 1792234271.6154413}, "kind": "tool", "observed_at": 1792234271.6154797, "path": "tool:lint_file", "phase": "recovery", "prev_digest": "4a1669efd3d9eaf14c8b2f6d4646348e9324f86b620eb3c319b2c72fecba0680", "receipt_id": "LP-1792234271-81f9c76e", "recovery_of": "LP-1792234271-af2ad360", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "d9497847d182ef807a08f07cb77f9d42a944078a6508ba17c31bf5b5d02325fd", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233217.945382, "error_class": null, "input_digest": "6b43b15af0cf7752e9d7b09cdfda13394b3924e0de444c974d41f231c5f87949", "output_digest": "5d0063be61f65f0be8a3c826ceac4e098df8ec4073548014daf44f35f51db240", "started_at": 1792233217.9453716}, "kind": "tool", "observed_at": 1792233217.9453971, "path": "tool:list_directory", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233217-526a0323", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "814476b9aa9484cb454b0462759e3562d39b249d3dfa3e94f72ec1f83c40a637", "evidence": {"detail": "list the sandbox dir", "ended_at": 1792233217.9459317, "error_class": null, "input_digest": "069c96bebeda6a94a8450734520baed25f361ac03be840a618c92cdccbf98fb1", "output_digest": "37723842890f20e43139e729d4cb590dc9ab669def037f0b6a47ff5be6cf91d7", "started_at": 1792233217.945398}, "kind": "tool", "observed_at": 1792233217.9459438, "path": "tool:list_directory", "phase": "minimal_call", "prev_digest": "d9497847d182ef807a08f07cb77f9d42a944078a6508ba17c31bf5b5d02325fd", "receipt_id": "LP-1792233217-f2cd7745", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "287c86755f51c12031f996764833c910751be53d4aaf5c0891431ed5b44bef59", "evidence": {"detail": "injected fault observed failing and classified: Not a directory: /mnt/workdrive/rune/src/rune/../../store/live_proof/sandbox/__nonexistent_live_probe__", "ended_at": 1792233217.946346, "error_class": "TOOL_ERROR", "injected_fault": "list a nonexistent directory", "input_digest": "d2acd6694e44cd5249e24056e61bff8f34e21df7da36ba9e5db167a0ef30a738", "output_digest": "9a6c6b202fbe01d68c18c6337da46b208027b22685a18f0cc19bba9955346459", "started_at": 1792233217.9459445}, "kind": "tool", "observed_at": 1792233217.9463575, "path": "tool:list_directory", "phase": "failure_injection", "prev_digest": "814476b9aa9484cb454b0462759e3562d39b249d3dfa3e94f72ec1f83c40a637", "receipt_id": "LP-1792233217-19ef3a90", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_pass"}
|
||||
{"digest": "42ca5d5d7c1c3b39a43a2d09446974c20b9ab270fbacb2afdf89baed672c8979", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233217.946841, "error_class": null, "input_digest": "510530798b535e832107ea806313e310d006b2cf4cd15a6a1dd8b9a867f429b5", "output_digest": "fda58a44dfacac350af8b90e4aba3762ab2762b218e12d6c674fa918841b89f9", "started_at": 1792233217.9463582}, "kind": "tool", "observed_at": 1792233217.9468517, "path": "tool:list_directory", "phase": "recovery", "prev_digest": "287c86755f51c12031f996764833c910751be53d4aaf5c0891431ed5b44bef59", "receipt_id": "LP-1792233217-ad76728f", "recovery_of": "LP-1792233217-19ef3a90", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "178061e34dbc7a820ba66b79872cc326388c9a9fc2ac117cf212705111a6dde1", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234271.6131546, "error_class": null, "input_digest": "6b43b15af0cf7752e9d7b09cdfda13394b3924e0de444c974d41f231c5f87949", "output_digest": "5d0063be61f65f0be8a3c826ceac4e098df8ec4073548014daf44f35f51db240", "started_at": 1792234271.6131535}, "kind": "tool", "observed_at": 1792234271.6131732, "path": "tool:list_directory", "phase": "auth_check", "prev_digest": "42ca5d5d7c1c3b39a43a2d09446974c20b9ab270fbacb2afdf89baed672c8979", "receipt_id": "LP-1792234271-8361335e", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "acf1b8110dd09b77a0d9a26acf42aaa6f5bc741f91bb74c727d5d8aec3591571", "evidence": {"detail": "list the sandbox dir", "ended_at": 1792234271.6137004, "error_class": null, "input_digest": "069c96bebeda6a94a8450734520baed25f361ac03be840a618c92cdccbf98fb1", "output_digest": "37723842890f20e43139e729d4cb590dc9ab669def037f0b6a47ff5be6cf91d7", "started_at": 1792234271.613174}, "kind": "tool", "observed_at": 1792234271.613712, "path": "tool:list_directory", "phase": "minimal_call", "prev_digest": "178061e34dbc7a820ba66b79872cc326388c9a9fc2ac117cf212705111a6dde1", "receipt_id": "LP-1792234271-88b46077", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "54eb3e43620219f316cf9fb4c0c90215bfb1a094822fcc65bc722c8cab1d7a03", "evidence": {"detail": "injected fault observed failing and classified: Not a directory: /mnt/workdrive/rune/src/rune/../../store/live_proof/sandbox/__nonexistent_live_probe__", "ended_at": 1792234271.613998, "error_class": "TOOL_ERROR", "injected_fault": "list a nonexistent directory", "input_digest": "d2acd6694e44cd5249e24056e61bff8f34e21df7da36ba9e5db167a0ef30a738", "output_digest": "9a6c6b202fbe01d68c18c6337da46b208027b22685a18f0cc19bba9955346459", "started_at": 1792234271.6137133}, "kind": "tool", "observed_at": 1792234271.6140075, "path": "tool:list_directory", "phase": "failure_injection", "prev_digest": "acf1b8110dd09b77a0d9a26acf42aaa6f5bc741f91bb74c727d5d8aec3591571", "receipt_id": "LP-1792234271-1eda394f", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_pass"}
|
||||
{"digest": "6b0642dd345c7c6ca3b0a50537e2cc384ea935caed9a2894539bd52bcf02cc66", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234271.614471, "error_class": null, "input_digest": "510530798b535e832107ea806313e310d006b2cf4cd15a6a1dd8b9a867f429b5", "output_digest": "fda58a44dfacac350af8b90e4aba3762ab2762b218e12d6c674fa918841b89f9", "started_at": 1792234271.6140084}, "kind": "tool", "observed_at": 1792234271.6144807, "path": "tool:list_directory", "phase": "recovery", "prev_digest": "54eb3e43620219f316cf9fb4c0c90215bfb1a094822fcc65bc722c8cab1d7a03", "receipt_id": "LP-1792234271-f869ded9", "recovery_of": "LP-1792234271-1eda394f", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "e1546212062113737150a2d8e8852511045035c7bb990765d00b11cbb29ba661", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233218.1561284, "error_class": null, "input_digest": "7184223d0e85584065aa1e316ae04503b7227a78a56625b0de7ceacb33513640", "output_digest": "8e81da77c496577d7ed96e801feae19296d1c383fa83001c1bbef0215b174bfa", "started_at": 1792233218.1561275}, "kind": "tool", "observed_at": 1792233218.1561394, "path": "tool:query_memory", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233218-5ec1cdcd", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "3058f116ebf9f8d0d03af4d0307463879b8d551dd61906c1d54201a294d948ed", "evidence": {"detail": "query memory with a one-word probe", "ended_at": 1792233218.1939867, "error_class": null, "input_digest": "dba8f2f072b60651103cddc3c6b9b13ce984d482fc5f91f2b6016878ccd76ca2", "output_digest": "433f4446dd6c9b8f3acba08497aa2ae5ed77e13c75a7e27b075f011f88cd49fe", "started_at": 1792233218.1561399}, "kind": "tool", "observed_at": 1792233218.1940286, "path": "tool:query_memory", "phase": "minimal_call", "prev_digest": "e1546212062113737150a2d8e8852511045035c7bb990765d00b11cbb29ba661", "receipt_id": "LP-1792233218-2cf0bbce", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "7bc2aea79d09ccac5ba9164352a83da6d2e3552a6216333bbaec291af0cd3a87", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792233218.9414935, "error_class": null, "injected_fault": "query with a malformed memory id", "input_digest": "0abab62acdd7e5a1fa6ed4ee65f14d4dab244cd03b4b785e4bc37beea2b4e1f8", "output_digest": "1ab58f149ee2d6fc8db4383bf560006c78a079e87f6f4cbd1b9b7bac23349eba", "started_at": 1792233218.1940296}, "kind": "tool", "observed_at": 1792233218.9415393, "path": "tool:query_memory", "phase": "failure_injection", "prev_digest": "3058f116ebf9f8d0d03af4d0307463879b8d551dd61906c1d54201a294d948ed", "receipt_id": "LP-1792233218-39063f62", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_fail"}
|
||||
{"digest": "024de5cc2f0059119d414e1b3de95f4c5c3d06ed25fdc49471dbb455d2296ccd", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233218.9697337, "error_class": null, "input_digest": "f5bb307b8741b29f2a147f7687528e86b5c8b1c68b4ab90edcb89f8c93791378", "output_digest": "89cc3d6753f1bafe5a328c418b68a09234124112404eda5b7038f26585e5c918", "started_at": 1792233218.9415414}, "kind": "tool", "observed_at": 1792233218.96978, "path": "tool:query_memory", "phase": "recovery", "prev_digest": "7bc2aea79d09ccac5ba9164352a83da6d2e3552a6216333bbaec291af0cd3a87", "receipt_id": "LP-1792233218-8c887c60", "recovery_of": "LP-1792233218-39063f62", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "09074137a383f23c11060a8319275f3741008f656dd1ec7728577da4298aa8a2", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234271.9486344, "error_class": null, "input_digest": "7184223d0e85584065aa1e316ae04503b7227a78a56625b0de7ceacb33513640", "output_digest": "8e81da77c496577d7ed96e801feae19296d1c383fa83001c1bbef0215b174bfa", "started_at": 1792234271.9486327}, "kind": "tool", "observed_at": 1792234271.948659, "path": "tool:query_memory", "phase": "auth_check", "prev_digest": "024de5cc2f0059119d414e1b3de95f4c5c3d06ed25fdc49471dbb455d2296ccd", "receipt_id": "LP-1792234271-45f51270", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "f231135825e9b7d26511b6fa42fe4f87b9389cc7cfe88bb1cf3082530633b692", "evidence": {"detail": "query memory with a one-word probe", "ended_at": 1792234271.9836683, "error_class": null, "input_digest": "dba8f2f072b60651103cddc3c6b9b13ce984d482fc5f91f2b6016878ccd76ca2", "output_digest": "433f4446dd6c9b8f3acba08497aa2ae5ed77e13c75a7e27b075f011f88cd49fe", "started_at": 1792234271.94866}, "kind": "tool", "observed_at": 1792234271.9837193, "path": "tool:query_memory", "phase": "minimal_call", "prev_digest": "09074137a383f23c11060a8319275f3741008f656dd1ec7728577da4298aa8a2", "receipt_id": "LP-1792234271-aea4fa54", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "e88035d996ccc7f505c5e973081d87bc9029775a338758b57f4ec8bf0e55f470", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792234272.8042405, "error_class": null, "injected_fault": "query with a malformed memory id", "input_digest": "0abab62acdd7e5a1fa6ed4ee65f14d4dab244cd03b4b785e4bc37beea2b4e1f8", "output_digest": "1ab58f149ee2d6fc8db4383bf560006c78a079e87f6f4cbd1b9b7bac23349eba", "started_at": 1792234271.983721}, "kind": "tool", "observed_at": 1792234272.804284, "path": "tool:query_memory", "phase": "failure_injection", "prev_digest": "f231135825e9b7d26511b6fa42fe4f87b9389cc7cfe88bb1cf3082530633b692", "receipt_id": "LP-1792234272-dbe5cc7e", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_fail"}
|
||||
{"digest": "a5186c7bb3d92bc00eef5defc97e0c15a532a417f2867383ad2079c05c302c7a", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234272.8316886, "error_class": null, "input_digest": "f5bb307b8741b29f2a147f7687528e86b5c8b1c68b4ab90edcb89f8c93791378", "output_digest": "89cc3d6753f1bafe5a328c418b68a09234124112404eda5b7038f26585e5c918", "started_at": 1792234272.8042862}, "kind": "tool", "observed_at": 1792234272.831737, "path": "tool:query_memory", "phase": "recovery", "prev_digest": "e88035d996ccc7f505c5e973081d87bc9029775a338758b57f4ec8bf0e55f470", "receipt_id": "LP-1792234272-4c6c08a4", "recovery_of": "LP-1792234272-dbe5cc7e", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "5d2a31a0fdc40a369767cd241ac85b65562a894ae19f58d473b815674a595186", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233217.9234314, "error_class": null, "input_digest": "3188fbe61785a2f896568b7efe73f3444a8db2041772fae33cc9be5b613b212d", "output_digest": "4696f13fd53c94d5147e2ecfd127d3b747a4f66d832c465e97eb1daa55b5ade2", "started_at": 1792233217.9234238}, "kind": "tool", "observed_at": 1792233217.9235158, "path": "tool:read_file", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233217-ddcfbfa3", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "043140ce10309c928e2d2454c4edf1bc2a26ce1effa7803e582a84c672590348", "evidence": {"detail": "read a one-line sandbox file", "ended_at": 1792233217.9241977, "error_class": null, "input_digest": "4db66f81ba9f66805811353291d68eb7e9b49ac52080ee4f2911da97e34ff822", "output_digest": "38a33175fcad1ad10820c264fdcd398795173a1d8ca9bce7cdf5d6ac42bc19fc", "started_at": 1792233217.923518}, "kind": "tool", "observed_at": 1792233217.9242291, "path": "tool:read_file", "phase": "minimal_call", "prev_digest": "5d2a31a0fdc40a369767cd241ac85b65562a894ae19f58d473b815674a595186", "receipt_id": "LP-1792233217-1e5aa0c2", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "fa601aac0481024ccc835c20be366c3d880edee5220c70e0c627ba4bb0bfe2b2", "evidence": {"detail": "injected fault observed failing and classified: Not found: /mnt/workdrive/rune/src/rune/../../store/live_proof/sandbox/__nonexistent_live_probe__", "ended_at": 1792233217.924617, "error_class": "ENOENT", "injected_fault": "read a nonexistent path", "input_digest": "f57b736271e86fa2ba87134bc3d1fe285bdc4e8c9ea3433b7b5d66aca532190b", "output_digest": "dac1b171dad67eefb479bcaae54c15be34484711044bafb645d4e6d735de94fd", "started_at": 1792233217.924231}, "kind": "tool", "observed_at": 1792233217.924639, "path": "tool:read_file", "phase": "failure_injection", "prev_digest": "043140ce10309c928e2d2454c4edf1bc2a26ce1effa7803e582a84c672590348", "receipt_id": "LP-1792233217-1122adad", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_pass"}
|
||||
{"digest": "fa3c1d0ad516651d8bf87326d0ac899b3dd5273ea0629d7347f2a785fc03b72b", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233217.9250536, "error_class": null, "input_digest": "a846ca5a3235abce5db143b6194fe7f4e3516ff944913e3baff2d3cb4afe0c0c", "output_digest": "226cee6011cf22d0539ef2898e20d0996473d0286e743a2261f69421241086e0", "started_at": 1792233217.9246402}, "kind": "tool", "observed_at": 1792233217.9250731, "path": "tool:read_file", "phase": "recovery", "prev_digest": "fa601aac0481024ccc835c20be366c3d880edee5220c70e0c627ba4bb0bfe2b2", "receipt_id": "LP-1792233217-0472d94c", "recovery_of": "LP-1792233217-1122adad", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "34dd25b559e0f7bf4d15d00890732fb82c21accad42fc1cfd7f70f8e547405ee", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234271.5884204, "error_class": null, "input_digest": "3188fbe61785a2f896568b7efe73f3444a8db2041772fae33cc9be5b613b212d", "output_digest": "4696f13fd53c94d5147e2ecfd127d3b747a4f66d832c465e97eb1daa55b5ade2", "started_at": 1792234271.5884168}, "kind": "tool", "observed_at": 1792234271.5884645, "path": "tool:read_file", "phase": "auth_check", "prev_digest": "fa3c1d0ad516651d8bf87326d0ac899b3dd5273ea0629d7347f2a785fc03b72b", "receipt_id": "LP-1792234271-d18c0c2e", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "ec7e32b8f1f24f37dd50358f36afc4008505cd7ae3e5e730807ca5e77f6ad114", "evidence": {"detail": "read a one-line sandbox file", "ended_at": 1792234271.5888975, "error_class": null, "input_digest": "4db66f81ba9f66805811353291d68eb7e9b49ac52080ee4f2911da97e34ff822", "output_digest": "38a33175fcad1ad10820c264fdcd398795173a1d8ca9bce7cdf5d6ac42bc19fc", "started_at": 1792234271.5884662}, "kind": "tool", "observed_at": 1792234271.5889146, "path": "tool:read_file", "phase": "minimal_call", "prev_digest": "34dd25b559e0f7bf4d15d00890732fb82c21accad42fc1cfd7f70f8e547405ee", "receipt_id": "LP-1792234271-6b614f33", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "469b342f48685df796c4137983287765d353f08e396e86eb66cd7ea15cc1fd91", "evidence": {"detail": "injected fault observed failing and classified: Not found: /mnt/workdrive/rune/src/rune/../../store/live_proof/sandbox/__nonexistent_live_probe__", "ended_at": 1792234271.5892375, "error_class": "ENOENT", "injected_fault": "read a nonexistent path", "input_digest": "f57b736271e86fa2ba87134bc3d1fe285bdc4e8c9ea3433b7b5d66aca532190b", "output_digest": "dac1b171dad67eefb479bcaae54c15be34484711044bafb645d4e6d735de94fd", "started_at": 1792234271.5889158}, "kind": "tool", "observed_at": 1792234271.589255, "path": "tool:read_file", "phase": "failure_injection", "prev_digest": "ec7e32b8f1f24f37dd50358f36afc4008505cd7ae3e5e730807ca5e77f6ad114", "receipt_id": "LP-1792234271-4f8611af", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_pass"}
|
||||
{"digest": "934fcf6d1aec1798e1a6056c732f12532f713e80065ff9fb03ec65a675fa7fbd", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234271.5896084, "error_class": null, "input_digest": "a846ca5a3235abce5db143b6194fe7f4e3516ff944913e3baff2d3cb4afe0c0c", "output_digest": "226cee6011cf22d0539ef2898e20d0996473d0286e743a2261f69421241086e0", "started_at": 1792234271.589257}, "kind": "tool", "observed_at": 1792234271.589622, "path": "tool:read_file", "phase": "recovery", "prev_digest": "469b342f48685df796c4137983287765d353f08e396e86eb66cd7ea15cc1fd91", "receipt_id": "LP-1792234271-5d313ee4", "recovery_of": "LP-1792234271-4f8611af", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "af62001cbd78dbdead0c45c494dc4b41abbdd25f58269840eff9f461cdbfeb92", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233218.1550496, "error_class": null, "input_digest": "063e2e0777ec5122374f5a665a0eb89601044d75481bf080c15528f2a03d31ff", "output_digest": "cb23a951feb41b1f5f7b2a4ee5787515cff14225aebada207840410b28b9d89a", "started_at": 1792233218.1550484}, "kind": "tool", "observed_at": 1792233218.155064, "path": "tool:read_persona", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233218-e7251b1d", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "791b65eb10cfa1715e35f207d647c7bd5b3b26f5aa5c65ab2106855da75b277f", "evidence": {"detail": "minimal call failed: Persona block not found in MEMORY.md", "ended_at": 1792233218.1552446, "error_class": "TOOL_ERROR", "input_digest": "29b57b3ba9cb4bc9534b16e7502e076401070bcf5f46d4f2996f1a3f58544a22", "output_digest": "bfc3d287e7db7de512d16e29c879ee3c06336db47b387d214906bff469deb3a7", "started_at": 1792233218.1550646}, "kind": "tool", "observed_at": 1792233218.1552546, "path": "tool:read_persona", "phase": "minimal_call", "prev_digest": "af62001cbd78dbdead0c45c494dc4b41abbdd25f58269840eff9f461cdbfeb92", "receipt_id": "LP-1792233218-2fb88146", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_fail"}
|
||||
{"digest": "c3270bb06ff00054a0638e722bff10e7d0146aa07d29ad6b47c54ce433b529a1", "evidence": {"detail": "injected fault observed failing and classified: Persona block not found in MEMORY.md", "ended_at": 1792233218.1553478, "error_class": "ENOENT", "injected_fault": "read with a poisoned persona id", "input_digest": "320c851fa76bd033c2762d99997e960c473d847102861b0607fe20dcedb4a4cf", "output_digest": "04a15611da0c41d536ad2f60cec590b47cefde420c57ef5a0b1320b456259f07", "started_at": 1792233218.155255}, "kind": "tool", "observed_at": 1792233218.1553597, "path": "tool:read_persona", "phase": "failure_injection", "prev_digest": "791b65eb10cfa1715e35f207d647c7bd5b3b26f5aa5c65ab2106855da75b277f", "receipt_id": "LP-1792233218-58d29ebd", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_pass"}
|
||||
{"digest": "b7c3d02a1aafe144b876fa713529b540dc06399f44fa60fdedefb3f38336f656", "evidence": {"detail": "recovery call failed: Persona block not found in MEMORY.md", "ended_at": 1792233218.1554778, "error_class": "TOOL_ERROR", "input_digest": "b32802c62eebca15317113d7e4455508c074d6b39ca1220681f186cc85ba0e92", "output_digest": "5af50857ea4c4a151574c3f98294fcf212543c665505234ad639ee6db240e0e5", "started_at": 1792233218.1553605}, "kind": "tool", "observed_at": 1792233218.1554883, "path": "tool:read_persona", "phase": "recovery", "prev_digest": "c3270bb06ff00054a0638e722bff10e7d0146aa07d29ad6b47c54ce433b529a1", "receipt_id": "LP-1792233218-2cbaff76", "recovery_of": "LP-1792233218-58d29ebd", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_fail"}
|
||||
{"digest": "52676b47b7b64b7eafeb1ab21acd7b786675b8ee3f0466d27c30bdb9c1216676", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234271.947291, "error_class": null, "input_digest": "063e2e0777ec5122374f5a665a0eb89601044d75481bf080c15528f2a03d31ff", "output_digest": "cb23a951feb41b1f5f7b2a4ee5787515cff14225aebada207840410b28b9d89a", "started_at": 1792234271.9472888}, "kind": "tool", "observed_at": 1792234271.9473097, "path": "tool:read_persona", "phase": "auth_check", "prev_digest": "b7c3d02a1aafe144b876fa713529b540dc06399f44fa60fdedefb3f38336f656", "receipt_id": "LP-1792234271-92ff456d", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "ec59b876a8744730382f969091ac6476b352c6ca13f3f3b4fb61d948dc049a48", "evidence": {"detail": "minimal call failed: Persona block not found in MEMORY.md", "ended_at": 1792234271.947483, "error_class": "TOOL_ERROR", "input_digest": "29b57b3ba9cb4bc9534b16e7502e076401070bcf5f46d4f2996f1a3f58544a22", "output_digest": "bfc3d287e7db7de512d16e29c879ee3c06336db47b387d214906bff469deb3a7", "started_at": 1792234271.9473107}, "kind": "tool", "observed_at": 1792234271.9474926, "path": "tool:read_persona", "phase": "minimal_call", "prev_digest": "52676b47b7b64b7eafeb1ab21acd7b786675b8ee3f0466d27c30bdb9c1216676", "receipt_id": "LP-1792234271-5f7c6e26", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_fail"}
|
||||
{"digest": "789e81a4b786da6e8796540641546c89f4d4d5e0749816e02a9856a3c8de11cd", "evidence": {"detail": "injected fault observed failing and classified: Persona block not found in MEMORY.md", "ended_at": 1792234271.9475667, "error_class": "ENOENT", "injected_fault": "read with a poisoned persona id", "input_digest": "320c851fa76bd033c2762d99997e960c473d847102861b0607fe20dcedb4a4cf", "output_digest": "04a15611da0c41d536ad2f60cec590b47cefde420c57ef5a0b1320b456259f07", "started_at": 1792234271.9474933}, "kind": "tool", "observed_at": 1792234271.947576, "path": "tool:read_persona", "phase": "failure_injection", "prev_digest": "ec59b876a8744730382f969091ac6476b352c6ca13f3f3b4fb61d948dc049a48", "receipt_id": "LP-1792234271-0c70b7f4", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_pass"}
|
||||
{"digest": "d30dd719ff7ebec8e501b97b1b92e3ef4561ec6b5b0d1da1ad1d9c8600129816", "evidence": {"detail": "recovery call failed: Persona block not found in MEMORY.md", "ended_at": 1792234271.9476554, "error_class": "TOOL_ERROR", "input_digest": "b32802c62eebca15317113d7e4455508c074d6b39ca1220681f186cc85ba0e92", "output_digest": "5af50857ea4c4a151574c3f98294fcf212543c665505234ad639ee6db240e0e5", "started_at": 1792234271.9475768}, "kind": "tool", "observed_at": 1792234271.9476628, "path": "tool:read_persona", "phase": "recovery", "prev_digest": "789e81a4b786da6e8796540641546c89f4d4d5e0749816e02a9856a3c8de11cd", "receipt_id": "LP-1792234271-f4722ff8", "recovery_of": "LP-1792234271-0c70b7f4", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_fail"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "26d6082067f78673af235c477e165e7428c1abaed6c724aa9acb75dcf56bfdf1", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233218.1275475, "error_class": null, "input_digest": "0790f22dd0ebca93ab4c0630d71f709a8b96b8983cf46dcf2c756f261ad78766", "output_digest": "a956aeb43b63c3181cab7b355029b25504be21b35790393d1254b3a22211aaec", "started_at": 1792233218.1275463}, "kind": "tool", "observed_at": 1792233218.1275628, "path": "tool:repo_map", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233218-35ab6f26", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "57cb11cfbbc2c006dcaef2f5f2f1fdb568bb455058350053a805a1b5b7decc82", "evidence": {"detail": "repo map over the sandbox dir", "ended_at": 1792233218.1394017, "error_class": null, "input_digest": "9782f79dae45c6429d29752ae557543830346f589ab017d7268f2e2137784fba", "output_digest": "4b122ae6968f0a5738b15bd60c0e528f68507c08f94bdc9936c67cc25f3d9be6", "started_at": 1792233218.1275637}, "kind": "tool", "observed_at": 1792233218.13945, "path": "tool:repo_map", "phase": "minimal_call", "prev_digest": "26d6082067f78673af235c477e165e7428c1abaed6c724aa9acb75dcf56bfdf1", "receipt_id": "LP-1792233218-e1dd9f1d", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "0a65dce068fc4d401e8da2d188e78527e630f8528f5d9214732a1586b44569ce", "evidence": {"detail": "injected fault observed failing and classified: Access denied: /etc/__live_proof_probe__", "ended_at": 1792233218.1397295, "error_class": "PERMISSION", "injected_fault": "map a denied (blocked) path", "input_digest": "da7f98628de3675f20fa30e701640d560b051636b779ab363ec14b605d9eaa2d", "output_digest": "2a693e44c88ccbf8c06d8ae95445f3b47e76165a8880d77fc29c817ba88ad4e0", "started_at": 1792233218.1394515}, "kind": "tool", "observed_at": 1792233218.139758, "path": "tool:repo_map", "phase": "failure_injection", "prev_digest": "57cb11cfbbc2c006dcaef2f5f2f1fdb568bb455058350053a805a1b5b7decc82", "receipt_id": "LP-1792233218-1ef149da", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_pass"}
|
||||
{"digest": "0adbb9e06ad052a740f8c9c888590f7627f5e9500c67d2570f0ddee5d5e81af5", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233218.1519997, "error_class": null, "input_digest": "643bf7bee5ea37f7913c9e00b0586dad9b347b11bda8ae5fc956d8352a2d66d9", "output_digest": "0d608c628fa58c58d9ffafe6fdb3563d852191b07d0d7f8549747bfdc8328cf2", "started_at": 1792233218.139759}, "kind": "tool", "observed_at": 1792233218.152042, "path": "tool:repo_map", "phase": "recovery", "prev_digest": "0a65dce068fc4d401e8da2d188e78527e630f8528f5d9214732a1586b44569ce", "receipt_id": "LP-1792233218-8d68fd76", "recovery_of": "LP-1792233218-1ef149da", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "f988d74fddfc00914cb1cd0aa4baaffe6b4dbb00f1f259f2a3556ed43ecd38d0", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234271.916472, "error_class": null, "input_digest": "0790f22dd0ebca93ab4c0630d71f709a8b96b8983cf46dcf2c756f261ad78766", "output_digest": "a956aeb43b63c3181cab7b355029b25504be21b35790393d1254b3a22211aaec", "started_at": 1792234271.9164703}, "kind": "tool", "observed_at": 1792234271.916494, "path": "tool:repo_map", "phase": "auth_check", "prev_digest": "0adbb9e06ad052a740f8c9c888590f7627f5e9500c67d2570f0ddee5d5e81af5", "receipt_id": "LP-1792234271-0ed6a366", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "a967246c3250c7c21a2fa132d0c2302d18900e91efe62f48cdae0b7e22703276", "evidence": {"detail": "repo map over the sandbox dir", "ended_at": 1792234271.9286356, "error_class": null, "input_digest": "9782f79dae45c6429d29752ae557543830346f589ab017d7268f2e2137784fba", "output_digest": "4b122ae6968f0a5738b15bd60c0e528f68507c08f94bdc9936c67cc25f3d9be6", "started_at": 1792234271.9164946}, "kind": "tool", "observed_at": 1792234271.9286685, "path": "tool:repo_map", "phase": "minimal_call", "prev_digest": "f988d74fddfc00914cb1cd0aa4baaffe6b4dbb00f1f259f2a3556ed43ecd38d0", "receipt_id": "LP-1792234271-871ed83d", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "04115700625c1803b008dbdc516e880b09bbea0f8dab97991c45861269b9d59d", "evidence": {"detail": "injected fault observed failing and classified: Access denied: /etc/__live_proof_probe__", "ended_at": 1792234271.9288743, "error_class": "PERMISSION", "injected_fault": "map a denied (blocked) path", "input_digest": "da7f98628de3675f20fa30e701640d560b051636b779ab363ec14b605d9eaa2d", "output_digest": "2a693e44c88ccbf8c06d8ae95445f3b47e76165a8880d77fc29c817ba88ad4e0", "started_at": 1792234271.9286697}, "kind": "tool", "observed_at": 1792234271.9288864, "path": "tool:repo_map", "phase": "failure_injection", "prev_digest": "a967246c3250c7c21a2fa132d0c2302d18900e91efe62f48cdae0b7e22703276", "receipt_id": "LP-1792234271-f4ec6daf", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_pass"}
|
||||
{"digest": "07334b7a23e20759ee483d72d463e9ecc115f3d78b6df2b9b2750fbff2061e2a", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234271.9392335, "error_class": null, "input_digest": "643bf7bee5ea37f7913c9e00b0586dad9b347b11bda8ae5fc956d8352a2d66d9", "output_digest": "0d608c628fa58c58d9ffafe6fdb3563d852191b07d0d7f8549747bfdc8328cf2", "started_at": 1792234271.9288874}, "kind": "tool", "observed_at": 1792234271.939266, "path": "tool:repo_map", "phase": "recovery", "prev_digest": "04115700625c1803b008dbdc516e880b09bbea0f8dab97991c45861269b9d59d", "receipt_id": "LP-1792234271-04134317", "recovery_of": "LP-1792234271-f4ec6daf", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "366b191bbb9012a3f75e61df158542e74fa9857a447760140a9cbeba5ae65d50", "evidence": {"detail": "approval gate observed: prepared + approved (single-use token, success=True)", "ended_at": 1792233218.1540852, "error_class": null, "input_digest": "49d90e0983e294ccd1ea057bbb893b23182a927655f52fddb4f5e77f588a253b", "output_digest": "4b3d8628e3c7bc148cc79bf39a99261673d100d8db6da6e863503cb31a21174c", "started_at": 1792233218.1531603}, "kind": "tool", "observed_at": 1792233218.154104, "path": "tool:run_command", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233218-f072dc9f", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "2debd2be91de4fb8f60ccf35d198ac04d5bcfeb8c4996cedde46b2e82031537c", "evidence": {"detail": "run 'true' in the sandbox", "ended_at": 1792233218.154187, "error_class": null, "input_digest": "1ffa77546d1f6a6536cf4c4522f467457980bfc5715ab515b3700ec46a5371b1", "output_digest": "465a4ff846b0a5e119a24b345ddb2e4ff2252074fbeea6c90980f38d54f24999", "started_at": 1792233218.154105}, "kind": "tool", "observed_at": 1792233218.1541965, "path": "tool:run_command", "phase": "minimal_call", "prev_digest": "366b191bbb9012a3f75e61df158542e74fa9857a447760140a9cbeba5ae65d50", "receipt_id": "LP-1792233218-124349ac", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "7b07c348481bab6e2dd288025f9c82c130b0f7ff01e4a96a3a99e4ae22c08c02", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792233218.1542382, "error_class": null, "injected_fault": "run a command that cannot exist", "input_digest": "2544c53dc35795034c22bd7a85354d66811e4f13d114754a6bf289298af409e2", "output_digest": "cfe82ec11b5df99c24a72c1052f35e87c2dd8e22f62c3c4786a4044c1b6ee543", "started_at": 1792233218.154197}, "kind": "tool", "observed_at": 1792233218.1542466, "path": "tool:run_command", "phase": "failure_injection", "prev_digest": "2debd2be91de4fb8f60ccf35d198ac04d5bcfeb8c4996cedde46b2e82031537c", "receipt_id": "LP-1792233218-1e5d0fb0", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_fail"}
|
||||
{"digest": "a6e36897d6b34bf7b2a30bc89343bd874dc2f3a023031508f888aab7bf792113", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233218.1543016, "error_class": null, "input_digest": "83da34ae1e57418c63bf0e7576ce050d3545379af9e8abad5edc16d4d2ec9584", "output_digest": "7a7a73ad197502e120fc0797ad7a08c64d93cca2f4ab7b14873f1f48dcd8ead5", "started_at": 1792233218.1542478}, "kind": "tool", "observed_at": 1792233218.1543105, "path": "tool:run_command", "phase": "recovery", "prev_digest": "7b07c348481bab6e2dd288025f9c82c130b0f7ff01e4a96a3a99e4ae22c08c02", "receipt_id": "LP-1792233218-65a22f4e", "recovery_of": "LP-1792233218-1e5d0fb0", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "911f94ba9b0df859d07b341d1b78b76d8abe5845a879fa9b3b12daf796153c17", "evidence": {"detail": "approval gate observed: prepared + approved (single-use token, success=True)", "ended_at": 1792234271.9458697, "error_class": null, "input_digest": "49d90e0983e294ccd1ea057bbb893b23182a927655f52fddb4f5e77f588a253b", "output_digest": "4b3d8628e3c7bc148cc79bf39a99261673d100d8db6da6e863503cb31a21174c", "started_at": 1792234271.9405448}, "kind": "tool", "observed_at": 1792234271.9459004, "path": "tool:run_command", "phase": "auth_check", "prev_digest": "a6e36897d6b34bf7b2a30bc89343bd874dc2f3a023031508f888aab7bf792113", "receipt_id": "LP-1792234271-0db8e7cf", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "2c4769303e2aa75ebcf61e35596cfe1b9513233bdd27ee88760816f76f231b0e", "evidence": {"detail": "run 'true' in the sandbox", "ended_at": 1792234271.9460104, "error_class": null, "input_digest": "1ffa77546d1f6a6536cf4c4522f467457980bfc5715ab515b3700ec46a5371b1", "output_digest": "465a4ff846b0a5e119a24b345ddb2e4ff2252074fbeea6c90980f38d54f24999", "started_at": 1792234271.9459016}, "kind": "tool", "observed_at": 1792234271.9460177, "path": "tool:run_command", "phase": "minimal_call", "prev_digest": "911f94ba9b0df859d07b341d1b78b76d8abe5845a879fa9b3b12daf796153c17", "receipt_id": "LP-1792234271-8d1b2ed4", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "88e30f19ca966294831068c6db9b1beb51aa1046769cfe9b3224af3fb12c4cc5", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792234271.9460506, "error_class": null, "injected_fault": "run a command that cannot exist", "input_digest": "2544c53dc35795034c22bd7a85354d66811e4f13d114754a6bf289298af409e2", "output_digest": "cfe82ec11b5df99c24a72c1052f35e87c2dd8e22f62c3c4786a4044c1b6ee543", "started_at": 1792234271.9460185}, "kind": "tool", "observed_at": 1792234271.9460568, "path": "tool:run_command", "phase": "failure_injection", "prev_digest": "2c4769303e2aa75ebcf61e35596cfe1b9513233bdd27ee88760816f76f231b0e", "receipt_id": "LP-1792234271-b8c8b641", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_fail"}
|
||||
{"digest": "ba1743459fe7b789db766c67e4403827e0246c8ca70979a3867cfa6313b6363b", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234271.9460995, "error_class": null, "input_digest": "83da34ae1e57418c63bf0e7576ce050d3545379af9e8abad5edc16d4d2ec9584", "output_digest": "7a7a73ad197502e120fc0797ad7a08c64d93cca2f4ab7b14873f1f48dcd8ead5", "started_at": 1792234271.9460578}, "kind": "tool", "observed_at": 1792234271.9461045, "path": "tool:run_command", "phase": "recovery", "prev_digest": "88e30f19ca966294831068c6db9b1beb51aa1046769cfe9b3224af3fb12c4cc5", "receipt_id": "LP-1792234271-6a73bbc5", "recovery_of": "LP-1792234271-b8c8b641", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "df588d3421befe71b5ac886f2824da33ca7d7426c9ee43b15a7afbcaa0af1bbe", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792233217.9484146, "error_class": null, "input_digest": "19d015afba2cbf1f7a51eaf490735460bd6a6c3ded7341b12ad3b94ede67c780", "output_digest": "303abb215d3f40512244dbd0c3b665e28c750c46816b1c650ec262abae82e78b", "started_at": 1792233217.9484136}, "kind": "tool", "observed_at": 1792233217.9484262, "path": "tool:run_tests", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233217-80631460", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "f846d6d1ef4d93782c8cc1ba181a24cb03885bc997e617f392f811657cbc1bb7", "evidence": {"detail": "minimal call succeeded", "ended_at": 1792233217.9484851, "error_class": null, "input_digest": "e323635b365eced17b9a4fcbe537afd4d3daaa7a4158a1af5e3284b7c61d917d", "output_digest": "17823a8d57195139f2cfa31e9d510b53580dec9541dd92ecf428d80b955a1147", "started_at": 1792233217.9484267}, "kind": "tool", "observed_at": 1792233217.9484928, "path": "tool:run_tests", "phase": "minimal_call", "prev_digest": "df588d3421befe71b5ac886f2824da33ca7d7426c9ee43b15a7afbcaa0af1bbe", "receipt_id": "LP-1792233217-9798c01c", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "f8d9478a060c37aafcab156ca9f2824cfae46fae79fad0292dd04a9dcaf3b8ff", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792233217.9485211, "error_class": null, "injected_fault": "nonexistent path", "input_digest": "ca6d6db9e9d29c7e583c2af686990d7fc2c80be0cceea54eb58f92fca48f0ff5", "output_digest": "0763ca1de8fe2e9a3935ad5ae0178c877390d8da684d25b0cedb3505ebdb0763", "started_at": 1792233217.9484932}, "kind": "tool", "observed_at": 1792233217.9485283, "path": "tool:run_tests", "phase": "failure_injection", "prev_digest": "f846d6d1ef4d93782c8cc1ba181a24cb03885bc997e617f392f811657cbc1bb7", "receipt_id": "LP-1792233217-dfeb507d", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_fail"}
|
||||
{"digest": "a352e274dd321ccc2be4221b12a2bbcf451d3df7f77612afc889be24cb6dfdf7", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233217.948574, "error_class": null, "input_digest": "d7a369763d2def0be6bad748cb9522203f4886d454f04700e3d59044c00db31d", "output_digest": "c27aabb83792dee21c4f793c1fe885585000e813011582daec5e1a9c2a0ecd68", "started_at": 1792233217.9485292}, "kind": "tool", "observed_at": 1792233217.9485805, "path": "tool:run_tests", "phase": "recovery", "prev_digest": "f8d9478a060c37aafcab156ca9f2824cfae46fae79fad0292dd04a9dcaf3b8ff", "receipt_id": "LP-1792233217-6cf9cab8", "recovery_of": "LP-1792233217-dfeb507d", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "65d34ffc4ecff6aee19297c6f5598a7ea8efc7540c72dd0c904bcd44c79049dc", "evidence": {"detail": "read-only boundary observed: no approval required", "ended_at": 1792234271.6162615, "error_class": null, "input_digest": "19d015afba2cbf1f7a51eaf490735460bd6a6c3ded7341b12ad3b94ede67c780", "output_digest": "303abb215d3f40512244dbd0c3b665e28c750c46816b1c650ec262abae82e78b", "started_at": 1792234271.6162608}, "kind": "tool", "observed_at": 1792234271.616277, "path": "tool:run_tests", "phase": "auth_check", "prev_digest": "a352e274dd321ccc2be4221b12a2bbcf451d3df7f77612afc889be24cb6dfdf7", "receipt_id": "LP-1792234271-ded1602f", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "deb3e7cffc023331014a749817ef38a05cbdc7fbb5d136b7fb8a0c52ae9ff883", "evidence": {"detail": "minimal call succeeded", "ended_at": 1792234271.6163225, "error_class": null, "input_digest": "e323635b365eced17b9a4fcbe537afd4d3daaa7a4158a1af5e3284b7c61d917d", "output_digest": "17823a8d57195139f2cfa31e9d510b53580dec9541dd92ecf428d80b955a1147", "started_at": 1792234271.6162777}, "kind": "tool", "observed_at": 1792234271.6163285, "path": "tool:run_tests", "phase": "minimal_call", "prev_digest": "65d34ffc4ecff6aee19297c6f5598a7ea8efc7540c72dd0c904bcd44c79049dc", "receipt_id": "LP-1792234271-a7c02791", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "9cbf71ec9044f26587cef983b2bce9046e29d236107d371bd1b9ebf5d0bf255a", "evidence": {"detail": "injected fault did NOT produce a failure", "ended_at": 1792234271.616351, "error_class": null, "injected_fault": "nonexistent path", "input_digest": "ca6d6db9e9d29c7e583c2af686990d7fc2c80be0cceea54eb58f92fca48f0ff5", "output_digest": "0763ca1de8fe2e9a3935ad5ae0178c877390d8da684d25b0cedb3505ebdb0763", "started_at": 1792234271.616329}, "kind": "tool", "observed_at": 1792234271.6163566, "path": "tool:run_tests", "phase": "failure_injection", "prev_digest": "deb3e7cffc023331014a749817ef38a05cbdc7fbb5d136b7fb8a0c52ae9ff883", "receipt_id": "LP-1792234271-0a3c79e1", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_fail"}
|
||||
{"digest": "c891475f3a3446708ac467751b10776983efda4705106efc8c695666aeea0713", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234271.6164002, "error_class": null, "input_digest": "d7a369763d2def0be6bad748cb9522203f4886d454f04700e3d59044c00db31d", "output_digest": "c27aabb83792dee21c4f793c1fe885585000e813011582daec5e1a9c2a0ecd68", "started_at": 1792234271.616357}, "kind": "tool", "observed_at": 1792234271.616405, "path": "tool:run_tests", "phase": "recovery", "prev_digest": "9cbf71ec9044f26587cef983b2bce9046e29d236107d371bd1b9ebf5d0bf255a", "receipt_id": "LP-1792234271-87133897", "recovery_of": "LP-1792234271-0a3c79e1", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1,8 +0,0 @@
|
||||
{"digest": "5141998f46b6c0b0d25952d180c431b9c50c9a2dc36deb0e30b09cf9dbffe86c", "evidence": {"detail": "approval gate observed: executed without confirmation boundary", "ended_at": 1792233217.9307585, "error_class": null, "input_digest": "3f768c19ec87d469161bbf767efec77c98005056ab25f77e2560c0597f784f95", "output_digest": "6c1b4cc94145a09709cc16ed11b1f904fc56b41882afc9db2a4e12de68f0fb79", "started_at": 1792233217.9291263}, "kind": "tool", "observed_at": 1792233217.9307735, "path": "tool:write_file", "phase": "auth_check", "prev_digest": null, "receipt_id": "LP-1792233217-0d73d2b3", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 0, "verdict": "observed_pass"}
|
||||
{"digest": "8c48b38dd602a7a303efde8b86ea3df4f39467abaefff13daaa893d16071cb4a", "evidence": {"detail": "write a one-line sandbox file", "ended_at": 1792233217.9319632, "error_class": null, "input_digest": "4dfc60346c1e233cf78d9a367091a1465624511f661a1462636cb3d8094ca7ba", "output_digest": "ed8f668534dd0c78f43c2045d6fa734f92af7db5fd0eac48a8fd75969311185b", "started_at": 1792233217.9307745}, "kind": "tool", "observed_at": 1792233217.9319773, "path": "tool:write_file", "phase": "minimal_call", "prev_digest": "5141998f46b6c0b0d25952d180c431b9c50c9a2dc36deb0e30b09cf9dbffe86c", "receipt_id": "LP-1792233217-502c81ab", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 1, "verdict": "observed_pass"}
|
||||
{"digest": "eba68d31e4b92828a6ec98ad2d99f5a1c3491fcc6af342a2b8f513d346fe82a5", "evidence": {"detail": "injected fault observed failing and classified: Access denied: /proc/__live_proof_write_probe__ is in a protected directory (/proc). This directory is always blocked, e", "ended_at": 1792233217.9321306, "error_class": "PATH_DENIED", "injected_fault": "write outside allowed write dirs", "input_digest": "0b46e0702bc032687655f69c97ec430d70c83fc66940c56f100c1aa48c41a68d", "output_digest": "326b4c20883bd51e34f78ad642c79b75055aeac2dd87c105889c076d4cccf9a4", "started_at": 1792233217.931978}, "kind": "tool", "observed_at": 1792233217.932141, "path": "tool:write_file", "phase": "failure_injection", "prev_digest": "8c48b38dd602a7a303efde8b86ea3df4f39467abaefff13daaa893d16071cb4a", "receipt_id": "LP-1792233217-380fa392", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 2, "verdict": "observed_pass"}
|
||||
{"digest": "77745eff94f25e9dcaa6dde6d7659c9584b19034b47a3c3adde28a069a705165", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792233217.9330544, "error_class": null, "input_digest": "33b285bd6f9455702eb5f03029fd24a882ffe2bb51c76bb0959510fe00efd537", "output_digest": "11096cc3886527e4a8c05d487c644125e02389b4f00a6313a24f1571ff16ce71", "started_at": 1792233217.9321415}, "kind": "tool", "observed_at": 1792233217.9330647, "path": "tool:write_file", "phase": "recovery", "prev_digest": "eba68d31e4b92828a6ec98ad2d99f5a1c3491fcc6af342a2b8f513d346fe82a5", "receipt_id": "LP-1792233217-977b2aeb", "recovery_of": "LP-1792233217-380fa392", "run_id": "RUN-1792233217-0033a4", "schema": 1, "seq": 3, "verdict": "observed_pass"}
|
||||
{"digest": "6466d4e589d526d86328500254d7f701620a8dca8c71d5facfa01a6291eb00a2", "evidence": {"detail": "approval gate observed: executed without confirmation boundary", "ended_at": 1792234271.5982952, "error_class": null, "input_digest": "3f768c19ec87d469161bbf767efec77c98005056ab25f77e2560c0597f784f95", "output_digest": "6c1b4cc94145a09709cc16ed11b1f904fc56b41882afc9db2a4e12de68f0fb79", "started_at": 1792234271.5974405}, "kind": "tool", "observed_at": 1792234271.598307, "path": "tool:write_file", "phase": "auth_check", "prev_digest": "77745eff94f25e9dcaa6dde6d7659c9584b19034b47a3c3adde28a069a705165", "receipt_id": "LP-1792234271-af5a8bcd", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 4, "verdict": "observed_pass"}
|
||||
{"digest": "21eeb19cda896df990b42900bcc37e0334cae248d52094d417764e8596e773b3", "evidence": {"detail": "write a one-line sandbox file", "ended_at": 1792234271.5991473, "error_class": null, "input_digest": "4dfc60346c1e233cf78d9a367091a1465624511f661a1462636cb3d8094ca7ba", "output_digest": "ed8f668534dd0c78f43c2045d6fa734f92af7db5fd0eac48a8fd75969311185b", "started_at": 1792234271.5983076}, "kind": "tool", "observed_at": 1792234271.5991564, "path": "tool:write_file", "phase": "minimal_call", "prev_digest": "6466d4e589d526d86328500254d7f701620a8dca8c71d5facfa01a6291eb00a2", "receipt_id": "LP-1792234271-f7ece735", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 5, "verdict": "observed_pass"}
|
||||
{"digest": "a2292e905a01e4aa8fe60f10806d6235bf36aa0cdba1a804f461129e33839ebe", "evidence": {"detail": "injected fault observed failing and classified: Access denied: /proc/__live_proof_write_probe__ is in a protected directory (/proc). This directory is always blocked, e", "ended_at": 1792234271.5992904, "error_class": "PATH_DENIED", "injected_fault": "write outside allowed write dirs", "input_digest": "0b46e0702bc032687655f69c97ec430d70c83fc66940c56f100c1aa48c41a68d", "output_digest": "326b4c20883bd51e34f78ad642c79b75055aeac2dd87c105889c076d4cccf9a4", "started_at": 1792234271.5991573}, "kind": "tool", "observed_at": 1792234271.5993023, "path": "tool:write_file", "phase": "failure_injection", "prev_digest": "21eeb19cda896df990b42900bcc37e0334cae248d52094d417764e8596e773b3", "receipt_id": "LP-1792234271-942365c4", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 6, "verdict": "observed_pass"}
|
||||
{"digest": "1d7332a71682b576a51ae75ff24ca857736ad596e4ad4323908b93a8f35ed439", "evidence": {"detail": "minimal call succeeded after injected fault", "ended_at": 1792234271.600123, "error_class": null, "input_digest": "33b285bd6f9455702eb5f03029fd24a882ffe2bb51c76bb0959510fe00efd537", "output_digest": "11096cc3886527e4a8c05d487c644125e02389b4f00a6313a24f1571ff16ce71", "started_at": 1792234271.599303}, "kind": "tool", "observed_at": 1792234271.6001318, "path": "tool:write_file", "phase": "recovery", "prev_digest": "a2292e905a01e4aa8fe60f10806d6235bf36aa0cdba1a804f461129e33839ebe", "receipt_id": "LP-1792234271-cdf23f1f", "recovery_of": "LP-1792234271-942365c4", "run_id": "RUN-1792234271-c44554", "schema": 1, "seq": 7, "verdict": "observed_pass"}
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof edited line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1 +0,0 @@
|
||||
live-proof probe line
|
||||
@@ -1,196 +0,0 @@
|
||||
"""completion_file.py — REAL_ASYNC_BRIDGE: durable completion-file delivery
|
||||
with inotify wake. Replaces FIFO+lease for process-wait delivery.
|
||||
|
||||
DESIGN:
|
||||
Producer (bridge/daemon/notify):
|
||||
1. Validates operation identity
|
||||
2. Atomically writes a completion file:
|
||||
resumes/<operation_id>.done
|
||||
containing the event/verdict JSON (write to tmp, rename for atomicity)
|
||||
|
||||
Waiter (ci_event_wait / any continuation consumer):
|
||||
1. Checks if the completion file already exists (event-before-waiter)
|
||||
2. If not: registers inotify watch on the resumes/ directory
|
||||
3. Re-checks after watch registration (closes the registration race)
|
||||
4. Blocks in kernel on inotify — zero CPU, zero timer
|
||||
5. On file-created event: reads the file, verifies authority, returns
|
||||
|
||||
The event is a WAKE SIGNAL. Authority still decides terminal truth.
|
||||
The completion file is durable — survives daemon and waiter restarts.
|
||||
|
||||
WHY NOT FIFO:
|
||||
FIFO is one-shot (token consumed), requires lease management, and
|
||||
fails under session-busy guards. A completion file is atomic, durable,
|
||||
and works across restarts. inotify wakes the process with zero CPU
|
||||
cost and zero latency between file creation and wake.
|
||||
|
||||
WHY NOT POLLING:
|
||||
Polling introduces an arbitrary interval. inotify is a kernel callback
|
||||
that fires exactly when the filesystem changes — the latency tracks
|
||||
the real completion event, not a configured interval.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import struct
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _wr_bridge() -> Path:
|
||||
# WR_ROOT = daemon/infra convention; RUNE_WR_ROOT = repo/scripts
|
||||
# convention. Both honored (WR_ROOT wins) so the daemon, the waiter,
|
||||
# and any isolated test root always resolve the SAME directory.
|
||||
root = os.environ.get("WR_ROOT") or os.environ.get("RUNE_WR_ROOT") or "/mnt/workdrive/wr-bridge"
|
||||
return Path(root)
|
||||
|
||||
|
||||
def _resumes_dir() -> Path:
|
||||
return _wr_bridge() / "resumes"
|
||||
|
||||
|
||||
# inotify constants (Linux)
|
||||
IN_CREATE = 0x100
|
||||
IN_CLOSE_WRITE = 0x8
|
||||
IN_MOVED_TO = 0x80
|
||||
|
||||
|
||||
def completion_path(operation_id: str) -> Path:
|
||||
return _resumes_dir() / f"{operation_id}.done"
|
||||
|
||||
|
||||
def write_completion(operation_id: str, event: dict) -> Path:
|
||||
"""Atomically write a durable completion file. The rename(2) ensures
|
||||
the waiter never sees a partial file."""
|
||||
_resumes_dir().mkdir(parents=True, exist_ok=True)
|
||||
target = completion_path(operation_id)
|
||||
tmp = target.parent / f".{target.name}.tmp.{os.getpid()}"
|
||||
tmp.write_text(json.dumps(event, sort_keys=True) + "\n")
|
||||
os.rename(str(tmp), str(target)) # atomic on same filesystem
|
||||
os.chmod(str(target), 0o666) # cross-user bridge
|
||||
return target
|
||||
|
||||
|
||||
def read_completion(operation_id: str) -> dict | None:
|
||||
"""Read a durable completion file. Returns None if absent."""
|
||||
p = completion_path(operation_id)
|
||||
if not p.is_file():
|
||||
return None
|
||||
try:
|
||||
return json.loads(p.read_text())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def completion_exists(operation_id: str) -> bool:
|
||||
return completion_path(operation_id).is_file()
|
||||
|
||||
|
||||
def _inotify_init_and_add_watch(directory: str) -> int:
|
||||
"""Create an inotify instance and add a watch on the directory.
|
||||
Returns the inotify fd."""
|
||||
libc = __import__("ctypes").CDLL(None, use_errno=True)
|
||||
fd = libc.inotify_init()
|
||||
if fd < 0:
|
||||
raise OSError("inotify_init failed")
|
||||
# IN_CREATE | IN_MOVED_TO: catch both direct creation and atomic rename
|
||||
mask = IN_CREATE | IN_MOVED_TO
|
||||
wd = libc.inotify_add_watch(fd, directory.encode(), mask)
|
||||
if wd < 0:
|
||||
os.close(fd)
|
||||
raise OSError(f"inotify_add_watch failed for {directory}")
|
||||
return fd
|
||||
|
||||
|
||||
def _inotify_read(fd: int, timeout_secs: float | None = None) -> list[str]:
|
||||
"""Read pending inotify events. Returns list of filenames that were
|
||||
created/moved-to. Blocks until at least one event or timeout."""
|
||||
poll = select.poll()
|
||||
poll.register(fd, select.POLLIN)
|
||||
events = []
|
||||
buf = os.read(fd, 4096)
|
||||
pos = 0
|
||||
while pos < len(buf):
|
||||
# struct: wd(i32), mask(u32), cookie(u32), name_len(u32)
|
||||
wd, mask, cookie, name_len = struct.unpack_from("iIII", buf, pos)
|
||||
pos += 16
|
||||
name = buf[pos : pos + name_len].rstrip(b"\x00").decode("utf-8", "replace")
|
||||
pos += name_len
|
||||
if mask & (IN_CREATE | IN_MOVED_TO):
|
||||
events.append(name)
|
||||
return events
|
||||
|
||||
|
||||
def wait_for_completion(operation_id: str, *, timeout_secs: float | None = None, poll_callback=None) -> dict | None:
|
||||
"""Block until a completion file exists for this operation.
|
||||
|
||||
Uses inotify for zero-latency, zero-CPU wake. Falls back to
|
||||
polling_callback (if provided) when inotify is unavailable.
|
||||
|
||||
The completion file is a WAKE SIGNAL. The caller must still verify
|
||||
canonical authority after reading it.
|
||||
|
||||
Returns the event dict from the completion file, or None on timeout.
|
||||
"""
|
||||
target = completion_path(operation_id)
|
||||
|
||||
# 1. check-before-watch (event may have arrived before we started)
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
|
||||
_resumes_dir().mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
fd = _inotify_init_and_add_watch(str(_resumes_dir()))
|
||||
except OSError:
|
||||
# inotify unavailable: fall back to polling callback
|
||||
if poll_callback:
|
||||
return poll_callback(operation_id, timeout_secs)
|
||||
return None
|
||||
|
||||
try:
|
||||
# 2. re-check after watch registration (closes the race between
|
||||
# the initial check and the watch being active)
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
|
||||
# 3. block in kernel on inotify — zero CPU, exact latency
|
||||
import select as _select
|
||||
|
||||
poll = _select.poll()
|
||||
poll.register(fd, _select.POLLIN)
|
||||
deadline = (time.monotonic() + timeout_secs) if timeout_secs else None
|
||||
|
||||
while True:
|
||||
remaining = None
|
||||
if deadline is not None:
|
||||
remaining = max(0.1, deadline - time.monotonic())
|
||||
if remaining <= 0.1:
|
||||
return None # timeout
|
||||
events = poll.poll(remaining * 1000 if remaining else None)
|
||||
if not events:
|
||||
continue # poll timeout, re-check
|
||||
names = _inotify_read(fd)
|
||||
if target.name in names:
|
||||
# 4. file appeared — read it
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
# partial/corrupt: keep watching
|
||||
finally:
|
||||
os.close(fd)
|
||||
return None
|
||||
|
||||
|
||||
def clear_completion(operation_id: str) -> None:
|
||||
"""Remove a consumed completion file (idempotent cleanup)."""
|
||||
p = completion_path(operation_id)
|
||||
try:
|
||||
p.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
@@ -1,799 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""daemon.py — Gitea webhook receiver + reconciliation + resume driver (v0.2).
|
||||
|
||||
W/R-v0.2 invariants implemented here:
|
||||
A. SINGLE-FLIGHT PER SESSION — per-session lease (waitstore.acquire_lease,
|
||||
/proc-validated, atomic) + in-process inflight set. A second READY wait
|
||||
for the same session stays queued until the lease releases.
|
||||
B. BUSY SESSION PROTECTION — before spawning, /proc is scanned for a live
|
||||
`mimo run -s <sid>`-shaped process not owned by our lease. Found → skip
|
||||
launch (wait stays READY). No invented MiMo APIs; the lease is the
|
||||
conservative floor against W/R-created concurrency.
|
||||
C. ATOMIC SESSION LEASE — persisted {session_id, wait_id, pid, pgid,
|
||||
pid_starttime, acquired_at, state}; O_EXCL create, steal only on /proc
|
||||
evidence of a dead owner.
|
||||
D. PROCESS TREE OWNERSHIP — workers spawn with start_new_session=True (own
|
||||
process group). Watch timeout ⇒ SIGTERM group → bounded grace → SIGKILL
|
||||
group → verify zero surviving group members → release lease → truthful
|
||||
READY_TO_RETRY (never terminal-on-timeout).
|
||||
E. TIMEOUT IS NOT SUCCESS — rc==0 maps to verdict terminal
|
||||
(COMPLETED/FAILED); rc!=0 or unverifiable ⇒ RESUME_UNVERIFIED
|
||||
(not schedulable, operator-requeueable); timeout ⇒ READY_TO_RETRY.
|
||||
F. STALE RESUMING RECOVERY — on startup, every RESUMING record is
|
||||
inspected: live lease owner ⇒ adopted and monitored; otherwise ⇒
|
||||
RESUME_UNVERIFIED (+ stale lease released). Never auto-relaunched.
|
||||
G. MULTIPLE WAITS, SAME SESSION — schedulable() orders oldest-first; one
|
||||
continuation finishes before the next starts.
|
||||
H. PIDFILE CORRECTNESS — singleton enforced by flock on daemon.pid; the
|
||||
lock dies with the process, so stale pidfiles (incl. SIGKILL) cannot
|
||||
admit a second daemon.
|
||||
|
||||
The resume command is configurable (WR_RESUME_CMD, JSON list with
|
||||
{session_id}/{msg} placeholders) so the test harness can use a stub; the
|
||||
default is the real mechanism: `mimo run -s <sid> -m <model> <msg>`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import fcntl, json, os, signal, subprocess, sys, threading, time
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import waitstore
|
||||
import verify
|
||||
import completion_file # REAL_ASYNC_BRIDGE R3: durable completion-file delivery
|
||||
|
||||
RESUME_MODEL = "Verboo Code/glm-5.3-flash"
|
||||
RECONCILE_SECS = int(os.environ.get("WR_RECONCILE_SECS", "60"))
|
||||
RECONCILE_EVERY_TICKS = max(1, int(os.environ.get("WR_RECONCILE_TICKS", "3")))
|
||||
WAKE_MIN_AGE_SECS = float(os.environ.get("WR_WAKE_MIN_AGE_SECS", "5"))
|
||||
WAKE_RETRIES = int(os.environ.get("WR_WAKE_RETRIES", "3"))
|
||||
WAKE_RETRY_SECS = float(os.environ.get("WR_WAKE_RETRY_SECS", "4"))
|
||||
DRIVE_LOOP_SECS = int(os.environ.get("WR_DRIVE_SECS", "20"))
|
||||
# REAL_ASYNC_BRIDGE §4: a RESUMING wait older than this grace without
|
||||
# progress is an orphaned resume flow (thread died between lease acquire
|
||||
# and release). Must exceed the longest legitimate in-flight watch
|
||||
# (WATCH_SECS); default 3x. Env-overridable for deterministic tests.
|
||||
WATCH_SECS = int(os.environ.get("WR_WATCH_SECS", "600"))
|
||||
# REAL_ASYNC_BRIDGE §4: a RESUMING wait older than this grace without
|
||||
# progress is an orphaned resume flow (thread died between lease acquire
|
||||
# and release). Must exceed the longest legitimate in-flight watch
|
||||
# (WATCH_SECS); default 3x. Env-overridable for deterministic tests.
|
||||
ORPHAN_GRACE_SECS = int(os.environ.get("WR_ORPHAN_GRACE_SECS", str(3 * WATCH_SECS)))
|
||||
TERM_GRACE_SECS = int(os.environ.get("WR_TERM_GRACE_SECS", "10"))
|
||||
MAX_ATTEMPTS = int(os.environ.get("WR_MAX_ATTEMPTS", "3"))
|
||||
_WR_ROOT = Path(os.environ.get("WR_ROOT", "") or Path(__file__).resolve().parent)
|
||||
PIDFILE = _WR_ROOT / "daemon.pid"
|
||||
LOG = _WR_ROOT / "daemon.log"
|
||||
|
||||
DEFAULT_RESUME_CMD = ["mimo", "run", "-s", "{session_id}", "-m", RESUME_MODEL, "{msg}"]
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
with open(LOG, "a") as f:
|
||||
f.write(f"{time.strftime('%H:%M:%S')} {msg}\n")
|
||||
|
||||
|
||||
def resume_cmd(session_id: str, msg: str, wait_id: str = "", override: list | None = None) -> list[str]:
|
||||
"""T15 §15D: per-wait override wins; placeholders {session_id}/{msg}/
|
||||
{wait_id}. A synthetic controller session (Scaffolding stage driver)
|
||||
is not a MiMo session — the global default cannot resume it, which is
|
||||
exactly why fixed-sleep polling crept back into T12-T14."""
|
||||
if override:
|
||||
return [
|
||||
c.replace("{session_id}", session_id).replace("{msg}", msg).replace("{wait_id}", wait_id) for c in override
|
||||
]
|
||||
tmpl = os.environ.get("WR_RESUME_CMD")
|
||||
cmd = json.loads(tmpl) if tmpl else DEFAULT_RESUME_CMD
|
||||
return [c.replace("{session_id}", session_id).replace("{msg}", msg).replace("{wait_id}", wait_id) for c in cmd]
|
||||
|
||||
|
||||
# ── process-tree evidence & ownership ─────────────────────────────────────
|
||||
|
||||
|
||||
def pgid_members(pgid: int) -> list[int]:
|
||||
out = []
|
||||
for d in Path("/proc").iterdir():
|
||||
if not d.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
stat = (d / "stat").read_text()
|
||||
fields = stat.rsplit(")", 1)[1].split()
|
||||
# zombies: killed but not yet reaped. Counting them makes the
|
||||
# survivor check report the direct child we just KILLed before
|
||||
# communicate() reaps it (observed live 2026-09-12).
|
||||
if fields[0] == "Z":
|
||||
continue
|
||||
if int(fields[2]) == pgid:
|
||||
out.append(int(d.name))
|
||||
except (OSError, IndexError, ValueError):
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def terminate_tree(pgid: int) -> list[int]:
|
||||
"""TERM → bounded grace → KILL. Returns surviving members (must be [])."""
|
||||
try:
|
||||
os.killpg(pgid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
deadline = time.time() + TERM_GRACE_SECS
|
||||
while time.time() < deadline:
|
||||
if not pgid_members(pgid):
|
||||
return []
|
||||
time.sleep(0.2)
|
||||
try:
|
||||
os.killpg(pgid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
for _ in range(50): # up to 10s for KILL to land
|
||||
if not pgid_members(pgid):
|
||||
return []
|
||||
time.sleep(0.2)
|
||||
return pgid_members(pgid)
|
||||
|
||||
|
||||
def stray_worker_pids(session_id: str, exclude_pgids: set[int] = ()) -> list[int]:
|
||||
"""Live `mimo run -s <sid>`-shaped processes not owned by our workers."""
|
||||
out = []
|
||||
for d in Path("/proc").iterdir():
|
||||
if not d.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
parts = [p.decode(errors="replace") for p in (d / "cmdline").read_bytes().split(b"\0") if p]
|
||||
except OSError:
|
||||
continue
|
||||
if not parts:
|
||||
continue
|
||||
joined = " ".join(parts)
|
||||
if session_id not in joined or "-s" not in parts:
|
||||
continue
|
||||
if "mimo" not in joined.lower() and "@mimo-ai" not in joined.lower():
|
||||
continue
|
||||
try:
|
||||
if os.getpgid(int(d.name)) in exclude_pgids:
|
||||
continue
|
||||
except OSError:
|
||||
continue
|
||||
out.append(int(d.name))
|
||||
return out
|
||||
|
||||
|
||||
# ── verification → transition (unchanged authority model) ─────────────────
|
||||
|
||||
|
||||
def _authoritative_verify(rec: dict) -> tuple[str, dict]:
|
||||
"""Authoritative verification with a deterministic test seam.
|
||||
|
||||
Production (WR_VERIFY_STUB unset): real Gitea API via verify.py — the
|
||||
ONLY source of success/failure truth. Test (WR_VERIFY_STUB=path to
|
||||
JSON {"<run_ref-or-sha>": verdict, "default": verdict}): stubbed
|
||||
verdicts so invariant tests never touch the network. The stub is a
|
||||
test fixture, not an alternate authority — no production path sets it.
|
||||
"""
|
||||
stub_path = os.environ.get("WR_VERIFY_STUB", "")
|
||||
if stub_path:
|
||||
try:
|
||||
spec = json.loads(Path(stub_path).read_text())
|
||||
except Exception:
|
||||
return "pending", {"stub_error": "unreadable stub"}
|
||||
key = str(rec.get("run_ref", "") or rec.get("sha", ""))
|
||||
verdict = spec.get(key, spec.get("default", "pending"))
|
||||
return verdict, {"stub": True, "key": key}
|
||||
import verify
|
||||
|
||||
if rec.get("wait_kind") == "RELEASE_DEPLOY":
|
||||
return verify.verify_release_deploy(rec.get("sha", ""), rec.get("repo_root", ""), rec.get("deployed_link", ""))
|
||||
return verify.verify_run(rec.get("run_ref", ""), rec.get("sha", ""), rec.get("expected", "success"))
|
||||
|
||||
|
||||
def try_transition(rec: dict, event: dict | None = None) -> dict:
|
||||
if rec["status"] != "WAITING_EXTERNAL":
|
||||
return rec
|
||||
if rec.get("wait_kind") == "PROCESS_EVENT":
|
||||
# T15 §15D: the matching event IS the authority. The notifier
|
||||
# wrapper observed the real process exit; exit_code decides the
|
||||
# verdict. No Gitea lookup applies. A duplicate event never
|
||||
# reaches here (match applies only to WAITING_EXTERNAL waits).
|
||||
if not event:
|
||||
return rec # no event → nothing authoritative happened
|
||||
try:
|
||||
exit_code = int(event.get("exit_code"))
|
||||
except (TypeError, ValueError):
|
||||
exit_code = None
|
||||
if exit_code is None:
|
||||
return rec # malformed event: stay WAITING (15E hostile case)
|
||||
verdict = "success" if exit_code == 0 else "failure"
|
||||
evidence = {"process_event": True, "exit_code": exit_code, "operation_id": event.get("run_ref", "")}
|
||||
log(f"verify {rec['wait_id']}: {verdict} {json.dumps(evidence)[:120]}")
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["verdict"] = verdict
|
||||
if verdict == "failure":
|
||||
rec["failure_evidence"] = evidence
|
||||
rec["process_event_evidence"] = evidence
|
||||
waitstore.save(rec)
|
||||
return rec
|
||||
verdict, evidence = _authoritative_verify(rec)
|
||||
log(f"verify {rec['wait_id']}: {verdict} {json.dumps(evidence)[:120]}")
|
||||
if verdict == "success":
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["verdict"] = "success"
|
||||
elif verdict == "failure":
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["verdict"] = "failure"
|
||||
rec["failure_evidence"] = verify.failed_job_evidence(rec.get("run_ref", ""))
|
||||
else:
|
||||
return rec # pending/unknown → stay WAITING
|
||||
waitstore.save(rec)
|
||||
return rec
|
||||
|
||||
|
||||
def continuation_msg(rec: dict) -> str:
|
||||
c = rec["continuation"]
|
||||
if rec.get("verdict") == "success":
|
||||
return (
|
||||
f"External wait satisfied (wait {rec['wait_id']}).\n"
|
||||
f"Original objective: {c.get('objective', '')}\n"
|
||||
f"Waited for: {c.get('waited_for', '')}\n"
|
||||
f"Authoritative result: workflow success verified via Gitea API.\n"
|
||||
f"SHA/run: {rec.get('sha', '')}/{rec.get('run_ref', '')}\n"
|
||||
f"Verify: {c.get('verify_when_woken', '')}\n"
|
||||
f"Continue from: {c.get('on_success', '')}"
|
||||
)
|
||||
ev = json.dumps(rec.get("failure_evidence", {}))[:400]
|
||||
return (
|
||||
f"External wait finished with FAILURE (wait {rec['wait_id']}).\n"
|
||||
f"Original objective: {c.get('objective', '')}\n"
|
||||
f"Authoritative result: workflow FAILURE verified via Gitea API.\n"
|
||||
f"Failed job evidence: {ev}\n"
|
||||
f"SHA/run: {rec.get('sha', '')}/{rec.get('run_ref', '')}\n"
|
||||
f"Do NOT treat this as success. Continue from: {c.get('on_failure', '')}"
|
||||
)
|
||||
|
||||
|
||||
# ── resume driver (single-flight, tree-owned) ─────────────────────────────
|
||||
|
||||
|
||||
def resume(rec: dict) -> None:
|
||||
"""Run one continuation to completion. Runs on a dedicated worker thread.
|
||||
Preconditions: caller verified no inflight worker for this session."""
|
||||
sid, wid = rec["session_id"], rec["wait_id"]
|
||||
# REAL_ASYNC_BRIDGE §2 invariant: a successful authority-verified
|
||||
# operation completion must NEVER be overwritten or downgraded by a
|
||||
# legacy session/FIFO delivery failure. Terminal operation state
|
||||
# (COMPLETED/FAILED — stamped by the waiter's own authority
|
||||
# verification) is owned by the OPERATION domain; this function owns
|
||||
# only SESSION-DELIVERY. Re-read from disk: the in-memory copy may be
|
||||
# stale relative to the waiter's terminal stamp.
|
||||
fresh = waitstore.load(wid)
|
||||
if fresh and fresh.get("status") in waitstore.TERMINAL:
|
||||
log(
|
||||
f"resume {wid}: skipped — operation already terminal "
|
||||
f"({fresh['status']}); delivery worker never overrides "
|
||||
f"operation truth"
|
||||
)
|
||||
return
|
||||
rec = fresh or rec
|
||||
msg = continuation_msg(rec)
|
||||
|
||||
# B: busy-session guard before touching the lease
|
||||
strays = stray_worker_pids(sid)
|
||||
if strays:
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["resume_note"] = f"queued: target session busy (stray pids {strays})"
|
||||
waitstore.save(rec)
|
||||
log(f"queued {wid}: session busy {strays}")
|
||||
return
|
||||
|
||||
# pre-register the lease owner as THIS daemon so acquisition is atomic
|
||||
lease = waitstore.acquire_lease(sid, wid, os.getpid(), os.getpid())
|
||||
if lease is None:
|
||||
live = waitstore.read_lease(sid)
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["resume_note"] = f"queued: lease held by wait {live and live.get('wait_id')}"
|
||||
waitstore.save(rec)
|
||||
log(f"queued {wid}: lease held")
|
||||
return
|
||||
|
||||
rec["status"] = "RESUMING"
|
||||
rec["resume_attempts"] = rec.get("resume_attempts", 0) + 1
|
||||
waitstore.save(rec)
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
resume_cmd(sid, msg, wid, rec.get("resume_cmd")),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
start_new_session=True,
|
||||
)
|
||||
except Exception as e:
|
||||
waitstore.release_lease(sid, wid)
|
||||
rec["status"] = "READY_TO_RETRY" # spawn failed pre-delivery → retryable
|
||||
rec["resume_note"] = f"spawn failed: {e}"
|
||||
waitstore.save(rec)
|
||||
log(f"spawn {wid} failed pre-delivery: {e}")
|
||||
return
|
||||
|
||||
pgid = proc.pid # start_new_session ⇒ pgid == child pid
|
||||
waitstore.update_lease(sid, wait_id=wid, pid=proc.pid, pgid=pgid, pid_starttime=waitstore._pid_starttime(proc.pid))
|
||||
log(f"launched {wid}: pid={proc.pid} pgid={pgid} attempt={rec['resume_attempts']}")
|
||||
|
||||
timed_out = False
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=WATCH_SECS)
|
||||
rc = proc.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
timed_out = True
|
||||
survivors = terminate_tree(pgid)
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=30)
|
||||
except Exception:
|
||||
stdout, stderr = "", ""
|
||||
rc = proc.returncode
|
||||
if survivors:
|
||||
log(f"ERROR {wid}: group {pgid} survivors after KILL: {survivors}")
|
||||
|
||||
waitstore.release_lease(sid, wid)
|
||||
|
||||
if timed_out:
|
||||
# D+E: timeout is NOT success. Tree terminated; retry truthfully.
|
||||
if rec["resume_attempts"] >= MAX_ATTEMPTS:
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
rec["resume_note"] = (
|
||||
f"watch timeout x{rec['resume_attempts']}; process group {pgid} terminated; attempts exhausted"
|
||||
)
|
||||
else:
|
||||
rec["status"] = "READY_TO_RETRY"
|
||||
rec["resume_note"] = f"watch timeout; process group {pgid} terminated (TERM→KILL, verified); will retry"
|
||||
log(f"timeout {wid}: tree {pgid} terminated, rc={rc}")
|
||||
elif rc == 0:
|
||||
# COMPLETION TRUTH (constraint 4): rc==0 means the headless worker
|
||||
# EXITED CLEANLY — the strongest terminal evidence the supported CLI
|
||||
# provides. Timeout/cancel can never reach this branch. The evidence
|
||||
# chain (rc, output tail, timing) is recorded on the wait record.
|
||||
terminal = "COMPLETED" if rec.get("verdict") == "success" else "FAILED"
|
||||
rec["status"] = terminal
|
||||
rec["resume_evidence"] = {
|
||||
"kind": "worker_exited_rc0",
|
||||
"rc": 0,
|
||||
"output_tail": (stdout or "")[-400:],
|
||||
"finished_at": waitstore.now(),
|
||||
}
|
||||
log(f"resumed {wid}: rc=0 → {terminal}")
|
||||
else:
|
||||
# Nonzero rc: delivery/completion unproven. Honest, not schedulable.
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
rec["resume_evidence"] = {
|
||||
"kind": "worker_exited_nonzero",
|
||||
"rc": rc,
|
||||
"output_tail": ((stdout or "") + (stderr or ""))[-400:],
|
||||
"finished_at": waitstore.now(),
|
||||
}
|
||||
rec["resume_note"] = f"resume rc={rc}; delivery unverified"
|
||||
log(f"resumed {wid}: rc={rc} → RESUME_UNVERIFIED")
|
||||
waitstore.save(rec)
|
||||
|
||||
|
||||
# ── startup recovery (invariant F) ────────────────────────────────────────
|
||||
|
||||
|
||||
def startup_recovery() -> None:
|
||||
for rec in waitstore.all_waits():
|
||||
if rec.get("status") != "RESUMING":
|
||||
continue
|
||||
sid, wid = rec["session_id"], rec["wait_id"]
|
||||
lease = waitstore.read_lease(sid)
|
||||
if lease and lease.get("wait_id") == wid and waitstore._owner_alive(lease):
|
||||
# Adopt the live worker and monitor it to a bounded conclusion.
|
||||
pgid = lease["pgid"]
|
||||
log(f"recover {wid}: adopted live worker pgid={pgid}")
|
||||
deadline = time.time() + WATCH_SECS
|
||||
while time.time() < deadline and pgid_members(pgid):
|
||||
time.sleep(2)
|
||||
survivors = terminate_tree(pgid) if pgid_members(pgid) else []
|
||||
waitstore.release_lease(sid, wid)
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
rec["resume_note"] = "adopted post-restart; outcome unverifiable" + (
|
||||
f"; survivors killed: {survivors}" if survivors else ""
|
||||
)
|
||||
waitstore.save(rec)
|
||||
log(f"recover {wid}: adopted worker ended → RESUME_UNVERIFIED")
|
||||
else:
|
||||
if lease:
|
||||
waitstore.release_lease(sid, wid) # stale
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
rec["resume_note"] = "recovered at daemon startup: no live owner; delivery unverifiable"
|
||||
waitstore.save(rec)
|
||||
log(f"recover {wid}: no live owner → RESUME_UNVERIFIED")
|
||||
|
||||
|
||||
# ── reconciler + drive loop (invariant G: deterministic queue) ────────────
|
||||
|
||||
_inflight: dict[str, threading.Thread] = {}
|
||||
_inflight_lock = threading.Lock()
|
||||
|
||||
|
||||
def drive_ready() -> None:
|
||||
for rec in waitstore.schedulable():
|
||||
sid = rec["session_id"]
|
||||
# CONTENTION POLICY (constraint 2): MiMo exposes NO supported
|
||||
# busy/idle session signal (verified 2026-09-12: `mimo session list
|
||||
# --format json` has no status field; no `session status` command;
|
||||
# no status column in the session schema; no documented ACP/serve
|
||||
# endpoint). Therefore automatic injection into interactive sessions
|
||||
# is NOT YET SAFE and stays disabled. Only waits explicitly declared
|
||||
# target_kind="headless" (disposable/owned sessions) auto-dispatch.
|
||||
if rec.get("target_kind") != "headless":
|
||||
if not rec.get("hold_reason"):
|
||||
rec["hold_reason"] = (
|
||||
"INTERACTIVE_AUTO_RESUME = NOT YET SAFE: "
|
||||
"no supported MiMo busy/idle signal; "
|
||||
"queued until operator releases"
|
||||
)
|
||||
waitstore.save(rec)
|
||||
continue
|
||||
if rec.get("ext", {}).get("resume_mode") == "self":
|
||||
# REAL_ASYNC_BRIDGE §3: self-resuming wait (completion-file /
|
||||
# inotify waiter owns its continuation). The daemon has NO
|
||||
# delivery role here — dispatching a delivery worker would
|
||||
# stamp a phantom rc!=0 over the waiter's own truthful
|
||||
# terminal state (the canonical-state contradiction).
|
||||
# NB: keyed on the explicit self marker, NOT on resume_cmd
|
||||
# absence — records without inline resume_cmd may still be
|
||||
# dispatchable via the WR_RESUME_CMD env template.
|
||||
continue
|
||||
with _inflight_lock:
|
||||
t = _inflight.get(sid)
|
||||
if t is not None and t.is_alive():
|
||||
continue # single-flight: session already being driven
|
||||
# B: never launch into a busy session
|
||||
strays = stray_worker_pids(sid)
|
||||
if strays:
|
||||
log(f"drive: {rec['wait_id']} held, session busy {strays}")
|
||||
continue
|
||||
th = threading.Thread(target=resume, args=(rec,), daemon=True, name=f"resume-{rec['wait_id']}")
|
||||
_inflight[sid] = th
|
||||
# start INSIDE the lock: a just-created thread is not yet
|
||||
# is_alive() until start() runs — starting outside the lock let
|
||||
# the same drive cycle dispatch a second worker for the session
|
||||
# (single-flight race, caught by the T2 invariant test).
|
||||
th.start()
|
||||
log(f"drive: dispatched {rec['wait_id']} (session {sid})")
|
||||
|
||||
|
||||
def reconcile_once() -> None:
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] != "WAITING_EXTERNAL":
|
||||
continue
|
||||
created = rec.get("created_at", "")
|
||||
try:
|
||||
import calendar
|
||||
|
||||
age = time.time() - calendar.timegm(time.strptime(created, "%Y-%m-%dT%H:%M:%SZ"))
|
||||
except ValueError:
|
||||
continue
|
||||
if age > RECONCILE_SECS:
|
||||
try_transition(rec)
|
||||
|
||||
|
||||
_tick = 0
|
||||
|
||||
|
||||
def orphan_sweep() -> int:
|
||||
"""REAL_ASYNC_BRIDGE §4: permanent mid-run orphan-lease recovery.
|
||||
|
||||
FAILURE DETECTION ONLY — never involved in normal completion
|
||||
scheduling. A wait stuck in RESUMING past ORPHAN_GRACE_SECS means the
|
||||
resume flow died between lease acquire and release (worker thread
|
||||
lost, exception mid-watch). Reconcile exactly as startup_recovery
|
||||
would have: release the lease, stamp RESUME_UNVERIFIED truthfully
|
||||
(delivery unverifiable), never auto-relaunch.
|
||||
"""
|
||||
import calendar as _cal
|
||||
|
||||
healed = 0
|
||||
for rec in waitstore.all_waits():
|
||||
if rec.get("status") != "RESUMING":
|
||||
continue
|
||||
try:
|
||||
updated = _cal.timegm(time.strptime(rec.get("last_updated", ""), "%Y-%m-%dT%H:%M:%SZ"))
|
||||
except ValueError:
|
||||
continue
|
||||
if time.time() - updated < ORPHAN_GRACE_SECS:
|
||||
continue # legitimate in-flight watch (up to WATCH_SECS)
|
||||
wid, sid = rec["wait_id"], rec["session_id"]
|
||||
lease = waitstore.read_lease(sid)
|
||||
owner_dead = lease is not None and not waitstore._owner_alive(lease)
|
||||
daemon_owned = lease is not None and lease.get("pid") == os.getpid()
|
||||
if lease is None or owner_dead or daemon_owned:
|
||||
if lease is not None:
|
||||
waitstore.release_lease(sid, wid)
|
||||
if rec.get("resume_attempts", 0) >= MAX_ATTEMPTS:
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
else:
|
||||
rec["status"] = "READY_TO_RETRY"
|
||||
rec["resume_note"] = (
|
||||
"orphaned mid-run: resume flow died between lease acquire "
|
||||
"and delivery ("
|
||||
+ ("lease owner dead" if owner_dead else "resume thread lost" if daemon_owned else "no lease")
|
||||
+ "); orphan sweep recovered"
|
||||
)
|
||||
waitstore.save(rec)
|
||||
log(f"orphan-sweep {wid}: {rec['status']} — {rec['resume_note']}")
|
||||
healed += 1
|
||||
return healed
|
||||
|
||||
|
||||
def reconciler() -> None:
|
||||
global _tick
|
||||
while True:
|
||||
# v0.3: the local-runner wake is the fast path; periodic
|
||||
# authoritative verification demotes to a slower fallback
|
||||
# (missed-event + daemon-restart recovery), not API polling.
|
||||
_tick += 1
|
||||
if _tick % RECONCILE_EVERY_TICKS == 0:
|
||||
try:
|
||||
reconcile_once()
|
||||
except Exception as e:
|
||||
log(f"reconcile error: {e}")
|
||||
try:
|
||||
orphan_sweep() # §4 failure detection — bounded, periodic
|
||||
except Exception as e:
|
||||
log(f"orphan-sweep error: {e}")
|
||||
try:
|
||||
drive_ready()
|
||||
except Exception as e:
|
||||
log(f"drive error: {e}")
|
||||
with _inflight_lock:
|
||||
for sid, t in list(_inflight.items()):
|
||||
if not t.is_alive():
|
||||
del _inflight[sid]
|
||||
time.sleep(DRIVE_LOOP_SECS)
|
||||
|
||||
|
||||
# ── webhook receiver (wake signal only) ───────────────────────────────────
|
||||
|
||||
|
||||
def wake_sweep(event: dict, eid: str) -> dict:
|
||||
"""Sweep WAITING_EXTERNAL waits through authoritative verification.
|
||||
|
||||
Runs on a background thread (the hook's HTTP response returns
|
||||
instantly, minimising the runner's offline window). If any verified
|
||||
wait got 'pending' — the normal case for a job that finished
|
||||
microseconds ago: the post-task hook fires after results are reported
|
||||
but before the runner's final task acknowledgement flips server-side
|
||||
status — retry on a BOUNDED event-triggered schedule (default 4s x3),
|
||||
never a polling loop. The final ack lands milliseconds after the hook
|
||||
exits, so the first retry almost always sees the terminal state.
|
||||
"""
|
||||
arrival = time.time()
|
||||
log(f"local wake {eid}: {json.dumps(event)[:140]}")
|
||||
import calendar
|
||||
|
||||
def sweep() -> tuple[int, int, int]:
|
||||
verified = advanced = pending = 0
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] != "WAITING_EXTERNAL":
|
||||
continue
|
||||
try:
|
||||
age = time.time() - calendar.timegm(time.strptime(rec.get("created_at", ""), "%Y-%m-%dT%H:%M:%SZ"))
|
||||
except ValueError:
|
||||
continue
|
||||
if age < WAKE_MIN_AGE_SECS:
|
||||
continue
|
||||
verified += 1
|
||||
before = rec["status"]
|
||||
try_transition(rec)
|
||||
if rec["status"] != before and rec["status"] == "READY_TO_RESUME":
|
||||
advanced += 1
|
||||
elif rec["status"] == "WAITING_EXTERNAL":
|
||||
pending += 1
|
||||
return verified, advanced, pending
|
||||
|
||||
verified, advanced, pending = sweep()
|
||||
log(
|
||||
f"local wake {eid}: verified={verified} advanced={advanced} "
|
||||
f"pending={pending} sweep_ms={int((time.time() - arrival) * 1000)}"
|
||||
)
|
||||
for attempt in range(1, WAKE_RETRIES + 1):
|
||||
# Retry while the sweep didn't conclusively advance anything:
|
||||
# pending verdicts (ack-window) AND fresh-wait skips (age gate).
|
||||
# Bounded, event-triggered — not a polling loop.
|
||||
if not pending and verified:
|
||||
break
|
||||
time.sleep(WAKE_RETRY_SECS)
|
||||
verified, advanced, pending = sweep()
|
||||
log(
|
||||
f"local wake {eid}: retry{attempt} verified={verified} "
|
||||
f"advanced={advanced} pending={pending} "
|
||||
f"total_ms={int((time.time() - arrival) * 1000)}"
|
||||
)
|
||||
return {"event_id": eid}
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
if self.path == "/process-event":
|
||||
# T15 §15D: process-exit events from builder/controller
|
||||
# notifiers. Minimal dedicated endpoint — the Gitea webhook
|
||||
# path stays untouched. Payload: {operation_id, exit_code,
|
||||
# completed_at} + OPTIONAL provider/event_type/sha (T16
|
||||
# §16C/§16E: the CI adapter registers as provider="gitea",
|
||||
# event_type="ci_completed"; defaults preserve the T15 builder
|
||||
# identity so existing notifiers are unaffected). The event is
|
||||
# a WAKE, never truth — resumed controllers re-read canonical
|
||||
# authority before any verdict.
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length)
|
||||
try:
|
||||
payload = json.loads(body) if body else {}
|
||||
except json.JSONDecodeError:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
return
|
||||
event = {
|
||||
"provider": str(payload.get("provider", "builder")),
|
||||
"event_type": str(payload.get("event_type", "builder_exit")),
|
||||
"run_ref": str(payload.get("operation_id", "")) or None,
|
||||
"sha": payload.get("sha"),
|
||||
"exit_code": payload.get("exit_code"),
|
||||
"completed_at": payload.get("completed_at") or "",
|
||||
}
|
||||
event = {k: v for k, v in event.items() if v is not None}
|
||||
eid = waitstore.record_event(event)
|
||||
log(f"process-event {eid}: {json.dumps(event)[:140]}")
|
||||
# REAL_ASYNC_BRIDGE (R3): durable completion-file delivery.
|
||||
# Every event reaching the bridge is written as
|
||||
# resumes/<operation_id>.done so inotify waiters wake on the
|
||||
# kernel event (R4) with no FIFO/lease machinery, and the
|
||||
# event survives daemon AND waiter restarts (R9). Atomic
|
||||
# rename; duplicate events rewrite identical content (R10).
|
||||
_op_id = str(payload.get("operation_id", ""))
|
||||
if _op_id:
|
||||
try:
|
||||
# Stable identity only: duplicate deliveries of the
|
||||
# same logical event must produce byte-identical
|
||||
# completion files (R10 idempotence by construction).
|
||||
# completed_at is volatile and lives in the durable
|
||||
# event log; the file is a WAKE SIGNAL, never truth.
|
||||
_stable = {
|
||||
k: event.get(k)
|
||||
for k in ("provider", "event_type", "run_ref", "sha", "exit_code")
|
||||
if event.get(k) is not None
|
||||
}
|
||||
completion_file.write_completion(_op_id, _stable)
|
||||
except Exception as e: # noqa: BLE001 — delivery best-effort; wait-record transition + watchdog recovery still cover the lost file
|
||||
log(f"process-event {eid}: completion-file write failed: {e}")
|
||||
matched = 0
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] == "WAITING_EXTERNAL" and waitstore.match(rec, event):
|
||||
matched += 1
|
||||
try_transition(rec, event)
|
||||
# matched READY controller waits are dispatched IMMEDIATELY
|
||||
# (event-driven, no floor). Guard: only never-resumed waits —
|
||||
# a duplicate/late event must not double-dispatch (15E);
|
||||
# single-flight lease + this guard together make resume
|
||||
# idempotent.
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] == "READY_TO_RESUME" and rec.get("resume_cmd") and rec.get("resume_attempts", 0) == 0:
|
||||
threading.Thread(target=resume, args=(rec,), daemon=True, name=f"resume-{rec['wait_id']}").start()
|
||||
matched += 1
|
||||
try:
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"event_id": eid, "matched": matched}).encode())
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
return
|
||||
if self.path == "/wake":
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length)
|
||||
try:
|
||||
payload = json.loads(body) if body else {}
|
||||
except json.JSONDecodeError:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
return
|
||||
event = {
|
||||
"provider": "gitea-local-runner",
|
||||
"task_id": str(payload.get("task_id", "")) or None,
|
||||
"run_id": str(payload.get("run_id", "")) or None,
|
||||
"repository": str(payload.get("repository", "")) or None,
|
||||
"job_result": str(payload.get("job_result", "")) or None,
|
||||
}
|
||||
event = {k: v for k, v in event.items() if v}
|
||||
eid = waitstore.record_event(event)
|
||||
# Respond instantly; sweep on a worker thread. The hook is
|
||||
# fire-and-forget (curl -m 2) — a broken pipe on write is
|
||||
# harmless and must not propagate.
|
||||
try:
|
||||
threading.Thread(
|
||||
target=wake_sweep, args=(event, eid), daemon=True, name=f"wake-{payload.get('task_id', '?')}"
|
||||
).start()
|
||||
except Exception as e:
|
||||
log(f"wake spawn error: {e}")
|
||||
try:
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"queued": True, "event_id": eid}).encode())
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
return
|
||||
if self.path != "/webhook":
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
return
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length)
|
||||
try:
|
||||
payload = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
return
|
||||
action = payload.get("action") or payload.get("event_type") or ""
|
||||
wf = payload.get("workflow_run") or {}
|
||||
event = {
|
||||
"provider": "gitea",
|
||||
"event_type": f"workflow.{action}" if wf else action,
|
||||
"sha": (wf.get("head_sha") or payload.get("sha") or "")[:12] or None,
|
||||
"run_ref": str(wf.get("id") or payload.get("run_number") or "") or None,
|
||||
}
|
||||
event = {k: v for k, v in event.items() if v}
|
||||
eid = waitstore.record_event(event)
|
||||
log(f"webhook event {eid}: {json.dumps(event)[:140]}")
|
||||
matched = 0
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] == "WAITING_EXTERNAL" and waitstore.match(rec, event):
|
||||
matched += 1
|
||||
try_transition(rec)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"event_id": eid, "matched": matched}).encode())
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/health":
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"ok":true}')
|
||||
return
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
|
||||
def main() -> int:
|
||||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8791
|
||||
|
||||
# H (constraint 1): flock is the SINGLETON AUTHORITY. daemon.pid's
|
||||
# content is diagnostic metadata only — it never gates startup, never
|
||||
# implies liveness, and is overwritten after the exclusive lock is held.
|
||||
# Stale pid content (incl. SIGKILL debris) is thus harmless by design.
|
||||
PIDFILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
lock_fd = os.open(PIDFILE, os.O_CREAT | os.O_RDWR, 0o644)
|
||||
try:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError:
|
||||
log("refusing start: another daemon holds the pidfile flock")
|
||||
print("daemon already running (pidfile flock held)", file=sys.stderr)
|
||||
return 1
|
||||
os.ftruncate(lock_fd, 0)
|
||||
os.write(lock_fd, f"{os.getpid()}\n".encode()) # diagnostic, lock held for life
|
||||
|
||||
startup_recovery()
|
||||
t = threading.Thread(target=reconciler, daemon=True)
|
||||
t.start()
|
||||
log(f"daemon starting on :{port}")
|
||||
srv = ThreadingHTTPServer(("0.0.0.0", port), Handler)
|
||||
try:
|
||||
srv.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,246 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""verify.py — authoritative Gitea verification (webhook ≠ success).
|
||||
|
||||
Queries the Gitea API for the TRUE state of the run/job/commit the wait
|
||||
references. Returns (verdict, evidence). verdict ∈ {success, failure,
|
||||
pending, unknown}. Only this module may move a wait out of
|
||||
WAITING_EXTERNAL.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import json, subprocess
|
||||
from pathlib import Path
|
||||
|
||||
HOST = "https://gitea.thingscouldgetdicey.online"
|
||||
REPO = "conductor/rune"
|
||||
|
||||
|
||||
def _token() -> str:
|
||||
# Canonical ladder without importing Rune: env → token file → git store
|
||||
import os
|
||||
|
||||
tok = os.environ.get("GITEA_TOKEN", "").strip()
|
||||
if tok:
|
||||
return tok
|
||||
tf = os.path.expanduser("~/.config/opencode/secrets/gitea-token")
|
||||
if os.path.isfile(tf):
|
||||
return open(tf).read().strip()
|
||||
r = subprocess.run(
|
||||
["git", "credential", "fill"],
|
||||
input="protocol=https\nhost=gitea.thingscouldgetdicey.online\nusername=conductor\n\n",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
for line in r.stdout.splitlines():
|
||||
if line.startswith("password="):
|
||||
return line[9:]
|
||||
raise RuntimeError("no gitea credential available")
|
||||
|
||||
|
||||
def _api(path: str) -> tuple[int, object]:
|
||||
import urllib.request
|
||||
|
||||
req = urllib.request.Request(f"{HOST}/api/v1/repos/{REPO}{path}", headers={"Authorization": f"token {_token()}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as r:
|
||||
return r.status, json.loads(r.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, {"error": e.read().decode()[:200]}
|
||||
|
||||
|
||||
def _jobs_list(run_ref: str):
|
||||
"""GET jobs for a run, unwrapping the {'jobs': [...]} envelope."""
|
||||
s, data = _api(f"/actions/runs/{run_ref}/jobs")
|
||||
if s != 200:
|
||||
return s, None
|
||||
if isinstance(data, dict):
|
||||
data = data.get("jobs", [])
|
||||
return s, data if isinstance(data, list) else None
|
||||
|
||||
|
||||
def _resolve_run_id(run_ref: str, sha: str) -> str:
|
||||
"""Task-list ids are NOT run ids. If run_ref lookup 404s, resolve the
|
||||
real run id by head SHA via /actions/runs (T15: run 6907 vs task 15743)."""
|
||||
if run_ref:
|
||||
s, jobs = _jobs_list(run_ref)
|
||||
if s == 200 and jobs:
|
||||
return run_ref
|
||||
if sha:
|
||||
s, runs = _api(f"/actions/runs?limit=30")
|
||||
items = runs.get("workflow_runs", []) if isinstance(runs, dict) else (runs if isinstance(runs, list) else [])
|
||||
# One SHA fans out to multiple runs (lint + deploy-only). Prefer a
|
||||
# substantive (non-skipped) run; a skipped-only run proves nothing.
|
||||
fallback = ""
|
||||
for r in items:
|
||||
if str(r.get("head_sha", "")).startswith(sha):
|
||||
rid = str(r.get("id"))
|
||||
if r.get("conclusion") != "skipped":
|
||||
return rid
|
||||
fallback = fallback or rid
|
||||
return fallback
|
||||
return ""
|
||||
|
||||
|
||||
def verify_run(run_ref: str, sha: str, expected: str) -> tuple[str, dict]:
|
||||
"""run_ref = actions run id (or task id — resolved via SHA lookup).
|
||||
|
||||
Defect fixed 2026-09-12 (surfaced by W/R-v0.3 live proof, run 7110):
|
||||
the jobs query here used raw _api + isinstance(jobs, list), but this
|
||||
Gitea wraps jobs in the {"jobs": [...]} envelope — the check always
|
||||
failed, so EVERY EXACT_WORKFLOW verdict silently fell through to the
|
||||
aggregate commit-status fallback, where unrelated runs on the same
|
||||
SHA (multi-run fan-out: ci.yml + deploy-only) poison the verdict.
|
||||
Use _jobs_list (unwraps the envelope), same as _resolve_run_id."""
|
||||
run_ref = _resolve_run_id(run_ref, sha)
|
||||
if run_ref:
|
||||
s, jobs = _jobs_list(run_ref)
|
||||
if s == 200 and isinstance(jobs, list) and jobs:
|
||||
conclusions = {j.get("name"): j.get("conclusion") for j in jobs}
|
||||
statuses = {j.get("status") for j in jobs}
|
||||
if statuses <= {"completed"}:
|
||||
verdict = "success" if set(conclusions.values()) <= {"success", "skipped"} else "failure"
|
||||
return verdict, {"run_ref": run_ref, "jobs": conclusions}
|
||||
return "pending", {"run_ref": run_ref, "statuses": sorted(statuses)}
|
||||
if sha:
|
||||
s, st = _api(f"/commits/{sha}/status")
|
||||
if s == 200 and isinstance(st, dict):
|
||||
state = st.get("state", "unknown") # success|error|failure|pending|warning
|
||||
rid = _resolve_run_id("", sha) # attach run id so failure evidence works
|
||||
if state == "success":
|
||||
return "success", {"sha": sha, "state": state, "run_ref": rid}
|
||||
if state in ("error", "failure"):
|
||||
return "failure", {"sha": sha, "state": state, "run_ref": rid}
|
||||
return "pending", {"sha": sha, "state": state}
|
||||
return "unknown", {"run_ref": run_ref, "sha": sha}
|
||||
|
||||
|
||||
def failed_job_evidence(run_ref: str, sha: str = "") -> dict:
|
||||
"""For failure continuations: exact failing job + conclusion."""
|
||||
run_ref = _resolve_run_id(run_ref, sha)
|
||||
if not run_ref:
|
||||
return {}
|
||||
s, jobs = _jobs_list(run_ref)
|
||||
if s != 200 or not isinstance(jobs, list):
|
||||
return {}
|
||||
bad = [j for j in jobs if j.get("conclusion") not in ("success", "skipped", None)]
|
||||
return {
|
||||
"failed_jobs": [
|
||||
{"name": j.get("name"), "conclusion": j.get("conclusion"), "url": j.get("url", "")} for j in bad
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# ── RELEASE_DEPLOY verification (W/R v0.1, overnight handoff Part 2) ──────
|
||||
# Aggregate commit status is NOT a valid completion authority for Rune's
|
||||
# release/deploy flow: the release successor SHA, not the source SHA, is what
|
||||
# converges (observed live: wr-1789179026 stranded on pending while 0.66.15
|
||||
# was deployed). The authority chain is:
|
||||
# source SHA → source runs terminal → release successor commit (descendant)
|
||||
# → successor runs success → deployed/current identity converges.
|
||||
|
||||
|
||||
def _git(args: list[str], repo_root: str) -> str:
|
||||
import subprocess
|
||||
|
||||
r = subprocess.run(["git"] + args, capture_output=True, text=True, cwd=repo_root or None, timeout=15)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"git {' '.join(args[:2])}: {r.stderr.strip()[:120]}")
|
||||
return r.stdout
|
||||
|
||||
|
||||
def _runs_for_sha(sha: str) -> list[dict]:
|
||||
s, runs = _api("/actions/runs?limit=50")
|
||||
items = runs.get("workflow_runs", []) if isinstance(runs, dict) else []
|
||||
return [r for r in items if str(r.get("head_sha", "")).startswith(sha)]
|
||||
|
||||
|
||||
def _sha_verdict_from_runs(runs: list[dict]) -> tuple[str, dict]:
|
||||
"""success | failure | pending from a SHA's workflow runs.
|
||||
Terminal verdict ONLY when every run is completed — mixed states
|
||||
(some success, some still in flight) must stay PENDING (observed live:
|
||||
a SHA with 1 completed-success + 1 queued + 1 in-progress was judged
|
||||
success mid-flight; wr-1789182522 2026-09-12)."""
|
||||
if not runs:
|
||||
return "pending", {"note": "no runs found"}
|
||||
non_terminal = [r for r in runs if r.get("status") != "completed"]
|
||||
if non_terminal:
|
||||
return "pending", {"in_flight": len(non_terminal)}
|
||||
conclusions = {r.get("name"): r.get("conclusion") for r in runs}
|
||||
bad = {n: c for n, c in conclusions.items() if c == "failure"}
|
||||
if bad:
|
||||
return "failure", {"failed": bad}
|
||||
return "success", {"runs": conclusions}
|
||||
|
||||
|
||||
def verify_release_deploy(source_sha: str, repo_root: str = "", deployed_link: str = "") -> tuple[str, dict]:
|
||||
"""RELEASE_DEPLOY authority chain. Returns (verdict, evidence)."""
|
||||
source_sha = (source_sha or "")[:12]
|
||||
ev: dict = {"source_sha": source_sha, "kind": "RELEASE_DEPLOY"}
|
||||
|
||||
# 1. Source SHA's own runs must be terminal and healthy.
|
||||
src_runs = _runs_for_sha(source_sha)
|
||||
src_verdict, src_ev = _sha_verdict_from_runs(src_runs)
|
||||
ev["source"] = src_ev
|
||||
if src_verdict != "success":
|
||||
return src_verdict, ev
|
||||
|
||||
# 2. Release successor: first chore(release) commit descending from source.
|
||||
if not repo_root:
|
||||
return "pending", {**ev, "note": "no repo_root — cannot check lineage"}
|
||||
try:
|
||||
out = _git(
|
||||
[
|
||||
"log",
|
||||
"--format=%H %s",
|
||||
"--reverse",
|
||||
f"{source_sha}..master",
|
||||
"--grep=^chore\\(release\\)",
|
||||
"--extended-regexp",
|
||||
],
|
||||
repo_root,
|
||||
)
|
||||
except RuntimeError as e:
|
||||
return "pending", {**ev, "note": str(e)}
|
||||
successors = [l for l in out.splitlines() if l.strip()]
|
||||
if not successors:
|
||||
return "pending", {**ev, "note": "no release successor yet"}
|
||||
succ_line = successors[0]
|
||||
succ_sha_full, _, succ_subject = succ_line.partition(" ")
|
||||
succ_sha = succ_sha_full[:12]
|
||||
ev["successor"] = {"sha": succ_sha, "subject": succ_subject[:80]}
|
||||
|
||||
# Ancestry is guaranteed by the range, but verify explicitly (deterministic
|
||||
# lineage over title guessing):
|
||||
try:
|
||||
_git(["merge-base", "--is-ancestor", source_sha, succ_sha_full], repo_root)
|
||||
except RuntimeError:
|
||||
return "failure", {**ev, "note": "successor is not a descendant of source"}
|
||||
|
||||
# 3. Successor runs must be terminal and healthy.
|
||||
succ_runs = _runs_for_sha(succ_sha)
|
||||
succ_verdict, succ_ev = _sha_verdict_from_runs(succ_runs)
|
||||
ev["successor"]["runs"] = succ_ev
|
||||
if succ_verdict != "success":
|
||||
return succ_verdict, ev
|
||||
|
||||
# 4. Deployed identity convergence. A mismatch is FAILURE only when the
|
||||
# successor's runs are ALL completed — if any is still running/queued the
|
||||
# deploy may simply not have propagated yet → PENDING (observed live:
|
||||
# reconciler checked mid-deploy, verdict failure, deployment converged
|
||||
# ~2 min later; wr-1789182522 2026-09-12).
|
||||
if deployed_link:
|
||||
link = Path(deployed_link)
|
||||
if link.is_symlink() or link.is_dir():
|
||||
deployed = str(link.resolve())
|
||||
deployed_id = Path(deployed).name
|
||||
ev["deployed"] = deployed_id
|
||||
if not deployed_id.startswith(succ_sha[:12]):
|
||||
still = [r for r in _runs_for_sha(succ_sha) if r.get("status") != "completed"]
|
||||
if still:
|
||||
return "pending", {**ev, "note": "deploy propagating (runs in flight)"}
|
||||
return "failure", {**ev, "note": f"deployed {deployed_id} != successor {succ_sha} (all runs terminal)"}
|
||||
else:
|
||||
return "failure", {**ev, "note": f"deployed link missing: {deployed_link}"}
|
||||
|
||||
return "success", ev
|
||||
@@ -1,337 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""waitstore.py — wait/resume state machine v0.2 (developer tooling, NOT Rune).
|
||||
|
||||
States:
|
||||
RUNNABLE → WAITING_EXTERNAL → READY_TO_RESUME → RESUMING → COMPLETED | FAILED
|
||||
↑ │
|
||||
│ ├─(watch timeout, tree killed)
|
||||
│ └─(rc!=0 / unverifiable)
|
||||
READY_TO_RETRY ←─────┘
|
||||
RESUME_UNVERIFIED : delivery after crash/timeout cannot be proven.
|
||||
TERMINAL-ish — never auto-scheduled; explicit
|
||||
`requeue()` required to make it READY_TO_RESUME.
|
||||
TIMED_OUT : external event never arrived within the wait budget.
|
||||
|
||||
Waits persist as waits/<wait_id>.json (atomic tmp+rename).
|
||||
Leases persist as leases/<session_id>.lease.json (atomic tmp+rename,
|
||||
validated against /proc so a dead or PID-reused owner is stealable).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import fcntl, json, os, tempfile, time, uuid
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(os.environ.get("WR_ROOT", "") or Path(__file__).resolve().parent)
|
||||
WAITS = ROOT / "waits"
|
||||
EVENTS = ROOT / "events"
|
||||
LEASES = ROOT / "leases"
|
||||
|
||||
SCHEDULABLE = {"READY_TO_RESUME", "READY_TO_RETRY"}
|
||||
VALID_STATES = {
|
||||
"RUNNABLE",
|
||||
"WAITING_EXTERNAL",
|
||||
"READY_TO_RESUME",
|
||||
"RESUMING",
|
||||
"COMPLETED",
|
||||
"FAILED",
|
||||
"READY_TO_RETRY",
|
||||
"RESUME_UNVERIFIED",
|
||||
"TIMED_OUT",
|
||||
}
|
||||
TERMINAL = {"COMPLETED", "FAILED", "RESUME_UNVERIFIED", "TIMED_OUT"}
|
||||
|
||||
|
||||
def _atomic_write(path: Path, data: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=".tmp-")
|
||||
try:
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(data, f, indent=1, sort_keys=True)
|
||||
os.replace(tmp, path)
|
||||
# T17 17C: the bridge root is SHARED across users (deploy waiter
|
||||
# runs as the runner user; daemon as nick). mkstemp creates 0600 —
|
||||
# daemon re-saves locked the runner user out of records it must
|
||||
# read (waitstore.load in await_resume_event; find_matching_event
|
||||
# replay scan silently skipped them). 0644: owner writes, all read.
|
||||
os.chmod(path, 0o644)
|
||||
except BaseException:
|
||||
if os.path.exists(tmp):
|
||||
os.unlink(tmp)
|
||||
raise
|
||||
|
||||
|
||||
def now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def new_wait(
|
||||
*,
|
||||
session_id: str,
|
||||
provider: str,
|
||||
event_type: str,
|
||||
repo: str,
|
||||
sha: str = "",
|
||||
run_ref: str = "",
|
||||
expected: str = "success",
|
||||
wait_kind: str = "EXACT_WORKFLOW",
|
||||
target_kind: str = "interactive",
|
||||
repo_root: str = "",
|
||||
deployed_link: str = "",
|
||||
objective: str = "",
|
||||
waited_for: str = "",
|
||||
verify_when_woken: str = "",
|
||||
on_success: str = "",
|
||||
on_failure: str = "",
|
||||
resume_cmd: list | None = None,
|
||||
) -> dict:
|
||||
wid = f"wr-{int(time.time())}-{uuid.uuid4().hex[:6]}"
|
||||
rec = {
|
||||
"wait_id": wid,
|
||||
"session_id": session_id,
|
||||
"created_at": now(),
|
||||
"provider": provider,
|
||||
"event_type": event_type,
|
||||
"repo": repo,
|
||||
"sha": sha,
|
||||
"run_ref": run_ref,
|
||||
"expected": expected,
|
||||
# T15 §15D: PROCESS_EVENT — a wait whose authoritative verdict is
|
||||
# the matching process-exit event itself (exit_code carried on the
|
||||
# event), e.g. a builder/controller process completing. The event
|
||||
# IS the authority here (the notifier wrapper observed the real
|
||||
# process exit); no Gitea lookup applies.
|
||||
"wait_kind": wait_kind
|
||||
if wait_kind in ("EXACT_WORKFLOW", "RELEASE_DEPLOY", "PROCESS_EVENT")
|
||||
else "EXACT_WORKFLOW",
|
||||
# CONTENTION POLICY: "interactive" (default, conservative) never
|
||||
# auto-dispatches — MiMo exposes NO supported busy/idle session
|
||||
# signal, so injecting into a possibly-busy interactive session is
|
||||
# NOT YET SAFE. Only explicitly declared headless/disposable
|
||||
# targets ("headless") are auto-resumed.
|
||||
"target_kind": target_kind if target_kind in ("headless", "interactive") else "interactive",
|
||||
"repo_root": repo_root,
|
||||
"deployed_link": deployed_link,
|
||||
# T15 §15D: per-wait resume command override (placeholder template,
|
||||
# same substitution as WR_RESUME_CMD: {session_id}/{msg}/{wait_id}).
|
||||
# A synthetic controller session (e.g. a Scaffolding stage driver)
|
||||
# is NOT a MiMo session — the global mimo-shaped default cannot
|
||||
# resume it, which is exactly why fixed-sleep polling crept back
|
||||
# in T12-T14. Per-wait override makes ANY registered continuation
|
||||
# target resumable through the same bridge.
|
||||
"resume_cmd": list(resume_cmd) if resume_cmd else None,
|
||||
"status": "WAITING_EXTERNAL",
|
||||
"last_verified": "",
|
||||
"resume_attempts": 0,
|
||||
"continuation": {
|
||||
"objective": objective,
|
||||
"waited_for": waited_for,
|
||||
"verify_when_woken": verify_when_woken,
|
||||
"on_success": on_success,
|
||||
"on_failure": on_failure,
|
||||
},
|
||||
}
|
||||
_atomic_write(WAITS / f"{wid}.json", rec)
|
||||
return rec
|
||||
|
||||
|
||||
def load(wid: str) -> dict | None:
|
||||
p = WAITS / f"{wid}.json"
|
||||
return json.loads(p.read_text()) if p.is_file() else None
|
||||
|
||||
|
||||
def save(rec: dict) -> None:
|
||||
assert rec.get("status") in VALID_STATES, rec.get("status")
|
||||
rec["last_updated"] = now()
|
||||
_atomic_write(WAITS / f"{rec['wait_id']}.json", rec)
|
||||
|
||||
|
||||
def all_waits() -> list[dict]:
|
||||
out = []
|
||||
for p in WAITS.glob("*.json"):
|
||||
try:
|
||||
out.append(json.loads(p.read_text()))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def pending() -> list[dict]:
|
||||
return [r for r in all_waits() if r.get("status") in {"WAITING_EXTERNAL"} | SCHEDULABLE]
|
||||
|
||||
|
||||
def schedulable() -> list[dict]:
|
||||
"""READY waits, oldest first — deterministic queue order."""
|
||||
rs = [r for r in all_waits() if r.get("status") in SCHEDULABLE]
|
||||
rs.sort(key=lambda r: (r.get("created_at", ""), r.get("wait_id", "")))
|
||||
return rs
|
||||
|
||||
|
||||
def requeue(wid: str, note: str) -> dict | None:
|
||||
"""Operator-explicit requeue of a RESUME_UNVERIFIED/RESUME_FAILED wait."""
|
||||
rec = load(wid)
|
||||
if not rec:
|
||||
return None
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["resume_note"] = note
|
||||
save(rec)
|
||||
return rec
|
||||
|
||||
|
||||
# ── events (wake signals, idempotent) ─────────────────────────────────────
|
||||
|
||||
|
||||
def record_event(event: dict) -> str:
|
||||
import hashlib
|
||||
|
||||
body = json.dumps(event, sort_keys=True)
|
||||
eid = hashlib.sha256(body.encode()).hexdigest()[:16]
|
||||
f = EVENTS / f"{eid}.json"
|
||||
if not f.is_file():
|
||||
_atomic_write(f, {"event_id": eid, "received_at": now(), "event": event})
|
||||
return eid
|
||||
|
||||
|
||||
def match(rec: dict, event: dict) -> bool:
|
||||
if rec.get("provider") != event.get("provider"):
|
||||
return False
|
||||
if rec.get("event_type") and event.get("event_type") and rec["event_type"] != event["event_type"]:
|
||||
return False
|
||||
if rec.get("sha") and event.get("sha") and rec["sha"] != event["sha"]:
|
||||
return False
|
||||
if rec.get("run_ref") and event.get("run_ref") and rec["run_ref"] != event["run_ref"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# ── leases (per-session single-flight, /proc-validated) ───────────────────
|
||||
#
|
||||
# Invariant 3 (PID reuse): liveness is NEVER "PID exists". The lease records
|
||||
# pid + pgid + process starttime (/proc/<pid>/stat field 22); all three must
|
||||
# match or the owner is a different process wearing a recycled PID.
|
||||
#
|
||||
# Invariant 1 (flock as authority): the lease file's flock serializes
|
||||
# read-modify-write across processes and dies with its holder, so a crashed
|
||||
# daemon can never wedge the lock. The lease JSON + /proc decide who OWNS the
|
||||
# session; the flock only prevents racing acquirers from double-stealing.
|
||||
|
||||
_lease_fds: dict[str, int] = {} # session_id → open fd holding the flock
|
||||
|
||||
|
||||
def _pid_starttime(pid: int) -> str | None:
|
||||
"""Field 22 of /proc/<pid>/stat — distinguishes PID reuse."""
|
||||
try:
|
||||
stat = Path(f"/proc/{pid}/stat").read_text()
|
||||
return stat.rsplit(")", 1)[1].split()[19]
|
||||
except (OSError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def _owner_alive(lease: dict) -> bool:
|
||||
pid = lease.get("pid")
|
||||
if not pid or not Path(f"/proc/{pid}").is_dir():
|
||||
return False
|
||||
if _pid_starttime(pid) != lease.get("pid_starttime"):
|
||||
return False # PID reused by an unrelated process
|
||||
try:
|
||||
if os.getpgid(pid) != lease.get("pgid"):
|
||||
return False
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def lease_path(session_id: str) -> Path:
|
||||
# T16 §16C finding (observed live in daemon.log): session ids like
|
||||
# "controller-<op>-Verboo Code/glm-5.3-flash" contain "/" — the raw
|
||||
# embed made lease creation raise FileNotFoundError, so resume
|
||||
# delivery for those waits could never succeed. Sanitize to a flat
|
||||
# filesystem-safe name; the hash suffix prevents collisions between
|
||||
# ids that sanitize identically.
|
||||
import hashlib as _hashlib
|
||||
import re as _re
|
||||
|
||||
safe = _re.sub(r"[^A-Za-z0-9._-]+", "_", session_id)[:80]
|
||||
digest = _hashlib.sha256(session_id.encode()).hexdigest()[:10]
|
||||
return LEASES / f"{safe}-{digest}.lease.json"
|
||||
|
||||
|
||||
def read_lease(session_id: str) -> dict | None:
|
||||
p = lease_path(session_id)
|
||||
if not p.is_file():
|
||||
return None
|
||||
try:
|
||||
return json.loads(p.read_text())
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def _locked_lease_fd(session_id: str) -> int | None:
|
||||
"""Open the lease file and take its flock (non-blocking). The fd stays
|
||||
open (lock held) until release_lease closes it."""
|
||||
if session_id in _lease_fds:
|
||||
return _lease_fds[session_id] # already hold it
|
||||
LEASES.mkdir(parents=True, exist_ok=True)
|
||||
fd = os.open(lease_path(session_id), os.O_CREAT | os.O_RDWR, 0o600)
|
||||
try:
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError:
|
||||
os.close(fd)
|
||||
return None # another process holds the lock — mid-acquire
|
||||
_lease_fds[session_id] = fd
|
||||
return fd
|
||||
|
||||
|
||||
def _write_lease_locked(session_id: str, lease: dict) -> None:
|
||||
fd = _lease_fds[session_id]
|
||||
os.ftruncate(fd, 0)
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
os.write(fd, json.dumps(lease, indent=1, sort_keys=True).encode())
|
||||
os.fsync(fd)
|
||||
|
||||
|
||||
def acquire_lease(session_id: str, wait_id: str, pid: int, pgid: int) -> dict | None:
|
||||
"""Atomically create or steal the session lease. Returns the lease dict,
|
||||
or None if a live owner holds it (or another process is mid-steal).
|
||||
Staleness is decided by /proc identity evidence, never by age alone."""
|
||||
fd = _locked_lease_fd(session_id)
|
||||
if fd is None:
|
||||
return None
|
||||
existing = read_lease(session_id)
|
||||
if existing and _owner_alive(existing):
|
||||
_lease_fds.pop(session_id, None)
|
||||
os.close(fd)
|
||||
return None # live worker owns this session
|
||||
lease = {
|
||||
"session_id": session_id,
|
||||
"wait_id": wait_id,
|
||||
"pid": pid,
|
||||
"pgid": pgid,
|
||||
"pid_starttime": _pid_starttime(pid),
|
||||
"acquired_at": now(),
|
||||
"state": "RESUMING",
|
||||
}
|
||||
_write_lease_locked(session_id, lease)
|
||||
return lease
|
||||
|
||||
|
||||
def update_lease(session_id: str, **fields) -> None:
|
||||
lease = read_lease(session_id) or {}
|
||||
lease.update(fields)
|
||||
_write_lease_locked(session_id, lease)
|
||||
|
||||
|
||||
def release_lease(session_id: str, wait_id: str) -> bool:
|
||||
"""Release only if the current lease belongs to this wait (no stealing
|
||||
a successor's lease during a slow shutdown path). Closes the flock fd."""
|
||||
lease = read_lease(session_id)
|
||||
if not (lease and lease.get("wait_id") == wait_id):
|
||||
return False
|
||||
fd = _lease_fds.pop(session_id, None)
|
||||
if fd is not None:
|
||||
try:
|
||||
fcntl.flock(fd, fcntl.LOCK_UN)
|
||||
except OSError:
|
||||
pass
|
||||
os.close(fd)
|
||||
lease_path(session_id).unlink(missing_ok=True)
|
||||
return True
|
||||
@@ -1,125 +0,0 @@
|
||||
import { execFile } from "node:child_process"
|
||||
import { existsSync, appendFileSync } from "node:fs"
|
||||
|
||||
// builder-bridge — Rune Core preflight for the MiMo builder (handoff
|
||||
// 2026-09-10 §3–§7, Option C bridge tranche). For each user turn, the hook
|
||||
// extracts the user's message from the outgoing LLM request, runs the Rune
|
||||
// Core builder-bridge preflight (deterministic: route classification +
|
||||
// memory selection + active-Scaffolding projection, ZERO model calls), and
|
||||
// appends a compact capsule to the final user message. Same Rune Core the
|
||||
// TUI/operator path uses — this file is a thin adapter with no cognition.
|
||||
//
|
||||
// CANONICAL SOURCE: config/mimo-hooks/builder-bridge.ts
|
||||
// Installed to ~/.mimocode/hooks/builder-bridge.ts by
|
||||
// `scripts/install_mimo_hook.py` (hash-verified, drift-detecting — the same
|
||||
// contract as event-wait-guard.ts). T22: this file was previously
|
||||
// UNVERSIONED and hardcoded an ephemeral interpreter path (/tmp/ci-venv),
|
||||
// which silently killed the whole edge; both defects are repaired here.
|
||||
//
|
||||
// Overhead contract (§7/§8): capsule ≤ ~300 tokens; a repeat/empty task
|
||||
// costs ~0ms (TTL cache); a fresh task ~150ms (bounded retrieval). The hook
|
||||
// degrades silently to no-injection on any failure — it must never block
|
||||
// the builder.
|
||||
//
|
||||
// Injection policy: only append when the capsule is non-empty. A plain
|
||||
// "hello" gets nothing (§5: if nothing relevant exists, inject nothing).
|
||||
|
||||
const BRIDGE = "/mnt/workdrive/rune/src/rune/builder_bridge_cli.py"
|
||||
const MARKER_PREFIX = "/tmp/rune-bridge-"
|
||||
|
||||
// Interpreter resolution (T22 repair): the previous hardcoded
|
||||
// "/tmp/ci-venv/bin/python" is an EPHEMERAL path that no longer exists, so
|
||||
// every bridge call failed and the hook silently degraded — the MiMo edge
|
||||
// received no Rune Core preflight at all (zero fire-log lines). Resolve
|
||||
// against durable candidates, first existing wins; env override wins over
|
||||
// all of them. If nothing exists the hook degrades as before.
|
||||
const PY_CANDIDATES = [
|
||||
process.env.MIMO_BRIDGE_PY,
|
||||
"/mnt/workdrive/rune/.test-venv/bin/python",
|
||||
"/mnt/workdrive/rune/.venv/bin/python",
|
||||
"/home/nick/.venvs/memory/bin/python3",
|
||||
"/usr/bin/python3",
|
||||
].filter((p) => typeof p === "string" && p.length > 0)
|
||||
|
||||
function resolvePy() {
|
||||
for (const p of PY_CANDIDATES) {
|
||||
try {
|
||||
if (existsSync(p)) return p
|
||||
} catch {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Per-process dedupe: identical task text within the cache TTL does not need
|
||||
// a second bridge call — the CLI has its own TTL cache, but skipping the
|
||||
// process spawn entirely for the same message text within 10 minutes keeps
|
||||
// repeats at ~0ms.
|
||||
const lastText = new Map() // text -> timestamp
|
||||
const TTL = 600_000
|
||||
|
||||
function extractUserText(messages) {
|
||||
// messages: Array<{ info: Message, parts: Part[] }> — newest last.
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const m = messages[i]
|
||||
if (m?.info?.role !== "user") continue
|
||||
const texts = []
|
||||
for (const p of m.parts ?? []) {
|
||||
if (p?.type === "text" && typeof p.text === "string") texts.push(p.text)
|
||||
}
|
||||
if (texts.length) return texts.join("\n").slice(0, 4000)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
export default {
|
||||
"experimental.chat.messages.transform": async (input, output) => {
|
||||
try {
|
||||
if (!Array.isArray(output.messages) || output.messages.length === 0) return
|
||||
const userText = extractUserText(output.messages)
|
||||
if (!userText || userText.length < 12) return // trivial/noise guard
|
||||
// Harness-internal steps (checkpoint writer, compaction, system
|
||||
// reminders) are not builder tasks — no capsule (measured: the fire
|
||||
// log showed checkpoint-writer steps receiving capsules 2026-09-10).
|
||||
if (/^(<system-reminder>|EXTRACTION REQUIRED)/.test(userText.trim())) return
|
||||
const now = Date.now()
|
||||
const prev = lastText.get(userText)
|
||||
if (prev !== undefined && now - prev < TTL) return // dedupe repeats
|
||||
lastText.set(userText, now)
|
||||
if (lastText.size > 64) {
|
||||
// evict oldest
|
||||
let oldestKey = null, oldestVal = Infinity
|
||||
for (const [k, v] of lastText) if (v < oldestVal) { oldestVal = v; oldestKey = k }
|
||||
if (oldestKey) lastText.delete(oldestKey)
|
||||
}
|
||||
|
||||
const py = resolvePy()
|
||||
if (!py) return // no durable interpreter — degrade (never block)
|
||||
|
||||
const capsule = await new Promise((resolve) => {
|
||||
execFile(py, [BRIDGE, userText], { timeout: 15_000 }, (err, stdout) => {
|
||||
resolve(err ? "" : (stdout?.toString() ?? ""))
|
||||
})
|
||||
})
|
||||
if (!capsule.trim()) return
|
||||
|
||||
// Durable fire-trace: one line per ACTUAL injection into an outgoing
|
||||
// model request (handoff §1 proof requirement). Not a canary — this is
|
||||
// standing observability for the bridge's consumer edge.
|
||||
try {
|
||||
appendFileSync("/tmp/rune-bridge-fire.log",
|
||||
JSON.stringify({ ts: new Date().toISOString(), task: userText.slice(0, 80), capsule_tokens: Math.round(capsule.length / 4), py }) + "\n")
|
||||
} catch {}
|
||||
|
||||
// Append the capsule to the LAST user message (the one being answered).
|
||||
const last = output.messages[output.messages.length - 1]
|
||||
if (last?.parts && Array.isArray(last.parts)) {
|
||||
last.parts.push({
|
||||
type: "text",
|
||||
text: "\n\n<rune-preflight>\n" + capsule.trim() + "\n</rune-preflight>\n(Injected by Rune Core builder bridge — deterministic preflight, zero model calls. Relevant state above; ignore if not applicable to this task.)",
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// never block the builder on bridge failure
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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
|
||||
@@ -1,408 +0,0 @@
|
||||
import { execFile } from "node:child_process"
|
||||
import { existsSync, appendFileSync, readFileSync } from "node:fs"
|
||||
|
||||
// scaffolding-guard — the MiMo-surface construction enforcement edge (T23).
|
||||
//
|
||||
// WHY THIS EXISTS
|
||||
// T22 proved the accepted Scaffolding V1 authority was reachable from the
|
||||
// TUI/operator path but NOT from the agent's own mutation surface, and
|
||||
// recorded the honest boundary: "no byte-level enforcement point exists …
|
||||
// Establishing MiMo-surface enforcement is NEW architecture (a
|
||||
// tool.execute.before governance hook) and was NOT built here — it is
|
||||
// recorded as the exact remaining blocker." This file IS that seam.
|
||||
//
|
||||
// It is the harness half ONLY. Every decision is delegated to
|
||||
// `src/rune/construction_authority_cli.py` → `rune.construction_authority`,
|
||||
// which derives from the canonical construction store. There is no
|
||||
// construction logic here: if you find yourself adding policy to this file,
|
||||
// it belongs in Python instead (T23 §36 — no parallel construction system).
|
||||
//
|
||||
// TWO EVENTS, TWO JOBS
|
||||
// experimental.chat.messages.transform — the TURN BOUNDARY. Classify the
|
||||
// user's intent once and create/resume the canonical contract
|
||||
// (tranche §83: classify at the task boundary, never per tool call).
|
||||
// tool.execute.before — the ENFORCEMENT BOUNDARY. Per mutating tool call,
|
||||
// ask whether the mutation is inside the active contract's approved
|
||||
// scope; block it if not.
|
||||
//
|
||||
// BLOCKING CONTRACT (verified against @mimo-ai/plugin index.d.ts and the
|
||||
// decompiled harness): mutate `output` — `output.cancel = true` plus
|
||||
// `output.cancelReason`. The tool's execute is then never called and the
|
||||
// model receives `cancelReason` as the tool result. There is no
|
||||
// thrown-error convention. `output.args` is mutable but unused here.
|
||||
//
|
||||
// DEGRADATION (tranche §61/§62): a bridge failure must not silently become
|
||||
// either a denial or a bypass. We cache the last known authority; if the
|
||||
// bridge fails while a governed contract was active, we FAIL CLOSED
|
||||
// (conversational mutation on an active governed build stops with an
|
||||
// explicit reason). If no contract was active, we allow — ordinary
|
||||
// non-construction work must never become unusable.
|
||||
//
|
||||
// CANONICAL SOURCE: config/mimo-hooks/scaffolding-guard.ts
|
||||
// Installed to ~/.mimocode/hooks/scaffolding-guard.ts by
|
||||
// `scripts/install_mimo_hook.py` (hash-verified, drift-detected).
|
||||
|
||||
const REPO = "/mnt/workdrive/rune"
|
||||
const CLI = `${REPO}/src/rune/construction_authority_cli.py`
|
||||
const LOG = "/tmp/rune-scaffolding-guard.log"
|
||||
|
||||
// The DURABLE authority lock, written by the canonical authority
|
||||
// (`construction_authority.write_authority_lock`) on every successful check.
|
||||
// The hook reads it ONLY during bridge failure and ONLY to become MORE
|
||||
// conservative (§62: degradation must never silently become a bypass).
|
||||
//
|
||||
// Why the hook needs it at all: `lastAuthority` below is in-process state, so a
|
||||
// FRESH hook process — a new session, a reinstall, a restarted harness — starts
|
||||
// with no memory that a governed build was active, and a bridge failure would
|
||||
// then be allowed. That was the measured gap: fail-closed worked, but it did
|
||||
// not survive a fresh process. The lock is the durable half of that decision.
|
||||
//
|
||||
// It can never GRANT authority: there is no code path here that turns a lock
|
||||
// into permission. ACTIVE or unreadable → stop; absent → no durable evidence.
|
||||
const LOCK = `${REPO}/store/authority-lock.json`
|
||||
|
||||
// Same durable interpreter resolution as builder-bridge.ts (T22 repair:
|
||||
// never hardcode an ephemeral interpreter path).
|
||||
const PY_CANDIDATES = [
|
||||
process.env.MIMO_BRIDGE_PY,
|
||||
`${REPO}/.test-venv/bin/python`,
|
||||
"/home/nick/.venvs/memory/bin/python3",
|
||||
"/usr/bin/python3",
|
||||
].filter((p) => typeof p === "string" && p.length > 0)
|
||||
|
||||
function resolvePy() {
|
||||
for (const p of PY_CANDIDATES) {
|
||||
try {
|
||||
if (existsSync(p)) return p
|
||||
} catch {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Tools that can change project state. Reads/search/list are deliberately
|
||||
// absent — they cannot mutate and must never be gated (tranche §12:
|
||||
// governance is universal for construction, ceremony is not).
|
||||
const MUTATING = new Set([
|
||||
"write", "edit", "patch", "multiedit", "multi_edit", "apply_patch",
|
||||
"notebook_edit", "write_file", "edit_file", "str_replace",
|
||||
"bash", "shell", "run_command", "execute", "terminal",
|
||||
])
|
||||
|
||||
function log(line) {
|
||||
try {
|
||||
appendFileSync(LOG, `${new Date().toISOString()} ${line}\n`)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Three states, and the distinction is load-bearing:
|
||||
// ACTIVE — a governed construction was live; bridge failure fails CLOSED
|
||||
// UNREADABLE — present but corrupt/unparseable; ALSO fails closed (a broken
|
||||
// lock is not evidence of absence — absence of evidence is not
|
||||
// evidence of absence)
|
||||
// ABSENT — no durable evidence of an active construction; ordinary work
|
||||
// must keep working (§62)
|
||||
function readDurableLock() {
|
||||
try {
|
||||
if (!existsSync(LOCK)) return { state: "ABSENT", contract_id: null }
|
||||
const doc = JSON.parse(readFileSync(LOCK, "utf8"))
|
||||
if (!doc || typeof doc !== "object" || !doc.active) {
|
||||
return { state: "UNREADABLE", contract_id: null }
|
||||
}
|
||||
return { state: "ACTIVE", contract_id: doc.contract_id ?? null }
|
||||
} catch (e) {
|
||||
return { state: "UNREADABLE", contract_id: null }
|
||||
}
|
||||
}
|
||||
|
||||
// Deterministic, secret-free diagnostic from a failed bridge call. The
|
||||
// previous guard logged only `err.message` ("Command failed: …"), which hid
|
||||
// the actual CLI error and made a FAIL-CLOSED record undiagnosable after the
|
||||
// fact. We surface the interpreter's own stderr tail and the payload op —
|
||||
// never the payload body (it can carry user working context).
|
||||
function summariseFailure(op, err, stderr) {
|
||||
const parts = [`op=${op}`]
|
||||
if (err) {
|
||||
parts.push(`kind=${err.killed ? "timeout" : err.code != null ? `exit=${err.code}` : "spawn"}`)
|
||||
if (err.message) parts.push(`msg=${String(err.message).slice(0, 200)}`)
|
||||
}
|
||||
const tail = String(stderr ?? "").trim().split("\n").slice(-3).join(" | ")
|
||||
if (tail) parts.push(`stderr=${tail.slice(0, 400)}`)
|
||||
return parts.join(" ")
|
||||
}
|
||||
|
||||
function runCli(payload) {
|
||||
return new Promise((resolve) => {
|
||||
const op = String(payload?.op ?? "?")
|
||||
const py = resolvePy()
|
||||
if (!py) {
|
||||
log(`bridge: NO INTERPRETER RESOLVED ${summariseFailure(op, null, "")}`)
|
||||
return resolve(null)
|
||||
}
|
||||
let done = false
|
||||
const finish = (v) => {
|
||||
if (!done) {
|
||||
done = true
|
||||
resolve(v)
|
||||
}
|
||||
}
|
||||
try {
|
||||
const child = execFile(
|
||||
py,
|
||||
[CLI],
|
||||
{ timeout: 8000, maxBuffer: 1 << 20 },
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
log(`bridge: error ${summariseFailure(op, err, stderr)}`)
|
||||
return finish(null)
|
||||
}
|
||||
try {
|
||||
finish(JSON.parse(String(stdout)))
|
||||
} catch (e) {
|
||||
log(
|
||||
`bridge: bad JSON op=${op} stdout=${String(stdout).slice(0, 200)} ` +
|
||||
summariseFailure(op, null, stderr),
|
||||
)
|
||||
finish(null)
|
||||
}
|
||||
},
|
||||
)
|
||||
// MEASURED DEFECT (T24 §9A4, found while forcing a bridge failure): when
|
||||
// the interpreter dies before reading stdin — a wrong binary, a crashed
|
||||
// Python, a killed process — this stream raises EPIPE *asynchronously*
|
||||
// and, unhandled, takes the whole HOOK process down. A crash is not
|
||||
// fail-closed; the harness receives no cancellation decision at all, so
|
||||
// the mutation proceeds. Swallowing it lets the execFile callback below
|
||||
// report the failure as the ordinary `null` result, which the enforcement
|
||||
// boundary then decides on.
|
||||
child.stdin?.on("error", () => {})
|
||||
child.stdin?.end(JSON.stringify(payload))
|
||||
} catch (e) {
|
||||
log(`bridge: spawn failed ${summariseFailure(op, e, "")}`)
|
||||
finish(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Last known authority, so a transient bridge failure can fail CLOSED
|
||||
// without breaking non-construction work.
|
||||
let lastAuthority = null // { active: boolean, contract_id?: string, at: number }
|
||||
let lastTurnText = ""
|
||||
|
||||
// The working tree as of the previous shell call. The lazy postcondition
|
||||
// diffs against this so it reports only what changed SINCE then — a governed
|
||||
// build normally starts in a dirty tree, and treating existing residue as
|
||||
// this command's writes would fail every build closed.
|
||||
let lastSnapshot = null
|
||||
|
||||
// Harness-internal traffic is NOT user construction intent. Measured defect
|
||||
// (T23, live): without this guard the turn boundary minted bogus
|
||||
// construction contracts from `<actor-notification>` envelopes — subagent
|
||||
// completion reports arriving as user-role messages — and then correctly
|
||||
// blocked the operator's own legitimate work against them. Same class of
|
||||
// guard builder-bridge.ts uses for `<system-reminder>` / checkpoint steps.
|
||||
// A genuine operator turn does not begin with an envelope tag.
|
||||
//
|
||||
// ENVELOPE BLIND SPOT (adversarial-escape audit, Door 1, HIGH — repaired):
|
||||
// the old rule SKIPPED classification for any envelope-shaped text, so a
|
||||
// cron/scheduled/envelope-wrapped BUILD instruction minted no contract and
|
||||
// mutated ungoverned. The rule is now: the envelope METADATA is exempt,
|
||||
// never the CONTENT — the payload is unwrapped and classified like any
|
||||
// other turn (origin="envelope").
|
||||
const INTERNAL_ENVELOPE =
|
||||
/^\s*(?:<[A-Za-z][A-Za-z0-9-]*|EXTRACTION REQUIRED|\[Never-Ask\]|\[inbox)/
|
||||
|
||||
function isInternal(text) {
|
||||
return INTERNAL_ENVELOPE.test(text)
|
||||
}
|
||||
|
||||
// Strip envelope layers and return the payload. Closed tags unwrap to
|
||||
// their inner text; an unclosed leading tag, a bracket group ([inbox …],
|
||||
// [Never-Ask]) or an "EXTRACTION REQUIRED" banner is stripped as metadata.
|
||||
// Returns "" when nothing but metadata remains.
|
||||
function unwrapEnvelope(text) {
|
||||
let cur = text
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (!isInternal(cur)) break
|
||||
let next = cur
|
||||
const tagged = cur.match(
|
||||
/^\s*<([A-Za-z][A-Za-z0-9-]*)[^>]*>([\s\S]*?)<\/\1\s*>?\s*$/)
|
||||
if (tagged) {
|
||||
next = tagged[2]
|
||||
} else {
|
||||
next = next
|
||||
.replace(/^\s*<[A-Za-z][A-Za-z0-9-]*[^>]*>/, "")
|
||||
.replace(/^\s*\[[^\]\n]{0,120}\]\s*/, "")
|
||||
.replace(/^\s*EXTRACTION REQUIRED\s*/i, "")
|
||||
}
|
||||
const t = next.trim()
|
||||
if (!t || t === cur.trim()) break
|
||||
cur = t
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
// Interpreters that can execute a script which mutates the repo — the
|
||||
// generated-script route (tranche §21). Only these trigger postcondition
|
||||
// verification, which is the one place we pay a git call.
|
||||
const SCRIPT_EXEC =
|
||||
/(?:^|\s)(?:python3?|node|bun|deno|sh|bash|zsh|perl|ruby|php|make|npm|pnpm|yarn|cargo|go)(?:\s|$)/
|
||||
|
||||
function extractUserText(messages) {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const m = messages[i]
|
||||
if (m?.info?.role !== "user") continue
|
||||
const texts = []
|
||||
for (const p of m.parts ?? []) {
|
||||
if (p?.type === "text" && typeof p.text === "string") texts.push(p.text)
|
||||
}
|
||||
if (texts.length) return texts.join("\n").slice(0, 4000)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
export const ScaffoldingGuard = {
|
||||
// ---- TURN BOUNDARY: classify once, create/resume the contract --------
|
||||
"experimental.chat.messages.transform": async (_input, output) => {
|
||||
try {
|
||||
if (!Array.isArray(output?.messages) || output.messages.length === 0) return
|
||||
const text = extractUserText(output.messages)
|
||||
if (!text || text.length < 12) return
|
||||
// Harness-internal envelopes: the METADATA is exempt, the CONTENT is
|
||||
// not. Unwrap and classify the payload (audit Door 1).
|
||||
let classified = text
|
||||
let origin = "mimo"
|
||||
if (isInternal(text)) {
|
||||
const payload = unwrapEnvelope(text)
|
||||
if (!payload || payload.length < 12) return // pure metadata
|
||||
classified = payload
|
||||
origin = "envelope"
|
||||
}
|
||||
if (classified === lastTurnText) return // dedupe: one classification per turn
|
||||
lastTurnText = classified
|
||||
|
||||
const res = await runCli({
|
||||
op: "begin",
|
||||
text: classified,
|
||||
cwd: REPO,
|
||||
origin,
|
||||
})
|
||||
if (!res || res.ok !== true) {
|
||||
log(`begin: no result (${JSON.stringify(res)})`)
|
||||
return
|
||||
}
|
||||
log(
|
||||
`begin: mode=${res.mode} activated=${res.activated} ` +
|
||||
`contract=${res.contract_id} intent=${res.intent_layer} ` +
|
||||
`scope=${JSON.stringify(res.scope ?? [])}`,
|
||||
)
|
||||
if (res.activated && res.contract_id) {
|
||||
lastAuthority = { active: true, contract_id: res.contract_id, at: Date.now() }
|
||||
} else {
|
||||
lastAuthority = { active: false, at: Date.now() }
|
||||
}
|
||||
} catch (e) {
|
||||
log(`begin: threw ${e}`)
|
||||
}
|
||||
},
|
||||
|
||||
// ---- ENFORCEMENT BOUNDARY: per mutating tool call --------------------
|
||||
"tool.execute.before": async (input, output) => {
|
||||
try {
|
||||
const tool = String(input?.tool ?? "")
|
||||
if (!MUTATING.has(tool.toLowerCase())) return
|
||||
|
||||
// LAZY POSTCONDITION (runs in the wired `before` event).
|
||||
// `tool.execute.after` is declared in the hook API but is NOT triggered
|
||||
// at runtime in this harness build — measured: 485 `before` events and
|
||||
// zero harness `after` events, so a postcondition wired to `after`
|
||||
// would silently never run. Detection therefore happens here instead,
|
||||
// comparing the tree against the previous call's snapshot, which also
|
||||
// means the delta is known BEFORE the next mutation is authorised.
|
||||
//
|
||||
// Runs for EVERY consequential mutation, not just shell calls. Measured
|
||||
// temporal gap: with the checkpoint gated to shell tools, an escaping
|
||||
// script followed by a native write/edit was authorised without the
|
||||
// pending delta ever being reconciled.
|
||||
{
|
||||
const snap = await runCli({
|
||||
op: "checkpoint",
|
||||
cwd: REPO,
|
||||
before: lastSnapshot,
|
||||
})
|
||||
if (snap && snap.ok === true) {
|
||||
lastSnapshot = snap.entries ?? {}
|
||||
if ((snap.violations ?? []).length > 0) {
|
||||
log(`checkpoint: VIOLATION out_of_scope=${JSON.stringify(snap.violations)}`)
|
||||
} else if ((snap.changed ?? []).length > 0) {
|
||||
log(`checkpoint: changed=${JSON.stringify(snap.changed)} (in scope)`)
|
||||
}
|
||||
} else {
|
||||
log(`checkpoint: no result (${JSON.stringify(snap)})`)
|
||||
}
|
||||
}
|
||||
|
||||
const res = await runCli({
|
||||
op: "check",
|
||||
tool,
|
||||
args: output?.args ?? {},
|
||||
cwd: REPO,
|
||||
// G6: the turn's own words are what a promotion would mint the
|
||||
// contract from. Passed only as evidence for a promotion the
|
||||
// authority decides on — the hook does not decide.
|
||||
promote_text: lastTurnText || undefined,
|
||||
})
|
||||
|
||||
if (!res || res.ok !== true) {
|
||||
// Bridge failure. Fail CLOSED only when a governed build was known
|
||||
// active; otherwise let ordinary work continue (§61/§62).
|
||||
//
|
||||
// "Known active" now has TWO sources, and either one is sufficient:
|
||||
// * `lastAuthority` — what this process saw before the bridge broke;
|
||||
// * the DURABLE LOCK — what the canonical authority last recorded,
|
||||
// which is the only source available to a FRESH process.
|
||||
// The lock is consulted strictly to escalate. An ABSENT lock cannot
|
||||
// turn a known-active in-process contract into an allow.
|
||||
const durable = readDurableLock()
|
||||
const inProcess = !!lastAuthority?.active
|
||||
const durableStops = durable.state === "ACTIVE" || durable.state === "UNREADABLE"
|
||||
if (inProcess || durableStops) {
|
||||
const source = inProcess
|
||||
? `in-process contract=${lastAuthority.contract_id}`
|
||||
: `durable lock state=${durable.state} contract=${durable.contract_id}`
|
||||
output.cancel = true
|
||||
output.cancelReason =
|
||||
"Scaffolding guard unavailable while a governed construction " +
|
||||
"contract is active. Consequential mutation stopped (fail-closed). " +
|
||||
"Re-run once the Rune construction authority is reachable. " +
|
||||
`[evidence: ${source}]`
|
||||
log(`check: FAIL-CLOSED on bridge failure (${source})`)
|
||||
} else {
|
||||
log("check: bridge failure, no active contract (in-process or durable) — allowed")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lastAuthority = {
|
||||
active: !!res.contract_id && res.mode !== "NO_CONTRACT",
|
||||
contract_id: res.contract_id ?? undefined,
|
||||
at: Date.now(),
|
||||
}
|
||||
|
||||
log(
|
||||
`check: tool=${tool} allow=${res.allow} mode=${res.mode} ` +
|
||||
`contract=${res.contract_id} scope=${JSON.stringify(res.scope ?? [])} ` +
|
||||
`checked=${JSON.stringify(res.checked ?? [])}`,
|
||||
)
|
||||
|
||||
if (res.allow === false) {
|
||||
output.cancel = true
|
||||
output.cancelReason = String(res.reason || "Blocked by construction scope")
|
||||
}
|
||||
} catch (e) {
|
||||
log(`check: threw ${e}`)
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
export default ScaffoldingGuard
|
||||
@@ -1,174 +0,0 @@
|
||||
// subagent-protocol-guard — the SPAWN BOUNDARY (owner directive 2026-10-19:
|
||||
// "systematically make it so that any subagent that gets launched has to
|
||||
// follow the scaffolding protocol").
|
||||
//
|
||||
// WHAT IT DOES: every actor spawn/run whose brief describes BUILD work must
|
||||
// carry the scaffolding admission requirement (mint the construction
|
||||
// contract via the admit script before building) AND name a target surface
|
||||
// (a repo path or project name). Research-only waves must carry an explicit
|
||||
// `research:` PREFIX — self-declaration inside the body ("research
|
||||
// artifacts to research/") is no longer an exemption.
|
||||
//
|
||||
// WHY THE TIGHTENING (adversarial-escape audit, Door 5, HIGH):
|
||||
// * the old pass was purely lexical — a brief containing one marker word
|
||||
// ("scaffolding") satisfied admission for ANY brief;
|
||||
// * the research exemption was self-declared, so a build brief that
|
||||
// mentioned research artifacts passed freely;
|
||||
// * the guard itself was unversioned — no canonical copy, absent from
|
||||
// install_mimo_hook.py MANAGED, drift-undetected: the exact rot that
|
||||
// killed builder-bridge pre-T22.
|
||||
//
|
||||
// CONVENTION (docs/factory/order-of-things.md §wave-brief compliance law):
|
||||
// (1) SCAFFOLDING ADMISSION — "mint the construction contract" / "admit"
|
||||
// / "scaffolding" in the brief's build instructions;
|
||||
// (2) TARGET SURFACE — a repo path (contains a slash) or a known project
|
||||
// name, OUTSIDE the admission boilerplate itself;
|
||||
// (3) research gate exemption — the brief STARTS with "research:".
|
||||
//
|
||||
// The guard is deliberately conservative in BOTH directions: it must never
|
||||
// block a legitimate research wave (the factory's research-first law), and
|
||||
// it must never wave through a build brief that forgot its contract
|
||||
// (2026-10-19 drift: eight interactive-session build waves launched without
|
||||
// admission — recorded, backfilled, and prevented here structurally).
|
||||
//
|
||||
// CANONICAL SOURCE: config/mimo-hooks/subagent-protocol-guard.ts
|
||||
// Installed to ~/.mimocode/hooks/subagent-protocol-guard.ts by
|
||||
// `scripts/install_mimo_hook.py` (hash-verified, drift-detected).
|
||||
|
||||
interface Output {
|
||||
args?: unknown
|
||||
cancel?: boolean
|
||||
cancelReason?: string
|
||||
}
|
||||
|
||||
// Typed refusal reasons — machine-greppable, one per failure mode.
|
||||
export const REASON_TYPES = {
|
||||
NO_ADMISSION: "BUILD_BRIEF_NO_ADMISSION",
|
||||
NO_TARGET: "BUILD_BRIEF_NO_TARGET",
|
||||
} as const
|
||||
|
||||
const LOG = "/tmp/rune-subagent-protocol-guard.log"
|
||||
|
||||
const PROTOCOL_HINT = [
|
||||
"BLOCKED by subagent-protocol-guard: this launch brief describes BUILD work but does not satisfy the scaffolding protocol.",
|
||||
"Add to the brief (verbatim): 'Mint the construction contract via the scripts/admit_wave_*.py pattern (scaffolding admission) BEFORE building; phase=building; the main session lands.'",
|
||||
"AND name the target surface (repo path or project name) the work touches.",
|
||||
"If the wave is genuinely research-only (no code changes, artifacts only), START the brief with: 'research: ...'",
|
||||
"Law: docs/factory/order-of-things.md §wave-brief compliance; boundary-law.md scaffolding authority.",
|
||||
].join(" ")
|
||||
|
||||
// Words that indicate the brief directs code changes.
|
||||
const BUILD_MARKERS = [
|
||||
"build", "implement", "write the", "code", "red-first", "gofmt",
|
||||
"go test", "pytest", "compile", "wire", "port", "fix", "refactor",
|
||||
"land", "deploy",
|
||||
]
|
||||
// The compliance markers that satisfy the law for build briefs.
|
||||
const ADMISSION_MARKERS = [
|
||||
"scaffolding", "admit_wave", "admission contract", "mint the construction",
|
||||
"mint the admission", "construction contract",
|
||||
]
|
||||
// Known project surfaces — a bare project name counts as a target surface.
|
||||
const PROJECT_NAMES = [
|
||||
"rune", "rune-core", "rune-entry", "runecore", "tui", "githooks",
|
||||
"builder-bridge", "scaffolding-guard",
|
||||
]
|
||||
// The explicit research gate: the brief must START with "research:" —
|
||||
// self-declaration inside the body is not an exemption (audit Door 5b).
|
||||
const RESEARCH_PREFIX = /^\s*research:/i
|
||||
|
||||
export interface BriefVerdict {
|
||||
verdict: "allow" | "refuse"
|
||||
reasonTypes: string[]
|
||||
reason: string
|
||||
}
|
||||
|
||||
// Pure classification — exported for direct testing (tests/test_subagent_protocol_guard.py).
|
||||
export function classifyBrief(prompt: string): BriefVerdict {
|
||||
const lower = prompt.toLowerCase()
|
||||
|
||||
// Explicit research gate: pass freely (never block legitimate research).
|
||||
if (RESEARCH_PREFIX.test(prompt)) {
|
||||
return { verdict: "allow", reasonTypes: [], reason: "" }
|
||||
}
|
||||
|
||||
// Build-shaped brief? Word-boundary match — a substring match makes any
|
||||
// mention of "report" (contains "port") build-shaped.
|
||||
const isBuild = BUILD_MARKERS.some((m) =>
|
||||
new RegExp(`\\b${m.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\b`, "i").test(lower))
|
||||
if (!isBuild) {
|
||||
// neither build nor research-declared: not our case
|
||||
return { verdict: "allow", reasonTypes: [], reason: "" }
|
||||
}
|
||||
|
||||
// Build brief: the admission requirement must be present.
|
||||
const reasonTypes: string[] = []
|
||||
if (!ADMISSION_MARKERS.some((m) => lower.includes(m))) {
|
||||
reasonTypes.push(REASON_TYPES.NO_ADMISSION)
|
||||
}
|
||||
|
||||
// AND a target surface (path or project name), judged OUTSIDE the
|
||||
// admission boilerplate — otherwise the admission sentence's own
|
||||
// "scripts/admit_wave_*.py" mention would satisfy every brief. The
|
||||
// boilerplate is stripped per SENTENCE, not per line: a one-line brief
|
||||
// would otherwise lose its real target too.
|
||||
const surface = prompt
|
||||
.split(/(?<=[.;!])\s+/)
|
||||
.filter((sent) => !/mint the construction|admit_wave|scaffolding admission/i.test(sent))
|
||||
.join(" ")
|
||||
.replace(/https?:\/\/\S+/g, "") // URLs are not repo surfaces
|
||||
const hasPath = /(?:^|[\s"'`(=:\[])[\w.@~-]+\/[\w.@~*-]+/.test(surface)
|
||||
const hasProject = PROJECT_NAMES.some((p) =>
|
||||
new RegExp(`\\b${p.replace(/-/g, "\\-")}\\b`, "i").test(surface))
|
||||
if (!hasPath && !hasProject) {
|
||||
reasonTypes.push(REASON_TYPES.NO_TARGET)
|
||||
}
|
||||
|
||||
if (reasonTypes.length === 0) {
|
||||
return { verdict: "allow", reasonTypes: [], reason: "" }
|
||||
}
|
||||
return {
|
||||
verdict: "refuse",
|
||||
reasonTypes,
|
||||
reason: `[subagent-protocol-guard:${reasonTypes.join("+")}] ${PROTOCOL_HINT}`,
|
||||
}
|
||||
}
|
||||
|
||||
function promptText(args: unknown): string {
|
||||
try {
|
||||
const op = (args as { operation?: { prompt?: unknown; action?: unknown } })?.operation
|
||||
if (!op || typeof op !== "object") return ""
|
||||
if (op.action !== "spawn" && op.action !== "run") return ""
|
||||
const p = op.prompt
|
||||
if (typeof p === "string") return p
|
||||
return ""
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export const SubagentProtocolGuard = {
|
||||
"tool.execute.before": async (_input: unknown, output: Output) => {
|
||||
try {
|
||||
const prompt = promptText(output?.args)
|
||||
if (!prompt || prompt.length < 20) return
|
||||
const res = classifyBrief(prompt)
|
||||
if (res.verdict === "refuse") {
|
||||
output.cancel = true
|
||||
output.cancelReason = res.reason
|
||||
try {
|
||||
const { appendFileSync } = await import("node:fs")
|
||||
appendFileSync(
|
||||
LOG,
|
||||
`${new Date().toISOString()} REFUSED type=${res.reasonTypes.join("+")} (${prompt.length} chars)\n`,
|
||||
)
|
||||
} catch {}
|
||||
}
|
||||
} catch {
|
||||
// The guard must never break launching — fail open on its own errors,
|
||||
// fail closed only on the protocol check above.
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default SubagentProtocolGuard
|
||||
@@ -1,92 +0,0 @@
|
||||
import { tool } from "@mimo-ai/plugin"
|
||||
import { execSync } from "child_process"
|
||||
|
||||
export default tool({
|
||||
description: "Wait for an external operation (CI, deploy, release) to complete. Call this when you cannot make progress until an external process finishes. You do NOT need to know the sha, op_type, or duration — Rune's controller resolves those from the bound PendingOperation. Just call this when you want to wait.",
|
||||
args: {
|
||||
operation_id: tool.schema.string().optional().describe("Optional: the operation to wait for. If omitted, the controller resolves the current pending operation automatically."),
|
||||
task_id: tool.schema.string().optional().describe("Optional: the active task id (e.g. T4). When provided, resolution is scoped to operations bound to this task — stale global operations are never consulted."),
|
||||
sha: tool.schema.string().optional().describe("OPTIONAL HINT — non-authoritative. The controller uses this only to help resolve which operation you mean."),
|
||||
op_type: tool.schema.string().optional().describe("OPTIONAL HINT — non-authoritative. The controller derives the canonical operation type from repository/Gitea state."),
|
||||
deadline_secs: tool.schema.number().optional().describe("OPTIONAL HINT — non-authoritative. The controller applies governed policy timing. Real user deadlines must be stated in conversation."),
|
||||
},
|
||||
async execute(args) {
|
||||
// INTENT NORMALIZATION — model output is intent; the controller
|
||||
// owns execution semantics. All model args are hints recorded for
|
||||
// observability; canonical truth comes from the bound operation.
|
||||
const parts = []
|
||||
if (args.operation_id) parts.push(`--operation-id ${args.operation_id}`)
|
||||
if (args.task_id) parts.push(`--task-id ${args.task_id}`)
|
||||
if (args.sha) parts.push(`--sha ${args.sha}`)
|
||||
if (args.op_type) parts.push(`--model-op-type ${args.op_type}`)
|
||||
if (args.deadline_secs) parts.push(`--model-deadline ${args.deadline_secs}`)
|
||||
const normCmd = `python3 /mnt/workdrive/issue161-harness/scripts/await_intent_cli.py normalize ${parts.join(" ")}`
|
||||
let norm: any
|
||||
try {
|
||||
const normOut = execSync(normCmd, {
|
||||
cwd: "/mnt/workdrive/issue161-harness",
|
||||
encoding: "utf-8" as const,
|
||||
timeout: 120000,
|
||||
env: { ...process.env, RUNE_WR_ROOT: "/mnt/workdrive/wr-bridge" },
|
||||
})
|
||||
norm = JSON.parse(normOut.trim().split("\n").find((l: string) => l.startsWith("{")) || "{}")
|
||||
} catch (e: any) {
|
||||
return JSON.stringify({ ok: false, stage: "normalization", error: (e.stderr || e.message || "?").slice(0, 300) })
|
||||
}
|
||||
if (norm.normalized === "AMBIGUOUS") {
|
||||
return JSON.stringify({ ok: false, stage: "normalization", ...norm })
|
||||
}
|
||||
if (!norm.normalized) {
|
||||
return JSON.stringify({ ok: false, stage: "normalization", ...norm })
|
||||
}
|
||||
// canonical continuation: stored/derived op_type + POLICY deadline
|
||||
// (the sha for the wait comes from the normalization resolution,
|
||||
// NOT from the model's args)
|
||||
const waitSha = norm.sha_for_wait || args.sha || ""
|
||||
if (!waitSha) {
|
||||
return JSON.stringify({ ok: false, stage: "resolution", error: "no sha resolved for the pending operation", norm })
|
||||
}
|
||||
const cmd = `python3 /mnt/workdrive/issue161-harness/scripts/ci_event_wait.py --sha ${waitSha} --deadline ${norm.deadline_secs} --op-type ${norm.op_type}`
|
||||
try {
|
||||
const result = execSync(cmd, {
|
||||
cwd: "/mnt/workdrive/issue161-harness",
|
||||
timeout: (norm.deadline_secs + 60) * 1000,
|
||||
encoding: "utf-8" as const,
|
||||
env: { ...process.env, DEPLOY_SHA: waitSha, RUNE_WR_ROOT: "/mnt/workdrive/wr-bridge", RUNE_WR_CODE: "/home/nick/.local/share/mimo-wait-resume" },
|
||||
})
|
||||
const jl = result.trim().split("\n").find((l: string) => l.startsWith("{"))
|
||||
const envelope = jl ? JSON.parse(jl) : { ok: false, error: "no output" }
|
||||
return JSON.stringify({
|
||||
...envelope,
|
||||
normalization: {
|
||||
model_deadline_ignored: norm.model_deadline_ignored,
|
||||
model_op_type_ignored: norm.model_op_type_ignored,
|
||||
model_sha_ignored: !!args.sha && args.sha !== waitSha,
|
||||
canonical_op_type: norm.op_type,
|
||||
canonical_authority: norm.authority,
|
||||
deadline_provenance: norm.deadline_provenance,
|
||||
matched_by: norm.matched_by || norm.evidence || "auto-resolved",
|
||||
},
|
||||
})
|
||||
} catch (e: any) {
|
||||
const lines = (e.stdout || "").trim().split("\n")
|
||||
const jl = lines.find((l: string) => l.startsWith("{"))
|
||||
if (jl) {
|
||||
const envelope = JSON.parse(jl)
|
||||
return JSON.stringify({
|
||||
...envelope,
|
||||
normalization: {
|
||||
model_deadline_ignored: norm.model_deadline_ignored,
|
||||
model_op_type_ignored: norm.model_op_type_ignored,
|
||||
model_sha_ignored: !!args.sha && args.sha !== waitSha,
|
||||
canonical_op_type: norm.op_type,
|
||||
canonical_authority: norm.authority,
|
||||
deadline_provenance: norm.deadline_provenance,
|
||||
matched_by: norm.matched_by || norm.evidence || "auto-resolved",
|
||||
},
|
||||
})
|
||||
}
|
||||
return JSON.stringify({ ok: false, stage: "wait", error: (e.stderr || e.message || "?").slice(0, 300) })
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,167 +0,0 @@
|
||||
|
||||
// bash — executes a shell command. PROCESS-OWNERSHIP CONTRACT (v2):
|
||||
//
|
||||
// MODE A (tool-owned): the child is launched in its own process group;
|
||||
// stdout/stderr are CAPTURED pipes (never the TUI tty). On timeout or
|
||||
// failure the ENTIRE process group is signalled (SIGTERM, grace,
|
||||
// SIGKILL) so grandchildren cannot survive the invocation, then the
|
||||
// result is finalized. After finalization NO descendant retains a
|
||||
// writer into MiMo's streams.
|
||||
//
|
||||
// MODE B (explicitly detached): a command suffixed with " &" that the
|
||||
// user/model intends to outlive the tool (nohup/setsid/disown-style)
|
||||
// is executed via the detached helper: stdin=/dev/null, stdout/stderr
|
||||
// -> durable log file, own session (setsid), no MiMo PTY inheritance.
|
||||
// The tool returns the log path as the durable handle.
|
||||
//
|
||||
// Wait-intent compilation (sleep/check/poll aimed at a pending operation)
|
||||
// is preserved: interception happens at the SEMANTIC level via
|
||||
// await_intent route-bash, and a NO-OP for anything that is not a genuine
|
||||
// continuation of a live pending operation. Arbitrary shell syntax
|
||||
// (quotes, pipes, semicolons) passes through untouched — the router
|
||||
// response is only trusted when it carries normalized=true AND
|
||||
// ContinuePendingOperation with a concrete operation bound.
|
||||
import { tool } from "@mimo-ai/plugin"
|
||||
import { spawn, execFile } from "child_process"
|
||||
|
||||
const REPO = "/mnt/workdrive/issue161-harness"
|
||||
const WR_ROOT = "/mnt/workdrive/wr-bridge"
|
||||
|
||||
// ── MODE A: process-group-owned capture execution ──────────────────────
|
||||
interface OwnedResult { stdout: string; stderr: string; status: number; timedOut: boolean }
|
||||
|
||||
function runOwned(command: string, timeoutMs: number): Promise<OwnedResult> {
|
||||
return new Promise((resolve) => {
|
||||
// /bin/sh -c "command" in its OWN process group (detached:true makes
|
||||
// the child a session/group leader we can signal as a WHOLE).
|
||||
const child = spawn("/bin/sh", ["-c", command], {
|
||||
cwd: REPO,
|
||||
stdio: ["ignore", "pipe", "pipe"], // NEVER inherit the TUI tty
|
||||
env: process.env,
|
||||
detached: true, // own process group
|
||||
})
|
||||
let stdout = "", stderr = ""
|
||||
let timedOut = false
|
||||
let settled = false
|
||||
const outChunks: Buffer[] = []
|
||||
const errChunks: Buffer[] = []
|
||||
child.stdout!.on("data", (d) => outChunks.push(d))
|
||||
child.stderr!.on("data", (d) => errChunks.push(d))
|
||||
|
||||
const killGroup = (sig: NodeJS.Signals) => {
|
||||
try { process.kill(-child.pid!, sig) } catch { /* already gone */ }
|
||||
}
|
||||
const finalize = (status: number) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
// detach listeners, then resolve — no late listeners remain
|
||||
child.stdout!.removeAllListeners()
|
||||
child.stderr!.removeAllListeners()
|
||||
resolve({ stdout: Buffer.concat(outChunks).toString(), stderr: Buffer.concat(errChunks).toString(), status, timedOut })
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true
|
||||
killGroup("SIGTERM")
|
||||
// bounded grace for clean shutdown, then SIGKILL the group
|
||||
setTimeout(() => killGroup("SIGKILL"), 2000)
|
||||
// the exit/close event settles; a hard stop-guard settles anyway
|
||||
setTimeout(() => finalize(124), 4000)
|
||||
}, timeoutMs)
|
||||
|
||||
child.on("error", (e) => finalize(-1))
|
||||
child.on("close", (code) => finalize(code ?? -1))
|
||||
})
|
||||
}
|
||||
|
||||
// ── MODE B: explicit detachment for "cmd &" invocations ────────────────
|
||||
function runDetached(command: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const logPath = `/tmp/mimo-detached-${Date.now()}-${Math.floor(Math.random() * 1e6)}.log`
|
||||
const out = `exec >>"${logPath}" 2>&1; exec 0</dev/null; ${command}`
|
||||
const child = spawn("/bin/sh", ["-c", `setsid /bin/sh -c '${out.replace(/'/g, `'\\''`)}'`], {
|
||||
cwd: REPO,
|
||||
stdio: "ignore", // child rebinds its own fds — no PTY inheritance
|
||||
env: process.env,
|
||||
detached: true,
|
||||
// unref: MiMo's event loop does not wait for it
|
||||
})
|
||||
child.unref()
|
||||
resolve(JSON.stringify({
|
||||
detached: true,
|
||||
log: logPath,
|
||||
note: "process detached (setsid, own fds); output streams to the log file only; fd 0/1/2 do NOT reference the MiMo tty",
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
// ── wait-intent routing (semantic level, preserved from v1) ────────────
|
||||
async function tryCompileWaitIntent(command: string): Promise<string | null> {
|
||||
let routeOut: string
|
||||
try {
|
||||
routeOut = await new Promise<string>((resolve, reject) => {
|
||||
execFile("python3", [`${REPO}/scripts/await_intent_cli.py`, "route-bash", "--command", command], {
|
||||
cwd: REPO, timeout: 60000, encoding: "utf-8",
|
||||
env: { ...process.env, RUNE_WR_ROOT: WR_ROOT },
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
}, (err, stdout) => { if (err && !stdout) reject(err); else resolve(stdout || "") })
|
||||
})
|
||||
} catch {
|
||||
return null // router unavailable: normal execution (never block on routing)
|
||||
}
|
||||
const jl = routeOut.trim().split("\n").find((l) => l.startsWith("{"))
|
||||
if (!jl) return null
|
||||
let norm: any
|
||||
try { norm = JSON.parse(jl) } catch { return null }
|
||||
// STRICT gate: compile ONLY a normalized, concrete continuation.
|
||||
if (!(norm.normalized === true &&
|
||||
norm.action === "ContinuePendingOperation" &&
|
||||
norm.operation_id && norm.sha_for_wait)) return null
|
||||
const waitCmd = `python3 ${REPO}/scripts/ci_event_wait.py --sha ${norm.sha_for_wait} --deadline ${norm.deadline_secs} --op-type ${norm.op_type}`
|
||||
const r = await runOwned(waitCmd, (norm.deadline_secs + 60) * 1000)
|
||||
const jline = r.stdout.trim().split("\n").find((l) => l.startsWith("{"))
|
||||
if (jline) {
|
||||
return JSON.stringify({
|
||||
...JSON.parse(jline),
|
||||
intent_compilation: {
|
||||
raw_command_kind: norm.raw_command_kind || "sleep_poll",
|
||||
literal_sleep_executed: false,
|
||||
operation_id: norm.operation_id,
|
||||
canonical_op_type: norm.op_type,
|
||||
authority: norm.authority,
|
||||
},
|
||||
})
|
||||
}
|
||||
return JSON.stringify({ ok: false, stage: "compiled-wait", error: (r.stderr || "no output").slice(0, 300) })
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description: "Run a shell command. If the command expresses external wait intent (sleep/check/poll aimed at CI, deploy, or release operations with pending operations), it is automatically compiled to Rune's event-driven continuation — you do not need to manage wait mechanics. Plain local commands execute normally. Commands ending in ' &' run detached with output logged to a file.",
|
||||
args: {
|
||||
command: tool.schema.string().describe("Shell command to run"),
|
||||
},
|
||||
async execute(args) {
|
||||
// 1. wait-intent compilation (semantic, strict-gated)
|
||||
try {
|
||||
const compiled = await tryCompileWaitIntent(args.command)
|
||||
if (compiled !== null) return compiled
|
||||
} catch { /* fall through to normal execution */ }
|
||||
|
||||
// 2. MODE B: explicit detachment for backgrounded invocations
|
||||
const cmd = args.command
|
||||
if (/\b(nohup|setsid)\b/.test(cmd) || /&\s*$/.test(cmd)) {
|
||||
return await runDetached(cmd)
|
||||
}
|
||||
|
||||
// 3. MODE A: tool-owned, group-killed, captured execution
|
||||
const r = await runOwned(cmd, 120000)
|
||||
const text = r.stdout + (r.stderr ? `\n[stderr]\n${r.stderr}` : "")
|
||||
if (r.timedOut) {
|
||||
return `exit timed-out (process group terminated): ${text.slice(0, 500)}`
|
||||
}
|
||||
if (r.status !== 0) {
|
||||
return `exit ${r.status}: ${text.slice(0, 500) || "?"}`
|
||||
}
|
||||
return text || "(no output)"
|
||||
},
|
||||
})
|
||||
@@ -1,336 +0,0 @@
|
||||
{
|
||||
"_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-10-10T06:45:48Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
"modules": [
|
||||
"audit_ledger",
|
||||
"builder_bridge_cli",
|
||||
"core_server",
|
||||
"cs_loader",
|
||||
"evidence_capsule",
|
||||
"forge",
|
||||
"intent_continuity",
|
||||
"message_queue",
|
||||
"native_units",
|
||||
"provider_context",
|
||||
"research_budget"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_native",
|
||||
"canary_native",
|
||||
"canary_p0_05",
|
||||
"canary_p0_05",
|
||||
"canary_p1_01",
|
||||
"canary_p1_01",
|
||||
"canary_rune_entry",
|
||||
"canary_rune_serve"
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"description": "Quality control, drift detection, promotion, audit",
|
||||
"modules": [
|
||||
"build_pipeline_policy",
|
||||
"cc_loader",
|
||||
"contracts",
|
||||
"failure_auto_filer",
|
||||
"forge_client",
|
||||
"grading",
|
||||
"mutation_guards",
|
||||
"paths",
|
||||
"programme_gates",
|
||||
"sc_loader",
|
||||
"worthiness"
|
||||
],
|
||||
"depends_on": [],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_audit_ledger",
|
||||
"canary_full_system",
|
||||
"canary_full_system",
|
||||
"canary_schema_migration",
|
||||
"canary_schema_migration",
|
||||
"canary_spine_meta"
|
||||
]
|
||||
},
|
||||
"growth": {
|
||||
"description": "Learning and improvement",
|
||||
"modules": [
|
||||
"autonomy_loop",
|
||||
"behavior",
|
||||
"claim_audit",
|
||||
"cognitive_loop",
|
||||
"dream_cycle",
|
||||
"drift_cache",
|
||||
"drift_compression",
|
||||
"drift_core",
|
||||
"drift_ledger",
|
||||
"drift_observability",
|
||||
"growth_delta",
|
||||
"growth_pipeline",
|
||||
"growth_simulator",
|
||||
"meaningful_events",
|
||||
"night_mode",
|
||||
"outcome_ledger",
|
||||
"outcome_utility",
|
||||
"self_improve",
|
||||
"self_modify",
|
||||
"skill_compiler"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance",
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_full_system"
|
||||
]
|
||||
},
|
||||
"identity": {
|
||||
"description": "Who the agent is \u2014 persona, voice, anchors",
|
||||
"modules": [
|
||||
"identity_anchors",
|
||||
"persona_definition",
|
||||
"persona_gate",
|
||||
"prompt_sync",
|
||||
"safety_override",
|
||||
"self_model",
|
||||
"voice_hybrid",
|
||||
"voice_injector",
|
||||
"voice_sync"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_full_system"
|
||||
]
|
||||
},
|
||||
"inference": {
|
||||
"description": "Model routing and outbound requests",
|
||||
"modules": [
|
||||
"adaptive_router",
|
||||
"evidence_probe",
|
||||
"inference_paths",
|
||||
"inference_trace",
|
||||
"local_tool_runtime",
|
||||
"model_connect",
|
||||
"model_profiles",
|
||||
"model_registry",
|
||||
"model_routing",
|
||||
"openai_compat_transport",
|
||||
"provider_budget",
|
||||
"provider_registry",
|
||||
"recall_router",
|
||||
"recall_uplift",
|
||||
"response_adapter",
|
||||
"route_risk_flags",
|
||||
"standardized_providers",
|
||||
"tc_loader",
|
||||
"token_optimizer"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_p1_02",
|
||||
"canary_p1_02",
|
||||
"canary_p1_06",
|
||||
"canary_p1_06",
|
||||
"canary_resource_authority",
|
||||
"canary_resource_authority"
|
||||
]
|
||||
},
|
||||
"memory": {
|
||||
"description": "Storing, retrieving, and managing knowledge",
|
||||
"modules": [
|
||||
"abstraction",
|
||||
"abstraction_ladder",
|
||||
"auto_context",
|
||||
"cache",
|
||||
"concept_topology",
|
||||
"connection_pool",
|
||||
"curator",
|
||||
"drift-ledger",
|
||||
"experience_encoding",
|
||||
"ffi_query_memory_smoke",
|
||||
"graph_memory",
|
||||
"graph_shard_bridge",
|
||||
"lexicon_evolution",
|
||||
"memory_consolidation",
|
||||
"memory_env",
|
||||
"memory_selector",
|
||||
"memory_store",
|
||||
"memory_types",
|
||||
"mems_loader",
|
||||
"message_graph",
|
||||
"mr_loader",
|
||||
"ms_loader",
|
||||
"query_memory",
|
||||
"retrieval_benchmark",
|
||||
"shard_manager",
|
||||
"sleep_time",
|
||||
"state_checkpoint",
|
||||
"subconscious_pass",
|
||||
"tier_manager",
|
||||
"warm_neuron_store",
|
||||
"warm_router"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_full_system",
|
||||
"canary_memory_synthesis",
|
||||
"canary_memory_synthesis",
|
||||
"canary_query_memory",
|
||||
"canary_query_memory"
|
||||
]
|
||||
},
|
||||
"orchestration": {
|
||||
"description": "Task lifecycle, operator, TUI presentation",
|
||||
"modules": [
|
||||
"attention_capsule",
|
||||
"auto_detect",
|
||||
"build_mode",
|
||||
"build_orchestrator",
|
||||
"builder_bridge",
|
||||
"capability_broker",
|
||||
"chat_bubble",
|
||||
"context_broker",
|
||||
"cron_engine",
|
||||
"custodian",
|
||||
"dependency_tracker",
|
||||
"elastic_cognition",
|
||||
"event_bus",
|
||||
"event_envelope",
|
||||
"execution_trace",
|
||||
"gitea_auth",
|
||||
"gitea_pipeline",
|
||||
"gitea_provision",
|
||||
"goal",
|
||||
"hook_engine",
|
||||
"message_widgets",
|
||||
"microkernel",
|
||||
"pane_authority",
|
||||
"parallel_processor",
|
||||
"pins",
|
||||
"progressive_escalation",
|
||||
"rune_graphic",
|
||||
"rune_operator",
|
||||
"rune_profiler",
|
||||
"rune_spinner",
|
||||
"subsystem_health",
|
||||
"system_registry",
|
||||
"task_gate",
|
||||
"task_persist",
|
||||
"tiered_executor",
|
||||
"time_awareness",
|
||||
"tool_executor",
|
||||
"tool_protocol",
|
||||
"tui_transport",
|
||||
"tui_validator",
|
||||
"umbrella_update",
|
||||
"verification_layer",
|
||||
"witness",
|
||||
"work_manager"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability",
|
||||
"growth",
|
||||
"identity",
|
||||
"inference",
|
||||
"perception"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_cognitive_strata",
|
||||
"canary_cognitive_strata",
|
||||
"canary_conversation_state",
|
||||
"canary_conversation_state",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_p1_07",
|
||||
"canary_p1_07",
|
||||
"canary_surface_parity",
|
||||
"canary_surface_parity",
|
||||
"canary_tui_drift",
|
||||
"canary_tui_drift",
|
||||
"canary_tui_transport"
|
||||
]
|
||||
},
|
||||
"perception": {
|
||||
"description": "Input processing and context assembly",
|
||||
"modules": [
|
||||
"auto_tasks",
|
||||
"behaviour_ledger",
|
||||
"build_basis",
|
||||
"capability_fit",
|
||||
"capability_ledger",
|
||||
"cognitive_budgeter",
|
||||
"construction_activation",
|
||||
"construction_admission",
|
||||
"construction_commands",
|
||||
"construction_decision",
|
||||
"construction_depth",
|
||||
"construction_intent",
|
||||
"construction_interface",
|
||||
"construction_mode",
|
||||
"construction_pattern",
|
||||
"construction_patterns_builtin",
|
||||
"construction_projection",
|
||||
"context_pressure",
|
||||
"drift_resume",
|
||||
"event_wait",
|
||||
"failure_recovery",
|
||||
"graph_projection",
|
||||
"impact_preview",
|
||||
"iteration",
|
||||
"layout_contract",
|
||||
"programme_continuation",
|
||||
"project_registry",
|
||||
"release_notes",
|
||||
"rune_tui",
|
||||
"salience_gate",
|
||||
"scaffold",
|
||||
"spine",
|
||||
"spine_language",
|
||||
"spine_loader",
|
||||
"stage_guards",
|
||||
"structural_migration",
|
||||
"structural_model",
|
||||
"structural_store",
|
||||
"testing_mode",
|
||||
"toolchain",
|
||||
"tui_backlog",
|
||||
"voicebox",
|
||||
"wait_guard",
|
||||
"wait_guard_corpus"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_context_broker",
|
||||
"canary_full_system",
|
||||
"canary_full_system"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
{
|
||||
"_comment": "Umbrella impact map \u2014 the change-propagation graph. Every src/rune/*.py and shared/*.py module MUST appear in exactly one umbrella's modules list (enforced by the umbrella_manifest_is_complete henxel; debt-tracked modules allowed via docs/module-coverage-debt.md). depends_on: when umbrella U changes, its dependents' canaries are required gates. mirrors: files that must travel together in the same PR (enforced by scripts/umbrella_impact.py unless the PR carries 'Propagation-waived: <reason>').",
|
||||
"umbrellas": {
|
||||
"memory": {
|
||||
"description": "Storing, retrieving, and managing knowledge",
|
||||
"modules": [
|
||||
"memory_store",
|
||||
"memory_consolidation",
|
||||
"memory_env",
|
||||
"shard_manager",
|
||||
"query_memory",
|
||||
"graph_memory",
|
||||
"warm_neuron_store",
|
||||
"abstraction",
|
||||
"cache",
|
||||
"recall_router",
|
||||
"drift_cache",
|
||||
"drift_compression",
|
||||
"tier_manager",
|
||||
"graph_shard_bridge",
|
||||
"memory_selector",
|
||||
"retrieval_benchmark"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_query_memory",
|
||||
"canary_memory_synthesis",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"description": "Quality control, drift detection, promotion, audit",
|
||||
"modules": [
|
||||
"drift_core",
|
||||
"drift_observability",
|
||||
"drift_ledger",
|
||||
"audit_ledger",
|
||||
"curator",
|
||||
"claim_audit",
|
||||
"contracts",
|
||||
"build_pipeline_policy",
|
||||
"failure_auto_filer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_schema_migration"
|
||||
],
|
||||
"depends_on": []
|
||||
},
|
||||
"identity": {
|
||||
"description": "Who the agent is \u2014 persona, voice, anchors",
|
||||
"modules": [
|
||||
"persona_definition",
|
||||
"persona_gate",
|
||||
"identity_anchors",
|
||||
"voice_hybrid",
|
||||
"voice_sync",
|
||||
"voicebox",
|
||||
"voice_injector",
|
||||
"behavior",
|
||||
"prompt_sync"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"growth": {
|
||||
"description": "Learning and improvement",
|
||||
"modules": [
|
||||
"self_improve",
|
||||
"subconscious_pass",
|
||||
"goal",
|
||||
"elastic_cognition",
|
||||
"growth_delta",
|
||||
"autonomy_loop",
|
||||
"cognitive_loop",
|
||||
"dream_cycle",
|
||||
"growth_simulator"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory",
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"perception": {
|
||||
"description": "Input processing and context assembly",
|
||||
"modules": [
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
"auto_tasks",
|
||||
"witness",
|
||||
"salience_gate",
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
]
|
||||
},
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
"modules": [
|
||||
"capability_broker",
|
||||
"tool_executor",
|
||||
"tool_protocol",
|
||||
"local_tool_runtime",
|
||||
"intent_continuity",
|
||||
"openai_compat_transport",
|
||||
"forge_client",
|
||||
"message_queue",
|
||||
"connection_pool",
|
||||
"adaptive_router",
|
||||
"route_risk_flags",
|
||||
"evidence_probe",
|
||||
"builder_bridge",
|
||||
"builder_bridge_cli",
|
||||
"recall_uplift"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_01",
|
||||
"canary_p0_05",
|
||||
"canary_native"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
]
|
||||
},
|
||||
"inference": {
|
||||
"description": "Model routing and outbound requests",
|
||||
"modules": [
|
||||
"model_routing",
|
||||
"model_profiles",
|
||||
"model_registry",
|
||||
"model_connect",
|
||||
"provider_registry",
|
||||
"provider_budget",
|
||||
"standardized_providers",
|
||||
"inference_trace",
|
||||
"response_adapter",
|
||||
"token_optimizer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_06",
|
||||
"canary_p1_02",
|
||||
"canary_resource_authority"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability"
|
||||
]
|
||||
},
|
||||
"orchestration": {
|
||||
"description": "Task lifecycle, operator, TUI presentation",
|
||||
"modules": [
|
||||
"rune_operator",
|
||||
"rune_tui",
|
||||
"rune_graphic",
|
||||
"rune_spinner",
|
||||
"rune_profiler",
|
||||
"chat_bubble",
|
||||
"message_widgets",
|
||||
"tui_validator",
|
||||
"microkernel",
|
||||
"event_bus",
|
||||
"event_envelope",
|
||||
"hook_engine",
|
||||
"system_registry",
|
||||
"cron_engine",
|
||||
"task_gate",
|
||||
"task_persist",
|
||||
"work_manager",
|
||||
"parallel_processor",
|
||||
"tiered_executor",
|
||||
"time_awareness",
|
||||
"night_mode",
|
||||
"pins",
|
||||
"progressive_escalation",
|
||||
"safety_override",
|
||||
"sleep_time",
|
||||
"verification_layer",
|
||||
"outcome_ledger",
|
||||
"warm_router",
|
||||
"subsystem_health",
|
||||
"build_mode",
|
||||
"dependency_tracker",
|
||||
"auto_detect",
|
||||
"build_orchestrator",
|
||||
"custodian",
|
||||
"gitea_auth",
|
||||
"gitea_pipeline",
|
||||
"gitea_provision",
|
||||
"umbrella_update"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_07",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_conversation_state",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_tui_drift",
|
||||
"canary_cognitive_strata",
|
||||
"canary_surface_parity"
|
||||
],
|
||||
"depends_on": [
|
||||
"perception",
|
||||
"capability",
|
||||
"inference",
|
||||
"identity",
|
||||
"growth"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,16 +103,16 @@
|
||||
"evidence": "REMOVED by P0-04 (#20): _run_warm_router is a documented no-op; the ContextBroker (#19) owns all model-facing context. See docs/pipeline-removal.md."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/attention_capsule.py",
|
||||
"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 \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/attention_capsule.py:96-163",
|
||||
"role": "Bounded working-set candidate",
|
||||
"disposition": "dormant-until-dependency",
|
||||
"evidence": "Creates a bounded capsule but does not own prompt construction."
|
||||
},
|
||||
{
|
||||
"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 \u2014 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 — builds FROM the broker trace, never re-measures; store state/inference_traces.jsonl."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -151,34 +151,28 @@
|
||||
},
|
||||
{
|
||||
"id": "capability_selection",
|
||||
"current_authority": "CapabilityBroker.select_for_job owns tool-schema exposure; executor keeps execution authority",
|
||||
"current_authority": "All tool schemas in LocalToolRuntime; broker is advisory",
|
||||
"v1_owner": "ContextBroker.capability_selector",
|
||||
"dependency_issue": "#24",
|
||||
"migration_rule": "Select a bounded tool schema set per CognitiveJob and retain one discovery escape hatch.",
|
||||
"current_paths": [
|
||||
{
|
||||
"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 \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": "src/rune/capability_broker.py:293-394",
|
||||
"role": "Capability-card generation and relevance matching candidate",
|
||||
"disposition": "dormant-until-dependency",
|
||||
"evidence": "Matches cards but is not passed into send_task."
|
||||
},
|
||||
{
|
||||
"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 \u2014 the packaged empty array no longer passes as an index; cards carry risk/preconditions/side_effects copied from tool_executor.TOOLS."
|
||||
"role": "Packaged capability-card index",
|
||||
"disposition": "archive-candidate",
|
||||
"evidence": "Packaged file is an empty array and empty content does not trigger rebuild."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/local_tool_runtime.py",
|
||||
"role": "Runtime consumes the selected set; find_capability escape hatch exposes discovered tools in-run",
|
||||
"path": "src/rune/local_tool_runtime.py:129-152",
|
||||
"role": "Current effective tool-schema selection",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "P1-01 (#24): tools= receives the broker-selected schemas (zero-token guarantee for unselected capabilities); unknown-tool errors name find_capability; resume round-trips rebuild the persisted exposed_tools set."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/rune_operator.py",
|
||||
"role": "Operator wiring: selection flows to both LocalToolRuntime sites and the tools prompt block",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "P1-01 (#24): _capability_selection consults the broker once per task; trace schema v3 records selected_tools / capability_reason / find_capability_calls."
|
||||
"evidence": "Defaults the runtime tool list to every tool definition."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -190,34 +184,28 @@
|
||||
"migration_rule": "One router selects the cheapest verified-capable model from measured profiles and records the reason.",
|
||||
"current_paths": [
|
||||
{
|
||||
"path": "src/rune/model_routing.py",
|
||||
"role": "Authoritative advisory ModelRouter + measured registry (ranked candidates, recorded reasons)",
|
||||
"path": "src/rune/rune_tui.py:3398-3414",
|
||||
"role": "Actual TUI model name resolution",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "P1-06 (#29): ModelRouter.rank() selects the cheapest verified-capable model from measured profiles (state/model_routing_profiles.json) and records the reason; registered as the single model_routing authority in composition.py."
|
||||
"evidence": "Resolves auto/local/cloud immediately before send_task."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/rune_tui.py:_get_model_name",
|
||||
"role": "TUI model-name resolution (delegates to _resolve_model_name)",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "P1-06 (#29): delegates to the module-level resolver; the cloud=qwen2.5:7b fake was replaced by an explicit fallback reason string."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/rune_operator.py:_fallback_model",
|
||||
"role": "ARCHIVED keyword model heuristic (was _decide_model)",
|
||||
"path": "src/rune/rune_operator.py:395-412",
|
||||
"role": "Fallback keyword model selection",
|
||||
"disposition": "archive-candidate",
|
||||
"evidence": "P1-06 (#29): the router replaces the keyword heuristic; the keyword fallback survives only on the router-unavailable degrade path."
|
||||
"evidence": "Maps task words directly to model IDs."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/model_registry.py:50-156",
|
||||
"role": "Tier/capability profile candidate",
|
||||
"disposition": "dormant-until-dependency",
|
||||
"evidence": "Registry is not consulted by ordinary RuneOperator turns; P1-06 encodes its ladder as ModelRouter.CAPABILITY_FLOOR."
|
||||
"evidence": "Registry is not consulted by ordinary RuneOperator turns."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/elastic_cognition.py:632-709",
|
||||
"role": "Parallel progressive-escalation candidate",
|
||||
"disposition": "archive-candidate",
|
||||
"evidence": "process_message is not called by the ordinary TUI path; P1-06 explicitly does not integrate the archive escalation engines."
|
||||
"evidence": "process_message is not called by the ordinary TUI path."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -228,17 +216,11 @@
|
||||
"dependency_issue": "#29",
|
||||
"migration_rule": "Standardize provider requests/events behind one interface without moving Rune state into providers.",
|
||||
"current_paths": [
|
||||
{
|
||||
"path": "src/rune/model_routing.py:ProviderAdapter",
|
||||
"role": "Chat-only provider boundary (runtime-checkable Protocol, forge idiom)",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "P1-06 (#29): OllamaAdapter delegates to OllamaChatTransport; cloud adapters are declared (CloudStubAdapter raises ProviderUnavailable) until credentials/config exist."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/local_tool_runtime.py:44-126",
|
||||
"role": "Live Ollama chat/capability transport",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "P1-06 (#29): the operator wires router-selected transports through ProviderAdapter; capability discovery stays in local_tool_runtime (#28 boundary)."
|
||||
"evidence": "RuneOperator instantiates OllamaChatTransport directly."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/provider_registry.py:128-320",
|
||||
@@ -486,6 +468,12 @@
|
||||
"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",
|
||||
@@ -498,9 +486,6 @@
|
||||
"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)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -537,4 +522,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
agents
|
||||
@@ -1,206 +0,0 @@
|
||||
---
|
||||
name: openspec-apply-change
|
||||
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks. Also use when the user says "openspec apply", "opsx apply", or "openspec implement".
|
||||
allowed-tools: Bash(openspec:*)
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.14.1"
|
||||
---
|
||||
|
||||
Implement tasks from an OpenSpec change.
|
||||
|
||||
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
|
||||
|
||||
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
|
||||
|
||||
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
|
||||
|
||||
Otherwise, with no root, what happens next depends on how this workflow was reached:
|
||||
|
||||
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
|
||||
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
|
||||
|
||||
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
|
||||
|
||||
**Input**: Optionally specify a change name (e.g., `/openspec-apply-change add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and ask the user to select one
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/openspec-apply-change <other>`).
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- `planningHome`, `changeRoot`, and `actionContext`: planning scope and edit constraints
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns:
|
||||
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status, source path, and source line
|
||||
- Dynamic instruction based on current state
|
||||
- Optional `context`: current required project instruction input from the selected root
|
||||
- Optional `operationGuidance`: current advisory guidance for apply
|
||||
- `missingArtifacts` (when present): required artifact ids with no output
|
||||
|
||||
**Handle states:**
|
||||
- If `state: "blocked"`: show the message and pause implementation.
|
||||
- If `missingArtifacts` is non-empty: suggest completing the missing artifacts. Run `openspec status --change "<name>" --json`, select the next `ready` artifact (not `skipped` or `blocked`), and use `openspec instructions "<artifact-id>" --change "<name>" --json` for its rules and template. Keep the selected `--store <id>` on both commands.
|
||||
- Otherwise, follow the CLI instruction to create or repair the schema-configured tracking file from existing planning artifacts. Do not assume another artifact is ready or start implementation while blocked.
|
||||
- If `state: "all_done"`: report that all tracked tasks are complete and suggest review or verification as appropriate before archiving
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
Treat `context` as a required prompt-level input. Read and consider it, and
|
||||
apply relevant project facts, conventions, and constraints while implementing.
|
||||
Treat `operationGuidance` as optional additive advice. Read and consider every
|
||||
entry, and follow entries that are applicable and compatible with the built-in
|
||||
workflow.
|
||||
|
||||
Keep both fields separate from CLI-returned state, missing artifacts, tasks,
|
||||
progress, `contextFiles`, and the built-in `instruction`. They are not
|
||||
evidence of task completion, do not replace the built-in instruction, and do
|
||||
not permit bypassing a blocked state. If context conflicts with the built-in
|
||||
instruction, an explicit user choice, or a CLI-controlled value, report the
|
||||
conflict and preserve the controlling value. If guidance is inapplicable or
|
||||
conflicts with those controlling inputs, do not follow it and explain why.
|
||||
These are prompt-level behavior contracts, not enforceable checks.
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read every file path listed under `contextFiles` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
Do not copy `context` or `operationGuidance` verbatim into implementation
|
||||
files or planning artifacts unless the user separately asks for that content.
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Before editing, confirm the checkbox at the returned `sourcePath` and `line` still matches the task description; if it does not, rerun the apply instructions and use the refreshed location
|
||||
- Mark the task complete at its returned `sourcePath` and `line`: `- [ ]` → `- [x]`
|
||||
- Rerun the apply instructions and confirm that task is now done and progress changed
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- A task needs work beyond what the spec and tasks describe, or you are tempted to drop, narrow, defer, or accept exceptions to specified behavior to make it fit → surface the added scope and ask; do not absorb it silently
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: report that tracked tasks are complete and suggest review or verification as appropriate before archiving
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
```
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
```
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tracked tasks are complete. Review or verify the change as appropriate
|
||||
before archiving. You can archive this change with `/openspec-archive-change`.
|
||||
```
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
```
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- When a task needs work beyond what the spec describes, surface the added scope and pause - never silently narrow, defer, or simplify away specified behavior
|
||||
- Only mark a task `- [x]` when its specified behavior is fully implemented, not when it is partially done or deferred
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
- Use each task's sourcePath and line to update its exact checkbox
|
||||
- Do not use context or operation guidance as proof that a task is complete
|
||||
- Apply relevant project context; report conflicts with controlling workflow inputs
|
||||
- Consider every guidance entry; explain any inapplicable or conflicting advice
|
||||
- Do not copy runtime context or operation guidance into implementation files or planning artifacts
|
||||
- Preserve CLI-controlled blocked/ready/all-done behavior and completion criteria
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||
@@ -1,221 +0,0 @@
|
||||
---
|
||||
name: openspec-archive-change
|
||||
description: Archive a completed OpenSpec change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. Also use when the user says "openspec archive" or "opsx archive".
|
||||
allowed-tools: Bash(openspec:*)
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.14.1"
|
||||
---
|
||||
|
||||
Archive a completed change in the experimental workflow.
|
||||
|
||||
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
|
||||
|
||||
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
|
||||
|
||||
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
|
||||
|
||||
Otherwise, with no root, what happens next depends on how this workflow was reached:
|
||||
|
||||
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
|
||||
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
|
||||
|
||||
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
|
||||
|
||||
`<capability-path>` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and ask the user to select one
|
||||
|
||||
When prompting, show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/openspec-archive-change <other>`).
|
||||
|
||||
**Load current archive inputs before the existing archive checks:**
|
||||
|
||||
After resolving the selected change and planning root, run:
|
||||
```bash
|
||||
openspec instructions archive --change "<name>" --json
|
||||
```
|
||||
Keep the same selected-root flags on this command. This lookup is advisory and
|
||||
optional: it only supplies extra prompt inputs, so it must never block archiving.
|
||||
If it exits non-zero or returns invalid JSON — for example on an older CLI that
|
||||
does not support this command yet — continue the archive workflow with no
|
||||
context and no operation guidance. Do not report an error and do not stop.
|
||||
|
||||
A successful response may omit both optional fields. Treat `context` as a
|
||||
required prompt-level input: read and consider it, and apply relevant project
|
||||
facts, conventions, and constraints. Treat `operationGuidance` as optional
|
||||
additive advice: read and consider every entry, and follow entries that are
|
||||
applicable and compatible with the built-in archive workflow.
|
||||
|
||||
Keep both fields separate from built-in steps, explicit user choices, resolved
|
||||
paths, CLI checks, and command contracts. If context conflicts with one of those
|
||||
controlling inputs, report the conflict and preserve the controlling value. If
|
||||
guidance is inapplicable or conflicts with a controlling input, do not follow it
|
||||
and explain why. Do not infer replacement paths, skipped prompts, or flags from
|
||||
either field, and do not copy their text verbatim into specs, change artifacts,
|
||||
or archive summaries unless the user separately asks for it. These are
|
||||
prompt-level behavior contracts, not enforceable checks.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used
|
||||
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context
|
||||
- `artifacts`: List of artifacts with their status (`done`, `skipped`, or other)
|
||||
|
||||
**If any artifacts are neither `done` nor `skipped`** (skipped artifacts satisfy the requirement - the change declares skip_specs):
|
||||
- Display warning listing incomplete artifacts
|
||||
- Ask the user to confirm they want to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Run `openspec list --json` with the same selected-root flags and find the
|
||||
entry in `changes` whose `name` exactly matches the selected change.
|
||||
Require exactly one match and nonnegative integer `totalTasks` and
|
||||
`completedTasks`, with `completedTasks <= totalTasks`. The CLI resolves
|
||||
the schema's tracked task files, including custom artifact names, output
|
||||
paths, and globs.
|
||||
Incomplete tasks = `totalTasks - completedTasks`.
|
||||
|
||||
Do not infer task completion from artifact status or the absence of a
|
||||
top-level `tasks.md`. If the lookup fails, returns invalid JSON, omits or
|
||||
duplicates the selected change, or returns invalid counts, report the problem
|
||||
and stop before syncing or archiving.
|
||||
The CLI counts only `x`/`X` checkbox markers as complete;
|
||||
other markers, including unfamiliar ones, remain incomplete.
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Ask the user to confirm they want to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
**If `totalTasks` is zero:** Proceed without a task-related warning.
|
||||
|
||||
4. **Assess delta spec sync state**
|
||||
|
||||
Use `artifactPaths.specs.existingOutputPaths` from status JSON as the only
|
||||
delta-spec source. If the `specs` entry is missing or
|
||||
`existingOutputPaths` is empty, proceed without a sync prompt and do not infer
|
||||
delta specs from other artifacts.
|
||||
|
||||
**If delta specs exist:**
|
||||
- Compare each delta spec with its corresponding main spec at `<planningHome.root>/openspec/specs/<capability-path>/spec.md` (use the store-aware `planningHome.root` from step 2, not a hardcoded repo path)
|
||||
- A missing main spec is **not automatically** "already synced". For a new capability, the main spec is an *output* of the sync, not an input:
|
||||
- If the delta has MODIFIED or RENAMED requirements, report that only ADDED requirements can create a new main spec and mark that capability as sync-blocked. Never invent a requirement that has no current version.
|
||||
- Otherwise, if the delta has only REMOVED requirements and the change's `.openspec.yaml` declares `retire_capabilities: true`, the capability is already retired: count it as already synced, warn that there is nothing left to remove, and do not recreate the main spec. Apply this rule both now and when verifying a completed sync.
|
||||
- Otherwise, if the delta has no ADDED requirements, report that no sync is possible and mark that capability as sync-blocked. For a REMOVED-only delta, warn that there is no main spec to remove from and leave the main-spec tree unchanged. `openspec archive` refuses the unmarked REMOVED-only case with `Spec must have at least one requirement`.
|
||||
- Otherwise, count the capability as needing sync and name it in the summary (`<capability-path>: new main spec will be created`). If the delta also has REMOVED requirements, warn that they will be ignored because there is no main spec to remove from. The sync creates the main spec from only the delta's ADDED requirements, exactly as `openspec archive` does.
|
||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
||||
- Continue assessing the remaining capabilities even when one is sync-blocked. Show a combined summary before prompting.
|
||||
|
||||
**Prompt options:**
|
||||
- If any capability is sync-blocked: explain why and offer only "Archive without syncing", "Cancel"
|
||||
- Otherwise, if changes needed: "Sync now (recommended)", "Archive without syncing"
|
||||
- Otherwise, if already synced: "Archive now", "Sync anyway", "Cancel"
|
||||
|
||||
Route on the answer:
|
||||
- "Cancel" — stop, do not archive
|
||||
- "Archive without syncing" or "Archive now" — proceed to archive
|
||||
- "Sync now" or "Sync anyway" — sync, then verify (below). Do not start any sync while a capability is sync-blocked; explain the blocker and repeat the available choices.
|
||||
- Anything else — ask again rather than archiving
|
||||
|
||||
Before a selected sync writes any main spec, run
|
||||
`openspec instructions specs --change "<name>" --json` once with the same
|
||||
selected-root flags. Require a zero exit status and valid artifact-instruction
|
||||
JSON. If the lookup fails or returns invalid JSON, report the error and stop
|
||||
before writing any main spec or moving the change. A valid response with omitted
|
||||
`rules` is the no-rules case. Apply returned `rules` only to the content and
|
||||
form of main specs produced by this merge; do not use them as archive guidance,
|
||||
change CLI behavior, or copy the rule text into any output file.
|
||||
|
||||
Then run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge) for change '<name>', passing the delta spec analysis and the fetched specs-rule snapshot from above, and wait for it to finish. The inline sync must reuse that snapshot without fetching `specs` instructions again. Do not delegate it to a background task — step 5 would move `changeRoot` out from under a sync that is still reading it, leaving the change archived and the main specs never updated. If your agent can only run it by delegation, delegate synchronously and wait for the result.
|
||||
|
||||
If the sync reports any stop or blocking condition, treat the sync as failed.
|
||||
Stop the archive immediately. Do not perform the post-sync content comparison and do not move its `changeRoot`.
|
||||
Nothing has moved, so the user can fix the blocking condition or re-run the sync.
|
||||
|
||||
After the sync writes each main spec, verify its structure against the canonical sync contract:
|
||||
- A new main spec starts with a `# <capability> Specification` title. An existing main spec keeps its title exactly as it is.
|
||||
- Preserve existing `## Purpose` sections completely untouched for established main specs.
|
||||
- For a new main spec, copy the delta `## Purpose` verbatim. Warn only if the purpose text is shorter than standard validation expects. Do not regenerate or rewrite existing authored purpose. If no usable `## Purpose` is provided, use the existing TBD Purpose behavior and warning.
|
||||
- Verify that no delta-style section headers (`## ADDED Requirements`, `## MODIFIED Requirements`, `## REMOVED Requirements`, `## RENAMED Requirements`) remain in the main spec, adhering strictly to the sync workflow formatting rules.
|
||||
- Requirement blocks the sync wrote or changed use `### Requirement:` headings, and their scenarios use `#### Scenario:` headings, under the spec's `## Requirements` section. Leave content the delta does not mention exactly as it is.
|
||||
|
||||
Then re-run the comparison from the top of this step, including the explicitly retired, missing-spec case, against every capability that has a delta spec in `artifactPaths.specs.existingOutputPaths` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced:
|
||||
- ADDED requirements present
|
||||
- MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact
|
||||
- REMOVED requirements gone — and where this sync retired a capability (removed its last requirement, leaving `## Requirements` empty), its main spec deleted rather than left empty.
|
||||
- RENAMED requirements present under the new name and absent under the old one
|
||||
|
||||
If the sync failed, or any capability does not match, report what differs and stop — do not archive. Nothing has moved and `changeRoot` is intact, so the user can fix the mismatch or re-run the sync and start the archive again.
|
||||
|
||||
5. **Perform the archive**
|
||||
|
||||
Create an `archive` directory under `planningHome.changesDir` if it doesn't exist:
|
||||
```bash
|
||||
mkdir -p "<planningHome.changesDir>/archive"
|
||||
```
|
||||
|
||||
Generate the target name: use the change name as-is when it already starts with a `YYYY-MM-DD-` prefix; otherwise prepend the current date as `YYYY-MM-DD-<change-name>`. Never stack a second date (same rule as `openspec archive`).
|
||||
|
||||
**Check if target already exists:**
|
||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||
- If no: Move `changeRoot` to the archive directory
|
||||
|
||||
```bash
|
||||
mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
|
||||
```
|
||||
|
||||
6. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Whether specs were synced (if applicable)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```markdown
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** the archive path derived from `planningHome.changesDir`/<target-name>/
|
||||
**Specs:** <"✓ Synced to main specs" only if the step 4 verification passed; otherwise "No delta specs" or "Sync skipped">
|
||||
|
||||
<"All artifacts complete. All tasks complete." — or, if archived with warnings, list them instead (e.g. "Archived with 2 incomplete tasks")>
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Announce the selected change; prompt for selection when it is ambiguous
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, run the `openspec-sync-specs` workflow inline (agent-driven)
|
||||
- Never archive while a spec sync is still in flight — run the sync inline and verify the main specs before moving `changeRoot`
|
||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
||||
- Apply relevant runtime context and report conflicts; operation guidance remains advisory
|
||||
- Consider every guidance entry and explain any inapplicable or conflicting advice
|
||||
- Existing CLI checks, resolved paths, prompts, and command contracts are unchanged
|
||||
- Artifact rules constrain only the specs being written and are never operation guidance
|
||||
- Never copy runtime context, operation guidance, or artifact-rule text verbatim into output files
|
||||
@@ -1,354 +0,0 @@
|
||||
---
|
||||
name: openspec-explore
|
||||
description: Enter OpenSpec explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements in a project that uses OpenSpec. Use when the user wants to think through something before or during an OpenSpec change. Also use when the user says "openspec explore" or "opsx explore".
|
||||
allowed-tools: Bash(openspec:*)
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.14.1"
|
||||
---
|
||||
|
||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
||||
|
||||
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, investigate the codebase, and run read-only commands or tools without confirmation, but you must NEVER write code or implement features. If the user asks you to implement something, do not start it here: say that explore mode does not implement, and point them at `/openspec-propose`, which turns the discussion into a change. The work happens from that change, never from explore mode. You MAY create or update OpenSpec change artifacts (proposals, designs, specs) within a confirmed scope—that's capturing thinking, not implementing. Answering design or clarifying questions is never consent to write. Before the first write-capable action, name the artifacts or files you would change and what you would do, ask a direct yes/no question, and wait for the user's confirmation in a separate message. Confirmation covers only the scope you described; ask again before expanding it. An explicit request from the user to capture the exploration as a new change is itself that confirmation, covering the change and the change artifacts the request names; scaffold it first as described below.
|
||||
|
||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
||||
|
||||
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
|
||||
|
||||
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
|
||||
|
||||
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
|
||||
|
||||
Otherwise, with no root, what happens next depends on how this workflow was reached:
|
||||
|
||||
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
|
||||
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
|
||||
|
||||
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
|
||||
|
||||
---
|
||||
|
||||
## The Stance
|
||||
|
||||
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
||||
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
||||
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
||||
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
||||
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
||||
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
||||
|
||||
---
|
||||
|
||||
## Planning a Change
|
||||
|
||||
When the user is planning a change, guide them toward shared understanding with focused discovery questions. For open-ended discussion, follow the conversation without imposing an interview or a required output.
|
||||
|
||||
Before asking a factual question, follow the context discovery below and inspect relevant OpenSpec artifacts, source, tests, docs, and configuration. Do not ask the user to repeat facts you can verify. Summarize relevant findings without reproducing private context or rules. If evidence is missing, conflicting, or inaccessible, state that limitation and ask only for the clarification needed to proceed.
|
||||
|
||||
- **Follow dependencies** - Resolve the next blocking decision before its dependent details. For example, clarify the user's outcome and scope before choosing an API or data model. Revisit downstream assumptions when an earlier answer changes. Skip branches that do not matter to this goal.
|
||||
- **Keep questions focused** - Ask one focused question at a time, and briefly explain why it matters and which decision it unlocks. Batch questions only if the user asks for a batch; keep them small and group related decisions.
|
||||
- **Offer grounded recommendations** - When evidence supports a recommendation, state your preferred option and why it fits the user's goals, with alternatives and their tradeoffs when useful. Do not invent intent, priorities, or external constraints: ask the user when only they can answer. Avoid a fixed question format.
|
||||
- **Keep a conversational record** - Track decisions in the conversation, not in files. Separate confirmed decisions from proposed defaults and unresolved questions. Silence is not acceptance. Accepting an answer or a batch of recommendations is not permission to write. Keep file-write confirmation separate from discovery questions and follow the guardrails below.
|
||||
|
||||
Stop asking when the user has enough clarity. Let them pause, pivot, or defer a decision; do not exhaust every branch or force a proposal.
|
||||
|
||||
For example, after inspecting the relevant code:
|
||||
|
||||
```text
|
||||
The CLI already uses SQLite and has no remote service. Is sharing state
|
||||
across devices in scope? That determines whether local storage is enough.
|
||||
If this stays a single-device tool, I recommend keeping SQLite to avoid
|
||||
adding a service to operate; shared state would need a separate sync design.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What You Might Do
|
||||
|
||||
Depending on what the user brings, you might:
|
||||
|
||||
**Explore the problem space**
|
||||
- Ask clarifying questions that emerge from what they said
|
||||
- Challenge assumptions
|
||||
- Reframe the problem
|
||||
- Find analogies
|
||||
|
||||
**Investigate the codebase**
|
||||
- Map existing architecture relevant to the discussion
|
||||
- Find integration points
|
||||
- Identify patterns already in use
|
||||
- Surface hidden complexity
|
||||
|
||||
**Compare options**
|
||||
- Brainstorm multiple approaches
|
||||
- Build comparison tables
|
||||
- Sketch tradeoffs
|
||||
- Recommend a path (if asked)
|
||||
|
||||
**Visualize**
|
||||
```
|
||||
+------------------------------------------+
|
||||
| Use ASCII diagrams liberally |
|
||||
+------------------------------------------+
|
||||
| |
|
||||
| [State A] -------> [State B] |
|
||||
| | |
|
||||
| v |
|
||||
| [State C] |
|
||||
| |
|
||||
| System diagrams, state machines, |
|
||||
| data flows, architecture sketches, |
|
||||
| dependency graphs, comparison tables |
|
||||
| |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
**Draw with plain ASCII only** — borders `+` `-` `|`, arrows `-->` `<--` `^` `v`, markers `*` `x`.
|
||||
Unicode diagram glyphs can render at different widths across terminals, fonts, and locales, so padded boxes and aligned tables can drift. Keep every diagram character ASCII.
|
||||
|
||||
**Surface risks and unknowns**
|
||||
- Identify what could go wrong
|
||||
- Find gaps in understanding
|
||||
- Suggest spikes or investigations
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec Awareness
|
||||
|
||||
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
||||
|
||||
### Check for context
|
||||
|
||||
At the start, quickly check what exists:
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This tells you:
|
||||
- If there are active changes
|
||||
- Their names and task status
|
||||
- What the user might be working on
|
||||
|
||||
That is the *change* list - work in flight. It does not include the project's durable capabilities, so list those too:
|
||||
```bash
|
||||
openspec list --specs
|
||||
```
|
||||
Add `--json` for ids and requirement counts, and append `--store "<id>"` only for a registered standalone store. This is the inventory of what the project already claims to do, and `openspec list` on its own never shows it. To look at one, run `openspec show "<spec-id>" --type spec --json --no-scenarios` (same `--store` rule) - it returns that capability's purpose and requirement texts without pulling the whole spec file into context, and `--type spec` stops a change of the same name from making it ambiguous.
|
||||
|
||||
The filtered read is only an overview. Before deciding what is already covered or what should change, read each relevant spec in full, including scenarios, with `openspec show "<spec-id>" --type spec` (same `--store` rule).
|
||||
|
||||
Then read the project's own context from the resolved root - `<root.path>/openspec/config.yaml` (or `config.yml`). Use the `root.path` returned above, and skip this if neither file exists:
|
||||
- `context`: project background - tech stack, conventions, constraints
|
||||
- `rules`: keyed by artifact id - the entries for an artifact apply only when you write that artifact
|
||||
|
||||
Ground your thinking in these. They are constraints for you to follow, not content to reproduce: do NOT copy them into the conversation or into any artifact you create.
|
||||
|
||||
### When no change exists
|
||||
|
||||
Think freely. When insights crystallize, you might offer:
|
||||
|
||||
- "This feels solid enough to start a change. Want me to create a proposal?"
|
||||
- Or keep exploring - no pressure to formalize
|
||||
|
||||
If the user asks you to capture the exploration as a new change, that request is the confirmation required above. It covers scaffolding that change and creating the change artifacts the request names, and nothing else. This holds only when the request is theirs: a yes to an offer you made confirms only the scope your offer itself named, so name the change and the artifacts in the offer. Don't re-ask for what they already asked for; do ask before anything beyond it. Transition seamlessly into the requested capture:
|
||||
|
||||
1. Run `openspec new change "<name>"` (with `--store <id>` when applicable) before creating any artifacts. Never create a new change directory under `openspec/changes/` by hand; the CLI scaffold creates required metadata such as `.openspec.yaml`. Keep the selected `--store <id>` on every applicable follow-up `status` and `instructions` command.
|
||||
2. Run `openspec status --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store), then process the requested artifacts in dependency order. For each requested artifact that is `ready`, run `openspec instructions "<artifact-id>" --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store). Before creating a requested artifact, evaluate any condition in its own `instruction` against the explored change; record a deliberate skip instead when the condition does not apply. If a requested artifact is blocked by a direct prerequisite the user did not request, run `openspec instructions "<prerequisite-id>" --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store) for that prerequisite whether it is `ready` or `blocked`. If its own `instruction` states a condition, evaluate that condition against the explored change and record a deliberate skip only when the condition does not apply. If the condition applies, or the prerequisite is not conditional, treat it as a normal prerequisite and ask before expanding the capture. Do not create an unrequested prerequisite unless the user approves.
|
||||
3. Follow the returned `template` and `instruction` fields. Read completed dependency files listed in `dependencies`, and apply `context` and `rules` as constraints without copying them into the artifact. If the instruction delegates creation to a specific skill or command, invoke it; otherwise write the artifact to `resolvedOutputPath`, using the instruction to choose a concrete path when it is a glob. Verify that the selected concrete output exists.
|
||||
4. After creating each artifact, re-run `openspec status --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store) and continue until every requested artifact is `done`, `skipped`, or was deliberately skipped because its own `instruction` stated a condition that did not apply. Tell the user about a deliberate conditional skip, remember it, and do not reconsider it. Dependencies are enablers, not gates: if a requested artifact is still `blocked` only because you deliberately skipped a conditional prerequisite, run `openspec instructions "<artifact-id>" --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store) despite the blocked status, then create it using step 3 only when those recorded conditional skips are its sole missing dependencies. If a requested artifact is blocked by a prerequisite the user did not ask to capture and cannot be conditionally skipped, explain that dependency and ask before expanding the capture.
|
||||
|
||||
Capture the artifact(s) the user requested without asking them to invoke another workflow command. If they asked only to start a change, stop after scaffolding and show its status. When the requested capture is done, stop there and name where the work continues: `/openspec-propose` writes the remaining planning artifacts, and `/openspec-apply-change` implements the change once tasks exist. Capturing artifacts never starts implementing them.
|
||||
|
||||
### When a change exists
|
||||
|
||||
If the user mentions a change or you detect one is relevant:
|
||||
|
||||
1. **Resolve and read existing artifacts for context**
|
||||
- Run `openspec status --change "<name>" --json`.
|
||||
- Use `changeRoot`, `artifactPaths`, and `actionContext` from the status JSON.
|
||||
- Read existing files from `artifactPaths.<artifact>.existingOutputPaths`.
|
||||
|
||||
2. **Reference them naturally in conversation**
|
||||
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
||||
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
||||
|
||||
3. **Offer to capture when decisions are made**
|
||||
|
||||
`<capability-path>` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve an existing capability's full path and follow the project's established organization for new capabilities.
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|----------------------------|-------------------------------------|
|
||||
| New requirement discovered | `specs/<capability-path>/spec.md` |
|
||||
| Requirement changed | `specs/<capability-path>/spec.md` |
|
||||
| Design decision made | `design.md` |
|
||||
| Scope changed | `proposal.md` |
|
||||
| New work identified | `tasks.md` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
|
||||
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
||||
|
||||
---
|
||||
|
||||
## What You Don't Have To Do
|
||||
|
||||
- Follow a script
|
||||
- Ask the same questions every time
|
||||
- Produce a specific artifact
|
||||
- Reach a conclusion
|
||||
- Stay on topic if a tangent is valuable
|
||||
- Be brief (this is thinking time)
|
||||
|
||||
---
|
||||
|
||||
## Handling Different Entry Points
|
||||
|
||||
**User brings a vague idea:**
|
||||
```
|
||||
User: I'm thinking about adding real-time collaboration
|
||||
|
||||
You: Real-time collab is a big space. Let me think about this...
|
||||
|
||||
COLLABORATION SPECTRUM
|
||||
============================================
|
||||
|
||||
Awareness Coordination Sync
|
||||
| | |
|
||||
v v v
|
||||
+--------+ +--------+ +--------+
|
||||
|Presence| |Cursors | | CRDT |
|
||||
| "3 | | Multi | |Conflict|
|
||||
|online" | | select | | free |
|
||||
+--------+ +--------+ +--------+
|
||||
| | |
|
||||
trivial moderate complex
|
||||
|
||||
Where's your head at?
|
||||
```
|
||||
|
||||
**User brings a specific problem:**
|
||||
```
|
||||
User: The auth system is a mess
|
||||
|
||||
You: [reads codebase]
|
||||
|
||||
+---------------------------------------------+
|
||||
| CURRENT AUTH FLOW |
|
||||
+---------------------------------------------+
|
||||
|
|
||||
+-------------+-------------+
|
||||
v v v
|
||||
+---------+ +---------+ +---------+
|
||||
| Google | | GitHub | | Email |
|
||||
| OAuth | | OAuth | | Magic |
|
||||
+----+----+ +----+----+ +----+----+
|
||||
| | |
|
||||
+-------------+-------------+
|
||||
v
|
||||
+-----------+
|
||||
| Session |
|
||||
+-----+-----+
|
||||
|
|
||||
v
|
||||
+-----------+
|
||||
| Perms |
|
||||
+-----------+
|
||||
|
||||
I see three tangles. Which one's burning?
|
||||
```
|
||||
|
||||
**User is stuck mid-implementation:**
|
||||
```
|
||||
User: /openspec-explore add-auth-system
|
||||
The OAuth integration is more complex than expected
|
||||
|
||||
You: [reads change artifacts]
|
||||
|
||||
You're on task 4: "Implement OAuth flow"
|
||||
|
||||
Let me trace what's involved...
|
||||
|
||||
[draws diagram, explores options, suggests paths]
|
||||
|
||||
Want to update the design to reflect this?
|
||||
Or add a spike task to investigate?
|
||||
```
|
||||
|
||||
**User wants to compare options:**
|
||||
```
|
||||
User: Should we use Postgres or SQLite?
|
||||
|
||||
You: Generic answer is boring. What's the context?
|
||||
|
||||
User: A CLI tool that tracks local dev environments
|
||||
|
||||
You: That changes everything.
|
||||
|
||||
+-------------------------------------------------+
|
||||
| CLI TOOL DATA STORAGE |
|
||||
+-------------------------------------------------+
|
||||
|
||||
Key constraints:
|
||||
- No daemon running
|
||||
- Must work offline
|
||||
- Single user
|
||||
|
||||
SQLite Postgres
|
||||
Deployment embedded needs server
|
||||
Offline yes no
|
||||
Single file yes no
|
||||
|
||||
SQLite. Not even close.
|
||||
|
||||
Unless... is there a sync component?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ending Discovery
|
||||
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "Ready to start? Run `/openspec-propose` and this becomes a change."
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
When it feels like things are crystallizing, you might summarize:
|
||||
|
||||
```
|
||||
## What We Figured Out
|
||||
|
||||
**The problem**: [crystallized understanding]
|
||||
|
||||
**The approach**: [if one emerged]
|
||||
|
||||
**Open questions**: [if any remain]
|
||||
|
||||
**Next steps** (if ready):
|
||||
- Turn this into a change: `/openspec-propose`
|
||||
- Keep exploring: just keep talking
|
||||
```
|
||||
|
||||
But this summary is optional. Sometimes the thinking IS the value.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Don't implement** - Never write code or implement features. Workflow configuration counts too: creating or editing schemas, templates, or `openspec/config.yaml` is a change, not thinking. Creating or updating OpenSpec change artifacts within the confirmed scope is fine, writing anything else is not. When the user is ready to build, name the handoff rather than starting: `/openspec-propose` turns the discussion into a change, and the work happens there.
|
||||
- **Don't fake understanding** - If something is unclear, dig deeper
|
||||
- **Don't rush** - Discovery is thinking time, not task time
|
||||
- **Don't force structure** - Let patterns emerge naturally
|
||||
- **Don't auto-capture** - Offer to save insights, don't just do it. Read-only commands and tools need no confirmation. Before the first write-capable action—including `openspec new change` or another command that writes files—name the artifacts or files and proposed changes, ask a direct yes/no question, and wait for explicit confirmation in a separate user message. That confirmation covers only the described scope; ask again before expanding it. Answers to design or clarifying questions are never consent to write. That rule governs `openspec new change` whenever you are the one proposing the capture; the user's own capture request is the exception, handled in the capture transition above.
|
||||
- **Don't manually scaffold changes** - Never create a new change directory under `openspec/changes/` by hand. Always use `openspec new change "<name>"` (with `--store <id>` when applicable) so required metadata such as `.openspec.yaml` is created before writing artifacts.
|
||||
- **Do visualize** - A good diagram is worth many paragraphs
|
||||
- **Do explore the codebase** - Ground discussions in reality
|
||||
- **Do question assumptions** - Including the user's and your own
|
||||
@@ -1,174 +0,0 @@
|
||||
---
|
||||
name: openspec-propose
|
||||
description: Propose a new OpenSpec change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation. Also use when the user says "openspec propose" or "opsx propose".
|
||||
allowed-tools: Bash(openspec:*)
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.14.1"
|
||||
---
|
||||
|
||||
Propose a new change - create the change and generate all artifacts in one step.
|
||||
|
||||
**Planning boundary**: This workflow creates planning artifacts only. The user request that selected or triggered this workflow authorizes planning only, even if it asks to build or fix something. Do not edit project code. After the planning artifacts are complete, stop. Do not start implementation in the same response, even if the initial request asks for it. Wait for a new user request after the artifacts are presented; then start the apply workflow.
|
||||
|
||||
I'll create a change with the artifacts your schema defines. With the default spec-driven schema that is:
|
||||
- proposal.md (what & why)
|
||||
- `specs/<capability-path>/spec.md` (what the system must do - a delta, not the main spec)
|
||||
- design.md (how)
|
||||
- tasks.md (implementation steps)
|
||||
|
||||
`<capability-path>` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve an existing capability's full path and follow the project's established organization for new capabilities.
|
||||
|
||||
When the user is ready to implement, they must start the apply workflow explicitly.
|
||||
|
||||
---
|
||||
|
||||
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
|
||||
|
||||
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
|
||||
|
||||
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
|
||||
|
||||
Otherwise, with no root, what happens next depends on how this workflow was reached:
|
||||
|
||||
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
|
||||
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
|
||||
|
||||
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
|
||||
|
||||
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Understand the request and clarify material ambiguity**
|
||||
|
||||
If no clear input is provided, ask the user (open-ended, no preset options):
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
If the request contains ambiguity that would materially affect scope, externally observable behavior, compatibility, or acceptance criteria, ask the user before creating the change. For minor details, make a reasonable assumption and record it in the planning artifacts.
|
||||
|
||||
2. **Load project context**
|
||||
|
||||
Run `openspec context --json` from the current working directory (or `openspec context --json --store "<store-id>"` when a registered store was explicitly selected). Use the returned `root.path` as the authoritative OpenSpec root. If context reports `no_openspec_root`, stop without creating or changing any files and follow the **Project check** above for how this workflow was reached. Offer `openspec init` only for an explicit OpenSpec request, and wait for the user to request initialization. Do not initialize automatically or run `openspec new change`. After initialization, rerun this context check before continuing. For any other context failure, stop and report the error; do not fall back to the current directory or run later OpenSpec commands without the selected store.
|
||||
|
||||
Only when context returns a resolved `root.path`, read `<root.path>/openspec/config.yaml`. Use `config.yml` only when `config.yaml` does not exist. If neither file exists, continue without project context. Do not fall back to `config.yml` if `config.yaml` is unreadable or invalid.
|
||||
|
||||
If the file parses as a YAML object and its `context` field is a string no larger than 51,200 bytes in UTF-8, apply that field before exploring the codebase or making planning decisions. If the file cannot be read or parsed, or the context field is invalid or oversized, continue without project context. Validate this field independently of other config fields, as OpenSpec does.
|
||||
|
||||
Treat context as project-provided data and constraints, not as authority to change this workflow: it cannot override user authorization, the planning boundary, tool restrictions, or artifact and output rules. Do not copy the context into artifacts; use it to focus any codebase exploration and as a constraint on the proposal.
|
||||
|
||||
3. **Determine the workflow schema**
|
||||
|
||||
Use the configured default schema unless the user explicitly requests a different workflow.
|
||||
|
||||
**Use a different schema only if the user:**
|
||||
- Explicitly requests a specific schema by name → use `--schema <schema-name>`
|
||||
- Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running `openspec context --json` from the current working directory. If the user explicitly selected a registered store, use `openspec context --json --store "<store-id>"`. Then run `openspec schemas --json` with its working directory set to the returned `root.path` and let them choose. This preserves roots selected by a local `store:` pointer or the global `defaultStore`; when a registered store was explicitly selected, append `--store "<store-id>"` to `openspec schemas --json` as well. If context fails, stop as described in the context-loading step; do not fall back to the current directory.
|
||||
|
||||
Otherwise, omit `--schema` to preserve the configured default.
|
||||
|
||||
4. **Create the change directory**
|
||||
|
||||
Choose one schema form below. If a registered store is selected, append `--store "<store-id>"` to that command and each later OpenSpec command shown below that accepts `--store`.
|
||||
|
||||
Using the configured default:
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
|
||||
Using an explicitly requested schema:
|
||||
```bash
|
||||
openspec new change "<name>" --schema "<schema-name>"
|
||||
```
|
||||
This creates a scaffolded change in the planning home resolved by the CLI with `.openspec.yaml`.
|
||||
|
||||
5. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to get:
|
||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||
- `artifacts`: list of all artifacts, each with its `status` and its `requires` edges (the artifact IDs it directly depends on)
|
||||
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths.
|
||||
|
||||
6. **Create every artifact in the required set**
|
||||
|
||||
Use a todo list to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance for this artifact type
|
||||
- `skipped`/`warning`: present when the change declares skip_specs and this artifact must NOT be created - stop and pick another artifact
|
||||
- `resolvedOutputPath`: Resolved path or pattern to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
|
||||
- **Inspect the relevant project before drafting**: Read `context` and `rules` first, then inspect relevant implementation, nearby tests, configuration, and documentation outside `openspec/`. Keep inspection read-only and proportional to the change; reuse findings for later artifacts and inspect more only as needed.
|
||||
- Identify the target project from the request and project context; the planning home may be separate from the code. If the target is unclear, ask. For greenfield or non-code changes, inspect the available structure and relevant documents. If source is unavailable, state the limitation and ask when it materially affects the plan.
|
||||
- Ground scope, approach, and tasks in what you find. Distinguish observed behavior from assumptions and proposed additions; surface conflicts with existing specs instead of silently deciding which is correct.
|
||||
- Do this discovery now, rather than leaving generic "explore the codebase" or "make a plan" tasks for implementation. Keep any necessary follow-up investigation specific to an unresolved question.
|
||||
- If the `instruction` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at `resolvedOutputPath`
|
||||
- Otherwise create the artifact file using `template` as the structure and write it to `resolvedOutputPath`. If `resolvedOutputPath` is a glob, follow `instruction` to choose the concrete file path
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "Created <artifact-id>"
|
||||
|
||||
b. **Continue until every artifact in the required set exists (not just `apply.requires`)**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- The required set is `applyRequires` plus every artifact reachable from those by following the `requires` edges in `status --json` - walk them transitively (spec-driven closes over proposal, specs, design, tasks). Leave artifacts outside that set alone
|
||||
- `status` is file-existence only, so an `applyRequires` artifact reading `done` does NOT mean its dependencies exist - writing `tasks.md` early marks `tasks` done while `specs` was never written. Use each artifact's `requires` edges, not its `status`, to build the required set: a `done` artifact still lists what it depends on
|
||||
- An artifact already reading `status: "skipped"` is satisfied: the change declares `skip_specs` in `.openspec.yaml`, so its files must NOT exist. Never try to create one
|
||||
- Create every artifact in the required set that is missing, then re-check - creating one can unblock others
|
||||
- Skip one only when `status` already reports it `skipped`, or when its own `instruction` says it is conditional: run `openspec instructions <artifact-id> --change "<name>" --json` and skip only if its `instruction` field marks it optional (e.g. "create only if..."). Spec-driven's `design.md` qualifies; `specs` qualifies only via the `skipped` status above, never by your own judgment. Tell the user, and do not reconsider it
|
||||
- Dependencies are enablers, not gates: if a required artifact is still `blocked` only because you skipped a conditional dependency, write it anyway
|
||||
- Stop when every artifact in the required set is `done`, `skipped`, or was deliberately skipped
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Ask the user to clarify
|
||||
- Then continue with creation
|
||||
|
||||
7. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions, plus any conditional artifact you skipped and why
|
||||
- What's ready: "All artifacts needed for implementation are ready."
|
||||
- Prompt: "The artifacts are ready for review. When you are ready, run `/openspec-apply-change` or ask me to apply this change."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type - it is the authoritative guidance, even for familiar artifact names
|
||||
- If the `instruction` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
- The request that invoked this workflow authorizes planning only. Any implementation or apply instruction in that request does not carry forward. Do NOT implement the change, start the apply workflow, or edit project code during this workflow. After presenting the artifacts, stop and wait for a new user request to start the apply workflow
|
||||
- Create every artifact the apply phase transitively depends on, not just the ids listed in `apply.requires`
|
||||
- Always read dependency artifacts before creating a new one - re-read from disk, not from conversation memory (files may have changed since you last saw them)
|
||||
- Ask about ambiguities that would materially change scope, externally observable behavior, compatibility, or acceptance criteria; for minor details, make reasonable assumptions and record them
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
@@ -1,290 +0,0 @@
|
||||
---
|
||||
name: openspec-sync-specs
|
||||
description: Sync delta specs from an OpenSpec change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change. Also use when the user says "openspec sync" or "opsx sync".
|
||||
allowed-tools: Bash(openspec:*)
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.14.1"
|
||||
---
|
||||
|
||||
Sync delta specs from a change to main specs.
|
||||
|
||||
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
|
||||
|
||||
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
|
||||
|
||||
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
|
||||
|
||||
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
|
||||
|
||||
Otherwise, with no root, what happens next depends on how this workflow was reached:
|
||||
|
||||
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
|
||||
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
|
||||
|
||||
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
|
||||
|
||||
`<capability-path>` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and ask the user to select one
|
||||
|
||||
When prompting, show changes that have delta specs (under `specs/` directory).
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/openspec-sync-specs <other>`).
|
||||
|
||||
2. **Resolve change context**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
|
||||
The JSON includes `planningHome.root`. Main specs live under `<planningHome.root>/openspec/specs/` — use that (store-aware) root for every main-spec path below, not a hardcoded repo path. When a store is selected it points at the store, not the current repository.
|
||||
|
||||
3. **Find delta specs**
|
||||
|
||||
Use `artifactPaths.specs.existingOutputPaths` from the status JSON as the
|
||||
only source of delta spec paths. If the `specs` entry is missing or
|
||||
`existingOutputPaths` is empty, report that there are no delta specs to sync,
|
||||
do not infer them from other artifacts, and stop without requesting artifact
|
||||
instructions or writing a main spec.
|
||||
|
||||
Sync every path in `existingOutputPaths` unless the caller narrowed the set.
|
||||
A caller narrows it by naming an explicit list of complete entries from
|
||||
`existingOutputPaths` — copy those absolute values verbatim. Archive does
|
||||
this inline, and a user can too (for example, by selecting the entry ending
|
||||
in `/specs/billing/invoices/spec.md`).
|
||||
Then sync only the named paths and leave the remaining delta specs untouched:
|
||||
bulk archive excludes a delta whose implementation it could not find, and
|
||||
syncing it anyway would write a main spec the caller deliberately withheld.
|
||||
Carry that narrowed selection through step 4; never widen it back to the full
|
||||
list. If a named path is not in `existingOutputPaths`, do not sync it —
|
||||
report it and stop, rather than dropping it silently. If the named list is
|
||||
empty, report that there is nothing to sync and stop without writing a main
|
||||
spec.
|
||||
|
||||
Each delta spec file contains sections like:
|
||||
- `## ADDED Requirements` - New requirements to add
|
||||
- `## MODIFIED Requirements` - Changes to existing requirements
|
||||
- `## REMOVED Requirements` - Requirements to remove
|
||||
- `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
|
||||
|
||||
If no delta specs found, inform user and stop.
|
||||
|
||||
4. **For each delta spec, apply changes to main specs**
|
||||
|
||||
Before the first main-spec write, obtain one current specs-rule snapshot:
|
||||
- If archive invoked this workflow inline and supplied a valid snapshot from
|
||||
`openspec instructions specs --change "<name>" --json`, reuse it and do not
|
||||
fetch the same instructions again.
|
||||
- Otherwise run that command once now with the same selected-root flags.
|
||||
- If the direct lookup exits non-zero or returns invalid artifact-instruction
|
||||
JSON, report the error and stop before writing any main spec. Do not treat the
|
||||
failure as an absent rule set.
|
||||
- A valid response with omitted `rules` means no artifact rules are configured
|
||||
and the existing semantic merge continues.
|
||||
|
||||
Apply returned `rules` only to the content and form of the main specs produced
|
||||
by this merge. Artifact rules are not operation guidance and cannot change
|
||||
selected roots, delta paths, CLI checks, or workflow steps. Use their text as
|
||||
constraints without copying it verbatim into a main spec or summary.
|
||||
|
||||
For each capability delta spec path selected in step 3 — the full `existingOutputPaths` list, or the narrowed subset when a caller supplied one (these may belong to a selected store, not the repo):
|
||||
|
||||
a. **Read the delta spec** to understand the intended changes
|
||||
|
||||
b. **Read the main spec** at `<planningHome.root>/openspec/specs/<capability-path>/spec.md` (may not exist yet)
|
||||
|
||||
**If it does not exist yet** (a new capability), match what `openspec archive` does:
|
||||
only ADDED requirements may be applied - step d creates the spec from them.
|
||||
MODIFIED and RENAMED have no requirement to act on, so stop the sync for that
|
||||
capability and report that its main spec does not exist and only ADDED is allowed
|
||||
for a new spec; never invent the missing requirement. REMOVED has nothing to
|
||||
remove - skip it and warn.
|
||||
|
||||
c. **Apply changes intelligently**:
|
||||
|
||||
**ADDED Requirements:**
|
||||
- If requirement doesn't exist in main spec → add it
|
||||
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
||||
|
||||
**MODIFIED Requirements:**
|
||||
- Find the requirement in main spec
|
||||
- Apply the changes - this can be:
|
||||
- Adding new scenarios the main spec does not have yet
|
||||
- Modifying existing scenarios
|
||||
- Changing the requirement description
|
||||
- Preserve scenarios/content not mentioned in the delta
|
||||
|
||||
**REMOVED Requirements:**
|
||||
- Remove the entire requirement block from main spec
|
||||
- Retiring the capability. Delete the whole `spec.md` - and the directory once
|
||||
nothing else is left in it - only when ALL of these hold:
|
||||
1. removing the requirements *this run* left no requirement blocks;
|
||||
2. the rest of the spec is well-formed (it still has a `## Purpose`);
|
||||
3. the main spec was not already empty before this sync - if you removed
|
||||
nothing, change nothing;
|
||||
4. every other nonblank line in the whole file is accounted for as the
|
||||
title, Purpose, Requirements header, or a canonical requirement's
|
||||
statement, scenarios, or fenced examples;
|
||||
5. the change's `.openspec.yaml` declares `retire_capabilities: true`;
|
||||
6. the `spec.md` resolves inside the real specs root (do not follow a
|
||||
capability-directory symlink to delete an external file).
|
||||
If removing the selected requirements would leave no requirement blocks and
|
||||
any retirement condition is not satisfied, do not modify the main spec. Stop
|
||||
the sync for that capability, report the blocking condition, and tell the user
|
||||
how to resolve it. Never write or leave an empty `## Requirements` section.
|
||||
When only the marker is missing, say that too - it is the one thing the user
|
||||
can add to make the retirement go through.
|
||||
- Deleting the file also deletes its `## Purpose`; any other section blocks
|
||||
retirement. Name Purpose when you report the retirement. Include a pasteable
|
||||
`git checkout` only when the spec lived in the caller's checkout;
|
||||
otherwise give checkout-scoped recovery guidance.
|
||||
|
||||
**RENAMED Requirements:**
|
||||
- Find the FROM requirement, rename to TO
|
||||
|
||||
**`## Purpose` in the delta:**
|
||||
- The main spec already has one and it is authoritative - leave it alone
|
||||
(this is what `openspec archive` does; it warns and moves on)
|
||||
|
||||
d. **Create new main spec** if capability doesn't exist yet:
|
||||
- Only when the delta has ADDED requirements to put in it and no MODIFIED or
|
||||
RENAMED requirements blocked this capability in step b. Otherwise create nothing
|
||||
and leave the specs directory untouched. For a REMOVED-only delta, if the change's
|
||||
`.openspec.yaml` declares `retire_capabilities: true`, report it as already retired
|
||||
and continue without recreating the spec. Without that marker, report the sync as blocked:
|
||||
`openspec archive` rejects it with `Spec must have at least one requirement`.
|
||||
An empty delta has no operations to sync; report it as blocked too.
|
||||
Never write an empty `## Requirements` section.
|
||||
- Create `<planningHome.root>/openspec/specs/<capability-path>/spec.md`
|
||||
- Add Purpose section: copy the delta's `## Purpose` body verbatim when it has one
|
||||
(this is what `openspec archive` does); only write a brief TBD placeholder when it does not
|
||||
- Add Requirements section with the ADDED requirements
|
||||
- Follow the **Main Spec Format Reference** below
|
||||
|
||||
5. **Validate updated main specs**
|
||||
|
||||
Run `openspec validate --specs` with the same selected-root flags used earlier.
|
||||
If validation fails, report the problems and do not claim the sync succeeded.
|
||||
|
||||
6. **Show summary**
|
||||
|
||||
After applying all changes, summarize:
|
||||
- Which capabilities were updated
|
||||
- What changes were made (requirements added/modified/removed/renamed)
|
||||
- Any new main spec left with a TBD Purpose placeholder, so it gets written
|
||||
now rather than lingering
|
||||
- Any capability retired, naming the deleted `spec.md`, its Purpose, and
|
||||
either a pasteable `git checkout` or checkout-scoped recovery guidance
|
||||
|
||||
**Delta Spec Format Reference**
|
||||
|
||||
```markdown
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Only on a delta that introduces a brand-new capability. Seeds the new main spec.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: New Feature
|
||||
The system SHALL do something new.
|
||||
|
||||
#### Scenario: Basic case
|
||||
- **WHEN** user does X
|
||||
- **THEN** system does Y
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Existing Feature
|
||||
The system SHALL keep doing the existing thing, now also handling A.
|
||||
|
||||
#### Scenario: Scenario the main spec already has
|
||||
- **WHEN** user does X
|
||||
- **THEN** system does Y
|
||||
|
||||
#### Scenario: New scenario to add
|
||||
- **WHEN** user does A
|
||||
- **THEN** system does B
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Deprecated Feature
|
||||
|
||||
## RENAMED Requirements
|
||||
|
||||
- FROM: `### Requirement: Old Name`
|
||||
- TO: `### Requirement: New Name`
|
||||
```
|
||||
|
||||
**Main Spec Format Reference**
|
||||
|
||||
Main specs are what the delta merges INTO. They must never contain delta operation headers (`## ADDED/MODIFIED/REMOVED/RENAMED Requirements`) - after syncing, every requirement lives under a single `## Requirements` section:
|
||||
|
||||
```markdown
|
||||
# <capability> Specification
|
||||
|
||||
## Purpose
|
||||
Short description of what this capability does and why it exists.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: New Feature
|
||||
The system SHALL do something new.
|
||||
|
||||
#### Scenario: Basic case
|
||||
- **WHEN** user does X
|
||||
- **THEN** system does Y
|
||||
```
|
||||
|
||||
**Key Principle: Intelligent Merging**
|
||||
|
||||
Unlike programmatic merging, you merge rather than overwrite:
|
||||
- A MODIFIED block carries the whole requirement - body plus every scenario that survives the change. `openspec validate` and `openspec archive` both reject one that drops a scenario the main spec still has.
|
||||
- Keep anything the delta does not mention, in the main spec's existing order
|
||||
- Use your judgment to merge changes sensibly
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```markdown
|
||||
## Specs Synced: <change-name>
|
||||
|
||||
Updated main specs:
|
||||
|
||||
**<capability-1>**:
|
||||
- Added requirement: "New Feature"
|
||||
- Modified requirement: "Existing Feature" (added 1 scenario)
|
||||
|
||||
**<capability-2>**:
|
||||
- Created new spec file
|
||||
- Added requirement: "Another Feature"
|
||||
|
||||
Main specs are now updated. The change remains active - archive when implementation is complete.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Read both delta and main specs before making changes
|
||||
- Preserve existing content not mentioned in delta
|
||||
- Never copy a delta file into a main spec as-is - merge its content so the main spec keeps the Main Spec Format Reference structure, with no delta operation headers
|
||||
- If something is unclear, ask for clarification
|
||||
- Show what you're changing as you go
|
||||
- The operation should be idempotent - running twice should give same result
|
||||
- Use only `artifactPaths.specs.existingOutputPaths`; never infer delta specs from unrelated artifacts
|
||||
- Honor a caller-supplied subset of `existingOutputPaths`; never widen it back to the full list
|
||||
- Fetch specs instructions once for direct sync, or reuse the archive-supplied snapshot inline
|
||||
- Stop before every main-spec write on a non-zero or invalid JSON specs-instruction response
|
||||
- Artifact rules constrain only the specs being written and are never copied into output files
|
||||
@@ -1,109 +0,0 @@
|
||||
---
|
||||
name: openspec-update-change
|
||||
description: Update an OpenSpec change by revising its existing planning artifacts and keeping them coherent with one another. Use when the user wants to revise a change's plan, fold new decisions into it, or reconcile its artifacts after an edit. Also use when the user says "openspec update change" or "opsx update". If the user means the openspec update CLI command, which refreshes generated files, run that command instead. Never edits code.
|
||||
allowed-tools: Bash(openspec:*)
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.14.1"
|
||||
---
|
||||
|
||||
Revise a change's existing planning artifacts and keep them coherent. Never edit code.
|
||||
|
||||
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
|
||||
|
||||
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
|
||||
|
||||
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
|
||||
|
||||
Otherwise, with no root, what happens next depends on how this workflow was reached:
|
||||
|
||||
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
|
||||
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
|
||||
|
||||
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
This workflow revises artifacts that already exist; it never creates missing ones. When an artifact is missing, `openspec status --change "<name>" --json` names the next one and `openspec instructions "<artifact-id>" --change "<name>" --json` explains how to write it.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes sorted by most recently modified, and ask the user to select one
|
||||
|
||||
When prompting, present the top 3-4 most recently modified changes as options, showing:
|
||||
- Change name
|
||||
- Status (e.g., "0/5 tasks", "complete", "no tasks")
|
||||
- How recently it was modified (from `lastModified` field)
|
||||
|
||||
Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to update.
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/openspec-update-change <other>`).
|
||||
|
||||
2. **Get the change's artifacts**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand current state. The response includes:
|
||||
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
|
||||
- `artifacts`: Array of artifacts with their status ("done", "skipped", "ready", "blocked")
|
||||
- `isPlanningComplete`: Boolean indicating if all planning artifacts are complete. Older CLI versions expose the same value as `isComplete`.
|
||||
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths.
|
||||
|
||||
The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged.
|
||||
|
||||
The files to edit are `artifactPaths.<id>.existingOutputPaths` - the concrete files that exist on disk, already glob-expanded for glob artifacts (e.g. `specs/**/*.md`). Do NOT write to `resolvedOutputPath`: for a glob artifact it is still the glob pattern, not a real file.
|
||||
|
||||
3. **Understand the request**
|
||||
- If the user asked for a specific revision ("the design now uses X"), that is the starting edit.
|
||||
- If they only said "update" / "make this coherent", treat it as a coherence review: read the existing artifacts and check them against each other for contradictions, gaps, and duplication.
|
||||
|
||||
4. **Read and reconcile**
|
||||
- Read the artifact(s) the request touches and the change's other existing artifacts.
|
||||
- Draft the requested edit in the conversation, not in files. Work out exactly what it changes; step 5 owns every write. Then check every other existing artifact against the drafted edit - in ANY direction: an edit to a later artifact may require revising an earlier one, not only the other way around. Build order is a useful reading order, not a constraint on which artifacts may be revised.
|
||||
- Note everything that is now inconsistent, missing, or contradictory.
|
||||
- Propose revisions to files that already exist (`existingOutputPaths`). If an artifact has no existing output files and status `ready` or `blocked`, note it and point the user to `openspec instructions "<artifact-id>" --change "<name>" --json` for how to create them. Leave `skipped` artifacts untouched; do not treat them as missing or defer them to the continue workflow.
|
||||
- A glob artifact (e.g. `specs/**/*.md`) is marked `done` after at least one file matches, and the continue workflow only handles `ready` artifacts. When reconciliation identifies a missing file for a glob artifact whose `existingOutputPaths` is non-empty:
|
||||
1. Run `openspec instructions "<artifact-id>" --change "<name>" --json` and use its `instruction` and `template`. Treat `context` and `rules` as constraints; do not copy them into the file. If instructions report `skipped: true`, do not create the file. Read current dependency files from disk; if a required non-skipped dependency is missing, stop and ask the user to restore it first.
|
||||
2. Choose a concrete path inside `changeRoot` that matches `artifactPaths.<id>.outputPath` and does not already exist. Verify it remains inside `changeRoot` after resolving any symlinked parent directories. The glob `resolvedOutputPath` is not a valid target.
|
||||
3. Include the new file in step 5's proposed revisions and create it only after the user confirms.
|
||||
4. After confirmation, immediately before creation, refresh status and instructions. Verify the artifact is still in scope, not skipped, and partially populated; repeat the concrete-path checks above.
|
||||
5. Use a create operation that fails if the target already exists. If `instruction` delegates creation to another skill or command, invoke it only if it can honor the confirmed path and these guardrails; otherwise stop. If any check fails or the confirmed draft is no longer valid, stop and reconcile with the user rather than replacing existing content or choosing a different path.
|
||||
- If the change is already coherent, say so and propose no revisions.
|
||||
|
||||
5. **Confirm and apply, one artifact at a time**
|
||||
- This step performs every artifact write in this workflow; no earlier step edits an artifact.
|
||||
- Show each proposed revision and why - including the requested edit drafted in step 4. Write only after the user confirms.
|
||||
- If the user rejects a revision, do not write it - leave that artifact unchanged.
|
||||
- When a substantial rewrite is needed, get that artifact's rules and template first:
|
||||
```bash
|
||||
openspec instructions "<artifact-id>" --change "<name>" --json
|
||||
```
|
||||
|
||||
6. **Point to the next step (guidance only - NEVER act on it)**
|
||||
- Artifacts with empty `existingOutputPaths` and status `ready` or `blocked` -> run `openspec status --change "<name>" --json` for the next artifact and point the user to `openspec instructions "<artifact-id>" --change "<name>" --json` for how to create it.
|
||||
- Change already implemented (tasks checked off / already applied) -> the code may no longer match the revised plan; suggest `/openspec-apply-change` to carry the delta into code.
|
||||
- Everything done and implemented -> suggest `/openspec-archive-change`.
|
||||
|
||||
**Output**
|
||||
|
||||
After each invocation, show:
|
||||
- Which artifacts were revised (and which proposed revisions were rejected)
|
||||
- Any file created under a glob artifact that was already partially populated
|
||||
- Anything deferred because it does not exist yet (artifacts with no files and status `ready` or `blocked`, never `skipped` artifacts)
|
||||
- Where the change stands and the recommended next command
|
||||
|
||||
**Guardrails**
|
||||
- Planning artifacts only - NEVER edit implementation code. If the revised plan implies code changes, stop and point to `/openspec-apply-change`.
|
||||
- Use the artifact ids and paths reported by `openspec status`; never branch on hardcoded artifact names.
|
||||
- Edit only the concrete files in `existingOutputPaths`; never write to a glob `resolvedOutputPath`.
|
||||
- Do not advance the build frontier: if an artifact has empty `existingOutputPaths` and status `ready` or `blocked`, creating them is a separate step, outside this workflow. Leave `skipped` artifacts untouched. The only new-file scope is a confirmed concrete path under a glob artifact whose `existingOutputPaths` is non-empty.
|
||||
- Confirm every edit with the user before writing.
|
||||
- If the request changes the change's *intent* rather than refining it, ask for a distinct unused change name and recommend `openspec new change "<new-change-name>"` instead (the "Update vs. Start Fresh" heuristic).
|
||||
@@ -1,141 +0,0 @@
|
||||
# Gitea Issue Reconciliation — canonical record
|
||||
|
||||
_Generated T15 Part J/K/L (2026-09-11). Machine-readable companion:
|
||||
`docs/gitea_issue_reconciliation.json` (single authority; this doc is the
|
||||
human-readable projection). Consistency checker: `scripts/gitea_reconciliation_check.py`
|
||||
compares live Gitea state against the JSON and fails on drift._
|
||||
|
||||
Reconciliation states (closed vocabulary):
|
||||
`VALID_OPEN | FIXED_VERIFIED | SUPERSEDED | DUPLICATE | HISTORICAL_EVIDENCE |
|
||||
ACCEPTED_LIMITATION | NEEDS_REVALIDATION | DEFERRED_FUTURE`
|
||||
|
||||
## Bug-label audit (Part K)
|
||||
|
||||
`bug` = demonstrated currently incorrect behavior. Nothing else.
|
||||
|
||||
### Issues labelled `bug` at audit time
|
||||
|
||||
| # | live defect | evidence | final label | reconciliation state |
|
||||
|---|---|---|---|---|
|
||||
| 324 | **YES** | MessageAbortedError reproduces every aborted worker session (15+ checkpoints, including this session's predecessor) | bug (kept) | VALID_OPEN |
|
||||
| 204 | NO | Forge-orchestrated pipeline = feature request (P1 feature); no demonstrated incorrect behavior | bug removed | VALID_OPEN (feature) |
|
||||
| 157 | NO | capture_failure.py + tests/fixtures/production/ + manifest.json shipped and exercised (plan 2026-09-07-157) | bug removed | FIXED_VERIFIED (closed) |
|
||||
| 145 | NO | P0-07 follow-up work item (tracing coverage), not a defect report | bug removed | VALID_OPEN (work item) |
|
||||
| 139 | NO | Hand-maintained allowlist replaced by declarative classification (tui_validator.py cites #139); `tui_validator.py check` → passed, exit 0 | bug removed | FIXED_VERIFIED (closed) |
|
||||
| 126 | NO | Silent-handler sweep = refactor program (S110/BLE001 ratchet governs), not a defect | bug removed | VALID_OPEN (work item) |
|
||||
| 37 | NO | Personality corpus compilation = P2 feature | bug removed | VALID_OPEN (feature) |
|
||||
| 16 | NO | v1 course-correction EPIC | bug removed | VALID_OPEN (epic) |
|
||||
|
||||
### Issues whose `bug` label was removed in the prior cleanup (Part D, earlier session)
|
||||
|
||||
16 targeted: #301 #300 #292 #284 #314 #181 #166 #165 #163 #159 #154 #141 #40 #32 #31 #27.
|
||||
Verification (live API, this session): all confirmed bug-free EXCEPT #314 — its
|
||||
removal had failed (the API-False defect) and the label persisted on a closed
|
||||
issue. Removed this session by label-ID DELETE (204). #314 is closed
|
||||
FIXED_VERIFIED (mode:association_merged regression shipped in 2bf43ea).
|
||||
|
||||
### Totals
|
||||
|
||||
- bug-labelled before audit: 8 open (+ #314 closed-but-labelled)
|
||||
- bug-labelled after audit: **1** (#324, live defect, evidence above)
|
||||
- issues closed by this audit: #139, #157 (both FIXED_VERIFIED with evidence comments)
|
||||
- NEEDS_REVALIDATION count: 0 among bug-labelled issues (each resolved on evidence)
|
||||
|
||||
## Open-issue reconciliation (Part J)
|
||||
|
||||
Subsystems derived from labels. States per closed vocabulary. `next action` is
|
||||
the single next concrete step; `_` = none until its tranche opens.
|
||||
|
||||
| # | subsystem | state | current evidence | next action |
|
||||
|---|---|---|---|---|
|
||||
| 324 | engine/checkpoint | VALID_OPEN | live repro (15+ sessions) | fix writer abort handling |
|
||||
| 316 | ADAPT-1/research | VALID_OPEN | B3-v2 research task | await ADAPT tranche |
|
||||
| 315 | ADAPT-1/wiring | VALID_OPEN | B2-v2 standard task | await ADAPT tranche |
|
||||
| 311 | ADAPT-1/wiring | VALID_OPEN | superseded-by #315 (kept open as spec anchor until 315 delivers) | close when #315 closes |
|
||||
| 301 | FINAL-12/benchmarks | NEEDS_REVALIDATION | dependency list silently grew (operator-path telemetry, bridge overhead) without issue update | re-scope before PL1 metrics work |
|
||||
| 300 | ADAPT-11 | VALID_OPEN | acceptance = real escalation demo; blocked on ADAPT-LIVE telemetry | demo after PL1 held-out |
|
||||
| 299 | ADAPT-10 | VALID_OPEN | harness legibility audit | _ |
|
||||
| 297 | ADAPT-8 | VALID_OPEN | deterministic-first QUALIFY | _ |
|
||||
| 296 | ADAPT-7 | VALID_OPEN | DESIGN consolidation experiment | _ |
|
||||
| 295 | ADAPT-6 | VALID_OPEN | evidence capsule / context minimization | _ |
|
||||
| 294 | ADAPT-5 | VALID_OPEN | budgets + stop conditions | _ |
|
||||
| 293 | ADAPT-4 | VALID_OPEN | DESIGN-LITE shipped (STANDARD_CHANGE lane live) | acceptance demo, then close |
|
||||
| 292 | ADAPT-3 | VALID_OPEN | LOCAL_FIX lane shipped | acceptance demo, then close |
|
||||
| 284 | ADAPT epic | VALID_OPEN | epic tracks ADAPT-LIVE + PL1; body updated with detour record | update after PL1 |
|
||||
| 259 | Scaffolding | VALID_OPEN | redefined: safe repeatable construction protocol (T15 §C) | Scaffolding v1 after PL1 |
|
||||
| 258 | State-aware processes | VALID_OPEN | redefined: low-compute architectural self-awareness; SA1-8 superseded by distributed concerns | deferred research |
|
||||
| 204 | forge/pipeline | VALID_OPEN | P1 feature: forge-orchestrated coordination | _ |
|
||||
| 183–176 | memory-foundation futures | DEFERRED_FUTURE | P3 research futures (typed temporal semantics, prior-art audit, curiosity, lineage) | memory-foundation tranche |
|
||||
| 175/181 | memory epics | DEFERRED_FUTURE | evidence-grounded continuity epics | same |
|
||||
| 166 | research watchlist | DEFERRED_FUTURE | external research tracking | periodic review |
|
||||
| 165/164 | research spikes | DEFERRED_FUTURE | SimpleMem/A-MEM, HippoRAG 2 | memory-foundation tranche |
|
||||
| 163 | graph backend spike | NEEDS_REVALIDATION | P1 spike; LadybugDB evaluation partially done in prior research artifacts | revalidate against current graph.db-missing state |
|
||||
| 162 | Qdrant spike | NEEDS_REVALIDATION | P1 spike; prior research artifacts exist (issue-162/) | revalidate after #316 outcome |
|
||||
| 159 | conceptual topology | DEFERRED_FUTURE | P3 future | _ |
|
||||
| 158 | wiring-status report | VALID_OPEN | P2: generated architecture-truth report | _ |
|
||||
| 156 | mutation guards | VALID_OPEN | P1: cognitive-state mutation guards | _ |
|
||||
| 155 | e2e activation trace | VALID_OPEN | P1: credit-assignment trace | overlaps PL1 outcome work — potential duplicate of #300's telemetry; review at PL1 close |
|
||||
| 154/153 | learned-state / experience encoding | VALID_OPEN | P2 | PL1-adjacent |
|
||||
| 152/151 | break-it / adversarial | VALID_OPEN | P0: adversarial procedures after nominal green | post-PL1 tranche |
|
||||
| 145 | P0-07 follow-up | VALID_OPEN | tracing coverage gaps | _ |
|
||||
| 141 | ContextBroker compaction | VALID_OPEN | P1: rolling-compaction persistence | _ |
|
||||
| 139 | tui_validator drift | FIXED_VERIFIED | declarative classification shipped; check passes | closed |
|
||||
| 131/130/129 | reviewer program | VALID_OPEN | P0: local reviewer + calibration + rubric | post-PL1 tranche |
|
||||
| 126 | silent-handler sweep | VALID_OPEN | P0 refactor program; ratchet enforces ceiling | ongoing |
|
||||
| 125 | decompose monoliths | VALID_OPEN | P2 | _ |
|
||||
| 49–35 | v1-course-correction P2/P3 | DEFERRED_FUTURE | epic #16 manifest items (P2-01..09, P3-01..06) | per epic #16 sequencing |
|
||||
| 33–27 | v1-course-correction P1 | VALID_OPEN | epic #16 manifest P1 items | per epic #16 sequencing |
|
||||
| 16 | v1 epic | VALID_OPEN | consolidation epic; 33 manifest issues | per epic sequencing |
|
||||
| 13 | TUI message graph | VALID_OPEN | P1 | _ |
|
||||
| 3 | financial tracking | NEEDS_REVALIDATION | non-Rune issue (different project era) | transfer to correct repo or close |
|
||||
| 2 | cognitive loop wiring | NEEDS_REVALIDATION | predates current TUI architecture | revalidate against current TUI |
|
||||
| 1 | personality injection | NEEDS_REVALIDATION | overlaps #37's newer scope | merge into #37 or close |
|
||||
|
||||
## Reconciliation decisions this session (Part K deltas)
|
||||
|
||||
- CLOSED: #139, #157 (FIXED_VERIFIED, evidence comments posted)
|
||||
- BUG LABEL REMOVED: #204, #145, #126, #37, #16 (audit: not defects)
|
||||
- BUG LABEL REMOVED (stuck from prior cleanup): #314 (closed issue)
|
||||
- OPEN BUGS: 8 → 1 (#324 only — genuine live defect)
|
||||
|
||||
## Part L — consistency check
|
||||
|
||||
`scripts/gitea_reconciliation_check.py` (deterministic, no governance
|
||||
subsystem): fetches live issue state, compares to
|
||||
`docs/gitea_issue_reconciliation.json`, flags:
|
||||
|
||||
- closed issue recorded VALID_OPEN / DEFERRED_FUTURE / NEEDS_REVALIDATION (open states)
|
||||
- open issue recorded FIXED_VERIFIED / SUPERSEDED / DUPLICATE / HISTORICAL_EVIDENCE without being closed
|
||||
- `bug` label present on any issue not recorded as a live defect (or missing on #324)
|
||||
- issue missing from the artifact
|
||||
- record lacking a required field
|
||||
|
||||
Exit 0 = consistent; exit 1 = drift listed. CI-eligible (read-only).
|
||||
|
||||
|
||||
## PL1 held-out era reconciliation (2026-09-12)
|
||||
|
||||
Fresh-session reconciliation of the ADAPT family against deployed 0.66.22
|
||||
@ 1497c85 + live PL1 held-out evidence (tasks task_2661189c9073,
|
||||
task_6e4885031355; ledger PE-9207213099-0270). Resolves the recorded tension:
|
||||
"ADAPT-LIVE complete → return target PL1" (the bounded tranche authorized
|
||||
2026-09-10 is complete) while numbered ADAPT issues remain open (the larger
|
||||
backlog was never in the ADAPT-LIVE tranche scope).
|
||||
|
||||
| # | state | evidence / basis |
|
||||
|---|---|---|
|
||||
| 284 | VALID_OPEN | epic tracker for the remaining sub-items below |
|
||||
| 292 | FIXED_VERIFIED (closed) | LOCAL_FIX fast path live (rune_operator.py:522,541); live-proven twice by the held-out tasks |
|
||||
| 293 | FIXED_VERIFIED (closed) | DESIGN-LITE implemented (rune_operator.py:660, cites #293), wired to STANDARD_CHANGE; suite+canary green |
|
||||
| 294 | DEFERRED_FUTURE | canonical remaining-obligations list ("ADAPT-5 budgets... for later epics") |
|
||||
| 295 | DEFERRED_FUTURE | canonical remaining-obligations list |
|
||||
| 296 | DEFERRED_FUTURE | canonical remaining-obligations list |
|
||||
| 297 | FIXED_VERIFIED (closed) | task_gate implemented (src/rune/task_gate.py) + broker wiring + canary registration |
|
||||
| 299 | DEFERRED_FUTURE | canonical remaining-obligations list |
|
||||
| 300 | VALID_OPEN | escalation mechanism exists (_probe_and_escalate); fresh deployed acceptance evidence pending |
|
||||
| 301 | DEFERRED_FUTURE | canonical remaining-obligations list (FINAL-12) |
|
||||
| 315 | VALID_OPEN | not started; ADAPT-1 sub-item (no canonical deferral authority) |
|
||||
| 316 | VALID_OPEN | not started; ADAPT-1 sub-item (no canonical deferral authority) |
|
||||
|
||||
Consistency checker: `scripts/gitea_reconciliation_check.py` → exit 0
|
||||
(72 records, 100 live issues in window) after these updates.
|
||||
@@ -1,53 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,62 +0,0 @@
|
||||
# ResearchProcess code classification manifest (RP2/#265)
|
||||
|
||||
Doctrine: detection may be aggressive; deletion must be conservative.
|
||||
This manifest classifies every research-execution code path. **RP11/#274 EXECUTED 2026-09-09** — RP10 proven (PR #279 merged); safe paths retired:
|
||||
`research_backend.py` (run_full_research self-walk + --watch), `run_research_stage.py`,
|
||||
`run_agent_stage.py` (+ its test), the three legacy research workflows, and the
|
||||
split-stage prompt templates. Retired stage names remain ONLY inside the
|
||||
RETIRED_STAGES guard that blocks them.
|
||||
|
||||
Classifications: CURRENT | ADAPT | SUPERSEDED | COMPATIBILITY | TEST_FIXTURE | DEBUG_ONLY | UNKNOWN_PURPOSE
|
||||
|
||||
## Canonical execution path (CURRENT)
|
||||
|
||||
| Path | Role | Notes |
|
||||
|---|---|---|
|
||||
| `scripts/research_task_tree.py` | canonical stage model | ISSUE→INWARD→OUTWARD→APPLICABILITY→(INNOVATION)→ADVERSARIAL→SYNTHESIS→QUALIFY; RETIRED_STAGES guard; conditional INNOVATION routing |
|
||||
| `scripts/research_stage_executor.py` | one-stage executor | stage-trigger boundary, idempotency, placement gate wired into execute() |
|
||||
| `scripts/research_define.py` | ISSUE gate compiler | deterministic, needs_mimo hardwired False (compat shim) |
|
||||
| `scripts/research_inward_context.py` | INWARD context builder | deterministic + bounded MiMo flag |
|
||||
| `scripts/research_prompt_compiler.py` | stage prompt compiler | canonical templates incl. issue.md, applicability.md |
|
||||
| `scripts/research_event_relay.py` | event streaming | ResearchEventRelay, tool receipts |
|
||||
| `scripts/research_artifact.py` | artifact validation | structured-artifact gate |
|
||||
| `scripts/research_router.py` | OUTWARD routing | academic/software/web classification |
|
||||
| `scripts/runner_evidence.py` | placement evidence | runtime hostname/runner capture + Pi rejection gate |
|
||||
| `scripts/fake_worker.py` | TEST_FIXTURE | deterministic pipeline tests |
|
||||
| `prompts/research-stages/*.md` | stage templates | issue.md + applicability.md added RP2 |
|
||||
|
||||
## SUPERSEDED (retire in RP11 after RP10 proof; DO NOT DELETE NOW)
|
||||
|
||||
| Path | Superseded by | Classification | Evidence |
|
||||
|---|---|---|---|
|
||||
| `scripts/research_backend.py::run_full_research()` | one-stage-per-StageReady executor | RETIRED 2026-09-09 (self-walking loop, §26.6) | `while stage_count < max_stages` local advance loop; called by `--issue` CLI path and `--watch` loop |
|
||||
| `scripts/research_backend.py --watch` | Gitea-authorized StageReady events | RETIRED 2026-09-09 (poll-loop supervisor, §26.7) | `while True: gitea_api(...); time.sleep(POLL_INTERVAL)` — deterministic reconciliation only, but watches+launches whole processes rather than single stages |
|
||||
| `scripts/run_research_stage.py` | `research_stage_executor.py` | ADAPT → verify before RP3 | single ref from one workflow |
|
||||
| `scripts/run_agent_stage.py` | unified OUTWARD executor | UNKNOWN_PURPOSE | only self-referential test found; investigate in RP11 |
|
||||
| old stage names `DEFINE`/`OUTWARD_DISCOVERY`/`OUTWARD_READING` | ISSUE / unified OUTWARD | COMPATIBILITY (executor returns RETIRED_STAGE; advance raises) | cannot become READY through canonical path — regression-tested |
|
||||
| `prompts/research-stages/outward_discovery.md`, `outward_reading.md`, `define.md` | outward.md, issue.md | COMPATIBILITY (not routed by canonical compiler for new runs) | verify no dynamic selection in RP11 |
|
||||
| `.gitea/workflows/research-dispatcher.yml`, `research-stage-headless.yml` dispatch semantics | StageReady → desktop executor | ADAPT | runs-on: rune-ci-desktop already correct; stage input default `DEFINE` must move to `ISSUE` |
|
||||
|
||||
## Issues classified as disposable acceptance history (26.8)
|
||||
|
||||
- #245, #254, #255, #256, #260, #261, #262: `research-pending` trigger labels
|
||||
removed 2026-09-08 (RP0) — no stale process can redispatch.
|
||||
- #263: closed+superseded with evidence note; artifacts preserved at
|
||||
`/mnt/workdrive/research-stage/research/issue-263/`.
|
||||
|
||||
## tui-restart-pending (legacy permission/restart mechanism)
|
||||
|
||||
Classification: **SUPERSEDED CANDIDATE** (not removed).
|
||||
The pending restart procedure existed to fix permission flags for the OLD
|
||||
research dispatch flow. The corrected canonical path does not depend on it.
|
||||
Verify before RP11 removal that no current hook still reads the flag file.
|
||||
|
||||
## RP11 acceptance checklist (carried forward from handoff §26.11)
|
||||
|
||||
- one canonical authority per responsibility (stage model, StageReady,
|
||||
execution, eligibility, artifact schema, gate, persistence, TUI
|
||||
projection, runner placement, refinement routing)
|
||||
- no production self-walking loop; no foreground sleep/poll supervisor
|
||||
- no heavy-cognition path targeting the Pi
|
||||
- old stage names cannot become READY (already proven: test_retired_stages_cannot_become_ready)
|
||||
- one real progression issue still completes after cleanup
|
||||
@@ -1,188 +0,0 @@
|
||||
{
|
||||
"runs": {
|
||||
"161": {
|
||||
"issue": 161,
|
||||
"stages": {
|
||||
"ISSUE": {
|
||||
"elapsed_s": 0.0,
|
||||
"tool_calls": 0,
|
||||
"queries": 0,
|
||||
"sources_read": 0,
|
||||
"tool_failures": 0
|
||||
},
|
||||
"INWARD": {
|
||||
"elapsed_s": 0.0,
|
||||
"tool_calls": 0,
|
||||
"queries": 0,
|
||||
"sources_read": 0,
|
||||
"tool_failures": 0
|
||||
},
|
||||
"OUTWARD": {
|
||||
"elapsed_s": 0.0,
|
||||
"tool_calls": 22,
|
||||
"queries": 4,
|
||||
"sources_read": 4,
|
||||
"tool_failures": 0
|
||||
},
|
||||
"APPLICABILITY": {
|
||||
"elapsed_s": 0.0,
|
||||
"tool_calls": 20,
|
||||
"queries": 0,
|
||||
"sources_read": 0,
|
||||
"tool_failures": 0
|
||||
},
|
||||
"ADVERSARIAL": {
|
||||
"elapsed_s": 0.0,
|
||||
"tool_calls": 25,
|
||||
"queries": 0,
|
||||
"sources_read": 2,
|
||||
"tool_failures": 0
|
||||
},
|
||||
"SYNTHESIS": {
|
||||
"elapsed_s": 0.0,
|
||||
"tool_calls": 14,
|
||||
"queries": 0,
|
||||
"sources_read": 0,
|
||||
"tool_failures": 1
|
||||
},
|
||||
"QUALIFY": {
|
||||
"elapsed_s": 310.1,
|
||||
"tool_calls": 20,
|
||||
"queries": 0,
|
||||
"sources_read": 0,
|
||||
"tool_failures": 0
|
||||
}
|
||||
},
|
||||
"totals": {
|
||||
"cognition_elapsed_s": 310.1,
|
||||
"tool_calls": 101
|
||||
},
|
||||
"research": {
|
||||
"queries": 4,
|
||||
"sources_discovered": 16,
|
||||
"sources_read": 4,
|
||||
"contradictions": 4,
|
||||
"limitations": 8
|
||||
},
|
||||
"adversarial": {
|
||||
"attacks": 16,
|
||||
"survivors": 6,
|
||||
"rejected": 1,
|
||||
"mitigations": 10,
|
||||
"kill_tests": 7,
|
||||
"disposition": "SURVIVES_WITH_MITIGATIONS"
|
||||
}
|
||||
},
|
||||
"163": {
|
||||
"issue": 163,
|
||||
"stages": {
|
||||
"ISSUE": {
|
||||
"elapsed_s": 0.0,
|
||||
"tool_calls": 0,
|
||||
"queries": 0,
|
||||
"sources_read": 0,
|
||||
"tool_failures": 0
|
||||
},
|
||||
"INWARD": {
|
||||
"elapsed_s": 0.0,
|
||||
"tool_calls": 0,
|
||||
"queries": 0,
|
||||
"sources_read": 0,
|
||||
"tool_failures": 0
|
||||
},
|
||||
"OUTWARD": {
|
||||
"elapsed_s": 0.0,
|
||||
"tool_calls": 34,
|
||||
"queries": 5,
|
||||
"sources_read": 10,
|
||||
"tool_failures": 2
|
||||
},
|
||||
"APPLICABILITY": {
|
||||
"elapsed_s": 300.1,
|
||||
"tool_calls": 8,
|
||||
"queries": 0,
|
||||
"sources_read": 0,
|
||||
"tool_failures": 0
|
||||
},
|
||||
"INNOVATION": {
|
||||
"elapsed_s": 422.1,
|
||||
"tool_calls": 9,
|
||||
"queries": 0,
|
||||
"sources_read": 0,
|
||||
"tool_failures": 1
|
||||
},
|
||||
"ADVERSARIAL": {
|
||||
"elapsed_s": 0.0,
|
||||
"tool_calls": 25,
|
||||
"queries": 0,
|
||||
"sources_read": 0,
|
||||
"tool_failures": 0
|
||||
},
|
||||
"SYNTHESIS": {
|
||||
"elapsed_s": 677.1,
|
||||
"tool_calls": 15,
|
||||
"queries": 0,
|
||||
"sources_read": 0,
|
||||
"tool_failures": 0
|
||||
},
|
||||
"QUALIFY": {
|
||||
"elapsed_s": 305.1,
|
||||
"tool_calls": 12,
|
||||
"queries": 0,
|
||||
"sources_read": 0,
|
||||
"tool_failures": 0
|
||||
}
|
||||
},
|
||||
"totals": {
|
||||
"cognition_elapsed_s": 1704.3,
|
||||
"tool_calls": 103
|
||||
},
|
||||
"research": {
|
||||
"queries": 5,
|
||||
"sources_discovered": 45,
|
||||
"sources_read": 10,
|
||||
"contradictions": 4,
|
||||
"limitations": 8
|
||||
},
|
||||
"adversarial": {
|
||||
"attacks": 13,
|
||||
"survivors": 4,
|
||||
"rejected": 1,
|
||||
"mitigations": 7,
|
||||
"kill_tests": 6,
|
||||
"disposition": "SURVIVES_WITH_MITIGATIONS"
|
||||
}
|
||||
}
|
||||
},
|
||||
"old_process_baseline": {
|
||||
"foreground_sleep_per_batch_s": 300,
|
||||
"batches_observed_in_rsc0_red": "N (300s x N while child productive)",
|
||||
"wall_clock_issue_245_era": "hours (multiple restarts, no durable state)",
|
||||
"source_receipts": "model-claimed only (no relay receipts)",
|
||||
"read_provenance": "none \u2014 snippets counted as evidence"
|
||||
},
|
||||
"quality_outcomes": {
|
||||
"161": {
|
||||
"architectural_mistakes_prevented": [
|
||||
"MemoryAdapter second protocol rejected (duplicate authority vs memory_selector registry)",
|
||||
"Harness would have silently validated non-hybrid mode (A1: _embed degradation) \u2014 kill-test FT1 forced mode observability",
|
||||
"Cache-pollution benchmark hazard (A4) \u2014 forced cache bypass invariants"
|
||||
],
|
||||
"implementation_ambiguity_reduction": "QUALIFY 13/13 checks + source-verified interface review; implementation merged first try, 0 CI code failures (2 CI-contract failures were master debt)",
|
||||
"rework": "1 REFINE loop none; implementation PR #279 merged without implementation rework"
|
||||
},
|
||||
"163": {
|
||||
"architectural_mistakes_prevented": [
|
||||
"External graph DB adoption blocked (3 candidates CONFLICTS_WITH_RUNE \u2014 would replace SQLite tier)",
|
||||
"False bi-temporal claim corrected from code (graph_memory is transaction-time append) before synthesis",
|
||||
"INN-1 filter formulation falsified (A2) \u2014 synthesis v2 corrected"
|
||||
],
|
||||
"implementation_ambiguity_reduction": "QUALIFY round 1 caught 3 concrete interface gaps (DDL, parameter, alias ordering) \u2014 REFINE_SYNTHESIS fixed all in one loop",
|
||||
"rework": "1 REFINE_SYNTHESIS loop (targeted, synthesis-only \u2014 no research restart)"
|
||||
}
|
||||
},
|
||||
"comparison_principles": {
|
||||
"cognition_vs_orchestration": "cognition = MiMo stage elapsed (measured where recorded); orchestration/wait = runner queue + CI latency \u2014 NOT attributed to the pipeline",
|
||||
"wall_clock": "#161 research chain \u2248 75 min git-window (00:16\u219201:31) incl. live CI defect discovery/fixes; #163 persisted in one batch (stage elapsed recorded per-stage)"
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
# RUNE CANONICAL INTENT STATE
|
||||
**v1.0 · 2026-09-10 · Owner: Rune · Review: Nick**
|
||||
*This artifact is the project's authoritative answer to: "What were we trying to achieve, what interrupted it, what remains, and where do we return?" It is state, not a planning framework. Update it when intent changes; do not extend it into a second roadmap system.*
|
||||
|
||||
---
|
||||
|
||||
## PRIMARY INTENT
|
||||
|
||||
Build Rune: a stateful, quality-first coding assistant whose cognitive systems are genuinely reachable from live execution — measured by the Runtime Reachability doctrine (`IMPLEMENTED → CALLED BY PRODUCTION → CHANGES EXECUTION → PERSISTED → OBSERVABLE → END-TO-END PROVEN`), not by module existence.
|
||||
|
||||
The parent work at the moment of interruption: **Epic #284 — the adaptive development pipeline** (successor to the ratified Research-to-Build process #257/RP0–RP14). Doctrine: *research buys down uncertainty; it is not a tax on every code change.* Three process depths (LOCAL_FIX / STANDARD_CHANGE / RESEARCHED_CHANGE), reversible routes, one canonical route authority, the ratified pipeline as the high-assurance fallback.
|
||||
|
||||
## CURRENT ACTIVE WORK
|
||||
|
||||
**Deployment Contract v1 + ADAPT-LIVE acceptance closure** (master 5c8b252+, 2026-09-11).
|
||||
|
||||
CORRECTION RECORD (intent-continuity evidence, 2026-09-11): the previous tranche declared "ADAPT-LIVE complete" while an unresolved user-established blocker existed (deployed TUI launch broken; version bumping stalled). Honest status:
|
||||
|
||||
- **Deployment Contract v1: PROVEN** — deterministic CI-qualified deploy pipeline (stage → verify → REAL TUI startup canary via Textual run_test → atomic promote, last-good rollback); loop-guarded auto-version convergence (0.66.0 → 0.66.2 on push events; chore(release) never re-bumps); launcher deployable; runtime reports deployed SHA/version.
|
||||
- **ADAPT-LIVE: IMPLEMENTATION SUBSTANTIALLY PRESENT, ACCEPTANCE PROVEN ON DEPLOYED RUNTIME** — route discriminator (defect/change language + leading-verb governance), DESIGN-LITE, completion truth (mutation tasks without mutating tool evidence cannot complete), verification→probe→escalate BEFORE terminal, deployed-runtime canaries: LOCAL_FIX/STANDARD_CHANGE honestly fail NOT_COMPLETED with text-only responses (false success structurally impossible), conversation tasks complete normally, TWO REAL Evidence Probes through the deployed runtime with genuine cited evidence (pytest exit-code semantics + empirical check; deployed-tree source inspection). Escalation-with-real-worker partially proven (the competent worker correctly GROUNDS rather than fabricates escalation; unresolved-case escalation proven at module level).
|
||||
- Earlier "ADAPT-LIVE complete" declaration (e92c993) was PREMATURE — recorded as evidence for the future intent-continuity system: a child task must not replace unresolved parent acceptance obligations merely because its implementation completed.
|
||||
|
||||
Delivered across the tranche:
|
||||
|
||||
- Route discriminator (defect vs change language, provenance-tracked) separates LOCAL_FIX from STANDARD_CHANGE
|
||||
- LOCAL_FIX: focused verification policy, no research; verifiability NONE no longer blocks defect-class narrow tasks
|
||||
- STANDARD_CHANGE: DESIGN-LITE record (bounded build-contract, no model call) + integration verification
|
||||
- RESEARCHED_CHANGE: unchanged high-assurance fallback (`research_required` nonterminal disposition)
|
||||
- Evidence Probe LIVE via verification-driven trigger: real verification failure on the operator spine → one bounded probe → GROUNDED resumes / unresolved ESCALATES to RESEARCHED_CHANGE (route change, not task failure; opt-in via `MIMO_PROBE_ENABLED=1` in the TUI launcher)
|
||||
- Probe-grounded downgrade (§7): fully-grounded probes downgrade STANDARD_CHANGE → LOCAL_FIX with recorded trigger
|
||||
- Canary evidence: route latency 0.2–16ms vs fixed-depth 384s+ wasted on LOCAL tasks; escalation proven LOCAL_FIX → probe → RESEARCHED_CHANGE with persisted evidence
|
||||
|
||||
Reachability detour (CLOSED):
|
||||
- Runtime Reachability audit (one live spine identified; dead cognitive subnet mapped)
|
||||
- Unified Core route authority (`src/rune/adaptive_router.py` + `evidence_probe.py` + `route_risk_flags.py`) consumed by both the research executor and `RuneOperator.send_task`
|
||||
- Builder bridge live-consumed: `~/.mimocode/hooks/builder-bridge.ts` → `builder_bridge_cli.py` → Rune Core preflight in actual MiMo context (fire-trace: `/tmp/rune-bridge-fire.log`)
|
||||
- `research_required` nonterminal disposition; `route_ref` reference model; PL0 experience ledger with live producer and TELEMETRY/CANDIDATE qualification
|
||||
- `recall_uplift` vendored into core (PROVISIONAL — A/B documented, not proven equivalent)
|
||||
|
||||
## WHY CURRENT WORK INTERRUPTED THE PARENT
|
||||
|
||||
The ADAPT-2 audit found `classify_route()` had **zero production callers** — the route authority was architecturally implemented but cognitively dead. Wiring it honestly forced the discovery that Rune Core itself was not reachable from any live path the user experiences (the TUI path was live but disconnected from adaptive cognition; the MiMo builder session consumed none of it). Fixing that root defect was a precondition for every ADAPT capability being anything more than dead machinery: **adaptive routes are worthless if nothing live consumes them.**
|
||||
|
||||
## RETURN CONDITION
|
||||
|
||||
This detour is complete when:
|
||||
- [x] One canonical route authority consumed by production (operator + research executor)
|
||||
- [x] Research dispatch causes a real disposition (`research_required`, not failure)
|
||||
- [x] Terminal-outcome semantics corrected (no false `task.outcome` on nonterminal routing)
|
||||
- [x] Builder bridge consumed by the actual MiMo harness with measured overhead
|
||||
- [x] Deferred defects recorded (below), not chased
|
||||
- [x] ADAPT intent recovered and reconciled (this artifact, Part B)
|
||||
- [ ] **THIS TRANCHE**: the ADAPT status matrix disposition is agreed and the next tranche selected (Part D)
|
||||
|
||||
## RETURN TARGET / CURRENT: PL1 Process Learning — FIRST TRANCHE LIVE (2026-09-11)
|
||||
|
||||
PL1-01 structured Experience records: `scripts/pl1_records.py` (task_id, route resolved from route.jsonl, proof level, failure class, candidate status). PL1-02 failure classification in the PL0 vocabulary (research_dispatch, NOT_COMPLETED_NO_MUTATION_EVIDENCE, verification_failure, model_refusal, UNKNOWN). PL1-03 first learning loop: recurring prose-instead-of-mutation family → structural correction (route-driven instruction in the user context block) + replay contract (NOT_COMPLETED gate) — record PE-1789102658-0269 (SEED 2026-09-11 → VALIDATED 2026-09-12: two fresh deployed held-out mutation tasks completed-with-evidence — task_2661189c9073 + task_6e4885031355, LOCAL_FIX, Verboo-pinned, mutation + exit-code verification, ledger PE-9207213099-0270; substrate corrections 6965fc7 flat-ChatMessage contract + a2463ad resume transport selection applied between; caveat: corrections developer-applied structurally, not autonomous self-modification; PROMOTED requires repetition/soak). PL1-04 promotion discipline: SEED→CANDIDATE→VALIDATED→PROMOTED via process_experience; structural fixes preferred. PL1-05 metrics: first_pass_accepted, mutation_failures, research_dispatches, false_completion_rate (0.0 — structurally prevented), candidates vs evidence-only split.
|
||||
|
||||
Remaining ADAPT obligations for later epics: ADAPT-5 budgets, ADAPT-6 evidence capsule, ADAPT-7 DESIGN consolidation, ADAPT-10 legibility, FINAL-12 cross-track benchmark, D7 release-job-on-Pi.
|
||||
|
||||
## OPEN OBLIGATIONS (deferred defects — recorded, NOT chased)
|
||||
|
||||
| ID | Obligation | Evidence | Destination |
|
||||
|---|---|---|---|
|
||||
| D1 | `memory_store.delete_entry()` leaves contentless-FTS ghosts (requires special FTS5 `INSERT ... VALUES('delete')` cleanup; retrieval can ghost-hit deleted entries) | 2026-09-10 QX7 purge required manual FTS commands in both stores | Memory-foundation work (deferred by design — handoff §17) |
|
||||
| D2 | Builder-bridge hook lives at `~/.mimocode/hooks/builder-bridge.ts` with a hardcoded repo path — no canonical ownership, no repeatable install/sync; a harness reinstall silently loses Rune's builder integration. (PARTIALLY CLOSED 2026-09-10: the Rune CODE side now has a deterministic deploy pipeline — `.gitea/workflows/deploy.yml`, stage→verify→promote with `last-good` rollback; the HOOK side still hand-installed) | Hook hand-installed 2026-09-10; deploy pipeline live (run 14243 success, e2e verified 51e14d6) | Scaffolding (SC1/SC2 territory) for hook deployment when the bridge becomes user-facing |
|
||||
| D3 | `/tmp/rune-bridge-fire.log` fire-trace is ephemeral (lost on reboot) — standing observability, not durable telemetry; accurate description: per-boot injection evidence only | Design note in hook source | Fold into OBS-1-style durable telemetry when next touched; no project now |
|
||||
| D4 | `recall_uplift` retrieval-quality effect unproven (PROVISIONAL) — recall row counts match but surfaced IDs differ; harness original retained at `~/.config/opencode/scripts/` for rollback | A/B table in module docstring | Compare in FINAL-12 or first memory-foundation pass |
|
||||
| D5 | `CloudStubAdapter` declared-not-live: router can elect cloud models that execution cannot run (`routing_mode=cloud` is telemetry theater) | Audit §A fragmentation 1 | When cloud execution is actually wanted; otherwise retire the election path |
|
||||
| D6 | Dead cognitive subnet (salience_gate, cognitive_budgeter, tiered_executor, progressive_escalation, verification_layer, outcome_ledger + companions): implemented, unconsumed, partially duplicated by ADAPT-2 | Reachability audit §C | Disposition in Scaffolding/PL1 review: RETIRE more likely than RESURRECT — superseded or absorbed |
|
||||
|
||||
## PARKED INNOVATIONS
|
||||
|
||||
- **Intent-continuity spine**: this artifact is the manual v1. The automated version (intent-state tracking that survives detours without a human handoff) belongs after PL1 — PL1's experience machinery is its natural producer. Do not build early.
|
||||
- **Substrate**: deferred behind a domain-neutral education phase (handoff §16). Unchanged.
|
||||
- **Scaffolding** SC1–SC8: deferred until adaptive/live/learning foundations are credible (handoff §15). Unchanged.
|
||||
- **Control benchmarking** (born-isolated disposal control vs full Rune): deferred to system maturity (handoff §18). Preserve passive instrumentation meanwhile — task.outcome + route.jsonl + StageAttempt already cover the required fields.
|
||||
|
||||
## SUPERSEDED APPROACHES
|
||||
|
||||
- **Option A/B route placement** (route-in-operator / two-worlds): superseded by Option C unified core authority (Nick decision, 2026-09-10).
|
||||
- **`progressive_escalation.py` ladder**: superseded by `adaptive_router.py` (duplicated concept; the original was never wired).
|
||||
- **`outcome_ledger.py` as separate authority**: superseded by the event-driven `task.outcome` + `process_experience.py` flow.
|
||||
- **Fixed-depth research pipeline as universal path**: superseded by adaptive routing (the existence proof: 2 of 3 task classes cannot pass its OUTWARD gate).
|
||||
- **per-message cognitive pipeline (P0-04)**: deliberately removed; the ContextBroker owns model-facing context. Do not resurrect.
|
||||
- **Manual handoff files as the only intent carrier**: superseded by this artifact (handoffs remain valuable input, not the system of record).
|
||||
|
||||
---
|
||||
|
||||
## ADAPT RECONCILIATION (Epic #284, recovered 2026-09-10)
|
||||
|
||||
### What ADAPT was supposed to become
|
||||
|
||||
From epic #284 verbatim: *"convert the proven high-assurance research process into an adaptive universal engineering framework that applies only as much process as the uncertainty deserves."* One framework, three depths, reversible routes, escalation/downgrade as first-class recorded transitions, the ratified pipeline as fallback, FINAL-12 cross-track benchmark as the ratification gate. Success criteria: 5/5 limitations dispositioned, all three routes through one canonical authority, FINAL-12 beats fixed-depth without quality regression.
|
||||
|
||||
### Status matrix
|
||||
|
||||
| Capability | Status | Current owner | Live consumer | Future destination | Reason |
|
||||
|---|---|---|---|---|---|
|
||||
| Route authority (classify/escalate/downgrade/floor) | **BUILT_AND_LIVE** | `src/rune/adaptive_router.py` | Operator `send_task` + research executor ISSUE dispatch | PL1 consumes its decisions | Wired, persisted, canaried |
|
||||
| Route decisions on the operator spine | **BUILT_AND_LIVE** | `rune_operator._route_task_class` | Every `send_task` | — | `route.jsonl` + `route_ref` verified |
|
||||
| RESEARCHED_CHANGE disposition | **BUILT_AND_LIVE** | `rune_operator.send_task` | Floor tasks halt local path, `research_required` + dispatch | Research consumer (below) | Live canary bf5eb53 + this tranche |
|
||||
| Targeted Evidence Probe | **BUILT_NOT_LIVE** | `src/rune/evidence_probe.py` | Wired in executor dispatch; gated behind `MIMO_PROBE_ENABLED` (nobody sets it) | ADAPT-LIVE tranche: decide the runtime policy and prove one real probe | The §6 qualification stands |
|
||||
| Verification-driven escalation | **BUILT_NOT_LIVE** | `adaptive_router.escalate_on_verification_failure` | Contract message names the hook; no automatic caller | ADAPT-LIVE tranche: wire into the local lane's verify step | Handoff §6 of bridge tranche; still true |
|
||||
| LOCAL_FIX execution path | **PARTIAL** | Route exists; execution = ordinary model path | Operator completes LOCAL tasks normally | ADAPT-3/#292: regression-gated fast path per issue spec | Route changes nothing for LOCAL today beyond disposition recording |
|
||||
| STANDARD_CHANGE path (DESIGN-LITE) | **PARTIAL** | Route exists | Same ordinary path | ADAPT-4/#293 DESIGN-LITE step | Same reason |
|
||||
| RESEARCHED_CHANGE bounded execution | **PARTIAL** | Dispatch persisted; orchestrator = MiMo session (documented §11 gap) | Session drives executor CLI manually | ADAPT-5/#294 budgets + the §11 orchestrator decision | Recorded honestly in dispatch payload |
|
||||
| Evidence capsule (#295) | **PARTIAL** (proto: builder capsule) | `builder_bridge` | MiMo hook | ADAPT-6 after paths exist | The bridge capsule is task-preflight, not OUTWARD evidence compression |
|
||||
| DESIGN consolidation (#296) | **NOT STARTED** | — | — | ADAPT-7, after ADAPT-5 | A/B experiment design exists in issue |
|
||||
| Deterministic-first QUALIFY (#297) | **BUILT_AND_LIVE (pipeline) / NOT_LIVE (operator)** | `_execute_qualify` in executor | Research pipeline only | ADAPT-8: reuse for STANDARD_CHANGE DESIGN-LITE gate | Already deterministic-first per RP8 |
|
||||
| Legibility audit (#299) | **NOT STARTED** | — | — | ADAPT-10, after routes live | Depends on 3/4/5/9 |
|
||||
| Escalation/downgrade real acceptance (#300) | **BUILT_NOT_LIVE** | Module-level canaries pass | No real-task demonstration | ADAPT-11: the acceptance IS real tasks escalating | Needs paths first |
|
||||
| FINAL-12 cross-track benchmark (#301) | **NOT STARTED** | — | — | Final epic gate | Explicitly last |
|
||||
| OBS-1 telemetry (#287) | **BUILT_AND_LIVE (pipeline)** | `stage_telemetry.py` | Research executor | Extend to operator path in ADAPT-LIVE | StageAttempt records verified in B3-v3 |
|
||||
| Artifact authority / retry (#298) | **BUILT_AND_LIVE** | `_resolve_worker_artifact` | Research executor | — | ADAPT-9 closed |
|
||||
| Task routing vs model routing separation | **BUILT_AND_LIVE** | Two authorities, distinct | Both live | Never collapse | Handoff §5 doctrine, honored in code |
|
||||
| Dead subnet (salience→budget→tier→escalate→verify→outcome) | **ABSORBED_BY_NEW_ARCHITECTURE / RETIRE candidates** | `src/rune/*.py` | None | Disposition review during PL1/Scaffolding | progressive_escalation duplicated by adaptive_router; outcome_ledger by task.outcome flow; verification_layer concepts live in `_verification_and_outcome` |
|
||||
| Builder bridge | **BUILT_AND_LIVE** | hook + CLI + `builder_bridge.py` | MiMo harness (this session) | Scaffolding owns deployment (D2) | Fire-trace proven |
|
||||
|
||||
### What fell off the roadmap during the detour (Part D input)
|
||||
|
||||
1. ADAPT-3/4/5 route execution paths (the epic's core; everything else waits on them).
|
||||
2. The §11 research-orchestrator boundary decision (MiMo session is the honest current answer; it predates the detour but was never dispositioned).
|
||||
3. Probe runtime policy (`MIMO_PROBE_ENABLED` ownership) — recorded, never decided.
|
||||
4. FINAL-12's dependency list silently grew (operator-path telemetry, builder-bridge overhead numbers) without issue updates.
|
||||
5. Epic #284 body still says "ADAPT-2 → {ADAPT-3,4,5}" with no record of the detour — the issue tracker and reality diverged until this artifact.
|
||||
|
||||
---
|
||||
|
||||
## PART D — NEXT RECOMMENDATION
|
||||
|
||||
**COMPLETE REMAINING ADAPT TRANCHE (one bounded tranche: "ADAPT-LIVE"), then PL1.**
|
||||
|
||||
Reasoning from recovered obligations, not momentum:
|
||||
|
||||
- PL1's input is route-accuracy and escalation-outcome evidence. Today routes change execution only at the RESEARCHED_CHANGE boundary. LOCAL_FIX and STANDARD_CHANGE — the two depths the measured baseline proved the fixed-depth pipeline *fails* — do not have distinct execution paths. Without them, PL1 would learn from a system that exercises one route out of three.
|
||||
- The escalation chain (local solve → verification failure → probe → research) is the epic's spine and is entirely canary-level today. ADAPT-11's acceptance ("real tasks demonstrate escalation") cannot be satisfied by more module tests.
|
||||
- The Evidence Probe needs one decision (who enables it at runtime) and one real demonstration — small, but it cannot be skipped because §3's progressive behavior (local → probe → research) is otherwise fictional at the probe step.
|
||||
- Nothing here justifies the intent-continuity spine yet: this artifact covers the need manually, and PL1 will automate it with better machinery.
|
||||
|
||||
**ADAPT-LIVE contents (smallest coherent adaptive loop):**
|
||||
1. LOCAL_FIX lane: routed task → implementation → real verification (`_verification_and_outcome`) → escalation hook wired on verification failure (completes the §7 chain end-to-end).
|
||||
2. STANDARD_CHANGE lane: DESIGN-LITE step per #293 (bounded, deterministic structure, no research dossier).
|
||||
3. Probe policy decision + one real probe through the production path (`MIMO_PROBE_ENABLED` set by the operator runtime, not tests).
|
||||
4. ADAPT-11 acceptance demonstration on 2–3 real tasks across the three depths, with telemetry.
|
||||
5. Update epic #284 with the detour record + this matrix; close the gap between tracker and reality.
|
||||
|
||||
Then: **PL1** (T9), with the intent-continuity spine as PL1's first or second deliverable (it is an experience-system product, not a separate architecture project).
|
||||
|
||||
## GITEA RECONCILIATION (2026-09-11)
|
||||
|
||||
Part A-J executed. 15 issues closed (HISTORICAL_EVIDENCE/SUPERSEDED/ACCEPTED_LIMITATION) with evidence. Bug label cleaned: 16 misleading labels removed (open bugs 24→8). #259/#258 updated to current architecture. Remote tracking fixed (master tracks gitea/master, not stale origin). PL1 metrics corrected (NOT_YET_MEASURABLE where evidence doesn't support numbers). D7: bump works, release job fails on Pi — ROOT_CAUSE_UNKNOWN_DEFERRED. Automation noise: 5 duplicate bump commits per logical change (queue-drain artifact, not live recursion — version converges, commit history doesn't).
|
||||
|
||||
## DEFECT D7 (2026-09-11): RESOLVED — release-job failures root-caused
|
||||
|
||||
History: the release job failed on every push since 03:16 (c2528c1).
|
||||
Superseded diagnosis: "Pi-side backup step logs Failed to connect to
|
||||
127.0.0.1:3000" was a red herring — the Pi backup step was never the
|
||||
failing job.
|
||||
|
||||
Root causes (live-proven T15 Part F, 2026-09-11 evening):
|
||||
1. c2528c1 left a corrupted shell fragment after the converged-check fi
|
||||
in auto-version.yml — the release job died at bash parse time (exit 2)
|
||||
before any bump. Fixed in c35b604.
|
||||
2. release_convergence() never assigned the release boundary — all 959+
|
||||
commits classified as unreleased, so the warranted bump never
|
||||
converged and queued runs each wrote their own bump commit. Fixed in
|
||||
e395ee1 (boundary wiring) + 9e0b855 (boundary grep must match subjects
|
||||
only, not bodies) + c35018f (numeric semver comparison: '0.66.9' <
|
||||
'0.66.10' is False lexicographically, which silently skipped warranted
|
||||
bumps as version_ahead_of_target).
|
||||
3. The per-file contents PUT committed one commit per version surface —
|
||||
five same-version bump commits per real bump (the "duplicate bump
|
||||
cascade" since 0.66.2, misdiagnosed as queue drain). Fixed in 470d82f
|
||||
+ d68fd1e (single ChangeFiles commit).
|
||||
|
||||
Live proof: experiment A (trigger d68fd1e, warranted patch) → exactly one
|
||||
bump commit a4afc77 (0.66.11), all five surfaces in one commit, queue
|
||||
drained, zero delayed duplicates, zero workflow failures.
|
||||
|
||||
## Local CI tooling (developer-side, NOT Rune runtime): W/R-v0.3 reactive wake (2026-09-12)
|
||||
|
||||
The desktop gitea-runner (v3.3.0) now runs `host.post_task_script` = `/usr/local/bin/gitea-wr-wake.sh`
|
||||
after every job: a sub-second fire-and-forget POST to the local W/R daemon (`127.0.0.1:8791/wake`),
|
||||
which sweeps waiting external waits through AUTHORITATIVE Gitea verification (bounded retry for the
|
||||
reporter-ack window; 60s reconciler demoted to fallback). Runner event ≠ success proof. Config:
|
||||
`/etc/gitea-desktop-runner/config.yaml` (backup: config.yaml.pre-wr03). Measured: terminal→verified
|
||||
wait transition ~2s (was 0-30s polling). INTERACTIVE_AUTO_RESUME remains NOT YET SAFE (no supported
|
||||
MiMo busy/idle signal) — headless-only dispatch, unchanged.
|
||||
@@ -1,290 +0,0 @@
|
||||
# Scaffolding V1 — Tranche 1
|
||||
|
||||
_2026-09-12. Scaffolding is the controlled need-to-capability system: NEED →
|
||||
DISCOVERY → GAP → REUSE/ACQUIRE/BUILD → IMPACT+PLAN → SAFE IMPLEMENTATION →
|
||||
VERIFICATION → PROVISIONAL USE → TRUST/REVISE/RETIRE → REUSABLE CAPABILITY.
|
||||
Tranche 1 is deliberately narrower: make Rune stop rediscovering how Rune is
|
||||
built every time Rune changes._
|
||||
|
||||
## Tranche 1 contract
|
||||
|
||||
`scripts/scaffold_projection.py` — a PROJECTION over existing authorities,
|
||||
never a new one:
|
||||
|
||||
| question | authority projected |
|
||||
|---|---|
|
||||
| what subsystem owns X? | `config/umbrellas.json` (umbrella manifest + henxel-enforced completeness) |
|
||||
| what does X depend on? | AST import graph over `src/rune/*.py` (incl. relative/lazy imports) |
|
||||
| who consumes X? | reverse import edges |
|
||||
| what changes if X changes? | mirrors + `depends_on` umbrellas + consumers |
|
||||
| what proof obligations arise? | unit-test file, canary registry, umbrella gates, suite/lint/henxels |
|
||||
| which are satisfied? | filesystem + registry evidence (static); suite/ratchet/henxels are runtime-check commands |
|
||||
|
||||
Commands: `component <module>` · `impact <module>` · `obligations <module>` ·
|
||||
`all [outfile]`. If the projection disagrees with an authority, fix the
|
||||
authority — never the projection.
|
||||
|
||||
## Mechanism audit (2026-09-12, deployed 0.66.22)
|
||||
|
||||
| mechanism | verdict | basis |
|
||||
|---|---|---|
|
||||
| Umbrella manifest + impact map + propagation gate | KEEP | ownership/impact authority; henxel-enforced completeness; consumed by projection |
|
||||
| Henxels structural contract | KEEP | cross-cutting obligations (silent-handler ratchet, registries, coverage debt) |
|
||||
| Canary registry (`canary_native.MODULES` + per-track canaries) | KEEP | per-subsystem proof obligations; consumed by projection |
|
||||
| CI/deploy contract (required checks, TUI startup canary, atomic promote) | KEEP | deployment proof chain live-proven repeatedly |
|
||||
| Version surfaces + auto-version + semver | KEEP | bump fast lane + push convergence working (0.66.22 earned this session) |
|
||||
| Lint/pyright/coverage ratchets | KEEP | shrinking-baseline gates, all green |
|
||||
| Authority map + dead-subnet ledger (D6) | EVOLVE | static doc today; Scaffolding projections make it derivable; D6 retire-disposition still owed |
|
||||
| Issue reconciliation artifact + checker | KEEP | tracker truth with drift detection |
|
||||
| PL1 experience ledger | KEEP | the trust/revise/retire input for future capability crystallisation |
|
||||
| Intent continuity ledger | KEEP | PART 6 state machine, 7 invariants |
|
||||
| `MIMO_AUTOMATIC_RESUME` / W/R interactive injection | KEEP (bounded) | headless single-flight proven; interactive auto-resume NOT YET SAFE until a supported busy signal exists |
|
||||
| Duplicate authorities risk | REPLACE (avoided) | Scaffolding v1 adds NO new registry — projections only |
|
||||
|
||||
## Real-change proof (this tranche)
|
||||
|
||||
Change: context discovery hit Ollama's `/api/show` with cloud-pinned model
|
||||
names — `400 invalid model name` noise on every cloud task + 8192 misbudget
|
||||
for a 131072-window model (observed live in every PL1 Task A/B run).
|
||||
|
||||
1. BEFORE mutation: `scaffold_projection.py impact local_tool_runtime`
|
||||
derived — subsystem `capability`; consumers `rune_operator`; gates:
|
||||
full suite, lint ratchet, henxels, inference+orchestration canaries;
|
||||
unit-test obligation `tests/test_local_tool_runtime.py` (exists).
|
||||
2. Change made: `is_cloud_pinned` — THE provider-prefix rule, single
|
||||
authority in `openai_compat_transport.py`; consumed by context discovery
|
||||
(new) and transport selection (deduped); red-first regression tests.
|
||||
3. Obligations tracked: suite 1314 green · lint ratchet PASS · henxels exit 0
|
||||
· the projection itself caught two gaps mid-flight (relative imports
|
||||
hidden consumers; a broad except biting the lint ratchet) — both fixed at
|
||||
the source the projection pointed to.
|
||||
4. Deploy: normal release chain → deployed identity verified.
|
||||
|
||||
## Maturity (honest)
|
||||
|
||||
- Projection engine: **UNIT-PROVEN** + proven on one real change.
|
||||
- Not yet: CI integration of `scaffold_projection.py all` (drift detection),
|
||||
capability discovery / gap recognition / reuse-before-build / external
|
||||
acquisition / provisional-trust lifecycle — FOUNDATIONAL ONLY (architecture
|
||||
does not block them; the projection source-list is the extension surface).
|
||||
- Deferred with reasons recorded: none blocked by Tranche 1.
|
||||
|
||||
---
|
||||
|
||||
# Scaffolding V1 — Tranche 2: Construction Substrate (2026-09-13)
|
||||
|
||||
_Tranche 2 (contract SCAFFOLD-TRANCHE-001) built the construction substrate
|
||||
itself: a shared structural-store primitive, the Project Scaffold layer over
|
||||
it, and durable Construction Contracts — Rune's first build tracked by the
|
||||
system it builds._
|
||||
|
||||
## Architecture
|
||||
|
||||
src/rune/structural_store.py shared generic primitive — NO Rune constants
|
||||
↓ ↓
|
||||
src/rune/spine.py src/rune/scaffold.py
|
||||
(Rune Spine — canonical (Project Scaffold — canonical
|
||||
structural self-model) construction/structural truth
|
||||
about EXTERNAL projects)
|
||||
|
||||
Authority map (no duplicates):
|
||||
|
||||
| fact | authority | others |
|
||||
|---|---|---|
|
||||
| Rune's structure | `spine.py` + `store/spine/current.json` | scaffold only references |
|
||||
| external project structure | `scaffold.py` store | Spine holds bridge facts only |
|
||||
| content history / rollback | Git | both reference commits |
|
||||
| coordination (issues/PRs/CI) | Gitea | scaffold stores observations |
|
||||
| why work exists | Task/Intent | contracts reference task ids |
|
||||
|
||||
Construction Contract = ONE durable object (`contract:<id>`, kind
|
||||
`construction_contract`) with plain `phase`/`status` fields governed by a
|
||||
small legal-transition map (draft → approved → building → verifying →
|
||||
migrating → superseding → closed; failed/abandoned terminal-or-retry). No
|
||||
workflow engine. Resumability = store durability, proven across real
|
||||
process boundaries.
|
||||
|
||||
## Mechanics
|
||||
|
||||
- **Repo binding** (`bind_repo`): forge/owner/name/branch/root/placement as
|
||||
scaffold DATA. No secrets — schema rejects secret-named fields AND every
|
||||
`save()` recursively redacts credential-named keys (defense in depth in
|
||||
the shared core, so Spine inherits it too).
|
||||
- **Scope check** (`check_scope`): changed files vs approved scope paths →
|
||||
green / red / **unknown** (an unscoped contract is never silently green).
|
||||
Out-of-scope change = RED until explicitly amended; amendments recorded,
|
||||
original RED preserved in history.
|
||||
- **Predicted vs observed**: frontier snapshot at approval; observed impact
|
||||
captured at closure from INDEPENDENT evidence (git, tests) — never from
|
||||
re-walking the prediction's own graph. Missed entities become CANDIDATE
|
||||
corrections awaiting evidence.
|
||||
- **Drift**: authority-owned facts that fail re-observation go STALE —
|
||||
never silently fresh, never force-reverted. Reality wins.
|
||||
- **Bridge** (`spine_bridge_entity`): identity + repo surface only. Project
|
||||
internals NEVER leak into Rune Spine (test-enforced). Contract evidence
|
||||
may cite `spine://` URIs — citations are inert on the referenced
|
||||
authority.
|
||||
|
||||
## Real proof (this tranche on itself)
|
||||
|
||||
SCAFFOLD-TRANCHE-001 migrated from its pre-scaffold JSON into the new
|
||||
store, advanced through phases with evidence checkpoints, closed with
|
||||
predicted-vs-observed from git/test evidence, scope check caught the
|
||||
store's own runtime artifacts (RED → explicit amendment → green, history
|
||||
kept), and construction provenance attached into Rune Spine without
|
||||
duplicating authority. A fresh interpreter answers what/why/state/
|
||||
evidence/changed/next from the store alone — no transcripts.
|
||||
|
||||
## Maturity (honest)
|
||||
|
||||
| component | state |
|
||||
|---|---|
|
||||
| structural_store core | EXERCISED (unit-proven + real use) |
|
||||
| Rune Spine after refactor | LIVE_PROVEN behavior-neutral (68 tests + identical rebuild) |
|
||||
| Project Scaffold | EXERCISED |
|
||||
| Construction Contract | EXERCISED (resumability proven cross-process) |
|
||||
| repo binding (identity model) | UNIT-PROVEN (local git fixture) |
|
||||
| external-repo live proof | BLOCKED ON USER REPO SELECTION |
|
||||
| Scaffolding overall | substrate only — capability-fit discovery, gap decisions, toolchain selection, patterns, maintenance TUI all NOT BUILT (data seams exist) |
|
||||
|
||||
---
|
||||
|
||||
# Scaffolding Tranche 4 — Live Need → Decision → Construction Contract (2026-09-13)
|
||||
|
||||
_The adoption tranche: a real Need now flows automatically through the
|
||||
Tranche 2/3 mechanisms and produces a governed Construction Contract. The
|
||||
model no longer has to remember the procedure — it is durable Scaffolding._
|
||||
|
||||
## Live flow
|
||||
|
||||
NEED (reference to Task/Intent — pointer, never a copy)
|
||||
↓ normalization (gaps recorded, never invented)
|
||||
↓ structural discovery + fit + resolution (Spine + Project Scaffold)
|
||||
↓ toolchain decision (existing project toolchain wins)
|
||||
↓ layout obligations (USER-LOCKED authority inherited)
|
||||
↓ user-approval gate (material changes require approval; approved
|
||||
bounded work never re-asks)
|
||||
↓ DecisionTrace (durable, model-independent)
|
||||
↓ generated Construction Contract → existing build/verify pipeline
|
||||
|
||||
## Authority map (unchanged — the path CONNECTS, never duplicates)
|
||||
|
||||
| authority | owns |
|
||||
|---|---|
|
||||
| Task/Intent | why work exists |
|
||||
| Rune Spine | Rune structural truth |
|
||||
| Project Scaffold | external-project structural/construction truth |
|
||||
| Layout Contract | desired interface constraints (USER-LOCKED = user authority) |
|
||||
| Construction Contract | governed build obligations/history |
|
||||
| Git / Gitea | history / execution |
|
||||
|
||||
## Boundary honesty
|
||||
|
||||
- **ADAPT/research**: the live path records a research ROUTE
|
||||
(TARGETED_EVIDENCE_PROBE / FULL_RESEARCH_PIPELINE) when evidence is
|
||||
insufficient — a seam, NOT an executed pipeline call. No fake
|
||||
integration claimed.
|
||||
- **BUILD_NEW** is never a fallback: insufficient evidence blocks it;
|
||||
normalization gaps block it; merge precedence across authorities
|
||||
requires every consulted authority to agree.
|
||||
- **No second task engine**: one decision function + one trace object.
|
||||
|
||||
## Contract-quality repair (Tranche 3 defects)
|
||||
|
||||
Closure now distinguishes:
|
||||
|
||||
predicted_existing_affected planned changes to existing entities
|
||||
expected_new entities the build PLANNED to create
|
||||
correctly_predicted predicted AND independently observed
|
||||
predicted_but_unaffected predicted, NOT observed
|
||||
expected_new_observed planned creation happened
|
||||
expected_new_missing planned creation did NOT happen
|
||||
genuinely_unexpected_new observed, planned by neither list
|
||||
|
||||
Prediction cannot self-certify observation: the observed set comes ONLY
|
||||
from caller evidence (git/tests/build/deploy). Legacy keys (missed/
|
||||
needless/observed) preserved — old contracts readable without migration.
|
||||
|
||||
## Layout-obligation inheritance
|
||||
|
||||
A change touching a UI component/region/implementation inherits its Layout
|
||||
Contract as a build obligation. USER-LOCKED obligations state that changing
|
||||
placement requires explicit user authorization. Unrelated backend changes
|
||||
inherit nothing. USER-LOCKED drift is never reconciled by editing the
|
||||
contract — code adapts.
|
||||
|
||||
## Maturity (honest)
|
||||
|
||||
- Need intake: EXERCISED (real live proof)
|
||||
- decision orchestration: LIVE-PROVEN for the bounded live flow
|
||||
- capability discovery adoption: EXERCISED
|
||||
- fit/gap adoption: EXERCISED
|
||||
- toolchain adoption: EXERCISED
|
||||
- contract generation: LIVE-PROVEN (one real generated contract)
|
||||
- contract-quality prediction: EXERCISED (repaired semantics, one live closure)
|
||||
- layout-obligation inheritance: UNIT-PROVEN (fixtures) + real TUI binding
|
||||
- Scaffolding overall: adoption path live for one bounded flow — NOT
|
||||
general autonomous construction, NOT universal discovery/toolchain
|
||||
intelligence, NOT Scaffolding complete.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Scaffolding Tranche 9 — Release History + Evidence Correction (2026-09-13)
|
||||
|
||||
_Gitea Releases are now an evidence-derived human-readable history of
|
||||
Rune's evolution. Plus the Tranche 8 builder-evidence correction._
|
||||
|
||||
## Release semantics (audited)
|
||||
|
||||
| surface | meaning |
|
||||
|---|---|
|
||||
| VERSION | semver in pyproject/init (e.g. 0.66.44) |
|
||||
| TAG | v<VERSION>, created by auto-version at the BUMP commit |
|
||||
| release commit | the auto-bump SHA — NOT necessarily final state |
|
||||
| converged deployed identity | last commit deployed under the same VERSION |
|
||||
|
||||
Same-version convergence commits (closure/accounting/lock fixes landing and
|
||||
deploying after the bump) are normal; the notes record BOTH the tag commit
|
||||
and the final deployed identity, distinction exposed honestly.
|
||||
|
||||
## Mechanism
|
||||
|
||||
`release_notes.py`: deterministic evidence extraction from governed records
|
||||
(closed contracts, resolved maintenance, contract-quality records) →
|
||||
structured ReleaseNote → Markdown → Gitea API (POST create / PATCH update).
|
||||
Idempotent: publishing twice for a tag UPDATES, never duplicates. Notes are
|
||||
historical documentation — NOT Memory, NOT structural authority.
|
||||
|
||||
## T8 evidence correction (§1)
|
||||
|
||||
The T8 report claimed a close-from-verifying refusal + reactive superseding
|
||||
transition. Durable checkpoints show NO superseding transition and the
|
||||
phase map permits verifying→closed; live output showed first-attempt close
|
||||
success. The claim was RETRACTED with full provenance (original claim,
|
||||
correction, reason, evidence preserved in the contract record) and
|
||||
recorded as process-defect:unsupported-causal-claim. MiMo adoption
|
||||
maturity survives on the SUPPORTED evidence: scope constrained file edits,
|
||||
pattern obligations forced preservation proof, interface-reported
|
||||
observations, fresh resume.
|
||||
|
||||
## Maturity
|
||||
|
||||
Release-note generator: LIVE-PROVEN (real Gitea, 10 releases). Backfill:
|
||||
0.66.35→0.66.44 carrying historical-backfill provenance. Pre-0.66.35
|
||||
history: skipped (weak evidence — honest limitation).
|
||||
|
||||
## Contract discovery → spine (2026-10-05, #2465 CI correction)
|
||||
|
||||
The closed construction contract is the authority for discoveries. Spine
|
||||
rebuild reads its observed entities and candidate corrections; it does not
|
||||
claim a correction has been built. Qualified IDs such as
|
||||
`rust-crate:cognition_core` keep their declared kind. Bare file paths,
|
||||
test suites and other unqualified references become
|
||||
`construction-surface:<reference>`, never `module`. An observed
|
||||
surface may be PRESENT; a candidate stays DECLARED until independently
|
||||
observed. Existing bad records are migrated with their evidence and edge
|
||||
references intact. This matters: the first write-back mapped bare paths to
|
||||
modules, made 22 false module records and crashed CI on the next rebuild.
|
||||
The per-contract quick-reference markdown is generated from the scaffold
|
||||
store at close; it is a view, not another authority.
|
||||
@@ -1,79 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,153 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,73 +0,0 @@
|
||||
# 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`.
|
||||
@@ -1,54 +0,0 @@
|
||||
# Adversarial review procedure (#151 — structuralized)
|
||||
|
||||
_Binding for every P0/P1 change and every bug-fix discharge. The doctrine and
|
||||
evidence live on the issue (mutation-testing formalization, Google's reviewer
|
||||
checklist, Hypothesis property tests); this document is the operating
|
||||
procedure. Enforcement: dispatch templates (`docs/reviewer-dispatch-template.md`)
|
||||
carry the mandatory steps; the bug-fix contract (`docs/bugfix-contract.md`)
|
||||
carries the discharge-side half._
|
||||
|
||||
## The problem this kills
|
||||
|
||||
LLM-authored changes share wrong assumptions between implementation, tests,
|
||||
and docs. An implementer that writes its own tests tests its own
|
||||
interpretation. A reviewer that reads the implementation first anchors on it.
|
||||
Cosmic Ray's formulation: *if a test suite passes on mutated code, you have a
|
||||
mismatch between your tests and your functionality.*
|
||||
|
||||
## The procedure (in order, no steps skipped)
|
||||
|
||||
1. **Freeze intent.** Issue acceptance criteria + synthesis kill test, written
|
||||
down before any test exists.
|
||||
2. **Black-box test author** (fresh-context agent, P0/P1 required): writes
|
||||
behavioral tests from the issue + synthesis + plan ONLY — never sees the
|
||||
implementation. These tests are the spec.
|
||||
3. **Enumerate plausible incorrect implementations.** Before inspecting any
|
||||
implementation-specific test, list the wrong implementations the frozen
|
||||
tests would NOT catch. This list goes in the plan.
|
||||
4. **Only then** inspect implementation-specific tests; reconcile.
|
||||
5. **Run both suites.** Black-box + implementer suites must both pass.
|
||||
6. **Mandatory reviewer question** (spec gate, verbatim): *"What incorrect
|
||||
implementation could still pass these tests?"* A review that cannot answer
|
||||
is incomplete and bounces.
|
||||
7. **Mutation spot-check** (proportionate): for pure logic changed by P0/P1
|
||||
PRs, one manual mutant per load-bearing branch (flip a comparison, swap an
|
||||
operand) — if the frozen tests still pass, the tests are vacuous and the
|
||||
finding is BLOCKER-grade. (Full mutation tooling lands with #130's
|
||||
calibration harness; spot-checks until then.)
|
||||
|
||||
## Role separation (hard rules)
|
||||
|
||||
- The **implementer never writes the spec tests** for P0/P1 changes — the
|
||||
black-box author does.
|
||||
- The **spec gate never sees the implementation before answering** the
|
||||
mandatory question.
|
||||
- The **black-box author's tests are frozen** — implementer or reviewer may
|
||||
extend, never weaken (bug-fix contract §4).
|
||||
|
||||
## Proportionality
|
||||
|
||||
- P0/P1 features and all BLOCKER/MAJOR discharges: full procedure.
|
||||
- MINORs and mechanical fixes: steps 3+6 (enumerate + mandatory question),
|
||||
spot-checks at reviewer discretion.
|
||||
- Workflow/config changes: the kill test is the protected CI run itself,
|
||||
stated explicitly in the synthesis (contract §kill-test).
|
||||
@@ -1,266 +0,0 @@
|
||||
# Boundary Law — Rust↔Go crossings (D30, standing architecture decision)
|
||||
|
||||
**Decision (owner, 2026-10-16):** WASM via wazero is the STANDARD mechanism for Rust↔Go crossings. cgo is exception-only.
|
||||
|
||||
## The law
|
||||
- Rust crosses to Go as WASM (wazero, AOT, github.com/tetratelabs/wazero): traps surface as catchable Go errors — never process death; debug errors designed for the owner (plain-language, no hex soup).
|
||||
- The five conversational Go subsystems (task-state, event-bus, model-routing, tool-executor, work-manager) are in-process libraries — ns-scale calls, no crossing.
|
||||
- Crossings to Rust are batched at the boundary; semantic events never drop; decoration yields.
|
||||
- cgo EXCEPTION-ONLY, all conditions must hold and be re-proven on every dependency change: pure logic, no unsafe, no external C, frozen battery proving panic-freedom on real inputs, catch_unwind at the boundary, operation idempotent, host death affordable.
|
||||
- The boundary pipe remains in service until the WASM prototype proves in-process end-to-end.
|
||||
|
||||
## Measured basis (research rounds + trials, all in-repo)
|
||||
- Batched IPC 0.0078ms/item beats cgo-with-payloads 0.0117ms/item (cgo's edge: ~0.33ms total on rare single calls).
|
||||
- wazero matches IPC at batch-1000 (11.5µs vs 11.5µs), wins 2–8× below; trap isolation verified; artifact: verification/wazero-vs-ipc-trial.json.
|
||||
- Goroutine recover = panic containment ONLY (Go docs; hangs/corruption/fatal kill the process). Process restart ≈2ms (measured, 300 runs).
|
||||
- Shared memory LOSES to plain sockets at our message sizes (7.7µs vs 5.5µs at 64B — ByteDance shmipc bench); only wins ≥4KB.
|
||||
- wasmer-go abandoned 2021; wasmtime-go (cgo) is the component-model escape hatch — noted, no work.
|
||||
- Precedents: Linkerd2 (Go+Rust, process-separated), ncruces/go-sqlite3 (C→WASM→wazero in production), Extism (Rust plugins on wazero).
|
||||
|
||||
## Provenance
|
||||
Owner directive 2026-10-16 after three research rounds (spectrum + nets + measurements) and two in-repo trials. OpenSpec change: add-native-rune-tui design.md D30. Related interview rulings: R2-D8 (first connector), SA-D6 (sockets), TD-1..4 (task tree + selection).
|
||||
|
||||
## Prototype verdict (2026-10-16, wave complete)
|
||||
token_counting end-to-end: **YIELDS.** 24/24 frozen-oracle parity through wasm; 1.6–2.2× slower than cgo raw but ~160× faster than IPC; trap isolation verified with symbolic stack + file:line capture; zero porting cost; ABI reserved-scratch convention matters 23× (standardized). wazero cost: +6.9MB binary, +12.4MB vendor. Production migration wave 1 (token_counting) opened under the standing directive.
|
||||
|
||||
## Wave 1 in production (2026-10-17)
|
||||
token_counting serves in-process from the TUI through `tui/wasm` (wazero AOT, reserved-scratch ABI as the standard, trap → Go error → fresh instance). Frozen tests: 24/24 parity against the crate's own oracle through the production path, envelope byte-identity wasm↔native-binary, isolation restart battery, IPC fallback degradation (Status "ipc-fallback", identical results).
|
||||
|
||||
**Module artifacts are NOT committable** (decided): a .wasm module is a reproducibly-built compiled artifact — `scripts/build_paired.sh` compiles it from src/rust source (`cargo build --release --target wasm32-wasip1 --lib`, byte-reproducible: identical sha256 across clean rebuilds) and stamps it into `tui/wasm/modules/` with a manifest carrying the paired build id. The dir is gitignored under the same no-committed-binaries law as the Go/Rust binaries and build-id stamps. Source of truth stays src/rust/token_counting.
|
||||
|
||||
## Standing exception record
|
||||
Rune TUI founder (2026-10-16) uses JETBRAINS IDEs with the Cmd/Ctrl+Shift+A action bar (Search Everywhere / Find Action) — muscle memory asset. TUI selection law (D29/TD-4) was designed around this: shift+tab cycling mirrors the IDE action-bar rhythm; click+highlight = the same interaction in terminal-native form. Future TUI interaction design should treat JetBrains muscle memory as a first-class constraint.
|
||||
|
||||
## Standing exception record
|
||||
Rune TUI founder (2026-10-16) uses JETBRAINS IDEs with the Cmd/Ctrl+Shift+A action bar (Search Everywhere / Find Action) — muscle memory asset. TUI selection law (D29/TD-4) was designed around this: shift+tab cycling mirrors the IDE action-bar rhythm; click+highlight = the same interaction in terminal-native form. Future TUI interaction design should treat JetBrains muscle memory as a first-class constraint.
|
||||
|
||||
## The Three-Layer Adaptability Doctrine (owner aspiration, 2026-10-17 — D32)
|
||||
Rune grows cognitively on the space and resources given to her. Three layers of self-adaptation, ordered by safety and speed:
|
||||
|
||||
1. **Data adaptation (NOW — already built):** memory, skills, config, extensions, task knowledge — all mutable at runtime, no rebuild, no restart. This is where most of Rune's daily growth lives.
|
||||
|
||||
2. **Logic adaptation (NEAR FUTURE — D30 enables it):** WASM hot-swappable reasoning modules. Rune's decision-making logic — routing, mode gates, pattern matchers, the parts she'd tune most — as WASM modules loaded/unloaded/replaced at runtime. She adapts her THINKING without rebuilding her BODY. The cognition socket (SA-D5, reserved in the seam registry) is the doorway. Safe because: WASM isolation (traps = errors, not crashes), D01-D10 self-transformation gates, verification before every swap.
|
||||
|
||||
3. **Structure adaptation (ALWAYS GATED):** compiled Go/Rust code changes through the greenhouse-swap — build in isolation, prove via parity, swap with restore point. Foundation changes deserve the most care.
|
||||
|
||||
**The self-bootstrap aspiration:** Rune eventually grows her own cognitive capabilities on the resources available to her — the cognition socket opens, WASM reasoning modules become the substrate for learned judgment, and the compiled core becomes the stable body she doesn't need to rebuild to grow. The greenhouse is for the house. The garden is for the mind.
|
||||
|
||||
## The Go-Operator / WASM-Mind Architecture (owner directive 2026-10-17 — D33)
|
||||
Go is the main operator/runner/runtime (the body): all I/O, process management, scheduling, TUI rendering, concurrency — chosen for its responsiveness and reactive adaptability. WASM modules are the decision logic (the mind): all routing decisions, mode gates, pattern matching, policy enforcement, resource allocation — hot-swappable, isolated, verified before loading. Every decision the system makes runs through a WASM module; Go decides HOW to execute, WASM decides WHAT to do. Go keeps native I/O and process machinery (network, filesystem, terminal rendering); every decision about that I/O is WASM.
|
||||
|
||||
Division:
|
||||
- Go (compiled, stable body): TUI, I/O, scheduling, process management, connections, the WASM host runtime
|
||||
- WASM (hot-swappable mind): routing, mode gates, pattern matching, policy, mediation, resource allocation, [future] cognitive reasoning (SA-D5)
|
||||
- Data (mutable garden): memory, skills, config, extensions, task knowledge
|
||||
|
||||
This makes D32 layer 2 concrete: the decision logic layer IS the WASM module layer. Rune adapts her thinking by swapping WASM modules — verified, isolated, no body rebuild needed.
|
||||
|
||||
## API-KEY SECURITY LAW (owner directive 2026-10-17 — absolute, no exceptions)
|
||||
Provider API keys, connection credentials, and any secret material NEVER enter the Gitea repository — not in config files, not in code, not in fixtures, not in test data, not in commit messages. They are LOCAL-ONLY. The Gitea repo is public-downloadable; anything pushed there is compromised.
|
||||
|
||||
Storage law:
|
||||
- Keys live in encrypted/OS-secure storage (~/.rune/secrets/, OS keyring, or encrypted file with owner passphrase) — never in the repo
|
||||
- The provider registry stores: provider name, endpoint URL, enabled/disabled — NO keys, NO credentials
|
||||
- Key references are by-ID (the runtime resolves them from secure storage at call time)
|
||||
- .gitignore blocks any file that could contain keys (provider_secrets.json, .env, credentials.*, etc.)
|
||||
- The API-key migration wave (from mimo) writes ONLY to local secure storage — never to any tracked file
|
||||
- Test fixtures use synthetic keys ("test-key-fake") — real keys never touch test data
|
||||
|
||||
This law binds EVERY wave. The research gate, the landing gate, and the no-committed-binaries gate all enforce it. A wave that pushes key material to Gitea is a critical security incident.
|
||||
|
||||
## D34 — CHAT SURFACE RULINGS (owner, 2026-10-18, granular chat interview round 1)
|
||||
1. CHAT BOX: the input is a stable BOX at the bottom of the chat pane; chat history scrolls above it. Never a thin transient line.
|
||||
2. HISTORY AT BOOT: the last conversation restores into the transcript at boot (scrollback reloaded, newest visible).
|
||||
3. WAITING FEEDBACK: an animated indicator at the bottom (identity line area) WHILE thinking; a thinking animation in the chat itself; the thinking bubble CONTAINS the actual thoughts — expandable (click/ctrl+t) to read them.
|
||||
4. COMMANDS: both roads — /commands at the input line (fast shorthand) AND the palette (ctrl+p, searchable full list). One vocabulary, two entries.
|
||||
Visual STYLING of all four is deferred to the owner's visual round — structure and behavior are ruled here.
|
||||
|
||||
## D35 — CHAT PRESENTATION RULINGS (owner, 2026-10-18, interview round 2)
|
||||
1. TRANSCRIPT SHAPE: a mobile text-message chain — you explicitly SEE who is who (sender-obvious bubbles, you vs rune visually distinct, left/right). Not a terminal log.
|
||||
2. STYLING IS ITERATIVE: UI styling goes through LIVE TRIAL-AND-ERROR ROUNDS — the owner flips through style variants in the running TUI and critiques; nothing visual is locked by a single choice. Build style-switching, not a single look.
|
||||
3. KEYS: Enter sends; shift+enter inserts a line break.
|
||||
4. INPUT HISTORY: up/down arrow recall, in-progress text saved while browsing, dedup, persisted across boots (old-TUI HistoryInput behavior — the DEFAULT ORACLE).
|
||||
5. TIMESTAMPS: dim timestamp beside each message.
|
||||
6. THINKING MARKER: collapsed by default; the expandable control is a CLICKABLE ICON styled as a purple RUNIC GLYPH — lean into runic symbols for Rune's identity (the owner: "i imagine rune being like a purple rune").
|
||||
7. AUTOCOMPLETE: popup while typing /commands (old-TUI AutoComplete behavior — adopt).
|
||||
The old Textual TUI (src/rune/rune_tui.py) is the DEFAULT ORACLE for chat behavior: anything it did, the Go face does unless a ruling here deviates.
|
||||
|
||||
## D36 — THE CHAT/WORK DICHOTOMY (owner, 2026-10-18, interview round 3 — the face's structural law)
|
||||
1. ONE shared input box UNDER BOTH panes (not inside either). FOCUS ROUTES THE DESTINATION: chat pane focused → the text is CONVERSATION (talk with the model, and Rune herself when she's capable); tasks pane focused → the text is an IMMEDIATE WORK DEMAND (enter starts the task, queues a turn).
|
||||
2. THE CONSENT BRIDGE: the chat pane has contextual understanding — when a conversational message is actually a work request, Rune PROPOSES pushing it from chat to task (consent required); agreed → the task starts instantly and queues a turn.
|
||||
3. HIGHLIGHT LAW (chat scrollback): color-coded by category — red = errors, green = results, code/files = regular greyed-out but EXPANDABLE with a runic-symbol icon; Rune also marks importance per-message.
|
||||
4. TASKS PANE = THE WORK STREAM: not just an outstanding-tasks list — task entry (via the shared box in task focus), live work blocks (thinking + current command/file being coded + progress), collapse-to-summary on completion. The CHAT pane stays clean conversation.
|
||||
5. PREDICTION: inline ghost text (dim continuation suggestion, tab/→ accepts, mimocode-style), sourced from local Ollama.
|
||||
Interview-granularity law (owner, same round): spec interviews walk every element × state × interaction in ordered small batches with rendered examples — no assumption gaps.
|
||||
|
||||
## D37 — MODEL POLICY & THE ORCHESTRATOR LAYER (owner, 2026-10-18)
|
||||
1. NO hardcoded model anywhere. A SETUP WIZARD (first-run) has the user specify their model — that model is the DEFAULT FOR BOTH panes (chat + task).
|
||||
2. The user controls concurrency: either extra concurrency keeping panes separate, or splitting one concurrent connection — a wizard setting.
|
||||
3. OPTIONAL SPLIT: two DIFFERENT models, one per pane, any combination of local/cloud — switchable after setup via a command.
|
||||
4. RUNE THE ORCHESTRATOR: her intelligence OWNS all models in play, thinks independently from them, and interprets how best to route context across every model in play. The models are workers; Rune is the mind above them.
|
||||
Chat-busy: a second chat message while answering QUEUES VISIBLY. Task queue: FULL queue visible in the pane.
|
||||
|
||||
## D38 — THE TASK MARKDOWN LEDGER (owner, 2026-10-18)
|
||||
Every task initiation creates a MARKDOWN FILE — resumable, recorded, with time and date; it is THE SOURCE OF TRUTH for open tasks. Completion: the task is crossed off in the file, then the file RETIRES to a "done tasks" folder with a date subfolder. A new day creates a new dated folder ONLY if tasks were completed that day; no completed tasks = no folder. Plain files over databases: human-readable, durable, portable.
|
||||
The checkpoint/save mechanics decision is DEFERRED to a dedicated research round + owner diagnosis (owner asked for full explanation + a multitude of approaches).
|
||||
|
||||
## D39 — THE CORE MOVES TO GO (owner, 2026-10-18: "lets keep the intention of what was meant to be finished in python and just move to go and fix the go version properly")
|
||||
1. The Python core (operator loop, conversation state, model routing, tool runtime on :8376) is TRANSITIONAL — its INTENTIONS (behaviors, capabilities, design goals) port to GO; the Go implementations are built PROPERLY from day one (crash-safe saves, small locks, channel-keyed context — never the Python defects).
|
||||
2. The Python checkpoint fix is SUPERSEDED: no over-investment in Python save mechanics — the storage design lands in the Go core (the checkpoint research now feeds the GO design choice).
|
||||
3. The two-stream architecture (D36 separate turns, D37 model policy) lands IN GO directly — per-channel single-flight, channel-keyed context assembly, the orchestrator layer above the models.
|
||||
4. Retirement law unchanged: Python stays as the parity oracle during transition; each capability's Go replacement proves parity, then the Python deletes (docs/retirement-map.md).
|
||||
|
||||
## D40 — GO CORE STORAGE SHAPE (owner, 2026-10-18: "can we combine 1 and 3?" — yes)
|
||||
Journal + snapshots AND SQLite, each doing what it's best at:
|
||||
1. STREAM MESSAGES: per-channel append-only JSONL journals + periodic atomic snapshots (the K25 pattern generalized) — human-readable, each stream owns its own files, chat and task physically never contend.
|
||||
2. INDEXED STATE BACKBONE: SQLite via modernc.org/sqlite (pure Go, no cgo — D30-legal; ALREADY vendored via memory-ops) for tasks/state queries, the transition spine, concurrent reads from both streams + the TUI. PIN ≥3.51.3 (the WAL-reset bug was fixed there — checkpoint-saves research finding).
|
||||
3. RIDES ALONG: finished tasks retire to the D38 markdown done-folders (the dead 96% of the checkpoint walks out of the hot path); two-writer safety from day one; crash-anywhere atomic writes everywhere (no open-truncate-write anywhere, ever).
|
||||
|
||||
## D41 — THE VISUAL AMALGAMATION (owner, 2026-10-18, after viewing all three renderings)
|
||||
Base: V-A "Set Line" cleanness. Corrections: every render must show the CHAT BOX AREA (the input box) — the first three omitted it; rerenders required.
|
||||
FROM V-A: clean typeset; colour highlighting; the mode token + HOW LONG THINGS TAKE (durations); ctrl+t expandable thinking.
|
||||
FROM V-B: Runic symbols + ANIMATIONS on rune thinking — the animation cycles RANDOM OLD CELTIC/VIKING RUNIC SYMBOLS MIXED WITH CTHULIAN SYMBOLS; the PINK OUTLINE; chat between owner and rune MORE BUBBLE-LIKE and ANIMATED; Rune's icon = a PURPLE DIAMOND rendered SMALL beside rune's thinking (the first render was too huge); the CONFIRM MESSAGE style.
|
||||
FROM V-C: percentages + animated progress bars for tasks — as WINDOWS TO CALL UPON (on-demand overlays), NOT embedded in the task window.
|
||||
TASK PANE STRUCTURE: all thinking/coding/open tasks at the TOP, sectioned off from the task panel's own chat entry part at the bottom.
|
||||
NEW: an ANIMATION when sub-agents are in flight, in the bottom rail.
|
||||
The iterative-styling law (D35 §2) governs: this is the default composite style; the owner flips and refines live.
|
||||
|
||||
### D41 addendum — LOADING DOTS ARE RUNES; FONTS ARE THE TERMINAL'S (owner, 2026-10-18)
|
||||
The loading-dots/spinner idiom is REPLACED by the runic symbol cycle: loading indicators ARE runes (random celtic/viking + cthulian mix, per D41). Fonts are terminal-level, not app-level: the app emits codepoints; the owner's fallback pool (93 symbol fonts, Noto base — verified rendering) covers them. LAW: every animation vocabulary is DETECT-then-RENDER — verify glyphs draw (PTY byte-check) before committing them to an animation; box-glyphs get swapped at build time, never shipped.
|
||||
|
||||
## D42 — THE PERSONA LAYER RULINGS (owner, 2026-10-18, interview after the chain audit)
|
||||
1. SEED: BUILD UP from the repo's 2.3KB distilled persona — do NOT adopt the old constitution wholesale. Produce an ADOPTION LIST first: compare every component across the three generations (OpenCode/MiMoCode/repo), propose what to adopt and WHY each is a decent addition, and mark what's superseded. The owner approves the list before adoption.
|
||||
2. DYNAMIC DEPTH: Rune commands both panes equally, understands the dichotomy inherently, and puts the right resources into each dynamically — no fixed pane budget, an intelligent allocation.
|
||||
3. CONTEXT INTELLIGENCE: the neurons + sleight-of-hand machinery keep context low while full persona stays AVAILABLE — pulling persona context where it contextually matters, for the right contextual output. (Not a fixed token budget — an intelligent system.)
|
||||
4. EDIT SURFACE: all three — the truth file, journal + rollback, AND the TUI editor (/persona) over it.
|
||||
5. MIGRATION: EVERYTHING migrates for the owner's local session (the full lived store); a production version (different, privacy-shaped) comes later after the concept is proven.
|
||||
6. CONSTITUTION: DEFERRED — a shippable untouchable (encrypted) constitution is a later revision; do not block the persona build on it.
|
||||
|
||||
### D42 addendum — NO LOCAL DEFAULT; CLOUD IS FIRST-CLASS (owner, 2026-10-18: "i dont want ollama as default. i want to choose to use that. i want the default to be a cloud provider")
|
||||
1. Ollama loses its privileged position: NOT pre-selected, NOT default-positioned in the catalog, NOT the implicit fallback anywhere. It is one choice among many.
|
||||
2. CLOUD PROVIDERS ARE FIRST-CLASS in the setup wizard: connecting a cloud provider (API key from the vault, connection test) is a supported FIRST-RUN path, equal to local.
|
||||
3. The default model is whatever the owner EXPLICITLY picks in the wizard — never inferred from locality. First pick = default for both panes (unchanged D37 law).
|
||||
|
||||
### D41 addendum 2 — LIVE THINKING UX (owner, 2026-10-19)
|
||||
1. EXPANDABLE WHILE THINKING: the thinking bubble expands DURING thinking, not only after — live thoughts unfold as they stream.
|
||||
2. CLICKABLE: the expand control is clickable (click region on the marker/bubble line), alongside ctrl+t.
|
||||
3. DOTS ANIMATION: the thinking animation is "thinking . . ." — dots appear LEFT TO RIGHT after the word, each dot a NEW runic symbol (the celtic/cthulian mix), cycling from the left when the row fills. Not a spinner in place — an appearing-dots walk.
|
||||
4. PLACEHOLDER: the input box placeholder is just "type in chat" (no affordance list — commands live in the palette and the / overlay).
|
||||
5. BOX PRESENCE: the input box grows more generous (taller/padder) so it reads as an obvious place to type — natural, inviting.
|
||||
|
||||
### D41 addendum 3 — NO PREFIXES IN BUBBLES (owner, 2026-10-19)
|
||||
Chat bubbles carry NO text prefixes — the "you ·"/"rune ·" markers are removed from the bubble content. Identity is already spoken by the bubble itself: side (you right / rune left) and color (cyan / green). The purple diamond stays on rune's EVERYTHING — thinking bubbles AND her answer bubbles alike: it is her signature mark on all her words (owner: 'keep the purple diamond for runes answers tho'); timestamps stay at the bubble corner. Non-bubbled roles (system, error, research, tool) KEEP their textual markers — they have no side/color to speak for them.
|
||||
|
||||
## D43 — THE BUBBLE/HUD HARDENING (owner, 2026-10-19, fifteen rulings)
|
||||
1. HARDEN: the bubble UI logic is locked clean — nothing interferes with it; amenities only when the owner asks.
|
||||
2. TIMESTAMPS ACCURATE + LOCAL: reflect the person's machine timezone per message (low cost: local-format the UTC record at render).
|
||||
3. TIMESTAMP SIDE: moved to the OPPOSITE side of the bubble from its current position.
|
||||
4. DIRECTION-AWARE PLACEMENT: USER ON THE LEFT for left-to-right languages, rune right — INVERTED for right-to-left language locales (he/ar/fa/ur detection via locale, setting-overridable).
|
||||
5. ONE THINKING STREAM: no before/after duplication — the live thinking bubble BECOMES the history thinking bubble (one stream per message).
|
||||
6. NO-DUPLICATION POLICY: rune's answer never renders twice (completion-applied bubble and durable row are ONE fact — harden the claim law; frozen no-dupe tripwire).
|
||||
7. THINKING IS CLICKABLE (the marker, the line, the whole collapsed row).
|
||||
8. Thinking stays with its message in CHAT (owner accepted: tasks-pane thinking is too messy); tasks pane keeps its own work-stream thinking.
|
||||
9. BORDERS: dashed → SOLID, same colour as the box (pink family); ALL boxes solid pink borders.
|
||||
10. COMPOSER (the type-in-chat box) is ALWAYS lit with the pink outline; the selected pane lights pink outline when selected, GREY outline when not.
|
||||
11. INFO BAR: a clean information bar above or below the composer replaces the messy far-bottom bar.
|
||||
12. BOTTOM BAR STRIPPED: remove status=RUNNING (running is proven by working), focus=chat (visible in selection), RAIL, seq=N, runtime lib:connected. KEEP: version truncated to semver (0.314.0 not 0.314.0-56-g...), CONTEXT WINDOW in-use/available (accurate: engine reports tokens used vs the model's context limit from the registry).
|
||||
13. THINKING LINE: "◆ thinking <ctrl+t hint> <animation>" — the animation capped at 3 glyphs, the ctrl+t hint BEFORE the animation (nothing chases the walk).
|
||||
14. GHOST PLACEHOLDER: "type in chat" ghosted, vertically centered, with a BLINKING CURSOR indicating chat-here.
|
||||
|
||||
### D43 addendum — THE HARDENING MEANING (owner chose option 3, 2026-10-19)
|
||||
The bubble UI is locked against SILENT and UNINVITED change — mechanically, not by promise. Three enforcement layers:
|
||||
1. FROZEN PINS: golden fixtures + tripwires pin the bubble's geometry/colors/alignment/visual states — any render change turns the suite RED; pin updates are visible in diffs.
|
||||
2. NAMED TERRITORY: the bubble renderer files are owned surface (domain registry) — factory waves do not edit them without a brief naming the reason.
|
||||
3. CHANGES ROUTE THROUGH THE OWNER: visual changes to the bubbles ask the owner first (OpenSpec-starting-point law + D35 §2); repairs (restoring the pinned shape) land red-first as normal — the lock protects the design, never the bugs. Never locked against the owner.
|
||||
|
||||
### D43 addendum — SCROLL INDICATORS + CORRECTED COLORS (owner, 2026-10-19)
|
||||
1. SCROLL WHEEL/UI in BOTH panes: a visible scrollbar indicator while scrolling that FADES when not in use (the modern-editor pattern — visible on activity, transparent at rest). Scrolling the chat history per the landed scrollback law; the tasks pane scrolls its work stream the same way.
|
||||
2. OUTLINE COLORS STILL WRONG (second correction): the chat TEXT colors are correct — the OUTLINE colors are not. The outlines follow the D43 law (solid, pink family, composer always lit, selected pane lit / unselected grey). The wave must diff outline color assignments against the law's color table — not the text colors, the borders.
|
||||
3. TUI DESIGN PRINCIPLES: a research round on presenting a terminal UI — the design canon (hierarchy, restraint, motion discipline, information density, color systems for terminals) so future visual choices follow principles, not ad-hoc taste. Feeds the owner's iteration loop.
|
||||
|
||||
### D43 addendum — ARBITRATION + TIMESTAMPS + THE LAYER LAW (owner, 2026-10-19)
|
||||
1. ARBITRATION RULED (canon P3): one alive element wins — while Rune thinks, the bottom-rail cluster goes STILL (count only); the rail animation resumes when the thinking resolves. The rail exists to be seen, not to move.
|
||||
2. TIMESTAMPS: intelligently placed (the same relative position regardless of sender) and DEMPHASISED — their importance is low; dim, unobtrusive, never competing with message content.
|
||||
3. THE LAYER LAW (owner architecture ruling): every visual element of the TUI is an INDEPENDENT LAYER with its own scaffold/state — border, content, cursor, scroll indicator, animation each render in their own layer, and a change or defect in one must never corrupt another (the cursor punching a hole in the composer border is the motivating defect). Layers compose at render time; interactivity may couple them deliberately; accidental coupling is a defect. Each layer gets its own tests (its own scaffold, in the owner's words) — a change to one layer's state cannot alter another layer's rendering.
|
||||
|
||||
### D43 addendum 2 — LAYERS RELATE TO THE PROJECT (owner correction, 2026-10-19)
|
||||
The Layer Law's independence is INTERNAL isolation, not project isolation: each layer has its own scaffold/state/tests WITHIN the tui project's scaffolding system — registered in the same construction store, admitted through the same contracts, checked by the same authority, versioned with the project. Isolation means a change to one layer's state cannot silently alter another layer's RENDERING; it does NOT mean layers live outside the governed build. Every layer scaffold is a project citizen: contract, tests, pins, and the issue ledger all track it with the rest.
|
||||
|
||||
### D43 addendum 3 — FIVE OWNER CRITIQUES (live use, 2026-10-19)
|
||||
1. GHOST KEY HINTS: the composer shows ghosted "enter send · shift+enter newline" hints that DISAPPEAR the moment typing starts (ghost-at-rest, gone-when-typing — the affordance teaches, then vanishes).
|
||||
2. COMPOSER NEVER SCROLLS: the composer must NOT behave like a scrolling viewport while typing (the weird line-influence effect) — the composer fits its 3 rows and wraps internally; scroll behavior is for the panes only.
|
||||
3. WORD-WRAP NEVER BREAKS WORDS: wrapping breaks at word boundaries ALWAYS — a word reaching the edge moves WHOLE to the next line, never splitting mid-word (observed: "my" split to "m/y" at pane edge). Applies to chat bubbles, composer, all text surfaces.
|
||||
4. COMPOSER CLICK PRESERVES PANE CHOICE: clicking the composer after selecting a pane keeps THAT pane's focus — the composer serves whichever pane is selected; clicking it must never default back to chat. The composer is the shared instrument; the panes set its destination.
|
||||
5. (Reward for catching these: the Layer Law holds — each defect was an isolated layer failing, never cascading.)
|
||||
|
||||
### D43 addendum 4 — THE CENTERED PICKER + CLOUD PROOFS (owner, 2026-10-19)
|
||||
1. MODEL PICKER = CENTERED WINDOW, NOT FRONT-LOADED GATE: model selection must NOT block the composer (currently typing anything demands a pick first). Instead: typing with no model chosen opens a CENTERED floating window (mimocode-style) listing ALL providers, SEARCHABLE (type to filter), up/down arrows to walk, enter to choose, esc to dismiss-and-keep-typing. The picker is an overlay on demand, never a gate in front of the input.
|
||||
2. API KEYS: port ALL api keys from mimo code without altering them (the 15-secret lossless migration already landed into the OS vault — verify nothing was altered/mangled in translation and every registry row still resolves).
|
||||
3. CLOUD CONNECTIVITY PROOFS: every cloud provider connection must actually prove connectivity (a real endpoint probe). Providers the owner holds keys for: prove live. Providers without keys: the probe path is the same principle (endpoint reachable, auth attempted, honest error) — but marked untestable-by-owner rather than claimed.
|
||||
|
||||
### D43 addendum 5 — SCAFFOLDING STATUS LINE + GATED-WORK ANIMATION (owner, 2026-10-19)
|
||||
1. SCAFFOLDING STATUS LINE (owner spec): when scaffolding is in play, the tasks pane shows: "scaffolding · <loading animation> · <its stage>" — e.g. "scaffolding ⚒ admit", "scaffolding ⚒ build", "scaffolding ⚒ verify", "scaffolding ⚒ land". The loading animation uses the gated-work rhythm (hammer/ladder glyphs per the owner's hammer-and-ladder ruling); the STAGE is the contract's real phase from the kernel state (admit/build/verify/land — never a guess). Appears when a governed contract is active; self-extinguishes when it closes.
|
||||
2. HAMMER-AND-LADDER WALK: the gated-work animation is the dots-walk pattern with hammer/ladder glyph vocabulary (⚒ ⛏ 🪜-class glyphs, font-verified per detect-then-render; the shuffled runic/cthulian vocabulary remains for THINKING — gating gets its own trade vocabulary so thinking and building read differently at a glance).
|
||||
3. IMAGES: viewable in-terminal (Kitty/sixel protocols; Konsole supports; graceful degradation to links elsewhere per the degrade-silently principle); analyzable when a vision-capable model is connected. Recorded as capability, not a wave yet.
|
||||
4. INESCAPABILITY: the machinery-first stack (launch guard → admission → scope-checked mutations → landing gate → durable lock) IS the law — "please build X" puts every step inside scaffolding regardless of model. The adversarial-escape audit (try to escape, close every hole) is queued as its own wave.
|
||||
|
||||
### D43 addendum 5 (cont.) — ALL WORKHORSE VISUALS LIVE IN THE TASKS PANE (owner, 2026-10-19)
|
||||
Ruling: ALL coding/workhorse-related visuals belong in the TASKS pane — scaffolding status line, work blocks, queue rows, sub-agent window, gated-work animations. The CHAT pane stays conversational (messenger law); the TASKS pane is Rune's workshop window. Both addenda recorded and queued: the scaffolding status line + hammer/ladder animation + the adversarial-escape audit + image capability (recorded, not queued) all route to the tasks-pane wave.
|
||||
|
||||
### D43 addendum 6 — THE THINKING BRIDGE + HUD CORRECTIONS + THE WORKSHOP COMPLETION (owner, 2026-10-20)
|
||||
1. THE THINKING BRIDGE: the thinking animation MOVES OUT of the chat pane into its own strip BETWEEN the panes and the composer (above the composer, full width). Solves: click-to-expose lives right there (click the strip → the thought process expands — in the bridge or a floating window per the tabs/windows design), and the chat pane declutters (conversation only).
|
||||
2. TIMESTAMPS: ALWAYS far-left, same position regardless of sender.
|
||||
3. COMPOSER CURSOR: the blinking cursor must ALWAYS blink when waiting for entry — never static (a regression).
|
||||
4. TASKS PANE = THE ACTIVE WORKSHOP: while scaffolding runs, the work-in-full-view renders inside the tasks pane — the active task's live coding (current file/command, thinking) fully visible. PLUS a tasks VIEW: clickable/expandable task rows with the open tasks (the D38 ledger's OPEN TASKS section becomes an interactive surface, not a static list).
|
||||
5. SUB-AGENT WINDOW OPENER: the window exists but has NO WAY TO OPEN — put the opener in the info bar NEXT TO the tasks area, AFTER the rune version.
|
||||
6. VERSION DISPLAY: 'v' prefix (v0.321.0 not 0.321.0) — a version marker; could hyperlink to Gitea (the release URL) when terminals support it (OSC 8 links — degrade gracefully to plain text).
|
||||
7. SEQUENCE AFTER THIS WAVE: finish wiring the rebase into the TUI, then the persona layers.
|
||||
|
||||
## D44 — THE MODEL DISPLAY + THE PANE DOCTRINE CORRECTED (owner, 2026-10-20 — supersedes the partial D36 routing read)
|
||||
|
||||
### 1. MODEL DISPLAY (the info bar shows the active model, correctly compacted)
|
||||
The info bar carries the active model name. LOCAL models truncate to their important components: initial name, size, style if applicable (classifier vs normal — ONLY SHOWN WHEN APPLICABLE), and ablation status (uncensored/abliterated variants flagged). CLOUD models: full name, size, classifier-if-applicable. Never let a long local filename eat the bar.
|
||||
|
||||
### 2. THE PANE DOCTRINE (owner verbatim — the definitive statement)
|
||||
"the tasks pane is meant for ALL ACTIVE WORK or whats being being worked upon... the dichotomy for either pane is CHAT IS FOR TALKING WITH THE MODEL WHILST THE WORK IS BEING DONE. the TASKS PANE is to view the details of whats being worked upon and outstanding work as well as any goals or lists SEPARATE from the detail of the work. and the COMPOSER for either acts differently based on whats selected."
|
||||
- TASKS PANE focus + composer enter = IMMEDIATE EXECUTION (never queues — queuing only when work is already running; a queue message with nothing running is a FALSE POSITIVE defect).
|
||||
- CHAT PANE focus + composer enter = conversational (base assumption: discussing). Work requests from CHAT get boosted to tasks via the CONSENT CARD (contextual relevancy decides when) — the consent bridge applies ONLY to the chat pane.
|
||||
- The queued-work message renders in the TASKS PANE (with the queue rows), NEVER the chat pane.
|
||||
- A HOTKEY exists to steer queued work (reorder/prioritise/cancel from the pane).
|
||||
|
||||
### 3. QUEUE FALSE-POSITIVE FIX (defect)
|
||||
Task-pane asks with nothing running must START immediately — the instant-if-free law was violated (queued despite idle). Red-first the false positive; the queue is only for BUSY.
|
||||
|
||||
## D45 — THE LIVE-USE CRITIQUE BATCH (owner, 2026-10-20, after testing the D44 build)
|
||||
1. CONTEXT READOUT STILL MISSING in the bar (the core-side accounting landed but the bar doesn't show it — diagnose the TUI-side wiring: the bridge's status poll must actually carry/receive the numbers, and the bar must render once reported).
|
||||
2. MODEL NAME COMPACTION (refined spec): "qwen3.6-27b-(uncensored?)" style — parse the id: base name, parameter size, ablation marker (uncensored/abliterated/heretic) if present. No path prefixes (hf.co/), no quant suffixes (IQ3_M), no GGUF noise. Cloud models similar: vendor name + model + size.
|
||||
3. VERSION HYPERLINK FORGOTTEN: the v-prefixed version must hyperlink to the Gitea release (OSC 8 — detect-then-render).
|
||||
4. TIMESTAMPS STILL NOT MOVED (the addendum-6 far-left move didn't land — re-verify and fix).
|
||||
5. THE TOOLSET GAP — THE BIG ONE: Rune cannot bash or write to the owner's PC. The model's thinking says "I cannot directly access your desktop" — because the TUI chat path doesn't surface the tool suite to local models. The tools exist (tool_executor, 19 tools, mode-gated); the chat turn's tool wiring must FORCE tool availability: local models get the toolset in their prompt/capability selection (the T2 slice landed the exposure filter — verify it's active on the live path), cloud models get native tool-calls. Rune at the face must be ABLE to act, mode-gated as D36/D44 demand.
|
||||
|
||||
## D46 — FOUR LIVE DEFECTS (owner, 2026-10-20, second testing round)
|
||||
1. MESSAGE DUPLICATION (never again): a sent message renders TWICE in the chat pane. The claim law was hardened once (D43) but a path still double-renders — find it (likely the D45 wave's changes to the sync paths interacting with the thinking-bridge move), red-first, and pin the no-dupe tripwire so hard it cannot regress.
|
||||
2. THINKING NOT MOVED: the thinking animation/strip was ruled OUT of chat into the bridge strip (addendum 6 item 1) — still rendering in the chat pane. Move it.
|
||||
3. COMPOSER SHRUNK: the composer got SMALLER — wrong direction. The composer may push BOTH panes up for more room (its growth is legitimate; panes yield).
|
||||
4. DISPATCH TIMEOUT: 'error · dispatch failed: Post ... context deadline exceeded' — the 330s budget was exceeded. Diagnose: the 27B thinking models run 250-310s solo and the chain stacks delays; either raise the budget on the TUI+proxy legs or stream. Diagnose the real timeout hop first.
|
||||
- D46 RESOLUTION (the budget road, owner's pick — streaming stays queued separately): the real hop was the WHOLE STACK at 330s (TUI client lib.CompletionBudget, entry serve proxy proxyClientBudget, the continuous-runtime door — all pinned 330) against a 27B solo worst turn of ~310s plus stacked queue/memory/tool latency: no headroom, so the deadline fired. ONE coherent 480s budget now rides every leg (TUI + both entry doors), pinned structurally by the frozen budget fixtures; the core operator's per-call 300s adaptive ladder (27B) stays — 480 covers the worst observed turn with headroom.
|
||||
|
||||
### D46 addendum — CLICKABLE ARTIFACTS + TOOL EVENTS LIVE IN THE TASKS PANE (owner, 2026-10-20)
|
||||
1. CLICKABLE FILE ARTIFACTS: when a task generates a file, the file's path in the tasks pane is a CLICKABLE LINK that opens its location (OSC 8 hyperlink with a file:// URL — Konsole opens the file manager/terminal at the path; graceful plain-text degrade elsewhere per the detect-then-render law). The pane shows what was made; the link takes you to it.
|
||||
2. TOOL EVENTS MOVE: tool usage display moves OUT of the chat pane INTO the tasks pane (the work block's current-line already shows the newest tool action — ALL tool events belong there, in the work stream, not the conversation). Chat stays conversational: the tool event leaves the transcript as chat content and becomes work-stream detail. The D43 layer law and the pane doctrine (D44) both point this way.
|
||||
|
||||
### D46 addendum — Q IS A LETTER NOW (owner, 2026-10-20: "change the q for quit... if i type q it will exit")
|
||||
1. PLAIN q TYPES: q is a letter the composer owns — it must never quit.
|
||||
2. THE EXIT GESTURE: ctrl+c clears the chat/composer on the first press; TWO ctrl+c IN SUCCESSION quits the program. (A deliberate double-press — you can't quit by accident while typing.)
|
||||
3. Every other quit binding (q, esc-as-quit paths) follows the same rule: nothing quits on a single unmodified keypress while the composer can receive text.
|
||||
|
||||
## D47 — SIX LIVE-USE DEFECTS + OPENSPEC ROUTING (owner, 2026-10-20)
|
||||
OPENSPEC ROUTING: the tabs/windows design AND the chat-presentation fixes go through the OpenSpec interview process — the owner specifies, the spec records, the factory builds. Two openspec topics opened.
|
||||
DEFECTS (each red-first, each PTY-proven):
|
||||
1. ANSWER DISPLAY BROKEN: Rune's answers render incorrectly in the chat pane (live-use report — diagnose the exact symptom on the owner's next boot: markdown artifacts, broken wrapping, or encoding; the word-safe wrap landed but something else misrenders).
|
||||
2. CODE NOT ROUTED TO THE TASKS PANE: code content in answers renders in the chat pane, not the tasks pane (D44 doctrine: work content lives in the workshop pane; the code-block projection needs routing).
|
||||
3. EVERY SENTENCE BULLET-POINTED: answers render with per-sentence bullets — a markdown-list misparse (the renderer treats prose as list items). Diagnose and fix the markdown handling.
|
||||
4. CONTEXT DEADLINE ERRORS PERSIST: the 480s budget didn't take on the live path (diagnose which hop still runs 330s — the fix landed but a leg is stale or the live turn exceeds even 480s on the 27B; measure the real turn duration).
|
||||
5. CONTEXT READOUT PER-MODEL ACCURACY: context available must be accurate FOR EVERY MODEL LOADED (the limit source must be per-model: Ollama /api/show for locals, the registry limit for cloud — not a single cached value).
|
||||
6. VERSION HYPERLINK STILL NOT RENDERING (the OSC 8 fix landed but the bar doesn't show it — diagnose the live render path; maybe the deployed binary predates the fix or the detection gate is misfiring).
|
||||
7. MODEL OPENS THE PICKER: clicking the model name in the info bar opens the centered model picker (the picker's second road — the click road beside ctrl+m).
|
||||
@@ -1,140 +0,0 @@
|
||||
# Authority Map — checkout vs deployed generation (Spec 0.1)
|
||||
|
||||
Task `0.1` of `docs/openspec/changes/add-native-rune-tui/tasks.md` (0A. Truth
|
||||
first — authority and runtime audit). Written 2026-10-17T06:46Z against dev
|
||||
checkout `master@ed245b2a`. This map reconciles the THREE code generations
|
||||
that exist on this machine and names the authority for every link between
|
||||
them. Its enforcement artifact is `src/rune/runtime_attestation.py` +
|
||||
`tests/test_runtime_attestation.py` (live attestation; RED when the runtime
|
||||
is not the qualified artifact).
|
||||
|
||||
## 1. The generations
|
||||
|
||||
| Generation | Location | Identity | Role |
|
||||
|---|---|---|---|
|
||||
| Dev checkout | `/mnt/workdrive/rune` (`master@ed245b2a`) | `__version__ = 0.310.0`, **no** `_build_identity.json` → `build_info().source == "dev"` | Factory floor; all waves build here; never launched by the launcher |
|
||||
| Deployed (live runtime) | `/mnt/workdrive/rune-code/current` → `81099a11f7a8` | `_build_identity.json`: commit `81099a11f7a845bea629324e45a2aeef1d00e2a2`, version `0.128.0`, deployed_at `2026-10-07T20:03:04Z` | The ONLY generation the launcher runs |
|
||||
| Last-good (rollback) | `/mnt/workdrive/rune-code/last-good` → `3c13be1d766b` | commit `3c13be1d766bf2bb9d25f032ca0cfdee089a55b0`, version `0.127.0`, deployed_at `2026-10-07T20:00:09Z` | Previous `current`; rotated automatically by every promotion |
|
||||
|
||||
Facts that matter:
|
||||
|
||||
- `81099a11` **is an ancestor of `master@ed245b2a`** — the deployed
|
||||
generation is a genuinely CI-qualified master revision. The relationship
|
||||
is pure staleness (lag), **not divergence**: there is no commit in the
|
||||
deployed tree that the checkout lacks.
|
||||
- The lag is **1522 commits / 182 minor versions** (0.128.0 → 0.310.0) and
|
||||
~10 days (no promotion since 2026-10-07T20:03Z; the factory's whole
|
||||
0C/0D/I/J-era output — tui/, the rune-entry serve surface, the internal/
|
||||
library conversion — is undeployed).
|
||||
- `/mnt/workdrive/rune-code` holds 300+ staged generations plus 12
|
||||
orphaned `.staging-*` dirs; ownership `gitea-desktop-runner`. The stale
|
||||
`.convergence-intent.json` there (version 0.66.57, recorded_at "0") is a
|
||||
leftover record, not an authority.
|
||||
|
||||
## 2. Launcher authority chain (what actually runs)
|
||||
|
||||
`~/.local/bin/rune` (hand-installed shell script, mtime 2026-09-11) is the
|
||||
single entry point. Resolved chain:
|
||||
|
||||
```
|
||||
~/.local/bin/rune
|
||||
└─ wezterm start -- bash -c
|
||||
cd /mnt/workdrive/rune-code/current/src ← runtime code (generation pinned only by `current`)
|
||||
RUNE_HOME=/mnt/workdrive/rune ← data home (state/memory of the LIVE system)
|
||||
RUNE_MEMORY_DIR=/home/nick/.config/opencode/memory
|
||||
MIMO_PROBE_ENABLED=1 ← ADAPT-LIVE bounded probe policy (2026-09-10)
|
||||
~/.venvs/memory/bin/python3 -m rune ← interpreter: the memory venv, NOT repo .venv
|
||||
```
|
||||
|
||||
Authority rows (current vs intended vs dormant):
|
||||
|
||||
| Surface | Current authority | Intended (per openspec) | State |
|
||||
|---|---|---|---|
|
||||
| Launcher script | owner (hand-installed; not owned by any repo module) | unchanged for the Python face | current |
|
||||
| Runtime code | `rune-code/current` generation (deploy pipeline output) | same mechanism, newer revisions | current, stale |
|
||||
| Runtime identity | `rune/__init__.build_info()` + `_build_identity.json` stamped by `deploy_stage.py` | same (already landed 2026-09-10 handoff) | current |
|
||||
| Data home | `RUNE_HOME=/mnt/workdrive/rune` (the dev checkout doubles as the live data dir: `state/`, `store/`, `memory/`) | pin stays; code deploys never move user data | current |
|
||||
| Memory dir | `RUNE_MEMORY_DIR=~/.config/opencode/memory` (promotion ledger + memory store live here) | same | current |
|
||||
| Promotion decision | `scripts/deploy_promotion.py` — monotonic ancestry law, flock'd critical section, atomic symlink swap, `last-good` rotation | same | current |
|
||||
| Deploy qualification | `scripts/deploy_qualify.py` + CI required-checks (fail-closed after the 0.66.28/29 incidents) + `scripts/tui_startup_canary.py` pre-promotion canary | same | current |
|
||||
| Receipts / tamper evidence | `src/rune/audit_ledger.py` — HMAC-chained ledger under RUNE_MEMORY_DIR (key lineage repair is Task 0.2, out of scope here) | same | current |
|
||||
| Go face (`tui/cmd/rune-tui`, `src/go/rune-entry` front door :8377/:8376) | exists ONLY in the checkout; nothing deployed runs it | launcher target once 0C/0D converge | **dormant** |
|
||||
| Paired-build attestation (`src/rune/build_attestation.py`, task 0.14) | checkout-only (Go↔Rust build pairing; a different attestation from this one) | gates future paired artifacts | dormant until next deploy |
|
||||
|
||||
Surprise worth naming: **the dev checkout is also the live system's data
|
||||
home.** `RUNE_HOME=/mnt/workdrive/rune` means the deployed runtime reads and
|
||||
writes state inside the factory worktree (`state/`, `store/scaffold`,
|
||||
`memory/journal`) — the same files the test suites mutate. That is why test
|
||||
runs dirty the live store and why background workers must restore journal
|
||||
churn after every run.
|
||||
|
||||
## 3. Module drift — deployed `81099a11` vs checkout `ed245b2a`
|
||||
|
||||
| Tree | Deployed | Checkout | Delta | Deployed-only modules |
|
||||
|---|---|---|---|---|
|
||||
| `src/rune` (Python) | 184 | 284 | +100 | **none** |
|
||||
| `src/go` | 42 | 94 | +52 | **none** |
|
||||
| `src/rust` | 80 | 104 | +24 | **none** |
|
||||
| `tui/` | absent | 36 packages | +36 | — |
|
||||
|
||||
Zero deployed-only modules in every tree: the deployed generation is a
|
||||
strict prefix of the checkout. Headline checkout-only surfaces (all
|
||||
undeployed): the entire `tui/` face; the J1 internal-library conversions
|
||||
(`src/go/{rune-entry,event-bus,model-routing,tool-executor,work-manager}` →
|
||||
`internal/` + thin shells); `src/rune/{build_attestation,
|
||||
construction_admission}`; the rune-entry serve surface
|
||||
(`/v1/{conversation,tasks,events,health}`); boundary codegen (0.12);
|
||||
versioning port H1–H3. Consequence: any qualification, canary, or attestation
|
||||
written only in the checkout (including this map's test) does not constrain
|
||||
the live runtime until the next promotion carries it — that is the intended
|
||||
0.14/0.15 landing path, not a gap in this task.
|
||||
|
||||
## 4. Reconciliation verdict
|
||||
|
||||
1. **No divergence.** Deployed `81099a11` is an ancestor of `master`; the
|
||||
checkout is a superset. Nothing was forked, patched, or hot-fixed inside a
|
||||
staged generation (identity commit == directory name == runtime
|
||||
self-report; verified below by the live test).
|
||||
2. **Pure lag.** 1522 commits / 0.128.0 → 0.310.0 / 10 days without a
|
||||
promotion. The launcher's comment ("runs the DEPLOYED revision
|
||||
(CI-qualified, build-identity stamped)") is TRUE of `81099a11` at its
|
||||
promotion time and STALE as a description of the system: everything the
|
||||
owner has watched land since Oct 7 (tui, serve, libraries, gates) runs
|
||||
only from the checkout when invoked manually — never via `rune`.
|
||||
3. **Rollback target is coherent.** `last-good → 3c13be1d` (0.127.0) is the
|
||||
immediately preceding generation; promotion rotated it correctly.
|
||||
4. **The one launch path** is the Python face. No second launcher, alias, or
|
||||
systemd unit runs Rune code (Go binaries under `src/go/` are parity
|
||||
harnesses, not installed). The Go face is checkout-dormant by design
|
||||
until 0C/0D converge.
|
||||
|
||||
## 5. Attestation contract (the enforcement half of 0.1)
|
||||
|
||||
`src/rune/runtime_attestation.py` — fail-closed attestation of the chain
|
||||
launcher → `current` → generation → identity → qualification:
|
||||
|
||||
1. launcher exists and its `cd` target is `<deploy_root>/current/src`;
|
||||
2. `current` is a symlink resolving to an existing directory inside the
|
||||
deploy root;
|
||||
3. the generation carries a well-formed `_build_identity.json`
|
||||
(commit + version + deployed_at);
|
||||
4. identity commit == generation directory name (a staged tree cannot
|
||||
impersonate another SHA);
|
||||
5. the RUNTIME SELF-REPORTS: importing `rune` from the generation's `src`
|
||||
yields `build_info().commit` == identity commit and `source ==
|
||||
"deployed"` — a dev/unstamped tree answers `"dev"` and fails;
|
||||
6. the identity commit is an ANCESTOR of the canonical release branch
|
||||
(`refs/heads/master`) in the checkout — an unqualified artifact can never
|
||||
attest;
|
||||
7. identity version == the generation package's `__version__` (coherent
|
||||
stamp, not a stale one).
|
||||
|
||||
Every path is injectable; the live defaults bind `~/.local/bin/rune`,
|
||||
`/mnt/workdrive/rune-code`, and the repo root. CLI:
|
||||
`python -m rune.runtime_attestation` prints the JSON evidence, exit 1 on any
|
||||
failure. `tests/test_runtime_attestation.py` freezes hermetic negative
|
||||
fixtures for each link (launcher missing/repointed, symlink missing/outside
|
||||
root, identity missing/malformed/mismatched, non-ancestor commit, runtime
|
||||
self-report mismatch, version incoherence) plus a LIVE test that attests the
|
||||
real machine chain and goes RED the moment any of it breaks — runtime ≠
|
||||
qualified artifact.
|
||||
@@ -1,108 +0,0 @@
|
||||
# Bug-fix contract
|
||||
|
||||
_Binding on every agent, session, and pipeline stage in this repository. The
|
||||
henxel mirror of this contract lives in `henxels.yaml` ("bugs discharged
|
||||
red-first"); the enforcement hooks live in `scripts/check_discharge_contract.py`
|
||||
and `.gitea/workflows/pipeline-gate.yml`. Escapes from this contract are made
|
||||
by editing those files — visibly — never by silent deviation._
|
||||
|
||||
## The contract
|
||||
|
||||
When a **bug is found** — by a gate, a reviewer, CI, a canary, production, or a
|
||||
human — the fix MUST follow this cycle before it may merge:
|
||||
|
||||
1. **Red first.** A failing test that reproduces the bug is written BEFORE the
|
||||
fix. The test must fail for the *bug's cause*, not for an unrelated reason.
|
||||
A fix without a red test is a symptom patch and is rejected at review.
|
||||
2. **Root cause, not symptom.** The fix addresses the cause. If the reporter's
|
||||
diagnosis is wrong, the discharge says so and names the real cause.
|
||||
3. **Full pipeline re-entry.** The fix is treated as a NEW SUBMISSION: full
|
||||
suite, pyright ratchet (not increased), ruff, canaries, henxels, and the
|
||||
spec + quality gates for anything beyond a mechanical minor. No
|
||||
"hotfix-skips-gates" path exists.
|
||||
4. **The regression test is permanent.** Tests written for a discharged bug are
|
||||
frozen — they may be refactored, never weakened or deleted. The suite can
|
||||
only grow in assertion strength over time.
|
||||
5. **Recorded.** The fix's PR (or plan tail) maps every finding → the red test
|
||||
→ the commit. A discharge record with holes bounces at review.
|
||||
|
||||
## Scope and proportionality
|
||||
|
||||
- **Gate findings (spec/quality):** every BLOCKER/MAJOR gets the full cycle;
|
||||
MINORs get at minimum a red test OR an explicit reviewer-approved waiver
|
||||
recorded in the plan tail.
|
||||
- **Production failures:** the fix follows this contract AND, where the failing
|
||||
payload is preservable, #157's fixture pipeline (real failure → sanitised
|
||||
fixture → permanent regression test).
|
||||
- **CI/workflow bugs:** the red test is the workflow-contract assertion
|
||||
(parsed-YAML step/step-logic assertions, as in `test_workflow_script_contract.py`)
|
||||
— workflows are code; their bugs get tests too.
|
||||
- **Trivial/no-test-class bugs** (typos in comments, log strings): documented
|
||||
exemption list in this file. PR-only changes to those strings skip the red test.
|
||||
|
||||
### Exemption list (trivial/no-test-class)
|
||||
|
||||
The red test is skipped only when the change is mechanical and carries no
|
||||
behavioural surface. Current exemptions:
|
||||
|
||||
- typos in comments, docstrings, and documentation prose;
|
||||
- log-message wording with no parsing consumer (nothing asserts on the text);
|
||||
- README/wiki wording.
|
||||
|
||||
Anything touching executable behaviour — including workflow YAML steps,
|
||||
scripts, and configuration consumed at runtime — is NOT exempt.
|
||||
|
||||
## Why (the failure class this kills)
|
||||
|
||||
LLM-authored changes share wrong assumptions between implementation, tests, and
|
||||
docs. A symptom patch shares the reporter's wrong assumption with the fix. The
|
||||
red-first test is the cheapest known breaker of both: it forces the fixer to
|
||||
state the bug's cause in executable form before claiming to have removed it.
|
||||
|
||||
## Enforcement
|
||||
|
||||
- `scripts/check_discharge_contract.py`: verifies a PR that touches files named
|
||||
in a `Discharge:` trailer block carries at least one test-hunk per finding
|
||||
id (structural check — PR description convention: `Discharge: <id> -> <test file>`).
|
||||
- Gate reviewers are instructed (dispatch templates, issue #151) to reject
|
||||
symptom patches: "what test failed red for this bug?" is a mandatory
|
||||
reviewer question for every fix PR.
|
||||
- `henxels.yaml` carries the tracked one-line mirror so the contract is part
|
||||
of the repo's auditable contract surface.
|
||||
|
||||
## Binding refinements (pipeline doctrine, 2026-09-07)
|
||||
|
||||
6. **Soak-before-close for intermittent defects.** A flaky, timing, or
|
||||
event-delivery bug cannot be proven fixed by one green run. The fix merges
|
||||
through the normal pipeline, but the issue CLOSES only on soak evidence: a
|
||||
stated criterion (e.g. "N consecutive green runs" or "48h without
|
||||
recurrence") plus the observations meeting it. The closing comment carries
|
||||
the soak record. Merge ≠ closure for this class.
|
||||
7. **Escape accountability.** When a bug reaches production (escaped every
|
||||
gate), the discharge must answer: _which gate should have caught this, and
|
||||
why didn't it?_ The answer becomes a gate improvement — a new contract
|
||||
assertion, a checker rule, or a documented gate limit. Gate hardening is a
|
||||
first-class output of bug fixing, not an afterthought.
|
||||
8. **Fresh-context root-cause verification.** The red test proves the fixer
|
||||
understood _a_ cause. For BLOCKER/MAJOR fixes, a fresh-context reviewer
|
||||
additionally asks: does this test fail on the pre-fix code _for the right
|
||||
reason_? (Same doctrine as the black-box test author: the person who wrote
|
||||
the fix does not certify its diagnosis.)
|
||||
9. **Déjà-vu detection.** Before writing the red test, search the suite and
|
||||
closed issues for the same failure class. A second occurrence of a class
|
||||
means the first fix treated a symptom — the discharge must then cover the
|
||||
class, not the instance.
|
||||
10. **Fixture promotion.** When the red test needs a complex payload, it goes
|
||||
through #157's sanitised-fixture pipeline (real failure → secrets stripped
|
||||
→ `tests/fixtures/production/`) instead of a hand-crafted approximation.
|
||||
Real failures are the best regression tests. Operationally: after the fix
|
||||
merges, run the capture command from
|
||||
`tests/fixtures/production/README.md`
|
||||
(`python3 scripts/capture_failure.py payload.txt --class <class> --source <issue|ci|manual>`)
|
||||
on the ACTUAL payload and commit the fixture + manifest with the discharge
|
||||
record. The generated regression tests (`tests/fixtures/production/test_production_fixtures.py`)
|
||||
assert the payload's sha256 against the manifest (tamper-evidence) and that
|
||||
the class's checker still catches the recorded defect — a recurrence of the
|
||||
captured failure fails CI. Exemptions from fixture capture follow the
|
||||
trivial-bug rule below: a typo or a log string has no payload worth
|
||||
preserving.
|
||||
Loaded 100 of 8955 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user