Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ Times are RFC 3339; ages are nonnegative seconds. `model` and `effort` are null

Each top-level `commands` entry adds the computed `group` (`needs you`, `ready for review`, `landing`, `working`, `done`, `resolved` or `history`), nullable `needs_you_kind` (`silent`, `slow`, `died`, `failed`, `triage`, `finding`), `landing` (also true for a Needs you item referenced from Landing), and `count`. A collapsed failure has a `collapsed` array containing the other Commands, each with its own group and kind. These fields are observations, absent from durable records and `show`; child Runs remain within their Command. The durable `security_findings_waiting` flag defaults to false for older records.

`state` is `live`, `died`, `ended`, or `unknown` when historical evidence has no ending. `stage` is null before the first known stage. Readers skip unknown stage names. Known stages are `implement`, `Resume`, `Security review`, `pull request`, `CI watch`, `Repair`, `Self-merge`, `cleanup`, `Tickets`, `Spec review`, `Architecture review`, `Security audit`, `Security reproduction` and `Security fix publishing`. A Pass retains its own stages while its dispatched work records its stages separately.
`state` is `live`, `died`, `ended`, or `unknown` when historical evidence has no ending. `stage` is null before the first known stage. Readers skip unknown stage names. Known stages are `implement`, `Resume`, `Security review`, `pull request`, `CI watch`, `Repair`, `Self-merge`, `cleanup`, `Tickets`, `Spec review`, `Architecture review`, `Security audit`, `Security reproduction`, `Security fix publishing`, `dispatched Run` and `dispatched Spec run`. A Pass retains its own stages while its dispatched work records its stages separately. A Pickup run or Security run enters `dispatched Run` or `dispatched Spec run` when it dispatches, so its row reads, for example, `dispatched Spec run · 19 hr 46 min`, timed from the dispatch, and `died at dispatched Spec run` if its process goes. A Pickup run's `issue.title` is the one its Ready issue was listed with; older records have null.

`children` recursively contains the immediate child Runs in separate processes, with the same fields as their parent. A Pickup's `dispatched` contains its Run or Spec run. A Spec's `tickets` is its complete roster in issue-number order; each entry has `issue` (`number`, `url`, nullable `title`), `state` (`running`, `merged`, `failed`, `waiting`, or `not started`) and `blockers` (the named open blocking issues, with the same issue fields). Match a Ticket's `issue.url` to the Run in that Spec's `children` for its stage, age, liveness and Base fix descendants. Tickets without a Run remain in the roster. Completed child Runs stay in the tree while its Command is listed. A killed child's `state: "died"` remains independent of the Spec's Ticket accounting. `parent_record` links each child to its parent's exact file; `parent_dispatch`, when present, names the dispatched issue's URL within that file. A top-level Command has null for both. Records from before these fields existed read as having no children or Tickets.

Expand Down Expand Up @@ -188,7 +188,7 @@ thirdshift logs 'widgets#7' --session repair --follow

`show` includes the Runs, stage history with start times and Usual ranges, outcomes, and Command and Session log paths. Each live Run also gets its latest event, such as `Bash: cargo test`, from the last 64 KiB of its current Session log. It uses the Harness's stream interpretation, including Claude and Codex, and ignores malformed or unknown lines. Missing or unreadable Session logs leave the event unavailable.

`status` and `show` describe live work as **Silent** when neither its Session log nor its Command log has grown for `inbox.silent_minutes` (10 by default). Silence starts no earlier than the current stage. At three times the threshold, the words escalate: `silent 45 min · past 30`. A fresh line in either log keeps the Run from being Silent. Missing logs use the stage's start time.
`status` and `show` describe live work as **Silent** when neither its Session log nor its Command log has grown for `inbox.silent_minutes` (10 by default). Silence starts no earlier than the current stage. A dispatch stage is never Silent or Slow and has no Usual range: the dispatched Run or Spec run carries its own. At three times the threshold, the words escalate: `silent 45 min · past 30`. A fresh line in either log keeps the Run from being Silent. Missing logs use the stage's start time.

A stage's **Usual range** is its 25th–75th percentile time over the repository's last 30 Command records on this machine. Completed stages supply times, with the next stage's start or the Command's end marking completion; an unfinished stage supplies none. The range appears once there are at least three stage times. Quartiles interpolate between sorted times. A live Run past the top of the range is **Slow**: `slower than usual (12–17 min), still working`. At twice the top it also shows the multiple, such as `2× usual` or `3× usual`. Silent takes precedence over Slow.

