diff --git a/agents/conductors/cortex/AGENTS.md b/agents/conductors/cortex/AGENTS.md index e4b1fdcc..28e72c18 100644 --- a/agents/conductors/cortex/AGENTS.md +++ b/agents/conductors/cortex/AGENTS.md @@ -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 diff --git a/agents/conductors/cortex/_cortex.py b/agents/conductors/cortex/_cortex.py index 651b3331..b03f1865 100644 --- a/agents/conductors/cortex/_cortex.py +++ b/agents/conductors/cortex/_cortex.py @@ -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 /{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" @@ -657,7 +695,7 @@ def _next_task_payload(r: dict, c: dict) -> str: f"python3 scripts/cortex.py move tasks/{r['project']}/" ".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: @@ -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:", @@ -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 " " # one run per call; --after 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: @@ -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 diff --git a/skills/COMMANDS.md b/skills/COMMANDS.md index a6bc4328..e668265b 100644 --- a/skills/COMMANDS.md +++ b/skills/COMMANDS.md @@ -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 | diff --git a/skills/WORKFLOW.md b/skills/WORKFLOW.md index 20991d90..5f943aa2 100644 --- a/skills/WORKFLOW.md +++ b/skills/WORKFLOW.md @@ -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. diff --git a/skills/cortex/cortex.md b/skills/cortex/cortex.md index 405da57f..cba41b20 100644 --- a/skills/cortex/cortex.md +++ b/skills/cortex/cortex.md @@ -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 @@ -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: diff --git a/tests/_cortex_board.py b/tests/_cortex_board.py index 8b19247d..c3cbb1e1 100644 --- a/tests/_cortex_board.py +++ b/tests/_cortex_board.py @@ -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 @@ -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 diff --git a/tests/test_cortex_conductor.py b/tests/test_cortex_conductor.py index 1a6448ba..d9b8ec68 100644 --- a/tests/test_cortex_conductor.py +++ b/tests/test_cortex_conductor.py @@ -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 @@ -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."""