create-principles-disciple 1.121.9 → 1.122.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 (133) hide show
  1. package/console/dist/server/index.js +17 -0
  2. package/console/dist/server/models/OwnerDecisionConsoleModel.d.ts +39 -0
  3. package/console/dist/server/models/OwnerDecisionConsoleModel.js +233 -0
  4. package/console/dist/server/models/PrincipleClassifier.d.ts +3 -3
  5. package/console/dist/server/models/PrincipleClassifier.js +12 -5
  6. package/console/dist/server/models/PrinciplesConsoleModel.d.ts +1 -1
  7. package/console/dist/server/models/PrinciplesConsoleModel.js +2 -2
  8. package/console/dist/server/routes/failed-tasks.js +40 -2
  9. package/console/dist/server/routes/owner-decisions.d.ts +25 -0
  10. package/console/dist/server/routes/owner-decisions.js +186 -0
  11. package/console/dist/server/routes/principles.js +15 -7
  12. package/console/dist/server/routes/update.js +82 -14
  13. package/console/dist/server/utils/update-links.d.ts +38 -0
  14. package/console/dist/server/utils/update-links.js +71 -0
  15. package/console/dist/ui/api.d.ts +12 -1
  16. package/console/dist/ui/api.js +9 -2
  17. package/console/dist/ui/components/notifications/NotificationProvider.js +8 -2
  18. package/console/dist/ui/i18n/en.json +33 -1
  19. package/console/dist/ui/i18n/zh-CN.json +33 -1
  20. package/console/dist/ui/pages/failed-tasks/FailedTasksPage.js +5 -1
  21. package/console/dist/ui/pages/focus/FocusPage.js +19 -2
  22. package/console/dist/ui/pages/focus/OwnerDecisionCard.d.ts +6 -0
  23. package/console/dist/ui/pages/focus/OwnerDecisionCard.js +86 -0
  24. package/console/dist/ui/pages/principles/PrinciplesPage.js +11 -5
  25. package/console/dist/ui/utils/validators.d.ts +37 -0
  26. package/console/dist/ui/utils/validators.js +123 -0
  27. package/console/dist/web/assets/app.css +11 -0
  28. package/console/dist/web/assets/app.js +2740 -2350
  29. package/core/dist/runtime-v2/__tests__/evaluator-repair-loop.test.js +20 -0
  30. package/core/dist/runtime-v2/__tests__/evaluator-repair-loop.test.js.map +1 -1
  31. package/core/dist/runtime-v2/__tests__/rollout-reviewer-verdict-paths.test.js +26 -5
  32. package/core/dist/runtime-v2/__tests__/rollout-reviewer-verdict-paths.test.js.map +1 -1
  33. package/core/dist/runtime-v2/index.d.ts +7 -1
  34. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  35. package/core/dist/runtime-v2/index.js +4 -0
  36. package/core/dist/runtime-v2/index.js.map +1 -1
  37. package/core/dist/runtime-v2/internalization/__tests__/evaluator-prompt-builder.test.js +1 -1
  38. package/core/dist/runtime-v2/internalization/__tests__/internalization-transition-decision.test.js +15 -14
  39. package/core/dist/runtime-v2/internalization/__tests__/internalization-transition-decision.test.js.map +1 -1
  40. package/core/dist/runtime-v2/internalization/__tests__/owner-decision-architecture.test.d.ts +2 -0
  41. package/core/dist/runtime-v2/internalization/__tests__/owner-decision-architecture.test.d.ts.map +1 -0
  42. package/core/dist/runtime-v2/internalization/__tests__/owner-decision-architecture.test.js +56 -0
  43. package/core/dist/runtime-v2/internalization/__tests__/owner-decision-architecture.test.js.map +1 -0
  44. package/core/dist/runtime-v2/internalization/__tests__/owner-decision.test.d.ts +2 -0
  45. package/core/dist/runtime-v2/internalization/__tests__/owner-decision.test.d.ts.map +1 -0
  46. package/core/dist/runtime-v2/internalization/__tests__/owner-decision.test.js +883 -0
  47. package/core/dist/runtime-v2/internalization/__tests__/owner-decision.test.js.map +1 -0
  48. package/core/dist/runtime-v2/internalization/__tests__/owner-override-resume.test.d.ts +2 -0
  49. package/core/dist/runtime-v2/internalization/__tests__/owner-override-resume.test.d.ts.map +1 -0
  50. package/core/dist/runtime-v2/internalization/__tests__/owner-override-resume.test.js +328 -0
  51. package/core/dist/runtime-v2/internalization/__tests__/owner-override-resume.test.js.map +1 -0
  52. package/core/dist/runtime-v2/internalization/evaluator-output.d.ts +63 -2
  53. package/core/dist/runtime-v2/internalization/evaluator-output.d.ts.map +1 -1
  54. package/core/dist/runtime-v2/internalization/evaluator-output.js +139 -2
  55. package/core/dist/runtime-v2/internalization/evaluator-output.js.map +1 -1
  56. package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts +54 -2
  57. package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts.map +1 -1
  58. package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.js +49 -2
  59. package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.js.map +1 -1
  60. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts +34 -0
  61. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
  62. package/core/dist/runtime-v2/internalization/evaluator-runner.js +295 -12
  63. package/core/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
  64. package/core/dist/runtime-v2/internalization/internalization-orchestrator.d.ts.map +1 -1
  65. package/core/dist/runtime-v2/internalization/internalization-orchestrator.js +10 -54
  66. package/core/dist/runtime-v2/internalization/internalization-orchestrator.js.map +1 -1
  67. package/core/dist/runtime-v2/internalization/internalization-transition-decision.d.ts +7 -1
  68. package/core/dist/runtime-v2/internalization/internalization-transition-decision.d.ts.map +1 -1
  69. package/core/dist/runtime-v2/internalization/internalization-transition-decision.js +9 -2
  70. package/core/dist/runtime-v2/internalization/internalization-transition-decision.js.map +1 -1
  71. package/core/dist/runtime-v2/internalization/owner-resolution-service.d.ts +89 -0
  72. package/core/dist/runtime-v2/internalization/owner-resolution-service.d.ts.map +1 -0
  73. package/core/dist/runtime-v2/internalization/owner-resolution-service.js +272 -0
  74. package/core/dist/runtime-v2/internalization/owner-resolution-service.js.map +1 -0
  75. package/core/dist/runtime-v2/internalization/owner-retry.d.ts +12 -0
  76. package/core/dist/runtime-v2/internalization/owner-retry.d.ts.map +1 -1
  77. package/core/dist/runtime-v2/internalization/owner-retry.js +23 -0
  78. package/core/dist/runtime-v2/internalization/owner-retry.js.map +1 -1
  79. package/core/dist/runtime-v2/internalization/owner-review.d.ts +195 -0
  80. package/core/dist/runtime-v2/internalization/owner-review.d.ts.map +1 -0
  81. package/core/dist/runtime-v2/internalization/owner-review.js +470 -0
  82. package/core/dist/runtime-v2/internalization/owner-review.js.map +1 -0
  83. package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts +5 -1
  84. package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts.map +1 -1
  85. package/core/dist/runtime-v2/internalization/peer-runner-contracts.js.map +1 -1
  86. package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts +68 -0
  87. package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts.map +1 -1
  88. package/core/dist/runtime-v2/internalization/pitask-metadata.js +104 -0
  89. package/core/dist/runtime-v2/internalization/pitask-metadata.js.map +1 -1
  90. package/core/dist/runtime-v2/internalization/revision-reopen.d.ts +56 -0
  91. package/core/dist/runtime-v2/internalization/revision-reopen.d.ts.map +1 -0
  92. package/core/dist/runtime-v2/internalization/revision-reopen.js +144 -0
  93. package/core/dist/runtime-v2/internalization/revision-reopen.js.map +1 -0
  94. package/core/dist/runtime-v2/internalization/rollout-reviewer-runner.d.ts +12 -0
  95. package/core/dist/runtime-v2/internalization/rollout-reviewer-runner.d.ts.map +1 -1
  96. package/core/dist/runtime-v2/internalization/rollout-reviewer-runner.js +145 -14
  97. package/core/dist/runtime-v2/internalization/rollout-reviewer-runner.js.map +1 -1
  98. package/core/dist/runtime-v2/store/context/sqlite-context-assembler.test.js +1 -0
  99. package/core/dist/runtime-v2/store/context/sqlite-context-assembler.test.js.map +1 -1
  100. package/core/dist/runtime-v2/store/runtime-state-manager.d.ts +6 -0
  101. package/core/dist/runtime-v2/store/runtime-state-manager.d.ts.map +1 -1
  102. package/core/dist/runtime-v2/store/runtime-state-manager.js +9 -0
  103. package/core/dist/runtime-v2/store/runtime-state-manager.js.map +1 -1
  104. package/core/dist/runtime-v2/store/task/__tests__/sqlite-task-store-cas.test.d.ts +2 -0
  105. package/core/dist/runtime-v2/store/task/__tests__/sqlite-task-store-cas.test.d.ts.map +1 -0
  106. package/core/dist/runtime-v2/store/task/__tests__/sqlite-task-store-cas.test.js +78 -0
  107. package/core/dist/runtime-v2/store/task/__tests__/sqlite-task-store-cas.test.js.map +1 -0
  108. package/core/dist/runtime-v2/store/task/memory-task-store.d.ts +1 -0
  109. package/core/dist/runtime-v2/store/task/memory-task-store.d.ts.map +1 -1
  110. package/core/dist/runtime-v2/store/task/memory-task-store.js +8 -0
  111. package/core/dist/runtime-v2/store/task/memory-task-store.js.map +1 -1
  112. package/core/dist/runtime-v2/store/task/sqlite-task-store.d.ts +1 -0
  113. package/core/dist/runtime-v2/store/task/sqlite-task-store.d.ts.map +1 -1
  114. package/core/dist/runtime-v2/store/task/sqlite-task-store.js +52 -0
  115. package/core/dist/runtime-v2/store/task/sqlite-task-store.js.map +1 -1
  116. package/core/dist/runtime-v2/store/task/task-store.d.ts +9 -0
  117. package/core/dist/runtime-v2/store/task/task-store.d.ts.map +1 -1
  118. package/core/dist/runtime-v2/store/workspace-isolation.test.js +1 -0
  119. package/core/dist/runtime-v2/store/workspace-isolation.test.js.map +1 -1
  120. package/core/package.json +1 -1
  121. package/host-runtime/package.json +1 -1
  122. package/install-layout/package.json +3 -7
  123. package/package.json +2 -2
  124. package/pd-cli/dist/commands/runtime-internalization-retry.d.ts.map +1 -1
  125. package/pd-cli/dist/commands/runtime-internalization-retry.js +14 -0
  126. package/pd-cli/dist/commands/runtime-internalization-retry.js.map +1 -1
  127. package/pd-cli/package.json +1 -1
  128. package/plugin/dist/bundle.js +580 -569
  129. package/plugin/dist/governance-audit.js +153 -153
  130. package/plugin/dist/openclaw.plugin.json +1 -1
  131. package/plugin/dist/rulehost-evidence.js +180 -180
  132. package/plugin/openclaw.plugin.json +1 -1
  133. package/plugin/package.json +1 -1
