@zq-silk/yui 0.15.8 → 0.15.11

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 (151) hide show
  1. package/ARCHITECTURE.md +2 -0
  2. package/ARCHITECTURE.zh-CN.md +151 -0
  3. package/README.md +211 -14
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/artifacts/artifactCapability.js +74 -0
  6. package/dist/artifacts/artifactCommitLock.js +249 -0
  7. package/dist/artifacts/artifactPaths.js +151 -0
  8. package/dist/artifacts/gitArtifactRef.js +146 -0
  9. package/dist/artifacts/managedGit.js +332 -0
  10. package/dist/artifacts/taskArtifactRepository.js +277 -0
  11. package/dist/cli/commandCatalog.js +40 -16
  12. package/dist/cli/interactionPolicy.js +3 -3
  13. package/dist/cli/updateOrchestrator.js +24 -1
  14. package/dist/cli/updatePorts.js +7 -3
  15. package/dist/cli/upgradeCommand.js +42 -2
  16. package/dist/cli.js +403 -93
  17. package/dist/commands/globalRoleCommands.js +314 -4
  18. package/dist/commands/operatorCommands.js +33 -2
  19. package/dist/commands/projectCommands.js +6 -7
  20. package/dist/commands/releaseCommands.js +18 -0
  21. package/dist/commands/taskActivationCommands.js +22 -0
  22. package/dist/commands/taskActor.js +25 -0
  23. package/dist/commands/taskCommands.js +846 -155
  24. package/dist/commands/taskIntegrationCommands.js +16 -38
  25. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  26. package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
  27. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  28. package/dist/context/runContextPack.js +28 -16
  29. package/dist/context/taskContext.js +64 -5
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +11 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
  34. package/dist/controller/globalInputDelivery.js +119 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/resourceInventory.js +14 -4
  37. package/dist/controller/resourceInventoryLinux.js +2 -6
  38. package/dist/controller/runtime.js +81 -6
  39. package/dist/controller/runtimeEventInbox.js +32 -3
  40. package/dist/controller/runtimeEventProcessor.js +26 -6
  41. package/dist/controller/runtimeHookRunFence.js +75 -19
  42. package/dist/controller/structuredProviderObservation.js +133 -70
  43. package/dist/coordination/workMailboxQueue.js +5 -0
  44. package/dist/execution/workItemExecutionProjection.js +1 -1
  45. package/dist/executor/agentExecutor.js +64 -4
  46. package/dist/executor/executorRegistry.js +3 -0
  47. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  48. package/dist/integration/deliveryObligation.js +2 -1
  49. package/dist/integration/gitIntegrationService.js +312 -382
  50. package/dist/integration/integrationAttempt.js +30 -4
  51. package/dist/integration/integrationQueueService.js +7 -7
  52. package/dist/integration/integrationSourceApplication.js +323 -0
  53. package/dist/kernel/builtinCapabilities.js +32 -24
  54. package/dist/message/globalInterrupt.js +33 -0
  55. package/dist/message/inputControlResolution.js +106 -0
  56. package/dist/message/message.js +423 -0
  57. package/dist/message/messageContinuation.js +126 -3
  58. package/dist/message/taskInterrupt.js +34 -0
  59. package/dist/observability/orchestrationMetrics.js +1 -1
  60. package/dist/plugins/pluginService.js +11 -3
  61. package/dist/release/releaseHandover.js +22 -0
  62. package/dist/release/releaseWorkflowPorts.js +15 -7
  63. package/dist/repository/gitWorkspace.js +72 -15
  64. package/dist/repository/taskWorkspaceCoordinator.js +134 -0
  65. package/dist/repository/taskWorkspacePreparer.js +120 -49
  66. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  67. package/dist/resources/projectResource.js +0 -48
  68. package/dist/resources/projectResourceService.js +3 -81
  69. package/dist/resources/resourceDiscovery.js +3 -2
  70. package/dist/runtime/agentHost.js +152 -72
  71. package/dist/runtime/agentHostCompatibility.js +127 -0
  72. package/dist/runtime/agentHostProtocol.js +53 -0
  73. package/dist/runtime/executionEnvironment.js +0 -19
  74. package/dist/runtime/launchBroker.js +6 -0
  75. package/dist/runtime/sessionReconciliation.js +4 -4
  76. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  77. package/dist/runtime/tmuxAdapters.js +5 -3
  78. package/dist/scheduler/operatorEvent.js +4 -0
  79. package/dist/scheduler/taskExecutionProjection.js +12 -1
  80. package/dist/scheduler/wakeReason.js +7 -1
  81. package/dist/scheduler/wakeupQueue.js +2 -0
  82. package/dist/setup/setupCommand.js +29 -16
  83. package/dist/storage/homeLayout.js +130 -0
  84. package/dist/storage/migrations/artifactsToGit.js +338 -0
  85. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  86. package/dist/storage/migrations/integrationContinuation.js +104 -0
  87. package/dist/storage/migrations/submitIntent.js +126 -0
  88. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  89. package/dist/storage/sqliteSchema.js +173 -7
  90. package/dist/storage/sqliteStore.js +41 -22
  91. package/dist/storage/storageVersions.js +1 -1
  92. package/dist/storage/storeRpc.js +2 -1
  93. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  94. package/dist/task/archiveDiagnostics.js +128 -0
  95. package/dist/task/nextAction.js +44 -11
  96. package/dist/task/taskActivation.js +26 -0
  97. package/dist/task/taskActivationService.js +85 -69
  98. package/dist/task/taskSubmission.js +236 -0
  99. package/dist/web/assets/client/app.js +58 -2
  100. package/dist/web/assets/client/components.js +1 -0
  101. package/dist/web/assets/client/i18n.js +6 -0
  102. package/dist/web/assets/client/taskSurface.js +202 -7
  103. package/dist/web/assets/client/view.js +7 -4
  104. package/dist/web/assets/shell.js +23 -0
  105. package/dist/web/assets/styles/layout.js +1 -1
  106. package/dist/web/assets/styles/widgets.js +12 -0
  107. package/dist/web/webServer.js +135 -4
  108. package/dist/web/webSnapshot.js +4 -3
  109. package/dist/web/webTaskSurface.js +225 -8
  110. package/dist/workItem/workItem.js +14 -10
  111. package/dist/workspace/workItemChangeSetManager.js +18 -2
  112. package/docs/agent-result-consumption.md +2 -0
  113. package/docs/agent-result-consumption.zh-CN.md +81 -0
  114. package/docs/agent-runtime-drivers.md +2 -0
  115. package/docs/agent-runtime-drivers.zh-CN.md +77 -0
  116. package/docs/architecture/README.md +44 -32
  117. package/docs/architecture/README.zh-CN.md +43 -0
  118. package/docs/architecture/capabilities-and-resources.md +118 -79
  119. package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
  120. package/docs/managed-turn-and-session-runtime.md +2 -0
  121. package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
  122. package/docs/observability/README.md +2 -0
  123. package/docs/observability/README.zh-CN.md +71 -0
  124. package/docs/plugin-sdk.md +320 -217
  125. package/docs/plugin-sdk.zh-CN.md +293 -0
  126. package/docs/provider-runtime.md +2 -0
  127. package/docs/provider-runtime.zh-CN.md +132 -0
  128. package/docs/release-workflow.md +41 -0
  129. package/docs/release-workflow.zh-CN.md +266 -0
  130. package/docs/roles-and-configuration.md +2 -0
  131. package/docs/roles-and-configuration.zh-CN.md +96 -0
  132. package/docs/sqlite-control-plane-design.md +225 -1
  133. package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
  134. package/docs/task-dag-semantics.md +80 -57
  135. package/docs/task-dag-semantics.zh-CN.md +59 -0
  136. package/docs/task-delivery.md +2 -0
  137. package/docs/task-delivery.zh-CN.md +82 -0
  138. package/docs/task-local-identity.md +2 -0
  139. package/docs/task-local-identity.zh-CN.md +58 -0
  140. package/docs/testing/verification-levels.md +26 -0
  141. package/docs/testing/verification-levels.zh-CN.md +80 -0
  142. package/i18n/README.zh-CN.md +199 -10
  143. package/package.json +2 -1
  144. package/skills/yui-leader/SKILL.md +88 -331
  145. package/skills/yui-leader/references/execution.md +405 -0
  146. package/skills/yui-leader/references/integration.md +52 -2
  147. package/skills/yui-leader/references/planning.md +109 -0
  148. package/skills/yui-leader/references/task-plugins.md +8 -4
  149. package/skills/yui-operator/SKILL.md +22 -4
  150. package/skills/yui-runtime/SKILL.md +27 -0
  151. package/skills/yui-runtime/references/publication.md +20 -0
