@principles/pd-cli 1.135.1 → 1.135.2

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 (26) hide show
  1. package/dist/commands/runtime-activation.d.ts.map +1 -1
  2. package/dist/commands/runtime-activation.js +36 -7
  3. package/dist/commands/runtime-activation.js.map +1 -1
  4. package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
  5. package/dist/commands/runtime-internalization-enqueue-successors.js +48 -0
  6. package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
  7. package/dist/commands/runtime-internalization-retry.d.ts +38 -0
  8. package/dist/commands/runtime-internalization-retry.d.ts.map +1 -0
  9. package/dist/commands/runtime-internalization-retry.js +143 -0
  10. package/dist/commands/runtime-internalization-retry.js.map +1 -0
  11. package/dist/index.js +11 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/services/__tests__/evaluator-runner-deps.test.js +19 -7
  14. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -1
  15. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  16. package/dist/services/rulehost-pipeline-runner.js +6 -2
  17. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  18. package/package.json +1 -1
  19. package/src/commands/runtime-activation.ts +38 -6
  20. package/src/commands/runtime-internalization-enqueue-successors.ts +48 -0
  21. package/src/commands/runtime-internalization-retry.ts +163 -0
  22. package/src/index.ts +12 -0
  23. package/src/services/__tests__/evaluator-runner-deps.test.ts +20 -8
  24. package/src/services/rulehost-pipeline-runner.ts +5 -2
  25. package/tests/commands/cli-command-tree.test.ts +15 -0
  26. package/tests/commands/runtime-internalization-retry-owner-authority.test.ts +431 -0
