@zq-silk/yui 2.10.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifacts/artifactCapability.js +14 -8
- package/dist/artifacts/artifactCommitLock.js +7 -5
- package/dist/artifacts/jobArtifact.js +93 -0
- package/dist/artifacts/jobArtifactTransport.js +13 -0
- package/dist/artifacts/taskArtifactRepository.js +58 -10
- package/dist/cli/callerDiscovery.js +18 -0
- package/dist/cli/commandCatalog.js +45 -14
- package/dist/cli/commandDiscovery.js +2 -3
- package/dist/commands/durableJobCommands.js +31 -6
- package/dist/commands/globalRoleCommands.js +5 -0
- package/dist/commands/scheduleCommands.js +14 -3
- package/dist/commands/taskCommands.js +5 -2
- package/dist/context/taskCatalog.js +6 -1
- package/dist/controlPlaneCli.js +12 -12
- package/dist/controller/agentJobControl.js +391 -0
- package/dist/controller/controller.js +24 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
- package/dist/controller/globalInputDelivery.js +8 -1
- package/dist/controller/jobArtifactControl.js +111 -0
- package/dist/controller/jobControl.js +4 -0
- package/dist/controller/jobDefinitionControl.js +191 -19
- package/dist/controller/jobSupervisor.js +83 -3
- package/dist/controller/jobTaskDelegationControl.js +193 -0
- package/dist/controller/runtime.js +7 -1
- package/dist/controller/scheduleControl.js +83 -20
- package/dist/integration/gitIntegrationService.js +2 -0
- package/dist/job/agentJob.js +131 -0
- package/dist/job/agentJobObservation.js +61 -0
- package/dist/job/agentRoleScope.js +33 -0
- package/dist/job/durableJob.js +63 -28
- package/dist/job/jobAssignmentScope.js +1 -0
- package/dist/job/jobBusinessResult.js +72 -0
- package/dist/job/jobCleanupInspection.js +4 -0
- package/dist/job/jobDefinition.js +26 -2
- package/dist/job/jobRunner.js +4 -3
- package/dist/job/projectRefreshRunner.js +54 -18
- package/dist/job/projectSync.js +162 -0
- package/dist/job/scheduleService.js +55 -0
- package/dist/job/taskDelegation.js +48 -0
- package/dist/message/messageContinuation.js +1 -1
- package/dist/resources/projectResourceService.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/role/role.js +22 -12
- package/dist/runtime/managedIdentity.js +5 -3
- package/dist/runtime/sessionCaller.js +9 -10
- package/dist/schedule/schedule.js +65 -15
- package/dist/scheduler/wakeReason.js +4 -0
- package/dist/storage/agentJobSchema.js +10 -0
- package/dist/storage/maintenanceCaller.js +17 -25
- package/dist/storage/nativeSessionLookup.js +23 -0
- package/dist/storage/recordValidation.js +2 -0
- package/dist/storage/sessionHistorySchema.js +17 -0
- package/dist/storage/sqliteSchema.js +96 -11
- package/dist/storage/sqliteStore.js +71 -4
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +4 -0
- package/dist/storage/taskCatalog.js +10 -2
- package/dist/storage/upgrade/jobBusinessResultMigration.js +20 -0
- package/dist/storage/upgrade/scheduleDispatchMigration.js +15 -0
- package/dist/task/archivePreflight.js +15 -0
- package/dist/task/completionReadiness.js +7 -0
- package/dist/task/task.js +17 -1
- package/dist/verification/verificationGateService.js +4 -0
- package/dist/web/assets/client/app/catalog.js +16 -0
- package/dist/web/assets/client/i18n/en.js +29 -2
- package/dist/web/assets/client/i18n/zh-CN.js +29 -2
- package/dist/web/assets/client/layout/manageShell.js +9 -0
- package/dist/web/assets/client/layout/router.js +1 -0
- package/dist/web/assets/client/views/jobs.js +166 -37
- package/dist/web/assets/client/views/task/files.js +5 -4
- package/dist/web/assets/client/views/task/page.js +5 -1
- package/dist/web/taskPreviews.js +2 -2
- package/dist/web/webJobs.js +36 -11
- package/dist/web/webMaterials.js +21 -9
- package/dist/web/webServer.js +6 -5
- package/dist/web/webTaskSurface.js +1 -0
- package/docs/authorization.md +130 -0
- package/docs/job-runs.md +392 -17
- package/docs/managed-turn-and-session-runtime.md +10 -5
- package/docs/managed-turn-and-session-runtime.zh-CN.md +12 -5
- package/docs/release-workflow.md +79 -0
- package/docs/release-workflow.zh-CN.md +58 -0
- package/docs/schedules.md +69 -15
- package/docs/storage-baseline.md +45 -1
- package/docs/storage-baseline.zh-CN.md +33 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-runtime/SKILL.md +12 -5
- package/skills/yui-runtime/VERSION +1 -1
|
@@ -20,6 +20,7 @@ import { recordOperationEvidence } from "../kernel/operationFacts.js";
|
|
|
20
20
|
import { wakeReason } from "../scheduler/wakeReason.js";
|
|
21
21
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
22
22
|
import { readProcessStartIdentity } from "../core/fileLockOwner.js";
|
|
23
|
+
import { processBusinessResult, resultDigest, runReference, validateBusinessResult } from "../job/jobBusinessResult.js";
|
|
23
24
|
const DEFAULT_STEP_TIMEOUT_MS = 30 * 60_000;
|
|
24
25
|
const HEARTBEAT_STALE_MS = 2 * 60_000;
|
|
25
26
|
const SIGKILL_GRACE_MS = 30_000;
|
|
@@ -31,6 +32,7 @@ export class DurableJobSupervisor {
|
|
|
31
32
|
#wake;
|
|
32
33
|
#onError;
|
|
33
34
|
#authorizeStart;
|
|
35
|
+
#reconcileDelegation;
|
|
34
36
|
// f5: Composite key (taskId/jobId) because job IDs are Task-local — every
|
|
35
37
|
// Task has a job-1, so a Task-local key would cross-kill healthy runners.
|
|
36
38
|
#sigkillAt = new Map();
|
|
@@ -42,6 +44,7 @@ export class DurableJobSupervisor {
|
|
|
42
44
|
this.#wake = options.wake ?? (() => undefined);
|
|
43
45
|
this.#onError = options.onError ?? (() => undefined);
|
|
44
46
|
this.#authorizeStart = options.authorizeStart;
|
|
47
|
+
this.#reconcileDelegation = options.reconcileDelegation;
|
|
45
48
|
}
|
|
46
49
|
reconcile(now) {
|
|
47
50
|
const jobs = this.#store.listActiveDurableJobs();
|
|
@@ -67,7 +70,38 @@ export class DurableJobSupervisor {
|
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
}
|
|
73
|
+
/** Explicit collection only. Never admits, spawns, cancels or retries work. */
|
|
74
|
+
confirmResult(namespace, runId, now) {
|
|
75
|
+
const run = this.#store.listActiveDurableJobs()
|
|
76
|
+
.find(run => executionNamespace(run) === namespace && run.id === runId);
|
|
77
|
+
if (!run)
|
|
78
|
+
return;
|
|
79
|
+
if (run.delegation) {
|
|
80
|
+
this.#reconcileDelegation?.(run, now);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (run.status === "queued") {
|
|
84
|
+
const spawned = this.#spawnedProcessFromEvidence(run, this.#artifacts.readStartMarker(namespace, runId));
|
|
85
|
+
if (spawned)
|
|
86
|
+
this.#adoptAndContinue(run, spawned, now);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const exit = this.#artifacts.readExitJson(namespace, runId);
|
|
90
|
+
if (exit)
|
|
91
|
+
this.#harvestExit(run, exit, now);
|
|
92
|
+
else if (run.process) {
|
|
93
|
+
const identity = this.#process.processStartIdentity(run.process.pid);
|
|
94
|
+
if (identity === undefined || identity !== run.process.startIdentity
|
|
95
|
+
|| !this.#process.isProcessAlive(run.process.pid))
|
|
96
|
+
this.#handleDeadProcess(run, now);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
70
100
|
#reconcileJob(job, now) {
|
|
101
|
+
if (job.delegation) {
|
|
102
|
+
this.#reconcileDelegation?.(job, now);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
71
105
|
if (job.status === "queued") {
|
|
72
106
|
this.#reconcileQueued(job, now);
|
|
73
107
|
return;
|
|
@@ -155,6 +189,8 @@ export class DurableJobSupervisor {
|
|
|
155
189
|
}
|
|
156
190
|
}
|
|
157
191
|
#startJob(job, now) {
|
|
192
|
+
if (job.delegation)
|
|
193
|
+
throw new Error("Task delegation cannot start a process.");
|
|
158
194
|
try {
|
|
159
195
|
this.#authorizeStart(job);
|
|
160
196
|
}
|
|
@@ -298,6 +334,7 @@ export class DurableJobSupervisor {
|
|
|
298
334
|
evidenceSource: "exit-artifact",
|
|
299
335
|
steps: exit.steps
|
|
300
336
|
};
|
|
337
|
+
const business = this.#businessResult(job, result);
|
|
301
338
|
// rr4/finding-7: The terminal transition and the Leader wakeup must be
|
|
302
339
|
// atomic. Compose complete + wakeupNotified and pass the wakeup param so
|
|
303
340
|
// the adapter enqueues the Leader mailbox entry in the same transaction.
|
|
@@ -306,11 +343,11 @@ export class DurableJobSupervisor {
|
|
|
306
343
|
const terminal = this.#store.transitionDurableJob(executionNamespace(job), job.id, (current) => {
|
|
307
344
|
let completed;
|
|
308
345
|
try {
|
|
309
|
-
completed = completeDurableJob(current, result, now);
|
|
346
|
+
completed = completeDurableJob(current, result, now, business);
|
|
310
347
|
}
|
|
311
348
|
catch {
|
|
312
349
|
completed = completeDurableJob(current, {
|
|
313
|
-
outcome: "
|
|
350
|
+
outcome: "unknown-needs-attention", exitCode: null, signal: null,
|
|
314
351
|
unknownReason: "runner output is invalid; original receipt is retained",
|
|
315
352
|
steps: []
|
|
316
353
|
}, now);
|
|
@@ -329,7 +366,7 @@ export class DurableJobSupervisor {
|
|
|
329
366
|
// The runner died without exit.json, but its step checkpoint covered
|
|
330
367
|
// every planned step: the outcome is still provable from durable
|
|
331
368
|
// evidence. No false pass — the checkpoint is the runner's own record.
|
|
332
|
-
const terminal = this.#store.transitionDurableJob(executionNamespace(job), job.id, (current) => markDurableJobWakeupNotified(completeDurableJob(current, { ...proven, evidenceSource: "checkpoint" }, now), now), now, { reason: wakeReason("job-finished"), refs: wakeupRefs(job) });
|
|
369
|
+
const terminal = this.#store.transitionDurableJob(executionNamespace(job), job.id, (current) => markDurableJobWakeupNotified(completeDurableJob(current, { ...proven, evidenceSource: "checkpoint" }, now, this.#businessResult(current, { ...proven, evidenceSource: "checkpoint" })), now), now, { reason: wakeReason("job-finished"), refs: wakeupRefs(job) });
|
|
333
370
|
this.#deliverTerminalEvent(terminal);
|
|
334
371
|
}
|
|
335
372
|
else {
|
|
@@ -339,6 +376,34 @@ export class DurableJobSupervisor {
|
|
|
339
376
|
}
|
|
340
377
|
this.#sigkillAt.delete(this.#sigkillKey(job));
|
|
341
378
|
}
|
|
379
|
+
#businessResult(job, result) {
|
|
380
|
+
const fallback = processBusinessResult(job, result);
|
|
381
|
+
if ("taskId" in job)
|
|
382
|
+
return fallback;
|
|
383
|
+
// The runner records this only when its cancel fence is seen before the
|
|
384
|
+
// first step. No domain action ran, so no business receipt is expected.
|
|
385
|
+
if (result.outcome === "cancelled" && result.steps.length === 0)
|
|
386
|
+
return fallback;
|
|
387
|
+
if (job.action === "system.noop")
|
|
388
|
+
return { ...fallback, data: { action: "system.noop" } };
|
|
389
|
+
const raw = this.#artifacts.readBusinessResult?.(executionNamespace(job), job.id);
|
|
390
|
+
if (raw !== null && raw !== undefined) {
|
|
391
|
+
try {
|
|
392
|
+
const business = validateBusinessResult(JSON.parse(raw), runReference(job));
|
|
393
|
+
const sourceRun = runReference(job);
|
|
394
|
+
return { ...business, evidence: [...business.evidence, {
|
|
395
|
+
kind: "execution-evidence", owner: sourceRun, sourceRun,
|
|
396
|
+
version: resultDigest(raw), locator: `${job.artifactsLocator}/business-result.json`
|
|
397
|
+
}, ...fallback.evidence] };
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
return { ...fallback, outcome: "unknown-needs-attention",
|
|
401
|
+
summary: "Invalid business result receipt; inspect retained execution evidence. Do not replay the action." };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return { ...fallback, outcome: "unknown-needs-attention",
|
|
405
|
+
summary: "Business result receipt is missing; process completion does not prove the business outcome. Do not replay the action." };
|
|
406
|
+
}
|
|
342
407
|
/**
|
|
343
408
|
* f7/rr5: Deliver a terminal notice to the runtime inbox channel when a
|
|
344
409
|
* Job reaches a terminal state. The notice is best-effort: the terminal
|
|
@@ -375,6 +440,8 @@ function wakeupRefs(job) {
|
|
|
375
440
|
* checkpoint covers every planned step exactly, by name.
|
|
376
441
|
*/
|
|
377
442
|
function proveOutcomeFromCheckpoint(job, checkpoint) {
|
|
443
|
+
if (job.delegation)
|
|
444
|
+
return null;
|
|
378
445
|
const byName = new Map(checkpoint.completedSteps.map((step) => [step.name, step]));
|
|
379
446
|
if (byName.size !== checkpoint.completedSteps.length)
|
|
380
447
|
return null;
|
|
@@ -501,6 +568,19 @@ export function createFileArtifactPort(home) {
|
|
|
501
568
|
},
|
|
502
569
|
readReadyFile(taskId, jobId) {
|
|
503
570
|
return readJsonFile(join(home, "artifacts", "jobs", taskId, jobId, "ready.json"));
|
|
571
|
+
},
|
|
572
|
+
readBusinessResult(taskId, jobId) {
|
|
573
|
+
const path = join(home, "artifacts", "jobs", taskId, jobId, "business-result.json");
|
|
574
|
+
try {
|
|
575
|
+
if (statSync(path).size > 256 * 1024)
|
|
576
|
+
return "oversized business result";
|
|
577
|
+
return readFileSync(path, "utf8");
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
if (error.code === "ENOENT")
|
|
581
|
+
return null;
|
|
582
|
+
throw error; // A transient collection failure is not absence or permission to rerun.
|
|
583
|
+
}
|
|
504
584
|
}
|
|
505
585
|
};
|
|
506
586
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { createTaskAggregate } from "../commands/taskCommands.js";
|
|
3
|
+
import { isDurableJobTerminal, validateDurableJob } from "../job/durableJob.js";
|
|
4
|
+
import { resultDigest, runReference } from "../job/jobBusinessResult.js";
|
|
5
|
+
import { validateTaskJobConfiguration, workItemDefinitionDigest } from "../job/taskDelegation.js";
|
|
6
|
+
import { assertCallerAuthorized } from "./jobControl.js";
|
|
7
|
+
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
8
|
+
import { isDeepStrictEqual } from "node:util";
|
|
9
|
+
/** Scope authorization is performed by the Job ingress; provenance adds none. */
|
|
10
|
+
export function authorizeTaskJob(store, job, caller, write) {
|
|
11
|
+
if (job.configuration.kind !== "task")
|
|
12
|
+
throw new Error("Not a Task delegation.");
|
|
13
|
+
validateTaskJobConfiguration(job.configuration);
|
|
14
|
+
if (caller.scope === "task") {
|
|
15
|
+
if (!write && caller.role === "leader")
|
|
16
|
+
return;
|
|
17
|
+
// Creating another Task is a cross-Task effect, not inherited authority.
|
|
18
|
+
if (caller.role !== "leader" || job.configuration.target.kind === "new"
|
|
19
|
+
|| job.configuration.target.taskId !== caller.taskId) {
|
|
20
|
+
throw new Error("Task delegation requires exact target authority; source Job scope grants no cross-Task permission.");
|
|
21
|
+
}
|
|
22
|
+
assertCallerAuthorized(store, caller, caller.taskId);
|
|
23
|
+
if (write) {
|
|
24
|
+
const session = store.getTaskRoleSessionSet(caller.taskId, "leader");
|
|
25
|
+
const effective = session?.sessions[session.activeAgentId]?.effective;
|
|
26
|
+
if (effective?.executionAuthority !== "delivery" || effective.profileAccess !== "write") {
|
|
27
|
+
throw new Error("Task delegation requires a current writable delivery Session.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!write)
|
|
32
|
+
return;
|
|
33
|
+
if (job.scope.kind === "task") {
|
|
34
|
+
const source = store.getTask(job.scope.taskId);
|
|
35
|
+
if (!source || source.status !== "active" || source.executionGate.state !== "enabled")
|
|
36
|
+
throw new Error("Source Task is not executable.");
|
|
37
|
+
}
|
|
38
|
+
const target = job.configuration.target;
|
|
39
|
+
if (target.kind === "new") {
|
|
40
|
+
for (const binding of target.projectBindings) {
|
|
41
|
+
const project = store.listProjects().find(project => project.id === binding.projectId);
|
|
42
|
+
if (!project || project.status !== "active")
|
|
43
|
+
throw new Error("Target Project is not active.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const task = store.getTask(target.taskId);
|
|
48
|
+
const work = store.getWorkItem(target.taskId, target.workItemId);
|
|
49
|
+
if (!task || task.status !== "active" || task.executionGate.state !== "enabled"
|
|
50
|
+
|| !work || work.status !== "open" || work.revision !== target.revision) {
|
|
51
|
+
throw new Error("An exact open WorkItem revision in an executable Task is required.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Inside Run admission's transaction: Task/Leader + Run + association commit together. */
|
|
56
|
+
export function admitTaskJob(store, job, caller, trigger, requestId, now, retryOf) {
|
|
57
|
+
const config = job.configuration;
|
|
58
|
+
const id = job.scope.kind === "global" ? `job-${randomUUID()}` : store.nextDurableJobId(job.scope.taskId);
|
|
59
|
+
const sourceRun = { scope: job.scope, runId: id };
|
|
60
|
+
const target = config.target;
|
|
61
|
+
const taskId = target.kind === "new" ? createTaskAggregate(store, target.title, {
|
|
62
|
+
description: target.description, projectBindings: target.projectBindings,
|
|
63
|
+
...(target.type === undefined ? {} : { type: target.type })
|
|
64
|
+
}, now, [], { kind: "job-run", jobId: job.id, run: sourceRun }).task.id : target.taskId;
|
|
65
|
+
const delegation = { taskId, goal: config.goal,
|
|
66
|
+
...(target.kind === "work-item" ? { workItemId: target.workItemId, workItemRevision: target.revision,
|
|
67
|
+
workItemDefinitionDigest: workItemDefinitionDigest(store.getWorkItem(taskId, target.workItemId)) } : {}) };
|
|
68
|
+
const actorId = caller.scope === "user" ? "user" : `${caller.scope}:${caller.role}`;
|
|
69
|
+
const inputDigest = resultDigest(JSON.stringify([job, trigger, retryOf]));
|
|
70
|
+
const locator = `artifacts/jobs/${job.scope.kind === "global" ? "global" : job.scope.taskId}/${id}`;
|
|
71
|
+
const run = validateDurableJob({
|
|
72
|
+
schemaVersion: 1,
|
|
73
|
+
...(job.scope.kind === "global" ? { scope: "global" }
|
|
74
|
+
: { taskId: job.scope.taskId, owner: { kind: "task" } }),
|
|
75
|
+
id, definitionId: job.id, definitionRevision: job.revision, trigger,
|
|
76
|
+
delegation, artifactsLocator: locator,
|
|
77
|
+
idempotencyKey: createHash("sha256").update(JSON.stringify([actorId, requestId])).digest("hex"),
|
|
78
|
+
operation: { requestId, actorId, inputDigest, authorityRef: inputDigest,
|
|
79
|
+
targetId: taskId, capability: "job.run", implementation: { id: "yui:task-delegation", generation: "1" },
|
|
80
|
+
effect: "confirmed", receiptRefs: [`tasks/${taskId}`], partialResultRefs: [] },
|
|
81
|
+
status: "running", startedAt: now.toISOString(),
|
|
82
|
+
...(retryOf === undefined ? {} : { retryOf }),
|
|
83
|
+
createdAt: now.toISOString(), updatedAt: now.toISOString()
|
|
84
|
+
});
|
|
85
|
+
saveRun(store, run);
|
|
86
|
+
if (config.goal === "task-created")
|
|
87
|
+
return reconcileTaskDelegation(store, run, now);
|
|
88
|
+
return run;
|
|
89
|
+
}
|
|
90
|
+
function saveRun(store, run) {
|
|
91
|
+
if ("taskId" in run)
|
|
92
|
+
store.saveDurableJob(run.taskId, run);
|
|
93
|
+
else
|
|
94
|
+
store.saveGlobalJobRun(run);
|
|
95
|
+
}
|
|
96
|
+
/** Collection is transactional, repeatable, and never executes/activates/stops a Task. */
|
|
97
|
+
export function reconcileTaskDelegation(store, observed, now) {
|
|
98
|
+
return store.transaction(tx => {
|
|
99
|
+
const run = "taskId" in observed ? tx.getDurableJob(observed.taskId, observed.id) : tx.getGlobalJobRun(observed.id);
|
|
100
|
+
if (!run || !run.delegation || isDurableJobTerminal(run.status))
|
|
101
|
+
return run ?? observed;
|
|
102
|
+
const link = run.delegation;
|
|
103
|
+
const task = tx.getTask(link.taskId);
|
|
104
|
+
if (!task)
|
|
105
|
+
throw new Error("Delegated Task is missing; preserve the exact Run for inspection.");
|
|
106
|
+
if (link.goal !== "work-item" && (task.source?.jobId !== run.definitionId
|
|
107
|
+
|| !isDeepStrictEqual(task.source.run, runReference(run)))) {
|
|
108
|
+
throw new Error("Delegated Task creation source does not match the exact Run.");
|
|
109
|
+
}
|
|
110
|
+
let outcome;
|
|
111
|
+
let summary = "";
|
|
112
|
+
let evidence = [];
|
|
113
|
+
if (link.goal === "task-created") {
|
|
114
|
+
outcome = "succeeded";
|
|
115
|
+
summary = `Explicit creation goal met: ${task.id}; no delivery completion is claimed.`;
|
|
116
|
+
const event = tx.listEventsByType(task.id, ["task.created"])[0];
|
|
117
|
+
if (!event)
|
|
118
|
+
throw new Error("Delegated Task creation evidence is missing.");
|
|
119
|
+
evidence = [{ kind: "task-result", owner: { kind: "task", taskId: task.id },
|
|
120
|
+
sourceRun: runReference(run), version: resultDigest(JSON.stringify(event)), locator: `events/${event.id}` }];
|
|
121
|
+
}
|
|
122
|
+
else if (link.goal === "work-item") {
|
|
123
|
+
const work = tx.getWorkItem(task.id, link.workItemId);
|
|
124
|
+
if (!work || workItemDefinitionDigest(work) !== link.workItemDefinitionDigest) {
|
|
125
|
+
outcome = "unknown-needs-attention";
|
|
126
|
+
summary = "Delegated WorkItem requirements changed or are missing; inspect the exact scope.";
|
|
127
|
+
}
|
|
128
|
+
else if (work.status === "retired") {
|
|
129
|
+
outcome = "failed";
|
|
130
|
+
summary = `Delegated WorkItem retired: ${work.id}.`;
|
|
131
|
+
}
|
|
132
|
+
else if (work.status === "accepted") {
|
|
133
|
+
const candidate = work.candidates.find(candidate => candidate.id === work.acceptedCandidateId
|
|
134
|
+
&& candidate.workItemRevision >= link.workItemRevision);
|
|
135
|
+
if (!candidate)
|
|
136
|
+
throw new Error("Accepted delegation has no exact Candidate; preserve evidence.");
|
|
137
|
+
outcome = "succeeded";
|
|
138
|
+
summary = work.outcome ?? candidate.summary;
|
|
139
|
+
evidence = [{ kind: "work-item-result", owner: { kind: "task", taskId: task.id },
|
|
140
|
+
sourceRun: runReference(run), version: resultDigest(JSON.stringify(candidate)),
|
|
141
|
+
locator: `work-items/${work.id}/candidates/${candidate.id}` }];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// The never-rewritten completion event identifies the exact lifecycle,
|
|
146
|
+
// even if the Task is reopened before this collector runs.
|
|
147
|
+
const event = tx.listEventsByType(task.id, ["task.completed"]).find(event => event.createdAt >= run.createdAt);
|
|
148
|
+
if (event) {
|
|
149
|
+
outcome = "succeeded";
|
|
150
|
+
summary = String(event.payload.summary);
|
|
151
|
+
evidence = [{ kind: "task-result", owner: { kind: "task", taskId: task.id },
|
|
152
|
+
sourceRun: runReference(run), version: resultDigest(JSON.stringify(event)), locator: `events/${event.id}` }];
|
|
153
|
+
}
|
|
154
|
+
else if (task.status === "cancelled" || (task.status === "archived" && task.completedAt === undefined)) {
|
|
155
|
+
outcome = "failed";
|
|
156
|
+
summary = `Delegated Task ended without the requested delivery: ${task.id}.`;
|
|
157
|
+
}
|
|
158
|
+
else if (task.completedAt !== undefined) {
|
|
159
|
+
outcome = "unknown-needs-attention";
|
|
160
|
+
summary = "Task reports completion without immutable completion-event evidence; inspect the target.";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Like process collection, already-proven delivery wins over a late cancel.
|
|
164
|
+
// Cancelling an unsettled wait never fabricates target resource quiescence.
|
|
165
|
+
if (!outcome && run.cancelRequestedAt) {
|
|
166
|
+
outcome = "cancelled";
|
|
167
|
+
summary = "This Run's delegation was cancelled; the target Task/Leader/WorkItem was not stopped. Inspect its own resources separately.";
|
|
168
|
+
}
|
|
169
|
+
if (!outcome)
|
|
170
|
+
return run;
|
|
171
|
+
const businessResult = { schemaVersion: 1, sourceRun: runReference(run), outcome,
|
|
172
|
+
summary, evidence };
|
|
173
|
+
const next = validateDurableJob({ ...run, status: outcome, businessResult, wakeupNotified: true,
|
|
174
|
+
terminalAt: now.toISOString(), updatedAt: now.toISOString() });
|
|
175
|
+
saveRun(tx, next);
|
|
176
|
+
if ("taskId" in run)
|
|
177
|
+
enqueueWork(tx, { kind: "role", taskId: run.taskId, roleName: "leader" }, "job-finished", now, [{ type: "task", id: run.taskId }]);
|
|
178
|
+
else if (run.operation.actorId !== "user")
|
|
179
|
+
enqueueWork(tx, { kind: "operator" }, "global-job-finished", now, [{ type: "global-job-run", id: run.id }], { source: "global-job", dedupeKey: `operator-global-job-run:${run.id}` });
|
|
180
|
+
return next;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/** Bounded projection only; source permissions never open the target's Context. */
|
|
184
|
+
export function taskDelegationView(store, run) {
|
|
185
|
+
if (!run.delegation)
|
|
186
|
+
return null;
|
|
187
|
+
const task = store.getTask(run.delegation.taskId);
|
|
188
|
+
return { ...run.delegation, taskStatus: task?.status ?? null,
|
|
189
|
+
executionGate: task?.executionGate.state ?? null,
|
|
190
|
+
openInputs: store.listInputRequests(run.delegation.taskId).filter(input => input.status === "open").length,
|
|
191
|
+
result: run.businessResult ?? null,
|
|
192
|
+
cancellation: run.cancelRequestedAt ? "requested; target resources not stopped" : null };
|
|
193
|
+
}
|
|
@@ -57,7 +57,9 @@ import { AgentHostProviderSessionBusyError, AgentHostProviderTurnFenceError, Fil
|
|
|
57
57
|
import { deliverGlobalInputs } from "./globalInputDelivery.js";
|
|
58
58
|
import { authorizeJobStart } from "./jobControl.js";
|
|
59
59
|
import { authorizeGlobalJobStart, createJobDefinitionControl } from "./jobDefinitionControl.js";
|
|
60
|
+
import { reconcileTaskDelegation } from "./jobTaskDelegationControl.js";
|
|
60
61
|
import { createScheduleControl, reconcileSchedules } from "./scheduleControl.js";
|
|
62
|
+
import { reconcileProjectSync } from "../job/projectSync.js";
|
|
61
63
|
import { createFileArtifactPort, createLinuxProcessPort, DurableJobSupervisor } from "./jobSupervisor.js";
|
|
62
64
|
import { createProviderRetryHooks } from "./providerRetryDelivery.js";
|
|
63
65
|
import { createRuntimeResourceActivityTracker } from "./resourceInventory.js";
|
|
@@ -488,6 +490,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
488
490
|
process: kernel.runner,
|
|
489
491
|
artifacts: createFileArtifactPort(home),
|
|
490
492
|
authorizeStart: (job) => "taskId" in job ? authorizeJobStart(store, job) : authorizeGlobalJobStart(store, job),
|
|
493
|
+
reconcileDelegation: (job, now) => { reconcileTaskDelegation(store, job, now); },
|
|
491
494
|
// rr6/f1: Bounded supervision wake. The supervisor signals the Controller
|
|
492
495
|
// after spawning a runner (queued→running adoption) and when a runner
|
|
493
496
|
// exits (terminal harvest), so a quick job converges without waiting for
|
|
@@ -575,7 +578,10 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
575
578
|
jobDefinitions: createJobDefinitionControl(store, home),
|
|
576
579
|
schedules: {
|
|
577
580
|
control: createScheduleControl(store, home),
|
|
578
|
-
reconcile: now =>
|
|
581
|
+
reconcile: now => {
|
|
582
|
+
reconcileProjectSync(store, now, options.onError);
|
|
583
|
+
reconcileSchedules(store, home, now, options.onError);
|
|
584
|
+
}
|
|
579
585
|
},
|
|
580
586
|
capabilityDispatcher: createCapabilityDispatcher(kernel.capabilities),
|
|
581
587
|
...(continuationReconciler === undefined ? {} : { continuationReconciler }),
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { requireKnownFields } from "../domain/validation.js";
|
|
2
|
-
import { nextScheduleDue, scheduleId, validateSchedule } from "../schedule/schedule.js";
|
|
3
|
-
import { admitJobRun, authorizeDefinition, authorizeScope,
|
|
3
|
+
import { nextScheduleDue, scheduleId, validateSchedule, validateScheduleTarget } from "../schedule/schedule.js";
|
|
4
|
+
import { admitJobRun, instantiateJobRun, authorizeDefinition, authorizeScope, authorizeRunRead } from "./jobDefinitionControl.js";
|
|
4
5
|
import { jobAuthorityBinding } from "./jobControl.js";
|
|
5
6
|
import { createGlobalRoleMessage } from "../message/message.js";
|
|
6
7
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
7
8
|
import { enqueueSupervisorEvent } from "../scheduler/operatorEvent.js";
|
|
8
9
|
import { redactLaunchText } from "../runtime/launchDiagnostics.js";
|
|
10
|
+
import { requireScheduleService, scheduleServiceBinding } from "../job/scheduleService.js";
|
|
11
|
+
import { updateProjectSync } from "../job/projectSync.js";
|
|
9
12
|
function binding(store, caller, scope) {
|
|
10
13
|
return jobAuthorityBinding(store, caller.scope, caller.role, scope.kind === "task" ? scope.taskId : "");
|
|
11
14
|
}
|
|
@@ -19,13 +22,35 @@ function authorizeSchedule(store, schedule, caller, write) {
|
|
|
19
22
|
function fence(schedule, now) {
|
|
20
23
|
return new Date(Math.max(now.getTime(), Date.parse(schedule?.processedThrough ?? "") || 0));
|
|
21
24
|
}
|
|
22
|
-
|
|
25
|
+
function authorization(store, plan, job, caller, now, requested, previous) {
|
|
26
|
+
if (requested !== undefined && requested !== "session" && requested !== "service")
|
|
27
|
+
throw new Error("Invalid Schedule authorization choice.");
|
|
28
|
+
if (plan.target.kind === "new-job" && plan.target.definitionRevision !== job.revision) {
|
|
29
|
+
throw new Error("Source Job revision changed; select its exact available revision.");
|
|
30
|
+
}
|
|
31
|
+
if (requested === "service") {
|
|
32
|
+
const grantId = randomUUID();
|
|
33
|
+
return { kind: "service", service: "schedule", grantId, revoked: false, jobRevision: job.revision,
|
|
34
|
+
grantedBy: structuredClone(caller), grantedAt: now.toISOString(),
|
|
35
|
+
binding: scheduleServiceBinding(store, plan, job, grantId) };
|
|
36
|
+
}
|
|
37
|
+
if (requested === undefined && previous?.kind === "service") {
|
|
38
|
+
if (previous.revoked || previous.binding !== scheduleServiceBinding(store, plan, job, previous.grantId)) {
|
|
39
|
+
throw new Error("Changed or revoked service authority requires explicit authorization=service.");
|
|
40
|
+
}
|
|
41
|
+
return previous;
|
|
42
|
+
}
|
|
43
|
+
if (caller.scope === "user")
|
|
44
|
+
throw new Error("Public Schedule creation/rebinding requires explicit authorization=service.");
|
|
45
|
+
return { kind: "session", caller: structuredClone(caller), binding: binding(store, caller, plan.scope), jobRevision: job.revision };
|
|
46
|
+
}
|
|
47
|
+
export function createScheduleControl(store, _home) {
|
|
23
48
|
return (value, now) => {
|
|
24
49
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
25
50
|
throw new Error("Invalid Schedule request.");
|
|
26
|
-
requireKnownFields(value, ["op", "scope", "caller", "id", "jobId", "rule", "expectedRevision"], "Schedule request");
|
|
51
|
+
requireKnownFields(value, ["op", "scope", "caller", "id", "jobId", "target", "authorization", "rule", "expectedRevision"], "Schedule request");
|
|
27
52
|
const input = value;
|
|
28
|
-
authorizeScope(store, input.scope, input.caller);
|
|
53
|
+
authorizeScope(store, input.scope, input.caller, true);
|
|
29
54
|
return store.transaction(tx => {
|
|
30
55
|
if (input.op === "list") {
|
|
31
56
|
return tx.listSchedules(input.scope).filter(s => {
|
|
@@ -45,9 +70,13 @@ export function createScheduleControl(store, home) {
|
|
|
45
70
|
const job = tx.getJobDefinition(input.scope, input.jobId);
|
|
46
71
|
if (job === null)
|
|
47
72
|
throw new Error("Schedule Job definition is missing.");
|
|
73
|
+
if (job.builtin)
|
|
74
|
+
throw new Error("Builtin Job has one installed Schedule; manage that Schedule instead.");
|
|
48
75
|
if (previous !== null) {
|
|
49
76
|
authorizeSchedule(tx, previous, input.caller, false);
|
|
50
|
-
if (previous.jobId !== input.jobId || JSON.stringify(previous.rule) !== JSON.stringify(input.rule)
|
|
77
|
+
if (previous.jobId !== input.jobId || JSON.stringify(previous.rule) !== JSON.stringify(input.rule)
|
|
78
|
+
|| JSON.stringify(previous.target) !== JSON.stringify(input.target ?? { kind: "existing-job" })
|
|
79
|
+
|| (input.authorization !== undefined && previous.authorization.kind !== input.authorization)) {
|
|
51
80
|
throw new Error("Schedule id already exists; use update with its revision.");
|
|
52
81
|
}
|
|
53
82
|
return previous;
|
|
@@ -56,10 +85,12 @@ export function createScheduleControl(store, home) {
|
|
|
56
85
|
const due = nextScheduleDue(input.rule, now);
|
|
57
86
|
if (due === null)
|
|
58
87
|
throw new Error("A new once Schedule must be in the future.");
|
|
88
|
+
const target = validateScheduleTarget(input.target ?? { kind: "existing-job" });
|
|
59
89
|
const schedule = validateSchedule({
|
|
60
90
|
schemaVersion: 1, id: input.id, scope: structuredClone(input.scope), jobId: input.jobId,
|
|
91
|
+
target,
|
|
61
92
|
rule: structuredClone(input.rule), state: "enabled", revision: 1, generation: 1, nextDue: due,
|
|
62
|
-
authorization: {
|
|
93
|
+
authorization: authorization(tx, { id: input.id, scope: input.scope, jobId: input.jobId, target }, job, input.caller, now, input.authorization),
|
|
63
94
|
createdAt: now.toISOString(), updatedAt: now.toISOString()
|
|
64
95
|
});
|
|
65
96
|
tx.saveSchedule(schedule);
|
|
@@ -69,20 +100,40 @@ export function createScheduleControl(store, home) {
|
|
|
69
100
|
throw new Error("Schedule not found.");
|
|
70
101
|
authorizeSchedule(tx, previous, input.caller, false);
|
|
71
102
|
if (input.op === "show") {
|
|
72
|
-
const runs =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
103
|
+
const runs = input.scope.kind === "task" ? tx.listDurableJobs(input.scope.taskId) : tx.listGlobalJobRuns();
|
|
104
|
+
const ownRuns = runs.filter(run => {
|
|
105
|
+
if (run.trigger.source !== "schedule" || !run.trigger.key.startsWith(`${previous.id}:`))
|
|
106
|
+
return false;
|
|
107
|
+
try {
|
|
108
|
+
authorizeRunRead(tx, run, input.caller);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
76
115
|
const lastSuccess = ownRuns.filter(r => r.status === "succeeded")
|
|
77
116
|
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0] ?? null;
|
|
78
|
-
return { schedule: previous, lastSuccess };
|
|
117
|
+
return { schedule: previous, lastSuccess, runs: ownRuns };
|
|
79
118
|
}
|
|
80
119
|
if (previous.revision !== input.expectedRevision)
|
|
81
120
|
throw new Error("Schedule revision changed; read it before modifying.");
|
|
121
|
+
if (previous.builtin) {
|
|
122
|
+
if (input.jobId !== undefined || input.target !== undefined || input.authorization !== undefined) {
|
|
123
|
+
throw new Error("Builtin Schedule target and service registration cannot be overridden.");
|
|
124
|
+
}
|
|
125
|
+
return updateProjectSync(tx, previous, input.op, input.rule, now);
|
|
126
|
+
}
|
|
82
127
|
let next;
|
|
83
128
|
if (input.op === "disable") {
|
|
84
129
|
next = { ...previous, state: "disabled", reason: "Explicitly disabled; no future admission. Existing Runs are unchanged." };
|
|
85
130
|
}
|
|
131
|
+
else if (input.op === "revoke") {
|
|
132
|
+
if (previous.authorization.kind !== "service")
|
|
133
|
+
throw new Error("Only a service grant can be revoked; disable Session plans.");
|
|
134
|
+
next = { ...previous, state: "disabled", authorization: { ...previous.authorization, revoked: true },
|
|
135
|
+
reason: "Service authorization explicitly revoked; future admission and not-yet-started service Runs are denied." };
|
|
136
|
+
}
|
|
86
137
|
else if (input.op === "enable" || input.op === "update") {
|
|
87
138
|
if (input.op === "enable" && ["completed", "retired"].includes(previous.state)) {
|
|
88
139
|
throw new Error("Completed/retired Schedule requires an explicit future rule update.");
|
|
@@ -90,7 +141,10 @@ export function createScheduleControl(store, home) {
|
|
|
90
141
|
const jobId = input.op === "update" ? input.jobId ?? previous.jobId : previous.jobId;
|
|
91
142
|
scheduleId(jobId);
|
|
92
143
|
const rule = input.op === "update" ? input.rule : previous.rule;
|
|
144
|
+
const target = validateScheduleTarget(input.op === "update" ? input.target ?? previous.target : previous.target);
|
|
93
145
|
const job = authorizeSchedule(tx, { ...previous, jobId }, input.caller, true);
|
|
146
|
+
if (job.builtin)
|
|
147
|
+
throw new Error("Builtin Job has one installed Schedule.");
|
|
94
148
|
const after = fence(previous, now);
|
|
95
149
|
// Once overlap/disabled intent keeps its original due instant on enable.
|
|
96
150
|
const nextDue = input.op === "enable" && rule.kind === "once"
|
|
@@ -98,11 +152,11 @@ export function createScheduleControl(store, home) {
|
|
|
98
152
|
if (nextDue === null)
|
|
99
153
|
throw new Error("Rule must have a future occurrence.");
|
|
100
154
|
next = {
|
|
101
|
-
...previous, jobId, rule: structuredClone(rule), nextDue,
|
|
155
|
+
...previous, jobId, target, rule: structuredClone(rule), nextDue,
|
|
102
156
|
state: input.op === "enable" || previous.state === "enabled" ? "enabled" : "disabled",
|
|
103
157
|
generation: previous.generation + (input.op === "update" ? 1 : 0),
|
|
104
158
|
processedThrough: after.toISOString(),
|
|
105
|
-
authorization: {
|
|
159
|
+
authorization: authorization(tx, { ...previous, jobId, target }, job, input.caller, now, input.authorization, previous.authorization)
|
|
106
160
|
};
|
|
107
161
|
const { reason: _reason, ...withoutReason } = next;
|
|
108
162
|
next = withoutReason;
|
|
@@ -133,18 +187,27 @@ export function reconcileSchedules(store, home, now, onError = () => { }, limit
|
|
|
133
187
|
return;
|
|
134
188
|
}
|
|
135
189
|
}
|
|
136
|
-
const
|
|
190
|
+
const auth = schedule.authorization;
|
|
191
|
+
const service = auth.kind === "service" ? { scheduleId: schedule.id, grantId: auth.grantId } : undefined;
|
|
192
|
+
if (service)
|
|
193
|
+
requireScheduleService(tx, service);
|
|
194
|
+
const caller = auth.kind === "session" ? auth.caller : { scope: "user" };
|
|
195
|
+
const job = authorizeSchedule(tx, schedule, caller, true);
|
|
137
196
|
if (job.revision !== schedule.authorization.jobRevision
|
|
138
|
-
|| binding(tx,
|
|
197
|
+
|| (auth.kind === "session" && binding(tx, caller, schedule.scope) !== auth.binding)) {
|
|
139
198
|
throw new Error("Schedule authority or Job revision changed; inspect and explicitly enable.");
|
|
140
199
|
}
|
|
141
|
-
const
|
|
200
|
+
const trigger = { source: "schedule", key: `${schedule.id}:${schedule.generation}:${dueAt}` };
|
|
201
|
+
const result = schedule.target.kind === "existing-job"
|
|
202
|
+
? admitJobRun(tx, home, schedule.scope, schedule.jobId, caller, trigger, now, undefined, service)
|
|
203
|
+
: instantiateJobRun(tx, home, schedule.scope, schedule.jobId, schedule.target.definitionRevision, caller, trigger, now, service);
|
|
142
204
|
const overlap = result.reason === "overlap";
|
|
143
205
|
const nextDue = overlap && schedule.rule.kind === "once" ? dueAt : nextScheduleDue(schedule.rule, after);
|
|
144
206
|
tx.saveSchedule({
|
|
145
207
|
...schedule, nextDue, state: nextDue === null ? "completed" : "enabled",
|
|
146
208
|
processedThrough: after.toISOString(), revision: schedule.revision + 1,
|
|
147
|
-
lastAttempt: { at: now.toISOString(), dueAt,
|
|
209
|
+
lastAttempt: { at: now.toISOString(), dueAt, generation: schedule.generation,
|
|
210
|
+
outcome: overlap ? "overlap" : "admitted", jobId: result.run.definitionId, runId: result.run.id },
|
|
148
211
|
updatedAt: now.toISOString()
|
|
149
212
|
});
|
|
150
213
|
});
|
|
@@ -157,14 +220,14 @@ export function reconcileSchedules(store, home, now, onError = () => { }, limit
|
|
|
157
220
|
if (current?.state !== "enabled" || current.revision !== candidate.revision)
|
|
158
221
|
return;
|
|
159
222
|
tx.saveSchedule({ ...current, state: "needs-attention", reason, revision: current.revision + 1,
|
|
160
|
-
lastAttempt: { at: now.toISOString(), dueAt: current.nextDue, outcome: "blocked", reason },
|
|
223
|
+
lastAttempt: { at: now.toISOString(), dueAt: current.nextDue, generation: current.generation, outcome: "blocked", reason },
|
|
161
224
|
updatedAt: now.toISOString() });
|
|
162
225
|
if (current.scope.kind === "task") {
|
|
163
226
|
const event = createTaskEvent(tx.nextEventId(current.scope.taskId), current.scope.taskId, "schedule.needs-attention", { scheduleId: current.id, reason }, now);
|
|
164
227
|
tx.saveEvent(current.scope.taskId, event);
|
|
165
228
|
enqueueSupervisorEvent(tx, event, "leader", "schedule-needs-attention", now);
|
|
166
229
|
}
|
|
167
|
-
else {
|
|
230
|
+
else if (tx.getGlobalRole("operator") !== null) {
|
|
168
231
|
tx.saveGlobalRoleMessage(createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), "operator", `Yui-Summary: Schedule ${current.id} needs attention; no Run was admitted.\n`
|
|
169
232
|
+ `Reason: ${reason}\nRead: yui schedule show --global --schedule ${current.id}\n`
|
|
170
233
|
+ "Inspect the original intent before explicitly enabling; no new authority is implied.", "system", { type: "system" }, now, { inputControl: { action: "queue", requestId: `schedule-attention:${current.id}:${current.revision}` } }));
|
|
@@ -460,6 +460,8 @@ export class GitIntegrationService {
|
|
|
460
460
|
*/
|
|
461
461
|
async #resumeCheckJob(attempt, workspace, path, repositoryPath, gate) {
|
|
462
462
|
const job = await this.jobPort.getJob(attempt.taskId, attempt.jobId);
|
|
463
|
+
if (job.delegation)
|
|
464
|
+
throw new Error("Task delegation is not an Integration check Job.");
|
|
463
465
|
assertCheckJobIdentity(attempt, job, path);
|
|
464
466
|
await assertIntegrationCandidate(path, job.head, workspace.branch);
|
|
465
467
|
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit, this.#targetCheckout(attempt));
|