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