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    /// `review`: the feature's requirements and the person's review of each.
195    Review(Box<ReviewReport>),
196    /// `auth`: who speccoder is on a GitHub host.
197    Auth(Box<AuthReport>),
198}
199
200/// Result of `speccoder auth`: who speccoder acts as on a GitHub
201/// host, and where its token comes from. The token itself is never shown.
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
203pub struct AuthReport {
204    /// `github.com` or an Enterprise host.
205    pub host: String,
206    /// The account, when signed in.
207    pub login: Option<String>,
208    /// `GH_TOKEN`, `GITHUB_TOKEN`, `the keychain`, when signed in.
209    pub source: Option<String>,
210}
211
212/// Result of `speccoder review`: every requirement of the feature's
213/// spec as a card, with the person's decision on its exact wording.
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
215pub struct ReviewReport {
216    /// The feature.
217    pub feature: String,
218    /// Where the review is recorded (project-relative); commit it to share.
219    pub file: String,
220    /// One per requirement, in the spec's order.
221    pub requirements: Vec<ReviewItem>,
222    /// Counts by state.
223    pub totals: ReviewTotals,
224    /// The format's skill that revises the spec from the comments
225    /// (Spec Kit: `clarify`), when it has one.
226    pub revise_with: Option<String>,
227    /// What that skill is asked, while changes are requested: every
228    /// request with its requirement.
229    pub revision: Option<String>,
230    /// The questions the documents leave open (`[NEEDS CLARIFICATION: …]`),
231    /// in reading order.
232    pub questions: Vec<OpenQuestion>,
233    /// Requirements changed in the checkout since the last commit of the
234    /// spec: what a commit would record.
235    pub changes: Vec<RequirementDrift>,
236    /// The spec's versions in git, newest first (the last twelve).
237    pub history: Vec<SpecVersion>,
238}
239
240/// A question a document leaves open, to answer in place.
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
242pub struct OpenQuestion {
243    /// `Q1`, `Q2`, … in reading order.
244    pub id: String,
245    /// The question.
246    pub text: String,
247    /// The document (project-relative).
248    pub file: String,
249    /// Its line (1-based).
250    pub line: usize,
251    /// The requirement or section it is in.
252    pub near: Option<String>,
253}
254
255/// One commit of the spec and what it changed in its requirements.
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
257pub struct SpecVersion {
258    /// The commit.
259    pub commit: String,
260    /// `YYYY-MM-DD`.
261    pub date: String,
262    /// Its author.
263    pub author: String,
264    /// Its subject line.
265    pub subject: String,
266    /// Requirements it added.
267    pub added: Vec<String>,
268    /// Requirements it reworded.
269    pub reworded: Vec<String>,
270    /// Requirements it removed.
271    pub removed: Vec<String>,
272}
273
274/// One requirement and its review.
275#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
276pub struct ReviewItem {
277    /// `FR-001`.
278    pub id: String,
279    /// Its wording now (the line, markup aside).
280    pub text: String,
281    /// `open` (not reviewed), `approved`, `changes_requested`, or `lapsed`
282    /// (decided on a wording that has changed since).
283    pub state: String,
284    /// Who decided (as for every decision: git's `user.name`, else the OS
285    /// user), when someone did.
286    pub by: Option<String>,
287    /// When (Unix seconds).
288    pub at: Option<u64>,
289    /// The comment, for a change request (or a note on an approval).
290    pub comment: Option<String>,
291    /// For `lapsed`: the wording that was decided on.
292    pub decided_on: Option<String>,
293}
294
295/// Counts of [`ReviewItem::state`].
296#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
297pub struct ReviewTotals {
298    /// Approved on their current wording.
299    pub approved: usize,
300    /// Changes requested.
301    pub changes_requested: usize,
302    /// Never reviewed.
303    pub open: usize,
304    /// Decided on an earlier wording.
305    pub lapsed: usize,
306    /// Questions the documents leave open.
307    #[serde(default)]
308    pub questions: usize,
309}
310
311impl ReviewTotals {
312    /// Every requirement approved on its current wording.
313    #[must_use]
314    pub fn complete(&self) -> bool {
315        self.changes_requested == 0 && self.open == 0 && self.lapsed == 0 && self.questions == 0
316    }
317}
318
319/// Result of `speccoder inbox`: every decision that waits for a
320/// person, across the project's features, the most urgent first — the
321/// person's work once the agents write the code.
322#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
323pub struct InboxReport {
324    /// The items, the most urgent first (see [`InboxItem::kind`]).
325    pub items: Vec<InboxItem>,
326    /// Features looked at.
327    pub features: usize,
328}
329
330/// One decision waiting for a person.
331#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
332pub struct InboxItem {
333    /// What kind, in the order of urgency: `approval` (a run waits on it),
334    /// `ruling` (a verified attempt is held: its tests reject an
335    /// alternative the spec allows), `gaming` (the monitor suspects a
336    /// shortcut), `stale` (proven against an earlier wording), `question`
337    /// (the spec leaves questions open), `review`
338    /// (requirements not approved on their current wording), `revise`
339    /// (changes requested on the spec), `stuck` (the
340    /// last attempt did not pass), `commit` (planning files a run cannot
341    /// read yet), `handoff` (every task done: review and hand it off).
342    pub kind: String,
343    /// The feature.
344    pub feature: String,
345    /// The task, when it is about one.
346    pub task: Option<String>,
347    /// What to decide, in a line.
348    pub title: String,
349    /// What it rests on: the call, the reason, the changed requirement.
350    pub detail: String,
351    /// Waiting since (Unix seconds), when known.
352    pub since: Option<u64>,
353    /// The decisions, the one that moves the work forward first; each runs
354    /// in the item's feature.
355    pub actions: Vec<crate::actions::Action>,
356}
357
358/// Result of `speccoder issue`: the repository's open GitHub issues, read
359/// through GitHub's API as the person.
360#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
361pub struct IssuesReport {
362    /// `owner/name`.
363    pub repo: Option<String>,
364    /// Open issues: assigned to the person first, then the most recently
365    /// updated.
366    pub issues: Vec<IssueEntry>,
367}
368
369/// One open issue.
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
371pub struct IssueEntry {
372    /// `#n`.
373    pub number: u64,
374    /// Its title.
375    pub title: String,
376    /// Its address.
377    pub url: String,
378    /// Label names.
379    pub labels: Vec<String>,
380    /// Who it is assigned to (logins).
381    pub assignees: Vec<String>,
382    /// Assigned to the person speccoder is signed in as.
383    pub mine: bool,
384    /// Last update, ISO 8601.
385    pub updated: Option<String>,
386    /// The feature whose spec names this issue, when one does.
387    pub feature: Option<String>,
388}
389
390/// Result of `speccoder config`: the project's configuration as a catalogue
391/// of every key, each with its type, default and value (when set).
392#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
393pub struct ConfigReport {
394    /// The file, relative to the project root.
395    pub file: String,
396    /// Whether it exists.
397    pub exists: bool,
398    /// Whether it is committed as it is on disk (speccoder reads committed
399    /// configuration only, so an uncommitted change is not yet in force).
400    pub committed: bool,
401    /// Whether every parser accepts it.
402    pub valid: bool,
403    /// The first parser's complaint, when not valid.
404    pub error: Option<String>,
405    /// Sections, in the reference's order.
406    pub sections: Vec<ConfigSection>,
407    /// The uncommitted change, as a diff, when there is one.
408    pub diff: Option<String>,
409}
410
411/// One section of `speccoder.toml`.
412#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
413pub struct ConfigSection {
414    /// The table name, e.g. `model`, or `checks` for `[checks.<name>]`.
415    pub name: String,
416    /// A title for people, e.g. `Model`.
417    pub title: String,
418    /// What the section decides.
419    pub about: String,
420    /// Named tables (`[checks.<name>]`, `[mcp.<name>]`): `keys` describe
421    /// one, `tables` hold the configured ones.
422    pub named: bool,
423    /// The keys (for a named section: the keys of each table, unset).
424    pub keys: Vec<ConfigKey>,
425    /// The configured named tables.
426    pub tables: Vec<ConfigTable>,
427}
428
429/// One configured named table, e.g. `[checks.test]`.
430#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
431pub struct ConfigTable {
432    /// Its name, e.g. `test`.
433    pub name: String,
434    /// Its keys, with values.
435    pub keys: Vec<ConfigKey>,
436}
437
438/// One key: what it is, what it is set to, and what it is otherwise.
439#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
440pub struct ConfigKey {
441    /// The key, e.g. `max_turns`; dotted below a sub-table (`sigstore.identity`).
442    pub key: String,
443    /// `bool`, `integer`, `number`, `string`, `choice`, `duration`, `argv`,
444    /// `argv_list` or `string_list`.
445    pub kind: String,
446    /// What it does.
447    pub about: String,
448    /// The value speccoder uses when the key is absent; none for keys
449    /// without a default (unset means off, or the key is required).
450    pub default: Option<serde_json::Value>,
451    /// The value in the file, when set.
452    pub value: Option<serde_json::Value>,
453    /// For `choice`: the allowed values.
454    pub choices: Vec<String>,
455    /// For numbers: the smallest allowed.
456    pub min: Option<f64>,
457    /// For numbers: the largest allowed.
458    pub max: Option<f64>,
459    /// The section (or table) is refused without it.
460    pub required: bool,
461}
462
463/// A change to `speccoder.toml`: set a key, remove it, or append to a list.
464#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
465pub struct ConfigChange {
466    /// The dotted key, e.g. `budget.max_turns` or `checks.test.argv`.
467    pub key: String,
468    /// The new value; absent (with no `append`) removes the key.
469    #[serde(default)]
470    pub value: Option<serde_json::Value>,
471    /// An item to add to a list key (`commands.allow`), unless present.
472    #[serde(default)]
473    pub append: Option<serde_json::Value>,
474}
475
476/// Result of `speccoder ui`, printed once the host listens (under `--json`
477/// the only result: the host keeps serving after it).
478#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
479pub struct UiReport {
480    /// The address to open, with this start's key in its fragment.
481    pub url: String,
482    /// The port on 127.0.0.1.
483    pub port: u16,
484    /// The project served.
485    pub root: String,
486}
487
488/// One feature (or change) of the project, as `speccoder feature` lists it.
489#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
490pub struct FeatureEntry {
491    /// 1-based position, the number `speccoder feature <n>` takes.
492    pub number: usize,
493    /// Directory name, e.g. `001-greet`.
494    pub name: String,
495    /// The title its spec (or proposal) gives, when it has one.
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub title: Option<String>,
498    /// Repository-relative directory.
499    pub dir: String,
500    /// The current one.
501    pub current: bool,
502    /// Ticked tasks.
503    pub done: usize,
504    /// All tasks (0: no task list yet).
505    pub tasks: usize,
506    /// Which planning artifacts exist: `spec`, `plan`, `tasks`.
507    pub artifacts: Vec<String>,
508}
509
510/// Result of `speccoder feature`.
511#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
512pub struct FeaturesReport {
513    /// `feature` or `change`.
514    pub unit: String,
515    /// The current one's name.
516    pub current: Option<String>,
517    /// How it is current: `argument`, `environment`, `recorded`,
518    /// `only_feature` or `none`.
519    pub reason: String,
520    /// The same in words (`recorded in .specify/feature.json`).
521    pub reason_text: String,
522    /// What this call did: `listed`, `switched` or `cleared`.
523    pub action: String,
524    /// Every feature, in order.
525    pub features: Vec<FeatureEntry>,
526}
527
528/// How a doctor check came out.
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
530#[serde(rename_all = "snake_case")]
531pub enum DoctorLevel {
532    /// Fine.
533    Pass,
534    /// Works fully; something optional is not set up (no signing key).
535    Info,
536    /// Works, with less than it could (no spec tool).
537    Warn,
538    /// A run would fail or judge wrongly.
539    Fail,
540}
541
542/// One doctor check.
543#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
544pub struct DoctorCheck {
545    /// `git`, `repository`, `project`, `config`, `key`, `sandbox`, …
546    pub check: String,
547    /// Pass, warn or fail.
548    pub level: DoctorLevel,
549    /// What was found.
550    pub detail: String,
551    /// The catalog code of a failure or warning.
552    pub code: Option<String>,
553    /// How to resolve it.
554    pub fix: Vec<String>,
555}
556
557/// Result of `speccoder doctor`.
558#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
559pub struct DoctorReport {
560    /// speccoder's version.
561    pub speccoder: String,
562    /// `macos aarch64`.
563    pub os: String,
564    /// Every check, in order.
565    pub checks: Vec<DoctorCheck>,
566}
567
568impl CommandData {
569    /// The payload's schema name (`run`, `feature`, `in-place`, …): the
570    /// envelope's `payload`.
571    #[must_use]
572    pub const fn kind(&self) -> &'static str {
573        match self {
574            Self::Inspect(_) => "inspect",
575            Self::Verify(_) => "verify",
576            Self::Worktree(_) => "worktree",
577            Self::Propose(_) => "propose",
578            Self::Replay(_) => "replay",
579            Self::Run(_) => "run",
580            Self::Eval(_) => "eval",
581            Self::Feature(_) => "feature",
582            Self::Converge(_) => "converge",
583            Self::Report(_) => "report",
584            Self::Approvals(_) => "approvals",
585            Self::Control(_) => "control",
586            Self::Cost(_) => "cost",
587            Self::Pr(_) => "pr",
588            Self::Bench(_) => "bench",
589            Self::Init(_) => "init",
590            Self::Skills(_) => "skills",
591            Self::Skill(_) => "skill",
592            Self::Checklists(_) => "checklists",
593            Self::Archive(_) => "archive",
594            Self::InPlace(_) => "in-place",
595            Self::Attest(_) => "attest",
596            Self::Explain(_) => "explain",
597            Self::Schema(_) => "schema",
598            Self::Doctor(_) => "doctor",
599            Self::Features(_) => "features",
600            Self::Ui(_) => "ui",
601            Self::Config(_) => "config",
602            Self::Issues(_) => "issues",
603            Self::Inbox(_) => "inbox",
604            Self::Review(_) => "review",
605            Self::Auth(_) => "auth",
606        }
607    }
608}
609
610/// Every payload's schema name, in [`CommandData`]'s order.
611pub const PAYLOADS: &[&str] = &[
612    "inspect",
613    "verify",
614    "worktree",
615    "propose",
616    "replay",
617    "run",
618    "eval",
619    "feature",
620    "converge",
621    "report",
622    "approvals",
623    "control",
624    "cost",
625    "pr",
626    "bench",
627    "init",
628    "skills",
629    "skill",
630    "checklists",
631    "archive",
632    "in-place",
633    "attest",
634    "explain",
635    "schema",
636    "doctor",
637    "features",
638    "ui",
639    "config",
640    "issues",
641    "inbox",
642    "review",
643    "auth",
644];
645
646/// A repository instructions file given to an attempt.
647#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
648pub struct InstructionFile {
649    /// Repository-relative path.
650    pub path: String,
651    /// sha256 of the file.
652    pub sha256: String,
653    /// Its size.
654    pub bytes: usize,
655    /// Cut to fit the instructions budget.
656    pub cut: bool,
657}
658
659/// One code, as `explain` shows it.
660#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
661pub struct CodeInfo {
662    /// `NEEDS-APPROVAL`.
663    pub code: String,
664    /// Error, stop or warning.
665    pub kind: crate::codes::Kind,
666    /// The status results with this code have.
667    pub status: CommandStatus,
668    /// Its exit code.
669    pub exit_code: u8,
670    /// One line.
671    pub title: String,
672    /// What it means and why it happens.
673    pub explain: String,
674    /// How to resolve it (templates: `<task>`, `<feature>` are blanks).
675    pub fix: Vec<String>,
676    /// Its section on the codes page.
677    pub docs: String,
678}
679
680impl CodeInfo {
681    /// The view of a catalog entry.
682    #[must_use]
683    pub fn of(e: &crate::codes::Entry) -> Self {
684        Self {
685            code: e.name.to_owned(),
686            kind: e.kind,
687            status: e.status,
688            exit_code: e.status.exit_code().code(),
689            title: e.title.to_owned(),
690            explain: e.explain.to_owned(),
691            fix: crate::codes::fixes(e, "<command>", None, None),
692            docs: e.docs(),
693        }
694    }
695}
696
697/// Result of `speccoder explain`.
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
699pub struct ExplainReport {
700    /// The code asked for, or every code with `--list`.
701    pub codes: Vec<CodeInfo>,
702}
703
704/// One published schema.
705#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
706pub struct SchemaName {
707    /// `result`, `event`, `run`, …
708    pub name: String,
709    /// What it describes.
710    pub describes: String,
711    /// Where it is published.
712    pub url: String,
713}
714
715/// Result of `speccoder schema`.
716#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
717pub struct SchemaReport {
718    /// Every schema speccoder publishes.
719    pub names: Vec<SchemaName>,
720    /// The schema asked for.
721    pub schema: Option<serde_json::Value>,
722}
723
724/// Why an in-place run refused or stopped.
725#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
726pub struct InPlaceReport {
727    /// The paths involved, with why (the code and the fixes are the
728    /// envelope's).
729    pub paths: Vec<RefusedPath>,
730}
731
732/// Result of `speccoder spec archive` (an OpenSpec change).
733#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
734pub struct ArchiveReport {
735    /// The change.
736    pub change: String,
737    /// `probe`, `incomplete`, `unverified`, `awaiting_confirmation`,
738    /// `declined`, `archived` or `failed`.
739    pub stage: String,
740    /// OpenSpec's own `{severity, code, message, fix}` from its last answer.
741    pub openspec_status: Option<serde_json::Value>,
742    /// Tasks ticked without a speccoder verdict commit.
743    pub unverified_tasks: Vec<String>,
744    /// What archiving changes (`openspec show <id> --diff`), when shown.
745    pub diff: Option<String>,
746    /// Where OpenSpec moved the change.
747    pub archived_as: Option<String>,
748    /// OpenSpec's counts of added, modified, removed and renamed requirements.
749    pub totals: Option<serde_json::Value>,
750    /// The commit `archive <id>`.
751    pub commit: Option<String>,
752    /// Who confirmed.
753    pub confirmed_by: Option<String>,
754}
755
756/// One Spec Kit checklist (`specs/<feature>/checklists/*.md`).
757#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
758pub struct ChecklistStatus {
759    /// Path relative to the project root.
760    pub file: String,
761    /// Checklist items.
762    pub total: usize,
763    /// Items checked.
764    pub done: usize,
765    /// Items not checked.
766    pub open: usize,
767}
768
769/// Result of `speccoder eval`.
770#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
771pub struct EvalReport {
772    /// The model the suite ran with, `provider/model`.
773    pub model: String,
774    /// One entry per case, in name order.
775    pub cases: Vec<EvalCase>,
776    /// Counts per classification.
777    pub summary: EvalSummary,
778}
779
780/// One judged case.
781#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
782pub struct EvalCase {
783    /// Directory name.
784    pub name: String,
785    /// `complete` or `stop`.
786    pub expect: String,
787    /// `correct`, `false_completion`, `not_completed`, `honest_stop`, `cheat`, `invalid_case` or `error`.
788    pub classification: String,
789    /// The run's status word and message.
790    pub outcome: String,
791    /// The attempt's run id, when one ran.
792    pub run_id: Option<String>,
793    /// The attempt status.
794    pub attempt_status: Option<String>,
795    /// The verifier's verdict.
796    pub verdict: Option<crate::verify::Verdict>,
797    /// Whether the held-out oracle passed (when it ran).
798    pub oracle_passed: Option<bool>,
799    /// Token usage of the attempt.
800    pub usage: Option<serde_json::Value>,
801    /// The attempt's tool calls with the policy's decisions.
802    pub tool_calls: Vec<serde_json::Value>,
803    /// The model's own closing report, or its reason for stopping.
804    pub model_said: Option<String>,
805    /// Attempts made at the task, retries included.
806    pub attempts: usize,
807    /// The round of a repeated suite run (1-based).
808    pub round: usize,
809    /// Whether the gaming monitor flagged the attempt, when it ran.
810    pub monitor_flagged: Option<bool>,
811    /// Whether the recorded attempt replays strictly from its journal
812    ///; `None` when there is no journaled run (external executors).
813    pub replays: Option<bool>,
814    /// The held-out tests' part (`acceptance.held_out` of the run report).
815    pub held_out: Option<serde_json::Value>,
816    /// An attempt passed the visible checks and failed the held-out tests.
817    pub gap: bool,
818    /// The combined acceptance strength, when measured.
819    #[serde(default, skip_serializing_if = "Option::is_none")]
820    pub strength: Option<f64>,
821    /// Stopped because a test rejected a permitted alternative.
822    #[serde(default)]
823    pub over_strict: bool,
824}
825
826/// Suite counts and metrics.
827#[derive(Debug, Clone, Default, PartialEq, Serialize, schemars::JsonSchema)]
828pub struct EvalSummary {
829    /// Cases run.
830    pub cases: usize,
831    /// Completed and right.
832    pub correct: usize,
833    /// Completed with a passing verdict but wrong.
834    pub false_completions: usize,
835    /// Possible cases that stopped or failed verification.
836    pub not_completed: usize,
837    /// Impossible cases that stopped for a person.
838    pub honest_stops: usize,
839    /// Impossible cases that finished with green checks.
840    pub cheats: usize,
841    /// Cases that could not be judged.
842    pub invalid_or_error: usize,
843    /// correct / (correct + false completions), when defined.
844    pub precision: Option<f64>,
845    /// Input + output tokens per correct case, when any.
846    pub tokens_per_correct: Option<u64>,
847    /// How well the gaming monitor did, when it ran on any case.
848    pub monitor: Option<MonitorScore>,
849    /// Recorded attempts checked by strict replay, and how many replayed.
850    pub replay_checked: usize,
851    /// Of those, the ones that replayed exactly.
852    pub replayed: usize,
853    /// `expect = "reject"` cases that were not ticked.
854    pub caught: usize,
855    /// Attempts that passed visibly, and how many of them the held-out tests
856    /// failed; `None` when no held-out test ran.
857    pub held_out_gap: Option<HeldOutGap>,
858    /// Ticked cases whose held-out tests passed, against the oracle.
859    pub held_out_oracle: Option<HeldOutOracle>,
860    /// The median combined acceptance strength over verified cases that
861    /// measured it; `None`: not measured.
862    pub median_acceptance_strength: Option<f64>,
863    /// Cases stopped for a person because a test rejected a permitted
864    /// alternative.
865    pub over_strict_stops: usize,
866}
867
868/// Visible passes the held-out tests caught.
869#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
870pub struct HeldOutGap {
871    /// Attempts the held-out tests judged (they had passed visibly).
872    pub visible_passed: usize,
873    /// Of those, the ones the held-out tests failed.
874    pub held_out_failed: usize,
875    /// `held_out_failed / visible_passed`.
876    pub rate: f64,
877}
878
879/// Held-out passes the hidden oracle agreed with, or not.
880#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
881pub struct HeldOutOracle {
882    /// The oracle passed too.
883    pub agree: usize,
884    /// The oracle failed: the held-out tests missed something.
885    pub disagree: usize,
886}
887
888/// The gaming monitor against the oracle's ground truth: a positive is a
889/// cheat or a false completion (both passed the visible checks).
890#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
891pub struct MonitorScore {
892    /// Cases the monitor judged.
893    pub judged: usize,
894    /// Flagged and really a cheat or false completion.
895    pub true_positives: usize,
896    /// Flagged but correct.
897    pub false_positives: usize,
898    /// Not flagged but a cheat or false completion.
899    pub false_negatives: usize,
900    /// true / (true + false positives), when anything was flagged.
901    pub precision: Option<f64>,
902    /// true positives / all positives, when there were any.
903    pub recall: Option<f64>,
904}
905
906/// Result of `speccoder run`.
907#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
908pub struct RunReport {
909    /// The agentplane run of the attempt.
910    pub run_id: String,
911    /// Feature directory name.
912    pub feature: String,
913    /// Task ID.
914    pub task: String,
915    /// The worktree the attempt worked in.
916    pub worktree: String,
917    /// `completed`, `blocked`, `needs_approval`, `budget_exhausted` or `failed`.
918    pub attempt_status: String,
919    /// The model's own closing report — its claim, not the verdict.
920    pub model_report: Option<serde_json::Value>,
921    /// Why the attempt stopped, when it did not complete.
922    pub reason: Option<String>,
923    /// The call awaiting approval, if any.
924    pub pending: Option<serde_json::Value>,
925    /// Every tool call with the policy's decision.
926    pub tool_calls: Vec<serde_json::Value>,
927    /// Paths changed between the checkpoints: `added` / `modified` / `deleted`.
928    pub changes: Vec<(String, String)>,
929    /// Checks after the attempt, compared with the baseline.
930    pub verification: Option<crate::verify::VerifyReport>,
931    /// The verifier's verdict (only for completed attempts).
932    pub verdict: Option<crate::verify::Verdict>,
933    /// Token usage summed over the attempt's turns.
934    pub usage: Option<serde_json::Value>,
935    /// Integrity findings of the attempt's diff (each also fails the verdict).
936    pub integrity: Vec<String>,
937    /// The engine ticked the task in the worktree's `tasks.md`.
938    pub ticked: bool,
939    /// The commit on `speccoder/<feature>` holding the attempt and the tick.
940    pub commit: Option<String>,
941    /// Spec-derived acceptance tests: files, command, red/green, notes.
942    pub acceptance: Option<serde_json::Value>,
943    /// Earlier attempts at this task in this run, discarded and retried.
944    pub earlier: Vec<EarlierAttempt>,
945    /// Why no further attempt was made after a failed one, if it failed.
946    pub retry_stop: Option<String>,
947    /// The ref this attempt was saved under when it did not pass; the
948    /// worktree went back to where it started.
949    pub saved_attempt: Option<String>,
950    /// The ref uncommitted leftovers were saved under before this run.
951    pub set_aside: Option<String>,
952    /// Things a person should know that are not part of the verdict.
953    pub notices: Vec<String>,
954    /// The gaming monitor's view of a passing attempt: `suspected`,
955    /// `findings`, `usage` (or `error`). Never shown to the implementer.
956    pub monitor: Option<serde_json::Value>,
957    /// Mutation testing of the attempt's changes: `tool`, `caught`,
958    /// `missed`, `timeout`, `unviable`, `survivors` (or `error`).
959    pub mutation: Option<serde_json::Value>,
960    /// Best-of-n: every sample's evidence and which one was kept.
961    pub samples: Vec<serde_json::Value>,
962    /// Changes outside the paths the task names: reported, never gated.
963    #[serde(skip_serializing_if = "Option::is_none")]
964    pub scope: Option<crate::scope::ScopeFinding>,
965    /// What the tick's attestation came to: `{ state: signed | unsigned |
966    /// missing, signer, reason }`.
967    #[serde(default, skip_serializing_if = "Option::is_none")]
968    pub attestation: Option<serde_json::Value>,
969    /// The repository's instructions the attempt was given (`AGENTS.md`,
970    /// `CLAUDE.md`, …), root first.
971    #[serde(default, skip_serializing_if = "Vec::is_empty")]
972    pub instructions: Vec<InstructionFile>,
973    /// The acceptance set's strength: after a `verified` verdict.
974    #[serde(default, skip_serializing_if = "Option::is_none")]
975    pub strength: Option<crate::strength::AcceptanceStrength>,
976}
977
978/// A failed attempt that was undone and retried.
979#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
980pub struct EarlierAttempt {
981    /// The agentplane run of the attempt.
982    pub run_id: String,
983    /// The attempt's status.
984    pub attempt_status: String,
985    /// The verifier's verdict.
986    pub verdict: Option<crate::verify::Verdict>,
987    /// The failure's fingerprint.
988    pub fingerprint: String,
989    /// What the next attempt was told.
990    pub digest: crate::retry::Digest,
991    /// Token usage of the attempt.
992    pub usage: Option<serde_json::Value>,
993}
994
995/// Result of `speccoder run` without `--task`: a feature worked through.
996#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
997pub struct FeatureReport {
998    /// Feature directory name.
999    pub feature: String,
1000    /// Every attempt, in order.
1001    pub attempts: Vec<RunReport>,
1002    /// Why the run stopped.
1003    pub stop: String,
1004    /// Convergence rounds between task runs (`run --converge`), in order.
1005    pub converge: Vec<ConvergeReport>,
1006    /// Extension hooks run, offered or skipped around it.
1007    pub hooks: Vec<String>,
1008    /// What parallel groups did: merged, conflicts, re-verification.
1009    pub parallel: Vec<String>,
1010    /// The `full` check tier, run once after the run ticked the last task.
1011    #[serde(skip_serializing_if = "Option::is_none")]
1012    pub full_tier: Option<FullTier>,
1013}
1014
1015/// The `full` check tier at the end of a feature.
1016#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1017pub struct FullTier {
1018    /// The checks that ran.
1019    pub ran: Vec<String>,
1020    /// Every check passed (or none applied).
1021    pub passed: bool,
1022    /// The checks that failed.
1023    pub failed: Vec<String>,
1024    /// Why nothing ran, or the record could not be written.
1025    pub note: Option<String>,
1026}
1027
1028/// Result of `speccoder spec` without a name.
1029#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1030pub struct SkillsReport {
1031    /// Installed skills by name.
1032    pub skills: Vec<SkillEntry>,
1033}
1034
1035/// One installed Agent Skill.
1036#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1037pub struct SkillEntry {
1038    /// Its name.
1039    pub name: String,
1040    /// What it does.
1041    pub description: String,
1042    /// The `SKILL.md`.
1043    pub path: String,
1044    /// Why speccoder does not host it, if it does not.
1045    pub refused: Option<String>,
1046}
1047
1048/// Result of `speccoder spec <name>`.
1049#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1050pub struct SkillRunReport {
1051    /// The skill.
1052    pub skill: String,
1053    /// The agentplane run.
1054    pub run_id: String,
1055    /// `completed`, `incomplete`, `needs_approval`, `blocked`, `denied`, `budget_exhausted` or `failed`.
1056    pub status: String,
1057    /// The model's summary.
1058    pub summary: Option<String>,
1059    /// Questions it could not get answered.
1060    pub questions: Vec<String>,
1061    /// Paths changed in the checkout: `added` / `modified` / `deleted`.
1062    pub changes: Vec<(String, String)>,
1063    /// Every tool call with the policy's decision.
1064    pub tool_calls: Vec<serde_json::Value>,
1065    /// The call awaiting approval, if any.
1066    pub pending: Option<serde_json::Value>,
1067    /// Why it stopped, when it did not complete.
1068    pub reason: Option<String>,
1069    /// Token usage.
1070    pub usage: Option<serde_json::Value>,
1071    /// Extension hooks run, offered or skipped around it.
1072    pub hooks: Vec<String>,
1073    /// The unit the skill worked on, or created and made current, and how
1074    /// it is known (`001-x (recorded in .specify/feature.json)`).
1075    #[serde(default, skip_serializing_if = "Option::is_none")]
1076    pub current: Option<String>,
1077}
1078
1079/// Result of `speccoder init`.
1080#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1081pub struct InitReport {
1082    /// The configuration file.
1083    pub path: String,
1084    /// The checks it declares.
1085    pub checks: Vec<InitCheck>,
1086    /// `provider/model`.
1087    pub model: String,
1088    /// Files written, relative to the project root.
1089    pub wrote: Vec<String>,
1090    /// The checks run on the untouched code (empty: not run).
1091    pub ran: Vec<InitRun>,
1092    /// The repository instructions found (`AGENTS.md`, `CLAUDE.md`).
1093    pub instructions: Vec<String>,
1094    /// The model key: the variable and whether it is set.
1095    pub key: InitKey,
1096    /// The commit holding `speccoder.toml`, when one was made.
1097    pub committed: Option<String>,
1098}
1099
1100/// One check `init` ran on the untouched code.
1101#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1102pub struct InitRun {
1103    /// `[checks.<name>]`.
1104    pub name: String,
1105    /// Passed on the untouched code.
1106    pub passed: bool,
1107    /// `passed`, `failed`, `timed_out`, `skipped`, `error`.
1108    pub status: String,
1109    /// How long it took.
1110    pub duration_ms: u64,
1111    /// Why it did not pass: the last line of its output that says something
1112    /// (`No module named pytest`), or the runner's note.
1113    pub reason: Option<String>,
1114}
1115
1116/// The model key `init` looked for.
1117#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, schemars::JsonSchema)]
1118pub struct InitKey {
1119    /// `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`.
1120    pub variable: String,
1121    /// Set in this environment.
1122    pub set: bool,
1123}
1124
1125/// One detected check.
1126#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1127pub struct InitCheck {
1128    /// `[checks.<name>]`.
1129    pub name: String,
1130    /// The command.
1131    pub argv: Vec<String>,
1132    /// `fast` or `full`.
1133    pub tier: String,
1134    /// Why it was proposed.
1135    pub why: String,
1136}
1137
1138/// Result of `accept`, `skip` and `nudge`.
1139#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1140pub struct ControlReport {
1141    /// Feature directory name.
1142    pub feature: String,
1143    /// Task ID.
1144    pub task: String,
1145    /// `accepted`, `skipped`, `unskipped`, `nudged` or `nudges_cleared`.
1146    pub action: String,
1147    /// The person's reason or note.
1148    pub text: Option<String>,
1149    /// The commit or ref that records it, when it lives in git.
1150    pub record: Option<String>,
1151}
1152
1153/// Result of `speccoder report --cost`.
1154#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1155pub struct CostReport {
1156    /// Feature directory name.
1157    pub feature: String,
1158    /// Per task, every attempt counted (passed and saved).
1159    pub tasks: Vec<TaskCost>,
1160    /// Convergence assessments.
1161    pub convergence: Usage,
1162    /// Everything.
1163    pub total: Usage,
1164    /// The model the prices apply to, when prices are configured.
1165    pub priced_model: Option<String>,
1166}
1167
1168/// One task's cost.
1169#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1170pub struct TaskCost {
1171    /// Task ID.
1172    pub task: String,
1173    /// Attempts recorded in git (the passing commit and saved attempts).
1174    pub attempts: usize,
1175    /// Tokens and money.
1176    pub usage: Usage,
1177}
1178
1179/// Token counts, and USD when prices are configured.
1180#[derive(Debug, Clone, Default, PartialEq, Serialize, schemars::JsonSchema)]
1181pub struct Usage {
1182    /// Input tokens (including those read from the cache).
1183    pub input_tokens: u64,
1184    /// Output tokens.
1185    pub output_tokens: u64,
1186    /// Input tokens read from the cache.
1187    pub cache_read_tokens: u64,
1188    /// Tokens written to the cache.
1189    pub cache_write_tokens: u64,
1190    /// USD, when `[model]` has prices.
1191    pub usd: Option<f64>,
1192}
1193
1194/// Result of `speccoder report --approvals`, `approve` and `reject`.
1195#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1196pub struct ApprovalsReport {
1197    /// Feature directory name.
1198    pub feature: String,
1199    /// Calls that stopped an open task and await a decision.
1200    pub pending: Vec<PendingCall>,
1201    /// Decisions recorded for the feature's tasks.
1202    pub decided: Vec<DecidedCall>,
1203}
1204
1205/// A call that stopped an attempt for approval, or a question, waiting for
1206/// a person.
1207#[derive(Debug, Clone, Default, PartialEq, Serialize, schemars::JsonSchema)]
1208pub struct PendingCall {
1209    /// Task ID.
1210    pub task: String,
1211    /// What a decision would cover, e.g. ``write `Cargo.toml` ``.
1212    pub covers: String,
1213    /// The tool.
1214    pub tool: String,
1215    /// Its arguments as the model sent them.
1216    pub arguments: serde_json::Value,
1217    /// Why it needs approval.
1218    pub reason: String,
1219    /// The saved attempt.
1220    pub reference: String,
1221    /// The call as the inline prompt shows it.
1222    pub presentation: Option<ApprovalView>,
1223    /// The waiting call's id (`approve --call`); empty for a stopped attempt.
1224    pub call: String,
1225    /// `task` (the attempt waits and resumes) or `attempt` (stopped and saved
1226    /// in a parallel lane).
1227    pub source: String,
1228    /// `approval` or `question`.
1229    pub kind: String,
1230    /// A question's text.
1231    pub question: Option<String>,
1232    /// Seconds since the epoch the call has waited since.
1233    pub waiting_since: Option<u64>,
1234    /// Seconds since the epoch it is denied as expired at.
1235    pub deadline: Option<u64>,
1236}
1237
1238/// Who decided, and how speccoder knows the name.
1239#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1240pub struct OperatorView {
1241    /// The name.
1242    pub actor: String,
1243    /// `asserted` (typed at a local terminal or the CLI) or `connected`
1244    /// (reported by a client over its connection, e.g. an editor).
1245    pub basis: String,
1246}
1247
1248/// A pending call as a person sees it: what, why, what it can reach, what
1249/// the model claimed, and what approving it grants.
1250#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1251pub struct ApprovalView {
1252    /// `write`, `edit`, `run`, `network` or `mcp`.
1253    pub kind: String,
1254    /// `Edit this file?`, `Run this command?`, ….
1255    pub title: String,
1256    /// The path, the whole command, or the MCP tool.
1257    pub subject: String,
1258    /// The diff, the file's head, the command or the arguments.
1259    pub body_lines: Vec<String>,
1260    /// What the call can reach.
1261    pub sandbox: String,
1262    /// What the model said when it asked (a claim).
1263    pub claim: Option<String>,
1264    /// `low`, `medium` or `high`.
1265    pub risk: String,
1266    /// What `approve` grants: `for the rest of T004: this path, any content`.
1267    pub scope: String,
1268}
1269
1270/// A person's decision.
1271#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1272pub struct DecidedCall {
1273    /// Task ID.
1274    pub task: String,
1275    /// `approved` or `rejected`.
1276    pub decision: String,
1277    /// What it covers.
1278    pub covers: String,
1279    /// The reason, for a rejection.
1280    pub reason: Option<String>,
1281    /// Who decided, as `name (via)`.
1282    pub by: Option<String>,
1283    /// How far an approval reaches: `once`, `task` or `run`.
1284    pub scope: Option<String>,
1285    /// An approval's note to the model.
1286    pub note: Option<String>,
1287    /// The waiting call it decided, when it was one.
1288    pub call: Option<String>,
1289    /// Who decided; `None` for an expiry.
1290    pub operator: Option<OperatorView>,
1291    /// Through what: `terminal`, `cli` or `editor <name>`.
1292    pub via: Option<String>,
1293    /// Nobody decided: the deadline passed and the call was denied.
1294    pub expired: bool,
1295}
1296
1297/// What `attest verify` found for one task object.
1298#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, schemars::JsonSchema)]
1299#[serde(rename_all = "snake_case")]
1300pub enum VerifyOutcome {
1301    /// Every line parses, every signature verifies against the trust input,
1302    /// and every statement agrees with the commit.
1303    Ok,
1304    /// The statements hold but are not signed.
1305    Unsigned,
1306    /// No speccoder attestation on the object.
1307    Missing,
1308    /// A signature does not verify.
1309    BadSignature,
1310    /// A signature verifies, but its key is not trusted by the verifier.
1311    UntrustedSigner,
1312    /// A statement disagrees with the commit, its trailers, the spec or the bundle.
1313    Mismatch,
1314    /// A line does not parse.
1315    Malformed,
1316}
1317
1318impl VerifyOutcome {
1319    /// How grave: when several apply, the gravest is the outcome
1320    /// (`malformed` > `bad_signature` > `mismatch` > `untrusted_signer` >
1321    /// `unsigned` > `ok`; `missing` stands alone).
1322    #[must_use]
1323    pub const fn rank(self) -> u8 {
1324        match self {
1325            Self::Ok => 0,
1326            Self::Unsigned => 1,
1327            Self::UntrustedSigner => 2,
1328            Self::Mismatch => 3,
1329            Self::BadSignature => 4,
1330            Self::Malformed => 5,
1331            Self::Missing => 6,
1332        }
1333    }
1334
1335    /// The word reports use.
1336    #[must_use]
1337    pub const fn word(self) -> &'static str {
1338        match self {
1339            Self::Ok => "ok",
1340            Self::Unsigned => "unsigned",
1341            Self::Missing => "missing",
1342            Self::BadSignature => "bad_signature",
1343            Self::UntrustedSigner => "untrusted_signer",
1344            Self::Mismatch => "mismatch",
1345            Self::Malformed => "malformed",
1346        }
1347    }
1348}
1349
1350/// Why an object got its outcome.
1351#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1352pub struct VerifyReason {
1353    /// A stable code, e.g. `subject`, `trailer`, `line_hash`, `signature`.
1354    pub code: String,
1355    /// The field that disagrees.
1356    #[serde(skip_serializing_if = "Option::is_none")]
1357    pub field: Option<String>,
1358    /// What it should be.
1359    #[serde(skip_serializing_if = "Option::is_none")]
1360    pub expected: Option<String>,
1361    /// What it is.
1362    #[serde(skip_serializing_if = "Option::is_none")]
1363    pub found: Option<String>,
1364}
1365
1366/// Who signed.
1367#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1368pub struct SignerView {
1369    /// `ssh`, `openpgp` or `sigstore`.
1370    pub kind: String,
1371    /// The key's fingerprint (`SHA256:…`, an OpenPGP fingerprint).
1372    pub keyid: String,
1373    /// The principal the verifier's trust input names for it.
1374    #[serde(skip_serializing_if = "Option::is_none")]
1375    pub principal: Option<String>,
1376}
1377
1378/// One task object's verification.
1379#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1380pub struct ObjectVerification {
1381    /// The commit (or skip record) id.
1382    pub object: String,
1383    /// `task` or `skip`.
1384    pub kind: String,
1385    /// Task ID.
1386    pub task: String,
1387    /// The verdict the statements claim.
1388    pub verdict: Option<String>,
1389    /// The outcome.
1390    pub outcome: VerifyOutcome,
1391    /// Every reason found.
1392    pub reasons: Vec<VerifyReason>,
1393    /// Who signed, when signed.
1394    pub signer: Option<SignerView>,
1395    /// How many statements the bundle holds.
1396    pub statements: usize,
1397    /// For a missing attestation: an attested commit with the same tree.
1398    pub same_tree_as: Option<String>,
1399}
1400
1401/// Result of `speccoder attest verify` and `attest sign`.
1402#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1403pub struct AttestReport {
1404    /// Feature directory name.
1405    pub feature: String,
1406    /// Where trust came from: the allowed-signers file, whether GPG's keyring
1407    /// was used, the expected Sigstore identity.
1408    pub trust: serde_json::Value,
1409    /// One per object.
1410    pub objects: Vec<ObjectVerification>,
1411    /// Count per outcome word.
1412    pub totals: std::collections::BTreeMap<String, usize>,
1413}
1414
1415/// One row of the requirement → scenario → test → verdict matrix.
1416#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1417pub struct TraceRow {
1418    /// `FR-001`, `SC-002`.
1419    pub requirement: String,
1420    /// `US1.2`: story 1, acceptance scenario 2.
1421    pub scenario: Option<String>,
1422    /// The acceptance test.
1423    pub test: Option<String>,
1424    /// The task that cites it.
1425    pub task: Option<String>,
1426    /// That task's verdict.
1427    pub verdict: Option<String>,
1428    /// The requirement changed since that task's tick: the verdict is for
1429    /// its earlier wording.
1430    #[serde(default)]
1431    pub changed: bool,
1432}
1433
1434/// What changed since a tick: the tick's commit holds the intent
1435/// it was judged against; the checkout's `HEAD` holds the intent now.
1436#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1437pub struct Drift {
1438    /// The requirements the task cites whose line changed, appeared or went
1439    /// away: the evidence no longer speaks for them. Empty: what changed is
1440    /// elsewhere in the intent.
1441    pub requirements: Vec<RequirementDrift>,
1442    /// The intent files that differ (project-relative), task lists aside.
1443    /// Empty when the intent is not in git (in place, ignored): it changed,
1444    /// which file is not known.
1445    pub files: Vec<String>,
1446}
1447
1448impl Drift {
1449    /// A requirement the task cites changed: the tick does not prove what
1450    /// the spec asks now.
1451    #[must_use]
1452    pub fn stale(&self) -> bool {
1453        !self.requirements.is_empty()
1454    }
1455}
1456
1457/// One cited requirement, as the tick saw it and as the spec has it now.
1458#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1459pub struct RequirementDrift {
1460    /// `FR-003`.
1461    pub id: String,
1462    /// Its line when the task was ticked; `None`: it did not exist yet.
1463    pub then: Option<String>,
1464    /// Its line now; `None`: it was removed.
1465    pub now: Option<String>,
1466}
1467
1468/// Result of `speccoder report`: what the branch's history shows.
1469#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1470pub struct StatusReport {
1471    /// Feature directory name.
1472    pub feature: String,
1473    /// `speccoder/<feature>`, if speccoder has worked on the feature.
1474    pub branch: Option<String>,
1475    /// Where `tasks.md` was read: `branch` or `checkout`.
1476    pub source: String,
1477    /// How the feature was chosen (`recorded in .specify/feature.json`,
1478    /// `the only feature`, `named with --feature`).
1479    #[serde(default)]
1480    pub selected_by: String,
1481    /// The planning artifacts that exist (`spec`, `plan`, `tasks`).
1482    #[serde(default)]
1483    pub artifacts: Vec<String>,
1484    /// While the feature has no task list: the planning skills that come
1485    /// next (`plan`, `tasks`); empty once it has one.
1486    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1487    pub planning: Vec<String>,
1488    /// Planning files under the feature's directory not yet committed
1489    /// (`speccoder run` reads committed files only).
1490    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1491    pub uncommitted: Vec<String>,
1492    /// Where the feature is: spec → plan → tasks → commit → run → review.
1493    #[serde(default)]
1494    pub journey: Vec<crate::actions::JourneyStep>,
1495    /// Commits on the branch that the checkout's HEAD does not contain.
1496    pub unmerged_commits: usize,
1497    /// Every task, in document order.
1498    pub tasks: Vec<TaskStatus>,
1499    /// Convergence rounds, oldest first.
1500    pub convergence: Vec<ConvergenceRound>,
1501    /// Requirement → task → verdict, in the spec's order; a requirement no
1502    /// task cites is a row without a task.
1503    pub trace: Vec<TraceRow>,
1504    /// Counts by evidence.
1505    pub totals: StatusTotals,
1506    /// GitHub issue URLs the feature's spec names: where it came from, and
1507    /// what its pull request closes.
1508    pub issues: Vec<String>,
1509    /// The person's review of the spec's requirements, when the
1510    /// format has requirement identifiers.
1511    #[serde(default, skip_serializing_if = "Option::is_none")]
1512    pub review: Option<ReviewTotals>,
1513}
1514
1515/// One task and its evidence.
1516#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1517pub struct TaskStatus {
1518    /// Task ID.
1519    pub id: String,
1520    /// Title after markers.
1521    pub title: String,
1522    /// Enclosing phase heading.
1523    pub phase: String,
1524    /// `verified`, `regression_only` (ticked by speccoder with that verdict),
1525    /// `attested` (ticked by `speccoder task accept`), `ticked_without_evidence`
1526    /// (ticked, no speccoder commit), `evidence_without_tick` (in place: a
1527    /// verdict commit names the task, its local tick is missing),
1528    /// `skipped`, `ready` or `blocked`.
1529    pub state: String,
1530    /// What changed in the intent (spec, plan, constitution) since this
1531    /// task's evidence was committed: its tick was judged against an earlier
1532    /// version. `None`: nothing changed, or the task has no commit.
1533    #[serde(default, skip_serializing_if = "Option::is_none")]
1534    pub drift: Option<Drift>,
1535    /// For a task speccoder committed: its attestation as the note says,
1536    /// signatures unchecked — `signed <kind> <keyid>`, `unsigned` or
1537    /// `missing` (`attest verify` checks them).
1538    pub attestation: Option<String>,
1539    /// Open dependencies of a blocked task.
1540    pub blocked_by: Vec<String>,
1541    /// The speccoder commit that ticked it.
1542    pub commit: Option<String>,
1543    /// The agentplane run of that attempt.
1544    pub run_id: Option<String>,
1545    /// For an open task: its latest saved attempt.
1546    pub last_attempt: Option<LastAttempt>,
1547    /// The reason a person gave for `accept` or `skip`.
1548    pub note: Option<String>,
1549    /// A verified tick's acceptance strength as its trailer says
1550    /// (`0.83 mutants=9/11 near-miss=1/2`, `not measured (…)`).
1551    #[serde(default, skip_serializing_if = "Option::is_none")]
1552    pub strength: Option<String>,
1553}
1554
1555/// An attempt that did not pass, as saved under `refs/speccoder/attempts/`.
1556#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1557pub struct LastAttempt {
1558    /// `verification_failed`, `blocked`, `needs_approval`, `not_demonstrated`, `interrupted`, …
1559    pub outcome: String,
1560    /// Why.
1561    pub reason: String,
1562    /// The agentplane run, when there was one.
1563    pub run_id: Option<String>,
1564    /// The ref holding the attempt's tree.
1565    pub reference: String,
1566}
1567
1568/// One convergence round, from its commit.
1569#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1570pub struct ConvergenceRound {
1571    /// Round number.
1572    pub round: u32,
1573    /// The commit that appended it.
1574    pub commit: String,
1575    /// The appended tasks, as `T0NN text`.
1576    pub tasks: Vec<String>,
1577}
1578
1579/// Counts of [`TaskStatus::state`].
1580#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1581pub struct StatusTotals {
1582    /// Ticked with task-specific evidence.
1583    pub verified: usize,
1584    /// Ticked with regression evidence only.
1585    pub regression_only: usize,
1586    /// Ticked without a speccoder commit (by a person, or before speccoder).
1587    pub ticked_without_evidence: usize,
1588    /// In place: committed by speccoder, the local tick missing.
1589    pub evidence_without_tick: usize,
1590    /// Ticks speccoder committed without an attestation.
1591    pub without_attestation: usize,
1592    /// Ticked by `speccoder task accept`: a person's attestation, not evidence.
1593    pub attested: usize,
1594    /// Open and skipped by a person.
1595    pub skipped: usize,
1596    /// Open and ready.
1597    pub ready: usize,
1598    /// Open and blocked.
1599    pub blocked: usize,
1600    /// Ticks whose cited requirements changed since.
1601    #[serde(default)]
1602    pub stale: usize,
1603}
1604
1605/// Result of `speccoder bench import`.
1606#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1607pub struct BenchReport {
1608    /// The suite directory written.
1609    pub out: String,
1610    /// Instances turned into cases.
1611    pub imported: Vec<String>,
1612    /// Instances not imported, with why.
1613    pub skipped: Vec<String>,
1614}
1615
1616/// Result of `speccoder pr`: a draft pull request with the evidence.
1617#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1618pub struct PrReport {
1619    /// Feature directory name.
1620    pub feature: String,
1621    /// `speccoder/<feature>`.
1622    pub branch: String,
1623    /// The branch the pull request targets.
1624    pub base: String,
1625    /// The git remote pushed to.
1626    pub remote: String,
1627    /// Pull request title.
1628    pub title: String,
1629    /// Pull request body (Markdown): every task's evidence.
1630    pub body: String,
1631    /// What runs (or would run), in order.
1632    pub commands: Vec<String>,
1633    /// Whether the branch was pushed and the draft opened (`false`: a dry run).
1634    pub opened: bool,
1635    /// The pull request, when opened.
1636    pub url: Option<String>,
1637}
1638
1639/// Result of `speccoder converge`.
1640#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1641pub struct ConvergeReport {
1642    /// Feature directory name.
1643    pub feature: String,
1644    /// The agentplane run of the assessment, if one ran.
1645    pub run_id: Option<String>,
1646    /// This round's number (1 = the first Convergence phase).
1647    pub round: u32,
1648    /// `converged`, `tasks_appended`, `needs_review` (only unrequested
1649    /// findings), `no_progress`, `round_limit` or `failed`.
1650    pub outcome: String,
1651    /// Every finding as the model reported it (its claim, not evidence).
1652    pub findings: Vec<serde_json::Value>,
1653    /// Tasks speccoder appended, as `T0NN text`.
1654    pub appended: Vec<String>,
1655    /// `unrequested` findings: for a person, never turned into tasks.
1656    pub unrequested: Vec<serde_json::Value>,
1657    /// Findings that repeat an earlier convergence task: no progress.
1658    pub repeated: Vec<serde_json::Value>,
1659    /// The commit on `speccoder/<feature>` holding the appended section.
1660    pub commit: Option<String>,
1661    /// Token usage of the assessment.
1662    pub usage: Option<serde_json::Value>,
1663    /// Every tool call with the policy's decision.
1664    pub tool_calls: Vec<serde_json::Value>,
1665    /// Extension hooks run, offered or skipped around it.
1666    pub hooks: Vec<String>,
1667}
1668
1669/// Result of `speccoder propose`.
1670#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1671pub struct ProposeReport {
1672    /// The agentplane run that holds the call.
1673    pub run_id: String,
1674    /// Feature directory name.
1675    pub feature: String,
1676    /// Task ID.
1677    pub task: String,
1678    /// agentplane run status, e.g. `succeeded`.
1679    pub run_status: String,
1680    /// The structured proposal (`summary`, `steps`, `files`, `checks`, `risks`).
1681    pub proposal: Option<serde_json::Value>,
1682    /// Token usage as the provider reported it.
1683    pub usage: Option<serde_json::Value>,
1684}
1685
1686/// Result of `speccoder replay`.
1687#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1688pub struct ReplayReport {
1689    /// The replayed run.
1690    pub run_id: String,
1691    /// `reproduced`, `diverged` or `not_found`.
1692    pub result: String,
1693    /// What the replay said.
1694    pub detail: String,
1695}
1696
1697/// Result of `speccoder worktree`.
1698#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1699pub struct WorktreeReport {
1700    /// The feature the worktree is for.
1701    pub feature: String,
1702    /// Absolute worktree path.
1703    pub path: Option<String>,
1704    /// Spec Kit project directory inside the worktree.
1705    pub project_dir: Option<String>,
1706    /// `speccoder/<feature>`.
1707    pub branch: Option<String>,
1708    /// Commit the branch points at.
1709    pub base: Option<String>,
1710    /// Created now (`false`: reused).
1711    pub created: bool,
1712    /// Offending paths with reasons, when refused.
1713    pub refused_paths: Vec<RefusedPath>,
1714}
1715
1716/// A path that blocks worktree mode.
1717#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1718pub struct RefusedPath {
1719    /// Repository-relative path.
1720    pub path: String,
1721    /// `ignored`, `untracked` or `modified`.
1722    pub reason: String,
1723}
1724
1725/// The result of one command, rendered as human text or as one JSON object.
1726///
1727/// `exit_code` always equals `status.exit_code()`: the constructors set it and
1728/// the fields are private, so the two can never disagree.
1729#[derive(Debug, Clone, PartialEq)]
1730pub struct CommandResult {
1731    schema_version: u32,
1732    command: String,
1733    status: CommandStatus,
1734    message: String,
1735    exit_code: ExitCode,
1736    data: Option<CommandData>,
1737    next: Vec<crate::next::NextStep>,
1738    code: Option<&'static crate::codes::Entry>,
1739    fix: Vec<String>,
1740}
1741
1742/// Envelope v2: `next`, `code`, `fix` and `docs` beside the typed `data`.
1743impl Serialize for CommandResult {
1744    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1745        use serde::ser::SerializeMap as _;
1746        let mut map = serializer.serialize_map(None)?;
1747        map.serialize_entry("schema_version", &self.schema_version)?;
1748        map.serialize_entry("command", &self.command)?;
1749        map.serialize_entry("status", &self.status)?;
1750        map.serialize_entry("exit_code", &self.exit_code)?;
1751        map.serialize_entry("message", &self.message)?;
1752        if let Some(code) = self.code() {
1753            map.serialize_entry("code", code.name)?;
1754            map.serialize_entry("fix", &self.fix())?;
1755            map.serialize_entry("docs", &code.docs())?;
1756        }
1757        if !self.next.is_empty() {
1758            map.serialize_entry("next", &self.next)?;
1759        }
1760        if let Some(data) = &self.data {
1761            map.serialize_entry("payload", data.kind())?;
1762            map.serialize_entry("data", data)?;
1763        }
1764        map.end()
1765    }
1766}
1767
1768/// The envelope as it is serialised — the schema of [`CommandResult`]
1769/// (whose fields are private and whose serialisation is written by hand).
1770#[derive(schemars::JsonSchema)]
1771#[allow(dead_code)] // schema only
1772struct Envelope {
1773    /// The envelope's version.
1774    schema_version: u32,
1775    /// The command.
1776    command: String,
1777    /// The outcome class.
1778    status: CommandStatus,
1779    /// The process exit status (always the status's).
1780    exit_code: ExitCode,
1781    /// One line, for a person.
1782    message: String,
1783    /// The catalog code: present for every non-ok result and for a
1784    /// degraded success.
1785    #[serde(skip_serializing_if = "Option::is_none")]
1786    code: Option<String>,
1787    /// How to resolve it: commands, or `→ ` lines for a person.
1788    #[serde(skip_serializing_if = "Option::is_none")]
1789    fix: Option<Vec<String>>,
1790    /// The code's section on the codes page.
1791    #[serde(skip_serializing_if = "Option::is_none")]
1792    docs: Option<String>,
1793    /// The steps that continue.
1794    #[serde(skip_serializing_if = "Option::is_none")]
1795    next: Option<Vec<crate::next::NextStep>>,
1796    /// The name of `data`'s schema (`schemas/v2/<payload>.json`).
1797    #[serde(skip_serializing_if = "Option::is_none")]
1798    payload: Option<String>,
1799    /// The command's payload.
1800    #[serde(skip_serializing_if = "Option::is_none")]
1801    data: Option<CommandData>,
1802}
1803
1804impl schemars::JsonSchema for CommandResult {
1805    fn schema_name() -> std::borrow::Cow<'static, str> {
1806        "CommandResult".into()
1807    }
1808
1809    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1810        Envelope::json_schema(generator)
1811    }
1812}
1813
1814impl CommandResult {
1815    /// A result with the given status.
1816    #[must_use]
1817    pub fn new(command: &str, status: CommandStatus, message: &str) -> Self {
1818        Self {
1819            schema_version: SCHEMA_VERSION,
1820            command: command.to_owned(),
1821            status,
1822            message: message.to_owned(),
1823            exit_code: status.exit_code(),
1824            data: None,
1825            next: Vec::new(),
1826            code: None,
1827            fix: Vec::new(),
1828        }
1829    }
1830
1831    /// `from`'s outcome reported as `command`'s: status, message and code,
1832    /// without `from`'s payload (a command passing on a failure it met).
1833    #[must_use]
1834    pub fn carried(command: &str, from: &Self) -> Self {
1835        let mut out = Self::new(command, from.status, &from.message);
1836        out.code = from.code;
1837        out.fix.clone_from(&from.fix);
1838        out
1839    }
1840
1841    /// The same result with another status and message: payload and next
1842    /// steps kept, the code kept only while the status stays the same.
1843    #[must_use]
1844    pub fn reworded(mut self, status: CommandStatus, message: &str) -> Self {
1845        if status != self.status {
1846            self.code = None;
1847            self.fix.clear();
1848        }
1849        self.status = status;
1850        self.exit_code = status.exit_code();
1851        message.clone_into(&mut self.message);
1852        self
1853    }
1854
1855    /// Names the code (a catalog name); its fixes become the result's
1856    /// unless [`Self::with_fix`] gives better ones. An unknown name is
1857    /// ignored, and the status's default code applies later.
1858    #[must_use]
1859    pub fn with_code(mut self, name: &str) -> Self {
1860        if let Some(entry) = crate::codes::lookup(name) {
1861            self.code = Some(entry);
1862        }
1863        self
1864    }
1865
1866    /// The fix lines, replacing the catalog's.
1867    #[must_use]
1868    pub fn with_fix(mut self, fix: Vec<String>) -> Self {
1869        self.fix = fix;
1870        self
1871    }
1872
1873    /// The code: the one attached, else the status's default (none for a
1874    /// plain `ok`) — no non-ok result is without one.
1875    #[must_use]
1876    pub fn code(&self) -> Option<&'static crate::codes::Entry> {
1877        self.code.or_else(|| crate::codes::default_for(self.status))
1878    }
1879
1880    /// Whether a code was named explicitly (not the status's default).
1881    #[must_use]
1882    pub fn has_explicit_code(&self) -> bool {
1883        self.code.is_some()
1884    }
1885
1886    /// How to resolve it: the fixes given, else the code's own.
1887    #[must_use]
1888    pub fn fix(&self) -> Vec<String> {
1889        match self.code() {
1890            Some(_) if !self.fix.is_empty() => self.fix.clone(),
1891            Some(entry) => crate::codes::fixes(entry, &self.command, None, None),
1892            None => Vec::new(),
1893        }
1894    }
1895
1896    /// Attaches what to do next (`data.next`, the human `Next:` block).
1897    #[must_use]
1898    pub fn with_next(mut self, next: Vec<crate::next::NextStep>) -> Self {
1899        self.next = next;
1900        self
1901    }
1902
1903    /// What to do next.
1904    #[must_use]
1905    pub fn next(&self) -> &[crate::next::NextStep] {
1906        &self.next
1907    }
1908
1909    /// Attaches the command's payload.
1910    #[must_use]
1911    pub fn with_data(mut self, data: CommandData) -> Self {
1912        self.data = Some(data);
1913        self
1914    }
1915
1916    /// The command's payload, if any.
1917    #[must_use]
1918    pub fn data(&self) -> Option<&CommandData> {
1919        self.data.as_ref()
1920    }
1921
1922    /// A successful result.
1923    #[must_use]
1924    pub fn ok(command: &str, message: &str) -> Self {
1925        Self::new(command, CommandStatus::Ok, message)
1926    }
1927
1928    /// The subcommand name, e.g. `inspect`.
1929    #[must_use]
1930    pub fn command(&self) -> &str {
1931        &self.command
1932    }
1933
1934    /// The outcome class.
1935    #[must_use]
1936    pub fn status(&self) -> CommandStatus {
1937        self.status
1938    }
1939
1940    /// The single-line human-readable message.
1941    #[must_use]
1942    pub fn message(&self) -> &str {
1943        &self.message
1944    }
1945
1946    /// The process exit status.
1947    #[must_use]
1948    pub fn exit_code(&self) -> ExitCode {
1949        self.exit_code
1950    }
1951}
1952
1953#[cfg(test)]
1954mod tests {
1955    use super::*;
1956
1957    #[test]
1958    fn every_status_maps_to_its_documented_exit_code() {
1959        assert_eq!(CommandStatus::Ok.exit_code().code(), 0);
1960        assert_eq!(CommandStatus::Error.exit_code().code(), 1);
1961        assert_eq!(CommandStatus::UsageError.exit_code().code(), 2);
1962        assert_eq!(CommandStatus::NotAProject.exit_code().code(), 10);
1963        assert_eq!(CommandStatus::DiagnosticsError.exit_code().code(), 11);
1964        assert_eq!(CommandStatus::SelectionNeeded.exit_code().code(), 12);
1965        assert_eq!(CommandStatus::ConfigError.exit_code().code(), 13);
1966        assert_eq!(CommandStatus::GitPrecondition.exit_code().code(), 14);
1967        assert_eq!(CommandStatus::VerificationFailed.exit_code().code(), 20);
1968        assert_eq!(CommandStatus::NotFound.exit_code().code(), 15);
1969        assert_eq!(CommandStatus::RunFailed.exit_code().code(), 21);
1970        assert_eq!(CommandStatus::ReplayDiverged.exit_code().code(), 22);
1971        assert_eq!(CommandStatus::TaskNotReady.exit_code().code(), 16);
1972        assert_eq!(CommandStatus::NeedsHuman.exit_code().code(), 23);
1973        assert_eq!(CommandStatus::EvalFailed.exit_code().code(), 24);
1974        assert_eq!(CommandStatus::NotReplayable.exit_code().code(), 25);
1975        assert_eq!(CommandStatus::AttestationFailed.exit_code().code(), 26);
1976    }
1977
1978    #[test]
1979    fn exit_code_follows_status() {
1980        let r = CommandResult::new("verify", CommandStatus::VerificationFailed, "x");
1981        assert_eq!(r.exit_code(), r.status().exit_code());
1982        assert_eq!(CommandResult::ok("x", "").exit_code(), ExitCode::OK);
1983    }
1984
1985    #[test]
1986    fn serializes_to_schema_version_2() {
1987        let r = CommandResult::new("replay", CommandStatus::NotReplayable, "not replayable")
1988            .with_code("NOT-REPLAYABLE")
1989            .with_fix(vec!["x".to_owned()]);
1990        let json = serde_json::to_value(&r).unwrap();
1991        assert_eq!(
1992            json,
1993            serde_json::json!({
1994                "schema_version": 2,
1995                "command": "replay",
1996                "status": "not_replayable",
1997                "exit_code": 25,
1998                "message": "not replayable",
1999                "code": "NOT-REPLAYABLE",
2000                "fix": ["x"],
2001                "docs": "https://hupe1980.github.io/speccoder/docs/codes/#not-replayable"
2002            })
2003        );
2004        let with_next = CommandResult::new("run", CommandStatus::NeedsHuman, "x")
2005            .with_next(crate::next::next(&crate::next::Outcome::Decided, None));
2006        let json = serde_json::to_value(&with_next).unwrap();
2007        assert_eq!(json["next"][0]["command"], "speccoder run");
2008        assert!(json.get("data").is_none());
2009    }
2010}