cowork-flow 0.0.37 → 0.0.39
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/README.md +27 -14
- package/package.json +1 -1
- package/src/commands/init.js +39 -28
- package/src/commands/sync.js +2 -2
- package/src/lib/asset-plan.js +127 -0
- package/src/lib/copy-template.js +101 -141
- package/src/lib/host-assets.js +231 -0
- package/src/lib/plan-applier.js +329 -0
- package/src/lib/platforms.js +15 -76
- package/src/lib/skill-registry.js +503 -0
- package/template/.claude/agents/cowork-check.md +10 -1
- package/template/.claude/agents/cowork-implement.md +11 -2
- package/template/.claude/hooks/inject-workflow-state.py +51 -254
- package/template/.codex/agents/cowork-check.toml +12 -3
- package/template/.codex/agents/cowork-implement.toml +11 -2
- package/template/.codex/agents/default.toml +3 -2
- package/template/.codex/agents/explorer.toml +3 -2
- package/template/.codex/agents/worker.toml +2 -1
- package/template/.codex/hooks/inject-workflow-state.py +46 -265
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/application/__init__.py +1 -0
- package/template/.cowork-flow/scripts/application/batch_execution.py +743 -0
- package/template/.cowork-flow/scripts/application/runtime_context_service.py +365 -0
- package/template/.cowork-flow/scripts/application/task_archive.py +220 -0
- package/template/.cowork-flow/scripts/application/task_context.py +765 -0
- package/template/.cowork-flow/scripts/application/task_creation.py +201 -0
- package/template/.cowork-flow/scripts/application/task_lifecycle.py +532 -0
- package/template/.cowork-flow/scripts/application/task_tree.py +179 -0
- package/template/.cowork-flow/scripts/commands/doctor.py +78 -57
- package/template/.cowork-flow/scripts/commands/party_mode_v2.py +671 -425
- package/template/.cowork-flow/scripts/commands/subagent.py +38 -26
- package/template/.cowork-flow/scripts/commands/task.py +535 -1953
- package/template/.cowork-flow/scripts/commands/task_archive_commands.py +195 -0
- package/template/.cowork-flow/scripts/commands/task_context_commands.py +240 -0
- package/template/.cowork-flow/scripts/commands/task_create_command.py +164 -0
- package/template/.cowork-flow/scripts/commands/task_navigation.py +407 -0
- package/template/.cowork-flow/scripts/commands/task_parser.py +364 -0
- package/template/.cowork-flow/scripts/commands/task_support.py +76 -0
- package/template/.cowork-flow/scripts/commands/task_tree_commands.py +160 -0
- package/template/.cowork-flow/scripts/common/core/execution_context.py +161 -85
- package/template/.cowork-flow/scripts/common/core/host_manifest.py +393 -0
- package/template/.cowork-flow/scripts/common/core/skill_registry.py +568 -0
- package/template/.cowork-flow/scripts/common/gates/coding_standards.py +58 -7
- package/template/.cowork-flow/scripts/common/gates/gates.py +323 -46
- package/template/.cowork-flow/scripts/common/gates/models.py +196 -0
- package/template/.cowork-flow/scripts/common/gates/registry.py +79 -0
- package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +13 -13
- package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +104 -62
- package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +39 -22
- package/template/.cowork-flow/scripts/common/gates/validate_rules.py +223 -40
- package/template/.cowork-flow/scripts/common/git/git_context.py +216 -182
- package/template/.cowork-flow/scripts/common/host/__init__.py +1 -0
- package/template/.cowork-flow/scripts/common/host/workflow_state_hook.py +303 -0
- package/template/.cowork-flow/scripts/common/storage/__init__.py +1 -0
- package/template/.cowork-flow/scripts/common/storage/operation_log.py +72 -0
- package/template/.cowork-flow/scripts/common/storage/state_store.py +279 -0
- package/template/.cowork-flow/scripts/common/storage/unit_of_work.py +214 -0
- package/template/.cowork-flow/scripts/common/task/active_task.py +93 -135
- package/template/.cowork-flow/scripts/common/task/batch_mode.py +33 -120
- package/template/.cowork-flow/scripts/common/task/decision_review.py +85 -0
- package/template/.cowork-flow/scripts/common/task/readiness.py +9 -15
- package/template/.cowork-flow/scripts/common/task/task_repository.py +156 -0
- package/template/.cowork-flow/scripts/run.py +24 -0
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
- package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
- package/template/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
- package/template/.cowork-flow/spec/protocols/decision-review.md +28 -0
- package/template/.cowork-flow/spec/protocols/review.md +34 -0
- package/template/.cowork-flow/spec/protocols/spec-maintenance.md +16 -0
- package/template/.cowork-flow/spec/protocols/tdd.md +30 -0
- package/template/.cowork-flow/spec/references/definition-of-done.md +4 -4
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
- package/template/.cowork-flow/spec/runtime/contract-registry.json +25 -0
- package/template/.cowork-flow/spec/runtime/host-assets.json +214 -0
- package/template/.cowork-flow/spec/runtime/index.md +8 -0
- package/template/.cowork-flow/spec/runtime/rules.json +61 -10
- package/template/.cowork-flow/spec/runtime/skill-registry.json +397 -0
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +173 -0
- package/template/.cowork-flow/spec/schemas/rules.schema.json +10 -1
- package/template/.cowork-flow/spec/schemas/skill-registry.schema.json +202 -0
- package/template/.cowork-flow/workflow.md +18 -13
- package/template/.opencode/agents/cowork-check.md +11 -0
- package/template/.opencode/agents/cowork-implement.md +16 -1
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/hooks/inject-context.js +3 -85
- package/template/.zcode/hooks/inject-context.selfcheck.mjs +6 -6
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
- package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +61 -10
- package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +10 -1
- package/template/.zcode/scaffold/.cowork-flow/workflow.md +18 -13
- package/template/.zcode/scaffold/AGENTS.md +18 -8
- package/template/AGENTS.md +1 -1
- package/template/skills/batch-mode/SKILL.md +140 -76
- package/template/skills/brainstorming/SKILL.md +2 -2
- package/template/skills/cowork-flow/SKILL.md +42 -0
- package/template/skills/party-mode/SHARED-BOUNDARIES.md +1 -1
- package/template/skills/party-mode/SKILL.md +48 -119
- package/template/.cowork-flow/scripts/common/gates/spec_validator.py +0 -53
- package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +0 -32
- package/template/skills/before-dev/SKILL.md +0 -41
- package/template/skills/check/SKILL.md +0 -53
- package/template/skills/check/simplification-guide.md +0 -47
- package/template/skills/continue/SKILL.md +0 -29
- package/template/skills/doubt-review/SKILL.md +0 -118
- package/template/skills/finish-work/SKILL.md +0 -32
- package/template/skills/party-mode-v2/SKILL.md +0 -75
- package/template/skills/start/SKILL.md +0 -78
- package/template/skills/tdd/SKILL.md +0 -74
- package/template/skills/update-spec/SKILL.md +0 -39
- package/template/skills/using-cowork-flow/SKILL.md +0 -80
|
@@ -207,7 +207,20 @@ def _prompt_text(
|
|
|
207
207
|
phase: str,
|
|
208
208
|
) -> str:
|
|
209
209
|
agent_id = agent["agent_id"]
|
|
210
|
-
base = (
|
|
210
|
+
base = _prompt_header(discussion_id, agent, round_number, phase)
|
|
211
|
+
if phase == "publish":
|
|
212
|
+
return base + _publish_prompt_payload(discussion_id, agent_id, round_number)
|
|
213
|
+
return base + _respond_prompt_payload(discussion_id, agent_id, round_number)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _prompt_header(
|
|
217
|
+
discussion_id: str,
|
|
218
|
+
agent: dict[str, str],
|
|
219
|
+
round_number: int,
|
|
220
|
+
phase: str,
|
|
221
|
+
) -> str:
|
|
222
|
+
agent_id = agent["agent_id"]
|
|
223
|
+
return (
|
|
211
224
|
f"# Party Mode V2 Child: {agent_id}\n\n"
|
|
212
225
|
f"discussion_id: {discussion_id}\n"
|
|
213
226
|
f"agent_id: {agent_id}\n"
|
|
@@ -221,29 +234,32 @@ def _prompt_text(
|
|
|
221
234
|
f".\\.cowork-flow\\run.cmd party-v2 view --discussion-id {discussion_id} --agent-id {agent_id}\n"
|
|
222
235
|
"```\n"
|
|
223
236
|
)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _publish_prompt_payload(discussion_id: str, agent_id: str, round_number: int) -> str:
|
|
240
|
+
return (
|
|
241
|
+
"\nSubmit one current-round post with `party-v2 post --file`:\n\n"
|
|
242
|
+
"```powershell\n"
|
|
243
|
+
f".\\.cowork-flow\\run.cmd party-v2 post --discussion-id {discussion_id} --agent-id {agent_id} --file <payload.json>\n"
|
|
244
|
+
"```\n\n"
|
|
245
|
+
"Payload fields:\n\n"
|
|
246
|
+
"```json\n"
|
|
247
|
+
"{\n"
|
|
248
|
+
f" \"round\": {round_number},\n"
|
|
249
|
+
" \"claim\": \"...\",\n"
|
|
250
|
+
" \"evidence\": [\"...\"],\n"
|
|
251
|
+
" \"risk\": \"...\",\n"
|
|
252
|
+
" \"tradeoff\": \"...\",\n"
|
|
253
|
+
" \"acceptance_signal\": \"...\",\n"
|
|
254
|
+
" \"what_would_change_my_mind\": \"...\"\n"
|
|
255
|
+
"}\n"
|
|
256
|
+
"```\n"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _respond_prompt_payload(discussion_id: str, agent_id: str, round_number: int) -> str:
|
|
244
261
|
return (
|
|
245
|
-
|
|
246
|
-
+ "\nRespond to current-round target posts only. Respect `max_rebuttal_targets_per_agent`.\n\n"
|
|
262
|
+
"\nRespond to current-round target posts only. Respect `max_rebuttal_targets_per_agent`.\n\n"
|
|
247
263
|
"Submit with `party-v2 respond --file`:\n\n"
|
|
248
264
|
"```powershell\n"
|
|
249
265
|
f".\\.cowork-flow\\run.cmd party-v2 respond --discussion-id {discussion_id} --agent-id {agent_id} --file <payload.json>\n"
|
|
@@ -267,6 +283,24 @@ def _prompt_text(
|
|
|
267
283
|
)
|
|
268
284
|
|
|
269
285
|
|
|
286
|
+
def _round_empty_state(items: list[Any], phase: str, *, item_type: str) -> str | None:
|
|
287
|
+
if items:
|
|
288
|
+
return None
|
|
289
|
+
if item_type == "posts":
|
|
290
|
+
if phase == "publish":
|
|
291
|
+
return "waiting_for_current_round_posts"
|
|
292
|
+
if phase == "closed":
|
|
293
|
+
return "discussion_closed"
|
|
294
|
+
return "no_current_round_posts"
|
|
295
|
+
if phase == "publish":
|
|
296
|
+
return "responses_not_open_yet"
|
|
297
|
+
if phase == "respond":
|
|
298
|
+
return "waiting_for_current_round_responses"
|
|
299
|
+
if phase == "closed":
|
|
300
|
+
return "discussion_closed"
|
|
301
|
+
return "no_current_round_responses"
|
|
302
|
+
|
|
303
|
+
|
|
270
304
|
def _build_public_round(board: dict[str, Any]) -> dict[str, Any]:
|
|
271
305
|
current_round = int(board["round"]["current"])
|
|
272
306
|
current = next(
|
|
@@ -280,24 +314,6 @@ def _build_public_round(board: dict[str, Any]) -> dict[str, Any]:
|
|
|
280
314
|
posts = list(current.get("posts", []))
|
|
281
315
|
responses = list(current.get("responses", []))
|
|
282
316
|
phase = board["round"]["phase"]
|
|
283
|
-
if posts:
|
|
284
|
-
posts_empty = None
|
|
285
|
-
elif phase == "publish":
|
|
286
|
-
posts_empty = "waiting_for_current_round_posts"
|
|
287
|
-
elif phase == "closed":
|
|
288
|
-
posts_empty = "discussion_closed"
|
|
289
|
-
else:
|
|
290
|
-
posts_empty = "no_current_round_posts"
|
|
291
|
-
if responses:
|
|
292
|
-
responses_empty = None
|
|
293
|
-
elif phase == "publish":
|
|
294
|
-
responses_empty = "responses_not_open_yet"
|
|
295
|
-
elif phase == "respond":
|
|
296
|
-
responses_empty = "waiting_for_current_round_responses"
|
|
297
|
-
elif phase == "closed":
|
|
298
|
-
responses_empty = "discussion_closed"
|
|
299
|
-
else:
|
|
300
|
-
responses_empty = "no_current_round_responses"
|
|
301
317
|
expected_next_action = {
|
|
302
318
|
"publish": "post",
|
|
303
319
|
"respond": "respond",
|
|
@@ -313,13 +329,59 @@ def _build_public_round(board: dict[str, Any]) -> dict[str, Any]:
|
|
|
313
329
|
"visible_responses": responses,
|
|
314
330
|
"moderator_events": list(current.get("moderator_events", [])),
|
|
315
331
|
"empty_state": {
|
|
316
|
-
"visible_posts":
|
|
317
|
-
"visible_responses":
|
|
332
|
+
"visible_posts": _round_empty_state(posts, phase, item_type="posts"),
|
|
333
|
+
"visible_responses": _round_empty_state(responses, phase, item_type="responses"),
|
|
318
334
|
},
|
|
319
335
|
"expected_next_action": expected_next_action,
|
|
320
336
|
}
|
|
321
337
|
|
|
322
338
|
|
|
339
|
+
def _agent_prompt_file(
|
|
340
|
+
base_dir: Path,
|
|
341
|
+
discussion_id: str,
|
|
342
|
+
agent: dict[str, str],
|
|
343
|
+
*,
|
|
344
|
+
round_number: int,
|
|
345
|
+
phase: str,
|
|
346
|
+
) -> tuple[str, str]:
|
|
347
|
+
agent_id = agent["agent_id"]
|
|
348
|
+
prompt_name = f"{agent_id}-r{round_number}-{phase}.md"
|
|
349
|
+
prompt_path = base_dir / "prompts" / prompt_name
|
|
350
|
+
prompt_path.write_text(
|
|
351
|
+
_prompt_text(discussion_id, agent, round_number, phase),
|
|
352
|
+
encoding="utf-8",
|
|
353
|
+
)
|
|
354
|
+
prompt_file = Path(DIR_WORKFLOW) / RUNTIME_DIR / MODE_DIR / discussion_id / "prompts" / prompt_name
|
|
355
|
+
return prompt_name, str(prompt_file)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _child_action(
|
|
359
|
+
base_dir: Path,
|
|
360
|
+
discussion_id: str,
|
|
361
|
+
agent: dict[str, str],
|
|
362
|
+
*,
|
|
363
|
+
round_number: int,
|
|
364
|
+
phase: str,
|
|
365
|
+
) -> dict[str, Any]:
|
|
366
|
+
agent_id = agent["agent_id"]
|
|
367
|
+
_prompt_name, prompt_file = _agent_prompt_file(
|
|
368
|
+
base_dir,
|
|
369
|
+
discussion_id,
|
|
370
|
+
agent,
|
|
371
|
+
round_number=round_number,
|
|
372
|
+
phase=phase,
|
|
373
|
+
)
|
|
374
|
+
return {
|
|
375
|
+
"action_id": f"r{round_number}-{phase}-{agent_id}",
|
|
376
|
+
"type": "dispatch_child" if phase == "publish" else "send_control_message",
|
|
377
|
+
"agent_id": agent_id,
|
|
378
|
+
"agent_kind": "advisory",
|
|
379
|
+
"lens": agent["lens"],
|
|
380
|
+
"message_kind": f"board_{phase}",
|
|
381
|
+
"prompt_file": prompt_file,
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
323
385
|
def _build_actions(
|
|
324
386
|
base_dir: Path,
|
|
325
387
|
discussion_id: str,
|
|
@@ -332,37 +394,21 @@ def _build_actions(
|
|
|
332
394
|
prompts_dir = base_dir / "prompts"
|
|
333
395
|
prompts_dir.mkdir(parents=True, exist_ok=True)
|
|
334
396
|
for agent in agents:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
)
|
|
342
|
-
action_id = f"r{round_number}-{phase}-{agent_id}"
|
|
343
|
-
action = {
|
|
344
|
-
"action_id": action_id,
|
|
345
|
-
"type": "dispatch_child" if phase == "publish" else "send_control_message",
|
|
346
|
-
"agent_id": agent_id,
|
|
347
|
-
"agent_kind": "advisory",
|
|
348
|
-
"lens": agent["lens"],
|
|
349
|
-
"message_kind": f"board_{phase}",
|
|
350
|
-
"prompt_file": str(
|
|
351
|
-
Path(DIR_WORKFLOW) / RUNTIME_DIR / MODE_DIR / discussion_id / "prompts" / prompt_name
|
|
352
|
-
),
|
|
353
|
-
}
|
|
354
|
-
actions.append(
|
|
355
|
-
action
|
|
397
|
+
action = _child_action(
|
|
398
|
+
base_dir,
|
|
399
|
+
discussion_id,
|
|
400
|
+
agent,
|
|
401
|
+
round_number=round_number,
|
|
402
|
+
phase=phase,
|
|
356
403
|
)
|
|
404
|
+
actions.append(action)
|
|
357
405
|
_append_action_history(base_dir, "action-issued", action)
|
|
358
406
|
wait_action = {
|
|
359
407
|
"action_id": f"r{round_number}-{phase}-wait",
|
|
360
408
|
"type": "wait_children",
|
|
361
409
|
"agent_ids": [agent["agent_id"] for agent in agents],
|
|
362
410
|
}
|
|
363
|
-
actions.append(
|
|
364
|
-
wait_action
|
|
365
|
-
)
|
|
411
|
+
actions.append(wait_action)
|
|
366
412
|
_append_action_history(base_dir, "action-issued", wait_action)
|
|
367
413
|
return {"schema_version": 1, "discussion_id": discussion_id, "next_actions": actions}
|
|
368
414
|
|
|
@@ -405,6 +451,34 @@ def _with_terminal_state(data: dict[str, Any], terminal: bool) -> dict[str, Any]
|
|
|
405
451
|
return data
|
|
406
452
|
|
|
407
453
|
|
|
454
|
+
def _initial_board(discussion_id: str, topic: str, max_rounds: int) -> dict[str, Any]:
|
|
455
|
+
return {
|
|
456
|
+
"schema_version": 1,
|
|
457
|
+
"discussion_id": discussion_id,
|
|
458
|
+
"topic": topic,
|
|
459
|
+
"round": {"current": 1, "max": max_rounds, "phase": "publish"},
|
|
460
|
+
"rounds": [{"round": 1, "posts": [], "responses": [], "moderator_events": []}],
|
|
461
|
+
"termination": {"reason": None},
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _initial_agents_state(discussion_id: str, agents: list[dict[str, str]]) -> dict[str, Any]:
|
|
466
|
+
return {
|
|
467
|
+
"schema_version": 1,
|
|
468
|
+
"discussion_id": discussion_id,
|
|
469
|
+
"agents": [
|
|
470
|
+
{
|
|
471
|
+
"agent_id": agent["agent_id"],
|
|
472
|
+
"lens": agent["lens"],
|
|
473
|
+
"status": "pending",
|
|
474
|
+
"drift_warnings": 0,
|
|
475
|
+
"host_child_id": None,
|
|
476
|
+
}
|
|
477
|
+
for agent in agents
|
|
478
|
+
],
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
408
482
|
def init_discussion(
|
|
409
483
|
repo_root: Path,
|
|
410
484
|
*,
|
|
@@ -419,35 +493,8 @@ def init_discussion(
|
|
|
419
493
|
base_dir = discussion_dir(repo_root, discussion_id)
|
|
420
494
|
with _state_lock(base_dir):
|
|
421
495
|
max_rounds = int(config["max_rounds"])
|
|
422
|
-
board =
|
|
423
|
-
|
|
424
|
-
"discussion_id": discussion_id,
|
|
425
|
-
"topic": topic,
|
|
426
|
-
"round": {"current": 1, "max": max_rounds, "phase": "publish"},
|
|
427
|
-
"rounds": [
|
|
428
|
-
{
|
|
429
|
-
"round": 1,
|
|
430
|
-
"posts": [],
|
|
431
|
-
"responses": [],
|
|
432
|
-
"moderator_events": [],
|
|
433
|
-
}
|
|
434
|
-
],
|
|
435
|
-
"termination": {"reason": None},
|
|
436
|
-
}
|
|
437
|
-
agents_state = {
|
|
438
|
-
"schema_version": 1,
|
|
439
|
-
"discussion_id": discussion_id,
|
|
440
|
-
"agents": [
|
|
441
|
-
{
|
|
442
|
-
"agent_id": agent["agent_id"],
|
|
443
|
-
"lens": agent["lens"],
|
|
444
|
-
"status": "pending",
|
|
445
|
-
"drift_warnings": 0,
|
|
446
|
-
"host_child_id": None,
|
|
447
|
-
}
|
|
448
|
-
for agent in agents
|
|
449
|
-
],
|
|
450
|
-
}
|
|
496
|
+
board = _initial_board(discussion_id, topic, max_rounds)
|
|
497
|
+
agents_state = _initial_agents_state(discussion_id, agents)
|
|
451
498
|
actions = _build_actions(base_dir, discussion_id, agents, round_number=1, phase="publish")
|
|
452
499
|
public_round = _build_public_round(board)
|
|
453
500
|
_write_json(base_dir / "board.json", board)
|
|
@@ -556,6 +603,33 @@ def _require_non_empty_list(payload: dict[str, Any], key: str, error: str) -> li
|
|
|
556
603
|
return value
|
|
557
604
|
|
|
558
605
|
|
|
606
|
+
def _build_post_payload(
|
|
607
|
+
*,
|
|
608
|
+
current_round: int,
|
|
609
|
+
agent_id: str,
|
|
610
|
+
payload: dict[str, Any],
|
|
611
|
+
post_number: int,
|
|
612
|
+
) -> dict[str, Any]:
|
|
613
|
+
return {
|
|
614
|
+
"post_id": f"r{current_round}-{agent_id}-p{post_number}",
|
|
615
|
+
"agent_id": agent_id,
|
|
616
|
+
"claim": _require_non_empty_text(payload, "claim", "missing_claim"),
|
|
617
|
+
"evidence": _require_non_empty_list(payload, "evidence", "missing_evidence"),
|
|
618
|
+
"risk": _require_non_empty_text(payload, "risk", "missing_risk"),
|
|
619
|
+
"tradeoff": _require_non_empty_text(payload, "tradeoff", "missing_tradeoff"),
|
|
620
|
+
"acceptance_signal": _require_non_empty_text(
|
|
621
|
+
payload,
|
|
622
|
+
"acceptance_signal",
|
|
623
|
+
"missing_acceptance_signal",
|
|
624
|
+
),
|
|
625
|
+
"what_would_change_my_mind": _require_non_empty_text(
|
|
626
|
+
payload,
|
|
627
|
+
"what_would_change_my_mind",
|
|
628
|
+
"missing_what_would_change_my_mind",
|
|
629
|
+
),
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
|
|
559
633
|
def _write_runtime_state(base_dir: Path, board: dict[str, Any]) -> None:
|
|
560
634
|
_write_json(base_dir / "board.json", board)
|
|
561
635
|
_write_json(base_dir / "public_round.json", _build_public_round(board))
|
|
@@ -579,34 +653,16 @@ def post_submission(
|
|
|
579
653
|
raise ValueError("agent_not_active")
|
|
580
654
|
current_round = int(board["round"]["current"])
|
|
581
655
|
_payload_round(payload, current_round, config)
|
|
582
|
-
claim = _require_non_empty_text(payload, "claim", "missing_claim")
|
|
583
|
-
evidence = _require_non_empty_list(payload, "evidence", "missing_evidence")
|
|
584
|
-
risk = _require_non_empty_text(payload, "risk", "missing_risk")
|
|
585
|
-
tradeoff = _require_non_empty_text(payload, "tradeoff", "missing_tradeoff")
|
|
586
|
-
acceptance_signal = _require_non_empty_text(
|
|
587
|
-
payload,
|
|
588
|
-
"acceptance_signal",
|
|
589
|
-
"missing_acceptance_signal",
|
|
590
|
-
)
|
|
591
|
-
what_would_change_my_mind = _require_non_empty_text(
|
|
592
|
-
payload,
|
|
593
|
-
"what_would_change_my_mind",
|
|
594
|
-
"missing_what_would_change_my_mind",
|
|
595
|
-
)
|
|
596
656
|
current = _current_round(board)
|
|
597
657
|
posts = current.setdefault("posts", [])
|
|
598
658
|
if any(post.get("agent_id") == agent_id for post in posts):
|
|
599
659
|
raise ValueError("duplicate_post")
|
|
600
|
-
post =
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
"tradeoff": tradeoff,
|
|
607
|
-
"acceptance_signal": acceptance_signal,
|
|
608
|
-
"what_would_change_my_mind": what_would_change_my_mind,
|
|
609
|
-
}
|
|
660
|
+
post = _build_post_payload(
|
|
661
|
+
current_round=current_round,
|
|
662
|
+
agent_id=agent_id,
|
|
663
|
+
payload=payload,
|
|
664
|
+
post_number=len(posts) + 1,
|
|
665
|
+
)
|
|
610
666
|
posts.append(post)
|
|
611
667
|
_write_runtime_state(base_dir, board)
|
|
612
668
|
_append_audit(base_dir, "post", {"agent_id": agent_id, "post_id": post["post_id"]})
|
|
@@ -652,6 +708,149 @@ def _confidence(payload: dict[str, Any]) -> str:
|
|
|
652
708
|
return value
|
|
653
709
|
|
|
654
710
|
|
|
711
|
+
def _response_target_and_decision(
|
|
712
|
+
board: dict[str, Any],
|
|
713
|
+
agent_id: str,
|
|
714
|
+
payload: dict[str, Any],
|
|
715
|
+
) -> tuple[str, str]:
|
|
716
|
+
target_post_id = _require_non_empty_text(payload, "target_post_id", "missing_target_post_id")
|
|
717
|
+
post_agent_map = _current_post_agent_map(board)
|
|
718
|
+
if target_post_id not in post_agent_map:
|
|
719
|
+
raise ValueError("target_not_in_current_round")
|
|
720
|
+
if post_agent_map[target_post_id] == agent_id:
|
|
721
|
+
raise ValueError("self_target_response")
|
|
722
|
+
decision = _require_non_empty_text(payload, "decision", "missing_decision")
|
|
723
|
+
if decision not in {"maintain", "revise", "concede"}:
|
|
724
|
+
raise ValueError("invalid_decision")
|
|
725
|
+
return target_post_id, decision
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _validate_response_target_limit(
|
|
729
|
+
responses: list[Any],
|
|
730
|
+
*,
|
|
731
|
+
agent_id: str,
|
|
732
|
+
target_post_id: str,
|
|
733
|
+
max_targets: int,
|
|
734
|
+
) -> None:
|
|
735
|
+
agent_targets = {
|
|
736
|
+
str(response["target_post_id"])
|
|
737
|
+
for response in responses
|
|
738
|
+
if str(response.get("agent_id")) == agent_id
|
|
739
|
+
}
|
|
740
|
+
if target_post_id in agent_targets:
|
|
741
|
+
raise ValueError("duplicate_target_response")
|
|
742
|
+
if len(agent_targets) >= max_targets:
|
|
743
|
+
raise ValueError("too_many_rebuttal_targets")
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _build_response_base(
|
|
747
|
+
*,
|
|
748
|
+
current_round: int,
|
|
749
|
+
agent_id: str,
|
|
750
|
+
target_post_id: str,
|
|
751
|
+
decision: str,
|
|
752
|
+
payload: dict[str, Any],
|
|
753
|
+
response_number: int,
|
|
754
|
+
) -> dict[str, Any]:
|
|
755
|
+
return {
|
|
756
|
+
"response_id": f"r{current_round}-{agent_id}-resp{response_number}",
|
|
757
|
+
"agent_id": agent_id,
|
|
758
|
+
"target_post_id": target_post_id,
|
|
759
|
+
"decision": decision,
|
|
760
|
+
"my_current_position": _require_non_empty_text(
|
|
761
|
+
payload,
|
|
762
|
+
"my_current_position",
|
|
763
|
+
"missing_current_position",
|
|
764
|
+
),
|
|
765
|
+
"opponent_claim": _require_non_empty_text(
|
|
766
|
+
payload,
|
|
767
|
+
"opponent_claim",
|
|
768
|
+
"missing_opponent_claim",
|
|
769
|
+
),
|
|
770
|
+
"opponent_evidence_i_checked": _require_non_empty_list(
|
|
771
|
+
payload,
|
|
772
|
+
"opponent_evidence_i_checked",
|
|
773
|
+
"missing_checked_evidence",
|
|
774
|
+
),
|
|
775
|
+
"reasoning": _require_non_empty_text(payload, "reasoning", "missing_reasoning"),
|
|
776
|
+
"confidence_after_review": _confidence(payload),
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _apply_concede_response(response: dict[str, Any], payload: dict[str, Any]) -> None:
|
|
781
|
+
response["why_opponent_is_right"] = _require_non_empty_text(
|
|
782
|
+
payload,
|
|
783
|
+
"why_opponent_is_right",
|
|
784
|
+
"shallow_concession",
|
|
785
|
+
)
|
|
786
|
+
response["accepted_evidence"] = _require_non_empty_list(
|
|
787
|
+
payload,
|
|
788
|
+
"accepted_evidence",
|
|
789
|
+
"shallow_concession",
|
|
790
|
+
)
|
|
791
|
+
response["why_my_previous_position_failed"] = _require_non_empty_text(
|
|
792
|
+
payload,
|
|
793
|
+
"why_my_previous_position_failed",
|
|
794
|
+
"shallow_concession",
|
|
795
|
+
)
|
|
796
|
+
response["position_delta"] = "changed"
|
|
797
|
+
response["still_disagree"] = False
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _apply_revise_response(response: dict[str, Any], payload: dict[str, Any]) -> None:
|
|
801
|
+
response["accepted_part"] = _require_non_empty_text(
|
|
802
|
+
payload,
|
|
803
|
+
"accepted_part",
|
|
804
|
+
"vague_revision",
|
|
805
|
+
)
|
|
806
|
+
response["rejected_part"] = _require_non_empty_text(
|
|
807
|
+
payload,
|
|
808
|
+
"rejected_part",
|
|
809
|
+
"vague_revision",
|
|
810
|
+
)
|
|
811
|
+
response["updated_position"] = _require_non_empty_text(
|
|
812
|
+
payload,
|
|
813
|
+
"updated_position",
|
|
814
|
+
"vague_revision",
|
|
815
|
+
)
|
|
816
|
+
response["position_delta"] = str(payload.get("position_delta") or "narrowed")
|
|
817
|
+
response["still_disagree"] = _require_bool(payload, "still_disagree", True)
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _apply_maintain_response(response: dict[str, Any], payload: dict[str, Any]) -> None:
|
|
821
|
+
response["why_opponent_is_wrong"] = _require_non_empty_text(
|
|
822
|
+
payload,
|
|
823
|
+
"why_opponent_is_wrong",
|
|
824
|
+
"unsupported_rebuttal",
|
|
825
|
+
)
|
|
826
|
+
counter_evidence = payload.get("counter_evidence")
|
|
827
|
+
counter_reasoning = payload.get("counter_reasoning")
|
|
828
|
+
if not counter_evidence and not counter_reasoning:
|
|
829
|
+
raise ValueError("unsupported_rebuttal")
|
|
830
|
+
if counter_evidence is not None:
|
|
831
|
+
if not isinstance(counter_evidence, list) or not counter_evidence:
|
|
832
|
+
raise ValueError("unsupported_rebuttal")
|
|
833
|
+
response["counter_evidence"] = counter_evidence
|
|
834
|
+
if counter_reasoning is not None:
|
|
835
|
+
response["counter_reasoning"] = str(counter_reasoning)
|
|
836
|
+
response["position_delta"] = "unchanged"
|
|
837
|
+
response["still_disagree"] = True
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _apply_decision_response(
|
|
841
|
+
response: dict[str, Any],
|
|
842
|
+
*,
|
|
843
|
+
decision: str,
|
|
844
|
+
payload: dict[str, Any],
|
|
845
|
+
) -> None:
|
|
846
|
+
if decision == "concede":
|
|
847
|
+
_apply_concede_response(response, payload)
|
|
848
|
+
elif decision == "revise":
|
|
849
|
+
_apply_revise_response(response, payload)
|
|
850
|
+
else:
|
|
851
|
+
_apply_maintain_response(response, payload)
|
|
852
|
+
|
|
853
|
+
|
|
655
854
|
def respond_submission(
|
|
656
855
|
repo_root: Path,
|
|
657
856
|
*,
|
|
@@ -670,107 +869,24 @@ def respond_submission(
|
|
|
670
869
|
raise ValueError("agent_not_active")
|
|
671
870
|
current_round = int(board["round"]["current"])
|
|
672
871
|
_payload_round(payload, current_round, config)
|
|
673
|
-
target_post_id =
|
|
674
|
-
post_agent_map = _current_post_agent_map(board)
|
|
675
|
-
if target_post_id not in post_agent_map:
|
|
676
|
-
raise ValueError("target_not_in_current_round")
|
|
677
|
-
if post_agent_map[target_post_id] == agent_id:
|
|
678
|
-
raise ValueError("self_target_response")
|
|
679
|
-
decision = _require_non_empty_text(payload, "decision", "missing_decision")
|
|
680
|
-
if decision not in {"maintain", "revise", "concede"}:
|
|
681
|
-
raise ValueError("invalid_decision")
|
|
682
|
-
my_current_position = _require_non_empty_text(
|
|
683
|
-
payload,
|
|
684
|
-
"my_current_position",
|
|
685
|
-
"missing_current_position",
|
|
686
|
-
)
|
|
687
|
-
opponent_claim = _require_non_empty_text(payload, "opponent_claim", "missing_opponent_claim")
|
|
688
|
-
checked_evidence = _require_non_empty_list(
|
|
689
|
-
payload,
|
|
690
|
-
"opponent_evidence_i_checked",
|
|
691
|
-
"missing_checked_evidence",
|
|
692
|
-
)
|
|
693
|
-
reasoning = _require_non_empty_text(payload, "reasoning", "missing_reasoning")
|
|
694
|
-
|
|
872
|
+
target_post_id, decision = _response_target_and_decision(board, agent_id, payload)
|
|
695
873
|
current = _current_round(board)
|
|
696
874
|
responses = current.setdefault("responses", [])
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
"decision": decision,
|
|
713
|
-
"my_current_position": my_current_position,
|
|
714
|
-
"opponent_claim": opponent_claim,
|
|
715
|
-
"opponent_evidence_i_checked": checked_evidence,
|
|
716
|
-
"reasoning": reasoning,
|
|
717
|
-
"confidence_after_review": _confidence(payload),
|
|
718
|
-
}
|
|
719
|
-
if decision == "concede":
|
|
720
|
-
response["why_opponent_is_right"] = _require_non_empty_text(
|
|
721
|
-
payload,
|
|
722
|
-
"why_opponent_is_right",
|
|
723
|
-
"shallow_concession",
|
|
724
|
-
)
|
|
725
|
-
response["accepted_evidence"] = _require_non_empty_list(
|
|
726
|
-
payload,
|
|
727
|
-
"accepted_evidence",
|
|
728
|
-
"shallow_concession",
|
|
729
|
-
)
|
|
730
|
-
response["why_my_previous_position_failed"] = _require_non_empty_text(
|
|
731
|
-
payload,
|
|
732
|
-
"why_my_previous_position_failed",
|
|
733
|
-
"shallow_concession",
|
|
734
|
-
)
|
|
735
|
-
response["position_delta"] = "changed"
|
|
736
|
-
response["still_disagree"] = False
|
|
737
|
-
elif decision == "revise":
|
|
738
|
-
response["accepted_part"] = _require_non_empty_text(
|
|
739
|
-
payload,
|
|
740
|
-
"accepted_part",
|
|
741
|
-
"vague_revision",
|
|
742
|
-
)
|
|
743
|
-
response["rejected_part"] = _require_non_empty_text(
|
|
744
|
-
payload,
|
|
745
|
-
"rejected_part",
|
|
746
|
-
"vague_revision",
|
|
747
|
-
)
|
|
748
|
-
response["updated_position"] = _require_non_empty_text(
|
|
749
|
-
payload,
|
|
750
|
-
"updated_position",
|
|
751
|
-
"vague_revision",
|
|
752
|
-
)
|
|
753
|
-
response["position_delta"] = str(payload.get("position_delta") or "narrowed")
|
|
754
|
-
response["still_disagree"] = _require_bool(payload, "still_disagree", True)
|
|
755
|
-
else:
|
|
756
|
-
response["why_opponent_is_wrong"] = _require_non_empty_text(
|
|
757
|
-
payload,
|
|
758
|
-
"why_opponent_is_wrong",
|
|
759
|
-
"unsupported_rebuttal",
|
|
760
|
-
)
|
|
761
|
-
counter_evidence = payload.get("counter_evidence")
|
|
762
|
-
counter_reasoning = payload.get("counter_reasoning")
|
|
763
|
-
if not counter_evidence and not counter_reasoning:
|
|
764
|
-
raise ValueError("unsupported_rebuttal")
|
|
765
|
-
if counter_evidence is not None:
|
|
766
|
-
if not isinstance(counter_evidence, list) or not counter_evidence:
|
|
767
|
-
raise ValueError("unsupported_rebuttal")
|
|
768
|
-
response["counter_evidence"] = counter_evidence
|
|
769
|
-
if counter_reasoning is not None:
|
|
770
|
-
response["counter_reasoning"] = str(counter_reasoning)
|
|
771
|
-
response["position_delta"] = "unchanged"
|
|
772
|
-
response["still_disagree"] = True
|
|
773
|
-
|
|
875
|
+
_validate_response_target_limit(
|
|
876
|
+
responses,
|
|
877
|
+
agent_id=agent_id,
|
|
878
|
+
target_post_id=target_post_id,
|
|
879
|
+
max_targets=int(config["max_rebuttal_targets_per_agent"]),
|
|
880
|
+
)
|
|
881
|
+
response = _build_response_base(
|
|
882
|
+
current_round=current_round,
|
|
883
|
+
agent_id=agent_id,
|
|
884
|
+
target_post_id=target_post_id,
|
|
885
|
+
decision=decision,
|
|
886
|
+
payload=payload,
|
|
887
|
+
response_number=len(responses) + 1,
|
|
888
|
+
)
|
|
889
|
+
_apply_decision_response(response, decision=decision, payload=payload)
|
|
774
890
|
responses.append(response)
|
|
775
891
|
_write_runtime_state(base_dir, board)
|
|
776
892
|
_append_audit(
|
|
@@ -801,6 +917,131 @@ def _write_actions_for_phase(
|
|
|
801
917
|
_write_json(base_dir / "actions.json", actions)
|
|
802
918
|
|
|
803
919
|
|
|
920
|
+
def _advance_publish_phase(
|
|
921
|
+
repo_root: Path,
|
|
922
|
+
*,
|
|
923
|
+
base_dir: Path,
|
|
924
|
+
discussion_id: str,
|
|
925
|
+
board: dict[str, Any],
|
|
926
|
+
current: dict[str, Any],
|
|
927
|
+
active_agents: list[str],
|
|
928
|
+
current_round: int,
|
|
929
|
+
) -> dict[str, Any]:
|
|
930
|
+
posted_agents = {str(post["agent_id"]) for post in current.get("posts", [])}
|
|
931
|
+
missing = sorted(set(active_agents) - posted_agents)
|
|
932
|
+
if missing:
|
|
933
|
+
raise ValueError(f"publish_incomplete:{','.join(missing)}")
|
|
934
|
+
board["round"]["phase"] = "respond"
|
|
935
|
+
_write_runtime_state(base_dir, board)
|
|
936
|
+
_write_actions_for_phase(base_dir, discussion_id, current_round, "respond")
|
|
937
|
+
_append_audit(
|
|
938
|
+
base_dir,
|
|
939
|
+
"advance",
|
|
940
|
+
{"round": current_round, "from": "publish", "to": "respond"},
|
|
941
|
+
)
|
|
942
|
+
return monitor_discussion(repo_root, discussion_id=discussion_id)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _close_discussion(
|
|
946
|
+
repo_root: Path,
|
|
947
|
+
*,
|
|
948
|
+
base_dir: Path,
|
|
949
|
+
discussion_id: str,
|
|
950
|
+
board: dict[str, Any],
|
|
951
|
+
current_round: int,
|
|
952
|
+
reason: str,
|
|
953
|
+
) -> dict[str, Any]:
|
|
954
|
+
board["round"]["phase"] = "closed"
|
|
955
|
+
board["termination"] = {"reason": reason}
|
|
956
|
+
_write_runtime_state(base_dir, board)
|
|
957
|
+
_write_json(base_dir / "actions.json", _empty_actions(discussion_id))
|
|
958
|
+
_append_audit(
|
|
959
|
+
base_dir,
|
|
960
|
+
"advance",
|
|
961
|
+
{"round": current_round, "from": "respond", "to": "closed", "reason": reason},
|
|
962
|
+
)
|
|
963
|
+
return finalize_discussion(repo_root, discussion_id=discussion_id)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _advance_next_round(
|
|
967
|
+
repo_root: Path,
|
|
968
|
+
*,
|
|
969
|
+
base_dir: Path,
|
|
970
|
+
discussion_id: str,
|
|
971
|
+
board: dict[str, Any],
|
|
972
|
+
current_round: int,
|
|
973
|
+
) -> dict[str, Any]:
|
|
974
|
+
next_round = current_round + 1
|
|
975
|
+
config = get_party_mode_v2_config(repo_root)
|
|
976
|
+
close_actions = []
|
|
977
|
+
if bool(config.get("fresh_context_per_round", True)):
|
|
978
|
+
close_actions = _build_close_actions(
|
|
979
|
+
base_dir,
|
|
980
|
+
_agent_lenses(base_dir),
|
|
981
|
+
round_number=current_round,
|
|
982
|
+
reason="fresh_context_per_round",
|
|
983
|
+
)
|
|
984
|
+
board["round"] = {"current": next_round, "max": board["round"]["max"], "phase": "publish"}
|
|
985
|
+
board.setdefault("rounds", []).append(
|
|
986
|
+
{"round": next_round, "posts": [], "responses": [], "moderator_events": []}
|
|
987
|
+
)
|
|
988
|
+
_write_runtime_state(base_dir, board)
|
|
989
|
+
_write_actions_for_phase(
|
|
990
|
+
base_dir,
|
|
991
|
+
discussion_id,
|
|
992
|
+
next_round,
|
|
993
|
+
"publish",
|
|
994
|
+
leading_actions=close_actions,
|
|
995
|
+
)
|
|
996
|
+
_append_audit(
|
|
997
|
+
base_dir,
|
|
998
|
+
"advance",
|
|
999
|
+
{"round": current_round, "from": "respond", "to": "publish", "next_round": next_round},
|
|
1000
|
+
)
|
|
1001
|
+
return monitor_discussion(repo_root, discussion_id=discussion_id)
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def _advance_respond_phase(
|
|
1005
|
+
repo_root: Path,
|
|
1006
|
+
*,
|
|
1007
|
+
base_dir: Path,
|
|
1008
|
+
discussion_id: str,
|
|
1009
|
+
board: dict[str, Any],
|
|
1010
|
+
current: dict[str, Any],
|
|
1011
|
+
active_agents: list[str],
|
|
1012
|
+
current_round: int,
|
|
1013
|
+
) -> dict[str, Any]:
|
|
1014
|
+
responded_agents = {str(response["agent_id"]) for response in current.get("responses", [])}
|
|
1015
|
+
missing = sorted(set(active_agents) - responded_agents)
|
|
1016
|
+
if missing:
|
|
1017
|
+
raise ValueError(f"respond_incomplete:{','.join(missing)}")
|
|
1018
|
+
if not any(bool(response.get("still_disagree")) for response in current.get("responses", [])):
|
|
1019
|
+
return _close_discussion(
|
|
1020
|
+
repo_root,
|
|
1021
|
+
base_dir=base_dir,
|
|
1022
|
+
discussion_id=discussion_id,
|
|
1023
|
+
board=board,
|
|
1024
|
+
current_round=current_round,
|
|
1025
|
+
reason="converged",
|
|
1026
|
+
)
|
|
1027
|
+
if current_round >= int(board["round"]["max"]):
|
|
1028
|
+
return _close_discussion(
|
|
1029
|
+
repo_root,
|
|
1030
|
+
base_dir=base_dir,
|
|
1031
|
+
discussion_id=discussion_id,
|
|
1032
|
+
board=board,
|
|
1033
|
+
current_round=current_round,
|
|
1034
|
+
reason="max_rounds_unconverged",
|
|
1035
|
+
)
|
|
1036
|
+
return _advance_next_round(
|
|
1037
|
+
repo_root,
|
|
1038
|
+
base_dir=base_dir,
|
|
1039
|
+
discussion_id=discussion_id,
|
|
1040
|
+
board=board,
|
|
1041
|
+
current_round=current_round,
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
|
|
804
1045
|
def advance_discussion(repo_root: Path, *, discussion_id: str) -> dict[str, Any]:
|
|
805
1046
|
base_dir = discussion_dir(repo_root, discussion_id)
|
|
806
1047
|
with _state_lock(base_dir):
|
|
@@ -809,85 +1050,28 @@ def advance_discussion(repo_root: Path, *, discussion_id: str) -> dict[str, Any]
|
|
|
809
1050
|
active_agents = _active_agent_ids(base_dir)
|
|
810
1051
|
current_round = int(board["round"]["current"])
|
|
811
1052
|
if board["round"]["phase"] == "publish":
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
base_dir,
|
|
821
|
-
"advance",
|
|
822
|
-
{"round": current_round, "from": "publish", "to": "respond"},
|
|
1053
|
+
return _advance_publish_phase(
|
|
1054
|
+
repo_root,
|
|
1055
|
+
base_dir=base_dir,
|
|
1056
|
+
discussion_id=discussion_id,
|
|
1057
|
+
board=board,
|
|
1058
|
+
current=current,
|
|
1059
|
+
active_agents=active_agents,
|
|
1060
|
+
current_round=current_round,
|
|
823
1061
|
)
|
|
824
|
-
return monitor_discussion(repo_root, discussion_id=discussion_id)
|
|
825
1062
|
|
|
826
1063
|
if board["round"]["phase"] != "respond":
|
|
827
1064
|
raise ValueError("phase_not_advanceable")
|
|
828
1065
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
_write_runtime_state(base_dir, board)
|
|
838
|
-
_write_json(base_dir / "actions.json", _empty_actions(discussion_id))
|
|
839
|
-
_append_audit(
|
|
840
|
-
base_dir,
|
|
841
|
-
"advance",
|
|
842
|
-
{"round": current_round, "from": "respond", "to": "closed", "reason": "converged"},
|
|
843
|
-
)
|
|
844
|
-
return finalize_discussion(repo_root, discussion_id=discussion_id)
|
|
845
|
-
|
|
846
|
-
if current_round >= int(board["round"]["max"]):
|
|
847
|
-
board["round"]["phase"] = "closed"
|
|
848
|
-
board["termination"] = {"reason": "max_rounds_unconverged"}
|
|
849
|
-
_write_runtime_state(base_dir, board)
|
|
850
|
-
_write_json(base_dir / "actions.json", _empty_actions(discussion_id))
|
|
851
|
-
_append_audit(
|
|
852
|
-
base_dir,
|
|
853
|
-
"advance",
|
|
854
|
-
{
|
|
855
|
-
"round": current_round,
|
|
856
|
-
"from": "respond",
|
|
857
|
-
"to": "closed",
|
|
858
|
-
"reason": "max_rounds_unconverged",
|
|
859
|
-
},
|
|
860
|
-
)
|
|
861
|
-
return finalize_discussion(repo_root, discussion_id=discussion_id)
|
|
862
|
-
|
|
863
|
-
next_round = current_round + 1
|
|
864
|
-
config = get_party_mode_v2_config(repo_root)
|
|
865
|
-
close_actions = []
|
|
866
|
-
if bool(config.get("fresh_context_per_round", True)):
|
|
867
|
-
close_actions = _build_close_actions(
|
|
868
|
-
base_dir,
|
|
869
|
-
_agent_lenses(base_dir),
|
|
870
|
-
round_number=current_round,
|
|
871
|
-
reason="fresh_context_per_round",
|
|
872
|
-
)
|
|
873
|
-
board["round"] = {"current": next_round, "max": board["round"]["max"], "phase": "publish"}
|
|
874
|
-
board.setdefault("rounds", []).append(
|
|
875
|
-
{"round": next_round, "posts": [], "responses": [], "moderator_events": []}
|
|
1066
|
+
return _advance_respond_phase(
|
|
1067
|
+
repo_root,
|
|
1068
|
+
base_dir=base_dir,
|
|
1069
|
+
discussion_id=discussion_id,
|
|
1070
|
+
board=board,
|
|
1071
|
+
current=current,
|
|
1072
|
+
active_agents=active_agents,
|
|
1073
|
+
current_round=current_round,
|
|
876
1074
|
)
|
|
877
|
-
_write_runtime_state(base_dir, board)
|
|
878
|
-
_write_actions_for_phase(
|
|
879
|
-
base_dir,
|
|
880
|
-
discussion_id,
|
|
881
|
-
next_round,
|
|
882
|
-
"publish",
|
|
883
|
-
leading_actions=close_actions,
|
|
884
|
-
)
|
|
885
|
-
_append_audit(
|
|
886
|
-
base_dir,
|
|
887
|
-
"advance",
|
|
888
|
-
{"round": current_round, "from": "respond", "to": "publish", "next_round": next_round},
|
|
889
|
-
)
|
|
890
|
-
return monitor_discussion(repo_root, discussion_id=discussion_id)
|
|
891
1075
|
|
|
892
1076
|
|
|
893
1077
|
def _set_all_agents_status(base_dir: Path, status: str) -> None:
|
|
@@ -905,6 +1089,132 @@ def _default_agent_status(action_type: str, outcome: str) -> str:
|
|
|
905
1089
|
return "active"
|
|
906
1090
|
|
|
907
1091
|
|
|
1092
|
+
def _ensure_finalizable(
|
|
1093
|
+
base_dir: Path,
|
|
1094
|
+
discussion_id: str,
|
|
1095
|
+
board: dict[str, Any],
|
|
1096
|
+
*,
|
|
1097
|
+
manual_termination: bool,
|
|
1098
|
+
) -> None:
|
|
1099
|
+
if board["round"]["phase"] == "closed":
|
|
1100
|
+
return
|
|
1101
|
+
if not manual_termination:
|
|
1102
|
+
raise ValueError("finalize_requires_closed_discussion")
|
|
1103
|
+
board["round"]["phase"] = "closed"
|
|
1104
|
+
board["termination"] = {"reason": "manual_terminated"}
|
|
1105
|
+
_write_runtime_state(base_dir, board)
|
|
1106
|
+
_write_json(base_dir / "actions.json", _empty_actions(discussion_id))
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def _discussion_posts(board: dict[str, Any]) -> list[dict[str, Any]]:
|
|
1110
|
+
return [post for item in board.get("rounds", []) for post in item.get("posts", [])]
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
def _discussion_responses(board: dict[str, Any]) -> list[dict[str, Any]]:
|
|
1114
|
+
return [
|
|
1115
|
+
response
|
|
1116
|
+
for item in board.get("rounds", [])
|
|
1117
|
+
for response in item.get("responses", [])
|
|
1118
|
+
]
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def _round_responses(board: dict[str, Any], round_number: int) -> list[dict[str, Any]]:
|
|
1122
|
+
return [
|
|
1123
|
+
response
|
|
1124
|
+
for item in board.get("rounds", [])
|
|
1125
|
+
if int(item.get("round", -1)) == round_number
|
|
1126
|
+
for response in item.get("responses", [])
|
|
1127
|
+
]
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def _historical_disagreements(
|
|
1131
|
+
board: dict[str, Any],
|
|
1132
|
+
current_round: int,
|
|
1133
|
+
) -> list[dict[str, Any]]:
|
|
1134
|
+
return [
|
|
1135
|
+
response
|
|
1136
|
+
for item in board.get("rounds", [])
|
|
1137
|
+
if int(item.get("round", -1)) < current_round
|
|
1138
|
+
for response in item.get("responses", [])
|
|
1139
|
+
if response.get("still_disagree")
|
|
1140
|
+
]
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def _build_final_report(
|
|
1144
|
+
*,
|
|
1145
|
+
discussion_id: str,
|
|
1146
|
+
board: dict[str, Any],
|
|
1147
|
+
posts: list[dict[str, Any]],
|
|
1148
|
+
responses: list[dict[str, Any]],
|
|
1149
|
+
current_responses: list[dict[str, Any]],
|
|
1150
|
+
historical_disagreements: list[dict[str, Any]],
|
|
1151
|
+
stop_reason: str,
|
|
1152
|
+
) -> dict[str, Any]:
|
|
1153
|
+
current_round = int(board["round"]["current"])
|
|
1154
|
+
current_unresolved = [
|
|
1155
|
+
response for response in current_responses if response.get("still_disagree")
|
|
1156
|
+
]
|
|
1157
|
+
board_status = {
|
|
1158
|
+
"round": current_round,
|
|
1159
|
+
"phase": board["round"]["phase"],
|
|
1160
|
+
"max_rounds": board["round"]["max"],
|
|
1161
|
+
"termination_reason": stop_reason,
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
"schema_version": 1,
|
|
1165
|
+
"discussion_id": discussion_id,
|
|
1166
|
+
"terminal": True,
|
|
1167
|
+
"stop_reason": stop_reason,
|
|
1168
|
+
"board_status": board_status,
|
|
1169
|
+
"next_actions": [],
|
|
1170
|
+
"pro": _report_pro_posts(posts),
|
|
1171
|
+
"con": _report_con_responses(responses),
|
|
1172
|
+
"changed_positions": _responses_with_delta(responses, "changed"),
|
|
1173
|
+
"maintained_positions": _responses_with_delta(responses, "unchanged"),
|
|
1174
|
+
"revised_positions": _responses_with_decision(responses, "revise"),
|
|
1175
|
+
"current_unresolved_disagreements": current_unresolved,
|
|
1176
|
+
"historical_disagreements": historical_disagreements,
|
|
1177
|
+
"unresolved_disagreements": current_unresolved,
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def _report_pro_posts(posts: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
1182
|
+
return [
|
|
1183
|
+
{"agent_id": post["agent_id"], "claim": post["claim"], "evidence": post["evidence"]}
|
|
1184
|
+
for post in posts
|
|
1185
|
+
]
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
def _report_con_responses(responses: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
1189
|
+
return [
|
|
1190
|
+
{
|
|
1191
|
+
"agent_id": response["agent_id"],
|
|
1192
|
+
"target_post_id": response["target_post_id"],
|
|
1193
|
+
"reasoning": response["reasoning"],
|
|
1194
|
+
}
|
|
1195
|
+
for response in responses
|
|
1196
|
+
if response.get("decision") == "maintain"
|
|
1197
|
+
]
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def _responses_with_delta(
|
|
1201
|
+
responses: list[dict[str, Any]],
|
|
1202
|
+
position_delta: str,
|
|
1203
|
+
) -> list[dict[str, Any]]:
|
|
1204
|
+
return [
|
|
1205
|
+
response for response in responses if response.get("position_delta") == position_delta
|
|
1206
|
+
]
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
def _responses_with_decision(
|
|
1210
|
+
responses: list[dict[str, Any]],
|
|
1211
|
+
decision: str,
|
|
1212
|
+
) -> list[dict[str, Any]]:
|
|
1213
|
+
return [
|
|
1214
|
+
response for response in responses if response.get("decision") == decision
|
|
1215
|
+
]
|
|
1216
|
+
|
|
1217
|
+
|
|
908
1218
|
def finalize_discussion(
|
|
909
1219
|
repo_root: Path,
|
|
910
1220
|
*,
|
|
@@ -914,77 +1224,24 @@ def finalize_discussion(
|
|
|
914
1224
|
base_dir = discussion_dir(repo_root, discussion_id)
|
|
915
1225
|
with _state_lock(base_dir):
|
|
916
1226
|
board = _read_json(base_dir / "board.json")
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
board
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
_write_json(base_dir / "actions.json", _empty_actions(discussion_id))
|
|
1227
|
+
_ensure_finalizable(
|
|
1228
|
+
base_dir,
|
|
1229
|
+
discussion_id,
|
|
1230
|
+
board,
|
|
1231
|
+
manual_termination=manual_termination,
|
|
1232
|
+
)
|
|
924
1233
|
_set_all_agents_status(base_dir, "closed")
|
|
925
1234
|
current_round = int(board["round"]["current"])
|
|
926
|
-
posts = [post for item in board.get("rounds", []) for post in item.get("posts", [])]
|
|
927
|
-
responses = [
|
|
928
|
-
response
|
|
929
|
-
for item in board.get("rounds", [])
|
|
930
|
-
for response in item.get("responses", [])
|
|
931
|
-
]
|
|
932
|
-
current_responses = [
|
|
933
|
-
response
|
|
934
|
-
for item in board.get("rounds", [])
|
|
935
|
-
if int(item.get("round", -1)) == current_round
|
|
936
|
-
for response in item.get("responses", [])
|
|
937
|
-
]
|
|
938
|
-
historical_disagreements = [
|
|
939
|
-
response
|
|
940
|
-
for item in board.get("rounds", [])
|
|
941
|
-
if int(item.get("round", -1)) < current_round
|
|
942
|
-
for response in item.get("responses", [])
|
|
943
|
-
if response.get("still_disagree")
|
|
944
|
-
]
|
|
945
1235
|
stop_reason = board.get("termination", {}).get("reason") or "manual_finalize"
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
"board_status": {
|
|
956
|
-
"round": current_round,
|
|
957
|
-
"phase": board["round"]["phase"],
|
|
958
|
-
"max_rounds": board["round"]["max"],
|
|
959
|
-
"termination_reason": stop_reason,
|
|
960
|
-
},
|
|
961
|
-
"next_actions": [],
|
|
962
|
-
"pro": [
|
|
963
|
-
{"agent_id": post["agent_id"], "claim": post["claim"], "evidence": post["evidence"]}
|
|
964
|
-
for post in posts
|
|
965
|
-
],
|
|
966
|
-
"con": [
|
|
967
|
-
{
|
|
968
|
-
"agent_id": response["agent_id"],
|
|
969
|
-
"target_post_id": response["target_post_id"],
|
|
970
|
-
"reasoning": response["reasoning"],
|
|
971
|
-
}
|
|
972
|
-
for response in responses
|
|
973
|
-
if response.get("decision") == "maintain"
|
|
974
|
-
],
|
|
975
|
-
"changed_positions": [
|
|
976
|
-
response for response in responses if response.get("position_delta") == "changed"
|
|
977
|
-
],
|
|
978
|
-
"maintained_positions": [
|
|
979
|
-
response for response in responses if response.get("position_delta") == "unchanged"
|
|
980
|
-
],
|
|
981
|
-
"revised_positions": [
|
|
982
|
-
response for response in responses if response.get("decision") == "revise"
|
|
983
|
-
],
|
|
984
|
-
"current_unresolved_disagreements": current_unresolved,
|
|
985
|
-
"historical_disagreements": historical_disagreements,
|
|
986
|
-
"unresolved_disagreements": unresolved,
|
|
987
|
-
}
|
|
1236
|
+
report = _build_final_report(
|
|
1237
|
+
discussion_id=discussion_id,
|
|
1238
|
+
board=board,
|
|
1239
|
+
posts=_discussion_posts(board),
|
|
1240
|
+
responses=_discussion_responses(board),
|
|
1241
|
+
current_responses=_round_responses(board, current_round),
|
|
1242
|
+
historical_disagreements=_historical_disagreements(board, current_round),
|
|
1243
|
+
stop_reason=stop_reason,
|
|
1244
|
+
)
|
|
988
1245
|
reports_dir = base_dir / "reports"
|
|
989
1246
|
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
990
1247
|
_write_json(reports_dir / "final.json", report)
|
|
@@ -1087,79 +1344,68 @@ def _read_payload(path: Path) -> dict[str, Any]:
|
|
|
1087
1344
|
return payload
|
|
1088
1345
|
|
|
1089
1346
|
|
|
1347
|
+
def _command_result(repo_root: Path, args: argparse.Namespace) -> dict[str, Any]:
|
|
1348
|
+
if args.command == "init":
|
|
1349
|
+
return init_discussion(
|
|
1350
|
+
repo_root,
|
|
1351
|
+
discussion_id=args.discussion_id,
|
|
1352
|
+
topic=args.topic,
|
|
1353
|
+
agent_specs=args.agent,
|
|
1354
|
+
)
|
|
1355
|
+
if args.command == "view":
|
|
1356
|
+
return view_discussion(
|
|
1357
|
+
repo_root,
|
|
1358
|
+
discussion_id=args.discussion_id,
|
|
1359
|
+
agent_id=args.agent_id,
|
|
1360
|
+
)
|
|
1361
|
+
if args.command == "monitor":
|
|
1362
|
+
return monitor_discussion(repo_root, discussion_id=args.discussion_id)
|
|
1363
|
+
if args.command == "post":
|
|
1364
|
+
return _payload_submission_result(repo_root, args, responder=False)
|
|
1365
|
+
if args.command == "respond":
|
|
1366
|
+
return _payload_submission_result(repo_root, args, responder=True)
|
|
1367
|
+
if args.command == "advance":
|
|
1368
|
+
return advance_discussion(repo_root, discussion_id=args.discussion_id)
|
|
1369
|
+
if args.command == "finalize":
|
|
1370
|
+
return finalize_discussion(
|
|
1371
|
+
repo_root,
|
|
1372
|
+
discussion_id=args.discussion_id,
|
|
1373
|
+
manual_termination=args.manual_termination,
|
|
1374
|
+
)
|
|
1375
|
+
if args.command == "record-action-result":
|
|
1376
|
+
return record_action_result(
|
|
1377
|
+
repo_root,
|
|
1378
|
+
discussion_id=args.discussion_id,
|
|
1379
|
+
payload=_read_payload(args.file),
|
|
1380
|
+
)
|
|
1381
|
+
raise ValueError(f"unknown command: {args.command}")
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
def _payload_submission_result(
|
|
1385
|
+
repo_root: Path,
|
|
1386
|
+
args: argparse.Namespace,
|
|
1387
|
+
*,
|
|
1388
|
+
responder: bool,
|
|
1389
|
+
) -> dict[str, Any]:
|
|
1390
|
+
submit = respond_submission if responder else post_submission
|
|
1391
|
+
return submit(
|
|
1392
|
+
repo_root,
|
|
1393
|
+
discussion_id=args.discussion_id,
|
|
1394
|
+
agent_id=args.agent_id,
|
|
1395
|
+
payload=_read_payload(args.file),
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
|
|
1090
1399
|
def main(argv: list[str] | None = None) -> int:
|
|
1091
1400
|
parser = build_parser()
|
|
1092
1401
|
args = parser.parse_args(argv)
|
|
1093
1402
|
repo_root = args.repo_root or get_repo_root()
|
|
1094
1403
|
try:
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
init_discussion(
|
|
1098
|
-
repo_root,
|
|
1099
|
-
discussion_id=args.discussion_id,
|
|
1100
|
-
topic=args.topic,
|
|
1101
|
-
agent_specs=args.agent,
|
|
1102
|
-
)
|
|
1103
|
-
)
|
|
1104
|
-
return 0
|
|
1105
|
-
if args.command == "view":
|
|
1106
|
-
_print_json(
|
|
1107
|
-
view_discussion(
|
|
1108
|
-
repo_root,
|
|
1109
|
-
discussion_id=args.discussion_id,
|
|
1110
|
-
agent_id=args.agent_id,
|
|
1111
|
-
)
|
|
1112
|
-
)
|
|
1113
|
-
return 0
|
|
1114
|
-
if args.command == "monitor":
|
|
1115
|
-
_print_json(monitor_discussion(repo_root, discussion_id=args.discussion_id))
|
|
1116
|
-
return 0
|
|
1117
|
-
if args.command == "post":
|
|
1118
|
-
_print_json(
|
|
1119
|
-
post_submission(
|
|
1120
|
-
repo_root,
|
|
1121
|
-
discussion_id=args.discussion_id,
|
|
1122
|
-
agent_id=args.agent_id,
|
|
1123
|
-
payload=_read_payload(args.file),
|
|
1124
|
-
)
|
|
1125
|
-
)
|
|
1126
|
-
return 0
|
|
1127
|
-
if args.command == "respond":
|
|
1128
|
-
_print_json(
|
|
1129
|
-
respond_submission(
|
|
1130
|
-
repo_root,
|
|
1131
|
-
discussion_id=args.discussion_id,
|
|
1132
|
-
agent_id=args.agent_id,
|
|
1133
|
-
payload=_read_payload(args.file),
|
|
1134
|
-
)
|
|
1135
|
-
)
|
|
1136
|
-
return 0
|
|
1137
|
-
if args.command == "advance":
|
|
1138
|
-
_print_json(advance_discussion(repo_root, discussion_id=args.discussion_id))
|
|
1139
|
-
return 0
|
|
1140
|
-
if args.command == "finalize":
|
|
1141
|
-
_print_json(
|
|
1142
|
-
finalize_discussion(
|
|
1143
|
-
repo_root,
|
|
1144
|
-
discussion_id=args.discussion_id,
|
|
1145
|
-
manual_termination=args.manual_termination,
|
|
1146
|
-
)
|
|
1147
|
-
)
|
|
1148
|
-
return 0
|
|
1149
|
-
if args.command == "record-action-result":
|
|
1150
|
-
_print_json(
|
|
1151
|
-
record_action_result(
|
|
1152
|
-
repo_root,
|
|
1153
|
-
discussion_id=args.discussion_id,
|
|
1154
|
-
payload=_read_payload(args.file),
|
|
1155
|
-
)
|
|
1156
|
-
)
|
|
1157
|
-
return 0
|
|
1404
|
+
_print_json(_command_result(repo_root, args))
|
|
1405
|
+
return 0
|
|
1158
1406
|
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
1159
1407
|
print(f"Error: {error}", file=sys.stderr)
|
|
1160
1408
|
return 2
|
|
1161
|
-
parser.print_usage()
|
|
1162
|
-
return 2
|
|
1163
1409
|
|
|
1164
1410
|
|
|
1165
1411
|
if __name__ == "__main__":
|