create-principles-disciple 1.120.6 → 1.121.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/console/dist/server/index.js +4 -0
  2. package/console/dist/server/models/GovernanceConsoleModel.d.ts +10 -2
  3. package/console/dist/server/models/GovernanceConsoleModel.js +28 -3
  4. package/console/dist/server/routes/failed-tasks.d.ts +9 -5
  5. package/console/dist/server/routes/failed-tasks.js +220 -8
  6. package/console/dist/ui/api.d.ts +4 -3
  7. package/console/dist/ui/api.js +14 -2
  8. package/console/dist/ui/i18n/en.json +12 -0
  9. package/console/dist/ui/i18n/zh-CN.json +12 -0
  10. package/console/dist/ui/pages/failed-tasks/FailedTasksPage.js +78 -5
  11. package/console/dist/ui/pages/focus/FocusPage.js +5 -1
  12. package/console/dist/ui/utils/validators.d.ts +11 -0
  13. package/console/dist/ui/utils/validators.js +32 -0
  14. package/console/dist/web/assets/app.js +169 -6
  15. package/core/dist/runtime-v2/__tests__/owner-retry.test.d.ts +2 -0
  16. package/core/dist/runtime-v2/__tests__/owner-retry.test.d.ts.map +1 -0
  17. package/core/dist/runtime-v2/__tests__/owner-retry.test.js +144 -0
  18. package/core/dist/runtime-v2/__tests__/owner-retry.test.js.map +1 -0
  19. package/core/dist/runtime-v2/__tests__/pain-diagnosis-persistence.test.d.ts +2 -0
  20. package/core/dist/runtime-v2/__tests__/pain-diagnosis-persistence.test.d.ts.map +1 -0
  21. package/core/dist/runtime-v2/__tests__/pain-diagnosis-persistence.test.js +293 -0
  22. package/core/dist/runtime-v2/__tests__/pain-diagnosis-persistence.test.js.map +1 -0
  23. package/core/dist/runtime-v2/__tests__/recovery-actions-log.test.d.ts +2 -0
  24. package/core/dist/runtime-v2/__tests__/recovery-actions-log.test.d.ts.map +1 -0
  25. package/core/dist/runtime-v2/__tests__/recovery-actions-log.test.js +112 -0
  26. package/core/dist/runtime-v2/__tests__/recovery-actions-log.test.js.map +1 -0
  27. package/core/dist/runtime-v2/diagnostician/rootcause-prompt-builder.d.ts +10 -0
  28. package/core/dist/runtime-v2/diagnostician/rootcause-prompt-builder.d.ts.map +1 -1
  29. package/core/dist/runtime-v2/diagnostician/rootcause-prompt-builder.js +40 -4
  30. package/core/dist/runtime-v2/diagnostician/rootcause-prompt-builder.js.map +1 -1
  31. package/core/dist/runtime-v2/diagnostician-prompt-builder.d.ts +6 -0
  32. package/core/dist/runtime-v2/diagnostician-prompt-builder.d.ts.map +1 -1
  33. package/core/dist/runtime-v2/diagnostician-prompt-builder.js.map +1 -1
  34. package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js +28 -0
  35. package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js.map +1 -1
  36. package/core/dist/runtime-v2/feature-flags/__tests__/pain-diagnosis-persistence-flag.test.d.ts +2 -0
  37. package/core/dist/runtime-v2/feature-flags/__tests__/pain-diagnosis-persistence-flag.test.d.ts.map +1 -0
  38. package/core/dist/runtime-v2/feature-flags/__tests__/pain-diagnosis-persistence-flag.test.js +44 -0
  39. package/core/dist/runtime-v2/feature-flags/__tests__/pain-diagnosis-persistence-flag.test.js.map +1 -0
  40. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
  41. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +18 -0
  42. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  43. package/core/dist/runtime-v2/index.d.ts +5 -0
  44. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  45. package/core/dist/runtime-v2/index.js +5 -0
  46. package/core/dist/runtime-v2/index.js.map +1 -1
  47. package/core/dist/runtime-v2/internalization/diag-rootcause-runner.d.ts.map +1 -1
  48. package/core/dist/runtime-v2/internalization/diag-rootcause-runner.js +5 -0
  49. package/core/dist/runtime-v2/internalization/diag-rootcause-runner.js.map +1 -1
  50. package/core/dist/runtime-v2/internalization/owner-retry.d.ts +49 -0
  51. package/core/dist/runtime-v2/internalization/owner-retry.d.ts.map +1 -0
  52. package/core/dist/runtime-v2/internalization/owner-retry.js +51 -0
  53. package/core/dist/runtime-v2/internalization/owner-retry.js.map +1 -0
  54. package/core/dist/runtime-v2/pain-signal-bridge.d.ts +19 -0
  55. package/core/dist/runtime-v2/pain-signal-bridge.d.ts.map +1 -1
  56. package/core/dist/runtime-v2/pain-signal-bridge.js +61 -0
  57. package/core/dist/runtime-v2/pain-signal-bridge.js.map +1 -1
  58. package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts +12 -0
  59. package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts.map +1 -1
  60. package/core/dist/runtime-v2/pain-signal-runtime-factory.js +48 -2
  61. package/core/dist/runtime-v2/pain-signal-runtime-factory.js.map +1 -1
  62. package/core/dist/runtime-v2/recovery-actions-log.d.ts +46 -0
  63. package/core/dist/runtime-v2/recovery-actions-log.d.ts.map +1 -0
  64. package/core/dist/runtime-v2/recovery-actions-log.js +116 -0
  65. package/core/dist/runtime-v2/recovery-actions-log.js.map +1 -0
  66. package/core/dist/runtime-v2/recovery-sweep-service.d.ts +8 -0
  67. package/core/dist/runtime-v2/recovery-sweep-service.d.ts.map +1 -1
  68. package/core/dist/runtime-v2/recovery-sweep-service.js +4 -0
  69. package/core/dist/runtime-v2/recovery-sweep-service.js.map +1 -1
  70. package/core/dist/runtime-v2/store/pain-diagnosis/pain-diagnosis-store.d.ts +77 -0
  71. package/core/dist/runtime-v2/store/pain-diagnosis/pain-diagnosis-store.d.ts.map +1 -0
  72. package/core/dist/runtime-v2/store/pain-diagnosis/pain-diagnosis-store.js +40 -0
  73. package/core/dist/runtime-v2/store/pain-diagnosis/pain-diagnosis-store.js.map +1 -0
  74. package/core/dist/runtime-v2/store/pain-diagnosis/sqlite-pain-diagnosis-store.d.ts +9 -0
  75. package/core/dist/runtime-v2/store/pain-diagnosis/sqlite-pain-diagnosis-store.d.ts.map +1 -0
  76. package/core/dist/runtime-v2/store/pain-diagnosis/sqlite-pain-diagnosis-store.js +118 -0
  77. package/core/dist/runtime-v2/store/pain-diagnosis/sqlite-pain-diagnosis-store.js.map +1 -0
  78. package/core/dist/runtime-v2/store/runtime-state-manager.d.ts +11 -0
  79. package/core/dist/runtime-v2/store/runtime-state-manager.d.ts.map +1 -1
  80. package/core/dist/runtime-v2/store/runtime-state-manager.js +18 -0
  81. package/core/dist/runtime-v2/store/runtime-state-manager.js.map +1 -1
  82. package/core/dist/runtime-v2/store/sqlite-connection.d.ts.map +1 -1
  83. package/core/dist/runtime-v2/store/sqlite-connection.js +23 -0
  84. package/core/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
  85. package/host-runtime/dist/production-pain-evidence.js +9 -2
  86. package/package.json +1 -1
  87. package/pd-cli/dist/commands/runtime-internalization-retry.d.ts.map +1 -1
  88. package/pd-cli/dist/commands/runtime-internalization-retry.js +32 -19
  89. package/pd-cli/dist/commands/runtime-internalization-retry.js.map +1 -1
  90. package/plugin/dist/bundle.js +438 -415
