@zq-silk/yui 2.8.1 → 2.10.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/README.md +5 -3
- package/dist/cli/commandCatalog.js +56 -21
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionPolicy.js +2 -2
- package/dist/commands/durableJobCommands.js +50 -0
- package/dist/commands/globalRoleCommands.js +30 -9
- package/dist/commands/inboxCommands.js +79 -22
- package/dist/commands/projectCommands.js +4 -33
- package/dist/commands/scheduleCommands.js +45 -0
- package/dist/commands/taskActivationCommands.js +19 -5
- package/dist/commands/taskCommands.js +8 -2
- package/dist/commands/taskFactCommands.js +24 -12
- package/dist/commands/taskProjectDeliveryCommand.js +56 -0
- package/dist/commands/taskPublicationCommands.js +6 -3
- package/dist/commands/taskPublicationVerifyCommand.js +74 -28
- package/dist/commands/taskRemoteDeliveryCommand.js +5 -3
- package/dist/context/runContextPack.js +25 -4
- package/dist/context/runInputContract.js +1 -0
- package/dist/context/taskContext.js +53 -16
- package/dist/context/taskPhase.js +99 -0
- package/dist/controlPlaneCli.js +38 -17
- package/dist/controller/controller.js +35 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +75 -16
- package/dist/controller/globalInputDelivery.js +17 -7
- package/dist/controller/inboxTurnCheck.js +64 -0
- package/dist/controller/jobControl.js +42 -12
- package/dist/controller/jobDefinitionControl.js +304 -0
- package/dist/controller/jobSupervisor.js +47 -32
- package/dist/controller/providerRetryAdmission.js +2 -0
- package/dist/controller/runtime.js +11 -2
- package/dist/controller/scheduleControl.js +176 -0
- package/dist/coordination/workMailbox.js +3 -3
- package/dist/external/githubPublicationVerifier.js +14 -5
- package/dist/external/gitlabPublicationVerifier.js +12 -29
- package/dist/grant/taskAuthorization.js +7 -2
- package/dist/interaction/notificationPresentation.js +12 -3
- package/dist/interaction/operatorPresentation.js +5 -0
- package/dist/job/builtinJob.js +58 -0
- package/dist/job/durableJob.js +72 -49
- package/dist/job/jobDefinition.js +68 -0
- package/dist/job/jobRunner.js +2 -1
- package/dist/job/projectRefreshRunner.js +48 -0
- package/dist/message/globalInboxRecovery.js +44 -0
- package/dist/message/globalInterrupt.js +1 -1
- package/dist/message/inboxReceipt.js +14 -6
- package/dist/message/inboxTurn.js +86 -0
- package/dist/message/message.js +11 -0
- package/dist/message/taskInboxOwnership.js +20 -6
- package/dist/repository/projectRefresh.js +58 -0
- package/dist/repository/taskWorkspacePreparer.js +2 -1
- package/dist/repository/workspaceCleanupInspection.js +6 -1
- package/dist/schedule/schedule.js +108 -0
- package/dist/scheduler/operatorEvent.js +2 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +8 -0
- package/dist/storage/contextRecords.js +8 -0
- package/dist/storage/jobSchema.js +15 -0
- package/dist/storage/maintenanceCaller.js +1 -1
- package/dist/storage/recordValidation.js +5 -0
- package/dist/storage/scheduleSchema.js +12 -0
- package/dist/storage/sqliteSchema.js +95 -24
- package/dist/storage/sqliteStore.js +123 -10
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +5 -0
- package/dist/storage/upgrade/jobDefinitionMigration.js +22 -0
- package/dist/task/archiveDiagnostics.js +1 -1
- package/dist/task/archivePreflight.js +1 -1
- package/dist/task/projectDeliveryScope.js +20 -0
- package/dist/task/publicationToolObservation.js +126 -0
- package/dist/task/remoteDelivery.js +19 -8
- package/dist/task/remoteDeliveryService.js +5 -4
- package/dist/task/taskActivationService.js +35 -0
- package/dist/web/assets/client/app/settings.js +7 -1
- package/dist/web/assets/client/i18n/en.js +65 -2
- package/dist/web/assets/client/i18n/zh-CN.js +65 -2
- package/dist/web/assets/client/index.js +2 -0
- package/dist/web/assets/client/views/jobs.js +222 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +6 -2
- package/dist/web/assets/client/views/task/page.js +11 -1
- package/dist/web/assets/shared/manageNav.js +1 -0
- package/dist/web/assets/shared/taskTabs.js +2 -2
- package/dist/web/assets/shell/settingsContent.js +3 -0
- package/dist/web/assets/styles/index.js +2 -0
- package/dist/web/assets/styles/views/jobs.js +19 -0
- package/dist/web/controllerWeb.js +73 -21
- package/dist/web/taskPreviews.js +21 -13
- package/dist/web/webAccess.js +109 -0
- package/dist/web/webJobs.js +52 -0
- package/dist/web/webServer.js +76 -44
- package/docs/architecture/capabilities-and-resources.md +6 -4
- package/docs/architecture/capabilities-and-resources.zh-CN.md +5 -4
- package/docs/cli-information-contract.md +57 -4
- package/docs/cli-information-contract.zh-CN.md +41 -2
- package/docs/job-runs.md +224 -0
- package/docs/project-delivery-scope.md +59 -0
- package/docs/release-workflow.md +86 -0
- package/docs/release-workflow.zh-CN.md +68 -0
- package/docs/schedules.md +132 -0
- package/docs/storage-baseline.md +31 -1
- package/docs/storage-baseline.zh-CN.md +25 -1
- package/docs/task-delivery.md +74 -2
- package/docs/task-delivery.zh-CN.md +39 -1
- package/docs/testing/verification-levels.md +6 -2
- package/docs/testing/verification-levels.zh-CN.md +6 -3
- package/i18n/README.zh-CN.md +6 -3
- package/package.json +4 -3
- package/skills/yui-leader/SKILL.md +44 -1
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/planning.md +9 -1
- package/skills/yui-operator/SKILL.md +31 -0
- package/skills/yui-operator/VERSION +1 -1
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-reviewer/VERSION +1 -1
- package/skills/yui-runtime/SKILL.md +67 -8
- package/skills/yui-runtime/VERSION +1 -1
- package/skills/yui-runtime/references/context-ordering.md +103 -0
- package/skills/yui-runtime/references/publication.md +44 -4
- package/skills/yui-runtime/references/recovery.md +21 -0
- package/skills/yui-worker/SKILL.md +5 -0
- package/skills/yui-worker/VERSION +1 -1
|
@@ -5,9 +5,10 @@ import { createTaskWake, latestTaskWake } from "../scheduler/taskWake.js";
|
|
|
5
5
|
import { expandTaskMessageResult } from "../message/message.js";
|
|
6
6
|
import { messageAssignmentBlocker } from "../message/messageContinuation.js";
|
|
7
7
|
import { globalInboxDeliveryState, inboxHandlingState, isGlobalInboxMessage, pendingGlobalInboxMessages } from "../message/inboxReceipt.js";
|
|
8
|
-
import {
|
|
8
|
+
import { nonReplayableMessageWake, isLeaderSteer, pinMessageRevisions, releasedNotificationWake, taskMessageInboxReceipt, unfinishedLeaderInbox } from "../message/taskInboxOwnership.js";
|
|
9
9
|
import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
10
10
|
import { isTaskRecordRetired, retiredTaskRecordIds } from "../task/taskRecordRetirement.js";
|
|
11
|
+
import { blocksInboxOrder, inboxEntryCategory, inboxSettled } from "../message/inboxTurn.js";
|
|
11
12
|
const target = (address) => ({ kind: "role", taskId: address.taskId, roleName: address.roleName });
|
|
12
13
|
const source = (address) => `inbox:${address.scope}:${address.taskId ?? ""}:${address.roleName}`;
|
|
13
14
|
const pendingId = (pending) => `pending-${readDigest(pending)}`;
|
|
@@ -15,15 +16,27 @@ const sameSession = (a, b) => a.scope === b.scope && a.taskId === b.taskId
|
|
|
15
16
|
&& a.roleName === b.roleName && a.agentId === b.agentId && a.nativeSessionId === b.nativeSessionId;
|
|
16
17
|
function addressOf(store, env) {
|
|
17
18
|
const caller = requireManagedCaller(store, env);
|
|
19
|
+
const binding = caller.scope === "global" ? store.getGlobalRoleSessionSet(caller.roleName)?.providerBinding
|
|
20
|
+
: store.getTaskRoleSessionSet(caller.taskId, caller.roleName)?.providerBinding;
|
|
21
|
+
const turn = binding?.conversations.find(c => c.epoch === binding.currentConversationEpoch)?.conversationId === caller.nativeSessionId
|
|
22
|
+
? binding?.run : undefined;
|
|
23
|
+
const attempt = turn && ["submitting", "accepted", "delivery-unknown"].includes(turn.status)
|
|
24
|
+
? { attemptId: turn.attemptId } : {};
|
|
18
25
|
if (caller.scope === "global") {
|
|
19
|
-
return { scope: "global", roleName: caller.roleName, agentId: caller.agentId, nativeSessionId: caller.nativeSessionId };
|
|
26
|
+
return { scope: "global", roleName: caller.roleName, agentId: caller.agentId, nativeSessionId: caller.nativeSessionId, ...attempt };
|
|
20
27
|
}
|
|
21
28
|
if (caller.roleName !== "leader" && caller.currentRunId === undefined) {
|
|
22
29
|
throw new Error("An Assignment inbox requires its current active AgentRun.");
|
|
23
30
|
}
|
|
24
31
|
return { scope: "task", taskId: caller.taskId, roleName: caller.roleName,
|
|
25
32
|
agentId: caller.agentId, nativeSessionId: caller.nativeSessionId,
|
|
26
|
-
...(caller.currentRunId === undefined ? {} : { runId: caller.currentRunId }) };
|
|
33
|
+
...(caller.currentRunId === undefined ? {} : { runId: caller.currentRunId }), ...attempt };
|
|
34
|
+
}
|
|
35
|
+
function currentTurn(store, address) {
|
|
36
|
+
const binding = address.scope === "global" ? store.getGlobalRoleSessionSet(address.roleName)?.providerBinding
|
|
37
|
+
: store.getTaskRoleSessionSet(address.taskId, address.roleName)?.providerBinding;
|
|
38
|
+
return binding?.conversations.find(c => c.epoch === binding.currentConversationEpoch)?.conversationId === address.nativeSessionId
|
|
39
|
+
? binding?.run : undefined;
|
|
27
40
|
}
|
|
28
41
|
function ownsMessage(store, address, message) {
|
|
29
42
|
if (address.roleName === "leader" && message.taskId === address.taskId && isLeaderSteer(message))
|
|
@@ -80,15 +93,15 @@ function blocker(store, address, entry) {
|
|
|
80
93
|
if (!isWake(entry) && address.scope === "task"
|
|
81
94
|
&& isTaskRecordRetired(store.listEvents(address.taskId), "message", entry.id))
|
|
82
95
|
return "message-retired";
|
|
83
|
-
if (entry.inbox
|
|
84
|
-
return "acknowledged";
|
|
96
|
+
if (inboxSettled(entry.inbox))
|
|
97
|
+
return entry.inbox?.retirement ? "retired" : "acknowledged";
|
|
85
98
|
if (entry.inbox !== undefined) {
|
|
86
99
|
return sameSession(entry.inbox.claim.actor, address) ? undefined : "claimed-by-previous-session";
|
|
87
100
|
}
|
|
88
101
|
if (isWake(entry)) {
|
|
89
102
|
// Native acceptance releases transport ownership, not semantic handling.
|
|
90
103
|
// The current Leader may explicitly claim the fixed delivered window.
|
|
91
|
-
if (entry.status === "consumed")
|
|
104
|
+
if (entry.status === "consumed" || releasedNotificationWake(store, entry.taskId, entry.id))
|
|
92
105
|
return undefined;
|
|
93
106
|
return "automatic-notification-owns-window";
|
|
94
107
|
}
|
|
@@ -150,9 +163,12 @@ function checkMutation(store, address) {
|
|
|
150
163
|
}
|
|
151
164
|
function summary(store, address, entry) {
|
|
152
165
|
return { id: entry.id, kind: isWake(entry) ? "notification-window" : "message",
|
|
166
|
+
category: inboxEntryCategory(entry, address, currentTurn(store, address)),
|
|
153
167
|
createdAt: entry.createdAt, summary: summaryText(isWake(entry) ? entry.reasons.join(", ") : entry.body),
|
|
154
168
|
blocked: blocker(store, address, entry) ?? null, inbox: summaryMetadata(entry.inbox ?? null),
|
|
155
169
|
handling: inboxHandlingState(entry.inbox),
|
|
170
|
+
...(entry.inbox !== undefined && entry.inbox.ack === undefined && !sameSession(entry.inbox.claim.actor, address)
|
|
171
|
+
? { recovery: "Read this original and prior effects under current authority; finish handling, then inbox ack this exact entry. Retain the original claim; do not claim again or replay business." } : {}),
|
|
156
172
|
delivery: isWake(entry) ? entry.status : summaryMetadata({
|
|
157
173
|
control: entry.control ?? null,
|
|
158
174
|
...(address.scope === "global" ? { receipt: entry.delivery ?? null,
|
|
@@ -173,23 +189,33 @@ export function runInboxCommand(args, store, env, now = new Date()) {
|
|
|
173
189
|
const [action, ...tail] = args;
|
|
174
190
|
const address = addressOf(store, env);
|
|
175
191
|
if (action === "list") {
|
|
176
|
-
const parsed = readOptions(tail);
|
|
177
|
-
|
|
178
|
-
|
|
192
|
+
const parsed = readOptions(tail, ["--limit", "--cursor", "--view"]);
|
|
193
|
+
const view = parsed.values.get("--view") ?? "all";
|
|
194
|
+
if (!["all", "current", "history"].includes(view) || parsed.positionals.length) {
|
|
195
|
+
throw new Error("Inbox list accepts --view current|history|all, --limit and --cursor.");
|
|
196
|
+
}
|
|
179
197
|
const retired = address.scope === "task" ? retiredTaskRecordIds(store.listEvents(address.taskId), "message") : new Set();
|
|
180
198
|
const selected = address.scope === "global" ? pendingGlobalInboxMessages(store.listGlobalRoleMessages(address.roleName))
|
|
181
199
|
: entries(store, address).filter(entry => (isWake(entry) || !retired.has(entry.id))
|
|
182
|
-
&&
|
|
183
|
-
const
|
|
200
|
+
&& !inboxSettled(entry.inbox));
|
|
201
|
+
const turn = currentTurn(store, address);
|
|
202
|
+
const categories = selected.map(entry => ({ entry, category: inboxEntryCategory(entry, address, turn) }));
|
|
203
|
+
const rank = { current: 0, available: 1, history: 2 };
|
|
204
|
+
categories.sort((a, b) => rank[a.category] - rank[b.category]);
|
|
205
|
+
const items = categories.filter(item => view === "all"
|
|
206
|
+
|| (view === "history") === (item.category === "history")).map(({ entry }) => summary(store, address, entry));
|
|
184
207
|
if (address.scope === "task" && address.roleName === "leader") {
|
|
185
208
|
const mailbox = store.getWorkMailbox(target(address));
|
|
186
|
-
if (mailbox?.pending != null)
|
|
187
|
-
items.
|
|
209
|
+
if (mailbox?.pending != null && view !== "history")
|
|
210
|
+
items.splice(categories.filter(item => item.category === "current").length, 0, { id: pendingId(mailbox.pending), kind: "notification-window",
|
|
211
|
+
category: "available",
|
|
188
212
|
summary: summaryText(mailbox.pending.reasons.join(", ")),
|
|
189
213
|
blocked: mailbox.processing === null && !unfinishedLeaderInbox(store, address.taskId) ? null : "earlier-window-owned",
|
|
190
214
|
read: `yui inbox show ${pendingId(mailbox.pending)}` });
|
|
191
215
|
}
|
|
192
|
-
return recordPage(items, source(address)
|
|
216
|
+
return recordPage(items, `${source(address)}/${view}`, parsed, { identity: address, view,
|
|
217
|
+
historyCount: categories.filter(item => item.category === "history").length,
|
|
218
|
+
note: "History lacks handling evidence; it is not unexecuted work. Inspect original effects before disposition." });
|
|
193
219
|
}
|
|
194
220
|
if (action === "show") {
|
|
195
221
|
const parsed = readOptions(tail, ["--cursor"]);
|
|
@@ -208,8 +234,8 @@ export function runInboxCommand(args, store, env, now = new Date()) {
|
|
|
208
234
|
? expandTaskMessageResult(entry, (task, run) => store.getRun(task, run)) : entry;
|
|
209
235
|
return boundedDocument(value, `${source(address)}/${id}`, parsed.cursor);
|
|
210
236
|
}
|
|
211
|
-
if (action !== "claim" && action !== "ack")
|
|
212
|
-
throw new Error("Inbox commands: list, show, claim, ack.");
|
|
237
|
+
if (action !== "claim" && action !== "ack" && action !== "retire")
|
|
238
|
+
throw new Error("Inbox commands: list, show, claim, ack, retire.");
|
|
213
239
|
const parsed = readOptions(tail, ["--request-id", "--reason"]);
|
|
214
240
|
const ids = parsed.positionals;
|
|
215
241
|
const requestId = parsed.values.get("--request-id");
|
|
@@ -220,7 +246,7 @@ export function runInboxCommand(args, store, env, now = new Date()) {
|
|
|
220
246
|
if (!ids.length || ids.length > 16 || new Set(ids).size !== ids.length) {
|
|
221
247
|
throw new Error("Select 1..16 distinct exact inbox entries; no wildcard acknowledgement.");
|
|
222
248
|
}
|
|
223
|
-
if (action
|
|
249
|
+
if (action !== "claim" && (!reason?.trim() || Buffer.byteLength(reason) > 2048)) {
|
|
224
250
|
throw new Error("Acknowledgement requires --reason describing handling or why no action remains (up to 2048 bytes).");
|
|
225
251
|
}
|
|
226
252
|
if (action === "claim" && reason !== undefined)
|
|
@@ -251,7 +277,7 @@ export function runInboxCommand(args, store, env, now = new Date()) {
|
|
|
251
277
|
continue;
|
|
252
278
|
const message = tx.listMessages(current.taskId).find(message => message.id === ref.id);
|
|
253
279
|
if (message !== undefined && (taskMessageInboxReceipt(tx, message) !== undefined
|
|
254
|
-
||
|
|
280
|
+
|| nonReplayableMessageWake(tx, message) !== undefined)
|
|
255
281
|
|| message?.interruptThen !== undefined
|
|
256
282
|
|| message?.control !== undefined && message.control.outcome !== "rejected"
|
|
257
283
|
|| message?.inputControl?.expectedSessionId !== undefined
|
|
@@ -274,7 +300,36 @@ export function runInboxCommand(args, store, env, now = new Date()) {
|
|
|
274
300
|
}
|
|
275
301
|
else {
|
|
276
302
|
entry = getEntry(tx, current, id);
|
|
277
|
-
if (action === "
|
|
303
|
+
if (action === "retire") {
|
|
304
|
+
if (current.scope !== "global" || isWake(entry)) {
|
|
305
|
+
throw new Error("Task input retirement uses the Leader's existing task message retire operation.");
|
|
306
|
+
}
|
|
307
|
+
const message = entry;
|
|
308
|
+
if (message.inbox?.retirement) {
|
|
309
|
+
const saved = message.inbox.retirement;
|
|
310
|
+
if (saved.requestId !== requestId || saved.reason !== reason || !sameSession(saved.actor, current)) {
|
|
311
|
+
throw new Error("Conflicting retirement; original disposition retained.");
|
|
312
|
+
}
|
|
313
|
+
return receipt(entry);
|
|
314
|
+
}
|
|
315
|
+
// A retained attempt in an older conversation is still evidence of
|
|
316
|
+
// submission; changing the active Session cannot erase that boundary.
|
|
317
|
+
const turn = tx.getGlobalRoleSessionSet(current.roleName)?.providerBinding?.run;
|
|
318
|
+
const foreignTarget = message.deliveryTarget && (message.deliveryTarget.agentId !== current.agentId
|
|
319
|
+
|| message.deliveryTarget.nativeSessionId !== current.nativeSessionId)
|
|
320
|
+
|| message.inputControl?.expectedSessionId !== undefined
|
|
321
|
+
&& message.inputControl.expectedSessionId !== current.nativeSessionId;
|
|
322
|
+
if (message.inbox?.ack || message.delivery !== undefined || message.interruptThen !== undefined
|
|
323
|
+
|| message.control !== undefined && message.control.outcome !== "rejected"
|
|
324
|
+
|| turn?.attemptId === `global-input:${current.roleName}/${message.id}`
|
|
325
|
+
|| !foreignTarget && message.notDelivered === undefined) {
|
|
326
|
+
throw new Error("Retirement requires obsolete, proven-unsubmitted input; unknown/accepted/interrupt-owned input must retain its exact handling path.");
|
|
327
|
+
}
|
|
328
|
+
entry = { ...message, inbox: { ...message.inbox,
|
|
329
|
+
claim: message.inbox?.claim ?? { requestId, actor: current, at: now.toISOString() },
|
|
330
|
+
retirement: { requestId, actor: current, at: now.toISOString(), reason: reason } } };
|
|
331
|
+
}
|
|
332
|
+
else if (action === "claim") {
|
|
278
333
|
if (entry.inbox !== undefined) {
|
|
279
334
|
if (!sameSession(entry.inbox.claim.actor, current))
|
|
280
335
|
throw new Error("Claim belongs to a previous Session; read the original and prior effects, then explicitly acknowledge only after finishing its handling.");
|
|
@@ -285,7 +340,7 @@ export function runInboxCommand(args, store, env, now = new Date()) {
|
|
|
285
340
|
const blocked = blocker(tx, current, entry);
|
|
286
341
|
if (blocked !== undefined)
|
|
287
342
|
throw new Error(`Inbox entry cannot be claimed: ${blocked}.`);
|
|
288
|
-
if (isWake(entry) && !acceptedInput(entry)) {
|
|
343
|
+
if (isWake(entry) && !acceptedInput(entry) && !releasedNotificationWake(tx, entry.taskId, entry.id)) {
|
|
289
344
|
for (const ref of entry.refs ?? []) {
|
|
290
345
|
if (ref.type !== "message")
|
|
291
346
|
continue;
|
|
@@ -299,7 +354,7 @@ export function runInboxCommand(args, store, env, now = new Date()) {
|
|
|
299
354
|
for (const previous of entries(tx, current)) {
|
|
300
355
|
if (previous.id === entry.id)
|
|
301
356
|
break;
|
|
302
|
-
if (
|
|
357
|
+
if (!blocksInboxOrder(previous, current, currentTurn(tx, current)))
|
|
303
358
|
continue;
|
|
304
359
|
if (current.scope === "task"
|
|
305
360
|
&& isTaskRecordRetired(tx.listEvents(current.taskId), "message", previous.id))
|
|
@@ -315,11 +370,13 @@ export function runInboxCommand(args, store, env, now = new Date()) {
|
|
|
315
370
|
else {
|
|
316
371
|
if (!entry.inbox)
|
|
317
372
|
throw new Error("Claim the exact entry before handling and acknowledgement.");
|
|
373
|
+
if (entry.inbox.retirement)
|
|
374
|
+
throw new Error("Retirement is not acknowledgement; original disposition retained.");
|
|
318
375
|
// Ack settles already-owned handling; it never submits or retargets
|
|
319
376
|
// the original. The current rightful successor may settle a claim
|
|
320
377
|
// acquired by the exact selected Session, retaining both identities.
|
|
321
378
|
const claimant = entry.inbox.claim.actor;
|
|
322
|
-
if (isWake(entry) && !acceptedInput(entry)) {
|
|
379
|
+
if (isWake(entry) && !acceptedInput(entry) && !releasedNotificationWake(tx, entry.taskId, entry.id)) {
|
|
323
380
|
const messageIds = new Set(entry.refs?.filter(ref => ref.type === "message").map(ref => ref.id));
|
|
324
381
|
for (const message of tx.listMessages(current.taskId)) {
|
|
325
382
|
if (messageIds.has(message.id))
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readdir, rename, rm } from "node:fs/promises";
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { usageError } from "../errors/cliError.js";
|
|
5
5
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
6
6
|
import { boundedDocument, readOptions, recordPage } from "../output/boundedRead.js";
|
|
7
7
|
import { healCheckoutSwap, restoreCheckoutSwap, swapManagedCheckout } from "../repository/checkoutSwap.js";
|
|
8
|
-
import {
|
|
8
|
+
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
9
9
|
import { acquireProjectMaintenanceLock } from "../repository/projectMaintenanceLock.js";
|
|
10
|
+
import { refreshRegisteredProject } from "../repository/projectRefresh.js";
|
|
10
11
|
import { addProjectKnowledge, addKnowledgeProposal, assertProjectActive, decideKnowledgeProposal, createProject, findKnowledgeProposal, findKnowledgeProposalByFingerprint, knowledgeEvidenceDigest, knowledgeProposalFingerprint, managedProjectPath, planKnowledgeAcceptance, retireProject, retireProjectKnowledge, resolveProject, updateProjectKnowledge, updateProjectMetadata, validateProject, validateProjectName } from "../repository/project.js";
|
|
11
12
|
import { managedWorkspacesRoot } from "../storage/homeLayout.js";
|
|
12
13
|
import { projectActor } from "../task/taskAuthority.js";
|
|
@@ -90,37 +91,7 @@ async function refreshProject(args, store, options) {
|
|
|
90
91
|
throw usageError("Project refresh usage: yui project refresh <project>.");
|
|
91
92
|
}
|
|
92
93
|
const project = requireProject(store, args[0]);
|
|
93
|
-
|
|
94
|
-
if (project.remoteUrl === undefined) {
|
|
95
|
-
throw usageError(`Project refresh requires a remote URL: ${project.id}.`);
|
|
96
|
-
}
|
|
97
|
-
if (project.stableBranch !== project.developmentBranch) {
|
|
98
|
-
throw usageError(`Project refresh requires matching stable and development branches: ${project.id}.`);
|
|
99
|
-
}
|
|
100
|
-
// RFC Phase 1: hold the per-Project maintenance fence for the whole refresh
|
|
101
|
-
// so Task workspace preparation and other maintenance cannot interleave
|
|
102
|
-
// with the canonical branch/working-tree move.
|
|
103
|
-
const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
|
|
104
|
-
try {
|
|
105
|
-
const current = requireUnchangedMaintenanceProject(store, project, "refresh");
|
|
106
|
-
const refreshed = await (options.git ?? new NodeGitWorkspace()).refresh({
|
|
107
|
-
repositoryPath: current.path,
|
|
108
|
-
remoteUrl: current.remoteUrl,
|
|
109
|
-
stableRef: current.stableBranch
|
|
110
|
-
});
|
|
111
|
-
return { project: current, ...refreshed };
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
if (error instanceof GitWorkspaceRefreshError) {
|
|
115
|
-
throw new CliError("RUNTIME_ERROR", error.message, undefined, {
|
|
116
|
-
projectId: project.id, refresh: error.result
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
throw error;
|
|
120
|
-
}
|
|
121
|
-
finally {
|
|
122
|
-
releaseMaintenance();
|
|
123
|
-
}
|
|
94
|
+
return refreshRegisteredProject(store, project.id, options);
|
|
124
95
|
}
|
|
125
96
|
/**
|
|
126
97
|
* RFC Phase 1: diagnose a managed Project's divergence without mutating it.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
import { callFileTaskController } from "../controller/clientRuntime.js";
|
|
3
|
+
import { resolveJobCaller } from "../task/taskAuthority.js";
|
|
4
|
+
export async function runScheduleCommand(args, options) {
|
|
5
|
+
const [op, ...rest] = args;
|
|
6
|
+
if (op === undefined || !["create", "list", "show", "update", "enable", "disable"].includes(op)) {
|
|
7
|
+
throw usageError("Schedule command must be create, list, show, update, enable, or disable.");
|
|
8
|
+
}
|
|
9
|
+
const flags = new Map();
|
|
10
|
+
for (let i = 0; i < rest.length; i++) {
|
|
11
|
+
const flag = rest[i];
|
|
12
|
+
if (!["--global", "--task", "--schedule", "--job", "--rule", "--revision"].includes(flag) || flags.has(flag)) {
|
|
13
|
+
throw usageError(`Unknown or repeated Schedule flag: ${flag}.`);
|
|
14
|
+
}
|
|
15
|
+
if (flag === "--global") {
|
|
16
|
+
flags.set(flag, "true");
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const value = rest[++i];
|
|
20
|
+
if (value === undefined || value.startsWith("--"))
|
|
21
|
+
throw usageError(`Missing value: ${flag}.`);
|
|
22
|
+
flags.set(flag, value);
|
|
23
|
+
}
|
|
24
|
+
if (flags.has("--global") === flags.has("--task"))
|
|
25
|
+
throw usageError("Choose exactly one of --global or --task.");
|
|
26
|
+
const taskId = flags.get("--task");
|
|
27
|
+
let rule;
|
|
28
|
+
if (flags.has("--rule")) {
|
|
29
|
+
try {
|
|
30
|
+
rule = JSON.parse(flags.get("--rule"));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw usageError("--rule must be JSON.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const result = await callFileTaskController(options.home, "schedule.control", {
|
|
37
|
+
op, scope: taskId === undefined ? { kind: "global" } : { kind: "task", taskId },
|
|
38
|
+
caller: resolveJobCaller(options.environment, taskId ?? ""),
|
|
39
|
+
...(flags.has("--schedule") ? { id: flags.get("--schedule") } : {}),
|
|
40
|
+
...(flags.has("--job") ? { jobId: flags.get("--job") } : {}),
|
|
41
|
+
...(rule === undefined ? {} : { rule }),
|
|
42
|
+
...(flags.has("--revision") ? { expectedRevision: Number(flags.get("--revision")) } : {})
|
|
43
|
+
}, { environment: options.environment });
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
@@ -6,6 +6,7 @@ import { currentManagedRuntime } from "../runtime/managedCaller.js";
|
|
|
6
6
|
import { describeEnvironmentPlan } from "../task/taskActivation.js";
|
|
7
7
|
import { cancelTaskActivation, requestTaskActivation, taskActivationOperationRef } from "../task/taskActivationService.js";
|
|
8
8
|
import { taskLocalActor } from "../task/taskAuthority.js";
|
|
9
|
+
import { taskPhase } from "../context/taskPhase.js";
|
|
9
10
|
/**
|
|
10
11
|
* Explicit Activation requests.
|
|
11
12
|
*
|
|
@@ -20,7 +21,7 @@ export function runTaskActivationCommand(args, store, options) {
|
|
|
20
21
|
switch (command) {
|
|
21
22
|
case "request": return requestActivation(rest, store, options);
|
|
22
23
|
case "cancel": return cancelActivation(rest, store, options);
|
|
23
|
-
case "show": return showActivation(rest,
|
|
24
|
+
case "show": return store.readTransaction((reader) => showActivation(rest, reader));
|
|
24
25
|
default:
|
|
25
26
|
throw usageError(command === undefined
|
|
26
27
|
? "Task activation command is required."
|
|
@@ -28,7 +29,8 @@ export function runTaskActivationCommand(args, store, options) {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
const REQUEST_USAGE = "Task activation request usage: yui task activation request <task> "
|
|
31
|
-
+ "--request-id <id> --environment <empty|scratch|local:<resource>:<read|write
|
|
32
|
+
+ "--request-id <id> --environment <empty|scratch|local:<resource>:<read|write>> "
|
|
33
|
+
+ "[--source-message <message-id> | --source-text <exact-input>].";
|
|
32
34
|
/**
|
|
33
35
|
* Records the request and returns its reference immediately.
|
|
34
36
|
*
|
|
@@ -39,19 +41,26 @@ const REQUEST_USAGE = "Task activation request usage: yui task activation reques
|
|
|
39
41
|
* current then.
|
|
40
42
|
*/
|
|
41
43
|
function requestActivation(args, store, options) {
|
|
42
|
-
const parsed = parseTail(args, new Set(["--request-id", "--environment"]), REQUEST_USAGE);
|
|
44
|
+
const parsed = parseTail(args, new Set(["--request-id", "--environment", "--source-message", "--source-text"]), REQUEST_USAGE);
|
|
43
45
|
exactPositionals(parsed.positionals, 1, REQUEST_USAGE);
|
|
44
46
|
const taskId = parsed.positionals[0];
|
|
45
47
|
const requestId = requiredOption(parsed.options, "--request-id", REQUEST_USAGE);
|
|
46
48
|
const environmentPlan = parseEnvironmentPlan(requiredOption(parsed.options, "--environment", REQUEST_USAGE), REQUEST_USAGE);
|
|
47
49
|
const now = options.now?.() ?? new Date();
|
|
48
50
|
const caller = activationCaller(store, options, taskId);
|
|
51
|
+
const sourceMessageId = parsed.options.get("--source-message");
|
|
52
|
+
const sourceText = parsed.options.get("--source-text");
|
|
53
|
+
if (caller.actorId.startsWith("task:") && sourceMessageId === undefined && sourceText === undefined) {
|
|
54
|
+
throw usageError("Leader activation requires --source-message or --source-text with the exact new user input. Read the original; do not substitute a Brief summary.", REQUEST_USAGE);
|
|
55
|
+
}
|
|
49
56
|
const result = attempt(REQUEST_USAGE, () => requestTaskActivation(store, {
|
|
50
57
|
taskId,
|
|
51
58
|
requestId,
|
|
52
59
|
actorId: caller.actorId,
|
|
53
60
|
authorityRef: caller.authorityRef,
|
|
54
61
|
environmentPlan,
|
|
62
|
+
sourceMessageId, sourceText,
|
|
63
|
+
sourceNativeSessionId: caller.nativeSessionId,
|
|
55
64
|
...(caller.planningRunId === undefined ? {} : { callerRunId: caller.planningRunId })
|
|
56
65
|
}, now));
|
|
57
66
|
// An immediate request has nothing left to wait for, so ask the Controller to
|
|
@@ -64,12 +73,14 @@ function requestActivation(args, store, options) {
|
|
|
64
73
|
void options.runtime?.notifyMailboxChanged({ kind: "task", taskId });
|
|
65
74
|
}
|
|
66
75
|
const request = result.request;
|
|
76
|
+
const source = taskPhase(store, { ...requireTask(store, taskId), activationRequest: request }).activation?.source;
|
|
67
77
|
const lines = [
|
|
68
78
|
`${result.created ? "Requested" : "Existing"} activation ${result.operationRef}`,
|
|
69
79
|
`Task: ${taskId} (draft)`,
|
|
70
80
|
`Start: ${result.startMode}${result.afterPlanningRun === undefined ? "" : ` after ${taskId}/${result.afterPlanningRun}`}`,
|
|
71
81
|
`Environment: ${describeEnvironmentPlan(request.environmentPlan)}`,
|
|
72
82
|
`Disposition: ${request.disposition}`,
|
|
83
|
+
...(source?.read === undefined ? [] : [`Source: yui ${source.read}`]),
|
|
73
84
|
result.startMode === "after-planning-turn"
|
|
74
85
|
? "This reference is returned now; the request is adopted after the planning Turn ends."
|
|
75
86
|
: `Adopt it with: yui task activate ${taskId}`
|
|
@@ -81,7 +92,8 @@ function requestActivation(args, store, options) {
|
|
|
81
92
|
...(result.afterPlanningRun === undefined
|
|
82
93
|
? {}
|
|
83
94
|
: { afterPlanningRun: result.afterPlanningRun }),
|
|
84
|
-
request
|
|
95
|
+
request,
|
|
96
|
+
source
|
|
85
97
|
});
|
|
86
98
|
}
|
|
87
99
|
/** A cancelled request is never adopted, even if the same id is retried. */
|
|
@@ -139,7 +151,8 @@ function showActivation(args, store) {
|
|
|
139
151
|
`Requested at: ${request.requestedAt}`,
|
|
140
152
|
...settledLines
|
|
141
153
|
];
|
|
142
|
-
return output(`${lines.join("\n")}\n`, { task, request, settledRequests: settled
|
|
154
|
+
return output(`${lines.join("\n")}\n`, { task, request, settledRequests: settled,
|
|
155
|
+
phase: taskPhase(store, task) });
|
|
143
156
|
}
|
|
144
157
|
/**
|
|
145
158
|
* Resolves who is requesting, and whether they are running inside this Task's
|
|
@@ -161,6 +174,7 @@ function activationCaller(store, options, taskId) {
|
|
|
161
174
|
: store.getRun(taskId, runtime.currentRunId);
|
|
162
175
|
return {
|
|
163
176
|
actorId: `task:${taskId}/role:${SYSTEM_LEADER_ROLE}`,
|
|
177
|
+
nativeSessionId: runtime.nativeSessionId,
|
|
164
178
|
// Non-secret binding fingerprint of the live Session, matching the Job
|
|
165
179
|
// boundary: reattaching the same Session keeps it, replacing it does not.
|
|
166
180
|
authorityRef: fingerprint([
|
|
@@ -83,6 +83,7 @@ import { assertTaskOpen, clock, exactPositionals, leaderActionEventPayload, lead
|
|
|
83
83
|
import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
|
|
84
84
|
import { runTaskContextCommand, taskRecordList } from "./taskContextCommand.js";
|
|
85
85
|
import { taskBriefCommand, taskDecisionCommand, taskEventCommand, taskMilestoneCommand } from "./taskFactCommands.js";
|
|
86
|
+
import { taskProjectDeliveryCommand } from "./taskProjectDeliveryCommand.js";
|
|
86
87
|
import { taskUsageCommand } from "./taskUsageCommand.js";
|
|
87
88
|
import { openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
88
89
|
import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
|
|
@@ -456,6 +457,8 @@ function assertCandidateReviewRoleIsolation(producerRoles, reviewerRoleName, lan
|
|
|
456
457
|
}
|
|
457
458
|
function taskProjectCommand(args, store, options) {
|
|
458
459
|
const [command, ...rest] = args;
|
|
460
|
+
if (command === "delivery")
|
|
461
|
+
return taskProjectDeliveryCommand(rest, store, options);
|
|
459
462
|
if (command === "list") {
|
|
460
463
|
exactPositionals(rest, 1, "Task project list usage: yui task project list <task>.");
|
|
461
464
|
const task = requireTask(store, rest[0]);
|
|
@@ -665,6 +668,7 @@ function routeUserSubmission(tx, task, actor, body, intent, now, submissionKey,
|
|
|
665
668
|
requestId: `submit-${message.id}`,
|
|
666
669
|
actorId: identity.actorId,
|
|
667
670
|
authorityRef: identity.authorityRef,
|
|
671
|
+
sourceMessageId: message.id,
|
|
668
672
|
environmentPlan: routing.environmentPlan
|
|
669
673
|
}, now);
|
|
670
674
|
activationRef = taskActivationOperationRef(task.id, `submit-${message.id}`);
|
|
@@ -1879,7 +1883,9 @@ function steerTaskInput(taskId, input, store, options) {
|
|
|
1879
1883
|
if (saved === undefined || requestId === undefined || taskMessageInboxReceipt(tx, saved) !== undefined)
|
|
1880
1884
|
return false;
|
|
1881
1885
|
tx.updateMessage(result.task.id, recordTaskMessageControlOutcome(saved, {
|
|
1882
|
-
requestId, receiptId, outcome: "pending", observedAt: clock(options)
|
|
1886
|
+
requestId, receiptId, outcome: "pending", observedAt: clock(options),
|
|
1887
|
+
target: { agentId: resolution.target.agentId, nativeSessionId: resolution.target.nativeSessionId,
|
|
1888
|
+
attemptId: resolution.target.attemptId }
|
|
1883
1889
|
}));
|
|
1884
1890
|
return true;
|
|
1885
1891
|
});
|
|
@@ -7008,7 +7014,7 @@ function taskWakeInspectionCommand(args, store) {
|
|
|
7008
7014
|
const windowEvents = allEvents.filter((e) => inWindow(e.createdAt) || referenced("event", e.id) || reasonEvent(e));
|
|
7009
7015
|
const events = history ? windowEvents : windowEvents.filter(e => referenced("event", e.id)
|
|
7010
7016
|
|| reasonEvent(e) || ["run.completed", "run.failed", "run.cancelled"].includes(e.type));
|
|
7011
|
-
const messages = wakeMessages(wake, store.listMessages(task.id));
|
|
7017
|
+
const messages = wakeMessages(wake, store.listMessages(task.id), history);
|
|
7012
7018
|
const allRuns = store.listRuns(task.id);
|
|
7013
7019
|
const referencedRunIds = new Set(referencedWakeRunIds(allRuns, allEvents, events));
|
|
7014
7020
|
for (const run of allRuns)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createTaskBrief, updateTaskBrief } from "../brief/taskBrief.js";
|
|
2
|
-
import { assertContextRecordReadable } from "../context/taskContext.js";
|
|
2
|
+
import { assertContextRecordReadable, resolveContextReader } from "../context/taskContext.js";
|
|
3
3
|
import { taskRecordList } from "./taskContextCommand.js";
|
|
4
4
|
import { materialize } from "../context/taskContext.js";
|
|
5
5
|
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
@@ -8,15 +8,22 @@ import { dataError, usageError } from "../errors/cliError.js";
|
|
|
8
8
|
import { createMilestone } from "../milestone/milestone.js";
|
|
9
9
|
import { assertTaskOpen, clock, exactPositionals, leaderActionEventPayload, leaderMailbox, notifyMailbox, output, parseMultiValueTail, parseTail, presentTime, recordTaskEvent, requiredOption, requiredText, requireTask, taskActor, taskMailbox, taskRef } from "./taskCommandSupport.js";
|
|
10
10
|
import { failureCapabilitiesCommand } from "../runtime/agentFailureContext.js";
|
|
11
|
+
import { taskBriefGuard, taskPhase } from "../context/taskPhase.js";
|
|
11
12
|
export function taskBriefCommand(args, store, options) {
|
|
12
13
|
const [command, ...rest] = args;
|
|
13
14
|
if (command === "show") {
|
|
14
15
|
exactPositionals(rest, 1, "Task brief show usage: yui task brief show <task>.");
|
|
15
|
-
const task =
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const { task, brief, writeGuard, phase } = store.readTransaction(reader => {
|
|
17
|
+
const task = requireTask(reader, rest[0]);
|
|
18
|
+
assertContextRecordReadable(reader, task.id, "task-brief", task.id, options.environment);
|
|
19
|
+
const caller = resolveContextReader(reader, options.environment ?? {});
|
|
20
|
+
const shared = caller === undefined || caller.roleName === "leader";
|
|
21
|
+
return { task, brief: reader.getTaskBrief(task.id),
|
|
22
|
+
writeGuard: shared ? taskBriefGuard(reader, task) : undefined,
|
|
23
|
+
phase: shared ? taskPhase(reader, task) : undefined };
|
|
24
|
+
});
|
|
18
25
|
if (brief === null) {
|
|
19
|
-
return output(`Task ${task.id} has no brief.\n`, { taskId: task.id, brief: null });
|
|
26
|
+
return output(`Task ${task.id} has no brief.\n${writeGuard === undefined ? "" : `Write guard: ${writeGuard}\n`}`, { taskId: task.id, brief: null, writeGuard, phase });
|
|
20
27
|
}
|
|
21
28
|
const timeZone = store.getConfig().timeZone;
|
|
22
29
|
const working = task.status === "draft" || task.status === "active";
|
|
@@ -32,12 +39,13 @@ export function taskBriefCommand(args, store, options) {
|
|
|
32
39
|
`${working ? "Current focus" : "Last working focus"}: ${brief.currentFocus}`,
|
|
33
40
|
`${working ? "Leader summary" : "Last working summary"}: ${brief.leaderSummary}`,
|
|
34
41
|
`Updated by: ${brief.updatedBy}`,
|
|
35
|
-
`Updated at: ${presentTime(brief.updatedAt, timeZone)}
|
|
36
|
-
|
|
42
|
+
`Updated at: ${presentTime(brief.updatedAt, timeZone)}`,
|
|
43
|
+
...(writeGuard === undefined ? [] : [`Write guard: ${writeGuard}`])
|
|
44
|
+
].join("\n").concat("\n"), { taskId: task.id, brief, writeGuard, phase });
|
|
37
45
|
}
|
|
38
46
|
if (command === "update") {
|
|
39
|
-
const usage = "Task brief update usage: yui task brief update <task> [--objective <text>] [--boundary <text> ...] [--approach <text>] [--focus <text>] [--leader-summary <text>].";
|
|
40
|
-
const parsed = parseMultiValueTail(rest, new Set(["--objective", "--approach", "--focus", "--leader-summary"]), new Set(["--boundary"]), usage);
|
|
47
|
+
const usage = "Task brief update usage: yui task brief update <task> --if-current <writeGuard> [--objective <text>] [--boundary <text> ...] [--approach <text>] [--focus <text>] [--leader-summary <text>].";
|
|
48
|
+
const parsed = parseMultiValueTail(rest, new Set(["--objective", "--approach", "--focus", "--leader-summary", "--if-current"]), new Set(["--boundary"]), usage);
|
|
41
49
|
exactPositionals(parsed.positionals, 1, usage);
|
|
42
50
|
const hasObjective = parsed.options.has("--objective");
|
|
43
51
|
const hasApproach = parsed.options.has("--approach");
|
|
@@ -53,6 +61,9 @@ export function taskBriefCommand(args, store, options) {
|
|
|
53
61
|
assertTaskOpen(task);
|
|
54
62
|
const existing = tx.getTaskBrief(task.id);
|
|
55
63
|
const updatedBy = taskActor(tx, options, task.id);
|
|
64
|
+
if (parsed.options.get("--if-current") !== taskBriefGuard(tx, task)) {
|
|
65
|
+
throw usageError(`Task context or Brief changed, or --if-current is missing; read task brief show ${task.id}, reconcile new input, then retry with its writeGuard.`);
|
|
66
|
+
}
|
|
56
67
|
const brief = existing === null
|
|
57
68
|
? createTaskBrief({
|
|
58
69
|
objective: requiredText(parsed.options.get("--objective"), "--objective"),
|
|
@@ -244,9 +255,10 @@ export function taskEventCommand(args, store, options = {}) {
|
|
|
244
255
|
exactPositionals(rest, 2, "Task event show usage: yui task event show <task> <event>.");
|
|
245
256
|
const task = requireTask(store, rest[0]);
|
|
246
257
|
assertContextRecordReadable(store, task.id, "task-event", rest[1], options.environment);
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
258
|
+
const event = store.queryContextRecords(task.id, {
|
|
259
|
+
family: "task-event", ids: [rest[1]], limit: 1
|
|
260
|
+
}).records[0]?.value;
|
|
261
|
+
if (event === undefined)
|
|
250
262
|
throw dataError(`Event not found: ${rest[1]}.`);
|
|
251
263
|
const timeZone = store.getConfig().timeZone;
|
|
252
264
|
const capabilitiesCommand = event.type === "runtime.agent-error" && event.payload.roleName
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readTaskAuthorizationMessage, taskAuthorizationSource } from "../grant/taskAuthorization.js";
|
|
2
|
+
import { assertTaskDeliveryAuthority } from "../task/taskAuthority.js";
|
|
3
|
+
import { PROJECT_DELIVERY_DECIDED_EVENT, projectDeliveryDecision } from "../task/projectDeliveryScope.js";
|
|
4
|
+
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
5
|
+
import { usageError } from "../errors/cliError.js";
|
|
6
|
+
import { clock, exactPositionals, leaderMailbox, notifyMailbox, output, parseTail, recordTaskEvent, requiredOption, requireTask, taskMailbox, taskRef } from "./taskCommandSupport.js";
|
|
7
|
+
export function taskProjectDeliveryCommand(args, store, options) {
|
|
8
|
+
const usage = "yui task project delivery <task> <project-id> --requirement <required|excluded> "
|
|
9
|
+
+ "--reason <text> --source-message <message-id> --purpose <exact-quote> --if-current <event-id|none>";
|
|
10
|
+
const parsed = parseTail(args, new Set([
|
|
11
|
+
"--requirement", "--reason", "--source-message", "--purpose", "--if-current"
|
|
12
|
+
]), usage);
|
|
13
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
14
|
+
const requirement = requiredOption(parsed.options, "--requirement");
|
|
15
|
+
if (requirement !== "required" && requirement !== "excluded")
|
|
16
|
+
throw usageError(usage);
|
|
17
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
18
|
+
const sourceMessage = requiredOption(parsed.options, "--source-message");
|
|
19
|
+
const purpose = requiredOption(parsed.options, "--purpose");
|
|
20
|
+
const guard = requiredOption(parsed.options, "--if-current");
|
|
21
|
+
const now = clock(options);
|
|
22
|
+
const result = store.transaction(tx => {
|
|
23
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
24
|
+
const actor = assertTaskDeliveryAuthority(tx, options.environment, task.id);
|
|
25
|
+
if (!["active", "completed"].includes(task.status) || task.executionGate.state !== "enabled") {
|
|
26
|
+
throw usageError("Delivery scope requires an active or completed, unarchived Task with execution enabled; it does not reopen or resume work.");
|
|
27
|
+
}
|
|
28
|
+
const projectId = parsed.positionals[1];
|
|
29
|
+
if (!task.projectBindings.some(binding => binding.projectId === projectId)) {
|
|
30
|
+
throw usageError(`Project is not bound to this Task: ${projectId}.`);
|
|
31
|
+
}
|
|
32
|
+
const source = actor === "leader"
|
|
33
|
+
? taskAuthorizationSource(tx, task.id, options.environment ?? {}, sourceMessage, purpose)
|
|
34
|
+
: readTaskAuthorizationMessage(tx, task.id, sourceMessage, purpose);
|
|
35
|
+
const previous = projectDeliveryDecision(tx.listEventsByType(task.id, [PROJECT_DELIVERY_DECIDED_EVENT]), task.id, projectId);
|
|
36
|
+
if (guard !== (previous?.id ?? "none")) {
|
|
37
|
+
throw usageError("Project delivery scope changed; read task remote-delivery and reconcile before retrying with its scopeDecision.id (or none).");
|
|
38
|
+
}
|
|
39
|
+
const event = recordTaskEvent(tx, task.id, PROJECT_DELIVERY_DECIDED_EVENT, {
|
|
40
|
+
projectId, requirement, reason, by: actor,
|
|
41
|
+
sourceMessageId: source.messageId, sourceDigest: source.digest, purpose: source.purpose,
|
|
42
|
+
...("nativeSessionId" in source && typeof source.nativeSessionId === "string"
|
|
43
|
+
? { nativeSessionId: source.nativeSessionId } : {}),
|
|
44
|
+
...(previous === null ? {} : { supersedes: previous.id })
|
|
45
|
+
}, now);
|
|
46
|
+
enqueueWork(tx, taskMailbox(task.id), "project-delivery-decided", now, [taskRef(task.id)]);
|
|
47
|
+
if (task.status === "active" && actor !== "leader") {
|
|
48
|
+
enqueueWork(tx, leaderMailbox(task.id), "project-delivery-decided", now, [taskRef(task.id)]);
|
|
49
|
+
}
|
|
50
|
+
return { task, event };
|
|
51
|
+
});
|
|
52
|
+
notifyMailbox(options.runtime, taskMailbox(result.task.id));
|
|
53
|
+
if (result.task.status === "active")
|
|
54
|
+
notifyMailbox(options.runtime, leaderMailbox(result.task.id));
|
|
55
|
+
return output(`Recorded Project delivery requirement (${requirement}); merge facts and cleanup authority are unchanged.\n`, result.event);
|
|
56
|
+
}
|
|
@@ -35,7 +35,7 @@ export function runPublicationCommand(args, store, options) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
function upsertPublication(args, store, options) {
|
|
38
|
-
const usage = "Task publication upsert usage: yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--head-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported
|
|
38
|
+
const usage = "Task publication upsert usage: yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--head-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported] [--evidence <text>] [--merged-at <iso-timestamp>].";
|
|
39
39
|
const parsed = parseUpsertArgs(args, usage);
|
|
40
40
|
exactPositionals(parsed.positionals, 1, usage);
|
|
41
41
|
const task = requireTask(store, parsed.positionals[0]);
|
|
@@ -173,8 +173,9 @@ function parseVerification(parsed, usage) {
|
|
|
173
173
|
if (parsed.flags.has("--reported") && parsed.flags.has("--verified")) {
|
|
174
174
|
throw usageError("--reported and --verified are mutually exclusive.", usage);
|
|
175
175
|
}
|
|
176
|
-
if (parsed.flags.has("--verified"))
|
|
177
|
-
|
|
176
|
+
if (parsed.flags.has("--verified")) {
|
|
177
|
+
throw usageError("Use task publication verify with a provider read or --observation tool evidence; upsert cannot assert --verified.");
|
|
178
|
+
}
|
|
178
179
|
if (parsed.flags.has("--reported"))
|
|
179
180
|
return "reported";
|
|
180
181
|
return undefined;
|
|
@@ -210,6 +211,8 @@ function inheritPublicationInput(existing, input) {
|
|
|
210
211
|
&& normalizeCommitForComparison(input.headCommit) !== existing.headCommit)
|
|
211
212
|
|| input.projectId !== existing.projectId
|
|
212
213
|
|| input.externalKind !== existing.externalKind
|
|
214
|
+
|| (input.externalUrl !== undefined && input.externalUrl !== existing.externalUrl)
|
|
215
|
+
|| (input.targetBranch !== undefined && input.targetBranch !== existing.targetBranch)
|
|
213
216
|
|| stateChanged
|
|
214
217
|
|| remoteCommitChanged
|
|
215
218
|
|| evidenceChanged
|