cowork-flow 0.0.37 → 0.0.38

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 (110) hide show
  1. package/README.md +27 -14
  2. package/package.json +1 -1
  3. package/src/commands/init.js +39 -28
  4. package/src/commands/sync.js +2 -2
  5. package/src/lib/asset-plan.js +127 -0
  6. package/src/lib/copy-template.js +101 -141
  7. package/src/lib/host-assets.js +231 -0
  8. package/src/lib/plan-applier.js +329 -0
  9. package/src/lib/platforms.js +15 -76
  10. package/src/lib/skill-registry.js +503 -0
  11. package/template/.claude/agents/cowork-check.md +10 -1
  12. package/template/.claude/agents/cowork-implement.md +11 -2
  13. package/template/.claude/hooks/inject-workflow-state.py +51 -254
  14. package/template/.codex/agents/cowork-check.toml +12 -3
  15. package/template/.codex/agents/cowork-implement.toml +11 -2
  16. package/template/.codex/agents/default.toml +3 -2
  17. package/template/.codex/agents/explorer.toml +3 -2
  18. package/template/.codex/agents/worker.toml +2 -1
  19. package/template/.codex/hooks/inject-workflow-state.py +46 -265
  20. package/template/.cowork-flow/.version +1 -1
  21. package/template/.cowork-flow/scripts/application/__init__.py +1 -0
  22. package/template/.cowork-flow/scripts/application/batch_execution.py +743 -0
  23. package/template/.cowork-flow/scripts/application/runtime_context_service.py +365 -0
  24. package/template/.cowork-flow/scripts/application/task_archive.py +220 -0
  25. package/template/.cowork-flow/scripts/application/task_context.py +656 -0
  26. package/template/.cowork-flow/scripts/application/task_creation.py +201 -0
  27. package/template/.cowork-flow/scripts/application/task_lifecycle.py +532 -0
  28. package/template/.cowork-flow/scripts/application/task_tree.py +179 -0
  29. package/template/.cowork-flow/scripts/commands/doctor.py +78 -57
  30. package/template/.cowork-flow/scripts/commands/party_mode_v2.py +671 -425
  31. package/template/.cowork-flow/scripts/commands/subagent.py +38 -26
  32. package/template/.cowork-flow/scripts/commands/task.py +448 -1927
  33. package/template/.cowork-flow/scripts/commands/task_archive_commands.py +195 -0
  34. package/template/.cowork-flow/scripts/commands/task_context_commands.py +223 -0
  35. package/template/.cowork-flow/scripts/commands/task_create_command.py +137 -0
  36. package/template/.cowork-flow/scripts/commands/task_navigation.py +407 -0
  37. package/template/.cowork-flow/scripts/commands/task_parser.py +342 -0
  38. package/template/.cowork-flow/scripts/commands/task_support.py +76 -0
  39. package/template/.cowork-flow/scripts/commands/task_tree_commands.py +160 -0
  40. package/template/.cowork-flow/scripts/common/core/execution_context.py +161 -85
  41. package/template/.cowork-flow/scripts/common/core/host_manifest.py +393 -0
  42. package/template/.cowork-flow/scripts/common/core/skill_registry.py +568 -0
  43. package/template/.cowork-flow/scripts/common/gates/coding_standards.py +58 -7
  44. package/template/.cowork-flow/scripts/common/gates/gates.py +323 -46
  45. package/template/.cowork-flow/scripts/common/gates/models.py +196 -0
  46. package/template/.cowork-flow/scripts/common/gates/registry.py +79 -0
  47. package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +13 -13
  48. package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +39 -22
  49. package/template/.cowork-flow/scripts/common/gates/validate_rules.py +223 -40
  50. package/template/.cowork-flow/scripts/common/git/git_context.py +216 -182
  51. package/template/.cowork-flow/scripts/common/host/__init__.py +1 -0
  52. package/template/.cowork-flow/scripts/common/host/workflow_state_hook.py +303 -0
  53. package/template/.cowork-flow/scripts/common/storage/__init__.py +1 -0
  54. package/template/.cowork-flow/scripts/common/storage/operation_log.py +72 -0
  55. package/template/.cowork-flow/scripts/common/storage/state_store.py +279 -0
  56. package/template/.cowork-flow/scripts/common/storage/unit_of_work.py +214 -0
  57. package/template/.cowork-flow/scripts/common/task/active_task.py +59 -63
  58. package/template/.cowork-flow/scripts/common/task/batch_mode.py +33 -120
  59. package/template/.cowork-flow/scripts/common/task/decision_review.py +85 -0
  60. package/template/.cowork-flow/scripts/common/task/readiness.py +9 -15
  61. package/template/.cowork-flow/scripts/common/task/task_repository.py +156 -0
  62. package/template/.cowork-flow/scripts/run.py +24 -0
  63. package/template/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
  64. package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
  65. package/template/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
  66. package/template/.cowork-flow/spec/protocols/decision-review.md +28 -0
  67. package/template/.cowork-flow/spec/protocols/review.md +34 -0
  68. package/template/.cowork-flow/spec/protocols/spec-maintenance.md +16 -0
  69. package/template/.cowork-flow/spec/protocols/tdd.md +30 -0
  70. package/template/.cowork-flow/spec/references/definition-of-done.md +4 -4
  71. package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
  72. package/template/.cowork-flow/spec/runtime/contract-registry.json +25 -0
  73. package/template/.cowork-flow/spec/runtime/host-assets.json +214 -0
  74. package/template/.cowork-flow/spec/runtime/index.md +8 -0
  75. package/template/.cowork-flow/spec/runtime/rules.json +61 -10
  76. package/template/.cowork-flow/spec/runtime/skill-registry.json +397 -0
  77. package/template/.cowork-flow/spec/schemas/host-assets.schema.json +173 -0
  78. package/template/.cowork-flow/spec/schemas/rules.schema.json +10 -1
  79. package/template/.cowork-flow/spec/schemas/skill-registry.schema.json +202 -0
  80. package/template/.cowork-flow/workflow.md +18 -13
  81. package/template/.opencode/agents/cowork-check.md +11 -0
  82. package/template/.opencode/agents/cowork-implement.md +16 -1
  83. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  84. package/template/.zcode/hooks/inject-context.js +3 -85
  85. package/template/.zcode/hooks/inject-context.selfcheck.mjs +6 -6
  86. package/template/.zcode/scaffold/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
  87. package/template/.zcode/scaffold/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
  88. package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +61 -10
  89. package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +10 -1
  90. package/template/.zcode/scaffold/.cowork-flow/workflow.md +18 -13
  91. package/template/.zcode/scaffold/AGENTS.md +18 -8
  92. package/template/AGENTS.md +1 -1
  93. package/template/skills/batch-mode/SKILL.md +140 -76
  94. package/template/skills/brainstorming/SKILL.md +2 -2
  95. package/template/skills/cowork-flow/SKILL.md +42 -0
  96. package/template/skills/party-mode/SHARED-BOUNDARIES.md +1 -1
  97. package/template/skills/party-mode/SKILL.md +48 -119
  98. package/template/.cowork-flow/scripts/common/gates/spec_validator.py +0 -53
  99. package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +0 -32
  100. package/template/skills/before-dev/SKILL.md +0 -41
  101. package/template/skills/check/SKILL.md +0 -53
  102. package/template/skills/check/simplification-guide.md +0 -47
  103. package/template/skills/continue/SKILL.md +0 -29
  104. package/template/skills/doubt-review/SKILL.md +0 -118
  105. package/template/skills/finish-work/SKILL.md +0 -32
  106. package/template/skills/party-mode-v2/SKILL.md +0 -75
  107. package/template/skills/start/SKILL.md +0 -78
  108. package/template/skills/tdd/SKILL.md +0 -74
  109. package/template/skills/update-spec/SKILL.md +0 -39
  110. package/template/skills/using-cowork-flow/SKILL.md +0 -80
