@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.
Files changed (119) hide show
  1. package/README.md +5 -3
  2. package/dist/cli/commandCatalog.js +56 -21
  3. package/dist/cli/helpRenderer.js +3 -1
  4. package/dist/cli/interactionPolicy.js +2 -2
  5. package/dist/commands/durableJobCommands.js +50 -0
  6. package/dist/commands/globalRoleCommands.js +30 -9
  7. package/dist/commands/inboxCommands.js +79 -22
  8. package/dist/commands/projectCommands.js +4 -33
  9. package/dist/commands/scheduleCommands.js +45 -0
  10. package/dist/commands/taskActivationCommands.js +19 -5
  11. package/dist/commands/taskCommands.js +8 -2
  12. package/dist/commands/taskFactCommands.js +24 -12
  13. package/dist/commands/taskProjectDeliveryCommand.js +56 -0
  14. package/dist/commands/taskPublicationCommands.js +6 -3
  15. package/dist/commands/taskPublicationVerifyCommand.js +74 -28
  16. package/dist/commands/taskRemoteDeliveryCommand.js +5 -3
  17. package/dist/context/runContextPack.js +25 -4
  18. package/dist/context/runInputContract.js +1 -0
  19. package/dist/context/taskContext.js +53 -16
  20. package/dist/context/taskPhase.js +99 -0
  21. package/dist/controlPlaneCli.js +38 -17
  22. package/dist/controller/controller.js +35 -0
  23. package/dist/controller/fileSchedulerStoreAdapter.js +75 -16
  24. package/dist/controller/globalInputDelivery.js +17 -7
  25. package/dist/controller/inboxTurnCheck.js +64 -0
  26. package/dist/controller/jobControl.js +42 -12
  27. package/dist/controller/jobDefinitionControl.js +304 -0
  28. package/dist/controller/jobSupervisor.js +47 -32
  29. package/dist/controller/providerRetryAdmission.js +2 -0
  30. package/dist/controller/runtime.js +11 -2
  31. package/dist/controller/scheduleControl.js +176 -0
  32. package/dist/coordination/workMailbox.js +3 -3
  33. package/dist/external/githubPublicationVerifier.js +14 -5
  34. package/dist/external/gitlabPublicationVerifier.js +12 -29
  35. package/dist/grant/taskAuthorization.js +7 -2
  36. package/dist/interaction/notificationPresentation.js +12 -3
  37. package/dist/interaction/operatorPresentation.js +5 -0
  38. package/dist/job/builtinJob.js +58 -0
  39. package/dist/job/durableJob.js +72 -49
  40. package/dist/job/jobDefinition.js +68 -0
  41. package/dist/job/jobRunner.js +2 -1
  42. package/dist/job/projectRefreshRunner.js +48 -0
  43. package/dist/message/globalInboxRecovery.js +44 -0
  44. package/dist/message/globalInterrupt.js +1 -1
  45. package/dist/message/inboxReceipt.js +14 -6
  46. package/dist/message/inboxTurn.js +86 -0
  47. package/dist/message/message.js +11 -0
  48. package/dist/message/taskInboxOwnership.js +20 -6
  49. package/dist/repository/projectRefresh.js +58 -0
  50. package/dist/repository/taskWorkspacePreparer.js +2 -1
  51. package/dist/repository/workspaceCleanupInspection.js +6 -1
  52. package/dist/schedule/schedule.js +108 -0
  53. package/dist/scheduler/operatorEvent.js +2 -0
  54. package/dist/scheduler/operatorInputNotificationProcessor.js +8 -0
  55. package/dist/storage/contextRecords.js +8 -0
  56. package/dist/storage/jobSchema.js +15 -0
  57. package/dist/storage/maintenanceCaller.js +1 -1
  58. package/dist/storage/recordValidation.js +5 -0
  59. package/dist/storage/scheduleSchema.js +12 -0
  60. package/dist/storage/sqliteSchema.js +95 -24
  61. package/dist/storage/sqliteStore.js +123 -10
  62. package/dist/storage/storageVersions.js +1 -1
  63. package/dist/storage/storeRpc.js +5 -0
  64. package/dist/storage/upgrade/jobDefinitionMigration.js +22 -0
  65. package/dist/task/archiveDiagnostics.js +1 -1
  66. package/dist/task/archivePreflight.js +1 -1
  67. package/dist/task/projectDeliveryScope.js +20 -0
  68. package/dist/task/publicationToolObservation.js +126 -0
  69. package/dist/task/remoteDelivery.js +19 -8
  70. package/dist/task/remoteDeliveryService.js +5 -4
  71. package/dist/task/taskActivationService.js +35 -0
  72. package/dist/web/assets/client/app/settings.js +7 -1
  73. package/dist/web/assets/client/i18n/en.js +65 -2
  74. package/dist/web/assets/client/i18n/zh-CN.js +65 -2
  75. package/dist/web/assets/client/index.js +2 -0
  76. package/dist/web/assets/client/views/jobs.js +222 -0
  77. package/dist/web/assets/client/views/task/deliveryResults.js +6 -2
  78. package/dist/web/assets/client/views/task/page.js +11 -1
  79. package/dist/web/assets/shared/manageNav.js +1 -0
  80. package/dist/web/assets/shared/taskTabs.js +2 -2
  81. package/dist/web/assets/shell/settingsContent.js +3 -0
  82. package/dist/web/assets/styles/index.js +2 -0
  83. package/dist/web/assets/styles/views/jobs.js +19 -0
  84. package/dist/web/controllerWeb.js +73 -21
  85. package/dist/web/taskPreviews.js +21 -13
  86. package/dist/web/webAccess.js +109 -0
  87. package/dist/web/webJobs.js +52 -0
  88. package/dist/web/webServer.js +76 -44
  89. package/docs/architecture/capabilities-and-resources.md +6 -4
  90. package/docs/architecture/capabilities-and-resources.zh-CN.md +5 -4
  91. package/docs/cli-information-contract.md +57 -4
  92. package/docs/cli-information-contract.zh-CN.md +41 -2
  93. package/docs/job-runs.md +224 -0
  94. package/docs/project-delivery-scope.md +59 -0
  95. package/docs/release-workflow.md +86 -0
  96. package/docs/release-workflow.zh-CN.md +68 -0
  97. package/docs/schedules.md +132 -0
  98. package/docs/storage-baseline.md +31 -1
  99. package/docs/storage-baseline.zh-CN.md +25 -1
  100. package/docs/task-delivery.md +74 -2
  101. package/docs/task-delivery.zh-CN.md +39 -1
  102. package/docs/testing/verification-levels.md +6 -2
  103. package/docs/testing/verification-levels.zh-CN.md +6 -3
  104. package/i18n/README.zh-CN.md +6 -3
  105. package/package.json +4 -3
  106. package/skills/yui-leader/SKILL.md +44 -1
  107. package/skills/yui-leader/VERSION +1 -1
  108. package/skills/yui-leader/references/planning.md +9 -1
  109. package/skills/yui-operator/SKILL.md +31 -0
  110. package/skills/yui-operator/VERSION +1 -1
  111. package/skills/yui-reviewer/SKILL.md +4 -0
  112. package/skills/yui-reviewer/VERSION +1 -1
  113. package/skills/yui-runtime/SKILL.md +67 -8
  114. package/skills/yui-runtime/VERSION +1 -1
  115. package/skills/yui-runtime/references/context-ordering.md +103 -0
  116. package/skills/yui-runtime/references/publication.md +44 -4
  117. package/skills/yui-runtime/references/recovery.md +21 -0
  118. package/skills/yui-worker/SKILL.md +5 -0
  119. 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 { deliveredMessageWake, isLeaderSteer, pinMessageRevisions, taskMessageInboxReceipt, unfinishedLeaderInbox } from "../message/taskInboxOwnership.js";
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?.ack !== undefined)
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
- if (parsed.positionals.length)
178
- throw new Error("Inbox list accepts only --limit and --cursor.");
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
- && inboxHandlingState(entry.inbox) !== "handled");
183
- const items = selected.map(entry => summary(store, address, entry));
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.push({ id: pendingId(mailbox.pending), kind: "notification-window",
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), parsed, { identity: 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 === "ack" && (!reason?.trim() || Buffer.byteLength(reason) > 2048)) {
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
- || deliveredMessageWake(tx, message) !== undefined)
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 === "claim") {
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 (isWake(previous) || previous.inbox?.ack !== undefined)
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 { CliError, usageError } from "../errors/cliError.js";
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 { GitWorkspaceRefreshError, NodeGitWorkspace } from "../repository/gitWorkspace.js";
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
- assertProjectActive(project, "refresh");
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, store);
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 = requireTask(store, rest[0]);
16
- assertContextRecordReadable(store, task.id, "task-brief", task.id, options.environment);
17
- const brief = store.getTaskBrief(task.id);
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
- ].join("\n").concat("\n"), { taskId: task.id, brief });
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 events = store.listEvents(task.id);
248
- const event = events.find((e) => e.id === rest[1]) ?? null;
249
- if (event === null)
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|--verified] [--evidence <text>] [--merged-at <iso-timestamp>].";
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
- return "verified";
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