@xemahq/dsl 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/dist/deliverable-spec/lib/schema.d.ts.map +1 -1
- package/dist/deliverable-spec/lib/schema.js.map +1 -1
- package/dist/payload-codec/index.d.ts +1 -0
- package/dist/payload-codec/index.d.ts.map +1 -1
- package/dist/payload-codec/index.js.map +1 -1
- package/dist/payload-codec/lib/codec.d.ts +1 -1
- package/dist/payload-codec/lib/codec.d.ts.map +1 -1
- package/dist/payload-codec/lib/codec.js +12 -1
- package/dist/payload-codec/lib/codec.js.map +1 -1
- package/dist/payload-codec/lib/payload.d.ts +9 -0
- package/dist/payload-codec/lib/payload.d.ts.map +1 -0
- package/dist/payload-codec/lib/payload.js +3 -0
- package/dist/payload-codec/lib/payload.js.map +1 -0
- package/dist/payload-codec/temporal/index.d.ts +5 -0
- package/dist/payload-codec/temporal/index.d.ts.map +1 -0
- package/dist/payload-codec/temporal/index.js +7 -0
- package/dist/payload-codec/temporal/index.js.map +1 -0
- package/dist/schema/workflow.schema.json +29 -2
- package/dist/workflow/index.d.ts +2 -2
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +3 -1
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/lib/compiler/canonical-json.js +15 -4
- package/dist/workflow/lib/compiler/canonical-json.js.map +1 -1
- package/dist/workflow/lib/compiler/compile.d.ts.map +1 -1
- package/dist/workflow/lib/compiler/compile.js +3 -1
- package/dist/workflow/lib/compiler/compile.js.map +1 -1
- package/dist/workflow/lib/compiler/gate-defaults.d.ts +5 -0
- package/dist/workflow/lib/compiler/gate-defaults.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/gate-defaults.js +76 -0
- package/dist/workflow/lib/compiler/gate-defaults.js.map +1 -0
- package/dist/workflow/lib/compiler/index.d.ts +1 -0
- package/dist/workflow/lib/compiler/index.d.ts.map +1 -1
- package/dist/workflow/lib/compiler/index.js +4 -1
- package/dist/workflow/lib/compiler/index.js.map +1 -1
- package/dist/workflow/lib/duration.d.ts.map +1 -1
- package/dist/workflow/lib/duration.js +3 -9
- package/dist/workflow/lib/duration.js.map +1 -1
- package/dist/workflow/lib/types.d.ts +9 -0
- package/dist/workflow/lib/types.d.ts.map +1 -1
- package/dist/workspace-manifest/lib/compile.js.map +1 -1
- package/dist/workspace-manifest/lib/interpolate.js +26 -4
- package/dist/workspace-manifest/lib/interpolate.js.map +1 -1
- package/dist/workspace-manifest/lib/schema.d.ts +4 -0
- package/dist/workspace-manifest/lib/schema.d.ts.map +1 -1
- package/dist/workspace-manifest/lib/schema.js +8 -0
- package/dist/workspace-manifest/lib/schema.js.map +1 -1
- package/dist/workspace-manifest/lib/types.d.ts +1 -0
- package/dist/workspace-manifest/lib/types.d.ts.map +1 -1
- package/package.json +43 -20
- package/schema/workflow.schema.json +29 -2
- package/src/deliverable-spec/index.ts +1 -1
- package/src/deliverable-spec/lib/schema.ts +1 -2
- package/src/payload-codec/index.ts +8 -4
- package/src/payload-codec/lib/codec.ts +14 -2
- package/src/payload-codec/lib/payload.ts +26 -0
- package/src/payload-codec/temporal/index.ts +36 -0
- package/src/workflow/index.ts +3 -0
- package/src/workflow/lib/compiler/canonical-json.ts +17 -4
- package/src/workflow/lib/compiler/compile.ts +13 -6
- package/src/workflow/lib/compiler/gate-defaults.ts +153 -0
- package/src/workflow/lib/compiler/index.ts +1 -0
- package/src/workflow/lib/compiler/payload-reach-in.ts +6 -6
- package/src/workflow/lib/compiler/review-step.ts +3 -3
- package/src/workflow/lib/duration.ts +5 -10
- package/src/workflow/lib/expression/context.ts +2 -2
- package/src/workflow/lib/expression/evaluator.ts +2 -2
- package/src/workflow/lib/expression/walk-artifact-refs.ts +2 -2
- package/src/workflow/lib/serializer.ts +1 -1
- package/src/workflow/lib/types.ts +27 -1
- package/src/workflow/lib/validate.ts +2 -2
- package/src/workspace-manifest/lib/compile.ts +1 -2
- package/src/workspace-manifest/lib/interpolate.ts +29 -3
- package/src/workspace-manifest/lib/resolve-extends.ts +3 -3
- package/src/workspace-manifest/lib/schema.ts +13 -2
- package/src/workspace-manifest/lib/types.ts +10 -3
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// ── Workflow-level decision-gate defaults (`defaults.gate`) ──
|
|
3
|
+
//
|
|
4
|
+
// Gate-heavy workflows (e.g. the software-dev product-development pipeline)
|
|
5
|
+
// repeat the same decision-gate configuration on every gate job:
|
|
6
|
+
// timeoutSeconds, onTimeout, autonomyMode, recipients, policy. `defaults.gate`
|
|
7
|
+
// lets the author declare that configuration ONCE at the workflow level; the
|
|
8
|
+
// compiler folds it into each gate job's `with:` before any other compile
|
|
9
|
+
// pass runs, so downstream validation, expression compilation, and the
|
|
10
|
+
// CompiledRun snapshot all observe the merged document — expressions inside
|
|
11
|
+
// defaults compile per-job exactly as if written inline.
|
|
12
|
+
//
|
|
13
|
+
// Merge contract (deterministic, no silent deep-merging):
|
|
14
|
+
// • Applies ONLY to jobs whose `uses:` resolves to the `xema/decision-gate`
|
|
15
|
+
// action id — matched on the resolved ref's id, regardless of version.
|
|
16
|
+
// • Fills ABSENT `with:` keys only. Explicit per-gate values always win.
|
|
17
|
+
// • Fill is per top-level key: a gate that declares its own `policy` or
|
|
18
|
+
// `recipients` keeps that value verbatim — there is no partial merge
|
|
19
|
+
// inside a key's object/array value.
|
|
20
|
+
// • Fail-fast validation: every `defaults.gate` key must be declared in
|
|
21
|
+
// the resolved gate manifest's `inputs.properties` (guards drift between
|
|
22
|
+
// the DSL schema's closed key set and the pinned gate action version).
|
|
23
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
|
|
26
|
+
import { WorkflowDslError } from '../errors';
|
|
27
|
+
import { compileExpression, extractInterpolations } from '../expression';
|
|
28
|
+
import type { WorkflowDocument, WorkflowGateDefaults } from '../types';
|
|
29
|
+
import type { ResolvedRef } from './types';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Canonical action id of the decision-gate primitive. `defaults.gate`
|
|
33
|
+
* targets jobs by this id — never by the raw `uses:` string — so any
|
|
34
|
+
* pinned version (`xema/decision-gate@1.2.0`, `@2.x`, …) participates.
|
|
35
|
+
*/
|
|
36
|
+
export const DECISION_GATE_ACTION_ID = 'xema/decision-gate';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fold `defaults.gate` into every decision-gate job's `with:` block and
|
|
40
|
+
* return the (possibly rewritten) document. Pure: never mutates the input.
|
|
41
|
+
*
|
|
42
|
+
* Runs BEFORE all other compile passes so the merged `with:` flows through
|
|
43
|
+
* expression validation, needs-shape validation, review rewriting, the
|
|
44
|
+
* action input-schema pin, and the snapshot hash exactly like authored
|
|
45
|
+
* values.
|
|
46
|
+
*
|
|
47
|
+
* Jobs whose `uses:` is missing from `resolvedRefs` are skipped here — the
|
|
48
|
+
* per-job compile loop already fails them with `DSL_UNKNOWN_ACTION`, and
|
|
49
|
+
* pre-empting that error would reorder author-facing diagnostics.
|
|
50
|
+
*/
|
|
51
|
+
export function applyGateDefaults(
|
|
52
|
+
workflow: WorkflowDocument,
|
|
53
|
+
resolvedRefs: Readonly<Record<string, ResolvedRef>>,
|
|
54
|
+
): WorkflowDocument {
|
|
55
|
+
const gate = workflow.defaults?.gate;
|
|
56
|
+
if (gate === undefined) return workflow;
|
|
57
|
+
const gateEntries = Object.entries(gate).filter(
|
|
58
|
+
([, value]) => value !== undefined,
|
|
59
|
+
);
|
|
60
|
+
if (gateEntries.length === 0) return workflow;
|
|
61
|
+
|
|
62
|
+
// Validate every `${{ … }}` expression inside the defaults block up
|
|
63
|
+
// front — even when no gate job consumes it. A malformed expression in
|
|
64
|
+
// `defaults.gate` is an authoring error regardless of merge fan-out;
|
|
65
|
+
// deferring it until a gate job appears would be a silent latency.
|
|
66
|
+
for (const ext of extractInterpolations(
|
|
67
|
+
gate as Readonly<Record<string, unknown>>,
|
|
68
|
+
['defaults', 'gate'],
|
|
69
|
+
)) {
|
|
70
|
+
compileExpression(ext.source);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let anyChanged = false;
|
|
74
|
+
const rewrittenJobs: Record<string, WorkflowDocument['jobs'][string]> = {};
|
|
75
|
+
for (const [jobKey, job] of Object.entries(workflow.jobs)) {
|
|
76
|
+
const resolved = resolvedRefs[job.uses];
|
|
77
|
+
if (
|
|
78
|
+
resolved === undefined ||
|
|
79
|
+
resolved.isReusableWorkflow ||
|
|
80
|
+
resolved.id !== DECISION_GATE_ACTION_ID
|
|
81
|
+
) {
|
|
82
|
+
rewrittenJobs[jobKey] = job;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
assertGateDefaultKeysDeclared(jobKey, gate, resolved);
|
|
87
|
+
|
|
88
|
+
const authored = job.with ?? {};
|
|
89
|
+
let jobChanged = false;
|
|
90
|
+
const merged: Record<string, unknown> = { ...authored };
|
|
91
|
+
for (const [key, value] of gateEntries) {
|
|
92
|
+
if (Object.hasOwn(authored, key)) continue;
|
|
93
|
+
merged[key] = value;
|
|
94
|
+
jobChanged = true;
|
|
95
|
+
}
|
|
96
|
+
if (!jobChanged) {
|
|
97
|
+
rewrittenJobs[jobKey] = job;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
anyChanged = true;
|
|
101
|
+
rewrittenJobs[jobKey] = { ...job, with: merged };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!anyChanged) return workflow;
|
|
105
|
+
return { ...workflow, jobs: rewrittenJobs };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Fail-fast subset check: every `defaults.gate` key must be a declared
|
|
110
|
+
* input of the resolved gate action version. The JSON schema already
|
|
111
|
+
* closes the key set to the canonical decision-gate input names; this
|
|
112
|
+
* check guards manifest drift (e.g. a future gate version renaming or
|
|
113
|
+
* dropping an input while a workflow still defaults it).
|
|
114
|
+
*
|
|
115
|
+
* Skipped only when the manifest declares no introspectable
|
|
116
|
+
* `inputs.properties` map — nothing to prove against; the worker's
|
|
117
|
+
* pinned input-schema validation still rejects unknown keys at dispatch.
|
|
118
|
+
*/
|
|
119
|
+
function assertGateDefaultKeysDeclared(
|
|
120
|
+
jobKey: string,
|
|
121
|
+
gate: WorkflowGateDefaults,
|
|
122
|
+
resolved: ResolvedRef,
|
|
123
|
+
): void {
|
|
124
|
+
const inputs = resolved.actionManifest?.spec.inputs;
|
|
125
|
+
const properties =
|
|
126
|
+
inputs && typeof inputs === 'object'
|
|
127
|
+
? (inputs as { properties?: unknown }).properties
|
|
128
|
+
: undefined;
|
|
129
|
+
if (
|
|
130
|
+
properties === null ||
|
|
131
|
+
properties === undefined ||
|
|
132
|
+
typeof properties !== 'object' ||
|
|
133
|
+
Array.isArray(properties)
|
|
134
|
+
) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const declared = properties as Readonly<Record<string, unknown>>;
|
|
138
|
+
for (const [key, value] of Object.entries(gate)) {
|
|
139
|
+
if (value === undefined) continue;
|
|
140
|
+
if (Object.hasOwn(declared, key)) continue;
|
|
141
|
+
throw new WorkflowDslError(
|
|
142
|
+
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
143
|
+
`defaults.gate.${key} is not a declared input of ${resolved.id}@${resolved.version} (consumed by job '${jobKey}'). Declared inputs: [${Object.keys(declared).sort((a, b) => a.localeCompare(b)).join(', ')}].`,
|
|
144
|
+
{
|
|
145
|
+
jobKey,
|
|
146
|
+
key,
|
|
147
|
+
actionId: resolved.id,
|
|
148
|
+
actionVersion: resolved.version,
|
|
149
|
+
declaredInputs: Object.keys(declared),
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { compileWorkflow } from './compile';
|
|
2
|
+
export { DECISION_GATE_ACTION_ID, applyGateDefaults } from './gate-defaults';
|
|
2
3
|
export { canonicalJsonSha256, canonicalJsonStringify } from './canonical-json';
|
|
3
4
|
export { compileManifestSource } from './manifest-source';
|
|
4
5
|
export { getWorkspaceManifestContract, isAgentShapedAction } from './action-shape';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// ── Payload reach-in validation
|
|
2
|
+
// ── Payload reach-in validation ──
|
|
3
3
|
//
|
|
4
4
|
// Workflow YAML supports two flavours of `needs.<X>.outputs.<name>.<field>`:
|
|
5
5
|
//
|
|
@@ -74,7 +74,7 @@ function lookupOutputBindingSpecRef(
|
|
|
74
74
|
* Decision table for each `<name>.<field>`:
|
|
75
75
|
* • `<field>` is an ArtifactRef envelope field → OK (no payload fetch).
|
|
76
76
|
* • Upstream activity manifest declares `outputBindings.<name>.deliverableSpecRef`
|
|
77
|
-
* (
|
|
77
|
+
* (per-output binding) → use that spec; otherwise fall back
|
|
78
78
|
* to the upstream job's `with.deliverableSpecRef`. Then:
|
|
79
79
|
* - resolved spec has `topLevelKeys` and `<field>` is one → OK.
|
|
80
80
|
* - resolved spec has `topLevelKeys` and `<field>` is NOT one →
|
|
@@ -136,7 +136,7 @@ export function validatePayloadReachIns(
|
|
|
136
136
|
/**
|
|
137
137
|
* Walk every reach-in for a single job and emit one pin per
|
|
138
138
|
* `(upstreamJobKey, specRef)` pair. Pins drive the runtime spec-version
|
|
139
|
-
* assertion
|
|
139
|
+
* assertion — the worker pre-fetches
|
|
140
140
|
* the artifact and refuses to proceed when its `schemaVersion` doesn't
|
|
141
141
|
* match the pin's `specVersion`.
|
|
142
142
|
*
|
|
@@ -203,8 +203,8 @@ function resolveReachInPin(
|
|
|
203
203
|
return null;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
// Prefer per-output binding from the upstream activity manifest
|
|
207
|
-
//
|
|
206
|
+
// Prefer per-output binding from the upstream activity manifest.
|
|
207
|
+
// Falls back to the job-level `with.deliverableSpecRef`
|
|
208
208
|
// when no per-output binding is declared OR when the reach-in's
|
|
209
209
|
// outputName is one of the indexed sentinels.
|
|
210
210
|
let specRef: string | null = null;
|
|
@@ -431,7 +431,7 @@ function validateReachIn(
|
|
|
431
431
|
return;
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
-
// Per-output binding from the upstream activity manifest
|
|
434
|
+
// Per-output binding from the upstream activity manifest
|
|
435
435
|
// wins over the job-level `with.deliverableSpecRef` when present.
|
|
436
436
|
const perOutputBinding =
|
|
437
437
|
access.outputName === '<indexed>' || access.outputName === '<byKey>'
|
|
@@ -75,15 +75,15 @@ interface ReviewWithShape {
|
|
|
75
75
|
/**
|
|
76
76
|
* Per-job redraft-strategy descriptor carried on the IR `redraft` block
|
|
77
77
|
* when the producer step has a matrix strategy. Mirrors the worker
|
|
78
|
-
* consumer
|
|
79
|
-
*
|
|
78
|
+
* consumer that runs the review workflow — kept JSON-serializable so the
|
|
79
|
+
* IR survives Temporal payload encoding.
|
|
80
80
|
*
|
|
81
81
|
* `static` entries are already cartesian-expanded at compile time;
|
|
82
82
|
* `dynamic.fromExpression` is the producer's expression body (already
|
|
83
83
|
* `${{ … }}`-stripped) which the parent dispatcher evaluates against
|
|
84
84
|
* the review step's expression context before spawning the child
|
|
85
85
|
* workflow. `keyBy` is informational only — per-CU keyed targeting is
|
|
86
|
-
* a
|
|
86
|
+
* a planned future feature; today every matrix entry is always re-run.
|
|
87
87
|
*/
|
|
88
88
|
export type ReviewRedraftStrategyIR =
|
|
89
89
|
| {
|
|
@@ -7,7 +7,8 @@ import { WorkflowDslError } from './errors';
|
|
|
7
7
|
* no silent zero fallback — callers must pass a value that matches the
|
|
8
8
|
* schema pattern (already enforced by Ajv upstream).
|
|
9
9
|
*
|
|
10
|
-
* Cap:
|
|
10
|
+
* Cap: total duration limited to 8760h (= 365 days), regardless of the
|
|
11
|
+
* unit used. The cap is a sanity guard
|
|
11
12
|
* against typos (`100000h` ≈ 11 years), not a policy bound — real
|
|
12
13
|
* human-driven flows (compliance reviews, multi-stakeholder approvals,
|
|
13
14
|
* SCM PRs that go a quarter without merging) routinely take months.
|
|
@@ -30,19 +31,13 @@ export function parseDurationMs(literal: string): number {
|
|
|
30
31
|
}
|
|
31
32
|
const value = Number.parseInt(match[1]!, 10);
|
|
32
33
|
const unit = match[2] as 's' | 'm' | 'h';
|
|
33
|
-
|
|
34
|
+
const ms = unit === 's' ? value * 1_000 : unit === 'm' ? value * 60_000 : value * 3_600_000;
|
|
35
|
+
if (ms > MAX_HOURS * 3_600_000) {
|
|
34
36
|
throw new WorkflowDslError(
|
|
35
37
|
WorkflowErrorCode.DSL_SCHEMA_INVALID,
|
|
36
38
|
`Duration exceeds cap of ${MAX_HOURS}h: ${literal}`,
|
|
37
39
|
{ literal, maxHours: MAX_HOURS },
|
|
38
40
|
);
|
|
39
41
|
}
|
|
40
|
-
|
|
41
|
-
case 's':
|
|
42
|
-
return value * 1_000;
|
|
43
|
-
case 'm':
|
|
44
|
-
return value * 60_000;
|
|
45
|
-
case 'h':
|
|
46
|
-
return value * 3_600_000;
|
|
47
|
-
}
|
|
42
|
+
return ms;
|
|
48
43
|
}
|
|
@@ -3,8 +3,8 @@ import type { TriggerPayload } from '@xemahq/kernel-contracts/workflow';
|
|
|
3
3
|
/**
|
|
4
4
|
* Per-run, read-only cache of pre-fetched artifact payloads keyed by
|
|
5
5
|
* `versionId`. Populated by the workflow runtime BEFORE each step's
|
|
6
|
-
* expressions are evaluated
|
|
7
|
-
*
|
|
6
|
+
* expressions are evaluated, which owns the fetch + cache plumbing.
|
|
7
|
+
* The evaluator only ever reads from this map;
|
|
8
8
|
* concurrent reads are safe by construction (no writes once frozen for a
|
|
9
9
|
* step). Empty when no expression in scope reaches into a typed
|
|
10
10
|
* payload or invokes `fromJSON(ref)`.
|
|
@@ -172,8 +172,8 @@ function evalCall(node: CallNode, ctx: ExpressionContext): unknown {
|
|
|
172
172
|
* evaluator runs. A cache miss is a runtime infra bug, not an
|
|
173
173
|
* author bug, so it carries a runtime-coded error.
|
|
174
174
|
*
|
|
175
|
-
* This is the
|
|
176
|
-
* validation use the typed reach-in (`outputs.foo.field
|
|
175
|
+
* This is the untyped escape hatch. Authors who want compile-time field
|
|
176
|
+
* validation use the typed reach-in (`outputs.foo.field`).
|
|
177
177
|
*/
|
|
178
178
|
function evalFromJson(node: CallNode, ctx: ExpressionContext): unknown {
|
|
179
179
|
const argNode = singleArg(node, 'fromJSON');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
2
|
// ── Runtime walker — find every AST sub-expression that resolves to an
|
|
3
|
-
// ArtifactRef the runtime must pre-fetch
|
|
3
|
+
// ArtifactRef the runtime must pre-fetch. ──
|
|
4
4
|
//
|
|
5
5
|
// Two flavours of pre-fetch target:
|
|
6
6
|
//
|
|
@@ -202,7 +202,7 @@ export function extractUpstreamJobKey(node: ExpressionNode): string | null {
|
|
|
202
202
|
* known there.
|
|
203
203
|
*
|
|
204
204
|
* Pair with `extractUpstreamJobKey` to resolve per-output spec pins at
|
|
205
|
-
* runtime
|
|
205
|
+
* runtime. Falling back to `null` is the cue the prefetch
|
|
206
206
|
* helper uses to consult the job-level pin instead.
|
|
207
207
|
*/
|
|
208
208
|
export function extractUpstreamOutputName(
|
|
@@ -8,7 +8,7 @@ import type { WorkflowDocument } from './types';
|
|
|
8
8
|
* Note on comment preservation: this serializer does NOT carry over
|
|
9
9
|
* source comments from the original YAML — it dumps the AST verbatim.
|
|
10
10
|
* Use it when the canonical representation lives in the AST (e.g. the
|
|
11
|
-
* canvas editor
|
|
11
|
+
* canvas editor) and the YAML view is a derived presentation.
|
|
12
12
|
* If you need to apply a single mutation to existing hand-authored YAML
|
|
13
13
|
* while keeping comments + key ordering intact, use the `yaml`
|
|
14
14
|
* package's CST/Document API directly (follow-up; not in this PR).
|
|
@@ -171,6 +171,32 @@ export interface WorkflowConcurrencyDeclaration {
|
|
|
171
171
|
export interface WorkflowDefaults {
|
|
172
172
|
readonly retry?: WorkflowRetryDeclaration;
|
|
173
173
|
readonly timeout?: string;
|
|
174
|
+
readonly gate?: WorkflowGateDefaults;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Workflow-level defaults for decision-gate jobs (`defaults.gate`).
|
|
179
|
+
*
|
|
180
|
+
* For every job whose `uses:` resolves to the `xema/decision-gate` action
|
|
181
|
+
* (matched on the action id, regardless of version), the compiler fills
|
|
182
|
+
* each ABSENT `with:` key from this block before any other compile pass
|
|
183
|
+
* runs. Explicit per-gate values always win, and the fill happens per
|
|
184
|
+
* top-level key only — a gate that declares its own `policy` /
|
|
185
|
+
* `recipients` keeps that value verbatim (no partial merge inside it).
|
|
186
|
+
*
|
|
187
|
+
* Every value may be a `${{ … }}` expression; merged values compile and
|
|
188
|
+
* evaluate per-job exactly as if they had been written inline. The key
|
|
189
|
+
* set is closed by the JSON schema AND cross-validated at compile time
|
|
190
|
+
* against the resolved gate manifest's declared `inputs` (fail-fast on
|
|
191
|
+
* drift between the schema's key set and the pinned gate version).
|
|
192
|
+
*/
|
|
193
|
+
export interface WorkflowGateDefaults {
|
|
194
|
+
readonly timeoutSeconds?: unknown;
|
|
195
|
+
readonly onTimeout?: unknown;
|
|
196
|
+
readonly autonomyMode?: unknown;
|
|
197
|
+
readonly recipients?: unknown;
|
|
198
|
+
readonly policy?: unknown;
|
|
199
|
+
readonly escalationChain?: unknown;
|
|
174
200
|
}
|
|
175
201
|
|
|
176
202
|
export interface WorkflowRetryDeclaration {
|
|
@@ -263,7 +289,7 @@ export interface ActionManifestSpec {
|
|
|
263
289
|
readonly inputs: Readonly<Record<string, unknown>>;
|
|
264
290
|
readonly outputs: Readonly<Record<string, unknown>>;
|
|
265
291
|
/**
|
|
266
|
-
* Per-output deliverable-spec bindings
|
|
292
|
+
* Per-output deliverable-spec bindings.
|
|
267
293
|
* Each entry pins a named output to a deliverable-spec ref like
|
|
268
294
|
* `agent-summary@1.0.0`. The DSL compiler consults this first when
|
|
269
295
|
* resolving typed payload reach-in (`outputs.<name>.<field>`); it
|
|
@@ -137,7 +137,7 @@ function parseAndValidate<T>(
|
|
|
137
137
|
{ kind },
|
|
138
138
|
);
|
|
139
139
|
}
|
|
140
|
-
//
|
|
140
|
+
// Shorthand: `needs: <string>` → `[<string>]`. Normalize
|
|
141
141
|
// BEFORE validation so the schema stays strict (one canonical shape)
|
|
142
142
|
// and the rest of the compiler never branches on shorthand. Workflow +
|
|
143
143
|
// reusable-workflow documents both have `jobs: { <key>: { needs?... } }`;
|
|
@@ -151,7 +151,7 @@ function parseAndValidate<T>(
|
|
|
151
151
|
/**
|
|
152
152
|
* Mutates `doc.jobs.<key>.needs` from `<string>` to `[<string>]` in
|
|
153
153
|
* place. Authors can write either form; the canonical form is the
|
|
154
|
-
* array. Documented
|
|
154
|
+
* array. Documented under "Simplified DSL".
|
|
155
155
|
*/
|
|
156
156
|
function normalizeJobNeedsShorthand(doc: object): void {
|
|
157
157
|
const jobs = (doc as { jobs?: unknown }).jobs;
|
|
@@ -580,8 +580,7 @@ function compileWorkingFiles(
|
|
|
580
580
|
}
|
|
581
581
|
// `binaryFormat` is the closed-enum hint that signals a companion
|
|
582
582
|
// binary (XLSX/PPTX/DOCX/PDF) lives alongside the synopsis. Validate now so
|
|
583
|
-
// a typo cannot reach the worker as a silent no-op
|
|
584
|
-
// `.claude/plans/xlsx-pptx-binary-kb-attachment.md` D6.
|
|
583
|
+
// a typo cannot reach the worker as a silent no-op.
|
|
585
584
|
const binaryFormatRaw = wf.sourceRef['binaryFormat'];
|
|
586
585
|
if (
|
|
587
586
|
typeof binaryFormatRaw === 'string' &&
|
|
@@ -42,7 +42,7 @@ export function interpolate(
|
|
|
42
42
|
basePath = '$',
|
|
43
43
|
): { readonly resolved: unknown; readonly issues: readonly InterpolationIssue[] } {
|
|
44
44
|
const issues: InterpolationIssue[] = [];
|
|
45
|
-
const resolved = walk(value, ctx, basePath, issues);
|
|
45
|
+
const resolved = walk(value, ctx, basePath, issues, new Set());
|
|
46
46
|
return { resolved, issues };
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -51,16 +51,42 @@ function walk(
|
|
|
51
51
|
ctx: InterpolationContext,
|
|
52
52
|
path: string,
|
|
53
53
|
issues: InterpolationIssue[],
|
|
54
|
+
seen: Set<object>,
|
|
54
55
|
): unknown {
|
|
55
56
|
if (typeof value === 'string') return interpolateString(value, ctx, path, issues);
|
|
56
57
|
if (Array.isArray(value)) {
|
|
57
|
-
|
|
58
|
+
// Reject cyclic structures (e.g. self-referential YAML anchors) with a
|
|
59
|
+
// clear error rather than recursing until the stack overflows.
|
|
60
|
+
if (seen.has(value)) {
|
|
61
|
+
throw new Error(`interpolate: circular reference at ${path}`);
|
|
62
|
+
}
|
|
63
|
+
seen.add(value);
|
|
64
|
+
const out = value.map((item, idx) => walk(item, ctx, `${path}[${idx}]`, issues, seen));
|
|
65
|
+
seen.delete(value);
|
|
66
|
+
return out;
|
|
58
67
|
}
|
|
59
68
|
if (value !== null && typeof value === 'object') {
|
|
69
|
+
if (seen.has(value)) {
|
|
70
|
+
throw new Error(`interpolate: circular reference at ${path}`);
|
|
71
|
+
}
|
|
72
|
+
seen.add(value);
|
|
60
73
|
const out: Record<string, unknown> = {};
|
|
61
74
|
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
62
|
-
|
|
75
|
+
const resolvedChild = walk(child, ctx, `${path}.${key}`, issues, seen);
|
|
76
|
+
// Assign `__proto__` as an own data property instead of invoking the
|
|
77
|
+
// prototype setter, which would silently drop the key and reparent `out`.
|
|
78
|
+
if (key === '__proto__') {
|
|
79
|
+
Object.defineProperty(out, key, {
|
|
80
|
+
value: resolvedChild,
|
|
81
|
+
writable: true,
|
|
82
|
+
enumerable: true,
|
|
83
|
+
configurable: true,
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
out[key] = resolvedChild;
|
|
87
|
+
}
|
|
63
88
|
}
|
|
89
|
+
seen.delete(value);
|
|
64
90
|
return out;
|
|
65
91
|
}
|
|
66
92
|
return value;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
2
|
// ── WorkspaceManifest `extends:` resolver ──
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// Provides a deterministic merge primitive plus a thin orchestrator the
|
|
5
|
+
// composer / preview endpoint / llm-registry calls to materialize the
|
|
6
|
+
// effective manifest.
|
|
7
7
|
//
|
|
8
8
|
// Design constraints:
|
|
9
9
|
// • Single-level extends only. The base MUST NOT carry its own
|
|
@@ -31,7 +31,7 @@ const SlugRegex = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
|
|
|
31
31
|
const VersionRegex = /^\d+\.\d+\.\d+$/;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Upper bound on authored node-level `instructions` (
|
|
34
|
+
* Upper bound on authored node-level `instructions` (author a
|
|
35
35
|
* migrated agent as `base agent slug + instructions`). A prompt fragment
|
|
36
36
|
* layered onto the referenced agent definition's intrinsic system prompt —
|
|
37
37
|
* the same lever as `AgentNode.instructions`. The cap is a sanity
|
|
@@ -330,6 +330,17 @@ export const ManifestSlotPersistenceOverrideSchema = z
|
|
|
330
330
|
),
|
|
331
331
|
branchSuffix: GitPushBranchSuffixEnumSchema.optional(),
|
|
332
332
|
concurrencyMode: GitPushConcurrencyModeEnumSchema.optional(),
|
|
333
|
+
mergeTarget: z
|
|
334
|
+
.string()
|
|
335
|
+
.min(1)
|
|
336
|
+
.regex(
|
|
337
|
+
/^[a-zA-Z0-9][a-zA-Z0-9._\-/]*$/,
|
|
338
|
+
'mergeTarget must be a git-safe branch name ([A-Za-z0-9._-/], no leading slash)',
|
|
339
|
+
)
|
|
340
|
+
.refine((v) => !v.includes('..'), {
|
|
341
|
+
message: 'mergeTarget must not contain `..` segments',
|
|
342
|
+
})
|
|
343
|
+
.optional(),
|
|
333
344
|
})
|
|
334
345
|
.strict();
|
|
335
346
|
|
|
@@ -641,7 +652,7 @@ export const WorkspaceManifestMetadataSchema = z.object({
|
|
|
641
652
|
* Optional in the YAML for additive rollout; the compiler defaults
|
|
642
653
|
* absent values to `[workflow, agent-session]` so existing
|
|
643
654
|
* manifests keep compiling. New biome/org manifests SHOULD declare
|
|
644
|
-
* this explicitly —
|
|
655
|
+
* this explicitly — a later step sweeps the shipped manifests.
|
|
645
656
|
*/
|
|
646
657
|
surfaceCompat: z
|
|
647
658
|
.array(ManifestSurfaceEnumSchema)
|
|
@@ -210,6 +210,13 @@ export interface ManifestSlotPersistenceOverride {
|
|
|
210
210
|
readonly branchPrefix: string;
|
|
211
211
|
readonly branchSuffix?: import('@xemahq/kernel-contracts/workflow').ManifestGitPushBranchSuffix;
|
|
212
212
|
readonly concurrencyMode?: import('@xemahq/kernel-contracts/workflow').ManifestGitPushConcurrencyMode;
|
|
213
|
+
/**
|
|
214
|
+
* Fully-qualified long-lived branch the per-session branch auto-merges
|
|
215
|
+
* into on pause (per-session-branch + auto-merge model). Mirrors
|
|
216
|
+
* `SlotPersistencePolicy.git-push.mergeTarget` in
|
|
217
|
+
* `@xemahq/kernel-contracts/agent-workspace`.
|
|
218
|
+
*/
|
|
219
|
+
readonly mergeTarget?: string;
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
export interface ManifestPersistence {
|
|
@@ -312,7 +319,7 @@ export interface ManifestAgentBlock {
|
|
|
312
319
|
*/
|
|
313
320
|
readonly defaultModel?: ManifestModelRef;
|
|
314
321
|
/**
|
|
315
|
-
* Optional node-level instructions
|
|
322
|
+
* Optional node-level instructions. A prompt
|
|
316
323
|
* fragment layered ONTO the referenced base agent's intrinsic system
|
|
317
324
|
* prompt — the same lever as `AgentNode.instructions`, exposed here
|
|
318
325
|
* so a WorkspaceManifest can author a migrated agent as
|
|
@@ -329,7 +336,7 @@ export interface ManifestAgentBlock {
|
|
|
329
336
|
*/
|
|
330
337
|
readonly promptMode?: PromptMergeMode;
|
|
331
338
|
/**
|
|
332
|
-
* Optional per-agent permission override
|
|
339
|
+
* Optional per-agent permission override. A partial
|
|
333
340
|
* OpenCode-style `PermissionMap` deep-merged (per top-level key) over the
|
|
334
341
|
* referenced base agent's authored permission before the system tiers run.
|
|
335
342
|
* Lets a generic base agent be specialized into a bounded role (read-only
|
|
@@ -365,7 +372,7 @@ export interface ManifestSubAgent {
|
|
|
365
372
|
readonly alias?: string;
|
|
366
373
|
readonly defaultModel?: ManifestModelRef;
|
|
367
374
|
/**
|
|
368
|
-
* Optional node-level instructions for this delegate
|
|
375
|
+
* Optional node-level instructions for this delegate. Same
|
|
369
376
|
* `base agent slug + instructions` lever as the primary agent block;
|
|
370
377
|
* layered onto the referenced agent's intrinsic system prompt. May
|
|
371
378
|
* reference `${input.x}`. Bounded by `MAX_AGENT_INSTRUCTIONS_LENGTH`.
|