CLAUDE.md · git:20260911.8150cdd · 2026-09-11 · sha256 ffb58a7e6dba08c9

CLAUDE.md git:20260911.8150cddA

Immutable. This exact content is served forever at /api/v1/blob/ffb58a7e6dba08c9.

# a2ui-catalogue — Working Rules

Declarative-first repo. State, policy, and **processes** are declared in
`project.yaml`; `ops/ops.py` executes them. Read `project.yaml` before
operating — it is the inventory (deployments, script IDs, properties,
staging policy, declared debt). Never carry identifiers in conversation
memory when the manifest declares them.

## Operations discipline

- **Every operation goes through a declared process:**
  `python3 ops/ops.py list` → `python3 ops/ops.py run <process>`.
  If the operation you need isn't declared, DECLARE IT in `project.yaml`
  `processes:` first, then run it. Improvised command sequences
  (raw `clasp push`/`clasp deploy`, ad-hoc regeneration chains) are the
  failure mode this system exists to prevent.
- After editing `atoms/schema.yaml` → `ops.py run atom-change`.
- After editing `spec/` or prompt contracts → `ops.py run prompt-update`
  (it bakes in the gemini_handoff cache bump — do not skip it).
- Renderer code changes → `ops.py run renderer-release` (deploys in place
  to the public deployment from the manifest inventory).
- Commits: `python3 ops/ops.py commit "<message>"` — stamps per the
  sync-window time mapping (09:00-11:59→08:00, 14:00-17:59→13:30,
  otherwise 13:00, Europe/Paris). Push: `ops.py run repo-publish`.
- Every run logs to `ops/log.jsonl` (local tier) — check it before
  re-diagnosing something a previous run already recorded.

## Publication boundaries (hard rules)

- **Nothing new is published to a2uicatalog.ai or any public surface
  without explicit per-artifact opt-in from Curtis.** Publication is
  declared in `project.yaml` (`policy.published`, `published_prompts`)
  and enforced by `tests/test_project_manifest.py`.
- Atoms carry `stage: preview | stable` (stable = default). Preview atoms
  are repo-only; all publication pipelines filter on the field
  (`tests/test_staging.py` enforces). Promotion = delete the stage line
  + `ops.py run atom-change`. New dev-first atoms START as preview.
- Private tier (`ops/`, `**/Code.private.gs`, `.clasp.json`) must never
  be tracked — the manifest audit fails the build if it is.
- Secrets live in Script Properties only (key names declared in
  `project.yaml`); never in code, payloads, or tracked files.

## Verification norms

- `python3 -m pytest tests/ -q` before any push; CI runs the manifest +
  staging audits before deploying the site.
- Encoded payload URLs are never hand-typed or hand-copied between
  surfaces — emit them from tool output (`scripts/make_url.py`) only.
- The stable /exec URL is server knowledge: inject `_getWebAppUrl()`
  via template at serve time; `window.location` only as gas-fakes
  fallback (see a2uithoughts.md for the incident history).

## Deploy discipline: deployed ≠ reachable

This system's characteristic failure is not a broken build — it is a
CORRECT build that never became true at the surface a user touches.
Six instances to date: the `ui://` template cache, a lost deploy race,
a clasp identity clobber, a Worker serving a stale compiled renderer,
a parity gate that blocked the deploy that would have satisfied it, and
a live capability three hosts could not discover. Assume this class
first, not last.

- **Never trust "I deployed it". Ask the live surface what it is
  running.** `worker-verify`, `gas-verify`, `check_ui_view_version`,
  `check_worker_renderer` each interrogate reality rather than a state
  file. After any deploy, run the ones that cover what you changed.
- **A change that is committed and pushed but not observable live is a
  DEPLOY problem until proven otherwise.** Check the CI run before
  re-reading the code — and before re-pushing.
- **Two failures on the same symptom: STOP.** Report what you observed,
  what you ruled out, and what you need. Do not attempt a third fix.
  (Adapted from `ops/improve.yaml` `budget.stop_when`, which had this
  rule while four consecutive pushes chased one misdiagnosed cause.)
- **Distinguish "appeared then reverted" from "never appeared".** The
  first is a race; the second is a gate or a trigger. They look
  identical in a single snapshot and different in a timeline — poll
  before concluding.
- **Any deliberate `tools/list` change needs the parity suite updated in
  the same commit** — the gate compares local to live, so an intended
  difference deadlocks the deploy that would resolve it.
- **A wired surface's `mcp:<verb>` action is only real if the renderer's own
  transport allowlist knows it too.** `mcp-worker/src/tools.js` declaring an
  action and `apps-script-surface/gas-wired-renderer/A2UIState.html`'s
  `MCP_VERBS` map are two hand-synced lists in two repos; a verb in one but
  not the other fails INSTANTLY and — unless the action also has its own
  error-feedback element wired — SILENTLY (found live 2026-08-04: the
  Workspace nav restructure shipped `mcp:open_workspace` actions with no
  transport entry; every click did nothing visible at all). Guarded by
  `mcp-worker`'s `test-workspace-verb-parity.mjs` (a deploy gate, vendored
  mirror of `A2UIState.html` — same pattern as the `bom_emitter`/
  `training_parser` parity tests), but that only catches the drift, it
  doesn't remove the hand-sync. See `a2uithoughts.md`'s "workspace verb
  parity" entry for the proposed manifest-driven fix.
- **A hand-kept dependency list is a hand-sync bug waiting to happen.**
  `deploy.yml`'s pip-install step and a2ui-private's
  `deploy-full-catalog.yml` both used to hardcode their own copy of
  `requirements.txt` — a package added to one and not the other broke CI
  on the NEXT unrelated push, not the one that added it, so the actual
  cause was already out of context. Found 2026-09-11: `defusedxml` had
  been missing from `deploy-full-catalog.yml` for two days, failing every
  push silently, past a same-day commit that already claimed to fix it.
  Both workflows now `pip install -r requirements.txt` instead — see
  `AGENTS.md`'s "Working style expected of agents here" (a2ui-private) for
  the general rule this incident is an instance of: a known issue is not
  an accepted one.

## Improvement work is measured, not asserted

`ops/improve.yaml` (private tier) declares the benchmark. When the work
is improvement-shaped — "make X better", picking up backlog, or any
session likely to touch a scored dimension:

- **Score first.** A session is judged on the index delta it moved, not
  the work it appeared to do. Without a baseline there is no delta.
- **Report the delta at the end**, including when it is zero.
- **If the session's real work fitted no dimension, say so** — that is
  the declared trigger to revisit the benchmark, not a reason to skip it.

## Key references

- `project.yaml` — inventory, policy, processes (the lifecycle catalogue)
- `a2uithoughts.md` — design rationale and incident lessons (gitignored)
- `spec/training-md-v0.1.md` — training domain contract; prompts are
  GENERATED from it (`gen_training_prompt.py`) — edit the spec, not
  the prompt files
- Parser parity: `scripts/parse_training_md.py` is the reference;
  `training_parser.gs` must stay deep-equal (parity harness in pytest)