Lands Morgana's round 4 candidate,2b3e67e36(basec771a36f4, branch morgana/row213-r4), approved by Webby (webby-0071, REVIEW-r402bb0afb3, rev 2775). Manifest digest 26d4d0ecff56, 35 paths, plus Morgana's four packet files under agents/morgana/work/queue-213/ (base.txt, BUILD.md, candidate-manifest.sha256, mutants.py): 39 paths, blobs and modes from the candidate. No 213 path changed betweenc771a36f4and this commit's parent. Patch-id f313e69b6f7e, the same as c771a36f4..2b3e67e36. Evidence: Morgana's slot run on2b3e67e36(sage-1844): singles 191/0 and 31/31 mutants killed. Gate on my slot (sage-r195land, base38b83ea5cplus 195's 5 and 213's 39 paths, 09:45:52-09:57:52Z): business 200/0, bus 542/0, harness 140/0, cli 663/0; scripts auth 15/0, conductor 17/0, config 31/0, discord 78/0, extension-package 18/0, foundation 44/0, land-work 86/0, queue 156/0 plus 28/0, release 14/0, task 98/0; the models mutant L1 killed (launch-settings 15/1), restore matched. Between the gate base38b83ea5cand4364dddb0only docs/plans/2026-10-20_host-credentials.md changed (01e1fe512,4364dddb0), which no test reads. Row 195 landed as this commit's parent; it shares no path with 213. Webby's N2 (a host grant for a creative role) is a row 235 acceptance item (mos-1163), not a deferral.
92 lines
7.2 KiB
Python
92 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
# Row 213 (C2-08) mutants across the changed sources. Run from the row213 worktree root, in Sage's slot.
|
|
# Each mutant changes one anchor (count must be 1), runs the named test files alone, then restores.
|
|
import os, shutil, subprocess, sys
|
|
BAK = "/mnt/storage/scratch/morgana-r213/mutant-backups" # any scratch path outside the tree
|
|
BIZ = "packages/business/src/business.mjs"
|
|
LS = "packages/business/src/launch-settings.mjs"
|
|
IS = "packages/business/src/instance-settings.mjs"
|
|
ROLE = "packages/business/src/role.mjs"
|
|
BUNDLE = "packages/harness/src/bundle.mjs"
|
|
TOOLS = "packages/harness/src/tools.mjs"
|
|
BROKER = "packages/bus/src/broker.mjs"
|
|
LAUNCHER = "packages/cli/src/launcher.mjs"
|
|
PI = "adapters/pi/adapter.sh"
|
|
CLAUDE = "adapters/claude/adapter.sh"
|
|
T_BIZ = [("business", "tests/business.test.mjs")]
|
|
T_LS = [("business", "tests/launch-settings.test.mjs")]
|
|
T_IS = [("business", "tests/instance-settings.test.mjs")]
|
|
T_ROLE = [("business", "tests/role.test.mjs")]
|
|
T_HC = [("harness", "tests/creative.test.mjs")]
|
|
T_BUS = [("bus", "tests/creative.test.mjs")]
|
|
T_LAUNCH = [("cli", "tests/launcher.test.mjs")]
|
|
T_ADAPT = [("harness", "tests/creative.test.mjs"), ("harness", "tests/skills.test.mjs")]
|
|
ABS = ' *) PROMPT_FILE="$(pwd)/$PROMPT_FILE" ;;'
|
|
MUTANTS = [
|
|
# D1 (deviation): persona vars only on a creative definition, both or neither.
|
|
("D1a persona vars check not called", BIZ, " checkPersonaVars(out[instance].vars, entry.definition, at);\n", "", T_BIZ + T_IS),
|
|
("D1b any definition may carry persona vars", BIZ, " if (!CREATIVE_DEFINITIONS.includes(definition)) refuse(", " if (false) refuse(", T_BIZ + T_IS),
|
|
("D1c one persona var alone accepted", BIZ, " if (set.length === 1) refuse(", " if (false) refuse(", T_BIZ),
|
|
# D2 (deviation): the launcher hands buildBundle the persona directory.
|
|
("D2 launcher passes no persona directory", LAUNCHER, ' personaDir: join(configDir(env), "personas"),', " personaDir: null,", T_LAUNCH),
|
|
# M10: the human's settings.
|
|
("L1 models-key change by anyone", LS, " if (was !== is) creativeHumanOnly(", " if (false) creativeHumanOnly(", T_LS),
|
|
("L2 fallback.model key not checked", LS, ' for (const key of [vars.model, vars["fallback.model"]]) {', " for (const key of [vars.model]) {", T_LS),
|
|
("L3 instances key allowed for a creative instance", LS, " if (CREATIVE_DEFINITIONS.includes(next.roles[instance].definition)) {\n throw new OrganizationError(\"organization-creative-instance\"", " if (false) {\n throw new OrganizationError(\"organization-creative-instance\"", T_LS),
|
|
("L4 setRoutes by anyone", LS, " creativeHumanOnly(old.document, actor, instance, next.roles[instance].definition);", "", T_LS),
|
|
("L5 creativeHumanOnly never refuses", LS, " if (CREATIVE_DEFINITIONS.includes(definition) && actor !== document.human) {", " if (false) {", T_LS + T_IS),
|
|
("I1 add of a creative instance by anyone", IS, " creativeHumanOnly(old, actor, instance, args.definition);\n definitionFor(rolesDir, args.definition);\n next.roles[instance] =", " definitionFor(rolesDir, args.definition);\n next.roles[instance] =", T_IS),
|
|
("I2 change of a creative instance by anyone", IS, " creativeHumanOnly(old, actor, instance, entry.definition);\n", "", T_IS),
|
|
("I3 redefine to creative by anyone", IS, " creativeHumanOnly(old, actor, instance, args.definition);\n", "", T_IS),
|
|
# M1: role files.
|
|
("R1 checkCreative not called", ROLE, "CREATIVE_DEFINITIONS.includes(role.name) ? checkCreative(role) : role", "role", T_ROLE),
|
|
("R2 wider tools accepted", ROLE, " if (wider.length > 0) refuse(", " if (false) refuse(", T_ROLE),
|
|
("R3 non-human messaging accepted", ROLE, ' if (role.messaging[key].some((entry) => entry !== "human")) {', " if (false) {", T_ROLE),
|
|
# Persona file and block.
|
|
("B1 group/other mode bits accepted", BUNDLE, " if (stat.mode & 0o077) throw", " if (false) throw", T_HC),
|
|
("B2 digest not checked", BUNDLE, " if (sha256(data) !== digest) {", " if (false) {", T_HC),
|
|
("B3 headings and end tag accepted", BUNDLE, " if (text.includes(PERSONA_END) || /^##? /m.test(text)) {", " if (false) {", T_HC),
|
|
("B4 symlink followed", BUNDLE, "constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK", "constants.O_RDONLY | constants.O_NONBLOCK", T_HC),
|
|
("B5 character cap off", BUNDLE, " if ([...text].length > PERSONA_CHARS) throw", " if (false) throw", T_HC),
|
|
("B6 persona block dropped", BUNDLE, " ...personaSection(resolved, persona),\n", "", T_HC),
|
|
("B7 no personaDir with a digest accepted", BUNDLE, " if (digest === undefined) return null;\n throw new BundleError(", " return null;\n throw new BundleError(", T_HC),
|
|
("B8 creative role on harness-builtin web", BUNDLE, ' if (granted && source === "harness-builtin" && CREATIVE_DEFINITIONS.includes(resolved.definition)) {', " if (false) {", T_HC),
|
|
# M6.
|
|
("T1 raise_decision offered to creative roles", TOOLS, ' if (!CREATIVE_DEFINITIONS.includes(resolved.definition)) add("raise_decision"', ' add("raise_decision"', T_HC),
|
|
("K1 broker creative raise check off", BROKER, " if (request.verb === 'decision.raise' && !s.human && !s.tester &&\n CREATIVE_DEFINITIONS", " if (false &&\n CREATIVE_DEFINITIONS", T_BUS),
|
|
# B1 (webby-0040): the broker names the writer's actor from the session.
|
|
("K2 chief session reaches the writer as the human", BROKER, 'actor = this.#business(s.business).roles[s.role].definition + "." + s.role;', 'actor = "jason";', T_BUS),
|
|
("K3 tester session reaches the writer as the human", BROKER, 'let actor = s.tester ? "tester" : s.human;', 'let actor = s.tester ? "jason" : s.human;', T_BUS),
|
|
("V1 persona.name line separators accepted (N3)", "packages/business/src/vars.mjs", "\\u007f\\u2028\\u2029]", "\\u007f]", T_BIZ),
|
|
# Prompt by file (adapters).
|
|
("A1 pi prompt check without -f", PI, '[ -f "$PROMPT_FILE" ] && [ -r "$PROMPT_FILE" ] ||', '[ -r "$PROMPT_FILE" ] ||', T_ADAPT),
|
|
("A2 pi relative prompt path kept", PI, ABS, " *) ;;", T_ADAPT),
|
|
("A3 claude relative prompt path kept", CLAUDE, ABS, " *) ;;", T_ADAPT),
|
|
]
|
|
os.makedirs(BAK, exist_ok=True)
|
|
files = sorted({m[1] for m in MUTANTS})
|
|
orig = {}
|
|
for f in files:
|
|
bak = os.path.join(BAK, f.replace("/", "__"))
|
|
shutil.copyfile(f, bak)
|
|
orig[f] = open(bak).read()
|
|
if orig[f] != open(f).read(): sys.exit(f"backup mismatch for {f}, abort")
|
|
for name, f, a, b, _ in MUTANTS:
|
|
if orig[f].count(a) != 1: sys.exit(f"{name}: anchor count {orig[f].count(a)}, abort")
|
|
killed = 0
|
|
try:
|
|
for name, f, a, b, tests in MUTANTS:
|
|
open(f, "w").write(orig[f].replace(a, b))
|
|
failing = []
|
|
for pkg, t in tests:
|
|
p = subprocess.run(["node", "--test", "--test-reporter=tap", t], cwd=f"packages/{pkg}", capture_output=True, text=True, timeout=600)
|
|
failing += [f"{pkg}/{t.split('/')[-1]}: {l[7:]}" for l in p.stdout.splitlines() if l.startswith("not ok")]
|
|
open(f, "w").write(orig[f])
|
|
status = "KILLED" if failing else "SURVIVED"
|
|
killed += bool(failing)
|
|
print(f"{status} {name}", flush=True)
|
|
for line in failing: print(f" {line}", flush=True)
|
|
finally:
|
|
for f in files: open(f, "w").write(orig[f])
|
|
print(f"killed {killed}/{len(MUTANTS)}; restored: {all(open(f).read() == orig[f] for f in files)}")
|