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
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:
{
"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 requiredentry_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:
[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:
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.