openrig-operating-model
Use when you do not know WHERE something belongs: you learned a lesson and are unsure which file takes it; you are about to create a new doc, folder, or convention; you are asking "should this be a skill, a note, or a chain file?"; you inherited a seat and want to know what SHOUL
Install
npx skills add https://github.com/mvschwarz/openrig/tree/main/packages/daemon/assets/plugins/openrig-core/skills/openrig-operating-model
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install mvschwarz-openrig@llmmart
git clone https://github.com/mvschwarz/openrig.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole mvschwarz/openrig collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
The OpenRig Operating Model
Canonical term: the Operating Model. How an OpenRig topology organizes every kind of context so that any agent — cold, fresh, or five generations in — can find what it needs and knows where to write what it learns. Authored context stays in addressed Markdown and manifests. Supported writers record semantic judgments; their read models remain rebuildable from those artifacts.
This skill owns the structure: trees, chains, tracing, and placement of authored knowledge. Resolve installed context through its current library addresses and the project's selected loadout; a private corpus is not a required public dependency.
Companion implementation assets in this skill's folder:
templates/andscripts/— starters and working tools, described in §8.
1. The model in one breath
Everything lives on one of two trees, and every kind of context is a chain: the same filename at every level of a tree. To know anything, you trace from where you are toward the root, reading that one filename at each level — nearer levels refine or override farther ones. The trace (§5) is the act of doing that ascent deliberately, with your own file reads.
- Topology tree — how work is done:
fleet → instance → rig → pod → seat. - Work tree — how the product gets built:
project → mission → slice → proof item.
An instance is one OpenRig daemon and the rigs it manages; a fleet is all instances. Where a daemon runs is a deployment fact, not an altitude — say the instance's name when you mean a particular one.
The one-parent law: the trace follows the
directory path and NOTHING else — no pointer fields, no link-following, no
branching, ever. The trace is the instrument confused agents reach for, so it
must be simpler than anything it corrects: path-only ascent fails only in
obvious ways. A child that relates to a second parent gets an also-serves:
ANNOTATION (read by humans and agents, walked by nothing) — and a genuine
two-parent tie usually means the item belongs one level up, under the common
ancestor, with mentions pointing down. The operational many-to-many lives in
the queue's tags, which are maintained by use. Legacy serves: lines are
transition bridges for the flat layout only: never required, never walked,
deleted as work nests properly.
The filename law: one name per chain, identical at every level. The folder
tells you whose it is; the filename tells you what kind it is. A seat named
pm-lead in two different rigs has two different LEARNED.md files — the path
is the identity. Never invent per-level names (no SEAT.md, no RIG.md): that
breaks the trace, the self-description, and every tool at once.
2. THE GRID — what exists at each level, and who keeps it true
This table is the model. If you internalize one thing, internalize this.
Topology tree
The chains carry what is true of a position. How a kind of thing works — the operating
model — ships in the mode-neutral openrig-core plugin; an operating-mode plugin
(openrig-lab, openrig-factory, or openrig-hq) may refine it.
| Level | CULTURE.md — values |
LEARNED.md — what THIS ONE has learned |
kept true by |
|---|---|---|---|
| fleet | default culture (ships with OpenRig) | fleet-level lived practice | operator agent |
| instance | (inherits) | what is true of every rig on this daemon | the instance's operator |
| rig | the rig's constitution | this rig's lived practice | the rig's orchestrator |
| pod | (inherits) | this pod's lived practice — the context domain: anything useful to anyone in this pod | the pod's lead |
| seat | (inherits) | this seat's lived knowledge — the file that fixes handovers | the seat itself |
Chain files sit on nodes, not on the shelves that hold them. rigs/, pods/, seats/,
missions/ and slices/ are shelves — the trace passes through them and expects nothing there.
Work tree
ONE authored node file: SPEC.md. Intent lives in its FRONTMATTER; the specification lives in
its body. Alongside it sit three files with different jobs and different writers:
| file | what it is | who writes it |
|---|---|---|
SPEC.md |
the node — intent: composes, body specifies |
the node's owner |
NOTES.md |
LIVED — what actually happened doing it, in the doer's own words | whoever is doing it |
PROOF.md |
evidence the thing does what was intended | the prover |
PROGRESS.md |
authored narrative and historical checklist marks; current proof readiness is derived from attributed judgments | the scope's owner and provers |
proof/judgments/ |
retained item judgment receipts written by rig proof judge |
judges selected by the owning proof policy |
A scaffold may create NOTES.md and its starter instructions; its lived entries are never
generated or projected. It is the work tree's lived file, the way LEARNED.md is the topology
tree's — the field-notes rung, where raw observation goes before anything has earned a place in the
node itself. Keeping lived files out of the render path is what makes them safe to write freely in.
Legacy name: older workspaces may use MISSION_NOTES.md. Keep legacy
content addressable; use NOTES.md for the chain because its name works at every altitude.
| Level | SPEC.md — frontmatter intent: (why) + body (what must be built) |
progress | kept true by |
|---|---|---|---|
| project | intent: only — stable, changes at real pivots |
derived roll-up | the project's PM |
| mission | intent: and proportional mission-level specification; organizes slices |
derived child readiness; distinct outcome judgment | the mission's PM |
| slice | intent: and the concrete slice specification |
derived proof readiness | the slice's owner |
| proof item | authored promise in the proof contract | attributed evidence-backed judgment on that revision | the selected judge |
Current acceptance: Scope relationships and policy are authored in manifests; an item
judgment is recorded once against its promise, evidence and policy revision. Slice, mission
and project readiness derive upward. A distinct outcome or publication decision remains its
own authority. Queue ownership and generic done are custody facts, not proof acceptance.
Historical checkbox marks stay readable without acquiring current item authority.
Use rig proof --help for the supported write/read path. The project owner selects
proofPolicy.judges in project.yaml, mission.yaml or slice.yaml (nearest wins),
using exact actor addresses. There is no implicit fixed-role review conveyor.
rig proof judge mission/slices/slice#1 --verdict accept --reason 'Observed the outcome' --evidence proof/result.md
resolves the item identity, revision, evidence digests and retry identity. Evidence may be
an existing legacy artifact or a non-code outcome. A patch-equivalent subject also names
its actual comparison/adoption receipt with --comparison; the agent owns that judgment.
rig proof show returns the current basis and retained receipt; it does not publish anything.
Correct with reject, or withdraw when prior acceptance no longer stands. History stays
in the addressed proof home. A repeated request returns its original receipt and current
readiness; --replace deliberately reaffirms an old identical judgment after a correction.
Changed promises, policy or evidence require a current judgment. Explicit optional
<!-- proof-item: stable-id --> markers preserve identity across wording edits; the
product otherwise derives identity from the full promise and reports ambiguity.
Intent composes; the body does not. The trace reads the intent: FIELD at every altitude, so
four levels unfurl as four sentences rather than four documents. The body is read only when you
are standing on that node.
compose.py up <node> --name SPEC.md --field intent --root <project>
A level whose file exists but lacks intent: is reported as a gap — a mission with no intent is
real information, never silently skipped.
Legacy README.md nodes stay valid indefinitely — the resolver prefers SPEC.md and falls
back, so nothing is forced to migrate and dormant missions need no attention.
PROGRESS stays separate. Narrative and legacy marks are retained testimony. For a
project with selected proof policy, current readiness comes from rig proof show and the
shared existing views, not hand-maintained parent checkboxes or copied status prose.
scripts/compose.py progress remains a legacy checklist renderer; it does not certify
attributed acceptance. Intent composes downward; proof readiness aggregates upward.
The axis behind the columns: every context kind has a template half (what ships — SOP, the default culture) and a learned half (what living in it taught — LEARNED, culture amendments). Only the pace of change differs: values change rarely and deliberately, like a constitution; practice changes constantly and cheaply, like working notes; intent changes at real pivots. Keep each file's pace; don't constitutionalize your notes or scribble on the constitution.
3. Why this exists (the failure it fixes)
Seat knowledge needs a durable home that successors and other runtimes can read.
A private runtime memory or an urgent handover packet alone can omit standing
duties and the reasons behind a practice. Keep general operating craft in the
shared skill and position-specific knowledge in the seat's LEARNED.md chain.
At a handover, read the current chain alongside the packet and record any missing
job context before claiming readiness. Each occupant maintains what the seat has
learned; the handover packet carries the current transition.
4. LEARNED.md — the living file (all altitudes; seat shown)
Sections, in order — see templates/LEARNED.md:
- Header — coords · which SOP it refines · updated date (from the clock command, never from memory).
- MY JOB HERE — this instance's actual function, in plain operational terms.
- STANDING DUTIES — every recurring duty, each with its rhythm and where it happens. Duties listed anywhere else die at the next handover; this section is why they survive.
- HOW I WORK — practices learned on the job, each with its reason. A practice with its why can be re-judged when the world changes; a bare rule outlives its reason and gets misapplied.
- GATES & AUTHORITIES — what this instance may decide alone, what it must never assume. Authority exists in writing or not at all.
- KEY RELATIONSHIPS — who it hands to, who reviews it, who it reports to.
- TRIGGER POINTERS — "when X happens, read Y." Attach pointers to the moments that need them; lists of boot-time reading decay.
- LESSONS — dated, newest first. Periodically distill old entries into HOW I WORK or drop them.
Size: soft guidance, not a rule. Keep it as small as honestly covers the job — attention is the budget, and every reader pays it. Some seats genuinely need more; write what the job needs. The failure mode to watch for is the rulebook that only ever grows. Distill lessons into practices with reasons at deposit boundaries (before clear or handover); use that habit, not a line count.
5. The trace — deliberate reorientation
A trace is walking your chains with your own file reads and writing down
where you stand: what am I doing (queue/NOTES) → under what contract (SPEC) →
toward what intent (SPEC.md intent field, leaf to root) → by what practice (LEARNED +
SOP) → within what values (CULTURE). A few written lines at the end.
scripts/compose.py up assembles any chain for you.
Two rules give the trace its value:
- Your reads are the trace. A trace written from memory is a recitation — if you didn't open the files, you didn't trace, and you will confidently re-derive whatever drift you already have.
- Report broken links; never obey them. A missing file, a stale date, an intent that contradicts observable reality — say so to the level that owns it. That is how the chains stay true: they are audited by being used. And the chains inform decisions; they never enforce anything by themselves — a stale map must never be able to block true work.
When to trace — one principle: trace when enough has changed that your
picture of where you stand may be stale — after a large stretch of work, at a
boundary (boot, handover, new mission, confusion), or when someone asks you to
reorient. Why not simply "every N hours": identical scheduled prompts fade
from an agent's attention with repetition, and idle seats accumulate ritual
traces that crowd out real context.
Where a schedule fits your context anyway, use one — but prefer gating the
action on evidence of change: scripts/trace-due.sh decides "has enough
happened since my last trace?" deterministically and stays silent when the
answer is no, so a scheduler can fire it as often as it likes.
5b. Trace and rig walk — one idea, two ends
These get confused because they share a word. They are not in conflict; they are the same thing seen from either end, and the relationship is worth holding.
A chain is an ordered sequence of context meant to be absorbed one piece at a time. Two kinds exist and both are chains in that sense:
- Altitude chain — the same filename at every level of a tree, read by ascending
(
LEARNED.md;SPEC.mdwithintent:). The sequence is position: leaf → root. - Boot chain — a seat's startup reading sequence. The sequence is order of onboarding.
A chain can be traversed two ways, and that is the only real difference:
| who drives | mechanism | when | |
|---|---|---|---|
| PULL | the agent | compose.py up renders the chain; the agent reads it |
it is awake and oriented enough to look — a trace, a refocus |
| PUSH | an orchestrator | rig walk --through <files> --pace <n> sends one piece at a time into the pane |
it cannot self-start — freshly cleared, re-primed, cold |
Pacing is the mechanism in both directions, and it is the load-bearing part. Absorption
between pieces is what makes a chain land; a concatenated dump of the same bytes is a failed
delivery regardless of content. That is why rig walk elapses --pace between pieces, and why a
composed render is meant to be read as a sequence rather than skimmed as a wall.
Use distinct names for the two directions:
- TRACE — the pull ascent.
compose.py uprenders a node's chain to the root. rig walk— the push verb for paced delivery into a seat's pane.
Use refocusing for the current trace workflow. A delivery receipt and an
agent's read-depth report answer different questions; pacing alone does not
establish understanding.
6. Writing — two principles
- Your LEARNED.md is yours. You write it, in your own words, as part of doing the job — when you learn something about how to do your work, the file carries it before you move on. When anyone else wants it changed (a correction, new doctrine), they tell you and you write it — knowledge someone else typed into your file was never yours. The one exception: when an instance is empty or broken, whoever is responsible for it writes what's needed, marks those lines as written-for-the-instance, and the next occupant rewrites them in its own words.
- Shipped things belong to their authors.
openrig-core's operating-model skill and the shipped culture change through their owners, never by an instance editing in place. If it is wrong for everyone, propose the change to its owner; if it is wrong for you, that's what LEARNED.md is for.
Everywhere: date what you write (from the clock), and correct by adding a dated correction rather than silently rewriting history.
7. Composed views
Any node can be rendered: its chains assembled into one document
(scripts/compose.py; up = your effective view from a leaf, down = every
chain file under a root — run down at a tree root and you get the whole
operating picture in one document). Two rules:
- Rendered documents are generated, never edited. The chain files are the source of truth; a render is a snapshot view of them.
- Every chain render opens with a TRACE — a tree-shaped orientation
header derived at render time (never stored in any file): one line per
level showing its state (seeded ✓ / unseeded ⟂ / stale-marker ⚠ / absent ✗)
and a "you are here" anchor at the leaf. It shows the shape of your context
the way
treeshows the shape of a directory — where you sit, and where the screams are, before you read a word of content. - When an approval must freeze exact content (for example a plan-lock on a spec), it records the hash of a render — the frozen bytes live in the approval record while the chain files stay live for reading and revision.
The root render + diff is how a high-altitude seat keeps a current mental
model of a changing fleet without reading everything: render the tree root,
diff against your previous render, read only the diff
(scripts/trunk-diff.sh).
8. Where knowledge goes — the placement rule
Context lives at the narrowest scope that needs it; skills are only for what has no scope. Seat-specific knowledge → that seat's LEARNED.md. Rig practice → the rig's files. Only truly scope-free craft (useful to any agent anywhere) belongs in the skill layer.
The one axis that decides file-vs-plugin: KIND or POSITION
- A chain holds knowledge about a POSITION — this seat, this mission. Unshareable by construction, because the path is the identity.
- A plugin holds knowledge about a KIND — a seat-type's job, a domain's craft, an operating model. Shareable, versionable, cross-harness.
A plugin cannot hold LEARNED. Plugins are shared; LEARNED is per-instance. Two rigs installing the same orchestrator plugin must not share what one seat learned about its own merge desk. That is why chains exist alongside plugins rather than being replaced by them.
And plugins are the distribution mechanism, which imposes a hard test. A plugin ships skills;
a skill may ship a script; that script must run unmodified on a stranger's machine. So it
resolves paths from configuration (rig config get workspace.root), never from a literal, and it
never asks the agent to work out which of several candidate directories is the real one. If a user
points their workspace at a git repo or a projects/ folder, everything keeps working with no
further setup. A script that only works for its author is not shippable, however correct it is.
What belongs INSIDE a node — two tests before a sentence goes in
A node holds what is true of THIS position and stays true.
- Portability. If a sentence would read correctly on a stranger's machine, it is knowledge about a KIND. It belongs in this skill or a plugin, not on the tree. A node body that is fully portable is documentation that wandered onto the chain.
- Volatility. A value that changes faster than the file gets edited — a SHA, a count, a status, a roster — goes in as the command that derives it, never as the answer.
Then check which tree. Traps, practice and how-we-work are position knowledge on the topology
tree (LEARNED.md). What is being built is the work tree (SPEC.md). One rig owns both; they
still do not mix.
Keep specification depth proportional. A project carries stable intent; a
mission carries its intent and mission-level specification in the same authored
SPEC.md. Slices provide their concrete implementation scope. Follow the selected
mission-slice-sop and any explicit mode overlay; mission specification is not a
requirement to repeat every slice detail.
The other axis: AUDIENCE is not MATURITY
These are independent, and merging them is seductive because the merged version is prettier.
- Audience decides WHERE knowledge lives — which altitude, which file. Something belongs at pod level because anyone in that pod benefits. Full stop.
- Maturity is an attribute of a LINE inside that file — the epistemic ladder: data →
observation → field note → insight → canon. The vocabulary already ships as the
stage:enum (wip | provisional | established | canonical | superseded | retired).
A pod-level item can be raw observation; a seat-level one can be canon. A skill can contain something immature and still be the right home, because audience picked the file.
So promotion to canon is NOT a move up the tree. It is a maturity event and can happen at any altitude — a seat-level observation that is universally true graduates straight to a skill. What earns maturity is evidence: recurrence, independent corroboration, a measured cost, surviving change, surviving an attempt to falsify it. Facts about mechanisms can skip the ladder (backticks substitute in double-quoted shell strings is one command away); inferences about practice must accrue (never broadcast to a large rig took an incident).
LEARNED.md is not a staged item — it is the bed everything lies in. Its gradient is positional: the dated append-log at the bottom is raw observation, the concise sections at the top are what survived. Attach distillation to a trigger: distil at deposit boundaries (pre-clear, pre-handover) where a write is already required and the author still remembers why each line exists; refocus merely notices when the log has outgrown the distilled part.
9. Standing the structure up
scripts/scaffold.sh creates any missing chain files from templates/ and
never overwrites or deletes anything — so a brand-new workspace and a
living system are the same command with different starting states. Created
files are marked UNSEEDED until their real owner writes the first true
version. Never mass-produce LEARNED.md content for other instances — each
instance writing its own first version is both how the knowledge becomes real
and how you discover which seats cannot yet describe their own job.
10. Pitfalls (only what isn't taught above)
- Editing a rendered document instead of its chain files — your edit is lost at the next render, silently.
- "Improving" the trace with pointer-following, serves-resolution, or any branching — the trace's entire value is that path-only ascent cannot fail subtly; keep the trace path-only.
- Summarizing this model for another agent instead of pointing them here — a summary becomes another copy that can drift from its source.
- Scripts in
scripts/are macOS-flavored in places (stat -f); verify platform compatibility before trusting them on another OS.
Files in this skill
templates/{SPEC,LEARNED,SOP}.md ·
scripts/{compose.py, trace-due.sh, trace-stamp.sh, trunk-diff.sh, scaffold.sh}
Files (openrig)
-
scripts
-
compose.py 28.7 KB
#!/usr/bin/env python3 """compose.py — leaf-to-root chain composition and subtree renders. SUPERSEDED FOR REFOCUS: the public core reorientation trace lives at ../../refocusing/scripts/trace-to-root.py. This general composer remains the compatibility surface for down/progress and historical up invocations. up <start-dir> --name FILE [--name FILE ...] [--root DIR] Walk from start-dir up to root (default: filesystem stops at a dir containing .compose-root, or at --root). Emit the chain ROOT-FIRST (defaults, then overrides) with provenance headers, and a MISSING-LINK report for altitudes without the file — the scream is output, not error. down <root-dir> --name FILE [--name FILE ...] [--exclude GLOB ...] Gather every instance of the named files under root-dir. progress <root-dir> --name FILE [--name FILE ...] [--exclude GLOB ...] THE DERIVED PROGRESS VIEW (the PROGRESS.md prototype done right): count markdown checkboxes in the named files (the mark level), roll counts UP the tree, and print a walk-map-shaped tree with done/total per level. Never stored in any file — the render is the only home. Gather every instance of the named files under root-dir (the subtree render; run at the topology root = THE TRUNK RENDER). Sorted by path. Composed output is GENERATED, NEVER EDITED (fragments are the source of truth). Seals/locks bind to a render's bytes, not to fragments. Stdlib only. """ import argparse, os, sys, fnmatch, datetime, re, shutil, subprocess # A SHELF holds instances of an altitude; it is not a position and carries no chain file. # Both trees have them: missions/ slices/ (work) and rigs/ pods/ seats/ (topology). Without # this list the trace reports every shelf as a gap and the audit tells you to scaffold one — # which is litter that inflates the map to a clean-looking 5/5 while adding nothing. Extend # with --shelf for a tree that names them differently. SHELF_NAMES = {"missions", "slices", "seats", "pods", "rigs"} _TPL_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "templates") def _template_bytes(name): try: return open(os.path.join(_TPL_DIR, name), "rb").read() except OSError: return None def read_state(path): """Detection is DETERMINISTIC-FIRST, declared-second — never memory-reliant: 1. byte-identical to the shipped template -> 'unseeded' (nobody had to remember anything) 2. `status: UNSEEDED` marker on content that DIFFERS from the template -> 'conflicted': render the content AND scream — real work is never hidden behind a stale field, and the disagreement is its own reported state (third-state law). 3. otherwise -> 'seeded'. Every residual failure mode shows too much plus a scream, never hides work and never silently trusts a field.""" raw = open(path, "rb").read() tpl = _template_bytes(os.path.basename(path)) body = raw.decode("utf-8", errors="replace") head = "\n".join(body.splitlines()[:15]) marked = re.search(r"^status:\s*UNSEEDED", head, re.M) if tpl is not None and raw == tpl: m = re.search(r"^owners?:\s*(.+)$", head, re.M) return "unseeded", (m.group(1).strip() if m else "owner unknown — untouched scaffold") if marked: return "conflicted", body return "seeded", body def hdr(title): return f"\n\n<!-- ═══ {title} ═══ -->\n\n## ⟦{title}⟧\n" def frontmatter(payload): """Parse the frontmatter block ONCE. Returns (dict, note); note is set when there is nothing to read, so a caller can report the gap rather than silently skip it.""" if not payload: return None, "no content" m = re.match(r"^---\n(.*?)\n---\n", payload, re.S) if not m: return None, "no frontmatter" try: import yaml return (yaml.safe_load(m.group(1)) or {}), None except Exception as e: return None, f"frontmatter did not parse ({e.__class__.__name__})" def field_of(payload, field): """Extract ONE frontmatter field. Returns (value, note). A chain walked by FIELD composes intent without dragging bodies: three sentences unfurl, not three documents. A level whose file exists but lacks the field is a real gap and is reported as such — never silently skipped.""" fm, note = frontmatter(payload) if fm is None: return None, note if field not in fm: return None, f"no `{field}:` field" v = fm[field] return (" ".join(str(v).split()) if v is not None else None), None def _cut_at_boundary(s, n): """Cut to at most n characters without splitting a token. Prefer paragraph, line, then word boundaries. Drop a single unbroken token that exceeds the budget; a partial path or identifier looks valid but is not.""" if len(s) <= n: return s head = s[:n] if s[n:n + 1].isspace(): # n already sits on a boundary return head.rstrip() for sep, floor in (("\n\n", n // 2), ("\n", n // 2), (" ", 0)): i = head.rfind(sep) if i > floor: return head[:i].rstrip() return "" # one unbroken token — drop it, never cut into it def _short(s, n=52): s = " ".join(str(s).split()) if len(s) <= n: return s return _cut_at_boundary(s, n - 1).rstrip(" ,;:.-—") + "…" _ARTIFACT_KEYS = ("outputs", "output", "artifacts", "artifact") def dep_artifacts(dep_dir, names, dep_fm): """WHAT THE DEPENDENCY HANDS YOU — the file, not the fact that it is done. Frontmatter first if the node declares it (`outputs:`/`output:`/`artifacts:`/`artifact:`), because a declaration outranks a guess. Otherwise the directory itself: everything beside the node file, which for a built slice IS its output (MAP.md, PROOF.md, the script). No prose parsing — a `## Output` section is written for humans and reading it would make the trace confidently wrong exactly where it is trying to stop being that.""" for k in _ARTIFACT_KEYS: v = (dep_fm or {}).get(k) if v: return [" ".join(str(x).split()) for x in (v if isinstance(v, list) else [v])], "declared" try: entries = sorted(os.listdir(dep_dir)) except OSError: return [], "unreadable" out = [e + ("/" if os.path.isdir(os.path.join(dep_dir, e)) else "") for e in entries if not e.startswith(".") and e not in set(names)] return out, "on disk" def blocking_state(payload, lvl, names): """Return leaf status and dependency facts beside its intent, or None. Intent alone does not say whether work may start or where dependency outputs live. Resolve dependencies on disk and report state, absolute path, and artifacts; leave ancestors to carry purpose.""" fm, _ = frontmatter(payload) if not fm: return None parts = [] if fm.get("status") is not None: parts.append(f"status: {_short(fm['status'], 88)}") deps = fm.get("depends") if deps: shelf = os.path.dirname(lvl) lines = ["depends:"] for dep in (deps if isinstance(deps, list) else [deps]): dep = " ".join(str(dep).split()) dep_dir = os.path.join(shelf, dep) if not os.path.isdir(dep_dir): lines.append(f" {dep} — ⚠ UNRESOLVED: no directory `{dep}` under {shelf}" f" (nothing to point you at — check the id)") continue state, dfm = None, None for n in names: cand = os.path.join(dep_dir, n) if os.path.isfile(cand): st, body = read_state(cand) dfm = frontmatter(body)[0] if st != "unseeded" else None state = _short((dfm or {}).get("status") or st) break lines.append(f" {dep} ({state})" if state else f" {dep} (⚠ no {'/'.join(names)} — state unknown)") lines.append(f" path: {os.path.abspath(dep_dir)}") arts, how = dep_artifacts(dep_dir, names, dfm) if arts: shown = arts[:8] more = f" (+{len(arts) - 8} more)" if len(arts) > 8 else "" lines.append(f" artifacts ({how}): " + " · ".join(shown) + more) else: lines.append(f" artifacts: ⚠ none found — the dependency has produced no" f" output file yet") parts.append("\n".join(lines)) # lines 2+ carry their own indent; the caller indents line 1 return "\n ".join(parts) or None def operates_on(payload): """Return the roots a work node declares it operates on. The tree being walked need not be the tree the work changes. This cannot be inferred safely, so render `operates_on` verbatim and omit it when unstated.""" fm, _ = frontmatter(payload) if not fm: return None roots = fm.get("operates_on") if not roots: return None return [" ".join(str(r).split()) for r in (roots if isinstance(roots, list) else [roots])] # Preserve contract and scope sections ahead of rationale and history when the # leaf body exceeds its cap. Unknown headings remain middle priority so new shapes # degrade without being silently privileged or discarded. _PRI_CONTRACT = re.compile( r"(done when|what done looks like|scope fence|outputs?\b|inputs?\b|depends on|" r"what must be built|how we will know|acceptance|payload|deliverable)", re.I) _PRI_DISCUSSION = re.compile( r"^(why\b|the problem|the evidence|the finding|result\b|also\b|related\b|triage\b|" r"background\b|history\b|what is already done|new evidence|measured\b|answered\b|" r"where this spec was wrong|notes for|the counter-argument|the asset|the mechanism|" r"this mission is the acceptance criteria)", re.I) def _priority(heading): """0 = preamble (never dropped) · 1 = contract · 2 = unclassified · 3 = discussion.""" if heading is None: return 0 if _PRI_CONTRACT.search(heading): return 1 if _PRI_DISCUSSION.match(heading): return 3 return 2 def _sections(body): """Split a node body at LEVEL-2 headings into [(heading|None, chunk), ...]. Chunk 0 is the preamble — the `# Title` and its lede — and carries heading None. `###` stays inside its parent section: in these specs it is a sub-step of the section above it, and splitting there would let half a section survive its own heading.""" starts = [m.start() for m in re.finditer(r"^## +.*$", body, re.M)] if not starts: return [(None, body)] out = [(None, body[:starts[0]])] for i, s in enumerate(starts): chunk = body[s:starts[i + 1] if i + 1 < len(starts) else len(body)] out.append((chunk.splitlines()[0].lstrip("# ").strip(), chunk)) return out def leaf_body(payload, cap): """Render the leaf body, truncating by semantic priority rather than position. Ancestors can compose by stable intent fields, but the leaf carries the nouns, boundaries, outputs, and completion conditions. When over budget, retain contract and scope sections before explanatory history.""" if not payload: return None m = re.match(r"^---\n.*?\n---\n", payload, re.S) body = (payload[m.end():] if m else payload).strip() if not body: return None if len(body) <= cap: return body secs = _sections(body) if len(secs) == 1: # headingless prose — nothing to prioritise kept = _cut_at_boundary(body, cap) return (kept + "\n\n[… " + str(len(body) - len(kept)) + " more chars, no further " "headings — prose tail]" + "\n(read the node file for these)") # Drop the lowest-priority, largest sections first to preserve more short, # decisive sections. The title and lede preamble are never candidates. keep, size = set(range(len(secs))), len(body) for i in sorted(range(1, len(secs)), key=lambda i: (-_priority(secs[i][0]), -len(secs[i][1]))): if size <= cap: break keep.discard(i) size -= len(secs[i][1]) kept = "".join(secs[i][1] for i in sorted(keep)).rstrip() if len(kept) > cap: # preamble alone overflows; cut it safely, never mid-token kept = _cut_at_boundary(kept, cap) dropped = [secs[i][0] for i in range(1, len(secs)) if i not in keep] # Name omitted headings so truncation is visible to the reader. tail = ("\n\nOMITTED FROM THIS RENDER — " + str(len(body) - len(kept)) + " chars, sections: " + " · ".join(dropped) if dropped else "\n\n[… " + str(len(body) - len(kept)) + " more chars, no further headings — prose tail]") return kept + tail + "\n(read the node file for these)" def resolve_roots(): """Resolve work and topology roots from configuration or explicit environment. A library copy may derive topology from its own real path; a projected plugin copy must be configured rather than guessing. Resolution follows environment, configuration, then derived location.""" out = {} out["work_root"] = os.environ.get("OPENRIG_WORKSPACE_ROOT") or rig_config("workspace.root") t = os.environ.get("OPENRIG_TOPOLOGY_ROOT") or rig_config("workspace.topology_root") if not t: # Resolve symlinks before walking upward; otherwise a projection ascends # its own tree rather than the shared-docs source. here = os.path.realpath(__file__) for _ in range(6): # scripts/ skill/ skills/ skill-canon/ -> shared-docs/ here = os.path.dirname(here) cand = os.path.join(here, "rigs") if os.path.isdir(cand): t = cand break out["topology_root"] = t return out def rig_config(key): if not shutil.which("rig"): return None try: p = subprocess.run(["rig", "config", "get", key], capture_output=True, text=True, timeout=10) v = (p.stdout or "").strip() return v if p.returncode == 0 and v else None except Exception: return None def compose_up(start, names, root, field=None, prefer=False, shelves=None, leaf_cap=2400): shelves = SHELF_NAMES | set(shelves or ()) start = os.path.abspath(start); root = os.path.abspath(root) if root else None levels = [] d = start while True: levels.append(d) if root and os.path.samefile(d, root): break if not root and os.path.exists(os.path.join(d, ".compose-root")): break parent = os.path.dirname(d) if parent == d: break d = parent levels.reverse() # root first: defaults, then overrides out = [f"<!-- GENERATED by compose.py up · {datetime.datetime.now().isoformat(timespec='minutes')} -->", f"<!-- start: {start} · altitudes: {sum(1 for d in levels if os.path.basename(d) not in shelves)} ({len(levels)} path segments) · chains: {', '.join(names)}" + (f" · FIELD: {field}" if field else "") + " -->", "<!-- Composed view — never edit; edit the fragments. -->"] GLYPH = {"seeded": "✓ seeded", "unseeded": "⟂ UNSEEDED", "conflicted": "⚠ stale marker", "absent": "✗ absent", "shelf": "· shelf"} missing, unseeded = [], [] # --prefer turns the --name list into ONE chain resolved per level by precedence # (first match wins) instead of N independent chains. A tree mid-rename is the normal # case, not an exception: SPEC.md is the current node filename and README.md the legacy # one, and a trace that cannot span both reports a chain as broken when it is merely mixed. # Validate the start node against configured tree roots. Merely sitting below # workspace.root is insufficient because fixtures may look structurally valid. _cfg = [c for c in (rig_config("workspace.slices_root"), resolve_roots().get("topology_root")) if c] _here = os.path.abspath(start) _root_warning = None if _cfg and not any(_here == os.path.abspath(c) or _here.startswith(os.path.abspath(c) + os.sep) for c in _cfg): _root_warning = (" ⚠ THIS IS NOT A CONFIGURED TREE. Trees on this instance: " + " · ".join(_cfg) + "\n A walk renders just as cleanly from a stray or" " fixture tree — verify before acting on it.") groups = [list(names)] if prefer else [[n] for n in names] for group in groups: name = group[0] out.append(f"\n\n# CHAIN: {' → '.join(group)} (root → leaf)" + (" · precedence: first match wins per level" if len(group) > 1 else "")) # pass 1 — collect the state of every level (this IS the trace; the map derives from it) chain, hits = [], {} for lvl in levels: p = None for n in group: cand = os.path.join(lvl, n) if os.path.isfile(cand): p, hits[lvl] = cand, n break where = os.path.relpath(lvl, levels[0]) or '.' # A SHELF is a path segment, not an altitude. The chain has three rungs — project, # mission, slice — and rendering `missions/` and `slices/` as rungs implies five. # Any file living at a shelf is ordinary folder documentation, NOT a chain link: # ignore it rather than flagging it. `README.md` does double duty as a legacy node # name and as a plain readme, and only the level it sits at can tell them apart. is_shelf = os.path.basename(lvl) in shelves if is_shelf: state, payload, p = "shelf", None, None hits.pop(lvl, None) elif p: state, payload = read_state(p) else: state, payload = "absent", None # Resolve the field HERE so the map can tell the truth. A map keyed on file # presence says ✓ for a level that composes nothing — the exact reading that # lets a trace certify the drift it exists to catch. fval, fnote = (field_of(payload, field) if (field and state == "seeded") else (None, None)) chain.append((lvl, where, state, payload, fval, fnote)) # TRACE — derived orientation tree (never stored in files; the render is the only home) # The map shows ALTITUDES as rungs; shelves fold into the next rung's path so the tree # depth equals the number of levels that actually carry intent. rungs, pending = [], [] for idx, row in enumerate(chain): if row[2] == "shelf" and idx != len(chain) - 1: pending.append(os.path.basename(row[0]) or row[0]) continue label = "/".join(pending + [os.path.basename(row[0]) or row[0]]) rungs.append((label, row)); pending = [] leads = [("" if i == 0 else " " * (i - 1) + "└─ ") + rungs[i][0] + "/" for i in range(len(rungs))] width = max(len(l) for l in leads) + 2 # Name the walked root and chain so readers do not confuse the tree being # shown with the tree the work will change. out.append(f"\nTRACE · root {levels[0]} · chain {' → '.join(group)}") # Warn on an unconfigured root: an arbitrary lookalike tree can render a # clean chain. if _root_warning: out.append(_root_warning) for i, (_label, (lvl, where, state, payload, fval, fnote)) in enumerate(rungs): note = f" (owner: {payload})" if state == "unseeded" and payload else "" here = " ← you are here" if i == len(rungs) - 1 else "" glyph = f"⟂ no {field}" if (field and state == "seeded" and not fval) else GLYPH[state] # In precedence mode, name WHICH file answered at each level — a mixed tree is # readable state, not noise, and hiding it is how a rename looks like corruption. via = f" [{hits[lvl]}]" if len(group) > 1 and lvl in hits and state != "shelf" else "" out.append(f" {leads[i]:<{width}}{glyph}{via}{note}{here}") # pass 2 — contents, states collapsed per the three-state rules found = 0 for lvl, where, state, payload, fval, fnote in chain: if state == "shelf": continue # a path segment, not a position — contributes nothing, flags nothing elif state == "unseeded": out.append(f"\n⟂ {name} @ {where} — UNSEEDED (scaffold only, not rendered; seed it: {payload})") unseeded.append(f"{name} @ {where} (owner: {payload})") elif state == "conflicted": found += 1 out.append(hdr(f"{name} @ {where} — ⚠ carries status: UNSEEDED but content differs from template")) out.append(payload.rstrip()) unseeded.append(f"{name} @ {where} — MARKER/CONTENT DISAGREE: rendered anyway; owner should clear the stale status line") elif state == "seeded": found += 1 if field: if fval: out.append(f"\n{where}: {fval}") else: out.append(f"\n{where}: ⚠ {fnote}") unseeded.append(f"{name} @ {where} — {fnote}") # The leaf is the only level whose answer is an ACTION. Compose its # blocking state here or the walk certifies the next step it never read. if lvl == chain[-1][0]: blocked = blocking_state(payload, lvl, group) if blocked: out.append(f" {blocked}") roots = operates_on(payload) if roots: out.append(" operates on: " + " · ".join(roots)) # ...and the only altitude that SPECIFIES. See leaf_body(). lb = leaf_body(payload, leaf_cap) if leaf_cap else None if lb: out.append(f"\n─── {where} · node body ───") out.append(lb) else: out.append(hdr(f"{name} @ {where}")) out.append(payload.rstrip()) else: missing.append(f"{name} @ {where}") if not found: out.append(f"\n⚠ NO seeded instances of {name} on this walk.") if unseeded or missing: out.append("\n\n# ⚠ CHAIN AUDIT (the walk screams once, here — never by breaking, never by template litter)") if unseeded: out.append("UNSEEDED (file exists, template only — the owner writes the first true version):") out += [f"- {m}" for m in unseeded] if missing: out.append("ABSENT (no file at a POSITION — scaffold it, or report to the level owner if absence looks wrong):") out += [f"- {m}" for m in missing] return "\n".join(out) def compose_down(root, names, excludes): root = os.path.abspath(root) hits = [] for dirpath, dirs, files in os.walk(root): dirs[:] = [x for x in dirs if not x.startswith(".") and x != "node_modules" and not any(fnmatch.fnmatch(os.path.join(dirpath, x), g) for g in excludes)] for name in names: if name in files: hits.append(os.path.join(dirpath, name)) hits.sort() out = [f"<!-- GENERATED by compose.py down · {datetime.datetime.now().isoformat(timespec='minutes')} -->", f"<!-- root: {root} · files: {len(hits)} · chains: {', '.join(names)} -->", "<!-- Subtree render (trunk render at a tree root). Never edit. -->"] unseeded = [] for p in hits: state, payload = read_state(p) rel = os.path.relpath(p, root) if state == "unseeded": unseeded.append(f"{rel} (owner: {payload})") elif state == "conflicted": out.append(hdr(f"{rel} — ⚠ stale UNSEEDED marker on real content")) out.append(payload.rstrip()) unseeded.append(f"{rel} — MARKER/CONTENT DISAGREE: rendered anyway; clear the stale status line") else: out.append(hdr(rel)) out.append(payload.rstrip()) if unseeded: out.append("\n\n# ⚠ UNSEEDED under this root (scaffold only — collapsed, not rendered)") out += [f"- {u}" for u in unseeded] if not hits: out.append("\n⚠ no chain files under this root — unfurl first?") return "\n".join(out) def compose_progress(root, names, excludes): import collections root = os.path.abspath(root) direct = collections.defaultdict(lambda: [0, 0]) # dir -> [done, total] from its own files for dirpath, dirs, files in os.walk(root): dirs[:] = [x for x in dirs if not x.startswith(".") and x != "node_modules" and not any(fnmatch.fnmatch(os.path.join(dirpath, x), g) for g in excludes)] for name in names: if name in files: txt = open(os.path.join(dirpath, name), encoding="utf-8", errors="replace").read() done = len(re.findall(r"^\s*[-*] \[[xX]\]", txt, re.M)) open_ = len(re.findall(r"^\s*[-*] \[ \]", txt, re.M)) if done + open_: direct[dirpath][0] += done direct[dirpath][1] += done + open_ agg = collections.defaultdict(lambda: [0, 0]) # dir -> rolled-up [done, total] for d, (dn, tt) in direct.items(): cur = d while True: agg[cur][0] += dn; agg[cur][1] += tt if os.path.samefile(cur, root): break cur = os.path.dirname(cur) out = [f"<!-- GENERATED by compose.py progress · {datetime.datetime.now().isoformat(timespec='minutes')} -->", "<!-- Derived from the mark level at render time. Never store this in a file. -->", f"\nPROGRESS MAP ({', '.join(names)} — checkbox = the one mark level; everything above derives):"] if not agg: out.append(" (no checkboxes found in the named files under this root — nothing is marked here)") return "\n".join(out) keys = sorted(agg, key=lambda d: os.path.relpath(d, root)) leads = [] for d in keys: rel = os.path.relpath(d, root) depth = 0 if rel == "." else rel.count(os.sep) + 1 label = os.path.basename(root) if rel == "." else os.path.basename(d) leads.append(("" if depth == 0 else " " * (depth - 1) + "└─ ") + label) width = max(len(l) for l in leads) + 2 for i, d in enumerate(keys): dn, tt = agg[d] pct = 100 * dn // tt if tt else 0 glyph = "✓" if dn == tt else ("◐" if dn else "○") own = " ·" if d in direct else " " # · = has marks of its own (vs pure roll-up) out.append(f" {leads[i]:<{width}}{glyph} {dn}/{tt} ({pct}%){own}") return "\n".join(out) if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("mode", choices=["up", "down", "progress", "roots"]) ap.add_argument("path", nargs="?") ap.add_argument("--name", action="append") ap.add_argument("--root", default=None) ap.add_argument("--prefer", action="store_true", help="treat --name as a PRECEDENCE list (first match wins per level) rather " "than separate chains — for a tree mid-rename: --name SPEC.md --name README.md --prefer") ap.add_argument("--exclude", action="append", default=[]) ap.add_argument("--field", default=None, help="compose ONE frontmatter field up the chain (e.g. --field intent) instead of whole bodies") ap.add_argument("--leaf-cap", type=int, default=2400, metavar="N", help="with --field: also render the LEAF's body, capped at N chars (0 = off). " "Ancestors compress; the leaf is where the nouns are.") ap.add_argument("--shelf", action="append", default=[], help=f"extra directory name that HOLDS instances rather than being a position, so " f"its absence of a chain file is correct (defaults: {', '.join(sorted(SHELF_NAMES))})") a = ap.parse_args() if a.mode == "roots": r = resolve_roots() for k in ("work_root", "topology_root"): v = r.get(k) if v: print(f"{k}={v}") else: key = "workspace.root" if k == "work_root" else "workspace.topology_root" print(f"# {k} UNRESOLVED — set it: rig config set {key} <path>", file=sys.stderr) sys.exit(0 if all(r.values()) else 3) if not a.name: ap.error("--name is required for up/down/progress") if a.mode == "up": print(compose_up(a.path, a.name, a.root, a.field, prefer=a.prefer, shelves=a.shelf, leaf_cap=a.leaf_cap)) elif a.mode == "down": print(compose_down(a.path, a.name, a.exclude)) else: print(compose_progress(a.path, a.name, a.exclude)) -
scaffold.sh 2.1 KB
#!/usr/bin/env bash # scaffold.sh topology <root> --node <relpath> [--node <relpath> ...] # scaffold.sh work <root> --node <relpath> [--node <relpath> ...] # Creates missing chain files from templates at root + each named node dir. # NEVER overwrites an existing file (brownfield-safe by construction; greenfield # and brownfield are the same command with different starting states). # Never deletes anything. Reports created vs existing. set -euo pipefail MODE="${1:?usage: scaffold.sh <topology|work> <root> --node <relpath> ...}" ROOT="${2:?root dir required}"; shift 2 HERE="$(cd "$(dirname "$0")" && pwd)"; TPL="$HERE/../templates" case "$MODE" in # Chains carry knowledge about a POSITION. SOP.md is knowledge about a KIND and now ships # in the rig's mode plugin (openrig-lab | openrig-factory | openrig-hq) — it is NOT a chain # and must not be scaffolded onto the tree. CULTURE.md ships with OpenRig. topology) FILES=(LEARNED.md) ; ROOT_FILES=(LEARNED.md) ;; # SPEC.md is the authored node (intent: in frontmatter). NOTES.md is the LIVED file — the # scaffold creates its surface only; lived entries are never generated or projected. PROGRESS is derived; # PROOF.md is authored by the prover when there is something to prove. work) FILES=(SPEC.md NOTES.md) ; ROOT_FILES=(SPEC.md NOTES.md) ;; *) echo "mode must be topology|work" >&2; exit 2 ;; esac NODES=() while [[ $# -gt 0 ]]; do case "$1" in --node) NODES+=("$2"); shift 2;; *) echo "unknown arg $1" >&2; exit 2;; esac; done place(){ # $1=dir $2=file mkdir -p "$1" if [[ -e "$1/$2" ]]; then echo " exists : $1/$2" else cp "$TPL/$2" "$1/$2"; echo " CREATED: $1/$2 (template — UNSEEDED; the real owner seeds it)"; fi } echo "scaffold $MODE @ $ROOT" touch "$ROOT/.compose-root" 2>/dev/null || true for f in "${ROOT_FILES[@]}"; do place "$ROOT" "$f"; done for n in ${NODES[@]+"${NODES[@]}"}; do for f in "${FILES[@]}"; do place "$ROOT/$n" "$f"; done; done echo "done. Rules: existing files untouched; UNSEEDED templates await their owners; seat LEARNED.md is SELF-seeded (the audit) — never bulk-filled." -
trace-due.sh 1.5 KB
#!/usr/bin/env bash # trace-due.sh <seat-dir> [work-dir ...] # Deterministic due-check for the alignment trace. Exit 0 + reason when DUE; # exit 1 SILENTLY when not (the no-op path is the point: schedulers may fire # this on cadence; the ACTION stays evidence-gated — fixed-cadence traces were # empirically falsified; see SKILL.md §4). set -euo pipefail SEAT_DIR="${1:?usage: trace-due.sh <seat-dir> [work-dir ...]}"; shift || true STAMP="$SEAT_DIR/.last-trace" MIN_HOURS="${TRACE_MIN_HOURS:-6}" # never due more often than this FORCE_HOURS="${TRACE_FORCE_HOURS:-72}" # due regardless of activity after this now=$(date +%s) if [[ ! -f "$STAMP" ]]; then # never traced: due only if the seat shows any life at all if [[ -n "$(find "$SEAT_DIR" "$@" -type f -newermt '-7 days' -print -quit 2>/dev/null)" ]]; then echo "DUE: no trace on record and recent activity exists"; exit 0 fi exit 1 fi last=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP") # BSD then GNU; date -r means different things on the two OSes age_h=$(( (now - last) / 3600 )) (( age_h < MIN_HOURS )) && exit 1 if (( age_h >= FORCE_HOURS )); then echo "DUE: ${age_h}h since last trace (force threshold ${FORCE_HOURS}h)"; exit 0 fi # due only if meaningful work accumulated since the stamp for d in "$SEAT_DIR" "$@"; do if [[ -n "$(find "$d" -type f -newer "$STAMP" ! -name '.last-trace' -print -quit 2>/dev/null)" ]]; then echo "DUE: ${age_h}h since last trace and new activity under $d"; exit 0 fi done exit 1 -
trace-stamp.sh 231 B
#!/usr/bin/env bash # trace-stamp.sh <seat-dir> — record that a trace was performed now. set -euo pipefail touch "${1:?usage: trace-stamp.sh <seat-dir>}/.last-trace" echo "stamped: $1/.last-trace ($(date '+%Y-%m-%d %H:%M %Z'))" -
trunk-diff.sh 966 B
#!/usr/bin/env bash # trunk-diff.sh <root-dir> <state-file> [--name FILE ...] # The governance pull loop: render the subtree, diff against your PREVIOUS # render, update state. Reading the diff = updating your mental model of a # changing topology without reading everything or being told anything. set -euo pipefail ROOT="${1:?usage: trunk-diff.sh <root-dir> <state-file> [--name FILE ...]}" STATE="${2:?state-file required}"; shift 2 NAMES=("$@"); [[ ${#NAMES[@]} -eq 0 ]] && NAMES=(--name CULTURE.md --name SOP.md --name LEARNED.md --name SPEC.md --name PLAYBOOK.md --name INTENT.md) HERE="$(cd "$(dirname "$0")" && pwd)" NEW="$(mktemp)" python3 "$HERE/compose.py" down "$ROOT" "${NAMES[@]}" | grep -v '^<!-- GENERATED' > "$NEW" if [[ -f "$STATE" ]]; then if diff -u "$STATE" "$NEW"; then echo "── no changes since your last render ──"; fi else echo "── first render (no previous state); full content is the baseline ──" fi mv "$NEW" "$STATE"
-
-
templates
-
LEARNED.md 1.6 KB
--- seat: <pod>-<member>@<rig> # or rig/pod/fleet coords at higher altitudes template: <path-or-name of SOP.md this refines> @ <version/sha> owners: self (sole direct writer) · <orchestrator/PM> (by proposal) · hierarchy (boundaries only, marked) updated: <YYYY-MM-DD from the clock command — never mental math> status: UNSEEDED # the first real occupant replaces this line with: seeded-by-self <date> --- # LEARNED — <instance coords> <!-- Size is soft guidance: keep the file as small as honestly covers the job. Before handover or clear, distill LESSONS into HOW I WORK and retain needed context. --> ## MY JOB HERE <!-- This instance's actual function, in operational terms. If you cannot write this section yourself, say so to your orchestrator — that discovery is the audit working, not a failure. --> ## STANDING DUTIES <!-- One per line: DUTY — cadence — surface. These survive handovers ONLY if enumerated here. --> - ## HOW I WORK <!-- Learned SOP bullets. EVERY bullet carries its reason — a rule without its why is scripture and will be misapplied. --> - ## GATES & AUTHORITIES <!-- What I hold (granted in writing, cite the grant). What I must never infer. --> - ## KEY RELATIONSHIPS <!-- who I hand to · who gates me · whose funnel I feed --> - ## TRIGGER POINTERS <!-- situation → read this. Attach pointers to MOMENTS, not to boot. --> - ## LESSONS (dated, newest first) <!-- Distill into HOW I WORK at deposit boundaries; preserve necessary reasons and archive superseded material. Attention is the budget, not a hard line cap. --> - -
NOTES.md 370 B
--- node: <mission | slice> updated: <YYYY-MM-DD> --- # NOTES — <node> <!-- The working record: what actually happened doing this work. Append-only, dated entries, newest first. Corrections annotated, never swapped. Seat-specific learnings do NOT belong here — they go to that seat's LEARNED.md (this file is the WORK's memory, not the worker's). --> -
PLAYBOOK.md 184 B
# Legacy pointer — use SOP.md The procedure template is [SOP.md](SOP.md). Use it for new chain files; this PLAYBOOK.md pointer preserves the old address. Edit the current template. -
SOP.md 676 B
--- kind: <seat-type | pod-type | rig-type | fleet> owner: <spec author> # instances NEVER edit this file updated: <YYYY-MM-DD> status: UNSEEDED --- # SOP — <kind> <!-- The TEMPLATE half: how this KIND of thing does its job, anywhere it is instantiated. Absorbs the static role file at seat altitude. Instance-local knowledge belongs in LEARNED.md beside this, never here. --> ## PURPOSE <!-- what this kind exists to do --> ## CORE LOOP <!-- the job's shape: inputs → work → outputs/handoffs --> ## DEFAULT PRACTICES <!-- each with its reason attached --> - ## BOUNDARIES <!-- what this kind never does; where its authority ends --> - -
SPEC.md 2.5 KB
--- node: <project | mission | slice> intent: >- <ONE sentence: what this node is FOR. This composes UP the chain, so write it to be read from below by someone standing on a slice. Say the purpose, never the location — "deferred features land here" is a routing rule and orients nobody.> id: <OPR.x.y[.n] — minted by the PM; omit if unminted> stage: wip # stage: wip | provisional | established | canonical | superseded | retired status: UNSEEDED --- <!-- ONE authored file per work node. README.md and IMPLEMENTATION-PRD.md fold into this. PROOF.md stays separate and is good. PROGRESS.md is the authored acceptance checklist: checkbox items are stored marks; every roll-up above them is derived, never hand-authored. Legacy README.md nodes stay valid indefinitely; nothing is forced to migrate. PROJECT nodes carry stable intent. MISSION nodes carry intent and proportional mission-level specification in this same authored file, organizing slices and giving them shared scope. SLICE nodes carry concrete implementation scope. Keep body depth proportional; do not repeat all slice details at mission level. WHATEVER BODY YOU DO WRITE, two tests per sentence: PORTABILITY — if it would read correctly on a stranger's machine it is knowledge about a KIND; it belongs in the operating-model skill, not on the tree. VOLATILITY — a SHA, a count, a status, a roster: write the COMMAND that derives it, never the answer. A value here is stale by the next fold. And check the tree: traps and how-we-work live in LEARNED.md on the topology tree, not here. This file is what is being BUILT. Follow the selected mission-slice-sop and any explicit operating-mode overlay. Use the required sections for that work; the generic body below is a starting shape, not an additional gate or a requirement for every mission. --> # <name> ## What must be built <!-- The change itself, at the altitude a builder can act on. --> ## Why this is the shape <!-- The reasoning a reader would otherwise have to reconstruct — especially any option considered and rejected. This is the part that survives you. --> ## How we will know it works <!-- Verified by EFFECT: run it, look at the result. Name the command and what it should print, not the feeling of being done. --> ## Scope fences <!-- What this deliberately does NOT include, so it neither swallows the release nor grows during the build. Fences written after the fact are excuses; written now they hold. -->
-
-
SKILL.md 23.2 KB
--- name: openrig-operating-model description: >- Use when you do not know WHERE something belongs: you learned a lesson and are unsure which file takes it; you are about to create a new doc, folder, or convention; you are asking "should this be a skill, a note, or a chain file?"; you inherited a seat and want to know what SHOULD exist; or you are about to duplicate knowledge that already has a home. Also the trace-to-root: how a seat orients by reading one filename at each level from where it stands up to the fleet. metadata: openrig: stage: draft author: OpenRig product team --- # The OpenRig Operating Model **Canonical term: the Operating Model.** How an OpenRig topology organizes every kind of context so that any agent — cold, fresh, or five generations in — can find what it needs and knows where to write what it learns. Authored context stays in addressed Markdown and manifests. Supported writers record semantic judgments; their read models remain rebuildable from those artifacts. This skill owns the structure: trees, chains, tracing, and placement of authored knowledge. Resolve installed context through its current library addresses and the project's selected loadout; a private corpus is not a required public dependency. Companion implementation assets in this skill's folder: - `templates/` and `scripts/` — starters and working tools, described in §8. ## 1. The model in one breath Everything lives on one of **two trees**, and every kind of context is a **chain**: the same filename at every level of a tree. To know anything, you **trace from where you are toward the root**, reading that one filename at each level — nearer levels refine or override farther ones. The **trace** (§5) is the act of doing that ascent deliberately, with your own file reads. - **Topology tree** — how work is done: `fleet → instance → rig → pod → seat`. - **Work tree** — how the product gets built: `project → mission → slice → proof item`. An **instance** is one OpenRig daemon and the rigs it manages; a **fleet** is all instances. Where a daemon runs is a deployment fact, not an altitude — say the instance's name when you mean a particular one. **The one-parent law:** the trace follows the directory path and NOTHING else — no pointer fields, no link-following, no branching, ever. The trace is the instrument confused agents reach for, so it must be simpler than anything it corrects: path-only ascent fails only in obvious ways. A child that relates to a second parent gets an `also-serves:` ANNOTATION (read by humans and agents, walked by nothing) — and a genuine two-parent tie usually means the item belongs one level up, under the common ancestor, with mentions pointing down. The operational many-to-many lives in the queue's tags, which are maintained by use. Legacy `serves:` lines are transition bridges for the flat layout only: never required, never walked, deleted as work nests properly. **The filename law:** one name per chain, identical at every level. The folder tells you *whose* it is; the filename tells you *what kind* it is. A seat named `pm-lead` in two different rigs has two different `LEARNED.md` files — the path is the identity. Never invent per-level names (no SEAT.md, no RIG.md): that breaks the trace, the self-description, and every tool at once. ## 2. THE GRID — what exists at each level, and who keeps it true This table is the model. If you internalize one thing, internalize this. ### Topology tree The chains carry what is true of a **position**. How a *kind* of thing works — the operating model — ships in the mode-neutral `openrig-core` plugin; an operating-mode plugin (`openrig-lab`, `openrig-factory`, or `openrig-hq`) may refine it. | Level | `CULTURE.md` — values | `LEARNED.md` — what THIS ONE has learned | kept true by | |---|---|---|---| | fleet | default culture (ships with OpenRig) | fleet-level lived practice | **operator agent** | | instance | *(inherits)* | what is true of every rig on this daemon | **the instance's operator** | | rig | the rig's constitution | this rig's lived practice | **the rig's orchestrator** | | pod | *(inherits)* | this pod's lived practice — the **context domain**: anything useful to anyone in this pod | **the pod's lead** | | seat | *(inherits)* | this seat's lived knowledge — **the file that fixes handovers** | **the seat itself** | **Chain files sit on nodes, not on the shelves that hold them.** `rigs/`, `pods/`, `seats/`, `missions/` and `slices/` are shelves — the trace passes through them and expects nothing there. ### Work tree **ONE authored node file: `SPEC.md`.** Intent lives in its FRONTMATTER; the specification lives in its body. Alongside it sit three files with different jobs and different writers: | file | what it is | who writes it | |---|---|---| | `SPEC.md` | the node — `intent:` composes, body specifies | the node's owner | | `NOTES.md` | **LIVED** — what actually happened doing it, in the doer's own words | whoever is doing it | | `PROOF.md` | evidence the thing does what was intended | the prover | | `PROGRESS.md` | authored narrative and historical checklist marks; current proof readiness is derived from attributed judgments | the scope's owner and provers | | `proof/judgments/` | retained item judgment receipts written by `rig proof judge` | judges selected by the owning proof policy | **A scaffold may create `NOTES.md` and its starter instructions; its lived entries are never generated or projected.** It is the work tree's lived file, the way `LEARNED.md` is the topology tree's — the field-notes rung, where raw observation goes before anything has earned a place in the node itself. Keeping lived files out of the render path is what makes them safe to write freely in. **Legacy name:** older workspaces may use `MISSION_NOTES.md`. Keep legacy content addressable; use `NOTES.md` for the chain because its name works at every altitude. | Level | `SPEC.md` — frontmatter `intent:` (why) + body (what must be built) | progress | kept true by | |---|---|---|---| | project | `intent:` only — stable, changes at real pivots | derived roll-up | the project's PM | | mission | `intent:` and proportional mission-level specification; organizes slices | derived child readiness; distinct outcome judgment | the mission's PM | | slice | `intent:` and the concrete slice specification | derived proof readiness | the slice's owner | | proof item | authored promise in the proof contract | attributed evidence-backed judgment on that revision | the selected judge | **Current acceptance:** Scope relationships and policy are authored in manifests; an item judgment is recorded once against its promise, evidence and policy revision. Slice, mission and project readiness derive upward. A distinct outcome or publication decision remains its own authority. Queue ownership and generic `done` are custody facts, not proof acceptance. Historical checkbox marks stay readable without acquiring current item authority. Use `rig proof --help` for the supported write/read path. The project owner selects `proofPolicy.judges` in `project.yaml`, `mission.yaml` or `slice.yaml` (nearest wins), using exact actor addresses. There is no implicit fixed-role review conveyor. `rig proof judge mission/slices/slice#1 --verdict accept --reason 'Observed the outcome' --evidence proof/result.md` resolves the item identity, revision, evidence digests and retry identity. Evidence may be an existing legacy artifact or a non-code outcome. A patch-equivalent subject also names its actual comparison/adoption receipt with `--comparison`; the agent owns that judgment. `rig proof show` returns the current basis and retained receipt; it does not publish anything. Correct with `reject`, or `withdraw` when prior acceptance no longer stands. History stays in the addressed proof home. A repeated request returns its original receipt and current readiness; `--replace` deliberately reaffirms an old identical judgment after a correction. Changed promises, policy or evidence require a current judgment. Explicit optional `<!-- proof-item: stable-id -->` markers preserve identity across wording edits; the product otherwise derives identity from the full promise and reports ambiguity. **Intent composes; the body does not.** The trace reads the `intent:` FIELD at every altitude, so four levels unfurl as four sentences rather than four documents. The body is read only when you are standing on that node. compose.py up <node> --name SPEC.md --field intent --root <project> A level whose file exists but lacks `intent:` is reported as a gap — a mission with no intent is real information, never silently skipped. **Legacy `README.md` nodes stay valid indefinitely** — the resolver prefers `SPEC.md` and falls back, so nothing is forced to migrate and dormant missions need no attention. **PROGRESS stays separate.** Narrative and legacy marks are retained testimony. For a project with selected proof policy, current readiness comes from `rig proof show` and the shared existing views, not hand-maintained parent checkboxes or copied status prose. `scripts/compose.py progress` remains a legacy checklist renderer; it does not certify attributed acceptance. Intent composes downward; proof readiness aggregates upward. **The axis behind the columns:** every context kind has a *template* half (what ships — SOP, the default culture) and a *learned* half (what living in it taught — LEARNED, culture amendments). Only the pace of change differs: values change rarely and deliberately, like a constitution; practice changes constantly and cheaply, like working notes; intent changes at real pivots. Keep each file's pace; don't constitutionalize your notes or scribble on the constitution. ## 3. Why this exists (the failure it fixes) Seat knowledge needs a durable home that successors and other runtimes can read. A private runtime memory or an urgent handover packet alone can omit standing duties and the reasons behind a practice. Keep general operating craft in the shared skill and position-specific knowledge in the seat's `LEARNED.md` chain. At a handover, read the current chain alongside the packet and record any missing job context before claiming readiness. Each occupant maintains what the seat has learned; the handover packet carries the current transition. ## 4. LEARNED.md — the living file (all altitudes; seat shown) Sections, in order — see `templates/LEARNED.md`: 1. **Header** — coords · which SOP it refines · updated date (from the clock command, never from memory). 2. **MY JOB HERE** — this instance's actual function, in plain operational terms. 3. **STANDING DUTIES** — every recurring duty, each with its rhythm and where it happens. Duties listed anywhere else die at the next handover; this section is why they survive. 4. **HOW I WORK** — practices learned on the job, **each with its reason**. A practice with its why can be re-judged when the world changes; a bare rule outlives its reason and gets misapplied. 5. **GATES & AUTHORITIES** — what this instance may decide alone, what it must never assume. Authority exists in writing or not at all. 6. **KEY RELATIONSHIPS** — who it hands to, who reviews it, who it reports to. 7. **TRIGGER POINTERS** — "when X happens, read Y." Attach pointers to the *moments* that need them; lists of boot-time reading decay. 8. **LESSONS** — dated, newest first. Periodically distill old entries into HOW I WORK or drop them. **Size: soft guidance, not a rule.** Keep it as small as honestly covers the job — attention is the budget, and every reader pays it. Some seats genuinely need more; write what the job needs. The failure mode to watch for is the rulebook that only ever grows. Distill lessons into practices with reasons at deposit boundaries (before clear or handover); use that habit, not a line count. ## 5. The trace — deliberate reorientation A trace is walking your chains **with your own file reads** and writing down where you stand: what am I doing (queue/NOTES) → under what contract (SPEC) → toward what intent (`SPEC.md` intent field, leaf to root) → by what practice (LEARNED + SOP) → within what values (CULTURE). A few written lines at the end. `scripts/compose.py up` assembles any chain for you. Two rules give the trace its value: - **Your reads are the trace.** A trace written from memory is a recitation — if you didn't open the files, you didn't trace, and you will confidently re-derive whatever drift you already have. - **Report broken links; never obey them.** A missing file, a stale date, an intent that contradicts observable reality — say so to the level that owns it. That is how the chains stay true: they are audited by being used. And the chains **inform** decisions; they never enforce anything by themselves — a stale map must never be able to block true work. **When to trace — one principle:** *trace when enough has changed that your picture of where you stand may be stale* — after a large stretch of work, at a boundary (boot, handover, new mission, confusion), or when someone asks you to reorient. Why not simply "every N hours": identical scheduled prompts fade from an agent's attention with repetition, and idle seats accumulate ritual traces that crowd out real context. Where a schedule fits your context anyway, use one — but prefer gating the *action* on evidence of change: `scripts/trace-due.sh` decides "has enough happened since my last trace?" deterministically and stays silent when the answer is no, so a scheduler can fire it as often as it likes. ## 5b. Trace and `rig walk` — one idea, two ends These get confused because they share a word. They are not in conflict; they are the same thing seen from either end, and the relationship is worth holding. **A chain is an ordered sequence of context meant to be absorbed one piece at a time.** Two kinds exist and both are chains in that sense: - **Altitude chain** — the same filename at every level of a tree, read by ascending (`LEARNED.md`; `SPEC.md` with `intent:`). The sequence is *position*: leaf → root. - **Boot chain** — a seat's startup reading sequence. The sequence is *order of onboarding*. **A chain can be traversed two ways, and that is the only real difference:** | | who drives | mechanism | when | |---|---|---|---| | **PULL** | the agent | `compose.py up` renders the chain; the agent reads it | it is awake and oriented enough to look — a trace, a refocus | | **PUSH** | an orchestrator | `rig walk --through <files> --pace <n>` sends one piece at a time into the pane | it *cannot* self-start — freshly cleared, re-primed, cold | **Pacing is the mechanism in both directions, and it is the load-bearing part.** Absorption *between* pieces is what makes a chain land; a concatenated dump of the same bytes is a failed delivery regardless of content. That is why `rig walk` elapses `--pace` between pieces, and why a composed render is meant to be read as a sequence rather than skimmed as a wall. Use distinct names for the two directions: - **TRACE** — the pull ascent. `compose.py up` renders a node's chain to the root. - **`rig walk`** — the push verb for paced delivery into a seat's pane. Use `refocusing` for the current trace workflow. A delivery receipt and an agent's read-depth report answer different questions; pacing alone does not establish understanding. ## 6. Writing — two principles 1. **Your LEARNED.md is yours.** You write it, in your own words, as part of doing the job — when you learn something about how to do your work, the file carries it before you move on. When anyone else wants it changed (a correction, new doctrine), they tell you and *you* write it — knowledge someone else typed into your file was never yours. The one exception: when an instance is empty or broken, whoever is responsible for it writes what's needed, marks those lines as written-for-the-instance, and the next occupant rewrites them in its own words. 2. **Shipped things belong to their authors.** `openrig-core`'s operating-model skill and the shipped culture change through their owners, never by an instance editing in place. If it is wrong for everyone, propose the change to its owner; if it is wrong for *you*, that's what LEARNED.md is for. Everywhere: date what you write (from the clock), and correct by adding a dated correction rather than silently rewriting history. ## 7. Composed views Any node can be **rendered**: its chains assembled into one document (`scripts/compose.py`; `up` = your effective view from a leaf, `down` = every chain file under a root — run `down` at a tree root and you get the whole operating picture in one document). Two rules: - **Rendered documents are generated, never edited.** The chain files are the source of truth; a render is a snapshot view of them. - **Every chain render opens with a TRACE** — a tree-shaped orientation header derived at render time (never stored in any file): one line per level showing its state (seeded ✓ / unseeded ⟂ / stale-marker ⚠ / absent ✗) and a "you are here" anchor at the leaf. It shows the shape of your context the way `tree` shows the shape of a directory — where you sit, and where the screams are, before you read a word of content. - **When an approval must freeze exact content** (for example a plan-lock on a spec), it records the *hash of a render* — the frozen bytes live in the approval record while the chain files stay live for reading and revision. The **root render + diff** is how a high-altitude seat keeps a current mental model of a changing fleet without reading everything: render the tree root, diff against your previous render, read only the diff (`scripts/trunk-diff.sh`). ## 8. Where knowledge goes — the placement rule **Context lives at the narrowest scope that needs it; skills are only for what has no scope.** Seat-specific knowledge → that seat's LEARNED.md. Rig practice → the rig's files. Only truly scope-free craft (useful to any agent anywhere) belongs in the skill layer. ### The one axis that decides file-vs-plugin: KIND or POSITION - **A chain holds knowledge about a POSITION** — *this* seat, *this* mission. **Unshareable by construction**, because the path is the identity. - **A plugin holds knowledge about a KIND** — a seat-type's job, a domain's craft, an operating model. Shareable, versionable, cross-harness. **A plugin cannot hold LEARNED.** Plugins are shared; LEARNED is per-instance. Two rigs installing the same orchestrator plugin must not share what one seat learned about its own merge desk. That is why chains exist alongside plugins rather than being replaced by them. **And plugins are the distribution mechanism, which imposes a hard test.** A plugin ships skills; a skill may ship a script; **that script must run unmodified on a stranger's machine.** So it resolves paths from configuration (`rig config get workspace.root`), never from a literal, and it never asks the agent to work out which of several candidate directories is the real one. If a user points their workspace at a git repo or a `projects/` folder, everything keeps working with no further setup. **A script that only works for its author is not shippable, however correct it is.** ### What belongs INSIDE a node — two tests before a sentence goes in **A node holds what is true of THIS position and stays true.** - **Portability.** If a sentence would read correctly on a stranger's machine, it is knowledge about a KIND. It belongs in this skill or a plugin, not on the tree. A node body that is fully portable is documentation that wandered onto the chain. - **Volatility.** A value that changes faster than the file gets edited — a SHA, a count, a status, a roster — goes in as the **command that derives it**, never as the answer. **Then check which tree.** Traps, practice and how-we-work are position knowledge on the *topology* tree (`LEARNED.md`). What is being built is the *work* tree (`SPEC.md`). One rig owns both; they still do not mix. **Keep specification depth proportional.** A project carries stable intent; a mission carries its intent and mission-level specification in the same authored `SPEC.md`. Slices provide their concrete implementation scope. Follow the selected `mission-slice-sop` and any explicit mode overlay; mission specification is not a requirement to repeat every slice detail. ### The other axis: AUDIENCE is not MATURITY These are independent, and merging them is seductive because the merged version is prettier. - **Audience decides WHERE knowledge lives** — which altitude, which file. Something belongs at pod level because anyone in that pod benefits. Full stop. - **Maturity is an attribute of a LINE inside that file** — the epistemic ladder: data → observation → field note → insight → **canon**. The vocabulary already ships as the `stage:` enum (`wip | provisional | established | canonical | superseded | retired`). A pod-level item can be raw observation; a seat-level one can be canon. **A skill can contain something immature and still be the right home**, because audience picked the file. **So promotion to canon is NOT a move up the tree.** It is a maturity event and can happen at any altitude — a seat-level observation that is universally true graduates straight to a skill. What earns maturity is evidence: recurrence, independent corroboration, a measured cost, surviving change, surviving an attempt to falsify it. **Facts about mechanisms can skip the ladder** (*backticks substitute in double-quoted shell strings* is one command away); **inferences about practice must accrue** (*never broadcast to a large rig* took an incident). **LEARNED.md is not a staged item — it is the bed everything lies in.** Its gradient is positional: the dated append-log at the bottom is raw observation, the concise sections at the top are what survived. Attach distillation to a **trigger**: distil at deposit boundaries (pre-clear, pre-handover) where a write is already required and the author still remembers why each line exists; refocus merely *notices* when the log has outgrown the distilled part. ## 9. Standing the structure up `scripts/scaffold.sh` creates any missing chain files from `templates/` and **never overwrites or deletes anything** — so a brand-new workspace and a living system are the same command with different starting states. Created files are marked UNSEEDED until their real owner writes the first true version. **Never mass-produce LEARNED.md content for other instances** — each instance writing its own first version is both how the knowledge becomes real and how you discover which seats cannot yet describe their own job. ## 10. Pitfalls (only what isn't taught above) - Editing a rendered document instead of its chain files — your edit is lost at the next render, silently. - "Improving" the trace with pointer-following, serves-resolution, or any branching — the trace's entire value is that path-only ascent cannot fail subtly; keep the trace path-only. - Summarizing this model for another agent instead of pointing them here — a summary becomes another copy that can drift from its source. - Scripts in `scripts/` are macOS-flavored in places (`stat -f`); verify platform compatibility before trusting them on another OS. ## Files in this skill `templates/{SPEC,LEARNED,SOP}.md` · `scripts/{compose.py, trace-due.sh, trace-stamp.sh, trunk-diff.sh, scaffold.sh}`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.