crear:tarea
- mutating
- requiresConfirmation
- scopes: tarea:crear
- handler: TaskService::create
Build it up capability by capability. The field accepts a module only when everything it requires is already planted.
The stalk is ready to receive modules.
Field ready
Select a seed or drag it to the field.
The game represents modules as nodes and their capabilities as edges. Each module can also be planted from the keyboard.
What you just did
Capability validation, Kahn topological sort, and cycle detection. The real runtime lists the modules in the cycle; the A → B → A path here is a visual explanation.
docs/GUION-WEBINAR-JUNIORS.md · getmilpa-resolver/src/Engine/GraphResolver.php:895-987 (computeLoadOrder, the real Kahn pass) · getmilpa-runtime/src/Kernel.php:156-177 (the gate and the loadOrder boot).
CLI and MCP reach the same action through the same execution mechanism. The channel changes the context and can change the policy; the pipe is shared.
enviar_correo
One declared definition; two callers with explicit contexts.
Waiting for caller
The same action can enter through CLI or through tools/call.
Same pipeline doesn't mean same permission
The runtime receives a ToolContext. The current configuration allows CLI by default and requires authentication for MCP; both paths do converge in ToolRegistry::call().
ToolRegistry.php:348-570 · JsonRpcService.php:282-300 · PolicyGate.php:34-56. The buttons simulate callers; they don't represent commands shipped in the skeleton today.
A sensitive action stays pending until it receives an explicit verdict. Approve, reject, and waive produce different data.
Two related contracts, not a single call
VerificationResult models PENDING/PASSED/FAILED/WAIVED. Self-approval prevention lives in the gate flow; justified waiving lives in workflow. This screen brings them together to teach the system.
VerificationStatus.php:17-48 · HumanGate.php:101-165 · ProcessSubmitDecisionTool.php:63-99.
A map of responsibilities and declared dependencies. Choose a path to see which packages take part without collapsing the ecosystem into a single box.
The arrows are declared dependencies
They don't try to infer every runtime call. In MCP, HTTP/SSE/stdio belong to the host; JsonRpcService only knows decoded arrays.
getmilpa-runtime/composer.json · getmilpa-orchestrator/composer.json · JsonRpcService.php:25-37 · ComponentDefinitionInterface.php:23-63 · milpa-design/package.json:11-42.
The eleven-step walk is a good entry point. The real implementation adds boundaries that matter for security, cost, confirmation, and interception.
Select a path
Audit coverage changes depending on the return point.
| Output | Callback | Audit evidence |
|---|---|---|
| Nonexistent tool | No | no explicit log |
| Invalid args | No | validation failure |
| Policy denied | No | auth failure |
| Plan / confirmation | No | no explicit log |
| Cache short-circuit | No | tool.executed |
| Pure veto | No | known gap |
| Callback / throw | Yes | executed / failed |
| Step | Role | Presence | Source |
|---|---|---|---|
| Resolve | Guard | Active | registry lookup |
| Validate | Guard | Active | tool declares inputSchema |
| Clamp | Transform | Skipped | tool declares no clamps |
| Authorize | Guard | Active | channel 'web': require_auth=true, allow_all=false; tool declares no scopes; DB rules: skipped (no provider) |
| Rate limit | Guard | Skipped | host wiring: rateLimiter absent; cost mutating?5:1 (mutating=false → cost 1) |
| Plan mode | Branch | Conditional | fires if ctx.mode is 'plan'; current value: execute |
| Confirm | Branch | Dormant | neither tool.requiresConfirmation nor the channel policy require confirmation for this tool |
| Emit executing | Hook | Skipped | host wiring: dispatcher absent (anchor + cache/veto) |
| Execute | Execution | Active | callback; _ctx injected |
| Contain exception | Boundary | Active | wraps execute; \Throwable → INTERNAL_ERROR |
| Audit | Outcome | Active | audits: validate-fail, authz-fail, rate-limit, cache-hit, execute-success, execute-failure; does NOT audit: resolve-miss, plan-mode, confirm, veto |
The guarantee reads per branch
“Everything is audited” doesn't describe every early return today. This artifact makes both the implemented guarantees and the gaps visible.
getmilpa-tool-runtime/src/ToolRegistry.php:348-570 · wording in ToolAuditLogger.php:45-57,204-232.
No current_state is stored. Facts are appended and a pure reducer reconstructs the state by replaying them in order.
the state doesn't exist as a persisted column — it's re-derived from the log at each cut
Minimalist on purpose
The current file store rescans JSONL for replay and sequence; it's useful for grasping the pattern and light persistence, and shouldn't be presented as a large-scale concurrent event store.
EventStoreInterface.php:7-42 · FileEventStore.php:25-83 · orchestrator/Reducer.php:19-68.
Visual intent doesn't end in a document: tokens, contracts, and contrast pairs pass through repeatable generators and gates.
tokens/milpa-tokens.json concentrates primitives and semantics.
Generates CSS, a Tailwind preset, and theme.contract.json.
Contrast, governance, layers, drift, and skins.
Loads six bundles and can override tokens by contract.
AA passes
The gate measures the effective color, not the alpha value in isolation.
Concrete guarantee, concrete scope
verify-governance validates basic shape and tokens, not the whole JSON Schema. verify-theme directly enforces the opacity of --bg; other invariants stay documented.
scripts/build-tokens.mjs:189-265 · verify-governance.mjs:38-98 · verify-contrast.mjs:28-42 · verify-theme.mjs:107-149.
The generator returns planned files in memory. That lets you inspect the result and run a collision preflight before writing.
entity Product
fields: nombre:string:120,
precio:decimal:10,2final class Product implements EntityInterface
{ public string $nombre; }
{ public Decimal $precio; }Produces PlannedFile(path, contents).
Aborts before writing if a target exists.
Creates directories and writes contents.
Entity closes the loop against runtime rules.
Preview possible, CLI pending
GenerationResult already makes the plan inspectable, but the current skeleton doesn't expose --dry-run: it runs preflight, writes, and then verifies entities. WriteGuard prevents overwrite; it doesn't promise atomicity or rollback.
getmilpa-devtools/src/Make/GenerationResult.php:8-29 · PlannedFile.php:8-28 · WriteGuard.php:12-41 · getmilpa-skeleton/src/Console/Application.php:238-334 (makeController/makeEntity).
An operation is declared once. coa, MCP and HTTP are adapters of the same handler — but changing doors can change the policy.
crear:tarea
Coverage by surface: MCP and HTTP now enforce the same scopes: HTTP runs RequireScopeMiddleware and then the same PolicyGate MCP uses. coa does not enforce scopes — it's the local/trusted surface, by design. This was a real coverage gap; it closed when HttpProjector started requiring scopes. The Runtime x-ray shows the full, now-governed pipeline. Source: Operation.php, HttpProjector.php.
coa crear:tarea --titulo=… --yes
Pick a door to project.
tools/call · crear:tarea
Pick a door to project.
POST /crear/tarea
Pick a door to project.
| Surface | Confirm | Scopes applied |
|---|---|---|
| coa | flag --yes | no (local/trusted) |
| MCP | inherited gate (tool-runtime) | yes (PolicyGate) |
| POST | token 428→201 | yes (RequireScopeMiddleware + PolicyGate) |
The handler never changed. You changed doors and the framework synthesized the invocation — but the door can change the policy, and today not all of them apply the same scopes.
Didactic model over an audited implementation. This artifact does claim that HTTP applies scopes (via HttpProjector + PolicyGate) — it does not claim that the in-memory token store is production-grade.
milpa/command: getmilpa-command/src/Operation.php, CommandProvider.php, SurfaceProjector.php.skeleton ≥0.7.0, ns App\Command, not part of the package): getmilpa-skeleton/src/Command/{CliProjector,McpProjector,HttpProjector}.php.milpa/auth ≥0.1, RequireScopeMiddleware.php + the same PolicyGate MCP uses.ConfirmTokenStore.php (in-memory, single-process — not production), SchemaCoercer.php.docs/GUION-WEBINAR-JUNIORS.md (Artifact 2).When pure logic emits prose in one language, localizing it forces a choice about where the translation lives. Mapping at the boundary keeps the API intact — but it opens a leak class: it is airtight only if every consumption point goes through the map.
Prefer (a) when you can refactor the core: the guarantee comes from the compiler, not your memory. Use (b) when the API must stay still — and then treat it for what it is: a total-coverage contract.
The frontier below has several core outputs. Switch the demo's language to en and pick the output the map doesn't cover: the engine reports mapped:false and the consumer, with no translation, passes the raw code through — a Spanish literal inside the English view. One forgotten point is enough.
A frontier with eight core outputs and a map that translates to English. One key is missing on purpose.
| Core output | en · mapped |
|---|---|
sin iniciar |
not started mapped |
solicitado |
requested mapped |
esperando verificación |
awaiting verification mapped |
listo para ejecutar |
ready to execute mapped |
detenido |
leak |
ejecutando |
executing mapped |
completado |
completed mapped |
fallido |
failed mapped |
missing: detenido
With the missing key, the coupling fails: that is exactly what would break CI before it reaches production.
Add the missing key with Add to map. The output that was leaking now resolves to its translation and the English view comes out clean. You fixed the symptom; what comes next keeps it from coming back.
coupleCheck couples the codes the core emits with the keys the map translates. It reports missing (a code with no translation) and orphan (a dead key), and only says ok when coverage is total both ways. A new core state without its key breaks CI — it doesn't slip into production through the ?? code fallback.
While making this gallery bilingual, applyGateDecision localized the visible result (#gate-result) but the push to the audit trail kept using the core's raw prose (verdict.reason). On /en/, the self-approval flow showed a full Spanish sentence inside the audit trail. Review caught it by enumerating every output of the four functions and chasing each consumption point; the browser smoke missed it because it looked at the result, not the log. The fix pushed t.gateConstructionReason (localized) instead of verdict.reason.
This demo is the distilled pattern: a toy boundary so you can see the leak and close it. The audited implementation lives in artifacts.js (the PROJECTION_*_EN maps, the audit-trail fix) and artifacts-core.js (the neutral core) of this very gallery.
A boundary is total coverage, not a majority
The real defenses, in order: prefer neutral codes in the core when you can (the leak becomes impossible by construction); if you map at the boundary, couple the core enums to the map keys with a test that breaks CI on an untranslated state; and verify the full surface, not just the happy path you can see. It is the i18n twin of The gate: what the system guarantees by construction versus what it promises by discipline.
artifacts/artifacts.js (PROJECTION_*_EN maps, audit-trail fix) · artifacts/artifacts-core.js (frontierProject, coupleCheck) · tests/i18n-contract.test.mjs (enum↔map coupling) · docs/LESSON-CANDIDATES.md · commit 058fdf9.
The real boot doesn't start by executing: it starts by resolving. The kernel reflects the manifests, resolves the whole graph with milpa/resolver and only then decides — a blocked graph throws ArchitectureBlockedException with the ResolutionReport on board; a closed graph boots in the order the report itself carries: loadOrder[].
Pick a scenario. Each panel shows the REAL report the engine emitted for that graph, frozen from milpa/resolver 0.5.0.
Three packages and everything required has a provider: the graph closes and the status is valid. The report carries loadOrder[] — the same resolution that gated the graph also ordered it, so the boot order cannot diverge from what was validated.
milpa/config1.0.0milpa/correo0.3.0milpa/notificador1.2.0Nobody provides correo.transport. The resolver reports the gap as a learnable error and the status blocks: a partial order exists, but the gate doesn't open while the graph is open — the kernel throws ArchitectureBlockedException before booting anything.
milpa/config1.0.0milpa/notificador1.2.0Gate closed: the kernel throws ArchitectureBlockedException with this report on board — nothing boots, even though a partial order exists.
milpa/riego and milpa/siembra require each other in a circle: for them no boot order exists. The engine excludes the cycle members from loadOrder[] — the independent milpa/config keeps its place — and the status blocks the whole boot.
milpa/config1.0.0milpa/riego and milpa/siembra stay out of loadOrder[]: they are the cycle members.
Gate closed: the kernel throws ArchitectureBlockedException with this report on board — nothing boots, even though a partial order exists.
The milpa.json declares one architecture and the code's #[PluginMetadata] carries another. The report alone does NOT carry the drift: the engine never emits that code. DriftDetector detects it caller-side and coa:inspect architecture presents it next to the report — exactly like here.
| field | declared · milpa.json | actual · #[PluginMetadata] |
|---|---|---|
provides | inventario.precios | |
version | 1.0.0 | 1.1.0 |
milpa/inventario1.1.0Real output of GraphResolver::resolve() and DriftDetector::toLearnableErrors(); the exact PHP capture snippet lives as a comment next to each blob in this gallery's source.
{
"status": "valid",
"errors": [],
"resolved": [
{
"kind": "capability",
"id": "config.provider",
"constraint": "*",
"level": "required",
"requiredBy": "hostProfile:tienda-demo@2026.07",
"providedBy": "milpa/config@1.0.0",
"via": "direct"
},
{
"kind": "capability",
"id": "correo.transport",
"constraint": "*",
"level": "required",
"requiredBy": "hostProfile:tienda-demo@2026.07",
"providedBy": "milpa/correo@0.3.0",
"via": "direct"
}
],
"loadOrder": [
{
"name": "milpa/config",
"version": "1.0.0"
},
{
"name": "milpa/correo",
"version": "0.3.0"
},
{
"name": "milpa/notificador",
"version": "1.2.0"
}
],
"missing": [],
"conflicts": [],
"warnings": [],
"legacy": [],
"migrationHints": [],
"learnLinks": [],
"metadata": {
"hostProfile": "tienda-demo@2026.07",
"hostMetadata": []
}
}The gate is the boot's contract
The kernel doesn't collect loose checks: it delegates the whole verdict to milpa/resolver and obeys the report. If the graph doesn't close, the learnable error tells you what's missing, why it blocks and where to learn it; if it closes, loadOrder[] already is the boot order. The same resolution that validated also ordered — there are no two sources that could diverge.
getmilpa-runtime/src/Kernel.php:156-177 (the gate and the loadOrder boot) · getmilpa-resolver/src/Engine/GraphResolver.php · getmilpa-resolver/src/Ingest/DriftDetector.php · frozen reports from milpa/resolver 0.5.0 (the capture snippets are commented next to each JSON).