docks-kit 0.5.0 → 0.7.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.
@@ -0,0 +1,395 @@
1
+ import {
2
+ CLAUDE_EFFORT_LEVELS,
3
+ CODEX_REASONING_EFFORTS
4
+ } from "./efforts"
5
+ import { payloadText } from "./payload"
6
+
7
+ export type WorkflowTool = "claude" | "codex"
8
+ export type WorkflowCompany = "anthropic" | "openai"
9
+ export type WorkflowRoleName = "orchestrator" | "reviewer" | "implementer"
10
+ export type WorkflowBoundName = "minimum_score" | "max_rounds"
11
+
12
+ export interface WorkflowCandidate {
13
+ readonly company: WorkflowCompany
14
+ readonly tool: WorkflowTool
15
+ readonly model: string
16
+ readonly effort: string
17
+ }
18
+
19
+ export interface WorkflowRole {
20
+ readonly selector: string
21
+ readonly candidates: ReadonlyArray<WorkflowCandidate>
22
+ }
23
+
24
+ export interface WorkflowRecordV1 {
25
+ readonly schema: 1
26
+ readonly orchestrator: WorkflowRole
27
+ readonly reviewer: WorkflowRole
28
+ readonly implementer: WorkflowRole
29
+ readonly review: {
30
+ readonly minimum_score: number
31
+ readonly max_rounds: number
32
+ }
33
+ }
34
+
35
+ export interface WorkflowOverrides {
36
+ readonly orchestrator?: string
37
+ readonly reviewer?: string
38
+ readonly implementer?: string
39
+ readonly minimumScore?: string
40
+ readonly maxRounds?: string
41
+ }
42
+
43
+ interface WorkflowProfile {
44
+ readonly candidates: ReadonlyArray<WorkflowCandidate>
45
+ }
46
+
47
+ interface WorkflowRegistryCore {
48
+ readonly schema: 1
49
+ readonly profiles: Readonly<Record<string, WorkflowProfile>>
50
+ readonly defaults: {
51
+ readonly orchestrator: string
52
+ readonly reviewer: string
53
+ readonly implementer: string
54
+ readonly review: {
55
+ readonly minimum_score: number
56
+ readonly max_rounds: number
57
+ }
58
+ }
59
+ readonly exact_target_grammar: "<tool>:<model>@<effort>"
60
+ readonly availability: "checked_when_used"
61
+ }
62
+
63
+ export interface WorkflowRegistryView extends WorkflowRegistryCore {
64
+ readonly tools: Readonly<Record<WorkflowTool, {
65
+ readonly models: ReadonlyArray<string>
66
+ readonly efforts: ReadonlyArray<string>
67
+ }>>
68
+ }
69
+
70
+ type UnknownRecord = Record<string, unknown>
71
+
72
+ export const WORKFLOW_RECORD_PREFIX = "Docks-workflow-models: "
73
+
74
+ const TOOL_COMPANIES: Readonly<Record<WorkflowTool, WorkflowCompany>> = {
75
+ claude: "anthropic",
76
+ codex: "openai"
77
+ }
78
+
79
+ const isRecord = (value: unknown): value is UnknownRecord =>
80
+ typeof value === "object" && value !== null && !Array.isArray(value)
81
+
82
+ function hasExactKeys(value: UnknownRecord, keys: ReadonlyArray<string>): boolean {
83
+ const actual = Object.keys(value).sort()
84
+ return actual.length === keys.length && keys.every((key, index) => actual[index] === key)
85
+ }
86
+
87
+ function expectRecord(value: unknown, label: string, keys: ReadonlyArray<string>): UnknownRecord {
88
+ if (!isRecord(value) || !hasExactKeys(value, [...keys].sort())) {
89
+ throw new Error(`${label} must be a closed record`)
90
+ }
91
+ return value
92
+ }
93
+
94
+ function parseManifest(): UnknownRecord {
95
+ const parsed = JSON.parse(payloadText("SoT/models.json")) as unknown
96
+ if (!isRecord(parsed)) throw new Error("Embedded model catalog must be an object")
97
+ return parsed
98
+ }
99
+
100
+ function toolModels(manifest: UnknownRecord, tool: WorkflowTool): ReadonlyArray<string> {
101
+ const entry = manifest[tool]
102
+ if (!isRecord(entry) || !Array.isArray(entry["models"])) {
103
+ throw new Error(`Embedded ${tool} model catalog is invalid`)
104
+ }
105
+ const models = entry["models"].map((item) => {
106
+ if (!isRecord(item) || typeof item["id"] !== "string") {
107
+ throw new Error(`Embedded ${tool} model entry is invalid`)
108
+ }
109
+ return item["id"]
110
+ })
111
+ return models.filter((model) => model !== "default")
112
+ }
113
+
114
+ const toolEfforts = (tool: WorkflowTool): ReadonlyArray<string> =>
115
+ tool === "claude" ? CLAUDE_EFFORT_LEVELS : CODEX_REASONING_EFFORTS
116
+
117
+ function parseCandidate(
118
+ value: unknown,
119
+ label: string,
120
+ tools: WorkflowRegistryView["tools"]
121
+ ): WorkflowCandidate {
122
+ const candidate = expectRecord(value, label, ["company", "effort", "model", "tool"])
123
+ const tool = candidate["tool"]
124
+ if (tool !== "claude" && tool !== "codex") throw new Error(`${label} has an invalid tool`)
125
+ const company = candidate["company"]
126
+ const expectedCompany = TOOL_COMPANIES[tool]
127
+ if (company !== expectedCompany) throw new Error(`${label} has an invalid company`)
128
+ const model = candidate["model"]
129
+ if (typeof model !== "string" || !tools[tool].models.includes(model)) {
130
+ throw new Error(`${label} has an unverified ${tool} model`)
131
+ }
132
+ const effort = candidate["effort"]
133
+ if (typeof effort !== "string" || !tools[tool].efforts.includes(effort)) {
134
+ throw new Error(`${label} has an unverified ${tool} effort`)
135
+ }
136
+ return { company: expectedCompany, tool, model, effort }
137
+ }
138
+
139
+ function parseCandidates(
140
+ value: unknown,
141
+ label: string,
142
+ tools: WorkflowRegistryView["tools"]
143
+ ): ReadonlyArray<WorkflowCandidate> {
144
+ if (!Array.isArray(value) || value.length < 1 || value.length > 3) {
145
+ throw new Error(`${label} must contain one to three candidates`)
146
+ }
147
+ return value.map((candidate, index) => parseCandidate(candidate, `${label}[${index}]`, tools))
148
+ }
149
+
150
+ function parseNumericBound(name: WorkflowBoundName, value: unknown): number {
151
+ const [minimum, maximum] = name === "minimum_score" ? [0, 100] : [1, 10]
152
+ if (typeof value !== "number" || !Number.isInteger(value) || value < minimum || value > maximum) {
153
+ throw new Error(`${name} must be an integer in ${minimum}..${maximum}`)
154
+ }
155
+ return value
156
+ }
157
+
158
+ function resolveSelector(
159
+ selector: string,
160
+ registry: Pick<WorkflowRegistryView, "profiles" | "tools">
161
+ ): WorkflowRole {
162
+ const profileMatch = /^profile:([A-Za-z0-9._-]+)$/.exec(selector)
163
+ if (profileMatch !== null) {
164
+ const profileName = profileMatch[1]!
165
+ const profile = registry.profiles[profileName]
166
+ if (profile === undefined) throw new Error(`Unknown workflow profile '${profileName}'`)
167
+ return { selector, candidates: profile.candidates }
168
+ }
169
+
170
+ const exactMatch = /^(claude|codex):([A-Za-z0-9._-]+)@([A-Za-z0-9._-]+)$/.exec(selector)
171
+ if (exactMatch === null) {
172
+ throw new Error(
173
+ `Invalid workflow selector '${selector}' — expected profile:<name> or <tool>:<model>@<effort>`
174
+ )
175
+ }
176
+ const tool = exactMatch[1] as WorkflowTool
177
+ const model = exactMatch[2]!
178
+ const effort = exactMatch[3]!
179
+ if (!registry.tools[tool].models.includes(model)) {
180
+ throw new Error(`Unknown ${tool} workflow model '${model}'`)
181
+ }
182
+ if (!registry.tools[tool].efforts.includes(effort)) {
183
+ throw new Error(`Unknown ${tool} workflow effort '${effort}'`)
184
+ }
185
+ return {
186
+ selector,
187
+ candidates: [{ company: TOOL_COMPANIES[tool], tool, model, effort }]
188
+ }
189
+ }
190
+
191
+ function loadWorkflowRegistry(): WorkflowRegistryView {
192
+ const manifest = parseManifest()
193
+ const tools: WorkflowRegistryView["tools"] = {
194
+ claude: { models: toolModels(manifest, "claude"), efforts: toolEfforts("claude") },
195
+ codex: { models: toolModels(manifest, "codex"), efforts: toolEfforts("codex") }
196
+ }
197
+ const workflow = expectRecord(
198
+ manifest["workflow"],
199
+ "Embedded workflow registry",
200
+ ["availability", "defaults", "exact_target_grammar", "profiles", "schema"]
201
+ )
202
+ if (workflow["schema"] !== 1) throw new Error("Embedded workflow registry schema must be 1")
203
+ if (workflow["exact_target_grammar"] !== "<tool>:<model>@<effort>") {
204
+ throw new Error("Embedded workflow exact-target grammar is invalid")
205
+ }
206
+ if (workflow["availability"] !== "checked_when_used") {
207
+ throw new Error("Embedded workflow availability contract is invalid")
208
+ }
209
+
210
+ const rawProfiles = workflow["profiles"]
211
+ if (!isRecord(rawProfiles) || Object.keys(rawProfiles).length === 0) {
212
+ throw new Error("Embedded workflow profiles must be a nonempty record")
213
+ }
214
+ const profiles: Record<string, WorkflowProfile> = {}
215
+ for (const [name, rawProfile] of Object.entries(rawProfiles)) {
216
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) throw new Error(`Invalid workflow profile name '${name}'`)
217
+ const profile = expectRecord(rawProfile, `Workflow profile '${name}'`, ["candidates"])
218
+ profiles[name] = {
219
+ candidates: parseCandidates(profile["candidates"], `Workflow profile '${name}' candidates`, tools)
220
+ }
221
+ }
222
+
223
+ const rawDefaults = expectRecord(
224
+ workflow["defaults"],
225
+ "Embedded workflow defaults",
226
+ ["implementer", "orchestrator", "review", "reviewer"]
227
+ )
228
+ const rawReview = expectRecord(
229
+ rawDefaults["review"],
230
+ "Embedded workflow review defaults",
231
+ ["max_rounds", "minimum_score"]
232
+ )
233
+ for (const role of ["orchestrator", "reviewer", "implementer"] as const) {
234
+ if (typeof rawDefaults[role] !== "string") throw new Error(`Embedded workflow ${role} default is invalid`)
235
+ }
236
+ const registry: WorkflowRegistryView = {
237
+ schema: 1,
238
+ profiles,
239
+ defaults: {
240
+ orchestrator: rawDefaults["orchestrator"] as string,
241
+ reviewer: rawDefaults["reviewer"] as string,
242
+ implementer: rawDefaults["implementer"] as string,
243
+ review: {
244
+ minimum_score: parseNumericBound("minimum_score", rawReview["minimum_score"]),
245
+ max_rounds: parseNumericBound("max_rounds", rawReview["max_rounds"])
246
+ }
247
+ },
248
+ tools,
249
+ exact_target_grammar: "<tool>:<model>@<effort>",
250
+ availability: "checked_when_used"
251
+ }
252
+ resolveSelector(registry.defaults.orchestrator, registry)
253
+ resolveSelector(registry.defaults.reviewer, registry)
254
+ resolveSelector(registry.defaults.implementer, registry)
255
+ return registry
256
+ }
257
+
258
+ export function workflowRegistryView(): WorkflowRegistryView {
259
+ return loadWorkflowRegistry()
260
+ }
261
+
262
+ export function resolveWorkflowSelector(selector: string): WorkflowRole {
263
+ return resolveSelector(selector, loadWorkflowRegistry())
264
+ }
265
+
266
+ export function parseWorkflowBound(name: WorkflowBoundName, value: string): number {
267
+ if (!/^[0-9]+$/.test(value)) {
268
+ const range = name === "minimum_score" ? "0..100" : "1..10"
269
+ throw new Error(`${name} must be a base-10 integer in ${range}`)
270
+ }
271
+ return parseNumericBound(name, Number(value))
272
+ }
273
+
274
+ export function defaultWorkflowRecord(): WorkflowRecordV1 {
275
+ const registry = loadWorkflowRegistry()
276
+ return {
277
+ schema: 1,
278
+ orchestrator: resolveSelector(registry.defaults.orchestrator, registry),
279
+ reviewer: resolveSelector(registry.defaults.reviewer, registry),
280
+ implementer: resolveSelector(registry.defaults.implementer, registry),
281
+ review: registry.defaults.review
282
+ }
283
+ }
284
+
285
+ function parseRole(
286
+ value: unknown,
287
+ name: WorkflowRoleName,
288
+ registry: WorkflowRegistryView
289
+ ): WorkflowRole {
290
+ const role = expectRecord(value, `Workflow ${name} role`, ["candidates", "selector"])
291
+ if (typeof role["selector"] !== "string") throw new Error(`Workflow ${name} selector is invalid`)
292
+ const resolved = resolveSelector(role["selector"], registry)
293
+ const candidates = parseCandidates(role["candidates"], `Workflow ${name} candidates`, registry.tools)
294
+ if (compactJcs(candidates) !== compactJcs(resolved.candidates)) {
295
+ throw new Error(`Workflow ${name} candidates do not match its selector`)
296
+ }
297
+ return resolved
298
+ }
299
+
300
+ export function parseWorkflowRecord(value: unknown): WorkflowRecordV1 {
301
+ const record = expectRecord(
302
+ value,
303
+ "Workflow record",
304
+ ["implementer", "orchestrator", "review", "reviewer", "schema"]
305
+ )
306
+ if (record["schema"] !== 1) throw new Error("Workflow record schema must be 1")
307
+ const registry = loadWorkflowRegistry()
308
+ const review = expectRecord(record["review"], "Workflow record review", ["max_rounds", "minimum_score"])
309
+ return {
310
+ schema: 1,
311
+ orchestrator: parseRole(record["orchestrator"], "orchestrator", registry),
312
+ reviewer: parseRole(record["reviewer"], "reviewer", registry),
313
+ implementer: parseRole(record["implementer"], "implementer", registry),
314
+ review: {
315
+ minimum_score: parseNumericBound("minimum_score", review["minimum_score"]),
316
+ max_rounds: parseNumericBound("max_rounds", review["max_rounds"])
317
+ }
318
+ }
319
+ }
320
+
321
+ export function buildWorkflowRecord(
322
+ overrides: WorkflowOverrides,
323
+ base: WorkflowRecordV1 = defaultWorkflowRecord()
324
+ ): WorkflowRecordV1 {
325
+ const current = parseWorkflowRecord(base)
326
+ return {
327
+ schema: 1,
328
+ orchestrator: overrides.orchestrator === undefined
329
+ ? current.orchestrator
330
+ : resolveWorkflowSelector(overrides.orchestrator),
331
+ reviewer: overrides.reviewer === undefined
332
+ ? current.reviewer
333
+ : resolveWorkflowSelector(overrides.reviewer),
334
+ implementer: overrides.implementer === undefined
335
+ ? current.implementer
336
+ : resolveWorkflowSelector(overrides.implementer),
337
+ review: {
338
+ minimum_score: overrides.minimumScore === undefined
339
+ ? current.review.minimum_score
340
+ : parseWorkflowBound("minimum_score", overrides.minimumScore),
341
+ max_rounds: overrides.maxRounds === undefined
342
+ ? current.review.max_rounds
343
+ : parseWorkflowBound("max_rounds", overrides.maxRounds)
344
+ }
345
+ }
346
+ }
347
+
348
+ function canonicalValue(value: unknown): unknown {
349
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value
350
+ if (typeof value === "number") {
351
+ if (!Number.isFinite(value)) throw new Error("JCS cannot encode a non-finite number")
352
+ return value
353
+ }
354
+ if (Array.isArray(value)) return value.map(canonicalValue)
355
+ if (isRecord(value)) {
356
+ return Object.fromEntries(
357
+ Object.entries(value)
358
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
359
+ .map(([key, item]) => [key, canonicalValue(item)])
360
+ )
361
+ }
362
+ throw new Error("JCS can encode only JSON values")
363
+ }
364
+
365
+ export function compactJcs(value: unknown): string {
366
+ return JSON.stringify(canonicalValue(value))
367
+ }
368
+
369
+ export function renderWorkflowRecordLine(record: WorkflowRecordV1): string {
370
+ return `${WORKFLOW_RECORD_PREFIX}${compactJcs(parseWorkflowRecord(record))}`
371
+ }
372
+
373
+ export function workflowRegistryJson(): string {
374
+ return JSON.stringify(workflowRegistryView(), null, 2)
375
+ }
376
+
377
+ export function workflowCatalog(): string {
378
+ const registry = workflowRegistryView()
379
+ const profileLines = Object.entries(registry.profiles).flatMap(([name, profile]) => [
380
+ ` profile:${name}`,
381
+ ...profile.candidates.map(({ tool, model, effort }) => ` ${tool}:${model}@${effort}`)
382
+ ])
383
+ return [
384
+ "Workflow model registry:",
385
+ "Profiles:",
386
+ ...profileLines,
387
+ "Defaults:",
388
+ ` orchestrator ${registry.defaults.orchestrator}`,
389
+ ` reviewer ${registry.defaults.reviewer}`,
390
+ ` implementer ${registry.defaults.implementer}`,
391
+ ` review minimum score ${registry.defaults.review.minimum_score}; maximum rounds ${registry.defaults.review.max_rounds}`,
392
+ `Exact targets: ${registry.exact_target_grammar}`,
393
+ "Availability: checked when used by Docks; docks-kit does not probe providers."
394
+ ].join("\n")
395
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docks-kit",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Portable AI coding agent config kit — SoT sync engine + typed CLI for Claude Code, Codex, and universal agent skills",
5
5
  "type": "module",
6
6
  "license": "MIT",