cowork-flow 0.0.19 → 0.0.21

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 (69) hide show
  1. package/README.md +65 -20
  2. package/package.json +1 -1
  3. package/src/cli.js +109 -2
  4. package/src/commands/init.js +61 -4
  5. package/src/commands/sync.js +10 -1
  6. package/src/lib/copy-template.js +38 -7
  7. package/src/lib/platforms.js +73 -0
  8. package/template/.agent/skills/brainstorming/SKILL.md +13 -9
  9. package/template/.agent/skills/finish-work/SKILL.md +3 -3
  10. package/template/.agent/skills/meta/SKILL.md +1 -1
  11. package/template/.agent/skills/start/SKILL.md +38 -14
  12. package/template/.agent/skills/writing-plans/SKILL.md +1 -1
  13. package/template/.claude/agents/cowork-check.md +41 -0
  14. package/template/.claude/agents/cowork-implement.md +40 -0
  15. package/template/.claude/agents/cowork-research.md +40 -0
  16. package/template/.claude/commands/cowork-check.md +27 -0
  17. package/template/.claude/commands/cowork-implement.md +27 -0
  18. package/template/.claude/commands/cowork-research.md +27 -0
  19. package/template/.claude/hooks/inject-workflow-state.py +298 -0
  20. package/template/.claude/settings.json +24 -0
  21. package/template/.claude/skills/before-dev/SKILL.md +31 -0
  22. package/template/.claude/skills/brainstorming/SKILL.md +32 -0
  23. package/template/.claude/skills/break-loop/SKILL.md +26 -0
  24. package/template/.claude/skills/check/SKILL.md +29 -0
  25. package/template/.claude/skills/continue/SKILL.md +29 -0
  26. package/template/.claude/skills/finish-work/SKILL.md +32 -0
  27. package/template/.claude/skills/meta/SKILL.md +30 -0
  28. package/template/.claude/skills/python-design/SKILL.md +27 -0
  29. package/template/.claude/skills/start/SKILL.md +78 -0
  30. package/template/.claude/skills/update-spec/SKILL.md +39 -0
  31. package/template/.claude/skills/writing-plans/SKILL.md +70 -0
  32. package/template/.codex/agents/cowork-check.toml +17 -22
  33. package/template/.codex/agents/cowork-implement.toml +18 -23
  34. package/template/.codex/agents/cowork-research.toml +14 -19
  35. package/template/.codex/agents/default.toml +27 -0
  36. package/template/.codex/agents/explorer.toml +28 -0
  37. package/template/.codex/agents/worker.toml +29 -0
  38. package/template/.codex/hooks/inject-workflow-state.py +172 -90
  39. package/template/.cowork-flow/.version +1 -1
  40. package/template/.cowork-flow/adapters/claude-code/adapter.yaml +35 -0
  41. package/template/.cowork-flow/adapters/codex/adapter.yaml +35 -0
  42. package/template/.cowork-flow/adapters/opencode/adapter.yaml +33 -0
  43. package/template/.cowork-flow/config.yaml +4 -1
  44. package/template/.cowork-flow/scripts/add_session.py +20 -7
  45. package/template/.cowork-flow/scripts/common/active_task.py +212 -1
  46. package/template/.cowork-flow/scripts/common/entry_classifier.py +122 -0
  47. package/template/.cowork-flow/scripts/common/execution_context.py +10 -17
  48. package/template/.cowork-flow/scripts/common/readiness.py +249 -0
  49. package/template/.cowork-flow/scripts/doctor.py +417 -29
  50. package/template/.cowork-flow/scripts/project_context.py +230 -0
  51. package/template/.cowork-flow/scripts/run.py +3 -0
  52. package/template/.cowork-flow/scripts/subagent.py +152 -238
  53. package/template/.cowork-flow/scripts/task.py +330 -17
  54. package/template/.cowork-flow/spec/adapter.schema.json +132 -0
  55. package/template/.cowork-flow/spec/capabilities.md +34 -0
  56. package/template/.cowork-flow/spec/entry-contract.md +45 -0
  57. package/template/.cowork-flow/spec/registry.json +54 -0
  58. package/template/.cowork-flow/spec/subagent-dispatch.md +77 -0
  59. package/template/.cowork-flow/spec/workflow-state-templates.md +66 -0
  60. package/template/.cowork-flow/workflow.md +154 -190
  61. package/template/.opencode/agents/cowork-check.md +46 -0
  62. package/template/.opencode/agents/cowork-implement.md +45 -0
  63. package/template/.opencode/agents/cowork-research.md +46 -0
  64. package/template/.opencode/commands/cowork-check.md +26 -0
  65. package/template/.opencode/commands/cowork-implement.md +26 -0
  66. package/template/.opencode/commands/cowork-research.md +26 -0
  67. package/template/.opencode/plugins/cowork-flow.js +317 -0
  68. package/template/CLAUDE.md +5 -0
  69. package/template/.agent/skills/entry-boundary/SKILL.md +0 -81
