@zq-silk/yui 1.0.0 → 1.0.1

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.
@@ -827,9 +827,8 @@ function createTaskCommand(args, store, options) {
827
827
  return output(`Created Draft task ${created.task.id}: ${created.task.title}\n`
828
828
  + `Assigned role: ${created.leader.name}\n`
829
829
  + `Type: ${created.task.type ?? "unspecified"}\n`
830
- + (created.task.projectBindings.length > 0
831
- ? "Execution: Leader decides whether independent WorkItems are warranted\n"
832
- : "Execution: no Project delivery evidence required\n"), {
830
+ + "Status: Draft; discussion and development have not started\n"
831
+ + "Next: explicitly submit with intent discuss or develop when the Task should start\n", {
833
832
  task: created.task,
834
833
  leader: created.leader
835
834
  });
@@ -17,7 +17,7 @@ import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
17
17
  import { builtinAgentEndpointImplementation, validateAgentEndpointImplementation } from "../runtime/agentEndpointIdentity.js";
18
18
  import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
19
19
  import { assertProviderConversationReplaceable } from "../runtime/providerRuntimeIdentity.js";
20
- import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
20
+ import { operatorSessionTitle, resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
21
21
  import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
22
22
  import { planningRuntimeCwd } from "../storage/homeLayout.js";
23
23
  import { resolveAgentAdapter } from "./agentAdapter.js";
@@ -33,6 +33,7 @@ export class FileRoleLaunchPlanner {
33
33
  #agentEnvironment;
34
34
  #nativeAgentEnvironment;
35
35
  #createNativeSessionId;
36
+ #now;
36
37
  #cliPath;
37
38
  #inspectWorkspacePhysicalState;
38
39
  #entryPoint;
@@ -51,6 +52,7 @@ export class FileRoleLaunchPlanner {
51
52
  this.#agentEnvironment = this.#selectConfiguredAgentEnvironment(sourceEnvironment);
52
53
  this.#nativeAgentEnvironment = this.#selectConfiguredNativeEnvironment(sourceEnvironment);
53
54
  this.#createNativeSessionId = options.createNativeSessionId ?? randomUUID;
55
+ this.#now = options.now ?? (() => new Date());
54
56
  this.#inspectWorkspacePhysicalState = options.inspectWorkspacePhysicalState
55
57
  ?? inspectWorkspacePhysicalState;
56
58
  this.#cliPath = canonicalPath(options.cliPath
@@ -259,7 +261,9 @@ export class FileRoleLaunchPlanner {
259
261
  if (input.mode === "new" && sessionSet?.providerBinding != null) {
260
262
  assertProviderConversationReplaceable(sessionSet.providerBinding);
261
263
  }
262
- return this.#compile(role, input, { scope: "global" }, undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, undefined, effective, { purpose: "execution" });
264
+ return this.#compile(role, input, { scope: "global" }, input.mode === "new" && role.name === "operator"
265
+ ? operatorSessionTitle(this.#now(), this.store.getConfig().timeZone)
266
+ : undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, undefined, effective, { purpose: "execution" });
263
267
  }
264
268
  #compile(role, input, owner, sessionTitle, knownNativeSessionId, workspaceOverride, effective, sessionPolicy) {
265
269
  const launchRole = effectiveRoleForLaunch(role, effective);
@@ -389,11 +393,16 @@ export class FileRoleLaunchPlanner {
389
393
  const effectiveConfig = withNativeProjectDirectories(roleConfig, effective.executionEnvironment === undefined
390
394
  ? nativeAdditionalDirectories(effective.workspace, agentWorkspace)
391
395
  : []);
396
+ const postCreateOperatorTitle = owner.scope === "global"
397
+ && role.name === "operator"
398
+ && sessionTitle !== undefined;
392
399
  const compileInput = {
393
400
  agent,
394
401
  config: effectiveConfig,
395
402
  workspace: agentWorkspace,
396
- ...(sessionTitle === undefined ? {} : { sessionTitle }),
403
+ ...(sessionTitle === undefined || postCreateOperatorTitle
404
+ ? {}
405
+ : { sessionTitle }),
397
406
  ...sessionContext
398
407
  };
399
408
  if (input.mode === "resume"
@@ -328,7 +328,12 @@ export async function runAgentHost(input) {
328
328
  kind: "restore",
329
329
  mode: "resume",
330
330
  nativeSessionId: disconnectedSession.nativeSessionId,
331
+ // The dated Operator title is a one-shot effect of creating the
332
+ // Conversation. Reattaching this client must not replay metadata whose
333
+ // earlier outcome may be unknown.
331
334
  ...(previousControl.sessionTitle === undefined
335
+ || (currentPayload.environment.YUI_SESSION_SCOPE === "global"
336
+ && currentPayload.environment.YUI_ROLE === "operator")
332
337
  ? {}
333
338
  : { sessionTitle: previousControl.sessionTitle }),
334
339
  codexThread: disconnectedSession.configuration.threadOptions,
@@ -1,3 +1,4 @@
1
+ import { resolveTimeZone } from "../config/timeZone.js";
1
2
  const TITLE_SEPARATOR = " · ";
2
3
  export const MAX_SESSION_TITLE_LENGTH = 80;
3
4
  const TASK_TITLE_MAX_LENGTH = 20;
@@ -22,6 +23,20 @@ export function resolveTaskRoleSessionTitle(existingTitle, task, roleName) {
22
23
  }
23
24
  return taskRoleSessionTitle(task, roleName);
24
25
  }
26
+ export function operatorSessionTitle(now, configuredTimeZone) {
27
+ if (!(now instanceof Date) || !Number.isFinite(now.getTime())) {
28
+ throw new TypeError("Operator session title date is invalid.");
29
+ }
30
+ const parts = Object.fromEntries(new Intl.DateTimeFormat("en-CA", {
31
+ timeZone: resolveTimeZone(configuredTimeZone),
32
+ month: "2-digit",
33
+ day: "2-digit"
34
+ }).formatToParts(now).map(({ type, value }) => [type, value]));
35
+ if (parts.month === undefined || parts.day === undefined) {
36
+ throw new TypeError("Operator session title date is unavailable.");
37
+ }
38
+ return `Yui${TITLE_SEPARATOR}Operator${TITLE_SEPARATOR}${parts.month}${parts.day}`;
39
+ }
25
40
  function displayTitle(value, maxLength) {
26
41
  if (value.length <= maxLength)
27
42
  return value;
@@ -68,10 +68,21 @@ export async function startStructuredProviderSession(payload, input = {}) {
68
68
  ...(input.onDiagnostic === undefined ? {} : { onDiagnostic: input.onDiagnostic }),
69
69
  mirror
70
70
  });
71
+ if (isPostCreateOperatorTitle(payload)) {
72
+ input.onDiagnostic?.({
73
+ nativeSessionId: session.nativeSessionId,
74
+ failure: {
75
+ detail: "This Agent protocol does not support setting a native Session title.",
76
+ phase: "session-start",
77
+ inputDisposition: "not-accepted",
78
+ sessionDisposition: "recoverable"
79
+ }
80
+ });
81
+ }
71
82
  return Object.freeze({ session });
72
83
  }
73
84
  if (control.adapterId === "codex") {
74
- const opened = await CodexStructuredProviderSession.open(child, exit, processInstanceId, payload, control, input.onStarted, input.onTerminal, input.onGoal, input.onInput, input.onActivity, mirror);
85
+ const opened = await CodexStructuredProviderSession.open(child, exit, processInstanceId, payload, control, input.onStarted, input.onTerminal, input.onGoal, input.onInput, input.onActivity, input.onDiagnostic, mirror);
75
86
  return Object.freeze({
76
87
  session: opened.session,
77
88
  ...(opened.recoveredTerminal === undefined
@@ -81,6 +92,17 @@ export async function startStructuredProviderSession(payload, input = {}) {
81
92
  });
82
93
  }
83
94
  const session = await ClaudeStructuredProviderSession.open(child, exit, processInstanceId, control, input.onAccepted, input.onTerminal, input.onGoal, input.onActivity, input.onDiagnostic, mirror);
95
+ if (isPostCreateOperatorTitle(payload)) {
96
+ input.onDiagnostic?.({
97
+ nativeSessionId: session.nativeSessionId,
98
+ failure: {
99
+ detail: "This Agent provider does not support renaming a Session after creation.",
100
+ phase: "session-start",
101
+ inputDisposition: "not-accepted",
102
+ sessionDisposition: "recoverable"
103
+ }
104
+ });
105
+ }
84
106
  return Object.freeze({ session });
85
107
  }
86
108
  catch (error) {
@@ -88,6 +110,12 @@ export async function startStructuredProviderSession(payload, input = {}) {
88
110
  throw error;
89
111
  }
90
112
  }
113
+ function isPostCreateOperatorTitle(payload) {
114
+ return payload.providerControl?.sessionTitle !== undefined
115
+ && payload.providerControl.mode === "new"
116
+ && payload.environment.YUI_SESSION_SCOPE === "global"
117
+ && payload.environment.YUI_ROLE === "operator";
118
+ }
91
119
  /**
92
120
  * Open an uninitialized, transparent connection for a native TUI. The TUI
93
121
  * owns its requests; Yui observes only its exact startup response.
@@ -351,7 +379,7 @@ class CodexStructuredProviderSession {
351
379
  this.mirror = mirror;
352
380
  this.nativeAccountHome = nativeAccountHome;
353
381
  }
354
- static async open(child, exit, processInstanceId, payload, control, onStarted, onTerminal, onGoal, onInput, onActivity, mirror) {
382
+ static async open(child, exit, processInstanceId, payload, control, onStarted, onTerminal, onGoal, onInput, onActivity, onDiagnostic, mirror) {
355
383
  const channel = await CodexProxyWebSocketChannel.connect(child, mirror);
356
384
  const openingMessages = [];
357
385
  const stopOpeningBuffer = channel.onMessage((message) => openingMessages.push(message));
@@ -385,10 +413,22 @@ class CodexStructuredProviderSession {
385
413
  }
386
414
  }
387
415
  if (control.sessionTitle !== undefined) {
388
- await runtime.setConversationName({
389
- conversationId,
390
- name: control.sessionTitle
391
- });
416
+ try {
417
+ await runtime.setConversationName({
418
+ conversationId,
419
+ name: control.sessionTitle
420
+ });
421
+ }
422
+ catch (error) {
423
+ onDiagnostic?.({
424
+ nativeSessionId: conversationId,
425
+ failure: providerDeliveryFailureFrom(error, {
426
+ phase: "session-start",
427
+ inputDisposition: "not-accepted",
428
+ sessionDisposition: "recoverable"
429
+ })
430
+ });
431
+ }
392
432
  }
393
433
  const session = new CodexStructuredProviderSession(child, exit, processInstanceId, conversationId, runtime, onStarted, onTerminal, onGoal, onInput, onActivity, mirror, optionalId(initialized.codexHome));
394
434
  session.#activeTurnId = resumedActiveTurnId;
@@ -11,6 +11,13 @@ notifications cannot restart an explicitly stopped Session; normal Host detach
11
11
  preserves an active Session and remains reconnectable. A switch after a partial
12
12
  failure still settles the retained input before selecting a new conversation.
13
13
 
14
+ A newly created global Operator conversation requests the native title
15
+ `Yui · Operator · MMdd`, using Yui's configured time zone. The request is made
16
+ only after the provider returns the exact new conversation identity; restore
17
+ and reconnect paths never replay it. Providers without a post-create rename
18
+ capability keep the usable Session and expose a diagnostic instead of treating
19
+ metadata failure as Session-creation failure.
20
+
14
21
  ## Authority
15
22
 
16
23
  Task, WorkItem, Message, Decision, Artifact and Project Knowledge preserve
@@ -8,6 +8,11 @@ Provider binding,再结算执行占用。Global 停止证据保存为仅记录
8
8
  不触发新的原生输入。待投递通知不能重启已明确停止的 Session;普通 Host 脱离仍
9
9
  保留 active Session,可正常重连。部分切换失败后,仍须先结算旧输入再选择新会话。
10
10
 
11
+ 新建 global Operator 对话会按 Yui 当前配置时区请求原生标题
12
+ `Yui · Operator · MMdd`。只有 Provider 返回新对话的确切身份后才执行该请求;
13
+ 恢复与重连路径不会重放。若 Provider 不支持创建后改名,已创建的 Session 仍保持
14
+ 可用,并留下诊断,而不会把元数据失败冒充为 Session 创建失败。
15
+
11
16
  ## 权威
12
17
 
13
18
  Task、WorkItem、Message、Decision、Artifact 和 Project Knowledge 保存持久工作。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -74,13 +74,30 @@ determine Task identity. They also do not determine WorkItem count. Let
74
74
  isolated workspaces and Integration handle independent Git changes.
75
75
 
76
76
  Keep the Task title concise and put detailed intent, constraints, and evidence
77
- in its description or routed Message. The examples below are separate
78
- operations, not an automatic create/submit/activate sequence. For creation-only
79
- intent, save the Task without starting planning. Discussion does not authorize
80
- delivery; follow the Leader's [planning and activation boundary](../yui-leader/references/planning.md)
81
- before activation. Queries, record-only input and discussion do not authorize
82
- reopening. An explicit request to continue the same completed result can authorize
83
- the necessary reopening; follow the delivery guidance below.
77
+ in its description or routed Message.
78
+
79
+ Treat Task creation and Task submission as separate user-authorized actions.
80
+ When the user asks only to add, create, register, or record a Task, create the
81
+ Draft and write all known requirements into its metadata, then stop. Do not
82
+ submit a `discuss` or `develop` Message, request activation, start a Leader
83
+ Session, or create execution resources. A detailed or immediately actionable
84
+ requirement, or the user's desire for its eventual completion, is not permission
85
+ to start it. Report that the Task is a Draft and has not started, and explain
86
+ that the user can explicitly ask to discuss/plan it or to develop/implement it.
87
+
88
+ Start the corresponding route only when the user's current request contains
89
+ that additional intent: discussion or planning authorizes `discuss`;
90
+ development, implementation, fixing, continuing execution, or immediate
91
+ progress authorizes `develop` or the applicable lifecycle continuation. Do not
92
+ weaken explicit intent merely because the request also says “create a Task.”
93
+ The examples below are separate operations, never an automatic
94
+ create/submit/activate sequence.
95
+
96
+ Discussion does not authorize delivery; follow the Leader's
97
+ [planning and activation boundary](../yui-leader/references/planning.md) before
98
+ activation. Queries, record-only input and discussion do not authorize
99
+ reopening. An explicit request to continue the same completed result can
100
+ authorize the necessary reopening; follow the delivery guidance below.
84
101
 
85
102
  ```sh
86
103
  yui operator submit "<related request and delta>" --task <task-id> --intent discuss
@@ -94,14 +111,17 @@ Inspect an existing request before creating another. `task activate <task-id>`
94
111
  can adopt an eligible request in the foreground; it never activates a Draft
95
112
  without a recorded request and environment plan.
96
113
 
97
- Choose the submission intent explicitly; it is never inferred from the message
98
- text. `discuss` (the default) routes the Draft to
99
- planning; `record` saves the message without waking the Leader; `develop` asks an
100
- unplanned Draft to activate now, and reports the exact next step when it cannot
101
- (already planned → request activation explicitly; execution stopped → start it first). Pass
102
- `--request-id <key>` to make a submission idempotent: retrying the same key
103
- returns the original message and routing instead of creating a duplicate, and the
104
- same key with different text is refused as a conflict.
114
+ After the user has authorized a submission, choose its intent explicitly; it is
115
+ never inferred by the CLI from the message text. `discuss` (the CLI default when
116
+ `operator submit` is already being invoked) routes the Draft to planning;
117
+ `record` saves the message without waking the Leader; `develop` asks an unplanned
118
+ Draft to activate now, and reports the exact next step when it cannot (already
119
+ planned → request activation explicitly; execution stopped → start it first).
120
+ The CLI default is not permission for the Operator to invoke submission after a
121
+ creation-only request. Pass `--request-id <key>` to make a submission idempotent:
122
+ retrying the same key returns the original message and routing instead of
123
+ creating a duplicate, and the same key with different text is refused as a
124
+ conflict.
105
125
 
106
126
  Resolve all known Projects before repository-backed execution. A stable Project
107
127
  checkout is read-only reference state, not the Task base authority. Yui records