@zq-silk/yui 0.6.16 → 0.7.1

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 (70) hide show
  1. package/dist/cli/commandCatalog.js +3 -7
  2. package/dist/cli.js +12 -33
  3. package/dist/commands/executionAuditCommands.js +19 -0
  4. package/dist/commands/globalRoleCommands.js +70 -0
  5. package/dist/commands/taskActor.js +3 -2
  6. package/dist/commands/taskCommands.js +160 -41
  7. package/dist/commands/taskContextCommand.js +1 -1
  8. package/dist/commands/taskInputCommands.js +3 -2
  9. package/dist/commands/taskRoleRuntimeStatus.js +3 -3
  10. package/dist/context/contextSnapshot.js +228 -0
  11. package/dist/context/roleSessionContext.js +3 -1
  12. package/dist/context/runContextContract.js +162 -0
  13. package/dist/context/runContextPack.js +322 -0
  14. package/dist/context/sessionBootstrapManifest.js +81 -0
  15. package/dist/context/sessionProtocolIdentity.js +23 -0
  16. package/dist/controller/agentRuntimeObserver.js +6 -1
  17. package/dist/controller/controller.js +4 -3
  18. package/dist/controller/fileSchedulerStoreAdapter.js +446 -145
  19. package/dist/controller/jobControl.js +2 -1
  20. package/dist/controller/runtime.js +83 -0
  21. package/dist/controller/runtimeHookRunFence.js +6 -2
  22. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  23. package/dist/coordination/workMailbox.js +25 -22
  24. package/dist/executor/agentAdapter.js +7 -2
  25. package/dist/executor/agentExecutor.js +23 -0
  26. package/dist/executor/effectiveLaunch.js +24 -0
  27. package/dist/executor/executorRegistry.js +7 -1
  28. package/dist/executor/fileRoleLaunchPlanner.js +73 -27
  29. package/dist/lifecycle/exactRunTerminalization.js +2 -3
  30. package/dist/lifecycle/providerErrorClass.js +8 -3
  31. package/dist/observability/executionAudit.js +87 -2
  32. package/dist/repository/taskWorkspacePreparer.js +2 -2
  33. package/dist/run/agentRun.js +101 -16
  34. package/dist/run/providerRetry.js +167 -56
  35. package/dist/run/providerRetryConfig.js +5 -1
  36. package/dist/run/runControlRequest.js +50 -0
  37. package/dist/runtime/agentDriver.js +47 -0
  38. package/dist/runtime/agentHost.js +327 -0
  39. package/dist/runtime/builtinAgentDrivers.js +23 -1
  40. package/dist/runtime/builtinTranscriptObserver.js +4 -0
  41. package/dist/runtime/builtinTranscriptUsage.js +2 -0
  42. package/dist/runtime/exactControlPlane.js +2 -2
  43. package/dist/runtime/globalProcessExitStore.js +38 -0
  44. package/dist/runtime/launchBroker.js +95 -0
  45. package/dist/runtime/processExitObservation.js +60 -0
  46. package/dist/runtime/runtimeBinding.js +6 -0
  47. package/dist/runtime/runtimeObservation.js +27 -6
  48. package/dist/runtime/runtimeProjection.js +6 -3
  49. package/dist/runtime/runtimeStopReceipt.js +42 -0
  50. package/dist/runtime/sessionTerminationGuard.js +13 -0
  51. package/dist/runtime/tmuxAdapters.js +203 -220
  52. package/dist/scheduler/activeRoleRunDelivery.js +24 -3
  53. package/dist/scheduler/leaderWakeupProcessor.js +18 -60
  54. package/dist/scheduler/roleRunLiveness.js +61 -27
  55. package/dist/storage/migration/productionRegistry.js +264 -0
  56. package/dist/storage/sqliteSchema.js +23 -2
  57. package/dist/storage/sqliteStore.js +48 -4
  58. package/dist/storage/taskStore.js +54 -5
  59. package/dist/storage/upgrade/recordVersions.js +3 -1
  60. package/dist/storage/upgrade/sqliteStateMigration.js +10 -0
  61. package/dist/task/taskRecordReference.js +1 -0
  62. package/dist/tmux/tmuxManager.js +15 -4
  63. package/dist/web/assets/client/components.js +1 -1
  64. package/package.json +1 -1
  65. package/skills/yui-leader/SKILL.md +10 -5
  66. package/skills/yui-operator/SKILL.md +4 -0
  67. package/skills/yui-reviewer/SKILL.md +4 -0
  68. package/skills/yui-runtime/SKILL.md +61 -0
  69. package/skills/yui-worker/SKILL.md +82 -218
  70. package/dist/executor/managedClaudeRunner.js +0 -121