package/README.md CHANGED
@@ -23,8 +23,19 @@ cowork-flow 是一个用于新项目初始化协作流程的模板仓库。它
23
23
  ├── README.md
24
24
  └── template/
25
25
  ├── AGENTS.md
26
+ ├── CLAUDE.md
26
27
  ├── .codex/
27
28
  │ └── agents/
29
+ ├── .claude/
30
+ │ ├── settings.json
31
+ │ ├── agents/
32
+ │ ├── skills/
33
+ │ ├── commands/
34
+ │ └── hooks/
35
+ ├── .opencode/
36
+ │ ├── agents/
37
+ │ ├── commands/
38
+ │ └── plugins/
28
39
  ├── .agent/
29
40
  │ └── skills/
30
41
  └── .cowork-flow/
@@ -47,10 +58,16 @@ cowork-flow 是一个用于新项目初始化协作流程的模板仓库。它
47
58
  本地技能入口,覆盖 start、before-dev、brainstorming、writing-plans、check、finish-work、continue、meta、python-design、update-spec 和 break-loop 等协作动作。这里的 skill 应保持通用,不承载某个业务项目的一次性细节。
48
59
 
49
60
  `template/.codex/agents/`
50
- 固定角色 agent 定义,包含 `cowork-research`、`cowork-implement` 和 `cowork-check`。主会话负责计划与收口,并用 Codex `spawn_agent` 派发固定 agent;派发必须使用 `fork_turns="none"`,提示词首行使用 `Active task: <task-dir>`。
61
+ 固定角色 agent 定义,包含 `cowork-research`、`cowork-implement` 和 `cowork-check`,以及对 Codex 默认 `worker`、`default`、`explorer` 的项目级防漂移约束。主会话负责计划与收口,并通过 host adapter 派发固定 agent;正式派发必须先创建 runtime context,再把 `cowork_runtime_context_id` 和 `cowork_host_context_key` 传给子代理。
51
62
 
52
63
  `template/.codex/config.toml`、`template/.codex/hooks.json`、`template/.codex/hooks/`
53
- Codex 项目级配置与每轮上下文注入入口。hook 会读取当前 session task、`.cowork-flow/workflow.md` 中的 `workflow-state` 片段和 `codex.dispatch_mode`,把流程状态注入到当前轮对话。
64
+ Codex 项目级配置与每轮上下文注入入口。hook 会先用共享 entry classifier 判定当前输入,再读取当前 session task、`.cowork-flow/spec/workflow-state-templates.md` 中的 `workflow-state` 片段和 `codex.dispatch_mode`,把流程状态注入到当前轮对话。注入的 `codex-dispatch-mode` 只是主会话调度提示,不代表当前线程身份。
65
+
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/`。
68
+
69
+ `template/.opencode/agents/`、`template/.opencode/commands/`、`template/.opencode/plugins/`
70
+ OpenCode 项目级固定 agent、slash command 和系统上下文注入插件。插件会读取 `.cowork-flow/spec/registry.json`,注入短 contract digest。
54
71
 
55
72
  `template/.cowork-flow/`
56
73
  工作流目录,包含流程说明、任务状态、开发者工作区、项目规范、行为变更规格、实现计划和辅助脚本。`.cowork-flow/config.yaml` 只承载真实接线的 session/journal, Codex hint, and task lifecycle hook settings。
@@ -69,20 +86,20 @@ Codex 项目级配置与每轮上下文注入入口。hook 会读取当前 sessi
69
86
  使用 CLI 把模板内容安装到目标项目根目录:
70
87
 
71
88
  ```bash
72
- npx cowork-flow init ./my-project
89
+ npx cowork-flow init ./my-project --platform codex
73
90
  ```
74
91
 
75
- `init` 会显式要求开发者身份。交互式终端会提示输入;脚本或 CI 请传入:
92
+ `init` 会显式要求平台和开发者身份。平台可选 `codex`、`opencode`、`claude-code`、`all`,其中 `both` 仍作为兼容别名表示 `codex,opencode`。交互式终端会提示输入;脚本或 CI 请传入:
76
93
 
77
94
  ```bash
