Architect Files Contract

The disk + runtime contract shared by every architect-pipeline variant — a reader's guide.

This contract is not about how the search works — it is about what every variant shares: the folder skeleton, which type each file belongs to, how arguments resolve to paths, what may and may not be edited, and how the pipeline talks to the backend. The contract itself is two layers: a global layer defines the type system, and a local layer (each variant's 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.
Contents
  1. Two-layer contract — global defines types, local fills the instance
  2. Folder layout
  3. The 8 file types
  4. The one universal file — active_workspace/
  5. Variant manifest — the local layer
  6. Resolve protocol — user input to disk path
  7. vN versus iter_M
  8. Edit whitelist — two boundaries
  9. Backend API + the active_workspace bridge
  10. Frozen workspace contract

1.Two-layer contract — global defines types, local fills the instance

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.

LayerLives inDefines
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.

Why was it rewritten as layered on 2026-05-20? The old contract's "universal" layer hard-coded a set of files that only the ADAS-family produces (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.

2.Folder layout

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

3.The 8 file types

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.

TypeDefinitionPlacementMutabilityLifecycle
inputuser- or bootstrap-authored config the run consumesvN/ root, or frozen workspace/architect read-onlyset before / at bootstrap
mutationone iter's workspace overlay (see §4, §9)iter_M/overlay (last-write-wins vs frozen)written during the iter
eval-outputproducts of a backend eval runiter_M/immutable once writtenwritten when eval finalizes
phase-artifacta product of one variant pipeline phase (proposal, spec, trace, debug log, lineage pointer, graph snapshot…)iter_M/immutable once committedwritten by the phase that owns it
iter-recordthe one canonical record / index of an iteriter_M/written oncewritten at iter commit
working-memoryknowledge persisting across iters (archive, distilled facts, self-authored tools)vN/ rootvariant-declared (append-only | mutable)grows across the run
rollupa cross-iter human-readable digestvN/ rootappend-only or write-once-at-endper iter, or at termination
bookkeepingresume / termination / staging statevN/ root (hidden dirs)transientengine-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.

4.The one universal file — active_workspace/

Regardless of the search algorithm, exactly one named file is mandatory for every variant:

Every 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:

5.Variant manifest — the local layer

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:

Form-1 default iter is driven by the phase-sentinel table

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".

6.Resolve protocol — user input to disk path

Every /architect:* skill uses the same rules to resolve command-line arguments into a (graph, vN, iter_M) triple. Three input forms, mixable.

Form 1 — all defaults

/architect:<skill>

Form 2 — named flags

/architect:<skill> --graph <name> --version <N> --iter <M>

Any subset; unset slots fall back to Form-1 defaults.

Form 3 — positional

/architect:<skill> <graph> [<version> [<iter>]]

Fixed order, trailing slots optional, but no skipping.

Graph uses fuzzy matching (graph only)

Resolution order: (1) exact; (2) case-insensitive exact with -/space normalised to _; (3) case-insensitive substring.

HitsBehaviourWhy
1use it, print graph: "<input>" → <resolved>lets the user confirm at a glance
0ERR + list all available graphsa candidate list beats "not found"
≥2ERR + list conflicts + require re-runnever 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.

Why graph, not nodeset

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.

7.vN versus iter_M

iter_MvN
Meaningone cycle within a vN (what a cycle is is variant-defined)a major pivot / restart
Incrementautomaticmanual experiment --new-version
Rerun in placeoverwrites files (for transient retry, not variance estimation)needs --allow-old-version
Cross-copywriting iter_{M+1} first copies iter_M/active_workspace/ over, then layers this iter's edits; iter_0 starts emptya 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.

8.Edit whitelist — two boundaries

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.

Hard wall (always BLOCK)

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.

Soft scope (warn, do not block)

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.

Why can't the hard wall be dropped? People often ask "since there's an overlay, isn't the whitelist redundant" — for the soft-scope half that is nearly true (the overlay sandboxes it). But the hard-wall half is not: framework and vendored code are outside the overlay's coverage — there is no sandboxed copy of them. One write to 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.

9.Backend API + the active_workspace bridge

This section looks like a network-protocol note, but it is really the rule that pins the disk file contract to runtime behaviour.

The active_workspace overlay — the contract's "bridge"

POST /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.

Why is this section in files-contract? This is the bridge that makes the 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.

10.Frozen workspace contract

<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.

Legal writers to frozen workspace

Source-of-truth quick reference

QuestionWhere 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