@kici-dev/engine 0.4.0 → 0.6.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 (81) hide show
  1. package/dist/audit/access-log-policy.js +1 -0
  2. package/dist/audit/retention-policy.js +2 -0
  3. package/dist/context/held-run-job-id.d.ts +26 -10
  4. package/dist/context/held-run-job-id.js +30 -11
  5. package/dist/context/host-match.js +2 -1
  6. package/dist/context/index.d.ts +1 -1
  7. package/dist/context/index.js +3 -3
  8. package/dist/context/types.d.ts +10 -1
  9. package/dist/context/types.js +10 -1
  10. package/dist/index.d.ts +6 -2
  11. package/dist/index.js +17 -12
  12. package/dist/labels/compile.d.ts +2 -7
  13. package/dist/labels/compile.js +1 -10
  14. package/dist/labels.d.ts +74 -12
  15. package/dist/labels.js +82 -17
  16. package/dist/mcp/held-run-resolve.d.ts +40 -0
  17. package/dist/mcp/held-run-resolve.js +68 -15
  18. package/dist/mcp/tool-schemas.d.ts +4 -0
  19. package/dist/mcp/tool-schemas.js +13 -1
  20. package/dist/metrics/catalog-policy.d.ts +17 -3
  21. package/dist/metrics/catalog-policy.js +44 -13
  22. package/dist/metrics/metric-catalog.generated.d.ts +140 -0
  23. package/dist/metrics/metric-catalog.generated.js +168 -0
  24. package/dist/protocol/dashboard-global-workflows.js +2 -2
  25. package/dist/protocol/event-log-payload.js +1 -1
  26. package/dist/protocol/messages/access-log.d.ts +5 -0
  27. package/dist/protocol/messages/access-log.js +1 -0
  28. package/dist/protocol/messages/actor.d.ts +13 -2
  29. package/dist/protocol/messages/actor.js +16 -5
  30. package/dist/protocol/messages/common.js +1 -1
  31. package/dist/protocol/messages/dashboard-global-workflows.d.ts +29 -3
  32. package/dist/protocol/messages/dashboard-global-workflows.js +48 -4
  33. package/dist/protocol/messages/dashboard.d.ts +48 -7
  34. package/dist/protocol/messages/dashboard.js +62 -6
  35. package/dist/protocol/messages/execution-status.d.ts +44 -0
  36. package/dist/protocol/messages/execution-status.js +69 -2
  37. package/dist/protocol/messages/git-credential-relay.d.ts +78 -0
  38. package/dist/protocol/messages/git-credential-relay.js +86 -0
  39. package/dist/protocol/messages/orchestrator-agent.d.ts +138 -0
  40. package/dist/protocol/messages/orchestrator-agent.js +131 -3
  41. package/dist/protocol/messages/peer.d.ts +7 -0
  42. package/dist/protocol/messages/peer.js +18 -1
  43. package/dist/protocol/messages/platform-orchestrator.d.ts +155 -2
  44. package/dist/protocol/messages/platform-orchestrator.js +178 -19
  45. package/dist/protocol/version.d.ts +19 -2
  46. package/dist/protocol/version.js +20 -3
  47. package/dist/provenance/verify.js +11 -10
  48. package/dist/provider/check-status-poster.d.ts +37 -2
  49. package/dist/provider/contributor-resolver.d.ts +11 -3
  50. package/dist/provider/file-contents-fetcher.d.ts +39 -0
  51. package/dist/provider/file-contents-fetcher.js +2 -0
  52. package/dist/provider/git-credential.d.ts +77 -0
  53. package/dist/provider/git-credential.js +10 -0
  54. package/dist/provider/index.d.ts +4 -0
  55. package/dist/provider/index.js +2 -1
  56. package/dist/provider/webhook-normalizer.d.ts +12 -12
  57. package/dist/repo/pattern-negation.d.ts +73 -0
  58. package/dist/repo/pattern-negation.js +86 -0
  59. package/dist/safe-regex.d.ts +16 -0
  60. package/dist/safe-regex.js +24 -0
  61. package/dist/scaler/registry-auth.d.ts +18 -0
  62. package/dist/scaler/registry-auth.js +28 -0
  63. package/dist/scaler/scaler-backend-type.d.ts +35 -0
  64. package/dist/scaler/scaler-backend-type.js +39 -2
  65. package/dist/scaler/scaler-events.d.ts +79 -0
  66. package/dist/scaler/scaler-events.js +87 -0
  67. package/dist/trigger/compiled-matchers.d.ts +21 -1
  68. package/dist/trigger/compiled-matchers.js +30 -3
  69. package/dist/trigger/content-requirements.d.ts +31 -0
  70. package/dist/trigger/content-requirements.js +125 -0
  71. package/dist/trigger/decision-trace.d.ts +168 -0
  72. package/dist/trigger/decision-trace.js +207 -4
  73. package/dist/trigger/jsonpath-matcher.js +5 -1
  74. package/dist/trigger/matcher.js +61 -10
  75. package/dist/trigger/text-match.d.ts +27 -0
  76. package/dist/trigger/text-match.js +86 -0
  77. package/dist/trigger/types.d.ts +265 -23
  78. package/dist/trigger/types.js +37 -5
  79. package/dist/ws/rate-limiter.js +3 -3
  80. package/package.json +20 -3
  81. package/sbom.spdx.json +40 -15