@@ -121,6 +121,16 @@ export function projectNextAction(facts) {
121
121
  recommendedCommand: `yui task activate ${task.id}`
122
122
  });
123
123
  }
124
+ const conflictedIntegration = facts.integrations.find(attempt => attempt.status === "conflicted");
125
+ if (conflictedIntegration !== undefined) {
126
+ return buildAction(facts, {
127
+ kind: "resolve-integration-conflicts",
128
+ reason: `Integration ${conflictedIntegration.id} has Git conflicts; the Leader resolves its workspace and continues without prior resolve or user approval.`,
129
+ refs: [ref("integration", conflictedIntegration.id)],
130
+ preconditions: [{ fact: "Git conflict resolution belongs to the Leader", satisfied: true }],
131
+ recommendedCommand: `yui task integration continue ${task.id}/${conflictedIntegration.id}`
132
+ });
133
+ }
124
134
  const checkingIntegration = facts.integrations.find(attempt => attempt.status === "running" && attempt.jobId !== undefined);
125
135
  if (checkingIntegration !== undefined) {
126
136
  const job = facts.integrationJobs?.find(job => job.id === checkingIntegration.jobId);
@@ -146,6 +156,21 @@ export function projectNextAction(facts) {
146
156
  recommendedCommand: `yui task integration continue ${task.id}/${checkingIntegration.id}`
147
157
  });
148
158
  }
