@zq-silk/yui 0.2.0 → 0.4.2
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 +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
2
|
+
import { resolveProject } from "../repository/project.js";
|
|
3
|
+
import { workspaceProjectEntry } from "../worktree/managedWorkspace.js";
|
|
4
|
+
import { usageError } from "../errors/cliError.js";
|
|
5
|
+
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
6
|
+
import { createIntegrationAttempt, recordResolutionDecision, updateIntegrationAttempt } from "../integration/integrationAttempt.js";
|
|
7
|
+
import { GitIntegrationService } from "../integration/gitIntegrationService.js";
|
|
8
|
+
import { taskActor } from "./taskActor.js";
|
|
9
|
+
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
10
|
+
export async function runTaskIntegrationCommand(args, store, home, options = {}) {
|
|
11
|
+
const now = options.now ?? (() => new Date());
|
|
12
|
+
const [command, ...rest] = args;
|
|
13
|
+
if (command === "start")
|
|
14
|
+
return start(rest, store, home, now, options.environment);
|
|
15
|
+
if (command === "continue") {
|
|
16
|
+
return continueIntegration(rest, store, home, now, options.environment);
|
|
17
|
+
}
|
|
18
|
+
if (command === "resolve") {
|
|
19
|
+
return resolveDecision(rest, store, now, options.environment);
|
|
20
|
+
}
|
|
21
|
+
if (command === "abort")
|
|
22
|
+
return abortIntegration(rest, store, now(), options.environment);
|
|
23
|
+
if (command === "cleanup") {
|
|
24
|
+
return cleanupIntegration(rest, store, home, options.environment);
|
|
25
|
+
}
|
|
26
|
+
if (command === "list")
|
|
27
|
+
return list(rest, store);
|
|
28
|
+
if (command === "show")
|
|
29
|
+
return show(rest, store, options.environment);
|
|
30
|
+
throw usageError(command === undefined
|
|
31
|
+
? "Task Integration command is required."
|
|
32
|
+
: `Unknown command: task integration ${command}`);
|
|
33
|
+
}
|
|
34
|
+
async function cleanupIntegration(args, store, home, environment) {
|
|
35
|
+
if (args.length !== 1) {
|
|
36
|
+
throw usageError("Task Integration cleanup usage: yui task integration cleanup <task>/<integration>.");
|
|
37
|
+
}
|
|
38
|
+
const integration = requireIntegration(store, args[0], environment);
|
|
39
|
+
if (integration.status !== "committed" && integration.status !== "failed") {
|
|
40
|
+
throw usageError(`Integration is not terminal: ${integration.id}/${integration.status}.`);
|
|
41
|
+
}
|
|
42
|
+
const result = await new GitIntegrationService(home, store).cleanup(integration);
|
|
43
|
+
if (result === "dirty") {
|
|
44
|
+
throw usageError(`Integration worktree contains unresolved or uncommitted changes: ${integration.id}. Inspect it before cleanup.`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
output: result === "removed"
|
|
48
|
+
? `Cleaned Integration worktree and check logs ${integration.id}\n`
|
|
49
|
+
: `Integration worktree and check logs already clean: ${integration.id}\n`,
|
|
50
|
+
data: { integrationId: integration.id, cleanup: result }
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function start(args, store, home, now, environment) {
|
|
54
|
+
const usage = "Task Integration start usage: yui task integration start <task> [--project <project>] --change-set <id> [--change-set <id> ...] [--target <ref>] [--check <command> ...].";
|
|
55
|
+
const parsed = parseRepeatable(args, new Set(["--change-set", "--check"]), new Set(["--project", "--target"]), usage);
|
|
56
|
+
if (parsed.positionals.length !== 1)
|
|
57
|
+
throw usageError(usage);
|
|
58
|
+
const task = store.getTask(parsed.positionals[0]);
|
|
59
|
+
if (task === null)
|
|
60
|
+
throw usageError(`Task not found: ${parsed.positionals[0]}.`);
|
|
61
|
+
if (task.status !== "active") {
|
|
62
|
+
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
63
|
+
}
|
|
64
|
+
const changeSetIds = parsed.many.get("--change-set") ?? [];
|
|
65
|
+
if (changeSetIds.length === 0)
|
|
66
|
+
throw usageError("--change-set is required.", usage);
|
|
67
|
+
const changeSets = changeSetIds.map((id) => {
|
|
68
|
+
const changeSet = store.getChangeSet(task.id, id);
|
|
69
|
+
if (changeSet === null)
|
|
70
|
+
throw usageError(`ChangeSet not found: ${id}.`);
|
|
71
|
+
return changeSet;
|
|
72
|
+
});
|
|
73
|
+
const reviewEvidence = new Set(store.listReviewRounds(task.id)
|
|
74
|
+
.flatMap(({ evidenceCommit }) => evidenceCommit === undefined ? [] : [evidenceCommit]));
|
|
75
|
+
const reviewSource = changeSets.find(({ headCommit }) => reviewEvidence.has(headCommit));
|
|
76
|
+
if (reviewSource !== undefined) {
|
|
77
|
+
throw usageError(`ReviewRound evidence commit cannot become an Integration source: ${reviewSource.id}.`);
|
|
78
|
+
}
|
|
79
|
+
const projectIds = [...new Set(changeSets.map(({ projectId }) => projectId))];
|
|
80
|
+
if (projectIds.length !== 1) {
|
|
81
|
+
throw usageError("An Integration may only contain ChangeSets from one Project.");
|
|
82
|
+
}
|
|
83
|
+
const requestedProject = parsed.one.get("--project");
|
|
84
|
+
const project = requestedProject === undefined
|
|
85
|
+
? store.getProject(projectIds[0])
|
|
86
|
+
: resolveProject(store.listProjects(), requestedProject);
|
|
87
|
+
if (project === null)
|
|
88
|
+
throw usageError(`Project not found: ${requestedProject ?? projectIds[0]}.`);
|
|
89
|
+
if (project.id !== projectIds[0]) {
|
|
90
|
+
throw usageError(`ChangeSets belong to another Project: ${projectIds[0]}.`);
|
|
91
|
+
}
|
|
92
|
+
if (!task.projectBindings.some(({ projectId }) => projectId === project.id)) {
|
|
93
|
+
throw usageError(`Project does not belong to Task: ${project.id}.`);
|
|
94
|
+
}
|
|
95
|
+
const mainWorkspace = store.getTaskWorkspace(task.id);
|
|
96
|
+
const mainEntry = mainWorkspace === null
|
|
97
|
+
? undefined
|
|
98
|
+
: workspaceProjectEntry(mainWorkspace, project.id);
|
|
99
|
+
const targetRef = parsed.one.get("--target") ?? mainEntry?.branch;
|
|
100
|
+
if (targetRef === undefined) {
|
|
101
|
+
throw usageError(`Task main worktree is not ready; reconcile the Task first: ${task.id}.`);
|
|
102
|
+
}
|
|
103
|
+
const expectedHead = (await new NodeGitWorkspace().inspect(project.path, targetRef)).baseCommit;
|
|
104
|
+
const integration = store.transaction((tx) => {
|
|
105
|
+
const created = createIntegrationAttempt({
|
|
106
|
+
id: tx.nextIntegrationAttemptId(task.id),
|
|
107
|
+
taskId: task.id,
|
|
108
|
+
projectId: project.id,
|
|
109
|
+
targetRef,
|
|
110
|
+
expectedHead,
|
|
111
|
+
changeSetIds,
|
|
112
|
+
checkCommands: parsed.many.get("--check") ?? []
|
|
113
|
+
}, now());
|
|
114
|
+
tx.saveIntegrationAttempt(task.id, created);
|
|
115
|
+
return created;
|
|
116
|
+
});
|
|
117
|
+
return runIntegration(store, home, integration, now, environment);
|
|
118
|
+
}
|
|
119
|
+
async function continueIntegration(args, store, home, now, environment) {
|
|
120
|
+
const usage = "Task Integration continue usage: yui task integration continue <task>/<integration>.";
|
|
121
|
+
const parsed = parseRepeatable(args, new Set(), new Set(), usage);
|
|
122
|
+
if (parsed.positionals.length !== 1)
|
|
123
|
+
throw usageError(usage);
|
|
124
|
+
const integration = requireIntegration(store, parsed.positionals[0], environment);
|
|
125
|
+
requireActiveIntegrationTask(store, integration);
|
|
126
|
+
if (integration.status !== "validating"
|
|
127
|
+
&& (integration.status !== "blocked"
|
|
128
|
+
|| integration.resolution?.action !== "manual-resolution")) {
|
|
129
|
+
throw usageError(`Integration is not ready to continue: ${integration.id}/${integration.status}.`);
|
|
130
|
+
}
|
|
131
|
+
return runIntegration(store, home, integration, now, environment);
|
|
132
|
+
}
|
|
133
|
+
async function runIntegration(store, home, integration, now, environment) {
|
|
134
|
+
const result = await new GitIntegrationService(home, store, undefined, now, environment)
|
|
135
|
+
.integrate(integration.taskId, integration.id);
|
|
136
|
+
const output = result.status === "committed"
|
|
137
|
+
? `Integrated ${result.attempt.changeSetIds.join(", ")} into ${result.attempt.targetRef} with CAS (${result.attempt.id})\n`
|
|
138
|
+
: result.status === "blocked"
|
|
139
|
+
? `Integration ${result.attempt.id} requires a Leader resolution decision in ${result.workspace.path}\n`
|
|
140
|
+
: `Integration ${result.attempt.id} failed; target ref was not advanced\n`;
|
|
141
|
+
return { output, data: result };
|
|
142
|
+
}
|
|
143
|
+
function resolveDecision(args, store, now, environment) {
|
|
144
|
+
const usage = "Task Integration resolve usage: yui task integration resolve <task>/<integration> --option <manual-resolution|reject> --rationale <text>.";
|
|
145
|
+
const parsed = parseRepeatable(args, new Set(), new Set(["--option", "--rationale"]), usage);
|
|
146
|
+
if (parsed.positionals.length !== 1)
|
|
147
|
+
throw usageError(usage);
|
|
148
|
+
const integration = requireIntegration(store, parsed.positionals[0], environment);
|
|
149
|
+
const task = requireActiveIntegrationTask(store, integration);
|
|
150
|
+
if (taskActor(environment, task.id) !== "leader") {
|
|
151
|
+
throw usageError("Only the Task Leader can resolve an Integration conflict.");
|
|
152
|
+
}
|
|
153
|
+
const selectedOption = parsed.one.get("--option");
|
|
154
|
+
const rationale = parsed.one.get("--rationale");
|
|
155
|
+
if (selectedOption === undefined || rationale === undefined)
|
|
156
|
+
throw usageError(usage);
|
|
157
|
+
const resolved = recordResolutionDecision(integration, {
|
|
158
|
+
action: selectedOption,
|
|
159
|
+
rationale
|
|
160
|
+
}, now());
|
|
161
|
+
store.saveIntegrationAttempt(resolved.taskId, resolved);
|
|
162
|
+
return {
|
|
163
|
+
output: selectedOption === "manual-resolution"
|
|
164
|
+
? `Leader selected manual resolution for ${resolved.id}; resolve its candidate worktree, then run integration continue\n`
|
|
165
|
+
: `Leader rejected Integration ${resolved.id}\n`,
|
|
166
|
+
data: { integration: resolved }
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function abortIntegration(args, store, now, environment) {
|
|
170
|
+
const usage = "Task Integration abort usage: yui task integration abort <task>/<integration> --reason <text>.";
|
|
171
|
+
const parsed = parseRepeatable(args, new Set(), new Set(["--reason"]), usage);
|
|
172
|
+
if (parsed.positionals.length !== 1)
|
|
173
|
+
throw usageError(usage);
|
|
174
|
+
const integration = requireIntegration(store, parsed.positionals[0], environment);
|
|
175
|
+
requireActiveIntegrationTask(store, integration);
|
|
176
|
+
if (integration.status !== "running" && integration.status !== "blocked") {
|
|
177
|
+
throw usageError(`Integration cannot be aborted from ${integration.status}: ${integration.id}.`);
|
|
178
|
+
}
|
|
179
|
+
const reason = parsed.one.get("--reason");
|
|
180
|
+
if (reason === undefined)
|
|
181
|
+
throw usageError(usage);
|
|
182
|
+
const aborted = updateIntegrationAttempt(integration, {
|
|
183
|
+
status: "failed",
|
|
184
|
+
checks: [
|
|
185
|
+
...(integration.checks ?? []),
|
|
186
|
+
{ name: "aborted", outcome: "failed", details: reason }
|
|
187
|
+
]
|
|
188
|
+
}, now);
|
|
189
|
+
store.saveIntegrationAttempt(aborted.taskId, aborted);
|
|
190
|
+
return {
|
|
191
|
+
output: `Aborted Integration ${aborted.id}; start a new Integration Attempt to retry\n`,
|
|
192
|
+
data: { integration: aborted }
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function requireActiveIntegrationTask(store, integration) {
|
|
196
|
+
const task = store.getTask(integration.taskId);
|
|
197
|
+
if (task === null)
|
|
198
|
+
throw usageError(`Task not found: ${integration.taskId}.`);
|
|
199
|
+
if (task.status !== "active") {
|
|
200
|
+
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
201
|
+
}
|
|
202
|
+
return task;
|
|
203
|
+
}
|
|
204
|
+
function list(args, store) {
|
|
205
|
+
if (args.length !== 1)
|
|
206
|
+
throw usageError("Task Integration list usage: yui task integration list <task>.");
|
|
207
|
+
const task = store.getTask(args[0]);
|
|
208
|
+
if (task === null)
|
|
209
|
+
throw usageError(`Task not found: ${args[0]}.`);
|
|
210
|
+
const integrations = store.listIntegrationAttempts(task.id);
|
|
211
|
+
const output = integrations.length === 0
|
|
212
|
+
? "No Integration Attempts found.\n"
|
|
213
|
+
: `${renderTable(`Integration Attempts: ${task.id}`, [
|
|
214
|
+
{ header: "Integration", minWidth: 11, maxWidth: 24 },
|
|
215
|
+
{ header: "Project", minWidth: 8, maxWidth: 20 },
|
|
216
|
+
{ header: "Target", minWidth: 8, maxWidth: 30 },
|
|
217
|
+
{ header: "Changes", minWidth: 7, maxWidth: 10 },
|
|
218
|
+
{ header: "Status", minWidth: 7, maxWidth: 20 }
|
|
219
|
+
], integrations.map((entry) => [
|
|
220
|
+
entry.id,
|
|
221
|
+
entry.projectId,
|
|
222
|
+
entry.targetRef,
|
|
223
|
+
String(entry.changeSetIds.length),
|
|
224
|
+
entry.status
|
|
225
|
+
]), defaultTableWidth())}\n`;
|
|
226
|
+
return { output, data: { integrations } };
|
|
227
|
+
}
|
|
228
|
+
function show(args, store, environment) {
|
|
229
|
+
if (args.length !== 1)
|
|
230
|
+
throw usageError("Task Integration show usage: yui task integration show <task>/<integration>.");
|
|
231
|
+
const integration = requireIntegration(store, args[0], environment);
|
|
232
|
+
return {
|
|
233
|
+
output: `${[
|
|
234
|
+
`Integration Attempt: ${integration.id}`,
|
|
235
|
+
`Task: ${integration.taskId}`,
|
|
236
|
+
`Project: ${integration.projectId}`,
|
|
237
|
+
`Target: ${integration.targetRef}`,
|
|
238
|
+
`Expected head: ${integration.expectedHead}`,
|
|
239
|
+
`Candidate: ${integration.candidateCommit ?? "-"}`,
|
|
240
|
+
`ChangeSets: ${integration.changeSetIds.join(", ")}`,
|
|
241
|
+
`Status: ${integration.status}`,
|
|
242
|
+
`Conflict: ${integration.conflict?.summary ?? "-"}`,
|
|
243
|
+
`Resolution: ${integration.resolution?.action ?? "-"}`,
|
|
244
|
+
...(integration.checks === undefined
|
|
245
|
+
? ["Checks: -"]
|
|
246
|
+
: [
|
|
247
|
+
"Checks:",
|
|
248
|
+
...integration.checks.flatMap((check) => [
|
|
249
|
+
`- ${check.outcome}: ${check.name}`,
|
|
250
|
+
...(check.details === undefined ? [] : [` ${check.details}`]),
|
|
251
|
+
...(check.logPath === undefined ? [] : [` Log: ${check.logPath}`])
|
|
252
|
+
])
|
|
253
|
+
])
|
|
254
|
+
].join("\n")}\n`,
|
|
255
|
+
data: { integration }
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function requireIntegration(store, value, environment) {
|
|
259
|
+
let reference;
|
|
260
|
+
try {
|
|
261
|
+
reference = resolveTaskRecordReference(value, {
|
|
262
|
+
kind: "integrationAttempt",
|
|
263
|
+
contextTaskId: environment?.YUI_TASK_ID,
|
|
264
|
+
label: "Integration Attempt"
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
269
|
+
}
|
|
270
|
+
const attempt = store.getIntegrationAttempt(reference.taskId, reference.localId);
|
|
271
|
+
if (attempt === null) {
|
|
272
|
+
throw usageError(`Integration Attempt not found: ${reference.taskId}/${reference.localId}.`);
|
|
273
|
+
}
|
|
274
|
+
return attempt;
|
|
275
|
+
}
|
|
276
|
+
function parseRepeatable(args, repeatable, singular, usage) {
|
|
277
|
+
const positionals = [];
|
|
278
|
+
const many = new Map();
|
|
279
|
+
const one = new Map();
|
|
280
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
281
|
+
const value = args[index];
|
|
282
|
+
if (!value.startsWith("--")) {
|
|
283
|
+
positionals.push(value);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (!repeatable.has(value) && !singular.has(value)) {
|
|
287
|
+
throw usageError(`Unsupported option: ${value}.`, usage);
|
|
288
|
+
}
|
|
289
|
+
if (singular.has(value) && one.has(value)) {
|
|
290
|
+
throw usageError(`Option may only be specified once: ${value}.`, usage);
|
|
291
|
+
}
|
|
292
|
+
const optionValue = args[index + 1];
|
|
293
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
294
|
+
throw usageError(`${value} is required.`, usage);
|
|
295
|
+
}
|
|
296
|
+
if (repeatable.has(value))
|
|
297
|
+
many.set(value, [...(many.get(value) ?? []), optionValue]);
|
|
298
|
+
else
|
|
299
|
+
one.set(value, optionValue);
|
|
300
|
+
index += 1;
|
|
301
|
+
}
|
|
302
|
+
return { positionals, many, one };
|
|
303
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
import { isRoleRunStalled, latestStallProgressAt } from "../scheduler/roleRunStall.js";
|
|
3
|
+
import { formatTimestamp } from "../output/timePresentation.js";
|
|
4
|
+
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
5
|
+
import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
|
|
6
|
+
export function parseTaskListOptions(args) {
|
|
7
|
+
const allowed = new Set(["--all", "--verbose"]);
|
|
8
|
+
if (args.some((argument) => !allowed.has(argument))
|
|
9
|
+
|| new Set(args).size !== args.length) {
|
|
10
|
+
throw usageError("Task list usage: yui task list [--all] [--verbose].");
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
all: args.includes("--all"),
|
|
14
|
+
verbose: args.includes("--verbose")
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function buildTaskOverview(store, options) {
|
|
18
|
+
const tasks = store.listTasks()
|
|
19
|
+
.filter((task) => options.all || task.status !== "archived")
|
|
20
|
+
.map((task) => buildTaskOverviewEntry(task, store));
|
|
21
|
+
return { tasks };
|
|
22
|
+
}
|
|
23
|
+
export function renderTaskOverview(result, options, timeZone, width = defaultTableWidth()) {
|
|
24
|
+
if (result.tasks.length === 0)
|
|
25
|
+
return "No tasks found.\n";
|
|
26
|
+
const title = options.all ? "Tasks (all)" : "Tasks (unarchived)";
|
|
27
|
+
const output = renderTable(title, [
|
|
28
|
+
{ header: "Task", minWidth: 8, maxWidth: 20 },
|
|
29
|
+
{ header: "Title", minWidth: 10, maxWidth: 46 },
|
|
30
|
+
{ header: "Lifecycle", minWidth: 9, maxWidth: 14 },
|
|
31
|
+
{ header: "Leader", minWidth: 12, maxWidth: 48 },
|
|
32
|
+
{ header: "Summary updated", minWidth: 16, maxWidth: 28 },
|
|
33
|
+
{ header: "Work", minWidth: 8, maxWidth: 38 },
|
|
34
|
+
{ header: "Blockers", minWidth: 9, maxWidth: 42 },
|
|
35
|
+
{ header: "Execution", minWidth: 14, maxWidth: 34 },
|
|
36
|
+
{ header: "Next action / owner", minWidth: 16, maxWidth: 42 }
|
|
37
|
+
], result.tasks.map((task) => [
|
|
38
|
+
task.id,
|
|
39
|
+
task.title,
|
|
40
|
+
task.status,
|
|
41
|
+
leaderCell(task),
|
|
42
|
+
task.summaryUpdatedAt === null
|
|
43
|
+
? "missing"
|
|
44
|
+
: formatTimestamp(task.summaryUpdatedAt, timeZone),
|
|
45
|
+
workCell(task.work.counts),
|
|
46
|
+
blockersCell(task.blockers),
|
|
47
|
+
`${task.execution.status} / ${task.execution.owner}`,
|
|
48
|
+
nextCell(task.next)
|
|
49
|
+
]), width);
|
|
50
|
+
if (!options.verbose)
|
|
51
|
+
return `${output}\n`;
|
|
52
|
+
return `${output}\n\n${renderVerboseDetails(result.tasks, timeZone)}\n`;
|
|
53
|
+
}
|
|
54
|
+
function buildTaskOverviewEntry(task, store) {
|
|
55
|
+
const brief = store.getTaskBrief(task.id);
|
|
56
|
+
const leaderRole = store.getRole(task.id, "leader");
|
|
57
|
+
const workItems = store.listWorkItems(task.id);
|
|
58
|
+
const inputRequests = store.listInputRequests(task.id);
|
|
59
|
+
const openInputRequests = inputRequests.filter((request) => request.status === "open");
|
|
60
|
+
const agentRuns = store.listAgentRuns(task.id);
|
|
61
|
+
const events = store.listEvents(task.id);
|
|
62
|
+
const attention = collectAttention(task, agentRuns, events, store.getLeaderFailure(task.id), store.getOperatorNotification(task.id));
|
|
63
|
+
const blockers = collectBlockers(workItems, openInputRequests, attention);
|
|
64
|
+
const next = deriveNextAction(task, brief, workItems, openInputRequests, blockers, store.getPendingWakeup(task.id));
|
|
65
|
+
const leader = {
|
|
66
|
+
role: "leader",
|
|
67
|
+
roleStatus: leaderRole?.status ?? "missing",
|
|
68
|
+
summary: brief?.leaderSummary ?? null,
|
|
69
|
+
currentFocus: brief?.currentFocus ?? null,
|
|
70
|
+
updatedAt: brief?.updatedAt ?? null,
|
|
71
|
+
updatedBy: brief?.updatedBy ?? null,
|
|
72
|
+
summaryStatus: brief === null ? "missing" : "available"
|
|
73
|
+
};
|
|
74
|
+
const counts = countWorkItems(workItems);
|
|
75
|
+
const runtimeRuns = agentRuns
|
|
76
|
+
.filter((run) => run.status === "active")
|
|
77
|
+
.map((run) => ({
|
|
78
|
+
id: run.id,
|
|
79
|
+
roleName: run.roleName,
|
|
80
|
+
status: run.status,
|
|
81
|
+
delivery: run.deliveredAt === undefined ? "pending" : "delivered",
|
|
82
|
+
deliveredAt: run.deliveredAt ?? null,
|
|
83
|
+
createdAt: run.createdAt,
|
|
84
|
+
updatedAt: run.updatedAt
|
|
85
|
+
}));
|
|
86
|
+
const runtime = {
|
|
87
|
+
activeRuns: runtimeRuns,
|
|
88
|
+
activeRunCount: runtimeRuns.length,
|
|
89
|
+
pendingDeliveryCount: runtimeRuns.filter((run) => run.delivery === "pending").length
|
|
90
|
+
};
|
|
91
|
+
const execution = buildTaskExecutionProjection(store, task.id, task);
|
|
92
|
+
if (execution === null) {
|
|
93
|
+
throw new Error(`Task execution projection disappeared: ${task.id}.`);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
...task,
|
|
97
|
+
brief,
|
|
98
|
+
leader,
|
|
99
|
+
leaderSummary: leader.summary,
|
|
100
|
+
currentFocus: leader.currentFocus,
|
|
101
|
+
summaryUpdatedAt: leader.updatedAt,
|
|
102
|
+
summaryUpdatedBy: leader.updatedBy,
|
|
103
|
+
summaryStatus: leader.summaryStatus,
|
|
104
|
+
work: { counts, items: workItems },
|
|
105
|
+
input: { open: openInputRequests, openCount: openInputRequests.length },
|
|
106
|
+
attention,
|
|
107
|
+
attentionCount: attention.length,
|
|
108
|
+
blockers,
|
|
109
|
+
next,
|
|
110
|
+
nextAction: next?.action ?? null,
|
|
111
|
+
nextOwner: next?.owner ?? null,
|
|
112
|
+
runtime,
|
|
113
|
+
execution
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function collectAttention(task, runs, events, failure, notification) {
|
|
117
|
+
const items = [];
|
|
118
|
+
if (failure !== null || notification?.type === "leader-recovery-failed") {
|
|
119
|
+
items.push({
|
|
120
|
+
kind: "leader-recovery",
|
|
121
|
+
id: `leader-recovery:${task.id}`,
|
|
122
|
+
status: "failed",
|
|
123
|
+
owner: "operator",
|
|
124
|
+
summary: failure?.message
|
|
125
|
+
?? (notification?.type === "leader-recovery-failed"
|
|
126
|
+
? notification.message
|
|
127
|
+
: "Leader recovery failed."),
|
|
128
|
+
updatedAt: failure?.lastFailedAt
|
|
129
|
+
?? (notification?.type === "leader-recovery-failed"
|
|
130
|
+
? notification.updatedAt
|
|
131
|
+
: task.updatedAt)
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const notificationStall = notification?.type === "leader-stalled"
|
|
135
|
+
? notification
|
|
136
|
+
: null;
|
|
137
|
+
if (notificationStall !== null) {
|
|
138
|
+
items.push({
|
|
139
|
+
kind: "leader-stalled",
|
|
140
|
+
id: `leader-stall:${notificationStall.runId}:${notificationStall.progressAt}`,
|
|
141
|
+
status: "needs-attention",
|
|
142
|
+
owner: "operator",
|
|
143
|
+
summary: notificationStall.message,
|
|
144
|
+
updatedAt: notificationStall.updatedAt,
|
|
145
|
+
runId: notificationStall.runId,
|
|
146
|
+
roleName: "leader"
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
for (const run of runs) {
|
|
150
|
+
if (run.roleName !== "leader"
|
|
151
|
+
|| run.status !== "active"
|
|
152
|
+
|| !isRoleRunStalled(events, run.id))
|
|
153
|
+
continue;
|
|
154
|
+
if (notificationStall?.runId === run.id)
|
|
155
|
+
continue;
|
|
156
|
+
const progressAt = latestStallProgressAt(events, run.id) ?? run.updatedAt;
|
|
157
|
+
items.push({
|
|
158
|
+
kind: "leader-stalled",
|
|
159
|
+
id: `leader-stall:${run.id}:${progressAt}`,
|
|
160
|
+
status: "needs-attention",
|
|
161
|
+
owner: "operator",
|
|
162
|
+
summary: `Run ${run.id} for ${run.roleName} has no durable progress after ${progressAt}.`,
|
|
163
|
+
updatedAt: progressAt,
|
|
164
|
+
runId: run.id,
|
|
165
|
+
roleName: run.roleName
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return items.sort((left, right) => (Date.parse(left.updatedAt) - Date.parse(right.updatedAt)
|
|
169
|
+
|| left.id.localeCompare(right.id)));
|
|
170
|
+
}
|
|
171
|
+
function collectBlockers(workItems, openInputRequests, attention) {
|
|
172
|
+
const blockers = [];
|
|
173
|
+
const workById = new Map(workItems.map((item) => [item.id, item]));
|
|
174
|
+
for (const item of workItems) {
|
|
175
|
+
if (item.status === "failed") {
|
|
176
|
+
blockers.push({
|
|
177
|
+
kind: "work",
|
|
178
|
+
type: "work",
|
|
179
|
+
id: item.id,
|
|
180
|
+
status: item.status,
|
|
181
|
+
owner: item.assignee ?? "leader",
|
|
182
|
+
action: "review-failed-work",
|
|
183
|
+
summary: item.outcome ?? `Work Item ${item.id} failed.`
|
|
184
|
+
});
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (item.status === "awaiting_acceptance") {
|
|
188
|
+
blockers.push({
|
|
189
|
+
kind: "work",
|
|
190
|
+
type: "work",
|
|
191
|
+
id: item.id,
|
|
192
|
+
status: item.status,
|
|
193
|
+
owner: "leader",
|
|
194
|
+
action: "accept-work-item",
|
|
195
|
+
summary: `Work Item ${item.id} is awaiting Leader acceptance.`
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (item.status !== "pending")
|
|
200
|
+
continue;
|
|
201
|
+
const dependencies = item.dependsOn.filter((dependency) => (workById.get(dependency)?.status !== "completed"));
|
|
202
|
+
if (dependencies.length === 0)
|
|
203
|
+
continue;
|
|
204
|
+
blockers.push({
|
|
205
|
+
kind: "work",
|
|
206
|
+
type: "work",
|
|
207
|
+
id: item.id,
|
|
208
|
+
status: item.status,
|
|
209
|
+
owner: item.assignee ?? "leader",
|
|
210
|
+
action: "unblock-work-item",
|
|
211
|
+
summary: `Work Item ${item.id} is waiting on ${dependencies.join(", ")}.`
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
for (const request of openInputRequests) {
|
|
215
|
+
blockers.push({
|
|
216
|
+
kind: "input",
|
|
217
|
+
type: "input",
|
|
218
|
+
id: request.id,
|
|
219
|
+
status: request.status,
|
|
220
|
+
owner: "user",
|
|
221
|
+
action: "answer-input",
|
|
222
|
+
summary: request.question,
|
|
223
|
+
blockedRefs: request.blockedRefs
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
for (const item of attention) {
|
|
227
|
+
blockers.push({
|
|
228
|
+
kind: "attention",
|
|
229
|
+
type: "attention",
|
|
230
|
+
id: item.id,
|
|
231
|
+
status: item.status,
|
|
232
|
+
owner: item.owner,
|
|
233
|
+
action: item.kind === "leader-recovery"
|
|
234
|
+
? "inspect-leader-recovery"
|
|
235
|
+
: "inspect-leader-stall",
|
|
236
|
+
summary: item.summary
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return blockers;
|
|
240
|
+
}
|
|
241
|
+
function deriveNextAction(task, brief, workItems, openInputRequests, blockers, pendingWakeup) {
|
|
242
|
+
const input = openInputRequests[0];
|
|
243
|
+
if (input !== undefined) {
|
|
244
|
+
return {
|
|
245
|
+
action: "answer-input",
|
|
246
|
+
owner: "user",
|
|
247
|
+
kind: "input",
|
|
248
|
+
id: input.id,
|
|
249
|
+
summary: input.question
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const attention = blockers.find((blocker) => blocker.kind === "attention");
|
|
253
|
+
if (attention !== undefined) {
|
|
254
|
+
return {
|
|
255
|
+
action: attention.action,
|
|
256
|
+
owner: attention.owner,
|
|
257
|
+
kind: "attention",
|
|
258
|
+
id: attention.id,
|
|
259
|
+
summary: attention.summary
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const workBlocker = blockers.find((blocker) => blocker.kind === "work");
|
|
263
|
+
if (workBlocker !== undefined) {
|
|
264
|
+
return {
|
|
265
|
+
action: workBlocker.action,
|
|
266
|
+
owner: workBlocker.owner,
|
|
267
|
+
kind: "work",
|
|
268
|
+
id: workBlocker.id,
|
|
269
|
+
summary: workBlocker.summary
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (pendingWakeup !== null) {
|
|
273
|
+
return {
|
|
274
|
+
action: "process-leader-wakeup",
|
|
275
|
+
owner: "leader",
|
|
276
|
+
kind: "wakeup",
|
|
277
|
+
summary: pendingWakeup.reasons.join(", ")
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const pending = workItems.find((item) => item.status === "pending");
|
|
281
|
+
if (pending !== undefined) {
|
|
282
|
+
return {
|
|
283
|
+
action: "start-work-item",
|
|
284
|
+
owner: pending.assignee ?? "leader",
|
|
285
|
+
kind: "work",
|
|
286
|
+
id: pending.id,
|
|
287
|
+
summary: pending.title
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (task.status === "active" && brief === null) {
|
|
291
|
+
return {
|
|
292
|
+
action: "update-leader-summary",
|
|
293
|
+
owner: "leader",
|
|
294
|
+
kind: "summary",
|
|
295
|
+
summary: "No persisted Task Brief is available."
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
function countWorkItems(items) {
|
|
301
|
+
const counts = {
|
|
302
|
+
total: items.length,
|
|
303
|
+
pending: 0,
|
|
304
|
+
running: 0,
|
|
305
|
+
awaiting_acceptance: 0,
|
|
306
|
+
completed: 0,
|
|
307
|
+
failed: 0,
|
|
308
|
+
retired: 0
|
|
309
|
+
};
|
|
310
|
+
for (const item of items)
|
|
311
|
+
counts[item.status] += 1;
|
|
312
|
+
return counts;
|
|
313
|
+
}
|
|
314
|
+
function leaderCell(task) {
|
|
315
|
+
const status = task.leader.roleStatus;
|
|
316
|
+
if (task.leader.summaryStatus === "missing")
|
|
317
|
+
return `${status} · missing summary`;
|
|
318
|
+
const values = [task.leader.summary, task.leader.currentFocus]
|
|
319
|
+
.filter((value) => value !== null && value.length > 0);
|
|
320
|
+
return values.length === 0
|
|
321
|
+
? `${status} · summary unavailable`
|
|
322
|
+
: compactText(`${status} · ${values.join(" · ")}`);
|
|
323
|
+
}
|
|
324
|
+
function workCell(counts) {
|
|
325
|
+
if (counts.total === 0)
|
|
326
|
+
return "none";
|
|
327
|
+
const details = Object.keys(counts)
|
|
328
|
+
.filter((status) => status !== "total" && counts[status] > 0)
|
|
329
|
+
.map((status) => `${status}:${counts[status]}`);
|
|
330
|
+
return `${counts.total} total${details.length === 0 ? "" : ` (${details.join(", ")})`}`;
|
|
331
|
+
}
|
|
332
|
+
function blockersCell(blockers) {
|
|
333
|
+
if (blockers.length === 0)
|
|
334
|
+
return "none";
|
|
335
|
+
return blockers.map((blocker) => `${blocker.kind}:${blocker.id}`).join(", ");
|
|
336
|
+
}
|
|
337
|
+
function nextCell(next) {
|
|
338
|
+
return next === null ? "—" : `${next.action} / ${next.owner}`;
|
|
339
|
+
}
|
|
340
|
+
function compactText(value) {
|
|
341
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
342
|
+
return oneLine.length <= 240 ? oneLine : `${oneLine.slice(0, 237)}...`;
|
|
343
|
+
}
|
|
344
|
+
function renderVerboseDetails(tasks, timeZone) {
|
|
345
|
+
return [
|
|
346
|
+
"Task details",
|
|
347
|
+
...tasks.flatMap((task) => [
|
|
348
|
+
`${task.id}: ${task.title}`,
|
|
349
|
+
` Description: ${task.description ?? "—"}`,
|
|
350
|
+
` Current focus: ${task.currentFocus ?? "missing"}`,
|
|
351
|
+
` Leader summary: ${task.leaderSummary ?? "missing"}`,
|
|
352
|
+
` Summary updated by: ${task.summaryUpdatedBy ?? "unknown"}`,
|
|
353
|
+
` Summary updated at: ${task.summaryUpdatedAt === null
|
|
354
|
+
? "missing"
|
|
355
|
+
: formatTimestamp(task.summaryUpdatedAt, timeZone)}`,
|
|
356
|
+
` Execution: ${task.execution.status} (${task.execution.owner}); ${task.execution.summary}`,
|
|
357
|
+
` Monitoring: ${task.execution.monitoring}; attention: ${task.execution.attention.length}`,
|
|
358
|
+
` Projects: ${task.projectBindings.length === 0
|
|
359
|
+
? "none"
|
|
360
|
+
: task.projectBindings.map(({ directory, projectId, baseRef }) => (`${directory} (${projectId} @ ${baseRef})`)).join(", ")}`
|
|
361
|
+
])
|
|
362
|
+
].join("\n");
|
|
363
|
+
}
|