@@ -11,6 +11,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
11
11
  * Each row exposes a "Create Feedback Draft" button that deep-links into the
12
12
  * Report Problem page with the task/pain context pre-filled.
13
13
  *
14
+ * Governance Recovery Actions v1: when the `failed_task_recovery_console`
15
+ * feature flag is enabled, each row also exposes a "Resume Evolution" button.
16
+ * Recovery is an Owner governance action (never auto-triggered on load): it
17
+ * always goes through an explicit confirmation dialog before POSTing
18
+ * /api/v1/failed-tasks/:id/recover. The success state means "Recovery
19
+ * Accepted" — the task is pending again; execution is asynchronous.
20
+ *
14
21
  * Runtime Contract (rc-*):
15
22
  * - rc-1: API response is treated as `unknown` and validated before use
16
23
  * - rc-2: no `as` type assertions — runtime type guards only
@@ -20,13 +27,16 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
20
27
  import { useState, useEffect, useCallback } from "react";
21
28
  import { useNavigate } from "react-router-dom";
22
29
  import { useTranslation } from "react-i18next";
30
+ import { toast } from "sonner";
23
31
  import { PageShell } from "../../components/layout/page-shell.js";
24
32
  import { PageLoading } from "../../components/layout/page-loading.js";
25
33
  import { Badge } from "../../components/ui/badge.js";
