create-principles-disciple 1.136.0 → 1.137.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/codex-adapter/dist/worker/workspace-worker.js +3 -0
  2. package/console/dist/server/routes/failed-tasks.js +31 -3
  3. package/console/dist/ui/api.d.ts +4 -3
  4. package/console/dist/ui/api.js +9 -2
  5. package/console/dist/ui/i18n/en.json +12 -1
  6. package/console/dist/ui/i18n/zh-CN.json +12 -1
  7. package/console/dist/ui/pages/failed-tasks/FailedTasksPage.js +80 -4
  8. package/console/dist/ui/pages/pain/PainPage.js +13 -3
  9. package/console/dist/ui/utils/validators.d.ts +25 -0
  10. package/console/dist/ui/utils/validators.js +87 -0
  11. package/console/dist/web/assets/app.css +29 -2
  12. package/console/dist/web/assets/app.js +259 -46
  13. package/core/dist/runtime-v2/index.d.ts +2 -1
  14. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  15. package/core/dist/runtime-v2/index.js +2 -0
  16. package/core/dist/runtime-v2/index.js.map +1 -1
  17. package/core/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder.test.js +75 -5
  18. package/core/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder.test.js.map +1 -1
  19. package/core/dist/runtime-v2/internalization/__tests__/evaluator-host-alias-replay.test.d.ts +2 -0
  20. package/core/dist/runtime-v2/internalization/__tests__/evaluator-host-alias-replay.test.d.ts.map +1 -0
  21. package/core/dist/runtime-v2/internalization/__tests__/evaluator-host-alias-replay.test.js +279 -0
  22. package/core/dist/runtime-v2/internalization/__tests__/evaluator-host-alias-replay.test.js.map +1 -0
  23. package/core/dist/runtime-v2/internalization/__tests__/tool-semantic-registry.test.js +43 -0
  24. package/core/dist/runtime-v2/internalization/__tests__/tool-semantic-registry.test.js.map +1 -1
  25. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts +36 -2
  26. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts.map +1 -1
  27. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js +64 -7
  28. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js.map +1 -1
  29. package/core/dist/runtime-v2/internalization/artificer-runner.d.ts +10 -1
  30. package/core/dist/runtime-v2/internalization/artificer-runner.d.ts.map +1 -1
  31. package/core/dist/runtime-v2/internalization/artificer-runner.js +6 -0
  32. package/core/dist/runtime-v2/internalization/artificer-runner.js.map +1 -1
  33. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts +31 -0
  34. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
  35. package/core/dist/runtime-v2/internalization/evaluator-runner.js +93 -1
  36. package/core/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
  37. package/core/dist/runtime-v2/internalization/index.d.ts +2 -1
  38. package/core/dist/runtime-v2/internalization/index.d.ts.map +1 -1
  39. package/core/dist/runtime-v2/internalization/index.js +2 -0
  40. package/core/dist/runtime-v2/internalization/index.js.map +1 -1
  41. package/core/dist/runtime-v2/internalization/tool-semantic-registry.d.ts +7 -0
  42. package/core/dist/runtime-v2/internalization/tool-semantic-registry.d.ts.map +1 -1
  43. package/core/dist/runtime-v2/internalization/tool-semantic-registry.js +9 -0
  44. package/core/dist/runtime-v2/internalization/tool-semantic-registry.js.map +1 -1
  45. package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.d.ts.map +1 -1
  46. package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.js +6 -0
  47. package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.js.map +1 -1
  48. package/core/dist/telemetry-event.d.ts +2 -2
  49. package/core/dist/telemetry-event.d.ts.map +1 -1
  50. package/core/dist/telemetry-event.js +4 -0
  51. package/core/dist/telemetry-event.js.map +1 -1
  52. package/host-runtime/dist/evaluator-runtime-context.d.ts +10 -0
  53. package/host-runtime/dist/evaluator-runtime-context.js +3 -0
  54. package/host-runtime/dist/internalization-consumer-cycle.d.ts +6 -0
  55. package/host-runtime/dist/internalization-consumer-cycle.js +14 -2
  56. package/package.json +2 -2
  57. package/pd-cli/dist/commands/pain-record.d.ts +6 -0
  58. package/pd-cli/dist/commands/pain-record.d.ts.map +1 -1
  59. package/pd-cli/dist/commands/pain-record.js +78 -29
  60. package/pd-cli/dist/commands/pain-record.js.map +1 -1
  61. package/pd-cli/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  62. package/pd-cli/dist/commands/runtime-internalization-run-once.js +29 -2
  63. package/pd-cli/dist/commands/runtime-internalization-run-once.js.map +1 -1
  64. package/pd-cli/dist/index.js +4 -1
  65. package/pd-cli/dist/index.js.map +1 -1
  66. package/pd-cli/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  67. package/pd-cli/dist/services/rulehost-pipeline-runner.js +18 -3
  68. package/pd-cli/dist/services/rulehost-pipeline-runner.js.map +1 -1
  69. package/pd-cli/package.json +1 -1
  70. package/plugin/dist/bundle.js +459 -449
  71. package/plugin/dist/constants/tools.d.ts +13 -5
  72. package/plugin/dist/governance-audit.js +3 -3
  73. package/plugin/dist/rulehost-evidence.js +127 -127
  74. package/plugin/dist/service/internalization-auto-consumer-service.d.ts +13 -0
  75. package/plugin/openclaw.plugin.json +1 -1
  76. package/plugin/package.json +1 -1
  77. package/release-manager/package.json +2 -2
  78. package/console/dist/ui/utils/format.d.ts +0 -3
  79. package/console/dist/ui/utils/format.js +0 -20
@@ -213,6 +213,9 @@ export async function runCodexWorkspaceWorkerCycle(options) {
213
213
  logger,
214
214
  emitEvent,
215
215
  toolSemantics: CODEX_TOOL_SEMANTICS,
216
+ // PRI-741: artificer generation gets the Codex host projection
217
+ // (Bash/apply_patch — evidence-bound, intentionally sparse).
218
+ hostKinds: ['codex'],
216
219
  // No hostToolCatalog: PD has not declared a Codex tool catalog; a wrong
217
220
  // (OpenClaw) catalog would be worse than none (PRI-630 follow-up).
218
221
  });
@@ -30,6 +30,24 @@ const entries = new Map();
30
30
  function stateDbExists(workspaceDir) {
31
31
  return fs.existsSync(path.join(workspaceDir, '.pd', 'state.db'));
32
32
  }
