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 @@ -95,6 +95,11 @@ human's turn. Full rules in [rulings/AGENTS.md](rulings/AGENTS.md).
REFERENCE.md; refuses the ruling edges.
- `new <project> <slug> --phase <n> …` — writes a phase file from the
template.
- `retire <project> --why "<one line>"` — the only verb that writes
`projects.yaml`: the row's `status:` becomes `retired` and its
`note:` records `retired <today>: <why>`. The row, its phases and its
rulings all stay; it refuses while any phase of that project is
outside `accepted | rerun | dropped | planned`.
- **`dashboard.md` / `dashboard.html`** — GENERATED, never hand-edited: the
board, rendered by the Brain's cortex conductor and self-healed on `main`
(see "Driving the Cortex" below). They are ledger for the merge gate.
Expand Down
25 changes: 23 additions & 2 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ PyAutoCortex/
├── docs/schema_decisions.md ← every dated choice the epic did not fix
├── policy/never_rewrite_history.md policy/remote_sessions.md ← copies of the Mind's; spliced into AGENTS.md
│
├── scripts/cortex.py ← check · gates · rule · move · new (stdlib + PyYAML)
├── scripts/cortex.py ← check · gates · rule · move · new · retire (stdlib + PyYAML)
├── scripts/ledger_merge.py ← the default-deny ledger classifier (+ append-only on rulings/, batches/)
├── tests/test_cortex.py tests/test_ledger_merge.py
├── tests/fixtures/skeleton/ ← one project, one phase per state, five rulings — the witness
Expand Down Expand Up @@ -387,10 +387,24 @@ listed here. A document PyYAML cannot read is reported as one problem.
`local_path` (absolute laptop path — the Cortex-only exception to the
workspace-paths rule), `ral_root`, `mirror` (path | `none`), `sync_cli`,
`sync_verbs`, `ledger`, `witness_file` (glob), `partition`
(`gpu | ral | both`), `status` (`active | dormant | planned`).
(`gpu | ral | both`), `status`
(`active | dormant | planned | retired`).
- `note` (free text) is the **one optional field**: a row may omit it, an
empty `note:` is drift, and any other field is still an unknown-field
error. A note holding `:` or `#` is quoted like any other scalar.
- **Retiring a project.** `cortex.py retire <key> --why "<one line>"` is
the one verb that writes this file: it flips the row's `status:` to
`retired` and rewrites (or appends) its `note:` as
`"retired <today>: <why>"`. The **row stays** — it is the only record of
where that project's data lives — and no phase or ruling is touched;
`rulings/` is append-only and a change of status is not a rewrite of
history. It refuses while the project holds a phase in any state
outside `accepted | rerun | dropped | planned`, naming each one, so the
live questions are ruled or dropped first; `planned` phases are unasked
questions and may stay. The edit is exactly those two lines — every
other byte of the file is preserved — and the result is re-parsed
before it is kept, the original bytes restored if it does not read
back clean.
- Science repos are **not** added to `PyAutoMind/repos.yaml` — that map is the
workspace, this one is the science.
- A file with no rows parses to an empty map (`{}`); PyYAML returns `None`
Expand Down Expand Up @@ -493,6 +507,13 @@ Stdlib only; `main(argv)`; no import-time side effects; every verb takes
wall by hand — and `--where` (required) as its `where:`; a legacy-born
phase refuses `--gates`. Refuses a duplicate phase number, an existing
file or an unknown project.
- **`retire <project> --why "<one line>"`** — the only verb that writes
`projects.yaml`: `status: retired` plus a `note:` reading
`"retired <today>: <why>"`, two lines edited in place and every other
byte preserved. Refuses an unknown key, an already-retired row, and any
project still holding a phase outside `accepted | rerun | dropped |
planned` (the message names each `<phase> — <state>`). The row, its
phases and its rulings all stay; see `projects.yaml` above.

---

