@zq-silk/yui 0.8.8 → 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/ARCHITECTURE.md +48 -46
- package/README.md +33 -31
- package/dist/cli/commandCatalog.js +7 -7
- package/dist/cli.js +1 -31
- package/dist/commands/executionAuditCommands.js +2 -2
- package/dist/commands/taskCommands.js +110 -307
- package/dist/commands/taskCompletionGate.js +15 -12
- package/dist/commands/taskContextCommand.js +18 -11
- package/dist/commands/taskNextActionCommand.js +4 -5
- package/dist/commands/taskWorkspaceCommands.js +2 -2
- package/dist/execution/executionGroup.js +0 -3
- package/dist/executor/agentExecutor.js +4 -2
- package/dist/executor/effectiveLaunch.js +33 -3
- package/dist/integration/gitIntegrationService.js +1 -1
- package/dist/lifecycle/exactRunTerminalization.js +12 -8
- package/dist/observability/orchestrationMetrics.js +5 -19
- package/dist/observability/runtimeIdentity.js +14 -3
- package/dist/profile/agentProfile.js +1 -1
- package/dist/repository/taskWorkspaceCoordinator.js +2 -0
- package/dist/repository/taskWorkspacePreparer.js +173 -26
- package/dist/review/reviewRound.js +41 -24
- package/dist/storage/migration/productionRegistry.js +138 -0
- package/dist/storage/sqliteStore.js +1 -1
- package/dist/storage/taskStore.js +26 -27
- package/dist/task/completionReadiness.js +24 -22
- package/dist/task/nextAction.js +47 -61
- package/dist/task/task.js +12 -19
- package/dist/web/assets/client/components.js +3 -1
- package/dist/web/assets/client/i18n.js +6 -0
- package/dist/web/webSnapshot.js +0 -3
- package/i18n/README.zh-CN.md +18 -19
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +47 -43
- package/skills/yui-operator/SKILL.md +24 -33
- package/skills/yui-reviewer/SKILL.md +18 -12
- package/skills/yui-worker/SKILL.md +5 -3
|
@@ -1168,19 +1168,22 @@ export class FileTaskWorkspacePreparer {
|
|
|
1168
1168
|
if (round.status !== "pending") {
|
|
1169
1169
|
throw new Error(`ReviewRound workspace can only prepare while pending: ${round.id}.`);
|
|
1170
1170
|
}
|
|
1171
|
-
const
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1171
|
+
const taskScope = (round.scope ?? "work-item") === "task";
|
|
1172
|
+
const item = taskScope || round.workItemId === undefined
|
|
1173
|
+
? undefined
|
|
1174
|
+
: requireWorkItem(this.store, task.id, round.workItemId);
|
|
1175
|
+
const candidate = taskScope
|
|
1176
|
+
? undefined
|
|
1177
|
+
: item?.candidates.find(({ id }) => id === round.candidateId);
|
|
1178
|
+
if (!taskScope && candidate === undefined) {
|
|
1174
1179
|
throw new Error(`ReviewRound Candidate not found: ${round.candidateId}.`);
|
|
1175
1180
|
}
|
|
1176
|
-
const taskScope = (round.scope ?? "work-item") === "task";
|
|
1177
1181
|
// A WorkItem ReviewRound is an immutable snapshot of Develop. A Task
|
|
1178
|
-
// ReviewRound
|
|
1179
|
-
//
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
throw new Error(`Candidate has no frozen managed Git snapshot: ${candidate.id}.`);
|
|
1182
|
+
// ReviewRound instead freezes every bound Project at the unified Task
|
|
1183
|
+
// heads and deliberately has no WorkItem/Candidate anchor.
|
|
1184
|
+
const develop = candidate?.workspace;
|
|
1185
|
+
if (!taskScope && (develop === undefined || candidate?.gitSnapshot === undefined)) {
|
|
1186
|
+
throw new Error(`Candidate has no frozen managed Git snapshot: ${candidate?.id ?? "unknown"}.`);
|
|
1184
1187
|
}
|
|
1185
1188
|
if (!taskScope && candidate.gitSnapshot.reviewBaseCommit !== round.reviewBaseCommit) {
|
|
1186
1189
|
throw new Error(`ReviewRound base no longer matches its Candidate: ${round.id}.`);
|
|
@@ -1199,7 +1202,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1199
1202
|
throw new Error(`Task Review candidate Project is missing: ${binding.projectId}.`);
|
|
1200
1203
|
}
|
|
1201
1204
|
const project = requireProject(this.store, binding.projectId);
|
|
1202
|
-
const identity = worktreeIdentity(taskSegment, round
|
|
1205
|
+
const identity = worktreeIdentity(taskSegment, this.#reviewWorktreeName(round));
|
|
1203
1206
|
return {
|
|
1204
1207
|
projectId: binding.projectId,
|
|
1205
1208
|
directory: binding.directory,
|
|
@@ -1222,7 +1225,12 @@ export class FileTaskWorkspacePreparer {
|
|
|
1222
1225
|
}
|
|
1223
1226
|
const expectedEntries = new Map(frozenEntries.map((entry) => [entry.projectId, entry]));
|
|
1224
1227
|
const existing = this.store.getReviewRoundWorkspace(task.id, round.id);
|
|
1225
|
-
const reviewRoot = this.#reviewRoundWorkspaceRoot(task.id, round
|
|
1228
|
+
const reviewRoot = this.#reviewRoundWorkspaceRoot(task.id, round);
|
|
1229
|
+
if (taskScope && existing === null) {
|
|
1230
|
+
const reassigned = await this.#reassignTaskReviewWorkspace(task, round, reviewer, reviewRoot, frozenEntries);
|
|
1231
|
+
if (reassigned !== null)
|
|
1232
|
+
return reassigned;
|
|
1233
|
+
}
|
|
1226
1234
|
const retained = new Map();
|
|
1227
1235
|
const missing = new Set();
|
|
1228
1236
|
const adopted = existing?.root === reviewRoot;
|
|
@@ -1251,7 +1259,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1251
1259
|
throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound workspace metadata changed for ${round.id}/${entry.projectId}.`);
|
|
1252
1260
|
}
|
|
1253
1261
|
const project = requireProject(this.store, entry.projectId);
|
|
1254
|
-
const identity = worktreeIdentity(taskSegment, round
|
|
1262
|
+
const identity = worktreeIdentity(taskSegment, this.#reviewWorktreeName(round));
|
|
1255
1263
|
const expectedPath = join(this.#projectContainer(project.name), identity.directory);
|
|
1256
1264
|
if (entry.path !== expectedPath || entry.branch !== identity.branch) {
|
|
1257
1265
|
throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound workspace managed identity mismatch for ${round.id}/${entry.projectId}.`);
|
|
@@ -1339,7 +1347,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1339
1347
|
repositoryPath: project.path,
|
|
1340
1348
|
container: this.#projectContainer(project.name),
|
|
1341
1349
|
taskSegment,
|
|
1342
|
-
roleName: round
|
|
1350
|
+
roleName: this.#reviewWorktreeName(round),
|
|
1343
1351
|
baseRef: source.baseCommit
|
|
1344
1352
|
});
|
|
1345
1353
|
const entry = {
|
|
@@ -1404,10 +1412,14 @@ export class FileTaskWorkspacePreparer {
|
|
|
1404
1412
|
this.#registerWorkspace(stored);
|
|
1405
1413
|
return this.store.transaction((tx) => {
|
|
1406
1414
|
const currentRound = tx.getReviewRound(task.id, round.id);
|
|
1407
|
-
const currentItem = tx.getWorkItem(task.id, item.id);
|
|
1415
|
+
const currentItem = item === undefined ? null : tx.getWorkItem(task.id, item.id);
|
|
1416
|
+
const candidateChanged = taskScope
|
|
1417
|
+
? currentRound === null
|
|
1418
|
+
|| !isDeepStrictEqual(currentRound.taskCandidate, round.taskCandidate)
|
|
1419
|
+
: currentItem === null
|
|
1420
|
+
|| !isDeepStrictEqual(currentItem.candidates.find(({ id }) => id === candidate.id), candidate);
|
|
1408
1421
|
if (currentRound === null || currentRound.status !== "pending"
|
|
1409
|
-
||
|
|
1410
|
-
|| !isDeepStrictEqual(currentItem.candidates.find(({ id }) => id === candidate.id), candidate)) {
|
|
1422
|
+
|| candidateChanged) {
|
|
1411
1423
|
throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound changed while preparing its workspace: ${round.id}.`);
|
|
1412
1424
|
}
|
|
1413
1425
|
if (tx.getActiveAgentRun(task.id, reviewer.name) !== null) {
|
|
@@ -1441,7 +1453,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1441
1453
|
});
|
|
1442
1454
|
}
|
|
1443
1455
|
catch (error) {
|
|
1444
|
-
await this.#discardUnadoptedEntries(task, taskSegment, prepared, round
|
|
1456
|
+
await this.#discardUnadoptedEntries(task, taskSegment, prepared, this.#reviewWorktreeName(round), existing === null, new Set([...retained.values()].map(({ entry }) => entry.path)));
|
|
1445
1457
|
throw error;
|
|
1446
1458
|
}
|
|
1447
1459
|
}
|
|
@@ -1449,6 +1461,135 @@ export class FileTaskWorkspacePreparer {
|
|
|
1449
1461
|
release();
|
|
1450
1462
|
}
|
|
1451
1463
|
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Keep one physical workspace per Task Reviewer Role. A new semantic Round
|
|
1466
|
+
* receives a new immutable ManagedWorkspace owner and frozen base record,
|
|
1467
|
+
* while a clean, unmodified terminal workspace is reset in place so the
|
|
1468
|
+
* provider-native Reviewer Session can continue at the same cwd.
|
|
1469
|
+
*/
|
|
1470
|
+
async #reassignTaskReviewWorkspace(task, round, reviewer, reviewRoot, frozenEntries) {
|
|
1471
|
+
const previous = this.store.listReviewRounds(task.id)
|
|
1472
|
+
.filter((candidate) => (candidate.id !== round.id
|
|
1473
|
+
&& (candidate.scope ?? "work-item") === "task"
|
|
1474
|
+
&& candidate.reviewerRoleName === round.reviewerRoleName
|
|
1475
|
+
&& (candidate.status === "completed" || candidate.status === "failed")
|
|
1476
|
+
&& candidate.workspaceDisposition?.kind !== "removed"
|
|
1477
|
+
&& candidate.workspaceDisposition?.kind !== "reassigned"))
|
|
1478
|
+
.sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
|
|
1479
|
+
|| left.id.localeCompare(right.id, undefined, { numeric: true })))
|
|
1480
|
+
.at(-1);
|
|
1481
|
+
if (previous === undefined)
|
|
1482
|
+
return null;
|
|
1483
|
+
const previousWorkspace = this.store.getReviewRoundWorkspace(task.id, previous.id);
|
|
1484
|
+
if (previousWorkspace === null)
|
|
1485
|
+
return null;
|
|
1486
|
+
if (previous.workspace === undefined
|
|
1487
|
+
|| !isDeepStrictEqual(previous.workspace, previousWorkspace)
|
|
1488
|
+
|| previousWorkspace.root !== reviewRoot) {
|
|
1489
|
+
throw new ReviewRoundWorkspaceEvidenceError(`Previous Task-final Review workspace cannot be continued: ${previous.id}.`);
|
|
1490
|
+
}
|
|
1491
|
+
if (this.store.getActiveAgentRun(task.id, reviewer.name) !== null) {
|
|
1492
|
+
throw new ReviewRoundWorkspaceEvidenceError(`Reviewer Role has an active Run: ${task.id}/${reviewer.name}.`);
|
|
1493
|
+
}
|
|
1494
|
+
const expected = new Map(frozenEntries.map((entry) => [entry.projectId, entry]));
|
|
1495
|
+
if (previousWorkspace.entries.length !== expected.size) {
|
|
1496
|
+
throw new ReviewRoundWorkspaceEvidenceError(`Task-final Review Project scope changed for Reviewer ${reviewer.name}.`);
|
|
1497
|
+
}
|
|
1498
|
+
const nextEntries = [];
|
|
1499
|
+
const reset = [];
|
|
1500
|
+
for (const prior of previousWorkspace.entries) {
|
|
1501
|
+
const next = expected.get(prior.projectId);
|
|
1502
|
+
if (next === undefined
|
|
1503
|
+
|| prior.path !== next.path
|
|
1504
|
+
|| prior.branch !== next.branch
|
|
1505
|
+
|| prior.directory !== next.directory
|
|
1506
|
+
|| prior.access !== "write") {
|
|
1507
|
+
throw new ReviewRoundWorkspaceEvidenceError(`Task-final Review workspace identity changed for ${reviewer.name}/${prior.projectId}.`);
|
|
1508
|
+
}
|
|
1509
|
+
const physical = await this.git.inspect(prior.path, "HEAD");
|
|
1510
|
+
if (!await this.git.isClean(prior.path)
|
|
1511
|
+
|| !sameCommit(physical.baseCommit, prior.baseCommit)) {
|
|
1512
|
+
throw new ReviewRoundWorkspaceEvidenceError(`Previous Task-final Review workspace contains retained diagnostics: `
|
|
1513
|
+
+ `${previous.id}/${prior.projectId}; preserve or clean it before continuing the Reviewer Session.`);
|
|
1514
|
+
}
|
|
1515
|
+
nextEntries.push({
|
|
1516
|
+
...next,
|
|
1517
|
+
path: prior.path,
|
|
1518
|
+
branch: prior.branch,
|
|
1519
|
+
baseRef: next.baseCommit,
|
|
1520
|
+
baseCommit: next.baseCommit
|
|
1521
|
+
});
|
|
1522
|
+
if (!sameCommit(physical.baseCommit, next.baseCommit)) {
|
|
1523
|
+
reset.push({
|
|
1524
|
+
path: prior.path,
|
|
1525
|
+
previousHead: physical.baseCommit,
|
|
1526
|
+
nextHead: next.baseCommit
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
const stored = createManagedWorkspace({
|
|
1531
|
+
owner: { type: "review-round", taskId: task.id, reviewRoundId: round.id },
|
|
1532
|
+
root: reviewRoot,
|
|
1533
|
+
entries: nextEntries
|
|
1534
|
+
}, this.now());
|
|
1535
|
+
try {
|
|
1536
|
+
for (const entry of reset) {
|
|
1537
|
+
await this.git.resetWorktree({
|
|
1538
|
+
targetPath: entry.path,
|
|
1539
|
+
expectedHead: entry.previousHead,
|
|
1540
|
+
restoreHead: entry.nextHead
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
await ensureWorkspaceView(reviewRoot, nextEntries);
|
|
1544
|
+
const reassigned = this.store.transaction((tx) => {
|
|
1545
|
+
const currentPrevious = tx.getReviewRound(task.id, previous.id);
|
|
1546
|
+
const currentRound = tx.getReviewRound(task.id, round.id);
|
|
1547
|
+
const currentPreviousWorkspace = tx.getReviewRoundWorkspace(task.id, previous.id);
|
|
1548
|
+
if (currentPrevious === null
|
|
1549
|
+
|| currentPrevious.status !== previous.status
|
|
1550
|
+
|| currentPrevious.workspaceDisposition?.kind === "removed"
|
|
1551
|
+
|| currentPrevious.workspaceDisposition?.kind === "reassigned"
|
|
1552
|
+
|| currentPreviousWorkspace === null
|
|
1553
|
+
|| !sameManagedWorkspace(currentPreviousWorkspace, previousWorkspace)
|
|
1554
|
+
|| currentRound === null
|
|
1555
|
+
|| currentRound.status !== "pending"
|
|
1556
|
+
|| currentRound.workspace !== undefined
|
|
1557
|
+
|| !isDeepStrictEqual(currentRound.taskCandidate, round.taskCandidate)
|
|
1558
|
+
|| tx.getReviewRoundWorkspace(task.id, round.id) !== null
|
|
1559
|
+
|| tx.getActiveAgentRun(task.id, reviewer.name) !== null) {
|
|
1560
|
+
throw new ReviewRoundWorkspaceEvidenceError(`Task-final Review workspace changed before reassignment: ${previous.id}/${round.id}.`);
|
|
1561
|
+
}
|
|
1562
|
+
const latestReviewer = tx.getRole(task.id, reviewer.name);
|
|
1563
|
+
if (latestReviewer === null || latestReviewer.workspace !== reviewRoot) {
|
|
1564
|
+
throw new ReviewRoundWorkspaceEvidenceError(`Reviewer Role workspace changed before reassignment: ${reviewer.name}.`);
|
|
1565
|
+
}
|
|
1566
|
+
tx.removeManagedWorkspace(previousWorkspace.owner);
|
|
1567
|
+
tx.saveReviewRound(task.id, recordReviewWorkspaceDisposition(currentPrevious, "reassigned", this.now()));
|
|
1568
|
+
tx.saveManagedWorkspace(stored);
|
|
1569
|
+
tx.saveReviewRound(task.id, attachReviewRoundWorkspace(currentRound, stored));
|
|
1570
|
+
return stored;
|
|
1571
|
+
});
|
|
1572
|
+
this.#registerWorkspace(reassigned);
|
|
1573
|
+
return reassigned;
|
|
1574
|
+
}
|
|
1575
|
+
catch (error) {
|
|
1576
|
+
for (const entry of [...reset].reverse()) {
|
|
1577
|
+
try {
|
|
1578
|
+
await this.git.resetWorktree({
|
|
1579
|
+
targetPath: entry.path,
|
|
1580
|
+
expectedHead: entry.nextHead,
|
|
1581
|
+
restoreHead: entry.previousHead
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
catch {
|
|
1585
|
+
// Keep the original failure; the durable owner still names the
|
|
1586
|
+
// previous Round and exposes any compensation problem for cleanup.
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
await ensureWorkspaceView(reviewRoot, previousWorkspace.entries);
|
|
1590
|
+
throw error;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1452
1593
|
async inspectReviewRoundWorkspace(taskId, reviewRoundId) {
|
|
1453
1594
|
const task = requireTask(this.store, taskId);
|
|
1454
1595
|
const round = this.store.getReviewRound(taskId, reviewRoundId);
|
|
@@ -1461,7 +1602,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1461
1602
|
if (round.workspace !== undefined && !isDeepStrictEqual(round.workspace, workspace)) {
|
|
1462
1603
|
throw new Error(`ReviewRound workspace record diverged: ${round.id}.`);
|
|
1463
1604
|
}
|
|
1464
|
-
return this.#inspectEntries(this.#taskSegment(task), round
|
|
1605
|
+
return this.#inspectEntries(this.#taskSegment(task), this.#reviewWorktreeName(round), workspace.entries);
|
|
1465
1606
|
}
|
|
1466
1607
|
async snapshotReviewRoundResult(taskId, reviewRoundId) {
|
|
1467
1608
|
const round = this.store.getReviewRound(taskId, reviewRoundId);
|
|
@@ -1548,7 +1689,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
1548
1689
|
if (round.status !== "completed" && round.status !== "failed") {
|
|
1549
1690
|
throw new Error(`ReviewRound must be terminal before cleanup: ${round.id}.`);
|
|
1550
1691
|
}
|
|
1551
|
-
if (round.workspaceDisposition?.kind === "removed"
|
|
1692
|
+
if (round.workspaceDisposition?.kind === "removed"
|
|
1693
|
+
|| round.workspaceDisposition?.kind === "reassigned")
|
|
1552
1694
|
return "missing";
|
|
1553
1695
|
const workspace = this.store.getReviewRoundWorkspace(task.id, round.id);
|
|
1554
1696
|
if (workspace === null || round.workspace === undefined) {
|
|
@@ -1562,7 +1704,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1562
1704
|
for (const lane of round.executionGroup?.lanes ?? []) {
|
|
1563
1705
|
assertWorkspaceSessionsRetirable(this.store, task.id, lane.roleName, this.now());
|
|
1564
1706
|
}
|
|
1565
|
-
if (await this.#inspectEntries(this.#taskSegment(task),
|
|
1707
|
+
if (await this.#inspectEntries(this.#taskSegment(task), this.#reviewWorktreeName(round), workspace.entries) === "dirty")
|
|
1566
1708
|
return "dirty";
|
|
1567
1709
|
let removed = false;
|
|
1568
1710
|
for (const entry of workspace.entries) {
|
|
@@ -1571,7 +1713,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1571
1713
|
repositoryPath: project.path,
|
|
1572
1714
|
container: this.#projectContainer(project.name),
|
|
1573
1715
|
taskSegment: this.#taskSegment(task),
|
|
1574
|
-
roleName:
|
|
1716
|
+
roleName: this.#reviewWorktreeName(round),
|
|
1575
1717
|
deleteBranch: true
|
|
1576
1718
|
});
|
|
1577
1719
|
if (result === "dirty") {
|
|
@@ -2064,8 +2206,13 @@ export class FileTaskWorkspacePreparer {
|
|
|
2064
2206
|
#workItemWorkspaceRoot(taskId, workItemId) {
|
|
2065
2207
|
return join(resolveTaskRoot(this.home, this.store.getConfig().defaultWorkspace), safePathSegment(taskId), "work-items", safePathSegment(workItemId));
|
|
2066
2208
|
}
|
|
2067
|
-
#reviewRoundWorkspaceRoot(taskId,
|
|
2068
|
-
return join(resolveTaskRoot(this.home, this.store.getConfig().defaultWorkspace), safePathSegment(taskId), "reviews", safePathSegment(
|
|
2209
|
+
#reviewRoundWorkspaceRoot(taskId, round) {
|
|
2210
|
+
return join(resolveTaskRoot(this.home, this.store.getConfig().defaultWorkspace), safePathSegment(taskId), "reviews", safePathSegment(this.#reviewWorktreeName(round)));
|
|
2211
|
+
}
|
|
2212
|
+
#reviewWorktreeName(round) {
|
|
2213
|
+
return (round.scope ?? "work-item") === "task"
|
|
2214
|
+
? `reviewer-${round.reviewerRoleName}`
|
|
2215
|
+
: round.id;
|
|
2069
2216
|
}
|
|
2070
2217
|
#executionLaneWorkspaceRoot(taskId, groupId, laneId) {
|
|
2071
2218
|
return join(resolveTaskRoot(this.home, this.store.getConfig().defaultWorkspace), safePathSegment(taskId), "execution-lanes", safePathSegment(groupId), safePathSegment(laneId));
|
|
@@ -2409,7 +2556,7 @@ function executionLaneLineage(store, task, executionGroupId, executionLaneId, hi
|
|
|
2409
2556
|
const round = store.getReviewRound(task.id, hint.reviewRoundId);
|
|
2410
2557
|
if (round === null)
|
|
2411
2558
|
throw new Error(`ReviewRound not found: ${hint.reviewRoundId}.`);
|
|
2412
|
-
return { purpose: "review",
|
|
2559
|
+
return { purpose: "review", reviewRoundId: round.id };
|
|
2413
2560
|
}
|
|
2414
2561
|
// Prefer the active Run's exact WorkItem for this Lane; fall back to the
|
|
2415
2562
|
// first queued WorkItem only when no active Run owns the Lane.
|
|
@@ -2430,7 +2577,7 @@ function executionLaneLineage(store, task, executionGroupId, executionLaneId, hi
|
|
|
2430
2577
|
for (const round of store.listReviewRounds(task.id)) {
|
|
2431
2578
|
if (round.executionGroup?.id === executionGroupId
|
|
2432
2579
|
&& round.executionGroup.lanes.some(({ id }) => id === executionLaneId)) {
|
|
2433
|
-
return { purpose: "review",
|
|
2580
|
+
return { purpose: "review", reviewRoundId: round.id };
|
|
2434
2581
|
}
|
|
2435
2582
|
}
|
|
2436
2583
|
throw new Error(`Execution Lane lineage not found: ${task.id}/${executionGroupId}/${executionLaneId}.`);
|
|
@@ -5,7 +5,7 @@ import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
|
5
5
|
import { assertExecutionGroupTransition, resetReviewExecutionLane, validateExecutionGroup } from "../execution/executionGroup.js";
|
|
6
6
|
export function createReviewRound(id, taskId, workItemId, candidateId, reviewerRoleName, requestedBy, reviewBaseCommit, now, executionGroup) {
|
|
7
7
|
return validateReviewRound({
|
|
8
|
-
schemaVersion:
|
|
8
|
+
schemaVersion: 5,
|
|
9
9
|
id: requireIdentity(id, "ReviewRound id"),
|
|
10
10
|
taskId: requireIdentity(taskId, "Task id"),
|
|
11
11
|
workItemId: requireIdentity(workItemId, "Work Item id"),
|
|
@@ -18,14 +18,12 @@ export function createReviewRound(id, taskId, workItemId, candidateId, reviewerR
|
|
|
18
18
|
createdAt: now.toISOString()
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
|
-
export function createTaskReviewRound(id, taskId,
|
|
21
|
+
export function createTaskReviewRound(id, taskId, reviewerRoleName, requestedBy, taskCandidate, now, taskFinalReviewContract, executionGroup) {
|
|
22
22
|
const candidate = validateTaskReviewCandidate(taskCandidate);
|
|
23
23
|
return validateReviewRound({
|
|
24
|
-
schemaVersion:
|
|
24
|
+
schemaVersion: 5,
|
|
25
25
|
id: requireIdentity(id, "ReviewRound id"),
|
|
26
26
|
taskId: requireIdentity(taskId, "Task id"),
|
|
27
|
-
workItemId: requireIdentity(workItemId, "Work Item id"),
|
|
28
|
-
candidateId: requireIdentity(candidateId, "Candidate id"),
|
|
29
27
|
reviewerRoleName: requireIdentity(reviewerRoleName, "Reviewer Role"),
|
|
30
28
|
reviewBaseCommit: candidate.projects[0].commit,
|
|
31
29
|
scope: "task",
|
|
@@ -46,14 +44,12 @@ export function createTaskReviewRound(id, taskId, workItemId, candidateId, revie
|
|
|
46
44
|
* diff so the Reviewer can prove equivalence instead of reloading every
|
|
47
45
|
* first-round evidence.
|
|
48
46
|
*/
|
|
49
|
-
export function createTaskDeltaReviewRound(id, taskId,
|
|
47
|
+
export function createTaskDeltaReviewRound(id, taskId, reviewerRoleName, requestedBy, taskCandidate, deltaRecheck, now, taskFinalReviewContract, executionGroup) {
|
|
50
48
|
const candidate = validateTaskReviewCandidate(taskCandidate);
|
|
51
49
|
return validateReviewRound({
|
|
52
|
-
schemaVersion:
|
|
50
|
+
schemaVersion: 5,
|
|
53
51
|
id: requireIdentity(id, "ReviewRound id"),
|
|
54
52
|
taskId: requireIdentity(taskId, "Task id"),
|
|
55
|
-
workItemId: requireIdentity(workItemId, "Work Item id"),
|
|
56
|
-
candidateId: requireIdentity(candidateId, "Candidate id"),
|
|
57
53
|
reviewerRoleName: requireIdentity(reviewerRoleName, "Reviewer Role"),
|
|
58
54
|
reviewBaseCommit: candidate.projects[0].commit,
|
|
59
55
|
scope: "task",
|
|
@@ -157,8 +153,7 @@ export function retryTaskReviewRound(round) {
|
|
|
157
153
|
schemaVersion: round.schemaVersion,
|
|
158
154
|
id: round.id,
|
|
159
155
|
taskId: round.taskId,
|
|
160
|
-
|
|
161
|
-
candidateId: round.candidateId,
|
|
156
|
+
...(round.legacyAnchor === undefined ? {} : { legacyAnchor: round.legacyAnchor }),
|
|
162
157
|
reviewerRoleName: round.reviewerRoleName,
|
|
163
158
|
reviewBaseCommit: round.reviewBaseCommit,
|
|
164
159
|
scope: "task",
|
|
@@ -293,7 +288,9 @@ export function recordReviewWorkspaceDisposition(round, disposition, now) {
|
|
|
293
288
|
if (round.workspace === undefined) {
|
|
294
289
|
throw new Error(`ReviewRound has no managed workspace: ${round.id}.`);
|
|
295
290
|
}
|
|
296
|
-
if (disposition !== "preserved"
|
|
291
|
+
if (disposition !== "preserved"
|
|
292
|
+
&& disposition !== "reassigned"
|
|
293
|
+
&& disposition !== "removed") {
|
|
297
294
|
throw new Error(`Review workspace disposition is invalid: ${String(disposition)}.`);
|
|
298
295
|
}
|
|
299
296
|
if (round.workspaceDisposition?.kind === disposition)
|
|
@@ -341,13 +338,9 @@ export function updateReviewExecutionGroup(round, executionGroup) {
|
|
|
341
338
|
return validateReviewRound({ ...round, executionGroup });
|
|
342
339
|
}
|
|
343
340
|
export function validateReviewRound(round) {
|
|
344
|
-
if (round.schemaVersion !==
|
|
345
|
-
throw new Error("ReviewRound must use schemaVersion
|
|
341
|
+
if (round.schemaVersion !== 5)
|
|
342
|
+
throw new Error("ReviewRound must use schemaVersion 5.");
|
|
346
343
|
validateTaskRecordReference({ taskId: round.taskId, localId: round.id }, "reviewRound");
|
|
347
|
-
validateTaskRecordReference({ taskId: round.taskId, localId: round.workItemId }, "workItem");
|
|
348
|
-
if (!/^candidate-[1-9]\d*$/.test(round.candidateId)) {
|
|
349
|
-
throw new Error(`Candidate local id is invalid: ${round.candidateId}.`);
|
|
350
|
-
}
|
|
351
344
|
requireIdentity(round.reviewerRoleName, "Reviewer Role");
|
|
352
345
|
requireCommit(round.reviewBaseCommit, "Review base commit");
|
|
353
346
|
const scope = round.scope ?? "work-item";
|
|
@@ -355,6 +348,18 @@ export function validateReviewRound(round) {
|
|
|
355
348
|
throw new Error(`ReviewRound scope is invalid: ${String(round.scope)}.`);
|
|
356
349
|
}
|
|
357
350
|
if (scope === "task") {
|
|
351
|
+
if (round.workItemId !== undefined || round.candidateId !== undefined) {
|
|
352
|
+
throw new Error(`Task ReviewRound cannot use a WorkItem Candidate anchor: ${round.id}.`);
|
|
353
|
+
}
|
|
354
|
+
if (round.legacyAnchor !== undefined) {
|
|
355
|
+
validateTaskRecordReference({
|
|
356
|
+
taskId: round.taskId,
|
|
357
|
+
localId: round.legacyAnchor.workItemId
|
|
358
|
+
}, "workItem");
|
|
359
|
+
if (!/^candidate-[1-9]\d*$/.test(round.legacyAnchor.candidateId)) {
|
|
360
|
+
throw new Error(`Legacy Candidate local id is invalid: ${round.legacyAnchor.candidateId}.`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
358
363
|
if (round.taskCandidate === undefined) {
|
|
359
364
|
throw new Error(`Task ReviewRound requires a frozen Task candidate: ${round.id}.`);
|
|
360
365
|
}
|
|
@@ -372,9 +377,19 @@ export function validateReviewRound(round) {
|
|
|
372
377
|
}
|
|
373
378
|
}
|
|
374
379
|
}
|
|
375
|
-
else
|
|
376
|
-
|| round.
|
|
377
|
-
|
|
380
|
+
else {
|
|
381
|
+
if (round.workItemId === undefined || round.candidateId === undefined) {
|
|
382
|
+
throw new Error(`WorkItem ReviewRound requires a Candidate anchor: ${round.id}.`);
|
|
383
|
+
}
|
|
384
|
+
validateTaskRecordReference({ taskId: round.taskId, localId: round.workItemId }, "workItem");
|
|
385
|
+
if (!/^candidate-[1-9]\d*$/.test(round.candidateId)) {
|
|
386
|
+
throw new Error(`Candidate local id is invalid: ${round.candidateId}.`);
|
|
387
|
+
}
|
|
388
|
+
if (round.taskCandidate !== undefined
|
|
389
|
+
|| round.taskFinalReviewContract !== undefined
|
|
390
|
+
|| round.legacyAnchor !== undefined) {
|
|
391
|
+
throw new Error(`WorkItem ReviewRound cannot carry Task-final metadata: ${round.id}.`);
|
|
392
|
+
}
|
|
378
393
|
}
|
|
379
394
|
if (round.executionGroup !== undefined)
|
|
380
395
|
validateReviewExecutionGroup(round.executionGroup, round);
|
|
@@ -428,6 +443,7 @@ export function validateReviewRound(round) {
|
|
|
428
443
|
throw new Error("Only a terminal ReviewRound workspace can have a disposition.");
|
|
429
444
|
}
|
|
430
445
|
if (round.workspaceDisposition.kind !== "preserved"
|
|
446
|
+
&& round.workspaceDisposition.kind !== "reassigned"
|
|
431
447
|
&& round.workspaceDisposition.kind !== "removed") {
|
|
432
448
|
throw new Error("Review workspace disposition is invalid.");
|
|
433
449
|
}
|
|
@@ -511,11 +527,12 @@ function validateReviewExecutionGroup(group, round) {
|
|
|
511
527
|
? "task-final-review"
|
|
512
528
|
: "work-item";
|
|
513
529
|
if (group.target.kind !== expectedKind
|
|
514
|
-
|| group.target.taskId !== round.taskId
|
|
515
|
-
|| group.target.candidateId !== round.candidateId) {
|
|
530
|
+
|| group.target.taskId !== round.taskId) {
|
|
516
531
|
throw new Error(`ReviewRound ExecutionGroup target is invalid: ${round.id}.`);
|
|
517
532
|
}
|
|
518
|
-
if (expectedKind === "work-item"
|
|
533
|
+
if (expectedKind === "work-item"
|
|
534
|
+
&& (group.target.workItemId !== round.workItemId
|
|
535
|
+
|| group.target.candidateId !== round.candidateId)) {
|
|
519
536
|
throw new Error(`ReviewRound ExecutionGroup WorkItem target is invalid: ${round.id}.`);
|
|
520
537
|
}
|
|
521
538
|
return group;
|
|
@@ -20,6 +20,8 @@ const PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION = 3;
|
|
|
20
20
|
const PROJECT_KNOWLEDGE_PROPOSALS_TO_VERSION = 4;
|
|
21
21
|
const TASK_FROM_VERSION = 3;
|
|
22
22
|
const TASK_TO_VERSION = 4;
|
|
23
|
+
const TASK_INTENT_FROM_VERSION = 4;
|
|
24
|
+
const TASK_INTENT_TO_VERSION = 5;
|
|
23
25
|
const WORK_ITEM_FROM_VERSION = 6;
|
|
24
26
|
const WORK_ITEM_TO_VERSION = 7;
|
|
25
27
|
const WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION = 7;
|
|
@@ -45,6 +47,8 @@ const REVIEW_ROUND_FROM_VERSION = 2;
|
|
|
45
47
|
const REVIEW_ROUND_TO_VERSION = 3;
|
|
46
48
|
const REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION = 3;
|
|
47
49
|
const REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION = 4;
|
|
50
|
+
const REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION = 4;
|
|
51
|
+
const REVIEW_ROUND_TASK_ANCHOR_TO_VERSION = 5;
|
|
48
52
|
const ACTIVE_RUN_POINTER_FROM_VERSION = 1;
|
|
49
53
|
const ACTIVE_RUN_POINTER_TO_VERSION = 2;
|
|
50
54
|
const ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION = 2;
|
|
@@ -136,6 +140,7 @@ export function createProductionStorageRegistry() {
|
|
|
136
140
|
.registerOfflineMigration(configV2Step())
|
|
137
141
|
.registerCompatible(projectKnowledgeProposalsStep())
|
|
138
142
|
.registerOfflineMigration(taskWorkspaceIdentityStep())
|
|
143
|
+
.registerOfflineMigration(taskIntentStep())
|
|
139
144
|
.registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_FROM_VERSION, WORK_ITEM_TO_VERSION, "workItems"))
|
|
140
145
|
.registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION, WORK_ITEM_GIT_SNAPSHOT_TO_VERSION, "workItems"))
|
|
141
146
|
.registerOfflineMigration(workItemExecutionGroupHistoryStep())
|
|
@@ -146,6 +151,7 @@ export function createProductionStorageRegistry() {
|
|
|
146
151
|
.registerOfflineMigration(messageWakePolicyStep())
|
|
147
152
|
.registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_FROM_VERSION, REVIEW_ROUND_TO_VERSION, "reviewRounds"))
|
|
148
153
|
.registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION, REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION, "reviewRounds"))
|
|
154
|
+
.registerOfflineMigration(reviewRoundTaskAnchorStep())
|
|
149
155
|
.registerOfflineMigration(recordFamilyStep("activeRunPointer", ACTIVE_RUN_POINTER_FROM_VERSION, ACTIVE_RUN_POINTER_TO_VERSION, "activeRuns"))
|
|
150
156
|
.registerOfflineMigration(managedWorkspaceFamilyStep())
|
|
151
157
|
.registerOfflineMigration(activeRunPointerNamespaceStep())
|
|
@@ -1546,6 +1552,65 @@ function recordFamilyStep(recordKind, fromVersion, toVersion, taskMapKey) {
|
|
|
1546
1552
|
declaredEffects: []
|
|
1547
1553
|
};
|
|
1548
1554
|
}
|
|
1555
|
+
function reviewRoundTaskAnchorStep() {
|
|
1556
|
+
return {
|
|
1557
|
+
axis: "record",
|
|
1558
|
+
recordKind: "reviewRound",
|
|
1559
|
+
fromVersion: REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION,
|
|
1560
|
+
toVersion: REVIEW_ROUND_TASK_ANCHOR_TO_VERSION,
|
|
1561
|
+
preconditions: (snapshot) => requireRecordFamilyVersion(snapshot, "reviewRound", REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION, "reviewRounds"),
|
|
1562
|
+
transform: migrateReviewRoundTaskAnchors,
|
|
1563
|
+
declaredEffects: []
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function migrateReviewRoundTaskAnchors(snapshot) {
|
|
1567
|
+
requireRecordFamilyVersion(snapshot, "reviewRound", REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION, "reviewRounds");
|
|
1568
|
+
const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
1569
|
+
const schemaManifest = {
|
|
1570
|
+
...snapshot.schemaManifest,
|
|
1571
|
+
recordVersions: {
|
|
1572
|
+
...manifestVersions,
|
|
1573
|
+
reviewRound: REVIEW_ROUND_TASK_ANCHOR_TO_VERSION
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
if (snapshot.state === null)
|
|
1577
|
+
return { schemaManifest, state: null };
|
|
1578
|
+
const tasks = asObject(snapshot.state.tasks, "state tasks");
|
|
1579
|
+
const nextTasks = {};
|
|
1580
|
+
for (const [taskId, rawTask] of Object.entries(tasks)) {
|
|
1581
|
+
const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
|
|
1582
|
+
const rawRounds = aggregate.reviewRounds;
|
|
1583
|
+
if (rawRounds === undefined) {
|
|
1584
|
+
nextTasks[taskId] = { ...aggregate };
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
const rounds = asObject(rawRounds, `reviewRound map ${taskId}`);
|
|
1588
|
+
const nextRounds = {};
|
|
1589
|
+
for (const [roundId, rawRound] of Object.entries(rounds)) {
|
|
1590
|
+
const round = asObject(rawRound, `reviewRound ${taskId}/${roundId}`);
|
|
1591
|
+
if (round.scope !== "task") {
|
|
1592
|
+
nextRounds[roundId] = {
|
|
1593
|
+
...round,
|
|
1594
|
+
schemaVersion: REVIEW_ROUND_TASK_ANCHOR_TO_VERSION
|
|
1595
|
+
};
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
const workItemId = requiredMigrationText(round.workItemId, `reviewRound ${taskId}/${roundId} WorkItem id`);
|
|
1599
|
+
const candidateId = requiredMigrationText(round.candidateId, `reviewRound ${taskId}/${roundId} Candidate id`);
|
|
1600
|
+
const { workItemId: _workItemId, candidateId: _candidateId, schemaVersion: _schemaVersion, ...retained } = round;
|
|
1601
|
+
nextRounds[roundId] = {
|
|
1602
|
+
...retained,
|
|
1603
|
+
schemaVersion: REVIEW_ROUND_TASK_ANCHOR_TO_VERSION,
|
|
1604
|
+
legacyAnchor: { workItemId, candidateId }
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
nextTasks[taskId] = { ...aggregate, reviewRounds: nextRounds };
|
|
1608
|
+
}
|
|
1609
|
+
return {
|
|
1610
|
+
schemaManifest,
|
|
1611
|
+
state: { ...snapshot.state, tasks: nextTasks }
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1549
1614
|
function agentRunContextProtocolStep() {
|
|
1550
1615
|
return {
|
|
1551
1616
|
axis: "record",
|
|
@@ -2747,6 +2812,79 @@ function migrateTaskV3ToV4(snapshot) {
|
|
|
2747
2812
|
state: { ...snapshot.state, tasks: nextTasks }
|
|
2748
2813
|
};
|
|
2749
2814
|
}
|
|
2815
|
+
/**
|
|
2816
|
+
* Task v5 replaces the stored delivery topology switch with an optional,
|
|
2817
|
+
* Project-defined intent. Historical topology is retained as audit context,
|
|
2818
|
+
* but no current workflow decision reads it.
|
|
2819
|
+
*/
|
|
2820
|
+
function taskIntentStep() {
|
|
2821
|
+
return {
|
|
2822
|
+
axis: "record",
|
|
2823
|
+
recordKind: "task",
|
|
2824
|
+
fromVersion: TASK_INTENT_FROM_VERSION,
|
|
2825
|
+
toVersion: TASK_INTENT_TO_VERSION,
|
|
2826
|
+
preconditions: requireTaskV4Family,
|
|
2827
|
+
transform: migrateTaskV4ToV5,
|
|
2828
|
+
declaredEffects: []
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
function requireTaskV4Family(snapshot) {
|
|
2832
|
+
const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
2833
|
+
if (manifestVersions.task !== TASK_INTENT_FROM_VERSION) {
|
|
2834
|
+
throw new Error(`Record task migration requires manifest version ${TASK_INTENT_FROM_VERSION}.`);
|
|
2835
|
+
}
|
|
2836
|
+
if (snapshot.state === null)
|
|
2837
|
+
return;
|
|
2838
|
+
const tasks = asObject(snapshot.state.tasks, "state tasks");
|
|
2839
|
+
for (const [taskId, rawTask] of Object.entries(tasks)) {
|
|
2840
|
+
const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
|
|
2841
|
+
if (aggregate.task === undefined)
|
|
2842
|
+
continue;
|
|
2843
|
+
const record = asObject(aggregate.task, `Task ${taskId}`);
|
|
2844
|
+
if (record.schemaVersion !== TASK_INTENT_FROM_VERSION) {
|
|
2845
|
+
throw new Error(`Task ${taskId} must use schemaVersion ${TASK_INTENT_FROM_VERSION}.`);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
function migrateTaskV4ToV5(snapshot) {
|
|
2850
|
+
requireTaskV4Family(snapshot);
|
|
2851
|
+
const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
2852
|
+
const schemaManifest = {
|
|
2853
|
+
...snapshot.schemaManifest,
|
|
2854
|
+
recordVersions: { ...manifestVersions, task: TASK_INTENT_TO_VERSION }
|
|
2855
|
+
};
|
|
2856
|
+
if (snapshot.state === null)
|
|
2857
|
+
return { schemaManifest, state: null };
|
|
2858
|
+
const tasks = asObject(snapshot.state.tasks, "state tasks");
|
|
2859
|
+
const nextTasks = {};
|
|
2860
|
+
for (const [taskId, rawTask] of Object.entries(tasks)) {
|
|
2861
|
+
const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
|
|
2862
|
+
if (aggregate.task === undefined) {
|
|
2863
|
+
nextTasks[taskId] = { ...aggregate };
|
|
2864
|
+
continue;
|
|
2865
|
+
}
|
|
2866
|
+
const task = asObject(aggregate.task, `Task ${taskId}`);
|
|
2867
|
+
const { requireIntegration, schemaVersion: _schemaVersion, ...retained } = task;
|
|
2868
|
+
const projectBindings = Array.isArray(task.projectBindings)
|
|
2869
|
+
? task.projectBindings
|
|
2870
|
+
: [];
|
|
2871
|
+
const legacyDeliveryPath = projectBindings.length === 0
|
|
2872
|
+
? undefined
|
|
2873
|
+
: requireIntegration === true ? "integrated" : "direct";
|
|
2874
|
+
nextTasks[taskId] = {
|
|
2875
|
+
...aggregate,
|
|
2876
|
+
task: {
|
|
2877
|
+
...retained,
|
|
2878
|
+
schemaVersion: TASK_INTENT_TO_VERSION,
|
|
2879
|
+
...(legacyDeliveryPath === undefined ? {} : { legacyDeliveryPath })
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
return {
|
|
2884
|
+
schemaManifest,
|
|
2885
|
+
state: { ...snapshot.state, tasks: nextTasks }
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2750
2888
|
/**
|
|
2751
2889
|
* A version bump is deliverable only when the shared planner resolves the full
|
|
2752
2890
|
* adjacent path. This also covers target-only record families as explicit 0->1
|
|
@@ -722,7 +722,7 @@ export class SqliteTaskStore {
|
|
|
722
722
|
id: task.id,
|
|
723
723
|
status: task.status,
|
|
724
724
|
projectBindings: task.projectBindings,
|
|
725
|
-
|
|
725
|
+
type: task.type
|
|
726
726
|
},
|
|
727
727
|
workItems: this.#sortById(this.#listPayload("work_items", "task_id = ?", [taskId]), (item) => item.id),
|
|
728
728
|
changeSets: this.#sortById(this.#listPayload("change_sets", "task_id = ?", [taskId]), (changeSet) => changeSet.id),
|