78
- npx cowork-flow init ./my-project --developer <your-name>
95
+ npx cowork-flow init ./my-project --platform codex --developer <your-name>
79
96
  ```
80
97
 
81
98
  也可以先全局安装:
82
99
 
83
100
  ```bash
84
101
  npm install -g cowork-flow
85
- cowork-flow init ./my-project --developer <your-name>
102
+ cowork-flow init ./my-project --platform codex --developer <your-name>
86
103
  ```
87
104
 
88
105
  初始化后优先完成这些配置:
@@ -104,27 +121,27 @@ npx cowork-flow --help
104
121
  初始化到新项目:
105
122
 
106
123
  ```bash
107
- cowork-flow init ./my-project
124
+ cowork-flow init ./my-project --platform codex
108
125
  ```
109
126
 
110
- `init` 会直接复制模板中的 `.agent/skills/`、`.codex/` 和 `.cowork-flow/`,并初始化 `.cowork-flow/.developer` 与 `.cowork-flow/workspace/<developer>/`。交互式终端会提示输入开发者名称;非交互式环境必须传入 `--developer <name>`。`init` 不会复制额外技能包。
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` 不会复制额外技能包。
111
128
 
112
129
  初始化到当前项目:
113
130
 
114
131
  ```bash
115
- cowork-flow init .
132
+ cowork-flow init . --platform opencode
116
133
  ```
117
134
 
118
135
  默认不会覆盖已有文件。需要预览时使用:
119
136
 
120
137
  ```bash
121
- cowork-flow init ./my-project --dry-run
138
+ cowork-flow init ./my-project --platform all --dry-run
122
139
  ```
123
140
 
124
141
  需要明确覆盖已有文件时使用:
125
142
 
126
143
  ```bash
127
- cowork-flow init ./my-project --force --developer <your-name>
144
+ cowork-flow init ./my-project --platform codex --force --developer <your-name>
128
145
  ```
129
146
 
130
147
  `--force` 不会静默覆盖既有 `.cowork-flow/.developer`;如需重设身份,请先显式处理旧身份文件。
@@ -142,7 +159,7 @@ cowork-flow sync .
142
159
  cowork-flow sync . --dry-run
143
160
  ```
144
161
 
145
- `sync` 默认刷新 `.agent/skills/`、`.cowork-flow/scripts/` 和 `AGENTS.md` 中的 `<!-- COWORK-FLOW:START --> ... <!-- COWORK-FLOW:END -->` 托管块,保留 `AGENTS.md` 托管块之外的项目自定义内容。`.cowork-flow/config.yaml`、`.cowork-flow/workflow.md`、`.cowork-flow/spec/`、任务、计划、变更和 workspace 记录默认受保护。只有明确传入 `--force` 时才整文件覆盖保护文件。
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` 时才整文件覆盖保护文件。
146
163
 
147
164
  ## 常用入口
148
165
 
@@ -169,7 +186,9 @@ cowork-flow sync . --dry-run
169
186
 
170
187
  ```bash
171
188
  ./.cowork-flow/run get-context
189
+ ./.cowork-flow/run project-context refresh
172
190
  ./.cowork-flow/run task list
191
+ ./.cowork-flow/run task next
173
192
  ```
174
193
 
175
194
  创建并验证行为变更:
@@ -183,24 +202,50 @@ cowork-flow sync . --dry-run
183
202
 
184
203
  ```bash
185
204
  ./.cowork-flow/run task create "<title>" --slug <task-name>
205
+ ./.cowork-flow/run task next
186
206
  ./.cowork-flow/run task start <task-dir>
187
207
  ```
188
208
 
209
+ `task next` is read-only. It reports the active task, status, blockers, and the
210
+ next safe command before the main session starts work, dispatches fixed agents,
211
+ checks, archives, or records the session.
212
+
213
+ `project-context refresh` 会创建或刷新 `.cowork-flow/project-context.md`。生成块由
214
+ 本地项目文件确定,`Manual Notes` 区保留人工补充;该文件是项目索引,不替代
215
+ `AGENTS.md`、`.cowork-flow/workflow.md` 或 `.cowork-flow/spec/`。
216
+
217
+ L2 任务的 readiness gate 会在 `task start` 前阻塞缺失的 proposal/spec/design、
218
+ 计划、任务链接、边界、假设、验收标准或 verification commands;同一 blocker
219
+ 会出现在 `task next` 输出中。
220
+
221
+ 任务状态由阶段命令推进,`task next` 只读取状态:
222
+
223
+ | 阶段 | 命令 | `task.json.status` |
224
+ | --- | --- | --- |
225
+ | 创建/计划 | `task create` | `planning` |
226
+ | 开始执行 | `task start <task-dir>` | `in_progress` |
227
+ | 进入检查 | `task review [task-dir]` | `review` |
228
+ | 检查完成 | `task complete [task-dir]` | `completed` |
229
+ | 清会话指针 | `task finish` | 不改变状态 |
230
+
189
231
  执行 plan 时使用固定 cowork agents:
