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