cowork-flow 0.0.5 → 0.0.7

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 CHANGED
@@ -219,4 +219,30 @@ cowork-flow sync . --dry-run
219
219
 
220
220
  ## 发布流程
221
221
 
222
- CI 会运行 Node CLI 测试、npm pack 内容检查和现有 Python 模板测试。发布到 npm 通过 GitHub Actions 的 `Publish npm Package` workflow 完成,需要在仓库 secrets 中配置 `NPM_TOKEN`。
222
+ 发布前先确认本地或 CI 具备 npm 发布凭据,例如已执行 `npm login`,或在 CI 中配置了 `NPM_TOKEN`。
223
+
224
+ 推荐使用 release 脚本发布:
225
+
226
+ ```bash
227
+ npm run release
228
+ ```
229
+
230
+ 默认会升级 patch 版本,执行顺序是:
231
+
232
+ 1. `npm run test:all`
233
+ 2. `npm version patch --no-git-tag-version`
234
+ 3. 将 `package.json` 中的新版本同步写入 `template/.cowork-flow/.version`
235
+ 4. `git add package.json package-lock.json template/.cowork-flow/.version`
236
+ 5. `git commit -m "chore(release): <version>"`
237
+ 6. `git tag v<version>`
238
+ 7. `npm publish`
239
+
240
+ 需要发布其他版本类型时,把 npm 支持的版本类型传给脚本:
241
+
242
+ ```bash
243
+ npm run release -- minor
244
+ npm run release -- major
245
+ npm run release -- prerelease
246
+ ```
247
+
248
+ 脚本会在任一步骤失败时停止后续发布动作。CI 仍会运行 Node CLI 测试、npm pack 内容检查和现有 Python 模板测试;GitHub Actions 的 `Publish npm Package` workflow 仍需要在仓库 secrets 中配置 `NPM_TOKEN`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,8 @@
18
18
  "test": "node --test",
19
19
  "test:template": "node scripts/run-template-tests.js",
20
20
  "pack:check": "node scripts/pack-check.js",
21
- "test:all": "npm test && npm run test:template && npm run pack:check"
21
+ "test:all": "npm test && npm run test:template && npm run pack:check",
22
+ "release": "sh scripts/release.sh"
22
23
  },
23
24
  "engines": {
24
25
  "node": ">=20"
@@ -1 +1 @@
1
- 0.0.5
1
+ 0.0.7
@@ -5,7 +5,7 @@ default_adapter: codex
5
5
 
6
6
  agents:
7
7
  implementer:
8
- codex_type: worker
8
+ agent_type: worker
9
9
  capabilities:
10
10
  - implementation
11
11
  - test-writing
@@ -19,9 +19,51 @@ agents:
19
19
  - "test/**"
20
20
  risk_limits:
21
21
  max_parallel_write_conflicts: 0
22
+ prompt: |
23
+ Implement the smallest change that satisfies the approved plan and tests.
24
+ Write or update focused regression tests first, keep edits scoped, and report exact verification commands.
25
+
26
+ tester:
27
+ agent_type: worker
28
+ capabilities:
29
+ - test-writing
30
+ - verification
31
+ preferred_task_types:
32
+ - test
33
+ - verification
34
+ file_patterns:
35
+ - "tests/**"
36
+ - "test/**"
37
+ - "src/**"
38
+ - "template/**"
39
+ risk_limits:
40
+ max_parallel_write_conflicts: 0
41
+ prompt: |
42
+ Design tests that fail for the broken behavior and pass for the intended behavior.
43
+ Prefer clear behavior assertions over implementation details, and include the command that proves the result.
44
+
45
+ debugger:
46
+ agent_type: worker
47
+ capabilities:
48
+ - debugging
49
+ - implementation
50
+ - verification
51
+ preferred_task_types:
52
+ - bugfix
53
+ - code
54
+ file_patterns:
55
+ - "src/**"
56
+ - "template/**"
57
+ - "tests/**"
58
+ - "test/**"
59
+ risk_limits:
60
+ max_parallel_write_conflicts: 0
61
+ prompt: |
62
+ Find the root cause before changing code. Reproduce the failure, compare with nearby working patterns,
63
+ then make the smallest fix and verify the original symptom.
22
64
 
23
65
  spec-reviewer:
24
- codex_type: default
66
+ agent_type: default
25
67
  capabilities:
26
68
  - spec-review
27
69
  - acceptance-check
@@ -35,12 +77,16 @@ agents:
35
77
  - "template/**"
36
78
  risk_limits:
37
79
  max_parallel_write_conflicts: 0
80
+ prompt: |
81
+ Review the proposal, spec, plan, and task PRD for contradictions, missing acceptance criteria,
82
+ unclear scope, and behavior that is not covered by verification.
38
83
 
39
84
  quality-reviewer:
40
- codex_type: default
85
+ agent_type: default
41
86
  capabilities:
42
87
  - code-quality-review
43
88
  - test-review
89
+ - verification
44
90
  preferred_task_types:
45
91
  - review
46
92
  - verification
@@ -51,14 +97,18 @@ agents:
51
97
  - "test/**"
52
98
  risk_limits:
53
99
  max_parallel_write_conflicts: 0
100
+ prompt: |
101
+ Review the diff for correctness, maintainability, focused scope, and meaningful tests.
102
+ Check that verification evidence matches the behavior being claimed.
54
103
 
55
104
  docs-agent:
56
- codex_type: worker
105
+ agent_type: worker
57
106
  capabilities:
58
107
  - documentation
59
108
  - workflow-writing
60
109
  preferred_task_types:
61
110
  - docs
111
+ - documentation
62
112
  file_patterns:
63
113
  - "README.md"
64
114
  - "AGENTS.md"
@@ -66,3 +116,27 @@ agents:
66
116
  - ".agent/skills/**"
67
117
  risk_limits:
68
118
  max_parallel_write_conflicts: 0
119
+ prompt: |
120
+ Write concise project documentation that reflects actual commands, files, and workflow behavior.
121
+ Avoid aspirational text that is not backed by the implementation.
122
+
123
+ release-reviewer:
124
+ agent_type: default
125
+ capabilities:
126
+ - release-review
127
+ - acceptance-check
128
+ - verification
129
+ preferred_task_types:
130
+ - release
131
+ - review
132
+ file_patterns:
133
+ - "package.json"
134
+ - "package-lock.json"
135
+ - "scripts/**"
136
+ - "README.md"
137
+ - ".cowork-flow/changes/**"
138
+ risk_limits:
139
+ max_parallel_write_conflicts: 0
140
+ prompt: |
141
+ Review release-facing changes for versioning, packaging, documentation, and command safety.
142
+ Confirm that generated artifacts and publish steps are intentional.
@@ -3,30 +3,8 @@ set -u
3
3
 
4
4
  MIN_PYTHON_LABEL="Python 3.8+"
5
5
  VERSION_CHECK='import sys; raise SystemExit(0 if sys.version_info >= (3, 8) else 1)'
6
-
7
6
  WORKFLOW_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd) || exit 1
