@zq-silk/yui 0.0.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 +141 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/agent/adapterCatalog.js +10 -0
- package/dist/agent/agent.js +89 -0
- package/dist/agent/agentRegistry.js +10 -0
- package/dist/agent/argumentPolicy.js +80 -0
- package/dist/brief/taskBrief.js +37 -0
- package/dist/cli/commandCatalog.js +647 -0
- package/dist/cli/completion.js +111 -0
- package/dist/cli/completionWizard.js +143 -0
- package/dist/cli/dynamicCompletion.js +48 -0
- package/dist/cli/helpRenderer.js +32 -0
- package/dist/cli/interactionCandidates.js +139 -0
- package/dist/cli/interactionPolicy.js +389 -0
- package/dist/cli/interactiveSelection.js +185 -0
- package/dist/cli/invocationRouter.js +51 -0
- package/dist/cli/roleOptionCatalog.js +67 -0
- package/dist/cli/roleWizard.js +546 -0
- package/dist/cli/selectionPorts.js +1 -0
- package/dist/cli/updateCommand.js +22 -0
- package/dist/cli.js +402 -0
- package/dist/commands/agentCommands.js +196 -0
- package/dist/commands/globalRoleCommands.js +367 -0
- package/dist/commands/jobCommands.js +100 -0
- package/dist/commands/operatorCommands.js +38 -0
- package/dist/commands/repositoryCommands.js +86 -0
- package/dist/commands/roleConfiguration.js +201 -0
- package/dist/commands/taskCommands.js +1344 -0
- package/dist/commands/taskContextCommand.js +215 -0
- package/dist/commands/taskInputCommands.js +423 -0
- package/dist/commands/taskRoleRuntimeStatus.js +152 -0
- package/dist/completion/completionInstaller.js +168 -0
- package/dist/completion/completionPort.js +1 -0
- package/dist/completion/completionState.js +137 -0
- package/dist/completion/completionWizard.js +125 -0
- package/dist/completion/fileCompletionManager.js +51 -0
- package/dist/config/yuiConfig.js +17 -0
- package/dist/context/dispatchContext.js +74 -0
- package/dist/controller/clientRuntime.js +215 -0
- package/dist/controller/controller.js +158 -0
- package/dist/controller/controllerMain.js +37 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +322 -0
- package/dist/controller/runtime.js +31 -0
- package/dist/controller/sessionNotify.js +136 -0
- package/dist/core/controllerClient.js +127 -0
- package/dist/core/controllerServer.js +269 -0
- package/dist/core/protocol.js +169 -0
- package/dist/decision/decision.js +42 -0
- package/dist/doctor/doctor.js +229 -0
- package/dist/errors/cliError.js +38 -0
- package/dist/event/taskEvent.js +44 -0
- package/dist/executor/agentAdapter.js +338 -0
- package/dist/executor/agentExecutor.js +144 -0
- package/dist/executor/executorRegistry.js +101 -0
- package/dist/executor/fileRoleLaunchPlanner.js +156 -0
- package/dist/executor/launchPlan.js +16 -0
- package/dist/input/inputRequest.js +326 -0
- package/dist/message/message.js +69 -0
- package/dist/milestone/milestone.js +27 -0
- package/dist/operator/operatorContext.js +66 -0
- package/dist/output/rolePresentation.js +82 -0
- package/dist/output/table.js +77 -0
- package/dist/output/terminal.js +198 -0
- package/dist/repository/gitWorkspace.js +210 -0
- package/dist/repository/repository.js +55 -0
- package/dist/repository/taskWorkspacePreparer.js +256 -0
- package/dist/role/role.js +246 -0
- package/dist/role/systemRoles.js +20 -0
- package/dist/run/agentRun.js +102 -0
- package/dist/scheduler/activeRoleRunDelivery.js +94 -0
- package/dist/scheduler/archivedTaskRuntime.js +12 -0
- package/dist/scheduler/leaderFailure.js +18 -0
- package/dist/scheduler/leaderWakeupProcessor.js +143 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +85 -0
- package/dist/scheduler/operatorNotification.js +17 -0
- package/dist/scheduler/pendingWakeup.js +33 -0
- package/dist/scheduler/ports.js +1 -0
- package/dist/scheduler/roleRunLiveness.js +41 -0
- package/dist/scheduler/wakeupQueue.js +13 -0
- package/dist/setup/setupCommand.js +317 -0
- package/dist/storage/durableFile.js +38 -0
- package/dist/storage/storageSchema.js +259 -0
- package/dist/storage/taskStore.js +1032 -0
- package/dist/task/task.js +216 -0
- package/dist/tmux/commandExecutor.js +69 -0
- package/dist/tmux/terminalHandoff.js +17 -0
- package/dist/tmux/tmuxManager.js +408 -0
- package/dist/workItem/workItem.js +45 -0
- package/dist/worktree/roleWorkspace.js +62 -0
- package/i18n/README.zh-CN.md +205 -0
- package/package.json +47 -0
- package/skills/yui-leader/SKILL.md +72 -0
- package/skills/yui-operator/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +31 -0
|
@@ -0,0 +1,1344 @@
|
|
|
1
|
+
import { compileDispatchInput } from "../context/dispatchContext.js";
|
|
2
|
+
import { dataError, roleNotFound, runtimeError, taskNotFound, usageError } from "../errors/cliError.js";
|
|
3
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
4
|
+
import { createRoleSessionSet, roleAgentSessionResumeMode, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
|
|
5
|
+
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
6
|
+
import { renderRoleDetails } from "../output/rolePresentation.js";
|
|
7
|
+
import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js";
|
|
8
|
+
import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
|
|
9
|
+
import { createAgentRun, failAgentRun, yieldAgentRun } from "../run/agentRun.js";
|
|
10
|
+
import { createTaskBrief, updateTaskBrief } from "../brief/taskBrief.js";
|
|
11
|
+
import { createDecision, supersedeDecision } from "../decision/decision.js";
|
|
12
|
+
import { createMilestone } from "../milestone/milestone.js";
|
|
13
|
+
import { queueLeaderWakeup, queueLeaderWakeupAfterYield } from "../scheduler/wakeupQueue.js";
|
|
14
|
+
import { activateTask, archiveTask, completeTask, createTask, reopenTask, updateTaskMetadata } from "../task/task.js";
|
|
15
|
+
import { createWorkItem, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
16
|
+
import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
|
|
17
|
+
import { runTaskContextCommand } from "./taskContextCommand.js";
|
|
18
|
+
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
|
|
19
|
+
import { assertNoOpenInputRequests, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
20
|
+
const LEADER_ROLE = "leader";
|
|
21
|
+
export function runTaskCommand(args, store, options = {}) {
|
|
22
|
+
const [command, ...rest] = args;
|
|
23
|
+
switch (command) {
|
|
24
|
+
case "create": return output(createTaskCommand(rest, store, options));
|
|
25
|
+
case "update": return output(updateTaskCommand(rest, store, options));
|
|
26
|
+
case "list": return listTaskCommand(rest, store);
|
|
27
|
+
case "show": return showTaskCommand(rest, store);
|
|
28
|
+
case "context": return runTaskContextCommand(rest, store);
|
|
29
|
+
case "activate": return output(activateTaskCommand(rest, store, options));
|
|
30
|
+
case "complete": return output(completeTaskCommand(rest, store, options));
|
|
31
|
+
case "reopen": return output(reopenTaskCommand(rest, store, options));
|
|
32
|
+
case "archive": return output(archiveTaskCommand(rest, store, options));
|
|
33
|
+
case "reconcile": return output(reconcileTaskCommand(rest, store, options));
|
|
34
|
+
case "message": return output(taskMessageCommand(rest, store, options));
|
|
35
|
+
case "input": return runTaskInputCommand(rest, store, options);
|
|
36
|
+
case "role": return taskRoleCommand(rest, store, options);
|
|
37
|
+
case "work": return output(taskWorkCommand(rest, store, options));
|
|
38
|
+
case "run": return output(taskRunCommand(rest, store, options));
|
|
39
|
+
case "brief": return taskBriefCommand(rest, store, options);
|
|
40
|
+
case "decision": return taskDecisionCommand(rest, store, options);
|
|
41
|
+
case "milestone": return taskMilestoneCommand(rest, store, options);
|
|
42
|
+
case "event": return taskEventCommand(rest, store);
|
|
43
|
+
case "enter": return enterTaskRoleAlias(rest, store, options);
|
|
44
|
+
default:
|
|
45
|
+
throw usageError(command === undefined
|
|
46
|
+
? "Task command is required."
|
|
47
|
+
: `Unknown command: task ${command}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function updateTaskCommand(args, store, options) {
|
|
51
|
+
const optionNames = new Set(["--title", "--description", "--priority", "--tags", "--due-at"]);
|
|
52
|
+
const clearOptions = new Set([
|
|
53
|
+
"--clear-description", "--clear-priority", "--clear-tags", "--clear-due-at"
|
|
54
|
+
]);
|
|
55
|
+
const usage = "Task update usage: yui task update <id> [--title <text>] [--description <text>|--clear-description] [--priority <low|medium|high|urgent>|--clear-priority] [--tags <comma-separated>|--clear-tags] [--due-at <RFC3339>|--clear-due-at].";
|
|
56
|
+
const parsed = parseTail(args, optionNames, usage, clearOptions);
|
|
57
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
58
|
+
if (parsed.options.size === 0)
|
|
59
|
+
throw usageError("At least one Task metadata option is required.", usage);
|
|
60
|
+
for (const [setOption, clearOption] of [
|
|
61
|
+
["--description", "--clear-description"],
|
|
62
|
+
["--priority", "--clear-priority"],
|
|
63
|
+
["--tags", "--clear-tags"],
|
|
64
|
+
["--due-at", "--clear-due-at"]
|
|
65
|
+
]) {
|
|
66
|
+
if (parsed.options.has(setOption) && parsed.options.has(clearOption)) {
|
|
67
|
+
throw usageError(`${setOption} and ${clearOption} cannot be used together.`, usage);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const priority = parsed.options.has("--priority")
|
|
71
|
+
? parseTaskPriority(requiredOption(parsed.options, "--priority"))
|
|
72
|
+
: undefined;
|
|
73
|
+
const dueAt = parsed.options.has("--due-at")
|
|
74
|
+
? parseIsoTimestamp(requiredOption(parsed.options, "--due-at"), "--due-at")
|
|
75
|
+
: undefined;
|
|
76
|
+
const tags = parsed.options.has("--tags")
|
|
77
|
+
? parseTaskTags(requiredOption(parsed.options, "--tags"))
|
|
78
|
+
: undefined;
|
|
79
|
+
const now = clock(options);
|
|
80
|
+
const task = store.transaction((tx) => {
|
|
81
|
+
const current = requireTask(tx, parsed.positionals[0]);
|
|
82
|
+
if (current.status === "archived")
|
|
83
|
+
throw usageError(`Task is archived: ${current.id}.`);
|
|
84
|
+
const updated = updateTaskMetadata(current, {
|
|
85
|
+
...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
|
|
86
|
+
...(parsed.options.has("--description")
|
|
87
|
+
? { description: requiredOption(parsed.options, "--description") }
|
|
88
|
+
: parsed.options.has("--clear-description") ? { description: null } : {}),
|
|
89
|
+
...(priority === undefined
|
|
90
|
+
? parsed.options.has("--clear-priority") ? { priority: null } : {}
|
|
91
|
+
: { priority }),
|
|
92
|
+
...(tags === undefined
|
|
93
|
+
? parsed.options.has("--clear-tags") ? { tags: null } : {}
|
|
94
|
+
: { tags }),
|
|
95
|
+
...(dueAt === undefined
|
|
96
|
+
? parsed.options.has("--clear-due-at") ? { dueAt: null } : {}
|
|
97
|
+
: { dueAt })
|
|
98
|
+
}, now);
|
|
99
|
+
tx.saveTask(updated);
|
|
100
|
+
recordTaskEvent(tx, updated.id, "task.updated", { status: updated.status }, now);
|
|
101
|
+
return updated;
|
|
102
|
+
});
|
|
103
|
+
options.runtime?.notifyStateChanged(task.id);
|
|
104
|
+
return `Updated task ${task.id}\n`;
|
|
105
|
+
}
|
|
106
|
+
/** Compatibility helper for call sites that cannot yet handle foreground enter. */
|
|
107
|
+
export function runTaskOutputCommand(args, store, options = {}) {
|
|
108
|
+
const execution = runTaskCommand(args, store, options);
|
|
109
|
+
if (execution.kind === "enter") {
|
|
110
|
+
throw runtimeError("Task role enter requires foreground tmux handoff by the CLI.");
|
|
111
|
+
}
|
|
112
|
+
return execution.output;
|
|
113
|
+
}
|
|
114
|
+
export function submitOperatorMessage(body, taskId, store, options = {}) {
|
|
115
|
+
const now = clock(options);
|
|
116
|
+
const result = store.transaction((tx) => {
|
|
117
|
+
if (taskId !== undefined) {
|
|
118
|
+
const task = requireTask(tx, taskId);
|
|
119
|
+
assertTaskOpen(task);
|
|
120
|
+
const message = appendMessage(tx, task.id, body, "operator", { type: "operator" }, now);
|
|
121
|
+
if (task.status === "active") {
|
|
122
|
+
queueLeaderWakeup(tx, task.id, "operator-input", now);
|
|
123
|
+
}
|
|
124
|
+
return { task, message, created: false };
|
|
125
|
+
}
|
|
126
|
+
const created = createTaskAggregate(tx, titleFrom(body), {}, now);
|
|
127
|
+
const message = appendMessage(tx, created.task.id, body, "operator", { type: "operator" }, now);
|
|
128
|
+
return { ...created, message, created: true };
|
|
129
|
+
});
|
|
130
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
131
|
+
return result.created
|
|
132
|
+
? `Created Draft task ${result.task.id}: ${result.task.title}\nSubmitted message ${result.message.id}\n`
|
|
133
|
+
: `Submitted message ${result.message.id} to ${result.task.id}\n`;
|
|
134
|
+
}
|
|
135
|
+
function createTaskCommand(args, store, options) {
|
|
136
|
+
const usage = "Task create usage: yui task create <title> [--repository <id>] [--base <ref>].";
|
|
137
|
+
const parsed = parseTail(args, new Set(["--repository", "--base"]), usage);
|
|
138
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
139
|
+
const repositoryId = optionalNonEmptyOption(parsed.options, "--repository");
|
|
140
|
+
const baseRef = optionalNonEmptyOption(parsed.options, "--base");
|
|
141
|
+
if (baseRef !== undefined && repositoryId === undefined) {
|
|
142
|
+
throw usageError("--base requires --repository.");
|
|
143
|
+
}
|
|
144
|
+
if (repositoryId !== undefined && store.getRepository(repositoryId) === null) {
|
|
145
|
+
throw usageError(`Repository not found: ${repositoryId}.`);
|
|
146
|
+
}
|
|
147
|
+
const now = clock(options);
|
|
148
|
+
const created = store.transaction((tx) => createTaskAggregate(tx, parsed.positionals[0], {
|
|
149
|
+
...(repositoryId === undefined ? {} : { repositoryId }),
|
|
150
|
+
...(baseRef === undefined ? {} : { baseRef })
|
|
151
|
+
}, now));
|
|
152
|
+
options.runtime?.notifyStateChanged(created.task.id);
|
|
153
|
+
return `Created Draft task ${created.task.id}: ${created.task.title}\nAssigned role: ${created.leader.name}\n`;
|
|
154
|
+
}
|
|
155
|
+
function createTaskAggregate(store, title, metadata, now) {
|
|
156
|
+
const task = createTask(store.nextTaskId(), title, now, metadata);
|
|
157
|
+
const leader = createTaskRole(store, task, LEADER_ROLE, undefined, now);
|
|
158
|
+
store.saveTask(task);
|
|
159
|
+
store.saveRole(task.id, leader);
|
|
160
|
+
recordTaskEvent(store, task.id, "task.created", { status: task.status }, now);
|
|
161
|
+
return { task, leader };
|
|
162
|
+
}
|
|
163
|
+
function listTaskCommand(args, store) {
|
|
164
|
+
assertNoArguments(args, "Task list usage: yui task list.");
|
|
165
|
+
const tasks = store.listTasks();
|
|
166
|
+
const rendered = tasks.length === 0
|
|
167
|
+
? "No tasks found.\n"
|
|
168
|
+
: `${renderTable("Tasks", [
|
|
169
|
+
{ header: "Task", minWidth: 6, maxWidth: 20 },
|
|
170
|
+
{ header: "Status", minWidth: 6, maxWidth: 10 },
|
|
171
|
+
{ header: "Title", minWidth: 8, maxWidth: 64 },
|
|
172
|
+
{ header: "Repository", minWidth: 10, maxWidth: 24 }
|
|
173
|
+
], tasks.map((task) => [task.id, task.status, task.title, task.repositoryId ?? "-"]), defaultTableWidth())}\n`;
|
|
174
|
+
return output(rendered, { tasks });
|
|
175
|
+
}
|
|
176
|
+
function showTaskCommand(args, store) {
|
|
177
|
+
const [taskId] = args;
|
|
178
|
+
exactPositionals(args, 1, "Task show usage: yui task show <id>.");
|
|
179
|
+
const task = requireTask(store, taskId);
|
|
180
|
+
const roles = store.listRoles(task.id);
|
|
181
|
+
const messages = store.listMessages(task.id);
|
|
182
|
+
const brief = store.getTaskBrief(task.id);
|
|
183
|
+
const decisions = store.listDecisions(task.id);
|
|
184
|
+
const milestones = store.listMilestones(task.id);
|
|
185
|
+
const events = store.listEvents(task.id);
|
|
186
|
+
const openInputs = openInputRequestCount(store, task.id);
|
|
187
|
+
const work = store.listWorkItems(task.id);
|
|
188
|
+
const runs = store.listAgentRuns(task.id);
|
|
189
|
+
const counts = {
|
|
190
|
+
roles: roles.length,
|
|
191
|
+
messages: messages.length,
|
|
192
|
+
decisions: decisions.length,
|
|
193
|
+
milestones: milestones.length,
|
|
194
|
+
events: events.length,
|
|
195
|
+
workItems: work.length,
|
|
196
|
+
runs: runs.length,
|
|
197
|
+
openInputs
|
|
198
|
+
};
|
|
199
|
+
const rendered = [
|
|
200
|
+
`Task: ${task.id}`,
|
|
201
|
+
`Title: ${task.title}`,
|
|
202
|
+
`Status: ${task.status}`,
|
|
203
|
+
...(task.description === undefined ? [] : [`Description: ${task.description}`]),
|
|
204
|
+
...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
|
|
205
|
+
...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
|
|
206
|
+
...(task.dueAt === undefined ? [] : [`Due: ${task.dueAt}`]),
|
|
207
|
+
...(task.completedAt === undefined ? [] : [`Completed: ${task.completedAt}`]),
|
|
208
|
+
...(task.completedBy === undefined ? [] : [`Completed by: ${task.completedBy}`]),
|
|
209
|
+
...(task.completionSummary === undefined ? [] : [`Completion summary: ${task.completionSummary}`]),
|
|
210
|
+
...(task.repositoryId === undefined ? [] : [`Repository: ${task.repositoryId}`]),
|
|
211
|
+
...(task.baseRef === undefined ? [] : [`Base: ${task.baseRef}`]),
|
|
212
|
+
...(task.cwd === undefined ? [] : [`Workspace: ${task.cwd}`]),
|
|
213
|
+
`Roles: ${counts.roles}`,
|
|
214
|
+
`Messages: ${counts.messages}`,
|
|
215
|
+
`Brief: ${brief === null ? "no" : "yes"}`,
|
|
216
|
+
`Decisions: ${counts.decisions}`,
|
|
217
|
+
`Milestones: ${counts.milestones}`,
|
|
218
|
+
`Events: ${counts.events}`,
|
|
219
|
+
`Work items: ${counts.workItems}`,
|
|
220
|
+
`Runs: ${counts.runs}`,
|
|
221
|
+
`Open inputs: ${counts.openInputs}`,
|
|
222
|
+
`Created: ${task.createdAt}`,
|
|
223
|
+
`Updated: ${task.updatedAt}`
|
|
224
|
+
].join("\n").concat("\n");
|
|
225
|
+
return output(rendered, { task, counts, hasBrief: brief !== null });
|
|
226
|
+
}
|
|
227
|
+
function activateTaskCommand(args, store, options) {
|
|
228
|
+
exactPositionals(args, 1, "Task activate usage: yui task activate <task>.");
|
|
229
|
+
const now = clock(options);
|
|
230
|
+
const result = store.transaction((tx) => {
|
|
231
|
+
const task = requireTask(tx, args[0]);
|
|
232
|
+
if (task.status === "archived")
|
|
233
|
+
throw usageError(`Task is archived: ${task.id}.`);
|
|
234
|
+
if (task.status === "completed") {
|
|
235
|
+
throw usageError(`Task ${task.id} is completed; use task reopen before activating it.`);
|
|
236
|
+
}
|
|
237
|
+
if (task.status === "active")
|
|
238
|
+
return { task, changed: false };
|
|
239
|
+
const active = activateTask(task, now);
|
|
240
|
+
tx.saveTask(active);
|
|
241
|
+
// Repository-backed Tasks keep this durable wake pending until the
|
|
242
|
+
// Controller has prepared and recorded the Task workspace.
|
|
243
|
+
queueLeaderWakeup(tx, task.id, "task-created", now);
|
|
244
|
+
recordTaskEvent(tx, task.id, "task.activated", {
|
|
245
|
+
fromStatus: task.status,
|
|
246
|
+
status: active.status
|
|
247
|
+
}, now);
|
|
248
|
+
return { task: active, changed: true };
|
|
249
|
+
});
|
|
250
|
+
if (result.changed)
|
|
251
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
252
|
+
return result.changed
|
|
253
|
+
? `Activated task ${result.task.id}\n`
|
|
254
|
+
: `Task ${result.task.id} is already active\n`;
|
|
255
|
+
}
|
|
256
|
+
function completeTaskCommand(args, store, options) {
|
|
257
|
+
const usage = "Task complete usage: yui task complete <id> --summary <text>.";
|
|
258
|
+
const parsed = parseTail(args, new Set(["--summary"]), usage);
|
|
259
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
260
|
+
const summary = requiredOption(parsed.options, "--summary");
|
|
261
|
+
const now = clock(options);
|
|
262
|
+
const result = store.transaction((tx) => {
|
|
263
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
264
|
+
const actor = taskActor(options, task.id);
|
|
265
|
+
if (task.status === "completed")
|
|
266
|
+
return { task, changed: false };
|
|
267
|
+
if (task.status === "archived")
|
|
268
|
+
throw usageError(`Task is archived: ${task.id}.`);
|
|
269
|
+
if (task.status !== "active")
|
|
270
|
+
throw usageError(`Task is not active: ${task.id}.`);
|
|
271
|
+
assertNoOpenInputRequests(tx, task.id, "completing the Task");
|
|
272
|
+
const roles = tx.listRoles(task.id);
|
|
273
|
+
const activeRuns = roles
|
|
274
|
+
.map((role) => ({ role, run: tx.getActiveAgentRun(task.id, role.name) }))
|
|
275
|
+
.filter((entry) => entry.run !== null);
|
|
276
|
+
const workerRun = activeRuns.find(({ role }) => role.name !== LEADER_ROLE);
|
|
277
|
+
if (workerRun !== undefined) {
|
|
278
|
+
throw usageError(`Task ${task.id} has an active run for Role ${workerRun.role.name}.`);
|
|
279
|
+
}
|
|
280
|
+
const runningWork = tx.listWorkItems(task.id).find((item) => item.status === "running");
|
|
281
|
+
if (runningWork !== undefined) {
|
|
282
|
+
throw usageError(`Task ${task.id} has running work: ${runningWork.id}.`);
|
|
283
|
+
}
|
|
284
|
+
const leaderEntry = activeRuns.find(({ role }) => role.name === LEADER_ROLE);
|
|
285
|
+
if (leaderEntry !== undefined) {
|
|
286
|
+
if (actor !== "leader") {
|
|
287
|
+
throw usageError(`Task ${task.id} has an active Leader run.`);
|
|
288
|
+
}
|
|
289
|
+
if (leaderEntry.run.workItemId !== undefined) {
|
|
290
|
+
throw usageError(`Task ${task.id} has running work: ${leaderEntry.run.workItemId}.`);
|
|
291
|
+
}
|
|
292
|
+
if (leaderEntry.run.deliveredAt === undefined) {
|
|
293
|
+
throw usageError(`Task ${task.id} Leader delivery is still pending.`);
|
|
294
|
+
}
|
|
295
|
+
tx.saveAgentRun(yieldAgentRun(leaderEntry.run, summary, now));
|
|
296
|
+
tx.clearActiveAgentRun(task.id, LEADER_ROLE);
|
|
297
|
+
tx.saveRole(task.id, updateRoleStatus(leaderEntry.role, "idle", now));
|
|
298
|
+
const sessions = tx.getTaskRoleSessionSet(task.id, LEADER_ROLE);
|
|
299
|
+
if (sessions?.sessions[leaderEntry.role.activeAgentId]?.status === "running") {
|
|
300
|
+
tx.saveTaskRoleSessionSet(updateRoleAgentSessionStatus(sessions, leaderEntry.role.activeAgentId, "ready", now));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const completed = completeTask(task, now, { by: actor, summary });
|
|
304
|
+
tx.saveTask(completed);
|
|
305
|
+
tx.clearPendingWakeup(task.id);
|
|
306
|
+
tx.clearLeaderFailure(task.id);
|
|
307
|
+
tx.clearOperatorNotification(task.id);
|
|
308
|
+
recordTaskEvent(tx, task.id, "task.completed", { by: actor, summary }, now);
|
|
309
|
+
return { task: completed, changed: true };
|
|
310
|
+
});
|
|
311
|
+
if (result.changed)
|
|
312
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
313
|
+
return result.changed
|
|
314
|
+
? `Completed task ${result.task.id}\n`
|
|
315
|
+
: `Task ${result.task.id} is already completed\n`;
|
|
316
|
+
}
|
|
317
|
+
function reopenTaskCommand(args, store, options) {
|
|
318
|
+
exactPositionals(args, 1, "Task reopen usage: yui task reopen <id>.");
|
|
319
|
+
const now = clock(options);
|
|
320
|
+
const result = store.transaction((tx) => {
|
|
321
|
+
const task = requireTask(tx, args[0]);
|
|
322
|
+
if (task.status === "active")
|
|
323
|
+
return { task, changed: false };
|
|
324
|
+
if (task.status === "archived")
|
|
325
|
+
throw usageError(`Task is archived: ${task.id}.`);
|
|
326
|
+
if (task.status !== "completed")
|
|
327
|
+
throw usageError(`Task is not completed: ${task.id}.`);
|
|
328
|
+
const active = reopenTask(task, now);
|
|
329
|
+
tx.saveTask(active);
|
|
330
|
+
queueLeaderWakeup(tx, task.id, "task-reopened", now);
|
|
331
|
+
recordTaskEvent(tx, task.id, "task.reopened", { status: active.status }, now);
|
|
332
|
+
return { task: active, changed: true };
|
|
333
|
+
});
|
|
334
|
+
if (result.changed)
|
|
335
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
336
|
+
return result.changed
|
|
337
|
+
? `Reopened task ${result.task.id}\n`
|
|
338
|
+
: `Task ${result.task.id} is already active\n`;
|
|
339
|
+
}
|
|
340
|
+
function archiveTaskCommand(args, store, options) {
|
|
341
|
+
exactPositionals(args, 1, "Task archive usage: yui task archive <id>.");
|
|
342
|
+
const now = clock(options);
|
|
343
|
+
const result = store.transaction((tx) => {
|
|
344
|
+
const task = requireTask(tx, args[0]);
|
|
345
|
+
const actor = taskActor(options, task.id);
|
|
346
|
+
if (task.status === "archived")
|
|
347
|
+
return { task, changed: false };
|
|
348
|
+
assertNoOpenInputRequests(tx, task.id, "archiving the Task");
|
|
349
|
+
const archived = archiveTask(task, now, { by: actor });
|
|
350
|
+
tx.saveTask(archived);
|
|
351
|
+
tx.clearPendingWakeup(task.id);
|
|
352
|
+
tx.clearLeaderFailure(task.id);
|
|
353
|
+
tx.clearOperatorNotification(task.id);
|
|
354
|
+
for (const role of tx.listRoles(task.id)) {
|
|
355
|
+
const activeRun = tx.getActiveAgentRun(task.id, role.name);
|
|
356
|
+
if (activeRun === null)
|
|
357
|
+
continue;
|
|
358
|
+
const failed = failAgentRun(activeRun, "Task archived.", now);
|
|
359
|
+
tx.saveAgentRun(failed);
|
|
360
|
+
tx.clearActiveAgentRun(task.id, role.name);
|
|
361
|
+
tx.saveRole(task.id, updateRoleStatus(role, "idle", now));
|
|
362
|
+
if (failed.workItemId !== undefined) {
|
|
363
|
+
const item = tx.getWorkItem(task.id, failed.workItemId);
|
|
364
|
+
if (item !== null && item.status === "running") {
|
|
365
|
+
tx.saveWorkItem(task.id, updateWorkItemStatus(item, "failed", "Task archived.", now));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
recordTaskEvent(tx, task.id, "task.archived", { by: actor }, now);
|
|
370
|
+
return { task: archived, changed: true };
|
|
371
|
+
});
|
|
372
|
+
if (result.changed)
|
|
373
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
374
|
+
return result.changed
|
|
375
|
+
? `Archived task ${result.task.id}\n`
|
|
376
|
+
: `Task ${result.task.id} is already archived\n`;
|
|
377
|
+
}
|
|
378
|
+
function reconcileTaskCommand(args, store, options) {
|
|
379
|
+
exactPositionals(args, 1, "Task reconcile usage: yui task reconcile <task>.");
|
|
380
|
+
const task = requireTask(store, args[0]);
|
|
381
|
+
const runtime = requireRuntime(options);
|
|
382
|
+
runtime.reconcileTask(task.id);
|
|
383
|
+
return `Reconcile requested for task ${task.id}\n`;
|
|
384
|
+
}
|
|
385
|
+
function taskMessageCommand(args, store, options) {
|
|
386
|
+
const [command, ...rest] = args;
|
|
387
|
+
if (command === "send") {
|
|
388
|
+
exactPositionals(rest, 2, "Task message send usage: yui task message send <id> <body>.");
|
|
389
|
+
const now = clock(options);
|
|
390
|
+
const result = store.transaction((tx) => {
|
|
391
|
+
const task = requireTask(tx, rest[0]);
|
|
392
|
+
assertTaskOpen(task);
|
|
393
|
+
const message = appendMessage(tx, task.id, rest[1], "user", { type: "user" }, now);
|
|
394
|
+
if (task.status === "active")
|
|
395
|
+
queueLeaderWakeup(tx, task.id, "user-message", now);
|
|
396
|
+
return { task, message };
|
|
397
|
+
});
|
|
398
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
399
|
+
return `Sent message ${result.message.id} to ${result.task.id}\n`;
|
|
400
|
+
}
|
|
401
|
+
if (command === "list") {
|
|
402
|
+
exactPositionals(rest, 1, "Task message list usage: yui task message list <id>.");
|
|
403
|
+
const task = requireTask(store, rest[0]);
|
|
404
|
+
const messages = store.listMessages(task.id);
|
|
405
|
+
if (messages.length === 0)
|
|
406
|
+
return "No messages found.\n";
|
|
407
|
+
return `${renderTable(`Task messages: ${task.id}`, [
|
|
408
|
+
{ header: "Message", minWidth: 7, maxWidth: 18 },
|
|
409
|
+
{ header: "Author", minWidth: 6, maxWidth: 18 },
|
|
410
|
+
{ header: "Created", minWidth: 10, maxWidth: 28 },
|
|
411
|
+
{ header: "Body", minWidth: 8, maxWidth: 72 }
|
|
412
|
+
], messages.map((message) => [
|
|
413
|
+
message.id,
|
|
414
|
+
taskMessageAuthorLabel(message.author),
|
|
415
|
+
message.createdAt,
|
|
416
|
+
message.body
|
|
417
|
+
]), defaultTableWidth())}\n`;
|
|
418
|
+
}
|
|
419
|
+
throw usageError(command === undefined
|
|
420
|
+
? "Task message command is required."
|
|
421
|
+
: `Unknown command: task message ${command}`);
|
|
422
|
+
}
|
|
423
|
+
function taskRoleCommand(args, store, options) {
|
|
424
|
+
const [command, ...rest] = args;
|
|
425
|
+
if (command === "add")
|
|
426
|
+
return output(addTaskRole(rest, store, options));
|
|
427
|
+
if (command === "list")
|
|
428
|
+
return listTaskRoles(rest, store, options);
|
|
429
|
+
if (command === "status")
|
|
430
|
+
return taskRoleStatus(rest, store, options);
|
|
431
|
+
if (command === "show")
|
|
432
|
+
return output(showTaskRole(rest, store));
|
|
433
|
+
if (command === "update")
|
|
434
|
+
return output(updateTaskRole(rest, store, options));
|
|
435
|
+
if (command === "remove")
|
|
436
|
+
return output(removeTaskRole(rest, store, options));
|
|
437
|
+
if (command === "bind")
|
|
438
|
+
return output(bindTaskRole(rest, store, options));
|
|
439
|
+
if (command === "enter")
|
|
440
|
+
return enterTaskRole(rest, store, options);
|
|
441
|
+
throw usageError(command === undefined
|
|
442
|
+
? "Task role command is required."
|
|
443
|
+
: `Unknown command: task role ${command}`);
|
|
444
|
+
}
|
|
445
|
+
function addTaskRole(args, store, options) {
|
|
446
|
+
const usage = "Task role add usage: yui task role add <task> <name> [Role and Agent settings].";
|
|
447
|
+
const [taskId, roleName, ...tail] = args;
|
|
448
|
+
if (taskId === undefined || roleName === undefined || taskId.startsWith("--") || roleName.startsWith("--")) {
|
|
449
|
+
throw usageError("Task id and Role name are required.", usage);
|
|
450
|
+
}
|
|
451
|
+
const parsed = parseRoleOptions(tail, roleOptionSpecs({ update: false, includeAgent: true }), usage);
|
|
452
|
+
const agentId = parsed.one("--agent")?.trim();
|
|
453
|
+
if (parsed.has("--agent") && (agentId === undefined || agentId.length === 0)) {
|
|
454
|
+
throw usageError("--agent is required.", usage);
|
|
455
|
+
}
|
|
456
|
+
const now = clock(options);
|
|
457
|
+
const role = store.transaction((tx) => {
|
|
458
|
+
const task = requireTask(tx, taskId);
|
|
459
|
+
assertTaskOpen(task);
|
|
460
|
+
if (roleName === LEADER_ROLE)
|
|
461
|
+
throw usageError("The Task leader role already exists.");
|
|
462
|
+
if (tx.getRole(task.id, roleName) !== null)
|
|
463
|
+
throw usageError(`Role already exists: ${roleName}.`);
|
|
464
|
+
let created = createTaskRole(tx, task, roleName, agentId, now);
|
|
465
|
+
const profile = roleProfilePatch(parsed);
|
|
466
|
+
if (Object.keys(profile).length > 0)
|
|
467
|
+
created = updateRole(created, profile, now);
|
|
468
|
+
if (hasAgentConfigOptions(parsed)) {
|
|
469
|
+
const targetAgentId = agentId || created.activeAgentId;
|
|
470
|
+
const binding = created.agentBindings[targetAgentId];
|
|
471
|
+
if (binding === undefined)
|
|
472
|
+
throw usageError(`Role Agent is not bound: ${targetAgentId}.`);
|
|
473
|
+
created = updateRole(created, {
|
|
474
|
+
agentBindings: {
|
|
475
|
+
...created.agentBindings,
|
|
476
|
+
[targetAgentId]: patchRoleAgentBinding(binding, parsed)
|
|
477
|
+
}
|
|
478
|
+
}, now);
|
|
479
|
+
}
|
|
480
|
+
tx.saveRole(task.id, created);
|
|
481
|
+
return created;
|
|
482
|
+
});
|
|
483
|
+
options.runtime?.notifyStateChanged(role.taskId);
|
|
484
|
+
return `Added role ${role.name} to ${role.taskId} (Agent: ${role.activeAgentId})\n`;
|
|
485
|
+
}
|
|
486
|
+
function listTaskRoles(args, store, options) {
|
|
487
|
+
exactPositionals(args, 1, "Task role list usage: yui task role list <task>.");
|
|
488
|
+
const task = requireTask(store, args[0]);
|
|
489
|
+
const roles = store.listRoles(task.id);
|
|
490
|
+
const statuses = inspectTaskRoleRuntimeStatuses(task.id, roles, store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? []);
|
|
491
|
+
if (statuses.length === 0)
|
|
492
|
+
return output("No roles assigned.\n", { roles: statuses });
|
|
493
|
+
return output(`${renderTable(`Task roles: ${task.id}`, [
|
|
494
|
+
{ header: "Role", minWidth: 4, maxWidth: 24 },
|
|
495
|
+
{ header: "Agent", minWidth: 5, maxWidth: 20 },
|
|
496
|
+
{ header: "Health", minWidth: 6, maxWidth: 15 },
|
|
497
|
+
{ header: "Open input", minWidth: 5, maxWidth: 10 },
|
|
498
|
+
{ header: "Active work", minWidth: 10, maxWidth: 34 },
|
|
499
|
+
{ header: "Native session", minWidth: 10, maxWidth: 28 },
|
|
500
|
+
{ header: "tmux", minWidth: 6, maxWidth: 22 }
|
|
501
|
+
], statuses.map((status) => [
|
|
502
|
+
status.roleName,
|
|
503
|
+
status.agentId,
|
|
504
|
+
status.health,
|
|
505
|
+
taskRoleOpenInputLabel(status),
|
|
506
|
+
taskRoleActiveWorkLabel(status),
|
|
507
|
+
taskRoleNativeSessionLabel(status),
|
|
508
|
+
taskRoleTmuxLabel(status)
|
|
509
|
+
]), defaultTableWidth())}\n`, { roles: statuses });
|
|
510
|
+
}
|
|
511
|
+
function taskRoleStatus(args, store, options) {
|
|
512
|
+
exactPositionals(args, 2, "Task role status usage: yui task role status <task> <role>.");
|
|
513
|
+
const task = requireTask(store, args[0]);
|
|
514
|
+
const role = requireRole(store, task.id, args[1]);
|
|
515
|
+
const [status] = inspectTaskRoleRuntimeStatuses(task.id, [role], store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? []);
|
|
516
|
+
if (status === undefined)
|
|
517
|
+
throw roleNotFound(role.name);
|
|
518
|
+
return output(renderTaskRoleRuntimeStatus(status), { role: status });
|
|
519
|
+
}
|
|
520
|
+
function showTaskRole(args, store) {
|
|
521
|
+
exactPositionals(args, 2, "Task role show usage: yui task role show <task> <role>.");
|
|
522
|
+
const task = requireTask(store, args[0]);
|
|
523
|
+
const role = requireRole(store, task.id, args[1]);
|
|
524
|
+
return renderRoleDetails(`Task Role: ${role.name}`, role, {
|
|
525
|
+
kind: "task",
|
|
526
|
+
sessions: store.getTaskRoleSessionSet(task.id, role.name)
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function updateTaskRole(args, store, options) {
|
|
530
|
+
const usage = "Task role update usage: yui task role update <task> <role> [Role and Agent settings].";
|
|
531
|
+
const [taskId, roleName, ...tail] = args;
|
|
532
|
+
if (taskId === undefined || roleName === undefined || taskId.startsWith("--") || roleName.startsWith("--")) {
|
|
533
|
+
throw usageError("Task id and Role name are required.", usage);
|
|
534
|
+
}
|
|
535
|
+
const parsed = parseRoleOptions(tail, roleOptionSpecs({ update: true, includeAgent: true }), usage);
|
|
536
|
+
if (parsed.has("--agent") && (parsed.one("--agent")?.trim().length ?? 0) === 0) {
|
|
537
|
+
throw usageError("--agent is required.", usage);
|
|
538
|
+
}
|
|
539
|
+
if ([...parsed.seen].every((option) => option === "--agent")) {
|
|
540
|
+
throw usageError("At least one role update option is required.", usage);
|
|
541
|
+
}
|
|
542
|
+
const now = clock(options);
|
|
543
|
+
const updated = store.transaction((tx) => {
|
|
544
|
+
const task = requireTask(tx, taskId);
|
|
545
|
+
assertTaskOpen(task);
|
|
546
|
+
const role = requireRole(tx, task.id, roleName);
|
|
547
|
+
let bindings = role.agentBindings;
|
|
548
|
+
if (hasAgentConfigOptions(parsed)) {
|
|
549
|
+
const agentId = parsed.one("--agent")?.trim() || role.activeAgentId;
|
|
550
|
+
const agent = requireAgent(tx, agentId);
|
|
551
|
+
const binding = bindings[agentId]
|
|
552
|
+
?? createRoleAgentBinding({ id: agent.id, adapterId: agent.adapterId });
|
|
553
|
+
const activeSession = tx.getTaskRoleSessionSet(task.id, role.name)?.sessions[agentId];
|
|
554
|
+
if (agentId === role.activeAgentId && (tx.getActiveAgentRun(task.id, role.name) !== null || activeSession?.status === "running")) {
|
|
555
|
+
throw usageError("Active Agent settings cannot be changed while its Run or native process is running.");
|
|
556
|
+
}
|
|
557
|
+
bindings = { ...bindings, [agentId]: patchRoleAgentBinding(binding, parsed) };
|
|
558
|
+
}
|
|
559
|
+
const next = updateRole(role, {
|
|
560
|
+
...(bindings === role.agentBindings ? {} : { agentBindings: bindings }),
|
|
561
|
+
...roleProfilePatch(parsed)
|
|
562
|
+
}, now);
|
|
563
|
+
tx.saveRole(task.id, next);
|
|
564
|
+
return next;
|
|
565
|
+
});
|
|
566
|
+
options.runtime?.notifyStateChanged(updated.taskId);
|
|
567
|
+
return renderRoleDetails(`Updated Task Role: ${updated.name}`, updated, {
|
|
568
|
+
kind: "task",
|
|
569
|
+
sessions: store.getTaskRoleSessionSet(updated.taskId, updated.name)
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
function removeTaskRole(args, store, options) {
|
|
573
|
+
exactPositionals(args, 2, "Task role remove usage: yui task role remove <task> <role>.");
|
|
574
|
+
const removed = store.transaction((tx) => {
|
|
575
|
+
const task = requireTask(tx, args[0]);
|
|
576
|
+
assertTaskOpen(task);
|
|
577
|
+
const role = requireRole(tx, task.id, args[1]);
|
|
578
|
+
if (role.name === LEADER_ROLE)
|
|
579
|
+
throw usageError("The Task Leader role cannot be removed.");
|
|
580
|
+
if (tx.getActiveAgentRun(task.id, role.name) !== null) {
|
|
581
|
+
throw usageError(`Task Role has an active Run and cannot be removed: ${task.id}/${role.name}.`);
|
|
582
|
+
}
|
|
583
|
+
const session = tx.getTaskRoleSessionSet(task.id, role.name)?.sessions[role.activeAgentId];
|
|
584
|
+
if (session?.status === "running") {
|
|
585
|
+
throw usageError(`Task Role has a running native Agent and cannot be removed: ${task.id}/${role.name}.`);
|
|
586
|
+
}
|
|
587
|
+
if (!tx.removeTaskRole(task.id, role.name))
|
|
588
|
+
throw roleNotFound(role.name);
|
|
589
|
+
return role;
|
|
590
|
+
});
|
|
591
|
+
options.runtime?.notifyStateChanged(removed.taskId);
|
|
592
|
+
return `Removed role ${removed.name} from ${removed.taskId}\n`;
|
|
593
|
+
}
|
|
594
|
+
function bindTaskRole(args, store, options) {
|
|
595
|
+
exactPositionals(args, 3, "Task role bind usage: yui task role bind <task> <role> <agent-id>.");
|
|
596
|
+
const now = clock(options);
|
|
597
|
+
const result = store.transaction((tx) => {
|
|
598
|
+
const task = requireTask(tx, args[0]);
|
|
599
|
+
assertTaskOpen(task);
|
|
600
|
+
const role = requireRole(tx, task.id, args[1]);
|
|
601
|
+
const agent = requireAgent(tx, args[2]);
|
|
602
|
+
const binding = role.agentBindings[agent.id]
|
|
603
|
+
?? createRoleAgentBinding({ id: agent.id, adapterId: agent.adapterId });
|
|
604
|
+
const bound = updateRole(role, {
|
|
605
|
+
agentBindings: { ...role.agentBindings, [agent.id]: binding }
|
|
606
|
+
}, now);
|
|
607
|
+
if (agent.id === role.activeAgentId) {
|
|
608
|
+
tx.saveRole(task.id, bound);
|
|
609
|
+
return { role: bound, mode: "current" };
|
|
610
|
+
}
|
|
611
|
+
const existing = tx.getTaskRoleSessionSet(task.id, role.name)
|
|
612
|
+
?? createRoleSessionSet({ scope: "task", taskId: task.id, roleName: role.name }, role.activeAgentId, now);
|
|
613
|
+
const currentSession = existing.sessions[role.activeAgentId];
|
|
614
|
+
const switched = (() => {
|
|
615
|
+
try {
|
|
616
|
+
return switchActiveRoleAgent(bound, existing, agent.id, {
|
|
617
|
+
activeRun: tx.getActiveAgentRun(task.id, role.name) !== null,
|
|
618
|
+
nativeProcessRunning: currentSession?.status === "running"
|
|
619
|
+
}, now);
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
throw usageError(messageOf(error));
|
|
623
|
+
}
|
|
624
|
+
})();
|
|
625
|
+
tx.saveTaskRoleWithSessionSet(switched.role, switched.sessions);
|
|
626
|
+
return { role: switched.role, mode: switched.mode };
|
|
627
|
+
});
|
|
628
|
+
options.runtime?.notifyStateChanged(result.role.taskId);
|
|
629
|
+
return `Bound ${result.role.taskId}/${result.role.name} to ${result.role.activeAgentId} (${result.mode})\n`;
|
|
630
|
+
}
|
|
631
|
+
function enterTaskRole(args, store, options) {
|
|
632
|
+
exactPositionals(args, 2, "Task role enter usage: yui task role enter <task> <role>.");
|
|
633
|
+
const task = requireTask(store, args[0]);
|
|
634
|
+
if (task.status !== "active") {
|
|
635
|
+
throw usageError(inactiveTaskMessage(task, "entering a role session"));
|
|
636
|
+
}
|
|
637
|
+
const role = requireRole(store, task.id, args[1]);
|
|
638
|
+
requireRuntime(options).prepareTaskRoleEnter({ taskId: task.id, roleName: role.name });
|
|
639
|
+
return {
|
|
640
|
+
kind: "enter",
|
|
641
|
+
taskId: task.id,
|
|
642
|
+
roleName: role.name,
|
|
643
|
+
output: `Prepared role ${role.name} for ${task.id}\n`
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function enterTaskRoleAlias(args, store, options) {
|
|
647
|
+
if (args.length < 1 || args.length > 2 || args.some((value) => value.trim().length === 0)) {
|
|
648
|
+
throw usageError("Task enter usage: yui task enter <task> [role].");
|
|
649
|
+
}
|
|
650
|
+
return enterTaskRole([args[0], args[1] ?? LEADER_ROLE], store, options);
|
|
651
|
+
}
|
|
652
|
+
function taskWorkCommand(args, store, options) {
|
|
653
|
+
const [command, ...rest] = args;
|
|
654
|
+
if (command === "create")
|
|
655
|
+
return createWork(rest, store, options);
|
|
656
|
+
if (command === "list")
|
|
657
|
+
return listWork(rest, store);
|
|
658
|
+
if (command === "update")
|
|
659
|
+
return updateWork(rest, store, options);
|
|
660
|
+
if (command === "dispatch")
|
|
661
|
+
return dispatchWork(rest, store, options);
|
|
662
|
+
throw usageError(command === undefined
|
|
663
|
+
? "Task work command is required."
|
|
664
|
+
: `Unknown command: task work ${command}`);
|
|
665
|
+
}
|
|
666
|
+
function createWork(args, store, options) {
|
|
667
|
+
const usage = "Task work create usage: yui task work create <task> <title> [--role <name>].";
|
|
668
|
+
const parsed = parseTail(args, new Set(["--role"]), usage);
|
|
669
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
670
|
+
const now = clock(options);
|
|
671
|
+
const item = store.transaction((tx) => {
|
|
672
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
673
|
+
assertTaskOpen(task);
|
|
674
|
+
const roleName = parsed.options.get("--role")?.trim() ?? LEADER_ROLE;
|
|
675
|
+
requireRole(tx, task.id, roleName);
|
|
676
|
+
const created = createWorkItem(tx.nextWorkItemId(task.id), task.id, {
|
|
677
|
+
title: parsed.positionals[1],
|
|
678
|
+
assignee: roleName
|
|
679
|
+
}, now);
|
|
680
|
+
tx.saveWorkItem(task.id, created);
|
|
681
|
+
return created;
|
|
682
|
+
});
|
|
683
|
+
options.runtime?.notifyStateChanged(item.taskId);
|
|
684
|
+
return `Created work item ${item.id} for ${item.taskId}\n`;
|
|
685
|
+
}
|
|
686
|
+
function listWork(args, store) {
|
|
687
|
+
exactPositionals(args, 1, "Task work list usage: yui task work list <task>.");
|
|
688
|
+
const task = requireTask(store, args[0]);
|
|
689
|
+
const items = store.listWorkItems(task.id);
|
|
690
|
+
if (items.length === 0)
|
|
691
|
+
return "No work items found.\n";
|
|
692
|
+
return `${renderTable(`Task work: ${task.id}`, [
|
|
693
|
+
{ header: "Work", minWidth: 6, maxWidth: 20 },
|
|
694
|
+
{ header: "Status", minWidth: 6, maxWidth: 12 },
|
|
695
|
+
{ header: "Role", minWidth: 4, maxWidth: 22 },
|
|
696
|
+
{ header: "Title", minWidth: 8, maxWidth: 64 },
|
|
697
|
+
{ header: "Summary", minWidth: 8, maxWidth: 48 }
|
|
698
|
+
], items.map((item) => [item.id, presentWorkStatus(item.status), item.assignee, item.title, item.outcome ?? "-"]), defaultTableWidth())}\n`;
|
|
699
|
+
}
|
|
700
|
+
function updateWork(args, store, options) {
|
|
701
|
+
const usage = "Task work update usage: yui task work update <id> <todo|running|done|failed> [--summary <text>].";
|
|
702
|
+
const parsed = parseTail(args, new Set(["--summary"]), usage);
|
|
703
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
704
|
+
const requested = parsed.positionals[1];
|
|
705
|
+
const status = parseWorkStatus(requested);
|
|
706
|
+
const summary = parsed.options.get("--summary");
|
|
707
|
+
if ((status === "completed" || status === "failed") && trimmed(summary) === undefined) {
|
|
708
|
+
throw usageError(`--summary is required when work becomes ${requested}.`);
|
|
709
|
+
}
|
|
710
|
+
const now = clock(options);
|
|
711
|
+
const item = store.transaction((tx) => {
|
|
712
|
+
const current = requireWorkItem(tx, parsed.positionals[0]);
|
|
713
|
+
const task = requireTask(tx, current.taskId);
|
|
714
|
+
assertTaskOpen(task);
|
|
715
|
+
const active = tx.getActiveAgentRun(task.id, current.assignee);
|
|
716
|
+
if (active?.workItemId === current.id && status !== "running") {
|
|
717
|
+
throw usageError(`Work item ${current.id} has an active run; yield the run instead.`);
|
|
718
|
+
}
|
|
719
|
+
const updated = updateWorkItemStatus(current, status, trimmed(summary), now);
|
|
720
|
+
tx.saveWorkItem(task.id, updated);
|
|
721
|
+
return updated;
|
|
722
|
+
});
|
|
723
|
+
options.runtime?.notifyStateChanged(item.taskId);
|
|
724
|
+
return `Updated work item ${item.id} to ${requested}\n`;
|
|
725
|
+
}
|
|
726
|
+
function dispatchWork(args, store, options) {
|
|
727
|
+
const usage = "Task work dispatch usage: yui task work dispatch <id> [--input <text>].";
|
|
728
|
+
const parsed = parseTail(args, new Set(["--input"]), usage);
|
|
729
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
730
|
+
const now = clock(options);
|
|
731
|
+
const run = store.transaction((tx) => {
|
|
732
|
+
const item = requireWorkItem(tx, parsed.positionals[0]);
|
|
733
|
+
const task = requireTask(tx, item.taskId);
|
|
734
|
+
if (task.status !== "active") {
|
|
735
|
+
throw usageError(inactiveTaskMessage(task, "dispatch"));
|
|
736
|
+
}
|
|
737
|
+
if (item.status !== "pending") {
|
|
738
|
+
throw usageError(`Work item ${item.id} cannot be dispatched from ${item.status}.`);
|
|
739
|
+
}
|
|
740
|
+
const role = requireRole(tx, task.id, item.assignee);
|
|
741
|
+
if (tx.getActiveAgentRun(task.id, role.name) !== null) {
|
|
742
|
+
throw usageError(`${task.id}/${role.name} already has an active run.`);
|
|
743
|
+
}
|
|
744
|
+
const rawInput = trimmed(parsed.options.get("--input")) ?? item.title;
|
|
745
|
+
const input = compileDispatchInput({}, task.id, role, rawInput);
|
|
746
|
+
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
747
|
+
const created = createAgentRun(tx.nextAgentRunId(task.id), task.id, role.name, roleAgentSessionResumeMode(sessions, role.activeAgentId), input, now, { workItemId: item.id });
|
|
748
|
+
tx.saveAgentRun(created);
|
|
749
|
+
tx.saveActiveAgentRun(created);
|
|
750
|
+
tx.saveWorkItem(task.id, updateWorkItemStatus(item, "running", undefined, now));
|
|
751
|
+
tx.saveRole(task.id, updateRoleStatus(role, "running", now));
|
|
752
|
+
return created;
|
|
753
|
+
});
|
|
754
|
+
options.runtime?.notifyStateChanged(run.taskId);
|
|
755
|
+
return `Dispatch queued for ${run.taskId}/${run.roleName} (${run.id})\n`;
|
|
756
|
+
}
|
|
757
|
+
function taskRunCommand(args, store, options) {
|
|
758
|
+
const [command, ...rest] = args;
|
|
759
|
+
if (command === "list")
|
|
760
|
+
return listRuns(rest, store);
|
|
761
|
+
if (command === "retry")
|
|
762
|
+
return retryRun(rest, store, options);
|
|
763
|
+
if (command === "yield")
|
|
764
|
+
return yieldRun(rest, store, options);
|
|
765
|
+
throw usageError(command === undefined
|
|
766
|
+
? "Task run command is required."
|
|
767
|
+
: `Unknown command: task run ${command}`);
|
|
768
|
+
}
|
|
769
|
+
function listRuns(args, store) {
|
|
770
|
+
exactPositionals(args, 1, "Task run list usage: yui task run list <work>.");
|
|
771
|
+
const item = requireWorkItem(store, args[0]);
|
|
772
|
+
const runs = store.listAgentRuns(item.taskId).filter((run) => run.workItemId === item.id);
|
|
773
|
+
if (runs.length === 0)
|
|
774
|
+
return "No runs found.\n";
|
|
775
|
+
return `${renderTable(`Runs: ${item.id}`, [
|
|
776
|
+
{ header: "Run", minWidth: 6, maxWidth: 20 },
|
|
777
|
+
{ header: "Role", minWidth: 4, maxWidth: 22 },
|
|
778
|
+
{ header: "Mode", minWidth: 4, maxWidth: 8 },
|
|
779
|
+
{ header: "Status", minWidth: 6, maxWidth: 12 },
|
|
780
|
+
{ header: "Summary", minWidth: 8, maxWidth: 58 }
|
|
781
|
+
], runs.map((run) => [run.id, run.roleName, run.mode, run.status, run.summary ?? "-"]), defaultTableWidth())}\n`;
|
|
782
|
+
}
|
|
783
|
+
function retryRun(args, store, options) {
|
|
784
|
+
exactPositionals(args, 1, "Task run retry usage: yui task run retry <run>.");
|
|
785
|
+
const now = clock(options);
|
|
786
|
+
const retried = store.transaction((tx) => {
|
|
787
|
+
const previous = requireRun(tx, args[0]);
|
|
788
|
+
if (previous.status !== "failed") {
|
|
789
|
+
throw usageError(`Run ${previous.id} is not retryable from ${previous.status}.`);
|
|
790
|
+
}
|
|
791
|
+
const task = requireTask(tx, previous.taskId);
|
|
792
|
+
if (task.status !== "active")
|
|
793
|
+
throw usageError(`Task is not active: ${task.id}.`);
|
|
794
|
+
const role = requireRole(tx, task.id, previous.roleName);
|
|
795
|
+
if (tx.getActiveAgentRun(task.id, role.name) !== null) {
|
|
796
|
+
throw usageError(`${task.id}/${role.name} already has an active run.`);
|
|
797
|
+
}
|
|
798
|
+
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
799
|
+
const created = createAgentRun(tx.nextAgentRunId(task.id), task.id, role.name, roleAgentSessionResumeMode(sessions, role.activeAgentId), previous.input, now, { workItemId: previous.workItemId });
|
|
800
|
+
tx.saveAgentRun(created);
|
|
801
|
+
tx.saveActiveAgentRun(created);
|
|
802
|
+
tx.saveRole(task.id, updateRoleStatus(role, "running", now));
|
|
803
|
+
if (previous.workItemId !== undefined) {
|
|
804
|
+
const item = tx.getWorkItem(task.id, previous.workItemId);
|
|
805
|
+
if (item === null)
|
|
806
|
+
throw dataError(`Work item not found for run ${previous.id}: ${previous.workItemId}.`);
|
|
807
|
+
tx.saveWorkItem(task.id, updateWorkItemStatus(item, "running", undefined, now));
|
|
808
|
+
}
|
|
809
|
+
return created;
|
|
810
|
+
});
|
|
811
|
+
options.runtime?.notifyStateChanged(retried.taskId);
|
|
812
|
+
return `Retry queued as ${retried.id} for ${retried.taskId}/${retried.roleName}\n`;
|
|
813
|
+
}
|
|
814
|
+
function yieldRun(args, store, options) {
|
|
815
|
+
const usage = "Task run yield usage: yui task run yield <run> --summary <text>.";
|
|
816
|
+
const parsed = parseTail(args, new Set(["--summary"]), usage);
|
|
817
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
818
|
+
const summary = requiredOption(parsed.options, "--summary");
|
|
819
|
+
const now = clock(options);
|
|
820
|
+
const yielded = store.transaction((tx) => {
|
|
821
|
+
const active = requireRun(tx, parsed.positionals[0]);
|
|
822
|
+
if (active.status !== "active") {
|
|
823
|
+
throw usageError(`Run ${active.id} is already terminal: ${active.status}.`);
|
|
824
|
+
}
|
|
825
|
+
if (active.deliveredAt === undefined) {
|
|
826
|
+
throw usageError(`Run ${active.id} delivery is still pending.`);
|
|
827
|
+
}
|
|
828
|
+
const task = requireTask(tx, active.taskId);
|
|
829
|
+
if (task.status !== "active")
|
|
830
|
+
throw usageError(inactiveTaskMessage(task, "yielding a run"));
|
|
831
|
+
const role = requireRole(tx, task.id, active.roleName);
|
|
832
|
+
const pointer = tx.getActiveAgentRun(task.id, role.name);
|
|
833
|
+
if (pointer?.id !== active.id)
|
|
834
|
+
throw usageError(`Run is not active for ${task.id}/${role.name}: ${active.id}.`);
|
|
835
|
+
const terminal = yieldAgentRun(active, summary, now);
|
|
836
|
+
const message = appendMessage(tx, task.id, summary, "role-result", { type: "role", roleName: role.name }, now, { runId: terminal.id, workItemId: active.workItemId });
|
|
837
|
+
tx.saveAgentRun(terminal);
|
|
838
|
+
tx.clearActiveAgentRun(task.id, role.name);
|
|
839
|
+
if (active.workItemId !== undefined) {
|
|
840
|
+
const item = tx.getWorkItem(task.id, active.workItemId);
|
|
841
|
+
if (item === null)
|
|
842
|
+
throw dataError(`Work item not found for run ${active.id}: ${active.workItemId}.`);
|
|
843
|
+
tx.saveWorkItem(task.id, updateWorkItemStatus(item, "completed", summary, now));
|
|
844
|
+
}
|
|
845
|
+
else if (role.name !== LEADER_ROLE) {
|
|
846
|
+
throw usageError(`Run ${active.id} is not a work run.`);
|
|
847
|
+
}
|
|
848
|
+
tx.saveRole(task.id, updateRoleStatus(role, "idle", now));
|
|
849
|
+
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
850
|
+
if (sessions?.sessions[role.activeAgentId]?.status === "running") {
|
|
851
|
+
tx.saveTaskRoleSessionSet(updateRoleAgentSessionStatus(sessions, role.activeAgentId, "ready", now));
|
|
852
|
+
}
|
|
853
|
+
queueLeaderWakeupAfterYield(tx, task, terminal, now);
|
|
854
|
+
return { run: terminal, message };
|
|
855
|
+
});
|
|
856
|
+
// For a Leader yield this also advances a wake that arrived while the Leader
|
|
857
|
+
// was busy; queueLeaderWakeupAfterYield deliberately does not self-wake.
|
|
858
|
+
options.runtime?.notifyStateChanged(yielded.run.taskId);
|
|
859
|
+
return `Yielded ${yielded.run.id}: ${yielded.message.body}\n`;
|
|
860
|
+
}
|
|
861
|
+
function createTaskRole(store, task, roleName, explicitAgentId, now) {
|
|
862
|
+
const workspace = task.cwd ?? store.getConfig().defaultWorkspace ?? process.cwd();
|
|
863
|
+
if (explicitAgentId === undefined) {
|
|
864
|
+
const globalRole = store.getGlobalRole(roleName);
|
|
865
|
+
if (globalRole !== null) {
|
|
866
|
+
const copied = copyGlobalRoleToTaskRole(globalRole, task.id, now, roleName);
|
|
867
|
+
return copied.workspace === workspace ? copied : updateRole(copied, { workspace }, now);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const agentId = explicitAgentId?.trim() || store.getConfig().defaultAgent;
|
|
871
|
+
if (agentId === undefined) {
|
|
872
|
+
throw dataError(`No Agent is configured for Task role: ${roleName}.`);
|
|
873
|
+
}
|
|
874
|
+
const agent = requireAgent(store, agentId);
|
|
875
|
+
const binding = createRoleAgentBinding({ id: agent.id, adapterId: agent.adapterId });
|
|
876
|
+
return createRole(task.id, roleName, [binding], agent.id, workspace, now);
|
|
877
|
+
}
|
|
878
|
+
function appendMessage(store, taskId, body, kind, author, now, context = {}) {
|
|
879
|
+
const message = createTaskMessage(store.nextMessageId(taskId), body, kind, author, now, context);
|
|
880
|
+
store.saveMessage(taskId, message);
|
|
881
|
+
recordTaskEvent(store, taskId, "message.sent", { messageId: message.id, kind: message.kind }, now);
|
|
882
|
+
return message;
|
|
883
|
+
}
|
|
884
|
+
function recordTaskEvent(store, taskId, type, payload, now) {
|
|
885
|
+
store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), type, payload, now));
|
|
886
|
+
}
|
|
887
|
+
function requireTask(store, taskId) {
|
|
888
|
+
const id = requiredText(taskId, "Task id");
|
|
889
|
+
const task = store.getTask(id);
|
|
890
|
+
if (task === null)
|
|
891
|
+
throw taskNotFound(id);
|
|
892
|
+
return task;
|
|
893
|
+
}
|
|
894
|
+
function requireRole(store, taskId, roleName) {
|
|
895
|
+
const name = requiredText(roleName, "Role name");
|
|
896
|
+
const role = store.getRole(taskId, name);
|
|
897
|
+
if (role === null)
|
|
898
|
+
throw roleNotFound(name);
|
|
899
|
+
return role;
|
|
900
|
+
}
|
|
901
|
+
function requireAgent(store, agentId) {
|
|
902
|
+
const id = requiredText(agentId, "Agent id");
|
|
903
|
+
const agent = store.getConfiguredAgent(id);
|
|
904
|
+
if (agent === null)
|
|
905
|
+
throw usageError(`Agent not found: ${id}.`);
|
|
906
|
+
return agent;
|
|
907
|
+
}
|
|
908
|
+
function requireWorkItem(store, workItemId) {
|
|
909
|
+
const id = requiredText(workItemId, "Work item id");
|
|
910
|
+
const item = store.findWorkItem(id);
|
|
911
|
+
if (item === null)
|
|
912
|
+
throw usageError(`Work item not found: ${id}.`);
|
|
913
|
+
return item;
|
|
914
|
+
}
|
|
915
|
+
function requireRun(store, runId) {
|
|
916
|
+
const id = requiredText(runId, "Run id");
|
|
917
|
+
const run = store.findAgentRun(id);
|
|
918
|
+
if (run === null)
|
|
919
|
+
throw usageError(`Run not found: ${id}.`);
|
|
920
|
+
return run;
|
|
921
|
+
}
|
|
922
|
+
function assertTaskOpen(task) {
|
|
923
|
+
if (task.status === "completed") {
|
|
924
|
+
throw usageError(`Task ${task.id} is completed; reopen it before continuing.`);
|
|
925
|
+
}
|
|
926
|
+
if (task.status === "archived")
|
|
927
|
+
throw usageError(`Task is archived: ${task.id}.`);
|
|
928
|
+
}
|
|
929
|
+
function taskActor(options, taskId) {
|
|
930
|
+
const environment = options.environment;
|
|
931
|
+
if (environment?.YUI_SESSION_SCOPE === "task"
|
|
932
|
+
&& environment.YUI_TASK_ID === taskId
|
|
933
|
+
&& environment.YUI_ROLE === "leader") {
|
|
934
|
+
return "leader";
|
|
935
|
+
}
|
|
936
|
+
return environment?.YUI_SESSION_SCOPE === "global"
|
|
937
|
+
&& environment.YUI_ROLE === "operator" ? "operator" : "user";
|
|
938
|
+
}
|
|
939
|
+
function inactiveTaskMessage(task, action) {
|
|
940
|
+
if (task.status === "draft") {
|
|
941
|
+
return `Task ${task.id} is a Draft; activate it before ${action}.`;
|
|
942
|
+
}
|
|
943
|
+
if (task.status === "completed") {
|
|
944
|
+
return `Task ${task.id} is completed; reopen it before ${action}.`;
|
|
945
|
+
}
|
|
946
|
+
return `Task is archived: ${task.id}.`;
|
|
947
|
+
}
|
|
948
|
+
function requireRuntime(options) {
|
|
949
|
+
if (options.runtime === undefined)
|
|
950
|
+
throw runtimeError("Task workflow runtime is not configured.");
|
|
951
|
+
return options.runtime;
|
|
952
|
+
}
|
|
953
|
+
function parseWorkStatus(value) {
|
|
954
|
+
if (value === "todo")
|
|
955
|
+
return "pending";
|
|
956
|
+
if (value === "running")
|
|
957
|
+
return "running";
|
|
958
|
+
if (value === "done")
|
|
959
|
+
return "completed";
|
|
960
|
+
if (value === "failed")
|
|
961
|
+
return "failed";
|
|
962
|
+
throw usageError(`Invalid work item status: ${value}.`);
|
|
963
|
+
}
|
|
964
|
+
function presentWorkStatus(status) {
|
|
965
|
+
if (status === "pending")
|
|
966
|
+
return "todo";
|
|
967
|
+
if (status === "completed")
|
|
968
|
+
return "done";
|
|
969
|
+
return status;
|
|
970
|
+
}
|
|
971
|
+
function parseTaskPriority(value) {
|
|
972
|
+
if (["low", "medium", "high", "urgent"].includes(value))
|
|
973
|
+
return value;
|
|
974
|
+
throw usageError(`Invalid Task priority: ${value}.`);
|
|
975
|
+
}
|
|
976
|
+
function parseTaskTags(value) {
|
|
977
|
+
const tags = [...new Set(value.split(",").map((tag) => tag.trim()).filter(Boolean))];
|
|
978
|
+
if (tags.length === 0)
|
|
979
|
+
throw usageError("--tags must contain at least one tag.");
|
|
980
|
+
return tags;
|
|
981
|
+
}
|
|
982
|
+
function parseIsoTimestamp(value, label) {
|
|
983
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/.test(value)) {
|
|
984
|
+
throw usageError(`${label} must be an ISO/RFC 3339 timestamp with a timezone.`);
|
|
985
|
+
}
|
|
986
|
+
const timestamp = new Date(value);
|
|
987
|
+
if (!Number.isFinite(timestamp.getTime())) {
|
|
988
|
+
throw usageError(`${label} must be an ISO/RFC 3339 timestamp with a timezone.`);
|
|
989
|
+
}
|
|
990
|
+
return timestamp.toISOString();
|
|
991
|
+
}
|
|
992
|
+
function taskBriefCommand(args, store, options) {
|
|
993
|
+
const [command, ...rest] = args;
|
|
994
|
+
if (command === "show") {
|
|
995
|
+
exactPositionals(rest, 1, "Task brief show usage: yui task brief show <task>.");
|
|
996
|
+
const task = requireTask(store, rest[0]);
|
|
997
|
+
const brief = store.getTaskBrief(task.id);
|
|
998
|
+
if (brief === null) {
|
|
999
|
+
return output(`Task ${task.id} has no brief.\n`, { taskId: task.id, brief: null });
|
|
1000
|
+
}
|
|
1001
|
+
return output([
|
|
1002
|
+
`Task: ${task.id}`,
|
|
1003
|
+
`Objective: ${brief.objective}`,
|
|
1004
|
+
`Boundaries:`,
|
|
1005
|
+
...(brief.boundaries.length === 0 ? [" (none)"] : brief.boundaries.map((b) => ` - ${b}`)),
|
|
1006
|
+
`Current focus: ${brief.currentFocus}`,
|
|
1007
|
+
`Leader summary: ${brief.leaderSummary}`,
|
|
1008
|
+
`Updated by: ${brief.updatedBy}`,
|
|
1009
|
+
`Updated at: ${brief.updatedAt}`
|
|
1010
|
+
].join("\n").concat("\n"), { taskId: task.id, brief });
|
|
1011
|
+
}
|
|
1012
|
+
if (command === "update") {
|
|
1013
|
+
const usage = "Task brief update usage: yui task brief update <task> [--objective <text>] [--boundary <text> ...] [--focus <text>] [--leader-summary <text>].";
|
|
1014
|
+
const parsed = parseMultiValueTail(rest, new Set(["--objective", "--focus", "--leader-summary"]), new Set(["--boundary"]), usage);
|
|
1015
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
1016
|
+
const hasObjective = parsed.options.has("--objective");
|
|
1017
|
+
const hasFocus = parsed.options.has("--focus");
|
|
1018
|
+
const hasSummary = parsed.options.has("--leader-summary");
|
|
1019
|
+
const boundaries = parsed.multiOptions.get("--boundary") ?? [];
|
|
1020
|
+
if (!hasObjective && !hasFocus && !hasSummary && boundaries.length === 0) {
|
|
1021
|
+
throw usageError("At least one brief field is required.", usage);
|
|
1022
|
+
}
|
|
1023
|
+
const now = clock(options);
|
|
1024
|
+
const result = store.transaction((tx) => {
|
|
1025
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
1026
|
+
assertTaskOpen(task);
|
|
1027
|
+
const existing = tx.getTaskBrief(task.id);
|
|
1028
|
+
const updatedBy = taskActor(options, task.id);
|
|
1029
|
+
const brief = existing === null
|
|
1030
|
+
? createTaskBrief({
|
|
1031
|
+
objective: requiredText(parsed.options.get("--objective"), "--objective"),
|
|
1032
|
+
boundaries,
|
|
1033
|
+
currentFocus: requiredText(parsed.options.get("--focus"), "--focus"),
|
|
1034
|
+
leaderSummary: requiredText(parsed.options.get("--leader-summary"), "--leader-summary"),
|
|
1035
|
+
updatedBy
|
|
1036
|
+
}, now)
|
|
1037
|
+
: updateTaskBrief(existing, {
|
|
1038
|
+
...(hasObjective ? { objective: parsed.options.get("--objective") } : {}),
|
|
1039
|
+
...(boundaries.length > 0 ? { boundaries } : {}),
|
|
1040
|
+
...(hasFocus ? { currentFocus: parsed.options.get("--focus") } : {}),
|
|
1041
|
+
...(hasSummary ? { leaderSummary: parsed.options.get("--leader-summary") } : {})
|
|
1042
|
+
}, updatedBy, now);
|
|
1043
|
+
tx.saveTaskBrief(task.id, brief);
|
|
1044
|
+
recordTaskEvent(tx, task.id, "brief.updated", { updatedBy }, now);
|
|
1045
|
+
if (task.status === "active" && updatedBy !== "leader") {
|
|
1046
|
+
queueLeaderWakeup(tx, task.id, "brief-updated", now);
|
|
1047
|
+
}
|
|
1048
|
+
return { task, brief };
|
|
1049
|
+
});
|
|
1050
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
1051
|
+
return output(`Updated brief for ${result.task.id}\n`);
|
|
1052
|
+
}
|
|
1053
|
+
throw usageError(command === undefined
|
|
1054
|
+
? "Task brief command is required."
|
|
1055
|
+
: `Unknown command: task brief ${command}`);
|
|
1056
|
+
}
|
|
1057
|
+
function taskDecisionCommand(args, store, options) {
|
|
1058
|
+
const [command, ...rest] = args;
|
|
1059
|
+
if (command === "record") {
|
|
1060
|
+
const usage = "Task decision record usage: yui task decision record <task> --title <text> --rationale <text>.";
|
|
1061
|
+
const parsed = parseTail(rest, new Set(["--title", "--rationale"]), usage);
|
|
1062
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
1063
|
+
const title = requiredOption(parsed.options, "--title");
|
|
1064
|
+
const rationale = requiredOption(parsed.options, "--rationale");
|
|
1065
|
+
const now = clock(options);
|
|
1066
|
+
const result = store.transaction((tx) => {
|
|
1067
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
1068
|
+
assertTaskOpen(task);
|
|
1069
|
+
const actor = taskActor(options, task.id);
|
|
1070
|
+
const decision = createDecision(tx.nextDecisionId(task.id), task.id, title, rationale, now);
|
|
1071
|
+
tx.saveDecision(task.id, decision);
|
|
1072
|
+
recordTaskEvent(tx, task.id, "decision.recorded", { decisionId: decision.id, title }, now);
|
|
1073
|
+
if (task.status === "active" && actor !== "leader") {
|
|
1074
|
+
queueLeaderWakeup(tx, task.id, "decision-recorded", now);
|
|
1075
|
+
}
|
|
1076
|
+
return { task, decision };
|
|
1077
|
+
});
|
|
1078
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
1079
|
+
return output(`Recorded decision ${result.decision.id} for ${result.task.id}\n`);
|
|
1080
|
+
}
|
|
1081
|
+
if (command === "list") {
|
|
1082
|
+
const usage = "Task decision list usage: yui task decision list <task> [--status active|superseded].";
|
|
1083
|
+
const parsed = parseTail(rest, new Set(["--status"]), usage);
|
|
1084
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
1085
|
+
const task = requireTask(store, parsed.positionals[0]);
|
|
1086
|
+
let decisions = store.listDecisions(task.id);
|
|
1087
|
+
const status = parsed.options.get("--status");
|
|
1088
|
+
if (status !== undefined) {
|
|
1089
|
+
if (status !== "active" && status !== "superseded") {
|
|
1090
|
+
throw usageError("--status must be active or superseded.", usage);
|
|
1091
|
+
}
|
|
1092
|
+
decisions = decisions.filter((d) => d.status === status);
|
|
1093
|
+
}
|
|
1094
|
+
if (decisions.length === 0) {
|
|
1095
|
+
return output(`No decisions found for ${task.id}.\n`, { taskId: task.id, decisions: [] });
|
|
1096
|
+
}
|
|
1097
|
+
return output(`${renderTable(`Decisions: ${task.id}`, [
|
|
1098
|
+
{ header: "Decision", minWidth: 8, maxWidth: 18 },
|
|
1099
|
+
{ header: "Status", minWidth: 6, maxWidth: 12 },
|
|
1100
|
+
{ header: "Title", minWidth: 8, maxWidth: 64 },
|
|
1101
|
+
{ header: "Created", minWidth: 10, maxWidth: 28 }
|
|
1102
|
+
], decisions.map((d) => [d.id, d.status, d.title, d.createdAt]), defaultTableWidth())}\n`, { taskId: task.id, decisions });
|
|
1103
|
+
}
|
|
1104
|
+
if (command === "show") {
|
|
1105
|
+
exactPositionals(rest, 2, "Task decision show usage: yui task decision show <task> <decision>.");
|
|
1106
|
+
const task = requireTask(store, rest[0]);
|
|
1107
|
+
const decision = store.getDecision(task.id, rest[1]);
|
|
1108
|
+
if (decision === null)
|
|
1109
|
+
throw dataError(`Decision not found: ${rest[1]}.`);
|
|
1110
|
+
return output([
|
|
1111
|
+
`Decision: ${decision.id}`,
|
|
1112
|
+
`Task: ${task.id}`,
|
|
1113
|
+
`Title: ${decision.title}`,
|
|
1114
|
+
`Rationale: ${decision.rationale}`,
|
|
1115
|
+
`Status: ${decision.status}`,
|
|
1116
|
+
...(decision.supersededReason === undefined ? [] : [`Superseded reason: ${decision.supersededReason}`]),
|
|
1117
|
+
...(decision.supersededAt === undefined ? [] : [`Superseded at: ${decision.supersededAt}`]),
|
|
1118
|
+
`Created: ${decision.createdAt}`,
|
|
1119
|
+
`Updated: ${decision.updatedAt}`
|
|
1120
|
+
].join("\n").concat("\n"), { taskId: task.id, decision });
|
|
1121
|
+
}
|
|
1122
|
+
if (command === "supersede") {
|
|
1123
|
+
const usage = "Task decision supersede usage: yui task decision supersede <task> <decision> --reason <text>.";
|
|
1124
|
+
const parsed = parseTail(rest, new Set(["--reason"]), usage);
|
|
1125
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
1126
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
1127
|
+
const now = clock(options);
|
|
1128
|
+
const result = store.transaction((tx) => {
|
|
1129
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
1130
|
+
assertTaskOpen(task);
|
|
1131
|
+
const actor = taskActor(options, task.id);
|
|
1132
|
+
const existing = tx.getDecision(task.id, parsed.positionals[1]);
|
|
1133
|
+
if (existing === null)
|
|
1134
|
+
throw dataError(`Decision not found: ${parsed.positionals[1]}.`);
|
|
1135
|
+
const decision = supersedeDecision(existing, reason, now);
|
|
1136
|
+
tx.saveDecision(task.id, decision);
|
|
1137
|
+
recordTaskEvent(tx, task.id, "decision.superseded", { decisionId: decision.id, reason }, now);
|
|
1138
|
+
if (task.status === "active" && actor !== "leader") {
|
|
1139
|
+
queueLeaderWakeup(tx, task.id, "decision-superseded", now);
|
|
1140
|
+
}
|
|
1141
|
+
return { task, decision };
|
|
1142
|
+
});
|
|
1143
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
1144
|
+
return output(`Superseded decision ${result.decision.id} for ${result.task.id}\n`);
|
|
1145
|
+
}
|
|
1146
|
+
throw usageError(command === undefined
|
|
1147
|
+
? "Task decision command is required."
|
|
1148
|
+
: `Unknown command: task decision ${command}`);
|
|
1149
|
+
}
|
|
1150
|
+
function taskMilestoneCommand(args, store, options) {
|
|
1151
|
+
const [command, ...rest] = args;
|
|
1152
|
+
if (command === "add") {
|
|
1153
|
+
const usage = "Task milestone add usage: yui task milestone add <task> --title <text> --summary <text>.";
|
|
1154
|
+
const parsed = parseTail(rest, new Set(["--title", "--summary"]), usage);
|
|
1155
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
1156
|
+
const title = requiredOption(parsed.options, "--title");
|
|
1157
|
+
const summary = requiredOption(parsed.options, "--summary");
|
|
1158
|
+
const now = clock(options);
|
|
1159
|
+
const result = store.transaction((tx) => {
|
|
1160
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
1161
|
+
assertTaskOpen(task);
|
|
1162
|
+
if (taskActor(options, task.id) !== "leader") {
|
|
1163
|
+
throw usageError("Only the Task Leader can add a Milestone.");
|
|
1164
|
+
}
|
|
1165
|
+
const milestone = createMilestone(tx.nextMilestoneId(task.id), task.id, title, summary, now);
|
|
1166
|
+
tx.saveMilestone(task.id, milestone);
|
|
1167
|
+
recordTaskEvent(tx, task.id, "milestone.added", { milestoneId: milestone.id, title }, now);
|
|
1168
|
+
return { task, milestone };
|
|
1169
|
+
});
|
|
1170
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
1171
|
+
return output(`Added milestone ${result.milestone.id} for ${result.task.id}\n`);
|
|
1172
|
+
}
|
|
1173
|
+
if (command === "list") {
|
|
1174
|
+
exactPositionals(rest, 1, "Task milestone list usage: yui task milestone list <task>.");
|
|
1175
|
+
const task = requireTask(store, rest[0]);
|
|
1176
|
+
const milestones = store.listMilestones(task.id);
|
|
1177
|
+
if (milestones.length === 0) {
|
|
1178
|
+
return output(`No milestones found for ${task.id}.\n`, { taskId: task.id, milestones: [] });
|
|
1179
|
+
}
|
|
1180
|
+
return output(`${renderTable(`Milestones: ${task.id}`, [
|
|
1181
|
+
{ header: "Milestone", minWidth: 9, maxWidth: 18 },
|
|
1182
|
+
{ header: "Title", minWidth: 8, maxWidth: 64 },
|
|
1183
|
+
{ header: "Created", minWidth: 10, maxWidth: 28 }
|
|
1184
|
+
], milestones.map((m) => [m.id, m.title, m.createdAt]), defaultTableWidth())}\n`, { taskId: task.id, milestones });
|
|
1185
|
+
}
|
|
1186
|
+
if (command === "show") {
|
|
1187
|
+
exactPositionals(rest, 2, "Task milestone show usage: yui task milestone show <task> <milestone>.");
|
|
1188
|
+
const task = requireTask(store, rest[0]);
|
|
1189
|
+
const milestone = store.getMilestone(task.id, rest[1]);
|
|
1190
|
+
if (milestone === null)
|
|
1191
|
+
throw dataError(`Milestone not found: ${rest[1]}.`);
|
|
1192
|
+
return output([
|
|
1193
|
+
`Milestone: ${milestone.id}`,
|
|
1194
|
+
`Task: ${task.id}`,
|
|
1195
|
+
`Title: ${milestone.title}`,
|
|
1196
|
+
`Summary: ${milestone.summary}`,
|
|
1197
|
+
`Created by: ${milestone.createdBy}`,
|
|
1198
|
+
`Created: ${milestone.createdAt}`
|
|
1199
|
+
].join("\n").concat("\n"), { taskId: task.id, milestone });
|
|
1200
|
+
}
|
|
1201
|
+
throw usageError(command === undefined
|
|
1202
|
+
? "Task milestone command is required."
|
|
1203
|
+
: `Unknown command: task milestone ${command}`);
|
|
1204
|
+
}
|
|
1205
|
+
function taskEventCommand(args, store) {
|
|
1206
|
+
const [command, ...rest] = args;
|
|
1207
|
+
if (command === "list") {
|
|
1208
|
+
exactPositionals(rest, 1, "Task event list usage: yui task event list <task>.");
|
|
1209
|
+
const task = requireTask(store, rest[0]);
|
|
1210
|
+
const events = store.listEvents(task.id);
|
|
1211
|
+
if (events.length === 0) {
|
|
1212
|
+
return output(`No events found for ${task.id}.\n`, { taskId: task.id, events: [] });
|
|
1213
|
+
}
|
|
1214
|
+
return output(`${renderTable(`Events: ${task.id}`, [
|
|
1215
|
+
{ header: "Event", minWidth: 8, maxWidth: 18 },
|
|
1216
|
+
{ header: "Type", minWidth: 8, maxWidth: 28 },
|
|
1217
|
+
{ header: "Created", minWidth: 10, maxWidth: 28 }
|
|
1218
|
+
], events.map((e) => [e.id, e.type, e.createdAt]), defaultTableWidth())}\n`, { taskId: task.id, events });
|
|
1219
|
+
}
|
|
1220
|
+
if (command === "show") {
|
|
1221
|
+
exactPositionals(rest, 2, "Task event show usage: yui task event show <task> <event>.");
|
|
1222
|
+
const task = requireTask(store, rest[0]);
|
|
1223
|
+
const events = store.listEvents(task.id);
|
|
1224
|
+
const event = events.find((e) => e.id === rest[1]) ?? null;
|
|
1225
|
+
if (event === null)
|
|
1226
|
+
throw dataError(`Event not found: ${rest[1]}.`);
|
|
1227
|
+
return output([
|
|
1228
|
+
`Event: ${event.id}`,
|
|
1229
|
+
`Task: ${task.id}`,
|
|
1230
|
+
`Type: ${event.type}`,
|
|
1231
|
+
`Created: ${event.createdAt}`,
|
|
1232
|
+
`Payload:`,
|
|
1233
|
+
...(Object.keys(event.payload).length === 0
|
|
1234
|
+
? [" (none)"]
|
|
1235
|
+
: Object.entries(event.payload).map(([k, v]) => ` ${k}: ${v}`))
|
|
1236
|
+
].join("\n").concat("\n"), { taskId: task.id, event });
|
|
1237
|
+
}
|
|
1238
|
+
throw usageError(command === undefined
|
|
1239
|
+
? "Task event command is required."
|
|
1240
|
+
: `Unknown command: task event ${command}`);
|
|
1241
|
+
}
|
|
1242
|
+
function parseMultiValueTail(args, valueOptions, repeatOptions, usage) {
|
|
1243
|
+
const positionals = [];
|
|
1244
|
+
const options = new Map();
|
|
1245
|
+
const multiOptions = new Map();
|
|
1246
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1247
|
+
const value = args[index];
|
|
1248
|
+
if (!value.startsWith("--")) {
|
|
1249
|
+
positionals.push(value);
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
if (!valueOptions.has(value) && !repeatOptions.has(value)) {
|
|
1253
|
+
throw usageError(`Unsupported option: ${value}.`, usage);
|
|
1254
|
+
}
|
|
1255
|
+
if (repeatOptions.has(value)) {
|
|
1256
|
+
const optionValue = args[index + 1];
|
|
1257
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
1258
|
+
throw usageError(`${value} is required.`, usage);
|
|
1259
|
+
}
|
|
1260
|
+
const existing = multiOptions.get(value) ?? [];
|
|
1261
|
+
multiOptions.set(value, [...existing, optionValue]);
|
|
1262
|
+
index += 1;
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
if (options.has(value))
|
|
1266
|
+
throw usageError(`Option may only be specified once: ${value}.`, usage);
|
|
1267
|
+
const optionValue = args[index + 1];
|
|
1268
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
1269
|
+
throw usageError(`${value} is required.`, usage);
|
|
1270
|
+
}
|
|
1271
|
+
options.set(value, optionValue);
|
|
1272
|
+
index += 1;
|
|
1273
|
+
}
|
|
1274
|
+
return { positionals, options, multiOptions };
|
|
1275
|
+
}
|
|
1276
|
+
function parseTail(args, valueOptions, usage, flagOptions = new Set()) {
|
|
1277
|
+
const positionals = [];
|
|
1278
|
+
const options = new Map();
|
|
1279
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1280
|
+
const value = args[index];
|
|
1281
|
+
if (!value.startsWith("--")) {
|
|
1282
|
+
positionals.push(value);
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
if (!valueOptions.has(value) && !flagOptions.has(value)) {
|
|
1286
|
+
throw usageError(`Unsupported option: ${value}.`, usage);
|
|
1287
|
+
}
|
|
1288
|
+
if (options.has(value))
|
|
1289
|
+
throw usageError(`Option may only be specified once: ${value}.`, usage);
|
|
1290
|
+
if (flagOptions.has(value)) {
|
|
1291
|
+
options.set(value, "");
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
const optionValue = args[index + 1];
|
|
1295
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
1296
|
+
throw usageError(`${value} is required.`, usage);
|
|
1297
|
+
}
|
|
1298
|
+
options.set(value, optionValue);
|
|
1299
|
+
index += 1;
|
|
1300
|
+
}
|
|
1301
|
+
return { positionals, options };
|
|
1302
|
+
}
|
|
1303
|
+
function requiredOption(options, name) {
|
|
1304
|
+
return requiredText(options.get(name), name);
|
|
1305
|
+
}
|
|
1306
|
+
function optionalNonEmptyOption(options, name) {
|
|
1307
|
+
if (!options.has(name))
|
|
1308
|
+
return undefined;
|
|
1309
|
+
return requiredText(options.get(name), name);
|
|
1310
|
+
}
|
|
1311
|
+
function exactPositionals(values, count, usage) {
|
|
1312
|
+
if (values.length !== count || values.some((value) => value.trim().length === 0)) {
|
|
1313
|
+
throw usageError(usage);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
function assertNoArguments(args, usage) {
|
|
1317
|
+
if (args.length > 0)
|
|
1318
|
+
throw usageError(usage);
|
|
1319
|
+
}
|
|
1320
|
+
function requiredText(value, label) {
|
|
1321
|
+
const normalized = value?.trim();
|
|
1322
|
+
if (normalized === undefined || normalized.length === 0)
|
|
1323
|
+
throw usageError(`${label} is required.`);
|
|
1324
|
+
return normalized;
|
|
1325
|
+
}
|
|
1326
|
+
function trimmed(value) {
|
|
1327
|
+
const normalized = value?.trim();
|
|
1328
|
+
return normalized === undefined || normalized.length === 0 ? undefined : normalized;
|
|
1329
|
+
}
|
|
1330
|
+
function titleFrom(body) {
|
|
1331
|
+
const oneLine = requiredText(body, "Message body").replace(/\s+/g, " ");
|
|
1332
|
+
return oneLine.length <= 80 ? oneLine : `${oneLine.slice(0, 77)}...`;
|
|
1333
|
+
}
|
|
1334
|
+
function output(value, data) {
|
|
1335
|
+
return data === undefined
|
|
1336
|
+
? { kind: "output", output: value }
|
|
1337
|
+
: { kind: "output", output: value, data };
|
|
1338
|
+
}
|
|
1339
|
+
function clock(options) {
|
|
1340
|
+
return options.now?.() ?? new Date();
|
|
1341
|
+
}
|
|
1342
|
+
function messageOf(error) {
|
|
1343
|
+
return error instanceof Error ? error.message : String(error);
|
|
1344
|
+
}
|