Skip to main content

speccoder_core/
command.rs

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