Why the Proposal Is Not Allowed to Write

AutoHarness draws its most important trust boundary between a model’s proposal and a change to the live skill library. The previous chapter ended with a reflector placing a candidate lesson in a queue; this chapter follows what happens next. The goal is to understand why stage_skill is deliberately narrow, why the promoter is the only writer, and which guarantees protect the host without pretending that a validated file must be a correct lesson.

A proposal becomes an intent, not a file

The reflector runs language-model reasoning over an episode. It may decide that a repository convention deserves a new skill, that an existing rule needs a patch, or that two pieces of guidance conflict. That decision is useful precisely because it requires interpretation. It is also the reason to keep the reflector away from direct file operations: the model is handling untrusted transcript material, incomplete context, and a library whose instructions may themselves be imperfect.

AutoHarness gives the reflector a single write surface: a small MCP service named stage_skill. The name is slightly misleading if it is read as “write a skill.” The service stages an intent. It checks that the requested action is one of the supported kinds, that a skill name and a reason/evidence pair exist, and that the fields fit the action. A new or replaced body uses one shape; a patch uses an old string and a new string; a support-file removal uses a path; a deletion uses neither body nor delta. The service also checks basic frontmatter, body size, subfile paths, and other immediate structural conditions before it accepts the intent.

After those checks, the service appends a record to a per-run queue. It does not create the skill directory, write the proposed Markdown, create a sidecar, or alter an archive. The repository's tests make this boundary explicit: a successful call leaves an intent in the queue while the live skill tree remains absent. That is a valuable failure property. If the child process ends after staging, the system has a durable proposal but no half-authorized memory.

The queue also makes recovery legible. If the promoter never runs, nothing lands. If a process fails before the queue is cleared, a later drain can read the pending intent again. The intended pattern is append, validate and land, then clear. This is not a distributed transaction, and the source notes that cross-process locking and a small crash/replay window remain open concerns. But the queue is safer than an untracked direct edit because it leaves a discrete object that can be inspected, retried, or rejected.

The promoter behaves like admission control

The promoter is the component that turns an intent into a candidate filesystem state. It first resolves which layer owns the target. A new skill declares a project or global level; an update, patch, removal, or deletion must resolve an existing name across the two layers. Ambiguous same-name skills are rejected rather than guessed. This matters because a correct patch applied to the wrong scope is still a bad change.

For a create or update, the promoter uses the proposed full body. For a patch, it reads the live body and applies a delta only if the old text appears exactly once. A missing match means the proposal is stale; multiple matches mean the requested change is ambiguous. In either case, the promoter refuses to improvise. The result is shaped in memory before any write begins.

That in-memory result passes several kinds of admission checks. Structure verifies frontmatter and required identity fields. The description is treated as a routing signal: a new description must fit the index budget and contain a usable trigger, rather than merely naming a topic. Body altitude limits keep a skill closer to a rule than a transcript. Support files must sit under approved relative directories, stay within count and size limits, and be referenced by the body. Placeholders, unsafe paths, and representative malicious patterns are rejected. A global skill receives an additional repository-agnostic check because a project-specific path should not be propagated into every project.

These rules may look like formatting preferences until the system is viewed as a recall surface. A long or cue-less description can fail to route the lesson. An unreferenced support file is invisible to the future reader. A transcript-shaped body spends attention without giving the agent a clear instruction. A path that escapes the skill directory can turn a memory update into an unrelated filesystem operation. Admission therefore combines usability and safety: it asks whether the proposed state can be found, understood, and contained.

The promoter also checks provenance. A new skill receives the created_by: agent marker only after all validation passes. Updates, patches, file removals, and deletions require that marker already to exist. A skill written by the operator or installed from another source is not an object that AutoHarness may silently maintain. The same rule applies to consolidation: if a deletion claims that a narrow skill was absorbed into an umbrella, the umbrella must be a live, self-produced skill. A plausible name is not enough.

Return to the testing convention from the earlier example. The reflector might propose a short rule saying when a repository test uses parameterization, its cases should carry readable identifiers. The proposal must identify the skill, supply a reason for keeping the convention, and quote evidence from the episode. If it proposes a patch, the promoter will apply that patch to the current live body only when the expected old text occurs uniquely. If the skill has changed since reflection, the old text may no longer match; refusing the stale patch is safer than overwriting a newer lesson. If the proposed description is too long to route or the body contains unsafe instructions, the whole proposal is rejected before it can leave support files behind.

This is why “the model wrote a good Markdown file” is not the right success condition. The system needs a change that is attributable, scoped, routable, and compatible with the state that exists when the background job finally runs. The queue preserves the delay between those moments; the promoter rechecks the state at the point where the delay becomes a filesystem decision.

