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