@@ -1,6 +1,7 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
- import { createTraceEntry, createWorkflowDecision } from "./decision-trace.js";
3
- import { getCompiledRegex, getGlobMatcher } from "./compiled-matchers.js";
2
+ import { evaluateTextMatch } from "./text-match.js";
3
+ import { TraceCheck, createCommitMessageTraceEntry, createTraceEntry, createWorkflowDecision } from "./decision-trace.js";
4
+ import { getCompiledRegex, getGlobMatcher, getRepoGlobMatcher } from "./compiled-matchers.js";
4
5
  import { matchJsonPath, matchJsonPathNot } from "./jsonpath-matcher.js";
5
6
  //#region src/trigger/matcher.ts
6
7
  /**
@@ -35,7 +36,21 @@ function splitBranchPatterns(patterns) {
35
36
  */
36
37
  function matchBranchPattern(pattern, branch) {
37
38
  if (pattern.type === "glob") return getGlobMatcher(pattern.pattern)(branch);
38
- else return getCompiledRegex(pattern.pattern, pattern.flags).test(branch);
39
+ else return getCompiledRegex(pattern.pattern, pattern.flags, "branch/tag/repo pattern").test(branch);
40
+ }
41
+ /**
42
+ * Match a repo pattern against a repository identifier.
43
+ *
44
+ * Same shape as {@link matchBranchPattern}, but glob patterns compile through
45
+ * {@link getRepoGlobMatcher} so a dot-prefixed identifier (`.hidden/repo`) is
46
+ * matched by `**`. Repo identifiers are org/name pairs, not paths, so there is
47
+ * no dotfile convention to respect — and `repos: ['**']` means every repo.
48
+ *
49
+ * Regex patterns are unaffected: `dot` is a glob option with no regex analogue.
50
+ */
51
+ function matchRepoPattern(pattern, repo) {
52
+ if (pattern.type === "glob") return getRepoGlobMatcher(pattern.pattern)(repo);
53
+ return getCompiledRegex(pattern.pattern, pattern.flags, "branch/tag/repo pattern").test(repo);
39
54
  }
40
55
  /**
41
56
  * Match any of the branch patterns.
@@ -73,10 +88,10 @@ function matchRepoPatterns(repos, sourceRepo) {
73
88
  if (repos.length === 0) return true;
74
89
  const { include, exclude } = splitBranchPatterns(repos);
75
90
  if (exclude.length > 0) {
76
- if (exclude.some((p) => matchBranchPattern(p, sourceRepo))) return false;
91
+ if (exclude.some((p) => matchRepoPattern(p, sourceRepo))) return false;
77
92
  }
78
93
  if (include.length === 0) return true;
79
- return include.some((p) => matchBranchPattern(p, sourceRepo));
94
+ return include.some((p) => matchRepoPattern(p, sourceRepo));
80
95
  }
81
96
  /**
82
97
  * Evaluate repo pattern filter for a trigger against an event.
@@ -86,17 +101,48 @@ function matchRepoPatterns(repos, sourceRepo) {
86
101
  function evaluateRepoFilter(trigger, event, traces) {
87
102
  if (trigger.repos?.length) {
88
103
  if (!event.sourceRepo) {
89
- traces.push(createTraceEntry("repo", "required", "(missing)", false));
104
+ traces.push(createTraceEntry(TraceCheck.RepoFilter, "required", "(missing)", false));
90
105
  return false;
91
106
  }
92
107
  const { include, exclude } = splitBranchPatterns(trigger.repos);
93
108
  const repoMatch = matchRepoPatterns(trigger.repos, event.sourceRepo);
94
- traces.push(createTraceEntry("repo", `include:[${include.map((p) => p.pattern).join(",")}] exclude:[${exclude.map((p) => p.pattern).join(",")}]`, event.sourceRepo, repoMatch));
109
+ traces.push(createTraceEntry(TraceCheck.RepoFilter, `include:[${include.map((p) => p.pattern).join(",")}] exclude:[${exclude.map((p) => p.pattern).join(",")}]`, event.sourceRepo, repoMatch));
95
110
  if (!repoMatch) return false;
96
111
  }
97
112
  return true;
98
113
  }
99
114
  /**
115
+ * Evaluate a trigger's `commitMessage` filter. Returns true when the trigger
116
+ * declares none (the fast path).
117
+ *
118
+ * An event with no message is INDETERMINATE, not a clean exclusion: the filter
119
+ * is fail-visible, mirroring the Tier-1 `requires` gate, so a workflow whose
120
+ * declared gate was never evaluated does not run.
121
+ */
122
+ function evaluateCommitMessageFilter(trigger, event, traces) {
123
+ const match = trigger.commitMessage;
124
+ if (!match) return true;
125
+ if (event.commitMessage === void 0) {
126
+ traces.push(createCommitMessageTraceEntry({
127
+ match,
128
+ text: void 0,
129
+ passed: false,
130
+ indeterminate: true,
131
+ reason: "no commit message in payload (provider carries none for this event)"
132
+ }));
133
+ return false;
134
+ }
135
+ const result = evaluateTextMatch(event.commitMessage, match);
136
+ traces.push(createCommitMessageTraceEntry({
137
+ match,
138
+ text: event.commitMessage,
139
+ passed: result.pass,
140
+ indeterminate: result.indeterminate !== void 0,
141
+ ...result.indeterminate !== void 0 && { reason: result.indeterminate }
142
+ }));
143
+ return result.pass;
144
+ }
145
+ /**
100
146
  * Match a PR trigger against a simulated event.
101
147
  */
