Contracts

invocation-payload.yaml

title: InvocationPayload description: | The JSON response returned by spec-kitty advise, spec-kitty ask, and spec-kitty do. This payload is ephemeral — it is returned to the caller (typically a host LLM) but not persisted. The caller uses invocation_id to call spec-kitty profile-invocation complete when work is done. version: "1.0" type: object required:

properties: invocation_id: type: string description: ULID. Used by caller to close the record via profile-invocation complete. example: "01KPQRX2EVGMRVB4Q1JQBAZJV3" profile_id: type: string description: Resolved profile identifier. example: "cleo" profile_friendly_name: type: string description: Human-readable profile name. example: "Cleo — Senior Implementer" action: type: string description: Resolved canonical action token. enum: [implement, review, plan, specify, analyze, design, curate, coordinate, advise] example: "implement" governance_context_text: type: string description: | Full governance context assembled by build_charter_context(profile=profile_id, action=action). This is the content the host LLM should use to guide its work. Empty string when governance_context_available is false. governance_context_hash: type: string description: First 16 hex characters of SHA-256 of governance_context_text. Stored in InvocationRecord for provenance. example: "a1b2c3d4e5f67890" governance_context_available: type: boolean description: | false when charter synthesis has not been run for this project (no DRG, no profiles). When false, governance_context_text is empty and a warning is surfaced. router_confidence: type: string nullable: true description: | How the profile/action resolution was made. null when the caller supplied an explicit --profile hint (resolution was not via the router). enum: [exact, canonical_verb, domain_keyword, null] example: "canonical_verb"

  • invocation_id
  • profile_id
  • profile_friendly_name
  • action
  • governance_context_text
  • governance_context_hash
  • governance_context_available

invocation-record.yaml

title: InvocationRecord (v1 JSONL schema) description: | The Tier 1 minimal viable trail record. Each invocation produces one JSONL file at .kittify/events/profile-invocations/<profile_id>-<invocation_id>.jsonl containing exactly two event lines: started (written at invoke time) and completed (appended when profile-invocation complete is called). Lines are append-only and immutable after write. version: "1.0" storage_path_pattern: ".kittify/events/profile-invocations/{invocation_id}.jsonl" storage_path_note: | Filename is invocation_id ONLY (no profile_id prefix). This enables profile-invocation complete to locate the file with --invocation-id alone. Profile filtering in invocations list reads profile_id from file content.

events: started: description: Written by ProfileInvocationExecutor.invoke() before returning to caller. type: object required:

properties: event: const: "started" invocation_id: type: string description: ULID (26 chars, Crockford base32) profile_id: type: string action: type: string description: Canonical action token request_text: type: string description: Original request string governance_context_hash: type: string description: First 16 hex chars of SHA-256 of governance_context_text governance_context_available: type: boolean actor: type: string enum: [claude, operator, unknown] router_confidence: type: ["string", "null"] enum: [exact, canonical_verb, domain_keyword, null] started_at: type: string description: ISO-8601 UTC timestamp example: "2026-04-21T10:20:47Z"

  • event
  • invocation_id
  • profile_id
  • action
  • request_text
  • governance_context_hash
  • governance_context_available
  • actor
  • started_at

completed: description: Appended by InvocationWriter.write_completed() via profile-invocation complete. type: object required:

properties: event: const: "completed" invocation_id: type: string description: Must match the started event in this file outcome: type: ["string", "null"] enum: [done, failed, abandoned, null] evidence_ref: type: ["string", "null"] description: Relative path to EvidenceArtifact directory, or null example: ".kittify/evidence/01KPQRX2EVGMRVB4Q1JQBAZJV3" completed_at: type: string description: ISO-8601 UTC timestamp

  • event
  • invocation_id
  • completed_at

validation_rules:

  • duplicate_started: Writer raises InvocationWriteError; reader skips second started event with warning.
  • invocation_id_mismatch: Reader skips the completed event with warning.
  • missing_completed: Valid open record. invocations list shows status as "open".
  • corrupt_json_line: Reader skips the line with warning; does not fail entire scan.

router-decision.yaml

title: ActionRouter contracts description: | Discriminated union returned by ActionRouter.route(request_text, profile_hint?). Either RouterDecision (success) or RouterAmbiguityError (failure). The router is a pure function — no I/O, no LLM calls. version: "1.0"

RouterDecision: description: Returned when exactly one (profile, action) pair is resolved. type: object required:

properties: profile_id: type: string action: type: string description: Canonical action token enum: [implement, review, plan, specify, analyze, design, curate, coordinate, advise] confidence: type: string enum:

match_reason: type: string description: Human-readable description of which signal produced the match example: "token 'implement' matched IMPLEMENTER canonical verb via CANONICAL_VERB_MAP"

  • profile_id
  • action
  • confidence
  • match_reason
  • exact # profile_hint was supplied and resolved unambiguously
  • canonical_verb # request token matched a role's canonical_verbs entry
  • domain_keyword # request token matched a profile's domain_keywords entry

RouterAmbiguityError: description: | Returned when the router cannot unambiguously resolve (profile, action). Three causes: ROUTER_AMBIGUOUS (multiple matches), ROUTER_NO_MATCH (zero matches), PROFILE_NOT_FOUND (explicit hint references unknown profile). type: object required:

properties: error_code: type: string enum: [ROUTER_AMBIGUOUS, ROUTER_NO_MATCH, PROFILE_NOT_FOUND] message: type: string description: Human-readable explanation of why routing failed request_text: type: string description: Original request that could not be routed candidates: type: array description: Non-empty for ROUTER_AMBIGUOUS; empty for ROUTER_NO_MATCH / PROFILE_NOT_FOUND items: type: object properties: profile_id: type: string action: type: string match_reason: type: string suggestion: type: string description: Actionable guidance for the operator or host LLM example: "Use 'spec-kitty ask <profile> <request>' to specify a profile explicitly."

  • error_code
  • message
  • request_text
  • candidates
  • suggestion

ActionRouterPlugin: description: | No-op Protocol stub reserved for future hybrid routing extension. Not called in v1. Documented here so the extension point is explicit. type: protocol methods: [] notes: | A future release may implement a hybrid router that falls back to a constrained-vocabulary LLM call when the deterministic table produces ROUTER_NO_MATCH. That implementation will satisfy this Protocol interface. WP4.2 documents the slot but never calls it.