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.
Files changed (90) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +102 -23
  3. package/package.json +8 -3
  4. package/src/cli.js +6 -0
  5. package/src/commands/init.js +0 -83
  6. package/src/commands/source-refresh.js +40 -0
  7. package/src/commands/sync.js +6 -0
  8. package/src/commands/update.js +59 -6
  9. package/src/lib/copy-template.js +200 -50
  10. package/src/lib/host-assets.js +367 -64
  11. package/src/lib/plan-applier.js +47 -0
  12. package/template/.claude/hooks/inject-workflow-state.py +7 -0
  13. package/template/.codex/hooks/inject-workflow-state.py +7 -0
  14. package/template/.codex/hooks.json +1 -1
  15. package/template/.cowork-flow/.version +1 -1
  16. package/template/.cowork-flow/config.yaml +0 -11
  17. package/template/.cowork-flow/scripts/adapters/cli/init_developer.py +0 -1
  18. package/template/.cowork-flow/scripts/adapters/cli/subagent.py +7 -4
  19. package/template/.cowork-flow/scripts/adapters/cli/task.py +8 -484
  20. package/template/.cowork-flow/scripts/adapters/cli/task_archive_commands.py +15 -134
  21. package/template/.cowork-flow/scripts/adapters/cli/task_context_commands.py +7 -1
  22. package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +388 -0
  23. package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +163 -2
  24. package/template/.cowork-flow/scripts/adapters/git/git_context.py +16 -193
  25. package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +170 -84
  26. package/template/.cowork-flow/scripts/infra/config.py +0 -17
  27. package/template/.cowork-flow/scripts/infra/developer_profile.py +0 -95
  28. package/template/.cowork-flow/scripts/infra/paths.py +0 -79
  29. package/template/.cowork-flow/scripts/infra/skill_manifest.py +23 -3
  30. package/template/.cowork-flow/scripts/infra/storage/operation_log.py +40 -0
  31. package/template/.cowork-flow/scripts/infra/storage/state_store.py +356 -1
  32. package/template/.cowork-flow/scripts/kernel/workflow_route.py +15 -15
  33. package/template/.cowork-flow/scripts/run.py +0 -5
  34. package/template/.cowork-flow/scripts/services/context_discovery.py +156 -0
  35. package/template/.cowork-flow/scripts/services/context_jsonl.py +119 -0
  36. package/template/.cowork-flow/scripts/services/context_paths.py +186 -0
  37. package/template/.cowork-flow/scripts/services/lifecycle_checks.py +186 -69
  38. package/template/.cowork-flow/scripts/services/lifecycle_policy.py +128 -0
  39. package/template/.cowork-flow/scripts/services/plan_binding.py +146 -0
  40. package/template/.cowork-flow/scripts/services/readiness.py +3 -170
  41. package/template/.cowork-flow/scripts/services/task_archive.py +115 -24
  42. package/template/.cowork-flow/scripts/services/task_context.py +103 -365
  43. package/template/.cowork-flow/scripts/services/task_creation.py +40 -5
  44. package/template/.cowork-flow/scripts/services/task_lifecycle.py +155 -18
  45. package/template/.cowork-flow/scripts/services/task_routing.py +80 -67
  46. package/template/.cowork-flow/scripts/services/task_utils.py +0 -146
  47. package/template/.cowork-flow/scripts/services/workflow_runtime.py +1 -1
  48. package/template/.cowork-flow/spec/contracts/capabilities.md +23 -0
  49. package/template/.cowork-flow/spec/contracts/decision-anchor.md +2 -2
  50. package/template/.cowork-flow/spec/contracts/error-output-as-data.md +8 -0
  51. package/template/.cowork-flow/spec/contracts/index.md +1 -0
  52. package/template/.cowork-flow/spec/contracts/party-mode-v2-board.md +20 -0
  53. package/template/.cowork-flow/spec/contracts/plan-binding.md +50 -0
  54. package/template/.cowork-flow/spec/contracts/skill-owned-actions.md +31 -7
  55. package/template/.cowork-flow/spec/guides/index.md +3 -1
  56. package/template/.cowork-flow/spec/index.md +5 -4
  57. package/template/.cowork-flow/spec/references/definition-of-done.md +2 -3
  58. package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
  59. package/template/.cowork-flow/spec/runtime/contract-registry.json +14 -0
  60. package/template/.cowork-flow/spec/runtime/host-assets.json +73 -3
  61. package/template/.cowork-flow/spec/runtime/index.md +1 -1
  62. package/template/.cowork-flow/spec/schemas/host-assets.schema.json +150 -8
  63. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  64. package/template/skills/batch-execution/SKILL.md +37 -3
  65. package/template/skills/batch-execution/scripts/batch_action.py +4 -0
  66. package/template/skills/batch-execution/scripts/batch_execution.py +120 -499
  67. package/template/skills/batch-execution/scripts/batch_recovery.py +94 -0
  68. package/template/skills/batch-execution/scripts/batch_state_machine.py +201 -0
  69. package/template/skills/batch-execution/scripts/batch_verification.py +326 -0
  70. package/template/skills/brainstorming/SKILL.md +8 -7
  71. package/template/skills/cowork-flow/SKILL.md +3 -0
  72. package/template/skills/party-mode/scripts/party_action_contract.py +113 -0
  73. package/template/skills/party-mode/scripts/party_actions.py +95 -0
  74. package/template/skills/party-mode/scripts/party_board_store.py +148 -0
  75. package/template/skills/party-mode/scripts/party_mode_v2.py +345 -317
  76. package/template/skills/party-mode/scripts/party_reports.py +177 -0
  77. package/template/skills/party-mode/scripts/party_state_machine.py +189 -0
  78. package/template/skills/runtime-health/SKILL.md +14 -2
  79. package/template/skills/runtime-health/scripts/doctor.py +278 -41
  80. package/template/skills/task-planning/SKILL.md +105 -61
  81. package/template/skills/task-review/SKILL.md +4 -4
  82. package/template/skills/task-review/scripts/review_check.py +277 -20
  83. package/template/skills/test-first/SKILL.md +18 -3
  84. package/template/skills/test-first/manifest.json +8 -4
  85. package/template/.cowork-flow/changes/archive/.gitkeep +0 -1
  86. package/template/.cowork-flow/scripts/adapters/cli/add_session.py +0 -465
  87. package/template/.cowork-flow/scripts/adapters/cli/change.py +0 -412
  88. package/template/.cowork-flow/workspace/index.md +0 -102
  89. package/template/.zcode/scaffold/AGENTS.md +0 -145
  90. 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 tracked source checkout bootstrap files, ignored local live runtime, and the template distribution source.
28
- - Source checkout health must not force-track the whole `.cowork-flow/` tree; ignored local live runtime files are checked only when present.
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.