@hunsu/protocol 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +157 -0
  2. package/dist/command-decoder.d.ts +5 -0
  3. package/dist/command-decoder.js +329 -0
  4. package/dist/constants.d.ts +15 -0
  5. package/dist/constants.js +15 -0
  6. package/dist/decoder-helpers.d.ts +58 -0
  7. package/dist/decoder-helpers.js +1757 -0
  8. package/dist/errors.d.ts +14 -0
  9. package/dist/errors.js +28 -0
  10. package/dist/event-decoder.d.ts +5 -0
  11. package/dist/event-decoder.js +232 -0
  12. package/dist/index.d.ts +10 -0
  13. package/dist/index.js +10 -0
  14. package/dist/lifecycle.d.ts +67 -0
  15. package/dist/lifecycle.js +271 -0
  16. package/dist/model.d.ts +1058 -0
  17. package/dist/model.js +1 -0
  18. package/dist/primitives.d.ts +59 -0
  19. package/dist/primitives.js +135 -0
  20. package/dist/projection.d.ts +8 -0
  21. package/dist/projection.js +875 -0
  22. package/dist/prompt-template.d.ts +12 -0
  23. package/dist/prompt-template.js +83 -0
  24. package/dist/protocol-validation.d.ts +36 -0
  25. package/dist/protocol-validation.js +1139 -0
  26. package/dist/result.d.ts +20 -0
  27. package/dist/result.js +21 -0
  28. package/dist/workflow-helpers.d.ts +31 -0
  29. package/dist/workflow-helpers.js +193 -0
  30. package/dist/workflow.d.ts +10 -0
  31. package/dist/workflow.js +594 -0
  32. package/package.json +30 -0
  33. package/src/command-decoder.ts +292 -0
  34. package/src/constants.ts +27 -0
  35. package/src/decoder-helpers.ts +1672 -0
  36. package/src/errors.ts +38 -0
  37. package/src/event-decoder.ts +225 -0
  38. package/src/index.ts +43 -0
  39. package/src/lifecycle.ts +403 -0
  40. package/src/model.ts +1233 -0
  41. package/src/primitives.ts +196 -0
  42. package/src/projection.ts +1081 -0
  43. package/src/prompt-template.ts +97 -0
  44. package/src/protocol-validation.ts +1252 -0
  45. package/src/result.ts +35 -0
  46. package/src/workflow-helpers.ts +239 -0
  47. package/src/workflow.ts +753 -0
