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
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ The designed behaviour described in this README is implemented. Known gaps and p

## Machine status

`thirdshift status` names this host and lists live Commands across every repository in `logs.dir`, with their issue and title when present, Harness, Model, Effort, current stage and its age. Architect runs, Pickup runs and Security runs nest the Run or Spec run they dispatch beneath themselves. A Pass with no issue is named by its kind and Command log stamp, such as `architect-20261018T120000-0400` or `secure-20261018T120000-0400`. A Command with no outcome whose process has gone shows as `died at <stage>`. Process start identifiers distinguish a reused process id on Linux and macOS. Completed Commands leave the live list; their Command records remain beside their Command logs, as `*.command.json`. A record write failure warns once per Command and does not change its outcome.
`thirdshift status` names this host and lists live Commands across every repository in `logs.dir`, with their issue and title when present, Harness, Model, Effort, current stage and its age. Architect runs, Pickup runs and Security runs nest the Run or Spec run they dispatch beneath themselves. A Pass with no issue is named by its kind and Command log stamp, such as `architect-20261018T120000-0400` or `secure-20261018T120000-0400`. After the live Commands it lists those that ended in the last 24 hours, with their outcomes. `thirdshift status --all` lists every Command this machine has logs for, newest start first. A Command record with no outcome whose process has gone shows as `died at <stage>` in either view. Process start identifiers distinguish a reused process id on Linux and macOS. Command records remain beside their Command logs, as `*.command.json`. A record write failure warns once per Command and does not change its outcome.

History uses each source only for Commands missing from the preceding sources: Command records, then Activity log start/end lines with their Command logs, then Session logs in the current `<logs.dir>/<owner>/<repo>/commands/<kind>/` layout. Activity entries have start and end times and outcomes, with no stages. Session entries group logs by issue and Command stamp; they have no known end or outcome and appear only with `--all`. Sessions covered by a Spec run or Pass's stamp belong to that Command. The flat layout from before [ADR 0011](docs/adr/0011-thirdshift-keeps-a-per-repository-activity-log.md) is ignored.

For each repository and kind of Pass whose latest Activity log line is a skip, status also shows that Pass as idle with its reason, even when the skip is older than another kind's activity. A later start or end for that kind clears its idle entry. For example:

