@aipper/aiws 0.0.24 → 0.0.25

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.
@@ -5,7 +5,8 @@ import { fileURLToPath } from "node:url";
5
5
  import { UserError } from "../errors.js";
6
6
  import { runCommand } from "../exec.js";
7
7
  import { pathExists } from "../fs.js";
8
- import { computeChangeStatus, validateChangeArtifacts } from "./change.js";
8
+ import { loadWorkflowStageContracts as loadWorkflowStageContractsSpec } from "../spec.js";
9
+ import { computeChangeNextAdvice, computeChangeStatus, validateChangeArtifacts } from "./change.js";
9
10
 
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = path.dirname(__filename);
@@ -82,6 +83,33 @@ async function listChangeIds(gitRoot) {
82
83
  }
83
84
  }
84
85
 
86
+ export async function loadWorkflowStageContracts() {
87
+ return loadWorkflowStageContractsSpec();
88
+ }
89
+
90
+ /**
91
+ * @param {string} gitRoot
92
+ */
93
+ export async function collectDashboardChanges(gitRoot) {
94
+ const ids = await listChangeIds(gitRoot);
95
+ const changes = [];
96
+ for (const id of ids) {
97
+ try {
98
+ const st = await computeChangeStatus(gitRoot, id);
99
+ const nextAdvice = await computeChangeNextAdvice(gitRoot, id);
100
+ changes.push({
101
+ ...st,
102
+ collaboration: st.collaboration?.counts || null,
103
+ nextAdvice,
104
+ governance: st.governance || null,
105
+ });
106
+ } catch (e) {
107
+ changes.push({ changeId: id, ok: false, error: e instanceof Error ? e.message : String(e) });
108
+ }
109
+ }
110
+ return changes;
111
+ }
112
+
85
113
  function parseUrl(reqUrl) {
86
114
  const u = new URL(reqUrl || "/", "http://127.0.0.1");
87
115
  return { pathname: u.pathname, searchParams: u.searchParams };
@@ -109,26 +137,24 @@ export async function dashboardServeCommand(options) {
109
137
  }
110
138
 
111
139
  if (pathname === "/api/changes") {
112
- const ids = await listChangeIds(gitRoot);
113
- const changes = [];
114
- for (const id of ids) {
115
- try {
116
- const st = await computeChangeStatus(gitRoot, id);
117
- changes.push(st);
118
- } catch (e) {
119
- changes.push({ changeId: id, ok: false, error: e instanceof Error ? e.message : String(e) });
120
- }
121
- }
140
+ const changes = await collectDashboardChanges(gitRoot);
122
141
  sendJson(res, 200, { ok: true, changes });
123
142
  return;
124
143
  }
125
144
 
145
+ if (pathname === "/api/workflow-stages") {
146
+ const stages = await loadWorkflowStageContracts();
147
+ sendJson(res, 200, stages);
148
+ return;
149
+ }
150
+
126
151
  const m = pathname.match(/^\/api\/change\/([^/]+)\/validate$/);
127
152
  if (m) {
128
153
  const changeId = decodeURIComponent(m[1] || "");
129
154
  const strict = searchParams.get("strict") === "1" || searchParams.get("strict") === "true";
130
155
  const allowTruthDrift = searchParams.get("allow_truth_drift") === "1" || searchParams.get("allow_truth_drift") === "true";
131
- const result = await validateChangeArtifacts(gitRoot, changeId, { strict, allowTruthDrift });
156
+ const checkScope = searchParams.get("check_scope") === "1" || searchParams.get("check_scope") === "true";
157
+ const result = await validateChangeArtifacts(gitRoot, changeId, { strict, allowTruthDrift, checkScope });
132
158
  sendJson(res, 200, result);
133
159
  return;
134
160
  }
@@ -0,0 +1,61 @@
1
+ import { resolveWorkspaceRoot } from "../workspace.js";
2
+ import { detectOpenCodeEnvironment } from "../opencode-env.js";
3
+
4
+ /**
5
+ * @param {{ targetPath: string }} options
6
+ */
7
+ export async function opencodeStatusCommand(options) {
8
+ const workspaceRoot = await resolveWorkspaceRoot(options.targetPath, { create: false });
9
+ const env = await detectOpenCodeEnvironment(workspaceRoot);
10
+
11
+ console.log(`✓ aiws opencode status: ${workspaceRoot}`);
12
+ console.log(`.opencode/: ${env.opencodeDirExists ? "ok" : "missing"}`);
13
+ console.log(`mode: ${env.mode}`);
14
+ console.log(
15
+ `oMo config: ${
16
+ env.configStatus === "loaded"
17
+ ? `${env.rel.configPath} (ok)`
18
+ : env.configStatus === "invalid"
19
+ ? `${env.rel.configPath} (invalid JSON: ${env.configError})`
20
+ : "missing"
21
+ }`,
22
+ );
23
+ console.log(`oMo example: ${env.exampleExists ? `${env.rel.examplePath} (present)` : "missing"}`);
24
+ console.log(`recommended_agents_ready: ${env.recommendedAgentsReady}`);
25
+ console.log(`enabled_agents: ${env.enabledAgents.length > 0 ? env.enabledAgents.join(", ") : "(none)"}`);
26
+
27
+ const planner = env.agents["planner-sisyphus"];
28
+ const librarian = env.agents.librarian;
29
+ const explore = env.agents.explore;
30
+ const oracle = env.agents.oracle;
31
+ console.log(
32
+ `aiws_roles: planner=${planner.enabled ? "planner-sisyphus" : "missing"}, explorer=${[
33
+ explore.enabled ? "explore" : "",
34
+ librarian.enabled ? "librarian" : "",
35
+ ]
36
+ .filter(Boolean)
37
+ .join("+") || "missing"}, reviewer=${oracle.enabled ? "oracle" : "missing"}, integrator=current-agent`,
38
+ );
39
+
40
+ if (planner.present) {
41
+ console.log(`planner-sisyphus.replace_plan: ${planner.replacePlan === null ? "(unset)" : String(planner.replacePlan)}`);
42
+ }
43
+
44
+ if (env.configStatus === "invalid") {
45
+ console.log(`Next: 修复 ${env.rel.configPath} 的 JSON,然后重新运行 aiws opencode status .`);
46
+ return;
47
+ }
48
+ if (!env.configExists && env.exampleExists) {
49
+ console.log(`Next: 如需启用 oMo 优先模式,可复制 ${env.rel.examplePath} -> ${env.rel.configPath} 后按项目调整 agents。`);
50
+ return;
51
+ }
52
+ if (env.mode === "oMo-enabled" && !env.recommendedAgentsReady) {
53
+ console.log("Next: 补齐 planner-sisyphus / librarian / explore / oracle,或接受 fallback 到 standard-opencode。");
54
+ return;
55
+ }
56
+ if (env.mode === "oMo-enabled") {
57
+ console.log("Next: 运行 /using-aiws 或 /ws-preflight,确认技能路由输出 OpenCode mode: oMo-enabled。");
58
+ return;
59
+ }
60
+ console.log("Next: 当前按 standard-opencode 运行;如需多 agent 优先委托,可启用 oh-my-opencode 项目配置。");
61
+ }
@@ -38,6 +38,14 @@ function tasksBadge(done, total, unchecked) {
38
38
  return `<span class="badge"><span class="dot ${cls}"></span>${label}</span>`;
39
39
  }
40
40
 
41
+ function summarizeNextAdvice(advice) {
42
+ const a = advice && typeof advice === "object" ? advice : {};
43
+ const actions = Array.isArray(a.actions) ? a.actions : [];
44
+ const recommended = Array.isArray(a.recommended) ? a.recommended : [];
45
+ const prohibitions = Array.isArray(a.prohibitions) ? a.prohibitions : [];
46
+ return actions[0] || recommended[0] || prohibitions[0] || "";
47
+ }
48
+
41
49
  function renderDetails(obj) {
42
50
  return JSON.stringify(obj, null, 2);
43
51
  }
@@ -48,6 +56,8 @@ function renderValidateGroups(groups) {
48
56
  ["missing_files", "Missing files"],
49
57
  ["placeholders", "Placeholders / WS:TODO"],
50
58
  ["bindings", "Bindings"],
59
+ ["scope_gate", "Scope gate"],
60
+ ["finish_gate", "Finish gate"],
51
61
  ["plan_quality", "Plan quality"],
52
62
  ["other", "Other"],
53
63
  ];
@@ -70,20 +80,148 @@ async function runStrictValidate(changeId) {
70
80
  return fetchJson(`/api/change/${encodeURIComponent(changeId)}/validate?strict=1`);
71
81
  }
72
82
 
83
+ function normalizeStageId(value) {
84
+ return String(value || "").trim();
85
+ }
86
+
87
+ function stageLabel(stageId) {
88
+ return normalizeStageId(stageId) || "(unknown)";
89
+ }
90
+
91
+ function renderContractField(label, value) {
92
+ return `<div class="contract-field">
93
+ <div class="contract-field-label">${escapeHtml(label)}</div>
94
+ <div>${escapeHtml(value || "(none)")}</div>
95
+ </div>`;
96
+ }
97
+
98
+ function renderContractCard(title, contract) {
99
+ if (!contract) return "";
100
+ return `<div class="contract-card">
101
+ <div class="contract-title">${escapeHtml(title)}</div>
102
+ <div class="contract-stage"><code>${escapeHtml(contract.stage || "")}</code></div>
103
+ ${renderContractField("目标", contract.goal)}
104
+ ${renderContractField("必需输入", contract.requiredInputs)}
105
+ ${renderContractField("必需输出", contract.requiredOutputs)}
106
+ ${renderContractField("阻断条件", contract.blockers)}
107
+ ${renderContractField("下一步", contract.next)}
108
+ </div>`;
109
+ }
110
+
111
+ function normalizeGateStatus(value) {
112
+ return String(value || "missing").trim() || "missing";
113
+ }
114
+
115
+ function reviewGateText(reviewGates) {
116
+ const gates = reviewGates && typeof reviewGates === "object" ? reviewGates : {};
117
+ const spec = normalizeGateStatus(gates?.specReview?.status);
118
+ const quality = normalizeGateStatus(gates?.qualityReview?.status);
119
+ const validate = normalizeGateStatus(gates?.validateStamp?.status);
120
+ const preComplete = normalizeGateStatus(gates?.verifyBeforeComplete?.status);
121
+ return `review-gates: spec=${spec}, quality=${quality}, validate=${validate}, pre-complete=${preComplete}`;
122
+ }
123
+
124
+ function renderReviewGateBadges(reviewGates) {
125
+ const gates = reviewGates && typeof reviewGates === "object" ? reviewGates : {};
126
+ const items = [
127
+ ["spec", gates?.specReview],
128
+ ["quality", gates?.qualityReview],
129
+ ["validate", gates?.validateStamp],
130
+ ["pre-complete", gates?.verifyBeforeComplete],
131
+ ];
132
+ return `<div class="governance-head">${items
133
+ .map(([label, gate]) => {
134
+ const ready = gate?.ready === true;
135
+ const status = normalizeGateStatus(gate?.status);
136
+ const cls = ready ? "good" : "warn";
137
+ return `<span class="badge"><span class="dot ${cls}"></span>${escapeHtml(`${label}=${status}`)}</span>`;
138
+ })
139
+ .join("")}</div>`;
140
+ }
141
+
142
+ function scopeGateText(scopeGate) {
143
+ const gate = scopeGate && typeof scopeGate === "object" ? scopeGate : {};
144
+ const status = String(gate.status || "not_run").trim() || "not_run";
145
+ const details = [];
146
+ if (typeof gate.errors === "number") details.push(`errors=${gate.errors}`);
147
+ if (typeof gate.warnings === "number") details.push(`warnings=${gate.warnings}`);
148
+ if (gate.lastRunAt) details.push(`at=${gate.lastRunAt}`);
149
+ return `scope-gate: ${status}${details.length > 0 ? ` (${details.join(", ")})` : ""}`;
150
+ }
151
+
152
+ function renderScopeGateBadge(scopeGate) {
153
+ const gate = scopeGate && typeof scopeGate === "object" ? scopeGate : {};
154
+ const status = String(gate.status || "not_run").trim() || "not_run";
155
+ const ready = gate.ready === true;
156
+ const cls = ready ? "good" : status === "failed" ? "bad" : "warn";
157
+ return `<span class="badge"><span class="dot ${cls}"></span>${escapeHtml(`scope=${status}`)}</span>`;
158
+ }
159
+
160
+ function renderGovernanceSummary(selected, stageData) {
161
+ const governance = selected?.governance && typeof selected.governance === "object" ? selected.governance : {};
162
+ const currentStage = normalizeStageId(governance.currentStage);
163
+ const recommendedStage = normalizeStageId(governance.recommendedStage);
164
+ const phase = normalizeStageId(governance.phase || selected?.phase);
165
+ const contracts = Array.isArray(stageData?.stages) ? stageData.stages : [];
166
+ const byStage = new Map(contracts.map((contract) => [normalizeStageId(contract.stage), contract]));
167
+ const currentContract = byStage.get(currentStage) || null;
168
+ const recommendedContract = byStage.get(recommendedStage) || null;
169
+
170
+ const rail = contracts.length > 0
171
+ ? `<div class="governance-rail">${contracts
172
+ .map((contract, index) => {
173
+ const stage = normalizeStageId(contract.stage);
174
+ const cls = [
175
+ "stage-chip",
176
+ stage === currentStage ? "active" : "",
177
+ stage === recommendedStage && stage !== currentStage ? "recommended" : "",
178
+ ]
179
+ .filter(Boolean)
180
+ .join(" ");
181
+ const arrow = index < contracts.length - 1 ? `<span class="rail-arrow">&rarr;</span>` : "";
182
+ return `<span class="${cls}"><code>${escapeHtml(stageLabel(stage))}</code></span>${arrow}`;
183
+ })
184
+ .join("")}</div>`
185
+ : `<div class="muted">(workflow-stage-contracts unavailable)</div>`;
186
+
187
+ const cards = [];
188
+ if (currentContract) cards.push(renderContractCard(`Current Contract (${phase || "unknown"})`, currentContract));
189
+ if (recommendedContract && recommendedStage && recommendedStage !== currentStage) {
190
+ cards.push(renderContractCard("Next Contract", recommendedContract));
191
+ }
192
+
193
+ return `<div class="governance-head">
194
+ <span class="badge"><span class="dot good"></span>phase=${escapeHtml(phase || "(unknown)")}</span>
195
+ <span class="badge"><span class="dot warn"></span>current=${escapeHtml(stageLabel(currentStage))}</span>
196
+ <span class="badge"><span class="dot good"></span>next=${escapeHtml(stageLabel(recommendedStage || currentStage))}</span>
197
+ </div>
198
+ ${governance.rationale ? `<div class="muted">${escapeHtml(governance.rationale)}</div>` : ""}
199
+ ${governance.warning ? `<div class="muted">warning: ${escapeHtml(governance.warning)}</div>` : ""}
200
+ ${selected?.reviewGates ? renderReviewGateBadges(selected.reviewGates) : ""}
201
+ ${selected?.scopeGate ? `<div class="governance-head">${renderScopeGateBadge(selected.scopeGate)}</div>` : ""}
202
+ ${rail}
203
+ <div class="governance-grid">
204
+ ${cards.join("") || `<div class="muted">No contract summary available.</div>`}
205
+ </div>`;
206
+ }
207
+
73
208
  async function refresh() {
74
209
  const summaryEl = document.getElementById("summary");
75
210
  const bodyEl = document.getElementById("changes-body");
76
211
  const detailsTitleEl = document.getElementById("details-title");
77
212
  const detailsEl = document.getElementById("details");
213
+ const governanceEl = document.getElementById("governance-summary");
78
214
 
79
215
  summaryEl.textContent = "Loading…";
80
216
  bodyEl.innerHTML = "";
217
+ if (governanceEl) governanceEl.innerHTML = `<div class="muted">Loading workflow governance…</div>`;
81
218
 
82
- const data = await fetchJson("/api/changes");
219
+ const [data, stageData] = await Promise.all([fetchJson("/api/changes"), fetchJson("/api/workflow-stages")]);
83
220
  if (!data || data.ok === false) {
84
221
  summaryEl.textContent = "Failed";
85
222
  detailsTitleEl.textContent = "Error";
86
223
  detailsEl.textContent = renderDetails(data);
224
+ if (governanceEl) governanceEl.innerHTML = `<div class="muted">Failed to load governance summary.</div>`;
87
225
  return;
88
226
  }
89
227
 
@@ -99,8 +237,24 @@ async function refresh() {
99
237
  const bindings = c.bindings || {};
100
238
  const planFile = bindings.planFile || "";
101
239
  const evidencePaths = Array.isArray(bindings.evidencePaths) ? bindings.evidencePaths : [];
240
+ const collaboration = c.collaboration && typeof c.collaboration === "object" ? c.collaboration : {};
241
+ const nextHint = summarizeNextAdvice(c.nextAdvice);
242
+ const governance = c.governance && typeof c.governance === "object" ? c.governance : {};
243
+ const reviewGateLine = reviewGateText(c.reviewGates);
244
+ const scopeGateLine = scopeGateText(c.scopeGate);
245
+ const collabLabel = `analysis=${collaboration.analysis || 0}, patches=${collaboration.patches || 0}, review=${collaboration.review || 0}`;
246
+ const stageLine = `phase=${governance.phase || c.phase || "unknown"}, current=${governance.currentStage || "unknown"}, next=${governance.recommendedStage || governance.currentStage || "unknown"}`;
247
+ const rationaleLine = governance.rationale ? `<div class="muted">${escapeHtml(governance.rationale)}</div>` : "";
248
+ const warningLine = governance.warning ? `<div class="muted">warning: ${escapeHtml(governance.warning)}</div>` : "";
102
249
  const pe = `${planFile ? `<div><code>${escapeHtml(planFile)}</code></div>` : `<div class="muted">(no Plan_File)</div>`}
103
- <div class="muted">${evidencePaths.length} evidence path(s)</div>`;
250
+ <div class="muted">${evidencePaths.length} evidence path(s)</div>
251
+ <div class="muted">${escapeHtml(collabLabel)}</div>
252
+ <div class="muted">${escapeHtml(stageLine)}</div>
253
+ <div class="muted">${escapeHtml(reviewGateLine)}</div>
254
+ <div class="muted">${escapeHtml(scopeGateLine)}</div>
255
+ ${rationaleLine}
256
+ ${warningLine}
257
+ ${nextHint ? `<div class="muted">${escapeHtml(nextHint)}</div>` : ""}`;
104
258
  return `<tr data-change="${escapeHtml(changeId)}">
105
259
  <td><a class="link" href="#" data-open="${escapeHtml(changeId)}">${escapeHtml(changeId)}</a></td>
106
260
  <td>${tasksBadge(tasks.done || 0, tasks.total || 0, tasks.unchecked || 0)}</td>
@@ -122,9 +276,12 @@ async function refresh() {
122
276
  detailsTitleEl.textContent = changeId ? `change: ${changeId}` : "Details";
123
277
  if (!selected) {
124
278
  detailsEl.textContent = renderDetails(selected);
279
+ if (governanceEl) governanceEl.innerHTML = `<div class="muted">No governance summary available.</div>`;
125
280
  return;
126
281
  }
282
+ if (governanceEl) governanceEl.innerHTML = renderGovernanceSummary(selected, stageData);
127
283
  const b = selected.bindings || {};
284
+ const governance = selected.governance && typeof selected.governance === "object" ? selected.governance : {};
128
285
  const evid = Array.isArray(b.evidencePaths) ? b.evidencePaths : [];
129
286
  const lines = [];
130
287
  lines.push(`changeId: ${selected.changeId || ""}`);
@@ -136,6 +293,52 @@ async function refresh() {
136
293
  lines.push("Evidence_Path:");
137
294
  for (const p of evid) lines.push(`- ${p}`);
138
295
  }
296
+ if (selected.collaboration && typeof selected.collaboration === "object") {
297
+ lines.push("Collaboration:");
298
+ lines.push(`- analysis: ${selected.collaboration.analysis || 0}`);
299
+ lines.push(`- patches: ${selected.collaboration.patches || 0}`);
300
+ lines.push(`- review: ${selected.collaboration.review || 0}`);
301
+ lines.push(`- evidence: ${selected.collaboration.evidence || 0}`);
302
+ }
303
+ if (selected.reviewGates && typeof selected.reviewGates === "object") {
304
+ lines.push("");
305
+ lines.push("Review gates:");
306
+ lines.push(`- ${reviewGateText(selected.reviewGates)}`);
307
+ for (const [label, gate] of [
308
+ ["ws-spec-review", selected.reviewGates.specReview],
309
+ ["ws-quality-review", selected.reviewGates.qualityReview],
310
+ ["validate-stamp", selected.reviewGates.validateStamp],
311
+ ["ws-verify-before-complete", selected.reviewGates.verifyBeforeComplete],
312
+ ]) {
313
+ if (!gate || typeof gate !== "object") continue;
314
+ const foundPath = gate.foundPath ? ` path=${gate.foundPath}` : "";
315
+ lines.push(`- ${label}: ready=${gate.ready === true} status=${normalizeGateStatus(gate.status)}${foundPath}`);
316
+ }
317
+ }
318
+ if (selected.scopeGate && typeof selected.scopeGate === "object") {
319
+ lines.push("");
320
+ lines.push("Scope gate:");
321
+ lines.push(`- ${scopeGateText(selected.scopeGate)}`);
322
+ }
323
+ if (selected.nextAdvice && typeof selected.nextAdvice === "object") {
324
+ const actions = Array.isArray(selected.nextAdvice.actions) ? selected.nextAdvice.actions : [];
325
+ const recommended = Array.isArray(selected.nextAdvice.recommended) ? selected.nextAdvice.recommended : [];
326
+ const prohibitions = Array.isArray(selected.nextAdvice.prohibitions) ? selected.nextAdvice.prohibitions : [];
327
+ if (actions.length > 0 || recommended.length > 0 || prohibitions.length > 0) {
328
+ lines.push("");
329
+ lines.push("Next:");
330
+ for (const x of actions) lines.push(`- action: ${x}`);
331
+ for (const x of recommended) lines.push(`- recommended: ${x}`);
332
+ for (const x of prohibitions) lines.push(`- do-not: ${x}`);
333
+ }
334
+ }
335
+ if (governance.rationale) {
336
+ lines.push("");
337
+ lines.push(`Governance rationale: ${governance.rationale}`);
338
+ }
339
+ if (governance.warning) {
340
+ lines.push(`Governance warning: ${governance.warning}`);
341
+ }
139
342
  if (Array.isArray(selected.blockersStrict) && selected.blockersStrict.length > 0) {
140
343
  lines.push("");
141
344
  lines.push("Blockers (strict):");
@@ -34,7 +34,7 @@
34
34
  <th>Tasks</th>
35
35
  <th>Truth drift</th>
36
36
  <th>Blockers (strict)</th>
37
- <th>Plan & Evidence</th>
37
+ <th>Plan / Evidence / Next</th>
38
38
  <th>Actions</th>
39
39
  </tr>
40
40
  </thead>
@@ -42,7 +42,7 @@
42
42
  </table>
43
43
  </div>
44
44
  <div class="hint">
45
- 建议:先运行执行前质量门 <code>aiws change validate &lt;id&gt; --strict</code>(等价 <code>$ws-plan-verify</code>),通过后再进入 <code>$ws-dev</code>。
45
+ 建议:先看 Details 里的 <code>Next</code>,优先通过 <code>aiws change validate &lt;id&gt; --strict</code>,再按阶段进入 <code>$ws-dev</code> <code>$ws-deliver</code>。
46
46
  </div>
47
47
  </section>
48
48
 
@@ -51,6 +51,9 @@
51
51
  <div class="panel-title">Details</div>
52
52
  <div class="panel-meta" id="details-title">Select a change</div>
53
53
  </div>
54
+ <div class="governance" id="governance-summary">
55
+ <div class="muted">Select a change to inspect workflow stage contracts.</div>
56
+ </div>
54
57
  <pre class="log" id="details"></pre>
55
58
  </section>
56
59
  </main>
@@ -205,6 +205,92 @@ code {
205
205
  font-size: 12px;
206
206
  }
207
207
 
208
+ .governance {
209
+ padding: 14px;
210
+ border-bottom: 1px solid var(--border);
211
+ background: rgba(255, 255, 255, 0.02);
212
+ }
213
+
214
+ .governance-head {
215
+ display: flex;
216
+ flex-wrap: wrap;
217
+ gap: 8px;
218
+ margin-bottom: 12px;
219
+ }
220
+
221
+ .governance-rail {
222
+ display: flex;
223
+ flex-wrap: wrap;
224
+ gap: 6px;
225
+ align-items: center;
226
+ margin-bottom: 12px;
227
+ }
228
+
229
+ .rail-arrow {
230
+ color: var(--muted);
231
+ font-size: 12px;
232
+ }
233
+
234
+ .stage-chip {
235
+ display: inline-flex;
236
+ align-items: center;
237
+ gap: 6px;
238
+ padding: 5px 9px;
239
+ border-radius: 999px;
240
+ border: 1px solid var(--border);
241
+ font-size: 12px;
242
+ color: var(--muted);
243
+ background: rgba(255, 255, 255, 0.03);
244
+ }
245
+
246
+ .stage-chip.active {
247
+ color: var(--text);
248
+ border-color: rgba(61, 220, 151, 0.55);
249
+ background: rgba(61, 220, 151, 0.14);
250
+ }
251
+
252
+ .stage-chip.recommended {
253
+ color: var(--text);
254
+ border-color: rgba(138, 180, 255, 0.55);
255
+ background: rgba(138, 180, 255, 0.14);
256
+ }
257
+
258
+ .governance-grid {
259
+ display: grid;
260
+ grid-template-columns: 1fr;
261
+ gap: 10px;
262
+ }
263
+
264
+ .contract-card {
265
+ border: 1px solid var(--border);
266
+ border-radius: 12px;
267
+ background: rgba(255, 255, 255, 0.04);
268
+ padding: 12px;
269
+ }
270
+
271
+ .contract-title {
272
+ font-weight: 700;
273
+ margin-bottom: 8px;
274
+ }
275
+
276
+ .contract-stage {
277
+ color: var(--link);
278
+ font-size: 12px;
279
+ margin-bottom: 8px;
280
+ }
281
+
282
+ .contract-field {
283
+ margin-top: 8px;
284
+ font-size: 12px;
285
+ line-height: 1.45;
286
+ }
287
+
288
+ .contract-field-label {
289
+ color: rgba(255, 255, 255, 0.78);
290
+ font-weight: 600;
291
+ margin-bottom: 3px;
292
+ }
293
+
208
294
  .log {
209
295
  margin: 0;
210
296
  padding: 12px 14px;