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:
1 parent
7b761be7b8
commit
ec61511699
51 files changed
+3477
-293
No files matched your search
+154
-11
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user