@abelxiaoxing/cadence 1.2.1 → 1.2.2
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/agents/implementation-worker.md +9 -2
- package/package.json +1 -1
- package/src/index.ts +57 -9
- package/src/run-store.ts +17 -0
- package/src/submit-tool.ts +250 -30
- package/src/workflow-engine.ts +566 -26
|
@@ -20,8 +20,15 @@ Your output is one complete structured patch delivered through `abel_submit_resu
|
|
|
20
20
|
- every operation path is within the declared phase boundary; paths that do
|
|
21
21
|
not need a change may remain untouched;
|
|
22
22
|
- preserve the expected parent-owned verification contract without claiming a result;
|
|
23
|
-
- use a typed `context-request` for bounded context
|
|
24
|
-
|
|
23
|
+
- use a typed `context-request` for bounded context or task splitting instead
|
|
24
|
+
of submitting a partial patch; classify every ref as a requested path with
|
|
25
|
+
`read | write` access, a source citation with path and line, or a
|
|
26
|
+
contract-diagnostic ref;
|
|
27
|
+
- report only the missing context and structural facts: never select
|
|
28
|
+
`approval-needed`, `boundary-review-needed`, a Gate, or a workflow stage;
|
|
29
|
+
- an `AGENTS.md` path is never a Worker write path;
|
|
30
|
+
- report an AGENTS read need separately, and leave every sealed AGENTS contract
|
|
31
|
+
and managed-block update to the parent control plane.
|
|
25
32
|
|
|
26
33
|
If the submit tool rejects malformed operations, use its concrete error to correct the submission once in this same disposable session; a second rejected structural submission ends the session.
|
|
27
34
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abelxiaoxing/cadence",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Four explicit Abel workflow prompts with stage-isolated design and diagnosis, durable resumable implementation, and three package-owned professional Agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/index.ts
CHANGED
|
@@ -1525,19 +1525,46 @@ export function openPackageWorkflowControlEngine(
|
|
|
1525
1525
|
if (!phase) {
|
|
1526
1526
|
return { kind: "paused", code: "task-phase-unavailable" };
|
|
1527
1527
|
}
|
|
1528
|
+
const taskPhases = Object.values(input.task.phases);
|
|
1529
|
+
const executionBoundary = input.artifactCorrection
|
|
1530
|
+
? {
|
|
1531
|
+
read: [
|
|
1532
|
+
...new Set(
|
|
1533
|
+
taskPhases.flatMap((boundary) => [
|
|
1534
|
+
...boundary.read,
|
|
1535
|
+
...boundary.write,
|
|
1536
|
+
...boundary.delete,
|
|
1537
|
+
]),
|
|
1538
|
+
),
|
|
1539
|
+
].sort(),
|
|
1540
|
+
write: [
|
|
1541
|
+
...new Set(taskPhases.flatMap((boundary) => boundary.write)),
|
|
1542
|
+
].sort(),
|
|
1543
|
+
delete: [
|
|
1544
|
+
...new Set(taskPhases.flatMap((boundary) => boundary.delete)),
|
|
1545
|
+
].sort(),
|
|
1546
|
+
}
|
|
1547
|
+
: {
|
|
1548
|
+
read: [...phase.read],
|
|
1549
|
+
write: [...phase.write],
|
|
1550
|
+
delete: [...phase.delete],
|
|
1551
|
+
};
|
|
1528
1552
|
const phaseContract = {
|
|
1529
1553
|
candidateId: input.candidateArtifact.identity.candidateId,
|
|
1530
1554
|
taskId: input.taskId,
|
|
1531
1555
|
phase: input.phase,
|
|
1532
|
-
readSet:
|
|
1533
|
-
writeSet:
|
|
1534
|
-
deleteSet:
|
|
1556
|
+
readSet: executionBoundary.read,
|
|
1557
|
+
writeSet: executionBoundary.write,
|
|
1558
|
+
deleteSet: executionBoundary.delete,
|
|
1535
1559
|
verification: structuredClone(phase.verification),
|
|
1536
1560
|
agentsImpact: input.task.agents.impact,
|
|
1537
1561
|
agentsTarget: input.task.agents.target ?? null,
|
|
1538
1562
|
agentsManagedOnly: true,
|
|
1539
1563
|
agentsWriteAllowed: false,
|
|
1540
1564
|
impactClosure: structuredClone(input.task.impactClosure),
|
|
1565
|
+
...(input.artifactCorrection
|
|
1566
|
+
? { artifactCorrection: structuredClone(input.artifactCorrection) }
|
|
1567
|
+
: {}),
|
|
1541
1568
|
...(input.repair
|
|
1542
1569
|
? {
|
|
1543
1570
|
repair: {
|
|
@@ -1570,7 +1597,11 @@ export function openPackageWorkflowControlEngine(
|
|
|
1570
1597
|
path.resolve(input.workspaceRoot, root),
|
|
1571
1598
|
),
|
|
1572
1599
|
allowedPaths: [
|
|
1573
|
-
...new Set([
|
|
1600
|
+
...new Set([
|
|
1601
|
+
...executionBoundary.read,
|
|
1602
|
+
...executionBoundary.write,
|
|
1603
|
+
...executionBoundary.delete,
|
|
1604
|
+
]),
|
|
1574
1605
|
],
|
|
1575
1606
|
timeoutMs: LIMITS.phaseTimeoutMs,
|
|
1576
1607
|
signal: input.signal,
|
|
@@ -1600,11 +1631,28 @@ export function openPackageWorkflowControlEngine(
|
|
|
1600
1631
|
}
|
|
1601
1632
|
const result = child.result;
|
|
1602
1633
|
if (result.kind === "context-request") {
|
|
1603
|
-
return classifyCandidateContextRequest(result,
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1634
|
+
return classifyCandidateContextRequest(result, {
|
|
1635
|
+
phase: input.phase,
|
|
1636
|
+
readPaths: phase.read,
|
|
1637
|
+
writePaths: phase.write,
|
|
1638
|
+
deletePaths: phase.delete,
|
|
1639
|
+
taskPaths: [
|
|
1640
|
+
...new Set(
|
|
1641
|
+
Object.values(input.task.phases).flatMap((boundary) => [
|
|
1642
|
+
...boundary.read,
|
|
1643
|
+
...boundary.write,
|
|
1644
|
+
...boundary.delete,
|
|
1645
|
+
]),
|
|
1646
|
+
),
|
|
1647
|
+
],
|
|
1648
|
+
redWritePaths: input.task.phases.red.write,
|
|
1649
|
+
agents: {
|
|
1650
|
+
impact: input.task.agents.impact,
|
|
1651
|
+
...(input.task.agents.target
|
|
1652
|
+
? { target: input.task.agents.target }
|
|
1653
|
+
: {}),
|
|
1654
|
+
},
|
|
1655
|
+
});
|
|
1608
1656
|
}
|
|
1609
1657
|
if (result.kind !== "sealed-candidate") {
|
|
1610
1658
|
return { kind: "retryable", code: "candidate-diff-invalid" };
|
package/src/run-store.ts
CHANGED
|
@@ -917,7 +917,23 @@ export class RunStore {
|
|
|
917
917
|
code?: string;
|
|
918
918
|
lease?: OperationLease;
|
|
919
919
|
}): RunProjection {
|
|
920
|
+
return this.transitionAtomically(input, () => {});
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
transitionAtomically(
|
|
924
|
+
input: {
|
|
925
|
+
runId: string;
|
|
926
|
+
to: RunState;
|
|
927
|
+
operationId: string;
|
|
928
|
+
code?: string;
|
|
929
|
+
lease?: OperationLease;
|
|
930
|
+
},
|
|
931
|
+
write: (database: DatabaseSync) => void,
|
|
932
|
+
): RunProjection {
|
|
920
933
|
requireIdentifier(input.operationId, "operation-id");
|
|
934
|
+
if (typeof write !== "function") {
|
|
935
|
+
throw new Error("state-transition-write-invalid");
|
|
936
|
+
}
|
|
921
937
|
return this.#transaction(() => {
|
|
922
938
|
if (input.lease) {
|
|
923
939
|
if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
|
|
@@ -935,6 +951,7 @@ export class RunStore {
|
|
|
935
951
|
to: input.to,
|
|
936
952
|
...(input.code ? { code: input.code } : {}),
|
|
937
953
|
});
|
|
954
|
+
write(this.#database);
|
|
938
955
|
this.#recordOperation(
|
|
939
956
|
input.runId,
|
|
940
957
|
input.operationId,
|
package/src/submit-tool.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type CandidateFailure,
|
|
10
10
|
type DiffResult,
|
|
11
11
|
type EvidenceResult,
|
|
12
|
+
isAgentsPath,
|
|
12
13
|
isValidRelativePath,
|
|
13
14
|
type SubmitFinalCategory,
|
|
14
15
|
type SubmitSchemaState,
|
|
@@ -144,15 +145,57 @@ function candidateArtifactSchema(candidateId: string) {
|
|
|
144
145
|
code: Type.Union([
|
|
145
146
|
Type.Literal("approved-context-needed"),
|
|
146
147
|
Type.Literal("task-split-needed"),
|
|
147
|
-
Type.Literal("boundary-review-needed"),
|
|
148
148
|
]),
|
|
149
|
-
refs: Type.Array(
|
|
149
|
+
refs: Type.Array(
|
|
150
|
+
Type.Union([
|
|
151
|
+
Type.String(),
|
|
152
|
+
Type.Object(
|
|
153
|
+
{
|
|
154
|
+
kind: Type.Literal("requested-path"),
|
|
155
|
+
path: Type.String(),
|
|
156
|
+
access: Type.Union([
|
|
157
|
+
Type.Literal("read"),
|
|
158
|
+
Type.Literal("write"),
|
|
159
|
+
]),
|
|
160
|
+
},
|
|
161
|
+
{ additionalProperties: false },
|
|
162
|
+
),
|
|
163
|
+
Type.Object(
|
|
164
|
+
{
|
|
165
|
+
kind: Type.Literal("source-citation"),
|
|
166
|
+
path: Type.String(),
|
|
167
|
+
line: Type.Integer({ minimum: 1 }),
|
|
168
|
+
},
|
|
169
|
+
{ additionalProperties: false },
|
|
170
|
+
),
|
|
171
|
+
Type.Object(
|
|
172
|
+
{
|
|
173
|
+
kind: Type.Literal("contract-diagnostic"),
|
|
174
|
+
ref: Type.String(),
|
|
175
|
+
},
|
|
176
|
+
{ additionalProperties: false },
|
|
177
|
+
),
|
|
178
|
+
]),
|
|
179
|
+
{ minItems: 1, maxItems: 32 },
|
|
180
|
+
),
|
|
150
181
|
},
|
|
151
182
|
{ additionalProperties: false },
|
|
152
183
|
),
|
|
153
184
|
]);
|
|
154
185
|
}
|
|
155
186
|
|
|
187
|
+
export type CandidateContextRequestCode =
|
|
188
|
+
| "approved-context-needed"
|
|
189
|
+
| "task-split-needed"
|
|
190
|
+
| "boundary-review-needed";
|
|
191
|
+
|
|
192
|
+
export type CandidateContextRef =
|
|
193
|
+
| { kind: "requested-path"; path: string; access: "read" | "write" }
|
|
194
|
+
| { kind: "source-citation"; path: string; line: number }
|
|
195
|
+
| { kind: "contract-diagnostic"; ref: string };
|
|
196
|
+
|
|
197
|
+
export type CandidateContextRefInput = string | CandidateContextRef;
|
|
198
|
+
|
|
156
199
|
export type CandidateArtifactSubmissionResult =
|
|
157
200
|
| {
|
|
158
201
|
kind: "sealed-candidate";
|
|
@@ -166,11 +209,8 @@ export type CandidateArtifactSubmissionResult =
|
|
|
166
209
|
| {
|
|
167
210
|
kind: "context-request";
|
|
168
211
|
candidateId: string;
|
|
169
|
-
code:
|
|
170
|
-
|
|
171
|
-
| "task-split-needed"
|
|
172
|
-
| "boundary-review-needed";
|
|
173
|
-
refs: string[];
|
|
212
|
+
code: CandidateContextRequestCode;
|
|
213
|
+
refs: CandidateContextRef[];
|
|
174
214
|
};
|
|
175
215
|
|
|
176
216
|
export type CandidateContextRequest = Extract<
|
|
@@ -178,18 +218,114 @@ export type CandidateContextRequest = Extract<
|
|
|
178
218
|
{ kind: "context-request" }
|
|
179
219
|
>;
|
|
180
220
|
|
|
221
|
+
export interface CandidateContextBoundary {
|
|
222
|
+
phase: "red" | "green" | "refactor";
|
|
223
|
+
readPaths: readonly string[];
|
|
224
|
+
writePaths: readonly string[];
|
|
225
|
+
deletePaths?: readonly string[];
|
|
226
|
+
taskPaths: readonly string[];
|
|
227
|
+
redWritePaths: readonly string[];
|
|
228
|
+
agents: {
|
|
229
|
+
impact: "none" | "update-existing" | "create-index" | "remove-index";
|
|
230
|
+
target?: string;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
181
234
|
export interface ClassifiedCandidateContextRequest {
|
|
182
|
-
kind: "paused" | "approval-needed";
|
|
235
|
+
kind: "paused" | "retryable" | "approval-needed";
|
|
183
236
|
code:
|
|
184
237
|
| "approved-context-needed"
|
|
185
238
|
| "task-split-needed"
|
|
186
|
-
| "boundary-review-needed"
|
|
239
|
+
| "boundary-review-needed"
|
|
240
|
+
| "red-artifact-constraint"
|
|
241
|
+
| "agents-context-needed"
|
|
242
|
+
| "agents-write-parent-owned"
|
|
243
|
+
| "agents-contract-insufficient";
|
|
187
244
|
contextRequest: {
|
|
188
|
-
code:
|
|
189
|
-
refs:
|
|
245
|
+
code: CandidateContextRequestCode;
|
|
246
|
+
refs: CandidateContextRef[];
|
|
190
247
|
};
|
|
191
248
|
}
|
|
192
249
|
|
|
250
|
+
const LEGACY_CITATION = /^(.*):(\d+)$/u;
|
|
251
|
+
const LEGACY_DIAGNOSTIC =
|
|
252
|
+
/^(?:phase-contract|task-contract|sealed-contract|workflow-contract)\.[a-z0-9._-]+$/iu;
|
|
253
|
+
|
|
254
|
+
function validDiagnosticRef(value: unknown): value is string {
|
|
255
|
+
return (
|
|
256
|
+
typeof value === "string" &&
|
|
257
|
+
value.length > 0 &&
|
|
258
|
+
value.length <= 512 &&
|
|
259
|
+
value.trim() === value &&
|
|
260
|
+
!/[\r\n\0]/u.test(value)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function normalizeCandidateContextRef(
|
|
265
|
+
input: CandidateContextRefInput,
|
|
266
|
+
): CandidateContextRef {
|
|
267
|
+
if (typeof input === "string") {
|
|
268
|
+
const citation = LEGACY_CITATION.exec(input);
|
|
269
|
+
if (citation && isValidRelativePath(citation[1])) {
|
|
270
|
+
const line = Number(citation[2]);
|
|
271
|
+
if (Number.isSafeInteger(line) && line >= 1) {
|
|
272
|
+
return { kind: "source-citation", path: citation[1], line };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (LEGACY_DIAGNOSTIC.test(input)) {
|
|
276
|
+
return { kind: "contract-diagnostic", ref: input };
|
|
277
|
+
}
|
|
278
|
+
if (!isValidRelativePath(input)) {
|
|
279
|
+
throw new Error("context request refs are invalid");
|
|
280
|
+
}
|
|
281
|
+
return { kind: "requested-path", path: input, access: "read" };
|
|
282
|
+
}
|
|
283
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
284
|
+
throw new Error("context request refs are invalid");
|
|
285
|
+
}
|
|
286
|
+
if (
|
|
287
|
+
input.kind === "requested-path" &&
|
|
288
|
+
Object.keys(input).length === 3 &&
|
|
289
|
+
isValidRelativePath(input.path) &&
|
|
290
|
+
(input.access === "read" || input.access === "write")
|
|
291
|
+
) {
|
|
292
|
+
return { ...input };
|
|
293
|
+
}
|
|
294
|
+
if (
|
|
295
|
+
input.kind === "source-citation" &&
|
|
296
|
+
Object.keys(input).length === 3 &&
|
|
297
|
+
isValidRelativePath(input.path) &&
|
|
298
|
+
Number.isSafeInteger(input.line) &&
|
|
299
|
+
input.line >= 1
|
|
300
|
+
) {
|
|
301
|
+
return { ...input };
|
|
302
|
+
}
|
|
303
|
+
if (
|
|
304
|
+
input.kind === "contract-diagnostic" &&
|
|
305
|
+
Object.keys(input).length === 2 &&
|
|
306
|
+
validDiagnosticRef(input.ref)
|
|
307
|
+
) {
|
|
308
|
+
return { ...input };
|
|
309
|
+
}
|
|
310
|
+
throw new Error("context request refs are invalid");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function normalizeCandidateContextRefs(
|
|
314
|
+
value: unknown,
|
|
315
|
+
): CandidateContextRef[] {
|
|
316
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 32) {
|
|
317
|
+
throw new Error("context request refs are invalid");
|
|
318
|
+
}
|
|
319
|
+
const refs = value.map((entry) =>
|
|
320
|
+
normalizeCandidateContextRef(entry as CandidateContextRefInput),
|
|
321
|
+
);
|
|
322
|
+
const identities = refs.map((ref) => JSON.stringify(ref));
|
|
323
|
+
if (new Set(identities).size !== refs.length) {
|
|
324
|
+
throw new Error("context request refs are invalid");
|
|
325
|
+
}
|
|
326
|
+
return refs;
|
|
327
|
+
}
|
|
328
|
+
|
|
193
329
|
function contextRefWithinBoundary(relative: string, approved: string): boolean {
|
|
194
330
|
return (
|
|
195
331
|
approved === "." ||
|
|
@@ -198,23 +334,108 @@ function contextRefWithinBoundary(relative: string, approved: string): boolean {
|
|
|
198
334
|
);
|
|
199
335
|
}
|
|
200
336
|
|
|
337
|
+
function normalizedBoundary(
|
|
338
|
+
boundary: readonly string[] | CandidateContextBoundary,
|
|
339
|
+
): CandidateContextBoundary {
|
|
340
|
+
if (!Array.isArray(boundary)) return boundary as CandidateContextBoundary;
|
|
341
|
+
return {
|
|
342
|
+
phase: "green",
|
|
343
|
+
readPaths: boundary,
|
|
344
|
+
writePaths: [],
|
|
345
|
+
deletePaths: [],
|
|
346
|
+
taskPaths: boundary,
|
|
347
|
+
redWritePaths: [],
|
|
348
|
+
agents: { impact: "none" },
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function pathWithinAny(relative: string, approved: readonly string[]): boolean {
|
|
353
|
+
return approved.some((path) => contextRefWithinBoundary(relative, path));
|
|
354
|
+
}
|
|
355
|
+
|
|
201
356
|
export function classifyCandidateContextRequest(
|
|
202
|
-
request:
|
|
203
|
-
|
|
357
|
+
request: {
|
|
358
|
+
kind: "context-request";
|
|
359
|
+
candidateId: string;
|
|
360
|
+
code: CandidateContextRequestCode;
|
|
361
|
+
refs: readonly CandidateContextRefInput[];
|
|
362
|
+
},
|
|
363
|
+
inputBoundary: readonly string[] | CandidateContextBoundary,
|
|
204
364
|
): ClassifiedCandidateContextRequest {
|
|
205
|
-
const refs =
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
365
|
+
const refs = normalizeCandidateContextRefs(request.refs);
|
|
366
|
+
const boundary = normalizedBoundary(inputBoundary);
|
|
367
|
+
const phasePaths = [
|
|
368
|
+
...boundary.readPaths,
|
|
369
|
+
...boundary.writePaths,
|
|
370
|
+
...(boundary.deletePaths ?? []),
|
|
371
|
+
];
|
|
372
|
+
const requested = refs.filter(
|
|
373
|
+
(ref): ref is Extract<CandidateContextRef, { kind: "requested-path" }> =>
|
|
374
|
+
ref.kind === "requested-path",
|
|
375
|
+
);
|
|
376
|
+
const ordinaryRequested = requested.filter((ref) => !isAgentsPath(ref.path));
|
|
377
|
+
const outsideOrdinary = ordinaryRequested.filter(
|
|
378
|
+
(ref) =>
|
|
379
|
+
!pathWithinAny(
|
|
380
|
+
ref.path,
|
|
381
|
+
ref.access === "write"
|
|
382
|
+
? [...boundary.writePaths, ...(boundary.deletePaths ?? [])]
|
|
383
|
+
: phasePaths,
|
|
211
384
|
),
|
|
385
|
+
);
|
|
386
|
+
const agentsRequested = requested.filter((ref) => isAgentsPath(ref.path));
|
|
387
|
+
const agentsWrites = agentsRequested.filter((ref) => ref.access === "write");
|
|
388
|
+
const citedRedArtifacts = refs.filter(
|
|
389
|
+
(ref): ref is Extract<CandidateContextRef, { kind: "source-citation" }> =>
|
|
390
|
+
ref.kind === "source-citation" &&
|
|
391
|
+
pathWithinAny(ref.path, boundary.redWritePaths) &&
|
|
392
|
+
!pathWithinAny(ref.path, [
|
|
393
|
+
...boundary.writePaths,
|
|
394
|
+
...(boundary.deletePaths ?? []),
|
|
395
|
+
]),
|
|
396
|
+
);
|
|
397
|
+
const requestedRedArtifacts = outsideOrdinary.filter(
|
|
398
|
+
(ref) =>
|
|
399
|
+
pathWithinAny(ref.path, boundary.redWritePaths) &&
|
|
400
|
+
pathWithinAny(ref.path, boundary.taskPaths),
|
|
401
|
+
);
|
|
402
|
+
const redArtifactConstraint =
|
|
403
|
+
boundary.phase === "green" &&
|
|
404
|
+
(citedRedArtifacts.length > 0 || requestedRedArtifacts.length > 0) &&
|
|
405
|
+
outsideOrdinary.every((ref) => requestedRedArtifacts.includes(ref)) &&
|
|
406
|
+
agentsRequested.every((ref) => ref.access === "read");
|
|
407
|
+
|
|
408
|
+
let classification: Pick<ClassifiedCandidateContextRequest, "kind" | "code">;
|
|
409
|
+
if (outsideOrdinary.length > 0 && !redArtifactConstraint) {
|
|
410
|
+
classification = {
|
|
411
|
+
kind: "approval-needed",
|
|
412
|
+
code: "boundary-review-needed",
|
|
413
|
+
};
|
|
414
|
+
} else if (agentsWrites.length > 0) {
|
|
415
|
+
const targetMismatch = agentsWrites.some(
|
|
416
|
+
(ref) =>
|
|
417
|
+
boundary.agents.impact === "none" ||
|
|
418
|
+
boundary.agents.target === undefined ||
|
|
419
|
+
ref.path !== boundary.agents.target,
|
|
212
420
|
);
|
|
213
|
-
|
|
214
|
-
|
|
421
|
+
classification = targetMismatch
|
|
422
|
+
? { kind: "approval-needed", code: "agents-contract-insufficient" }
|
|
423
|
+
: { kind: "paused", code: "agents-write-parent-owned" };
|
|
424
|
+
} else if (redArtifactConstraint) {
|
|
425
|
+
classification = { kind: "retryable", code: "red-artifact-constraint" };
|
|
426
|
+
} else if (agentsRequested.length > 0) {
|
|
427
|
+
classification = { kind: "paused", code: "agents-context-needed" };
|
|
428
|
+
} else {
|
|
429
|
+
classification = {
|
|
430
|
+
kind: "paused",
|
|
431
|
+
code:
|
|
432
|
+
request.code === "task-split-needed"
|
|
433
|
+
? "task-split-needed"
|
|
434
|
+
: "approved-context-needed",
|
|
435
|
+
};
|
|
436
|
+
}
|
|
215
437
|
return {
|
|
216
|
-
|
|
217
|
-
code: inBoundary ? request.code : "boundary-review-needed",
|
|
438
|
+
...classification,
|
|
218
439
|
contextRequest: {
|
|
219
440
|
code: request.code,
|
|
220
441
|
refs,
|
|
@@ -391,18 +612,17 @@ export function createCandidateArtifactTool(
|
|
|
391
612
|
terminate: true,
|
|
392
613
|
};
|
|
393
614
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
refs
|
|
397
|
-
|
|
398
|
-
) {
|
|
615
|
+
let refs: CandidateContextRef[];
|
|
616
|
+
try {
|
|
617
|
+
refs = normalizeCandidateContextRefs(value.refs);
|
|
618
|
+
} catch (error) {
|
|
399
619
|
schema = "invalid";
|
|
400
620
|
failure = {
|
|
401
621
|
kind: "artifact",
|
|
402
622
|
code: "invalid-structural-result",
|
|
403
623
|
stage: "structural-submit",
|
|
404
624
|
};
|
|
405
|
-
throw
|
|
625
|
+
throw error;
|
|
406
626
|
}
|
|
407
627
|
result = {
|
|
408
628
|
kind: "context-request",
|
|
@@ -411,7 +631,7 @@ export function createCandidateArtifactTool(
|
|
|
411
631
|
CandidateArtifactSubmissionResult,
|
|
412
632
|
{ kind: "context-request" }
|
|
413
633
|
>["code"],
|
|
414
|
-
refs
|
|
634
|
+
refs,
|
|
415
635
|
};
|
|
416
636
|
return {
|
|
417
637
|
content: [
|
package/src/workflow-engine.ts
CHANGED
|
@@ -45,7 +45,13 @@ import { type OperationLease, RunStore } from "./run-store.ts";
|
|
|
45
45
|
import { observeSafePath } from "./safe-path.ts";
|
|
46
46
|
import type { ResolvedStateRoot } from "./state-root.ts";
|
|
47
47
|
import type { WorkflowActivityUpdate } from "./subagent-activity.ts";
|
|
48
|
-
import
|
|
48
|
+
import {
|
|
49
|
+
type CandidateArtifactSubmission,
|
|
50
|
+
type CandidateContextBoundary,
|
|
51
|
+
type CandidateContextRef,
|
|
52
|
+
classifyCandidateContextRequest,
|
|
53
|
+
normalizeCandidateContextRefs,
|
|
54
|
+
} from "./submit-tool.ts";
|
|
49
55
|
import {
|
|
50
56
|
type BeginCandidateInput,
|
|
51
57
|
TASK_LEDGER_LIMITS,
|
|
@@ -171,7 +177,14 @@ interface WorkflowContextRequest {
|
|
|
171
177
|
| "approved-context-needed"
|
|
172
178
|
| "task-split-needed"
|
|
173
179
|
| "boundary-review-needed";
|
|
174
|
-
refs:
|
|
180
|
+
refs: CandidateContextRef[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface RedArtifactCorrection {
|
|
184
|
+
code: "red-artifact-constraint";
|
|
185
|
+
attempt: number;
|
|
186
|
+
maxAttempts: number;
|
|
187
|
+
contextRequest?: WorkflowContextRequest;
|
|
175
188
|
}
|
|
176
189
|
|
|
177
190
|
export const APPROVAL_AUTHORITY_CATEGORIES = [
|
|
@@ -296,6 +309,7 @@ export interface WorkflowWorker {
|
|
|
296
309
|
attribution: "introduced";
|
|
297
310
|
failureIdentities: string[];
|
|
298
311
|
};
|
|
312
|
+
artifactCorrection?: RedArtifactCorrection;
|
|
299
313
|
signal: AbortSignal;
|
|
300
314
|
onActivity?: (
|
|
301
315
|
event: Pick<
|
|
@@ -568,24 +582,72 @@ function normalizeWorkflowContextRequest(
|
|
|
568
582
|
"approved-context-needed",
|
|
569
583
|
"task-split-needed",
|
|
570
584
|
"boundary-review-needed",
|
|
571
|
-
].includes(String(value.code))
|
|
572
|
-
!Array.isArray(value.refs) ||
|
|
573
|
-
value.refs.length === 0 ||
|
|
574
|
-
value.refs.length > 32 ||
|
|
575
|
-
value.refs.some(
|
|
576
|
-
(relative) =>
|
|
577
|
-
typeof relative !== "string" || !isValidRelativePath(relative),
|
|
578
|
-
) ||
|
|
579
|
-
new Set(value.refs).size !== value.refs.length
|
|
585
|
+
].includes(String(value.code))
|
|
580
586
|
) {
|
|
581
587
|
throw new Error("workflow-context-request-invalid");
|
|
582
588
|
}
|
|
589
|
+
let refs: CandidateContextRef[];
|
|
590
|
+
try {
|
|
591
|
+
refs = normalizeCandidateContextRefs(value.refs);
|
|
592
|
+
} catch {
|
|
593
|
+
throw new Error("workflow-context-request-invalid");
|
|
594
|
+
}
|
|
583
595
|
return {
|
|
584
596
|
code: value.code as WorkflowContextRequest["code"],
|
|
585
|
-
refs
|
|
597
|
+
refs,
|
|
586
598
|
};
|
|
587
599
|
}
|
|
588
600
|
|
|
601
|
+
function contextApprovalRefs(request: WorkflowContextRequest): string[] {
|
|
602
|
+
return request.refs.flatMap((ref) =>
|
|
603
|
+
ref.kind === "requested-path" ? [ref.path] : [],
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function contextBoundaryForTask(
|
|
608
|
+
task: PlanTaskDraft,
|
|
609
|
+
phase: "red" | "green" | "refactor",
|
|
610
|
+
): CandidateContextBoundary {
|
|
611
|
+
const boundary = task.phases[phase];
|
|
612
|
+
if (!boundary) throw new Error("workflow-task-phase-invalid");
|
|
613
|
+
return {
|
|
614
|
+
phase,
|
|
615
|
+
readPaths: boundary.read,
|
|
616
|
+
writePaths: boundary.write,
|
|
617
|
+
deletePaths: boundary.delete,
|
|
618
|
+
taskPaths: [
|
|
619
|
+
...new Set(
|
|
620
|
+
Object.values(task.phases).flatMap((candidate) => [
|
|
621
|
+
...candidate.read,
|
|
622
|
+
...candidate.write,
|
|
623
|
+
...candidate.delete,
|
|
624
|
+
]),
|
|
625
|
+
),
|
|
626
|
+
],
|
|
627
|
+
redWritePaths: task.phases.red.write,
|
|
628
|
+
agents: {
|
|
629
|
+
impact: task.agents.impact,
|
|
630
|
+
...(task.agents.target ? { target: task.agents.target } : {}),
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function classifyPersistedContextRequest(
|
|
636
|
+
request: WorkflowContextRequest,
|
|
637
|
+
task: PlanTaskDraft,
|
|
638
|
+
phase: "red" | "green" | "refactor",
|
|
639
|
+
) {
|
|
640
|
+
return classifyCandidateContextRequest(
|
|
641
|
+
{
|
|
642
|
+
kind: "context-request",
|
|
643
|
+
candidateId: "candidate-durable-reclassification",
|
|
644
|
+
code: request.code,
|
|
645
|
+
refs: request.refs,
|
|
646
|
+
},
|
|
647
|
+
contextBoundaryForTask(task, phase),
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
589
651
|
function approvalRequirement(
|
|
590
652
|
code: string,
|
|
591
653
|
contextRequest?: WorkflowContextRequest,
|
|
@@ -599,7 +661,7 @@ function approvalRequirement(
|
|
|
599
661
|
return {
|
|
600
662
|
category: requirement.category,
|
|
601
663
|
requiredGates: [...requirement.requiredGates],
|
|
602
|
-
refs: contextRequest ?
|
|
664
|
+
refs: contextRequest ? contextApprovalRefs(contextRequest) : [],
|
|
603
665
|
};
|
|
604
666
|
}
|
|
605
667
|
|
|
@@ -1282,6 +1344,7 @@ export interface DurableWorkflowEngineOptions {
|
|
|
1282
1344
|
attribution: "introduced";
|
|
1283
1345
|
failureIdentities: string[];
|
|
1284
1346
|
};
|
|
1347
|
+
artifactCorrection?: RedArtifactCorrection;
|
|
1285
1348
|
signal: AbortSignal;
|
|
1286
1349
|
onHeaders(): void;
|
|
1287
1350
|
onProgress(): void;
|
|
@@ -2319,6 +2382,9 @@ class DurableWorkflowComposition
|
|
|
2319
2382
|
ledger: TaskLedger,
|
|
2320
2383
|
input: Parameters<WorkflowWorker["runAttempt"]>[0],
|
|
2321
2384
|
): WorkflowAttemptOutcome | undefined {
|
|
2385
|
+
if (input.artifactCorrection?.code === "red-artifact-constraint") {
|
|
2386
|
+
return undefined;
|
|
2387
|
+
}
|
|
2322
2388
|
const projection = ledger.projection({
|
|
2323
2389
|
runId: input.runId,
|
|
2324
2390
|
taskId: input.taskId,
|
|
@@ -2925,6 +2991,9 @@ class DurableWorkflowComposition
|
|
|
2925
2991
|
},
|
|
2926
2992
|
route: attempt.route,
|
|
2927
2993
|
repair,
|
|
2994
|
+
...(request.artifactCorrection
|
|
2995
|
+
? { artifactCorrection: request.artifactCorrection }
|
|
2996
|
+
: {}),
|
|
2928
2997
|
signal: attempt.signal,
|
|
2929
2998
|
onHeaders: attempt.onHeaders,
|
|
2930
2999
|
onProgress: attempt.onProgress,
|
|
@@ -2972,7 +3041,8 @@ class DurableWorkflowComposition
|
|
|
2972
3041
|
proposal.kind !== "sealed-candidate"
|
|
2973
3042
|
) {
|
|
2974
3043
|
return proposal.kind === "operation-cancelled" ||
|
|
2975
|
-
proposal.kind === "paused"
|
|
3044
|
+
proposal.kind === "paused" ||
|
|
3045
|
+
proposal.kind === "approval-needed"
|
|
2976
3046
|
? withRoute(proposal)
|
|
2977
3047
|
: reject(proposal, {
|
|
2978
3048
|
category: "artifact",
|
|
@@ -3213,7 +3283,9 @@ class DurableWorkflowComposition
|
|
|
3213
3283
|
changes: revisionChanges(proposalRoot, approvedPaths),
|
|
3214
3284
|
});
|
|
3215
3285
|
const requiredOutputs = request.plan.outputs.filter(
|
|
3216
|
-
(output) =>
|
|
3286
|
+
(output) =>
|
|
3287
|
+
output.producer.taskId === request.taskId &&
|
|
3288
|
+
(!request.artifactCorrection || output.producer.phase !== "refactor"),
|
|
3217
3289
|
);
|
|
3218
3290
|
if (
|
|
3219
3291
|
requiredOutputs.some(
|
|
@@ -3352,7 +3424,7 @@ class DurableWorkflowComposition
|
|
|
3352
3424
|
#verifiedPhaseReplay(
|
|
3353
3425
|
ledger: TaskLedger,
|
|
3354
3426
|
input: Parameters<WorkflowWorker["runAttempt"]>[0],
|
|
3355
|
-
): { artifactHash: string; exitCode: number } {
|
|
3427
|
+
): { artifactHash: string; exitCode: number; isolatedRevisionId: string } {
|
|
3356
3428
|
const projection = ledger.projection({
|
|
3357
3429
|
runId: input.runId,
|
|
3358
3430
|
taskId: input.taskId,
|
|
@@ -3366,6 +3438,8 @@ class DurableWorkflowComposition
|
|
|
3366
3438
|
!event ||
|
|
3367
3439
|
typeof event.artifactHash !== "string" ||
|
|
3368
3440
|
!SHA256.test(event.artifactHash) ||
|
|
3441
|
+
typeof event.isolatedRevisionId !== "string" ||
|
|
3442
|
+
!SHA256.test(event.isolatedRevisionId) ||
|
|
3369
3443
|
typeof event.exitCode !== "number" ||
|
|
3370
3444
|
event.actualClassification !== expectedClassification(input.phase)
|
|
3371
3445
|
) {
|
|
@@ -3374,6 +3448,285 @@ class DurableWorkflowComposition
|
|
|
3374
3448
|
return {
|
|
3375
3449
|
artifactHash: event.artifactHash,
|
|
3376
3450
|
exitCode: event.exitCode,
|
|
3451
|
+
isolatedRevisionId: event.isolatedRevisionId,
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
async #runRedArtifactCorrection(input: {
|
|
3456
|
+
resources: DurableRunResources;
|
|
3457
|
+
ledger: TaskLedger;
|
|
3458
|
+
baseline?: DurableVerificationBaseline;
|
|
3459
|
+
attempt: Parameters<WorkflowWorker["runAttempt"]>[0];
|
|
3460
|
+
}): Promise<WorkflowAttemptOutcome> {
|
|
3461
|
+
const { resources, ledger } = input;
|
|
3462
|
+
const request = input.attempt;
|
|
3463
|
+
if (
|
|
3464
|
+
request.phase !== "green" ||
|
|
3465
|
+
request.artifactCorrection?.code !== "red-artifact-constraint"
|
|
3466
|
+
) {
|
|
3467
|
+
throw new Error("workflow-red-artifact-correction-invalid");
|
|
3468
|
+
}
|
|
3469
|
+
const priorRed = this.#verifiedPhaseReplay(ledger, {
|
|
3470
|
+
...request,
|
|
3471
|
+
phase: "red",
|
|
3472
|
+
});
|
|
3473
|
+
if (!hasVerificationLifecycle(request.plan)) {
|
|
3474
|
+
return { kind: "paused", code: "red-artifact-constraint" };
|
|
3475
|
+
}
|
|
3476
|
+
const stableRevisionId = resources.currentRevisionId;
|
|
3477
|
+
const correctionPaths = [
|
|
3478
|
+
...new Set(
|
|
3479
|
+
Object.values(request.task.phases).flatMap((phase) => [
|
|
3480
|
+
...phase.write,
|
|
3481
|
+
...phase.delete,
|
|
3482
|
+
]),
|
|
3483
|
+
),
|
|
3484
|
+
];
|
|
3485
|
+
const rejectCorrection = async <T extends WorkflowAttemptOutcome>(
|
|
3486
|
+
outcome: T,
|
|
3487
|
+
): Promise<T> => {
|
|
3488
|
+
if (resources.currentRevisionId !== stableRevisionId) {
|
|
3489
|
+
await this.#rollbackWorkspacePaths({
|
|
3490
|
+
resources,
|
|
3491
|
+
baseRevisionId: stableRevisionId,
|
|
3492
|
+
rejectedRevisionId: resources.currentRevisionId,
|
|
3493
|
+
paths: correctionPaths,
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
return outcome;
|
|
3497
|
+
};
|
|
3498
|
+
const failureIdentities = [
|
|
3499
|
+
hash(
|
|
3500
|
+
"red-artifact-constraint",
|
|
3501
|
+
request.taskId,
|
|
3502
|
+
request.task.phases.red.verification.id,
|
|
3503
|
+
),
|
|
3504
|
+
];
|
|
3505
|
+
const correctionAttemptUsed = request.artifactCorrection.attempt;
|
|
3506
|
+
if (
|
|
3507
|
+
correctionAttemptUsed < 1 ||
|
|
3508
|
+
correctionAttemptUsed > request.artifactCorrection.maxAttempts
|
|
3509
|
+
) {
|
|
3510
|
+
throw new Error("workflow-red-artifact-correction-invalid");
|
|
3511
|
+
}
|
|
3512
|
+
const repaired = await this.#runRepairCandidate({
|
|
3513
|
+
resources,
|
|
3514
|
+
ledger,
|
|
3515
|
+
attempt: request,
|
|
3516
|
+
repairAttempt: correctionAttemptUsed,
|
|
3517
|
+
failureIdentities,
|
|
3518
|
+
});
|
|
3519
|
+
if (repaired.kind !== "repair-committed") {
|
|
3520
|
+
return rejectCorrection(repaired);
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
const correctedRevision = resources.workspaces.getRevision(
|
|
3524
|
+
repaired.commit.isolatedRevisionId,
|
|
3525
|
+
);
|
|
3526
|
+
const acceptedRedRevision = resources.workspaces.getRevision(
|
|
3527
|
+
priorRed.isolatedRevisionId,
|
|
3528
|
+
);
|
|
3529
|
+
if (!acceptedRedRevision.parentRevisionId) {
|
|
3530
|
+
throw new Error("workflow-ledger-revision-base-invalid");
|
|
3531
|
+
}
|
|
3532
|
+
const correctedRedChanges: Record<
|
|
3533
|
+
string,
|
|
3534
|
+
{ kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
|
|
3535
|
+
> = {};
|
|
3536
|
+
for (const relative of [
|
|
3537
|
+
...new Set([
|
|
3538
|
+
...request.task.phases.red.write,
|
|
3539
|
+
...request.task.phases.red.delete,
|
|
3540
|
+
]),
|
|
3541
|
+
]) {
|
|
3542
|
+
const entry = correctedRevision.entries[relative];
|
|
3543
|
+
correctedRedChanges[relative] =
|
|
3544
|
+
entry?.kind === "file"
|
|
3545
|
+
? {
|
|
3546
|
+
kind: "file",
|
|
3547
|
+
bytes: resources.artifacts.read(entry.hash),
|
|
3548
|
+
mode: entry.mode,
|
|
3549
|
+
}
|
|
3550
|
+
: { kind: "absent" };
|
|
3551
|
+
}
|
|
3552
|
+
const correctedRedRevision = resources.workspaces.createRevision({
|
|
3553
|
+
parentRevisionId: acceptedRedRevision.parentRevisionId,
|
|
3554
|
+
changes: correctedRedChanges,
|
|
3555
|
+
});
|
|
3556
|
+
const verifyCorrectedContract = async (input: {
|
|
3557
|
+
phase: "red" | "green";
|
|
3558
|
+
revisionId: string;
|
|
3559
|
+
verification: StructuredVerificationContract;
|
|
3560
|
+
}): Promise<DurablePhaseVerificationResult> => {
|
|
3561
|
+
const root = mkdtempSync(path.join(resources.root, "correction-check-"));
|
|
3562
|
+
try {
|
|
3563
|
+
resources.workspaces.materialize(input.revisionId, root);
|
|
3564
|
+
return await this.#options.verifyPhase({
|
|
3565
|
+
runId: request.runId,
|
|
3566
|
+
deliveryRevision: request.deliveryRevision,
|
|
3567
|
+
taskId: request.taskId,
|
|
3568
|
+
phase: input.phase,
|
|
3569
|
+
root,
|
|
3570
|
+
verification: structuredClone(input.verification),
|
|
3571
|
+
signal: request.signal,
|
|
3572
|
+
});
|
|
3573
|
+
} finally {
|
|
3574
|
+
rmSync(root, { recursive: true, force: true });
|
|
3575
|
+
}
|
|
3576
|
+
};
|
|
3577
|
+
emitWorkflowActivity(request.onActivity, { state: "verifying" });
|
|
3578
|
+
const redVerification = await verifyCorrectedContract({
|
|
3579
|
+
phase: "red",
|
|
3580
|
+
revisionId: correctedRedRevision.revisionId,
|
|
3581
|
+
verification: request.task.phases.red.verification,
|
|
3582
|
+
});
|
|
3583
|
+
if (!redVerification.ok) {
|
|
3584
|
+
return rejectCorrection({
|
|
3585
|
+
...redVerification,
|
|
3586
|
+
...(redVerification.kind === "retryable"
|
|
3587
|
+
? { retryPolicy: "verification" as const }
|
|
3588
|
+
: {}),
|
|
3589
|
+
});
|
|
3590
|
+
}
|
|
3591
|
+
if (
|
|
3592
|
+
redVerification.classification !== "expected-red" ||
|
|
3593
|
+
redVerification.exitCode === 0
|
|
3594
|
+
) {
|
|
3595
|
+
return rejectCorrection({
|
|
3596
|
+
kind: "retryable",
|
|
3597
|
+
code: "verification-rejected",
|
|
3598
|
+
retryPolicy: "verification",
|
|
3599
|
+
});
|
|
3600
|
+
}
|
|
3601
|
+
const greenVerification = await verifyCorrectedContract({
|
|
3602
|
+
phase: "green",
|
|
3603
|
+
revisionId: repaired.commit.isolatedRevisionId,
|
|
3604
|
+
verification: request.task.phases.green.verification,
|
|
3605
|
+
});
|
|
3606
|
+
if (!greenVerification.ok) {
|
|
3607
|
+
return rejectCorrection({
|
|
3608
|
+
...greenVerification,
|
|
3609
|
+
...(greenVerification.kind === "retryable"
|
|
3610
|
+
? { retryPolicy: "verification" as const }
|
|
3611
|
+
: {}),
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
if (
|
|
3615
|
+
greenVerification.classification !== "expected-green" ||
|
|
3616
|
+
greenVerification.exitCode !== 0
|
|
3617
|
+
) {
|
|
3618
|
+
return rejectCorrection({
|
|
3619
|
+
kind: "retryable",
|
|
3620
|
+
code: "verification-rejected",
|
|
3621
|
+
retryPolicy: "verification",
|
|
3622
|
+
});
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
const repairEvent: VerifiedTaskEvent = {
|
|
3626
|
+
runId: request.runId,
|
|
3627
|
+
taskId: request.taskId,
|
|
3628
|
+
eventId: `red-artifact-correction-${hash(
|
|
3629
|
+
request.operationId,
|
|
3630
|
+
request.taskId,
|
|
3631
|
+
repaired.commit.artifactHash,
|
|
3632
|
+
).slice(0, 40)}`,
|
|
3633
|
+
kind: "repair-verified",
|
|
3634
|
+
phase: request.phase,
|
|
3635
|
+
attempt: correctionAttemptUsed,
|
|
3636
|
+
commandId: request.task.repairVerification.id,
|
|
3637
|
+
exitCode: 0,
|
|
3638
|
+
diagnostic: repaired.commit.diagnostic,
|
|
3639
|
+
artifactHash: repaired.commit.artifactHash,
|
|
3640
|
+
isolatedRevisionId: repaired.commit.isolatedRevisionId,
|
|
3641
|
+
outputFacts: repaired.commit.outputFacts,
|
|
3642
|
+
attribution: "introduced",
|
|
3643
|
+
failureIdentities: [
|
|
3644
|
+
hash(
|
|
3645
|
+
"red-artifact-constraint",
|
|
3646
|
+
request.taskId,
|
|
3647
|
+
request.task.phases.red.verification.id,
|
|
3648
|
+
),
|
|
3649
|
+
],
|
|
3650
|
+
routeId: repaired.commit.routeId,
|
|
3651
|
+
routeFingerprint: repaired.commit.routeFingerprint,
|
|
3652
|
+
};
|
|
3653
|
+
const greenPaths = new Set([
|
|
3654
|
+
...request.task.phases.green.write,
|
|
3655
|
+
...request.task.phases.green.delete,
|
|
3656
|
+
]);
|
|
3657
|
+
const greenEvent: VerifiedTaskEvent = {
|
|
3658
|
+
runId: request.runId,
|
|
3659
|
+
taskId: request.taskId,
|
|
3660
|
+
eventId: `green-corrected-${hash(
|
|
3661
|
+
request.operationId,
|
|
3662
|
+
request.taskId,
|
|
3663
|
+
repaired.commit.artifactHash,
|
|
3664
|
+
).slice(0, 40)}`,
|
|
3665
|
+
kind: "phase-verified",
|
|
3666
|
+
phase: "green",
|
|
3667
|
+
commandId: request.task.phases.green.verification.id,
|
|
3668
|
+
exitCode: greenVerification.exitCode,
|
|
3669
|
+
expectedClassification: "expected-green",
|
|
3670
|
+
actualClassification: greenVerification.classification,
|
|
3671
|
+
diagnostic: greenVerification.diagnostic,
|
|
3672
|
+
artifactHash: repaired.commit.artifactHash,
|
|
3673
|
+
isolatedRevisionId: repaired.commit.isolatedRevisionId,
|
|
3674
|
+
outputFacts: repaired.commit.outputFacts.filter((fact) =>
|
|
3675
|
+
greenPaths.has(fact.path),
|
|
3676
|
+
),
|
|
3677
|
+
routeId: repaired.commit.routeId,
|
|
3678
|
+
routeFingerprint: repaired.commit.routeFingerprint,
|
|
3679
|
+
};
|
|
3680
|
+
const events: VerifiedTaskEvent[] = [repairEvent, greenEvent];
|
|
3681
|
+
if (input.baseline) {
|
|
3682
|
+
const affected = await this.#verifyTaskAffected({
|
|
3683
|
+
resources,
|
|
3684
|
+
baseline: input.baseline,
|
|
3685
|
+
task: request.task,
|
|
3686
|
+
revisionId: repaired.commit.isolatedRevisionId,
|
|
3687
|
+
signal: request.signal,
|
|
3688
|
+
});
|
|
3689
|
+
if (affected.kind === "repairable") {
|
|
3690
|
+
return rejectCorrection({
|
|
3691
|
+
kind: "retryable",
|
|
3692
|
+
code: "red-artifact-constraint",
|
|
3693
|
+
retryPolicy: "artifact",
|
|
3694
|
+
});
|
|
3695
|
+
}
|
|
3696
|
+
if (affected.kind !== "verified") return rejectCorrection(affected);
|
|
3697
|
+
}
|
|
3698
|
+
if (request.task.phases.refactor) {
|
|
3699
|
+
ledger.commitVerifiedEvents(events);
|
|
3700
|
+
return {
|
|
3701
|
+
kind: "phase-committed",
|
|
3702
|
+
artifactHash: repaired.commit.artifactHash,
|
|
3703
|
+
isolatedRevisionId: repaired.commit.isolatedRevisionId,
|
|
3704
|
+
exitCode: greenVerification.exitCode,
|
|
3705
|
+
classification: greenVerification.classification,
|
|
3706
|
+
routeId: repaired.commit.routeId,
|
|
3707
|
+
routeFingerprint: repaired.commit.routeFingerprint,
|
|
3708
|
+
};
|
|
3709
|
+
}
|
|
3710
|
+
const completedRevision = this.#completeTracking(resources, request.taskId);
|
|
3711
|
+
ledger.commitTaskCompletion({
|
|
3712
|
+
events,
|
|
3713
|
+
factKey: `task-final-${hash(request.taskId).slice(0, 40)}`,
|
|
3714
|
+
fact: {
|
|
3715
|
+
taskId: request.taskId,
|
|
3716
|
+
phase: request.phase,
|
|
3717
|
+
isolatedRevisionId: completedRevision.revisionId,
|
|
3718
|
+
attribution: "introduced",
|
|
3719
|
+
repairAttempts: correctionAttemptUsed,
|
|
3720
|
+
},
|
|
3721
|
+
});
|
|
3722
|
+
return {
|
|
3723
|
+
kind: "phase-committed",
|
|
3724
|
+
artifactHash: repaired.commit.artifactHash,
|
|
3725
|
+
isolatedRevisionId: completedRevision.revisionId,
|
|
3726
|
+
exitCode: greenVerification.exitCode,
|
|
3727
|
+
classification: greenVerification.classification,
|
|
3728
|
+
routeId: repaired.commit.routeId,
|
|
3729
|
+
routeFingerprint: repaired.commit.routeFingerprint,
|
|
3377
3730
|
};
|
|
3378
3731
|
}
|
|
3379
3732
|
|
|
@@ -3598,6 +3951,16 @@ class DurableWorkflowComposition
|
|
|
3598
3951
|
verificationBaseline = captured.baseline;
|
|
3599
3952
|
}
|
|
3600
3953
|
this.#openTask(resources, ledger, input);
|
|
3954
|
+
if (input.artifactCorrection?.code === "red-artifact-constraint") {
|
|
3955
|
+
return retained(
|
|
3956
|
+
await this.#runRedArtifactCorrection({
|
|
3957
|
+
resources,
|
|
3958
|
+
ledger,
|
|
3959
|
+
...(verificationBaseline ? { baseline: verificationBaseline } : {}),
|
|
3960
|
+
attempt: input,
|
|
3961
|
+
}),
|
|
3962
|
+
);
|
|
3963
|
+
}
|
|
3601
3964
|
if (input.repair) {
|
|
3602
3965
|
if (!verificationBaseline) {
|
|
3603
3966
|
throw new Error("workflow-verification-baseline-unavailable");
|
|
@@ -3747,7 +4110,8 @@ class DurableWorkflowComposition
|
|
|
3747
4110
|
proposal.kind !== "sealed-candidate"
|
|
3748
4111
|
) {
|
|
3749
4112
|
return proposal.kind === "operation-cancelled" ||
|
|
3750
|
-
proposal.kind === "paused"
|
|
4113
|
+
proposal.kind === "paused" ||
|
|
4114
|
+
proposal.kind === "approval-needed"
|
|
3751
4115
|
? retained(proposal)
|
|
3752
4116
|
: reject(proposal, {
|
|
3753
4117
|
category: "artifact",
|
|
@@ -5080,8 +5444,18 @@ export class WorkflowEngine {
|
|
|
5080
5444
|
this.#database
|
|
5081
5445
|
.prepare(
|
|
5082
5446
|
`UPDATE workflow_engine_tasks
|
|
5083
|
-
SET state = 'paused',
|
|
5084
|
-
|
|
5447
|
+
SET state = 'paused',
|
|
5448
|
+
pause_code = CASE
|
|
5449
|
+
WHEN pause_code = 'red-artifact-constraint'
|
|
5450
|
+
THEN pause_code
|
|
5451
|
+
ELSE 'operation-interrupted'
|
|
5452
|
+
END,
|
|
5453
|
+
context_request_json = CASE
|
|
5454
|
+
WHEN pause_code = 'red-artifact-constraint'
|
|
5455
|
+
THEN context_request_json
|
|
5456
|
+
ELSE NULL
|
|
5457
|
+
END,
|
|
5458
|
+
queue_position = NULL
|
|
5085
5459
|
WHERE run_id = ? AND state IN ('phase-running', 'validating')`,
|
|
5086
5460
|
)
|
|
5087
5461
|
.run(row.run_id);
|
|
@@ -5745,6 +6119,23 @@ export class WorkflowEngine {
|
|
|
5745
6119
|
const orderedPhases = phases(task);
|
|
5746
6120
|
let phaseIndex = orderedPhases.indexOf(row.phase);
|
|
5747
6121
|
let artifactAttempts = 0;
|
|
6122
|
+
const persistedContextRequest = row.context_request_json
|
|
6123
|
+
? normalizeWorkflowContextRequest(
|
|
6124
|
+
JSON.parse(row.context_request_json) as unknown,
|
|
6125
|
+
)
|
|
6126
|
+
: undefined;
|
|
6127
|
+
const persistedRedArtifactCorrection =
|
|
6128
|
+
row.phase === "green" &&
|
|
6129
|
+
persistedContextRequest !== undefined &&
|
|
6130
|
+
classifyPersistedContextRequest(persistedContextRequest, task, "green")
|
|
6131
|
+
.code === "red-artifact-constraint";
|
|
6132
|
+
let redArtifactCorrection =
|
|
6133
|
+
row.phase === "green" &&
|
|
6134
|
+
(row.pause_code === "red-artifact-constraint" ||
|
|
6135
|
+
persistedRedArtifactCorrection);
|
|
6136
|
+
let correctionContextRequest = redArtifactCorrection
|
|
6137
|
+
? persistedContextRequest
|
|
6138
|
+
: undefined;
|
|
5748
6139
|
if (phaseIndex < 0) throw new Error("workflow-task-phase-invalid");
|
|
5749
6140
|
while (phaseIndex < orderedPhases.length) {
|
|
5750
6141
|
const phase = orderedPhases[phaseIndex];
|
|
@@ -5771,6 +6162,7 @@ export class WorkflowEngine {
|
|
|
5771
6162
|
JSON.parse(current.verification_json) as unknown,
|
|
5772
6163
|
)
|
|
5773
6164
|
: undefined;
|
|
6165
|
+
const plan = this.#planForRevision(runId, row.delivery_revision);
|
|
5774
6166
|
const repair =
|
|
5775
6167
|
verificationStatus?.scope === "change-task-affected" &&
|
|
5776
6168
|
verificationStatus.attribution === "introduced" &&
|
|
@@ -5782,19 +6174,31 @@ export class WorkflowEngine {
|
|
|
5782
6174
|
failureIdentities: [...verificationStatus.failureIdentities],
|
|
5783
6175
|
}
|
|
5784
6176
|
: undefined;
|
|
6177
|
+
const artifactCorrection =
|
|
6178
|
+
redArtifactCorrection && phase === "green"
|
|
6179
|
+
? {
|
|
6180
|
+
code: "red-artifact-constraint" as const,
|
|
6181
|
+
attempt: artifactAttempts + 1,
|
|
6182
|
+
maxAttempts: plan.verification.artifactCorrection.maxAttempts,
|
|
6183
|
+
...(correctionContextRequest
|
|
6184
|
+
? {
|
|
6185
|
+
contextRequest: structuredClone(correctionContextRequest),
|
|
6186
|
+
}
|
|
6187
|
+
: {}),
|
|
6188
|
+
}
|
|
6189
|
+
: undefined;
|
|
5785
6190
|
this.#setTask(
|
|
5786
6191
|
runId,
|
|
5787
6192
|
row.task_id,
|
|
5788
6193
|
{
|
|
5789
6194
|
state: "phase-running",
|
|
5790
6195
|
phase,
|
|
5791
|
-
pauseCode: null,
|
|
5792
|
-
contextRequest: null,
|
|
6196
|
+
pauseCode: artifactCorrection?.code ?? null,
|
|
6197
|
+
contextRequest: correctionContextRequest ?? null,
|
|
5793
6198
|
queuePosition: null,
|
|
5794
6199
|
},
|
|
5795
6200
|
lease,
|
|
5796
6201
|
);
|
|
5797
|
-
const plan = this.#planForRevision(runId, row.delivery_revision);
|
|
5798
6202
|
let outcome: WorkflowAttemptOutcome;
|
|
5799
6203
|
try {
|
|
5800
6204
|
outcome = await this.#worker.runAttempt({
|
|
@@ -5825,6 +6229,7 @@ export class WorkflowEngine {
|
|
|
5825
6229
|
}
|
|
5826
6230
|
: {}),
|
|
5827
6231
|
...(repair ? { repair } : {}),
|
|
6232
|
+
...(artifactCorrection ? { artifactCorrection } : {}),
|
|
5828
6233
|
signal,
|
|
5829
6234
|
onActivity: (event) => {
|
|
5830
6235
|
const projection = this.#runStore.status(runId);
|
|
@@ -5864,9 +6269,29 @@ export class WorkflowEngine {
|
|
|
5864
6269
|
this.#setVerificationStatus(runId, outcome.verification, lease);
|
|
5865
6270
|
}
|
|
5866
6271
|
if (outcome.kind === "retryable" && outcome.retryPolicy === "artifact") {
|
|
6272
|
+
if (outcome.code === "red-artifact-constraint") {
|
|
6273
|
+
redArtifactCorrection = true;
|
|
6274
|
+
correctionContextRequest = outcome.contextRequest
|
|
6275
|
+
? normalizeWorkflowContextRequest(outcome.contextRequest)
|
|
6276
|
+
: correctionContextRequest;
|
|
6277
|
+
}
|
|
5867
6278
|
artifactAttempts += 1;
|
|
5868
6279
|
const maxAttempts = plan.verification.artifactCorrection.maxAttempts;
|
|
5869
6280
|
if (artifactAttempts < maxAttempts) {
|
|
6281
|
+
this.#setTask(
|
|
6282
|
+
runId,
|
|
6283
|
+
row.task_id,
|
|
6284
|
+
{
|
|
6285
|
+
state: "retryable",
|
|
6286
|
+
phase,
|
|
6287
|
+
pauseCode: redArtifactCorrection
|
|
6288
|
+
? "red-artifact-constraint"
|
|
6289
|
+
: outcome.code,
|
|
6290
|
+
contextRequest: correctionContextRequest ?? null,
|
|
6291
|
+
queuePosition: null,
|
|
6292
|
+
},
|
|
6293
|
+
lease,
|
|
6294
|
+
);
|
|
5870
6295
|
emitWorkflowActivity(onActivity, {
|
|
5871
6296
|
state: "retrying",
|
|
5872
6297
|
stage: this.#runStore.status(runId).stage,
|
|
@@ -5910,6 +6335,8 @@ export class WorkflowEngine {
|
|
|
5910
6335
|
return;
|
|
5911
6336
|
}
|
|
5912
6337
|
artifactAttempts = 0;
|
|
6338
|
+
redArtifactCorrection = false;
|
|
6339
|
+
correctionContextRequest = undefined;
|
|
5913
6340
|
phaseIndex += 1;
|
|
5914
6341
|
if (phaseIndex >= orderedPhases.length) {
|
|
5915
6342
|
this.#setTask(
|
|
@@ -5978,7 +6405,7 @@ export class WorkflowEngine {
|
|
|
5978
6405
|
contextRequest:
|
|
5979
6406
|
!approvalCodeInvalid && outcome.contextRequest
|
|
5980
6407
|
? normalizeWorkflowContextRequest(outcome.contextRequest)
|
|
5981
|
-
: null,
|
|
6408
|
+
: (correctionContextRequest ?? null),
|
|
5982
6409
|
queuePosition: null,
|
|
5983
6410
|
},
|
|
5984
6411
|
lease,
|
|
@@ -5990,11 +6417,119 @@ export class WorkflowEngine {
|
|
|
5990
6417
|
}
|
|
5991
6418
|
|
|
5992
6419
|
#firstPaused(rows: EngineTaskRow[]): EngineTaskRow | undefined {
|
|
5993
|
-
return
|
|
5994
|
-
|
|
6420
|
+
return (
|
|
6421
|
+
rows.find((row) => row.state === "approval-needed") ??
|
|
6422
|
+
rows.find((row) => ["paused", "retryable"].includes(row.state))
|
|
5995
6423
|
);
|
|
5996
6424
|
}
|
|
5997
6425
|
|
|
6426
|
+
#recoverLegacyContextApproval(runId: string): boolean {
|
|
6427
|
+
const projection = this.#runStore.status(runId);
|
|
6428
|
+
if (!["approval-needed", "paused"].includes(projection.state)) return false;
|
|
6429
|
+
const rows = this.#tasks(runId);
|
|
6430
|
+
const approvalRows = rows.filter((row) => row.state === "approval-needed");
|
|
6431
|
+
if (approvalRows.length === 0) return false;
|
|
6432
|
+
const reclassified = rows.flatMap((row) => {
|
|
6433
|
+
if (
|
|
6434
|
+
row.state !== "approval-needed" ||
|
|
6435
|
+
row.pause_code !== "boundary-review-needed" ||
|
|
6436
|
+
row.context_request_json === null
|
|
6437
|
+
) {
|
|
6438
|
+
return [];
|
|
6439
|
+
}
|
|
6440
|
+
try {
|
|
6441
|
+
const request = normalizeWorkflowContextRequest(
|
|
6442
|
+
JSON.parse(row.context_request_json) as unknown,
|
|
6443
|
+
);
|
|
6444
|
+
const classified = classifyPersistedContextRequest(
|
|
6445
|
+
request,
|
|
6446
|
+
this.#taskPlan(row),
|
|
6447
|
+
row.phase,
|
|
6448
|
+
);
|
|
6449
|
+
if (classified.kind === "approval-needed") return [];
|
|
6450
|
+
return [
|
|
6451
|
+
{
|
|
6452
|
+
row,
|
|
6453
|
+
state: classified.kind === "retryable" ? "retryable" : "paused",
|
|
6454
|
+
code: classified.code,
|
|
6455
|
+
contextRequest: JSON.stringify(
|
|
6456
|
+
normalizeWorkflowContextRequest(classified.contextRequest),
|
|
6457
|
+
),
|
|
6458
|
+
},
|
|
6459
|
+
];
|
|
6460
|
+
} catch {
|
|
6461
|
+
return [];
|
|
6462
|
+
}
|
|
6463
|
+
});
|
|
6464
|
+
const reclassifiedTaskIds = new Set(
|
|
6465
|
+
reclassified.map((entry) => entry.row.task_id),
|
|
6466
|
+
);
|
|
6467
|
+
const retainedApprovals = approvalRows.filter(
|
|
6468
|
+
(row) => !reclassifiedTaskIds.has(row.task_id),
|
|
6469
|
+
);
|
|
6470
|
+
const updateTasks = (database: DatabaseSync) => {
|
|
6471
|
+
for (const entry of reclassified) {
|
|
6472
|
+
const updated = database
|
|
6473
|
+
.prepare(
|
|
6474
|
+
`UPDATE workflow_engine_tasks
|
|
6475
|
+
SET state = ?, phase = ?, pause_code = ?, context_request_json = ?,
|
|
6476
|
+
queue_position = NULL
|
|
6477
|
+
WHERE run_id = ? AND task_id = ?
|
|
6478
|
+
AND state = 'approval-needed'
|
|
6479
|
+
AND pause_code = 'boundary-review-needed'`,
|
|
6480
|
+
)
|
|
6481
|
+
.run(
|
|
6482
|
+
entry.state,
|
|
6483
|
+
entry.row.phase,
|
|
6484
|
+
entry.code,
|
|
6485
|
+
entry.contextRequest,
|
|
6486
|
+
runId,
|
|
6487
|
+
entry.row.task_id,
|
|
6488
|
+
);
|
|
6489
|
+
if (Number(updated.changes) !== 1) {
|
|
6490
|
+
throw new Error("workflow-legacy-context-race");
|
|
6491
|
+
}
|
|
6492
|
+
}
|
|
6493
|
+
};
|
|
6494
|
+
const transitionWithUpdates = (
|
|
6495
|
+
to: "paused" | "approval-needed",
|
|
6496
|
+
code: string,
|
|
6497
|
+
) =>
|
|
6498
|
+
this.#runStore.transitionAtomically(
|
|
6499
|
+
{
|
|
6500
|
+
runId,
|
|
6501
|
+
to,
|
|
6502
|
+
operationId: `legacy-context-${hash(
|
|
6503
|
+
runId,
|
|
6504
|
+
String(projection.sequence),
|
|
6505
|
+
to,
|
|
6506
|
+
code,
|
|
6507
|
+
JSON.stringify(
|
|
6508
|
+
reclassified.map((entry) => [entry.row.task_id, entry.code]),
|
|
6509
|
+
),
|
|
6510
|
+
).slice(0, 40)}`,
|
|
6511
|
+
code,
|
|
6512
|
+
},
|
|
6513
|
+
updateTasks,
|
|
6514
|
+
);
|
|
6515
|
+
if (retainedApprovals.length > 0) {
|
|
6516
|
+
if (projection.state === "approval-needed") {
|
|
6517
|
+
if (reclassified.length === 0) return false;
|
|
6518
|
+
this.#transaction(() => updateTasks(this.#database));
|
|
6519
|
+
} else {
|
|
6520
|
+
transitionWithUpdates(
|
|
6521
|
+
"approval-needed",
|
|
6522
|
+
retainedApprovals[0].pause_code ?? "approval-code-invalid",
|
|
6523
|
+
);
|
|
6524
|
+
}
|
|
6525
|
+
return true;
|
|
6526
|
+
}
|
|
6527
|
+
const first = reclassified[0];
|
|
6528
|
+
if (!first) return false;
|
|
6529
|
+
transitionWithUpdates("paused", first.code);
|
|
6530
|
+
return true;
|
|
6531
|
+
}
|
|
6532
|
+
|
|
5998
6533
|
async #finishChange(
|
|
5999
6534
|
runId: string,
|
|
6000
6535
|
operationId: string,
|
|
@@ -6592,7 +7127,10 @@ export class WorkflowEngine {
|
|
|
6592
7127
|
)
|
|
6593
7128
|
: undefined;
|
|
6594
7129
|
const pauseCode =
|
|
6595
|
-
projection.
|
|
7130
|
+
projection.state === "approval-needed" &&
|
|
7131
|
+
firstPaused?.state === "approval-needed"
|
|
7132
|
+
? (firstPaused.pause_code ?? projection.pauseCode ?? "task-paused")
|
|
7133
|
+
: (projection.pauseCode ?? firstPaused?.pause_code ?? "task-paused");
|
|
6596
7134
|
const approval =
|
|
6597
7135
|
projection.state === "approval-needed"
|
|
6598
7136
|
? approvalRequirement(pauseCode, contextRequest)
|
|
@@ -6941,6 +7479,7 @@ export class WorkflowEngine {
|
|
|
6941
7479
|
) {
|
|
6942
7480
|
const runId = this.#lookupRun(command.stage, command.change);
|
|
6943
7481
|
if (!runId) throw new Error("run-not-found");
|
|
7482
|
+
this.#recoverLegacyContextApproval(runId);
|
|
6944
7483
|
const replay = this.#beginOperation(
|
|
6945
7484
|
runId,
|
|
6946
7485
|
command.operationId,
|
|
@@ -7337,6 +7876,7 @@ export class WorkflowEngine {
|
|
|
7337
7876
|
case "status": {
|
|
7338
7877
|
const runId = this.#lookupRun(command.stage, command.change);
|
|
7339
7878
|
if (!runId) throw new Error("run-not-found");
|
|
7879
|
+
this.#recoverLegacyContextApproval(runId);
|
|
7340
7880
|
return this.#withAvailableDelivery(this.#statusByRun(runId));
|
|
7341
7881
|
}
|
|
7342
7882
|
case "resume":
|