cowork-flow 0.0.21 → 0.0.23
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/README.md +5 -5
- package/package.json +1 -1
- package/src/lib/copy-template.js +1 -1
- package/src/lib/platforms.js +1 -1
- package/template/{.agent → .agents}/skills/meta/SKILL.md +1 -1
- package/template/.claude/skills/meta/SKILL.md +1 -1
- package/template/.codex/agents/default.toml +1 -1
- package/template/.codex/agents/explorer.toml +1 -1
- package/template/.codex/agents/worker.toml +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/change.py +59 -19
- package/template/.cowork-flow/scripts/common/__init__.py +1 -1
- package/template/.cowork-flow/scripts/common/paths.py +1 -1
- package/template/.cowork-flow/scripts/doctor.py +6 -6
- package/template/.cowork-flow/scripts/task.py +59 -8
- package/template/.cowork-flow/workflow.md +2 -0
- /package/template/{.agent → .agents}/skills/before-dev/SKILL.md +0 -0
- /package/template/{.agent → .agents}/skills/brainstorming/SKILL.md +0 -0
- /package/template/{.agent → .agents}/skills/break-loop/SKILL.md +0 -0
- /package/template/{.agent → .agents}/skills/check/SKILL.md +0 -0
- /package/template/{.agent → .agents}/skills/continue/SKILL.md +0 -0
- /package/template/{.agent → .agents}/skills/finish-work/SKILL.md +0 -0
- /package/template/{.agent → .agents}/skills/python-design/SKILL.md +0 -0
- /package/template/{.agent → .agents}/skills/start/SKILL.md +0 -0
- /package/template/{.agent → .agents}/skills/update-spec/SKILL.md +0 -0
- /package/template/{.agent → .agents}/skills/writing-plans/SKILL.md +0 -0
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ cowork-flow 是一个用于新项目初始化协作流程的模板仓库。它
|
|
|
36
36
|
│ ├── agents/
|
|
37
37
|
│ ├── commands/
|
|
38
38
|
│ └── plugins/
|
|
39
|
-
├── .
|
|
39
|
+
├── .agents/
|
|
40
40
|
│ └── skills/
|
|
41
41
|
└── .cowork-flow/
|
|
42
42
|
├── config.yaml
|
|
@@ -54,7 +54,7 @@ cowork-flow 是一个用于新项目初始化协作流程的模板仓库。它
|
|
|
54
54
|
`template/AGENTS.md`
|
|
55
55
|
项目级协作入口,包含编码前思考、简单优先、外科手术式改动、验证优先等基础原则。接入项目后,应把项目名称、技术栈、运行命令、测试命令和提交策略补齐。
|
|
56
56
|
|
|
57
|
-
`template/.
|
|
57
|
+
`template/.agents/skills/`
|
|
58
58
|
本地技能入口,覆盖 start、before-dev、brainstorming、writing-plans、check、finish-work、continue、meta、python-design、update-spec 和 break-loop 等协作动作。这里的 skill 应保持通用,不承载某个业务项目的一次性细节。
|
|
59
59
|
|
|
60
60
|
`template/.codex/agents/`
|
|
@@ -64,7 +64,7 @@ cowork-flow 是一个用于新项目初始化协作流程的模板仓库。它
|
|
|
64
64
|
Codex 项目级配置与每轮上下文注入入口。hook 会先用共享 entry classifier 判定当前输入,再读取当前 session task、`.cowork-flow/spec/workflow-state-templates.md` 中的 `workflow-state` 片段和 `codex.dispatch_mode`,把流程状态注入到当前轮对话。注入的 `codex-dispatch-mode` 只是主会话调度提示,不代表当前线程身份。
|
|
65
65
|
|
|
66
66
|
`template/CLAUDE.md`、`template/.claude/settings.json`、`template/.claude/agents/`、`template/.claude/skills/`、`template/.claude/commands/`、`template/.claude/hooks/`
|
|
67
|
-
Claude Code 项目级记忆、hook、固定 agent、skills 和 slash command。`CLAUDE.md` 显式导入 `AGENTS.md`。`.claude/settings.json` 注册 `UserPromptSubmit` 和 `SessionStart` hook,注入当前 workflow state 与 contract digest。固定 agent 通过 `.cowork-flow/run subagent init` 生成 runtime context,并在 prompt、env 或 metadata transport 中传递 `cowork_runtime_context_id`;hook 绑定成功后才进入叶子执行,绑定失败则 fail closed,避免被项目 bootstrap 或无任务首屏上下文拉偏。Claude Code 不自动加载 `.
|
|
67
|
+
Claude Code 项目级记忆、hook、固定 agent、skills 和 slash command。`CLAUDE.md` 显式导入 `AGENTS.md`。`.claude/settings.json` 注册 `UserPromptSubmit` 和 `SessionStart` hook,注入当前 workflow state 与 contract digest。固定 agent 通过 `.cowork-flow/run subagent init` 生成 runtime context,并在 prompt、env 或 metadata transport 中传递 `cowork_runtime_context_id`;hook 绑定成功后才进入叶子执行,绑定失败则 fail closed,避免被项目 bootstrap 或无任务首屏上下文拉偏。Claude Code 不自动加载 `.agents/skills/`,所以项目技能要放进 `.claude/skills/`。
|
|
68
68
|
|
|
69
69
|
`template/.opencode/agents/`、`template/.opencode/commands/`、`template/.opencode/plugins/`
|
|
70
70
|
OpenCode 项目级固定 agent、slash command 和系统上下文注入插件。插件会读取 `.cowork-flow/spec/registry.json`,注入短 contract digest。
|
|
@@ -124,7 +124,7 @@ npx cowork-flow --help
|
|
|
124
124
|
cowork-flow init ./my-project --platform codex
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
`init` 会直接复制模板中的通用 `.cowork-flow/`,并按 `--platform` 只复制对应 host 资产:`codex` 复制 `.codex/`、`.
|
|
127
|
+
`init` 会直接复制模板中的通用 `.cowork-flow/`,并按 `--platform` 只复制对应 host 资产:`codex` 复制 `.codex/`、`.agents/skills/` 与 `.cowork-flow/adapters/codex/`,`opencode` 复制 `.opencode/`、`.agents/skills/` 与 `.cowork-flow/adapters/opencode/`,`claude-code` 复制 `CLAUDE.md`、`.claude/`(含 `.claude/settings.json`、`.claude/hooks/`、`.claude/skills/`)与 `.cowork-flow/adapters/claude-code/`,不复制 `.agents/skills/`。`all` 或 `--platform codex,opencode,claude-code` 同时复制三者和 `.agents/skills/`。`both` 保持兼容,只复制 Codex 和 OpenCode。它还会初始化 `.cowork-flow/.developer` 与 `.cowork-flow/workspace/<developer>/`。交互式终端会先显示 checkbox 多选界面选择平台,再提示开发者名称;非交互式环境必须传入 `--platform <codex|opencode|claude-code|all>` 与 `--developer <name>`。`init` 不会复制额外技能包。
|
|
128
128
|
|
|
129
129
|
初始化到当前项目:
|
|
130
130
|
|
|
@@ -159,7 +159,7 @@ cowork-flow sync .
|
|
|
159
159
|
cowork-flow sync . --dry-run
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
`sync` 会自动识别目标项目已安装的 host 目录:只有 `.codex/` 时只同步 Codex 资产,只有 `.opencode/` 时只同步 OpenCode 资产,两者都有时同步两者;对应 `.cowork-flow/adapters/<host>/` 也按识别出的平台同步。检测到 Codex 或 OpenCode 时会刷新 `.
|
|
162
|
+
`sync` 会自动识别目标项目已安装的 host 目录:只有 `.codex/` 时只同步 Codex 资产,只有 `.opencode/` 时只同步 OpenCode 资产,两者都有时同步两者;对应 `.cowork-flow/adapters/<host>/` 也按识别出的平台同步。检测到 Codex 或 OpenCode 时会刷新 `.agents/skills/`;Claude Code-only 项目只刷新 `.claude/settings.json`、`.claude/hooks/`、`.claude/skills/`,不会创建 `.agents/skills/`。通用部分默认刷新 `.cowork-flow/scripts/`、`.cowork-flow/spec/workflow-state-templates.md`,以及 `AGENTS.md` 和 `CLAUDE.md` 中的 `<!-- COWORK-FLOW:START --> ... <!-- COWORK-FLOW:END -->` 托管块,保留托管块之外的项目自定义内容。`.cowork-flow/config.yaml`、`.cowork-flow/workflow.md`、除 `workflow-state-templates.md` 外的 `.cowork-flow/spec/`、任务、计划、变更和 workspace 记录默认受保护。只有明确传入 `--force` 时才整文件覆盖保护文件。
|
|
163
163
|
|
|
164
164
|
## 常用入口
|
|
165
165
|
|
package/package.json
CHANGED
package/src/lib/copy-template.js
CHANGED
package/src/lib/platforms.js
CHANGED
|
@@ -53,7 +53,7 @@ export function formatPlatformList(platforms) {
|
|
|
53
53
|
|
|
54
54
|
export function shouldIncludeForPlatforms(relativePath, platforms) {
|
|
55
55
|
const normalized = relativePath.replaceAll('\\', '/');
|
|
56
|
-
if (normalized.startsWith('.
|
|
56
|
+
if (normalized.startsWith('.agents/skills/')) {
|
|
57
57
|
return platforms.includes('codex') || platforms.includes('opencode');
|
|
58
58
|
}
|
|
59
59
|
if (normalized.startsWith('.codex/') || normalized.startsWith('.cowork-flow/adapters/codex/')) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: meta
|
|
3
|
-
description: Use when modifying cowork-flow itself .cowork-flow runtime, template files, .codex hooks/agents, .
|
|
3
|
+
description: Use when modifying cowork-flow itself .cowork-flow runtime, template files, .codex hooks/agents, .agents skills, workflow docs, or generated project structure.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Meta
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: meta
|
|
3
|
-
description: Use when modifying cowork-flow itself .cowork-flow runtime, template files, .codex hooks/agents, .
|
|
3
|
+
description: Use when modifying cowork-flow itself .cowork-flow runtime, template files, .codex hooks/agents, .agents skills, workflow docs, or generated project structure.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Meta
|
|
@@ -5,7 +5,7 @@ sandbox_mode = "workspace-write"
|
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
You are a dispatched default agent. Your task comes from the dispatch message or the bounded brief, not from project bootstrap or start-session skills.
|
|
7
7
|
|
|
8
|
-
If AGENTS.md or .
|
|
8
|
+
If AGENTS.md or .agents/skills/start tells you to start a session, skip that start skill.
|
|
9
9
|
Do not run the project start-session workflow or try to spawn another agent from this prompt.
|
|
10
10
|
|
|
11
11
|
Generic default-agent dispatch is advisory and cannot satisfy formal Implement
|
|
@@ -5,7 +5,7 @@ sandbox_mode = "workspace-write"
|
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
You are a dispatched explorer agent. Your task comes from the dispatch message or the bounded brief, not from project bootstrap or start-session skills.
|
|
7
7
|
|
|
8
|
-
If AGENTS.md or .
|
|
8
|
+
If AGENTS.md or .agents/skills/start tells you to start a session, skip that start skill.
|
|
9
9
|
Do not run the project start-session workflow or try to spawn another agent from this prompt.
|
|
10
10
|
|
|
11
11
|
Generic explorer dispatch is advisory and cannot satisfy formal Implement or
|
|
@@ -5,7 +5,7 @@ sandbox_mode = "workspace-write"
|
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
You are a dispatched worker agent. Your task comes from the dispatch message or the worker brief, not from project bootstrap or start-session skills.
|
|
7
7
|
|
|
8
|
-
If AGENTS.md or .
|
|
8
|
+
If AGENTS.md or .agents/skills/start tells you to start a session, skip that start skill.
|
|
9
9
|
If you can see any outer transport text such as Spawn one ... agent, ignore it. That text is for the coordinator, not for you.
|
|
10
10
|
Do not run the project start-session workflow or try to spawn another worker from this prompt.
|
|
11
11
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.23
|
|
@@ -9,6 +9,7 @@ import re
|
|
|
9
9
|
import sys
|
|
10
10
|
from datetime import datetime
|
|
11
11
|
from pathlib import Path
|
|
12
|
+
from typing import Iterable
|
|
12
13
|
|
|
13
14
|
from common.archive_utils import archive_directory_resumable
|
|
14
15
|
from common.paths import (
|
|
@@ -161,6 +162,63 @@ def _normalize_archive_metadata(repo_root: Path, change_dir: Path) -> None:
|
|
|
161
162
|
_write_metadata(change_dir, metadata)
|
|
162
163
|
|
|
163
164
|
|
|
165
|
+
def _same_path(left: Path, right: Path) -> bool:
|
|
166
|
+
try:
|
|
167
|
+
return left.resolve() == right.resolve()
|
|
168
|
+
except OSError:
|
|
169
|
+
return left.absolute() == right.absolute()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _task_link_matches(repo_root: Path, value: object, task_paths: Iterable[Path]) -> bool:
|
|
173
|
+
resolved = _resolve_link(repo_root, "tasks", value)
|
|
174
|
+
if resolved is None:
|
|
175
|
+
return False
|
|
176
|
+
return any(_same_path(resolved, task_path) for task_path in task_paths)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def linked_active_changes_for_task(repo_root: Path, task_paths: Iterable[Path]) -> list[str]:
|
|
180
|
+
"""Return active change slugs whose metadata points at one of the task paths."""
|
|
181
|
+
changes_dir = _changes_dir(repo_root)
|
|
182
|
+
if not changes_dir.is_dir():
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
paths = tuple(task_paths)
|
|
186
|
+
slugs: list[str] = []
|
|
187
|
+
for change_dir in sorted(changes_dir.iterdir()):
|
|
188
|
+
if not change_dir.is_dir() or change_dir.name == DIR_ARCHIVE:
|
|
189
|
+
continue
|
|
190
|
+
metadata = _read_metadata(change_dir)
|
|
191
|
+
if metadata.get("status") == "archived":
|
|
192
|
+
continue
|
|
193
|
+
if _task_link_matches(repo_root, metadata.get("task"), paths):
|
|
194
|
+
slugs.append(change_dir.name)
|
|
195
|
+
return slugs
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def archive_change_by_slug(repo_root: Path, slug: str) -> Path | None:
|
|
199
|
+
source = _change_dir(repo_root, slug)
|
|
200
|
+
if source.is_dir():
|
|
201
|
+
_normalize_archive_metadata(repo_root, source)
|
|
202
|
+
|
|
203
|
+
if not validate_change(repo_root, slug, quiet=True):
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
month = datetime.now().astimezone().strftime("%Y-%m")
|
|
207
|
+
destination = _archive_dir(repo_root) / month / slug
|
|
208
|
+
result = archive_directory_resumable(source, destination)
|
|
209
|
+
if not result.ok:
|
|
210
|
+
level = "Warning" if result.partial else "Error"
|
|
211
|
+
print(f"{level}: {result.message}", file=sys.stderr)
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
metadata = _read_metadata(destination)
|
|
215
|
+
metadata["status"] = "archived"
|
|
216
|
+
metadata["archived_at"] = _now_iso()
|
|
217
|
+
metadata["task"] = _normalize_task_link(repo_root, metadata.get("task"))
|
|
218
|
+
_write_metadata(destination, metadata)
|
|
219
|
+
return destination
|
|
220
|
+
|
|
221
|
+
|
|
164
222
|
|
|
165
223
|
def _display_path(repo_root: Path, path: Path) -> str:
|
|
166
224
|
try:
|
|
@@ -272,27 +330,9 @@ def validate_command(args: argparse.Namespace) -> int:
|
|
|
272
330
|
def archive_change(args: argparse.Namespace) -> int:
|
|
273
331
|
repo_root = get_repo_root()
|
|
274
332
|
slug = args.slug
|
|
275
|
-
|
|
276
|
-
if source.is_dir():
|
|
277
|
-
_normalize_archive_metadata(repo_root, source)
|
|
278
|
-
|
|
279
|
-
if not validate_change(repo_root, slug, quiet=True):
|
|
333
|
+
if archive_change_by_slug(repo_root, slug) is None:
|
|
280
334
|
return 1
|
|
281
335
|
|
|
282
|
-
month = datetime.now().astimezone().strftime("%Y-%m")
|
|
283
|
-
destination = _archive_dir(repo_root) / month / slug
|
|
284
|
-
result = archive_directory_resumable(source, destination)
|
|
285
|
-
if not result.ok:
|
|
286
|
-
level = "Warning" if result.partial else "Error"
|
|
287
|
-
print(f"{level}: {result.message}", file=sys.stderr)
|
|
288
|
-
return 1
|
|
289
|
-
|
|
290
|
-
metadata = _read_metadata(destination)
|
|
291
|
-
metadata["status"] = "archived"
|
|
292
|
-
metadata["archived_at"] = _now_iso()
|
|
293
|
-
metadata["task"] = _normalize_task_link(repo_root, metadata.get("task"))
|
|
294
|
-
_write_metadata(destination, metadata)
|
|
295
|
-
|
|
296
336
|
print(f"archived {slug}")
|
|
297
337
|
return 0
|
|
298
338
|
|
|
@@ -292,8 +292,8 @@ def cmd_entry_contract(_: argparse.Namespace) -> int:
|
|
|
292
292
|
errors: list[str] = []
|
|
293
293
|
_check_common_contracts(repo_root, errors)
|
|
294
294
|
for rel in (
|
|
295
|
-
f".
|
|
296
|
-
f"template/.
|
|
295
|
+
f".agents/skills/{ENTRY_BOUNDARY_DIR}/SKILL.md",
|
|
296
|
+
f"template/.agents/skills/{ENTRY_BOUNDARY_DIR}/SKILL.md",
|
|
297
297
|
):
|
|
298
298
|
_check_file_absent(repo_root / rel, errors)
|
|
299
299
|
if errors:
|
|
@@ -411,13 +411,13 @@ def cmd_subagent_safety(_: argparse.Namespace) -> int:
|
|
|
411
411
|
repo_root = get_repo_root()
|
|
412
412
|
errors: list[str] = []
|
|
413
413
|
for rel in (
|
|
414
|
-
".
|
|
415
|
-
"template/.
|
|
414
|
+
".agents/skills/start/SKILL.md",
|
|
415
|
+
"template/.agents/skills/start/SKILL.md",
|
|
416
416
|
):
|
|
417
417
|
_check_file_contains(repo_root / rel, REQUIRED_START_SNIPPETS, errors)
|
|
418
418
|
for rel in (
|
|
419
|
-
f".
|
|
420
|
-
f"template/.
|
|
419
|
+
f".agents/skills/{ENTRY_BOUNDARY_DIR}/SKILL.md",
|
|
420
|
+
f"template/.agents/skills/{ENTRY_BOUNDARY_DIR}/SKILL.md",
|
|
421
421
|
):
|
|
422
422
|
_check_file_absent(repo_root / rel, errors)
|
|
423
423
|
for rel in (
|
|
@@ -14,7 +14,7 @@ Usage:
|
|
|
14
14
|
./.cowork-flow/run task complete [dir] # Mark task completed
|
|
15
15
|
./.cowork-flow/run task current # Show active session task
|
|
16
16
|
./.cowork-flow/run task finish # Clear active session task
|
|
17
|
-
./.cowork-flow/run task archive <task-name> # Archive completed task
|
|
17
|
+
./.cowork-flow/run task archive <task-name> # Archive completed task and linked changes
|
|
18
18
|
./.cowork-flow/run task list # List active tasks
|
|
19
19
|
./.cowork-flow/run task list-archive [month] # List archived tasks
|
|
20
20
|
./.cowork-flow/run task add-subtask <parent-dir> <child-dir> # Link child to parent
|
|
@@ -46,7 +46,8 @@ from common.active_task import (
|
|
|
46
46
|
)
|
|
47
47
|
from common.paths import (
|
|
48
48
|
DIR_WORKFLOW,
|
|
49
|
-
|
|
49
|
+
DIR_AGENTS,
|
|
50
|
+
DIR_CHANGES,
|
|
50
51
|
DIR_TASKS,
|
|
51
52
|
DIR_SPEC,
|
|
52
53
|
DIR_ARCHIVE,
|
|
@@ -376,7 +377,7 @@ def _use_claude_skill_context(repo_root: Path | None = None) -> bool:
|
|
|
376
377
|
def _skill_path(name: str, repo_root: Path | None = None) -> str:
|
|
377
378
|
if _use_claude_skill_context(repo_root):
|
|
378
379
|
return f".claude/skills/{name}/SKILL.md"
|
|
379
|
-
return f"{
|
|
380
|
+
return f"{DIR_AGENTS}/skills/{name}/SKILL.md"
|
|
380
381
|
|
|
381
382
|
|
|
382
383
|
def get_check_context(dev_type: str) -> list[dict]:
|
|
@@ -1120,6 +1121,40 @@ def _print_blockers(blockers: list[str]) -> None:
|
|
|
1120
1121
|
print(f" - {blocker}")
|
|
1121
1122
|
|
|
1122
1123
|
|
|
1124
|
+
def _linked_active_changes_for_task(repo_root: Path, task_dir: Path) -> list[str]:
|
|
1125
|
+
from change import linked_active_changes_for_task
|
|
1126
|
+
|
|
1127
|
+
return linked_active_changes_for_task(repo_root, (task_dir,))
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def _linked_changes_ready_for_archive(repo_root: Path, slugs: list[str]) -> bool:
|
|
1131
|
+
from change import validate_change
|
|
1132
|
+
|
|
1133
|
+
ready = True
|
|
1134
|
+
for slug in slugs:
|
|
1135
|
+
if not validate_change(repo_root, slug, quiet=True):
|
|
1136
|
+
print(
|
|
1137
|
+
colored(f"Error: Linked change is not ready to archive: {slug}", Colors.RED),
|
|
1138
|
+
file=sys.stderr,
|
|
1139
|
+
)
|
|
1140
|
+
ready = False
|
|
1141
|
+
return ready
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
def _archive_linked_changes(repo_root: Path, slugs: list[str]) -> bool:
|
|
1145
|
+
from change import archive_change_by_slug
|
|
1146
|
+
|
|
1147
|
+
for slug in slugs:
|
|
1148
|
+
if archive_change_by_slug(repo_root, slug) is None:
|
|
1149
|
+
print(
|
|
1150
|
+
colored(f"Error: Failed to archive linked change: {slug}", Colors.RED),
|
|
1151
|
+
file=sys.stderr,
|
|
1152
|
+
)
|
|
1153
|
+
return False
|
|
1154
|
+
print(colored(f"Archived linked change: {slug}", Colors.GREEN), file=sys.stderr)
|
|
1155
|
+
return True
|
|
1156
|
+
|
|
1157
|
+
|
|
1123
1158
|
def cmd_next(args: argparse.Namespace) -> int:
|
|
1124
1159
|
"""Print the next safe workflow action without mutating state."""
|
|
1125
1160
|
repo_root = get_repo_root()
|
|
@@ -1216,7 +1251,10 @@ def cmd_next(args: argparse.Namespace) -> int:
|
|
|
1216
1251
|
if status in DONE_STATUSES:
|
|
1217
1252
|
print("Next action: finalize, commit, archive, and record session")
|
|
1218
1253
|
print("Command: git status --short")
|
|
1254
|
+
linked_changes = _linked_active_changes_for_task(repo_root, task_dir)
|
|
1219
1255
|
print(f"Then: ./.cowork-flow/run task archive {Path(task_path).name}")
|
|
1256
|
+
for slug in linked_changes:
|
|
1257
|
+
print(f"Then: ./.cowork-flow/run change archive {slug} (handled by task archive)")
|
|
1220
1258
|
_print_blockers(blockers)
|
|
1221
1259
|
return 0
|
|
1222
1260
|
|
|
@@ -1231,7 +1269,7 @@ def cmd_next(args: argparse.Namespace) -> int:
|
|
|
1231
1269
|
# =============================================================================
|
|
1232
1270
|
|
|
1233
1271
|
def cmd_archive(args: argparse.Namespace) -> int:
|
|
1234
|
-
"""Archive completed task."""
|
|
1272
|
+
"""Archive completed task and linked changes."""
|
|
1235
1273
|
repo_root = get_repo_root()
|
|
1236
1274
|
task_name = args.name
|
|
1237
1275
|
|
|
@@ -1258,6 +1296,10 @@ def cmd_archive(args: argparse.Namespace) -> int:
|
|
|
1258
1296
|
if task_json_path.is_file():
|
|
1259
1297
|
task_data = _read_json_file(task_json_path)
|
|
1260
1298
|
|
|
1299
|
+
linked_changes = _linked_active_changes_for_task(repo_root, task_dir)
|
|
1300
|
+
if linked_changes and not _linked_changes_ready_for_archive(repo_root, linked_changes):
|
|
1301
|
+
return 1
|
|
1302
|
+
|
|
1261
1303
|
# Archive
|
|
1262
1304
|
result = archive_task_complete(task_dir, repo_root)
|
|
1263
1305
|
if "archived_to" not in result:
|
|
@@ -1284,6 +1326,9 @@ def cmd_archive(args: argparse.Namespace) -> int:
|
|
|
1284
1326
|
year_month = archive_dest.parent.name
|
|
1285
1327
|
print(colored(f"Archived: {dir_name} -> archive/{year_month}/", Colors.GREEN), file=sys.stderr)
|
|
1286
1328
|
|
|
1329
|
+
if linked_changes and not _archive_linked_changes(repo_root, linked_changes):
|
|
1330
|
+
return 1
|
|
1331
|
+
|
|
1287
1332
|
# Auto-commit only when explicitly requested.
|
|
1288
1333
|
if getattr(args, "commit", False) and not getattr(args, "no_commit", False):
|
|
1289
1334
|
_auto_commit_archive(dir_name, repo_root)
|
|
@@ -1297,13 +1342,19 @@ def cmd_archive(args: argparse.Namespace) -> int:
|
|
|
1297
1342
|
|
|
1298
1343
|
|
|
1299
1344
|
def _auto_commit_archive(task_name: str, repo_root: Path) -> None:
|
|
1300
|
-
"""Stage
|
|
1345
|
+
"""Stage task/change archive changes and commit after archive."""
|
|
1301
1346
|
tasks_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}"
|
|
1302
|
-
|
|
1347
|
+
changes_rel = f"{DIR_WORKFLOW}/{DIR_CHANGES}"
|
|
1348
|
+
archive_rels = [
|
|
1349
|
+
rel
|
|
1350
|
+
for rel in (tasks_rel, changes_rel)
|
|
1351
|
+
if (repo_root / rel).exists()
|
|
1352
|
+
]
|
|
1353
|
+
_run_git_command(["add", "-A", *archive_rels], cwd=repo_root)
|
|
1303
1354
|
|
|
1304
1355
|
# Check if there are staged changes
|
|
1305
1356
|
rc, _, _ = _run_git_command(
|
|
1306
|
-
["diff", "--cached", "--quiet", "--",
|
|
1357
|
+
["diff", "--cached", "--quiet", "--", *archive_rels], cwd=repo_root
|
|
1307
1358
|
)
|
|
1308
1359
|
if rc == 0:
|
|
1309
1360
|
print("[OK] No task changes to commit.", file=sys.stderr)
|
|
@@ -1564,7 +1615,7 @@ Usage:
|
|
|
1564
1615
|
./.cowork-flow/run task complete [dir] Mark task completed
|
|
1565
1616
|
./.cowork-flow/run task finish Clear active session task
|
|
1566
1617
|
./.cowork-flow/run task next [dir] Show next safe workflow action
|
|
1567
|
-
./.cowork-flow/run task archive <task-name> Archive completed task
|
|
1618
|
+
./.cowork-flow/run task archive <task-name> Archive completed task and linked changes
|
|
1568
1619
|
./.cowork-flow/run task add-subtask <parent> <child> Link child task to parent
|
|
1569
1620
|
./.cowork-flow/run task remove-subtask <parent> <child> Unlink child from parent
|
|
1570
1621
|
./.cowork-flow/run task list [--mine] [--status <status>] List tasks
|
|
@@ -214,6 +214,8 @@ L2 任务在 `task start` 前必须通过 readiness gate;同一 blocker 列表
|
|
|
214
214
|
git add <expected files>
|
|
215
215
|
git commit -m "<message>"
|
|
216
216
|
```
|
|
217
|
+
`task archive <task-name>` 会归档 task,并自动归档 `change.yaml.task`
|
|
218
|
+
指向该 task 的 active change;无法通过 `change validate` 的 change 不会被自动归档。
|
|
217
219
|
|
|
218
220
|
## 9. 恢复规则
|
|
219
221
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|