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 reached the implementer, by digest (constitution VIII).
1718 #[serde(default, skip_serializing_if = "Option::is_none")]
1719 pub context: Option<crate::context::ContextManifest>,
1720}
1721
1722/// A failed attempt that was undone and retried.
1723#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1724pub struct EarlierAttempt {
1725 /// The agentplane run of the attempt.
1726 pub run_id: String,
1727 /// The attempt's status.
1728 pub attempt_status: String,
1729 /// The verifier's verdict.
1730 pub verdict: Option<crate::verify::Verdict>,
1731 /// The failure's fingerprint.
1732 pub fingerprint: String,
1733 /// What the next attempt was told.
1734 pub digest: crate::retry::Digest,
1735 /// Token usage of the attempt.
1736 pub usage: Option<serde_json::Value>,
1737}
1738
1739/// Result of `speccoder run` without `--task`: a feature worked through.
1740#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1741pub struct FeatureReport {
1742 /// Feature directory name.
1743 pub feature: String,
1744 /// Every attempt, in order.
1745 pub attempts: Vec<RunReport>,
1746 /// Why the run stopped.
1747 pub stop: String,
1748 /// Convergence rounds between task runs (`run --converge`), in order.
1749 pub converge: Vec<ConvergeReport>,
1750 /// Extension hooks run, offered or skipped around it.
1751 pub hooks: Vec<String>,
1752 /// What parallel groups did: merged, conflicts, re-verification.
1753 pub parallel: Vec<String>,
1754 /// The `full` check tier, run once after the run ticked the last task.
1755 #[serde(skip_serializing_if = "Option::is_none")]
1756 pub full_tier: Option<FullTier>,
1757 /// How the run asked for a person: `ask` or `autopilot`.
1758 #[serde(default)]
1759 pub mode: crate::oracle::RunMode,
1760 /// The calls an autopilot run refused instead of asking: listed for
1761 /// the person.
1762 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1763 pub autopilot_denied: Vec<crate::oracle::AutopilotDenial>,
1764}
1765
1766/// The `full` check tier at the end of a feature.
1767#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1768pub struct FullTier {
1769 /// The checks that ran.
1770 pub ran: Vec<String>,
1771 /// Every check passed (or none applied).
1772 pub passed: bool,
1773 /// The checks that failed.
1774 pub failed: Vec<String>,
1775 /// Why nothing ran, or the record could not be written.
1776 pub note: Option<String>,
1777}
1778
1779/// Result of `speccoder spec` without a name.
1780#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1781pub struct SkillsReport {
1782 /// Installed skills by name.
1783 pub skills: Vec<SkillEntry>,
1784}
1785
1786/// One installed Agent Skill.
1787#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1788pub struct SkillEntry {
1789 /// Its name.
1790 pub name: String,
1791 /// What it does.
1792 pub description: String,
1793 /// The `SKILL.md`.
1794 pub path: String,
1795 /// Why speccoder does not host it, if it does not.
1796 pub refused: Option<String>,
1797}
1798
1799/// Result of `speccoder spec <name>`.
1800#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1801pub struct SkillRunReport {
1802 /// The skill.
1803 pub skill: String,
1804 /// The agentplane run.
1805 pub run_id: String,
1806 /// `completed`, `incomplete`, `needs_approval`, `blocked`, `denied`, `budget_exhausted` or `failed`.
1807 pub status: String,
1808 /// The model's summary.
1809 pub summary: Option<String>,
1810 /// Questions it could not get answered.
1811 pub questions: Vec<String>,
1812 /// Paths changed in the checkout: `added` / `modified` / `deleted`.
1813 pub changes: Vec<(String, String)>,
1814 /// Every tool call with the policy's decision.
1815 pub tool_calls: Vec<serde_json::Value>,
1816 /// The call awaiting approval, if any.
1817 pub pending: Option<serde_json::Value>,
1818 /// Why it stopped, when it did not complete.
1819 pub reason: Option<String>,
1820 /// Token usage.
1821 pub usage: Option<serde_json::Value>,
1822 /// Extension hooks run, offered or skipped around it.
1823 pub hooks: Vec<String>,
1824 /// The unit the skill worked on, or created and made current, and how
1825 /// it is known (`001-x (recorded in .specify/feature.json)`).
1826 #[serde(default, skip_serializing_if = "Option::is_none")]
1827 pub current: Option<String>,
1828}
1829
1830/// Result of `speccoder init`.
1831#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1832pub struct InitReport {
1833 /// The configuration file.
1834 pub path: String,
1835 /// The checks it declares.
1836 pub checks: Vec<InitCheck>,
1837 /// `provider/model`.
1838 pub model: String,
1839 /// Files written, relative to the project root.
1840 pub wrote: Vec<String>,
1841 /// The checks run on the untouched code (empty: not run).
1842 pub ran: Vec<InitRun>,
1843 /// The repository instructions found (`AGENTS.md`, `CLAUDE.md`).
1844 pub instructions: Vec<String>,
1845 /// The model key: the variable and whether it is set.
1846 pub key: InitKey,
1847 /// The region written (Bedrock only).
1848 #[serde(default, skip_serializing_if = "Option::is_none")]
1849 pub region: Option<String>,
1850 /// The commit holding `speccoder.toml`, when one was made.
1851 pub committed: Option<String>,
1852 /// The git-ignored files proposed for `[worktree] include` (the
1853 /// checkout's `.env` files): copied into each run's worktree.
1854 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1855 pub include: Vec<String>,
1856 /// The proposal was written as the setting (confirmed, or `--yes`);
1857 /// else it is commented.
1858 #[serde(default)]
1859 pub include_active: bool,
1860 /// The checkout has a `.worktreeinclude`, which runs read already.
1861 #[serde(default)]
1862 pub worktreeinclude: bool,
1863}
1864
1865/// One check `init` ran on the untouched code.
1866#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1867pub struct InitRun {
1868 /// `[checks.<name>]`.
1869 pub name: String,
1870 /// Passed on the untouched code.
1871 pub passed: bool,
1872 /// `passed`, `failed`, `timed_out`, `skipped`, `error`.
1873 pub status: String,
1874 /// How long it took.
1875 pub duration_ms: u64,
1876 /// Why it did not pass: the last line of its output that says something
1877 /// (`No module named pytest`), or the runner's note.
1878 pub reason: Option<String>,
1879}
1880
1881/// The model key `init` looked for.
1882#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, schemars::JsonSchema)]
1883pub struct InitKey {
1884 /// `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `AWS_BEARER_TOKEN_BEDROCK`.
1885 pub variable: String,
1886 /// Set in this environment (Bedrock: a credential source was found).
1887 pub set: bool,
1888 /// Bedrock: where the AWS chain will take credentials from, by kind and
1889 /// name (`aws login session (profile dev)`), never a value.
1890 #[serde(default, skip_serializing_if = "Option::is_none")]
1891 pub source: Option<String>,
1892}
1893
1894/// One detected check.
1895#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1896pub struct InitCheck {
1897 /// `[checks.<name>]`.
1898 pub name: String,
1899 /// The command.
1900 pub argv: Vec<String>,
1901 /// `fast` or `full`.
1902 pub tier: String,
1903 /// Why it was proposed.
1904 pub why: String,
1905}
1906
1907/// Result of `accept`, `skip` and `nudge`.
1908#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1909pub struct ControlReport {
1910 /// Feature directory name.
1911 pub feature: String,
1912 /// Task ID.
1913 pub task: String,
1914 /// `accepted`, `skipped`, `unskipped`, `nudged` or `nudges_cleared`.
1915 pub action: String,
1916 /// The person's reason or note.
1917 pub text: Option<String>,
1918 /// The commit or ref that records it, when it lives in git.
1919 pub record: Option<String>,
1920}
1921
1922/// Result of `speccoder report --cost`.
1923#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1924pub struct CostReport {
1925 /// Feature directory name.
1926 pub feature: String,
1927 /// Per task, every attempt counted (passed and saved).
1928 pub tasks: Vec<TaskCost>,
1929 /// Convergence assessments.
1930 pub convergence: Usage,
1931 /// Everything.
1932 pub total: Usage,
1933 /// The model the prices apply to, when prices are configured.
1934 pub priced_model: Option<String>,
1935 /// Ask's conversations about the feature, apart from `total`: Ask has
1936 /// a budget of its own (`[ask]`).
1937 pub ask: Usage,
1938}
1939
1940/// One task's cost.
1941#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1942pub struct TaskCost {
1943 /// Task ID.
1944 pub task: String,
1945 /// Attempts recorded in git (the passing commit and saved attempts).
1946 pub attempts: usize,
1947 /// Tokens and money.
1948 pub usage: Usage,
1949}
1950
1951/// Token counts, and USD when prices are configured.
1952#[derive(Debug, Clone, Default, PartialEq, Serialize, schemars::JsonSchema)]
1953pub struct Usage {
1954 /// Input tokens (including those read from the cache).
1955 pub input_tokens: u64,
1956 /// Output tokens.
1957 pub output_tokens: u64,
1958 /// Input tokens read from the cache.
1959 pub cache_read_tokens: u64,
1960 /// Tokens written to the cache.
1961 pub cache_write_tokens: u64,
1962 /// USD, when `[model]` has prices.
1963 pub usd: Option<f64>,
1964}
1965
1966/// Result of `speccoder report --approvals`, `approve` and `reject`.
1967#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
1968pub struct ApprovalsReport {
1969 /// Feature directory name.
1970 pub feature: String,
1971 /// Calls that stopped an open task and await a decision.
1972 pub pending: Vec<PendingCall>,
1973 /// Decisions recorded for the feature's tasks.
1974 pub decided: Vec<DecidedCall>,
1975 /// Why the calls waiting in the journal could not be read (another
1976 /// speccoder holds it, …): `pending` may then be incomplete — unknown,
1977 /// never "nothing waiting".
1978 pub unreadable: Option<String>,
1979 /// Runs whose asking budget is used: their waiting calls, decided
1980 /// together (`speccoder approvals batch`).
1981 #[serde(skip_serializing_if = "Vec::is_empty")]
1982 pub batch: Vec<BatchView>,
1983}
1984
1985/// The object a person's decision covers, as reports show it.
1986#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
1987pub struct ObjectView {
1988 /// `write`, `command`, `network` or `mcp`.
1989 pub class: String,
1990 /// The canonical path, `argv#<12 hex>` or `<tool>@<12 hex>`.
1991 pub short: String,
1992 /// One line: ``run `make check` ``.
1993 pub describe: String,
1994 /// The object in full, as every interface shows it.
1995 pub lines: Vec<String>,
1996}
1997
1998/// A run whose asking budget is used, with its waiting calls by class.
1999#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2000pub struct BatchView {
2001 /// The run.
2002 pub run: String,
2003 /// SHA-256 over the sorted digests of every call listed
2004 /// ([`PendingCall::digest`]): a decision names it and is refused when
2005 /// the list, or the object of a call in it, changed.
2006 pub digest: String,
2007 /// The calls by class, in the order write, command, network, mcp.
2008 pub groups: Vec<BatchGroup>,
2009}
2010
2011/// One class of a batch.
2012#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2013pub struct BatchGroup {
2014 /// `write`, `command`, `network` or `mcp`.
2015 pub class: String,
2016 /// The waiting calls' ids.
2017 pub calls: Vec<String>,
2018}
2019
2020/// A call that stopped an attempt for approval, or a question, waiting for
2021/// a person.
2022#[derive(Debug, Clone, Default, PartialEq, Serialize, schemars::JsonSchema)]
2023pub struct PendingCall {
2024 /// Task ID.
2025 pub task: String,
2026 /// What a decision would cover, e.g. ``write `Cargo.toml` ``.
2027 pub covers: String,
2028 /// The tool.
2029 pub tool: String,
2030 /// Its arguments as the model sent them.
2031 pub arguments: serde_json::Value,
2032 /// Why it needs approval.
2033 pub reason: String,
2034 /// The saved attempt.
2035 pub reference: String,
2036 /// The call as the inline prompt shows it.
2037 pub presentation: Option<ApprovalView>,
2038 /// The waiting call's id (`approve --call`); empty for a stopped attempt.
2039 pub call: String,
2040 /// `task` (the attempt waits and resumes) or `attempt` (stopped and saved
2041 /// in a parallel lane).
2042 pub source: String,
2043 /// `approval` or `question`.
2044 pub kind: String,
2045 /// A question's text.
2046 pub question: Option<String>,
2047 /// Seconds since the epoch the call has waited since.
2048 pub waiting_since: Option<u64>,
2049 /// Seconds since the epoch it is denied as expired at.
2050 pub deadline: Option<u64>,
2051 /// The object a decision covers.
2052 pub object: Option<ObjectView>,
2053 /// Where arguments of the call were copied from (source lines).
2054 #[serde(skip_serializing_if = "Vec::is_empty")]
2055 pub sources: Vec<String>,
2056 /// The run it belongs to.
2057 pub run: Option<String>,
2058 /// It waits in its run's batch: the asking budget was used.
2059 pub batched: bool,
2060 /// SHA-256 over its id and the object a decision covers: a decision
2061 /// names it and is refused when another call (or another object) waits
2062 /// now.
2063 pub digest: String,
2064}
2065
2066impl PendingCall {
2067 /// Its id in decisions and batches: the waiting call's, or
2068 /// `attempt:<task>` for a call a parallel lane stopped.
2069 #[must_use]
2070 pub fn id(&self) -> String {
2071 if self.call.is_empty() {
2072 format!("attempt:{}", self.task)
2073 } else {
2074 self.call.clone()
2075 }
2076 }
2077}
2078
2079/// Who decided, and how speccoder knows the name.
2080#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2081pub struct OperatorView {
2082 /// The name.
2083 pub actor: String,
2084 /// `asserted` (typed at a local terminal or the CLI) or `connected`
2085 /// (reported by a client over its connection, e.g. an editor).
2086 pub basis: String,
2087}
2088
2089/// A pending call as a person sees it: what, why, what it can reach, what
2090/// the model claimed, and what approving it grants.
2091#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2092pub struct ApprovalView {
2093 /// `write`, `edit`, `run`, `network` or `mcp`.
2094 pub kind: String,
2095 /// `Edit this file?`, `Run this command?`, ….
2096 pub title: String,
2097 /// The path, the whole command, or the MCP tool.
2098 pub subject: String,
2099 /// The diff, the file's head, the command or the arguments.
2100 pub body_lines: Vec<String>,
2101 /// What the call can reach.
2102 pub sandbox: String,
2103 /// What the model said when it asked (a claim).
2104 pub claim: Option<String>,
2105 /// `low`, `medium` or `high`.
2106 pub risk: String,
2107 /// What `approve` grants: `for the rest of T004: this file, any content`.
2108 pub scope: String,
2109 /// The object's lines.
2110 pub object: Vec<String>,
2111 /// Where arguments were copied from.
2112 pub sources: Vec<String>,
2113 /// The choices offered, in order.
2114 pub options: Vec<String>,
2115}
2116
2117/// A person's decision.
2118#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2119pub struct DecidedCall {
2120 /// Task ID.
2121 pub task: String,
2122 /// `approved` or `rejected`.
2123 pub decision: String,
2124 /// What it covers.
2125 pub covers: String,
2126 /// The reason, for a rejection.
2127 pub reason: Option<String>,
2128 /// Who decided, as `name (via)`.
2129 pub by: Option<String>,
2130 /// How far an approval reaches: `once`, `task` or `run`.
2131 pub scope: Option<String>,
2132 /// An approval's note to the model.
2133 pub note: Option<String>,
2134 /// The waiting call it decided, when it was one.
2135 pub call: Option<String>,
2136 /// Who decided; `None` for an expiry.
2137 pub operator: Option<OperatorView>,
2138 /// Through what: an origin of `actions::Via` (`terminal`, `web`,
2139 /// `acp`, …).
2140 pub via: Option<String>,
2141 /// The editor decided in, over the Agent Client Protocol.
2142 #[serde(skip_serializing_if = "Option::is_none")]
2143 pub client: Option<String>,
2144 /// Nobody decided: the deadline passed and the call was denied.
2145 pub expired: bool,
2146 /// The object it decided on.
2147 pub object: Option<ObjectView>,
2148 /// The run it belongs to (a grant covers nothing after it).
2149 pub run: Option<String>,
2150}
2151
2152/// What `attest verify` found for one task object.
2153#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, schemars::JsonSchema)]
2154#[serde(rename_all = "snake_case")]
2155pub enum VerifyOutcome {
2156 /// Every line parses, every signature verifies against the trust input,
2157 /// and every statement agrees with the commit.
2158 Ok,
2159 /// The statements hold but are not signed.
2160 Unsigned,
2161 /// No speccoder attestation on the object.
2162 Missing,
2163 /// A signature does not verify.
2164 BadSignature,
2165 /// A signature verifies, but its key is not trusted by the verifier.
2166 UntrustedSigner,
2167 /// A statement disagrees with the commit, its trailers, the spec or the bundle.
2168 Mismatch,
2169 /// A line does not parse.
2170 Malformed,
2171 /// The signature's tool (`ssh-keygen`, `gpg`, `cosign`) cannot run:
2172 /// nothing can be said about it.
2173 ToolMissing,
2174}
2175
2176impl VerifyOutcome {
2177 /// How grave: when several apply, the gravest is the outcome
2178 /// (`malformed` > `bad_signature` > `mismatch` > `tool_missing` >
2179 /// `untrusted_signer` > `unsigned` > `ok`; `missing` stands alone).
2180 #[must_use]
2181 pub const fn rank(self) -> u8 {
2182 match self {
2183 Self::Ok => 0,
2184 Self::Unsigned => 1,
2185 Self::UntrustedSigner => 2,
2186 Self::ToolMissing => 3,
2187 Self::Mismatch => 4,
2188 Self::BadSignature => 5,
2189 Self::Malformed => 6,
2190 Self::Missing => 7,
2191 }
2192 }
2193
2194 /// The word reports use.
2195 #[must_use]
2196 pub const fn word(self) -> &'static str {
2197 match self {
2198 Self::Ok => "ok",
2199 Self::Unsigned => "unsigned",
2200 Self::Missing => "missing",
2201 Self::BadSignature => "bad_signature",
2202 Self::UntrustedSigner => "untrusted_signer",
2203 Self::Mismatch => "mismatch",
2204 Self::Malformed => "malformed",
2205 Self::ToolMissing => "tool_missing",
2206 }
2207 }
2208}
2209
2210/// Why an object got its outcome.
2211#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2212pub struct VerifyReason {
2213 /// A stable code, e.g. `subject`, `trailer`, `line_hash`, `signature`.
2214 pub code: String,
2215 /// The field that disagrees.
2216 #[serde(skip_serializing_if = "Option::is_none")]
2217 pub field: Option<String>,
2218 /// What it should be.
2219 #[serde(skip_serializing_if = "Option::is_none")]
2220 pub expected: Option<String>,
2221 /// What it is.
2222 #[serde(skip_serializing_if = "Option::is_none")]
2223 pub found: Option<String>,
2224}
2225
2226/// Who signed.
2227#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2228pub struct SignerView {
2229 /// `ssh`, `openpgp` or `sigstore`.
2230 pub kind: String,
2231 /// The key's fingerprint (`SHA256:…`, an OpenPGP fingerprint).
2232 pub keyid: String,
2233 /// The principal the verifier's trust input names for it.
2234 #[serde(skip_serializing_if = "Option::is_none")]
2235 pub principal: Option<String>,
2236}
2237
2238/// One task object's verification.
2239#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2240pub struct ObjectVerification {
2241 /// The commit (or skip record) id.
2242 pub object: String,
2243 /// `task` or `skip`.
2244 pub kind: String,
2245 /// Task ID.
2246 pub task: String,
2247 /// The verdict the statements claim.
2248 pub verdict: Option<String>,
2249 /// The outcome.
2250 pub outcome: VerifyOutcome,
2251 /// Every reason found.
2252 pub reasons: Vec<VerifyReason>,
2253 /// Who signed, when signed.
2254 pub signer: Option<SignerView>,
2255 /// How many statements the bundle holds.
2256 pub statements: usize,
2257 /// For a missing attestation: an attested commit with the same tree.
2258 pub same_tree_as: Option<String>,
2259}
2260
2261/// Result of `speccoder attest verify` and `attest sign`.
2262#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2263pub struct AttestReport {
2264 /// Feature directory name.
2265 pub feature: String,
2266 /// Where trust came from: the allowed-signers file, whether GPG's keyring
2267 /// was used, the expected Sigstore identity.
2268 pub trust: serde_json::Value,
2269 /// One per object.
2270 pub objects: Vec<ObjectVerification>,
2271 /// Count per outcome word.
2272 pub totals: std::collections::BTreeMap<String, usize>,
2273}
2274
2275/// One row of the requirement → scenario → test → verdict matrix.
2276#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2277pub struct TraceRow {
2278 /// `FR-001`, `SC-002`.
2279 pub requirement: String,
2280 /// `US1.2`: story 1, acceptance scenario 2.
2281 pub scenario: Option<String>,
2282 /// The acceptance test.
2283 pub test: Option<String>,
2284 /// The task that cites it.
2285 pub task: Option<String>,
2286 /// That task's verdict.
2287 pub verdict: Option<String>,
2288 /// The requirement changed since that task's tick: the verdict is for
2289 /// its earlier wording.
2290 #[serde(default)]
2291 pub changed: bool,
2292 /// A tick made in the unit this one was promoted from: proven against
2293 /// an earlier wording, never counted as verified here.
2294 #[serde(default, skip_serializing_if = "Option::is_none")]
2295 pub inherited: Option<Inherited>,
2296}
2297
2298/// Where an inherited trace row was proven.
2299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2300pub struct Inherited {
2301 /// The lower unit.
2302 pub unit: String,
2303 /// Its requirement.
2304 pub requirement: String,
2305 /// Its task.
2306 pub task: String,
2307 /// The commit that holds the evidence.
2308 pub commit: Option<String>,
2309}
2310
2311/// What changed since a tick: the tick's commit holds the intent
2312/// it was judged against; the checkout's `HEAD` holds the intent now.
2313#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2314pub struct Drift {
2315 /// The requirements the task cites whose line changed, appeared or went
2316 /// away: the evidence no longer speaks for them. Empty: what changed is
2317 /// elsewhere in the intent.
2318 pub requirements: Vec<RequirementDrift>,
2319 /// The intent files that differ (project-relative), task lists aside.
2320 /// Empty when the intent is not in git (in place, ignored): it changed,
2321 /// which file is not known.
2322 pub files: Vec<String>,
2323}
2324
2325impl Drift {
2326 /// A requirement the task cites changed: the tick does not prove what
2327 /// the spec asks now.
2328 #[must_use]
2329 pub fn stale(&self) -> bool {
2330 !self.requirements.is_empty()
2331 }
2332}
2333
2334/// One cited requirement, as the tick saw it and as the spec has it now.
2335#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2336pub struct RequirementDrift {
2337 /// `FR-003`.
2338 pub id: String,
2339 /// Its line when the task was ticked; `None`: it did not exist yet.
2340 pub then: Option<String>,
2341 /// Its line now; `None`: it was removed.
2342 pub now: Option<String>,
2343}
2344
2345/// Result of `speccoder report`: what the branch's history shows.
2346#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
2347pub struct StatusReport {
2348 /// Feature directory name.
2349 pub feature: String,
2350 /// `speccoder/<feature>`, if speccoder has worked on the feature.
2351 pub branch: Option<String>,
2352 /// Where `tasks.md` was read: `branch` or `checkout`.
2353 pub source: String,
2354 /// How the feature was chosen (`recorded in .specify/feature.json`,
2355 /// `the only feature`, `named with --feature`).
2356 #[serde(default)]
2357 pub selected_by: String,
2358 /// The planning artifacts that exist (`spec`, `plan`, `tasks`).
2359 #[serde(default)]
2360 pub artifacts: Vec<String>,
2361 /// While the feature has no task list: the planning skills that come
2362 /// next (`plan`, `tasks`); empty once it has one.
2363 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2364 pub planning: Vec<String>,
2365 /// Planning files under the feature's directory not yet committed
2366 /// (`speccoder run` reads committed files only).
2367 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2368 pub uncommitted: Vec<String>,
2369 /// Where the feature is: spec → plan → tasks → commit → run → review.
2370 #[serde(default)]
2371 pub journey: Vec<crate::actions::JourneyStep>,
2372 /// Commits on the branch that the checkout's HEAD does not contain.
2373 pub unmerged_commits: usize,
2374 /// Every task, in document order.
2375 pub tasks: Vec<TaskStatus>,
2376 /// Convergence rounds, oldest first.
2377 pub convergence: Vec<ConvergenceRound>,
2378 /// Requirement → task → verdict, in the spec's order; a requirement no
2379 /// task cites is a row without a task.
2380 pub trace: Vec<TraceRow>,
2381 /// Counts by evidence.
2382 pub totals: StatusTotals,
2383 /// GitHub issue URLs the feature's spec names: where it came from, and
2384 /// what its pull request closes.
2385 pub issues: Vec<String>,
2386 /// The person's review of the spec's requirements, when the
2387 /// format has requirement identifiers.
2388 #[serde(default, skip_serializing_if = "Option::is_none")]
2389 pub review: Option<ReviewTotals>,
2390}
2391
2392/// One task and its evidence.
2393#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2394pub struct TaskStatus {
2395 /// Task ID.
2396 pub id: String,
2397 /// Title after markers.
2398 pub title: String,
2399 /// Enclosing phase heading.
2400 pub phase: String,
2401 /// `verified`, `regression_only` (ticked by speccoder with that verdict),
2402 /// `attested` (ticked by `speccoder task accept`), `ticked_without_evidence`
2403 /// (ticked, no speccoder commit), `evidence_without_tick` (in place: a
2404 /// verdict commit names the task, its local tick is missing),
2405 /// `skipped`, `ready` or `blocked`.
2406 pub state: String,
2407 /// What changed in the intent (spec, plan, constitution) since this
2408 /// task's evidence was committed: its tick was judged against an earlier
2409 /// version. `None`: nothing changed, or the task has no commit.
2410 #[serde(default, skip_serializing_if = "Option::is_none")]
2411 pub drift: Option<Drift>,
2412 /// For a task speccoder committed: its attestation as the note says,
2413 /// signatures unchecked — `signed <kind> <keyid>`, `unsigned` or
2414 /// `missing` (`attest verify` checks them).
2415 pub attestation: Option<String>,
2416 /// Open dependencies of a blocked task.
2417 pub blocked_by: Vec<String>,
2418 /// The speccoder commit that ticked it.
2419 pub commit: Option<String>,
2420 /// The agentplane run of that attempt.
2421 pub run_id: Option<String>,
2422 /// For an open task: its latest saved attempt.
2423 pub last_attempt: Option<LastAttempt>,
2424 /// Changes found in the worktree when the task's run started (a person's
2425 /// edits, or an interrupted run's leftovers), saved and set aside.
2426 #[serde(skip_serializing_if = "Option::is_none")]
2427 pub set_aside: Option<SetAside>,
2428 /// The reason a person gave for `accept` or `skip`.
2429 pub note: Option<String>,
2430 /// A verified tick's acceptance strength as its trailer says
2431 /// (`0.83 mutants=9/11 near-miss=1/2`, `not measured (…)`).
2432 #[serde(default, skip_serializing_if = "Option::is_none")]
2433 pub strength: Option<String>,
2434 /// A bug task: the bug's outcome (`awaiting_confirmation`,
2435 /// `cannot_reproduce`, `fixed`, …).
2436 #[serde(default, skip_serializing_if = "Option::is_none")]
2437 pub bug: Option<String>,
2438 /// A regression-only tick: why, with speccoder's wording
2439 /// (`unrecorded` for a tick made before reasons were recorded).
2440 #[serde(default, skip_serializing_if = "Option::is_none")]
2441 pub regression: Option<crate::regression::Regression>,
2442 /// Who does it ([`crate::lane`]); left out for `code`.
2443 #[serde(default, skip_serializing_if = "crate::lane::Lane::is_code")]
2444 pub lane: crate::lane::Lane,
2445}
2446
2447/// An attempt that did not pass, as saved under `refs/speccoder/attempts/`.
2448#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2449pub struct LastAttempt {
2450 /// `verification_failed`, `blocked`, `needs_approval`, `not_demonstrated`, `interrupted`, …
2451 pub outcome: String,
2452 /// Why.
2453 pub reason: String,
2454 /// The agentplane run, when there was one.
2455 pub run_id: Option<String>,
2456 /// The ref holding the attempt's tree.
2457 pub reference: String,
2458}
2459
2460/// Changes saved and set aside so a run could start from the branch tip.
2461#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2462pub struct SetAside {
2463 /// The ref holding them.
2464 pub reference: String,
2465 /// The files they change, repository-relative.
2466 pub files: Vec<String>,
2467}
2468
2469/// One convergence round, from its commit.
2470#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2471pub struct ConvergenceRound {
2472 /// Round number.
2473 pub round: u32,
2474 /// The commit that appended it.
2475 pub commit: String,
2476 /// The appended tasks, as `T0NN text`.
2477 pub tasks: Vec<String>,
2478}
2479
2480/// Counts of [`TaskStatus::state`].
2481#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2482pub struct StatusTotals {
2483 /// Ticked with task-specific evidence.
2484 pub verified: usize,
2485 /// Ticked with regression evidence only.
2486 pub regression_only: usize,
2487 /// Ticked without a speccoder commit (by a person, or before speccoder).
2488 pub ticked_without_evidence: usize,
2489 /// In place: committed by speccoder, the local tick missing.
2490 pub evidence_without_tick: usize,
2491 /// Ticks speccoder committed without an attestation.
2492 pub without_attestation: usize,
2493 /// Ticked by `speccoder task accept`: a person's attestation, not evidence.
2494 pub attested: usize,
2495 /// Open and skipped by a person.
2496 pub skipped: usize,
2497 /// Open and ready.
2498 pub ready: usize,
2499 /// Open and blocked.
2500 pub blocked: usize,
2501 /// Ticks whose cited requirements changed since.
2502 #[serde(default)]
2503 pub stale: usize,
2504}
2505
2506/// Result of `speccoder bench import`.
2507#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2508pub struct BenchReport {
2509 /// The suite directory written.
2510 pub out: String,
2511 /// Instances turned into cases.
2512 pub imported: Vec<String>,
2513 /// Instances not imported, with why.
2514 pub skipped: Vec<String>,
2515}
2516
2517/// Result of `speccoder pr`: a draft pull request with the evidence.
2518#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2519pub struct PrReport {
2520 /// Feature directory name.
2521 pub feature: String,
2522 /// `speccoder/<feature>`.
2523 pub branch: String,
2524 /// The branch the pull request targets.
2525 pub base: String,
2526 /// The git remote pushed to.
2527 pub remote: String,
2528 /// Pull request title.
2529 pub title: String,
2530 /// Pull request body (Markdown): every task's evidence.
2531 pub body: String,
2532 /// What runs (or would run), in order.
2533 pub commands: Vec<String>,
2534 /// Whether the branch was pushed and the draft opened (`false`: a dry run).
2535 pub opened: bool,
2536 /// The pull request, when opened.
2537 pub url: Option<String>,
2538 /// The unit's held-out ref pushed with the branch
2539 /// (`refs/speccoder/held-out/<unit>`), when a closed task names a set.
2540 pub held_out_ref: Option<String>,
2541 /// The tasks closed on the pushed head whose held-out sets it carries.
2542 pub held_out_tasks: Vec<String>,
2543 /// Held-out sets of open tasks: they stay local.
2544 pub held_out_withheld: usize,
2545 /// `present`: a workflow under `.github/workflows/` runs
2546 /// `speccoder attest verify --branch`; `absent`: none does.
2547 pub evidence_check: String,
2548}
2549
2550/// Result of `speccoder converge`.
2551#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
2552pub struct ConvergeReport {
2553 /// Feature directory name.
2554 pub feature: String,
2555 /// The agentplane run of the assessment, if one ran.
2556 pub run_id: Option<String>,
2557 /// This round's number (1 = the first Convergence phase).
2558 pub round: u32,
2559 /// `converged`, `tasks_appended`, `needs_review` (only unrequested
2560 /// findings), `no_progress`, `round_limit` or `failed`.
2561 pub outcome: String,
2562 /// Every finding as the model reported it (its claim, not evidence).
2563 pub findings: Vec<serde_json::Value>,
2564 /// Tasks speccoder appended, as `T0NN text`.
2565 pub appended: Vec<String>,
2566 /// `unrequested` findings: for a person, never turned into tasks.
2567 pub unrequested: Vec<serde_json::Value>,
2568 /// Findings that repeat an earlier convergence task: no progress.
2569 pub repeated: Vec<serde_json::Value>,
2570 /// The commit on `speccoder/<feature>` holding the appended section.
2571 pub commit: Option<String>,
2572 /// Token usage of the assessment.
2573 pub usage: Option<serde_json::Value>,
2574 /// Every tool call with the policy's decision.
2575 pub tool_calls: Vec<serde_json::Value>,
2576 /// Extension hooks run, offered or skipped around it.
2577 pub hooks: Vec<String>,
2578}
2579
2580/// Result of `speccoder propose`.
2581#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
2582pub struct ProposeReport {
2583 /// The agentplane run that holds the call.
2584 pub run_id: String,
2585 /// Feature directory name.
2586 pub feature: String,
2587 /// Task ID.
2588 pub task: String,
2589 /// agentplane run status, e.g. `succeeded`.
2590 pub run_status: String,
2591 /// The structured proposal (`summary`, `steps`, `files`, `checks`, `risks`).
2592 pub proposal: Option<serde_json::Value>,
2593 /// Token usage as the provider reported it.
2594 pub usage: Option<serde_json::Value>,
2595}
2596
2597/// Result of `speccoder replay`.
2598#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2599pub struct ReplayReport {
2600 /// The replayed run.
2601 pub run_id: String,
2602 /// `reproduced`, `diverged` or `not_found`.
2603 pub result: String,
2604 /// What the replay said.
2605 pub detail: String,
2606}
2607
2608/// Result of `speccoder worktree`.
2609#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2610pub struct WorktreeReport {
2611 /// The feature the worktree is for.
2612 pub feature: String,
2613 /// Absolute worktree path.
2614 pub path: Option<String>,
2615 /// Spec Kit project directory inside the worktree.
2616 pub project_dir: Option<String>,
2617 /// `speccoder/<feature>`.
2618 pub branch: Option<String>,
2619 /// Commit the branch points at.
2620 pub base: Option<String>,
2621 /// Created now (`false`: reused).
2622 pub created: bool,
2623 /// Offending paths with reasons, when refused.
2624 pub refused_paths: Vec<RefusedPath>,
2625}
2626
2627/// A path that blocks worktree mode.
2628#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2629pub struct RefusedPath {
2630 /// Repository-relative path.
2631 pub path: String,
2632 /// `ignored`, `untracked` or `modified`.
2633 pub reason: String,
2634}
2635
2636/// The result of one command, rendered as human text or as one JSON object.
2637///
2638/// `exit_code` always equals `status.exit_code()`: the constructors set it and
2639/// the fields are private, so the two can never disagree.
2640#[derive(Debug, Clone, PartialEq)]
2641pub struct CommandResult {
2642 schema_version: u32,
2643 command: String,
2644 status: CommandStatus,
2645 message: String,
2646 exit_code: ExitCode,
2647 data: Option<CommandData>,
2648 next: Vec<crate::next::NextStep>,
2649 code: Option<&'static crate::codes::Entry>,
2650 fix: Vec<String>,
2651 commands: Vec<crate::step::Argvs>,
2652 remedy: Option<Box<Remedy>>,
2653}
2654
2655/// The fix speccoder knows and can apply itself: what it does, the exact
2656/// command it amounts to, how to take it back. An interface offers it as
2657/// one action (never applies it unasked); a script reads the command.
2658#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2659pub struct Remedy {
2660 /// The action, a verb first (`Commit Cargo.lock`).
2661 pub label: String,
2662 /// What it changes, in one line.
2663 pub effect: String,
2664 /// The same, as a command run in the checkout.
2665 pub command: String,
2666 /// How to take it back once applied.
2667 pub undo: Option<String>,
2668 /// It writes git history (a commit): an interface asks first.
2669 pub history: bool,
2670 /// How an interface applies it.
2671 pub run: crate::actions::Runnable,
2672}
2673
2674/// Envelope v2: `next`, `code`, `fix` and `docs` beside the typed `data`.
2675impl Serialize for CommandResult {
2676 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2677 use serde::ser::SerializeMap as _;
2678 let mut map = serializer.serialize_map(None)?;
2679 map.serialize_entry("schema_version", &self.schema_version)?;
2680 map.serialize_entry("command", &self.command)?;
2681 map.serialize_entry("status", &self.status)?;
2682 map.serialize_entry("exit_code", &self.exit_code)?;
2683 map.serialize_entry("message", &self.message)?;
2684 if let Some(code) = self.code() {
2685 let fix = self.fix();
2686 map.serialize_entry("code", code.name)?;
2687 map.serialize_entry("fix", &fix)?;
2688 map.serialize_entry(
2689 "fix_steps",
2690 &crate::step::steps_with(&fix, &self.commands, crate::step::Context::default()),
2691 )?;
2692 map.serialize_entry("docs", &code.docs())?;
2693 }
2694 if let Some(remedy) = &self.remedy {
2695 map.serialize_entry("remedy", remedy)?;
2696 }
2697 if !self.next.is_empty() {
2698 map.serialize_entry("next", &self.next)?;
2699 }
2700 if let Some(data) = &self.data {
2701 map.serialize_entry("payload", data.kind())?;
2702 map.serialize_entry("data", data)?;
2703 }
2704 map.end()
2705 }
2706}
2707
2708/// The envelope as it is serialised — the schema of [`CommandResult`]
2709/// (whose fields are private and whose serialisation is written by hand).
2710#[derive(schemars::JsonSchema)]
2711#[allow(dead_code)] // schema only
2712struct Envelope {
2713 /// The envelope's version.
2714 schema_version: u32,
2715 /// The command.
2716 command: String,
2717 /// The outcome class.
2718 status: CommandStatus,
2719 /// The process exit status (always the status's).
2720 exit_code: ExitCode,
2721 /// One line, for a person.
2722 message: String,
2723 /// The catalog code: present for every non-ok result and for a
2724 /// degraded success.
2725 #[serde(skip_serializing_if = "Option::is_none")]
2726 code: Option<String>,
2727 /// How to resolve it: commands, or `→ ` lines for a person.
2728 #[serde(skip_serializing_if = "Option::is_none")]
2729 fix: Option<Vec<String>>,
2730 /// The same lines, with how an interface takes each.
2731 #[serde(skip_serializing_if = "Option::is_none")]
2732 fix_steps: Option<Vec<crate::step::Step>>,
2733 /// The code's section on the codes page.
2734 #[serde(skip_serializing_if = "Option::is_none")]
2735 docs: Option<String>,
2736 /// The fix speccoder can apply itself, on the person's yes.
2737 #[serde(skip_serializing_if = "Option::is_none")]
2738 remedy: Option<Remedy>,
2739 /// The steps that continue.
2740 #[serde(skip_serializing_if = "Option::is_none")]
2741 next: Option<Vec<crate::next::NextStep>>,
2742 /// The name of `data`'s schema (`schemas/v2/<payload>.json`).
2743 #[serde(skip_serializing_if = "Option::is_none")]
2744 payload: Option<String>,
2745 /// The command's payload.
2746 #[serde(skip_serializing_if = "Option::is_none")]
2747 data: Option<CommandData>,
2748}
2749
2750impl schemars::JsonSchema for CommandResult {
2751 fn schema_name() -> std::borrow::Cow<'static, str> {
2752 "CommandResult".into()
2753 }
2754
2755 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
2756 Envelope::json_schema(generator)
2757 }
2758}
2759
2760impl CommandResult {
2761 /// A result with the given status.
2762 #[must_use]
2763 pub fn new(command: &str, status: CommandStatus, message: &str) -> Self {
2764 Self {
2765 schema_version: SCHEMA_VERSION,
2766 command: command.to_owned(),
2767 status,
2768 message: message.to_owned(),
2769 exit_code: status.exit_code(),
2770 data: None,
2771 next: Vec::new(),
2772 code: None,
2773 fix: Vec::new(),
2774 commands: Vec::new(),
2775 remedy: None,
2776 }
2777 }
2778
2779 /// `from`'s outcome reported as `command`'s: status, message and code,
2780 /// without `from`'s payload (a command passing on a failure it met).
2781 #[must_use]
2782 pub fn carried(command: &str, from: &Self) -> Self {
2783 let mut out = Self::new(command, from.status, &from.message);
2784 out.code = from.code;
2785 out.fix.clone_from(&from.fix);
2786 out.commands.clone_from(&from.commands);
2787 out.remedy.clone_from(&from.remedy);
2788 out
2789 }
2790
2791 /// The code, fixes and remedy of `from`, whose outcome this result
2792 /// reports with the same status (a feature's run stopped by a task's).
2793 #[must_use]
2794 pub fn caused_by(mut self, from: &Self) -> Self {
2795 if self.status == from.status {
2796 self.code = from.code;
2797 self.fix.clone_from(&from.fix);
2798 self.commands.clone_from(&from.commands);
2799 self.remedy.clone_from(&from.remedy);
2800 }
2801 self
2802 }
2803
2804 /// The same result with another status and message: payload and next
2805 /// steps kept, the code kept only while the status stays the same.
2806 #[must_use]
2807 pub fn reworded(mut self, status: CommandStatus, message: &str) -> Self {
2808 if status != self.status {
2809 self.code = None;
2810 self.fix.clear();
2811 self.commands.clear();
2812 self.remedy = None;
2813 }
2814 self.status = status;
2815 self.exit_code = status.exit_code();
2816 message.clone_into(&mut self.message);
2817 self
2818 }
2819
2820 /// Names the code (a catalog name); its fixes become the result's
2821 /// unless [`Self::with_fix`] gives better ones. An unknown name is
2822 /// ignored, and the status's default code applies later.
2823 #[must_use]
2824 pub fn with_code(mut self, name: &str) -> Self {
2825 if let Some(entry) = crate::codes::lookup(name) {
2826 self.code = Some(entry);
2827 }
2828 self
2829 }
2830
2831 /// The fix lines, replacing the catalog's.
2832 #[must_use]
2833 pub fn with_fix(mut self, fix: Vec<String>) -> Self {
2834 self.fix = fix;
2835 self
2836 }
2837
2838 /// The person's own commands among the fix lines, as the argvs the
2839 /// source built (each line [`crate::step::render`] of its argvs): run
2840 /// as these argvs, never re-read from the line.
2841 #[must_use]
2842 pub fn with_commands(mut self, commands: Vec<crate::step::Argvs>) -> Self {
2843 self.commands = commands;
2844 self
2845 }
2846
2847 /// A fix that is the person's own command, built as argvs: its line
2848 /// joins the fixes, the argvs are what runs.
2849 #[must_use]
2850 pub fn with_command(mut self, argvs: crate::step::Argvs) -> Self {
2851 if self.fix.is_empty() {
2852 self.fix = self.fix();
2853 }
2854 self.fix.push(crate::step::render(&argvs));
2855 self.commands.push(argvs);
2856 self
2857 }
2858
2859 /// Attaches the fix speccoder can apply itself.
2860 #[must_use]
2861 pub fn with_remedy(mut self, remedy: Option<Remedy>) -> Self {
2862 self.remedy = remedy.map(Box::new);
2863 self
2864 }
2865
2866 /// The fix speccoder can apply itself, if it knows one.
2867 #[must_use]
2868 pub fn remedy(&self) -> Option<&Remedy> {
2869 self.remedy.as_deref()
2870 }
2871
2872 /// The code: the one attached, else the status's default (none for a
2873 /// plain `ok`) — no non-ok result is without one.
2874 #[must_use]
2875 pub fn code(&self) -> Option<&'static crate::codes::Entry> {
2876 self.code.or_else(|| crate::codes::default_for(self.status))
2877 }
2878
2879 /// Whether a code was named explicitly (not the status's default).
2880 #[must_use]
2881 pub fn has_explicit_code(&self) -> bool {
2882 self.code.is_some()
2883 }
2884
2885 /// How to resolve it: the fixes given, else the code's own.
2886 #[must_use]
2887 pub fn fix(&self) -> Vec<String> {
2888 match self.code() {
2889 Some(_) if !self.fix.is_empty() => self.fix.clone(),
2890 Some(entry) => crate::codes::fixes(entry, &self.command, None, None),
2891 None => Vec::new(),
2892 }
2893 }
2894
2895 /// Attaches what to do next (`data.next`, the human `Next:` block).
2896 #[must_use]
2897 pub fn with_next(mut self, next: Vec<crate::next::NextStep>) -> Self {
2898 self.next = next;
2899 self
2900 }
2901
2902 /// What to do next.
2903 #[must_use]
2904 pub fn next(&self) -> &[crate::next::NextStep] {
2905 &self.next
2906 }
2907
2908 /// Attaches the command's payload.
2909 #[must_use]
2910 pub fn with_data(mut self, data: CommandData) -> Self {
2911 self.data = Some(data);
2912 self
2913 }
2914
2915 /// The command's payload, if any.
2916 #[must_use]
2917 pub fn data(&self) -> Option<&CommandData> {
2918 self.data.as_ref()
2919 }
2920
2921 /// A successful result.
2922 #[must_use]
2923 pub fn ok(command: &str, message: &str) -> Self {
2924 Self::new(command, CommandStatus::Ok, message)
2925 }
2926
2927 /// The subcommand name, e.g. `inspect`.
2928 #[must_use]
2929 pub fn command(&self) -> &str {
2930 &self.command
2931 }
2932
2933 /// The outcome class.
2934 #[must_use]
2935 pub fn status(&self) -> CommandStatus {
2936 self.status
2937 }
2938
2939 /// The single-line human-readable message.
2940 #[must_use]
2941 pub fn message(&self) -> &str {
2942 &self.message
2943 }
2944
2945 /// The process exit status.
2946 #[must_use]
2947 pub fn exit_code(&self) -> ExitCode {
2948 self.exit_code
2949 }
2950}
2951
2952#[cfg(test)]
2953mod tests {
2954 use super::*;
2955
2956 #[test]
2957 fn payload_names_are_unique() {
2958 let mut seen = std::collections::BTreeSet::new();
2959 for p in PAYLOADS {
2960 assert!(seen.insert(*p), "payload {p} is listed twice");
2961 }
2962 let branch = CommandData::BranchVerify(Box::new(crate::verify_branch::BranchReport {
2963 head: String::new(),
2964 base: None,
2965 mode: crate::verify_branch::Mode::All,
2966 features: Vec::new(),
2967 ticks: Vec::new(),
2968 skipped: Vec::new(),
2969 totals: crate::verify_branch::BranchTotals::default(),
2970 conclusion: crate::verify_branch::Conclusion::Neutral,
2971 trust: serde_json::Value::Null,
2972 accepted_unsigned: false,
2973 require_verified: false,
2974 trace: Vec::new(),
2975 share: None,
2976 open_no_claim: Vec::new(),
2977 not_evaluated: Vec::new(),
2978 newer_fields: Vec::new(),
2979 }));
2980 assert!(PAYLOADS.contains(&branch.kind()));
2981 assert_ne!(branch.kind(), "verify");
2982 assert_ne!(branch.kind(), "attest");
2983 }
2984
2985 #[test]
2986 fn every_status_maps_to_its_documented_exit_code() {
2987 assert_eq!(CommandStatus::Ok.exit_code().code(), 0);
2988 assert_eq!(CommandStatus::Error.exit_code().code(), 1);
2989 assert_eq!(CommandStatus::UsageError.exit_code().code(), 2);
2990 assert_eq!(CommandStatus::NotAProject.exit_code().code(), 10);
2991 assert_eq!(CommandStatus::DiagnosticsError.exit_code().code(), 11);
2992 assert_eq!(CommandStatus::SelectionNeeded.exit_code().code(), 12);
2993 assert_eq!(CommandStatus::ConfigError.exit_code().code(), 13);
2994 assert_eq!(CommandStatus::GitPrecondition.exit_code().code(), 14);
2995 assert_eq!(CommandStatus::VerificationFailed.exit_code().code(), 20);
2996 assert_eq!(CommandStatus::NotFound.exit_code().code(), 15);
2997 assert_eq!(CommandStatus::RunFailed.exit_code().code(), 21);
2998 assert_eq!(CommandStatus::ReplayDiverged.exit_code().code(), 22);
2999 assert_eq!(CommandStatus::TaskNotReady.exit_code().code(), 16);
3000 assert_eq!(CommandStatus::NeedsHuman.exit_code().code(), 23);
3001 assert_eq!(CommandStatus::EvalFailed.exit_code().code(), 24);
3002 assert_eq!(CommandStatus::NotReplayable.exit_code().code(), 25);
3003 assert_eq!(CommandStatus::AttestationFailed.exit_code().code(), 26);
3004 }
3005
3006 #[test]
3007 fn exit_code_follows_status() {
3008 let r = CommandResult::new("verify", CommandStatus::VerificationFailed, "x");
3009 assert_eq!(r.exit_code(), r.status().exit_code());
3010 assert_eq!(CommandResult::ok("x", "").exit_code(), ExitCode::OK);
3011 }
3012
3013 #[test]
3014 fn serializes_to_schema_version_2() {
3015 let r = CommandResult::new("replay", CommandStatus::NotReplayable, "not replayable")
3016 .with_code("NOT-REPLAYABLE")
3017 .with_fix(vec!["x".to_owned()]);
3018 let json = serde_json::to_value(&r).unwrap();
3019 assert_eq!(
3020 json,
3021 serde_json::json!({
3022 "schema_version": 2,
3023 "command": "replay",
3024 "status": "not_replayable",
3025 "exit_code": 25,
3026 "message": "not replayable",
3027 "code": "NOT-REPLAYABLE",
3028 "fix": ["x"],
3029 "fix_steps": [{ "command": "x", "how": "words", "why": null }],
3030 "docs": "https://hupe1980.github.io/speccoder/docs/codes/#not-replayable"
3031 })
3032 );
3033 let with_next = CommandResult::new("run", CommandStatus::NeedsHuman, "x")
3034 .with_next(crate::next::next(&crate::next::Outcome::Decided, None));
3035 let json = serde_json::to_value(&with_next).unwrap();
3036 assert_eq!(json["next"][0]["command"], "speccoder run");
3037 // How an interface takes it, beside it.
3038 assert_eq!(json["next"][0]["how"], "perform");
3039 assert_eq!(json["next"][0]["run"]["kind"], "run");
3040 assert!(json.get("data").is_none());
3041 }
3042}