Expand Down
4 changes: 2 additions & 2 deletions site/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@
</code></pre>
<p>Times are RFC 3339; ages are nonnegative seconds. <code>model</code> and <code>effort</code> are null when left to the Harness. <code>issue</code> is null for a Pass with no issue; <code>session_log</code>, <code>issue_branch</code>, <code>pull_request</code>, <code>outcome</code>, <code>cause</code>, <code>ended_at</code>, <code>parent_record</code> and <code>parent_dispatch</code> are nullable. Command kinds are <code>Run</code>, <code>Spec run</code>, <code>Architect run</code>, <code>Pickup run</code>, <code>Security run</code> and <code>Base fix</code>. <code>name</code> identifies an issue or a Pass&#39;s kind and stamp. <code>dispatched</code> is an array of the Runs or Spec runs a Pass dispatched, with the same fields as its parent (and an empty <code>dispatched</code> array); it shares the Pass&#39;s process identity and Command log. It retains its own outcome, end time, cause, Issue branch and pull request. The retained Command record has the same fields, including dispatched work, without the computed <code>state</code>, <code>stage</code>, <code>source</code>, <code>command_log</code> and <code>children</code> or empty leaf <code>dispatched</code> arrays. Older records without an end time use their last modification time. <code>idle_passes</code> contains one entry per repository and kind whose latest Activity log line is a skip; it is independent of <code>commands</code>, and never creates a Command record or log for a skipped Pass.</p>
<p>Each top-level <code>commands</code> entry adds the computed <code>group</code> (<code>needs you</code>, <code>ready for review</code>, <code>landing</code>, <code>working</code>, <code>done</code>, <code>resolved</code> or <code>history</code>), nullable <code>needs_you_kind</code> (<code>silent</code>, <code>slow</code>, <code>died</code>, <code>failed</code>, <code>triage</code>, <code>finding</code>), <code>landing</code> (also true for a Needs you item referenced from Landing), and <code>count</code>. A collapsed failure has a <code>collapsed</code> array containing the other Commands, each with its own group and kind. These fields are observations, absent from durable records and <code>show</code>; child Runs remain within their Command. The durable <code>security_findings_waiting</code> flag defaults to false for older records.</p>
<p><code>state</code> is <code>live</code>, <code>died</code>, <code>ended</code>, or <code>unknown</code> when historical evidence has no ending. <code>stage</code> is null before the first known stage. Readers skip unknown stage names. Known stages are <code>implement</code>, <code>Resume</code>, <code>Security review</code>, <code>pull request</code>, <code>CI watch</code>, <code>Repair</code>, <code>Self-merge</code>, <code>cleanup</code>, <code>Tickets</code>, <code>Spec review</code>, <code>Architecture review</code>, <code>Security audit</code>, <code>Security reproduction</code> and <code>Security fix publishing</code>. A Pass retains its own stages while its dispatched work records its stages separately.</p>
<p><code>state</code> is <code>live</code>, <code>died</code>, <code>ended</code>, or <code>unknown</code> when historical evidence has no ending. <code>stage</code> is null before the first known stage. Readers skip unknown stage names. Known stages are <code>implement</code>, <code>Resume</code>, <code>Security review</code>, <code>pull request</code>, <code>CI watch</code>, <code>Repair</code>, <code>Self-merge</code>, <code>cleanup</code>, <code>Tickets</code>, <code>Spec review</code>, <code>Architecture review</code>, <code>Security audit</code>, <code>Security reproduction</code>, <code>Security fix publishing</code>, <code>dispatched Run</code> and <code>dispatched Spec run</code>. A Pass retains its own stages while its dispatched work records its stages separately. A Pickup run or Security run enters <code>dispatched Run</code> or <code>dispatched Spec run</code> when it dispatches, so its row reads, for example, <code>dispatched Spec run · 19 hr 46 min</code>, timed from the dispatch, and <code>died at dispatched Spec run</code> if its process goes. A Pickup run&#39;s <code>issue.title</code> is the one its Ready issue was listed with; older records have null.</p>
<p><code>children</code> recursively contains the immediate child Runs in separate processes, with the same fields as their parent. A Pickup&#39;s <code>dispatched</code> contains its Run or Spec run. A Spec&#39;s <code>tickets</code> is its complete roster in issue-number order; each entry has <code>issue</code> (<code>number</code>, <code>url</code>, nullable <code>title</code>), <code>state</code> (<code>running</code>, <code>merged</code>, <code>failed</code>, <code>waiting</code>, or <code>not started</code>) and <code>blockers</code> (the named open blocking issues, with the same issue fields). Match a Ticket&#39;s <code>issue.url</code> to the Run in that Spec&#39;s <code>children</code> for its stage, age, liveness and Base fix descendants. Tickets without a Run remain in the roster. Completed child Runs stay in the tree while its Command is listed. A killed child&#39;s <code>state: &quot;died&quot;</code> remains independent of the Spec&#39;s Ticket accounting. <code>parent_record</code> links each child to its parent&#39;s exact file; <code>parent_dispatch</code>, when present, names the dispatched issue&#39;s URL within that file. A top-level Command has null for both. Records from before these fields existed read as having no children or Tickets.</p>
<p>For example, this tree excerpt omits the common Run fields shown above:</p>
<pre><code class="language-json">{
Expand Down Expand Up @@ -224,7 +224,7 @@
thirdshift logs &#39;widgets#7&#39; --session repair --follow
</code></pre>
<p><code>show</code> includes the Runs, stage history with start times and Usual ranges, outcomes, and Command and Session log paths. Each live Run also gets its latest event, such as <code>Bash: cargo test</code>, from the last 64 KiB of its current Session log. It uses the Harness&#39;s stream interpretation, including Claude and Codex, and ignores malformed or unknown lines. Missing or unreadable Session logs leave the event unavailable.</p>
<p><code>status</code> and <code>show</code> describe live work as <strong>Silent</strong> when neither its Session log nor its Command log has grown for <code>inbox.silent_minutes</code> (10 by default). Silence starts no earlier than the current stage. At three times the threshold, the words escalate: <code>silent 45 min · past 30</code>. A fresh line in either log keeps the Run from being Silent. Missing logs use the stage&#39;s start time.</p>
<p><code>status</code> and <code>show</code> describe live work as <strong>Silent</strong> when neither its Session log nor its Command log has grown for <code>inbox.silent_minutes</code> (10 by default). Silence starts no earlier than the current stage. A dispatch stage is never Silent or Slow and has no Usual range: the dispatched Run or Spec run carries its own. At three times the threshold, the words escalate: <code>silent 45 min · past 30</code>. A fresh line in either log keeps the Run from being Silent. Missing logs use the stage&#39;s start time.</p>
<p>A stage&#39;s <strong>Usual range</strong> is its 25th–75th percentile time over the repository&#39;s last 30 Command records on this machine. Completed stages supply times, with the next stage&#39;s start or the Command&#39;s end marking completion; an unfinished stage supplies none. The range appears once there are at least three stage times. Quartiles interpolate between sorted times. A live Run past the top of the range is <strong>Slow</strong>: <code>slower than usual (12–17 min), still working</code>. At twice the top it also shows the multiple, such as <code>2× usual</code> or <code>3× usual</code>. Silent takes precedence over Slow.</p>
<p>Both JSON views add <code>health</code> to recorded Commands: <code>{&quot;state&quot;:&quot;ok&quot;}</code>, <code>{&quot;state&quot;:&quot;slow&quot;,&quot;step&quot;:1,&quot;usual_range&quot;:{&quot;lower_seconds&quot;:720.0,&quot;upper_seconds&quot;:1020.0},&quot;multiple&quot;:1}</code>, or <code>{&quot;state&quot;:&quot;silent&quot;,&quot;step&quot;:2,&quot;seconds&quot;:2700,&quot;threshold_seconds&quot;:600}</code>. Step 1 is the first threshold; step 2 is the escalation. Ended or dead Commands have null health. The <code>usual_ranges</code> map gives each stage&#39;s range by name, for example <code>{&quot;implement&quot;:{&quot;lower_seconds&quot;:720.0,&quot;upper_seconds&quot;:1020.0}}</code>; a stage is omitted until enough times exist. Health and ranges are reader observations, never written back to Command records.</p>
<p><code>show --json</code> is a versioned API, usable outside a repository. Version 1 wraps the selected Command in this shape:</p>
Expand Down
28 changes: 15 additions & 13 deletions src/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex, MutexGuard, PoisonError};

