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
3 changes: 3 additions & 0 deletions agents/conductors/cortex/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ Mind's: a session holding one organ and not the other still works.
- **No path is named in this code.** Science projects live outside the
workspace; the one place carrying such a path is the Cortex's own
`projects.yaml`, and every path the board prints is read from a row of it.
- **The assistant is a name, not a page.** `assistant` is a `projects.yaml`
field rendered by name into the work briefs; the conductor never resolves
it on disk and never reads an assistant page.

## The board

Expand Down
53 changes: 46 additions & 7 deletions agents/conductors/cortex/_cortex.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,44 @@ def _src_url(blob: str, src: str) -> str:
# ------------------------------------------------------------- the payloads ---
# Every row hands the reader the next command rather than a decision. The
# science verbs stay the Cortex script's and a run is only ever the human's ask.
def _work_brief(r: dict, projects: dict) -> str:
"""The entry protocol for the subagent that works this task, or `""`.

A project row names the domain assistant its work enters through; this
renders that name into the three work-shaped payloads. The conductor
NAMES the assistant and never reads it — no stat, no resolve, no
assistant page loaded in the Cortex chat (see this conductor's
AGENTS.md). A row without the field, or with `none`, gets no brief: the
work is plain workspace work.
"""
row = projects.get(r["project"], {})
assistant = row.get("assistant", "none")
if not assistant or assistant == "none":
return ""
env = re.sub(r"[^A-Za-z0-9]", "_", assistant).upper()
return "\n".join([
f"Work brief — {r['project']} / {r['slug']}. Delegate this to an "
"execution-tier subagent; never work it in the Cortex chat.",
f"cd {row.get('local_path', '')} && source activate.sh",
f"Enter through the assistant `{assistant}`: resolve it as ${env}, "
f"else <workspace root>/{assistant}; read its AGENTS.md first and "
"follow its constitution.",
"Then read wiki/project/state.md, the newest "
"wiki/project/YYYY-MM-DD-*.md, and the skills the task names.",
f"Then the task {r['rel']} — its ## Witness is the contract.",
"Finish: write today's journal entry, rewrite wiki/project/state.md, "
"and return (1) the outcome against the witness and (2) assistant "
"drift — any skill or wiki page that was wrong, missing or stale.",
])


def _with_brief(payload: str, r: dict, projects: dict) -> str:
"""`payload` with the work brief under it, separated by a blank line —
or `payload` unchanged when the project declares no assistant."""
brief = _work_brief(r, projects)
return f"{payload}\n\n{brief}" if brief else payload


def _checkin_payload(c: dict) -> str:
"""The paste for the laptop's science chat — the whole door as one
sentence, naming the stamp it is refreshing *from* so "since last time"
Expand Down Expand Up @@ -657,7 +695,7 @@ def _next_task_payload(r: dict, c: dict) -> str:
f"python3 scripts/cortex.py move tasks/{r['project']}/"
"<slug>.md ready",
_submit_line(r["project"], c["projects"])]
return "\n".join(L)
return _with_brief("\n".join(L), r, c["projects"])


def _rerun_payload(r: dict, c: dict) -> str:
Expand All @@ -667,7 +705,7 @@ def _rerun_payload(r: dict, c: dict) -> str:
the next submission a rerun rather than a repeat, and `rule … rerun`
is the only edge that puts a task back on the board.
"""
return "\n".join([
return _with_brief("\n".join([
f"The PyAutoCortex task {r['rel']} needs running again. Draft the "
"rerun body — what came back, and what changes — for my approval, "
"then file it and relaunch:",
Expand All @@ -676,12 +714,13 @@ def _rerun_payload(r: dict, c: dict) -> str:
_submit_line(r["project"], c["projects"]),
f"python3 scripts/cortex.py move {r['rel']} submitted --run <jobid>"
" # one run per call; --after <run> chains the next",
])
]), r, c["projects"])


def _planned_payload(r: dict) -> str:
return (f"python3 scripts/cortex.py move {r['rel']} ready # when the "
"`Ready when:` clause in its `## Question` is met")
def _planned_payload(r: dict, projects: dict) -> str:
return _with_brief(
f"python3 scripts/cortex.py move {r['rel']} ready # when the "
"`Ready when:` clause in its `## Question` is met", r, projects)


