cowork-flow 2.2.0 → 2.4.0

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 (61) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.en.md +46 -22
  3. package/README.md +10 -7
  4. package/docs/architecture.en.md +95 -0
  5. package/docs/architecture.md +7 -3
  6. package/docs/direction.en.md +71 -0
  7. package/docs/direction.md +6 -1
  8. package/docs/hosts.en.md +299 -0
  9. package/docs/hosts.md +5 -1
  10. package/docs/index.en.md +30 -0
  11. package/docs/index.md +2 -0
  12. package/docs/release.en.md +108 -0
  13. package/docs/release.md +2 -0
  14. package/package.json +2 -2
  15. package/presets/claude-code/.claude-plugin/plugin.json +1 -1
  16. package/presets/claude-code/skills/cowork-flow-bootstrap/SKILL.md +8 -2
  17. package/presets/codex/.codex-plugin/plugin.json +1 -1
  18. package/presets/codex/skills/cowork-flow-bootstrap/SKILL.md +8 -2
  19. package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
  20. package/presets/kimi-code/skills/cowork-flow-bootstrap/SKILL.md +8 -2
  21. package/presets/opencode/cowork-flow/skills/cowork-flow-bootstrap/SKILL.md +8 -2
  22. package/presets/qoder/.qoder-plugin/plugin.json +1 -1
  23. package/presets/qoder/skills/cowork-flow-bootstrap/SKILL.md +8 -2
  24. package/presets/zcode/.zcode-plugin/plugin.json +1 -1
  25. package/presets/zcode/skills/cowork-flow-bootstrap/SKILL.md +8 -2
  26. package/src/cli.js +1 -1
  27. package/src/commands/init.js +200 -16
  28. package/src/commands/registry.js +10 -7
  29. package/src/commands/source-refresh.js +5 -1
  30. package/src/commands/sync.js +4 -1
  31. package/src/lib/copy-template.js +23 -5
  32. package/src/lib/host-assets.js +119 -4
  33. package/src/lib/language.js +162 -0
  34. package/template/.cowork-flow/.version +1 -1
  35. package/template/.cowork-flow/config.yaml +8 -0
  36. package/template/.cowork-flow/scripts/adapters/cli/task_create_command.py +5 -3
  37. package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +71 -2
  38. package/template/.cowork-flow/scripts/infra/config.py +85 -0
  39. package/template/.cowork-flow/scripts/services/decision_anchor_schema.py +104 -0
  40. package/template/.cowork-flow/scripts/services/fact_view.py +21 -15
  41. package/template/.cowork-flow/scripts/services/task_context.py +13 -7
  42. package/template/.cowork-flow/scripts/services/task_creation.py +72 -30
  43. package/template/.cowork-flow/scripts/services/task_gate_profiles.py +7 -24
  44. package/template/.cowork-flow/spec/contracts/context-injection.md +5 -4
  45. package/template/.cowork-flow/spec/contracts/decision-anchor.md +21 -14
  46. package/template/.cowork-flow/spec/contracts/plan-binding.md +2 -1
  47. package/template/.cowork-flow/spec/index.md +26 -26
  48. package/template/.cowork-flow/spec/references/definition-of-done.md +26 -26
  49. package/template/.cowork-flow/spec/references/index.md +16 -16
  50. package/template/.cowork-flow/spec/references/orchestration-patterns.md +71 -71
  51. package/template/.cowork-flow/spec/references/security-checklist.md +37 -37
  52. package/template/.cowork-flow/spec/references/testing-checklist.md +32 -32
  53. package/template/.cowork-flow/spec/runtime/host-assets.json +23 -4
  54. package/template/.cowork-flow/spec/runtime/index.md +10 -12
  55. package/template/.cowork-flow/spec/schemas/host-assets.schema.json +31 -0
  56. package/template/.cowork-flow/spec/schemas/index.md +5 -5
  57. package/template/.cowork-flow/templates/plan-template.md +91 -0
  58. package/template/locales/en-US/.cowork-flow/spec/contracts/decision-anchor.md +160 -0
  59. package/template/locales/en-US/.cowork-flow/templates/plan-template.md +91 -0
  60. package/template/skills/runtime-health/scripts/doctor.py +110 -0
  61. package/template/skills/task-planning/SKILL.md +19 -92
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 ZCode。",
5
5
  "author": {
6
6
  "name": "fishlikewater",
@@ -17,12 +17,18 @@ workflow entry `./.cowork-flow/run`).
17
17
 
