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
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ One invocation of thirdshift that does factory work: a **Run** or **Spec run** s
_Avoid_: invocation, job

**Command log**:
Everything one **Run**, **Spec run**, **Architect run**, **Pickup run** or **Security run** printed, kept in a file of its own with its repository's other logs, stamped like its **Session logs**. An Architect run's, Pickup run's or Security run's covers the Spec runs or Runs it dispatched, and a Spec run's covers its **Tickets**' Runs, as a Run's covers its **Base fix**: none of those keeps one of its own. It is kept once the command starts work: a Pickup run, Architect run or Security run skipped before doing any work keeps none, and neither does a Run that fails before it starts work, such as on **Origin match**.
Everything one **Run**, **Spec run**, **Architect run**, **Pickup run** or **Security run** printed, kept in a file of its own with its repository's other logs, stamped like its **Session logs**. Commands with the same start time still keep separate Command logs and **Command records**. An Architect run's, Pickup run's or Security run's covers the Spec runs or Runs it dispatched, and a Spec run's covers its **Tickets**' Runs, as a Run's covers its **Base fix**: none of those keeps one of its own. It is kept once the command starts work: a Pickup run, Architect run or Security run skipped before doing any work keeps none, and neither does a Run that fails before it starts work, such as on **Origin match**.
_Avoid_: cron log, output log, run log (a **Run** is one kind of command)

**Activity log**:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ This replaces ADR 0009's consequence that a skip leaves only its line in the sch

## Consequences

- The repository in the path is the GitHub repository, `<owner>/<repo>`, not the name of the local checkout, so log file names drop it: `commands/pickup/41-<stamp>.log`, `sessions/41-<stamp>-implement.jsonl`.
- The repository in the path is the GitHub repository, `<owner>/<repo>`, not the name of the local checkout, so log file names drop it: `commands/pickup/41-<stamp>~<token>.log`. Command logs are exclusively allocated with an immutable random owner token, so Commands sharing a start stamp retain separate output and records. Session writers temporarily retain `commands/pickup/41-<stamp>-implement.jsonl` until the Session-ownership Ticket; readers accept both tokenless and token-bearing Session names. Existing legacy Command logs and records remain readable.
- Whether the scheduler is still firing is no longer a line per pass: an idle repository's Activity log ends on one skip line.
- Logs written before this, under `<logs.dir>/sessions/` and `<logs.dir>/commands/`, are not moved by thirdshift.
1 change: 1 addition & 0 deletions docs/code-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +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-log allocation, artifact ownership and filename decoding | `src/logs/owner.rs` — `allocate`, `Owner`, `decode`, `session_matches` | `src/logs/owner/tests.rs`, `src/logs/recording/tests.rs`, `tests/status.rs`, `tests/show_logs.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` |
| Command naming, show, logs | `src/logs/status/inspect.rs` — Command resolution, detailed state, Session selection and log following | `tests/show_logs.rs` |
| Stop | `src/logs/stop.rs` — request, signaling and waiting; `src/claim.rs` — Stop endings | `tests/stop.rs`; inline Claim and Stop tests |
Expand Down
14 changes: 8 additions & 6 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -624,10 +624,10 @@ Everything thirdshift logs goes under `~/.thirdshift/logs/`, or the `logs.dir` s
├── audits/ each Security audit's report, with its evidence
├── security-reviews/ each Security review's private report of old findings
└── commands/
├── architect/ <stamp>.log and Session logs
├── pickup/ <n>-<stamp>.log and Session logs
├── secure/ <stamp>.log and Session logs
└── issue/ <n>-<stamp>.log and Session logs
├── architect/ <stamp>~<token>.log and Session logs
├── pickup/ <n>-<stamp>~<token>.log and Session logs
├── secure/ <stamp>~<token>.log and Session logs
└── issue/ <n>-<stamp>~<token>.log and Session logs
```

The folder already names the repository, so no file name repeats it. `audits/` and `security-reviews/` hold vulnerability evidence, so keep the logs private (see [Security runs](#security-runs)). A **Session log** is one session's full transcript, as the Harness's JSON output, such as Claude Code's `stream-json`:
Expand All @@ -646,11 +646,13 @@ A **Command log** is everything one command printed, stderr and stdout in the or

The stamp is the local time the command started, with its UTC offset, as in `20261003T120000-0400`, in the machine's time zone, or `TZ`'s if set, so it agrees with `date` and `ls -l`. A command's Command log and all of its Session logs, its Tickets' Runs' and its Base fix's included, share that one stamp, so they sort together and each can be found from the other. When a Run fails, stderr ends with the path of its most recent Session log, the place to start looking, then that of its Command log.

New Command logs insert `~<token>` immediately after the stamp. The random token gives each Command its own storage even when start times agree; its Command record uses that exact stem followed by `.command.json`. Human Command names stay the same. Session writers still use the tokenless examples above during this transition; readers also accept Session names with `~<token>`. A matching stamp alone does not attach a tokenless Session to a new Command: a record reference or exact Model evidence must establish that ownership. Legacy logs and records remain readable without renaming.

The **Activity log**, `activity.log`, is a short running record of what the factory did on the repository ([ADR-0011](adr/0011-thirdshift-keeps-a-per-repository-activity-log.md)). A Run, a Spec run or a Pass writes a line when it starts work, naming its Command log, and one when it ends, with its outcome; a Run you type by hand writes them too, and the run a Pass dispatched, a Spec run's Tickets and a Base fix write none of their own. A skipped Pass writes a line only when its reason differs from the last line of its own kind, so a repository that sits idle shows one line, not one per pass, and the first skip after a pass that did work always shows: that is when the repository went idle. Every line starts with the local date and time:

```
2026-10-03 02:00:01 Pickup run skipped: no Ready issue on acme/widgets
2026-10-03 09:30:01 Pickup run #41 started: commands/pickup/41-20261003T093001-0400.log
2026-10-03 09:30:01 Pickup run #41 started: commands/pickup/41-20261003T093001-0400~aB3cD4eF5gH6iJ7k.log
2026-10-03 09:52:17 Pickup run #41 ended: PR https://github.com/acme/widgets/pull/42 is merged
2026-10-03 10:00:01 Pickup run skipped: no Ready issue on acme/widgets
```
Expand Down Expand Up @@ -1086,7 +1088,7 @@ thirdshift: 03:00:01 Pickup run starting, 2026-10-03 -0400
thirdshift: 03:00:02 #18 labelled needs-info
thirdshift: 03:00:03 #20 blocked by #17
thirdshift: 03:00:04 taking Ready issue #22 "Sharpen the widgets", as thirdshift https://github.com/acme/widgets/issues/22 would
thirdshift: 03:00:04 logging this command to ~/.thirdshift/logs/acme/widgets/commands/pickup/22-20261003T030001-0400.log
thirdshift: 03:00:04 logging this command to ~/.thirdshift/logs/acme/widgets/commands/pickup/22-20261003T030001-0400~aB3cD4eF5gH6iJ7k.log
```