def _relaunch_payload(r: dict, projects: dict) -> str:
Expand Down Expand Up @@ -713,7 +752,7 @@ def task_chips(r: dict, c: dict) -> list[tuple[str, str]]:
if state == "gated":
chips.append(("its gates", _gate_payload(r)))
if state == "planned":
chips.append(("open it", _planned_payload(r)))
chips.append(("open it", _planned_payload(r, c["projects"])))
if state == "rerun":
chips.append(("relaunch it", _relaunch_payload(r, c["projects"])))
# Only where a rerun would BE a verdict. On a running task the job is
Expand Down
2 changes: 1 addition & 1 deletion skills/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ readiness gate, or execution — those belong to the organs.
| `/workspace` | Workspace Agent | `bin/pyauto-brain workspace` → WorkspaceDecision (plan/survey example authorship) → `start_dev` → `start_workspace` → `ship_workspace` |
| `/eyes` | Eyes Agent | `bin/pyauto-brain eyes` → survey/review a visualization workspace's figures; render via its `gallery_run.sh`; accepted critiques → `/intake` → `start_dev` |
| `/profiling` | Profiling Agent | `bin/pyauto-brain profiling` → campaign/ingest/triage plans over the autolens_profiling workspace |
| `/cortex` | Cortex Agent | `bin/pyauto-brain cortex checkin` → the science check-in over PyAutoCortex: pull every active project, score every live run, move what came back, re-render the board and summarise it by project (`census`/`dashboard`/`gates`/`collect` are the verbs it composes); a run is submitted only on the human's ask; the ruling stays the human's |
| `/cortex` | Cortex Agent | `bin/pyauto-brain cortex checkin` → the science check-in over PyAutoCortex: pull every active project, score every live run, move what came back, re-render the board and summarise it by project (`census`/`dashboard`/`gates`/`collect` are the verbs it composes); a run is submitted only on the human's ask; the ruling stays the human's; each work-shaped task gets a brief that enters the project through its declared assistant |
| `/hygiene` | Hygiene Agent | `bin/pyauto-brain hygiene` → perf/tidy/noise/deps/docs/docstrings upkeep plans; delegates fixes to refactor/bug/feature |
| `/build` | Build Agent | `bin/pyauto-brain build` → vitals faculty → Heart → PyAutoHands |
| `/health` | Health Agent | `bin/pyauto-brain health` loop → vitals faculty → Heart → GREEN |
Expand Down
14 changes: 14 additions & 0 deletions skills/WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,20 @@ asking for the minor version, printing the summary.
- **Judgment tier after return:** interpret failures, decide routing, update
registries, talk to the user.

