@zq-silk/yui 0.15.11 → 0.16.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 (280) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/ARCHITECTURE.zh-CN.md +8 -4
  3. package/README.md +13 -5
  4. package/dist/agentRun/agentRun.js +3 -0
  5. package/dist/artifacts/artifactCommitLock.js +16 -55
  6. package/dist/artifacts/taskArtifactRepository.js +3 -3
  7. package/dist/cli/agentConfigurationPicker.js +1 -1
  8. package/dist/cli/commandCatalog.js +92 -48
  9. package/dist/cli/completion.js +18 -18
  10. package/dist/cli/completionWizard.js +1 -1
  11. package/dist/cli/dynamicCompletion.js +1 -1
  12. package/dist/cli/interactionPolicy.js +11 -3
  13. package/dist/cli/invocationAuthority.js +64 -0
  14. package/dist/cli/managedDiagnostics.js +2 -2
  15. package/dist/cli/parseRepeatable.js +35 -0
  16. package/dist/cli/updatePorts.js +26 -5
  17. package/dist/cli.js +1303 -1235
  18. package/dist/commands/agentCommands.js +4 -4
  19. package/dist/commands/capabilityCommands.js +1 -1
  20. package/dist/commands/configCommands.js +6 -57
  21. package/dist/commands/configOverview.js +2 -2
  22. package/dist/commands/durableJobCommands.js +12 -6
  23. package/dist/commands/executionAuditCommands.js +10 -0
  24. package/dist/commands/globalRoleCommands.js +78 -29
  25. package/dist/commands/grantCommands.js +0 -3
  26. package/dist/commands/jobCommands.js +1 -4
  27. package/dist/commands/operatorCommands.js +3 -3
  28. package/dist/commands/projectCommands.js +45 -25
  29. package/dist/commands/resourcesCommands.js +10 -40
  30. package/dist/commands/roleConfiguration.js +2 -6
  31. package/dist/commands/taskActivationCommands.js +4 -8
  32. package/dist/commands/taskAgentCapabilities.js +9 -0
  33. package/dist/commands/taskCommandSupport.js +155 -0
  34. package/dist/commands/taskCommandTypes.js +1 -0
  35. package/dist/commands/taskCommands.js +355 -732
  36. package/dist/commands/taskCompletionGate.js +1 -1
  37. package/dist/commands/taskExecutionCommands.js +1 -1
  38. package/dist/commands/taskFactCommands.js +325 -0
  39. package/dist/commands/taskInputCommands.js +12 -42
  40. package/dist/commands/taskIntegrationCommands.js +20 -58
  41. package/dist/commands/taskNextActionCommand.js +7 -6
  42. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  43. package/dist/commands/taskPublicationCommands.js +12 -3
  44. package/dist/commands/taskPublicationVerifyCommand.js +24 -40
  45. package/dist/commands/taskRemoteDeliveryCommand.js +5 -86
  46. package/dist/commands/taskUpstreamCommands.js +19 -8
  47. package/dist/commands/workflowCommands.js +6 -1
  48. package/dist/completion/completionInstaller.js +26 -26
  49. package/dist/completion/completionState.js +26 -26
  50. package/dist/completion/fileCompletionManager.js +6 -11
  51. package/dist/config/configCatalog.js +0 -2
  52. package/dist/config/timeZone.js +14 -0
  53. package/dist/config/yuiConfig.js +0 -29
  54. package/dist/context/runContextPack.js +5 -3
  55. package/dist/context/taskCatalog.js +185 -0
  56. package/dist/context/taskContext.js +152 -151
  57. package/dist/context/wakeRunReferences.js +11 -0
  58. package/dist/controller/agentCapabilities.js +58 -0
  59. package/dist/controller/agentRuntimeObserver.js +6 -11
  60. package/dist/controller/clientRuntime.js +22 -14
  61. package/dist/controller/controller.js +20 -24
  62. package/dist/controller/fileSchedulerStoreAdapter.js +191 -225
  63. package/dist/controller/globalInputDelivery.js +13 -0
  64. package/dist/controller/jobClient.js +4 -4
  65. package/dist/controller/jobControl.js +60 -25
  66. package/dist/controller/jobSupervisor.js +4 -4
  67. package/dist/controller/providerRetryAdmission.js +100 -0
  68. package/dist/controller/providerRetryDelivery.js +218 -0
  69. package/dist/controller/runtime.js +85 -43
  70. package/dist/controller/runtimeEventProcessor.js +0 -5
  71. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  72. package/dist/controller/taskAgentError.js +35 -0
  73. package/dist/coordination/workMailboxQueue.js +2 -24
  74. package/dist/core/controllerClient.js +2 -2
  75. package/dist/core/fileLockOwner.js +74 -0
  76. package/dist/decision/decision.js +17 -0
  77. package/dist/doctor/doctor.js +18 -9
  78. package/dist/event/taskEvent.js +12 -0
  79. package/dist/execution/workItemExecutionProjection.js +2 -2
  80. package/dist/executor/agentCapabilityConfig.js +43 -0
  81. package/dist/executor/agentConfigurationCatalog.js +49 -11
  82. package/dist/executor/agentExecutor.js +5 -8
  83. package/dist/executor/fileRoleLaunchPlanner.js +11 -53
  84. package/dist/executor/taskAgentCapabilities.js +64 -0
  85. package/dist/integration/deliveryObligation.js +1 -69
  86. package/dist/integration/gitIntegrationService.js +79 -90
  87. package/dist/integration/integrationAttempt.js +3 -14
  88. package/dist/integration/integrationSourceApplication.js +4 -12
  89. package/dist/integration/manifestTags.js +2 -2
  90. package/dist/job/durableJob.js +4 -25
  91. package/dist/job/jobAssignmentScope.js +22 -0
  92. package/dist/job/jobOperation.js +16 -0
  93. package/dist/job/jobRunner.js +2 -1
  94. package/dist/job/stepDirectory.js +15 -0
  95. package/dist/kernel/builtinCapabilities.js +15 -11
  96. package/dist/kernel/kernelPorts.js +1 -17
  97. package/dist/lifecycle/exactRunTerminalization.js +5 -10
  98. package/dist/message/globalProviderRetry.js +15 -0
  99. package/dist/message/inputControlResolution.js +3 -4
  100. package/dist/message/message.js +17 -20
  101. package/dist/message/messageContinuation.js +1 -1
  102. package/dist/milestone/milestone.js +11 -0
  103. package/dist/observability/executionAudit.js +19 -0
  104. package/dist/observability/orchestrationMetrics.js +24 -1
  105. package/dist/observability/runtimeIdentity.js +1 -17
  106. package/dist/output/agentConfigurationPresentation.js +2 -0
  107. package/dist/output/timePresentation.js +1 -14
  108. package/dist/plugins/pluginService.js +12 -4
  109. package/dist/release/releaseWorkflowPorts.js +45 -96
  110. package/dist/release/runtimeRelease.js +9 -15
  111. package/dist/repository/gitWorkspace.js +371 -105
  112. package/dist/repository/projectMaintenanceLock.js +98 -82
  113. package/dist/repository/taskBaseFreshness.js +1 -1
  114. package/dist/repository/taskWorkspaceCoordinator.js +82 -144
  115. package/dist/repository/taskWorkspacePreparer.js +113 -47
  116. package/dist/repository/workspaceCleanupInspection.js +187 -0
  117. package/dist/resources/autoResourceGc.js +4 -77
  118. package/dist/resources/liveReferences.js +7 -54
  119. package/dist/resources/resourceDiscovery.js +5 -118
  120. package/dist/resources/resourceGc.js +233 -181
  121. package/dist/resources/resourceRegistrar.js +5 -4
  122. package/dist/resources/resourceRegistry.js +3 -37
  123. package/dist/resources/resourceRegistryStore.js +13 -36
  124. package/dist/resources/sqliteResourceRegistry.js +43 -26
  125. package/dist/review/deltaRecheck.js +1 -1
  126. package/dist/review/reviewAcceptance.js +1 -1
  127. package/dist/review/reviewDecision.js +1 -1
  128. package/dist/review/reviewRound.js +8 -7
  129. package/dist/review/taskFinalReviewContractResolution.js +1 -1
  130. package/dist/runtime/acpProtocol.js +4 -51
  131. package/dist/runtime/acpSession.js +10 -75
  132. package/dist/runtime/acpSessionConfiguration.js +1 -8
  133. package/dist/runtime/agentEndpoint.js +3 -2
  134. package/dist/runtime/agentError.js +5 -3
  135. package/dist/runtime/agentFailureContext.js +43 -0
  136. package/dist/runtime/agentHost.js +42 -25
  137. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  138. package/dist/runtime/codexAppServerRuntime.js +34 -3
  139. package/dist/runtime/codexInteractiveHost.js +2 -2
  140. package/dist/runtime/index.js +0 -1
  141. package/dist/runtime/managedCaller.js +21 -1
  142. package/dist/runtime/providerControl.js +5 -1
  143. package/dist/runtime/providerErrors.js +38 -0
  144. package/dist/runtime/providerRetry.js +198 -0
  145. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  146. package/dist/runtime/runtimeObservation.js +0 -5
  147. package/dist/runtime/runtimeSessionCandidate.js +2 -3
  148. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  149. package/dist/runtime/sessionTokenMetrics.js +15 -5
  150. package/dist/runtime/structuredProviderHost.js +10 -43
  151. package/dist/runtime/taskUsageMetrics.js +275 -0
  152. package/dist/runtime/tmuxAdapters.js +6 -8
  153. package/dist/scheduler/activeRoleRunDelivery.js +19 -7
  154. package/dist/scheduler/leaderWakeupProcessor.js +24 -6
  155. package/dist/scheduler/operatorEvent.js +12 -20
  156. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  157. package/dist/scheduler/ports.js +5 -8
  158. package/dist/scheduler/roleRunLiveness.js +4 -4
  159. package/dist/scheduler/roleRunStall.js +16 -23
  160. package/dist/scheduler/taskExecutionProjection.js +49 -36
  161. package/dist/scheduler/taskObservabilityProjection.js +6 -45
  162. package/dist/scheduler/taskWake.js +5 -10
  163. package/dist/scheduler/wakeReason.js +2 -0
  164. package/dist/scheduler/wakeupQueue.js +2 -4
  165. package/dist/setup/setupCommand.js +1 -1
  166. package/dist/storage/contextRecords.js +106 -0
  167. package/dist/storage/currentTaskStore.js +1 -1
  168. package/dist/storage/homeLayout.js +13 -17
  169. package/dist/storage/migrations/agentFailureContext.js +22 -0
  170. package/dist/storage/migrations/currentInputContract.js +86 -0
  171. package/dist/storage/migrations/currentRuntimeContract.js +228 -0
  172. package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
  173. package/dist/storage/migrations/integrationContinuation.js +5 -4
  174. package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
  175. package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
  176. package/dist/storage/migrations/verificationPlanV1.js +162 -0
  177. package/dist/storage/migrations/verificationPolicy.js +74 -0
  178. package/dist/storage/migrations/workItemHistory.js +46 -0
  179. package/dist/storage/persistenceWorker.js +12 -4
  180. package/dist/storage/recordValidation.js +87 -0
  181. package/dist/storage/sqliteSchema.js +173 -1
  182. package/dist/storage/sqliteStore.js +172 -161
  183. package/dist/storage/storageSchema.js +2 -14
  184. package/dist/storage/storageVersions.js +1 -1
  185. package/dist/storage/storeRpc.js +16 -7
  186. package/dist/storage/taskCatalog.js +123 -0
  187. package/dist/storage/taskStore.js +5 -7
  188. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
  189. package/dist/task/archiveDiagnostics.js +1 -0
  190. package/dist/task/archivePreflight.js +124 -0
  191. package/dist/task/completionReadiness.js +5 -24
  192. package/dist/task/draftPlan.js +0 -53
  193. package/dist/task/nextAction.js +7 -13
  194. package/dist/task/publicationAdoption.js +56 -0
  195. package/dist/task/publicationReference.js +10 -0
  196. package/dist/task/remoteDelivery.js +31 -16
  197. package/dist/task/remoteDeliveryService.js +89 -0
  198. package/dist/task/taskActivation.js +2 -25
  199. package/dist/task/taskActivationService.js +0 -2
  200. package/dist/task/taskRecordReference.js +0 -1
  201. package/dist/task/taskSubmission.js +3 -14
  202. package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
  203. package/dist/telemetry/sqliteTelemetryStore.js +49 -49
  204. package/dist/telemetry/telemetryWiring.js +4 -3
  205. package/dist/tmux/tmuxManager.js +29 -20
  206. package/dist/verification/gateArtifact.js +15 -24
  207. package/dist/verification/gateArtifactStore.js +14 -7
  208. package/dist/verification/verificationGateService.js +29 -88
  209. package/dist/verification/verificationPlan.js +27 -72
  210. package/dist/web/assets/client/app.js +92 -17
  211. package/dist/web/assets/client/components.js +55 -13
  212. package/dist/web/assets/client/i18n.js +72 -4
  213. package/dist/web/assets/client/taskSurface.js +6 -5
  214. package/dist/web/assets/client/view.js +35 -7
  215. package/dist/web/assets/shell.js +6 -0
  216. package/dist/web/assets/styles/layout.js +7 -0
  217. package/dist/web/webServer.js +12 -3
  218. package/dist/web/webSnapshot.js +14 -85
  219. package/dist/web/webTaskSurface.js +17 -46
  220. package/dist/workItem/workItem.js +1 -12
  221. package/dist/workspace/cleanupInspection.js +63 -0
  222. package/dist/workspace/workItemChangeSetManager.js +110 -50
  223. package/docs/agent-result-consumption.md +4 -0
  224. package/docs/agent-result-consumption.zh-CN.md +3 -0
  225. package/docs/agent-runtime-drivers.md +7 -0
  226. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  227. package/docs/architecture/README.md +2 -0
  228. package/docs/architecture/README.zh-CN.md +3 -1
  229. package/docs/architecture/capabilities-and-resources.md +42 -5
  230. package/docs/architecture/capabilities-and-resources.zh-CN.md +33 -3
  231. package/docs/managed-turn-and-session-runtime.md +121 -1
  232. package/docs/managed-turn-and-session-runtime.zh-CN.md +96 -2
  233. package/docs/observability/README.md +62 -0
  234. package/docs/observability/README.zh-CN.md +47 -0
  235. package/docs/plugin-sdk.md +4 -2
  236. package/docs/project-refresh.md +77 -0
  237. package/docs/project-refresh.zh-CN.md +59 -0
  238. package/docs/provider-retry.md +70 -0
  239. package/docs/provider-runtime.md +3 -1
  240. package/docs/provider-runtime.zh-CN.md +4 -2
  241. package/docs/release-workflow.md +190 -7
  242. package/docs/release-workflow.zh-CN.md +139 -5
  243. package/docs/roles-and-configuration.md +29 -0
  244. package/docs/roles-and-configuration.zh-CN.md +21 -0
  245. package/docs/sqlite-control-plane-design.md +16 -3
  246. package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
  247. package/docs/task-dag-semantics.md +3 -3
  248. package/docs/task-dag-semantics.zh-CN.md +2 -2
  249. package/docs/task-delivery.md +223 -17
  250. package/docs/task-delivery.zh-CN.md +171 -14
  251. package/docs/task-discovery.md +101 -0
  252. package/docs/task-discovery.zh-CN.md +85 -0
  253. package/docs/testing/verification-levels.md +161 -9
  254. package/docs/testing/verification-levels.zh-CN.md +116 -9
  255. package/i18n/README.zh-CN.md +13 -7
  256. package/package.json +1 -1
  257. package/skills/yui-leader/SKILL.md +5 -0
  258. package/skills/yui-leader/references/execution.md +3 -2
  259. package/skills/yui-leader/references/integration.md +9 -5
  260. package/skills/yui-leader/references/planning.md +9 -3
  261. package/skills/yui-operator/SKILL.md +24 -8
  262. package/skills/yui-reviewer/SKILL.md +4 -0
  263. package/skills/yui-runtime/SKILL.md +17 -2
  264. package/skills/yui-runtime/references/publication.md +26 -4
  265. package/skills/yui-runtime/references/recovery.md +71 -0
  266. package/dist/agent/agentRegistry.js +0 -10
  267. package/dist/agentRun/runIdentity.js +0 -22
  268. package/dist/commands/deliveryGuardPreflight.js +0 -30
  269. package/dist/commands/taskIntegrationQueueCommands.js +0 -226
  270. package/dist/commands/taskOverviewCommand.js +0 -376
  271. package/dist/completion/completionWizard.js +0 -125
  272. package/dist/context/dispatchContext.js +0 -110
  273. package/dist/context/wakeNotification.js +0 -144
  274. package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
  275. package/dist/integration/integrationQueueEntry.js +0 -191
  276. package/dist/integration/integrationQueueService.js +0 -810
  277. package/dist/release/fakeReleasePorts.js +0 -55
  278. package/dist/runtime/sessionOwnerRegistry.js +0 -137
  279. package/dist/task/deliveryGuard.js +0 -226
  280. /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
