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