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