@zq-silk/yui 0.16.0 → 0.99.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 (114) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/ARCHITECTURE.zh-CN.md +8 -0
  3. package/README.md +3 -0
  4. package/dist/agent/managedRuntimeEnvironment.js +0 -4
  5. package/dist/agentRun/agentRun.js +7 -17
  6. package/dist/cli/agentConfigurationPicker.js +24 -21
  7. package/dist/cli/commandCatalog.js +18 -10
  8. package/dist/cli/roleWizard.js +41 -30
  9. package/dist/cli/updateCommand.js +35 -13
  10. package/dist/cli/updateOrchestrator.js +73 -33
  11. package/dist/cli/updatePorts.js +51 -20
  12. package/dist/cli.js +35 -26
  13. package/dist/commands/controllerCommands.js +1 -1
  14. package/dist/commands/executionAuditCommands.js +2 -2
  15. package/dist/commands/projectCommands.js +1 -1
  16. package/dist/commands/releaseCommands.js +9 -38
  17. package/dist/commands/taskCommands.js +84 -81
  18. package/dist/commands/taskCompletionGate.js +0 -117
  19. package/dist/commands/taskRoleRuntimeStatus.js +1 -1
  20. package/dist/commands/taskUpstreamCommands.js +93 -54
  21. package/dist/context/runContextPack.js +30 -54
  22. package/dist/context/runInputContract.js +9 -0
  23. package/dist/context/sessionBootstrapManifest.js +11 -19
  24. package/dist/controller/agentHostObservation.js +4 -2
  25. package/dist/controller/controller.js +8 -6
  26. package/dist/controller/fileSchedulerStoreAdapter.js +85 -18
  27. package/dist/controller/globalInputDelivery.js +28 -7
  28. package/dist/controller/globalRuntimeAttention.js +34 -0
  29. package/dist/controller/operatorNotification.js +31 -0
  30. package/dist/controller/providerRetryAdmission.js +3 -1
  31. package/dist/controller/providerRetryDelivery.js +10 -3
  32. package/dist/controller/runtime.js +1 -27
  33. package/dist/controller/sessionOwnerReconciliation.js +42 -24
  34. package/dist/controller/structuredProviderObservation.js +27 -0
  35. package/dist/controller/updateReconciliation.js +72 -20
  36. package/dist/doctor/doctor.js +2 -1
  37. package/dist/executor/agentAdapter.js +28 -96
  38. package/dist/executor/agentConfigurationCatalog.js +13 -68
  39. package/dist/executor/agentConfigurationFields.js +120 -0
  40. package/dist/executor/agentConfigurationProbe.js +55 -80
  41. package/dist/executor/agentExecutor.js +5 -2
  42. package/dist/executor/executorRegistry.js +0 -9
  43. package/dist/executor/fileRoleLaunchPlanner.js +12 -11
  44. package/dist/interaction/operatorPresentation.js +2 -1
  45. package/dist/message/messageContinuation.js +8 -9
  46. package/dist/observability/executionAudit.js +1 -10
  47. package/dist/observability/runtimeIdentity.js +0 -23
  48. package/dist/output/agentConfigurationPresentation.js +4 -2
  49. package/dist/release/releaseHandover.js +2 -2
  50. package/dist/release/releaseIdempotencyStore.js +0 -23
  51. package/dist/release/releaseWorkflowPorts.js +8 -4
  52. package/dist/repository/gitWorkspace.js +66 -19
  53. package/dist/repository/taskWorkspacePreparer.js +75 -68
  54. package/dist/resources/resourceRegistry.js +90 -44
  55. package/dist/resources/sqliteResourceRegistry.js +3 -5
  56. package/dist/runtime/acpSession.js +21 -5
  57. package/dist/runtime/agentEndpoint.js +4 -1
  58. package/dist/runtime/agentHost.js +22 -62
  59. package/dist/runtime/agentHostCleanup.js +85 -0
  60. package/dist/runtime/agentHostProtocol.js +1 -1
  61. package/dist/runtime/builtinAgentDrivers.js +46 -119
  62. package/dist/runtime/builtinTranscriptObserver.js +10 -6
  63. package/dist/runtime/codexAppServerRuntime.js +55 -89
  64. package/dist/runtime/jsonLineChannel.js +35 -7
  65. package/dist/runtime/providerContinuationReconciliationService.js +42 -3
  66. package/dist/runtime/providerRetry.js +15 -0
  67. package/dist/runtime/providerRuntimeReconciler.js +12 -2
  68. package/dist/runtime/structuredProviderHost.js +106 -81
  69. package/dist/scheduler/activeRoleRunDelivery.js +5 -1
  70. package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
  71. package/dist/storage/upgrade/upgradeOrchestrator.js +0 -10
  72. package/dist/task/nextAction.js +33 -23
  73. package/dist/task/task.js +2 -1
  74. package/dist/web/assets/assetManifest.js +2 -0
  75. package/dist/web/assets/client/app.js +72 -3
  76. package/dist/web/assets/client/components.js +2 -1
  77. package/dist/web/assets/client/i18n.js +2 -0
  78. package/dist/web/assets/client/taskSummary.js +345 -0
  79. package/dist/web/assets/client/taskSurface.js +44 -26
  80. package/dist/web/assets/client/view.js +33 -0
  81. package/dist/web/assets/shell.js +1 -0
  82. package/dist/web/assets/styles/cards.js +21 -0
  83. package/dist/web/webServer.js +39 -5
  84. package/dist/web/webSessions.js +165 -0
  85. package/dist/web/webSnapshot.js +23 -0
  86. package/dist/web/webTaskSurface.js +23 -0
  87. package/docs/managed-turn-and-session-runtime.md +31 -0
  88. package/docs/managed-turn-and-session-runtime.zh-CN.md +23 -0
  89. package/docs/observability/README.md +47 -0
  90. package/docs/observability/README.zh-CN.md +37 -0
  91. package/docs/project-refresh.md +9 -0
  92. package/docs/project-refresh.zh-CN.md +8 -0
  93. package/docs/provider-retry.md +35 -0
  94. package/docs/release-workflow.md +128 -20
  95. package/docs/release-workflow.zh-CN.md +107 -14
  96. package/docs/roles-and-configuration.md +30 -0
  97. package/docs/roles-and-configuration.zh-CN.md +20 -0
  98. package/docs/task-delivery.md +109 -0
  99. package/docs/task-delivery.zh-CN.md +80 -0
  100. package/docs/task-discovery.md +9 -0
  101. package/docs/task-discovery.zh-CN.md +6 -0
  102. package/docs/testing/verification-levels.md +33 -12
  103. package/docs/testing/verification-levels.zh-CN.md +28 -8
  104. package/i18n/README.zh-CN.md +2 -0
  105. package/package.json +1 -1
  106. package/skills/yui-leader/SKILL.md +11 -0
  107. package/skills/yui-leader/references/execution.md +22 -0
  108. package/skills/yui-leader/references/planning.md +6 -2
  109. package/skills/yui-operator/SKILL.md +45 -13
  110. package/skills/yui-operator/references/task-delivery.md +99 -0
  111. package/skills/yui-runtime/SKILL.md +5 -3
  112. package/skills/yui-runtime/references/publication.md +56 -5
  113. package/skills/yui-runtime/references/recovery.md +12 -2
  114. package/dist/runtime/agentHostCompatibility.js +0 -127
