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