@zq-silk/yui 0.15.12 → 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 (236) hide show
  1. package/ARCHITECTURE.md +3 -2
  2. package/ARCHITECTURE.zh-CN.md +3 -2
  3. package/dist/artifacts/artifactCommitLock.js +16 -55
  4. package/dist/artifacts/taskArtifactRepository.js +3 -3
  5. package/dist/cli/agentConfigurationPicker.js +1 -1
  6. package/dist/cli/commandCatalog.js +57 -41
  7. package/dist/cli/completion.js +18 -18
  8. package/dist/cli/completionWizard.js +1 -1
  9. package/dist/cli/dynamicCompletion.js +1 -1
  10. package/dist/cli/interactionPolicy.js +7 -3
  11. package/dist/cli/invocationAuthority.js +64 -0
  12. package/dist/cli/managedDiagnostics.js +1 -1
  13. package/dist/cli/parseRepeatable.js +35 -0
  14. package/dist/cli/updatePorts.js +26 -5
  15. package/dist/cli.js +1280 -1243
  16. package/dist/commands/agentCommands.js +4 -4
  17. package/dist/commands/capabilityCommands.js +1 -1
  18. package/dist/commands/configCommands.js +6 -57
  19. package/dist/commands/configOverview.js +2 -2
  20. package/dist/commands/durableJobCommands.js +12 -6
  21. package/dist/commands/globalRoleCommands.js +53 -29
  22. package/dist/commands/grantCommands.js +0 -3
  23. package/dist/commands/jobCommands.js +1 -4
  24. package/dist/commands/operatorCommands.js +3 -3
  25. package/dist/commands/projectCommands.js +1 -10
  26. package/dist/commands/resourcesCommands.js +10 -40
  27. package/dist/commands/roleConfiguration.js +2 -6
  28. package/dist/commands/taskActivationCommands.js +4 -8
  29. package/dist/commands/taskAgentCapabilities.js +9 -0
  30. package/dist/commands/taskCommandSupport.js +155 -0
  31. package/dist/commands/taskCommandTypes.js +1 -0
  32. package/dist/commands/taskCommands.js +271 -710
  33. package/dist/commands/taskCompletionGate.js +1 -1
  34. package/dist/commands/taskExecutionCommands.js +1 -1
  35. package/dist/commands/taskFactCommands.js +325 -0
  36. package/dist/commands/taskInputCommands.js +12 -42
  37. package/dist/commands/taskIntegrationCommands.js +17 -57
  38. package/dist/commands/taskNextActionCommand.js +7 -6
  39. package/dist/commands/taskPublicationAdoptCommand.js +3 -3
  40. package/dist/commands/taskPublicationCommands.js +1 -1
  41. package/dist/commands/taskPublicationVerifyCommand.js +1 -1
  42. package/dist/commands/taskRemoteDeliveryCommand.js +2 -94
  43. package/dist/commands/taskUpstreamCommands.js +19 -8
  44. package/dist/commands/workflowCommands.js +6 -1
  45. package/dist/completion/completionInstaller.js +26 -26
  46. package/dist/completion/completionState.js +26 -26
  47. package/dist/completion/fileCompletionManager.js +6 -11
  48. package/dist/config/configCatalog.js +0 -2
  49. package/dist/config/timeZone.js +14 -0
  50. package/dist/config/yuiConfig.js +0 -29
  51. package/dist/context/runContextPack.js +2 -3
  52. package/dist/context/taskCatalog.js +3 -5
  53. package/dist/context/taskContext.js +139 -151
  54. package/dist/context/wakeRunReferences.js +11 -0
  55. package/dist/controller/agentCapabilities.js +58 -0
  56. package/dist/controller/agentRuntimeObserver.js +6 -11
  57. package/dist/controller/clientRuntime.js +22 -14
  58. package/dist/controller/controller.js +9 -22
  59. package/dist/controller/fileSchedulerStoreAdapter.js +117 -212
  60. package/dist/controller/jobClient.js +4 -4
  61. package/dist/controller/jobControl.js +60 -25
  62. package/dist/controller/jobSupervisor.js +4 -4
  63. package/dist/controller/providerRetryDelivery.js +7 -7
  64. package/dist/controller/runtime.js +51 -44
  65. package/dist/controller/runtimeEventProcessor.js +0 -5
  66. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  67. package/dist/controller/taskAgentError.js +35 -0
  68. package/dist/coordination/workMailboxQueue.js +2 -24
  69. package/dist/core/controllerClient.js +2 -2
  70. package/dist/core/fileLockOwner.js +74 -0
  71. package/dist/decision/decision.js +17 -0
  72. package/dist/doctor/doctor.js +18 -9
  73. package/dist/event/taskEvent.js +12 -0
  74. package/dist/execution/workItemExecutionProjection.js +2 -2
  75. package/dist/executor/agentCapabilityConfig.js +43 -0
  76. package/dist/executor/agentConfigurationCatalog.js +49 -11
  77. package/dist/executor/agentExecutor.js +5 -8
  78. package/dist/executor/fileRoleLaunchPlanner.js +11 -53
  79. package/dist/executor/taskAgentCapabilities.js +64 -0
  80. package/dist/integration/deliveryObligation.js +1 -69
  81. package/dist/integration/gitIntegrationService.js +60 -84
  82. package/dist/integration/integrationAttempt.js +3 -14
  83. package/dist/integration/integrationSourceApplication.js +4 -12
  84. package/dist/integration/manifestTags.js +2 -2
  85. package/dist/job/durableJob.js +4 -25
  86. package/dist/job/jobAssignmentScope.js +22 -0
  87. package/dist/job/jobOperation.js +16 -0
  88. package/dist/job/jobRunner.js +2 -1
  89. package/dist/job/stepDirectory.js +15 -0
  90. package/dist/kernel/builtinCapabilities.js +15 -11
  91. package/dist/kernel/kernelPorts.js +1 -17
  92. package/dist/lifecycle/exactRunTerminalization.js +1 -9
  93. package/dist/message/inputControlResolution.js +3 -4
  94. package/dist/message/message.js +17 -20
  95. package/dist/message/messageContinuation.js +1 -1
  96. package/dist/milestone/milestone.js +11 -0
  97. package/dist/observability/orchestrationMetrics.js +24 -1
  98. package/dist/observability/runtimeIdentity.js +1 -17
  99. package/dist/output/agentConfigurationPresentation.js +2 -0
  100. package/dist/output/timePresentation.js +1 -14
  101. package/dist/plugins/pluginService.js +12 -4
  102. package/dist/release/releaseWorkflowPorts.js +45 -96
  103. package/dist/release/runtimeRelease.js +9 -15
  104. package/dist/repository/projectMaintenanceLock.js +23 -64
  105. package/dist/repository/taskBaseFreshness.js +1 -1
  106. package/dist/repository/taskWorkspaceCoordinator.js +13 -22
  107. package/dist/repository/taskWorkspacePreparer.js +29 -25
  108. package/dist/repository/workspaceCleanupInspection.js +5 -5
  109. package/dist/resources/autoResourceGc.js +4 -77
  110. package/dist/resources/liveReferences.js +7 -54
  111. package/dist/resources/resourceDiscovery.js +5 -118
  112. package/dist/resources/resourceGc.js +233 -181
  113. package/dist/resources/resourceRegistrar.js +5 -4
  114. package/dist/resources/resourceRegistry.js +3 -37
  115. package/dist/resources/resourceRegistryStore.js +13 -36
  116. package/dist/resources/sqliteResourceRegistry.js +43 -26
  117. package/dist/review/deltaRecheck.js +1 -1
  118. package/dist/review/reviewAcceptance.js +1 -1
  119. package/dist/review/reviewDecision.js +1 -1
  120. package/dist/review/reviewRound.js +8 -7
  121. package/dist/review/taskFinalReviewContractResolution.js +1 -1
  122. package/dist/runtime/acpProtocol.js +4 -51
  123. package/dist/runtime/acpSession.js +10 -75
  124. package/dist/runtime/acpSessionConfiguration.js +1 -8
  125. package/dist/runtime/agentEndpoint.js +3 -2
  126. package/dist/runtime/agentFailureContext.js +43 -0
  127. package/dist/runtime/agentHost.js +15 -15
  128. package/dist/runtime/codexInteractiveHost.js +2 -2
  129. package/dist/runtime/index.js +0 -1
  130. package/dist/runtime/managedCaller.js +21 -1
  131. package/dist/runtime/providerErrors.js +38 -0
  132. package/dist/runtime/runtimeObservation.js +0 -5
  133. package/dist/runtime/runtimeSessionCandidate.js +2 -3
  134. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  135. package/dist/runtime/structuredProviderHost.js +4 -41
  136. package/dist/runtime/tmuxAdapters.js +6 -8
  137. package/dist/scheduler/activeRoleRunDelivery.js +7 -7
  138. package/dist/scheduler/leaderWakeupProcessor.js +19 -6
  139. package/dist/scheduler/operatorEvent.js +12 -20
  140. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  141. package/dist/scheduler/ports.js +5 -8
  142. package/dist/scheduler/roleRunLiveness.js +4 -4
  143. package/dist/scheduler/roleRunStall.js +16 -23
  144. package/dist/scheduler/taskExecutionProjection.js +25 -33
  145. package/dist/scheduler/taskObservabilityProjection.js +0 -1
  146. package/dist/scheduler/taskWake.js +5 -10
  147. package/dist/scheduler/wakeReason.js +2 -0
  148. package/dist/scheduler/wakeupQueue.js +2 -4
  149. package/dist/setup/setupCommand.js +1 -1
  150. package/dist/storage/contextRecords.js +106 -0
  151. package/dist/storage/currentTaskStore.js +1 -1
  152. package/dist/storage/homeLayout.js +13 -17
  153. package/dist/storage/migrations/agentFailureContext.js +22 -0
  154. package/dist/storage/migrations/currentInputContract.js +86 -0
  155. package/dist/storage/migrations/currentRuntimeContract.js +228 -0
  156. package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
  157. package/dist/storage/migrations/integrationContinuation.js +5 -4
  158. package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
  159. package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
  160. package/dist/storage/migrations/verificationPlanV1.js +162 -0
  161. package/dist/storage/migrations/verificationPolicy.js +74 -0
  162. package/dist/storage/migrations/workItemHistory.js +46 -0
  163. package/dist/storage/persistenceWorker.js +12 -4
  164. package/dist/storage/recordValidation.js +87 -0
  165. package/dist/storage/sqliteSchema.js +142 -1
  166. package/dist/storage/sqliteStore.js +156 -162
  167. package/dist/storage/storageSchema.js +2 -14
  168. package/dist/storage/storageVersions.js +1 -1
  169. package/dist/storage/storeRpc.js +15 -7
  170. package/dist/storage/taskStore.js +3 -7
  171. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
  172. package/dist/task/completionReadiness.js +5 -24
  173. package/dist/task/draftPlan.js +0 -53
  174. package/dist/task/nextAction.js +7 -13
  175. package/dist/task/remoteDeliveryService.js +89 -0
  176. package/dist/task/taskActivation.js +2 -25
  177. package/dist/task/taskActivationService.js +0 -2
  178. package/dist/task/taskRecordReference.js +0 -1
  179. package/dist/task/taskSubmission.js +3 -14
  180. package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
  181. package/dist/telemetry/sqliteTelemetryStore.js +49 -49
  182. package/dist/telemetry/telemetryWiring.js +4 -3
  183. package/dist/tmux/tmuxManager.js +29 -20
  184. package/dist/verification/gateArtifact.js +15 -24
  185. package/dist/verification/gateArtifactStore.js +14 -7
  186. package/dist/verification/verificationGateService.js +29 -88
  187. package/dist/verification/verificationPlan.js +27 -72
  188. package/dist/web/assets/client/app.js +1 -1
  189. package/dist/web/assets/client/taskSurface.js +4 -4
  190. package/dist/web/webServer.js +4 -6
  191. package/dist/web/webSnapshot.js +3 -83
  192. package/dist/web/webTaskSurface.js +17 -46
  193. package/dist/workItem/workItem.js +1 -12
  194. package/docs/architecture/capabilities-and-resources.md +14 -2
  195. package/docs/architecture/capabilities-and-resources.zh-CN.md +11 -1
  196. package/docs/managed-turn-and-session-runtime.md +83 -10
  197. package/docs/managed-turn-and-session-runtime.zh-CN.md +65 -11
  198. package/docs/plugin-sdk.md +4 -2
  199. package/docs/provider-runtime.md +3 -1
  200. package/docs/provider-runtime.zh-CN.md +4 -2
  201. package/docs/release-workflow.md +190 -7
  202. package/docs/release-workflow.zh-CN.md +139 -5
  203. package/docs/roles-and-configuration.md +29 -0
  204. package/docs/roles-and-configuration.zh-CN.md +21 -0
  205. package/docs/sqlite-control-plane-design.md +16 -3
  206. package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
  207. package/docs/task-dag-semantics.md +3 -3
  208. package/docs/task-dag-semantics.zh-CN.md +2 -2
  209. package/docs/task-delivery.md +91 -5
  210. package/docs/task-delivery.zh-CN.md +72 -4
  211. package/docs/task-discovery.md +10 -11
  212. package/docs/task-discovery.zh-CN.md +8 -9
  213. package/docs/testing/verification-levels.md +145 -9
  214. package/docs/testing/verification-levels.zh-CN.md +103 -8
  215. package/package.json +1 -1
  216. package/skills/yui-leader/SKILL.md +5 -0
  217. package/skills/yui-leader/references/integration.md +9 -5
  218. package/skills/yui-leader/references/planning.md +9 -3
  219. package/skills/yui-operator/SKILL.md +15 -10
  220. package/skills/yui-runtime/SKILL.md +6 -6
  221. package/skills/yui-runtime/references/recovery.md +47 -0
  222. package/dist/agent/agentRegistry.js +0 -10
  223. package/dist/agentRun/runIdentity.js +0 -22
  224. package/dist/commands/deliveryGuardPreflight.js +0 -30
  225. package/dist/commands/taskIntegrationQueueCommands.js +0 -226
  226. package/dist/commands/taskOverviewCommand.js +0 -377
  227. package/dist/completion/completionWizard.js +0 -125
  228. package/dist/context/dispatchContext.js +0 -110
  229. package/dist/context/wakeNotification.js +0 -144
  230. package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
  231. package/dist/integration/integrationQueueEntry.js +0 -191
  232. package/dist/integration/integrationQueueService.js +0 -810
  233. package/dist/release/fakeReleasePorts.js +0 -55
  234. package/dist/runtime/sessionOwnerRegistry.js +0 -137
  235. package/dist/task/deliveryGuard.js +0 -226
  236. /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
