cowork-flow 0.0.31 → 0.0.33
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/.codex/agents/cowork-check.toml +7 -0
- package/template/.codex/agents/cowork-implement.toml +6 -0
- package/template/.codex/hooks/inject-workflow-state.py +0 -0
- package/template/.codex/hooks.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/commands/task.py +28 -1
- package/template/.cowork-flow/scripts/common/core/files.py +8 -0
- package/template/.cowork-flow/scripts/common/gates/anti_rationalization.py +55 -0
- package/template/.cowork-flow/scripts/common/gates/gates.py +5 -0
- package/template/.cowork-flow/scripts/common/gates/spec_validator.py +53 -0
- package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +4 -5
- package/template/.cowork-flow/scripts/common/gates/test_intent.py +1 -5
- package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +294 -133
- package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +32 -0
- package/template/.cowork-flow/scripts/common/gates/validate_rules.py +14 -17
- package/template/.cowork-flow/scripts/common/task/batch_mode.py +145 -0
- package/template/.cowork-flow/scripts/common/task/readiness.py +22 -4
- package/template/.cowork-flow/spec/backend/quality-guidelines.md +2 -0
- package/template/.cowork-flow/spec/contracts/error-output-as-data.md +54 -0
- package/template/.cowork-flow/spec/frontend/quality-guidelines.md +2 -0
- package/template/.cowork-flow/spec/index.md +16 -0
- package/template/.cowork-flow/spec/references/definition-of-done.md +45 -0
- package/template/.cowork-flow/spec/references/index.md +20 -0
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +139 -0
- package/template/.cowork-flow/spec/references/security-checklist.md +51 -0
- package/template/.cowork-flow/spec/references/testing-checklist.md +45 -0
- package/template/.cowork-flow/spec/runtime/contract-registry.json +57 -0
- package/template/.cowork-flow/spec/runtime/rules.json +60 -0
- package/template/.cowork-flow/workflow.md +5 -0
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/hooks/runtime/run +2 -0
- package/template/.zcode/hooks/runtime/scripts/common/core/files.py +8 -0
- package/template/.zcode/hooks/runtime/scripts/common/gates/anti_rationalization.py +55 -0
- package/template/.zcode/hooks/runtime/scripts/common/gates/gates.py +5 -0
- package/template/.zcode/hooks/runtime/scripts/common/gates/tdd_evidence.py +4 -5
- package/template/.zcode/hooks/runtime/scripts/common/gates/test_intent.py +1 -5
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_coding_standards.py +294 -133
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_rules.py +14 -17
- package/template/.zcode/hooks/runtime/scripts/common/task/readiness.py +4 -4
- package/template/AGENTS.md +31 -12
- package/template/skills/batch-mode/SKILL.md +117 -0
- package/template/skills/before-dev/SKILL.md +8 -0
- package/template/skills/break-loop/SKILL.md +53 -0
- package/template/skills/check/SKILL.md +63 -0
- package/template/skills/doubt-review/SKILL.md +118 -0
- package/template/skills/tdd/SKILL.md +13 -0
- package/template/skills/writing-plans/SKILL.md +23 -0
package/package.json
CHANGED
|
@@ -38,6 +38,13 @@ Rules:
|
|
|
38
38
|
- MUST NOT commit, archive, or mutate cowork-flow task state.
|
|
39
39
|
- Report findings, changed files, and exact verification commands.
|
|
40
40
|
- Include test intent findings that map tests back to PRD acceptance or regression behavior.
|
|
41
|
+
- BEFORE declaring PRD acceptance passed, run the spec-driven coding gate and
|
|
42
|
+
treat a nonzero exit as a hard blocker:
|
|
43
|
+
./.cowork-flow/run python .cowork-flow/scripts/common/gates/validate_coding_standaries.py --validate --repo-root .
|
|
44
|
+
This naturally enforces every rule the user added to
|
|
45
|
+
.cowork-flow/spec/backend/*.md and spec/frontend/*.md — they are not just
|
|
46
|
+
documentation but active validators. On violation, either fix the code or
|
|
47
|
+
explicitly discard the rule from the spec; do not skip the gate.
|
|
41
48
|
"""
|
|
42
49
|
|
|
43
50
|
[features]
|
|
@@ -40,6 +40,12 @@ Rules:
|
|
|
40
40
|
- Keep edits inside requested scope.
|
|
41
41
|
- Do not treat shallow tests as TDD evidence; red-green-refactor proof must include redCommand, redExitCode, greenCommand, greenExitCode, and PRD acceptance ID mapping.
|
|
42
42
|
- Report changed files and exact verification commands.
|
|
43
|
+
- BEFORE editing any source file, run the spec-driven coding gate and treat a
|
|
44
|
+
nonzero exit as a hard blocker:
|
|
45
|
+
./.cowork-flow/run python .cowork-flow/scripts/common/gates/validate_coding_standaries.py --validate --repo-root .
|
|
46
|
+
Parse every *.md under .cowork-flow/spec/backend and spec/frontend; rules
|
|
47
|
+
declared there activate validators dynamically. On violation, fix the code
|
|
48
|
+
(do not disable the rule) and re-run until exit code 0.
|
|
43
49
|
"""
|
|
44
50
|
|
|
45
51
|
[features]
|
|
File without changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.33
|
|
@@ -988,6 +988,10 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
988
988
|
)
|
|
989
989
|
return 1
|
|
990
990
|
|
|
991
|
+
if getattr(args, "auto", False):
|
|
992
|
+
from common.task.batch_mode import run_batch_entry
|
|
993
|
+
return run_batch_entry(repo_root, full_path, args)
|
|
994
|
+
|
|
991
995
|
state_blockers = transition_blockers(_load_task_status(full_path), "in_progress")
|
|
992
996
|
if state_blockers:
|
|
993
997
|
_print_transition_blockers(state_blockers)
|
|
@@ -1351,6 +1355,17 @@ def _print_blockers(blockers: list[str]) -> None:
|
|
|
1351
1355
|
print(f" - {blocker}")
|
|
1352
1356
|
|
|
1353
1357
|
|
|
1358
|
+
def _is_git_dirty(repo_root: Path) -> bool:
|
|
1359
|
+
"""Check if the git working tree has uncommitted changes."""
|
|
1360
|
+
try:
|
|
1361
|
+
rc, _, _ = _run_git_command(
|
|
1362
|
+
["status", "--porcelain"], cwd=repo_root
|
|
1363
|
+
)
|
|
1364
|
+
return rc != 0
|
|
1365
|
+
except (OSError, Exception):
|
|
1366
|
+
return False
|
|
1367
|
+
|
|
1368
|
+
|
|
1354
1369
|
def _linked_active_changes_for_task(repo_root: Path, task_dir: Path) -> list[str]:
|
|
1355
1370
|
from commands.change import linked_active_changes_for_task
|
|
1356
1371
|
|
|
@@ -1479,7 +1494,7 @@ def cmd_next(args: argparse.Namespace) -> int:
|
|
|
1479
1494
|
return 0
|
|
1480
1495
|
|
|
1481
1496
|
if status in DONE_STATUSES:
|
|
1482
|
-
print("Next action: finalize,
|
|
1497
|
+
print("Next action: finalize, archive, commit, and record session")
|
|
1483
1498
|
print("Command: git status --short")
|
|
1484
1499
|
linked_changes = _linked_active_changes_for_task(repo_root, task_dir)
|
|
1485
1500
|
print(f"Then: ./.cowork-flow/run task archive {Path(task_path).name}")
|
|
@@ -1546,6 +1561,14 @@ def cmd_archive(args: argparse.Namespace) -> int:
|
|
|
1546
1561
|
if linked_changes and not _linked_changes_ready_for_archive(repo_root, linked_changes):
|
|
1547
1562
|
return 1
|
|
1548
1563
|
|
|
1564
|
+
# R-WF-011: warn if uncommitted changes detected (archive should come before commit)
|
|
1565
|
+
if _is_git_dirty(repo_root):
|
|
1566
|
+
print(colored(
|
|
1567
|
+
"Warning: Uncommitted changes detected. "
|
|
1568
|
+
"Archive the task first, then commit the archived result.",
|
|
1569
|
+
Colors.YELLOW,
|
|
1570
|
+
), file=sys.stderr)
|
|
1571
|
+
|
|
1549
1572
|
# Archive
|
|
1550
1573
|
result = archive_task_complete(task_dir, repo_root)
|
|
1551
1574
|
if "archived_to" not in result:
|
|
@@ -1938,6 +1961,10 @@ def main() -> int:
|
|
|
1938
1961
|
# start
|
|
1939
1962
|
p_start = subparsers.add_parser("start", help="Set active session task")
|
|
1940
1963
|
p_start.add_argument("dir", help="Task directory")
|
|
1964
|
+
p_start.add_argument("--auto", action="store_true",
|
|
1965
|
+
help="Enable batch mode (requires --approved)")
|
|
1966
|
+
p_start.add_argument("--approved", action="store_true",
|
|
1967
|
+
help="User has approved the plan")
|
|
1941
1968
|
|
|
1942
1969
|
# current
|
|
1943
1970
|
subparsers.add_parser("current", help="Show active session task")
|
|
@@ -25,6 +25,14 @@ def read_json_file(path: Path) -> dict | None:
|
|
|
25
25
|
return None
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def read_text_utf8(path: Path) -> str:
|
|
29
|
+
"""Read a text file as UTF-8; return "" if missing or unreadable."""
|
|
30
|
+
try:
|
|
31
|
+
return path.read_text(encoding="utf-8")
|
|
32
|
+
except (OSError, UnicodeDecodeError):
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
|
|
28
36
|
def write_json_file(path: Path, data: dict) -> bool:
|
|
29
37
|
"""Write dict to JSON file atomically (temp + os.replace)."""
|
|
30
38
|
try:
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Anti-rationalization pattern detection for TDD exemptions."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
RATIONALIZATION_PATTERNS = (
|
|
7
|
+
"太简单",
|
|
8
|
+
"显而易",
|
|
9
|
+
"没必要",
|
|
10
|
+
"不需要测试",
|
|
11
|
+
"已经有其他测试",
|
|
12
|
+
"肉眼",
|
|
13
|
+
"self-evident",
|
|
14
|
+
"trivial",
|
|
15
|
+
"obvious",
|
|
16
|
+
"no need to test",
|
|
17
|
+
"covered elsewhere",
|
|
18
|
+
"already tested",
|
|
19
|
+
"just a",
|
|
20
|
+
"only a",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_exemption_rationalization(reason: str) -> list[dict]:
|
|
25
|
+
"""Detect anti-rationalization patterns in TDD exemption reasons.
|
|
26
|
+
|
|
27
|
+
Returns violation dicts with rule_id TDD-RATIONALIZE-001.
|
|
28
|
+
Severity is warn (does not block task start/review).
|
|
29
|
+
"""
|
|
30
|
+
violations: list[dict] = []
|
|
31
|
+
if not reason:
|
|
32
|
+
return violations
|
|
33
|
+
lower = reason.lower()
|
|
34
|
+
for pattern in RATIONALIZATION_PATTERNS:
|
|
35
|
+
if pattern.lower() in lower:
|
|
36
|
+
violations.append({
|
|
37
|
+
"rule_id": "TDD-RATIONALIZE-001",
|
|
38
|
+
"type": "tdd_evidence",
|
|
39
|
+
"severity": "warn",
|
|
40
|
+
"passed": False,
|
|
41
|
+
"message": (
|
|
42
|
+
f"TDD exemption reason contains rationalization pattern: "
|
|
43
|
+
f"'{pattern}'. Replace with a concrete behavior test or "
|
|
44
|
+
f"specific verification command."
|
|
45
|
+
),
|
|
46
|
+
"file": "<task>/tdd.jsonl",
|
|
47
|
+
"fix_hint": (
|
|
48
|
+
"Remove rationalization from exemption reason. "
|
|
49
|
+
"Either write a failing behavior test for this AC "
|
|
50
|
+
"or provide a verification command that exercises "
|
|
51
|
+
"the claimed unchanged behavior."
|
|
52
|
+
),
|
|
53
|
+
})
|
|
54
|
+
break
|
|
55
|
+
return violations
|
|
@@ -58,6 +58,11 @@ class GateRunner:
|
|
|
58
58
|
violations.extend(
|
|
59
59
|
validate_coding_standards(self.repo_root, normalized_task_dir)
|
|
60
60
|
)
|
|
61
|
+
from .validate_coding_standards import validate_complexity_signals
|
|
62
|
+
|
|
63
|
+
violations.extend(
|
|
64
|
+
validate_complexity_signals(self.repo_root, normalized_task_dir)
|
|
65
|
+
)
|
|
61
66
|
return GateResult.from_violations(scope, violations, normalized_task_dir)
|
|
62
67
|
|
|
63
68
|
def log(self, result: GateResult) -> None:
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Spec markdown section validator for cowork-flow."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
SECTION_CONFIG: dict[str, tuple[str, ...]] = {
|
|
10
|
+
"contract": ("Goal", "When to Use", "Rules", "Examples"),
|
|
11
|
+
"skill": ("Overview", "When to Use", "Steps", "Verification"),
|
|
12
|
+
"guide": ("Goal", "When to Use", "Process", "Anti-Rationalization"),
|
|
13
|
+
"default": ("Goal", "Rules"),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
HEADER_RE = re.compile(r"^##\s+(.+)$", re.MULTILINE)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_sections(file_path: Path | str, spec_type: str = "default") -> tuple[bool, list[str]]:
|
|
20
|
+
"""Validate a spec markdown file has all required sections.
|
|
21
|
+
|
|
22
|
+
Returns (is_valid, error_messages).
|
|
23
|
+
"""
|
|
24
|
+
path = Path(file_path)
|
|
25
|
+
errors: list[str] = []
|
|
26
|
+
|
|
27
|
+
if not path.is_file():
|
|
28
|
+
return False, [f"File not found: {path}"]
|
|
29
|
+
|
|
30
|
+
content = path.read_text(encoding="utf-8").strip()
|
|
31
|
+
if not content:
|
|
32
|
+
return False, ["File is empty"]
|
|
33
|
+
|
|
34
|
+
required = SECTION_CONFIG.get(spec_type)
|
|
35
|
+
if required is None:
|
|
36
|
+
return False, [f"Unknown spec type: {spec_type}"]
|
|
37
|
+
|
|
38
|
+
matches = list(HEADER_RE.finditer(content))
|
|
39
|
+
found: dict[str, str] = {}
|
|
40
|
+
for i, m in enumerate(matches):
|
|
41
|
+
name = m.group(1).strip()
|
|
42
|
+
start = m.end()
|
|
43
|
+
end = matches[i + 1].start() if i + 1 < len(matches) else len(content)
|
|
44
|
+
body = content[start:end].strip()
|
|
45
|
+
found[name] = body
|
|
46
|
+
|
|
47
|
+
for req in required:
|
|
48
|
+
if req not in found:
|
|
49
|
+
errors.append(f"Missing: {req}")
|
|
50
|
+
elif not found[req]:
|
|
51
|
+
errors.append(f"Empty: {req}")
|
|
52
|
+
|
|
53
|
+
return (not errors, errors)
|
|
@@ -161,11 +161,7 @@ def _task_requires_tdd(prd_text: str) -> bool:
|
|
|
161
161
|
return any(marker.lower() in lower for marker in TDD_REQUIRED_MARKERS)
|
|
162
162
|
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
try:
|
|
166
|
-
return path.read_text(encoding="utf-8")
|
|
167
|
-
except (OSError, UnicodeDecodeError):
|
|
168
|
-
return ""
|
|
164
|
+
from common.core.files import read_text_utf8 as _read_text
|
|
169
165
|
|
|
170
166
|
|
|
171
167
|
def _read_jsonl(path: Path) -> tuple[list[dict], list[dict]]:
|
|
@@ -343,6 +339,9 @@ def _validate_exemption(
|
|
|
343
339
|
)
|
|
344
340
|
)
|
|
345
341
|
|
|
342
|
+
from .anti_rationalization import validate_exemption_rationalization
|
|
343
|
+
violations.extend(validate_exemption_rationalization(str(entry.get("reason", ""))))
|
|
344
|
+
|
|
346
345
|
return violations
|
|
347
346
|
|
|
348
347
|
|
|
@@ -143,11 +143,7 @@ def _read_tdd_entries(path: Path) -> list[dict]:
|
|
|
143
143
|
return entries
|
|
144
144
|
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
try:
|
|
148
|
-
return path.read_text(encoding="utf-8")
|
|
149
|
-
except (OSError, UnicodeDecodeError):
|
|
150
|
-
return ""
|
|
146
|
+
from common.core.files import read_text_utf8 as _read_text
|
|
151
147
|
|
|
152
148
|
|
|
153
149
|
def _classify_test_content(content: str, test_name: str) -> str:
|