Data Model — CLI Interview Decision Moments
Phase 1 output for mission cli-interview-decision-moments-01KPWT8P.
1. Runtime models (src/specify_cli/decisions/models.py)
1.1 Enums
class OriginFlow(str, Enum):
CHARTER = "charter"
SPECIFY = "specify"
PLAN = "plan"
class DecisionStatus(str, Enum):
OPEN = "open"
RESOLVED = "resolved"
DEFERRED = "deferred"
CANCELED = "canceled"
class DecisionErrorCode(str, Enum):
MISSING_STEP_OR_SLOT = "DECISION_MISSING_STEP_OR_SLOT"
ALREADY_CLOSED = "DECISION_ALREADY_CLOSED"
TERMINAL_CONFLICT = "DECISION_TERMINAL_CONFLICT"
NOT_FOUND = "DECISION_NOT_FOUND"
MISSION_NOT_FOUND = "MISSION_NOT_FOUND"
VERIFY_DRIFT = "DECISION_VERIFY_DRIFT"
1.2 Pydantic models (or dataclasses; choose the pattern consistent with adjacent src/specify_cli/status/models.py)
class IndexEntry(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
decision_id: str # ULID
origin_flow: OriginFlow
step_id: Optional[str] = None
slot_key: Optional[str] = None
input_key: str
question: str
options: Tuple[str, ...] = ()
status: DecisionStatus
final_answer: Optional[str] = None
rationale: Optional[str] = None
other_answer: bool = False
created_at: datetime
resolved_at: Optional[datetime] = None
resolved_by: Optional[str] = None
mission_id: str
mission_slug: str
@model_validator(mode="after")
def _step_or_slot(self):
if not self.step_id and not self.slot_key:
raise ValueError("step_id or slot_key required")
return self
class DecisionIndex(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
version: Literal[1] = 1
mission_id: str
entries: Tuple[IndexEntry, ...] = ()
2. On-disk formats
2.1 kitty-specs/<mission>/decisions/index.json
Deterministic JSON (sorted keys, LF line endings, trailing newline):
{
"version": 1,
"mission_id": "01KPWT8PNY8683QX3WBW6VXYM7",
"entries": [
{
"decision_id": "01J2A...",
"origin_flow": "specify",
"step_id": null,
"slot_key": "specify.intent-summary.q1",
"input_key": "auth_strategy",
"question": "Which auth strategy should we use?",
"options": ["session", "oauth2", "oidc", "Other"],
"status": "resolved",
"final_answer": "oauth2",
"rationale": null,
"other_answer": false,
"created_at": "2026-04-23T10:00:00+00:00",
"resolved_at": "2026-04-23T10:01:00+00:00",
"resolved_by": "robert@robshouse.net",
"mission_id": "01KPWT8PNY8683QX3WBW6VXYM7",
"mission_slug": "cli-interview-decision-moments-01KPWT8P"
}
]
}
Entries sorted by created_at ASC then decision_id ASC (lexicographic). Write pattern: atomic tmp + os.replace().
2.2 kitty-specs/<mission>/decisions/DM-<decision_id>.md
Human-readable, with change log:
# Decision Moment `01J2A...`
- **Mission:** `cli-interview-decision-moments-01KPWT8P`
- **Origin flow:** `specify`
- **Slot key:** `specify.intent-summary.q1`
- **Input key:** `auth_strategy`
- **Status:** `resolved`
- **Created:** `2026-04-23T10:00:00+00:00`
- **Resolved:** `2026-04-23T10:01:00+00:00`
- **Resolved by:** `robert@robshouse.net`
- **Other answer:** `false`
## Question
Which auth strategy should we use?
## Options
- session
- oauth2
- oidc
- Other
## Final answer
oauth2
## Rationale
_(none)_
## Change log
- `2026-04-23T10:00:00+00:00` — opened
- `2026-04-23T10:01:00+00:00` — resolved (final_answer="oauth2")
2.3 Event emission to kitty-specs/<mission>/status.events.jsonl
On decision open: append one DecisionPointOpened event (interview variant) using the 4.0.0 envelope. The payload model is DecisionPointOpenedInterviewPayload from spec_kitty_events.decisionpoint (vendored).
On decision resolve|defer|cancel: append one DecisionPointResolved event (interview variant) with terminal_outcome ∈ {resolved, deferred, canceled}.
Events carry the identity/origin/question/options fields mirrored from the index entry. step_id on the wire is populated from either step_id (preferred) or slot_key (if caller passed slot_key instead). The wire field name remains step_id for 4.0.0 compat.
3. Idempotency key
Logical key = (mission_id, origin_flow, step_id or slot_key, input_key). The store looks up the most recent entry matching that key. If found and non-terminal, return. If found and terminal, raise ALREADY_CLOSED.
4. CLI surfaces
See contracts/cli-contracts.md.
5. Sentinel marker format (LLM-authored in spec.md / plan.md)
Single inline form the verifier recognizes:
[NEEDS CLARIFICATION: <text>] <!-- decision_id: <decision_id> -->
Verifier regex (sketch): \[NEEDS CLARIFICATION: [^\]]\]\s<!--\sdecision_id:\s(\S+?)\s*-->.
6. Verifier behavior
Inputs: --mission <slug>. Loads the index, identifies decisions with status=deferred. Scans spec.md and plan.md (if they exist) for markers.
Rules:
- Every deferred decision must have ≥1 inline marker with matching
decision_id. - Every inline marker must reference a
decision_idthat exists in the index and is instatus=deferred. - A marker referencing a decision that has moved out of deferred state is a stale-marker finding.
Returns: JSON report of findings. Exit code: 0 on clean, non-zero on any finding.
7. Charter integration
In src/specify_cli/cli/commands/charter.py, find the existing interview loop (around charter interview command). Before each question is displayed, call the new decisions.service.open(...) API with origin_flow=charter, step_id=charter.<question_id>. After the answer, call resolve / defer / cancel. Preserve answers.yaml writes.
8. Template updates
src/specify_cli/missions/software-dev/command-templates/specify.md and plan.md gain an explicit instruction block:
> Before asking any interview question, you MUST run spec-kitty agent decision open --mission <slug> --flow specify --slot-key <slot> --input-key <key> --question "<q>" [--options '["a","b",...]'] and use the returned decision_id for any subsequent terminal command. After the user answers, run exactly one of: > - spec-kitty agent decision resolve <decision_id> --final-answer "<answer>" [--other-answer] > - spec-kitty agent decision defer <decision_id> --rationale "<why>" > - spec-kitty agent decision cancel <decision_id> --rationale "<why>" > > When deferring, write the inline marker [NEEDS CLARIFICATION: <text>] <!-- decision_id: <decision_id> --> in the target doc (spec.md for specify, plan.md for plan). Before finishing this command, run spec-kitty agent decision verify --mission <slug> and address any findings before declaring interview complete.
9. Dep/version changes
pyproject.toml:"spec-kitty-events==4.0.0"(was 3.3.0).src/specify_cli/spec_kitty_events/: replace with copy ofspec-kitty-events/src/spec_kitty_events/at 4.0.0.- Any CLI code that already imports from
spec_kitty_eventsand emits DecisionPoint events is checked and updated to satisfy 4.0.0 (addorigin_surface="adr"if it emits ADR-style).