Expand Down
18 changes: 9 additions & 9 deletions dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -238,15 +238,15 @@
</table>
<h3>Nothing open</h3>
<table class="map">
<tr><th>Project</th><th>Status</th><th>Phases</th></tr>
<tr><td>euclid_dr1_prelim</td><td>planned</td><td>none</td></tr>
<tr><td>concr</td><td>dormant</td><td>none</td></tr>
<tr><td>cowls_diana</td><td>dormant</td><td>none</td></tr>
<tr><td>ic50_workspace</td><td>dormant</td><td>planned 1</td></tr>
<tr><td>pj011646</td><td>dormant</td><td>none</td></tr>
<tr><td>profiling</td><td>dormant</td><td>none</td></tr>
<tr><td>slope_hierarchy</td><td>dormant</td><td>planned 2</td></tr>
<tr><td>subhalo_simulations</td><td>dormant</td><td>none</td></tr>
<tr><th>Project</th><th>Status</th><th>Phases</th><th>Retire</th></tr>
<tr><td>euclid_dr1_prelim</td><td>planned</td><td>none</td><td></td></tr>
<tr><td>concr</td><td>dormant</td><td>none</td><td><button class="copy text" data-cmd="/cortex — retire the science project concr: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire concr --why &quot;&lt;one line on why&gt;&quot;`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change." aria-label="Copy the retire prompt">📋 retire</button></td></tr>
<tr><td>cowls_diana</td><td>dormant</td><td>none</td><td><button class="copy text" data-cmd="/cortex — retire the science project cowls_diana: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire cowls_diana --why &quot;&lt;one line on why&gt;&quot;`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change." aria-label="Copy the retire prompt">📋 retire</button></td></tr>
<tr><td>ic50_workspace</td><td>dormant</td><td>planned 1</td><td><button class="copy text" data-cmd="/cortex — retire the science project ic50_workspace: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire ic50_workspace --why &quot;&lt;one line on why&gt;&quot;`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change." aria-label="Copy the retire prompt">📋 retire</button></td></tr>
<tr><td>pj011646</td><td>dormant</td><td>none</td><td><button class="copy text" data-cmd="/cortex — retire the science project pj011646: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire pj011646 --why &quot;&lt;one line on why&gt;&quot;`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change." aria-label="Copy the retire prompt">📋 retire</button></td></tr>
<tr><td>profiling</td><td>dormant</td><td>none</td><td><button class="copy text" data-cmd="/cortex — retire the science project profiling: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire profiling --why &quot;&lt;one line on why&gt;&quot;`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change." aria-label="Copy the retire prompt">📋 retire</button></td></tr>
<tr><td>slope_hierarchy</td><td>dormant</td><td>planned 2</td><td><button class="copy text" data-cmd="/cortex — retire the science project slope_hierarchy: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire slope_hierarchy --why &quot;&lt;one line on why&gt;&quot;`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change." aria-label="Copy the retire prompt">📋 retire</button></td></tr>
<tr><td>subhalo_simulations</td><td>dormant</td><td>none</td><td><button class="copy text" data-cmd="/cortex — retire the science project subhalo_simulations: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire subhalo_simulations --why &quot;&lt;one line on why&gt;&quot;`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change." aria-label="Copy the retire prompt">📋 retire</button></td></tr>
</table>
<a id="projects"></a><h2>Projects <a class="mdsrc" href="https://github.com/PyAutoLabs/PyAutoCortex/blob/main/projects.yaml">markdown version</a></h2>
<section class="project">
Expand Down
76 changes: 66 additions & 10 deletions dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,72 @@ One row per active project — the next thing, what it is holding, and when it l

#### Nothing open

| Project | Status | Phases |
|---|---|---|
| euclid_dr1_prelim | planned | none |
| concr | dormant | none |
| cowls_diana | dormant | none |
| ic50_workspace | dormant | planned 1 |
| pj011646 | dormant | none |
| profiling | dormant | none |
| slope_hierarchy | dormant | planned 2 |
| subhalo_simulations | dormant | none |
| Project | Status | Phases | Retire |
|---|---|---|---|
| euclid_dr1_prelim | planned | none | |
| concr | dormant | none | retire ↓ |
| cowls_diana | dormant | none | retire ↓ |
| ic50_workspace | dormant | planned 1 | retire ↓ |
| pj011646 | dormant | none | retire ↓ |
| profiling | dormant | none | retire ↓ |
| slope_hierarchy | dormant | planned 2 | retire ↓ |
| subhalo_simulations | dormant | none | retire ↓ |