@@ -18,6 +18,7 @@ import { handleActivationsRoute, disposeActivationsModels } from './routes/activ
18
18
  import { handleReceiptsRoute, disposeReceiptsModels } from './routes/receipts.js';
19
19
  import { handleApprovalsGroupedRoute, disposeApprovalsGroupedModels } from './routes/approvals-grouped.js';
20
20
  import { handleGovernanceRoute, handleGovernanceExperienceRoute, resolveOwnerConfigSnapshot, disposeGovernanceModels } from './routes/governance.js';
21
+ import { handleOwnerDecisionsRoute } from './routes/owner-decisions.js';
21
22
  import { handleOwnerIdentityRoute } from './routes/owner-identity.js';
22
23
  import { handleEvidenceChainRoute, disposeEvidenceChainModels } from './routes/evidence-chain.js';
23
24
  import { handleIntentRoute, disposeIntentModels } from './routes/intent.js';
@@ -418,6 +419,22 @@ function handleRequest(services) {
418
419
  asyncHandler(() => handleGovernanceRoute(req, res, services.workspaceDir))(req, res);
419
420
  return;
420
421
  }
422
+ // PRI-629: GET/POST /api/v1/governance/owner-decisions — unified Owner Inbox
423
+ // (read projection + resolution). Identity derived server-side (SPEC §29):
424
+ // configured owner when registered, else the authenticated console operator.
425
+ if (urlPath === '/api/v1/governance/owner-decisions' || urlPath.startsWith('/api/v1/governance/owner-decisions/')) {
426
+ const subPath = urlPath.slice('/api/v1/governance/owner-decisions'.length);
427
+ const odIdentity = resolveOwnerIdentity(process.env, defaultOwnerIdentityHomeDir());
428
+ const authEnabledForOwner = services.authConfig.isEnabled() && odIdentity.ownerId && odIdentity.credentialId;
429
+ asyncHandler(() => handleOwnerDecisionsRoute(req, res, {
430
+ workspaceDir: services.workspaceDir,
431
+ subPath,
432
+ ownerIdentity: authEnabledForOwner
433
+ ? { ownerId: odIdentity.ownerId ?? 'console_operator', credentialId: odIdentity.credentialId ?? undefined }
434
+ : { ownerId: 'console_operator' },
435
+ }))(req, res);
436
+ return;
437
+ }
421
438
  // PRI-466/477: GET/POST/PUT /api/v1/intent (+ /init, /content sub-paths)