@@ -1,5 +1,6 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { createTaskMessage } from "../message/message.js";
3
+ import { providerRetryPending } from "../runtime/providerRetry.js";
3
4
  import { captureRoleRunDispatch, enqueueWork, settleRoleRunDispatch } from "../coordination/workMailboxQueue.js";
4
5
  import { finishReviewRound, updateReviewExecutionGroup } from "../review/reviewRound.js";
5
6
  import { completeRun, failRun, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
@@ -7,7 +8,6 @@ import { boundedRunFailureDiagnostic } from "../domain/agentResultTransport.js";
7
8
  import { updateExecutionLane, updateWorkItemExecutionLane } from "../execution/workItemExecution.js";
8
9
  import { managedProviderTurnId } from "../runtime/providerRuntimeIdentity.js";
9
10
  import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
10
- import { markTaskWakeConsumed } from "../scheduler/taskWake.js";
11
11
  import { workItemExecutionGroupById, updateWorkItemExecutionGroup } from "../workItem/workItem.js";
12
12
  /**
13
13
  * Validate every immutable identity and frozen Project head needed before a
@@ -80,7 +80,7 @@ export function validateExactRunReviewRound(store, run, options = {}) {
80
80
  }
81
81
  }
82
82
  const task = store.getTask(run.taskId);
83
- const taskScope = (round.scope ?? "work-item") === "task";
83
+ const taskScope = round.scope === "task";
84
84
  const item = taskScope || round.workItemId === undefined
85
85
  ? null
86
86
  : store.getWorkItem(run.taskId, round.workItemId);
@@ -394,7 +394,9 @@ export function terminalizeExactTaskRun(store, input, now) {
394
394
  const message = createTaskMessage(store.nextMessageId(terminal.taskId), terminal.taskId, `Execution ${terminal.id} ${terminal.status}.`, "role-result", { type: "role", roleName: terminal.roleName }, now, { resultRef: { type: "agent-run-result", runId: terminal.id }, ...(terminal.workItemId === undefined
395
395
  ? {} : { workItemId: terminal.workItemId }) });
396
396
  store.saveMessage(terminal.taskId, message);
397
- enqueueWork(store, { kind: "role", taskId: terminal.taskId, roleName: "leader" }, "role-result", now, [{ type: "message", taskId: terminal.taskId, id: message.id }], { source: "task-event", dedupeKey: `result:${terminal.taskId}/${terminal.id}` });
397
+ if (!providerRetryPending(store.getTaskRoleSessionSet(terminal.taskId, terminal.roleName)?.providerBinding)) {
398
+ enqueueWork(store, { kind: "role", taskId: terminal.taskId, roleName: "leader" }, "role-result", now, [{ type: "message", taskId: terminal.taskId, id: message.id }], { source: "task-event", dedupeKey: `result:${terminal.taskId}/${terminal.id}` });
399
+ }
398
400
  }
399
401
  const dispatchIdentity = {
400
402
  taskId: terminal.taskId,
@@ -407,13 +409,6 @@ export function terminalizeExactTaskRun(store, input, now) {
407
409
  roleName: terminal.roleName
408
410
  }), dispatchIdentity);
409
411
  settleRoleRunDispatch(store, dispatchIdentity, dispatchToken);
410
- if (terminal.roleName === "leader") {
411
- const wake = store.listTaskWakes(input.taskId)
412
- .find((candidate) => candidate.runId === terminal.id && candidate.status === "dispatched");
413
- if (wake !== undefined) {
414
- store.saveTaskWake(input.taskId, markTaskWakeConsumed(wake, now));
415
- }
416
- }
417
412
  if (terminal.executionGroupId !== undefined && terminal.executionLaneId !== undefined) {
418
413
  store.clearActiveExecutionLaneRun(input.taskId, terminal.executionGroupId, terminal.executionLaneId);
419
414
  }
@@ -0,0 +1,15 @@
1
+ import { markGlobalRoleMessageNotDelivered } from "./message.js";
2
+ /** Settle the original queue entry when recovery ends with exact nondelivery.
3
+ * Cancellation of a submitted/unknown request is not negative Provider proof. */
4
+ export function settleGlobalRetryInput(store, roleName, binding, now) {
5
+ const retry = binding?.retry;
6
+ if (retry === undefined || retry.input.kind !== "message" || retry.input.roleName !== roleName
7
+ || !["cancelled", "exhausted", "needs-attention"].includes(retry.status)
8
+ || binding?.run == null || ["submitting", "accepted", "delivery-unknown"].includes(binding.run.status))
9
+ return;
10
+ const messageId = retry.input.messageId;
11
+ const message = store.listGlobalRoleMessages(roleName).find(m => m.id === messageId);
12
+ if (message === undefined || message.delivery !== undefined || message.notDelivered !== undefined)
13
+ return;
14
+ store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(message, `provider-retry-${retry.status}: ${retry.reason ?? retry.failureRef}`, now));
15
+ }
@@ -36,9 +36,8 @@ function resolveInputControl(sessions, kind, expectedTarget, roleName) {
36
36
  detail: `Agent plan '${active.adapterId}' has no native interrupt; Yui will not `
37
37
  + "stop the owned process, kill, restart, or detach it." };
