create-principles-disciple 1.145.11 → 1.145.12

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 (65) hide show
  1. package/_release/product-identity.json +1 -1
  2. package/codex-adapter/dist/codec/input-decoder.js +11 -3
  3. package/codex-adapter/package.json +1 -1
  4. package/console/dist/server/models/ActivationsConsoleModel.js +1 -1
  5. package/console/dist/server/models/ApprovalsConsoleModel.d.ts +1 -1
  6. package/console/dist/server/models/ApprovalsConsoleModel.js +8 -1
  7. package/console/dist/server/models/ApprovalsGroupedConsoleModel.d.ts +13 -1
  8. package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +51 -16
  9. package/console/dist/shared/prompt-injection-contract.d.ts +30 -2
  10. package/console/dist/ui/pages/activation/ActivationPage.js +17 -4
  11. package/console/dist/ui/pages/focus/FocusPage.js +36 -12
  12. package/console/dist/ui/pages/focus/focus-validation.js +12 -0
  13. package/console/dist/ui/utils/validators.d.ts +6 -0
  14. package/console/dist/ui/utils/validators.js +33 -3
  15. package/console/dist/web/assets/app.js +20 -20
  16. package/core/dist/host/host-adapter.d.ts +27 -0
  17. package/core/dist/host/host-adapter.d.ts.map +1 -1
  18. package/core/dist/host/host-adapter.js.map +1 -1
  19. package/core/dist/host/index.d.ts +1 -1
  20. package/core/dist/host/index.d.ts.map +1 -1
  21. package/core/dist/host/index.js.map +1 -1
  22. package/core/dist/runtime-v2/activation/index.d.ts +2 -0
  23. package/core/dist/runtime-v2/activation/index.d.ts.map +1 -1
  24. package/core/dist/runtime-v2/activation/index.js +2 -0
  25. package/core/dist/runtime-v2/activation/index.js.map +1 -1
  26. package/core/dist/runtime-v2/activation/prompt-budget-policy.d.ts +106 -0
  27. package/core/dist/runtime-v2/activation/prompt-budget-policy.d.ts.map +1 -0
  28. package/core/dist/runtime-v2/activation/prompt-budget-policy.js +229 -0
  29. package/core/dist/runtime-v2/activation/prompt-budget-policy.js.map +1 -0
  30. package/core/dist/runtime-v2/adapter/pi-ai-catalog.d.ts +2 -0
  31. package/core/dist/runtime-v2/adapter/pi-ai-catalog.d.ts.map +1 -1
  32. package/core/dist/runtime-v2/adapter/pi-ai-catalog.js +50 -1
  33. package/core/dist/runtime-v2/adapter/pi-ai-catalog.js.map +1 -1
  34. package/core/dist/runtime-v2/index.d.ts +3 -3
  35. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  36. package/core/dist/runtime-v2/index.js +2 -2
  37. package/core/dist/runtime-v2/index.js.map +1 -1
  38. package/core/dist/runtime-v2/types/event-types.d.ts +24 -0
  39. package/core/dist/runtime-v2/types/event-types.d.ts.map +1 -1
  40. package/core/dist/runtime-v2/types/event-types.js +22 -0
  41. package/core/dist/runtime-v2/types/event-types.js.map +1 -1
  42. package/core/package.json +1 -1
  43. package/host-runtime/dist/active-principle-prompt.d.ts +23 -1
  44. package/host-runtime/dist/active-principle-prompt.js +21 -5
  45. package/host-runtime/dist/index.js +33 -2
  46. package/host-runtime/dist/prompt-activation-deliverability.d.ts +36 -3
  47. package/host-runtime/dist/prompt-activation-deliverability.js +44 -26
  48. package/host-runtime/dist/prompt-injection-projection.d.ts +23 -1
  49. package/host-runtime/dist/prompt-injection-projection.js +25 -6
  50. package/host-runtime/dist/prompt-injection-status.d.ts +20 -5
  51. package/host-runtime/dist/prompt-injection-status.js +30 -7
  52. package/host-runtime/package.json +1 -1
  53. package/package.json +2 -2
  54. package/pd-cli/dist/commands/runtime-activation.d.ts.map +1 -1
  55. package/pd-cli/dist/commands/runtime-activation.js +18 -3
  56. package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
  57. package/plugin/dist/bundle.js +426 -426
  58. package/plugin/dist/governance-audit.js +2 -2
  59. package/plugin/dist/openclaw-sdk.d.ts +9 -1
  60. package/plugin/dist/openclaw.plugin.json +1 -1
  61. package/plugin/openclaw.plugin.json +1 -1
  62. package/plugin/package.json +1 -1
  63. package/release-manager/package.json +2 -2
  64. package/workbuddy-adapter/dist/codec/input-decoder.js +16 -4
  65. package/workbuddy-adapter/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "productVersion": "2.3.8",
4
- "sourceCommit": "ec66d23385b88a7f36442702ea862432435aa470"
4
+ "sourceCommit": "0062c218a3ccffcfc3c1b79783bb147dff353648"
5
5
  }
@@ -59,10 +59,15 @@ function validateCommon(raw, needsTurn) {
59
59
  const sessionId = requiredString(raw, 'session_id');
60
60
  requiredNullableString(raw, 'transcript_path');
61
61
  const workspaceDir = requiredString(raw, 'cwd');
62
- requiredString(raw, 'model');
62
+ // SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: keep the VALIDATED model identity
63
+ // (was: checked then discarded). It is an identity string only — never a
64
+ // capacity; the budget resolver turns it into a budget via the installed
65
+ // catalog, falling back to the fixed budget on unknown/ambiguous ids. It
66
+ // must not block the hook or leak into tool allow/deny decisions.
67
+ const modelId = requiredString(raw, 'model');
63
68
  requiredString(raw, 'permission_mode');
64
69
  const turnId = needsTurn ? requiredString(raw, 'turn_id') : undefined;
65
- return { workspaceDir, sessionId, ...(turnId ? { turnId } : {}) };
70
+ return { base: { workspaceDir, sessionId, ...(turnId ? { turnId } : {}) }, modelId };
66
71
  }
67
72
  export function decodeCodexInput(raw) {
68
73
  if (!isRecord(raw))
@@ -71,7 +76,10 @@ export function decodeCodexInput(raw) {
71
76
  const kind = EVENT_KINDS.get(eventName);
72
77
  if (!kind)
73
78
  throw new CodexDecoderError(`unknown hook_event_name "${eventName}"`, 'Configure only PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, or Stop.');
74
- const common = validateCommon(raw, kind !== 'session_start');
79
+ const { base, modelId } = validateCommon(raw, kind !== 'session_start');
80
+ // Model identity rides every event context under modelContext (budget
81
+ // resolution happens on the prompt path; other paths carry it harmlessly).
82
+ const common = { ...base, modelContext: { modelId } };
75
83
  let context;
76
84
  let rawPayload = raw;
77
85
  if (kind === 'before_tool_call' || kind === 'after_tool_call') {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/codex-adapter",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
4
4
  "description": "Codex CLI host adapter for Principles Disciple — implements HostAdapter interface for OpenAI Codex CLI's stdin/stdout JSON hook model (ADR-0020).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -343,7 +343,7 @@ export class ActivationsConsoleModel {
343
343
  ...(entry.route !== undefined ? { route: entry.route } : {}),
344
344
  ...(entry.reason !== undefined ? { reason: entry.reason } : {}),
345
345
  ...(entry.perHostFits !== undefined
346
- ? { perHostFits: entry.perHostFits.map((fit) => ({ hostKind: fit.hostKind, fits: fit.fits })) }
346
+ ? { perHostFits: entry.perHostFits.map((fit) => ({ hostKind: fit.hostKind, tierFit: fit.tierFit })) }
347
347
  : {}),
348
348
  },
349
349
  ]));
