Skip to content

Protocol declarations

The declaration models in axm_init.models.protocol_scaffold describe the components needed to scaffold a protocol. They validate names and references before any files are generated.

Protocol model

ProtocolScaffoldDecl requires the three graph-name segments, contracts, and nodes. Its graph_name property joins domain, unit, and action with dots. This name is independent from an optional ticket type.

Prompts, phases, and the complete ticket block may be omitted. Omitted collections default to empty lists.

Component Required fields Optional fields
ContractDecl name —
PromptDecl name, text —
NodeDecl name contract, prompt
PhaseDecl name nodes (defaults to an empty list)
TicketDecl ticket_type, input_contract —

A ticket block is optional as a whole. When present, its input_contract remains required.

Reference validation

Every supplied reference must resolve inside the same declaration:

  • a node contract names a declared contract;
  • a node prompt names a declared prompt;
  • each phase node names a declared node;
  • a ticket input contract names a declared contract.

Validation errors include the unresolved reference. Component names must also be unique within each list, and generated model or factory names must not collide.

All name segments must be safe lowercase Python identifiers. Path separators, parent traversal, absolute paths, reserved words, and extra fields are rejected.

Scaffold, preview and application surface

init_scaffold accepts protocol declarations through the same AXMTool signature used by MCP and the generated CLI. Set profile="protocols" and a domain when creating a standalone Python package or workspace member; the tool records [tool.axm-init.protocols] in that package's pyproject.toml. The structured result includes the derived distribution name, package root, and creation mode.

Select kind="protocol_unit" for a new unit or kind="protocol" when targeting an existing unit. In both cases, also provide unit and one or more action-only payloads in protocols. Set preview=true to inspect the plan without mutation; leave it false to apply that exact plan. The shared domain and unit belong to the request and are injected into every payload before validation as a strict ProtocolScaffoldDecl. The action then completes each qualified graph name; plan_protocol_scaffold is the sole source of the relative operations and rendered contents.

The preview result has the same structured shape through direct AXMTool, MCP, and CLI calls:

Field Meaning
profile Selected profile (protocols)
mode unit for protocol planning or application; standalone or member for package creation
root Absolute target package root
preview true for a non-mutating plan; false after application
created, updated, unchanged, conflicts Relative paths grouped by planner status
protocols Qualified logical graph names

Preview never applies planned file contents or merged metadata. Application creates or updates only paths carried by the plan and writes the merged [tool.axm-init.protocols] metadata; it does not invoke a standalone-project template. Reapplying an owned plan leaves unchanged files byte-for-byte, including skeletons extended by a compatible implementation.

Before reading its filesystem snapshot, application acquires a lock keyed by the fully resolved target root and holds it through preflight, writes, and any rollback. Concurrent applications through aliases of one root therefore cannot silently overwrite compatible declarations, while applications to distinct roots remain concurrent. Application then resolves every destination and rejects paths outside the root, outward-pointing symlinks, conflicts, and incompatible occupied paths. If a file or metadata write fails after application begins, it removes paths created by that operation, restores the original file and metadata bytes, and releases the root lock. This is process-local application-level coordination and rollback, not cross-process or crash-safe atomicity.

Invalid request combinations are also rejected before any write: protocol options without a profile, an empty protocol list when unit or preview is set, the profile on a non-Python framework, or protocol declarations without a unit.

Example

Python
from axm_init.models.protocol_scaffold import ProtocolScaffoldDecl

declaration = ProtocolScaffoldDecl(
    domain="dev",
    unit="work",
    action="exec",
    contracts=[
        {"name": "work_request"},
        {"name": "work_result"},
    ],
    prompts=[
        {"name": "implement", "text": "Implement the requested change."},
    ],
    nodes=[
        {"name": "prepare"},
        {
            "name": "implement",
            "contract": "work_result",
            "prompt": "implement",
        },
    ],
    phases=[
        {"name": "build", "nodes": ["prepare", "implement"]},
    ],
    ticket={
        "ticket_type": "engineering.change",
        "input_contract": "work_request",
    },
)

assert declaration.graph_name == "dev.work.exec"

Ticket declaration check

check_protocol_ticket(project: Path) -> CheckResult in axm_init.checks.protocols is discovered as protocols.protocol_ticket by CheckEngine. Request it through the explicit protocols category. It has weight 2 when a declared domain has its protocol module root; otherwise it returns a passing, zero-weight result. Omitting both a ticket announcement and its file is valid.

Defect Diagnostic location and correction
Action announces ticket_type, but its ticket.py is absent The announcement's pyproject.toml line and expected file path; create the file or remove the announcement
ticket.py exists without an action announcement The TICKET_TYPE line, or line 1 if absent; declare ticket_type in the action metadata or remove the file
A present INPUT_CONTRACT references an undeclared contract The assignment line; select a name from that action's contracts inventory
A present GRAPH_NAME differs from the path-derived identity The assignment line, expected and observed names; use <domain>.<unit>.<action>

