cowork-flow 0.0.34 → 0.0.35
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/package.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/__init__.py +1 -0
- package/template/.cowork-flow/scripts/commands/task.py +6 -16
- package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +10 -1
- package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +7 -2
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/hooks/runtime/scripts/__init__.py +1 -0
- package/template/.zcode/hooks/runtime/scripts/commands/task.py +7 -17
- package/template/.zcode/hooks/runtime/scripts/common/gates/tdd_evidence.py +10 -1
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_implementation.py +7 -2
- package/template/.zcode/hooks/runtime/scripts/common/task/readiness.py +18 -0
- package/template/.zcode/scaffold/.cowork-flow/run +79 -0
- package/template/.zcode/scaffold/.cowork-flow/run.cmd +88 -0
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.35
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Cowork Flow runtime scripts."""
|
|
@@ -39,6 +39,7 @@ else:
|
|
|
39
39
|
import _bootstrap # noqa: F401
|
|
40
40
|
from common.core.files import (
|
|
41
41
|
read_json_file as _read_json_file,
|
|
42
|
+
read_text_utf8 as _read_text,
|
|
42
43
|
write_json_file as _write_json_file,
|
|
43
44
|
)
|
|
44
45
|
from common.gates.gates import GateResult, GateRunner
|
|
@@ -466,17 +467,6 @@ def _write_jsonl(path: Path, entries: list[dict]) -> None:
|
|
|
466
467
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
467
468
|
|
|
468
469
|
|
|
469
|
-
def _read_text_if_present(path: Path) -> str:
|
|
470
|
-
"""读取文本文件并返回去空白后的内容;不存在或读取失败时返回空字符串。"""
|
|
471
|
-
if not path.is_file():
|
|
472
|
-
return ""
|
|
473
|
-
|
|
474
|
-
try:
|
|
475
|
-
return path.read_text(encoding="utf-8").strip()
|
|
476
|
-
except (OSError, IOError):
|
|
477
|
-
return ""
|
|
478
|
-
|
|
479
|
-
|
|
480
470
|
def _task_start_blockers(task_dir: Path) -> list[str]:
|
|
481
471
|
"""返回启动任务前必须处理的准备阻塞项。"""
|
|
482
472
|
blockers: list[str] = []
|
|
@@ -486,16 +476,16 @@ def _task_start_blockers(task_dir: Path) -> list[str]:
|
|
|
486
476
|
blockers.append("task.json is missing")
|
|
487
477
|
|
|
488
478
|
# 只检查存在性 — R-WF-008 负责章节内容完整性
|
|
489
|
-
if
|
|
479
|
+
if _read_text(task_dir / "prd.md"):
|
|
490
480
|
# 旧格式:提示需要迁移(cmd_start 会自动迁移)
|
|
491
|
-
if not
|
|
481
|
+
if not _read_text(task_dir / "decision-anchor.md"):
|
|
492
482
|
blockers.append("prd.md found; task start will auto-migrate to decision-anchor.md")
|
|
493
|
-
elif not
|
|
483
|
+
elif not _read_text(task_dir / "decision-anchor.md"):
|
|
494
484
|
blockers.append("decision-anchor.md is missing or empty")
|
|
495
485
|
|
|
496
486
|
for jsonl_name in CONTEXT_JSONL_FILES:
|
|
497
487
|
jsonl_file = task_dir / jsonl_name
|
|
498
|
-
if not
|
|
488
|
+
if not _read_text(jsonl_file):
|
|
499
489
|
blockers.append(f"{jsonl_name} is missing or empty")
|
|
500
490
|
|
|
501
491
|
return blockers
|
|
@@ -516,7 +506,7 @@ def _task_context_validation_issues(
|
|
|
516
506
|
|
|
517
507
|
for jsonl_name in CONTEXT_JSONL_FILES:
|
|
518
508
|
jsonl_file = task_dir / jsonl_name
|
|
519
|
-
if not
|
|
509
|
+
if not _read_text(jsonl_file):
|
|
520
510
|
continue
|
|
521
511
|
|
|
522
512
|
error_count = _validate_jsonl(jsonl_file, repo_root, quiet=quiet)
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""
|
|
3
|
+
TDD 红绿证据校验(task_review / task_complete 阶段)。
|
|
4
|
+
|
|
5
|
+
读取 <task>/tdd.jsonl,验证每条 evidence 或 exemption 记录的结构完整
|
|
6
|
+
性与业务意图。验证结果分两类出口:
|
|
7
|
+
- validate_tdd_evidence: 全量校验(review 阶段)
|
|
8
|
+
- validate_tdd_red_evidence: 仅检查 red 证据完整性(implement 入口提醒)
|
|
9
|
+
|
|
10
|
+
acceptanceId 的提取同时支持 AC-001、AC-1、验收标准:1 三种写法。
|
|
11
|
+
"""
|
|
3
12
|
|
|
4
13
|
from __future__ import annotations
|
|
5
14
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
R-AG-002 到 R-AG-009 的门禁检查器。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
在 task_review 阶段运行:通过 git diff 获取工作树的未提交改动,逐项
|
|
6
|
+
检查 forbidden_action 规则。每项规则以 (repo_root, modified_files, diff_output,
|
|
7
|
+
rule_index) 为入参,返回 violation dict 列表。
|
|
8
|
+
|
|
9
|
+
结果汇总到 GateResult:存在 severity=block 的 violation 时阻断 review 流程,
|
|
10
|
+
warn 级别仅输出警告。
|
|
6
11
|
"""
|
|
7
12
|
|
|
8
13
|
from __future__ import annotations
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Cowork Flow zcode plugin runtime scripts."""
|
|
@@ -39,6 +39,7 @@ else:
|
|
|
39
39
|
import _bootstrap # noqa: F401
|
|
40
40
|
from common.core.files import (
|
|
41
41
|
read_json_file as _read_json_file,
|
|
42
|
+
read_text_utf8 as _read_text,
|
|
42
43
|
write_json_file as _write_json_file,
|
|
43
44
|
)
|
|
44
45
|
from common.gates.gates import GateResult, GateRunner
|
|
@@ -466,17 +467,6 @@ def _write_jsonl(path: Path, entries: list[dict]) -> None:
|
|
|
466
467
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
467
468
|
|
|
468
469
|
|
|
469
|
-
def _read_text_if_present(path: Path) -> str:
|
|
470
|
-
"""读取文本文件并返回去空白后的内容;不存在或读取失败时返回空字符串。"""
|
|
471
|
-
if not path.is_file():
|
|
472
|
-
return ""
|
|
473
|
-
|
|
474
|
-
try:
|
|
475
|
-
return path.read_text(encoding="utf-8").strip()
|
|
476
|
-
except (OSError, IOError):
|
|
477
|
-
return ""
|
|
478
|
-
|
|
479
|
-
|
|
480
470
|
def _task_start_blockers(task_dir: Path) -> list[str]:
|
|
481
471
|
"""返回启动任务前必须处理的准备阻塞项。"""
|
|
482
472
|
blockers: list[str] = []
|
|
@@ -486,16 +476,16 @@ def _task_start_blockers(task_dir: Path) -> list[str]:
|
|
|
486
476
|
blockers.append("task.json is missing")
|
|
487
477
|
|
|
488
478
|
# 只检查存在性 — R-WF-008 负责章节内容完整性
|
|
489
|
-
if
|
|
479
|
+
if _read_text(task_dir / "prd.md"):
|
|
490
480
|
# 旧格式:提示需要迁移(cmd_start 会自动迁移)
|
|
491
|
-
if not
|
|
481
|
+
if not _read_text(task_dir / "decision-anchor.md"):
|
|
492
482
|
blockers.append("prd.md found; task start will auto-migrate to decision-anchor.md")
|
|
493
|
-
elif not
|
|
483
|
+
elif not _read_text(task_dir / "decision-anchor.md"):
|
|
494
484
|
blockers.append("decision-anchor.md is missing or empty")
|
|
495
485
|
|
|
496
486
|
for jsonl_name in CONTEXT_JSONL_FILES:
|
|
497
487
|
jsonl_file = task_dir / jsonl_name
|
|
498
|
-
if not
|
|
488
|
+
if not _read_text(jsonl_file):
|
|
499
489
|
blockers.append(f"{jsonl_name} is missing or empty")
|
|
500
490
|
|
|
501
491
|
return blockers
|
|
@@ -516,7 +506,7 @@ def _task_context_validation_issues(
|
|
|
516
506
|
|
|
517
507
|
for jsonl_name in CONTEXT_JSONL_FILES:
|
|
518
508
|
jsonl_file = task_dir / jsonl_name
|
|
519
|
-
if not
|
|
509
|
+
if not _read_text(jsonl_file):
|
|
520
510
|
continue
|
|
521
511
|
|
|
522
512
|
error_count = _validate_jsonl(jsonl_file, repo_root, quiet=quiet)
|
|
@@ -1520,7 +1510,7 @@ def cmd_next(args: argparse.Namespace) -> int:
|
|
|
1520
1510
|
return 0
|
|
1521
1511
|
|
|
1522
1512
|
if status in DONE_STATUSES:
|
|
1523
|
-
print("Next action: finalize,
|
|
1513
|
+
print("Next action: finalize, archive, commit, and record session")
|
|
1524
1514
|
print("Command: git status --short")
|
|
1525
1515
|
linked_changes = _linked_active_changes_for_task(repo_root, task_dir)
|
|
1526
1516
|
print(f"Then: ./.cowork-flow/run task archive {Path(task_path).name}")
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""
|
|
3
|
+
TDD 红绿证据校验(task_review / task_complete 阶段)。
|
|
4
|
+
|
|
5
|
+
读取 <task>/tdd.jsonl,验证每条 evidence 或 exemption 记录的结构完整
|
|
6
|
+
性与业务意图。验证结果分两类出口:
|
|
7
|
+
- validate_tdd_evidence: 全量校验(review 阶段)
|
|
8
|
+
- validate_tdd_red_evidence: 仅检查 red 证据完整性(implement 入口提醒)
|
|
9
|
+
|
|
10
|
+
acceptanceId 的提取同时支持 AC-001、AC-1、验收标准:1 三种写法。
|
|
11
|
+
"""
|
|
3
12
|
|
|
4
13
|
from __future__ import annotations
|
|
5
14
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
R-AG-002 到 R-AG-009 的门禁检查器。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
在 task_review 阶段运行:通过 git diff 获取工作树的未提交改动,逐项
|
|
6
|
+
检查 forbidden_action 规则。每项规则以 (repo_root, modified_files, diff_output,
|
|
7
|
+
rule_index) 为入参,返回 violation dict 列表。
|
|
8
|
+
|
|
9
|
+
结果汇总到 GateResult:存在 severity=block 的 violation 时阻断 review 流程,
|
|
10
|
+
warn 级别仅输出警告。
|
|
6
11
|
"""
|
|
7
12
|
|
|
8
13
|
from __future__ import annotations
|
|
@@ -246,4 +246,22 @@ def task_readiness_blockers(repo_root: Path, task_dir: Path) -> list[str]:
|
|
|
246
246
|
if not _has_verification_command(plan_text):
|
|
247
247
|
blockers.append(f"L2 readiness ({slug}): linked plan is missing verification commands")
|
|
248
248
|
|
|
249
|
+
blockers.extend(_check_doubt_review(task_dir, change_dir))
|
|
250
|
+
|
|
249
251
|
return blockers
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _check_doubt_review(task_dir: Path, change_dir: Path) -> list[str]:
|
|
255
|
+
"""L2 optional check: doubt-review 记录是否存在 (warn 级别, 不阻断)."""
|
|
256
|
+
doubt_file = Path(task_dir) / "doubt-review.md"
|
|
257
|
+
if not doubt_file.is_file():
|
|
258
|
+
return [
|
|
259
|
+
f"L2 doubt-review 建议: 在 {doubt_file} 中记录关键决策的 "
|
|
260
|
+
f"CLAIM+CONTRACT(非强制,但强烈推荐)"
|
|
261
|
+
]
|
|
262
|
+
text = _read_text(doubt_file)
|
|
263
|
+
if "CLAIM:" not in text:
|
|
264
|
+
return [
|
|
265
|
+
f"L2 doubt-review: {doubt_file} 存在但没有 CLAIM 记录"
|
|
266
|
+
]
|
|
267
|
+
return []
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -u
|
|
3
|
+
|
|
4
|
+
MIN_PYTHON_LABEL="Python 3.9+"
|
|
5
|
+
VERSION_CHECK='import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)'
|
|
6
|
+
WORKFLOW_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd) || exit 1
|
|
7
|
+
RUNNER_SCRIPT="$WORKFLOW_DIR/scripts/run.py"
|
|
8
|
+
|
|
9
|
+
candidate_is_valid() {
|
|
10
|
+
candidate_cmd=$1
|
|
11
|
+
candidate_arg=${2:-}
|
|
12
|
+
|
|
13
|
+
command -v "$candidate_cmd" >/dev/null 2>&1 || return 1
|
|
14
|
+
|
|
15
|
+
if [ -n "$candidate_arg" ]; then
|
|
16
|
+
"$candidate_cmd" "$candidate_arg" -c "$VERSION_CHECK" >/dev/null 2>&1
|
|
17
|
+
else
|
|
18
|
+
"$candidate_cmd" -c "$VERSION_CHECK" >/dev/null 2>&1
|
|
19
|
+
fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
select_explicit_python() {
|
|
23
|
+
env_name=$1
|
|
24
|
+
env_value=$2
|
|
25
|
+
|
|
26
|
+
if candidate_is_valid "$env_value"; then
|
|
27
|
+
SELECTED_PYTHON=$env_value
|
|
28
|
+
SELECTED_PYTHON_ARG=
|
|
29
|
+
return 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
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 ${MIN_PYTHON_LABEL} interpreter path." >&2
|
|
34
|
+
exit 127
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
select_python() {
|
|
38
|
+
SELECTED_PYTHON=
|
|
39
|
+
SELECTED_PYTHON_ARG=
|
|
40
|
+
|
|
41
|
+
if [ -n "${COWORK_FLOW_PYTHON:-}" ]; then
|
|
42
|
+
select_explicit_python "COWORK_FLOW_PYTHON" "$COWORK_FLOW_PYTHON"
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [ -n "${PYTHON:-}" ]; then
|
|
47
|
+
select_explicit_python "PYTHON" "$PYTHON"
|
|
48
|
+
return 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if candidate_is_valid "python3"; then
|
|
52
|
+
SELECTED_PYTHON=python3
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if candidate_is_valid "python"; then
|
|
57
|
+
SELECTED_PYTHON=python
|
|
58
|
+
return 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
if candidate_is_valid "py" "-3"; then
|
|
62
|
+
SELECTED_PYTHON=py
|
|
63
|
+
SELECTED_PYTHON_ARG=-3
|
|
64
|
+
return 0
|
|
65
|
+
fi
|
|
66
|
+
|
|
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
|
|
70
|
+
exit 127
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
select_python
|
|
74
|
+
|
|
75
|
+
if [ -n "$SELECTED_PYTHON_ARG" ]; then
|
|
76
|
+
exec "$SELECTED_PYTHON" "$SELECTED_PYTHON_ARG" "$RUNNER_SCRIPT" "$@"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
exec "$SELECTED_PYTHON" "$RUNNER_SCRIPT" "$@"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal EnableExtensions
|
|
3
|
+
|
|
4
|
+
set "MIN_PYTHON_LABEL=Python 3.9+"
|
|
5
|
+
set "VERSION_CHECK=import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)"
|
|
6
|
+
set "WORKFLOW_DIR=%~dp0"
|
|
7
|
+
set "RUNNER_SCRIPT=%WORKFLOW_DIR%scripts\run.py"
|
|
8
|
+
set "SELECTED_PYTHON="
|
|
9
|
+
set "SELECTED_PYTHON_ARG="
|
|
10
|
+
|
|
11
|
+
call :select_python
|
|
12
|
+
if errorlevel 1 exit /b %ERRORLEVEL%
|
|
13
|
+
|
|
14
|
+
if not "%SELECTED_PYTHON_ARG%"=="" (
|
|
15
|
+
"%SELECTED_PYTHON%" "%SELECTED_PYTHON_ARG%" "%RUNNER_SCRIPT%" %*
|
|
16
|
+
) else (
|
|
17
|
+
"%SELECTED_PYTHON%" "%RUNNER_SCRIPT%" %*
|
|
18
|
+
)
|
|
19
|
+
exit /b %ERRORLEVEL%
|
|
20
|
+
|
|
21
|
+
:candidate_is_valid
|
|
22
|
+
set "CANDIDATE_CMD=%~1"
|
|
23
|
+
set "CANDIDATE_ARG=%~2"
|
|
24
|
+
|
|
25
|
+
where "%CANDIDATE_CMD%" >nul 2>nul
|
|
26
|
+
if errorlevel 1 (
|
|
27
|
+
if not exist "%CANDIDATE_CMD%" exit /b 1
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if not "%CANDIDATE_ARG%"=="" (
|
|
31
|
+
"%CANDIDATE_CMD%" "%CANDIDATE_ARG%" -c "%VERSION_CHECK%" >nul 2>nul
|
|
32
|
+
) else (
|
|
33
|
+
"%CANDIDATE_CMD%" -c "%VERSION_CHECK%" >nul 2>nul
|
|
34
|
+
)
|
|
35
|
+
exit /b %ERRORLEVEL%
|
|
36
|
+
|
|
37
|
+
:select_explicit_python
|
|
38
|
+
set "ENV_NAME=%~1"
|
|
39
|
+
set "ENV_VALUE=%~2"
|
|
40
|
+
|
|
41
|
+
call :candidate_is_valid "%ENV_VALUE%" ""
|
|
42
|
+
if not errorlevel 1 (
|
|
43
|
+
set "SELECTED_PYTHON=%ENV_VALUE%"
|
|
44
|
+
set "SELECTED_PYTHON_ARG="
|
|
45
|
+
exit /b 0
|
|
46
|
+
)
|
|
47
|
+
|
|
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.9+ interpreter path. 1>&2
|
|
50
|
+
exit /b 127
|
|
51
|
+
|
|
52
|
+
:select_python
|
|
53
|
+
set "SELECTED_PYTHON="
|
|
54
|
+
set "SELECTED_PYTHON_ARG="
|
|
55
|
+
|
|
56
|
+
if not "%COWORK_FLOW_PYTHON%"=="" (
|
|
57
|
+
call :select_explicit_python "COWORK_FLOW_PYTHON" "%COWORK_FLOW_PYTHON%"
|
|
58
|
+
exit /b %ERRORLEVEL%
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if not "%PYTHON%"=="" (
|
|
62
|
+
call :select_explicit_python "PYTHON" "%PYTHON%"
|
|
63
|
+
exit /b %ERRORLEVEL%
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
call :candidate_is_valid "python3" ""
|
|
67
|
+
if not errorlevel 1 (
|
|
68
|
+
set "SELECTED_PYTHON=python3"
|
|
69
|
+
exit /b 0
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
call :candidate_is_valid "python" ""
|
|
73
|
+
if not errorlevel 1 (
|
|
74
|
+
set "SELECTED_PYTHON=python"
|
|
75
|
+
exit /b 0
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
call :candidate_is_valid "py" "-3"
|
|
79
|
+
if not errorlevel 1 (
|
|
80
|
+
set "SELECTED_PYTHON=py"
|
|
81
|
+
set "SELECTED_PYTHON_ARG=-3"
|
|
82
|
+
exit /b 0
|
|
83
|
+
)
|
|
84
|
+
|
|
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
|
|
88
|
+
exit /b 127
|