Agent context vs human docs: a boundary pattern for im-web-prototypes

Draft

Research, 2026-10-09. Not applied as a whole; the parts in use are described in docs/routing.md and AGENTS.md.

Research date 2026-10-09, Claude Code 2.1.295. Read-only: nothing in the repo was changed. (Plan mode was active, so this report is written to the plan file. The requested path, …-agent-context-boundaries.md, was not writable.) Builds on the earlier report (…-agent-abff93c9579131301.md, now also ideas/agent-context-setup.md). Only new or re-verified facts are repeated here.

Labels: [V] verified against a source listed in §9 (fetched 2026-10-09). [O] my opinion or design. [?] unverified, test before relying on it.


1. The problem in one picture

                 written for            read by            binds?
AGENTS.md        agents + humans   ->   every session      yes, but mixed with explanation
-docs/*.md       humans + agents   ->   agent when nearby  "decisions.md wins" (prose rule)
DocKindBanner    agents (!)        ->   humans             shows binding talk to people
handovers        agents            ->   humans in Docs nav  WIP shown as docs
ideas/drafts     nobody decided    ->   agent reads them   become decision records

Three different things are mixed: what an agent must obey, what a person wants to understand, and what nobody has decided yet. Today a file's location doesn't tell you which of the three it is, so both readers guess. [O]

Main idea [O]: decide by where a file is, not by what it says. Claude Code tooling (rule paths, permission rules, hook if) can only select paths, never frontmatter or status fields [V: S1 "paths is the only frontmatter field"; S2 if takes one permission rule; S3 rules match file_path]. So the lifecycle state has to be part of the path, or live in typed data that a generator filters.


2. Verified mechanics this pattern relies on (new or re-checked)

NeedMechanismExact behaviourSource
Hard rule for one area.claude/rules/*.md with paths:Loads when Read/Write/Edit (or cat/head on one file) touches a match. No precedence: "Claude may pick one arbitrarily" when two conflictS1 [V]
Keep a folder out of reachpermissions.deny / ask with Read(...)Applies to Read and Edit tools, @file mentions, IDE context, recognised Bash file commands (cat, head, tail, sed, tee) and redirects. Grep/Glob: "best-effort", rules applied to the searched directory. Not covered: grep -r pattern . run from a parent directory, and scripts that open files themselves. OS-level blocking needs the sandboxS3 [V]
Hard OS-level read blocksandbox.filesystem.denyReadApplies to Bash and its child processes only. "A denyRead entry doesn't stop the Read tool": pair it with a Read denyS4 [V]
No .claudeignorenone"If your project has a .claudeignore file, it has no effect, so move its entries into Read deny rules"S3 [V]
Skip instruction filesclaudeMdExcludes (glob against absolute paths, any settings layer, arrays merge)Excludes CLAUDE.md / rules files only. It doesn't hide ordinary docsS1 [V]
Block writes to a pathEdit(...) deny/ask (covers Write too; Write(...) rules are never consulted)Order deny → ask → allow. A PreToolUse hook's "allow" can't override a deny/ask ruleS3 [V]
Block with your own logicPreToolUse hook, exit 2 or {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"…"}}Can read env vars and files, so it can know a "focus"S2 [V]
Procedure only the user triggersskill with disable-model-invocation: trueDescription not in context, only /name runs itS5 [V]
See what loaded/context (Memory files at launch), /memory (files, auto-memory toggle), Loaded <path> line for nested files, InstructionsLoaded hookHook input: file_path, memory_type, load_reason (session_start, nested_traversal, path_glob_match, include, compact), globs, trigger_file_path, parent_file_path. Can't block; async, observability onlyS1, S2 [V]
Auto memory as a bleed channelautoMemoryEnabledAlready false in the user's ~/.claude/settings.json (checked). Keep it off: it would carry lessons from a draft session into every later sessionS1 [V]
Plugin skillsskillOverrides doesn't affect plugin skills; manage them with /pluginNote: the installed mattpocock-skills:domain-modeling description says "record an architectural decision": a likely source of unprompted decision recordsS5 [V] (cause: [O])

Practice sources:

  • agents.md: "README.md files are for humans… AGENTS.md… the extra, sometimes detailed context coding agents need"; "The closest AGENTS.md to the edited file wins"; "explicit user chat prompts override everything" (S6 [V], undated live page).
  • Anthropic, context engineering: "the smallest possible set of high-signal tokens"; just-in-time context; context rot (S7, 2025-09-29 [V]).
  • MADR 4.0.0 (2024-09-17): status: proposed | rejected | accepted | deprecated | … | superseded by ADR-0123 (S8 [V]).
  • Diátaxis: four forms on two axes, action/cognition × acquisition/application (S9 [V]).
  • llms.txt (Jeremy Howard, 2024-09-03): an H1, a blockquote, H2 link lists, an "Optional" section, and .md twins of pages (S10 [V]). It targets agents reading a website. Agents here read the repo, so it isn't needed [O].
  • Secondhand only [?]: Osmani's "≤150 lines; if AGENTS.md is longer than README it's too long"; an "ETH Zurich study" claiming README-style AGENTS.md hurts agents by 2–3%. Both were found only through blog summaries (S11), not verified.

3. The pattern: four zones, one direction of flow [O]

   IDEAS (drafts)           PROPOSALS              DECIDED                 AGENT CONTEXT
   ideas/**            ->   proposals (data)  ->   -lib/decisions.ts  ==>  .claude/rules/generated/*.md
   agent: asks first        agent: reads,           -lib/rules/*.ts         AGENTS.md (core)
   human: Docs › Ideas      "not adopted" rule      agent: binding          .claude/rules/*.md (hand-written)
                            human: Docs › Proposals human: illustrated      never in the docs nav
                                                    pages                   (only via the context map)
             promotion is a human act: /decide (user-only skill) + Edit "ask" on decided files
ZoneFolder / globWho writesAgent sees itHuman sees it
A. Agent context (hard rules)AGENTS.md, CLAUDE.md (@AGENTS.md), .claude/rules/**/*.md, .claude/rules/generated/**, .claude/skills/**, .claude/settings.jsonhuman, or generated from Dalways, or on matching pathonly through the context map (§6), never as prose pages
B. Human docssrc/routes/*/*/-docs/** (prose), src/routes/*/*/_docs/** (pages), src/routes/docs/**, docs/*.mdhuman (agent on request)as reference when touched; never bindingthe docs site
C. Ideas / draftsideas/<slug>.md or ideas/<experiment-id>/<slug>.md (repo root, outside src)human, or agent when askedonly when the user names it: Read and Edit set to askDocs › Ideas, marked "Draft"
D. Decided recordssrc/routes/*/*/-lib/decisions.ts, src/routes/*/*/-lib/rules/*.ts (machines), src/shared/terms/*.ts (decided terms)human via /decide; edits ask firstthrough generated rules (decided items only) plus the TS itselfillustrated pages built from the same data

Proposals stay where they are (src/shared/docs/proposals/*.ts). A rule scoped to that path says "under discussion, not adopted". They are a sub-state between C and D that the team owns.

Why typed data for D [O]: one record has two short texts, a human headline and an agent rule line, plus status. A generator gives the agent decided items only, as compact rule lines, scoped to the experiment's path. The human page renders headline, illustration and "why" with progressive disclosure. Superseded and open items never reach the agent as rules. This is what the XState charts already do for business rules, generalised.

3.1 Lifecycle, where the state lives

StateWhereAgent treats asEnforced by
idea, draftideas/**nothing (asks before reading)permissions.ask: Read(/ideas/**), Edit(/ideas/**) plus .claude/rules/ideas.md (paths ideas/**: "draft, binds nothing, don't turn into decisions, don't read other ideas")
proposalsrc/shared/docs/proposals/**input, not a rule.claude/rules/proposals.md
open questionmeta.question in machines; status: 'open' in decisions.tsexplicitly undecided: never invent an answergenerator emits open items as a "Don't decide these" list; already the repo's statechart rule
decideddecisions.ts status: 'decided', machines in -lib/rules/binding inside its experimentgenerated rule file; bun test fails when the generated file is stale
supersededsame record, status: 'superseded', supersededBy: 'D5'absentthe generator skips it; the human page shows it in a history timeline

The folder separates draft from not-draft (tooling can see it). A typed field separates decided, open and superseded inside D (the generator and types can see it). Frontmatter status in markdown is used for display only, never for enforcement, because nothing in Claude Code reads it [V: S1].

3.2 Zones run inside silos

Zones A–D describe what kind of context a file is. Silos (§4) describe whose it is. Every file in zones C and D, and every generated rule, belongs to exactly one silo or to the shared tier. That is a core requirement, not an option.


4. Silos: one experiment, one context (core requirement)

The repo exists to hold conflicting approaches side by side. An agent working on prototype A must never read or apply prototype B's decisions, rules, drafts or notes. "Aligning" experiments is the failure mode, not a kindness. [user requirement]

4.1 Verified enforcement options

OptionWhat it doesVerdict for silosSource
Path rules per experiment (paths: src/routes/prototypes/baseline/**)B's rules load only when a B file is touchedNecessary, not sufficient: nothing stops the touchS1 [V]
Nested CLAUDE.md per experimentLoads on the first Read/Write/Edit of a file in that folderSame weakness: reading one B file pulls in all of B's instructions. Prefer central generated rules (visible to the context map)S1 [V]
permissions.deny / ask on sibling pathsBlocks Read/Edit tools, @ mentions, cat/head/tail/sed, redirects. Grep/Glob best-effort. Not grep -r from a parent dir or scriptsPrimary mechanism. Applies to subagents too: "The rule applies to the main conversation and to subagents"S3, S12 [V]
claude --settings <file>Adds a settings layer for this session, above project and user. Lists merge across layers, so a silo file adds deny rules to the project'sHow focus is set: one generated settings file per siloS13 [V]
claudeMdExcludes in the same silo fileSkips sibling rule files by glob, even if a sibling file gets touchedBelt and braces for instruction bleed. Works "at any settings layer" [V]; that --settings counts as one is [?] (S13 says it "can set any key your user settings file can set")S1, S13
PreToolUse hook with the silo id in its command (silo-guard.sh baseline)Sees tool_input.file_path (always absolute) for Read/Edit/Write and path/pattern for Grep/Glob; for Bash, the command text. Exit 2 or permissionDecision: "deny"Closes what permissions miss: grep -r/rg/find over src/routes, a Grep/Glob whose path is a parent of siblingsS2 [V]
Launch from the experiment folder (cwd = silo) + blockReadsOutsideWorkingDirectoriesFile tools refuse reads outside the working dirs in every modeDon't use. .claude/settings.json is read "from the session's primary working directory", so launching in a subfolder loses the project's hooks and permissions. Bash commands (bun run dev, lint) need the rootS3, S13 [V]
Worktree (claude -w, worktree.sparsePaths)Separate checkout; sparse paths check out only listed dirsPhysically removes siblings, but the router, lint (type-aware, whole repo) and typed links need every route. Fine for an idea worktree, wrong for experiment work. Note: worktree isolation checks guard edits into the main checkout, not readsS14 [V]; sparse details [?] (index entry only)
Sandbox filesystem.denyRead for siblingsOS-level, Bash and children onlyBreaks bun run lint, tsr generate, vite, which read all routes. Hooks run outside the sandbox. Not recommendedS4 [V]
Subagents (.claude/agents/<x>.md with hooks, disallowedTools, omitClaudeMd)Per-agent hooks and tool lists. No per-agent path permissionsUseful for a one-off "work only in X" delegate (hooks with the silo id). Project agents' hooks run only after folder trustS12 [V]
Import boundary check (Fallow boundaries, §4.4)Fails when silo A imports silo B. Resolved graph, autoDiscover per folderCode-level isolation, independent of agents. Today's edges: baseline→receipt, product-admin→receipt and image-grid, presentations→image-grid (14 imports, Fallow run). No prototype imports another prototyperepo + S15 [V]

Tiers (each file belongs to exactly one, shown in the context map). The code boundary and the context boundary are the same line (§4.3):

TierPathsHolds product decisions?Code may import it?Agent in silo A
Project (how to work here)AGENTS.md, .claude/rules/*.md (hand-written), scripts/**, confignon/areads; rules load as usual
Platform (neutral mechanisms)src/platform/** (today's src/shared/{format,media,docs shell,statechart,theme,brand,page-meta,route-guard}, src/components/ui, src/lib/utils, src/styles), data/**no, by definitionyes, every experimentreads; Edit asks (a change reaches every silo)
Reference (never binding, never imported)src/reference/{im-web,proposals,working-model}/** (today src/shared/docs/im-web, …/proposals, baseline's -lib/rules as working model), docs/im-web/**describes or proposes themno: only src/routes/docs/** renders it; experiments copy from itreads, as input; a rule per folder says what it is
Silo <id>src/routes/<kind>/<id>/**, ideas/<id>/**, .claude/rules/generated/<id>.mdyes, its ownonly by itselfown silo: everything. Other silos: deny Read (no uses exceptions once §4.3 is done)
Unsiloed ideasideas/_inbox/**not yetnoask

The working model moves from "baseline's rules that others may follow" to a reference folder that every experiment, baseline included, copies from. Departing from it is then a visible diff plus a decision record, not a silent edit to shared code. [O]

How a session gets a silo:

bun run focus baseline
  -> scripts/focus.ts reads registry.ts and writes .claude/silos/baseline.json (gitignored, regenerated each time)
  -> exec claude --settings .claude/silos/baseline.json -n "baseline"

.claude/silos/baseline.json (generated):

{
  "permissions": {
    "deny": [
      "Read(/src/routes/prototypes/product-admin/-docs/**)",
      "Read(/src/routes/prototypes/product-admin/-lib/decisions.ts)",
      "Read(/src/routes/prototypes/product-admin/-lib/rules/**)",
      "Read(/ideas/product-admin/**)"
      // ... per sibling; with §4.3 done, simply "Read(/src/routes/prototypes/product-admin/**)"
      // Read deny also blocks Edit and Write (S3)
    ],
    "ask": ["Edit(/src/platform/**)", "Edit(/src/reference/**)"]
  },
  "claudeMdExcludes": ["**/.claude/rules/generated/product-admin.md", "..."],
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Grep|Glob",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/silo-guard.sh baseline"
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/silo-intro.sh baseline"
          }
        ]
      }
    ]
  },
  "statusLine": {
    "type": "command",
    "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/silo-status.sh baseline"
  }
}
  • silo-guard.sh <id> denies Grep/Glob whose path is a parent of sibling silos (e.g. src/routes), with the reason "search inside src/routes/prototypes/baseline or src/shared". It also denies Bash grep -r/rg/find/cat naming sibling silo paths or recursing over src/routes. It is best-effort for Bash (S2 says so for if). The script parses the command itself.
  • silo-intro.sh prints SessionStart additionalContext as facts: "This session works on Baseline (prototype). Shared: project rules, working model, reference. Other experiments are closed in this session." (S2 [V] additionalContext.)
  • The status line shows ◆ baseline so the human always sees which silo the session is in.
  • The silo id is baked into hook commands, so nothing depends on env-var inheritance [?].
  • Leaving a silo means ending the session. Settings from --settings are fixed for the session. Cross-silo work ("compare baseline and product-admin") is a deliberate unfocused session started with plain claude, where the generated rules still scope each silo's decisions to its own paths.
  • Delegation: subagents inherit the deny rules (S12 [V]). Explore/Plan skip CLAUDE.md but not permissions.
  • A silo is a folder. Fallow's autoDiscover turns every child of src/routes/prototypes and src/routes/features into a zone (§4.4). scripts/focus.ts reads the zones from fallow list --boundaries -f json (verified output: zone names, patterns, file counts, rules), and the registry adds titles. Silo settings, generated rules and the context map share that one definition, so a new experiment folder is siloed automatically.

