Files
stack/agents/researcher/work/queue-88/r1/mutants.py
T
jason.woltjeandClaude Opus 5.5 f0bab46e9a researcher: row 88 round 1 packet, Haiku subagent build on 8e0fbca4 (#1582)
Five paths: tasks fields.mjs and fields.test.mjs, webui README, public/bus.js
(aria-label only) and bus-browser.test.mjs. Phase B: 14 node suites and 9
scripts green, 8 of 8 mutants killed. The literal U+202E is now an escape.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 11:16:12 -05:00

130 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""Row 88 (#1582) mutants, r1. Written in phase A; NOT RUN in phase A.
Run only after Sage sends "slot clear" and the suites pass:
python3 -I agents/researcher/work/queue-88/r1/mutants.py <worktree-root>
Optional: --only <id> runs one mutant after the baseline.
Each mutant is one exact text replacement that must match exactly once. The killer
command runs with the mutant in place. The file is restored in a finally block and
its bytes are checked against the original. Each run has its own process group, and
the group is killed on exit and on timeout. A timeout is reported as a gate failure,
never as a kill. Baseline: every killer must pass unmutated, or nothing runs.
"""
import hashlib
import os
import pathlib
import signal
import subprocess
import sys
WEBUI_BROWSER = ['node', '--test', 'tests/bus-browser.test.mjs']
TASKS_FIELDS = ['node', '--test', 'tests/fields.test.mjs']
TIMEOUT = 600
MUTANTS = [
dict(id='M1', file='packages/webui/src/public/bus.js',
old='aria-label="Who may write each field, scroll for all columns"',
new='aria-label="Who may write each field"',
cwd='packages/webui', argv=WEBUI_BROWSER, why='item 1: aria-label suffix'),
dict(id='M2', file='packages/tasks/src/fields.mjs',
old='if (!Object.hasOwn(table.verbs, w.verb))',
new='if (!(w.verb in table.verbs))',
cwd='packages/tasks', argv=TASKS_FIELDS, why='item 7: inherited verb key accepted'),
dict(id='M3', file='packages/webui/src/public/bus.js',
old='${txt(f.label)}${f.note',
new='${f.label}${f.note',
cwd='packages/webui', argv=WEBUI_BROWSER, why='item 8: label not escaped'),
dict(id='M3b', file='packages/webui/src/public/bus.js',
old='${txt(f.note)}</span>',
new='${f.note}</span>',
cwd='packages/webui', argv=WEBUI_BROWSER, why='item 8: note not escaped'),
dict(id='M4', file='docs/plans/2026-10-04_slice-1.md',
old='| due date | pm | `task.create`, `task.schedule` |',
new='| due date | pm | `task.create`, `task.assign` |',
cwd='packages/tasks', argv=TASKS_FIELDS, why='item 5: prose verb edit'),
dict(id='M5', file='packages/tasks/src/verbs.mjs',
old="if (PM_VERBS.has(verb) && mine.definition !== 'pm') fail('field-writer');",
new="if (PM_VERBS.has(verb) && mine.definition !== 'pm' && mine.definition !== 'cto') fail('field-writer');",
cwd='packages/tasks', argv=TASKS_FIELDS, why='item 6: cto passes the pm writer check'),
dict(id='M6', file='packages/webui/README.md',
old='names the file or the key at fault, never a path',
new='names the file and never a path',
cwd='packages/webui', argv=WEBUI_BROWSER, why='item 2: README wording reverted'),
dict(id='M9', file='packages/bus/src/broker.mjs',
old="const route = cls === 'cross-role' && proposedRoute === s.role ? 'human' : proposedRoute;",
new='const route = proposedRoute;',
cwd='packages/tasks', argv=TASKS_FIELDS, why='item 6: arbiter own decision not sent to human'),
]
def sha(data):
return hashlib.sha256(data).hexdigest()
def run(argv, cwd, timeout):
proc = subprocess.Popen(argv, cwd=cwd, start_new_session=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
try:
out, _ = proc.communicate(timeout=timeout)
return proc.returncode, out.decode(errors='replace')
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL)
out, _ = proc.communicate()
return 'timeout', out.decode(errors='replace')
finally:
try:
os.killpg(proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
def main():
root = pathlib.Path(sys.argv[1]).resolve()
only = sys.argv[3] if len(sys.argv) > 3 and sys.argv[2] == '--only' else None
seen = set()
for m in MUTANTS:
key = (m['cwd'], tuple(m['argv']))
if key in seen:
continue
seen.add(key)
rc, _ = run(m['argv'], root / m['cwd'], TIMEOUT)
if rc != 0:
print(f"BASELINE FAILED {m['cwd']} {' '.join(m['argv'])} rc={rc}; no mutant was run")
return 2
print(f"baseline ok {m['cwd']} {' '.join(m['argv'])}")
results = []
for m in MUTANTS:
if only and m['id'] != only:
continue
path = root / m['file']
orig = path.read_bytes()
text = orig.decode()
if text.count(m['old']) != 1:
results.append((m['id'], 'ANCHOR', f"old text matches {text.count(m['old'])} times"))
continue
path.write_bytes(text.replace(m['old'], m['new'], 1).encode())
try:
rc, _ = run(m['argv'], root / m['cwd'], TIMEOUT)
finally:
path.write_bytes(orig)
if sha(path.read_bytes()) != sha(orig):
print(f"RESTORE FAILED {m['file']}; stop and report")
return 3
if rc == 'timeout':
verdict = 'TIMEOUT (gate failure, not a kill)'
elif rc == 0:
verdict = 'SURVIVED'
else:
verdict = 'KILLED'
results.append((m['id'], verdict, m['why']))
print(f"{m['id']}: {verdict} ({m['why']})")
print('---')
for r in results:
print(*r)
return 0
if __name__ == '__main__':
sys.exit(main())