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
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { templateRoot as defaultTemplateRoot } from './paths.js';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const KINDS = new Set(['phase', 'protocol', 'domain', 'mode', 'runtime']);
|
|
8
|
+
const VISIBILITIES = new Set(['public', 'internal']);
|
|
9
|
+
const ENTRY_STATUSES = new Set(['active', 'disabled']);
|
|
10
|
+
const WORKFLOW_STATUSES = new Set([
|
|
11
|
+
'no_task',
|
|
12
|
+
'planning',
|
|
13
|
+
'in_progress',
|
|
14
|
+
'review',
|
|
15
|
+
'completed',
|
|
16
|
+
'delegated_subtask'
|
|
17
|
+
]);
|
|
18
|
+
const ENFORCEMENTS = new Set(['advisory', 'mandatory', 'runtime']);
|
|
19
|
+
const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
20
|
+
const INTENT_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
21
|
+
const REQUIRED_FIELDS = new Set([
|
|
22
|
+
'id',
|
|
23
|
+
'displayName',
|
|
24
|
+
'aliases',
|
|
25
|
+
'kind',
|
|
26
|
+
'visibility',
|
|
27
|
+
'status',
|
|
28
|
+
'statuses',
|
|
29
|
+
'intents',
|
|
30
|
+
'enforcement',
|
|
31
|
+
'runtimeGate',
|
|
32
|
+
'runtimeCommand',
|
|
33
|
+
'evidenceArtifact',
|
|
34
|
+
'source',
|
|
35
|
+
'managedPaths'
|
|
36
|
+
]);
|
|
37
|
+
const ALLOWED_FIELDS = new Set([
|
|
38
|
+
...REQUIRED_FIELDS,
|
|
39
|
+
'readWhen'
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
export class SkillRegistryError extends Error {
|
|
44
|
+
constructor(message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = 'SkillRegistryError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
export const skillRegistryPath = join(
|
|
52
|
+
defaultTemplateRoot,
|
|
53
|
+
'.cowork-flow',
|
|
54
|
+
'spec',
|
|
55
|
+
'runtime',
|
|
56
|
+
'skill-registry.json'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
export const skillRegistrySchemaPath = join(
|
|
61
|
+
defaultTemplateRoot,
|
|
62
|
+
'.cowork-flow',
|
|
63
|
+
'spec',
|
|
64
|
+
'schemas',
|
|
65
|
+
'skill-registry.schema.json'
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
export function loadSkillRegistry(
|
|
70
|
+
path = skillRegistryPath,
|
|
71
|
+
options = {}
|
|
72
|
+
) {
|
|
73
|
+
let raw;
|
|
74
|
+
try {
|
|
75
|
+
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new SkillRegistryError(
|
|
78
|
+
`unable to read Skill Registry ${path}: ${error.message}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return createSkillRegistry(raw, options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
export function createSkillRegistry(raw, options = {}) {
|
|
86
|
+
const templateRoot = options.templateRoot ?? defaultTemplateRoot;
|
|
87
|
+
validateRoot(raw);
|
|
88
|
+
const entries = raw.entries
|
|
89
|
+
.map((entry) => normalizeEntry(entry, templateRoot))
|
|
90
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
91
|
+
|
|
92
|
+
const byId = new Map();
|
|
93
|
+
const aliases = new Map();
|
|
94
|
+
const tokens = new Set();
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
for (const token of [entry.id, ...entry.aliases]) {
|
|
97
|
+
if (tokens.has(token)) {
|
|
98
|
+
throw new SkillRegistryError(
|
|
99
|
+
`duplicate skill id or alias: ${token}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
tokens.add(token);
|
|
103
|
+
if (token === entry.id) {
|
|
104
|
+
byId.set(token, entry);
|
|
105
|
+
} else {
|
|
106
|
+
aliases.set(token, entry.id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const publicIntents = new Set();
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (entry.enforcement === 'mandatory' && !entry.runtimeGate) {
|
|
114
|
+
throw new SkillRegistryError(
|
|
115
|
+
`mandatory entry ${entry.id} requires runtimeGate`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (entry.runtimeGate) {
|
|
119
|
+
const gate = byId.get(entry.runtimeGate);
|
|
120
|
+
if (!gate || gate.kind !== 'runtime') {
|
|
121
|
+
throw new SkillRegistryError(
|
|
122
|
+
`runtimeGate ${entry.runtimeGate} must reference a runtime entry`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (entry.kind === 'runtime') {
|
|
127
|
+
if (
|
|
128
|
+
entry.visibility !== 'internal'
|
|
129
|
+
|| entry.enforcement !== 'runtime'
|
|
130
|
+
) {
|
|
131
|
+
throw new SkillRegistryError(
|
|
132
|
+
`runtime entry ${entry.id} must be internal with runtime enforcement`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
} else if (entry.enforcement === 'runtime') {
|
|
136
|
+
throw new SkillRegistryError(
|
|
137
|
+
`non-runtime entry ${entry.id} cannot use runtime enforcement`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (entry.visibility === 'public' && entry.status === 'active') {
|
|
141
|
+
for (const intent of entry.intents) {
|
|
142
|
+
if (publicIntents.has(intent)) {
|
|
143
|
+
throw new SkillRegistryError(
|
|
144
|
+
`duplicate public intent: ${intent}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
publicIntents.add(intent);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
validateManagedPaths(entries);
|
|
153
|
+
const normalized = {
|
|
154
|
+
schemaVersion: 1,
|
|
155
|
+
entries: entries.map((entry) => ({ ...entry }))
|
|
156
|
+
};
|
|
157
|
+
const publicEntries = entries.filter(
|
|
158
|
+
(entry) => entry.visibility === 'public' && entry.status === 'active'
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
schemaVersion: 1,
|
|
163
|
+
entries,
|
|
164
|
+
normalized,
|
|
165
|
+
publicEntries,
|
|
166
|
+
publicSkillIds: publicEntries.map((entry) => entry.id),
|
|
167
|
+
domainEntriesFor({ devType = null, paths = [] } = {}) {
|
|
168
|
+
const normalizedDevType = typeof devType === 'string'
|
|
169
|
+
? devType.trim()
|
|
170
|
+
: '';
|
|
171
|
+
const normalizedPaths = paths.map(
|
|
172
|
+
(path) => String(path).replaceAll('\\', '/').replace(/^\.\//, '')
|
|
173
|
+
);
|
|
174
|
+
return entries.filter((entry) => (
|
|
175
|
+
entry.kind === 'domain'
|
|
176
|
+
&& entry.status === 'active'
|
|
177
|
+
&& (
|
|
178
|
+
entry.readWhen.devTypes.includes(normalizedDevType)
|
|
179
|
+
|| normalizedPaths.some((path) => (
|
|
180
|
+
entry.readWhen.pathPatterns.some(
|
|
181
|
+
(pattern) => globMatches(path, pattern)
|
|
182
|
+
)
|
|
183
|
+
))
|
|
184
|
+
)
|
|
185
|
+
));
|
|
186
|
+
},
|
|
187
|
+
entry(idOrAlias) {
|
|
188
|
+
if (typeof idOrAlias !== 'string') {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const id = byId.has(idOrAlias)
|
|
192
|
+
? idOrAlias
|
|
193
|
+
: aliases.get(idOrAlias);
|
|
194
|
+
return id ? byId.get(id) ?? null : null;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
function validateRoot(raw) {
|
|
201
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
202
|
+
throw new SkillRegistryError('Skill Registry must be an object');
|
|
203
|
+
}
|
|
204
|
+
const rootKeys = Object.keys(raw);
|
|
205
|
+
for (const key of rootKeys) {
|
|
206
|
+
if (!['schemaVersion', 'entries'].includes(key)) {
|
|
207
|
+
throw new SkillRegistryError(
|
|
208
|
+
`unexpected Skill Registry field: ${key}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (raw.schemaVersion !== 1) {
|
|
213
|
+
throw new SkillRegistryError(
|
|
214
|
+
`unsupported Skill Registry schemaVersion: ${raw.schemaVersion}`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (!Array.isArray(raw.entries) || raw.entries.length === 0) {
|
|
218
|
+
throw new SkillRegistryError(
|
|
219
|
+
'Skill Registry entries must be a non-empty array'
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
function normalizeEntry(raw, templateRoot) {
|
|
226
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
227
|
+
throw new SkillRegistryError('Skill Registry entry must be an object');
|
|
228
|
+
}
|
|
229
|
+
const id = typeof raw.id === 'string' ? raw.id : '<unknown>';
|
|
230
|
+
for (const field of REQUIRED_FIELDS) {
|
|
231
|
+
if (!Object.hasOwn(raw, field)) {
|
|
232
|
+
throw new SkillRegistryError(
|
|
233
|
+
`entry ${id} missing required field: ${field}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (const field of Object.keys(raw)) {
|
|
238
|
+
if (!ALLOWED_FIELDS.has(field)) {
|
|
239
|
+
throw new SkillRegistryError(
|
|
240
|
+
`unexpected field for ${id}: ${field}`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!ID_PATTERN.test(raw.id)) {
|
|
245
|
+
throw new SkillRegistryError(`invalid id: ${raw.id}`);
|
|
246
|
+
}
|
|
247
|
+
if (typeof raw.displayName !== 'string' || raw.displayName.trim() === '') {
|
|
248
|
+
throw new SkillRegistryError(
|
|
249
|
+
`displayName for ${raw.id} must be a non-empty string`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const aliases = normalizeStringArray(raw.aliases, raw.id, 'aliases');
|
|
254
|
+
for (const alias of aliases) {
|
|
255
|
+
if (!ID_PATTERN.test(alias)) {
|
|
256
|
+
throw new SkillRegistryError(
|
|
257
|
+
`invalid alias for ${raw.id}: ${alias}`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const kind = validateEnum(raw.kind, KINDS, raw.id, 'kind');
|
|
262
|
+
const visibility = validateEnum(
|
|
263
|
+
raw.visibility,
|
|
264
|
+
VISIBILITIES,
|
|
265
|
+
raw.id,
|
|
266
|
+
'visibility'
|
|
267
|
+
);
|
|
268
|
+
const status = validateEnum(
|
|
269
|
+
raw.status,
|
|
270
|
+
ENTRY_STATUSES,
|
|
271
|
+
raw.id,
|
|
272
|
+
'status'
|
|
273
|
+
);
|
|
274
|
+
const statuses = normalizeStringArray(raw.statuses, raw.id, 'statuses');
|
|
275
|
+
for (const workflowStatus of statuses) {
|
|
276
|
+
if (!WORKFLOW_STATUSES.has(workflowStatus)) {
|
|
277
|
+
throw new SkillRegistryError(
|
|
278
|
+
`invalid workflow status for ${raw.id}: ${workflowStatus}`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const intents = normalizeStringArray(raw.intents, raw.id, 'intents');
|
|
283
|
+
for (const intent of intents) {
|
|
284
|
+
if (!INTENT_PATTERN.test(intent)) {
|
|
285
|
+
throw new SkillRegistryError(
|
|
286
|
+
`invalid intent for ${raw.id}: ${intent}`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const enforcement = validateEnum(
|
|
291
|
+
raw.enforcement,
|
|
292
|
+
ENFORCEMENTS,
|
|
293
|
+
raw.id,
|
|
294
|
+
'enforcement'
|
|
295
|
+
);
|
|
296
|
+
const runtimeGate = nullableString(
|
|
297
|
+
raw.runtimeGate,
|
|
298
|
+
raw.id,
|
|
299
|
+
'runtimeGate'
|
|
300
|
+
);
|
|
301
|
+
if (runtimeGate && !ID_PATTERN.test(runtimeGate)) {
|
|
302
|
+
throw new SkillRegistryError(
|
|
303
|
+
`invalid runtimeGate for ${raw.id}: ${runtimeGate}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const runtimeCommand = nullableString(
|
|
307
|
+
raw.runtimeCommand,
|
|
308
|
+
raw.id,
|
|
309
|
+
'runtimeCommand'
|
|
310
|
+
);
|
|
311
|
+
const evidenceArtifact = nullableString(
|
|
312
|
+
raw.evidenceArtifact,
|
|
313
|
+
raw.id,
|
|
314
|
+
'evidenceArtifact'
|
|
315
|
+
);
|
|
316
|
+
const source = normalizeRelativePath(raw.source, raw.id, 'source');
|
|
317
|
+
if (!existsSync(join(templateRoot, source))) {
|
|
318
|
+
throw new SkillRegistryError(
|
|
319
|
+
`source does not exist for ${raw.id}: ${source}`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const managedPaths = normalizeStringArray(
|
|
323
|
+
raw.managedPaths,
|
|
324
|
+
raw.id,
|
|
325
|
+
'managedPaths'
|
|
326
|
+
).map((path) => normalizeManagedPath(path, raw.id));
|
|
327
|
+
const readWhen = normalizeReadWhen(raw.readWhen ?? null, raw.id, kind);
|
|
328
|
+
return {
|
|
329
|
+
id: raw.id,
|
|
330
|
+
displayName: raw.displayName.trim(),
|
|
331
|
+
aliases: uniqueSorted(aliases),
|
|
332
|
+
kind,
|
|
333
|
+
visibility,
|
|
334
|
+
status,
|
|
335
|
+
statuses: uniqueSorted(statuses),
|
|
336
|
+
intents: uniqueSorted(intents),
|
|
337
|
+
enforcement,
|
|
338
|
+
runtimeGate,
|
|
339
|
+
runtimeCommand,
|
|
340
|
+
evidenceArtifact,
|
|
341
|
+
source,
|
|
342
|
+
managedPaths: uniqueSorted(managedPaths),
|
|
343
|
+
readWhen
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
function validateEnum(value, allowed, id, field) {
|
|
349
|
+
if (!allowed.has(value)) {
|
|
350
|
+
throw new SkillRegistryError(
|
|
351
|
+
`invalid ${field} for ${id}: ${value}`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
function normalizeStringArray(value, id, field) {
|
|
359
|
+
if (!Array.isArray(value)) {
|
|
360
|
+
throw new SkillRegistryError(`${field} for ${id} must be an array`);
|
|
361
|
+
}
|
|
362
|
+
return value.map((item) => {
|
|
363
|
+
if (typeof item !== 'string' || item.trim() === '') {
|
|
364
|
+
throw new SkillRegistryError(
|
|
365
|
+
`${field} for ${id} must contain non-empty strings`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return item.trim();
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
function nullableString(value, id, field) {
|
|
374
|
+
if (value === null) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
378
|
+
throw new SkillRegistryError(
|
|
379
|
+
`${field} for ${id} must be null or a non-empty string`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return value.trim();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
function normalizeReadWhen(value, id, kind) {
|
|
387
|
+
if (value === null) {
|
|
388
|
+
return { devTypes: [], pathPatterns: [] };
|
|
389
|
+
}
|
|
390
|
+
if (kind !== 'domain' || typeof value !== 'object' || Array.isArray(value)) {
|
|
391
|
+
throw new SkillRegistryError(
|
|
392
|
+
`readWhen for ${id} is only valid for domain entries`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
for (const field of Object.keys(value)) {
|
|
396
|
+
if (!['devTypes', 'pathPatterns'].includes(field)) {
|
|
397
|
+
throw new SkillRegistryError(
|
|
398
|
+
`unexpected readWhen field for ${id}: ${field}`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const devTypes = uniqueSorted(
|
|
403
|
+
normalizeStringArray(value.devTypes ?? [], id, 'readWhen.devTypes')
|
|
404
|
+
);
|
|
405
|
+
const pathPatterns = uniqueSorted(
|
|
406
|
+
normalizeStringArray(
|
|
407
|
+
value.pathPatterns ?? [],
|
|
408
|
+
id,
|
|
409
|
+
'readWhen.pathPatterns'
|
|
410
|
+
)
|
|
411
|
+
);
|
|
412
|
+
if (devTypes.length === 0 && pathPatterns.length === 0) {
|
|
413
|
+
throw new SkillRegistryError(
|
|
414
|
+
`readWhen for ${id} requires devTypes or pathPatterns`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
for (const pattern of pathPatterns) {
|
|
418
|
+
const normalized = pattern.replaceAll('\\', '/');
|
|
419
|
+
if (
|
|
420
|
+
normalized.startsWith('/')
|
|
421
|
+
|| normalized.startsWith('../')
|
|
422
|
+
|| normalized.includes('/../')
|
|
423
|
+
) {
|
|
424
|
+
throw new SkillRegistryError(
|
|
425
|
+
`invalid readWhen.pathPatterns for ${id}: ${pattern}`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return { devTypes, pathPatterns };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
function globMatches(path, pattern) {
|
|
434
|
+
const escaped = pattern
|
|
435
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
436
|
+
.replaceAll('**', '\u0000')
|
|
437
|
+
.replaceAll('*', '[^/]*')
|
|
438
|
+
.replaceAll('\u0000', '.*')
|
|
439
|
+
.replaceAll('?', '.');
|
|
440
|
+
return new RegExp(`^${escaped}$`).test(path);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
function normalizeRelativePath(value, id, field) {
|
|
445
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
446
|
+
throw new SkillRegistryError(
|
|
447
|
+
`${field} for ${id} must be a non-empty relative path`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const normalized = value.trim().replaceAll('\\', '/').replace(/^\.\//, '');
|
|
451
|
+
if (
|
|
452
|
+
isAbsolute(normalized)
|
|
453
|
+
|| normalized.startsWith('//')
|
|
454
|
+
|| /^[A-Za-z]:/.test(normalized)
|
|
455
|
+
|| normalized.split('/').includes('..')
|
|
456
|
+
|| /[*?[\]]/.test(normalized)
|
|
457
|
+
) {
|
|
458
|
+
throw new SkillRegistryError(
|
|
459
|
+
`invalid ${field} for ${id}: ${value}`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
return normalized;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
function normalizeManagedPath(value, id) {
|
|
467
|
+
const normalized = normalizeRelativePath(value, id, 'managedPaths');
|
|
468
|
+
if (!normalized.endsWith('/')) {
|
|
469
|
+
throw new SkillRegistryError(
|
|
470
|
+
`managedPaths for ${id} must end with /: ${value}`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
return normalized;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
function validateManagedPaths(entries) {
|
|
478
|
+
const owners = [];
|
|
479
|
+
for (const entry of entries) {
|
|
480
|
+
for (const path of entry.managedPaths) {
|
|
481
|
+
for (const existing of owners) {
|
|
482
|
+
if (
|
|
483
|
+
path === existing.path
|
|
484
|
+
|| path.startsWith(existing.path)
|
|
485
|
+
|| existing.path.startsWith(path)
|
|
486
|
+
) {
|
|
487
|
+
throw new SkillRegistryError(
|
|
488
|
+
`managed path overlap: ${existing.entry}:${existing.path} and ${entry.id}:${path}`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
owners.push({ entry: entry.id, path });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
function uniqueSorted(values) {
|
|
499
|
+
return [...new Set(values)].sort();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
export const skillRegistry = loadSkillRegistry();
|
|
@@ -42,11 +42,20 @@ Load context before checking:
|
|
|
42
42
|
6. Include `test_intent_review` in the review output for meaningful behavior
|
|
43
43
|
breaks coverage and shallow tests rejection.
|
|
44
44
|
|
|
45
|
+
Authoritative internal protocols:
|
|
46
|
+
- `.cowork-flow/spec/protocols/review.md`
|
|
47
|
+
- `.cowork-flow/spec/protocols/decision-review.md`
|
|
48
|
+
- `.cowork-flow/spec/protocols/spec-maintenance.md`
|
|
49
|
+
|
|
50
|
+
Apply their output contracts exactly. Review output must preserve
|
|
51
|
+
`acceptanceId`, `findings`, `test_intent_review`, `resolution`, and
|
|
52
|
+
`specUpdates`.
|
|
53
|
+
|
|
45
54
|
Rules:
|
|
46
55
|
- Fix issues directly when they are clearly in scope.
|
|
47
56
|
- Do not use the Task tool or invoke subagents.
|
|
48
57
|
- MUST NOT spawn, wait for, list, or close other agents.
|
|
49
58
|
- MUST NOT commit, archive, or mutate cowork-flow task state.
|
|
50
59
|
- Report findings, changed files, and exact verification commands.
|
|
51
|
-
- Include test intent findings that map tests back to
|
|
60
|
+
- Include test intent findings that map tests back to decision-anchor acceptance or
|
|
52
61
|
regression behavior.
|
|
@@ -43,6 +43,15 @@ Load context before editing:
|
|
|
43
43
|
7. For behavior-change tasks, create or verify `<task>/tdd.jsonl` before
|
|
44
44
|
implementation or record a documented exemption.
|
|
45
45
|
|
|
46
|
+
Authoritative internal protocols:
|
|
47
|
+
- `.cowork-flow/spec/protocols/tdd.md`
|
|
48
|
+
- `.cowork-flow/spec/protocols/decision-review.md`
|
|
49
|
+
- `.cowork-flow/spec/protocols/spec-maintenance.md`
|
|
50
|
+
|
|
51
|
+
Apply their output contracts exactly. Implementation reports and `tdd.jsonl`
|
|
52
|
+
must preserve `acceptanceId`, `redCommand`, `redExitCode`, `greenCommand`,
|
|
53
|
+
`greenExitCode`, and `specUpdates`.
|
|
54
|
+
|
|
46
55
|
Rules:
|
|
47
56
|
- Do not use the Task tool or invoke subagents.
|
|
48
57
|
- MUST NOT spawn, wait for, list, or close other agents.
|
|
@@ -50,6 +59,6 @@ Rules:
|
|
|
50
59
|
- MUST NOT commit or push.
|
|
51
60
|
- Keep edits inside requested scope.
|
|
52
61
|
- Do not treat shallow tests as TDD evidence; red-green-refactor proof must
|
|
53
|
-
include redCommand, redExitCode, greenCommand, greenExitCode, and
|
|
54
|
-
acceptance ID mapping.
|
|
62
|
+
include redCommand, redExitCode, greenCommand, greenExitCode, and
|
|
63
|
+
decision-anchor acceptance ID mapping.
|
|
55
64
|
- Report changed files and exact verification commands.
|