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