102
148
  function matchPrTrigger(trigger, event, traces) {
@@ -132,6 +178,7 @@ function matchPrTrigger(trigger, event, traces) {
132
178
  traces.push(createTraceEntry("paths", `include: [${include.join(", ")}] exclude: [${exclude.join(", ")}]`, event.changedFilesStatus === "unavailable" ? "[unavailable — matched conservatively]" : `[${changedFiles.join(", ")}]`, matches));
133
179
  if (!matches) return false;
134
180
  }
181
+ if (!evaluateCommitMessageFilter(trigger, event, traces)) return false;
135
182
  if (!evaluateRepoFilter(trigger, event, traces)) return false;
136
183
  return true;
137
184
  }
@@ -156,6 +203,7 @@ function matchPushTrigger(trigger, event, traces) {
156
203
  traces.push(createTraceEntry("paths", `include: [${include.join(", ")}] exclude: [${exclude.join(", ")}]`, event.changedFilesStatus === "unavailable" ? "[unavailable — matched conservatively]" : `[${changedFiles.join(", ")}]`, matches));
157
204
  if (!matches) return false;
158
205
  }
206
+ if (!evaluateCommitMessageFilter(trigger, event, traces)) return false;
159
207
  if (!evaluateRepoFilter(trigger, event, traces)) return false;
160
208
  return true;
161
209
  }
@@ -174,6 +222,7 @@ function matchTagTrigger(trigger, event, traces) {
174
222
  traces.push(createTraceEntry("tag pattern", trigger.patterns.map((p) => p.pattern).join("|"), event.targetBranch, matches));
175
223
  if (!matches) return false;
176
224
  }
225
+ if (!evaluateCommitMessageFilter(trigger, event, traces)) return false;
177
226
  if (!evaluateRepoFilter(trigger, event, traces)) return false;
178
227
  return true;
179
228
  }
@@ -213,7 +262,7 @@ function matchCommentTrigger(trigger, event, traces) {
213
262
  traces.push(createTraceEntry("bodyMatch (glob)", trigger.bodyMatch.pattern, body, matches));
214
263
  if (!matches) return false;
215
264
  } else {
216
- const matches = getCompiledRegex(trigger.bodyMatch.pattern, trigger.bodyMatch.flags).test(body);
265
+ const matches = getCompiledRegex(trigger.bodyMatch.pattern, trigger.bodyMatch.flags, "comment bodyMatch").test(body);
217
266
  traces.push(createTraceEntry("bodyMatch (regex)", trigger.bodyMatch.pattern, body, matches));
218
267
  if (!matches) return false;
219
268
  }
