@aotter/mantle 0.0.11-alpha.16 → 0.0.11-alpha.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,509 @@
1
+ # ADR-0007: AI-as-primary-author — three feedback loops, two role surfaces
2
+
3
+ **Status:** Carried over from POC v0.0.x; refreshed and folded for v0.1.0 (incorporates POC ADR-0013).
4
+
5
+ **Date**: 2026-05-03
6
+
7
+ **Deciders**: phsu
8
+
9
+ **Related**: ADR-0001 (the manifest the contract operates on), ADR-0008 (the structured diagnostic shape this contract emits)
10
+
11
+ ---
12
+
13
+ ## Context
14
+
15
+ This ADR is the local statement of an Aotter-wide thesis. mantle is
16
+ one half of **Mantle** (**C**onfig **L**anguage for **A**pps &
17
+ **M**odeling) — the other half is the OLAP / data-warehouse work in
18
+ [`aotter-mantle`](https://github.com/aotter/aotter-mantle). Both share a
19
+ single design grammar:
20
+
21
+ > **Agents write config; the runtime carries the complexity.**
22
+
23
+ Hard problems — schema validation, OAuth, locale canonicalization,
24
+ cache invalidation, transactional state — live in the runtime, where
25
+ they're written once by people who understand them. The authoring
26
+ surface is YAML the agent fills in, with structured diagnostics
27
+ catching mistakes before they become production failures. Non-coders
28
+ benefit from agent leverage *safely* because the load-bearing logic
29
+ isn't in their hands.
30
+
31
+ This ADR documents what that thesis means concretely for mantle's
32
+ authoring contract.
33
+
34
+ The primary author/integrator of consumer apps that depend on
35
+ `@aotter/mantle-*` is expected to be Claude Code (or a peer LLM
36
+ agent) running inside the consumer project. Human contributors review
37
+ and steer, but the moment-to-moment work — wiring a manifest, writing
38
+ a Procedure handler, registering it at boot, bisecting a failure — is
39
+ the AI's.
40
+
41
+ This is a different DX target from the typical SDK. Conventional
42
+ "developer experience" thinking optimizes for a human at a terminal:
43
+ prose error messages, IDE squiggles, stack traces inspected by eye,
44
+ "let me try one more thing and see what the server logs say." That
45
+ loop is fine for humans and bad for AI authors:
46
+
47
+ - An AI cannot cheaply form an intuition by "running the thing and
48
+ seeing what happens." It can run a CLI and read structured output;
49
+ it cannot easily watch a streaming server log over an SSH session
50
+ and infer state.
51
+ - Prose error messages force the AI to do natural-language parsing
52
+ before it can act. Structured diagnostics ({code, path, expected,
53
+ actual, candidates, suggestion}) are directly actionable.
54
+ - Errors that surface only at production runtime are invisible to an
55
+ AI working in a dev loop — by the time they appear, the AI has
56
+ finished its turn and walked away. Failures must show up in the
57
+ feedback channel the AI is currently watching: the CLI it just ran,
58
+ the test suite it just executed, the deploy it is currently
59
+ blocking on.
60
+ - "Try and see" debugging on an opaque deployed system has a long
61
+ feedback latency that compounds badly with AI cost-per-turn. Cheap,
62
+ deterministic, local feedback dominates.
63
+
64
+ The naive SDK posture is "it works at runtime, errors come back as
65
+ HTTP 500s." That is the correct target for the customer of the
66
+ consumer's app. It is the wrong target for the consumer-app author.
67
+
68
+ This ADR defines the contract that fixes that gap. It also folds in
69
+ the role-split decision (originally POC ADR-0013), because the
70
+ feedback loops below only make sense when paired with the question of
71
+ *which AI is reading them*: the **coder agent** (Claude Code in the
72
+ consumer's repo) versus the **operator agent** (a content-editing
73
+ client like Claude Desktop, talking to the deployed Worker over MCP).
74
+
75
+ ## Decision
76
+
77
+ ### Part A — Three feedback loops for the coder agent
78
+
79
+ The SDK provides **three explicit feedback loops**, ordered by
80
+ "leftward shift" — catch the failure as early in the author's
81
+ workflow as physically possible:
82
+
83
+ #### Loop 1 — Static validation (`mantle validate`)
84
+
85
+ Pure manifest + handler-source check. No D1, no runtime, no
86
+ network. Runs in the AI's terminal in milliseconds. Reads YAML files
87
+ and handler registration source files; emits structured diagnostics;
88
+ exits non-zero on any error.
89
+
90
+ The CLI ships in the `@aotter/mantle-spec` package — it's the
91
+ spec authority for what a v0.1 manifest must look like.
92
+
93
+ What it catches:
94
+ - Manifest envelope (`apiVersion`, `kind`, `metadata.name`)
95
+ - `Trigger.target.procedure` references a Procedure that exists
96
+ - `View.from` references a Schema that exists; `View.fields` and
97
+ `View.filter` refer only to declared properties of that Schema
98
+ - `Schema.uniqueIndexes` lists only properties declared in the
99
+ Schema's `spec.schema.properties`
100
+ - `x-mantle-bind: <value>` is in the closed enum
101
+ - `requires.auth.all` predicates are in the v0.1 vocabulary
102
+ - `Trigger.source.path` does not collide with another Trigger
103
+ - `metadata.name` is unique within `kind`
104
+ - Each `Procedure.handler.ref` has a corresponding
105
+ `sdk.registerHandler("<ref>", ...)` call somewhere in the
106
+ consumer's TS source (textual grep, not runtime — boot loop is
107
+ Loop 2's job)
108
+
109
+ This is the cheapest loop; it must be runnable without booting
110
+ anything. AI authors should run it after every manifest edit.
111
+
112
+ #### Test harness (planned public package surface)
113
+
114
+ Supporting tooling that consumers use inside their own test suites —
115
+ it is not itself a feedback loop, it's the harness those tests run
116
+ against. In-memory dispatcher invocation: the consumer's test suite
117
+ imports a public testing module that spins up a complete dispatcher
118
+ against an in-memory D1 substitute, seeds rows, and calls Procedures
119
+ or Views directly without going through HTTP.
120
+
121
+ Target API sketch:
122
+
123
+ ```ts
124
+ import { createTestDispatcher } from "@aotter/mantle-runtime/testing";
125
+ import { manifests } from "../manifests"; // loaded by build hook
126
+ import { handlers } from "../handlers"; // map of ref → fn
127
+
128
+ const dispatcher = await createTestDispatcher({ manifests, handlers });
129
+ await dispatcher.seed({ collection: "posts", rows: [...] });
130
+
131
+ const result = await dispatcher.invoke(
132
+ "send-contact-message",
133
+ { name: "Alex", message: "hi" },
134
+ { user: { id: "user-uuid" } },
135
+ );
136
+ expect(result).toEqual({ ok: true });
137
+
138
+ const view = await dispatcher.queryView("recent-published");
139
+ expect(view.items).toHaveLength(2);
140
+ ```
141
+
142
+ Catches:
143
+ - Handler logic bugs (validation rules, branching, error returns)
144
+ - Auth predicate evaluation (handler called with wrong user/staff
145
+ context returns `AUTH_DENIED`)
146
+ - Input/output schema enforcement (caller-supplied `x-mantle-bind` field
147
+ is rejected; handler-returned shape mismatching `output` is
148
+ rejected)
149
+ - View result correctness against fixture data
150
+ - Cross-Trigger correctness when one Procedure has multiple bindings
151
+
152
+ The test harness is **not** mocked — it should share the real
153
+ dispatcher, validator, handler registry, and in-memory runtime ports.
154
+ The public `testing` export has not shipped yet; current v0.1.0 code
155
+ uses runtime package tests plus starter integration smokes to cover
156
+ the same contract until the consumer-facing harness is promoted.
157
+
158
+ #### Loop 2 — Boot-time fail-fast
159
+
160
+ Dispatcher build phase, after manifests are parsed and handlers are
161
+ registered, before the serve loop starts. Walks the entire manifest
162
+ graph and the in-memory handler registry; refuses to start serving if
163
+ anything is missing or inconsistent.
164
+
165
+ What it catches that Loop 1 cannot:
166
+ - `Procedure.handler.ref` that has a `sdk.registerHandler(...)` call
167
+ in source (Loop 1 saw it textually) but did not actually execute
168
+ (e.g. the registration file wasn't imported, the boot function was
169
+ skipped behind a feature flag)
170
+ - D1 schema drift — table state in the connected D1 does not match
171
+ the DDL derived from current manifests; SDK refuses to serve until
172
+ a migration is applied or the drift is acknowledged
173
+ - Cross-manifest references that a recent deploy introduced without
174
+ the corresponding atom (e.g. updated Trigger pointed at a Procedure
175
+ that wasn't included in the deploy bundle)
176
+
177
+ Failure mode is **process exit non-zero**, not "log a warning and
178
+ serve anyway." A missing handler must not surface as a runtime 500 to
179
+ a customer; it must surface as a boot failure to the deploy pipeline,
180
+ which is visible to the AI author who just shipped.
181
+
182
+ #### Loop 3 — Runtime errors
183
+
184
+ Runtime diagnostics (see ADR-0008 and the design-atoms reference)
185
+ cover the cases that reach this far. The contract's job is to make
186
+ Loop 3 the layer that handles **input it had no way to predict**
187
+ (request shape, auth state, transient infrastructure failures), not
188
+ the layer that handles **author-side mistakes** (those are Loops 1
189
+ and 2's job).
190
+
191
+ #### Phase ordering rationale
192
+
193
+ The author's lifecycle is:
194
+
195
+ ```
196
+ edit YAML → run validate (Loop 1) → run tests (test harness)
197
+ → push → deploy boots (Loop 2) → serves (Loop 3)
198
+ ```
199
+
200
+ The earlier a class of error is caught, the cheaper it is to fix and
201
+ the smaller the surface area for surprise. An author waits seconds for
202
+ `validate`, seconds-to-minutes for the test suite, minutes for boot,
203
+ hours-to-days for runtime errors to be reported. The discipline is:
204
+ **never let a class of error surface in a later loop than where it
205
+ could have been caught**. If the validator could find it, boot must
206
+ not be the first to find it.
207
+
208
+ An earlier draft of this contract ordered Boot before Test. That was
209
+ wrong: tests run during the AI's edit cycle, on the AI's machine,
210
+ inside the same turn. Boot runs only after a deploy is committed.
211
+ Tests must be left of Boot in the diagram and left of Boot in the
212
+ discipline.
213
+
214
+ ### Part B — Role-split surfaces
215
+
216
+ (Folded in from POC ADR-0013, which was a separate ADR in the POC tree
217
+ and is consolidated here in v0.1.0.)
218
+
219
+ Two AI agents read and write to a `@aotter/mantle-*` deployment:
220
+
221
+ 1. **The coder agent** — Claude Code (cc) running inside the
222
+ consumer's repo. Edits manifests, handlers, scaffold files,
223
+ `wrangler.toml`. Runs CLI commands (`mantle validate`,
224
+ `mantle emit-openapi`). Reads skill files. Has shell, filesystem, git,
225
+ and network access in the consumer's dev environment.
226
+
227
+ 2. **The operator agent** — a content-editing client (Claude Desktop,
228
+ claude.ai, or any MCP-speaking host) connected to the deployed
229
+ Worker's `/mcp` endpoint over OAuth. Calls the Day-1 MCP tool
230
+ catalog: generic tools (`list_entries`, `get_entry`,
231
+ `request_publish`, `unpublish_entry`, `archive_entry`) plus
232
+ per-collection authoring tools (`create_draft_<collection>`,
233
+ `update_draft_<collection>`). Has *only* the MCP tools the Worker
234
+ exposes; no shell, no filesystem, no git.
235
+
236
+ These are different roles with different *capability surfaces*. The
237
+ coder agent has many more capabilities than the operator agent *by
238
+ virtue of where it runs*, not because anyone designed the asymmetry —
239
+ it's a property of the environments. cc can run an arbitrary command
240
+ in the consumer's repo; an MCP-only client cannot.
241
+
242
+ Without an explicit principle, every feature decision can be expressed
243
+ on either surface. "Should `mantle validate` also be an MCP tool?"
244
+ "Should `update_draft` also be a CLI command?" Answered ad-hoc, the
245
+ two surfaces drift toward feature parity, which destroys the role
246
+ boundary, which destroys the security and DX properties the boundary
247
+ protects.
248
+
249
+ The SDK has **two surfaces, mapped to two roles**:
250
+
251
+ | Surface | Audience | Trust | Default access |
252
+ |---|---|---|---|
253
+ | **Skills + CLI + SDK TS API** | the coder agent (cc) | unconstrained — runs in consumer's repo | filesystem, shell, git, full SDK |
254
+ | **MCP tools** | the operator agent (MCP client) | constrained — ops session over OAuth | only the verbs declared in the Day-1 catalog |
255
+
256
+ The two surfaces are **not symmetric** and should not become
257
+ symmetric. Each new feature picks **one** surface based on which role
258
+ needs it; if the answer is "both," the feature is fundamentally about
259
+ content state (in which case it belongs on MCP and the coder uses the
260
+ SDK TS API instead of needing a matching MCP tool).
261
+
262
+ #### What goes on Skills + CLI + SDK TS API (coder surface)
263
+
264
+ - Anything that touches the **manifest set** (Schema / View /
265
+ Procedure / Trigger declarations). The coder edits YAML; the
266
+ operator never sees the manifest layer.
267
+ - Anything that requires **shell or filesystem access** —
268
+ scaffolding, test running, deploy.
269
+ - Anything **dev-loop shaped** — the three feedback loops above
270
+ (validate / boot / runtime) plus the test harness are
271
+ coder-surface concerns, not operator-surface.
272
+ - Anything that **mutates code** in the consumer's repo — registering
273
+ handlers, wiring imports, updating `wrangler.toml`.
274
+ - **Skill files** carrying domain context. cc reads these; MCP clients
275
+ do not.
276
+
277
+ Concrete artifacts today:
278
+ - `skills/<name>/SKILL.md` files in the
279
+ [`aotter/mantle`](https://github.com/aotter/mantle)
280
+ repo, discoverable by URL. Distribution as a Claude plugin is
281
+ **optional** (a v0.1.x convenience), not required — the canonical
282
+ location is the in-repo path, and any agent that can fetch a URL
283
+ can consume them.
284
+ - `mantle validate` CLI (shipped in `@aotter/mantle-spec`)
285
+ - `mantle emit-openapi` CLI (likewise)
286
+ - Public testing harness (planned; current coverage is SDK tests +
287
+ starter integration smokes)
288
+
289
+ #### What goes on MCP (operator surface)
290
+
291
+ - **CRUD on content state** — the Day-1 generic tools plus the
292
+ per-collection create/update tools generated from Schemas.
293
+ - Anything an **operator user** would do in a hypothetical WordPress
294
+ admin: write a post, update a draft, request publish, list
295
+ submissions.
296
+ - Future ops-role verbs that don't exist yet but have an obvious
297
+ operator use case ("translate this draft", "schedule for Friday").
298
+ When added, they land as **new SDK builtin tools** — declared in
299
+ the MCP catalog source under the closed Day-1 set, not
300
+ auto-generated from consumer Procedures.
301
+
302
+ #### What goes on neither (the SDK TS API only)
303
+
304
+ A small third category exists: APIs that only the **runtime itself**
305
+ consumes (the dispatcher, the boot validator, the diagnostic types).
306
+ These are exported from `@aotter/mantle-spec` for the coder to
307
+ import inside their handler code, but are not CLI commands and not
308
+ MCP tools — they're library types.
309
+
310
+ #### What does NOT go on either surface
311
+
312
+ - **Consumer Procedures auto-emitted as MCP tools.** This was
313
+ considered as `Trigger.source.kind: mcp` and rejected. A
314
+ consumer-defined Procedure carries arbitrary code; auto-emitting it
315
+ to MCP would let the operator agent trigger arbitrary side effects
316
+ defined by the coder agent. The role boundary collapses. Ops-role
317
+ verbs land as SDK builtins, not as auto-bound consumer Procedures.
318
+ - **CLI commands that mutate runtime state.** The CLI is for the
319
+ pre-deploy authoring loop. Once deployed, runtime state changes via
320
+ MCP (operator) or via direct SDK calls inside handler code
321
+ (developer). No `mantle publish-entry <id>` is planned — that
322
+ would mix the two surfaces.
323
+
324
+ ## Consequences
325
+
326
+ ### Pros
327
+
328
+ - Errors land in the channel the coder agent is watching at the
329
+ moment they author. Validation errors → terminal output of the CLI
330
+ they just ran. Test errors → test runner output of the suite they
331
+ just executed. Boot errors → deploy pipeline log the deploy is
332
+ blocked on. Runtime errors → customer-visible failures (the only
333
+ place runtime errors should appear).
334
+ - One diagnostic format (ADR-0008) across all loops means the coder
335
+ agent writes one parser, not three.
336
+ - Test harness as a public SDK export means consumer apps can ship
337
+ handler tests as part of normal CI without wiring up their own
338
+ dispatcher harness.
339
+ - Boot fail-fast turns a class of "runtime 500 reaches customer"
340
+ failures into "deploy blocked, fix forward" failures.
341
+ - The framing — coder agent as primary author, operator agent as
342
+ bounded MCP consumer, with explicit phase ordering — gives reviewers
343
+ a yardstick for proposed SDK features: "which loop does this
344
+ affect, and which surface owns it?" New error types must declare a
345
+ code.
346
+ - Permission boundary maps cleanly to surface boundary. The operator
347
+ agent's permissions are bounded by the MCP tool catalog; the coder
348
+ agent's by what shell + git + the SDK can do. Neither agent can
349
+ escalate by switching tools.
350
+ - MCP catalog stays narrow as a **property**, not a discipline.
351
+ Adding things to MCP requires demonstrating an operator-role use
352
+ case.
353
+
354
+ ### Costs
355
+
356
+ - v0.1 ships more than just a runtime: also the `mantle validate`
357
+ CLI, a public testing module, and a boot validator. Roughly +30%
358
+ of the v0.1 dispatcher work.
359
+ - Test harness needs an in-memory D1 substitute (most plausible:
360
+ `better-sqlite3` or an equivalent SQLite binding running the same
361
+ DDL the production D1 adapter applies). Adds a dev-dependency to
362
+ the SDK and a compatibility surface to keep honest against real D1
363
+ quirks.
364
+ - Diagnostic format is locked early (ADR-0008); any field-shape
365
+ change forces a doc revise and a code change across all loops.
366
+ - The error catalog (one named code per failure mode, per loop) must
367
+ stay synchronized with the validator/harness/runtime
368
+ implementations. A regression where the validator and the boot
369
+ loop emit different codes for the same underlying issue would
370
+ defeat the point.
371
+ - Two surfaces means two surfaces. Documentation, releases,
372
+ versioning, and contracts apply to both. Mitigated by the fact
373
+ that the two surfaces have little overlap by design — most features
374
+ touch only one.
375
+ - No cross-surface convenience. A hypothetical "consumer defines a
376
+ Procedure that's also an MCP tool with one YAML block" would be
377
+ ergonomically nice but is structurally rejected by Part B.
378
+
379
+ ### Risks
380
+
381
+ - **Validator under-approximates** (rejects valid manifests): authors
382
+ will route around it (`--no-validate` etc.), defeating the purpose.
383
+ Mitigation: validator MUST err on the permissive side; ambiguous
384
+ cases emit `severity: warning` not `error`; exit code 0 unless an
385
+ error is found.
386
+ - **Test harness diverges from runtime**: in-memory SQLite has
387
+ slightly different SQL semantics (collation, type affinity, JSON1
388
+ edge cases) from production D1. Tests pass here, prod fails. Same
389
+ trap as mocking the database. Mitigation: SDK itself runs starter
390
+ integration tests against real D1 in CI; document the
391
+ known-divergence list in the testing module's README.
392
+ - **Boot fail-fast aggressiveness**: if boot refuses to start on
393
+ cosmetic issues (a Schema property has an unrecognized but harmless
394
+ `x-` extension), the deploy pipeline becomes hostile. Mitigation:
395
+ boot validates only **load-bearing** invariants (missing handler
396
+ refs, dangling Trigger targets, schema drift). Cosmetic / advisory
397
+ checks are warnings via `--strict` flag, not errors.
398
+ - **AI authors over-trust the contract**: "the SDK said it was fine
399
+ therefore it is fine." Loop 3 (runtime) still exists for reasons
400
+ the contract cannot prevent. Mitigation: docs make clear that
401
+ runtime errors are still possible and that the contract reduces —
402
+ not eliminates — surface area.
403
+ - **Drift via "MCP-ifying everything".** The MCP transport is trendy;
404
+ there's pressure (industry-wide) to expose every API as MCP.
405
+ Following that trend would erase the role boundary. Mitigation:
406
+ this ADR is the bar to clear. Any "expose X as MCP" PR has to
407
+ argue X is operator-role.
408
+ - **Confusion when cc *uses* MCP tools to inspect a deployed
409
+ worker.** cc CAN call MCP tools (it has MCP-tool capability). This
410
+ isn't a violation — when cc calls MCP tools, it's playing the
411
+ operator role to inspect runtime state, not extending the coder
412
+ surface. The principle is about which surface a feature is
413
+ *exposed on*, not who calls into it.
414
+ - **Future Anthropic SDK changes that blur the line.** If Claude
415
+ Code gains MCP-tool-only modes, or if MCP clients gain shell
416
+ access, the role/surface mapping might need revision. This ADR is
417
+ grounded in the current capabilities asymmetry; if that asymmetry
418
+ collapses, supersede with a new ADR.
419
+
420
+ ## Alternatives considered
421
+
422
+ **(a) Runtime errors only.**
423
+ Rejected: primary author is AI; CF deploy logs are an inappropriate
424
+ UI for that author. Failures must land in the loop the author is
425
+ currently in.
426
+
427
+ **(b) Static validation only.**
428
+ Rejected as insufficient: cannot catch handler logic bugs, cannot
429
+ verify that a `registerHandler` call actually executes (only that it
430
+ appears textually), cannot test data-flow against fixtures.
431
+
432
+ **(c) Test harness only.**
433
+ Rejected: better than (a) but still lets misconfigurations land in
434
+ deploys (handler renamed but not re-registered, manifest references
435
+ stale schema name). Static is cheaper and finds these faster.
436
+
437
+ **(d) Skip the test harness; rely on consumer's own integration
438
+ tests.**
439
+ Rejected: it is the SDK's job to make handler-level testing trivial;
440
+ pushing the dispatcher-spin-up burden to every consumer is exactly
441
+ the kind of boilerplate that demotivates writing tests in the first
442
+ place.
443
+
444
+ **(e) Prose error messages.**
445
+ Rejected: see ADR-0008. AI parseability outweighs human readability
446
+ for the primary author; a `--format=text` mode preserves the human
447
+ option without locking the structure.
448
+
449
+ **(f) Feature parity across both surfaces.** Every CLI command also
450
+ an MCP tool; every MCP tool also a CLI command. Rejected: this
451
+ destroys the role boundary by giving each agent all capabilities.
452
+ The "operator agent has fewer capabilities than the coder agent"
453
+ property would no longer be a structural fact about the surfaces; it
454
+ would be a discipline imposed on individual operator sessions, which
455
+ we have no way to enforce.
456
+
457
+ **(g) MCP-only — drop the CLI.** All SDK operations as MCP tools.
458
+ Rejected: cc loses the sub-second feedback loops that depend on CLI
459
+ tools (validate, typecheck, build). MCP tool calls are slower (HTTP
460
+ round trip, JSON-RPC envelope), and harder to integrate into editor
461
+ / filesystem contexts where cc lives.
462
+
463
+ **(h) CLI-only — drop MCP.** All operations as CLI; the operator
464
+ agent shells out. Rejected: the operator agent (an MCP-only client)
465
+ can't shell out. MCP exists precisely because the operator-role
466
+ environment doesn't have a shell.
467
+
468
+ ## How to apply
469
+
470
+ When proposing a new SDK feature or behavior, declare:
471
+
472
+ 1. **Which agent runs this?**
473
+ - If the coder agent → CLI subcommand and/or SDK TS API and/or
474
+ SKILL.md section.
475
+ - If the operator agent → MCP tool added to the Day-1 catalog.
476
+ - If "both" — recheck. The feature is probably fundamentally a
477
+ CRUD-on-content verb (operator → MCP, coder uses the SDK TS API
478
+ directly) or a manifest-side authoring concern (coder →
479
+ CLI/skills, operator never sees it). Real "both" cases should
480
+ be vanishingly rare.
481
+
482
+ 2. **Which loop owns the failure mode?** (Loop 1 static, Loop 2
483
+ boot, Loop 3 runtime)
484
+
485
+ 3. **Could it move left?** (e.g. a runtime check that could be a
486
+ boot check, a boot check that could be a static check)
487
+
488
+ 4. **Does it emit the structured diagnostic shape?** (ADR-0008)
489
+
490
+ 5. **Can a consumer reproduce the failure offline?** (i.e. is it
491
+ visible to Loop 1 or to the test harness, not only Loop 3)
492
+
493
+ 6. **Does the feature mutate the consumer's repo?** → CLI (or SDK
494
+ API the CLI uses). Never MCP — the operator agent is not in the
495
+ consumer's repo.
496
+
497
+ 7. **Does the feature observe / mutate runtime state in the deployed
498
+ Worker?** → MCP tool, with the ops-role description in the
499
+ tool's `description` field. Or HTTP Trigger + handler if the
500
+ feature is consumer-shaped (a Procedure the consumer wrote).
501
+ Don't auto-bridge consumer Procedures to MCP.
502
+
503
+ Reviewers should treat "this lands as a runtime 500 only" as a
504
+ yellow flag — sometimes correct (genuine runtime conditions), often
505
+ a sign that earlier loops were not considered. Likewise, when a PR
506
+ adds either an MCP tool or a CLI command, check whether the *other*
507
+ surface tries to mirror the change. If yes, that's a flag — push
508
+ the proposer to pick one surface or to articulate a real two-role
509
+ use case.