38
38
  }
39
- // A Global Role now carries the same optional providerBinding as a Task Role;
40
- // both read it the same way. Its absence is "no current Turn", never an error.
41
- const binding = "providerBinding" in sessions ? sessions.providerBinding ?? null : null;
39
+ // Both scopes use the same explicit null for an uncontrolled Session.
40
+ const binding = sessions.providerBinding;
42
41
  const turn = binding?.run ?? null;
43
42
  if (binding === null || turn === null || !PRESENT_TURN_STATUSES.has(turn.status)) {
44
43
  return { outcome: "no-active-turn", code: "NO_ACTIVE_TURN",
@@ -78,7 +77,7 @@ function resolveInputControl(sessions, kind, expectedTarget, roleName) {
78
77
  ...(turn.nativeTurnId === undefined ? {} : { nativeTurnId: turn.nativeTurnId }),
79
78
  attemptId: turn.attemptId,
80
79
  authority: { epoch: binding.authority.epoch, owner: "controller",
81
- holderId: binding.authority.holderId ?? "controller" }
80
+ holderId: binding.authority.holderId }
82
81
  } };
83
82
  }
84
83
  /**
@@ -5,7 +5,7 @@ export const TASK_MESSAGE_KINDS = ["user", "operator", "role-result", "system"];
5
5
  *
6
6
  * - `record`: save only. No Leader wake, no planning, no activation.
7
7
  * - `discuss`: save and route to planning (the default when a submission omits
8
- * an intent, so an old client keeps its existing Leader-waking behaviour).
8
+ * an intent).
9
9
  * - `develop`: save the requirement and its activation intent; an unplanned
10
10
  * Draft records an activation request and queues only activation processing.
11
11
  *
@@ -30,12 +30,9 @@ export function createTaskMessage(id, taskId, body, kind, author, now, context =
30
30
  kind,
31
31
  author: normalizeAuthor(author),
32
32
  body: requireBody(body),
33
- ...(context.wakePolicy === undefined
34
- ? {}
35
- : { wakePolicy: context.wakePolicy }),
36
- ...(context.intent === undefined
37
- ? {}
38
- : { intent: context.intent }),
33
+ ...(kind === "user" || kind === "operator"
34
+ ? { intent: context.intent ?? "discuss" }
35
+ : context.intent === undefined ? {} : { intent: context.intent }),
39
36
  ...(context.submissionKey === undefined
40
37
  ? {}
41
38
  : { submissionKey: requireText(context.submissionKey, "Message submission key") }),
@@ -163,18 +160,22 @@ export function expandTaskMessageResult(message, getRun) {
163
160
  }
164
161
  return { ...message, result: run.result };
165
162
  }
166
- /** Replace only the mutable content of a Draft user/operator Message. */
163
+ /** Edit unkeyed Draft context. Once an input has a request identity, its body
164
+ * is the retry comparison and must stay frozen, including through a handoff. */
167
165
  export function updateDraftTaskMessage(message, update) {
168
166
  validateTaskMessage(message);
169
167
  if (message.kind !== "user" && message.kind !== "operator") {
170
168
  throw new Error(`Only user/operator Task Messages can be updated: ${message.id}.`);
171
169
  }
170
+ const body = requireBody(update.body);
171
+ if (body === message.body)
172
+ return message;
173
+ if (message.submissionKey !== undefined || message.inputControl !== undefined || message.interruptThen !== undefined) {
174
+ throw new Error(`Message ${message.id} has a request identity; submit a new Message with a new request ID to change its content.`);
175
+ }
172
176
  const updated = {
173
177
  ...message,
174
- body: requireBody(update.body),
175
- ...(update.wakePolicy === undefined
176
- ? {}
177
- : { wakePolicy: update.wakePolicy })
178
+ body
178
179
  };
179
180
  validateTaskMessage(updated);
180
181
  return updated;
@@ -204,15 +205,11 @@ export function validateTaskMessage(message) {
204
205
  requireText(message.body, "Message body");
205
206
  validateKindAndAuthor(message.kind, message.author);
206
207
  normalizeAuthor(message.author);
207
- if (message.wakePolicy !== undefined
208
- && message.wakePolicy !== "leader"
209
- && message.wakePolicy !== "none") {
210
- throw new Error(`Message wakePolicy is invalid: ${String(message.wakePolicy)}.`);
208
+ if (Object.hasOwn(message, "wakePolicy")) {
209
+ throw new Error("Message contains a retired wake policy; use the storage upgrade boundary.");
211
210
  }
212
- if (message.wakePolicy !== undefined
213
- && message.kind !== "user"
214
- && message.kind !== "operator") {
215
- throw new Error("Message wakePolicy is only valid for user/operator messages.");
211
+ if ((message.kind === "user" || message.kind === "operator") && message.intent === undefined) {
212
+ throw new Error("User/operator Message intent is required.");
216
213
  }
217
214
  if (message.intent !== undefined
218
215
  && !TASK_SUBMISSION_INTENTS.includes(message.intent)) {
@@ -80,7 +80,7 @@ export function messageContinuationBlocker(store, message) {
80
80
  const round = store.getReviewRound(message.taskId, target.reviewRoundId);
81
81
  if (round === null)
82
82
  return "review-round-missing";
83
- if ((round.scope ?? "work-item") === "task") {
83
+ if (round.scope === "task") {
84
84
  if (round.taskCandidate?.projects.some((project) => task.projectBindings.find((binding) => binding.projectId === project.projectId)?.currentCommit !== project.commit)) {
85
85
  return "review-candidate-stale";
86
86
  }
@@ -1,4 +1,15 @@
1
1
  import { validateTaskRecordReference } from "../task/taskRecordReference.js";
2
+ import { requireTimestamp } from "../domain/validation.js";
3
+ export function validateMilestone(milestone) {
4
+ if (milestone.schemaVersion !== 2)
5
+ throw new Error("Milestone must use schemaVersion 2.");
6
+ validateTaskRecordReference({ taskId: milestone.taskId, localId: milestone.id }, "milestone");
7
+ requireText(milestone.title, "Milestone title");
8
+ requireText(milestone.summary, "Milestone summary");
9
+ requireTaskControlActor(milestone.createdBy);
10
+ requireTimestamp(milestone.createdAt, "Milestone createdAt");
11
+ return milestone;
12
+ }
2
13
  export function createMilestone(id, taskId, title, summary, createdBy, now) {
3
14
  return {
4
15
  schemaVersion: 2,
@@ -15,6 +15,7 @@ import { join } from "node:path";
15
15
  import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
16
16
  import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
17
17
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
18
+ import { projectTaskUsageMetrics } from "../runtime/taskUsageMetrics.js";
18
19
  import { classifyRunFailure, classifyIntegrationAttempt, classifyReviewRound, countFaultClasses } from "./faultClassification.js";
19
20
  import { RUNTIME_LAUNCH_KINDS, RUNTIME_LAUNCH_PHASES } from "../runtime/launchDiagnostics.js";
20
21
  import { UNSUPPORTED } from "./runtimeIdentity.js";
@@ -192,6 +193,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
192
193
  agentErrors: section,
193
194
  workItems: section,
194
195
  orchestration: section,
196
+ usage: section,
195
197
  storage: section,
196
198
  runtimeProtocol: section,
197
199
  topLongRunning: section
@@ -658,6 +660,22 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
658
660
  return failed(error);
659
661
  }
660
662
  })();
663
+ const usage = (() => {
664
+ try {
665
+ return ok(taskIds.map((taskId) => {
666
+ const task = store.getTask(taskId);
667
+ if (task === null)
668
+ throw new Error(`Task not found: ${taskId}.`);
669
+ return projectTaskUsageMetrics({
670
+ task, events: store.listEvents(taskId), runs: store.listRuns(taskId),
671
+ now: new Date(generatedAt)
672
+ });
673
+ }));
674
+ }
675
+ catch (error) {
676
+ return failed(error);
677
+ }
678
+ })();
661
679
  const storage = (() => {
662
680
  try {
663
681
  let stateJsonBytes = UNSUPPORTED;
@@ -796,6 +814,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
796
814
  agentErrors,
797
815
  workItems,
798
816
  orchestration,
817
+ usage,
799
818
  storage,
800
819
  runtimeProtocol,
801
820
  topLongRunning
@@ -1,6 +1,7 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { isCompletedReviewExecutionFromRuns } from "../review/reviewAcceptance.js";
3
3
  import { projectFirstProgressAdvisory } from "../runtime/firstProgressAdvisory.js";
4
+ import { parseGateArtifactRef } from "../verification/gateArtifact.js";
4
5
  /** One Task's orchestration cost and advisory projection, with no writes. */
5
6
  export function projectTaskOrchestration(facts) {
6
7
  const fullRounds = facts.reviewRounds.filter((round) => round.deltaRecheck === undefined);
@@ -63,7 +64,7 @@ export function projectTaskOrchestration(facts) {
63
64
  attempts: facts.integrations.length,
64
65
  failed: facts.integrations.filter(({ status }) => status === "failed").length,
65
66
  repeatedIdentities,
66
- evidenceReuses: facts.integrations.filter((attempt) => ((attempt.checks ?? []).some(({ details }) => details?.startsWith("Reused successful check evidence from ")))).length
67
+ evidenceReuses: facts.integrations.filter((attempt) => ((attempt.checks ?? []).some(check => check.outcome === "passed" && parseGateArtifactRef(check.name) !== undefined))).length
67
68
  },
68
69
  providerSessionsBeforeFirstProgress: firstProgress.sessionsBeforeFirstProgress,
69
70
  publicationToCompletionMs: publicationAt === undefined || facts.task.completedAt === undefined
@@ -75,6 +76,28 @@ export function projectTaskOrchestration(facts) {
75
76
  }
76
77
  function projectAdvisories(facts, fullRounds, repeatedIdentities, firstProgressAttention) {
77
78
  const result = [];
79
+ const scopes = new Map();
80
+ for (const item of facts.workItems) {
81
+ if (item.status !== "open")
82
+ continue;
83
+ const key = JSON.stringify([
84
+ item.title.trim().toLowerCase(), item.objective.trim().toLowerCase(),
85
+ item.acceptance.map(value => value.trim().toLowerCase()).filter(Boolean).sort(),
86
+ [...item.writeProjectIds].sort()
87
+ ]);
88
+ const refs = scopes.get(key) ?? [];
89
+ refs.push(`work-item:${item.id}`);
90
+ scopes.set(key, refs);
91
+ }
92
+ for (const refs of scopes.values()) {
93
+ if (refs.length < 2)
94
+ continue;
95
+ result.push({
96
+ code: "work-item-scope-overlap",
97
+ reason: "Open WorkItems have matching scope text. Inspect their original intent and decide whether they are independent; this advisory grants or denies no operation.",
98
+ refs
99
+ });
100
+ }
78
101
  if (facts.task.type === "bugfix" && facts.workItems.length > 0) {
79
102
  result.push({
80
103
  code: "bugfix-workitem-overhead",
@@ -17,22 +17,6 @@ import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "../stora
17
17
  import { inspectStorageSchema } from "../storage/storageSchema.js";
18
18
  import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
19
19
  export const UNSUPPORTED = "unsupported";
20
- /**
21
- * Feature flag for the Issue 11 read-only status/audit presentation.
22
- *
23
- * Everything behind this flag is observe-only: identity fields, storage
24
- * evidence, and metrics snapshots. The flag defaults ON because the fields
25
- * change no behavior; set `YUI_STATUS_IDENTITY=0` (or `false`) to hide the new
26
- * sections and the fail-closed health exit. `execution audit` is an explicit
27
- * command and is not gated by this flag.
28
- */
29
- export function resolveStatusIdentityEnabled(env = process.env) {
30
- const value = env.YUI_STATUS_IDENTITY;
31
- if (value === undefined)
32
- return true;
33
- const normalized = value.trim().toLowerCase();
34
- return normalized !== "0" && normalized !== "false" && normalized !== "off";
35
- }
36
20
  export function evaluateStorageHealth(identity) {
37
21
  const contradictions = identity.findings.filter(({ severity }) => severity === "contradiction");
38
22
  const needsRepair = identity.findings.filter(({ severity }) => severity === "needs-repair");
@@ -242,7 +226,7 @@ export function collectStorageIdentity(home, ports = createProductionStorageIden
242
226
  code: "ignored-legacy-storage-manifest",
243
227
  severity: "warning",
244
228
  message: "schema.json is legacy metadata and is not a storage authority.",
245
- remediation: "A successful storage upgrade removes it automatically."
229
+ remediation: "This file is not used. Preserve or archive it separately after inspecting its contents."
246
230
  });
247
231
  }
248
232
  if (dbPresent && dbHealth !== null && dbHealth !== "ok") {
@@ -1,3 +1,4 @@
1
+ import { agentModelOptionsSummary } from "../executor/agentConfigurationCatalog.js";
1
2
  import { renderAgentConfigurationResolutionNotice } from "../cli/agentConfigurationPicker.js";
2
3
  import { agentHandshakeLine } from "./agentRunConfigurationPresentation.js";
3
4
  import { defaultTableWidth, renderTable } from "./table.js";
@@ -35,6 +36,7 @@ export function renderAgentConfigurationCatalog(resolved) {
35
36
  { header: "Default effort", minWidth: 14, maxWidth: 18 },
36
37
  { header: "Service tiers", minWidth: 13, maxWidth: 24 }
37
38
  ], modelRows, defaultTableWidth()),
39
+ agentModelOptionsSummary(catalog),
38
40
  ...(fieldRows.length === 0
39
41
  ? []
40
42
  : [renderTable("Other runtime configuration", [
@@ -1,17 +1,4 @@
1
- export const DEFAULT_TIME_ZONE = "Asia/Shanghai";
2
- export function resolveTimeZone(value) {
3
- const timeZone = value ?? DEFAULT_TIME_ZONE;
4
- if (typeof timeZone !== "string" || timeZone.trim() !== timeZone || timeZone.length === 0) {
5
- throw new TypeError("timeZone must be a valid IANA timezone.");
6
- }
7
- try {
8
- new Intl.DateTimeFormat("en", { timeZone }).format(0);
9
- }
10
- catch {
11
- throw new TypeError("timeZone must be a valid IANA timezone.");
12
- }
13
- return timeZone;
14
- }
1
+ import { resolveTimeZone } from "../config/timeZone.js";
15
2
  /** Formats persisted UTC/RFC 3339 timestamps for human-facing CLI output. */
16
3
  export function formatTimestamp(value, configuredTimeZone) {
17
4
  const instant = new Date(value);
@@ -354,10 +354,18 @@ export function createPluginService(store, host, registry) {
354
354
  // recorded on the plugin's own failure channel — not a Task product
355
355
  // artifact (product artifacts are files in the Task's Git repository).
356
356
  try {
357
- store.recordPluginIntentFailure(taskId, retired.ref.id, intent.revision, {
358
- message: `Plugin instance cleanup failed for ${retired.ref.id}/${retired.ref.generation}: `
359
- + (error instanceof Error ? error.message : String(error)),
360
- occurredAt: new Date().toISOString()
357
+ store.transaction(tx => {
358
+ const current = tx.getPluginIntent(taskId, pkg.manifest.id);
359
+ if (current === null)
360
+ return;
361
+ // A draining generation can fail after another replacement.
362
+ // Attach this diagnostic to the current selection, without
363
+ // changing it or treating its activation as a failure.
364
+ tx.recordPluginIntentFailure(taskId, pkg.manifest.id, current.revision, {
365
+ message: `Plugin instance cleanup failed for ${retired.ref.id}/${retired.ref.generation}: `
366
+ + (error instanceof Error ? error.message : String(error)),
367
+ occurredAt: new Date().toISOString()
368
+ });
361
369
  });
362
370
  }
363
371
  catch { /* best-effort telemetry; never mask the successful activation */ }
@@ -1,17 +1,17 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { chmod, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
3
- import { dirname, join, resolve } from "node:path";
4
- import { releaseWorkflowScratchRoot } from "../storage/homeLayout.js";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
4
  import { runUpdate } from "../cli/updateOrchestrator.js";
6
5
  import { activatedControllerEntrypoint } from "../cli/updatePorts.js";
7
- import { restartFileTaskController, stopFileTaskController } from "../controller/clientRuntime.js";
8
6
  import { runProjectCommand } from "../commands/projectCommands.js";
9
- import { createFileReleaseIdempotencyStore } from "./releaseIdempotencyStore.js";
7
+ import { restartFileTaskController, stopFileTaskController } from "../controller/clientRuntime.js";
10
8
  import { isConcreteVersion } from "../domain/validation.js";
9
+ import { createExecFileCommandRunner, createPinnedCommandRunner, resolveExecutable } from "../external/pinnedCommandRunner.js";
11
10
  import { resolveProject } from "../repository/project.js";
12
- import { resolveVerificationGate } from "../verification/verificationGateService.js";
11
+ import { releaseWorkflowScratchRoot } from "../storage/homeLayout.js";
13
12
  import { findL2ArtifactForCommit } from "../verification/gateArtifactStore.js";
14
- import { createExecFileCommandRunner, createPinnedCommandRunner, resolveExecutable } from "../external/pinnedCommandRunner.js";
13
+ import { resolveVerificationGate } from "../verification/verificationGateService.js";
14
+ import { createFileReleaseIdempotencyStore } from "./releaseIdempotencyStore.js";
15
15
  export { resolveExecutable } from "../external/pinnedCommandRunner.js";
16
16
  const defaultRunCommand = createExecFileCommandRunner();
17
17
  /**
@@ -41,8 +41,8 @@ export function createPinnedRunner(base) {
41
41
  return createPinnedCommandRunner(base, PINNED_EXTERNAL_COMMANDS);
42
42
  }
43
43
  /**
44
- * The real adapter. It is NEVER exercised by the deterministic test suite
45
- * (fakes are); every branch dispatches to an existing atomic operation.
44
+ * The production adapter dispatches existing atomic operations. Deterministic
45
+ * tests inject command/idempotency ports and never contact release services.
46
46
  */
47
47
  export function createReleaseWorkflowPorts(deps) {
48
48
  const run = deps.runCommand !== undefined
@@ -1032,37 +1032,12 @@ export function createReleaseWorkflowPorts(deps) {
1032
1032
  return { state: "unknown" };
1033
1033
  }
1034
1034
  case "controller-home": {
1035
- // P1-2 (rr22): the identity value is a JSON envelope
1036
- // {home, globalPrefix?} that pins the exact activation target;
1037
- // legacy identities are bare Home path strings and re-derive the
1038
- // prefix from the caller's environment.
1039
- let queriedHome = deps.home;
1040
- let pinnedPrefix;
1035
+ // Recovery uses the exact persisted activation target, never the
1036
+ // resume caller's current installation.
1041
1037
  const parsedIdentity = parseControllerHomeIdentity(externalIdentity.value);
1042
- if (parsedIdentity !== undefined) {
1043
- if (parsedIdentity.globalPrefix === undefined)
1044
- return { state: "unknown" };
1045
- queriedHome = parsedIdentity.home;
1046
- pinnedPrefix = parsedIdentity.globalPrefix;
1047
- }
1048
- else {
1049
- queriedHome = externalIdentity.value;
1050
- }
1051
- // P1-3 (rr20): Query the GLOBAL install target — the same binary
1052
- // `cli-update` activates via `npm install --global` — not the
1053
- // current checkout module. A checkout that happens to be the target
1054
- // version must not confirm a global install that is stale or never
1055
- // activated. A pinned prefix is used as-is; only legacy/unpinned
1056
- // identities re-derive via `npm prefix --global`, never PATH.
1057
- let globalPrefix = pinnedPrefix;
1058
- if (globalPrefix === undefined) {
1059
- const prefixResult = await run("npm", ["prefix", "--global"]);
1060
- if (prefixResult.code !== 0)
1061
- return { state: "unknown" };
1062
- globalPrefix = prefixResult.stdout.trim();
1063
- if (globalPrefix.length === 0)
1064
- return { state: "unknown" };
1065
- }
1038
+ if (parsedIdentity === undefined)
1039
+ return { state: "unknown" };
1040
+ const { home: queriedHome, globalPrefix } = parsedIdentity;
1066
1041
  const globalYui = join(globalPrefix, "bin", "yui");
1067
1042
  const homeEnv = { YUI_HOME: queriedHome };
1068
1043
  const checked = await run(process.execPath, [globalYui, "--json", "doctor"], undefined, homeEnv);
@@ -1092,13 +1067,6 @@ export function createReleaseWorkflowPorts(deps) {
1092
1067
  }
1093
1068
  };
1094
1069
  }
1095
- /**
1096
- * Finds the open PR for a head ref. Prefers `gh pr view --head`; older gh
1097
- * versions that lack the flag fall back to `gh pr list --head`. A non-zero
1098
- * exit from both commands is a failed lookup (transport error), not an
1099
- * authoritative "no PR found". Both paths request the head object id so the
1100
- * caller can prove the PR names the frozen source commit before reuse.
1101
- */
1102
1070
  /**
1103
1071
  * Resolves the SHA a remote head branch currently points at, or undefined
1104
1072
  * when the branch cannot be resolved or the answer is not a 40-hex commit.
@@ -1122,55 +1090,40 @@ async function resolveRemoteHead(run, repo, head) {
1122
1090
  return /^[0-9a-f]{40}$/i.test(sha) ? sha : undefined;
1123
1091
  }
1124
1092
  async function findHeadPullRequest(run, head, repo) {
1125
- const viewed = await run("gh", [
1126
- "pr", "view", "--head", head,
1127
- "--repo", repo,
1128
- "--json", "number,headRefOid"
1129
- ]);
1130
- if (viewed.code === 0) {
1131
- const found = parsePrHeadEntry(viewed.stdout);
1132
- if (found !== undefined) {
1133
- return { status: "found", prNumber: found.prNumber, headSha: found.headSha };
1134
- }
1135
- }
1136
- // A non-zero exit from `gh pr view` may mean "no PR for head" (exit 1 with
1137
- // empty stdout) or a transport failure. The list fallback disambiguates.
1093
+ // One documented query. Two matches are enough to prove ambiguity; never
1094
+ // choose an arbitrary PR or mistake malformed output for authoritative absence.
1138
1095
  const listed = await run("gh", [
1139
1096
  "pr", "list", "--head", head,
1140
1097
  "--repo", repo,
1141
1098
  "--state", "open",
1099
+ "--limit", "2",
1142
1100
  "--json", "number,headRefOid"
1143
1101
  ]);
1144
- if (listed.code === 0) {
1145
- const found = parsePrHeadList(listed.stdout);
1146
- if (found !== undefined) {
1147
- return { status: "found", prNumber: found.prNumber, headSha: found.headSha };
1148
- }
1149
- // Authoritative empty result: no open PR for this head.
1150
- return { status: "not-found" };
1102
+ if (listed.code !== 0) {
1103
+ return { status: "failed", error: listed.stderr.trim() || "gh PR lookup failed" };
1151
1104
  }
1152
- // Both commands failed: transport error, not "no PR found".
1153
- return {
1154
- status: "failed",
1155
- error: listed.stderr.trim() || viewed.stderr.trim() || "gh PR lookup failed"
1156
- };
1157
- }
1158
- /** Parses `gh pr view --json number,headRefOid` output. */
1159
- function parsePrHeadEntry(stdout) {
1105
+ let entries;
1160
1106
  try {
1161
- return prHeadFromJson(JSON.parse(stdout.trim()));
1107
+ entries = JSON.parse(listed.stdout);
1162
1108
  }
1163
1109
  catch {
1164
- return undefined;
1110
+ return { status: "failed", error: "gh PR list returned invalid JSON." };
1165
1111
  }
1112
+ if (!Array.isArray(entries))
1113
+ return { status: "failed", error: "gh PR list did not return an array." };
1114
+ if (entries.length === 0)
1115
+ return { status: "not-found" };
1116
+ if (entries.length !== 1)
1117
+ return { status: "failed", error: "Multiple open PRs match this head; select the intended PR explicitly." };
1118
+ const found = prHeadFromJson(entries[0]);
1119
+ return found === undefined
1120
+ ? { status: "failed", error: "gh PR list returned an invalid PR identity." }
1121
+ : { status: "found", prNumber: found.prNumber, headSha: found.headSha };
1166
1122
  }
1167
- /** Parses the first entry of `gh pr list --json number,headRefOid` output. */
1168
- function parsePrHeadList(stdout) {
1123
+ /** Parses `gh pr view --json number,headRefOid` output. */
1124
+ function parsePrHeadEntry(stdout) {
1169
1125
  try {
1170
- const parsed = JSON.parse(stdout.trim());
1171
- if (!Array.isArray(parsed) || parsed.length === 0)
1172
- return undefined;
1173
- return prHeadFromJson(parsed[0]);
1126
+ return prHeadFromJson(JSON.parse(stdout.trim()));
1174
1127
  }
1175
1128
  catch {
1176
1129
  return undefined;
@@ -1180,9 +1133,9 @@ function prHeadFromJson(value) {
1180
1133
  if (typeof value !== "object" || value === null)
1181
1134
  return undefined;
1182
1135
  const record = value;
1183
- if (typeof record.number !== "number" || !Number.isFinite(record.number))
1136
+ if (typeof record.number !== "number" || !Number.isSafeInteger(record.number) || record.number < 1)
1184
1137
  return undefined;
1185
- if (typeof record.headRefOid !== "string" || record.headRefOid.length === 0)
1138
+ if (typeof record.headRefOid !== "string" || !/^[0-9a-f]{40}$/iu.test(record.headRefOid))
1186
1139
  return undefined;
1187
1140
  return { prNumber: String(record.number), headSha: record.headRefOid };
1188
1141
  }
@@ -1283,7 +1236,7 @@ async function queryIdentityFor(step, params, effect, source, home, run) {
1283
1236
  * idempotency/identity persistence). Only kinds whose identity is fully
1284
1237
  * determined by the immutable plan + source can be recovered this way.
1285
1238
  */
1286
- function deriveIdentityFromPlan(step, source) {
1239
+ function deriveIdentityFromPlan(step, _source) {
1287
1240
  switch (step.kind) {
1288
1241
  case "version-tag": {
1289
1242
  const tag = step.params?.tag;
@@ -1410,11 +1363,10 @@ async function resolveGlobalPrefix(run) {
1410
1363
  }
1411
1364
  /**
1412
1365
  * The controller-home query identity for a cli-update effect. The value is a
1413
- * JSON envelope carrying the Home and — when resolvable — the exact global
1366
+ * JSON envelope carrying the Home and the exact global
1414
1367
  * prefix that was activated, so a resume query checks the same installation
1415
1368
  * instead of re-deriving the target from the caller's npm/PATH environment
1416
- * (P1-2, rr22). Legacy identities are bare Home path strings; the query
1417
- * accepts both shapes via {@link parseControllerHomeIdentity}.
1369
+ * (P1-2, rr22). Without that evidence no identity is issued.
1418
1370
  */
1419
1371
  async function controllerHomeIdentity(run, home) {
1420
1372
  const globalPrefix = await resolveGlobalPrefix(run);
@@ -1471,7 +1423,7 @@ async function readPersistedCliUpdateIdentity(home, idempotencyKey) {
1471
1423
  return undefined;
1472
1424
  }
1473
1425
  const parsed = parseControllerHomeIdentity(raw);
1474
- if (parsed === undefined || parsed.globalPrefix === undefined)
1426
+ if (parsed === undefined)
1475
1427
  return undefined;
1476
1428
  return { kind: "controller-home", value: JSON.stringify(parsed) };
1477
1429
  }
@@ -1550,22 +1502,19 @@ async function readPersistedNpmPublishTarget(home, idempotencyKey) {
1550
1502
  }
1551
1503
  }
1552
1504
  /**
1553
- * Parse a controller-home identity value. Accepts the JSON envelope
1554
- * {home, globalPrefix?} written by {@link controllerHomeIdentity}; a legacy
1555
- * bare Home path string (or any unparseable value) yields undefined so the
1556
- * caller treats the raw value as the Home.
1505
+ * Read the exact activation target. Missing or malformed identity remains
1506
+ * unknown; no alternative installation can attest the original effect.
1557
1507
  */
1558
1508
  function parseControllerHomeIdentity(value) {
1559
1509
  try {
1560
1510
  const parsed = JSON.parse(value);
1561
- if (!isRecord(parsed) || typeof parsed.home !== "string" || parsed.home.length === 0) {
1511
+ if (!isRecord(parsed) || typeof parsed.home !== "string" || !isAbsolute(parsed.home)
1512
+ || typeof parsed.globalPrefix !== "string" || !isAbsolute(parsed.globalPrefix)) {
1562
1513
  return undefined;
1563
1514
  }
1564
1515
  return {
1565
1516
  home: parsed.home,
1566
- ...(typeof parsed.globalPrefix === "string" && parsed.globalPrefix.length > 0
1567
- ? { globalPrefix: parsed.globalPrefix }
1568
- : {})
1517
+ globalPrefix: parsed.globalPrefix
1569
1518
  };
1570
1519
  }
1571
1520
  catch {