@zq-silk/yui 0.4.2 → 0.5.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/cli/commandCatalog.js +31 -1
- package/dist/cli.js +8 -0
- package/dist/commands/projectCommands.js +206 -15
- package/dist/commands/taskWorkspaceCommands.js +135 -0
- package/dist/integration/gitIntegrationService.js +3 -2
- package/dist/repository/gitWorkspace.js +58 -5
- package/dist/repository/homeIdentity.js +28 -0
- package/dist/repository/project.js +13 -4
- package/dist/repository/taskWorkspaceIdentity.js +158 -0
- package/dist/repository/taskWorkspacePreparer.js +352 -29
- package/dist/storage/migration/productionRegistry.js +180 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskStore.js +13 -2
- package/dist/task/task.js +36 -3
- package/package.json +1 -1
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/** The one production transition registry and its baseline-to-current delivery gate. */
|
|
2
|
+
import { generateHomeIdentity } from "../../repository/homeIdentity.js";
|
|
2
3
|
import { latestStorageVersionState } from "../upgrade/recordVersions.js";
|
|
3
4
|
import { assertBaselineConsistency, baselineStorageVersionState } from "./baseline.js";
|
|
4
5
|
import { planMigration } from "./planner.js";
|
|
5
6
|
import { MigrationRegistry } from "./registry.js";
|
|
6
7
|
const FINAL_REVIEW_AGGREGATE_FROM_VERSION = 16;
|
|
7
8
|
const FINAL_REVIEW_AGGREGATE_TO_VERSION = 17;
|
|
9
|
+
const HOME_IDENTITY_AGGREGATE_FROM_VERSION = 17;
|
|
10
|
+
const HOME_IDENTITY_AGGREGATE_TO_VERSION = 18;
|
|
11
|
+
const PROJECT_FROM_VERSION = 2;
|
|
12
|
+
const PROJECT_TO_VERSION = 3;
|
|
13
|
+
const TASK_FROM_VERSION = 3;
|
|
14
|
+
const TASK_TO_VERSION = 4;
|
|
8
15
|
const WORK_ITEM_FROM_VERSION = 6;
|
|
9
16
|
const WORK_ITEM_TO_VERSION = 7;
|
|
10
17
|
const WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION = 7;
|
|
@@ -38,6 +45,16 @@ export function createProductionStorageRegistry() {
|
|
|
38
45
|
transform: migrateAggregateV16ToV17,
|
|
39
46
|
declaredEffects: []
|
|
40
47
|
})
|
|
48
|
+
.registerOfflineMigration({
|
|
49
|
+
axis: "aggregate",
|
|
50
|
+
fromVersion: HOME_IDENTITY_AGGREGATE_FROM_VERSION,
|
|
51
|
+
toVersion: HOME_IDENTITY_AGGREGATE_TO_VERSION,
|
|
52
|
+
preconditions: requireAggregateV17Snapshot,
|
|
53
|
+
transform: migrateAggregateV17ToV18,
|
|
54
|
+
declaredEffects: []
|
|
55
|
+
})
|
|
56
|
+
.registerOfflineMigration(projectOwnershipStep())
|
|
57
|
+
.registerOfflineMigration(taskWorkspaceIdentityStep())
|
|
41
58
|
.registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_FROM_VERSION, WORK_ITEM_TO_VERSION, "workItems"))
|
|
42
59
|
.registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION, WORK_ITEM_GIT_SNAPSHOT_TO_VERSION, "workItems"))
|
|
43
60
|
.registerOfflineMigration(workItemExecutionGroupHistoryStep())
|
|
@@ -470,6 +487,169 @@ function requireAggregateV16Snapshot(snapshot) {
|
|
|
470
487
|
throw new Error("Aggregate 16->17 migration requires state.json schemaVersion 16 to match schema.json.");
|
|
471
488
|
}
|
|
472
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* Introduce the durable Home identity. A v18 aggregate always carries one; a
|
|
492
|
+
* Home with no state.json yet gets its identity when its first state is
|
|
493
|
+
* written, so this step only mints one when state.json already exists.
|
|
494
|
+
*/
|
|
495
|
+
function migrateAggregateV17ToV18(snapshot) {
|
|
496
|
+
const schemaManifest = {
|
|
497
|
+
...snapshot.schemaManifest,
|
|
498
|
+
aggregateSchemaVersion: HOME_IDENTITY_AGGREGATE_TO_VERSION
|
|
499
|
+
};
|
|
500
|
+
if (snapshot.state === null)
|
|
501
|
+
return { schemaManifest, state: null };
|
|
502
|
+
const state = snapshot.state;
|
|
503
|
+
if (state.homeIdentity !== undefined) {
|
|
504
|
+
throw new Error("Aggregate 17->18 migration found an unexpected homeIdentity.");
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
schemaManifest,
|
|
508
|
+
state: {
|
|
509
|
+
...state,
|
|
510
|
+
schemaVersion: HOME_IDENTITY_AGGREGATE_TO_VERSION,
|
|
511
|
+
homeIdentity: generateHomeIdentity(new Date())
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function requireAggregateV17Snapshot(snapshot) {
|
|
516
|
+
if (snapshot.schemaManifest.aggregateSchemaVersion
|
|
517
|
+
!== HOME_IDENTITY_AGGREGATE_FROM_VERSION) {
|
|
518
|
+
throw new Error("Aggregate 17->18 migration requires schema.json aggregateSchemaVersion 17.");
|
|
519
|
+
}
|
|
520
|
+
if (snapshot.state !== null
|
|
521
|
+
&& snapshot.state.schemaVersion !== HOME_IDENTITY_AGGREGATE_FROM_VERSION) {
|
|
522
|
+
throw new Error("Aggregate 17->18 migration requires state.json schemaVersion 17 to match schema.json.");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Project ownership is a new required field. Every pre-v3 Project is a
|
|
527
|
+
* user-registered checkout, so the historical binding is `external`; a managed
|
|
528
|
+
* Home-owned repository is only ever created explicitly by the new binding
|
|
529
|
+
* path. The version transition is durable and centralized.
|
|
530
|
+
*/
|
|
531
|
+
function projectOwnershipStep() {
|
|
532
|
+
return {
|
|
533
|
+
axis: "record",
|
|
534
|
+
recordKind: "project",
|
|
535
|
+
fromVersion: PROJECT_FROM_VERSION,
|
|
536
|
+
toVersion: PROJECT_TO_VERSION,
|
|
537
|
+
preconditions: requireProjectV2Family,
|
|
538
|
+
transform: migrateProjectV2ToV3,
|
|
539
|
+
declaredEffects: []
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function requireProjectV2Family(snapshot) {
|
|
543
|
+
const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
544
|
+
if (manifestVersions.project !== PROJECT_FROM_VERSION) {
|
|
545
|
+
throw new Error(`Record project migration requires manifest version ${PROJECT_FROM_VERSION}.`);
|
|
546
|
+
}
|
|
547
|
+
if (snapshot.state === null)
|
|
548
|
+
return;
|
|
549
|
+
const projects = snapshot.state.projects;
|
|
550
|
+
if (projects === undefined)
|
|
551
|
+
return;
|
|
552
|
+
const map = asObject(projects, "project map");
|
|
553
|
+
for (const [projectId, rawProject] of Object.entries(map)) {
|
|
554
|
+
const project = asObject(rawProject, `Project ${projectId}`);
|
|
555
|
+
if (project.schemaVersion !== PROJECT_FROM_VERSION) {
|
|
556
|
+
throw new Error(`Project ${projectId} must use schemaVersion ${PROJECT_FROM_VERSION}.`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function migrateProjectV2ToV3(snapshot) {
|
|
561
|
+
requireProjectV2Family(snapshot);
|
|
562
|
+
const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
563
|
+
const schemaManifest = {
|
|
564
|
+
...snapshot.schemaManifest,
|
|
565
|
+
recordVersions: { ...manifestVersions, project: PROJECT_TO_VERSION }
|
|
566
|
+
};
|
|
567
|
+
if (snapshot.state === null)
|
|
568
|
+
return { schemaManifest, state: null };
|
|
569
|
+
const projects = snapshot.state.projects;
|
|
570
|
+
if (projects === undefined) {
|
|
571
|
+
return { schemaManifest, state: { ...snapshot.state } };
|
|
572
|
+
}
|
|
573
|
+
const map = asObject(projects, "project map");
|
|
574
|
+
const nextProjects = {};
|
|
575
|
+
for (const [projectId, rawProject] of Object.entries(map)) {
|
|
576
|
+
const project = asObject(rawProject, `Project ${projectId}`);
|
|
577
|
+
nextProjects[projectId] = {
|
|
578
|
+
...project,
|
|
579
|
+
schemaVersion: PROJECT_TO_VERSION,
|
|
580
|
+
ownership: "external"
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
schemaManifest,
|
|
585
|
+
state: { ...snapshot.state, projects: nextProjects }
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Task v4 adds the optional durable workspace identity. Historical Tasks have
|
|
590
|
+
* no identity; they keep working against their existing (legacy) refs until the
|
|
591
|
+
* controlled rebuild mints one. This adjacent step performs no field rewrite,
|
|
592
|
+
* preserving old records while keeping a pre-v4 Task out of the strict current
|
|
593
|
+
* parser.
|
|
594
|
+
*/
|
|
595
|
+
function taskWorkspaceIdentityStep() {
|
|
596
|
+
return {
|
|
597
|
+
axis: "record",
|
|
598
|
+
recordKind: "task",
|
|
599
|
+
fromVersion: TASK_FROM_VERSION,
|
|
600
|
+
toVersion: TASK_TO_VERSION,
|
|
601
|
+
preconditions: requireTaskV3Family,
|
|
602
|
+
transform: migrateTaskV3ToV4,
|
|
603
|
+
declaredEffects: []
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function requireTaskV3Family(snapshot) {
|
|
607
|
+
const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
608
|
+
if (manifestVersions.task !== TASK_FROM_VERSION) {
|
|
609
|
+
throw new Error(`Record task migration requires manifest version ${TASK_FROM_VERSION}.`);
|
|
610
|
+
}
|
|
611
|
+
if (snapshot.state === null)
|
|
612
|
+
return;
|
|
613
|
+
const tasks = asObject(snapshot.state.tasks, "state tasks");
|
|
614
|
+
for (const [taskId, rawTask] of Object.entries(tasks)) {
|
|
615
|
+
const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
|
|
616
|
+
const task = aggregate.task;
|
|
617
|
+
if (task === undefined)
|
|
618
|
+
continue;
|
|
619
|
+
const record = asObject(task, `Task ${taskId}`);
|
|
620
|
+
if (record.schemaVersion !== TASK_FROM_VERSION) {
|
|
621
|
+
throw new Error(`Task ${taskId} must use schemaVersion ${TASK_FROM_VERSION}.`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function migrateTaskV3ToV4(snapshot) {
|
|
626
|
+
requireTaskV3Family(snapshot);
|
|
627
|
+
const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
628
|
+
const schemaManifest = {
|
|
629
|
+
...snapshot.schemaManifest,
|
|
630
|
+
recordVersions: { ...manifestVersions, task: TASK_TO_VERSION }
|
|
631
|
+
};
|
|
632
|
+
if (snapshot.state === null)
|
|
633
|
+
return { schemaManifest, state: null };
|
|
634
|
+
const tasks = asObject(snapshot.state.tasks, "state tasks");
|
|
635
|
+
const nextTasks = {};
|
|
636
|
+
for (const [taskId, rawTask] of Object.entries(tasks)) {
|
|
637
|
+
const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
|
|
638
|
+
if (aggregate.task === undefined) {
|
|
639
|
+
nextTasks[taskId] = { ...aggregate };
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const task = asObject(aggregate.task, `Task ${taskId}`);
|
|
643
|
+
nextTasks[taskId] = {
|
|
644
|
+
...aggregate,
|
|
645
|
+
task: { ...task, schemaVersion: TASK_TO_VERSION }
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
schemaManifest,
|
|
650
|
+
state: { ...snapshot.state, tasks: nextTasks }
|
|
651
|
+
};
|
|
652
|
+
}
|
|
473
653
|
/**
|
|
474
654
|
* A version bump is deliverable only when the shared planner resolves the full
|
|
475
655
|
* adjacent path. This also covers target-only record families as explicit 0->1
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
/** Version of the on-disk layout (`schema.json`, root `state.json`, and locks). */
|
|
9
9
|
export const CURRENT_STORAGE_LAYOUT_VERSION = 6;
|
|
10
10
|
/** Version of the authoritative aggregate stored in `state.json`. */
|
|
11
|
-
export const CURRENT_AGGREGATE_SCHEMA_VERSION =
|
|
11
|
+
export const CURRENT_AGGREGATE_SCHEMA_VERSION = 18;
|
|
@@ -14,6 +14,7 @@ import { validateReviewConfig } from "../review/reviewConfig.js";
|
|
|
14
14
|
import { validateReviewRound } from "../review/reviewRound.js";
|
|
15
15
|
import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
16
16
|
import { assertProjectCatalog, validateProject } from "../repository/project.js";
|
|
17
|
+
import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeIdentity.js";
|
|
17
18
|
import { validateAgentProfile } from "../profile/agentProfile.js";
|
|
18
19
|
import { validateChangeSet } from "../integration/changeSet.js";
|
|
19
20
|
import { validateIntegrationAttempt } from "../integration/integrationAttempt.js";
|
|
@@ -32,6 +33,7 @@ export const STORAGE_STATE_FILE = "state.json";
|
|
|
32
33
|
/** The root StorageState schema is the persisted aggregate document version. */
|
|
33
34
|
export const CURRENT_STORAGE_STATE_SCHEMA_VERSION = CURRENT_AGGREGATE_SCHEMA_VERSION;
|
|
34
35
|
export const CURRENT_CONFIG_SCHEMA_VERSION = 1;
|
|
36
|
+
export const CURRENT_HOME_IDENTITY_SCHEMA_VERSION = 1;
|
|
35
37
|
export const CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION = 3;
|
|
36
38
|
/**
|
|
37
39
|
* Persisted StorageState/StoredTask family versions owned by this boundary.
|
|
@@ -42,11 +44,11 @@ export const CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION = 3;
|
|
|
42
44
|
* below, but are not independent record-axis families.
|
|
43
45
|
*/
|
|
44
46
|
export const CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION = 2;
|
|
45
|
-
export const CURRENT_PROJECT_SCHEMA_VERSION =
|
|
47
|
+
export const CURRENT_PROJECT_SCHEMA_VERSION = 3;
|
|
46
48
|
export const CURRENT_AGENT_PROFILE_SCHEMA_VERSION = 2;
|
|
47
49
|
export const CURRENT_GLOBAL_ROLE_SCHEMA_VERSION = 3;
|
|
48
50
|
export const CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION = 3;
|
|
49
|
-
export const CURRENT_TASK_SCHEMA_VERSION =
|
|
51
|
+
export const CURRENT_TASK_SCHEMA_VERSION = 4;
|
|
50
52
|
export const CURRENT_TASK_BRIEF_SCHEMA_VERSION = 2;
|
|
51
53
|
export const CURRENT_TASK_ROLE_SCHEMA_VERSION = 3;
|
|
52
54
|
export const CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION = 2;
|
|
@@ -134,6 +136,7 @@ export class FileTaskStore {
|
|
|
134
136
|
});
|
|
135
137
|
}
|
|
136
138
|
getConfig() { return clone(this.#state().config); }
|
|
139
|
+
getHomeIdentity() { return clone(this.#state().homeIdentity); }
|
|
137
140
|
saveConfig(config) {
|
|
138
141
|
const stored = versioned(config, CURRENT_CONFIG_SCHEMA_VERSION, "Yui config");
|
|
139
142
|
validateYuiConfig(stored);
|
|
@@ -1172,6 +1175,7 @@ function emptyState() {
|
|
|
1172
1175
|
return {
|
|
1173
1176
|
schemaVersion: CURRENT_STORAGE_STATE_SCHEMA_VERSION,
|
|
1174
1177
|
revision: 0,
|
|
1178
|
+
homeIdentity: generateHomeIdentity(new Date()),
|
|
1175
1179
|
config: { schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION },
|
|
1176
1180
|
configuredAgents: {},
|
|
1177
1181
|
projects: {},
|
|
@@ -1249,6 +1253,7 @@ function parseState(raw) {
|
|
|
1249
1253
|
exact(state, [
|
|
1250
1254
|
"schemaVersion",
|
|
1251
1255
|
"revision",
|
|
1256
|
+
"homeIdentity",
|
|
1252
1257
|
"config",
|
|
1253
1258
|
"configuredAgents",
|
|
1254
1259
|
"projects",
|
|
@@ -1261,6 +1266,12 @@ function parseState(raw) {
|
|
|
1261
1266
|
if (state.schemaVersion !== CURRENT_STORAGE_STATE_SCHEMA_VERSION || !Number.isInteger(state.revision) || state.revision < 0)
|
|
1262
1267
|
throw new StorageRecordError("Storage state schemaVersion/revision is invalid.");
|
|
1263
1268
|
const result = clone(state);
|
|
1269
|
+
try {
|
|
1270
|
+
result.homeIdentity = validateHomeIdentity(versioned(result.homeIdentity, CURRENT_HOME_IDENTITY_SCHEMA_VERSION, "Home identity"));
|
|
1271
|
+
}
|
|
1272
|
+
catch (error) {
|
|
1273
|
+
throw new StorageRecordError(error instanceof Error ? error.message : String(error));
|
|
1274
|
+
}
|
|
1264
1275
|
result.config = versioned(result.config, CURRENT_CONFIG_SCHEMA_VERSION, "Yui config");
|
|
1265
1276
|
validateYuiConfig(result.config);
|
|
1266
1277
|
parseMap(result.configuredAgents, (value, key) => {
|
package/dist/task/task.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { validateTaskWorkspaceIdentity } from "../repository/taskWorkspaceIdentity.js";
|
|
1
2
|
export function createTask(id, title, now, metadata = {}) {
|
|
2
3
|
const timestamp = now.toISOString();
|
|
3
4
|
return {
|
|
4
|
-
schemaVersion:
|
|
5
|
+
schemaVersion: 4,
|
|
5
6
|
id: requireSafeIdentity(id, "Task id"),
|
|
6
7
|
title: requireText(title, "Task title"),
|
|
7
8
|
...cloneMetadata(metadata),
|
|
@@ -10,6 +11,32 @@ export function createTask(id, title, now, metadata = {}) {
|
|
|
10
11
|
updatedAt: timestamp
|
|
11
12
|
};
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Persist the Task's durable workspace identity. The identity is immutable on
|
|
16
|
+
* the Task: a second binding is rejected so restart/reconcile/attach can never
|
|
17
|
+
* silently adopt a different (foreign or legacy) ref namespace.
|
|
18
|
+
*/
|
|
19
|
+
export function bindTaskWorkspaceIdentity(task, identity, now) {
|
|
20
|
+
validateTask(task);
|
|
21
|
+
const valid = validateTaskWorkspaceIdentity(identity);
|
|
22
|
+
if (valid.taskId !== task.id) {
|
|
23
|
+
throw new Error(`Task workspace identity belongs to another Task: ${valid.taskId}.`);
|
|
24
|
+
}
|
|
25
|
+
if (task.workspaceIdentity !== undefined) {
|
|
26
|
+
const existing = validateTaskWorkspaceIdentity(task.workspaceIdentity);
|
|
27
|
+
if (existing.token !== valid.token
|
|
28
|
+
|| existing.generatedAt !== valid.generatedAt
|
|
29
|
+
|| existing.homeId !== valid.homeId) {
|
|
30
|
+
throw new Error(`Task workspace identity is already bound and immutable: ${task.id}.`);
|
|
31
|
+
}
|
|
32
|
+
return task;
|
|
33
|
+
}
|
|
34
|
+
return validateTask({
|
|
35
|
+
...task,
|
|
36
|
+
workspaceIdentity: valid,
|
|
37
|
+
updatedAt: now.toISOString()
|
|
38
|
+
});
|
|
39
|
+
}
|
|
13
40
|
export function activateTask(task, now) {
|
|
14
41
|
if (task.status === "archived")
|
|
15
42
|
throw new Error(`Cannot activate archived Task: ${task.id}.`);
|
|
@@ -144,8 +171,8 @@ export function isTaskArchived(task) {
|
|
|
144
171
|
return task.status === "archived";
|
|
145
172
|
}
|
|
146
173
|
export function validateTask(task) {
|
|
147
|
-
if (task.schemaVersion !==
|
|
148
|
-
throw new Error("Task must use schemaVersion
|
|
174
|
+
if (task.schemaVersion !== 4)
|
|
175
|
+
throw new Error("Task must use schemaVersion 4.");
|
|
149
176
|
requireSafeIdentity(task.id, "Task id");
|
|
150
177
|
requireText(task.title, "Task title");
|
|
151
178
|
if (!["draft", "active", "completed", "retired", "archived"].includes(task.status)) {
|
|
@@ -156,6 +183,12 @@ export function validateTask(task) {
|
|
|
156
183
|
if (Date.parse(task.updatedAt) < Date.parse(task.createdAt)) {
|
|
157
184
|
throw new Error("Task updatedAt cannot precede createdAt.");
|
|
158
185
|
}
|
|
186
|
+
if (task.workspaceIdentity !== undefined) {
|
|
187
|
+
const identity = validateTaskWorkspaceIdentity(task.workspaceIdentity);
|
|
188
|
+
if (identity.taskId !== task.id) {
|
|
189
|
+
throw new Error(`Task workspace identity belongs to another Task: ${identity.taskId}.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
159
192
|
if (task.priority !== undefined
|
|
160
193
|
&& !["low", "medium", "high", "urgent"].includes(task.priority)) {
|
|
161
194
|
throw new Error(`Task priority is invalid: ${String(task.priority)}.`);
|