@@ -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,
@@ -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 {
@@ -18,6 +18,7 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
18
18
  import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, writeFileSync } from "node:fs";
19
19
  import { dirname, join, resolve } from "node:path";
20
20
  import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
21
+ import { processGenerationIsLive } from "../core/fileLockOwner.js";
21
22
  import { writeTextFileAtomically } from "../storage/durableFile.js";
22
23
  export const RELEASES_DIRECTORY = "runtime/releases";
23
24
  export const ACTIVE_RELEASE_POINTER_PATH = "runtime/active-release.json";
@@ -334,10 +335,7 @@ export function acquireHandoverLock(home) {
334
335
  continue;
335
336
  throw error;
336
337
  }
337
- if (typeof existing.pid === "number"
338
- && Number.isSafeInteger(existing.pid)
339
- && existing.pid > 0
340
- && isHandoverLockLive(existing)) {
338
+ if (isHandoverLockLive(existing)) {
341
339
  throw new Error(`Another Controller handover is already running (owner PID ${existing.pid}, `
342
340
  + `createdAt ${String(existing.createdAt)}): ${lockPath}`);
343
341
  }
@@ -350,22 +348,18 @@ export function acquireHandoverLock(home) {
350
348
  * A handover lock is live only when its owner process is still the same
351
349
  * generation that wrote it. A PID that exists but with a different start
352
350
  * identity was reused by an unrelated process, so the lock is stale.
353
- * Locks written before the start-identity field existed fall back to the
354
- * PID-exists check for backward compatibility.
351
+ * An incomplete identity is not proof of a stale owner. Diagnose it without
352
+ * removing the lock or authorizing an apparent parent by PID alone.
355
353
  */
356
354
  function isHandoverLockLive(existing) {
357
355
  if (typeof existing.pid !== "number"
358
356
  || !Number.isSafeInteger(existing.pid)
359
- || existing.pid <= 0) {
360
- return false;
357
+ || existing.pid <= 0
358
+ || typeof existing.processStartIdentity !== "string"
359
+ || !/^[0-9]{1,32}$/u.test(existing.processStartIdentity)) {
360
+ throw new Error("Unsupported handover lock identity; preserve the lock and inspect its owner before cleanup.");
361
361
  }
362
- const currentIdentity = readLinuxProcessStartIdentity(existing.pid);
363
- if (currentIdentity === undefined)
364
- return false;
365
- if (typeof existing.processStartIdentity === "string") {
366
- return currentIdentity === existing.processStartIdentity;
367
- }
368
- return true;
362
+ return processGenerationIsLive(existing.pid, existing.processStartIdentity);
369
363
  }
370
364
  export function newHandoverId() {
371
365
  return `handover-${randomUUID()}`;