cowork-flow 0.0.47 → 0.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +102 -23
- package/package.json +8 -3
- package/src/cli.js +6 -0
- package/src/commands/init.js +0 -83
- package/src/commands/source-refresh.js +40 -0
- package/src/commands/sync.js +6 -0
- package/src/commands/update.js +59 -6
- package/src/lib/copy-template.js +200 -50
- package/src/lib/host-assets.js +367 -64
- package/src/lib/plan-applier.js +47 -0
- package/template/.claude/hooks/inject-workflow-state.py +7 -0
- package/template/.codex/hooks/inject-workflow-state.py +7 -0
- package/template/.codex/hooks.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/config.yaml +0 -11
- package/template/.cowork-flow/scripts/adapters/cli/init_developer.py +0 -1
- package/template/.cowork-flow/scripts/adapters/cli/subagent.py +7 -4
- package/template/.cowork-flow/scripts/adapters/cli/task.py +8 -484
- package/template/.cowork-flow/scripts/adapters/cli/task_archive_commands.py +15 -134
- package/template/.cowork-flow/scripts/adapters/cli/task_context_commands.py +7 -1
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +388 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +163 -2
- package/template/.cowork-flow/scripts/adapters/git/git_context.py +16 -193
- package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +170 -84
- package/template/.cowork-flow/scripts/infra/config.py +0 -17
- package/template/.cowork-flow/scripts/infra/developer_profile.py +0 -95
- package/template/.cowork-flow/scripts/infra/paths.py +0 -79
- package/template/.cowork-flow/scripts/infra/skill_manifest.py +23 -3
- package/template/.cowork-flow/scripts/infra/storage/operation_log.py +40 -0
- package/template/.cowork-flow/scripts/infra/storage/state_store.py +356 -1
- package/template/.cowork-flow/scripts/kernel/workflow_route.py +15 -15
- package/template/.cowork-flow/scripts/run.py +0 -5
- package/template/.cowork-flow/scripts/services/context_discovery.py +156 -0
- package/template/.cowork-flow/scripts/services/context_jsonl.py +119 -0
- package/template/.cowork-flow/scripts/services/context_paths.py +186 -0
- package/template/.cowork-flow/scripts/services/lifecycle_checks.py +186 -69
- package/template/.cowork-flow/scripts/services/lifecycle_policy.py +128 -0
- package/template/.cowork-flow/scripts/services/plan_binding.py +146 -0
- package/template/.cowork-flow/scripts/services/readiness.py +3 -170
- package/template/.cowork-flow/scripts/services/task_archive.py +115 -24
- package/template/.cowork-flow/scripts/services/task_context.py +103 -365
- package/template/.cowork-flow/scripts/services/task_creation.py +40 -5
- package/template/.cowork-flow/scripts/services/task_lifecycle.py +155 -18
- package/template/.cowork-flow/scripts/services/task_routing.py +80 -67
- package/template/.cowork-flow/scripts/services/task_utils.py +0 -146
- package/template/.cowork-flow/scripts/services/workflow_runtime.py +1 -1
- package/template/.cowork-flow/spec/contracts/capabilities.md +23 -0
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +2 -2
- package/template/.cowork-flow/spec/contracts/error-output-as-data.md +8 -0
- package/template/.cowork-flow/spec/contracts/index.md +1 -0
- package/template/.cowork-flow/spec/contracts/party-mode-v2-board.md +20 -0
- package/template/.cowork-flow/spec/contracts/plan-binding.md +50 -0
- package/template/.cowork-flow/spec/contracts/skill-owned-actions.md +31 -7
- package/template/.cowork-flow/spec/guides/index.md +3 -1
- package/template/.cowork-flow/spec/index.md +5 -4
- package/template/.cowork-flow/spec/references/definition-of-done.md +2 -3
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
- package/template/.cowork-flow/spec/runtime/contract-registry.json +14 -0
- package/template/.cowork-flow/spec/runtime/host-assets.json +73 -3
- package/template/.cowork-flow/spec/runtime/index.md +1 -1
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +150 -8
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/skills/batch-execution/SKILL.md +37 -3
- package/template/skills/batch-execution/scripts/batch_action.py +4 -0
- package/template/skills/batch-execution/scripts/batch_execution.py +120 -499
- package/template/skills/batch-execution/scripts/batch_recovery.py +94 -0
- package/template/skills/batch-execution/scripts/batch_state_machine.py +201 -0
- package/template/skills/batch-execution/scripts/batch_verification.py +326 -0
- package/template/skills/brainstorming/SKILL.md +8 -7
- package/template/skills/cowork-flow/SKILL.md +3 -0
- package/template/skills/party-mode/scripts/party_action_contract.py +113 -0
- package/template/skills/party-mode/scripts/party_actions.py +95 -0
- package/template/skills/party-mode/scripts/party_board_store.py +148 -0
- package/template/skills/party-mode/scripts/party_mode_v2.py +345 -317
- package/template/skills/party-mode/scripts/party_reports.py +177 -0
- package/template/skills/party-mode/scripts/party_state_machine.py +189 -0
- package/template/skills/runtime-health/SKILL.md +14 -2
- package/template/skills/runtime-health/scripts/doctor.py +278 -41
- package/template/skills/task-planning/SKILL.md +105 -61
- package/template/skills/task-review/SKILL.md +4 -4
- package/template/skills/task-review/scripts/review_check.py +277 -20
- package/template/skills/test-first/SKILL.md +18 -3
- package/template/skills/test-first/manifest.json +8 -4
- package/template/.cowork-flow/changes/archive/.gitkeep +0 -1
- package/template/.cowork-flow/scripts/adapters/cli/add_session.py +0 -465
- package/template/.cowork-flow/scripts/adapters/cli/change.py +0 -412
- package/template/.cowork-flow/workspace/index.md +0 -102
- package/template/.zcode/scaffold/AGENTS.md +0 -145
- package/template/.zcode/scaffold/CLAUDE.md +0 -5
|
@@ -10,11 +10,8 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import argparse
|
|
12
12
|
import json
|
|
13
|
-
import os
|
|
14
13
|
import re
|
|
15
14
|
import sys
|
|
16
|
-
import threading
|
|
17
|
-
from datetime import datetime, timezone
|
|
18
15
|
from pathlib import Path
|
|
19
16
|
from typing import Any
|
|
20
17
|
|
|
@@ -29,13 +26,49 @@ def _add_runtime_scripts_path() -> None:
|
|
|
29
26
|
|
|
30
27
|
|
|
31
28
|
_add_runtime_scripts_path()
|
|
29
|
+
_SCRIPT_DIR = Path(__file__).resolve().parent
|
|
30
|
+
if str(_SCRIPT_DIR) not in sys.path:
|
|
31
|
+
sys.path.insert(0, str(_SCRIPT_DIR))
|
|
32
|
+
|
|
33
|
+
from party_board_store import (
|
|
34
|
+
append_action_history as _append_action_history,
|
|
35
|
+
append_audit as _append_audit,
|
|
36
|
+
load_board,
|
|
37
|
+
read_json as _read_json,
|
|
38
|
+
save_board,
|
|
39
|
+
state_lock as _state_lock,
|
|
40
|
+
write_json as _write_json,
|
|
41
|
+
)
|
|
42
|
+
from party_action_contract import validate_actions_document
|
|
43
|
+
from party_actions import (
|
|
44
|
+
actions_document as _project_actions_document,
|
|
45
|
+
child_action as _project_child_action,
|
|
46
|
+
close_actions as _project_close_actions,
|
|
47
|
+
empty_actions_document as _project_empty_actions,
|
|
48
|
+
fallback_actions_document as _project_fallback_actions,
|
|
49
|
+
)
|
|
50
|
+
from party_reports import (
|
|
51
|
+
action_results_summary as _project_action_results_summary,
|
|
52
|
+
build_final_report as _project_final_report,
|
|
53
|
+
)
|
|
54
|
+
from party_state_machine import (
|
|
55
|
+
advance_board_state as _advance_board_state,
|
|
56
|
+
build_public_round as _project_public_round,
|
|
57
|
+
current_round as _project_current_round,
|
|
58
|
+
ensure_finalizable as _ensure_finalizable_state,
|
|
59
|
+
initial_agents_state as _project_initial_agents_state,
|
|
60
|
+
initial_board as _project_initial_board,
|
|
61
|
+
)
|
|
32
62
|
from infra.config import get_party_mode_v2_config
|
|
33
63
|
from infra.paths import DIR_WORKFLOW, get_repo_root
|
|
64
|
+
from adapters.host.host_manifest import load_host_manifest
|
|
34
65
|
|
|
35
66
|
RUNTIME_DIR = ".runtime"
|
|
36
67
|
MODE_DIR = "party-mode-v2"
|
|
37
68
|
SAFE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
|
38
69
|
CONFIDENCE_VALUES = {"low", "medium", "high"}
|
|
70
|
+
PARTY_BOARD_ACTION_CAPABILITY = "party_board_action"
|
|
71
|
+
SUPPORTED_HOST_CAPABILITY_STATUSES = {"native", "shim", "plugin", "external", "experimental"}
|
|
39
72
|
HOST_FORBIDDEN_TERMS = (
|
|
40
73
|
"spawn_agent",
|
|
41
74
|
"wait_agent",
|
|
@@ -44,12 +77,6 @@ HOST_FORBIDDEN_TERMS = (
|
|
|
44
77
|
"Claude Task",
|
|
45
78
|
"OpenCode task",
|
|
46
79
|
)
|
|
47
|
-
_LOCKS_GUARD = threading.Lock()
|
|
48
|
-
_STATE_LOCKS: dict[str, dict[str, Any]] = {}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _now() -> str:
|
|
52
|
-
return datetime.now(timezone.utc).isoformat()
|
|
53
80
|
|
|
54
81
|
|
|
55
82
|
def _validate_identifier(value: str, *, label: str) -> str:
|
|
@@ -71,117 +98,6 @@ def discussion_dir(repo_root: Path, discussion_id: str) -> Path:
|
|
|
71
98
|
return path
|
|
72
99
|
|
|
73
100
|
|
|
74
|
-
def _lock_file(handle: Any) -> None:
|
|
75
|
-
if os.name == "nt":
|
|
76
|
-
import msvcrt
|
|
77
|
-
|
|
78
|
-
handle.seek(0)
|
|
79
|
-
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
import fcntl
|
|
83
|
-
|
|
84
|
-
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _unlock_file(handle: Any) -> None:
|
|
88
|
-
if os.name == "nt":
|
|
89
|
-
import msvcrt
|
|
90
|
-
|
|
91
|
-
handle.seek(0)
|
|
92
|
-
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
import fcntl
|
|
96
|
-
|
|
97
|
-
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class _StateLock:
|
|
101
|
-
def __init__(self, base_dir: Path, state: dict[str, Any]) -> None:
|
|
102
|
-
self.base_dir = base_dir
|
|
103
|
-
self.state = state
|
|
104
|
-
|
|
105
|
-
def __enter__(self) -> "_StateLock":
|
|
106
|
-
thread_lock = self.state["thread_lock"]
|
|
107
|
-
thread_lock.acquire()
|
|
108
|
-
current_thread = threading.get_ident()
|
|
109
|
-
if self.state.get("owner_thread") == current_thread:
|
|
110
|
-
self.state["depth"] += 1
|
|
111
|
-
return self
|
|
112
|
-
|
|
113
|
-
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
114
|
-
lock_path = self.base_dir / ".state.lock"
|
|
115
|
-
handle = lock_path.open("a+b")
|
|
116
|
-
handle.seek(0)
|
|
117
|
-
handle.write(b"0")
|
|
118
|
-
handle.flush()
|
|
119
|
-
_lock_file(handle)
|
|
120
|
-
self.state["owner_thread"] = current_thread
|
|
121
|
-
self.state["depth"] = 1
|
|
122
|
-
self.state["handle"] = handle
|
|
123
|
-
return self
|
|
124
|
-
|
|
125
|
-
def __exit__(self, exc_type: Any, exc: Any, traceback: Any) -> None:
|
|
126
|
-
thread_lock = self.state["thread_lock"]
|
|
127
|
-
try:
|
|
128
|
-
current_thread = threading.get_ident()
|
|
129
|
-
if self.state.get("owner_thread") == current_thread:
|
|
130
|
-
self.state["depth"] -= 1
|
|
131
|
-
if self.state["depth"] == 0:
|
|
132
|
-
handle = self.state.pop("handle", None)
|
|
133
|
-
self.state["owner_thread"] = None
|
|
134
|
-
if handle is not None:
|
|
135
|
-
try:
|
|
136
|
-
_unlock_file(handle)
|
|
137
|
-
finally:
|
|
138
|
-
handle.close()
|
|
139
|
-
finally:
|
|
140
|
-
thread_lock.release()
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def _state_lock(base_dir: Path) -> _StateLock:
|
|
144
|
-
key = str(base_dir.resolve())
|
|
145
|
-
with _LOCKS_GUARD:
|
|
146
|
-
state = _STATE_LOCKS.get(key)
|
|
147
|
-
if state is None:
|
|
148
|
-
state = {
|
|
149
|
-
"thread_lock": threading.RLock(),
|
|
150
|
-
"owner_thread": None,
|
|
151
|
-
"depth": 0,
|
|
152
|
-
}
|
|
153
|
-
_STATE_LOCKS[key] = state
|
|
154
|
-
return _StateLock(base_dir, state)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def _write_json(path: Path, data: dict[str, Any]) -> None:
|
|
158
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
-
temp_path = path.with_name(f".{path.name}.{threading.get_ident()}.tmp")
|
|
160
|
-
temp_path.write_text(
|
|
161
|
-
json.dumps(data, ensure_ascii=False, indent=2) + "\n",
|
|
162
|
-
encoding="utf-8",
|
|
163
|
-
)
|
|
164
|
-
os.replace(temp_path, path)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def _read_json(path: Path) -> dict[str, Any]:
|
|
168
|
-
return json.loads(path.read_text(encoding="utf-8"))
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def _append_audit(base_dir: Path, event: str, payload: dict[str, Any]) -> None:
|
|
172
|
-
audit_path = base_dir / "audit.jsonl"
|
|
173
|
-
entry = {"timestamp": _now(), "event": event, "payload": payload}
|
|
174
|
-
with audit_path.open("a", encoding="utf-8") as handle:
|
|
175
|
-
handle.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def _append_action_history(base_dir: Path, event: str, payload: dict[str, Any]) -> None:
|
|
179
|
-
history_path = base_dir / "action_history.jsonl"
|
|
180
|
-
entry = {"timestamp": _now(), "event": event, "payload": payload}
|
|
181
|
-
with history_path.open("a", encoding="utf-8") as handle:
|
|
182
|
-
handle.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
183
|
-
|
|
184
|
-
|
|
185
101
|
def _parse_agent_spec(spec: str) -> dict[str, str]:
|
|
186
102
|
agent_id, separator, lens = spec.partition(":")
|
|
187
103
|
agent_id = agent_id.strip()
|
|
@@ -309,40 +225,7 @@ def _round_empty_state(items: list[Any], phase: str, *, item_type: str) -> str |
|
|
|
309
225
|
|
|
310
226
|
|
|
311
227
|
def _build_public_round(board: dict[str, Any]) -> dict[str, Any]:
|
|
312
|
-
|
|
313
|
-
current = next(
|
|
314
|
-
(
|
|
315
|
-
item
|
|
316
|
-
for item in board.get("rounds", [])
|
|
317
|
-
if int(item.get("round", -1)) == current_round
|
|
318
|
-
),
|
|
319
|
-
{"round": current_round, "posts": [], "responses": [], "moderator_events": []},
|
|
320
|
-
)
|
|
321
|
-
posts = list(current.get("posts", []))
|
|
322
|
-
responses = list(current.get("responses", []))
|
|
323
|
-
phase = board["round"]["phase"]
|
|
324
|
-
expected_next_action = {
|
|
325
|
-
"publish": "post",
|
|
326
|
-
"respond": "respond",
|
|
327
|
-
"closed": "finalized",
|
|
328
|
-
}.get(str(phase), "monitor")
|
|
329
|
-
return {
|
|
330
|
-
"schema_version": 1,
|
|
331
|
-
"discussion_id": board["discussion_id"],
|
|
332
|
-
"round": current_round,
|
|
333
|
-
"phase": phase,
|
|
334
|
-
"topic": board["topic"],
|
|
335
|
-
"visible_posts": posts,
|
|
336
|
-
"visible_responses": responses,
|
|
337
|
-
"moderator_events": list(current.get("moderator_events", [])),
|
|
338
|
-
"empty_state": {
|
|
339
|
-
"visible_posts": _round_empty_state(posts, phase, item_type="posts"),
|
|
340
|
-
"visible_responses": _round_empty_state(responses, phase, item_type="responses"),
|
|
341
|
-
},
|
|
342
|
-
"expected_next_action": expected_next_action,
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
|
|
228
|
+
return _project_public_round(board)
|
|
346
229
|
def _agent_prompt_file(
|
|
347
230
|
base_dir: Path,
|
|
348
231
|
discussion_id: str,
|
|
@@ -362,6 +245,81 @@ def _agent_prompt_file(
|
|
|
362
245
|
return prompt_name, str(prompt_file)
|
|
363
246
|
|
|
364
247
|
|
|
248
|
+
def _host_manifest_root(repo_root: Path) -> Path:
|
|
249
|
+
repo_manifest = repo_root / DIR_WORKFLOW / "spec" / "runtime" / "host-assets.json"
|
|
250
|
+
if repo_manifest.is_file():
|
|
251
|
+
return repo_root
|
|
252
|
+
for parent in _SCRIPT_DIR.parents:
|
|
253
|
+
if (parent / DIR_WORKFLOW / "spec" / "runtime" / "host-assets.json").is_file():
|
|
254
|
+
return parent
|
|
255
|
+
return repo_root
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _normalize_host_id(host_id: str | None) -> str | None:
|
|
259
|
+
if host_id is None or not str(host_id).strip():
|
|
260
|
+
return None
|
|
261
|
+
return _validate_identifier(str(host_id).strip(), label="host_id")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _party_board_capability(repo_root: Path, host_id: str | None) -> Any | None:
|
|
265
|
+
if host_id is None:
|
|
266
|
+
return None
|
|
267
|
+
manifest = load_host_manifest(_host_manifest_root(repo_root))
|
|
268
|
+
return manifest.host_capability(host_id, PARTY_BOARD_ACTION_CAPABILITY)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _party_board_action_is_supported(repo_root: Path, host_id: str | None) -> bool:
|
|
272
|
+
capability = _party_board_capability(repo_root, host_id)
|
|
273
|
+
if capability is None:
|
|
274
|
+
return True
|
|
275
|
+
return capability.status in SUPPORTED_HOST_CAPABILITY_STATUSES
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _party_board_fallback_actions(
|
|
279
|
+
repo_root: Path,
|
|
280
|
+
base_dir: Path,
|
|
281
|
+
discussion_id: str,
|
|
282
|
+
agents: list[dict[str, str]],
|
|
283
|
+
*,
|
|
284
|
+
round_number: int,
|
|
285
|
+
phase: str,
|
|
286
|
+
host_id: str,
|
|
287
|
+
) -> dict[str, Any]:
|
|
288
|
+
capability = _party_board_capability(repo_root, host_id)
|
|
289
|
+
fallback = capability.fallback if capability is not None else "inline_or_manual"
|
|
290
|
+
(base_dir / "prompts").mkdir(parents=True, exist_ok=True)
|
|
291
|
+
prompt_files: list[str] = []
|
|
292
|
+
for agent in agents:
|
|
293
|
+
_prompt_name, prompt_file = _agent_prompt_file(
|
|
294
|
+
base_dir,
|
|
295
|
+
discussion_id,
|
|
296
|
+
agent,
|
|
297
|
+
round_number=round_number,
|
|
298
|
+
phase=phase,
|
|
299
|
+
)
|
|
300
|
+
prompt_files.append(prompt_file)
|
|
301
|
+
document = _project_fallback_actions(
|
|
302
|
+
discussion_id,
|
|
303
|
+
round_number=round_number,
|
|
304
|
+
phase=phase,
|
|
305
|
+
host_id=host_id,
|
|
306
|
+
fallback=fallback,
|
|
307
|
+
)
|
|
308
|
+
action = document["next_actions"][0]
|
|
309
|
+
_append_action_history(base_dir, "action-issued", action)
|
|
310
|
+
_append_audit(
|
|
311
|
+
base_dir,
|
|
312
|
+
"fallback",
|
|
313
|
+
{
|
|
314
|
+
"host_id": host_id,
|
|
315
|
+
"capability": PARTY_BOARD_ACTION_CAPABILITY,
|
|
316
|
+
"fallback": fallback,
|
|
317
|
+
"round": round_number,
|
|
318
|
+
"phase": phase,
|
|
319
|
+
"prompt_files": prompt_files,
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
return document
|
|
365
323
|
def _child_action(
|
|
366
324
|
base_dir: Path,
|
|
367
325
|
discussion_id: str,
|
|
@@ -370,7 +328,6 @@ def _child_action(
|
|
|
370
328
|
round_number: int,
|
|
371
329
|
phase: str,
|
|
372
330
|
) -> dict[str, Any]:
|
|
373
|
-
agent_id = agent["agent_id"]
|
|
374
331
|
_prompt_name, prompt_file = _agent_prompt_file(
|
|
375
332
|
base_dir,
|
|
376
333
|
discussion_id,
|
|
@@ -378,48 +335,55 @@ def _child_action(
|
|
|
378
335
|
round_number=round_number,
|
|
379
336
|
phase=phase,
|
|
380
337
|
)
|
|
381
|
-
return
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
"message_kind": f"board_{phase}",
|
|
388
|
-
"prompt_file": prompt_file,
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
338
|
+
return _project_child_action(
|
|
339
|
+
agent=agent,
|
|
340
|
+
round_number=round_number,
|
|
341
|
+
phase=phase,
|
|
342
|
+
prompt_file=prompt_file,
|
|
343
|
+
)
|
|
392
344
|
def _build_actions(
|
|
345
|
+
repo_root: Path,
|
|
393
346
|
base_dir: Path,
|
|
394
347
|
discussion_id: str,
|
|
395
348
|
agents: list[dict[str, str]],
|
|
396
349
|
*,
|
|
397
350
|
round_number: int,
|
|
398
351
|
phase: str,
|
|
352
|
+
host_id: str | None = None,
|
|
399
353
|
) -> dict[str, Any]:
|
|
400
|
-
|
|
354
|
+
host_id = _normalize_host_id(host_id)
|
|
355
|
+
if host_id is not None and not _party_board_action_is_supported(repo_root, host_id):
|
|
356
|
+
return _party_board_fallback_actions(
|
|
357
|
+
repo_root,
|
|
358
|
+
base_dir,
|
|
359
|
+
discussion_id,
|
|
360
|
+
agents,
|
|
361
|
+
round_number=round_number,
|
|
362
|
+
phase=phase,
|
|
363
|
+
host_id=host_id,
|
|
364
|
+
)
|
|
401
365
|
prompts_dir = base_dir / "prompts"
|
|
402
366
|
prompts_dir.mkdir(parents=True, exist_ok=True)
|
|
403
|
-
|
|
404
|
-
|
|
367
|
+
child_actions = [
|
|
368
|
+
_child_action(
|
|
405
369
|
base_dir,
|
|
406
370
|
discussion_id,
|
|
407
371
|
agent,
|
|
408
372
|
round_number=round_number,
|
|
409
373
|
phase=phase,
|
|
410
374
|
)
|
|
411
|
-
|
|
375
|
+
for agent in agents
|
|
376
|
+
]
|
|
377
|
+
document = _project_actions_document(
|
|
378
|
+
discussion_id,
|
|
379
|
+
child_actions=child_actions,
|
|
380
|
+
agent_ids=[agent["agent_id"] for agent in agents],
|
|
381
|
+
round_number=round_number,
|
|
382
|
+
phase=phase,
|
|
383
|
+
)
|
|
384
|
+
for action in document["next_actions"]:
|
|
412
385
|
_append_action_history(base_dir, "action-issued", action)
|
|
413
|
-
|
|
414
|
-
"action_id": f"r{round_number}-{phase}-wait",
|
|
415
|
-
"type": "wait_children",
|
|
416
|
-
"agent_ids": [agent["agent_id"] for agent in agents],
|
|
417
|
-
}
|
|
418
|
-
actions.append(wait_action)
|
|
419
|
-
_append_action_history(base_dir, "action-issued", wait_action)
|
|
420
|
-
return {"schema_version": 1, "discussion_id": discussion_id, "next_actions": actions}
|
|
421
|
-
|
|
422
|
-
|
|
386
|
+
return document
|
|
423
387
|
def _build_close_actions(
|
|
424
388
|
base_dir: Path,
|
|
425
389
|
agents: list[dict[str, str]],
|
|
@@ -427,30 +391,27 @@ def _build_close_actions(
|
|
|
427
391
|
round_number: int,
|
|
428
392
|
reason: str,
|
|
429
393
|
) -> list[dict[str, Any]]:
|
|
430
|
-
actions =
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
"reason": reason,
|
|
437
|
-
}
|
|
438
|
-
actions.append(action)
|
|
394
|
+
actions = _project_close_actions(
|
|
395
|
+
agents,
|
|
396
|
+
round_number=round_number,
|
|
397
|
+
reason=reason,
|
|
398
|
+
)
|
|
399
|
+
for action in actions:
|
|
439
400
|
_append_action_history(base_dir, "action-issued", action)
|
|
440
401
|
_append_audit(
|
|
441
402
|
base_dir,
|
|
442
403
|
"close",
|
|
443
404
|
{
|
|
444
|
-
"agent_id":
|
|
405
|
+
"agent_id": action["agent_id"],
|
|
445
406
|
"round": round_number,
|
|
446
407
|
"reason": reason,
|
|
447
408
|
},
|
|
448
409
|
)
|
|
449
410
|
return actions
|
|
450
|
-
|
|
451
|
-
|
|
452
411
|
def _empty_actions(discussion_id: str) -> dict[str, Any]:
|
|
453
|
-
return
|
|
412
|
+
return _project_empty_actions(discussion_id)
|
|
413
|
+
def _write_actions_json(base_dir: Path, actions: dict[str, Any]) -> None:
|
|
414
|
+
_write_json(base_dir / "actions.json", validate_actions_document(actions))
|
|
454
415
|
|
|
455
416
|
|
|
456
417
|
def _with_terminal_state(data: dict[str, Any], terminal: bool) -> dict[str, Any]:
|
|
@@ -458,55 +419,52 @@ def _with_terminal_state(data: dict[str, Any], terminal: bool) -> dict[str, Any]
|
|
|
458
419
|
return data
|
|
459
420
|
|
|
460
421
|
|
|
461
|
-
def _initial_board(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
422
|
+
def _initial_board(
|
|
423
|
+
discussion_id: str,
|
|
424
|
+
topic: str,
|
|
425
|
+
max_rounds: int,
|
|
426
|
+
*,
|
|
427
|
+
host_id: str | None = None,
|
|
428
|
+
) -> dict[str, Any]:
|
|
429
|
+
return _project_initial_board(
|
|
430
|
+
discussion_id,
|
|
431
|
+
topic,
|
|
432
|
+
max_rounds,
|
|
433
|
+
host_id=host_id,
|
|
434
|
+
)
|
|
472
435
|
def _initial_agents_state(discussion_id: str, agents: list[dict[str, str]]) -> dict[str, Any]:
|
|
473
|
-
return
|
|
474
|
-
"schema_version": 1,
|
|
475
|
-
"discussion_id": discussion_id,
|
|
476
|
-
"agents": [
|
|
477
|
-
{
|
|
478
|
-
"agent_id": agent["agent_id"],
|
|
479
|
-
"lens": agent["lens"],
|
|
480
|
-
"status": "pending",
|
|
481
|
-
"drift_warnings": 0,
|
|
482
|
-
"host_child_id": None,
|
|
483
|
-
}
|
|
484
|
-
for agent in agents
|
|
485
|
-
],
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
|
|
436
|
+
return _project_initial_agents_state(discussion_id, agents)
|
|
489
437
|
def init_discussion(
|
|
490
438
|
repo_root: Path,
|
|
491
439
|
*,
|
|
492
440
|
discussion_id: str,
|
|
493
441
|
topic: str,
|
|
494
442
|
agent_specs: list[str],
|
|
443
|
+
host_id: str | None = None,
|
|
495
444
|
) -> dict[str, Any]:
|
|
496
445
|
_validate_identifier(discussion_id, label="discussion_id")
|
|
497
446
|
config = get_party_mode_v2_config(repo_root)
|
|
498
447
|
agents = [_parse_agent_spec(spec) for spec in agent_specs]
|
|
499
448
|
_validate_agents(agents, config)
|
|
449
|
+
host_id = _normalize_host_id(host_id)
|
|
500
450
|
base_dir = discussion_dir(repo_root, discussion_id)
|
|
501
451
|
with _state_lock(base_dir):
|
|
502
452
|
max_rounds = int(config["max_rounds"])
|
|
503
|
-
board = _initial_board(discussion_id, topic, max_rounds)
|
|
453
|
+
board = _initial_board(discussion_id, topic, max_rounds, host_id=host_id)
|
|
504
454
|
agents_state = _initial_agents_state(discussion_id, agents)
|
|
505
|
-
actions = _build_actions(
|
|
455
|
+
actions = _build_actions(
|
|
456
|
+
repo_root,
|
|
457
|
+
base_dir,
|
|
458
|
+
discussion_id,
|
|
459
|
+
agents,
|
|
460
|
+
round_number=1,
|
|
461
|
+
phase="publish",
|
|
462
|
+
host_id=host_id,
|
|
463
|
+
)
|
|
506
464
|
public_round = _build_public_round(board)
|
|
507
|
-
|
|
465
|
+
save_board(base_dir, board)
|
|
508
466
|
_write_json(base_dir / "agents.json", agents_state)
|
|
509
|
-
|
|
467
|
+
_write_actions_json(base_dir, actions)
|
|
510
468
|
_write_json(base_dir / "public_round.json", public_round)
|
|
511
469
|
_append_audit(
|
|
512
470
|
base_dir,
|
|
@@ -530,7 +488,7 @@ def view_discussion(
|
|
|
530
488
|
if agent_id:
|
|
531
489
|
_validate_identifier(agent_id, label="agent_id")
|
|
532
490
|
with _state_lock(base_dir):
|
|
533
|
-
board =
|
|
491
|
+
board = load_board(base_dir)
|
|
534
492
|
public_round = _build_public_round(board)
|
|
535
493
|
if agent_id:
|
|
536
494
|
public_round["agent_id"] = agent_id
|
|
@@ -545,9 +503,9 @@ def view_discussion(
|
|
|
545
503
|
|
|
546
504
|
def monitor_discussion(repo_root: Path, *, discussion_id: str) -> dict[str, Any]:
|
|
547
505
|
base_dir = discussion_dir(repo_root, discussion_id)
|
|
548
|
-
board =
|
|
506
|
+
board = load_board(base_dir)
|
|
549
507
|
agents = _read_json(base_dir / "agents.json")
|
|
550
|
-
actions = _read_json(base_dir / "actions.json")
|
|
508
|
+
actions = validate_actions_document(_read_json(base_dir / "actions.json"))
|
|
551
509
|
active_agents = [
|
|
552
510
|
agent
|
|
553
511
|
for agent in agents.get("agents", [])
|
|
@@ -569,15 +527,7 @@ def monitor_discussion(repo_root: Path, *, discussion_id: str) -> dict[str, Any]
|
|
|
569
527
|
|
|
570
528
|
|
|
571
529
|
def _current_round(board: dict[str, Any]) -> dict[str, Any]:
|
|
572
|
-
|
|
573
|
-
for item in board.get("rounds", []):
|
|
574
|
-
if int(item.get("round", -1)) == current_round:
|
|
575
|
-
return item
|
|
576
|
-
item = {"round": current_round, "posts": [], "responses": [], "moderator_events": []}
|
|
577
|
-
board.setdefault("rounds", []).append(item)
|
|
578
|
-
return item
|
|
579
|
-
|
|
580
|
-
|
|
530
|
+
return _project_current_round(board)
|
|
581
531
|
def _active_agent_ids(base_dir: Path) -> list[str]:
|
|
582
532
|
agents = _read_json(base_dir / "agents.json")
|
|
583
533
|
return [
|
|
@@ -638,7 +588,7 @@ def _build_post_payload(
|
|
|
638
588
|
|
|
639
589
|
|
|
640
590
|
def _write_runtime_state(base_dir: Path, board: dict[str, Any]) -> None:
|
|
641
|
-
|
|
591
|
+
save_board(base_dir, board)
|
|
642
592
|
_write_json(base_dir / "public_round.json", _build_public_round(board))
|
|
643
593
|
|
|
644
594
|
|
|
@@ -653,7 +603,7 @@ def post_submission(
|
|
|
653
603
|
base_dir = discussion_dir(repo_root, discussion_id)
|
|
654
604
|
config = get_party_mode_v2_config(repo_root)
|
|
655
605
|
with _state_lock(base_dir):
|
|
656
|
-
board =
|
|
606
|
+
board = load_board(base_dir)
|
|
657
607
|
if board["round"]["phase"] != "publish":
|
|
658
608
|
raise ValueError("phase_not_publish")
|
|
659
609
|
if agent_id not in _active_agent_ids(base_dir):
|
|
@@ -869,7 +819,7 @@ def respond_submission(
|
|
|
869
819
|
base_dir = discussion_dir(repo_root, discussion_id)
|
|
870
820
|
config = get_party_mode_v2_config(repo_root)
|
|
871
821
|
with _state_lock(base_dir):
|
|
872
|
-
board =
|
|
822
|
+
board = load_board(base_dir)
|
|
873
823
|
if board["round"]["phase"] != "respond":
|
|
874
824
|
raise ValueError("phase_not_respond")
|
|
875
825
|
if agent_id not in _active_agent_ids(base_dir):
|
|
@@ -905,23 +855,27 @@ def respond_submission(
|
|
|
905
855
|
|
|
906
856
|
|
|
907
857
|
def _write_actions_for_phase(
|
|
858
|
+
repo_root: Path,
|
|
908
859
|
base_dir: Path,
|
|
909
860
|
discussion_id: str,
|
|
910
861
|
round_number: int,
|
|
911
862
|
phase: str,
|
|
912
863
|
*,
|
|
864
|
+
host_id: str | None = None,
|
|
913
865
|
leading_actions: list[dict[str, Any]] | None = None,
|
|
914
866
|
) -> None:
|
|
915
867
|
actions = _build_actions(
|
|
868
|
+
repo_root,
|
|
916
869
|
base_dir,
|
|
917
870
|
discussion_id,
|
|
918
871
|
_agent_lenses(base_dir),
|
|
919
872
|
round_number=round_number,
|
|
920
873
|
phase=phase,
|
|
874
|
+
host_id=host_id,
|
|
921
875
|
)
|
|
922
876
|
if leading_actions:
|
|
923
877
|
actions["next_actions"] = leading_actions + actions["next_actions"]
|
|
924
|
-
|
|
878
|
+
_write_actions_json(base_dir, actions)
|
|
925
879
|
|
|
926
880
|
|
|
927
881
|
def _advance_publish_phase(
|
|
@@ -934,21 +888,26 @@ def _advance_publish_phase(
|
|
|
934
888
|
active_agents: list[str],
|
|
935
889
|
current_round: int,
|
|
936
890
|
) -> dict[str, Any]:
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
891
|
+
transition = _advance_board_state(
|
|
892
|
+
board,
|
|
893
|
+
active_agent_ids=active_agents,
|
|
894
|
+
fresh_context_per_round=True,
|
|
895
|
+
)
|
|
942
896
|
_write_runtime_state(base_dir, board)
|
|
943
|
-
_write_actions_for_phase(
|
|
897
|
+
_write_actions_for_phase(
|
|
898
|
+
repo_root,
|
|
899
|
+
base_dir,
|
|
900
|
+
discussion_id,
|
|
901
|
+
transition["next_actions"]["round"],
|
|
902
|
+
transition["next_actions"]["phase"],
|
|
903
|
+
host_id=board.get("host_id"),
|
|
904
|
+
)
|
|
944
905
|
_append_audit(
|
|
945
906
|
base_dir,
|
|
946
907
|
"advance",
|
|
947
|
-
{"round": current_round, "from": "
|
|
908
|
+
{"round": current_round, "from": transition["from"], "to": transition["to"]},
|
|
948
909
|
)
|
|
949
910
|
return monitor_discussion(repo_root, discussion_id=discussion_id)
|
|
950
|
-
|
|
951
|
-
|
|
952
911
|
def _close_discussion(
|
|
953
912
|
repo_root: Path,
|
|
954
913
|
*,
|
|
@@ -961,7 +920,7 @@ def _close_discussion(
|
|
|
961
920
|
board["round"]["phase"] = "closed"
|
|
962
921
|
board["termination"] = {"reason": reason}
|
|
963
922
|
_write_runtime_state(base_dir, board)
|
|
964
|
-
|
|
923
|
+
_write_actions_json(base_dir, _empty_actions(discussion_id))
|
|
965
924
|
_append_audit(
|
|
966
925
|
base_dir,
|
|
967
926
|
"advance",
|
|
@@ -994,10 +953,12 @@ def _advance_next_round(
|
|
|
994
953
|
)
|
|
995
954
|
_write_runtime_state(base_dir, board)
|
|
996
955
|
_write_actions_for_phase(
|
|
956
|
+
repo_root,
|
|
997
957
|
base_dir,
|
|
998
958
|
discussion_id,
|
|
999
959
|
next_round,
|
|
1000
960
|
"publish",
|
|
961
|
+
host_id=board.get("host_id"),
|
|
1001
962
|
leading_actions=close_actions,
|
|
1002
963
|
)
|
|
1003
964
|
_append_audit(
|
|
@@ -1018,41 +979,63 @@ def _advance_respond_phase(
|
|
|
1018
979
|
active_agents: list[str],
|
|
1019
980
|
current_round: int,
|
|
1020
981
|
) -> dict[str, Any]:
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
982
|
+
config = get_party_mode_v2_config(repo_root)
|
|
983
|
+
transition = _advance_board_state(
|
|
984
|
+
board,
|
|
985
|
+
active_agent_ids=active_agents,
|
|
986
|
+
fresh_context_per_round=bool(config.get("fresh_context_per_round", True)),
|
|
987
|
+
)
|
|
988
|
+
if transition["to"] == "closed":
|
|
989
|
+
_write_runtime_state(base_dir, board)
|
|
990
|
+
_write_actions_json(base_dir, _empty_actions(discussion_id))
|
|
991
|
+
_append_audit(
|
|
992
|
+
base_dir,
|
|
993
|
+
"advance",
|
|
994
|
+
{
|
|
995
|
+
"round": current_round,
|
|
996
|
+
"from": transition["from"],
|
|
997
|
+
"to": transition["to"],
|
|
998
|
+
"reason": transition["reason"],
|
|
999
|
+
},
|
|
1033
1000
|
)
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1001
|
+
return finalize_discussion(repo_root, discussion_id=discussion_id)
|
|
1002
|
+
|
|
1003
|
+
if transition["to"] != "publish":
|
|
1004
|
+
raise ValueError("phase_not_advanceable")
|
|
1005
|
+
close_actions = []
|
|
1006
|
+
close_reason = transition.get("close_children_reason")
|
|
1007
|
+
if close_reason:
|
|
1008
|
+
close_actions = _build_close_actions(
|
|
1009
|
+
base_dir,
|
|
1010
|
+
_agent_lenses(base_dir),
|
|
1011
|
+
round_number=current_round,
|
|
1012
|
+
reason=str(close_reason),
|
|
1042
1013
|
)
|
|
1043
|
-
|
|
1014
|
+
_write_runtime_state(base_dir, board)
|
|
1015
|
+
_write_actions_for_phase(
|
|
1044
1016
|
repo_root,
|
|
1045
|
-
base_dir
|
|
1046
|
-
discussion_id
|
|
1047
|
-
|
|
1048
|
-
|
|
1017
|
+
base_dir,
|
|
1018
|
+
discussion_id,
|
|
1019
|
+
transition["next_actions"]["round"],
|
|
1020
|
+
transition["next_actions"]["phase"],
|
|
1021
|
+
host_id=board.get("host_id"),
|
|
1022
|
+
leading_actions=close_actions,
|
|
1049
1023
|
)
|
|
1050
|
-
|
|
1051
|
-
|
|
1024
|
+
_append_audit(
|
|
1025
|
+
base_dir,
|
|
1026
|
+
"advance",
|
|
1027
|
+
{
|
|
1028
|
+
"round": current_round,
|
|
1029
|
+
"from": transition["from"],
|
|
1030
|
+
"to": transition["to"],
|
|
1031
|
+
"next_round": transition["next_round"],
|
|
1032
|
+
},
|
|
1033
|
+
)
|
|
1034
|
+
return monitor_discussion(repo_root, discussion_id=discussion_id)
|
|
1052
1035
|
def advance_discussion(repo_root: Path, *, discussion_id: str) -> dict[str, Any]:
|
|
1053
1036
|
base_dir = discussion_dir(repo_root, discussion_id)
|
|
1054
1037
|
with _state_lock(base_dir):
|
|
1055
|
-
board =
|
|
1038
|
+
board = load_board(base_dir)
|
|
1056
1039
|
current = _current_round(board)
|
|
1057
1040
|
active_agents = _active_agent_ids(base_dir)
|
|
1058
1041
|
current_round = int(board["round"]["current"])
|
|
@@ -1103,16 +1086,13 @@ def _ensure_finalizable(
|
|
|
1103
1086
|
*,
|
|
1104
1087
|
manual_termination: bool,
|
|
1105
1088
|
) -> None:
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
_write_json(base_dir / "actions.json", _empty_actions(discussion_id))
|
|
1114
|
-
|
|
1115
|
-
|
|
1089
|
+
changed = _ensure_finalizable_state(
|
|
1090
|
+
board,
|
|
1091
|
+
manual_termination=manual_termination,
|
|
1092
|
+
)
|
|
1093
|
+
if changed:
|
|
1094
|
+
_write_runtime_state(base_dir, board)
|
|
1095
|
+
_write_actions_json(base_dir, _empty_actions(discussion_id))
|
|
1116
1096
|
def _discussion_posts(board: dict[str, Any]) -> list[dict[str, Any]]:
|
|
1117
1097
|
return [post for item in board.get("rounds", []) for post in item.get("posts", [])]
|
|
1118
1098
|
|
|
@@ -1147,8 +1127,75 @@ def _historical_disagreements(
|
|
|
1147
1127
|
]
|
|
1148
1128
|
|
|
1149
1129
|
|
|
1130
|
+
def _round_summary_phase(board: dict[str, Any], round_number: int) -> str:
|
|
1131
|
+
if round_number == int(board["round"]["current"]):
|
|
1132
|
+
return str(board["round"]["phase"])
|
|
1133
|
+
return "completed"
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def _rounds_summary(board: dict[str, Any]) -> list[dict[str, Any]]:
|
|
1137
|
+
summaries: list[dict[str, Any]] = []
|
|
1138
|
+
for item in board.get("rounds", []):
|
|
1139
|
+
round_number = int(item.get("round", 0))
|
|
1140
|
+
posts = item.get("posts", [])
|
|
1141
|
+
responses = item.get("responses", [])
|
|
1142
|
+
unresolved = [r for r in responses if r.get("still_disagree")]
|
|
1143
|
+
summaries.append(
|
|
1144
|
+
{
|
|
1145
|
+
"round": round_number,
|
|
1146
|
+
"phase": _round_summary_phase(board, round_number),
|
|
1147
|
+
"post_count": len(posts),
|
|
1148
|
+
"response_count": len(responses),
|
|
1149
|
+
"unresolved_count": len(unresolved),
|
|
1150
|
+
}
|
|
1151
|
+
)
|
|
1152
|
+
return summaries
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def _action_results_summary(base_dir: Path) -> list[dict[str, Any]]:
|
|
1156
|
+
history_path = base_dir / "action_history.jsonl"
|
|
1157
|
+
if not history_path.is_file():
|
|
1158
|
+
return []
|
|
1159
|
+
results: list[dict[str, Any]] = []
|
|
1160
|
+
with history_path.open("r", encoding="utf-8") as handle:
|
|
1161
|
+
for line in handle:
|
|
1162
|
+
if not line.strip():
|
|
1163
|
+
continue
|
|
1164
|
+
entry = json.loads(line)
|
|
1165
|
+
if entry.get("event") != "action-result":
|
|
1166
|
+
continue
|
|
1167
|
+
payload = entry.get("payload", {})
|
|
1168
|
+
summary: dict[str, Any] = {
|
|
1169
|
+
"action_id": payload.get("action_id"),
|
|
1170
|
+
"type": payload.get("type"),
|
|
1171
|
+
"outcome": payload.get("outcome"),
|
|
1172
|
+
}
|
|
1173
|
+
if payload.get("agent_id"):
|
|
1174
|
+
summary["agent_id"] = payload["agent_id"]
|
|
1175
|
+
results.append(summary)
|
|
1176
|
+
return results
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def _accepted_evidence_summary(responses: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
1180
|
+
summaries: list[dict[str, Any]] = []
|
|
1181
|
+
for response in responses:
|
|
1182
|
+
if response.get("decision") != "concede":
|
|
1183
|
+
continue
|
|
1184
|
+
evidence = response.get("accepted_evidence", [])
|
|
1185
|
+
if not evidence:
|
|
1186
|
+
continue
|
|
1187
|
+
summaries.append(
|
|
1188
|
+
{
|
|
1189
|
+
"agent_id": response.get("agent_id"),
|
|
1190
|
+
"target_post_id": response.get("target_post_id"),
|
|
1191
|
+
"accepted_evidence": evidence,
|
|
1192
|
+
}
|
|
1193
|
+
)
|
|
1194
|
+
return summaries
|
|
1195
|
+
|
|
1150
1196
|
def _build_final_report(
|
|
1151
1197
|
*,
|
|
1198
|
+
base_dir: Path,
|
|
1152
1199
|
discussion_id: str,
|
|
1153
1200
|
board: dict[str, Any],
|
|
1154
1201
|
posts: list[dict[str, Any]],
|
|
@@ -1157,34 +1204,12 @@ def _build_final_report(
|
|
|
1157
1204
|
historical_disagreements: list[dict[str, Any]],
|
|
1158
1205
|
stop_reason: str,
|
|
1159
1206
|
) -> dict[str, Any]:
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
"phase": board["round"]["phase"],
|
|
1167
|
-
"max_rounds": board["round"]["max"],
|
|
1168
|
-
"termination_reason": stop_reason,
|
|
1169
|
-
}
|
|
1170
|
-
return {
|
|
1171
|
-
"schema_version": 1,
|
|
1172
|
-
"discussion_id": discussion_id,
|
|
1173
|
-
"terminal": True,
|
|
1174
|
-
"stop_reason": stop_reason,
|
|
1175
|
-
"board_status": board_status,
|
|
1176
|
-
"next_actions": [],
|
|
1177
|
-
"pro": _report_pro_posts(posts),
|
|
1178
|
-
"con": _report_con_responses(responses),
|
|
1179
|
-
"changed_positions": _responses_with_delta(responses, "changed"),
|
|
1180
|
-
"maintained_positions": _responses_with_delta(responses, "unchanged"),
|
|
1181
|
-
"revised_positions": _responses_with_decision(responses, "revise"),
|
|
1182
|
-
"current_unresolved_disagreements": current_unresolved,
|
|
1183
|
-
"historical_disagreements": historical_disagreements,
|
|
1184
|
-
"unresolved_disagreements": current_unresolved,
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1207
|
+
return _project_final_report(
|
|
1208
|
+
discussion_id=discussion_id,
|
|
1209
|
+
board=board,
|
|
1210
|
+
action_results=_project_action_results_summary(base_dir),
|
|
1211
|
+
stop_reason=stop_reason,
|
|
1212
|
+
)
|
|
1188
1213
|
def _report_pro_posts(posts: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
1189
1214
|
return [
|
|
1190
1215
|
{"agent_id": post["agent_id"], "claim": post["claim"], "evidence": post["evidence"]}
|
|
@@ -1230,7 +1255,7 @@ def finalize_discussion(
|
|
|
1230
1255
|
) -> dict[str, Any]:
|
|
1231
1256
|
base_dir = discussion_dir(repo_root, discussion_id)
|
|
1232
1257
|
with _state_lock(base_dir):
|
|
1233
|
-
board =
|
|
1258
|
+
board = load_board(base_dir)
|
|
1234
1259
|
_ensure_finalizable(
|
|
1235
1260
|
base_dir,
|
|
1236
1261
|
discussion_id,
|
|
@@ -1241,6 +1266,7 @@ def finalize_discussion(
|
|
|
1241
1266
|
current_round = int(board["round"]["current"])
|
|
1242
1267
|
stop_reason = board.get("termination", {}).get("reason") or "manual_finalize"
|
|
1243
1268
|
report = _build_final_report(
|
|
1269
|
+
base_dir=base_dir,
|
|
1244
1270
|
discussion_id=discussion_id,
|
|
1245
1271
|
board=board,
|
|
1246
1272
|
posts=_discussion_posts(board),
|
|
@@ -1312,6 +1338,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1312
1338
|
init_parser.add_argument("--discussion-id", required=True)
|
|
1313
1339
|
init_parser.add_argument("--topic", required=True)
|
|
1314
1340
|
init_parser.add_argument("--agent", action="append", required=True)
|
|
1341
|
+
init_parser.add_argument("--host-id", default=None)
|
|
1315
1342
|
|
|
1316
1343
|
view_parser = subcommands.add_parser("view")
|
|
1317
1344
|
view_parser.add_argument("--discussion-id", required=True)
|
|
@@ -1358,6 +1385,7 @@ def _command_result(repo_root: Path, args: argparse.Namespace) -> dict[str, Any]
|
|
|
1358
1385
|
discussion_id=args.discussion_id,
|
|
1359
1386
|
topic=args.topic,
|
|
1360
1387
|
agent_specs=args.agent,
|
|
1388
|
+
host_id=args.host_id,
|
|
1361
1389
|
)
|
|
1362
1390
|
if args.command == "view":
|
|
1363
1391
|
return view_discussion(
|