flowchart LR Q["Queued intent"] --> Shape["Shape proposed state"] Shape --> Gates["Validate structure, safety, and paths"] Gates --> Owner["Check layer and self-authored ownership"] Owner --> Pass{"Admission passes?"} Pass -->|No| Reject["Reject with no live-tree write"] Pass -->|Yes| Files["Land approved support files"] Files --> Evidence["Materialize redacted evidence"] Evidence --> Body["Atomically replace SKILL.md"] Body --> Meta["Update sidecar and append ledger"] Meta --> Live["Live or reversible archive"]

The order matters. The promoter checks the proposed structure before it writes support files. It resolves and checks every subfile destination before landing any of them, including the possibility that a pre-existing symlink could lead outside the skill directory. Only after that preparation does it materialize the evidence slice, write the live body, stamp or update operational metadata, and append the ledger entry. The live body is the final commit point for a create or update: a reader should not see a body that points at a support file which failed to land.

Evidence is handled as provenance, not as another model-authored support file. The promoter redacts the supplied slice again, stores it under a content-derived reference, and points the ledger at that materialized copy. This has two consequences. First, the skill body can remain short and readable while an operator can still inspect the source of a decision. Second, the model cannot forge a promoter-owned evidence filename or remove an evidence slice through the normal subfile channel. The record is therefore connected to the proposal without making the proposal responsible for the entire audit trail.

The same ordering makes retries less surprising. A missing support file, a rejected path, or a stale patch is handled before the body reaches its commit point. A crash may leave a temporary artifact for cleanup, but it should not expose a half-written skill. The durable queue can then be retried under the promoter's checks instead of relying on a human to guess which parts of a previous attempt succeeded.

Atomic storage protects state, not meaning

The low-level write primitive uses a temporary file in the same directory, flushes and synchronizes it, and then replaces the target. A crash during the write should leave either the old file or the new file rather than a half-written body. The same primitive supports skill bodies, sidecars, counters, and state files. Orphan temporary files can be swept later.

That guarantee is narrower than semantic correctness. Atomic replacement cannot tell whether the reflector misunderstood the repository convention. It cannot know whether a rule that passes the description trigger is too broad, or whether a valid patch makes two instructions subtly inconsistent. The promoter is an admission controller, not an expert reviewer. Its value is that it makes the model's authority explicit and narrows the classes of failure that can reach the live tree.

The evidence and ledger make accepted changes inspectable. The promoter redacts the evidence again at the point where it materializes it, gives the slice a content-derived name, and records a relative pointer in an append-only per-skill ledger. The model does not choose the evidence filename or write the provenance file itself. Later, an operator can see why a rule was created or changed without putting that explanation into the skill body that the host must recall.

Rejection is also part of the account. A failed proposal should not leave a sidecar marker that makes a nonexistent skill look self-authored, a ledger line that implies success, or a support file that survives a rejected body. The promoter's run-level account records which intents landed and which were rejected, along with rejection families. A later session can report that a proposal failed rather than allowing a silent disappearance to look like successful learning.

Deletion does not mean destruction. A retired or absorbed skill is moved into an archive with its evidence, sidecar, and ledger history. A merged narrow skill can carry an absorbed_into record that distinguishes consolidation from ordinary pruning. This distinction matters for both trust and diagnosis. If a skill vanished because it lost a capacity contest, that is a lifecycle event. If it vanished because a curator folded it into an umbrella, the system should preserve that different explanation.

A layered defense, with honest limits

The promoter is protected by more than one check. The child reflector is denied ordinary write tools at the hook layer. The proposal service restricts its action shape. The promoter revalidates instead of trusting the service's earlier checks. Layer and name functions reject unsafe identifiers and traversal attempts. The safety guard scans skill bodies and carried files for representative exfiltration, prompt-injection, destructive, persistence, network, and obfuscation patterns. Redaction protects transcript and evidence exits. Archives provide a reversible boundary after a skill leaves active recall.

This is defense in depth, not a sandbox. The safety guard is a regular-expression heuristic; it can miss an unsafe idea expressed outside its patterns and can reject benign text that resembles one. Redaction has the same pattern-based limit. The repository's tests demonstrate representative cases, including poisoned content and a symlink escape, but they are not a proof of comprehensive malicious-content detection. The strongest guarantee is structural: the model does not have the final filesystem write authority, and failed admission is designed to leave no live change.

The same caution applies to concurrency. Atomic replacement makes individual files robust against partial writes, while the durable queue makes an interrupted proposal recoverable. Neither one turns several related writes into an all-process transaction, and the source identifies cross-process locking as unfinished work. The system is therefore most trustworthy when its single-writer assumptions hold and when operators treat accepted lessons as inspectable artifacts rather than unquestionable truth.

By the end of promotion, a lesson has changed status in a precise way. It is no longer merely a model-generated suggestion; it is an owned, validated, evidenced skill or an archived state transition. That does not prove that the lesson is useful. It does establish who was allowed to change the host, which checks the change passed, and where its history can be found. The next problem is equally concrete: once the skill exists, how does it become visible to a future session, and how can the system distinguish being seen from being followed?

References