@bluewombat/conductor 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 +3 -3
- package/SPECS.md +128 -146
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/run/open-conductor.d.ts +12 -21
- package/dist/run/open-conductor.d.ts.map +1 -1
- package/dist/run/open-conductor.js +128 -259
- package/dist/run/open-conductor.js.map +1 -1
- package/dist/run/paths.d.ts +1 -2
- package/dist/run/paths.d.ts.map +1 -1
- package/dist/run/paths.js +2 -5
- package/dist/run/paths.js.map +1 -1
- package/dist/transformers/port.d.ts +12 -41
- package/dist/transformers/port.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readdir, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { isAbsolute, join, resolve } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { ticketWorkspacePath } from "./paths.js";
|
|
5
5
|
const DEFAULT_MAX_REFUSALS = 3;
|
|
6
6
|
const DEFAULT_TRANSFORMER_DURATION_MS = 30_000;
|
|
7
|
-
function featureJsonOf(admission) {
|
|
8
|
-
const document = {
|
|
9
|
-
key: admission.key,
|
|
10
|
-
intention: admission.intention,
|
|
11
|
-
};
|
|
12
|
-
if (admission.title !== undefined) {
|
|
13
|
-
document.title = admission.title;
|
|
14
|
-
}
|
|
15
|
-
return JSON.stringify(document);
|
|
16
|
-
}
|
|
17
7
|
async function isDirectory(path) {
|
|
18
8
|
try {
|
|
19
9
|
return (await stat(path)).isDirectory();
|
|
@@ -26,13 +16,12 @@ async function destroy(path) {
|
|
|
26
16
|
await rm(path, { recursive: true, force: true });
|
|
27
17
|
}
|
|
28
18
|
/**
|
|
29
|
-
* Where a
|
|
30
|
-
* made, else the one the
|
|
31
|
-
*
|
|
32
|
-
* where they were; a layout that changed since must not strand them.
|
|
19
|
+
* Where a Ticket's workspace is: the path the ledger declared when it was
|
|
20
|
+
* made, else the one the layout gives. The declared path wins because a
|
|
21
|
+
* Ticket in flight keeps its work and its git branch where they were.
|
|
33
22
|
*/
|
|
34
|
-
function
|
|
35
|
-
return aggregate?.
|
|
23
|
+
function declaredTicketPath(workspaceRoot, key, aggregate) {
|
|
24
|
+
return aggregate?.workspace ?? ticketWorkspacePath(workspaceRoot, key);
|
|
36
25
|
}
|
|
37
26
|
async function destroyOrphans(workspaceRoot, allowed) {
|
|
38
27
|
if (!(await isDirectory(workspaceRoot))) {
|
|
@@ -113,12 +102,12 @@ export function openConductor(options) {
|
|
|
113
102
|
const durationMs = options.transformerDurationMs ?? DEFAULT_TRANSFORMER_DURATION_MS;
|
|
114
103
|
const authority = options.authority;
|
|
115
104
|
const workLineTarget = options.workLineTarget ?? "";
|
|
105
|
+
/** The line a Ticket lands on: its own when it names one, else the Project's. */
|
|
106
|
+
const targetOf = (aggregate) => aggregate.intention.line ?? workLineTarget;
|
|
116
107
|
const maxRefusals = options.maxRefusals ?? DEFAULT_MAX_REFUSALS;
|
|
117
108
|
const assemblyValidate = options.assemblyValidate ?? false;
|
|
118
109
|
const assemblyFixDeclared = options.assemblyFixDeclared ?? false;
|
|
119
110
|
const warm = options.warm;
|
|
120
|
-
/** Features whose warm already succeeded in this Conductor instance. */
|
|
121
|
-
const warmed = new Set();
|
|
122
111
|
let paused = false;
|
|
123
112
|
const refused = (code) => ({
|
|
124
113
|
outcome: "refused",
|
|
@@ -149,13 +138,13 @@ export function openConductor(options) {
|
|
|
149
138
|
return refused("illegal-transition");
|
|
150
139
|
};
|
|
151
140
|
/**
|
|
152
|
-
* Freeze the
|
|
141
|
+
* Freeze the Ticket for a human. If a newer fingerprint arrived in flight,
|
|
153
142
|
* it is named on the escalation and consumed by the ledger.
|
|
154
143
|
*/
|
|
155
144
|
async function freeze(key, input) {
|
|
156
145
|
const got = await ledger.get(key);
|
|
157
146
|
const pending = got.ok && got.aggregate.pending_fingerprint !== undefined
|
|
158
|
-
? `The intention was edited while the work was in flight (now fingerprint ${got.aggregate.pending_fingerprint}). The work stops here for a human to decide: on resume
|
|
147
|
+
? `The intention was edited while the work was in flight (now fingerprint ${got.aggregate.pending_fingerprint}). The work stops here for a human to decide: on resume the work starts again from zero.`
|
|
159
148
|
: undefined;
|
|
160
149
|
const report = [input.report, pending]
|
|
161
150
|
.filter((part) => part !== undefined && part.length > 0)
|
|
@@ -163,7 +152,6 @@ export function openConductor(options) {
|
|
|
163
152
|
const escalated = await ledger.escalate({
|
|
164
153
|
key,
|
|
165
154
|
kind: input.kind,
|
|
166
|
-
...(input.subtaskId === undefined ? {} : { subtaskId: input.subtaskId }),
|
|
167
155
|
...(report.length === 0 ? {} : { report }),
|
|
168
156
|
});
|
|
169
157
|
const failure = fromLedger(escalated);
|
|
@@ -177,24 +165,23 @@ export function openConductor(options) {
|
|
|
177
165
|
if (!got.ok || got.aggregate.pending_fingerprint === undefined) {
|
|
178
166
|
return undefined;
|
|
179
167
|
}
|
|
180
|
-
return await freeze(key, { kind: "
|
|
168
|
+
return await freeze(key, { kind: "unit" });
|
|
181
169
|
}
|
|
182
170
|
async function expireIfHeld(key) {
|
|
171
|
+
const before = await ledger.get(key);
|
|
172
|
+
const held = before.ok && before.aggregate.state === "running" ? before.aggregate.workspace : undefined;
|
|
183
173
|
const expired = await ledger.expireBail(key);
|
|
184
|
-
if (expired.ok) {
|
|
185
|
-
|
|
186
|
-
if (got.ok && got.aggregate.workspaces?.subtask !== undefined) {
|
|
187
|
-
await destroy(got.aggregate.workspaces.subtask);
|
|
188
|
-
}
|
|
174
|
+
if (expired.ok && held !== undefined) {
|
|
175
|
+
await destroy(held);
|
|
189
176
|
}
|
|
190
177
|
}
|
|
191
178
|
/**
|
|
192
|
-
* A
|
|
179
|
+
* A unit was started and a transformer stopped on interrupt. Release the
|
|
193
180
|
* bail now so the next process does not wait an hour for the clock.
|
|
194
181
|
*/
|
|
195
182
|
async function releaseHeldAfterInterrupt(key) {
|
|
196
183
|
const before = await ledger.get(key);
|
|
197
|
-
const child = before.ok ? before.aggregate.
|
|
184
|
+
const child = before.ok ? before.aggregate.workspace : undefined;
|
|
198
185
|
const released = await ledger.releaseBail(key);
|
|
199
186
|
if (!released.ok) {
|
|
200
187
|
if (released.code === "persist-failed") {
|
|
@@ -213,12 +200,7 @@ export function openConductor(options) {
|
|
|
213
200
|
const declared = await ledger.listDeclaredWorkspaces();
|
|
214
201
|
const allowed = new Set();
|
|
215
202
|
for (const row of declared) {
|
|
216
|
-
|
|
217
|
-
allowed.add(resolve(row.feature));
|
|
218
|
-
}
|
|
219
|
-
if (row.subtask !== undefined) {
|
|
220
|
-
allowed.add(resolve(row.subtask));
|
|
221
|
-
}
|
|
203
|
+
allowed.add(resolve(row.path));
|
|
222
204
|
}
|
|
223
205
|
await destroyOrphans(workspaceRoot, allowed);
|
|
224
206
|
for (const row of declared) {
|
|
@@ -234,61 +216,45 @@ export function openConductor(options) {
|
|
|
234
216
|
if (!got.ok) {
|
|
235
217
|
return;
|
|
236
218
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (subtask !== undefined) {
|
|
240
|
-
await destroy(subtask);
|
|
219
|
+
if (got.aggregate.workspace !== undefined) {
|
|
220
|
+
await destroy(got.aggregate.workspace);
|
|
241
221
|
}
|
|
242
|
-
|
|
243
|
-
await destroy(feature);
|
|
244
|
-
}
|
|
245
|
-
await ledger.clearWorkspace({ key, feature: true, subtask: true });
|
|
222
|
+
await ledger.clearWorkspace(key);
|
|
246
223
|
}
|
|
247
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Make the Ticket's workspace from nothing: a unit that starts, starts from
|
|
226
|
+
* zero, so what an earlier pass left is destroyed first. No lease is held
|
|
227
|
+
* yet: a failure here costs no Attempt, and the next pass makes it again.
|
|
228
|
+
*/
|
|
229
|
+
async function isolateFresh(key) {
|
|
248
230
|
const got = await ledger.get(key);
|
|
249
231
|
if (!got.ok) {
|
|
250
232
|
return refused("not-found");
|
|
251
233
|
}
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
const failure = fromLedger(declared);
|
|
256
|
-
if (failure !== undefined) {
|
|
257
|
-
return failure;
|
|
258
|
-
}
|
|
234
|
+
const workspace = declaredTicketPath(workspaceRoot, key, got.aggregate);
|
|
235
|
+
if (existsSync(workspace)) {
|
|
236
|
+
await destroy(workspace);
|
|
259
237
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// longer here. Isolating again would rebuild the feature from the work
|
|
265
|
-
// line and publish it over that Submission, which would drop whatever the
|
|
266
|
-
// Authority has already been shown. That is a human's call, not this
|
|
267
|
-
// loop's.
|
|
268
|
-
const escalated = await freeze(key, { kind: "submitted" });
|
|
269
|
-
return escalated;
|
|
270
|
-
}
|
|
271
|
-
const isolated = await transformers.isolate({
|
|
272
|
-
id: key,
|
|
273
|
-
context: key,
|
|
274
|
-
parent: workLineStable,
|
|
275
|
-
child,
|
|
276
|
-
durationMs,
|
|
277
|
-
});
|
|
278
|
-
if (isolated.outcome === "interrupted") {
|
|
279
|
-
return refused("interrupted");
|
|
280
|
-
}
|
|
281
|
-
if (isolated.outcome !== "isolated") {
|
|
282
|
-
return refused(isolated.outcome === "invalid-invocation" ? "transformer-invalid" : "isolate-failed");
|
|
283
|
-
}
|
|
284
|
-
fresh = true;
|
|
285
|
-
warmed.delete(key);
|
|
238
|
+
const declared = await ledger.declareWorkspace({ key, path: workspace });
|
|
239
|
+
const declareFail = fromLedger(declared);
|
|
240
|
+
if (declareFail !== undefined) {
|
|
241
|
+
return declareFail;
|
|
286
242
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
243
|
+
const isolated = await transformers.isolate({
|
|
244
|
+
id: key,
|
|
245
|
+
context: key,
|
|
246
|
+
parent: workLineStable,
|
|
247
|
+
child: workspace,
|
|
248
|
+
durationMs,
|
|
249
|
+
});
|
|
250
|
+
if (isolated.outcome === "interrupted") {
|
|
251
|
+
return refused("interrupted");
|
|
252
|
+
}
|
|
253
|
+
if (isolated.outcome !== "isolated") {
|
|
254
|
+
return refused(isolated.outcome === "invalid-invocation" ? "transformer-invalid" : "isolate-failed");
|
|
255
|
+
}
|
|
256
|
+
if (warm !== undefined) {
|
|
257
|
+
const prepared = await warm.prepare({ workspace, durationMs: warm.durationMs });
|
|
292
258
|
if (prepared.outcome === "interrupted") {
|
|
293
259
|
return refused("interrupted");
|
|
294
260
|
}
|
|
@@ -298,107 +264,50 @@ export function openConductor(options) {
|
|
|
298
264
|
if (prepared.outcome !== "warmed") {
|
|
299
265
|
return refused("warm-failed");
|
|
300
266
|
}
|
|
301
|
-
warmed.add(key);
|
|
302
267
|
}
|
|
303
268
|
return undefined;
|
|
304
269
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Run the unit: Builder, then the Gates, in the workspace Isolation made.
|
|
272
|
+
* The result stays loose in the workspace; the Integration in `doIntegrating`
|
|
273
|
+
* is what takes it up.
|
|
274
|
+
*/
|
|
275
|
+
async function doRunning(key) {
|
|
276
|
+
if (paused) {
|
|
277
|
+
return { outcome: "paused", key };
|
|
278
|
+
}
|
|
279
|
+
const before = await ledger.get(key);
|
|
280
|
+
if (!before.ok) {
|
|
308
281
|
return refused("not-found");
|
|
309
282
|
}
|
|
310
|
-
if (
|
|
283
|
+
if (before.aggregate.state !== "running") {
|
|
311
284
|
return undefined;
|
|
312
285
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (broken.outcome === "refused") {
|
|
317
|
-
const recorded = await ledger.refusePlan({
|
|
318
|
-
key,
|
|
319
|
-
code: broken.code,
|
|
320
|
-
reason: broken.reason,
|
|
321
|
-
});
|
|
322
|
-
const failure = fromLedger(recorded);
|
|
323
|
-
if (failure !== undefined) {
|
|
324
|
-
return failure;
|
|
325
|
-
}
|
|
326
|
-
return { outcome: "escalated", key };
|
|
327
|
-
}
|
|
328
|
-
if (broken.outcome === "interrupted") {
|
|
329
|
-
return refused("interrupted");
|
|
330
|
-
}
|
|
331
|
-
if (broken.outcome === "unavailable") {
|
|
332
|
-
return refused("unavailable");
|
|
286
|
+
if (before.aggregate.bail !== undefined) {
|
|
287
|
+
// A unit is held: its lease runs out on its own if nobody is on it.
|
|
288
|
+
return { outcome: "paused", key };
|
|
333
289
|
}
|
|
334
|
-
|
|
335
|
-
|
|
290
|
+
const isolated = await isolateFresh(key);
|
|
291
|
+
if (isolated !== undefined) {
|
|
292
|
+
return isolated;
|
|
336
293
|
}
|
|
337
|
-
|
|
338
|
-
key
|
|
339
|
-
plannedAt: broken.plannedAt,
|
|
340
|
-
subtasks: broken.subtasks,
|
|
341
|
-
});
|
|
342
|
-
const recordFail = fromLedger(recorded);
|
|
343
|
-
if (recordFail !== undefined) {
|
|
344
|
-
return recordFail;
|
|
294
|
+
if (paused) {
|
|
295
|
+
return { outcome: "paused", key };
|
|
345
296
|
}
|
|
346
|
-
|
|
347
|
-
return undefined;
|
|
348
|
-
}
|
|
349
|
-
async function runOneSubtask(key, subtaskId) {
|
|
350
|
-
const before = await ledger.get(key);
|
|
351
|
-
const started = await ledger.startSubtask({ key, subtaskId });
|
|
297
|
+
const started = await ledger.startUnit(key);
|
|
352
298
|
const startFail = fromLedger(started);
|
|
353
299
|
if (startFail !== undefined) {
|
|
354
300
|
return startFail;
|
|
355
301
|
}
|
|
356
|
-
const child = subtaskWorkspacePath(workspaceRoot, key, subtaskId);
|
|
357
|
-
// Resume from zero: Isolator refuses an existing Child. A kept one may sit
|
|
358
|
-
// where an earlier layout put it; it goes too, or its branch stays taken.
|
|
359
|
-
const kept = before.ok ? before.aggregate.workspaces?.subtask : undefined;
|
|
360
|
-
for (const path of new Set([child, ...(kept === undefined ? [] : [kept])])) {
|
|
361
|
-
if (existsSync(path)) {
|
|
362
|
-
await destroy(path);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
const declared = await ledger.declareWorkspace({ key, subtask: child });
|
|
366
|
-
const declareFail = fromLedger(declared);
|
|
367
|
-
if (declareFail !== undefined) {
|
|
368
|
-
return declareFail;
|
|
369
|
-
}
|
|
370
|
-
const featureNow = await ledger.get(key);
|
|
371
|
-
const parent = declaredFeaturePath(workspaceRoot, key, featureNow.ok ? featureNow.aggregate : undefined);
|
|
372
|
-
const isolated = await transformers.isolate({
|
|
373
|
-
// Scoped to the feature: a Subtask id is unique inside its plan and
|
|
374
|
-
// nowhere else, so "A" alone would name every feature's first Subtask.
|
|
375
|
-
id: `${key}:${subtaskId}`,
|
|
376
|
-
context: key,
|
|
377
|
-
parent,
|
|
378
|
-
child,
|
|
379
|
-
durationMs,
|
|
380
|
-
});
|
|
381
|
-
if (isolated.outcome === "interrupted") {
|
|
382
|
-
const failure = await releaseHeldAfterInterrupt(key);
|
|
383
|
-
return failure ?? refused("interrupted");
|
|
384
|
-
}
|
|
385
|
-
if (isolated.outcome !== "isolated") {
|
|
386
|
-
return refused(isolated.outcome === "invalid-invocation" ? "transformer-invalid" : "isolate-failed");
|
|
387
|
-
}
|
|
388
302
|
const got = await ledger.get(key);
|
|
389
303
|
if (!got.ok) {
|
|
390
304
|
return refused("not-found");
|
|
391
305
|
}
|
|
392
|
-
const subtask = got.aggregate.plan?.subtasks.find((st) => st.id === subtaskId);
|
|
393
|
-
if (subtask === undefined) {
|
|
394
|
-
return refused("not-found");
|
|
395
|
-
}
|
|
396
306
|
const implemented = await transformers.implement({
|
|
397
|
-
id:
|
|
307
|
+
id: key,
|
|
398
308
|
context: key,
|
|
399
|
-
intention:
|
|
400
|
-
|
|
401
|
-
workspace: child,
|
|
309
|
+
intention: got.aggregate.intention.intention,
|
|
310
|
+
workspace: declaredTicketPath(workspaceRoot, key, got.aggregate),
|
|
402
311
|
});
|
|
403
312
|
if (implemented.outcome === "interrupted") {
|
|
404
313
|
const failure = await releaseHeldAfterInterrupt(key);
|
|
@@ -407,11 +316,10 @@ export function openConductor(options) {
|
|
|
407
316
|
if (implemented.outcome === "invalid-invocation") {
|
|
408
317
|
return refused("transformer-invalid");
|
|
409
318
|
}
|
|
410
|
-
let number = got.aggregate.attempts.
|
|
319
|
+
let number = got.aggregate.attempts.length + 1;
|
|
411
320
|
for (const trace of implemented.traces) {
|
|
412
321
|
const recorded = await ledger.recordAttempt({
|
|
413
322
|
key,
|
|
414
|
-
subtaskId,
|
|
415
323
|
number,
|
|
416
324
|
trace: {
|
|
417
325
|
ended: trace.ended,
|
|
@@ -430,77 +338,17 @@ export function openConductor(options) {
|
|
|
430
338
|
if (implemented.outcome === "escalated") {
|
|
431
339
|
const why = reasonOfTraces(implemented.traces);
|
|
432
340
|
return await freeze(key, {
|
|
433
|
-
kind: "
|
|
434
|
-
subtaskId,
|
|
341
|
+
kind: "unit",
|
|
435
342
|
...(why === undefined ? {} : { report: why }),
|
|
436
343
|
});
|
|
437
344
|
}
|
|
438
|
-
const
|
|
439
|
-
id: subtaskId,
|
|
440
|
-
context: key,
|
|
441
|
-
parent,
|
|
442
|
-
child,
|
|
443
|
-
durationMs,
|
|
444
|
-
// What carries the work into the feature says what the work was.
|
|
445
|
-
subject: foldMessageOf(subtask.intention),
|
|
446
|
-
mergeSubject: `Merge ${subtaskId} into the feature`,
|
|
447
|
-
});
|
|
448
|
-
if (folded.outcome === "interrupted") {
|
|
449
|
-
const failure = await releaseHeldAfterInterrupt(key);
|
|
450
|
-
return failure ?? refused("interrupted");
|
|
451
|
-
}
|
|
452
|
-
if (folded.outcome === "conflict") {
|
|
453
|
-
// The unit passed its Gates; what stopped it is the fold, which leaves
|
|
454
|
-
// no trace of its own. Said here, or the escalation says nothing.
|
|
455
|
-
return await freeze(key, {
|
|
456
|
-
kind: "subtask",
|
|
457
|
-
subtaskId,
|
|
458
|
-
report: `Folding ${subtaskId} into the feature hit a conflict.`,
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
if (folded.outcome !== "integrated") {
|
|
462
|
-
return refused("transformer-invalid");
|
|
463
|
-
}
|
|
464
|
-
const marked = await ledger.markSubtaskIntegrated({ key, subtaskId });
|
|
345
|
+
const marked = await ledger.markIntegrating(key);
|
|
465
346
|
const markFail = fromLedger(marked);
|
|
466
347
|
if (markFail !== undefined) {
|
|
467
348
|
return markFail;
|
|
468
349
|
}
|
|
469
|
-
await destroy(child);
|
|
470
|
-
await ledger.clearWorkspace({ key, subtask: true });
|
|
471
|
-
const after = await ledger.get(key);
|
|
472
|
-
if (after.ok && after.aggregate.plan !== undefined) {
|
|
473
|
-
const subtasks = after.aggregate.plan.subtasks;
|
|
474
|
-
await observe({
|
|
475
|
-
kind: "subtask-integrated",
|
|
476
|
-
key,
|
|
477
|
-
subtaskId,
|
|
478
|
-
integrated: subtasks.filter((subtask) => subtask.state === "integrated").length,
|
|
479
|
-
total: subtasks.length,
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
350
|
return await freezeIfPending(key);
|
|
483
351
|
}
|
|
484
|
-
async function doRunning(key) {
|
|
485
|
-
const isolated = await ensureFeatureIsolated(key);
|
|
486
|
-
if (isolated !== undefined) {
|
|
487
|
-
return isolated;
|
|
488
|
-
}
|
|
489
|
-
while (!paused) {
|
|
490
|
-
const next = await ledger.nextRunnable(key);
|
|
491
|
-
if (next === undefined) {
|
|
492
|
-
break;
|
|
493
|
-
}
|
|
494
|
-
const step = await runOneSubtask(key, next);
|
|
495
|
-
if (step !== undefined) {
|
|
496
|
-
return step;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
if (paused) {
|
|
500
|
-
return { outcome: "paused", key };
|
|
501
|
-
}
|
|
502
|
-
return undefined;
|
|
503
|
-
}
|
|
504
352
|
async function doIntegrating(key) {
|
|
505
353
|
if (paused) {
|
|
506
354
|
return { outcome: "paused", key };
|
|
@@ -513,14 +361,14 @@ export function openConductor(options) {
|
|
|
513
361
|
if (!got.ok) {
|
|
514
362
|
return refused("not-found");
|
|
515
363
|
}
|
|
516
|
-
const
|
|
364
|
+
const ticketPath = declaredTicketPath(workspaceRoot, key, got.aggregate);
|
|
517
365
|
// A parked refusal is this round's; a Submission's is carried forward on
|
|
518
366
|
// purpose and may already be repaired. The fresher one wins.
|
|
519
367
|
const parked = got.aggregate.parked_refusal;
|
|
520
368
|
const sentBack = parked?.report ?? got.aggregate.submission?.last_report;
|
|
521
369
|
if (sentBack !== undefined) {
|
|
522
370
|
// Something refused the last round and said why. That report is the only
|
|
523
|
-
// reason this stage has to produce: the
|
|
371
|
+
// reason this stage has to produce: the unit is already validated.
|
|
524
372
|
//
|
|
525
373
|
// Before the Integration below, and that order is the whole point. What a
|
|
526
374
|
// producer leaves is loose in the workspace; the Integration is the only
|
|
@@ -535,7 +383,7 @@ export function openConductor(options) {
|
|
|
535
383
|
stage: "assembly",
|
|
536
384
|
produce: true,
|
|
537
385
|
intention: got.aggregate.intention.intention,
|
|
538
|
-
workspace:
|
|
386
|
+
workspace: ticketPath,
|
|
539
387
|
report: sentBack,
|
|
540
388
|
...(refusedBy === undefined ? {} : { reportFrom: refusedBy }),
|
|
541
389
|
});
|
|
@@ -546,7 +394,7 @@ export function openConductor(options) {
|
|
|
546
394
|
return refused("transformer-invalid");
|
|
547
395
|
}
|
|
548
396
|
if (remade.outcome === "escalated") {
|
|
549
|
-
// The
|
|
397
|
+
// The assembled work, not the unit: naming the unit here sends a reader
|
|
550
398
|
// to look at work that did not fail.
|
|
551
399
|
const why = reasonOfTraces(remade.traces);
|
|
552
400
|
return await freeze(key, {
|
|
@@ -558,14 +406,15 @@ export function openConductor(options) {
|
|
|
558
406
|
const aligned = await transformers.integrate({
|
|
559
407
|
id: `${key}:align`,
|
|
560
408
|
context: key,
|
|
561
|
-
parent:
|
|
409
|
+
parent: ticketPath,
|
|
562
410
|
child: workLineStable,
|
|
563
411
|
durationMs,
|
|
564
|
-
// What is loose in the
|
|
412
|
+
// What is loose in the workspace here is the work the Builder made, or a
|
|
413
|
+
// repair of it.
|
|
565
414
|
subject: sentBack === undefined
|
|
566
|
-
?
|
|
415
|
+
? foldMessageOf(got.aggregate.intention.title ?? got.aggregate.intention.intention)
|
|
567
416
|
: "Repair after it was refused",
|
|
568
|
-
mergeSubject: `Merge ${
|
|
417
|
+
mergeSubject: `Merge ${targetOf(got.aggregate) || "the work line"} into the ticket`,
|
|
569
418
|
});
|
|
570
419
|
if (aligned.outcome === "interrupted") {
|
|
571
420
|
return refused("interrupted");
|
|
@@ -586,7 +435,7 @@ export function openConductor(options) {
|
|
|
586
435
|
stage: "assembly",
|
|
587
436
|
validate: true,
|
|
588
437
|
intention: got.aggregate.intention.intention,
|
|
589
|
-
workspace:
|
|
438
|
+
workspace: ticketPath,
|
|
590
439
|
});
|
|
591
440
|
if (validated.outcome === "interrupted") {
|
|
592
441
|
return refused("interrupted");
|
|
@@ -596,7 +445,7 @@ export function openConductor(options) {
|
|
|
596
445
|
}
|
|
597
446
|
if (validated.outcome === "escalated") {
|
|
598
447
|
const last = validated.traces.at(-1);
|
|
599
|
-
const report = last?.report ?? "The assembled
|
|
448
|
+
const report = last?.report ?? "The assembled ticket was refused, with no reason given.";
|
|
600
449
|
const spent = (got.aggregate.submission?.refusals ?? 0) + (got.aggregate.parked_refusals ?? 0);
|
|
601
450
|
const canRepair = assemblyFixDeclared && last?.ended !== "fail-blocking" && spent + 1 < maxRefusals;
|
|
602
451
|
if (!canRepair) {
|
|
@@ -626,7 +475,7 @@ export function openConductor(options) {
|
|
|
626
475
|
}
|
|
627
476
|
if (authority === undefined) {
|
|
628
477
|
// Nobody outside will look at it, so the Project's own sequence is the
|
|
629
|
-
// only judgement the assembled
|
|
478
|
+
// only judgement the assembled ticket gets. Skipping it here would let
|
|
630
479
|
// work into the work line that nothing ever judged as a whole.
|
|
631
480
|
const judged = await judge(key, got.aggregate);
|
|
632
481
|
if (judged.outcome === "interrupted") {
|
|
@@ -648,9 +497,9 @@ export function openConductor(options) {
|
|
|
648
497
|
return await offer(key, got.aggregate);
|
|
649
498
|
}
|
|
650
499
|
/**
|
|
651
|
-
* Put the assembled
|
|
500
|
+
* Put the assembled ticket in front of the Authority.
|
|
652
501
|
*
|
|
653
|
-
* A
|
|
502
|
+
* A ticket already submitted is offered again rather than a second time:
|
|
654
503
|
* the work was published to the same place, so the Authority is judging the
|
|
655
504
|
* same Submission with new content.
|
|
656
505
|
*/
|
|
@@ -660,16 +509,14 @@ export function openConductor(options) {
|
|
|
660
509
|
}
|
|
661
510
|
const title = aggregate.intention.title;
|
|
662
511
|
const intention = aggregate.intention.intention;
|
|
663
|
-
const steps = aggregate.plan?.subtasks.map((subtask) => subtask.intention) ?? [];
|
|
664
512
|
const submitted = await authority.submit({
|
|
665
513
|
key,
|
|
666
514
|
project: aggregate.intention.project,
|
|
667
515
|
ref: key,
|
|
668
|
-
target:
|
|
669
|
-
workspace:
|
|
516
|
+
target: targetOf(aggregate),
|
|
517
|
+
workspace: declaredTicketPath(workspaceRoot, key, aggregate),
|
|
670
518
|
...(title === undefined ? {} : { title }),
|
|
671
519
|
...(intention === undefined ? {} : { intention }),
|
|
672
|
-
...(steps.length === 0 ? {} : { steps }),
|
|
673
520
|
});
|
|
674
521
|
if (submitted.outcome === "unavailable") {
|
|
675
522
|
// Nothing is lost: the state still says integrating, and the next pass
|
|
@@ -688,7 +535,7 @@ export function openConductor(options) {
|
|
|
688
535
|
return { outcome: "paused", key };
|
|
689
536
|
}
|
|
690
537
|
/**
|
|
691
|
-
* Judge the assembled
|
|
538
|
+
* Judge the assembled ticket with the Project's Gate sequence for it.
|
|
692
539
|
*
|
|
693
540
|
* Nothing is produced: the work is done, and this pass only decides whether it
|
|
694
541
|
* may enter the work line. A Gate that reads an outside answer belongs in that
|
|
@@ -701,7 +548,7 @@ export function openConductor(options) {
|
|
|
701
548
|
stage: "assembly",
|
|
702
549
|
produce: false,
|
|
703
550
|
intention: aggregate.intention.intention,
|
|
704
|
-
workspace:
|
|
551
|
+
workspace: declaredTicketPath(workspaceRoot, key, aggregate),
|
|
705
552
|
});
|
|
706
553
|
}
|
|
707
554
|
async function doSubmitted(key) {
|
|
@@ -713,7 +560,7 @@ export function openConductor(options) {
|
|
|
713
560
|
if (authority === undefined || submission === undefined) {
|
|
714
561
|
return refused("transformer-invalid");
|
|
715
562
|
}
|
|
716
|
-
const
|
|
563
|
+
const ticketPath = declaredTicketPath(workspaceRoot, key, got.aggregate);
|
|
717
564
|
const judged = await judge(key, got.aggregate);
|
|
718
565
|
if (judged.outcome === "interrupted") {
|
|
719
566
|
return refused("interrupted");
|
|
@@ -766,13 +613,13 @@ export function openConductor(options) {
|
|
|
766
613
|
if (doneFailure !== undefined) {
|
|
767
614
|
return doneFailure;
|
|
768
615
|
}
|
|
769
|
-
await destroy(
|
|
770
|
-
await ledger.clearWorkspace(
|
|
616
|
+
await destroy(ticketPath);
|
|
617
|
+
await ledger.clearWorkspace(key);
|
|
771
618
|
return { outcome: "done", key };
|
|
772
619
|
}
|
|
773
620
|
async function doMerging(key) {
|
|
774
621
|
const got = await ledger.get(key);
|
|
775
|
-
const
|
|
622
|
+
const ticketPath = declaredTicketPath(workspaceRoot, key, got.ok ? got.aggregate : undefined);
|
|
776
623
|
const title = got.ok
|
|
777
624
|
? (got.aggregate.intention.title ?? got.aggregate.intention.intention)
|
|
778
625
|
: key;
|
|
@@ -780,7 +627,7 @@ export function openConductor(options) {
|
|
|
780
627
|
id: key,
|
|
781
628
|
context: key,
|
|
782
629
|
parent: workLineStable,
|
|
783
|
-
child:
|
|
630
|
+
child: ticketPath,
|
|
784
631
|
durationMs,
|
|
785
632
|
subject: foldMessageOf(title),
|
|
786
633
|
mergeSubject: `Merge ${subjectOf(title)}`,
|
|
@@ -799,15 +646,30 @@ export function openConductor(options) {
|
|
|
799
646
|
if (failure !== undefined) {
|
|
800
647
|
return failure;
|
|
801
648
|
}
|
|
802
|
-
await destroy(
|
|
803
|
-
await ledger.clearWorkspace(
|
|
649
|
+
await destroy(ticketPath);
|
|
650
|
+
await ledger.clearWorkspace(key);
|
|
804
651
|
return { outcome: "done", key };
|
|
805
652
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
653
|
+
/**
|
|
654
|
+
* The assembled work is in the workspace and nowhere else. When it is gone,
|
|
655
|
+
* making it again from the work line would drop what was made, and when the
|
|
656
|
+
* Authority holds a Submission built from it, publish over what it was
|
|
657
|
+
* shown. Either is a human's call, not this loop's.
|
|
658
|
+
*/
|
|
659
|
+
async function workspaceGone(key) {
|
|
660
|
+
const got = await ledger.get(key);
|
|
661
|
+
if (!got.ok) {
|
|
662
|
+
return refused("not-found");
|
|
810
663
|
}
|
|
664
|
+
if (existsSync(declaredTicketPath(workspaceRoot, key, got.aggregate))) {
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
return await freeze(key, {
|
|
668
|
+
kind: got.aggregate.submission === undefined ? "assembly" : "submitted",
|
|
669
|
+
report: "The workspace the work was assembled in is gone.",
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
async function drive(key) {
|
|
811
673
|
const runningResult = await doRunning(key);
|
|
812
674
|
if (runningResult !== undefined) {
|
|
813
675
|
return runningResult;
|
|
@@ -822,6 +684,13 @@ export function openConductor(options) {
|
|
|
822
684
|
if (got.aggregate.state === "running") {
|
|
823
685
|
return { outcome: "paused", key };
|
|
824
686
|
}
|
|
687
|
+
if (got.aggregate.state === "done") {
|
|
688
|
+
return { outcome: "done", key };
|
|
689
|
+
}
|
|
690
|
+
const gone = await workspaceGone(key);
|
|
691
|
+
if (gone !== undefined) {
|
|
692
|
+
return gone;
|
|
693
|
+
}
|
|
825
694
|
if (got.aggregate.state === "integrating") {
|
|
826
695
|
const integratingResult = await doIntegrating(key);
|
|
827
696
|
if (integratingResult !== undefined) {
|
|
@@ -887,7 +756,7 @@ export function openConductor(options) {
|
|
|
887
756
|
},
|
|
888
757
|
async release(key) {
|
|
889
758
|
const before = await ledger.get(key);
|
|
890
|
-
const child = before.ok ? before.aggregate.
|
|
759
|
+
const child = before.ok ? before.aggregate.workspace : undefined;
|
|
891
760
|
const released = await ledger.releaseBail(key);
|
|
892
761
|
if (released.ok && child !== undefined) {
|
|
893
762
|
await destroy(child);
|