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.
Files changed (26) hide show
  1. package/README.md +5 -5
  2. package/package.json +1 -1
  3. package/src/lib/copy-template.js +1 -1
  4. package/src/lib/platforms.js +1 -1
  5. package/template/{.agent → .agents}/skills/meta/SKILL.md +1 -1
  6. package/template/.claude/skills/meta/SKILL.md +1 -1
  7. package/template/.codex/agents/default.toml +1 -1
  8. package/template/.codex/agents/explorer.toml +1 -1
  9. package/template/.codex/agents/worker.toml +1 -1
  10. package/template/.cowork-flow/.version +1 -1
  11. package/template/.cowork-flow/scripts/change.py +59 -19
  12. package/template/.cowork-flow/scripts/common/__init__.py +1 -1
  13. package/template/.cowork-flow/scripts/common/paths.py +1 -1
  14. package/template/.cowork-flow/scripts/doctor.py +6 -6
  15. package/template/.cowork-flow/scripts/task.py +59 -8
  16. package/template/.cowork-flow/workflow.md +2 -0
  17. /package/template/{.agent → .agents}/skills/before-dev/SKILL.md +0 -0
  18. /package/template/{.agent → .agents}/skills/brainstorming/SKILL.md +0 -0
  19. /package/template/{.agent → .agents}/skills/break-loop/SKILL.md +0 -0
  20. /package/template/{.agent → .agents}/skills/check/SKILL.md +0 -0
  21. /package/template/{.agent → .agents}/skills/continue/SKILL.md +0 -0
  22. /package/template/{.agent → .agents}/skills/finish-work/SKILL.md +0 -0
  23. /package/template/{.agent → .agents}/skills/python-design/SKILL.md +0 -0
  24. /package/template/{.agent → .agents}/skills/start/SKILL.md +0 -0
  25. /package/template/{.agent → .agents}/skills/update-spec/SKILL.md +0 -0
  26. /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
- ├── .agent/
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/.agent/skills/`
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 不自动加载 `.agent/skills/`,所以项目技能要放进 `.claude/skills/`。
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/`、`.agent/skills/` 与 `.cowork-flow/adapters/codex/`,`opencode` 复制 `.opencode/`、`.agent/skills/` 与 `.cowork-flow/adapters/opencode/`,`claude-code` 复制 `CLAUDE.md`、`.claude/`(含 `.claude/settings.json`、`.claude/hooks/`、`.claude/skills/`)与 `.cowork-flow/adapters/claude-code/`,不复制 `.agent/skills/`。`all` 或 `--platform codex,opencode,claude-code` 同时复制三者和 `.agent/skills/`。`both` 保持兼容,只复制 Codex 和 OpenCode。它还会初始化 `.cowork-flow/.developer` 与 `.cowork-flow/workspace/<developer>/`。交互式终端会先显示 checkbox 多选界面选择平台,再提示开发者名称;非交互式环境必须传入 `--platform <codex|opencode|claude-code|all>` 与 `--developer <name>`。`init` 不会复制额外技能包。
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 时会刷新 `.agent/skills/`;Claude Code-only 项目只刷新 `.claude/settings.json`、`.claude/hooks/`、`.claude/skills/`,不会创建 `.agent/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` 时才整文件覆盖保护文件。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -87,7 +87,7 @@ const SAFE_SYNC_PREFIXES = [
87
87
  '.codex/',
88
88
  '.opencode/',
89
89
  '.claude/',
90
- '.agent/skills/',
90
+ '.agents/skills/',
91
91
  '.cowork-flow/'
92
92
  ];
93
93
 
@@ -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('.agent/skills/')) {
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, .agent skills, workflow docs, or generated project structure.
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, .agent skills, workflow docs, or generated project structure.
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 .agent/skills/start tells you to start a session, skip that start skill.
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 .agent/skills/start tells you to start a session, skip that start skill.
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 .agent/skills/start tells you to start a session, skip that start skill.
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.21
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
- source = _change_dir(repo_root, slug)
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
 
@@ -57,7 +57,7 @@ def configure_encoding() -> None:
57
57
 
58
58
  from .paths import (
59
59
  DIR_WORKFLOW,
60
- DIR_AGENT,
60
+ DIR_AGENTS,
61
61
  DIR_CHANGES,
62
62
  DIR_WORKSPACE,
63
63
  DIR_TASKS,
@@ -23,7 +23,7 @@ from pathlib import Path
23
23
 
24
24
  # Directory names
25
25
  DIR_WORKFLOW = ".cowork-flow"
26
- DIR_AGENT = ".agent"
26
+ DIR_AGENTS = ".agents"
27
27
  DIR_CHANGES = "changes"
28
28
  DIR_WORKSPACE = "workspace"
29
29
  DIR_TASKS = "tasks"
@@ -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".agent/skills/{ENTRY_BOUNDARY_DIR}/SKILL.md",
296
- f"template/.agent/skills/{ENTRY_BOUNDARY_DIR}/SKILL.md",
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
- ".agent/skills/start/SKILL.md",
415
- "template/.agent/skills/start/SKILL.md",
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".agent/skills/{ENTRY_BOUNDARY_DIR}/SKILL.md",
420
- f"template/.agent/skills/{ENTRY_BOUNDARY_DIR}/SKILL.md",
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
- DIR_AGENT,
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"{DIR_AGENT}/skills/{name}/SKILL.md"
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 .cowork-flow/tasks/ changes and commit after archive."""
1345
+ """Stage task/change archive changes and commit after archive."""
1301
1346
  tasks_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}"
1302
- _run_git_command(["add", "-A", tasks_rel], cwd=repo_root)
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", "--", tasks_rel], cwd=repo_root
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