@xemahq/dsl 0.6.1 → 0.8.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 (58) hide show
  1. package/dist/deliverable-spec/lib/schema.d.ts +2 -0
  2. package/dist/deliverable-spec/lib/schema.d.ts.map +1 -1
  3. package/dist/deliverable-spec/lib/schema.js +10 -1
  4. package/dist/deliverable-spec/lib/schema.js.map +1 -1
  5. package/dist/schema/reusable-workflow.schema.json +9 -1
  6. package/dist/schema/workflow.schema.json +39 -13
  7. package/dist/workflow/index.d.ts +3 -0
  8. package/dist/workflow/index.d.ts.map +1 -1
  9. package/dist/workflow/index.js +7 -1
  10. package/dist/workflow/index.js.map +1 -1
  11. package/dist/workflow/lib/compiler/compile.d.ts.map +1 -1
  12. package/dist/workflow/lib/compiler/compile.js +105 -20
  13. package/dist/workflow/lib/compiler/compile.js.map +1 -1
  14. package/dist/workflow/lib/compiler/concurrency.js +1 -1
  15. package/dist/workflow/lib/compiler/concurrency.js.map +1 -1
  16. package/dist/workflow/lib/compiler/manifest-source.d.ts +2 -1
  17. package/dist/workflow/lib/compiler/manifest-source.d.ts.map +1 -1
  18. package/dist/workflow/lib/compiler/manifest-source.js +13 -4
  19. package/dist/workflow/lib/compiler/manifest-source.js.map +1 -1
  20. package/dist/workflow/lib/compiler/review-step.d.ts +2 -1
  21. package/dist/workflow/lib/compiler/review-step.d.ts.map +1 -1
  22. package/dist/workflow/lib/compiler/review-step.js +13 -1
  23. package/dist/workflow/lib/compiler/review-step.js.map +1 -1
  24. package/dist/workflow/lib/compiler/types.d.ts +3 -2
  25. package/dist/workflow/lib/compiler/types.d.ts.map +1 -1
  26. package/dist/workflow/lib/deliverable-spec-introspection-error.d.ts +12 -0
  27. package/dist/workflow/lib/deliverable-spec-introspection-error.d.ts.map +1 -0
  28. package/dist/workflow/lib/deliverable-spec-introspection-error.js +33 -0
  29. package/dist/workflow/lib/deliverable-spec-introspection-error.js.map +1 -0
  30. package/dist/workflow/lib/deliverable-spec-keys.d.ts +1 -1
  31. package/dist/workflow/lib/deliverable-spec-keys.d.ts.map +1 -1
  32. package/dist/workflow/lib/deliverable-spec-keys.js +27 -52
  33. package/dist/workflow/lib/deliverable-spec-keys.js.map +1 -1
  34. package/dist/workflow/lib/deliverable-spec-source-scan.d.ts +6 -0
  35. package/dist/workflow/lib/deliverable-spec-source-scan.d.ts.map +1 -0
  36. package/dist/workflow/lib/deliverable-spec-source-scan.js +189 -0
  37. package/dist/workflow/lib/deliverable-spec-source-scan.js.map +1 -0
  38. package/dist/workflow/lib/expression/context.d.ts +1 -1
  39. package/dist/workflow/lib/expression/context.d.ts.map +1 -1
  40. package/dist/workflow/lib/expression/context.js +1 -1
  41. package/dist/workflow/lib/expression/context.js.map +1 -1
  42. package/dist/workflow/lib/types.d.ts +1 -1
  43. package/dist/workflow/lib/types.d.ts.map +1 -1
  44. package/package.json +2 -2
  45. package/schema/reusable-workflow.schema.json +9 -1
  46. package/schema/workflow.schema.json +39 -13
  47. package/src/deliverable-spec/lib/schema.ts +23 -0
  48. package/src/workflow/index.ts +7 -0
  49. package/src/workflow/lib/compiler/compile.ts +302 -149
  50. package/src/workflow/lib/compiler/concurrency.ts +1 -1
  51. package/src/workflow/lib/compiler/manifest-source.ts +23 -8
  52. package/src/workflow/lib/compiler/review-step.ts +33 -2
  53. package/src/workflow/lib/compiler/types.ts +9 -9
  54. package/src/workflow/lib/deliverable-spec-introspection-error.ts +63 -0
  55. package/src/workflow/lib/deliverable-spec-keys.ts +88 -50
  56. package/src/workflow/lib/deliverable-spec-source-scan.ts +280 -0
  57. package/src/workflow/lib/expression/context.ts +2 -2
  58. package/src/workflow/lib/types.ts +38 -8
