cowork-flow 0.0.47 → 0.0.49
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 +41 -0
- package/README.md +102 -23
- package/package.json +8 -3
- package/src/cli.js +6 -0
- package/src/commands/init.js +0 -83
- package/src/commands/source-refresh.js +40 -0
- package/src/commands/sync.js +6 -0
- package/src/commands/update.js +59 -6
- package/src/lib/copy-template.js +200 -50
- package/src/lib/host-assets.js +367 -64
- package/src/lib/plan-applier.js +47 -0
- package/template/.claude/hooks/inject-workflow-state.py +7 -0
- package/template/.codex/hooks/inject-workflow-state.py +7 -0
- package/template/.codex/hooks.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/config.yaml +0 -11
- package/template/.cowork-flow/scripts/adapters/cli/init_developer.py +0 -1
- package/template/.cowork-flow/scripts/adapters/cli/subagent.py +7 -4
- package/template/.cowork-flow/scripts/adapters/cli/task.py +8 -484
- package/template/.cowork-flow/scripts/adapters/cli/task_archive_commands.py +15 -134
- package/template/.cowork-flow/scripts/adapters/cli/task_context_commands.py +7 -1
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +388 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +163 -2
- package/template/.cowork-flow/scripts/adapters/git/git_context.py +16 -193
- package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +170 -84
- package/template/.cowork-flow/scripts/infra/config.py +0 -17
- package/template/.cowork-flow/scripts/infra/developer_profile.py +0 -95
- package/template/.cowork-flow/scripts/infra/paths.py +0 -79
- package/template/.cowork-flow/scripts/infra/skill_manifest.py +23 -3
- package/template/.cowork-flow/scripts/infra/storage/operation_log.py +40 -0
- package/template/.cowork-flow/scripts/infra/storage/state_store.py +356 -1
- package/template/.cowork-flow/scripts/kernel/workflow_route.py +15 -15
- package/template/.cowork-flow/scripts/run.py +0 -5
- package/template/.cowork-flow/scripts/services/context_discovery.py +156 -0
- package/template/.cowork-flow/scripts/services/context_jsonl.py +119 -0
- package/template/.cowork-flow/scripts/services/context_paths.py +186 -0
- package/template/.cowork-flow/scripts/services/lifecycle_checks.py +186 -69
- package/template/.cowork-flow/scripts/services/lifecycle_policy.py +128 -0
- package/template/.cowork-flow/scripts/services/plan_binding.py +146 -0
- package/template/.cowork-flow/scripts/services/readiness.py +3 -170
- package/template/.cowork-flow/scripts/services/task_archive.py +115 -24
- package/template/.cowork-flow/scripts/services/task_context.py +103 -365
- package/template/.cowork-flow/scripts/services/task_creation.py +40 -5
- package/template/.cowork-flow/scripts/services/task_lifecycle.py +155 -18
- package/template/.cowork-flow/scripts/services/task_routing.py +80 -67
- package/template/.cowork-flow/scripts/services/task_utils.py +0 -146
- package/template/.cowork-flow/scripts/services/workflow_runtime.py +1 -1
- package/template/.cowork-flow/spec/contracts/capabilities.md +23 -0
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +2 -2
- package/template/.cowork-flow/spec/contracts/error-output-as-data.md +8 -0
- package/template/.cowork-flow/spec/contracts/index.md +1 -0
- package/template/.cowork-flow/spec/contracts/party-mode-v2-board.md +20 -0
- package/template/.cowork-flow/spec/contracts/plan-binding.md +50 -0
- package/template/.cowork-flow/spec/contracts/skill-owned-actions.md +31 -7
- package/template/.cowork-flow/spec/guides/index.md +3 -1
- package/template/.cowork-flow/spec/index.md +5 -4
- package/template/.cowork-flow/spec/references/definition-of-done.md +2 -3
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
- package/template/.cowork-flow/spec/runtime/contract-registry.json +14 -0
- package/template/.cowork-flow/spec/runtime/host-assets.json +73 -3
- package/template/.cowork-flow/spec/runtime/index.md +1 -1
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +150 -8
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/skills/batch-execution/SKILL.md +37 -3
- package/template/skills/batch-execution/scripts/batch_action.py +4 -0
- package/template/skills/batch-execution/scripts/batch_execution.py +120 -499
- package/template/skills/batch-execution/scripts/batch_recovery.py +94 -0
- package/template/skills/batch-execution/scripts/batch_state_machine.py +201 -0
- package/template/skills/batch-execution/scripts/batch_verification.py +326 -0
- package/template/skills/brainstorming/SKILL.md +8 -7
- package/template/skills/cowork-flow/SKILL.md +3 -0
- package/template/skills/party-mode/scripts/party_action_contract.py +113 -0
- package/template/skills/party-mode/scripts/party_actions.py +95 -0
- package/template/skills/party-mode/scripts/party_board_store.py +148 -0
- package/template/skills/party-mode/scripts/party_mode_v2.py +345 -317
- package/template/skills/party-mode/scripts/party_reports.py +177 -0
- package/template/skills/party-mode/scripts/party_state_machine.py +189 -0
- package/template/skills/runtime-health/SKILL.md +14 -2
- package/template/skills/runtime-health/scripts/doctor.py +278 -41
- package/template/skills/task-planning/SKILL.md +105 -61
- package/template/skills/task-review/SKILL.md +4 -4
- package/template/skills/task-review/scripts/review_check.py +277 -20
- package/template/skills/test-first/SKILL.md +18 -3
- package/template/skills/test-first/manifest.json +8 -4
- package/template/.cowork-flow/changes/archive/.gitkeep +0 -1
- package/template/.cowork-flow/scripts/adapters/cli/add_session.py +0 -465
- package/template/.cowork-flow/scripts/adapters/cli/change.py +0 -412
- package/template/.cowork-flow/workspace/index.md +0 -102
- package/template/.zcode/scaffold/AGENTS.md +0 -145
- package/template/.zcode/scaffold/CLAUDE.md +0 -5
|
@@ -7,6 +7,7 @@ import argparse
|
|
|
7
7
|
import ast
|
|
8
8
|
import json
|
|
9
9
|
import re
|
|
10
|
+
import subprocess
|
|
10
11
|
import sys
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Any
|
|
@@ -35,6 +36,7 @@ from runtime.session_state import is_main_session
|
|
|
35
36
|
PROTECTED_PREFIX = "Protected workflow/spec file changed outside main session: "
|
|
36
37
|
UNLISTED_PREFIX = "Modified file not listed in implement.jsonl: "
|
|
37
38
|
TEST_SUFFIXES = {".py", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
|
|
39
|
+
FUNCTION_LINE_SOFT_LIMIT = 60
|
|
38
40
|
CODE_SUFFIXES = {
|
|
39
41
|
".py",
|
|
40
42
|
".js",
|
|
@@ -119,12 +121,71 @@ def _infer_dev_type(task_data: dict[str, Any], changed_files: list[str]) -> str:
|
|
|
119
121
|
return ""
|
|
120
122
|
|
|
121
123
|
|
|
124
|
+
def _normalized_issue(
|
|
125
|
+
*,
|
|
126
|
+
code: str,
|
|
127
|
+
severity: str,
|
|
128
|
+
path: str,
|
|
129
|
+
message: str,
|
|
130
|
+
source: str,
|
|
131
|
+
**extra: str,
|
|
132
|
+
) -> dict[str, str]:
|
|
133
|
+
issue = {
|
|
134
|
+
"code": code,
|
|
135
|
+
"severity": severity,
|
|
136
|
+
"path": path,
|
|
137
|
+
"message": message,
|
|
138
|
+
"source": source,
|
|
139
|
+
}
|
|
140
|
+
issue.update(extra)
|
|
141
|
+
return issue
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _lifecycle_issue_fact(issue: Any) -> dict[str, str]:
|
|
145
|
+
return _normalized_issue(
|
|
146
|
+
code=str(getattr(issue, "code", "") or "lifecycle_issue"),
|
|
147
|
+
severity="error",
|
|
148
|
+
path=str(getattr(issue, "path", "") or ""),
|
|
149
|
+
message=str(getattr(issue, "message", "") or ""),
|
|
150
|
+
source="lifecycle",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _fallback_lifecycle_issue(
|
|
155
|
+
*,
|
|
156
|
+
code: str,
|
|
157
|
+
path: str,
|
|
158
|
+
message: str,
|
|
159
|
+
) -> dict[str, str]:
|
|
160
|
+
return _normalized_issue(
|
|
161
|
+
code=code,
|
|
162
|
+
severity="error",
|
|
163
|
+
path=path,
|
|
164
|
+
message=message,
|
|
165
|
+
source="lifecycle",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
122
169
|
def _scope_facts(
|
|
123
170
|
repo_root: Path,
|
|
124
171
|
task_dir: Path,
|
|
125
172
|
*,
|
|
126
173
|
allow_spec_file_modifications: bool,
|
|
127
174
|
) -> dict[str, Any]:
|
|
175
|
+
scope_facts, _ = _scope_facts_with_issues(
|
|
176
|
+
repo_root,
|
|
177
|
+
task_dir,
|
|
178
|
+
allow_spec_file_modifications=allow_spec_file_modifications,
|
|
179
|
+
)
|
|
180
|
+
return scope_facts
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _scope_facts_with_issues(
|
|
184
|
+
repo_root: Path,
|
|
185
|
+
task_dir: Path,
|
|
186
|
+
*,
|
|
187
|
+
allow_spec_file_modifications: bool,
|
|
188
|
+
) -> tuple[dict[str, Any], list[dict[str, str]]]:
|
|
128
189
|
result = LifecycleCheckRunner(repo_root).review(
|
|
129
190
|
task_dir,
|
|
130
191
|
allow_spec_file_modifications=allow_spec_file_modifications,
|
|
@@ -132,19 +193,62 @@ def _scope_facts(
|
|
|
132
193
|
protected: list[str] = []
|
|
133
194
|
unlisted: list[str] = []
|
|
134
195
|
context_issues: list[str] = []
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
196
|
+
normalized_issues: list[dict[str, str]] = []
|
|
197
|
+
issues = tuple(getattr(result, "issues", ()) or ())
|
|
198
|
+
if issues:
|
|
199
|
+
for issue in issues:
|
|
200
|
+
code = str(getattr(issue, "code", "") or "")
|
|
201
|
+
path = getattr(issue, "path", None)
|
|
202
|
+
message = str(getattr(issue, "message", "") or "")
|
|
203
|
+
normalized_issues.append(_lifecycle_issue_fact(issue))
|
|
204
|
+
if code == "protected_workflow_file":
|
|
205
|
+
protected.append(str(path or message.removeprefix(PROTECTED_PREFIX)))
|
|
206
|
+
elif code == "unlisted_changed_file":
|
|
207
|
+
normalized = str(path or message.removeprefix(UNLISTED_PREFIX))
|
|
208
|
+
if normalized not in protected:
|
|
209
|
+
unlisted.append(normalized)
|
|
210
|
+
else:
|
|
211
|
+
context_issues.append(message)
|
|
212
|
+
else:
|
|
213
|
+
for message in result.blockers:
|
|
214
|
+
if message.startswith(PROTECTED_PREFIX):
|
|
215
|
+
path = message.removeprefix(PROTECTED_PREFIX)
|
|
216
|
+
protected.append(path)
|
|
217
|
+
normalized_issues.append(
|
|
218
|
+
_fallback_lifecycle_issue(
|
|
219
|
+
code="protected_workflow_file",
|
|
220
|
+
path=path,
|
|
221
|
+
message=message,
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
elif message.startswith(UNLISTED_PREFIX):
|
|
225
|
+
path = message.removeprefix(UNLISTED_PREFIX)
|
|
226
|
+
unlisted.append(path)
|
|
227
|
+
normalized_issues.append(
|
|
228
|
+
_fallback_lifecycle_issue(
|
|
229
|
+
code="unlisted_changed_file",
|
|
230
|
+
path=path,
|
|
231
|
+
message=message,
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
context_issues.append(message)
|
|
236
|
+
normalized_issues.append(
|
|
237
|
+
_fallback_lifecycle_issue(
|
|
238
|
+
code="lifecycle_context_issue",
|
|
239
|
+
path="",
|
|
240
|
+
message=message,
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
return (
|
|
244
|
+
{
|
|
245
|
+
"unlistedChangedFiles": unlisted,
|
|
246
|
+
"protectedWorkflowFiles": protected,
|
|
247
|
+
"contextIssues": context_issues,
|
|
248
|
+
"lifecycleMessages": list(result.blockers),
|
|
249
|
+
},
|
|
250
|
+
normalized_issues,
|
|
251
|
+
)
|
|
148
252
|
|
|
149
253
|
|
|
150
254
|
def _check_context_sources(repo_root: Path, task_dir: Path) -> list[dict[str, Any]]:
|
|
@@ -214,6 +318,124 @@ def _is_code_file(path: str) -> bool:
|
|
|
214
318
|
return Path(path).suffix in CODE_SUFFIXES
|
|
215
319
|
|
|
216
320
|
|
|
321
|
+
def _python_function_lengths(content: str) -> dict[str, dict[str, int]]:
|
|
322
|
+
try:
|
|
323
|
+
tree = ast.parse(content.lstrip("\ufeff"))
|
|
324
|
+
except SyntaxError:
|
|
325
|
+
return {}
|
|
326
|
+
|
|
327
|
+
functions: dict[str, dict[str, int]] = {}
|
|
328
|
+
|
|
329
|
+
def visit_nodes(nodes: list[ast.stmt], prefix: tuple[str, ...]) -> None:
|
|
330
|
+
for node in nodes:
|
|
331
|
+
if isinstance(node, ast.ClassDef):
|
|
332
|
+
visit_nodes(node.body, (*prefix, node.name))
|
|
333
|
+
continue
|
|
334
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
335
|
+
end_lineno = getattr(node, "end_lineno", node.lineno)
|
|
336
|
+
name = ".".join((*prefix, node.name))
|
|
337
|
+
functions[name] = {
|
|
338
|
+
"line": node.lineno,
|
|
339
|
+
"lines": end_lineno - node.lineno + 1,
|
|
340
|
+
}
|
|
341
|
+
visit_nodes(node.body, (*prefix, node.name))
|
|
342
|
+
|
|
343
|
+
visit_nodes(tree.body, ())
|
|
344
|
+
return functions
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _git_baseline_text(repo_root: Path, path: str) -> str | None:
|
|
348
|
+
result = subprocess.run(
|
|
349
|
+
["git", "show", f"HEAD:{path}"],
|
|
350
|
+
cwd=repo_root,
|
|
351
|
+
capture_output=True,
|
|
352
|
+
text=True,
|
|
353
|
+
encoding="utf-8",
|
|
354
|
+
check=False,
|
|
355
|
+
)
|
|
356
|
+
if result.returncode != 0:
|
|
357
|
+
return None
|
|
358
|
+
return result.stdout
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _complexity_signals(repo_root: Path, changed_files: list[str]) -> list[dict[str, str]]:
|
|
362
|
+
signals: list[dict[str, str]] = []
|
|
363
|
+
for raw_path in changed_files:
|
|
364
|
+
normalized = raw_path.replace("\\", "/").removeprefix("./")
|
|
365
|
+
if Path(normalized).suffix != ".py":
|
|
366
|
+
continue
|
|
367
|
+
full_path = repo_root / normalized
|
|
368
|
+
if not full_path.is_file():
|
|
369
|
+
continue
|
|
370
|
+
try:
|
|
371
|
+
current_functions = _python_function_lengths(
|
|
372
|
+
full_path.read_text(encoding="utf-8")
|
|
373
|
+
)
|
|
374
|
+
except (OSError, UnicodeDecodeError):
|
|
375
|
+
continue
|
|
376
|
+
baseline_text = _git_baseline_text(repo_root, normalized)
|
|
377
|
+
baseline_functions = (
|
|
378
|
+
_python_function_lengths(baseline_text) if baseline_text is not None else {}
|
|
379
|
+
)
|
|
380
|
+
for function_name, current in sorted(current_functions.items()):
|
|
381
|
+
current_lines = current["lines"]
|
|
382
|
+
if current_lines <= FUNCTION_LINE_SOFT_LIMIT:
|
|
383
|
+
continue
|
|
384
|
+
baseline_lines = baseline_functions.get(function_name, {}).get("lines", 0)
|
|
385
|
+
budget_baseline = (
|
|
386
|
+
baseline_lines if baseline_lines > FUNCTION_LINE_SOFT_LIMIT else 0
|
|
387
|
+
)
|
|
388
|
+
if budget_baseline == 0:
|
|
389
|
+
code = "complexity_new_long_function"
|
|
390
|
+
message = (
|
|
391
|
+
f"Function {function_name} is {current_lines} lines; "
|
|
392
|
+
f"soft budget is {FUNCTION_LINE_SOFT_LIMIT} lines."
|
|
393
|
+
)
|
|
394
|
+
elif current_lines > baseline_lines:
|
|
395
|
+
code = "complexity_hotspot_grew"
|
|
396
|
+
message = (
|
|
397
|
+
f"Function {function_name} grew from {baseline_lines} "
|
|
398
|
+
f"to {current_lines} lines; split responsibilities or record intent."
|
|
399
|
+
)
|
|
400
|
+
else:
|
|
401
|
+
code = "complexity_existing_hotspot"
|
|
402
|
+
message = (
|
|
403
|
+
f"Existing hotspot {function_name} remains {current_lines} lines; "
|
|
404
|
+
"warning only for reviewer awareness."
|
|
405
|
+
)
|
|
406
|
+
signals.append(
|
|
407
|
+
{
|
|
408
|
+
"code": code,
|
|
409
|
+
"file": normalized,
|
|
410
|
+
"function": function_name,
|
|
411
|
+
"line": str(current["line"]),
|
|
412
|
+
"baselineLines": str(budget_baseline),
|
|
413
|
+
"currentLines": str(current_lines),
|
|
414
|
+
"limitLines": str(FUNCTION_LINE_SOFT_LIMIT),
|
|
415
|
+
"reason": message,
|
|
416
|
+
}
|
|
417
|
+
)
|
|
418
|
+
return signals
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _complexity_issues(signals: list[dict[str, str]]) -> list[dict[str, str]]:
|
|
422
|
+
return [
|
|
423
|
+
_normalized_issue(
|
|
424
|
+
code=signal["code"],
|
|
425
|
+
severity="warning",
|
|
426
|
+
path=signal["file"],
|
|
427
|
+
message=signal["reason"],
|
|
428
|
+
source="complexity",
|
|
429
|
+
function=signal["function"],
|
|
430
|
+
line=signal["line"],
|
|
431
|
+
baselineLines=signal["baselineLines"],
|
|
432
|
+
currentLines=signal["currentLines"],
|
|
433
|
+
limitLines=signal["limitLines"],
|
|
434
|
+
)
|
|
435
|
+
for signal in signals
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
|
|
217
439
|
def _python_test_names(content: str) -> list[str]:
|
|
218
440
|
try:
|
|
219
441
|
tree = ast.parse(content.lstrip("\ufeff"))
|
|
@@ -284,7 +506,28 @@ def _test_intent_signals(repo_root: Path, changed_files: list[str]) -> dict[str,
|
|
|
284
506
|
}
|
|
285
507
|
|
|
286
508
|
|
|
287
|
-
def
|
|
509
|
+
def _test_intent_issues(test_signals: dict[str, Any]) -> list[dict[str, str]]:
|
|
510
|
+
issues: list[dict[str, str]] = []
|
|
511
|
+
for signal in test_signals["shallowAssertionSignals"]:
|
|
512
|
+
issues.append(
|
|
513
|
+
_normalized_issue(
|
|
514
|
+
code="shallow_assertion_signal",
|
|
515
|
+
severity="warning",
|
|
516
|
+
path=str(signal.get("file") or ""),
|
|
517
|
+
message=str(signal.get("reason") or "shallow test intent signal"),
|
|
518
|
+
source="test-intent",
|
|
519
|
+
testName=str(signal.get("testName") or ""),
|
|
520
|
+
signal=str(signal.get("signal") or ""),
|
|
521
|
+
)
|
|
522
|
+
)
|
|
523
|
+
return issues
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _verification_hints(
|
|
527
|
+
changed_files: list[str],
|
|
528
|
+
test_signals: dict[str, Any],
|
|
529
|
+
complexity_signals: list[dict[str, str]],
|
|
530
|
+
) -> list[str]:
|
|
288
531
|
hints: list[str] = []
|
|
289
532
|
code_files = [
|
|
290
533
|
path
|
|
@@ -298,6 +541,8 @@ def _verification_hints(changed_files: list[str], test_signals: dict[str, Any])
|
|
|
298
541
|
)
|
|
299
542
|
if test_signals["shallowAssertionSignals"]:
|
|
300
543
|
hints.append("Review shallowAssertionSignals before accepting test intent.")
|
|
544
|
+
if complexity_signals:
|
|
545
|
+
hints.append("Review complexity soft-budget warnings before accepting large functions.")
|
|
301
546
|
return hints
|
|
302
547
|
|
|
303
548
|
|
|
@@ -306,6 +551,12 @@ def build_report(repo_root: Path, task_dir: Path) -> dict[str, Any]:
|
|
|
306
551
|
changed_files = collect_changed_paths(repo_root)
|
|
307
552
|
dev_type = _infer_dev_type(task_data, changed_files)
|
|
308
553
|
test_signals = _test_intent_signals(repo_root, changed_files)
|
|
554
|
+
complexity_signals = _complexity_signals(repo_root, changed_files)
|
|
555
|
+
scope_facts, scope_issues = _scope_facts_with_issues(
|
|
556
|
+
repo_root,
|
|
557
|
+
task_dir,
|
|
558
|
+
allow_spec_file_modifications=is_main_session(repo_root),
|
|
559
|
+
)
|
|
309
560
|
return {
|
|
310
561
|
"reportVersion": 1,
|
|
311
562
|
"mode": "advisory",
|
|
@@ -315,11 +566,12 @@ def build_report(repo_root: Path, task_dir: Path) -> dict[str, Any]:
|
|
|
315
566
|
"devType": dev_type,
|
|
316
567
|
},
|
|
317
568
|
"changedFiles": changed_files,
|
|
318
|
-
"scopeFacts":
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
569
|
+
"scopeFacts": scope_facts,
|
|
570
|
+
"normalizedIssues": [
|
|
571
|
+
*scope_issues,
|
|
572
|
+
*_test_intent_issues(test_signals),
|
|
573
|
+
*_complexity_issues(complexity_signals),
|
|
574
|
+
],
|
|
323
575
|
"specSources": _spec_sources(
|
|
324
576
|
repo_root,
|
|
325
577
|
task_dir,
|
|
@@ -327,7 +579,12 @@ def build_report(repo_root: Path, task_dir: Path) -> dict[str, Any]:
|
|
|
327
579
|
changed_files=changed_files,
|
|
328
580
|
),
|
|
329
581
|
"testIntentSignals": test_signals,
|
|
330
|
-
"
|
|
582
|
+
"complexitySignals": complexity_signals,
|
|
583
|
+
"verificationHints": _verification_hints(
|
|
584
|
+
changed_files,
|
|
585
|
+
test_signals,
|
|
586
|
+
complexity_signals,
|
|
587
|
+
),
|
|
331
588
|
"notes": [
|
|
332
589
|
"This report is advisory and does not mutate task state.",
|
|
333
590
|
"Do not treat natural-language spec review as a runtime lifecycle gate.",
|
|
@@ -9,9 +9,13 @@ Use this Skill to practice test-first implementation inside the active cowork-fl
|
|
|
9
9
|
|
|
10
10
|
## Trigger
|
|
11
11
|
|
|
12
|
-
Use for behavior, state, protocol, CLI, data-format, permission, or error-handling changes
|
|
12
|
+
Use for behavior, state, protocol, CLI, data-format, permission, or error-handling changes. Behavior changes default to a red test before implementation: first pin the expected behavior with the smallest meaningful failing test, then make the minimum code change needed to turn it green.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
For docs/comment/format-only work, skip TDD and run the smallest relevant verification command.
|
|
15
|
+
|
|
16
|
+
## Red test before implementation
|
|
17
|
+
|
|
18
|
+
Do not modify production code before the red test for a behavior change unless a documented exception applies. The normal path is:
|
|
15
19
|
|
|
16
20
|
1. Map the behavior to a `decision-anchor.md` acceptance ID when one exists.
|
|
17
21
|
2. Write the smallest meaningful test that fails because the target behavior is missing or wrong.
|
|
@@ -20,6 +24,17 @@ Use for behavior, state, protocol, CLI, data-format, permission, or error-handli
|
|
|
20
24
|
5. Run the same test to green, then run directly dependent tests.
|
|
21
25
|
6. Refactor only after the behavior is pinned green.
|
|
22
26
|
|
|
27
|
+
Tests added after implementation are regression tests, not evidence that a red-green cycle happened. They may still be useful, but do not describe them as test-first work unless the red failure was observed before the implementation change.
|
|
28
|
+
|
|
29
|
+
## Exceptions
|
|
30
|
+
|
|
31
|
+
If red-first is skipped, state the reason before or with the implementation summary and name the substitute verification. Acceptable reasons are limited to:
|
|
32
|
+
|
|
33
|
+
- docs/comment/format-only changes where no behavior changes.
|
|
34
|
+
- an existing precise failing test already reproduces the target behavior.
|
|
35
|
+
- the behavior cannot be reproduced locally after a focused attempt; describe the attempted command and observed result.
|
|
36
|
+
- no usable test entry point exists; explain the gap and use the narrowest manual or static verification available.
|
|
37
|
+
|
|
23
38
|
## Output
|
|
24
39
|
|
|
25
40
|
Do not write TDD evidence objects to `check.jsonl`, do not create `tdd.jsonl`, and do not create TDD exemption records. Report the exact red/green commands in the agent response or task review narrative when useful; the workflow does not validate them as separate evidence artifacts.
|
|
@@ -32,6 +47,6 @@ Do not use these excuses to skip meaningful behavior tests:
|
|
|
32
47
|
|---|---|---|
|
|
33
48
|
| "This logic is simple, no test needed" | Simple logic still breaks on edge cases and later edits. | Write the smallest behavior test, or state the concrete non-test verification used. |
|
|
34
49
|
| "Other tests already cover this" | Invisible coverage is hard to review. | Name the exact existing test command and behavior it covers. |
|
|
35
|
-
| "I'll add tests after implementation" | Post-implementation tests can miss the original failure mode. |
|
|
50
|
+
| "I'll add tests after implementation" | Post-implementation tests can miss the original failure mode and is not test-first. | Red first, then green; if this already happened, call the new test a regression test. |
|
|
36
51
|
| "It looks correct" | Visual inspection misses scenarios that tests can pin down. | Turn expected behavior and edge cases into assertions. |
|
|
37
52
|
| "This is internal only" | Internal behavior still affects callers and workflow state. | Test through the public entry point, or directly test the narrow internal contract. |
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
"skill": "test-first",
|
|
4
4
|
"context": [
|
|
5
5
|
{
|
|
6
|
-
"contexts": [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
"contexts": [
|
|
7
|
+
"implement",
|
|
8
|
+
"check"
|
|
9
|
+
],
|
|
10
|
+
"devTypes": [
|
|
11
|
+
"*"
|
|
12
|
+
],
|
|
13
|
+
"reason": "Auto-routed context-only red-green guide"
|
|
10
14
|
}
|
|
11
15
|
]
|
|
12
16
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|