@zq-silk/yui 0.7.1 → 0.8.2

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 (73) hide show
  1. package/ARCHITECTURE.md +27 -28
  2. package/README.md +79 -71
  3. package/dist/cli/commandCatalog.js +283 -136
  4. package/dist/cli/completion.js +3 -3
  5. package/dist/cli/helpRenderer.js +3 -0
  6. package/dist/cli/interactionPolicy.js +48 -33
  7. package/dist/cli/interactiveSelection.js +1 -1
  8. package/dist/cli/invocationRouter.js +3 -2
  9. package/dist/cli/roleWizard.js +8 -8
  10. package/dist/cli.js +189 -93
  11. package/dist/commands/agentCommands.js +5 -5
  12. package/dist/commands/configCommands.js +351 -104
  13. package/dist/commands/configOverview.js +60 -0
  14. package/dist/commands/deliveryGuardPreflight.js +2 -2
  15. package/dist/commands/globalRoleCommands.js +9 -9
  16. package/dist/commands/profileCommands.js +8 -8
  17. package/dist/commands/resourcesCommands.js +6 -5
  18. package/dist/commands/taskCommands.js +111 -59
  19. package/dist/commands/taskRoleRuntimeStatus.js +3 -1
  20. package/dist/commands/telemetryCommands.js +11 -6
  21. package/dist/config/configCatalog.js +42 -0
  22. package/dist/config/yuiConfig.js +80 -35
  23. package/dist/context/sessionBootstrapManifest.js +1 -1
  24. package/dist/controller/clientRuntime.js +0 -2
  25. package/dist/controller/controller.js +21 -9
  26. package/dist/controller/fileSchedulerStoreAdapter.js +409 -79
  27. package/dist/controller/resourceInventory.js +9 -5
  28. package/dist/controller/runtime.js +112 -25
  29. package/dist/controller/runtimeLaunchCoordinator.js +18 -78
  30. package/dist/controller/structuredProviderObservation.js +273 -0
  31. package/dist/doctor/doctor.js +2 -2
  32. package/dist/executor/agentAdapter.js +40 -0
  33. package/dist/executor/agentExecutor.js +31 -7
  34. package/dist/executor/executorRegistry.js +11 -49
  35. package/dist/executor/fileRoleLaunchPlanner.js +115 -37
  36. package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
  37. package/dist/resources/autoResourceGc.js +3 -1
  38. package/dist/review/reviewConfig.js +0 -2
  39. package/dist/run/agentRun.js +2 -2
  40. package/dist/run/providerRetry.js +29 -16
  41. package/dist/run/providerRetryConfig.js +5 -3
  42. package/dist/runtime/agentHost.js +767 -158
  43. package/dist/runtime/builtinAgentDrivers.js +1 -5
  44. package/dist/runtime/codexAppServerRuntime.js +67 -60
  45. package/dist/runtime/exactControlPlane.js +7 -2
  46. package/dist/runtime/index.js +6 -2
  47. package/dist/runtime/launchBroker.js +30 -8
  48. package/dist/runtime/launchDiagnostics.js +1 -1
  49. package/dist/runtime/providerAuthorityFence.js +24 -0
  50. package/dist/runtime/providerControl.js +63 -0
  51. package/dist/runtime/providerRecoveryDecision.js +55 -0
  52. package/dist/runtime/providerRuntimeIdentity.js +269 -19
  53. package/dist/runtime/runtimeBinding.js +20 -11
  54. package/dist/runtime/structuredProviderHost.js +476 -0
  55. package/dist/runtime/tmuxAdapters.js +143 -42
  56. package/dist/scheduler/activeRoleRunDelivery.js +206 -120
  57. package/dist/scheduler/leaderWakeupProcessor.js +141 -16
  58. package/dist/scheduler/roleRunStall.js +12 -9
  59. package/dist/setup/setupCommand.js +153 -492
  60. package/dist/storage/compatibleTaskStore.js +9 -5
  61. package/dist/storage/migration/productionRegistry.js +169 -0
  62. package/dist/storage/taskStore.js +22 -3
  63. package/dist/telemetry/sqliteTelemetryStore.js +9 -1
  64. package/dist/telemetry/telemetryConfig.js +1 -18
  65. package/dist/telemetry/telemetryStore.js +2 -2
  66. package/dist/telemetry/telemetryWiring.js +6 -5
  67. package/dist/tmux/tmuxManager.js +1 -1
  68. package/dist/web/webSnapshot.js +5 -3
  69. package/i18n/README.zh-CN.md +48 -40
  70. package/package.json +1 -1
  71. package/skills/yui-leader/SKILL.md +12 -5
  72. package/skills/yui-operator/SKILL.md +44 -6
  73. package/skills/yui-runtime/SKILL.md +1 -1