@@ -6,6 +6,7 @@ import {
6
6
  type ActionRef,
7
7
  type Briefcase,
8
8
  type CompiledJob,
9
+ type CompiledManifestSource,
9
10
  type CompiledRun,
10
11
  type PermissionResource,
11
12
  type PermissionScope,
@@ -75,7 +76,8 @@ import type {
75
76
  * 8. Emit CompiledRun with a deterministic sha256.
76
77
  */
77
78
  export function compileWorkflow(input: CompileInput): CompiledRun {
78
- const { workflowRef, trigger, resolvedRefs, workflowDefinitionVersionSha256 } = input;
79
+ const { workflowRef, trigger, resolvedRefs, workflowDefinitionContentHash } =
80
+ input;
79
81
 
80
82
  // Fold `defaults.gate` into every decision-gate job's `with:` BEFORE any
81
83
  // other pass — expression validation, needs-shape validation, and the
@@ -83,7 +85,11 @@ export function compileWorkflow(input: CompileInput): CompiledRun {
83
85
  // like inline-authored values. Explicit per-gate keys always win.
84
86
  const workflow = applyGateDefaults(input.workflow, resolvedRefs);
85
87
 
86
- const inputs = bindTriggerInputs(workflow, trigger, input.previewMode ?? false);
88
+ const inputs = bindTriggerInputs(
89
+ workflow,
90
+ trigger,
91
+ input.previewMode ?? false,
92
+ );
87
93
  const vars: Readonly<Record<string, unknown>> = workflow.vars ?? {};
88
94
 
89
95
  // W5 definition-vs-run boundary: the CompiledRun is the single
@@ -103,7 +109,12 @@ export function compileWorkflow(input: CompileInput): CompiledRun {
103
109
  ...(workflow.requires?.wallets ?? []),
104
110
  ]) as readonly string[];
105
111
  const permissions = normalizePermissions(workflow.permissions);
106
- const concurrency = compileConcurrency(workflow.concurrency, trigger, inputs, vars);
112
+ const concurrency = compileConcurrency(
113
+ workflow.concurrency,
114
+ trigger,
115
+ inputs,
116
+ vars,
117
+ );
107
118
  const defaults = resolveWorkflowDefaults(workflow.defaults);
108
119
 
109
120
  // Validate every expression everywhere at compile time. We throw away the
@@ -161,7 +172,10 @@ export function compileWorkflow(input: CompileInput): CompiledRun {
161
172
  // job's declared deliverable spec. Skips when the engine couldn't
162
173
  // pre-fetch spec content (preview mode or specs without introspectable
163
174
  // shape).
164
- validateDeliverableValueExpressions(workflow, input.resolvedDeliverableSpecs ?? {});
175
+ validateDeliverableValueExpressions(
176
+ workflow,
177
+ input.resolvedDeliverableSpecs ?? {},
178
+ );
165
179
 
166
180
  // Payload reach-in validation: `needs.<X>.outputs.<name>.<field>`
167
181
  // chains where `<field>` is not an ArtifactRef envelope field reach
@@ -189,128 +203,150 @@ export function compileWorkflow(input: CompileInput): CompiledRun {
189
203
  }),
190
204
  );
191
205
 
192
- const compiledJobs: CompiledJob[] = orderedJobs.map(({ key, payload, needs }) => {
193
- const resolved = lookupResolvedRef(payload.uses, resolvedRefs);
194
- const actionRef: ActionRef = {
195
- id: resolved.id,
196
- version: resolved.version,
197
- manifestSha256: resolved.manifestSha256,
198
- executionKind: resolveJobExecutionKind(key, payload.with, resolved),
199
- taskQueue: resolved.taskQueue,
200
- actionKind: resolveActionKind(resolved),
201
- isReusableWorkflow: resolved.isReusableWorkflow,
202
- // Pin the manifest's `inputs:` schema into the compiled ref so the
203
- // worker validates `with:` against the schema that was in effect at
204
- // compile time — not whatever's currently published. Reusable
205
- // workflows have their own `workflow_call.inputs`, validated by
206
- // their own compile pass.
207
- inputsSchema: resolved.isReusableWorkflow
208
- ? null
209
- : resolved.actionManifest?.spec.inputs ?? null,
210
- };
211
-
212
- const jobPermissions = normalizePermissions(payload.permissions);
213
- assertJobPermissionsFit(jobPermissions, permissions, key);
214
-
215
- const strategy = compileStrategy(payload.strategy, key);
216
-
217
- // When the job uses a reusable workflow, mount planning is governed
218
- // by the reusable workflow's own jobs, not by this job. So we emit an
219
- // empty plan — the child workflow's compiler run will produce its own.
220
- const mountPlan = resolved.isReusableWorkflow
221
- ? { readOnly: {}, readWrite: {} }
222
- : compileMountPlan(key, payload.with, resolved.actionManifest);
223
-
224
- // For agent-shaped actions, pre-resolve the manifest source so the
225
- // worker sees a single discriminated shape (`ref` / `inline` /
226
- // `inline-deferred`) instead of branching on input fields at
227
- // dispatch. Reusable-workflow jobs return null — their nested
228
- // CompiledRun owns its own agent steps.
229
- const manifestSource = resolved.isReusableWorkflow
206
+ const manifestSourcesByJob: Record<string, CompiledManifestSource | null> =
207
+ {};
208
+ for (const [jobKey, job] of Object.entries(workflow.jobs)) {
209
+ const resolved = lookupResolvedRef(job.uses, resolvedRefs);
210
+ manifestSourcesByJob[jobKey] = resolved.isReusableWorkflow
230
211
  ? null
231
- : compileManifestSource(key, payload.with, resolved.actionManifest);
212
+ : compileManifestSource(
213
+ jobKey,
214
+ job.with,
215
+ resolved.actionManifest,
216
+ input.resolvedAgents ?? {},
217
+ );
218
+ }
232
219
 
233
- const retry = resolveJobRetry(
234
- defaults.retry,
235
- resolved.actionManifest?.spec.retryDefaults ?? null,
236
- payload.retry,
237
- );
238
- const timeoutMs = resolveJobTimeout(
239
- defaults.timeoutMs,
240
- resolved.actionManifest?.spec.timeoutDefaults ?? null,
241
- payload.timeout,
242
- );
220
+ const compiledJobs: CompiledJob[] = orderedJobs.map(
221
+ ({ key, payload, needs }) => {
222
+ const resolved = lookupResolvedRef(payload.uses, resolvedRefs);
223
+ const actionRef: ActionRef = {
224
+ id: resolved.id,
225
+ version: resolved.version,
226
+ manifestSha256: resolved.manifestSha256,
227
+ executionKind: resolveJobExecutionKind(key, payload.with, resolved),
228
+ taskQueue: resolved.taskQueue,
229
+ actionKind: resolveActionKind(resolved),
230
+ isReusableWorkflow: resolved.isReusableWorkflow,
231
+ // Pin the manifest's `inputs:` schema into the compiled ref so the
232
+ // worker validates `with:` against the schema that was in effect at
233
+ // compile time — not whatever's currently published. Reusable
234
+ // workflows have their own `workflow_call.inputs`, validated by
235
+ // their own compile pass.
236
+ inputsSchema: resolved.isReusableWorkflow
237
+ ? null
238
+ : (resolved.actionManifest?.spec.inputs ?? null),
239
+ };
243
240
 
244
- // Validate `if` expression shape now — runtime evaluator only fails on
245
- // unknown bindings after this point. Shorthand: rewrite a
246
- // bare `needs.<X>` reference to `needs.<X>.outcome == 'ok'` so the
247
- // evaluator sees the explicit success check (an envelope object is
248
- // truthy even on failure — silent always-true is the trap this
249
- // rewrite explicitly avoids).
250
- const ifExpression = payload.if !== undefined
251
- ? (() => {
252
- const body = stripInterpolation(payload.if!);
253
- const rewritten = rewriteBareNeedsInIf(body);
254
- compileExpression(rewritten);
255
- return rewritten;
256
- })()
257
- : null;
258
-
259
- // For `xema/review@*` steps, rewrite `with:` from the author shape
260
- // (`subject` + `redraft.step`) to the worker contract
261
- // (`subjects` + embedded `redraft: { uses, with }`). Other steps
262
- // pass through verbatim.
263
- const compiledWith = rewriteReviewStepWith(key, payload, workflow);
264
-
265
- // Compile-time installation-binding gate: when the engine threaded
266
- // an installationScope through CompileInput, walk every literal
267
- // `x-installation-resource` field in the `with:` block and confirm
268
- // its value is bound to the calling installation. Rejects unbound
269
- // walletIds (etc.) BEFORE the dispatch creates a run, instead of
270
- // failing 3 activities later when connector-gateway-api refuses to
271
- // mint credentials. Skips entirely for system / org-wide dispatches.
272
- validateInstallationResourceBindings({
273
- jobKey: key,
274
- actionId: actionRef.id,
275
- inputsSchema: actionRef.inputsSchema,
276
- withValue: compiledWith,
277
- scope: input.installationScope,
278
- });
241
+ const jobPermissions = normalizePermissions(payload.permissions);
242
+ assertJobPermissionsFit(jobPermissions, permissions, key);
243
+
244
+ const strategy = compileStrategy(payload.strategy, key);
245
+
246
+ // When the job uses a reusable workflow, mount planning is governed
247
+ // by the reusable workflow's own jobs, not by this job. So we emit an
248
+ // empty plan — the child workflow's compiler run will produce its own.
249
+ const mountPlan = resolved.isReusableWorkflow
250
+ ? { readOnly: {}, readWrite: {} }
251
+ : compileMountPlan(key, payload.with, resolved.actionManifest);
252
+
253
+ // For agent-shaped actions, pre-resolve the manifest source so the
254
+ // worker sees one immutable revision pin or an explicitly deferred
255
+ // expression instead of branching on authoring selectors at dispatch.
256
+ // Reusable-workflow jobs return null — their nested CompiledRun owns its
257
+ // own agent steps.
258
+ const manifestSource = manifestSourcesByJob[key] ?? null;
259
+
260
+ const retry = resolveJobRetry(
261
+ defaults.retry,
262
+ resolved.actionManifest?.spec.retryDefaults ?? null,
263
+ payload.retry,
264
+ );
265
+ const timeoutMs = resolveJobTimeout(
266
+ defaults.timeoutMs,
267
+ resolved.actionManifest?.spec.timeoutDefaults ?? null,
268
+ payload.timeout,
269
+ );
279
270
 
280
- return Object.freeze({
281
- jobKey: key,
282
- title: payload.title ?? null,
283
- needs,
284
- matrixGather: Object.freeze([...(payload.matrixGather ?? [])]) as readonly string[],
285
- ifExpression,
286
- strategy,
287
- action: actionRef,
288
- mountPlan,
289
- manifestSource,
290
- with: Object.freeze({ ...compiledWith }) as Readonly<Record<string, unknown>>,
291
- // Strip the `${{ ... }}` wrapper at compile time so the runtime
292
- // evaluator receives expression bodies directly — same convention
293
- // as `ifExpression`. `validateAllAuthoredExpressions` already
294
- // compiled each body to surface invalid expressions as DSL errors.
295
- outputs: compileOutputsMap(payload.outputs, `jobs.${key}.outputs`),
296
- retry,
297
- timeoutMs,
298
- permissions: jobPermissions,
299
- // The DSL emits null; the engine (which owns the reusable-workflow
300
- // registry) post-processes each CompiledRun and attaches the nested
301
- // compiled body for jobs whose action is a reusable workflow.
302
- reusableCompiledRun: null,
303
- payloadReachIns: Object.freeze(
304
- collectPayloadReachInsForJob(
305
- workflow,
306
- key,
307
- payload,
308
- input.resolvedDeliverableSpecs ?? {},
309
- resolvedRefs,
271
+ // Validate `if` expression shape now — runtime evaluator only fails on
272
+ // unknown bindings after this point. Shorthand: rewrite a
273
+ // bare `needs.<X>` reference to `needs.<X>.outcome == 'ok'` so the
274
+ // evaluator sees the explicit success check (an envelope object is
275
+ // truthy even on failure — silent always-true is the trap this
276
+ // rewrite explicitly avoids).
277
+ const ifExpression =
278
+ payload.if !== undefined
279
+ ? (() => {
280
+ const body = stripInterpolation(payload.if!);
281
+ const rewritten = rewriteBareNeedsInIf(body);
282
+ compileExpression(rewritten);
283
+ return rewritten;
284
+ })()
285
+ : null;
286
+
287
+ // For `xema/review@*` steps, rewrite `with:` from the author shape
288
+ // (`subject` + `redraft.step`) to the worker contract
289
+ // (`subjects` + embedded `redraft: { uses, with }`). Other steps
290
+ // pass through verbatim.
291
+ const compiledWith = attachResolvedReviewerPins(
292
+ rewriteReviewStepWith(key, payload, workflow, manifestSourcesByJob),
293
+ input.resolvedAgents ?? {},
294
+ );
295
+
296
+ // Compile-time installation-binding gate: when the engine threaded
297
+ // an installationScope through CompileInput, walk every literal
298
+ // `x-installation-resource` field in the `with:` block and confirm
299
+ // its value is bound to the calling installation. Rejects unbound
300
+ // walletIds (etc.) BEFORE the dispatch creates a run, instead of
301
+ // failing 3 activities later when connector-gateway-api refuses to
302
+ // mint credentials. Skips entirely for system / org-wide dispatches.
303
+ validateInstallationResourceBindings({
304
+ jobKey: key,
305
+ actionId: actionRef.id,
306
+ inputsSchema: actionRef.inputsSchema,
307
+ withValue: compiledWith,
308
+ scope: input.installationScope,
309
+ });
310
+
311
+ return Object.freeze({
312
+ jobKey: key,
313
+ title: payload.title ?? null,
314
+ needs,
315
+ matrixGather: Object.freeze([
316
+ ...(payload.matrixGather ?? []),
317
+ ]) as readonly string[],
318
+ ifExpression,
319
+ strategy,
320
+ action: actionRef,
321
+ mountPlan,
322
+ manifestSource,
323
+ with: Object.freeze({ ...compiledWith }) as Readonly<
324
+ Record<string, unknown>
325
+ >,
326
+ // Strip the `${{ ... }}` wrapper at compile time so the runtime
327
+ // evaluator receives expression bodies directly — same convention
328
+ // as `ifExpression`. `validateAllAuthoredExpressions` already
329
+ // compiled each body to surface invalid expressions as DSL errors.
330
+ outputs: compileOutputsMap(payload.outputs, `jobs.${key}.outputs`),
331
+ retry,
332
+ timeoutMs,
333
+ permissions: jobPermissions,
334
+ // The DSL emits null; the engine (which owns the reusable-workflow
335
+ // registry) post-processes each CompiledRun and attaches the nested
336
+ // compiled body for jobs whose action is a reusable workflow.
337
+ reusableCompiledRun: null,
338
+ payloadReachIns: Object.freeze(
339
+ collectPayloadReachInsForJob(
340
+ workflow,
341
+ key,
342
+ payload,
343
+ input.resolvedDeliverableSpecs ?? {},
344
+ resolvedRefs,
345
+ ),
310
346
  ),
311
- ),
312
- }) as CompiledJob;
313
- });
347
+ }) as CompiledJob;
348
+ },
349
+ );
314
350
 
315
351
  const snapshotCreatedAt = input.trigger.triggeredAt;
316
352
  const workflowCallOutputs = compileOutputsMap(
@@ -332,7 +368,7 @@ export function compileWorkflow(input: CompileInput): CompiledRun {
332
368
  defaults,
333
369
  jobs: Object.freeze(compiledJobs) as readonly CompiledJob[],
334
370
  snapshotCreatedAt,
335
- workflowDefinitionVersionSha256,
371
+ workflowDefinitionContentHash,
336
372
  workflowCallOutputs,
337
373
  workflowOutputs,
338
374
  // Spread only when present so a run dispatched without a briefcase
@@ -593,6 +629,24 @@ function validateJobLiteralReferences(
593
629
  resolvedAgents,
594
630
  );
595
631
  validateReviewerAgents(jobKey, withMap['reviewers'], resolvedAgents);
632
+ validateReviewerAgents(jobKey, withMap['recipients'], resolvedAgents);
633
+ const escalationChain = withMap['escalationChain'];
634
+ if (Array.isArray(escalationChain)) {
635
+ for (let i = 0; i < escalationChain.length; i++) {
636
+ const step = escalationChain[i];
637
+ if (
638
+ step !== null &&
639
+ typeof step === 'object' &&
640
+ !Array.isArray(step)
641
+ ) {
642
+ validateReviewerAgents(
643
+ jobKey,
644
+ (step as Readonly<Record<string, unknown>>)['recipients'],
645
+ resolvedAgents,
646
+ );
647
+ }
648
+ }
649
+ }
596
650
  }
597
651
  if (resolvedSpecs) {
598
652
  assertLiteralDeliverableSpec(
@@ -626,12 +680,84 @@ function validateReviewerAgents(
626
680
  assertLiteralAgent(
627
681
  jobKey,
628
682
  `with.reviewers[${i}].agentRef`,
629
- (reviewer as Record<string, unknown>)['agentRef'],
683
+ (
684
+ (reviewer as Record<string, unknown>)['target'] as
685
+ | Readonly<Record<string, unknown>>
686
+ | undefined
687
+ )?.['agentRef'] ?? (reviewer as Record<string, unknown>)['agentRef'],
630
688
  resolvedAgents,
631
689
  );
632
690
  }
633
691
  }
634
692
 
693
+ /**
694
+ * Freeze literal Agent recipients at compile time. Reviewer/decision-gate
695
+ * targets are independent Agent invocations, so their authored `agentRef`
696
+ * selector must not survive as the runtime identity. The pin travels inside
697
+ * the target (whose action schema deliberately permits extension fields) and
698
+ * is copied into AgentDeciderActivity input by the deterministic workflow.
699
+ */
700
+ function attachResolvedReviewerPins(
701
+ withBlock: Readonly<Record<string, unknown>>,
702
+ resolvedAgents: Readonly<Record<string, ResolvedAgentMeta>>,
703
+ ): Readonly<Record<string, unknown>> {
704
+ const pinRecipients = (value: unknown): unknown => {
705
+ if (!Array.isArray(value)) return value;
706
+ return value.map((recipient) => {
707
+ if (
708
+ recipient === null ||
709
+ typeof recipient !== 'object' ||
710
+ Array.isArray(recipient)
711
+ ) {
712
+ return recipient;
713
+ }
714
+ const record = recipient as Readonly<Record<string, unknown>>;
715
+ if (record['kind'] !== 'agent') return recipient;
716
+ const target = record['target'];
717
+ if (
718
+ target === null ||
719
+ typeof target !== 'object' ||
720
+ Array.isArray(target)
721
+ ) {
722
+ return recipient;
723
+ }
724
+ const targetRecord = target as Readonly<Record<string, unknown>>;
725
+ const agentRef = targetRecord['agentRef'];
726
+ if (!isLiteralStringValue(agentRef)) return recipient;
727
+ const resolved = resolvedAgents[agentRef];
728
+ if (resolved === undefined) return recipient;
729
+ return Object.freeze({
730
+ ...record,
731
+ target: Object.freeze({
732
+ ...targetRecord,
733
+ agentRevisionPin: Object.freeze({
734
+ agentRevisionId: resolved.agentRevisionId,
735
+ agentContentHash: resolved.agentContentHash,
736
+ agentSlug: resolved.slug,
737
+ }),
738
+ }),
739
+ });
740
+ });
741
+ };
742
+
743
+ const out: Record<string, unknown> = { ...withBlock };
744
+ if ('reviewers' in out) out['reviewers'] = pinRecipients(out['reviewers']);
745
+ if ('recipients' in out) out['recipients'] = pinRecipients(out['recipients']);
746
+ if (Array.isArray(out['escalationChain'])) {
747
+ out['escalationChain'] = out['escalationChain'].map((step) => {
748
+ if (step === null || typeof step !== 'object' || Array.isArray(step)) {
749
+ return step;
750
+ }
751
+ const record = step as Readonly<Record<string, unknown>>;
752
+ return Object.freeze({
753
+ ...record,
754
+ recipients: pinRecipients(record['recipients']),
755
+ });
756
+ });
757
+ }
758
+ return Object.freeze(out);
759
+ }
760
+
635
761
  /**
636
762
  * Match the literal/expression policy used by `validateAllAuthoredExpressions`:
637
763
  * a value is "literal" when it is a string that does NOT contain `${{`. The
@@ -651,31 +777,22 @@ function assertLiteralAgent(
651
777
  if (!isLiteralStringValue(value)) {
652
778
  return;
653
779
  }
654
- // `value` is an `agentRef`: a `<slug>` or `<slug>@<version>` reference.
655
- // `resolvedAgents` is keyed by bare slug, so parse the slug segment off
656
- // before the existence check. This is the single agent-naming path.
657
- const slug = agentRefSlug(value);
658
- if (resolvedAgents[slug] !== undefined) {
780
+ if (resolvedAgents[value] !== undefined) {
659
781
  return;
660
782
  }
661
783
  const knownPreview = formatKnownPreview(Object.keys(resolvedAgents));
662
784
  throw new WorkflowDslError(
663
785
  WorkflowErrorCode.DSL_UNKNOWN_AGENT,
664
786
  `Job '${jobKey}' ${fieldPath} = '${value}' is not a registered agent. Known: [${knownPreview}].`,
665
- { jobKey, fieldPath, value, knownCount: Object.keys(resolvedAgents).length },
787
+ {
788
+ jobKey,
789
+ fieldPath,
790
+ value,
791
+ knownCount: Object.keys(resolvedAgents).length,
792
+ },
666
793
  );
667
794
  }
668
795
 
669
- /**
670
- * Parse the bare slug off an `agentRef`. An `agentRef` is either a
671
- * `<slug>` or a `<slug>@<version>` reference; `resolvedAgents` is keyed
672
- * by bare slug.
673
- */
674
- function agentRefSlug(ref: string): string {
675
- const at = ref.indexOf('@');
676
- return at === -1 ? ref : ref.slice(0, at);
677
- }
678
-
679
796
  function assertLiteralDeliverableSpec(
680
797
  jobKey: string,
681
798
  fieldPath: string,
@@ -816,11 +933,19 @@ function compileOutputsMap(
816
933
  */
817
934
  function compileWorkflowOutputs(
818
935
  workflow: WorkflowDocument,
819
- ): Readonly<Record<string, import('@xemahq/kernel-contracts/workflow').WorkflowOutputDescriptor>> {
936
+ ): Readonly<
937
+ Record<
938
+ string,
939
+ import('@xemahq/kernel-contracts/workflow').WorkflowOutputDescriptor
940
+ >
941
+ > {
820
942
  const raw = workflow.outputs;
821
943
  if (!raw || Object.keys(raw).length === 0) {
822
944
  return Object.freeze({}) as Readonly<
823
- Record<string, import('@xemahq/kernel-contracts/workflow').WorkflowOutputDescriptor>
945
+ Record<
946
+ string,
947
+ import('@xemahq/kernel-contracts/workflow').WorkflowOutputDescriptor
948
+ >
824
949
  >;
825
950
  }
826
951
  const seenSlugs = new Set<string>();
@@ -841,7 +966,11 @@ function compileWorkflowOutputs(
841
966
  throw new WorkflowDslError(
842
967
  WorkflowErrorCode.DSL_SEMANTIC_INVALID,
843
968
  `outputs.${name}: job "${decl.fromJob}" does not declare an output named "${decl.fromOutput}"`,
844
- { outputName: name, fromJob: decl.fromJob, fromOutput: decl.fromOutput },
969
+ {
970
+ outputName: name,
971
+ fromJob: decl.fromJob,
972
+ fromOutput: decl.fromOutput,
973
+ },
845
974
  );
846
975
  }
847
976
  if (seenSlugs.has(decl.slug)) {
@@ -881,7 +1010,10 @@ function compileWorkflowOutputs(
881
1010
  }
882
1011
  }
883
1012
  return Object.freeze(compiled) as Readonly<
884
- Record<string, import('@xemahq/kernel-contracts/workflow').WorkflowOutputDescriptor>
1013
+ Record<
1014
+ string,
1015
+ import('@xemahq/kernel-contracts/workflow').WorkflowOutputDescriptor
1016
+ >
885
1017
  >;
886
1018
  }
887
1019
 
@@ -912,7 +1044,11 @@ function validateMatrixGather(doc: WorkflowDocument): void {
912
1044
  { jobKey, target },
913
1045
  );
914
1046
  }
915
- if (!targetDecl.strategy || (!('matrix' in targetDecl.strategy) && !('dynamic' in targetDecl.strategy))) {
1047
+ if (
1048
+ !targetDecl.strategy ||
1049
+ (!('matrix' in targetDecl.strategy) &&
1050
+ !('dynamic' in targetDecl.strategy))
1051
+ ) {
916
1052
  throw new WorkflowDslError(
917
1053
  WorkflowErrorCode.DSL_SEMANTIC_INVALID,
918
1054
  `Job '${jobKey}' matrixGather references '${target}' which has no matrix/dynamic strategy; gathering only makes sense across matrix entries.`,
@@ -1189,7 +1325,8 @@ function validateNeedsAccess(
1189
1325
  }
1190
1326
 
1191
1327
  const keyBy = isDynamicMatrix
1192
- ? (upstream.strategy as { dynamic: { keyBy?: string } }).dynamic.keyBy ?? null
1328
+ ? ((upstream.strategy as { dynamic: { keyBy?: string } }).dynamic.keyBy ??
1329
+ null)
1193
1330
  : null;
1194
1331
 
1195
1332
  // Bare `needs.X.outputs` is always OK — consumer chooses to receive
@@ -1199,7 +1336,13 @@ function validateNeedsAccess(
1199
1336
  const head = access.steps[0]!;
1200
1337
 
1201
1338
  if (head.kind === 'member' && head.name === 'byKey') {
1202
- validateByKeyChain(access, keyBy, consumerJobKey, consumerBinding, fieldPath);
1339
+ validateByKeyChain(
1340
+ access,
1341
+ keyBy,
1342
+ consumerJobKey,
1343
+ consumerBinding,
1344
+ fieldPath,
1345
+ );
1203
1346
  return;
1204
1347
  }
1205
1348
 
@@ -1373,7 +1516,10 @@ function collectMatrixMemberPath(
1373
1516
 
1374
1517
  function assertStrategyInvariants(compiled: CompiledRun): void {
1375
1518
  for (const job of compiled.jobs) {
1376
- if (job.strategy.kind === MatrixStrategyKind.STATIC && job.strategy.entries.length === 0) {
1519
+ if (
1520
+ job.strategy.kind === MatrixStrategyKind.STATIC &&
1521
+ job.strategy.entries.length === 0
1522
+ ) {
1377
1523
  throw new WorkflowDslError(
1378
1524
  WorkflowErrorCode.DSL_SEMANTIC_INVALID,
1379
1525
  `Job '${job.jobKey}' static strategy expansion produced zero entries.`,
@@ -1384,7 +1530,8 @@ function assertStrategyInvariants(compiled: CompiledRun): void {
1384
1530
 
1385
1531
  // Permission escalation is already checked per-job. Cycle checked in DAG.
1386
1532
  // No-op placeholder kept explicit for the next author.
1387
- const _permissionRef: Readonly<Record<PermissionResource, PermissionScope>> = compiled.permissions;
1533
+ const _permissionRef: Readonly<Record<PermissionResource, PermissionScope>> =
1534
+ compiled.permissions;
1388
1535
  void _permissionRef;
1389
1536
  }
1390
1537
 
@@ -1408,7 +1555,13 @@ function validateDeliverableValueExpressions(
1408
1555
  for (const { source, fieldPath } of expressions) {
1409
1556
  const ast = compileExpression(source);
1410
1557
  walkDeliverableAccesses(ast, (access) => {
1411
- validateDeliverableAccess(doc, jobKey, fieldPath, access, resolvedSpecs);
1558
+ validateDeliverableAccess(
1559
+ doc,
1560
+ jobKey,
1561
+ fieldPath,
1562
+ access,
1563
+ resolvedSpecs,
1564
+ );
1412
1565
  });
1413
1566
  }
1414
1567
  }
@@ -147,7 +147,7 @@ function buildConcurrencyContext(
147
147
  kind: actorKindFromTrigger(trigger),
148
148
  },
149
149
  },
150
- workflow: { key: '', version: '' },
150
+ workflow: { key: '', revisionNumber: 0 },
151
151
  job: { key: '', attempt: 0 },
152
152
  },
153
153
  };
@@ -6,6 +6,7 @@ import {
6
6
  import { WorkflowDslError } from '../errors';
7
7
  import { ANY_INTERPOLATION_RE } from '../expression/interpolation';
8
8
  import type { ActionManifest } from '../types';
9
+ import type { ResolvedAgentMeta } from './types';
9
10
  import { isAgentShapedAction } from './action-shape';
10
11
 
11
12
  /**
@@ -14,11 +15,11 @@ import { isAgentShapedAction } from './action-shape';
14
15
  * `agentRef` is the SOLE way a workflow names its agent. Terminal shapes:
15
16
  * • `null` — non-agent action (the action manifest doesn't expose
16
17
  * `agentRef` on its `inputs:` schema).
17
- * • `{ kind: 'ref', ref }` — `agentRef` is a literal string. The
18
- * activity resolves the Agent it names at boot.
19
- * • `{ kind: 'inline-deferred' }` — `agentRef` is a `${{ … }}`
20
- * expression that must be evaluated per dispatch. The worker resolves
21
- * the named Agent after expression evaluation.
18
+ * • `{ kind: 'revision', ...pin }` — `agentRef` is a literal string
19
+ * already resolved by the engine to an immutable Agent revision.
20
+ * • `{ kind: 'deferred' }` — `agentRef` is a `${{ … }}` expression.
21
+ * The worker records one immutable resolution in Temporal history after
22
+ * expression evaluation and before scheduling the Agent activity.
22
23
  *
23
24
  * An agent-shaped action with no `agentRef` fails fast — there is no
24
25
  * inline `mounts` short-form anymore.
@@ -27,6 +28,7 @@ export function compileManifestSource(
27
28
  jobKey: string,
28
29
  withBlock: Readonly<Record<string, unknown>> | undefined,
29
30
  actionManifest: ActionManifest | null,
31
+ resolvedAgents: Readonly<Record<string, ResolvedAgentMeta>>,
30
32
  ): CompiledManifestSource | null {
31
33
  if (!isAgentShapedAction(actionManifest)) return null;
32
34
  const block = withBlock ?? {};
@@ -34,13 +36,26 @@ export function compileManifestSource(
34
36
  const ref = block['agentRef'];
35
37
  if (typeof ref === 'string' && ref.length > 0) {
36
38
  if (isLiteralString(ref)) {
37
- return Object.freeze({ kind: 'ref' as const, ref });
39
+ const resolved = resolvedAgents[ref];
40
+ if (resolved === undefined) {
41
+ throw new WorkflowDslError(
42
+ WorkflowErrorCode.DSL_UNKNOWN_AGENT,
43
+ `Job '${jobKey}' with.agentRef = '${ref}' was not resolved to an immutable Agent revision.`,
44
+ { jobKey, fieldPath: 'with.agentRef', value: ref },
45
+ );
46
+ }
47
+ return Object.freeze({
48
+ kind: 'revision' as const,
49
+ agentRevisionId: resolved.agentRevisionId,
50
+ agentContentHash: resolved.agentContentHash,
51
+ agentSlug: resolved.slug,
52
+ });
38
53
  }
39
- return Object.freeze({ kind: 'inline-deferred' as const });
54
+ return Object.freeze({ kind: 'deferred' as const });
40
55
  }
41
56
  if (ref !== undefined) {
42
57
  // ref is present but not a literal string → expression.
43
- return Object.freeze({ kind: 'inline-deferred' as const });
58
+ return Object.freeze({ kind: 'deferred' as const });
44
59
  }
45
60
 
46
61
  throw new WorkflowDslError(