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.
- package/CHANGELOG.md +41 -0
- package/README.md +102 -23
- package/package.json +8 -3
- package/src/cli.js +6 -0
- package/src/commands/init.js +0 -83
- package/src/commands/source-refresh.js +40 -0
- package/src/commands/sync.js +6 -0
- package/src/commands/update.js +59 -6
- package/src/lib/copy-template.js +200 -50
- package/src/lib/host-assets.js +367 -64
- package/src/lib/plan-applier.js +47 -0
- package/template/.claude/hooks/inject-workflow-state.py +7 -0
- package/template/.codex/hooks/inject-workflow-state.py +7 -0
- package/template/.codex/hooks.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/config.yaml +0 -11
- package/template/.cowork-flow/scripts/adapters/cli/init_developer.py +0 -1
- package/template/.cowork-flow/scripts/adapters/cli/subagent.py +7 -4
- package/template/.cowork-flow/scripts/adapters/cli/task.py +8 -484
- package/template/.cowork-flow/scripts/adapters/cli/task_archive_commands.py +15 -134
- package/template/.cowork-flow/scripts/adapters/cli/task_context_commands.py +7 -1
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +388 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +163 -2
- package/template/.cowork-flow/scripts/adapters/git/git_context.py +16 -193
- package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +170 -84
- package/template/.cowork-flow/scripts/infra/config.py +0 -17
- package/template/.cowork-flow/scripts/infra/developer_profile.py +0 -95
- package/template/.cowork-flow/scripts/infra/paths.py +0 -79
- package/template/.cowork-flow/scripts/infra/skill_manifest.py +23 -3
- package/template/.cowork-flow/scripts/infra/storage/operation_log.py +40 -0
- package/template/.cowork-flow/scripts/infra/storage/state_store.py +356 -1
- package/template/.cowork-flow/scripts/kernel/workflow_route.py +15 -15
- package/template/.cowork-flow/scripts/run.py +0 -5
- package/template/.cowork-flow/scripts/services/context_discovery.py +156 -0
- package/template/.cowork-flow/scripts/services/context_jsonl.py +119 -0
- package/template/.cowork-flow/scripts/services/context_paths.py +186 -0
- package/template/.cowork-flow/scripts/services/lifecycle_checks.py +186 -69
- package/template/.cowork-flow/scripts/services/lifecycle_policy.py +128 -0
- package/template/.cowork-flow/scripts/services/plan_binding.py +146 -0
- package/template/.cowork-flow/scripts/services/readiness.py +3 -170
- package/template/.cowork-flow/scripts/services/task_archive.py +115 -24
- package/template/.cowork-flow/scripts/services/task_context.py +103 -365
- package/template/.cowork-flow/scripts/services/task_creation.py +40 -5
- package/template/.cowork-flow/scripts/services/task_lifecycle.py +155 -18
- package/template/.cowork-flow/scripts/services/task_routing.py +80 -67
- package/template/.cowork-flow/scripts/services/task_utils.py +0 -146
- package/template/.cowork-flow/scripts/services/workflow_runtime.py +1 -1
- package/template/.cowork-flow/spec/contracts/capabilities.md +23 -0
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +2 -2
- package/template/.cowork-flow/spec/contracts/error-output-as-data.md +8 -0
- package/template/.cowork-flow/spec/contracts/index.md +1 -0
- package/template/.cowork-flow/spec/contracts/party-mode-v2-board.md +20 -0
- package/template/.cowork-flow/spec/contracts/plan-binding.md +50 -0
- package/template/.cowork-flow/spec/contracts/skill-owned-actions.md +31 -7
- package/template/.cowork-flow/spec/guides/index.md +3 -1
- package/template/.cowork-flow/spec/index.md +5 -4
- package/template/.cowork-flow/spec/references/definition-of-done.md +2 -3
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
- package/template/.cowork-flow/spec/runtime/contract-registry.json +14 -0
- package/template/.cowork-flow/spec/runtime/host-assets.json +73 -3
- package/template/.cowork-flow/spec/runtime/index.md +1 -1
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +150 -8
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/skills/batch-execution/SKILL.md +37 -3
- package/template/skills/batch-execution/scripts/batch_action.py +4 -0
- package/template/skills/batch-execution/scripts/batch_execution.py +120 -499
- package/template/skills/batch-execution/scripts/batch_recovery.py +94 -0
- package/template/skills/batch-execution/scripts/batch_state_machine.py +201 -0
- package/template/skills/batch-execution/scripts/batch_verification.py +326 -0
- package/template/skills/brainstorming/SKILL.md +8 -7
- package/template/skills/cowork-flow/SKILL.md +3 -0
- package/template/skills/party-mode/scripts/party_action_contract.py +113 -0
- package/template/skills/party-mode/scripts/party_actions.py +95 -0
- package/template/skills/party-mode/scripts/party_board_store.py +148 -0
- package/template/skills/party-mode/scripts/party_mode_v2.py +345 -317
- package/template/skills/party-mode/scripts/party_reports.py +177 -0
- package/template/skills/party-mode/scripts/party_state_machine.py +189 -0
- package/template/skills/runtime-health/SKILL.md +14 -2
- package/template/skills/runtime-health/scripts/doctor.py +278 -41
- package/template/skills/task-planning/SKILL.md +105 -61
- package/template/skills/task-review/SKILL.md +4 -4
- package/template/skills/task-review/scripts/review_check.py +277 -20
- package/template/skills/test-first/SKILL.md +18 -3
- package/template/skills/test-first/manifest.json +8 -4
- package/template/.cowork-flow/changes/archive/.gitkeep +0 -1
- package/template/.cowork-flow/scripts/adapters/cli/add_session.py +0 -465
- package/template/.cowork-flow/scripts/adapters/cli/change.py +0 -412
- package/template/.cowork-flow/workspace/index.md +0 -102
- package/template/.zcode/scaffold/AGENTS.md +0 -145
- package/template/.zcode/scaffold/CLAUDE.md +0 -5
package/src/lib/host-assets.js
CHANGED
|
@@ -4,13 +4,66 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { templateRoot } from './paths.js';
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
export const hostAssetManifestPath =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 =
|
|
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
|
-
(
|
|
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
|
|
128
|
-
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
307
|
+
`Host Asset Manifest capabilityValues must be ${CAPABILITY_STATUS_VALUES.join(', ')}`
|
|
180
308
|
);
|
|
181
309
|
}
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
204
|
-
'
|
|
205
|
-
'
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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);
|
package/src/lib/plan-applier.js
CHANGED
|
@@ -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())
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
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
|
#-------------------------------------------------------------------------------
|