26
34
  import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card.js";
27
35
  import { Button } from "../../components/ui/button.js";
36
+ import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel, } from "../../components/ui/alert-dialog.js";
28
37
  import { ShinyText } from "../../components/ui/shiny-text.js";
29
38
  import { formatDate } from "../../utils/format.js";
39
+ import { recoverFailedTask, fetchConfigSummary } from "../../api.js";
30
40
  // ── Runtime validators (rc-1, rc-2, rc-5) ───────────────────────────────────
31
41
  //
32
42
  // The API response is `unknown` until these guards run. No `as` casts.
@@ -133,6 +143,30 @@ export function FailedTasksPage() {
133
143
  const { t } = useTranslation();
134
144
  const navigate = useNavigate();
135
145
  const [state, setState] = useState({ status: "loading" });
146
+ // Governance Recovery Actions v1 — flag-gated recovery UI (fail-closed:
147
+ // absent/false → no recovery entry, Console stays read-only).
148
+ const [recoveryEnabled, setRecoveryEnabled] = useState(false);
149
+ const [recoverTarget, setRecoverTarget] = useState(null);
150
+ const [recoverLoading, setRecoverLoading] = useState(false);
151
+ useEffect(() => {
152
+ let cancelled = false;
153
+ fetchConfigSummary()
154
+ .then((result) => {
155
+ if (cancelled)
156
+ return;
157
+ // Fail-closed: only an explicitly enabled flag shows the action
158
+ if (result.success && result.data) {
159
+ const flag = result.data.features.find((f) => f.id === "failed_task_recovery_console");
160
+ setRecoveryEnabled(flag?.enabled === true);
161
+ }
162
+ })
163
+ .catch(() => {
164
+ // rc-9: flag probe failure keeps recovery hidden (fail-closed)
165
+ });
166
+ return () => {
167
+ cancelled = true;
168
+ };
169
+ }, []);
136
170
  const loadData = useCallback(async () => {
137
171
  setState({ status: "loading" });
138
172
  try {
@@ -205,9 +239,48 @@ export function FailedTasksPage() {
205
239
  }
206
240
  navigate(`/report-problem?${params.toString()}`);
207
241
  };
208
- return (_jsxs(PageShell, { children: [_jsxs("div", { className: "mb-8", children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.14em] text-ink-3", children: t("pages.failedTasks.title") }), _jsx(ShinyText, { as: "h1", className: "text-[29px] font-semibold tracking-tight text-ink mt-3 mb-2", duration: 4.5, brightness: 0.5, disabled: state.status !== "loaded" || state.data.tasks.length === 0, children: t("pages.failedTasks.title") }), _jsx("p", { className: "text-ink-3 text-sm leading-relaxed max-w-[712px]", children: t("pages.failedTasks.subtitle") })] }), state.status === "loading" && (_jsx(PageLoading, { cardCount: 3, label: t("common.loading") })), state.status === "disabled" && (_jsxs("div", { className: "py-8", children: [_jsx("div", { className: "p-5 bg-panel border border-amber/20 rounded-[var(--radius-md)] text-amber text-[13px] leading-relaxed", children: t("pages.failedTasks.disabled") }), _jsx("div", { className: "mt-4", children: _jsx(Button, { variant: "outline", size: "sm", onClick: loadData, children: t("common.refresh") }) })] })), state.status === "error" && (_jsxs("div", { className: "py-8", children: [_jsxs("div", { className: "text-danger text-sm mb-2", children: [t("pages.failedTasks.error"), ": ", state.message] }), state.nextAction && (_jsx("div", { className: "text-ink-3 text-[13px] mb-3", children: state.nextAction })), _jsx(Button, { variant: "outline", size: "sm", onClick: loadData, children: t("common.refresh") })] })), state.status === "loaded" && (_jsx("div", { className: "animate-[pdFadeIn_400ms_ease-out]", children: _jsx(LoadedContent, { data: state.data, onCreateDraft: handleCreateDraft, t: t }) }))] }));
242
+ // Owner-confirmed recovery (SPEC §6.3: explicit confirmation only — this
243
+ // handler runs exclusively from the dialog's action button, never on load).
244
+ const handleRecoverConfirmed = async () => {
245
+ if (!recoverTarget || recoverLoading)
246
+ return;
247
+ setRecoverLoading(true);
248
+ try {
249
+ const result = await recoverFailedTask(recoverTarget.taskId);
250
+ if (result.success) {
251
+ toast.success(t("pages.failedTasks.recoverSuccess"));
252
+ setRecoverTarget(null);
253
+ await loadData();
254
+ }
255
+ else {
256
+ // rc-9: server-provided reason + next action accompany the failure
257
+ const detail = result.nextAction
258
+ ? `${result.error} — ${result.nextAction}`
259
+ : result.error;
260
+ toast.error(`${t("pages.failedTasks.recoverFailed")}: ${detail}`);
261
+ }
262
+ }
263
+ catch (err) {
264
+ toast.error(`${t("pages.failedTasks.recoverFailed")}: ${err instanceof Error ? err.message : "Network error"}`);
265
+ }
266
+ finally {
267
+ setRecoverLoading(false);
268
+ }
269
+ };
270
+ return (_jsxs(PageShell, { children: [_jsxs("div", { className: "mb-8", children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.14em] text-ink-3", children: t("pages.failedTasks.title") }), _jsx(ShinyText, { as: "h1", className: "text-[29px] font-semibold tracking-tight text-ink mt-3 mb-2", duration: 4.5, brightness: 0.5, disabled: state.status !== "loaded" || state.data.tasks.length === 0, children: t("pages.failedTasks.title") }), _jsx("p", { className: "text-ink-3 text-sm leading-relaxed max-w-[712px]", children: t("pages.failedTasks.subtitle") })] }), state.status === "loading" && (_jsx(PageLoading, { cardCount: 3, label: t("common.loading") })), state.status === "disabled" && (_jsxs("div", { className: "py-8", children: [_jsx("div", { className: "p-5 bg-panel border border-amber/20 rounded-[var(--radius-md)] text-amber text-[13px] leading-relaxed", children: t("pages.failedTasks.disabled") }), _jsx("div", { className: "mt-4", children: _jsx(Button, { variant: "outline", size: "sm", onClick: loadData, children: t("common.refresh") }) })] })), state.status === "error" && (_jsxs("div", { className: "py-8", children: [_jsxs("div", { className: "text-danger text-sm mb-2", children: [t("pages.failedTasks.error"), ": ", state.message] }), state.nextAction && (_jsx("div", { className: "text-ink-3 text-[13px] mb-3", children: state.nextAction })), _jsx(Button, { variant: "outline", size: "sm", onClick: loadData, children: t("common.refresh") })] })), state.status === "loaded" && (_jsx("div", { className: "animate-[pdFadeIn_400ms_ease-out]", children: _jsx(LoadedContent, { data: state.data, onCreateDraft: handleCreateDraft, onRecover: recoveryEnabled ? (task) => setRecoverTarget(task) : undefined, t: t }) })), _jsx(AlertDialog, { open: recoverTarget !== null, onOpenChange: (open) => {
271
+ if (!open && !recoverLoading) {
272
+ setRecoverTarget(null);
273
+ }
274
+ }, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: t("pages.failedTasks.recoverConfirmTitle") }), _jsx(AlertDialogDescription, { asChild: true, children: _jsxs("div", { className: "space-y-2 text-[13px] leading-relaxed", children: [_jsxs("div", { children: [_jsxs("span", { className: "font-medium", children: [t("pages.failedTasks.recoverConfirmStatus"), ":"] }), " ", _jsx("span", { className: "font-mono", children: recoverTarget?.status ?? "—" })] }), _jsxs("div", { children: [_jsxs("span", { className: "font-medium", children: [t("pages.failedTasks.recoverConfirmActionLabel"), ":"] }), " ", t("pages.failedTasks.recoverConfirmActionDesc")] }), _jsxs("div", { children: [_jsxs("span", { className: "font-medium", children: [t("pages.failedTasks.recoverConfirmImpactLabel"), ":"] }), " ", t("pages.failedTasks.recoverConfirmImpactDesc")] })] }) })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: recoverLoading, children: t("common.cancel") }), _jsx(AlertDialogAction, { disabled: recoverLoading, onClick: (event) => {
275
+ // Prevent Radix from auto-closing before the async POST
276
+ // resolves; handleRecoverConfirmed closes on success.
277
+ event.preventDefault();
278
+ void handleRecoverConfirmed();
279
+ }, children: recoverLoading
280
+ ? t("common.loading")
281
+ : t("pages.failedTasks.recoverConfirmButton") })] })] }) })] }));
209
282
  }
