7a58a530b1
The selector forced every task through the heaviest methodology's
critical path: a behaviour-preserving, type-enumerable change paid the
same specify -> planner -> implement front-half as a novel feature,
because it was neither new behaviour (tdd) nor an observed bug (debug)
and so fell to specify by elimination. Two coupled defects — a selector
with no verification axis, and an all-or-nothing executor — kept the
existing lighter path unreachable and uneconomical. This fixes both.
Part A — verification-keyed selector (boss/SKILL.md):
- Replace the three-way "design line" with an ordered cascade that adds
a verification/enumeration axis ahead of the settled-vs-fork question.
Each lighter arm carries a positive trigger matched by signature, not
reached by elimination.
- New `compiler-driven` arm: a type/signature edit at a definition site
that propagates mechanically. Observe-then-bounce — make the edit,
build, run the suite; clean build AND suite green unchanged commits;
a hole bounces up (specify for a design choice, tdd for discovered
test-specifiable new behaviour); a regression bounces to debug.
- The observed-bug RED-first gate is first in the cascade, so a
mechanical-looking fix cannot bypass it.
- The straddle rule ("add an enum variant") is codified as a rule:
mechanical/forwarding -> compiler-driven; encodes new behaviour ->
tdd/spec; doubt routes up.
- The executor is the elevated inline carve-out plus a shipped workflow,
not a heavy new skill ("the largest concrete win is small").
Part B — Workflow substrate (implement/workflows/):
- implement-loop.js: the per-task loop as a deterministic script. Each
phase (implementer -> spec-compliance -> quality, + tester for E2E) is
a separate top-level agent() call, so a single phase is independently
invokable and inter-phase aggregation/re-loop is code. Retires the
implement-orchestrator agent's inline-role-switch workaround (the four
phase agents survive as the agent-types the script dispatches).
- compiler-driven-edit.js: the observe-then-bounce loop.
- install.sh / uninstall.sh symlink shipped workflows into
~/.claude/workflows/.
- specify and brainstorm stay prose + interactive (human-intent oracle);
only the autonomous/mechanical loops moved. try-and-error is deferred.
Docs (pipeline taxonomy, design, agent-template, migration, README) and
all selector<->executor cross-references updated; the arm and its
executor are co-located so a future re-route through the full loop is a
visible regression.
Verified by an adversarial multi-agent pass: PASS on all six acceptance
criteria; two coherence concerns fixed. The shipped scripts are
syntax-validated but exercised only in a downstream target project (the
skills repo is not itself a pipeline target).
closes #7
126 lines
4.0 KiB
Python
126 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Executable spec for postmortem.subagent_terminal_status.
|
|
|
|
Run directly: `python3 postmortem/tests/test_terminal_status.py`.
|
|
Exits 0 on pass, 1 on failure. No pytest dependency — the plugin has
|
|
no test harness, and a status-classifier is small enough to pin with
|
|
plain asserts.
|
|
|
|
Pins issue #3: the terminal-status text-scan over-reports BLOCKED for
|
|
shipped implement runs, because `\\bBLOCKED\\b` fires on the end-report's
|
|
own template lines (`BLOCKED file: BLOCKED.md`, `Blocked detail:`)
|
|
and on prose narrating a surmounted blocker. The authoritative signal is
|
|
the structured `Status:` line of the end-report, not any keyword
|
|
occurrence in the body.
|
|
"""
|
|
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
SCRIPT = os.path.join(HERE, "..", "scripts", "postmortem.py")
|
|
|
|
spec = importlib.util.spec_from_file_location("postmortem", SCRIPT)
|
|
pm = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(pm)
|
|
|
|
|
|
def write_subagent_log(final_text):
|
|
"""Write a one-assistant-turn subagent jsonl whose last text block is
|
|
`final_text`, return its path. Caller cleans up."""
|
|
fd, path = tempfile.mkstemp(suffix=".jsonl")
|
|
rec = {"type": "assistant",
|
|
"message": {"role": "assistant",
|
|
"content": [{"type": "text", "text": final_text}]}}
|
|
with os.fdopen(fd, "w") as fh:
|
|
fh.write(json.dumps(rec) + "\n")
|
|
return path
|
|
|
|
|
|
def status_of(final_text):
|
|
path = write_subagent_log(final_text)
|
|
try:
|
|
return pm.subagent_terminal_status(path)
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
|
|
# A realistic plain-text iteration end-report that SHIPPED (Status: DONE)
|
|
# but whose body carries the template's BLOCKED-bearing lines. This is the
|
|
# exact shape that issue #3 reports as misclassified.
|
|
SHIPPED_DONE_REPORT = """\
|
|
Status: DONE
|
|
Iter: 0126-leg3
|
|
Started from: a1b2c3d
|
|
Tasks completed: 3 of 3
|
|
- poly RawBuf elem-var mono fix
|
|
- series kernel module
|
|
- owned-param tail-recursion leak
|
|
Working tree: dirty (7 files changed)
|
|
BLOCKED file: BLOCKED.md (uncommitted; only on PARTIAL/BLOCKED)
|
|
Stats: .claude/stats/0126.json (uncommitted)
|
|
Files touched: 7
|
|
Tests: 12 green, 0 red
|
|
E2E coverage: none (mini mode)
|
|
Blocked detail: (only if BLOCKED or PARTIAL — also written to BLOCKED.md)
|
|
|
|
Note: task 2 hit a blocker early (missing fixture) which was resolved
|
|
in-phase; no BLOCKED.md was written.
|
|
"""
|
|
|
|
GENUINE_BLOCKED_REPORT = """\
|
|
Status: BLOCKED
|
|
Iter: 0127
|
|
Started from: d4e5f6a
|
|
Tasks completed: 1 of 3
|
|
Working tree: dirty (2 files changed)
|
|
BLOCKED file: BLOCKED.md (uncommitted)
|
|
Blocked detail: Task: 2
|
|
Reason: review-loop-exhausted
|
|
Worker says: spec-compliance never reached approved
|
|
"""
|
|
|
|
PARTIAL_REPORT = """\
|
|
Status: PARTIAL
|
|
Iter: 0128
|
|
Tasks completed: 2 of 3
|
|
BLOCKED file: BLOCKED.md (uncommitted)
|
|
"""
|
|
|
|
CASES = [
|
|
# (name, final_text, expected)
|
|
("shipped DONE with BLOCKED template lines (issue #3)",
|
|
SHIPPED_DONE_REPORT, "DONE"),
|
|
("genuine BLOCKED end-report",
|
|
GENUINE_BLOCKED_REPORT, "BLOCKED"),
|
|
("PARTIAL end-report mentioning BLOCKED.md",
|
|
PARTIAL_REPORT, "PARTIAL"),
|
|
("narrating agent whose final line is a bare status token",
|
|
"Work complete, tests green.\n\nDONE", "DONE"),
|
|
("prose mentioning a resolved blocker, no status line",
|
|
"I was briefly BLOCKED by a missing import but fixed it. All good.",
|
|
"unknown"),
|
|
]
|
|
|
|
|
|
def main():
|
|
failures = []
|
|
for name, text, expected in CASES:
|
|
got = status_of(text)
|
|
ok = got == expected
|
|
print(f"[{'PASS' if ok else 'FAIL'}] {name}: expected {expected!r}, got {got!r}")
|
|
if not ok:
|
|
failures.append(name)
|
|
if failures:
|
|
print(f"\n{len(failures)} failure(s).")
|
|
return 1
|
|
print(f"\nAll {len(CASES)} cases passed.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|