fix(issue-6): fail-closed registry timeout and TUI turn handshake

- Introduce finalize_unfinished in multi-agent-mux-delegate-job for fail-closed atomic transitions
- Enforce CAS dispatch_token and attempt_id pinning across engine, CLI, and lifecycle scripts
- Integrate Herdr wait_for_agent_idle turn handshake to prevent prompt loss and race conditions
- Resolve bash 3.2 set -u empty array expansion compatibility in lib.sh
- Preserve Issue #5 R2 contract in CAS transition engine (engine.py, publish_event.py, service.py)
- Add comprehensive regression suites (test_issue6_fail_closed.py, test_issue6_turn_handshake.py)
- Bump lockstep framework and skills to v4.4.1 across lib.sh, eager.py, VERSIONS.md, 8 SKILL.md
- Synchronize documentation (IMPROVEMENTS.md, MESSAGING.md, state-transitions.md, task.md, phase5-e2e.md)
- Unanimous PASS achieved across 4 reviewer team leaders (Claude, Codex, OpenCode, Grok)

Closes #6
This commit is contained in:
Godopu committed 2026-09-09 14:02:47 +09:00
1 parent 7b761be7b8
commit ec61511699
51 files changed
+3477 -293

No files matched your search

+154 -11
View File
@@ -469,7 +469,8 @@ elif cmd1 == "agent":
agents_list.append({
"name": name,
"agent": data.get("agent", "claude"),
"agent_status": data.get("status", "running"),
"agent_status": data.get("agent_status", "idle"),
"status": data.get("status", "running"),
"cwd": data.get("cwd", ""),
"pane_id": data.get("pane_id", "w1:p1"),
"workspace_id": data.get("workspace_id", "w1")
@@ -697,12 +698,23 @@ elif cmd1 == "agent":
"command": agent_data.get("command", ""),
"argv": agent_data.get("command", "")
}
agent_st = agent_data.get("agent_status")
if agent_st is None:
seq = agent_data.get("status_sequence")
if seq is None:
seq = state.get("status_sequence")
if seq and isinstance(seq, list) and len(seq) > 0:
agent_st = seq[0]
else:
agent_st = "idle"
print(json.dumps({
"pane": pane_info,
"result": {
"agent": {
"agent": agent_data.get("agent", "claude"),
"status": agent_data.get("status", "running"),
"agent_status": agent_st,
"interactive_ready": agent_data.get("interactive_ready", True),
"cwd": agent_data.get("cwd", ""),
"pane_id": agent_data.get("pane_id", "w1:p1"),
"workspace_id": agent_data.get("workspace_id", "w1")
@@ -713,6 +725,76 @@ elif cmd1 == "agent":
else:
sys.stderr.write("Agent " + name + " not found\\n")
sys.exit(1)
elif cmd2 == "wait":
if len(args) < 3:
sys.exit(1)
name = args[2]
until_list = []
timeout_ms = None
i = 3
while i < len(args):
if args[i] == "--until" and i + 1 < len(args):
until_list.append(args[i+1])
i += 2
elif args[i] == "--timeout" and i + 1 < len(args):
try:
timeout_ms = int(args[i+1])
except Exception:
pass
i += 2
else:
i += 1
agents = state.get("agents", {})
matched_k = None
for k in agents:
if _match_agent(k, name):
matched_k = k
break
if not matched_k:
sys.stderr.write(json.dumps({
"error": {
"code": "agent_not_found",
"message": "agent " + name + " not found"
}
}) + "\\n")
sys.exit(1)
agent_data = agents[matched_k]
seq = agent_data.get("status_sequence")
if seq is None:
seq = state.get("status_sequence")
if seq is not None and isinstance(seq, list) and len(seq) > 0:
current_status = seq.pop(0)
agent_data["agent_status"] = current_status
state["agents"] = agents
save_state()
else:
current_status = agent_data.get("agent_status", "idle")
targets = until_list if until_list else ["idle", "done", "blocked"]
if current_status in targets:
print(json.dumps({
"result": {
"agent": {
"agent": agent_data.get("agent", "claude"),
"agent_status": current_status,
"interactive_ready": agent_data.get("interactive_ready", True),
"cwd": agent_data.get("cwd", ""),
"pane_id": agent_data.get("pane_id", "w1:p1"),
"workspace_id": agent_data.get("workspace_id", "w1")
}
}
}))
sys.exit(0)
else:
sys.stderr.write(json.dumps({
"error": {
"code": "timeout",
"message": "timed out waiting for agent " + name
}
}) + "\\n")
sys.exit(1)
elif cmd2 == "read":
if len(args) < 3:
sys.exit(1)
@@ -731,25 +813,86 @@ elif cmd1 == "agent":
sys.stderr.write("Agent " + name + " not found\\n")
sys.exit(1)
elif cmd2 == "prompt":
if len(args) < 4:
wait_flag = False
until_val = None
timeout_val = None
pos_args = []
i = 2
while i < len(args):
if args[i] == "--wait":
wait_flag = True
i += 1
elif args[i] == "--until" and i + 1 < len(args):
until_val = args[i+1]
i += 2
elif args[i] == "--timeout" and i + 1 < len(args):
timeout_val = args[i+1]
i += 2
else:
pos_args.append(args[i])
i += 1
if len(pos_args) < 2:
sys.exit(1)
name = args[2]
text = args[3]
name, text = pos_args[0], pos_args[1]
agents = state.get("agents", {})
matched_k = None
for k in agents:
if _match_agent(k, name):
matched_k = k
break
if matched_k:
agents[matched_k]["sent_text"] = agents[matched_k].get("sent_text", "") + text
agents[matched_k]["buffer"] = agents[matched_k].get("buffer", "") + "\\n" + text + "\\n\\nesc to interrupt"
state["agents"] = agents
save_state()
if not matched_k:
sys.stderr.write(json.dumps({
"error": {
"code": "agent_not_found",
"message": "Agent " + name + " not found"
}
}) + "\\n")
sys.exit(1)
target_agent = agents[matched_k]
target_agent["sent_text"] = target_agent.get("sent_text", "") + text
target_agent["buffer"] = target_agent.get("buffer", "") + "\\n" + text + "\\n\\nesc to interrupt"
if wait_flag:
p_res = state.get("prompt_wait_result")
if p_res is None:
p_res = target_agent.get("prompt_wait_result", "ok")
if p_res == "ok":
if until_val == "working":
target_agent["agent_status"] = "working"
state["agents"] = agents
save_state()
print(json.dumps({"id": "cli:agent:prompt", "result": {"type": "ok"}}))
sys.exit(0)
elif p_res == "empty_output":
state["agents"] = agents
save_state()
sys.exit(1)
else:
state["agents"] = agents
save_state()
sys.stderr.write(json.dumps({
"error": {
"code": p_res,
"message": "agent prompt result: " + str(p_res)
}
}) + "\\n")
sys.exit(1)
else:
p_res = state.get("prompt_wait_result")
if p_res is None:
p_res = target_agent.get("prompt_wait_result", "ok")
if p_res == "empty_output":
sys.exit(1)
if p_res != "ok":
sys.stderr.write(json.dumps({
"error": {
"code": p_res,
"message": "agent prompt result: " + str(p_res)
}
}) + "\\n")
sys.exit(1)
print(json.dumps({"id": "cli:agent:prompt", "result": {"type": "ok"}}))
sys.exit(0)
sys.stderr.write("Agent " + name + " not found\\n")
sys.exit(1)
else:
# Real herdr has no `agent send`. Unknown subcommands must fail
# (exit 1) rather than silently succeeding.