@henryqw/pi-subagent 27.5.2 → 27.5.4
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/runner.d.ts +1 -0
- package/dist/runner.js +96 -72
- package/docs/orchestration.md +4 -2
- package/extensions/subagent-command.ts +20 -9
- package/package.json +1 -1
package/dist/runner.d.ts
CHANGED
|
@@ -350,6 +350,7 @@ export declare class IsolatedRunner {
|
|
|
350
350
|
private releaseRejected;
|
|
351
351
|
private releaseSuperseded;
|
|
352
352
|
private cleanupPromoted;
|
|
353
|
+
private cleanupPromotedStage;
|
|
353
354
|
private requireExactIntegration;
|
|
354
355
|
listOrphanRequests(cwd: string): Promise<{
|
|
355
356
|
invalidIds: string[];
|
package/dist/runner.js
CHANGED
|
@@ -592,6 +592,8 @@ export function nextIntegrationAction(state) {
|
|
|
592
592
|
return `Call subagent_integrate refresh with generation ${generation.number + 1}, expectedTip ${JSON.stringify(generation.combinedTip)}, and identical expectedMain and newMain; then restage the candidates and validate again.`;
|
|
593
593
|
}
|
|
594
594
|
return undefined;
|
|
595
|
+
case "promoted":
|
|
596
|
+
return `Promotion is proven, but cleanup retained resources; resolve the recorded cleanup failures without discarding work, then call subagent_integrate cleanup with ${exact}.`;
|
|
595
597
|
default:
|
|
596
598
|
return undefined;
|
|
597
599
|
}
|
|
@@ -1062,6 +1064,8 @@ export class IsolatedRunner {
|
|
|
1062
1064
|
if (action.action === "reject" && allocationByKind(attempt, "agent")) {
|
|
1063
1065
|
if (await this.terminateWithSafety(handle, task, attempt, action.candidate, scope.signal))
|
|
1064
1066
|
task.status = "rejected";
|
|
1067
|
+
else
|
|
1068
|
+
this.attention(task, `Worker termination is unproved: ${attempt.termination.failure}`);
|
|
1065
1069
|
await this.saveProductive(handle);
|
|
1066
1070
|
}
|
|
1067
1071
|
if (action.action === "revise") {
|
|
@@ -1605,101 +1609,123 @@ export class IsolatedRunner {
|
|
|
1605
1609
|
// Promotion proves selection independently of worker or checkout cleanup.
|
|
1606
1610
|
for (const stage of generation.stages)
|
|
1607
1611
|
changesetTaskState(state, stage.taskId).status = "completed";
|
|
1612
|
+
const generationCleanup = generation.cleanup ??= [{ kind: "worktree", status: "pending" }, { kind: "branch", status: "pending" }];
|
|
1613
|
+
const stepLists = [...generation.stages.map((stage) => changesetTaskState(state, stage.taskId).attempts[stage.attempt - 1].cleanup),
|
|
1614
|
+
generationCleanup];
|
|
1615
|
+
// Record each failure on the step it blocks; other stages and the integration checkout continue.
|
|
1616
|
+
const blocked = (steps, error) => {
|
|
1617
|
+
const step = steps.find((item) => item.status === "running") ?? steps.find((item) => item.status !== "completed");
|
|
1618
|
+
if (step)
|
|
1619
|
+
step.failure = bounded(`Cleanup requires attention: ${errorText(error)}`);
|
|
1620
|
+
};
|
|
1608
1621
|
await this.saveProductive(handle);
|
|
1622
|
+
let main;
|
|
1609
1623
|
try {
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1624
|
+
main = await this.callProductive(handle, scope, (context) => this.gitRuntime.inspectMain({ root: state.root }, context));
|
|
1625
|
+
}
|
|
1626
|
+
catch (error) {
|
|
1627
|
+
for (const steps of stepLists)
|
|
1628
|
+
blocked(steps, error);
|
|
1629
|
+
}
|
|
1630
|
+
if (main && !sameIdentity(main, promoted)) {
|
|
1631
|
+
for (const steps of stepLists)
|
|
1632
|
+
blocked(steps, new Error("Main changed or became dirty after promotion; cleanup is blocked."));
|
|
1633
|
+
}
|
|
1634
|
+
else if (main) {
|
|
1613
1635
|
for (const stage of generation.stages) {
|
|
1614
1636
|
const task = changesetTaskState(state, stage.taskId);
|
|
1615
1637
|
const attempt = task.attempts[stage.attempt - 1];
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
const worker = allocationByKind(attempt, "worktree")?.worktree;
|
|
1619
|
-
if (!worker)
|
|
1620
|
-
throw new Error("Promoted worker lacks a proven checkout.");
|
|
1621
|
-
candidate.worker = "release_pending";
|
|
1622
|
-
await this.saveProductive(handle);
|
|
1623
|
-
if (attempt.termination?.status !== "terminated") {
|
|
1624
|
-
if (attempt.termination) {
|
|
1625
|
-
const result = await this.callProductive(handle, scope, (context) => this.hostRuntime.reconcileWorkerTermination({
|
|
1626
|
-
task: changesetTaskRequest(state, task.taskId), attempt,
|
|
1627
|
-
workerId: attempt.termination.workerId, candidate: stage.source,
|
|
1628
|
-
}, context));
|
|
1629
|
-
if (result.outcome !== "terminated")
|
|
1630
|
-
throw new Error("Worker termination cannot be proved; never replay termination.");
|
|
1631
|
-
attempt.termination = { status: "terminated", workerId: attempt.termination.workerId,
|
|
1632
|
-
candidate: stage.source, at: this.coordinatorRuntime.now() };
|
|
1633
|
-
await this.saveProductive(handle);
|
|
1634
|
-
}
|
|
1635
|
-
else if (!await this.terminateWithSafety(handle, task, attempt, stage.source, scope.signal)) {
|
|
1636
|
-
throw new Error("Worker termination was not proved.");
|
|
1637
|
-
}
|
|
1638
|
+
try {
|
|
1639
|
+
await this.cleanupPromotedStage(handle, generation, receipts, promoted, stage, task, attempt, scope);
|
|
1638
1640
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
+
catch (error) {
|
|
1642
|
+
blocked(attempt.cleanup, error);
|
|
1643
|
+
}
|
|
1644
|
+
await this.saveProductive(handle);
|
|
1645
|
+
}
|
|
1646
|
+
try {
|
|
1647
|
+
for (const step of generationCleanup) {
|
|
1641
1648
|
if (step.status === "completed")
|
|
1642
1649
|
continue;
|
|
1643
1650
|
step.status = "running";
|
|
1644
1651
|
await this.saveProductive(handle);
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
}, context));
|
|
1649
|
-
if (result.outcome === "blocked")
|
|
1650
|
-
throw new Error(result.failure);
|
|
1651
|
-
}
|
|
1652
|
-
else {
|
|
1653
|
-
const result = await this.callProductive(handle, scope, (context) => this.integrationGit.cleanup(state.root, generation.worktree, worker, generation.expectedMain, receipts, promoted, kind, context.signal, generation.correction));
|
|
1654
|
-
if (result.outcome !== "ready")
|
|
1655
|
-
throw new Error(result.failure);
|
|
1656
|
-
}
|
|
1652
|
+
const result = await this.callProductive(handle, scope, (context) => this.integrationGit.cleanup(state.root, generation.worktree, generation.worktree, generation.expectedMain, receipts, promoted, step.kind, context.signal, generation.correction));
|
|
1653
|
+
if (result.outcome !== "ready")
|
|
1654
|
+
throw new Error(result.failure);
|
|
1657
1655
|
step.status = "completed";
|
|
1658
1656
|
delete step.failure;
|
|
1659
1657
|
await this.saveProductive(handle);
|
|
1660
1658
|
}
|
|
1661
|
-
candidate.worker = "released";
|
|
1662
|
-
// Same-attempt revisions share this exact worker checkout; release historical
|
|
1663
|
-
// rejected tips only after the physical checkout has been proved removed.
|
|
1664
|
-
for (const old of state.integration.candidates.filter((item) => item.decision === "rejected"
|
|
1665
|
-
&& item.taskId === stage.taskId && item.attempt === stage.attempt))
|
|
1666
|
-
old.worker = "released";
|
|
1667
|
-
await this.saveProductive(handle);
|
|
1668
1659
|
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
for (const kind of ["worktree", "branch"]) {
|
|
1672
|
-
const step = generation.cleanup.find((item) => item.kind === kind);
|
|
1673
|
-
if (step.status === "completed")
|
|
1674
|
-
continue;
|
|
1675
|
-
step.status = "running";
|
|
1676
|
-
await this.saveProductive(handle);
|
|
1677
|
-
const result = await this.callProductive(handle, scope, (context) => this.integrationGit.cleanup(state.root, generation.worktree, generation.worktree, generation.expectedMain, receipts, promoted, kind, context.signal, generation.correction));
|
|
1678
|
-
if (result.outcome !== "ready")
|
|
1679
|
-
throw new Error(result.failure);
|
|
1680
|
-
step.status = "completed";
|
|
1681
|
-
delete step.failure;
|
|
1682
|
-
await this.saveProductive(handle);
|
|
1660
|
+
catch (error) {
|
|
1661
|
+
blocked(generationCleanup, error);
|
|
1683
1662
|
}
|
|
1684
1663
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
?? generation.stages.flatMap((stage) => changesetTaskState(state, stage.taskId).attempts[stage.attempt - 1].cleanup)
|
|
1689
|
-
.find((step) => step.status === "running");
|
|
1690
|
-
if (pending)
|
|
1691
|
-
pending.failure = bounded(`Cleanup requires attention: ${errorText(error)}`);
|
|
1692
|
-
await this.saveProductive(handle);
|
|
1693
|
-
}
|
|
1694
|
-
if (state.tasks.every((task) => task.status === "completed" || task.status === "rejected")
|
|
1664
|
+
// Promotion remains proven; the request stays visible until every promoted resource is released.
|
|
1665
|
+
if (stepLists.every((steps) => steps.every((step) => step.status === "completed"))
|
|
1666
|
+
&& state.tasks.every((task) => task.status === "completed" || task.status === "rejected")
|
|
1695
1667
|
&& state.integration.candidates.every((candidate) => candidate.decision || generation.stages.some((stage) => stage.taskId === candidate.taskId && stage.attempt === candidate.attempt && sameIdentity(stage.source, candidate.tip)))) {
|
|
1696
1668
|
state.final = { status: "passed", identity: promoted, checks: generation.checks,
|
|
1697
1669
|
...(generation.review ? { review: generation.review } : {}) };
|
|
1698
1670
|
state.status = "completed";
|
|
1699
1671
|
state.accepted = true;
|
|
1700
1672
|
state.acceptedAt = this.coordinatorRuntime.now();
|
|
1673
|
+
}
|
|
1674
|
+
await this.saveProductive(handle);
|
|
1675
|
+
}
|
|
1676
|
+
async cleanupPromotedStage(handle, generation, receipts, promoted, stage, task, attempt, scope) {
|
|
1677
|
+
const state = handle.state;
|
|
1678
|
+
const candidate = state.integration.candidates.find((item) => item.taskId === stage.taskId && item.attempt === stage.attempt
|
|
1679
|
+
&& sameIdentity(item.tip, stage.source));
|
|
1680
|
+
const worker = allocationByKind(attempt, "worktree")?.worktree;
|
|
1681
|
+
if (!worker)
|
|
1682
|
+
throw new Error("Promoted worker lacks a proven checkout.");
|
|
1683
|
+
candidate.worker = "release_pending";
|
|
1684
|
+
await this.saveProductive(handle);
|
|
1685
|
+
if (attempt.termination?.status !== "terminated") {
|
|
1686
|
+
if (attempt.termination) {
|
|
1687
|
+
const result = await this.callProductive(handle, scope, (context) => this.hostRuntime.reconcileWorkerTermination({
|
|
1688
|
+
task: changesetTaskRequest(state, task.taskId), attempt,
|
|
1689
|
+
workerId: attempt.termination.workerId, candidate: stage.source,
|
|
1690
|
+
}, context));
|
|
1691
|
+
if (result.outcome !== "terminated")
|
|
1692
|
+
throw new Error("Worker termination cannot be proved; never replay termination.");
|
|
1693
|
+
attempt.termination = { status: "terminated", workerId: attempt.termination.workerId,
|
|
1694
|
+
candidate: stage.source, at: this.coordinatorRuntime.now() };
|
|
1695
|
+
await this.saveProductive(handle);
|
|
1696
|
+
}
|
|
1697
|
+
else if (!await this.terminateWithSafety(handle, task, attempt, stage.source, scope.signal)) {
|
|
1698
|
+
throw new Error(`Worker termination was not proved: ${attempt.termination.failure}`);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
for (const kind of CLEANUP_KINDS) {
|
|
1702
|
+
const step = attempt.cleanup.find((item) => item.kind === kind);
|
|
1703
|
+
if (step.status === "completed")
|
|
1704
|
+
continue;
|
|
1705
|
+
step.status = "running";
|
|
1706
|
+
await this.saveProductive(handle);
|
|
1707
|
+
if (kind === "worker_tab" || kind === "workspace") {
|
|
1708
|
+
const result = await this.callProductive(handle, scope, (context) => this.hostRuntime.cleanupHost({
|
|
1709
|
+
requestId: state.request.id, kind, task: changesetTaskRequest(state, task.taskId), attempt,
|
|
1710
|
+
}, context));
|
|
1711
|
+
if (result.outcome === "blocked")
|
|
1712
|
+
throw new Error(result.failure);
|
|
1713
|
+
}
|
|
1714
|
+
else {
|
|
1715
|
+
const result = await this.callProductive(handle, scope, (context) => this.integrationGit.cleanup(state.root, generation.worktree, worker, generation.expectedMain, receipts, promoted, kind, context.signal, generation.correction));
|
|
1716
|
+
if (result.outcome !== "ready")
|
|
1717
|
+
throw new Error(result.failure);
|
|
1718
|
+
}
|
|
1719
|
+
step.status = "completed";
|
|
1720
|
+
delete step.failure;
|
|
1701
1721
|
await this.saveProductive(handle);
|
|
1702
1722
|
}
|
|
1723
|
+
candidate.worker = "released";
|
|
1724
|
+
// Same-attempt revisions share this exact worker checkout; release historical
|
|
1725
|
+
// rejected tips only after the physical checkout has been proved removed.
|
|
1726
|
+
for (const old of state.integration.candidates.filter((item) => item.decision === "rejected"
|
|
1727
|
+
&& item.taskId === stage.taskId && item.attempt === stage.attempt))
|
|
1728
|
+
old.worker = "released";
|
|
1703
1729
|
}
|
|
1704
1730
|
async requireExactIntegration(handle, generation, receipts, scope, allowDirtyMain = false) {
|
|
1705
1731
|
const root = handle.state.root;
|
|
@@ -2736,7 +2762,6 @@ export class IsolatedRunner {
|
|
|
2736
2762
|
if (result.outcome !== "terminated") {
|
|
2737
2763
|
const failure = boundedRuntimeText(result.failure, "Worker termination failure");
|
|
2738
2764
|
attempt.termination = { status: "unknown", workerId, candidate, failure };
|
|
2739
|
-
this.attention(task, `Worker termination is unproved: ${failure}`);
|
|
2740
2765
|
return false;
|
|
2741
2766
|
}
|
|
2742
2767
|
attempt.termination = { status: "terminated", workerId, candidate, at: this.coordinatorRuntime.now() };
|
|
@@ -2745,7 +2770,6 @@ export class IsolatedRunner {
|
|
|
2745
2770
|
catch (error) {
|
|
2746
2771
|
this.rethrowStopped(error);
|
|
2747
2772
|
attempt.termination = { status: "unknown", workerId, candidate, failure: errorText(error) };
|
|
2748
|
-
this.attention(task, `Worker termination is unproved: ${errorText(error)}`);
|
|
2749
2773
|
return false;
|
|
2750
2774
|
}
|
|
2751
2775
|
finally {
|
package/docs/orchestration.md
CHANGED
|
@@ -295,6 +295,8 @@ Rejection invalidates dependent work. Rejection and revision ignore dependent wo
|
|
|
295
295
|
|
|
296
296
|
Use the generation's exact `combinedTip` as `expectedTip`. `advance` dispatches ready dependents from that snapshot. `correct` records one clean committed correction after a definitive failed check or review. `reconcile` inspects interrupted validation or promotion without replaying uncertain promotion. `cleanup` finishes proved post-promotion cleanup.
|
|
297
297
|
|
|
298
|
+
Post-promotion cleanup handles each promoted stage separately. A blocked termination or cleanup step records its failure on that step, and cleanup continues with the other stages and then the promoted integration checkout. Clean Main at the promoted identity is a shared precondition; if Main changed, every pending step records the blocker. Dirty, untracked, or ignored files block worktree removal and stay preserved. Until every promoted termination and cleanup step completes, the request stays `needs_attention` with promotion proven, selected tasks `completed`, and not accepted, so `/subagent` lists it. Resolve the recorded blockers, then call `cleanup` again; the request becomes `completed` and accepted only after cleanup completes.
|
|
299
|
+
|
|
298
300
|
Validation acknowledges only after saving the exact generation's validation intent. Checks and final judgment then run in the background. Later turn cancellation does not cancel acknowledged validation; session replacement or shutdown does. Acknowledgement is not passing evidence. Inspect status if the completion follow-up fails.
|
|
299
301
|
|
|
300
302
|
#### Refresh
|
|
@@ -375,7 +377,7 @@ The report lists removed, skipped, and failed resources. After complete cleanup,
|
|
|
375
377
|
|
|
376
378
|
### Prune leftover worktrees
|
|
377
379
|
|
|
378
|
-
Run `/subagent prune` to clean up leftover worker and integration checkouts under `.worktrees/`. Their branches have the name `pi-subagent/subagent-` plus 24 hex characters. The command also cleans up such branches that have no checkout. The review
|
|
380
|
+
Run `/subagent prune` to clean up leftover worker and integration checkouts under `.worktrees/`. Their branches have the name `pi-subagent/subagent-` plus 24 hex characters. The command also cleans up such branches that have no checkout. The review starts with the number of removable and blocked items. It groups the items by the exact reason that the command can remove them, or by their exact blockers, and shows each reason once. Under each reason, it identifies each item by the first 8 of its 24 hex characters. The command can remove an item only when all of these conditions are true:
|
|
379
381
|
|
|
380
382
|
- Its branch tip is already in Main's HEAD or in the local `origin` default-branch ref. The command does not fetch.
|
|
381
383
|
- Its checkout has no tracked or untracked changes. No merge, rebase, cherry-pick, revert, or bisect is in progress.
|
|
@@ -387,7 +389,7 @@ One confirmation removes every listed removable item. When the command removes a
|
|
|
387
389
|
|
|
388
390
|
If a top-level ignored entry is new or changed, the command skips the checkout. If the full list of ignored entries of a checkout does not fit in that dialog, the command excludes the checkout. The command blocks checkouts with initialized submodules. It also blocks owned checkouts that are detached or on another branch. Remove them manually.
|
|
389
391
|
|
|
390
|
-
The list shows removable items first. The command never cuts a long list silently. The list ends with `+N more not shown`, and the command does not remove the items that it did not show in that run. Run `/subagent prune` again to review them.
|
|
392
|
+
The list shows removable items first. The command never cuts a long list silently. The list ends with `+N more not shown`, where `N` counts items, and the command does not remove the items that it did not show in that run. Run `/subagent prune` again to review them.
|
|
391
393
|
|
|
392
394
|
The command checks each item again immediately before removal and skips an item that changed. If the session or branch changes during removal, the command skips the remaining steps. The command deletes a branch only if the branch still points at the reviewed tip. Then the command reconciles each aborted request that recorded a removed resource. It uses the same guarded path as a repeated `subagent_abort`, so the cleanup of that request no longer shows as pending.
|
|
393
395
|
|
|
@@ -100,13 +100,12 @@ const errorText = (error: unknown): string => clean(error instanceof Error ? err
|
|
|
100
100
|
const completed = (status: string) => ["completed", "success", "failed", "failure", "aborted", "integrated"].includes(status);
|
|
101
101
|
const priority = (status: string) => /attention|blocked|retained|failed/i.test(status) ? 0 : completed(status) ? 2 : 1;
|
|
102
102
|
|
|
103
|
-
const ignoredSummary = ({ ignored }: PruneItem) => !ignored.length ? "none"
|
|
104
|
-
: `${ignored.length} (${ignored.slice(0, 5).join(", ")}${ignored.length > 5 ? `, +${ignored.length - 5} more` : ""})`;
|
|
105
103
|
const PRUNE_IGNORED_CONFIRM_CODE_POINTS = 4_000;
|
|
106
104
|
const NOT_SHOWN = "they are not removed in this run; run /subagent prune again after this run.";
|
|
107
105
|
|
|
108
106
|
/** Fit whole item lines within `max` code points; never cut silently, and report exactly which items were shown. */
|
|
109
|
-
function fitItems<T>(header: string, items: readonly T[], line: (item: T) => string, max: number, omitted = NOT_SHOWN
|
|
107
|
+
function fitItems<T>(header: string, items: readonly T[], line: (item: T) => string, max: number, omitted = NOT_SHOWN,
|
|
108
|
+
count: (item: T) => number = () => 1): { text: string; shown: T[] } {
|
|
110
109
|
const lines = [header];
|
|
111
110
|
const shown: T[] = [];
|
|
112
111
|
let size = [...header].length;
|
|
@@ -119,18 +118,30 @@ function fitItems<T>(header: string, items: readonly T[], line: (item: T) => str
|
|
|
119
118
|
size += length;
|
|
120
119
|
shown.push(item);
|
|
121
120
|
}
|
|
122
|
-
|
|
121
|
+
const hidden = items.slice(shown.length).reduce((sum, item) => sum + count(item), 0);
|
|
122
|
+
if (hidden) lines.push(`+${hidden} more not shown; ${omitted}`);
|
|
123
123
|
return { text: lines.join("\n"), shown };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
/**
|
|
126
|
+
/** Groups items by exact reason, prunable groups first, in lines of 10 IDs so a long list is cut at whole lines.
|
|
127
|
+
* Only shown items may be removed. IDs are display-only; removal uses the reviewed items. */
|
|
127
128
|
function pruneReview({ root, items }: PrunePlan): { text: string; shown: PruneItem[] } {
|
|
128
129
|
if (!items.length) return { text: `No pi-subagent worktrees or branches in ${root}.`, shown: [] };
|
|
130
|
+
const groups = new Map<string, PruneItem[]>();
|
|
131
|
+
for (const item of [...items.filter((item) => item.prunable), ...items.filter((item) => !item.prunable)]) {
|
|
132
|
+
const title = item.prunable ? `✓ Prunable: ${item.reason}` : `✗ Blocked: ${item.path ? item.reason : `${item.reason}; no checkout`}`;
|
|
133
|
+
if (groups.has(title)) groups.get(title)!.push(item);
|
|
134
|
+
else groups.set(title, [item]);
|
|
135
|
+
}
|
|
136
|
+
const rows = [...groups].flatMap(([title, group]) => Array.from({ length: Math.ceil(group.length / 10) }, (_, index) => {
|
|
137
|
+
const chunk = group.slice(index * 10, index * 10 + 10);
|
|
138
|
+
const ids = chunk.map(({ branch }) => clean(branch.replace(/^pi-subagent\/subagent-/, "").slice(0, 8))).join(", ");
|
|
139
|
+
return { items: chunk, text: `${index ? "" : `${clean(title, 600)} (${group.length}):\n`} ${ids}` };
|
|
140
|
+
}));
|
|
129
141
|
const prunable = items.filter((item) => item.prunable).length;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
ROLES_REPORT_CODE_POINTS);
|
|
142
|
+
const reviewed = fitItems(`Subagent prune review in ${root}: ${prunable} prunable, ${items.length - prunable} blocked. IDs are the first 8 characters after pi-subagent/subagent-.`,
|
|
143
|
+
rows, (row) => row.text, ROLES_REPORT_CODE_POINTS, NOT_SHOWN, (row) => row.items.length);
|
|
144
|
+
return { text: reviewed.text, shown: reviewed.shown.flatMap((row) => row.items) };
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
function discardReview(plan: DiscardPlan) {
|