phase: merge task/46-a-board-with-no-test-command-says-so into phase/61-what-an-agent-is-told-before-it-starts

This commit is contained in:
istos
2026-08-02 13:43:17 +02:00
7 changed files with 358 additions and 4 deletions
+13
View File
@@ -193,6 +193,19 @@ in. A wrong guess costs nothing an empty value would not, since a prefix
that matches nothing denies exactly the same way. An existing value is
never overwritten, so `--setup` cannot undo a hand-edit.
Empty is honest, but honest and invisible is a board that lets an agent
discover the problem on your behalf, slowly — so the board says it out
loud. With `BOARD_AGENT_COMMANDS` holding nothing (unset, whitespace, or
a lone comma, which is how the adapters read it too) the header carries a
quiet `no agent commands` chip naming the setting and the file that holds
it, `manager/local/.env`, and every launch of the two intents that would
have run those commands — **▸ start work** and **↻ act on PR** — appends
the same sentence to its line in the ticker. It is a configuration fact,
not an alarm: it wears the settled register rather than `--alarm`, it
appears only while the setting is empty, and it never refuses a launch.
An agent that only edits files is still useful, and bench does not
decline work because a project is unconfigured.
Bare Enter takes the default, Ctrl-D skips the rest, and what lands is
`core/.env.example` with the answers substituted into their lines: every
other key, every comment, so the written file is where the project reads
+3 -1
View File
@@ -46,7 +46,9 @@ BOARD_AGENT_MODEL_REVIEW=
# (each adapter renders them into its vendor's permission rules; the
# git/gh grants per launch intent are built in). Headless runs have no
# human at a permission prompt, so a test runner missing from this list
# is a test the work agent cannot run.
# is a test the work agent cannot run. Left empty — or holding only
# whitespace or a lone comma — the board says so: a quiet `no agent
# commands` chip in the header, and a note on every work launch.
BOARD_AGENT_COMMANDS=python3 -m unittest
# What counts as a definition-of-done check (the Focus view's CHECKS
+23 -3
View File
@@ -254,6 +254,21 @@ def _launch(mode: str, prompt: str, cwd: Path, agent_id: str, filename: str, log
return proc, log_file, model
def _no_commands_note() -> str | None:
"""What a launch owes the ticker when the project configured nothing for
its agents to run: this run can edit and commit, but it cannot check its
own work. Only the two intents that would have run the commands say it
(work and act-pr); a read-only kind never had them.
A note, never a refusal an agent that only edits files is still
useful, and bench does not decline work because a project is
unconfigured. The header says the same thing standing still."""
if config.agent_commands():
return None
return ("no project commands configured, so it cannot run this project's "
"tests — set BOARD_AGENT_COMMANDS in manager/local/.env")
def _fresh_branch_point() -> tuple[str | None, str | None]:
"""Where a brand-new task branch should start: the newest main that
exists. With an `origin` remote, fetch its main (bounded by
@@ -429,8 +444,9 @@ def start_agent(filename: str, stage: str, takeover: bool = False) -> dict:
summary = (f"{name} is back on {filename} — continuing branch {branch}"
if continuing else
f"{name} started on {filename} (branch {branch})")
if base_note:
summary += f"{base_note}"
for note in (base_note, _no_commands_note()):
if note:
summary += f"{note}"
state.record_board_event({
"kind": "agent", "actor": "agent", "file": filename,
"summary": summary,
@@ -739,9 +755,13 @@ def start_pr_fix(filename: str, stage: str) -> dict:
}
with state.LOCK:
state.AGENTS[agent_id] = record
summary = f"{name} is acting on the review of {filename}'s PR"
note = _no_commands_note()
if note:
summary += f"{note}"
state.record_board_event({
"kind": "agent", "actor": "agent", "file": filename,
"summary": f"{name} is acting on the review of {filename}'s PR",
"summary": summary,
})
threading.Thread(target=_reap_pr_fix, args=(agent_id, proc, log_file),
daemon=True).start()
+23
View File
@@ -661,6 +661,7 @@
<span class="spacer"></span>
<div class="phasechips" id="phasechips" hidden></div>
<div class="livechip" id="syncchip" hidden style="cursor:default"></div>
<div class="livechip" id="cmdchip" hidden style="cursor:default"></div>
<div class="livechip" id="livechip" title="open Sessions"></div>
<button id="themebtn">Daylight</button>
<button id="refresh">Refresh</button>
@@ -1066,6 +1067,27 @@ function renderSync() {
`<span class="mono">${esc(detail.split(' — ')[0].replace(/^sync[^:]*:\s*/, ''))}</span>`;
}
/* The commands a headless agent may run are the one thing it cannot work
around: with none configured a work agent still edits and commits, but it
can never run this project's tests, and nothing said so until a run had
already ended saying it. Same shape as the drive's "no driver" — a quiet
statement of what this project has not set up, in the settled register,
never `--alarm`: nothing is failing here. A project that set it, by
detection or by hand, sees nothing at all. */
function renderAgentCommands() {
const el = $('#cmdchip');
const missing = S.state && S.state.hasAgentCommands === false;
el.hidden = !missing;
if (!missing) return;
el.title = 'Headless agents have no project commands to run here, so a ' +
"work agent cannot run this project's tests. Set BOARD_AGENT_COMMANDS " +
'in manager/local/.env — comma-separated command prefixes, e.g. ' +
'"npm test". Work still runs; the agent simply cannot check itself.';
el.innerHTML = `<span class="dot" style="background:var(--idle)"></span>` +
`<span style="color:var(--muted)">no agent commands</span>` +
`<span class="mono">BOARD_AGENT_COMMANDS</span>`;
}
/* ── a phase in flight ────────────────────────────────────────────────── */
/* The phases the header has something to say about: one running, or one
@@ -1214,6 +1236,7 @@ function render() {
renderPhases();
renderViews();
renderSync();
renderAgentCommands();
if (S.view === 'flight') renderFlight();
else if (S.view === 'focus') renderFocus();
else renderCards();
+10
View File
@@ -105,6 +105,16 @@ ADAPTER = setting("BOARD_AGENT_ADAPTER", "claude")
# the adapter's own knowledge; this list is the project's half.
AGENT_COMMANDS = setting("BOARD_AGENT_COMMANDS", "python3 -m unittest")
def agent_commands() -> list[str]:
"""The prefixes as an adapter reads them: comma-separated, blanks
dropped so whitespace, or a lone comma, is exactly nothing
configured. The adapters split the same string in their own standalone
copies of `split_commands()`; this is core's, and it is what the board
asks so the page and the launch can never disagree about *empty*."""
return [part.strip() for part in AGENT_COMMANDS.split(",") if part.strip()]
# Model per launch intent — an opaque vendor-native name core passes to the
# adapter untranslated (what names mean anything is vendor knowledge). Empty
# = inherit the vendor's own default, exactly today's behaviour. A per-intent
+4
View File
@@ -42,6 +42,10 @@ def state_payload() -> dict:
"phases": phases.public_state(),
"drive": drive.public(),
"hasDriver": config.driver_path() is not None,
# whether this project gave its headless agents anything to run. The
# one setting an agent cannot work around, so the board says it is
# missing rather than letting a run discover it — see the header chip
"hasAgentCommands": bool(config.agent_commands()),
"branches": github.task_branches(),
"commands": config.commands(),
"commandRuns": commands.public(),
+282
View File
@@ -0,0 +1,282 @@
"""A board whose agents cannot run anything says so (task 46).
`BOARD_AGENT_COMMANDS` is the one setting a headless agent cannot work
around: a test runner missing from it is a test the work agent cannot run.
Since the install stopped asking for it, a project the detector does not
recognise starts with it empty and until this card nothing mentioned that
until a run had already ended with an agent explaining it could not verify
its work.
Three places have to agree on *empty*: the splitter (core's, and the
standalone copies each adapter carries), the state payload the page reads,
and the launch that says it in the ticker. So the suite is one class each,
plus the page's own function run under node.
python3 -m unittest discover -s tests -v
"""
from __future__ import annotations
import importlib.util
import json
import re
import shutil
import subprocess
import sys
import unittest
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
CORE = REPO / "manager" / "core"
sys.path.insert(0, str(CORE))
import agents # noqa: E402
import config # noqa: E402
from tests.test_phase_runs import PhaseCase, card, git # noqa: E402
BOARD = CORE / "board.html"
NODE = shutil.which("node")
ALONE = "77-alone.md"
PR_URL = "https://github.com/acme/widget/pull/7"
# The forms of "nothing configured" a person can actually produce: never
# set, cleared, left as whitespace, or reduced to the separator.
NOTHING = ("", " ", "\t", ",", " , ", ",,", " ,\t,")
SOMETHING = {"npm test": ["npm test"],
" npm test ": ["npm test"],
"npm test, make check": ["npm test", "make check"],
"python3 -m unittest,": ["python3 -m unittest"]}
def _load(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class WhatCountsAsNothing(unittest.TestCase):
"""`config.agent_commands()` is the board's half of the split the
adapters already do. If the two disagreed, a board could show a chip a
launch contradicts, or stay silent about a run with nothing to run."""
def commands(self, raw: str) -> list[str]:
saved = config.AGENT_COMMANDS
self.addCleanup(setattr, config, "AGENT_COMMANDS", saved)
config.AGENT_COMMANDS = raw
return config.agent_commands()
def test_every_shape_of_empty_is_empty(self):
for raw in NOTHING:
self.assertEqual(self.commands(raw), [],
f"{raw!r} configures nothing")
def test_a_value_survives_stripped(self):
for raw, expected in SOMETHING.items():
self.assertEqual(self.commands(raw), expected)
def test_it_agrees_with_the_adapters(self):
"""Both shipped adapters carry a standalone `split_commands()` (the
hooks run outside the board's imports). Same answers, or the board
is guessing about somebody else's rules."""
splitters = [
_load("claude_hook_settings",
CORE / "adapters" / "claude" / "hook_settings.py").split_commands,
_load("opencode_permission_config",
CORE / "adapters" / "opencode" / "permission_config.py").split_commands,
]
for raw in (*NOTHING, *SOMETHING):
for split in splitters:
self.assertEqual(self.commands(raw), split(raw),
f"core and an adapter disagree about {raw!r}")
class TheStatePayloadSaysWhichItIs(unittest.TestCase):
"""One boolean, beside `hasDriver` — the same shape of fact, answered by
the server so the page never re-parses the setting itself."""
def flag(self, raw: str) -> bool:
import httpd
saved = config.AGENT_COMMANDS
self.addCleanup(setattr, config, "AGENT_COMMANDS", saved)
config.AGENT_COMMANDS = raw
return httpd.state_payload()["hasAgentCommands"]
def test_empty_is_reported_as_empty(self):
for raw in NOTHING:
self.assertFalse(self.flag(raw), f"{raw!r} is nothing configured")
def test_a_configured_board_reports_true(self):
self.assertTrue(self.flag("npm test"))
self.assertTrue(self.flag("python3 -m unittest"))
def test_the_page_does_not_read_the_raw_setting(self):
html = BOARD.read_text(encoding="utf-8")
self.assertNotIn("state.agentCommands", html)
self.assertIn("hasAgentCommands", html)
@unittest.skipUnless(NODE, "node is needed to run the page's own rules")
class TheHeaderChip(unittest.TestCase):
"""The indicator itself, run as the page runs it: a stub element in
place of the DOM, a state payload in place of the server."""
@classmethod
def setUpClass(cls):
cls.html = BOARD.read_text(encoding="utf-8")
match = re.search(r"function renderAgentCommands\(\) \{.*?\n\}",
cls.html, re.S)
if match is None:
raise AssertionError("board.html no longer defines renderAgentCommands")
cls.src = match.group(0)
def chip(self, state: dict | None) -> dict:
script = (
"const el = {hidden: null, title: '', innerHTML: ''};\n"
"function $(sel) { if (sel !== '#cmdchip') "
"throw new Error('unexpected ' + sel); return el; }\n"
"var S = " + json.dumps({"state": state}) + ";\n"
+ self.src + "\nrenderAgentCommands();\n"
"console.log(JSON.stringify(el));\n")
out = subprocess.run([NODE, "-e", script], capture_output=True, text=True)
self.assertEqual(out.returncode, 0, out.stderr)
return json.loads(out.stdout)
def test_an_empty_setting_is_said_once_naming_setting_and_file(self):
chip = self.chip({"hasAgentCommands": False})
self.assertFalse(chip["hidden"])
self.assertIn("BOARD_AGENT_COMMANDS", chip["innerHTML"] + chip["title"])
self.assertIn("manager/local/.env", chip["title"],
"a reader who has never opened that file has to be told "
"which file it is")
def test_a_configured_board_shows_nothing_at_all(self):
chip = self.chip({"hasAgentCommands": True})
self.assertTrue(chip["hidden"])
self.assertEqual(chip["innerHTML"], "",
"nothing is drawn, not even hidden")
def test_a_payload_that_does_not_say_is_not_guessed_at(self):
"""An older server, or the first frame before any state: the board
says nothing rather than accusing a project of being unconfigured."""
self.assertTrue(self.chip(None)["hidden"])
self.assertTrue(self.chip({})["hidden"])
def test_it_is_not_an_alarm(self):
"""Nothing is failing — something is unconfigured. `--alarm` is
reserved for blocked, failed or HIGH."""
self.assertNotIn("--alarm", self.src)
self.assertNotIn("--accent", self.src, "and nothing here is working")
self.assertIn("var(--idle)", self.src)
def test_the_chip_exists_and_is_drawn_every_frame(self):
self.assertIn('id="cmdchip"', self.html)
render = re.search(r"function render\(\) \{.*?\n\}", self.html, re.S).group(0)
self.assertIn("renderAgentCommands();", render)
class ALaunchWithNothingToRun(PhaseCase):
"""The sharper half: the moment it matters is the launch. It is a note
in the ticker beside the run's own line — never a refusal, because an
agent that only edits files is still useful."""
SETTING = "BOARD_AGENT_COMMANDS in manager/local/.env"
def setUp(self):
super().setUp()
self.write(ALONE, card("77 — On its own", status="In Progress"),
"in-progress")
def said(self) -> list[str]:
return [s for s in self.summaries() if self.SETTING in s]
def test_a_work_launch_says_it_and_still_runs(self):
self.patch(AGENT_COMMANDS="")
agents.start_agent(ALONE, "in-progress")
self.settle()
self.assertEqual(len(self.said()), 1, self.summaries())
self.assertIn("cannot run this project's tests", self.said()[0])
self.assertEqual(self.stage_of(ALONE), "review",
"the launch was not blocked: it ran, committed and landed")
def test_a_configured_board_never_mentions_it(self):
self.patch(AGENT_COMMANDS="npm test")
agents.start_agent(ALONE, "in-progress")
self.settle()
self.assertEqual(self.said(), [])
self.assertFalse([s for s in self.summaries()
if "BOARD_AGENT_COMMANDS" in s])
def test_whitespace_and_a_lone_comma_count_as_nothing_here_too(self):
for raw in (" ", ","):
with self.subTest(raw=raw):
self.patch(AGENT_COMMANDS=raw)
self.assertIsNotNone(agents._no_commands_note())
def test_the_note_rides_beside_the_branch_point_note(self):
"""Two things worth saying about one launch, one line: the note is
appended, it does not replace what the launch already said."""
self.patch(AGENT_COMMANDS="")
agents.start_agent(ALONE, "in-progress")
self.settle()
line = self.said()[0]
self.assertIn(f"started on {ALONE}", line)
self.assertIn(f"task/{ALONE[:-3]}", line)
def test_acting_on_a_pr_says_it_too(self):
"""↻ act on PR is a work agent with a push — same intent, same
commands, same silence to break."""
self.patch(AGENT_COMMANDS="")
text = card("77 — On its own", status="Review").replace(
"**Priority:** High\n", f"**Priority:** High\n**PR:** {PR_URL}\n")
(self.tasks / "in-progress" / ALONE).unlink()
self.write(ALONE, text, "review")
git(self.repo, "branch", f"task/{ALONE[:-3]}")
agents.start_pr_fix(ALONE, "review")
self.settle()
self.assertEqual(len(self.said()), 1, self.summaries())
self.assertIn("acting on the review", self.said()[0])
def test_a_read_only_kind_says_nothing(self):
"""`◔ still true?` never had the project's commands: telling it
about them would be noise on every card in every stage."""
self.patch(AGENT_COMMANDS="")
self.adapter_is("#!/usr/bin/env python3\n"
"print('RELEVANCE REVIEW: Still relevant')\n")
agents.start_review(ALONE, "in-progress")
self.settle()
self.assertEqual(self.said(), [])
class TheDocumentedTrade(unittest.TestCase):
"""The install stopped asking on purpose; AGENTS.md carries the reason,
so it has to carry what the board now does about it."""
@classmethod
def setUpClass(cls):
doc = (REPO / "AGENTS.md").read_text(encoding="utf-8")
cls.flat = re.sub(r"\s+", " ", doc.replace("**", "").replace("`", ""))
def test_the_indicator_is_written_down(self):
self.assertIn("no agent commands", self.flat)
self.assertIn("BOARD_AGENT_COMMANDS", self.flat)
def test_it_says_the_launch_is_not_blocked(self):
self.assertIn("never refuses a launch", self.flat)
if __name__ == "__main__":
unittest.main()