@@ -14,6 +14,33 @@ let deliveryFailure;
14
14
  // Only live diagnostics for this process's immutable files, not another queue.
15
15
  // No payload, retry, or authoritative delivery state is kept in this map.
16
16
  const emittedEvents = new Map();
17
+ /** Retained in the existing Host Inbox; it never settles a Turn or a resource. */
18
+ export async function publishStructuredProviderDiagnostic(input) {
19
+ const diagnostic = input.diagnostic;
20
+ const adapterId = requireIdentity(input.environment.YUI_ADAPTER_ID, "Agent adapter id");
21
+ const owner = describeHostIdentity(input.environment, adapterId, diagnostic.nativeSessionId);
22
+ const failure = diagnostic.failure;
23
+ await persistAndApply(input.home, [observation({
24
+ kind: diagnostic.attemptId === undefined ? "conversation.observed" : "observer.health",
25
+ sequence: nextStructuredSequence(), ordinal: 0,
26
+ observedAt: new Date().toISOString(),
27
+ fence: {
28
+ ...owner, driverId: builtinAgentDriverRegistry().requireByAdapterId(adapterId).id,
29
+ nativeSessionId: diagnostic.nativeSessionId, conversationId: diagnostic.nativeSessionId,
30
+ ...(diagnostic.attemptId === undefined ? {} : { receiptId: diagnostic.attemptId }),
31
+ ...(diagnostic.nativeTurnId === undefined ? {} : { nativeTurnId: diagnostic.nativeTurnId })
32
+ },
33
+ payload: {
34
+ ...(diagnostic.attemptId === undefined ? { recoverability: "unknown" } : {}),
35
+ sourceId: failure.phase === "host-stop" ? "host-cleanup" : "provider-channel",
36
+ observerStatus: "unavailable", observerDetail: failure.detail,
37
+ failure: { error: standardAgentError({
38
+ source: "host", phase: failure.phase, message: failure.detail, raw: failure.raw ?? failure.detail,
39
+ inputDisposition: failure.inputDisposition, sessionDisposition: failure.sessionDisposition
40
+ }) }
41
+ }
42
+ })], input.environment);
43
+ }
17
44
  /** A live transport observation, independent of Provider state and Run truth. */