422
439
  if (urlPath === '/api/v1/intent' || urlPath.startsWith('/api/v1/intent/')) {
423
440
  const subPath = urlPath === '/api/v1/intent' ? '' : urlPath.slice('/api/v1/intent'.length);
@@ -0,0 +1,39 @@
1
+ export type OwnerDecisionItemKind = 'evaluator_review' | 'rollout_review' | 'activation_approval' | 'rulecode_decision';
2
+ export interface OwnerDecisionItem {
3
+ readonly reviewKey: string;
4
+ readonly kind: OwnerDecisionItemKind;
5
+ readonly taskId: string;
6
+ readonly principleId?: string;
7
+ readonly title: string;
8
+ readonly summary: string;
9
+ readonly machineRecommendation?: string;
10
+ readonly score?: number;
11
+ readonly reasonCode: string;
12
+ readonly legacy: boolean;
13
+ readonly allowedActions: readonly string[];
14
+ /** stale 防护事实快照 — POST /resolve 原样回传,服务端重读比对 */
15
+ readonly expectedRevisionEpoch: number;
16
+ readonly expectedSourceRunId: string;
17
+ readonly expectedSourceArtifactId: string;
18
+ readonly expectedSourceArtifactHash: string;
19
+ readonly createdAt: string;
20
+ }
21
+ export interface OwnerDecisionListResult {
22
+ readonly items: readonly OwnerDecisionItem[];
23
+ readonly total: number;
24
+ readonly generatedAt: string;
25
+ }
26
+ export declare class OwnerDecisionConsoleModel {
27
+ private readonly workspaceDir;
28
+ constructor(workspaceDir: string);
29
+ /**
30
+ * 派生当前真实可执行的 Owner 决策列表(INV-01 / SPEC §27 badge 的唯一来源)。
31
+ * N = items.length — 不是 candidate 数、不是 NHR 总数、不是 failed 数。
32
+ */
33
+ listOwnerDecisionItems(): Promise<OwnerDecisionListResult>;
34
+ /**
35
+ * failed-tasks 列表分流字段: 该 NHR 任务是否 decision-capable
36
+ * (decision-capable → 前往治理焦点;technical → Recover)。
37
+ */
38
+ ownerDecisionRequiredTaskIds(): Promise<Set<string>>;
39
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * OwnerDecisionConsoleModel — PRI-629 统一 Owner Decision 读模型(投影)。
3
+ *
4
+ * SPEC §21: 不新建 authority table — 本模型是纯派生 read model,从既有
5
+ * durable facts(tasks / pi_metadata / pi_artifacts / approvals / activations)
6
+ * 经 core 的 collectOwnerDecisionFacts + deriveOwnerDecisionCapability 派生
7
+ * OwnerDecisionItem。INV-01: 只有 allowedActions 非空的条目才进入列表。
8
+ *
9
+ * 策略全部在 @principles/core 的 owner-review.ts(单一深模块);
10
+ * 本模型只负责 workspace I/O 与条目包装(标题/摘要等展示字段)。
11
+ */
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+ import { SqliteConnection, SqlitePIArtifactStore, SqliteApprovalQueueStore, collectOwnerDecisionFacts, deriveOwnerDecisionCapability, } from '@principles/core/runtime-v2';
15
+ const GOVERNANCE_TASK_KINDS = new Set(['evaluator', 'rollout_reviewer']);
16
+ /** tasks.status 合法值(PDTaskStatus)运行时守卫。 */
17
+ const TASK_STATUSES = new Set(['pending', 'leased', 'succeeded', 'retry_wait', 'failed', 'needs_human_review']);
18
+ function isTaskStatus(v) {
19
+ return typeof v === 'string' && TASK_STATUSES.has(v);
20
+ }
21
+ /** 行 → TaskRecord 的窄映射(tasks 表全部列;snake_case 键名转换)。 */
22
+ function rowToTaskRecord(row) {
23
+ if (typeof row.task_id !== 'string' || typeof row.task_kind !== 'string' || typeof row.status !== 'string') {
24
+ return null;
25
+ }
26
+ return {
27
+ taskId: row.task_id,
28
+ taskKind: row.task_kind,
29
+ status: isTaskStatus(row.status) ? row.status : 'pending',
30
+ attemptCount: typeof row.attempt_count === 'number' ? row.attempt_count : 0,
31
+ maxAttempts: typeof row.max_attempts === 'number' ? row.max_attempts : 3,
32
+ createdAt: typeof row.created_at === 'string' ? row.created_at : '',
33
+ updatedAt: typeof row.updated_at === 'string' ? row.updated_at : '',
34
+ // lastError (PDErrorCategory 联合) 与 lease/input/result 列为事实收集器
35
+ // 不需要的字段 — 不映射 (避免对未校验枚举的 as 断言)。
36
+ ...(typeof row.diagnostic_json === 'string' ? { diagnosticJson: row.diagnostic_json } : {}),
37
+ };
38
+ }
39
+ /** 同一 readonly 连接上的 task 行读取闭包(依赖链遍历用)。 */
40
+ /** DB 行守卫: 非空普通对象(列读取处再逐字段 typeof 校验)。 */
41
+ function isRecordRow(v) {
42
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
43
+ }
44
+ function makeRowReader(db) {
45
+ const stmt = db.prepare('SELECT * FROM tasks WHERE task_id = ?');
46
+ return (taskId) => {
47
+ const row = stmt.get(taskId);
48
+ if (!isRecordRow(row))
49
+ return null;
50
+ return row ? rowToTaskRecord(row) : null;
51
+ };
52
+ }
53
+ /** 行 → TaskRecord 的窄映射(tasks 表全部列;diagnostic_json 键名转换)。 */
54
+ /** 单任务 capability → 决策条目(不 eligible → null,INV-01)。 */
55
+ async function deriveTaskDecisionItem(task, artifactStore, readTaskRow) {
56
+ if (!GOVERNANCE_TASK_KINDS.has(task.taskKind))
57
+ return null;
58
+ const factStore = {
59
+ getTask: async (taskId) => {
60
+ if (taskId === task.taskId)
61
+ return task;
62
+ try {
63
+ return readTaskRow(taskId);
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ },
69
+ listArtifactsBySourceTask: async (taskId) => {
70
+ const list = await artifactStore.listBySourceTaskId(taskId).catch(() => []);
71
+ return list.map((a) => ({
72
+ artifactId: a.artifactId,
73
+ artifactKind: a.artifactKind,
74
+ validationStatus: a.validationStatus,
75
+ contentJson: a.contentJson,
76
+ }));
77
+ },
78
+ };
79
+ const facts = await collectOwnerDecisionFacts(factStore, task.taskId);
80
+ if (!facts)
81
+ return null;
82
+ const capability = deriveOwnerDecisionCapability(facts);
83
+ if (!capability.eligible || !capability.reviewKey)
84
+ return null;
85
+ const sourceRunId = facts.task.humanReviewContext?.sourceRunId
86
+ ?? facts.task.completionIntent?.sourceRunId ?? '';
87
+ const artifact = facts.decisionArtifact;
88
+ const kind = task.taskKind === 'rollout_reviewer' ? 'rollout_review' : 'evaluator_review';
89
+ return {
90
+ reviewKey: capability.reviewKey,
91
+ kind,
92
+ taskId: task.taskId,
93
+ title: kind === 'rollout_review'
94
+ ? 'Rollout 评审无法收敛'
95
+ : '自动改进已达到本轮上限',
96
+ summary: `机器建议继续修改(${capability.reasonCode}${capability.legacy ? ',由历史数据安全推断' : ''})。需要你的判断:接受当前版本、再修改一次,或拒绝。`,
97
+ machineRecommendation: 'needs_revision(继续修改)',
98
+ reasonCode: capability.reasonCode,
99
+ legacy: capability.legacy,
100
+ allowedActions: [...capability.allowedActions],
101
+ expectedRevisionEpoch: facts.task.revisionCount ?? 0,
102
+ expectedSourceRunId: sourceRunId,
103
+ expectedSourceArtifactId: artifact?.artifactId ?? '',
104
+ expectedSourceArtifactHash: artifact?.contentHash ?? '',
105
+ createdAt: task.updatedAt,
106
+ };
107
+ }
108
+ export class OwnerDecisionConsoleModel {
109
+ workspaceDir;
110
+ constructor(workspaceDir) {
111
+ this.workspaceDir = workspaceDir;
112
+ }
113
+ /**
114
+ * 派生当前真实可执行的 Owner 决策列表(INV-01 / SPEC §27 badge 的唯一来源)。
115
+ * N = items.length — 不是 candidate 数、不是 NHR 总数、不是 failed 数。
116
+ */
117
+ async listOwnerDecisionItems() {
118
+ const stateDbPath = path.join(this.workspaceDir, '.pd', 'state.db');
119
+ if (!fs.existsSync(stateDbPath)) {
120
+ return { items: [], total: 0, generatedAt: new Date().toISOString() };
121
+ }
122
+ const conn = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: true });
123
+ try {
124
+ const db = conn.getDb();
125
+ const artifactStore = new SqlitePIArtifactStore(conn);
126
+ const items = [];
127
+ // ── 1. needs_human_review 的 decision-capable 任务 (PRI-629 核心) ──
128
+ let nhrRows = [];
129
+ try {
130
+ nhrRows = db.prepare(`SELECT * FROM tasks WHERE status = 'needs_human_review' AND task_kind IN ('evaluator', 'rollout_reviewer')`).all().filter(isRecordRow);
131
+ }
132
+ catch {
133
+ nhrRows = []; // tasks 表缺失(旧库)→ 无决策项
134
+ }
135
+ for (const row of nhrRows) {
136
+ const task = rowToTaskRecord(row);
137
+ if (!task)
138
+ continue;
139
+ const item = await deriveTaskDecisionItem(task, artifactStore, makeRowReader(db));
140
+ if (item)
141
+ items.push(item);
142
+ }
143
+ // ── 2. 既有 approvals pending(高风险部署审批 — 独立门,动作走既有 API)──
144
+ try {
145
+ const approvals = await new SqliteApprovalQueueStore(conn).listPending();
146
+ for (const approval of approvals) {
147
+ items.push({
148
+ reviewKey: `apr:${approval.approvalId}`,
149
+ kind: 'activation_approval',
150
+ taskId: approval.artifactId,
151
+ title: `部署审批 · ${approval.channel}`,
152
+ summary: `${approval.riskLevel} 风险通道 ${approval.channel} 的原则部署等待批准。`,
153
+ reasonCode: 'activation_approval_pending',
154
+ legacy: false,
155
+ allowedActions: ['approve', 'reject'],
156
+ expectedRevisionEpoch: 0,
157
+ expectedSourceRunId: '',
158
+ expectedSourceArtifactId: approval.artifactId,
159
+ expectedSourceArtifactHash: '',
160
+ createdAt: approval.requestedAt,
161
+ });
162
+ }
163
+ }
164
+ catch {
165
+ // approvals 表缺失 → 跳过(恒空也是常态:低风险自动激活)
166
+ }
167
+ // ── 3. RuleCode shadow 待决(promote / reject — 独立 authority,动作走既有 API)──
168
+ try {
169
+ const shadowRows = db.prepare(`SELECT activation_id, channel, activated_at FROM activations
170
+ WHERE action = 'code_tool_hook_shadow_activate'
171
+ AND promoted_at IS NULL AND deactivated_at IS NULL`).all().filter(isRecordRow);
172
+ for (const row of shadowRows) {
173
+ items.push({
174
+ reviewKey: `rulecode:${String(row.activation_id ?? '')}`,
175
+ kind: 'rulecode_decision',
176
+ taskId: String(row.activation_id ?? ''),
177
+ title: 'RuleCode 影子规则待决',
178
+ summary: '一条 code_tool_hook 规则处于影子激活期,等待拥有者裁决(提升 / 拒绝)。',
179
+ reasonCode: 'rulecode_shadow_pending_promotion',
180
+ legacy: false,
181
+ allowedActions: ['promote', 'reject_after_shadow'],
182
+ expectedRevisionEpoch: 0,
183
+ expectedSourceRunId: '',
184
+ expectedSourceArtifactId: String(row.activation_id ?? ''),
185
+ expectedSourceArtifactHash: '',
186
+ createdAt: typeof row.activated_at === 'string' ? row.activated_at : '',
187
+ });
188
+ }
189
+ }
190
+ catch {
191
+ // activations 表缺失 → 跳过
192
+ }
193
+ return { items, total: items.length, generatedAt: new Date().toISOString() };
194
+ }
195
+ finally {
196
+ conn.close();
197
+ }
198
+ }
199
+ /**
200
+ * failed-tasks 列表分流字段: 该 NHR 任务是否 decision-capable
201
+ * (decision-capable → 前往治理焦点;technical → Recover)。
202
+ */
203
+ async ownerDecisionRequiredTaskIds() {
204
+ const stateDbPath = path.join(this.workspaceDir, '.pd', 'state.db');
205
+ if (!fs.existsSync(stateDbPath))
206
+ return new Set();
207
+ const conn = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: true });
208
+ try {
209
+ const db = conn.getDb();
210
+ const artifactStore = new SqlitePIArtifactStore(conn);
211
+ const ids = new Set();
212
+ let rows = [];
213
+ try {
214
+ rows = db.prepare(`SELECT * FROM tasks WHERE status = 'needs_human_review' AND task_kind IN ('evaluator', 'rollout_reviewer')`).all().filter(isRecordRow);
215
+ }
216
+ catch {
217
+ return ids;
218
+ }
219
+ for (const row of rows) {
220
+ const task = rowToTaskRecord(row);
221
+ if (!task)
222
+ continue;
223
+ const item = await deriveTaskDecisionItem(task, artifactStore, makeRowReader(db));
224
+ if (item)
225
+ ids.add(task.taskId);
226
+ }
227
+ return ids;
228
+ }
229
+ finally {
230
+ conn.close();
231
+ }
232
+ }
233
+ }
@@ -13,7 +13,7 @@
13
13
  * Classification is purely read-side; it does NOT mutate any data.