Checks that keep the silos true:

  1. Import boundary: fallow dead-code --boundary-violations with the config in §4.4. It works on the resolved module graph (it caught the @/ alias imports; relative climbs resolve the same way), so no separate import-graph test is needed. It runs per edited file in a PostToolUse hook and in full at commit.
  2. Zone coverage: boundaries.coverage.requireAllFiles reports any source file outside every zone, so a new top-level folder can't slip past the silo map.
  3. Generated rule paths for <id> never match a file outside <id>.
  4. No hand-written rule in .claude/rules/ has paths reaching into a single experiment (those belong in its generated rule).
  5. No -docs/*.md names another experiment's decision IDs ("as product-admin D12") except in a declared comparison doc.

4.3 Code isolation: experiments import only the platform [O, evaluated against the repo]

The main thread's proposal: experiments never import other experiments, they copy. They import only a neutral platform layer with no product decisions. Anything shared that carries a product decision moves into an experiment. Proposed enforcement: no-restricted-imports overrides generated per experiment folder in oxlint.config.ts. Recommended instead: Fallow boundaries (§4.4), which need no generated config and check resolved imports.

Verdict: adopt it. Don't go further to full self-containment (zero imports outside the folder).

OptionGainCostVerdict
Status quo (prototypes may import features)No copiesSilo A's agent must read B's code (uses exceptions); a fix in receipt silently changes baseline's checkout; context and code boundaries differNo
Platform-only imports (proposal)Code boundary = context boundary; permissions become "deny every sibling"; each experiment can change anything it owns3 dependency edges to cut: baseline→receipt, product-admin→receipt+image-grid, presentations→image-grid (verified: no prototype imports a prototype; src/routes/docs imports no experiment)Yes
Full self-containment (copy shadcn, format, docs shell into each)Total independencecomponents/ui alone has 215 imports across experiments, shared/format 81, shared/docs 79 (counted). Copies of mechanisms with no product meaning drift for no benefit, and the docs site loses one lookNo: the copies would carry no decisions, so there's nothing to isolate

The test for "platform": could two experiments reasonably want it to behave differently as a product question? If yes, it's not platform. [O]

TodayProduct decision inside?Goes to
components/ui, lib/utils, styles, shared/theme, brandno (vendored, look of the live site)platform
shared/format.tsFORMAT_LOCALE = 'nb-NO' is a project convention (baseline D1 restates it). Price display rules (rounding, "incl. VAT") are product decisionsplatform keeps formatters with a locale parameter; rounding and VAT wording stay in experiments
shared/media (types, fixtures, preview)no: mirrors NTB's API, must not changeplatform
shared/statechart, docs shell, page-meta, route-guard, docs navno (engines)platform
shared/prototypes (registry, prototype menu, app header)registry = project metadata; app header = live-site chromeplatform (registry could sit under project)
shared/docs/im-web, shared/docs/proposals, doc-kinds textcontent, not mechanismsrc/reference/
baseline -lib/rules as working modelyessrc/reference/working-model/, copied by each experiment
features/receipt used by baseline and product-adminyes (seller, VAT lines, layout = R decisions)copied into each prototype
features/image-grid used by product-admin and presentationsmostly engine (virtualisation, tiles); its decisions are about the grid's look and behavioureither graduate the engine to platform (src/platform/image-grid) and keep the feature as its docs and demos, or copy. Graduate if the engine is stable

Naming and location: src/platform/ (flat: ui/, format/, media/, statechart/, docs/, theme/, brand/, routing/), with an @/platform/* alias. "Platform" says "what experiments stand on" and pairs with "reference" (what they read) and "routes" (what they are). shared says nothing about decisions, which is the property that matters. Keep shadcn where components.json aliases expect it, or repoint the aliases to src/platform/ui [?: check shadcn CLI with custom alias]. src/reference/ holds data and renderers for /docs only.

Lint fallback (only if Fallow isn't used). oxlint 1.87 supports overrides[].files and no-restricted-imports with patterns (group, regex), checked in node_modules/oxlint/dist/index.d.ts. A readdirSync in oxlint.config.ts can generate one override per experiment, with regex: ^@/(routes/(?!<kind>/<id>/)|reference/). Weaknesses: it matches the specifier text, not the resolved file, so it misses relative climbs (../../receipt/-lib/x) and import.meta.glob, and it needs regex escaping per folder. Fallow covers all of that with five lines of config. [O]

Copies and drift:

  • Copy with a script, not by hand: bun scripts/copy-from.ts features/receipt prototypes/baseline copies into prototypes/baseline/-lib/from-receipt/ and writes copied-from.json (source path, commit hash, file hashes). This is the shadcn "copy in, own it" model.
  • fallow dupes measures sameness, the inverse of drift. Run read-only on this repo: fallow dupes --skip-local --min-occurrences 2 took about 0.1 s and found baseline/product-admin _checkout.tsx sharing 89 lines and purchase-panel.tsx sharing 70 lines (product-admin started as a copy of baseline). Its output also caught order-flow's board/graph.ts and routing.ts duplicating src/shared/statechart, a platform engine copied into a feature. So for a copy pair, "shared lines falling over time" = divergence. Record fallow dupes -f json per pair in the drift report. It can't say which side changed or whether the source moved since the copy; that needs copied-from.json plus git diff <hash> -- <source>.
  • Fallow's default advice is wrong here: it suggests "Extract shared function … into a shared directory" for cross-experiment clones. fallow explain code-duplication itself says "Leave it duplicated when the similarity is accidental and likely to diverge", which is exactly the experiment case. Keep cross-experiment clones out of any gate (§4.4) and tell agents so in the experiments rule, or an agent will "fix" the silos by merging them.
  • Drift is reported, never auto-fixed. A docs block on the experiment's page (and a column in the context map) says "receipt changed in 3 files since baseline copied it at abc123". A human decides: re-copy, or keep the difference and record why.
  • Risk: bug fixes don't propagate. Accept for experiments (each is a snapshot of an idea). For mechanisms that keep needing the same fix, the fix is a signal to graduate that code to platform.
  • Risk: accidental differences look like decisions. Mitigation: a difference in a copied folder with no decision record citing it shows as "unexplained" in the drift report.
  • Risk: platform turns into a back door (silo A edits platform and changes B). Mitigation: Edit(/src/platform/**) is ask in silo sessions, a platform rule says "changes reach every experiment", and the context map shows platform as one band under all silos.

Agent view this produces: an agent in A sees A, the platform, read-only reference and project rules. Nothing else exists for it: siblings are denied, and nothing in A imports them, so following an import never leads out of the silo. That's why the code rule makes the context rule simple: one deny per sibling, no uses carve-outs.

4.4 Fallow for code silos (verified locally, read-only, Fallow 3.14.0)

What I ran (config in the scratchpad, --no-cache, git status clean afterwards): fallow config-schema, fallow list --boundaries, fallow dead-code --boundary-violations, fallow dupes, fallow explain, fallow hooks install --target agent --agent claude --dry-run.

Result with the config below: 10 zones (2 prototypes, 6 features, platform, site). Exactly 14 violations, all of them the three known edges: baseline→receipt (5), product-admin→receipt (5) and image-grid (2), presentations→image-grid (2). There were none from platform to routes and none from site to experiments. A single-file check (--file <path>) took 0.13 s, fast enough for a per-edit hook.

Proposed .fallowrc.json. It follows the format and settings of im-web's (.fallowrc.json at b802eaf): JSONC with comments, the same $schema, duplicates.minOccurrences: 3, rules.unused-dependencies: "warn", and ignoreDependencies with a reason per entry. There are no workspaces (this repo isn't a monorepo), and boundaries is added:

{
  // This file is JSONC: fallow reads these comments; generic JSON tools may not.
  "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
  "entry": ["src/router.tsx", "src/routes/**/*.{ts,tsx}", "scripts/*.ts"],
  "duplicates": {
    // As im-web: hide pair-only clones. Copies between experiments are pairs on
    // purpose; drift is measured separately with `--min-occurrences 2` (see the drift report).
    "minOccurrences": 3
  },
  "rules": {
    "unused-dependencies": "warn"
  },
  "ignoreDependencies": [
    // fill as findings show up, one comment per entry (im-web style)
  ],
  "boundaries": {
    "zones": [
      // Every child folder is its own zone: prototype/baseline, feature/receipt, ...
      { "name": "prototype", "autoDiscover": ["src/routes/prototypes"] },
      { "name": "feature", "autoDiscover": ["src/routes/features"] },
      // Neutral mechanisms, no product decisions (today src/shared + vendored UI; later src/platform).
      { "name": "platform", "patterns": ["src/shared/**", "src/components/**", "src/lib/**", "src/hooks/**", "src/styles/**", "src/styles.css"] },
      // Reference content: im-web today, proposals, working model. Rendered by the site only.
      { "name": "reference", "patterns": ["src/reference/**"] },
      // The site around experiments: root, start page, Docs, section pages, router.
      { "name": "site", "patterns": [
        "src/routes/__root.tsx", "src/routes/index.tsx", "src/routes/docs/**", "src/routes/meta/**",
        "src/routes/prototypes/{route,index}.tsx", "src/routes/features/{route,index}.tsx", "src/router.tsx"
      ] }
    ],
    "rules": [
      // An experiment imports itself (implicit) and the platform. Nothing else: copy instead.
      { "from": "prototype", "allow": ["platform"] },
      { "from": "feature", "allow": ["platform"] },
      { "from": "platform", "allow": [] },
      { "from": "reference", "allow": ["platform"] },
      { "from": "site", "allow": ["platform", "reference"] }
    ],
    "coverage": {
      "requireAllFiles": true,
      // Generated route tree imports every route; config and scripts sit outside the app.
      "allowUnmatched": ["src/routeTree.gen.ts", "*.config.ts", "scripts/**"]
    }
  }
}

Notes:

  • Autodiscovered children are verified. A rule on the group (from: "prototype") expands to one rule per child (prototype/baseline → platform), and imports within one child are allowed. A sibling child is a violation (prototype/baseline -> feature/receipt).
  • src/routeTree.gen.ts must stay unzoned. In the first run it fell into site through a src/*.ts pattern and produced about 60 false violations. Unzoned files are unrestricted (schema: "unzoned files are unrestricted").
  • allowUnmatched patterns for config files and src/routes/{prototypes,features}/{route,index}.tsx are inferred from the coverage report of the trial run. The brace pattern is [?]: the trial used plain lists.
  • calls.forbidden can carry silo-level bans with no product meaning, e.g. { "from": "platform", "callee": ["fetch"] }, or ban toLocaleString everywhere (the Intl rule from AGENTS.md) [?: segment-aware matching of *.toLocaleString].
  • Presets (layered, hexagonal, feature-sliced, bulletproof): none fits "siblings that never talk". Custom zones are simpler. [O]
  • fallow viz writes an interactive HTML module map (or --viz-format mermaid|dot). It is useful to see code silos, but it shows module graphs, not agent context. For the context map, read fallow list --boundaries -f json instead (§6).

Fallow in the hook plan:

WhenHookWhat runsBlocks?
After each edit in src/**PostToolUse Edit|Write, if: "Edit(src/**)" (next to oxc.sh)fallow dead-code --boundary-violations --file "$FILE" -f compact --fail-on-issuesexit 2 with the violation, so Claude sees "prototype/baseline -> feature/receipt" immediately
Before git commit / git push by the agentFallow's own agent gate: fallow hooks install --target agent --agent claude (dry run: adds 1 handler to .claude/settings.json and creates .claude/hooks/fallow-gate.sh). The script parses Bash commands for git commit/push, runs fallow audit, and blocks on a fail verdict. It needs jq and fails open on runtime errorsfallow audit (dead code, complexity, duplication, boundaries on changed files; gate: new-only by default)yes
Before commit by a humanfallow hooks install --target git (pre-commit)same audityes

Two adjustments before installing the gate [O]:

  1. Duplication must not gate copies. A fresh copy-from creates clones on purpose. Either run fallow dupes --save-baseline inside scripts/copy-from.ts and point audit.dupesBaseline at it, or downgrade duplication in audit for src/routes/**. Otherwise the gate pushes the agent to extract shared code across silos, the exact bleed this pattern prevents.
  2. Unused-code findings in experiments: route files and -lib exports used only by docs previews may show as unused. Tune entry and ignoreExports before gating, or start with --gate new-only (the default).

Codex variant: --agent codex appends a managed block to AGENTS.md, which would put tool text into the human-readable core file. Don't use it here, or keep it below a clear marker. [O]


5. Which tool for which kind of content (one source, two renderings) [O]

XState is one way to keep one source with two renderings. Pick the tool by the content's shape:

ContentSource of truthHuman renderingAgent renderingNotes
Lifecycles, flows with states (pack, subscription, checkout, order flow)XState ChartDefinition (exists)interactive chart, scenarios, derived decision tablethe machine TS; optionally a generated transition list (state, event, guard text, target, sender) in the experiment's generated rulemeta.question = open question, already derived. Keep it
Condition → outcome rules without state (price rounding, VAT basis, which badge for which plan)decision table as data: rows: {when: {...}, then: {...}, note?}[] typed in TScoloured grid, row highlight from a scenario pickercompact markdown table in the generated ruleEach row doubles as a bun test case against the real function: the table can't lie
Decisions (D1–D38, DS1–DS15, R1…)-lib/decisions.ts: Decision[] (shape below)cards grouped by topic: plain headline, real component preview or Figma ref, "Why" and sources collapsed, a supersession timelinegenerated .claude/rules/generated/<id>.md: ID: rule line, decided only, plus an "Open, don't decide" listReplaces decisions.md. Code paths and IDs go in the agent line, not the human headline
Terminology (im-web today, proposals, our decided words)typed data (exists: im-web/terms.ts, proposals/terminology-*.ts); add src/shared/terms/decided.ts or per-experiment termsglossary with no/en side by side, "we say / we don't say", example in contextgenerated "Use these words" table, scoped to UI paths (src/routes/*/*/app/**, -lib/components/**)Only decided terms reach the agent; im-web terms and proposals stay reference
Design tokens, layout principlestokens.ts + tokens.css (tested)specimens, diagrams (exist)the TS; a 5-line rule scoped to UI pathsAlready single source plus test
Structure (routes, experiments)route tree + staticData (exists), registry.tsgenerated nav, section index, a tree diagramtype guards (route-guard.ts), short ruleA failing type check is the best agent doc
Status / what's built / next stepsstatus in data (registry, or -lib/status.ts with `builtplannedblocked`) or plain markdownprogress board or checklist
Explanations, ELI5, rationale, user-test findings, UX handovers copied as sourceplain human prose (markdown in -docs/), with diagramsthe docs pagenone: agents may read it when relevant but it never bindsThis is where human-only prose is the right answer. No binding words in it (§7)
Flows that aren't rules (a user-test script, a system context, a sequence)diagrams-as-code: Mermaid in markdown for hand-drawn ones; ELK (exists) only for charts derived from datarendered diagramnot neededMermaid would be a new dependency for @tanstack/markdown rendering [?]. Don't hand-draw a diagram of something a machine already defines
Procedures (new experiment, handover, refresh fixtures, perf, stable copy)skills (agent) + a short how-to page (human)how-to page (Diátaxis "how-to")SKILL.mdTwo texts on purpose: the audiences differ completely
Validated data from outside TS (frontmatter of idea files, imported JSON)Zod schemaform, tablez.toJSONSchema() if an agent needs the shape [?: check Zod version in repo]Use only where runtime validation is needed. Plain TS types are enough for in-repo data
MDX———Not needed: markdown plus typed data plus real React pages already covers it, and MDX would mix prose and code in one file, which is the thing to avoid

Decision record shape (sketch):

/** One decision in one experiment. Human page and agent rule are both built from this. */
export type Decision = {
  id: `D${number}`
  topic: 'Pricing' | 'Checkout' | 'Copy' | 'Layout' | ...
  status: 'decided' | 'open' | 'superseded'
  /** For people: one plain sentence, no IDs, no paths. */
  headline: string
  /** For agents: one imperative line, may name files and tokens. Required when decided. */
  rule?: string
  why?: string
  sources?: { label: string; ref: string }[] // Jira key, Figma node, Slack date
  date: string
  supersedes?: Decision['id']; supersededBy?: Decision['id']
  /** Narrower than the experiment, e.g. ['app/_checkout/**']. */
  appliesTo?: string[]
  preview?: () => ReactNode  // the real component, in the human page only
}

