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
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ not re-derive them.
- `running <project> <jobid>` / `done <project> <jobid> [--failed] [--wall
H:MM] [--note …]` — what the cluster said; `done` moves the run into the
log.
- `sync-runs <project> --digest <digest.json> [--dry-run]` — the same two
facts, read from a PyAutoFit campaign digest (`autofit.campaign`): `open`
runs whose tasks started become `running`, runs whose tasks all ended go
through `done` with the task counts. Matched by SLURM job id; never writes
a `result`, `lesson` or `## Now` (decision 64).
- `log <project> "<text>" [--kind note|result|lesson]` — the human's words,
dated today, at the head of the log.
- `now <project> "<text>"` — rewrites `## Now`.
Expand Down
35 changes: 33 additions & 2 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Four kinds:

| Kind | Written by | What it is |
|---|---|---|
| `run` | `cortex.py run` / `done` | `<jobid> submitted: <what>` · `<jobid> finished — wall H:MM: <what>` · `<jobid> failed: <what>` |
| `run` | `cortex.py run` / `done` / `sync-runs` | `<jobid> submitted: <what>` · `<jobid> running: <what> — <counts> (digest …)` · `<jobid> finished — wall H:MM: <what>` · `<jobid> failed: <what>` |
| `result` | `cortex.py log --kind result` | what the human read off the results, in their words |
| `lesson` | `cortex.py log --kind lesson` | something the human wants kept — a trap, a rule, a fact about the setup |
| `note` | `cortex.py log` (default), `new`, `retire` | anything else: a thought, a plan, `ledger opened`, `retired: <why>` |
Expand Down Expand Up @@ -96,6 +96,7 @@ creates an issue; `Issue:` is set with `new --issue` or `link`.
| `issue <project> [-n N]` | nothing (prints) | — |
| `link <project> <ref>` | the `Issue:` header line | the ref is not `Repo#N`, an issue URL or `none` |
| `retire <project> --why "…"` | `projects.yaml` (`status: retired`, `note:`), one `note` entry | the row is already retired; the ledger still lists a run; `--why` holds `"` |
| `sync-runs <project> [--digest PATH] [--dry-run]` | each run line the digest settles: `open` → `running` (+ a `run` entry), or `done` with the task counts | the digest is not a schema-1 `digest.json` with a readable `tasks.jsonl` beside it; no `--digest` and no `digest:` in the row |

Every verb takes `--root <dir>` (default: this checkout) and every dated verb
`--today YYYY-MM-DD`, so the tests run against a copy of the fixture with a
Expand Down Expand Up @@ -123,7 +124,9 @@ scans the raw column-0 keys and reports every duplicate with its line numbers:
| `witness_file` | glob relative to the mirror | kept for the projects' own tooling; the Cortex no longer scores it |
| `partition` | `gpu` \| `ral` \| `both` | which SLURM partition(s) the project runs on; `run` needs `--partition` on `both` |
| `status` | `active` \| `dormant` \| `planned` \| `retired` | `retire` writes the last |
| `note` | free text, **optional** | the only optional field; an empty `note:` is drift |
| `note` | free text, **optional** | an empty `note:` is drift |
| `campaign_file` | path relative to `local_path` (or absolute), **optional** | the project's PyAutoFit `campaign.yaml`; must end `.yaml`/`.yml`, no `..` |
| `digest` | path relative to `local_path` (or absolute), **optional** | the pulled campaign `digest.json` `sync-runs` reads by default; must name a `digest.json`, no `..` |

**Partition rule (human, 2026-09-30).** `both` means a project has GPU jobs *and*
CPU jobs, each on its own partition — never a CPU-only array submitted as
Expand All @@ -135,6 +138,34 @@ CPUs/task, throttle ≤`%2` and no pending gres/gpu jobs (`squeue -p gpu -t PD`)
Record a run as GPU only after `scontrol show job` shows `gres/gpu` in its TRES.
Never reorder, hold or cancel another campaign's jobs without the human's OK.

## Run status from a campaign digest (`sync-runs`)

A project whose runs are PyAutoFit campaigns (`autofit.campaign`) has a
`digest.json` with a `tasks.jsonl` beside it, pulled from the cluster by the
project's sync CLI. `sync-runs` reads them and brings `## Runs` in line:

- **The key is the SLURM job id.** A run line `3002_[0-3]` is matched to the
`tasks.jsonl` rows whose `job_id` is `3002` and whose `key`
(`<job>_<array index>`) falls in `0–3`; a bare `3002` takes every task of
the array. The digest's `campaign` name is reported, not matched on.
- An `open` run with a task in `running`/`timeout_pending` or already ended
becomes `running`, with one `run` entry giving the task counts.
- A run whose matched tasks have all ended (`completed`, `failed`, `timeout`,
`oom`, `node_fail`, `cancelled`, `preempted`) goes through `done`: the counts
are its note, the wall is first start → last end, and it is `failed` when no
task completed.
- A run with no tasks in the digest, and a digest job with no run line, are
printed and left alone — `sync-runs` never adds a run (`run` records a
submission) and never removes one it cannot account for.
- It is idempotent, `--dry-run` writes nothing, and the first line printed is
`sync-runs: <project>: <n> changed`. Exit 0 on success (changes or not),
1 on a malformed digest.

**The boundary.** Run status is a cluster fact, so a script may record it
from the scheduler's accounting. What the runs *mean* is the human's: a
`result` or `lesson` entry and `## Now` are only ever written on their ask, and
`sync-runs` writes neither (schema decision 64).

## Repository layout

```
Expand Down
16 changes: 16 additions & 0 deletions docs/schema_decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,3 +506,19 @@ Choices made in phase 2 — the conductor, the board and the workflows
Why: projects were started by hand-copying an older one and never got a
row; `drift` (decision 62) now reports such a folder as unregistered, and
birth is the one step that prevents it.

64. **Run status may be synced from a campaign digest; meaning may not**
(2026-10-30, PyAutoCortex#82; hpc-campaign ruling F3 — named "decision 62"
in the hpc-campaign ledger before 62 and 63 were taken). `cortex.py
sync-runs <project> --digest <digest.json>` reads a PyAutoFit
`autofit.campaign` digest and its `tasks.jsonl`, matches run lines by
SLURM job id (narrowed to the array indices the ident names), flips
`open → running` and calls `done` with the task counts when every matched
task has ended. The two run states stay (`open`, `running`); a finished run
is still a log entry. It writes `run` entries only — never `result`,
`lesson` or `## Now` — and never adds or drops a run it cannot account for.
`projects.yaml` gains two optional fields, `campaign_file` and `digest`.
Why: a status check of a campaign was 5–10 ssh round trips and a
hand-read `squeue`; the digest is one small file the collector already
writes. Decision 60 still holds: nothing in a ledger is inferred from
results, and a run's start and end are cluster facts.
Loading
Loading