33
+ /**
34
+ * Decode a percent-encoded path segment into the raw task id (PRI-747 F22
35
+ * review round). The UI client sends `encodeURIComponent(taskId)`; without
36
+ * this decode the store lookup would compare the encoded string ('task%2Fwith
37
+ * %20spaces') against raw ids and 404. Malformed sequences ('%ZZ') are
38
+ * rejected as invalid_encoding rather than thrown (400, same shape as the
39
+ * lifecycle route's decode guard).
40
+ */
41
+ function decodeTaskIdSegment(raw) {
42
+ if (!raw.includes('%'))
43
+ return { ok: true, value: raw };
44
+ try {
45
+ return { ok: true, value: decodeURIComponent(raw) };
46
+ }
47
+ catch {
48
+ return { ok: false };
49
+ }
50
+ }
33
51
  function getStore(workspaceDir) {
34
52
  let entry = entries.get(workspaceDir);
35
53
  if (!entry) {
@@ -371,8 +389,13 @@ export async function handleFailedTasksRoute(req, res, ctx) {
371
389
  const recoverMatch = subPath.match(/^\/([^/]+)\/recover$/);
372
390
  if (recoverMatch) {
373
391
  // recoverMatch[1] = captured task id (segment between the leading '/'
374
- // and the trailing '/recover')
375
- const taskId = recoverMatch[1] ?? '';
392
+ // and the trailing '/recover'); percent-decoded before store lookup
393
+ const decodedTaskId = decodeTaskIdSegment(recoverMatch[1] ?? '');
394
+ if (!decodedTaskId.ok) {
395
+ sendError(res, 400, 'invalid_encoding', 'task id path segment is not valid percent-encoding');
396
+ return;
397
+ }
398
+ const taskId = decodedTaskId.value;
376
399
  // Fail-closed gate: this is a mutation endpoint — recovery is enabled by
377
400
  // default (2026-08-24 owner decision) and 403 only when explicitly disabled
378
401
  // via config (features.failed_task_recovery_console.enabled: false).
@@ -441,7 +464,12 @@ export async function handleFailedTasksRoute(req, res, ctx) {
441
464
  return;
442
465
  }
443
466
  // Detail: GET /api/v1/failed-tasks/:id
444
- const id = subPath.replace(/^\//, '');
467
+ const decodedId = decodeTaskIdSegment(subPath.replace(/^\//, ''));
468
+ if (!decodedId.ok) {
469
+ sendError(res, 400, 'invalid_encoding', 'task id path segment is not valid percent-encoding');
470
+ return;
471
+ }
472
+ const id = decodedId.value;
445
473
  if (id.length === 0) {
446
474
  sendNotFound(res, `Route /api/v1/failed-tasks${subPath} not found`);
447
475
  return;
@@ -3,7 +3,7 @@ import type { OwnerDecisionsData, OwnerResolutionResultData } from "./utils/vali
3
3
  import { listActiveSignalKeywords, listPendingSignalTerms, fetchKeywordStore, fetchPendingTerms, updateKeywordStore, admitPendingTerm, rejectPendingTerm } from "./utils/signal-keywords-api.js";
4
4
  import type { GovernanceExperienceSnapshot, OwnerGovernanceView } from '@principles/core/runtime-v2';
5
5
  import type { OwnerIdentityViewData, OwnerIdentityRegisterData, OwnerIdentityUnregisterData } from "./utils/validators.js";
6
- import type { FeedbackReportData, FeedbackDraftSummaryData, FeedbackDraftEnvelopeData, FeedbackChannelsData, FeedbackSubmitResultData, DeleteEnvelopeData, WorkspaceEntryData, RemovedEnvelopeData, SyncResultData, ConfigSummaryData, ConfigCatalogData, AgentBindingUpdateData, DefaultRuntimeUpdateData, RuntimeProfileMutationData, ReadinessCheckData, FeatureFlagUpdateData, OutputLanguageData, GovernanceQueueData, RecoveryResultData, ActivationsData, DisableActivationData, LifecycleMetricsData, UpdateStatusData, UpdateHistoryData, ApplyUpdateResultData, RollbackResultData, ApprovalRecordData, PrinciplesListData, CorePrinciplesData, ApprovalsGroupedData, EvidenceChainData, TrajectoryData, IntentSummaryData, IntentDecisionRecordData, IntentDecisionResultData, IntentDecisionSummaryData, FollowUpResponseData, IntentInitResultData, IntentSaveResultData, IntentRawContentData, IntentVersionData, RuleCodeOwnerReviewData, RuleCodeMutationData } from "./utils/validators.js";
6
+ import type { FeedbackReportData, FeedbackDraftSummaryData, FeedbackDraftEnvelopeData, FeedbackChannelsData, FeedbackSubmitResultData, DeleteEnvelopeData, WorkspaceEntryData, RemovedEnvelopeData, SyncResultData, ConfigSummaryData, ConfigCatalogData, AgentBindingUpdateData, DefaultRuntimeUpdateData, RuntimeProfileMutationData, ReadinessCheckData, FeatureFlagUpdateData, OutputLanguageData, GovernanceQueueData, RecoveryResultData, FailedTaskDetailData, ActivationsData, DisableActivationData, LifecycleMetricsData, UpdateStatusData, UpdateHistoryData, ApplyUpdateResultData, RollbackResultData, ApprovalRecordData, PrinciplesListData, CorePrinciplesData, ApprovalsGroupedData, EvidenceChainData, TrajectoryData, IntentSummaryData, IntentDecisionRecordData, IntentDecisionResultData, IntentDecisionSummaryData, FollowUpResponseData, IntentInitResultData, IntentSaveResultData, IntentRawContentData, IntentVersionData, RuleCodeOwnerReviewData, RuleCodeMutationData } from "./utils/validators.js";
7
7
  declare function getToken(): string | null;
8
8
  declare function setToken(token: string): void;
9
9
  declare function clearToken(): void;
@@ -161,6 +161,7 @@ declare function updateOutputLanguage(outputLanguage: string): Promise<ApiRespon
161
161
  declare function fetchGovernanceQueue(): Promise<ApiResponse<GovernanceQueueData>>;
162
162
  declare function fetchGovernanceExperience(): Promise<ApiResponse<GovernanceExperienceSnapshot>>;
163
163
  declare function recoverFailedTask(taskId: string, reason?: string, force?: boolean): Promise<ApiResponse<RecoveryResultData>>;
164
+ declare function fetchFailedTaskDetail(taskId: string): Promise<ApiResponse<FailedTaskDetailData>>;
164
165
  declare function fetchOwnerDecisions(): Promise<ApiResponse<OwnerDecisionsData>>;
165
166
  declare function resolveOwnerDecision(taskId: string, body: {
166
167
  action: 'accept_current' | 'revise_once' | 'reject_current';
@@ -242,8 +243,8 @@ export interface FollowUpPayload {
242
243
  candidateId?: string;
243
244
  }
244
245
  declare function dispatchFollowUp(decisionId: string, payload: FollowUpPayload): Promise<ApiResponse<FollowUpResponseData>>;
245
- export { request, listActiveSignalKeywords, listPendingSignalTerms, fetchKeywordStore, fetchPendingTerms, updateKeywordStore, admitPendingTerm, rejectPendingTerm, getToken, setToken, clearToken, checkAuth, approveApproval, rejectApproval, editApproval, fetchPrinciples, fetchCorePrinciples, fetchPrincipleDetail, fetchPrincipleGovernance, fetchPrincipleTrajectory, fetchPrincipleReceipts, fetchReceiptCounts, archivePrinciple, unarchivePrinciple, createFeedbackReport, listFeedbackReports, getFeedbackReport, deleteFeedbackReport, fetchFeedbackChannels, submitFeedbackReport, markFeedbackReportSent, fetchConfigSummary, fetchConfigCatalog, updateAgentBinding, updateDefaultRuntime, createRuntimeProfile, updateRuntimeProfile, deleteRuntimeProfile, fetchAgentReadiness, patchFeatureFlag, fetchOutputLanguage, updateOutputLanguage, fetchWorkspaces, addWorkspace, removeWorkspace, syncWorkspace, fetchGovernanceQueue, fetchGovernanceExperience, recoverFailedTask, fetchOwnerDecisions, resolveOwnerDecision, fetchApprovalsGrouped, fetchAllActivations, disableActivation, fetchRuleCodeOwnerReview, ruleCodeDecision, pauseAllRuleCode, releaseRuleCodePause, fetchLifecycleMetrics, fetchUpdateStatus, fetchUpdateHistory, applyUpdate, applyFullUpdate, rollbackUpdate, fetchEvidenceChain, fetchIntentSummary, fetchIntentContent, createIntentTemplate, saveIntentContent, fetchIntentVersions, recordIntentDecision, listIntentDecisionsByPainId, listIntentDecisionsByTaskId, fetchIntentDecisionSummary, dispatchFollowUp, };
246
- export type { FeedbackReportData, FeedbackDraftSummaryData, FeedbackDraftEnvelopeData, FeedbackChannelsData, FeedbackSubmitResultData, DeleteEnvelopeData, WorkspaceEntryData, RemovedEnvelopeData, SyncResultData, ConfigSummaryData, ConfigCatalogData, AgentBindingUpdateData, DefaultRuntimeUpdateData, RuntimeProfileMutationData, ReadinessCheckData, FeatureFlagUpdateData, OutputLanguageData, GovernanceQueueData, RecoveryResultData, ActivationsData, DisableActivationData, RuleCodeOwnerReviewData, RuleCodeMutationData, LifecycleMetricsData, UpdateStatusData, UpdateHistoryData, ApplyUpdateResultData, RollbackResultData, ApprovalRecordData, PrinciplesListData, CorePrinciplesData, CorePrincipleData, ApprovalsGroupedData, EvidenceChainData, EvidenceChainRecordData, EvidenceChainStateData, TrajectoryData, TrajectoryStageData, IntentSummaryData, IntentSectionsData, IntentDocWarningData, IntentDecisionRecordData, IntentDecisionResultData, IntentDecisionSummaryData, FollowUpResponseData, LinkCandidateFollowUpData, GuideRulehostFollowUpData, GeneratePatchProposalFollowUpData, IntentVersionEntry, IntentVersionData, } from "./utils/validators.js";
246
+ export { request, listActiveSignalKeywords, listPendingSignalTerms, fetchKeywordStore, fetchPendingTerms, updateKeywordStore, admitPendingTerm, rejectPendingTerm, getToken, setToken, clearToken, checkAuth, approveApproval, rejectApproval, editApproval, fetchPrinciples, fetchCorePrinciples, fetchPrincipleDetail, fetchPrincipleGovernance, fetchPrincipleTrajectory, fetchPrincipleReceipts, fetchReceiptCounts, archivePrinciple, unarchivePrinciple, createFeedbackReport, listFeedbackReports, getFeedbackReport, deleteFeedbackReport, fetchFeedbackChannels, submitFeedbackReport, markFeedbackReportSent, fetchConfigSummary, fetchConfigCatalog, updateAgentBinding, updateDefaultRuntime, createRuntimeProfile, updateRuntimeProfile, deleteRuntimeProfile, fetchAgentReadiness, patchFeatureFlag, fetchOutputLanguage, updateOutputLanguage, fetchWorkspaces, addWorkspace, removeWorkspace, syncWorkspace, fetchGovernanceQueue, fetchGovernanceExperience, recoverFailedTask, fetchFailedTaskDetail, fetchOwnerDecisions, resolveOwnerDecision, fetchApprovalsGrouped, fetchAllActivations, disableActivation, fetchRuleCodeOwnerReview, ruleCodeDecision, pauseAllRuleCode, releaseRuleCodePause, fetchLifecycleMetrics, fetchUpdateStatus, fetchUpdateHistory, applyUpdate, applyFullUpdate, rollbackUpdate, fetchEvidenceChain, fetchIntentSummary, fetchIntentContent, createIntentTemplate, saveIntentContent, fetchIntentVersions, recordIntentDecision, listIntentDecisionsByPainId, listIntentDecisionsByTaskId, fetchIntentDecisionSummary, dispatchFollowUp, };
247
+ export type { FeedbackReportData, FeedbackDraftSummaryData, FeedbackDraftEnvelopeData, FeedbackChannelsData, FeedbackSubmitResultData, DeleteEnvelopeData, WorkspaceEntryData, RemovedEnvelopeData, SyncResultData, ConfigSummaryData, ConfigCatalogData, AgentBindingUpdateData, DefaultRuntimeUpdateData, RuntimeProfileMutationData, ReadinessCheckData, FeatureFlagUpdateData, OutputLanguageData, GovernanceQueueData, RecoveryResultData, ActivationsData, DisableActivationData, RuleCodeOwnerReviewData, RuleCodeMutationData, FailedTaskDetailData, LifecycleMetricsData, UpdateStatusData, UpdateHistoryData, ApplyUpdateResultData, RollbackResultData, ApprovalRecordData, PrinciplesListData, CorePrinciplesData, CorePrincipleData, ApprovalsGroupedData, EvidenceChainData, EvidenceChainRecordData, EvidenceChainStateData, TrajectoryData, TrajectoryStageData, IntentSummaryData, IntentSectionsData, IntentDocWarningData, IntentDecisionRecordData, IntentDecisionResultData, IntentDecisionSummaryData, FollowUpResponseData, LinkCandidateFollowUpData, GuideRulehostFollowUpData, GeneratePatchProposalFollowUpData, IntentVersionEntry, IntentVersionData, } from "./utils/validators.js";
247
248
  export type { SignalKeyword, PendingSignalTerm } from "./utils/signal-keywords-types.js";
248
249
  export type { ActivationRecordData as ActivationRecord } from "./utils/validators.js";
249
250
  export type { ApprovalRecordData as ApprovalRecord } from "./utils/validators.js";
@@ -1,5 +1,5 @@
1
1
  import { listActiveSignalKeywords, listPendingSignalTerms, fetchKeywordStore, fetchPendingTerms, updateKeywordStore, admitPendingTerm, rejectPendingTerm, } from "./utils/signal-keywords-api.js";
2
- import { validateErrorResponse, validateHeaders, validateFeedbackReport, validateFeedbackDraftsList, validateFeedbackDraftEnvelope, validateFeedbackChannels, validateFeedbackSubmitResult, validateDeleteEnvelope, validateWorkspaceEntry, validateWorkspaceList, validateRemovedEnvelope, validateSyncResult, validateConfigSummary, validateConfigCatalog, validateAgentBindingUpdate, validateDefaultRuntimeUpdate, validateRuntimeProfileMutation, validateReadinessCheck, validateFeatureFlagUpdate, validateOutputLanguage, validateGovernanceQueue, validateRecoveryResult, validateOwnerDecisionsData, validateOwnerResolutionResult, validateActivations, validateDisableActivation, validateLifecycleMetrics, validateUpdateStatus, validateUpdateHistory, validateApplyUpdateResult, validateRollbackResult, validateApprovalRecordDirect, validatePrinciplesList, validateCorePrinciples, validateApprovalsGrouped, validateEvidenceChain, validateTrajectoryData, validateIntentSummary, validateIntentDecisionList, validateIntentDecisionResult, validateIntentDecisionSummary, validateFollowUpResponse, validateIntentInitResult, validateIntentSaveResult, validateIntentRawContent, validateIntentVersions, validateOwnerGovernanceView, validateGovernanceExperienceSnapshot, validateRuleCodeOwnerReview, validateRuleCodeMutation, validateOwnerIdentityView, validateOwnerIdentityRegister, validateOwnerIdentityUnregister, } from "./utils/validators.js";
2
+ import { validateErrorResponse, validateHeaders, validateFeedbackReport, validateFeedbackDraftsList, validateFeedbackDraftEnvelope, validateFeedbackChannels, validateFeedbackSubmitResult, validateDeleteEnvelope, validateWorkspaceEntry, validateWorkspaceList, validateRemovedEnvelope, validateSyncResult, validateConfigSummary, validateConfigCatalog, validateAgentBindingUpdate, validateDefaultRuntimeUpdate, validateRuntimeProfileMutation, validateReadinessCheck, validateFeatureFlagUpdate, validateOutputLanguage, validateGovernanceQueue, validateRecoveryResult, validateFailedTaskDetail, validateOwnerDecisionsData, validateOwnerResolutionResult, validateActivations, validateDisableActivation, validateLifecycleMetrics, validateUpdateStatus, validateUpdateHistory, validateApplyUpdateResult, validateRollbackResult, validateApprovalRecordDirect, validatePrinciplesList, validateCorePrinciples, validateApprovalsGrouped, validateEvidenceChain, validateTrajectoryData, validateIntentSummary, validateIntentDecisionList, validateIntentDecisionResult, validateIntentDecisionSummary, validateFollowUpResponse, validateIntentInitResult, validateIntentSaveResult, validateIntentRawContent, validateIntentVersions, validateOwnerGovernanceView, validateGovernanceExperienceSnapshot, validateRuleCodeOwnerReview, validateRuleCodeMutation, validateOwnerIdentityView, validateOwnerIdentityRegister, validateOwnerIdentityUnregister, } from "./utils/validators.js";
3
3
  // ── Auth ──────────────────────────────────────────────────────────────────────
4
4
  function getToken() {
5
5
  return sessionStorage.getItem("pd_token");
@@ -474,6 +474,13 @@ async function recoverFailedTask(taskId, reason, force) {
474
474
  body: JSON.stringify(body),
475
475
  }, validateRecoveryResult);
476
476
  }
477
+ // PRI-747 F22 / PRI-749: consume the existing single-task detail endpoint
478
+ // (task record + run history). The server route and
479
+ // SqliteTaskStore.getFailedTaskDetail already existed; this wrapper is the
480
+ // missing UI-side consumer the Failed Tasks page expands into.
481
+ async function fetchFailedTaskDetail(taskId) {
482
+ return request('/api/v1/failed-tasks/' + encodeURIComponent(taskId), undefined, validateFailedTaskDetail);
483
+ }
477
484
  // PRI-629: unified Owner Decision inbox — read projection + resolution.
478
485
  async function fetchOwnerDecisions() {
479
486
  return request('/api/v1/governance/owner-decisions', undefined, validateOwnerDecisionsData);
@@ -597,4 +604,4 @@ listActiveSignalKeywords, listPendingSignalTerms, fetchKeywordStore, fetchPendin
597
604
  // auth
598
605
  getToken, setToken, clearToken, checkAuth, approveApproval, rejectApproval, editApproval, fetchPrinciples, fetchCorePrinciples, fetchPrincipleDetail, fetchPrincipleGovernance, fetchPrincipleTrajectory, fetchPrincipleReceipts, fetchReceiptCounts, archivePrinciple, unarchivePrinciple, createFeedbackReport, listFeedbackReports, getFeedbackReport, deleteFeedbackReport,
599
606
  // Feedback submit ladder (Slice 3, spec §8)
600
- fetchFeedbackChannels, submitFeedbackReport, markFeedbackReportSent, fetchConfigSummary, fetchConfigCatalog, updateAgentBinding, updateDefaultRuntime, createRuntimeProfile, updateRuntimeProfile, deleteRuntimeProfile, fetchAgentReadiness, patchFeatureFlag, fetchOutputLanguage, updateOutputLanguage, fetchWorkspaces, addWorkspace, removeWorkspace, syncWorkspace, fetchGovernanceQueue, fetchGovernanceExperience, recoverFailedTask, fetchOwnerDecisions, resolveOwnerDecision, fetchApprovalsGrouped, fetchAllActivations, disableActivation, fetchRuleCodeOwnerReview, ruleCodeDecision, pauseAllRuleCode, releaseRuleCodePause, fetchLifecycleMetrics, fetchUpdateStatus, fetchUpdateHistory, applyUpdate, applyFullUpdate, rollbackUpdate, fetchEvidenceChain, fetchIntentSummary, fetchIntentContent, createIntentTemplate, saveIntentContent, fetchIntentVersions, recordIntentDecision, listIntentDecisionsByPainId, listIntentDecisionsByTaskId, fetchIntentDecisionSummary, dispatchFollowUp, };
607
+ fetchFeedbackChannels, submitFeedbackReport, markFeedbackReportSent, fetchConfigSummary, fetchConfigCatalog, updateAgentBinding, updateDefaultRuntime, createRuntimeProfile, updateRuntimeProfile, deleteRuntimeProfile, fetchAgentReadiness, patchFeatureFlag, fetchOutputLanguage, updateOutputLanguage, fetchWorkspaces, addWorkspace, removeWorkspace, syncWorkspace, fetchGovernanceQueue, fetchGovernanceExperience, recoverFailedTask, fetchFailedTaskDetail, fetchOwnerDecisions, resolveOwnerDecision, fetchApprovalsGrouped, fetchAllActivations, disableActivation, fetchRuleCodeOwnerReview, ruleCodeDecision, pauseAllRuleCode, releaseRuleCodePause, fetchLifecycleMetrics, fetchUpdateStatus, fetchUpdateHistory, applyUpdate, applyFullUpdate, rollbackUpdate, fetchEvidenceChain, fetchIntentSummary, fetchIntentContent, createIntentTemplate, saveIntentContent, fetchIntentVersions, recordIntentDecision, listIntentDecisionsByPainId, listIntentDecisionsByTaskId, fetchIntentDecisionSummary, dispatchFollowUp, };
@@ -1668,7 +1668,18 @@
1668
1668
  "attempts": "Attempts",
1669
1669
  "lastAttempt": "Last Attempt",
1670
1670
  "noPainId": "—",
1671
- "partialResults": "Showing first {shown} of {total} records. Narrow filters or use 'pd errors list' with --offset to see more."
1671
+ "partialResults": "Showing first {shown} of {total} records. Narrow filters or use 'pd errors list' with --offset to see more.",
1672
+ "detailToggle": "Toggle task detail",
1673
+ "detailError": "Failed to load task detail",
1674
+ "createdAt": "Created",
1675
+ "updatedAt": "Updated",
1676
+ "runsTitle": "Run history",
1677
+ "runAttempt": "Attempt",
1678
+ "runStarted": "Started",
1679
+ "runEnded": "Ended",
1680
+ "runReason": "Reason",
1681
+ "runErrorCategory": "Error category",
1682
+ "noRuns": "No run records for this task."
1672
1683
  }
1673
1684
  },
1674
1685
  "components": {
@@ -1668,7 +1668,18 @@
1668
1668
  "attempts": "尝试次数",
1669
1669
  "lastAttempt": "最近尝试",
1670
1670
  "noPainId": "—",
1671
- "partialResults": "仅显示前 {shown} 条,共 {total} 条。请缩小筛选范围或使用 'pd errors list --offset' 查看更多。"
1671
+ "partialResults": "仅显示前 {shown} 条,共 {total} 条。请缩小筛选范围或使用 'pd errors list --offset' 查看更多。",
1672
+ "detailToggle": "展开/收起任务详情",
1673
+ "detailError": "加载任务详情失败",
1674
+ "createdAt": "创建时间",
1675
+ "updatedAt": "更新时间",
1676
+ "runsTitle": "执行历史",
1677
+ "runAttempt": "尝试",
1678
+ "runStarted": "开始时间",
1679
+ "runEnded": "结束时间",
1680
+ "runReason": "原因",
1681
+ "runErrorCategory": "错误类别",
1682
+ "noRuns": "该任务暂无执行记录。"
1672
1683
  }
1673
1684
  },
1674
1685
  "components": {
@@ -18,13 +18,19 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
18
18
  * /api/v1/failed-tasks/:id/recover. The success state means "Recovery
19
19
  * Accepted" — the task is pending again; execution is asynchronous.
20
20
  *
21
+ * PRI-747 F22 / PRI-749: each row can be expanded into its detail — the full
22
+ * task record plus run history — fetched lazily from the pre-existing
23
+ * GET /api/v1/failed-tasks/:id endpoint (server route +
24
+ * SqliteTaskStore.getFailedTaskDetail already existed; the UI simply never
25
+ * consumed them).
26
+ *
21
27
  * Runtime Contract (rc-*):
22
28
  * - rc-1: API response is treated as `unknown` and validated before use
23
29
  * - rc-2: no `as` type assertions — runtime type guards only
24
30
  * - rc-5: `Object.hasOwn()` for untrusted object keys
25
31
  * - rc-9: every error/disabled path surfaces a reason + next action
26
32
  */
27
- import { useState, useEffect, useCallback } from "react";
33
+ import { useState, useEffect, useCallback, useRef, Fragment } from "react";
28
34
  import { useNavigate } from "react-router-dom";
29
35
  import { useTranslation } from "react-i18next";
30
36
  import { toast } from "sonner";
@@ -35,8 +41,23 @@ import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/ca
35
41
  import { Button } from "../../components/ui/button.js";
36
42
  import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel, } from "../../components/ui/alert-dialog.js";
37
43
  import { ShinyText } from "../../components/ui/shiny-text.js";
38
- import { formatDate } from "../../utils/format.js";
39
- import { recoverFailedTask, fetchConfigSummary, request } from "../../api.js";
44
+ import { formatDate } from "../../utils/format-date.js";
45
+ import i18n from "../../i18n/index.js";
46
+ import { recoverFailedTask, fetchConfigSummary, fetchFailedTaskDetail, request } from "../../api.js";
47
+ // Same component set the former utils/format.js rendered, now served by the
48
+ // defensive single-owner formatter: an invalid date string returns the raw
49
+ // input instead of Intl throwing RangeError mid-render (review round of
50
+ // PRI-747 F22 — utils/format.js was retired with this migration).
51
+ const DATETIME_FORMAT_OPTS = {
52
+ year: "numeric",
53
+ month: "2-digit",
54
+ day: "2-digit",
55
+ hour: "2-digit",
56
+ minute: "2-digit",
57
+ };
58
+ function formatDateTime(iso) {
59
+ return formatDate(iso, i18n.language, DATETIME_FORMAT_OPTS);
60
+ }
40
61
  // ── Runtime validators (rc-1, rc-2, rc-5) ───────────────────────────────────
41
62
  //
42
63
  // The API response is `unknown` until these guards run. No `as` casts.
@@ -280,5 +301,60 @@ function LoadedContent({ data, onCreateDraft, onRecover, t }) {
280
301
  .replace("{total}", String(data.total)) }))] })] }));
