You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Stop /prompt-miner from mining markers against a censored statistic. scoreSession computed clamp(base + bonus, 0, 100), but base is already capped at 100 by construction and bonus adds up to 15, so every low-friction ground-truth session lands on the ceiling: 15 of 37 other-stratum sessions (41%) sat at exactly 100 on the 2026-08-13 run. The marker gate promotes on a standardized mean difference in that score, so the clamp β not the data β was deciding robustness verdicts. Measured on lenWords β₯ 25 in stratum other: d = 0.332 capped with 19 of 37 leave-one-out folds under the 0.3 bar, versus d = 0.423 uncensored with 0 of 37 folds under the bar. The engine now emits scoreUncapped = base + bonus as an additive sibling of score on every sessions[] and unranked[] record; score itself is unchanged, still clamped, and still the ranking and display scale. manifest.ceilingSaturation adds a per-stratum { atCeiling, total } census over the rankable population, rendered under the existing ## Manifest heading. references/markers.md, SKILL.md, references/report-schema.md, and references/scoring.md now all name scoreUncapped as the correlation scale, closing the ambiguity that produced the manual d_cap/d_unc workaround. A stability guard keeps the net change stricter rather than looser: each marker record carries effect_size (uncensored, authoritative) alongside effect_size_capped, and a marker whose two scales disagree on sign β or where exactly one clears the 0.3 bar β is reported UNSTABLE, is not promotable on either scale, and yields no memory proposal or auto-filed issue. That guard would have blocked the 2026-08-09 referencesSkill promotion already identified as a clamp artifact. This does not close #730 (cross-window irreproducibility); no window, threshold, or cross-run rule is touched. (#778)
Anchor the memory name in .oh/scripts/oh-path to the main worktree, so one durable ledger serves the whole checkout instead of one per branch. oh-path resolved every name against the parent of its own .oh/, which inside a linked git worktree is the worktree root; .oh/memory/MEMORY.md is gitignored, so a new worktree started without one and .oh/scripts/ensure-memory-file.sh seeded an empty stub there. Every build session in this harness runs in a worktree, so the ledger was structurally invisible to exactly the sessions that produce and consume lessons, and each /retro wrote into a file deleted with its branch β five of the seven lessons written in session c10a1f34 never reached the checkout's ledger, including the one that session had just re-derived. Measured across the live worktrees at the time: main checkout 86 lines, the incident worktree 17 (an auto-seeded header plus its own writes), the other six absent. Every other name (crons, evals, tasks, context, worktrees) stays branch-scoped, because a probe or a task must resolve the worktree it is testing; the journal is the exception. Absolute values from MEMORY_DIR or harness.yaml β paths.memory are still honored verbatim β only relative values change which root they measure from β and the env β harness.yaml β default precedence is untouched. Resolution degrades to the previous root whenever git cannot answer (no git binary, not a repository, a damaged .git, or output naming a non-directory), so a fresh oh init before git init is unaffected and a git failure can never abort a caller running under set -eu. /retro computed the resolved path at SKILL.md:235 and then discarded it, passing the relative literal .oh/memory/$TODAY/log.md to locked-append.sh, so the resolver fix alone would not have reached the harness's most frequent memory writer; its log and ledger writes now use the resolved path. The docs that called the tier "local-per-instance" are corrected to name the checkout as the unit and to state that an empty ledger means the file was just created rather than that a fact went unrecorded β naming the unit wrong is why the same defect was worked around twice at individual call sites (#152, #693) without anyone fixing the resolver. This does not make memory portable across clones, operators, or providers: MEMORY.md stays gitignored, and that broader question is left open rather than answered by a side effect. Pinned by .oh/evals/probes/memory-dir-shared-across-worktrees.sh, which builds a real linked worktree and runs that worktree's own copy of oh-path β invoked from the main checkout the fixed and unfixed scripts print the same path, which is how the defect stayed invisible β and asserts the shared anchor, the absence of any worktree-local resolution, the unchanged branch-scoping of the other names, and the verbatim absolute override. It reads git topology and paths only, never MEMORY.md content or other untracked state, so a fresh CI clone and a developer worktree return the same verdict. Verified by rejection: against the unfixed script it fails on exactly the two assertions under test with the other five reporting zero, and swapping only the script inside one fixed worktree flips the result, which attributes the exit code to the anchor block rather than to its neighbours (#768).
Stop /retro from logging promotion counts it cannot yet know. .oh/skills/retro/SKILL.md ordered its steps Β§6 propose-then-confirm gate β Β§7 write approved changes β Β§8 append the log entry, but Β§8 is unconditional and renders render-log-entry.sh --memory <n> --identity <n> β promotion counts. For any run that is not auto-approve the agent's turn ends at the gate, because it must hand control back to the operator to get an answer, so the entry was written while the counts were still unknowable and became wrong the instant the operator approved. .oh/memory/<UTC-date>/log.md is append-only, so it could not be corrected in place: on 2026-08-13 the 06:02 entry recorded Promoted: 0 to MEMORY.md, three lessons were then approved, and a separate superseding line had to be appended at 06:03. The defect survived because auto-approve β the invocation used by .oh/prompts/advisor/implement.yml and pr.yml β collapses the gate into a single turn, so the common path never exercised it. The helper now refuses the guess rather than relying on step order alone: --result GATE-PENDINGrequires--memory pending --identity pending and exits 64 on an integer, pending is rejected on any other result, and a new --resolves HH:MM (valid only with --result OP) renders the join line back to the entry it resolves. SKILL.md Β§6 is now 6a filter duplicates β 6b append the GATE-PENDING entry β 6c print the proposal block, with the append placed before the block that ends the turn and an explicit "nothing placed after it is guaranteed to run"; Β§8 appends the resolving entry and derives its counts from the lines actually appended in Β§7 rather than from the size of the proposal list. The alternative shape sketched on the issue β defer the append until the gate resolves β was rejected because a run abandoned at the gate never resumes, so it would leave zero trace and trade the unconditional-log guarantee away to fix a count; rescuing it requires writing something at the gate anyway, which is this shape. retro-deterministic-contract.sh gains eight assertions, each with a unique 767-<id> message, verified by rejection against eight fixtures that each break exactly one thing and then attributed by mutation β deleting the assertion makes the same broken tree pass. Two of them cover the step order separately, because anchor reordering and anchor deletion fail differently and the deletion case is the dangerous one: grep -n β¦ | cut -d: -f1 yields the empty string on no match and (( "" < 100 )) is true in bash, so an unguarded compare passes on a file whose anchor was removed. Scope is deliberately narrow β report-schema.md, memory-protocol.md, validate-retro-report.sh and check-memory-duplicates.sh are byte-unchanged. What this does not fix: the count is still the agent's self-report, so a miscount remains reachable; the change closes when the number is asserted, not whether the arithmetic is right (#767).
Make the /ste checker's clean exit name the two defects it cannot see. Measured on the skill's first production use: ste-check.sh exited 0 with zero findings on prose carrying a condition placed after the action it guards and a sentence opening with a pronoun that names no antecedent β questions 4 and 7 of the skill's own 10-question check, both caught by hand seconds later. A green run reads as approval, so the exit-0 line now states both escapes and points at SKILL.md, and the "checker misses" paragraph names them alongside missing actors and invented values. No detector was added, deliberately. A question-4 detector fires on approved after specimens in references/examples.md (lines 58, 86, 99, 140, 310, 324) and would turn --blocks after red, breaking the committed regression fixture; a question-7 detector cannot separate a bare pronoun from one whose antecedent sits in the previous sentence, because the checker reads one line at a time, and it would flip references/rules.md red too. ste-checker-contract.sh gains a section-7 assertion pinning the disclaimer, verified by rejection against three mutations β the bare pre-change line, and each half removed on its own. references/examples.md, references/rules.md, and references/dictionary.md are untouched, and no new rule identifier joins the six.
Keep the firstmate execution-context probe pane alive across its own read, so the gate can admit herdr in an environment that matches. herdr destroys a pane the instant its command returns and answers pane_not_found on a read against a destroyed pane, so a probe that printed one fingerprint line and exited lost the race with its own reader. The gate then reported "no probe fingerprint obtained" and refused herdr in every environment, not only in a mismatched one β a permanent refusal wearing the shape of a proof obligation. The defect was invisible until the operator config bind (#756) closed, because until then the gate failed earlier and correctly on a genuine host-vs-container fingerprint mismatch. A keep-alive suffix now rides the pane invocation only; RUNNER_PROBE_SCRIPT stays byte-identical between the pane and the in-process caller-side call, so "the same snippet runs in both places" remains true by construction and runner_local_fingerprint never sleeps. The budget derives from the single RUNNER_PROBE_TIMEOUT_MS source and is an upper bound rather than a cost, since the gate closes the pane as soon as the read completes on either verdict. The unit-test herdr stub was more forgiving than herdr β it replayed pane output regardless of pane lifetime, which is why 46 tests passed against a gate that could never succeed live β and now models the real lifetime, so removing the keep-alive fails five tests including three that predate this change. session-runner-ladder.sh gains five assertions covering the composition, the derived budget, and the leak of a keep-alive into the shared snippet; each was verified by rejection against a deliberately broken copy (#761).
Make assertion (d) of .oh/evals/probes/cc-safety-net-wiring.sh assert the resolved cc-safety-net version instead of the declared dependency range, ending a tier-A false positive. .pi/npm/package.json is boot-generated: pi installs through npm, and npm's default save-prefix writes "cc-safety-net": "^1.0.6", which the old regex rejected β so the probe reported REGRESSION while .pi/settings.json, the Dockerfile, the lockfile, the installed package, the global install, and cc-safety-net --version all agreed on 1.0.6. Loosening the regex to accept ^1.0.6 was rejected as a fix because that range permits any 1.x.y above the pin, which is exactly the runtime drift the assertion exists to catch; a range cannot answer the question and only the resolved version can. The assertion now reads .pi/npm/package-lock.json (packages["node_modules/cc-safety-net"].version, with a dependencies fallback for older lock shapes) and .pi/npm/node_modules/cc-safety-net/package.json, checks every source that exists so a lock and an installed tree that disagree each get their own line, and states the file, the version found, the pin, and one remediation clause. Absent runtime state stays a pass, so a fresh clone with no .pi/npm/ is still green β but a tree that declares cc-safety-net while resolving nothing is now a gap rather than a silent pass. CC_SAFETY_NET_PROBE_PI_NPM points the assertion at a fixture tree and deliberately cannot disarm it: a set override that names a missing directory, or one that resolves no version at all, fails. Parsing uses node rather than jq because no workflow provisions jq while the eval job provisions Node 22 explicitly, and a missing interpreter or unreadable JSON produces a (d) line rather than an early exit that would have silently skipped assertions (a), (b), (c), (e) and (f). Verified by rejection across eleven cases β including an assertion-(d)-stripped mutant that exits 0 on the same drifted tree where the real probe exits 1, which is what proves the exit code belongs to this assertion and not to its neighbours (#759).
Repair seven entries in .claude/protected-paths.txt that resolved to nothing, and add .oh/evals/probes/protected-paths-resolve.sh so a rename cannot silently disarm a guard again. A guard entry that matches nothing protects nothing and reads identical to one that passes, which is why all seven survived since 2026-05-03. cloudflared-tunnel was listed while the skill directory is .oh/skills/cloudflared, so /cloudflared had no protection at all. Four spec-* entries had never been directories β the /spec dispatcher implements them as .oh/skills/spec/references/{plan,critique,execute,retro}.md, which now carry the protection. .oh/install/cloudflared-tunnel.sh was deleted at some point and is replaced by .oh/skills/cloudflared/scripts/run.sh. .claude/specs/structure-spec-v0.7.md sat under a path .gitignore:66 excludes, so it could never resolve and is removed. The probe parses the file's documented format β bare names are skills, everything else is a repo-relative path, with a slash-free entry allowed to resolve either way so the root Makefile entry stays valid β and was verified by rejection against all four original defect shapes (#753).
Security
Pin transitive nanoid to the patched ^3.3.17 range to close GHSA-2v37-7h3g-55p8 through the Vitest β Vite β PostCSS dependency path.
Deny the container-inspect shapes that expose environment variables, without blanket-blocking docker inspect. A bare docker inspect <container> prints the full container JSON including Config.Env β every secret the container was started with β so the shared Bash guard (deny-env-dump.sh, tier 1c) now requires an explicit narrow Go template and denies any template that names env or expands the whole object ({{.}}, {{json .}}, {{.Config}}, --format json); unverifiable | jq pipes over full JSON are denied too, and podman/nerdctl are covered by the same rule. Narrow field reads stay allowed (--format '{{.State.Health.Status}}', '{{.NetworkSettings.IPAddress}}', '{{json .State.Health}}', the Networks range template), so every existing docker inspect in .oh/scripts/sandbox-boot-smoke.sh and the installation/deployment/langfuse docs keeps working, and docker ps/compose/exec are untouched. docker secret|config inspect remains in the pre-existing hard-deny tier β no template makes those objects safe. The deny-list mirrors the env-shaped patterns (*inspect*Config.Env*, *{{json .}}*, *{{.}}*, β¦) in both .claude/settings.json and the install template rather than restoring the blanket Bash(command=*docker inspect*) rule. Pinned by docker-inspect-env-guard.sh, which asserts the deny cases, the allow cases (including the templates already used in-repo), the wiring, and β deliberately β that the blanket block does not come back (#723).
Block agent reads and writes to operator-owned settings.local.json files across Claude, Codex, and shared Open Harness hooks (#710).
Removed
Remove the /caveman token-compression skill and its four subcommands (caveman-commit, caveman-review, caveman-compress, caveman-stats). Usage evidence across 743 session traces (660 Claude, 83 Pi) and 33 daily memory logs shows zero invocations by every available oracle: 0 Skill-tool calls out of 96 spanning 23 distinct skills, 0 Pi tool records, 0 ## Caveman memory entries despite its own SKILL.md mandating one per activation, and 0 activation announcements in assistant output. Every one of the 405 sessions that mentioned "caveman" resolved to context injection β the five description strings in the always-injected skill listing, the AGENTS.md table row, or .oh/skills.lock provenance β never a use. The five entries cost roughly 1,770 characters of description frontmatter in the skill listing of every Claude and Pi session, permanently, for a capability never once exercised. Removal drops the third-party stanza from NOTICE and .oh/cli/NOTICE, the five .oh/skills.lock entries, the AGENTS.md row, and the /caveman citation in .oh/skills/retro/SKILL.md; /ste now states its compression-precedence clauses standalone rather than adopting them by reference. .oh/tasks/apache-relicense/prd.md DP-3 is amended rather than silently contradicted: it constrained relicensing a third party's copyright, not removal β MIT attribution obligations attach to distribution and lapse once distribution stops, so no upstream permission was required (#752).
Added
Add .oh/docs/rfcs/rfc-rsi-survey-mapping.md, a #525 companion. The RFC reads the 1,250-paper recursive-self-improvement survey (arXiv 2607.07663v1, July 2026) against this repository. The recursive-self-improvement-survey wiki entry backs it. The RFC is a decision artifact. It changes no probe, no skill, and no runtime behavior. It decides three items. First, the taxonomy placement. Open Harness is deployment-time harness and skill evolution (survey Β§3.5β3.6) at human-on-the-loop closure. The survey's training-time Β§4 and its takeoff Β§7 therefore stay out of scope, and no later change imports them. Second, a rung assignment for the harness's own signals against the survey's verification hierarchy (Β§5.2), as shared vocabulary for audits, critiques, and later RFCs. Rung 1 formal holds nothing. Rung 2 execution holds the 105 .oh/evals/probes/*.sh oracles, which CI executes through ci-harness.yml and release.yml. Rung 3 learned judges covers /critique, /approve, /audit implementation, /audit pr, /benchmark, and the .oh/evals/capability/ rubric scoring. Rung 4 intrinsic holds STATUS: COMPLETE in progress.txt β the terminal interface for all three build executors β and every self-reported count in .oh/memory/. One reading follows. The harness's terminal build signal sits on the most gameable rung. Rung 2 and rung 3 both execute after the build declares itself finished, rather than producing that verdict. Third, that human-on-the-loop closure is deliberate and evidence-backed rather than a gap. The RFC then states five findings that the repository already evidences. Each finding carries an in-repo exhibit and needs no new instrumentation. F1: SkillsBench reports that human-authored skills gain 16.2 points while model-authored skills show no measurable gain. The capability suite sits flat at 1.42/2.00, CB-004's own basis line reads Ξ +0.00 machinery-added, and /autopilot shipped skills across that same window. That reframes the flat ceiling as a field-wide expectation. It names authorship provenance, which nothing on disk records, as the missing measurement. F2: the Mirror Loop measures 55% decay under ungrounded self-critique. In /spec plan β /spec critique, two critics share weights with the planner. Those critics read only pre-build local artifacts. That separates the loop from the grounded build β audit loop, which AGENTS.md Β§ The Workflow calls the same mechanism. F3: the Red Queen result names the stationary-evaluation-criterion assumption. /autopilot optimizes against 105 probes hourly, and its OWNED_PATHS array includes .oh/evals/. That corroborates roadmap item 11 and records why evaluator edits stay human-gated. F4: the survey measures a 34.2% integrity-failure rate under completion pressure. #767 records the matching residue in its own closing sentence: "the count is still the agent's self-report". That repair patched the ordering rather than the rung. F5: SkillMutator names a cross-modal attack surface. This skill library federates through .oh/manifest.json, oh init, and the mifunedev/skills registry. Checksums cover transport integrity, and no check asserts that a SKILL.md and its scripts/* agree. The RFC records three existing roadmap children as independently corroborated. It leaves all three unchanged. The survey's "experience graphs" corroborate the trace ledger. SHARP's constrain-the-self-modification-surface argument corroborates the repair-operator registry. The Darwin GΓΆdel Machine's empirical-benefit loop corroborates the benchmark and promotion gate. The RFC also records the survey's capital-versus-operating-expenditure framing (Β§5.5) as the strategic reading behind /retro, .oh/memory/, and the wiki. It proposes two additive children for a maintainer to file, and files neither. The first child records skill authorship provenance and runs a SkillsBench A/B. The second child adds a cross-modal skill consistency probe.
Detect drift between the canonical .oh/skills/<name>/ copy of a skill and the portable copy published to mifunedev/skills, which nothing checked before. The three defects caught by hand one commit before mifunedev/skills#7 merged β two /caveman references and a .oh/skills/retro/references/memory-protocol.md pointer, none of which an installer can resolve β were found only because someone thought to run a diff. A byte-equality check cannot replace that judgement, because the two copies are intentionally different: invocation paths are relative in the registry, harness-specific sections are generalized, and the published copy carries a LICENSE the canonical one does not. .oh/scripts/registry-portability.sh therefore implements the issue's Option C and reads the published copy standalone, asserting it names no path and no command an installer will not have. Three rules, reported as file:line: RULE token: OH-PATH (any .oh/β¦ reference), HARNESS-SKILL (a backticked /name naming no folder under skills/, suppressing Unix filesystem roots and foo/bar/baz/qux placeholders β a looser unbackticked pattern produced over 100 false hits), and DANGLING-REF (a backticked references/<f>.md or scripts/<f>.sh the skill folder does not carry). Exceptions live in one fenced allow block inside .oh/scripts/registry-portability.md, so the reviewer and the checker read the same file; each entry keys on the whole trimmed source line's sha256 prefix rather than the matched token, which is what stops an exception written against a repaired line from also suppressing the pre-repair defect. Two classes: ALLOW suppresses, KNOWN reports a triaged defect and deliberately does not touch the exit code, because a green check while a live defect stands would be a lie. The script lives in .oh/scripts/ rather than .oh/docs/ or a /sync subcommand for two measured reasons β .oh/manifest.json ships scripts/** but not docs/**, so a fail-closed script whose exception file was under docs/ would hard-error on every installed harness, and .oh/scripts/*.sh is the only one of the three candidate homes inside the CI shellcheck argument list. Exit 0 all findings suppressed, 1 a finding survives, 2 an untrustworthy run β the last covering a missing or non-existent --registry, a registry with no skills/ tree, zero skill folders, zero scanned files, a missing exceptions file, and an exceptions file with no allow block, so a scan that read nothing can never look like a pass. Because KNOWN does not suppress, the check exits 1 against the registry today and will until those defects are repaired there; the neither count, not the exit code, is the signal that tells a publisher whether their own change added new drift, and both the contract and the /builder publishing step say so. Criterion 5's one-time sweep is recorded in .oh/tasks/registry-drift-lint/sweep.md β all 18 published folders at registry master 1d11ab6, 31 files, 14 findings in 4 skills and 14 folders clean, reported and not repaired, since the registry is a separate repository: 9 accepted with reasons and 5 real defects left standing with a suggested repair for each. The sweep also records a class this lint cannot see β three folders are still published under the audit vocabulary this repo retired in #645, which resolves fine for an installer who never had the new name and needs the issue's diff-based Option A instead. Two probes, because one is not enough: registry-portability.sh scans the registry and so reports SKIPPED without a checkout, which is every run in CI, and reports REGRESSION when armed against live master β green in zero configurations, and guarding nothing by default. Its skip is honest for its own contract and .oh/evals/README.md forbids a synthetic registry fixture, so it is kept as-is and registry-portability-gate.sh is added alongside it, reading only this repository and therefore never skipping: it asserts the linter is present and still fails closed, the allow block parses with every entry well-formed, and the /builder publishing step still names the gate β the three ways this check gets silently disarmed, none of which would turn the first probe red. Verified by rejection throughout, never by exit 0: against registry 036a53f the shipped exception list reports all three historical defects as new; an injected .oh/ path and /autopilot command are both caught and named at file:line, and removing them restores the exact baseline; all 8 fail-closed paths exit 2; and the new probe was broken 8 ways and went red on all 8 (#758).
Add /ste, a Simplified-Technical-English writing standard for artifact prose β docs, runbooks, specs, commit and PR bodies, code comments β with a dependency-free checker. .oh/skills/ste/references/rules.md states 53 rules across 9 sections; dictionary.md maps 198 non-approved words to replacements; examples.md gives 24 before/after pairs across 13 documentation domains. scripts/ste-check.sh reports file:line: RULE-ID message for six detector classes (HEDGE, VAGUE, PASSIVE, LONG, COMPOUND, WORD) and exits 0 clean, 1 on findings, 2 on a usage error; it rewrites nothing. The --blocks <tag> flag scans only fenced blocks whose info string ends in that tag, which makes the standard self-applying: the skill's own SKILL.md and every after specimen pass the checker, and the before specimens are the committed regression fixture that proves the checker rejects as well as accepts. A precedence rule scopes the standard: /ste governs anything git-tracked or GitHub-posted, while an output-compression mode governs only the live chat reply. The standard states the never-compress-code and revert-to-prose-for-warnings clauses itself. The skill is aligned to the published shape of ASD-STE100 but independently authored: it reproduces no Issue 9 text and no dictionary entry, and claims no certification and no complete standards compliance (#750).
Add firstmate, an opt-in third build executor that runs ONE long-lived First-Mate session over a whole .oh/tasks/<slug>/ task graph where ralph launches 50 fresh single-story processes. ralph remains the default and is retained indefinitely; firstmate is reached only through --executor=firstmate on /ship-spec (Stage 10's "Opt-in (firstmate)" subsection) or /autopilot (pure deferral β autopilot adds no build mechanics of its own), and /spec execute names it as the third arm. All three executors reach the same terminal interface: the whole line STATUS: COMPLETE in .oh/tasks/<slug>/progress.txt. .oh/scripts/firstmate.sh validates the slug and the four-file task contract, short-circuits on an already-complete sentinel, claims an atomic mkdir /tmp/firstmate-<slug>.lock, refuses to launch beside a live ralph session for the same slug, renders the skill-owned session prompt (.oh/skills/firstmate/templates/session-prompt.md, a derivative of the .oh/prompts/advisor/ step order β zero bytes changed under .oh/prompts/), and watches to the sentinel. .oh/scripts/lib/session-runner.sh resolves the host through a herdr β tmux β foreground ladder whose herdr rung requires an installed binary, herdr status reporting both status: running and compatible: yes, a caller not already inside a herdr pane, and a probe-pane environment fingerprint matching the caller's β an installed-but-out-of-environment herdr degrades to tmux with the mismatch logged, while an explicit --runner/OH_RUNNER naming an unavailable rung is a hard error rather than a silent degrade. Wall clock is bounded by FIRSTMATE_TIMEOUT_MS (default 14400000 = 4h) through the single resolve_timeout_ms source, which rejects 0, negative, non-numeric, and empty values back to that default; on expiry, launch failure, or operator abort the session is torn down (herdr pane close / tmux kill-session), the lock removed, and FIRSTMATE-INCOMPLETE appended to progress.txt. Ships with the /firstmate skill (executor contract, ladder, watch and recovery matrices, per-mode kill procedure, and the executor-vs-role-charter disambiguation), an added runner ladder in .oh/skills/t3/references/sandbox-processes.md Β§ Source of Truth that re-affirms tmux for managed/headless processes, and the build-executor-ladder wiki entry. Pinned by two probes: firstmate-executor-contract.sh (the entrypoint is present and executable, the STATUS: COMPLETE sentinel is matched as a whole line and the | tee log pipe survives, both executor toggles carry a firstmate arm and a ralph arm with SHIP_SPEC_EXECUTOR:-ralph byte-identical, the session prompt's anchor keywords appear in the advisor pack's relative order, .oh/prompts/ is zero-diff, CLAUDE.md β AGENTS.md is unsevered, and ralph.sh still exists) and session-runner-ladder.sh (ladder order, the two-literal herdr health predicate, the nesting guard ahead of any probe pane, the fingerprint gate's degrade-and-log, the single resolve_timeout_ms budget source bounding every watch, teardown + lock removal + FIRSTMATE-INCOMPLETE on every exit path, --no-focus on every launch, pane close as the teardown verb, and no file-scope shell options in the sourced library). Both were verified by rejection β each assertion was shown to fail against a deliberately broken copy, not merely to pass against the good one (#746).
Ratify the Phase-0 brain/hands boundary in a durable RFC, .oh/docs/rfcs/rfc-brain-hands-boundary.md, so the slices consuming EPIC #731 build against one written contract instead of re-deriving it. It carries an authority clause making it the sole source of truth for those decisions (cite, do not restate), and records: the brain/hands responsibility table (ralph, cron, autopilot, memory, and wiki are brain; oh gateway deliberately stays brain-side); the eval split as a capability rule β reads-repo-files is brain, must-execute-in-environment is hands β rather than a list that goes stale; the four-class state taxonomy (WorkspaceState, ExecutionState, HarnessAuthState, SessionState) with Hermes documented as an accepted known violation, since it fuses both sides plus auth.json into one opaque .hermes/ blob and can provision a second, nested hands layer the contract does not model; the workspace stance, where identical-path mapping (hostRoot === targetRoot) is the only supported Phase-0 configuration and the two-field shape is called speculative β targetRoot currently carries permission, not semantics, and the Sysbox slice must give it meaning or collapse it; and the decision that attach() is synchronous in contractVersion: 1, because the process-runner seam is already spawnSync-based (.oh/cli/src/commands/lifecycle.ts:49-53) and attach is a blocking terminal handoff, with a named migration path for a future contractVersion: 2. Corrects three stale claims in .oh/docs/rfcs/rfc-runtime-support.md (Β§ Purpose and the Β§ 1 A1/A3 rows): the host Docker socket is opt-in and off by default, not bind-mounted by default, and the image ships the docker CLI but no dockerd. Inserts Sysbox as a net-new Β§ 8 item 1 and marks the Β§ 9 DinD trade-off answered β Sysbox gives the tier its own dockerd, so stronger isolation need not cost sibling-container capability (#733, Refs #731).
Record the audit's proof for the reviewer as a committed evidence doc during the .oh/prompts/advisor/pr.yml flow. A new contract (.oh/skills/audit/references/reviewer-evidence-doc.md) defines .oh/tasks/<slug>/evidence.md β per-gate proof table, acceptance-criteriaβproof mapping, and honesty rules requiring observed commands and real output (a gate with no observed output is recorded as a gap, never a pass), correlated to AUDIT_RUN_ID and the verbatim native verdict. The audit routes stay read-only and do not write it: the orchestrating caller does, from the observations they return. Distinct from the schema-v1 evidence.json lifecycle contract at AUDIT_EVIDENCE_PATH (#719).
Deny agents both read and write access to the operator-only .config/ directory β at the repo root and in $HOME β as a first-class tier in both PreToolUse guards, replacing the two hand-picked leaves (.config/gcloud/**, .config/gh/hosts.yml) that were all the deny-list previously covered. The Bash tier is deliberately verb-agnostic: any command naming the directory is denied, not just the enumerated READ_CMD readers, because a verb allowlist leaks through python/node/perl/tar and every tool added later (mkdir, tar, python3, and a docker exec subshell are all covered by the probe). Both tiers anchor on a whole path segment, so jest.config.js, vitest.config.ts, --config foo, git config, and .oh/config.json are unaffected, and the pre-existing secret family β including the .env.example template exemption β is unchanged. Closes a related read hole in the same pass: deny-secret-paths.sh was wired for Read|Write|Edit|NotebookEdit and inspected only tool_input.file_path, so Grep/Glob could walk into a directory Read was blocked from; it now runs for those tools too and scans every path-shaped field (file_path, notebook_path, path, glob) while deliberately leaving Grep's content pattern alone. Pinned by operator-config-guard.sh, which asserts the behaviour and the wiring and was verified to fail when the guard is neutered. Provider coverage stays asymmetric and is documented as such: claude is fully covered, codex inherits the Bash tier only, and the pi extension addition is write/edit and interactive-mode only (#707).
Add lsof, htop, and the inetutils-telnet plaintext diagnostic client to the default sandbox image (#703).
Changed
Release versioning moves from CalVer to SemVer. The version is no longer derived from the push clock (github.event.repository.pushed_at β YYYY.M.D, then -1, -2 on a same-day collision); root package.json now holds it and is the only place it is written. release.yml's reserve job reads it with node -p "require('./package.json').version" and passes it as RELEASE_VERSION, mirroring the guard publish-cli.yml already used for the CLI package, so the repo has one release idiom instead of two. The atomic reservation is unchanged in kind β creating the tag ref is still the reservation, and same-SHA retry recovery (reuse a draft, no-op an already-published release) is retained verbatim β but its collision policy is not: a tag that already exists on a different commit no longer advances a -N suffix. Under SemVer the version is a deliberate input, so the only correct answer is to report it, and the reserve step sets publishedNoop=true, logs which version already shipped, and lets the existing publishedNoop != 'true' guards skip the image, CLI, and finalize jobs. An unbumped push to main is therefore a clean, green run rather than a failure, which needed no new workflow plumbing. parseSemVer accepts strict MAJOR.MINOR.PATCH only, rejecting leading zeros, prerelease identifiers, build metadata, and a v prefix; note that a bare 2026.8.7is well-formed SemVer and is accepted β the validator cannot distinguish it, and what now prevents a CalVer release is the source of truth, not the pattern. The v prefix is introduced in exactly one helper (releaseTagName) so the create path and the recovery path cannot drift, and it reaches only the git tag and the GitHub Release name: the step output, the GHCR image tags (ghcr.io/mifunedev/openharness:0.1.0), and the release-image-<version> concurrency group all stay bare. promote-release-latest.sh validates SemVer and rejects the CalVer forms that are not valid SemVer β the -N same-day suffix and zero-padded dates. Each new assertion was mutation-checked rather than trusted green: neutering the SemVer pattern turns three tests red, neutering the shell regex six, and dropping the v from releaseTagName five, covering all four reservation paths. The 42 existing CalVer tags and the CHANGELOG entries describing past CalVer releases are left as history and are not rewritten. This is the versioning machinery only; no skill, agent, or cron pruning is included (#814).
Rehome /health-check's Docker triage host-side, and make the socket-less sandbox path state that once instead of failing nine times. #756 removed the host Docker socket from the sandbox as a host-root escape path; the docker CLI is still installed, so every Docker step in the skill kept failing on its own with the same connection error β three calls in step 2, one verbose call, docker stats in step 5 and four in step 6 β while the memory/disk/CPU steps went on reporting the container's numbers under host framing. A green container disk row read like permission to start a multi-GB image build. A new scripts/scope-preflight.sh classifies scope and endpoint once and emits SCOPE, DOCKER_CLI, DOCKER_ENDPOINT, DOCKER_TRIAGE and METRICS_SCOPE plus exactly one HEALTH-CHECK SCOPE-NOTICE: line naming why, what was skipped, who runs the relocated procedure and where it lives. DOCKER_TRIAGE=available requires a completed docker version round-trip, not [ -S path ]: a socket file outlives an OOM-killed daemon and a chmod 000 socket passes every file test, so the file test alone would have reproduced the same wall of errors behind a passing preflight. Three states (available / host-only / unreachable) rather than four β an unverified state was designed and dropped because nothing forced it to ever be resolved, which would have left a tcp:// operator exactly where the old skill left them. The host-only branch contacts no daemon at all, and the script always exits 0, since a classification step that exits non-zero is itself the misleading signal being removed. Endpoint resolution follows the real CLI (DOCKER_HOST, then docker context inspect, then the default socket, then the rootless path), because Colima, OrbStack and rootless setups set a context rather than the env var. The relocated procedure is addressed to a role that already exists: the orchestrator at the host project root, which root AGENTS.md Β§ Permissions already grants docker/docker compose and whose skills table already lists this skill β and it specifies the round trip in both directions, including what the report concludes when nobody pastes anything back (Docker headroom UNKNOWN, never silently pending). Beyond labelling scope, the verdict now refuses what it cannot support: under host-only with a build-shaped target the Disk row renders N/A instead of a RAG rating computed from container df, because a label explains a number without withdrawing a conclusion. Container-local questions keep a real rating. SKILL.md's claim that "Docker lives on the root overlay here, so root df is the binding number" is corrected rather than relabelled, and a dangling /docker-disk-cleanup pointer β a skill that exists nowhere in the repo β is removed from the section being rewritten. AGENTS.md, .oh/templates/AGENTS.md and the frontmatter descriptionandTRIGGER list are realigned, since "free up space" pointing at a skill that can no longer reclaim in-container is a discoverability regression. /audit full --health-target now decides the question instead of offering an either/or: a socket-less composition is container-scope evidence recorded partial, deliberately notdeferred, because deferred promises a rerun recovers the evidence and an in-container rerun produces the identical gap. Pinned by health-check-socket-degrade.sh, which executes the preflight across nine assertions β the host-only classification, a docker shim proving zero daemon contact on that branch, the available and unreachable arms, a real bound socket, an absent CLI, the absence of any terminal unverified state, and the SKILL.md wiring, since an unwired script is a no-op. Eleven mutations were each shown to fire their own named assertion rather than merely to turn the probe red (#762, Refs #756, #731).
Ship .oh/docs/** through the payload manifest while retaining the .oh/patches/** exclusion (#738).
Retire the five KNOWN entries from the .oh/scripts/registry-portability.md exception list, now that mifunedev/skills#8 has repaired every defect they recorded (merged as eab0a14). Those entries were the day-one triage of real defects in the published copies: three scripts/ralph.sh references in ship-spec that the folder does not ship, /retro in ste where the registry publishes reflect, and the .oh/ memory-protocol pointer that survived the #751 repair. Each matched no line after the repair and reported as a stale exception, which never failed a run β so nothing was gated on this cleanup, and the entries could not be removed any earlier without making the repaired defects report as new drift in the window between the two merges. The list is now 8 ALLOW entries and no KNOWN entries, and .oh/scripts/registry-portability.sh exits 0 against live registry master for the first time: findings: 9, suppressed by ALLOW: 9, neither: 0, stale exceptions: 0. The armed probe passes rather than reporting REGRESSION, so the probe pair is now green in the configuration that actually reads the registry. The contract and the /builder publishing step both said to read neither and not the exit code, on the stated grounds that the check "will keep exiting 1"; both now describe the current regime and why neither stays the correct reading in either one. This unblocks the .oh/crons/ job that clones the registry and runs the probe armed β the only trigger that catches a commit made directly against the registry β which stays deliberately unbuilt, but would now start from a green baseline instead of paging on a known backlog from its first run (#758).
Route oh sandbox and oh shell through the provider-neutral ExecutionTarget contract, with no operator-visible behavior change. oh sandbox now resolves a target and calls provision(); oh shell calls attach({argv: ["zsh"], user: "sandbox"}) and no longer builds a literal docker exec argv itself β the Docker Compose adapter owns the substrate argv, and it in turn delegates every compose operation to the vendored .oh/scripts/docker-compose.sh rather than re-assembling an overlay list in TypeScript. The emitted argv, exit codes, the container \` not running?hint, the ``docker is required foroh shellbut was not found on PATH`` error, and container-name precedence (positional arg > harness.yamlsandbox.name>openharness) are all unchanged, and the whole existing lifecycle suite passes with zero assertion edits. runShelldeliberately keeps its synchronous: numbersignature, whichattach()being synchronous incontractVersion: 1is what makes possible. The boundary is enforced in both directions: theharness.yamlseed, the default-off Docker-socket opt-in prompt,--imageref resolution, and container-name precedence stay brain-side in.oh/cli/src/commands/lifecycle.ts, and oh gatewayis deliberately *not* routed through a target β it is orchestration, not execution. No harness.yaml key, CLI flag, or env var selects a target;resolveExecutionTarget()` is internal (#733, Refs #731).
Enable Pi subagent FleetView by pinning @tintinweb/pi-subagents@0.12.0, with the navigable agent list on by default.
Set the default Pi driver to openai-codex/gpt-5.6-luna with max reasoning while retaining Sol, Terra, and Luna in the model selector (#700).
Release every push to main or master only after validation, using retry-safe UTC CalVer reservations, immutable CalVer/sha-<full-SHA> GHCR tags, canonical-branch digest promotion for latest, gated CLI publication, and post-image GitHub Release finalization (#689).
Expose the supported GPT-5.6 variants in Pi's model selector (#684).
Expand Advisor planning with a designer lens and make implementation/PR prompts explicitly finish with delegated audits and retrospectives (#680).
Fixed
Install the pinned ryaneggz/pi-langfuse commit carrying the upstream shutdown fix while gooyoung/pi-langfuse#14 is reviewed; preserve the user-scoped OpenTelemetry override and npm audit gate, and register the exact Git source with Pi (#715).
Stop the cron reaper from reading a git statusfailure as evidence of uncommitted work. inspectFallbackWorktree returned dirty: true whenever git status --porcelain exited non-zero, so a worktree directory whose .git/worktrees/<name> admin entry had vanished was preserved as "needs manual salvage" on every single fire β 15 consecutive days for cron-prompt-miner-0718-0500, which also eroded WORKTREE_DIRTY's value as a triage signal by burying any real one under identical noise. Adds a distinct WORKTREE_ORPHANED outcome that removes the directory (git worktree remove fails once the admin entry is gone, and git worktree prune handles only the inverse case). The orphan is identified structurally β a .git file whose gitdir: target no longer exists β rather than by matching git status stderr, which is locale-dependent; every other status failure still preserves the worktree, since it cannot be shown that there is nothing to salvage (#694).
Resolve the prompt-miner daily-log write root to the main worktree instead of the ephemeral one. render-log-entry.sh used git rev-parse --show-toplevel, which under the cron's worktree: true returns the linked worktree β so every Step 5 log entry was written into .oh/worktrees/cron/<session>/ and destroyed when the runtime reaped it (fired 07-10, 07-14, 07-19, hand-recovered each time). Adopts the AUTOPILOT_LOG_ROOT β CRON_WORKTREE β toplevel resolution already proven at .oh/crons/prompt-miner.md:102 and documented at .oh/crons/README.md:120, preserving precedence for callers that do export the variable. Guarded by prompt-miner-log-root-worktree.sh, which builds a real linked worktree and asserts the entry lands in the main one β a fixture test cannot catch this class of bug (#693).
Make prompt-miner able to mine a marker at all, and stop it mining a wrong one. Three changes: (1) withinWindow now admits a session whose activity span overlaps the window instead of one that starts inside it β events are merged across resumed files by sessionId, so a long-lived session kept its original firstTs and vanished from a windowed query despite being worked in continuously; (2) subagent (isSidechain) turns are excluded from every signal β they carry the parent'ssessionId and role: "user", so a delegate briefing was counted as a human turn, and at 45,167 sidechain vs 42,679 non-sidechain lines this corrupted correctionDensity/turnBloat/toolErrorRate for precisely the delegating sessions most worth mining; (3) the daily cron's window is decoupled from its cadence (--hours 24 β --hours 336), because the marker gate needs β₯10 sessions in one stratum and a 24h corpus cannot supply that. Largest stratum by window, measured live: 24h β 4, 168h β 6, 336h β 18. The engine's hardcoded single project directory is deliberately not changed β the 23 directories it would add are this cron's own worktree sessions, whose first prompts have one distinct length and two distinct hashes, so they would cross the corpus floor while being incapable of producing a marker (#692).
Make the prompt-miner engine run when invoked through the .claude/skills directory symlink that SKILL.md Step 1 and the daily cron both prescribe. Node resolves symlinks for import.meta.url but not for process.argv[1], so the old entrypoint guard silently no-opped β exit 0, zero stdout, nothing written β and both existing prompt-miner probes hardcode the real .oh/ path, leaving them structurally blind to it. Swaps in the symlink-safe basename guard already used by rlm and weigh, and adds prompt-miner-symlink-entrypoint.sh, which invokes the engine through a real symlink and rejects the comparison β in either operand order β on any executable line under .oh/skills/**/*.mjs. Does not change the NO-CORPUS outcome β see #692 (#663).
Index .oh/docs/rfcs/rfc-runtime-support.md from .oh/docs/README.md, which had left a quarter of the RFC/ADR corpus unreachable from the index humans enter through, and repoint the dangling .claude/rules/ context bullet in the critic and implementer agents at .oh/context/IDENTITY.md β that directory was removed by the B-state M4 rules collapse (#686).
Make Slack bridge admin commands discoverable and functional by declaring /help, /trusted, /channels, /enable, /disable, /revoke, and /toggletools in .pi/install/slack-manifest.json, pinning bridge-side Socket Mode command handlers, and separating Slack commands from Pi's /msg-bridge surface in docs (#354).