@zq-silk/yui 2.16.0 → 2.17.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 (69) hide show
  1. package/dist/cli/commandCatalog.js +17 -11
  2. package/dist/cli/interactionCandidates.js +20 -5
  3. package/dist/cli/interactionPolicy.js +5 -4
  4. package/dist/cli/roleWizard.js +7 -0
  5. package/dist/commands/globalRoleCommands.js +13 -2
  6. package/dist/commands/operatorCommands.js +2 -0
  7. package/dist/commands/profileCommands.js +19 -2
  8. package/dist/commands/roleConfiguration.js +3 -0
  9. package/dist/commands/taskCommands.js +16 -0
  10. package/dist/commands/taskRoleRuntimeStatus.js +3 -0
  11. package/dist/controller/agentJobControl.js +9 -1
  12. package/dist/controller/fileSchedulerStoreAdapter.js +35 -4
  13. package/dist/controller/globalInputDelivery.js +14 -8
  14. package/dist/controller/providerRetryAdmission.js +9 -0
  15. package/dist/controller/providerRetryDelivery.js +16 -0
  16. package/dist/controller/runtime.js +14 -0
  17. package/dist/controller/runtimeLaunchCoordinator.js +3 -1
  18. package/dist/executor/fileRoleLaunchPlanner.js +3 -0
  19. package/dist/message/messageContinuation.js +4 -0
  20. package/dist/output/rolePresentation.js +3 -0
  21. package/dist/profile/agentProfile.js +8 -0
  22. package/dist/profile/agentProfileRuntime.js +16 -1
  23. package/dist/review/reviewDecision.js +6 -0
  24. package/dist/role/availability.js +28 -0
  25. package/dist/role/role.js +6 -0
  26. package/dist/runtime/codexNativeAccess.js +21 -4
  27. package/dist/runtime/tmuxAdapters.js +5 -0
  28. package/dist/scheduler/activeRoleRunDelivery.js +9 -0
  29. package/dist/scheduler/leaderWakeupProcessor.js +8 -1
  30. package/dist/scheduler/roleRunLiveness.js +3 -0
  31. package/dist/storage/sqliteSchema.js +10 -0
  32. package/dist/storage/storageVersions.js +1 -1
  33. package/dist/web/assets/client/app/main.js +15 -22
  34. package/dist/web/assets/client/app/taskView.js +7 -11
  35. package/dist/web/assets/client/i18n/en.js +62 -18
  36. package/dist/web/assets/client/i18n/zh-CN.js +62 -18
  37. package/dist/web/assets/client/index.js +2 -0
  38. package/dist/web/assets/client/layout/shortcuts.js +2 -2
  39. package/dist/web/assets/client/layout/workspace.js +99 -112
  40. package/dist/web/assets/client/ui/primitives.js +15 -2
  41. package/dist/web/assets/client/views/dock/conversation.js +421 -123
  42. package/dist/web/assets/client/views/dock/session.js +229 -0
  43. package/dist/web/assets/client/views/dock/terminal.js +48 -39
  44. package/dist/web/assets/client/views/overview.js +2 -2
  45. package/dist/web/assets/client/views/task/page.js +2 -12
  46. package/dist/web/assets/shell/dock.js +9 -27
  47. package/dist/web/assets/styles/views/conversation.js +96 -17
  48. package/dist/web/assets/styles/views/dock.js +17 -5
  49. package/dist/web/assets/styles/views/task.js +1 -2
  50. package/dist/web/assets/styles/views/terminal.js +12 -8
  51. package/dist/web/webConversation.js +1 -0
  52. package/dist/web/webSettings.js +10 -2
  53. package/docs/managed-turn-and-session-runtime.md +10 -0
  54. package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
  55. package/docs/release-workflow.md +24 -0
  56. package/docs/release-workflow.zh-CN.md +19 -0
  57. package/docs/roles-and-configuration.md +45 -1
  58. package/docs/roles-and-configuration.zh-CN.md +35 -0
  59. package/docs/storage-baseline.md +6 -1
  60. package/docs/storage-baseline.zh-CN.md +5 -1
  61. package/docs/testing/review-evaluation.md +35 -1
  62. package/docs/testing/review-evaluation.zh-CN.md +25 -1
  63. package/docs/testing/verification-levels.md +2 -2
  64. package/docs/testing/verification-levels.zh-CN.md +2 -2
  65. package/package.json +1 -1
  66. package/skills/yui-leader/VERSION +1 -1
  67. package/skills/yui-leader/references/execution.md +19 -8
  68. package/skills/yui-reviewer/SKILL.md +13 -2
  69. package/skills/yui-reviewer/VERSION +1 -1
@@ -176,13 +176,15 @@ const roleChildren = [
176
176
  },
177
177
  { name: "list", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "global"] }, summary: "List global Roles." },
178
178
  { name: "show", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "global"] }, summary: "Show one global Role.", usage: "yui config role show <name>" },
