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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Pure Party Mode V2 final report projection helpers."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def discussion_posts(board: dict[str, Any]) -> list[dict[str, Any]]:
|
|
13
|
+
return [post for item in board.get("rounds", []) for post in item.get("posts", [])]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def discussion_responses(board: dict[str, Any]) -> list[dict[str, Any]]:
|
|
17
|
+
return [
|
|
18
|
+
response
|
|
19
|
+
for item in board.get("rounds", [])
|
|
20
|
+
for response in item.get("responses", [])
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def round_responses(board: dict[str, Any], round_number: int) -> list[dict[str, Any]]:
|
|
25
|
+
return [
|
|
26
|
+
response
|
|
27
|
+
for item in board.get("rounds", [])
|
|
28
|
+
if int(item.get("round", -1)) == round_number
|
|
29
|
+
for response in item.get("responses", [])
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def historical_disagreements(board: dict[str, Any], current_round: int) -> list[dict[str, Any]]:
|
|
34
|
+
return [
|
|
35
|
+
response
|
|
36
|
+
for item in board.get("rounds", [])
|
|
37
|
+
if int(item.get("round", -1)) < current_round
|
|
38
|
+
for response in item.get("responses", [])
|
|
39
|
+
if response.get("still_disagree")
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def round_summary_phase(board: dict[str, Any], round_number: int) -> str:
|
|
44
|
+
if round_number == int(board["round"]["current"]):
|
|
45
|
+
return str(board["round"]["phase"])
|
|
46
|
+
return "completed"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def rounds_summary(board: dict[str, Any]) -> list[dict[str, Any]]:
|
|
50
|
+
summaries: list[dict[str, Any]] = []
|
|
51
|
+
for item in board.get("rounds", []):
|
|
52
|
+
round_number = int(item.get("round", 0))
|
|
53
|
+
posts = item.get("posts", [])
|
|
54
|
+
responses = item.get("responses", [])
|
|
55
|
+
unresolved = [response for response in responses if response.get("still_disagree")]
|
|
56
|
+
summaries.append(
|
|
57
|
+
{
|
|
58
|
+
"round": round_number,
|
|
59
|
+
"phase": round_summary_phase(board, round_number),
|
|
60
|
+
"post_count": len(posts),
|
|
61
|
+
"response_count": len(responses),
|
|
62
|
+
"unresolved_count": len(unresolved),
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
return summaries
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def action_results_summary(base_dir: Path) -> list[dict[str, Any]]:
|
|
69
|
+
history_path = base_dir / "action_history.jsonl"
|
|
70
|
+
if not history_path.is_file():
|
|
71
|
+
return []
|
|
72
|
+
results: list[dict[str, Any]] = []
|
|
73
|
+
with history_path.open("r", encoding="utf-8") as handle:
|
|
74
|
+
for line in handle:
|
|
75
|
+
if not line.strip():
|
|
76
|
+
continue
|
|
77
|
+
entry = json.loads(line)
|
|
78
|
+
if entry.get("event") != "action-result":
|
|
79
|
+
continue
|
|
80
|
+
payload = entry.get("payload", {})
|
|
81
|
+
summary: dict[str, Any] = {
|
|
82
|
+
"action_id": payload.get("action_id"),
|
|
83
|
+
"type": payload.get("type"),
|
|
84
|
+
"outcome": payload.get("outcome"),
|
|
85
|
+
}
|
|
86
|
+
if payload.get("agent_id"):
|
|
87
|
+
summary["agent_id"] = payload["agent_id"]
|
|
88
|
+
results.append(summary)
|
|
89
|
+
return results
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def accepted_evidence_summary(responses: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
93
|
+
summaries: list[dict[str, Any]] = []
|
|
94
|
+
for response in responses:
|
|
95
|
+
if response.get("decision") != "concede":
|
|
96
|
+
continue
|
|
97
|
+
evidence = response.get("accepted_evidence", [])
|
|
98
|
+
if not evidence:
|
|
99
|
+
continue
|
|
100
|
+
summaries.append(
|
|
101
|
+
{
|
|
102
|
+
"agent_id": response.get("agent_id"),
|
|
103
|
+
"target_post_id": response.get("target_post_id"),
|
|
104
|
+
"accepted_evidence": evidence,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return summaries
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def report_pro_posts(posts: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
111
|
+
return [
|
|
112
|
+
{"agent_id": post["agent_id"], "claim": post["claim"], "evidence": post["evidence"]}
|
|
113
|
+
for post in posts
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def report_con_responses(responses: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
118
|
+
return [
|
|
119
|
+
{
|
|
120
|
+
"agent_id": response["agent_id"],
|
|
121
|
+
"target_post_id": response["target_post_id"],
|
|
122
|
+
"reasoning": response["reasoning"],
|
|
123
|
+
}
|
|
124
|
+
for response in responses
|
|
125
|
+
if response.get("decision") == "maintain"
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def responses_with_delta(responses: list[dict[str, Any]], position_delta: str) -> list[dict[str, Any]]:
|
|
130
|
+
return [response for response in responses if response.get("position_delta") == position_delta]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def responses_with_decision(responses: list[dict[str, Any]], decision: str) -> list[dict[str, Any]]:
|
|
134
|
+
return [response for response in responses if response.get("decision") == decision]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def build_final_report(
|
|
138
|
+
*,
|
|
139
|
+
discussion_id: str,
|
|
140
|
+
board: dict[str, Any],
|
|
141
|
+
action_results: list[dict[str, Any]],
|
|
142
|
+
stop_reason: str,
|
|
143
|
+
) -> dict[str, Any]:
|
|
144
|
+
"""Build a final advisory report from board facts without file writes."""
|
|
145
|
+
|
|
146
|
+
current_round = int(board["round"]["current"])
|
|
147
|
+
posts = discussion_posts(board)
|
|
148
|
+
responses = discussion_responses(board)
|
|
149
|
+
current_responses = round_responses(board, current_round)
|
|
150
|
+
current_unresolved = [
|
|
151
|
+
response for response in current_responses if response.get("still_disagree")
|
|
152
|
+
]
|
|
153
|
+
board_status = {
|
|
154
|
+
"round": current_round,
|
|
155
|
+
"phase": board["round"]["phase"],
|
|
156
|
+
"max_rounds": board["round"]["max"],
|
|
157
|
+
"termination_reason": stop_reason,
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
"schema_version": 1,
|
|
161
|
+
"discussion_id": discussion_id,
|
|
162
|
+
"terminal": True,
|
|
163
|
+
"stop_reason": stop_reason,
|
|
164
|
+
"board_status": board_status,
|
|
165
|
+
"rounds_summary": rounds_summary(board),
|
|
166
|
+
"action_results": list(action_results),
|
|
167
|
+
"accepted_evidence": accepted_evidence_summary(responses),
|
|
168
|
+
"next_actions": [],
|
|
169
|
+
"pro": report_pro_posts(posts),
|
|
170
|
+
"con": report_con_responses(responses),
|
|
171
|
+
"changed_positions": responses_with_delta(responses, "changed"),
|
|
172
|
+
"maintained_positions": responses_with_delta(responses, "unchanged"),
|
|
173
|
+
"revised_positions": responses_with_decision(responses, "revise"),
|
|
174
|
+
"current_unresolved_disagreements": current_unresolved,
|
|
175
|
+
"historical_disagreements": historical_disagreements(board, current_round),
|
|
176
|
+
"unresolved_disagreements": current_unresolved,
|
|
177
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Pure Party Mode V2 board state transitions and projections."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
CONFIDENCE_VALUES = {"low", "medium", "high"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def round_empty_state(items: list[Any], phase: str, *, item_type: str) -> str | None:
|
|
13
|
+
if items:
|
|
14
|
+
return None
|
|
15
|
+
if item_type == "posts":
|
|
16
|
+
if phase == "publish":
|
|
17
|
+
return "waiting_for_current_round_posts"
|
|
18
|
+
if phase == "closed":
|
|
19
|
+
return "discussion_closed"
|
|
20
|
+
return "no_current_round_posts"
|
|
21
|
+
if phase == "publish":
|
|
22
|
+
return "responses_not_open_yet"
|
|
23
|
+
if phase == "respond":
|
|
24
|
+
return "waiting_for_current_round_responses"
|
|
25
|
+
if phase == "closed":
|
|
26
|
+
return "discussion_closed"
|
|
27
|
+
return "no_current_round_responses"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_public_round(board: dict[str, Any]) -> dict[str, Any]:
|
|
31
|
+
"""Project the public current-round board without file IO."""
|
|
32
|
+
|
|
33
|
+
current_round_number = int(board["round"]["current"])
|
|
34
|
+
current = next(
|
|
35
|
+
(
|
|
36
|
+
item
|
|
37
|
+
for item in board.get("rounds", [])
|
|
38
|
+
if int(item.get("round", -1)) == current_round_number
|
|
39
|
+
),
|
|
40
|
+
{"round": current_round_number, "posts": [], "responses": [], "moderator_events": []},
|
|
41
|
+
)
|
|
42
|
+
posts = list(current.get("posts", []))
|
|
43
|
+
responses = list(current.get("responses", []))
|
|
44
|
+
phase = board["round"]["phase"]
|
|
45
|
+
expected_next_action = {
|
|
46
|
+
"publish": "post",
|
|
47
|
+
"respond": "respond",
|
|
48
|
+
"closed": "finalized",
|
|
49
|
+
}.get(str(phase), "monitor")
|
|
50
|
+
return {
|
|
51
|
+
"schema_version": 1,
|
|
52
|
+
"discussion_id": board["discussion_id"],
|
|
53
|
+
"round": current_round_number,
|
|
54
|
+
"phase": phase,
|
|
55
|
+
"topic": board["topic"],
|
|
56
|
+
"visible_posts": posts,
|
|
57
|
+
"visible_responses": responses,
|
|
58
|
+
"moderator_events": list(current.get("moderator_events", [])),
|
|
59
|
+
"empty_state": {
|
|
60
|
+
"visible_posts": round_empty_state(posts, phase, item_type="posts"),
|
|
61
|
+
"visible_responses": round_empty_state(responses, phase, item_type="responses"),
|
|
62
|
+
},
|
|
63
|
+
"expected_next_action": expected_next_action,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def initial_board(
|
|
68
|
+
discussion_id: str,
|
|
69
|
+
topic: str,
|
|
70
|
+
max_rounds: int,
|
|
71
|
+
*,
|
|
72
|
+
host_id: str | None = None,
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
board: dict[str, Any] = {
|
|
75
|
+
"schema_version": 1,
|
|
76
|
+
"discussion_id": discussion_id,
|
|
77
|
+
"topic": topic,
|
|
78
|
+
"round": {"current": 1, "max": max_rounds, "phase": "publish"},
|
|
79
|
+
"rounds": [{"round": 1, "posts": [], "responses": [], "moderator_events": []}],
|
|
80
|
+
"termination": {"reason": None},
|
|
81
|
+
}
|
|
82
|
+
if host_id is not None:
|
|
83
|
+
board["host_id"] = host_id
|
|
84
|
+
return board
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def initial_agents_state(discussion_id: str, agents: list[dict[str, str]]) -> dict[str, Any]:
|
|
88
|
+
return {
|
|
89
|
+
"schema_version": 1,
|
|
90
|
+
"discussion_id": discussion_id,
|
|
91
|
+
"agents": [
|
|
92
|
+
{
|
|
93
|
+
"agent_id": agent["agent_id"],
|
|
94
|
+
"lens": agent["lens"],
|
|
95
|
+
"status": "pending",
|
|
96
|
+
"drift_warnings": 0,
|
|
97
|
+
"host_child_id": None,
|
|
98
|
+
}
|
|
99
|
+
for agent in agents
|
|
100
|
+
],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def current_round(board: dict[str, Any]) -> dict[str, Any]:
|
|
105
|
+
current_round_number = int(board["round"]["current"])
|
|
106
|
+
for item in board.get("rounds", []):
|
|
107
|
+
if int(item.get("round", -1)) == current_round_number:
|
|
108
|
+
return item
|
|
109
|
+
item = {"round": current_round_number, "posts": [], "responses": [], "moderator_events": []}
|
|
110
|
+
board.setdefault("rounds", []).append(item)
|
|
111
|
+
return item
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def advance_board_state(
|
|
115
|
+
board: dict[str, Any],
|
|
116
|
+
*,
|
|
117
|
+
active_agent_ids: list[str],
|
|
118
|
+
fresh_context_per_round: bool,
|
|
119
|
+
) -> dict[str, Any]:
|
|
120
|
+
"""Advance the Party board in memory and return action/report facts."""
|
|
121
|
+
|
|
122
|
+
current = current_round(board)
|
|
123
|
+
current_round_number = int(board["round"]["current"])
|
|
124
|
+
phase = board["round"]["phase"]
|
|
125
|
+
if phase == "publish":
|
|
126
|
+
posted_agents = {str(post["agent_id"]) for post in current.get("posts", [])}
|
|
127
|
+
missing = sorted(set(active_agent_ids) - posted_agents)
|
|
128
|
+
if missing:
|
|
129
|
+
raise ValueError(f"publish_incomplete:{','.join(missing)}")
|
|
130
|
+
board["round"]["phase"] = "respond"
|
|
131
|
+
return {
|
|
132
|
+
"round": current_round_number,
|
|
133
|
+
"from": "publish",
|
|
134
|
+
"to": "respond",
|
|
135
|
+
"next_actions": {"round": current_round_number, "phase": "respond"},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if phase != "respond":
|
|
139
|
+
raise ValueError("phase_not_advanceable")
|
|
140
|
+
|
|
141
|
+
responded_agents = {str(response["agent_id"]) for response in current.get("responses", [])}
|
|
142
|
+
missing = sorted(set(active_agent_ids) - responded_agents)
|
|
143
|
+
if missing:
|
|
144
|
+
raise ValueError(f"respond_incomplete:{','.join(missing)}")
|
|
145
|
+
if not any(bool(response.get("still_disagree")) for response in current.get("responses", [])):
|
|
146
|
+
board["round"]["phase"] = "closed"
|
|
147
|
+
board["termination"] = {"reason": "converged"}
|
|
148
|
+
return {"round": current_round_number, "from": "respond", "to": "closed", "reason": "converged"}
|
|
149
|
+
if current_round_number >= int(board["round"]["max"]):
|
|
150
|
+
board["round"]["phase"] = "closed"
|
|
151
|
+
board["termination"] = {"reason": "max_rounds_unconverged"}
|
|
152
|
+
return {
|
|
153
|
+
"round": current_round_number,
|
|
154
|
+
"from": "respond",
|
|
155
|
+
"to": "closed",
|
|
156
|
+
"reason": "max_rounds_unconverged",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
next_round_number = current_round_number + 1
|
|
160
|
+
board["round"] = {
|
|
161
|
+
"current": next_round_number,
|
|
162
|
+
"max": board["round"]["max"],
|
|
163
|
+
"phase": "publish",
|
|
164
|
+
}
|
|
165
|
+
board.setdefault("rounds", []).append(
|
|
166
|
+
{"round": next_round_number, "posts": [], "responses": [], "moderator_events": []}
|
|
167
|
+
)
|
|
168
|
+
transition: dict[str, Any] = {
|
|
169
|
+
"round": current_round_number,
|
|
170
|
+
"from": "respond",
|
|
171
|
+
"to": "publish",
|
|
172
|
+
"next_round": next_round_number,
|
|
173
|
+
"next_actions": {"round": next_round_number, "phase": "publish"},
|
|
174
|
+
}
|
|
175
|
+
if fresh_context_per_round:
|
|
176
|
+
transition["close_children_reason"] = "fresh_context_per_round"
|
|
177
|
+
return transition
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def ensure_finalizable(board: dict[str, Any], *, manual_termination: bool) -> bool:
|
|
181
|
+
"""Close a board in memory for manual finalization when allowed."""
|
|
182
|
+
|
|
183
|
+
if board["round"]["phase"] == "closed":
|
|
184
|
+
return False
|
|
185
|
+
if not manual_termination:
|
|
186
|
+
raise ValueError("finalize_requires_closed_discussion")
|
|
187
|
+
board["round"]["phase"] = "closed"
|
|
188
|
+
board["termination"] = {"reason": "manual_terminated"}
|
|
189
|
+
return True
|
|
@@ -20,12 +20,24 @@ Run diagnostics through the common runner:
|
|
|
20
20
|
.cowork-flow/run doctor --task-hygiene --json
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
## Support Triage
|
|
24
|
+
|
|
25
|
+
Use runtime-health when the user reports host asset drift, hook drift, fixed-agent
|
|
26
|
+
binding failures, stale completed tasks, missing task context, or template/source
|
|
27
|
+
checkout health issues. Start with the narrowest doctor command that matches the
|
|
28
|
+
symptom, then broaden to `doctor --all` only when the focused result is
|
|
29
|
+
insufficient.
|
|
30
|
+
|
|
31
|
+
Runtime-health output is diagnostic evidence. It may provide command hints, but
|
|
32
|
+
it must not replace `task next --json` as the workflow router and must not mutate
|
|
33
|
+
lifecycle state.
|
|
34
|
+
|
|
23
35
|
## Boundaries
|
|
24
36
|
|
|
25
37
|
- Diagnostics may inspect runtime contracts, host assets, hooks, and template files.
|
|
26
38
|
- Installed projects validate only detected host platforms and do not require a source `template/` tree.
|
|
27
|
-
- Source checkouts distinguish
|
|
28
|
-
- Source checkout health must not force-track
|
|
39
|
+
- Source checkouts distinguish ignored local live runtime from the template distribution source.
|
|
40
|
+
- Source checkout health must not force-track `.cowork-flow/` runtime files; ignored local live runtime files are checked only when present.
|
|
29
41
|
- Source checkouts also validate Skill replica parity for detected installed host targets.
|
|
30
42
|
- stale task hygiene reports completed-unarchived tasks, active-unbound tasks, and missing task context with command hints only.
|
|
31
43
|
- Diagnostics must not mutate task lifecycle state.
|