@@ -0,0 +1,163 @@
1
+ /**
2
+ * pd runtime internalization retry — needs_human_review 的 Owner 出边
3
+ * (MVP_CORE_LOOP_CONTRACT INV-03: inspect / retry / revise / reject-archive)。
4
+ *
5
+ * 修复前 needs_human_review 是 display-only 单向终态 (审计 ISSUE-006)。
6
+ * 本命令把 needs_human_review 任务重新入队 (→ pending, attemptCount 重置),
7
+ * 由 auto-consumer / run-once 重新驱动。
8
+ *
9
+ * Owner retry = 显式人类 authority reset,与 crash retry 严格区分:
10
+ * crash / lease recovery / automatic retry 保留 completionIntent(入口门
11
+ * resume 原 verdict,零 LLM);Owner retry 必须同时清空 runnerDecision 与
12
+ * completionIntent,允许新一轮 LLM verdict 成为 authority——否则入口门会
13
+ * resume/finalize 旧 verdict,LLM 永不运行,Owner retry 实际失效。
14
+ *
15
+ * 落库形态: status/attemptCount 与清空后的 metadata 在同一次 updateTask
16
+ * (SQLite 单条 UPDATE) 中原子生效——两个独立写之间失败会留下
17
+ * "authority 已清但任务仍 needs_human_review" 的 partial Owner action。
18
+ * metadata 不可 hydrate 时 fail closed (metadata_invalid),不得只改 status。
19
+ *
20
+ * CLI gate: 默认 dry-run;--confirm 才落地 (cli-4);JSON 模式严格单对象 (cli-1);
21
+ * 失败路径不产生任何状态变更 (cli-5)。
22
+ */
23
+
24
+ import * as path from 'path';
25
+ import { RuntimeStateManager } from '@principles/core/runtime-v2';
26
+ import { hydratePITaskRecord, createPITaskDiagnosticJson, mergePITaskMetadata } from '@principles/core/runtime-v2';
27
+ import { resolveWorkspaceDir } from '../resolve-workspace.js';
28
+
29
+ export interface InternalizationRetryOptions {
30
+ workspace?: string;
31
+ taskId?: string;
32
+ confirm?: boolean;
33
+ json?: boolean;
34
+ }
35
+
36
+ export interface InternalizationRetryOutput {
37
+ status: 'requeued' | 'dry_run' | 'skipped' | 'failed';
38
+ taskId: string;
39
+ taskKind?: string;
40
+ previousStatus?: string;
41
+ reason?: string;
42
+ nextAction?: string;
43
+ }
44
+
45
+ function emit(out: InternalizationRetryOutput, json?: boolean): void {
46
+ if (json) {
47
+ console.log(JSON.stringify(out, null, 2));
48
+ return;
49
+ }
50
+ console.log(`Retry: ${out.status}${out.previousStatus ? ` (was ${out.previousStatus})` : ''}`);
51
+ if (out.reason) console.log(` reason: ${out.reason}`);
52
+ if (out.nextAction) console.log(` nextAction: ${out.nextAction}`);
53
+ }
54
+
55
+ export async function handleRuntimeInternalizationRetry(opts: InternalizationRetryOptions): Promise<void> {
56
+ if (!opts.taskId) {
57
+ const out: InternalizationRetryOutput = {
58
+ status: 'failed',
59
+ taskId: '',
60
+ reason: 'task_id_required',
61
+ nextAction: 'Pass --task <taskId> (find ids via: pd runtime internalization queue --json or pd errors list)',
62
+ };
63
+ emit(out, opts.json);
64
+ process.exitCode = 1;
65
+ return;
66
+ }
67
+
68
+ const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
69
+ const stateManager = new RuntimeStateManager({ workspaceDir });
70
+ try {
71
+ await stateManager.initialize();
72
+ const task = await stateManager.getTask(opts.taskId);
73
+ if (!task) {
74
+ const out: InternalizationRetryOutput = {
75
+ status: 'failed',
76
+ taskId: opts.taskId,
77
+ reason: 'task_not_found',
78
+ nextAction: 'Verify the task id and workspace',
79
+ };
80
+ emit(out, opts.json);
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+
85
+ if (task.status !== 'needs_human_review') {
86
+ const out: InternalizationRetryOutput = {
87
+ status: 'skipped',
88
+ taskId: opts.taskId,
89
+ taskKind: task.taskKind,
90
+ previousStatus: task.status,
91
+ reason: 'only_needs_human_review_tasks_are_retryable',
92
+ nextAction: 'This task is not in the owner attention queue; use run-once / enqueue-successors instead',
93
+ };
94
+ emit(out, opts.json);
95
+ return;
96
+ }
97
+
98
+ if (!opts.confirm) {
99
+ const out: InternalizationRetryOutput = {
100
+ status: 'dry_run',
101
+ taskId: opts.taskId,
102
+ taskKind: task.taskKind,
103
+ previousStatus: task.status,
104
+ reason: 'dry_run_no_mutation',
105
+ nextAction: 'Re-run with --confirm to requeue this task',
106
+ };
107
+ emit(out, opts.json);
108
+ return;
109
+ }
110
+
111
+ // Owner retry = authority reset: runnerDecision 与 completionIntent 同时
112
+ // 清空 (保留 revisionCount / revisionCauseId / rolloutRevisionPayload /
113
+ // repairPayload / lineage — revision budget 证据不动)。
114
+ const piTask = hydratePITaskRecord(task);
115
+ if (!piTask) {
116
+ // fail closed: 只改 status 会把(可能损坏的)旧 authority 记录原样留在
117
+ // metadata 里,下一次 run 由它接管 —— 产生 partial retry。
118
+ const out: InternalizationRetryOutput = {
119
+ status: 'failed',
120
+ taskId: opts.taskId,
121
+ taskKind: task.taskKind,
122
+ previousStatus: task.status,
123
+ reason: 'metadata_invalid',
124
+ nextAction: 'Task metadata failed PI hydration; a retry now would risk a partial authority reset. Inspect: pd runtime internalization integrity --json',
125
+ };
126
+ emit(out, opts.json);
127
+ process.exitCode = 1;
128
+ return;
129
+ }
130
+ // 原子单写: 同一 patch 同时落 status=pending / attemptCount=0 / 清空后的
131
+ // diagnosticJson。updateTask 抛错时 DB 行保持原样(单条 UPDATE),无 partial reset。
132
+ const merged = mergePITaskMetadata(piTask, {
133
+ runnerDecision: undefined,
134
+ completionIntent: undefined,
135
+ });
136
+ await stateManager.updateTask(opts.taskId, {
137
+ status: 'pending',
138
+ attemptCount: 0,
139
+ diagnosticJson: createPITaskDiagnosticJson(merged),
140
+ });
141
+
142
+ const out: InternalizationRetryOutput = {
143
+ status: 'requeued',
144
+ taskId: opts.taskId,
145
+ taskKind: task.taskKind,
146
+ previousStatus: task.status,
147
+ nextAction: 'Task requeued; it will be picked up by the auto-consumer cycle, or advance manually: pd runtime internalization run-once',
148
+ };
149
+ emit(out, opts.json);
150
+ } catch (err) {
151
+ const out: InternalizationRetryOutput = {
152
+ status: 'failed',
153
+ taskId: opts.taskId,
154
+ reason: err instanceof Error ? err.message : String(err),
155
+ nextAction: 'Check workspace DB integrity (pd runtime internalization integrity)',
156
+ };
157
+ emit(out, opts.json);
158
+ process.exitCode = 1;
159
+ } finally {
160
+ await stateManager.close();
161
+ }
162
+ }
163
+
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ import { handleRuntimeUat } from './commands/runtime-uat.js';
33
33
  import { handleRuntimeInternalizationQueue } from './commands/runtime-internalization-queue.js';
34
34
  import { handleRuntimeInternalizationWakeOnce } from './commands/runtime-internalization-wake-once.js';
35
35
  import { handleRuntimeInternalizationRunOnce } from './commands/runtime-internalization-run-once.js';
36
+ import { handleRuntimeInternalizationRetry } from './commands/runtime-internalization-retry.js';
36
37
  import { registerRunRuleHostCommand } from './commands/runtime-internalization-run-rulehost.js';
37
38
  import { handleCandidateList, handleCandidateShow, handleCandidateIntake, handleCandidateAudit, handleCandidateRepair, handleCandidateRoute, handleCandidateInternalize, handleCandidateInternalizationBackfill } from './commands/candidate.js';
38
39
  import { handleArtifactShow } from './commands/artifact.js';
@@ -554,6 +555,17 @@ internalizationCmd
554
555
  await handleRuntimeInternalizationWakeOnce({ workspace: opts.workspace, dryRun: opts.dryRun, json: opts.json });
555
556
  });
