@tea-agent/loop-agent 0.17.0 → 0.17.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # 更新日志
2
2
 
3
+ ## [0.17.1] - 2026-07-22
4
+
5
+ ### 修复
6
+
7
+ - TaskSpec materialize 在写入 task.json、需求和约束后会自动 adopt 或刷新 managed Task Contract,避免生成 v4 DAG 后立刻因 binding drift 无法 validate/run。
8
+ - Operator Console 的 Pi readiness 改为真实探测 SDK、认证和可用模型;本机已配置 Pi 时不再误报 `setup-required`,界面同步对齐 Observe 暖白画布。
9
+
3
10
  ## [0.17.0] - 2026-07-22
4
11
 
5
12
  ### 新增
@@ -2,6 +2,7 @@
2
2
  * Pi readiness gate for Official Console Happy Path.
3
3
  * setup-required | ready | degraded — no pure form fallback.
4
4
  */
5
+ import path from "node:path";
5
6
  const SETUP_BLOCKED = Object.freeze([
6
7
  "newTask",
7
8
  "importPrd",
@@ -20,6 +21,13 @@ const DEGRADED_BLOCKED = Object.freeze([
20
21
  "runDag",
21
22
  // new Interview may resume; brand-new apply/run still blocked
22
23
  ]);
24
+ /** Default Interview / DAG model matrix (provider, modelId). */
25
+ const INTERVIEW_MODEL_CANDIDATES = Object.freeze([
26
+ ["openai", "gpt-5.5"],
27
+ ["openai", "gpt-5.3-codex-spark"],
28
+ ["zai", "glm-5.2"],
29
+ ["zhipu", "glm-5.2"],
30
+ ]);
23
31
  export function classifyPiReadiness(probe) {
24
32
  if (!probe.sdkAvailable ||
25
33
  !probe.authReady ||
@@ -57,30 +65,194 @@ export function isActionBlockedByReadiness(state, action) {
57
65
  }
58
66
  return DEGRADED_BLOCKED.includes(action);
59
67
  }
68
+ function countAvailableModels(snapshot) {
69
+ return Array.isArray(snapshot.available) ? snapshot.available.length : 0;
70
+ }
71
+ function hasProviderConfig(snapshot) {
72
+ const authKeys = snapshot.auth && typeof snapshot.auth === "object"
73
+ ? Object.keys(snapshot.auth).length
74
+ : 0;
75
+ const configured = snapshot.configuredProviders &&
76
+ typeof snapshot.configuredProviders === "object"
77
+ ? Object.keys(snapshot.configuredProviders).length
78
+ : 0;
79
+ const stored = snapshot.storedProviders && typeof snapshot.storedProviders === "object"
80
+ ? Object.keys(snapshot.storedProviders).length
81
+ : 0;
82
+ return authKeys > 0 || configured > 0 || stored > 0;
83
+ }
60
84
  /**
61
- * Default probe for process boot. Without a real Pi ModelRuntime injection,
62
- * Official Console is setup-required (no form fallback).
63
- * Tests and server may inject a readiness probe override.
85
+ * Discover live Pi SDK / auth / model readiness via ModelRuntime.
86
+ * Kept inside worker/console (dynamic import) so Worker does not depend on
87
+ * executors; contract matches pi-sdk-executor 0.80.10.
64
88
  */
65
- export async function probePiReadiness(options) {
66
- if (options?.override) {
67
- return buildPiReadinessReport(options.override);
89
+ export async function discoverPiReadinessProbe() {
90
+ let sdk;
91
+ try {
92
+ sdk = (await import("@earendil-works/pi-coding-agent"));
93
+ }
94
+ catch (error) {
95
+ const message = error instanceof Error ? error.message : String(error);
96
+ return {
97
+ sdkAvailable: false,
98
+ authReady: false,
99
+ modelsAvailable: false,
100
+ interviewSessionOk: false,
101
+ details: {
102
+ sdkMessage: `Pi SDK import failed: ${message}`,
103
+ authMessage: "skipped (sdk unavailable)",
104
+ modelsMessage: "skipped (sdk unavailable)",
105
+ sessionMessage: "skipped (sdk unavailable)",
106
+ },
107
+ };
108
+ }
109
+ const ModelRuntime = sdk.ModelRuntime;
110
+ if (typeof sdk.createAgentSession !== "function" ||
111
+ typeof sdk.getAgentDir !== "function" ||
112
+ typeof ModelRuntime?.create !== "function") {
113
+ return {
114
+ sdkAvailable: false,
115
+ authReady: false,
116
+ modelsAvailable: false,
117
+ interviewSessionOk: false,
118
+ details: {
119
+ sdkMessage: "Pi SDK incompatible: requires createAgentSession, getAgentDir, and ModelRuntime.create (0.80.10)",
120
+ authMessage: "skipped (sdk incompatible)",
121
+ modelsMessage: "skipped (sdk incompatible)",
122
+ sessionMessage: "skipped (sdk incompatible)",
123
+ },
124
+ };
125
+ }
126
+ let agentDir;
127
+ try {
128
+ agentDir = sdk.getAgentDir();
129
+ }
130
+ catch (error) {
131
+ const message = error instanceof Error ? error.message : String(error);
132
+ return {
133
+ sdkAvailable: false,
134
+ authReady: false,
135
+ modelsAvailable: false,
136
+ interviewSessionOk: false,
137
+ details: {
138
+ sdkMessage: `getAgentDir failed: ${message}`,
139
+ authMessage: "skipped",
140
+ modelsMessage: "skipped",
141
+ sessionMessage: "skipped",
142
+ },
143
+ };
144
+ }
145
+ let runtime;
146
+ try {
147
+ runtime = await ModelRuntime.create({
148
+ authPath: path.join(agentDir, "auth.json"),
149
+ modelsPath: path.join(agentDir, "models.json"),
150
+ });
151
+ }
152
+ catch (error) {
153
+ const message = error instanceof Error ? error.message : String(error);
154
+ return {
155
+ sdkAvailable: true,
156
+ authReady: false,
157
+ modelsAvailable: false,
158
+ interviewSessionOk: false,
159
+ details: {
160
+ sdkMessage: "Pi SDK 0.80.10 contract available",
161
+ authMessage: `ModelRuntime.create failed: ${message}`,
162
+ modelsMessage: "skipped (runtime init failed)",
163
+ sessionMessage: "skipped (runtime init failed)",
164
+ },
165
+ };
166
+ }
167
+ const snapshot = runtime.snapshot ?? {};
168
+ const availableCount = countAvailableModels(snapshot);
169
+ const providerConfigured = hasProviderConfig(snapshot);
170
+ const resolvedInterviewModels = [];
171
+ if (typeof runtime.getModel === "function") {
172
+ for (const [provider, modelId] of INTERVIEW_MODEL_CANDIDATES) {
173
+ try {
174
+ if (runtime.getModel(provider, modelId)) {
175
+ resolvedInterviewModels.push(`${provider}/${modelId}`);
176
+ }
177
+ }
178
+ catch {
179
+ // ignore per-model resolution errors
180
+ }
181
+ }
182
+ }
183
+ const authReady = availableCount > 0 || providerConfigured;
184
+ const modelsAvailable = availableCount > 0 || resolvedInterviewModels.length > 0;
185
+ let interviewSessionOk = false;
186
+ let sessionMessage;
187
+ if (!authReady || !modelsAvailable) {
188
+ sessionMessage =
189
+ "Interview session preflight skipped (auth/models not ready)";
190
+ }
191
+ else if (typeof sdk.SessionManager?.inMemory !== "function") {
192
+ sessionMessage =
193
+ "SessionManager.inMemory unavailable; Interview session preflight failed";
68
194
  }
69
- // Phase 2: real Pi SDK/auth/model discovery is optional injection.
70
- // Default fail-closed to setup-required so Happy Path cannot skip Interview.
71
- const probe = {
72
- sdkAvailable: false,
73
- authReady: false,
74
- modelsAvailable: false,
75
- interviewSessionOk: false,
195
+ else {
196
+ try {
197
+ sdk.SessionManager.inMemory(process.cwd());
198
+ interviewSessionOk = true;
199
+ sessionMessage = `Interview session preflight ok (${availableCount} available model(s); matrix=${resolvedInterviewModels.join(",") || "none"})`;
200
+ }
201
+ catch (error) {
202
+ const message = error instanceof Error ? error.message : String(error);
203
+ sessionMessage = `Interview session preflight failed: ${message}`;
204
+ }
205
+ }
206
+ return {
207
+ sdkAvailable: true,
208
+ authReady,
209
+ modelsAvailable,
210
+ interviewSessionOk,
76
211
  details: {
77
- sdkMessage: "Pi SDK/ModelRuntime not injected; configure Interview model/auth (setup-required)",
78
- authMessage: "no credential probe configured",
79
- modelsMessage: "no model list available",
80
- sessionMessage: "Interview session preflight not run",
212
+ sdkMessage: `Pi SDK 0.80.10 contract available (agentDir=${agentDir})`,
213
+ authMessage: authReady
214
+ ? `credentials/providers ready (available=${availableCount}, providerConfig=${providerConfigured})`
215
+ : "no available models and no stored/configured provider credentials (set Pi auth or provider API keys)",
216
+ modelsMessage: modelsAvailable
217
+ ? `models ready (available=${availableCount}, interviewMatrix=${resolvedInterviewModels.join(",") || "none"})`
218
+ : "no available models for Interview",
219
+ sessionMessage,
81
220
  },
82
221
  };
83
- return buildPiReadinessReport(probe);
222
+ }
223
+ /**
224
+ * Default probe for process boot. Discovers live Pi ModelRuntime when no
225
+ * override is injected. Tests inject override for determinism.
226
+ */
227
+ export async function probePiReadiness(options) {
228
+ if (options?.override) {
229
+ return buildPiReadinessReport(options.override);
230
+ }
231
+ try {
232
+ const probe = await discoverPiReadinessProbe();
233
+ if (options?.siblingControllerOk && !probe.details) {
234
+ probe.details = {};
235
+ }
236
+ if (options?.siblingControllerOk && probe.details) {
237
+ probe.details.sdkMessage = `${probe.details.sdkMessage ?? "sdk probed"}; sibling controller identity ok`;
238
+ }
239
+ return buildPiReadinessReport(probe);
240
+ }
241
+ catch (error) {
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ return buildPiReadinessReport({
244
+ sdkAvailable: false,
245
+ authReady: false,
246
+ modelsAvailable: false,
247
+ interviewSessionOk: false,
248
+ details: {
249
+ sdkMessage: `Pi readiness discovery error: ${message}`,
250
+ authMessage: "no credential probe completed",
251
+ modelsMessage: "no model list available",
252
+ sessionMessage: "Interview session preflight not run",
253
+ },
254
+ });
255
+ }
84
256
  }
85
257
  export function readinessGateFailure(report, action) {
86
258
  if (!isActionBlockedByReadiness(report.state, action))
@@ -0,0 +1 @@
1
+ :root{--canvas: #f7f7f4;--canvas-soft: #fafaf7;--surface: #ffffff;--surface-muted: #f0efe9;--ink: #26251e;--body: #5a5852;--muted: #807d72;--muted-soft: #a09c92;--hairline: #e6e5e0;--hairline-strong: #cfcdc4;--orange: #f54e00;--orange-active: #d04200;--blue: #3f6ea4;--green: #1f8a65;--amber: #a66a00;--red: #c72d53;--peach: #f1d8cb;--mint: #d8ead5;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-8: 32px;--radius-sm: 4px;--radius-md: 6px;--radius-lg: 8px;--transition-fast: .16s ease;--shadow-panel: 0 1px 2px rgb(38 37 30 / 4%);color-scheme:light;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;line-height:1.5}*{box-sizing:border-box}html{background:var(--canvas)}body{margin:0;min-height:100vh;min-width:320px;background:var(--canvas);color:var(--ink);font-size:14px}button,input,select,textarea{font:inherit}a{color:inherit}code,.cmd,.kv dd code{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace}.console-shell{width:min(100% - 48px,1080px);margin:0 auto;padding:var(--space-8) 0 var(--space-12, 48px)}.console-header{display:grid;gap:var(--space-5);grid-template-columns:1.4fr 1fr;align-items:start;margin-bottom:var(--space-6);padding-bottom:var(--space-5);border-bottom:1px solid var(--hairline)}.eyebrow{margin:0 0 var(--space-2);color:var(--orange);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}h1{margin:0;color:var(--ink);font-size:28px;font-weight:500;line-height:1.16;letter-spacing:0}.subtitle{max-width:66ch;margin:var(--space-2) 0 0;color:var(--body)}.muted{color:var(--muted)}.readiness{border:1px solid var(--hairline);background:var(--surface);border-radius:var(--radius-lg);padding:var(--space-5);box-shadow:var(--shadow-panel)}.readiness[data-state=setup-required]{border-color:color-mix(in srgb,var(--amber) 45%,var(--hairline));background:color-mix(in srgb,var(--peach) 35%,var(--surface))}.readiness[data-state=ready]{border-color:color-mix(in srgb,var(--green) 40%,var(--hairline));background:color-mix(in srgb,var(--mint) 40%,var(--surface))}.readiness[data-state=degraded]{border-color:color-mix(in srgb,var(--amber) 45%,var(--hairline));background:color-mix(in srgb,var(--peach) 28%,var(--surface))}.readiness-label{display:block;margin-bottom:var(--space-1);color:var(--muted);font-size:12px;font-weight:600}.readiness strong{color:var(--ink);font-size:16px;font-weight:650}.readiness[data-state=ready] strong{color:var(--green)}.readiness[data-state=setup-required] strong,.readiness[data-state=degraded] strong{color:var(--amber)}.readiness .muted{margin:var(--space-2) 0 0}.console-main{display:grid;gap:var(--space-4)}.panel{min-width:0;border:1px solid var(--hairline);background:var(--surface);border-radius:var(--radius-lg);padding:var(--space-5);box-shadow:var(--shadow-panel)}.panel h2{margin:0 0 var(--space-4);color:var(--ink);font-size:16px;font-weight:700}.panel h3{margin:0 0 var(--space-3);color:var(--ink);font-size:14px;font-weight:700}.empty-state{border:1px dashed var(--hairline-strong);border-radius:var(--radius-md);padding:var(--space-5);background:var(--canvas-soft);color:var(--body)}.empty-title{margin:0 0 var(--space-2);color:var(--ink);font-weight:700}.checklist{margin:var(--space-3) 0 0;padding-left:1.2rem;color:var(--body)}.kv{display:grid;gap:var(--space-2);margin:0}.kv div{display:grid;grid-template-columns:10rem 1fr;gap:var(--space-2);padding:var(--space-2) 0;border-bottom:1px solid var(--hairline)}.kv div:last-child{border-bottom:none}.kv dt{color:var(--muted);font-size:12px;font-weight:600}.kv dd{margin:0;color:var(--ink);overflow-wrap:anywhere}code{font-size:.85rem;color:var(--ink)}.cmd{display:block;margin:0;padding:var(--space-3);border:1px solid var(--hairline);border-radius:var(--radius-md);background:#f0ebe3;color:var(--ink);font-size:.85rem;overflow-x:auto;word-break:break-all}.external-link{color:var(--orange)}.error{color:var(--red)}.wizard-steps{display:flex;flex-wrap:wrap;gap:var(--space-2);list-style:none;padding:0;margin:0 0 var(--space-4);font-size:12px;color:var(--muted)}.wizard-steps li{min-height:28px;display:inline-flex;align-items:center;border:1px solid var(--hairline);border-radius:var(--radius-sm);padding:0 10px;background:var(--canvas-soft);font-weight:600}.wizard-steps li[data-active=true]{border-color:var(--orange);background:var(--surface);color:var(--ink)}.field{display:grid;gap:var(--space-1);margin:0 0 var(--space-4)}.field span{font-size:12px;font-weight:600}.field input{min-height:36px;padding:0 var(--space-3);border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--surface);color:var(--ink)}.field input:focus-visible{outline:2px solid color-mix(in srgb,var(--orange) 55%,white);outline-offset:2px;border-color:var(--orange)}.field input:disabled{background:var(--surface-muted);color:var(--muted);cursor:not-allowed}.cta-row,.wizard-body,.recovery-cta-row{display:flex;flex-wrap:wrap;gap:var(--space-2);align-items:flex-start}.cta-row{margin-top:var(--space-3)}.cta-row .cmd{flex:1 1 100%}button{appearance:none;display:inline-flex;align-items:center;justify-content:center;min-height:36px;padding:0 14px;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--surface);color:var(--ink);font-size:13px;font-weight:600;cursor:pointer;transition:border-color var(--transition-fast),background-color var(--transition-fast),color var(--transition-fast)}button:hover:not(:disabled),button:focus-visible:not(:disabled){border-color:var(--orange);background:var(--surface);color:var(--ink);outline:none}button:disabled{opacity:.5;cursor:not-allowed}.wizard-body button,.cta-row>button:first-child,button.primary{border-color:var(--orange);background:var(--orange);color:#fff}.wizard-body button:hover:not(:disabled),.cta-row>button:first-child:hover:not(:disabled),button.primary:hover:not(:disabled){border-color:var(--orange-active);background:var(--orange-active);color:#fff}.wizard-body{display:block}.wizard-body>div{display:grid;gap:var(--space-2);justify-items:start}.wizard-body button+button{margin-left:0}.recovery-cta-row{margin-top:var(--space-3)}.recovery-cta-row button[disabled]{opacity:.4}@media(max-width:800px){.console-shell{width:min(100% - 24px,1080px);padding-top:var(--space-5)}.console-header,.kv div{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Loop Operator Console</title>
7
- <script type="module" crossorigin src="/assets/index-CbnMgdWa.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-Dnj0RVs8.css">
7
+ <script type="module" crossorigin src="/assets/index-BEIdBogJ.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-Rt0TqimP.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -4,6 +4,9 @@ import path from "node:path";
4
4
  import YAML from "yaml";
5
5
  import { writeTaskConfig, writeTaskArtifactFile } from "../../infrastructure/harness/task-store.js";
6
6
  import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
7
+ import { adoptTaskContract } from "../../task/contract/adopt.js";
8
+ import { observeTaskContractState } from "../../task/contract/observe.js";
9
+ import { sha256Utf8 } from "../../task/contract/hash.js";
7
10
  import { resolveLoopAgentProfile } from "../profile-mapping.js";
8
11
  import { mapRiskLevelToComplexity } from "../task-spec/complexity-mapping.js";
9
12
  import { resolveWorkflow } from "../task-spec/workflow-routing.js";
@@ -89,6 +92,11 @@ export async function materializeTaskSpec(options) {
89
92
  }), "utf-8");
90
93
  await writeFile(sourcePaths.constraintsPath, renderConstraintsMarkdown(options.taskSpec), "utf-8");
91
94
  await writeFile(sourcePaths.taskYamlPath, YAML.stringify(options.taskSpec), "utf-8");
95
+ await ensureMaterializedTaskContractManaged({
96
+ repoRoot: options.repoRoot,
97
+ taskId: harnessTaskId,
98
+ taskSpecPath: options.taskSpecPath,
99
+ });
92
100
  const profileMapping = resolveLoopAgentProfile(options.taskSpec);
93
101
  const manifest = {
94
102
  schemaVersion: 1,
@@ -138,6 +146,32 @@ function slugify(value) {
138
146
  .replace(/-{2,}/g, "-");
139
147
  }
140
148
  /** Exported for unit tests: derived contract must point back to immutable references. */
149
+ async function ensureMaterializedTaskContractManaged(input) {
150
+ const state = await observeTaskContractState(input.repoRoot, input.taskId);
151
+ if (state.effectiveStatus === "managed")
152
+ return;
153
+ if (state.effectiveStatus === "transaction-incomplete") {
154
+ throw new Error(`task contract transaction incomplete for ${input.taskId}; recover before materializing`);
155
+ }
156
+ if (!state.observedCanonicalHash) {
157
+ throw new Error(`unable to observe task contract hash for ${input.taskId}: ${state.observationErrors.map((error) => error.message).join("; ")}`);
158
+ }
159
+ const expectedRevision = state.ref?.revision ?? 0;
160
+ const requestPayload = JSON.stringify({
161
+ taskId: input.taskId,
162
+ taskSpecPath: input.taskSpecPath,
163
+ expectedRevision,
164
+ observedCanonicalHash: state.observedCanonicalHash,
165
+ });
166
+ await adoptTaskContract({
167
+ repoRoot: input.repoRoot,
168
+ taskId: input.taskId,
169
+ expectedRevision,
170
+ expectedObservedHash: state.observedCanonicalHash,
171
+ requestId: `worker-materialize-${sha256Utf8(requestPayload).slice(0, 24)}`,
172
+ requestPayloadSha256: sha256Utf8(requestPayload),
173
+ });
174
+ }
141
175
  export function renderRequirementMarkdown(taskSpec, options = {}) {
142
176
  const lines = [
143
177
  `# ${taskSpec.title}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -1 +0,0 @@
1
- :root{color-scheme:light dark;--bg: #0f1419;--panel: #1a222c;--border: #2c3845;--text: #e7eef7;--muted: #9aabbc;--accent: #5b9fd4;--warn: #d4a15b;--error: #d45b5b;font-family:IBM Plex Sans,Segoe UI,system-ui,sans-serif;line-height:1.5}*{box-sizing:border-box}body{margin:0;min-height:100vh;background:radial-gradient(1200px 600px at 10% -10%,#1b2a3a,var(--bg));color:var(--text)}.console-shell{max-width:1080px;margin:0 auto;padding:2rem 1.25rem 3rem}.console-header{display:grid;gap:1rem;grid-template-columns:1.4fr 1fr;margin-bottom:1.5rem}.eyebrow{margin:0;text-transform:uppercase;letter-spacing:.08em;font-size:.75rem;color:var(--accent)}h1{margin:.25rem 0;font-size:1.75rem}.subtitle,.muted{color:var(--muted)}.readiness{border:1px solid var(--border);background:var(--panel);border-radius:12px;padding:1rem}.readiness[data-state=setup-required]{border-color:var(--warn)}.readiness-label{display:block;font-size:.8rem;color:var(--muted)}.console-main{display:grid;gap:1rem}.panel{border:1px solid var(--border);background:color-mix(in srgb,var(--panel) 92%,black);border-radius:12px;padding:1.25rem}.empty-state{border:1px dashed var(--border);border-radius:10px;padding:1rem;background:#00000026}.empty-title{margin:0 0 .5rem;font-weight:600}.checklist{margin:.75rem 0 0;padding-left:1.2rem;color:var(--muted)}.kv{display:grid;gap:.5rem;margin:0}.kv div{display:grid;grid-template-columns:10rem 1fr;gap:.5rem}.kv dt{color:var(--muted)}.kv dd{margin:0;overflow-wrap:anywhere}code,.cmd{font-family:IBM Plex Mono,ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.85rem}.cmd{display:block;padding:.75rem;border-radius:8px;background:#0b1016;overflow-x:auto}.external-link{color:var(--accent)}.error{color:var(--error)}.wizard-steps{display:flex;flex-wrap:wrap;gap:.5rem;list-style:none;padding:0;margin:0 0 1rem;font-size:.85rem;color:var(--muted)}.wizard-steps li{border:1px solid var(--border);border-radius:999px;padding:.25rem .65rem}.wizard-steps li[data-active=true]{border-color:var(--accent);color:var(--text)}.wizard-body h3{margin-top:0}.wizard-body button{margin-top:.75rem;background:var(--accent);color:#0b1016;border:none;border-radius:8px;padding:.5rem .9rem;font-weight:600;cursor:pointer}.wizard-body button:disabled{opacity:.5;cursor:not-allowed}.readiness[data-state=ready]{border-color:#5bd49a}.readiness[data-state=degraded]{border-color:var(--warn)}@media(max-width:800px){.console-header,.kv div{grid-template-columns:1fr}}