package/README.md CHANGED
@@ -51,6 +51,14 @@ template/
51
51
  └── workspace/ # 开发者工作区
52
52
  ```
53
53
 
54
+ ## 架构与扩展点
55
+
56
+ - **应用服务层**:任务创建、生命周期、归档、上下文、任务树和 runtime context 编排位于 `scripts/application/`;命令层只负责参数和输出适配。
57
+ - **状态存储层**:`scripts/common/storage/` 提供显式 UTF-8、修订检查、操作日志和可恢复 Unit of Work;任务与会话写入不再直接散落在命令函数中。
58
+ - **Host Asset Manifest**:`spec/runtime/host-assets.json` 是宿主资产、平台识别、同步策略和 obsolete 迁移清单的权威来源。新增平台或资产时更新 Manifest 与 schema,不在 CLI 中新增硬编码集合。
59
+ - **事务式 init/sync**:CLI 先构建不可变 Asset Plan,在同文件系统 staging 中校验 hash/权限,再按备份清单提交;失败时逆序回滚,`.cowork-flow/.version` 最后更新。
60
+ - **共享 Hook 核心**:Codex 与 Claude Code Hook 只做宿主输入适配,工作流状态解析由 `scripts/common/host/workflow_state_hook.py` 统一实现。
61
+
54
62
  ## Skills 分发机制
55
63
 
56
64
  Skills 维护在 `template/skills/` 唯一源码,`init` 时按平台分发到对应目录:
@@ -60,7 +68,7 @@ Skills 维护在 `template/skills/` 唯一源码,`init` 时按平台分发到
60
68
  | `codex` / `opencode` | `.agents/skills/` |
61
69
  | `claude-code` | `.claude/skills/` |
62
70
 
63
- 分发动作:`before-dev`、`brainstorming`、`break-loop`、`check`、`continue`、`finish-work`、`game-design`、`meta`、`party-mode`、`party-mode-v2`、`python-design`、`start`、`tdd`、`update-spec`、`writing-plans`
71
+ 分发动作:`batch-mode`、`brainstorming`、`break-loop`、`cowork-flow`、`party-mode`、`writing-plans`
64
72
 
65
73
  ## CLI 命令
66
74
 
@@ -85,6 +93,8 @@ Skills 维护在 `template/skills/` 唯一源码,`init` 时按平台分发到
85
93
  - **自动识别**已安装 host 目录,只同步对应平台资产
86
94
  - **Skills** 从 `template/skills/` 按平台分发
87
95
  - **保护文件**:`config.yaml`、`workflow.md`、`spec/`(除 `workflow-state-templates.md`)、任务、计划、变更、workspace
96
+ - **正式版旧资产清理**:旧脚本位置、旧 adapter 资产和已废弃文件按 Host Asset Manifest 的 `obsoleteFiles` 清理;用户保护文件保持不变
97
+ - **事务恢复**:上次未完成事务会在新一轮 sync 前恢复;事务元数据缺失或损坏时 fail-closed,不在未知状态上继续写入
88
98
  - `--force` 整文件覆盖保护文件
89
99
 
90
100
  ## ZCode 插件
@@ -116,6 +126,11 @@ changes → brainstorming → read spec → plan → tasks → implement → che
116
126
  | 归档 | `task archive <name>` | `completed`(归档副本) |
117
127
  | 清会话指针 | `task finish` | 不变 |
118
128
 
129
+ > Batch 使用任务图和持久化 Host action:运行
130
+ > `task start <parent-task> --auto --approved` 获取 `next_action`,
131
+ > Host 完成真实生命周期动作后用 `task batch-record-result <batch-id> --file <result.json>`
132
+ > 回写结果;失败会暂停,可用 `task batch-resume <batch-id>` 生成新的重试动作。
133
+
119
134
  ## 常用命令
120
135
 
121
136
  ```bash