18
18
  ## No runtime: offer the install
19
19
 
20
- Tell the user this repository has no cowork-flow runtime, then offer:
20
+ Tell the user this repository has no cowork-flow runtime, then offer (replace
21
+ `<host>` with the host you are running in: `codex`, `claude-code`, `opencode`,
22
+ `zcode`, `qoder`, `kimi-code`, or `dsh`):
21
23
 
22
24
  ```bash
23
- npx cowork-flow project init .
25
+ npx cowork-flow project init --host <host>
24
26
  ```
25
27
 
28
+ The default artifact language is `zh-CN`. When the user asks for English task or
29
+ plan artifacts, append `--language en-US`. An existing
30
+ `.cowork-flow/config.yaml` remains authoritative.
31
+
26
32
  After init the project carries its own Skills (`.agents/skills/`), the task
27
33
  runtime (`.cowork-flow/`), and the workflow entry `./.cowork-flow/run task next`.
28
34
  Resume the work through that entry.
package/src/cli.js CHANGED
@@ -149,7 +149,7 @@ async function defaultSelectPlatforms({ message, choices, defaultSelected = [] }
149
149
  const action = applyPickerKey(state, text, key, choices);
150
150
  if (action?.outcome === 'cancel') {
151
151
  cleanup();
152
- reject(new Error('Platform selection cancelled'));
152
+ reject(new Error('Host selection cancelled'));
153
153
  return;
154
154
  }
155
155
  if (action?.outcome === 'confirm') {
@@ -1,9 +1,12 @@
1
+ import { execFile } from 'node:child_process';
1
2
  import { access, readFile } from 'node:fs/promises';
2
3
  import { join, resolve } from 'node:path';
4
+ import { promisify } from 'node:util';
3
5
 
4
6
  import {
5
7
  applyPlan,
6
8
  buildInitPlan,
9
+ detectInstalledPlatforms,
7
10
  summarizePlan
8
11
  } from '../lib/copy-template.js';
9
12
  import { readPackageInfo } from '../lib/package-info.js';
@@ -14,6 +17,16 @@ import {
14
17
  platformLabel
15
18
  } from '../lib/platforms.js';
16
19
  import { parseFlags, UsageError } from '../lib/cli-flags.js';
20
+ import {
21
+ DEFAULT_LANGUAGE,
22
+ SUPPORTED_LANGUAGES,
23
+ normalizeLanguage,
24
+ readProjectLanguage,
25
+ withLanguage
26
+ } from '../lib/language.js';
27
+ import { templateRoot } from '../lib/paths.js';
28
+
29
+ const execFileAsync = promisify(execFile);
17
30
 
18
31
  // The manifest layer reports a plain Error; re-thrown as a
19
32
  // usage error so a bad host name exits with the
@@ -29,15 +42,20 @@ function parsePlatformSelectionOrUsage(values) {
29
42
  function parseInitArgs(args) {
30
43
  const { flags, positionals } = parseFlags(args, {
31
44
  boolean: ['--dry-run', '--force'],
32
- value: ['--platform', '--platforms', '--developer'],
33
- repeatable: ['--platform', '--platforms'],
45
+ value: ['--host', '--platform', '--platforms', '--developer', '--language'],
46
+ repeatable: ['--host', '--platform', '--platforms'],
34
47
  positional: { min: 0, max: 1, name: '[target]' }
35
48
  });
36
49
  return {
37
50
  developer: flags['--developer'] ?? null,
38
51
  dryRun: Boolean(flags['--dry-run']),
39
52
  force: Boolean(flags['--force']),
40
- platforms: [...(flags['--platform'] ?? []), ...(flags['--platforms'] ?? [])],
53
+ language: flags['--language'] ?? null,
54
+ platforms: [
55
+ ...(flags['--host'] ?? []),
56
+ ...(flags['--platform'] ?? []),
57
+ ...(flags['--platforms'] ?? [])
58
+ ],
41
59
  target: positionals[0] === undefined ? process.cwd() : resolve(positionals[0])
42
60
  };
43
61
  }
@@ -57,7 +75,7 @@ async function pathExists(path) {
57
75
  function normalizeDeveloperName(value) {
58
76
  const name = String(value ?? '').trim();
59
77
  if (!name) {
60
- throw new UsageError('Developer name required. Run: cwf project init <target> --developer <name>');
78
+ throw new UsageError('Developer name required. Run: cwf project init [target] --developer <name>');
61
79
  }
62
80
  if (/[\\/]/.test(name)) {
63
81
  throw new UsageError('Developer name must not contain path separators');
@@ -65,6 +83,32 @@ function normalizeDeveloperName(value) {
65
83
  return name;
66
84
  }
67
85
 
86
+ function normalizeGitDeveloper(value) {
87
+ const name = String(value ?? '').trim();
88
+ if (!name || /[\\/]/.test(name) || (name.startsWith('<') && name.endsWith('>'))) {
89
+ return null;
90
+ }
91
+ return name;
92
+ }
93
+
94
+ // `git config user.name` reads the local repository first and falls back to the
95
+ // global config, which is the "nearest identity" this command wants. A missing
96
+ // binary, an unset value, or a directory that vanishes under us all mean "no
97
+ // git identity", never a failed init.
98
+ async function readGitDeveloper(target) {
99
+ const cwd = await pathExists(target) ? target : process.cwd();
100
+ try {
101
+ const { stdout } = await execFileAsync('git', ['config', 'user.name'], {
102
+ cwd,
103
+ timeout: 5000,
104
+ windowsHide: true
105
+ });
106
+ return normalizeGitDeveloper(stdout);
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
68
112
  async function readExistingDeveloper(target) {
69
113
  const developerFile = join(target, '.cowork-flow', '.developer');
70
114
  if (!await pathExists(developerFile)) {
@@ -78,19 +122,30 @@ async function readExistingDeveloper(target) {
78
122
  async function resolveDeveloperName(options, prompt) {
79
123
  const existing = await readExistingDeveloper(options.target);
80
124
  if (existing) {
81
- return { existing: true, name: existing };
125
+ return { existing: true, name: existing, source: 'existing' };
126
+ }
127
+
128
+ if (options.developer) {
129
+ return { existing: false, name: normalizeDeveloperName(options.developer), source: 'flag' };
82
130
  }
83
131
 
84
- if (options.dryRun && !options.developer) {
85
- return { existing: false, name: null };
132
+ const gitName = await readGitDeveloper(options.target);
133
+
134
+ if (options.dryRun) {
135
+ return { existing: false, name: gitName, source: gitName ? 'git' : null };
86
136
  }
87
137
 
88
- let name = options.developer;
89
- if (!name && typeof prompt === 'function') {
90
- name = await prompt('Developer name: ');
138
+ if (typeof prompt === 'function') {
139
+ const answer = await prompt(`Developer name${gitName ? ` [${gitName}]` : ''}: `);
140
+ const typed = String(answer ?? '').trim();
141
+ return {
142
+ existing: false,
143
+ name: normalizeDeveloperName(typed || gitName),
144
+ source: typed ? 'prompt' : 'git'
145
+ };
91
146
  }
92
147
 
93
- return { existing: false, name: normalizeDeveloperName(name) };
148
+ return { existing: false, name: normalizeDeveloperName(gitName), source: 'git' };
94
149
  }
95
150
 
96
151
  async function resolvePlatforms(options, selectPlatforms) {
@@ -98,10 +153,17 @@ async function resolvePlatforms(options, selectPlatforms) {
98
153
  return parsePlatformSelectionOrUsage(options.platforms);
99
154
  }
100
155
 
156
+ // Reuse the same marker-based detection `project sync` uses, so init and sync
157
+ // agree on what "this project already uses" means.
158
+ const detected = await detectInstalledPlatforms(options.target);
159
+ if (detected.length > 0) {
160
+ return detected;
161
+ }
162
+
101
163
  let selected = null;
102
164
  if (typeof selectPlatforms === 'function') {
103
165
  selected = await selectPlatforms({
104
- message: 'Select platforms to set up',
166
+ message: 'Select hosts to set up',
105
167
  choices: SUPPORTED_PLATFORMS.map((platform) => ({
106
168
  label: platformLabel(platform),
107
169
  value: platform
@@ -112,6 +174,104 @@ async function resolvePlatforms(options, selectPlatforms) {
112
174
  return parsePlatformSelectionOrUsage(selected);
113
175
  }
114
176
 
177
+ function normalizeLanguageOrUsage(value) {
178
+ const language = normalizeLanguage(value);
179
+ if (!language) {
180
+ throw new UsageError(
181
+ `Unsupported language: ${value}. Supported: ${SUPPORTED_LANGUAGES.join(', ')}`
182
+ );
183
+ }
184
+ return language;
185
+ }
186
+
187
+ // An existing config is authoritative: the choice is never re-prompted or
188
+ // rewritten, even by --force. A missing config prompts once, with zh-CN as the
189
+ // default for both non-interactive and dry-run runs.
190
+ async function resolveLanguage(options, prompt) {
191
+ const requested = options.language
192
+ ? normalizeLanguageOrUsage(options.language)
193
+ : null;
194
+ const existing = await readProjectLanguage(options.target);
195
+ if (existing.present) {
196
+ return {
197
+ existing: true,
198
+ configured: existing.configured,
199
+ language: existing.language,
200
+ valid: existing.valid,
201
+ raw: existing.raw,
202
+ requested,
203
+ source: 'existing'
204
+ };
205
+ }
206
+
207
+ if (requested) {
208
+ return {
209
+ existing: false,
210
+ language: requested,
211
+ requested,
212
+ source: 'flag'
213
+ };
214
+ }
215
+
216
+ if (options.dryRun || typeof prompt !== 'function') {
217
+ return {
218
+ existing: false,
219
+ language: DEFAULT_LANGUAGE,
220
+ requested,
221
+ source: 'default'
222
+ };
223
+ }
224
+
225
+ const answer = String(
226
+ await prompt(`Project language (${SUPPORTED_LANGUAGES.join('/')}) [${DEFAULT_LANGUAGE}]: `) ?? ''
227
+ ).trim();
228
+ return {
229
+ existing: false,
230
+ language: normalizeLanguageOrUsage(answer || DEFAULT_LANGUAGE),
231
+ requested,
232
+ source: answer ? 'prompt' : 'default'
233
+ };
234
+ }
235
+
236
+ function emitLanguageWarnings(io, language) {
237
+ if (!language.existing) {
238
+ return;
239
+ }
240
+ if (!language.valid) {
241
+ io.writeErr(
242
+ `Warning: unsupported language ${JSON.stringify(language.raw)} in .cowork-flow/config.yaml; using ${language.language}. Edit the file to change it.\n`
243
+ );
244
+ return;
245
+ }
246
+ if (language.requested && !language.configured) {
247
+ io.writeErr(
248
+ `Warning: .cowork-flow/config.yaml has no language setting; using ${language.language}. Add language: ${language.requested} and run cwf project sync to switch.\n`
249
+ );
250
+ return;
251
+ }
252
+ if (language.requested && language.requested !== language.language) {
253
+ io.writeErr(
254
+ `Warning: existing .cowork-flow/config.yaml language ${language.language} wins over --language ${language.requested}.\n`
255
+ );
256
+ }
257
+ }
258
+
259
+ async function buildConfigActions(target, language, existing) {
260
+ if (existing) {
261
+ return [];
262
+ }
263
+ const source = join(templateRoot, '.cowork-flow', 'config.yaml');
264
+ const content = withLanguage(await readFile(source, 'utf8'), language);
265
+ return [{
266
+ action: 'create',
267
+ source,
268
+ destination: join(target, '.cowork-flow', 'config.yaml'),
269
+ relativePath: '.cowork-flow/config.yaml',
270
+ content,
271
+ targetExists: false
272
+ }];
273
+ }
274
+
115
275
 
116
276
  async function buildDeveloperActions(target, developer) {
117
277
  const workflowDir = join(target, '.cowork-flow');
@@ -142,27 +302,43 @@ export async function runInitWithOptions(
142
302
  ) {
143
303
  const options = parseInitArgs(args);
144
304
  const platforms = await resolvePlatforms(options, selectPlatforms);
305
+ const language = await resolveLanguage(options, prompt);
306
+ emitLanguageWarnings(io, language);
145
307
  const developer = await resolveDeveloperName(options, prompt);
146
308
  const packageInfo = await readPackageInfo();
147
309
  const developerActions = developer.name
148
310
  ? await buildDeveloperActions(options.target, developer)
149
311
  : [];
312
+ const configActions = await buildConfigActions(
313
+ options.target,
314
+ language.language,
315
+ language.existing
316
+ );
150
317
  const plan = await buildInitPlan(options.target, {
151
- additionalActions: developerActions,
318
+ additionalActions: [...developerActions, ...configActions],
152
319
  force: options.force,
320
+ language: language.language,
153
321
  platforms,
154
322
  version: packageInfo.version
155
323
  });
156
324
 
157
325
  await applyPlan(plan, { dryRun: options.dryRun, fileSystem });
158
326
  io.writeOut(summarizePlan(plan, options.dryRun));
159
- io.writeOut(`Platforms: ${formatPlatformList(platforms)}\n`);
327
+ io.writeOut(`Hosts: ${formatPlatformList(platforms)}\n`);
160
328
 
161
329
  if (options.dryRun) {
162
330
  if (developer.existing) {
163
331
  io.writeOut(`dry-run preserve-existing=.cowork-flow/.developer for developer ${developer.name}\n`);
164
332
  } else if (developer.name) {
165
- io.writeOut(`dry-run would-create=.cowork-flow/.developer for developer ${developer.name}\n`);
333
+ const source = developer.source === 'git' ? ' (from git user.name)' : '';
334
+ io.writeOut(`dry-run would-create=.cowork-flow/.developer for developer ${developer.name}${source}\n`);
335
+ } else {
336
+ io.writeOut('dry-run would-skip=.cowork-flow/.developer (no developer identity; pass --developer)\n');
337
+ }
338
+ if (language.existing) {
339
+ io.writeOut(`dry-run preserve-existing=.cowork-flow/config.yaml for language ${language.language}\n`);
340
+ } else {
341
+ io.writeOut(`dry-run would-create=.cowork-flow/config.yaml for language ${language.language}\n`);
166
342
  }
167
343
  io.writeOut('Next: update AGENTS.md and .cowork-flow/config.yaml for this project.\n');
168
344
  return 0;
@@ -171,7 +347,15 @@ export async function runInitWithOptions(
171
347
  if (developer.existing) {
172
348
  io.writeOut(`Developer already initialized: ${developer.name}\n`);
173
349
  } else {
174
- io.writeOut(`Developer initialized: ${developer.name}\n`);
350
+ const source = developer.source === 'git' ? ' (from git user.name)' : '';
351
+ io.writeOut(`Developer initialized: ${developer.name}${source}\n`);
352
+ }
353
+ if (language.existing) {
354
+ const missing = language.configured ? '' : ' (default; no language key)';
355
+ io.writeOut(`Language already initialized: ${language.language}${missing}\n`);
356
+ } else {
357
+ const source = language.source === 'default' ? ' (default)' : '';
358
+ io.writeOut(`Language initialized: ${language.language}${source}\n`);
175
359
  }
176
360
  io.writeOut('Next: update AGENTS.md and .cowork-flow/config.yaml for this project.\n');
177
361
  return 0;
@@ -35,7 +35,7 @@ const FLAG_HELP = {
35
35
  const flag = (name) => ({ name, kind: 'boolean', description: FLAG_HELP[name] });
36
36
  const valueFlag = (name, valueName) => ({ name, kind: 'value', valueName, description: FLAG_HELP[name] });
37
37
 
38
- const PLATFORM_TOKENS = `${hostRegistry.platformIds.join('|')}|all`;
38
+ const HOST_TOKENS = `${hostRegistry.platformIds.join('|')}|all`;
39
39
  const HOST_NOTE = `Hosts: ${Object.entries(HOST_COMPONENTS)
40
40
  .map(([id, host]) => `${id} (${Object.keys(host.components).join(', ')})`)
41
41
  .join('; ')}.`;
@@ -51,18 +51,21 @@ export const COMMANDS = [
51
51
  {
52
52
  path: ['project', 'init'],
53
53
  summary: 'Copy the cowork-flow template into a project',
54
- usage: `project init [target] --platform <${PLATFORM_TOKENS}> [--developer <name>] [--dry-run] [--force]`,
54
+ usage: `project init [target] --host <${HOST_TOKENS}> [--developer <name>] [--language <zh-CN|en-US>] [--dry-run] [--force]`,
55
55
  flags: [
56
- { name: '--platform', kind: 'value', valueName: 'host', repeatable: true, description: 'Host to set the project up for; repeatable and comma-separated' },
57
- { name: '--platforms', kind: 'value', valueName: 'host', repeatable: true, description: 'Alias of --platform' },
58
- { name: '--developer', kind: 'value', valueName: 'name', description: 'Developer name recorded in .cowork-flow/.developer' },
56
+ { name: '--host', kind: 'value', valueName: 'host', repeatable: true, description: 'Host to set the project up for; repeatable and comma-separated, auto-detected when omitted' },
57
+ { name: '--platform', kind: 'value', valueName: 'host', repeatable: true, description: 'Alias of --host' },
58
+ { name: '--platforms', kind: 'value', valueName: 'host', repeatable: true, description: 'Alias of --host' },
59
+ { name: '--developer', kind: 'value', valueName: 'name', description: 'Developer name recorded in .cowork-flow/.developer; falls back to git user.name' },
60
+ { name: '--language', kind: 'value', valueName: 'language', description: 'Output language for task and plan artifacts; accepts zh-CN or en-US (default: zh-CN)' },
59
61
  flag('--dry-run'),
60
62
  flag('--force')
61
63
  ],
62
64
  positional: { min: 0, max: 1, name: '[target]' },
63
65
  examples: [
64
- 'cwf project init . --platform codex --developer alice',
65
- 'cwf project init ./my-project --platform all --dry-run'
66
+ 'cwf project init --host codex --developer alice',
67
+ 'cwf project init --host codex --developer alice --language en-US',
68
+ 'cwf project init ./my-project --host all --dry-run'
66
69
  ],
67
70
  run: (args, context) => runInit(args, context)
68
71
  },
@@ -9,6 +9,7 @@ import {
9
9
  formatReadinessReport,
10
10
  summarizePlan
11
11
  } from '../lib/copy-template.js';
12
+ import { readProjectLanguageStrict } from '../lib/language.js';
12
13
 
13
14
  import { parseFlags } from '../lib/cli-flags.js';
14
15
 
@@ -25,7 +26,10 @@ function parseSourceRefreshArgs(args) {
25
26
 
26
27
  export async function runSourceRefresh(args, { io, fileSystem } = {}) {
27
28
  const options = parseSourceRefreshArgs(args);
28
- const plan = await buildSourceCheckoutRefreshPlan(options.target);
29
+ const language = await readProjectLanguageStrict(options.target);
30
+ const plan = await buildSourceCheckoutRefreshPlan(options.target, {
31
+ language: language.language
32
+ });
29
33
 
30
34
  await applyPlan(plan, { dryRun: options.dryRun, fileSystem });
31
35
  io.writeOut(summarizePlan(plan, options.dryRun));
@@ -12,6 +12,7 @@ import {
12
12
  } from '../lib/copy-template.js';
13
13
  import { readPackageInfo } from '../lib/package-info.js';
14
14
  import { formatPlatformList } from '../lib/platforms.js';
15
+ import { readProjectLanguageStrict } from '../lib/language.js';
15
16
  import { parseFlags } from '../lib/cli-flags.js';
16
17
 
17
18
  function parseSyncArgs(args) {
@@ -29,9 +30,11 @@ function parseSyncArgs(args) {
29
30
  export async function runSync(args, { io, fileSystem }) {
30
31
  const options = parseSyncArgs(args);
31
32
  const packageInfo = await readPackageInfo();
33
+ const language = await readProjectLanguageStrict(options.target);
32
34
  const platforms = await detectInstalledPlatforms(options.target);
33
35
  const plan = await buildSyncPlan(options.target, {
34
36
  force: options.force,
37
+ language: language.language,
35
38
  platforms,
36
39
  version: packageInfo.version
37
40
  });
@@ -43,6 +46,6 @@ export async function runSync(args, { io, fileSystem }) {
43
46
  const readiness = await buildReadinessReport(plan, { fileSystem });
44
47
  io.writeOut(formatReadinessReport(readiness));
45
48
  }
46
- io.writeOut(`Platforms: ${platforms.length > 0 ? formatPlatformList(platforms) : 'none'}\n`);
49
+ io.writeOut(`Hosts: ${platforms.length > 0 ? formatPlatformList(platforms) : 'none'}\n`);
47
50
  return 0;
48
51
  }
@@ -153,13 +153,17 @@ async function appendSkillReadRootActions(actions, {
153
153
  }
154
154
  }
155
155
 
156
+ // The source checkout's `.cowork-flow/spec` tree is a mirror of the template,
157
+ // not a downstream project's spec tree: project-owned directories are framework
158
+ // content here too, so dev refresh rewrites every spec directory and keeps only
159
+ // task/plan state and the project config protected.
156
160
  function isSourceCheckoutProtected(relativePath) {
157
161
  const normalized = toTemplatePath(relativePath);
158
162
  if (hostRegistry.syncPolicy.protectedFiles.includes(normalized)) {
159
163
  return true;
160
164
  }
161
165
  return hostRegistry.syncPolicy.protectedPrefixes
162
- .filter((prefix) => prefix !== '.cowork-flow/spec/')
166
+ .filter((prefix) => !prefix.startsWith('.cowork-flow/spec/'))
163
167
  .some((prefix) => normalized.startsWith(prefix))
164
168
  || normalized.startsWith('.cowork-flow/.runtime/');
165
169
  }
@@ -196,12 +200,16 @@ export async function buildInitPlan(targetDir, options = {}) {
196
200
  continue;
197
201
  }
198
202
 
199
- const source = join(templateRoot, file);
203
+ const source = join(
204
+ templateRoot,
205
+ ...hostRegistry.localizedSource(templatePath, options.language).split('/')
206
+ );
200
207
  const destination = join(targetDir, file);
201
208
  seen.add(destination);
202
209
  const exists = await pathExists(destination);
210
+ const forceProtected = hostRegistry.isForceProtectedSyncFile(templatePath);
203
211
  const action = exists
204
- ? (options.force && templatePath !== '.cowork-flow/.developer' ? 'update' : 'skip')
212
+ ? (forceProtected ? 'protected' : (options.force && templatePath !== '.cowork-flow/.developer' ? 'update' : 'skip'))
205
213
  : 'create';
206
214
  actions.push({ action, source, destination, relativePath: file });
207
215
  }
@@ -448,7 +456,10 @@ export async function buildSourceCheckoutRefreshPlan(targetDir, options = {}) {
448
456
  if (!shouldIncludeSourceCheckoutRuntimeFile(templatePath)) {
449
457
  continue;
450
458
  }
451
- const source = join(templateRoot, file);
459
+ const source = join(
460
+ templateRoot,
461
+ ...hostRegistry.localizedSource(templatePath, options.language).split('/')
462
+ );
452
463
  const destination = join(targetDir, file);
453
464
  seen.add(destination);
454
465
  const exists = await pathExists(destination);
@@ -508,12 +519,19 @@ async function planSyncFile(file, { targetDir, options, platforms, seen }) {
508
519
  return null;
509
520
  }
510
521
 
511
- const source = join(templateRoot, file);
522
+ const source = join(
523
+ templateRoot,
524
+ ...hostRegistry.localizedSource(file, options.language).split('/')
525
+ );
512
526
  const exists = await pathExists(destination);
513
527
  if (hostRegistry.isManagedBlockFile(file)) {
514
528
  return await buildManagedBlockSyncAction({ source, destination, exists, options, relativePath: file });
515
529
  }
516
530
 
531
+ if (exists && hostRegistry.isForceProtectedSyncFile(file)) {
532
+ return { action: 'protected', source, destination, relativePath: file };
533
+ }
534
+
517
535
  const protectedFile = hostRegistry.isProtectedSyncFile(file) && !options.force && exists;
518
536
  const safeFile = hostRegistry.isSafeSyncFile(file);
519
537