18
45
  export function structuredProviderEventDelivery(home, environment, nativeSessionId) {
19
46
  try {
@@ -3,6 +3,14 @@ import { acquireHomeLifecycleLock } from "../core/controllerServer.js";
3
3
  import { cleanControllerResource } from "./resourceCleanupLinux.js";
4
4
  import { scanControllerResourceInventory } from "./resourceInventoryLinux.js";
5
5
  const MAX_RECONCILIATION_PASSES = 4;
6
+ export class UpdateControllerReconciliationError extends Error {
7
+ result;
8
+ constructor(message, result, options) {
9
+ super(message, options);
10
+ this.result = result;
11
+ this.name = "UpdateControllerReconciliationError";
12
+ }
13
+ }
6
14
  /**
7
15
  * Reconcile only Controller-owned resources for the Home being updated.
8
16
  *
@@ -12,21 +20,49 @@ const MAX_RECONCILIATION_PASSES = 4;
12
20
  * and inode fingerprints. Agent, tmux, app, and foreign-Home resources are
13
21
  * deliberately outside this operation.
14
22
  */
15
- export async function reconcileControllerResourcesForUpdate(home, environment = process.env, tmuxBin) {
23
+ export async function reconcileControllerResourcesForUpdate(home, environment = process.env, tmuxBin, ports = {
24
+ scan: scanControllerResourceInventory,
25
+ clean: cleanControllerResource,
26
+ acquireLock: acquireHomeLifecycleLock
27
+ }) {
16
28
  const resolvedHome = resolve(home);
17
- const releaseLock = await acquireHomeLifecycleLock(resolvedHome, {
18
- removeStaleOwner: true
19
- });
20
29
  const cleaned = new Set();
21
- try {
22
- for (let pass = 0; pass < MAX_RECONCILIATION_PASSES; pass += 1) {
23
- const snapshot = await scanControllerResourceInventory({
24
- currentHome: resolvedHome,
25
- scope: "current",
26
- environment,
30
+ const attempts = [];
31
+ let remaining = [];
32
+ let observedAt;
33
+ let observationError;
34
+ let lockReleaseError;
35
+ const result = () => ({
36
+ home: resolvedHome, cleaned: [...cleaned], attempts: [...attempts], remaining: [...remaining],
37
+ ...(observedAt === undefined ? {} : { observedAt }),
38
+ ...(observationError === undefined ? {} : { observationError }),
39
+ ...(lockReleaseError === undefined ? {} : { lockReleaseError })
40
+ });
41
+ const scan = async () => {
42
+ try {
43
+ const snapshot = await ports.scan({
44
+ currentHome: resolvedHome, scope: "current", environment,
27
45
  ...(tmuxBin === undefined ? {} : { tmuxBin })
28
46
  });
47
+ if (snapshot.scope === "current" && resolve(snapshot.currentHome) === resolvedHome) {
48
+ remaining = controllerResources(snapshot, resolvedHome);
49
+ observedAt = snapshot.observedAt;
50
+ }
29
51
  assertCertainSnapshot(snapshot, resolvedHome);
52
+ observationError = undefined;
53
+ return snapshot;
54
+ }
55
+ catch (error) {
56
+ observationError = messageOf(error);
57
+ throw error;
58
+ }
59
+ };
60
+ let releaseLock;
61
+ let failure;
62
+ try {
63
+ releaseLock = await ports.acquireLock(resolvedHome, { removeStaleOwner: true });
64
+ for (let pass = 0; pass < MAX_RECONCILIATION_PASSES; pass += 1) {
65
+ const snapshot = await scan();
30
66
  const resources = controllerResources(snapshot, resolvedHome);
31
67
  const controllers = resources.filter(({ kind }) => kind === "controller");
32
68
  const current = controllers.filter(({ state }) => state === "current");
@@ -50,36 +86,52 @@ export async function reconcileControllerResourcesForUpdate(home, environment =
50
86
  }
51
87
  const candidates = [...historicalCleanup, ...staleArtifactCleanup];
52
88
  if (candidates.length === 0) {
53
- return { cleaned: [...cleaned] };
89
+ break;
54
90
  }
55
91
  for (const candidate of candidates) {
56
92
  try {
57
- await cleanControllerResource(candidate, { environment, ...(tmuxBin === undefined ? {} : { tmuxBin }) });
93
+ await ports.clean(candidate, { environment, ...(tmuxBin === undefined ? {} : { tmuxBin }) });
58
94
  cleaned.add(candidate.id);
95
+ attempts.push({ resource: candidate, outcome: "cleaned" });
96
+ remaining = remaining.filter(({ id }) => id !== candidate.id);
59
97
  }
60
98
  catch (error) {
61
99
  // A concurrent exact cleanup that already reached the desired state
62
100
  // is harmless. Anything still present or reclassified is a real
63
101
  // ownership change and must remain a user-visible blocker.
64
- const afterFailure = await scanControllerResourceInventory({
65
- currentHome: resolvedHome,
66
- scope: "current",
67
- environment
68
- });
69
- assertCertainSnapshot(afterFailure, resolvedHome);
102
+ const attempt = { resource: candidate, outcome: "unknown", error: messageOf(error) };
103
+ attempts.push(attempt);
104
+ // If this observation also fails, keep BOTH failures and the exact
105
+ // attempted identity. Failure to observe absence is never success.
106
+ const afterFailure = await scan();
70
107
  if (!controllerResources(afterFailure, resolvedHome).some(({ id }) => id === candidate.id)) {
71
108
  cleaned.add(candidate.id);
109
+ attempts[attempts.length - 1] = { ...attempt, outcome: "absent" };
72
110
  continue;
73
111
  }
74
112
  throw reconciliationBlocked(`resource ${candidate.id} changed or could not be cleaned: ${messageOf(error)}`);
75
113
  }
76
114
  }
115
+ if (pass === MAX_RECONCILIATION_PASSES - 1) {
116
+ throw reconciliationBlocked("Controller resources did not converge after bounded cleanup");
117
+ }
77
118
  }
78
- throw reconciliationBlocked("Controller resources did not converge after bounded cleanup");
119
+ }
120
+ catch (error) {
121
+ failure = error;
79
122
  }
80
123
  finally {
81
- await releaseLock();
124
+ try {
125
+ await releaseLock?.();
126
+ }
127
+ catch (error) {
128
+ lockReleaseError = messageOf(error);
129
+ failure ??= error;
130
+ }
82
131
  }
132
+ if (failure !== undefined)
133
+ throw new UpdateControllerReconciliationError(messageOf(failure), result(), { cause: failure });
134
+ return result();
83
135
  }
84
136
  function assertCertainSnapshot(snapshot, resolvedHome) {
85
137
  if (snapshot.scope !== "current"
@@ -694,7 +694,8 @@ function checkAgent(agent, executor, environment) {
694
694
  detail: [
695
695
  `start resume interrupt nativeSession=${snapshot.lifecycle.nativeSessionDiscovery}`,
696
696
  `preInputReady=${snapshot.lifecycle.preInputReadiness.status}`,
697
- `fields=${available}/${degraded}/${unavailable}`,
697
+ `fields(help-observed/static-or-unverified/unavailable)=${available}/${degraded}/${unavailable}`,
698
+ "installation/help inspection only; no model or account capability query",
698
699
  ...snapshot.warnings.map((warning) => `warning=${warning}`)
699
700
  ].join(" ")
700
701
  }
@@ -6,11 +6,12 @@ import { ownedArgumentsForAdapter, validateAgentAdvancedArguments, validateAgent
6
6
  import { writeTextFileAtomically } from "../storage/durableFile.js";
7
7
  import { discoverAcpConfiguration, discoverClaudeConfiguration, discoverCodexConfiguration } from "./agentConfigurationProbe.js";
8
8
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
9
- const SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
10
- const APPROVALS = ["untrusted", "on-request", "never"];
9
+ import { CODEX_SANDBOXES as SANDBOXES, CODEX_APPROVALS as APPROVALS, configurationFieldsFromHelp, configurationFieldWarnings, staticAgentConfigurationFields, STATIC_CONFIGURATION_NOTICE } from "./agentConfigurationFields.js";
11
10
  const PROBE_TIMEOUT_MS = 2_000;
12
11
  const PROBE_MAX_BYTES = 1024 * 1024;
13
- const CODEX_TESTED_THROUGH_VERSION = "0.150.1";
12
+ // Offline producer/type evidence, not a claim of live account/model testing.
13
+ // Keep the existing minimum support floors separate: see provider-protocol-contracts.md.
14
+ const AUDITED_PRODUCER_VERSIONS = { codex: "0.153.4", claude: "2.1.270" };
14
15
  class BaseAdapter {
15
16
  launchContextArgs(_input) {
16
17
  return [];
@@ -556,7 +557,7 @@ export function inspectAgentCapabilities(agent, optionsOrNow = {}) {
556
557
  const at = now.toISOString();
557
558
  const adapter = resolveAgentAdapter(agent.adapterId);
558
559
  validateAgentBaseArguments(agent.adapterId, agent.baseArgs);
559
- const versionRun = run(agent.command, ["--version"]);
560
+ const versionRun = run(agent.command, [...agent.baseArgs, "--version"]);
560
561
  const failure = failed(versionRun);
561
562
  if (failure !== undefined) {
562
563
  const missing = versionRun.error?.code === "ENOENT";
@@ -583,7 +584,7 @@ export function inspectAgentCapabilities(agent, optionsOrNow = {}) {
583
584
  probedAt: at
584
585
  }, fields, at, [`Installed version ${version} is not supported by adapter ${adapter.id}.`]);
585
586
  }
586
- const help = run(agent.command, ["--help"]);
587
+ const help = run(agent.command, [...agent.baseArgs, "--help"]);
587
588
  const helpFailure = failed(help);
588
589
  if (helpFailure !== undefined) {
589
590
  return snapshot(agent, adapter, {
@@ -593,114 +594,45 @@ export function inspectAgentCapabilities(agent, optionsOrNow = {}) {
593
594
  }, fields, at);
594
595
  }
595
596
  const helpOutput = output(help.stdout, help.stderr);
596
- fields = fromHelp(agent.adapterId, helpOutput);
597
+ const helpFields = configurationFieldsFromHelp(agent.adapterId, helpOutput);
598
+ fields = capabilityFields(helpFields);
599
+ warnings.push(...configurationFieldWarnings(helpFields));
597
600
  const missing = missingRequiredCapabilities(adapter.id, helpOutput);
598
601
  if (missing.length > 0) {
599
602
  return snapshot(agent, adapter, {
600
603
  status: "unsupported-version", command: agent.command, version,
601
604
  reason: `${adapter.label} CLI is missing required capabilities: ${missing.join(", ")}.`,
602
605
  probedAt: at
603
- }, fields, at);
606
+ }, fields, at, warnings);
604
607
  }
605
- if (adapter.id === "codex"
606
- && compareVersions(version, CODEX_TESTED_THROUGH_VERSION) > 0) {
607
- warnings.push(`Installed Codex version ${version} is newer than the latest tested version `
608
- + `${CODEX_TESTED_THROUGH_VERSION}; required capabilities were detected.`);
608
+ const audited = adapter.id === "acp" ? undefined : AUDITED_PRODUCER_VERSIONS[adapter.id];
609
+ if (audited !== undefined && compareVersions(version, audited) > 0) {
610
+ warnings.push(`Installed ${adapter.label} version ${version} is newer than the latest audited producer `
611
+ + `${audited}; required CLI flags were detected, not live protocol compatibility.`);
609
612
  }
610
613
  return snapshot(agent, adapter, {
611
614
  status: "installed", command: agent.command, version, probedAt: at
612
615
  }, fields, at, warnings);
613
616
  }
614
617
  function baseline(id) {
615
- // ACP negotiates its whole configurable surface per Session, so a static
616
- // baseline can only say which axes exist — never which values one Agent
617
- // accepts. Model, effort and mode are all `degraded` for exactly that reason:
618
- // they are configurable and the enumeration comes from the live Session, so
619
- // promising values here would be inventing them.
620
- if (id === "acp")
621
- return [
622
- field("model", "enum", "degraded", true),
623
- field("effort", "enum", "degraded", true),
624
- field("permission.strategy", "enum", "available", false, ["default", "bypass", "configured"]),
625
- // The exact mode ids belong to the Agent, and only a live Session lists
626
- // them. `configured` carries whichever id the user names.
627
- field("permission.mode", "enum", "degraded", true),
628
- // Configurable, but only reaches an Agent that advertises
629
- // `sessionCapabilities.additionalDirectories` at `initialize`. A static
630
- // baseline cannot see that handshake, so it reports the field as degraded
631
- // rather than promising delivery it cannot guarantee.
632
- field("additionalDirectories", "path-list", "degraded", true)
633
- ];
634
- if (id === "codex")
635
- return [
636
- field("model", "enum", "degraded", true), field("effort", "enum", "unavailable", true),
637
- field("permission.strategy", "enum", "available", false, ["default", "bypass", "configured"]),
638
- field("permission.sandbox", "enum", "available", false, SANDBOXES),
639
- field("permission.approval", "enum", "available", false, APPROVALS),
640
- field("profile", "string", "available", true), field("search", "boolean", "available", false, ["true"]),
641
- field("additionalDirectories", "path-list", "available", true)
642
- ];
643
- return [
644
- field("model", "enum", "degraded", true, ["fable", "opus", "sonnet"]),
645
- field("effort", "enum", "unavailable", true),
646
- field("permission.strategy", "enum", "available", false, ["default", "bypass", "configured"]),
647
- field("permission.mode", "enum", "unavailable", true),
648
- field("permission.allowedTools", "string-list", "available", true),
649
- field("permission.disallowedTools", "string-list", "available", true),
650
- field("additionalDirectories", "path-list", "available", true), field("settingsFile", "path", "available", true),
651
- field("settingsSources", "string-list", "degraded", false, ["user", "project", "local"])
652
- ];
653
- }
654
- function fromHelp(id, help) {
655
- const fields = baseline(id);
656
- // Nothing in an ACP Agent's `--help` describes the protocol it speaks, so
657
- // there is nothing here to refine: the baseline is already the whole truth.
658
- if (id === "acp")
659
- return fields;
660
- const replacements = id === "codex"
661
- ? [permissionStrategyField(help, "--dangerously-bypass-approvals-and-sandbox"),
662
- choiceField("permission.sandbox", help, "--sandbox", SANDBOXES),
663
- choiceField("permission.approval", help, "--ask-for-approval", APPROVALS)]
664
- : [choiceField("model", help, "--model", ["fable", "opus", "sonnet"], true),
665
- choiceField("effort", help, "--effort", [], true),
666
- permissionStrategyField(help, "--dangerously-skip-permissions"),
667
- choiceField("permission.mode", help, "--permission-mode", [], true),
668
- choiceField("settingsSources", help, "--setting-sources", ["user", "project", "local"])];
669
- const byKey = new Map(replacements.map((value) => [value.key, value]));
670
- return fields.map((value) => byKey.get(value.key) ?? value);
671
- }
672
- function permissionStrategyField(help, bypassFlag) {
673
- const choices = [
674
- "default",
675
- ...(help.includes(bypassFlag) ? ["bypass"] : []),
676
- "configured"
677
- ];
678
- return field("permission.strategy", "enum", choices.includes("bypass") ? "available" : "degraded", false, choices);
679
- }
680
- function choiceField(key, help, flag, fallback, custom = false) {
681
- const choices = helpChoices(help, flag);
682
- return field(key, key === "settingsSources" ? "string-list" : "enum", choices.length > 0 ? "available" : fallback.length > 0 ? "degraded" : "unavailable", custom, choices.length > 0 ? choices : fallback);
683
- }
684
- function field(key, kind, status, allowCustom, choices) {
685
- return { key, kind, status, allowCustom, ...(choices === undefined ? {} : { choices: [...choices] }) };
686
- }
687
- function helpChoices(help, flag) {
688
- const lines = help.replace(/\r\n/g, "\n").split("\n");
689
- const start = lines.findIndex((line) => line.includes(flag));
690
- if (start < 0)
691
- return [];
692
- let end = start + 1;
693
- while (end < lines.length && !/^\s{2,}(?:-[A-Za-z](?:,\s*)?|--)[\w-]/.test(lines[end]))
694
- end += 1;
695
- const body = /(?:\[possible values:\s*([^\]]+)\]|\(choices:\s*([^)]*)\)|\(([^)]*)\))/i
696
- .exec(lines.slice(start, end).join("\n"));
697
- return [...new Set((body?.[1] ?? body?.[2] ?? body?.[3] ?? "").replace(/["']/g, "").split(",")
698
- .map((value) => value.trim().replace(/\.$/, "")).filter((value) => /^[\w.+-]+$/.test(value)))];
618
+ return capabilityFields(staticAgentConfigurationFields(id));
619
+ }
620
+ function capabilityFields(fields) {
621
+ const kinds = {
622
+ profile: "string", search: "boolean", additionalDirectories: "path-list", settingsFile: "path",
623
+ settingsSources: "string-list", "permission.allowedTools": "string-list", "permission.disallowedTools": "string-list"
624
+ };
625
+ return fields.map(field => ({
626
+ key: field.key, kind: kinds[field.key] ?? "enum", allowCustom: field.allowCustom,
627
+ status: field.available === true ? "available" : field.available === false ? "unavailable" : "degraded",
628
+ choices: field.choices.map(choice => choice.value)
629
+ }));
699
630
  }
700
631
  function snapshot(agent, adapter, installation, fields, at, warnings = []) {
701
632
  return { schemaVersion: 1, agentId: agent.id, adapterId: agent.adapterId, installation,
702
633
  lifecycle: { start: true, resume: true, nativeSessionDiscovery: adapter.capabilities.nativeSessionDiscovery,
703
- interrupt: true, preInputReadiness: adapter.capabilities.preInputReadiness }, fields, warnings, refreshedAt: at };
634
+ interrupt: true, preInputReadiness: adapter.capabilities.preInputReadiness }, fields,
635
+ warnings: [...new Set([STATIC_CONFIGURATION_NOTICE, ...warnings])], refreshedAt: at };
704
636
  }
705
637
  function runProbe(command, args) {
706
638
  const result = spawnSync(command, [...args], { encoding: "utf8", shell: false, timeout: PROBE_TIMEOUT_MS,
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { writeTextFileAtomically } from "../storage/durableFile.js";
6
6
  import { resolveAgentAdapter } from "./agentAdapter.js";
7
7
  import { redactAgentErrorText } from "../runtime/agentError.js";
8
+ import { staticAgentConfigurationFields, STATIC_CONFIGURATION_NOTICE } from "./agentConfigurationFields.js";
8
9
  const DEFAULT_TIMEOUT_MS = 8_000;
9
10
  export class AgentConfigurationCatalogService {
10
11
  yuiHome;
@@ -119,71 +120,14 @@ export class AgentConfigurationCatalogService {
119
120
  }
120
121
  }
121
122
  export function fallbackAgentConfigurationCatalog(agent) {
122
- const choice = (value) => ({ value, label: value });
123
- const common = [
124
- field("model", [], true),
125
- field("effort", [], true)
126
- ];
127
- // The fallback exists for when the probe could not run, so it must still be
128
- // this Agent's own shape. Falling through to Claude's fields handed the
129
- // caller a catalog whose `adapterId` contradicted the Agent it described.
130
- if (agent.adapterId === "acp") {
131
- return {
132
- schemaVersion: 1,
133
- agentId: agent.id,
134
- adapterId: "acp",
135
- models: [],
136
- fields: [
137
- ...common,
138
- field("permission.strategy", [choice("default"), choice("bypass"), choice("configured")], false),
139
- // The mode ids come from a live Session, so an offline catalog can only
140
- // say the axis exists. Listing candidates here would invent an Agent's
141
- // vocabulary from a build-time guess.
142
- field("permission.mode", [], true)
143
- ],
144
- warnings: ["Runtime configuration catalog is unavailable."]
145
- };
146
- }
147
- return agent.adapterId === "codex"
148
- ? {
149
- schemaVersion: 1,
150
- agentId: agent.id,
151
- adapterId: "codex",
152
- models: [],
153
- fields: [
154
- ...common,
155
- field("permission.strategy", ["default", "bypass", "configured"].map(choice), false),
156
- field("permission.sandbox", [
157
- "read-only", "workspace-write", "danger-full-access"
158
- ].map(choice), false),
159
- field("permission.approval", [
160
- "untrusted", "on-request", "never"
161
- ].map(choice), false),
162
- field("search", [choice("true")], false),
163
- field("profile", [], true),
164
- field("additionalDirectories", [], true)
165
- ],
166
- warnings: ["Runtime configuration catalog is unavailable."]
167
- }
168
- : {
169
- schemaVersion: 1,
170
- agentId: agent.id,
171
- adapterId: "claude",
172
- models: [],
173
- fields: [
174
- ...common,
175
- field("permission.strategy", ["default", "bypass", "configured"].map(choice), false),
176
- field("permission.mode", [
177
- "acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"
178
- ].map(choice), true),
179
- field("permission.allowedTools", [], true),
180
- field("permission.disallowedTools", [], true),
181
- field("settingsSources", ["user", "project", "local"].map(choice), false),
182
- field("settingsFile", [], true),
183
- field("additionalDirectories", [], true)
184
- ],
185
- warnings: ["Runtime configuration catalog is unavailable."]
186
- };
123
+ return {
124
+ schemaVersion: 1,
125
+ agentId: agent.id,
126
+ adapterId: agent.adapterId,
127
+ models: [],
128
+ fields: staticAgentConfigurationFields(agent.adapterId),
129
+ warnings: ["Runtime configuration catalog is unavailable.", STATIC_CONFIGURATION_NOTICE]
130
+ };
187
131
  }
188
132
  export function configurationField(catalog, key) {
189
133
  return catalog.fields.find((candidate) => candidate.key === key);
@@ -232,9 +176,6 @@ export function validateAgentLaunchConfiguration(catalog, config) {
232
176
  + `Inspect current options: yui config agent capabilities ${catalog.agentId}.`);
233
177
  }
234
178
  }
235
- function field(key, choices, allowCustom) {
236
- return { key, choices, allowCustom };
237
- }
238
179
  function catalogFingerprint(input, environment) {
239
180
  const bindings = input.agent.environment.map((binding) => ({
240
181
  target: binding.target,
@@ -259,6 +200,10 @@ function catalogFingerprint(input, environment) {
259
200
  }
260
201
  : null;
261
202
  return createHash("sha256").update(JSON.stringify({
203
+ // Old derived caches may contain guessed help values. They cannot establish
204
+ // the current discovery contract. No durable record or cache shape changes.
205
+ discoveryContract: "explicit-native-enumeration",
206
+ agentId: input.agent.id,
262
207
  // The component, not just the plan: two ACP products answer the same
263
208
  // handshake differently, so a cache keyed on the plan alone would serve one
264
209
  // product's capabilities for the other.
@@ -0,0 +1,120 @@
1
+ // These are Yui's accepted configuration values, not a native capability probe.
2
+ // Keep existing explicit configuration legal; current native choices come from
3
+ // help and requirements, and may be a smaller set.
4
+ export const CODEX_SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
5
+ export const CODEX_APPROVALS = ["untrusted", "on-request", "never"];
6
+ export const STATIC_CONFIGURATION_NOTICE = "Static adapter configuration contracts are not native enumeration or proof of Provider acceptance; see field reasons.";
7
+ export function staticAgentConfigurationFields(id) {
8
+ const fields = [
9
+ staticField("model", [], true, "Model values require native metadata; custom IDs remain explicit input."),
10
+ staticField("effort", [], true, "Effort values depend on the selected native model."),
11
+ staticField("permission.strategy", ["default", "bypass", "configured"], false, "These are Yui strategies, not Provider modes.")
12
+ ];
13
+ if (id === "codex")
14
+ return [
15
+ ...fields,
16
+ staticField("permission.sandbox", CODEX_SANDBOXES, false),
17
+ staticField("permission.approval", CODEX_APPROVALS, false, "Native versions may retire values accepted by the adapter."),
18
+ staticField("search", ["true"], false),
19
+ staticField("profile", [], true),
20
+ staticField("additionalDirectories", [], true)
21
+ ];
22
+ if (id === "acp")
23
+ return [
24
+ ...fields,
25
+ staticField("permission.mode", [], true, "Mode IDs are enumerated per Session and checked at launch."),
26
+ staticField("additionalDirectories", [], true, "Delivery requires the Agent's initialize capability; it is unverified offline.")
27
+ ];
28
+ return [
29
+ ...fields,
30
+ staticField("permission.mode", [], true, "No native modes were enumerated."),
31
+ staticField("permission.allowedTools", [], true),
32
+ staticField("permission.disallowedTools", [], true),
33
+ staticField("settingsSources", ["user", "project", "local"], false),
34
+ staticField("settingsFile", [], true),
35
+ staticField("additionalDirectories", [], true)
36
+ ];
37
+ }
38
+ function staticField(key, values, allowCustom, detail = "") {
39
+ return {
40
+ key, choices: values.map(value => ({ value, label: value })), allowCustom,
41
+ reason: `Static adapter configuration contract; native support is unverified.${detail ? ` ${detail}` : ""}`
42
+ };
43
+ }
44
+ /** One help interpretation shared by installation inspection and live probes. */
45
+ export function configurationFieldsFromHelp(id, help) {
46
+ const fields = staticAgentConfigurationFields(id);
47
+ if (id === "acp")
48
+ return fields;
49
+ const bypassFlag = id === "codex"
50
+ ? "--dangerously-bypass-approvals-and-sandbox" : "--dangerously-skip-permissions";
51
+ const replacements = [
52
+ {
53
+ ...staticField("permission.strategy", [
54
+ "default", ...(configurationFlagAvailable(help, bypassFlag) ? ["bypass"] : []), "configured"
55
+ ], false),
56
+ reason: `Static Yui strategies; ${bypassFlag} ${configurationFlagAvailable(help, bypassFlag) ? "reported" : "not reported"} by native --help. This does not confirm account policy.`
57
+ },
58
+ ...(id === "codex"
59
+ ? [
60
+ helpChoiceField("permission.sandbox", help, "--sandbox", false, CODEX_SANDBOXES),
61
+ helpChoiceField("permission.approval", help, "--ask-for-approval", false, CODEX_APPROVALS)
62
+ ]
63
+ : [
64
+ helpChoiceField("permission.mode", help, "--permission-mode", true),
65
+ helpChoiceField("effort", help, "--effort", true),
66
+ // The CLI's description often names settings sources without an enum.
67
+ // Retain the explicit static contract, never relabel it as discovery.
68
+ ...(configurationHelpChoices(help, "--setting-sources").length === 0 ? []
69
+ : [helpChoiceField("settingsSources", help, "--setting-sources", false)])
70
+ ])
71
+ ];
72
+ const byKey = new Map(replacements.map(value => [value.key, value]));
73
+ return fields.map(value => byKey.get(value.key) ?? value);
74
+ }
75
+ function helpChoiceField(key, help, flag, allowCustom, supported) {
76
+ const reported = configurationHelpChoices(help, flag);
77
+ const values = supported === undefined ? reported : reported.filter(value => supported.includes(value));
78
+ return {
79
+ key, choices: values.map(value => ({ value, label: value })), allowCustom,
80
+ available: values.length > 0,
81
+ reason: values.length > 0
82
+ ? `Native --help ${flag} enumeration${supported === undefined ? "" : ", limited to adapter-supported values"}.`
83
+ : `Native enumeration unavailable: ${flag} values were not reported or are not supported by this adapter.`
84
+ };
85
+ }
86
+ export function configurationFieldWarnings(fields) {
87
+ return [
88
+ STATIC_CONFIGURATION_NOTICE,
89
+ ...fields.filter(field => field.available === false)
90
+ .map(field => `${field.key}: ${field.reason ?? "Native capability unavailable."}`)
91
+ ];
92
+ }
93
+ /** Read only an explicit native enum. Prose examples and missing fields are not choices. */
94
+ export function configurationHelpChoices(help, flag) {
95
+ const section = helpSection(help, flag);
96
+ if (section === undefined)
97
+ return [];
98
+ const inline = /(?:\[possible values:\s*([^\]]+)\]|\(choices:\s*([^)]*)\))/i.exec(section);
99
+ const declared = inline?.[1] ?? inline?.[2];
100
+ const bullets = declared === undefined && /Possible values:/i.test(section)
101
+ ? [...section.matchAll(/^\s*-\s*([\w.+-]+)\s*:/gm)].map(match => match[1] ?? "")
102
+ : [];
103
+ return [...new Set((declared ?? bullets.join(",")).replace(/["']/g, "").split(",")
104
+ .map(value => value.trim().replace(/\.$/, "")).filter(value => /^[\w.+-]+$/.test(value)))];
105
+ }
106
+ export function configurationFlagAvailable(help, flag) {
107
+ return helpSection(help, flag) !== undefined;
108
+ }
109
+ function helpSection(help, flag) {
110
+ const lines = help.replace(/\r\n/g, "\n").split("\n");
111
+ const escaped = flag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
112
+ const declaration = new RegExp(`^\\s*(?:-[A-Za-z],?\\s+)?${escaped}(?=[\\s=,<\\[]|$)`);
113
+ const start = lines.findIndex(line => declaration.test(line));
114
+ if (start < 0)
115
+ return undefined;
116
+ let end = start + 1;
117
+ while (end < lines.length && !/^\s*(?:-[A-Za-z],?\s+|--)[\w-]/.test(lines[end]))
118
+ end++;
119
+ return lines.slice(start, end).join("\n");
120
+ }