Skip to main content

speccoder_core/
command.rs

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