210
- function LoadedContent({ data, onCreateDraft, t }) {
283
+ function LoadedContent({ data, onCreateDraft, onRecover, t }) {
211
284
  const hasTasks = data.tasks.length > 0;
212
285
  // Empty state — surface nextAction from the API (rc-9: no silent fallback)
213
286
  if (!hasTasks) {
@@ -219,11 +292,11 @@ function LoadedContent({ data, onCreateDraft, t }) {
219
292
  const tasks = grouped.get(kind);
220
293
  if (!tasks || tasks.length === 0)
221
294
  return null;
222
- return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: "outline", children: t("pages.failedTasks.kind") }), _jsxs(CardTitle, { className: "text-[15px] font-mono", children: [kind, " (", tasks.length, ")"] })] }) }), _jsx(CardContent, { children: _jsx(TaskTable, { tasks: tasks, onCreateDraft: onCreateDraft, t: t }) })] }, kind));
295
+ return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: "outline", children: t("pages.failedTasks.kind") }), _jsxs(CardTitle, { className: "text-[15px] font-mono", children: [kind, " (", tasks.length, ")"] })] }) }), _jsx(CardContent, { children: _jsx(TaskTable, { tasks: tasks, onCreateDraft: onCreateDraft, onRecover: onRecover, t: t }) })] }, kind));
223
296
  }) }), _jsxs("div", { className: "mt-8 font-mono text-[11px] text-ink-4 space-y-1", children: [_jsxs("div", { children: [t("common.total"), ": ", data.total] }), data.total > data.tasks.length && (_jsx("div", { className: "text-amber/80 leading-relaxed", children: t("pages.failedTasks.partialResults")
224
297
  .replace("{shown}", String(data.tasks.length))
225
298
  .replace("{total}", String(data.total)) }))] })] }));
