@snappedly-tools/shipyard 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +24 -14
  2. package/dist/MountConfig-K5ILnfht.d.ts +26 -0
  3. package/dist/{MountConfig-BHnKnA4h.d.ts → SandboxProvider-oUAwYlWm.d.ts} +1 -26
  4. package/dist/{chunk-L6PX5QTU.js → chunk-57EEKW3R.js} +57 -55
  5. package/dist/chunk-57EEKW3R.js.map +1 -0
  6. package/dist/{chunk-JI3HDDMS.js → chunk-HXSZM52J.js} +3 -3
  7. package/dist/{chunk-JI3HDDMS.js.map → chunk-HXSZM52J.js.map} +1 -1
  8. package/dist/{chunk-44I2BL6E.js → chunk-JZUBT4WG.js} +20 -4
  9. package/dist/chunk-JZUBT4WG.js.map +1 -0
  10. package/dist/chunk-NQRFVKCU.js +1840 -0
  11. package/dist/chunk-NQRFVKCU.js.map +1 -0
  12. package/dist/chunk-Z5C4LHVP.js +137 -0
  13. package/dist/chunk-Z5C4LHVP.js.map +1 -0
  14. package/dist/createSandbox-DmbnWAZv.d.ts +739 -0
  15. package/dist/index.d.ts +7 -2534
  16. package/dist/index.js +216 -7269
  17. package/dist/index.js.map +1 -1
  18. package/dist/integrations/github.d.ts +52 -0
  19. package/dist/integrations/github.js +1049 -0
  20. package/dist/integrations/github.js.map +1 -0
  21. package/dist/integrations/releases.d.ts +136 -0
  22. package/dist/integrations/releases.js +500 -0
  23. package/dist/integrations/releases.js.map +1 -0
  24. package/dist/main.js +641 -383
  25. package/dist/main.js.map +1 -1
  26. package/dist/publication-BPoy_M9M.d.ts +1200 -0
  27. package/dist/sandboxes/docker.d.ts +2 -1
  28. package/dist/sandboxes/docker.js +2 -2
  29. package/dist/templates/parallel-planner/main.mts +11 -7
  30. package/dist/templates/parallel-planner/setup.sh +1 -0
  31. package/dist/templates/parallel-planner-with-review/main.mts +11 -7
  32. package/dist/templates/parallel-planner-with-review/setup.sh +1 -0
  33. package/dist/templates/sequential-reviewer/main.mts +7 -3
  34. package/dist/templates/sequential-reviewer/setup.sh +1 -0
  35. package/dist/templates/shared/setup.sh +1 -0
  36. package/dist/templates/simple-loop/main.mts +7 -3
  37. package/dist/templates/simple-loop/setup.sh +1 -0
  38. package/dist/workflow/coordinator/migrations/002_workflow_phase_records.sql +11 -0
  39. package/dist/workflow/coordinator/migrations/003_phase_record_schema_version.sql +6 -0
  40. package/dist/workflow.d.ts +472 -0
  41. package/dist/workflow.js +4345 -0
  42. package/dist/workflow.js.map +1 -0
  43. package/package.json +13 -1
  44. package/dist/chunk-44I2BL6E.js.map +0 -1
  45. package/dist/chunk-L6PX5QTU.js.map +0 -1