Two short texts per record is deliberate duplication. They sit side by side, so drift is visible in review. A test requires rule when status === 'decided' [O].


6. The context map: "What Claude reads, where" [O]

A human docs page at /docs/project/agent-context that replaces ideas/agent-context-setup.md as the visible account of agent context.

What it shows (simple first, details on demand):

  1. Silo view (first screen). One column per experiment (title and kind from the registry), with the platform, reference and project tiers as bands under all columns. Each column card lists what an agent in that silo gets: "Always: AGENTS.md · When touched: 3 rules + its decisions (12 decided, 3 open) · Closed: 7 other experiments". It also shows the code boundary (Fallow zone, files, violations, normally 0) and copies with drift ("copied receipt at abc123 · 89 % still identical"). The project band shows what applies everywhere. A red marker appears on any rule, permission or import that crosses a column: a leak.
  2. Matrix. Rows are areas named by their route staticData titles: each experiment × {App, Docs pages, Code, Notes}, plus Shared, Ideas, Data. Columns are context items. A cell holds a dot in the item's colour. Legend (six kinds, one colour each): Always (AGENTS.md) · When touched (path rule, nested CLAUDE.md) · Generated from decisions · Asks first (ask) · Blocked (deny) · Checked after edit (hook if) · Procedure (skill, user-only or automatic).
  3. Click a row: "When Claude works here it reads…", a list of cards with each rule's title and first line, full text behind a disclosure, and a link to the source file.
  4. Warnings: a rule whose globs match no files (stale), an area with no rules, two rules on one area that mention the same topic.
  5. Optional: "Seen in practice". An InstructionsLoaded hook appends {file_path, load_reason, globs, trigger_file_path} to .claude/logs/instructions.jsonl (gitignored). The map compares declared rules with observed loads ("declared for app/**, loaded 14 times this week").