190
232
 
191
233
  主会话负责创建/启动任务、维护计划与收口验证;可并行的实现或检查工作通过 Codex subagent 交给固定角色 agent。
192
234
 
193
- ```python
194
- spawn_agent(
195
- agent_type="cowork-implement",
196
- fork_turns="none",
197
- message="Active task: <task-dir>\n\n<assignment>",
198
- )
235
+ ```text
236
+ ./.cowork-flow/run subagent init --role implement --agent-type cowork-implement --execution-task-dir <task-dir> --title "<title>"
237
+ ```
238
+
239
+ 将命令返回的 prompt transport 放入子代理 prompt:
240
+
241
+ ```text
242
+ cowork_runtime_context_id: <runtime_context_id>
243
+ cowork_host_context_key: <host_context_key>
199
244
  ```
200
245
 
201
- 默认角色为 `cowork-research`、`cowork-implement`、`cowork-check`,任务上下文由 agent 根据 active task 自行加载。主会话必须用 `wait_agent` 等待结果,验收输出,再用 `list_agents` / `close_agent` 收口。
246
+ 子代理首步执行 `./.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>`;主会话验收前必须确认 runtime context 中 `status=bound` 且 `bound_context_key` 匹配。默认角色为 `cowork-research`、`cowork-implement`、`cowork-check`,任务上下文来自已绑定 runtime context,而不是 prompt 形状。
202
247
 
203
- Codex hook 启用后,每轮会自动注入 `<workflow-state>`,其中包含当前任务、状态来源和下一步流程提示。hook 不替代主会话验收;它只负责把 task 状态和 workflow gate 放进上下文。
248
+ Codex hook 启用后,每轮会自动注入 `<workflow-state>`,其中包含入口分类、当前任务、状态来源和下一步流程提示。状态文本来自 `.cowork-flow/spec/workflow-state-templates.md`;`workflow.md` 只描述流程,不内联状态片段。hook 不替代主会话验收;它只负责把 task 状态和 workflow gate 放进上下文。
204
249
 
205
250
  记录 session:
206
251
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { stdin as input, stdout as output } from 'node:process';
2
+ import { emitKeypressEvents } from 'node:readline';
2
3
  import { createInterface } from 'node:readline/promises';
3
4
 
4
5
  import { runInit } from './commands/init.js';
@@ -9,7 +10,7 @@ import { readPackageInfo } from './lib/package-info.js';
9
10
  const HELP = `cowork-flow
10
11
 
11
12
  Usage:
12
- cowork-flow init [target] [--developer <name>] [--dry-run] [--force]
13
+ cowork-flow init [target] --platform <codex|opencode|claude-code|all> [--developer <name>] [--dry-run] [--force]
13
14
  cowork-flow update
14
15
  cowork-flow sync [target] [--dry-run] [--force]
15
16
  cowork-flow --version
@@ -39,9 +40,115 @@ async function defaultPrompt(message) {
39
40
  }
40
41
  }
41
42
 
