SPEC — `uni_propose_change` MCP verb (C-C1)
How to read this page
A Plain and a Clear version of this page have not been written yet. What follows is the document itself.
Precise — the source document
This is the document. Rendered from the repository at the commit above, with nothing rewritten for the web. A gate re-renders it on every deploy and fails the build if a single byte differs.
Status: SPEC. Awaits /lab-team-review before the implementation lands in production/mcp/server.py.
Author of record: the UNI OS+MIND Deepening Plan (workstream C-C1).
Ship gate: MERGED VERDICT required. Extension of production/mcp/server.py:235-260 (the MUTATING_TOOLS/HUMAN_GATED sets) AND production/mcp/help.py:56-186 (the TOOL_HELP dict) — both edits are required for the tool to boot (see Registration below). See docs/handoffs/UNI_OS_MIND_DEEPENING_HANDOFF.md for context.
Purpose
UNI must be able to author its own PRs against the SAME ship gate as any operator change. Today this is described in CLAUDE.md and docs/UNIVERSE.md but not codified as an MCP verb. Codifying it makes "code becomes UNI" auditable, reproducible, and revertable — every UNI-authored change lands via the existing git archive-of-tagged-ref path with a full evidence bundle attached.
Signature
@mcp.tool()
def uni_propose_change(
diff: str, # unified diff (git format-patch style) rooted at repo root
spec_path: str, # repo-relative typed spec, e.g. docs/specs/spine_lineage.md
red_launcher_path: str, # repo-relative RED launcher, e.g. runs/spine_red.exs
review_verdict_path: str, # repo-relative /lab-team-review receipt, e.g. docs/receipts/lab_team_review_<sha>.md
evidence_bundle: dict, # conforms to production/schemas/evidence_bundle.schema.json v1
) -> dict: # flat metadata()-shaped response (server.py:91-118); see
# "Envelope conformance" below — envelope.schema.json is an
# aspirational future target, not met by this or any other
# tool in server.py today.
Registration
Four edits land together (a partial registration boots a server that either can't gate the verb correctly or won't boot at all — see the third bullet):
production/mcp/server.py:247-254— add"uni_propose_change"to theMUTATING_TOOLSset literal.production/mcp/server.py:256— add"uni_propose_change"to theHUMAN_GATEDset literal (it is outward-facing/irreversible, so it isforce=True, session-auth-exempt, the same tier asadmit_guest/schedule). Never toIN_SHOW_VERBS(server.py:241-245) — this is not an in-show, session-pre-authorized verb.production/mcp/help.py:56-186— add a"uni_propose_change"entry to theTOOL_HELPdict. This is not optional:server.py's own boot-time self-check,_verify_tool_consistency()(server.py:922-945, the read-only-tools literal atserver.py:928-932), assertsTOOL_HELPis bijective withread_only | MUTATING_TOOLS | SESSION_UNGATEDand raisesRuntimeErrorat server build time (create_server(), called frommain()) if any registered tool is missing aTOOL_HELPkey. Registering the tool inMUTATING_TOOLSwithout a matchingTOOL_HELPentry means the server never boots.- The implementation itself, once it lands, follows the
async defMUTATING-tool shape documented atserver.py:8-14: firstawait asyncio.to_thread(approvals.require, "uni_propose_change", {"diff_sha": diff_sha, "spec_path": spec_path, "red_launcher_path": red_launcher_path, "tag": tag}, summary=json.dumps(evidence_bundle, sort_keys=True, separators=(",", ":")), force=True)— matching the real_LocalApprovals.require(self, tool: str, args: Dict[str, Any], *, summary: str = "", force: bool = False)signature exactly (server.py:188-189): there is noaction=parameter and no**kwargscatch-all, so any tag/action context belongs inside theargsdict (here, the"tag"key), not a separate keyword; andsummaryis typedstr, matching every existing call site inserver.py(e.g.summary=f"admit guest {guestId} to air"atserver.py:819) — never a raw dict.json.dumps(evidence_bundle, sort_keys=True, separators=(",", ":"))is the canonical serialization that makes the D-B1 claim ("the human's approval decision must be reproducible from the SAME bytes the agent saw") mechanically true: it is deterministic byte-for-byte regardless of the caller's dict key order, and it is what the human actually reads (the refusal path interpolatessummaryvia!r}into the denial reason atserver.py:197, so an un-serialized dict would show a Pythonrepr, not canonical bytes). On refusal, return_approval_refusal("uni_propose_change", decision, _AUDIT); on approval, do the apply/commit/tag work of the Approval + landing flow below viaasyncio.to_thread(...); write the audit row with_AUDIT.write({...}); returnmetadata("uni_propose_change", audit_id=rid, evidence_class="C", ...).
Refusal shape (up-front, before approvals)
The verb refuses with a fixed error if any of the following is missing or does not conform:
diffempty OR unparseable as a unified diff.spec_pathdoes not exist in the working tree.red_launcher_pathdoes not exist in the working tree.review_verdict_pathdoes not exist OR its frontmatter's merged verdict is notSIGNorSIGN_WITH_CHANGES.evidence_bundlefails schema validation againstproduction/schemas/evidence_bundle.schema.json#/$defs/uni_propose_change— theuni_propose_change-specific variant of the bundle schema, which requires (as non-empty strings, in addition to the genericschema_version/git_head_sha/timestampbase every mutating call carries)lab_team_review_receipt_path,typed_spec_path, andpaired_red_launcher_path. The base schema (top-levelrequired) stays generic across ALL mutating MCP calls per D-B1 and leaves those three fields optional so pure-OS mutations on other tools aren't forced to carry them; this verb validates against the stricter named variant specifically, which is what makes the prose requirement in Purpose/Signature above mechanically refusable rather than advisory. Resolution note (implementation-critical):#/$defs/uni_propose_changeis self-referential (allOf: [{"$ref": "#"}, {...}]) and MUST be resolved against the enclosing document — e.g.jsonschema.validate(instance, {"$ref": "evidence_bundle.schema.json#/$defs/uni_propose_change"}, resolver=...)or an equivalent call that keeps the schema's declared$idas the base URI. Do not extractschema["$defs"]["uni_propose_change"]into a standalone dict and validate against it in isolation — its"$ref": "#"then resolves back to that same standalone dict and recurses toRecursionError(reproduced directly againstjsonschema4.23.0; this repo has no otherjsonschema-validation call site to anchor the convention, so this sentence is the only guidance an implementer has).evidence_bundle.git_head_shais null OR does not match the current server-sidegit rev-parse HEAD.
Refusal envelope: the SAME flat shape every refusal in server.py returns —
metadata("uni_propose_change", ok=False, evidence_class="Sec", data={"reason": "<one-line>", "failed_field": "<field>"}, how_to_fix="<one-line>")
(server.py:91-118, and matching _approval_refusal()'s own real return shape at
server.py:223-231 for the post-approval-request refusal case below) — not the nested
{envelope:{...}, result:{...}} shape from production/schemas/envelope.schema.json, which
no tool in this file produces (see "Envelope conformance" below).
Approval + landing flow
- Verb accepts the call, validates all six invariants above.
- Computes the tag name deterministically:
uni-authored/<utc-ymd>/<first-8-of-diff-sha>. - Calls
approvals.require("uni_propose_change", {"diff_sha": diff_sha, "spec_path": spec_path, "red_launcher_path": red_launcher_path, "tag": tag}, summary=json.dumps(evidence_bundle, sort_keys=True, separators=(",", ":")), force=True)(per Registration step 4 — the real_LocalApprovals.require()signature,server.py:188-189, has noaction=parameter and typessummaryasstr;uni_propose_changeisHUMAN_GATED, soforce=Truealways, never satisfied by a live session). Blocks on human. - On approve:
a. Applies the diff on a fresh worktree at the current HEAD (git-apply --check first).
b. Commits with message
UNI-authored: <spec_path>and the review verdict as trailerLabTeamReview: <review_verdict_path>. c. Tags the commit with the deterministic tag. d. Runs the RED launcher named inred_launcher_path(best-effort; result recorded in the receipt). e. Writes a landing receiptproduction/docs/receipts/uni_authored_<tag>.mdwith YAML frontmatter (verdict: PASS|PARTIAL|FAIL,evidence_class: A). Since the tag itself contains slashes (uni-authored/<utc-ymd>/<first-8-of-diff-sha>), the filename MUST sanitize the tag first (e.g.tag.replace("/", "_")→uni_authored_uni-authored_2026-07-13_a1b2c3d4.md) rather than substituting it raw, which would otherwise create nested directories from a single path component. The slash-bearing tag format itself is unchanged and inherited fromproduction/docs/DEPLOYED_STATE.md:149; only this filename-construction step is newly specified here. f. Appends a row to thereplaced-by-unitable inproduction/docs/DEPLOYED_STATE.md. g. Appends a row toevidence/gates.ndjsonif a gate transitioned. The row's free-text fields (notes,pass_condition,falsifies_conditionpergate_row.schema.json) are populated verbatim from the already-reviewed diff/spec underreview_verdict_path— this step does not itself author new free-text gate-row prose, and does not check that text againstproduction/schemas/claim_fence.json(a systemic gap shared by every manualgates.ndjsonappend today, not introduced by this verb). - On deny: no side effects. Returns via
_approval_refusal("uni_propose_change", decision, _AUDIT)(server.py:215-231) — the same flatmetadata()-shaped refusal named under "Refusal shape" above, withdecision.reason == "operator denied"indata/how_to_fix, not a nested{refused: true, ...}envelope.
Non-goals
- The verb does NOT ship the tag to any peer. Shipping is a separate
os_execunder approvals. - The verb does NOT run the FULL RED against the live colony. It runs the launcher in check-mode where supported; the operator remains responsible for the live-RED session.
FALSIFIES
Per CLAUDE.md's science-gate discipline #4 (pre-registered PASS + FALSIFIES before the run —
every registered claim is judged only against what was registered): uni_propose_change's
landing-fidelity claim is falsified by —
the file tree at the landed tag diverges from
git applyof the exact diff bytes attested byevidence_bundle.git_head_sha, OR the landing receipt'sverdictfield disagrees with the RED launcher's actual exit code/stdout.
A disagreement means the verb landed a tag that does not faithfully represent the diff it
claims to have applied, or wrote a receipt that misrepresents the RED launcher's real result —
either voids the receipt's evidence_class: A claim until root-caused. This is exercised
pre-ship by the landing-fidelity and RED-launcher-fidelity tests in Test coverage below.
Test coverage the plan owes
The repo's test tree (test/) is Elixir/ExUnit-only (test/test_helper.exs + *_test.exs
under test/sp/, test/producer/, etc.) plus two .cjs scripts under test/body/; there is
no test/production/ directory and no pytest scaffolding anywhere in the repo (no
pytest.ini, no pyproject.toml test config, no conftest.py) to collect a *_test.py
file. The clearest precedent for a standalone Python test harness (as opposed to a
diagnostic/report script) in this repo is uni/brain/test_active_inference.py: a
self-contained script (no test framework — a
test_*() function per property, an assert-based body returning a one-line detail string
on success, and a main() that runs a [(label, fn), ...] list, prints PASS/FAIL per
case, and exits 1 on any failure), invoked directly with python uni/brain/test_active_inference.py.
This spec follows that precedent rather than inventing unbuilt pytest infrastructure:
production/mcp/test_uni_propose_change.py— co-located withserver.py/help.py(likeuni/brain/test_active_inference.pyis co-located withactive_inference.py), same self-contained shape: onetest_*()per refusal check (1–6 above) plus one for the schema-variant check (emptylab_team_review_receipt_path/typed_spec_path/paired_red_launcher_pathrefused byevidence_bundle.schema.json#/$defs/uni_propose_changeeven when the base schema would accept the bundle), amain()runner, invoked withpython production/mcp/test_uni_propose_change.py. If a future pass wants real pytest collection/CI wiring instead, creatingpytest.ini(or a[tool.pytest.ini_options]block in a newpyproject.toml) plusconftest.pyis a NAMED PREREQUISITE step that spec would have to add explicitly — it does not exist today and this spec does not assume it.production/mcp/red_team_uni_propose_change.sh— red-team: attempt to land a diff without a review verdict, without a spec, without an evidence bundle, and without the three C-C1-specific evidence fields (schema-variant refusal). ALL must fail closed. Lives besideproduction/mcp/server.py/help.py, mirroring the one red-team script that already exists for this MCP server,production/mcp/red_team_g_pa.sh(a live-HTTP CAPTURE tool driven against a real/prod-mcpendpoint with an operator-supplied bearer token, doing the real MCP streamable-HTTPinitialize+Mcp-Session-Idhandshake) — NOT underruns/, which is the Minecraft colony/lab-simulation harness tree (nursery_train.exs,pureworld_qa.exs,*_red.exs/*_gate.exsfor the SP genome/colony work) and an unrelated convention.(
production/scripts/broadcast_test.pyalso matches the*_test.pynaming convention and also exists — but it is a live 5-stage broadcast-diagnostic script (PASS/FAIL/SKIP per stage, JSON+Markdown report), not a self-contained unit-test harness, so it is not a competing shape for the file above to follow.)
None of the checks above touch the Approval + landing flow's positive path (apply/commit/tag/
RED-run/receipt-write) — a fabricated-but-schema-valid implementation that correctly refuses
malformed calls but, on approval, skips the real git apply/RED run and just writes a
hardcoded verdict: PASS receipt plus a fake DEPLOYED_STATE.md row would pass every check
named above. Closing that gap, in production/mcp/test_uni_propose_change.py:
- Landing-fidelity test: approve a call against a fixture diff on a scratch worktree;
assert the resulting file tree byte-matches an independent
git applyof the exact same diff bytes, and that the commit trailer / tag / receipt fields are all DERIVED from that real git state, never hardcoded. - RED-launcher-fidelity test: stub
red_launcher_path's script to exit non-zero; assert the landing receipt'sverdictfield comes outFAIL/PARTIAL(matching the real exit code), never a hardcodedPASS. - Fabrication-resistance test: stub
approvals.requireto approve but make the git-apply step itself a no-op; assert the test suite fails — i.e. this test proves the suite would actually catch an implementation that writes a schema-validPASSreceipt +DEPLOYED_STATE.mdrow without ever landing the diff. This is the FALSIFIES condition above, exercised pre-ship.
Envelope conformance
Every response from this verb follows the SAME flat envelope every other tool in
production/mcp/server.py returns: metadata() (server.py:91-118) on success,
_approval_refusal() (server.py:215-231, itself wrapping metadata()) on refusal —
{ok, tool, data, evidence_class, provenance:{server,version,git_commit,timestamp}, help, docs}
(+ audit_id/how_to_fix where applicable). This does NOT conform to
production/schemas/envelope.schema.json, whose nested {schema_version, envelope:{...}, result} shape with additionalProperties:false at both levels (envelope.schema.json:6-46)
no tool in server.py produces today. envelope.schema.json is a stated future migration
target, tracked separately — matching the identical correction already made in
SPEC_uni_self_audit.md's "Result shape" section — and is not claimed as met by this spec.
Cross-references
production/mcp/SPEC_uni_self_audit.md— the read-only self-attestation verb. Re-verified live (SPEC_uni_self_audit.md:117-131): that spec has already been rewritten with its own 3-step Registration checklist (tool decoration, theread_onlyliteral atserver.py:928-932, and a matchingTOOL_HELPentry) in the same shape as this spec's Registration section above — parity is already achieved, not pending. (This corrects a stale claim in an earlier draft of this section, which described the sibling spec as still missing theTOOL_HELP/boot-check step; it was fixed in the same pass that fixed this spec.) Both verbs land in the sameserver.pytool-registry area; keep the two Registration sections in the same shape going forward.production/docs/DEPLOYED_STATE.md— the growing "Replaced by UNI" table (added by this pass), whose own "Adding a row REQUIRES" list (DEPLOYED_STATE.md:133-136) independently names the same typed-spec + paired-RED-launcher + evidence-bundle requirement as refusal check #5.production/schemas/evidence_bundle.schema.json— the bundle contract; this spec validates against the#/$defs/uni_propose_changevariant specifically (refusal check #5), not the bare base schema.production/mcp/help.py— theTOOL_HELPdict this spec's Registration step 3 adds to.production/mcp/red_team_g_pa.sh— the existing red-team script this spec'sred_team_uni_propose_change.sh(Test coverage) mirrors in location and shape.runs/lab_team_review.exs— the runner that produces the review verdict receipt.
sha256 a2f021e9a9c5373b — of the original file, so what was ingested stays checkable.