281
302
  }
282
303
  function TaskTable({ tasks, onCreateDraft, onRecover, t }) {
283
- 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) : "—" }), task.ownerDecisionRequired === true ? (_jsxs("span", { className: "whitespace-nowrap text-[12px]", children: [_jsx("span", { className: "text-amber", children: t("pages.failedTasks.awaitingOwnerDecision") }), " ", _jsx("a", { href: "#/focus", className: "text-gov underline underline-offset-2 hover:text-gov/80", "data-testid": `go-focus-${task.taskId}`, children: t("pages.failedTasks.goGovernanceFocus") })] })) : onRecover ? (_jsx(Button, { variant: "default", size: "sm", onClick: () => onRecover(task), className: "whitespace-nowrap", children: t("pages.failedTasks.recover") })) : null, _jsx(Button, { variant: "outline", size: "sm", onClick: () => onCreateDraft(task), className: "whitespace-nowrap", children: t("pages.failedTasks.createDraft") })] }, task.taskId)))] }));
304
+ // PRI-747 F22: one row at a time is expanded into its detail panel.
305
+ const [expandedId, setExpandedId] = useState(null);
306
+ const [detail, setDetail] = useState({ status: "idle" });
307
+ // Drop responses from a superseded expand/collapse (rc-7: only the newest
308
+ // request's result may become current state).
309
+ const detailRequestIdRef = useRef(0);
310
+ // Fetch (or refetch) one task's detail; a stale response from a superseded
311
+ // request is dropped (rc-7: only the newest request's result may land).
312
+ const requestDetail = useCallback(async (taskId) => {
313
+ const requestId = ++detailRequestIdRef.current;
314
+ setDetail({ status: "loading", taskId });
315
+ // Lazy fetch on expand: the list view stays cheap; the run history is
316
+ // only loaded when the Owner actually asks for this task's detail.
317
+ const result = await fetchFailedTaskDetail(taskId);
318
+ if (requestId !== detailRequestIdRef.current)
319
+ return;
320
+ if (result.success) {
321
+ setDetail({ status: "loaded", taskId, data: result.data });
322
+ }
323
+ else {
324
+ // rc-9: surface the server-provided reason + next action (404 = the
325
+ // task recovered/changed state after the list was loaded).
326
+ setDetail({ status: "error", taskId, message: result.error, nextAction: result.nextAction });
327
+ }
328
+ }, []);
329
+ const toggleDetail = useCallback((taskId) => {
330
+ if (expandedId === taskId) {
331
+ // Invalidate any in-flight response, then collapse.
332
+ detailRequestIdRef.current += 1;
333
+ setExpandedId(null);
334
+ setDetail({ status: "idle" });
335
+ return;
336
+ }
337
+ setExpandedId(taskId);
338
+ void requestDetail(taskId);
339
+ }, [expandedId, requestDetail]);
340
+ return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "grid grid-cols-[auto_minmax(0,1fr)_minmax(0,1fr)_auto_minmax(0,1fr)_auto_minmax(0,1fr)_auto_auto] gap-3 pb-2 border-b border-line font-mono text-[10px] uppercase tracking-[0.08em] text-ink-4", children: [_jsx("span", { "aria-hidden": "true" }), _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" }), _jsx("span", { "aria-hidden": "true" })] }), tasks.map((task) => {
341
+ const isExpanded = expandedId === task.taskId;
342
+ return (_jsxs(Fragment, { children: [_jsxs("div", { className: "grid grid-cols-[auto_minmax(0,1fr)_minmax(0,1fr)_auto_minmax(0,1fr)_auto_minmax(0,1fr)_auto_auto] gap-3 py-2 items-center text-[13px] border-b border-line/50 last:border-b-0", children: [_jsx(Button, { variant: "quiet", size: "icon", className: "h-6 w-6 shrink-0 text-[12px]", onClick: () => toggleDetail(task.taskId), "aria-label": t("pages.failedTasks.detailToggle"), "aria-expanded": isExpanded, "data-testid": `detail-toggle-${task.taskId}`, children: isExpanded ? "▾" : "▸" }), _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 ? formatDateTime(task.lastAttemptAt) : "—" }), task.ownerDecisionRequired === true ? (_jsxs("span", { className: "whitespace-nowrap text-[12px]", children: [_jsx("span", { className: "text-amber", children: t("pages.failedTasks.awaitingOwnerDecision") }), " ", _jsx("a", { href: "#/focus", className: "text-gov underline underline-offset-2 hover:text-gov/80", "data-testid": `go-focus-${task.taskId}`, children: t("pages.failedTasks.goGovernanceFocus") })] })) : onRecover ? (_jsx(Button, { variant: "default", size: "sm", onClick: () => onRecover(task), className: "whitespace-nowrap", children: t("pages.failedTasks.recover") })) : null, _jsx(Button, { variant: "outline", size: "sm", onClick: () => onCreateDraft(task), className: "whitespace-nowrap", children: t("pages.failedTasks.createDraft") })] }), isExpanded && (_jsx("div", { className: "border border-line/60 rounded-[var(--radius-md)] bg-panel/60 p-4 mb-2", "data-testid": `detail-panel-${task.taskId}`, children: _jsx(TaskDetailPanel, { state: detail.status === "idle" ? { status: "loading", taskId: task.taskId } : detail, taskId: task.taskId, onRetry: requestDetail, t: t }) }))] }, task.taskId));
343
+ })] }));
344
+ }
345
+ function TaskDetailPanel({ state, taskId, onRetry, t }) {
346
+ if (state.status === "loading") {
347
+ return (_jsx("div", { className: "text-ink-3 text-[13px]", "data-testid": "detail-loading", children: t("common.loading") }));
348
+ }
349
+ if (state.status === "error") {
350
+ // rc-9: reason + next action + an explicit retry affordance
351
+ return (_jsxs("div", { className: "text-[13px] space-y-2", "data-testid": "detail-error", children: [_jsxs("div", { className: "text-danger", children: [t("pages.failedTasks.detailError"), ": ", state.message] }), state.nextAction && _jsx("div", { className: "text-ink-3", children: state.nextAction }), _jsx("div", { children: _jsx(Button, { variant: "outline", size: "sm", onClick: () => onRetry(taskId), children: t("common.refresh") }) })] }));
352
+ }
353
+ // The idle state only occurs if the panel renders for a task whose fetch was
354
+ // superseded mid-flight; treat it as loading for the row just expanded.
355
+ if (state.status === "idle" || state.taskId !== taskId) {
356
+ return (_jsx("div", { className: "text-ink-3 text-[13px]", "data-testid": "detail-loading", children: t("common.loading") }));
357
+ }
358
+ const { data } = state;
359
+ return (_jsxs("div", { className: "space-y-4 text-[13px]", children: [_jsxs("div", { className: "grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1 font-mono text-[12px]", children: [_jsx("span", { className: "text-ink-4 uppercase tracking-[0.08em] self-center", children: t("pages.failedTasks.taskId") }), _jsx("span", { className: "text-ink-2 break-all", children: data.taskId }), _jsx("span", { className: "text-ink-4 uppercase tracking-[0.08em] self-center", children: t("pages.failedTasks.kind") }), _jsx("span", { className: "text-ink-2", children: data.taskKind }), _jsx("span", { className: "text-ink-4 uppercase tracking-[0.08em] self-center", children: t("pages.failedTasks.status") }), _jsx("span", { className: "text-ink-2", children: data.status }), _jsx("span", { className: "text-ink-4 uppercase tracking-[0.08em] self-center", children: t("pages.failedTasks.createdAt") }), _jsx("span", { className: "text-ink-3", children: formatDateTime(data.createdAt) }), data.updatedAt && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-ink-4 uppercase tracking-[0.08em] self-center", children: t("pages.failedTasks.updatedAt") }), _jsx("span", { className: "text-ink-3", children: formatDateTime(data.updatedAt) })] })), _jsx("span", { className: "text-ink-4 uppercase tracking-[0.08em] self-center", children: t("pages.failedTasks.attempts") }), _jsxs("span", { className: "text-ink-2", children: [data.attemptCount, " / ", data.maxAttempts] }), _jsx("span", { className: "text-ink-4 uppercase tracking-[0.08em] self-center", children: t("pages.failedTasks.lastError") }), _jsx("span", { className: data.lastError ? "text-danger" : "text-ink-3", children: data.lastError ?? "—" })] }), _jsxs("div", { children: [_jsxs("div", { className: "font-mono text-[10px] uppercase tracking-[0.08em] text-ink-4 mb-2", children: [t("pages.failedTasks.runsTitle"), " (", data.runs.length, ")"] }), data.runs.length === 0 ? (_jsx("div", { className: "text-ink-3", children: t("pages.failedTasks.noRuns") })) : (_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "grid grid-cols-[auto_auto_minmax(0,1fr)_minmax(0,1fr)] gap-3 pb-1 border-b border-line/50 font-mono text-[10px] uppercase tracking-[0.08em] text-ink-4", children: [_jsx("span", { children: t("pages.failedTasks.runAttempt") }), _jsx("span", { children: t("pages.failedTasks.status") }), _jsx("span", { children: t("pages.failedTasks.runStarted") }), _jsx("span", { children: t("pages.failedTasks.runReason") })] }), data.runs.map((run) => (_jsxs("div", { className: "grid grid-cols-[auto_auto_minmax(0,1fr)_minmax(0,1fr)] gap-3 py-1 text-[12px] border-b border-line/30 last:border-b-0", title: `${t("pages.failedTasks.runEnded")}: ${run.endedAt ? formatDateTime(run.endedAt) : "—"}${run.errorCategory ? ` · ${t("pages.failedTasks.runErrorCategory")}: ${run.errorCategory}` : ""}`, children: [_jsxs("span", { className: "font-mono text-ink-2", children: ["#", run.attemptNumber] }), _jsx("span", { className: "font-mono text-ink-3", children: run.executionStatus }), _jsx("span", { className: "font-mono text-ink-3", children: formatDateTime(run.startedAt) }), _jsx("span", { className: "text-ink-3 break-words", title: run.reason ?? undefined, children: run.reason ?? (run.errorCategory ?? "—") })] }, run.runId)))] }))] })] }));
284
360
  }
@@ -20,9 +20,19 @@ import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/ca
20
20
  import { Button } from '../../components/ui/button.js';
21
21
  import { ShinyText } from '../../components/ui/shiny-text.js';
22
22
  import { fetchEvidenceChain, recordIntentDecision, listIntentDecisionsByPainId, listIntentDecisionsByTaskId, dispatchFollowUp } from '../../api.js';
23
- import { formatDate } from '../../utils/format.js';
23
+ import { formatDate } from '../../utils/format-date.js';
24
+ import i18n from '../../i18n/index.js';
24
25
  import { buildCardLayers, buildDebugIdSummary, isLayer2EffectivelyEmpty, shouldRenderIntentTensionPanel, shouldRenderFollowUpActions, buildIntentDecisionPayload } from './pain-card-helpers.js';
25
26
  import { enumLabel } from '../../utils/enum-labels.js';
27
+ // utils/format.js was retired into the defensive single-owner formatter
28
+ // (format-date.ts) — same datetime components, invalid dates now render the
29
+ // raw string instead of Intl throwing.
30
+ const DATETIME_FORMAT_OPTS = {
31
+ year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
32
+ };
33
+ function formatDateTime(iso) {
34
+ return formatDate(iso, i18n.language, DATETIME_FORMAT_OPTS);
35
+ }
26
36
  function groupForState(state) {
27
37
  switch (state) {
28
38
  case 'evidence-only':
@@ -117,7 +127,7 @@ function LoadedContent({ data, expandedIds, onToggle, onRefresh, t }) {
117
127
  return (_jsxs("div", { children: [_jsxs("div", { className: "font-mono text-[11px] uppercase tracking-[0.12em] text-ink-4 mb-3", children: [t(`pages.pain.group_${group}`), " (", records.length, ")"] }), _jsxs("div", { className: "space-y-4", children: [others.map((record) => (_jsx(EvidenceChainCard, { record: record, expanded: expandedIds.has(record.id), onToggle: () => onToggle(record.id), t: t }, record.id))), foldReviewQueue && !reviewQueueExpanded && (_jsxs("button", { type: "button", onClick: () => setReviewQueueExpanded(true), className: "w-full text-left p-3 bg-panel border border-line rounded-[var(--radius-md)] text-ink-3 text-sm hover:bg-paper-2 transition-colors", children: [reviewQueueLabel, " \u00B7 ", t('pages.pain.goToGovernance')] })), (!foldReviewQueue || reviewQueueExpanded) && inReview.map((record) => (_jsx(EvidenceChainCard, { record: record, expanded: expandedIds.has(record.id), onToggle: () => onToggle(record.id), t: t }, record.id))), foldReviewQueue && reviewQueueExpanded && (_jsxs("button", { type: "button", onClick: () => setReviewQueueExpanded(false), className: "font-mono text-[11px] text-ink-4 hover:text-ink-3 transition-colors", children: [reviewQueueLabel, " \u25B2"] }))] })] }, group));
118
128
  }
119
129
  return (_jsxs("div", { children: [_jsxs("div", { className: "font-mono text-[11px] uppercase tracking-[0.12em] text-ink-4 mb-3", children: [t(`pages.pain.group_${group}`), " (", records.length, ")"] }), _jsx("div", { className: "space-y-4", children: records.map((record) => (_jsx(EvidenceChainCard, { record: record, expanded: expandedIds.has(record.id), onToggle: () => onToggle(record.id), t: t }, record.id))) })] }, group));
120
- }) })), _jsxs("div", { className: "mt-8 font-mono text-[11px] text-ink-4", children: [t('pages.pain.generatedAt'), ": ", formatDate(data.generatedAt)] })] }));
130
+ }) })), _jsxs("div", { className: "mt-8 font-mono text-[11px] text-ink-4", children: [t('pages.pain.generatedAt'), ": ", formatDateTime(data.generatedAt)] })] }));
121
131
  }