use crate::config::UserConfig;
use crate::github::ListedIssue;
use crate::harness::Choice;
use crate::issue::{IssueUrl, Repo};
use crate::session::Logged;
Expand Down Expand Up @@ -91,8 +92,8 @@ pub enum Work<'a> {
BaseFix(&'a IssueUrl),
/// A Spec run on its Spec.
SpecRun(&'a IssueUrl),
/// A Pickup run, on the Ready issue it took.
PickupRun(&'a IssueUrl),
/// A Pickup run, on the Ready issue it took, as the listing named it.
PickupRun(&'a ListedIssue),
/// An Architect run, on its repository.
ArchitectRun(&'a Repo),
/// A Security run, on its repository.
Expand Down Expand Up @@ -301,7 +302,7 @@ fn root_under(logs_dir: &Path, repo: &Repo) -> PathBuf {
logs_dir.join(&repo.owner).join(&repo.name)
}

impl Work<'_> {
impl<'a> Work<'a> {
fn initial_stage(self) -> Option<Stage> {
match self {
Work::Run(_) | Work::BaseFix(_) => Some(Stage::Implement),
Expand All @@ -314,10 +315,8 @@ impl Work<'_> {
/// The repository whose work it records.
fn repo(self) -> Repo {
match self {
Work::Run(issue)
| Work::BaseFix(issue)
| Work::SpecRun(issue)
| Work::PickupRun(issue) => issue.repo(),
Work::Run(issue) | Work::BaseFix(issue) | Work::SpecRun(issue) => issue.repo(),
Work::PickupRun(listed) => listed.issue.repo(),
Work::ArchitectRun(repo) | Work::SecurityRun(repo) => repo.clone(),
}
}
Expand All @@ -333,16 +332,19 @@ impl Work<'_> {
}
}

/// Its issue's number, if it has one.
fn issue(self) -> Option<u64> {
/// Its issue, if it has one.
pub(crate) fn issue_url(self) -> Option<&'a IssueUrl> {
match self {
Work::Run(issue)
| Work::BaseFix(issue)
| Work::SpecRun(issue)
| Work::PickupRun(issue) => Some(issue.number),
Work::Run(issue) | Work::BaseFix(issue) | Work::SpecRun(issue) => Some(issue),
Work::PickupRun(listed) => Some(&listed.issue),
Work::ArchitectRun(_) | Work::SecurityRun(_) => None,
}
}

/// Its issue's number, if it has one.
fn issue(self) -> Option<u64> {
self.issue_url().map(|issue| issue.number)
}
}

/// The [`Record`], locked, even if a thread panicked holding it.
Expand Down
49 changes: 38 additions & 11 deletions src/logs/command_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ pub enum StageName {
SecurityAudit,
SecurityReproduction,
SecurityFixPublishing,
/// A Pickup run or Security run waiting on the Run it dispatched.
DispatchedRun,
/// A Pickup run or Security run waiting on the Spec run it dispatched.
DispatchedSpecRun,
}

impl StageName {
Expand All @@ -156,9 +160,32 @@ impl StageName {
Self::SecurityAudit => "Security audit",
Self::SecurityReproduction => "Security reproduction",
Self::SecurityFixPublishing => "Security fix publishing",
Self::DispatchedRun => "dispatched Run",
Self::DispatchedSpecRun => "dispatched Spec run",
}
}

/// The stage a pass enters while `work`, the Command it dispatched, runs.
pub(super) fn dispatched(work: Work<'_>) -> Self {
match work {
Work::SpecRun(_) => Self::DispatchedSpecRun,
Work::Run(_)
| Work::BaseFix(_)
| Work::PickupRun(_)
| Work::ArchitectRun(_)
| Work::SecurityRun(_) => Self::DispatchedRun,
}
}

/// Whether the stage named `name` is a pass's dispatch stage. Such a
/// stage has no Usual range and is never Silent: the dispatched
/// Command's own stages carry its health.
pub(super) fn is_dispatch(name: &str) -> bool {
[Self::DispatchedRun, Self::DispatchedSpecRun]
.into_iter()
.any(|stage| stage.name() == name)
}

pub(super) fn known(name: &str) -> bool {
[
Self::Implement,
Expand All @@ -175,6 +202,8 @@ impl StageName {
Self::SecurityAudit,
Self::SecurityReproduction,
Self::SecurityFixPublishing,
Self::DispatchedRun,
Self::DispatchedSpecRun,
]
.into_iter()
.any(|stage| stage.name() == name)
Expand Down Expand Up @@ -235,17 +264,15 @@ impl CommandRecord {
}

pub fn new(work: Work<'_>, stamp: &str, harness: &Choice, now: DateTime<FixedOffset>) -> Self {
let issue = match work {
Work::Run(issue)
| Work::BaseFix(issue)
| Work::SpecRun(issue)
| Work::PickupRun(issue) => Some(Issue {
number: issue.number,
url: issue.url.clone(),
title: None,
}),
Work::ArchitectRun(_) | Work::SecurityRun(_) => None,
};
let issue = work.issue_url().map(|issue| Issue {
number: issue.number,
url: issue.url.clone(),
// The Ready issue listing named a Pickup run's issue before it started.
title: match work {
Work::PickupRun(listed) => Some(listed.title.clone()),
_ => None,
},
});
let name = match work {
Work::ArchitectRun(_) => format!("architect-{stamp}"),
Work::SecurityRun(_) => format!("secure-{stamp}"),
Expand Down
2 changes: 1 addition & 1 deletion src/logs/owner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ pub(super) fn allocate<F: Files>(
Work::Run(issue) | Work::BaseFix(issue) | Work::SpecRun(issue) => {
(CommandKind::Issue, format!("{}-", issue.number))
}
Work::PickupRun(issue) => (CommandKind::Pickup, format!("{}-", issue.number)),
Work::PickupRun(listed) => (CommandKind::Pickup, format!("{}-", listed.issue.number)),
Work::ArchitectRun(_) => (CommandKind::Architect, String::new()),
Work::SecurityRun(_) => (CommandKind::Secure, String::new()),
};
Expand Down
Loading
Loading