159
+ const unfinishedIntegration = facts.integrations.find(attempt => attempt.status === "running" || attempt.status === "validating");
160
+ if (unfinishedIntegration !== undefined) {
161
+ return buildAction(facts, {
162
+ kind: "integrate-work-item",
163
+ reason: `Integration ${unfinishedIntegration.id} is unfinished. ${unfinishedIntegration.summary ?? "Continue from its persisted source and check evidence."}`,
164
+ refs: [ref("integration", unfinishedIntegration.id)],
165
+ preconditions: [{ fact: "Inspect exact Integration evidence before any retry", satisfied: true }],
166
+ recommendedCommand: `yui task integration continue ${task.id}/${unfinishedIntegration.id}`,
167
+ alternatives: [{
168
+ kind: "inspect-integration",
169
+ reason: "If completion cannot be proved, preserve evidence and choose formal abort and an authorized alternative.",
170
+ recommendedCommand: `yui task integration show ${task.id}/${unfinishedIntegration.id}`
171
+ }]
172
+ });
173
+ }
149
174
  const candidateReady = facts.workItems
150
175
  .find((item) => (item.status === "open" && item.currentCandidateId !== undefined));
151
176
  if (candidateReady !== undefined) {
@@ -320,27 +345,35 @@ export function projectNextAction(facts) {
320
345
  return synthesisSelectionAction(facts, "work", item.id);
321
346
  }
322
347
  const refs = [ref("work-item", item.id)];
348
+ const direct = item.assignee === undefined;
323
349
  return buildAction(facts, {
324
350
  kind: "implement-current-work-item",
325
- reason: `Work Item ${item.id} is ${item.status}; dispatch or continue its implementation.`,
351
+ reason: direct
352
+ ? `Work Item ${item.id} has no managed assignee; the Leader can execute it directly.`
353
+ : `Work Item ${item.id} is assigned to ${item.assignee}; dispatch or continue that assignment.`,
326
354
  refs,
327
355
  preconditions: [
328
356
  { fact: `Work Item is ${item.status}`, satisfied: true, ref: refs[0] }
329
357
  ],
330
- recommendedCommand: `yui task work dispatch ${task.id}/${item.id}`,
331
- alternatives: [
332
- {
333
- kind: "execute-directly",
334
- reason: "Execute the bounded Work Item directly when the Leader's current context and authority are sufficient.",
335
- refs
336
- },
358
+ recommendedCommand: direct
359
+ ? `yui task work update ${task.id}/${item.id} running`
360
+ : `yui task work dispatch ${task.id}/${item.id}`,
361
+ alternatives: direct ? [
362
+ ...(item.writeProjectIds.length === 0 ? [] : [{
363
+ kind: "isolate-work-item",
364
+ reason: "Prepare and inspect the WorkItem-owned code workspace before direct implementation.",
365
+ recommendedCommand: `yui task work isolate ${task.id}/${item.id}`,
366
+ refs
367
+ }]),
337
368
  {
338
369
  kind: "native-subagent",
339
- reason: "Use native implementer subagents when bounded work benefits from specialist attention or parallel fan-out inside the Leader Session.",
370
+ reason: "Use a bounded native child only when authorized and useful within the Leader's current scope.",
340
371
  refs
341
372
  }
342
- ],
343
- judgmentRequired: "Leader must choose the execution path: direct execution, a native subagent, or managed Task Role dispatch."
373
+ ] : [],
374
+ judgmentRequired: direct
375
+ ? "Honor explicit execution preferences. For code, use the WorkItem-owned workspace and integrate its Candidate before acceptance; no self-dispatch is needed."
376
+ : "Preserve the current managed Assignment and original results; do not switch ownership merely to follow a different default."
344
377
  });
345
378
  }