8
- SCRIPTS_DIR="$WORKFLOW_DIR/scripts"
9
-
10
- print_usage() {
11
- cat <<'EOF'
12
- 用法:
13
- ./.cowork-flow/run <command> [args...]
14
- ./.cowork-flow/run python [python-args...]
15
-
16
- 常用 command:
17
- resume
18
- task
19
- change
20
- get-context
21
- get-developer
22
- init-developer
23
- add-session
24
- agent-team
25
-
26
- 解释器选择顺序:
27
- COWORK_FLOW_PYTHON -> PYTHON -> python3 -> python -> py -3
28
- EOF
29
- }
7
+ RUNNER_SCRIPT="$WORKFLOW_DIR/scripts/run.py"
30
8
 
31
9
  candidate_is_valid() {
32
10
  candidate_cmd=$1
@@ -51,8 +29,8 @@ select_explicit_python() {
51
29
  return 0
52
30
  fi
53
31
 
54
- echo "错误:$env_name 指向的解释器不可用或版本低于 ${MIN_PYTHON_LABEL}:$env_value" >&2
55
- echo "请设置 $env_name 为可执行的 Python 3.8+ 解释器路径。" >&2
32
+ echo "Error: $env_name does not point to a usable ${MIN_PYTHON_LABEL} interpreter: $env_value" >&2
33
+ echo "Set $env_name to an executable Python 3.8+ interpreter path." >&2
56
34
  exit 127
57
35
  }
58
36
 
@@ -86,81 +64,16 @@ select_python() {
86
64
  return 0
87
65
  fi
88
66
 
89
- echo "错误:需要 ${MIN_PYTHON_LABEL},但未找到可用解释器。" >&2
90
- echo "已尝试:COWORK_FLOW_PYTHON、PYTHON、python3、python、py -3。" >&2
91
- echo "可设置 COWORK_FLOW_PYTHON=/path/to/python3 后重试。" >&2
67
+ echo "Error: ${MIN_PYTHON_LABEL} is required, but no usable interpreter was found." >&2
68
+ echo "Tried: COWORK_FLOW_PYTHON, PYTHON, python3, python, py -3." >&2
69
+ echo "Set COWORK_FLOW_PYTHON=/path/to/python3 and retry." >&2
92
70
  exit 127
93
71
  }
94
72
 
95
- exec_python() {
96
- select_python
97
-
98
- if [ -n "$SELECTED_PYTHON_ARG" ]; then
99
- exec "$SELECTED_PYTHON" "$SELECTED_PYTHON_ARG" "$@"
100
- fi
101
-
102
- exec "$SELECTED_PYTHON" "$@"
103
- }
104
-
105
- run_script() {
106
- script_path=$1
107
- shift
108
-
109
- if [ ! -f "$script_path" ]; then
110
- echo "错误:找不到脚本:$script_path" >&2
111
- exit 2
112
- fi
113
-
114
- exec_python "$script_path" "$@"
115
- }
73
+ select_python
116
74
 
117
- if [ "$#" -eq 0 ]; then
118
- print_usage >&2
119
- exit 2
75
+ if [ -n "$SELECTED_PYTHON_ARG" ]; then
76
+ exec "$SELECTED_PYTHON" "$SELECTED_PYTHON_ARG" "$RUNNER_SCRIPT" "$@"
120
77
  fi
121
78
 
122
- command_name=$1
123
- shift
124
-
125
- case "$command_name" in
126
- -h|--help|help)
127
- print_usage
128
- exit 0
129
- ;;
130
- python)
131
- exec_python "$@"
132
- ;;
133
- resume)
134
- run_script "$SCRIPTS_DIR/resume.py" "$@"
135
- ;;
136
- task)
137
- run_script "$SCRIPTS_DIR/task.py" "$@"
138
- ;;
139
- change)
140
- run_script "$SCRIPTS_DIR/change.py" "$@"
141
- ;;
142
- get-context|get_context)
143
- run_script "$SCRIPTS_DIR/get_context.py" "$@"
144
- ;;
145
- get-developer|get_developer)
146
- run_script "$SCRIPTS_DIR/get_developer.py" "$@"
147
- ;;
148
- init-developer|init_developer)
149
- run_script "$SCRIPTS_DIR/init_developer.py" "$@"
150
- ;;
151
- add-session|add_session)
152
- run_script "$SCRIPTS_DIR/add_session.py" "$@"
153
- ;;
154
- agent-team|agent_team)
155
- run_script "$SCRIPTS_DIR/agent_team.py" "$@"
156
- ;;
157
- *)
158
- if [ -f "$SCRIPTS_DIR/$command_name.py" ]; then
159
- run_script "$SCRIPTS_DIR/$command_name.py" "$@"
160
- fi
161
-
162
- echo "错误:未知 cowork-flow 命令:$command_name" >&2
163
- print_usage >&2
164
- exit 2
165
- ;;
166
- esac
79
+ exec "$SELECTED_PYTHON" "$RUNNER_SCRIPT" "$@"
@@ -4,108 +4,20 @@ setlocal EnableExtensions
4
4
  set "MIN_PYTHON_LABEL=Python 3.8+"
5
5
  set "VERSION_CHECK=import sys; raise SystemExit(0 if sys.version_info >= (3, 8) else 1)"
6
6
  set "WORKFLOW_DIR=%~dp0"
7
- set "SCRIPTS_DIR=%WORKFLOW_DIR%scripts"
7
+ set "RUNNER_SCRIPT=%WORKFLOW_DIR%scripts\run.py"
8
8
  set "SELECTED_PYTHON="
9
9
  set "SELECTED_PYTHON_ARG="
10
- set "REST_ARGS="
11
10
 
12
- if "%~1"=="" goto usage_error
13
-
14
- set "COMMAND_NAME=%~1"
15
- if not "%~2"=="" (
16
- for /F "tokens=1,* delims= " %%A in ("%*") do set "REST_ARGS=%%B"
17
- )
11
+ call :select_python
12
+ if errorlevel 1 exit /b %ERRORLEVEL%
18
13
 