<details><summary>📋 retire concr</summary>

```
/cortex — retire the science project concr: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire concr --why "<one line on why>"`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change.
```

</details>

<details><summary>📋 retire cowls_diana</summary>

```
/cortex — retire the science project cowls_diana: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire cowls_diana --why "<one line on why>"`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change.
```

</details>

<details><summary>📋 retire ic50_workspace</summary>

```
/cortex — retire the science project ic50_workspace: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire ic50_workspace --why "<one line on why>"`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change.
```

</details>

<details><summary>📋 retire pj011646</summary>

```
/cortex — retire the science project pj011646: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire pj011646 --why "<one line on why>"`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change.
```

</details>

<details><summary>📋 retire profiling</summary>

```
/cortex — retire the science project profiling: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire profiling --why "<one line on why>"`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change.
```

</details>

<details><summary>📋 retire slope_hierarchy</summary>

```
/cortex — retire the science project slope_hierarchy: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire slope_hierarchy --why "<one line on why>"`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change.
```

</details>

<details><summary>📋 retire subhalo_simulations</summary>

```
/cortex — retire the science project subhalo_simulations: confirm it is holding nothing live, then in PyAutoCortex run `python3 scripts/cortex.py retire subhalo_simulations --why "<one line on why>"`, `python3 scripts/cortex.py check`, `pyauto-brain cortex dashboard --apply`, and push the ledger. The row, its phases and its rulings all stay — only the status and the note change.
```

</details>

## Projects

Expand Down
2 changes: 1 addition & 1 deletion projects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
# ledger: <path> # the project's own commentary ledger, relative to local_path
# witness_file: <glob> # where a run's witness lands, relative to the mirror
# partition: gpu | ral | both # which SLURM partition(s) the project runs on
# status: active | dormant | planned
# status: active | dormant | planned | retired # `cortex.py retire` writes the last
# note: <text> # OPTIONAL, and the only optional field — free text
# # about the row; an empty `note:` is drift
#
Expand Down
97 changes: 94 additions & 3 deletions scripts/cortex.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""cortex.py — the Cortex's lifecycle script: check · gates · rule · move · new.
"""cortex.py — the Cortex's lifecycle script: check · gates · rule · move · new · retire.

The run-and-ruling registry (project → phase → runs → rulings) is a tree of
markdown files with light headers, and this script is the one thing that
Expand All @@ -19,6 +19,7 @@
python3 scripts/cortex.py rule <phase> <verb> --body <file> [...]
python3 scripts/cortex.py move <phase> <state> [...]
python3 scripts/cortex.py new <project> <slug> --phase <n> [...]
python3 scripts/cortex.py retire <project> --why "<one line>" # a project's row

Exit codes: 0 = done · 1 = drift or a refused edit · 2 = bad arguments.
"""
Expand Down Expand Up @@ -99,7 +100,7 @@
#: any field outside these two tuples is still an error.
PROJECT_OPTIONAL_FIELDS = ("note",)
PARTITIONS = ("gpu", "ral", "both")
PROJECT_STATUSES = ("active", "dormant", "planned")
PROJECT_STATUSES = ("active", "dormant", "planned", "retired")

# --------------------------------------------------------------------------- #
# grammars (REFERENCE.md)
Expand Down Expand Up @@ -347,7 +348,7 @@ def _finish_row(key: str, row: dict, problems: "list[str]") -> None:
f"not {row['partition']}")
if row.get("status") and row["status"] not in PROJECT_STATUSES:
problems.append(f"projects.yaml: {key}.status must be "
f"active | dormant | planned, not {row['status']}")
f"active | dormant | planned | retired, not {row['status']}")
remote = row.get("remote")
if remote and remote != "none" and not re.match(r"^[\w.-]+/[\w.-]+$", remote):
problems.append(f"projects.yaml: {key}.remote must be owner/repo or none")
Expand Down Expand Up @@ -1010,6 +1011,89 @@ def cmd_move(args) -> int:
return 0