@@ -0,0 +1,1252 @@
1
+ import {
2
+ GUARDRAIL_SCOPES,
3
+ GUARDRAIL_SEVERITIES,
4
+ REASONING_EFFORTS,
5
+ SERVICE_TIERS,
6
+ VOTE_RULES
7
+ } from "./constants.ts";
8
+ import {
9
+ DomainInvariantError,
10
+ unwrapDomainModelResult,
11
+ type DomainWorkflowError
12
+ } from "./errors.ts";
13
+ import type {
14
+ ExecutorEntity,
15
+ ExecutorId,
16
+ ExecutorVisibleProfile,
17
+ Harness,
18
+ HarnessPlannerSnapshot,
19
+ HarnessSnapshot,
20
+ HubPackageKind,
21
+ HubPackageLock,
22
+ ManagerConfig,
23
+ Member,
24
+ MemberApprovalConstraint,
25
+ MemberConfig,
26
+ MemberExecutionConstraint,
27
+ MemberPluginBinding,
28
+ Membership,
29
+ ResourceBinding,
30
+ ResourceEntity,
31
+ RuntimePolicy,
32
+ SkillBinding
33
+ } from "./model.ts";
34
+ import { assertValidPromptTemplate, decodePromptTemplate, promptTemplateFromText, type PromptTemplate } from "./prompt-template.ts";
35
+ import { makeNonEmptyText, makePositiveInteger } from "./primitives.ts";
36
+ import { err, ok, type Result } from "./result.ts";
37
+
38
+ export const DEFAULT_TEAM_PROMPT = "Create a closure-free ExecutionPlan using the available Members.";
39
+ export const DEFAULT_MANAGER_PROMPT = [
40
+ "Discuss HUNSU changes conversationally and edit only decoded request files under .hunsu-request when a file-backed draft change is needed.",
41
+ "Help the user refine divergent Hunsu changes while preserving the current Execute result as the source of truth."
42
+ ].join(" ");
43
+ export const DEFAULT_MEMBER_EXECUTION: MemberExecutionConstraint = { kind: "read_only", network: "disabled" };
44
+ export const DEFAULT_MEMBER_APPROVAL: MemberApprovalConstraint = { policy: "never" };
45
+ const MEMBER_EXECUTION_KINDS = ["read_only", "worktree_write", "unrestricted"] as const;
46
+ const MEMBER_EXECUTION_NETWORKS = ["disabled", "enabled"] as const;
47
+ const MEMBER_APPROVAL_POLICIES = ["never", "on_request"] as const;
48
+ const MEMBER_APPROVAL_REVIEWERS = ["user", "auto_review"] as const;
49
+ const DEFAULT_ROOT_TEAM_ID = "root-team";
50
+
51
+ export function validateHarness(value: unknown): Result<HarnessSnapshot, DomainWorkflowError> {
52
+ return parseHarness(value);
53
+ }
54
+
55
+ export function validateHarnessPlanner(value: unknown): Result<HarnessPlannerSnapshot, DomainWorkflowError> {
56
+ return parseHarnessPlanner(value);
57
+ }
58
+
59
+ export function validateHarnessEntity(value: unknown, path = "Harness"): Result<Harness, DomainWorkflowError> {
60
+ const harness = recordResult(value, path);
61
+ if (!harness.ok) return harness;
62
+ if (typeof harness.value.rootTeamId !== "string" || harness.value.rootTeamId.trim() === "") {
63
+ return workflowValidationError(`${path}.rootTeamId must be a non-empty string`);
64
+ }
65
+ if (!Array.isArray(harness.value.executors)) {
66
+ return workflowValidationError(`${path}.executors must be an array`);
67
+ }
68
+ if (!Array.isArray(harness.value.resources)) {
69
+ return workflowValidationError(`${path}.resources must be an array`);
70
+ }
71
+ const executorIds = new Set<string>();
72
+ for (const [index, executor] of harness.value.executors.entries()) {
73
+ const validation = executorEntityResult(executor, `${path}.executors[${index}]`);
74
+ if (!validation.ok) return validation;
75
+ const id = String((executor as { id: unknown }).id);
76
+ if (executorIds.has(id)) {
77
+ return workflowValidationError(`${path}.executors must not include duplicate Executor ${id}`);
78
+ }
79
+ executorIds.add(id);
80
+ }
81
+ const root = harness.value.executors.find(executor => {
82
+ const record = executor as { id?: unknown; kind?: unknown };
83
+ return record.id === harness.value.rootTeamId && record.kind === "team";
84
+ });
85
+ if (!root) {
86
+ return workflowValidationError(`${path}.rootTeamId must reference a Team Executor`);
87
+ }
88
+ const executorKindById = new Map<string, "team" | "member">();
89
+ const teamMemberships = new Map<string, string[]>();
90
+ for (const executor of harness.value.executors) {
91
+ const record = executor as { id?: unknown; kind?: unknown; members?: Membership[] };
92
+ const id = String(record.id);
93
+ const kind = record.kind === "team" ? "team" : "member";
94
+ executorKindById.set(id, kind);
95
+ if (kind === "team") {
96
+ teamMemberships.set(id, (record.members ?? []).map(membership => String(membership.executorId)));
97
+ }
98
+ }
99
+ for (const [index, executor] of harness.value.executors.entries()) {
100
+ const record = executor as { kind?: unknown; members?: unknown };
101
+ if (record.kind !== "team") {
102
+ continue;
103
+ }
104
+ const memberships = record.members as Membership[];
105
+ for (const [membershipIndex, membership] of memberships.entries()) {
106
+ const memberId = String(membership.executorId);
107
+ const memberKind = executorKindById.get(memberId);
108
+ if (!memberKind) {
109
+ return workflowValidationError(`${path}.executors[${index}].members[${membershipIndex}].executorId references unknown Executor ${membership.executorId}`);
110
+ }
111
+ if (membership.visibleProfile.kind !== memberKind) {
112
+ return workflowValidationError(`${path}.executors[${index}].members[${membershipIndex}].visibleProfile.kind must match Executor ${memberId} kind ${memberKind}`);
113
+ }
114
+ }
115
+ }
116
+ const cycle = findTeamCycle(teamMemberships);
117
+ if (cycle) {
118
+ return workflowValidationError(`${path}.executors must not contain cyclic Team Memberships: ${cycle.join(" -> ")}`);
119
+ }
120
+ for (const [index, resource] of harness.value.resources.entries()) {
121
+ const validation = resourceEntityResult(resource, `${path}.resources[${index}]`);
122
+ if (!validation.ok) return validation;
123
+ }
124
+ const guardrails = guardrailsResult(harness.value.guardrails, `${path}.guardrails`);
125
+ if (!guardrails.ok) return guardrails;
126
+ if (!Array.isArray(harness.value.artifactActions)) {
127
+ return workflowValidationError(`${path}.artifactActions must be an array`);
128
+ }
129
+ return ok(cloneHarnessEntity(harness.value as Harness));
130
+ }
131
+
132
+ function findTeamCycle(teamMemberships: Map<string, string[]>): string[] | undefined {
133
+ const visiting = new Set<string>();
134
+ const visited = new Set<string>();
135
+ const stack: string[] = [];
136
+
137
+ const visit = (teamId: string): string[] | undefined => {
138
+ if (visiting.has(teamId)) {
139
+ const start = stack.indexOf(teamId);
140
+ return [...stack.slice(start), teamId];
141
+ }
142
+ if (visited.has(teamId)) {
143
+ return undefined;
144
+ }
145
+ visiting.add(teamId);
146
+ stack.push(teamId);
147
+ for (const childId of teamMemberships.get(teamId) ?? []) {
148
+ if (!teamMemberships.has(childId)) {
149
+ continue;
150
+ }
151
+ const cycle = visit(childId);
152
+ if (cycle) {
153
+ return cycle;
154
+ }
155
+ }
156
+ stack.pop();
157
+ visiting.delete(teamId);
158
+ visited.add(teamId);
159
+ return undefined;
160
+ };
161
+
162
+ for (const teamId of teamMemberships.keys()) {
163
+ const cycle = visit(teamId);
164
+ if (cycle) {
165
+ return cycle;
166
+ }
167
+ }
168
+ return undefined;
169
+ }
170
+
171
+ export function validateMemberConfig(value: unknown, path = "Member"): Result<MemberConfig, DomainWorkflowError> {
172
+ const member = recordResult(value, path);
173
+ if (!member.ok) return member;
174
+ const validation = memberConfigResult(member.value, path);
175
+ return validation.ok ? ok(cloneMemberConfig(member.value as MemberConfig)) : validation;
176
+ }
177
+
178
+ export function validateManagerConfig(value: unknown, path = "Manager"): Result<ManagerConfig, DomainWorkflowError> {
179
+ const manager = recordResult(value, path);
180
+ if (!manager.ok) return manager;
181
+ const validation = managerConfigResult(manager.value, path);
182
+ return validation.ok ? ok(cloneManagerConfig(manager.value as ManagerConfig)) : validation;
183
+ }
184
+
185
+ function parseHarness(value: unknown): Result<HarnessSnapshot, DomainWorkflowError> {
186
+ const protocol = recordResult(value, "Harness");
187
+ if (!protocol.ok) return protocol;
188
+ const normalized = normalizeHarnessKind(protocol.value);
189
+ switch (normalized.kind) {
190
+ case "team_execution_plan":
191
+ return validateHarnessSnapshotShape({ ...normalized, kind: "team_execution_plan" }, "team_execution_plan", "maxAttemptCount");
192
+ case "role_squad":
193
+ return validateHarnessSnapshotShape(normalized, "role_squad", "maxRoundCount");
194
+ case "council_vote":
195
+ return chainResult(
196
+ allowedResult("council_vote.voteRule", normalized.voteRule, VOTE_RULES),
197
+ () => validateHarnessSnapshotShape(normalized, "council_vote", "maxRoundCount")
198
+ );
199
+ case "court_debate":
200
+ return validateHarnessSnapshotShape(normalized, "court_debate", "maxRoundCount");
201
+ default:
202
+ return workflowValidationError(`Unsupported Harness kind: ${String(normalized.kind)}`);
203
+ }
204
+ }
205
+
206
+ function parseHarnessPlanner(value: unknown): Result<HarnessPlannerSnapshot, DomainWorkflowError> {
207
+ const protocol = recordResult(value, "Planner Harness");
208
+ if (!protocol.ok) return protocol;
209
+ const normalized = normalizeHarnessKind(protocol.value);
210
+ switch (normalized.kind) {
211
+ case "team_execution_plan":
212
+ return validateHarnessPlannerSnapshotShape({ ...normalized, kind: "team_execution_plan" }, "team_execution_plan", "maxAttemptCount");
213
+ case "role_squad":
214
+ return validateHarnessPlannerSnapshotShape(normalized, "role_squad", "maxRoundCount");
215
+ case "council_vote":
216
+ return chainResult(
217
+ allowedResult("council_vote.voteRule", normalized.voteRule, VOTE_RULES),
218
+ () => validateHarnessPlannerSnapshotShape(normalized, "council_vote", "maxRoundCount")
219
+ );
220
+ case "court_debate":
221
+ return validateHarnessPlannerSnapshotShape(normalized, "court_debate", "maxRoundCount");
222
+ default:
223
+ return workflowValidationError(`Unsupported Planner Harness kind: ${String(normalized.kind)}`);
224
+ }
225
+ }
226
+
227
+ export function isExecutableHarness(protocol: HarnessSnapshot): boolean {
228
+ return protocol.kind === "team_execution_plan";
229
+ }
230
+
231
+ export function validateExecutableHarness(protocol: HarnessSnapshot): Result<HarnessSnapshot, DomainWorkflowError> {
232
+ if (!isExecutableHarness(protocol)) {
233
+ return err({ type: "DomainWorkflowError", message: `Harness ${protocol.kind} is not executable yet` });
234
+ }
235
+ return ok(protocol);
236
+ }
237
+
238
+ export function createDefaultHarness(instructions = "", skills: SkillBinding[] = []): HarnessSnapshot {
239
+ return {
240
+ kind: "team_execution_plan",
241
+ maxAttemptCount: unwrapDomainModelResult(makePositiveInteger(5, "team_execution_plan.maxAttemptCount")),
242
+ team: {
243
+ promptTemplate: promptTemplateFromText(instructions || DEFAULT_TEAM_PROMPT)
244
+ },
245
+ members: [
246
+ createDefaultMemberConfig("azir", "Plan briefly, then implement the focused goal in this repository.", skills),
247
+ createDefaultMemberConfig("galio", "Verify the completed work against the focused goal with concrete evidence.", [])
248
+ ]
249
+ };
250
+ }
251
+
252
+ export function createDefaultMemberConfig(
253
+ id: string,
254
+ prompt = "",
255
+ skills: SkillBinding[] = [],
256
+ execution: MemberExecutionConstraint = DEFAULT_MEMBER_EXECUTION,
257
+ approval: MemberApprovalConstraint = DEFAULT_MEMBER_APPROVAL,
258
+ plugins: MemberPluginBinding[] = []
259
+ ): MemberConfig {
260
+ return {
261
+ id: unwrapDomainModelResult(makeNonEmptyText(id, "executorId")),
262
+ promptTemplate: promptTemplateFromText(prompt),
263
+ skills: cloneSkills(skills),
264
+ plugins: clonePlugins(plugins),
265
+ model: unwrapDomainModelResult(makeNonEmptyText("codex-default", "member.model")),
266
+ reasoningEffort: "default",
267
+ serviceTier: "default",
268
+ execution: cloneMemberExecution(execution),
269
+ approval: cloneMemberApproval(approval)
270
+ };
271
+ }
272
+
273
+ export function createDefaultManagerConfig(
274
+ id = "manager.hunsu.default",
275
+ prompt = DEFAULT_MANAGER_PROMPT,
276
+ skills: SkillBinding[] = [],
277
+ plugins: MemberPluginBinding[] = []
278
+ ): ManagerConfig {
279
+ return {
280
+ id: unwrapDomainModelResult(makeNonEmptyText(id, "manager.id")),
281
+ promptTemplate: promptTemplateFromText(prompt),
282
+ skills: cloneSkills(skills),
283
+ plugins: clonePlugins(plugins)
284
+ };
285
+ }
286
+
287
+ export function cloneHarness(protocol: HarnessSnapshot): HarnessSnapshot {
288
+ const cloned = JSON.parse(JSON.stringify(protocol)) as HarnessSnapshot;
289
+ return {
290
+ ...cloned,
291
+ guardrails: cloned.guardrails?.map(guardrail => ({ ...guardrail })),
292
+ members: cloned.members.map(cloneMemberConfig)
293
+ };
294
+ }
295
+
296
+ export function cloneHarnessPlanner(protocol: HarnessPlannerSnapshot): HarnessPlannerSnapshot {
297
+ const cloned = JSON.parse(JSON.stringify(protocol)) as HarnessPlannerSnapshot;
298
+ return {
299
+ ...cloned,
300
+ team: { promptTemplate: { ...cloned.team.promptTemplate } },
301
+ guardrails: cloned.guardrails?.map(guardrail => ({ ...guardrail }))
302
+ };
303
+ }
304
+
305
+ export function harnessPlannerFromSnapshot(protocol: HarnessSnapshot): HarnessPlannerSnapshot {
306
+ const cloned = cloneHarness(protocol);
307
+ const { members: _members, ...planner } = cloned;
308
+ return cloneHarnessPlanner(planner as HarnessPlannerSnapshot);
309
+ }
310
+
311
+ export function composeHarnessSnapshot(planner: HarnessPlannerSnapshot, members: MemberConfig[]): HarnessSnapshot {
312
+ const memberValidation = memberArrayResult(members, "members.members", true);
313
+ if (!memberValidation.ok) {
314
+ throw new DomainInvariantError(memberValidation.error.message);
315
+ }
316
+ const clonedPlanner = cloneHarnessPlanner(planner);
317
+ const clonedMembers = members.map(cloneMemberConfig);
318
+ return {
319
+ ...clonedPlanner,
320
+ members: clonedMembers
321
+ } as HarnessSnapshot;
322
+ }
323
+
324
+ export function harnessEntityFromSnapshot(protocol: HarnessSnapshot, rootTeamId = DEFAULT_ROOT_TEAM_ID): Harness {
325
+ const rootId = executorId(rootTeamId, "rootTeamId");
326
+ const planner = harnessPlannerFromSnapshot(protocol);
327
+ const members = protocol.members.map(cloneMemberConfig);
328
+ const rootMemberships: Membership[] = members.map(member => ({
329
+ executorId: member.id,
330
+ visibleProfile: visibleProfileFromMemberConfig(member)
331
+ }));
332
+ return {
333
+ rootTeamId: rootId,
334
+ executors: [
335
+ {
336
+ kind: "team",
337
+ id: rootId,
338
+ planner: {
339
+ promptTemplate: { ...planner.team.promptTemplate },
340
+ maxAttemptCount: "maxAttemptCount" in planner ? planner.maxAttemptCount : undefined,
341
+ guardrails: planner.guardrails?.map(guardrail => ({ ...guardrail }))
342
+ },
343
+ members: rootMemberships
344
+ },
345
+ ...members.map(memberEntityFromConfig)
346
+ ],
347
+ resources: resourceEntitiesFromMembers(members),
348
+ guardrails: planner.guardrails?.map(guardrail => ({ ...guardrail })) ?? [],
349
+ artifactActions: []
350
+ };
351
+ }
352
+
353
+ export function harnessSnapshotForTeam(harness: Harness, teamId: string = harness.rootTeamId): HarnessSnapshot {
354
+ const team = getHarnessTeam(harness, teamId);
355
+ if (!team) {
356
+ throw new DomainInvariantError(`Harness does not define Team ${teamId}`);
357
+ }
358
+ const members = team.members.map(membership => memberConfigForMembership(harness, membership));
359
+ const maxAttemptCount = team.planner.maxAttemptCount
360
+ ?? unwrapDomainModelResult(makePositiveInteger(5, "team.maxAttemptCount"));
361
+ return {
362
+ kind: "team_execution_plan",
363
+ maxAttemptCount,
364
+ guardrails: team.planner.guardrails?.map(guardrail => ({ ...guardrail })),
365
+ team: {
366
+ promptTemplate: { ...team.planner.promptTemplate }
367
+ },
368
+ members
369
+ };
370
+ }
371
+
372
+ export function rootHarnessSnapshot(harness: Harness): HarnessSnapshot {
373
+ return harnessSnapshotForTeam(harness, harness.rootTeamId);
374
+ }
375
+
376
+ export function cloneHarnessEntity(harness: Harness): Harness {
377
+ return {
378
+ rootTeamId: harness.rootTeamId,
379
+ executors: harness.executors.map(cloneExecutorEntity),
380
+ resources: harness.resources.map(cloneResourceEntity),
381
+ guardrails: harness.guardrails.map(guardrail => ({ ...guardrail })),
382
+ artifactActions: JSON.parse(JSON.stringify(harness.artifactActions)) as Harness["artifactActions"]
383
+ };
384
+ }
385
+
386
+ export function getHarnessExecutor(harness: Harness, executorId: string): ExecutorEntity | undefined {
387
+ const executor = harness.executors.find(candidate => candidate.id === executorId);
388
+ return executor ? cloneExecutorEntity(executor) : undefined;
389
+ }
390
+
391
+ export function getHarnessTeam(harness: Harness, executorId: string): Extract<ExecutorEntity, { kind: "team" }> | undefined {
392
+ const executor = harness.executors.find(candidate => candidate.id === executorId && candidate.kind === "team");
393
+ return executor ? cloneExecutorEntity(executor) as Extract<ExecutorEntity, { kind: "team" }> : undefined;
394
+ }
395
+
396
+ export function getHarnessMember(harness: Harness, executorId: string): Member | undefined {
397
+ const executor = harness.executors.find(candidate => candidate.id === executorId && candidate.kind === "member");
398
+ return executor ? cloneMemberEntity(executor as Member) : undefined;
399
+ }
400
+
401
+ export function memberConfigFromMemberEntity(member: Member): MemberConfig {
402
+ return {
403
+ id: member.id,
404
+ promptTemplate: { ...member.promptTemplate },
405
+ skills: member.resources
406
+ .filter((binding): binding is Extract<ResourceBinding, { kind: "skill" }> => binding.kind === "skill")
407
+ .map(binding => cloneSkill(binding.skill)),
408
+ plugins: member.resources
409
+ .filter((binding): binding is Extract<ResourceBinding, { kind: "plugin" }> => binding.kind === "plugin")
410
+ .map(binding => ({ ...binding.plugin })),
411
+ model: member.runtimePolicy.model,
412
+ reasoningEffort: member.runtimePolicy.reasoningEffort,
413
+ serviceTier: member.runtimePolicy.serviceTier,
414
+ execution: cloneMemberExecution(member.runtimePolicy.execution),
415
+ approval: cloneMemberApproval(member.runtimePolicy.approval)
416
+ };
417
+ }
418
+
419
+ export function getHarnessMemberConfig(protocol: HarnessSnapshot, executorId: string): MemberConfig | undefined {
420
+ return protocol.members.find(member => member.id === executorId)
421
+ ? cloneMemberConfig(protocol.members.find(member => member.id === executorId)!)
422
+ : undefined;
423
+ }
424
+
425
+ export function updateHarnessMemberConfig(protocol: HarnessSnapshot, executorId: string, update: (member: MemberConfig) => MemberConfig): HarnessSnapshot {
426
+ if (!protocol.members.some(member => member.id === executorId)) {
427
+ throw new DomainInvariantError(`Unknown Member ${executorId} for Harness ${protocol.kind}`);
428
+ }
429
+ return {
430
+ ...protocol,
431
+ team: { ...protocol.team },
432
+ guardrails: protocol.guardrails?.map(guardrail => ({ ...guardrail })),
433
+ members: protocol.members.map(member => member.id === executorId ? update(cloneMemberConfig(member)) : cloneMemberConfig(member))
434
+ };
435
+ }
436
+
437
+ export function updateHarnessTeamPromptTemplate(protocol: HarnessSnapshot, promptTemplate: PromptTemplate): HarnessSnapshot {
438
+ assertValidPromptTemplate(promptTemplate, "Team prompt template");
439
+ return {
440
+ ...protocol,
441
+ team: { promptTemplate: { ...promptTemplate } },
442
+ guardrails: protocol.guardrails?.map(guardrail => ({ ...guardrail })),
443
+ members: protocol.members.map(cloneMemberConfig)
444
+ };
445
+ }
446
+
447
+ function executorId(value: string, path: string): ExecutorId {
448
+ return unwrapDomainModelResult(makeNonEmptyText(value, path));
449
+ }
450
+
451
+ function runtimePolicyFromMemberConfig(member: MemberConfig): RuntimePolicy {
452
+ return {
453
+ model: member.model,
454
+ reasoningEffort: member.reasoningEffort,
455
+ serviceTier: member.serviceTier,
456
+ execution: cloneMemberExecution(member.execution),
457
+ approval: cloneMemberApproval(member.approval)
458
+ };
459
+ }
460
+
461
+ function resourceBindingsFromMemberConfig(member: MemberConfig): ResourceBinding[] {
462
+ return [
463
+ ...member.skills.map(skill => ({ kind: "skill" as const, skill: cloneSkill(skill) })),
464
+ ...(member.plugins ?? []).map(plugin => ({ kind: "plugin" as const, plugin: { ...plugin } }))
465
+ ];
466
+ }
467
+
468
+ function memberEntityFromConfig(member: MemberConfig): Member {
469
+ return {
470
+ kind: "member",
471
+ id: member.id,
472
+ promptTemplate: { ...member.promptTemplate },
473
+ resources: resourceBindingsFromMemberConfig(member),
474
+ runtimePolicy: runtimePolicyFromMemberConfig(member)
475
+ };
476
+ }
477
+
478
+ function visibleProfileFromMemberConfig(member: MemberConfig): ExecutorVisibleProfile {
479
+ return {
480
+ kind: "member",
481
+ label: member.id,
482
+ summary: unwrapDomainModelResult(makeNonEmptyText(member.promptTemplate.template || `Member ${member.id}`, "visibleProfile.summary")),
483
+ capabilities: member.skills.map(skill => skill.name)
484
+ };
485
+ }
486
+
487
+ function memberConfigForMembership(harness: Harness, membership: Membership): MemberConfig {
488
+ const executor = harness.executors.find(candidate => candidate.id === membership.executorId);
489
+ if (!executor) {
490
+ throw new DomainInvariantError(`Team Membership references unknown Executor ${membership.executorId}`);
491
+ }
492
+ if (executor.kind === "member") {
493
+ return memberConfigFromMemberEntity(executor);
494
+ }
495
+ return createDefaultMemberConfig(
496
+ executor.id,
497
+ visibleProfilePrompt(membership.visibleProfile),
498
+ [],
499
+ DEFAULT_MEMBER_EXECUTION,
500
+ DEFAULT_MEMBER_APPROVAL,
501
+ []
502
+ );
503
+ }
504
+
505
+ function visibleProfilePrompt(profile: ExecutorVisibleProfile): string {
506
+ const capabilities = profile.capabilities?.length ? `\nCapabilities:\n${profile.capabilities.map(capability => `- ${capability}`).join("\n")}` : "";
507
+ return [
508
+ profile.kind === "team" ? "Composite Team Executor." : "Member Executor.",
509
+ profile.label ? `Label: ${profile.label}` : undefined,
510
+ profile.summary ? `Summary: ${profile.summary}` : undefined,
511
+ capabilities || undefined
512
+ ].filter(Boolean).join("\n");
513
+ }
514
+
515
+ function resourceEntitiesFromMembers(members: MemberConfig[]): ResourceEntity[] {
516
+ const resources: ResourceEntity[] = [];
517
+ const seen = new Set<string>();
518
+ for (const member of members) {
519
+ for (const binding of resourceBindingsFromMemberConfig(member)) {
520
+ const id = resourceEntityId(member.id, binding);
521
+ if (seen.has(id)) {
522
+ continue;
523
+ }
524
+ seen.add(id);
525
+ resources.push({
526
+ id: id as ResourceEntity["id"],
527
+ binding
528
+ });
529
+ }
530
+ }
531
+ return resources;
532
+ }
533
+
534
+ function resourceEntityId(memberId: string, binding: ResourceBinding): string {
535
+ if (binding.kind === "skill") {
536
+ return `resource.${memberId}.skill.${binding.skill.name}`;
537
+ }
538
+ if (binding.kind === "plugin") {
539
+ return `resource.${memberId}.plugin.${binding.plugin.id}`;
540
+ }
541
+ return `resource.${memberId}.package.${binding.lock.kind}.${binding.lock.key}.${binding.lock.version}`;
542
+ }
543
+
544
+ function cloneExecutorEntity(executor: ExecutorEntity): ExecutorEntity {
545
+ if (executor.kind === "member") {
546
+ return { ...cloneMemberEntity(executor), packageLock: executor.packageLock ? { ...executor.packageLock } : undefined };
547
+ }
548
+ return {
549
+ ...executor,
550
+ planner: {
551
+ promptTemplate: { ...executor.planner.promptTemplate },
552
+ maxAttemptCount: executor.planner.maxAttemptCount,
553
+ guardrails: executor.planner.guardrails?.map(guardrail => ({ ...guardrail }))
554
+ },
555
+ members: executor.members.map(membership => ({
556
+ executorId: membership.executorId,
557
+ visibleProfile: {
558
+ ...membership.visibleProfile,
559
+ capabilities: membership.visibleProfile.capabilities ? [...membership.visibleProfile.capabilities] : undefined
560
+ }
561
+ })),
562
+ packageLock: executor.packageLock ? { ...executor.packageLock } : undefined
563
+ };
564
+ }
565
+
566
+ function cloneMemberEntity(member: Member): Member {
567
+ return {
568
+ kind: "member",
569
+ id: member.id,
570
+ promptTemplate: { ...member.promptTemplate },
571
+ resources: member.resources.map(cloneResourceBinding),
572
+ runtimePolicy: {
573
+ model: member.runtimePolicy.model,
574
+ reasoningEffort: member.runtimePolicy.reasoningEffort,
575
+ serviceTier: member.runtimePolicy.serviceTier,
576
+ execution: cloneMemberExecution(member.runtimePolicy.execution),
577
+ approval: cloneMemberApproval(member.runtimePolicy.approval)
578
+ }
579
+ };
580
+ }
581
+
582
+ function cloneResourceBinding(binding: ResourceBinding): ResourceBinding {
583
+ if (binding.kind === "skill") {
584
+ return { kind: "skill", skill: cloneSkill(binding.skill) };
585
+ }
586
+ if (binding.kind === "plugin") {
587
+ return { kind: "plugin", plugin: { ...binding.plugin } };
588
+ }
589
+ return { kind: "package", lock: { ...binding.lock } };
590
+ }
591
+
592
+ function cloneResourceEntity(resource: ResourceEntity): ResourceEntity {
593
+ return {
594
+ id: resource.id,
595
+ binding: cloneResourceBinding(resource.binding),
596
+ packageLock: resource.packageLock ? { ...resource.packageLock } : undefined
597
+ };
598
+ }
599
+
600
+ export function validateSkillBinding(value: unknown, path: string): void {
601
+ const validation = validateSkillBindingResult(value, path);
602
+ if (!validation.ok) {
603
+ throw new DomainInvariantError(validation.error.message);
604
+ }
605
+ }
606
+
607
+ function validateHarnessSnapshotShape(
608
+ protocol: Record<string, unknown>,
609
+ kind: HarnessSnapshot["kind"],
610
+ countField: "maxAttemptCount" | "maxRoundCount"
611
+ ): Result<HarnessSnapshot, DomainWorkflowError> {
612
+ for (const validation of [
613
+ positiveIntegerResult(protocol[countField], `${kind}.${countField}`),
614
+ guardrailsResult(protocol.guardrails, `${kind}.guardrails`),
615
+ teamConfigResult(protocol.team, `${kind}.team`),
616
+ memberArrayResult(protocol.members, `${kind}.members`, true)
617
+ ]) {
618
+ if (!validation.ok) {
619
+ return validation;
620
+ }
621
+ }
622
+ return ok(cloneHarness(protocol as HarnessSnapshot));
623
+ }
624
+
625
+ function normalizeHarnessKind(protocol: Record<string, unknown>): Record<string, unknown> {
626
+ return protocol.kind === "team_execution_plan"
627
+ ? { ...protocol, kind: "team_execution_plan" }
628
+ : protocol;
629
+ }
630
+
631
+ function validateHarnessPlannerSnapshotShape(
632
+ protocol: Record<string, unknown>,
633
+ kind: HarnessPlannerSnapshot["kind"],
634
+ countField: "maxAttemptCount" | "maxRoundCount"
635
+ ): Result<HarnessPlannerSnapshot, DomainWorkflowError> {
636
+ if (Object.prototype.hasOwnProperty.call(protocol, "members")) {
637
+ return workflowValidationError(`${kind}.members is stored in executors.json, not harness.json`);
638
+ }
639
+ for (const validation of [
640
+ positiveIntegerResult(protocol[countField], `${kind}.${countField}`),
641
+ guardrailsResult(protocol.guardrails, `${kind}.guardrails`),
642
+ teamConfigResult(protocol.team, `${kind}.team`)
643
+ ]) {
644
+ if (!validation.ok) {
645
+ return validation;
646
+ }
647
+ }
648
+ return ok(cloneHarnessPlanner(protocol as HarnessPlannerSnapshot));
649
+ }
650
+
651
+ function validateSkillBindingResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
652
+ const skill = recordResult(value, path);
653
+ if (!skill.ok) return skill;
654
+ if (skill.value.kind !== "local-snapshot" && skill.value.kind !== "local-root-installed" && skill.value.kind !== "registry-package" && skill.value.kind !== "skillMeta") {
655
+ return workflowValidationError(`${path}.kind must be local-snapshot, local-root-installed, registry-package, or skillMeta`);
656
+ }
657
+ if (typeof skill.value.name !== "string" || skill.value.name.trim() === "") {
658
+ return workflowValidationError(`${path}.name must be a non-empty string`);
659
+ }
660
+ if (skill.value.kind === "skillMeta") {
661
+ const exactKeys = exactKeysResult(skill.value, ["agent", "kind", "name", "source"], path);
662
+ if (!exactKeys.ok) return exactKeys;
663
+ if (typeof skill.value.source !== "string" || skill.value.source.trim() === "") {
664
+ return workflowValidationError(`${path}.source must be a non-empty string`);
665
+ }
666
+ if (skill.value.agent !== "codex") {
667
+ return workflowValidationError(`${path}.agent must be codex`);
668
+ }
669
+ return ok(undefined);
670
+ }
671
+ if (skill.value.kind === "local-root-installed") {
672
+ if (skill.value.sourcePath !== undefined && (typeof skill.value.sourcePath !== "string" || skill.value.sourcePath.trim() === "")) {
673
+ return workflowValidationError(`${path}.sourcePath must be a non-empty string when provided`);
674
+ }
675
+ return ok(undefined);
676
+ }
677
+ if (skill.value.kind === "local-snapshot") {
678
+ for (const key of ["sourcePath", "contentHash", "snapshotRef"]) {
679
+ if (typeof skill.value[key] !== "string" || skill.value[key].trim() === "") {
680
+ return workflowValidationError(`${path}.${key} must be a non-empty string`);
681
+ }
682
+ }
683
+ if (skill.value.snapshotFiles !== undefined) {
684
+ if (!Array.isArray(skill.value.snapshotFiles)) {
685
+ return workflowValidationError(`${path}.snapshotFiles must be an array`);
686
+ }
687
+ for (const [index, file] of skill.value.snapshotFiles.entries()) {
688
+ const snapshotFile = recordResult(file, `${path}.snapshotFiles[${index}]`);
689
+ if (!snapshotFile.ok) return snapshotFile;
690
+ if (typeof snapshotFile.value.path !== "string" || snapshotFile.value.path.trim() === "") {
691
+ return workflowValidationError(`${path}.snapshotFiles[${index}].path must be a non-empty string`);
692
+ }
693
+ if (typeof snapshotFile.value.text !== "string") {
694
+ return workflowValidationError(`${path}.snapshotFiles[${index}].text must be a string`);
695
+ }
696
+ }
697
+ }
698
+ return ok(undefined);
699
+ }
700
+ if (skill.value.registryKind !== "apm") {
701
+ return workflowValidationError(`${path}.registryKind must be apm`);
702
+ }
703
+ for (const key of ["registry", "package", "version", "integrity", "contentHash"]) {
704
+ if (typeof skill.value[key] !== "string" || skill.value[key].trim() === "") {
705
+ return workflowValidationError(`${path}.${key} must be a non-empty string`);
706
+ }
707
+ }
708
+ if (!isExactSemver(String(skill.value.version))) {
709
+ return workflowValidationError(`${path}.version must be an exact semver version`);
710
+ }
711
+ if (skill.value.snapshotFiles !== undefined) {
712
+ return workflowValidationError(`${path}.snapshotFiles is not supported for registry-package skills`);
713
+ }
714
+ return ok(undefined);
715
+ }
716
+
717
+ function validateMemberPluginBindingResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
718
+ const plugin = recordResult(value, path);
719
+ if (!plugin.ok) return plugin;
720
+ if (plugin.value.kind !== "local-root-installed") {
721
+ return workflowValidationError(`${path}.kind must be local-root-installed`);
722
+ }
723
+ if (typeof plugin.value.id !== "string" || plugin.value.id.trim() === "") {
724
+ return workflowValidationError(`${path}.id must be a non-empty string`);
725
+ }
726
+ return ok(undefined);
727
+ }
728
+
729
+ function teamConfigResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
730
+ const team = recordResult(value, path);
731
+ if (!team.ok) return team;
732
+ const template = decodePromptTemplate(team.value.promptTemplate, `${path}.promptTemplate`);
733
+ return template.ok ? ok(undefined) : template;
734
+ }
735
+
736
+ function memberArrayResult(value: unknown, path: string, requireNonEmpty: boolean): Result<void, DomainWorkflowError> {
737
+ if (!Array.isArray(value)) {
738
+ return workflowValidationError(`${path} must be an array`);
739
+ }
740
+ if (requireNonEmpty && value.length === 0) {
741
+ return workflowValidationError(`${path} must include at least one Member`);
742
+ }
743
+ const seen = new Set<string>();
744
+ for (const [index, member] of value.entries()) {
745
+ const record = recordResult(member, `${path}[${index}]`);
746
+ if (!record.ok) return record;
747
+ const validation = memberConfigResult(record.value, `${path}[${index}]`);
748
+ if (!validation.ok) return validation;
749
+ const id = String(record.value.id);
750
+ if (seen.has(id)) {
751
+ return workflowValidationError(`${path} must not include duplicate Member ${id}`);
752
+ }
753
+ seen.add(id);
754
+ }
755
+ return ok(undefined);
756
+ }
757
+
758
+ function memberConfigResult(member: Record<string, unknown>, path: string): Result<void, DomainWorkflowError> {
759
+ if (typeof member.id !== "string" || member.id.trim() === "") {
760
+ return workflowValidationError(`${path}.id must be a non-empty string`);
761
+ }
762
+ const promptTemplate = decodePromptTemplate(member.promptTemplate, `${path}.promptTemplate`);
763
+ if (!promptTemplate.ok) return promptTemplate;
764
+ if (typeof member.model !== "string" || member.model.trim() === "") {
765
+ return workflowValidationError(`${path}.model must be a non-empty string`);
766
+ }
767
+ for (const validation of [
768
+ allowedResult(`${path}.reasoningEffort`, member.reasoningEffort, REASONING_EFFORTS),
769
+ member.serviceTier === undefined ? ok(undefined) : allowedResult(`${path}.serviceTier`, member.serviceTier, SERVICE_TIERS),
770
+ memberExecutionResult(member.execution, `${path}.execution`),
771
+ memberApprovalResult(member.approval, `${path}.approval`)
772
+ ]) {
773
+ if (!validation.ok) {
774
+ return validation;
775
+ }
776
+ }
777
+ if (!Array.isArray(member.skills)) {
778
+ return workflowValidationError(`${path}.skills must be an array`);
779
+ }
780
+ for (const [index, skill] of member.skills.entries()) {
781
+ const validation = validateSkillBindingResult(skill, `${path}.skills[${index}]`);
782
+ if (!validation.ok) return validation;
783
+ }
784
+ if (member.plugins !== undefined) {
785
+ if (!Array.isArray(member.plugins)) {
786
+ return workflowValidationError(`${path}.plugins must be an array`);
787
+ }
788
+ for (const [index, plugin] of member.plugins.entries()) {
789
+ const validation = validateMemberPluginBindingResult(plugin, `${path}.plugins[${index}]`);
790
+ if (!validation.ok) return validation;
791
+ }
792
+ }
793
+ return ok(undefined);
794
+ }
795
+
796
+ function managerConfigResult(manager: Record<string, unknown>, path: string): Result<void, DomainWorkflowError> {
797
+ const keys = exactKeysResult(manager, ["id", "plugins", "promptTemplate", "skills"], path);
798
+ if (!keys.ok) return keys;
799
+ if (typeof manager.id !== "string" || manager.id.trim() === "") {
800
+ return workflowValidationError(`${path}.id must be a non-empty string`);
801
+ }
802
+ const promptTemplate = decodePromptTemplate(manager.promptTemplate, `${path}.promptTemplate`);
803
+ if (!promptTemplate.ok) return promptTemplate;
804
+ if (!Array.isArray(manager.skills)) {
805
+ return workflowValidationError(`${path}.skills must be an array`);
806
+ }
807
+ for (const [index, skill] of manager.skills.entries()) {
808
+ const validation = validateSkillBindingResult(skill, `${path}.skills[${index}]`);
809
+ if (!validation.ok) return validation;
810
+ }
811
+ if (!Array.isArray(manager.plugins)) {
812
+ return workflowValidationError(`${path}.plugins must be an array`);
813
+ }
814
+ for (const [index, plugin] of manager.plugins.entries()) {
815
+ const validation = validateMemberPluginBindingResult(plugin, `${path}.plugins[${index}]`);
816
+ if (!validation.ok) return validation;
817
+ }
818
+ return ok(undefined);
819
+ }
820
+
821
+ function executorEntityResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
822
+ const executor = recordResult(value, path);
823
+ if (!executor.ok) return executor;
824
+ if (executor.value.kind === "team") {
825
+ return teamExecutorResult(executor.value, path);
826
+ }
827
+ if (executor.value.kind === "member") {
828
+ return memberExecutorResult(executor.value, path);
829
+ }
830
+ return workflowValidationError(`${path}.kind must be team or member`);
831
+ }
832
+
833
+ function teamExecutorResult(team: Record<string, unknown>, path: string): Result<void, DomainWorkflowError> {
834
+ if (typeof team.id !== "string" || team.id.trim() === "") {
835
+ return workflowValidationError(`${path}.id must be a non-empty string`);
836
+ }
837
+ const planner = teamPlannerResult(team.planner, `${path}.planner`);
838
+ if (!planner.ok) return planner;
839
+ if (!Array.isArray(team.members)) {
840
+ return workflowValidationError(`${path}.members must be an array`);
841
+ }
842
+ const seen = new Set<string>();
843
+ for (const [index, membership] of team.members.entries()) {
844
+ const validation = membershipResult(membership, `${path}.members[${index}]`);
845
+ if (!validation.ok) return validation;
846
+ const executorId = String((membership as { executorId: unknown }).executorId);
847
+ if (seen.has(executorId)) {
848
+ return workflowValidationError(`${path}.members must not include duplicate Membership ${executorId}`);
849
+ }
850
+ seen.add(executorId);
851
+ }
852
+ return packageLockResult(team.packageLock, `${path}.packageLock`);
853
+ }
854
+
855
+ function teamPlannerResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
856
+ const planner = recordResult(value, path);
857
+ if (!planner.ok) return planner;
858
+ const template = decodePromptTemplate(planner.value.promptTemplate, `${path}.promptTemplate`);
859
+ if (!template.ok) return template;
860
+ if (planner.value.maxAttemptCount !== undefined) {
861
+ const maxAttemptCount = positiveIntegerResult(planner.value.maxAttemptCount, `${path}.maxAttemptCount`);
862
+ if (!maxAttemptCount.ok) return maxAttemptCount;
863
+ }
864
+ return guardrailsResult(planner.value.guardrails, `${path}.guardrails`);
865
+ }
866
+
867
+ function membershipResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
868
+ const membership = recordResult(value, path);
869
+ if (!membership.ok) return membership;
870
+ if (typeof membership.value.executorId !== "string" || membership.value.executorId.trim() === "") {
871
+ return workflowValidationError(`${path}.executorId must be a non-empty string`);
872
+ }
873
+ return visibleProfileResult(membership.value.visibleProfile, `${path}.visibleProfile`);
874
+ }
875
+
876
+ function visibleProfileResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
877
+ const profile = recordResult(value, path);
878
+ if (!profile.ok) return profile;
879
+ const kind = allowedResult(`${path}.kind`, profile.value.kind, ["team", "member"] as const);
880
+ if (!kind.ok) return kind;
881
+ for (const key of ["label", "summary"]) {
882
+ if (profile.value[key] !== undefined && (typeof profile.value[key] !== "string" || String(profile.value[key]).trim() === "")) {
883
+ return workflowValidationError(`${path}.${key} must be a non-empty string when provided`);
884
+ }
885
+ }
886
+ if (profile.value.capabilities !== undefined) {
887
+ if (!Array.isArray(profile.value.capabilities)) {
888
+ return workflowValidationError(`${path}.capabilities must be an array`);
889
+ }
890
+ for (const [index, capability] of profile.value.capabilities.entries()) {
891
+ if (typeof capability !== "string" || capability.trim() === "") {
892
+ return workflowValidationError(`${path}.capabilities[${index}] must be a non-empty string`);
893
+ }
894
+ }
895
+ }
896
+ return ok(undefined);
897
+ }
898
+
899
+ function memberExecutorResult(member: Record<string, unknown>, path: string): Result<void, DomainWorkflowError> {
900
+ if (typeof member.id !== "string" || member.id.trim() === "") {
901
+ return workflowValidationError(`${path}.id must be a non-empty string`);
902
+ }
903
+ const promptTemplate = decodePromptTemplate(member.promptTemplate, `${path}.promptTemplate`);
904
+ if (!promptTemplate.ok) return promptTemplate;
905
+ if (!Array.isArray(member.resources)) {
906
+ return workflowValidationError(`${path}.resources must be an array`);
907
+ }
908
+ for (const [index, resource] of member.resources.entries()) {
909
+ const validation = resourceBindingResult(resource, `${path}.resources[${index}]`);
910
+ if (!validation.ok) return validation;
911
+ }
912
+ const runtimePolicy = runtimePolicyResult(member.runtimePolicy, `${path}.runtimePolicy`);
913
+ if (!runtimePolicy.ok) return runtimePolicy;
914
+ return packageLockResult(member.packageLock, `${path}.packageLock`);
915
+ }
916
+
917
+ function runtimePolicyResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
918
+ const policy = recordResult(value, path);
919
+ if (!policy.ok) return policy;
920
+ if (typeof policy.value.model !== "string" || policy.value.model.trim() === "") {
921
+ return workflowValidationError(`${path}.model must be a non-empty string`);
922
+ }
923
+ for (const validation of [
924
+ allowedResult(`${path}.reasoningEffort`, policy.value.reasoningEffort, REASONING_EFFORTS),
925
+ policy.value.serviceTier === undefined ? ok(undefined) : allowedResult(`${path}.serviceTier`, policy.value.serviceTier, SERVICE_TIERS),
926
+ memberExecutionResult(policy.value.execution, `${path}.execution`),
927
+ memberApprovalResult(policy.value.approval, `${path}.approval`)
928
+ ]) {
929
+ if (!validation.ok) {
930
+ return validation;
931
+ }
932
+ }
933
+ return ok(undefined);
934
+ }
935
+
936
+ function resourceEntityResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
937
+ const resource = recordResult(value, path);
938
+ if (!resource.ok) return resource;
939
+ if (typeof resource.value.id !== "string" || resource.value.id.trim() === "") {
940
+ return workflowValidationError(`${path}.id must be a non-empty string`);
941
+ }
942
+ const binding = resourceBindingResult(resource.value.binding, `${path}.binding`);
943
+ if (!binding.ok) return binding;
944
+ return packageLockResult(resource.value.packageLock, `${path}.packageLock`);
945
+ }
946
+
947
+ function resourceBindingResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
948
+ const binding = recordResult(value, path);
949
+ if (!binding.ok) return binding;
950
+ if (binding.value.kind === "skill") {
951
+ return validateSkillBindingResult(binding.value.skill, `${path}.skill`);
952
+ }
953
+ if (binding.value.kind === "plugin") {
954
+ return validateMemberPluginBindingResult(binding.value.plugin, `${path}.plugin`);
955
+ }
956
+ if (binding.value.kind === "package") {
957
+ return packageLockResult(binding.value.lock, `${path}.lock`);
958
+ }
959
+ return workflowValidationError(`${path}.kind must be skill, plugin, or package`);
960
+ }
961
+
962
+ function packageLockResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
963
+ if (value === undefined) {
964
+ return ok(undefined);
965
+ }
966
+ const lock = recordResult(value, path);
967
+ if (!lock.ok) return lock;
968
+ for (const key of ["origin", "kind", "key", "version", "integrity"]) {
969
+ if (typeof lock.value[key] !== "string" || String(lock.value[key]).trim() === "") {
970
+ return workflowValidationError(`${path}.${key} must be a non-empty string`);
971
+ }
972
+ }
973
+ return ok(undefined);
974
+ }
975
+
976
+ function memberExecutionResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
977
+ const execution = recordResult(value, path);
978
+ if (!execution.ok) return execution;
979
+ const kind = allowedResult(`${path}.kind`, execution.value.kind, MEMBER_EXECUTION_KINDS);
980
+ if (!kind.ok) return kind;
981
+ const network = allowedResult(`${path}.network`, execution.value.network, MEMBER_EXECUTION_NETWORKS);
982
+ return network.ok ? ok(undefined) : network;
983
+ }
984
+
985
+ function memberApprovalResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
986
+ const approval = recordResult(value, path);
987
+ if (!approval.ok) return approval;
988
+ const policy = allowedResult(`${path}.policy`, approval.value.policy, MEMBER_APPROVAL_POLICIES);
989
+ if (!policy.ok) return policy;
990
+ if (approval.value.policy === "on_request") {
991
+ const reviewer = allowedResult(`${path}.reviewer`, approval.value.reviewer, MEMBER_APPROVAL_REVIEWERS);
992
+ return reviewer.ok ? ok(undefined) : reviewer;
993
+ }
994
+ return "reviewer" in approval.value
995
+ ? workflowValidationError(`${path}.reviewer is only allowed when policy is on_request`)
996
+ : ok(undefined);
997
+ }
998
+
999
+ function guardrailsResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
1000
+ if (value === undefined) {
1001
+ return ok(undefined);
1002
+ }
1003
+ if (!Array.isArray(value)) {
1004
+ return workflowValidationError(`${path} must be an array`);
1005
+ }
1006
+ for (const [index, entry] of value.entries()) {
1007
+ const guardrail = recordResult(entry, `${path}[${index}]`);
1008
+ if (!guardrail.ok) return guardrail;
1009
+ if (typeof guardrail.value.name !== "string" || guardrail.value.name.trim() === "") {
1010
+ return workflowValidationError(`${path}[${index}].name must be a non-empty string`);
1011
+ }
1012
+ const scope = allowedResult(`${path}[${index}].scope`, guardrail.value.scope, GUARDRAIL_SCOPES);
1013
+ if (!scope.ok) return scope;
1014
+ if (typeof guardrail.value.rule !== "string" || guardrail.value.rule.trim() === "") {
1015
+ return workflowValidationError(`${path}[${index}].rule must be a non-empty string`);
1016
+ }
1017
+ const severity = allowedResult(`${path}[${index}].severity`, guardrail.value.severity, GUARDRAIL_SEVERITIES);
1018
+ if (!severity.ok) return severity;
1019
+ }
1020
+ return ok(undefined);
1021
+ }
1022
+
1023
+ function recordResult(value: unknown, path: string): Result<Record<string, unknown>, DomainWorkflowError> {
1024
+ return value && typeof value === "object" && !Array.isArray(value)
1025
+ ? ok(value as Record<string, unknown>)
1026
+ : workflowValidationError(`${path} must be an object`);
1027
+ }
1028
+
1029
+ function exactKeysResult(record: Record<string, unknown>, allowedKeys: readonly string[], path: string): Result<void, DomainWorkflowError> {
1030
+ const missing = allowedKeys.filter(key => !(key in record));
1031
+ if (missing.length > 0) {
1032
+ return workflowValidationError(`${path} is missing required keys: ${missing.join(", ")}`);
1033
+ }
1034
+ const extra = Object.keys(record).filter(key => !allowedKeys.includes(key));
1035
+ return extra.length > 0
1036
+ ? workflowValidationError(`${path} has unsupported keys: ${extra.join(", ")}`)
1037
+ : ok(undefined);
1038
+ }
1039
+
1040
+ function positiveIntegerResult(value: unknown, path: string): Result<void, DomainWorkflowError> {
1041
+ return Number.isInteger(value) && Number(value) >= 1
1042
+ ? ok(undefined)
1043
+ : workflowValidationError(`${path} must be a positive integer`);
1044
+ }
1045
+
1046
+ function allowedResult<const T extends string>(path: string, value: unknown, allowed: readonly T[]): Result<T, DomainWorkflowError> {
1047
+ return typeof value === "string" && allowed.includes(value as T)
1048
+ ? ok(value as T)
1049
+ : workflowValidationError(`${path} must be one of: ${allowed.join(", ")}`);
1050
+ }
1051
+
1052
+ function chainResult<T, U>(result: Result<T, DomainWorkflowError>, next: () => Result<U, DomainWorkflowError>): Result<U, DomainWorkflowError> {
1053
+ return result.ok ? next() : result;
1054
+ }
1055
+
1056
+ function workflowValidationError(message: string): Result<never, DomainWorkflowError> {
1057
+ return err({ type: "DomainWorkflowError", message });
1058
+ }
1059
+
1060
+ function requireHarnessMemberConfig(protocol: HarnessSnapshot, executorId: string): MemberConfig {
1061
+ const member = getHarnessMemberConfig(protocol, executorId);
1062
+ if (!member) {
1063
+ throw new DomainInvariantError(`Unknown Member ${executorId} for Harness ${protocol.kind}`);
1064
+ }
1065
+ return member;
1066
+ }
1067
+
1068
+ function validateHubPackageLock(lock: HubPackageLock, expectedKind: HubPackageKind, label: string): void {
1069
+ assertText(lock.origin, `${label} origin must be non-empty`);
1070
+ if (lock.kind !== expectedKind) {
1071
+ throw new DomainInvariantError(`${label} kind must be ${expectedKind}`);
1072
+ }
1073
+ assertText(lock.key, `${label} key must be non-empty`);
1074
+ assertText(lock.version, `${label} version must be non-empty`);
1075
+ assertText(lock.integrity, `${label} integrity must be non-empty`);
1076
+ }
1077
+
1078
+ function validateTeamConfig(value: unknown, path: string): void {
1079
+ const team = requireRecord(value, path);
1080
+ assertValidPromptTemplate(team.promptTemplate, `${path}.promptTemplate`);
1081
+ }
1082
+
1083
+ function validateMemberArray(value: unknown, path: string, requireNonEmpty: boolean): void {
1084
+ if (!Array.isArray(value)) {
1085
+ throw new DomainInvariantError(`${path} must be an array`);
1086
+ }
1087
+ if (requireNonEmpty && value.length === 0) {
1088
+ throw new DomainInvariantError(`${path} must include at least one Member`);
1089
+ }
1090
+ const seen = new Set<string>();
1091
+ value.forEach((member, index) => {
1092
+ const record = requireRecord(member, `${path}[${index}]`);
1093
+ assertValidMemberConfig(record, `${path}[${index}]`);
1094
+ const id = String(record.id);
1095
+ if (seen.has(id)) {
1096
+ throw new DomainInvariantError(`${path} must not include duplicate Member ${id}`);
1097
+ }
1098
+ seen.add(id);
1099
+ });
1100
+ }
1101
+
1102
+ function assertValidMemberConfig(member: Record<string, unknown>, path: string): void {
1103
+ if (typeof member.id !== "string" || member.id.trim() === "") {
1104
+ throw new DomainInvariantError(`${path}.id must be a non-empty string`);
1105
+ }
1106
+ assertValidPromptTemplate(member.promptTemplate, `${path}.promptTemplate`);
1107
+ if (typeof member.model !== "string" || member.model.trim() === "") {
1108
+ throw new DomainInvariantError(`${path}.model must be a non-empty string`);
1109
+ }
1110
+ assertAllowed(`${path}.reasoningEffort`, member.reasoningEffort, REASONING_EFFORTS);
1111
+ if (member.serviceTier !== undefined) {
1112
+ assertAllowed(`${path}.serviceTier`, member.serviceTier, SERVICE_TIERS);
1113
+ }
1114
+ validateMemberExecution(member.execution, `${path}.execution`);
1115
+ validateMemberApproval(member.approval, `${path}.approval`);
1116
+ if (!Array.isArray(member.skills)) {
1117
+ throw new DomainInvariantError(`${path}.skills must be an array`);
1118
+ }
1119
+ member.skills.forEach((skill, index) => validateSkillBinding(skill, `${path}.skills[${index}]`));
1120
+ if (member.plugins !== undefined) {
1121
+ if (!Array.isArray(member.plugins)) {
1122
+ throw new DomainInvariantError(`${path}.plugins must be an array`);
1123
+ }
1124
+ member.plugins.forEach((plugin, index) => validateMemberPluginBinding(plugin, `${path}.plugins[${index}]`));
1125
+ }
1126
+ }
1127
+
1128
+ function validateMemberPluginBinding(value: unknown, path: string): void {
1129
+ const validation = validateMemberPluginBindingResult(value, path);
1130
+ if (!validation.ok) {
1131
+ throw new DomainInvariantError(validation.error.message);
1132
+ }
1133
+ }
1134
+
1135
+ function validateMemberExecution(value: unknown, path: string): void {
1136
+ const execution = requireRecord(value, path);
1137
+ assertAllowed(`${path}.kind`, execution.kind, MEMBER_EXECUTION_KINDS);
1138
+ assertAllowed(`${path}.network`, execution.network, MEMBER_EXECUTION_NETWORKS);
1139
+ }
1140
+
1141
+ function validateMemberApproval(value: unknown, path: string): void {
1142
+ const approval = requireRecord(value, path);
1143
+ assertAllowed(`${path}.policy`, approval.policy, MEMBER_APPROVAL_POLICIES);
1144
+ if (approval.policy === "on_request") {
1145
+ assertAllowed(`${path}.reviewer`, approval.reviewer, MEMBER_APPROVAL_REVIEWERS);
1146
+ return;
1147
+ }
1148
+ if ("reviewer" in approval) {
1149
+ throw new DomainInvariantError(`${path}.reviewer is only allowed when policy is on_request`);
1150
+ }
1151
+ }
1152
+
1153
+ function validateGuardrails(value: unknown, path: string): void {
1154
+ if (value === undefined) {
1155
+ return;
1156
+ }
1157
+ if (!Array.isArray(value)) {
1158
+ throw new DomainInvariantError(`${path} must be an array`);
1159
+ }
1160
+ value.forEach((entry, index) => {
1161
+ const guardrail = requireRecord(entry, `${path}[${index}]`);
1162
+ if (typeof guardrail.name !== "string" || guardrail.name.trim() === "") {
1163
+ throw new DomainInvariantError(`${path}[${index}].name must be a non-empty string`);
1164
+ }
1165
+ assertAllowed(`${path}[${index}].scope`, guardrail.scope, GUARDRAIL_SCOPES);
1166
+ if (typeof guardrail.rule !== "string" || guardrail.rule.trim() === "") {
1167
+ throw new DomainInvariantError(`${path}[${index}].rule must be a non-empty string`);
1168
+ }
1169
+ assertAllowed(`${path}[${index}].severity`, guardrail.severity, GUARDRAIL_SEVERITIES);
1170
+ });
1171
+ }
1172
+
1173
+ function requireRecord(value: unknown, path: string): Record<string, unknown> {
1174
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1175
+ throw new DomainInvariantError(`${path} must be an object`);
1176
+ }
1177
+ return value as Record<string, unknown>;
1178
+ }
1179
+
1180
+ function assertPositiveInteger(value: unknown, path: string): void {
1181
+ if (!Number.isInteger(value) || Number(value) < 1) {
1182
+ throw new DomainInvariantError(`${path} must be a positive integer`);
1183
+ }
1184
+ }
1185
+
1186
+ function assertAllowed<const T extends string>(path: string, value: unknown, allowed: readonly T[]): asserts value is T {
1187
+ if (typeof value !== "string" || !allowed.includes(value as T)) {
1188
+ throw new DomainInvariantError(`${path} must be one of: ${allowed.join(", ")}`);
1189
+ }
1190
+ }
1191
+
1192
+ function assertText(value: string, message: string): void {
1193
+ if (value.trim() === "") {
1194
+ throw new DomainInvariantError(message);
1195
+ }
1196
+ }
1197
+
1198
+ function assertString(value: unknown, message: string): void {
1199
+ if (typeof value !== "string") {
1200
+ throw new DomainInvariantError(message);
1201
+ }
1202
+ }
1203
+
1204
+ function cloneMemberConfig(member: MemberConfig): MemberConfig {
1205
+ return {
1206
+ ...member,
1207
+ promptTemplate: { ...member.promptTemplate },
1208
+ skills: cloneSkills(member.skills),
1209
+ plugins: clonePlugins(member.plugins ?? []),
1210
+ execution: cloneMemberExecution(member.execution),
1211
+ approval: cloneMemberApproval(member.approval)
1212
+ };
1213
+ }
1214
+
1215
+ function cloneManagerConfig(manager: ManagerConfig): ManagerConfig {
1216
+ return {
1217
+ ...manager,
1218
+ promptTemplate: { ...manager.promptTemplate },
1219
+ skills: cloneSkills(manager.skills),
1220
+ plugins: clonePlugins(manager.plugins)
1221
+ };
1222
+ }
1223
+
1224
+ function cloneMemberExecution(execution: MemberExecutionConstraint): MemberExecutionConstraint {
1225
+ return { ...execution };
1226
+ }
1227
+
1228
+ function cloneMemberApproval(approval: MemberApprovalConstraint): MemberApprovalConstraint {
1229
+ return { ...approval };
1230
+ }
1231
+
1232
+ function cloneSkills(skills: SkillBinding[] = []): SkillBinding[] {
1233
+ return skills.map(cloneSkill);
1234
+ }
1235
+
1236
+ function cloneSkill(skill: SkillBinding): SkillBinding {
1237
+ if (skill.kind === "local-snapshot") {
1238
+ return {
1239
+ ...skill,
1240
+ snapshotFiles: skill.snapshotFiles?.map(file => ({ ...file }))
1241
+ };
1242
+ }
1243
+ return { ...skill };
1244
+ }
1245
+
1246
+ function clonePlugins(plugins: MemberPluginBinding[] = []): MemberPluginBinding[] {
1247
+ return plugins.map(plugin => ({ ...plugin }));
1248
+ }
1249
+
1250
+ function isExactSemver(value: string): boolean {
1251
+ return /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value);
1252
+ }