Installs into .claude/skills of the current project.
Are you the author of Aif Implement?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/maister-dev-aif-implement-maister)
# Project Rules for /aif-implement
> Auto-generated by `/aif-evolve` (sections below the "Auto-generated rules" marker).
> Sections above the marker are curated from review pass-through findings.
> Last updated: 2026-09-04
> Based on: 2 analyzed patches + 1 adversarial-review pass-through (M6 / 2026-05-28)
>
> - M10 verify pass-through (2026-05-30)
> - /aif-evolve M11b/M11c adversarial-review batch (2026-06-01)
> - /aif-evolve 107-patch batch (2026-06-17, cursor 2026-05-30 → 2026-06-16)
> - /aif-evolve 114-patch batch (2026-07-11, cursor 2026-06-16 → 2026-07-07)
> - /aif-evolve 74-patch batch (2026-09-04, cursor 2026-07-07 → 2026-09-04)
## Rules
### Bidirectional contract for config→DB persistence (SET and CLEAR symmetry)
**Source**: M6 adversarial review pass-through (2026-05-28)
**Rule**: A helper that materializes a YAML/config field into a DB column that downstream readers rely on MUST handle BOTH the SET path (`field present` → write resolved value) AND the CLEAR/UNSET path (`field absent` → reset to the column default — usually `null`, `false`, or zero) inside the SAME transaction. Looping `for (entry of config) { if (!entry.field) continue; ... }` is a bug: the previous DB value persists after the operator removes the YAML field, but the reader believes "absent = default". A bug enshrined in an integration test as "documented behavior" is the worst possible outcome — it makes the asymmetry load-bearing.
Concrete pattern (Drizzle, pseudo-code):
```ts
// Wrong — only the SET branch
for (const entry of cfg.entries) {
if (!entry.field) continue;
await tx.update(rows).set({ col: resolve(entry.field) }).where(...);
}
// Right — explicit SET and CLEAR in one pass
for (const entry of cfg.entries) {
const resolved = entry.field ? resolve(entry.field) : null;
await tx.update(rows).set({ col: resolved }).where(...);
}
```
Mandatory test shape: a round-trip integration case that (a) writes with the field set, asserts the column equals the resolved value, (b) re-runs with the field removed, asserts the column equals the default. Both halves are required.
### Runtime contract symmetry — never half-ship an env/config dep
**Source**: M6 adversarial review pass-through (2026-05-28)
**Rule**: When a server process newly consumes an env var, reads a new config file, depends on a new sidecar binary, or binds a new port, the deployment artifacts in the SAME PR MUST be touched: `Dockerfile`, `compose.yml`, `compose.override.yml`, `compose.production.yml`, `.env.example`. The minimum touch set per change:
| New dependency | Files to update |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| New env var consumed by web or supervisor | `.env.example` + the consuming service's `environment:` block in `compose.yml` (+ prod overlay) |
| New config file read from a host path | host-path mount (bind or named volume) on the consuming service + a `.env.example` toggle for the host path if it's tunable |
| New sidecar binary spawned by the supervisor | dep listed in the consuming package.json (lockfile committed), and `pnpm exec <bin> --help` smoke check covered in CI or a documented manual step |
| New listening port | port declared on the service (when reachable from outside) + firewall/expose notes |
If you intentionally defer runtime wiring, the docs MUST contain an explicit "not supported in Docker — enable by …" callout in both `docs/getting-started.md` and the relevant `docs/configuration.md` section. Silent dev/prod skew is forbidden: tests passing under mocks do not prove the deployment runs the new path.
**Wire contracts span deploy units:** adding a field to a cross-process request body (e.g. `hooksConfig` on supervisor `POST /sessions`) requires the OTHER deploy unit's acceptor schema (`StartSessionRequestSchema`, which ends `.strict()` and `.parse()`s) to change in the SAME commit — even if it only accepts-and-ignores until a later phase; "this phase is web-only" is not a license to draw a phase boundary through a contract. Pin it with a test that feeds a representative resolved payload through the REAL acceptor schema (a web-side mock proves nothing; the two sides' types are structurally unrelated). Review wire schemas as a PAIR: even when the emitter is stricter today, make the acceptor symmetric (`z.array(z.string().min(1)).min(1)` + mirrored `minItems`/`minLength` in the OpenAPI) so a direct POST can't smuggle a shape the authoring layer forbids.
**Source (additions)**: 2026-06-23-13.22, 2026-06-23-18.25
### Process lifecycle: never trust `subprocess.killed` for escalation
**Source**: M6 adversarial review pass-through (2026-05-28)
**Rule**: Node's `subprocess.killed === true` after `subprocess.kill(sig)` successfully _sends_ the signal — NOT after the child exits. A child that ignores or hangs on SIGTERM sets `killed=true` and `exitedInTime=false`, but the predicate `!exitedInTime && !proc.killed` then evaluates FALSE and SIGKILL never fires; the daemon stays alive and keeps its port bound across supervisor restarts.
Correct pattern:
```ts
// Wrong
if (!exitedInTime && !proc.killed) proc.kill("SIGKILL");
// Right — escalate based on the observed exit event, not `killed`
const exited = new Promise<void>((r) => proc.once("exit", () => r()));
proc.kill("SIGTERM");
const exitedInTime = await Promise.race([
exited.then(() => true),
wait(graceMs).then(() => false),
]);
if (!exitedInTime) {
proc.kill("SIGKILL");
await exited; // wait for the actual death
}
```
Mandatory test shape: a fixture child that traps SIGTERM (`process.on("SIGTERM", () => {})` ignoring it OR a long-running blocker) and asserts the port is freed / pid is gone after `shutdown()` returns. The cooperative-exit happy path does not exercise this branch.
### Health probes must validate target identity, not just liveness
**Source**: M6 adversarial review pass-through (2026-05-28)
**Rule**: For sidecar/proxy daemons managed by the supervisor, the readiness probe MUST identify the _intended_ service, not merely "something answered on `host:port`". Generic `GET /` returning any non-5xx is insufficient: if the configured port is already occupied and the spawned child fails to bind (`EADDRINUSE`) and exits, an unrelated local HTTP service can answer 200/404 and the manager will transition to `ready`. Later traffic routes into the wrong process and surfaces failures far from the real cause.
Acceptable identity signals (pick one):
1. A target-specific endpoint that the daemon exposes (e.g. `GET /health` returning a known body shape, `GET /version` returning a known name string).
2. A recognizable response header set by the target.
3. A response body matching a known schema, validated before transition.
Additionally, register `child.once("exit", ...)` BEFORE starting the health-probe loop. If the _target_ child exits before becoming ready, abort the probe immediately and surface `EXECUTOR_UNAVAILABLE` (or the equivalent domain error) with the exit code/signal in the message.
Mandatory test shape: a case where another HTTP server is bound to the configured port BEFORE the manager starts the target child. Assert the manager either (a) detects the port is occupied and fails fast, or (b) detects the target child exited before readiness and fails with a target-identifying error. A green test against an empty port does not cover this branch.
### Every committed test MUST execute, and the suite stays green (or is explicitly quarantined) at each phase
**Source**: M10 verify pass-through (2026-05-30)
**Rule**: A committed test that no runner runs gives false confidence; a suite left silently red across phases hides regressions. Two obligations, checked at every phase-completion checkpoint:
1. **No dead tests.** Every committed `*.test.ts` / `*.integration.test.ts` MUST be matched by exactly one runner project's `include` glob and not swallowed by an `exclude`. After adding a test — _especially in a new location_ — prove it runs: `vitest list --project <unit|integration>` must show the file, and it must appear in the run summary's collected set. A test under a path the config does not glob never runs (M10: `app/api/runs/__tests__/route.trust-boundary.integration.test.ts` lived under `app/**` while the integration project only globbed `lib/**` — it never executed). If a test lands in a new location, update the runner config in the SAME commit.
2. **Phase-green or explicit quarantine.** At each phase checkpoint run the full suite (`pnpm test:unit && pnpm test:integration`, or the project equivalent). The phase is NOT done while a test the phase touched is red. A test red for a reason genuinely outside the phase's scope (pre-existing breakage, or a harness limit like an unresolvable import) MUST be quarantined VISIBLY — a config-level `exclude` or `*.skip` carrying a one-line reason + tracked follow-up — NEVER left silently red and NEVER deleted to "go green". Note: a module that throws at IMPORT time cannot be quarantined with `describe.skip` (the skip never evaluates) — exclude it at the runner-config level instead.
When a refactor changes observable behavior an existing test asserts (error-message text, resolved cache paths, addressing scheme), migrating those assertions is part of the SAME task that changes the behavior — a stale assertion is a red test today, not a follow-up (M10: the loader's new `flow install failed [stage=clone] …` message and digest-addressed local cache paths left three loader assertions stale).
Mandatory before declaring a phase done: (a) `vitest list` (or equivalent) shows every new test file; (b) the suite summary shows zero unexplained failures; (c) every quarantined test carries a reason + follow-up reference.
2026-07..09 additions: "green" requires the suite to actually LOAD — a 0-test suite (`Failed to load url sqlite`) is a failure; when the dependency tree is unavailable, every gate is reported UNRUN and the delivery NO-GO (static review + `git diff --check` are not evidence). Judge integration regression on NAME SETS, never counts (the suite flips ±2 tests run-to-run); BASELINE-PROVE before blaming the diff (stash and run on main — 7 `lib/runs` failures were identical on main); a "red on main / accepted baseline" test is DEBT to diagnose, not a constant. Environment gaps are categorized before chasing: `supervisor unavailable (network): fetch failed` (no :7777), `listen EPERM` (sandbox), no container runtime.
**Source (additions)**: 2026-07-12-11.21, 2026-07-11-13.34, 2026-07-27-20.55, 2026-07-17-00.00, 2026-07-22-01.14, 2026-07-11-23.47-mcp-v2-review-fixes, 2026-07-11-12.42
### DB-schema and contract edits are not done until the spec set is updated AND verified by grep
**Source**: M10 verify pass-through (2026-05-30)
**Rule**: Extend the "Runtime contract symmetry" discipline to DB schema and every contract surface, and VERIFY each wire rather than assuming it — a surface silently skipped passes every code test yet ships a broken contract.
- **DB schema change** (new table/column/index + Drizzle migration): update `docs/database-schema.md` AND the relevant `docs/db/*.md` Mermaid ERD in the SAME commit. The narrative table and the ERD diagram are two separate artifacts — touching one is not touching the other (M10 updated `database-schema.md` but left the `docs/db/*.md` ERDs stale).
- **New env var**: after editing, `grep` the var name across `compose.yml`, `compose.override.yml`, AND `compose.production.yml`; absence from the production overlay (when production-relevant) is a half-ship, not a follow-up (M10 wired only `compose.yml`). `.env.example` + the `docs/configuration.md` env table are both required.
- **HTTP route / error code / manifest field / script**: trace each to its spec file per the aif-plan contract-surface table and open the file to confirm the edit landed.
This is the implement-side enforcement of the plan-side "trace every contract surface" rule: the plan lists the surfaces; implementation closes every one and PROVES it (grep / open the spec).
- **Adding auth to a route is a CONTRACT change**: any guard that can refuse (`requireActiveSession`, `requireProjectAction`, a new zod refinement) adds status codes — edit the route's spec entry (`401`/`403` via the `components/responses/Unauthenticated`/`Forbidden` `$ref`s) in the SAME edit; the client-side consequence (stop `use-activity-ping` on 401/403) and the spec-side consequence are one thought.
- **`additionalProperties: false` turns a missing property into a REJECTED request**: every `.strict()` zod body has exactly ONE spec schema, edited as a pair; sweep BOTH classes over every route the branch changed — Class A (zod field ⊄ spec properties: `syncStrategyDefault`/`syncRunnerId` missing while the UI PATCHes them) and Class B (route returns 401/403, spec omits); `/api/v1/ext/*` lives in `operations.openapi.yaml`, everything else in `web.openapi.yaml`.
- **An engine bump is a cross-surface change**: constant + compatibility tests + fixtures + the agent-facing DSL grammar (`settings.plan_review` + floor) + current host-version docs in ONE change. When a write path changes its STORED form (`[@<id>](/agents/<id>)` markers), every DTO returning that text (`TaskCommentDTO`, ext `CommentDTO`, MCP `comment_list`) is in scope. When the UI phase of a feature lands after its backend merged, RE-RUN the as-built docs reconcile — the earlier reconcile cannot cover surfaces that did not exist yet. Release tasks record the external package TAG + COMMIT, and release evidence is reproducible from the TAGGED bytes (patched live during acceptance → re-cut the tag, re-run the gates from a fresh checkout).
**Source (additions)**: 2026-07-16-15.20, 2026-07-15-12.41-engine-3.1-and-product-contract-sweep, 2026-07-27-09.30, 2026-07-12-22.40, 2026-07-14-22.10, 2026-08-01-09.09
## Auto-generated rules (managed by `/aif-evolve` — do not hand-edit below this line)
### Validate at the SINK's invariant, not just the type's shape
**Source**: 2026-05-26-12.45.md, 2026-05-26-12.53.md
**Rule**: When implementing input validators (Zod or any boundary validator) whose fields flow into `path.resolve` / `path.join` / `fs.*` / `child_process.spawn` argv (including `cwd`) / SQL / HTML, add a regex or `.refine()` matching the sink's invariant. Concrete patterns:
- Filesystem path **segment** (no `/`, no `..`): `z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/)`
- **Absolute** filesystem path: `z.string().min(1).refine(p => p.startsWith("/") && !p.split("/").includes(".."), { message: "must be absolute and contain no .. segment" })`
- SQL: parameterized query — never string concat
- HTML: escape function or framework auto-escape
`z.string()` / `z.string().min(1)` is **shape** validation. The sink demands **invariant** validation. These are not the same. Reason: patch 12.45 — `runId` / `stepId` were `z.string().min(1)` and were path-resolved as filename segments; an HTTP caller could set `runId: "../../../etc"` to escape `.maister/`.
Extensions: (a) any value interpolated into a STRUCTURED sink — XML attribute, dotted Mustache template path, regex — is validated against that sink's grammar at the boundary that introduces the interpolation (`isInjectableArtifactId` `^[A-Za-z0-9_-]+$` with a load-time `CONFIG` refusal); (b) a normalizer (trim + strip `./`) is NOT a validator — pair it with a namespace predicate in the same module (`isRootSchemaFilePath` enforcing `./schemas/<name>.json`) or free-text refs overwrite unrelated package files; (c) validate producer output against the CONSUMER's contract (schema-valid-but-empty distill output wedged a paid re-distill loop) and config against the ENGINE's real limits (embedding dims ≤ pgvector's 2000 HNSW cap, not "positive int" — the DDL failed AFTER the settings commit); (d) if a settings schema declares types, validate the declaration's `default` AND every per-instance write at the service seam (`updateAgentLink` → `MaisterError("CONFIG")` → 422) so ALL callers are covered.
**Source (additions)**: 2026-07-01-01.27, 2026-06-26-03.12, 2026-07-02-17.52, 2026-06-25-17.42
(e) a git URL/remote is a subprocess-argv sink: `packageSourceCreateBodySchema.url` was a bare `z.string()` and `kind:'git'` was never validated — `ext::sh -c '…'` reached `git remote add` and executed on the host; `validateUrl` for every producer incl. `ensureDefaultPackageSources`, `--` in `gitSetRemote`, reject `::` smart-transport and `-`-leading scp user/host (option injection), `..`-segment refusal on local paths (never rely on git's default `protocol.ext.allow`); (f) graph node ids are restricted to safe path/object-key values; (g) every `::timestamptz` sink gets `isoTimestamp` (all three `mergedAt` parse sites).
**Source (additions)**: 2026-07-12-11.12, 2026-07-12-11.42, 2026-07-16-16.05
### AbortSignal on every async generator/iterator that talks to a network resource
**Source**: 2026-05-26-12.53.md
**Rule**: Any consumer-facing async iterator, async generator, or streaming function that wraps `fetch` / SSE / WebSocket / any cancellable network call MUST accept `opts.signal?: AbortSignal` and forward it to the underlying transport (e.g. `fetch(url, { headers, signal: opts.signal })`). Do not ship the public API surface without it — every consumer (Route Handlers, Server Actions, UI hooks) will eventually need a clean cancel path, and retrofitting later is painful. Concrete shape: add `opts.signal?: AbortSignal` to the function signature, document it, write at least one test asserting forwarding.
### Cap buffers + fast-path expensive parsers + uncap legitimate listeners — in the SAME commit as the data path
**Source**: 2026-05-26-12.53.md
**Rule**: When implementing a data path that consumes external bytes (subprocess stdout, network stream, file tail), the following defenses are part of the shipping bar, not "Phase 2 hardening":
1. **Cap unbounded line/chunk buffer.** Pick a sane `MAX_LINE_BYTES` (default 1 MB for log-like streams). When `buffer.length > MAX_LINE_BYTES`, emit the truncated line (`buffer.slice(0, MAX_LINE_BYTES)`), log `WARN line-buffer-overflow` with the source, then drop the rest of the buffer. Add a test that drives the overflow path via a fixture (e.g. supervisor uses a `--giant-bytes N` flag on `fake-acp.mjs`).
2. **Fast-path expensive parsers.** If a per-line operation (`JSON.parse` + tree walk) is only needed for ~1% of lines, short-circuit with a cheap substring check first: `if (!line.includes("usage")) return;` before `JSON.parse`. Pick the substring from a stable token the target structure always contains.
3. **Uncap legitimate listener counts.** For per-session `EventEmitter`s with multiple legitimate subscribers (cost tracker + registry auto-subscribe + N SSE consumers), call `emitter.setMaxListeners(0)` right after construction. Node's default 10-listener cap exists to detect leaks, not to enforce a real ceiling.
Each item is a one-line addition. Ship them with the commit that introduces the data path; do not file as "Phase 2 hardening" tickets.
Cap placement rules: enforce size/count limits at the REQUEST boundary on declared metadata BEFORE materializing (`fileParts.length` ≤ maxEntries, each `file.size` ≤ maxFileBytes, running total ≤ maxBytes → 422 before any `arrayBuffer()`/`Promise.all`); decompression limits run DURING streaming with a running actual-bytes counter (declared `header.size` pre-check + abort flag on first breach) — a compressed-size cap is not an uncompressed-size cap; a "size cap" must bound the READ/allocation itself (positioned `FileHandle.read` with `maxBytes`; pass `cap + 1` to detect truncation), never trim an already-loaded buffer; enumerate EVERY overflow-prone source on a capped path and check each one's overflow semantics (`git diff` stream-truncates but `logRange`'s 4 MiB child-process buffer THROWS — share `streamGitTruncatedTo(repo, args, maxBytes, label)`); enforce caps DURING enumeration, not post-hoc after walking the whole tree, and assert the boundary value in the error evidence; broad globs / high-fragmentation files need explicit source budgets (match count, aggregate bytes, chunk budgets) at registration AND worker time; paid APIs are sinks too — cap every field feeding embedding/LLM token spend (`content` ≤ 32k, `q` ≤ 2k, `tags` ≤ 10×64) and mirror caps route-Zod ↔ MCP TOOL_SPECS ↔ both OpenAPI specs.
**Source (additions)**: 2026-06-21-01.26, 2026-06-20-21.18-m36-flow-package-viewer, 2026-07-01-01.27, 2026-07-01-11.51, 2026-07-04-01.04, 2026-07-04-14.15, 2026-07-02-17.52
### `logger.flush()` before `process.exit` when transport is async
**Source**: 2026-05-26-12.53.md
**Rule**: In any shutdown handler that calls `process.exit`, await `logger.flush()` (wrap in a promise if needed) before exiting. Otherwise the final log lines — including the shutdown reason — are truncated mid-flight. This applies to pino with pretty-print or transport workers (a worker thread holds the unflushed output) and to any logger whose sink is async (network, batched file writes). Treat it as part of the shutdown template, not a per-service decision.
### Claim the transition FIRST (status CAS), then the unique-constrained write; never mark Failed until execution is confirmed stopped
**Source**: 2026-05-31-22.46 (#1/#2), 2026-06-01-12.55 (#1/#2)
**Rule**: For a state-changing handler OR a background sweep/watchdog that performs a contention guard plus other writes:
1. **CAS before any other uniquely-constrained insert.** Run the status CAS FIRST inside the transaction; only the CAS winner performs the unique-constrained INSERT (e.g. a `node_attempts` row with `UNIQUE(run_id,node_id,attempt)`). A loser then rolls back at the CAS → deterministic mapped 409, never a raw Postgres `23505` surfacing as 500. Placing the constrained insert before the guard inverts this and leaks an unmapped DB error.
2. **A background sweep racing a live single-writer claims atomically.** Use one guarded `UPDATE runs … WHERE status='Running' AND current_step_id = <node> RETURNING`, and ONLY on a non-empty claim mutate the ledger (`markNodeFailed(…, tx)` in the same tx). A predicate-less `markNodeFailed(attempt.id, …)` in a separate statement clobbers a concurrently-`Succeeded` attempt to `Failed` while the guarded run update matches zero rows.
3. **Never mark work terminally `Failed` until its execution is confirmed stopped or confirmed absent.** Key the teardown lookup on the server-owned identity the supervisor tracks — `(record.runId === run.id && record.stepId === run.currentStepId && status === 'live')` — NOT `runs.acp_session_id` (which the graph runner persists only AFTER the node's prompt returns, so it is null exactly during the long/over-cap window the watchdog must act on). A retryable teardown failure (`EXECUTOR_UNAVAILABLE`) leaves the run `Running` to retry next tick (no split-brain); only a confirmed-stopped/absent (404) session proceeds to `Failed`. Matching by exact `stepId` also avoids killing a later node's session after the cursor advanced.
Mandatory tests: a mid-prompt kill with null `acp_session_id` + a live record (matched by runId+stepId), a retryable 5xx that leaves the run `Running`, and the loser of two racing claims getting 409 (not 500).
Session-teardown extensions: a "find the live supervisor session for this run" lookup keys on the server-owned `runId` from `listSessions()` records (status `live`), NEVER on `run_sessions.acp_session_id` — that column is NULL for the entire mid-prompt window (persisted only AFTER the node's prompt returns); use `(runId, stepId)` only for exact-node protection, plain `runId` for "all of this run's live sessions" (stop/terminate). The live process is torn down BEFORE the run flips terminal — assert the call ORDER in a test (`deleteSession` before `markStoppedAndCloseAssignments`, driven by a mid-prompt fixture). A terminal flip meant to STOP external work must actually kill the sessions — a DB-only cascade let a tree keep spending past a hard budget ceiling (kill the root's live session first, `EXECUTOR_UNAVAILABLE` → leave for next tick, then best-effort per cascaded child). Before copying a bail-on-`EXECUTOR_UNAVAILABLE` pattern from a sibling escalation helper, check whether the caller RE-FIRES (budget watchdog ticks again — safe) or fires ONCE (supervisor `hookHalted` — a swallowed failure is terminal). Composite HITL recovery checks run BEFORE normal run-state availability checks; preserve-before-terminal composites use `checkpointSession`, never a generic stop dispatcher.
**Source (additions)**: 2026-06-27-17.49, 2026-06-24-03.08, 2026-06-22-13.05, 2026-07-02-22.22-budget-breach-review-fixes
### Public route responses are explicit DTO projections — never serialize a DB row or a server-only handle
**Source**: 2026-05-30-13.38, 2026-05-31-20.48, 2026-05-31-23.58, 2026-06-01-01.29
**Rule**: A public route handler / server action MUST project an explicit response DTO at the boundary — never `return` a DB row or a service object verbatim. Service/internal return types may stay rich; the route narrows them. The following MUST NOT appear in any client-visible response: `acp_session_id`, supervisor session id, adapter launch argv/env, materialized instruction/profile paths, worktree paths, internal cost handles. Mirror the projected DTO in `docs/api/web.openapi.yaml` AND a focused integration/unit test asserting the EXACT shape (no extra keys). Keep `runs.status` and any `*.dialog_status` as a single contract: stop/discard/abandon on already-terminal runs are idempotent (a `Done` run is never resurrected to `Review`, an `Abandoned` discard is a no-op). Route handlers stay thin — request parsing, auth/session checks, service calls, HTTP error mapping — with launch/message/lifecycle orchestration in the service layer.
Redaction proof obligations: the safe-projection boundary gets a test at the MAPPING function fed an input that actually CONTAINS the secrets (`authToken: "env:X"`), asserting the output's EXACT key set and that its JSON has no `env:` substring — a test fed the already-safe DTO literal is vacuous (a future `{ ...runner }` spread would leak with zero failures). When the safe DTO is a discriminated union, drive the sweep from the type union via a table covering EVERY secret-bearing variant, plus a guard test that fails when a new union entry is neither in the secret-free allowlist nor in the sweep. Don't keep disabled-state fields in a DTO when the endpoint filters those records out.
**Source (additions)**: 2026-06-18-12.07, 2026-06-18-12.57, 2026-06-25-17.16-flow-assistant-review-followup
2026-07..09 additions: two serializers for one error type WILL diverge — a module-local `*ErrorResponse` (`catalogErrorResponse` dropped the `details` that `project-route-helpers#errorResponse` forwards per ADR-093, voiding the documented `details.reason="edit_lock_not_held"` 409 discriminator) is diffed field-by-field against the canonical one, and every reachable `details` payload is enumerated before forwarding (no paths, session ids, argv). Admin summaries expose LOGICAL artifact context, never host-local paths. A read-model field with a different visibility requirement (cross-project attachment counts) is an explicit OPT-IN aggregation requested only for authorized operators, tested for default omission AND authorized opt-in. A result DTO gates its payload on the DERIVED status (never a payload under "no result available"); a collect marker takes the SERVED row id on an internal companion type so the wire body never gains a ledger handle. See the authorization rule for RSC reads, two-domain reads, and `redacted` flags.
**Source (additions)**: 2026-07-21-23.50, 2026-07-11-13.34, 2026-07-13-11.21, 2026-09-03-17.20
### Honesty + write-once guard + sparse-default + enforce-external-contracts
**Source**: 2026-06-01-13.16 (#1/#2/#3), 2026-05-31-23.49 (#2), 2026-05-31-22.48
**Rule**: Four habits adversarial review keeps re-finding:
1. **Honest visibility.** A surface that RECORDS a value but does not ACT on it (an enforcement/settings snapshot not yet delivered to the agent) MUST say so in label/UI/docs. An unqualified status word (`Instructed`, `Enforced`) reads as "action was taken" — a false compliance signal. State what the surface does NOT do (e.g. EN+RU "declared intent, recorded for visibility, not yet delivered").
2. **Write-once guard.** A column documented append-only/write-once MUST encode it as a write guard — `UPDATE … WHERE id = ? AND col IS NULL` (+ `.returning()` to log whether the write landed) — not rely on callers writing it once. A `NeedsInput` resume reuses the same attempt row and re-runs the gate, overwriting the first-attempt audit. Prove with a resume/re-entry regression.
3. **Defaults at the non-erasing layer.** A sparse config map MUST stay sparse through the Zod parse (keys `.optional()`, NOT per-key `.default()`); fold the default in at evaluation (`enforcement?.[cls] ?? "instruct"`). A parse-time default expands `{ mcps: "strict" }` into all classes and destroys the "was this explicitly declared?" signal. A test built from a hand-written object literal bypasses parse-time defaults — parse real input through the production schema.
4. **Enforce, don't assume, an external/human-side precondition.** If a flow says "the user commits before returning," verify it server-side (`git status --porcelain` clean-worktree check) and reject a dirty/untracked tree or a zero-commit range with a retryable 409 — model the artifact as the working tree (what the human actually changed), not a proxy (`base..branch`, which silently drops uncommitted work). When code intentionally implements a simpler model than the plan proposed, DELETE the unimplemented contract variants from the docs rather than leaving them tagged "Designed".
5. **Phase-honest vocabulary.** In a phased feature, log/comment/UI words like "armed"/"enforced"/"active" must describe what THIS phase does — `"guardrail hooks resolved (enforcement: Phase 2)"`, not `"armed"`; add a `// NOTE: resolved-and-sent ≠ enforced` at each site. A reviewer flagging a "false safety boundary" on a deliberately-phased branch means make the intermediate state HONEST, not build the next phase early.
6. **Write-once timestamps via a shared helper.** First-occurrence timestamps (`launchedAt`) go through one status-timestamp transition helper so recompute transitions back to an earlier state never rewrite them.
**Source (additions)**: 2026-06-23-14.19, 2026-07-04-14.25-experiment-comparison-review-followup
7. **Claims of safety nets are verified, WARNs are not fixes.** A "co-evolve deferral" comment ("preflight already refuses X — fidelity, not safety") is a CLAIM to check against the actual gate — if a seam cannot honor a declared input, the validation layer WARNS or REFUSES, never silently passes; a WARN or a provenance record over a known gap is a SURFACING measure — track it as an open defect until enforcement lands, or an adversarial pass re-files it as no-ship. A requirement or ADR sentence that names a MECHANISM ("reads the persisted snapshot", "discriminated Zod union") is met literally or AMENDED in the same pass when the implementation chose otherwise (the snapshot is audit-only; re-derive + re-deliver), and false code comments claiming the old mechanism are corrected. An INVERTED rationale is worse than a missing one — "target-scoped fetch" documented 8× as WHY the lease is safe while the code is safe for the opposite reason would lead a reader to optimize away the `ls-remote` that makes it work; prose that names a mechanism is verified against the mechanism it names. Every documented MUST names its enforcement point (load gate, runtime guard, test) — "zero published ports" enforced only in a fixture is a convention: enforce on the RENDERED `compose config` model; the `MAISTER_FLOW_DIR` floor got a load-time scan (`declaresFlowDirCommand` in `loadFlowManifest`: a cli/check `action.command` containing `MAISTER_FLOW_DIR` requires `engine_min >= 3.3.0`, else `CONFIG`). An ASYMMETRIC boolean (`summonable`: `false` hard negative, `true` weak) gets both arms documented and one test per arm. Document an enforcement BOUNDARY where the reader stands — limits (the seam sees only permission-gated calls; the D5 backstop is WRITE_KINDS-only) go as comments at the exact check plus an Edge-cases bullet.
**Source (additions)**: 2026-07-24-15.19, 2026-07-26-12.32, 2026-07-11-21.30, 2026-09-02-03.51, 2026-07-16-14.05, 2026-08-01-09.09, 2026-07-27-09.30, 2026-07-12-10.43
### Concurrency control: a transaction is atomicity, NOT mutual exclusion
**Source**: 2026-06-02-18.44, 2026-06-03-00.52, 2026-06-03-01.27, 2026-06-07-18.35, 2026-06-08-13.13, 2026-06-10-11.42, 2026-06-10-20.48, 2026-06-11-11.54, 2026-06-11-15.00
**Rule**: This GENERALIZES the existing CAS-first rule. For any read-then-write that can run concurrently:
1. **Re-read the guarded state UNDER the lock.** A `SELECT id … FOR UPDATE` that selects only `id` does NOT re-read `status`; re-read `{ id, status }` under the lock before mutating. A pre-check outside the lock is a fast-path optimization, never the decision.
2. **Lock the row whose invariant you protect.** Locking `runs` while the mutation writes `gate_results` does not serialize writers to `gate_results` (`markGateOverridden`/`markDownstreamStale` bypass the run lock). Put the lock/CAS on the row being protected.
3. **CAS on the EXACT observed value**, not a permissive set. `casLiveTransition(d, id, fromStatus, toStatus)` → `UPDATE … WHERE id = $1 AND status = $fromStatus RETURNING id`; 0 rows → throw the mapped domain error (409), never let a raw Postgres `23505` surface as 500. A set CAS (`status IN LIVE_STATUSES`) silently permits within-set transitions.
4. **Inserts:** `insert(...).onConflictDoNothing().returning({id})` → empty → typed 409. Never gate uniqueness on a read-then-write `SELECT` (TOCTOU). Any `onConflictDoUpdate` whose conflict target excludes the owning entity needs an ownership pre-check (or the owner in the key); any bulk DELETE keyed on author-chosen ids needs an ownership predicate.
5. **Append/versioning:** lock a STABLE parent row (`SELECT id FROM runs WHERE id = $1 FOR UPDATE`), not the rows being read — the new row does not exist yet, so `FOR UPDATE` on the read set cannot serialize it. Wrap a "record current" + "supersede prior" pair in ONE `db.transaction`.
6. **Fencing token** when a guard state is re-enterable by a timeout/reclaim: carry a per-attempt stamp in the WHERE (`eq(lastFiredAt, stagedAt)`), not just the state label — else a hung attempt clobbers a newer reclaim's marker and cross-attributes its run id.
7. **Batch loops** staging side-effects must thread accumulated state (`reservedSlots: intents.length`; `capFull = countLiveRuns(tx) + reservedSlots >= cap`) into per-item decisions — a pre-batch snapshot read let cap-3 launch 10 (staged runs create no rows until the claim tx commits).
8. **Claim-before-side-effect** when the side-effect is destructive or racers' effects diverge (irreversible `discard` BEFORE the CAS is a bug; reuse the `promote.ts` 3-phase / `resumeRun` claim-before-spawn-with-rollback shape). Marker-after is acceptable ONLY when every racer's side-effect is idempotent AND identical.
9. `NOT (nullable_col = X AND …)` is a three-valued-logic row-drop (`NULL = 'x'` is NULL, a NULL WHERE term drops the row) — write `IS DISTINCT FROM` / explicit `IS NULL` arms, and re-run the happy-path suite after touching any claim predicate.
Prove the 409-vs-500 contract with a REAL two-racer test (hold an uncommitted write on a 2nd pg connection, wait on `pg_stat_activity` `wait_event_type='Lock'`, then commit), never a single-threaded one.
10. **Re-assert the selecting predicate in the UPDATE's WHERE.** Under READ COMMITTED the SELECT's view is not the UPDATE's view — an UPDATE acting on a row selected earlier re-asserts the predicate (+ `RETURNING`, fall-through on 0 rows) whenever another writer can flip it; likewise a claim tx re-checks EVERY precondition written by out-of-band transactions (`promotion_hold`, master toggles) under the lock, not just `status` — one TOCTOU per un-rechecked predicate.
11. **Verify a claimed unique-constraint backstop actually applies.** A partial index keyed on a column that is NULL for the racing path is INERT (`runs_auto_task_uq (task_id) WHERE launch_mode='auto'` + targetless `onConflictDoNothing()` was the fix); prove backstops with a CONCURRENT test — "call it twice" passes while the index is inert.
12. **Insert-first claims before filesystem work.** Claim the unique identity via DB INSERT (23505 → CONFLICT) BEFORE scaffolding; give each attempt a UNIQUE path (`${slug}-${uuid8}`) so a loser's cleanup can never `rm` the winner's dir.
13. **`FOR UPDATE` on the parent serializes FK binds.** A guard that reads-then-deletes across an `ON DELETE SET NULL` FK wraps read+mutate in one tx with `.for("update")` on the parent row — it conflicts with the concurrent bind's `FOR KEY SHARE`, so the bind serializes then fails its FK.
14. **CAS mutexes release by token** (the claim timestamp passed to release), never an unconditional clear, with staleness auto-recovery so a crashed holder never wedges the lock.
15. **Guard-on-every-edge.** Grep every transition INTO a guarded state (`tryStartRun` AND `promoteNextPending` are independent Pending→Running edges) and guard each, or factor one shared admit() helper — a guard on one of N edges is a guard on none.
16. **Idempotency-key sharing changes downstream identity.** When a dedup key makes two callers share a `taskId`, every derived identity (branch `task-<id>/attempt-N`) collides — only the claim winner launches; losers converge via a bounded one-shot reselect of the winner's run (`convergeToTriggerWinner`, ≤1s, `deduped` fallback), never state-polling.
17. **Deterministic race tests.** Use a controllable barrier (blocking stub inside the locked tx) or stream-FIFO ordering instead of sleeps; a flaky single-winner test flaking to `[200,200]` is a REAL lost-update bug — read what it flaked TO before blaming infra.
**Source (additions)**: 2026-07-02-17.52, 2026-07-03-14.58, 2026-06-20-23.22, 2026-06-21-01.26, 2026-06-21-02.04, 2026-06-21-03.05, 2026-06-23-18.38, 2026-06-25-20.14, 2026-06-24-20.47, 2026-06-22-14.06, 2026-06-23-18.25, 2026-06-30-01.37
18. **A lock is not a gate; the lock's KEY equals the invariant's scope.** Before trusting a transaction to serialize a check ask what rows it LOCKS: a GLOBAL invariant (count, cap, quota) with per-row updates serializes nothing — take the advisory lock the codebase already uses (`takeSchedulerLock(tx)` before `countLiveRuns`, as `claimGraphResumeSlot`/`claimAgentResumeSlot` do); a lock keyed on the immediate parent can never protect an ancestor-scoped count (`max_child_runs` over a subtree — two coordinators hash to different keys and both pass under READ COMMITTED) → lock the TREE ROOT too, parent-then-root on every path (re-entrant when equal). A "CAS" over a FILESYSTEM is check-then-act until something serializes it — `atomicWrite*` answers "is the file torn?", never "can two writers both win?"; hold a per-`(project, agent)` `pg_advisory_xact_lock` across read → compare → write (mirror `social/relations.ts`) and return the post-write state captured INSIDE the lock.
19. **A status-guarded CAS is NOT a precondition check.** It fires at the END, so everything irreversible above it has already happened when it refuses; if a handler can refuse, validate BEFORE the first irreversible act (`ABANDONABLE_STATUSES` before the cascade, identical 409) and re-read the value at the MOMENT it matters — a status fetched five awaits earlier is a hint. A refused request leaves NO TRACE. Order is lock → verify not-already-done → commit intent → perform effect: `reset --hard` + `git clean -fd` ran BEFORE the tx locking `hitl_requests`; split policy RESOLUTION (read-only, before the tx) from APPLICATION (after the marker commits, winner only) — an "idempotent on retry" comment justifies crash recovery, not admission. Preconditions protecting irreversible work (a clean committed tree before a version cut) run INSIDE the same exclusive lease as the work, never at the route level before it.
20. **Every call to a status-guarded transition branches on its RESULT** before any dependent effect — grep `crashRunningRun`, `finalizeAgentRun`, `markAbandoned`, `crashWaitingOnChildren` for callers that discard the return; a lost CAS is `skipped` at WARN and touches nothing (materialization cleanup, assignment close, slot promote, counters). State read BEFORE a CAS and acted on AFTER it is covered BY the CAS (guard on `current_step_id`, not only `status` — `Running` survives node A finishing and B starting across a cross-process `await checkpointSession`) or re-read inside the tx; `markNodeNeedsInput({requireRunning})` rolls back a lost attempt-level race; resolve a parked attempt INSIDE the lock after the already-delivered check.
21. **Guarded and unguarded variants of one write coexisting = a bug queue** (`failAttemptIfActive` at 1 of 9 sites) — fix the CLASS at the choke point: `setAttemptPhase` CASes on the EXACT observed non-terminal phase and returns boolean; `advancePhaseOrCancel` throws `CONFLICT` on a loss, cancelling every git side effect below; the attempt phase is the DURABLE cancellation signal (survives restart; a lost CAS MEANS the run was terminalized). A `// NO same-session escape` mutex signals the SAME hazard on every sibling that mutates the resource — generalize to a working-dir mutex and re-read state UNDER it; a mutation on the SHARED parent repo takes `withRepoPromotionLock` and PROVES divergence with `isAncestor` (a dirty parent checkout also fails `merge --ff-only`).
22. **Before inverting an ordering to close a race, enumerate the recovery for the NEW partial state** — a window with a tested backstop beats a narrower window with none (abandon stays cascade-first: reconcile M-2 covers it; the inverted order has an unrecovered window and `auto_launch_run_plan` could launch into an abandoned tree).
**Source (additions)**: 2026-09-01-04.15, 2026-09-03-14.30, 2026-07-27-20.55, 2026-09-03-15.37, 2026-09-03-17.21, 2026-09-03-23.46, 2026-07-17-00.00, 2026-07-12-11.12, 2026-07-16-20.43-canonical-create-flow-review-findings, 2026-07-16-16.05
### Git mechanics: diff range, immutable refs, worktree isolation, branch-ref sinks
**Source**: 2026-06-02-03.12, 2026-06-04-13.03, 2026-06-05-15.06, 2026-06-09-13.01, 2026-06-09-20.30, 2026-06-01-15.31, 2026-05-31-14.18
**Rule**:
- **2-dot vs 3-dot:** a fixed stored SHA base → `git diff base..branch` (literal tree delta); two MOVING branch tips where excluding the base's own advancement is intended → `base...branch` (3-dot). A `...` against a fixed point silently omits committed changes on a history-rewritten branch (`diffRunWorkspace`/`diffNameStatus` used 3-dot wrongly).
- **Immutable refs for stored evidence:** persist `resolveRefSha` = `git rev-parse --verify <ref>^{commit}` (schema-hardened, no shell), never a live branch NAME — an old artifact renders the wrong diff once the branch advances.
- **Worktree ignore:** materialized run files go in a worktree `.gitignore` (+ `git update-index --skip-worktree` on the `.gitignore` itself), NEVER `.git/info/exclude` — in a LINKED worktree `git rev-parse --git-path info/exclude` resolves to the SHARED common dir (`<main>/.git/info/exclude`) and leaks the rule to every worktree + main. `--skip-worktree` hides changes to TRACKED files only; a freshly-materialized UNTRACKED file needs gitignore. First ask whether the artifact NEEDS to be in the worktree at all (a `command_check` script runs with CWD=worktree, so its logic can stay inline in the manifest — nothing to materialize or leak).
- **Branch-ref validation at EVERY sink:** export `branchNameSchema` and apply a shared `assertSafeBranchRefs(args)` as the first statement of every git/PR adapter (`gh pr create --base`, `glab`, Gitea `base`), sweeping BOTH server-generated `sourceBranch` and request-derived `targetBranch` — a bypass flag (`allowTargetDrift`) must not route around it.
- **Ordering & compensation:** the irreversible/external mutation (`git init` / `git clone` / `git worktree add -b`) is the LAST committed step after all cheap preconditions pass; every compensating `rm`/branch-delete is gated on proof THIS request created the path (`clonedByUs`, `gitStatus === "initialized"`), and failed-launch compensation must delete the branch ref `git worktree add -b` created, not just the worktree dir.
- **Gates run BEFORE irreversible side-effects:** a finalize-tx re-check cannot undo an already-executed `git merge` + push — the failure-terminal/promotability guard runs in the claim tx before ANY git side-effect, and rework-vs-promote serialize on the SAME `workspaces` row `FOR UPDATE`. Ask "if this gate fails, can I undo what already happened?" — if not, move the gate earlier.
- **Ownership state lives in the DB, not a filesystem observation:** deciding allocator-vs-reuser from `listWorktrees` leaves a crash window between `git worktree add` and the row insert — decide from the `workspaces` row, make the insert idempotent (`onConflictDoNothing(worktree_path)`), add orphan-claim + reconcile recovery, and scope teardown with an `allocatedWorktree` flag so it removes only a dir THIS launch created.
- **Proof-of-ownership = exclusive create:** a `rm -rf`/branch-delete compensation flag is set only from an EXCLUSIVE create (non-recursive `mkdir` with `EEXIST` → refuse `PRECONDITION`; `mkdtemp`+rename; `clone -b`), never an idempotent recursive mkdir that can't distinguish create from already-exists.
- **Counter/sequence bumps another feature reads as a budget** (`tasks.attempt_number` as the ralph-loop cap) are side-effects — sequence them after every cheap throwing precondition (launchability, branch allow-list, `resolveBaseCommit`), immediately before the point of no return; grep every reader of the column before accepting a "burned value is meaningless" claim.
- **Relaunch/restart pins to the STORED immutable base commit**, never re-resolves a moving ref.
- **`git check-ignore` is the ground-truth ignore test**, but first branch on `rev-parse --is-inside-work-tree` ("not a git tree" = no leak surface = write; "git tree, not ignored" = skip), and a best-effort `ensureWorktreeGitExclude` has its OUTCOME confirmed before the dependent write.
**Source (additions)**: 2026-06-22-11.08, 2026-06-22-13.40, 2026-06-18-15.42, 2026-06-30-12.23, 2026-07-04-00.47-experiment-comparison-review-fixes, 2026-06-22-22.14
- **Model the POINT OF NO RETURN explicitly:** once an irreversible external side effect lands (force-push, PR create, publish) a flag (`pushCommitted`) marks it and EVERY compensating path consults it — the catch aborts ONLY when `!settled && !pushCommitted`; post-push it logs the decisive fields (`headShaAfter`) and releases the run best-effort (Review + claim + slot) so a landed push is neither rolled back nor deadlocks promotion; "add the missing case to the cleanup path" past that point manufactures divergence — ask of every catch that restores/reverts which side effects may already have landed above it.
- **A side effect before a transaction needs compensation, or the transaction comes FIRST** — ask BOTH directions; "the side effect succeeds and the tx fails" is the one that gets skipped (reopen attached a GC'd worktree before its tx; a throw rolled back `removed_at` while the worktree stayed attached, and no sweep converges that shape); the compensating `removeWorktree` on the throw path is best-effort and RETHROWS the original error.
- **Rebase INSIDE the source worktree, then fast-forward the parent target** — never check out a branch already owned by a linked worktree from the parent checkout; reject a stale `Maister-Node` trailer when no active node metadata exists, including `git commit -C` reuse.
- **A git RANGE answers a QUESTION** — "what did the reviewer see" (`merge-base..branch`, review artifacts) ≠ "what did the operator add" (`claim_head_sha..branch`); record the second anchor at the ONE insert point both claim shapes share (`node_attempts.claim_head_sha` written by `claimTakeover`); a null head falls back to the old count (a wrong zero would refuse a return that carried work).
- **Bootstrap-then-compensate:** a generated file (`maister.yaml` at registration) is removed on failure ONLY when byte-for-byte unchanged; an existing invalid manifest is never replaced.
**Source (additions)**: 2026-07-15-18.10, 2026-07-16-11.20, 2026-07-17-00.00, 2026-07-12-23.49-observatory-agentization-review-fixes, 2026-09-01-05.40, 2026-07-14-14.31
### Validate the wire, not the schema — fail-closed on present-but-invalid
**Source**: 2026-06-04-17.50, 2026-06-08-13.13
**Rule**: A zod range (`z.number().min(0).max(1)`) on the producer/config side does NOT hold for an untrusted producer (agent stdout `{"confidence":2}`, `Infinity`, an external report, a request body). Re-check the domain at every untrusted sink. **Out-of-range ≠ missing**: range-guard INSIDE the present branch (`!Number.isFinite(c) || c < 0 || c > 1` → fail-closed with a distinct outcome like `invalid_confidence`) so a `allow_missing_*` branch can never rescue an out-of-range value (it fail-OPENED a promotion gate). Every `typeof x === "number"` before a comparison that gates promotion needs `Number.isFinite` + the domain bound in the SAME expression. Server-authoritative fields (`readinessStatus`, ids, timestamps) stay out of request Zod schemas; use `z.string().trim().min(1)` to reject whitespace-only; trust-boundary wire responses get optional-chained access (`!models?.availableModels?.length`) even when the SDK type says the field is required — a type is not validation, and a guard protecting a no-throw contract belongs INSIDE the `try` it guards.
Parse-boundary extensions: `await req.json()` runs as an explicit step BEFORE Zod so malformed JSON maps to the documented 422 `CONFIG` family, never an unhandled throw; a field consumed by a service exists in EVERY parse gate on the request path (route AND ext route — `z.object()` silently strips unknown keys; `raiseTo` reached the service as `Number(undefined)=NaN`), pinned by a test through the actual `bodySchema.parse` (service-level tests constructing the input object bypass the transport contract); never accept-and-drop a body field — remove it from schema + OpenAPI instead (`abandon.reason`).
**Source (additions)**: 2026-07-04-22.53-experiment-create-flow-advisory-json, 2026-06-22-13.05, 2026-07-04-00.47-experiment-comparison-review-fixes
### Centralize authored / flow-package validation at every entrypoint; check original path bytes
**Source**: 2026-06-08-12.02, 2026-06-08-12.15, 2026-06-08-13.05, 2026-06-08-13.59, 2026-06-08-14.41
**Rule**: Validation belongs at the shared authored-package contract, not the newest UI boundary. Wire ONE `assertPublishableAuthoredFlowRevision` into UI publish, the catalog API publish route, AND the install bridge (which must validate the authored directory before invoking the generic Flow installer). For path validators: reject `..` in the ORIGINAL slash/backslash segments BEFORE normalization (`schemas/../setup.sh` collapses to `setup.sh`); test both normalized and original forms and both POSIX `../` and Windows `..\`; every `stat`→`readFile` sink needs an `isFile()` check (a directory passes realpath+stat then throws `EISDIR` → 500). Read package bytes as bytes, then decode with FATAL UTF-8 (`readFile(..,"utf8")` silently replaces invalid bytes); reject non-regular entries (a symlinked `setup.sh` enters the cache as untrusted bytes); reject ancestor/descendant path collisions, not just exact duplicates. Canonicalize a source dir to an ABSOLUTE path before the installer boundary (the installer classifies only absolute paths / `file://` as local → a relative dir is misclassified as a git source). Keep display label separate from filesystem slug (don't overload `flow.yaml.name`). Hidden form fields (`capabilitySlug`) and body fields are locators/concurrency tokens, never identity — load the server-owned value after authorization. Repo-local precedence over a bundled multi-file artifact (a skill dir) is PER-ENTRY, not per-file (`cp force:false` merging produced a Frankenstein skill of repo `SKILL.md` + bundle `references/*`).
Path/identity extensions: confinement realpaths the FINAL resolved leaf when it exists AND the nearest EXISTING ancestor for not-yet-existing paths (`mkdir -p`/git follow symlinks in ANY existing segment; a lexical fallback for a missing parent is always unsafe); enumerations use `lstat` and skip symlink leaves; `cp`/fork/export filters reject symlinks per source. Internal runtime dirs (`.git`, `.maister` path segments) are PRUNED before traversal, not only rejected at read time (a nested `codex-home/.tmp/plugins-clone-*/.git/config` reached the guard and crashed the page). Derive an identifier from the SAME source its downstream contract checks, using the SAME loader (installer requires `id === flow.yaml.name` → registration parses the copied `flow.yaml` via `loadFlowManifest`, never the directory basename). A rename of an invariant-coupled identity rewrites ALL coupled files in one atomic draft op (manifest entry + directory + moved `flow.yaml.name`), using `parseDocument` (preserves comments + key order) and failing `CONFIG` on unparseable input rather than shipping a stale name.
**Source (additions)**: 2026-06-21-01.26, 2026-06-20-21.18-m36-flow-package-viewer, 2026-06-30-13.04, 2026-06-24-18.55, 2026-06-29-01.03
2026-07..09 additions: package-root schema references are enforced on direct AND package installs (every install entry point) and reused schema caches are repaired from FRESH source bytes; a schema-migration contract distinguishes an ABSENT source artifact set from a PRESENT canonical set (absent root `schemas/` ≠ empty canonical set — every member schema was rejected as a collision and legacy packages became un-upgradeable) with tests for both branches; Studio and package lifecycle gates enforce the SAME capability-profile / agent-definition / schema contracts as runtime. Package form-schema docs are JSON at package-root `schemas/<name>.json` (the cut validator hard-blocks YAML and non-root refs); field `type` ∈ `string|number|boolean|enum|array|object`.
**Source (additions)**: 2026-07-12-10.39, 2026-07-14-14.00-legacy-package-schema-compat, 2026-07-11-13.34, 2026-07-26-12.32
### Flow rework-target prompts read the top-level commentsVar; strict templating throws into the run
**Source**: 2026-06-09-19.23
**Rule**: A rework-target node prompt MUST read reviewer feedback via the TOP-LEVEL `{{ <commentsVar> }}`, NEVER `{{ steps.<reviewNode>.vars.<commentsVar> }}` — `markNodeReworked` persists status/decision only (no vars); comments are injected as a top-level context var named by the node's `commentsVar` (`pendingInjectedVars`). `renderStrict` THROWS `MaisterError("CONFIG", "undefined template var: …")` on a missing deep leaf → node Failed → run Failed. When a node is reachable on BOTH its initial visit AND a rework jump (entry == rework target), seed EVERY declared `commentsVar` to `""` once per run (`collectDeclaredCommentsVars`, overlaid by real comments on rework) — `pendingInjectedVars` is set only on a rework jump, so a naive `{{ review_comments }}` append swaps a rework crash for an ENTRY crash. A new authored flow needs a RUNTIME test rendering its rework prompts through real `buildContext` + `renderStrict` (RED before, GREEN after), not just a manifest-load test.
### No write may be sequenced after a terminal status flip; required accountability writes share the success tx
**Source**: 2026-06-10-23.44, 2026-06-08-21.13, 2026-06-08-21.45, 2026-06-08-22.26
**Rule**: A terminal row is never re-executed, so an `UPDATE … SET outputArtifactRef` AFTER `markGatePassed`/`markGateFailed` leaves a crash-window where the terminal gate permanently lacks its back-ref. Write every outcome field (back-refs, evidence links, payload columns) in the SAME UPDATE as the terminal transition, or strictly BEFORE it — record the report while the row is still `running` and rely on the re-execution-of-non-terminal-rows recovery path. Required accountability/audit writes are part of the success commit: never wrap `recordTokenAudit()` in `.catch(() => {})` — use `recordRequiredTokenAudit()` that logs `[FIX:token-audit-required]` and RETHROWS (fail-closed), and write the success audit in the SAME transaction as the durable success marker (task create/update, run launch inside the launch tx so audit failure triggers worktree compensation, HITL respond in the delivered-marker tx, idle resume `NeedsInputIdle → NeedsInput` with the 202 audit). A mutating `/api/v1/ext/*` route needs `successAuditInWork: true` — `handleExt` writes the default success audit AFTER `work()`, safe only for read-only routes. Close active `open`/`claimed` assignments from EVERY terminal lifecycle path (promote, discard/abandon, crash, reconcile, keepalive/HITL timeout) through a system actor, in the same status-guarded transaction.
Extensions: a durable outbox row (`emitDomainEvent`/`emitWebhookEvent`) is a persistent write and belongs in the SAME tx as the state change (ADR-086 exactly-once) — never the "notify after commit" bucket (that is for `scheduleResume`-style in-process wakes only); if a sibling terminal path emits in-tx (`run.failed`), the new path (`run.escalated`) must too. A give-up/compensating write re-asserts the state it compensates FOR under its CAS (`holdAndComment` gained `status='Review'` and reports whether it landed — count the give-up metric only then; otherwise classify as a benign lost race), or it corrupts a concurrent winner's terminal record. A safety gate keyed on `requiredFor:[review]` evidence fires on EVERY terminal route to Review — a `!isRework && terminal` predicate silently excluded the new `onExhaustion` branch; encode the invariant as a property of REACHING terminal, not of one branch. Operator/human input files are deleted only AFTER downstream durable side-effects commit; artifact rows and their semantic ledger rows share ONE transaction.
**Source (additions)**: 2026-06-22-11.32, 2026-07-03-16.16, 2026-06-30-01.37, 2026-06-24-18.46
2026-07..09 additions: when a status flip carries a "MUST emit" invariant, the emit lives in ONE helper called from EVERY writer of that status (`emitDelegatedReviewIfChild(tx,row)`; `casToReviewAndEmit` for the `state-transitions` Review re-entries) — inlined at one edge, operator stop / sync return / rework release never woke the parked parent and the tree was cascade-abandoned; grep `status: "Review"` for writers. A payload consumed by a DECISION-MAKING consumer carries a discriminant for the CAUSE, not just the fact — a REQUIRED `cause` on `ReviewFlipRow` (`RUN_REVIEW_CAUSES` = `graph_completed|agent_exit|operator_stop|rework_released|sync_returned`) so auto-promotion admits only completion causes and fails closed on a missing one (an operator stop had auto-merged a half-done diff). Non-final response effects (child stamp, assignment completion, audit, REDACTED outbox row) share the response-marker tx and the outbox emit is CONDITIONAL on a newly-stamped row so a same-payload retry cannot emit twice; a successor-created supersession lives in the successor's insert tx (transaction-scoped helper); dependent rows + the keyed record that can reject them are created in ONE tx (nested savepoints) so a rejection leaves no orphans. Never write a TERMINAL idempotency marker for a transition that did not land — on a lost CAS the marker belongs to the WINNER (`notified.tree="terminate"` stamped on the lost-CAS path permanently disabled tree enforcement, since nothing outranks `terminate`). Before any hard DELETE list the inbound `ON DELETE CASCADE` FKs — `tasks` cascades from `runs` AND `domain_events`, so the terminal exit is `Abandoned` with the no-run guard INSIDE the UPDATE (`NOT EXISTS runs`). A function whose result says "what you must do next" (`cascadedRunIds`) ships the COMPOSITION (`cascadeAbandonRunTreeAndStopSessions`) to all callers, and the crash window between its halves gets a sweep (reap a live session under an `Abandoned` row, allow-listed to that one status).
**Source (additions)**: 2026-09-02-03.51, 2026-09-02-12.10, 2026-07-14-22.50-plan-review-review-fixes, 2026-07-14-human-ask-review-remediation, 2026-07-25-12.09, 2026-09-03-17.21
### Enforcement acts on what the run DID; shared dispatch branches on run_kind
**Source**: 2026-06-13-16.55, 2026-06-16-22.45
**Rule**: Terminal/enforcement logic must act on the value SNAPSHOTTED on the run row at spawn, never re-derive it from a mutable catalog/projection that can drift after launch. `finalizeAgentRun` gated the L3 dirty-watchdog + ephemeral `-ro` cleanup on `agents.workspace` (the catalog INDEX of the package's NEWEST installed revision) while launch / L1 / L2 / cwd used the project's PINNED effective definition — a pinned-older `repo_read` run that dirtied the parent checkout was NOT quarantined and its checkout leaked. Persist the decisive field (`runs.agent_workspace`, migration `0052`) at the single launch insert from `ctx.effective.parsed.workspace`; gate `row.agentWorkspace ?? wsCtx?.workspace` (the `??` keeps legacy/seeded rows working). Any SHARED dispatch site that feeds a kind-specific mechanism (reconcile `classifyRunReconcile`, a sweep, a composed op switching on `run_kind`) MUST branch on `run_kind` BEFORE routing — a scratch dialog driven into the flow-only `RESUME_CONTINUATION_PROMPT` resume driver replies context-less and `Crashed`s; add a belt-and-suspenders guard at the impure apply site (`case "reattach"` refuses `driveResumed` for any non-`flow` run_kind) in addition to the pure classifier.
Extensions: validate against the SAME source the executor reads — write-time validation and run-time consumption must both read the pinned install's artifact (`resolveEffectiveAgentDefinition` on the pinned `maister-agents/<stem>.md`), never a mutable catalog projection re-keyed on resync; add a version-skew test (catalog-only key rejected, pinned-only key accepted). A decision gating a RETRYABLE side-effect derives from the durable policy snapshot, never a non-idempotent op's return an earlier attempt may have consumed (`forcePush` from `didSquash` went false on reclaim → non-fast-forward `CONFLICT`; extract a pure `squashesRunHistory(claim)` predicate used by BOTH the squash guard and the PR forcePush; ask "what does the SECOND attempt see?"). A recovery/self-heal path re-uses its happy path's DISPATCHER (`scheduleBudgetBreachResume({db, runId, runKind, stepId})`), never a hand-rolled subset (`scheduleResume` missed agent runs + `NeedsInputIdle`).
**Source (additions)**: 2026-06-25-19.58, 2026-06-20-19.48, 2026-06-24-14.22
2026-07..09 additions: a provenance record captures the OUTCOME, not the intent — when the seam cannot honor a pin, record what actually ran (read `runs.flowRevisionId` post-seam), never copy the declared value; when a later stage re-derives a value an earlier stage wrote into an "immutable" record, the LATER stage owns the record (the launcher overrides `flowRevisionId`/`resolvedRevision`/`engineMin`/`engineMax` from the revision it selected, exactly like the branch pair per D7 — check for SIBLINGS of a precedent). An entry point serving TWO lifecycles (`startAgentSession` = spawn AND resume via `resumeSessionId`) gets an explicit gate PER lifecycle — grep callers of any `start*` before assuming "first time" (memory was re-injected and `runs.agent_memory_hash` re-stamped on every resume).
**Source (additions)**: 2026-07-25-11.14, 2026-09-02-12.10, 2026-07-27-20.55
### Degrade with a structured flag, never an in-band marker; gate irreversible actions on incomplete evidence
**Source**: 2026-06-09-13.01, 2026-06-15-12.31
**Rule**: When a reader truncates/caps/samples instead of failing, the result MUST carry a structured boolean/enum the typed consumer branches on — thread `{ text, truncated }` end-to-end (`DiffPrepResult` → `RunDiffResponse` + OpenAPI → `ReviewPanelDiff` → `DiffView`), NEVER append a magic marker string to the payload (a swallowed marker left Promote enabled on a partial diff). Two duplicated readers of the same thing MUST agree on the degradation mode (`diffRange` truncated while `diffRunWorkspace` threw 409 — share one `streamGitDiffTruncated`). Any "you can't fully see X" state at a promote/merge/approve gate MUST gate the action (block, or block + explicit "Promote anyway (diff truncated)" override), never just annotate. Filter runtime-materialized files (flow `.claude/skills/**` bundle paths) BEFORE computing user-facing review counts or building a heavyweight diff payload, with a summary-only fallback when full bundle prep throws `Invalid string length`.
Extensions: "degrade, never 500" needs BOTH halves — every loader catch that degrades to `[]`/`null` also `log.warn`s with `runId` (+ `currentStepId`) context, or a broken read is indistinguishable from a healthy empty one. A decorative/optional enrichment read (an ambient enable-check) sits INSIDE its own try (+ a short negative cache) so a transient DB error degrades to no-enrichment instead of crashing opted-in runs. When a best-effort secondary source fails AFTER the primary succeeded (git file metadata after the text diff), preserve the primary and log a structured fallback — never one all-or-nothing `Promise.all`. Preview/display DTOs that catch resolver errors carry provenance distinguishing a resolvable implicit default from an unresolved DECLARED contract (`selectedRunnerId: null`, no synthetic entry) — a display fallback never crosses the request boundary to become a real launch override; non-fatal preview catches log structured task/project/flow/session context + the normalized error code (no provider credentials).
**Source (additions)**: 2026-06-19-00.02, 2026-07-02-17.52, 2026-07-04-13.37-experiment-comparison-review-fix-followup, 2026-07-07-11.08
### i18n: every key needs a render site; $count not {count} for client templates; EN+RU parity
**Source**: 2026-06-11-12.25, 2026-06-07-19.53, 2026-06-10-20.48, 2026-06-13-20.02, 2026-06-05-11.29, 2026-06-01-15.31
**Rule**: A new i18n key is done only when something CONSUMES it — parity tests check key PRESENCE, not wiring (server-hardcoded EN strings shipped while EN/RU `modelSuggestions.sources.*` keys sat unused); grep the key's usage before calling an i18n task complete, and the i18n key-coverage test must include every key the component actually uses. Use `$count`, not `{count}`, for strings loaded as client-side templates — `intl-messageformat` treats `{count}` as an ICU variable and THROWS when no value is supplied. Localize status/enum labels (never render the raw enum); route API errors through a client-safe `readApiError` + a shared `apiErrors` (EN+RU) namespace that keeps server detail behind a translated label, not a copied untranslated-English fallback. Prune orphan keys when their UI is deleted (line-based, JSON-validated, EN/RU parity preserved); use a local theme token (`danger`/`danger-soft`), not a bare framework `danger` class with no matching token. Route every new UI string through i18n EN+RU. Format token/usage/cost totals with `Intl.NumberFormat(locale)` from the active `next-intl` locale (never fixed `en-US` or raw integers), with RU-grouped regression coverage in shared formatters. Give a new key distinct copy from its neighbors or alias it explicitly — a byte-identical duplicate EN value while RU differs is a latent bug.
**Source (additions)**: 2026-06-30-14.45, 2026-06-30-13.02
2026-07..09 additions: Server Component label DTOs stay DATA-ONLY (a localized prefix; the client composes ICU/timestamps) — a function crossing the RSC→client boundary is illegal; delayed ICU templates are retrieved with `t.raw()` and the RECEIVING component's interpolation boundary is tested (passing a template through `t()` before the component that supplies its values fails at runtime). Shared notice labels describe the actual event class ("call denied, continuing", not the path-guard-centric "write denied") EN+RU.
**Source (additions)**: 2026-07-12-22.35-observatory-agentization-final-hardening, 2026-07-16-22.51-canonical-create-flow-verification, 2026-07-12-10.43
### ACP resume uses session/resume and fails loud; CLI-native providers need no host API key
**Source**: 2026-06-08-13.13, 2026-06-11-17.19, 2026-06-11-23.39
**Rule**: Resume MUST use the ACP `session/resume` protocol call; if the adapter does not advertise `sessionCapabilities.resume`, fail loud (`throw new SupervisorError("CHECKPOINT", …)`) — NEVER silently fall back to `session/new`, which creates an EMPTY session and orphans the conversation (the original resume-bug class). CLI-native providers (`anthropic`, `openai`, `google_gemini`, `agent_native`) default to the tool's OWN configured auth — a missing `GEMINI_API_KEY` / `GOOGLE_API_KEY` is NOT a smoke blocker; optional env refs are valid only as explicit operator overrides / compatible-provider routing / gateway / sidecar config. Prove a new adapter's readiness with binary diagnostics + cached SDK smoke (`pnpm -C supervisor smoke:acp --cache <path> <agent>`), not host API-key vars, and its fixture must cover the FULL shared lifecycle (new session, prompt permission round-trip, model advisory, resume/checkpoint), not just registration + resume at the enum level.
### ACP/supervisor calls that wait on agent work bypass undici's default timeouts
**Source**: 2026-06-26-14.42
**Rule**: Node's global `fetch` inherits undici's ~5-minute headers/body timeouts. Any web→supervisor call that intentionally waits on agent work (`sendPrompt` — response headers not sent until the ACP turn completes) or a quiet SSE stream (`streamSession`) must go through package-`undici`'s `fetch` with a dedicated `Agent({ headersTimeout: 0, bodyTimeout: 0 })` dispatcher. Never `setGlobalDispatcher` (built-in fetch and package-undici dispatchers can be incompatible); ordinary calls stay on global fetch defaults. Fixed ceilings belong to explicit watchdogs/budgets/run time-limit policy, never the transport — a transport timeout converts a LIVE session into `EXECUTOR_UNAVAILABLE` and tears down a healthy supervisor session. Regression tests assert the transport choice (mock package `undici.fetch`, assert the dispatcher reaches `sendPrompt`/`streamSession` while ordinary calls keep `globalThis.fetch`), not a 5-minute sleep.
### ACP usage/cost parsing: accept both key casings, dedup per turn by position
**Source**: 2026-06-29-20.55-cost-camelcase-usage, 2026-06-29-22.16
**Rule**: Usage/telemetry parsers accept snake_case AND camelCase adapter shapes per field with snake-wins fallback (`input_tokens ?? inputTokens`, `output_tokens ?? outputTokens`, `cache_read_input_tokens ?? cachedReadTokens`, `cache_creation_input_tokens ?? cachedWriteTokens`) — the adapter's end-turn `result.usage` is camelCase; a snake-only reader hits the all-undefined guard and silently drops the record. "Never count both / per-turn" is enforced across the EMISSION boundary, stateful per session: classify usage by POSITION (`source:"result"` when under the top-level JSON-RPC `result`, else `"stream"`) and collapse per turn (`createTurnUsageRecorder`: a `result` usage writes immediately and discards buffered same-turn `stream` usage; a `stream` usage buffers and flushes at session close only if no `result` superseded; flush on detach) — a per-field guard inside one object is not dedup. Before trusting any shape assumption, replay the REAL on-disk `.maister/<slug>/runs/<id>/<step>.log` JSON-RPC lines; synthetic one-record-per-node fixtures never exercise a turn emitting both nested and end-turn usage.
### Persistent ACP chat sessions ground once, then send minimal follow-ups
**Source**: 2026-06-28-19.44
**Rule**: A session that retains history gets its heavy grounding block (DSL grammar + editing contract + file inventory + full YAML/graph dump, ~28k tokens) exactly ONCE, on the first turn. Follow-ups send just the user's text plus a one-line delta hint for anything that can change since launch (`(editor focus: …)`), trusting session memory — the action-JSON contract + grammar remain in session history from turn 1. Re-injecting the context each turn wastes tokens AND makes the model re-anchor, re-emitting its first answer verbatim while ignoring the short follow-up.
### Capability materialization is adapter-surface driven; watchdog manifests track every generated path class
**Source**: 2026-06-29-22.01, 2026-07-07-18.55, 2026-07-07-18.04-review-blockers
**Rule**: `skills` and `subagents` are SEPARATE adapter surfaces, never collapsed into one mode: Claude cwd-dir materialization copies both `skills/` and `agents/` (native `.claude/agents/` gets capability-local subagents from the same pinned package roots); Gemini stays skills-only into `<worktree>/.gemini/skills` with NO redirect env (per-run `GEMINI_CLI_HOME` detaches `gemini --acp` from the user/workspace settings merge) and NO forced ACP `authenticate` when the runner has no `apiKeyEnv` (`session/new` reads `~/.gemini/settings.json` natively; forcing `oauth-personal` can open an interactive browser login); home-redirect adapters keep their existing skill homes. Gate adapter behavior on an explicit capability descriptor (`readOnlySessionSmoke`), never a private adapter-name set. When changing the shared materializer, check EVERY caller category (standalone package-agent, scratch, flow authoring, flow-node binding). The dirty-watchdog ownership manifest tracks EVERY generated path class (copied `.claude/agents/...` too); restore logic never deletes user-owned same-name files; tests cover generated + user-owned entries per path class. Never cache `ok` operational evidence for behavior the code path did not actually exercise — report `skipped` until a dedicated probe exists, and diagnostics ignore `ok` evidence when the generic adapter smoke itself was skipped/failed.
2026-07..09 additions: see the filesystem-ownership rule for markers-before-content, foreign-lease preservation, lock-directory staleness, and rename-aware L3 porcelain filtering — all four were re-found on the agent-L2 settings and materialization release paths.
**Source (additions)**: 2026-07-12-10.39, 2026-07-12-11.21
### React state & layout for long agent output
**Source**: 2026-06-29-01.37, 2026-06-28-19.20, 2026-06-23-14.56, 2026-06-30-14.12, 2026-06-23-23.03, 2026-06-24-19.45
**Rule**: A value that gates conditional RENDERING (`disabled`, a className, a branch) lives in `useState`, never a `useRef` — a ref set in `onDragStart` and read during render is a silent no-re-render bug (browsers suppress `onDrop` on disabled targets; pair with `onDragEnd` cleanup); reserve refs for focus/measurement/event-only reads. A drawer that must preserve live state (running ACP session, unsent draft) stays MOUNTED and toggles via `hidden` — never conditional unmount; never tear down a React Flow `createPortal` target; when two surfaces share one slot, define precedence explicitly and bubble the flipping trigger (node-select → `aiOpen=false`). ReactFlow `nodeTypes`/`edgeTypes` must be referentially stable — per-node render state goes through `node.data` (grep changed `useMemo<NodeTypes>` deps for per-interaction values). Any grid column containing logs/commands/transcripts/diffs uses `minmax(0,1fr)` (bare `1fr` keeps an auto minimum that long unbroken content inflates past the shell); intermediate flex/grid children need `min-w-0` (+ `max-w-full`/local `overflow-hidden`) or inner `truncate`/`overflow-auto` cannot engage. A height-capped grid modal needs the ROW track bounded (`grid-rows-[minmax(0,1fr)]`, `lg:`-scoped) for child `overflow-y-auto` to engage — `min-h-0` on the item is necessary but NOT sufficient. Two regions with tall fixed chrome never share one bounded vertical axis (graph = full column, chat = right drawer `w-[clamp(420px,34vw,560px)]`). Measure the actual DOM (`getBoundingClientRect`, `scrollHeight`/`clientHeight`) before assuming a className idiom transfers between call sites; scroll-regression e2e pins a SHORT viewport (1280×600) and asserts `scrollHeight > clientHeight`. Reuse existing `--cv-*` tokens (new `globals.css` tokens hit the stale-Turbopack-CSS trap). A `cond && labels.x ? <Right/> : <Wrong/>` dispatch whose wrong branch corrupts data is made unreachable and asserted (`labels.mcp!` + WHY comment), never left to fall through.
2026-07..09 additions: a server action that can throw a domain `CONFLICT` needs a SEGMENT-level `error.tsx` (`app/(app)/flows/[projectSlug]/[capId]/error.tsx`, mirroring the studio boundary) or the whole app unwinds to the root boundary and takes the unsaved buffer with it — same-user multi-tab lock steal is a STEADY-STATE trigger, so prefer an in-place `retry` affordance over auto-reacquire (ping-pong). A hook that exports a GUARD (`useEditorLock().confirmed`) is checked at EVERY consumer — when adding a second consumer, diff its destructuring against the first (`effectiveCanManage = canManage && heldByMe && confirmed`). Every fire-and-forget action shows its result: a button that POSTs then only `router.refresh()`es is silent on a 200-with-failure-body — read the body regardless of status, branch on `res.ok && payload.ok`, render inline Testing… → ✓ latency / ✗ reason. A component consuming an enum from TWO producers (hub DTO `platform|project|package` vs raw `capability_records` `platform|project|flow-package`) enumerates BOTH token sets in every ordering/lookup table. Each screen's EventSource is a single-owner resource (`RunStreamProvider`); child consumers receive context. Run detail exposes ALL unresolved requests (count shown, focus restored after an answer) and orders a parent AHEAD of its decision children wherever the parent owns the only full HITL surface. Two UI surfaces invoking the same irreversible operation SHARE its eligibility + payload builder (header promotion vs inspector). When two service methods gate on the same row, report the DOMINANT invariant (ARCHIVED immutability) before the weaker (lock), each with a distinct discriminator.
**Source (additions)**: 2026-07-22-01.14, 2026-07-21-23.50, 2026-07-12-22.40, 2026-07-13-11.21, 2026-07-14T19-18-32-plan-review-handoff-recovery, 2026-07-14-22.10
### Form/serialize normalization, tri-state controls, sparse payloads
**Source**: 2026-06-26-12.17, 2026-06-26-12.43, 2026-06-26-03.12, 2026-07-01-11.45, 2026-07-01-12.55, 2026-07-07-11.08, 2026-06-24-19.45
**Rule**: Normalize collection inputs at the SERIALIZE boundary (the single canvas→YAML conversion point), never per-keystroke — per-keystroke filtering erases the transient blank row the author just added; prune the serialized output AND the in-memory ref together so `parse(yaml) === canvasManifestRef` holds. When replacing an input control, diff the OLD control's onChange for implicit normalization (`value.split(",").map(s=>s.trim()).filter(Boolean)`-style split/trim/filter/dedupe) and re-establish it explicitly. A normalizer/sanitizer walking a `.passthrough()` schema operates on a KNOWN-key allowlist (`PRUNED_LIST_FIELDS.has(key)`), never a structural predicate — tested `x-*` extension fields are the tell that a blanket transform will corrupt author data. A setting whose server contract has an inherit/null state gets a tri-state Inherit/On/Off control showing the env-resolved effective default — never a boolean checkbox that silently invents an override; save builds the override from explicitly-set keys only and PATCHes `null` to clear. Client request builders emit SPARSE objects (omit empty optional fields entirely; no `undefined`-valued keys). Web settings controls use HeroUI (`Select`/`Input`/`Button`) with dirty-state Save gating and a green-check success glyph — never hand-rolled native elements. Editor "blank node" scaffolds satisfy the schema's min-constraints (`prompt: "TODO"` for `min(1)`, required `settings.form_schema`) — verify brief-suggested literals against the schema. React state seeded from props needs an interaction test that rerenders with changed props BEFORE the consuming action (stale content otherwise writes into the newly-selected item).
### Bounded background sweeps must guarantee progress and isolate poison items
**Source**: 2026-06-29-17.25, 2026-07-03-14.58, 2026-07-03-16.16, 2026-07-02-17.52, 2026-06-22-13.05
**Rule**: A capped (`LIMIT`ed) periodic scan over rows that can stay ineligible indefinitely needs BOTH: (a) a durable per-item attempt marker stamped in a `finally` on EVERY attempt (`runs.cost_reconciled_at`: reconciled / missing-cost / error) — never key candidacy on the OUTPUT artifact's row state, or unprocessable items monopolize the scan forever; and (b) where eligibility is external ("Review + auto-promo enabled"), a durable rotating keyset cursor (deterministic `ORDER BY coalesce(ts,'infinity'), id`; persisted in the singleton job's `scheduler_jobs.target` jsonb; reset at a short tail) so every candidate is visited within `ceil(N/LIMIT)` ticks. Per-row work inside a shared singleton job is crash-proof: one malformed row must not fail the tick (after `max_failures` the platform-wide job is disabled) — per-candidate load/compile errors fail closed to empty-set + WARN. Classify deterministic provider 4xx as PERMANENT per-item failure (`CONFIG`; item marked `failed`, error recorded in `resumable_cursor`, sweep continues) vs transient (timeout/429/5xx/network/malformed-200 → bounded retry, `EMBEDDING_UNAVAILABLE`); skip disabled projects; reconcile-enqueue on settings save AND re-enable. Every outbound `fetch` in a sweep/request path carries an explicit `signal: AbortSignal.timeout(ms)` — a stalled provider otherwise never triggers the retry branch. `setInterval` sweepers carry a re-entrancy guard (`running` flag); a lost-race cleanup never unlinks resources the winner depends on (`needs-input.json`).
2026-07..09 additions: compare a batch's WORST CASE against the lease that contains it (`batch × per-call budget vs lease`: 50 × 60 s vs 300 s → LEASE_EXPIRED + a replacement scan writing concurrently) — derive the deadline from the container's real value (`schedulerAttemptTimeoutSeconds()`) reserving one worst-case call plus margin, stop STARTING candidates at the budget, and resume the cursor from the last one actually processed. A lease around a LONG external operation (`launchRun`: fetch + worktree + materialization) needs BOTH renewal (fenced, non-overlapping 1-minute claim heartbeat that renews the EXPIRY, not only the observability timestamp) AND a durable fence at the irreversible persistence boundary (locked ownership assertion before worktree creation and inside the Run insert tx); a renewable lease is tested for long-running renewal AND stale completion after expiry (`system_sweep` outlived its lease and persisted a stale result). Acquire each recovery lease at PROCESSING time, not scan time, so later rows keep their full window; pass a FRESH clock through reconciliation mutations and PRESERVE a finding whose claimed lease is no longer valid. No FAILED READ may write a terminal state — providers answer permission-denied with the same 404 as a deleted PR, so one rotated `GH_TOKEN` permanently marked every live PR `closed`; 404 is retryable, and an attempt-counter would recreate the class (the keyset cursor already guarantees progress). Eligibility predicates go INSIDE the bounded query's WHERE before `LIMIT`, not in a post-query validator (historical rows consumed the limit). Service-level bundle failure and per-candidate retry are DISTINCT observable outcomes (`SYSTEM_SWEEP_FAILED` attempt + candidate retries) — a bundle failure had reported success.
**Source (additions)**: 2026-07-15-18.10, 2026-07-16-13.06, 2026-07-16-22.26-workspace-reconciliation-review-fixes, 2026-07-17-00.10-workspace-lifecycle-review-remediation, 2026-07-15-11.19-gate-chat-recovery, 2026-07-16-16.05, 2026-07-14-22.10
### Unattended launchers: bounded, intent-scoped, semantics re-derived per caller
**Source**: 2026-06-25-17.42, 2026-06-25-19.13, 2026-06-25-19.58
**Rule**: An automated/level-triggered launcher bounds retries (attempt cap, e.g. `MAX_AUTO_LAUNCH_ATTEMPTS = 3`, + exponential backoff keyed on the last failed run's `endedAt`) and never reuses a classifier whose "launchable" verdict was designed for human retry without re-deriving what each terminal state means for THIS caller (`Failed`-latest = "offer manual relaunch" to a human, "back off / stop" to a timer; `Crashed` is held for human recover — never auto-relaunched). An attempt cap is SCOPED to the intent it bounds: stamp `armed_at` when arming, count `started_at >= launch_armed_at` (null → count all, legacy-safe), and pin `armed_at` into the give-up CAS — bare `count(*)` over all failure history conflates intents and makes a deliberate re-arm unrecoverable. A consumed intent column (`launch_mode='auto'`) has an explicit lifecycle — cleared on consumption (one-shot) or bounded by a cap (level-triggered) — with ONE authoritative writer semantics across ALL write paths (grep every writer when adding a consumer; one path clearing while siblings leave-stale is the tell). Retry/give-up loops classify the FULL non-retryable set explicitly per `MaisterError.code`: `CONFIG` is non-retryable alongside `PRECONDITION`; `EXECUTOR_UNAVAILABLE`/`CONFLICT` stay transient; a `default → transient` fallback in a no-silent-stall loop is a latent infinite retry. Selection→act windows spanning async I/O get a CAS over the full selected identity tuple (`triage_status='triaged' AND launch_mode='auto' AND flow_id=<candidate>`) + `.returning()` row-count check BEFORE any side-effect — a singleton lease only guards tick-vs-tick, never tick-vs-route. When a system action posts a comment that emits a trigger event the agent subscribes to (`task.comment_added`), the agent prompt no-ops on the resulting state (`flagged` → human-owned) to prevent comment/trigger storms.
### Column DROP / table re-key is a whole-tree sweep with data preservation
**Source**: 2026-06-27-14.28, 2026-06-27-16.03, 2026-06-27-17.08
**Rule**: Dropping/moving a column sweeps every reader AND writer in ALL forms — the camelCase accessor (`runs.capabilityAgent`) AND raw snake_case SQL incl. aliases (`\b(r|runs)\.(capability_agent|…)\b`, `INSERT INTO runs`, `SELECT … FROM runs`) — raw `sql`-template readers are invisible to symbol refactors and schema-accessor greps; `tsc` is blind, Drizzle silently drops unknown INSERT keys, and a stale-keys-only `.set({})` reduces to an empty SET → Postgres "syntax error at or near WHERE" that crashes AFTER the session spawns (leaking it). Sweep test seeds across ALL harnesses — integration + e2e `seed-e2e.ts` + per-spec files (e2e is not CI-gated, so grep-to-zero over `e2e/`, don't rely on a green suite). A migration DROPping/re-keying LIVE state either backfills (`INSERT INTO … SELECT … ON CONFLICT DO NOTHING` BEFORE the drops) or abort-guards (`RAISE EXCEPTION` if non-empty) — never silently drops; when the new key is not SQL-derivable from the old, the loud guard is the honest fix. When moving per-row state to a side table, use the merge-at-load idiom (merge the active session's fields once via `loadActiveRunSession` at each read site; ONE persist helper — `persistRunSessionAcpSessionId` — for writes). A migration's abort message is an operator CONTRACT: instructions must be order-correct against the failure path it fires in ("export/record → clear → re-run migrations → re-map via UI after upgrade" — never "do X after upgrade" when the guard blocks the upgrade).
### Slot-freed states, claim counting, and resume dispatch per lifecycle
**Source**: 2026-07-01-12.55, 2026-07-01-11.45, 2026-06-27-23.45
**Rule**: `NeedsInputIdle`, `WaitingOnChildren`, and `Review` are slot-FREED states (excluded from `countLiveRuns`); ANY transition from them back to `Running`/`NeedsInput` reclaims a slot and is cap-gated (under the scheduler advisory lock: at cap, stamp `resume_requested_at` and return `"queued"` for the admission gate to pick up) or consciously exempted with a recorded reason; direct `NeedsInput` flips are slot-neutral. Capacity guards gating two-phase claims count in-flight claim markers (`tasks.queue_claimed_at NOT NULL`) in addition to committed occupancy rows — `liveCount + outstandingClaims` (also in the poll baseline) so serialized admitters see prior claims as consumed capacity. Sibling concurrency counters apply the SAME predicate — grep ALL cap-count sites in one pass when changing pool membership (`isNull(runs.localPackageId)` had three flow-pool counters). Before reusing a resume/recover dispatcher across lifecycles, grep the downstream driver for `eq(runs.status, …)` guards: a flow HITL resume keeps the run `NeedsInput` (its completion transitions are `NeedsInput`-guarded) unlike agent/crash recovery's `→Running` flip — a mismatched claim status fails the completion CAS and strands the run holding a slot. A terminal/`worktree-gone` reconcile check that precedes the live-session skip must resolve `worktreeExists=true` for runs that legitimately have no worktree (agent `none`/`repo_read`, project-less assistant), keyed on the precise discriminant (`cand.projectId == null`), or a LIVE session gets crashed.
2026-07..09 additions: a cancel/stop tool on a child frees whatever the ADMISSION COUNTER counts — check the cap predicate against the tool's resulting status (`run_cancel` parked a flow child in `Review`, which counts as LIVE for the fan-out cap → `markAbandoned` with slot release). A response answered after the keep-alive sweeper idled the run cannot use bare `scheduleResume` (it drives `runFlow`, which claims only `NeedsInput` → `NeedsInputIdle` stuck forever with `respondedAt` stamped); use `claimGraphResumeSlot`. `Running`-only allow-lists hide orphans: a choke point's CAS SOURCE SET is checked against every caller's need (`finalizeAgentRun`'s `Crashed` CAS admitted only `Running|NeedsInput` and refused the `NeedsInputIdle`/`Review` orphans the classifier now routes to it; `crashRunningRun` takes `fromStatuses` and reconcile passes the status it CLASSIFIED the run in so a moved run loses the CAS instead of being clobbered).
**Source (additions)**: 2026-09-02-03.51, 2026-09-01-04.15, 2026-09-03-21.47
### Fail-closed verdict routing and enum honesty
**Source**: 2026-06-22-20.53, 2026-06-22-22.14, 2026-06-20-18.20, 2026-07-03-16.16, 2026-06-30-00.47, 2026-06-22-19.56
**Rule**: A producer FAILURE never shares a code path with "value produced but unmatched": the decide `default` branch serves only the latter — a suppressed/failed verdict gate must fail closed, tested with the empty/unparseable-producer case (exactly where fail-open hides). Put the no-verdict check on the DIRECT signal (`routedVerdict === undefined`) as a single post-loop invariant at the decision point — never behind a relaxable predicate like `isEffectivelyBlockingGate` (advisory mode / policy downgrades silently disarm it). A schema must not accept a value with no implementation: implement it or reject at the entry boundary with typed `CONFIG` (`assertExecutionPolicySupported`) — a dispatcher matching only implemented arms turns an unimplemented enum into a silent no-op; grep every consumer of the enum's resolver when adding a value. Don't ship contract states the producer can never emit (`not_declared` driven only via mocks) — implement or remove them. Never discard the return of a function that may pause/escalate — honor "did I actually pause?" or pass an explicit `forcePause` so a policy short-circuit (`humanGate=auto_pass`) cannot fire (a `NeedsInput` run with no HITL request is invisible and unresolvable). A statically-decidable misconfiguration is rejected at COMPILE/load time (`verifyReworkReset` refusing `transitions[onExhaustion] ∈ rework.allowedTargets` with `CONFIG`), with the runtime re-assert as defense in depth. A source selector (`decide:{from:…}`) verifies the selected producer exists at compile — exactly one verdict-producing gate (zero → silent default; >1 → last-wins), declared outputs for `output.<path>`; a compile guard requiring a parent block also requires the specific sub-field the runtime consumes (`rework.commentsVar` for `on_mismatch`). Adding stricter `compileManifest` validation is safe only after grepping every fixture that compiles a manifest (unit + integration + e2e seed) — compile runs on READ paths (run-detail, packages viewer, inbox, topology), so a non-compliant fixture turns a view into a throw.
2026-07..09 additions: fan a new enum value to ENCODE AND DECODE — after widening a type + producer, grep every `=== "<sibling-value>"` allow-list / parser (`parseScratchMessageContent`) and add the value (an unreachable miss becomes a silent drop later); widening a jsonb discriminated union (`delegation_spec` gained `kind:"flow"`) greps every reader for FIELD SNIFFING (`"agentDefinitionId" in`, `-> 'agentId' IS NULL` → `delegation_spec IS NULL`) and every DTO/UI read model that projects it. Status guards over `runs.status` are ALLOW-lists derived from an exhaustive `satisfies Record<RunStatus, boolean>` map shared by every consumer (`STALE_ASSIGNMENT_RUN_STATUSES`, `DRIVER_OWNED_RUN_STATUSES` — `Pending` included), never `NOT IN (...)`; when a predicate PARTITIONS a closed enum, derive the arms from the enum and make the consequential arm the allow-list (an unknown status reads `pending`, not `valid`). The one legitimate inversion: a RECOVERY arm over a phase set that grows on the producer's side derives the complement (`!terminal + mode`) instead of listing phases. A typed union with ZERO importers is decoration — either the column and every writer take it, or delete it; `Record<string, unknown>` on a payload type disables the compiler where you need it — NAME every field a consumer branches on (`parentRunId` on the admission ref); a real third state gets a discriminated-union MEMBER (`{kind: "agent-driven-flow"}` consent), never an `else` fallthrough into a branch written for another state. A comment asserting an invariant ("the scratch finalizer is never a tree root") is a MISSING GUARD — grep the candidate query; if nothing filters it, add the branch (the tree-terminate arm now mirrors the run-scope arm's agent/scratch/flow three-way — a dispatcher grown one arm at a time must give the NEWEST arm the sibling arms' discriminant branches). Exhaustive mappers (`haltRuleFromEvent`) live in a PURE module so a new member is a compile error and the mapper survives tests that mock the DB-heavy sibling. Promote a competing verdict at CLASSIFICATION, not after arbitration, when the arbitrator keeps the first at an equal rung. When calling a helper, read its parameter semantics AND its sibling call sites (`markDownstreamStale(runId, [node, ...downstreamOf(graph, node)])` — the target itself is excluded by `downstreamOf`); grep for the helper the NEIGHBOURING code uses on the same input before introducing a stricter one (`budgetFromSnapshot`, not `expandExecutionPolicy`, on a run without a snapshot). `null` is UNKNOWN, not "no": any "unknown ⇒ leave as-is" rule needs a companion for states where the field is MEANINGLESS (terminal PR → clear `pr_has_conflicts`), and the flag's downstream consumers are grepped first. A load-time gate may accept a well-formed FUTURE-engine graph (shape compatibility) while only EXECUTION checks host compatibility; no stored JSONB manifest reaches `compileManifest` through a TYPE CAST — one shared classifier (shape-only vs executable-stored) returns a typed `MaisterError` or read-model branch; an irreversible migration failure is a DISTINCT state-machine event with a CAS over the current enqueue intent and idempotent cleanup bounded by the event timestamp.
**Source (additions)**: 2026-07-12-10.43, 2026-09-02-03.51, 2026-09-04-00.21, 2026-09-03-14.30, 2026-07-16-11.20, 2026-09-03-17.20, 2026-08-06-19.10, 2026-09-03-17.21, 2026-07-11-14.47, 2026-09-03-15.06, 2026-09-01-04.15, 2026-07-15-18.10, 2026-07-11-22.35-m43-review-remediation
### Error-handling precision: try scope, controlled boundaries, honest sentinels
**Source**: 2026-07-03-16.16, 2026-07-01-12.38, 2026-06-24-03.08, 2026-06-18-15.42, 2026-06-21-02.04, 2026-07-04-13.37-experiment-comparison-review-fix-followup, 2026-06-29-22.01, 2026-06-23-23.16
**Rule**: A `try` wraps exactly the operation whose failure it classifies — never one span across an irreversible side-effect (`promote()`) and a best-effort follow-up (`addTaskComment`); follow-ups get their own WARN-only try/catch. Before deferring a possible domain throw to a later path, verify that path wraps it into the domain's terminal FSM (`runGateStepGuarded` → `markGateFailed`, route by blocking/advisory, rethrow non-domain) — otherwise the throw escapes to a generic catch and leaks half-created rows (`gate_results` stuck `running`); prefer failing BEFORE a side-effecting step (pre-spawn `markNodeFailed(CONFIG)`) over deferring into an unguarded call site. A sentinel must not conflate benign no-op with failed-critical (`{escalated:false}` covered both "run advanced" and "stranded live halt") — throw (`EXECUTOR_UNAVAILABLE`) or return a distinct consumer-handled variant, and audit every consumer of the sentinel. When an internal caller must distinguish benign-expected from real failure, use `MaisterError.details` (`details.autoPromotionSuperseded: 'held' | 'disabled'`, per ADR-093), never a new code or message match — UI still branches on `code`. Presence probes allow-list the exact "absent" errnos (`ENOENT`/`ENOTDIR`) and rethrow the rest as `CONFIG` — `catch → false` routes `EACCES`/`ELOOP` into the fallback branch. Catches mapping to success/advisory filter by ERROR CLASS (`GitPushRejectedError`, `EXECUTOR_UNAVAILABLE` → `{ok:true, warning}`) and rethrow everything else. Child-process stream readers resolve/reject on `close` (process exit), never stdout `end` — stream-finished ≠ process-succeeded (a valid-shaped nonexistent ref read as `{text:'', truncated:false}`); track intentional byte-bound SIGKILL truncation, buffer bounded stderr, reject non-zero exits with `CONFLICT`. New failure modes reuse `statusForCode` conventions (`EXECUTOR_UNAVAILABLE → 503` across ~20 routes), never fall to generic 500. Pages map only the TYPED not-found to `notFound()`/404 — other read failures propagate. Post-`session_ready` prompt failures route to the retryable path (`markScratchPromptRetryable`, dialog stays `WaitingForUser`), not the generic crash handler — the session and worktree are still valid. A `.then(onFulfilled)` pushed into `Promise.allSettled` is a swallowed-rejection trap: pair success-only flag mutations with an `onRejected`.
2026-07..09 additions: an OVERLOADED error code (`CONFLICT` = rebase conflicted AND push rejected) cannot be disambiguated by a smarter predicate — the fix is STRUCTURAL: move the push OUTSIDE the merge's catch (a rejected push of an already-landed merge had spawned an AI resolver and returned `ok:true`); only `agent_launched` returns `ok:true`, `noop`/`synced` rethrow. Evidence must never kill the thing it describes — a provenance/snapshot write inside a `try` whose catch finalizes the run `Failed` gets its own WARN-only catch. When a readiness probe precedes a resource op, later runtime-bound failures map to the SAME public typed error as the probe, and probe timeouts are cleared on every race outcome. A "best-effort, keep going" comment states the case it covers — the degenerate case (ALL sources refused) usually needs the OPPOSITE policy (abandon the committed DAG, answer the refusal's code, `launchError` per row). Log wording is a contract: `log.info "skipped crash"` for a path that destroyed a sub-tree becomes `log.warn` naming the cascaded sub-tree; structured log keys match their values (`workspaceId` was logged under `attemptId`); an audit line names EVERYTHING the action changed (`coupledScopes`), not the scope that triggered it. Client-visible error details are a CLOSED allow-listed enum localized EN/RU, never server messages.
**Source (additions)**: 2026-07-16-16.05, 2026-07-27-20.55, 2026-07-13-01.45-test-database-review-findings, 2026-09-02-12.10, 2026-09-03-15.37, 2026-07-17-00.00, 2026-09-03-23.46, 2026-07-17-00.10
### Runtime roots, git identity, SSE offsets, streamed frames, wire forms
**Source**: 2026-06-29-16.21-flow-runtime-root, 2026-06-28-22.10, 2026-06-25-17.19, 2026-06-29-14.59, 2026-06-24-03.55, 2026-06-17-19.53
**Rule**: All runtime artifact readers/writers resolve through the shared `runtimeRoot()`/`resolveFlowRuntimeRoot()` contract — never bare `process.cwd()` in long-lived runner code (Next's cwd can be `web/`, splitting `.maister/` from `web/.maister/`). Every server-side `git commit` routes through `commitIdentityArgs(repo)`/`commitFile` (per-field `-c user.<field>=<default>` only when unset; grep for the helper when adding a commit site — an identity-less host dies `fatal: empty ident name`); tests doing real commits run green under empty `HOME` + `GIT_CONFIG_NOSYSTEM=1` + no `GIT_AUTHOR_*`/`GIT_COMMITTER_*`. Never hardcode `main` as PR base/default branch — `gitRemoteDefaultBranch(dir, remote)` via `git ls-remote --symref <remote> HEAD` (batch-mode env, null on failure) with `?? DEFAULT_PR_BASE`. Coalesced transcript rows are display records AND offset checkpoints — every row update advances `content` AND the stored `supervisor_event_id` (a frozen offset makes `Last-Event-ID` resume replay the previous turn); keep offset math dialect-agnostic (compute the max projected event id in TypeScript, not a Postgres-only cast); fake SSE streams in tests honor `Last-Event-ID`. A launch-progress stream carries `runId`/`dialogUrl` in the EARLIEST frame that unblocks navigation (`session_ready`), not only the final result frame. Every `JSON.parse` of a streamed/partial frame (SSE/NDJSON `data:`) is guarded — skip the malformed line, keep reading. Cross-runner wire forms: only re-derive a value from a canonical token when it HAS a canonical identity (`staticSkillBySlug.get(slug)` hit); anything sourced from a live/native stream is preserved verbatim or excluded — never re-apply a per-adapter sigil to a string you didn't mint (codex `/` built-ins vs `$` skills turned `/status` into unrecognized `$status`).
2026-07..09 additions: the transcript projector preserves real adapter monotonic ids when present and synthesizes a strictly increasing replay horizon ONLY for lines lacking one (legacy logs still replay honestly). See the toolchain rule for the explicit-`cd` shell hygiene.
**Source (additions)**: 2026-07-27-11.56
### MCP facade mirrors move in lockstep with ext routes, locked by an OpenAPI-anchored contract test
**Source**: 2026-07-02-11.12, 2026-07-02-14.14, 2026-07-02-16.42, 2026-07-02-17.52
**Rule**: `mcp/src/tools.ts` `TOOL_SPECS` + the per-tool `dispatchTool` destructuring are a hand-maintained mirror of the ext routes with no shared source — destructuring known keys DROPS unknown args silently, so the agent sees "success" while its fields never leave the facade. Any ext-route body/enum change updates TOOL_SPECS + dispatchTool forwarding + the external OpenAPI in the SAME pass; any "agent gains an op" change moves the route scope, the scope→action map, AND `AGENT_TOKEN_SCOPES` together (the fixed grant list is the one that gets forgotten → 403 on a documented capability). Lock the mirror with an OpenAPI-anchored contract test (`mcp/src/__tests__/tool-contract.test.ts`): for every tool assert the inputSchema property-name set, required set, per-field base types, enum values (incl. `null`), AND declared bounds (`minimum`/`maximum`/`minLength`) equal the operation's path params ∪ query params ∪ request-body schema; write it RED-first (it caught drift no human spotted); new tools register in the `TOOL_OP` map via a "maps every registered tool" case; record what stays unlocked (`format`, `pattern`, nested inner types). Cheap grant-list guard: mint a token holding exactly `AGENT_TOKEN_SCOPES` and hit the route in a real-PG integration test. Table-driven contract tests fail with clean per-item assertions on missing fixtures (collection-time `?.` + per-tool existence case), never a collection-crashing TypeError; tests formatting relative/local time stub the `Intl` timezone.
2026-07..09 additions: `nullable: true` is an OpenAPI keyword meaningless in JSON Schema — MCP `inputSchema` uses `type: ["string","null"]` (a validating client rejects the documented first-writer form). A tool description explains a null field (`relation_list` null title) so a model does not read it as missing data and retry. Ext tool contracts refuse unsupported per-kind options BEFORE resolution (`refuseUnsupportedDelegationOption` → CONFIG 422 for ALL unsupported entries at once); resolution failures keep one shared code, mixed → PRECONDITION with `[CODE]` per line. `mcp/src` is inert until `pnpm --filter @maister/mcp build`.
**Source (additions)**: 2026-07-27-20.55, 2026-08-06-19.10, 2026-09-02-03.51, 2026-07-18-13.35
### Raw-SQL correctness: qualification, non-throwing predicates, scoped KNN, temporal reducers
**Source**: 2026-06-29-16.04-run-session-scalar, 2026-07-03-16.16, 2026-07-02-17.52, 2026-07-03-16.40
**Rule**: Correlated scalar subqueries built with raw Drizzle SQL construct the outer reference explicitly (`sql.identifier(getTableName(runIdCol.table))` + `sql.identifier(runIdCol.name)`) — `AnyColumn` interpolation can render an unqualified `"id"` that Postgres resolves against the INNER table; add a unit test compiling the query to SQL asserting the qualified form (`rs.run_id = "runs"."id"`), and smoke BOTH read-model shapes (`getRunDetail` AND `getRunSettings` — the simple `FROM runs` selector is where it broke). Never push a value a TS parser deliberately fail-closes into an SQL cast — `(col->>'enabled')::boolean` THROWS on stored data the tolerant parser degrades to `config_invalid`; use non-throwing predicates (`@> '{"enabled": true}'::jsonb` or `->> = 'literal'`) and keep the tolerant parser as the authority. Vector KNN scopes tenant + status (project/active/unexpired) INSIDE the CTE BEFORE top-K — a global top-K filtered afterwards starves cross-project recall. Any consumer reducing the append-only `gate_results` ledger routes through readiness-core SSOT helpers (`getNodeAttemptsForRun` + `latestAttemptIdsByNode` + `collapseLatestExternalPerGate` + `isExternalGateReady`) — `rows.some(passed)` any-ever-true reads stale superseded evidence; grep `gate_results` consumers when adding one. Graph gate declarations come from `compileManifest` over `flow_revisions.manifest` (pinned) else `flows.manifest` — never a bundle re-read.
2026-07..09 additions: derive SQL literal lists from the TS constant (`sql.join` over `TERMINAL_RUN_STATUSES`) — never duplicate a status set as string literals (the classifier and the loader could only drift together). Re-run a PURE gate under `FOR UPDATE` rather than re-encode its predicate in a WHERE when a column is nullable (`pr_state` NULL on the conflicted-only case — `eq()` never matches). Every replay cursor is a CONTRACT with ordering rules: `domain_events` replay and bootstrap-tail selection carry the commit-horizon fence `tx_id < pg_snapshot_xmin(pg_current_snapshot())` (naive `id > since` skips an older in-flight tx FOREVER once a newer one commits); per-item cursors are opaque `{ lastMutationId, lastItemId }` with a stable-item tiebreak (equal-horizon siblings vanished across pages). Read-model time windows use the domain event's `occurred_at`, never insertion `created_at`. Any first-match resolver with `LIMIT 1` gets a deterministic `ORDER BY` (`ORDER BY (id = ref) DESC`; `orderBy(asc(pi.id))` on a reverse join) — Postgres row order is not a contract, and a UUID-shaped `flow_ref_id` passes `capabilityRefIdSchema`. A cursor-pagination contract is complete only with DB predicates + per-source `limit + 1` candidate windows applied BEFORE the merge + client continuation (receive/advance/reset the opaque cursor). When an enum or nullable set already exists in code or schema, ALIAS or DERIVE (`PulseEventKind` from the domain-event taxonomy, `NeedsYouItem.kind` from the DB `HitlRequest` union) — never hand-mirror in a second type. Observatory page reads are CACHE-ONLY (no Git or scheduler call in the query path); shared-worktree delivery evidence is persisted only on the immutable ROOT allocator row; flow-ledger metrics are flow-scoped with cost/budget via run-kind JOINS, not denormalized historical columns; a uniqueness claim is enforced in BOTH directions (one delivery ref per run AND one run per delivery ref — a collision yields `insufficient`, never a double count).
**Source (additions)**: 2026-09-03-23.46, 2026-07-16-14.05, 2026-07-27-11.56, 2026-07-12-22.35-observatory-agentization-final-hardening, 2026-07-14-13.45, 2026-07-12-11.12, 2026-07-16-13.06, 2026-07-12-20.46-observatory-agentization-review-remediation, 2026-07-12-23.49-observatory-agentization-review-fixes
### Mirror-pair syncs diff first; prompt invariants get pinning tests
**Source**: 2026-07-02-12.48
**Rule**: Before `cp`-syncing a contractual mirror pair (test fixture ↔ package deliverable), `diff` the two FULL current bodies (frontmatter-only checks miss body divergence); if they diverge, reconcile the delta explicitly — syncing in either direction destroys whichever side was ahead (a fixture-only hold guard was silently clobbered by a deliverable→fixture sync). Load-bearing prompt/persona invariants (early-stop guards, no-op wording) get `toContain` pinning tests at the definition-parse level so silent prose loss turns into a red test — a wire test cannot exercise agent reasoning; the parse level is the honest enforcement point.
2026-07..09 additions: a `// mirrors X` comment is a defect to extract as the COMPOSITION — the whole check sequence WITH its messages (`evaluateFlowLaunchability` / `describeFlowLaunchabilityRefusal` / `assertFlowLaunchable`), not only the constant it reads (three launchability copies had diverged: the board also required a Ready runner + an executable stored manifest). Near-duplicate helpers diverge on their SAFETY property (`releaseSyncClaim` fenced on the attempt id while its copy `releaseClaim` kept the buggy `name='sync'` guard; `failResolver` vs `failAgentAttemptToReview`) — fix BOTH twins and thread the fence token onto the row type. A new predicate built next to an existing one (`connectable` next to `candidatesFor`) applies the SAME guards (`enabled`, source-scoped match) — diff them. Two functions answering the same classification question (`deleteSession` vs `deleteSessionIfPresent`) diverge — extract the answer once; the correct implementation UNUSED on the paths that need it IS the bug.
**Source (additions)**: 2026-09-02-03.51, 2026-07-16-16.05, 2026-07-12-22.40, 2026-09-03-15.06
### Filesystem ownership: markers before content, release after the terminal commit, private staging before publish
**Source**: 2026-07-11-12.25, 2026-07-11-13.34, 2026-07-12-10.39, 2026-07-12-11.21, 2026-07-16-20.43-canonical-create-flow-review-findings, 2026-07-17-00.10, 2026-07-14-14.31
**Rule**: Keep filesystem mutation OUTSIDE terminal DB transactions: commit the terminal run transition FIRST, release/clean materialization AFTER the commit, and log a release refusal as a structured error WITHOUT reverting the run (an expected ownership refusal inside the tx rolled back the terminal status and left a crashed run stuck forever); retain the record for a guarded retry. Ownership is state-aware (`preparing` / `active` / `releasing`): roll back only zero-owner `preparing` intents after path prevalidation; PROVE foreign-lease preservation before any deletion and keep foreign-leased or corrupt targets fail-closed; recover a stale ownerless lock from the lock-DIRECTORY timestamp, never from an owner file that may not exist after a crash; check required L2 paths by membership; serialize per-cwd work with the SQLite mutex. Any single-owner artifact that gates cleanup writes its OWNERSHIP MARKER BEFORE the content it guards and exposes a marker-attributed reclaim (`reclaimAgentL2Settings`, lock-guarded) so a crash mid-write leaves an attributable, removable artifact — never one mislabeled user-owned; journal capability settings through explicit backup → write → restore phases and distinguish LOCAL from FOREIGN L2 leases. Filesystem cleanup is conditioned on explicit OPERATION-LOCAL ownership (exclusive `mkdir` claim before writing, or a byte-for-byte-unchanged check on a generated file) — never a derived path or a DB-row identity: a DB-to-filesystem workflow is a PUBLICATION PROTOCOL — assemble in an operation-UUID private staging dir, publish by atomic rename only after the journal, manifest, and initial commit are complete; compensate unjournaled state by removing only its private stage + row; never adopt or remove a FINAL directory without durable evidence. The L3 dirty-watchdog filters porcelain only for EXPLICITLY owned paths — including BOTH sides of a rename; unknown metadata and user-owned settings stay dirty.
### Recovery owners for durable interactions; recovery-window tables are normative
**Source**: 2026-07-15-11.19-gate-chat-recovery, 2026-07-14-23.40-flow-review-workspace-findings, 2026-07-14T19-18-32-plan-review-handoff-recovery, 2026-07-16-16.05, 2026-07-16-11.20, 2026-09-03-21.47, 2026-09-03-23.46
**Rule**: Any durable interaction normally settled by an IN-MEMORY callback (a lease timer, a pending gate-chat turn, a persisted-but-unmarked decision answer) needs a RECONCILIATION OWNER after a web-process restart: claim it under a durable recovery lease, cancel the live supervisor prompt, restore the checkpoint baseline, and only then terminalize; a failed recovery stays fenced for retry. A lease expiry is a CANCELLATION DEADLINE owned by the running handler, not a terminal coordinator outcome — the coordinator stays pending until the owner has awaited the prompt, performed L3 restore, and written its terminal abort (a lease can never release the rework fence before L3 settles). Every recovery window is a NORMATIVE table in the ADR/analytics enumerating each `status × mode` cell — the implementation must not define the windows it happens to satisfy: the resolver's own parking spot (`NeedsInput`, `agent_running`) had no arm because reconcile was `Running`-only; a mechanical sync parks in `Review` and had none either. Never enumerate a closed phase set POSITIVELY inside a recovery arm when its complement is derivable (`mode==="mechanical" && !hasSyncDriver` over terminal-filtered candidates) — the set grows on the PRODUCER's side (`starting|rebasing` was written when `verifying|pushing` did not exist) and a stranded claim blocked all six lifecycle ops forever; a phase past the point of no return (`pushing`) gets a FORWARD-SETTLE branch (ask ORIGIN: `remoteBranchHead` vs worktree HEAD → `succeeded/pushed`), never a restore, and an UNPROVEN push is never restored. Under a dead coordinator enumerate what each child can be HOLDING (crash paths never cascade): `Pending → Abandoned`; `NeedsInput`/`NeedsInputIdle`/`Review → Crashed` with the open `hitl_requests` row closed in the same tx; `HumanWorking` → left, WARN; a `WaitingOnChildren` sub-orchestrator → `orphaned-orchestrator` via the stuck path (its own children cascaded first); coordinator death is ANY terminal parent (`Failed` without cascade, `Done` leaving a child), not the `Crashed|Abandoned` pair the cascade writes. Load orphans with a TARGETED query (`NOT EXISTS (a parent row still alive)`), never by widening the hot candidate list (starves real candidates under `PER_TICK_LIMIT`). A new route into an existing arm INHERITS that arm's preconditions — the stuck path's classifier guaranteed `!liveSession`, so `orphaned-orchestrator` must stop the coordinator's own live sessions BEFORE the cascade with E5 semantics (`EXECUTOR_UNAVAILABLE` → leave the whole sub-tree for the next tick).
### Idempotency keys, digests, and canonical payload equality
**Source**: 2026-07-25-12.09, 2026-07-25-11.14, 2026-09-01-04.15, 2026-07-14-23.40-flow-review-workspace-findings
**Rule**: An idempotency/dedup key that resolves to a STORED result verifies the incoming payload matches before aliasing (`existing.definitionDigest === contentDigest(definition)` — the same digest that stamped the row); "return existing on conflict" is a silent-substitution bug unless content-guarded — a changed payload under a used key CONFLICTS; cover same-key/same-payload (dedup) AND same-key/different-payload (CONFLICT). `sameDecision`, not `sameRequest`: an already-delivered branch reached by a DIFFERENT payload is a LOSER, not a retry — it mutates nothing and re-drives only when the arriving payload is byte-identical to the stored decision. Every value feeding a dedup digest is reproducible from the idempotency key (`inline-${idempotencyKey}-${index}`) — never inject `Date.now()`/random ABOVE a dedup. Payload equality for retries compares CANONICALIZED bodies (recursively sorted object keys, array order preserved), never raw JSON serialization.
### Freeze/pin/snapshot features: every read path reads the frozen source; validation lives on the action path
**Source**: 2026-07-26-12.32, 2026-07-25-11.14, 2026-07-24-15.19, 2026-07-27-21.50, 2026-07-11-22.35-m43-review-remediation
**Rule**: "Recorded for provenance" without a SWITCHED read path is the defect shape: for any freeze/snapshot/pin feature enumerate EVERY read path and make it read the frozen source (judge context, pair fan-out, tournament matrix, objective checks, launch resolution all read live membership/revision) — the self-check is a grep for the LIVE accessor (`orderedStudyParticipantIds`, `isNull(removedAt)` inside the execution pipeline, `flow.enabledRevisionId` after a pin exists). A fan-out (pairs, checks) and its aggregation in different modules SHARE one participant-set function derived from the sealed snapshot (`frozenExecutionParticipants()` from `participant_watermarks`, tombstone-inclusive, fail-closed without a snapshot); walk the frozen expected matrix and synthesize empty verdicts so a lost pair keeps `partial`. Pinned `flow_revisions.manifest` is immutable run evidence — never pre-parse or fall back to the mutable Flow cache first. Apply the hard-warn rule to EVERY un-honorable declared-input class (intent AND `mode:"runner"` pins on non-`session:` slots) and enforce validation ON THE ACTION PATH (the launch route runs `preflightStudyRecipe` and BLOCKS on hard refusal before any write) — an optional preview plus warn-on-one-variant leaves a hole either alone closes; one pure SSOT predicate (`isSeamThreadableSlot`) feeds BOTH the seam and preflight. Contract fixtures derive digests from the REAL projection (`buildFlowContractProjection` + digest fns), never literals — fake digests exempt the suite from every drift gate until real enforcement lands and breaks it. Mutating pairs are SYMMETRIC: if save is CAS-guarded, delete takes the same `ifHash` (`ifHash: null` matching an absent file IS the idempotent case); when N artifacts describe the SAME BYTES (prompt block, snapshot, hash), normalize ONCE at the source or not at all.
### Activating dormant gates and reasoning about limits, budgets, and coupled defaults
**Source**: 2026-09-03-15.06, 2026-09-03-17.20, 2026-09-03-17.21, 2026-09-01-05.40, 2026-07-12-11.42, 2026-07-15-18.13-trusted-package-flow-enablement, 2026-07-12-23.49-observatory-agentization-review-fixes, 2026-08-01-09.09
**Rule**: When a guard turns out to be DEAD the fix is TWO tasks: re-enable it, then RE-DERIVE every semantic that was never exercised (grep the spec for the enabled feature — it addressed only a parked `WaitingOnChildren` root; a `Running` root's tree breach in the escalate band was swallowed because `pickHigher` keeps the FIRST verdict at an equal rung → promote tree verdicts at CLASSIFICATION, not after arbitration). FIRST search for COMPENSATING WORKAROUNDS — a comment "X never fires, so we also do Y" (`budget-default.ts` seeding BOTH `run` and `tree` ceilings) is a load-bearing dependency on the bug; `rg` the feature name in comments. When a fix corrects a CONVENTION, grep the PREDICATE, not the symptom (`rg "rootRunId ===|root_run_id ="` — launchers write `rootRunId = parent.rootRunId ?? parent.id`, so the ROOT carries NULL, nothing self-stamps; rootness = `parentRunId === null` (+ `hasDescendants` for tree ceilings); tree reads scope `id = root OR root_run_id = root`). A guard's PRESENCE is not its REACHABILITY — trace every field a guard reads off a loose payload back to the producer that populates it (`ref.parentRunId` was never set on the `promoteNextPending` edge). Redundant MEASUREMENT ≠ redundant LIMIT: two ceilings over one meter are two constraints and the stricter binds — skip a meter only when the other ceilings are not stricter (`tree.escalate >= run.escalate && tree.hard >= run.hard`), and skip NARROWLY (tree TOKENS for a childless root; `wallclock` is tree-scope-only, so dropping the scope silently disables lone-run duration bounds). A COUPLED default needs a COUPLED raise — seeding two ceilings from one value must lift both on a raise, only on EXACT equality (the shape `applyDefaultBudgetForUnattended` writes), preserving a deliberately tighter or looser value, and the audit names every `coupledScopes` it changed. Before building a fence, verify it can BLOCK the racing path (`runner-graph.ts` holds zero `claimLifecycleOperation` refs, so a lifecycle claim cannot fence the runner). Ask whether a guard's FAILING state is reachable in production — a refusal whose condition the system never produces (empty `merge-base..branch` on a Review run) is dead code that reads as protection. Adding a scheduler kind fans out to EVERY closed set: registration, lifecycle, `claimDueJobs` CTE, dispatch, admin, OpenAPI, AND the localized admin label map. A trust/gate transition also ADVANCES the rows waiting on it (`trustPackageRevision` left ready member Flows `Installed`) with a one-time repair migration for existing rows; conflict-ignore seeding (`onConflictDoNothing`) has NO reactivation transition — reseed archive-disabled jobs on reactivation while retaining native-threshold disables. Preflight probes assert only state INVARIANT across revisions (host tools); branch-owned config is validated from the EXECUTION revision (ADR-091 probes run in `project.repoPath` before the worktree exists). A hard cut-over needs BOTH a DATABASE backstop against stale binaries (trigger rejecting new legacy-flow runs) AND a post-migration reconciliation of orphaned EXTERNAL resources.
### Process-boundary contracts: single-flight lookups, fake parity, real wire order, fenced binding
**Source**: 2026-09-04-00.21, 2026-07-11-14.47, 2026-07-11-21.30, 2026-07-11-12.25, 2026-07-27-11.56, 2026-07-14-human-ask-review-remediation, 2026-09-03-15.06
**Rule**: A resolver/registrar/LOOKUP MUST NOT await domain sweeps — the registrar awaited `runReconcileSweep` inside the resolver's single-flight promise, the sweep re-dispatched `runFlow`, whose first agent node re-entered the same resolver → a process-wide hang after any supervisor restart; schedule domain work out-of-band (`restarted:true` + the periodic `system_sweep`); self-check: "does anything inside a single-flight promise call back into the same resolver?". When a contract is split across two processes (host handle registry vs web re-adopt policy; released-but-reused paths), rebuild the fake RULE-BY-RULE to the peer's semantics and run ONE scenario table against the fake AND a real peer child (~90 fake-backed suites had proved only the fake's contract). A mock for an ordering-sensitive interceptor emits the REAL adapter sequence — grep the bundled adapter `dist`: `sessionUpdate:"tool_call"` `status:"pending"` streams BEFORE `requestPermission`, and the execution signal is `tool_call_update` `status:"completed"|"failed"`; replay the triple pending→permission→execution, never the author's mental model. A value derived in module A and consumed in service B gets a test at the SEND site (the spy captures `ctx.enforcementProfile` on the outgoing `createSession` body) — derive-unit + accept-schema leave the join unguarded. Derive-then-inject is an ordering bug: appending a value (the maister MCP facade) to a delivered list AFTER a dependent field was derived from it (`enforcementProfile.mcps.allowServers`) updates the dependent field in the same step. Enumerate every CLAIM site and every CATCH site as a checklist when a rule says "every re-entry mints" / "every fenced outcome yields" — grep `forRun(`, `mintPlacement(`, `isFencedError(` and classify each hit; drivers bind by the assignment id their OWN claim minted (`executionFor(runId, {assignmentId})`), never "whatever is active at bind time". A list-then-delete against a peer is a two-step protocol: classify scoped ABSENCE (bound-session 404 → `gone`) separately from transport availability (5xx/network → `EXECUTOR_UNAVAILABLE`) and identity misuse; reject a live same-run session whose ACP id DIFFERS from the stored binding; share ONE deletion-failure classifier (`deleteSession` mapped any non-ok without a recognized body code to terminal `ACP_PROTOCOL` while the supervisor's unhandled-error handler emits exactly `500 {code:"ACP_PROTOCOL"}`). Adapter-generic behavior is tested ONCE at the shared seam while each adapter separately proves it REACHES that seam; cross-package descriptor mirrors pass the ROOT contract validator. When one replay reader uses a consistency fence, port it to every SIBLING replay surface in the same pass.
### Tests written during implementation: seams, seeds, races, and wire assertions
**Source**: 2026-07-16-13.03, 2026-07-22-01.14, 2026-07-16-16.05, 2026-09-03-23.46, 2026-09-04-01.33, 2026-07-27-20.55, 2026-07-27-21.50, 2026-09-03-14.30, 2026-07-21-23.50, 2026-08-06-19.10, 2026-07-16-14.05, 2026-09-03-21.47, 2026-09-03-16.10, 2026-07-13-00.59-unified-test-database-review-remediation, 2026-09-02-12.10, 2026-09-01-04.15, 2026-07-12-22.35-observatory-agentization-final-hardening, 2026-07-11-21.30
**Rule**: Mocks for HOOK-RETURNED functions used in `useCallback`/`useEffect` deps preserve the real hook's STABLE IDENTITY (cache one translator per namespace inside the hoisted `vi.mock` factory) — a fresh function per render looped to a 4 GB OOM; mounted tests that load on effect assert EXACT side-effect counts. When a SHARED service (`launchRun`, a HITL composite, a promotion service) gains a new DB-touching dependency or a required argument, grep its `__tests__` for the fake-db/mock/harness pattern and update EVERY caller test in the SAME change ("unknown table" / "innerJoin is not a function" from a hand-rolled fake DB is the tell; assert the new production ARGUMENT rather than relying on a permissive stub; a new shared React provider sweeps every test render). SEEDS ARE PRODUCTION-SHAPED: never seed a state no writer produces (a claim with no `lifecycle_operation_attempt_id`, a self-stamped `root_run_id`, `prState:"closed"+prHasConflicts:true`, a driverless paused run that is really a post-restart orphan); READ the DB CHECK constraints before seeding (`hitl_requests` requires a task binding only on agent questions); an unusual precondition that makes a guard fire earns a second test in the ordinary shape; correct the seed to the realistic precondition — never weaken the gate. RACES: put the two-racer at the layer that OWNS the invariant (store, not route — racers traversing identical I/O self-serialize), contend on the INVARIANT not the lock (two DIFFERENT parents for an ancestor-scoped count; the winner INSERTS its children uncommitted; assert the racer PARKS via `pg_stat_activity`), assert ORDER via signals (mocks that fire when the winner ENTERS its side-effect) never "already happened by now", cover BOTH orderings, release every barrier in `finally` and await both racers to settlement (a parked winner leaked into the NEXT test's spies), inject through the sweep's `listWorktrees` hook rather than module mocks, and mutation-check by disabling the guard. ASSERTIONS: a wire field exists to reach a client, so at least one test reads it off a real `Response` (`(await res.json()).details`) — a value placed in a mock is not tested; every allow-list/grant gets a POSITIVE test (a deny-only test cannot distinguish "refused" from "broken"); assert the DISCRIMINANT only the new path produces (`status === "Failed"` was produced by the bug too; the token revoked + a queued AGENT run promoted was not); a composition test asserts `errors: []`; pin the INERT/negative contract with `not.toHaveBeenCalledWith(objectContaining({ field: expect.anything() }))`; prefer `toHaveBeenCalledWith(expect.objectContaining(...))` over `mock.calls[0][0]` on a no-param `vi.fn`; role mocks refuse the real role's FULL deny set; run integration tests with the REAL child kind the fixture produces. E2E: a fixture seeding N runs against ONE mutable git ref couples its tests — each scenario gets its own ref (`release-merge`/`release-conflict`/`release-pr`); never assert run status with a page-wide `getByText` (shared chrome renders every sibling run's hover-only status span) — `getByTestId("run-header-status")` + `toHaveText`; role-specific locators when a label is also decorative; every spec needing a session is listed in the AUTHENTICATED Playwright project. Seams: a second `vi.mock` of the same module in one file silently SHADOWS the first; lazy-import `runner-graph` (a top-level import pulls the supervisor client into 57 unit tests); a wrapper owning an ephemeral resource has a process-signal contract tested with a SIGTERM-resistant child, plus a cleanup-FAILURE case (`Pool.end()` throws → container still shut down).
### Toolchain pins, generated migrations, and shell hygiene
**Source**: 2026-07-12-11.21, 2026-07-12-11.42, 2026-07-18-13.35, 2026-07-18-14.55, 2026-09-01-05.40, 2026-07-16-22.26-workspace-reconciliation-review-fixes, 2026-07-15-11.19-gate-chat-recovery, 2026-07-17-00.10-workspace-lifecycle-review-remediation, 2026-08-01-09.09, 2026-09-02-03.51, 2026-09-04-00.21
**Rule**: A new `node:*` builtin import (`node:sqlite`) is verified under the PINNED Vite (5.4.21 strips the prefix and vite-node fails on bare `sqlite`; `server.deps.external` does not help) — the escape hatch is a resolve-to-virtual-module `enforce:"pre"` plugin pulling the builtin via `createRequire`, applied to both workspace projects. TS 5.6's iterator lib vs `@types/node@20.9.0` makes `Buffer` not assignable to `Uint8Array` — run `pnpm typecheck` on any branch adding binary `readFile`/`writeFile` flows and coerce with `Uint8Array.from(buf)`. Every executable CLI import of a `server-only` module (db CLI, Playwright global setup, drizzle config) gets a loader-shim smoke test. `mcp/src` edits are inert until `pnpm --filter @maister/mcp build` (the facade runs `mcp/dist/main.js`). MIGRATIONS: `schema.ts` is the FOURTH leg of the migration contract — after hand-authoring SQL run `drizzle-kit generate` and READ the diff; a proposed UNDO means `schema.ts` was never updated (0125 widened a CHECK to 13 kinds while `schema.ts` declared 11). A migration that DROP+re-CREATEs a CHECK/enum on a SHARED table is re-derived from `schema.ts` at rebase time, never carried verbatim (0111 dropped main's `run_pr_merged`; schema + snapshot correctness MASKS it from every drizzle tool) — grep the re-created value list against main's newest migration on that constraint. A migration that STRENGTHENS a multi-column invariant (a claim = state + attempt id + op name + expected status + unexpired lease) enumerates EVERY writer and fixture for that state and runs the owning real-Postgres suites — typecheck cannot see check-constraint violations. Encode lifecycle terminal-state invariants as generated CHECK constraints (`completed` requires the agent message + timestamp; `failed`/`aborted` require an error and no agent message; non-negative attempt counts), locate them in the journal test by CONTRACT not "newest", and give a cross-cutting constraint its OWN migration number. In-place amendment of migration SQL is safe ONLY under a recorded "applied nowhere durable" premise re-confirmed with the owner. SHELL: every Bash call `cd`s to the package directory explicitly (a bare `pnpm exec vitest` from the wrong cwd prints only "Command not found"); a script branching on a WRAPPER tool's exit code tests the wrapper's actual codes (docker compose FLATTENS 125/126/127 to 1 — classify env failures by code OR a trailing `Error response from daemon:` marker); after a rebase grep consecutive duplicate statements (`awk 'prev==$0 && $0!=""{print FILENAME": "FNR": "$0}{prev=$0}'`). Before discarding a KILLED agent's work, audit it: full diff → interruption markers → typecheck (one compile error localizes the frontier) → its test files; verdict PER SLICE; parallel same-tree agents need disjoint file ownership and cross-file contracts stated in both prompts. Prose retention windows ("7d") are checked against the config constant (`DEFAULT_GC_AGE_DAYS = 14`) before being baked into fixtures.
### Authorization at reads and writes: gate by data class, scope per row, bind tokens to principals, redact by kind
**Source**: 2026-07-27-21.50, 2026-08-06-17.03, 2026-07-22-01.14, 2026-08-06-19.10, 2026-07-13-11.21, 2026-09-04-00.21, 2026-07-12-11.12, 2026-07-18-14.55
**Rule**: Pick a READ gate from the DATA'S CLASS, not the route's neighbourhood ("what is the most sensitive thing that can end up in this response, and who else can already see that?") — a file an agent writes WITH REPO ACCESS inherits `readRepoFiles` (ADR-053), not `readBoard`. Any RSC read that feeds CLIENT props is gated by the SAME authorization as the surface that displays the value ("rendered only under X" ≠ "transmitted only under X" — the holder's email reached viewers in the Flight payload); a sensitive read-model aggregation (cross-project attachment counts) is an explicit OPT-IN at the query boundary. When a read spans TWO authorization domains, re-derive the question from the RESPONSE SHAPE ("which rows in this body belong to something the caller was never authorized for?"): redact foreign content behind an explicit `redacted` boolean while keeping the ADDRESS the contract needs (`taskKey`+`number`), memoize decisions per project. When an allow-list entry's JUSTIFICATION is narrower than the entry ("the edge IT CREATED" vs `relations:delete`), enforcement belongs in the operation's predicate — `onlyAuthoredBy` as AND-terms on the DELETE's OWN `WHERE` (never a preceding SELECT), preserving the existence-hidden `200 {removed:false}`; every DENIAL keeps the existence-hidden 404 while the granted path is proven by a positive test. A client-minted session/bearer token is bound to a server-authoritative principal at EVERY privileged write (`locked_by_user_id = ${userId}` in assert/refresh/release) and never degrades to `Date.now()`/`Math.random()` (use `crypto.getRandomValues`). A git URL/remote is a subprocess-argv sink: `validateUrl` at every producer including defaults, `--` before positional args in `gitSetRemote`, reject `::` smart-transport (`ext::sh -c …`) and `-`-leading scp user/host (`-oProxyCommand=…@host:`), `branchNameSchema` on base branches. Redaction is a per-KIND allow-list projection — key-based redaction let `args`/`url` secrets through. Lineage/immutability guards hold at EVERY mutation path (participant hard-delete, restart, sync, GC, holds); validate ids against membership before a cross-project pull.