# --------------------------------------------------------------------------- #
# retire
# --------------------------------------------------------------------------- #
def retire_project(root: Path, key: str, why: str, today: date) -> str:
"""Flip one `projects.yaml` row to `status: retired` and stamp the reason
on its `note:`. The only verb that writes `projects.yaml`.

Three things it deliberately does not do. It does not **delete the row**:
that row is the only record of where the project's data lives, and a
retired project still has to be findable. It does not touch a **phase** or
a **ruling**: `rulings/` is append-only and history is not rewritten by a
change of status. And it does not retire over **live work** — every state
outside `RULED_STATES` and `planned` is an unfinished question, so the
refusal names each one and the human rules or drops it first. `planned`
stays: an unasked question costs nothing to leave behind.

The edit is two lines. Everything else in the file — comments, blank
lines, the order of the rows, the other rows' bytes — is preserved, and
the result is re-parsed before it is kept: a `projects.yaml` this verb
could not read back is restored to the bytes it had.
"""
why = (why or "").strip()
if not why:
raise CortexError("--why must say why, in one line")
if '"' in why or "\n" in why:
raise CortexError("--why cannot hold a double quote or a newline "
"(projects.yaml quotes with no escapes)")
projects, problems = load_projects(root)
if problems:
raise CortexError("projects.yaml does not parse clean — run `check` "
"and fix it before retiring: " + "; ".join(problems))
if key not in projects:
raise CortexError(f"{key} is not a projects.yaml key")
if projects[key].get("status") == "retired":
raise CortexError(f"{key} is already retired")
phases, _ = load_phases(root)
live = [f"{ph.rel} — {ph.state}" for ph in phases
if ph.project_dir == key
and ph.state not in RULED_STATES | {"planned"}]
if live:
raise CortexError(f"{key} still has live work — rule or drop it "
f"first: {', '.join(live)}")

path = root / "projects.yaml"
original = path.read_text(encoding="utf-8")
lines = original.split("\n")
starts = [i for i, ln in enumerate(lines) if ln == f"{key}:"]
if len(starts) != 1:
raise CortexError(f"projects.yaml: expected one `{key}:` line, "
f"found {len(starts)}")
start = starts[0]
stop = start + 1
while stop < len(lines) and lines[stop].startswith(" "):
stop += 1
block = lines[start:stop]
at_status = [i for i, ln in enumerate(block) if re.match(r"^ status: .*$", ln)]
at_note = [i for i, ln in enumerate(block) if re.match(r"^ note:(?: .*)?$", ln)]
if len(at_status) != 1 or len(at_note) > 1:
raise CortexError(f"projects.yaml: {key} is not one ` status:` line "
f"and at most one ` note:` line")
block[at_status[0]] = " status: retired"
note = f' note: "retired {today.isoformat()}: {why}"'
if at_note:
block[at_note[0]] = note
else:
block.append(note)
lines[start:stop] = block
path.write_text("\n".join(lines), encoding="utf-8")

rows, problems = load_projects(root)
if problems or rows.get(key, {}).get("status") != "retired":
path.write_text(original, encoding="utf-8")
raise CortexError("the edit would not read back — projects.yaml is "
"unchanged: " + ("; ".join(problems) or
f"{key} did not come back retired"))
return f"retired {key}"


def cmd_retire(args) -> int:
print(retire_project(args.root, args.project, args.why, _today(args)))
return 0


# --------------------------------------------------------------------------- #
# rule
# --------------------------------------------------------------------------- #
Expand Down Expand Up @@ -1317,6 +1401,13 @@ def build_parser() -> argparse.ArgumentParser:
n.add_argument("--minutes", type=int, help="Review-minutes seed")
n.add_argument("--title", help="the title after `phase <n>:` (default: the slug's words)")
n.set_defaults(func=cmd_new)

t = sub.add_parser("retire", help="flip a project's row to status: retired")
_common(t, dated=True)
t.add_argument("project", help="the projects.yaml key")
t.add_argument("--why", required=True,
help="one line: why the project is being retired")
t.set_defaults(func=cmd_retire)
return parser


Expand Down
Loading
Loading