### One Run notification for the issue taken
Expand Down
13 changes: 7 additions & 6 deletions site/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -522,10 +522,10 @@ <h3 id="logs">Logs</h3>
├── audits/ each Security audit&#39;s report, with its evidence
├── security-reviews/ each Security review&#39;s private report of old findings
└── commands/
├── architect/ &lt;stamp&gt;.log and Session logs
├── pickup/ &lt;n&gt;-&lt;stamp&gt;.log and Session logs
├── secure/ &lt;stamp&gt;.log and Session logs
└── issue/ &lt;n&gt;-&lt;stamp&gt;.log and Session logs
├── architect/ &lt;stamp&gt;~&lt;token&gt;.log and Session logs
├── pickup/ &lt;n&gt;-&lt;stamp&gt;~&lt;token&gt;.log and Session logs
├── secure/ &lt;stamp&gt;~&lt;token&gt;.log and Session logs
└── issue/ &lt;n&gt;-&lt;stamp&gt;~&lt;token&gt;.log and Session logs
</code></pre>
<p>The folder already names the repository, so no file name repeats it. <code>audits/</code> and <code>security-reviews/</code> hold vulnerability evidence, so keep the logs private (see <a href="#security-runs">Security runs</a>). A <strong>Session log</strong> is one session&#39;s full transcript, as the Harness&#39;s JSON output, such as Claude Code&#39;s <code>stream-json</code>:</p>
<pre><code>~/.thirdshift/logs/&lt;owner&gt;/&lt;repo&gt;/commands/issue/&lt;n&gt;-&lt;stamp&gt;-implement.jsonl
Expand All @@ -536,9 +536,10 @@ <h3 id="logs">Logs</h3>
<p>A Resume is logged as its session&#39;s kind plus <code>-resume</code>, e.g. <code>implement-resume.jsonl</code>.</p>
<p>A <strong>Command log</strong> is everything one command printed, stderr and stdout in the order printed, while the terminal still shows all of it. Its folder is the command typed: <code>thirdshift &lt;Issue URL&gt;</code>, a Run or a Spec run, in <code>issue/</code>, <code>thirdshift pickup</code> in <code>pickup/</code>, named for the issue it took, and <code>thirdshift architect</code> in <code>architect/</code>. A Spec run&#39;s covers its Tickets&#39; Runs, a Run&#39;s covers its <a href="#base-fix">Base fix</a>, and a Pass&#39;s covers the Spec run or Run it dispatched: none of those keeps one of its own. So <code>ls -t ~/.thirdshift/logs/acme/widgets/commands/pickup/*.log | head</code> lists the recent passes on acme/widgets that took an issue. A command keeps its Command log once it starts work: a Pass skipped before doing any work keeps none, nor does a Run that fails before it starts work, as on the <a href="#what-a-run-does">Origin match</a>, and <code>setup</code>, <code>email-test</code>, <code>update</code>, <code>version</code> and <code>help</code> never keep one. Lines printed before the Command log&#39;s name is known, such as a Pickup run&#39;s lines on the issues it <a href="#why-an-issue-was-passed-over">passed over</a> before it took one, are written first, and once it is created a progress line says <code>logging this command to &lt;path&gt;</code>. A Command log that can&#39;t be written produces one <code>warning:</code> line on stderr. A failure confined to the Command log leaves the outcome, stdout and exit code unchanged; if the shared folder also prevents writing a Session log, the Run fails before starting that session.</p>
<p>The stamp is the local time the command started, with its UTC offset, as in <code>20261003T120000-0400</code>, in the machine&#39;s time zone, or <code>TZ</code>&#39;s if set, so it agrees with <code>date</code> and <code>ls -l</code>. A command&#39;s Command log and all of its Session logs, its Tickets&#39; Runs&#39; and its Base fix&#39;s included, share that one stamp, so they sort together and each can be found from the other. When a Run fails, stderr ends with the path of its most recent Session log, the place to start looking, then that of its Command log.</p>
<p>New Command logs insert <code>~&lt;token&gt;</code> immediately after the stamp. The random token gives each Command its own storage even when start times agree; its Command record uses that exact stem followed by <code>.command.json</code>. Human Command names stay the same. Session writers still use the tokenless examples above during this transition; readers also accept Session names with <code>~&lt;token&gt;</code>. A matching stamp alone does not attach a tokenless Session to a new Command: a record reference or exact Model evidence must establish that ownership. Legacy logs and records remain readable without renaming.</p>
<p>The <strong>Activity log</strong>, <code>activity.log</code>, is a short running record of what the factory did on the repository (<a href="https://github.com/JacobStephens2/thirdshift/blob/main/docs/adr/0011-thirdshift-keeps-a-per-repository-activity-log.md">ADR-0011</a>). A Run, a Spec run or a Pass writes a line when it starts work, naming its Command log, and one when it ends, with its outcome; a Run you type by hand writes them too, and the run a Pass dispatched, a Spec run&#39;s Tickets and a Base fix write none of their own. A skipped Pass writes a line only when its reason differs from the last line of its own kind, so a repository that sits idle shows one line, not one per pass, and the first skip after a pass that did work always shows: that is when the repository went idle. Every line starts with the local date and time:</p>
<pre><code>2026-10-03 02:00:01 Pickup run skipped: no Ready issue on acme/widgets
2026-10-03 09:30:01 Pickup run #41 started: commands/pickup/41-20261003T093001-0400.log
2026-10-03 09:30:01 Pickup run #41 started: commands/pickup/41-20261003T093001-0400~aB3cD4eF5gH6iJ7k.log
2026-10-03 09:52:17 Pickup run #41 ended: PR https://github.com/acme/widgets/pull/42 is merged
2026-10-03 10:00:01 Pickup run skipped: no Ready issue on acme/widgets
</code></pre>
Expand Down Expand Up @@ -840,7 +841,7 @@ <h3 id="why-an-issue-was-passed-over">Why an issue was passed over</h3>
thirdshift: 03:00:02 #18 labelled needs-info
thirdshift: 03:00:03 #20 blocked by #17
thirdshift: 03:00:04 taking Ready issue #22 &quot;Sharpen the widgets&quot;, as thirdshift https://github.com/acme/widgets/issues/22 would
thirdshift: 03:00:04 logging this command to ~/.thirdshift/logs/acme/widgets/commands/pickup/22-20261003T030001-0400.log
thirdshift: 03:00:04 logging this command to ~/.thirdshift/logs/acme/widgets/commands/pickup/22-20261003T030001-0400~aB3cD4eF5gH6iJ7k.log
</code></pre>
<h3 id="one-run-notification-for-the-issue-taken">One Run notification for the issue taken</h3>
<p>A Pickup run asked for a <a href="#run-notifications">Run notification</a>, by <code>email</code> or by <code>email.always = true</code> in the <a href="#user-config">User config</a> without <code>no-email</code>, sends exactly one when it took a Ready issue: the notification the Spec run or Run it dispatched would have sent started by hand. An address after <code>email</code> wins over <code>email.to</code>, as for a Run.</p>
Expand Down
23 changes: 1 addition & 22 deletions src/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
//! process-wide interface serializes its operations, including emitted output.

