cowork-flow 0.0.39 → 0.0.40
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/application/task_archive.py +148 -21
- package/template/.cowork-flow/scripts/application/task_lifecycle.py +3 -1
- package/template/.cowork-flow/scripts/commands/add_session.py +137 -124
- package/template/.cowork-flow/scripts/commands/task.py +1 -0
- package/template/.cowork-flow/scripts/commands/task_archive_commands.py +135 -49
- package/template/.cowork-flow/scripts/commands/task_navigation.py +145 -81
- package/template/.cowork-flow/scripts/commands/task_parser.py +1 -0
- package/template/.cowork-flow/scripts/common/core/paths.py +4 -1
- package/template/.cowork-flow/scripts/common/gates/gates.py +114 -108
- package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +50 -28
- package/template/.cowork-flow/scripts/common/git/git_snapshot.py +14 -0
- package/template/.cowork-flow/spec/references/definition-of-done.md +1 -1
- package/template/.cowork-flow/workflow.md +6 -2
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/scaffold/.cowork-flow/workflow.md +6 -2
|
@@ -28,6 +28,7 @@ STAGE_GATES = {
|
|
|
28
28
|
"complexity",
|
|
29
29
|
),
|
|
30
30
|
"task_complete": (
|
|
31
|
+
"implementation",
|
|
31
32
|
"tdd_evidence",
|
|
32
33
|
"test_intent",
|
|
33
34
|
"runtime_rules",
|
|
@@ -37,9 +38,8 @@ STAGE_GATES = {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
for binding in (
|
|
41
|
+
def _validator_bindings() -> tuple[ValidatorBinding, ...]:
|
|
42
|
+
return (
|
|
43
43
|
ValidatorBinding(
|
|
44
44
|
key="runtime_rules",
|
|
45
45
|
module="validate_rules",
|
|
@@ -79,10 +79,11 @@ def build_default_registry() -> GateRegistry:
|
|
|
79
79
|
function="validate_complexity_signals",
|
|
80
80
|
positional=("repo_root", "task_dir"),
|
|
81
81
|
),
|
|
82
|
-
)
|
|
83
|
-
|
|
82
|
+
)
|
|
83
|
+
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
def _gate_definitions() -> tuple[GateDefinition, ...]:
|
|
86
|
+
return (
|
|
86
87
|
GateDefinition(
|
|
87
88
|
id="runtime_rules",
|
|
88
89
|
validator_key="runtime_rules",
|
|
@@ -94,7 +95,7 @@ def build_default_registry() -> GateRegistry:
|
|
|
94
95
|
id="implementation",
|
|
95
96
|
validator_key="implementation",
|
|
96
97
|
required=True,
|
|
97
|
-
block_message="Implementation gate blocked
|
|
98
|
+
block_message="Implementation gate blocked lifecycle transition",
|
|
98
99
|
warning_message="Implementation violations detected",
|
|
99
100
|
),
|
|
100
101
|
GateDefinition(
|
|
@@ -124,7 +125,13 @@ def build_default_registry() -> GateRegistry:
|
|
|
124
125
|
warning_message="Complexity review warnings",
|
|
125
126
|
),
|
|
126
127
|
)
|
|
127
|
-
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def build_default_registry() -> GateRegistry:
|
|
131
|
+
registry = GateRegistry()
|
|
132
|
+
for binding in _validator_bindings():
|
|
133
|
+
registry.register_validator(binding)
|
|
134
|
+
for definition in _gate_definitions():
|
|
128
135
|
registry.register_gate(definition)
|
|
129
136
|
return registry
|
|
130
137
|
|
|
@@ -151,61 +158,57 @@ def _pipeline_violation(
|
|
|
151
158
|
).to_dict()
|
|
152
159
|
|
|
153
160
|
|
|
161
|
+
def _normalize_violation(
|
|
162
|
+
raw_violation: object,
|
|
163
|
+
context: GateContext,
|
|
164
|
+
definition: GateDefinition,
|
|
165
|
+
) -> dict:
|
|
166
|
+
if not isinstance(raw_violation, Mapping):
|
|
167
|
+
return _pipeline_violation(
|
|
168
|
+
rule_id="GATE-PROTOCOL-001",
|
|
169
|
+
context=context,
|
|
170
|
+
definition=definition,
|
|
171
|
+
message=f"Gate {definition.id} returned a non-object violation",
|
|
172
|
+
fix_hint="Return violation dictionaries with stable metadata.",
|
|
173
|
+
)
|
|
174
|
+
try:
|
|
175
|
+
return Violation.from_mapping(
|
|
176
|
+
raw_violation,
|
|
177
|
+
scope=context.scope,
|
|
178
|
+
gate_id=definition.id,
|
|
179
|
+
).to_dict()
|
|
180
|
+
except ValueError as error:
|
|
181
|
+
return _pipeline_violation(
|
|
182
|
+
rule_id="GATE-PROTOCOL-001",
|
|
183
|
+
context=context,
|
|
184
|
+
definition=definition,
|
|
185
|
+
message=f"Gate {definition.id} returned invalid metadata: {error}",
|
|
186
|
+
fix_hint="Return violations with rule_id, severity, and message.",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
154
190
|
def _normalize_violations(
|
|
155
191
|
raw_result: object,
|
|
156
192
|
context: GateContext,
|
|
157
193
|
definition: GateDefinition,
|
|
158
194
|
) -> list[dict]:
|
|
159
|
-
if
|
|
195
|
+
if isinstance(raw_result, list):
|
|
160
196
|
return [
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
context=context,
|
|
164
|
-
definition=definition,
|
|
165
|
-
message=(
|
|
166
|
-
f"Gate {definition.id} returned "
|
|
167
|
-
f"{type(raw_result).__name__}; expected a list of violations"
|
|
168
|
-
),
|
|
169
|
-
fix_hint="Return list[dict] from the registered gate validator.",
|
|
170
|
-
)
|
|
197
|
+
_normalize_violation(item, context, definition)
|
|
198
|
+
for item in raw_result
|
|
171
199
|
]
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
fix_hint="Return violation dictionaries with stable metadata.",
|
|
185
|
-
)
|
|
186
|
-
)
|
|
187
|
-
continue
|
|
188
|
-
try:
|
|
189
|
-
normalized.append(
|
|
190
|
-
Violation.from_mapping(
|
|
191
|
-
raw_violation,
|
|
192
|
-
scope=context.scope,
|
|
193
|
-
gate_id=definition.id,
|
|
194
|
-
).to_dict()
|
|
195
|
-
)
|
|
196
|
-
except ValueError as error:
|
|
197
|
-
normalized.append(
|
|
198
|
-
_pipeline_violation(
|
|
199
|
-
rule_id="GATE-PROTOCOL-001",
|
|
200
|
-
context=context,
|
|
201
|
-
definition=definition,
|
|
202
|
-
message=f"Gate {definition.id} returned invalid metadata: {error}",
|
|
203
|
-
fix_hint=(
|
|
204
|
-
"Return violations with rule_id, severity, and message."
|
|
205
|
-
),
|
|
206
|
-
)
|
|
207
|
-
)
|
|
208
|
-
return normalized
|
|
200
|
+
return [
|
|
201
|
+
_pipeline_violation(
|
|
202
|
+
rule_id="GATE-PROTOCOL-001",
|
|
203
|
+
context=context,
|
|
204
|
+
definition=definition,
|
|
205
|
+
message=(
|
|
206
|
+
f"Gate {definition.id} returned "
|
|
207
|
+
f"{type(raw_result).__name__}; expected a list of violations"
|
|
208
|
+
),
|
|
209
|
+
fix_hint="Return list[dict] from the registered gate validator.",
|
|
210
|
+
)
|
|
211
|
+
]
|
|
209
212
|
|
|
210
213
|
|
|
211
214
|
class GatePipeline:
|
|
@@ -214,62 +217,65 @@ class GatePipeline:
|
|
|
214
217
|
def __init__(self, registry: GateRegistry | None = None) -> None:
|
|
215
218
|
self.registry = registry or build_default_registry()
|
|
216
219
|
|
|
217
|
-
def
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
fix_hint=(
|
|
234
|
-
"Restore the required validator module and function."
|
|
235
|
-
),
|
|
236
|
-
)
|
|
237
|
-
]
|
|
238
|
-
except Exception as error:
|
|
239
|
-
violations = [
|
|
240
|
-
_pipeline_violation(
|
|
241
|
-
rule_id="GATE-EXEC-001",
|
|
242
|
-
context=context,
|
|
243
|
-
definition=definition,
|
|
244
|
-
message=f"Gate {definition.id} failed during execution: {error}",
|
|
245
|
-
fix_hint=(
|
|
246
|
-
"Fix the validator exception; lifecycle gates fail closed."
|
|
247
|
-
),
|
|
248
|
-
)
|
|
249
|
-
]
|
|
250
|
-
else:
|
|
251
|
-
violations = _normalize_violations(
|
|
252
|
-
raw_result,
|
|
253
|
-
context,
|
|
254
|
-
definition,
|
|
220
|
+
def _execute_gate(
|
|
221
|
+
self,
|
|
222
|
+
context: GateContext,
|
|
223
|
+
gate_id: str,
|
|
224
|
+
) -> GateExecution:
|
|
225
|
+
definition = self.registry.gate(gate_id)
|
|
226
|
+
try:
|
|
227
|
+
raw_result = self.registry.invoke(definition, context)
|
|
228
|
+
except GateLoadError as error:
|
|
229
|
+
violations = [
|
|
230
|
+
_pipeline_violation(
|
|
231
|
+
rule_id="GATE-LOAD-001",
|
|
232
|
+
context=context,
|
|
233
|
+
definition=definition,
|
|
234
|
+
message=f"Gate {definition.id} could not load: {error}",
|
|
235
|
+
fix_hint="Restore the required validator module and function.",
|
|
255
236
|
)
|
|
256
|
-
|
|
257
|
-
|
|
237
|
+
]
|
|
238
|
+
except Exception as error:
|
|
239
|
+
violations = [
|
|
240
|
+
_pipeline_violation(
|
|
241
|
+
rule_id="GATE-EXEC-001",
|
|
242
|
+
context=context,
|
|
243
|
+
definition=definition,
|
|
244
|
+
message=f"Gate {definition.id} failed during execution: {error}",
|
|
245
|
+
fix_hint="Fix the validator exception; lifecycle gates fail closed.",
|
|
246
|
+
)
|
|
247
|
+
]
|
|
248
|
+
else:
|
|
249
|
+
violations = _normalize_violations(raw_result, context, definition)
|
|
250
|
+
return GateExecution(
|
|
251
|
+
definition=definition,
|
|
252
|
+
result=GateResult.from_violations(
|
|
258
253
|
context.scope,
|
|
259
254
|
violations,
|
|
260
255
|
context.task_dir,
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def run(self, context: GateContext) -> GateResult:
|
|
260
|
+
executions = [
|
|
261
|
+
self._execute_gate(context, gate_id)
|
|
262
|
+
for gate_id in STAGE_GATES[context.scope]
|
|
263
|
+
]
|
|
264
|
+
violations = [
|
|
265
|
+
violation
|
|
266
|
+
for execution in executions
|
|
267
|
+
for violation in execution.result.violations
|
|
268
|
+
]
|
|
269
|
+
priority_violations = [
|
|
270
|
+
violation
|
|
271
|
+
for violation in violations
|
|
272
|
+
if str(violation.get("rule_id", "")).startswith("GATE-")
|
|
273
|
+
]
|
|
274
|
+
ordinary_violations = [
|
|
275
|
+
violation
|
|
276
|
+
for violation in violations
|
|
277
|
+
if not str(violation.get("rule_id", "")).startswith("GATE-")
|
|
278
|
+
]
|
|
273
279
|
|
|
274
280
|
return GateResult(
|
|
275
281
|
scope=context.scope,
|
|
@@ -18,6 +18,8 @@ import subprocess
|
|
|
18
18
|
import sys
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
+
from common.git.git_snapshot import collect_changed_paths
|
|
22
|
+
|
|
21
23
|
try:
|
|
22
24
|
from .validate_rules import (
|
|
23
25
|
config_violation,
|
|
@@ -71,7 +73,15 @@ def validate_implementation(
|
|
|
71
73
|
)
|
|
72
74
|
)
|
|
73
75
|
violations.extend(_check_premature_abstraction(repo_root, diff_output, rule_index))
|
|
74
|
-
violations.extend(
|
|
76
|
+
violations.extend(
|
|
77
|
+
_check_unrequested_features(
|
|
78
|
+
repo_root,
|
|
79
|
+
diff_output,
|
|
80
|
+
task_dir,
|
|
81
|
+
rule_index,
|
|
82
|
+
modified_files=modified_files,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
75
85
|
|
|
76
86
|
return violations
|
|
77
87
|
|
|
@@ -86,38 +96,44 @@ def _missing_rule(repo_root: Path, rule_id: str) -> dict:
|
|
|
86
96
|
|
|
87
97
|
|
|
88
98
|
def _get_modified_files(repo_root: Path) -> list[str]:
|
|
89
|
-
"""Get
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
"""Get staged, unstaged, and untracked files under repo_root."""
|
|
100
|
+
changed_files = collect_changed_paths(repo_root)
|
|
101
|
+
return [
|
|
102
|
+
file_path
|
|
103
|
+
for file_path in changed_files
|
|
104
|
+
if _is_implementation_relevant(file_path)
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _is_implementation_relevant(file_path: str) -> bool:
|
|
109
|
+
normalized = file_path.replace("\\", "/")
|
|
110
|
+
if not normalized.startswith(".cowork-flow/"):
|
|
111
|
+
return True
|
|
112
|
+
return (
|
|
113
|
+
normalized.startswith(".cowork-flow/spec/")
|
|
114
|
+
or normalized == ".cowork-flow/workflow.md"
|
|
115
|
+
)
|
|
104
116
|
|
|
105
117
|
|
|
106
118
|
def _get_git_diff(repo_root: Path, files: list[str]) -> str:
|
|
107
|
-
"""Get
|
|
119
|
+
"""Get unstaged and staged diff output for changed files."""
|
|
108
120
|
if not files:
|
|
109
121
|
return ""
|
|
110
122
|
|
|
111
123
|
try:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
outputs = []
|
|
125
|
+
for extra_args in ((), ("--cached",)):
|
|
126
|
+
diff_result = subprocess.run(
|
|
127
|
+
["git", "diff", *extra_args, "--", *files],
|
|
128
|
+
cwd=repo_root,
|
|
129
|
+
capture_output=True,
|
|
130
|
+
text=True,
|
|
131
|
+
encoding="utf-8",
|
|
132
|
+
errors="replace",
|
|
133
|
+
)
|
|
134
|
+
if diff_result.returncode == 0:
|
|
135
|
+
outputs.append(diff_result.stdout)
|
|
136
|
+
return "\n".join(outputs)
|
|
121
137
|
except Exception:
|
|
122
138
|
return ""
|
|
123
139
|
|
|
@@ -229,6 +245,8 @@ def _check_unrequested_features(
|
|
|
229
245
|
diff_output: str,
|
|
230
246
|
task_dir: Path,
|
|
231
247
|
rule_index: dict[str, dict],
|
|
248
|
+
*,
|
|
249
|
+
modified_files: list[str] | None = None,
|
|
232
250
|
) -> list[dict]:
|
|
233
251
|
"""R-AG-005: Require modified files to be explicitly listed."""
|
|
234
252
|
del diff_output
|
|
@@ -242,8 +260,12 @@ def _check_unrequested_features(
|
|
|
242
260
|
return violations
|
|
243
261
|
|
|
244
262
|
# Get modified project files (exclude .cowork-flow/ metadata)
|
|
245
|
-
|
|
246
|
-
|
|
263
|
+
changed_files = (
|
|
264
|
+
modified_files
|
|
265
|
+
if modified_files is not None
|
|
266
|
+
else _get_modified_files(repo_root)
|
|
267
|
+
)
|
|
268
|
+
for file_path in changed_files:
|
|
247
269
|
# Skip .cowork-flow metadata files and template files
|
|
248
270
|
if ".cowork-flow/" in file_path:
|
|
249
271
|
continue
|
|
@@ -19,6 +19,14 @@ class ChangedFile:
|
|
|
19
19
|
|
|
20
20
|
def collect_changed_files(repo_root: Path) -> list[ChangedFile]:
|
|
21
21
|
"""Return modified, staged, and untracked files from git status."""
|
|
22
|
+
prefix_rc, prefix_stdout, _ = _run_git_command(
|
|
23
|
+
["rev-parse", "--show-prefix"],
|
|
24
|
+
cwd=repo_root,
|
|
25
|
+
)
|
|
26
|
+
if prefix_rc != 0:
|
|
27
|
+
return []
|
|
28
|
+
repo_prefix = prefix_stdout.strip().replace("\\", "/")
|
|
29
|
+
|
|
22
30
|
rc, stdout, _ = _run_git_command(
|
|
23
31
|
["status", "--porcelain=v1", "-uall", "--", "."],
|
|
24
32
|
cwd=repo_root,
|
|
@@ -32,6 +40,12 @@ def collect_changed_files(repo_root: Path) -> list[ChangedFile]:
|
|
|
32
40
|
if parsed is None:
|
|
33
41
|
continue
|
|
34
42
|
path, statuses = parsed
|
|
43
|
+
if repo_prefix:
|
|
44
|
+
if not path.startswith(repo_prefix):
|
|
45
|
+
continue
|
|
46
|
+
path = path[len(repo_prefix):]
|
|
47
|
+
if not path:
|
|
48
|
+
continue
|
|
35
49
|
changed.setdefault(path, set()).update(statuses)
|
|
36
50
|
|
|
37
51
|
return [
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
| G2 | L1/L2: tdd.jsonl 有 red-green evidence | ./cowork-flow/run task validate |
|
|
11
11
|
| G3 | check phase 完成,无 unresolved blocker | check.jsonl status=pass |
|
|
12
12
|
| G4 | 无新引入 lint/type/build 警告 | lint / build 命令通过 |
|
|
13
|
-
| G5 |
|
|
13
|
+
| G5 | Git status snapshot 仅含任务相关变更,包括 staged、unstaged、untracked | git status --porcelain=v1 -uall |
|
|
14
14
|
|
|
15
15
|
## L0 专属(docs/format/comments only)
|
|
16
16
|
|
|
@@ -216,7 +216,7 @@ L2 任务在 `task start` 前必须通过 readiness gate;同一 blocker 列表
|
|
|
216
216
|
- 所有声明通过的验证都有命令输出依据。
|
|
217
217
|
- 规格已更新,或明确判断无需更新。
|
|
218
218
|
- 计划状态、任务状态、`change` 元数据不冲突。
|
|
219
|
-
-
|
|
219
|
+
- 先归档并提交实现,再用真实实现提交哈希记录会话;会话元数据使用 follow-up metadata commit 提交。
|
|
220
220
|
- 不纳入无关脏改。
|
|
221
221
|
|
|
222
222
|
2. 顺序:
|
|
@@ -226,11 +226,15 @@ L2 任务在 `task start` 前必须通过 readiness gate;同一 blocker 列表
|
|
|
226
226
|
git diff --check
|
|
227
227
|
npm run test:all
|
|
228
228
|
./.cowork-flow/run task archive <task-name>
|
|
229
|
-
./.cowork-flow/run add-session --title "<title>" --commit "-" --summary "<summary>"
|
|
230
229
|
git status --short
|
|
231
230
|
git add <expected files>
|
|
232
231
|
git commit -m "<message>"
|
|
232
|
+
implementation_commit="$(git rev-parse HEAD)"
|
|
233
|
+
./.cowork-flow/run add-session --title "<title>" --commit "$implementation_commit" --summary "<summary>"
|
|
234
|
+
git add .cowork-flow/workspace/<developer>/
|
|
235
|
+
git commit -m "docs(session): record <title>"
|
|
233
236
|
```
|
|
237
|
+
第二个提交是明确的 follow-up metadata commit;session 中的 `commit` 始终指向前一个真实实现提交,不使用永久占位值。
|
|
234
238
|
`task archive <task-name>` 会归档 task,并自动归档 `change.yaml.task`
|
|
235
239
|
指向该 task 的 active change;无法通过 `change validate` 的 change 不会被自动归档。
|
|
236
240
|
|
|
@@ -216,7 +216,7 @@ L2 任务在 `task start` 前必须通过 readiness gate;同一 blocker 列表
|
|
|
216
216
|
- 所有声明通过的验证都有命令输出依据。
|
|
217
217
|
- 规格已更新,或明确判断无需更新。
|
|
218
218
|
- 计划状态、任务状态、`change` 元数据不冲突。
|
|
219
|
-
-
|
|
219
|
+
- 先归档并提交实现,再用真实实现提交哈希记录会话;会话元数据使用 follow-up metadata commit 提交。
|
|
220
220
|
- 不纳入无关脏改。
|
|
221
221
|
|
|
222
222
|
2. 顺序:
|
|
@@ -226,11 +226,15 @@ L2 任务在 `task start` 前必须通过 readiness gate;同一 blocker 列表
|
|
|
226
226
|
git diff --check
|
|
227
227
|
npm run test:all
|
|
228
228
|
./.cowork-flow/run task archive <task-name>
|
|
229
|
-
./.cowork-flow/run add-session --title "<title>" --commit "-" --summary "<summary>"
|
|
230
229
|
git status --short
|
|
231
230
|
git add <expected files>
|
|
232
231
|
git commit -m "<message>"
|
|
232
|
+
implementation_commit="$(git rev-parse HEAD)"
|
|
233
|
+
./.cowork-flow/run add-session --title "<title>" --commit "$implementation_commit" --summary "<summary>"
|
|
234
|
+
git add .cowork-flow/workspace/<developer>/
|
|
235
|
+
git commit -m "docs(session): record <title>"
|
|
233
236
|
```
|
|
237
|
+
第二个提交是明确的 follow-up metadata commit;session 中的 `commit` 始终指向前一个真实实现提交,不使用永久占位值。
|
|
234
238
|
`task archive <task-name>` 会归档 task,并自动归档 `change.yaml.task`
|
|
235
239
|
指向该 task 的 active change;无法通过 `change validate` 的 change 不会被自动归档。
|
|
236
240
|
|