@@ -0,0 +1,60 @@
1
+ import { inspectCompletionStates, renderCompletionStateTable } from "../completion/completionState.js";
2
+ import { usageError } from "../errors/cliError.js";
3
+ import { SYSTEM_ROLE_NAMES } from "../role/systemRoles.js";
4
+ import { runAgentCommand } from "./agentCommands.js";
5
+ import { runConfigCommand } from "./configCommands.js";
6
+ import { runGlobalRoleCommand } from "./globalRoleCommands.js";
7
+ import { runProfileCommand } from "./profileCommands.js";
8
+ import { CONFIG_DOMAINS } from "../config/configCatalog.js";
9
+ /**
10
+ * One complete projection of every persistent configuration domain. Human and
11
+ * JSON callers intentionally consume the same store projection; the Operator can
12
+ * therefore explain what the user sees without reconstructing configuration
13
+ * from help text or implementation defaults.
14
+ */
15
+ export function runConfigOverview(args, store, environment, identity, roleOptions) {
16
+ if (args.length !== 0)
17
+ throw usageError("Config show usage: yui config show.");
18
+ const domains = Object.fromEntries(CONFIG_DOMAINS.map((domain) => [
19
+ domain,
20
+ runConfigCommand(domain, ["show"], store)
21
+ ]));
22
+ const agents = store.listConfiguredAgents();
23
+ const roles = store.listGlobalRoles();
24
+ const systemRoles = Object.fromEntries(SYSTEM_ROLE_NAMES.map((name) => [
25
+ name,
26
+ store.getGlobalRole(name)
27
+ ]));
28
+ const profiles = store.listAgentProfiles();
29
+ const completion = inspectCompletionStates(store.getConfig(), environment, identity);
30
+ const roleOutput = runGlobalRoleCommand(["list"], store, roleOptions);
31
+ if (typeof roleOutput !== "string") {
32
+ throw new Error("Config overview received an invalid Role control result.");
33
+ }
34
+ const profileOutput = runProfileCommand(["list"], store).output;
35
+ const agentOutput = runAgentCommand(["list"], store);
36
+ const domainData = Object.fromEntries(CONFIG_DOMAINS.map((domain) => [
37
+ domain,
38
+ domains[domain].data
39
+ ]));
40
+ return {
41
+ output: `${[
42
+ "Yui configuration",
43
+ ...CONFIG_DOMAINS.map((domain) => domains[domain].output.trimEnd()),
44
+ agentOutput.trimEnd(),
45
+ roleOutput.trimEnd(),
46
+ profileOutput.trimEnd(),
47
+ renderCompletionStateTable(completion).trimEnd()
48
+ ].join("\n\n")}\n`,
49
+ data: {
50
+ ...domainData,
51
+ agents,
52
+ roles: {
53
+ system: systemRoles,
54
+ custom: roles.filter(({ name }) => !SYSTEM_ROLE_NAMES.includes(name))
55
+ },
56
+ profiles,
57
+ completion
58
+ }
59
+ };
60
+ }
@@ -1,4 +1,4 @@
1
- import { resolveLeaderNextActionMode } from "../config/yuiConfig.js";
1
+ import { resolveLeaderNextActionMode, resolveLeaderSemanticBudgetTurns } from "../config/yuiConfig.js";
2
2
  import { usageError } from "../errors/cliError.js";
3
3
  import { detectDeliveryDuplicates, evaluateDeliveryGuard, evaluateSemanticBudget, formatDeliveryDuplicate } from "../task/deliveryGuard.js";
4
4
  export function runDeliveryGuardPreflight(store, taskId, intent, options = {}) {
@@ -15,7 +15,7 @@ export function runDeliveryGuardPreflight(store, taskId, intent, options = {}) {
15
15
  + "Reuse the existing proof, or set leader.nextActionMode=warn to proceed with a warning.");
16
16
  }
17
17
  if (options.budget === true) {
18
- const budget = evaluateSemanticBudget(facts);
18
+ const budget = evaluateSemanticBudget(facts, resolveLeaderSemanticBudgetTurns(store.getConfig().leaderSemanticBudgetTurns));
19
19
  if (budget.exhausted) {
20
20
  warnings.push(`Semantic progress budget: ${budget.reason}`);
21
21
  }
@@ -24,13 +24,13 @@ export function runGlobalRoleCommand(args, store, options = {}) {
24
24
  default:
25
25
  throw usageError(command === undefined
26
26
  ? "Role command is required."
27
- : `Unknown command: role ${command}`);
27
+ : `Unknown command: config role ${command}`);
28
28
  }
29
29
  }
