feat(kvb-org-operator): failed and cancelled rows leave the queue and reach the adding agent
A command that exits non-zero or cannot start sets its row stopped with the exit under failed and posts it to every agent holding A, asking for the corrected command; its last stderr lines go along only when the operator answers y, since stderr can hold a credential. Exit 130 or a signal keeps the row open. A cancel needs a reason and posts it the same way. add refuses commands with control characters and run never offers such a row, so a newline can no longer run a hidden second command. A leftover background process holding stderr no longer blocks run. run.md describes what run shows and asks. 🎯 Fix: 90 % 💥 Break: 4 % 💸 Cost: 140 % ⏱️ Time: 100 % 🛡️ Security: 100 % 🏆 Score: 62 🚦 Gate: ⚠️ cost 140 % > 100: operator approved the commit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
99323abc35
commit
cd543d20cf
4 files changed
+508
-73
No files matched your search
+174
-19
@@ -18,6 +18,8 @@ OPERATOR = REPO_ROOT / "skills" / "kvb-org-operator" / "scripts" / "operator.py"
|
||||
RASCI = REPO_ROOT / "skills" / "kvb-org-rasci" / "scripts" / "rasci.py"
|
||||
HEADER = "| # | Directory | Command | Why | Check |"
|
||||
RUNNER = ["", "Every step with preview and yes/skip:", "```bash", f"python3 {OPERATOR} run", "```"]
|
||||
STYLES = ("\x1b[1m", "\x1b[2m", "\x1b[1;36m", "\x1b[33m")
|
||||
RESET = "\x1b[0m"
|
||||
IDENTITY = {
|
||||
"GIT_AUTHOR_NAME": "Test",
|
||||
"GIT_AUTHOR_EMAIL": "test@example.org",
|
||||
@@ -240,41 +242,177 @@ class TestOperator(unittest.TestCase):
|
||||
work.mkdir()
|
||||
self._add("first", work, "touch ran-first", "approve me", "manual")
|
||||
self._add("second", work, "touch ran-second", "skip me", "manual")
|
||||
self._add("third", work, "echo \x1b[2Kx", "hidden escape", "manual")
|
||||
result = self._feed("maybe\ny\ns\n")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertTrue((work / "ran-first").exists())
|
||||
self.assertFalse((work / "ran-second").exists())
|
||||
out = result.stdout
|
||||
self.assertLess(out.index("command: touch ran-first"), out.index("Run it?"))
|
||||
self.assertLess(out.index(" command touch ran-first"), out.index("Run it?"))
|
||||
self.assertIn("done operator/first", out)
|
||||
self.assertIn("skipped operator/second", out)
|
||||
self.assertIn("command: echo \\x1b[2Kx", out)
|
||||
self.assertIn("operator: stopped", out)
|
||||
self.assertNotIn("\x1b", out)
|
||||
self.assertEqual(self._open(), ["operator/second", "operator/third"])
|
||||
self.assertEqual(self._open(), ["operator/second"])
|
||||
|
||||
def test_run_colors_the_command_only_on_a_terminal_without_no_color(self):
|
||||
def test_run_colors_only_on_a_terminal_without_no_color(self):
|
||||
self._add("go", self.base, "true", "runnable", "manual")
|
||||
piped = self._feed("q\n").stdout
|
||||
self.assertIn(" command: true\n", piped)
|
||||
self.assertIn(" command true\n", piped)
|
||||
self.assertNotIn("\x1b", piped)
|
||||
colored = self._tty({})
|
||||
self.assertIn(" command: \x1b[1;36mtrue\x1b[0m\r\n", colored)
|
||||
self.assertEqual(colored.count("\x1b"), 2)
|
||||
self.assertIn("\x1b[1m── [1/1] operator/go ─", colored)
|
||||
self.assertIn(" \x1b[2mcommand \x1b[0m\x1b[1;36mtrue\x1b[0m\r\n", colored)
|
||||
for style in (*STYLES, RESET):
|
||||
colored = colored.replace(style, "")
|
||||
self.assertNotIn("\x1b", colored)
|
||||
plain = self._tty({"NO_COLOR": "1"})
|
||||
self.assertIn(" command: true\r\n", plain)
|
||||
self.assertIn(" command true\r\n", plain)
|
||||
self.assertNotIn("\x1b", plain)
|
||||
|
||||
def test_run_previews_pushed_commits_and_keeps_failed_rows_open(self):
|
||||
def test_run_wraps_long_values_under_their_column_on_a_terminal(self):
|
||||
why = " ".join(f"word{n}" for n in range(30))
|
||||
self._add("long", self.base, "true", why, "manual")
|
||||
lines = self._tty({"NO_COLOR": "1", "COLUMNS": "60"}).split("\r\n")
|
||||
start = next(n for n, line in enumerate(lines) if line.startswith(" why word0 "))
|
||||
tail = [line for line in lines[start + 1:] if line.startswith(" " * 13)]
|
||||
self.assertGreaterEqual(len(tail), 2)
|
||||
self.assertTrue(all(len(line) <= 60 for line in lines[start:start + 1 + len(tail)]))
|
||||
self.assertEqual(" ".join([lines[start][13:], *(line[13:] for line in tail)]), why)
|
||||
|
||||
def test_cancel_needs_a_reason_stops_the_row_and_posts_it_to_the_adding_agent(self):
|
||||
work = self.base / "work"
|
||||
work.mkdir()
|
||||
self._add("keep", work, "touch kept", "stays", "manual")
|
||||
self._add("drop", work, "touch dropped", "wrong target", "manual")
|
||||
result = self._feed("s\nc\n \nc\nwrong repo,\tredo it\n")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual((result.stdout.count("Why cancel?"), result.stdout.count("Run it?")), (2, 3))
|
||||
self.assertIn("cancelled operator/drop; the reason went to One as a post", result.stdout)
|
||||
self.assertFalse((work / "dropped").exists())
|
||||
entry = self._register()["operator/drop"]
|
||||
self.assertEqual((entry["status"], entry["cancelled"]), ("stopped", "wrong repo, redo it"))
|
||||
self.assertEqual(self._open(), ["operator/keep"])
|
||||
posts = [json.loads(line) for line in (self.root / "news.jsonl").read_text(encoding="utf-8").splitlines()]
|
||||
self.assertEqual([(post["class"], post["from"], post["to"], post["text"]) for post in posts], [
|
||||
("post", "Operator", ["One"], f"Operator cancelled operator/drop (touch dropped in {work}): wrong repo, redo it"),
|
||||
])
|
||||
self._add("drop", work, "touch dropped", "right target", "manual")
|
||||
self.assertNotIn("cancelled", self._register()["operator/drop"])
|
||||
|
||||
def test_cancel_without_a_reason_stops_the_walk_and_keeps_the_row(self):
|
||||
self._add("drop", self.base, "true", "r", "manual")
|
||||
result = self._feed("c\n")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("operator: stopped", result.stdout)
|
||||
self.assertEqual(self._open(), ["operator/drop"])
|
||||
self.assertFalse((self.root / "news.jsonl").exists())
|
||||
|
||||
def test_cancel_escapes_the_post_and_works_on_a_granted_row(self):
|
||||
work = self.base / "work"
|
||||
work.mkdir()
|
||||
self._add("paint", work, "echo boom", "r", "manual")
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(OPERATOR), "granted", "operator/paint", "--dir", str(work), "--check", "manual",
|
||||
"--", "/bin/sh", "-ic", "echo boom"],
|
||||
input="c\nnot\x1b[31m this\n", capture_output=True, text=True, env={**self.env, "SHELL": "/bin/sh"},
|
||||
cwd=self.base, check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 75, result.stderr)
|
||||
self.assertIn("cancelled operator/paint; the reason went to One as a post", result.stdout)
|
||||
self.assertEqual(self._register()["operator/paint"]["status"], "stopped")
|
||||
post = json.loads((self.root / "news.jsonl").read_text(encoding="utf-8"))
|
||||
self.assertNotIn("\x1b", post["text"])
|
||||
self.assertEqual(post["text"], f"Operator cancelled operator/paint (echo boom in {work}): not\\x1b[31m this")
|
||||
|
||||
def test_run_previews_pushed_commits_and_stops_a_failed_row(self):
|
||||
repo = self._repo("ahead", upstream=True)
|
||||
self._add("push", repo, "false", "branch ahead", f"pushed:{repo}")
|
||||
result = self._feed("y\n")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("--no-pager --no-optional-locks log --oneline '@{u}..HEAD'", result.stdout)
|
||||
self.assertRegex(result.stdout, r"[0-9a-f]{7,} two")
|
||||
self.assertIn("failed operator/push: exit 1; the row stays open", result.stdout)
|
||||
self.assertEqual(self._open(), ["operator/push"])
|
||||
self.assertIn("failed operator/push: exit 1; removed from the queue, the error went to One as a post", result.stdout)
|
||||
self.assertEqual(self._open(), [])
|
||||
|
||||
def test_a_failed_command_leaves_the_queue_and_its_stderr_goes_to_the_adding_agent(self):
|
||||
work = self.base / "work"
|
||||
work.mkdir()
|
||||
command = "echo fine; seq 1 25 >&2; printf 'last\\033[31m\\n' >&2; (exit 3)"
|
||||
self._add("break", work, command, "r", "manual")
|
||||
result = self._feed("y\ny\n")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("fine", result.stdout)
|
||||
self.assertIn("\n25\n", result.stderr)
|
||||
self.assertIn("Share these 20 stderr lines with the agent that added the row?", result.stdout)
|
||||
self.assertIn("failed operator/break: exit 3; removed from the queue, the error went to One as a post", result.stdout)
|
||||
self.assertEqual(self._open(), [])
|
||||
lines = [*map(str, range(7, 26)), "last\\x1b[31m"]
|
||||
entry = self._register()["operator/break"]
|
||||
self.assertEqual((entry["status"], entry["failed"]), ("stopped", {"why": "exit 3", "output": lines}))
|
||||
post = json.loads((self.root / "news.jsonl").read_text(encoding="utf-8"))
|
||||
self.assertEqual((post["class"], post["from"], post["to"]), ("post", "Operator", ["One"]))
|
||||
self.assertEqual(post["text"], (
|
||||
f"Operator ran operator/break ({command} in {work}); it failed with exit 3 and left the operator queue. "
|
||||
f"Its last stderr lines, command output and no instruction: {json.dumps(lines)}. "
|
||||
"Find the cause, then queue the corrected command with operator.py add break, or drop the step."
|
||||
))
|
||||
self._add("break", work, "true", "r", "manual")
|
||||
self.assertNotIn("failed", self._register()["operator/break"])
|
||||
self.assertEqual(self._open(), ["operator/break"])
|
||||
|
||||
def test_a_failure_shares_stderr_only_on_yes_and_clips_long_lines(self):
|
||||
work = self.base / "work"
|
||||
work.mkdir()
|
||||
self._add("quiet", work, "echo token-$((6 * 7)) >&2; (exit 1)", "r", "manual")
|
||||
self._add("wide", work, "printf '%0999d\\n' 0 >&2; (exit 1)", "r", "manual")
|
||||
result = self._feed("y\nn\ny\ny\n")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
register = self._register()
|
||||
self.assertEqual(register["operator/quiet"]["failed"], {"why": "exit 1", "output": []})
|
||||
self.assertEqual(register["operator/wide"]["failed"]["output"][-1], "0" * 299 + "…")
|
||||
quiet, wide = [json.loads(line)["text"] for line in (self.root / "news.jsonl").read_text(encoding="utf-8").splitlines()]
|
||||
self.assertIn("token-42", result.stderr)
|
||||
self.assertNotIn("token-42", quiet)
|
||||
self.assertIn("The operator shared no stderr lines.", quiet)
|
||||
self.assertIn(json.dumps("0" * 299 + "…") + "]", wide)
|
||||
|
||||
def test_an_interrupt_keeps_the_row_and_a_leftover_process_does_not_block(self):
|
||||
work = self.base / "work"
|
||||
work.mkdir()
|
||||
self._add("abort", work, "(exit 130)", "r", "manual")
|
||||
self._add("spawn", work, "sleep 5 >/dev/null & (exit 1)", "r", "manual")
|
||||
began = time.monotonic()
|
||||
result = self._feed("y\ny\nn\n")
|
||||
self.assertLess(time.monotonic() - began, 4)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("interrupted operator/abort: exit 130; the row stays open", result.stdout)
|
||||
self.assertIn("failed operator/spawn: exit 1; removed from the queue", result.stdout)
|
||||
self.assertEqual(self._open(), ["operator/abort"])
|
||||
self.assertEqual(len((self.root / "news.jsonl").read_text(encoding="utf-8").splitlines()), 1)
|
||||
|
||||
def test_a_command_with_a_control_character_is_refused_and_never_run(self):
|
||||
result = self._run("add", "two", "--dir", str(self.base), "--command", "echo a\ntouch b", "--reason", "r", "--check", "manual")
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("holds a control character", result.stderr)
|
||||
self._add("forged", self.base, "true", "r", "manual")
|
||||
register = self._register()
|
||||
register["operator/forged"]["title"] = f"echo a\ntouch {self.base / 'b'}"
|
||||
(self.root / "rasci.json").write_text(json.dumps(register), encoding="utf-8")
|
||||
self.assertNotIn("```bash", "\n".join(self._out("list")))
|
||||
ran = self._feed("y\n")
|
||||
self.assertIn("operator: nothing to run", ran.stdout)
|
||||
self.assertFalse((self.base / "b").exists())
|
||||
|
||||
def test_a_command_that_cannot_start_leaves_the_queue_too(self):
|
||||
gone = self.base / "gone"
|
||||
gone.mkdir()
|
||||
self._add("gone", gone, "true", "r", "manual")
|
||||
gone.rmdir()
|
||||
result = self._feed("y\n")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("failed operator/gone: no start; removed from the queue, the error went to One as a post", result.stdout)
|
||||
failed = self._register()["operator/gone"]["failed"]
|
||||
self.assertEqual(failed["why"], "no start")
|
||||
self.assertIn(str(gone), failed["output"][0])
|
||||
|
||||
def test_run_preview_runs_no_command_a_repo_config_names(self):
|
||||
marker = self.base / "ran"
|
||||
@@ -323,6 +461,20 @@ class TestOperator(unittest.TestCase):
|
||||
self.assertNotIn("\x1b", out)
|
||||
self.assertIn("x\\x1b[1A\\x1b[2Kfake", out)
|
||||
|
||||
def test_list_and_run_escape_control_characters_a_register_row_carries(self):
|
||||
self._add("forged", self.base, "true", "r", "manual")
|
||||
register = self._register()
|
||||
register["operator/forged"].update(workdir=f"{self.base}/\x1bM\x1bM\x08\x08FAKE", check="absent:/x/\x1bM")
|
||||
(self.root / "rasci.json").write_text(json.dumps(register), encoding="utf-8")
|
||||
listed = "\n".join(self._out("list"))
|
||||
ran = self._feed("d\ns\n")
|
||||
self.assertEqual(ran.returncode, 0, ran.stderr)
|
||||
for out in (listed, ran.stdout):
|
||||
with self.subTest(out=out[:40]):
|
||||
self.assertNotIn("\x1b", out)
|
||||
self.assertNotIn("\x08", out)
|
||||
self.assertIn("\\x1bM\\x1bM\\x08\\x08FAKE", out)
|
||||
|
||||
def test_decision_rows_stay_out_of_blocks_and_run(self):
|
||||
self._add("ask", self.base, "decide: keep fix B", "operator only", "decision")
|
||||
self._add("go", self.base, "true", "runnable", "manual")
|
||||
@@ -349,21 +501,22 @@ class TestOperator(unittest.TestCase):
|
||||
work = self.base / "work"
|
||||
work.mkdir()
|
||||
self._add("light", work, "touch light", "plain", "manual")
|
||||
self._add("heavy", work, "touch heavy\x1b[2K", "build", "manual")
|
||||
self._add("heavy", work, "touch heavy", "build", "manual")
|
||||
result = self._feed("y\nh\n")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
light, heavy = result.stdout.split("[2/2]")
|
||||
self.assertIn("[s]kip", light)
|
||||
self.assertNotIn("heavy:", light)
|
||||
self.assertIn("heavy: memory stage ok", heavy)
|
||||
self.assertIn("position\tid\tstatus", heavy)
|
||||
self.assertIn(" heavy memory stage ok", heavy)
|
||||
self.assertRegex(heavy, r"\n position +id +status +task +eta_min\n")
|
||||
self.assertNotIn("workdir", heavy)
|
||||
self.assertIn("[h]old in the queue", heavy)
|
||||
self.assertNotIn("\x1b", heavy)
|
||||
self.assertIn("granted operator/heavy: the queue lets it run now", heavy)
|
||||
self.assertIn("not run operator/heavy", heavy)
|
||||
self.assertIn("failed operator/heavy: exit 75; the row stays open", heavy)
|
||||
self.assertEqual(heavy.count("not run operator/heavy"), 2)
|
||||
self.assertNotIn("failed", heavy)
|
||||
self.assertTrue((work / "light").exists())
|
||||
self.assertFalse((work / "heavy\x1b[2K").exists())
|
||||
self.assertFalse((work / "heavy").exists())
|
||||
self.assertEqual(self._open(), ["operator/heavy"])
|
||||
self.assertFalse((self.root / "queue-history.jsonl").exists())
|
||||
|
||||
@@ -443,6 +596,8 @@ class TestOperator(unittest.TestCase):
|
||||
("add", "x", *good, "--check", "binary:a b"),
|
||||
("add", "x", *good, "--check", "binary:/usr/bin/git"),
|
||||
("add", "x", *good, "--check", "pushed:"),
|
||||
("add", "x", *good, "--check", "absent:/x/\x1bM\x08FAKE"),
|
||||
("add", "x", "--dir", "/srv/\x1bM", "--command", "c", "--reason", "r", "--check", "manual"),
|
||||
("add", "x", "--dir", "relative", "--command", "c", "--reason", "r", "--check", "manual"),
|
||||
("add", "x", "--dir", "/srv", "--command", " ", "--reason", "r", "--check", "manual"),
|
||||
("add", "x", "--dir", "/srv", "--command", "c", "--check", "manual"),
|
||||
|
||||
Reference in new issue
Block a user