cowork-flow 0.0.47 → 0.0.49

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 (90) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +102 -23
  3. package/package.json +8 -3
  4. package/src/cli.js +6 -0
  5. package/src/commands/init.js +0 -83
  6. package/src/commands/source-refresh.js +40 -0
  7. package/src/commands/sync.js +6 -0
  8. package/src/commands/update.js +59 -6
  9. package/src/lib/copy-template.js +200 -50
  10. package/src/lib/host-assets.js +367 -64
  11. package/src/lib/plan-applier.js +47 -0
  12. package/template/.claude/hooks/inject-workflow-state.py +7 -0
  13. package/template/.codex/hooks/inject-workflow-state.py +7 -0
  14. package/template/.codex/hooks.json +1 -1
  15. package/template/.cowork-flow/.version +1 -1
  16. package/template/.cowork-flow/config.yaml +0 -11
  17. package/template/.cowork-flow/scripts/adapters/cli/init_developer.py +0 -1
  18. package/template/.cowork-flow/scripts/adapters/cli/subagent.py +7 -4
  19. package/template/.cowork-flow/scripts/adapters/cli/task.py +8 -484
  20. package/template/.cowork-flow/scripts/adapters/cli/task_archive_commands.py +15 -134
  21. package/template/.cowork-flow/scripts/adapters/cli/task_context_commands.py +7 -1
  22. package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +388 -0
  23. package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +163 -2
  24. package/template/.cowork-flow/scripts/adapters/git/git_context.py +16 -193
  25. package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +170 -84
  26. package/template/.cowork-flow/scripts/infra/config.py +0 -17
  27. package/template/.cowork-flow/scripts/infra/developer_profile.py +0 -95
  28. package/template/.cowork-flow/scripts/infra/paths.py +0 -79
  29. package/template/.cowork-flow/scripts/infra/skill_manifest.py +23 -3
  30. package/template/.cowork-flow/scripts/infra/storage/operation_log.py +40 -0
  31. package/template/.cowork-flow/scripts/infra/storage/state_store.py +356 -1
  32. package/template/.cowork-flow/scripts/kernel/workflow_route.py +15 -15
  33. package/template/.cowork-flow/scripts/run.py +0 -5
  34. package/template/.cowork-flow/scripts/services/context_discovery.py +156 -0
  35. package/template/.cowork-flow/scripts/services/context_jsonl.py +119 -0
  36. package/template/.cowork-flow/scripts/services/context_paths.py +186 -0
  37. package/template/.cowork-flow/scripts/services/lifecycle_checks.py +186 -69
  38. package/template/.cowork-flow/scripts/services/lifecycle_policy.py +128 -0
  39. package/template/.cowork-flow/scripts/services/plan_binding.py +146 -0
  40. package/template/.cowork-flow/scripts/services/readiness.py +3 -170
  41. package/template/.cowork-flow/scripts/services/task_archive.py +115 -24
  42. package/template/.cowork-flow/scripts/services/task_context.py +103 -365
  43. package/template/.cowork-flow/scripts/services/task_creation.py +40 -5
  44. package/template/.cowork-flow/scripts/services/task_lifecycle.py +155 -18
  45. package/template/.cowork-flow/scripts/services/task_routing.py +80 -67
  46. package/template/.cowork-flow/scripts/services/task_utils.py +0 -146
  47. package/template/.cowork-flow/scripts/services/workflow_runtime.py +1 -1
  48. package/template/.cowork-flow/spec/contracts/capabilities.md +23 -0
  49. package/template/.cowork-flow/spec/contracts/decision-anchor.md +2 -2
  50. package/template/.cowork-flow/spec/contracts/error-output-as-data.md +8 -0
  51. package/template/.cowork-flow/spec/contracts/index.md +1 -0
  52. package/template/.cowork-flow/spec/contracts/party-mode-v2-board.md +20 -0
  53. package/template/.cowork-flow/spec/contracts/plan-binding.md +50 -0
  54. package/template/.cowork-flow/spec/contracts/skill-owned-actions.md +31 -7
  55. package/template/.cowork-flow/spec/guides/index.md +3 -1
  56. package/template/.cowork-flow/spec/index.md +5 -4
  57. package/template/.cowork-flow/spec/references/definition-of-done.md +2 -3
  58. package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
  59. package/template/.cowork-flow/spec/runtime/contract-registry.json +14 -0
  60. package/template/.cowork-flow/spec/runtime/host-assets.json +73 -3
  61. package/template/.cowork-flow/spec/runtime/index.md +1 -1
  62. package/template/.cowork-flow/spec/schemas/host-assets.schema.json +150 -8
  63. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  64. package/template/skills/batch-execution/SKILL.md +37 -3
  65. package/template/skills/batch-execution/scripts/batch_action.py +4 -0
  66. package/template/skills/batch-execution/scripts/batch_execution.py +120 -499
  67. package/template/skills/batch-execution/scripts/batch_recovery.py +94 -0
  68. package/template/skills/batch-execution/scripts/batch_state_machine.py +201 -0
  69. package/template/skills/batch-execution/scripts/batch_verification.py +326 -0
  70. package/template/skills/brainstorming/SKILL.md +8 -7
  71. package/template/skills/cowork-flow/SKILL.md +3 -0
  72. package/template/skills/party-mode/scripts/party_action_contract.py +113 -0
  73. package/template/skills/party-mode/scripts/party_actions.py +95 -0
  74. package/template/skills/party-mode/scripts/party_board_store.py +148 -0
  75. package/template/skills/party-mode/scripts/party_mode_v2.py +345 -317
  76. package/template/skills/party-mode/scripts/party_reports.py +177 -0
  77. package/template/skills/party-mode/scripts/party_state_machine.py +189 -0
  78. package/template/skills/runtime-health/SKILL.md +14 -2
  79. package/template/skills/runtime-health/scripts/doctor.py +278 -41
  80. package/template/skills/task-planning/SKILL.md +105 -61
  81. package/template/skills/task-review/SKILL.md +4 -4
  82. package/template/skills/task-review/scripts/review_check.py +277 -20
  83. package/template/skills/test-first/SKILL.md +18 -3
  84. package/template/skills/test-first/manifest.json +8 -4
  85. package/template/.cowork-flow/changes/archive/.gitkeep +0 -1
  86. package/template/.cowork-flow/scripts/adapters/cli/add_session.py +0 -465
  87. package/template/.cowork-flow/scripts/adapters/cli/change.py +0 -412
  88. package/template/.cowork-flow/workspace/index.md +0 -102
  89. package/template/.zcode/scaffold/AGENTS.md +0 -145
  90. package/template/.zcode/scaffold/CLAUDE.md +0 -5
