The disk + runtime contract shared by every architect-pipeline variant — a reader's guide.
config.yaml § manifest) fills in
the instance. adas-subagent / aflow / myloop — and any future variant — work
freely on top of it. The source of truth is
.claude/commands/architect/_common/files-contract.md (rewritten
2026-05-20 as a layered contract); this page is its reading version,
emphasizing why it is shaped this way over a line-by-line translation.
The contract has two layers. The global
layer (this files-contract.md) defines the type
system; the local layer (each
variant's <variant>/config.yaml § manifest) fills in a
manifest instance.
| Layer | Lives in | Defines |
|---|---|---|
| global | _common/files-contract.md |
folder skeleton; the 8 file types (each fixes placement / mutability / lifecycle); the one universal file; the manifest schema; resolve protocol; versioning rules; edit whitelist; backend bridge; frozen-workspace rule |
| local | <variant>/config.yaml § manifest |
one manifest instance — every file this variant writes, classified into a global type, with a one-line purpose, a schema pointer, and an access matrix; plus the lineage model and phase-sentinel table |
The principle: global defines the type system and the manifest schema; local fills in a manifest instance. A file's mechanical rules (which folder, can it be mutated, when is it written) are inherited from its type; the local manifest only adds identity (purpose, shape, who touches it). Adding a new variant file never edits the global contract — you classify it.
metrics.json / parent.txt /
trace.md / summary.csv …) — files myloop never
writes, a latent contract violation. (parent.txt in fact was
produced by no variant at all.) The universal layer now holds exactly
one named file (active_workspace/); everything
else is classified by type and declared per variant in a manifest.
The old sentinel-file convention is folded into that manifest.
workspace/ # FROZEN inputs — architect NEVER writes
└── architect/exp_profiles/{graph}.yaml # per-graph eval profile (an `input` file)
outputs/design_runs/
└── {method}/ # variant slug: adas-subagent | aflow | myloop | ...
└── {graph}/ # one dir per graph this method has worked on
├── _archive/ # method+graph-scoped historical snapshots
└── v{N}/ # major version (manual --new-version)
├── <vN-scoped files> # type ∈ {input, working-memory, rollup}
├── iteration/
│ ├── iter_0/ # baseline (no parent, no mutation)
│ │ └── <per-iter files> # type ∈ {mutation, eval-output, phase-artifact, iter-record}
│ ├── iter_1/
│ └── ...
├── .staging/iter_M/ # bookkeeping — transient pre-commit area
└── .loop_state/ # bookkeeping — resume / termination state
outputs/design_runs/{method}/{graph}/v{N}/ — each method owns
its own tree, nothing is shared across methods (including
_archive/).v{N}/iteration/iter_{M}/, not directly under
v{N}/.workspace_snapshot/ instead of active_workspace/;
read-only skills try active_workspace/ first, then fall back.Every file an architect run writes belongs to exactly one of 8 types. A type fixes three mechanical rules — placement (which folder), mutability (can it change after first write), lifecycle (when it is written). The variant manifest classifies its files; it does not restate these rules.
| Type | Definition | Placement | Mutability | Lifecycle |
|---|---|---|---|---|
input | user- or bootstrap-authored config the run consumes | vN/ root, or frozen workspace/ | architect read-only | set before / at bootstrap |
mutation | one iter's workspace overlay (see §4, §9) | iter_M/ | overlay (last-write-wins vs frozen) | written during the iter |
eval-output | products of a backend eval run | iter_M/ | immutable once written | written when eval finalizes |
phase-artifact | a product of one variant pipeline phase (proposal, spec, trace, debug log, lineage pointer, graph snapshot…) | iter_M/ | immutable once committed | written by the phase that owns it |
iter-record | the one canonical record / index of an iter | iter_M/ | written once | written at iter commit |
working-memory | knowledge persisting across iters (archive, distilled facts, self-authored tools) | vN/ root | variant-declared (append-only | mutable) | grows across the run |
rollup | a cross-iter human-readable digest | vN/ root | append-only or write-once-at-end | per iter, or at termination |
bookkeeping | resume / termination / staging state | vN/ root (hidden dirs) | transient | engine-managed |
Two boundary notes: lineage pointers
(parent.txt, a per-iter graph.json copy,
eval_run_id.txt) are not a separate type — they are
phase-artifacts written by whichever phase records them; the
lineage model is declared once in the manifest. tools/*.py
(a variant's self-authored analysis code) is working-memory —
placement / mutability / lifecycle all match.
Regardless of the search algorithm, exactly one named file is mandatory for every variant:
iter_M/ has an active_workspace/ (type
mutation) — the complete mutation set of that iter relative to the
frozen workspace/. It MAY be empty or absent for an iter with no
mutations (a baseline, or a no-patch probe); the eval then falls through to
frozen for every file.
It is universal because it is coupled to the framework, not
to the search algorithm: the backend's eval API consumes it through the
active_workspace_dir field (see §9).
Two further per-iter properties are roles bound in the manifest, not universal files:
metrics.json; myloop: record.json).iter_M finds its parent. The
variant declares a lineage model: implicit_linear
(parent = iter_{M-1}) or parent_pointer (the parent
id is recorded in a variant-declared location — a standalone pointer file,
or a field in a phase-artifact's frontmatter).Every variant must ship a manifest: block in
<variant>/config.yaml. It is the single source of truth for
file identity; file shape stays in the
variant's schemas.md (or equivalent), pointed to by each entry's
schema: field.
manifest:
lineage:
model: implicit_linear # implicit_linear | parent_pointer
pointer: <location> # required iff parent_pointer — a file path,
# or "<file>:<field>" for a frontmatter field
metrics_payload: record.json # which file plays the "metrics payload" role (§4)
files:
<filename-or-dir>:
type: <one of the 8 types> # required
purpose: "<one line — what this file is for>" # required
schema: <pointer, e.g. schemas.md#4> # optional; omit for trivial files
mutation: <append-only|mutable|immutable> # optional; refines the type default
written_by: [<skill>, ...] # access matrix
read_by: [<skill>, ...]
phase_sentinels:
<skill>:
needs: <file produced by the previous phase, or null>
writes: <file this phase produces>
Global rules attached to the manifest:
manifest: block is non-conformant —
understand and validators ERR.<variant>/*.md) MAY name their own
files directly — they are variant-specific by construction, so this is not a
violation. But shared _common/ skills and any cross-variant
consumer (validators, a fresh session reconstructing run state) SHOULD
resolve file identity through the manifest so they stay correct as variants
diverge.experiment / evaluator class)
have no needs in phase_sentinels — they may open a
fresh iter_{max(M)+1}.In phase_sentinels: needs = the file that must
exist before this skill starts (proof the previous phase finished);
writes = the file this skill produces. The Form-1 default iter is
"the latest iter where needs exists and writes
does not".
Every /architect:* skill uses the same rules to resolve
command-line arguments into a (graph, vN, iter_M) triple. Three
input forms, mixable.
/architect:<skill>
graph = the most-recent-mtime entry under the skill's graph sourceversion = max N of v<N>/; if none, v0iter = driven by the variant's phase_sentinels
(see §5); entry-point skills default to iter_{max(M)+1}/architect:<skill> --graph <name> --version <N> --iter <M>
Any subset; unset slots fall back to Form-1 defaults.
/architect:<skill> <graph> [<version> [<iter>]]
Fixed order, trailing slots optional, but no skipping.
Resolution order: (1) exact; (2) case-insensitive exact with -/space normalised to _; (3) case-insensitive substring.
| Hits | Behaviour | Why |
|---|---|---|
| 1 | use it, print graph: "<input>" → <resolved> | lets the user confirm at a glance |
| 0 | ERR + list all available graphs | a candidate list beats "not found" |
| ≥2 | ERR + list conflicts + require re-run | never auto-pick — avoids silent cross-talk between parallel pipelines |
version / iter accept a bare or prefixed integer
(0/v0/iter_3), exact match only — no
fuzzy.
A nodeset is derived from a graph: each node's
<nodeset>__<node> prefix in graph.json
already enumerates every nodeset used. An iter is the atomic unit of evolution;
one iter may edit several nodesets at once, but there is no "per-nodeset iter"
concept.
| iter_M | vN | |
|---|---|---|
| Meaning | one cycle within a vN (what a cycle is is variant-defined) | a major pivot / restart |
| Increment | automatic | manual experiment --new-version |
| Rerun in place | overwrites files (for transient retry, not variance estimation) | needs --allow-old-version |
| Cross-copy | writing iter_{M+1} first copies iter_M/active_workspace/ over, then layers this iter's edits; iter_0 starts empty | a new vN starts from frozen workspace/; no cross-vN copy |
Write-skill version protection: any skill whose
phase_sentinels writes is non-empty, plus entry-point
skills, refuse to write a non-latest vN without an
explicit --allow-old-version. Read-only skills
(analyze, report, understand) need no
flag. The current version is max(N), derived from the filesystem —
no pointer file.
Write skills are bounded by two edit boundaries of different
strength. They differ because the active_workspace/ overlay only
covers workspace/{graphs,nodesets}/ — anything outside it has no
sandboxed copy.
agentcanvas/backend/app/** (framework) and
third_party/** (vendored) are never editable. These
paths have no overlay: writing them is a real, global,
cross-session mutation that also hits the user's :8000 backend. A
patch touching them → BLOCKED, surfaced to the user. The overlay cannot save
this wall — it does not reach here.
Everything under {ITER}/active_workspace/{graphs,nodesets}/ is
writable — the overlay sandboxes it, so a bad edit is discarded with the iter
and never touches frozen workspace/. The expected scope of
one iter is that iter's graph plus the nodesets it uses (the
<nodeset>__<node> prefix of each node's type;
a nodeset may be a flat <ns>.py, a package
<ns>/**, or a server-mode server/<ns>(.py|/**)).
A patch touching a graph or nodeset outside that expected scope
makes the implementer warn, not block — a nodeset imported
transitively by another never appears in any type prefix, is
legitimately "off-scope", and must remain editable.
backend/app/** is one real global mutation. The overlay cannot
substitute for this wall.
Server-mode nodesets (nodesets/server/**) are
editable when the graph uses them (TODO #60, 2026-05-15): an overlay edit to a
parallelism="shared" server nodeset triggers an ephemeral
auto_host child at eval-admit time.
This section looks like a network-protocol note, but it is really the rule that pins the disk file contract to runtime behaviour.
/api/eval/v2/{start,status,export,stop}{BACKEND_URL}/health (not /api/health)start_episode_indexPOST /api/eval/v2/start
{
...
"active_workspace_dir": "{ITER}/active_workspace" ← must be an absolute path
}
Every start must include
active_workspace_dir so the eval subprocess overlays this iter's
mutation set on frozen (frozen → active, last-write-wins). Without it the run
uses pure frozen workspace — correct only for an iter with no mutations.
POST /api/eval/v2/introspect accepts the same field.
mutation-type file (§4)
load-bearing at runtime — and the reason active_workspace/ is the
one universal file. Without this section the file contract would be an empty
promise.
<repo>/workspace/ is the frozen faithful
baseline; architect skills never write to it directly. All
mutations land in {ITER}/active_workspace/ and the backend overlays
them at run time.
/api/canvas/graphs.py) — the human editorexperiment auto-creating a missing
workspace/architect/exp_profiles/{g}.yaml on first run (an
input file, not method code)| Question | Where the answer lives |
|---|---|
| Which folder does file X go in? | X's type in the variant manifest → §3 placement rule |
| Can file X be mutated after writing? | X's type (+ optional mutation override) |
| What is file X for? | X's purpose in the variant manifest |
| What does file X's content look like? | X's schema pointer in the manifest → variant schemas.md |
| Who writes / reads file X? | X's written_by / read_by in the manifest |
| Which iter does a default-form skill target? | the variant phase_sentinels table → §6 |
| What file names does variant V use? | V/config.yaml § manifest — and only there |