cowork-flow 2.2.0 → 2.4.0
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/CHANGELOG.md +33 -1
- package/README.en.md +46 -22
- package/README.md +10 -7
- package/docs/architecture.en.md +95 -0
- package/docs/architecture.md +7 -3
- package/docs/direction.en.md +71 -0
- package/docs/direction.md +6 -1
- package/docs/hosts.en.md +299 -0
- package/docs/hosts.md +5 -1
- package/docs/index.en.md +30 -0
- package/docs/index.md +2 -0
- package/docs/release.en.md +108 -0
- package/docs/release.md +2 -0
- package/package.json +2 -2
- package/presets/claude-code/.claude-plugin/plugin.json +1 -1
- package/presets/claude-code/skills/cowork-flow-bootstrap/SKILL.md +8 -2
- package/presets/codex/.codex-plugin/plugin.json +1 -1
- package/presets/codex/skills/cowork-flow-bootstrap/SKILL.md +8 -2
- package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
- package/presets/kimi-code/skills/cowork-flow-bootstrap/SKILL.md +8 -2
- package/presets/opencode/cowork-flow/skills/cowork-flow-bootstrap/SKILL.md +8 -2
- package/presets/qoder/.qoder-plugin/plugin.json +1 -1
- package/presets/qoder/skills/cowork-flow-bootstrap/SKILL.md +8 -2
- package/presets/zcode/.zcode-plugin/plugin.json +1 -1
- package/presets/zcode/skills/cowork-flow-bootstrap/SKILL.md +8 -2
- package/src/cli.js +1 -1
- package/src/commands/init.js +200 -16
- package/src/commands/registry.js +10 -7
- package/src/commands/source-refresh.js +5 -1
- package/src/commands/sync.js +4 -1
- package/src/lib/copy-template.js +23 -5
- package/src/lib/host-assets.js +119 -4
- package/src/lib/language.js +162 -0
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/config.yaml +8 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_create_command.py +5 -3
- package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +71 -2
- package/template/.cowork-flow/scripts/infra/config.py +85 -0
- package/template/.cowork-flow/scripts/services/decision_anchor_schema.py +104 -0
- package/template/.cowork-flow/scripts/services/fact_view.py +21 -15
- package/template/.cowork-flow/scripts/services/task_context.py +13 -7
- package/template/.cowork-flow/scripts/services/task_creation.py +72 -30
- package/template/.cowork-flow/scripts/services/task_gate_profiles.py +7 -24
- package/template/.cowork-flow/spec/contracts/context-injection.md +5 -4
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +21 -14
- package/template/.cowork-flow/spec/contracts/plan-binding.md +2 -1
- package/template/.cowork-flow/spec/index.md +26 -26
- package/template/.cowork-flow/spec/references/definition-of-done.md +26 -26
- package/template/.cowork-flow/spec/references/index.md +16 -16
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +71 -71
- package/template/.cowork-flow/spec/references/security-checklist.md +37 -37
- package/template/.cowork-flow/spec/references/testing-checklist.md +32 -32
- package/template/.cowork-flow/spec/runtime/host-assets.json +23 -4
- package/template/.cowork-flow/spec/runtime/index.md +10 -12
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +31 -0
- package/template/.cowork-flow/spec/schemas/index.md +5 -5
- package/template/.cowork-flow/templates/plan-template.md +91 -0
- package/template/locales/en-US/.cowork-flow/spec/contracts/decision-anchor.md +160 -0
- package/template/locales/en-US/.cowork-flow/templates/plan-template.md +91 -0
- package/template/skills/runtime-health/scripts/doctor.py +110 -0
- package/template/skills/task-planning/SKILL.md +19 -92
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared decision-anchor heading schema.
|
|
3
|
+
|
|
4
|
+
The runtime recognizes two heading sets for the same sections: the historical
|
|
5
|
+
Chinese set and the English aliases introduced for English-language task
|
|
6
|
+
artifacts. Recognition is language-independent so existing tasks and mixed
|
|
7
|
+
language projects keep working; the configured project language only decides
|
|
8
|
+
which set a newly generated anchor uses.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
GOAL = "goal"
|
|
16
|
+
ACCEPTANCE = "acceptance"
|
|
17
|
+
REJECTED = "rejected"
|
|
18
|
+
ASSUMPTIONS = "assumptions"
|
|
19
|
+
SCOPE = "scope"
|
|
20
|
+
VERIFICATION = "verification"
|
|
21
|
+
|
|
22
|
+
REQUIRED_SECTIONS = (GOAL, ACCEPTANCE)
|
|
23
|
+
|
|
24
|
+
DEFAULT_LANGUAGE = "zh-CN"
|
|
25
|
+
|
|
26
|
+
LANGUAGE_HEADINGS: dict[str, dict[str, str]] = {
|
|
27
|
+
"zh-CN": {
|
|
28
|
+
GOAL: "目标",
|
|
29
|
+
ACCEPTANCE: "验收标准",
|
|
30
|
+
REJECTED: "被拒方案",
|
|
31
|
+
ASSUMPTIONS: "关键假设",
|
|
32
|
+
SCOPE: "范围边界",
|
|
33
|
+
VERIFICATION: "验证命令",
|
|
34
|
+
},
|
|
35
|
+
"en-US": {
|
|
36
|
+
GOAL: "Goal",
|
|
37
|
+
ACCEPTANCE: "Acceptance Criteria",
|
|
38
|
+
REJECTED: "Rejected Options",
|
|
39
|
+
ASSUMPTIONS: "Key Assumptions",
|
|
40
|
+
SCOPE: "Scope Boundary",
|
|
41
|
+
VERIFICATION: "Verification Commands",
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
SECTION_ALIASES: dict[str, str] = {
|
|
46
|
+
heading: section
|
|
47
|
+
for headings in LANGUAGE_HEADINGS.values()
|
|
48
|
+
for section, heading in headings.items()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
HEADING_RE = re.compile(r"^##\s*(.+?)\s*$")
|
|
52
|
+
ACCEPTANCE_RE = re.compile(
|
|
53
|
+
r"^\s*-\s*(?:\[([ xX])\]?\s*)?(AC-[A-Za-z0-9-]+)\s*[::]\s*(.+?)\s*$"
|
|
54
|
+
)
|
|
55
|
+
LIST_ITEM_RE = re.compile(r"^\s*[-*]\s")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def canonical_section(heading: str) -> str | None:
|
|
59
|
+
"""Canonical section key for a recognized heading, or None."""
|
|
60
|
+
return SECTION_ALIASES.get(heading)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def headings_for(language: str) -> dict[str, str]:
|
|
64
|
+
"""Heading set for a language, falling back to the default language."""
|
|
65
|
+
return LANGUAGE_HEADINGS.get(language, LANGUAGE_HEADINGS[DEFAULT_LANGUAGE])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def heading_for(section: str, language: str) -> str:
|
|
69
|
+
"""One heading in the requested language, falling back to the default."""
|
|
70
|
+
return headings_for(language)[section]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def section_headings(section: str) -> tuple[str, ...]:
|
|
74
|
+
"""Every recognized heading for one section, default language first."""
|
|
75
|
+
return tuple(
|
|
76
|
+
LANGUAGE_HEADINGS[language][section] for language in LANGUAGE_HEADINGS
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def present_sections(text: str) -> set[str]:
|
|
81
|
+
"""Canonical keys of the recognized `##` sections present in the text."""
|
|
82
|
+
present: set[str] = set()
|
|
83
|
+
for raw in text.splitlines():
|
|
84
|
+
heading = HEADING_RE.match(raw.strip())
|
|
85
|
+
if not heading:
|
|
86
|
+
continue
|
|
87
|
+
section = canonical_section(heading.group(1))
|
|
88
|
+
if section is not None:
|
|
89
|
+
present.add(section)
|
|
90
|
+
return present
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def acceptance_section_lines(text: str) -> list[str]:
|
|
94
|
+
"""Raw lines under the acceptance heading, up to the next heading."""
|
|
95
|
+
lines: list[str] = []
|
|
96
|
+
inside = False
|
|
97
|
+
for raw in text.splitlines():
|
|
98
|
+
heading = HEADING_RE.match(raw.strip())
|
|
99
|
+
if heading:
|
|
100
|
+
inside = canonical_section(heading.group(1)) == ACCEPTANCE
|
|
101
|
+
continue
|
|
102
|
+
if inside:
|
|
103
|
+
lines.append(raw)
|
|
104
|
+
return lines
|
|
@@ -17,23 +17,30 @@ from datetime import datetime, timezone
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
|
+
from services.decision_anchor_schema import (
|
|
21
|
+
ACCEPTANCE,
|
|
22
|
+
ACCEPTANCE_RE,
|
|
23
|
+
GOAL,
|
|
24
|
+
HEADING_RE,
|
|
25
|
+
REJECTED,
|
|
26
|
+
SCOPE,
|
|
27
|
+
VERIFICATION,
|
|
28
|
+
canonical_section,
|
|
29
|
+
)
|
|
30
|
+
|
|
20
31
|
GOAL_TRUNCATE = 300
|
|
21
32
|
VERIFY_COMMAND_TRUNCATE = 120
|
|
22
33
|
VERIFY_COMMAND_MAX = 10
|
|
23
34
|
SCOPE_BOUNDARY_TRUNCATE = 160
|
|
24
|
-
ANCHOR_SECTIONS = ("目标", "验收标准", "被拒方案", "验证命令", "范围边界")
|
|
25
|
-
ACCEPTANCE_RE = re.compile(
|
|
26
|
-
r"^\s*-\s*(?:\[([ xX])\]?\s*)?(AC-[A-Za-z0-9-]+)\s*[::]\s*(.+?)\s*$"
|
|
27
|
-
)
|
|
28
35
|
REJECTED_RE = re.compile(r"^\s*-\s*\*\*(.+?)\*\*")
|
|
29
|
-
_HEADING_RE = re.compile(r"^##\s*(.+?)\s*$")
|
|
30
36
|
|
|
31
37
|
|
|
32
38
|
def parse_decision_anchor(text: str) -> dict[str, Any]:
|
|
33
39
|
"""Extract goal / acceptance criteria / rejected option names / declared
|
|
34
40
|
verification commands / scope boundary from a decision-anchor.md body.
|
|
35
|
-
Section
|
|
36
|
-
line-level and degrades to
|
|
41
|
+
Section headings may be the Chinese set or the English aliases (see
|
|
42
|
+
spec/contracts/decision-anchor.md); parsing is line-level and degrades to
|
|
43
|
+
empty values when sections are missing."""
|
|
37
44
|
goal_lines: list[str] = []
|
|
38
45
|
acceptance: list[dict[str, str]] = []
|
|
39
46
|
rejected: list[str] = []
|
|
@@ -41,15 +48,14 @@ def parse_decision_anchor(text: str) -> dict[str, Any]:
|
|
|
41
48
|
scope_boundary = ""
|
|
42
49
|
section: str | None = None
|
|
43
50
|
for raw in text.splitlines():
|
|
44
|
-
heading =
|
|
51
|
+
heading = HEADING_RE.match(raw.strip())
|
|
45
52
|
if heading:
|
|
46
|
-
|
|
47
|
-
section = name if name in ANCHOR_SECTIONS else None
|
|
53
|
+
section = canonical_section(heading.group(1))
|
|
48
54
|
continue
|
|
49
|
-
if section ==
|
|
55
|
+
if section == GOAL:
|
|
50
56
|
if raw.strip():
|
|
51
57
|
goal_lines.append(raw.strip())
|
|
52
|
-
elif section ==
|
|
58
|
+
elif section == ACCEPTANCE:
|
|
53
59
|
match = ACCEPTANCE_RE.match(raw)
|
|
54
60
|
if match:
|
|
55
61
|
acceptance.append(
|
|
@@ -59,17 +65,17 @@ def parse_decision_anchor(text: str) -> dict[str, Any]:
|
|
|
59
65
|
"checked": match.group(1) in ("x", "X"),
|
|
60
66
|
}
|
|
61
67
|
)
|
|
62
|
-
elif section ==
|
|
68
|
+
elif section == REJECTED:
|
|
63
69
|
match = REJECTED_RE.match(raw)
|
|
64
70
|
if match:
|
|
65
71
|
rejected.append(match.group(1).strip())
|
|
66
|
-
elif section ==
|
|
72
|
+
elif section == VERIFICATION:
|
|
67
73
|
command = raw.strip()
|
|
68
74
|
if command.startswith("- "):
|
|
69
75
|
command = command[2:].strip()
|
|
70
76
|
if command and len(verify_commands) < VERIFY_COMMAND_MAX:
|
|
71
77
|
verify_commands.append(command[:VERIFY_COMMAND_TRUNCATE])
|
|
72
|
-
elif section ==
|
|
78
|
+
elif section == SCOPE:
|
|
73
79
|
if raw.strip() and not scope_boundary:
|
|
74
80
|
scope_boundary = raw.strip()[:SCOPE_BOUNDARY_TRUNCATE]
|
|
75
81
|
return {
|
|
@@ -32,6 +32,11 @@ from services.context_paths import (
|
|
|
32
32
|
normalize_context_path,
|
|
33
33
|
prepare_context_entry,
|
|
34
34
|
)
|
|
35
|
+
from services.decision_anchor_schema import (
|
|
36
|
+
REQUIRED_SECTIONS,
|
|
37
|
+
present_sections,
|
|
38
|
+
section_headings,
|
|
39
|
+
)
|
|
35
40
|
from services.plan_binding import normalize_plan_file, plan_file_blockers
|
|
36
41
|
from services.task_gate_profiles import (
|
|
37
42
|
GATE_PROFILE_STRICT,
|
|
@@ -360,13 +365,14 @@ class TaskContextService:
|
|
|
360
365
|
|
|
361
366
|
@staticmethod
|
|
362
367
|
def _decision_anchor_section_blockers(anchor_text: str) -> list[str]:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
368
|
+
present = present_sections(anchor_text)
|
|
369
|
+
return [
|
|
370
|
+
"decision-anchor.md missing required section: "
|
|
371
|
+
+ " (or ".join(f"## {heading}" for heading in section_headings(section))
|
|
372
|
+
+ ")"
|
|
373
|
+
for section in REQUIRED_SECTIONS
|
|
374
|
+
if section not in present
|
|
375
|
+
]
|
|
370
376
|
|
|
371
377
|
@staticmethod
|
|
372
378
|
def _is_tiny_task(task_data: dict) -> bool:
|
|
@@ -6,7 +6,14 @@ from __future__ import annotations
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
import re
|
|
9
10
|
|
|
11
|
+
from infra.config import get_language
|
|
12
|
+
from services.decision_anchor_schema import (
|
|
13
|
+
ACCEPTANCE,
|
|
14
|
+
GOAL,
|
|
15
|
+
heading_for,
|
|
16
|
+
)
|
|
10
17
|
from services.task_tree import TaskTreeError, TaskTreeService
|
|
11
18
|
from services import task_graph
|
|
12
19
|
from infra.paths import (
|
|
@@ -19,6 +26,20 @@ from services.task_gate_profiles import GATE_PROFILE_STRICT
|
|
|
19
26
|
from services.task_repository import TaskRepository, TaskRepositoryError
|
|
20
27
|
|
|
21
28
|
|
|
29
|
+
_GOAL_LINE_RE = re.compile(r"^\*\*(?:Goal|目标):\*\*\s*(.+?)\s*$")
|
|
30
|
+
_GOAL_BOLD_COLON_RE = re.compile(r"^\*\*(?:Goal|目标)\*\*:\s*(.+?)\s*$")
|
|
31
|
+
_GOAL_TABLE_RE = re.compile(
|
|
32
|
+
r"^\|\s*\*\*(?:Goal|目标)\*\*\s*\|\s*(.+?)\s*\|\s*$"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
_ANCHOR_PLACEHOLDERS = {
|
|
36
|
+
("zh-CN", GOAL): "<!-- 用一句话描述可观察的目标。 -->",
|
|
37
|
+
("zh-CN", ACCEPTANCE): "<!-- 至少一条,例如:- [ ] AC-001: ... -->",
|
|
38
|
+
("en-US", GOAL): "<!-- One sentence describing the observable outcome. -->",
|
|
39
|
+
("en-US", ACCEPTANCE): "<!-- At least one, e.g. - [ ] AC-001: ... -->",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
22
43
|
class TaskCreationError(RuntimeError):
|
|
23
44
|
"""Raised when task creation cannot be completed."""
|
|
24
45
|
|
|
@@ -66,6 +87,25 @@ class TaskCreationResult:
|
|
|
66
87
|
linked_parent: str | None
|
|
67
88
|
missing_parent: str | None
|
|
68
89
|
generated_anchor: bool
|
|
90
|
+
generated_anchor_from_plan: bool
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _extract_plan_goal(plan_text: str) -> str | None:
|
|
94
|
+
"""First machine-readable goal line, then the legacy table row."""
|
|
95
|
+
for raw in plan_text.splitlines():
|
|
96
|
+
line = raw.strip()
|
|
97
|
+
for pattern in (_GOAL_LINE_RE, _GOAL_BOLD_COLON_RE, _GOAL_TABLE_RE):
|
|
98
|
+
match = pattern.match(line)
|
|
99
|
+
if match:
|
|
100
|
+
return match.group(1).strip()
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _anchor_placeholder(section: str, language: str) -> str:
|
|
105
|
+
return _ANCHOR_PLACEHOLDERS.get(
|
|
106
|
+
(language, section),
|
|
107
|
+
_ANCHOR_PLACEHOLDERS[("zh-CN", section)],
|
|
108
|
+
)
|
|
69
109
|
|
|
70
110
|
|
|
71
111
|
def ensure_tasks_dir(repo_root: Path) -> Path:
|
|
@@ -122,7 +162,7 @@ class TaskCreationService:
|
|
|
122
162
|
created_files.append(task_dir / FILE_TASK_JSON)
|
|
123
163
|
|
|
124
164
|
linked_parent, missing_parent = self._link_parent(request, task_dir)
|
|
125
|
-
generated_anchor = self._generate_anchor(
|
|
165
|
+
generated_anchor, generated_anchor_from_plan = self._generate_anchor(
|
|
126
166
|
task_dir,
|
|
127
167
|
bound_plan_path,
|
|
128
168
|
)
|
|
@@ -136,6 +176,7 @@ class TaskCreationService:
|
|
|
136
176
|
linked_parent=linked_parent,
|
|
137
177
|
missing_parent=missing_parent,
|
|
138
178
|
generated_anchor=generated_anchor,
|
|
179
|
+
generated_anchor_from_plan=generated_anchor_from_plan,
|
|
139
180
|
)
|
|
140
181
|
|
|
141
182
|
def _task_data(
|
|
@@ -316,41 +357,42 @@ class TaskCreationService:
|
|
|
316
357
|
"plan file must live under .cowork-flow/plans",
|
|
317
358
|
)
|
|
318
359
|
|
|
319
|
-
@staticmethod
|
|
320
360
|
def _generate_anchor(
|
|
361
|
+
self,
|
|
321
362
|
task_dir: Path,
|
|
322
363
|
plan_path: Path | None,
|
|
323
|
-
) -> bool:
|
|
324
|
-
|
|
325
|
-
return False
|
|
326
|
-
if not plan_path.is_file():
|
|
327
|
-
return False
|
|
364
|
+
) -> tuple[bool, bool]:
|
|
365
|
+
"""Create a language-specific anchor scaffold.
|
|
328
366
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
"TASK-CREATE-PLAN-001",
|
|
334
|
-
plan_path,
|
|
335
|
-
"plan cannot be read as UTF-8",
|
|
336
|
-
) from error
|
|
337
|
-
|
|
338
|
-
goal = next(
|
|
339
|
-
(
|
|
340
|
-
line.split(":", 1)[1].strip()
|
|
341
|
-
for line in plan_text.splitlines()
|
|
342
|
-
if line.startswith("**Goal:**")
|
|
343
|
-
or line.startswith("**目标:**")
|
|
344
|
-
),
|
|
345
|
-
None,
|
|
346
|
-
)
|
|
367
|
+
A bound plan's goal line (or legacy table row) fills the goal section;
|
|
368
|
+
otherwise the scaffold carries a localized placeholder. An anchor that
|
|
369
|
+
already exists is never overwritten.
|
|
370
|
+
"""
|
|
347
371
|
anchor_file = task_dir / "decision-anchor.md"
|
|
348
|
-
if
|
|
349
|
-
return False
|
|
372
|
+
if anchor_file.exists():
|
|
373
|
+
return False, False
|
|
374
|
+
|
|
375
|
+
goal: str | None = None
|
|
376
|
+
if plan_path is not None and plan_path.is_file():
|
|
377
|
+
try:
|
|
378
|
+
plan_text = plan_path.read_text(encoding="utf-8")
|
|
379
|
+
except (OSError, UnicodeDecodeError) as error:
|
|
380
|
+
raise TaskCreationError(
|
|
381
|
+
"TASK-CREATE-PLAN-001",
|
|
382
|
+
plan_path,
|
|
383
|
+
"plan cannot be read as UTF-8",
|
|
384
|
+
) from error
|
|
385
|
+
goal = _extract_plan_goal(plan_text)
|
|
386
|
+
|
|
387
|
+
language = get_language(self.repo_root)
|
|
388
|
+
goal_heading = heading_for(GOAL, language)
|
|
389
|
+
acceptance_heading = heading_for(ACCEPTANCE, language)
|
|
390
|
+
goal_body = goal or _anchor_placeholder(GOAL, language)
|
|
391
|
+
acceptance_body = _anchor_placeholder(ACCEPTANCE, language)
|
|
350
392
|
try:
|
|
351
393
|
anchor_file.write_text(
|
|
352
|
-
f"##
|
|
353
|
-
"
|
|
394
|
+
f"## {goal_heading}\n\n{goal_body}\n\n"
|
|
395
|
+
f"## {acceptance_heading}\n\n{acceptance_body}\n",
|
|
354
396
|
encoding="utf-8",
|
|
355
397
|
)
|
|
356
398
|
except OSError as error:
|
|
@@ -363,7 +405,7 @@ class TaskCreationService:
|
|
|
363
405
|
anchor_file,
|
|
364
406
|
"decision anchor could not be written",
|
|
365
407
|
) from error
|
|
366
|
-
return True
|
|
408
|
+
return True, goal is not None
|
|
367
409
|
|
|
368
410
|
|
|
369
411
|
def _validate_dev_type(dev_type: str | None, path: Path) -> None:
|
|
@@ -4,7 +4,7 @@ A task created today carries `meta.gateProfile = "strict"`; a task without the
|
|
|
4
4
|
marker keeps the historical gate (legacy), so nothing that already exists is
|
|
5
5
|
retroactively re-gated.
|
|
6
6
|
|
|
7
|
-
The strict gate exists because the old one only checked that the
|
|
7
|
+
The strict gate exists because the old one only checked that the acceptance
|
|
8
8
|
heading was present. An anchor with an empty acceptance section passed, the
|
|
9
9
|
acceptance/evidence gates then found no AC rows to check, and the whole chain
|
|
10
10
|
silently did nothing.
|
|
@@ -12,20 +12,17 @@ silently did nothing.
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
from services.decision_anchor_schema import (
|
|
16
|
+
ACCEPTANCE_RE,
|
|
17
|
+
LIST_ITEM_RE,
|
|
18
|
+
acceptance_section_lines,
|
|
19
|
+
)
|
|
18
20
|
|
|
19
21
|
GATE_PROFILE_STRICT = "strict"
|
|
20
22
|
GATE_PROFILE_LEGACY = "legacy"
|
|
21
23
|
|
|
22
24
|
LIGHT_TASK_TYPE = "tiny"
|
|
23
25
|
|
|
24
|
-
ACCEPTANCE_HEADING = "验收标准"
|
|
25
|
-
|
|
26
|
-
_HEADING_RE = re.compile(r"^##\s*(.+?)\s*$")
|
|
27
|
-
_LIST_ITEM_RE = re.compile(r"^\s*[-*]\s")
|
|
28
|
-
|
|
29
26
|
|
|
30
27
|
def resolve_gate_profile(task_data: dict) -> str:
|
|
31
28
|
"""The gate a task is held to; an unmarked task is legacy."""
|
|
@@ -72,20 +69,6 @@ def is_light_task(task_data: dict) -> bool:
|
|
|
72
69
|
return declares_light_profile(task_data)
|
|
73
70
|
|
|
74
71
|
|
|
75
|
-
def acceptance_section_lines(anchor_text: str) -> list[str]:
|
|
76
|
-
"""Raw lines under `## 验收标准`, up to the next heading."""
|
|
77
|
-
lines: list[str] = []
|
|
78
|
-
inside = False
|
|
79
|
-
for raw in anchor_text.splitlines():
|
|
80
|
-
heading = _HEADING_RE.match(raw.strip())
|
|
81
|
-
if heading:
|
|
82
|
-
inside = heading.group(1) == ACCEPTANCE_HEADING
|
|
83
|
-
continue
|
|
84
|
-
if inside:
|
|
85
|
-
lines.append(raw)
|
|
86
|
-
return lines
|
|
87
|
-
|
|
88
|
-
|
|
89
72
|
def acceptance_row_blockers(anchor_text: str) -> list[str]:
|
|
90
73
|
"""Why the acceptance section cannot start a strict task.
|
|
91
74
|
|
|
@@ -101,7 +84,7 @@ def acceptance_row_blockers(anchor_text: str) -> list[str]:
|
|
|
101
84
|
if match:
|
|
102
85
|
ids.append(match.group(2))
|
|
103
86
|
continue
|
|
104
|
-
if
|
|
87
|
+
if LIST_ITEM_RE.match(raw):
|
|
105
88
|
malformed.append(raw.strip()[:80])
|
|
106
89
|
blockers: list[str] = []
|
|
107
90
|
if not ids:
|
|
@@ -289,7 +289,8 @@ Verify: npm run test:fast; python3 -m pytest tests/ -q
|
|
|
289
289
|
- **Gates**: static preview text, identical on all hosts; the delegated
|
|
290
290
|
variant replaces the self-declaration sentence with "scope is inherited
|
|
291
291
|
from the parent task (read-only reference)".
|
|
292
|
-
- **Verify**: `## 验证命令` lines from
|
|
292
|
+
- **Verify**: `## Verification Commands`/`## 验证命令` lines from
|
|
293
|
+
decision-anchor (first 10, 120 chars
|
|
293
294
|
each) — the agent's own declared self-checks; omitted when none.
|
|
294
295
|
- **Per-edit warning**: PostToolUse on an edit tool injects at most one
|
|
295
296
|
line when the edited file is outside the task's declared scope —
|
|
@@ -326,9 +327,9 @@ Verify: npm run test:fast; python3 -m pytest tests/ -q
|
|
|
326
327
|
budget/limits come from `.cowork-flow/spec/runtime/scope-rules.json`, the
|
|
327
328
|
single source consumed at runtime; a missing or malformed file degrades to
|
|
328
329
|
embedded defaults that are byte-identical with the shipped file (locked by
|
|
329
|
-
`tests/test_scope_rules.py`). The file is
|
|
330
|
-
|
|
331
|
-
it do not survive a sync.
|
|
330
|
+
`tests/test_scope_rules.py`). The file is framework-managed: `cwf project
|
|
331
|
+
sync` refreshes it as part of the synced `spec/runtime/` tree, so local edits
|
|
332
|
+
to it do not survive a sync.
|
|
332
333
|
- Absent for `no_task` / `planning` / `completed` states. Block content is
|
|
333
334
|
byte-identical across host lines for the whole fixture matrix
|
|
334
335
|
(`test/fixtures/stage-contract-matrix.json` drives the equality test:
|
|
@@ -8,8 +8,9 @@ the alternatives", which compresses the number of task artifacts.
|
|
|
8
8
|
|
|
9
9
|
## Schema
|
|
10
10
|
|
|
11
|
-
These section names are
|
|
12
|
-
|
|
11
|
+
These section names are machine-readable. The runtime parser accepts both the
|
|
12
|
+
Chinese and English heading sets, and new anchors use the language configured in
|
|
13
|
+
`.cowork-flow/config.yaml` (`zh-CN` is the default).
|
|
13
14
|
|
|
14
15
|
```markdown
|
|
15
16
|
# Decision Anchor
|
|
@@ -38,14 +39,17 @@ translated heading would simply stop being read.
|
|
|
38
39
|
|
|
39
40
|
## Section requirements
|
|
40
41
|
|
|
42
|
+
Both heading columns below are accepted; the left column is the `zh-CN` output
|
|
43
|
+
and the right column the `en-US` output.
|
|
44
|
+
|
|
41
45
|
| Section | Required | Why |
|
|
42
46
|
|------|------|------|
|
|
43
|
-
| `## 目标` | required | no goal, no acceptance baseline |
|
|
44
|
-
| `## 验收标准` | required | `task_start` is blocked without acceptance criteria |
|
|
45
|
-
| `## 被拒方案` | optional | required for brainstorming tasks, so a rejected option is not reconsidered during execution |
|
|
46
|
-
| `## 关键假设` | optional | filled in for complex tasks to surface implicit constraints; not read by the runtime parser |
|
|
47
|
-
| `## 范围边界` | optional | prevents scope creep |
|
|
48
|
-
| `## 验证命令` | optional | filled in for complex tasks, as the basis for automated verification; when the changed files are covered by `.cowork-flow/spec/backend/` or `frontend/` rules, it must also carry the spec/gate check for the task's `dev_type` (deciding command, or clause-by-clause `pass` / `not_applicable` with reason) |
|
|
47
|
+
| `## 目标` / `## Goal` | required | no goal, no acceptance baseline |
|
|
48
|
+
| `## 验收标准` / `## Acceptance Criteria` | required | `task_start` is blocked without acceptance criteria |
|
|
49
|
+
| `## 被拒方案` / `## Rejected Options` | optional | required for brainstorming tasks, so a rejected option is not reconsidered during execution |
|
|
50
|
+
| `## 关键假设` / `## Key Assumptions` | optional | filled in for complex tasks to surface implicit constraints; not read by the runtime parser |
|
|
51
|
+
| `## 范围边界` / `## Scope Boundary` | optional | prevents scope creep |
|
|
52
|
+
| `## 验证命令` / `## Verification Commands` | optional | filled in for complex tasks, as the basis for automated verification; when the changed files are covered by `.cowork-flow/spec/backend/` or `frontend/` rules, it must also carry the spec/gate check for the task's `dev_type` (deciding command, or clause-by-clause `pass` / `not_applicable` with reason) |
|
|
49
53
|
|
|
50
54
|
### Machine-readable line formats (for injection)
|
|
51
55
|
|
|
@@ -84,15 +88,17 @@ After merging (5 files): `decision-anchor.md` + `implement.jsonl` + `check.jsonl
|
|
|
84
88
|
## Lifecycle check coupling
|
|
85
89
|
|
|
86
90
|
- The preflight for `task next <dir> --run` checks that `decision-anchor.md` is
|
|
87
|
-
non-empty and requires the `## 目标`
|
|
91
|
+
non-empty and requires the `## 目标` / `## Goal` and `## 验收标准` /
|
|
92
|
+
`## Acceptance Criteria` sections to be present.
|
|
88
93
|
- **Gate profile `meta.gateProfile`**: a newly created task is written as
|
|
89
94
|
`"strict"`; an unmarked task is treated as `"legacy"` — existing tasks are not
|
|
90
95
|
retro-fitted with the stricter gate, and the profile is queryable through the
|
|
91
96
|
fact view. On top of the section check, a strict task additionally requires: at
|
|
92
|
-
least one parseable `- [ ] AC-xxx: ...` row in `##
|
|
93
|
-
no non-AC list rows in that
|
|
94
|
-
`task specs` dev-type dispatch
|
|
95
|
-
|
|
97
|
+
least one parseable `- [ ] AC-xxx: ...` row in `## 验收标准` /
|
|
98
|
+
`## Acceptance Criteria`, no duplicate AC ids, no non-AC list rows in that
|
|
99
|
+
section, and a declared `dev_type` (otherwise `task specs` dev-type dispatch
|
|
100
|
+
silently comes back shorter).
|
|
101
|
+
- An acceptance row's checkbox state (`- [ ]` / `- [x]`) and the evidence records
|
|
96
102
|
(the task directory's `evidence.jsonl`) are the fact source of the completion
|
|
97
103
|
gate: completion is blocked when an unchecked AC exists (`LIFECYCLE-AC-001`), a
|
|
98
104
|
declared AC has no evidence (`LIFECYCLE-AC-002`), a strict task cannot read any
|
|
@@ -149,4 +155,5 @@ source; if a task directory has only `prd.md` and no `decision-anchor.md`,
|
|
|
149
155
|
`decision-anchor.md is missing or empty`.
|
|
150
156
|
|
|
151
157
|
An existing legacy task needs a human to convert the `prd.md` content into
|
|
152
|
-
`decision-anchor.md`, fill in `## 目标`
|
|
158
|
+
`decision-anchor.md`, fill in `## 目标` / `## Goal` and `## 验收标准` /
|
|
159
|
+
`## Acceptance Criteria`, and only then start.
|
|
@@ -48,7 +48,8 @@ into `in_progress`:
|
|
|
48
48
|
- `task.json.meta.planFile` exists.
|
|
49
49
|
- `planFile` is a repo-relative `.cowork-flow/plans/` path.
|
|
50
50
|
- The plan file exists and is non-empty.
|
|
51
|
-
- `decision-anchor.md` contains `## 目标` and
|
|
51
|
+
- `decision-anchor.md` contains `## Goal`/`## 目标` and
|
|
52
|
+
`## Acceptance Criteria`/`## 验收标准`.
|
|
52
53
|
- `implement.jsonl` passes the existing context validation.
|
|
53
54
|
|
|
54
55
|
A Tiny task may have no planFile, but must still satisfy the existing anchor and
|
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
# Cowork Flow Spec Index
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This directory holds project specs, runtime contracts, and runtime configuration schemas. Layers by responsibility:
|
|
4
4
|
|
|
5
|
-
|
|
|
5
|
+
| Directory | Purpose |
|
|
6
6
|
| --- | --- |
|
|
7
|
-
| `contracts/` |
|
|
8
|
-
| `runtime/` | runtime
|
|
9
|
-
| `schemas/` | runtime
|
|
10
|
-
| `backend/` |
|
|
11
|
-
| `frontend/` |
|
|
12
|
-
| `guides/` | brainstorming/task-planning
|
|
13
|
-
| `references/` |
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`
|
|
19
|
-
`task-review` skill
|
|
20
|
-
`guides/`
|
|
7
|
+
| `contracts/` | Human-readable contracts for the workflow, host adapters, and subagent protocol. |
|
|
8
|
+
| `runtime/` | Machine configuration read by the runtime, including the contract registry and host asset manifest. |
|
|
9
|
+
| `schemas/` | JSON Schemas for runtime configuration and host contracts. |
|
|
10
|
+
| `backend/` | Backend implementation specs; the review skill reads them on demand and checks each clause. |
|
|
11
|
+
| `frontend/` | Frontend implementation specs; the review skill reads them on demand and checks each clause. |
|
|
12
|
+
| `guides/` | Pre-implementation thinking, cross-layer design, and reuse guidance for brainstorming/task-planning. |
|
|
13
|
+
| `references/` | On-demand reference material (DoD, testing checklist, security checklist, orchestration patterns); never auto-loaded. Load conditions are in the `references` array of contract-registry.json. |
|
|
14
|
+
|
|
15
|
+
Sync ownership: `contracts/`, `runtime/`, `schemas/`, and this file are framework-maintained and follow `cwf project sync`; `backend/`, `frontend/`, `guides/`, and `references/` are project-maintained, and `sync` never overwrites or deletes files there.
|
|
16
|
+
|
|
17
|
+
The workflow kernel only owns machine-checkable facts: task state, file scope, runtime context, and archive consistency.
|
|
18
|
+
Actions, stage guidance, and dedicated runtimes are declared by each Skill's `manifest.json`; there is no centralized Skill registry.
|
|
19
|
+
Natural-language specs under `backend/` and `frontend/` are not registered as runtime gates; the `task-review` skill reads them according to the current diff and task scope and checks each clause.
|
|
20
|
+
`guides/` is pre-implementation clarification and planning reference; selected conclusions belong in `decision-anchor.md` or the task plan.
|
|
21
21
|
|
|
22
22
|
## references/
|
|
23
23
|
|
|
24
|
-
>
|
|
24
|
+
> On-demand reference material. **Never auto-loaded in the main session** — read only when a skill entry or agent judges it necessary.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
1.
|
|
28
|
-
2.
|
|
29
|
-
3.
|
|
26
|
+
Reference files:
|
|
27
|
+
1. Provide supplemental context for skill entries (the main skill is the entry point; references supplement it).
|
|
28
|
+
2. Reduce what each session must load (narrow-domain specs such as `spec/backend/` are read only when needed).
|
|
29
|
+
3. Provide quick checklists during planning, check, or review.
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
- contract-registry
|
|
33
|
-
- skill
|
|
34
|
-
-
|
|
35
|
-
-
|
|
31
|
+
Loading rules:
|
|
32
|
+
- Read when one of the `loadWhen` conditions in contract-registry matches.
|
|
33
|
+
- Read when a skill body explicitly references a reference ID.
|
|
34
|
+
- Read when the user requests a specific check in the main session.
|
|
35
|
+
- **Never auto-loaded** — that is the difference from `contracts/`.
|