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 +27 -1
- package/package.json +3 -2
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/agent-team/agents.yaml +78 -4
- package/template/.cowork-flow/run +10 -97
- package/template/.cowork-flow/run.cmd +12 -122
- package/template/.cowork-flow/scripts/agent_team.py +84 -8
- package/template/.cowork-flow/scripts/change.py +68 -15
- package/template/.cowork-flow/scripts/common/agent_team.py +250 -24
- package/template/.cowork-flow/scripts/common/archive_utils.py +72 -0
- package/template/.cowork-flow/scripts/common/task_utils.py +5 -18
- package/template/.cowork-flow/scripts/run.py +92 -0
- package/template/.cowork-flow/workflow.md +27 -4
package/README.md
CHANGED
|
@@ -219,4 +219,30 @@ cowork-flow sync . --dry-run
|
|
|
219
219
|
|
|
220
220
|
## 发布流程
|
|
221
221
|
|
|
222
|
-
CI
|
|
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.
|
|
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.
|
|
1
|
+
0.0.7
|
|
@@ -5,7 +5,7 @@ default_adapter: codex
|
|
|
5
5
|
|
|
6
6
|
agents:
|
|
7
7
|
implementer:
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
55
|
-
echo "
|
|
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 "
|
|
90
|
-
echo "
|
|
91
|
-
echo "
|
|
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
|
-
|
|
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 [ "
|
|
118
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
137
|
-
echo
|
|
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
|
|
174
|
-
echo
|
|
175
|
-
echo
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 / "
|
|
396
|
+
adapters_dir / f"{dispatch_plan['adapter']}.json",
|
|
321
397
|
{
|
|
322
|
-
"adapter": "
|
|
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']}\
|
|
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.
|
|
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
|
-
|
|
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 /
|
|
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
|
-
|
|
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: {
|
|
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"# {
|
|
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":
|
|
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 {
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
"
|
|
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"
|
|
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
|
-
"
|
|
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
|
-
|
|
231
|
-
[
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|