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