179
+ { name: "enable", discovery: { surface: "public", audiences: ["public", "operator"] }, summary: "Enable new execution and template selection; does not start a Session.", usage: "yui config role enable <name>" },
180
+ { name: "disable", discovery: { surface: "public", audiences: ["public", "operator"] }, summary: "Disable new execution and template selection; keeps running Sessions and input.", usage: "yui config role disable <name>" },
179
181
  {
180
182
  name: "update", discovery: { surface: "public", audiences: ["public", "operator"] },
181
183
  summary: "Update a global Role.",
182
184
  usage: "yui config role update <name> [profile options] [clear options]",
183
- options: ["--agent", "--workspace", ...roleProfileOptions, ...roleAgentOptions,
185
+ options: ["--enabled", "--agent", "--workspace", ...roleProfileOptions, ...roleAgentOptions,
184
186
  ...roleProfileClearOptions, ...roleAgentClearOptions, "--yes"],
185
- optionValues: roleAgentOptionValues,
187
+ optionValues: { ...roleAgentOptionValues, "--enabled": ["true", "false"] },
186
188
  fileOptions: ["--workspace"]
187
189
  },
188
190
  { name: "remove", discovery: { surface: "public", audiences: ["public", "operator"] }, summary: "Remove a global Role.", usage: "yui config role remove <name>" },
@@ -238,17 +240,19 @@ const profileChildren = [
238
240
  name: "add", discovery: { surface: "public", audiences: ["public", "operator"] },
239
241
  summary: "Add a reusable Agent Profile that inherits Worker runtime or selects one explicit Agent.",
240
242
  usage: "yui config profile add <id> [--access <read|write>] [--inherit-worker | --agent <id> [--model <model>] [--effort <effort>]] [Profile settings]",
241
- options: ["--access", ...agentProfileOptions],
242
- optionValues: { "--access": ["read", "write"] }
243
+ options: ["--enabled", "--access", ...agentProfileOptions],
244
+ optionValues: { "--enabled": ["true", "false"], "--access": ["read", "write"] }
243
245
  },
244
246
  { name: "list", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "global"] }, summary: "List Agent Profiles." },
245
247
  { name: "show", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "global"] }, summary: "Show one Agent Profile.", usage: "yui config profile show <id>" },
248
+ { name: "enable", discovery: { surface: "public", audiences: ["public", "operator"] }, summary: "Enable new Profile applications; existing Task bindings are unchanged.", usage: "yui config profile enable <id>" },
249
+ { name: "disable", discovery: { surface: "public", audiences: ["public", "operator"] }, summary: "Disable new Profile applications without deleting configuration.", usage: "yui config profile disable <id>" },
246
250
  {
247
251
  name: "update", discovery: { surface: "public", audiences: ["public", "operator"] },
248
252
  summary: "Update an Agent Profile's behavior or runtime source.",
249
253
  usage: "yui config profile update <id> [--access <read|write>] [--inherit-worker | [--agent <id>] [--model <model>|--clear-model] [--effort <effort>|--clear-effort]] [Profile settings]",
250
- options: ["--access", ...agentProfileOptions, ...agentProfileClearOptions],
251
- optionValues: { "--access": ["read", "write"] }
254
+ options: ["--enabled", "--access", ...agentProfileOptions, ...agentProfileClearOptions],
255
+ optionValues: { "--enabled": ["true", "false"], "--access": ["read", "write"] }
252
256
  },
253
257
  { name: "remove", discovery: { surface: "public", audiences: ["public", "operator"] }, summary: "Remove a custom Agent Profile.", usage: "yui config profile remove <id>" },
254
258
  { name: "reset", discovery: { surface: "public", audiences: ["public", "operator"] }, summary: "Reset all built-in Agent Profiles." }
