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