@zq-silk/yui 0.7.0 → 0.8.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.
Files changed (49) hide show
  1. package/README.md +42 -23
  2. package/dist/cli/commandCatalog.js +234 -122
  3. package/dist/cli/completion.js +3 -3
  4. package/dist/cli/helpRenderer.js +3 -0
  5. package/dist/cli/interactionPolicy.js +44 -23
  6. package/dist/cli/interactiveSelection.js +1 -1
  7. package/dist/cli/invocationRouter.js +1 -1
  8. package/dist/cli/roleWizard.js +8 -8
  9. package/dist/cli.js +116 -72
  10. package/dist/commands/agentCommands.js +5 -5
  11. package/dist/commands/configCommands.js +351 -104
  12. package/dist/commands/configOverview.js +60 -0
  13. package/dist/commands/deliveryGuardPreflight.js +2 -2
  14. package/dist/commands/globalRoleCommands.js +9 -9
  15. package/dist/commands/profileCommands.js +8 -8
  16. package/dist/commands/resourcesCommands.js +6 -5
  17. package/dist/commands/taskCommands.js +3 -6
  18. package/dist/commands/taskRoleRuntimeStatus.js +3 -1
  19. package/dist/commands/telemetryCommands.js +11 -6
  20. package/dist/config/configCatalog.js +42 -0
  21. package/dist/config/yuiConfig.js +80 -35
  22. package/dist/context/sessionBootstrapManifest.js +1 -1
  23. package/dist/controller/clientRuntime.js +0 -2
  24. package/dist/controller/controller.js +21 -9
  25. package/dist/controller/fileSchedulerStoreAdapter.js +20 -9
  26. package/dist/controller/runtime.js +32 -18
  27. package/dist/coordination/workMailbox.js +25 -22
  28. package/dist/doctor/doctor.js +2 -2
  29. package/dist/resources/autoResourceGc.js +3 -1
  30. package/dist/review/reviewConfig.js +0 -2
  31. package/dist/run/providerRetry.js +29 -16
  32. package/dist/run/providerRetryConfig.js +5 -3
  33. package/dist/runtime/launchDiagnostics.js +1 -1
  34. package/dist/scheduler/roleRunStall.js +12 -9
  35. package/dist/setup/setupCommand.js +153 -492
  36. package/dist/storage/compatibleTaskStore.js +9 -5
  37. package/dist/storage/migration/productionRegistry.js +58 -0
  38. package/dist/storage/sqliteStore.js +9 -2
  39. package/dist/storage/taskStore.js +21 -2
  40. package/dist/telemetry/sqliteTelemetryStore.js +9 -1
  41. package/dist/telemetry/telemetryConfig.js +1 -18
  42. package/dist/telemetry/telemetryStore.js +2 -2
  43. package/dist/telemetry/telemetryWiring.js +6 -5
  44. package/dist/web/webSnapshot.js +5 -3
  45. package/i18n/README.zh-CN.md +37 -32
  46. package/package.json +1 -1
  47. package/skills/yui-leader/SKILL.md +12 -5
  48. package/skills/yui-operator/SKILL.md +44 -6
  49. 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.");
@@ -20,7 +20,6 @@ import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRol
20
20
  import { agentRunDeliveryReceiptId, createAgentRun, withAgentRunContextSnapshot } from "../run/agentRun.js";
21
21
  import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProjection.js";
22
22
  import { matchYieldReceipt } from "../run/yieldReceipt.js";
23
- import { providerRetryConfig } from "../run/providerRetryConfig.js";
24
23
  import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, deltaRecheckBlocksAcceptance, finishReviewRound, parseReviewYieldReport, recordReviewWorkspaceDisposition, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
25
24
  import { buildDeltaRecheckDispatchContext, verifyDeltaRecheckDiff } from "../review/deltaRecheck.js";
26
25
  import { buildTaskFinalReviewFindingContext, dispositionReviewFinding, planRepairGroups, reconcileReviewFindings, reconcileReviewFindingsAfterReview } from "../review/reviewFindingLedger.js";
