This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Townlet is a rapid DRL experimentation framework for game designers — a deep
reinforcement-learning substrate expressed as configuration. An environment (variables,
observation layout, substrate topology, affordances, effects, items, reward function) is written
in YAML, compiled into one frozen hash-carrying CompiledUniverse, and executed GPU-natively
against torch tensors.
The point is authoring. Someone with an idea for a mechanic should be able to turn it into a running, trainable, reproducible RL environment by writing config — no environment subclass, no observation-tensor plumbing, no reward-function code. Every subsystem exists to move a category of "you must write Python for this" into "you can declare this."
This is the load-bearing judgement call. When something can only be expressed by editing Python, that is a product defect, not a shortcut. Prefer declarative surface over engine special-casing, even when the special case is smaller.
The survival world in configs/default_curriculum — eight meters, fourteen affordances, one 8×8
grid — is the first-class demonstration of the idea, not the product. Do not harden its
content into the framework.
Repo directory is hamlet; the distribution and the only live source tree are townlet. Same
project. Work only in src/townlet/ — src/hamlet/ is obsolete legacy code.
Fuller framing, and the honest status section: README.md (it is current and accurate — prefer
it over anything in docs/architecture/). Product vision: docs/product/vision.md.
Pedagogical value is a property of the framework, not the mission. The project deliberately preserves "interesting failures" (like reward hacking) as teaching material rather than immediately fixing them.
Pre-release, zero users, zero downloads. Breaking changes are free — take them. No fallbacks, no deprecation warnings, no migration paths, no "support both old and new". If it's old and not in use, delete it; git history preserves it. Old configs should fail loudly, not be accommodated.
These are antipatterns here, not good practice. Recognise the shape, apply the fix:
| shape | fix |
|---|---|
if hasattr(obj, 'old_field') — old vs new attribute |
delete the old path, update references |
try/except catching an old config format |
let it raise, update the config, delete the handler |
| version check or feature flag for "legacy support" | delete both the check and the old path |
field made Optional that should be required |
make it required, set it explicitly in every config (see No-Defaults Principle) |
| a comment saying "for backwards compatibility" | delete the code and the comment |
| obsolete code kept "just in case" | delete it |
Done correctly before: old observation code deleted outright at VFS integration rather than
dual-pathed; reward_strategy removed with its RewardStrategy classes when drive.yaml
became required; src/hamlet/ abandoned rather than maintained alongside src/townlet/.
One exception, and it is not backwards compatibility: the pinned oracle below. It is a frozen specification to diff against, never a code path to keep alive.
docs/ predates the current recovery work. Treat it as design intent, never as a
record of what shipped. On 2026-08-24 the old architecture corpus was archived wholesale to
docs/architecture/archive/ and replaced by a six-document HLD set (PDR-0118); archive-internal
links may dangle, by design.
The sharpest case: docs/architecture/archive/BRAIN_AS_CODE.md and
docs/architecture/archive/hld/02-brain-as-code.md both say
"Status: Approved for Implementation", while execution_graph / cognitive_topology /
agent_architecture return zero grep hits in src/ and configs/. The design is the
target; the status line is false. That pattern repeats across the archived corpus.
Rules:
- Never cite a doc as evidence something is implemented. Check
src/townlet/first. - Where a doc disagrees with
README.md, README is right. It is current, honest about status, and carries the correct product framing. docs/architecture/archive/does not know aboutsrc/townlet/oracle/, the strangler rewrite,items/, oreffects/. Absence there means nothing.- Many (not all) files carry frontmatter with an "AI-Friendly Summary" and "Reading Strategy". Where present, read it first to decide relevance before opening a 2000-line file.
Current-and-trustworthy: README.md, docs/product/, docs/oracle/, the six-document HLD
set in docs/architecture/ — HLD.md, STRATA.md, UAC.md, BAC.md, COMPILER.md,
VFS.md (all reviewed against source 2026-08-24) — and docs/config-schemas/.
docs/architecture/archive/vfs-current-implementation.md also remains accurate per the
2026-08-24 audit except its access-control and agent_private claims.
On docs/config-schemas/ (restored 2026-08-26): the 2026-08-24 recut (commit c4e8bd58,
"zzz. archive") swept it into the archive on a fast visual pass, and a follow-up sweep
repointed every citation at the archive path. Both were reversed on 2026-08-26 — it is the
reference tier the HLD set delegates to, and nothing replaced it. It is back at
docs/config-schemas/ and back on the trustworthy list, with three exceptions that carry
dated staleness banners of their own: drive_as_code.md, enabled_actions.md, and training.md.
variables.md is the current Cut B canonical authoring contract. Trust a file in that directory
unless it opens with a banner telling you not to.
Work is mid strangler rewrite behind a pinned oracle. Tag oracle-2026-08-13 freezes the
previous system as the specification for preserved behaviour. From docs/oracle/ORACLE.md:
"The oracle never mutates", and "a diff against the oracle is a defect in the rebuild unless
the register says otherwise." Accepted differences are registered in
docs/oracle/known-divergences.md. Never edit anything under .oracle/.
- Source:
src/townlet/universe/compiler.py- seven-stage pipeline (parse → symbol table → resolve → cross-validate → metadata → optimization → emit/cache) - Docs:
docs/architecture/COMPILER.md.docs/architecture/archive/COMPILER_ARCHITECTURE.mdis design-era (2025-11): useful for intent, but it describes sub-compilers that were never wired (notablyCuesCompiler, never called and deleted outright atbb43e024) and asserts a backwards-compatibility success criterion this project rejects. - Tests:
uv run pytest tests/test_townlet/unit/universe/(useUV_CACHE_DIR=.uv-cachein sandboxed environments) - CLI:
python -m townlet.universe {compile,inspect,validate}- wired into CI via.github/workflows/config-validation.yml. Lint, Tests and Config Validation run on every push toproject-recovery*branches (first green run 2026-08-15,hamlet-2100105c9aclosed on it; corrected here 2026-09-02 — this line used to say no workflow had ever run). A "green" claim still requires reading every per-push row at the tip SHA (PDR-0127). The local pre-push set mirrors the Lint job —ruff check .,black --check src tests,mypy src/townlet --show-error-codes,scripts/no_defaults_lint.py— plusscripts/validate_compiler_cli.pyandpytestfor the other two gates. Mergingproject-recovery*tomainis autonomous underPDR-0101once gate 1 (every CI row green at the tip) and gate 2 (thePDR-0039README re-verification by method) are discharged..claude/settings.jsonallowsgh pr mergeand the read-onlygh pr/gh runcommands; the rules are prefix matches, so rungh pr merge <n> --mergeas its own plain command, never inside a;or&&compound. Tags, releases andgh pr closeare deliberately not allowed — they escalate to the owner.
uv sync # Runtime dependencies only
uv sync --extra dev --extra recording # Development environmentBoth extras are required for development. mypy src/townlet type-checks
src/townlet/recording/, whose imports (matplotlib, pillow) live in the
recording extra. Omitting it produces four spurious import-not-found errors.
Until the recording subsystem is removed (filigree hamlet-16ae192d42), the dev
environment needs both.
export PYTHONPATH=$(pwd)/src:$PYTHONPATH
uv run scripts/run_demo.py --config configs/default_curriculum --level L1_full_observabilityLevels live under configs/default_curriculum/levels/ — there are no flat
configs/<level>/ packs. Live visualization: the live-inference skill.
When using DemoRunner for checkpoint operations without running full training, use context manager for automatic resource cleanup:
# ✅ GOOD: Guaranteed cleanup
with DemoRunner(config_dir=..., db_path=..., checkpoint_dir=...) as runner:
runner.load_checkpoint()
network_weights = runner.population.q_network.state_dict()
# ❌ BAD: Resources leak if run() not called
runner = DemoRunner(...) # Opens DB connections and TensorBoard writers
runner.load_checkpoint() # Resources stay open indefinitely!Note: Work only in src/townlet/ - hamlet is obsolete legacy code.
Fixed Affordance Vocabulary: All curriculum levels observe the same 14 affordances (for
transfer learning), even if not all are deployed. In default_curriculum these are EAT, SLEEP,
WORK, SHOWER, EXERCISE, SOCIALIZE, MEDITATE, DRINK_WATER, BRUSH_TEETH, LAUNDRY, COOK,
CLEAN_HOUSE, ENTERTAINMENT, DOCTOR — affordances.yaml is byte-identical across all five levels
(verified 2026-08-15). Several docs/architecture/ documents list a different affordance set;
they are wrong, this one is the shipped pack.
Position encoding (stratum.yaml): observation_encoding is deleted. There is one token
position contract: substrate coordinates are normalized to [0, 1]; egocentric deltas use the
same per-axis denominator and land in [-1, 1]; both are padded to MAX_POSITION_RANK. A config
that still declares the old selector, or the deleted observation_mode key, fails validation as an extra field (PDR-0143).
Observation Dimensions — the observation is TOKENS, not a raster:
ObservationSpec, ObservationField,
ObservationActivity, curriculum_active and the per-level activity mask are deleted, not
renamed. There is no mask and no inactive slot: every dim is real, and absence is a token's
own presence feature. Do not carry an "allocated vs active" reading into any observation
question.
from townlet.universe.compiler import UniverseCompiler
u = UniverseCompiler().compile(Path("configs/default_curriculum"),
primary_level="L1_full_observability")
spec = u.get_level("L1_full_observability").token_spec
spec.total_dims # serialization width of the flat view
spec.census # {token type: count} — where the width actually goes
spec.row_layout() # (type, slot, start, end) per row; presence leads each rowcompile() requires an explicit primary_level — implicit selection raises. CompiledUniverse
is single-level by construction (get_level / to_level / all_levels navigate; there is no
.levels mapping).
What a TokenSpec is (spec
docs/superpowers/specs/2026-08-22-token-observation-representation-design.md §§1-2): seven
engine token types in a fixed canonical order — self, meter, affordance, agent,
item, effect, variable_element — each with a fixed payload width across all
universes, and a per-universe compiled capacity with deterministic slot bindings.
Content is per-universe; the type system is not. total_dims = Σ_type capacity × (1 + payload width). Identity is the declared payload applied recursively (a meter is its declared
parameters, an affordance carries its targets' meter signatures), never a name or a slot
index — so two entities identical in every declared parameter are refused at compile time.
Two consequences worth holding on to:
- Transfer is a property of the type schema, not of the width.
token_type_schema_hash— the transfer contract — is identical across a 2-D grid, a 3-D cubic grid and an aspatial universe (measured 2026-08-26:428982ef5d81dd26ondefault_curriculum,differential/div003_cubic_partial,aspatial_testand all threetoken_transfer_*packs, whosetotal_dimsrange 162–1132). That is whatMAX_POSITION_RANKpadding buys, and why rank-adaptive padding is not a free width saving.layout_hash— the flat-net contract — moves per universe, as it must. - POMDP does not shrink or reshape the tensor: same
TokenSpec, same width, samelayout_hash;vision_rangeis handed tosubstrate.visible()and out-of-range spatial tokens have presence (and payload) zeroed.
Action Space (corrected 2026-08-24 — the per-substrate count table previously here, "Grid2D
8 / Grid3D 10 / GridND(7D) 16 / Aspatial 4", disagreed with source): the action space is
composed — substrate movement actions (a function of substrate type and declared
parameters such as diagonals) plus custom actions from actions.yaml — under the canonical
ordering contract of substrate/base.py: movement, then INTERACT at [-2], then WAIT at
[-1]; aspatial has no movement actions. Never quote a per-substrate action-count literal; ask
the compiled artifact. See docs/architecture/STRATA.md §5.
POMDP Support:
active_vision: partialkeeps the compiledTokenSpecand flat width unchanged. It passes the level's normalizedvision_rangetosubstrate.visible(); spatial token publishers clear both presence and payload for entities outside that predicate.- Grid2D, Grid3D and GridND convert
vision_rangeto a discrete radius from the longest axis:max(1, ceil(vision_range * span / 2)). Continuous and ContinuousND use the corresponding world-unit radius without cell quantization. All use the substrate's declared distance metric, andwrapuses toroidal shortest-path deltas. substrate.egocentric_delta()supplies bounded entity-minus-observer offsets using the same per-axis denominator as normalized positions. Aspatial has no spatial filtering:visible()returns all true andegocentric_delta()returns width-zero deltas.stratum.vision_supportmust admit the level's declaredactive_vision; this is a config capability check, not a substrate window-capability matrix.
See docs/architecture/STRATA.md §7 and docs/manual/pomdp_compatibility_matrix.md.
Status: in production. VFS is the typed state / observation / transition ABI between UAC and BAC — not just an observation helper.
Purpose: Declarative state space configuration for observation specs, access control, action dependencies, and (via VTC) compiled transitions.
Pipeline (corrected 2026-08-26 at the token cut): YAML Config → Schema Validation → TokenSpec → Runtime Registry + token publishers → Observations
Key Components:
schema.py: VariableDef, NormalizationSpec, WriteSpec (ObservationField— the VFS observation mirror — was deleted at the token cut, along withVFSObservationSpecandvfs/observation_builder.py: the mirror was derived one hop downstream of anObservationSpecthat no longer exists)registry.py: Runtime storage with GPU tensors, access control enforcementuniverse/dto/token_spec.py: the compiledTokenSpec— engine constants, per-type payload schemas, capacity derivations, the exposure refusals and the indistinguishability checkenvironment/token_publishers.py: one publisher per token type; fills the flat viewvtc.py: VFS Transition Compiler — action writes, passive dynamics, cascades, terminal conditions, reward components, occupancy claims
Variable Scopes — nine, not three (VariableScope in vfs/schema.py): global, agent,
agent_private, item, pair, group, affordance, zone, message.
Access Control: readable_by / writable_by role lists per variable, enforced at
registry.get() / set(). Roles are open strings, not a closed enum — agent, engine,
actions, vtc, social_model are the common ones. ⚠ Caveat (2026-08-24 audit): the
enforcement is real where it runs, but it currently has no authoring surface (the compiler
applies one fixed role policy to the canonical variable family) and the observation path bypasses the
checked accessor entirely — see docs/architecture/VFS.md §6 caveat and
docs/architecture/archive/REVIEW-2026-08-24-vfs-implementation-vs-spec.md.
Which declarations a pack needs (declaration-store Cut B):
- One required pack-scope
variables:declaration supplies the explicit registry-variable roster, evaluator settings, scope extents and named item-profile groups. - Every variable declares type, scope, initialization, lifetime, semantic type and exposure. Global/agent expressions and supported item state lower into internal compiled profiles.
- Environment-variable, VFS-profile and static-overlay authoring languages are deleted. Old payloads fail; no filename reader, alias, permission-field authoring or translation remains.
- All variables enter the symbol inventory; item identities are profile-qualified. Token bindings carry typed scope, which selects the publisher independently of reference-string shape.
- Discovery reads all nested YAML/YML outside
.compiled; duplicates and unknown declarations name actual source locations. Arrays retain their authored order.
Documentation: canonical variables,
declaration discovery, and the Cut B acceptance evidence.
docs/architecture/VFS.md retains dated architecture material; its October 1 boundary takes
precedence over historical authoring/permission examples. PDR-0120 access design remains separate.
Architecture: Action Space = Substrate Actions + Custom Actions
- Global Vocabulary (pack-level
actions.yaml, e.g.configs/default_curriculum/actions.yaml): all levels in a pack share one action vocabulary. There is noconfigs/global_actions.yaml— that path is dead and several docs still cite it. - Custom Actions: REST (energy recovery), MEDITATE (mood boost)
- Action Labels: Configurable terminology (gaming, 6dof, cardinal, math presets)
See docs/config-schemas/enabled_actions.md for details (⚠ carries a dated staleness
banner: it still documents the dead configs/global_actions.yaml path).
Status: in production, runtime-integrated.
Purpose: Declarative reward function system for Townlet environments
Drive As Code (DAC) is a declarative reward function compiler that extracts all reward logic from Python into composable YAML configurations. Operators can A/B test reward structures without code changes. DAC compiles YAML specs into GPU-native computation graphs with provenance tracking.
Declarations: Each level requires a typed drive declaration. Pack versus
levels/<level>/ determines scope; filenames and additional subfolders are transport.
The following filenames are a readable convention, not compiler dispatch:
configs/default_curriculum/
├── stratum.yaml # substrate: grid 8×8, shared by EVERY level
├── environment.yaml # shared runtime and observation settings
├── brain.yaml # required pack brain; complete level overrides allowed
├── actions.yaml, effects.yaml, items.yaml, variables.yaml
└── levels/<level>/
├── bars.yaml
├── affordances.yaml
├── drive.yaml # required typed DAC reward declaration
├── training.yaml
└── curriculum.yaml # vision + temporal switches
A declaration may move to levels/<level>/mechanics/rewards.yml, or share a multi-document
file with other declarations. Keep the existing drive: wrapper: renaming a file does not
change its content vocabulary. Required families are checked after discovery. Catalog fragments
merge in sorted pack-relative path/document order, preserving each authored list's order;
duplicates, even identical ones, refuse with both file:line origins.
Architecture: Each level's drive declaration → compiled by UAC →
executed by DACEngine (src/townlet/environment/dac_engine.py). RewardStrategy classes
fully removed. Checkpoint provenance via drive_hash (SHA256 of the compiled DAC config).
Formula:
total_reward = extrinsic + (intrinsic × effective_intrinsic_weight) + shaping
where:
effective_intrinsic_weight = base_weight × modifier₁ × modifier₂ × ...
Modifier, extrinsic (9 types), intrinsic (5 types), and shaping (11 types) vocabularies:
see docs/config-schemas/drive_as_code.md. Its drive.yaml examples use the filename
convention; declaration identity supplies diagnostic locations.
Bug: Multiplicative reward (energy × health) + high intrinsic weight → agents exploit low bars for exploration
L0_0_minimal/drive.yaml and
L0_5_dual_resource/drive.yaml are byte-identical. Both declare
constant_base_with_shaped_bonus with adaptive_rnd at base_weight: 0.1. No shipped level
declares a multiplicative extrinsic, so the contrast the lesson depends on does not exist and
cannot be demonstrated by running these packs.
The intended design, for whoever authors it:
- L0_0_minimal: should demonstrate the bug (multiplicative, no suppression)
- L0_5_dual_resource: should fix it (constant_base_with_shaped_bonus)
- Comparison: students learn the importance of reward structure design
Old System (DELETED):
training.yaml: reward_strategyfield → REMOVEDsrc/townlet/environment/reward_strategy.py→ DELETED (583 lines removed)- Hardcoded Python reward classes → REPLACED
- All legacy reward strategy tests → DELETED (349 lines removed)
New System (REQUIRED):
- A
drivedeclaration is required for every level (see pack layout above) - DACEngine compiles YAML → GPU computation graphs
- Checkpoint provenance via
drive_hash(SHA256 of DAC config) - All checkpoints must have matching
drive_hash
Migration: See docs/guides/dac-migration.md
- Config Reference:
docs/config-schemas/drive_as_code.md(archived ⚠ staleness banner — see above) - Migration Guide:
docs/guides/dac-migration.md
training.yaml: use_double_dqn selects vanilla vs Double DQN; checkpoints persist the flag.
Non-obvious cost on a recurrent architecture (corrected 2026-08-24 — the "3 forward passes vs 2"
previously stated here does not match the current update path): one extra single-step boundary
forward per update; action selection reuses the online unroll (population/vectorized.py:862-880).
Details: docs/architecture/BAC.md §2.5 and docs/config-schemas/training.md — both now
agree (verified 2026-08-26; training.md was corrected in place on 2026-08-24, so the
warning previously here that it "still carries the stale 3-vs-2 figure" is itself obsolete).
Training is controlled via YAML config packs in configs/. The real pack layout —
pack-level shared files plus levels/<level>/ overrides, NOT flat configs/<level>/
directories — is shown in the DAC "Key Components" section above.
configs/default_curriculum/levels/:
| level | intended | actually shipped |
|---|---|---|
| L0_0_minimal | 3×3 grid, 1 affordance | 8×8, 14 affordances |
| L0_5_dual_resource | 7×7 grid, 4 affordances | 8×8, 14 affordances — training.yaml identical to L1 but for output_subdir |
| L1_full_observability | 8×8, 14 affordances | as intended |
| L2_partial_observability | token-filtered POMDP | genuinely differs (active_vision: partial) |
| L3_temporal_mechanics | 24-tick day/night | genuinely differs (active_temporal: true, day_length: {period_of: day_phase} resolves the authored clock period to 24) |
bars.yaml, affordances.yaml and drive.yaml are byte-identical across all five levels.
Grid size is set once in the pack-scope stratum declaration (8×8) and no level can override it.
For active temporal levels, curriculum.day_length: {period_of: day_phase} resolves the finite,
positive integral period of the declared global temporal variable derived from ambient tick.
An active literal day length duplicating that clock fact is refused; inactive levels keep
day_length: null. See clock references.
L0_0/L0_5/L1 differ from one another only in training hyperparameters; their curriculum.yaml
files differ only in comments. Five documented levels are three distinct universes.
Future: L4 (multi-zone), L5 (multi-agent), L6 (communication)
grid: 2D discrete grid (Grid2DSubstrate) — or 3D withtopology: cubic(Grid3DSubstrate). There is nogrid3dtype; that literal was deleted (it never had a factory branch).gridnd: 4D-100D discrete grid (GridNDSubstrate)continuous: 1D/2D/3D continuous spacecontinuousnd: 4D-100D continuous spaceaspatial: No positioning, pure resource management
Boundary Modes: clamp (hard walls), wrap (toroidal), bounce (elastic), sticky
Distance Metrics: manhattan (L1), euclidean (L2), chebyshev (L∞)
Substrate config examples: the shipped packs (configs/default_curriculum/stratum.yaml,
configs/aspatial_test/, configs/test/action_space/*/). There is no configs/templates/
directory — that path is dead. Schemas: docs/config-schemas/. Worked substrate examples
(aspatial, toroidal grid, euclidean distance) and a side-by-side comparison:
docs/examples/.
All behavioral parameters must be explicitly specified in config files. The DTO layer
enforces this, with ConfigDict(extra="forbid") so stray keys fail at parse time.
DTOs live in src/townlet/config/ — training_v2_config.py, environment_config.py,
bars_v2_config.py, affordances_v2_config.py, stratum_config.py (SubstrateConfig,
StratumConfig), curriculum_config.py, drive_as_code.py, variables_config.py,
effects_config.py, items_config.py — plus
townlet.environment.action_config.ActionConfig. (townlet.substrate.config does not exist;
SubstrateConfig is in config/stratum_config.py.)
Why: Hidden defaults create non-reproducible configs. Changing code defaults silently breaks old configs.
Exemptions: Only metadata (descriptions) and computed values (e.g., observation_dim).
brain.yaml selects feedforward, dueling, token_set, or recurrent. TokenSetQNetwork
and RecurrentTokenQNetwork share TokenSetEncoder: per-type projections and learned type
embeddings followed by the declared mean or attention aggregator. The recurrent network
folds [batch, sequence, observation] into frame batches for token encoding, then makes one LSTM
call over the complete pooled sequence before its Q-head. Observation width and token roster come
only from the compiled artifact; do not write dimension literals here.
- LSTM hidden state resets at episode start, persists during rollout, and observes replay sequence and terminal boundaries during training.
Training Details:
- Gradient clipping:
clip_grad_norm_(..., max_norm=self.max_grad_norm)— the threshold is the declared training hyperparametermax_grad_normintraining.yaml(config/training_v2_config.py), not an engine constant (corrected 2026-08-24; thedefault_curriculumpacks declare10.0) - Economic balance: WORK pays $22.5. This became true at runtime only in WS-1(e)
(2026-08-12) — before that, six hardcoded
[0.0, 1.0]clamps crushed every payout to1.0despitemoney.bounds.max: 999999.0, so six of seven priced affordances were permanently unaffordable. "Sustainable with proper cycles" has never been measured against a working economy; treat it as an intention, not a finding. - Intrinsic weight annealing: threshold=100.0, requires mean survival >50 steps
See frontend/CLAUDE.md (loads automatically when working under frontend/).
Tests focus on:
- Environment mechanics (vectorized operations, GPU tensors)
- Population training (batched updates, curriculum progression)
- Exploration (RND novelty, annealing logic)
- Integration (full training loop)
Do NOT test for "correct" strategies - emergent behaviors are valuable even if unexpected.
From game as experience to writing a game as experience.
When in doubt:
- Ask "can a designer express this in a config pack?" If the answer is "only by editing Python", that is the defect worth fixing — not the symptom you were chasing.
- Prefer declarative surface over engine special-casing, even when the special case is smaller.
- Never branch on a variable's name. A variable is a name plus declared parameters; the
compiler and runtime must not know that
moneyis money (PDR-0045). Behaviour that varies per variable comes from a declared parameter, never from what the variable is called. The in-tree model:drive_as_code.pydeclaresmoney_bar: stras required, no default — the engine holds the role, the author binds the referent. Name-based inference is the hardest form of this defect to spot, becauseif name == "money"reads as a helpful default rather than as a hardcoded domain fact. - Keep the framework/instance boundary sharp:
default_curriculumcontent (its meters, affordances, 8×8 grid) is example data, not framework. Do not freeze it into the engine. - Preserve "interesting failures" as teaching material; document unexpected behaviour rather than immediately fixing it.
- The goal is a framework others author in, not production-ready agents.
- Work only in
src/townlet/—src/hamlet/is obsolete legacy code.
filigree tracks this project's work. Use it to find, claim, update and close
issues: filigree session-context at session start, then
filigree start-next-work --assignee <name>.
Full reference: the filigree-workflow skill (patterns, priorities,
observations, error codes), filigree --help, and the mcp__filigree__* tool
schemas. Prefer the MCP tools when available; fall back to the CLI.
Two rules --help will not tell you:
- Claim atomically:
work_start/work_start_next(MCP) orstart-work/start-next-work(CLI). Never chain a claim with a separate status update; that two-step form races other agents. - On
SCHEMA_MISMATCHthe installed filigree is older than the project database. Surface it to the user; do not retry.
Loomweave pre-extracts this repo into a queryable map — entities, their
call/reference/import/relation edges, and subsystems — each carrying a Stable
Entity Identity (SEI). Ask its mcp__loomweave__* tools, not grep, for "what
calls X", "what subclasses X", "where is X defined", "find the thing that
does Y".
- Never hand-construct an entity id: take it from
entity_find/entity_at/entity_resolve, and bind cross-tool records on thesei, not theid. - If
project_status_getreports stale, re-index before answering.
Full reference: loomweave-workflow skill, loomweave --help, MCP schemas.