mod activity;
mod command_log;
mod command_record;
mod effects;
mod owner;
mod process_identity;
mod recording;
pub mod status;
Expand Down Expand Up @@ -356,27 +356,6 @@ impl Work<'_> {
Work::ArchitectRun(_) | Work::SecurityRun(_) => None,
}
}

/// Where its Command log goes, under its repository's logs at `root`,
/// for the command started at `stamp`:
/// `<root>/commands/<folder>/<prefix><stamp>.log`, one folder per
/// command typed, so the repository, already in the path, is not in the
/// name. `thirdshift <Issue URL>`, a Run or a Spec run, is in `issue/`,
/// `thirdshift pickup` in `pickup/`, both named for the issue, and
/// `thirdshift architect` in `architect/`.
fn command_log(self, root: &Path, stamp: &str) -> PathBuf {
let (folder, prefix) = match self {
Work::Run(issue) | Work::BaseFix(issue) | Work::SpecRun(issue) => {
(CommandKind::Issue, format!("{}-", issue.number))
}
Work::PickupRun(issue) => (CommandKind::Pickup, format!("{}-", issue.number)),
Work::ArchitectRun(_) => (CommandKind::Architect, String::new()),
Work::SecurityRun(_) => (CommandKind::Secure, String::new()),
};
root.join("commands")
.join(folder.folder())
.join(format!("{prefix}{stamp}.log"))
}
}