Expand All @@ -98,12 +100,15 @@ Architect run idle on acme/widgets: Ready issue #7 "Make widgets" goes first: ht
"model": "opus",
"effort": "high",
"started_at": "2026-10-18T12:00:00-04:00",
"ended_at": null,
"stages": [{"name": "implement", "began_at": "2026-10-18T12:00:01-04:00", "session_log": "/logs/acme/widgets/commands/issue/7-stamp-implement.jsonl"}],
"session_log": "/logs/acme/widgets/commands/issue/7-stamp-implement.jsonl",
"issue_branch": "issue-7",
"pull_request": null,
"outcome": null,
"cause": null,
"source": "command_record",
"command_log": "/logs/acme/widgets/commands/issue/7-stamp.log",
"dispatched": [],
"state": "live",
"stage": {"name": "implement", "began_at": "2026-10-18T12:00:01-04:00", "age_seconds": 52}
Expand All @@ -116,9 +121,11 @@ Architect run idle on acme/widgets: Ready issue #7 "Make widgets" goes first: ht
}
```

Times are RFC 3339; ages are nonnegative seconds. `model` and `effort` are null when left to the Harness. `issue` is null for a Pass with no issue; `session_log`, `issue_branch`, `pull_request`, `outcome` and `cause` are nullable. Command kinds are `Run`, `Spec run`, `Architect run`, `Pickup run` and `Security run`. `name` identifies an issue or a Pass's kind and stamp. `dispatched` is an array of the Run or Spec run a Pass dispatched, with the same fields as its parent (and an empty `dispatched` array); it shares the Pass's process identity. Its `state` may also be `ended`, and it retains its own outcome, cause, Issue branch and pull request. The retained Command record has the same fields, including dispatched work, without the computed `state` and `stage` or empty leaf `dispatched` arrays. `idle_passes` contains one entry per repository and kind whose latest Activity log line is a skip; it is independent of `commands`, and never creates a Command record or log for a skipped Pass.
Times are RFC 3339; ages are nonnegative seconds. `model` and `effort` are null when left to the Harness. `issue` is null for a Pass with no issue; `session_log`, `issue_branch`, `pull_request`, `outcome`, `cause` and `ended_at` are nullable. Command kinds are `Run`, `Spec run`, `Architect run`, `Pickup run` and `Security run`. `name` identifies an issue or a Pass's kind and stamp. `dispatched` is an array of the Run or Spec run a Pass dispatched, with the same fields as its parent (and an empty `dispatched` array); it shares the Pass'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 `state`, `stage`, `source` and `command_log` or empty leaf `dispatched` arrays. Older records without an end time use their last modification time. `idle_passes` contains one entry per repository and kind whose latest Activity log line is a skip; it is independent of `commands`, and never creates a Command record or log for a skipped Pass.

`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. The remaining Command views are tracked in [#711](https://github.com/JacobStephens2/thirdshift/issues/711).

`state` is `live` or `died` for an unfinished Command; `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. History and the remaining Command views are tracked in [#711](https://github.com/JacobStephens2/thirdshift/issues/711).
Every entry has `source`: `command_record`, `activity_log` or `session_log`. The latter two carry `repository`, `kind`, nullable `issue` (number, URL, nullable title), `harness`, `model`, `effort`, `started_at`, nullable `ended_at` and `outcome`, nullable `command_log`, `session_logs` (paths), `state`, `stages: []` and `stage: null`. They omit record-only fields such as process identity and branch information. Activity times use the UTC offset in the Command log's stamp; their outcome is the Activity log's full ending text. A Session entry has null Harness, Model, Effort, end time and outcome, and lists its grouped Session logs. Both flags can be combined as `status --all --json`.

## Install

Expand Down
2 changes: 1 addition & 1 deletion docs/code-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Start with the issue's terms in `CONTEXT.md`, then use this index to find their
| Session prompt, Factory skills | `src/prompt.rs`; `src/skills.rs`; `skills/` | `src/skills/tests.rs`, `tests/packaging.rs` |
| Session log, Resume | `src/session.rs` and `src/session/` | `src/session/execution_tests.rs`, `tests/background_work.rs` |
| Command log, Activity log | `src/logs.rs`; `src/logs/recording.rs` — `Record` | `src/logs/recording/tests.rs`, `tests/command_logs.rs`, `tests/activity_log.rs` |
| Command record, status | `src/logs/recording.rs`; `src/logs/command_record.rs`; `src/logs/status.rs`; `src/logs/process_identity.rs` | `tests/status.rs`, `src/logs/recording/tests.rs` |
| Command record, status | `src/logs/recording.rs`; `src/logs/command_record.rs`; `src/logs/status.rs` and `src/logs/status/history.rs`; `src/logs/process_identity.rs` | `tests/status.rs`, `src/logs/recording/tests.rs` |
| User config, Setup | `src/config.rs`; `src/setup.rs` | `tests/user_config.rs`, `tests/setup.rs` |
| Architecture review, Architect run | `src/architect.rs` | `tests/architect_run.rs` |
| Security audit, Security review, Security run | `src/security.rs` and `src/security/` | `tests/security_review.rs`, `tests/security_run.rs` |
Expand Down
23 changes: 20 additions & 3 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub enum Command {
Setup,
Status {
json: bool,
all: bool,
},
/// Internal interface for scripts/release.sh: prompt on stdin.
ReleaseSummary,
Expand Down Expand Up @@ -224,9 +225,25 @@ pub fn parse(args: &[String]) -> Result<Command> {
Some("update") => return Ok(Command::Update),
Some("status") => {
return match &args[1..] {
[] => Ok(Command::Status { json: false }),
[flag] if flag == "--json" => Ok(Command::Status { json: true }),
_ => bail!("usage: thirdshift status [--json]"),
[] => Ok(Command::Status {
json: false,
all: false,
}),
[flag] if flag == "--json" => Ok(Command::Status {
json: true,
all: false,
}),
[flag] if flag == "--all" => Ok(Command::Status {
json: false,
all: true,
}),
[a, b] if (a == "--all" && b == "--json") || (a == "--json" && b == "--all") => {
Ok(Command::Status {
json: true,
all: true,
})
}
_ => bail!("usage: thirdshift status [--all] [--json]"),
};
}
Some("--release-summary") => {
Expand Down
6 changes: 5 additions & 1 deletion src/logs/command_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub(super) struct CommandRecord {
pub model: Option<String>,
pub effort: Option<String>,
pub started_at: String,
#[serde(default)]
pub ended_at: Option<String>,
pub stages: Vec<Stage>,
pub session_log: Option<PathBuf>,
pub outcome: Option<String>,
Expand Down Expand Up @@ -161,6 +163,7 @@ impl CommandRecord {
model: harness.model.clone(),
effort: harness.effort.clone(),
started_at: now.to_rfc3339(),
ended_at: None,
stages: Vec::new(),
session_log: None,
outcome: None,
Expand All @@ -178,7 +181,8 @@ impl CommandRecord {
.find(|stage| StageName::known(&stage.name))
}

pub fn finish(&mut self, account: &crate::run_ending::Account<'_>) {
pub fn finish(&mut self, account: &crate::run_ending::Account<'_>, now: DateTime<FixedOffset>) {
self.ended_at = Some(now.to_rfc3339());
self.outcome = Some(account.outcome.into());
self.cause = account
.ended
Expand Down
4 changes: 2 additions & 2 deletions src/logs/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ impl<O: Outside> Record<O> {

pub(super) fn finished(&mut self, account: &crate::run_ending::Account<'_>) {
if let Some((_, tree)) = &mut self.command_record {
tree.command.finish(account);
tree.command.finish(account, self.outside.now());
self.write_record();
}
}
Expand All @@ -258,7 +258,7 @@ impl<O: Outside> Record<O> {
if let Some((_, tree)) = &mut self.command_record
&& let Some(dispatched) = tree.dispatched.last_mut()
{
dispatched.finish(account);
dispatched.finish(account, self.outside.now());
self.write_record();
}
}
Expand Down
Loading
Loading