**Cortex project work.** Science projects delegate the same way, and `/cortex`
writes the prompt for you: every next-task, rerun and planned payload of a
project whose `projects.yaml` row declares an `assistant:` carries a **work
brief** under it. Paste that brief verbatim into the execution-tier subagent —
it is the whole prompt, and it names the assistant the subagent enters through
(its `AGENTS.md` first, then the project's `wiki/project/` state and journal).
The subagent **finishes** by writing today's journal entry and rewriting
`wiki/project/state.md`, and **returns two parts**: the outcome against the
task's `## Witness`, and assistant drift — any skill or wiki page that was
wrong, missing or stale. The judgment tier files each drift item through
`/intake` against the assistant repo; nobody edits the assistant from the
Cortex chat. A brief that is empty means the row says `assistant: none`. The
heartbeat below applies whenever the run is a long one.

**Progress heartbeat (delegations expected to run > ~15 min).** Subagents have
no clock, so a wall-clock cadence cannot be promised; milestones can. The
judgment tier passes a progress-file path (under the session scratchpad, e.g.
Expand Down
23 changes: 23 additions & 0 deletions skills/cortex/cortex.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,27 @@ a task file by hand: every task edit is `python3 scripts/cortex.py move`,
every verdict is `python3 scripts/cortex.py rule`, both in the PyAutoCortex
checkout, and the ruling body is the human's words verbatim.

### 4. Work on a project

Every next-task, rerun and planned prompt of a project that declares an
`assistant:` in `projects.yaml` carries a **work brief** under it — the entry
protocol for the subagent that does the work.

- **Paste it verbatim into an execution-tier subagent**, one rung down the
ladder in [`../WORKFLOW.md`](../WORKFLOW.md) (from a Fable session,
`Agent(model="opus", …)`). The brief is the prompt; the project is never
worked in the Cortex chat.
- **The return is two parts**: the outcome against the task's `## Witness`,
and **assistant drift** — every skill or wiki page of the assistant that was
wrong, missing or stale.
- **File each drift item through `/intake`** against the assistant repo. The
assistant is never edited from the Cortex chat.
- **An empty brief means `assistant: none`** — the project routes through no
assistant and the work is plain workspace work.

The check-in itself and the ruling prompts carry no brief and load no
assistant page.

## The push rule

`--push` is allowed only when **`gh auth status` succeeds** *and* **the Cortex
Expand Down Expand Up @@ -113,6 +134,8 @@ and nothing for the human to merge. **Never `main` directly, never `--force`.**
`checkpoints` table (keyed by run directory) or a `runs` table (by job id).
- **The door runs once and ends.** No timer, no subscription, no cron, no
loop — you check in, you report, you stop.
- **The door names the assistant, never reads it.** No assistant page is
loaded in the Cortex chat; the assistant is the subagent's entry protocol.
- **`rulings/` is append-only.** Never edit or delete one.
- **Retiring ends a project, it does not erase one.** `cortex.py retire`
changes a `projects.yaml` row's `status:` and `note:` and nothing else:
Expand Down
2 changes: 2 additions & 0 deletions tests/_cortex_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def _task(project, slug, summary, budget, runs, where, witness_prose,
sync_cli: hpc/sync
sync_verbs: [pull, submit, jobs, tail]
ledger: wiki/project/state.md
assistant: none
witness_file: results/**/*.json
partition: gpu
status: active
Expand All @@ -84,6 +85,7 @@ def _task(project, slug, summary, budget, runs, where, witness_prose,
sync_cli: hpc/sync
sync_verbs: [pull, submit, jobs, tail]
ledger: wiki/project/state.md
assistant: example_assistant
witness_file: results/**/*.json
partition: ral
status: active
Expand Down
60 changes: 59 additions & 1 deletion tests/test_cortex_conductor.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,8 @@ def test_census_by_project_prints_the_same_tree(skeleton):
" local_path: /mnt/c/Users/Jammy/Science/dormant\n"
" ral_root: /mnt/ral/jnightin/dormant\n mirror: none\n"
" sync_cli: hpc/sync\n sync_verbs: [pull]\n"
" ledger: wiki/state.md\n witness_file: out/**/*.json\n"
" ledger: wiki/state.md\n assistant: none\n"
" witness_file: out/**/*.json\n"
" partition: gpu\n status: dormant\n")

#: Two more, so the Nothing-open table can be read for all three of the
Expand Down Expand Up @@ -828,6 +829,63 @@ def test_a_project_with_no_submit_verb_says_so_rather_than_inventing_one(skeleto
c["tasks"][0], c)


# --- the assistant entry protocol ------------------------------------------
BRIEF = "Enter through the assistant `example_assistant`"


def _with_assistant(c: dict, value: str) -> dict:
c["projects"]["proj"]["assistant"] = value
return c


def test_the_work_shaped_prompts_carry_the_assistant_entry_brief(skeleton):
"""A row that declares an assistant turns the three work-shaped payloads
into subagent briefs — the same three chips on both twins, because
`task_chips` is the one funnel."""
c = _with_assistant(_synthetic(_row("awaiting-ruling", "four")),
"example_assistant")
for payload in (_cortex._next_task_payload(c["tasks"][0], c),
_cortex._rerun_payload(c["tasks"][0], c),
_cortex._planned_payload(c["tasks"][0], c["projects"])):
assert BRIEF in payload
assert "EXAMPLE_ASSISTANT" in payload
assert "cd /s/proj && source activate.sh" in payload
assert "Work brief — proj / four" in payload
assert "tasks/proj/four.md — its ## Witness is the contract" in payload

labelled = dict(_cortex.task_chips(c["tasks"][0], c))
assert BRIEF in labelled[
"the results are good — accept and open the next task"]
assert BRIEF in labelled["run it again"]
planned = _with_assistant(_synthetic(_row("planned", "five")),
"example_assistant")
assert BRIEF in dict(_cortex.task_chips(planned["tasks"][0],
planned))["open it"]


def test_a_none_row_and_a_row_without_the_field_carry_no_brief(skeleton):
"""`none` is the declaration that the project routes through no
assistant; a Cortex checkout predating the field reads the same way."""
for c in (_with_assistant(_synthetic(_row("awaiting-ruling", "four")),
"none"),
_synthetic(_row("awaiting-ruling", "four"))):
assert "assistant" not in c["projects"]["proj"] or \
c["projects"]["proj"]["assistant"] == "none"
for payload in (_cortex._next_task_payload(c["tasks"][0], c),
_cortex._rerun_payload(c["tasks"][0], c),
_cortex._planned_payload(c["tasks"][0], c["projects"])):
assert "Work brief" not in payload and "assistant" not in payload


def test_the_ruling_and_checkin_prompts_name_no_assistant(skeleton):
"""The door and the verdict are assistant-free by ruling: the assistant is
the execution subagent's entry protocol, not part of checking in."""
c = _with_assistant(_synthetic(_row("awaiting-ruling", "four")),
"example_assistant")
assert "assistant" not in _cortex._ruling_payload(c["tasks"][0])
assert "assistant" not in _cortex._checkin_payload({"checkin": "never"})


def test_the_chips_a_state_carries_are_the_same_everywhere(skeleton):
"""One table, three renderings — the board, `--by-project` and the door
all ask `task_chips`, so a prompt cannot appear in one and not another."""
Expand Down
Loading