226
299
  }
227
- function TaskTable({ tasks, onCreateDraft, t }) {
228
- return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto_minmax(0,1fr)_auto_minmax(0,1fr)_auto] gap-3 pb-2 border-b border-line font-mono text-[10px] uppercase tracking-[0.08em] text-ink-4", children: [_jsx("span", { children: t("pages.failedTasks.taskId") }), _jsx("span", { children: t("pages.failedTasks.painId") }), _jsx("span", { children: t("pages.failedTasks.status") }), _jsx("span", { children: t("pages.failedTasks.lastError") }), _jsx("span", { children: t("pages.failedTasks.attempts") }), _jsx("span", { children: t("pages.failedTasks.lastAttempt") }), _jsx("span", { "aria-hidden": "true" })] }), tasks.map((task) => (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto_minmax(0,1fr)_auto_minmax(0,1fr)_auto] gap-3 py-2 items-center text-[13px] border-b border-line/50 last:border-b-0", children: [_jsx("span", { className: "font-mono text-ink-2 truncate", title: task.taskId, children: truncateId(task.taskId) }), _jsx("span", { className: "font-mono text-ink-3 truncate", title: task.painId ?? "", children: task.painId ? truncateId(task.painId) : t("pages.failedTasks.noPainId") }), _jsx(Badge, { variant: statusToVariant(task.status), children: task.status }), _jsx("span", { className: "text-ink-3 truncate", title: task.lastError ?? "", children: task.lastError ?? "—" }), _jsx("span", { className: "font-mono text-ink-2 text-center", children: task.attemptCount }), _jsx("span", { className: "font-mono text-ink-3 text-[12px]", children: task.lastAttemptAt ? formatDate(task.lastAttemptAt) : "—" }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => onCreateDraft(task), className: "whitespace-nowrap", children: t("pages.failedTasks.createDraft") })] }, task.taskId)))] }));
300
+ function TaskTable({ tasks, onCreateDraft, onRecover, t }) {
301
+ return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto_minmax(0,1fr)_auto_minmax(0,1fr)_auto] gap-3 pb-2 border-b border-line font-mono text-[10px] uppercase tracking-[0.08em] text-ink-4", children: [_jsx("span", { children: t("pages.failedTasks.taskId") }), _jsx("span", { children: t("pages.failedTasks.painId") }), _jsx("span", { children: t("pages.failedTasks.status") }), _jsx("span", { children: t("pages.failedTasks.lastError") }), _jsx("span", { children: t("pages.failedTasks.attempts") }), _jsx("span", { children: t("pages.failedTasks.lastAttempt") }), _jsx("span", { "aria-hidden": "true" })] }), tasks.map((task) => (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto_minmax(0,1fr)_auto_minmax(0,1fr)_auto] gap-3 py-2 items-center text-[13px] border-b border-line/50 last:border-b-0", children: [_jsx("span", { className: "font-mono text-ink-2 truncate", title: task.taskId, children: truncateId(task.taskId) }), _jsx("span", { className: "font-mono text-ink-3 truncate", title: task.painId ?? "", children: task.painId ? truncateId(task.painId) : t("pages.failedTasks.noPainId") }), _jsx(Badge, { variant: statusToVariant(task.status), children: task.status }), _jsx("span", { className: "text-ink-3 truncate", title: task.lastError ?? "", children: task.lastError ?? "—" }), _jsx("span", { className: "font-mono text-ink-2 text-center", children: task.attemptCount }), _jsx("span", { className: "font-mono text-ink-3 text-[12px]", children: task.lastAttemptAt ? formatDate(task.lastAttemptAt) : "—" }), onRecover && (_jsx(Button, { variant: "default", size: "sm", onClick: () => onRecover(task), className: "whitespace-nowrap", children: t("pages.failedTasks.recover") })), _jsx(Button, { variant: "outline", size: "sm", onClick: () => onCreateDraft(task), className: "whitespace-nowrap", children: t("pages.failedTasks.createDraft") })] }, task.taskId)))] }));
229
302
  }
