@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.
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +36 -7
- package/dist/commands/runtime-activation.js.map +1 -1
- package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js +48 -0
- package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
- package/dist/commands/runtime-internalization-retry.d.ts +38 -0
- package/dist/commands/runtime-internalization-retry.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-retry.js +143 -0
- package/dist/commands/runtime-internalization-retry.js.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.js +19 -7
- package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -1
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +6 -2
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/runtime-activation.ts +38 -6
- package/src/commands/runtime-internalization-enqueue-successors.ts +48 -0
- package/src/commands/runtime-internalization-retry.ts +163 -0
- package/src/index.ts +12 -0
- package/src/services/__tests__/evaluator-runner-deps.test.ts +20 -8
- package/src/services/rulehost-pipeline-runner.ts +5 -2
- package/tests/commands/cli-command-tree.test.ts +15 -0
- 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
|
|
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
|
-
|
|
206
|
+
// 契约变更 (2026-08-18, INV-02): registry 默认 ON;flag 缺省 = 默认生效
|
|
207
|
+
expect(deps.isRepairLoopEnabled()).toBe(true);
|
|
205
208
|
});
|
|
206
209
|
|
|
207
|
-
it('malformed config → isRepairLoopEnabled()
|
|
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 —
|
|
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(
|
|
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 →
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
});
|