@@ -493,12 +542,14 @@ function matchKiciEventTrigger(trigger, event, traces) {
493
542
  traces.push(createTraceEntry("event name", trigger.eventName, eventName, nameMatches));
494
543
  if (!nameMatches) return false;
495
544
  if (trigger.match && Object.keys(trigger.match).length > 0) {
496
- const matches = matchJsonPath(event.payload.payload ?? {}, trigger.match);
545
+ const eventPayload = event.payload.payload ?? {};
546
+ const matches = matchJsonPath(eventPayload, trigger.match);
497
547
  traces.push(createTraceEntry("jsonpath match", JSON.stringify(trigger.match), "(payload)", matches));
498
548
  if (!matches) return false;
499
549
  }
500
550
  if (trigger.not && Object.keys(trigger.not).length > 0) {
501
- const passes = matchJsonPathNot(event.payload.payload ?? {}, trigger.not);
551
+ const eventPayload = event.payload.payload ?? {};
552
+ const passes = matchJsonPathNot(eventPayload, trigger.not);
502
553
  traces.push(createTraceEntry("jsonpath not", JSON.stringify(trigger.not), "(payload)", passes));
503
554
  if (!passes) return false;
504
555
  }
@@ -0,0 +1,27 @@
1
+ import type { LockTextMatch, TextMatch } from './types.js';
2
+ /** A definite verdict, or fail-visible indeterminate. Mirrors `ContentRequirementResult`. */
3
+ export interface TextMatchResult {
4
+ readonly pass: boolean;
5
+ /** Set (with `pass: false`) when the match could not be evaluated. */
6
+ readonly indeterminate?: string;
7
+ }
8
+ /**
9
+ * Compile a `/pattern/flags` (or bare-pattern) string, rejecting a ReDoS-prone
10
+ * pattern. Returns `null` when the pattern is syntactically invalid or fails the
11
+ * `safe-regex` star-height heuristic — callers treat `null` as indeterminate.
12
+ *
13
+ * Always compiles a FRESH RegExp. A `g`-flagged instance carries `lastIndex`
14
+ * across `.test()` calls, so a cached one would return alternating verdicts for
15
+ * the same input.
16
+ */
17
+ export declare function compileSafeRegex(source: string): RegExp | null;
18
+ /** True when the matcher carries at least one populated query key. */
19
+ export declare function textMatchHasQuery(m: TextMatch | LockTextMatch): boolean;
20
+ /** Render the populated keys for a decision-trace `pattern` field. */
21
+ export declare function describeTextMatch(m: LockTextMatch): string;
22
+ /**
23
+ * Evaluate a text match. Every entry in every list is a conjunct, and every
24
+ * populated key ANDs with the others; an empty matcher passes.
25
+ */
26
+ export declare function evaluateTextMatch(text: string, m: LockTextMatch): TextMatchResult;
27
+ //# sourceMappingURL=text-match.d.ts.map
@@ -0,0 +1,86 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import safeRegex from "safe-regex";
3
+ //#region src/trigger/text-match.ts
4
+ /**
5
+ * The single definition of what `contains` / `notContains` / `matches` /
6
+ * `notMatches` mean.
7
+ *
8
+ * Two sites consume it: the Tier-0 `commitMessage` trigger filter (in
9
+ * `matcher.ts`) and the Tier-1 `requires` content filter (in
10
+ * `content-requirements.ts`). Sharing one function is what keeps them from
11
+ * drifting into two dialects of the same vocabulary.
12
+ *
13
+ * Pure string logic — it imports only `safe-regex` and no Node built-in, so it
14
+ * belongs in the browser-safe barrel rather than a subpath export.
15
+ */
16
+ /**
17
+ * Compile a `/pattern/flags` (or bare-pattern) string, rejecting a ReDoS-prone
18
+ * pattern. Returns `null` when the pattern is syntactically invalid or fails the
19
+ * `safe-regex` star-height heuristic — callers treat `null` as indeterminate.
20
+ *
21
+ * Always compiles a FRESH RegExp. A `g`-flagged instance carries `lastIndex`
22
+ * across `.test()` calls, so a cached one would return alternating verdicts for
23
+ * the same input.
24
+ */
25
+ function compileSafeRegex(source) {
26
+ const wrapped = /^\/(.+)\/([gimsuy]*)$/.exec(source);
27
+ const pattern = wrapped ? wrapped[1] : source;
28
+ const flags = wrapped ? wrapped[2] : "";
29
+ let re;
30
+ try {
31
+ re = new RegExp(pattern, flags);
32
+ } catch {
33
+ return null;
34
+ }
35
+ return safeRegex(re) ? re : null;
36
+ }
37
+ /** True when the matcher carries at least one populated query key. */
38
+ function textMatchHasQuery(m) {
39
+ const populated = (v) => Array.isArray(v) ? v.length > 0 : v !== void 0 && v !== null;
40
+ return populated(m.contains) || populated(m.notContains) || populated(m.matches) || populated(m.notMatches);
41
+ }
42
+ /** Render the populated keys for a decision-trace `pattern` field. */
43
+ function describeTextMatch(m) {
44
+ const parts = [];
45
+ if (m.contains?.length) parts.push(`contains: [${m.contains.join(", ")}]`);
46
+ if (m.notContains?.length) parts.push(`notContains: [${m.notContains.join(", ")}]`);
47
+ if (m.matches?.length) parts.push(`matches: [${m.matches.join(", ")}]`);
48
+ if (m.notMatches?.length) parts.push(`notMatches: [${m.notMatches.join(", ")}]`);
49
+ if (m.ignoreCase) parts.push("ignoreCase");
50
+ return parts.length > 0 ? parts.join("; ") : "(no query)";
51
+ }
52
+ /** Evaluate every regex in `sources`; `expectMatch` selects matches vs notMatches. */
53
+ function evaluateRegexes(text, sources, expectMatch) {
54
+ for (const source of sources) {
55
+ const re = compileSafeRegex(source);
56
+ if (!re) return {
57
+ pass: false,
58
+ indeterminate: `unsafe or invalid regex: ${source}`
59
+ };
60
+ if (re.test(text) !== expectMatch) return { pass: false };
61
+ }
62
+ return { pass: true };
63
+ }
64
+ /**
65
+ * Evaluate a text match. Every entry in every list is a conjunct, and every
66
+ * populated key ANDs with the others; an empty matcher passes.
67
+ */
68
+ function evaluateTextMatch(text, m) {
69
+ const fold = (s) => m.ignoreCase === true ? s.toLowerCase() : s;
70
+ const haystack = fold(text);
71
+ for (const needle of m.contains ?? []) if (!haystack.includes(fold(needle))) return { pass: false };
72
+ for (const needle of m.notContains ?? []) if (haystack.includes(fold(needle))) return { pass: false };
73
+ if (m.matches?.length) {
74
+ const result = evaluateRegexes(text, m.matches, true);
75
+ if (!result.pass) return result;
76
+ }
77
+ if (m.notMatches?.length) {
78
+ const result = evaluateRegexes(text, m.notMatches, false);
79
+ if (!result.pass) return result;
80
+ }
81
+ return { pass: true };
82
+ }
83
+ //#endregion
84
+ export { compileSafeRegex, describeTextMatch, evaluateTextMatch, textMatchHasQuery };
85
+
86
+ //# sourceMappingURL=text-match.js.map
@@ -29,6 +29,15 @@
29
29
  * Schema version 30 (BREAKING): renames job-level `environments` to `contexts`.
30
30
  * Schema version 31 (additive): adds the workflows_failed_batch lock trigger.
31
31
  * Schema version 32 (additive): adds LockJob.sandbox (per-job escape-hatch request).
32
+ * Schema version 33 (additive): adds `requires` (declarative static content filter) to the push/pr/tag git-event triggers.
33
+ * Schema version 34 (additive): adds LockWorkflow.hasFilter (workflow-level pre-dispatch filter predicate).
34
+ * Schema version 35 (additive): adds `commitMessage` (LockTextMatch) to the push/pr/tag
35
+ * git-event triggers, and contains/notContains/notMatches to LockContentRequirement.
36
+ * Schema version 36 (additive): adds LockJob.invoke (invokeSource gate, incl. optional).
37
+ * Schema version 37 (additive): adds LockJob.gitCredentials (named git credential refs).
38
+ * Schema version 38 (additive): adds container.auth (private-registry credentials for the job image).
39
+ * Schema version 39 (additive): adds container.dockerfile/context/target/args (build the
40
+ * job's image from a Dockerfile in the repo) and container.auth.registry.
32
41
  */
33
42
  import { z } from 'zod';
34
43
  import type { ProviderType } from '../provider/types.js';
@@ -41,7 +50,7 @@ import { ExecutionJobStatus } from '../protocol/messages/execution-status.js';
41
50
  * schema change (additive or breaking); the bump-history comment above records
42
51
  * which. See `BREAKING_FLOOR` for the compatibility-window semantics.
43
52
  */
44
- export declare const SCHEMA_VERSION: 32;
53
+ export declare const SCHEMA_VERSION: 39;
45
54
  /**
46
55
  * Oldest lock schema version this codebase can still read correctly — the lower
47
56
  * bound of the acceptance window.
@@ -54,8 +63,8 @@ export declare const SCHEMA_VERSION: 32;
54
63
  * Bump rule: move this to the current `SCHEMA_VERSION` ONLY in the commit that
55
64
  * lands a `BREAKING` schema change (see the bump-history convention above). It
56
65
  * currently sits at 30 because v30 (`environments`→`contexts`) was the most
57
- * recent breaking bump; v31 and v32 were additive, so a v30 lock still reads
58
- * correctly.
66
+ * recent breaking bump; v31 through v35 were additive, so a v30 lock still
67
+ * reads correctly.
59
68
  */
60
69
  export declare const BREAKING_FLOOR: 30;
61
70
  /**
@@ -92,6 +101,121 @@ export interface LockBranchPattern {
92
101
  readonly pattern: string;
93
102
  readonly flags?: string;
94
103
  }
104
+ /**
105
+ * A declarative query over one piece of text: literal substrings and/or a
106
+ * regex, in either direction. Pure DATA the orchestrator interprets — never
107
+ * author code — so it is safe to evaluate inside the orchestrator under the
108
+ * execution-purity model.
109
+ *
110
+ * Every entry in a list is a CONJUNCT: `contains: [a, b]` means the text
111
+ * contains `a` AND contains `b`. OR is expressed by declaring two triggers,
112
+ * since a workflow's trigger list is already "first match wins".
113
+ *
114
+ * This is the SDK-facing shape a workflow author writes; the compiler
115
+ * normalizes it to {@link LockTextMatch}.
116
+ */
117
+ export interface TextMatch {
118
+ /** Literal substring(s). Every entry must be present. */
119
+ readonly contains?: string | readonly string[];
120
+ /** Literal substring(s). No entry may be present. */
121
+ readonly notContains?: string | readonly string[];
122
+ /** Regex(es), as a RegExp or a `/pattern/flags` string. Every one must match. */
123
+ readonly matches?: string | RegExp | readonly (string | RegExp)[];
124
+ /** Regex(es). None may match. */
125
+ readonly notMatches?: string | RegExp | readonly (string | RegExp)[];
126
+ /**
127
+ * Case-insensitive comparison for `contains`/`notContains` ONLY. Default false.
128
+ * It deliberately does not touch the regex keys: a regex already carries its
129
+ * own flags, and injecting `i` into a pattern whose author omitted it would
130
+ * silently change its meaning.
131
+ */
132
+ readonly ignoreCase?: boolean;
133
+ }
134
+ /**
135
+ * Lock-file form of {@link TextMatch}. The compiler normalizes every key to a
136
+ * flat array, and every regex to a `/pattern/flags` string, so the orchestrator
137
+ * matcher has exactly one shape to interpret.
138
+ */
139
+ export interface LockTextMatch {
140
+ readonly contains?: readonly string[];
141
+ readonly notContains?: readonly string[];
142
+ /** Always in `/pattern/flags` form. */
143
+ readonly matches?: readonly string[];
144
+ /** Always in `/pattern/flags` form. */
145
+ readonly notMatches?: readonly string[];
146
+ readonly ignoreCase?: boolean;
147
+ }
148
+ /**
149
+ * How a file's bytes are parsed before a content query runs.
150
+ * `auto` picks by extension: `.json` → json, `.yaml`/`.yml` → yaml, else text.
151
+ */
152
+ export type ContentFormat = 'json' | 'yaml' | 'text' | 'auto';
153
+ /**
154
+ * Declarative static content filter: a query over the bytes of one source file
155
+ * at the event's ref. Pure DATA the orchestrator's own matcher interprets — never
156
+ * author code — so it is safe to evaluate in the orchestrator (Part B of the
157
+ * execution-purity model). Query keys
158
+ * (`exists`/`match`/`not`/`contains`/`notContains`/`matches`/`notMatches`) are
159
+ * AND-ed within an entry; `absent` is mutually exclusive with them and passes
160
+ * only when the file is missing.
161
+ *
162
+ * This is the SDK-facing shape a workflow author writes, which accepts a scalar
163
+ * or `RegExp` where the lock form ({@link LockContentRequirement}) carries a flat
164
+ * array of `/pattern/flags` strings — the compiler normalizes one to the other.
165
+ */
166
+ export interface ContentRequirement {
167
+ /** Repo-relative path of the file to query. */
168
+ readonly file: string;
169
+ /** Parse format; defaults to `auto` when unset. */
170
+ readonly format?: ContentFormat;
171
+ /** JSONPath expressions that must each resolve to ≥1 node (json/yaml only). */
172
+ readonly exists?: readonly string[];
173
+ /** JSONPath → expected-value map; every expression must match (json/yaml only). */
174
+ readonly match?: Record<string, unknown>;
175
+ /** JSONPath → value map; passes only when NONE match (json/yaml only). */
176
+ readonly not?: Record<string, unknown>;
177
+ /** Literal substring(s) that must ALL be present in the raw file text. */
178
+ readonly contains?: string | readonly string[];
179
+ /** Literal substring(s) of which NONE may be present in the raw file text. */
180
+ readonly notContains?: string | readonly string[];
181
+ /** Regex(es) that must ALL match the raw file text (RegExp or `/pattern/flags`). */
182
+ readonly matches?: string | RegExp | readonly (string | RegExp)[];
183
+ /** Regex(es) of which NONE may match the raw file text. */
184
+ readonly notMatches?: string | RegExp | readonly (string | RegExp)[];
185
+ /** Case-insensitive `contains`/`notContains`. Default false. */
186
+ readonly ignoreCase?: boolean;
187
+ /** When true, the entry passes only if the file is absent. Excludes all query keys. */
188
+ readonly absent?: boolean;
189
+ }
190
+ /**
191
+ * Lock-file form of {@link ContentRequirement}. The compiler normalizes each
192
+ * raw-text key to the flat {@link LockTextMatch} shape, so the orchestrator
193
+ * matcher has one shape to interpret.
194
+ */
195
+ export interface LockContentRequirement {
196
+ readonly file: string;
197
+ readonly format?: ContentFormat;
198
+ readonly exists?: readonly string[];
199
+ readonly match?: Record<string, unknown>;
200
+ readonly not?: Record<string, unknown>;
201
+ readonly contains?: readonly string[];
202
+ readonly notContains?: readonly string[];
203
+ readonly matches?: readonly string[];
204
+ readonly notMatches?: readonly string[];
205
+ readonly ignoreCase?: boolean;
206
+ readonly absent?: boolean;
207
+ }
208
+ /**
209
+ * Resolve a content requirement's parse format to a concrete value. An explicit
210
+ * non-`auto` format is returned as-is; `auto` (or unset) is resolved by the file
211
+ * extension: `.json` → json, `.yaml`/`.yml` → yaml, everything else text.
212
+ *
213
+ * Yaml-free (pure string logic) so it lives in the browser-safe barrel and is
214
+ * the single source of truth for both the compiler's compile-time serializer and
215
+ * the orchestrator's eval-time matcher — the two can never disagree about how a
216
+ * file's format is picked.
217
+ */
218
+ export declare function resolveContentFormat(file: string, format: ContentFormat | undefined): 'json' | 'yaml' | 'text';
95
219
  /**
96
220
  * PR trigger in lock file.
97
221
  * Optimized for orchestrator event matching - flat structure with all filters accessible.
@@ -103,6 +227,10 @@ export interface LockPrTrigger {
103
227
  readonly sourceBranches: readonly LockBranchPattern[];
104
228
  readonly paths: readonly string[];
105
229
  readonly repos?: readonly LockBranchPattern[];
230
+ /** Declarative static content filter over source files at the event ref (AND-ed). */
231
+ readonly requires?: readonly LockContentRequirement[];
232
+ /** Declarative static filter over the event's commit message / PR title+body. */
233
+ readonly commitMessage?: LockTextMatch;
106
234
  }
107
235
  /**
108
236
  * Push trigger in lock file.
@@ -113,6 +241,10 @@ export interface LockPushTrigger {
113
241
  readonly branches: readonly LockBranchPattern[];
114
242
  readonly paths: readonly string[];
115
243
  readonly repos?: readonly LockBranchPattern[];
244
+ /** Declarative static content filter over source files at the event ref (AND-ed). */
245
+ readonly requires?: readonly LockContentRequirement[];
246
+ /** Declarative static filter over the event's commit message / PR title+body. */
247
+ readonly commitMessage?: LockTextMatch;
116
248
  }