19
- if /I "%COMMAND_NAME%"=="-h" goto usage
20
- if /I "%COMMAND_NAME%"=="--help" goto usage
21
- if /I "%COMMAND_NAME%"=="help" goto usage
22
- if /I "%COMMAND_NAME%"=="python" goto run_python_passthrough
23
- if /I "%COMMAND_NAME%"=="resume" goto run_resume
24
- if /I "%COMMAND_NAME%"=="task" goto run_task
25
- if /I "%COMMAND_NAME%"=="change" goto run_change
26
- if /I "%COMMAND_NAME%"=="get-context" goto run_get_context
27
- if /I "%COMMAND_NAME%"=="get_context" goto run_get_context
28
- if /I "%COMMAND_NAME%"=="get-developer" goto run_get_developer
29
- if /I "%COMMAND_NAME%"=="get_developer" goto run_get_developer
30
- if /I "%COMMAND_NAME%"=="init-developer" goto run_init_developer
31
- if /I "%COMMAND_NAME%"=="init_developer" goto run_init_developer
32
- if /I "%COMMAND_NAME%"=="add-session" goto run_add_session
33
- if /I "%COMMAND_NAME%"=="add_session" goto run_add_session
34
- if /I "%COMMAND_NAME%"=="agent-team" goto run_agent_team
35
- if /I "%COMMAND_NAME%"=="agent_team" goto run_agent_team
36
-
37
- if exist "%SCRIPTS_DIR%\%COMMAND_NAME%.py" (
38
- call :run_script "%SCRIPTS_DIR%\%COMMAND_NAME%.py"
39
- exit /b %ERRORLEVEL%
14
+ if not "%SELECTED_PYTHON_ARG%"=="" (
15
+ "%SELECTED_PYTHON%" "%SELECTED_PYTHON_ARG%" "%RUNNER_SCRIPT%" %*
16
+ ) else (
17
+ "%SELECTED_PYTHON%" "%RUNNER_SCRIPT%" %*
40
18
  )
41
-
42
- echo 错误:未知 cowork-flow 命令:%COMMAND_NAME% 1>&2
43
- call :print_usage 1>&2
44
- exit /b 2
45
-
46
- :usage
47
- call :print_usage
48
- exit /b 0
49
-
50
- :usage_error
51
- call :print_usage 1>&2
52
- exit /b 2
53
-
54
- :run_python_passthrough
55
- call :exec_python %REST_ARGS%
56
- exit /b %ERRORLEVEL%
57
-
58
- :run_resume
59
- call :run_script "%SCRIPTS_DIR%\resume.py"
60
- exit /b %ERRORLEVEL%
61
-
62
- :run_task
63
- call :run_script "%SCRIPTS_DIR%\task.py"
64
- exit /b %ERRORLEVEL%
65
-
66
- :run_change
67
- call :run_script "%SCRIPTS_DIR%\change.py"
68
- exit /b %ERRORLEVEL%
69
-
70
- :run_get_context
71
- call :run_script "%SCRIPTS_DIR%\get_context.py"
72
- exit /b %ERRORLEVEL%
73
-
74
- :run_get_developer
75
- call :run_script "%SCRIPTS_DIR%\get_developer.py"
76
19
  exit /b %ERRORLEVEL%
77
20
 
78
- :run_init_developer
79
- call :run_script "%SCRIPTS_DIR%\init_developer.py"
80
- exit /b %ERRORLEVEL%
81
-
82
- :run_add_session
83
- call :run_script "%SCRIPTS_DIR%\add_session.py"
84
- exit /b %ERRORLEVEL%
85
-
86
- :run_agent_team
87
- call :run_script "%SCRIPTS_DIR%\agent_team.py"
88
- exit /b %ERRORLEVEL%
89
-
90
- :print_usage
91
- echo 用法:
92
- echo .\.cowork-flow\run.cmd ^<command^> [args...]
93
- echo .\.cowork-flow\run.cmd python [python-args...]
94
- echo.
95
- echo 常用 command:
96
- echo resume
97
- echo task
98
- echo change
99
- echo get-context
100
- echo get-developer
101
- echo init-developer
102
- echo add-session
103
- echo agent-team
104
- echo.
105
- echo 解释器选择顺序:
106
- echo COWORK_FLOW_PYTHON -^> PYTHON -^> python3 -^> python -^> py -3
107
- exit /b 0
108
-
109
21
  :candidate_is_valid
110
22
  set "CANDIDATE_CMD=%~1"
111
23
  set "CANDIDATE_ARG=%~2"
@@ -133,8 +45,8 @@ if not errorlevel 1 (
133
45
  exit /b 0
134
46
  )
135
47
 
136
- echo 错误:%ENV_NAME% 指向的解释器不可用或版本低于 %MIN_PYTHON_LABEL%:%ENV_VALUE% 1>&2
137
- echo 请设置 %ENV_NAME% 为可执行的 Python 3.8+ 解释器路径。 1>&2
48
+ echo Error: %ENV_NAME% does not point to a usable %MIN_PYTHON_LABEL% interpreter: %ENV_VALUE% 1>&2
49
+ echo Set %ENV_NAME% to an executable Python 3.8+ interpreter path. 1>&2
138
50
  exit /b 127
139
51
 
140
52
  :select_python
@@ -170,29 +82,7 @@ if not errorlevel 1 (
170
82
  exit /b 0
171
83
  )
172
84
 
173
- echo 错误:需要 %MIN_PYTHON_LABEL%,但未找到可用解释器。 1>&2
174
- echo 已尝试:COWORK_FLOW_PYTHON、PYTHON、python3、python、py -3。 1>&2
175
- echo 可设置 COWORK_FLOW_PYTHON=C:\Path\To\python.exe 后重试。 1>&2
85
+ echo Error: %MIN_PYTHON_LABEL% is required, but no usable interpreter was found. 1>&2
86
+ echo Tried: COWORK_FLOW_PYTHON, PYTHON, python3, python, py -3. 1>&2
87
+ echo Set COWORK_FLOW_PYTHON=C:\Path\To\python.exe and retry. 1>&2
176
88
  exit /b 127
177
-
178
- :exec_python
179
- call :select_python
180
- if errorlevel 1 exit /b %ERRORLEVEL%
181
-
182
- if not "%SELECTED_PYTHON_ARG%"=="" (
183
- "%SELECTED_PYTHON%" "%SELECTED_PYTHON_ARG%" %*
184
- ) else (
185
- "%SELECTED_PYTHON%" %*
186
- )
187
- exit /b %ERRORLEVEL%
188
-
189
- :run_script
190
- set "SCRIPT_PATH=%~1"
191
-
192
- if not exist "%SCRIPT_PATH%" (
193
- echo 错误:找不到脚本:%SCRIPT_PATH% 1>&2
194
- exit /b 2
195
- )
196
-
197
- call :exec_python "%SCRIPT_PATH%" %REST_ARGS%
198
- exit /b %ERRORLEVEL%
@@ -15,6 +15,7 @@ from common.agent_team import (
15
15
  build_initial_metrics,
16
16
  build_initial_status,
17
17
  parse_plan,
18
+ load_agent_registry,
18
19
  render_assignment_prompt,
19
20
  render_dispatch_plan,
20
21
  write_json,
@@ -31,7 +32,7 @@ default_adapter: codex
31
32
 
32
33
  agents:
33
34
  implementer:
34
- codex_type: worker
35
+ agent_type: worker
35
36
  capabilities:
36
37
  - implementation
37
38
  - test-writing
@@ -45,9 +46,51 @@ agents:
45
46
  - "test/**"
46
47
  risk_limits:
47
48
  max_parallel_write_conflicts: 0
49
+ prompt: |
50
+ Implement the smallest change that satisfies the approved plan and tests.
51
+ Write or update focused regression tests first, keep edits scoped, and report exact verification commands.
52
+
53
+ tester:
54
+ agent_type: worker
55
+ capabilities:
56
+ - test-writing
57
+ - verification
58
+ preferred_task_types:
59
+ - test
60
+ - verification
61
+ file_patterns:
62
+ - "tests/**"
63
+ - "test/**"
64
+ - "src/**"
65
+ - "template/**"
66
+ risk_limits:
67
+ max_parallel_write_conflicts: 0
68
+ prompt: |
69
+ Design tests that fail for the broken behavior and pass for the intended behavior.
70
+ Prefer clear behavior assertions over implementation details, and include the command that proves the result.
71
+
72
+ debugger:
73
+ agent_type: worker
74
+ capabilities:
75
+ - debugging
76
+ - implementation
77
+ - verification
78
+ preferred_task_types:
79
+ - bugfix
80
+ - code
81
+ file_patterns:
82
+ - "src/**"
83
+ - "template/**"
84
+ - "tests/**"
85
+ - "test/**"
86
+ risk_limits:
87
+ max_parallel_write_conflicts: 0
88
+ prompt: |
89
+ Find the root cause before changing code. Reproduce the failure, compare with nearby working patterns,
90
+ then make the smallest fix and verify the original symptom.
48
91
 
49
92
  spec-reviewer:
50
- codex_type: default
93
+ agent_type: default
51
94
  capabilities:
52
95
  - spec-review
53
96
  - acceptance-check
@@ -61,12 +104,16 @@ agents:
61
104
  - "template/**"
62
105
  risk_limits:
63
106
  max_parallel_write_conflicts: 0
107
+ prompt: |
108
+ Review the proposal, spec, plan, and task PRD for contradictions, missing acceptance criteria,
109
+ unclear scope, and behavior that is not covered by verification.
64
110
 
65
111
  quality-reviewer:
66
- codex_type: default
112
+ agent_type: default
67
113
  capabilities:
68
114
  - code-quality-review
69
115
  - test-review
116
+ - verification
70
117
  preferred_task_types:
71
118
  - review
72
119
  - verification
@@ -77,14 +124,18 @@ agents:
77
124
  - "test/**"
78
125
  risk_limits:
79
126
  max_parallel_write_conflicts: 0
127
+ prompt: |
128
+ Review the diff for correctness, maintainability, focused scope, and meaningful tests.
129
+ Check that verification evidence matches the behavior being claimed.
80
130
 
81
131
  docs-agent:
82
- codex_type: worker
132
+ agent_type: worker
83
133
  capabilities:
84
134
  - documentation
85
135
  - workflow-writing
86
136
  preferred_task_types:
87
137
  - docs
138
+ - documentation
88
139
  file_patterns:
89
140
  - "README.md"
90
141
  - "AGENTS.md"
@@ -92,6 +143,30 @@ agents:
92
143
  - ".agent/skills/**"
93
144
  risk_limits:
94
145
  max_parallel_write_conflicts: 0
146
+ prompt: |
147
+ Write concise project documentation that reflects actual commands, files, and workflow behavior.
148
+ Avoid aspirational text that is not backed by the implementation.
149
+
150
+ release-reviewer:
151
+ agent_type: default
152
+ capabilities:
153
+ - release-review
154
+ - acceptance-check
155
+ - verification
156
+ preferred_task_types:
157
+ - release
158
+ - review
159
+ file_patterns:
160
+ - "package.json"
161
+ - "package-lock.json"
162
+ - "scripts/**"
163
+ - "README.md"
164
+ - ".cowork-flow/changes/**"
165
+ risk_limits:
166
+ max_parallel_write_conflicts: 0
167
+ prompt: |
168
+ Review release-facing changes for versioning, packaging, documentation, and command safety.
169
+ Confirm that generated artifacts and publish steps are intentional.
95
170
  """,
96
171
  "adapters.yaml": """# Agent Team Adapters
97
172
 
@@ -309,7 +384,8 @@ def cmd_prepare(args: argparse.Namespace) -> int:
309
384
  ):
310
385
  directory.mkdir(parents=True, exist_ok=True)
311
386
 
312
- dispatch_plan = build_dispatch_plan(tasks)
387
+ registry = load_agent_registry(_agent_team_config_dir(repo_root) / "agents.yaml")
388
+ dispatch_plan = build_dispatch_plan(tasks, registry)
313
389
  (runtime_dir / "dispatch-plan.yaml").write_text(
314
390
  render_dispatch_plan(dispatch_plan),
315
391
  encoding="utf-8",
@@ -317,9 +393,9 @@ def cmd_prepare(args: argparse.Namespace) -> int:
317
393
  write_json(runtime_dir / "status.json", build_initial_status(dispatch_plan))
318
394
  write_json(runtime_dir / "metrics.json", build_initial_metrics(dispatch_plan))
319
395
  write_json(
320
- adapters_dir / "codex.json",
396
+ adapters_dir / f"{dispatch_plan['adapter']}.json",
321
397
  {
322
- "adapter": "codex",
398
+ "adapter": dispatch_plan["adapter"],
323
399
  "mode": "coordinator-dispatched",
324
400
  "assignmentCount": len(dispatch_plan["assignments"]),
325
401
  },
@@ -371,7 +447,7 @@ def cmd_next(args: argparse.Namespace) -> int:
371
447
  for assignment_id, assignment in ready:
372
448
  print(
373
449
  f"{assignment_id}\trole={assignment['role']}\t"
374
- f"agent={assignment['recommended_agent']}\tcodex_type={assignment['codex_type']}"
450
+ f"agent={assignment['recommended_agent']}\tagent_type={assignment['agent_type']}"
375
451
  )
376
452
  return 0
377
453
 
@@ -6,12 +6,18 @@ from __future__ import annotations
6
6
 
7
7
  import argparse
8
8
  import re
9
- import shutil
10
9
  import sys
11
10
  from datetime import datetime
12
11
  from pathlib import Path
13
12
 
14
- from common.paths import DIR_ARCHIVE, DIR_CHANGES, DIR_WORKFLOW, get_repo_root
13
+ from common.archive_utils import archive_directory_resumable
14
+ from common.paths import (
15
+ DIR_ARCHIVE,
16
+ DIR_CHANGES,
17
+ DIR_WORKFLOW,
18
+ generate_task_date_prefix,
19
+ get_repo_root,
20
+ )
15
21
 
16
22
 
17
23
  SLUG_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
@@ -107,9 +113,52 @@ def _resolve_link(repo_root: Path, base_dir: str, value: object) -> Path | None:
107
113
  return raw_path
108
114
 
109
115
  direct = repo_root / raw_path
110
- if direct.exists():
116
+ workflow_base = Path(DIR_WORKFLOW) / base_dir
117
+ if direct.exists() or raw_path.parts[:2] == workflow_base.parts:
111
118
  return direct
112
- return repo_root / DIR_WORKFLOW / base_dir / raw_path
119
+ return repo_root / workflow_base / raw_path
120
+
121
+
122
+ def _archived_task_link(repo_root: Path, task_name: str) -> str | None:
123
+ archive_root = repo_root / DIR_WORKFLOW / "tasks" / DIR_ARCHIVE
124
+ if not archive_root.is_dir():
125
+ return None
126
+
127
+ matches = [path for path in archive_root.glob(f"*/{task_name}") if path.is_dir()]
128
+ if len(matches) != 1:
129
+ return None
130
+
131
+ relative = matches[0].relative_to(archive_root)
132
+ return str(Path(DIR_ARCHIVE) / relative).replace("\\", "/")
133
+
134
+
135
+ def _normalize_task_link(repo_root: Path, value: object) -> object:
136
+ if not isinstance(value, str) or not value.strip():
137
+ return value
138
+
139
+ target = _resolve_link(repo_root, "tasks", value)
140
+ tasks_root = repo_root / DIR_WORKFLOW / "tasks"
141
+ archive_root = tasks_root / DIR_ARCHIVE
142
+ if target is None:
143
+ return value
144
+
145
+ if not target.exists():
146
+ return _archived_task_link(repo_root, target.name) or value
147
+
148
+ try:
149
+ relative = target.resolve().relative_to(archive_root.resolve())
150
+ except ValueError:
151
+ return value
152
+
153
+ return str(Path(DIR_ARCHIVE) / relative).replace("\\", "/")
154
+
155
+
156
+ def _normalize_archive_metadata(repo_root: Path, change_dir: Path) -> None:
157
+ metadata = _read_metadata(change_dir)
158
+ normalized_task = _normalize_task_link(repo_root, metadata.get("task"))
159
+ if normalized_task != metadata.get("task"):
160
+ metadata["task"] = normalized_task
161
+ _write_metadata(change_dir, metadata)
113
162
 
114
163
 
115
164
  def _validate_link(repo_root: Path, metadata: dict[str, object], field: str, base_dir: str) -> str | None:
@@ -178,14 +227,15 @@ def create_change(args: argparse.Namespace) -> int:
178
227
  if not _validate_slug(slug):
179
228
  return 2
180
229
 
181
- change_dir = _change_dir(repo_root, slug)
230
+ dir_name = f"{generate_task_date_prefix()}-{slug}"
231
+ change_dir = _change_dir(repo_root, dir_name)
182
232
  if change_dir.exists():
183
- print(f"Error: change already exists: {slug}", file=sys.stderr)
233
+ print(f"Error: change already exists: {dir_name}", file=sys.stderr)
184
234
  return 1
185
235
 
186
236
  change_dir.mkdir(parents=True)
187
237
  (change_dir / "proposal.md").write_text(
188
- f"# {slug}\n\nDescribe the proposed behavior change.\n",
238
+ f"# {dir_name}\n\nDescribe the proposed behavior change.\n",
189
239
  encoding="utf-8",
190
240
  )
191
241
  (change_dir / "design.md").write_text("", encoding="utf-8")
@@ -193,7 +243,7 @@ def create_change(args: argparse.Namespace) -> int:
193
243
  _write_metadata(
194
244
  change_dir,
195
245
  {
196
- "slug": slug,
246
+ "slug": dir_name,
197
247
  "status": "draft",
198
248
  "level": args.level,
199
249
  "created_at": _now_iso(),
@@ -203,7 +253,7 @@ def create_change(args: argparse.Namespace) -> int:
203
253
  },
204
254
  )
205
255
 
206
- print(f"created {slug}")
256
+ print(f"created {dir_name}")
207
257
  return 0
208
258
 
209
259
 
@@ -215,22 +265,25 @@ def validate_command(args: argparse.Namespace) -> int:
215
265
  def archive_change(args: argparse.Namespace) -> int:
216
266
  repo_root = get_repo_root()
217
267
  slug = args.slug
268
+ source = _change_dir(repo_root, slug)
269
+ if source.is_dir():
270
+ _normalize_archive_metadata(repo_root, source)
271
+
218
272
  if not validate_change(repo_root, slug, quiet=True):
219
273
  return 1
220
274
 
221
- source = _change_dir(repo_root, slug)
222
275
  month = datetime.now().astimezone().strftime("%Y-%m")
223
276
  destination = _archive_dir(repo_root) / month / slug
224
- if destination.exists():
225
- print(f"Error: archive destination already exists: {destination}", file=sys.stderr)
277
+ result = archive_directory_resumable(source, destination)
278
+ if not result.ok:
279
+ level = "Warning" if result.partial else "Error"
280
+ print(f"{level}: {result.message}", file=sys.stderr)
226
281
  return 1
227
282
 
228
- destination.parent.mkdir(parents=True, exist_ok=True)
229
- shutil.move(str(source), str(destination))
230
-
231
283
  metadata = _read_metadata(destination)
232
284
  metadata["status"] = "archived"
233
285
  metadata["archived_at"] = _now_iso()
286
+ metadata["task"] = _normalize_task_link(repo_root, metadata.get("task"))
234
287
  _write_metadata(destination, metadata)
235
288
 
236
289
  print(f"archived {slug}")
@@ -102,15 +102,222 @@ def _task_dependencies(tasks: list[dict[str, object]]) -> list[dict[str, str]]:
102
102
  return dependencies
103
103
 
104
104
 
105
- def _role_agent(role: str) -> tuple[str, str]:
106
- if role == "implementer":
107
- return ("implementer", "worker")
108
- if role == "spec-reviewer":
109
- return ("spec-reviewer", "default")
110
- return ("quality-reviewer", "default")
105
+ DEFAULT_ROLE_TYPES = {
106
+ "implementer": "worker",
107
+ "spec-reviewer": "default",
108
+ "quality-reviewer": "default",
109
+ }
110
+
111
+ ROLE_CAPABILITIES = {
112
+ "implementer": {"implementation", "test-writing"},
113
+ "spec-reviewer": {"spec-review", "acceptance-check"},
114
+ "quality-reviewer": {"code-quality-review", "test-review", "verification"},
115
+ }
116
+
117
+ ROLE_TASK_TYPES = {
118
+ "implementer": {"code", "test"},
119
+ "spec-reviewer": {"review", "documentation"},
120
+ "quality-reviewer": {"review", "verification"},
121
+ }
122
+
123
+
124
+ def _role_config(registry: dict[str, object] | None, role: str) -> dict[str, object]:
125
+ agents = registry.get("agents", {}) if registry else {}
126
+ if not isinstance(agents, dict):
127
+ return {}
128
+ config = agents.get(role, {})
129
+ return config if isinstance(config, dict) else {}
130
+
131
+
132
+ def _ensure_agent_config(registry: dict[str, object], role: str) -> dict[str, object]:
133
+ agents = registry.setdefault("agents", {})
134
+ if not isinstance(agents, dict):
135
+ registry["agents"] = {}
136
+ agents = registry["agents"]
137
+ config = agents.setdefault(role, {})
138
+ if not isinstance(config, dict):
139
+ agents[role] = {}
140
+ config = agents[role]
141
+ return config
142
+
143
+
144
+ def _parse_list_item(line: str) -> str | None:
145
+ stripped = line.strip()
146
+ if not stripped.startswith("-"):
147
+ return None
148
+ value = stripped[1:].strip()
149
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
150
+ value = value[1:-1]
151
+ return value
152
+
153
+
154
+ def load_agent_registry(path: Path) -> dict[str, object]:
155
+ registry: dict[str, object] = {"default_adapter": "codex", "agents": {}}
156
+ try:
157
+ lines = path.read_text(encoding="utf-8").splitlines()
158
+ except OSError:
159
+ return registry
160
+
161
+ current_role: str | None = None
162
+ current_list: str | None = None
163
+ current_nested: str | None = None
164
+ prompt_role: str | None = None
165
+ prompt_lines: list[str] = []
166
+ in_agents = False
167
+
168
+ def flush_prompt() -> None:
169
+ nonlocal prompt_role, prompt_lines
170
+ if prompt_role is None:
171
+ return
172
+ config = _ensure_agent_config(registry, prompt_role)
173
+ config["prompt"] = "\n".join(prompt_lines).rstrip()
174
+ prompt_role = None
175
+ prompt_lines = []
176
+
177
+ for raw_line in lines:
178
+ indent = len(raw_line) - len(raw_line.lstrip())
179
+ stripped = raw_line.strip()
180
+
181
+ if prompt_role is not None:
182
+ if raw_line.startswith(" ") or not stripped:
183
+ prompt_lines.append(raw_line[6:] if raw_line.startswith(" ") else "")
184
+ continue
185
+ flush_prompt()
186
+
187
+ if not stripped or stripped.startswith("#"):
188
+ continue
189
+
190
+ if indent == 0:
191
+ current_role = None
192
+ current_list = None
193
+ current_nested = None
194
+ in_agents = stripped == "agents:"
195
+ if stripped.startswith("default_adapter:"):
196
+ registry["default_adapter"] = stripped.partition(":")[2].strip() or "codex"
197
+ continue
198
+
199
+ if in_agents and indent == 2 and stripped.endswith(":"):
200
+ current_role = stripped[:-1].strip()
201
+ current_list = None
202
+ current_nested = None
203
+ _ensure_agent_config(registry, current_role)
204
+ continue
205
+
206
+ if not in_agents or current_role is None:
207
+ continue
208
+
209
+ config = _ensure_agent_config(registry, current_role)
210
+
211
+ if indent == 4 and stripped == "prompt: |":
212
+ current_list = None
213
+ current_nested = None
214
+ prompt_role = current_role
215
+ prompt_lines = []
216
+ continue
217
+
218
+ if indent == 4 and stripped.endswith(":"):
219
+ key = stripped[:-1].strip()
220
+ current_list = key if key in {"capabilities", "preferred_task_types", "file_patterns"} else None
221
+ current_nested = key if key == "risk_limits" else None
222
+ if current_list:
223
+ config[current_list] = []
224
+ elif current_nested:
225
+ config[current_nested] = {}
226
+ continue
227
+
228
+ if indent == 4 and ":" in stripped:
229
+ key, _, value = stripped.partition(":")
230
+ key = key.strip()
231
+ value = value.strip()
232
+ current_list = None
233
+ current_nested = None
234
+ if key == "agent_type":
235
+ config[key] = value
236
+ continue
237
+
238
+ if indent == 6 and current_list:
239
+ value = _parse_list_item(stripped)
240
+ if value is not None:
241
+ items = config.setdefault(current_list, [])
242
+ if isinstance(items, list):
243
+ items.append(value)
244
+ continue
245
+
246
+ if indent == 6 and current_nested == "risk_limits" and ":" in stripped:
247
+ key, _, value = stripped.partition(":")
248
+ limits = config.setdefault("risk_limits", {})
249
+ if isinstance(limits, dict):
250
+ try:
251
+ limits[key.strip()] = int(value.strip())
252
+ except ValueError:
253
+ limits[key.strip()] = value.strip()
254
+
255
+ flush_prompt()
256
+ return registry
257
+
111
258
 
259
+ def _as_string_set(value: object) -> set[str]:
260
+ if not isinstance(value, list):
261
+ return set()
262
+ return {item for item in value if isinstance(item, str)}
263
+
264
+
265
+ def _matches_pattern(path: str, pattern: str) -> bool:
266
+ if pattern.endswith("/**"):
267
+ return path.startswith(pattern[:-3])
268
+ return Path(path).match(pattern)
269
+
270
+
271
+ def _file_match_count(config: dict[str, object], task: dict[str, object]) -> int:
272
+ patterns = _as_string_set(config.get("file_patterns"))
273
+ if not patterns:
274
+ return 0
275
+ count = 0
276
+ for file_path in _task_files(task):
277
+ if any(_matches_pattern(file_path, pattern) for pattern in patterns):
278
+ count += 1
279
+ return count
280
+
281
+
282
+ def _agent_score(role: str, name: str, config: dict[str, object], task: dict[str, object]) -> int:
283
+ score = 0
284
+ if name == role:
285
+ score += 10
286
+ score += 5 * len(_as_string_set(config.get("capabilities")).intersection(ROLE_CAPABILITIES.get(role, set())))
287
+ score += 3 * len(_as_string_set(config.get("preferred_task_types")).intersection(ROLE_TASK_TYPES.get(role, set())))
288
+ score += 2 * _file_match_count(config, task)
289
+ return score
290
+
291
+
292
+ def _select_agent(role: str, task: dict[str, object], registry: dict[str, object] | None = None) -> tuple[str, str, str]:
293
+ agents = registry.get("agents", {}) if registry else {}
294
+ if not isinstance(agents, dict):
295
+ agents = {}
296
+
297
+ best_name = role
298
+ best_config = _role_config(registry, role)
299
+ best_score = _agent_score(role, role, best_config, task) if best_config else -1
112
300
 
113
- def build_dispatch_plan(tasks: list[dict[str, object]]) -> dict[str, object]:
301
+ for name, config in agents.items():
302
+ if not isinstance(name, str) or not isinstance(config, dict):
303
+ continue
304
+ score = _agent_score(role, name, config, task)
305
+ if score > best_score or (score == best_score and name == role):
306
+ best_name = name
307
+ best_config = config
308
+ best_score = score
309
+
310
+ agent_type = DEFAULT_ROLE_TYPES.get(role, "default")
311
+ configured_type = best_config.get("agent_type") if isinstance(best_config, dict) else None
312
+ if isinstance(configured_type, str) and configured_type:
313
+ agent_type = configured_type
314
+ prompt = best_config.get("prompt") if isinstance(best_config, dict) else ""
315
+ return best_name, agent_type, prompt if isinstance(prompt, str) else ""
316
+
317
+ def build_dispatch_plan(
318
+ tasks: list[dict[str, object]],
319
+ registry: dict[str, object] | None = None,
320
+ ) -> dict[str, object]:
114
321
  assignments: list[dict[str, object]] = []
115
322
  task_dependencies = _task_dependencies(tasks)
116
323
  dependency_lookup: dict[str, list[str]] = {}
@@ -129,7 +336,7 @@ def build_dispatch_plan(tasks: list[dict[str, object]]) -> dict[str, object]:
129
336
  ("quality-reviewer", [f"{task_id}-spec-reviewer"]),
130
337
  ]
131
338
  for role, depends_on in chain:
132
- agent, codex_type = _role_agent(role)
339
+ agent, agent_type, agent_prompt = _select_agent(role, task, registry)
133
340
  assignments.append(
134
341
  {
135
342
  "id": f"{task_id}-{role}",
@@ -137,7 +344,8 @@ def build_dispatch_plan(tasks: list[dict[str, object]]) -> dict[str, object]:
137
344
  "title": task["title"],
138
345
  "role": role,
139
346
  "recommended_agent": agent,
140
- "codex_type": codex_type,
347
+ "agent_type": agent_type,
348
+ "agent_prompt": agent_prompt,
141
349
  "depends_on": depends_on,
142
350
  "files": task.get("files", []),
143
351
  "steps": task.get("steps", []),
@@ -147,7 +355,7 @@ def build_dispatch_plan(tasks: list[dict[str, object]]) -> dict[str, object]:
147
355
 
148
356
  return {
149
357
  "version": 1,
150
- "adapter": "codex",
358
+ "adapter": str(registry.get("default_adapter", "codex")) if registry else "codex",
151
359
  "tasks": tasks,
152
360
  "task_dependencies": task_dependencies,
153
361
  "assignments": assignments,
@@ -176,7 +384,7 @@ def render_dispatch_plan(dispatch_plan: dict[str, object]) -> str:
176
384
  f" task: {assignment['task']}",
177
385
  f" role: {assignment['role']}",
178
386
  f" recommended_agent: {assignment['recommended_agent']}",
179
- f" codex_type: {assignment['codex_type']}",
387
+ f" agent_type: {assignment['agent_type']}",
180
388
  " depends_on:",
181
389
  ]
182
390
  )
@@ -209,7 +417,8 @@ def build_initial_status(dispatch_plan: dict[str, object]) -> dict[str, object]:
209
417
  "role": assignment["role"],
210
418
  "task": assignment["task"],
211
419
  "recommended_agent": assignment["recommended_agent"],
212
- "codex_type": assignment["codex_type"],
420
+ "agent_type": assignment["agent_type"],
421
+ "agent_prompt": assignment.get("agent_prompt", ""),
213
422
  }
214
423
 
215
424
  return {"version": 1, "current_batch": 1, "assignments": assignments}
@@ -226,16 +435,33 @@ def build_initial_metrics(dispatch_plan: dict[str, object]) -> dict[str, object]
226
435
  }
227
436
 
228
437
 
438
+ def _render_named_items(title: str, items: object) -> list[str]:
439
+ lines = [f"## {title}", ""]
440
+ if isinstance(items, list) and items:
441
+ for item in items:
442
+ lines.append(f"- {item}")
443
+ else:
444
+ lines.append("- none")
445
+ lines.append("")
446
+ return lines
447
+
448
+
229
449
  def render_assignment_prompt(assignment: dict[str, object]) -> str:
230
- return "\n".join(
231
- [
232
- f"# {assignment['id']}",
233
- "",
234
- f"Role: {assignment['role']}",
235
- f"Recommended agent: {assignment['recommended_agent']}",
236
- f"Codex type: {assignment['codex_type']}",
237
- "",
238
- "You are not alone in this codebase. Respect the write boundary, do not revert other agents' edits, and report changed files.",
239
- "",
240
- ]
241
- )
450
+ lines = [
451
+ f"# {assignment['id']}",
452
+ "",
453
+ f"Role: {assignment['role']}",
454
+ f"Recommended agent: {assignment['recommended_agent']}",
455
+ f"Agent type: {assignment['agent_type']}",
456
+ f"Task: {assignment.get('title', '')}",
457
+ "",
458
+ ]
459
+ agent_prompt = assignment.get("agent_prompt")
460
+ if isinstance(agent_prompt, str) and agent_prompt.strip():
461
+ lines.extend(["## Agent prompt", "", agent_prompt.strip(), ""])
462
+ lines.append("You are not alone in this codebase. Respect the write boundary, do not revert other agents' edits, and report changed files.")
463
+ lines.append("")
464
+ lines.extend(_render_named_items("Files", assignment.get("files")))
465
+ lines.extend(_render_named_items("Steps", assignment.get("steps")))
466
+ lines.extend(_render_named_items("Commands", assignment.get("commands")))
467
+ return "\n".join(lines)
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env python3
2
+ """Small resumable directory archive helpers."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ import filecmp
9
+ import shutil
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ArchiveResult:
14
+ status: str
15
+ destination: Path
16
+ message: str = ""
17
+
18
+ @property
19
+ def ok(self) -> bool:
20
+ return self.status == "archived"
21
+
22
+ @property
23
+ def partial(self) -> bool:
24
+ return self.status == "partial"
25
+
26
+
27
+ def directories_match(left: Path, right: Path) -> bool:
28
+ """Return True when two directory trees contain the same files and bytes."""
29
+ if not left.is_dir() or not right.is_dir():
30
+ return False
31
+
32
+ left_files = sorted(path.relative_to(left) for path in left.rglob("*") if path.is_file())
33
+ right_files = sorted(path.relative_to(right) for path in right.rglob("*") if path.is_file())
34
+ if left_files != right_files:
35
+ return False
36
+
37
+ return all(filecmp.cmp(left / rel, right / rel, shallow=False) for rel in left_files)
38
+
39
+
40
+ def archive_directory_resumable(source: Path, destination: Path) -> ArchiveResult:
41
+ """Copy, verify, then delete a directory, resuming matching duplicate state."""
42
+ if not source.is_dir():
43
+ return ArchiveResult("failed", destination, f"source directory not found: {source}")
44
+
45
+ if destination.exists():
46
+ if not destination.is_dir():
47
+ return ArchiveResult("failed", destination, f"archive destination is not a directory: {destination}")
48
+ if not directories_match(source, destination):
49
+ return ArchiveResult(
50
+ "failed",
51
+ destination,
52
+ f"archive source and destination differ: {source} | {destination}",
53
+ )
54
+ else:
55
+ destination.parent.mkdir(parents=True, exist_ok=True)
56
+ try:
57
+ shutil.copytree(source, destination)
58
+ except (OSError, IOError, shutil.Error) as exc:
59
+ return ArchiveResult("failed", destination, f"failed to copy directory to archive: {exc}")
60
+ if not directories_match(source, destination):
61
+ return ArchiveResult("failed", destination, f"archive verification failed: {source} | {destination}")
62
+
63
+ try:
64
+ shutil.rmtree(source)
65
+ except (OSError, IOError) as exc:
66
+ return ArchiveResult(
67
+ "partial",
68
+ destination,
69
+ f"archive verified but source remains: {source}; delete failed: {exc}",
70
+ )
71
+
72
+ return ArchiveResult("archived", destination)
@@ -10,11 +10,11 @@ Provides:
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- import shutil
14
13
  import sys
15
14
  from datetime import datetime
16
15
  from pathlib import Path
17
16
 
17
+ from .archive_utils import archive_directory_resumable
18
18
  from .paths import get_repo_root
19
19
 
20
20
  PATH_TRAVERSAL_PREFIXES = ("./", "../")
@@ -139,23 +139,10 @@ def archive_task_dir(task_dir_abs: Path, repo_root: Path | None = None) -> Path
139
139
  task_name = task_dir_abs.name
140
140
  archive_dest = month_dir / task_name
141
141
 
142
- if archive_dest.exists():
143
- print(f"Error: archive target already exists: {archive_dest}", file=sys.stderr)
144
- print(f"Source task directory is still present: {task_dir_abs}", file=sys.stderr)
145
- return None
146
-
147
- try:
148
- shutil.move(str(task_dir_abs), str(archive_dest))
149
- except (OSError, IOError, shutil.Error) as e:
150
- print(f"Error: Failed to move task to archive: {e}", file=sys.stderr)
151
- if archive_dest.exists() and task_dir_abs.exists():
152
- print("Error: archive move left both source and target paths present.", file=sys.stderr)
153
- print(f" Source: {task_dir_abs}", file=sys.stderr)
154
- print(f" Target: {archive_dest}", file=sys.stderr)
155
- elif archive_dest.exists():
156
- print(f"Error: archive target path exists after failure: {archive_dest}", file=sys.stderr)
157
- elif task_dir_abs.exists():
158
- print(f"Error: source task directory is still present: {task_dir_abs}", file=sys.stderr)
142
+ result = archive_directory_resumable(task_dir_abs, archive_dest)
143
+ if not result.ok:
144
+ level = "Warning" if result.partial else "Error"
145
+ print(f"{level}: {result.message}", file=sys.stderr)
159
146
  return None
160
147
 
161
148
  return archive_dest
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Shared cowork-flow command dispatcher."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ COMMAND_SCRIPTS = {
12
+ "resume": "resume.py",
13
+ "task": "task.py",
14
+ "change": "change.py",
15
+ "get-context": "get_context.py",
16
+ "get_context": "get_context.py",
17
+ "get-developer": "get_developer.py",
18
+ "get_developer": "get_developer.py",
19
+ "init-developer": "init_developer.py",
20
+ "init_developer": "init_developer.py",
21
+ "add-session": "add_session.py",
22
+ "add_session": "add_session.py",
23
+ "agent-team": "agent_team.py",
24
+ "agent_team": "agent_team.py",
25
+ }
26
+
27
+
28
+ def print_usage() -> None:
29
+ print(
30
+ """Usage:
31
+ ./.cowork-flow/run <command> [args...]
32
+ ./.cowork-flow/run python [python-args...]
33
+
34
+ Common commands:
35
+ resume
36
+ task
37
+ change
38
+ get-context
39
+ get-developer
40
+ init-developer
41
+ add-session
42
+ agent-team
43
+ """.rstrip()
44
+ )
45
+
46
+
47
+ def scripts_dir() -> Path:
48
+ return Path(__file__).resolve().parent
49
+
50
+
51
+ def run_python(args: list[str]) -> int:
52
+ completed = subprocess.run([sys.executable, *args], check=False)
53
+ return int(completed.returncode)
54
+
55
+
56
+ def run_script(script_name: str, args: list[str]) -> int:
57
+ script_path = scripts_dir() / script_name
58
+ if not script_path.is_file():
59
+ print(f"Error: script not found: {script_path}", file=sys.stderr)
60
+ return 2
61
+ return run_python([str(script_path), *args])
62
+
63
+
64
+ def main(argv: list[str] | None = None) -> int:
65
+ args = list(sys.argv[1:] if argv is None else argv)
66
+ if not args:
67
+ print_usage()
68
+ return 2
69
+
70
+ command = args[0]
71
+ rest = args[1:]
72
+ if command in {"-h", "--help", "help"}:
73
+ print_usage()
74
+ return 0
75
+ if command == "python":
76
+ return run_python(rest)
77
+
78
+ script_name = COMMAND_SCRIPTS.get(command)
79
+ if script_name is None:
80
+ candidate = scripts_dir() / f"{command}.py"
81
+ if candidate.is_file():
82
+ script_name = candidate.name
83
+ else:
84
+ print(f"Error: unknown cowork-flow command: {command}", file=sys.stderr)
85
+ print_usage()
86
+ return 2
87
+
88
+ return run_script(script_name, rest)
89
+
90
+
91
+ if __name__ == "__main__":
92
+ raise SystemExit(main())
@@ -179,10 +179,33 @@ done
179
179
 
180
180
  ### 7.7 按 superpowers 方法执行
181
181
 
182
- - 不支持 subAgent 模式时,使用 `superpowers:executing-plans`;否则:
183
- - 当 `.cowork-flow/config.yaml` 中设置 `agent_team.enabled: true` 时,执行 plan 优先使用 `./.cowork-flow/run agent-team prepare <task-dir> --plan <plan-file>` 生成调度图,再用 `./.cowork-flow/run agent-team next <task-dir>` 获取可并行 assignments,由主 agent 审核并调度 subAgent。
184
- - 当 `.cowork-flow/config.yaml` 中设置 `agent_team.enabled: false` 时,优先使用 `superpowers:subagent-driven-development` 开启多 agent 并行执行。
185
- - 严格遵循 `superpowers:test-driven-development`,不得先写生产代码再补测试。
182
+ 执行 plan 前,先判断**是否存在适合并行执行的独立任务**。并行是优化手段,不是完成定义;`agent_team.enabled` 只决定在适合并行时优先使用哪种机制,不决定是否必须并行。
183
+
184
+ 适合并行的条件包括:
185
+
186
+ - 子任务之间没有共享写文件,或共享写文件已有明确顺序边界。
187
+ - 子任务之间没有强顺序依赖。
188
+ - 每个子任务都有清晰输入、输出和验收方式。
189
+ - 主 agent 能在集成前独立审查每个结果。
190
+ - 并行带来的收益大于调度、审阅和集成成本。
191
+
192
+ 不适合并行的情况包括:
193
+
194
+ - 改动集中在同一组强耦合文件。
195
+ - 需要密集的 TDD 红绿循环或即时反馈。
196
+ - parser、schema、runtime、测试需要同步演进。
197
+ - 子任务边界不清,拆分后的主要成本会落在集成。
198
+ - 任务规模小于并行调度成本。
199
+
200
+ 不得为了满足流程形式而强行拆分高耦合任务。若不适合并行,必须简要记录不使用并行的理由,并在当前会话中按 plan 顺序执行。
201
+
202
+ 若适合并行:
203
+
204
+ - 当 `.cowork-flow/config.yaml` 中设置 `agent_team.enabled: true` 时,使用 `./.cowork-flow/run agent-team prepare <task-dir> --plan <plan-file>` 生成调度图,再用 `./.cowork-flow/run agent-team next <task-dir>` 获取可并行 assignments,由主 agent 审核并调度 subAgent。
205
+ - 当 `.cowork-flow/config.yaml` 中设置 `agent_team.enabled: false` 且宿主支持 subAgent 时,可使用 `superpowers:subagent-driven-development` 开启多 agent 并行执行。
206
+ - 当宿主不支持 subAgent 或任务不适合并行时,使用 `superpowers:executing-plans` 或当前会话顺序执行。
207
+
208
+ 无论是否并行,都必须严格遵循 `superpowers:test-driven-development`,不得先写生产代码再补测试。
186
209
 
187
210
  执行中必须持续同步:
188
211