@codmir/contracts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1970 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1233 -0
- package/dist/index.d.ts +1233 -0
- package/dist/index.js +1924 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1924 @@
|
|
|
1
|
+
// src/schema.ts
|
|
2
|
+
var DURATION_SYNC_THRESHOLDS = {
|
|
3
|
+
instant: 5e3,
|
|
4
|
+
// 5 seconds
|
|
5
|
+
quick: 3e4,
|
|
6
|
+
// 30 seconds
|
|
7
|
+
medium: 12e4,
|
|
8
|
+
// 2 minutes
|
|
9
|
+
long: 3e5
|
|
10
|
+
// 5 minutes (but usually async)
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/grace.ts
|
|
14
|
+
var DEFAULT_GRACE_EXTENSIONS = {
|
|
15
|
+
graceVersion: "grace.v1",
|
|
16
|
+
ethics: {
|
|
17
|
+
impacts: [],
|
|
18
|
+
overallRiskScore: 0,
|
|
19
|
+
requiresHumanReview: false
|
|
20
|
+
},
|
|
21
|
+
reversibility: {
|
|
22
|
+
fullyReversible: true,
|
|
23
|
+
snapshotBeforeExecution: true,
|
|
24
|
+
snapshotRetentionMs: 7 * 24 * 60 * 60 * 1e3,
|
|
25
|
+
// 7 days
|
|
26
|
+
rollbackStrategy: "full"
|
|
27
|
+
},
|
|
28
|
+
humanOversight: {
|
|
29
|
+
minCheckpoints: 0,
|
|
30
|
+
allowAutonomous: true
|
|
31
|
+
},
|
|
32
|
+
transparency: {
|
|
33
|
+
logDecisions: true,
|
|
34
|
+
logsAccessible: true,
|
|
35
|
+
auditRetentionDays: 90
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function calculateRiskScore(impacts) {
|
|
39
|
+
const severityWeights = {
|
|
40
|
+
none: 0,
|
|
41
|
+
low: 10,
|
|
42
|
+
medium: 25,
|
|
43
|
+
high: 50,
|
|
44
|
+
critical: 100
|
|
45
|
+
};
|
|
46
|
+
if (impacts.length === 0) return 0;
|
|
47
|
+
const totalWeight = impacts.reduce(
|
|
48
|
+
(sum, impact) => sum + severityWeights[impact.severity],
|
|
49
|
+
0
|
|
50
|
+
);
|
|
51
|
+
return Math.min(100, totalWeight);
|
|
52
|
+
}
|
|
53
|
+
function requiresHumanReview(impacts) {
|
|
54
|
+
return impacts.some(
|
|
55
|
+
(i) => i.severity === "high" || i.severity === "critical"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
var GRACE_PRINCIPLES = {
|
|
59
|
+
transparency: "Every action logged, every decision traceable",
|
|
60
|
+
consent: "Explicit permission scope, no implicit access",
|
|
61
|
+
reversibility: "Every AI action must be undoable",
|
|
62
|
+
accountability: "Task attribution, versioned modifications",
|
|
63
|
+
economicBoundaries: "Token usage limit, execution cost cap",
|
|
64
|
+
humanDignity: "AI augments, never replaces human judgment on critical decisions",
|
|
65
|
+
fairness: "Distributed power must not centralize control",
|
|
66
|
+
sustainability: "Prefer renewable-powered compute when available"
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/templates.ts
|
|
70
|
+
function getByPath(obj, path) {
|
|
71
|
+
const parts = path.split(".").filter(Boolean);
|
|
72
|
+
let cur = obj;
|
|
73
|
+
for (const p of parts) {
|
|
74
|
+
if (cur == null || typeof cur !== "object") return void 0;
|
|
75
|
+
cur = cur[p];
|
|
76
|
+
}
|
|
77
|
+
return cur;
|
|
78
|
+
}
|
|
79
|
+
function setByPath(obj, path, value) {
|
|
80
|
+
const parts = path.split(".").filter(Boolean);
|
|
81
|
+
if (parts.length === 0) return;
|
|
82
|
+
let cur = obj;
|
|
83
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
84
|
+
const p = parts[i];
|
|
85
|
+
if (cur[p] == null || typeof cur[p] !== "object") {
|
|
86
|
+
cur[p] = {};
|
|
87
|
+
}
|
|
88
|
+
cur = cur[p];
|
|
89
|
+
}
|
|
90
|
+
cur[parts[parts.length - 1]] = value;
|
|
91
|
+
}
|
|
92
|
+
function deleteByPath(obj, path) {
|
|
93
|
+
const parts = path.split(".").filter(Boolean);
|
|
94
|
+
if (parts.length === 0) return;
|
|
95
|
+
let cur = obj;
|
|
96
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
97
|
+
const p = parts[i];
|
|
98
|
+
if (cur[p] == null || typeof cur[p] !== "object") return;
|
|
99
|
+
cur = cur[p];
|
|
100
|
+
}
|
|
101
|
+
delete cur[parts[parts.length - 1]];
|
|
102
|
+
}
|
|
103
|
+
var TEMPLATE_PATTERN = /^\{\{(.+)\}\}$/;
|
|
104
|
+
function isTemplate(value) {
|
|
105
|
+
return TEMPLATE_PATTERN.test(value);
|
|
106
|
+
}
|
|
107
|
+
function extractTemplatePath(value) {
|
|
108
|
+
const match = value.match(TEMPLATE_PATTERN);
|
|
109
|
+
return match ? match[1].trim() : null;
|
|
110
|
+
}
|
|
111
|
+
function resolveTemplates(ctx, input) {
|
|
112
|
+
if (typeof input === "string") {
|
|
113
|
+
const path = extractTemplatePath(input);
|
|
114
|
+
if (path) {
|
|
115
|
+
const v = getByPath(ctx, path);
|
|
116
|
+
return v === void 0 ? null : v;
|
|
117
|
+
}
|
|
118
|
+
return input;
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(input)) {
|
|
121
|
+
return input.map((v) => resolveTemplates(ctx, v));
|
|
122
|
+
}
|
|
123
|
+
if (input && typeof input === "object") {
|
|
124
|
+
const out = {};
|
|
125
|
+
for (const [k, v] of Object.entries(input)) {
|
|
126
|
+
out[k] = resolveTemplates(ctx, v);
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
return input;
|
|
131
|
+
}
|
|
132
|
+
function resolveStepInput(ctx, input) {
|
|
133
|
+
return resolveTemplates(ctx, input);
|
|
134
|
+
}
|
|
135
|
+
function cloneJson(value) {
|
|
136
|
+
return JSON.parse(JSON.stringify(value));
|
|
137
|
+
}
|
|
138
|
+
function mergeJson(target, source) {
|
|
139
|
+
return { ...target, ...source };
|
|
140
|
+
}
|
|
141
|
+
function deepMergeJson(target, source) {
|
|
142
|
+
const result = { ...target };
|
|
143
|
+
for (const [key, value] of Object.entries(source)) {
|
|
144
|
+
if (value && typeof value === "object" && !Array.isArray(value) && result[key] && typeof result[key] === "object" && !Array.isArray(result[key])) {
|
|
145
|
+
result[key] = deepMergeJson(
|
|
146
|
+
result[key],
|
|
147
|
+
value
|
|
148
|
+
);
|
|
149
|
+
} else {
|
|
150
|
+
result[key] = value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/conditions.ts
|
|
157
|
+
function evalCondition(ctx, expr) {
|
|
158
|
+
switch (expr.op) {
|
|
159
|
+
case "eq": {
|
|
160
|
+
const actual = getByPath(ctx, expr.path);
|
|
161
|
+
return jsonEquals(actual, expr.value);
|
|
162
|
+
}
|
|
163
|
+
case "neq": {
|
|
164
|
+
const actual = getByPath(ctx, expr.path);
|
|
165
|
+
return !jsonEquals(actual, expr.value);
|
|
166
|
+
}
|
|
167
|
+
case "gt": {
|
|
168
|
+
const actual = getByPath(ctx, expr.path);
|
|
169
|
+
return typeof actual === "number" && actual > expr.value;
|
|
170
|
+
}
|
|
171
|
+
case "gte": {
|
|
172
|
+
const actual = getByPath(ctx, expr.path);
|
|
173
|
+
return typeof actual === "number" && actual >= expr.value;
|
|
174
|
+
}
|
|
175
|
+
case "lt": {
|
|
176
|
+
const actual = getByPath(ctx, expr.path);
|
|
177
|
+
return typeof actual === "number" && actual < expr.value;
|
|
178
|
+
}
|
|
179
|
+
case "lte": {
|
|
180
|
+
const actual = getByPath(ctx, expr.path);
|
|
181
|
+
return typeof actual === "number" && actual <= expr.value;
|
|
182
|
+
}
|
|
183
|
+
case "exists": {
|
|
184
|
+
return getByPath(ctx, expr.path) !== void 0;
|
|
185
|
+
}
|
|
186
|
+
case "contains": {
|
|
187
|
+
const actual = getByPath(ctx, expr.path);
|
|
188
|
+
if (typeof actual === "string") {
|
|
189
|
+
return actual.includes(expr.value);
|
|
190
|
+
}
|
|
191
|
+
if (Array.isArray(actual)) {
|
|
192
|
+
return actual.some((item) => jsonEquals(item, expr.value));
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
case "startsWith": {
|
|
197
|
+
const actual = getByPath(ctx, expr.path);
|
|
198
|
+
return typeof actual === "string" && actual.startsWith(expr.value);
|
|
199
|
+
}
|
|
200
|
+
case "endsWith": {
|
|
201
|
+
const actual = getByPath(ctx, expr.path);
|
|
202
|
+
return typeof actual === "string" && actual.endsWith(expr.value);
|
|
203
|
+
}
|
|
204
|
+
case "and": {
|
|
205
|
+
return expr.exprs.every((e) => evalCondition(ctx, e));
|
|
206
|
+
}
|
|
207
|
+
case "or": {
|
|
208
|
+
return expr.exprs.some((e) => evalCondition(ctx, e));
|
|
209
|
+
}
|
|
210
|
+
case "not": {
|
|
211
|
+
return !evalCondition(ctx, expr.expr);
|
|
212
|
+
}
|
|
213
|
+
default: {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function jsonEquals(a, b) {
|
|
219
|
+
if (a === b) return true;
|
|
220
|
+
if (a === void 0 || a === null || b === null) return a === b;
|
|
221
|
+
if (typeof a !== typeof b) return false;
|
|
222
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
223
|
+
if (a.length !== b.length) return false;
|
|
224
|
+
return a.every((item, index) => jsonEquals(item, b[index]));
|
|
225
|
+
}
|
|
226
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
227
|
+
const keysA = Object.keys(a);
|
|
228
|
+
const keysB = Object.keys(b);
|
|
229
|
+
if (keysA.length !== keysB.length) return false;
|
|
230
|
+
return keysA.every(
|
|
231
|
+
(key) => jsonEquals(
|
|
232
|
+
a[key],
|
|
233
|
+
b[key]
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return a === b;
|
|
238
|
+
}
|
|
239
|
+
function validateCondition(expr) {
|
|
240
|
+
switch (expr.op) {
|
|
241
|
+
case "eq":
|
|
242
|
+
case "neq":
|
|
243
|
+
if (typeof expr.path !== "string" || !expr.path) {
|
|
244
|
+
throw new Error(`Condition ${expr.op} requires a non-empty path`);
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
case "gt":
|
|
248
|
+
case "gte":
|
|
249
|
+
case "lt":
|
|
250
|
+
case "lte":
|
|
251
|
+
if (typeof expr.path !== "string" || !expr.path) {
|
|
252
|
+
throw new Error(`Condition ${expr.op} requires a non-empty path`);
|
|
253
|
+
}
|
|
254
|
+
if (typeof expr.value !== "number") {
|
|
255
|
+
throw new Error(`Condition ${expr.op} requires a numeric value`);
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
case "exists":
|
|
259
|
+
if (typeof expr.path !== "string" || !expr.path) {
|
|
260
|
+
throw new Error(`Condition ${expr.op} requires a non-empty path`);
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
case "contains":
|
|
264
|
+
case "startsWith":
|
|
265
|
+
case "endsWith":
|
|
266
|
+
if (typeof expr.path !== "string" || !expr.path) {
|
|
267
|
+
throw new Error(`Condition ${expr.op} requires a non-empty path`);
|
|
268
|
+
}
|
|
269
|
+
if (typeof expr.value !== "string") {
|
|
270
|
+
throw new Error(`Condition ${expr.op} requires a string value`);
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
case "and":
|
|
274
|
+
case "or":
|
|
275
|
+
if (!Array.isArray(expr.exprs) || expr.exprs.length === 0) {
|
|
276
|
+
throw new Error(`Condition ${expr.op} requires a non-empty array of expressions`);
|
|
277
|
+
}
|
|
278
|
+
for (const subExpr of expr.exprs) {
|
|
279
|
+
validateCondition(subExpr);
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
case "not":
|
|
283
|
+
if (!expr.expr) {
|
|
284
|
+
throw new Error(`Condition not requires an expression`);
|
|
285
|
+
}
|
|
286
|
+
validateCondition(expr.expr);
|
|
287
|
+
break;
|
|
288
|
+
default: {
|
|
289
|
+
throw new Error(`Unknown condition operator: ${expr.op}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function eq(path, value) {
|
|
294
|
+
return { op: "eq", path, value };
|
|
295
|
+
}
|
|
296
|
+
function neq(path, value) {
|
|
297
|
+
return { op: "neq", path, value };
|
|
298
|
+
}
|
|
299
|
+
function exists(path) {
|
|
300
|
+
return { op: "exists", path };
|
|
301
|
+
}
|
|
302
|
+
function and(...exprs) {
|
|
303
|
+
return { op: "and", exprs };
|
|
304
|
+
}
|
|
305
|
+
function or(...exprs) {
|
|
306
|
+
return { op: "or", exprs };
|
|
307
|
+
}
|
|
308
|
+
function not(expr) {
|
|
309
|
+
return { op: "not", expr };
|
|
310
|
+
}
|
|
311
|
+
function gt(path, value) {
|
|
312
|
+
return { op: "gt", path, value };
|
|
313
|
+
}
|
|
314
|
+
function gte(path, value) {
|
|
315
|
+
return { op: "gte", path, value };
|
|
316
|
+
}
|
|
317
|
+
function lt(path, value) {
|
|
318
|
+
return { op: "lt", path, value };
|
|
319
|
+
}
|
|
320
|
+
function lte(path, value) {
|
|
321
|
+
return { op: "lte", path, value };
|
|
322
|
+
}
|
|
323
|
+
function contains(path, value) {
|
|
324
|
+
return { op: "contains", path, value };
|
|
325
|
+
}
|
|
326
|
+
function startsWith(path, value) {
|
|
327
|
+
return { op: "startsWith", path, value };
|
|
328
|
+
}
|
|
329
|
+
function endsWith(path, value) {
|
|
330
|
+
return { op: "endsWith", path, value };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/validation.ts
|
|
334
|
+
var ContractValidationError = class extends Error {
|
|
335
|
+
constructor(message, field, details) {
|
|
336
|
+
super(message);
|
|
337
|
+
this.field = field;
|
|
338
|
+
this.details = details;
|
|
339
|
+
this.name = "ContractValidationError";
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
function validateContract(contract) {
|
|
343
|
+
const errors = collectValidationErrors(contract);
|
|
344
|
+
if (errors.length > 0) {
|
|
345
|
+
throw errors[0];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function validateContractSafe(contract) {
|
|
349
|
+
const errors = collectValidationErrors(contract);
|
|
350
|
+
if (errors.length === 0) {
|
|
351
|
+
return { valid: true };
|
|
352
|
+
}
|
|
353
|
+
return { valid: false, errors };
|
|
354
|
+
}
|
|
355
|
+
function collectValidationErrors(contract) {
|
|
356
|
+
const errors = [];
|
|
357
|
+
if (contract.schemaVersion !== "codmir.contract.v1") {
|
|
358
|
+
errors.push(
|
|
359
|
+
new ContractValidationError(
|
|
360
|
+
`Unsupported schemaVersion: ${contract.schemaVersion}`,
|
|
361
|
+
"schemaVersion"
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (!contract.id || typeof contract.id !== "string") {
|
|
366
|
+
errors.push(new ContractValidationError("Contract id is required", "id"));
|
|
367
|
+
}
|
|
368
|
+
if (!contract.version || typeof contract.version !== "string") {
|
|
369
|
+
errors.push(new ContractValidationError("Contract version is required", "version"));
|
|
370
|
+
}
|
|
371
|
+
if (!contract.title || typeof contract.title !== "string") {
|
|
372
|
+
errors.push(new ContractValidationError("Contract title is required", "title"));
|
|
373
|
+
}
|
|
374
|
+
if (!Array.isArray(contract.triggers) || contract.triggers.length === 0) {
|
|
375
|
+
errors.push(
|
|
376
|
+
new ContractValidationError("Contract must have at least one trigger", "triggers")
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
for (let i = 0; i < contract.triggers.length; i++) {
|
|
380
|
+
const trigger = contract.triggers[i];
|
|
381
|
+
if (trigger.type !== "event") {
|
|
382
|
+
errors.push(
|
|
383
|
+
new ContractValidationError(
|
|
384
|
+
`Trigger ${i} has unsupported type: ${trigger.type}`,
|
|
385
|
+
`triggers[${i}].type`
|
|
386
|
+
)
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
if (!trigger.event || typeof trigger.event !== "string") {
|
|
390
|
+
errors.push(
|
|
391
|
+
new ContractValidationError(
|
|
392
|
+
`Trigger ${i} requires an event pattern`,
|
|
393
|
+
`triggers[${i}].event`
|
|
394
|
+
)
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (!Array.isArray(contract.steps) || contract.steps.length === 0) {
|
|
400
|
+
errors.push(
|
|
401
|
+
new ContractValidationError("Contract must have at least one step", "steps")
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
405
|
+
for (const step of contract.steps) {
|
|
406
|
+
if (!step.id || typeof step.id !== "string") {
|
|
407
|
+
errors.push(new ContractValidationError("Step id is required", "steps[].id"));
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (stepIds.has(step.id)) {
|
|
411
|
+
errors.push(
|
|
412
|
+
new ContractValidationError(`Duplicate step id: ${step.id}`, `steps.${step.id}`)
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
stepIds.add(step.id);
|
|
416
|
+
const stepErrors = validateStep(step);
|
|
417
|
+
errors.push(...stepErrors);
|
|
418
|
+
}
|
|
419
|
+
if (!contract.entryStepId) {
|
|
420
|
+
errors.push(
|
|
421
|
+
new ContractValidationError("entryStepId is required", "entryStepId")
|
|
422
|
+
);
|
|
423
|
+
} else if (!stepIds.has(contract.entryStepId)) {
|
|
424
|
+
errors.push(
|
|
425
|
+
new ContractValidationError(
|
|
426
|
+
`entryStepId not found in steps: ${contract.entryStepId}`,
|
|
427
|
+
"entryStepId"
|
|
428
|
+
)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
for (const step of contract.steps) {
|
|
432
|
+
if (step.next != null && !stepIds.has(step.next)) {
|
|
433
|
+
errors.push(
|
|
434
|
+
new ContractValidationError(
|
|
435
|
+
`Step ${step.id} has invalid next: ${step.next}`,
|
|
436
|
+
`steps.${step.id}.next`
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (step.kind === "branch") {
|
|
441
|
+
const branchStep = step;
|
|
442
|
+
for (let i = 0; i < branchStep.branches.length; i++) {
|
|
443
|
+
const branch = branchStep.branches[i];
|
|
444
|
+
if (!stepIds.has(branch.next)) {
|
|
445
|
+
errors.push(
|
|
446
|
+
new ContractValidationError(
|
|
447
|
+
`Branch step ${step.id} has invalid branch next: ${branch.next}`,
|
|
448
|
+
`steps.${step.id}.branches[${i}].next`
|
|
449
|
+
)
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!stepIds.has(branchStep.defaultNext)) {
|
|
454
|
+
errors.push(
|
|
455
|
+
new ContractValidationError(
|
|
456
|
+
`Branch step ${step.id} has invalid defaultNext: ${branchStep.defaultNext}`,
|
|
457
|
+
`steps.${step.id}.defaultNext`
|
|
458
|
+
)
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (step.kind === "wait") {
|
|
463
|
+
const waitStep = step;
|
|
464
|
+
if (waitStep.nextOnTimeout != null && !stepIds.has(waitStep.nextOnTimeout)) {
|
|
465
|
+
errors.push(
|
|
466
|
+
new ContractValidationError(
|
|
467
|
+
`Wait step ${step.id} has invalid nextOnTimeout: ${waitStep.nextOnTimeout}`,
|
|
468
|
+
`steps.${step.id}.nextOnTimeout`
|
|
469
|
+
)
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (!contract.limits) {
|
|
475
|
+
errors.push(new ContractValidationError("Contract limits are required", "limits"));
|
|
476
|
+
} else {
|
|
477
|
+
if (typeof contract.limits.maxRunMs !== "number" || contract.limits.maxRunMs <= 0) {
|
|
478
|
+
errors.push(
|
|
479
|
+
new ContractValidationError(
|
|
480
|
+
"limits.maxRunMs must be a positive number",
|
|
481
|
+
"limits.maxRunMs"
|
|
482
|
+
)
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
if (typeof contract.limits.maxSteps !== "number" || contract.limits.maxSteps <= 0) {
|
|
486
|
+
errors.push(
|
|
487
|
+
new ContractValidationError(
|
|
488
|
+
"limits.maxSteps must be a positive number",
|
|
489
|
+
"limits.maxSteps"
|
|
490
|
+
)
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (!Array.isArray(contract.permissions)) {
|
|
495
|
+
errors.push(
|
|
496
|
+
new ContractValidationError("Contract permissions must be an array", "permissions")
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
return errors;
|
|
500
|
+
}
|
|
501
|
+
function validateStep(step, _stepIds) {
|
|
502
|
+
const errors = [];
|
|
503
|
+
if (!step.name || typeof step.name !== "string") {
|
|
504
|
+
errors.push(
|
|
505
|
+
new ContractValidationError(`Step ${step.id} requires a name`, `steps.${step.id}.name`)
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if (step.if) {
|
|
509
|
+
try {
|
|
510
|
+
validateCondition(step.if);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
errors.push(
|
|
513
|
+
new ContractValidationError(
|
|
514
|
+
`Step ${step.id} has invalid if condition: ${err.message}`,
|
|
515
|
+
`steps.${step.id}.if`
|
|
516
|
+
)
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
switch (step.kind) {
|
|
521
|
+
case "task": {
|
|
522
|
+
const taskStep = step;
|
|
523
|
+
if (!taskStep.task || typeof taskStep.task !== "string") {
|
|
524
|
+
errors.push(
|
|
525
|
+
new ContractValidationError(
|
|
526
|
+
`Task step ${step.id} requires a task name`,
|
|
527
|
+
`steps.${step.id}.task`
|
|
528
|
+
)
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (!taskStep.input || typeof taskStep.input !== "object") {
|
|
532
|
+
errors.push(
|
|
533
|
+
new ContractValidationError(
|
|
534
|
+
`Task step ${step.id} requires an input object`,
|
|
535
|
+
`steps.${step.id}.input`
|
|
536
|
+
)
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
case "llm": {
|
|
542
|
+
const llmStep = step;
|
|
543
|
+
if (!llmStep.prompt || typeof llmStep.prompt !== "object") {
|
|
544
|
+
errors.push(
|
|
545
|
+
new ContractValidationError(
|
|
546
|
+
`LLM step ${step.id} requires a prompt object`,
|
|
547
|
+
`steps.${step.id}.prompt`
|
|
548
|
+
)
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
case "emit": {
|
|
554
|
+
const emitStep = step;
|
|
555
|
+
if (!emitStep.event || typeof emitStep.event !== "string") {
|
|
556
|
+
errors.push(
|
|
557
|
+
new ContractValidationError(
|
|
558
|
+
`Emit step ${step.id} requires an event name`,
|
|
559
|
+
`steps.${step.id}.event`
|
|
560
|
+
)
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
if (!emitStep.payload || typeof emitStep.payload !== "object") {
|
|
564
|
+
errors.push(
|
|
565
|
+
new ContractValidationError(
|
|
566
|
+
`Emit step ${step.id} requires a payload object`,
|
|
567
|
+
`steps.${step.id}.payload`
|
|
568
|
+
)
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
case "wait": {
|
|
574
|
+
const waitStep = step;
|
|
575
|
+
if (!waitStep.event || typeof waitStep.event !== "string") {
|
|
576
|
+
errors.push(
|
|
577
|
+
new ContractValidationError(
|
|
578
|
+
`Wait step ${step.id} requires an event pattern`,
|
|
579
|
+
`steps.${step.id}.event`
|
|
580
|
+
)
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
case "branch": {
|
|
586
|
+
const branchStep = step;
|
|
587
|
+
if (!Array.isArray(branchStep.branches) || branchStep.branches.length === 0) {
|
|
588
|
+
errors.push(
|
|
589
|
+
new ContractValidationError(
|
|
590
|
+
`Branch step ${step.id} requires at least one branch`,
|
|
591
|
+
`steps.${step.id}.branches`
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
} else {
|
|
595
|
+
for (let i = 0; i < branchStep.branches.length; i++) {
|
|
596
|
+
const branch = branchStep.branches[i];
|
|
597
|
+
if (!branch.when) {
|
|
598
|
+
errors.push(
|
|
599
|
+
new ContractValidationError(
|
|
600
|
+
`Branch ${i} in step ${step.id} requires a when condition`,
|
|
601
|
+
`steps.${step.id}.branches[${i}].when`
|
|
602
|
+
)
|
|
603
|
+
);
|
|
604
|
+
} else {
|
|
605
|
+
try {
|
|
606
|
+
validateCondition(branch.when);
|
|
607
|
+
} catch (err) {
|
|
608
|
+
errors.push(
|
|
609
|
+
new ContractValidationError(
|
|
610
|
+
`Branch ${i} in step ${step.id} has invalid when condition: ${err.message}`,
|
|
611
|
+
`steps.${step.id}.branches[${i}].when`
|
|
612
|
+
)
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (!branchStep.defaultNext || typeof branchStep.defaultNext !== "string") {
|
|
619
|
+
errors.push(
|
|
620
|
+
new ContractValidationError(
|
|
621
|
+
`Branch step ${step.id} requires a defaultNext`,
|
|
622
|
+
`steps.${step.id}.defaultNext`
|
|
623
|
+
)
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
case "transform": {
|
|
629
|
+
const transformStep = step;
|
|
630
|
+
if (!transformStep.transforms || typeof transformStep.transforms !== "object") {
|
|
631
|
+
errors.push(
|
|
632
|
+
new ContractValidationError(
|
|
633
|
+
`Transform step ${step.id} requires a transforms object`,
|
|
634
|
+
`steps.${step.id}.transforms`
|
|
635
|
+
)
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
case "approval": {
|
|
641
|
+
const approvalStep = step;
|
|
642
|
+
if (!approvalStep.prompt || typeof approvalStep.prompt !== "string") {
|
|
643
|
+
errors.push(
|
|
644
|
+
new ContractValidationError(
|
|
645
|
+
`Approval step ${step.id} requires a prompt`,
|
|
646
|
+
`steps.${step.id}.prompt`
|
|
647
|
+
)
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (!approvalStep.nextOnApproved || typeof approvalStep.nextOnApproved !== "string") {
|
|
651
|
+
errors.push(
|
|
652
|
+
new ContractValidationError(
|
|
653
|
+
`Approval step ${step.id} requires nextOnApproved`,
|
|
654
|
+
`steps.${step.id}.nextOnApproved`
|
|
655
|
+
)
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
default: {
|
|
661
|
+
const unknownStep = step;
|
|
662
|
+
errors.push(
|
|
663
|
+
new ContractValidationError(
|
|
664
|
+
`Step ${unknownStep.id} has unknown kind: ${unknownStep.kind}`,
|
|
665
|
+
`steps.${unknownStep.id}.kind`
|
|
666
|
+
)
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return errors;
|
|
671
|
+
}
|
|
672
|
+
function hasPermission(contract, kind, scope) {
|
|
673
|
+
return contract.permissions.some((p) => {
|
|
674
|
+
if (p.kind !== kind) return false;
|
|
675
|
+
if (!scope) return true;
|
|
676
|
+
switch (p.kind) {
|
|
677
|
+
case "event:emit":
|
|
678
|
+
case "event:read":
|
|
679
|
+
return matchPattern(p.pattern, scope);
|
|
680
|
+
case "storage:read":
|
|
681
|
+
case "storage:write":
|
|
682
|
+
case "repo:read":
|
|
683
|
+
case "repo:write":
|
|
684
|
+
return matchPattern(p.scope, scope);
|
|
685
|
+
case "http:request":
|
|
686
|
+
return p.domains.some((d) => matchPattern(d, scope));
|
|
687
|
+
case "secrets:read":
|
|
688
|
+
return p.keys.includes(scope);
|
|
689
|
+
case "llm:invoke":
|
|
690
|
+
return !p.models || p.models.includes(scope);
|
|
691
|
+
default:
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
function matchPattern(pattern, value) {
|
|
697
|
+
if (pattern === "*") return true;
|
|
698
|
+
if (pattern.endsWith(".*")) {
|
|
699
|
+
const prefix = pattern.slice(0, -2);
|
|
700
|
+
return value === prefix || value.startsWith(prefix + ".");
|
|
701
|
+
}
|
|
702
|
+
if (pattern.endsWith("*")) {
|
|
703
|
+
const prefix = pattern.slice(0, -1);
|
|
704
|
+
return value.startsWith(prefix);
|
|
705
|
+
}
|
|
706
|
+
return pattern === value;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/examples/ticket-triage.ts
|
|
710
|
+
var ticketTriageContract = {
|
|
711
|
+
schemaVersion: "codmir.contract.v1",
|
|
712
|
+
id: "codmir.ticket-triage",
|
|
713
|
+
version: "1.0.0",
|
|
714
|
+
title: "Ticket Triage Agent",
|
|
715
|
+
description: "Automatically analyzes and triages incoming support tickets using AI classification",
|
|
716
|
+
triggers: [
|
|
717
|
+
{
|
|
718
|
+
type: "event",
|
|
719
|
+
event: "ticket.created"
|
|
720
|
+
}
|
|
721
|
+
],
|
|
722
|
+
permissions: [
|
|
723
|
+
{ kind: "event:emit", pattern: "ticket.*" },
|
|
724
|
+
{ kind: "event:emit", pattern: "agent.*" },
|
|
725
|
+
{ kind: "event:read", pattern: "ticket.*" },
|
|
726
|
+
{ kind: "llm:invoke", models: ["gpt-5-mini-2025-08-07", "gpt-5"] }
|
|
727
|
+
],
|
|
728
|
+
limits: {
|
|
729
|
+
maxRunMs: 6e4,
|
|
730
|
+
// 1 minute
|
|
731
|
+
maxSteps: 10,
|
|
732
|
+
maxLlmCalls: 3,
|
|
733
|
+
maxTokens: 5e3
|
|
734
|
+
},
|
|
735
|
+
entryStepId: "analyze",
|
|
736
|
+
steps: [
|
|
737
|
+
// Step 1: Analyze ticket with LLM
|
|
738
|
+
{
|
|
739
|
+
id: "analyze",
|
|
740
|
+
name: "Analyze Ticket",
|
|
741
|
+
kind: "llm",
|
|
742
|
+
description: "Use AI to analyze ticket content and extract key information",
|
|
743
|
+
model: "gpt-5-mini-2025-08-07",
|
|
744
|
+
prompt: {
|
|
745
|
+
system: "You are a support ticket analyst. Analyze the ticket and extract: priority (low/medium/high/urgent), category (bug/feature/question/other), sentiment (positive/neutral/negative), and a brief summary.",
|
|
746
|
+
user: "Ticket Title: {{trigger.title}}\n\nTicket Body:\n{{trigger.body}}"
|
|
747
|
+
},
|
|
748
|
+
saveAs: "steps.analysis",
|
|
749
|
+
temperature: 0.3,
|
|
750
|
+
maxTokens: 500,
|
|
751
|
+
next: "classify"
|
|
752
|
+
},
|
|
753
|
+
// Step 2: Transform analysis into structured data
|
|
754
|
+
{
|
|
755
|
+
id: "classify",
|
|
756
|
+
name: "Classify Ticket",
|
|
757
|
+
kind: "transform",
|
|
758
|
+
description: "Structure the analysis results",
|
|
759
|
+
transforms: {
|
|
760
|
+
"classification.priority": "{{steps.analysis.priority}}",
|
|
761
|
+
"classification.category": "{{steps.analysis.category}}",
|
|
762
|
+
"classification.sentiment": "{{steps.analysis.sentiment}}",
|
|
763
|
+
"classification.summary": "{{steps.analysis.summary}}"
|
|
764
|
+
},
|
|
765
|
+
next: "check-urgent"
|
|
766
|
+
},
|
|
767
|
+
// Step 3: Branch on urgency
|
|
768
|
+
{
|
|
769
|
+
id: "check-urgent",
|
|
770
|
+
name: "Check Urgency",
|
|
771
|
+
kind: "branch",
|
|
772
|
+
description: "Route urgent tickets to fast path",
|
|
773
|
+
branches: [
|
|
774
|
+
{
|
|
775
|
+
when: { op: "eq", path: "classification.priority", value: "urgent" },
|
|
776
|
+
next: "escalate"
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
when: { op: "eq", path: "classification.priority", value: "high" },
|
|
780
|
+
next: "assign-senior"
|
|
781
|
+
}
|
|
782
|
+
],
|
|
783
|
+
defaultNext: "assign-team"
|
|
784
|
+
},
|
|
785
|
+
// Step 4a: Escalate urgent tickets
|
|
786
|
+
{
|
|
787
|
+
id: "escalate",
|
|
788
|
+
name: "Escalate Urgent",
|
|
789
|
+
kind: "emit",
|
|
790
|
+
description: "Emit escalation event for urgent tickets",
|
|
791
|
+
event: "ticket.escalated",
|
|
792
|
+
payload: {
|
|
793
|
+
ticketId: "{{trigger.id}}",
|
|
794
|
+
priority: "urgent",
|
|
795
|
+
reason: "Auto-escalated by triage agent",
|
|
796
|
+
analysis: "{{classification}}"
|
|
797
|
+
},
|
|
798
|
+
next: "notify-complete"
|
|
799
|
+
},
|
|
800
|
+
// Step 4b: Assign to senior team
|
|
801
|
+
{
|
|
802
|
+
id: "assign-senior",
|
|
803
|
+
name: "Assign to Senior",
|
|
804
|
+
kind: "task",
|
|
805
|
+
description: "Assign high-priority ticket to senior team",
|
|
806
|
+
task: "assign-ticket",
|
|
807
|
+
input: {
|
|
808
|
+
ticketId: "{{trigger.id}}",
|
|
809
|
+
team: "senior-support",
|
|
810
|
+
priority: "high",
|
|
811
|
+
notes: "{{classification.summary}}"
|
|
812
|
+
},
|
|
813
|
+
saveAs: "steps.assignment",
|
|
814
|
+
next: "notify-complete"
|
|
815
|
+
},
|
|
816
|
+
// Step 4c: Assign to regular team
|
|
817
|
+
{
|
|
818
|
+
id: "assign-team",
|
|
819
|
+
name: "Assign to Team",
|
|
820
|
+
kind: "task",
|
|
821
|
+
description: "Assign ticket to appropriate team based on category",
|
|
822
|
+
task: "assign-ticket",
|
|
823
|
+
input: {
|
|
824
|
+
ticketId: "{{trigger.id}}",
|
|
825
|
+
team: "{{classification.category}}-team",
|
|
826
|
+
priority: "{{classification.priority}}",
|
|
827
|
+
notes: "{{classification.summary}}"
|
|
828
|
+
},
|
|
829
|
+
saveAs: "steps.assignment",
|
|
830
|
+
next: "notify-complete"
|
|
831
|
+
},
|
|
832
|
+
// Step 5: Emit completion event
|
|
833
|
+
{
|
|
834
|
+
id: "notify-complete",
|
|
835
|
+
name: "Notify Complete",
|
|
836
|
+
kind: "emit",
|
|
837
|
+
description: "Emit triage completion event",
|
|
838
|
+
event: "ticket.triaged",
|
|
839
|
+
payload: {
|
|
840
|
+
ticketId: "{{trigger.id}}",
|
|
841
|
+
classification: "{{classification}}",
|
|
842
|
+
assignment: "{{steps.assignment}}"
|
|
843
|
+
},
|
|
844
|
+
next: null
|
|
845
|
+
}
|
|
846
|
+
],
|
|
847
|
+
outputs: {
|
|
848
|
+
onCompleted: [
|
|
849
|
+
{
|
|
850
|
+
event: "agent.task.completed",
|
|
851
|
+
payload: {
|
|
852
|
+
agent: "ticket-triage",
|
|
853
|
+
ticketId: "{{trigger.id}}",
|
|
854
|
+
result: "{{classification}}"
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
],
|
|
858
|
+
onFailed: [
|
|
859
|
+
{
|
|
860
|
+
event: "agent.task.failed",
|
|
861
|
+
payload: {
|
|
862
|
+
agent: "ticket-triage",
|
|
863
|
+
ticketId: "{{trigger.id}}"
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
]
|
|
867
|
+
},
|
|
868
|
+
tags: ["support", "triage", "ai", "automation"],
|
|
869
|
+
author: "codmir-team"
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
// src/examples/code-review.ts
|
|
873
|
+
var codeReviewContract = {
|
|
874
|
+
schemaVersion: "codmir.contract.v1",
|
|
875
|
+
id: "codmir.code-review",
|
|
876
|
+
version: "1.0.0",
|
|
877
|
+
title: "Code Review Agent",
|
|
878
|
+
description: "Automatically reviews pull requests, checks code quality, and suggests improvements",
|
|
879
|
+
triggers: [
|
|
880
|
+
{
|
|
881
|
+
type: "event",
|
|
882
|
+
event: "github.pull_request.opened"
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
type: "event",
|
|
886
|
+
event: "github.pull_request.synchronize"
|
|
887
|
+
}
|
|
888
|
+
],
|
|
889
|
+
permissions: [
|
|
890
|
+
{ kind: "event:emit", pattern: "github.*" },
|
|
891
|
+
{ kind: "event:emit", pattern: "review.*" },
|
|
892
|
+
{ kind: "event:read", pattern: "github.*" },
|
|
893
|
+
{ kind: "repo:read", scope: "*" },
|
|
894
|
+
{ kind: "llm:invoke", models: ["gpt-5", "claude-3-sonnet"] },
|
|
895
|
+
{ kind: "http:request", domains: ["api.github.com"] }
|
|
896
|
+
],
|
|
897
|
+
limits: {
|
|
898
|
+
maxRunMs: 3e5,
|
|
899
|
+
// 5 minutes
|
|
900
|
+
maxSteps: 20,
|
|
901
|
+
maxLlmCalls: 10,
|
|
902
|
+
maxTokens: 5e4
|
|
903
|
+
},
|
|
904
|
+
entryStepId: "fetch-diff",
|
|
905
|
+
steps: [
|
|
906
|
+
// Step 1: Fetch PR diff
|
|
907
|
+
{
|
|
908
|
+
id: "fetch-diff",
|
|
909
|
+
name: "Fetch PR Diff",
|
|
910
|
+
kind: "task",
|
|
911
|
+
description: "Fetch the pull request diff from GitHub",
|
|
912
|
+
task: "github-fetch-diff",
|
|
913
|
+
input: {
|
|
914
|
+
owner: "{{trigger.repository.owner}}",
|
|
915
|
+
repo: "{{trigger.repository.name}}",
|
|
916
|
+
pullNumber: "{{trigger.pull_request.number}}"
|
|
917
|
+
},
|
|
918
|
+
saveAs: "steps.diff",
|
|
919
|
+
timeout: { ms: 3e4 },
|
|
920
|
+
retry: { maxAttempts: 2, backoffMs: 1e3 },
|
|
921
|
+
next: "check-size"
|
|
922
|
+
},
|
|
923
|
+
// Step 2: Check PR size
|
|
924
|
+
{
|
|
925
|
+
id: "check-size",
|
|
926
|
+
name: "Check PR Size",
|
|
927
|
+
kind: "branch",
|
|
928
|
+
description: "Route large PRs differently",
|
|
929
|
+
branches: [
|
|
930
|
+
{
|
|
931
|
+
when: { op: "gt", path: "steps.diff.linesChanged", value: 1e3 },
|
|
932
|
+
next: "large-pr-warning"
|
|
933
|
+
}
|
|
934
|
+
],
|
|
935
|
+
defaultNext: "analyze-code"
|
|
936
|
+
},
|
|
937
|
+
// Step 2a: Warn about large PR
|
|
938
|
+
{
|
|
939
|
+
id: "large-pr-warning",
|
|
940
|
+
name: "Large PR Warning",
|
|
941
|
+
kind: "emit",
|
|
942
|
+
description: "Emit warning about large PR size",
|
|
943
|
+
event: "review.warning",
|
|
944
|
+
payload: {
|
|
945
|
+
pullNumber: "{{trigger.pull_request.number}}",
|
|
946
|
+
reason: "PR is too large for effective review",
|
|
947
|
+
linesChanged: "{{steps.diff.linesChanged}}",
|
|
948
|
+
recommendation: "Consider breaking into smaller PRs"
|
|
949
|
+
},
|
|
950
|
+
next: "analyze-code"
|
|
951
|
+
},
|
|
952
|
+
// Step 3: Analyze code with LLM
|
|
953
|
+
{
|
|
954
|
+
id: "analyze-code",
|
|
955
|
+
name: "Analyze Code",
|
|
956
|
+
kind: "llm",
|
|
957
|
+
description: "Use AI to analyze code changes for issues",
|
|
958
|
+
model: "gpt-5",
|
|
959
|
+
systemPrompt: `You are an expert code reviewer. Analyze the diff and identify:
|
|
960
|
+
1. Potential bugs or logic errors
|
|
961
|
+
2. Security vulnerabilities
|
|
962
|
+
3. Performance issues
|
|
963
|
+
4. Code style violations
|
|
964
|
+
5. Missing tests
|
|
965
|
+
6. Documentation gaps
|
|
966
|
+
|
|
967
|
+
For each issue, provide:
|
|
968
|
+
- File and line number
|
|
969
|
+
- Severity (critical/warning/info)
|
|
970
|
+
- Description
|
|
971
|
+
- Suggested fix
|
|
972
|
+
|
|
973
|
+
Respond in JSON format with an "issues" array.`,
|
|
974
|
+
prompt: {
|
|
975
|
+
diff: "{{steps.diff.content}}",
|
|
976
|
+
prTitle: "{{trigger.pull_request.title}}",
|
|
977
|
+
prDescription: "{{trigger.pull_request.body}}"
|
|
978
|
+
},
|
|
979
|
+
saveAs: "steps.analysis",
|
|
980
|
+
temperature: 0.2,
|
|
981
|
+
maxTokens: 4e3,
|
|
982
|
+
next: "check-critical"
|
|
983
|
+
},
|
|
984
|
+
// Step 4: Check for critical issues
|
|
985
|
+
{
|
|
986
|
+
id: "check-critical",
|
|
987
|
+
name: "Check Critical Issues",
|
|
988
|
+
kind: "branch",
|
|
989
|
+
description: "Route critical issues to blocking review",
|
|
990
|
+
branches: [
|
|
991
|
+
{
|
|
992
|
+
when: {
|
|
993
|
+
op: "exists",
|
|
994
|
+
path: "steps.analysis.issues"
|
|
995
|
+
},
|
|
996
|
+
next: "filter-critical"
|
|
997
|
+
}
|
|
998
|
+
],
|
|
999
|
+
defaultNext: "approve"
|
|
1000
|
+
},
|
|
1001
|
+
// Step 4a: Filter critical issues
|
|
1002
|
+
{
|
|
1003
|
+
id: "filter-critical",
|
|
1004
|
+
name: "Filter Critical",
|
|
1005
|
+
kind: "transform",
|
|
1006
|
+
description: "Extract critical issues for blocking",
|
|
1007
|
+
transforms: {
|
|
1008
|
+
"review.hasCritical": true,
|
|
1009
|
+
"review.issues": "{{steps.analysis.issues}}"
|
|
1010
|
+
},
|
|
1011
|
+
next: "post-comments"
|
|
1012
|
+
},
|
|
1013
|
+
// Step 5: Post review comments
|
|
1014
|
+
{
|
|
1015
|
+
id: "post-comments",
|
|
1016
|
+
name: "Post Comments",
|
|
1017
|
+
kind: "task",
|
|
1018
|
+
description: "Post review comments on GitHub",
|
|
1019
|
+
task: "github-post-review",
|
|
1020
|
+
input: {
|
|
1021
|
+
owner: "{{trigger.repository.owner}}",
|
|
1022
|
+
repo: "{{trigger.repository.name}}",
|
|
1023
|
+
pullNumber: "{{trigger.pull_request.number}}",
|
|
1024
|
+
body: "## AI Code Review\n\nI found {{review.issues.length}} issue(s) in this PR.",
|
|
1025
|
+
comments: "{{review.issues}}",
|
|
1026
|
+
event: "REQUEST_CHANGES"
|
|
1027
|
+
},
|
|
1028
|
+
saveAs: "steps.reviewPost",
|
|
1029
|
+
next: "wait-author-response"
|
|
1030
|
+
},
|
|
1031
|
+
// Step 6: Wait for author response
|
|
1032
|
+
{
|
|
1033
|
+
id: "wait-author-response",
|
|
1034
|
+
name: "Wait for Response",
|
|
1035
|
+
kind: "wait",
|
|
1036
|
+
description: "Wait for author to address issues or respond",
|
|
1037
|
+
event: "github.pull_request.synchronize",
|
|
1038
|
+
saveEventAs: "events.authorResponse",
|
|
1039
|
+
timeout: { ms: 864e5 },
|
|
1040
|
+
// 24 hours
|
|
1041
|
+
next: "re-analyze",
|
|
1042
|
+
nextOnTimeout: "timeout-reminder"
|
|
1043
|
+
},
|
|
1044
|
+
// Step 6a: Re-analyze after author changes
|
|
1045
|
+
{
|
|
1046
|
+
id: "re-analyze",
|
|
1047
|
+
name: "Re-analyze",
|
|
1048
|
+
kind: "task",
|
|
1049
|
+
description: "Re-fetch and re-analyze after author changes",
|
|
1050
|
+
task: "github-fetch-diff",
|
|
1051
|
+
input: {
|
|
1052
|
+
owner: "{{trigger.repository.owner}}",
|
|
1053
|
+
repo: "{{trigger.repository.name}}",
|
|
1054
|
+
pullNumber: "{{trigger.pull_request.number}}"
|
|
1055
|
+
},
|
|
1056
|
+
saveAs: "steps.newDiff",
|
|
1057
|
+
next: "analyze-code"
|
|
1058
|
+
},
|
|
1059
|
+
// Step 6b: Timeout reminder
|
|
1060
|
+
{
|
|
1061
|
+
id: "timeout-reminder",
|
|
1062
|
+
name: "Timeout Reminder",
|
|
1063
|
+
kind: "emit",
|
|
1064
|
+
description: "Remind author about pending review",
|
|
1065
|
+
event: "review.reminder",
|
|
1066
|
+
payload: {
|
|
1067
|
+
pullNumber: "{{trigger.pull_request.number}}",
|
|
1068
|
+
author: "{{trigger.pull_request.user.login}}",
|
|
1069
|
+
message: "Code review feedback is still pending"
|
|
1070
|
+
},
|
|
1071
|
+
next: null
|
|
1072
|
+
},
|
|
1073
|
+
// Step 7: Approve PR
|
|
1074
|
+
{
|
|
1075
|
+
id: "approve",
|
|
1076
|
+
name: "Approve PR",
|
|
1077
|
+
kind: "task",
|
|
1078
|
+
description: "Approve the PR with positive review",
|
|
1079
|
+
task: "github-post-review",
|
|
1080
|
+
input: {
|
|
1081
|
+
owner: "{{trigger.repository.owner}}",
|
|
1082
|
+
repo: "{{trigger.repository.name}}",
|
|
1083
|
+
pullNumber: "{{trigger.pull_request.number}}",
|
|
1084
|
+
body: "## AI Code Review\n\n\u2705 LGTM! No issues found.",
|
|
1085
|
+
event: "APPROVE"
|
|
1086
|
+
},
|
|
1087
|
+
saveAs: "steps.approval",
|
|
1088
|
+
next: "complete"
|
|
1089
|
+
},
|
|
1090
|
+
// Step 8: Complete
|
|
1091
|
+
{
|
|
1092
|
+
id: "complete",
|
|
1093
|
+
name: "Complete",
|
|
1094
|
+
kind: "emit",
|
|
1095
|
+
description: "Emit review completion event",
|
|
1096
|
+
event: "review.completed",
|
|
1097
|
+
payload: {
|
|
1098
|
+
pullNumber: "{{trigger.pull_request.number}}",
|
|
1099
|
+
repository: "{{trigger.repository.full_name}}",
|
|
1100
|
+
result: "approved"
|
|
1101
|
+
},
|
|
1102
|
+
next: null
|
|
1103
|
+
}
|
|
1104
|
+
],
|
|
1105
|
+
outputs: {
|
|
1106
|
+
onCompleted: [
|
|
1107
|
+
{
|
|
1108
|
+
event: "agent.task.completed",
|
|
1109
|
+
payload: {
|
|
1110
|
+
agent: "code-review",
|
|
1111
|
+
repository: "{{trigger.repository.full_name}}",
|
|
1112
|
+
pullNumber: "{{trigger.pull_request.number}}"
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
],
|
|
1116
|
+
onFailed: [
|
|
1117
|
+
{
|
|
1118
|
+
event: "agent.task.failed",
|
|
1119
|
+
payload: {
|
|
1120
|
+
agent: "code-review",
|
|
1121
|
+
repository: "{{trigger.repository.full_name}}",
|
|
1122
|
+
pullNumber: "{{trigger.pull_request.number}}"
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
]
|
|
1126
|
+
},
|
|
1127
|
+
tags: ["github", "code-review", "ai", "automation"],
|
|
1128
|
+
author: "codmir-team"
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
// src/examples/pr-analysis.ts
|
|
1132
|
+
var prAnalysisContract = {
|
|
1133
|
+
schemaVersion: "codmir.contract.v1",
|
|
1134
|
+
id: "codmir.pr-analysis",
|
|
1135
|
+
version: "1.0.0",
|
|
1136
|
+
title: "PR Analysis Agent",
|
|
1137
|
+
description: "Automatically analyzes pull requests for code quality, secrets, and best practices when triggered by GitHub App webhook",
|
|
1138
|
+
triggers: [
|
|
1139
|
+
{
|
|
1140
|
+
type: "event",
|
|
1141
|
+
event: "github.pull_request.opened"
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
type: "event",
|
|
1145
|
+
event: "github.pull_request.synchronize"
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
type: "event",
|
|
1149
|
+
event: "github.pull_request.reopened"
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
type: "event",
|
|
1153
|
+
event: "github.pull_request.ready_for_review"
|
|
1154
|
+
}
|
|
1155
|
+
],
|
|
1156
|
+
permissions: [
|
|
1157
|
+
{ kind: "event:emit", pattern: "github.*" },
|
|
1158
|
+
{ kind: "event:emit", pattern: "pr.*" },
|
|
1159
|
+
{ kind: "event:read", pattern: "github.*" },
|
|
1160
|
+
{ kind: "repo:read", scope: "*" },
|
|
1161
|
+
{ kind: "http:request", domains: ["api.github.com"] },
|
|
1162
|
+
{ kind: "secrets:read", keys: ["GITHUB_TOKEN", "GITHUB_APP_*"] }
|
|
1163
|
+
],
|
|
1164
|
+
limits: {
|
|
1165
|
+
maxRunMs: 12e4,
|
|
1166
|
+
// 2 minutes
|
|
1167
|
+
maxSteps: 10,
|
|
1168
|
+
maxLlmCalls: 0,
|
|
1169
|
+
// No LLM calls needed for basic analysis
|
|
1170
|
+
maxTokens: 0
|
|
1171
|
+
},
|
|
1172
|
+
entryStepId: "run-analysis",
|
|
1173
|
+
steps: [
|
|
1174
|
+
// Step 1: Run full PR analysis (fetch + analyze + post comment)
|
|
1175
|
+
{
|
|
1176
|
+
id: "run-analysis",
|
|
1177
|
+
name: "Run PR Analysis",
|
|
1178
|
+
kind: "task",
|
|
1179
|
+
description: "Fetch PR info, analyze diff, and post analysis comment",
|
|
1180
|
+
task: "pr-run-analysis",
|
|
1181
|
+
input: {
|
|
1182
|
+
owner: "{{trigger.repository.owner.login}}",
|
|
1183
|
+
repo: "{{trigger.repository.name}}",
|
|
1184
|
+
pullNumber: "{{trigger.pull_request.number}}",
|
|
1185
|
+
installationId: "{{trigger.installation.id}}"
|
|
1186
|
+
},
|
|
1187
|
+
saveAs: "steps.analysis",
|
|
1188
|
+
timeout: { ms: 6e4 },
|
|
1189
|
+
retry: { maxAttempts: 2, backoffMs: 2e3 },
|
|
1190
|
+
next: "check-secrets"
|
|
1191
|
+
},
|
|
1192
|
+
// Step 2: Check for critical issues (secrets detected)
|
|
1193
|
+
{
|
|
1194
|
+
id: "check-secrets",
|
|
1195
|
+
name: "Check for Secrets",
|
|
1196
|
+
kind: "branch",
|
|
1197
|
+
description: "Check if secrets were detected in the PR",
|
|
1198
|
+
branches: [
|
|
1199
|
+
{
|
|
1200
|
+
when: {
|
|
1201
|
+
op: "gt",
|
|
1202
|
+
path: "steps.analysis.analysis.secrets.length",
|
|
1203
|
+
value: 0
|
|
1204
|
+
},
|
|
1205
|
+
next: "emit-secret-alert"
|
|
1206
|
+
}
|
|
1207
|
+
],
|
|
1208
|
+
defaultNext: "emit-completed"
|
|
1209
|
+
},
|
|
1210
|
+
// Step 2a: Emit secret alert
|
|
1211
|
+
{
|
|
1212
|
+
id: "emit-secret-alert",
|
|
1213
|
+
name: "Emit Secret Alert",
|
|
1214
|
+
kind: "emit",
|
|
1215
|
+
description: "Emit alert about potential secrets detected",
|
|
1216
|
+
event: "pr.secrets.detected",
|
|
1217
|
+
payload: {
|
|
1218
|
+
pullNumber: "{{trigger.pull_request.number}}",
|
|
1219
|
+
repository: "{{trigger.repository.full_name}}",
|
|
1220
|
+
secretCount: "{{steps.analysis.analysis.secrets.length}}",
|
|
1221
|
+
secrets: "{{steps.analysis.analysis.secrets}}",
|
|
1222
|
+
severity: "critical"
|
|
1223
|
+
},
|
|
1224
|
+
next: "emit-completed"
|
|
1225
|
+
},
|
|
1226
|
+
// Step 3: Emit completion event
|
|
1227
|
+
{
|
|
1228
|
+
id: "emit-completed",
|
|
1229
|
+
name: "Emit Completed",
|
|
1230
|
+
kind: "emit",
|
|
1231
|
+
description: "Emit PR analysis completion event",
|
|
1232
|
+
event: "pr.analysis.completed",
|
|
1233
|
+
payload: {
|
|
1234
|
+
pullNumber: "{{trigger.pull_request.number}}",
|
|
1235
|
+
repository: "{{trigger.repository.full_name}}",
|
|
1236
|
+
analysis: "{{steps.analysis.analysis}}",
|
|
1237
|
+
commentPosted: "{{steps.analysis.commentPosted}}",
|
|
1238
|
+
commentId: "{{steps.analysis.commentId}}"
|
|
1239
|
+
},
|
|
1240
|
+
next: null
|
|
1241
|
+
}
|
|
1242
|
+
],
|
|
1243
|
+
outputs: {
|
|
1244
|
+
onCompleted: [
|
|
1245
|
+
{
|
|
1246
|
+
event: "agent.task.completed",
|
|
1247
|
+
payload: {
|
|
1248
|
+
agent: "pr-analysis",
|
|
1249
|
+
repository: "{{trigger.repository.full_name}}",
|
|
1250
|
+
pullNumber: "{{trigger.pull_request.number}}",
|
|
1251
|
+
result: "{{steps.analysis}}"
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
],
|
|
1255
|
+
onFailed: [
|
|
1256
|
+
{
|
|
1257
|
+
event: "agent.task.failed",
|
|
1258
|
+
payload: {
|
|
1259
|
+
agent: "pr-analysis",
|
|
1260
|
+
repository: "{{trigger.repository.full_name}}",
|
|
1261
|
+
pullNumber: "{{trigger.pull_request.number}}"
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
]
|
|
1265
|
+
},
|
|
1266
|
+
tags: ["github", "pr-analysis", "automation", "webhook"],
|
|
1267
|
+
author: "codmir-team"
|
|
1268
|
+
};
|
|
1269
|
+
var prAnalysisWithLlmContract = {
|
|
1270
|
+
schemaVersion: "codmir.contract.v1",
|
|
1271
|
+
id: "codmir.pr-analysis-llm",
|
|
1272
|
+
version: "1.0.0",
|
|
1273
|
+
title: "PR Analysis Agent (with AI Review)",
|
|
1274
|
+
description: "Analyzes pull requests with AI-powered code review for deeper insights",
|
|
1275
|
+
triggers: [
|
|
1276
|
+
{
|
|
1277
|
+
type: "event",
|
|
1278
|
+
event: "github.pull_request.opened"
|
|
1279
|
+
},
|
|
1280
|
+
{
|
|
1281
|
+
type: "event",
|
|
1282
|
+
event: "github.pull_request.synchronize"
|
|
1283
|
+
}
|
|
1284
|
+
],
|
|
1285
|
+
permissions: [
|
|
1286
|
+
{ kind: "event:emit", pattern: "github.*" },
|
|
1287
|
+
{ kind: "event:emit", pattern: "pr.*" },
|
|
1288
|
+
{ kind: "event:read", pattern: "github.*" },
|
|
1289
|
+
{ kind: "repo:read", scope: "*" },
|
|
1290
|
+
{ kind: "http:request", domains: ["api.github.com"] },
|
|
1291
|
+
{ kind: "secrets:read", keys: ["GITHUB_TOKEN", "GITHUB_APP_*"] },
|
|
1292
|
+
{ kind: "llm:invoke", models: ["claude-3-sonnet", "claude-3-haiku"] }
|
|
1293
|
+
],
|
|
1294
|
+
limits: {
|
|
1295
|
+
maxRunMs: 3e5,
|
|
1296
|
+
// 5 minutes
|
|
1297
|
+
maxSteps: 15,
|
|
1298
|
+
maxLlmCalls: 5,
|
|
1299
|
+
maxTokens: 5e4
|
|
1300
|
+
},
|
|
1301
|
+
entryStepId: "fetch-pr",
|
|
1302
|
+
steps: [
|
|
1303
|
+
// Step 1: Fetch PR info
|
|
1304
|
+
{
|
|
1305
|
+
id: "fetch-pr",
|
|
1306
|
+
name: "Fetch PR Info",
|
|
1307
|
+
kind: "task",
|
|
1308
|
+
description: "Fetch pull request metadata from GitHub",
|
|
1309
|
+
task: "pr-fetch-info",
|
|
1310
|
+
input: {
|
|
1311
|
+
owner: "{{trigger.repository.owner.login}}",
|
|
1312
|
+
repo: "{{trigger.repository.name}}",
|
|
1313
|
+
pullNumber: "{{trigger.pull_request.number}}"
|
|
1314
|
+
},
|
|
1315
|
+
saveAs: "steps.pr",
|
|
1316
|
+
timeout: { ms: 15e3 },
|
|
1317
|
+
next: "fetch-files"
|
|
1318
|
+
},
|
|
1319
|
+
// Step 2: Fetch PR files
|
|
1320
|
+
{
|
|
1321
|
+
id: "fetch-files",
|
|
1322
|
+
name: "Fetch PR Files",
|
|
1323
|
+
kind: "task",
|
|
1324
|
+
description: "Fetch changed files with patches",
|
|
1325
|
+
task: "pr-fetch-files",
|
|
1326
|
+
input: {
|
|
1327
|
+
owner: "{{trigger.repository.owner.login}}",
|
|
1328
|
+
repo: "{{trigger.repository.name}}",
|
|
1329
|
+
pullNumber: "{{trigger.pull_request.number}}"
|
|
1330
|
+
},
|
|
1331
|
+
saveAs: "steps.files",
|
|
1332
|
+
timeout: { ms: 3e4 },
|
|
1333
|
+
next: "static-analysis"
|
|
1334
|
+
},
|
|
1335
|
+
// Step 3: Run static analysis
|
|
1336
|
+
{
|
|
1337
|
+
id: "static-analysis",
|
|
1338
|
+
name: "Static Analysis",
|
|
1339
|
+
kind: "task",
|
|
1340
|
+
description: "Run heuristic analysis on PR diff",
|
|
1341
|
+
task: "pr-analyze-diff",
|
|
1342
|
+
input: {
|
|
1343
|
+
pr: "{{steps.pr}}",
|
|
1344
|
+
files: "{{steps.files}}",
|
|
1345
|
+
owner: "{{trigger.repository.owner.login}}",
|
|
1346
|
+
repo: "{{trigger.repository.name}}"
|
|
1347
|
+
},
|
|
1348
|
+
saveAs: "steps.staticAnalysis",
|
|
1349
|
+
timeout: { ms: 3e4 },
|
|
1350
|
+
next: "llm-review"
|
|
1351
|
+
},
|
|
1352
|
+
// Step 4: AI-powered code review
|
|
1353
|
+
{
|
|
1354
|
+
id: "llm-review",
|
|
1355
|
+
name: "AI Code Review",
|
|
1356
|
+
kind: "llm",
|
|
1357
|
+
description: "Use AI to review code changes for issues and improvements",
|
|
1358
|
+
model: "claude-3-sonnet",
|
|
1359
|
+
systemPrompt: `You are an expert code reviewer. Analyze the PR diff and identify:
|
|
1360
|
+
1. Potential bugs or logic errors
|
|
1361
|
+
2. Security vulnerabilities
|
|
1362
|
+
3. Performance issues
|
|
1363
|
+
4. Code style violations
|
|
1364
|
+
5. Missing error handling
|
|
1365
|
+
6. Suggestions for improvement
|
|
1366
|
+
|
|
1367
|
+
Be concise and actionable. Focus on the most important issues.
|
|
1368
|
+
Respond in JSON format with an "issues" array containing objects with:
|
|
1369
|
+
- severity: "critical" | "warning" | "info"
|
|
1370
|
+
- file: string (filename)
|
|
1371
|
+
- line: number (approximate line if known)
|
|
1372
|
+
- issue: string (description)
|
|
1373
|
+
- suggestion: string (how to fix)`,
|
|
1374
|
+
prompt: {
|
|
1375
|
+
prTitle: "{{steps.pr.title}}",
|
|
1376
|
+
prDescription: "{{trigger.pull_request.body}}",
|
|
1377
|
+
staticIssues: "{{steps.staticAnalysis.analysis.issues}}",
|
|
1378
|
+
changedFiles: "{{steps.files}}"
|
|
1379
|
+
},
|
|
1380
|
+
saveAs: "steps.llmReview",
|
|
1381
|
+
temperature: 0.2,
|
|
1382
|
+
maxTokens: 4e3,
|
|
1383
|
+
next: "post-comment"
|
|
1384
|
+
},
|
|
1385
|
+
// Step 5: Post combined analysis comment
|
|
1386
|
+
{
|
|
1387
|
+
id: "post-comment",
|
|
1388
|
+
name: "Post Comment",
|
|
1389
|
+
kind: "task",
|
|
1390
|
+
description: "Post analysis and AI review comment to PR",
|
|
1391
|
+
task: "pr-post-comment",
|
|
1392
|
+
input: {
|
|
1393
|
+
owner: "{{trigger.repository.owner.login}}",
|
|
1394
|
+
repo: "{{trigger.repository.name}}",
|
|
1395
|
+
pullNumber: "{{trigger.pull_request.number}}",
|
|
1396
|
+
markdown: "{{steps.staticAnalysis.markdown}}\n\n### \u{1F916} AI Review\n\n{{steps.llmReview.content}}"
|
|
1397
|
+
},
|
|
1398
|
+
saveAs: "steps.comment",
|
|
1399
|
+
timeout: { ms: 15e3 },
|
|
1400
|
+
next: "emit-completed"
|
|
1401
|
+
},
|
|
1402
|
+
// Step 6: Emit completion
|
|
1403
|
+
{
|
|
1404
|
+
id: "emit-completed",
|
|
1405
|
+
name: "Complete",
|
|
1406
|
+
kind: "emit",
|
|
1407
|
+
description: "Emit completion event",
|
|
1408
|
+
event: "pr.analysis.completed",
|
|
1409
|
+
payload: {
|
|
1410
|
+
pullNumber: "{{trigger.pull_request.number}}",
|
|
1411
|
+
repository: "{{trigger.repository.full_name}}",
|
|
1412
|
+
staticAnalysis: "{{steps.staticAnalysis.analysis}}",
|
|
1413
|
+
llmReview: "{{steps.llmReview}}",
|
|
1414
|
+
commentPosted: true
|
|
1415
|
+
},
|
|
1416
|
+
next: null
|
|
1417
|
+
}
|
|
1418
|
+
],
|
|
1419
|
+
outputs: {
|
|
1420
|
+
onCompleted: [
|
|
1421
|
+
{
|
|
1422
|
+
event: "agent.task.completed",
|
|
1423
|
+
payload: {
|
|
1424
|
+
agent: "pr-analysis-llm",
|
|
1425
|
+
repository: "{{trigger.repository.full_name}}",
|
|
1426
|
+
pullNumber: "{{trigger.pull_request.number}}"
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
],
|
|
1430
|
+
onFailed: [
|
|
1431
|
+
{
|
|
1432
|
+
event: "agent.task.failed",
|
|
1433
|
+
payload: {
|
|
1434
|
+
agent: "pr-analysis-llm",
|
|
1435
|
+
repository: "{{trigger.repository.full_name}}",
|
|
1436
|
+
pullNumber: "{{trigger.pull_request.number}}"
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
]
|
|
1440
|
+
},
|
|
1441
|
+
tags: ["github", "pr-analysis", "ai-review", "automation", "webhook"],
|
|
1442
|
+
author: "codmir-team"
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
// src/voice/create-ticket.ts
|
|
1446
|
+
var voiceCreateTicketContract = {
|
|
1447
|
+
schemaVersion: "codmir.contract.v1",
|
|
1448
|
+
id: "voice.create-ticket",
|
|
1449
|
+
version: "1.0.0",
|
|
1450
|
+
title: "Create Ticket via Voice",
|
|
1451
|
+
description: "Creates a new ticket/issue from a voice command",
|
|
1452
|
+
execution: {
|
|
1453
|
+
expectedDuration: "instant",
|
|
1454
|
+
mode: "sync",
|
|
1455
|
+
asyncAckMessage: "Creating your ticket now..."
|
|
1456
|
+
},
|
|
1457
|
+
triggers: [
|
|
1458
|
+
{
|
|
1459
|
+
type: "event",
|
|
1460
|
+
event: "voice.action.requested",
|
|
1461
|
+
where: { action: "create_ticket" }
|
|
1462
|
+
}
|
|
1463
|
+
],
|
|
1464
|
+
permissions: [
|
|
1465
|
+
{ kind: "storage:write", scope: "tickets" },
|
|
1466
|
+
{ kind: "event:emit", pattern: "ticket.created" }
|
|
1467
|
+
],
|
|
1468
|
+
limits: {
|
|
1469
|
+
maxRunMs: 1e4,
|
|
1470
|
+
maxSteps: 5
|
|
1471
|
+
},
|
|
1472
|
+
entryStepId: "validate",
|
|
1473
|
+
steps: [
|
|
1474
|
+
{
|
|
1475
|
+
id: "validate",
|
|
1476
|
+
name: "Validate Input",
|
|
1477
|
+
kind: "transform",
|
|
1478
|
+
transforms: {
|
|
1479
|
+
"input.title": "{{trigger.payload.params.title}}",
|
|
1480
|
+
"input.description": "{{trigger.payload.params.description}}",
|
|
1481
|
+
"input.priority": "{{trigger.payload.params.priority}}",
|
|
1482
|
+
"input.projectId": "{{trigger.payload.params.projectId}}",
|
|
1483
|
+
"input.labels": "{{trigger.payload.params.labels}}",
|
|
1484
|
+
"input.userId": "{{trigger.payload.userId}}"
|
|
1485
|
+
},
|
|
1486
|
+
next: "create"
|
|
1487
|
+
},
|
|
1488
|
+
{
|
|
1489
|
+
id: "create",
|
|
1490
|
+
name: "Create Ticket",
|
|
1491
|
+
kind: "task",
|
|
1492
|
+
task: "voice.create-ticket",
|
|
1493
|
+
input: {
|
|
1494
|
+
title: "{{input.title}}",
|
|
1495
|
+
description: "{{input.description}}",
|
|
1496
|
+
priority: "{{input.priority}}",
|
|
1497
|
+
projectId: "{{input.projectId}}",
|
|
1498
|
+
labels: "{{input.labels}}",
|
|
1499
|
+
userId: "{{input.userId}}"
|
|
1500
|
+
},
|
|
1501
|
+
saveAs: "ticket",
|
|
1502
|
+
next: "emit"
|
|
1503
|
+
},
|
|
1504
|
+
{
|
|
1505
|
+
id: "emit",
|
|
1506
|
+
name: "Emit Ticket Created Event",
|
|
1507
|
+
kind: "emit",
|
|
1508
|
+
event: "ticket.created",
|
|
1509
|
+
payload: {
|
|
1510
|
+
ticketId: "{{ticket.id}}",
|
|
1511
|
+
title: "{{ticket.title}}",
|
|
1512
|
+
priority: "{{ticket.priority}}",
|
|
1513
|
+
source: "voice",
|
|
1514
|
+
userId: "{{input.userId}}"
|
|
1515
|
+
},
|
|
1516
|
+
next: "output"
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
id: "output",
|
|
1520
|
+
name: "Prepare Output",
|
|
1521
|
+
kind: "transform",
|
|
1522
|
+
transforms: {
|
|
1523
|
+
"output.success": true,
|
|
1524
|
+
"output.ticketId": "{{ticket.id}}",
|
|
1525
|
+
"output.title": "{{ticket.title}}",
|
|
1526
|
+
"output.url": "{{ticket.url}}",
|
|
1527
|
+
"output.message": "Ticket created successfully"
|
|
1528
|
+
},
|
|
1529
|
+
next: null
|
|
1530
|
+
}
|
|
1531
|
+
],
|
|
1532
|
+
outputs: {
|
|
1533
|
+
onCompleted: [
|
|
1534
|
+
{
|
|
1535
|
+
event: "voice.action.completed",
|
|
1536
|
+
payload: {
|
|
1537
|
+
action: "create_ticket",
|
|
1538
|
+
success: true,
|
|
1539
|
+
result: "{{output}}"
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
],
|
|
1543
|
+
onFailed: [
|
|
1544
|
+
{
|
|
1545
|
+
event: "voice.action.failed",
|
|
1546
|
+
payload: {
|
|
1547
|
+
action: "create_ticket",
|
|
1548
|
+
error: "{{error}}"
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
]
|
|
1552
|
+
},
|
|
1553
|
+
tags: ["voice", "tickets", "instant"]
|
|
1554
|
+
};
|
|
1555
|
+
|
|
1556
|
+
// src/voice/search-codebase.ts
|
|
1557
|
+
var voiceSearchCodebaseContract = {
|
|
1558
|
+
schemaVersion: "codmir.contract.v1",
|
|
1559
|
+
id: "voice.search-codebase",
|
|
1560
|
+
version: "1.0.0",
|
|
1561
|
+
title: "Search Codebase via Voice",
|
|
1562
|
+
description: "Searches the codebase for specific patterns, functions, or files",
|
|
1563
|
+
execution: {
|
|
1564
|
+
expectedDuration: "quick",
|
|
1565
|
+
mode: "sync",
|
|
1566
|
+
syncThreshold: 15e3,
|
|
1567
|
+
// 15 seconds max wait
|
|
1568
|
+
asyncAckMessage: "Searching the codebase now..."
|
|
1569
|
+
},
|
|
1570
|
+
triggers: [
|
|
1571
|
+
{
|
|
1572
|
+
type: "event",
|
|
1573
|
+
event: "voice.action.requested",
|
|
1574
|
+
where: { action: "search_codebase" }
|
|
1575
|
+
}
|
|
1576
|
+
],
|
|
1577
|
+
permissions: [
|
|
1578
|
+
{ kind: "repo:read", scope: "*" },
|
|
1579
|
+
{ kind: "event:emit", pattern: "search.completed" }
|
|
1580
|
+
],
|
|
1581
|
+
limits: {
|
|
1582
|
+
maxRunMs: 3e4,
|
|
1583
|
+
maxSteps: 5
|
|
1584
|
+
},
|
|
1585
|
+
entryStepId: "validate",
|
|
1586
|
+
steps: [
|
|
1587
|
+
{
|
|
1588
|
+
id: "validate",
|
|
1589
|
+
name: "Validate Input",
|
|
1590
|
+
kind: "transform",
|
|
1591
|
+
transforms: {
|
|
1592
|
+
"input.query": "{{trigger.payload.params.query}}",
|
|
1593
|
+
"input.fileTypes": "{{trigger.payload.params.file_types}}",
|
|
1594
|
+
"input.pathFilter": "{{trigger.payload.params.path_filter}}",
|
|
1595
|
+
"input.maxResults": "{{trigger.payload.params.max_results}}",
|
|
1596
|
+
"input.userId": "{{trigger.payload.userId}}"
|
|
1597
|
+
},
|
|
1598
|
+
next: "search"
|
|
1599
|
+
},
|
|
1600
|
+
{
|
|
1601
|
+
id: "search",
|
|
1602
|
+
name: "Search Codebase",
|
|
1603
|
+
kind: "task",
|
|
1604
|
+
task: "voice.search-codebase",
|
|
1605
|
+
input: {
|
|
1606
|
+
query: "{{input.query}}",
|
|
1607
|
+
fileTypes: "{{input.fileTypes}}",
|
|
1608
|
+
pathFilter: "{{input.pathFilter}}",
|
|
1609
|
+
maxResults: "{{input.maxResults}}"
|
|
1610
|
+
},
|
|
1611
|
+
saveAs: "searchResults",
|
|
1612
|
+
next: "output"
|
|
1613
|
+
},
|
|
1614
|
+
{
|
|
1615
|
+
id: "output",
|
|
1616
|
+
name: "Prepare Output",
|
|
1617
|
+
kind: "transform",
|
|
1618
|
+
transforms: {
|
|
1619
|
+
"output.success": true,
|
|
1620
|
+
"output.query": "{{input.query}}",
|
|
1621
|
+
"output.results": "{{searchResults.results}}",
|
|
1622
|
+
"output.total": "{{searchResults.total}}",
|
|
1623
|
+
"output.message": "Found {{searchResults.total}} results"
|
|
1624
|
+
},
|
|
1625
|
+
next: null
|
|
1626
|
+
}
|
|
1627
|
+
],
|
|
1628
|
+
outputs: {
|
|
1629
|
+
onCompleted: [
|
|
1630
|
+
{
|
|
1631
|
+
event: "voice.action.completed",
|
|
1632
|
+
payload: {
|
|
1633
|
+
action: "search_codebase",
|
|
1634
|
+
success: true,
|
|
1635
|
+
result: "{{output}}"
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
],
|
|
1639
|
+
onFailed: [
|
|
1640
|
+
{
|
|
1641
|
+
event: "voice.action.failed",
|
|
1642
|
+
payload: {
|
|
1643
|
+
action: "search_codebase",
|
|
1644
|
+
error: "{{error}}"
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
]
|
|
1648
|
+
},
|
|
1649
|
+
tags: ["voice", "search", "code", "quick"]
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
// src/voice/deploy-preview.ts
|
|
1653
|
+
var voiceDeployPreviewContract = {
|
|
1654
|
+
schemaVersion: "codmir.contract.v1",
|
|
1655
|
+
id: "voice.deploy-preview",
|
|
1656
|
+
version: "1.0.0",
|
|
1657
|
+
title: "Deploy Preview via Voice",
|
|
1658
|
+
description: "Deploys a preview environment for testing",
|
|
1659
|
+
execution: {
|
|
1660
|
+
expectedDuration: "long",
|
|
1661
|
+
mode: "async",
|
|
1662
|
+
interruptible: true,
|
|
1663
|
+
asyncAckMessage: "I'll start the deployment. This may take a few minutes - I'll notify you when it's ready."
|
|
1664
|
+
},
|
|
1665
|
+
triggers: [
|
|
1666
|
+
{
|
|
1667
|
+
type: "event",
|
|
1668
|
+
event: "voice.action.requested",
|
|
1669
|
+
where: { action: "deploy_preview" }
|
|
1670
|
+
}
|
|
1671
|
+
],
|
|
1672
|
+
permissions: [
|
|
1673
|
+
{ kind: "http:request", domains: ["api.vercel.com", "api.netlify.com", "api.railway.app"] },
|
|
1674
|
+
{ kind: "repo:read", scope: "*" },
|
|
1675
|
+
{ kind: "event:emit", pattern: "deploy.*" }
|
|
1676
|
+
],
|
|
1677
|
+
limits: {
|
|
1678
|
+
maxRunMs: 6e5,
|
|
1679
|
+
// 10 minutes
|
|
1680
|
+
maxSteps: 10
|
|
1681
|
+
},
|
|
1682
|
+
entryStepId: "validate",
|
|
1683
|
+
steps: [
|
|
1684
|
+
{
|
|
1685
|
+
id: "validate",
|
|
1686
|
+
name: "Validate Input",
|
|
1687
|
+
kind: "transform",
|
|
1688
|
+
transforms: {
|
|
1689
|
+
"input.branch": "{{trigger.payload.params.branch}}",
|
|
1690
|
+
"input.environment": "{{trigger.payload.params.environment}}",
|
|
1691
|
+
"input.projectId": "{{trigger.payload.params.projectId}}",
|
|
1692
|
+
"input.userId": "{{trigger.payload.userId}}"
|
|
1693
|
+
},
|
|
1694
|
+
next: "confirm"
|
|
1695
|
+
},
|
|
1696
|
+
{
|
|
1697
|
+
id: "confirm",
|
|
1698
|
+
name: "Confirm Deployment",
|
|
1699
|
+
kind: "approval",
|
|
1700
|
+
prompt: "Deploy branch '{{input.branch}}' to {{input.environment}} environment?",
|
|
1701
|
+
approvers: ["{{input.userId}}"],
|
|
1702
|
+
nextOnApproved: "deploy",
|
|
1703
|
+
nextOnDenied: "cancelled",
|
|
1704
|
+
timeout: { ms: 6e4 },
|
|
1705
|
+
timeoutAction: "deny",
|
|
1706
|
+
context: {
|
|
1707
|
+
branch: "{{input.branch}}",
|
|
1708
|
+
environment: "{{input.environment}}"
|
|
1709
|
+
}
|
|
1710
|
+
},
|
|
1711
|
+
{
|
|
1712
|
+
id: "deploy",
|
|
1713
|
+
name: "Deploy to Preview",
|
|
1714
|
+
kind: "task",
|
|
1715
|
+
task: "voice.deploy-preview",
|
|
1716
|
+
input: {
|
|
1717
|
+
branch: "{{input.branch}}",
|
|
1718
|
+
environment: "{{input.environment}}",
|
|
1719
|
+
projectId: "{{input.projectId}}"
|
|
1720
|
+
},
|
|
1721
|
+
saveAs: "deployment",
|
|
1722
|
+
timeout: { ms: 3e5 },
|
|
1723
|
+
// 5 min timeout for deployment
|
|
1724
|
+
next: "notify"
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
id: "notify",
|
|
1728
|
+
name: "Emit Deployment Event",
|
|
1729
|
+
kind: "emit",
|
|
1730
|
+
event: "deploy.completed",
|
|
1731
|
+
payload: {
|
|
1732
|
+
deploymentId: "{{deployment.id}}",
|
|
1733
|
+
url: "{{deployment.url}}",
|
|
1734
|
+
branch: "{{input.branch}}",
|
|
1735
|
+
environment: "{{input.environment}}",
|
|
1736
|
+
source: "voice",
|
|
1737
|
+
userId: "{{input.userId}}"
|
|
1738
|
+
},
|
|
1739
|
+
next: "output"
|
|
1740
|
+
},
|
|
1741
|
+
{
|
|
1742
|
+
id: "output",
|
|
1743
|
+
name: "Prepare Output",
|
|
1744
|
+
kind: "transform",
|
|
1745
|
+
transforms: {
|
|
1746
|
+
"output.success": true,
|
|
1747
|
+
"output.deploymentId": "{{deployment.id}}",
|
|
1748
|
+
"output.url": "{{deployment.url}}",
|
|
1749
|
+
"output.branch": "{{input.branch}}",
|
|
1750
|
+
"output.message": "Deployment complete! Preview available at {{deployment.url}}"
|
|
1751
|
+
},
|
|
1752
|
+
next: null
|
|
1753
|
+
},
|
|
1754
|
+
{
|
|
1755
|
+
id: "cancelled",
|
|
1756
|
+
name: "Handle Cancellation",
|
|
1757
|
+
kind: "transform",
|
|
1758
|
+
transforms: {
|
|
1759
|
+
"output.success": false,
|
|
1760
|
+
"output.message": "Deployment was cancelled"
|
|
1761
|
+
},
|
|
1762
|
+
next: null
|
|
1763
|
+
}
|
|
1764
|
+
],
|
|
1765
|
+
outputs: {
|
|
1766
|
+
onCompleted: [
|
|
1767
|
+
{
|
|
1768
|
+
event: "voice.action.completed",
|
|
1769
|
+
payload: {
|
|
1770
|
+
action: "deploy_preview",
|
|
1771
|
+
success: true,
|
|
1772
|
+
result: "{{output}}"
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
],
|
|
1776
|
+
onFailed: [
|
|
1777
|
+
{
|
|
1778
|
+
event: "voice.action.failed",
|
|
1779
|
+
payload: {
|
|
1780
|
+
action: "deploy_preview",
|
|
1781
|
+
error: "{{error}}"
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
],
|
|
1785
|
+
onCancelled: [
|
|
1786
|
+
{
|
|
1787
|
+
event: "voice.action.cancelled",
|
|
1788
|
+
payload: {
|
|
1789
|
+
action: "deploy_preview",
|
|
1790
|
+
reason: "User cancelled or approval timeout"
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
]
|
|
1794
|
+
},
|
|
1795
|
+
tags: ["voice", "deploy", "preview", "long", "approval"]
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
// src/voice/save-note.ts
|
|
1799
|
+
var voiceSaveNoteContract = {
|
|
1800
|
+
schemaVersion: "codmir.contract.v1",
|
|
1801
|
+
id: "voice.save-note",
|
|
1802
|
+
version: "1.0.0",
|
|
1803
|
+
title: "Save Note via Voice",
|
|
1804
|
+
description: "Saves a note or snippet for later reference",
|
|
1805
|
+
execution: {
|
|
1806
|
+
expectedDuration: "instant",
|
|
1807
|
+
mode: "sync",
|
|
1808
|
+
asyncAckMessage: "Saving your note..."
|
|
1809
|
+
},
|
|
1810
|
+
triggers: [
|
|
1811
|
+
{
|
|
1812
|
+
type: "event",
|
|
1813
|
+
event: "voice.action.requested",
|
|
1814
|
+
where: { action: "save_note" }
|
|
1815
|
+
}
|
|
1816
|
+
],
|
|
1817
|
+
permissions: [
|
|
1818
|
+
{ kind: "storage:write", scope: "notes" },
|
|
1819
|
+
{ kind: "event:emit", pattern: "note.saved" }
|
|
1820
|
+
],
|
|
1821
|
+
limits: {
|
|
1822
|
+
maxRunMs: 1e4,
|
|
1823
|
+
maxSteps: 4
|
|
1824
|
+
},
|
|
1825
|
+
entryStepId: "validate",
|
|
1826
|
+
steps: [
|
|
1827
|
+
{
|
|
1828
|
+
id: "validate",
|
|
1829
|
+
name: "Validate Input",
|
|
1830
|
+
kind: "transform",
|
|
1831
|
+
transforms: {
|
|
1832
|
+
"input.content": "{{trigger.payload.params.content}}",
|
|
1833
|
+
"input.title": "{{trigger.payload.params.title}}",
|
|
1834
|
+
"input.tags": "{{trigger.payload.params.tags}}",
|
|
1835
|
+
"input.userId": "{{trigger.payload.userId}}"
|
|
1836
|
+
},
|
|
1837
|
+
next: "save"
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
id: "save",
|
|
1841
|
+
name: "Save Note",
|
|
1842
|
+
kind: "task",
|
|
1843
|
+
task: "voice.save-note",
|
|
1844
|
+
input: {
|
|
1845
|
+
content: "{{input.content}}",
|
|
1846
|
+
title: "{{input.title}}",
|
|
1847
|
+
tags: "{{input.tags}}",
|
|
1848
|
+
userId: "{{input.userId}}"
|
|
1849
|
+
},
|
|
1850
|
+
saveAs: "note",
|
|
1851
|
+
next: "output"
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
id: "output",
|
|
1855
|
+
name: "Prepare Output",
|
|
1856
|
+
kind: "transform",
|
|
1857
|
+
transforms: {
|
|
1858
|
+
"output.success": true,
|
|
1859
|
+
"output.noteId": "{{note.id}}",
|
|
1860
|
+
"output.title": "{{note.title}}",
|
|
1861
|
+
"output.message": "Note saved successfully"
|
|
1862
|
+
},
|
|
1863
|
+
next: null
|
|
1864
|
+
}
|
|
1865
|
+
],
|
|
1866
|
+
outputs: {
|
|
1867
|
+
onCompleted: [
|
|
1868
|
+
{
|
|
1869
|
+
event: "voice.action.completed",
|
|
1870
|
+
payload: {
|
|
1871
|
+
action: "save_note",
|
|
1872
|
+
success: true,
|
|
1873
|
+
result: "{{output}}"
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
],
|
|
1877
|
+
onFailed: [
|
|
1878
|
+
{
|
|
1879
|
+
event: "voice.action.failed",
|
|
1880
|
+
payload: {
|
|
1881
|
+
action: "save_note",
|
|
1882
|
+
error: "{{error}}"
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
]
|
|
1886
|
+
},
|
|
1887
|
+
tags: ["voice", "notes", "instant"]
|
|
1888
|
+
};
|
|
1889
|
+
|
|
1890
|
+
// src/voice/registry.ts
|
|
1891
|
+
var ACTION_TO_CONTRACT_ID = {
|
|
1892
|
+
create_ticket: "voice.create-ticket",
|
|
1893
|
+
update_ticket: "voice.update-ticket",
|
|
1894
|
+
search_codebase: "voice.search-codebase",
|
|
1895
|
+
search_documentation: "voice.search-documentation",
|
|
1896
|
+
analyze_code: "voice.analyze-code",
|
|
1897
|
+
suggest_fix: "voice.suggest-fix",
|
|
1898
|
+
run_query: "voice.run-query",
|
|
1899
|
+
get_metrics: "voice.get-metrics",
|
|
1900
|
+
deploy_preview: "voice.deploy-preview",
|
|
1901
|
+
check_deploy_status: "voice.check-deploy-status",
|
|
1902
|
+
send_notification: "voice.send-notification",
|
|
1903
|
+
create_summary: "voice.create-summary",
|
|
1904
|
+
set_reminder: "voice.set-reminder",
|
|
1905
|
+
save_note: "voice.save-note"
|
|
1906
|
+
};
|
|
1907
|
+
var VOICE_CONTRACTS = {
|
|
1908
|
+
"voice.create-ticket": voiceCreateTicketContract,
|
|
1909
|
+
"voice.search-codebase": voiceSearchCodebaseContract,
|
|
1910
|
+
"voice.deploy-preview": voiceDeployPreviewContract,
|
|
1911
|
+
"voice.save-note": voiceSaveNoteContract
|
|
1912
|
+
};
|
|
1913
|
+
function getVoiceContract(contractId) {
|
|
1914
|
+
return VOICE_CONTRACTS[contractId];
|
|
1915
|
+
}
|
|
1916
|
+
function getVoiceContractForAction(actionName) {
|
|
1917
|
+
const contractId = ACTION_TO_CONTRACT_ID[actionName];
|
|
1918
|
+
if (!contractId) return void 0;
|
|
1919
|
+
return VOICE_CONTRACTS[contractId];
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
export { ContractValidationError, DEFAULT_GRACE_EXTENSIONS, DURATION_SYNC_THRESHOLDS, GRACE_PRINCIPLES, VOICE_CONTRACTS, and, calculateRiskScore, cloneJson, codeReviewContract, contains, deepMergeJson, deleteByPath, endsWith, eq, evalCondition, exists, extractTemplatePath, getByPath, getVoiceContract, getVoiceContractForAction, gt, gte, hasPermission, isTemplate, lt, lte, mergeJson, neq, not, or, prAnalysisContract, prAnalysisWithLlmContract, requiresHumanReview, resolveStepInput, resolveTemplates, setByPath, startsWith, ticketTriageContract, validateCondition, validateContract, validateContractSafe, voiceCreateTicketContract, voiceDeployPreviewContract, voiceSaveNoteContract, voiceSearchCodebaseContract };
|
|
1923
|
+
//# sourceMappingURL=index.js.map
|
|
1924
|
+
//# sourceMappingURL=index.js.map
|