117
249
  /**
118
250
  * Tag trigger in lock file.
@@ -122,6 +254,10 @@ export interface LockTagTrigger {
122
254
  readonly _type: 'tag';
123
255
  readonly patterns: readonly LockBranchPattern[];
124
256
  readonly repos?: readonly LockBranchPattern[];
257
+ /** Declarative static content filter over source files at the event ref (AND-ed). */
258
+ readonly requires?: readonly LockContentRequirement[];
259
+ /** Declarative static filter over the event's commit message / PR title+body. */
260
+ readonly commitMessage?: LockTextMatch;
125
261
  }
126
262
  /**
127
263
  * Comment trigger in lock file.
@@ -426,17 +562,27 @@ export type LockStepEntry = LockStep | LockParallelStep;
426
562
  /** Type guard distinguishing a parallel group from an ordinary lock step. */
427
563
  export declare function isLockParallelStep(entry: LockStepEntry): entry is LockParallelStep;
428
564
  /**
429
- * Inline expression value for pure dynamic functions.
430
- * The compiler serializes pure functions as { _type: 'inline', expression: '(event) => ...' }
431
- * and the orchestrator evaluates them via vm.runInNewContext at dispatch time.
432
- * struct with discriminant and expression field.
433
- * _type: 'inline' alongside existing 'static' and 'dynamic' discriminants.
565
+ * Serialized inline expression for a dynamic env/context/concurrencyGroup
566
+ * field, shaped as `{ _type: 'inline', expression: '(event) => ...' }`
567
+ * alongside the existing 'static' and 'dynamic' discriminants.
568
+ *
569
+ * @deprecated Schema v11 inline expressions are no longer evaluated in the
570
+ * orchestrator. Dynamic env/context/concurrencyGroup fields are resolved on the
571
+ * eval agent's init-runner. The compiler no longer emits this type; readers keep
572
+ * recognizing it only to defer an old lock's field to the init round. Removed at
573
+ * the next major (v1.0.0).
434
574
  */