@@ -0,0 +1,228 @@
1
+ import { createHash } from "node:crypto";
2
+ import { normalizedUniqueText, optionalText, requireIdentity, requirePositiveInteger, requireText, requireTimestamp } from "../domain/validation.js";
3
+ export const CONTEXT_SNAPSHOT_SCHEMA_VERSION = 1;
4
+ export const CONTEXT_SNAPSHOT_MAX_RESOURCES = 256;
5
+ export const CONTEXT_SNAPSHOT_MAX_RESOURCE_BYTES = 4 * 1024 * 1024;
6
+ export const CONTEXT_SNAPSHOT_MAX_BYTES = 8 * 1024 * 1024;
7
+ export const CONTEXT_REF_LAYERS = ["L1", "L2", "L3", "L4"];
8
+ export const CONTEXT_SNAPSHOT_SCOPES = ["task", "workitem", "stage"];
9
+ export function createContextSnapshot(input) {
10
+ const identity = snapshotIdentity(input);
11
+ const refs = normalizeContextRefs(input.refs);
12
+ const resources = normalizeSnapshotResources(input.resources, refs);
13
+ const acceptRefs = Object.freeze([...normalizedUniqueText(input.acceptRefs, "Context Snapshot acceptance reference")]
14
+ .sort((left, right) => left.localeCompare(right)));
15
+ const repoCommit = optionalText(input.repoCommit, "Context Snapshot repository commit");
16
+ const parentRef = input.parentRef === undefined
17
+ ? undefined
18
+ : validateContextSnapshotRef(input.parentRef);
19
+ if (parentRef !== undefined) {
20
+ if (parentRef.taskId !== identity.taskId) {
21
+ throw new Error("Context Snapshot parent belongs to another Task.");
22
+ }
23
+ if (parentRef.sequence >= identity.sequence) {
24
+ throw new Error("Context Snapshot parent sequence must precede its child.");
25
+ }
26
+ }
27
+ if (input.frozenBy !== "leader" && input.frozenBy !== "controller") {
28
+ throw new Error("Context Snapshot frozenBy is invalid.");
29
+ }
30
+ const frozenAt = requireTimestamp(input.frozenAt.toISOString(), "Context Snapshot frozenAt");
31
+ const digest = contentDigest({
32
+ taskId: identity.taskId,
33
+ scope: identity.scope,
34
+ ...(identity.scopeRef === undefined ? {} : { scopeRef: identity.scopeRef }),
35
+ sequence: identity.sequence,
36
+ refs,
37
+ resources,
38
+ ...(repoCommit === undefined ? {} : { repoCommit }),
39
+ acceptRefs,
40
+ ...(parentRef === undefined ? {} : { parentRef }),
41
+ frozenAt,
42
+ frozenBy: input.frozenBy
43
+ });
44
+ return Object.freeze({
45
+ schemaVersion: CONTEXT_SNAPSHOT_SCHEMA_VERSION,
46
+ ...identity,
47
+ digest,
48
+ refs,
49
+ resources,
50
+ ...(repoCommit === undefined ? {} : { repoCommit }),
51
+ acceptRefs,
52
+ ...(parentRef === undefined ? {} : { parentRef }),
53
+ frozenAt,
54
+ frozenBy: input.frozenBy
55
+ });
56
+ }
57
+ export function validateContextSnapshot(snapshot) {
58
+ if (snapshot.schemaVersion !== CONTEXT_SNAPSHOT_SCHEMA_VERSION) {
59
+ throw new Error("Context Snapshot must use schemaVersion 1.");
60
+ }
61
+ const identity = snapshotIdentity(snapshot);
62
+ const refs = normalizeContextRefs(snapshot.refs);
63
+ if (JSON.stringify(refs) !== JSON.stringify(snapshot.refs)) {
64
+ throw new Error("Context Snapshot refs must use canonical order.");
65
+ }
66
+ const resources = normalizeSnapshotResources(snapshot.resources, refs);
67
+ if (JSON.stringify(resources) !== JSON.stringify(snapshot.resources)) {
68
+ throw new Error("Context Snapshot resources must use canonical ref order.");
69
+ }
70
+ const acceptRefs = Object.freeze([...normalizedUniqueText(snapshot.acceptRefs, "Context Snapshot acceptance reference")]
71
+ .sort((left, right) => left.localeCompare(right)));
72
+ if (JSON.stringify(acceptRefs) !== JSON.stringify(snapshot.acceptRefs)) {
73
+ throw new Error("Context Snapshot acceptance refs must use canonical order.");
74
+ }
75
+ const repoCommit = optionalText(snapshot.repoCommit, "Context Snapshot repository commit");
76
+ const parentRef = snapshot.parentRef === undefined
77
+ ? undefined
78
+ : validateContextSnapshotRef(snapshot.parentRef);
79
+ if (parentRef !== undefined
80
+ && (parentRef.taskId !== identity.taskId || parentRef.sequence >= identity.sequence)) {
81
+ throw new Error("Context Snapshot parent is invalid.");
82
+ }
83
+ requireTimestamp(snapshot.frozenAt, "Context Snapshot frozenAt");
84
+ if (snapshot.frozenBy !== "leader" && snapshot.frozenBy !== "controller") {
85
+ throw new Error("Context Snapshot frozenBy is invalid.");
86
+ }
87
+ requireDigest(snapshot.digest, "Context Snapshot digest");
88
+ const expected = contentDigest({
89
+ taskId: identity.taskId,
90
+ scope: identity.scope,
91
+ ...(identity.scopeRef === undefined ? {} : { scopeRef: identity.scopeRef }),
92
+ sequence: identity.sequence,
93
+ refs,
94
+ resources,
95
+ ...(repoCommit === undefined ? {} : { repoCommit }),
96
+ acceptRefs,
97
+ ...(parentRef === undefined ? {} : { parentRef }),
98
+ frozenAt: snapshot.frozenAt,
99
+ frozenBy: snapshot.frozenBy
100
+ });
101
+ if (snapshot.digest !== expected) {
102
+ throw new Error("Context Snapshot digest does not match its content.");
103
+ }
104
+ return snapshot;
105
+ }
106
+ export function contextSnapshotRef(snapshot) {
107
+ validateContextSnapshot(snapshot);
108
+ return Object.freeze({
109
+ schemaVersion: CONTEXT_SNAPSHOT_SCHEMA_VERSION,
110
+ id: snapshot.id,
111
+ taskId: snapshot.taskId,
112
+ scope: snapshot.scope,
113
+ ...(snapshot.scopeRef === undefined ? {} : { scopeRef: snapshot.scopeRef }),
114
+ sequence: snapshot.sequence,
115
+ digest: snapshot.digest
116
+ });
117
+ }
118
+ export function validateContextSnapshotRef(ref) {
119
+ if (ref.schemaVersion !== CONTEXT_SNAPSHOT_SCHEMA_VERSION) {
120
+ throw new Error("Context Snapshot ref must use schemaVersion 1.");
121
+ }
122
+ const identity = snapshotIdentity(ref);
123
+ requireDigest(ref.digest, "Context Snapshot ref digest");
124
+ return Object.freeze({
125
+ schemaVersion: CONTEXT_SNAPSHOT_SCHEMA_VERSION,
126
+ ...identity,
127
+ digest: ref.digest
128
+ });
129
+ }
130
+ export function contextContentDigest(value) {
131
+ return contentDigest(value);
132
+ }
133
+ function snapshotIdentity(input) {
134
+ const id = requireIdentity(input.id, "Context Snapshot id");
135
+ const taskId = requireIdentity(input.taskId, "Context Snapshot Task id");
136
+ if (!CONTEXT_SNAPSHOT_SCOPES.includes(input.scope)) {
137
+ throw new Error("Context Snapshot scope is invalid.");
138
+ }
139
+ const scopeRef = optionalText(input.scopeRef, "Context Snapshot scope ref");
140
+ if (input.scope === "task" && scopeRef !== undefined) {
141
+ throw new Error("A task Context Snapshot cannot carry scopeRef.");
142
+ }
143
+ if (input.scope !== "task" && scopeRef === undefined) {
144
+ throw new Error(`A ${input.scope} Context Snapshot requires scopeRef.`);
145
+ }
146
+ return {
147
+ id,
148
+ taskId,
149
+ scope: input.scope,
150
+ ...(scopeRef === undefined ? {} : { scopeRef }),
151
+ sequence: requirePositiveInteger(input.sequence, "Context Snapshot sequence")
152
+ };
153
+ }
154
+ function normalizeContextRefs(values) {
155
+ if (!Array.isArray(values))
156
+ throw new Error("Context Snapshot refs must be an array.");
157
+ const refs = values.map(validateContextRef).sort((left, right) => (contextRefKey(left).localeCompare(contextRefKey(right))));
158
+ const keys = refs.map(contextRefKey);
159
+ if (new Set(keys).size !== keys.length) {
160
+ throw new Error("Context Snapshot refs must be unique.");
161
+ }
162
+ return Object.freeze(refs);
163
+ }
164
+ function validateContextRef(value) {
165
+ if (!CONTEXT_REF_LAYERS.includes(value.layer)) {
166
+ throw new Error("Context ref layer is invalid.");
167
+ }
168
+ const store = requireIdentity(value.store, "Context ref store");
169
+ const refId = requireText(value.refId, "Context ref id");
170
+ const revision = requireText(value.revision, "Context ref revision");
171
+ const digest = requireDigest(value.digest, "Context ref digest");
172
+ const summary = optionalText(value.summary, "Context ref summary");
173
+ const evidenceOf = optionalText(value.evidenceOf, "Context ref evidence target");
174
+ return Object.freeze({
175
+ layer: value.layer,
176
+ store,
177
+ refId,
178
+ revision,
179
+ digest,
180
+ ...(summary === undefined ? {} : { summary }),
181
+ ...(evidenceOf === undefined ? {} : { evidenceOf })
182
+ });
183
+ }
184
+ function normalizeSnapshotResources(values, refs) {
185
+ if (!Array.isArray(values))
186
+ throw new Error("Context Snapshot resources must be an array.");
187
+ if (values.length > CONTEXT_SNAPSHOT_MAX_RESOURCES) {
188
+ throw new Error(`Context Snapshot exceeds ${CONTEXT_SNAPSHOT_MAX_RESOURCES} resources.`);
189
+ }
190
+ const byRef = new Map(refs.map((ref) => [contextRefKey(ref), ref]));
191
+ const resources = values.map((resource) => {
192
+ if (resource === null || typeof resource !== "object" || Array.isArray(resource)) {
193
+ throw new Error("Context Snapshot resource must be an object.");
194
+ }
195
+ const ref = validateContextRef(resource.ref);
196
+ const exact = byRef.get(contextRefKey(ref));
197
+ if (exact === undefined || contextContentDigest(resource.value) !== ref.digest) {
198
+ throw new Error(`Context Snapshot resource does not match its ref: ${ref.store}/${ref.refId}.`);
199
+ }
200
+ const serialized = JSON.stringify(resource.value);
201
+ if (serialized === undefined) {
202
+ throw new Error("Context Snapshot resource value is not JSON serializable.");
203
+ }
204
+ if (Buffer.byteLength(serialized, "utf8") > CONTEXT_SNAPSHOT_MAX_RESOURCE_BYTES) {
205
+ throw new Error(`Context Snapshot resource exceeds ${CONTEXT_SNAPSHOT_MAX_RESOURCE_BYTES} bytes.`);
206
+ }
207
+ return Object.freeze({ ref: exact, value: resource.value });
208
+ }).sort((left, right) => contextRefKey(left.ref).localeCompare(contextRefKey(right.ref)));
209
+ if (resources.length !== refs.length
210
+ || new Set(resources.map(({ ref }) => contextRefKey(ref))).size !== refs.length) {
211
+ throw new Error("Context Snapshot resources must cover every ref exactly once.");
212
+ }
213
+ if (Buffer.byteLength(JSON.stringify(resources), "utf8") > CONTEXT_SNAPSHOT_MAX_BYTES) {
214
+ throw new Error(`Context Snapshot resources exceed ${CONTEXT_SNAPSHOT_MAX_BYTES} bytes.`);
215
+ }
216
+ return Object.freeze(resources);
217
+ }
218
+ function contextRefKey(ref) {
219
+ return [ref.layer, ref.store, ref.refId, ref.revision, ref.digest].join("\0");
220
+ }
221
+ function requireDigest(value, label) {
222
+ if (!/^[0-9a-f]{64}$/u.test(value))
223
+ throw new Error(`${label} must be SHA-256 hex.`);
224
+ return value;
225
+ }
226
+ function contentDigest(value) {
227
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
228
+ }
@@ -4,6 +4,7 @@ import { join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { SYSTEM_LEADER_ROLE, SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
6
6
  const BUILTIN_YUI_SKILLS = new Set([
7
+ "yui-runtime",
7
8
  "yui-operator",
8
9
  "yui-leader",
9
10
  "yui-worker",
@@ -17,6 +18,7 @@ export function compileRoleSessionContext(yuiHome, role, owner, options = {}) {
17
18
  const kind = roleSessionKind(role, owner, options.purpose ?? "execution");
18
19
  const builtInSkillId = kind === "global" ? undefined : `yui-${kind}`;
19
20
  const skillIds = unique([
21
+ "yui-runtime",
20
22
  ...(builtInSkillId === undefined ? [] : [builtInSkillId]),
21
23
  ...(role.skills ?? [])
22
24
  ]);
@@ -40,7 +42,7 @@ function renderDeveloperInstructions(kind, role, owner) {
40
42
  ].filter((value) => value !== null);
41
43
  return [...core, ...profile].join("\n");
42
44
  }
43
- function roleSessionKind(role, owner, purpose) {
45
+ export function roleSessionKind(role, owner, purpose) {
44
46
  if (owner.scope === "global") {
45
47
  return role.name === SYSTEM_OPERATOR_ROLE ? "operator" : "global";
46
48
  }
@@ -0,0 +1,162 @@
1
+ import { normalizedUniqueIdentities, requireIdentity, requireText } from "../domain/validation.js";
2
+ import { validateContextSnapshotRef } from "./contextSnapshot.js";
3
+ export const RUN_CONTEXT_PROTOCOL_VERSION = 1;
4
+ export const RUN_BOOTSTRAP_PROTOCOL = "yui-run/v1";
5
+ export const RUN_BOOTSTRAP_MAX_BYTES = 4 * 1024;
6
+ export const RUN_BOOTSTRAP_MAX_DELTAS = 16;
7
+ export const RUN_ACTIONS = [
8
+ "lead-task",
9
+ "leader-wake",
10
+ "execute-work-item",
11
+ "repair-work-item",
12
+ "review-round",
13
+ "review-delta",
14
+ "global-request"
15
+ ];
16
+ export function createRunAssignment(input) {
17
+ const common = normalizeCommon(input);
18
+ return Object.freeze({
19
+ schemaVersion: RUN_CONTEXT_PROTOCOL_VERSION,
20
+ ...common,
21
+ ...(input.directive === undefined
22
+ ? {}
23
+ : { directive: requireText(input.directive, "Run assignment directive") })
24
+ });
25
+ }
26
+ export function validateRunAssignment(value) {
27
+ if (value.schemaVersion !== RUN_CONTEXT_PROTOCOL_VERSION) {
28
+ throw new Error("Run assignment must use schemaVersion 1.");
29
+ }
30
+ const normalized = createRunAssignment(value);
31
+ if (JSON.stringify(normalized) !== JSON.stringify(value)) {
32
+ throw new Error("Run assignment is not canonical.");
33
+ }
34
+ return value;
35
+ }
36
+ export function createRunBootstrapEnvelope(assignment) {
37
+ validateRunAssignment(assignment);
38
+ return Object.freeze({
39
+ protocol: RUN_BOOTSTRAP_PROTOCOL,
40
+ runId: assignment.runId,
41
+ roleName: assignment.roleName,
42
+ purpose: assignment.purpose,
43
+ action: assignment.action,
44
+ subject: assignment.subject,
45
+ ...(assignment.contextSnapshotRef === undefined
46
+ ? {}
47
+ : { contextSnapshotRef: assignment.contextSnapshotRef }),
48
+ deltaRefIds: assignment.deltaRefIds
49
+ });
50
+ }
51
+ export function validateRunBootstrapEnvelope(value) {
52
+ if (value.protocol !== RUN_BOOTSTRAP_PROTOCOL) {
53
+ throw new Error("Run bootstrap protocol is unsupported.");
54
+ }
55
+ normalizeCommon(value);
56
+ const serialized = serializeRunBootstrapEnvelope(value);
57
+ if (Buffer.byteLength(serialized, "utf8") > RUN_BOOTSTRAP_MAX_BYTES) {
58
+ throw new Error("Run bootstrap envelope exceeds its protocol byte limit.");
59
+ }
60
+ return value;
61
+ }
62
+ export function serializeRunBootstrapEnvelope(value) {
63
+ if (value.protocol !== RUN_BOOTSTRAP_PROTOCOL) {
64
+ throw new Error("Run bootstrap protocol is unsupported.");
65
+ }
66
+ const normalized = normalizeCommon(value);
67
+ const subject = Object.entries(normalized.subject)
68
+ .map(([key, id]) => `${key}:${id}`)
69
+ .join(",");
70
+ const snapshot = normalized.contextSnapshotRef;
71
+ const lines = [
72
+ "Yui managed Run. Follow the Session Manifest and injected Skills.",
73
+ `${normalized.subject.taskId === undefined ? "" : `task=${normalized.subject.taskId} `}run=${normalized.runId} role=${normalized.roleName} action=${normalized.action}`,
74
+ `purpose=${normalized.purpose} subject=${subject || "global"} snapshot=${snapshot === undefined ? "none" : `${snapshot.id}@${snapshot.digest}`}`,
75
+ normalized.deltaRefIds.length === 0
76
+ ? "delta=none"
77
+ : `delta=${normalized.deltaRefIds.join(",")}`,
78
+ "Load the exact Run context before acting; fail closed if it is unavailable or mismatched."
79
+ ];
80
+ const serialized = lines.join("\n");
81
+ if (Buffer.byteLength(serialized, "utf8") > RUN_BOOTSTRAP_MAX_BYTES) {
82
+ throw new Error("Run bootstrap envelope exceeds its protocol byte limit.");
83
+ }
84
+ return serialized;
85
+ }
86
+ /**
87
+ * Bounded infrastructure-recovery input for an already-pushed Run. It resumes
88
+ * the provider-native transcript after Host/child replacement and never
89
+ * replays the Assignment or its directive.
90
+ */
91
+ export function serializeRunHostRecoveryEnvelope(value) {
92
+ const normalized = normalizeCommon(value);
93
+ const serialized = [
94
+ "Yui managed Host recovery for an existing Run.",
95
+ `${normalized.subject.taskId === undefined ? "" : `task=${normalized.subject.taskId} `}run=${normalized.runId} role=${normalized.roleName}`,
96
+ "Resume the same native conversation from its latest durable state. Do not repeat completed work or replay the original Assignment.",
97
+ "Load the exact Run context or delta only if needed, then continue toward the existing workflow outcome."
98
+ ].join("\n");
99
+ if (Buffer.byteLength(serialized, "utf8") > RUN_BOOTSTRAP_MAX_BYTES) {
100
+ throw new Error("Run Host recovery envelope exceeds its protocol byte limit.");
101
+ }
102
+ return serialized;
103
+ }
104
+ function normalizeCommon(input) {
105
+ if (!["execution", "review", "global"].includes(input.purpose)) {
106
+ throw new Error("Run assignment purpose is invalid.");
107
+ }
108
+ if (!RUN_ACTIONS.includes(input.action)) {
109
+ throw new Error("Run assignment action is invalid.");
110
+ }
111
+ const subject = normalizeSubject(input.subject);
112
+ const snapshot = input.contextSnapshotRef === undefined
113
+ ? undefined
114
+ : validateContextSnapshotRef(input.contextSnapshotRef);
115
+ if (snapshot !== undefined && snapshot.taskId !== subject.taskId) {
116
+ throw new Error("Run assignment Context Snapshot belongs to another Task.");
117
+ }
118
+ if (input.purpose !== "global" && subject.taskId === undefined) {
119
+ throw new Error("A Task Run assignment requires a Task subject.");
120
+ }
121
+ if (input.purpose === "review" && subject.reviewRoundId === undefined) {
122
+ throw new Error("A review Run assignment requires a ReviewRound subject.");
123
+ }
124
+ const deltaRefIds = normalizedUniqueIdentities(input.deltaRefIds, "Run assignment delta ref");
125
+ if (deltaRefIds.length > RUN_BOOTSTRAP_MAX_DELTAS) {
126
+ throw new Error(`Run assignment supports at most ${RUN_BOOTSTRAP_MAX_DELTAS} delta refs.`);
127
+ }
128
+ return {
129
+ runId: requireIdentity(input.runId, "Run assignment Run id"),
130
+ roleName: requireIdentity(input.roleName, "Run assignment Role name"),
131
+ purpose: input.purpose,
132
+ action: input.action,
133
+ subject,
134
+ ...(snapshot === undefined ? {} : { contextSnapshotRef: snapshot }),
135
+ deltaRefIds
136
+ };
137
+ }
138
+ function normalizeSubject(subject) {
139
+ const normalized = Object.fromEntries(Object.entries(subject).map(([key, value]) => [
140
+ key,
141
+ requireIdentity(value, `Run assignment ${key}`)
142
+ ]));
143
+ const allowed = [
144
+ "taskId",
145
+ "workItemId",
146
+ "reviewRoundId",
147
+ "executionGroupId",
148
+ "executionLaneId"
149
+ ];
150
+ if (Object.keys(normalized).some((key) => !allowed.includes(key))) {
151
+ throw new Error("Run assignment subject contains an unknown field.");
152
+ }
153
+ if ((normalized.executionGroupId === undefined) !== (normalized.executionLaneId === undefined)) {
154
+ throw new Error("Run assignment execution lineage is incomplete.");
155
+ }
156
+ if (normalized.taskId === undefined
157
+ && (normalized.workItemId !== undefined || normalized.reviewRoundId !== undefined
158
+ || normalized.executionGroupId !== undefined)) {
159
+ throw new Error("Run assignment child subject requires a Task id.");
160
+ }
161
+ return Object.freeze(normalized);
162
+ }