43
+ async function defaultSelectPlatforms({ message, choices, defaultSelected = [] }) {
44
+ if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== 'function') {
45
+ return null;
46
+ }
47
+
48
+ const selected = new Set(defaultSelected);
49
+ let activeIndex = 0;
50
+ let search = '';
51
+ let renderedLines = 0;
52
+
53
+ const filteredChoices = () => {
54
+ const term = search.toLowerCase();
55
+ return choices.filter((choice) => choice.label.toLowerCase().includes(term));
56
+ };
57
+
58
+ const render = () => {
59
+ const visibleChoices = filteredChoices();
60
+ if (activeIndex >= visibleChoices.length) {
61
+ activeIndex = Math.max(0, visibleChoices.length - 1);
62
+ }
63
+
64
+ if (renderedLines > 0) {
65
+ output.write(`\x1b[${renderedLines}A\x1b[0J`);
66
+ }
67
+
68
+ const selectedLabels = choices
69
+ .filter((choice) => selected.has(choice.value))
70
+ .map((choice) => choice.label);
71
+ const lines = [
72
+ `? ${message} (${choices.length} available)`,
73
+ `Selected: ${selectedLabels.length > 0 ? selectedLabels.join(', ') : '(none)'}`,
74
+ `Search: ${search || '[type to filter]'}`,
75
+ '↑↓ navigate • Space toggle • Backspace remove • Enter confirm'
76
+ ];
77
+
78
+ if (visibleChoices.length === 0) {
79
+ lines.push(' (no matches)');
80
+ } else {
81
+ for (let index = 0; index < visibleChoices.length; index += 1) {
82
+ const choice = visibleChoices[index];
83
+ const cursor = index === activeIndex ? '›' : ' ';
84
+ const marker = selected.has(choice.value) ? '◉' : '○';
85
+ const suffix = selected.has(choice.value) ? ' (selected)' : '';
86
+ lines.push(`${cursor} ${marker} ${choice.label}${suffix}`);
87
+ }
88
+ }
89
+
90
+ output.write(`${lines.join('\n')}\n`);
91
+ renderedLines = lines.length;
92
+ };
93
+
94
+ return await new Promise((resolve, reject) => {
95
+ emitKeypressEvents(input);
96
+ input.setRawMode(true);
97
+ input.resume();
98
+
99
+ const cleanup = () => {
100
+ input.off('keypress', onKeypress);
101
+ input.setRawMode(false);
102
+ output.write('\n');
103
+ };
104
+
105
+ const onKeypress = (text, key = {}) => {
106
+ const visibleChoices = filteredChoices();
107
+ if (key.ctrl && key.name === 'c') {
108
+ cleanup();
109
+ reject(new Error('Platform selection cancelled'));
110
+ return;
111
+ }
112
+ if (key.name === 'return' || key.name === 'enter') {
113
+ if (selected.size === 0 && visibleChoices[activeIndex]) {
114
+ selected.add(visibleChoices[activeIndex].value);
115
+ }
116
+ cleanup();
117
+ resolve([...selected]);
118
+ return;
119
+ }
120
+ if (key.name === 'up') {
121
+ activeIndex = Math.max(0, activeIndex - 1);
122
+ } else if (key.name === 'down') {
123
+ activeIndex = Math.min(Math.max(visibleChoices.length - 1, 0), activeIndex + 1);
124
+ } else if (key.name === 'space' && visibleChoices[activeIndex]) {
125
+ const value = visibleChoices[activeIndex].value;
126
+ if (selected.has(value)) {
127
+ selected.delete(value);
128
+ } else {
129
+ selected.add(value);
130
+ }
131
+ } else if (key.name === 'backspace') {
132
+ search = search.slice(0, -1);
133
+ activeIndex = 0;
134
+ } else if (text && text.trim() && text.length === 1) {
135
+ search += text;
136
+ activeIndex = 0;
137
+ }
138
+ render();
139
+ };
140
+
141
+ input.on('keypress', onKeypress);
142
+ render();
143
+ });
144
+ }
145
+
42
146
  export async function main(argv = process.argv.slice(2), options = {}) {
43
147
  const io = options.io ?? defaultIo();
44
148
  const prompt = Object.hasOwn(options, 'prompt') ? options.prompt : defaultPrompt;
149
+ const selectPlatforms = Object.hasOwn(options, 'selectPlatforms')
150
+ ? options.selectPlatforms
151
+ : defaultSelectPlatforms;
45
152
  const [command, ...args] = argv;
46
153
 
47
154
  try {
@@ -57,7 +164,7 @@ export async function main(argv = process.argv.slice(2), options = {}) {
57
164
  }
58
165
 
59
166
  if (command === 'init') {
60
- return await runInit(args, { io, prompt });
167
+ return await runInit(args, { io, prompt, selectPlatforms });
61
168
  }
62
169
 
63
170
  if (command === 'sync') {
@@ -7,15 +7,37 @@ import {
7
7
  summarizePlan
8
8
  } from '../lib/copy-template.js';
9
9
  import { readPackageInfo } from '../lib/package-info.js';
10
+ import {
11
+ SUPPORTED_PLATFORMS,
12
+ formatPlatformList,
13
+ parsePlatformSelection
14
+ } from '../lib/platforms.js';
10
15
 
11
16
  function parseInitArgs(args) {
12
- const options = { developer: null, dryRun: false, force: false, target: process.cwd() };
17
+ const options = {
18
+ developer: null,
19
+ dryRun: false,
20
+ force: false,
21
+ platforms: [],
22
+ target: process.cwd()
23
+ };
13
24
  for (let index = 0; index < args.length; index += 1) {
14
25
  const arg = args[index];
15
26
  if (arg === '--dry-run') {
16
27
  options.dryRun = true;
17
28
  } else if (arg === '--force') {
18
29
  options.force = true;
30
+ } else if (arg === '--platform' || arg === '--platforms') {
31
+ const value = args[index + 1];
32
+ if (!value || value.startsWith('--')) {
33
+ throw new Error('Missing value for --platform');
34
+ }
35
+ options.platforms.push(value);
36
+ index += 1;
37
+ } else if (arg.startsWith('--platform=')) {
38
+ options.platforms.push(arg.slice('--platform='.length));
39
+ } else if (arg.startsWith('--platforms=')) {
40
+ options.platforms.push(arg.slice('--platforms='.length));
19
41
  } else if (arg === '--developer') {
20
42
  const value = args[index + 1];
21
43
  if (!value || value.startsWith('--')) {
@@ -85,6 +107,38 @@ async function resolveDeveloperName(options, prompt) {
85
107
  return { existing: false, name: normalizeDeveloperName(name) };
86
108
  }
87
109
 
110
+ function platformLabel(platform) {
111
+ if (platform === 'codex') {
112
+ return 'Codex';
113
+ }
114
+ if (platform === 'opencode') {
115
+ return 'OpenCode';
116
+ }
117
+ if (platform === 'claude-code') {
118
+ return 'Claude Code';
119
+ }
120
+ return platform;
121
+ }
122
+
123
+ async function resolvePlatforms(options, selectPlatforms) {
124
+ if (options.platforms.length > 0) {
125
+ return parsePlatformSelection(options.platforms);
126
+ }
127
+
128
+ let selected = null;
129
+ if (typeof selectPlatforms === 'function') {
130
+ selected = await selectPlatforms({
131
+ message: 'Select platforms to set up',
132
+ choices: SUPPORTED_PLATFORMS.map((platform) => ({
133
+ label: platformLabel(platform),
134
+ value: platform
135
+ })),
136
+ defaultSelected: ['codex']
137
+ });
138
+ }
139
+ return parsePlatformSelection(selected);
140
+ }
141
+
88
142
  function todayIsoDate() {
89
143
  return new Date().toISOString().slice(0, 10);
90
144
  }
@@ -170,21 +224,24 @@ async function initializeDeveloper(target, developer) {
170
224
  }
171
225
  }
172
226
 
173
- export async function runInit(args, { io, prompt }) {
174
- return runInitWithOptions(args, { io, prompt });
227
+ export async function runInit(args, { io, prompt, selectPlatforms }) {
228
+ return runInitWithOptions(args, { io, prompt, selectPlatforms });
175
229
  }
176
230
 
177
- export async function runInitWithOptions(args, { io, prompt }) {
231
+ export async function runInitWithOptions(args, { io, prompt, selectPlatforms }) {
178
232
  const options = parseInitArgs(args);
233
+ const platforms = await resolvePlatforms(options, selectPlatforms);
179
234
  const developer = await resolveDeveloperName(options, prompt);
180
235
  const packageInfo = await readPackageInfo();
181
236
  const plan = await buildInitPlan(options.target, {
182
237
  force: options.force,
238
+ platforms,
183
239
  version: packageInfo.version
184
240
  });
185
241
 
186
242
  await applyPlan(plan, { dryRun: options.dryRun });
187
243
  io.writeOut(summarizePlan(plan, options.dryRun));
244
+ io.writeOut(`Platforms: ${formatPlatformList(platforms)}\n`);
188
245
 
189
246
  if (options.dryRun) {
190
247
  if (developer.existing) {
@@ -1,7 +1,13 @@
1
1
  import { resolve } from 'node:path';
2
2
 
3
- import { applyPlan, buildSyncPlan, summarizePlan } from '../lib/copy-template.js';
3
+ import {
4
+ applyPlan,
5
+ buildSyncPlan,
6
+ detectInstalledPlatforms,
7
+ summarizePlan
8
+ } from '../lib/copy-template.js';
4
9
  import { readPackageInfo } from '../lib/package-info.js';
10
+ import { formatPlatformList } from '../lib/platforms.js';
5
11
 
6
12
  function parseSyncArgs(args) {
7
13
  const options = { dryRun: false, force: false, target: process.cwd() };
@@ -22,12 +28,15 @@ function parseSyncArgs(args) {
22
28
  export async function runSync(args, { io }) {
23
29
  const options = parseSyncArgs(args);
24
30
  const packageInfo = await readPackageInfo();
31
+ const platforms = await detectInstalledPlatforms(options.target);
25
32
  const plan = await buildSyncPlan(options.target, {
26
33
  force: options.force,
34
+ platforms,
27
35
  version: packageInfo.version
28
36
  });
29
37
 
30
38
  await applyPlan(plan, { dryRun: options.dryRun });
31
39
  io.writeOut(summarizePlan(plan, options.dryRun));
40
+ io.writeOut(`Platforms: ${platforms.length > 0 ? formatPlatformList(platforms) : 'none'}\n`);
32
41
  return 0;
33
42
  }
@@ -3,6 +3,7 @@ import { access, chmod, copyFile, mkdir, readFile, readdir, stat, writeFile } fr
3
3
  import { dirname, join, relative } from 'node:path';
4
4
 
5
5
  import { templateRoot } from './paths.js';
6
+ import { shouldIncludeForPlatforms } from './platforms.js';
6
7
 
7
8
  async function pathExists(path) {
8
9
  try {
@@ -39,8 +40,13 @@ function toTemplatePath(relativePath) {
39
40
  export async function buildInitPlan(targetDir, options = {}) {
40
41
  const files = await listFiles(templateRoot);
41
42
  const actions = [];
43
+ const platforms = options.platforms ?? [];
42
44
 
43
45
  for (const file of files) {
46
+ if (!shouldIncludeForPlatforms(file, platforms)) {
47
+ continue;
48
+ }
49
+
44
50
  if (toTemplatePath(file) === '.cowork-flow/.version') {
45
51
  continue;
46
52
  }
@@ -79,6 +85,8 @@ const PROTECTED_SYNC_PREFIXES = [
79
85
 
80
86
  const SAFE_SYNC_PREFIXES = [
81
87
  '.codex/',
88
+ '.opencode/',
89
+ '.claude/',
82
90
  '.agent/skills/',
83
91
  '.cowork-flow/'
84
92
  ];
@@ -87,7 +95,8 @@ const SAFE_SYNC_FILES = new Set([
87
95
  '.cowork-flow/.gitignore',
88
96
  '.cowork-flow/.version',
89
97
  '.cowork-flow/run',
90
- '.cowork-flow/run.cmd'
98
+ '.cowork-flow/run.cmd',
99
+ '.cowork-flow/spec/workflow-state-templates.md'
91
100
  ]);
92
101
 
93
102
  const COWORK_FLOW_START = '<!-- COWORK-FLOW:START -->';
@@ -95,6 +104,9 @@ const COWORK_FLOW_END = '<!-- COWORK-FLOW:END -->';
95
104
 
96
105
  function isProtectedSyncFile(relativePath) {
97
106
  const templatePath = toTemplatePath(relativePath);
107
+ if (SAFE_SYNC_FILES.has(templatePath)) {
108
+ return false;
109
+ }
98
110
  return PROTECTED_SYNC_FILES.has(templatePath)
99
111
  || PROTECTED_SYNC_PREFIXES.some((prefix) => templatePath.startsWith(prefix));
100
112
  }
@@ -125,13 +137,13 @@ function replaceManagedBlock(targetContent, templateContent) {
125
137
  ].join('');
126
138
  }
127
139
 
128
- async function buildAgentsSyncAction({ source, destination, exists, options }) {
140
+ async function buildManagedBlockSyncAction({ source, destination, exists, options, relativePath }) {
129
141
  if (!exists || options.force) {
130
142
  return {
131
143
  action: exists ? 'update' : 'create',
132
144
  source,
133
145
  destination,
134
- relativePath: 'AGENTS.md'
146
+ relativePath
135
147
  };
136
148
  }
137
149
 
@@ -142,10 +154,10 @@ async function buildAgentsSyncAction({ source, destination, exists, options }) {
142
154
  const content = replaceManagedBlock(targetContent, templateContent);
143
155
 
144
156
  if (content === null) {
145
- return { action: 'protected', source, destination, relativePath: 'AGENTS.md' };
157
+ return { action: 'protected', source, destination, relativePath };
146
158
  }
147
159
 
148
- return { action: 'update', source, destination, relativePath: 'AGENTS.md', content };
160
+ return { action: 'update', source, destination, relativePath, content };
149
161
  }
150
162
 
151
163
  export async function buildSyncPlan(targetDir, options = {}) {
@@ -155,8 +167,13 @@ export async function buildSyncPlan(targetDir, options = {}) {
155
167
 
156
168
  const files = await listFiles(templateRoot);
157
169
  const actions = [];
170
+ const platforms = options.platforms ?? await detectInstalledPlatforms(targetDir);
158
171
 
159
172
  for (const file of files) {
173
+ if (!shouldIncludeForPlatforms(file, platforms)) {
174
+ continue;
175
+ }
176
+
160
177
  if (toTemplatePath(file) === '.cowork-flow/.version') {
161
178
  continue;
162
179
  }
@@ -164,8 +181,8 @@ export async function buildSyncPlan(targetDir, options = {}) {
164
181
  const source = join(templateRoot, file);
165
182
  const destination = join(targetDir, file);
166
183
  const exists = await pathExists(destination);
167
- if (file === 'AGENTS.md') {
168
- actions.push(await buildAgentsSyncAction({ source, destination, exists, options }));
184
+ if (file === 'AGENTS.md' || file === 'CLAUDE.md') {
185
+ actions.push(await buildManagedBlockSyncAction({ source, destination, exists, options, relativePath: file }));
169
186
  continue;
170
187
  }
171
188
 
@@ -194,6 +211,20 @@ export async function buildSyncPlan(targetDir, options = {}) {
194
211
  return actions;
195
212
  }
196
213
 
214
+ export async function detectInstalledPlatforms(targetDir) {
215
+ const platforms = [];
216
+ if (await pathExists(join(targetDir, '.codex'))) {
217
+ platforms.push('codex');
218
+ }
219
+ if (await pathExists(join(targetDir, '.opencode'))) {
220
+ platforms.push('opencode');
221
+ }
222
+ if (await pathExists(join(targetDir, '.claude')) || await pathExists(join(targetDir, 'CLAUDE.md'))) {
223
+ platforms.push('claude-code');
224
+ }
225
+ return platforms;
226
+ }
227
+
197
228
  export function summarizePlan(actions, dryRun = false) {
198
229
  const counts = { create: 0, update: 0, skip: 0, protected: 0 };
199
230
  for (const action of actions) {
@@ -0,0 +1,73 @@
1
+ export const SUPPORTED_PLATFORMS = ['codex', 'opencode', 'claude-code'];
2
+
3
+ const LEGACY_BOTH_PLATFORMS = ['codex', 'opencode'];
4
+
5
+ const PLATFORM_ALIASES = new Map([
6
+ ['all', SUPPORTED_PLATFORMS],
7
+ ['both', LEGACY_BOTH_PLATFORMS],
8
+ ['codex', ['codex']],
9
+ ['opencode', ['opencode']],
10
+ ['claude', ['claude-code']],
11
+ ['claude-code', ['claude-code']],
12
+ ['claudecode', ['claude-code']]
13
+ ]);
14
+
15
+ export function supportedPlatformMessage() {
16
+ return SUPPORTED_PLATFORMS.join(', ');
17
+ }
18
+
19
+ export function parsePlatformSelection(values) {
20
+ const rawValues = Array.isArray(values) ? values : [values];
21
+ const selected = new Set();
22
+
23
+ for (const rawValue of rawValues) {
24
+ const tokens = String(rawValue ?? '')
25
+ .toLowerCase()
26
+ .split(/[\s,;|+/]+/)
27
+ .map((token) => token.trim())
28
+ .filter(Boolean);
29
+
30
+ for (const token of tokens) {
31
+ const platforms = PLATFORM_ALIASES.get(token);
32
+ if (!platforms) {
33
+ throw new Error(`Unsupported platform: ${token}. Supported platforms: ${supportedPlatformMessage()}`);
34
+ }
35
+ for (const platform of platforms) {
36
+ selected.add(platform);
37
+ }
38
+ }
39
+ }
40
+
41
+ if (selected.size === 0) {
42
+ throw new Error(
43
+ 'Platform selection required. Run: cowork-flow init <target> --platform codex|opencode|claude-code'
44
+ );
45
+ }
46
+
47
+ return SUPPORTED_PLATFORMS.filter((platform) => selected.has(platform));
48
+ }
49
+
50
+ export function formatPlatformList(platforms) {
51
+ return platforms.join(', ');
52
+ }
53
+
54
+ export function shouldIncludeForPlatforms(relativePath, platforms) {
55
+ const normalized = relativePath.replaceAll('\\', '/');
56
+ if (normalized.startsWith('.agent/skills/')) {
57
+ return platforms.includes('codex') || platforms.includes('opencode');
58
+ }
59
+ if (normalized.startsWith('.codex/') || normalized.startsWith('.cowork-flow/adapters/codex/')) {
60
+ return platforms.includes('codex');
61
+ }
62
+ if (normalized.startsWith('.opencode/') || normalized.startsWith('.cowork-flow/adapters/opencode/')) {
63
+ return platforms.includes('opencode');
64
+ }
65
+ if (
66
+ normalized === 'CLAUDE.md'
67
+ || normalized.startsWith('.claude/')
68
+ || normalized.startsWith('.cowork-flow/adapters/claude-code/')
69
+ ) {
70
+ return platforms.includes('claude-code');
71
+ }
72
+ return true;
73
+ }