@@ -426,7 +426,11 @@ export function FocusPage() {
426
426
  const ruleCodePending = ruleCodeItems.filter(item => item.action === 'code_tool_hook_shadow_activate' && item.enforcement !== 'safety_isolated');
427
427
  const ruleCodeAlerts = ruleCodeItems.filter(item => item.enforcement === 'safety_isolated');
428
428
  // Map codes to i18n text
429
- const stateReason = getStateReasonText(stateReasonCode, t, pendingCount);
429
+ // Governance Recovery Actions v1: tasks_need_human_review interpolates the
430
+ // needs_human_review task count, not the approvals count.
431
+ const humanReviewCount = queueData?.pendingHumanReviewCount ?? 0;
432
+ const stateReasonCount = stateReasonCode === "tasks_need_human_review" ? humanReviewCount : pendingCount;
433
+ const stateReason = getStateReasonText(stateReasonCode, t, stateReasonCount);
430
434
  const nextAction = getNextActionText(nextActionCode, t);
431
435
  return (_jsx(PageShell, { children: _jsxs("div", { className: "animate-[pdFadeIn_400ms_ease-out]", children: [_jsx("div", { className: "font-mono text-[12px] tracking-[0.14em] text-ink-3 uppercase mb-3", children: t("pages.focus.eyebrow") }), _jsx(ShinyText, { as: "h1", className: "text-[29px] font-semibold tracking-tight text-ink mb-2", duration: 4.5, brightness: 0.5, disabled: pendingCount === 0, children: t("pages.focus.title") }), _jsx("p", { className: "text-ink-3 text-[14px] max-w-[760px] leading-relaxed mb-7", children: t("pages.focus.subtitle") }), _jsxs("div", { className: "mb-7 px-[18px] py-[14px] bg-panel border border-line rounded-[6px]", children: [_jsx("div", { className: "flex items-center gap-2 mb-1", children: _jsx("span", { className: `inline-flex items-center rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase ${governanceState === "owner_review_ready"
432
436
  ? "bg-gov/10 text-gov border border-gov/20"
@@ -219,6 +219,8 @@ export interface DegradedSignalData {
219
219
  export interface GovernanceQueueData {
220
220
  pendingReviewCount: number;
221
221
  behaviorDeviationCount: number;
222
+ /** Governance Recovery Actions v1: needs_human_review internalization tasks (owner-attention items) */
223
+ pendingHumanReviewCount?: number;
222
224
  stagnationSignals: StagnationSignalData[];
223
225
  governanceState: 'none' | 'in_progress' | 'owner_review_ready' | 'degraded';
224
226
  stateReasonCode: string;
@@ -232,6 +234,15 @@ export interface GovernanceQueueData {
232
234
  note?: string;
233
235
  generatedAt?: string;
234
236
  }
237
+ export interface RecoveryResultData {
238
+ taskId: string;
239
+ previousStatus: string;
240
+ newStatus: string;
241
+ /** 'recovered' (failed→pending) | 'requeued' (needs_human_review→pending) */
242
+ result: string;
243
+ nextAction?: string;
244
+ }
245
+ export declare function validateRecoveryResult(v: unknown): RecoveryResultData | null;
235
246
  export declare function validateGovernanceQueue(v: unknown): GovernanceQueueData | null;
236
247
  export interface ActivationRecordData {
237
248
  activationId: string;
@@ -596,10 +596,12 @@ function validateStagnationSignal(v) {
596
596
  const VALID_GOVERNANCE_STATES = new Set(['none', 'in_progress', 'owner_review_ready', 'degraded']);
597
597
  const VALID_STATE_REASON_CODES = new Set([
598
598
  'state_db_missing', 'no_pipeline_activity', 'pending_approvals',
599
+ 'tasks_need_human_review',
599
600
  'pipeline_active', 'consumed_candidates', 'degraded_state',
600
601
  ]);
601
602
  const VALID_NEXT_ACTION_CODES = new Set([
602
603
  'run_config_doctor', 'wait_for_pipeline', 'review_approvals',
604
+ 'review_failed_tasks',
603
605
  'check_degraded_signals', 'check_pipeline_status',
604
606
  ]);
605
607
  const VALID_DEGRADED_REASON_CODES = new Set(['task_retry_wait', 'task_failed', 'approval_table_missing', 'trajectory_db_unavailable']);
@@ -629,6 +631,30 @@ function validateDegradedSignal(v) {
629
631
  source: v.source,
630
632
  };
631
633
  }
634
+ export function validateRecoveryResult(v) {
635
+ if (!isObject(v))
636
+ return null;
637
+ if (!Object.hasOwn(v, 'taskId') || !isString(v.taskId))
638
+ return null;
639
+ if (!Object.hasOwn(v, 'previousStatus') || !isString(v.previousStatus))
640
+ return null;
641
+ if (!Object.hasOwn(v, 'newStatus') || !isString(v.newStatus))
642
+ return null;
643
+ if (!Object.hasOwn(v, 'result') || !isString(v.result))
644
+ return null;
645
+ const result = {
646
+ taskId: v.taskId,
647
+ previousStatus: v.previousStatus,
648
+ newStatus: v.newStatus,
649
+ result: v.result,
650
+ };
651
+ if (Object.hasOwn(v, 'nextAction')) {
652
+ if (!isString(v.nextAction))
653
+ return null;
654
+ result.nextAction = v.nextAction;
655
+ }
656
+ return result;
657
+ }
632
658
  export function validateGovernanceQueue(v) {
633
659
  if (!isObject(v))
634
660
  return null;
@@ -699,6 +725,12 @@ export function validateGovernanceQueue(v) {
699
725
  return null;
700
726
  result.evidenceInProgressCount = v.evidenceInProgressCount;
701
727
  }
728
+ // Governance Recovery Actions v1: needs_human_review task count
729
+ if (Object.hasOwn(v, 'pendingHumanReviewCount')) {
730
+ if (!isNumber(v.pendingHumanReviewCount))
731
+ return null;
732
+ result.pendingHumanReviewCount = v.pendingHumanReviewCount;
733
+ }
702
734
  // Wave 4: gate blocks today (seconds-level auto-blocks)
703
735
  if (Object.hasOwn(v, 'gateBlocksToday')) {
704
736
  if (!isNumber(v.gateBlocksToday))