346
379
  if (deliveryWorkItems.length === 0
@@ -22,6 +22,7 @@ export const TASK_ACTIVATION_EVENT = Object.freeze({
22
22
  adopted: "task.activation-adopted",
23
23
  failed: "task.activation-failed"
24
24
  });
25
+ export const TASK_ACTIVATION_ORIGINS = Object.freeze(["explicit", "submit-develop"]);
25
26
  /**
26
27
  * The digest that decides whether a repeated requestId is the same request.
27
28
  *
@@ -63,6 +64,7 @@ export function createTaskActivationRequest(taskId, input, now) {
63
64
  partialResultRefs: []
64
65
  },
65
66
  startMode: input.startMode,
67
+ ...(input.origin === undefined ? {} : { origin: input.origin }),
66
68
  ...(input.afterPlanningRun === undefined
67
69
  ? {}
68
70
  : { afterPlanningRun: input.afterPlanningRun }),
@@ -194,6 +196,26 @@ export function admitTaskActivationRequest(task, request, planningRunIsActive) {
194
196
  }
195
197
  return { disposition: "ready", request };
196
198
  }
199
+ /**
200
+ * Whether the Controller may continue (auto-adopt) a released request without an
201
+ * explicit activation boundary call (task-32 §2.4).
202
+ *
203
+ * A deferred request already holds activation authority and is released by its
204
+ * planning Turn ending, so it is adoptable regardless of origin. An immediate
205
+ * request is auto-continued only when it carries a recognised provable origin:
206
+ * an explicit activation action, or a develop submission the shared transaction
207
+ * accepted while the Task was unplanned. A request with no origin — every one
208
+ * migrated from before the field existed — is deliberately held back so an
209
+ * upgrade cannot turn a historical pending request into a silent activation; it
210
+ * still adopts through the explicit `yui task activate` boundary, which does not
211
+ * consult origin. This replaces the old actor heuristic that both dropped legal
212
+ * Operator/user immediate requests and could not distinguish provenance.
213
+ */
214
+ export function activationRequestIsControllerAdoptable(request) {
215
+ if (request.startMode === "after-planning-turn")
216
+ return true;
217
+ return request.origin !== undefined;
218
+ }
197
219
  /**
198
220
  * The durable terminal outcome for a requestId, read from the activation event
199
221
  * ledger.
@@ -236,6 +258,10 @@ export function validateTaskActivationRequest(request) {
236
258
  if (!["immediate", "after-planning-turn"].includes(request.startMode)) {
237
259
  throw new Error(`Activation start mode is invalid: ${String(request.startMode)}.`);
238
260
  }
261
+ if (request.origin !== undefined
262
+ && !TASK_ACTIVATION_ORIGINS.includes(request.origin)) {
263
+ throw new Error(`Activation origin is invalid: ${String(request.origin)}.`);
264
+ }
239
265
  if ((request.startMode === "after-planning-turn")
240
266
  !== (request.afterPlanningRun !== undefined)) {
241
267
  throw new Error("Activation deferral must name exactly the planning Turn it waits for.");
@@ -40,80 +40,96 @@ export function taskActivationOperationRef(taskId, requestId) {
40
40
  * unchanged, so a retried tool call cannot produce a second activation.
41
41
  */
42
42
  export function requestTaskActivation(store, input, now) {
43
- return store.transaction((tx) => {
44
- const task = requireOpenDraft(tx, input.taskId);
45
- const plan = canonicalEnvironmentPlan(input.environmentPlan);
46
- const planningRun = resolveCallerPlanningRun(tx, task.id, input.callerRunId);
47
- const startMode = planningRun === undefined
48
- ? "immediate"
49
- : "after-planning-turn";
50
- const existing = task.activationRequest;
51
- // A requestId whose outcome was already decided is never re-opened, whether
52
- // it still holds the slot or a later request displaced it. Cancelling A,
53
- // requesting B and then retrying A must not resurrect A: the withdrawal is
54
- // explicit authority, and adoption is exactly the effect it withdrew. An
55
- // adopted id is equally final — its environment and status change happened.
56
- // Only `failed` stays replayable, which is its documented contract.
57
- //
58
- // The authority is the durable activation event ledger, not the bounded
59
- // display payload: the payload trims its oldest entries once it overflows,
60
- // but the terminal events are never compacted, so an evicted cancellation is
61
- // still refused with its original outcome instead of silently resurrecting.
62
- const settled = settledActivationRequest(tx, task, input.requestId);
63
- if (settled !== undefined) {
64
- throw new Error(`Activation request ${input.requestId} was already ${settled.disposition} for ${task.id}`
65
- + `${settled.outcome === undefined ? "" : ` (${settled.outcome})`}. `
66
- + "Use a new requestId.");
67
- }
68
- if (existing?.operation.requestId === input.requestId) {
69
- const digest = taskActivationInputDigest(task.id, {
70
- startMode,
71
- ...(planningRun === undefined ? {} : { afterPlanningRun: planningRun }),
72
- environmentPlan: plan
73
- });
74
- if (existing.operation.inputDigest !== digest) {
75
- throw new Error(`Activation requestId ${input.requestId} was already used for different inputs.`);
76
- }
77
- return {
78
- operationRef: taskActivationOperationRef(task.id, existing.operation.requestId),
79
- request: existing,
80
- startMode: existing.startMode,
81
- created: false,
82
- ...(existing.afterPlanningRun === undefined
83
- ? {}
84
- : { afterPlanningRun: existing.afterPlanningRun })
85
- };
86
- }
87
- if (existing?.disposition === "pending") {
88
- throw new Error(`Task already has a pending activation request: ${task.id}/${existing.operation.requestId}. `
89
- + "Cancel it before requesting different inputs.");
90
- }
91
- const request = createTaskActivationRequest(task.id, {
92
- requestId: input.requestId,
93
- actorId: input.actorId,
94
- authorityRef: input.authorityRef,
43
+ return store.transaction((tx) => recordTaskActivationRequestInTransaction(tx, input, now));
44
+ }
45
+ /**
46
+ * The transactional core of {@link requestTaskActivation}, callable from inside
47
+ * a caller's own open transaction.
48
+ *
49
+ * The shared submission service (task-32 §2.3) records a develop route's
50
+ * activation request in the very transaction that saves the message, so the
51
+ * message and its activation intent commit together and no "submit then
52
+ * activate" two-command window exists. It reuses this exact logic rather than a
53
+ * second activation queue: the same idempotency, settled-id refusal and
54
+ * pending-collision rules apply whether the request arrives from the explicit
55
+ * boundary or from a develop submission. The caller owns transaction lifetime
56
+ * and any post-commit mailbox notification.
57
+ */
58
+ export function recordTaskActivationRequestInTransaction(tx, input, now) {
59
+ const task = requireOpenDraft(tx, input.taskId);
60
+ const plan = canonicalEnvironmentPlan(input.environmentPlan);
61
+ const planningRun = resolveCallerPlanningRun(tx, task.id, input.callerRunId);
62
+ const startMode = planningRun === undefined
63
+ ? "immediate"
64
+ : "after-planning-turn";
65
+ const existing = task.activationRequest;
66
+ // A requestId whose outcome was already decided is never re-opened, whether
67
+ // it still holds the slot or a later request displaced it. Cancelling A,
68
+ // requesting B and then retrying A must not resurrect A: the withdrawal is
69
+ // explicit authority, and adoption is exactly the effect it withdrew. An
70
+ // adopted id is equally final — its environment and status change happened.
71
+ // Only `failed` stays replayable, which is its documented contract.
72
+ //
73
+ // The authority is the durable activation event ledger, not the bounded
74
+ // display payload: the payload trims its oldest entries once it overflows,
75
+ // but the terminal events are never compacted, so an evicted cancellation is
76
+ // still refused with its original outcome instead of silently resurrecting.
77
+ const settled = settledActivationRequest(tx, task, input.requestId);
78
+ if (settled !== undefined) {
79
+ throw new Error(`Activation request ${input.requestId} was already ${settled.disposition} for ${task.id}`
80
+ + `${settled.outcome === undefined ? "" : ` (${settled.outcome})`}. `
81
+ + "Use a new requestId.");
82
+ }
83
+ if (existing?.operation.requestId === input.requestId) {
84
+ const digest = taskActivationInputDigest(task.id, {
95
85
  startMode,
96
86
  ...(planningRun === undefined ? {} : { afterPlanningRun: planningRun }),
97
87
  environmentPlan: plan
98
- }, now);
99
- tx.saveTask(setTaskActivationRequest(task, request, now));
100
- tx.saveEvent(task.id, createTaskEvent(tx.nextEventId(task.id), task.id, TASK_ACTIVATION_EVENT.requested, {
101
- requestId: request.operation.requestId,
102
- startMode: request.startMode,
103
- environmentPlan: describeEnvironmentPlan(request.environmentPlan),
104
- actor: request.operation.actorId,
105
- ...(request.afterPlanningRun === undefined
106
- ? {}
107
- : { afterPlanningRun: request.afterPlanningRun })
108
- }, now));
88
+ });
89
+ if (existing.operation.inputDigest !== digest) {
90
+ throw new Error(`Activation requestId ${input.requestId} was already used for different inputs.`);
91
+ }
109
92
  return {
110
- operationRef: taskActivationOperationRef(task.id, request.operation.requestId),
111
- request,
112
- startMode,
113
- created: true,
114
- ...(planningRun === undefined ? {} : { afterPlanningRun: planningRun })
93
+ operationRef: taskActivationOperationRef(task.id, existing.operation.requestId),
94
+ request: existing,
95
+ startMode: existing.startMode,
96
+ created: false,
97
+ ...(existing.afterPlanningRun === undefined
98
+ ? {}
99
+ : { afterPlanningRun: existing.afterPlanningRun })
115
100
  };
116
- });
101
+ }
102
+ if (existing?.disposition === "pending") {
103
+ throw new Error(`Task already has a pending activation request: ${task.id}/${existing.operation.requestId}. `
104
+ + "Cancel it before requesting different inputs.");
105
+ }
106
+ const request = createTaskActivationRequest(task.id, {
107
+ requestId: input.requestId,
108
+ actorId: input.actorId,
109
+ authorityRef: input.authorityRef,
110
+ startMode,
111
+ ...(input.origin === undefined ? {} : { origin: input.origin }),
112
+ ...(planningRun === undefined ? {} : { afterPlanningRun: planningRun }),
113
+ environmentPlan: plan
114
+ }, now);
115
+ tx.saveTask(setTaskActivationRequest(task, request, now));
116
+ tx.saveEvent(task.id, createTaskEvent(tx.nextEventId(task.id), task.id, TASK_ACTIVATION_EVENT.requested, {
117
+ requestId: request.operation.requestId,
118
+ startMode: request.startMode,
119
+ environmentPlan: describeEnvironmentPlan(request.environmentPlan),
120
+ actor: request.operation.actorId,
121
+ ...(request.origin === undefined ? {} : { origin: request.origin }),
122
+ ...(request.afterPlanningRun === undefined
123
+ ? {}
124
+ : { afterPlanningRun: request.afterPlanningRun })
125
+ }, now));
126
+ return {
127
+ operationRef: taskActivationOperationRef(task.id, request.operation.requestId),
128
+ request,
129
+ startMode,
130
+ created: true,
131
+ ...(planningRun === undefined ? {} : { afterPlanningRun: planningRun })
132
+ };
117
133
  }
118
134
  /** Explicit user or Operator cancellation. A cancelled request never adopts. */
119
135
  export function cancelTaskActivation(store, taskId, requestId, reason, now) {
@@ -0,0 +1,236 @@
1
+ import { SYSTEM_LEADER_ROLE } from "../role/systemRoles.js";
2
+ /**
3
+ * Requirement A phase and routing derivation (task-32 §2.1, §2.2).
4
+ *
5
+ * `Task.status` stays the only lifecycle statement. "Entered planning" is a
6
+ * derived fact, never a writable second state: this module reads it from the
7
+ * durable evidence a legitimate planning entry leaves behind, so idleness, a
8
+ * failed or replaced Session, a Controller restart, or a trimmed Context can
9
+ * never reset it to "not planned".
10
+ */
11
+ /**
12
+ * The never-compacted event that records a Draft was routed into planning by an
13
+ * accepted user/operator submission, inside the same transaction that saved the
14
+ * message. It is one of the three independent derivation sources below, and the
15
+ * only one the shared submission service itself writes; the others are produced
16
+ * by the planning Run and its Session. Migration appends the same event for
17
+ * legitimate historical discussions so a post-upgrade develop cannot snatch a
18
+ * mid-discussion Draft into auto-activation.
19
+ */
20
+ export const TASK_PLANNING_ENTERED_EVENT = "task.planning-entered";
21
+ /**
22
+ * Whether this Draft has already entered planning (task-32 §2.2).
23
+ *
24
+ * Derived — never a stored flag — from any of three independent, monotonic
25
+ * sources, each of which only ever comes into existence and never legitimately
26
+ * disappears:
27
+ *
28
+ * (a) a persisted planning-entered event, written in the message-save
29
+ * transaction the first time an accepted submission routed to planning;
30
+ * (b) a Leader planning AgentRun in history (the first-discuss Run, once the
31
+ * Controller has created it);
32
+ * (c) a Leader Session whose frozen launch carried planning authority.
33
+ *
34
+ * A role being configured, a non-empty body, or a non-empty Brief are
35
+ * deliberately NOT evidence: none of them proves the user's submission was ever
36
+ * accepted and routed. Only a Draft can be "in planning"; an active or terminal
37
+ * Task is past the question and answers false.
38
+ */
39
+ export function draftHasEnteredPlanning(store, task) {
40
+ if (task.status !== "draft")
41
+ return false;
42
+ return hasPlanningEnteredEvent(store.listEvents(task.id))
43
+ || hasLeaderPlanningRun(store, task.id)
44
+ || hasLeaderPlanningSession(store, task.id);
45
+ }
46
+ function hasPlanningEnteredEvent(events) {
47
+ return events.some((event) => event.type === TASK_PLANNING_ENTERED_EVENT);
48
+ }
49
+ function hasLeaderPlanningRun(store, taskId) {
50
+ return store.listRuns(taskId).some((run) => run.roleName === SYSTEM_LEADER_ROLE && run.purpose === "planning");
51
+ }
52
+ function hasLeaderPlanningSession(store, taskId) {
53
+ const sessions = store.getTaskRoleSessionSet(taskId, SYSTEM_LEADER_ROLE);
54
+ if (sessions === null)
55
+ return false;
56
+ const candidates = [
57
+ ...Object.values(sessions.sessions ?? {}),
58
+ ...(sessions.history ?? [])
59
+ ];
60
+ return candidates.some((session) => session.effective.executionAuthority === "planning");
61
+ }
62
+ export function draftActivationState(request) {
63
+ if (request === undefined)
64
+ return "none";
65
+ switch (request.disposition) {
66
+ case "pending": return "pending";
67
+ case "failed": return "failed";
68
+ case "adopted": return "adopted";
69
+ case "cancelled": return "none";
70
+ default: return "none";
71
+ }
72
+ }
73
+ /**
74
+ * Decide how a user/operator submission routes, given its normalized intent and
75
+ * the phase/activation facts read inside the save transaction.
76
+ *
77
+ * This is a pure decision over already-read facts, kept separate from the
78
+ * transaction that saves the message and enqueues work so it can be unit-tested
79
+ * exhaustively against the §2.1 table without a Store. The winner of a race is
80
+ * decided by which submission's transaction commits first: the second one reads
81
+ * the phase/activation the first one already wrote and routes accordingly.
82
+ *
83
+ * `executionEnabled` is the Task's own gate, read in the same transaction. It
84
+ * only ever gates a *fresh* develop activation: recording an activation request
85
+ * against a stopped Draft is refused at the activation boundary, so routing that
86
+ * case to `activate` would abort the whole transaction and lose the saved
87
+ * message. Planning and already-open activation obligations are unaffected — the
88
+ * gate is enforced later at dispatch, exactly as it is for a plain message.
89
+ */
90
+ export function decideSubmissionRouting(input) {
91
+ // record never wakes anything, in any state (§2.1 first column).
92
+ if (input.intent === "record")
93
+ return { kind: "record" };
94
+ if (input.status === "active") {
95
+ // Neither discuss nor develop re-activates or downgrades an active Task.
96
+ return { kind: "active-context" };
97
+ }
98
+ // Only Draft remains: terminal statuses are refused before routing (§2.1 last
99
+ // row) by the caller's assertTaskOpen, so this function is never asked about
100
+ // them.
101
+ if (input.activation === "pending") {
102
+ return input.intent === "develop"
103
+ // A compatible pending request already exists; reference it, never a
104
+ // second request (§2.3 "重复 develop 不产生第二个兼容激活请求").
105
+ ? { kind: "await-activation", state: "pending" }
106
+ // discuss becomes post-activation input; it does not start new planning.
107
+ : { kind: "await-activation", state: "pending" };
108
+ }
109
+ if (input.activation === "failed") {
110
+ // develop never auto-retries or downgrades to planning; discuss reports the
111
+ // original failure and the explicit retry/cancel next step. Both save only.
112
+ return { kind: "await-activation", state: "failed" };
113
+ }
114
+ if (input.intent === "develop") {
115
+ // develop on an already-planned Draft never auto-activates — it needs
116
+ // explicit manual activation. On an unplanned Draft with no open activation
117
+ // it records the request and routes to activation, unless execution is
118
+ // stopped: then the message is saved but no activation is recorded.
119
+ if (input.enteredPlanning)
120
+ return { kind: "planned-needs-manual-activation" };
121
+ return input.executionEnabled
122
+ ? { kind: "activate", environmentPlan: input.developEnvironmentPlan }
123
+ : { kind: "activation-blocked-execution-stopped" };
124
+ }
125
+ // discuss (the default): first discussion records the planning entry; later
126
+ // discussion continues the existing planning conversation.
127
+ return input.enteredPlanning
128
+ ? { kind: "continue-planning" }
129
+ : { kind: "enter-planning" };
130
+ }
131
+ /**
132
+ * Normalize a possibly-absent submission intent to its effective value.
133
+ *
134
+ * An absent intent is `discuss` (task-32 §2.5: "默认/旧客户端都按 discuss"), so
135
+ * every old client and every message saved before the field existed keeps its
136
+ * historical Leader-waking behaviour. Body text is never inspected.
137
+ *
138
+ * A legacy `--wake-policy none` carried the "save only, do not wake" meaning that
139
+ * `record` now names, so a caller that supplied only that older signal maps to
140
+ * `record` rather than silently becoming a Leader-waking `discuss`. An explicit
141
+ * intent always wins over the wake-policy shorthand.
142
+ */
143
+ export function normalizeSubmissionIntent(intent, wakePolicy) {
144
+ if (intent !== undefined)
145
+ return intent;
146
+ if (wakePolicy === "none")
147
+ return "record";
148
+ return "discuss";
149
+ }
150
+ /**
151
+ * Whether a retry aims at the same target the key was first bound to (§2.3). An
152
+ * absent prior target (a keyed Message from before receipts existed) never
153
+ * matches, so such a Message is treated as a conflict rather than replayed from
154
+ * a fabricated disposition.
155
+ */
156
+ export function sameSubmissionTarget(prior, next) {
157
+ if (prior === undefined || prior.kind !== next.kind)
158
+ return false;
159
+ if (prior.kind === "task" && next.kind === "task")
160
+ return prior.taskId === next.taskId;
161
+ return true;
162
+ }
163
+ /**
164
+ * Derive the §2.5 feedback for one committed submission from its routing and the
165
+ * facts read in the save transaction.
166
+ *
167
+ * Pure over already-read facts, so it is unit-testable against the whole table
168
+ * without a Store. `enteredPlanning` and `activationState` are the values read
169
+ * before routing executed; the routing itself names what the transaction then
170
+ * did, and phase is derived to reflect the post-commit truth (a just-entered
171
+ * planning Draft reports `draft-planning`).
172
+ */
173
+ export function describeSubmissionFeedback(input) {
174
+ const { routing } = input;
175
+ const phase = input.status === "active"
176
+ ? "active"
177
+ : routing.kind === "enter-planning"
178
+ || routing.kind === "continue-planning"
179
+ || routing.kind === "planned-needs-manual-activation"
180
+ || input.enteredPlanning
181
+ ? "draft-planning"
182
+ : "draft-unplanned";
183
+ const planning = routing.kind === "enter-planning"
184
+ ? "entered"
185
+ : routing.kind === "continue-planning"
186
+ ? "continued"
187
+ : "none";
188
+ const activation = routing.kind === "activate"
189
+ ? "requested"
190
+ : routing.kind === "planned-needs-manual-activation"
191
+ ? "manual-required"
192
+ : routing.kind === "activation-blocked-execution-stopped"
193
+ ? "execution-stopped"
194
+ : routing.kind === "await-activation"
195
+ ? routing.state
196
+ // record and the planning routes report the existing obligation, if
197
+ // any, truthfully without acting on it.
198
+ : input.activationState === "pending"
199
+ ? "pending"
200
+ : input.activationState === "failed"
201
+ ? "failed"
202
+ : "none";
203
+ const delivery = routing.kind === "enter-planning"
204
+ || routing.kind === "continue-planning"
205
+ || routing.kind === "active-context"
206
+ ? "queued"
207
+ : "none";
208
+ const nextStep = submissionNextStep(input);
209
+ return {
210
+ saved: { taskId: input.taskId, messageId: input.messageId },
211
+ phase,
212
+ planning,
213
+ activation,
214
+ delivery,
215
+ ...(nextStep === undefined ? {} : { nextStep })
216
+ };
217
+ }
218
+ function submissionNextStep(input) {
219
+ if (input.routing.kind === "planned-needs-manual-activation") {
220
+ return { kind: "activate-manually", taskId: input.taskId };
221
+ }
222
+ if (input.routing.kind === "activation-blocked-execution-stopped") {
223
+ return { kind: "start-execution", taskId: input.taskId };
224
+ }
225
+ if (input.routing.kind === "await-activation") {
226
+ if (input.routing.state === "failed") {
227
+ return {
228
+ kind: "resolve-failed-activation",
229
+ activationRef: input.activationRef ?? "",
230
+ failure: input.activationFailure ?? "activation failed"
231
+ };
232
+ }
233
+ return { kind: "await-pending-activation", activationRef: input.activationRef ?? "" };
234
+ }
235
+ return undefined;
236
+ }
@@ -128,8 +128,11 @@ function detailActions() {
128
128
  answerInput: answerInput,
129
129
  openTerminal: openTerminal,
130
130
  inspect: inspectRecord,
131
- sendMessage: (taskId, body, requestId) => submitMutation(taskId + "/messages",
132
- "/api/tasks/" + encodeURIComponent(taskId) + "/messages", { body, requestId }),
131
+ sendMessage: (taskId, body, requestId, intent) => submitMutation(taskId + "/messages",
132
+ "/api/tasks/" + encodeURIComponent(taskId) + "/messages",
133
+ { body, requestId, ...(intent === undefined ? {} : { intent }) }),
134
+ controlInput: (taskId, payload, requestId) => submitMutation(taskId + "/control/" + requestId,
135
+ "/api/tasks/" + encodeURIComponent(taskId) + "/control", payload),
133
136
  updateTask: (taskId, patch, requestId) => submitMutation(taskId + "/metadata",
134
137
  "/api/tasks/" + encodeURIComponent(taskId) + "/metadata", { patch, requestId }),
135
138
  panels: (taskId) => requestJson("/api/tasks/" + encodeURIComponent(taskId) + "/panels",
@@ -658,9 +661,62 @@ elements.refresh.addEventListener("click", function () { refreshDashboard(); });
658
661
  elements.operatorTerminal.addEventListener("click", function () {
659
662
  openTerminal({ scope: "global", roleName: "operator" });
660
663
  });
664
+ const globalDialog = document.querySelector("#global-input-dialog");
665
+ const globalForm = document.querySelector("#global-input-form");
666
+ const globalRole = document.querySelector("#global-input-role");
667
+ const globalAction = document.querySelector("#global-input-action");
668
+ const globalBody = document.querySelector("#global-input-body");
669
+ const globalTarget = document.querySelector("#global-input-target");
670
+ const globalThen = document.querySelector("#global-input-then");
671
+ const globalSubmit = document.querySelector("#global-input-submit");
672
+ const globalReceipt = document.querySelector("#global-input-receipt");
673
+ const globalState = document.querySelector("#global-input-state");
674
+ document.querySelector("#global-input-open").addEventListener("click", () => globalDialog.showModal());
675
+ document.querySelector("#global-input-close").addEventListener("click", () => globalDialog.close());
676
+ globalAction.addEventListener("change", () => {
677
+ document.querySelector("#global-input-body-label").hidden = globalAction.value === "interrupt";
678
+ document.querySelector("#global-input-target-label").hidden = globalAction.value === "queue";
679
+ document.querySelector("#global-input-then-label").hidden = globalAction.value !== "interrupt";
680
+ globalBody.required = globalAction.value !== "interrupt";
681
+ globalTarget.required = globalAction.value !== "queue";
682
+ });
683
+ document.querySelector("#global-input-inspect").addEventListener("click", async () => {
684
+ try {
685
+ const facts = await requestJson("/api/roles/" + encodeURIComponent(globalRole.value.trim()) + "/control");
686
+ globalState.textContent = JSON.stringify(facts, null, 2);
687
+ globalTarget.value = facts.turn?.nativeTurnId || facts.turn?.attemptId || "";
688
+ } catch (error) { globalState.textContent = error.message; }
689
+ });
690
+ globalForm.addEventListener("submit", async event => {
691
+ event.preventDefault();
692
+ if (globalSubmit.disabled) return;
693
+ globalSubmit.disabled = true;
694
+ const role = globalRole.value.trim();
695
+ const action = globalAction.value;
696
+ const requestId = crypto.randomUUID();
697
+ const input = { action, requestId };
698
+ if (action !== "interrupt") input.body = globalBody.value;
699
+ if (action !== "queue") input.expectedTarget = globalTarget.value.trim();
700
+ if (action === "interrupt" && globalThen.value.trim()) input.thenMessage = globalThen.value.trim();
701
+ globalReceipt.textContent = "Waiting for receipt · 等待回执 · " + requestId;
702
+ try {
703
+ const receipt = await submitMutation("global/" + role, "/api/roles/" + encodeURIComponent(role) + "/control", input);
704
+ const status = receipt.steer || receipt.interrupt || receipt.delivery || {};
705
+ globalReceipt.textContent = JSON.stringify(receipt);
706
+ const unknown = ["pending", "delivery-unknown", "steer-unknown", "interrupt-unknown"].includes(status.state)
707
+ || status.code === "DELIVERY_UNKNOWN";
708
+ globalSubmit.disabled = unknown;
709
+ if (!unknown && !status.code) globalBody.value = "";
710
+ } catch (error) {
711
+ globalReceipt.textContent = error.disposition === "not-submitted" ? error.message
712
+ : "Outcome unknown; read state before acting. 结果未知,请先读取状态,不要重发。 · " + requestId;
713
+ globalSubmit.disabled = error.disposition !== "not-submitted";
714
+ }
715
+ });
661
716
  elements.detailBack.addEventListener("click", clearSelection);
662
717
  elements.terminalClose.addEventListener("click", closeTerminalPanel);
663
718
  document.addEventListener("keydown", function (event) {
719
+ if (globalDialog.open) return;
664
720
  const active = document.activeElement;
665
721
  const typing = active && ["INPUT", "SELECT", "TEXTAREA"].includes(active.tagName);
666
722
  if (event.key === "Escape" && terminalPanelOpen()) {
@@ -107,6 +107,7 @@ const EXEC_STATUS_TONE = {
107
107
  "attention": "is-danger",
108
108
  "progressing-with-attention": "is-warning",
109
109
  "blocked": "is-danger",
110
+ "conflicted": "is-warning",
110
111
  "working": "is-active",
111
112
  "completed": "is-muted",
112
113
  "retired": "is-muted",