/// The [`Record`], locked, even if a thread panicked holding it.
Expand Down
14 changes: 0 additions & 14 deletions src/logs/command_log.rs

This file was deleted.

9 changes: 9 additions & 0 deletions src/logs/command_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@ impl StageName {
}

impl CommandRecord {
/// Exact retained Session references, including earlier stages. Callers
/// decide whether they need a deduplicated inventory.
pub fn session_references(&self) -> impl Iterator<Item = &PathBuf> {
self.stages
.iter()
.filter_map(|stage| stage.session_log.as_ref())
.chain(self.session_log.as_ref())
}

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());
Expand Down
24 changes: 9 additions & 15 deletions src/logs/effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::path::Path;
use anyhow::{Context, Result};
use chrono::{DateTime, FixedOffset, Local};

use super::{activity, command_log};
use super::{activity, owner};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Stream {
Expand All @@ -21,13 +21,9 @@ pub(super) struct Tail {
pub bytes: Vec<u8>,
}

pub(super) trait Outside {
type CommandLog: Write + Send;

pub(super) trait Outside: owner::Files {
fn now(&mut self) -> DateTime<FixedOffset>;
fn emit(&mut self, stream: Stream, line: &str) -> io::Result<()>;
fn open_command_log(&mut self, path: &Path) -> Result<Self::CommandLog>;
fn command_log_exists(&mut self, path: &Path) -> bool;
fn activity_tail(&mut self, path: &Path, limit: u64) -> Result<Tail>;
fn append_activity(&mut self, path: &Path, line: &str) -> Result<()>;
fn hostname(&mut self) -> String;
Expand All @@ -38,9 +34,15 @@ pub(super) trait Outside {

pub(super) struct OnMachine;

impl Outside for OnMachine {
impl owner::Files for OnMachine {
type CommandLog = File;

fn create_command_log(&mut self, path: &Path) -> io::Result<File> {
owner::create(path)
}
}

impl Outside for OnMachine {
fn stop_request(&mut self, record: &Path) -> Option<super::stop::Request> {
super::stop::requested(record)
}
Expand Down Expand Up @@ -80,14 +82,6 @@ impl Outside for OnMachine {
}
}

fn open_command_log(&mut self, path: &Path) -> Result<File> {
command_log::open(path)
}

fn command_log_exists(&mut self, path: &Path) -> bool {
path.exists()
}

fn activity_tail(&mut self, path: &Path, limit: u64) -> Result<Tail> {
activity::tail(path, limit)
}
Expand Down
Loading
Loading