cowork-flow 0.0.37 → 0.0.39
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 +27 -14
- package/package.json +1 -1
- package/src/commands/init.js +39 -28
- package/src/commands/sync.js +2 -2
- package/src/lib/asset-plan.js +127 -0
- package/src/lib/copy-template.js +101 -141
- package/src/lib/host-assets.js +231 -0
- package/src/lib/plan-applier.js +329 -0
- package/src/lib/platforms.js +15 -76
- package/src/lib/skill-registry.js +503 -0
- package/template/.claude/agents/cowork-check.md +10 -1
- package/template/.claude/agents/cowork-implement.md +11 -2
- package/template/.claude/hooks/inject-workflow-state.py +51 -254
- package/template/.codex/agents/cowork-check.toml +12 -3
- package/template/.codex/agents/cowork-implement.toml +11 -2
- package/template/.codex/agents/default.toml +3 -2
- package/template/.codex/agents/explorer.toml +3 -2
- package/template/.codex/agents/worker.toml +2 -1
- package/template/.codex/hooks/inject-workflow-state.py +46 -265
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/application/__init__.py +1 -0
- package/template/.cowork-flow/scripts/application/batch_execution.py +743 -0
- package/template/.cowork-flow/scripts/application/runtime_context_service.py +365 -0
- package/template/.cowork-flow/scripts/application/task_archive.py +220 -0
- package/template/.cowork-flow/scripts/application/task_context.py +765 -0
- package/template/.cowork-flow/scripts/application/task_creation.py +201 -0
- package/template/.cowork-flow/scripts/application/task_lifecycle.py +532 -0
- package/template/.cowork-flow/scripts/application/task_tree.py +179 -0
- package/template/.cowork-flow/scripts/commands/doctor.py +78 -57
- package/template/.cowork-flow/scripts/commands/party_mode_v2.py +671 -425
- package/template/.cowork-flow/scripts/commands/subagent.py +38 -26
- package/template/.cowork-flow/scripts/commands/task.py +535 -1953
- package/template/.cowork-flow/scripts/commands/task_archive_commands.py +195 -0
- package/template/.cowork-flow/scripts/commands/task_context_commands.py +240 -0
- package/template/.cowork-flow/scripts/commands/task_create_command.py +164 -0
- package/template/.cowork-flow/scripts/commands/task_navigation.py +407 -0
- package/template/.cowork-flow/scripts/commands/task_parser.py +364 -0
- package/template/.cowork-flow/scripts/commands/task_support.py +76 -0
- package/template/.cowork-flow/scripts/commands/task_tree_commands.py +160 -0
- package/template/.cowork-flow/scripts/common/core/execution_context.py +161 -85
- package/template/.cowork-flow/scripts/common/core/host_manifest.py +393 -0
- package/template/.cowork-flow/scripts/common/core/skill_registry.py +568 -0
- package/template/.cowork-flow/scripts/common/gates/coding_standards.py +58 -7
- package/template/.cowork-flow/scripts/common/gates/gates.py +323 -46
- package/template/.cowork-flow/scripts/common/gates/models.py +196 -0
- package/template/.cowork-flow/scripts/common/gates/registry.py +79 -0
- package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +13 -13
- package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +104 -62
- package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +39 -22
- package/template/.cowork-flow/scripts/common/gates/validate_rules.py +223 -40
- package/template/.cowork-flow/scripts/common/git/git_context.py +216 -182
- package/template/.cowork-flow/scripts/common/host/__init__.py +1 -0
- package/template/.cowork-flow/scripts/common/host/workflow_state_hook.py +303 -0
- package/template/.cowork-flow/scripts/common/storage/__init__.py +1 -0
- package/template/.cowork-flow/scripts/common/storage/operation_log.py +72 -0
- package/template/.cowork-flow/scripts/common/storage/state_store.py +279 -0
- package/template/.cowork-flow/scripts/common/storage/unit_of_work.py +214 -0
- package/template/.cowork-flow/scripts/common/task/active_task.py +93 -135
- package/template/.cowork-flow/scripts/common/task/batch_mode.py +33 -120
- package/template/.cowork-flow/scripts/common/task/decision_review.py +85 -0
- package/template/.cowork-flow/scripts/common/task/readiness.py +9 -15
- package/template/.cowork-flow/scripts/common/task/task_repository.py +156 -0
- package/template/.cowork-flow/scripts/run.py +24 -0
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
- package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
- package/template/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
- package/template/.cowork-flow/spec/protocols/decision-review.md +28 -0
- package/template/.cowork-flow/spec/protocols/review.md +34 -0
- package/template/.cowork-flow/spec/protocols/spec-maintenance.md +16 -0
- package/template/.cowork-flow/spec/protocols/tdd.md +30 -0
- package/template/.cowork-flow/spec/references/definition-of-done.md +4 -4
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
- package/template/.cowork-flow/spec/runtime/contract-registry.json +25 -0
- package/template/.cowork-flow/spec/runtime/host-assets.json +214 -0
- package/template/.cowork-flow/spec/runtime/index.md +8 -0
- package/template/.cowork-flow/spec/runtime/rules.json +61 -10
- package/template/.cowork-flow/spec/runtime/skill-registry.json +397 -0
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +173 -0
- package/template/.cowork-flow/spec/schemas/rules.schema.json +10 -1
- package/template/.cowork-flow/spec/schemas/skill-registry.schema.json +202 -0
- package/template/.cowork-flow/workflow.md +18 -13
- package/template/.opencode/agents/cowork-check.md +11 -0
- package/template/.opencode/agents/cowork-implement.md +16 -1
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/hooks/inject-context.js +3 -85
- package/template/.zcode/hooks/inject-context.selfcheck.mjs +6 -6
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
- package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +61 -10
- package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +10 -1
- package/template/.zcode/scaffold/.cowork-flow/workflow.md +18 -13
- package/template/.zcode/scaffold/AGENTS.md +18 -8
- package/template/AGENTS.md +1 -1
- package/template/skills/batch-mode/SKILL.md +140 -76
- package/template/skills/brainstorming/SKILL.md +2 -2
- package/template/skills/cowork-flow/SKILL.md +42 -0
- package/template/skills/party-mode/SHARED-BOUNDARIES.md +1 -1
- package/template/skills/party-mode/SKILL.md +48 -119
- package/template/.cowork-flow/scripts/common/gates/spec_validator.py +0 -53
- package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +0 -32
- package/template/skills/before-dev/SKILL.md +0 -41
- package/template/skills/check/SKILL.md +0 -53
- package/template/skills/check/simplification-guide.md +0 -47
- package/template/skills/continue/SKILL.md +0 -29
- package/template/skills/doubt-review/SKILL.md +0 -118
- package/template/skills/finish-work/SKILL.md +0 -32
- package/template/skills/party-mode-v2/SKILL.md +0 -75
- package/template/skills/start/SKILL.md +0 -78
- package/template/skills/tdd/SKILL.md +0 -74
- package/template/skills/update-spec/SKILL.md +0 -39
- 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
|
-
分发动作:`
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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:
|
|
170
|
-
2. `
|
|
171
|
-
3.
|
|
172
|
-
4.
|
|
173
|
-
5. `
|
|
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
package/src/commands/init.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access,
|
|
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
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
package/src/commands/sync.js
CHANGED
|
@@ -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
|
+
}
|
package/src/lib/copy-template.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
import { access,
|
|
3
|
-
import {
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
'.agents/skills/',
|
|
119
|
-
'.cowork-flow/'
|
|
116
|
+
...hostRegistry.syncPolicy.safePrefixes,
|
|
117
|
+
...hostRegistry.assetPrefixes,
|
|
118
|
+
...hostRegistry.skillTargets.map((target) => `${target}/`)
|
|
120
119
|
];
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 (
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
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
|
|
294
|
+
return createAssetPlan({
|
|
295
|
+
kind: 'sync',
|
|
296
|
+
targetDir,
|
|
297
|
+
actions
|
|
298
|
+
});
|
|
309
299
|
}
|
|
310
300
|
|
|
311
301
|
export async function detectInstalledPlatforms(targetDir) {
|
|
312
|
-
|
|
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(
|
|
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(
|
|
339
|
-
|
|
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
|
}
|