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