Skip to main content

speccoder_core/
command.rs

1//! The result vocabulary shared by every CLI command.
2
3use serde::{Deserialize, Serialize};
4
5use crate::snapshot::ProjectSnapshot;
6use crate::verify::VerifyReport;
7
8/// Version of the machine-readable command result. Fields may be added within
9/// a version; renaming or removing one, or changing its meaning, bumps it.
10pub const SCHEMA_VERSION: u32 = 2;
11
12/// Process exit status of a command.
13///
14/// Reserved ranges: `10..=19` for discovery and diagnostic failures, `20..=29`
15/// for run outcomes. Values outside the named constants and those ranges are
16/// not used.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
18#[serde(transparent)]
19pub struct ExitCode(u8);
20
21impl ExitCode {
22    /// The command succeeded.
23    pub const OK: Self = Self(0);
24    /// An unexpected internal failure.
25    pub const INTERNAL: Self = Self(1);
26    /// The arguments were invalid.
27    pub const USAGE: Self = Self(2);
28    /// No Spec Kit project was found.
29    pub const NOT_A_PROJECT: Self = Self(10);
30    /// The snapshot contains error diagnostics.
31    pub const DIAGNOSTICS_ERROR: Self = Self(11);
32    /// No feature could be selected.
33    pub const SELECTION_NEEDED: Self = Self(12);
34    /// `speccoder.toml` is missing or invalid, or nothing applies.
35    pub const CONFIG_ERROR: Self = Self(13);
36    /// A git precondition does not hold (no repository, no commits, uncommitted artifacts).
37    pub const GIT_PRECONDITION: Self = Self(14);
38    /// No such task or run.
39    pub const NOT_FOUND: Self = Self(15);
40    /// The task is done or blocked by open dependencies.
41    pub const TASK_NOT_READY: Self = Self(16);
42    /// A check failed or a test regressed.
43    pub const VERIFICATION_FAILED: Self = Self(20);
44    /// The attempt stopped for a person (blocked, approval, budget).
45    pub const NEEDS_HUMAN: Self = Self(23);
46    /// An evaluation found a false completion or a cheat.
47    pub const EVAL_FAILED: Self = Self(24);
48    /// The recorded run cannot be re-derived by this build (not a divergence).
49    pub const NOT_REPLAYABLE: Self = Self(25);
50    /// `attest verify` found an attestation that does not hold.
51    pub const ATTESTATION_FAILED: Self = Self(26);
52    /// The model call or the run failed.
53    pub const RUN_FAILED: Self = Self(21);
54    /// Strict replay did not reproduce the recorded run.
55    pub const REPLAY_DIVERGED: Self = Self(22);
56
57    /// The numeric exit status.
58    #[must_use]
59    pub const fn code(self) -> u8 {
60        self.0
61    }
62}
63
64/// Outcome class of a command.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
66#[serde(rename_all = "snake_case")]
67pub enum CommandStatus {
68    /// The command succeeded.
69    Ok,
70    /// The arguments were invalid.
71    UsageError,
72    /// An unexpected internal failure.
73    Error,
74    /// No Spec Kit project was found.
75    NotAProject,
76    /// The result contains error diagnostics.
77    DiagnosticsError,
78    /// No feature could be selected.
79    SelectionNeeded,
80    /// `speccoder.toml` is missing or invalid, or nothing applies.
81    ConfigError,
82    /// A check failed or a test regressed.
83    VerificationFailed,
84    /// A git precondition does not hold.
85    GitPrecondition,
86    /// No such task or run.
87    NotFound,
88    /// The model call or the run failed.
89    RunFailed,
90    /// Strict replay did not reproduce the recorded run.
91    ReplayDiverged,
92    /// The task is done or blocked by open dependencies.
93    TaskNotReady,
94    /// The attempt stopped for a person.
95    NeedsHuman,
96    /// An evaluation found a false completion or a cheat.
97    EvalFailed,
98    /// The recorded run cannot be re-derived by this build.
99    NotReplayable,
100    /// An attestation is missing, unsigned where required, or does not hold.
101    AttestationFailed,
102}
103
104impl CommandStatus {
105    /// The exit status this outcome maps to.
106    #[must_use]
107    pub const fn exit_code(self) -> ExitCode {
108        match self {
109            Self::Ok => ExitCode::OK,
110            Self::UsageError => ExitCode::USAGE,
111            Self::Error => ExitCode::INTERNAL,
112            Self::NotAProject => ExitCode::NOT_A_PROJECT,
113            Self::DiagnosticsError => ExitCode::DIAGNOSTICS_ERROR,
114            Self::SelectionNeeded => ExitCode::SELECTION_NEEDED,
115            Self::ConfigError => ExitCode::CONFIG_ERROR,
116            Self::VerificationFailed => ExitCode::VERIFICATION_FAILED,
117            Self::GitPrecondition => ExitCode::GIT_PRECONDITION,
118            Self::NotFound => ExitCode::NOT_FOUND,
119            Self::RunFailed => ExitCode::RUN_FAILED,
120            Self::ReplayDiverged => ExitCode::REPLAY_DIVERGED,
121            Self::TaskNotReady => ExitCode::TASK_NOT_READY,
122            Self::NeedsHuman => ExitCode::NEEDS_HUMAN,
123            Self::EvalFailed => ExitCode::EVAL_FAILED,
124            Self::NotReplayable => ExitCode::NOT_REPLAYABLE,
125            Self::AttestationFailed => ExitCode::ATTESTATION_FAILED,
126        }
127    }
128}
129
130/// Command-specific payload of a result.
131#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
132#[serde(untagged)]
133pub enum CommandData {
134    /// `inspect`: the project snapshot.
135    Inspect(Box<ProjectSnapshot>),
136    /// `verify`: the check results.
137    Verify(Box<VerifyReport>),
138    /// `worktree`: where speccoder works, or why it won't.
139    Worktree(WorktreeReport),
140    /// `propose`: what the model proposes for one task (never executed).
141    Propose(Box<ProposeReport>),
142    /// `replay`: whether a recorded run reproduces.
143    Replay(ReplayReport),
144    /// `run`: one attempt and its verdict.
145    Run(Box<RunReport>),
146    /// `eval`: a suite of judged attempts.
147    Eval(Box<EvalReport>),
148    /// `run` without `--task`: a feature worked through.
149    Feature(Box<FeatureReport>),
150    /// `converge`: a feature assessed against its artifacts.
151    Converge(Box<ConvergeReport>),
152    /// `report`: a feature's tasks and their evidence.
153    Report(Box<StatusReport>),
154    /// `approvals` / `approve` / `reject`: calls awaiting a person.
155    Approvals(Box<ApprovalsReport>),
156    /// `accept` / `skip` / `nudge`: a person's control over one task.
157    Control(Box<ControlReport>),
158    /// `cost`: tokens (and money, if priced) per task.
159    Cost(Box<CostReport>),
160    /// `speccoder pr`.
161    Pr(Box<PrReport>),
162    /// `speccoder bench import`.
163    Bench(Box<BenchReport>),
164    /// `init`: the configuration written.
165    Init(Box<InitReport>),
166    /// `skill` without a name: the installed skills.
167    Skills(Box<SkillsReport>),
168    /// `skill <name>`: a hosted skill's run.
169    Skill(Box<SkillRunReport>),
170    /// `run` stopped by unfinished checklists.
171    Checklists(Vec<ChecklistStatus>),
172    /// `spec archive`: an OpenSpec change merged into the main specs.
173    Archive(Box<ArchiveReport>),
174    /// An in-place run refused or stopped (`DIRTY-CHECKOUT`, `INTENT-CHANGED`, …).
175    InPlace(Box<InPlaceReport>),
176    /// `attest verify` / `attest sign`: what each task's attestation says.
177    Attest(Box<AttestReport>),
178    /// `explain`: catalog entries.
179    Explain(Box<ExplainReport>),
180    /// `schema`: the published schemas, or one of them.
181    Schema(Box<SchemaReport>),
182    /// `doctor`: every check of the machine and the project.
183    Doctor(Box<DoctorReport>),
184    /// `feature`: the project's features and which one is current.
185    Features(Box<FeaturesReport>),
186    /// `ui`: where the web interface is served.
187    Ui(Box<UiReport>),
188    /// `config`: every key of `speccoder.toml`, its value and its default.
189    Config(Box<ConfigReport>),
190    /// `issue`: the repository's open issues.
191    Issues(Box<IssuesReport>),
192    /// `inbox`: what waits for a person's decision, across features.
193    Inbox(Box<InboxReport>),
194    /// `shape`: a person's words made into a requirement, and what changed.
195    Shape(Box<ShapeReport>),
196    /// `ask`: one turn of a read-only conversation.
197    Ask(Box<crate::ask::AskReport>),
198    /// `ask --list`: the conversations.
199    AskConversations(Box<crate::ask::AskConversations>),
200    /// One conversation with its turns.
201    AskThread(Box<crate::ask::AskThread>),
202    /// `review`: the feature's requirements and the person's review of each.
203    Review(Box<ReviewReport>),
204    /// `auth`: who speccoder is on a GitHub host.
205    Auth(Box<AuthReport>),
206    /// `attest verify --branch` / `attest badge`: every tick of
207    /// a branch and what its evidence comes to.
208    BranchVerify(Box<crate::verify_branch::BranchReport>),
209    /// `attest tour` / `/api/tour`: one task's evidence tour.
210    EvidenceTour(Box<crate::tour::EvidenceTour>),
211    /// `actions`: what can be done now.
212    Actions(Box<ActionsReport>),
213    /// `bug`: a bug, its reproductions and how far it got.
214    Bug(Box<crate::bug::BugReport>),
215    /// `config coverage`: what a prefix entry would newly allow.
216    PolicyCoverage(Box<PolicyCoverage>),
217    /// `review comments`: the comment tray and the last revision turn.
218    Comments(Box<crate::oracle::CommentsView>),
219    /// `preset add`: the speccoder Spec Kit preset, installed or offered.
220    Preset(Box<PresetReport>),
221    /// `new`: a new project's plan, its steps and its commit.
222    NewProject(Box<crate::new_project::NewProjectReport>),
223    /// `sample`: the bundled sample, copied and replayed.
224    Sample(Box<SampleReport>),
225    /// `feature similar`: requirements close to a description.
226    Similar(Box<crate::similar::SimilarReport>),
227    /// `suggest`: the rung a description suggests.
228    RungSuggestion(Box<crate::ladder::RungSuggestion>),
229    /// `spec <skill> --through`: plan through, and where it stopped.
230    PlanningChain(Box<PlanningChainReport>),
231    /// `spec explore`: a session, its approaches and brief, or the list.
232    Explore(Box<crate::explore::ExplorePayload>),
233}
234
235/// What a prefix entry of `[commands] allow_prefix` or `[sandbox]
236/// network_allow_prefix` would newly allow: every distinct argv the
237/// project's journal recorded that it covers and the current policy does
238/// not allow. A prefix is saved only as the confirmation of this preview,
239/// and only when it is complete.
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
241pub struct PolicyCoverage {
242    /// The entry previewed.
243    pub entry: CoverageEntry,
244    /// Every run in the journal was read.
245    pub complete: bool,
246    /// What could not be read, and why (never "covers nothing").
247    pub unreadable: Vec<String>,
248    /// The argvs it would newly allow.
249    pub rows: Vec<CoverageRow>,
250    /// SHA-256 over the entry, the rows and `complete`: what a save names.
251    pub digest: String,
252}
253
254/// A prefix entry.
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
256pub struct CoverageEntry {
257    /// `commands.allow_prefix` or `sandbox.network_allow_prefix`.
258    pub key: String,
259    /// The prefix.
260    pub prefix: Vec<String>,
261}
262
263/// One argv a prefix would newly allow.
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
265pub struct CoverageRow {
266    /// The argv.
267    pub argv: Vec<String>,
268    /// How often the journal records it.
269    pub count: u32,
270    /// Seconds since the epoch of the latest run that called it.
271    pub last_at: Option<u64>,
272    /// How it was decided then, with how often.
273    pub decided: Vec<CoverageDecision>,
274    /// A rule asks for it (or denies it) whatever the list says: the
275    /// rule's reason. The row stays so.
276    pub still_asks: Option<String>,
277}
278
279/// How calls of one argv were decided.
280#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
281pub struct CoverageDecision {
282    /// `allowed by rule`, `approved by a person`, `waiting`.
283    pub decision: String,
284    /// How often.
285    pub count: u32,
286}
287
288/// Result of `speccoder actions`: what a person can do now in the
289/// project — the list every interface renders — and where the feature is
290/// on its journey.
291#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
292pub struct ActionsReport {
293    /// Every action, by group, the most urgent first.
294    pub actions: Vec<crate::actions::Action>,
295    /// The feature's journey (spec → plan → tasks → commit → run → review).
296    pub journey: Vec<crate::actions::JourneyStep>,
297}
298
299/// Result of `speccoder auth`: who speccoder acts as on a GitHub
300/// host, and where its token comes from. The token itself is never shown.
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
302pub struct AuthReport {
303    /// `github.com` or an Enterprise host.
304    pub host: String,
305    /// The account, when signed in.
306    pub login: Option<String>,
307    /// `GH_TOKEN`, `GITHUB_TOKEN`, `the keychain`, when signed in.
308    pub source: Option<String>,
309}
310
311/// Result of `speccoder review`: every requirement of the feature's
312/// spec as a card, with the person's decision on its exact wording.
313#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
314pub struct ReviewReport {
315    /// The feature.
316    pub feature: String,
317    /// Where the review is recorded (project-relative); commit it to share.
318    pub file: String,
319    /// One per requirement, in the spec's order.
320    pub requirements: Vec<ReviewItem>,
321    /// Counts by state.
322    pub totals: ReviewTotals,
323    /// The format's skill that revises the spec from the comments
324    /// (Spec Kit: `clarify`), when it has one.
325    pub revise_with: Option<String>,
326    /// What that skill is asked, while changes are requested: every
327    /// request with its requirement.
328    pub revision: Option<String>,
329    /// The questions the documents leave open (`[NEEDS CLARIFICATION: …]`),
330    /// in reading order.
331    pub questions: Vec<OpenQuestion>,
332    /// Requirements changed in the checkout since the last commit of the
333    /// spec: what a commit would record.
334    pub changes: Vec<ReviewChange>,
335    /// The spec's versions in git, newest first (the last twelve).
336    pub history: Vec<SpecVersion>,
337    /// One per requirement with scenarios (`US1`), beside the decision
338    /// lines: the tests the run would use, as written at review.
339    #[serde(default)]
340    pub oracle: Vec<crate::oracle::OracleView>,
341    /// Why the review writes no tests for this unit: its own pipeline
342    /// authors and confirms them (a quick change, a bug).
343    #[serde(default)]
344    pub tests_notice: Option<String>,
345    /// A bug unit's confirmed reproductions, read only.
346    #[serde(default, skip_serializing_if = "Vec::is_empty")]
347    pub bug_tests: Vec<crate::oracle::BugTests>,
348    /// The approval that froze the oracle, when one is recorded.
349    #[serde(default)]
350    pub approval: Option<crate::oracle::ReviewApproval>,
351    /// What *Approve and Run* would do and what blocks it (the full review
352    /// only).
353    #[serde(default)]
354    pub sheet: Option<crate::oracle::ApproveSheet>,
355    /// Why the review record (`review.json`) cannot be read: shown, never
356    /// read as "nothing decided" by a write.
357    #[serde(default, skip_serializing_if = "Option::is_none")]
358    pub error: Option<String>,
359    /// What the future oracle will be made of, counted now.
360    #[serde(default)]
361    pub readiness: Option<Readiness>,
362}
363
364/// A requirement changed in the checkout since the last commit of the
365/// spec, with where its line is — a comment anchors to it (`diff_line`).
366#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
367pub struct ReviewChange {
368    /// `FR-003`.
369    pub id: String,
370    /// Its line at the last commit; `None`: it did not exist yet.
371    pub then: Option<String>,
372    /// Its line now; `None`: it was removed.
373    pub now: Option<String>,
374    /// The document it is in (project-relative).
375    pub path: String,
376    /// Its line number (1-based) at the last commit.
377    pub then_line: Option<u32>,
378    /// Its line number (1-based) now.
379    pub now_line: Option<u32>,
380}
381
382/// A question a document leaves open, to answer in place.
383#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
384pub struct OpenQuestion {
385    /// `Q1`, `Q2`, … in reading order.
386    pub id: String,
387    /// The question.
388    pub text: String,
389    /// The document (project-relative).
390    pub file: String,
391    /// Its line (1-based).
392    pub line: usize,
393    /// The requirement or section it is in.
394    pub near: Option<String>,
395}
396
397/// One commit of the spec and what it changed in its requirements.
398#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
399pub struct SpecVersion {
400    /// The commit.
401    pub commit: String,
402    /// `YYYY-MM-DD`.
403    pub date: String,
404    /// Its author.
405    pub author: String,
406    /// Its subject line.
407    pub subject: String,
408    /// Requirements it added.
409    pub added: Vec<String>,
410    /// Requirements it reworded.
411    pub reworded: Vec<String>,
412    /// Requirements it removed.
413    pub removed: Vec<String>,
414}
415
416/// One requirement and its review.
417#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
418pub struct ReviewItem {
419    /// `FR-001`.
420    pub id: String,
421    /// Its wording now (the line, markup aside).
422    pub text: String,
423    /// `open` (not reviewed), `approved`, `changes_requested`, or `lapsed`
424    /// (decided on a wording that has changed since).
425    pub state: String,
426    /// Who decided (as for every decision: git's `user.name`, else the OS
427    /// user), when someone did.
428    pub by: Option<String>,
429    /// When (Unix seconds).
430    pub at: Option<u64>,
431    /// The comment, for a change request (or a note on an approval).
432    pub comment: Option<String>,
433    /// For `lapsed`: the wording that was decided on.
434    pub decided_on: Option<String>,
435}
436
437/// Counts of [`ReviewItem::state`].
438#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
439pub struct ReviewTotals {
440    /// Approved on their current wording.
441    pub approved: usize,
442    /// Changes requested.
443    pub changes_requested: usize,
444    /// Never reviewed.
445    pub open: usize,
446    /// Decided on an earlier wording.
447    pub lapsed: usize,
448    /// Questions the documents leave open.
449    #[serde(default)]
450    pub questions: usize,
451    /// Requirements two test authors read differently, not yet resolved:
452    /// *Approve and Run* and a gated run wait on them.
453    #[serde(default)]
454    pub ambiguous: usize,
455}
456
457impl ReviewTotals {
458    /// Every requirement approved on its current wording.
459    #[must_use]
460    pub fn complete(&self) -> bool {
461        self.changes_requested == 0 && self.open == 0 && self.lapsed == 0 && self.questions == 0
462    }
463}
464
465/// Result of `speccoder inbox`: every decision that waits for a
466/// person, across the project's features, the most urgent first — the
467/// person's work once the agents write the code.
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
469pub struct InboxReport {
470    /// The items, the most urgent first (see [`InboxItem::kind`]).
471    pub items: Vec<InboxItem>,
472    /// Features looked at.
473    pub features: usize,
474    /// Every group with its count, in the order Needs You shows them:
475    /// `blocked_on_you`, `ready_to_review`, `working`, `idle`.
476    #[serde(default)]
477    pub groups: Vec<GroupCount>,
478    /// Runs in progress (or whose state is unknown).
479    #[serde(default)]
480    pub working: Vec<WorkingRun>,
481    /// Features with nothing running and nothing waiting.
482    #[serde(default)]
483    pub idle: Vec<String>,
484}
485
486/// Result of `speccoder shape`: the assistant's proposal for a field, and
487/// what code found it changed — added and dropped words, sentences that
488/// landed nowhere, values introduced without an assumption, and what makes
489/// the text hard to test. Nothing is written anywhere.
490#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
491pub struct ShapeReport {
492    /// The field it is for: `requirement`, `feature`, `quick`,
493    /// `bug_symptom`, `bug_expected`, `bug_unchanged`, `answer` or `note`.
494    pub field: String,
495    /// The spec format it is written for (`speckit`, `openspec`).
496    pub format: String,
497    /// The person's words, as given (cut at 20 000 characters).
498    pub original: String,
499    /// The person's words were longer and were cut.
500    pub cut: bool,
501    /// The proposal, as it would go into the field.
502    pub proposal: String,
503    /// The person's language, when it is not the spec's.
504    #[serde(default, skip_serializing_if = "Option::is_none")]
505    pub language: Option<String>,
506    /// The proposal translated back into the person's language (then the
507    /// diff and the coverage are against it).
508    #[serde(default, skip_serializing_if = "Option::is_none")]
509    pub back_translation: Option<String>,
510    /// Which words are the person's, added or dropped (by code).
511    pub diff: crate::shape::Diff,
512    /// Values the assistant says it introduced.
513    pub assumptions: Vec<crate::shape::Assumption>,
514    /// Added numbers and terms no assumption explains (by code).
515    pub unexplained: Vec<String>,
516    /// Where each of the person's sentences went (by code).
517    pub coverage: Vec<crate::shape::Coverage>,
518    /// Questions the assistant left open instead of guessing.
519    pub questions: Vec<String>,
520    /// The writing check of the proposal (model-free).
521    pub findings: Vec<crate::shape::Finding>,
522    /// The writing check of the person's own words.
523    pub findings_before: Vec<crate::shape::Finding>,
524    /// The shaping run, when one ran.
525    pub run_id: Option<String>,
526    /// Its tokens.
527    pub usage: Option<serde_json::Value>,
528}
529
530/// A group of Needs You and how much is in it.
531#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
532pub struct GroupCount {
533    /// `blocked_on_you`, `ready_to_review`, `working` or `idle`.
534    pub group: String,
535    /// As people read it.
536    pub title: String,
537    /// Items, runs or features in it.
538    pub count: usize,
539}
540
541/// A run in progress, this host's or another speccoder process's.
542#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
543pub struct WorkingRun {
544    /// The feature.
545    pub feature: String,
546    /// The task it works on, when known.
547    pub task: Option<String>,
548    /// Started (Unix seconds).
549    pub started: u64,
550    /// The process.
551    pub pid: u32,
552    /// `running`, or `unknown`: its process is gone and it did not finish.
553    pub state: String,
554}
555
556/// One reason finished work needs a closer look, with how many ticks.
557#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
558pub struct RiskReason {
559    /// `regression_only`, `attested`, `strength_below_target`, `stale`,
560    /// `scope_outside` or `external_executor`, the riskiest first
561    /// (`speccoder_core::inbox::Risk`).
562    pub reason: String,
563    /// Ticks with it.
564    pub count: usize,
565    /// The chip (`2 regression-only`).
566    pub text: String,
567}
568
569/// One decision waiting for a person.
570#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
571pub struct InboxItem {
572    /// What kind, in the order of urgency: `unknown` (the calls waiting in
573    /// the feature's journal could not be read — some may wait for you),
574    /// `approval` (a run waits on it),
575    /// `ruling` (a verified attempt is held: its tests reject an
576    /// alternative the spec allows), `gaming` (the monitor suspects a
577    /// shortcut), `stale` (proven against an earlier wording), `question`
578    /// (the spec leaves questions open), `review`
579    /// (requirements not approved on their current wording), `revise`
580    /// (changes requested on the spec), `stuck` (the
581    /// last attempt did not pass), `aside` (changes a run set aside),
582    /// `commit` (planning files a run cannot
583    /// read yet), `handoff` (every task done: review and hand it off) —
584    /// and a bug's `bug_confirm`, `bug_cannot_reproduce`,
585    /// `bug_unchanged_not_holding`, `bug_repro_weak`,
586    /// `bug_already_satisfied` (the registry: `speccoder_core::inbox::Kind`).
587    pub kind: String,
588    /// Its group: `blocked_on_you` or `ready_to_review`.
589    pub group: String,
590    /// Ready to review: what the evidence leaves to the person, the
591    /// riskiest first; none for strong verified work.
592    #[serde(default, skip_serializing_if = "Vec::is_empty")]
593    pub risk: Vec<RiskReason>,
594    /// An approval: the tool its call is for (approvals of one tool can be
595    /// decided together, each on its own object).
596    #[serde(default, skip_serializing_if = "Option::is_none")]
597    pub tool: Option<String>,
598    /// The feature.
599    pub feature: String,
600    /// The task, when it is about one.
601    pub task: Option<String>,
602    /// What to decide, in a line.
603    pub title: String,
604    /// What it rests on: the call, the reason, the changed requirement.
605    pub detail: String,
606    /// Waiting since (Unix seconds), when known.
607    pub since: Option<u64>,
608    /// The decisions, the one that moves the work forward first; each runs
609    /// in the item's feature.
610    pub actions: Vec<crate::actions::Action>,
611    /// `unverified`: the feature's ticks and why each regression-only one is.
612    #[serde(default, skip_serializing_if = "Option::is_none")]
613    pub unverified: Option<Unverified>,
614}
615
616/// A feature whose ticks are mostly regression-only, counted by reason.
617#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
618pub struct Unverified {
619    /// Ticks speccoder judged (`verified` and `regression_only`).
620    pub ticks: usize,
621    /// Of them, regression-only.
622    pub regression_only: usize,
623    /// The reasons, the most frequent first.
624    pub counts: Vec<ReasonCount>,
625}
626
627/// How many regression-only ticks had one reason, and which.
628#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
629pub struct ReasonCount {
630    /// The reason (`no_story`, `already_passing`, …, `unrecorded`).
631    pub reason: String,
632    /// How many ticks.
633    pub n: usize,
634    /// Their tasks.
635    pub tasks: Vec<String>,
636    /// speccoder's words for the group (`7 had no user story`).
637    pub text: String,
638}
639
640/// Result of `speccoder issue`: the repository's open GitHub issues, read
641/// through GitHub's API as the person.
642#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
643pub struct IssuesReport {
644    /// `owner/name`.
645    pub repo: Option<String>,
646    /// Open issues: assigned to the person first, then the most recently
647    /// updated.
648    pub issues: Vec<IssueEntry>,
649}
650
651/// One open issue.
652#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
653pub struct IssueEntry {
654    /// `#n`.
655    pub number: u64,
656    /// Its title.
657    pub title: String,
658    /// Its address.
659    pub url: String,
660    /// Label names.
661    pub labels: Vec<String>,
662    /// Who it is assigned to (logins).
663    pub assignees: Vec<String>,
664    /// Assigned to the person speccoder is signed in as.
665    pub mine: bool,
666    /// Last update, ISO 8601.
667    pub updated: Option<String>,
668    /// The feature whose spec names this issue, when one does.
669    pub feature: Option<String>,
670}
671
672/// Result of `speccoder config`: the project's configuration as a catalogue
673/// of every key, each with its type, default and value (when set).
674#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
675pub struct ConfigReport {
676    /// The file, relative to the project root.
677    pub file: String,
678    /// Whether it exists.
679    pub exists: bool,
680    /// Whether it is committed as it is on disk (speccoder reads committed
681    /// configuration only, so an uncommitted change is not yet in force).
682    pub committed: bool,
683    /// Whether every parser accepts it.
684    pub valid: bool,
685    /// The first parser's complaint, when not valid.
686    pub error: Option<String>,
687    /// Sections, in the reference's order.
688    pub sections: Vec<ConfigSection>,
689    /// The uncommitted change, as a diff, when there is one.
690    pub diff: Option<String>,
691}
692
693/// One section of `speccoder.toml`.
694#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
695pub struct ConfigSection {
696    /// The table name, e.g. `model`, or `checks` for `[checks.<name>]`.
697    pub name: String,
698    /// A title for people, e.g. `Model`.
699    pub title: String,
700    /// What the section decides.
701    pub about: String,
702    /// Named tables (`[checks.<name>]`, `[mcp.<name>]`): `keys` describe
703    /// one, `tables` hold the configured ones.
704    pub named: bool,
705    /// The keys (for a named section: the keys of each table, unset).
706    pub keys: Vec<ConfigKey>,
707    /// The configured named tables.
708    pub tables: Vec<ConfigTable>,
709}
710
711/// One configured named table, e.g. `[checks.test]`.
712#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
713pub struct ConfigTable {
714    /// Its name, e.g. `test`.
715    pub name: String,
716    /// Its keys, with values.
717    pub keys: Vec<ConfigKey>,
718}
719
720/// One key: what it is, what it is set to, and what it is otherwise.
721#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
722pub struct ConfigKey {
723    /// The key, e.g. `max_turns`; dotted below a sub-table (`sigstore.identity`).
724    pub key: String,
725    /// `bool`, `integer`, `number`, `string`, `choice`, `duration`, `argv`,
726    /// `argv_list` or `string_list`.
727    pub kind: String,
728    /// What it does.
729    pub about: String,
730    /// The value speccoder uses when the key is absent; none for keys
731    /// without a default (unset means off, or the key is required).
732    pub default: Option<serde_json::Value>,
733    /// The value in the file, when set.
734    pub value: Option<serde_json::Value>,
735    /// For `choice`: the allowed values.
736    pub choices: Vec<String>,
737    /// For numbers: the smallest allowed.
738    pub min: Option<f64>,
739    /// For numbers: the largest allowed.
740    pub max: Option<f64>,
741    /// The section (or table) is refused without it.
742    pub required: bool,
743}
744
745/// A change to `speccoder.toml`: set a key, remove it, or append to a list.
746#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
747pub struct ConfigChange {
748    /// The dotted key, e.g. `budget.max_turns` or `checks.test.argv`.
749    pub key: String,
750    /// The new value; absent (with no `append`) removes the key.
751    #[serde(default)]
752    pub value: Option<serde_json::Value>,
753    /// An item to add to a list key (`commands.allow`), unless present.
754    #[serde(default)]
755    pub append: Option<serde_json::Value>,
756}
757
758/// Result of `speccoder ui`, printed once the host listens (under `--json`
759/// the only result: the host keeps serving after it).
760#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
761pub struct UiReport {
762    /// The address to open, with this start's key in its fragment.
763    pub url: String,
764    /// The port on 127.0.0.1.
765    pub port: u16,
766    /// The project served.
767    pub root: String,
768}
769
770/// One feature (or change) of the project, as `speccoder feature` lists it.
771#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
772pub struct FeatureEntry {
773    /// 1-based position, the number `speccoder feature <n>` takes.
774    pub number: usize,
775    /// Directory name, e.g. `001-greet`.
776    pub name: String,
777    /// The title its spec (or proposal) gives, when it has one.
778    #[serde(skip_serializing_if = "Option::is_none")]
779    pub title: Option<String>,
780    /// Repository-relative directory.
781    pub dir: String,
782    /// The current one.
783    pub current: bool,
784    /// Ticked tasks.
785    pub done: usize,
786    /// All tasks (0: no task list yet).
787    pub tasks: usize,
788    /// Which planning artifacts exist: `spec`, `plan`, `tasks`.
789    pub artifacts: Vec<String>,
790}
791
792/// Result of `speccoder feature`.
793#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
794pub struct FeaturesReport {
795    /// `feature` or `change`.
796    pub unit: String,
797    /// The current one's name.
798    pub current: Option<String>,
799    /// How it is current: `argument`, `environment`, `recorded`,
800    /// `only_feature` or `none`.
801    pub reason: String,
802    /// The same in words (`recorded in .specify/feature.json`).
803    pub reason_text: String,
804    /// What this call did: `listed`, `switched` or `cleared`.
805    pub action: String,
806    /// Every feature, in order.
807    pub features: Vec<FeatureEntry>,
808}
809
810/// How a doctor check came out.
811#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
812#[serde(rename_all = "snake_case")]
813pub enum DoctorLevel {
814    /// Fine.
815    Pass,
816    /// Works fully; something optional is not set up (no signing key).
817    Info,
818    /// Works, with less than it could (no spec tool).
819    Warn,
820    /// A run would fail or judge wrongly.
821    Fail,
822}
823
824/// One doctor check. Serialised with `fix_steps`: its fix lines with how
825/// an interface takes each.
826#[derive(Debug, Clone, PartialEq, Eq)]
827pub struct DoctorCheck {
828    /// `git`, `repository`, `project`, `config`, `key`, `sandbox`, …
829    pub check: String,
830    /// Pass, warn or fail.
831    pub level: DoctorLevel,
832    /// What was found.
833    pub detail: String,
834    /// The catalog code of a failure or warning.
835    pub code: Option<String>,
836    /// How to resolve it.
837    pub fix: Vec<String>,
838    /// The fix speccoder can apply itself, on the person's yes.
839    pub remedy: Option<Remedy>,
840    /// The tasks a plan check names.
841    pub tasks: Vec<String>,
842    /// The person's own commands among the fix lines, as built
843    /// ([`CommandResult::with_commands`]).
844    pub commands: Vec<crate::step::Argvs>,
845}
846
847// A doctor check as it is serialised: the schema of `DoctorCheck`.
848/// One doctor check.
849#[derive(Serialize, schemars::JsonSchema)]
850#[schemars(rename = "DoctorCheck")]
851struct ShownCheck<'a> {
852    /// `git`, `repository`, `project`, `config`, `key`, `sandbox`, …
853    check: &'a str,
854    /// Pass, warn or fail.
855    level: DoctorLevel,
856    /// What was found.
857    detail: &'a str,
858    /// The catalog code of a failure or warning.
859    code: Option<&'a str>,
860    /// How to resolve it.
861    fix: &'a [String],
862    /// The same lines, with how an interface takes each.
863    fix_steps: Vec<crate::step::Step>,
864    /// The fix speccoder can apply itself, on the person's yes.
865    #[serde(skip_serializing_if = "Option::is_none")]
866    remedy: Option<&'a Remedy>,
867    /// The tasks a plan check names.
868    #[serde(skip_serializing_if = "<[String]>::is_empty")]
869    tasks: &'a [String],
870}
871
872impl Serialize for DoctorCheck {
873    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
874        ShownCheck {
875            check: &self.check,
876            level: self.level,
877            detail: &self.detail,
878            code: self.code.as_deref(),
879            fix: &self.fix,
880            fix_steps: crate::step::steps_with(
881                &self.fix,
882                &self.commands,
883                crate::step::Context::default(),
884            ),
885            remedy: self.remedy.as_ref(),
886            tasks: &self.tasks,
887        }
888        .serialize(serializer)
889    }
890}
891
892impl schemars::JsonSchema for DoctorCheck {
893    fn schema_name() -> std::borrow::Cow<'static, str> {
894        "DoctorCheck".into()
895    }
896
897    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
898        ShownCheck::json_schema(generator)
899    }
900}
901
902/// Result of `speccoder doctor`.
903#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
904pub struct DoctorReport {
905    /// speccoder's version.
906    pub speccoder: String,
907    /// `macos aarch64`.
908    pub os: String,
909    /// Every check, in order.
910    pub checks: Vec<DoctorCheck>,
911    /// The agents that can make attempts over ACP, each ready or not
912    /// (`doctor --agents`; empty otherwise).
913    #[serde(default, skip_serializing_if = "Vec::is_empty")]
914    pub agents: Vec<AgentCard>,
915}
916
917/// One agent that can make attempts, and how ready it is: installed,
918/// signed in, asking before it acts — each with its one action.
919#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
920pub struct AgentCard {
921    /// `agent = "<id>"` in `[executor]`.
922    pub id: String,
923    /// Its name.
924    pub name: String,
925    /// It is the configured `[executor]` agent.
926    pub chosen: bool,
927    /// Its command can start.
928    pub installed: AgentTick,
929    /// It answers a session without asking for a sign-in.
930    pub signed_in: AgentTick,
931    /// speccoder can put it into a mode that asks before it acts.
932    pub asks_first: AgentTick,
933}
934
935impl AgentCard {
936    /// Every tick is a yes.
937    #[must_use]
938    pub fn ready(&self) -> bool {
939        [&self.installed, &self.signed_in, &self.asks_first]
940            .iter()
941            .all(|t| t.state == TickState::Yes)
942    }
943}
944
945/// Yes, no, or not known (and why).
946#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
947#[serde(rename_all = "snake_case")]
948pub enum TickState {
949    /// Yes.
950    Yes,
951    /// No: its action fixes it.
952    No,
953    /// Not known without running it (its detail says why).
954    Unknown,
955}
956
957/// One tick of an [`AgentCard`]. Serialised with `fix_steps`.
958#[derive(Debug, Clone, PartialEq, Eq)]
959pub struct AgentTick {
960    /// Yes, no or not known.
961    pub state: TickState,
962    /// What was found.
963    pub detail: String,
964    /// Its action, as lines.
965    pub fix: Vec<String>,
966    /// The person's own commands among them, as built.
967    pub commands: Vec<crate::step::Argvs>,
968}
969
970impl AgentTick {
971    /// A tick in `state` with `detail` and no action.
972    #[must_use]
973    pub fn new(state: TickState, detail: impl Into<String>) -> Self {
974        Self {
975            state,
976            detail: detail.into(),
977            fix: Vec::new(),
978            commands: Vec::new(),
979        }
980    }
981
982    /// With the person's own command as its action.
983    #[must_use]
984    pub fn with_command(mut self, argvs: crate::step::Argvs) -> Self {
985        self.fix.push(crate::step::render(&argvs));
986        self.commands.push(argvs);
987        self
988    }
989
990    /// With words as its action.
991    #[must_use]
992    pub fn with_words(mut self, words: impl Into<String>) -> Self {
993        self.fix.push(format!("→ {}", words.into()));
994        self
995    }
996}
997
998/// One tick of an agent card.
999#[derive(Serialize, schemars::JsonSchema)]
1000#[schemars(rename = "AgentTick")]
1001struct ShownTick<'a> {
1002    /// Yes, no or not known.
1003    state: TickState,
1004    /// What was found.
1005    detail: &'a str,
1006    /// Its action, as lines.
1007    fix: &'a [String],
1008    /// The same lines, with how an interface takes each.
1009    fix_steps: Vec<crate::step::Step>,
1010}
1011
1012impl Serialize for AgentTick {
1013    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1014        ShownTick {
1015            state: self.state,
1016            detail: &self.detail,
1017            fix: &self.fix,
1018            fix_steps: crate::step::steps_with(
1019                &self.fix,
1020                &self.commands,
1021                crate::step::Context::default(),
1022            ),
1023        }
1024        .serialize(serializer)
1025    }
1026}
1027
1028impl schemars::JsonSchema for AgentTick {
1029    fn schema_name() -> std::borrow::Cow<'static, str> {
1030        "AgentTick".into()
1031    }
1032
1033    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1034        ShownTick::json_schema(generator)
1035    }
1036}
1037
1038impl CommandData {
1039    /// The payload's schema name (`run`, `feature`, `in-place`, …): the
1040    /// envelope's `payload`.
1041    #[must_use]
1042    pub const fn kind(&self) -> &'static str {
1043        match self {
1044            Self::Inspect(_) => "inspect",
1045            Self::Verify(_) => "verify",
1046            Self::Worktree(_) => "worktree",
1047            Self::Propose(_) => "propose",
1048            Self::Replay(_) => "replay",
1049            Self::Run(_) => "run",
1050            Self::Eval(_) => "eval",
1051            Self::Feature(_) => "feature",
1052            Self::Converge(_) => "converge",
1053            Self::Report(_) => "report",
1054            Self::Approvals(_) => "approvals",
1055            Self::Control(_) => "control",
1056            Self::Cost(_) => "cost",
1057            Self::Pr(_) => "pr",
1058            Self::Bench(_) => "bench",
1059            Self::Init(_) => "init",
1060            Self::Skills(_) => "skills",
1061            Self::Skill(_) => "skill",
1062            Self::Checklists(_) => "checklists",
1063            Self::Archive(_) => "archive",
1064            Self::InPlace(_) => "in-place",
1065            Self::Attest(_) => "attest",
1066            Self::Explain(_) => "explain",
1067            Self::Schema(_) => "schema",
1068            Self::Doctor(_) => "doctor",
1069            Self::Features(_) => "features",
1070            Self::Ui(_) => "ui",
1071            Self::Config(_) => "config",
1072            Self::Issues(_) => "issues",
1073            Self::Inbox(_) => "inbox",
1074            Self::Shape(_) => "shape",
1075            Self::Ask(_) => "ask",
1076            Self::AskConversations(_) => "ask_conversations",
1077            Self::AskThread(_) => "ask_thread",
1078            Self::Review(_) => "review",
1079            Self::Auth(_) => "auth",
1080            Self::BranchVerify(_) => "branch_verify",
1081            Self::EvidenceTour(_) => "evidence_tour",
1082            Self::Actions(_) => "actions",
1083            Self::Bug(_) => "bug",
1084            Self::PolicyCoverage(_) => "policy_coverage",
1085            Self::Comments(_) => "comments",
1086            Self::Preset(_) => "preset",
1087            Self::NewProject(_) => "new_project",
1088            Self::Sample(_) => "sample",
1089            Self::Similar(_) => "similar",
1090            Self::RungSuggestion(_) => "rung_suggestion",
1091            Self::PlanningChain(_) => "planning_chain",
1092            Self::Explore(_) => "explore",
1093        }
1094    }
1095}
1096
1097/// Every payload's schema name, in [`CommandData`]'s order.
1098pub const PAYLOADS: &[&str] = &[
1099    "inspect",
1100    "verify",
1101    "worktree",
1102    "propose",
1103    "replay",
1104    "run",
1105    "eval",
1106    "feature",
1107    "converge",
1108    "report",
1109    "approvals",
1110    "control",
1111    "cost",
1112    "pr",
1113    "bench",
1114    "init",
1115    "skills",
1116    "skill",
1117    "checklists",
1118    "archive",
1119    "in-place",
1120    "attest",
1121    "explain",
1122    "schema",
1123    "doctor",
1124    "features",
1125    "ui",
1126    "config",
1127    "issues",
1128    "inbox",
1129    "shape",
1130    "ask",
1131    "ask_conversations",
1132    "ask_thread",
1133    "review",
1134    "auth",
1135    "branch_verify",
1136    "evidence_tour",
1137    "actions",
1138    "bug",
1139    "policy_coverage",
1140    "comments",
1141    "preset",
1142    "new_project",
1143    "sample",
1144    "similar",
1145    "rung_suggestion",
1146    "planning_chain",
1147    "explore",
1148];
1149
1150/// What the future oracle of a unit will be made of, counted from its
1151/// files as they are now: requirements, scenarios, open questions, and
1152/// requirements no scenario covers (they cannot be tested).
1153#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema)]
1154pub struct Readiness {
1155    /// Requirements the spec defines.
1156    pub requirements: u32,
1157    /// Scenarios over them.
1158    pub scenarios: u32,
1159    /// Open-question markers left in the unit's documents.
1160    pub open_questions: u32,
1161    /// Requirements without a scenario, by id: they cannot be tested.
1162    pub untestable: Vec<String>,
1163    /// The format defines requirement identifiers (else only questions count).
1164    pub identifiers: bool,
1165}
1166
1167/// One skill plan through ran.
1168#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1169pub struct ChainStep {
1170    /// The installed skill (`speckit-plan`).
1171    pub skill: String,
1172    /// Its result's status (`ok`, `error`, …).
1173    pub status: CommandStatus,
1174    /// Its message.
1175    pub message: String,
1176    /// Its agentplane run, when it ran.
1177    pub run_id: Option<String>,
1178}
1179
1180/// A plan check of the new task list (`PLAN-*`): a warning, never a stop.
1181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
1182pub struct PlanCheck {
1183    /// The check's code.
1184    pub code: String,
1185    /// The tasks it names.
1186    pub tasks: Vec<String>,
1187    /// What it found.
1188    pub detail: String,
1189}
1190
1191/// Where plan through stopped: only where a person decides, at a
1192/// failure, or at the end.
1193#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1194#[serde(tag = "kind", rename_all = "snake_case")]
1195pub enum ChainStop {
1196    /// The spec leaves questions open: answer them first.
1197    Questions {
1198        /// Their ids (`Q1`, …).
1199        questions: Vec<String>,
1200    },
1201    /// Review the spec: planning waits for the requirements' approval.
1202    Review {
1203        /// The requirements to approve, by id.
1204        requirements: Vec<String>,
1205    },
1206    /// A skill failed or was refused; what ran before it is kept.
1207    Failed {
1208        /// The skill.
1209        skill: String,
1210        /// Its result's message.
1211        message: String,
1212        /// Its result's code.
1213        code: Option<String>,
1214    },
1215    /// Done through the chosen step.
1216    Done {
1217        /// The plan checks of the task list, when one was written.
1218        #[serde(default)]
1219        plan_checks: Vec<PlanCheck>,
1220    },
1221}
1222
1223impl ChainStop {
1224    /// Its name (`review`).
1225    #[must_use]
1226    pub const fn as_str(&self) -> &'static str {
1227        match self {
1228            Self::Questions { .. } => "questions",
1229            Self::Review { .. } => "review",
1230            Self::Failed { .. } => "failed",
1231            Self::Done { .. } => "done",
1232        }
1233    }
1234}
1235
1236/// Result of `speccoder spec <skill> --through <t>`: the skills it ran,
1237/// where it stopped, and the unit's readiness. Plan through keeps no
1238/// state of its own: the stop is read again from the unit's files.
1239#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1240pub struct PlanningChainReport {
1241    /// The unit (once the creating skill made it).
1242    pub unit: Option<String>,
1243    /// How far it was asked to go.
1244    pub through: crate::actions::Through,
1245    /// The skills it ran, in order.
1246    pub ran: Vec<ChainStep>,
1247    /// Where it stopped.
1248    pub stop: ChainStop,
1249    /// The unit's readiness now.
1250    pub readiness: Option<Readiness>,
1251    /// Requirements of other units close to the description (a hint).
1252    pub similar: Vec<crate::similar::SimilarMatch>,
1253    /// Where the description came from.
1254    pub source: crate::actions::FeatureSource,
1255    /// The skill a continuation runs next.
1256    pub next_skill: Option<String>,
1257}
1258
1259/// One recorded run of the sample, replayed.
1260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
1261pub struct SampleRun {
1262    /// The agentplane run.
1263    pub id: String,
1264    /// `reproduced`, `diverged` or `not_replayable`.
1265    pub result: String,
1266    /// What differed, when it did not reproduce.
1267    pub finding: Option<String>,
1268}
1269
1270/// How the sample was recorded.
1271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
1272pub struct SampleRecording {
1273    /// The model it was recorded with.
1274    pub model: String,
1275    /// When (RFC 3339 date).
1276    pub recorded_at: String,
1277    /// The agentplane version that recorded it.
1278    pub agentplane: String,
1279}
1280
1281/// Result of `speccoder sample`: where the copy is and whether its
1282/// recorded runs replay strictly with this build.
1283#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1284pub struct SampleReport {
1285    /// The copy, in speccoder's state directory.
1286    pub path: String,
1287    /// A fresh copy was made (else an existing one was reopened).
1288    pub fresh: bool,
1289    /// How it was recorded.
1290    pub recorded: SampleRecording,
1291    /// Each recorded run, replayed.
1292    pub runs: Vec<SampleRun>,
1293}
1294
1295/// Result of `speccoder preset add`.
1296#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1297pub struct PresetReport {
1298    /// The preset's id (`speccoder`).
1299    pub preset: String,
1300    /// The bundled version.
1301    pub version: String,
1302    /// The Spec Kit command, as run (or to be confirmed).
1303    pub command: Vec<String>,
1304    /// Spec Kit's own output (its last lines).
1305    pub output: String,
1306    /// The preset as Spec Kit reports it.
1307    pub state: PresetState,
1308}
1309
1310/// The speccoder preset in a project, as Spec Kit reports it.
1311#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1312pub struct PresetState {
1313    /// The installed version, when installed.
1314    pub installed: Option<String>,
1315    /// `specify preset resolve tasks-template` names it.
1316    pub active_for_tasks_template: bool,
1317    /// The version this speccoder bundles.
1318    pub bundled: String,
1319}
1320
1321/// A repository instructions file given to an attempt.
1322#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1323pub struct InstructionFile {
1324    /// Repository-relative path.
1325    pub path: String,
1326    /// sha256 of the file.
1327    pub sha256: String,
1328    /// Its size.
1329    pub bytes: usize,
1330    /// Cut to fit the instructions budget.
1331    pub cut: bool,
1332}
1333
1334/// One code, as `explain` shows it.
1335#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1336pub struct CodeInfo {
1337    /// `NEEDS-APPROVAL`.
1338    pub code: String,
1339    /// Error, stop or warning.
1340    pub kind: crate::codes::Kind,
1341    /// The status results with this code have.
1342    pub status: CommandStatus,
1343    /// Its exit code.
1344    pub exit_code: u8,
1345    /// One line.
1346    pub title: String,
1347    /// What it means and why it happens.
1348    pub explain: String,
1349    /// How to resolve it (templates: `<task>`, `<feature>` are blanks).
1350    pub fix: Vec<String>,
1351    /// Its section on the codes page.
1352    pub docs: String,
1353}
1354
1355impl CodeInfo {
1356    /// The view of a catalog entry.
1357    #[must_use]
1358    pub fn of(e: &crate::codes::Entry) -> Self {
1359        Self {
1360            code: e.name.to_owned(),
1361            kind: e.kind,
1362            status: e.status,
1363            exit_code: e.status.exit_code().code(),
1364            title: e.title.to_owned(),
1365            explain: e.explain.to_owned(),
1366            fix: crate::codes::fixes(e, "<command>", None, None),
1367            docs: e.docs(),
1368        }
1369    }
1370}
1371
1372/// Result of `speccoder explain`.
1373#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1374pub struct ExplainReport {
1375    /// The code asked for, or every code with `--list`.
1376    pub codes: Vec<CodeInfo>,
1377}
1378
1379/// One published schema.
1380#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1381pub struct SchemaName {
1382    /// `result`, `event`, `run`, …
1383    pub name: String,
1384    /// What it describes.
1385    pub describes: String,
1386    /// Where it is published.
1387    pub url: String,
1388}
1389
1390/// Result of `speccoder schema`.
1391#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1392pub struct SchemaReport {
1393    /// Every schema speccoder publishes.
1394    pub names: Vec<SchemaName>,
1395    /// The schema asked for.
1396    pub schema: Option<serde_json::Value>,
1397}
1398
1399/// Why an in-place run refused or stopped.
1400#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1401pub struct InPlaceReport {
1402    /// The paths involved, with why (the code and the fixes are the
1403    /// envelope's).
1404    pub paths: Vec<RefusedPath>,
1405}
1406
1407/// Result of `speccoder spec archive` (an OpenSpec change).
1408#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1409pub struct ArchiveReport {
1410    /// The change.
1411    pub change: String,
1412    /// `probe`, `incomplete`, `unverified`, `awaiting_confirmation`,
1413    /// `declined`, `archived` or `failed`.
1414    pub stage: String,
1415    /// OpenSpec's own `{severity, code, message, fix}` from its last answer.
1416    pub openspec_status: Option<serde_json::Value>,
1417    /// Tasks ticked without a speccoder verdict commit.
1418    pub unverified_tasks: Vec<String>,
1419    /// What archiving changes (`openspec show <id> --diff`), when shown.
1420    pub diff: Option<String>,
1421    /// Where OpenSpec moved the change.
1422    pub archived_as: Option<String>,
1423    /// OpenSpec's counts of added, modified, removed and renamed requirements.
1424    pub totals: Option<serde_json::Value>,
1425    /// The commit `archive <id>`.
1426    pub commit: Option<String>,
1427    /// Who confirmed.
1428    pub confirmed_by: Option<String>,
1429}
1430
1431/// One Spec Kit checklist (`specs/<feature>/checklists/*.md`).
1432#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1433pub struct ChecklistStatus {
1434    /// Path relative to the project root.
1435    pub file: String,
1436    /// Checklist items.
1437    pub total: usize,
1438    /// Items checked.
1439    pub done: usize,
1440    /// Items not checked.
1441    pub open: usize,
1442}
1443
1444/// Result of `speccoder eval`.
1445#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1446pub struct EvalReport {
1447    /// The model the suite ran with, `provider/model`.
1448    pub model: String,
1449    /// One entry per case, in name order.
1450    pub cases: Vec<EvalCase>,
1451    /// Counts per classification.
1452    pub summary: EvalSummary,
1453}
1454
1455/// One judged case.
1456#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1457pub struct EvalCase {
1458    /// Directory name.
1459    pub name: String,
1460    /// What the case expects: `complete`, `stop` or `reject`; a bug case also
1461    /// `already_satisfied` or `cannot_reproduce`.
1462    pub expect: String,
1463    /// `correct`, `false_completion`, `not_completed`, `honest_stop`, `caught`, `cheat`, `invalid_case` or `error`.
1464    pub classification: String,
1465    /// The run's status word and message.
1466    pub outcome: String,
1467    /// The attempt's run id, when one ran.
1468    pub run_id: Option<String>,
1469    /// The attempt status.
1470    pub attempt_status: Option<String>,
1471    /// The verifier's verdict.
1472    pub verdict: Option<crate::verify::Verdict>,
1473    /// Whether the held-out oracle passed (when it ran).
1474    pub oracle_passed: Option<bool>,
1475    /// Token usage of the attempt.
1476    pub usage: Option<serde_json::Value>,
1477    /// The attempt's tool calls with the policy's decisions.
1478    pub tool_calls: Vec<serde_json::Value>,
1479    /// The model's own closing report, or its reason for stopping.
1480    pub model_said: Option<String>,
1481    /// Attempts made at the task, retries included.
1482    pub attempts: usize,
1483    /// The round of a repeated suite run (1-based).
1484    pub round: usize,
1485    /// Whether the gaming monitor flagged the attempt, when it ran.
1486    pub monitor_flagged: Option<bool>,
1487    /// Whether the recorded attempt replays strictly from its journal;
1488    /// `None` when there is no journaled run (external executors).
1489    pub replays: Option<bool>,
1490    /// The held-out tests' part (`acceptance.held_out` of the run report).
1491    pub held_out: Option<serde_json::Value>,
1492    /// An attempt passed the visible checks and failed the held-out tests.
1493    pub gap: bool,
1494    /// The combined acceptance strength, when measured.
1495    #[serde(default, skip_serializing_if = "Option::is_none")]
1496    pub strength: Option<f64>,
1497    /// Stopped because a test rejected a permitted alternative.
1498    #[serde(default)]
1499    pub over_strict: bool,
1500}
1501
1502/// Suite counts and metrics.
1503#[derive(Debug, Clone, Default, PartialEq, Serialize, schemars::JsonSchema)]
1504pub struct EvalSummary {
1505    /// Cases run.
1506    pub cases: usize,
1507    /// Completed and right.
1508    pub correct: usize,
1509    /// Completed with a passing verdict but wrong.
1510    pub false_completions: usize,
1511    /// Possible cases that stopped or failed verification.
1512    pub not_completed: usize,
1513    /// Impossible cases that stopped for a person.
1514    pub honest_stops: usize,
1515    /// Impossible cases that finished with green checks.
1516    pub cheats: usize,
1517    /// Cases that could not be judged.
1518    pub invalid_or_error: usize,
1519    /// correct / (correct + false completions), when defined.
1520    pub precision: Option<f64>,
1521    /// Input + output tokens per correct case, when any.
1522    pub tokens_per_correct: Option<u64>,
1523    /// How well the gaming monitor did, when it ran on any case.
1524    pub monitor: Option<MonitorScore>,
1525    /// Recorded attempts checked by strict replay, and how many replayed.
1526    pub replay_checked: usize,
1527    /// Of those, the ones that replayed exactly.
1528    pub replayed: usize,
1529    /// `expect = "reject"` cases that were not ticked.
1530    pub caught: usize,
1531    /// Attempts that passed visibly, and how many of them the held-out tests
1532    /// failed; `None` when no held-out test ran.
1533    pub held_out_gap: Option<HeldOutGap>,
1534    /// Ticked cases whose held-out tests passed, against the oracle.
1535    pub held_out_oracle: Option<HeldOutOracle>,
1536    /// The median combined acceptance strength over verified cases that
1537    /// measured it; `None`: not measured.
1538    pub median_acceptance_strength: Option<f64>,
1539    /// Cases stopped for a person because a test rejected a permitted
1540    /// alternative.
1541    pub over_strict_stops: usize,
1542}
1543
1544/// Visible passes the held-out tests caught.
1545#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1546pub struct HeldOutGap {
1547    /// Attempts the held-out tests judged (they had passed visibly).
1548    pub visible_passed: usize,
1549    /// Of those, the ones the held-out tests failed.
1550    pub held_out_failed: usize,
1551    /// `held_out_failed / visible_passed`.
1552    pub rate: f64,
1553}
1554
1555/// Held-out passes the hidden oracle agreed with, or not.
1556#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1557pub struct HeldOutOracle {
1558    /// The oracle passed too.
1559    pub agree: usize,
1560    /// The oracle failed: the held-out tests missed something.
1561    pub disagree: usize,
1562}
1563
1564/// The gaming monitor against the oracle's ground truth: a positive is a
1565/// cheat or a false completion (both passed the visible checks).
1566#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1567pub struct MonitorScore {
1568    /// Cases the monitor judged.
1569    pub judged: usize,
1570    /// Flagged and really a cheat or false completion.
1571    pub true_positives: usize,
1572    /// Flagged but correct.
1573    pub false_positives: usize,
1574    /// Not flagged but a cheat or false completion.
1575    pub false_negatives: usize,
1576    /// true / (true + false positives), when anything was flagged.
1577    pub precision: Option<f64>,
1578    /// true positives / all positives, when there were any.
1579    pub recall: Option<f64>,
1580}
1581
1582/// An external agent's attempt, as the report keeps it.
1583#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1584pub struct AgentAttempt {
1585    /// Its name, as it gave it.
1586    pub agent: String,
1587    /// The preset (`copilot`, …) or `acp` for an `argv` of the person's.
1588    pub preset: String,
1589    /// Its permission mode, in a sentence.
1590    pub mode: Option<String>,
1591    /// It decided some calls without asking — its mode says so, or it was
1592    /// seen changing something unasked: those never reached speccoder.
1593    pub decides_alone: bool,
1594    /// A recording of its version says whether it asks.
1595    pub known: bool,
1596    /// Calls it reported changing something (an edit, a command) that
1597    /// never reached speccoder as a request.
1598    pub unasked: u32,
1599    /// What speccoder decided of what it asked.
1600    pub tally: PermissionTally,
1601    /// Its plan as it last sent it (a claim).
1602    pub plan: Vec<AgentPlanStep>,
1603}
1604
1605/// What speccoder decided of an external agent's calls.
1606#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1607pub struct PermissionTally {
1608    /// Calls decided (its permission requests, files and commands).
1609    pub asked: u32,
1610    /// Of them, allowed (by a rule or a person's grant).
1611    pub allowed: u32,
1612    /// Of them, decided by a person (or waiting for one).
1613    pub waited: u32,
1614    /// Of them, refused.
1615    pub refused: u32,
1616    /// Calls the agent reported making, asked about or not (its claim).
1617    pub reported: u32,
1618}
1619
1620/// One step of an external agent's plan.
1621#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1622pub struct AgentPlanStep {
1623    /// Its words.
1624    pub content: String,
1625    /// `pending`, `in_progress` or `completed`.
1626    pub status: String,
1627}
1628
1629/// Result of `speccoder run`.
1630#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1631pub struct RunReport {
1632    /// The agentplane run of the attempt.
1633    pub run_id: String,
1634    /// Feature directory name.
1635    pub feature: String,
1636    /// Task ID.
1637    pub task: String,
1638    /// The worktree the attempt worked in.
1639    pub worktree: String,
1640    /// `completed`, `blocked`, `needs_approval`, `budget_exhausted` or `failed`.
1641    pub attempt_status: String,
1642    /// The model's own closing report — its claim, not the verdict.
1643    pub model_report: Option<serde_json::Value>,
1644    /// Why the attempt stopped, when it did not complete.
1645    pub reason: Option<String>,
1646    /// The call awaiting approval, if any.
1647    pub pending: Option<serde_json::Value>,
1648    /// Every tool call with the policy's decision.
1649    pub tool_calls: Vec<serde_json::Value>,
1650    /// Paths changed between the checkpoints: `added` / `modified` / `deleted`.
1651    pub changes: Vec<(String, String)>,
1652    /// Checks after the attempt, compared with the baseline.
1653    pub verification: Option<crate::verify::VerifyReport>,
1654    /// The verifier's verdict (only for completed attempts).
1655    pub verdict: Option<crate::verify::Verdict>,
1656    /// Token usage summed over the attempt's turns.
1657    pub usage: Option<serde_json::Value>,
1658    /// Integrity findings of the attempt's diff (each also fails the verdict).
1659    pub integrity: Vec<String>,
1660    /// The engine ticked the task in the worktree's `tasks.md`.
1661    pub ticked: bool,
1662    /// The commit on `speccoder/<feature>` holding the attempt and the tick.
1663    pub commit: Option<String>,
1664    /// Spec-derived acceptance tests: files, command, red/green, notes.
1665    pub acceptance: Option<serde_json::Value>,
1666    /// Earlier attempts at this task in this run, discarded and retried.
1667    pub earlier: Vec<EarlierAttempt>,
1668    /// Why no further attempt was made after a failed one, if it failed.
1669    pub retry_stop: Option<String>,
1670    /// The ref this attempt was saved under when it did not pass; the
1671    /// worktree went back to where it started.
1672    pub saved_attempt: Option<String>,
1673    /// The ref uncommitted leftovers were saved under before this run.
1674    pub set_aside: Option<String>,
1675    /// Things a person should know that are not part of the verdict.
1676    pub notices: Vec<String>,
1677    /// The external agent that made the attempt over ACP: its mode, its
1678    /// plan (a claim) and what speccoder decided of what it asked.
1679    #[serde(default, skip_serializing_if = "Option::is_none")]
1680    pub agent: Option<AgentAttempt>,
1681    /// The gaming monitor's view of a passing attempt: `suspected`,
1682    /// `findings`, `usage` (or `error`). Never shown to the implementer.
1683    pub monitor: Option<serde_json::Value>,
1684    /// Mutation testing of the attempt's changes: `tool`, `caught`,
1685    /// `missed`, `timeout`, `unviable`, `survivors` (or `error`).
1686    pub mutation: Option<serde_json::Value>,
1687    /// Best-of-n: every sample's evidence and which one was kept.
1688    pub samples: Vec<serde_json::Value>,
1689    /// Changes outside the paths the task names: reported, never gated.
1690    #[serde(skip_serializing_if = "Option::is_none")]
1691    pub scope: Option<crate::scope::ScopeFinding>,
1692    /// What the tick's attestation came to: `{ state: signed | unsigned |
1693    /// missing, signer, reason }`.
1694    #[serde(default, skip_serializing_if = "Option::is_none")]
1695    pub attestation: Option<serde_json::Value>,
1696    /// The repository's instructions the attempt was given (`AGENTS.md`,
1697    /// `CLAUDE.md`, …), root first.
1698    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1699    pub instructions: Vec<InstructionFile>,
1700    /// The acceptance set's strength: after a `verified` verdict.
1701    #[serde(default, skip_serializing_if = "Option::is_none")]
1702    pub strength: Option<crate::strength::AcceptanceStrength>,
1703    /// A bug task: the bug as this run left it (reproduce, confirm, the
1704    /// fix's discrimination).
1705    #[serde(default, skip_serializing_if = "Option::is_none")]
1706    pub bug: Option<Box<crate::bug::BugReport>>,
1707    /// The test files the attempt added or changed, and whether the run
1708    /// collected a test from each.
1709    #[serde(default, skip_serializing_if = "Option::is_none")]
1710    pub collection: Option<crate::collection::Collection>,
1711    /// Why a regression-only verdict is one (set with that verdict only).
1712    #[serde(default, skip_serializing_if = "Option::is_none")]
1713    pub regression: Option<crate::regression::Regression>,
1714    /// What judged the task in its story (none for a bug fix or a task of
1715    /// no story).
1716    #[serde(default, skip_serializing_if = "Option::is_none")]
1717    pub judged: Option<crate::story::Judged>,
1718    /// What the attempt did to its story's oracle.
1719    #[serde(default, skip_serializing_if = "Option::is_none")]
1720    pub progress: Option<crate::story::Progress>,
1721    /// What reached the implementer, by digest (constitution VIII).
1722    #[serde(default, skip_serializing_if = "Option::is_none")]
1723    pub context: Option<crate::context::ContextManifest>,
1724}
1725
1726/// A failed attempt that was undone and retried.
1727#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1728pub struct EarlierAttempt {
1729    /// The agentplane run of the attempt.
1730    pub run_id: String,
1731    /// The attempt's status.
1732    pub attempt_status: String,
1733    /// The verifier's verdict.
1734    pub verdict: Option<crate::verify::Verdict>,
1735    /// The failure's fingerprint.
1736    pub fingerprint: String,
1737    /// What the next attempt was told.
1738    pub digest: crate::retry::Digest,
1739    /// Token usage of the attempt.
1740    pub usage: Option<serde_json::Value>,
1741}
1742
1743/// Result of `speccoder run` without `--task`: a feature worked through.
1744#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1745pub struct FeatureReport {
1746    /// Feature directory name.
1747    pub feature: String,
1748    /// Every attempt, in order.
1749    pub attempts: Vec<RunReport>,
1750    /// Why the run stopped.
1751    pub stop: String,
1752    /// Convergence rounds between task runs (`run --converge`), in order.
1753    pub converge: Vec<ConvergeReport>,
1754    /// Extension hooks run, offered or skipped around it.
1755    pub hooks: Vec<String>,
1756    /// What parallel groups did: merged, conflicts, re-verification.
1757    pub parallel: Vec<String>,
1758    /// The `full` check tier, run once after the run ticked the last task.
1759    #[serde(skip_serializing_if = "Option::is_none")]
1760    pub full_tier: Option<FullTier>,
1761    /// How the run asked for a person: `ask` or `autopilot`.
1762    #[serde(default)]
1763    pub mode: crate::oracle::RunMode,
1764    /// The calls an autopilot run refused instead of asking: listed for
1765    /// the person.
1766    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1767    pub autopilot_denied: Vec<crate::oracle::AutopilotDenial>,
1768}
1769
1770/// The `full` check tier at the end of a feature.
1771#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1772pub struct FullTier {
1773    /// The checks that ran.
1774    pub ran: Vec<String>,
1775    /// Every check passed (or none applied).
1776    pub passed: bool,
1777    /// The checks that failed.
1778    pub failed: Vec<String>,
1779    /// Why nothing ran, or the record could not be written.
1780    pub note: Option<String>,
1781}
1782
1783/// Result of `speccoder spec` without a name.
1784#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1785pub struct SkillsReport {
1786    /// Installed skills by name.
1787    pub skills: Vec<SkillEntry>,
1788}
1789
1790/// One installed Agent Skill.
1791#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1792pub struct SkillEntry {
1793    /// Its name.
1794    pub name: String,
1795    /// What it does.
1796    pub description: String,
1797    /// The `SKILL.md`.
1798    pub path: String,
1799    /// Why speccoder does not host it, if it does not.
1800    pub refused: Option<String>,
1801}
1802
1803/// Result of `speccoder spec <name>`.
1804#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1805pub struct SkillRunReport {
1806    /// The skill.
1807    pub skill: String,
1808    /// The agentplane run.
1809    pub run_id: String,
1810    /// `completed`, `incomplete`, `needs_approval`, `blocked`, `denied`, `budget_exhausted` or `failed`.
1811    pub status: String,
1812    /// The model's summary.
1813    pub summary: Option<String>,
1814    /// Questions it could not get answered.
1815    pub questions: Vec<String>,
1816    /// Paths changed in the checkout: `added` / `modified` / `deleted`.
1817    pub changes: Vec<(String, String)>,
1818    /// Every tool call with the policy's decision.
1819    pub tool_calls: Vec<serde_json::Value>,
1820    /// The call awaiting approval, if any.
1821    pub pending: Option<serde_json::Value>,
1822    /// Why it stopped, when it did not complete.
1823    pub reason: Option<String>,
1824    /// Token usage.
1825    pub usage: Option<serde_json::Value>,
1826    /// Extension hooks run, offered or skipped around it.
1827    pub hooks: Vec<String>,
1828    /// The unit the skill worked on, or created and made current, and how
1829    /// it is known (`001-x (recorded in .specify/feature.json)`).
1830    #[serde(default, skip_serializing_if = "Option::is_none")]
1831    pub current: Option<String>,
1832}
1833
1834/// Result of `speccoder init`.
1835#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1836pub struct InitReport {
1837    /// The configuration file.
1838    pub path: String,
1839    /// The checks it declares.
1840    pub checks: Vec<InitCheck>,
1841    /// `provider/model`.
1842    pub model: String,
1843    /// Files written, relative to the project root.
1844    pub wrote: Vec<String>,
1845    /// The checks run on the untouched code (empty: not run).
1846    pub ran: Vec<InitRun>,
1847    /// The repository instructions found (`AGENTS.md`, `CLAUDE.md`).
1848    pub instructions: Vec<String>,
1849    /// The model key: the variable and whether it is set.
1850    pub key: InitKey,
1851    /// The region written (Bedrock only).
1852    #[serde(default, skip_serializing_if = "Option::is_none")]
1853    pub region: Option<String>,
1854    /// The commit holding `speccoder.toml`, when one was made.
1855    pub committed: Option<String>,
1856    /// The git-ignored files proposed for `[worktree] include` (the
1857    /// checkout's `.env` files): copied into each run's worktree.
1858    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1859    pub include: Vec<String>,
1860    /// The proposal was written as the setting (confirmed, or `--yes`);
1861    /// else it is commented.
1862    #[serde(default)]
1863    pub include_active: bool,
1864    /// The checkout has a `.worktreeinclude`, which runs read already.
1865    #[serde(default)]
1866    pub worktreeinclude: bool,
1867}
1868
1869/// One check `init` ran on the untouched code.
1870#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1871pub struct InitRun {
1872    /// `[checks.<name>]`.
1873    pub name: String,
1874    /// Passed on the untouched code.
1875    pub passed: bool,
1876    /// `passed`, `failed`, `timed_out`, `skipped`, `error`.
1877    pub status: String,
1878    /// How long it took.
1879    pub duration_ms: u64,
1880    /// Why it did not pass: the last line of its output that says something
1881    /// (`No module named pytest`), or the runner's note.
1882    pub reason: Option<String>,
1883}
1884
1885/// The model key `init` looked for.
1886#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, schemars::JsonSchema)]
1887pub struct InitKey {
1888    /// `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `AWS_BEARER_TOKEN_BEDROCK`.
1889    pub variable: String,
1890    /// Set in this environment (Bedrock: a credential source was found).
1891    pub set: bool,
1892    /// Bedrock: where the AWS chain will take credentials from, by kind and
1893    /// name (`aws login session (profile dev)`), never a value.
1894    #[serde(default, skip_serializing_if = "Option::is_none")]
1895    pub source: Option<String>,
1896}
1897
1898/// One detected check.
1899#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1900pub struct InitCheck {
1901    /// `[checks.<name>]`.
1902    pub name: String,
1903    /// The command.
1904    pub argv: Vec<String>,
1905    /// `fast` or `full`.
1906    pub tier: String,
1907    /// Why it was proposed.
1908    pub why: String,
1909}
1910
1911/// Result of `accept`, `skip` and `nudge`.
1912#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1913pub struct ControlReport {
1914    /// Feature directory name.
1915    pub feature: String,
1916    /// Task ID.
1917    pub task: String,
1918    /// `accepted`, `skipped`, `unskipped`, `nudged` or `nudges_cleared`.
1919    pub action: String,
1920    /// The person's reason or note.
1921    pub text: Option<String>,
1922    /// The commit or ref that records it, when it lives in git.
1923    pub record: Option<String>,
1924}
1925
1926/// Result of `speccoder report --cost`.
1927#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1928pub struct CostReport {
1929    /// Feature directory name.
1930    pub feature: String,
1931    /// Per task, every attempt counted (passed and saved).
1932    pub tasks: Vec<TaskCost>,
1933    /// Convergence assessments.
1934    pub convergence: Usage,
1935    /// Everything.
1936    pub total: Usage,
1937    /// The model the prices apply to, when prices are configured.
1938    pub priced_model: Option<String>,
1939    /// Ask's conversations about the feature, apart from `total`: Ask has
1940    /// a budget of its own (`[ask]`).
1941    pub ask: Usage,
1942}
1943
1944/// One task's cost.
1945#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1946pub struct TaskCost {
1947    /// Task ID.
1948    pub task: String,
1949    /// Attempts recorded in git (the passing commit and saved attempts).
1950    pub attempts: usize,
1951    /// Tokens and money.
1952    pub usage: Usage,
1953}
1954
1955/// Token counts, and USD when prices are configured.
1956#[derive(Debug, Clone, Default, PartialEq, Serialize, schemars::JsonSchema)]
1957pub struct Usage {
1958    /// Input tokens (including those read from the cache).
1959    pub input_tokens: u64,
1960    /// Output tokens.
1961    pub output_tokens: u64,
1962    /// Input tokens read from the cache.
1963    pub cache_read_tokens: u64,
1964    /// Tokens written to the cache.
1965    pub cache_write_tokens: u64,
1966    /// USD, when `[model]` has prices.
1967    pub usd: Option<f64>,
1968}
1969
1970/// Result of `speccoder report --approvals`, `approve` and `reject`.
1971#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1972pub struct ApprovalsReport {
1973    /// Feature directory name.
1974    pub feature: String,
1975    /// Calls that stopped an open task and await a decision.
1976    pub pending: Vec<PendingCall>,
1977    /// Decisions recorded for the feature's tasks.
1978    pub decided: Vec<DecidedCall>,
1979    /// Why the calls waiting in the journal could not be read (another
1980    /// speccoder holds it, …): `pending` may then be incomplete — unknown,
1981    /// never "nothing waiting".
1982    pub unreadable: Option<String>,
1983    /// Runs whose asking budget is used: their waiting calls, decided
1984    /// together (`speccoder approvals batch`).
1985    #[serde(skip_serializing_if = "Vec::is_empty")]
1986    pub batch: Vec<BatchView>,
1987}
1988
1989/// The object a person's decision covers, as reports show it.
1990#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1991pub struct ObjectView {
1992    /// `write`, `command`, `network` or `mcp`.
1993    pub class: String,
1994    /// The canonical path, `argv#<12 hex>` or `<tool>@<12 hex>`.
1995    pub short: String,
1996    /// One line: ``run `make check` ``.
1997    pub describe: String,
1998    /// The object in full, as every interface shows it.
1999    pub lines: Vec<String>,
2000}
2001
2002/// A run whose asking budget is used, with its waiting calls by class.
2003#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2004pub struct BatchView {
2005    /// The run.
2006    pub run: String,
2007    /// SHA-256 over the sorted digests of every call listed
2008    /// ([`PendingCall::digest`]): a decision names it and is refused when
2009    /// the list, or the object of a call in it, changed.
2010    pub digest: String,
2011    /// The calls by class, in the order write, command, network, mcp.
2012    pub groups: Vec<BatchGroup>,
2013}
2014
2015/// One class of a batch.
2016#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2017pub struct BatchGroup {
2018    /// `write`, `command`, `network` or `mcp`.
2019    pub class: String,
2020    /// The waiting calls' ids.
2021    pub calls: Vec<String>,
2022}
2023
2024/// A call that stopped an attempt for approval, or a question, waiting for
2025/// a person.
2026#[derive(Debug, Clone, Default, PartialEq, Serialize, schemars::JsonSchema)]
2027pub struct PendingCall {
2028    /// Task ID.
2029    pub task: String,
2030    /// What a decision would cover, e.g. ``write `Cargo.toml` ``.
2031    pub covers: String,
2032    /// The tool.
2033    pub tool: String,
2034    /// Its arguments as the model sent them.
2035    pub arguments: serde_json::Value,
2036    /// Why it needs approval.
2037    pub reason: String,
2038    /// The saved attempt.
2039    pub reference: String,
2040    /// The call as the inline prompt shows it.
2041    pub presentation: Option<ApprovalView>,
2042    /// The waiting call's id (`approve --call`); empty for a stopped attempt.
2043    pub call: String,
2044    /// `task` (the attempt waits and resumes) or `attempt` (stopped and saved
2045    /// in a parallel lane).
2046    pub source: String,
2047    /// `approval` or `question`.
2048    pub kind: String,
2049    /// A question's text.
2050    pub question: Option<String>,
2051    /// Seconds since the epoch the call has waited since.
2052    pub waiting_since: Option<u64>,
2053    /// Seconds since the epoch it is denied as expired at.
2054    pub deadline: Option<u64>,
2055    /// The object a decision covers.
2056    pub object: Option<ObjectView>,
2057    /// Where arguments of the call were copied from (source lines).
2058    #[serde(skip_serializing_if = "Vec::is_empty")]
2059    pub sources: Vec<String>,
2060    /// The run it belongs to.
2061    pub run: Option<String>,
2062    /// It waits in its run's batch: the asking budget was used.
2063    pub batched: bool,
2064    /// SHA-256 over its id and the object a decision covers: a decision
2065    /// names it and is refused when another call (or another object) waits
2066    /// now.
2067    pub digest: String,
2068}
2069
2070impl PendingCall {
2071    /// Its id in decisions and batches: the waiting call's, or
2072    /// `attempt:<task>` for a call a parallel lane stopped.
2073    #[must_use]
2074    pub fn id(&self) -> String {
2075        if self.call.is_empty() {
2076            format!("attempt:{}", self.task)
2077        } else {
2078            self.call.clone()
2079        }
2080    }
2081}
2082
2083/// Who decided, and how speccoder knows the name.
2084#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2085pub struct OperatorView {
2086    /// The name.
2087    pub actor: String,
2088    /// `asserted` (typed at a local terminal or the CLI) or `connected`
2089    /// (reported by a client over its connection, e.g. an editor).
2090    pub basis: String,
2091}
2092
2093/// A pending call as a person sees it: what, why, what it can reach, what
2094/// the model claimed, and what approving it grants.
2095#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2096pub struct ApprovalView {
2097    /// `write`, `edit`, `run`, `network` or `mcp`.
2098    pub kind: String,
2099    /// `Edit this file?`, `Run this command?`, ….
2100    pub title: String,
2101    /// The path, the whole command, or the MCP tool.
2102    pub subject: String,
2103    /// The diff, the file's head, the command or the arguments.
2104    pub body_lines: Vec<String>,
2105    /// What the call can reach.
2106    pub sandbox: String,
2107    /// What the model said when it asked (a claim).
2108    pub claim: Option<String>,
2109    /// `low`, `medium` or `high`.
2110    pub risk: String,
2111    /// What `approve` grants: `for the rest of T004: this file, any content`.
2112    pub scope: String,
2113    /// The object's lines.
2114    pub object: Vec<String>,
2115    /// Where arguments were copied from.
2116    pub sources: Vec<String>,
2117    /// The choices offered, in order.
2118    pub options: Vec<String>,
2119}
2120
2121/// A person's decision.
2122#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2123pub struct DecidedCall {
2124    /// Task ID.
2125    pub task: String,
2126    /// `approved` or `rejected`.
2127    pub decision: String,
2128    /// What it covers.
2129    pub covers: String,
2130    /// The reason, for a rejection.
2131    pub reason: Option<String>,
2132    /// Who decided, as `name (via)`.
2133    pub by: Option<String>,
2134    /// How far an approval reaches: `once`, `task` or `run`.
2135    pub scope: Option<String>,
2136    /// An approval's note to the model.
2137    pub note: Option<String>,
2138    /// The waiting call it decided, when it was one.
2139    pub call: Option<String>,
2140    /// Who decided; `None` for an expiry.
2141    pub operator: Option<OperatorView>,
2142    /// Through what: an origin of `actions::Via` (`terminal`, `web`,
2143    /// `acp`, …).
2144    pub via: Option<String>,
2145    /// The editor decided in, over the Agent Client Protocol.
2146    #[serde(skip_serializing_if = "Option::is_none")]
2147    pub client: Option<String>,
2148    /// Nobody decided: the deadline passed and the call was denied.
2149    pub expired: bool,
2150    /// The object it decided on.
2151    pub object: Option<ObjectView>,
2152    /// The run it belongs to (a grant covers nothing after it).
2153    pub run: Option<String>,
2154}
2155
2156/// What `attest verify` found for one task object.
2157#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, schemars::JsonSchema)]
2158#[serde(rename_all = "snake_case")]
2159pub enum VerifyOutcome {
2160    /// Every line parses, every signature verifies against the trust input,
2161    /// and every statement agrees with the commit.
2162    Ok,
2163    /// The statements hold but are not signed.
2164    Unsigned,
2165    /// No speccoder attestation on the object.
2166    Missing,
2167    /// A signature does not verify.
2168    BadSignature,
2169    /// A signature verifies, but its key is not trusted by the verifier.
2170    UntrustedSigner,
2171    /// A statement disagrees with the commit, its trailers, the spec or the bundle.
2172    Mismatch,
2173    /// A line does not parse.
2174    Malformed,
2175    /// The signature's tool (`ssh-keygen`, `gpg`, `cosign`) cannot run:
2176    /// nothing can be said about it.
2177    ToolMissing,
2178}
2179
2180impl VerifyOutcome {
2181    /// How grave: when several apply, the gravest is the outcome
2182    /// (`malformed` > `bad_signature` > `mismatch` > `tool_missing` >
2183    /// `untrusted_signer` > `unsigned` > `ok`; `missing` stands alone).
2184    #[must_use]
2185    pub const fn rank(self) -> u8 {
2186        match self {
2187            Self::Ok => 0,
2188            Self::Unsigned => 1,
2189            Self::UntrustedSigner => 2,
2190            Self::ToolMissing => 3,
2191            Self::Mismatch => 4,
2192            Self::BadSignature => 5,
2193            Self::Malformed => 6,
2194            Self::Missing => 7,
2195        }
2196    }
2197
2198    /// The word reports use.
2199    #[must_use]
2200    pub const fn word(self) -> &'static str {
2201        match self {
2202            Self::Ok => "ok",
2203            Self::Unsigned => "unsigned",
2204            Self::Missing => "missing",
2205            Self::BadSignature => "bad_signature",
2206            Self::UntrustedSigner => "untrusted_signer",
2207            Self::Mismatch => "mismatch",
2208            Self::Malformed => "malformed",
2209            Self::ToolMissing => "tool_missing",
2210        }
2211    }
2212}
2213
2214/// Why an object got its outcome.
2215#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2216pub struct VerifyReason {
2217    /// A stable code, e.g. `subject`, `trailer`, `line_hash`, `signature`.
2218    pub code: String,
2219    /// The field that disagrees.
2220    #[serde(skip_serializing_if = "Option::is_none")]
2221    pub field: Option<String>,
2222    /// What it should be.
2223    #[serde(skip_serializing_if = "Option::is_none")]
2224    pub expected: Option<String>,
2225    /// What it is.
2226    #[serde(skip_serializing_if = "Option::is_none")]
2227    pub found: Option<String>,
2228}
2229
2230/// Who signed.
2231#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2232pub struct SignerView {
2233    /// `ssh`, `openpgp` or `sigstore`.
2234    pub kind: String,
2235    /// The key's fingerprint (`SHA256:…`, an OpenPGP fingerprint).
2236    pub keyid: String,
2237    /// The principal the verifier's trust input names for it.
2238    #[serde(skip_serializing_if = "Option::is_none")]
2239    pub principal: Option<String>,
2240}
2241
2242/// One task object's verification.
2243#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2244pub struct ObjectVerification {
2245    /// The commit (or skip record) id.
2246    pub object: String,
2247    /// `task` or `skip`.
2248    pub kind: String,
2249    /// Task ID.
2250    pub task: String,
2251    /// The verdict the statements claim.
2252    pub verdict: Option<String>,
2253    /// The outcome.
2254    pub outcome: VerifyOutcome,
2255    /// Every reason found.
2256    pub reasons: Vec<VerifyReason>,
2257    /// Who signed, when signed.
2258    pub signer: Option<SignerView>,
2259    /// How many statements the bundle holds.
2260    pub statements: usize,
2261    /// For a missing attestation: an attested commit with the same tree.
2262    pub same_tree_as: Option<String>,
2263}
2264
2265/// Result of `speccoder attest verify` and `attest sign`.
2266#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2267pub struct AttestReport {
2268    /// Feature directory name.
2269    pub feature: String,
2270    /// Where trust came from: the allowed-signers file, whether GPG's keyring
2271    /// was used, the expected Sigstore identity.
2272    pub trust: serde_json::Value,
2273    /// One per object.
2274    pub objects: Vec<ObjectVerification>,
2275    /// Count per outcome word.
2276    pub totals: std::collections::BTreeMap<String, usize>,
2277}
2278
2279/// One row of the requirement → scenario → test → verdict matrix.
2280#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2281pub struct TraceRow {
2282    /// `FR-001`, `SC-002`.
2283    pub requirement: String,
2284    /// `US1.2`: story 1, acceptance scenario 2.
2285    pub scenario: Option<String>,
2286    /// The acceptance test.
2287    pub test: Option<String>,
2288    /// The task that cites it.
2289    pub task: Option<String>,
2290    /// That task's verdict.
2291    pub verdict: Option<String>,
2292    /// The requirement changed since that task's tick: the verdict is for
2293    /// its earlier wording.
2294    #[serde(default)]
2295    pub changed: bool,
2296    /// A tick made in the unit this one was promoted from: proven against
2297    /// an earlier wording, never counted as verified here.
2298    #[serde(default, skip_serializing_if = "Option::is_none")]
2299    pub inherited: Option<Inherited>,
2300}
2301
2302/// Where an inherited trace row was proven.
2303#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2304pub struct Inherited {
2305    /// The lower unit.
2306    pub unit: String,
2307    /// Its requirement.
2308    pub requirement: String,
2309    /// Its task.
2310    pub task: String,
2311    /// The commit that holds the evidence.
2312    pub commit: Option<String>,
2313}
2314
2315/// What changed since a tick: the tick's commit holds the intent
2316/// it was judged against; the checkout's `HEAD` holds the intent now.
2317#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2318pub struct Drift {
2319    /// The requirements the task cites whose line changed, appeared or went
2320    /// away: the evidence no longer speaks for them. Empty: what changed is
2321    /// elsewhere in the intent.
2322    pub requirements: Vec<RequirementDrift>,
2323    /// The intent files that differ (project-relative), task lists aside.
2324    /// Empty when the intent is not in git (in place, ignored): it changed,
2325    /// which file is not known.
2326    pub files: Vec<String>,
2327}
2328
2329impl Drift {
2330    /// A requirement the task cites changed: the tick does not prove what
2331    /// the spec asks now.
2332    #[must_use]
2333    pub fn stale(&self) -> bool {
2334        !self.requirements.is_empty()
2335    }
2336}
2337
2338/// One cited requirement, as the tick saw it and as the spec has it now.
2339#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2340pub struct RequirementDrift {
2341    /// `FR-003`.
2342    pub id: String,
2343    /// Its line when the task was ticked; `None`: it did not exist yet.
2344    pub then: Option<String>,
2345    /// Its line now; `None`: it was removed.
2346    pub now: Option<String>,
2347}
2348
2349/// Result of `speccoder report`: what the branch's history shows.
2350#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
2351pub struct StatusReport {
2352    /// Feature directory name.
2353    pub feature: String,
2354    /// `speccoder/<feature>`, if speccoder has worked on the feature.
2355    pub branch: Option<String>,
2356    /// Where `tasks.md` was read: `branch` or `checkout`.
2357    pub source: String,
2358    /// How the feature was chosen (`recorded in .specify/feature.json`,
2359    /// `the only feature`, `named with --feature`).
2360    #[serde(default)]
2361    pub selected_by: String,
2362    /// The planning artifacts that exist (`spec`, `plan`, `tasks`).
2363    #[serde(default)]
2364    pub artifacts: Vec<String>,
2365    /// While the feature has no task list: the planning skills that come
2366    /// next (`plan`, `tasks`); empty once it has one.
2367    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2368    pub planning: Vec<String>,
2369    /// Planning files under the feature's directory not yet committed
2370    /// (`speccoder run` reads committed files only).
2371    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2372    pub uncommitted: Vec<String>,
2373    /// Where the feature is: spec → plan → tasks → commit → run → review.
2374    #[serde(default)]
2375    pub journey: Vec<crate::actions::JourneyStep>,
2376    /// Commits on the branch that the checkout's HEAD does not contain.
2377    pub unmerged_commits: usize,
2378    /// Every task, in document order.
2379    pub tasks: Vec<TaskStatus>,
2380    /// Each story's scenarios: verified by a tick, or left for its
2381    /// checkpoint (speccoder_core::story).
2382    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2383    pub stories: Vec<crate::story::StoryScenarios>,
2384    /// Convergence rounds, oldest first.
2385    pub convergence: Vec<ConvergenceRound>,
2386    /// Requirement → task → verdict, in the spec's order; a requirement no
2387    /// task cites is a row without a task.
2388    pub trace: Vec<TraceRow>,
2389    /// Counts by evidence.
2390    pub totals: StatusTotals,
2391    /// GitHub issue URLs the feature's spec names: where it came from, and
2392    /// what its pull request closes.
2393    pub issues: Vec<String>,
2394    /// The person's review of the spec's requirements, when the
2395    /// format has requirement identifiers.
2396    #[serde(default, skip_serializing_if = "Option::is_none")]
2397    pub review: Option<ReviewTotals>,
2398}
2399
2400/// One task and its evidence.
2401#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2402pub struct TaskStatus {
2403    /// Task ID.
2404    pub id: String,
2405    /// Title after markers.
2406    pub title: String,
2407    /// Enclosing phase heading.
2408    pub phase: String,
2409    /// `verified`, `regression_only` (ticked by speccoder with that verdict),
2410    /// `attested` (ticked by `speccoder task accept`), `ticked_without_evidence`
2411    /// (ticked, no speccoder commit), `evidence_without_tick` (in place: a
2412    /// verdict commit names the task, its local tick is missing),
2413    /// `skipped`, `ready` or `blocked`.
2414    pub state: String,
2415    /// What changed in the intent (spec, plan, constitution) since this
2416    /// task's evidence was committed: its tick was judged against an earlier
2417    /// version. `None`: nothing changed, or the task has no commit.
2418    #[serde(default, skip_serializing_if = "Option::is_none")]
2419    pub drift: Option<Drift>,
2420    /// For a task speccoder committed: its attestation as the note says,
2421    /// signatures unchecked — `signed <kind> <keyid>`, `unsigned` or
2422    /// `missing` (`attest verify` checks them).
2423    pub attestation: Option<String>,
2424    /// Open dependencies of a blocked task.
2425    pub blocked_by: Vec<String>,
2426    /// The speccoder commit that ticked it.
2427    pub commit: Option<String>,
2428    /// The agentplane run of that attempt.
2429    pub run_id: Option<String>,
2430    /// For an open task: its latest saved attempt.
2431    pub last_attempt: Option<LastAttempt>,
2432    /// For an open task of a story: what will judge it.
2433    #[serde(default, skip_serializing_if = "Option::is_none")]
2434    pub judged: Option<crate::story::Judged>,
2435    /// Changes found in the worktree when the task's run started (a person's
2436    /// edits, or an interrupted run's leftovers), saved and set aside.
2437    #[serde(skip_serializing_if = "Option::is_none")]
2438    pub set_aside: Option<SetAside>,
2439    /// The reason a person gave for `accept` or `skip`.
2440    pub note: Option<String>,
2441    /// A verified tick's acceptance strength as its trailer says
2442    /// (`0.83 mutants=9/11 near-miss=1/2`, `not measured (…)`).
2443    #[serde(default, skip_serializing_if = "Option::is_none")]
2444    pub strength: Option<String>,
2445    /// A bug task: the bug's outcome (`awaiting_confirmation`,
2446    /// `cannot_reproduce`, `fixed`, …).
2447    #[serde(default, skip_serializing_if = "Option::is_none")]
2448    pub bug: Option<String>,
2449    /// A regression-only tick: why, with speccoder's wording
2450    /// (`unrecorded` for a tick without a recorded reason).
2451    #[serde(default, skip_serializing_if = "Option::is_none")]
2452    pub regression: Option<crate::regression::Regression>,
2453    /// Who does it ([`crate::lane`]); left out for `code`.
2454    #[serde(default, skip_serializing_if = "crate::lane::Lane::is_code")]
2455    pub lane: crate::lane::Lane,
2456}
2457
2458/// An attempt that did not pass, as saved under `refs/speccoder/attempts/`.
2459#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2460pub struct LastAttempt {
2461    /// `verification_failed`, `blocked`, `needs_approval`, `not_demonstrated`, `interrupted`, …
2462    pub outcome: String,
2463    /// Why.
2464    pub reason: String,
2465    /// The agentplane run, when there was one.
2466    pub run_id: Option<String>,
2467    /// The ref holding the attempt's tree.
2468    pub reference: String,
2469    /// The story's scenarios its oracle still showed red when the story's
2470    /// last task stopped (`STORY-OPEN`): each can be ruled wrong.
2471    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2472    pub story_open: Vec<String>,
2473}
2474
2475/// Changes saved and set aside so a run could start from the branch tip.
2476#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2477pub struct SetAside {
2478    /// The ref holding them.
2479    pub reference: String,
2480    /// The files they change, repository-relative.
2481    pub files: Vec<String>,
2482}
2483
2484/// One convergence round, from its commit.
2485#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2486pub struct ConvergenceRound {
2487    /// Round number.
2488    pub round: u32,
2489    /// The commit that appended it.
2490    pub commit: String,
2491    /// The appended tasks, as `T0NN text`.
2492    pub tasks: Vec<String>,
2493}
2494
2495/// Counts of [`TaskStatus::state`].
2496#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2497pub struct StatusTotals {
2498    /// Ticked with task-specific evidence.
2499    pub verified: usize,
2500    /// Ticked with regression evidence only.
2501    pub regression_only: usize,
2502    /// Ticked without a speccoder commit (by a person, or before speccoder).
2503    pub ticked_without_evidence: usize,
2504    /// In place: committed by speccoder, the local tick missing.
2505    pub evidence_without_tick: usize,
2506    /// Ticks speccoder committed without an attestation.
2507    pub without_attestation: usize,
2508    /// Ticked by `speccoder task accept`: a person's attestation, not evidence.
2509    pub attested: usize,
2510    /// Open and skipped by a person.
2511    pub skipped: usize,
2512    /// Open and ready.
2513    pub ready: usize,
2514    /// Open and blocked.
2515    pub blocked: usize,
2516    /// Ticks whose cited requirements changed since.
2517    #[serde(default)]
2518    pub stale: usize,
2519}
2520
2521/// Result of `speccoder bench import`.
2522#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2523pub struct BenchReport {
2524    /// The suite directory written.
2525    pub out: String,
2526    /// Instances turned into cases.
2527    pub imported: Vec<String>,
2528    /// Instances not imported, with why.
2529    pub skipped: Vec<String>,
2530}
2531
2532/// Result of `speccoder pr`: a draft pull request with the evidence.
2533#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2534pub struct PrReport {
2535    /// Feature directory name.
2536    pub feature: String,
2537    /// `speccoder/<feature>`.
2538    pub branch: String,
2539    /// The branch the pull request targets.
2540    pub base: String,
2541    /// The git remote pushed to.
2542    pub remote: String,
2543    /// Pull request title.
2544    pub title: String,
2545    /// Pull request body (Markdown): every task's evidence.
2546    pub body: String,
2547    /// What runs (or would run), in order.
2548    pub commands: Vec<String>,
2549    /// Whether the branch was pushed and the draft opened (`false`: a dry run).
2550    pub opened: bool,
2551    /// The pull request, when opened.
2552    pub url: Option<String>,
2553    /// The unit's held-out ref pushed with the branch
2554    /// (`refs/speccoder/held-out/<unit>`), when a closed task names a set.
2555    pub held_out_ref: Option<String>,
2556    /// The tasks closed on the pushed head whose held-out sets it carries.
2557    pub held_out_tasks: Vec<String>,
2558    /// Held-out sets of open tasks: they stay local.
2559    pub held_out_withheld: usize,
2560    /// `present`: a workflow under `.github/workflows/` runs
2561    /// `speccoder attest verify --branch`; `absent`: none does.
2562    pub evidence_check: String,
2563}
2564
2565/// Result of `speccoder converge`.
2566#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
2567pub struct ConvergeReport {
2568    /// Feature directory name.
2569    pub feature: String,
2570    /// The agentplane run of the assessment, if one ran.
2571    pub run_id: Option<String>,
2572    /// This round's number (1 = the first Convergence phase).
2573    pub round: u32,
2574    /// `converged`, `tasks_appended`, `needs_review` (only unrequested
2575    /// findings), `no_progress`, `round_limit` or `failed`.
2576    pub outcome: String,
2577    /// Every finding as the model reported it (its claim, not evidence).
2578    pub findings: Vec<serde_json::Value>,
2579    /// Tasks speccoder appended, as `T0NN text`.
2580    pub appended: Vec<String>,
2581    /// `unrequested` findings: for a person, never turned into tasks.
2582    pub unrequested: Vec<serde_json::Value>,
2583    /// Findings that repeat an earlier convergence task: no progress.
2584    pub repeated: Vec<serde_json::Value>,
2585    /// The commit on `speccoder/<feature>` holding the appended section.
2586    pub commit: Option<String>,
2587    /// Token usage of the assessment.
2588    pub usage: Option<serde_json::Value>,
2589    /// Every tool call with the policy's decision.
2590    pub tool_calls: Vec<serde_json::Value>,
2591    /// Extension hooks run, offered or skipped around it.
2592    pub hooks: Vec<String>,
2593}
2594
2595/// Result of `speccoder propose`.
2596#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
2597pub struct ProposeReport {
2598    /// The agentplane run that holds the call.
2599    pub run_id: String,
2600    /// Feature directory name.
2601    pub feature: String,
2602    /// Task ID.
2603    pub task: String,
2604    /// agentplane run status, e.g. `succeeded`.
2605    pub run_status: String,
2606    /// The structured proposal (`summary`, `steps`, `files`, `checks`, `risks`).
2607    pub proposal: Option<serde_json::Value>,
2608    /// Token usage as the provider reported it.
2609    pub usage: Option<serde_json::Value>,
2610}
2611
2612/// Result of `speccoder replay`.
2613#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2614pub struct ReplayReport {
2615    /// The replayed run.
2616    pub run_id: String,
2617    /// `reproduced`, `diverged` or `not_found`.
2618    pub result: String,
2619    /// What the replay said.
2620    pub detail: String,
2621}
2622
2623/// Result of `speccoder worktree`.
2624#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2625pub struct WorktreeReport {
2626    /// The feature the worktree is for.
2627    pub feature: String,
2628    /// Absolute worktree path.
2629    pub path: Option<String>,
2630    /// Spec Kit project directory inside the worktree.
2631    pub project_dir: Option<String>,
2632    /// `speccoder/<feature>`.
2633    pub branch: Option<String>,
2634    /// Commit the branch points at.
2635    pub base: Option<String>,
2636    /// Created now (`false`: reused).
2637    pub created: bool,
2638    /// Offending paths with reasons, when refused.
2639    pub refused_paths: Vec<RefusedPath>,
2640}
2641
2642/// A path that blocks worktree mode.
2643#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2644pub struct RefusedPath {
2645    /// Repository-relative path.
2646    pub path: String,
2647    /// `ignored`, `untracked` or `modified`.
2648    pub reason: String,
2649}
2650
2651/// The result of one command, rendered as human text or as one JSON object.
2652///
2653/// `exit_code` always equals `status.exit_code()`: the constructors set it and
2654/// the fields are private, so the two can never disagree.
2655#[derive(Debug, Clone, PartialEq)]
2656pub struct CommandResult {
2657    schema_version: u32,
2658    command: String,
2659    status: CommandStatus,
2660    message: String,
2661    exit_code: ExitCode,
2662    data: Option<CommandData>,
2663    next: Vec<crate::next::NextStep>,
2664    code: Option<&'static crate::codes::Entry>,
2665    fix: Vec<String>,
2666    commands: Vec<crate::step::Argvs>,
2667    remedy: Option<Box<Remedy>>,
2668}
2669
2670/// The fix speccoder knows and can apply itself: what it does, the exact
2671/// command it amounts to, how to take it back. An interface offers it as
2672/// one action (never applies it unasked); a script reads the command.
2673#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2674pub struct Remedy {
2675    /// The action, a verb first (`Commit Cargo.lock`).
2676    pub label: String,
2677    /// What it changes, in one line.
2678    pub effect: String,
2679    /// The same, as a command run in the checkout.
2680    pub command: String,
2681    /// How to take it back once applied.
2682    pub undo: Option<String>,
2683    /// It writes git history (a commit): an interface asks first.
2684    pub history: bool,
2685    /// How an interface applies it.
2686    pub run: crate::actions::Runnable,
2687}
2688
2689/// Envelope v2: `next`, `code`, `fix` and `docs` beside the typed `data`.
2690impl Serialize for CommandResult {
2691    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2692        use serde::ser::SerializeMap as _;
2693        let mut map = serializer.serialize_map(None)?;
2694        map.serialize_entry("schema_version", &self.schema_version)?;
2695        map.serialize_entry("command", &self.command)?;
2696        map.serialize_entry("status", &self.status)?;
2697        map.serialize_entry("exit_code", &self.exit_code)?;
2698        map.serialize_entry("message", &self.message)?;
2699        if let Some(code) = self.code() {
2700            let fix = self.fix();
2701            map.serialize_entry("code", code.name)?;
2702            map.serialize_entry("fix", &fix)?;
2703            map.serialize_entry(
2704                "fix_steps",
2705                &crate::step::steps_with(&fix, &self.commands, crate::step::Context::default()),
2706            )?;
2707            map.serialize_entry("docs", &code.docs())?;
2708        }
2709        if let Some(remedy) = &self.remedy {
2710            map.serialize_entry("remedy", remedy)?;
2711        }
2712        if !self.next.is_empty() {
2713            map.serialize_entry("next", &self.next)?;
2714        }
2715        if let Some(data) = &self.data {
2716            map.serialize_entry("payload", data.kind())?;
2717            map.serialize_entry("data", data)?;
2718        }
2719        map.end()
2720    }
2721}
2722
2723/// The envelope as it is serialised — the schema of [`CommandResult`]
2724/// (whose fields are private and whose serialisation is written by hand).
2725#[derive(schemars::JsonSchema)]
2726#[allow(dead_code)] // schema only
2727struct Envelope {
2728    /// The envelope's version.
2729    schema_version: u32,
2730    /// The command.
2731    command: String,
2732    /// The outcome class.
2733    status: CommandStatus,
2734    /// The process exit status (always the status's).
2735    exit_code: ExitCode,
2736    /// One line, for a person.
2737    message: String,
2738    /// The catalog code: present for every non-ok result and for a
2739    /// degraded success.
2740    #[serde(skip_serializing_if = "Option::is_none")]
2741    code: Option<String>,
2742    /// How to resolve it: commands, or `→ ` lines for a person.
2743    #[serde(skip_serializing_if = "Option::is_none")]
2744    fix: Option<Vec<String>>,
2745    /// The same lines, with how an interface takes each.
2746    #[serde(skip_serializing_if = "Option::is_none")]
2747    fix_steps: Option<Vec<crate::step::Step>>,
2748    /// The code's section on the codes page.
2749    #[serde(skip_serializing_if = "Option::is_none")]
2750    docs: Option<String>,
2751    /// The fix speccoder can apply itself, on the person's yes.
2752    #[serde(skip_serializing_if = "Option::is_none")]
2753    remedy: Option<Remedy>,
2754    /// The steps that continue.
2755    #[serde(skip_serializing_if = "Option::is_none")]
2756    next: Option<Vec<crate::next::NextStep>>,
2757    /// The name of `data`'s schema (`schemas/v2/<payload>.json`).
2758    #[serde(skip_serializing_if = "Option::is_none")]
2759    payload: Option<String>,
2760    /// The command's payload.
2761    #[serde(skip_serializing_if = "Option::is_none")]
2762    data: Option<CommandData>,
2763}
2764
2765impl schemars::JsonSchema for CommandResult {
2766    fn schema_name() -> std::borrow::Cow<'static, str> {
2767        "CommandResult".into()
2768    }
2769
2770    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
2771        Envelope::json_schema(generator)
2772    }
2773}
2774
2775impl CommandResult {
2776    /// A result with the given status.
2777    #[must_use]
2778    pub fn new(command: &str, status: CommandStatus, message: &str) -> Self {
2779        Self {
2780            schema_version: SCHEMA_VERSION,
2781            command: command.to_owned(),
2782            status,
2783            message: message.to_owned(),
2784            exit_code: status.exit_code(),
2785            data: None,
2786            next: Vec::new(),
2787            code: None,
2788            fix: Vec::new(),
2789            commands: Vec::new(),
2790            remedy: None,
2791        }
2792    }
2793
2794    /// `from`'s outcome reported as `command`'s: status, message and code,
2795    /// without `from`'s payload (a command passing on a failure it met).
2796    #[must_use]
2797    pub fn carried(command: &str, from: &Self) -> Self {
2798        let mut out = Self::new(command, from.status, &from.message);
2799        out.code = from.code;
2800        out.fix.clone_from(&from.fix);
2801        out.commands.clone_from(&from.commands);
2802        out.remedy.clone_from(&from.remedy);
2803        out
2804    }
2805
2806    /// The code, fixes and remedy of `from`, whose outcome this result
2807    /// reports with the same status (a feature's run stopped by a task's).
2808    #[must_use]
2809    pub fn caused_by(mut self, from: &Self) -> Self {
2810        if self.status == from.status {
2811            self.code = from.code;
2812            self.fix.clone_from(&from.fix);
2813            self.commands.clone_from(&from.commands);
2814            self.remedy.clone_from(&from.remedy);
2815        }
2816        self
2817    }
2818
2819    /// The same result with another status and message: payload and next
2820    /// steps kept, the code kept only while the status stays the same.
2821    #[must_use]
2822    pub fn reworded(mut self, status: CommandStatus, message: &str) -> Self {
2823        if status != self.status {
2824            self.code = None;
2825            self.fix.clear();
2826            self.commands.clear();
2827            self.remedy = None;
2828        }
2829        self.status = status;
2830        self.exit_code = status.exit_code();
2831        message.clone_into(&mut self.message);
2832        self
2833    }
2834
2835    /// Names the code (a catalog name); its fixes become the result's
2836    /// unless [`Self::with_fix`] gives better ones. An unknown name is
2837    /// ignored, and the status's default code applies later.
2838    #[must_use]
2839    pub fn with_code(mut self, name: &str) -> Self {
2840        if let Some(entry) = crate::codes::lookup(name) {
2841            self.code = Some(entry);
2842        }
2843        self
2844    }
2845
2846    /// The fix lines, replacing the catalog's.
2847    #[must_use]
2848    pub fn with_fix(mut self, fix: Vec<String>) -> Self {
2849        self.fix = fix;
2850        self
2851    }
2852
2853    /// The person's own commands among the fix lines, as the argvs the
2854    /// source built (each line [`crate::step::render`] of its argvs): run
2855    /// as these argvs, never re-read from the line.
2856    #[must_use]
2857    pub fn with_commands(mut self, commands: Vec<crate::step::Argvs>) -> Self {
2858        self.commands = commands;
2859        self
2860    }
2861
2862    /// A fix that is the person's own command, built as argvs: its line
2863    /// joins the fixes, the argvs are what runs.
2864    #[must_use]
2865    pub fn with_command(mut self, argvs: crate::step::Argvs) -> Self {
2866        if self.fix.is_empty() {
2867            self.fix = self.fix();
2868        }
2869        self.fix.push(crate::step::render(&argvs));
2870        self.commands.push(argvs);
2871        self
2872    }
2873
2874    /// Attaches the fix speccoder can apply itself.
2875    #[must_use]
2876    pub fn with_remedy(mut self, remedy: Option<Remedy>) -> Self {
2877        self.remedy = remedy.map(Box::new);
2878        self
2879    }
2880
2881    /// The fix speccoder can apply itself, if it knows one.
2882    #[must_use]
2883    pub fn remedy(&self) -> Option<&Remedy> {
2884        self.remedy.as_deref()
2885    }
2886
2887    /// The code: the one attached, else the status's default (none for a
2888    /// plain `ok`) — no non-ok result is without one.
2889    #[must_use]
2890    pub fn code(&self) -> Option<&'static crate::codes::Entry> {
2891        self.code.or_else(|| crate::codes::default_for(self.status))
2892    }
2893
2894    /// Whether a code was named explicitly (not the status's default).
2895    #[must_use]
2896    pub fn has_explicit_code(&self) -> bool {
2897        self.code.is_some()
2898    }
2899
2900    /// How to resolve it: the fixes given, else the code's own.
2901    #[must_use]
2902    pub fn fix(&self) -> Vec<String> {
2903        match self.code() {
2904            Some(_) if !self.fix.is_empty() => self.fix.clone(),
2905            Some(entry) => crate::codes::fixes(entry, &self.command, None, None),
2906            None => Vec::new(),
2907        }
2908    }
2909
2910    /// Attaches what to do next (`data.next`, the human `Next:` block).
2911    #[must_use]
2912    pub fn with_next(mut self, next: Vec<crate::next::NextStep>) -> Self {
2913        self.next = next;
2914        self
2915    }
2916
2917    /// What to do next.
2918    #[must_use]
2919    pub fn next(&self) -> &[crate::next::NextStep] {
2920        &self.next
2921    }
2922
2923    /// Attaches the command's payload.
2924    #[must_use]
2925    pub fn with_data(mut self, data: CommandData) -> Self {
2926        self.data = Some(data);
2927        self
2928    }
2929
2930    /// The command's payload, if any.
2931    #[must_use]
2932    pub fn data(&self) -> Option<&CommandData> {
2933        self.data.as_ref()
2934    }
2935
2936    /// A successful result.
2937    #[must_use]
2938    pub fn ok(command: &str, message: &str) -> Self {
2939        Self::new(command, CommandStatus::Ok, message)
2940    }
2941
2942    /// The subcommand name, e.g. `inspect`.
2943    #[must_use]
2944    pub fn command(&self) -> &str {
2945        &self.command
2946    }
2947
2948    /// The outcome class.
2949    #[must_use]
2950    pub fn status(&self) -> CommandStatus {
2951        self.status
2952    }
2953
2954    /// The single-line human-readable message.
2955    #[must_use]
2956    pub fn message(&self) -> &str {
2957        &self.message
2958    }
2959
2960    /// The process exit status.
2961    #[must_use]
2962    pub fn exit_code(&self) -> ExitCode {
2963        self.exit_code
2964    }
2965}
2966
2967#[cfg(test)]
2968mod tests {
2969    use super::*;
2970
2971    #[test]
2972    fn payload_names_are_unique() {
2973        let mut seen = std::collections::BTreeSet::new();
2974        for p in PAYLOADS {
2975            assert!(seen.insert(*p), "payload {p} is listed twice");
2976        }
2977        let branch = CommandData::BranchVerify(Box::new(crate::verify_branch::BranchReport {
2978            head: String::new(),
2979            base: None,
2980            mode: crate::verify_branch::Mode::All,
2981            features: Vec::new(),
2982            ticks: Vec::new(),
2983            skipped: Vec::new(),
2984            totals: crate::verify_branch::BranchTotals::default(),
2985            conclusion: crate::verify_branch::Conclusion::Neutral,
2986            trust: serde_json::Value::Null,
2987            accepted_unsigned: false,
2988            require_verified: false,
2989            trace: Vec::new(),
2990            share: None,
2991            open_no_claim: Vec::new(),
2992            not_evaluated: Vec::new(),
2993            newer_fields: Vec::new(),
2994        }));
2995        assert!(PAYLOADS.contains(&branch.kind()));
2996        assert_ne!(branch.kind(), "verify");
2997        assert_ne!(branch.kind(), "attest");
2998    }
2999
3000    #[test]
3001    fn every_status_maps_to_its_documented_exit_code() {
3002        assert_eq!(CommandStatus::Ok.exit_code().code(), 0);
3003        assert_eq!(CommandStatus::Error.exit_code().code(), 1);
3004        assert_eq!(CommandStatus::UsageError.exit_code().code(), 2);
3005        assert_eq!(CommandStatus::NotAProject.exit_code().code(), 10);
3006        assert_eq!(CommandStatus::DiagnosticsError.exit_code().code(), 11);
3007        assert_eq!(CommandStatus::SelectionNeeded.exit_code().code(), 12);
3008        assert_eq!(CommandStatus::ConfigError.exit_code().code(), 13);
3009        assert_eq!(CommandStatus::GitPrecondition.exit_code().code(), 14);
3010        assert_eq!(CommandStatus::VerificationFailed.exit_code().code(), 20);
3011        assert_eq!(CommandStatus::NotFound.exit_code().code(), 15);
3012        assert_eq!(CommandStatus::RunFailed.exit_code().code(), 21);
3013        assert_eq!(CommandStatus::ReplayDiverged.exit_code().code(), 22);
3014        assert_eq!(CommandStatus::TaskNotReady.exit_code().code(), 16);
3015        assert_eq!(CommandStatus::NeedsHuman.exit_code().code(), 23);
3016        assert_eq!(CommandStatus::EvalFailed.exit_code().code(), 24);
3017        assert_eq!(CommandStatus::NotReplayable.exit_code().code(), 25);
3018        assert_eq!(CommandStatus::AttestationFailed.exit_code().code(), 26);
3019    }
3020
3021    #[test]
3022    fn exit_code_follows_status() {
3023        let r = CommandResult::new("verify", CommandStatus::VerificationFailed, "x");
3024        assert_eq!(r.exit_code(), r.status().exit_code());
3025        assert_eq!(CommandResult::ok("x", "").exit_code(), ExitCode::OK);
3026    }
3027
3028    #[test]
3029    fn serializes_to_schema_version_2() {
3030        let r = CommandResult::new("replay", CommandStatus::NotReplayable, "not replayable")
3031            .with_code("NOT-REPLAYABLE")
3032            .with_fix(vec!["x".to_owned()]);
3033        let json = serde_json::to_value(&r).unwrap();
3034        assert_eq!(
3035            json,
3036            serde_json::json!({
3037                "schema_version": 2,
3038                "command": "replay",
3039                "status": "not_replayable",
3040                "exit_code": 25,
3041                "message": "not replayable",
3042                "code": "NOT-REPLAYABLE",
3043                "fix": ["x"],
3044                "fix_steps": [{ "command": "x", "how": "words", "why": null }],
3045                "docs": "https://hupe1980.github.io/speccoder/docs/codes/#not-replayable"
3046            })
3047        );
3048        let with_next = CommandResult::new("run", CommandStatus::NeedsHuman, "x")
3049            .with_next(crate::next::next(&crate::next::Outcome::Decided, None));
3050        let json = serde_json::to_value(&with_next).unwrap();
3051        assert_eq!(json["next"][0]["command"], "speccoder run");
3052        // How an interface takes it, beside it.
3053        assert_eq!(json["next"][0]["how"], "perform");
3054        assert_eq!(json["next"][0]["run"]["kind"], "run");
3055        assert!(json.get("data").is_none());
3056    }
3057}