@@ -4,13 +4,66 @@ import { join } from 'node:path';
4
4
  import { templateRoot } from './paths.js';
5
5
 
6
6
 
7
- export const hostAssetManifestPath = join(
8
- templateRoot,
9
- '.cowork-flow',
10
- 'spec',
11
- 'runtime',
12
- 'host-assets.json'
13
- );
7
+ export const hostAssetManifestPath = process.env.COWORK_FLOW_HOST_ASSET_MANIFEST
8
+ || join(
9
+ templateRoot,
10
+ '.cowork-flow',
11
+ 'spec',
12
+ 'runtime',
13
+ 'host-assets.json'
14
+ );
15
+
16
+ const REQUIRED_HOST_NEUTRAL_CAPABILITIES = [
17
+ 'task_action',
18
+ 'subagent_dispatch',
19
+ 'file_write',
20
+ 'party_board_action'
21
+ ];
22
+ const REQUIRED_CAPABILITY_MATRIX_HOSTS = [
23
+ 'codex',
24
+ 'claude-code',
25
+ 'opencode',
26
+ 'zcode'
27
+ ];
28
+ const CAPABILITY_STATUS_VALUES = [
29
+ 'native',
30
+ 'shim',
31
+ 'plugin',
32
+ 'external',
33
+ 'experimental',
34
+ 'unsupported'
35
+ ];
36
+ const CAPABILITY_DECLARATION_KEYS = ['status', 'fallback'];
37
+ const COMMAND_TARGET_KEYS = ['config', 'format', 'target'];
38
+ const COMMAND_TARGET_FORMATS = ['json', 'toml', 'yaml'];
39
+ const MANIFEST_KEYS = [
40
+ 'schemaVersion',
41
+ 'capabilityValues',
42
+ 'capabilityMatrix',
43
+ 'platforms',
44
+ 'excludedPrefixes',
45
+ 'syncPolicy'
46
+ ];
47
+ const PLATFORM_KEYS = [
48
+ 'id',
49
+ 'displayName',
50
+ 'aliases',
51
+ 'detectAny',
52
+ 'assetPrefixes',
53
+ 'assetFiles',
54
+ 'skillTarget',
55
+ 'adapterPath',
56
+ 'capabilities',
57
+ 'commandTargets'
58
+ ];
59
+ const SYNC_POLICY_KEYS = [
60
+ 'protectedFiles',
61
+ 'protectedPrefixes',
62
+ 'safeFiles',
63
+ 'safePrefixes',
64
+ 'managedBlockFiles',
65
+ 'obsoleteFiles'
66
+ ];
14
67
 
