Planfile exposes versioned Python contracts for analyzers and autonomous orchestrators. These contracts separate a tool's proposal from the authority to create, schedule or execute a ticket.
planfile.contracts.TicketProposalV1 is a strict, side-effect-free proposal.
Unknown fields are rejected. In particular it has no queue, sprint, executor,
capability, approval, transport or URI field.
from planfile.contracts import TicketProposalV1
proposal = TicketProposalV1.model_validate({
"schema": "planfile.ticket-proposal.v1",
"proposal_id": "code2llm:src/service.py:Service",
"dedupe_key": "symbol:src/service.py:Service",
"name": "Split Service responsibilities",
"priority": "high",
"source": {
"tool": "code2llm",
"tool_version": "0.9.0",
"finding_id": "src/service.py:Service",
},
"files": ["src/service.py"],
"acceptance_criteria": ["Focused tests pass"],
})
canonical_payload = proposal.canonical_json()
proposal_hash = proposal.proposal_hash
safe_ticket_fields = proposal.to_ticket_kwargs()Set-like fields are stripped, deduplicated and sorted before hashing. Ordered
acceptance criteria are stripped and deduplicated without changing their order.
The consumer remains responsible for deduplication, queue selection and
authorization before passing the safe fields to Planfile.create_ticket.
The JSON Schema for non-Python producers can be obtained from
TicketProposalV1.model_json_schema(by_alias=True).
planfile.client.PlanfileClient wraps lifecycle mutations and returns
TicketTransitionResult with a stable code:
okticket_not_foundinvalid_transitionlock_timeoutstore_error
from planfile.client import PlanfileClient
client = PlanfileClient("/workspace/project")
result = client.start("PLF-42", assigned_to="koru", actor="koru")
if result.code == "ok":
ticket = result.ticket
elif result.retryable:
schedule_retry(result.code)Lifecycle methods are:
claimandstartfor ownership and execution start;completefor verified success;failfor recording one failed attempt and its error;readyfor an explicit scheduler decision to reopen work;blockfor a human or external-state boundary;notefor additive evidence.
fail and ready are deliberately separate. Planfile persists state; it does
not assume that every error is retryable:
failed = client.fail("PLF-42", error="temporary upstream failure", actor="koru")
ticket = failed.ticket or {}
execution = ticket.get("execution") or {}
if failed.code == "ok" and execution.get("attempt", 0) < execution.get("max_attempts", 1):
reopened = client.ready(
"PLF-42",
note="Retry scheduled by koru",
actor="koru",
)
else:
blocked = client.block("PLF-42", reason="attempt budget exhausted", actor="koru")ready preserves the failure count but clears stale execution ownership,
timestamps, lease, and last error. Its returned ticket is runnable again with
status: open and execution.state: ready.
The client owns the bounded storage-lock retry. The caller owns the decision to perform a transition, its capability/grant checks and its retry budget outside the storage transport.
Compatibility policy: add a new version instead of weakening strict validation or changing the meaning of an existing result code.
Planfile.materialize_delivery_plan accepts the closed, execution-inert
subactor.compiled-work-plan/v1 DTO emitted by Strategy. It atomically assigns
stable Planfile ticket IDs to its candidates and persists the runtime projection
under .planfile/delivery-plans/<plan-id>.json. Replaying the same DTO returns
the same IDs; if power fails after the ticket batch lands but before the state
replace, the exact candidate hash bindings recover that batch without creating
duplicates.
state = planfile.materialize_delivery_plan(compiled_plan)
state = planfile.checkpoint_delivery_candidate(checkpoint_v1)
state = planfile.record_delivery_split(split_v1)
state = planfile.record_delivery_terminal_receipt(plan_id, terminal_receipt_v1)
frontier = planfile.resume_delivery_plan(plan_id)Checkpoints have an explicit sequence and phase and bind the plan hash,
candidate digest and evidence references. Splits bind a split_required parent
to at least two already-materialized bounded children and rewire its dependents.
Terminal receipts are immutable and deduplicated by their complete protected
binding. A conflicting checkpoint, receipt, plan revision or candidate digest
fails closed.
The returned resume frontier is a deterministic projection of those structured
records. Planfile does not infer completion from chat prose and the contracts
reject command, executor, grant and tool-authority fields. Materialization
creates tickets with executor=None; selecting an agent or executing tools
remains a separate authorized runtime concern.
The persisted format is published as
planfile/schemas/delivery-plan-state.schema.v1.json. The state file and its
ticket YAML/event journals are authoritative continuity data and must be backed
up together; disposable SQLite and fast-JSON indexes are not substitutes.
POST /tickets/{ticket_id}/evidence is the retry-safe write contract for an
external effect that has already happened. It atomically appends one evidence
event, notes and artifact references under the ticket store lock. The event is
durable in .planfile/evidence/<ticket-id>.jsonl and is projected into the
ticket's outputs on every read; the endpoint does not rewrite the complete
sprint snapshot. The required idempotency_key is persisted with the evidence
and suppresses duplicates when a caller retries after an ambiguous transport
timeout.
The same key and the same evidence is a successful deduplicated retry. The same
key with different evidence is rejected with HTTP 409
evidence_idempotency_conflict; accepted receipts are immutable.
Callers must not use generic PATCH /tickets/{ticket_id} with a previously read
outputs object for this purpose: that read/modify/write sequence can overwrite
evidence written concurrently by another executor.
The evidence journal and sprint YAML are complementary durable sources and must
be backed up together as the .planfile directory. A projection cache or JSON
mirror may be deleted and rebuilt; an evidence journal must not be discarded.