@@ -147,14 +162,9 @@ changes → brainstorming → read spec → plan → tasks → implement → che
147
162
 
148
163
  ## Party Mode
149
164
 
150
- | | v1 | v2 |
151
- |---|---|---|
152
- | 机制 | 真实子代理 roundtable | Runtime board controlled |
153
- | 默认 | `max_agents=3`、`max_rounds=5` | 由 runtime 管理 |
154
- | 交互 | Host Adapter 协调 | Board API |
155
- | 产出 | 建议、证据、分歧、验收信号 | 同上 |
156
-
157
- party-mode、party-mode-v2 是两种 advisory roundtable 模式。默认 `max_agents=3`、`max_rounds=5`(v1)。`party-mode-v2` 是 runtime board controlled advisory workflow,子代理通过 board API 交流,主持人只监控和纠偏。
165
+ `party-mode` 是唯一公开的 advisory roundtable 入口。它默认使用
166
+ runtime board controlled workflow:子代理通过 Board API 交流,主持人只执行
167
+ runtime 发出的 host-neutral action、记录执行结果并纠偏漂移。
158
168
 
159
169
  > Party Mode 只产出建议,不能推进任务状态,也不能替代 `cowork-implement` / `cowork-check`。
160
170
 
@@ -166,14 +176,17 @@ npm run release -- minor # minor
166
176
  ```
167
177
 
168
178
  **发布流程:**
169
- 1. `npm run test:all`(Node + Python + pack check)
170
- 2. `npm version` 升级版本
171
- 3. 同步版本到 `template/.cowork-flow/.version` 和 `template/.zcode/.zcode-plugin/plugin.json`
172
- 4. `git commit` + `git tag`
173
- 5. `npm publish`
179
+ 1. `npm test`、`npm run test:template`、`npm run pack:check`、`git diff --check`
180
+ 2. 稳定性变更使用 `COWORK_TEMPLATE_TEST_REPEAT=3` 和固定 `COWORK_TEMPLATE_TEST_SEED` 重复运行模板测试
181
+ 3. `npm version` 升级版本
182
+ 4. 同步版本到 `template/.cowork-flow/.version` 和 `template/.zcode/.zcode-plugin/plugin.json`
183
+ 5. `git commit` + `git tag`
184
+ 6. `npm publish`
174
185
 
175
186
  CI 需要 `NPM_TOKEN` secret。
176
187
 
188
+ Windows 上发布前使用 `run.cmd` 入口验证;POSIX shell 专属 release 用例在没有 shell 的 Windows 环境会明确跳过,不得记录为通过。
189
+
177
190
  ## 接入原则
178
191
 
179
192
  - 以目标项目事实为准,不把模板内容当成项目事实
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
1
+ import { access, readFile } from 'node:fs/promises';
2
2
  import { join, resolve } from 'node:path';
3
3
 
4
4
  import {
@@ -10,7 +10,8 @@ import { readPackageInfo } from '../lib/package-info.js';
10
10
  import {
11
11
  SUPPORTED_PLATFORMS,
12
12
  formatPlatformList,
13
- parsePlatformSelection
13
+ parsePlatformSelection,
14
+ platformLabel
14
15
  } from '../lib/platforms.js';
15
16
 
16
17
  function parseInitArgs(args) {
@@ -107,19 +108,6 @@ async function resolveDeveloperName(options, prompt) {
107
108
  return { existing: false, name: normalizeDeveloperName(name) };
108
109
  }
109
110
 
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
111
  async function resolvePlatforms(options, selectPlatforms) {
124
112
  if (options.platforms.length > 0) {
125
113
  return parsePlatformSelection(options.platforms);
@@ -199,47 +187,71 @@ function initialIndexContent(name) {
199
187
  `;