@@ -2840,7 +2839,7 @@ function requestTaskReviewRound(args, store, options) {
2840
2839
  const reviewConfig = tx.getReviewConfig();
2841
2840
  if (reviewConfig === null || reviewConfig.deltaRecheck !== "enabled") {
2842
2841
  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` "
2842
+ + "Set `yui config workflow set review --role <role> --trigger final --delta-recheck enabled` "
2844
2843
  + "or request a full Review.");
2845
2844
  }
2846
2845
  if (taskFinalContract !== undefined) {
@@ -4434,9 +4433,7 @@ function buildYieldOutcome(run, inputSummary, options) {
4434
4433
  * for the same outcome, fails closed for a different outcome, or returns
4435
4434
  * `null` to keep the legacy "already terminal" behavior.
4436
4435
  */
4437
- function replayYieldReceipt(run, inputSummary, options, retryConfig) {
4438
- if (!retryConfig.yieldReceiptReplay)
4439
- return null;
4436
+ function replayYieldReceipt(run, inputSummary, options) {
4440
4437
  if (run.yieldReceipt === undefined)
4441
4438
  return null;
4442
4439
  const outcome = buildYieldOutcome(run, inputSummary, options);
@@ -4492,7 +4489,7 @@ function yieldRun(args, store, options) {
4492
4489
  // transaction; the receipt is immutable once committed.
4493
4490
  const existing = requireRun(store, parsed.positionals[0], options);
4494
4491
  if (existing.status !== "active") {
4495
- const replayed = replayYieldReceipt(existing, inputSummary, options, providerRetryConfig(store.getConfig()));
4492
+ const replayed = replayYieldReceipt(existing, inputSummary, options);
4496
4493
  if (replayed !== null)
4497
4494
  return replayed;
4498
4495
  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
+ }
@@ -1,10 +1,13 @@
1
1
  import { supportedAgentAdapterIds } from "../agent/adapterCatalog.js";
2
- import { PROVIDER_RETRY_MAX_WINDOW_MS } from "../run/providerRetry.js";
3
- import { DEFAULT_RUN_CAP, DEFAULT_TELEMETRY_MODE, DEFAULT_TERMINAL_KEEP, MAX_RUN_CAP } from "../telemetry/telemetryConfig.js";
2
+ import { DEFAULT_RUN_CAP, DEFAULT_TERMINAL_KEEP, MAX_RUN_CAP } from "../telemetry/telemetryConfig.js";
3
+ import { DEFAULT_RUNTIME_HEALTH_POLICY } from "../runtime/runtimeHealthPolicy.js";
4
4
  export const DEFAULT_RECONCILIATION_INTERVAL_SECONDS = 120;
5
5
  export const MIN_RECONCILIATION_INTERVAL_SECONDS = 5;
6
6
  export const MAX_RECONCILIATION_INTERVAL_SECONDS = 300;
7
7
  export const DEFAULT_RESOURCES_GC_MODE = "report";
8
+ export const DEFAULT_RESOURCES_QUARANTINE_TTL_HOURS = 24;
9
+ export const MIN_RESOURCES_QUARANTINE_TTL_HOURS = 1;
10
+ export const MAX_RESOURCES_QUARANTINE_TTL_HOURS = 720;
8
11
  /**
9
12
  * Resolves the Resource GC mode. `report` (default) only reports candidates;
10
13
  * `quarantine` allows `yui resources gc --apply` to quarantine releasable
@@ -29,6 +32,9 @@ export function resolveResourcesGcAutoQuarantine(value) {
29
32
  return value;
30
33
  throw new TypeError("resourcesGcAutoQuarantine must be a boolean.");
31
34
  }
35
+ export function resolveResourcesQuarantineTtlHours(value) {
36
+ return resolveBoundedPositiveInteger(value, DEFAULT_RESOURCES_QUARANTINE_TTL_HOURS, MIN_RESOURCES_QUARANTINE_TTL_HOURS, MAX_RESOURCES_QUARANTINE_TTL_HOURS, "resourcesQuarantineTtlHours");
37
+ }
32
38
  /**
33
39
  * Resolves the durable Yui setting used for low-frequency recovery
34
40
  * reconciliation. Normal durable state changes wake the Controller through
@@ -72,6 +78,9 @@ export function resolveLeaderNextActionMode(value) {
72
78
  // ── Issue 01: Provider retry ──────────────────────────────────────────────
73
79
  export const PROVIDER_RETRY_MODES = ["off", "shadow", "enforce"];
74
80
  export const DEFAULT_PROVIDER_RETRY_MODE = "enforce";
81
+ export const DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS = Object.freeze([2, 5, 15]);
82
+ export const DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS = 600;
83
+ export const MAX_PROVIDER_RETRY_ATTEMPTS = 10;
75
84
  export function resolveProviderRetryMode(value) {
76
85
  if (value === undefined || value === null)
77
86
  return DEFAULT_PROVIDER_RETRY_MODE;
@@ -122,21 +131,27 @@ export function resolveProviderRetryAdapters(value) {
122
131
  }
123
132
  return adapters;
124
133
  }
125
- export function resolveProviderRetryMaxWindowMs(value) {
134
+ export function resolveProviderRetryDelaysSeconds(value) {
126
135
  if (value === undefined || value === null)
127
- return PROVIDER_RETRY_MAX_WINDOW_MS;
128
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
129
- throw new TypeError("providerRetryMaxWindowMs must be a positive integer.");
136
+ return [...DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS];
137
+ if (!Array.isArray(value) || value.length < 1 || value.length > MAX_PROVIDER_RETRY_ATTEMPTS) {
138
+ throw new TypeError(`providerRetryDelaysSeconds must contain 1-${MAX_PROVIDER_RETRY_ATTEMPTS} positive integers.`);
130
139
  }
131
- return value;
132
- }
133
- export function resolveYieldReceiptReplay(value) {
134
- if (value === undefined || value === null)
135
- return true;
136
- if (typeof value !== "boolean") {
137
- throw new TypeError("yieldReceiptReplay must be a boolean.");
140
+ const delays = value.map((entry) => {
141
+ if (typeof entry !== "number" || !Number.isSafeInteger(entry) || entry < 1 || entry > 600) {
142
+ throw new TypeError("providerRetryDelaysSeconds entries must be integers from 1 to 600.");
143
+ }
144
+ return entry;
145
+ });
146
+ for (let index = 1; index < delays.length; index += 1) {
147
+ if (delays[index] < delays[index - 1]) {
148
+ throw new TypeError("providerRetryDelaysSeconds must be ordered from shortest to longest.");
149
+ }
138
150
  }
139
- return value;
151
+ return delays;
152
+ }
153
+ export function resolveProviderRetryMaxWindowSeconds(value) {
154
+ return resolveBoundedPositiveInteger(value, DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS, 1, Number.MAX_SAFE_INTEGER, "providerRetryMaxWindowSeconds");
140
155
  }
141
156
  // ── Executable paths ──────────────────────────────────────────────────────
142
157
  export function resolveTmuxBin(value) {
@@ -147,29 +162,13 @@ export function resolveTmuxBin(value) {
147
162
  }
148
163
  return value.trim();
149
164
  }
150
- export function resolveGitBin(value) {
151
- if (value === undefined || value === null)
152
- return "git";
153
- if (typeof value !== "string" || value.trim().length === 0) {
154
- throw new TypeError("gitBin must be a non-empty string.");
155
- }
156
- return value.trim();
157
- }
158
165
  // ── Telemetry ─────────────────────────────────────────────────────────────
159
- const TELEMETRY_MODES = ["legacy", "dual", "bounded"];
160
- export function resolveTelemetryMode(value) {
166
+ export function resolveTelemetryEnabled(value) {
161
167
  if (value === undefined || value === null)
162
- return DEFAULT_TELEMETRY_MODE;
163
- if (typeof value !== "string") {
164
- throw new TypeError("telemetryMode must be legacy, dual, or bounded.");
165
- }
166
- const normalized = value.trim().toLowerCase();
167
- if (normalized.length === 0)
168
- return DEFAULT_TELEMETRY_MODE;
169
- if (!TELEMETRY_MODES.includes(normalized)) {
170
- throw new TypeError(`telemetryMode must be one of ${TELEMETRY_MODES.join(", ")}; got ${JSON.stringify(normalized)}.`);
171
- }
172
- return normalized;
168
+ return false;
169
+ if (typeof value !== "boolean")
170
+ throw new TypeError("telemetryEnabled must be a boolean.");
171
+ return value;
173
172
  }
174
173
  export function resolveTelemetryTerminalKeep(value) {
175
174
  if (value === undefined || value === null)
@@ -190,6 +189,43 @@ export function resolveTelemetryRunCap(value) {
190
189
  }
191
190
  return value;
192
191
  }
192
+ // ── Runtime and workflow policy ───────────────────────────────────────────
193
+ export const DEFAULT_CONTROLLER_TASK_CONCURRENCY = 4;
194
+ export const MAX_CONTROLLER_TASK_CONCURRENCY = 32;
195
+ export const DEFAULT_AGENT_LAUNCH_INACTIVITY_TIMEOUT_SECONDS = 300;
196
+ export const DEFAULT_DELIVERY_TIMEOUT_SECONDS = 120;
197
+ export const DEFAULT_LEADER_SEMANTIC_BUDGET_TURNS = 3;
198
+ export const DEFAULT_TMUX_HISTORY_LIMIT = 100_000;
199
+ export function resolveRuntimeHealth(configured) {
200
+ if (configured === undefined || configured === null)
201
+ return DEFAULT_RUNTIME_HEALTH_POLICY;
202
+ if (typeof configured !== "object" || Array.isArray(configured)) {
203
+ throw new TypeError("runtimeHealth must be an object.");
204
+ }
205
+ const value = configured;
206
+ const quietAfterMs = resolveBoundedPositiveInteger(value.quietAfterSeconds, DEFAULT_RUNTIME_HEALTH_POLICY.quietAfterMs / 1_000, 30, 86_400, "runtimeHealth.quietAfterSeconds") * 1_000;
207
+ const diagnosticAfterMs = resolveBoundedPositiveInteger(value.diagnosticAfterSeconds, DEFAULT_RUNTIME_HEALTH_POLICY.diagnosticAfterMs / 1_000, 30, 86_400, "runtimeHealth.diagnosticAfterSeconds") * 1_000;
208
+ const stallWindowMs = resolveBoundedPositiveInteger(value.stallAfterSeconds, DEFAULT_RUNTIME_HEALTH_POLICY.stallWindowMs / 1_000, 60, 604_800, "runtimeHealth.stallAfterSeconds") * 1_000;
209
+ if (!(quietAfterMs < diagnosticAfterMs && diagnosticAfterMs < stallWindowMs)) {
210
+ throw new TypeError("runtimeHealth thresholds must be ordered quietAfterSeconds < diagnosticAfterSeconds < stallAfterSeconds.");
211
+ }
212
+ return Object.freeze({ quietAfterMs, diagnosticAfterMs, stallWindowMs });
213
+ }
214
+ export function resolveControllerTaskConcurrency(value) {
215
+ return resolveBoundedPositiveInteger(value, DEFAULT_CONTROLLER_TASK_CONCURRENCY, 1, MAX_CONTROLLER_TASK_CONCURRENCY, "controllerTaskConcurrency");
216
+ }
217
+ export function resolveAgentLaunchInactivityTimeoutSeconds(value) {
218
+ return resolveBoundedPositiveInteger(value, DEFAULT_AGENT_LAUNCH_INACTIVITY_TIMEOUT_SECONDS, 15, 3_600, "agentLaunchInactivityTimeoutSeconds");
219
+ }
220
+ export function resolveDeliveryTimeoutSeconds(value) {
221
+ return resolveBoundedPositiveInteger(value, DEFAULT_DELIVERY_TIMEOUT_SECONDS, 5, 600, "deliveryTimeoutSeconds");
222
+ }
223
+ export function resolveLeaderSemanticBudgetTurns(value) {
224
+ return resolveBoundedPositiveInteger(value, DEFAULT_LEADER_SEMANTIC_BUDGET_TURNS, 1, 20, "leaderSemanticBudgetTurns");
225
+ }
226
+ export function resolveTmuxHistoryLimit(value) {
227
+ return resolveBoundedPositiveInteger(value, DEFAULT_TMUX_HISTORY_LIMIT, 1_000, 1_000_000, "tmuxHistoryLimit");
228
+ }
193
229
  /**
194
230
  * Issue 04 (context token budget): thresholds for one native Session
195
231
  * generation's observed per-request input peak, measured in tokens. When the
@@ -231,3 +267,12 @@ export function resolveContextBudget(configured) {
231
267
  }
232
268
  return { softTokens, hardTokens };
233
269
  }
270
+ function resolveBoundedPositiveInteger(value, fallback, minimum, maximum, label) {
271
+ if (value === undefined || value === null)
272
+ return fallback;
273
+ if (typeof value !== "number" || !Number.isSafeInteger(value)
274
+ || value < minimum || value > maximum) {
275
+ throw new TypeError(`${label} must be an integer from ${minimum} to ${maximum}.`);
276
+ }
277
+ return value;
278
+ }
@@ -53,7 +53,7 @@ export function materializeSessionBootstrap(input) {
53
53
  roleProfileRef: { digest: profileDigest, path: roleProfilePath },
54
54
  contextProtocol: input.owner.scope === "global"
55
55
  ? {
56
- loadCommand: `\"${sessionCliPath}\" role context \"$YUI_ROLE\" --json`
56
+ loadCommand: `\"${sessionCliPath}\" session context \"$YUI_ROLE\" --json`
57
57
  }
58
58
  : {
59
59
  loadCommand: `\"${sessionCliPath}\" task run context \"$YUI_TASK_ID/<run-id>\" --json`,
@@ -29,8 +29,6 @@ const CONTROLLER_OPERATIONAL_ENVIRONMENT = [
29
29
  // rr13/test: Forward the liveness seam so an integration test's Controller
30
30
  // subprocess does not reap a saved active Leader Run without a real tmux role.
31
31
  "YUI_TEST_ROLE_LIVENESS_PRESENT",
32
- // Issue 02: agent-signal-driven launch monitoring is Controller-owned.
33
- "YUI_LAUNCH_INACTIVITY_TIMEOUT_MS",
34
32
  ...EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES
35
33
  ];
36
34
  /**