30
30
  function roleContext(args, store, options) {
31
31
  const [rawName, ...rest] = args;
32
32
  const name = roleName(rawName);
33
- assertNoArguments(rest, "Role context usage: yui role context <role>");
33
+ assertNoArguments(rest, "Session context usage: yui session context <role>");
34
34
  const environment = options.env ?? process.env;
35
35
  if (environment.YUI_SESSION_SCOPE !== undefined) {
36
36
  if (environment.YUI_SESSION_SCOPE !== "global" || environment.YUI_ROLE !== name) {
@@ -125,7 +125,7 @@ function addRole(args, store, options) {
125
125
  return presentRole(`Added role ${name}`, created, store);
126
126
  }
127
127
  function listRoles(args, store) {
128
- assertNoArguments(args, "Role list usage: yui role list");
128
+ assertNoArguments(args, "Role list usage: yui config role list");
129
129
  const rows = new Map();
130
130
  for (const name of SYSTEM_ROLE_NAMES) {
131
131
  const role = store.getGlobalRole(name);
@@ -152,7 +152,7 @@ function listRoles(args, store) {
152
152
  function showRole(args, store) {
153
153
  const [rawName, ...rest] = args;
154
154
  const name = roleName(rawName);
155
- assertNoArguments(rest, "Role show usage: yui role show <role>");
155
+ assertNoArguments(rest, "Role show usage: yui config role show <role>");
156
156
  const role = store.getGlobalRole(name);
157
157
  if (role === null) {
158
158
  if (isSystemRoleName(name))
@@ -218,7 +218,7 @@ function bindRole(args, store) {
218
218
  const [rawName, rawAgentId, ...rest] = args;
219
219
  const name = roleName(rawName);
220
220
  const agentId = required(rawAgentId, "Agent id");
221
- assertNoArguments(rest, "Role bind usage: yui role bind <role> <agent-id>");
221
+ assertNoArguments(rest, "Role bind usage: yui config role bind <role> <agent-id>");
222
222
  const now = new Date();
223
223
  const result = store.transaction((tx) => {
224
224
  const role = requireRole(name, tx);
@@ -269,7 +269,7 @@ function assertOperatorAdapterAvailable(role, agent) {
269
269
  function removeRole(args, store) {
270
270
  const [rawName, ...rest] = args;
271
271
  const name = roleName(rawName);
272
- assertNoArguments(rest, "Role remove usage: yui role remove <role>");
272
+ assertNoArguments(rest, "Role remove usage: yui config role remove <role>");
273
273
  if (isSystemRoleName(name))
274
274
  throw usageError(`System role cannot be removed: ${name}`);
275
275
  store.transaction((tx) => {
@@ -291,7 +291,7 @@ function unbindRole(args, store) {
291
291
  const [rawName, rawAgentId, ...rest] = args;
292
292
  const name = roleName(rawName);
293
293
  const agentId = required(rawAgentId, "Agent id");
294
- assertNoArguments(rest, "Role unbind usage: yui role unbind <role> <agent-id>");
294
+ assertNoArguments(rest, "Role unbind usage: yui config role unbind <role> <agent-id>");
295
295
  store.transaction((tx) => {
296
296
  const role = requireRole(name, tx);
297
297
  try {
@@ -307,14 +307,14 @@ function unbindRole(args, store) {
307
307
  function enterRole(args, store) {
308
308
  const [rawName, ...rest] = args;
309
309
  const name = roleName(rawName);
310
- assertNoArguments(rest, "Role enter usage: yui role enter <role>");
310
+ assertNoArguments(rest, "Role enter usage: yui session enter <role>");
311
311
  const role = requireRole(name, store);
312
312
  return { kind: "enter", role };
313
313
  }
314
314
  function roleSession(args, store, options) {
315
315
  const [command, rawName, ...tail] = args;
316
316
  if (command !== "record" && command !== "replace") {
317
- throw usageError("Role session usage: yui role session record|replace <role> --native-id <id> [--reason <reason>].");
317
+ throw usageError("Session usage: yui session record|replace <role> --native-id <id> [--reason <reason>].");
318
318
  }
319
319
  const name = roleName(rawName);
320
320
  const parsed = parseOptions(tail, new Map([
@@ -14,11 +14,11 @@ export function runProfileCommand(args, store, now = () => new Date()) {
14
14
  default:
15
15
  throw usageError(command === undefined
16
16
  ? "Profile command is required."
17
- : `Unknown command: profile ${command}`);
17
+ : `Unknown command: config profile ${command}`);
18
18
  }
19
19
  }
20
20
  function addProfile(args, store, now) {
21
- const usage = "Profile add usage: yui profile add <id> [--access <read|write>] [Profile settings].";
21
+ const usage = "Profile add usage: yui config profile add <id> [--access <read|write>] [Profile settings].";
22
22
  const [id, ...tail] = args;
23
23
  if (id === undefined || id.startsWith("--"))
24
24
  throw usageError("Profile id is required.", usage);
@@ -37,7 +37,7 @@ function addProfile(args, store, now) {
37
37
  return { output: `Added Agent Profile ${profile.id}\n`, data: { profile } };
38
38
  }
39
39
  function listProfiles(args, store) {
40
- noArgs(args, "Profile list usage: yui profile list.");
40
+ noArgs(args, "Profile list usage: yui config profile list.");
41
41
  const profiles = store.listAgentProfiles();
42
42
  const output = profiles.length === 0
43
43
  ? "No Agent Profiles found.\n"
@@ -55,7 +55,7 @@ function listProfiles(args, store) {
55
55
  return { output, data: { profiles } };
56
56
  }
57
57
  function showProfile(args, store) {
58
- const profile = requireProfile(store, oneArg(args, "Profile show usage: yui profile show <id>."));
58
+ const profile = requireProfile(store, oneArg(args, "Profile show usage: yui config profile show <id>."));
59
59
  return {
60
60
  output: `${[
61
61
  `Agent Profile: ${profile.id}`,
@@ -71,7 +71,7 @@ function showProfile(args, store) {
71
71
  };
72
72
  }
73
73
  function updateProfile(args, store, now) {
74
- const usage = "Profile update usage: yui profile update <id> [--access <read|write>] [Profile settings].";
74
+ const usage = "Profile update usage: yui config profile update <id> [--access <read|write>] [Profile settings].";
75
75
  const [id, ...tail] = args;
76
76
  if (id === undefined || id.startsWith("--"))
77
77
  throw usageError("Profile id is required.", usage);
@@ -91,16 +91,16 @@ function updateProfile(args, store, now) {
91
91
  };
92
92
  }
93
93
  function removeProfile(args, store) {
94
- const id = oneArg(args, "Profile remove usage: yui profile remove <id>.");
94
+ const id = oneArg(args, "Profile remove usage: yui config profile remove <id>.");
95
95
  if (BUILTIN_PROFILE_IDS.includes(id)) {
96
- throw usageError(`Built-in Agent Profile cannot be removed: ${id}. Use profile reset instead.`);
96
+ throw usageError(`Built-in Agent Profile cannot be removed: ${id}. Use yui config profile reset instead.`);
97
97
  }
98
98
  if (!store.removeAgentProfile(id))
99
99
  throw usageError(`Agent Profile not found: ${id}.`);
100
100
  return { output: `Removed Agent Profile ${id}\n`, data: { profileId: id } };
101
101
  }
102
102
  function resetProfiles(args, store, now) {
103
- noArgs(args, "Profile reset usage: yui profile reset.");
103
+ noArgs(args, "Profile reset usage: yui config profile reset.");
104
104
  store.transaction((tx) => {
105
105
  for (const desired of builtinAgentProfileInputs()) {
106
106
  const existing = tx.getAgentProfile(desired.id);
@@ -12,7 +12,7 @@ import { defaultTableWidth, renderTable } from "../output/table.js";
12
12
  import { applyResourceGc, planResourceGc, purgeResourceQuarantine, restoreAllResourceGc } from "../resources/resourceGc.js";
13
13
  import { createResourceRegistryStore } from "../resources/resourceRegistryStore.js";
14
14
  import { resourceKindLabel, resourceOwnerLabel } from "../resources/resourceDiscovery.js";
15
- import { resolveResourcesGcMode } from "../config/yuiConfig.js";
15
+ import { resolveResourcesGcMode, resolveResourcesQuarantineTtlHours } from "../config/yuiConfig.js";
16
16
  export async function runResourcesCommand(args, store, options = {}) {
17
17
  const [command, ...rest] = args;
18
18
  if (command !== "gc") {
@@ -23,7 +23,7 @@ export async function runResourcesCommand(args, store, options = {}) {
23
23
  }
24
24
  async function runGcCommand(args, store, options) {
25
25
  const action = parseGcAction(args);
26
- const ttlHours = parseTtlHours(args);
26
+ const ttlHours = parseTtlHours(args, store);
27
27
  const now = options.now?.() ?? new Date();
28
28
  const home = resolve(store.rootDirectory());
29
29
  const mode = resolveGcMode(store);
@@ -95,10 +95,11 @@ function parseGcAction(args) {
95
95
  return "restore";
96
96
  return "dry-run";
97
97
  }
98
- function parseTtlHours(args) {
98
+ function parseTtlHours(args, store) {
99
99
  const index = args.indexOf("--quarantine-ttl-hours");
100
- if (index === -1)
101
- return 24;
100
+ if (index === -1) {
101
+ return resolveResourcesQuarantineTtlHours(store.getConfig().resourcesQuarantineTtlHours);
102
+ }
102
103
  const value = Number(args[index + 1]);
103
104
  if (!Number.isFinite(value) || value < 1 || value > 24 * 30) {
104
105
  throw usageError("Quarantine TTL hours must be between 1 and 720.");
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
3
  import { createRunAssignment } from "../context/runContextContract.js";
4
4
  import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds, expandRunContextRef, freezeRunContextSnapshot } from "../context/runContextPack.js";
@@ -7,7 +7,8 @@ import { CliError, dataError, roleNotFound, runtimeError, taskNotFound, usageErr
7
7
  import { createTaskEvent } from "../event/taskEvent.js";
8
8
  import { clearMatchingLeaderStallAttention, isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
9
9
  import { readCommandText } from "./textInput.js";
10
- import { createRoleSessionSet, roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
10
+ import { createRoleSessionSet, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
11
+ import { currentProviderActivation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
11
12
  import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
12
13
  import { defaultTableWidth, renderTable } from "../output/table.js";
13
14
  import { formatTimestamp } from "../output/timePresentation.js";
@@ -16,11 +17,10 @@ import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js
16
17
  import { cancelInputRequest } from "../input/inputRequest.js";
17
18
  import { recoverExactAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
18
19
  import { resetTaskRoleSessionGeneration } from "../lifecycle/taskRoleSessionReset.js";
19
- import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
20
+ import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
20
21
  import { agentRunDeliveryReceiptId, createAgentRun, withAgentRunContextSnapshot } from "../run/agentRun.js";
21
22
  import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProjection.js";
22
23
  import { matchYieldReceipt } from "../run/yieldReceipt.js";
23
- import { providerRetryConfig } from "../run/providerRetryConfig.js";
24
24
  import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, deltaRecheckBlocksAcceptance, finishReviewRound, parseReviewYieldReport, recordReviewWorkspaceDisposition, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
25
25
  import { buildDeltaRecheckDispatchContext, verifyDeltaRecheckDiff } from "../review/deltaRecheck.js";
26
26
  import { buildTaskFinalReviewFindingContext, dispositionReviewFinding, planRepairGroups, reconcileReviewFindings, reconcileReviewFindingsAfterReview } from "../review/reviewFindingLedger.js";
@@ -241,7 +241,6 @@ export function runTaskCommand(args, store, options = {}) {
241
241
  case "milestone": return taskMilestoneCommand(rest, store, options);
242
242
  case "event": return taskEventCommand(rest, store);
243
243
  case "continuation": return taskContinuationCommand(rest, store);
244
- case "enter": return enterTaskRoleAlias(rest, store, options);
245
244
  default:
246
245
  throw usageError(command === undefined
247
246
  ? "Task command is required."
@@ -430,8 +429,8 @@ function updateTaskCommand(args, store, options) {
430
429
  /** Compatibility helper for call sites that cannot yet handle foreground enter. */
431
430
  export function runTaskOutputCommand(args, store, options = {}) {
432
431
  const execution = runTaskCommand(args, store, options);
433
- if (execution.kind === "enter") {
434
- throw runtimeError("Task role enter requires foreground tmux handoff by the CLI.");
432
+ if (execution.kind !== "output") {
433
+ throw runtimeError("Task Role foreground runtime control requires the CLI.");
435
434
  }
436
435
  return execution.output;
437
436
  }
@@ -1193,8 +1192,12 @@ function taskRoleCommand(args, store, options) {
1193
1192
  return output(unbindTaskRole(rest, store, options));
1194
1193
  if (command === "reset")
1195
1194
  return resetTaskRole(rest, store, options);
1196
- if (command === "enter")
1197
- return enterTaskRole(rest, store, options);
1195
+ if (command === "view")
1196
+ return viewTaskRole(rest, store);
1197
+ if (command === "takeover")
1198
+ return transferTaskRoleAuthority(rest, store, options, "takeover");
1199
+ if (command === "release")
1200
+ return transferTaskRoleAuthority(rest, store, options, "release");
1198
1201
  throw usageError(command === undefined
1199
1202
  ? "Task role command is required."
1200
1203
  : `Unknown command: task role ${command}`);
@@ -1507,60 +1510,111 @@ function unbindTaskRole(args, store, options) {
1507
1510
  });
1508
1511
  return `Unbound Agent ${args[2]} from ${result.taskId}/${result.name}\n`;
1509
1512
  }
1510
- function enterTaskRole(args, store, _options) {
1511
- const usage = "Task role enter usage: yui task role enter <task> <role> "
1512
- + "[--read-only | --read-write].";
1513
- const parsed = parseTail(args, new Set(), usage, new Set(["--read-only", "--read-write"]));
1514
- exactPositionals(parsed.positionals, 2, usage);
1515
- if (parsed.options.has("--read-only") && parsed.options.has("--read-write")) {
1516
- throw usageError("--read-only and --read-write are mutually exclusive.", usage);
1517
- }
1518
- const task = requireTask(store, parsed.positionals[0]);
1513
+ function viewTaskRole(args, store) {
1514
+ const usage = "Task role view usage: yui task role view <task> <role>.";
1515
+ exactPositionals(args, 2, usage);
1516
+ const task = requireTask(store, args[0]);
1519
1517
  if (task.status !== "active") {
1520
- throw usageError(inactiveTaskMessage(task, "entering a role session"));
1518
+ throw usageError(inactiveTaskMessage(task, "viewing a role session"));
1521
1519
  }
1522
- const role = requireRole(store, task.id, parsed.positionals[1]);
1523
- const access = parsed.options.has("--read-write") ? "read-write" : "read-only";
1524
- if (access === "read-write") {
1525
- assertTaskRoleWritableAttachAvailable(store, task.id, role.name);
1520
+ const role = requireRole(store, task.id, args[1]);
1521
+ const session = store.getRoleSession(task.id, role.name);
1522
+ if (session === null || session.status === "stopped" || session.status === "broken") {
1523
+ throw usageError(`Task Role has no live Provider view: ${task.id}/${role.name}.`);
1526
1524
  }
1527
1525
  return {
1528
- kind: "enter",
1526
+ kind: "view",
1529
1527
  taskId: task.id,
1530
1528
  roleName: role.name,
1531
- access,
1532
- output: `Attaching to ${role.name} for ${task.id} (${access})\n`
1529
+ access: "read-only",
1530
+ output: `Viewing ${role.name} for ${task.id} (read-only)\n`
1533
1531
  };
1534
1532
  }
1535
- /** Re-run after the tmux writer lease exists to close attach/launch races. */
1536
- export function assertTaskRoleWritableAttachAvailable(store, taskId, roleName, options = {}) {
1537
- const role = requireRole(store, taskId, roleName);
1538
- if (store.getActiveAgentRun(taskId, roleName) !== null) {
1539
- throw usageError(`Role has an active managed Run; writable attach is unavailable: ${taskId}/${roleName}.`);
1540
- }
1541
- const session = store.getRoleSession(taskId, roleName);
1542
- if (activeRoleAgentBinding(role).adapterId === "claude"
1543
- && ((session !== null
1544
- && session.status !== "stopped"
1545
- && session.status !== "broken")
1546
- || options.isManagedProcessRunning?.() === true)) {
1547
- throw usageError(`A managed Claude process is still running; writable attach is unavailable: ${taskId}/${roleName}.`);
1548
- }
1549
- }
1550
- function enterTaskRoleAlias(args, store, options) {
1551
- const usage = "Task enter usage: yui task enter <task> [role] "
1552
- + "[--read-only | --read-write].";
1553
- const parsed = parseTail(args, new Set(), usage, new Set(["--read-only", "--read-write"]));
1554
- if (parsed.positionals.length < 1 || parsed.positionals.length > 2
1555
- || parsed.positionals.some((value) => value.trim().length === 0)) {
1556
- throw usageError(usage);
1533
+ function transferTaskRoleAuthority(args, store, options, action) {
1534
+ const usage = `Task role ${action} usage: yui task role ${action} <task> <role>.`;
1535
+ exactPositionals(args, 2, usage);
1536
+ const now = clock(options);
1537
+ try {
1538
+ return store.transaction((tx) => {
1539
+ const task = requireTask(tx, args[0]);
1540
+ if (task.status !== "active") {
1541
+ throw usageError(inactiveTaskMessage(task, `${action} Provider authority`));
1542
+ }
1543
+ const role = requireRole(tx, task.id, args[1]);
1544
+ const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
1545
+ const session = sessions?.sessions[role.activeAgentId];
1546
+ const binding = sessions?.providerBinding;
1547
+ if (sessions === null || sessions === undefined || session === undefined
1548
+ || binding === null || binding === undefined
1549
+ || session.launchId === undefined
1550
+ || session.status === "stopped" || session.status === "broken") {
1551
+ throw new Error(`Task Role has no live managed Provider: ${task.id}/${role.name}.`);
1552
+ }
1553
+ const activation = currentProviderActivation(binding);
1554
+ if (activation === null) {
1555
+ throw new Error(`Provider Activation is not live: ${task.id}/${role.name}.`);
1556
+ }
1557
+ if (action === "takeover") {
1558
+ const activeRun = tx.getActiveAgentRun(task.id, role.name);
1559
+ if (sessions.inFlight === null || activeRun?.id !== sessions.inFlight.runId) {
1560
+ throw new Error(`Task Role has no active managed Run for takeover: ${task.id}/${role.name}.`);
1561
+ }
1562
+ }
1563
+ if (action === "takeover"
1564
+ && binding.authority.owner !== "controller"
1565
+ && binding.authority.owner !== "human") {
1566
+ throw new Error(`Provider authority is not Controller-owned: ${task.id}/${role.name}.`);
1567
+ }
1568
+ if (action === "release"
1569
+ && binding.authority.owner !== "human"
1570
+ && binding.authority.owner !== "controller") {
1571
+ throw new Error(`Provider authority is not human-owned: ${task.id}/${role.name}.`);
1572
+ }
1573
+ const desiredOwner = action === "takeover" ? "human" : "controller";
1574
+ const unchanged = binding.authority.owner === desiredOwner;
1575
+ const updatedBinding = unchanged
1576
+ ? binding
1577
+ : transferProviderAuthority(binding, {
1578
+ expectedEpoch: binding.authority.epoch,
1579
+ expectedOwner: binding.authority.owner,
1580
+ owner: desiredOwner,
1581
+ holderId: action === "takeover" ? `human:${randomUUID()}` : activation.activationId,
1582
+ changedAt: now.toISOString()
1583
+ });
1584
+ const authority = updatedBinding.authority;
1585
+ if (authority.owner !== "controller" && authority.owner !== "human") {
1586
+ throw new Error("Provider authority transfer did not produce a writer.");
1587
+ }
1588
+ if (!unchanged) {
1589
+ tx.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updatedBinding, now));
1590
+ recordTaskEvent(tx, task.id, "runtime.provider-authority-transferred", {
1591
+ role: role.name,
1592
+ owner: authority.owner,
1593
+ holderId: authority.holderId,
1594
+ epoch: String(authority.epoch)
1595
+ }, now);
1596
+ }
1597
+ return {
1598
+ kind: "authority",
1599
+ action,
1600
+ taskId: task.id,
1601
+ roleName: role.name,
1602
+ launchId: session.launchId,
1603
+ nativeSessionId: session.nativeSessionId,
1604
+ authority: {
1605
+ epoch: authority.epoch,
1606
+ owner: authority.owner,
1607
+ holderId: authority.holderId
1608
+ },
1609
+ output: action === "takeover"
1610
+ ? `Human authority ${unchanged ? "replayed" : "acquired"} for ${task.id}/${role.name} at epoch ${authority.epoch}.\n`
1611
+ : `Controller authority ${unchanged ? "replayed" : "restored"} for ${task.id}/${role.name} at epoch ${authority.epoch}.\n`
1612
+ };
1613
+ });
1614
+ }
1615
+ catch (error) {
1616
+ throw usageError(messageOf(error), usage);
1557
1617
  }
1558
- return enterTaskRole([
1559
- parsed.positionals[0],
1560
- parsed.positionals[1] ?? LEADER_ROLE,
1561
- ...(parsed.options.has("--read-only") ? ["--read-only"] : []),
1562
- ...(parsed.options.has("--read-write") ? ["--read-write"] : [])
1563
- ], store, options);
1564
1618
  }
1565
1619
  function taskWorkCommand(args, store, options) {
1566
1620
  const [command, ...rest] = args;
@@ -2840,7 +2894,7 @@ function requestTaskReviewRound(args, store, options) {
2840
2894
  const reviewConfig = tx.getReviewConfig();
2841
2895
  if (reviewConfig === null || reviewConfig.deltaRecheck !== "enabled") {
2842
2896
  throw usageError("Delta-recheck is not enabled for this Project's review policy. "
2843
- + "Set `yui config set review --role <role> --trigger final --delta-recheck enabled` "
2897
+ + "Set `yui config workflow set review --role <role> --trigger final --delta-recheck enabled` "
2844
2898
  + "or request a full Review.");
2845
2899
  }
2846
2900
  if (taskFinalContract !== undefined) {
@@ -4434,9 +4488,7 @@ function buildYieldOutcome(run, inputSummary, options) {
4434
4488
  * for the same outcome, fails closed for a different outcome, or returns
4435
4489
  * `null` to keep the legacy "already terminal" behavior.
4436
4490
  */
4437
- function replayYieldReceipt(run, inputSummary, options, retryConfig) {
4438
- if (!retryConfig.yieldReceiptReplay)
4439
- return null;
4491
+ function replayYieldReceipt(run, inputSummary, options) {
4440
4492
  if (run.yieldReceipt === undefined)
4441
4493
  return null;
4442
4494
  const outcome = buildYieldOutcome(run, inputSummary, options);
@@ -4492,7 +4544,7 @@ function yieldRun(args, store, options) {
4492
4544
  // transaction; the receipt is immutable once committed.
4493
4545
  const existing = requireRun(store, parsed.positionals[0], options);
4494
4546
  if (existing.status !== "active") {
4495
- const replayed = replayYieldReceipt(existing, inputSummary, options, providerRetryConfig(store.getConfig()));
4547
+ const replayed = replayYieldReceipt(existing, inputSummary, options);
4496
4548
  if (replayed !== null)
4497
4549
  return replayed;
4498
4550
  throw usageError(`Run ${existing.id} is already terminal: ${existing.status}.`);
@@ -5,6 +5,7 @@ import { isRoleRunStalled, latestStallProgressAt } from "../scheduler/roleRunSta
5
5
  import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
6
6
  import { classifyRuntimeHealth, projectRuntimeMailbox, projectRuntimeObservation, projectRuntimeTaskEvents, runtimeDisplayStatus } from "../runtime/runtimeProjection.js";
7
7
  import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
8
+ import { resolveRuntimeHealth } from "../config/yuiConfig.js";
8
9
  import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
9
10
  export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
10
11
  const taskOpenInputRequestCount = store.listInputRequests(taskId)
@@ -397,7 +398,8 @@ function projectTaskRoleRuntime(run, session, tmux, events, mailbox, store, task
397
398
  const classification = classifyRuntimeHealth({
398
399
  projection,
399
400
  semanticProgressAt: semanticProgress.progressAt,
400
- now
401
+ now,
402
+ policy: resolveRuntimeHealth(store.getConfig().runtimeHealth)
401
403
  });
402
404
  return {
403
405
  driverId,
@@ -2,14 +2,19 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { usageError } from "../errors/cliError.js";
4
4
  import { STORAGE_SCHEMA_FILE } from "../storage/storageSchema.js";
5
+ import { CURRENT_CONFIG_SCHEMA_VERSION } from "../storage/taskStore.js";
5
6
  import { openTaskStore, SqliteTaskStore } from "../storage/sqliteStore.js";
6
7
  import { COMMITTED_DATABASE_FILENAME } from "../storage/upgrade/sqliteStateMigration.js";
7
- import { DEFAULT_RUN_CAP, DEFAULT_TERMINAL_KEEP, resolveRunCap, resolveTelemetryMode, resolveTerminalKeep } from "../telemetry/telemetryConfig.js";
8
+ import { DEFAULT_RUN_CAP, DEFAULT_TERMINAL_KEEP, resolveRunCap, resolveTerminalKeep } from "../telemetry/telemetryConfig.js";
9
+ import { resolveTelemetryEnabled } from "../config/yuiConfig.js";
8
10
  import { applyTelemetryCompaction, planTelemetryCompaction } from "../telemetry/telemetryCompaction.js";
9
11
  import { SqliteTelemetryStore } from "../telemetry/sqliteTelemetryStore.js";
10
12
  /** Read the durable config, falling back to defaults when no store is available. */
11
13
  function storeConfig(options) {
12
- return options.store?.getConfig() ?? { schemaVersion: 1 };
14
+ return options.store?.getConfig() ?? { schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION };
15
+ }
16
+ function telemetryMode(config) {
17
+ return resolveTelemetryEnabled(config.telemetryEnabled) ? "on" : "off";
13
18
  }
14
19
  export async function runTelemetryCommand(args, options) {
15
20
  const [command, ...rest] = args;
@@ -31,7 +36,7 @@ export async function runTelemetryCommand(args, options) {
31
36
  // -- status ---------------------------------------------------------------------
32
37
  function telemetryStatus(args, options) {
33
38
  const flags = parseFlags(args, new Set([]));
34
- const mode = resolveTelemetryMode(storeConfig(options).telemetryMode);
39
+ const mode = telemetryMode(storeConfig(options));
35
40
  const telemetry = new SqliteTelemetryStore(options.home, {
36
41
  mode,
37
42
  terminalKeep: resolveTerminalKeep(storeConfig(options).telemetryTerminalKeep),
@@ -90,7 +95,7 @@ function telemetryPrune(args, options) {
90
95
  const cap = resolveRunCap(storeConfig(options).telemetryRunCap);
91
96
  const store = requireStore(options);
92
97
  const telemetry = new SqliteTelemetryStore(options.home, {
93
- mode: resolveTelemetryMode(storeConfig(options).telemetryMode),
98
+ mode: telemetryMode(storeConfig(options)),
94
99
  terminalKeep: keep,
95
100
  runCap: cap
96
101
  });
@@ -196,7 +201,7 @@ function telemetryCompact(args, options) {
196
201
  copyStoreFiles(resolvedFrom, resolvedStaged);
197
202
  const store = openTaskStore(resolvedStaged, backend);
198
203
  const telemetry = new SqliteTelemetryStore(resolvedStaged, {
199
- mode: "bounded",
204
+ mode: "on",
200
205
  terminalKeep: keep,
201
206
  runCap: DEFAULT_RUN_CAP
202
207
  });
@@ -240,7 +245,7 @@ function telemetryRead(args, options) {
240
245
  const limit = integerOption(args, "--limit", 100);
241
246
  const offset = integerOption(args, "--offset", 0);
242
247
  const telemetry = new SqliteTelemetryStore(options.home, {
243
- mode: resolveTelemetryMode(storeConfig(options).telemetryMode),
248
+ mode: telemetryMode(storeConfig(options)),
244
249
  terminalKeep: resolveTerminalKeep(storeConfig(options).telemetryTerminalKeep),
245
250
  runCap: resolveRunCap(storeConfig(options).telemetryRunCap)
246
251
  });
@@ -0,0 +1,42 @@
1
+ import { supportedAgentAdapterIds } from "../agent/adapterCatalog.js";
2
+ import { MAX_RUN_CAP } from "../telemetry/telemetryConfig.js";
3
+ import { MAX_CONTEXT_BUDGET_TOKENS, MAX_PROVIDER_RETRY_ATTEMPTS, MIN_CONTEXT_BUDGET_TOKENS } from "./yuiConfig.js";
4
+ export const CONFIG_DOMAINS = ["system", "runtime", "workflow", "resources", "tools"];
5
+ /**
6
+ * Public durable configuration contract. CLI help, completion, config show,
7
+ * config describe, and Operator guidance all project from this one catalog.
8
+ * Runtime-only implementation constants deliberately do not appear here.
9
+ */
10
+ export const CONFIG_DEFINITIONS = Object.freeze([
11
+ { key: "default-agent", domain: "system", property: "defaultAgent", label: "Default Agent", summary: "Configured Agent used when a Task or Role does not select one explicitly.", takesEffect: "Future Role and Task defaults; existing bindings are unchanged." },
12
+ { key: "default-workspace", domain: "system", property: "defaultWorkspace", label: "Default workspace", summary: "Absolute workspace root outside YUI_HOME used when no workspace is selected.", takesEffect: "Future workspace selection; existing Roles and worktrees are unchanged." },
13
+ { key: "time-zone", domain: "system", property: "timeZone", label: "Time zone", summary: "IANA timezone for human-facing timestamps (default: Asia/Shanghai).", takesEffect: "The next human-readable timestamp rendering; durable JSON remains UTC." },
14
+ { key: "reconciliation-interval-seconds", domain: "runtime", property: "reconciliationIntervalSeconds", label: "Reconciliation interval", summary: "Recovery reconciliation interval, 5-300 seconds (default: 120).", takesEffect: "The running Controller is refreshed after the value is saved." },
15
+ { key: "controller-task-concurrency", domain: "runtime", property: "controllerTaskConcurrency", label: "Controller Task concurrency", summary: "Maximum Tasks reconciled concurrently, 1-32 (default: 4).", takesEffect: "After the Controller restarts." },
16
+ { key: "runtime-health", domain: "runtime", property: "runtimeHealth", label: "Runtime health thresholds", summary: "Quiet, diagnostic, and semantic-stall thresholds in seconds; values must be strictly increasing (defaults: 300/600/1800).", takesEffect: "CLI and Web projections immediately; scheduler stall handling after the Controller restarts." },
17
+ { key: "agent-launch-inactivity-timeout-seconds", domain: "runtime", property: "agentLaunchInactivityTimeoutSeconds", label: "Agent launch inactivity timeout", summary: "Maximum launch silence before startup fails, 15-3600 seconds (default: 300).", takesEffect: "After the Controller restarts." },
18
+ { key: "delivery-timeout-seconds", domain: "runtime", property: "deliveryTimeoutSeconds", label: "Delivery timeout", summary: "Total control-plane delivery retry budget, 5-600 seconds (default: 120).", takesEffect: "After the Controller restarts; internal retry cadence remains automatic." },
19
+ { key: "provider-retry-mode", domain: "runtime", property: "providerRetryMode", label: "Provider retry mode", summary: "Provider retry mode: off, shadow, or enforce (default: enforce).", takesEffect: "The next eligible provider-failure decision." },
20
+ { key: "provider-retry-adapters", domain: "runtime", property: "providerRetryAdapters", label: "Provider retry adapters", summary: `Adapters with in-place retry: all, off, or a comma-separated subset of ${supportedAgentAdapterIds().join(", ")} (default: all).`, takesEffect: "The next eligible provider-failure decision." },
21
+ { key: "provider-retry-delays-seconds", domain: "runtime", property: "providerRetryDelaysSeconds", label: "Provider retry delays", summary: `Ordered comma-separated retry delays of 1-600 seconds, with 1-${MAX_PROVIDER_RETRY_ATTEMPTS} attempts (default: 2,5,15).`, takesEffect: "The next eligible provider-failure decision; attempt count is the list length." },
22
+ { key: "provider-retry-max-window-seconds", domain: "runtime", property: "providerRetryMaxWindowSeconds", label: "Provider retry max window", summary: "Positive-integer total retry budget per Run lineage in seconds (default: 600).", takesEffect: "The next retry episode; an active episode keeps its existing deadline." },
23
+ { key: "leader-next-action", domain: "workflow", property: "leaderNextActionMode", label: "Leader next-action mode", summary: "Leader next-action mode: display, warn, or enforce (default: display).", takesEffect: "The next Leader next-action projection or gate." },
24
+ { key: "leader-semantic-budget-turns", domain: "workflow", property: "leaderSemanticBudgetTurns", label: "Leader semantic budget", summary: "Consecutive yielded Leader turns without durable delivery progress before warning, 1-20 (default: 3).", takesEffect: "The next Leader delivery guard evaluation." },
25
+ { key: "context-budget", domain: "workflow", property: "contextBudget", label: "Context budget", summary: `Per-Session context token budget; soft and hard are ${MIN_CONTEXT_BUDGET_TOKENS}-${MAX_CONTEXT_BUDGET_TOKENS}, with soft below hard (defaults: 100000/120000).`, takesEffect: "The next context-budget evaluation; retained conversation content is unchanged." },
26
+ { key: "review", domain: "workflow", property: "review", label: "Review", summary: "Optional global WorkItem review rule: a configured Role plus always, leader, or final trigger; optional finding-ledger and delta-recheck controls. Setup leaves it disabled so the Leader may review directly or delegate selectively.", takesEffect: "The next Candidate snapshot; in-flight Candidates and ReviewRounds keep their policy." },
27
+ { key: "resources-gc-mode", domain: "resources", property: "resourcesGcMode", label: "Resources GC mode", summary: "Resource GC mode: report or quarantine (default: report).", takesEffect: "The next resource GC operation." },
28
+ { key: "resources-gc-auto-quarantine", domain: "resources", property: "resourcesGcAutoQuarantine", label: "Resources GC auto-quarantine", summary: "Automatically quarantine eligible terminal Task resources (default: false).", takesEffect: "The next eligible automatic resource GC operation." },
29
+ { key: "resources-quarantine-ttl-hours", domain: "resources", property: "resourcesQuarantineTtlHours", label: "Resource quarantine TTL", summary: "Default observation window before purge, 1-720 hours (default: 24).", takesEffect: "The next resource GC command without an explicit TTL override." },
30
+ { key: "tmux-bin", domain: "tools", property: "tmuxBin", label: "Tmux bin", summary: "Command or path to the tmux binary (default: tmux).", takesEffect: "The next CLI-owned tmux invocation; restart the Controller for Controller-owned launches." },
31
+ { key: "tmux-history-limit", domain: "tools", property: "tmuxHistoryLimit", label: "Tmux history limit", summary: "History lines retained for newly created tmux sessions, 1000-1000000 (default: 100000).", takesEffect: "New tmux sessions; existing sessions report drift until recreated." },
32
+ { key: "telemetry-enabled", domain: "tools", property: "telemetryEnabled", label: "Diagnostic telemetry", summary: "Enable the optional SQLite diagnostic telemetry projection (default: false); requires a SQLite Home.", takesEffect: "Standalone telemetry commands immediately; scheduler writes after Controller restart." },
33
+ { key: "telemetry-terminal-keep", domain: "tools", property: "telemetryTerminalKeep", label: "Telemetry terminal keep", summary: "Positive-integer rows retained per terminal Run generation (default: 200).", takesEffect: "Explicit retention immediately; automatic retention after the Controller restarts." },
34
+ { key: "telemetry-run-cap", domain: "tools", property: "telemetryRunCap", label: "Telemetry Run cap", summary: `Positive-integer maximum rows retained per Run, at most ${MAX_RUN_CAP} (default: 50000).`, takesEffect: "Explicit retention immediately; automatic write-time capping after the Controller restarts." }
35
+ ]);
36
+ export const CONFIG_KEYS = Object.freeze(CONFIG_DEFINITIONS.map(({ key }) => key));
37
+ export function configDefinitionsForDomain(domain) {
38
+ return CONFIG_DEFINITIONS.filter((definition) => definition.domain === domain);
39
+ }
40
+ export function configDefinition(key) {
41
+ return CONFIG_DEFINITIONS.find((definition) => definition.key === key);
42
+ }