15
68
 
16
69
  export function loadHostAssetManifest(path = hostAssetManifestPath) {
@@ -41,6 +94,7 @@ export function createHostRegistry(manifest) {
41
94
  }
42
95
  }
43
96
  const platformIds = platforms.map((platform) => platform.id);
97
+ const capabilityMatrix = normalizeCapabilityMatrix(manifest.capabilityMatrix);
44
98
  const syncPolicy = {
45
99
  protectedFiles: manifest.syncPolicy.protectedFiles.map(normalizePath),
46
100
  protectedPrefixes: manifest.syncPolicy.protectedPrefixes.map(normalizePath),
@@ -49,6 +103,20 @@ export function createHostRegistry(manifest) {
49
103
  managedBlockFiles: manifest.syncPolicy.managedBlockFiles.map(normalizePath),
50
104
  obsoleteFiles: manifest.syncPolicy.obsoleteFiles.map(normalizePath)
51
105
  };
106
+ const assetPrefixes = unique(
107
+ platforms.flatMap((platform) => platform.assetPrefixes)
108
+ );
109
+ const skillTargets = unique(
110
+ platforms
111
+ .map((platform) => platform.skillTarget)
112
+ .filter(
113
+ (value) => typeof value === 'string' && value.length > 0
114
+ )
115
+ .map(normalizePath)
116
+ );
117
+ const protectedFiles = new Set(syncPolicy.protectedFiles);
118
+ const safeFiles = new Set(syncPolicy.safeFiles);
119
+ const managedBlockFiles = new Set(syncPolicy.managedBlockFiles);
52
120
 
53
121
  function parsePlatformSelection(values) {
54
122
  const rawValues = Array.isArray(values) ? values : [values];
@@ -92,20 +160,57 @@ export function createHostRegistry(manifest) {
92
160
  ) {
93
161
  return false;
94
162
  }
95
- const owners = platforms.filter((platform) => (
96
- platform.assetFiles.includes(normalized)
97
- || platform.assetPrefixes.some(
98
- (prefix) => normalized.startsWith(prefix)
99
- )
100
- ));
163
+ const owners = assetOwners(normalized);
101
164
  if (owners.length === 0) {
102
165
  return true;
103
166
  }
104
167
  return owners.some(
105
- (platform) => selectedPlatforms.includes(platform.id)
168
+ (platformId) => selectedPlatforms.includes(platformId)
106
169
  );
107
170
  }
108
171
 
172
+ function assetOwners(relativePath) {
173
+ const normalized = normalizePath(relativePath);
174
+ return platforms
175
+ .filter((platform) => ownsAsset(platform, normalized))
176
+ .map((platform) => platform.id);
177
+ }
178
+
179
+ function isKnownPlatformAsset(relativePath) {
180
+ return assetOwners(relativePath).length > 0;
181
+ }
182
+
183
+ function isSafeSyncFile(relativePath) {
184
+ const normalized = normalizePath(relativePath);
185
+ return safeFiles.has(normalized)
186
+ || syncPolicy.safePrefixes.some((prefix) => normalized.startsWith(prefix))
187
+ || assetPrefixes.some((prefix) => normalized.startsWith(prefix))
188
+ || skillTargets.some((target) => normalized.startsWith(`${target}/`))
189
+ || normalized.endsWith('/.gitkeep');
190
+ }
191
+
192
+ function isProtectedSyncFile(relativePath) {
193
+ const normalized = normalizePath(relativePath);
194
+ if (safeFiles.has(normalized)) {
195
+ return false;
196
+ }
197
+ return protectedFiles.has(normalized)
198
+ || syncPolicy.protectedPrefixes.some((prefix) => normalized.startsWith(prefix));
199
+ }
200
+
201
+ function isManagedBlockFile(relativePath) {
202
+ return managedBlockFiles.has(normalizePath(relativePath));
203
+ }
204
+
205
+ function obsoleteSyncFiles() {
206
+ return [...syncPolicy.obsoleteFiles].sort((left, right) => {
207
+ if (left.length !== right.length) {
208
+ return left.length - right.length;
209
+ }
210
+ return left.localeCompare(right);
211
+ });
212
+ }
213
+
109
214
  async function detectInstalledPlatforms(targetDir, pathExists) {
110
215
  const installed = [];
111
216
  for (const platform of platforms) {
@@ -123,20 +228,13 @@ export function createHostRegistry(manifest) {
123
228
  manifest,
124
229
  platforms,
125
230
  platformIds,
231
+ capabilityMatrix,
126
232
  syncPolicy,
127
- assetPrefixes: unique(
128
- platforms.flatMap((platform) => platform.assetPrefixes)
129
- ),
130
- skillTargets: unique(
131
- platforms
132
- .map((platform) => platform.skillTarget)
133
- .filter(
134
- (value) => typeof value === 'string' && value.length > 0
135
- )
136
- .map(normalizePath)
137
- ),
233
+ assetPrefixes,
234
+ skillTargets,
138
235
  parsePlatformSelection,
139
236
  shouldInclude,
237
+ assetOwners,
140
238
  detectInstalledPlatforms,
141
239
  platform(platformId) {
142
240
  return byId.get(platformId) ?? null;
@@ -144,78 +242,276 @@ export function createHostRegistry(manifest) {
144
242
  platformLabel(platformId) {
145
243
  return byId.get(platformId)?.displayName ?? platformId;
146
244
  },
245
+ hostCapability(hostId, capability) {
246
+ return capabilityMatrix.hosts[hostId]?.[capability] ?? null;
247
+ },
147
248
  skillDestination(platformId) {
148
249
  return byId.get(platformId)?.skillTarget ?? null;
149
250
  },
150
- isKnownPlatformAsset(relativePath) {
151
- const normalized = normalizePath(relativePath);
152
- return platforms.some((platform) => (
153
- platform.assetFiles.includes(normalized)
154
- || platform.assetPrefixes.some(
155
- (prefix) => normalized.startsWith(prefix)
156
- )
157
- ));
158
- }
251
+ isKnownPlatformAsset,
252
+ isSafeSyncFile,
253
+ isProtectedSyncFile,
254
+ isManagedBlockFile,
255
+ obsoleteSyncFiles
159
256
  };
160
257
  }
161
258
 
162
259
 
260
+ function ownsAsset(platform, normalizedPath) {
261
+ return platform.assetFiles.includes(normalizedPath)
262
+ || platform.assetPrefixes.some(
263
+ (prefix) => normalizedPath.startsWith(prefix)
264
+ );
265
+ }
266
+
267
+
163
268
  function validateManifest(manifest) {
164
269
  if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
165
270
  throw new Error('Host Asset Manifest must be an object');
166
271
  }
272
+ assertKnownKeys(manifest, MANIFEST_KEYS, 'Host Asset Manifest');
167
273
  if (manifest.schemaVersion !== 1) {
168
274
  throw new Error(
169
275
  `Unsupported Host Asset Manifest schemaVersion: ${manifest.schemaVersion}`
170
276
  );
171
277
  }
278
+ const allowed = validateCapabilityValues(manifest.capabilityValues);
172
279
  if (!Array.isArray(manifest.platforms) || manifest.platforms.length === 0) {
173
280
  throw new Error(
174
281
  'Host Asset Manifest platforms must be a non-empty array'
175
282
  );
176
283
  }
177
- if (!Array.isArray(manifest.excludedPrefixes)) {
284
+ const platformIds = [];
285
+ const seenPlatformIds = new Set();
286
+ const aliases = new Map();
287
+ for (const platform of manifest.platforms) {
288
+ validatePlatform(platform, allowed, seenPlatformIds, aliases);
289
+ platformIds.push(platform.id);
290
+ }
291
+ validateStringArray(
292
+ manifest.excludedPrefixes,
293
+ 'Host Asset Manifest excludedPrefixes'
294
+ );
295
+ validateCapabilityMatrix(manifest.capabilityMatrix, allowed, platformIds);
296
+ validateSyncPolicy(manifest.syncPolicy);
297
+ }
298
+
299
+ function validateCapabilityValues(values) {
300
+ validateStringArray(
301
+ values,
302
+ 'Host Asset Manifest capabilityValues',
303
+ { unique: true }
304
+ );
305
+ if (!arraysEqual(values, CAPABILITY_STATUS_VALUES)) {
178
306
  throw new Error(
179
- 'Host Asset Manifest excludedPrefixes must be an array'
307
+ `Host Asset Manifest capabilityValues must be ${CAPABILITY_STATUS_VALUES.join(', ')}`
180
308
  );
181
309
  }
182
- if (!manifest.syncPolicy || typeof manifest.syncPolicy !== 'object') {
310
+ return new Set(CAPABILITY_STATUS_VALUES);
311
+ }
312
+
313
+ function validatePlatform(platform, allowed, seenPlatformIds, aliases) {
314
+ if (!platform || typeof platform !== 'object' || Array.isArray(platform)) {
315
+ throw new Error('Every host platform must be an object');
316
+ }
317
+ assertKnownKeys(platform, PLATFORM_KEYS, `Host platform ${platform.id ?? '<unknown>'}`);
318
+ validateRequiredString(platform.id, 'Host platform id');
319
+ if (seenPlatformIds.has(platform.id)) {
320
+ throw new Error(`Duplicate host platform id: ${platform.id}`);
321
+ }
322
+ seenPlatformIds.add(platform.id);
323
+ validateRequiredString(platform.displayName, `Host platform ${platform.id} displayName`);
324
+ validateStringArray(platform.aliases, `Host platform ${platform.id} aliases`);
325
+ for (const alias of platform.aliases) {
326
+ const normalized = alias.toLowerCase();
327
+ const owner = aliases.get(normalized);
328
+ if (owner !== undefined) {
329
+ throw new Error(`Duplicate platform alias: ${alias}`);
330
+ }
331
+ aliases.set(normalized, platform.id);
332
+ }
333
+ validateStringArray(platform.detectAny, `Host platform ${platform.id} detectAny`);
334
+ validateStringArray(platform.assetPrefixes, `Host platform ${platform.id} assetPrefixes`);
335
+ validateStringArray(platform.assetFiles, `Host platform ${platform.id} assetFiles`);
336
+ if (platform.skillTarget !== null && typeof platform.skillTarget !== 'string') {
337
+ throw new Error(`Host platform ${platform.id} skillTarget must be a string or null`);
338
+ }
339
+ if (typeof platform.skillTarget === 'string' && platform.skillTarget.length === 0) {
340
+ throw new Error(`Host platform ${platform.id} skillTarget must be a non-empty string or null`);
341
+ }
342
+ validateRequiredString(platform.adapterPath, `Host platform ${platform.id} adapterPath`);
343
+ validatePlatformCapabilities(platform, allowed);
344
+ if (!Array.isArray(platform.commandTargets)) {
345
+ throw new Error(`Host platform ${platform.id} commandTargets must be an array`);
346
+ }
347
+ for (const target of platform.commandTargets) {
348
+ validateCommandTarget(target, platform.id);
349
+ }
350
+ }
351
+
352
+ function validatePlatformCapabilities(platform, allowed) {
353
+ if (!platform.capabilities || typeof platform.capabilities !== 'object' || Array.isArray(platform.capabilities)) {
354
+ throw new Error(`Host platform ${platform.id} capabilities must be an object`);
355
+ }
356
+ if (Object.keys(platform.capabilities).length === 0) {
357
+ throw new Error(`Host platform ${platform.id} capabilities must be a non-empty object`);
358
+ }
359
+ for (const [name, value] of Object.entries(platform.capabilities)) {
360
+ validateRequiredString(name, `Host platform ${platform.id} capability name`);
361
+ if (typeof value !== 'string' || !allowed.has(value)) {
362
+ throw new Error(`Host platform ${platform.id} illegal capability status ${name}=${value}`);
363
+ }
364
+ }
365
+ }
366
+
367
+ function validateCommandTarget(target, platformId) {
368
+ if (!target || typeof target !== 'object' || Array.isArray(target)) {
369
+ throw new Error(`Host platform ${platformId} commandTargets must be objects`);
370
+ }
371
+ assertKnownKeys(target, COMMAND_TARGET_KEYS, `Host platform ${platformId} commandTarget`);
372
+ validateRequiredString(target.config, `Host platform ${platformId} commandTarget.config`);
373
+ validateRequiredString(target.target, `Host platform ${platformId} commandTarget.target`);
374
+ if (!COMMAND_TARGET_FORMATS.includes(target.format)) {
375
+ throw new Error(`Host platform ${platformId} commandTarget.format must be json, toml, or yaml`);
376
+ }
377
+ }
378
+
379
+ function validateSyncPolicy(syncPolicy) {
380
+ if (!syncPolicy || typeof syncPolicy !== 'object' || Array.isArray(syncPolicy)) {
183
381
  throw new Error('Host Asset Manifest syncPolicy must be an object');
184
382
  }
185
- for (const platform of manifest.platforms) {
186
- if (!platform || typeof platform !== 'object' || !platform.id) {
187
- throw new Error('Every host platform must define an id');
188
- }
189
- for (const key of [
190
- 'aliases',
191
- 'detectAny',
192
- 'assetPrefixes',
193
- 'assetFiles',
194
- 'commandTargets'
195
- ]) {
196
- if (!Array.isArray(platform[key])) {
197
- throw new Error(
198
- `Host platform ${platform.id} ${key} must be an array`
199
- );
200
- }
383
+ assertKnownKeys(syncPolicy, SYNC_POLICY_KEYS, 'Host Asset Manifest syncPolicy');
384
+ for (const key of SYNC_POLICY_KEYS) {
385
+ validateStringArray(
386
+ syncPolicy[key],
387
+ `Host Asset Manifest syncPolicy.${key}`
388
+ );
389
+ }
390
+ }
391
+
392
+ function validateCapabilityMatrix(matrix, allowed, platformIds) {
393
+ if (!matrix || typeof matrix !== 'object' || Array.isArray(matrix)) {
394
+ throw new Error('Host Asset Manifest capabilityMatrix must be an object');
395
+ }
396
+ assertKnownKeys(matrix, ['required', 'hosts'], 'Host Asset Manifest capabilityMatrix');
397
+ validateStringArray(
398
+ matrix.required,
399
+ 'Host Asset Manifest capabilityMatrix.required'
400
+ );
401
+ if (!arraysEqual(matrix.required, REQUIRED_HOST_NEUTRAL_CAPABILITIES)) {
402
+ throw new Error(
403
+ `Host Asset Manifest capabilityMatrix.required must be ${REQUIRED_HOST_NEUTRAL_CAPABILITIES.join(', ')}`
404
+ );
405
+ }
406
+ if (!matrix.hosts || typeof matrix.hosts !== 'object' || Array.isArray(matrix.hosts)) {
407
+ throw new Error('Host Asset Manifest capabilityMatrix.hosts must be an object');
408
+ }
409
+ const requiredHosts = new Set([
410
+ ...REQUIRED_CAPABILITY_MATRIX_HOSTS,
411
+ ...platformIds
412
+ ]);
413
+ for (const hostId of [...requiredHosts].sort()) {
414
+ if (!matrix.hosts[hostId]) {
415
+ throw new Error(`Host Asset Manifest capability matrix missing host: ${hostId}`);
201
416
  }
202
417
  }
203
- for (const key of [
204
- 'protectedFiles',
205
- 'protectedPrefixes',
206
- 'safeFiles',
207
- 'safePrefixes',
208
- 'managedBlockFiles',
209
- 'obsoleteFiles'
210
- ]) {
211
- if (!Array.isArray(manifest.syncPolicy[key])) {
212
- throw new Error(
213
- `Host Asset Manifest syncPolicy.${key} must be an array`
418
+ for (const [hostId, capabilities] of Object.entries(matrix.hosts)) {
419
+ validateRequiredString(hostId, 'Host Asset Manifest capabilityMatrix host id');
420
+ if (!capabilities || typeof capabilities !== 'object' || Array.isArray(capabilities)) {
421
+ throw new Error(`Host Asset Manifest capabilityMatrix host ${hostId} must be an object`);
422
+ }
423
+ for (const key of Object.keys(capabilities)) {
424
+ if (!REQUIRED_HOST_NEUTRAL_CAPABILITIES.includes(key)) {
425
+ throw new Error(`Host Asset Manifest unknown host-neutral capability ${hostId}:${key}`);
426
+ }
427
+ }
428
+ for (const capability of REQUIRED_HOST_NEUTRAL_CAPABILITIES) {
429
+ validateCapabilityDeclaration(
430
+ capabilities[capability],
431
+ allowed,
432
+ hostId,
433
+ capability
214
434
  );
215
435
  }
216
436
  }
217
437
  }
218
438
 
439
+ function validateCapabilityDeclaration(declaration, allowed, hostId, capability) {
440
+ if (!declaration || typeof declaration !== 'object' || Array.isArray(declaration)) {
441
+ throw new Error(`Host Asset Manifest missing host-neutral capability ${hostId}:${capability}`);
442
+ }
443
+ assertKnownKeys(
444
+ declaration,
445
+ CAPABILITY_DECLARATION_KEYS,
446
+ `Host Asset Manifest capabilityMatrix.hosts.${hostId}.${capability}`
447
+ );
448
+ if (!allowed.has(declaration.status)) {
449
+ throw new Error(
450
+ `Host Asset Manifest illegal host-neutral capability ${hostId}:${capability}=${declaration.status}`
451
+ );
452
+ }
453
+ if (declaration.fallback !== undefined && (
454
+ typeof declaration.fallback !== 'string' || declaration.fallback.length === 0
455
+ )) {
456
+ throw new Error(
457
+ `Host Asset Manifest capability fallback must be a non-empty string: ${hostId}:${capability}`
458
+ );
459
+ }
460
+ if (declaration.status === 'unsupported' && !declaration.fallback) {
461
+ throw new Error(
462
+ `Host Asset Manifest unsupported capability requires fallback: ${hostId}:${capability}`
463
+ );
464
+ }
465
+ }
466
+
467
+ function validateRequiredString(value, label) {
468
+ if (typeof value !== 'string' || value.length === 0) {
469
+ throw new Error(`${label} must be a non-empty string`);
470
+ }
471
+ }
472
+
473
+ function validateStringArray(value, label, { unique = false } = {}) {
474
+ if (!Array.isArray(value)) {
475
+ throw new Error(`${label} must be an array`);
476
+ }
477
+ const seen = new Set();
478
+ for (const item of value) {
479
+ if (typeof item !== 'string' || item.length === 0) {
480
+ throw new Error(`${label} entries must be non-empty strings`);
481
+ }
482
+ if (unique && seen.has(item)) {
483
+ throw new Error(`${label} entries must be unique: ${item}`);
484
+ }
485
+ seen.add(item);
486
+ }
487
+ }
488
+
489
+ function assertKnownKeys(value, allowedKeys, label) {
490
+ const allowed = new Set(allowedKeys);
491
+ for (const key of Object.keys(value)) {
492
+ if (!allowed.has(key)) {
493
+ throw new Error(`${label} unknown field: ${key}`);
494
+ }
495
+ }
496
+ }
497
+
498
+ function normalizeCapabilityMatrix(matrix) {
499
+ return {
500
+ required: [...matrix.required],
501
+ hosts: Object.fromEntries(
502
+ Object.entries(matrix.hosts).map(([hostId, capabilities]) => [
503
+ hostId,
504
+ Object.fromEntries(
505
+ Object.entries(capabilities).map(([capability, declaration]) => [
506
+ capability,
507
+ { ...declaration }
508
+ ])
509
+ )
510
+ ])
511
+ )
512
+ };
513
+ }
514
+
219
515
 
220
516
  function normalizePath(value) {
221
517
  return String(value).replaceAll('\\', '/');
@@ -227,5 +523,12 @@ function unique(values) {
227
523
  }
228
524
 
229
525
 
526
+ function arraysEqual(left, right) {
527
+ return Array.isArray(left)
528
+ && left.length === right.length
529
+ && left.every((value, index) => value === right[index]);
530
+ }
531
+
532
+
230
533
  export const hostAssetManifest = loadHostAssetManifest();
231
534
  export const hostRegistry = createHostRegistry(hostAssetManifest);
@@ -226,6 +226,53 @@ function validateMetadata(metadata, transactionRoot, resolvedTarget) {
226
226
  }
227
227
  }
228
228
 
229
+ export async function inspectAssetTransactions(
230
+ targetDir,
231
+ { fileSystem = defaultFileSystem } = {}
232
+ ) {
233
+ const fs = fileSystem ?? defaultFileSystem;
234
+ const resolvedTarget = resolve(targetDir);
235
+ const parentDir = dirname(resolvedTarget);
236
+ let entries;
237
+ try {
238
+ entries = await fs.readdir(parentDir, { withFileTypes: true });
239
+ } catch (error) {
240
+ if (error && error.code === 'ENOENT') {
241
+ return [];
242
+ }
243
+ throw error;
244
+ }
245
+
246
+ const prefix = transactionPrefix(resolvedTarget);
247
+ const transactions = [];
248
+ for (const entry of entries) {
249
+ if (!entry.isDirectory() || !entry.name.startsWith(prefix)) {
250
+ continue;
251
+ }
252
+ const transactionRoot = join(parentDir, entry.name);
253
+ try {
254
+ const metadata = await readMetadata(fs, transactionRoot);
255
+ validateMetadata(metadata, transactionRoot, resolvedTarget);
256
+ transactions.push({
257
+ path: transactionRoot,
258
+ status: metadata.status,
259
+ actionCount: metadata.actions.length,
260
+ pending: metadata.status !== 'committed'
261
+ });
262
+ } catch (error) {
263
+ transactions.push({
264
+ path: transactionRoot,
265
+ status: 'unknown',
266
+ actionCount: null,
267
+ pending: true,
268
+ error: error instanceof Error ? error.message : String(error)
269
+ });
270
+ }
271
+ }
272
+
273
+ return transactions.sort((left, right) => left.path.localeCompare(right.path));
274
+ }
275
+
229
276
  export async function recoverAssetTransactions(
230
277
  targetDir,
231
278
  { fileSystem = defaultFileSystem } = {}
@@ -10,6 +10,12 @@ from pathlib import Path
10
10
  from typing import Any
11
11
 
12
12
 
13
+ def _configure_stdio() -> None:
14
+ for stream in (sys.stdout, sys.stderr):
15
+ if hasattr(stream, "reconfigure"):
16
+ stream.reconfigure(encoding="utf-8", errors="replace")
17
+
18
+
13
19
  def _read_input() -> dict[str, Any]:
14
20
  raw = sys.stdin.buffer.read()
15
21
  if not raw.strip():
@@ -90,4 +96,5 @@ def main() -> int:
90
96
 
91
97
 
92
98
  if __name__ == "__main__":
99
+ _configure_stdio()
93
100
  raise SystemExit(main())
@@ -10,6 +10,12 @@ from pathlib import Path
10
10
  from typing import Any
11
11
 
12
12
 
13
+ def _configure_stdio() -> None:
14
+ for stream in (sys.stdout, sys.stderr):
15
+ if hasattr(stream, "reconfigure"):
16
+ stream.reconfigure(encoding="utf-8", errors="replace")
17
+
18
+
13
19
  def _read_input() -> dict[str, Any]:
14
20
  raw = sys.stdin.buffer.read()
15
21
  if not raw.strip():
@@ -87,4 +93,5 @@ def main() -> int:
87
93
 
88
94
 
89
95
  if __name__ == "__main__":
96
+ _configure_stdio()
90
97
  raise SystemExit(main())
@@ -5,7 +5,7 @@
5
5
  "hooks": [
6
6
  {
7
7
  "type": "command",
8
- "command": ".codex/hooks/inject-workflow-state.py",
8
+ "command": ".cowork-flow/run python .codex/hooks/inject-workflow-state.py",
9
9
  "timeout": 5
10
10
  }
11
11
  ]
@@ -1 +1 @@
1
- 0.0.47
1
+ 0.0.49
@@ -3,17 +3,6 @@
3
3
  #
4
4
  # All values have sensible defaults. Only override what you need.
5
5
 
6
- #-------------------------------------------------------------------------------
7
- # Session Recording
8
- #-------------------------------------------------------------------------------
9
-
10
- # Commit message used when auto-committing journal/index changes
11
- # after running add_session.py
12
- session_commit_message: "chore: record journal"
13
-
14
- # Maximum lines per journal file before rotating to a new one
15
- max_journal_lines: 2000
16
-
17
6
  #-------------------------------------------------------------------------------
18
7
  # Codex Runtime
19
8
  #-------------------------------------------------------------------------------
@@ -7,7 +7,6 @@ Usage:
7
7
 
8
8
  This creates:
9
9
  - .cowork-flow/.developer file with developer info
10
- - .cowork-flow/workspace/<name>/ directory structure
11
10
  """
12
11
 
13
12
  from __future__ import annotations