122
132
  // ── Empty state (E section: guide next step, not "暂无数据") ──────────────────
123
133
  function EmptyState({ t }) {
@@ -138,7 +148,7 @@ function EvidenceChainCard({ record, expanded, onToggle, t }) {
138
148
  return (_jsxs(Card, { className: "overflow-hidden", children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx(Badge, { variant: stateVariant, children: stateLabel }), _jsx(Badge, { variant: "outline", children: sourceLabel }), record.hostKind && record.hostKind !== 'unknown' && (_jsx(Badge, { variant: "secondary", children: t(`pages.pain.hostKind.${record.hostKind}`) })), record.admissionDecision && (_jsx(Badge, { variant: "secondary", children: enumLabel('admission', record.admissionDecision, t) }))] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsx(Button, { variant: "ghost", size: "sm", "data-testid": `pain-feedback-${record.id}`, onClick: () => {
139
149
  const params = new URLSearchParams({ source: "pain_page", painId: record.id });
140
150
  navigate(`/report-problem?${params.toString()}`);
141
- }, children: t('pages.reportProblem.entryFeedback') }), _jsx("span", { className: "font-mono text-[11px] text-ink-4 whitespace-nowrap", children: formatDate(record.observedAt) })] })] }) }), _jsxs(CardContent, { children: [_jsxs("div", { className: "space-y-3 mb-4", children: [_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldTrigger') }), layer2Empty ? (_jsx("p", { className: "text-ink-3 text-sm italic leading-relaxed", children: t('pages.pain.noHumanSummary') })) : (_jsx(CardTitle, { className: "text-[15px] leading-relaxed", children: layers.layer2.triggerSummary }))] }), layers.layer2.conclusion && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldConclusion') }), _jsx("p", { className: "text-ink text-sm leading-relaxed font-medium", children: layers.layer2.conclusion })] })), layers.layer2.applicability && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldApplicability') }), _jsx("p", { className: "text-ink-2 text-sm leading-relaxed", children: layers.layer2.applicability })] })), confidenceDisplay && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldConfidence') }), _jsx("p", { className: "text-ink-2 text-sm", children: confidenceDisplay })] })), layers.layer2.failureReason && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldFailureReason') }), _jsx("p", { className: "text-danger text-sm leading-relaxed", children: layers.layer2.failureReason })] })), layers.layer2.degradedReason && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldDegradedReason') }), _jsx("p", { className: "text-amber text-sm leading-relaxed", children: layers.layer2.degradedReason })] })), layers.layer2.nextAction && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldNextAction') }), _jsx("p", { className: "text-ink-2 text-sm leading-relaxed", children: layers.layer2.nextAction })] }))] }), shouldRenderIntentTensionPanel(layers.layer2.intentTension) && (_jsx(IntentTensionPanel, { tension: layers.layer2.intentTension, recordId: record.id, painId: record.id, taskId: record.linkedTaskId ?? undefined, linkedCandidateId: record.linkedCandidateId ?? undefined, t: t })), _jsxs("div", { className: "mt-4 pt-3 border-t border-line", children: [_jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: async () => {
151
+ }, children: t('pages.reportProblem.entryFeedback') }), _jsx("span", { className: "font-mono text-[11px] text-ink-4 whitespace-nowrap", children: formatDateTime(record.observedAt) })] })] }) }), _jsxs(CardContent, { children: [_jsxs("div", { className: "space-y-3 mb-4", children: [_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldTrigger') }), layer2Empty ? (_jsx("p", { className: "text-ink-3 text-sm italic leading-relaxed", children: t('pages.pain.noHumanSummary') })) : (_jsx(CardTitle, { className: "text-[15px] leading-relaxed", children: layers.layer2.triggerSummary }))] }), layers.layer2.conclusion && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldConclusion') }), _jsx("p", { className: "text-ink text-sm leading-relaxed font-medium", children: layers.layer2.conclusion })] })), layers.layer2.applicability && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldApplicability') }), _jsx("p", { className: "text-ink-2 text-sm leading-relaxed", children: layers.layer2.applicability })] })), confidenceDisplay && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldConfidence') }), _jsx("p", { className: "text-ink-2 text-sm", children: confidenceDisplay })] })), layers.layer2.failureReason && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldFailureReason') }), _jsx("p", { className: "text-danger text-sm leading-relaxed", children: layers.layer2.failureReason })] })), layers.layer2.degradedReason && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldDegradedReason') }), _jsx("p", { className: "text-amber text-sm leading-relaxed", children: layers.layer2.degradedReason })] })), layers.layer2.nextAction && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldNextAction') }), _jsx("p", { className: "text-ink-2 text-sm leading-relaxed", children: layers.layer2.nextAction })] }))] }), shouldRenderIntentTensionPanel(layers.layer2.intentTension) && (_jsx(IntentTensionPanel, { tension: layers.layer2.intentTension, recordId: record.id, painId: record.id, taskId: record.linkedTaskId ?? undefined, linkedCandidateId: record.linkedCandidateId ?? undefined, t: t })), _jsxs("div", { className: "mt-4 pt-3 border-t border-line", children: [_jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: async () => {
142
152
  try {
143
153
  await navigator.clipboard.writeText(buildDebugIdSummary(record));
144
154
  setCopied(true);
@@ -222,6 +222,31 @@ export interface GovernanceQueueData {
222
222
  note?: string;
223
223
  generatedAt?: string;
224
224
  }
225
+ /** One execution attempt of a failed task (RunRecord projection). */
226
+ export interface FailedTaskDetailRun {
227
+ runId: string;
228
+ /** Owning task id — validated to equal the detail's taskId (rc-6 lineage). */
229
+ taskId: string;
230
+ attemptNumber: number;
231
+ executionStatus: string;
232
+ startedAt: string;
233
+ endedAt: string | null;
234
+ reason: string | null;
235
+ errorCategory: string | null;
236
+ }
237
+ /** Validated shape of GET /api/v1/failed-tasks/:id. */
238
+ export interface FailedTaskDetailData {
239
+ taskId: string;
240
+ taskKind: string;
241
+ status: string;
242
+ createdAt: string;
243
+ updatedAt: string;
244
+ attemptCount: number;
245
+ maxAttempts: number;
246
+ lastError: string | null;
247
+ runs: FailedTaskDetailRun[];
248
+ }
249
+ export declare function validateFailedTaskDetail(v: unknown): FailedTaskDetailData | null;
225
250
  export interface RecoveryResultData {
226
251
  taskId: string;
227
252
  previousStatus: string;
@@ -735,6 +735,93 @@ function validateDegradedSignal(v) {
735
735
  source: v.source,
736
736
  };
737
737
  }
738
+ function validateFailedTaskDetailRun(v) {
739
+ if (!isObject(v))
740
+ return null;
741
+ if (!Object.hasOwn(v, 'runId') || !isString(v.runId))
742
+ return null;
743
+ // Required by the RunRecord contract and by the lineage check in
744
+ // validateFailedTaskDetail (rc-6): a run must name the task it belongs to.
745
+ if (!Object.hasOwn(v, 'taskId') || !isString(v.taskId))
746
+ return null;
747
+ if (!Object.hasOwn(v, 'attemptNumber') || !isNumber(v.attemptNumber))
748
+ return null;
749
+ if (!Object.hasOwn(v, 'executionStatus') || !isString(v.executionStatus))
750
+ return null;
751
+ if (!Object.hasOwn(v, 'startedAt') || !isString(v.startedAt))
752
+ return null;
753
+ const endedAt = readNullableString(v, 'endedAt');
754
+ if (!endedAt.valid)
755
+ return null;
756
+ const reason = readNullableString(v, 'reason');
757
+ if (!reason.valid)
758
+ return null;
759
+ const errorCategory = readNullableString(v, 'errorCategory');
760
+ if (!errorCategory.valid)
761
+ return null;
762
+ return {
763
+ runId: v.runId,
764
+ taskId: v.taskId,
765
+ attemptNumber: v.attemptNumber,
766
+ executionStatus: v.executionStatus,
767
+ startedAt: v.startedAt,
768
+ endedAt: endedAt.value,
769
+ reason: reason.value,
770
+ errorCategory: errorCategory.value,
771
+ };
772
+ }
773
+ export function validateFailedTaskDetail(v) {
774
+ if (!isObject(v))
775
+ return null;
776
+ if (!Object.hasOwn(v, 'task') || !isObject(v.task))
777
+ return null;
778
+ if (!Object.hasOwn(v, 'runs') || !Array.isArray(v.runs))
779
+ return null;
780
+ const t = v.task;
781
+ if (!Object.hasOwn(t, 'taskId') || !isString(t.taskId))
782
+ return null;
783
+ if (!Object.hasOwn(t, 'taskKind') || !isString(t.taskKind))
784
+ return null;
785
+ if (!Object.hasOwn(t, 'status') || !isString(t.status))
786
+ return null;
787
+ if (!Object.hasOwn(t, 'createdAt') || !isString(t.createdAt))
788
+ return null;
789
+ // updatedAt is required on the server's TaskRecord (typebox: no .optional())
790
+ // — required here too, so contract drift reaches the validation-error state
791
+ // instead of a silently partial record (rc-3).
792
+ if (!Object.hasOwn(t, 'updatedAt') || !isString(t.updatedAt))
793
+ return null;
794
+ if (!Object.hasOwn(t, 'attemptCount') || !isNumber(t.attemptCount))
795
+ return null;
796
+ if (!Object.hasOwn(t, 'maxAttempts') || !isNumber(t.maxAttempts))
797
+ return null;
798
+ const taskLastError = readNullableString(t, 'lastError');
799
+ if (!taskLastError.valid)
800
+ return null;
801
+ const topLastError = readNullableString(v, 'lastError');
802
+ if (!topLastError.valid)
803
+ return null;
804
+ const runs = validateArray(v.runs, validateFailedTaskDetailRun);
805
+ if (runs === null)
806
+ return null;
807
+ // rc-6-lineage-consistency: every run must belong to THIS task — a detail
808
+ // response mixing another task's runs would display foreign failure reasons.
809
+ for (const run of runs) {
810
+ if (run.taskId !== t.taskId)
811
+ return null;
812
+ }
813
+ return {
814
+ taskId: t.taskId,
815
+ taskKind: t.taskKind,
816
+ status: t.status,
817
+ createdAt: t.createdAt,
818
+ updatedAt: t.updatedAt,
819
+ attemptCount: t.attemptCount,
820
+ maxAttempts: t.maxAttempts,
821
+ lastError: topLastError.value ?? taskLastError.value,
822
+ runs,
823
+ };
824
+ }
738
825
  export function validateRecoveryResult(v) {
739
826
  if (!isObject(v))
740
827
  return null;
@@ -957,8 +957,14 @@
957
957
  .grid-cols-\[auto_1fr\] {
958
958
  grid-template-columns: auto 1fr;
959
959
  }
960
- .grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1fr\)_auto_minmax\(0\,1fr\)_auto_minmax\(0\,1fr\)_auto\] {
961
- grid-template-columns: minmax(0,1fr) minmax(0,1fr) auto minmax(0,1fr) auto minmax(0,1fr) auto;
960
+ .grid-cols-\[auto_auto_minmax\(0\,1fr\)_minmax\(0\,1fr\)\] {
961
+ grid-template-columns: auto auto minmax(0,1fr) minmax(0,1fr);
962
+ }
963
+ .grid-cols-\[auto_minmax\(0\,1fr\)\] {
964
+ grid-template-columns: auto minmax(0,1fr);
965
+ }
966
+ .grid-cols-\[auto_minmax\(0\,1fr\)_minmax\(0\,1fr\)_auto_minmax\(0\,1fr\)_auto_minmax\(0\,1fr\)_auto_auto\] {
967
+ grid-template-columns: auto minmax(0,1fr) minmax(0,1fr) auto minmax(0,1fr) auto minmax(0,1fr) auto auto;
962
968
  }
963
969
  .flex-col {
964
970
  flex-direction: column;
@@ -1099,6 +1105,9 @@
1099
1105
  -moz-column-gap: 14px;
1100
1106
  column-gap: 14px;
1101
1107
  }
1108
+ .gap-y-1 {
1109
+ row-gap: var(--spacing);
1110
+ }
1102
1111
  .gap-y-2 {
1103
1112
  row-gap: calc(var(--spacing) * 2);
1104
1113
  }
@@ -1108,6 +1117,9 @@
1108
1117
  .gap-y-\[12px\] {
1109
1118
  row-gap: 12px;
1110
1119
  }
1120
+ .self-center {
1121
+ align-self: center;
1122
+ }
1111
1123
  .truncate {
1112
1124
  overflow: hidden;
1113
1125
  text-overflow: ellipsis;
@@ -1380,6 +1392,12 @@
1380
1392
  .border-line-2 {
1381
1393
  border-color: var(--color-line-2);
1382
1394
  }
1395
+ .border-line\/30 {
1396
+ border-color: color-mix(in srgb, #d7d1c4 30%, transparent);
1397
+ @supports (color: color-mix(in lab, red, red)) {
1398
+ border-color: color-mix(in oklab, var(--color-line) 30%, transparent);
1399
+ }
1400
+ }
1383
1401
  .border-line\/50 {
1384
1402
  border-color: color-mix(in srgb, #d7d1c4 50%, transparent);
1385
1403
  @supports (color: color-mix(in lab, red, red)) {
@@ -1569,6 +1587,12 @@
1569
1587
  .bg-panel {
1570
1588
  background-color: var(--color-panel);
1571
1589
  }
1590
+ .bg-panel\/60 {
1591
+ background-color: color-mix(in srgb, #fffdf7 60%, transparent);
1592
+ @supports (color: color-mix(in lab, red, red)) {
1593
+ background-color: color-mix(in oklab, var(--color-panel) 60%, transparent);
1594
+ }
1595
+ }
1572
1596
  .bg-paper {
1573
1597
  background-color: var(--color-paper);
1574
1598
  }
@@ -2065,6 +2089,9 @@
2065
2089
  --tw-tracking: var(--tracking-wider);
2066
2090
  letter-spacing: var(--tracking-wider);
2067
2091
  }
2092
+ .break-words {
2093
+ overflow-wrap: break-word;
2094
+ }
2068
2095
  .break-all {
2069
2096
  word-break: break-all;
2070
2097
  }