14
14
  */
15
15
  import type { PrincipleListItem } from './PrinciplesConsoleModel.js';
16
- export type PrincipleCategory = 'owner_actionable' | 'demo' | 'smoke' | 'historical' | 'builtin' | 'already_decided';
16
+ export type PrincipleCategory = 'owner_actionable' | 'in_pipeline' | 'demo' | 'smoke' | 'historical' | 'builtin' | 'already_decided';
17
17
  export interface ClassifiedPrinciple {
18
18
  principle: PrincipleListItem;
19
19
  category: PrincipleCategory;
@@ -26,7 +26,7 @@ export interface ClassifiedPrinciple {
26
26
  * via the approval queue (approved or rejected). These should be classified
27
27
  * as already_decided regardless of their ledger status.
28
28
  */
29
- export declare function classifyPrinciple(p: PrincipleListItem, decidedPrincipleIds?: Set<string>): PrincipleCategory;
29
+ export declare function classifyPrinciple(p: PrincipleListItem, decidedPrincipleIds?: Set<string>, pendingApprovalPrincipleIds?: Set<string>): PrincipleCategory;
30
30
  /**
31
31
  * Classify a batch of principles. Returns the classified array.
32
32
  *
@@ -34,7 +34,7 @@ export declare function classifyPrinciple(p: PrincipleListItem, decidedPrinciple
34
34
  * @param decidedPrincipleIds - Set of principle IDs that have been decided
35
35
  * via the approval queue (approved or rejected).
36
36
  */
37
- export declare function classifyPrinciples(principles: PrincipleListItem[], decidedPrincipleIds?: Set<string>): ClassifiedPrinciple[];
37
+ export declare function classifyPrinciples(principles: PrincipleListItem[], decidedPrincipleIds?: Set<string>, pendingApprovalPrincipleIds?: Set<string>): ClassifiedPrinciple[];
38
38
  /**
39
39
  * Filter to only owner-actionable principles.
40
40
  */
@@ -47,7 +47,7 @@ function isBuiltinId(id) {
47
47
  * via the approval queue (approved or rejected). These should be classified
48
48
  * as already_decided regardless of their ledger status.
49
49
  */
50
- export function classifyPrinciple(p, decidedPrincipleIds) {
50
+ export function classifyPrinciple(p, decidedPrincipleIds, pendingApprovalPrincipleIds) {
51
51
  // 1. Builtin axioms (T-01..T-10) are never governance targets
52
52
  if (isBuiltinId(p.id)) {
53
53
  return 'builtin';
@@ -74,6 +74,11 @@ export function classifyPrinciple(p, decidedPrincipleIds) {
74
74
  if (decidedPrincipleIds && decidedPrincipleIds.has(p.id)) {
75
75
  return 'already_decided';
76
76
  }
77
+ // 4b. PRI-629: a PENDING approval is a real Owner decision — owner_actionable
78
+ // (INV-01: actionability requires an executable action, and it exists here).
79
+ if (pendingApprovalPrincipleIds && pendingApprovalPrincipleIds.has(p.id)) {
80
+ return 'owner_actionable';
81
+ }
77
82
  // 5. Already decided: active = approved & in effect, no governance needed
78
83
  if (p.status === 'active') {
79
84
  return 'already_decided';
@@ -82,8 +87,10 @@ export function classifyPrinciple(p, decidedPrincipleIds) {
82
87
  if (p.status === 'archived' || p.status === 'deprecated') {
83
88
  return 'historical';
84
89
  }
85
- // 7. Only candidate / probation need owner decision
86
- return 'owner_actionable';
90
+ // 7. candidate / probation = 内化管线进行中 (PRI-629 INV-02: lifecycle ≠
91
+ // Owner attention)。真正的 Owner 决策由治理焦点的 Owner Decision
92
+ // 投影呈现;此处不再产生假"待审查"。
93
+ return 'in_pipeline';
87
94
  }
88
95
  /**
89
96
  * Classify a batch of principles. Returns the classified array.
@@ -92,8 +99,8 @@ export function classifyPrinciple(p, decidedPrincipleIds) {
92
99
  * @param decidedPrincipleIds - Set of principle IDs that have been decided
93
100
  * via the approval queue (approved or rejected).
94
101
  */
95
- export function classifyPrinciples(principles, decidedPrincipleIds) {
96
- return principles.map((p) => ({ principle: p, category: classifyPrinciple(p, decidedPrincipleIds) }));
102
+ export function classifyPrinciples(principles, decidedPrincipleIds, pendingApprovalPrincipleIds) {
103
+ return principles.map((p) => ({ principle: p, category: classifyPrinciple(p, decidedPrincipleIds, pendingApprovalPrincipleIds) }));
97
104
  }
98
105
  /**
99
106
  * Filter to only owner-actionable principles.
@@ -76,7 +76,7 @@ export declare class PrinciplesConsoleModel {
76
76
  constructor(workspaceDir: string);
77
77
  private getLedgerPath;
78
78
  private loadLedger;
79
- listPrinciples(filter?: PrincipleFilter, decidedPrincipleIds?: Set<string>): Promise<PrinciplesListOutput>;
79
+ listPrinciples(filter?: PrincipleFilter, decidedPrincipleIds?: Set<string>, pendingApprovalPrincipleIds?: Set<string>): Promise<PrinciplesListOutput>;
80
80
  getPrincipleDetail(principleId: string): Promise<PrincipleDetailOutput | null>;
81
81
  archivePrinciple(principleId: string): Promise<boolean>;
82
82
  unarchivePrinciple(principleId: string): Promise<boolean>;
@@ -125,7 +125,7 @@ export class PrinciplesConsoleModel {
125
125
  this.cacheTimestamp = now;
126
126
  return ledger;
127
127
  }
128
- async listPrinciples(filter, decidedPrincipleIds) {
128
+ async listPrinciples(filter, decidedPrincipleIds, pendingApprovalPrincipleIds) {
129
129
  const ledger = this.loadLedger();
130
130
  const principles = Object.values(ledger.tree.principles);
131
131
  const summary = {
@@ -181,7 +181,7 @@ export class PrinciplesConsoleModel {
181
181
  }
182
182
  items.sort((a, b) => b.valueScore - a.valueScore);
183
183
  // PRI-330: classify and optionally filter
184
- const classified = classifyPrinciples(items, decidedPrincipleIds);
184
+ const classified = classifyPrinciples(items, decidedPrincipleIds, pendingApprovalPrincipleIds);
185
185
  const categories = {};
186
186
  for (const c of classified) {
187
187
  categories[c.category] = (categories[c.category] ?? 0) + 1;
@@ -25,6 +25,7 @@ import * as path from 'node:path';
25
25
  import { SqliteConnection, SqliteTaskStore, PDRuntimeError, createRecoverySweepService, appendRecoveryAction, } from '@principles/core/runtime-v2';
26
26
  import { sendSuccess, sendError, sendNotFound, sendMethodNotAllowed, sendBadRequest, } from '../utils/response.js';
27
27
  import { readBody } from '../utils/request.js';
28
+ import { OwnerDecisionConsoleModel } from '../models/OwnerDecisionConsoleModel.js';
28
29
  const entries = new Map();
29
30
  function stateDbExists(workspaceDir) {
30
31
  return fs.existsSync(path.join(workspaceDir, '.pd', 'state.db'));
@@ -149,6 +150,15 @@ function parseRecoverBody(body) {
149
150
  * is raised (core raises it by +3). needs_human_review resets ignore it —
150
151
  * the owner authority reset has its own budget semantics.
151
152
  */
153
+ const ownerDecisionModels = new Map();
154
+ function getOwnerDecisionModel(workspaceDir) {
155
+ let model = ownerDecisionModels.get(workspaceDir);
156
+ if (!model) {
157
+ model = new OwnerDecisionConsoleModel(workspaceDir);
158
+ ownerDecisionModels.set(workspaceDir, model);
159
+ }
160
+ return model;
161
+ }
152
162
  async function dispatchRecovery(workspaceDir, taskId, force) {
153
163
  const { service, close } = await createRecoverySweepService({ workspaceDir });
154
164
  try {
@@ -184,6 +194,19 @@ async function dispatchRecovery(workspaceDir, taskId, force) {
184
194
  }
185
195
  // not failed (or missing) → owner authority reset path
186
196
  const outcome = await service.recoverNeedsHumanReviewTask(taskId);
197
+ if (outcome.status === 'rejected') {
198
+ // PRI-629 Recover guard (SPEC §17): decision-capable 人工裁决不允许
199
+ // authority reset — Recover 不是治理出口,出口在治理焦点 Owner Decision。
200
+ return {
201
+ ok: false,
202
+ failure: {
203
+ httpStatus: 409,
204
+ errorCode: 'owner_decision_required',
205
+ message: 'This task awaits an Owner decision (accept current / revise once / reject). Recover is not a governance exit and was refused.',
206
+ nextAction: 'Open the Console governance focus (#/focus) and resolve the decision there.',
207
+ },
208
+ };
209
+ }
187
210
  if (outcome.status === 'requeued') {
188
211
  return {
189
212
  ok: true,
@@ -307,19 +330,34 @@ export async function handleFailedTasksRoute(req, res, ctx) {
307
330
  const store = ctx.sqliteTaskStore ?? getStore(workspaceDir);
308
331
  const tasks = await store.listFailedTasks({ kind, since, limit: limitResult, offset: offsetResult });
309
332
  const total = await store.countFailedTasks({ kind, since });
333
+ // PRI-629 分流: decision-capable NHR 任务标 ownerDecisionRequired —
334
+ // UI 隐藏 Recover、引导前往治理焦点 (决策出口不在失败页)。
335
+ let decisionRequiredIds = null;
336
+ try {
337
+ decisionRequiredIds = await getOwnerDecisionModel(workspaceDir).ownerDecisionRequiredTaskIds();
338
+ }
339
+ catch {
340
+ decisionRequiredIds = null; // 投影失败不阻塞失败列表 (rc-9 降级由 UI 兜底)
341
+ }
342
+ const enriched = tasks.map((task) => ({
343
+ ...task,
344
+ ...(decisionRequiredIds !== null && task.status === 'needs_human_review'
345
+ ? { ownerDecisionRequired: decisionRequiredIds.has(task.taskId ?? '') }
346
+ : {}),
347
+ }));
310
348
  // Only report "PD pipeline is healthy" when total === 0. When
311
349
  // tasks.length === 0 but total > 0, the caller has paginated past the
312
350
  // last page — returning "healthy" here would mask the failures that
313
351
  // exist on earlier pages.
314
352
  if (total === 0) {
315
353
  sendSuccess(res, {
316
- tasks,
354
+ tasks: enriched,
317
355
  total,
318
356
  nextAction: 'No failed tasks. PD pipeline is healthy.',
319
357
  });
320
358
  }
321
359
  else {
322
- sendSuccess(res, { tasks, total });
360
+ sendSuccess(res, { tasks: enriched, total });
323
361
  }
324
362
  }
325
363
  catch (err) {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * PRI-629 — /api/v1/governance/owner-decisions 路由。
3
+ *
4
+ * GET /api/v1/governance/owner-decisions — 统一 Owner Inbox 投影
5
+ * POST /api/v1/governance/owner-decisions/:taskId/resolve — Owner 裁决
6
+ *
7
+ * 安全模型(SPEC §28/§29):
8
+ * - 全局 console token 认证由 server/index.ts 统一强制;
9
+ * - 身份从 server-side auth context 推导(configured_owner 优先,其次
10
+ * console operator),body 里的身份字段一律不信任;
11
+ * - effectiveDecision / eligibility 全部服务端重读 durable facts 判定;
12
+ * - stale 防护: reviewKey + expected* 事实快照逐字段比对(409)。
13
+ */
14
+ import type { IncomingMessage, ServerResponse } from 'node:http';
15
+ export interface OwnerDecisionRouteContext {
16
+ workspaceDir: string;
17
+ /** server-side auth context 推导的身份(SPEC §29 — 不信任 body) */
18
+ ownerIdentity: {
19
+ ownerId: string;
20
+ credentialId?: string;
21
+ };
22
+ }
23
+ export declare function handleOwnerDecisionsRoute(req: IncomingMessage, res: ServerResponse, ctx: OwnerDecisionRouteContext & {
24
+ subPath: string;
25
+ }): Promise<void>;