@@ -84,7 +84,7 @@ export type ApproveWithActivationResult = {
84
84
  overByChars?: number;
85
85
  perHost?: {
86
86
  hostKind: 'openclaw' | 'codex' | 'workbuddy';
87
- fits: boolean;
87
+ tierFit: 'fits_all_tiers' | 'model_dependent' | 'exceeds_max_tier';
88
88
  costChars: number;
89
89
  }[];
90
90
  };
@@ -252,6 +252,13 @@ export class ApprovalsConsoleModel {
252
252
  }
253
253
  if (deliverability.status === 'deliverable')
254
254
  return undefined;
255
+ // SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.3: model_dependent is NOT a
256
+ // refusal — the entry fits within the supported budget range and whether
257
+ // it injects depends on the runtime model. The approval proceeds; the
258
+ // honest uncertainty is already visible on the pre-approval badge
259
+ // (promptBudgetTierFit) and the activation status surface.
260
+ if (deliverability.status === 'model_dependent')
261
+ return undefined;
255
262
  if (deliverability.status === 'undeliverable') {
256
263
  return {
257
264
  ok: false,
@@ -276,7 +283,7 @@ export class ApprovalsConsoleModel {
276
283
  capacity: {
277
284
  category: deliverability.reason.startsWith('route_unconfirmed') ? 'route_unconfirmed' : 'content_unconfirmed',
278
285
  ...(deliverability.perHost !== undefined
279
- ? { perHost: deliverability.perHost.map((entry) => ({ hostKind: entry.hostKind, fits: entry.fits, costChars: entry.costChars })) }
286
+ ? { perHost: deliverability.perHost.map((entry) => ({ hostKind: entry.hostKind, tierFit: entry.tierFit, costChars: entry.costChars })) }
280
287
  : {}),
281
288
  },
282
289
  };
@@ -11,11 +11,23 @@ export interface ApprovalGroup {
11
11
  /**
12
12
  * PR-1894: whether this candidate's own serialized prompt entry fits the
13
13
  * injection budget on its own. `false` means the entry is oversized, so fair
14
- * rotation can NEVER inject it �?the pre-approval badge must not promise it
14
+ * rotation can NEVER inject it — the pre-approval badge must not promise it
15
15
  * will come around. Computed server-side with the production serializer
16
16
  * (the client has no access to the escaped, serialized size).
17
+ * SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: `true` now means "fits within the
18
+ * supported budget range" — see `promptBudgetTierFit` for whether that is
19
+ * unconditional (fits every tier) or model-dependent.
17
20
  */
18
21
  fitsPromptBudget?: boolean;
22
+ /**
23
+ * SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.3: honest tier classification of
24
+ * the candidate's own serialized entry against the supported
25
+ * [min, max] budget range. `model_dependent` = fits only on
26
+ * larger-context models — the badge must qualify rotation promises, not
27
+ * promise unconditionally. `unconfirmed` mirrors the precheck's honest
28
+ * unknown.
29
+ */
30
+ promptBudgetTierFit?: 'fits_all_tiers' | 'model_dependent' | 'exceeds_max_tier' | 'unconfirmed';
19
31
  revisionReviewUnavailable?: boolean;
20
32
  revisionReview?: {
21
33
  oldStatement: string;
@@ -104,19 +104,21 @@ function extractCandidateDescription(contentJson) {
104
104
  return null;
105
105
  }
106
106
  /**
107
- * PR-1894 → PD_PROMPT_CAPACITY_V1 R-A2: would this candidate's own prompt
108
- * entry fit the injection budget if it were the ONLY entry?
107
+ * PR-1894 → PD_PROMPT_CAPACITY_V1 R-A2 → SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001:
108
+ * classify this candidate's own prompt entry against the SUPPORTED budget
109
+ * range (not one fixed number — approval time cannot know the executing
110
+ * model).
109
111
  *
110
- * The judgment now comes from the ONE shared host-runtime precheck
112
+ * The judgment comes from the ONE shared host-runtime precheck
111
113
  * (`checkPromptArtifactDeliverability`): the REAL resolved principle id (not a
112
114
  * fixed 36-char UUID allowance — a title-derived id can only be longer), the
113
115
  * effective route's own serializer (list entry vs full directive render incl.
114
116
  * the self-report footer), and an honest `unconfirmed` verdict when the route
115
- * or the artifact content cannot be confirmed. `false` is also the
116
- * conservative answer for unconfirmed candidates — an unknown size must
117
+ * or the artifact content cannot be confirmed. `unconfirmed` is also the
118
+ * conservative answer when the precheck itself fails — an unknown size must
117
119
  * never become a rotation promise (rc-9).
118
120
  */
119
- async function candidateFitsPromptBudget(workspaceDir, artifact, targetHost) {
121
+ async function candidatePromptBudgetTierFit(workspaceDir, artifact, targetHost) {
120
122
  const deliverability = await checkPromptArtifactDeliverability({
121
123
  workspaceDir,
122
124
  targetHost,
@@ -130,15 +132,31 @@ async function candidateFitsPromptBudget(workspaceDir, artifact, targetHost) {
130
132
  : null,
131
133
  });
132
134
  if (deliverability.status === 'deliverable')
133
- return true;
135
+ return 'fits_all_tiers';
136
+ if (deliverability.status === 'model_dependent')
137
+ return 'model_dependent';
134
138
  if (deliverability.status === 'undeliverable')
135
- return false;
136
- // Unconfirmed: with per-host facts, only promise when EVERY applicable host
137
- // can deliver; without them, do not promise at all.
139
+ return 'exceeds_max_tier';
140
+ // Route/artifact unconfirmed: aggregate the per-host tier verdicts
141
+ // MONOTONICALLY. The list route bills ~700 fewer chars than the shared
142
+ // route for the same entry (fixed wrapper delta), so "shared exceeds but
143
+ // list does not" is REACHABLE (e.g. a ~15300-char statement: list ≈15341
144
+ // = model_dependent, shared ≈16058 = exceeds). Collapsing that to
145
+ // exceeds_max_tier would tell an OpenClaw owner their entry can never
146
+ // inject when a large-window list-route host could carry it. Only when
147
+ // EVERY applicable host exceeds is "oversized everywhere" true; any
148
+ // disagreement leaves the honest model_dependent. Without per-host
149
+ // facts, do not promise at all.
138
150
  if (deliverability.perHost && deliverability.perHost.length > 0) {
139
- return deliverability.perHost.every((entry) => entry.fits);
151
+ if (deliverability.perHost.every((entry) => entry.tierFit === 'exceeds_max_tier')) {
152
+ return 'exceeds_max_tier';
153
+ }
154
+ if (deliverability.perHost.every((entry) => entry.tierFit === 'fits_all_tiers')) {
155
+ return 'fits_all_tiers';
156
+ }
157
+ return 'model_dependent';
140
158
  }
141
- return false;
159
+ return 'unconfirmed';
142
160
  }
143
161
  export class ApprovalsGroupedConsoleModel {
144
162
  workspaceDir;
@@ -231,14 +249,14 @@ export class ApprovalsGroupedConsoleModel {
231
249
  if (!detection.ok || (detection.target !== null && !revisionReviews.has(artifact.artifactId)))
232
250
  revisionReviewUnavailable.add(artifact.artifactId);
233
251
  }
234
- artifactFitsBudgetMap.set(approval.artifactId, await candidateFitsPromptBudget(this.workspaceDir, artifact, targetHost));
252
+ artifactFitsBudgetMap.set(approval.artifactId, await candidatePromptBudgetTierFit(this.workspaceDir, artifact, targetHost));
235
253
  artifactUnavailableMap.set(approval.artifactId, artifact === null);
236
254
  }
237
255
  catch (err) {
238
256
  if (isMissingTableError(err)) {
239
257
  artifactPrincipleMap.set(approval.artifactId, null);
240
258
  artifactDescriptionMap.set(approval.artifactId, null);
241
- artifactFitsBudgetMap.set(approval.artifactId, false);
259
+ artifactFitsBudgetMap.set(approval.artifactId, 'unconfirmed');
242
260
  artifactUnavailableMap.set(approval.artifactId, true);
243
261
  }
244
262
  else {
@@ -302,10 +320,18 @@ export class ApprovalsGroupedConsoleModel {
302
320
  principleTitle,
303
321
  candidateDescription,
304
322
  status,
305
- // PR-1894: undefined-safe �?absent when the artifact could not be
323
+ // PR-1894: undefined-safe — absent when the artifact could not be
306
324
  // read, which the UI treats as "do not promise rotation".
325
+ // SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: the boolean stays the
326
+ // rotation-promise signal (true = within the supported budget
327
+ // range); the tier fit carries the honest fits-everywhere vs
328
+ // model-dependent distinction.
307
329
  ...(firstArtifactId !== undefined && artifactFitsBudgetMap.has(firstArtifactId)
308
- ? { fitsPromptBudget: artifactFitsBudgetMap.get(firstArtifactId) === true }
330
+ ? {
331
+ fitsPromptBudget: artifactFitsBudgetMap.get(firstArtifactId) === 'fits_all_tiers'
332
+ || artifactFitsBudgetMap.get(firstArtifactId) === 'model_dependent',
333
+ promptBudgetTierFit: artifactFitsBudgetMap.get(firstArtifactId),
334
+ }
309
335
  : {}),
310
336
  // PRI-940: absent when the pinned artifact is readable — same
311
337
  // spread style as fitsPromptBudget so the wire contract stays
@@ -370,6 +396,7 @@ export class ApprovalsGroupedConsoleModel {
370
396
  usedChars: entry.projection.usedChars,
371
397
  truncated: entry.projection.truncated,
372
398
  })),
399
+ ...(reference?.budgetFallbackReason !== undefined ? { budgetFallbackReason: reference.budgetFallbackReason } : {}),
373
400
  },
374
401
  };
375
402
  }
@@ -391,6 +418,14 @@ export class ApprovalsGroupedConsoleModel {
391
418
  ...(projection.fullRenderChars !== undefined ? { fullRenderChars: projection.fullRenderChars } : {}),
392
419
  oversizedActivationIds: projection.oversizedActivationIds,
393
420
  oversizedDiagnosticTruncated: projection.oversizedDiagnosticTruncated,
421
+ // SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §7: budget provenance — the
422
+ // console holds no runtime model facts, so this is the fixed-budget
423
+ // view by construction; the UI must label it as such rather than
424
+ // presenting it as a model-accurate forecast.
425
+ ...(projection.budgetCapacitySource !== undefined ? { budgetCapacitySource: projection.budgetCapacitySource } : {}),
426
+ ...(projection.budgetCapacityStatus !== undefined ? { budgetCapacityStatus: projection.budgetCapacityStatus } : {}),
427
+ ...(projection.budgetContextWindowTokens != null ? { budgetContextWindowTokens: projection.budgetContextWindowTokens } : {}),
428
+ ...(projection.budgetFallbackReason !== undefined ? { budgetFallbackReason: projection.budgetFallbackReason } : {}),
394
429
  },
395
430
  };
396
431
  }
@@ -29,7 +29,14 @@ export interface PromptInjectionPerHostForecast {
29
29
  */
30
30
  export interface PromptActivationInjectionStatusWire {
31
31
  activationId: string;
32
- status: 'injectable' | 'oversized' | 'window_excluded' | 'resolution_failed' | 'route_unconfirmed';
32
+ /**
33
+ * SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: `model_dependent` = the row's own
34
+ * entry misses the CURRENT projection budget but fits within the supported
35
+ * [min, max] budget range — injectability depends on the runtime model's
36
+ * context window, which this surface cannot see (never displayed as a
37
+ * definite yes or a permanent no).
38
+ */
39
+ status: 'injectable' | 'oversized' | 'model_dependent' | 'window_excluded' | 'resolution_failed' | 'route_unconfirmed';
33
40
  inCurrentWindow?: boolean;
34
41
  costChars?: number;
35
42
  budget?: number;
@@ -38,9 +45,11 @@ export interface PromptActivationInjectionStatusWire {
38
45
  reason?: string;
39
46
  perHostFits?: {
40
47
  hostKind: PromptInjectionTargetHostName;
41
- fits: boolean;
48
+ tierFit: PromptBudgetTierFitName;
42
49
  }[];
43
50
  }
51
+ /** Tier classification of a single serialized entry vs the supported budget range. */
52
+ export type PromptBudgetTierFitName = 'fits_all_tiers' | 'model_dependent' | 'exceeds_max_tier';
44
53
  /** Route context attached to the activations payload (AC-01 visibility). */
45
54
  export interface PromptInjectionRouteSummaryWire {
46
55
  status: 'confirmed' | 'unconfirmed';
@@ -105,4 +114,23 @@ export interface PromptInjectionBudgetStatus {
105
114
  nextAction?: string;
106
115
  /** Present when capacityStatus === 'unconfirmed': per-host forecasts. */
107
116
  perHost?: PromptInjectionPerHostForecast[];
117
+ /**
118
+ * SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §7: provenance of `budget` — the
119
+ * same resolution the real injection runs. `legacy_fallback` + `unknown`
120
+ * means these numbers are the fixed-budget view (no runtime model capacity
121
+ * was confirmed); the UI must present them as such, not as a
122
+ * model-accurate forecast. Absent behaves as the legacy fixed view
123
+ * (pre-SPEC payload compat).
124
+ */
125
+ budgetCapacitySource?: 'host_effective_window' | 'pi_catalog_exact' | 'legacy_fallback';
126
+ /** confirmed = a real context window was resolved; unknown = fixed-budget fallback. */
127
+ budgetCapacityStatus?: 'confirmed' | 'unknown';
128
+ /** The context window the budget was derived from; present only when confirmed. */
129
+ budgetContextWindowTokens?: number;
130
+ /**
131
+ * SPEC §7 v0.2: bounded machine-readable reason — present only when
132
+ * budgetCapacityStatus is 'unknown'. Closed set mirroring the core
133
+ * `PromptBudgetFallbackReason`; never free-form detail or user content.
134
+ */
135
+ budgetFallbackReason?: 'host_capacity_absent' | 'host_capacity_source_untrusted' | 'host_capacity_invalid_value' | 'catalog_unavailable' | 'model_identity_absent' | 'catalog_no_exact_match' | 'catalog_no_match_for_model_id' | 'catalog_ambiguous_model_id' | 'catalog_provider_absent' | 'catalog_metadata_invalid';
108
136
  }
@@ -13,6 +13,15 @@ import { ReceiptCoverageDisclosure, getReceiptSourceStatusLabelKey } from "../..
13
13
  import { validateActivationsData, validateLifecycleMetricsData, isReversibleChannel, } from "./ActivationValidators.js";
14
14
  import { enumLabel } from "../../utils/enum-labels.js";
15
15
  import { formatDate } from "../../utils/format-date.js";
16
+ function tierFitLabel(t, tierFit) {
17
+ if (tierFit === undefined)
18
+ return t("pages.activation.fitUnknown", { defaultValue: "未知" });
19
+ if (tierFit === "fits_all_tiers")
20
+ return t("pages.activation.fitYes", { defaultValue: "可装入" });
21
+ if (tierFit === "model_dependent")
22
+ return t("pages.activation.fitModelDependent", { defaultValue: "视模型而定" });
23
+ return t("pages.activation.fitNo", { defaultValue: "超长" });
24
+ }
16
25
  // Date+time rendering options — mirrors the old default toLocaleString() shape
17
26
  // (numeric date, hours/minutes/seconds) while following the UI language.
18
27
  const DATETIME_OPTS = {
@@ -151,7 +160,7 @@ function ActivationFactCard({ record, lifecycleData, receiptCount, onDisable, di
151
160
  navigate(`/report-problem?${params.toString()}`);
152
161
  }, children: t("pages.reportProblem.entryFeedback") }), _jsxs("div", { className: "mt-[14px] mb-2", children: [_jsx("span", { className: "text-ink-2 text-[13px]", children: t("pages.activation.principleLabel") }), " ", _jsx(Link, { to: `/principles/${record.principleId}`, className: "text-gov text-[13px] hover:underline focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", "data-testid": `principle-link-${record.principleId}`, title: record.principleId, children: record.action }), _jsx("span", { className: "ml-2 font-mono text-[11px] text-ink-4", children: record.principleId })] }), record.legacyDecisionUnknown && _jsx("div", { className: "mt-3 border-l-2 border-amber px-3 text-[12px] text-amber", children: t('pages.activation.legacyUnknown', { due: record.ownerReviewDueAt ? formatDate(record.ownerReviewDueAt, i18n.language, DATETIME_OPTS) : t('pages.activation.sevenDays') }) }), _jsxs("div", { className: "text-ink-3 text-[13px] leading-relaxed space-y-1", children: [_jsxs("div", { children: [_jsx("span", { className: "text-ink-4 font-mono text-[11px] uppercase", children: t("pages.activation.actionLabel") }), " ", _jsx("span", { className: "text-ink-2", children: record.action })] }), _jsxs("div", { children: [_jsx("span", { className: "text-ink-4 font-mono text-[11px] uppercase", children: t("pages.activation.targetLabel") }), " ", _jsx("span", { className: "text-ink-2 font-mono text-[12px]", children: record.targetRef })] }), _jsxs("div", { children: [_jsx("span", { className: "text-ink-4 font-mono text-[11px] uppercase", children: t("pages.activation.activatedAtLabel") }), " ", neverActivated ? (_jsx("span", { className: "text-amber", children: t("pages.activation.neverActivated") })) : (_jsx("span", { className: "text-ink-2 font-mono text-[12px] tabular-nums", children: formatDate(record.activatedAt, i18n.language, DATETIME_OPTS) }))] }), record.promptInjection !== undefined && (_jsxs("div", { className: "flex items-start gap-2", "data-testid": `activation-injection-${record.activationId}`, children: [_jsx("span", { className: "text-ink-4 font-mono text-[11px] uppercase", children: t("pages.activation.injectionLabel", { defaultValue: "注入状态" }) }), _jsxs("span", { className: `text-[12.5px] leading-relaxed ${record.promptInjection.status === "injectable"
153
162
  ? "text-ink-2"
154
- : record.promptInjection.status === "window_excluded"
163
+ : record.promptInjection.status === "window_excluded" || record.promptInjection.status === "model_dependent"
155
164
  ? "text-amber"
156
165
  : "text-danger"}`, children: [record.promptInjection.status === "injectable" &&
157
166
  t("pages.activation.injectionInjectable", {
@@ -174,15 +183,19 @@ function ActivationFactCard({ record, lifecycleData, receiptCount, onDisable, di
174
183
  defaultValue: "超长不可注入:单条已 {{cost}} 字符 > 上限 {{budget}} 字符,轮转也救不了——需精简正文",
175
184
  cost: record.promptInjection.costChars ?? 0,
176
185
  budget: record.promptInjection.budget ?? 0,
186
+ }), record.promptInjection.status === "model_dependent" &&
187
+ t("pages.activation.injectionModelDependent", {
188
+ defaultValue: "取决于模型上下文:单条 {{cost}} 字符在最小预算内装不下、但在支持上限内——较小上下文模型会排除它,较大模型可注入",
189
+ cost: record.promptInjection.costChars ?? 0,
177
190
  }), record.promptInjection.status === "resolution_failed" &&
178
191
  t("pages.activation.injectionResolutionFailed", {
179
192
  defaultValue: "无法判定:{{reason}}",
180
193
  reason: record.promptInjection.reason ?? "unknown",
181
194
  }), record.promptInjection.status === "route_unconfirmed" && (_jsxs(_Fragment, { children: [t("pages.activation.injectionRouteUnconfirmed", {
182
195
  defaultValue: "当前宿主容量未确认:OpenClaw {{oc}} / Codex {{cx}} / WorkBuddy {{wb}}",
183
- oc: record.promptInjection.perHostFits?.find((f) => f.hostKind === "openclaw")?.fits === undefined ? t("pages.activation.fitUnknown", { defaultValue: "未知" }) : record.promptInjection.perHostFits?.find((f) => f.hostKind === "openclaw")?.fits ? t("pages.activation.fitYes", { defaultValue: "可装入" }) : t("pages.activation.fitNo", { defaultValue: "超长" }),
184
- cx: record.promptInjection.perHostFits?.find((f) => f.hostKind === "codex")?.fits === undefined ? t("pages.activation.fitUnknown", { defaultValue: "未知" }) : record.promptInjection.perHostFits?.find((f) => f.hostKind === "codex")?.fits ? t("pages.activation.fitYes", { defaultValue: "可装入" }) : t("pages.activation.fitNo", { defaultValue: "超长" }),
185
- wb: record.promptInjection.perHostFits?.find((f) => f.hostKind === "workbuddy")?.fits === undefined ? t("pages.activation.fitUnknown", { defaultValue: "未知" }) : record.promptInjection.perHostFits?.find((f) => f.hostKind === "workbuddy")?.fits ? t("pages.activation.fitYes", { defaultValue: "可装入" }) : t("pages.activation.fitNo", { defaultValue: "超长" }),
196
+ oc: tierFitLabel(t, record.promptInjection.perHostFits?.find((f) => f.hostKind === "openclaw")?.tierFit),
197
+ cx: tierFitLabel(t, record.promptInjection.perHostFits?.find((f) => f.hostKind === "codex")?.tierFit),
198
+ wb: tierFitLabel(t, record.promptInjection.perHostFits?.find((f) => f.hostKind === "workbuddy")?.tierFit),
186
199
  }), record.promptInjection.reason !== undefined && (_jsx("span", { className: "block text-ink-4 text-[11.5px]", children: record.promptInjection.reason }))] }))] })] })), canProposeRevision && (_jsxs("div", { className: "mt-2", "data-testid": `activation-revision-${record.activationId}`, children: [!showRevisionInput && revisionResult === null && (_jsx("button", { type: "button", onClick: () => { setShowRevisionInput(true); setRevisionText(""); }, "data-testid": `activation-revision-open-${record.activationId}`, className: "inline-flex items-center border border-line rounded-[3px] px-[10px] py-[4px] text-[12px] hover:border-line-2", children: t("pages.activation.revisionOpen", { defaultValue: "修改为可注入版本" }) })), showRevisionInput && (_jsxs("div", { className: "rounded-[3px] border border-line bg-surface/60 p-2", children: [_jsx("textarea", { value: revisionText, onChange: (e) => setRevisionText(e.target.value), rows: 3, className: "w-full rounded-[2px] border border-line bg-panel px-2 py-1 text-[12.5px]", placeholder: t("pages.activation.revisionPlaceholder", { defaultValue: "输入新的原则正文(保留触发条件、必须动作和必要例外,尽量精简)" }), "data-testid": `activation-revision-input-${record.activationId}` }), _jsxs("div", { className: "mt-1 flex gap-2", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: handleProposeRevision, disabled: revisionSubmitting || revisionText.trim().length === 0, "data-testid": `activation-revision-submit-${record.activationId}`, children: revisionSubmitting ? t("common.loading") + "…" : t("pages.activation.revisionSubmit", { defaultValue: "创建待审批的新版本" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setShowRevisionInput(false), children: t("common.cancel", { defaultValue: "取消" }) })] })] })), revisionResult !== null && (_jsxs("div", { className: "rounded-[3px] border border-gov/40 bg-gov/5 p-2 text-[12px] leading-relaxed", "data-testid": `activation-revision-result-${record.activationId}`, children: [_jsx("p", { children: t("pages.activation.revisionResultNote", {
187
200
  defaultValue: "新版本已进入待审批(审批号 {{approvalId}})。批准后旧版本自动停用并由新版本替换;拒绝则旧版本继续生效。",
188
201
  approvalId: revisionResult.approvalId ?? "",
@@ -402,29 +402,53 @@ function PendingReviewCard({ group, actionsLockedReason, onDecisionApplied, prom
402
402
  ? "border-green/40 bg-green/10 text-green"
403
403
  : "border-line bg-surface/80 text-ink-3"}`, role: "status", "data-testid": `decided-banner-${group.principleId}`, children: decidedOutcome === "approved"
404
404
  ? t("pages.focus.decidedApproved", { defaultValue: "已批准 ✓ 正在写入生效记录…" })
405
- : t("pages.focus.decidedRejected", { defaultValue: "已拒绝" }) })), _jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("span", { className: "inline-flex items-center border border-line rounded-[2px] px-[7px] py-1 font-mono text-[11px] text-ink-3 bg-surface/80 uppercase", children: channelLabel }), isReversible && (_jsx("span", { className: "inline-flex items-center border border-amber/35 text-amber rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase", children: primaryChannel === "prompt" ? t("pages.focus.tagReversible") : t("pages.focus.tagLowRisk") }))] }), _jsx("div", { className: "mt-[14px] mb-2 font-semibold text-ink leading-snug", "data-testid": `candidate-title-${group.principleId}`, children: readingLayout.title }), readingLayout.body !== null && (_jsx("div", { className: "mb-2 text-[15px] leading-relaxed text-ink whitespace-pre-wrap break-words", "data-testid": `candidate-body-${group.principleId}`, children: readingLayout.body })), group.artifactUnavailable === true && (_jsx("div", { className: "mb-2 text-[12.5px] leading-relaxed text-amber", "data-testid": `artifact-unavailable-${group.principleId}`, children: t("pages.focus.artifactUnavailableNote") })), unlinkedCandidateNote && (_jsx("div", { className: "mb-2 text-[12.5px] leading-relaxed text-amber", "data-testid": `unlinked-candidate-${group.principleId}`, children: t("pages.focus.unlinkedCandidateNote") })), _jsx("div", { className: "mt-2 px-3 py-2 bg-surface/60 border-l-2 border-gov text-ink-3 text-[13px] leading-snug", children: t("pages.focus.evidenceSummary", { count: group.records.length }) }), actionsLocked && (_jsxs("div", { className: "mt-3 rounded-[3px] border border-amber/40 bg-amber/5 p-3", "data-testid": `pending-actions-locked-${group.principleId}`, children: [_jsx("p", { className: "text-ink-2 text-[12.5px] leading-relaxed", children: actionsLockedReason }), _jsx(Link, { to: "/settings", "data-testid": `pending-actions-locked-settings-${group.principleId}`, className: "mt-2 inline-flex items-center border border-line text-ink bg-surface rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors", children: t("pages.focus.ownerDecision.goSettingsCta") })] })), decidedOutcome === null && primaryChannel === "prompt" && promptInjection?.truncated === true && (_jsxs("div", { className: "mt-3 rounded-[3px] border border-amber/40 bg-amber/5 px-3 py-2 text-[12.5px] text-amber", "data-testid": `approve-queue-badge-${group.principleId}`, children: [promptInjection.productionRotates === true && group.fitsPromptBudget === true
406
- ? t("pages.focus.approveQueueBadgeRotation", {
407
- defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。当前按公平轮转选取,新原则会在后续轮次自行轮到,无需停用旧原则。",
405
+ : t("pages.focus.decidedRejected", { defaultValue: "已拒绝" }) })), _jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("span", { className: "inline-flex items-center border border-line rounded-[2px] px-[7px] py-1 font-mono text-[11px] text-ink-3 bg-surface/80 uppercase", children: channelLabel }), isReversible && (_jsx("span", { className: "inline-flex items-center border border-amber/35 text-amber rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase", children: primaryChannel === "prompt" ? t("pages.focus.tagReversible") : t("pages.focus.tagLowRisk") }))] }), _jsx("div", { className: "mt-[14px] mb-2 font-semibold text-ink leading-snug", "data-testid": `candidate-title-${group.principleId}`, children: readingLayout.title }), readingLayout.body !== null && (_jsx("div", { className: "mb-2 text-[15px] leading-relaxed text-ink whitespace-pre-wrap break-words", "data-testid": `candidate-body-${group.principleId}`, children: readingLayout.body })), group.artifactUnavailable === true && (_jsx("div", { className: "mb-2 text-[12.5px] leading-relaxed text-amber", "data-testid": `artifact-unavailable-${group.principleId}`, children: t("pages.focus.artifactUnavailableNote") })), unlinkedCandidateNote && (_jsx("div", { className: "mb-2 text-[12.5px] leading-relaxed text-amber", "data-testid": `unlinked-candidate-${group.principleId}`, children: t("pages.focus.unlinkedCandidateNote") })), _jsx("div", { className: "mt-2 px-3 py-2 bg-surface/60 border-l-2 border-gov text-ink-3 text-[13px] leading-snug", children: t("pages.focus.evidenceSummary", { count: group.records.length }) }), actionsLocked && (_jsxs("div", { className: "mt-3 rounded-[3px] border border-amber/40 bg-amber/5 p-3", "data-testid": `pending-actions-locked-${group.principleId}`, children: [_jsx("p", { className: "text-ink-2 text-[12.5px] leading-relaxed", children: actionsLockedReason }), _jsx(Link, { to: "/settings", "data-testid": `pending-actions-locked-settings-${group.principleId}`, className: "mt-2 inline-flex items-center border border-line text-ink bg-surface rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors", children: t("pages.focus.ownerDecision.goSettingsCta") })] })), decidedOutcome === null && primaryChannel === "prompt" && promptInjection?.truncated === true && (_jsxs("div", { className: "mt-3 rounded-[3px] border border-amber/40 bg-amber/5 px-3 py-2 text-[12.5px] text-amber", "data-testid": `approve-queue-badge-${group.principleId}`, children: [promptInjection.productionRotates === true && group.fitsPromptBudget === true && group.promptBudgetTierFit === "model_dependent"
406
+ ? t("pages.focus.approveQueueBadgeRotationModelDependent", {
407
+ defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。这条原则的篇幅在较小上下文模型的注入预算内装不下,轮到它时也未必进入提示词——是否生效取决于当时模型的上限。",
408
408
  used: promptInjection.usedChars,
409
409
  budget: promptInjection.budget,
410
410
  })
411
- : promptInjection.productionRotates === true
412
- ? t("pages.focus.approveQueueBadgeOversized", {
413
- defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。这条原则本身已超出注入位上限,轮转也无法让它进入提示词——需要先精简它的内容。",
411
+ : promptInjection.productionRotates === true && group.fitsPromptBudget === true
412
+ ? t("pages.focus.approveQueueBadgeRotation", {
413
+ defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。当前按公平轮转选取,新原则会在后续轮次自行轮到,无需停用旧原则。",
414
414
  used: promptInjection.usedChars,
415
415
  budget: promptInjection.budget,
416
416
  })
417
- : t("pages.focus.approveQueueBadge", {
418
- defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。",
419
- used: promptInjection.usedChars,
420
- budget: promptInjection.budget,
421
- }), _jsxs("div", { className: "mt-1 text-ink-3 text-[11.5px]", "data-testid": `approve-queue-badge-scope-${group.principleId}`, children: [promptInjection.route === "shared_render"
417
+ : promptInjection.productionRotates === true
418
+ ? t("pages.focus.approveQueueBadgeOversized", {
419
+ defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。这条原则本身已超出注入位上限,轮转也无法让它进入提示词——需要先精简它的内容。",
420
+ used: promptInjection.usedChars,
421
+ budget: promptInjection.budget,
422
+ })
423
+ : t("pages.focus.approveQueueBadge", {
424
+ defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。",
425
+ used: promptInjection.usedChars,
426
+ budget: promptInjection.budget,
427
+ }), _jsxs("div", { className: "mt-1 text-ink-3 text-[11.5px]", "data-testid": `approve-queue-badge-scope-${group.principleId}`, children: [promptInjection.route === "shared_render"
422
428
  ? t("pages.focus.promptScopeShared")
423
429
  : t("pages.focus.promptScopeList", {
424
430
  full: promptInjection.fullRenderChars !== undefined
425
431
  ? t("pages.focus.promptScopeFullRender", { count: promptInjection.fullRenderChars })
426
432
  : "",
427
- }), promptInjection.hostKind !== undefined ? ` · ${promptInjection.hostKind === "openclaw" ? "OpenClaw" : promptInjection.hostKind === "workbuddy" ? "WorkBuddy" : "Codex"}` : ""] })] })), group.revisionReviewUnavailable === true && _jsx("p", { role: "alert", className: "mt-3 text-danger", children: t("pages.focus.revisionReviewUnavailable") }), group.revisionReview !== undefined && (_jsxs("div", { className: "mt-3 border border-line p-3 text-[12.5px]", "data-testid": `revision-intent-review-${group.principleId}`, children: [_jsx("p", { children: t("pages.focus.revisionIntentReview", { defaultValue: "修订复核:确认新正文保留触发条件、行动要求和必要例外,并与沿用意图一致。" }) }), _jsx("pre", { className: "mt-2 max-h-32 overflow-auto whitespace-pre-wrap", children: group.revisionReview.oldStatement }), _jsx("pre", { className: "mt-2 max-h-32 overflow-auto whitespace-pre-wrap", children: group.revisionReview.newStatement }), _jsx("dl", { className: "mt-2", children: Object.entries(group.revisionReview.intentFields).map(([key, value]) => _jsxs("div", { children: [_jsx("dt", { children: t(`pages.focus.intentField.${key}`, { defaultValue: key }) }), _jsx("dd", { children: value })] }, key)) }), _jsxs("label", { className: "mt-2 flex gap-2", children: [_jsx("input", { type: "checkbox", checked: intentReviewed, onChange: (event) => setIntentReviewed(event.target.checked) }), t("pages.focus.revisionIntentConfirmed", { defaultValue: "我已核对上述意图与新正文一致" })] })] })), _jsxs("div", { className: "flex gap-2 mt-4 flex-wrap items-center", children: [_jsx("button", { type: "button", onClick: handleApprove, disabled: decisionDisabled || (group.revisionReviewUnavailable === true || (group.revisionReview !== undefined && !intentReviewed)), "data-testid": `approve-btn-${group.principleId}`, className: "inline-flex items-center border border-gov bg-gov text-paper rounded-[3px] px-[14px] py-[6px] text-[12.5px] font-medium hover:bg-gov-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: actionLoading ? t("common.loading") + "…" : t("pages.focus.approveAction", { defaultValue: "批准" }) }), _jsx("button", { type: "button", onClick: () => { setShowEditInput((v) => !v); setShowRejectInput(false); }, disabled: decisionDisabled, "data-testid": `edit-btn-${group.principleId}`, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.editAction", { defaultValue: "替换待审版本" }) }), _jsx("button", { type: "button", onClick: () => { setShowRejectInput((v) => !v); setShowEditInput(false); }, disabled: decisionDisabled, "data-testid": `reject-btn-${group.principleId}`, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.rejectAction", { defaultValue: "拒绝" }) }), _jsx(Link, { to: "/pain", className: "inline-flex items-center text-gov text-[12.5px] hover:underline focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.viewFullChain") })] }), showRejectInput && (_jsxs("div", { className: "mt-3 p-3 border border-danger/20 rounded-[var(--radius-md)]", children: [_jsx("label", { className: "block font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("pages.focus.rejectReasonLabel", { defaultValue: "拒绝原因(必填)" }) }), _jsx("textarea", { value: rejectReason, onChange: (e) => setRejectReason(e.target.value), "data-testid": `reject-reason-${group.principleId}`, placeholder: t("pages.focus.rejectReasonPlaceholder", { defaultValue: "请说明拒绝原因" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] min-h-[80px] resize-y focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov" }), _jsxs("div", { className: "flex gap-2 mt-2", children: [_jsx("button", { type: "button", onClick: handleReject, disabled: !rejectReason.trim() || actionLoading, "data-testid": `confirm-reject-${group.principleId}`, className: "inline-flex items-center border border-danger text-danger bg-surface rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:bg-danger/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed", children: t("pages.focus.confirmReject", { defaultValue: "确认拒绝" }) }), _jsx("button", { type: "button", onClick: () => { setShowRejectInput(false); setRejectReason(""); }, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors", children: t("pages.focus.cancelAction", { defaultValue: "取消" }) })] })] })), showEditInput && (_jsxs("div", { className: "mt-3 p-3 border border-gov/20 rounded-[var(--radius-md)]", children: [_jsx("label", { className: "block font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("pages.focus.editReasonLabel", { defaultValue: "替换原因(必填)" }) }), _jsxs("div", { className: "mb-2 text-ink-4 text-[12px] font-mono", children: [t("pages.focus.currentArtifactLabel", { defaultValue: "当前工件" }), ": ", currentArtifactId] }), _jsx("input", { type: "text", value: newArtifactId, onChange: (e) => setNewArtifactId(e.target.value), "data-testid": `edit-new-artifact-${group.principleId}`, placeholder: t("pages.focus.editNewArtifactPlaceholder", { defaultValue: "输入新的已验证工件 ID" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] mb-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov", "aria-label": t("pages.focus.editNewArtifactPlaceholder", { defaultValue: "输入新的已验证工件 ID" }) }), _jsx("textarea", { value: editReason, onChange: (e) => setEditReason(e.target.value), "data-testid": `edit-reason-${group.principleId}`, placeholder: t("pages.focus.editReasonPlaceholder", { defaultValue: "请说明为什么需要替换这份待审版本" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] min-h-[80px] resize-y focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov" }), _jsxs("div", { className: "flex gap-2 mt-2", children: [_jsx("button", { type: "button", onClick: handleEdit, disabled: !editReason.trim() || !newArtifactId.trim() || actionLoading, "data-testid": `confirm-edit-${group.principleId}`, className: "inline-flex items-center border border-gov text-gov bg-surface rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:bg-gov/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed", children: t("pages.focus.confirmEdit", { defaultValue: "确认替换" }) }), _jsx("button", { type: "button", onClick: () => { setShowEditInput(false); setEditReason(""); setNewArtifactId(""); }, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors", children: t("pages.focus.cancelAction", { defaultValue: "取消" }) })] })] }))] }));
433
+ }), promptInjection.hostKind !== undefined ? ` · ${promptInjection.hostKind === "openclaw" ? "OpenClaw" : promptInjection.hostKind === "workbuddy" ? "WorkBuddy" : "Codex"}` : "", ` · ${promptInjection.budgetCapacityStatus === "confirmed"
434
+ ? t("pages.focus.promptBudgetModelAwareNote", {
435
+ defaultValue: "预算按已确认的模型上下文解析({{window}} tokens)",
436
+ window: promptInjection.budgetContextWindowTokens ?? 0,
437
+ })
438
+ : t("pages.focus.promptBudgetFallbackNote", {
439
+ defaultValue: "按固定回退预算估算(控制台未绑定运行时模型{{reason}})",
440
+ reason: promptInjection.budgetFallbackReason !== undefined
441
+ ? t("pages.focus.promptBudgetFallbackReasonSuffix", {
442
+ defaultValue: ",原因: {{code}}",
443
+ // The raw bounded code stays the default: it is the
444
+ // grep-able identifier shared with events/logs. Locales
445
+ // may override via pages.focus.budgetFallbackReason.<code>.
446
+ code: t(`pages.focus.budgetFallbackReason.${promptInjection.budgetFallbackReason}`, {
447
+ defaultValue: promptInjection.budgetFallbackReason,
448
+ }),
449
+ })
450
+ : "",
451
+ })}`] })] })), group.revisionReviewUnavailable === true && _jsx("p", { role: "alert", className: "mt-3 text-danger", children: t("pages.focus.revisionReviewUnavailable") }), group.revisionReview !== undefined && (_jsxs("div", { className: "mt-3 border border-line p-3 text-[12.5px]", "data-testid": `revision-intent-review-${group.principleId}`, children: [_jsx("p", { children: t("pages.focus.revisionIntentReview", { defaultValue: "修订复核:确认新正文保留触发条件、行动要求和必要例外,并与沿用意图一致。" }) }), _jsx("pre", { className: "mt-2 max-h-32 overflow-auto whitespace-pre-wrap", children: group.revisionReview.oldStatement }), _jsx("pre", { className: "mt-2 max-h-32 overflow-auto whitespace-pre-wrap", children: group.revisionReview.newStatement }), _jsx("dl", { className: "mt-2", children: Object.entries(group.revisionReview.intentFields).map(([key, value]) => _jsxs("div", { children: [_jsx("dt", { children: t(`pages.focus.intentField.${key}`, { defaultValue: key }) }), _jsx("dd", { children: value })] }, key)) }), _jsxs("label", { className: "mt-2 flex gap-2", children: [_jsx("input", { type: "checkbox", checked: intentReviewed, onChange: (event) => setIntentReviewed(event.target.checked) }), t("pages.focus.revisionIntentConfirmed", { defaultValue: "我已核对上述意图与新正文一致" })] })] })), _jsxs("div", { className: "flex gap-2 mt-4 flex-wrap items-center", children: [_jsx("button", { type: "button", onClick: handleApprove, disabled: decisionDisabled || (group.revisionReviewUnavailable === true || (group.revisionReview !== undefined && !intentReviewed)), "data-testid": `approve-btn-${group.principleId}`, className: "inline-flex items-center border border-gov bg-gov text-paper rounded-[3px] px-[14px] py-[6px] text-[12.5px] font-medium hover:bg-gov-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: actionLoading ? t("common.loading") + "…" : t("pages.focus.approveAction", { defaultValue: "批准" }) }), _jsx("button", { type: "button", onClick: () => { setShowEditInput((v) => !v); setShowRejectInput(false); }, disabled: decisionDisabled, "data-testid": `edit-btn-${group.principleId}`, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.editAction", { defaultValue: "替换待审版本" }) }), _jsx("button", { type: "button", onClick: () => { setShowRejectInput((v) => !v); setShowEditInput(false); }, disabled: decisionDisabled, "data-testid": `reject-btn-${group.principleId}`, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.rejectAction", { defaultValue: "拒绝" }) }), _jsx(Link, { to: "/pain", className: "inline-flex items-center text-gov text-[12.5px] hover:underline focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.viewFullChain") })] }), showRejectInput && (_jsxs("div", { className: "mt-3 p-3 border border-danger/20 rounded-[var(--radius-md)]", children: [_jsx("label", { className: "block font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("pages.focus.rejectReasonLabel", { defaultValue: "拒绝原因(必填)" }) }), _jsx("textarea", { value: rejectReason, onChange: (e) => setRejectReason(e.target.value), "data-testid": `reject-reason-${group.principleId}`, placeholder: t("pages.focus.rejectReasonPlaceholder", { defaultValue: "请说明拒绝原因" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] min-h-[80px] resize-y focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov" }), _jsxs("div", { className: "flex gap-2 mt-2", children: [_jsx("button", { type: "button", onClick: handleReject, disabled: !rejectReason.trim() || actionLoading, "data-testid": `confirm-reject-${group.principleId}`, className: "inline-flex items-center border border-danger text-danger bg-surface rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:bg-danger/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed", children: t("pages.focus.confirmReject", { defaultValue: "确认拒绝" }) }), _jsx("button", { type: "button", onClick: () => { setShowRejectInput(false); setRejectReason(""); }, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors", children: t("pages.focus.cancelAction", { defaultValue: "取消" }) })] })] })), showEditInput && (_jsxs("div", { className: "mt-3 p-3 border border-gov/20 rounded-[var(--radius-md)]", children: [_jsx("label", { className: "block font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("pages.focus.editReasonLabel", { defaultValue: "替换原因(必填)" }) }), _jsxs("div", { className: "mb-2 text-ink-4 text-[12px] font-mono", children: [t("pages.focus.currentArtifactLabel", { defaultValue: "当前工件" }), ": ", currentArtifactId] }), _jsx("input", { type: "text", value: newArtifactId, onChange: (e) => setNewArtifactId(e.target.value), "data-testid": `edit-new-artifact-${group.principleId}`, placeholder: t("pages.focus.editNewArtifactPlaceholder", { defaultValue: "输入新的已验证工件 ID" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] mb-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov", "aria-label": t("pages.focus.editNewArtifactPlaceholder", { defaultValue: "输入新的已验证工件 ID" }) }), _jsx("textarea", { value: editReason, onChange: (e) => setEditReason(e.target.value), "data-testid": `edit-reason-${group.principleId}`, placeholder: t("pages.focus.editReasonPlaceholder", { defaultValue: "请说明为什么需要替换这份待审版本" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] min-h-[80px] resize-y focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov" }), _jsxs("div", { className: "flex gap-2 mt-2", children: [_jsx("button", { type: "button", onClick: handleEdit, disabled: !editReason.trim() || !newArtifactId.trim() || actionLoading, "data-testid": `confirm-edit-${group.principleId}`, className: "inline-flex items-center border border-gov text-gov bg-surface rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:bg-gov/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed", children: t("pages.focus.confirmEdit", { defaultValue: "确认替换" }) }), _jsx("button", { type: "button", onClick: () => { setShowEditInput(false); setEditReason(""); setNewArtifactId(""); }, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors", children: t("pages.focus.cancelAction", { defaultValue: "取消" }) })] })] }))] }));
428
452
  }
429
453
  function StagnationSignalCard({ signal }) {
430
454
  const { t } = useTranslation();
@@ -64,6 +64,17 @@ function validateApprovalGroup(raw) {
64
64
  return null;
65
65
  fitsPromptBudget = fits;
66
66
  }
67
+ // SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: promptBudgetTierFit is optional,
68
+ // whitelist-checked. model_dependent qualifies the rotation promise.
69
+ let promptBudgetTierFit;
70
+ if (Object.hasOwn(raw, "promptBudgetTierFit")) {
71
+ const { promptBudgetTierFit: tier } = raw;
72
+ if (tier !== "fits_all_tiers" && tier !== "model_dependent"
73
+ && tier !== "exceeds_max_tier" && tier !== "unconfirmed") {
74
+ return null;
75
+ }
76
+ promptBudgetTierFit = tier;
77
+ }
67
78
  // PRI-940: artifactUnavailable is optional. ERR-009 discipline — present but
68
79
  // wrong type fails loud (return null); absent stays undefined, which keeps
69
80
  // the card title rendering exactly as before.
@@ -80,6 +91,7 @@ function validateApprovalGroup(raw) {
80
91
  candidateDescription,
81
92
  status,
82
93
  fitsPromptBudget,
94
+ promptBudgetTierFit,
83
95
  artifactUnavailable,
84
96
  records: validRecords,
85
97
  };
@@ -655,6 +655,12 @@ export interface ApprovalGroupData {
655
655
  intentFields: Record<string, string>;
656
656
  };
657
657
  fitsPromptBudget?: boolean;
658
+ /**
659
+ * SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: honest tier classification of the
660
+ * candidate's own entry vs the supported budget range. `model_dependent`
661
+ * qualifies the rotation promise (fits only on larger-context models).
662
+ */
663
+ promptBudgetTierFit?: 'fits_all_tiers' | 'model_dependent' | 'exceeds_max_tier' | 'unconfirmed';
658
664
  /**
659
665
  * PRI-940: the pinned artifact is missing from the store, so the card has no
660
666
  * human-readable payload and `principleTitle` carries the synthesized
@@ -1061,7 +1061,7 @@ export function validateGovernanceQueue(v) {
1061
1061
  return result;
1062
1062
  }
1063
1063
  const PROMPT_INJECTION_STATUS_KINDS = new Set([
1064
- 'injectable', 'oversized', 'window_excluded', 'resolution_failed', 'route_unconfirmed',
1064
+ 'injectable', 'oversized', 'model_dependent', 'window_excluded', 'resolution_failed', 'route_unconfirmed',
1065
1065
  ]);
1066
1066
  /** rc-1..rc-4: validate the per-activation injection status as untrusted input. */
1067
1067
  function validatePromptActivationInjectionStatus(v) {
@@ -1089,9 +1089,10 @@ function validatePromptActivationInjectionStatus(v) {
1089
1089
  for (const entry of v.perHostFits) {
1090
1090
  if (!isObject(entry))
1091
1091
  return null;
1092
- if ((entry.hostKind !== 'openclaw' && entry.hostKind !== 'codex' && entry.hostKind !== 'workbuddy') || typeof entry.fits !== 'boolean')
1092
+ if ((entry.hostKind !== 'openclaw' && entry.hostKind !== 'codex' && entry.hostKind !== 'workbuddy')
1093
+ || (entry.tierFit !== 'fits_all_tiers' && entry.tierFit !== 'model_dependent' && entry.tierFit !== 'exceeds_max_tier'))
1093
1094
  return null;
1094
- perHostFits.push({ hostKind: entry.hostKind, fits: entry.fits });
1095
+ perHostFits.push({ hostKind: entry.hostKind, tierFit: entry.tierFit });
1095
1096
  }
1096
1097
  result.perHostFits = perHostFits;
1097
1098
  }
@@ -1743,6 +1744,13 @@ function validateApprovalGroup(v) {
1743
1744
  if (Object.hasOwn(v, 'fitsPromptBudget') && typeof v.fitsPromptBudget === 'boolean') {
1744
1745
  result.fitsPromptBudget = v.fitsPromptBudget;
1745
1746
  }
1747
+ // SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: optional tier fit, whitelist +
1748
+ // drop-on-invalid (same rule). Absent keeps the boolean-only behavior.
1749
+ if (Object.hasOwn(v, 'promptBudgetTierFit')
1750
+ && (v.promptBudgetTierFit === 'fits_all_tiers' || v.promptBudgetTierFit === 'model_dependent'
1751
+ || v.promptBudgetTierFit === 'exceeds_max_tier' || v.promptBudgetTierFit === 'unconfirmed')) {
1752
+ result.promptBudgetTierFit = v.promptBudgetTierFit;
1753
+ }
1746
1754
  // PRI-940: optional artifactUnavailable. Only a real boolean is propagated; a
1747
1755
  // present-but-wrong value is dropped so the card keeps its plain untitled
1748
1756
  // fallback rather than rendering an unverified claim.
@@ -1806,6 +1814,28 @@ export function validatePromptInjectionBudgetStatus(v) {
1806
1814
  if (Object.hasOwn(v, 'nextAction') && isString(v.nextAction)) {
1807
1815
  result.nextAction = v.nextAction;
1808
1816
  }
1817
+ // SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §7: budget provenance, additive +
1818
+ // drop-on-invalid. Absent behaves as the legacy fixed-budget view
1819
+ // (pre-SPEC server compat).
1820
+ if (Object.hasOwn(v, 'budgetCapacitySource')
1821
+ && (v.budgetCapacitySource === 'host_effective_window' || v.budgetCapacitySource === 'pi_catalog_exact'
1822
+ || v.budgetCapacitySource === 'legacy_fallback')) {
1823
+ result.budgetCapacitySource = v.budgetCapacitySource;
1824
+ }
1825
+ if (Object.hasOwn(v, 'budgetFallbackReason')
1826
+ && (v.budgetFallbackReason === 'host_capacity_absent' || v.budgetFallbackReason === 'host_capacity_source_untrusted'
1827
+ || v.budgetFallbackReason === 'host_capacity_invalid_value' || v.budgetFallbackReason === 'catalog_unavailable'
1828
+ || v.budgetFallbackReason === 'model_identity_absent' || v.budgetFallbackReason === 'catalog_no_exact_match'
1829
+ || v.budgetFallbackReason === 'catalog_no_match_for_model_id' || v.budgetFallbackReason === 'catalog_ambiguous_model_id'
1830
+ || v.budgetFallbackReason === 'catalog_provider_absent' || v.budgetFallbackReason === 'catalog_metadata_invalid')) {
1831
+ result.budgetFallbackReason = v.budgetFallbackReason;
1832
+ }
1833
+ if (Object.hasOwn(v, 'budgetCapacityStatus') && (v.budgetCapacityStatus === 'confirmed' || v.budgetCapacityStatus === 'unknown')) {
1834
+ result.budgetCapacityStatus = v.budgetCapacityStatus;
1835
+ }
1836
+ if (Object.hasOwn(v, 'budgetContextWindowTokens') && typeof v.budgetContextWindowTokens === 'number' && Number.isInteger(v.budgetContextWindowTokens) && v.budgetContextWindowTokens > 0) {
1837
+ result.budgetContextWindowTokens = v.budgetContextWindowTokens;
1838
+ }
1809
1839
  if (Object.hasOwn(v, 'perHost') && Array.isArray(v.perHost)) {
1810
1840
  // rc-4: validate every element, not just "is array".
1811
1841
  const perHost = [];