@@ -688,7 +692,7 @@ const taskChildren = [
688
692
  name: "role", discovery: { surface: "managed", audiences: ["operator", "leader"] },
689
693
  summary: "Manage Roles within a Task.",
690
694
  sections: [{ id: "manage", title: "Commands", entries: [
691
- "add", "list", "status", "show", "update", "remove", "bind", "unbind",
695
+ "add", "list", "status", "show", "update", "enable", "disable", "remove", "bind", "unbind",
692
696
  "capabilities", "session", "interrupt", "view", "takeover", "release"
693
697
  ] }],
694
698
  children: [
@@ -707,6 +711,8 @@ const taskChildren = [
707
711
  options: ["--cursor"]
708
712
  },
709
713
  { name: "show", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Show one Task Role.", usage: "yui task role show <task> <role> [--cursor <cursor>]", options: ["--cursor"] },
714
+ { name: "enable", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Enable this Task Role; does not change Task execution authority.", usage: "yui task role enable <task> <role>" },
715
+ { name: "disable", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Block new execution of this Task Role without stopping its current Run.", usage: "yui task role disable <task> <role>" },
710
716
  { name: "capabilities", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Read native options using this Role's desired or exact failed launch configuration.",
711
717
  usage: "yui task role capabilities <task> <role> [--error <event-id>] [--refresh]",
712
718
  options: ["--error", "--refresh"] },
@@ -714,9 +720,9 @@ const taskChildren = [
714
720
  name: "update", discovery: { surface: "managed", audiences: ["operator", "leader"] },
715
721
  summary: "Update a Task Role; Agent settings target the named or active binding without switching it.",
716
722
  usage: "yui task role update <task> <role> [--profile <id>] [--agent <id>] [--environment <preparation-id> | --managed-environment] [Role and Agent settings]",
717
- options: ["--profile", "--agent", "--environment", "--managed-environment", ...roleProfileOptions, ...roleAgentOptions,
723
+ options: ["--enabled", "--profile", "--agent", "--environment", "--managed-environment", ...roleProfileOptions, ...roleAgentOptions,
718
724
  ...roleProfileClearOptions, ...roleAgentClearOptions, "--yes"],
719
- optionValues: roleAgentOptionValues
725
+ optionValues: { ...roleAgentOptionValues, "--enabled": ["true", "false"] }
720
726
  },
721
727
  { name: "remove", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Remove a Task Role.", usage: "yui task role remove <task> <role>" },
722
728
  { name: "bind", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Bind and activate an Agent for a Task Role.", usage: "yui task role bind <task> <role> <agent-id>" },
@@ -1394,7 +1400,7 @@ export const ROOT_COMMAND = buildNode({
1394
1400
  examples: ["yui config profile list", "yui config profile show implementer"],
1395
1401
  sections: [
1396
1402
  { id: "inspect", title: "Inspect", entries: ["list", "show"] },
1397
- { id: "manage", title: "Manage", entries: ["add", "update", "remove", "reset"] }
1403
+ { id: "manage", title: "Manage", entries: ["add", "update", "enable", "disable", "remove", "reset"] }
1398
1404
  ],
1399
1405
  children: profileChildren
1400
1406
  },
@@ -1404,7 +1410,7 @@ export const ROOT_COMMAND = buildNode({
1404
1410
  examples: ["yui config role list", "yui config role show operator"],
1405
1411
  sections: [
1406
1412
  { id: "inspect", title: "Inspect", entries: ["list", "show"] },
1407
- { id: "manage", title: "Manage", entries: ["add", "update", "remove", "bind", "unbind"] }
1413
+ { id: "manage", title: "Manage", entries: ["add", "update", "enable", "disable", "remove", "bind", "unbind"] }
1408
1414
  ],
1409
1415
  children: roleChildren
1410
1416
  },
@@ -21,9 +21,22 @@ export async function getSelectionCandidates(selector, ports, args) {
21
21
  return { ...set, defaultValue };
22
22
  }
23
23
  case "agent-profiles":
24
- return entities("Agent Profile", "Select Agent Profile", await list(ports, "profile.list", {}), ["id", "revision", "defaultAccess", "agentId"]);
25
- case "global-roles":
26
- return entities("global role", "Select global Role", await list(ports, "role.list", {}), ["name", "activeAgentId", "workspace"]);
24
+ return entities("Agent Profile", "Select Agent Profile", (await list(ports, "profile.list", {}))
25
+ .filter(p => selector.actionTarget || p.enabled !== false
26
+ && p.effectiveRuntime?.status !== "unavailable")
27
+ .map(p => ({ ...p, availability: p.enabled === false ? "disabled" : "enabled" })), ["id", "availability", "revision", "defaultAccess", "agentId"]);
28
+ case "global-roles": {
29
+ let roles = await list(ports, "role.list", {});
30
+ // Review uses a Task-local binding first; a disabled same-named template
31
+ // must not hide an independently enabled existing Reviewer (or vice versa).
32
+ if (args[0] === "task" && args[1] === "review" && args[2] === "request" && args[3] !== undefined) {
33
+ const local = await list(ports, "task.role.list", { taskId: args[3] });
34
+ roles = [...roles.filter(r => !local.some(t => t.name === r.name)), ...local];
35
+ }
36
+ return entities("global role", "Select global Role", roles
37
+ .filter(r => selector.actionTarget && !(args[0] === "session" && args[1] === "enter") || r.enabled !== false)
38
+ .map(r => ({ ...r, availability: r.enabled === false ? "disabled" : "enabled" })), ["name", "availability", "activeAgentId", "workspace"]);
39
+ }
27
40
  case "jobs":
28
41
  return entities("job", "Select job", await list(ports, "jobs.list", {}), ["id", "type", "status"]);
29
42
  case "input-requests": {
@@ -41,9 +54,11 @@ export async function getSelectionCandidates(selector, ports, args) {
41
54
  const taskId = dependencyValue(selector, args);
42
55
  if (taskId === undefined)
43
56
  return null;
44
- const roles = orderRoleOptions(await list(ports, "task.role.list", { taskId }));
57
+ const roles = orderRoleOptions((await list(ports, "task.role.list", { taskId }))
58
+ .filter(r => selector.actionTarget || r.enabled !== false))
59
+ .map(r => ({ ...r, availability: r.enabled === false ? "disabled" : "enabled" }));
45
60
  return entities("task role", `Select Task role: ${taskId}`, roles, [
46
- "name", "kind", "activeAgentId"
61
+ "name", "availability", "kind", "activeAgentId"
47
62
  ]);
48
63
  }
49
64
  case "task-decisions": {
@@ -92,7 +92,7 @@ export const INTERACTION_POLICIES = Object.freeze([
92
92
  "--search": "value"
93
93
  }
94
94
  },
95
- ...["show", "update", "remove", "enter", "context"].map((command) => ({
95
+ ...["show", "update", "remove", "enable", "disable", "enter", "context"].map((command) => ({
96
96
  commandPath: command === "enter" || command === "context"
97
97
  ? ["session", command]
98
98
  : ["config", "role", command],
@@ -129,12 +129,13 @@ export const INTERACTION_POLICIES = Object.freeze([
129
129
  commandPath: ["config", "profile", "add"],
130
130
  selectors: [],
131
131
  trailingOptions: {
132
+ "--enabled": "value",
132
133
  "--description": "value", "--instructions": "value",
133
134
  "--skill": "value", "--agent": "value", "--model": "value",
134
135
  "--effort": "value", "--access": "value", "--inherit-worker": "flag"
135
136
  }
136
137
  },
137
- ...["show", "update", "remove"].map((command) => ({
138
+ ...["show", "update", "remove", "enable", "disable"].map((command) => ({
138
139
  commandPath: ["config", "profile", command],
139
140
  selectors: [{
140
141
  argumentIndex: 3,
@@ -145,7 +146,7 @@ export const INTERACTION_POLICIES = Object.freeze([
145
146
  ...(command === "update"
146
147
  ? {
147
148
  trailingOptions: {
148
- "--description": "option-like-value",
149
+ "--enabled": "value", "--description": "option-like-value",
149
150
  "--instructions": "option-like-value", "--skill": "value",
150
151
  "--agent": "value", "--inherit-worker": "flag",
151
152
  "--model": "option-like-value", "--effort": "option-like-value",
@@ -380,7 +381,7 @@ export const INTERACTION_POLICIES = Object.freeze([
380
381
  commandPath: ["task", "role", "list"],
381
382
  selectors: [{ argumentIndex: 3, entity: "task", provider: "tasks", actionTarget: true }]
382
383
  },
383
- ...["show", "status", "update", "remove"].map((command) => ({
384
+ ...["show", "status", "update", "remove", "enable", "disable"].map((command) => ({
384
385
  commandPath: ["task", "role", command],
385
386
  selectors: [
386
387
  { argumentIndex: 3, entity: "task", provider: "tasks", actionTarget: true },
@@ -281,6 +281,7 @@ async function settingsSection(title, io, taskRole) {
281
281
  }
282
282
  async function updateRoleSettings(args, role, ports, io, taskRole) {
283
283
  const fields = [
284
+ { value: "enabled", cells: ["Enabled (not Session state)", role.enabled === false ? "false" : "true"] },
284
285
  profileChoice("description", "Description", role.description),
285
286
  profileChoice("responsibilities", "Responsibilities", role.responsibilities),
286
287
  profileChoice("constraints", "Constraints", role.constraints),
@@ -292,6 +293,11 @@ async function updateRoleSettings(args, role, ports, io, taskRole) {
292
293
  const field = await choose("Role settings", fields, [TEXT_COLUMN, { header: "Current", minWidth: 12, maxWidth: 52 }], io, undefined, "field");
293
294
  if (field === undefined)
294
295
  return { kind: "cancelled", args };
296
+ if (field === "enabled") {
297
+ const value = await choose("Allow new execution; running Sessions are not stopped", [{ value: "true", cells: ["Enabled"] }, { value: "false", cells: ["Disabled"] }], [TEXT_COLUMN], io, role.enabled === false ? "false" : "true", "enabled");
298
+ return value === undefined ? { kind: "cancelled", args }
299
+ : { kind: "resolved", args: [...args, "--enabled", value] };
300
+ }
295
301
  if (field === "active-agent") {
296
302
  const target = await selectActiveAgent(role, ports, io);
297
303
  if (target === undefined)
@@ -672,6 +678,7 @@ function asRole(value) {
672
678
  }));
673
679
  return {
674
680
  name,
681
+ enabled: input.enabled !== false,
675
682
  activeAgentId,
676
683
  agentBindings,
677
684
  ...(stringField(input, "workspace") === undefined ? {} : { workspace: stringField(input, "workspace") }),
@@ -10,6 +10,7 @@ import { inboxEntryCategory } from "../message/inboxTurn.js";
10
10
  import { checkCurrentSkills, SKILL_REUSE_RULE } from "../context/currentSkills.js";
11
11
  import { runtimeDiagnosticGroups, runtimeDiagnosticEvidence } from "../controller/globalRuntimeAttention.js";
12
12
  import { roleSkillIds } from "../context/roleSessionContext.js";
13
+ import { isEnabled } from "../role/availability.js";
13
14
  import { createRoleSessionSet, recordRoleAgentSession } from "../executor/agentExecutor.js";
14
15
  import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
15
16
  import { managedGlobalRoleWorkspace } from "../storage/homeLayout.js";
@@ -96,6 +97,12 @@ export function runGlobalRoleCommand(args, store, options = {}) {
96
97
  case "show": return showRole(rest, store);
97
98
  case "context": return roleContext(rest, store, options);
98
99
  case "update": return updateRole(rest, store, options);
100
+ case "enable":
101
+ case "disable": {
102
+ if (rest.length !== 1)
103
+ throw usageError(`Usage: yui config role ${command} <role>`);
104
+ return updateRole([rest[0], "--enabled", String(command === "enable")], store, options);
105
+ }
99
106
  case "remove": return removeRole(rest, store);
100
107
  case "bind": return bindRole(rest, store);
101
108
  case "unbind": return unbindRole(rest, store);
@@ -292,7 +299,7 @@ function listRoles(args, store) {
292
299
  for (const name of SYSTEM_ROLE_NAMES) {
293
300
  const role = store.getGlobalRole(name);
294
301
  rows.set(name, role === null
295
- ? [name, "system", "?", "?", "?", "?"]
302
+ ? [name, "system", "?", "?", "?", "?", "?"]
296
303
  : roleListRow(role, "system"));
297
304
  }
298
305
  for (const role of store.listGlobalRoles()) {
@@ -305,6 +312,7 @@ function listRoles(args, store) {
305
312
  return `${renderTable("Roles", [
306
313
  { header: "Role", minWidth: 4, maxWidth: 24 },
307
314
  { header: "Kind", minWidth: 6, maxWidth: 8 },
315
+ { header: "Enabled", minWidth: 7, maxWidth: 8 },
308
316
  { header: "Active Agent", minWidth: 8, maxWidth: 20 },
309
317
  { header: "Model", minWidth: 8, maxWidth: 22 },
310
318
  { header: "Effort", minWidth: 8, maxWidth: 14 },
@@ -344,6 +352,9 @@ function updateRole(args, store, options) {
344
352
  const workspace = trimmed(parsed.one("--workspace"));
345
353
  const next = store.transaction((tx) => {
346
354
  const role = requireRole(name, tx);
355
+ if (parsed.seen.size === 1 && parsed.has("--enabled")
356
+ && isEnabled(role) === roleProfilePatch(parsed).enabled)
357
+ return role;
347
358
  const changesLaunchContext = hasRoleLaunchContextOptions(parsed);
348
359
  const changesAgentConfig = hasAgentConfigOptions(parsed);
349
360
  if (changesLaunchContext || changesAgentConfig) {
@@ -971,7 +982,7 @@ function presentRole(title, role, store) {
971
982
  }
972
983
  function roleListRow(role, kind) {
973
984
  const summary = activeRoleSummary(role);
974
- return [role.name, kind, summary.agent, summary.model, summary.effort, role.workspace];
985
+ return [role.name, kind, String(isEnabled(role)), summary.agent, summary.model, summary.effort, role.workspace];
975
986
  }
976
987
  function renderMissingSystemRole(name) {
977
988
  return [
@@ -1,4 +1,5 @@
1
1
  import { agentAdapterLabel as adapterLabel } from "../agent/adapterCatalog.js";
2
+ import { assertRoleEnabled } from "../role/availability.js";
2
3
  import { usageError } from "../errors/cliError.js";
3
4
  import { createRoleSessionSet } from "../executor/agentExecutor.js";
4
5
  import { TASK_SUBMISSION_INTENTS } from "../message/message.js";
@@ -222,6 +223,7 @@ function resumeSession(args, store) {
222
223
  export function applyOperatorSessionControl(control, store, now = new Date()) {
223
224
  return store.transaction((tx) => {
224
225
  const role = requireOperator(tx);
226
+ assertRoleEnabled(role);
225
227
  const binding = role.agentBindings[control.targetAgentId];
226
228
  if (binding === undefined) {
227
229
  throw usageError(`Operator Agent is not bound: ${control.targetAgentId}.`);
@@ -1,4 +1,5 @@
1
1
  import { usageError } from "../errors/cliError.js";
2
+ import { isEnabled, parseEnabled } from "../role/availability.js";
2
3
  import { defaultTableWidth, renderTable } from "../output/table.js";
3
4
  import { BUILTIN_PROFILE_IDS, builtinAgentProfileInputs, createAgentProfile, updateAgentProfile } from "../profile/agentProfile.js";
4
5
  import { resolveAgentProfileRuntime, resolveAgentProfileView } from "../profile/agentProfileRuntime.js";
@@ -35,7 +36,7 @@ export function previewProfileAgentConfigurationMutation(args, store) {
35
36
  else {
36
37
  return undefined;
37
38
  }
38
- const runtime = resolveAgentProfileRuntime(profile, store);
39
+ const runtime = resolveAgentProfileRuntime(profile, store, { configurationOnly: true });
39
40
  if (runtime.status === "unavailable")
40
41
  throw usageError(runtime.reason);
41
42
  return {
@@ -51,6 +52,11 @@ export function runProfileCommand(args, store, now = () => new Date(), options =
51
52
  case "list": return listProfiles(rest, store);
52
53
  case "show": return showProfile(rest, store);
53
54
  case "update": return updateProfile(rest, store, now(), options);
55
+ case "enable":
56
+ case "disable": {
57
+ const id = oneArg(rest, `Usage: yui config profile ${command} <id>`);
58
+ return updateProfile([id, "--enabled", String(command === "enable")], store, now(), options);
59
+ }
54
60
  case "remove": return removeProfile(rest, store);
55
61
  case "reset": return resetProfiles(rest, store, now());
56
62
  default:
@@ -68,6 +74,7 @@ function addProfile(args, store, now, options) {
68
74
  assertOptionPairs(parsed);
69
75
  const profile = createAgentProfile({
70
76
  id,
77
+ ...(parsed.has("--enabled") ? { enabled: parseEnabled(parsed.one("--enabled")) } : {}),
71
78
  defaultAccess: parseAccess(parsed.one("--access") ?? "read"),
72
79
  ...profileValues(parsed),
73
80
  runtime: addedRuntime(parsed, usage)
@@ -91,6 +98,7 @@ function listProfiles(args, store) {
91
98
  ? "No Agent Profiles found.\n"
92
99
  : `${renderTable("Agent Profiles", [
93
100
  { header: "Profile", minWidth: 7, maxWidth: 24 },
101
+ { header: "Enabled", minWidth: 7, maxWidth: 8 },
94
102
  { header: "Revision", minWidth: 8, maxWidth: 10 },
95
103
  { header: "Access", minWidth: 6, maxWidth: 8 },
96
104
  { header: "Runtime", minWidth: 9, maxWidth: 14 },
@@ -109,6 +117,7 @@ function showProfile(args, store) {
109
117
  return {
110
118
  output: `${[
111
119
  `Agent Profile: ${profile.id}`,
120
+ `Enabled: ${isEnabled(profile)}`,
112
121
  `Revision: ${profile.revision}`,
113
122
  `Default access: ${profile.defaultAccess}`,
114
123
  `Description: ${profile.description ?? "-"}`,
@@ -140,8 +149,14 @@ function updateProfile(args, store, now, options) {
140
149
  throw usageError("At least one Profile option is required.", usage);
141
150
  assertOptionPairs(parsed);
142
151
  const current = requireProfile(store, id);
152
+ if (parsed.seen.size === 1 && parsed.has("--enabled")
153
+ && isEnabled(current) === parseEnabled(parsed.one("--enabled"))) {
154
+ return { output: `Agent Profile ${id} is already ${isEnabled(current) ? "enabled" : "disabled"}\n`,
155
+ data: resolveAgentProfileView(current, store) };
156
+ }
143
157
  const runtime = updatedRuntime(current, parsed, usage);
144
158
  const updated = updateAgentProfile(current, {
159
+ ...(parsed.has("--enabled") ? { enabled: parseEnabled(parsed.one("--enabled")) } : {}),
145
160
  ...(parsed.has("--access") ? { defaultAccess: parseAccess(parsed.one("--access")) } : {}),
146
161
  ...profilePatch(parsed),
147
162
  ...(runtime === undefined ? {} : { runtime })
@@ -187,6 +202,7 @@ function resetProfiles(args, store, now) {
187
202
  };
188
203
  }
189
204
  const BASE_OPTIONS = [
205
+ ["--enabled", "value"],
190
206
  ["--access", "value"],
191
207
  ["--description", "value"],
192
208
  ["--instructions", "value"],
@@ -307,7 +323,7 @@ function assertRuntimeSourceOptions(parsed, usage) {
307
323
  function validateExplicitRuntime(profile, store, options) {
308
324
  if (profile.runtime.source !== "explicit")
309
325
  return;
310
- const runtime = resolveAgentProfileRuntime(profile, store);
326
+ const runtime = resolveAgentProfileRuntime(profile, store, { configurationOnly: true });
311
327
  if (runtime.status === "unavailable")
312
328
  throw usageError(runtime.reason);
313
329
  options.validateAgentConfiguration?.({
@@ -320,6 +336,7 @@ function profileListRow(view) {
320
336
  const { profile, runtime } = view;
321
337
  return [
322
338
  profile.id,
339
+ String(isEnabled(profile)),
323
340
  String(profile.revision),
324
341
  profile.defaultAccess,
325
342
  runtime.source,
@@ -1,4 +1,5 @@
1
1
  import { usageError } from "../errors/cliError.js";
2
+ import { parseEnabled } from "../role/availability.js";
2
3
  import { defaultRoleAgentConfig, resolveAgentAdapter } from "../executor/agentAdapter.js";
3
4
  const PROFILE_OPTIONS = [
4
5
  ["--description", "value"],
@@ -64,6 +65,7 @@ export function roleOptionSpecs(input) {
64
65
  ...(input.includeAgent === true ? [["--agent", "value"]] : []),
65
66
  ...(input.includeWorkspace === true ? [["--workspace", "value"]] : []),
66
67
  ...(input.update ? [["--yes", "flag"]] : []),
68
+ ...(input.update ? [["--enabled", "value"]] : []),
67
69
  ...PROFILE_OPTIONS,
68
70
  ...agentValueOptions,
69
71
  ...agentRepeatableOptions,
@@ -123,6 +125,7 @@ export function roleProfilePatch(parsed) {
123
125
  ["--skill", "--clear-skills"]
124
126
  ]);
125
127
  return {
128
+ ...(parsed.has("--enabled") ? { enabled: parseEnabled(parsed.one("--enabled")) } : {}),
126
129
  ...(parsed.has("--description") ? { description: requiredText(parsed.one("--description")) } : {}),
127
130
  ...(parsed.has("--clear-description") ? { description: undefined } : {}),
128
131
  ...(parsed.has("--responsibility")
@@ -3,6 +3,7 @@ import { isDeepStrictEqual } from "node:util";
3
3
  import { taskMessageInboxReceipt } from "../message/taskInboxOwnership.js";
4
4
  import { agentExecutionComponentLabel } from "../agent/executionComponents.js";
5
5
  import { assertLeaderArchiveAdmission } from "../task/leaderArchiveAuthority.js";
6
+ import { assertRoleEnabled, isEnabled } from "../role/availability.js";
6
7
  import { createRun, runExecutionObservation, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
7
8
  import { isGitArtifactRefString, parseGitArtifactRef } from "../artifacts/gitArtifactRef.js";
8
9
  import { contextContentDigest, contextSnapshotRef } from "../context/contextSnapshot.js";
@@ -2251,6 +2252,11 @@ function taskRoleCommand(args, store, options) {
2251
2252
  return showTaskRole(rest, store);
2252
2253
  if (command === "update")
2253
2254
  return updateTaskRole(rest, store, options);
2255
+ if (command === "enable" || command === "disable") {
2256
+ if (rest.length !== 2)
2257
+ throw usageError(`Usage: yui task role ${command} <task> <role>`);
2258
+ return updateTaskRole([...rest, "--enabled", String(command === "enable")], store, options);
2259
+ }
2254
2260
  if (command === "remove")
2255
2261
  return output(removeTaskRole(rest, store, options));
2256
2262
  if (command === "bind")
@@ -2751,6 +2757,7 @@ function listTaskRoles(args, store, options) {
2751
2757
  const statuses = inspectTaskRoleRuntimeStatuses(task.id, roles, store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? [], options.now?.() ?? new Date());
2752
2758
  const data = recordPage(statuses.map(status => ({
2753
2759
  roleName: status.roleName, agentId: status.agentId, recordedHealth: status.health,
2760
+ enabled: isEnabled(status.role),
2754
2761
  hostObservation: "not-requested",
2755
2762
  openInput: taskRoleOpenInputLabel(status), activeWork: taskRoleActiveWorkLabel(status),
2756
2763
  lastRun: taskRoleLastRunLabel(status), nativeSession: taskRoleNativeSessionLabel(status),
@@ -2810,6 +2817,9 @@ function updateTaskRole(args, store, options) {
2810
2817
  taskActor(tx, options, task.id);
2811
2818
  const role = requireRole(tx, task.id, roleName);
2812
2819
  const changesEnvironment = parsed.has("--environment") || parsed.has("--managed-environment");
2820
+ if (parsed.seen.size === 1 && parsed.has("--enabled")
2821
+ && isEnabled(role) === roleProfilePatch(parsed).enabled)
2822
+ return role;
2813
2823
  const changesLaunchContext = hasRoleLaunchContextOptions(parsed) || parsed.has("--profile")
2814
2824
  || changesEnvironment;
2815
2825
  const changesAgentConfig = hasAgentConfigOptions(parsed);
@@ -3562,6 +3572,7 @@ function dispatchWork(args, store, options) {
3562
3572
  }
3563
3573
  const roles = lanePlan.roles.map((name) => requireRole(tx, task.id, name));
3564
3574
  for (const role of roles) {
3575
+ assertRoleEnabled(role);
3565
3576
  if (tx.getActiveRun(task.id, role.name) !== null) {
3566
3577
  throw usageError(`${task.id}/${role.name} already has an active run.`);
3567
3578
  }
@@ -3570,6 +3581,7 @@ function dispatchWork(args, store, options) {
3570
3581
  let workItemForDispatch = prepareWorkItemDispatch(item, now);
3571
3582
  if (lanePlan.roles.length === 0) {
3572
3583
  const role = requireRole(tx, task.id, assignee);
3584
+ assertRoleEnabled(role);
3573
3585
  if (tx.getActiveRun(task.id, role.name) !== null) {
3574
3586
  throw usageError(`${task.id}/${role.name} already has an active run.`);
3575
3587
  }
@@ -4383,6 +4395,9 @@ function assertTaskReviewRequestLane(store, taskId, reviewerRoleName, reusableRo
4383
4395
  assertReviewerAvailable(store, taskId, reviewerRoleName, reusableRound);
4384
4396
  }
4385
4397
  function assertReviewerAvailable(store, taskId, reviewerRoleName, reusableRound) {
4398
+ const role = store.getRole(taskId, reviewerRoleName);
4399
+ if (role !== null)
4400
+ assertRoleEnabled(role);
4386
4401
  const availability = projectReviewerAvailability(store, taskId, reviewerRoleName);
4387
4402
  if (availability.kind === "available")
4388
4403
  return;
@@ -5001,6 +5016,7 @@ function retryRunOperation(args, store, options) {
5001
5016
  throw usageError(`Task does not admit ${previous.purpose} retry: ${task.id}/${task.status}.`);
5002
5017
  }
5003
5018
  const role = requireRole(tx, task.id, previous.roleName);
5019
+ assertRoleEnabled(role);
5004
5020
  if (tx.getActiveRun(task.id, role.name) !== null) {
5005
5021
  throw usageError(`${task.id}/${role.name} already has an active run.`);
5006
5022
  }
@@ -1,5 +1,6 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { runExecutionObservation } from "../agentRun/agentRun.js";
3
+ import { isEnabled, roleDisabledReason } from "../role/availability.js";
3
4
  import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
4
5
  import { currentProviderConversation, managedProviderTurnId } from "../runtime/providerRuntimeIdentity.js";
5
6
  import { isRoleRunStalled, latestStallProgressAt } from "../scheduler/roleRunStall.js";
@@ -118,6 +119,8 @@ export function renderTaskRoleRuntimeStatus(status) {
118
119
  `Task Role status: ${status.taskId}/${status.roleName}`,
119
120
  "",
120
121
  ` Health ${status.health}`,
122
+ ` Enabled ${isEnabled(status.role)}`,
123
+ ...(!isEnabled(status.role) ? [` Availability ${roleDisabledReason(status.role)}`] : []),
121
124
  ` Reason ${status.healthReason}`,
122
125
  ` Open inputs ${status.openInputRequestCount}`,
123
126
  ` Agent ${status.agentId}`,
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { assertRoleEnabled } from "../role/availability.js";
2
3
  import { mkdirSync } from "node:fs";
3
4
  import { isDeepStrictEqual } from "node:util";
4
5
  import { join } from "node:path";
@@ -63,7 +64,8 @@ function requireTaskTarget(store, target, runnable = true) {
63
64
  const task = store.getTask(target.taskId);
64
65
  if (!task || runnable && (task.status !== "active" || task.executionGate.state !== "enabled"))
65
66
  throw new Error("Task Agent requires an active, execution-enabled Task.");
66
- if (!store.getRole(target.taskId, target.roleName))
67
+ const role = store.getRole(target.taskId, target.roleName);
68
+ if (!role)
67
69
  throw new Error("Task Agent Role is unavailable.");
68
70
  const recipient = taskRecipient(store, target);
69
71
  if (recipient.ownerRunId) {
@@ -117,6 +119,7 @@ function ownedRole(store, home, job, runId, now) {
117
119
  const jobOwner = { scope: job.scope, jobId: job.id, ...(isolated ? { runId } : {}) };
118
120
  const existing = store.getGlobalRole(name);
119
121
  if (existing) {
122
+ assertRoleEnabled(existing);
120
123
  if (!isDeepStrictEqual(existing.jobOwner, jobOwner) || existing.activeAgentId !== job.configuration.target.agentId) {
121
124
  throw new Error("Job Role binding changed; inspect its Session/configuration before dispatch.");
122
125
  }
@@ -155,6 +158,9 @@ function queueAttempt(store, home, job, runId, index, now) {
155
158
  const inputControl = { action: "queue", requestId: `job-agent:${runId}:${index}` };
156
159
  let messageId;
157
160
  if (recipient.scope === "global") {
161
+ const role = store.getGlobalRole(recipient.roleName);
162
+ if (role !== null)
163
+ assertRoleEnabled(role);
158
164
  messageId = store.nextGlobalRoleMessageId();
159
165
  let message = createGlobalRoleMessage(messageId, recipient.roleName, body, "system", { type: "system" }, now, { inputControl });
160
166
  const set = store.getGlobalRoleSessionSet(recipient.roleName);
@@ -165,6 +171,7 @@ function queueAttempt(store, home, job, runId, index, now) {
165
171
  }
166
172
  else {
167
173
  requireTaskTarget(store, { ...recipient });
174
+ assertRoleEnabled(store.getRole(recipient.taskId, recipient.roleName));
168
175
  const resolved = taskRecipient(store, recipient);
169
176
  messageId = store.nextMessageId(recipient.taskId);
170
177
  store.saveMessage(recipient.taskId, createTaskMessage(messageId, recipient.taskId, body, "system", { type: "system" }, now, { ...(resolved.ownerRunId ? { recipient: resolved } : {}), inputControl }));
@@ -256,6 +263,7 @@ export function controlAgentJob(store, home, job, input, now) {
256
263
  if (op === "session-new") {
257
264
  if (!role)
258
265
  throw new Error("Create the Job Role first.");
266
+ assertRoleEnabled(role);
259
267
  assertRoleRuntimeMutationAllowed(store, { scope: "global", roleName }, "Job Session replacement");
260
268
  const set = store.getGlobalRoleSessionSet(roleName);
261
269
  const old = set?.sessions[set.activeAgentId];
@@ -3145,9 +3145,9 @@ function recordTaskRuntimeNativeSession(store, input, now) {
3145
3145
  if (task.status === "archived") {
3146
3146
  throw new Error(`Cannot register a native session for archived Task: ${input.taskId}.`);
3147
3147
  }
3148
- // A Draft registers a Session only for its planning Turn. Admission is decided
3149
- // by the active Turn's purpose through the one shared invariant, so a Draft
3150
- // still cannot open an execution Session before activation.
3148
+ // A Draft admits its first planning Run, restoration of that exact Session,
3149
+ // or the selected successor's initialization after verified replacement.
3150
+ // Ordinary Draft notifications and native hooks cannot create a Session.
3151
3151
  const admittingRun = store.getActiveRun(input.taskId, input.roleName);
3152
3152
  const existingPlanning = activeLiveRoleAgentSession(store.getTaskRoleSessionSet(input.taskId, input.roleName));
3153
3153
  if (!(admittingRun !== null
@@ -3156,6 +3156,9 @@ function recordTaskRuntimeNativeSession(store, input, now) {
3156
3156
  && !(task.status === "draft" && task.executionGate.state === "enabled" && input.roleName === "leader"
3157
3157
  && existingPlanning?.effective.executionAuthority === "planning"
3158
3158
  && existingPlanning.nativeSessionId === input.nativeSessionId)
3159
+ && !(task.status === "draft" && task.executionGate.state === "enabled" && input.roleName === "leader"
3160
+ && input.effective?.executionAuthority === "planning"
3161
+ && draftReplacementInitializationIsCurrent(store, input.taskId, input.agentId))
3159
3162
  && (task.status !== "active" || task.executionGate.state !== "enabled")) {
3160
3163
  throw new Error(`Cannot register a native session for a Task that is not active: ${input.taskId}.`);
3161
3164
  }
@@ -3191,6 +3194,32 @@ function recordTaskRuntimeNativeSession(store, input, now) {
3191
3194
  store.saveRoleSessionSet(updated);
3192
3195
  return updated.sessions[input.agentId];
3193
3196
  }
3197
+ /** The existing initialization wake is the launch authority, not historical
3198
+ * planning success or an arbitrary "session-replaced" reason. It is retained
3199
+ * on startup rejection for the normal explicit wake retry/replacement path. */
3200
+ function draftReplacementInitializationIsCurrent(store, taskId, agentId) {
3201
+ const sessions = store.getTaskRoleSessionSet(taskId, "leader");
3202
+ if (sessions === null || sessions.activeAgentId !== agentId
3203
+ || sessions.sessions[agentId] !== undefined || sessions.providerBinding !== null
3204
+ || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({
3205
+ scope: "task", taskId, roleName: "leader"
3206
+ }))))
3207
+ return false;
3208
+ const processing = store.getWorkMailbox({ kind: "role", taskId, roleName: "leader" })?.processing;
3209
+ if (!processing?.owner.startsWith("leader-notification:"))
3210
+ return false;
3211
+ const wake = store.getTaskWake(taskId, processing.owner.slice("leader-notification:".length));
3212
+ if (wake === null || !wake.reasons.includes("session-replaced"))
3213
+ return false;
3214
+ const events = store.listEventsByType(taskId, ["runtime.session-replaced", "notification.delivery"]);
3215
+ const replacement = events.filter(event => event.type === "runtime.session-replaced"
3216
+ && event.payload.roleName === "leader").at(-1);
3217
+ return replacement !== undefined
3218
+ && (wake.refs ?? []).some(ref => ref.type === "event" && ref.taskId === taskId && ref.id === replacement.id)
3219
+ && !events.some(event => event.type === "notification.delivery"
3220
+ && event.payload.attemptId === processing.batchId
3221
+ && event.payload.outcome !== "deferred");
3222
+ }
3194
3223
  function recordGlobalRuntimeNativeSession(store, input, now) {
3195
3224
  const role = store.getGlobalRole(input.roleName);
3196
3225
  if (role === null)
@@ -3271,6 +3300,7 @@ function mapRole(store, role) {
3271
3300
  return {
3272
3301
  taskId: role.taskId,
3273
3302
  name: role.name,
3303
+ enabled: role.enabled !== false,
3274
3304
  activeAgentId: role.activeAgentId,
3275
3305
  adapterId: binding.adapterId,
3276
3306
  ...(binding.config.model === undefined ? {} : { model: binding.config.model }),
@@ -3307,8 +3337,9 @@ function taskSessionEffective(store, taskId, roleName, agentId, existing) {
3307
3337
  ?? store.getTaskWorkspace(taskId)
3308
3338
  ?? undefined;
3309
3339
  const effective = resolveEffectiveLaunch({
3340
+ store,
3310
3341
  role,
3311
- purpose: "execution",
3342
+ purpose: store.getTask(taskId)?.status === "draft" && roleName === "leader" ? "planning" : "execution",
3312
3343
  ...(workspace === undefined ? {} : { workspace }),
3313
3344
  ...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
3314
3345
  });