Claude Skill

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

LLM Mart · 0 points · 10 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download mvschwarz-openrig-packages_daemon_assets_plugins_openrig-core_skills_openrig-operating-model-bda26cb.zip · 27 KB
Part of mvschwarz/openrig — 47 skills

Install

skills CLI npx skills add https://github.com/mvschwarz/openrig/tree/main/packages/daemon/assets/plugins/openrig-core/skills/openrig-operating-model
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install mvschwarz-openrig@llmmart
Git 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/ 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}

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.

No comments yet.

Reviews (0)

No reviews yet.

Related