How to build it here:

  • src/shared/agent-context/context.server.ts (createServerFn, per the repo's data-flow rule) reads:
    • AGENTS.md and CLAUDE.md, plus nested CLAUDE.md/AGENTS.md found with git ls-files
    • .claude/rules/**/*.md frontmatter paths (YAML), title = first # heading
    • .claude/settings.json permissions.deny/ask and hooks with if
    • .claude/skills/*/SKILL.md frontmatter (paths, disable-model-invocation)
  • It matches globs against git ls-files with Bun.Glob or picomatch. Caveat [V: S1/earlier report]: Claude's engine for rule paths isn't named. Permissions use gitignore semantics. Keep globs plain (*, **, {a,b}) so every engine agrees.
  • Silos come from fallow list --boundaries -f json (zones, patterns, file counts, rules) and violations from fallow dead-code --boundary-violations -f json, run by the server function or a bun run context-map script that writes a JSON snapshot (Fallow is a global install, so the page must not require it at runtime: fall back to the snapshot).
  • Leak detection: a rule whose paths match files in two silos; a silo settings file that doesn't deny some sibling; a generated rule outside its silo; a Fallow violation.
  • It names areas from the router's route tree (the same walk as docs-nav-tree.ts) plus registry.ts, so people see "Baseline › App", not src/routes/prototypes/baseline/app/**.
  • bun test: every rule glob matches at least one file; every generated rule is up to date; every experiment in the registry has a generated rule if it has decisions.
  • The page is about agents but written for people: it shows what applies where. It isn't agent context itself and uses no binding words.

7. Hard rules for agents (few) and how each is enforced

#Rule (agent wording)EnforcementStrength
1Only decided records bind: generated rules, AGENTS.md, .claude/rules/. Everything else is reference.Structure: only the generator writes decided items into agent contextdeterministic (structure)
2Don't read or edit ideas/ unless the user names the idea.permissions.ask: Read(/ideas/**), Edit(/ideas/**); rule ideas.md loads on read with "draft, binds nothing"deterministic except grep -r from a parent dir [V: S3]. Optionally sandbox.filesystem.denyRead: ["./ideas"] if the sandbox is on [?: path form]
3Never create or change decisions, rules or agent instructions unless asked; use /decide.permissions.ask: Edit(/AGENTS.md), Edit(/CLAUDE.md), Edit(/.claude/rules/**), Edit(**/-lib/decisions.ts); deny: Edit(/.claude/rules/generated/**) (only the generator writes there); /decide skill with disable-model-invocation: truedeterministic (ask = human approves each one)
4Human docs are for people: plain words, no "binds", "rule for", "decision", "agent", "may depart on purpose".Rule scoped to **/-docs/**, **/_docs/**, docs/**, src/shared/docs/**; a bun test that scans those files (and docs TS strings) for a short banned-phrase listtest = deterministic at commit; the rule guides writing
5Stay inside one experiment per session; never read, apply or "align with" another experiment's decisions or code. Copy code only through copy-from.Silo session (bun run focus <id>): sibling Read deny, claudeMdExcludes, silo-guard hook, status line (§4.2). Code: Fallow boundaries per edit and at commit (§4.4). Generated rules are path-scoped in unfocused sessionsdeterministic in silo sessions (Bash best-effort); code boundary deterministic everywhere
6Copies between experiments are intended; never extract shared code from experiments into src/platform or merge them unless asked.Rule in experiments.md; Edit(/src/platform/**) = ask in silo sessions; duplication not gating src/routes/** (§4.4)ask = deterministic
7Open questions stay open: never answer a meta.question or an open decision on your own.Generator lists them under "Don't decide"; a test fails if a decided item references an open one [O]mostly instruction

Everything else already in AGENTS.md (Intl, ports, fixtures, tokens) follows the earlier report: lint, tests, permissions and hooks.


8. Migration sketch (brief)

TodayBecomes
AGENTS.md (145 lines, includes "Kinds of docs: what binds you")Core of about 60 lines (earlier report §3.8). The kinds-of-docs table becomes path rules (im-web-today.md, proposals.md, working-model.md, ideas.md), each loading only where it applies. Human explanations (structure tree, design sources narrative) move to docs/ pages
DocKindBanner / doc-kinds.ts binding textDrop binding from human pages. Keep label plus a fact: "im-web today · as of 7 Oct 2026, commit 3bedd2a", "Proposal · from Ferdinand, 7 Oct · under discussion", "Draft". The binding meaning moves to rules
-docs/decisions.md (baseline, product-admin, design-system, page-builder, receipt, image-grid)-lib/decisions.ts → human Decisions page (cards) plus .claude/rules/generated/<id>.md. Start with baseline as a pilot
order-flow assumptions.md, open-questions.mdOpen questions are already meta.question: derive the page. Assumptions become status: 'open' decisions
status.mdStays human (or data in the registry). Never in agent context. "Next steps" go to handovers or Jira
docs/handovers/Session notes are agent context (continuity). Move to .claude/handovers/, listed by a SessionStart hook, not in the Docs nav. Human-facing "what's open" comes from status
ideas/agent-context-setup.mdReplaced by the context map page plus a short human "How Claude works here" explanation
ux-lip-1440.md (copied UX handover)Stays as source material (human, unchanged). Decisions cite it by section
registry.tsTitles and status for silos (folders define the silos via Fallow autoDiscover). workingModel links point to src/reference/working-model
src/shared/**Split: mechanisms → src/platform/ (alias @/platform/*); src/shared/docs/im-web, …/proposals → src/reference/; baseline's working-model machines → src/reference/working-model/, copied back into baseline
baseline/product-admin → receipt, product-admin/presentations → image-gridcopy-from into each experiment (receipt), or graduate the image-grid engine to src/platform/image-grid. Afterwards Fallow reports 0 violations
order-flow board/graph.ts, routing.ts (copies of shared/statechart, found by fallow dupes)Leave them (the feature owns its board), or import the platform engine if they haven't diverged on purpose. Decide once and record it
no Fallow config.fallowrc.json as in §4.4; PostToolUse boundary check; Fallow agent gate after the dupes adjustment
update-im-web skillAdd disable-model-invocation: true (its description already says user-only)
Plugin skills that write ADRs (e.g. domain-modeling)Disable for this project via /plugin, or rely on rule 3's ask

Order: (1) .fallowrc.json with boundaries in report-only mode (shows the 14 known violations); (2) cut the three edges (copy receipt, decide image-grid), then make boundary violations blocking; (3) bun run focus <id> with silo settings, guard hook and status line; (4) permissions and the ideas/ folder; (5) banner cleanup and the banned-phrase test; (6) baseline decisions.ts plus generator, generated rule and human page; (7) context map with the silo view; (8) the platform/reference rename; (9) other experiments' decisions.


9. Risks and open questions

  • Read ask fatigue on ideas/. Mitigation: approve "for this session" when you open an idea, or start a silo session for the idea's experiment, which allows ideas/<id>/** [O].
  • Silo sessions are only as strong as their launch. A plain claude session has no sibling denies. Mitigation: the status line shows "no silo" in amber; generated rules still scope decisions by path; a SessionStart hook in project settings could print "unfocused: start with bun run focus <id> for experiment work" [O].
  • Grep/Glob on a parent folder (src/routes): S3 applies Read denies "to that directory" best-effort. Whether results from denied subfolders are filtered is [?]. The silo-guard hook denies such searches outright.
  • Fallow is a global install, not in package.json (same as im-web). CI and other machines need it, or pin it as a dev dependency. Fallow's agent gate fails open when jq or fallow is missing.
  • Fallow's refactoring advice ("extract shared") conflicts with the copy policy. Keep it out of gates for src/routes/** and say so in agent context.
  • Copy drift. Bug fixes don't propagate, and accidental differences look like decisions. The drift report (copied-from.json + git diff + fallow dupes) makes both visible; humans decide.
  • Bypasses: grep -r from a parent directory, and scripts, ignore Read rules [V]. Only the sandbox closes this, and only for Bash.
  • Generated files drift. Mitigation: a test that regenerates and diffs; deny agent edits to generated/.
  • Two texts per decision (headline, rule) can disagree. Mitigation: co-location and review. Accepted cost.
  • Claude-only: .claude/rules/ and generated rules are invisible to Codex and other agents [V: S1]. Alternative: generate nested AGENTS.md per experiment (agents.md "closest wins"; Claude loads a subdirectory AGENTS.md on Read when there's no CLAUDE.md there [V: S1]). Check that the router ignores .md in src/routes [?].
  • Glob engine differences between rules (unnamed engine), permissions (gitignore) and the context map (Bun.Glob or picomatch). Keep globs simple; $slug.tsx and _docs are safe, avoid (…) [V/earlier report].
  • .claude/ protected paths: edits under .claude/ may already prompt in some permission modes [?]. Test before relying on rule 3 alone.
  • Open: do ideas live at the root (ideas/) or inside experiments (<id>/-ideas/)? Root gives one glob and keeps drafts away from code; per-experiment keeps context together. Recommendation: root, with an optional ideas/<experiment-id>/ subfolder.
  • Open: should Docs › Ideas be visible to test users and the team? It is rendered with a "Draft" label; hide it from the start page.
  • Open: the working model as src/reference/working-model/ copied into every experiment (recommended, §4.2) means baseline's copy can drift from the reference too. Is the reference updated from baseline (baseline = where the working model is developed) or the other way round? Pick one direction.
  • Open: image-grid: graduate the engine to platform, or copy it into product-admin and presentations?
  • Same pattern for Obsidian [O]: folders by state (Inbox/, Drafts/ vs Decided/), Read ask or deny on drafts in the vault's .claude/settings.json, auto memory off, a user-only /decide skill.

10. Sources (fetched 2026-10-09 unless dated)

  • S1 https://code.claude.com/docs/en/memory: CLAUDE.md, rules paths, claudeMdExcludes, AGENTS.md, /context, /memory, auto memory, InstructionsLoaded
  • S2 https://code.claude.com/docs/en/hooks: InstructionsLoaded fields, PreToolUse deny JSON, if, SessionStart additionalContext
  • S3 https://code.claude.com/docs/en/permissions: Read/Edit rules, best-effort Grep/Glob, Bash limits, .claudeignore has no effect, deny → ask → allow
  • S4 https://code.claude.com/docs/en/sandboxing: sandbox.filesystem.denyRead covers Bash only
  • S5 https://code.claude.com/docs/en/skills: disable-model-invocation, paths, skillOverrides (not for plugin skills)
  • S6 https://agents.md/: README for humans, AGENTS.md for agents, closest file wins (undated)
  • S7 https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents (2025-09-29)
  • S8 https://adr.github.io/madr/ (MADR 4.0.0, 2024-09-17)
  • S9 https://diataxis.fr/ and https://diataxis.fr/compass/
  • S10 https://llmstxt.org/ (2024-09-03)
  • S11 secondhand, not verified: https://addyosmani.com/agents/15-agents-md/, https://blog.buildbetter.ai/agents-md-complete-guide-for-engineering-teams-in-2026/, https://developer.upsun.com/posts/insights/why-your-readme-matters-more-than-ai-configuration-files, https://arxiv.org/pdf/2609.00252 (spec-driven development: an ADR in a wiki the agent never saw)
  • S12 https://code.claude.com/docs/en/sub-agents: frontmatter (hooks, disallowedTools, omitClaudeMd, isolation: worktree), settings deny rules apply to subagents, no per-agent path permissions
  • S13 https://code.claude.com/docs/en/settings: --settings layer above project and user, lists merge, project settings read from the primary working directory
  • S14 https://code.claude.com/docs/en/worktrees: --worktree, isolation checks (edits and commands into the main checkout), what worktrees share; worktree.sparsePaths known only from the settings-reference index
  • S15 Fallow 3.14.0, local CLI: fallow config-schema, fallow explain boundary-violation|code-duplication, fallow hooks install --dry-run; gate script source via opensrc (fallow-rs/fallow 3.14.0, crates/cli/src/setup_hooks/fallow-gate.sh); docs https://docs.fallow.tools
  • S16 im-web .fallowrc.json: https://bitbucket.org/NTB-NO/im-web/src/b802eaf45b8b53a833e27cb13fef42855a0c08c5/.fallowrc.json
  • Earlier report (S1–S11 there): …-agent-abff93c9579131301.md