200
188
  }
201
189
 
202
- async function initializeDeveloper(target, developer) {
190
+ async function buildDeveloperActions(target, developer) {
203
191
  const workflowDir = join(target, '.cowork-flow');
204
192
  const developerFile = join(workflowDir, '.developer');
205
193
  const workspaceDir = join(workflowDir, 'workspace', developer.name);
206
194
  const journalFile = join(workspaceDir, 'journal-1.md');
207
195
  const indexFile = join(workspaceDir, 'index.md');
196
+ const actions = [];
208
197
 
209
- await mkdir(workflowDir, { recursive: true });
210
198
  if (!developer.existing) {
211
- await writeFile(
212
- developerFile,
213
- `name=${developer.name}\ninitialized_at=${new Date().toISOString()}\n`,
214
- 'utf8'
215
- );
199
+ actions.push({
200
+ action: 'create',
201
+ source: null,
202
+ destination: developerFile,
203
+ relativePath: '.cowork-flow/.developer',
204
+ content: `name=${developer.name}\ninitialized_at=${new Date().toISOString()}\n`,
205
+ targetExists: false
206
+ });
216
207
  }
217
208
 
218
- await mkdir(workspaceDir, { recursive: true });
219
209
  if (!await pathExists(journalFile)) {
220
- await writeFile(journalFile, initialJournalContent(developer.name), 'utf8');
210
+ actions.push({
211
+ action: 'create',
212
+ source: null,
213
+ destination: journalFile,
214
+ relativePath: `.cowork-flow/workspace/${developer.name}/journal-1.md`,
215
+ content: initialJournalContent(developer.name),
216
+ targetExists: false
217
+ });
221
218
  }
222
219
  if (!await pathExists(indexFile)) {
223
- await writeFile(indexFile, initialIndexContent(developer.name), 'utf8');
220
+ actions.push({
221
+ action: 'create',
222
+ source: null,
223
+ destination: indexFile,
224
+ relativePath: `.cowork-flow/workspace/${developer.name}/index.md`,
225
+ content: initialIndexContent(developer.name),
226
+ targetExists: false
227
+ });
224
228
  }
229
+ return actions;
225
230
  }
226
231
 
227
232
  export async function runInit(args, { io, prompt, selectPlatforms }) {
228
233
  return runInitWithOptions(args, { io, prompt, selectPlatforms });
229
234
  }
230
235
 
231
- export async function runInitWithOptions(args, { io, prompt, selectPlatforms }) {
236
+ export async function runInitWithOptions(
237
+ args,
238
+ { io, prompt, selectPlatforms, fileSystem }
239
+ ) {
232
240
  const options = parseInitArgs(args);
233
241
  const platforms = await resolvePlatforms(options, selectPlatforms);
234
242
  const developer = await resolveDeveloperName(options, prompt);
235
243
  const packageInfo = await readPackageInfo();
244
+ const developerActions = developer.name
245
+ ? await buildDeveloperActions(options.target, developer)
246
+ : [];
236
247
  const plan = await buildInitPlan(options.target, {
248
+ additionalActions: developerActions,
237
249
  force: options.force,
238
250
  platforms,
239
251
  version: packageInfo.version
240
252
  });
241
253
 
242
- await applyPlan(plan, { dryRun: options.dryRun });
254
+ await applyPlan(plan, { dryRun: options.dryRun, fileSystem });
243
255
  io.writeOut(summarizePlan(plan, options.dryRun));
244
256
  io.writeOut(`Platforms: ${formatPlatformList(platforms)}\n`);
245
257
 
@@ -254,7 +266,6 @@ export async function runInitWithOptions(args, { io, prompt, selectPlatforms })
254
266
  return 0;
255
267
  }
256
268
 
257
- await initializeDeveloper(options.target, developer);
258
269
  if (developer.existing) {
259
270
  io.writeOut(`Developer already initialized: ${developer.name}\n`);
260
271
  } else {
@@ -25,7 +25,7 @@ function parseSyncArgs(args) {
25
25
  return options;
26
26
  }
27
27
 
28
- export async function runSync(args, { io }) {
28
+ export async function runSync(args, { io, fileSystem }) {
29
29
  const options = parseSyncArgs(args);
30
30
  const packageInfo = await readPackageInfo();
31
31
  const platforms = await detectInstalledPlatforms(options.target);
@@ -35,7 +35,7 @@ export async function runSync(args, { io }) {
35
35
  version: packageInfo.version
36
36
  });
37
37
 
38
- await applyPlan(plan, { dryRun: options.dryRun });
38
+ await applyPlan(plan, { dryRun: options.dryRun, fileSystem });
39
39
  io.writeOut(summarizePlan(plan, options.dryRun));
40
40
  io.writeOut(`Platforms: ${platforms.length > 0 ? formatPlatformList(platforms) : 'none'}\n`);
41
41
  return 0;
@@ -0,0 +1,127 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+
5
+ const VERSION_PATH = '.cowork-flow/.version';
6
+ const MATERIALIZED_ACTIONS = new Set(['create', 'update']);
7
+
8
+ function sha256(content) {
9
+ return createHash('sha256').update(content).digest('hex');
10
+ }
11
+
12
+ function normalizeRelativePath(value) {
13
+ const normalized = String(value).replaceAll('\\', '/');
14
+ if (
15
+ normalized.startsWith('/')
16
+ || normalized === '..'
17
+ || normalized.startsWith('../')
18
+ || normalized.includes('/../')
19
+ ) {
20
+ throw new Error(`Asset plan path must stay inside the target: ${value}`);
21
+ }
22
+ return normalized;
23
+ }
24
+
25
+ function targetPolicy(action) {
26
+ if (action === 'create') return 'create';
27
+ if (action === 'update') return 'replace';
28
+ if (action === 'delete') return 'delete';
29
+ return 'preserve';
30
+ }
31
+
32
+ function rollbackStrategy(action, targetExists) {
33
+ if (action === 'skip' || action === 'protected') return 'none';
34
+ if (targetExists || action === 'update' || action === 'delete') {
35
+ return 'restore-backup';
36
+ }
37
+ return 'remove-created';
38
+ }
39
+
40
+ async function readMode(action) {
41
+ try {
42
+ const path = action.source ?? action.destination;
43
+ if (!path) return null;
44
+ const stats = await stat(path);
45
+ return stats.mode & 0o777;
46
+ } catch (error) {
47
+ if (error && error.code === 'ENOENT') {
48
+ return null;
49
+ }
50
+ throw error;
51
+ }
52
+ }
53
+
54
+ async function enrichAction(rawAction) {
55
+ const relativePath = normalizeRelativePath(rawAction.relativePath);
56
+ const action = rawAction.action;
57
+ const targetExists = rawAction.targetExists
58
+ ?? (action === 'update' || action === 'delete');
59
+ let content = null;
60
+ let mode = rawAction.mode ?? null;
61
+
62
+ if (MATERIALIZED_ACTIONS.has(action)) {
63
+ if (rawAction.content !== undefined) {
64
+ content = Buffer.from(rawAction.content, 'utf8');
65
+ if (mode === null && targetExists) {
66
+ mode = await readMode(rawAction);
67
+ }
68
+ } else {
69
+ content = await readFile(rawAction.source, 'utf8');
70
+ if (mode === null) {
71
+ mode = await readMode(rawAction);
72
+ }
73
+ }
74
+ }
75
+
76
+ const sourceHash = content === null ? null : sha256(content);
77
+ const enriched = {
78
+ ...rawAction,
79
+ relativePath,
80
+ targetExists: Boolean(targetExists),
81
+ sourceHash,
82
+ mode,
83
+ targetPolicy: targetPolicy(action),
84
+ validation: {
85
+ sha256: sourceHash,
86
+ executable: mode !== null && (mode & 0o111) !== 0
87
+ },
88
+ rollback: {
89
+ strategy: rollbackStrategy(action, Boolean(targetExists))
90
+ }
91
+ };
92
+ return deepFreeze(enriched);
93
+ }
94
+
95
+ function actionOrder(left, right) {
96
+ const leftVersion = left.relativePath === VERSION_PATH;
97
+ const rightVersion = right.relativePath === VERSION_PATH;
98
+ if (leftVersion !== rightVersion) {
99
+ return leftVersion ? 1 : -1;
100
+ }
101
+ return left.relativePath.localeCompare(right.relativePath);
102
+ }
103
+
104
+ function deepFreeze(value) {
105
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) {
106
+ return value;
107
+ }
108
+ for (const child of Object.values(value)) {
109
+ deepFreeze(child);
110
+ }
111
+ return Object.freeze(value);
112
+ }
113
+
114
+ export function planActions(planOrActions) {
115
+ return Array.isArray(planOrActions) ? planOrActions : planOrActions.actions;
116
+ }
117
+
118
+ export async function createAssetPlan({ kind, targetDir, actions }) {
119
+ const enriched = await Promise.all(actions.map(enrichAction));
120
+ enriched.sort(actionOrder);
121
+ return deepFreeze({
122
+ schemaVersion: 1,
123
+ kind,
124
+ targetDir: resolve(targetDir),
125
+ actions: enriched
126
+ });
127
+ }
@@ -1,9 +1,13 @@
1
- import { constants } from 'node:fs';
2
- import { access, chmod, copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
3
- import { dirname, join, relative } from 'node:path';
1
+ import { constants } from 'node:fs';
2
+ import { access, readFile, readdir } from 'node:fs/promises';
3
+ import { join, relative } from 'node:path';
4
4
 
5
+ import { createAssetPlan, planActions } from './asset-plan.js';
6
+ import { hostRegistry } from './host-assets.js';
7
+ import { applyAssetPlan } from './plan-applier.js';
5
8
  import { templateRoot } from './paths.js';
6
9
  import { shouldIncludeForPlatforms, skillDestinationForPlatform } from './platforms.js';
10
+ import { skillRegistry } from './skill-registry.js';
7
11
 
8
12
  async function pathExists(path) {
9
13
  try {
@@ -63,26 +67,19 @@ export async function buildInitPlan(targetDir, options = {}) {
63
67
  actions.push({ action, source, destination, relativePath: file });
64
68
  }
65
69
 
66
- // Inject per-platform skills directories from the canonical template/skills/.
67
- const skillsSrc = join(templateRoot, 'skills');
68
- if (await pathExists(skillsSrc)) {
69
- const skillEntries = await readdir(skillsSrc, { withFileTypes: true });
70
- for (const entry of skillEntries) {
71
- if (!entry.isDirectory()) continue;
72
- const skillName = entry.name;
73
- for (const platform of platforms) {
74
- const destBase = skillDestinationForPlatform(platform);
75
- if (!destBase) continue;
76
- const dest = join(targetDir, destBase, skillName, 'SKILL.md');
77
- if (seen.has(dest)) continue;
78
- seen.add(dest);
79
- actions.push({
80
- action: (await pathExists(dest)) ? 'skip' : 'create',
81
- source: join(skillsSrc, skillName, 'SKILL.md'),
82
- destination: dest,
83
- relativePath: join(destBase, skillName, 'SKILL.md')
84
- });
85
- }
70
+ for (const entry of skillRegistry.publicEntries) {
71
+ for (const platform of platforms) {
72
+ const destBase = skillDestinationForPlatform(platform);
73
+ if (!destBase) continue;
74
+ const dest = join(targetDir, destBase, entry.id, 'SKILL.md');
75
+ if (seen.has(dest)) continue;
76
+ seen.add(dest);
77
+ actions.push({
78
+ action: (await pathExists(dest)) ? 'skip' : 'create',
79
+ source: join(templateRoot, entry.source),
80
+ destination: dest,
81
+ relativePath: join(destBase, entry.id, 'SKILL.md')
82
+ });
86
83
  }
87
84
  }
88
85
 
@@ -96,71 +93,36 @@ export async function buildInitPlan(targetDir, options = {}) {
96
93
  content: `${options.version}\n`
97
94
  });
98
95
 
99
- return actions;
96
+ const additionalActions = options.additionalActions ?? [];
97
+ const overrides = new Set(
98
+ additionalActions.map((action) => action.destination)
99
+ );
100
+ return createAssetPlan({
101
+ kind: 'init',
102
+ targetDir,
103
+ actions: [
104
+ ...actions.filter((action) => !overrides.has(action.destination)),
105
+ ...additionalActions
106
+ ]
107
+ });
100
108
  }
101
109
 
102
- const PROTECTED_SYNC_FILES = new Set([
103
- '.cowork-flow/config.yaml'
104
- ]);
105
-
106
- const PROTECTED_SYNC_PREFIXES = [
107
- '.cowork-flow/spec/',
108
- '.cowork-flow/workspace/',
109
- '.cowork-flow/tasks/',
110
- '.cowork-flow/changes/',
111
- '.cowork-flow/plans/'
112
- ];
113
-
110
+ const PROTECTED_SYNC_FILES = new Set(
111
+ hostRegistry.syncPolicy.protectedFiles
112
+ );
113
+ const PROTECTED_SYNC_PREFIXES = hostRegistry.syncPolicy.protectedPrefixes;
114
+ const SAFE_SYNC_FILES = new Set(hostRegistry.syncPolicy.safeFiles);
114
115
  const SAFE_SYNC_PREFIXES = [
115
- '.codex/',
116
- '.opencode/',
117
- '.claude/',
118
- '.agents/skills/',
119
- '.cowork-flow/'
116
+ ...hostRegistry.syncPolicy.safePrefixes,
117
+ ...hostRegistry.assetPrefixes,
118
+ ...hostRegistry.skillTargets.map((target) => `${target}/`)
120
119
  ];
121
-
122
- const SAFE_SYNC_FILES = new Set([
123
- '.cowork-flow/.gitignore',
124
- '.cowork-flow/.version',
125
- '.cowork-flow/run',
126
- '.cowork-flow/run.cmd',
127
- '.cowork-flow/spec/contracts/workflow-state-templates.md'
128
- ]);
129
-
130
- const OBSOLETE_SYNC_FILES = new Set([
131
- '.cowork-flow/project-context.md',
132
- '.cowork-flow/scripts/add_session.py',
133
- '.cowork-flow/scripts/change.py',
134
- '.cowork-flow/scripts/doctor.py',
135
- '.cowork-flow/scripts/get_context.py',
136
- '.cowork-flow/scripts/get_developer.py',
137
- '.cowork-flow/scripts/init_developer.py',
138
- '.cowork-flow/scripts/party_mode_v2.py',
139
- '.cowork-flow/scripts/project_context.py',
140
- '.cowork-flow/scripts/resume.py',
141
- '.cowork-flow/scripts/subagent.py',
142
- '.cowork-flow/scripts/task.py',
143
- '.cowork-flow/scripts/common/active_task.py',
144
- '.cowork-flow/scripts/common/archive_utils.py',
145
- '.cowork-flow/scripts/common/coding_standards.py',
146
- '.cowork-flow/scripts/common/config.py',
147
- '.cowork-flow/scripts/common/developer.py',
148
- '.cowork-flow/scripts/common/execution_context.py',
149
- '.cowork-flow/scripts/common/files.py',
150
- '.cowork-flow/scripts/common/gates.py',
151
- '.cowork-flow/scripts/common/git_context.py',
152
- '.cowork-flow/scripts/common/git_snapshot.py',
153
- '.cowork-flow/scripts/common/paths.py',
154
- '.cowork-flow/scripts/common/readiness.py',
155
- '.cowork-flow/scripts/common/state_machine.py',
156
- '.cowork-flow/scripts/common/task_utils.py',
157
- '.cowork-flow/scripts/common/tdd_evidence.py',
158
- '.cowork-flow/scripts/common/test_intent.py',
159
- '.cowork-flow/scripts/common/validate_coding_standards.py',
160
- '.cowork-flow/scripts/common/validate_implementation.py',
161
- '.cowork-flow/scripts/common/validate_rules.py',
162
- '.cowork-flow/spec/contracts/entry-contract.md'
163
- ]);
120
+ const MANAGED_BLOCK_FILES = new Set(
121
+ hostRegistry.syncPolicy.managedBlockFiles
122
+ );
123
+ const OBSOLETE_SYNC_FILES = new Set(
124
+ hostRegistry.syncPolicy.obsoleteFiles
125
+ );
164
126
 
165
127
  const COWORK_FLOW_START = '<!-- COWORK-FLOW:START -->';
166
128
  const COWORK_FLOW_END = '<!-- COWORK-FLOW:END -->';
@@ -178,6 +140,7 @@ function isSafeSyncFile(relativePath) {
178
140
  const templatePath = toTemplatePath(relativePath);
179
141
  return SAFE_SYNC_FILES.has(templatePath)
180
142
  || SAFE_SYNC_PREFIXES.some((prefix) => templatePath.startsWith(prefix))
143
+ || hostRegistry.isKnownPlatformAsset(templatePath)
181
144
  || templatePath.endsWith('/.gitkeep');
182
145
  }
183
146
 
@@ -247,7 +210,7 @@ export async function buildSyncPlan(targetDir, options = {}) {
247
210
 
248
211
  const source = join(templateRoot, file);
249
212
  const exists = await pathExists(destination);
250
- if (file === 'AGENTS.md' || file === 'CLAUDE.md') {
213
+ if (MANAGED_BLOCK_FILES.has(toTemplatePath(file))) {
251
214
  actions.push(await buildManagedBlockSyncAction({ source, destination, exists, options, relativePath: file }));
252
215
  continue;
253
216
  }
@@ -266,26 +229,49 @@ export async function buildSyncPlan(targetDir, options = {}) {
266
229
  }
267
230
  }
268
231
 
269
- // Inject per-platform skills directories from the canonical template/skills/.
270
- const skillsSrc = join(templateRoot, 'skills');
271
- if (await pathExists(skillsSrc)) {
272
- const skillEntries = await readdir(skillsSrc, { withFileTypes: true });
273
- for (const entry of skillEntries) {
274
- if (!entry.isDirectory()) continue;
275
- const skillName = entry.name;
276
- for (const platform of platforms) {
277
- const destBase = skillDestinationForPlatform(platform);
278
- if (!destBase) continue;
279
- const dest = join(targetDir, destBase, skillName, 'SKILL.md');
280
- if (seen.has(dest)) continue;
281
- seen.add(dest);
282
- const destExists = await pathExists(dest);
283
- const safe = ['.agents/skills', '.claude/skills'].some((p) => dest.startsWith(join(targetDir, p)));
284
- if (destExists && (safe || options.force)) {
285
- actions.push({ action: 'update', source: join(skillsSrc, skillName, 'SKILL.md'), destination: dest, relativePath: join(destBase, skillName, 'SKILL.md') });
286
- } else if (!destExists) {
287
- actions.push({ action: 'create', source: join(skillsSrc, skillName, 'SKILL.md'), destination: dest, relativePath: join(destBase, skillName, 'SKILL.md') });
288
- }
232
+ for (const entry of skillRegistry.publicEntries) {
233
+ for (const platform of platforms) {
234
+ const destBase = skillDestinationForPlatform(platform);
235
+ if (!destBase) continue;
236
+ const dest = join(targetDir, destBase, entry.id, 'SKILL.md');
237
+ if (seen.has(dest)) continue;
238
+ seen.add(dest);
239
+ const destExists = await pathExists(dest);
240
+ const safe = hostRegistry.skillTargets.some(
241
+ (target) => dest.startsWith(join(targetDir, target))
242
+ );
243
+ if (destExists && (safe || options.force)) {
244
+ actions.push({
245
+ action: 'update',
246
+ source: join(templateRoot, entry.source),
247
+ destination: dest,
248
+ relativePath: join(destBase, entry.id, 'SKILL.md')
249
+ });
250
+ } else if (!destExists) {
251
+ actions.push({
252
+ action: 'create',
253
+ source: join(templateRoot, entry.source),
254
+ destination: dest,
255
+ relativePath: join(destBase, entry.id, 'SKILL.md')
256
+ });
257
+ }
258
+ }
259
+ }
260
+
261
+ for (const entry of skillRegistry.entries) {
262
+ if (entry.status === 'active') {
263
+ continue;
264
+ }
265
+ for (const managedPath of entry.managedPaths) {
266
+ const relativePath = managedPath.slice(0, -1);
267
+ const destination = join(targetDir, relativePath);
268
+ if (await pathExists(destination)) {
269
+ actions.push({
270
+ action: 'delete',
271
+ source: null,
272
+ destination,
273
+ relativePath
274
+ });
289
275
  }
290
276
  }
291
277
  }
@@ -305,24 +291,19 @@ export async function buildSyncPlan(targetDir, options = {}) {
305
291
  content: `${options.version}\n`
306
292
  });
307
293
 
308
- return actions;
294
+ return createAssetPlan({
295
+ kind: 'sync',
296
+ targetDir,
297
+ actions
298
+ });
309
299
  }
310
300
 
311
301
  export async function detectInstalledPlatforms(targetDir) {
312
- const platforms = [];
313
- if (await pathExists(join(targetDir, '.codex'))) {
314
- platforms.push('codex');
315
- }
316
- if (await pathExists(join(targetDir, '.opencode'))) {
317
- platforms.push('opencode');
318
- }
319
- if (await pathExists(join(targetDir, '.claude')) || await pathExists(join(targetDir, 'CLAUDE.md'))) {
320
- platforms.push('claude-code');
321
- }
322
- return platforms;
302
+ return hostRegistry.detectInstalledPlatforms(targetDir, pathExists);
323
303
  }
324
304
 
325
- export function summarizePlan(actions, dryRun = false) {
305
+ export function summarizePlan(plan, dryRun = false) {
306
+ const actions = planActions(plan);
326
307
  const counts = { create: 0, update: 0, skip: 0, protected: 0, delete: 0 };
327
308
  for (const action of actions) {
328
309
  counts[action.action] += 1;
@@ -335,27 +316,6 @@ export function summarizePlan(actions, dryRun = false) {
335
316
  return `${prefix}${createLabel}=${counts.create} ${updateLabel}=${counts.update} ${deleteLabel}=${counts.delete} skipped=${counts.skip} protected=${counts.protected}\n`;
336
317
  }
337
318
 
338
- export async function applyPlan(actions, options = {}) {
339
- if (options.dryRun) {
340
- return;
341
- }
342
-
343
- for (const item of actions) {
344
- if (item.action === 'skip' || item.action === 'protected') {
345
- continue;
346
- }
347
- if (item.action === 'delete') {
348
- await rm(item.destination, { force: true });
349
- continue;
350
- }
351
-
352
- await mkdir(dirname(item.destination), { recursive: true });
353
- if (item.content !== undefined) {
354
- await writeFile(item.destination, item.content, 'utf8');
355
- } else {
356
- await copyFile(item.source, item.destination);
357
- const sourceStats = await stat(item.source);
358
- await chmod(item.destination, sourceStats.mode & 0o777);
359
- }
360
- }
319
+ export async function applyPlan(plan, options = {}) {
320
+ await applyAssetPlan(plan, options);
361
321
  }