556
557
 
558
+ internalizationCmd
559
+ .command('retry')
560
+ .description('Requeue a needs_human_review task (owner attention queue out-edge)')
561
+ .option('-w, --workspace <path>', 'Workspace directory')
562
+ .requiredOption('--task <taskId>', 'Task id to requeue')
563
+ .option('--confirm', 'Actually requeue (default is dry-run)')
564
+ .option('--json', 'Output as JSON')
565
+ .action(async (opts) => {
566
+ await handleRuntimeInternalizationRetry({ workspace: opts.workspace, taskId: opts.task, confirm: opts.confirm, json: opts.json });
567
+ });
568
+
557
569
  internalizationCmd
558
570
  .command('run-once')
559
571
  .description('Wake-and-run: lease the next PI task and execute it')
@@ -100,6 +100,8 @@ function createMockStateManager(): {
100
100
  } {
101
101
  const createdTasks: TaskRecord[] = [];
102
102
  const stateManager = {
103
+ getTask: vi.fn(async (taskId: string) =>
104
+ createdTasks.find((t) => t.taskId === taskId) ?? null),
103
105
  createTask: vi.fn(async (record: Omit<TaskRecord, 'createdAt' | 'updatedAt'>) => {
104
106
  const now = new Date().toISOString();
105
107
  const task: TaskRecord = { ...record, createdAt: now, updatedAt: now };
@@ -185,7 +187,7 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
185
187
  expect(deps.isRepairLoopEnabled()).toBe(false);
186
188
  });
187
189
 
188
- it('flag absent in config → isRepairLoopEnabled() returns false (defaults apply)', () => {
190
+ it('flag absent in config → isRepairLoopEnabled() returns true (P0-D: default-on since core-loop closure)', () => {
189
191
  const workspaceDir = createTempWorkspace(null);
190
192
  tmpWorkspaces.push(workspaceDir);
191
193
  const { stateManager } = createMockStateManager();
@@ -201,10 +203,11 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
201
203
 
202
204
  expect(typeof deps.isRepairLoopEnabled).toBe('function');
203
205
  if (typeof deps.isRepairLoopEnabled !== 'function') throw new Error('isRepairLoopEnabled missing');
204
- expect(deps.isRepairLoopEnabled()).toBe(false);
206
+ // 契约变更 (2026-08-18, INV-02): registry 默认 ON;flag 缺省 = 默认生效
207
+ expect(deps.isRepairLoopEnabled()).toBe(true);
205
208
  });
206
209
 
207
- it('malformed config → isRepairLoopEnabled() returns false (rc-9: fail safe, not throw)', () => {
210
+ it('malformed config → isRepairLoopEnabled() falls back to registry defaults (default-on), never throws', () => {
208
211
  // CodeQL: use mkdtempSync for atomic, unpredictable temp dir creation.
209
212
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pri-510-malformed-'));
210
213
  tmpWorkspaces.push(tmpDir);
@@ -223,10 +226,11 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
223
226
  workspaceDir: tmpDir,
224
227
  });
225
228
 
226
- // Malformed config must NOT throw — return false so the legacy path runs.
229
+ // Malformed config must NOT throw — falls back to registry defaults
230
+ // (P0-D: evaluator_artificer_repair_loop default-on since core-loop closure).
227
231
  expect(typeof deps.isRepairLoopEnabled).toBe('function');
228
232
  if (typeof deps.isRepairLoopEnabled !== 'function') throw new Error('isRepairLoopEnabled missing');
229
- expect(deps.isRepairLoopEnabled()).toBe(false);
233
+ expect(deps.isRepairLoopEnabled()).toBe(true);
230
234
  });
231
235
 
232
236
  it('seedArtificerRepairTask → creates artificer task with repairPayload in diagnosticJson (rc-1, rc-6)', async () => {
@@ -286,7 +290,7 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
286
290
  expect(meta.inputArtifactRefs).toEqual(params.inheritedInputArtifactRefs);
287
291
  });
288
292
 
289
- it('seedArtificerRepairTask → each call returns a UNIQUE task ID (rc-7: no stale state)', async () => {
293
+ it('seedArtificerRepairTask → deterministic id + replay reuse (P0-4); 不同 iteration 不同 id (rc-7)', async () => {
290
294
  const workspaceDir = createTempWorkspace(true);
291
295
  tmpWorkspaces.push(workspaceDir);
292
296
  const { stateManager } = createMockStateManager();
@@ -303,9 +307,17 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
303
307
 
304
308
  if (typeof deps.seedArtificerRepairTask !== 'function') throw new Error('seedArtificerRepairTask missing');
305
309
  const id1 = await deps.seedArtificerRepairTask(params);
310
+ // P0-4: 同一 evaluator+iteration 的重放 (consumer 重复周期 / crash 恢复)
311
+ // reuse 同一确定性 id,不重复创建
312
+ expect(id1).toBe(`artificer-repair-${params.repairPayload.sourceEvaluatorTaskId}-r${params.repairPayload.repairIteration}`);
306
313
  const id2 = await deps.seedArtificerRepairTask(params);
307
-
308
- expect(id1).not.toBe(id2);
314
+ expect(id2).toBe(id1);
315
+ // 不同 iteration (下一逻辑修复轮) → 不同 id
316
+ const id3 = await deps.seedArtificerRepairTask({
317
+ ...params,
318
+ repairPayload: { ...params.repairPayload, repairIteration: params.repairPayload.repairIteration + 1 },
319
+ });
320
+ expect(id3).not.toBe(id1);
309
321
  });
310
322
 
311
323
  it('deps spread contains all required base PeerRunnerDeps fields (EP-02: real path gets full deps)', () => {
@@ -66,7 +66,7 @@ import type {
66
66
  SeedArtificerRepairParams,
67
67
  EvaluatorValidator,
68
68
  } from '@principles/core/runtime-v2';
69
- import { randomUUID, createHash } from 'node:crypto';
69
+ import { createHash } from 'node:crypto';
70
70
  import { loadPdConfig } from './pd-config-loader.js';
71
71
  /* eslint-disable @typescript-eslint/no-use-before-define -- helpers declared after main, matching codebase convention */
72
72
  import { compileDemoRule } from './demo-rule-compiler.js';
@@ -646,7 +646,10 @@ export function createEvaluatorRunnerDeps(inputs: CreateEvaluatorRunnerDepsInput
646
646
  },
647
647
  seedArtificerRepairTask: async (params: SeedArtificerRepairParams): Promise<string> => {
648
648
  // rc-7: each call gets a fresh task ID — never reuse a cached ID.
649
- const repairTaskId = `artificer-repair-${randomUUID()}`;
649
+ // P0-4: deterministic revision identity + reuse on replay
650
+ const repairTaskId = `artificer-repair-${params.repairPayload.sourceEvaluatorTaskId}-r${params.repairPayload.repairIteration}`;
651
+ const existing = await stateManager.getTask(repairTaskId);
652
+ if (existing) return repairTaskId;
650
653
  await stateManager.createTask({
651
654
  taskId: repairTaskId,
652
655
  // D1 (PRI-509): task kind is 'artificer' — reuses the artificer
@@ -126,4 +126,19 @@ describe('CLI command tree structure', () => {
126
126
  const output = runPdHelp(['legacy', 'cleanup', '--help']);
127
127
  expect(output).toContain('V1 Artificer');
128
128
  });
129
+
130
+ // cli-7-test-wiring (PR #1358 known gap 补齐): retry 是 INV-03 的核心 Owner
131
+ // 出边,command-tree 注册必须有 wiring 证明,不能只靠 handler 测试。
132
+ it('internalization retry command exists under runtime internalization (pd runtime internalization retry --help)', () => {
133
+ const output = runPdHelp(['runtime', 'internalization', 'retry', '--help']);
134
+ expect(output).toContain('--task');
135
+ expect(output).toContain('--confirm');
136
+ expect(output).toContain('--workspace');
137
+ expect(output).toContain('--json');
138
+ });
139
+
140
+ it('internalization subcommand list includes retry (pd runtime internalization --help)', () => {
141
+ const output = runPdHelp(['runtime', 'internalization', '--help']);
142
+ expect(output).toMatch(/retry\s/);
143
+ });
129
144
  });