@snappedly-tools/shipyard 0.8.0 → 0.9.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/README.md +24 -14
- package/dist/MountConfig-K5ILnfht.d.ts +26 -0
- package/dist/{MountConfig-BHnKnA4h.d.ts → SandboxProvider-oUAwYlWm.d.ts} +1 -26
- package/dist/{chunk-L6PX5QTU.js → chunk-57EEKW3R.js} +57 -55
- package/dist/chunk-57EEKW3R.js.map +1 -0
- package/dist/{chunk-JI3HDDMS.js → chunk-HXSZM52J.js} +3 -3
- package/dist/{chunk-JI3HDDMS.js.map → chunk-HXSZM52J.js.map} +1 -1
- package/dist/{chunk-44I2BL6E.js → chunk-JZUBT4WG.js} +20 -4
- package/dist/chunk-JZUBT4WG.js.map +1 -0
- package/dist/chunk-NQRFVKCU.js +1840 -0
- package/dist/chunk-NQRFVKCU.js.map +1 -0
- package/dist/chunk-Z5C4LHVP.js +137 -0
- package/dist/chunk-Z5C4LHVP.js.map +1 -0
- package/dist/createSandbox-DmbnWAZv.d.ts +739 -0
- package/dist/index.d.ts +7 -2534
- package/dist/index.js +216 -7269
- package/dist/index.js.map +1 -1
- package/dist/integrations/github.d.ts +52 -0
- package/dist/integrations/github.js +1049 -0
- package/dist/integrations/github.js.map +1 -0
- package/dist/integrations/releases.d.ts +136 -0
- package/dist/integrations/releases.js +500 -0
- package/dist/integrations/releases.js.map +1 -0
- package/dist/main.js +641 -383
- package/dist/main.js.map +1 -1
- package/dist/publication-BPoy_M9M.d.ts +1200 -0
- package/dist/sandboxes/docker.d.ts +2 -1
- package/dist/sandboxes/docker.js +2 -2
- package/dist/templates/parallel-planner/main.mts +11 -7
- package/dist/templates/parallel-planner/setup.sh +1 -0
- package/dist/templates/parallel-planner-with-review/main.mts +11 -7
- package/dist/templates/parallel-planner-with-review/setup.sh +1 -0
- package/dist/templates/sequential-reviewer/main.mts +7 -3
- package/dist/templates/sequential-reviewer/setup.sh +1 -0
- package/dist/templates/shared/setup.sh +1 -0
- package/dist/templates/simple-loop/main.mts +7 -3
- package/dist/templates/simple-loop/setup.sh +1 -0
- package/dist/workflow/coordinator/migrations/002_workflow_phase_records.sql +11 -0
- package/dist/workflow/coordinator/migrations/003_phase_record_schema_version.sql +6 -0
- package/dist/workflow.d.ts +472 -0
- package/dist/workflow.js +4345 -0
- package/dist/workflow.js.map +1 -0
- package/package.json +13 -1
- package/dist/chunk-44I2BL6E.js.map +0 -1
- 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
|