| name | acture-command-record-shape |
| description | Load the canonical CommandRecord shape and the closed-metadata-surface principle. Use when defining, modifying, reviewing, or extending the CommandRecord interface, when adding a new field, when removing/renaming a field, or when asked about command identity, schema, when-clauses, kind (atomic/handoff), tier, keybindings, or the Result<R> shape. Triggers on "CommandRecord", "defineCommand", "command metadata", "what fields does a command have", "add a field to commands", "the dispatch signature". Do NOT use for palette/UI behavior (load `acture-palette-design`) or migration helpers (load `acture-migration-package`). |
The acture CommandRecord shape
The CommandRecord is acture's central type. It is closed — fields cannot be added without three-caller validation. This skill is the canonical spec.
The spec
type CommandRecord<P = unknown, R = unknown> = {
id: string;
title: string;
description?: string;
category?: string;
icon?: string;
params?: StandardSchema<P>;
when?: string | ((ctx: Context) => boolean);
keybinding?: string | string[];
aliases?: string[];
kind?: "atomic" | "handoff";
tier?: "stable" | "experimental" | "internal" | "deprecated";
deprecationReason?: string;
internalToken?: symbol;
defaultScore?: number | ((ctx: Context) => number);
follow?: string[];
execute: (params: P, ctx: Context) => Result<R> | Promise<Result<R>>;
};
Result shape
type Result<R> =
| { ok: true; value: R; patches?: Patch[]; effects?: Effect[] }
| { ok: false; error: { code: string; message: string; details?: unknown } };
The patches? and effects? fields are the runtime contract consumed by acture-undo (shipped v1.11). Core itself ignores them — they're forwarded to the undo subsystem when one is installed. The pre-v1.11 framing ("reserved hooks for a future undo") is outdated.
Fields that are deliberately NOT on this record
These were proposed at various points and rejected. Do not add them.
| Field | Why not |
|---|
inputComponent?: unknown | UI components live in palette adapter config, not on commands. |
metadata: PolicyMetadata (readOnly, idempotent, riskLevel, requiresConfirmation) | The wrapex implementation had this bag. Rejected: too many ad-hoc fields, none load-bearing. readOnly/requiresConfirmation may return as top-level fields if three callers demand them. |
tags?: string[] | category + tier cover the use cases. |
isVisible?, isEnabled? callbacks | Folded into when. Use the function escape hatch. |
requiresConfirmation: boolean at top level | Confirmation is a middleware concern, gated by kind and tier. |
version?: string per-command | Pinned to consumer package version; per-tool semver deferred until SEP-1575 lands (research-5 §3). |
defineCommand helper
export function defineCommand<TParams, TResult>(
spec: CommandRecord<TParams, TResult>
): Readonly<CommandRecord<TParams, TResult>> {
validateAtRegistration(spec);
return Object.freeze(spec);
}
Registration-time validation MUST check:
id matches /^[a-z][a-zA-Z0-9]*(\.[a-z][a-zA-Z0-9]*)*$/ (namespaced dot-separated).
params (if present) is in the JSON-Schema-representable subset (no transform, date, bigint, set, map, custom).
when (if string) is parseable as DSL.
kind (if explicit) is one of the two enum values.
tier (if explicit) is one of the four enum values.
ID naming conventions
- Namespaced dots:
app.domain.action.
app. prefix configurable; default app.
- Action is camelCase, verb-first:
addNode, zoomToFit, applyFilter.
- One word per segment.
app.graph.addNode, not app.graph_model.add_node.
- Avoid abbreviations except universally known (
png, csv, sql).
Adding a new field
Before adding ANY new field:
- Name the concrete consumer(s) that need it — a specific command, adapter, or consumer surface. "What if someone wanted…" is not enough; a real named need is. (One named consumer can be enough if the use case is irreducible; more is fine but the test is concrete, not a count — see
docs/redesign_takeaways.md §6 on why a numeric callers gate is the wrong framing.)
- Confirm it cannot be done by composition (a wrapper function like
palettable(cmd, ...) or toolCallable(cmd, ...)).
- Confirm it doesn't introduce conditional logic into metadata (inner platform effect).
- Write the migration story: how does v1 code adopt the new field without breaking?
If any of these fails: do not add the field. Use composition.
Why the surface is closed
Per docs/redesign_takeaways.md §1.2 and the central paper §6.1. The Inner Platform Effect is the most dangerous risk: command metadata growing toward a mini-language with conditionals, inheritance, and dynamic composition. The guardrail is structural: keep the interface minimal and resist additions. If a use case requires conditional logic, it belongs in the handler, not the metadata.
See also
acture-schema-bridge — how params becomes JSON Schema / MCP tool / AI tool
acture-palette-design — how kind drives palette UX
acture-tier-system — how tier is enforced at runtime
docs/v1_plan.md §4 — same spec, with research-citation trail