435
575
  export interface LockInlineValue {
436
576
  readonly _type: 'inline';
437
577
  readonly expression: string;
438
578
  }
439
- /** Type guard for inline expression values */
579
+ /**
580
+ * Type guard for inline expression values.
581
+ *
582
+ * @deprecated See {@link LockInlineValue}. Retained only so a reader can
583
+ * recognize an old lock's inline field and defer it to the eval agent's
584
+ * init-runner. Removed at the next major (v1.0.0).
585
+ */
440
586
  export declare function isLockInlineValue(value: unknown): value is LockInlineValue;
441
587
  /**
442
588
  * Author-facing keyword sugar for a `needs` edge's run condition. Each keyword
@@ -595,9 +741,34 @@ export interface ResolvedSandboxGrant {
595
741
  /** Run the container as this user (operator-config path only in Phase 2). */
596
742
  user?: string;
597
743
  }
744
+ /**
745
+ * Invoke-gate action carried on a lock job (compiled from `invokeSource()`).
746
+ * A job carrying this never runs steps on an agent: the orchestrator emits the
747
+ * named kici event at the source repo and gates on the runs it triggers.
748
+ */
749
+ export interface LockInvoke {
750
+ /** The kici event name to emit; source-repo workflows subscribe with `kiciEvent({ name })`. */
751
+ readonly event: string;
752
+ /** Target scope. `'source'` targets exactly the source repo (the only v1 scope). */
753
+ readonly scope: 'source';
754
+ /** Optional event payload delivered to subscribers. */
755
+ readonly payload?: Readonly<Record<string, unknown>>;
756
+ /**
757
+ * When true, a zero-subscriber emit succeeds immediately (the repo may opt
758
+ * out). Absent/false (the default) fails the gate on zero subscribers.
759
+ */
760
+ readonly optional?: boolean;
761
+ }
598
762
  export interface LockJob {
599
763
  readonly _type: 'static';
600
764
  readonly name: string;
765
+ /**
766
+ * Invoke-gate action. When set, the job is a gate: it never dispatches steps
767
+ * to an agent; the orchestrator emits `invoke.event` at the source repo and
768
+ * tracks each triggered run as a proxy child. Additive — an older
769
+ * orchestrator that does not understand it ignores it (the gate stays inert).
770
+ */
771
+ readonly invoke?: LockInvoke;
601
772
  /** Single-agent targeting matchers. Absent when the job uses `runsOnAll` instead. */
602
773
  readonly runsOn?: readonly LabelMatcher[];
603
774
  readonly excludeLabels?: readonly LabelMatcher[];
@@ -607,6 +778,16 @@ export interface LockJob {
607
778
  * `any` picks any available agent. Absent on a `runsOnAll` fan-out job.
608
779
  */
609
780
  readonly runsOnPick?: RunsOnPick;
781
+ /**
782
+ * Named git credentials for this job, as SECRET NAMES in qualified
783
+ * `<context>:<secret-name>` form — never credential material.
784
+ *
785
+ * `default` is used when a call names no credential; any other key is
786
+ * referenced by name. Additive: an older orchestrator that does not
787
+ * understand it simply passes it through in `jobConfig`, and an older agent
788
+ * ignores it (git falls back to its own mechanisms, exactly as before).
789
+ */
790
+ readonly gitCredentials?: Readonly<Record<string, Readonly<Record<string, string>>>>;
610
791
  /**
611
792
  * Host fan-out predicate (mutually exclusive with `runsOn`). When set, the job
612
793
  * fans out to every roster host matching the predicate, one pinned child per host.
@@ -640,21 +821,28 @@ export interface LockJob {
640
821
  readonly rules?: readonly LockRule[];
641
822
  readonly description?: string;
642
823
  /**
643
- * Bound contexts in merge order. Each entry is a static name or inline
644
- * expression (pure function); `dynamic` is set when it is a function resolved at
645
- * two-phase eval. Later entries override earlier ones on name collisions.
824
+ * Bound contexts in merge order. Each entry is a static name; `dynamic` is set
825
+ * when it is a function resolved on the eval agent's init-runner. Later entries
826
+ * override earlier ones on name collisions. The `LockInlineValue` shape is a
827
+ * deprecated form still accepted from old locks (see {@link LockInlineValue}).
646
828
  */
647
829
  readonly contexts?: ReadonlyArray<{
648
830
  value: string | LockInlineValue;
649
831
  dynamic: boolean;
650
832
  }>;
651
- /** Static environment variables or inline expression (pure function). */
833
+ /**
834
+ * Static environment variables. A deprecated `LockInlineValue` shape is still
835
+ * accepted from old locks (see {@link LockInlineValue}).
836
+ */
652
837
  readonly env?: Record<string, string> | LockInlineValue;
653
- /** When true, env is dynamic (function) -- resolved at orchestrator two-phase eval or inline. */
838
+ /** When true, env is dynamic (function) -- resolved on the eval agent's init-runner. */
654
839
  readonly dynamicEnv?: boolean;
655
- /** Concurrency group name (static string) or inline expression (pure function). */
840
+ /**
841
+ * Concurrency group name (static string). A deprecated `LockInlineValue` shape
842
+ * is still accepted from old locks (see {@link LockInlineValue}).
843
+ */
656
844
  readonly concurrencyGroup?: string | LockInlineValue;
657
- /** When true, concurrencyGroup is dynamic (function) -- resolved at orchestrator two-phase eval or inline. */
845
+ /** When true, concurrencyGroup is dynamic (function) -- resolved on the eval agent's init-runner. */
658
846
  readonly dynamicConcurrencyGroup?: boolean;
659
847
  /** Total job wall-clock timeout in milliseconds (init + all steps + hooks). Threaded to the agent via jobConfig. */
660
848
  readonly timeout?: number;
@@ -666,15 +854,55 @@ export interface LockJob {
666
854
  readonly resources?: import('../scaler/resource-types.js').ResourceRequest;
667
855
  /**
668
856
  * Container image selecting the container execution backend on the agent. A
669
- * bare image string or an object with `image` + optional `env`. When set, the
670
- * agent's `determineExecutionMode` routes the job to the container sandbox
671
- * (top priority), so the orchestrator threads it through dispatch as
672
- * `jobConfig.container`. (Shape mirrors the SDK `string | ContainerConfig`;
673
- * the engine cannot import the SDK, so it is inlined here.)
857
+ * bare image string, or an object naming exactly one image source: a
858
+ * finalized `image`, or a `dockerfile` the agent builds from the cloned tree
859
+ * before the job starts. When set, the agent's `determineExecutionMode`
860
+ * routes the job to the container sandbox (top priority), so the orchestrator
861
+ * threads it through dispatch as `jobConfig.container`. (Shape mirrors the SDK
862
+ * `string | ContainerConfig`, including `ContainerRegistryAuth`; the engine
863
+ * cannot import the SDK, so it is inlined here.)
674
864
  */
675
865
  readonly container?: string | {
676
- readonly image: string;
866
+ /** Finalized image to pull. Exactly one of `image` / `dockerfile` is set. */
867
+ readonly image?: string;
868
+ /**
869
+ * Repo-relative Dockerfile the agent builds before the job runs.
870
+ *
871
+ * The agent re-validates this path against the resolved workdir: a lock
872
+ * file is repo content, so a path that escapes the tree must be refused
873
+ * on the agent too, not only by the SDK that wrote it.
874
+ */
875
+ readonly dockerfile?: string;
876
+ /** Repo-relative build context. Defaults to the repository root. */
877
+ readonly context?: string;
878
+ /** Build stage to stop at. */
879
+ readonly target?: string;
880
+ /**
881
+ * Build arguments. Plain strings — never secret references, because a
882
+ * build argument is recorded in the built image's history.
883
+ */
884
+ readonly args?: Record<string, string>;
677
885
  readonly env?: Record<string, string>;
886
+ /**
887
+ * Private-registry credentials for pulling `image`.
888
+ *
889
+ * Flattened `Sourced<Name>` pairs: exactly one half of each pair is
890
+ * set — `*Secret` names a `<context>:<secret-name>` entry to resolve,
891
+ * `*Value` carries material supplied at run time.
892
+ */
893
+ readonly auth?: {
894
+ readonly username?: string;
895
+ readonly usernameSecret?: string;
896
+ readonly usernameValue?: string;
897
+ readonly tokenSecret?: string;
898
+ readonly tokenValue?: string;
899
+ /**
900
+ * Registry host the credentials belong to. Derived from `image` when
901
+ * one is set; REQUIRED with `dockerfile`, whose base image is named
902
+ * inside the Dockerfile and cannot be read back out reliably.
903
+ */
904
+ readonly registry?: string;
905
+ };
678
906
  };
679
907
  /**
680
908
  * Workflow-declared per-job sandbox escape-hatch request (container jobs
@@ -769,6 +997,13 @@ export interface LockWorkflow {
769
997
  readonly timeout?: number;
770
998
  /** Normalized approval gate; when set the whole run is held before any job dispatches. */
771
999
  readonly approval?: LockApproval;
1000
+ /**
1001
+ * True when the workflow declares a `filter` predicate. A bare flag, not a
1002
+ * source reference: `LockWorkflow.source` already identifies the module and
1003
+ * export, so the eval agent loads it and reads `.filter` off the workflow
1004
+ * object. Mirrors the `dynamicEnv` / `dynamicConcurrencyGroup` convention.
1005
+ */
1006
+ readonly hasFilter?: boolean;
772
1007
  }
773
1008
  /**
774
1009
  * Complete lock file structure.
@@ -783,7 +1018,7 @@ export interface LockWorkflow {
783
1018
  * v8 adds runsOn polymorphic type (string | string[] | selector) and excludeLabels.
784
1019
  * v9 adds repos/notRepos repo pattern fields to git-event triggers for global workflow matching.
785
1020
  * v10 removes notRepos/notPaths fields; negative patterns use ! prefix in repos/paths arrays.
786
- * v11 adds LockInlineValue type for pure function inline evaluation.
1021
+ * v11 adds the LockInlineValue type (deprecated; inline dynamic fields are resolved on the eval agent).
787
1022
  * v12 adds workflow-level registries and installEnv for private npm registry auth.
788
1023
  * v13 adds job-level and workflow-level timeout.
789
1024
  */
@@ -869,6 +1104,13 @@ export interface SimulatedEvent {
869
1104
  * to the previous owner.
870
1105
  */
871
1106
  senderUserId?: string;
1107
+ /**
1108
+ * Text a `commitMessage` trigger filter is tested against: the full head-commit
1109
+ * message for push/tag, or PR title + body for pull-request events. Absent when
1110
+ * the provider payload carries none — which a `commitMessage` filter treats as
1111
+ * INDETERMINATE (fail-visible), never as an empty string.
1112
+ */
1113
+ commitMessage?: string;
872
1114
  /** Repository identifier where the event occurred (e.g., "owner/repo").
873
1115
  * Used by global workflow repo pattern matching. */
874
1116
  sourceRepo?: string;