@@ -0,0 +1,1840 @@
1
+ import { createHash } from 'crypto';
2
+
3
+ // src/workflow/contracts/index.ts
4
+
5
+ // src/workflow/shared.ts
6
+ var sameWorkIdentity = (left, right) => left.repository === right.repository && left.itemId === right.itemId && left.kind === right.kind;
7
+ var sameRevision = (left, right) => left.branch === right.branch && left.sha === right.sha;
8
+ var isBlockingFinding = (finding) => finding.severity !== "info" && (finding.disposition === "open" || finding.disposition === "deferred");
9
+ var requiredCheckNames = (policy) => policy.checks.filter((check) => check.required).map((check) => check.name);
10
+ var deepFreeze = (value) => {
11
+ if (typeof value !== "object" || value === null) return value;
12
+ for (const child of Object.values(value)) {
13
+ deepFreeze(child);
14
+ }
15
+ return Object.freeze(value);
16
+ };
17
+
18
+ // src/workflow/contracts/index.ts
19
+ var WORKFLOW_CONTRACT_VERSION = 1;
20
+ var isAuthorizationAllowed = (brief, policy) => brief.authorization.status === "approved" && brief.authorization.actor !== void 0 && brief.authorization.actor.trim().length > 0 && brief.authorization.approvedAt !== void 0 && brief.authorization.approvedAt.trim().length > 0 && brief.authorization.actorRole !== void 0 && policy.authorization.allowedActors.includes(brief.authorization.actorRole);
21
+ var ContractValidationError = class extends Error {
22
+ issues;
23
+ constructor(message, issues = []) {
24
+ super(message);
25
+ this.name = "ContractValidationError";
26
+ this.issues = issues.length > 0 ? issues : [message];
27
+ }
28
+ };
29
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
30
+ var nonEmptyString = (value, path) => {
31
+ if (typeof value !== "string" || value.trim().length === 0) {
32
+ throw new ContractValidationError(`${path} must be a non-empty string`);
33
+ }
34
+ return value;
35
+ };
36
+ var optionalString = (value, path) => {
37
+ if (value === void 0) return void 0;
38
+ return nonEmptyString(value, path);
39
+ };
40
+ var enumValue = (value, values, path) => {
41
+ if (typeof value !== "string" || !values.includes(value)) {
42
+ throw new ContractValidationError(
43
+ `${path} must be one of: ${values.join(", ")}`
44
+ );
45
+ }
46
+ return value;
47
+ };
48
+ var stringArray = (value, path) => {
49
+ if (!Array.isArray(value)) {
50
+ throw new ContractValidationError(`${path} must be an array`);
51
+ }
52
+ return value.map(
53
+ (entry, index) => nonEmptyString(entry, `${path}[${index}]`)
54
+ );
55
+ };
56
+ var enumArray = (value, values, path) => stringArray(value, path).map(
57
+ (entry, index) => enumValue(entry, values, `${path}[${index}]`)
58
+ );
59
+ var positiveInteger = (value, path) => {
60
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
61
+ throw new ContractValidationError(`${path} must be a positive integer`);
62
+ }
63
+ return value;
64
+ };
65
+ var nonNegativeInteger = (value, path) => {
66
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
67
+ throw new ContractValidationError(`${path} must be a non-negative integer`);
68
+ }
69
+ return value;
70
+ };
71
+ var contractVersion = (value) => {
72
+ if (value !== WORKFLOW_CONTRACT_VERSION) {
73
+ throw new ContractValidationError(
74
+ `Unsupported workflow contract version: ${String(value)}`
75
+ );
76
+ }
77
+ return WORKFLOW_CONTRACT_VERSION;
78
+ };
79
+ var parseIdentity = (value, path = "identity") => {
80
+ if (!isRecord(value)) {
81
+ throw new ContractValidationError(`${path} must be an object`);
82
+ }
83
+ return {
84
+ repository: nonEmptyString(value.repository, `${path}.repository`),
85
+ itemId: nonEmptyString(value.itemId, `${path}.itemId`),
86
+ kind: enumValue(
87
+ value.kind,
88
+ ["planning-spec", "executable-issue", "pr-repair"],
89
+ `${path}.kind`
90
+ )
91
+ };
92
+ };
93
+ var parseRevision = (value, path) => {
94
+ if (!isRecord(value)) {
95
+ throw new ContractValidationError(`${path} must be an object`);
96
+ }
97
+ return {
98
+ branch: nonEmptyString(value.branch, `${path}.branch`),
99
+ sha: nonEmptyString(value.sha, `${path}.sha`)
100
+ };
101
+ };
102
+ var parseAuthorization = (value) => {
103
+ if (!isRecord(value)) {
104
+ throw new ContractValidationError("authorization must be an object");
105
+ }
106
+ const result = {
107
+ status: enumValue(
108
+ value.status,
109
+ ["pending", "approved", "withdrawn"],
110
+ "authorization.status"
111
+ ),
112
+ actor: optionalString(value.actor, "authorization.actor"),
113
+ actorRole: value.actorRole === void 0 ? void 0 : enumValue(
114
+ value.actorRole,
115
+ ["maintainer", "owner", "policy"],
116
+ "authorization.actorRole"
117
+ ),
118
+ approvedAt: optionalString(value.approvedAt, "authorization.approvedAt")
119
+ };
120
+ if (result.status === "approved" && (!result.actor || !result.approvedAt)) {
121
+ throw new ContractValidationError(
122
+ "approved authorization requires actor and approvedAt"
123
+ );
124
+ }
125
+ return result;
126
+ };
127
+ var parseSource = (value) => {
128
+ if (!isRecord(value)) {
129
+ throw new ContractValidationError("source must be an object");
130
+ }
131
+ return {
132
+ provider: enumValue(
133
+ value.provider,
134
+ ["github", "slack", "manual"],
135
+ "source.provider"
136
+ ),
137
+ repository: nonEmptyString(value.repository, "source.repository"),
138
+ itemId: nonEmptyString(value.itemId, "source.itemId"),
139
+ url: optionalString(value.url, "source.url"),
140
+ originalBody: nonEmptyString(value.originalBody, "source.originalBody"),
141
+ author: optionalString(value.author, "source.author")
142
+ };
143
+ };
144
+ var parseVerification = (value) => {
145
+ if (!isRecord(value)) {
146
+ throw new ContractValidationError("verification must be an object");
147
+ }
148
+ return {
149
+ checks: stringArray(value.checks, "verification.checks"),
150
+ artifacts: stringArray(value.artifacts, "verification.artifacts")
151
+ };
152
+ };
153
+ var briefHashInput = (brief) => canonicalJson(brief);
154
+ var canonicalJson = (value) => {
155
+ if (Array.isArray(value)) {
156
+ return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
157
+ }
158
+ if (isRecord(value)) {
159
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
160
+ }
161
+ return JSON.stringify(value);
162
+ };
163
+ var sha256 = (value) => createHash("sha256").update(value).digest("hex");
164
+ var createWorkBrief = (input) => {
165
+ const withoutHash = {
166
+ contractVersion: WORKFLOW_CONTRACT_VERSION,
167
+ id: input.id ?? `${input.identity.repository}:${input.identity.kind}:${input.identity.itemId}`,
168
+ revision: input.revision ?? 1,
169
+ identity: parseIdentity(input.identity),
170
+ source: parseSource(input.source),
171
+ problem: nonEmptyString(input.problem, "problem"),
172
+ evidence: stringArray(input.evidence, "evidence"),
173
+ acceptanceCriteria: stringArray(
174
+ input.acceptanceCriteria,
175
+ "acceptanceCriteria"
176
+ ),
177
+ exclusions: stringArray(input.exclusions, "exclusions"),
178
+ risk: enumValue(
179
+ input.risk,
180
+ ["low", "medium", "high", "critical", "unknown"],
181
+ "risk"
182
+ ),
183
+ ...input.scope === void 0 ? {} : {
184
+ scope: enumValue(
185
+ input.scope,
186
+ ["small", "substantial", "unknown"],
187
+ "scope"
188
+ )
189
+ },
190
+ verification: parseVerification(input.verification),
191
+ unresolvedQuestions: stringArray(
192
+ input.unresolvedQuestions,
193
+ "unresolvedQuestions"
194
+ ),
195
+ authorization: parseAuthorization(input.authorization),
196
+ base: parseRevision(input.base, "base"),
197
+ policyRevision: nonEmptyString(input.policyRevision, "policyRevision"),
198
+ skillRevision: nonEmptyString(input.skillRevision, "skillRevision"),
199
+ createdAt: nonEmptyString(input.createdAt, "createdAt")
200
+ };
201
+ if (withoutHash.identity.repository !== withoutHash.source.repository || withoutHash.identity.itemId !== withoutHash.source.itemId) {
202
+ throw new ContractValidationError(
203
+ "work brief source must identify the same repository and item as its identity"
204
+ );
205
+ }
206
+ const brief = {
207
+ ...withoutHash,
208
+ hash: sha256(briefHashInput(withoutHash))
209
+ };
210
+ return parseWorkBrief(brief);
211
+ };
212
+ var parseWorkBrief = (value) => {
213
+ if (!isRecord(value)) {
214
+ throw new ContractValidationError("work brief must be an object");
215
+ }
216
+ const parsedWithoutHash = {
217
+ contractVersion: contractVersion(value.contractVersion),
218
+ id: nonEmptyString(value.id, "id"),
219
+ revision: positiveInteger(value.revision, "revision"),
220
+ identity: parseIdentity(value.identity),
221
+ source: parseSource(value.source),
222
+ problem: nonEmptyString(value.problem, "problem"),
223
+ evidence: stringArray(value.evidence, "evidence"),
224
+ acceptanceCriteria: stringArray(
225
+ value.acceptanceCriteria,
226
+ "acceptanceCriteria"
227
+ ),
228
+ exclusions: stringArray(value.exclusions, "exclusions"),
229
+ risk: enumValue(
230
+ value.risk,
231
+ ["low", "medium", "high", "critical", "unknown"],
232
+ "risk"
233
+ ),
234
+ ...value.scope === void 0 ? {} : {
235
+ scope: enumValue(
236
+ value.scope,
237
+ ["small", "substantial", "unknown"],
238
+ "scope"
239
+ )
240
+ },
241
+ verification: parseVerification(value.verification),
242
+ unresolvedQuestions: stringArray(
243
+ value.unresolvedQuestions,
244
+ "unresolvedQuestions"
245
+ ),
246
+ authorization: parseAuthorization(value.authorization),
247
+ base: parseRevision(value.base, "base"),
248
+ policyRevision: nonEmptyString(value.policyRevision, "policyRevision"),
249
+ skillRevision: nonEmptyString(value.skillRevision, "skillRevision"),
250
+ createdAt: nonEmptyString(value.createdAt, "createdAt")
251
+ };
252
+ const hash = nonEmptyString(value.hash, "hash");
253
+ if (parsedWithoutHash.identity.repository !== parsedWithoutHash.source.repository || parsedWithoutHash.identity.itemId !== parsedWithoutHash.source.itemId) {
254
+ throw new ContractValidationError(
255
+ "work brief source must identify the same repository and item as its identity"
256
+ );
257
+ }
258
+ const expectedHash = sha256(briefHashInput(parsedWithoutHash));
259
+ if (hash !== expectedHash) {
260
+ throw new ContractValidationError("work brief hash does not match content");
261
+ }
262
+ return { ...parsedWithoutHash, hash };
263
+ };
264
+ var parsePhaseBudget = (value, path) => {
265
+ if (!isRecord(value)) {
266
+ throw new ContractValidationError(`${path} must be an object`);
267
+ }
268
+ return {
269
+ maxAttempts: positiveInteger(value.maxAttempts, `${path}.maxAttempts`),
270
+ timeoutSeconds: positiveInteger(
271
+ value.timeoutSeconds,
272
+ `${path}.timeoutSeconds`
273
+ )
274
+ };
275
+ };
276
+ var workflowPhases = [
277
+ "triage",
278
+ "implementation",
279
+ "checking",
280
+ "review",
281
+ "repair",
282
+ "handoff",
283
+ "merge",
284
+ "release-verification"
285
+ ];
286
+ var parseRepositoryPolicy = (value) => {
287
+ if (!isRecord(value)) {
288
+ throw new ContractValidationError("repository policy must be an object");
289
+ }
290
+ if (!isRecord(value.authorization)) {
291
+ throw new ContractValidationError("policy.authorization must be an object");
292
+ }
293
+ if (!isRecord(value.worker)) {
294
+ throw new ContractValidationError("policy.worker must be an object");
295
+ }
296
+ const workerModels = value.worker.models === void 0 ? void 0 : (() => {
297
+ if (value.worker.model !== void 0) {
298
+ throw new ContractValidationError(
299
+ "policy.worker.model cannot be combined with policy.worker.models"
300
+ );
301
+ }
302
+ if (!isRecord(value.worker.models)) {
303
+ throw new ContractValidationError(
304
+ "policy.worker.models must be an object"
305
+ );
306
+ }
307
+ return {
308
+ routine: nonEmptyString(
309
+ value.worker.models.routine,
310
+ "policy.worker.models.routine"
311
+ ),
312
+ strong: nonEmptyString(
313
+ value.worker.models.strong,
314
+ "policy.worker.models.strong"
315
+ )
316
+ };
317
+ })();
318
+ if (!Array.isArray(value.checks)) {
319
+ throw new ContractValidationError("policy.checks must be an array");
320
+ }
321
+ const phaseBudgetRecord = value.phaseBudgets;
322
+ if (!isRecord(phaseBudgetRecord)) {
323
+ throw new ContractValidationError("policy.phaseBudgets must be an object");
324
+ }
325
+ if (!isRecord(value.repairBudget)) {
326
+ throw new ContractValidationError("policy.repairBudget must be an object");
327
+ }
328
+ const phaseBudgets = Object.fromEntries(
329
+ workflowPhases.map((phase) => {
330
+ if (!(phase in phaseBudgetRecord)) {
331
+ throw new ContractValidationError(
332
+ `policy.phaseBudgets.${phase} is required`
333
+ );
334
+ }
335
+ return [
336
+ phase,
337
+ parsePhaseBudget(
338
+ phaseBudgetRecord[phase],
339
+ `policy.phaseBudgets.${phase}`
340
+ )
341
+ ];
342
+ })
343
+ );
344
+ const checks = value.checks.map((check, index) => {
345
+ if (!isRecord(check)) {
346
+ throw new ContractValidationError(
347
+ `policy.checks[${index}] must be an object`
348
+ );
349
+ }
350
+ return {
351
+ name: nonEmptyString(check.name, `policy.checks[${index}].name`),
352
+ command: nonEmptyString(check.command, `policy.checks[${index}].command`),
353
+ required: typeof check.required === "boolean" ? check.required : (() => {
354
+ throw new ContractValidationError(
355
+ `policy.checks[${index}].required must be a boolean`
356
+ );
357
+ })()
358
+ };
359
+ });
360
+ const allowedActors = stringArray(
361
+ value.authorization.allowedActors,
362
+ "policy.authorization.allowedActors"
363
+ ).map(
364
+ (actor) => enumValue(
365
+ actor,
366
+ ["maintainer", "owner", "policy"],
367
+ "policy.authorization.allowedActors"
368
+ )
369
+ );
370
+ const autoStartRisk = stringArray(
371
+ value.authorization.autoStartRisk,
372
+ "policy.authorization.autoStartRisk"
373
+ ).map(
374
+ (risk) => enumValue(
375
+ risk,
376
+ ["low", "medium", "high", "critical"],
377
+ "policy.authorization.autoStartRisk"
378
+ )
379
+ );
380
+ const required = value.authorization.required;
381
+ if (typeof required !== "boolean") {
382
+ throw new ContractValidationError(
383
+ "policy.authorization.required must be a boolean"
384
+ );
385
+ }
386
+ const maxBatches = positiveInteger(
387
+ value.repairBudget.maxBatches,
388
+ "policy.repairBudget.maxBatches"
389
+ );
390
+ const maxFollowUps = nonNegativeInteger(
391
+ value.repairBudget.maxFollowUps,
392
+ "policy.repairBudget.maxFollowUps"
393
+ );
394
+ return {
395
+ contractVersion: contractVersion(value.contractVersion),
396
+ repository: nonEmptyString(value.repository, "policy.repository"),
397
+ revision: nonEmptyString(value.revision, "policy.revision"),
398
+ baseBranch: nonEmptyString(value.baseBranch, "policy.baseBranch"),
399
+ issueClosure: enumValue(
400
+ value.issueClosure,
401
+ [
402
+ "merge-and-ci",
403
+ "staging-verification",
404
+ "production-verification"
405
+ ],
406
+ "policy.issueClosure"
407
+ ),
408
+ authorization: { required, allowedActors, autoStartRisk },
409
+ worker: {
410
+ provider: nonEmptyString(value.worker.provider, "policy.worker.provider"),
411
+ ...workerModels === void 0 ? { model: nonEmptyString(value.worker.model, "policy.worker.model") } : { models: workerModels },
412
+ sandbox: nonEmptyString(value.worker.sandbox, "policy.worker.sandbox"),
413
+ skillRevision: nonEmptyString(
414
+ value.worker.skillRevision,
415
+ "policy.worker.skillRevision"
416
+ )
417
+ },
418
+ checks,
419
+ phaseBudgets,
420
+ repairBudget: { maxBatches, maxFollowUps }
421
+ };
422
+ };
423
+ var createRepositoryPolicy = (input) => parseRepositoryPolicy({
424
+ contractVersion: WORKFLOW_CONTRACT_VERSION,
425
+ ...input
426
+ });
427
+ var resolveAgentSelection = (policy, phase, risk, scope) => {
428
+ const role = phase === "review" && !(risk === "low" && scope === "small") ? "strong" : "routine";
429
+ const worker = policy.worker;
430
+ const modelPath = worker.models ? `policy.worker.models.${role}` : "policy.worker.model";
431
+ return Object.freeze({
432
+ provider: nonEmptyString(worker.provider, "policy.worker.provider"),
433
+ model: nonEmptyString(worker.models?.[role] ?? worker.model, modelPath),
434
+ role
435
+ });
436
+ };
437
+ var parseCheckEvidence = (value) => {
438
+ if (!isRecord(value)) {
439
+ throw new ContractValidationError("check evidence must be an object");
440
+ }
441
+ const status = enumValue(
442
+ value.status,
443
+ ["passed", "failed", "incomplete", "blocked", "unknown"],
444
+ "check.status"
445
+ );
446
+ const summary = nonEmptyString(value.summary, "check.summary");
447
+ const exitCode = value.exitCode === void 0 ? void 0 : nonNegativeInteger(value.exitCode, "check.exitCode");
448
+ return {
449
+ name: nonEmptyString(value.name, "check.name"),
450
+ command: nonEmptyString(value.command, "check.command"),
451
+ status,
452
+ summary,
453
+ baseSha: optionalString(value.baseSha, "check.baseSha"),
454
+ headSha: optionalString(value.headSha, "check.headSha"),
455
+ briefHash: optionalString(value.briefHash, "check.briefHash"),
456
+ startedAt: optionalString(value.startedAt, "check.startedAt"),
457
+ completedAt: optionalString(value.completedAt, "check.completedAt"),
458
+ exitCode,
459
+ artifactRefs: value.artifactRefs === void 0 ? void 0 : stringArray(value.artifactRefs, "check.artifactRefs")
460
+ };
461
+ };
462
+ var allowedTransitions = {
463
+ queued: ["waiting-info", "authorized", "cancelled", "blocked", "failed"],
464
+ "waiting-info": ["queued", "authorized", "cancelled"],
465
+ authorized: ["implementing", "waiting-info", "cancelled", "blocked"],
466
+ implementing: ["checking", "waiting-info", "blocked", "failed", "cancelled"],
467
+ checking: ["reviewing", "repairing", "blocked", "failed", "cancelled"],
468
+ reviewing: ["repairing", "human-review", "blocked", "failed", "cancelled"],
469
+ repairing: ["checking", "reviewing", "blocked", "failed", "cancelled"],
470
+ "human-review": ["merged", "repairing", "blocked", "cancelled"],
471
+ merged: ["release-verifying", "completed", "failed", "blocked"],
472
+ "release-verifying": ["completed", "failed", "blocked"],
473
+ completed: [],
474
+ failed: [],
475
+ blocked: [],
476
+ cancelled: []
477
+ };
478
+ var allRequiredChecksPassed = (context) => {
479
+ const checks = context.checks;
480
+ if (checks === void 0) return false;
481
+ if (context.requiredCheckNames === void 0) {
482
+ return checks.length > 0 && checks.every((check) => check.status === "passed");
483
+ }
484
+ if (context.requiredCheckNames.length > 0 && context.checkCandidate === void 0) {
485
+ return false;
486
+ }
487
+ return context.requiredCheckNames.every(
488
+ (name) => checks.some(
489
+ (check) => check.name === name && check.status === "passed" && (context.checkCandidate === void 0 || check.baseSha === context.checkCandidate.baseSha && check.headSha === context.checkCandidate.headSha && check.briefHash === context.checkCandidate.briefHash)
490
+ )
491
+ );
492
+ };
493
+ var requireTransition = (from, to, context) => {
494
+ if (!allowedTransitions[from].includes(to)) {
495
+ throw new ContractValidationError(
496
+ `Transition from ${from} to ${to} is not allowed`
497
+ );
498
+ }
499
+ if (to === "implementing") {
500
+ if (context.kind !== "executable-issue") {
501
+ throw new ContractValidationError(
502
+ "Only an executable issue may enter implementation; planning spec and PR repair are separate flows"
503
+ );
504
+ }
505
+ if (context.authorization !== "approved") {
506
+ throw new ContractValidationError(
507
+ "Implementation requires approved authorization"
508
+ );
509
+ }
510
+ }
511
+ if (to === "reviewing" && !allRequiredChecksPassed(context)) {
512
+ const unknown = context.checks?.find((check) => check.status === "unknown");
513
+ throw new ContractValidationError(
514
+ unknown ? `Cannot review while check ${unknown.name} is unknown` : "Cannot review until all required checks pass"
515
+ );
516
+ }
517
+ if (to === "human-review") {
518
+ if (!allRequiredChecksPassed(context)) {
519
+ throw new ContractValidationError(
520
+ "Human review requires a complete set of passing checks"
521
+ );
522
+ }
523
+ if (context.review?.outcome !== "passed" || context.review.axes.length === 0 || context.review.findings.some(isBlockingFinding)) {
524
+ throw new ContractValidationError(
525
+ "Human review requires every review axis to pass and blocking findings to be disposed"
526
+ );
527
+ }
528
+ if (context.currentHeadSha !== void 0 && context.assignedHeadSha !== void 0 && context.currentHeadSha !== context.assignedHeadSha) {
529
+ throw new ContractValidationError(
530
+ "Human review evidence is stale for the current candidate head"
531
+ );
532
+ }
533
+ }
534
+ };
535
+ var createAssignment = (input) => {
536
+ const brief = parseWorkBrief(input.brief);
537
+ if (brief.identity.kind === "planning-spec") {
538
+ throw new ContractValidationError(
539
+ "A planning spec cannot receive an execution assignment"
540
+ );
541
+ }
542
+ if (input.phase === "implementation" && brief.identity.kind !== "executable-issue") {
543
+ throw new ContractValidationError(
544
+ "PR repair work must use the repair phase; planning spec cannot be implemented"
545
+ );
546
+ }
547
+ if (input.phase === "implementation" && !isAuthorizationAllowed(brief, input.policy)) {
548
+ throw new ContractValidationError(
549
+ "Implementation assignments require approved authorization"
550
+ );
551
+ }
552
+ if (input.policy.repository !== brief.identity.repository) {
553
+ throw new ContractValidationError(
554
+ "Assignment policy repository does not match work identity"
555
+ );
556
+ }
557
+ if (["checking", "review", "handoff", "merge"].includes(input.phase) && input.head === void 0) {
558
+ throw new ContractValidationError(
559
+ `${input.phase} assignments require an immutable candidate head`
560
+ );
561
+ }
562
+ const assignment = {
563
+ contractVersion: WORKFLOW_CONTRACT_VERSION,
564
+ id: nonEmptyString(input.id, "assignment.id"),
565
+ phase: input.phase,
566
+ attempt: positiveInteger(input.attempt, "assignment.attempt"),
567
+ identity: brief.identity,
568
+ briefId: brief.id,
569
+ briefRevision: brief.revision,
570
+ briefHash: brief.hash,
571
+ policyRevision: input.policy.revision,
572
+ skillRevision: brief.skillRevision,
573
+ agentSelection: resolveAgentSelection(
574
+ input.policy,
575
+ input.phase,
576
+ brief.risk,
577
+ brief.scope
578
+ ),
579
+ base: brief.base,
580
+ head: input.head,
581
+ createdAt: nonEmptyString(input.createdAt, "assignment.createdAt")
582
+ };
583
+ return Object.freeze(assignment);
584
+ };
585
+ var parsePhaseResult = (value) => {
586
+ if (!isRecord(value)) {
587
+ throw new ContractValidationError("phase result must be an object");
588
+ }
589
+ contractVersion(value.contractVersion);
590
+ const outcome = enumValue(
591
+ value.outcome,
592
+ ["completed", "needs-info", "blocked", "failed", "cancelled"],
593
+ "phase result.outcome"
594
+ );
595
+ const evidence = stringArray(value.evidence, "phase result.evidence");
596
+ const commits = stringArray(value.commits, "phase result.commits");
597
+ if (outcome === "completed" && evidence.length === 0 && commits.length === 0) {
598
+ throw new ContractValidationError(
599
+ "A completed phase requires evidence; zero commits alone cannot mean success"
600
+ );
601
+ }
602
+ const checksValue = value.checks;
603
+ if (!Array.isArray(checksValue)) {
604
+ throw new ContractValidationError("phase result.checks must be an array");
605
+ }
606
+ const checks = checksValue.map(parseCheckEvidence);
607
+ return {
608
+ contractVersion: WORKFLOW_CONTRACT_VERSION,
609
+ assignmentId: nonEmptyString(
610
+ value.assignmentId,
611
+ "phase result.assignmentId"
612
+ ),
613
+ phase: enumValue(
614
+ value.phase,
615
+ [
616
+ "triage",
617
+ "implementation",
618
+ "checking",
619
+ "review",
620
+ "repair",
621
+ "handoff",
622
+ "merge",
623
+ "release-verification"
624
+ ],
625
+ "phase result.phase"
626
+ ),
627
+ outcome,
628
+ identity: parseIdentity(value.identity, "phase result.identity"),
629
+ briefHash: nonEmptyString(value.briefHash, "phase result.briefHash"),
630
+ base: value.base === void 0 ? void 0 : parseRevision(value.base, "phase result.base"),
631
+ head: value.head === void 0 ? void 0 : parseRevision(value.head, "phase result.head"),
632
+ summary: nonEmptyString(value.summary, "phase result.summary"),
633
+ evidence,
634
+ checks,
635
+ commits,
636
+ artifacts: stringArray(value.artifacts, "phase result.artifacts"),
637
+ questions: stringArray(value.questions, "phase result.questions"),
638
+ findings: Array.isArray(value.findings) ? value.findings.map(parseFinding) : (() => {
639
+ throw new ContractValidationError(
640
+ "phase result.findings must be an array"
641
+ );
642
+ })(),
643
+ reviewAxes: value.reviewAxes === void 0 ? void 0 : enumArray(
644
+ value.reviewAxes,
645
+ ["standards", "spec", "interface"],
646
+ "phase result.reviewAxes"
647
+ ),
648
+ completedAt: nonEmptyString(value.completedAt, "phase result.completedAt")
649
+ };
650
+ };
651
+ var parseFinding = (value) => {
652
+ if (!isRecord(value)) {
653
+ throw new ContractValidationError("finding must be an object");
654
+ }
655
+ return {
656
+ id: nonEmptyString(value.id, "finding.id"),
657
+ severity: enumValue(
658
+ value.severity,
659
+ ["info", "low", "medium", "high", "critical"],
660
+ "finding.severity"
661
+ ),
662
+ axis: enumValue(
663
+ value.axis,
664
+ ["standards", "spec", "interface"],
665
+ "finding.axis"
666
+ ),
667
+ disposition: enumValue(
668
+ value.disposition,
669
+ ["open", "fixed", "rejected", "accepted", "deferred"],
670
+ "finding.disposition"
671
+ ),
672
+ title: nonEmptyString(value.title, "finding.title"),
673
+ evidence: nonEmptyString(value.evidence, "finding.evidence"),
674
+ location: optionalString(value.location, "finding.location"),
675
+ requirement: optionalString(value.requirement, "finding.requirement"),
676
+ verification: optionalString(value.verification, "finding.verification")
677
+ };
678
+ };
679
+
680
+ // src/workflow/handoff/index.ts
681
+ var defaultAxes = ["standards", "spec"];
682
+ var isFreshTimestamp = (value, now, freshnessWindowSeconds) => {
683
+ const timestamp = Date.parse(value);
684
+ const current = Date.parse(now);
685
+ return Number.isFinite(timestamp) && Number.isFinite(current) && timestamp <= current && current - timestamp <= freshnessWindowSeconds * 1e3;
686
+ };
687
+ var implementationEvidence = (job, candidate) => [...job.phaseResults].reverse().find(
688
+ (result) => result.outcome === "completed" && (result.phase === "implementation" || result.phase === "repair") && result.base !== void 0 && result.head !== void 0 && sameRevision(result.base, candidate.base) && sameRevision(result.head, candidate.head) && result.briefHash === candidate.briefHash
689
+ )?.evidence ?? [];
690
+ var allRequiredChecksPassed2 = (policy, checks, expected) => {
691
+ const reasons = [];
692
+ for (const name of requiredCheckNames(policy)) {
693
+ const named = checks.filter((candidate) => candidate.name === name);
694
+ if (named.length === 0) {
695
+ reasons.push(`Required check is missing: ${name}`);
696
+ continue;
697
+ }
698
+ const check = expected === void 0 ? named[0] : named.find(
699
+ (candidate) => candidate.headSha === expected.headSha && (expected.baseSha === void 0 || candidate.baseSha === expected.baseSha) && (expected.briefHash === void 0 || candidate.briefHash === expected.briefHash)
700
+ );
701
+ if (check === void 0) {
702
+ reasons.push(
703
+ `Required check is stale for the current candidate: ${name}`
704
+ );
705
+ } else if (check.status !== "passed") {
706
+ reasons.push(`Required check ${name} is ${check.status}`);
707
+ }
708
+ }
709
+ return reasons;
710
+ };
711
+ var candidateReasons = (job, candidate, review, requiredAxes) => {
712
+ const reasons = [];
713
+ let brief;
714
+ try {
715
+ brief = parseWorkBrief(job.brief);
716
+ } catch {
717
+ return ["Workflow brief is malformed"];
718
+ }
719
+ if (brief.identity.kind !== "executable-issue") {
720
+ reasons.push("Only executable issues may reach human PR handoff");
721
+ }
722
+ if (!isAuthorizationAllowed(brief, job.policy)) {
723
+ reasons.push("Implementation authorization is not approved");
724
+ }
725
+ if (brief.identity.repository !== job.policy.repository) {
726
+ reasons.push("Workflow policy repository does not match the brief");
727
+ }
728
+ if (job.control !== "active") reasons.push(`Workflow job is ${job.control}`);
729
+ if (!sameRevision(brief.base, candidate.base)) {
730
+ reasons.push("Candidate base does not match the brief base");
731
+ }
732
+ if (review.baseSha !== candidate.base.sha) {
733
+ reasons.push("Review evidence is bound to a different base revision");
734
+ }
735
+ if (review.headSha !== candidate.head.sha) {
736
+ reasons.push("Review evidence is bound to a different candidate head");
737
+ }
738
+ if (review.briefHash !== candidate.briefHash || review.briefHash !== brief.hash) {
739
+ reasons.push("Review evidence is bound to a different brief revision");
740
+ }
741
+ const implementationHead = [...job.phaseResults].reverse().find(
742
+ (result) => result.phase === "implementation" || result.phase === "repair"
743
+ )?.head;
744
+ if (implementationHead !== void 0 && !sameRevision(implementationHead, candidate.head)) {
745
+ reasons.push("Candidate head does not match the implementation result");
746
+ }
747
+ if (review.outcome !== "passed") {
748
+ reasons.push(`Independent review outcome is ${review.outcome}`);
749
+ }
750
+ if (implementationEvidence(job, candidate).length < brief.acceptanceCriteria.length) {
751
+ reasons.push("Implementation acceptance evidence is missing");
752
+ }
753
+ for (const axis of requiredAxes) {
754
+ if (!review.axes.includes(axis))
755
+ reasons.push(`Review axis is missing: ${axis}`);
756
+ }
757
+ for (const finding of review.findings) {
758
+ if (isBlockingFinding(finding)) {
759
+ reasons.push(`Blocking finding remains: ${finding.id}`);
760
+ }
761
+ }
762
+ return reasons;
763
+ };
764
+ var packetFor = (input, brief) => ({
765
+ sourceIssue: `#${brief.identity.itemId}`,
766
+ candidate: input.candidate,
767
+ briefRevision: brief.revision,
768
+ briefHash: brief.hash,
769
+ change: brief.problem,
770
+ risk: brief.risk,
771
+ acceptanceCriteria: brief.acceptanceCriteria,
772
+ acceptanceEvidence: implementationEvidence(input.job, input.candidate),
773
+ checks: input.checks,
774
+ reviewAxes: input.review.axes,
775
+ findings: input.review.findings,
776
+ limitations: [
777
+ ...brief.exclusions,
778
+ ...brief.unresolvedQuestions.map((question) => `Unresolved: ${question}`)
779
+ ]
780
+ });
781
+ var approvalMatches = (candidate, brief, policy, approval) => approval !== void 0 && approval.actor.trim().length > 0 && approval.approvedAt.trim().length > 0 && policy.authorization.allowedActors.includes(approval.actorRole) && approval.baseSha === candidate.base.sha && approval.headSha === candidate.head.sha && approval.briefHash === brief.hash;
782
+ var evaluateHandoffReadiness = (input) => {
783
+ const brief = parseWorkBrief(input.job.brief);
784
+ const policy = parseRepositoryPolicy(input.job.policy);
785
+ const requiredAxes = [
786
+ .../* @__PURE__ */ new Set([...defaultAxes, ...input.requiredAxes ?? []])
787
+ ];
788
+ const reasons = [
789
+ ...candidateReasons(input.job, input.candidate, input.review, requiredAxes),
790
+ ...allRequiredChecksPassed2(policy, input.checks, {
791
+ baseSha: input.candidate.base.sha,
792
+ headSha: input.candidate.head.sha,
793
+ briefHash: input.candidate.briefHash
794
+ })
795
+ ];
796
+ const packet = {
797
+ ...packetFor(input, brief),
798
+ pullRequest: void 0
799
+ };
800
+ const readyForHuman = reasons.length === 0;
801
+ const protection = input.branchProtection;
802
+ const freshnessWindowSeconds = input.freshnessWindowSeconds ?? 300;
803
+ const freshMergeEvidence = Number.isFinite(freshnessWindowSeconds) && freshnessWindowSeconds > 0 && protection?.provider === "github" && isFreshTimestamp(
804
+ protection.verifiedAt,
805
+ input.now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
806
+ freshnessWindowSeconds
807
+ ) && input.humanApproval !== void 0 && isFreshTimestamp(
808
+ input.humanApproval.approvedAt,
809
+ input.now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
810
+ freshnessWindowSeconds
811
+ );
812
+ const mergeReady = readyForHuman && protection?.enforced === true && protection.humanApprovalRequired === true && freshMergeEvidence && approvalMatches(input.candidate, brief, policy, input.humanApproval);
813
+ if (!readyForHuman) {
814
+ return {
815
+ outcome: "blocked",
816
+ readyForReview: false,
817
+ reviewRequested: false,
818
+ readyForHuman: false,
819
+ mergeReady: false,
820
+ reasons,
821
+ packet
822
+ };
823
+ }
824
+ return {
825
+ outcome: mergeReady ? "merge-ready" : "ready-for-review",
826
+ readyForReview: true,
827
+ reviewRequested: false,
828
+ readyForHuman: true,
829
+ mergeReady,
830
+ reasons,
831
+ packet
832
+ };
833
+ };
834
+ var prepareHumanHandoff = async (input) => {
835
+ if (input.readCurrent !== void 0) {
836
+ let current;
837
+ try {
838
+ current = await input.readCurrent();
839
+ } catch (error) {
840
+ const brief = parseWorkBrief(input.job.brief);
841
+ return {
842
+ outcome: "blocked",
843
+ readyForReview: false,
844
+ reviewRequested: false,
845
+ readyForHuman: false,
846
+ mergeReady: false,
847
+ reasons: [
848
+ `Could not validate the current candidate: ${error instanceof Error ? error.message : String(error)}`
849
+ ],
850
+ packet: {
851
+ ...packetFor(input, brief),
852
+ pullRequest: `#${input.pullRequestNumber}`
853
+ }
854
+ };
855
+ }
856
+ if (!sameRevision(current.base, input.candidate.base) || !sameRevision(current.head, input.candidate.head) || current.briefHash !== input.candidate.briefHash) {
857
+ const brief = parseWorkBrief(input.job.brief);
858
+ return {
859
+ outcome: "blocked",
860
+ readyForReview: false,
861
+ reviewRequested: false,
862
+ readyForHuman: false,
863
+ mergeReady: false,
864
+ reasons: ["Candidate changed before human handoff"],
865
+ packet: {
866
+ ...packetFor(input, brief),
867
+ pullRequest: `#${input.pullRequestNumber}`
868
+ }
869
+ };
870
+ }
871
+ }
872
+ const readiness = evaluateHandoffReadiness(input);
873
+ const packet = {
874
+ ...readiness.packet,
875
+ pullRequest: `#${input.pullRequestNumber}`
876
+ };
877
+ let result = { ...readiness, packet };
878
+ if (result.readyForHuman && input.publisher !== void 0) {
879
+ try {
880
+ await input.publisher.requestReview({
881
+ packet,
882
+ pullRequestNumber: input.pullRequestNumber
883
+ });
884
+ result = {
885
+ ...result,
886
+ outcome: "review-requested",
887
+ reviewRequested: true
888
+ };
889
+ } catch (error) {
890
+ result = {
891
+ ...result,
892
+ outcome: "blocked",
893
+ readyForReview: false,
894
+ reviewRequested: false,
895
+ readyForHuman: false,
896
+ reasons: [
897
+ `Could not request human review: ${error instanceof Error ? error.message : String(error)}`
898
+ ]
899
+ };
900
+ }
901
+ }
902
+ return result;
903
+ };
904
+ var resolveHumanReviewDecision = (input) => {
905
+ switch (input.decision) {
906
+ case "approved":
907
+ return { outcome: "merge-ready", reason: input.reason };
908
+ case "changes-requested":
909
+ return {
910
+ outcome: "repair-needed",
911
+ reason: input.reason ?? "Human review requested changes"
912
+ };
913
+ case "rejected":
914
+ return {
915
+ outcome: "rejected",
916
+ reason: input.reason ?? "Human review rejected the candidate"
917
+ };
918
+ case "abandoned":
919
+ return {
920
+ outcome: "abandoned",
921
+ reason: input.reason ?? "Pull request was abandoned"
922
+ };
923
+ }
924
+ };
925
+ var processHumanReviewDecision = async (input) => {
926
+ let current;
927
+ try {
928
+ current = await input.readCurrent();
929
+ } catch (error) {
930
+ return {
931
+ outcome: "blocked",
932
+ reason: `Could not validate the current candidate: ${error instanceof Error ? error.message : String(error)}`,
933
+ candidate: input.candidate,
934
+ pullRequestNumber: input.pullRequestNumber
935
+ };
936
+ }
937
+ if (!sameRevision(current.base, input.candidate.base) || !sameRevision(current.head, input.candidate.head) || current.briefHash !== input.candidate.briefHash) {
938
+ return {
939
+ outcome: "blocked",
940
+ reason: "Human review decision is stale for the current candidate",
941
+ candidate: input.candidate,
942
+ pullRequestNumber: input.pullRequestNumber
943
+ };
944
+ }
945
+ const decision = resolveHumanReviewDecision(input.decision);
946
+ if (decision.outcome === "repair-needed") {
947
+ try {
948
+ await input.requestRepair({
949
+ candidate: input.candidate,
950
+ pullRequestNumber: input.pullRequestNumber,
951
+ reason: decision.reason ?? "Human review requested changes"
952
+ });
953
+ } catch (error) {
954
+ return {
955
+ outcome: "blocked",
956
+ reason: `Could not schedule the requested repair: ${error instanceof Error ? error.message : String(error)}`,
957
+ candidate: input.candidate,
958
+ pullRequestNumber: input.pullRequestNumber
959
+ };
960
+ }
961
+ }
962
+ return {
963
+ ...decision,
964
+ candidate: input.candidate,
965
+ pullRequestNumber: input.pullRequestNumber
966
+ };
967
+ };
968
+ var mergeProtectedCandidate = async (input) => {
969
+ const brief = parseWorkBrief(input.job.brief);
970
+ const policy = parseRepositoryPolicy(input.job.policy);
971
+ let current;
972
+ try {
973
+ current = await input.readCurrent();
974
+ } catch (error) {
975
+ return {
976
+ outcome: "blocked",
977
+ reason: `Could not validate the current candidate: ${error instanceof Error ? error.message : String(error)}`
978
+ };
979
+ }
980
+ if (!sameRevision(current.base, input.candidate.base) || !sameRevision(current.head, input.candidate.head) || current.briefHash !== input.candidate.briefHash) {
981
+ return {
982
+ outcome: "blocked",
983
+ reason: "current candidate changed before protected merge"
984
+ };
985
+ }
986
+ const freshnessWindowSeconds = input.freshnessWindowSeconds ?? 300;
987
+ if (!Number.isFinite(freshnessWindowSeconds) || freshnessWindowSeconds <= 0) {
988
+ return {
989
+ outcome: "blocked",
990
+ reason: "Freshness window must be a positive number of seconds"
991
+ };
992
+ }
993
+ let branchProtection;
994
+ try {
995
+ branchProtection = await input.readBranchProtection();
996
+ } catch (error) {
997
+ return {
998
+ outcome: "blocked",
999
+ reason: `Could not verify branch protection: ${error instanceof Error ? error.message : String(error)}`
1000
+ };
1001
+ }
1002
+ const readiness = evaluateHandoffReadiness({
1003
+ ...input,
1004
+ branchProtection
1005
+ });
1006
+ if (!readiness.readyForHuman) {
1007
+ return { outcome: "blocked", reason: readiness.reasons.join("; ") };
1008
+ }
1009
+ if (!branchProtection.enforced) {
1010
+ return {
1011
+ outcome: "blocked",
1012
+ reason: "Branch protection is not proven enforced"
1013
+ };
1014
+ }
1015
+ if (!branchProtection.humanApprovalRequired) {
1016
+ return {
1017
+ outcome: "blocked",
1018
+ reason: "Human approval gate is not configured"
1019
+ };
1020
+ }
1021
+ if (branchProtection.provider !== "github" || !isFreshTimestamp(
1022
+ branchProtection.verifiedAt,
1023
+ input.now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
1024
+ freshnessWindowSeconds
1025
+ )) {
1026
+ return {
1027
+ outcome: "blocked",
1028
+ reason: "Branch protection evidence is not freshly verified"
1029
+ };
1030
+ }
1031
+ if (!isFreshTimestamp(
1032
+ input.humanApproval.approvedAt,
1033
+ input.now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
1034
+ freshnessWindowSeconds
1035
+ )) {
1036
+ return {
1037
+ outcome: "blocked",
1038
+ reason: "Human approval evidence is not fresh"
1039
+ };
1040
+ }
1041
+ if (!approvalMatches(input.candidate, brief, policy, input.humanApproval)) {
1042
+ return {
1043
+ outcome: "blocked",
1044
+ reason: "Human approval does not match the exact candidate"
1045
+ };
1046
+ }
1047
+ try {
1048
+ const merged = await input.transport.mergeProtected({
1049
+ pullRequestNumber: input.pullRequestNumber,
1050
+ headSha: input.candidate.head.sha,
1051
+ baseBranch: policy.baseBranch
1052
+ });
1053
+ return { outcome: "merged", mergedSha: merged.mergedSha };
1054
+ } catch (error) {
1055
+ return {
1056
+ outcome: "blocked",
1057
+ reason: error instanceof Error ? error.message : String(error)
1058
+ };
1059
+ }
1060
+ };
1061
+ var completeSourceIssue = (input) => {
1062
+ const policy = parseRepositoryPolicy(input.policy);
1063
+ if (input.mergedSha.trim().length === 0) {
1064
+ return {
1065
+ outcome: "open",
1066
+ reason: "Merged revision is missing",
1067
+ mergedSha: input.mergedSha,
1068
+ checks: input.checks
1069
+ };
1070
+ }
1071
+ if (policy.issueClosure !== "merge-and-ci") {
1072
+ return {
1073
+ outcome: "open",
1074
+ reason: "Source issue remains open until release verification",
1075
+ mergedSha: input.mergedSha,
1076
+ checks: input.checks
1077
+ };
1078
+ }
1079
+ const checkReasons = allRequiredChecksPassed2(policy, input.checks, {
1080
+ headSha: input.mergedSha
1081
+ });
1082
+ if (checkReasons.length > 0) {
1083
+ return {
1084
+ outcome: "open",
1085
+ reason: checkReasons.join("; "),
1086
+ mergedSha: input.mergedSha,
1087
+ checks: input.checks
1088
+ };
1089
+ }
1090
+ return {
1091
+ outcome: "completed",
1092
+ mergedSha: input.mergedSha,
1093
+ checks: input.checks
1094
+ };
1095
+ };
1096
+ var closeSourceIssue = async (input) => {
1097
+ const completion = completeSourceIssue(input);
1098
+ if (completion.outcome === "open") {
1099
+ return { ...completion, closed: false };
1100
+ }
1101
+ try {
1102
+ await input.closer.closeIssue({
1103
+ issueNumber: input.sourceIssueNumber,
1104
+ mergedSha: input.mergedSha,
1105
+ checks: input.checks
1106
+ });
1107
+ return { ...completion, closed: true };
1108
+ } catch (error) {
1109
+ return {
1110
+ ...completion,
1111
+ outcome: "open",
1112
+ closed: false,
1113
+ reason: `Could not close source issue: ${error instanceof Error ? error.message : String(error)}`
1114
+ };
1115
+ }
1116
+ };
1117
+
1118
+ // src/workflow/phase-storage.ts
1119
+ var PostgresWorkflowPhaseRecordStore = class {
1120
+ client;
1121
+ constructor(options) {
1122
+ this.client = options.client;
1123
+ }
1124
+ async get(kind, key) {
1125
+ const result = await this.client.query(
1126
+ `SELECT record, schema_version
1127
+ FROM shipyard_workflow_phase_records
1128
+ WHERE namespace = $1 AND record_key = $2`,
1129
+ [kind, encodeURIComponent(key)]
1130
+ );
1131
+ const row = result.rows[0];
1132
+ if (row === void 0) return void 0;
1133
+ if (Number(row.schema_version) !== 1) {
1134
+ throw new Error(
1135
+ `Unsupported ${kind} record schema version: ${String(row.schema_version)}`
1136
+ );
1137
+ }
1138
+ if (typeof row.record === "string")
1139
+ return JSON.parse(row.record);
1140
+ return row.record;
1141
+ }
1142
+ async save(kind, key, record, updatedAt) {
1143
+ await this.client.query(
1144
+ `INSERT INTO shipyard_workflow_phase_records
1145
+ (namespace, record_key, schema_version, record, updated_at)
1146
+ VALUES ($1, $2, 1, $3::jsonb, $4::timestamptz)
1147
+ ON CONFLICT (namespace, record_key) DO UPDATE SET
1148
+ schema_version = EXCLUDED.schema_version,
1149
+ record = EXCLUDED.record,
1150
+ updated_at = EXCLUDED.updated_at`,
1151
+ [kind, encodeURIComponent(key), JSON.stringify(record), updatedAt]
1152
+ );
1153
+ }
1154
+ async compareAndSaveTriage(key, expectedRevision, record, updatedAt) {
1155
+ const encodedKey = encodeURIComponent(key);
1156
+ const result = expectedRevision === void 0 ? await this.client.query(
1157
+ `INSERT INTO shipyard_workflow_phase_records
1158
+ (namespace, record_key, schema_version, record, updated_at)
1159
+ VALUES ('triage', $1, 1, $2::jsonb, $3::timestamptz)
1160
+ ON CONFLICT (namespace, record_key) DO NOTHING
1161
+ RETURNING record_key`,
1162
+ [encodedKey, JSON.stringify(record), updatedAt]
1163
+ ) : await this.client.query(
1164
+ `UPDATE shipyard_workflow_phase_records
1165
+ SET schema_version = 1, record = $3::jsonb, updated_at = $4::timestamptz
1166
+ WHERE namespace = 'triage'
1167
+ AND record_key = $1
1168
+ AND record->>'revision' = $2
1169
+ RETURNING record_key`,
1170
+ [
1171
+ encodedKey,
1172
+ String(expectedRevision),
1173
+ JSON.stringify(record),
1174
+ updatedAt
1175
+ ]
1176
+ );
1177
+ return result.rows.length > 0;
1178
+ }
1179
+ };
1180
+ var nonEmpty = (value, path) => {
1181
+ if (typeof value !== "string" || value.trim().length === 0) {
1182
+ throw new Error(`${path} must be a non-empty string`);
1183
+ }
1184
+ return value.trim();
1185
+ };
1186
+ var optionalNonEmpty = (value, path) => value === void 0 ? void 0 : nonEmpty(value, path);
1187
+ var isCategory = (value) => typeof value === "string" && [
1188
+ "bug",
1189
+ "enhancement",
1190
+ "support",
1191
+ "duplicate",
1192
+ "sensitive",
1193
+ "non-actionable"
1194
+ ].includes(value);
1195
+ var isRisk = (value) => typeof value === "string" && ["low", "medium", "high", "critical"].includes(value);
1196
+ var stringArray2 = (value, path) => {
1197
+ if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
1198
+ return value.map((entry, index) => nonEmpty(entry, `${path}[${index}]`));
1199
+ };
1200
+ var sha2562 = (value) => createHash("sha256").update(value).digest("hex");
1201
+ var sourceKind = (source) => source.kind ?? "executable-issue";
1202
+ var sourceKey = (source) => [source.repository, source.itemId, sourceKind(source)].join("\0");
1203
+ var sourceFingerprint = (source) => sha2562(
1204
+ JSON.stringify({
1205
+ provider: source.provider,
1206
+ repository: source.repository,
1207
+ itemId: source.itemId,
1208
+ title: source.title,
1209
+ body: source.body,
1210
+ author: source.author ?? "",
1211
+ url: source.url ?? "",
1212
+ updatedAt: source.updatedAt,
1213
+ kind: sourceKind(source),
1214
+ labels: [...source.labels ?? []].sort()
1215
+ })
1216
+ );
1217
+ var sourceTimestampOrder = (candidateUpdatedAt, storedUpdatedAt) => {
1218
+ const candidateTime = Date.parse(candidateUpdatedAt);
1219
+ const storedTime = Date.parse(storedUpdatedAt);
1220
+ if (!Number.isFinite(candidateTime) || !Number.isFinite(storedTime)) {
1221
+ return "unknown";
1222
+ }
1223
+ return candidateTime < storedTime ? "older" : candidateTime > storedTime ? "newer" : "same";
1224
+ };
1225
+ var storedObject = (value, path) => {
1226
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1227
+ throw new Error(`${path} must be an object`);
1228
+ }
1229
+ return value;
1230
+ };
1231
+ var parseStoredSource = (value) => {
1232
+ const source = storedObject(value, "triage.source");
1233
+ const provider = source.provider;
1234
+ if (provider !== "github" && provider !== "slack" && provider !== "manual") {
1235
+ throw new Error("triage.source.provider is invalid");
1236
+ }
1237
+ const allowedKinds = [
1238
+ "planning-spec",
1239
+ "executable-issue",
1240
+ "pr-repair"
1241
+ ];
1242
+ const kind = source.kind ?? "executable-issue";
1243
+ if (typeof kind !== "string" || !allowedKinds.includes(kind)) {
1244
+ throw new Error("triage.source.kind is invalid");
1245
+ }
1246
+ if (typeof source.body !== "string") {
1247
+ throw new Error("triage.source.body must be a string");
1248
+ }
1249
+ if (source.labels !== void 0 && !Array.isArray(source.labels)) {
1250
+ throw new Error("triage.source.labels must be an array");
1251
+ }
1252
+ return normalizeSource({
1253
+ provider,
1254
+ repository: nonEmpty(source.repository, "triage.source.repository"),
1255
+ itemId: nonEmpty(source.itemId, "triage.source.itemId"),
1256
+ title: nonEmpty(source.title, "triage.source.title"),
1257
+ body: source.body,
1258
+ author: optionalNonEmpty(source.author, "triage.source.author"),
1259
+ url: optionalNonEmpty(source.url, "triage.source.url"),
1260
+ updatedAt: nonEmpty(source.updatedAt, "triage.source.updatedAt"),
1261
+ kind,
1262
+ labels: stringArray2(source.labels ?? [], "triage.source.labels")
1263
+ });
1264
+ };
1265
+ var parseClarificationReply = (value, path) => {
1266
+ const reply = storedObject(value, path);
1267
+ if (typeof reply.body !== "string") {
1268
+ throw new Error(`${path}.body must be a string`);
1269
+ }
1270
+ return {
1271
+ id: nonEmpty(reply.id, `${path}.id`),
1272
+ body: reply.body,
1273
+ author: optionalNonEmpty(reply.author, `${path}.author`),
1274
+ updatedAt: nonEmpty(reply.updatedAt, `${path}.updatedAt`)
1275
+ };
1276
+ };
1277
+ var normalizeClarificationReply = (reply) => parseClarificationReply(reply, "clarificationReply");
1278
+ var parseTriageRecord = (value) => {
1279
+ const record = storedObject(value, "triage record");
1280
+ const source = parseStoredSource(record.source);
1281
+ const assessment = normalizeAssessment(record.assessment);
1282
+ const outcome = record.outcome;
1283
+ const outcomes = [
1284
+ "completed",
1285
+ "needs-info",
1286
+ "duplicate",
1287
+ "sensitive",
1288
+ "non-actionable",
1289
+ "blocked",
1290
+ "failed"
1291
+ ];
1292
+ if (typeof outcome !== "string" || !outcomes.includes(outcome)) {
1293
+ throw new Error("triage.outcome is invalid");
1294
+ }
1295
+ if (!isCategory(record.category))
1296
+ throw new Error("triage.category is invalid");
1297
+ const revision = record.revision;
1298
+ if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 1) {
1299
+ throw new Error("triage.revision must be a positive integer");
1300
+ }
1301
+ const parsedBrief = record.brief === void 0 ? void 0 : parseWorkBrief(record.brief);
1302
+ const clarificationIds = stringArray2(
1303
+ record.clarificationIds,
1304
+ "triage.clarificationIds"
1305
+ );
1306
+ const pendingClarificationReplies = record.pendingClarificationReplies === void 0 ? [] : (() => {
1307
+ if (!Array.isArray(record.pendingClarificationReplies)) {
1308
+ throw new Error(
1309
+ "triage.pendingClarificationReplies must be an array"
1310
+ );
1311
+ }
1312
+ return record.pendingClarificationReplies.map(
1313
+ (reply, index) => parseClarificationReply(
1314
+ reply,
1315
+ `triage.pendingClarificationReplies[${index}]`
1316
+ )
1317
+ );
1318
+ })();
1319
+ const sourceConflict = record.sourceConflict === void 0 ? void 0 : (() => {
1320
+ const conflict = storedObject(
1321
+ record.sourceConflict,
1322
+ "triage.sourceConflict"
1323
+ );
1324
+ const fingerprint = nonEmpty(
1325
+ conflict.fingerprint,
1326
+ "triage.sourceConflict.fingerprint"
1327
+ );
1328
+ if (!/^[0-9a-f]{64}$/.test(fingerprint)) {
1329
+ throw new Error("triage.sourceConflict.fingerprint is invalid");
1330
+ }
1331
+ return {
1332
+ fingerprint,
1333
+ updatedAt: nonEmpty(
1334
+ conflict.updatedAt,
1335
+ "triage.sourceConflict.updatedAt"
1336
+ )
1337
+ };
1338
+ })();
1339
+ const pendingIds = pendingClarificationReplies.map(({ id }) => id);
1340
+ if (new Set(pendingIds).size !== pendingIds.length || pendingIds.some((id) => clarificationIds.includes(id))) {
1341
+ throw new Error("Stored triage clarification replies are not unique");
1342
+ }
1343
+ const parsed = {
1344
+ id: nonEmpty(record.id, "triage.id"),
1345
+ sourceKey: nonEmpty(record.sourceKey, "triage.sourceKey"),
1346
+ source,
1347
+ sourceUpdatedAt: nonEmpty(record.sourceUpdatedAt, "triage.sourceUpdatedAt"),
1348
+ sourceFingerprint: nonEmpty(
1349
+ record.sourceFingerprint,
1350
+ "triage.sourceFingerprint"
1351
+ ),
1352
+ revision,
1353
+ category: record.category,
1354
+ outcome,
1355
+ assessment,
1356
+ brief: parsedBrief,
1357
+ questions: stringArray2(record.questions, "triage.questions"),
1358
+ clarificationIds,
1359
+ ...pendingClarificationReplies.length === 0 ? {} : { pendingClarificationReplies },
1360
+ ...sourceConflict === void 0 ? {} : { sourceConflict },
1361
+ duplicateOf: optionalNonEmpty(record.duplicateOf, "triage.duplicateOf"),
1362
+ publicMessage: nonEmpty(record.publicMessage, "triage.publicMessage"),
1363
+ createdAt: nonEmpty(record.createdAt, "triage.createdAt"),
1364
+ updatedAt: nonEmpty(record.updatedAt, "triage.updatedAt")
1365
+ };
1366
+ if (parsed.sourceKey !== sourceKey(source)) {
1367
+ throw new Error("Stored triage source key does not match its source");
1368
+ }
1369
+ if (parsed.sourceUpdatedAt !== source.updatedAt) {
1370
+ throw new Error("Stored triage source timestamp does not match its source");
1371
+ }
1372
+ if (parsed.sourceFingerprint !== sourceFingerprint(source)) {
1373
+ throw new Error("Stored triage fingerprint does not match its source");
1374
+ }
1375
+ if (parsed.category !== assessment.category) {
1376
+ throw new Error("Stored triage category does not match its assessment");
1377
+ }
1378
+ if (sourceConflict !== void 0 && (parsed.outcome !== "blocked" || parsedBrief !== void 0 || sourceConflict.fingerprint === parsed.sourceFingerprint)) {
1379
+ throw new Error("Stored triage source conflict is inconsistent");
1380
+ }
1381
+ if (parsedBrief !== void 0 && (parsedBrief.revision !== revision || parsedBrief.identity.repository !== source.repository || parsedBrief.identity.itemId !== source.itemId || parsedBrief.identity.kind !== sourceKind(source))) {
1382
+ throw new Error("Stored triage brief does not match its source revision");
1383
+ }
1384
+ return parsed;
1385
+ };
1386
+ var normalizeSource = (source) => ({
1387
+ ...source,
1388
+ repository: nonEmpty(source.repository, "source.repository"),
1389
+ itemId: nonEmpty(source.itemId, "source.itemId"),
1390
+ title: nonEmpty(source.title, "source.title"),
1391
+ body: typeof source.body === "string" ? source.body : "",
1392
+ author: optionalNonEmpty(source.author, "source.author"),
1393
+ url: optionalNonEmpty(source.url, "source.url"),
1394
+ updatedAt: nonEmpty(source.updatedAt, "source.updatedAt"),
1395
+ kind: sourceKind(source),
1396
+ labels: [...source.labels ?? []].map(
1397
+ (label, index) => nonEmpty(label, `source.labels[${index}]`)
1398
+ )
1399
+ });
1400
+ var normalizeAssessment = (value) => {
1401
+ const assessment = storedObject(value, "assessment");
1402
+ if (!isCategory(assessment.category))
1403
+ throw new Error("assessment.category is invalid");
1404
+ if (!isRisk(assessment.risk)) throw new Error("assessment.risk is invalid");
1405
+ if (typeof assessment.requirementsConfirmed !== "boolean") {
1406
+ throw new Error("assessment.requirementsConfirmed must be a boolean");
1407
+ }
1408
+ const duplicateOf = optionalNonEmpty(
1409
+ assessment.duplicateOf,
1410
+ "assessment.duplicateOf"
1411
+ );
1412
+ const sensitiveReason = optionalNonEmpty(
1413
+ assessment.sensitiveReason,
1414
+ "assessment.sensitiveReason"
1415
+ );
1416
+ return {
1417
+ category: assessment.category,
1418
+ evidence: stringArray2(assessment.evidence, "assessment.evidence"),
1419
+ relevantFiles: stringArray2(
1420
+ assessment.relevantFiles,
1421
+ "assessment.relevantFiles"
1422
+ ),
1423
+ acceptanceCriteria: stringArray2(
1424
+ assessment.acceptanceCriteria,
1425
+ "assessment.acceptanceCriteria"
1426
+ ),
1427
+ exclusions: stringArray2(assessment.exclusions, "assessment.exclusions"),
1428
+ risk: assessment.risk,
1429
+ verification: stringArray2(
1430
+ assessment.verification,
1431
+ "assessment.verification"
1432
+ ),
1433
+ unresolvedQuestions: stringArray2(
1434
+ assessment.unresolvedQuestions,
1435
+ "assessment.unresolvedQuestions"
1436
+ ),
1437
+ requirementsConfirmed: assessment.requirementsConfirmed,
1438
+ duplicateOf,
1439
+ sensitiveReason
1440
+ };
1441
+ };
1442
+
1443
+ // src/workflow/triage/reconciliation.ts
1444
+ var reconcileTriageSource = (input) => {
1445
+ const { source, sourceFingerprint: sourceFingerprint2, existing, incomingReply } = input;
1446
+ const timestampOrder = existing === void 0 ? "newer" : sourceTimestampOrder(source.updatedAt, existing.sourceUpdatedAt);
1447
+ const sourceChanged = existing !== void 0 && existing.sourceFingerprint !== sourceFingerprint2;
1448
+ const sourceTimestampAmbiguous = sourceChanged && timestampOrder !== "older" && timestampOrder !== "newer";
1449
+ const replyAlreadyApplied = incomingReply !== void 0 && (existing?.clarificationIds.includes(incomingReply.id) ?? false);
1450
+ const storedReplies = existing?.pendingClarificationReplies ?? [];
1451
+ const pendingIds = new Set(storedReplies.map(({ id }) => id));
1452
+ const newReply = incomingReply !== void 0 && !replyAlreadyApplied && !pendingIds.has(incomingReply.id) ? incomingReply : void 0;
1453
+ const pendingReplies = newReply === void 0 ? [...storedReplies] : [...storedReplies, newReply];
1454
+ const priorConflictIsUnresolved = existing?.sourceConflict !== void 0 && timestampOrder !== "newer";
1455
+ if (existing !== void 0 && (sourceTimestampAmbiguous || priorConflictIsUnresolved)) {
1456
+ const sourceConflict = sourceTimestampAmbiguous ? { fingerprint: sourceFingerprint2, updatedAt: source.updatedAt } : existing.sourceConflict;
1457
+ if (existing.sourceConflict?.fingerprint === sourceConflict.fingerprint && existing.outcome === "blocked" && existing.brief === void 0 && newReply === void 0) {
1458
+ return { kind: "unchanged", record: existing };
1459
+ }
1460
+ return {
1461
+ kind: "source-conflict",
1462
+ existing,
1463
+ sourceConflict,
1464
+ pendingReplies
1465
+ };
1466
+ }
1467
+ const storedSnapshotWins = existing !== void 0 && (existing.sourceFingerprint === sourceFingerprint2 || timestampOrder !== "newer");
1468
+ const currentSource = storedSnapshotWins ? existing.source : source;
1469
+ const currentFingerprint = storedSnapshotWins ? existing.sourceFingerprint : sourceFingerprint2;
1470
+ if (existing !== void 0 && pendingReplies.length === 0 && existing.sourceFingerprint === currentFingerprint) {
1471
+ return { kind: "unchanged", record: existing };
1472
+ }
1473
+ return {
1474
+ kind: "investigate",
1475
+ source: currentSource,
1476
+ sourceFingerprint: currentFingerprint,
1477
+ pendingReplies
1478
+ };
1479
+ };
1480
+
1481
+ // src/workflow/triage/index.ts
1482
+ var InMemoryTriageStore = class {
1483
+ records = /* @__PURE__ */ new Map();
1484
+ get(sourceKey2) {
1485
+ const record = this.records.get(sourceKey2);
1486
+ return record === void 0 ? void 0 : clone(record);
1487
+ }
1488
+ compareAndSave(record, expectedRevision) {
1489
+ const current = this.records.get(record.sourceKey);
1490
+ if (current?.revision !== expectedRevision) return false;
1491
+ this.records.set(record.sourceKey, clone(record));
1492
+ return true;
1493
+ }
1494
+ };
1495
+ var PostgresTriageStore = class {
1496
+ records;
1497
+ constructor(options) {
1498
+ this.records = new PostgresWorkflowPhaseRecordStore(options);
1499
+ }
1500
+ async get(sourceKey2) {
1501
+ const record = await this.records.get("triage", sourceKey2);
1502
+ return record === void 0 ? void 0 : parseTriageRecord(record);
1503
+ }
1504
+ compareAndSave(record, expectedRevision) {
1505
+ return this.records.compareAndSaveTriage(
1506
+ record.sourceKey,
1507
+ expectedRevision,
1508
+ record,
1509
+ record.updatedAt
1510
+ );
1511
+ }
1512
+ };
1513
+ var defaultNow = () => (/* @__PURE__ */ new Date()).toISOString();
1514
+ var clone = (value) => JSON.parse(JSON.stringify(value));
1515
+ var MAX_TRIAGE_WRITE_CONFLICT_RETRIES = 5;
1516
+ var safeDuplicateId = (value) => {
1517
+ if (value === void 0 || !/^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,79}$/.test(value)) {
1518
+ return void 0;
1519
+ }
1520
+ return value;
1521
+ };
1522
+ var defaultInvestigator = {
1523
+ async investigate({ source }) {
1524
+ const text = `${source.title}
1525
+ ${source.body}`.toLowerCase();
1526
+ const labels = new Set(
1527
+ (source.labels ?? []).map((label) => label.toLowerCase())
1528
+ );
1529
+ const duplicateMatch = text.match(
1530
+ /(?:duplicate|dupe)(?: of|:|#)\s*([\w./:-]+)/i
1531
+ );
1532
+ const category = labels.has("sensitive") || /\b(secret|token|password|credential|security report)\b/.test(text) ? "sensitive" : labels.has("duplicate") || duplicateMatch !== null ? "duplicate" : labels.has("support") || /\?|\b(how do i|support|question)\b/.test(text) ? "support" : labels.has("bug") || /\b(bug|fix|error|crash|broken|fail(?:ed|ure)?)\b/.test(text) ? "bug" : "enhancement";
1533
+ const highRisk = /\b(auth|billing|payment|migration|destructive|security)\b/.test(text);
1534
+ const hasExplicitRequirements = /\b(acceptance|expected|should|must|given|when|then)\b/.test(text) && source.body.trim().length > 0;
1535
+ return {
1536
+ category,
1537
+ evidence: [
1538
+ "Classification derived from the submitted title, body, and labels."
1539
+ ],
1540
+ relevantFiles: [],
1541
+ acceptanceCriteria: hasExplicitRequirements ? ["Implement only the explicitly described behavior."] : [],
1542
+ exclusions: [
1543
+ "Do not expand scope beyond the retained source and confirmed answers."
1544
+ ],
1545
+ risk: highRisk ? "high" : "low",
1546
+ verification: [],
1547
+ unresolvedQuestions: hasExplicitRequirements ? [] : [
1548
+ "What observable behavior should change, and how will it be accepted?"
1549
+ ],
1550
+ requirementsConfirmed: hasExplicitRequirements,
1551
+ duplicateOf: safeDuplicateId(duplicateMatch?.[1])
1552
+ };
1553
+ }
1554
+ };
1555
+ var investigate = (investigator, request) => typeof investigator === "function" ? investigator(request) : investigator.investigate(request);
1556
+ var canAutoAuthorize = (source, assessment, policy) => sourceKind(source) === "executable-issue" && !policy.authorization.required && policy.authorization.allowedActors.includes("policy") && policy.authorization.autoStartRisk.includes(assessment.risk) && assessment.requirementsConfirmed && assessment.unresolvedQuestions.length === 0 && assessment.acceptanceCriteria.length > 0 && assessment.category !== "support" && assessment.category !== "duplicate" && assessment.category !== "sensitive" && assessment.category !== "non-actionable";
1557
+ var outcomeForAssessment = (source, assessment) => {
1558
+ const category = assessment.category;
1559
+ const questions = assessment.unresolvedQuestions;
1560
+ if (sourceKind(source) !== "executable-issue") return "blocked";
1561
+ if (category === "duplicate") return "duplicate";
1562
+ if (category === "sensitive") return "sensitive";
1563
+ if (category === "non-actionable") return "non-actionable";
1564
+ return !assessment.requirementsConfirmed || questions.length > 0 || assessment.acceptanceCriteria.length === 0 ? "needs-info" : "completed";
1565
+ };
1566
+ var sourceConflictMessage = (hasPendingReplies) => hasPendingReplies ? "Triage is blocked because the source update cannot be ordered against the saved revision; the clarification reply is saved and will be applied after the source is refreshed." : "Triage is blocked because the source update cannot be ordered against the saved revision; refresh the source before triage can continue.";
1567
+ var publicMessage = (outcome, category, questions, duplicateId) => {
1568
+ switch (outcome) {
1569
+ case "needs-info":
1570
+ return `Triage needs clarification (${questions.length} question${questions.length === 1 ? "" : "s"}); the work item remains paused.`;
1571
+ case "duplicate":
1572
+ return duplicateId === void 0 ? "Triage routed this report as a duplicate for maintainer review." : `Triage routed this report as a duplicate of #${duplicateId}.`;
1573
+ case "sensitive":
1574
+ return "Triage routed this report to the private security channel; sensitive details are not reproduced here.";
1575
+ case "non-actionable":
1576
+ return "Triage routed this report for maintainer disposition.";
1577
+ case "blocked":
1578
+ return "Triage recorded the work item but ordinary implementation dispatch is blocked for this item type.";
1579
+ case "failed":
1580
+ return "Triage could not complete; a maintainer must inspect the failure without relying on source text in public output.";
1581
+ case "completed":
1582
+ return category === "support" ? "Triage recorded this support request for a maintainer response." : "Triage completed; implementation authorization remains a separate policy decision.";
1583
+ }
1584
+ };
1585
+ var makeBrief = (source, assessment, policy, base, revision, createdAt) => {
1586
+ const originalBody = source.body.trim().length > 0 ? source.body : "(empty source body)";
1587
+ const authorization = canAutoAuthorize(source, assessment, policy) ? {
1588
+ status: "approved",
1589
+ actor: "policy",
1590
+ actorRole: "policy",
1591
+ approvedAt: createdAt
1592
+ } : { status: "pending" };
1593
+ return createWorkBrief({
1594
+ id: `${source.repository}:${sourceKind(source)}:${source.itemId}`,
1595
+ revision,
1596
+ identity: {
1597
+ repository: source.repository,
1598
+ itemId: source.itemId,
1599
+ kind: sourceKind(source)
1600
+ },
1601
+ source: {
1602
+ provider: source.provider,
1603
+ repository: source.repository,
1604
+ itemId: source.itemId,
1605
+ url: source.url,
1606
+ originalBody,
1607
+ author: source.author
1608
+ },
1609
+ problem: source.title,
1610
+ evidence: assessment.evidence,
1611
+ acceptanceCriteria: assessment.acceptanceCriteria,
1612
+ exclusions: assessment.exclusions,
1613
+ risk: assessment.risk,
1614
+ verification: {
1615
+ checks: assessment.verification,
1616
+ artifacts: assessment.relevantFiles
1617
+ },
1618
+ unresolvedQuestions: assessment.unresolvedQuestions,
1619
+ authorization,
1620
+ base,
1621
+ policyRevision: policy.revision,
1622
+ skillRevision: policy.worker.skillRevision,
1623
+ createdAt
1624
+ });
1625
+ };
1626
+ var resultFromRecord = (record, policy) => ({
1627
+ outcome: record.outcome,
1628
+ category: record.category,
1629
+ brief: record.brief,
1630
+ questions: record.questions,
1631
+ publicMessage: record.publicMessage,
1632
+ record,
1633
+ implementationEligible: record.outcome === "completed" && record.category !== "support" && record.brief?.identity.kind === "executable-issue" && record.brief !== void 0 && record.brief.policyRevision === policy.revision && isAuthorizationAllowed(record.brief, policy)
1634
+ });
1635
+ var runTriage = async ({
1636
+ source: rawSource,
1637
+ policy,
1638
+ base,
1639
+ store,
1640
+ investigator = defaultInvestigator,
1641
+ clarificationReply,
1642
+ now = defaultNow
1643
+ }) => {
1644
+ const source = normalizeSource(rawSource);
1645
+ if (source.repository !== policy.repository) {
1646
+ throw new Error("source.repository must match policy.repository");
1647
+ }
1648
+ const incomingReply = clarificationReply === void 0 ? void 0 : normalizeClarificationReply(clarificationReply);
1649
+ const key = sourceKey(source);
1650
+ const fingerprint = sourceFingerprint(source);
1651
+ for (let conflictAttempt = 0; conflictAttempt <= MAX_TRIAGE_WRITE_CONFLICT_RETRIES; conflictAttempt += 1) {
1652
+ const existing = await store.get(key);
1653
+ const reconciliation = reconcileTriageSource({
1654
+ source,
1655
+ sourceFingerprint: fingerprint,
1656
+ existing,
1657
+ incomingReply
1658
+ });
1659
+ if (reconciliation.kind === "unchanged") {
1660
+ return resultFromRecord(reconciliation.record, policy);
1661
+ }
1662
+ if (reconciliation.kind === "source-conflict") {
1663
+ const { existing: existing2, sourceConflict, pendingReplies } = reconciliation;
1664
+ const timestamp2 = now();
1665
+ const record2 = {
1666
+ id: existing2.id,
1667
+ sourceKey: key,
1668
+ source: existing2.source,
1669
+ sourceUpdatedAt: existing2.sourceUpdatedAt,
1670
+ sourceFingerprint: existing2.sourceFingerprint,
1671
+ revision: existing2.revision + 1,
1672
+ category: existing2.category,
1673
+ outcome: "blocked",
1674
+ assessment: existing2.assessment,
1675
+ questions: [],
1676
+ clarificationIds: existing2.clarificationIds,
1677
+ ...pendingReplies.length === 0 ? {} : { pendingClarificationReplies: pendingReplies },
1678
+ sourceConflict,
1679
+ publicMessage: sourceConflictMessage(pendingReplies.length > 0),
1680
+ duplicateOf: existing2.duplicateOf,
1681
+ createdAt: existing2.createdAt,
1682
+ updatedAt: timestamp2
1683
+ };
1684
+ if (await store.compareAndSave(record2, existing2.revision)) {
1685
+ return resultFromRecord(record2, policy);
1686
+ }
1687
+ continue;
1688
+ }
1689
+ const {
1690
+ source: currentSource,
1691
+ sourceFingerprint: currentFingerprint,
1692
+ pendingReplies: currentPendingReplies
1693
+ } = reconciliation;
1694
+ const timestamp = now();
1695
+ let assessment;
1696
+ try {
1697
+ let previous = existing;
1698
+ if (currentPendingReplies.length === 0) {
1699
+ assessment = normalizeAssessment(
1700
+ await investigate(investigator, {
1701
+ source: currentSource,
1702
+ policy,
1703
+ base,
1704
+ previous
1705
+ })
1706
+ );
1707
+ } else {
1708
+ for (const [index, reply] of currentPendingReplies.entries()) {
1709
+ assessment = normalizeAssessment(
1710
+ await investigate(investigator, {
1711
+ source: currentSource,
1712
+ policy,
1713
+ base,
1714
+ previous,
1715
+ clarificationReply: reply
1716
+ })
1717
+ );
1718
+ const processedReplyIds = currentPendingReplies.slice(0, index + 1).map(({ id }) => id);
1719
+ const remainingReplies = currentPendingReplies.slice(index + 1);
1720
+ const previousOutcome = outcomeForAssessment(
1721
+ currentSource,
1722
+ assessment
1723
+ );
1724
+ const previousCategory = assessment.category;
1725
+ const previousDuplicateId = safeDuplicateId(assessment.duplicateOf);
1726
+ previous = {
1727
+ id: existing?.id ?? `${currentSource.repository}:${sourceKind(currentSource)}:${currentSource.itemId}`,
1728
+ sourceKey: key,
1729
+ source: currentSource,
1730
+ sourceUpdatedAt: currentSource.updatedAt,
1731
+ sourceFingerprint: currentFingerprint,
1732
+ revision: existing?.revision ?? 1,
1733
+ category: previousCategory,
1734
+ outcome: previousOutcome,
1735
+ assessment,
1736
+ questions: assessment.unresolvedQuestions,
1737
+ clarificationIds: [
1738
+ ...existing?.clarificationIds ?? [],
1739
+ ...processedReplyIds
1740
+ ],
1741
+ ...remainingReplies.length === 0 ? {} : { pendingClarificationReplies: remainingReplies },
1742
+ duplicateOf: previousDuplicateId,
1743
+ publicMessage: publicMessage(
1744
+ previousOutcome,
1745
+ previousCategory,
1746
+ assessment.unresolvedQuestions,
1747
+ previousDuplicateId
1748
+ ),
1749
+ createdAt: existing?.createdAt ?? timestamp,
1750
+ updatedAt: timestamp
1751
+ };
1752
+ }
1753
+ }
1754
+ } catch {
1755
+ const failedAssessment = {
1756
+ category: "non-actionable",
1757
+ evidence: [
1758
+ "The investigator did not return a usable structured assessment."
1759
+ ],
1760
+ relevantFiles: [],
1761
+ acceptanceCriteria: [],
1762
+ exclusions: [
1763
+ "No implementation may start from an incomplete assessment."
1764
+ ],
1765
+ risk: "high",
1766
+ verification: [],
1767
+ unresolvedQuestions: [],
1768
+ requirementsConfirmed: false
1769
+ };
1770
+ const record2 = {
1771
+ id: existing?.id ?? `${currentSource.repository}:${sourceKind(currentSource)}:${currentSource.itemId}`,
1772
+ sourceKey: key,
1773
+ source: currentSource,
1774
+ sourceUpdatedAt: currentSource.updatedAt,
1775
+ sourceFingerprint: currentFingerprint,
1776
+ revision: existing === void 0 ? 1 : existing.revision + 1,
1777
+ category: failedAssessment.category,
1778
+ outcome: "failed",
1779
+ assessment: failedAssessment,
1780
+ questions: [],
1781
+ clarificationIds: existing?.clarificationIds ?? [],
1782
+ ...currentPendingReplies.length === 0 ? {} : { pendingClarificationReplies: currentPendingReplies },
1783
+ publicMessage: publicMessage("failed", failedAssessment.category, []),
1784
+ createdAt: existing?.createdAt ?? timestamp,
1785
+ updatedAt: timestamp
1786
+ };
1787
+ if (await store.compareAndSave(record2, existing?.revision)) {
1788
+ return resultFromRecord(record2, policy);
1789
+ }
1790
+ continue;
1791
+ }
1792
+ if (assessment === void 0) {
1793
+ throw new Error("Triage investigator returned no assessment");
1794
+ }
1795
+ const questions = assessment.unresolvedQuestions;
1796
+ const category = assessment.category;
1797
+ const duplicateId = safeDuplicateId(assessment.duplicateOf);
1798
+ const outcome = outcomeForAssessment(currentSource, assessment);
1799
+ const nextRevision = existing === void 0 ? 1 : existing.revision + 1;
1800
+ const brief = makeBrief(
1801
+ currentSource,
1802
+ assessment,
1803
+ policy,
1804
+ base,
1805
+ nextRevision,
1806
+ existing?.createdAt ?? timestamp
1807
+ );
1808
+ const record = {
1809
+ id: existing?.id ?? brief.id,
1810
+ sourceKey: key,
1811
+ source: currentSource,
1812
+ sourceUpdatedAt: currentSource.updatedAt,
1813
+ sourceFingerprint: currentFingerprint,
1814
+ revision: nextRevision,
1815
+ category,
1816
+ outcome,
1817
+ assessment,
1818
+ brief,
1819
+ questions,
1820
+ clarificationIds: [
1821
+ ...existing?.clarificationIds ?? [],
1822
+ ...currentPendingReplies.map(({ id }) => id)
1823
+ ],
1824
+ duplicateOf: duplicateId,
1825
+ publicMessage: publicMessage(outcome, category, questions, duplicateId),
1826
+ createdAt: existing?.createdAt ?? timestamp,
1827
+ updatedAt: timestamp
1828
+ };
1829
+ if (await store.compareAndSave(record, existing?.revision)) {
1830
+ return resultFromRecord(record, policy);
1831
+ }
1832
+ }
1833
+ throw new Error(
1834
+ `Triage record for ${source.repository} item ${source.itemId} changed during ${MAX_TRIAGE_WRITE_CONFLICT_RETRIES + 1} consecutive updates`
1835
+ );
1836
+ };
1837
+
1838
+ export { ContractValidationError, InMemoryTriageStore, PostgresTriageStore, PostgresWorkflowPhaseRecordStore, WORKFLOW_CONTRACT_VERSION, closeSourceIssue, completeSourceIssue, createAssignment, createRepositoryPolicy, createWorkBrief, deepFreeze, defaultInvestigator, evaluateHandoffReadiness, isAuthorizationAllowed, isBlockingFinding, mergeProtectedCandidate, parseCheckEvidence, parsePhaseResult, parseRepositoryPolicy, parseWorkBrief, prepareHumanHandoff, processHumanReviewDecision, requireTransition, requiredCheckNames, resolveAgentSelection, resolveHumanReviewDecision, runTriage, sameRevision, sameWorkIdentity };
1839
+ //# sourceMappingURL=chunk-NQRFVKCU.js.map
1840
+ //# sourceMappingURL=chunk-NQRFVKCU.js.map