Reference checks inspect the constants present in the typed variable inventory. They require literal values; unresolved expressions fail those checks. They do not require an absent INPUT_CONTRACT or GRAPH_NAME assignment to be added. The planner generates TICKET_TYPE and INPUT_CONTRACT; it does not currently add GRAPH_NAME to ticket.py. Ticket type identifiers are independent of graph names, and this rule does not compare the value of TICKET_TYPE with the metadata value.

Inspection uses axm-ast module and variable information without importing the inspected package. At a workspace root, the existing engine aggregation keeps the canonical rule name and prefixes member findings with their member identity. See checking and repairing declarations.

Shared unit tickets (schema version 2)

The public InitScaffoldTool.execute API accepts an optional unit_declaration object (or JSON object string). It contains shared contracts and one ticket binding; domain and unit come from the request. The strict ProtocolUnitDecl model validates the binding:

JSON
{
  "contracts": [{"name": "run_context"}],
  "ticket": {
    "ticket_type": "ml.run",
    "input_contract": "run_context",
    "entry_action": "exec"
  }
}

Each consuming action declares "unit_ticket": "ml.run", alongside its usual action-local components. A local ticket and unit_ticket are mutually exclusive. The reference resolves only within the requested domain and unit, including declarations persisted by a previous request. A later action can therefore reference the same ticket without repeating unit_declaration.

The planner generates one shared context under src/protocols_ml/run/contracts/run_context.py and one factory under src/protocols_ml/run/ticket.py. Each consuming action's protocol.py explicitly imports the shared context and get_ticket_type; there is no action-local ticket copy. The factory returns the public protocols_core.TicketTypeDef with independent identities:

  • ticket type: ml.run;
  • context model: RunContext;
  • entry graph: ml.run.exec, selected by required entry_action.

The entry action must exist in the requested or persisted action inventory. Shared contract names and derived model names must be unique. A changed persisted unit declaration, unresolved/cross-unit reference, or a local ticket claiming a shared type is refused before filesystem mutation. Existing unowned shared files produce plan conflicts. Reapplication keeps implemented user files and the ready state of an unchanged action binding.

Shared unit metadata uses schema_version = 2, with contracts, ticket_type, input_contract, and entry_action on the unit table, and unit_ticket on consuming action tables. Old per-action declarations remain supported in versions 1 and 2. Reapplying an old action never downgrades a version-2 profile or converts its ticket to a shared binding.

Scaffolding does not register the type or create a runnable graph: actions remain draft. Once the entry graph and shared context are implemented and the entry action is ready, registration is explicit and unique:

TOML
[project.entry-points."axm.tribunal.graphs"]
"ml.run.exec" = "protocols_ml.run.exec.protocol:build_protocol"

[project.entry-points."axm.tribunal.ticket_types"]
"ml.run" = "protocols_ml.run.ticket:get_ticket_type"

Static checks resolve the shared context, factory, constant identities, consumer imports and entry graph. A registration while the entry is draft, a second factory alias, a legacy axm.ticket_types copy, or an incorrect entry-graph target fails the checks. Ready consumers also require an implemented shared context. No runtime Tribunal dependency is added to the minimal axm-init package: generated code is inspected statically. The Forge repository's explicit qualification at qualifications/protocols/README.md checks the generated factory against the real protocols environment; it is not part of standard package CI until protocols-core is published. The generic generated definition retains TicketTypeDef defaults; domain-specific authors may set additional public factory options such as repository_writes=False without changing the shared binding.

Registration and declared state

The explicit category also checks factory registration and ready-state skeleton markers. Its tool result exposes declared state separately from validation. See protocol registration and declared state for the canonical rules, static resolution boundaries and structured result fields.

Current interface notes

Unit and protocol requests require an existing package with a readable pyproject.toml and an already-declared [tool.axm-init.protocols] profile whose domain matches the request. The tool rejects missing profiles or conflicting domains before mutation; profile adoption remains a lower-level capability, not an implicit effect of these tool requests. The orchestrator processes actions sequentially against a virtual inventory, so later actions see earlier planned content.

Validate the materialized package explicitly with:

Bash
axm init_check /path/to/package --category protocols

This static check requires schema version 1, a distribution named axm-{domain}, a src/protocols_{domain} module root, and wheel packages configuration that includes that protocol root. For every declared action, it also requires protocol.py, the package initializers, and the contracts/, nodes/, prompts/, and phases/ directories generated by the planner.

The component inventory is checked in both directions: each declared contract, node, prompt, or phase must have its corresponding local file, and every local component file must be declared in metadata. Findings distinguish declared components missing from disk from local components missing from the inventory, and name the component and corrective path.

When the same explicit category is run on a uv workspace root, the check evaluates its members that declare the protocol profile. A member layout error fails the workspace result, whose details retain the member name. Workspaces without a profiled member remain non-applicable. Every finding carries a file location and a correction; inspected modules are parsed with axm-ast, never imported.

ScaffoldResult also carries success, path, message, files_created and optional distribution. The human message currently remains Protocol scaffold preview even on application; preview is the authoritative mode flag. A preview can report conflicts without applying them.

Rollback and the per-root lock are process-local; they do not coordinate writers in separate processes or supply a durable recovery journal. See the operational guide.