@principles/pd-cli 1.133.0 → 1.135.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.
- package/dist/commands/__tests__/intent-flag-wiring.test.js +74 -3
- package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +2 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +1 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +60 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +1 -0
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js +11 -4
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +1 -1
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +9 -0
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -1
- package/dist/commands/admission-gate.d.ts +24 -0
- package/dist/commands/admission-gate.d.ts.map +1 -0
- package/dist/commands/admission-gate.js +29 -0
- package/dist/commands/admission-gate.js.map +1 -0
- package/dist/commands/candidate.d.ts.map +1 -1
- package/dist/commands/candidate.js +72 -6
- package/dist/commands/candidate.js.map +1 -1
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +42 -0
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/demo-story-a.d.ts.map +1 -1
- package/dist/commands/demo-story-a.js +4 -2
- package/dist/commands/demo-story-a.js.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +155 -8
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/errors-list.d.ts +25 -0
- package/dist/commands/errors-list.d.ts.map +1 -0
- package/dist/commands/errors-list.js +368 -0
- package/dist/commands/errors-list.js.map +1 -0
- package/dist/commands/health-codex.d.ts +7 -0
- package/dist/commands/health-codex.d.ts.map +1 -0
- package/dist/commands/health-codex.js +209 -0
- package/dist/commands/health-codex.js.map +1 -0
- package/dist/commands/health.d.ts +24 -0
- package/dist/commands/health.d.ts.map +1 -1
- package/dist/commands/health.js +48 -0
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/intent.d.ts +21 -4
- package/dist/commands/intent.d.ts.map +1 -1
- package/dist/commands/intent.js +88 -27
- package/dist/commands/intent.js.map +1 -1
- package/dist/commands/pain-evidence.d.ts +6 -0
- package/dist/commands/pain-evidence.d.ts.map +1 -1
- package/dist/commands/pain-evidence.js +6 -0
- package/dist/commands/pain-evidence.js.map +1 -1
- package/dist/commands/pain-record.d.ts.map +1 -1
- package/dist/commands/pain-record.js +21 -0
- package/dist/commands/pain-record.js.map +1 -1
- package/dist/commands/pain-retry.d.ts +4 -0
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +327 -32
- package/dist/commands/pain-retry.js.map +1 -1
- package/dist/commands/proven-channel-baseline.d.ts.map +1 -1
- package/dist/commands/proven-channel-baseline.js +4 -2
- package/dist/commands/proven-channel-baseline.js.map +1 -1
- package/dist/commands/runtime-activation.d.ts +30 -0
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +567 -11
- package/dist/commands/runtime-activation.js.map +1 -1
- package/dist/commands/runtime-canary.d.ts.map +1 -1
- package/dist/commands/runtime-canary.js +32 -7
- package/dist/commands/runtime-canary.js.map +1 -1
- package/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
- package/dist/commands/runtime-health-snapshot.js +4 -2
- package/dist/commands/runtime-health-snapshot.js.map +1 -1
- package/dist/commands/runtime-init.d.ts +43 -0
- package/dist/commands/runtime-init.d.ts.map +1 -0
- package/dist/commands/runtime-init.js +264 -0
- package/dist/commands/runtime-init.js.map +1 -0
- package/dist/commands/runtime-internalization-context-trace.d.ts +25 -0
- package/dist/commands/runtime-internalization-context-trace.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-context-trace.js +374 -0
- package/dist/commands/runtime-internalization-context-trace.js.map +1 -0
- package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js +3 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
- package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-integrity.js +4 -2
- package/dist/commands/runtime-internalization-integrity.js.map +1 -1
- package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-once.js +20 -5
- package/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.d.ts +1 -0
- package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.js +149 -6
- package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
- package/dist/commands/runtime-uat.d.ts.map +1 -1
- package/dist/commands/runtime-uat.guard.test.js +13 -15
- package/dist/commands/runtime-uat.guard.test.js.map +1 -1
- package/dist/commands/runtime-uat.js +4 -3
- package/dist/commands/runtime-uat.js.map +1 -1
- package/dist/config-reader.js +1 -1
- package/dist/config-reader.js.map +1 -1
- package/dist/index.js +54 -78
- package/dist/index.js.map +1 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +2 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +1 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.js +304 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -0
- package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
- package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
- package/dist/services/config-doctor.d.ts.map +1 -1
- package/dist/services/config-doctor.js +9 -2
- package/dist/services/config-doctor.js.map +1 -1
- package/dist/services/console-launcher.d.ts +6 -0
- package/dist/services/console-launcher.d.ts.map +1 -1
- package/dist/services/console-launcher.js.map +1 -1
- package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
- package/dist/services/pain-flood-simulation-runner.js +9 -0
- package/dist/services/pain-flood-simulation-runner.js.map +1 -1
- package/dist/services/pd-config-loader.js +2 -2
- package/dist/services/pd-config-loader.js.map +1 -1
- package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
- package/dist/services/proven-channel-baseline-runner.js +10 -2
- package/dist/services/proven-channel-baseline-runner.js.map +1 -1
- package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
- package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
- package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +111 -7
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/dist/services/rulehost-readiness.js.map +1 -1
- package/dist/services/runtime-adapter-resolver.d.ts +1 -0
- package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
- package/dist/services/runtime-adapter-resolver.js +3 -0
- package/dist/services/runtime-adapter-resolver.js.map +1 -1
- package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
- package/dist/services/synthetic-baseline-runner.js +11 -0
- package/dist/services/synthetic-baseline-runner.js.map +1 -1
- package/dist/utils/production-workspace-guard.d.ts +4 -4
- package/dist/utils/production-workspace-guard.d.ts.map +1 -1
- package/dist/utils/production-workspace-guard.js +19 -16
- package/dist/utils/production-workspace-guard.js.map +1 -1
- package/dist/utils/production-workspace-guard.test.js +54 -19
- package/dist/utils/production-workspace-guard.test.js.map +1 -1
- package/package.json +8 -5
- package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
- package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
- package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
- package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
- package/src/commands/admission-gate.ts +30 -0
- package/src/commands/candidate.ts +74 -6
- package/src/commands/console.ts +41 -0
- package/src/commands/demo-story-a.ts +4 -2
- package/src/commands/diagnose.ts +158 -10
- package/src/commands/errors-list.ts +440 -0
- package/src/commands/health-codex.ts +249 -0
- package/src/commands/health.ts +60 -0
- package/src/commands/intent.ts +98 -29
- package/src/commands/pain-evidence.ts +6 -0
- package/src/commands/pain-record.ts +21 -0
- package/src/commands/pain-retry.ts +353 -35
- package/src/commands/proven-channel-baseline.ts +4 -2
- package/src/commands/runtime-activation.ts +657 -12
- package/src/commands/runtime-canary.ts +31 -5
- package/src/commands/runtime-health-snapshot.ts +4 -2
- package/src/commands/runtime-init.ts +305 -0
- package/src/commands/runtime-internalization-context-trace.ts +445 -0
- package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
- package/src/commands/runtime-internalization-integrity.ts +4 -2
- package/src/commands/runtime-internalization-run-once.ts +20 -5
- package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
- package/src/commands/runtime-uat.guard.test.ts +14 -16
- package/src/commands/runtime-uat.ts +4 -3
- package/src/config-reader.ts +1 -1
- package/src/index.ts +69 -78
- package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
- package/src/services/config-doctor.ts +9 -2
- package/src/services/console-launcher.ts +6 -0
- package/src/services/pain-flood-simulation-runner.ts +9 -0
- package/src/services/pd-config-loader.ts +2 -2
- package/src/services/proven-channel-baseline-runner.ts +10 -3
- package/src/services/rulehost-pipeline-runner.ts +157 -6
- package/src/services/runtime-adapter-resolver.ts +4 -0
- package/src/services/synthetic-baseline-runner.ts +11 -0
- package/src/utils/production-workspace-guard.test.ts +62 -20
- package/src/utils/production-workspace-guard.ts +19 -16
- package/tests/bdd/cli-contract.steps.ts +343 -0
- package/tests/bdd/context-trace.steps.ts +229 -0
- package/tests/bdd/support/gherkin-loader.ts +147 -0
- package/tests/bdd/support/repo-root.ts +62 -0
- package/tests/bdd/support/vitest-bdd.ts +179 -0
- package/tests/commands/admission-gate-coverage.test.ts +79 -0
- package/tests/commands/candidate-audit-repair.test.ts +77 -0
- package/tests/commands/candidate-intake.test.ts +155 -10
- package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
- package/tests/commands/cli-help-snapshot.test.ts +15 -1
- package/tests/commands/console-open.test.ts +229 -32
- package/tests/commands/diagnose.test.ts +619 -14
- package/tests/commands/errors-list.test.ts +678 -0
- package/tests/commands/health-codex.test.ts +243 -0
- package/tests/commands/health-host-wiring.test.ts +76 -0
- package/tests/commands/intent.test.ts +4 -4
- package/tests/commands/pain-record.test.ts +80 -0
- package/tests/commands/pain-retry.test.ts +94 -3
- package/tests/commands/run-rulehost-handler.test.ts +133 -0
- package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
- package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
- package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
- package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
- package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
- package/tests/commands/runtime-activation.test.ts +1090 -5
- package/tests/commands/runtime-canary.test.ts +50 -0
- package/tests/commands/runtime-features.test.ts +2 -0
- package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
- package/tests/commands/runtime-init.test.ts +346 -0
- package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
- package/tests/commands/runtime-internalization.test.ts +2 -2
- package/tests/config-reader.test.ts +6 -5
- package/tests/e2e/cross-package-acceptance.test.ts +55 -13
- package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
- package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
- package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
- package/vitest.config.ts +1 -1
|
@@ -32,6 +32,27 @@ export async function handlePainRecord(opts: RecordOptions): Promise<void> {
|
|
|
32
32
|
console.error('Error: --reason <text> is required');
|
|
33
33
|
console.error('Usage: pd pain record --reason <text> [--score N] [--source manual] [--workspace <path>] [--session <id>] [--json]');
|
|
34
34
|
process.exit(1);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Bug-P fix: enforce reason length limit to prevent oversized data entering the pipeline.
|
|
39
|
+
// Consistent with pain-flood-simulation.ts (MAX_REASON_LENGTH = 500) and synthetic-baseline.ts.
|
|
40
|
+
// CodeRabbit review fix: emit JSON on --json and stop execution after exit (cli-1, cli-2, cli-5).
|
|
41
|
+
const MAX_REASON_LENGTH = 500;
|
|
42
|
+
if (opts.reason.length > MAX_REASON_LENGTH) {
|
|
43
|
+
if (opts.json) {
|
|
44
|
+
console.log(JSON.stringify({
|
|
45
|
+
status: 'failed',
|
|
46
|
+
reason: 'reason_too_long',
|
|
47
|
+
message: `--reason must be at most ${MAX_REASON_LENGTH} characters (got ${opts.reason.length})`,
|
|
48
|
+
nextAction: 'Shorten the reason text or split it into multiple pain records.',
|
|
49
|
+
}, null, 2));
|
|
50
|
+
} else {
|
|
51
|
+
console.error(`Error: --reason must be at most ${MAX_REASON_LENGTH} characters (got ${opts.reason.length})`);
|
|
52
|
+
console.error('Next action: shorten the reason text or split into multiple pain records.');
|
|
53
|
+
}
|
|
54
|
+
process.exit(1);
|
|
55
|
+
return;
|
|
35
56
|
}
|
|
36
57
|
|
|
37
58
|
if (opts.score !== undefined && (isNaN(opts.score) || opts.score < 0 || opts.score > 100)) {
|
|
@@ -36,13 +36,23 @@ import {
|
|
|
36
36
|
isFeatureEnabled,
|
|
37
37
|
SPLIT_PIPELINE_TOTAL_TIMEOUT_MS,
|
|
38
38
|
PrincipleTreeLedgerAdapter,
|
|
39
|
+
SqliteDeadLetterStore,
|
|
40
|
+
PainSignalBridge,
|
|
41
|
+
type PainDetectedData,
|
|
42
|
+
type PainSignalBridgeResult,
|
|
43
|
+
type DeadLetterRow,
|
|
39
44
|
} from '@principles/core/runtime-v2';
|
|
40
45
|
import type { PDRuntimeAdapter, RuntimeConfig, OutputLanguage } from '@principles/core/runtime-v2';
|
|
46
|
+
import type { Command } from 'commander';
|
|
41
47
|
import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
|
|
42
48
|
import { resolveRuntimeFromPdConfig } from '../services/resolve-runtime-from-pd-config.js';
|
|
49
|
+
import { createHash } from 'node:crypto';
|
|
50
|
+
/** Layer 0 content-hash (design §6.1); injected so diag writers can attach predecessorSummary hashes. */
|
|
51
|
+
const contentHashFn = (input: string): string => createHash('sha256').update(input).digest('hex');
|
|
43
52
|
import type { PDTaskStatus } from '@principles/core/runtime-v2';
|
|
44
53
|
import { readOutputLanguageFromWorkspace } from '../config-reader.js';
|
|
45
54
|
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
55
|
+
import { checkAdmissionGate } from './admission-gate.js';
|
|
46
56
|
import * as path from 'path';
|
|
47
57
|
|
|
48
58
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
|
@@ -61,6 +71,7 @@ interface PainRetryOptions {
|
|
|
61
71
|
apiKeyEnv?: string;
|
|
62
72
|
baseUrl?: string;
|
|
63
73
|
maxRetries?: number;
|
|
74
|
+
maxTokens?: number;
|
|
64
75
|
timeoutMs?: number;
|
|
65
76
|
}
|
|
66
77
|
|
|
@@ -92,8 +103,116 @@ function readNonBlankString(value: unknown): string | null {
|
|
|
92
103
|
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
93
104
|
}
|
|
94
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Validate that an unknown value (read from dead_letter_pains.pain_data) has
|
|
108
|
+
* the shape of PainDetectedData.
|
|
109
|
+
*
|
|
110
|
+
* rc-1: Treat parsed JSON as unknown.
|
|
111
|
+
* rc-2: No `as` bypass — use typeof checks and literal narrowing.
|
|
112
|
+
* rc-3: Required fields (painId, painType, source, reason) fail loud.
|
|
113
|
+
* rc-5: Use Object.hasOwn for field presence checks on untrusted objects.
|
|
114
|
+
*/
|
|
115
|
+
function parsePainDetectedData(value: unknown):
|
|
116
|
+
| { valid: true; data: PainDetectedData }
|
|
117
|
+
| { valid: false; error: string } {
|
|
118
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
119
|
+
return {
|
|
120
|
+
valid: false,
|
|
121
|
+
error: `painData is not an object (got ${value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value})`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const obj = value as Record<string, unknown>;
|
|
125
|
+
|
|
126
|
+
// Detect dead-letter error envelopes from SqliteDeadLetterStore parse/serialize failures.
|
|
127
|
+
if (Object.hasOwn(obj, '__deadLetterParseError') || Object.hasOwn(obj, '__deadLetterSerializeError')) {
|
|
128
|
+
return {
|
|
129
|
+
valid: false,
|
|
130
|
+
error: `painData contains dead-letter error envelope: ${JSON.stringify(obj)}`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Required string fields
|
|
135
|
+
if (typeof obj.painId !== 'string' || obj.painId.trim().length === 0) {
|
|
136
|
+
return { valid: false, error: `painData.painId is not a non-blank string (got ${JSON.stringify(obj.painId)})` };
|
|
137
|
+
}
|
|
138
|
+
if (typeof obj.source !== 'string') {
|
|
139
|
+
return { valid: false, error: `painData.source is not a string (got ${JSON.stringify(obj.source)})` };
|
|
140
|
+
}
|
|
141
|
+
if (typeof obj.reason !== 'string') {
|
|
142
|
+
return { valid: false, error: `painData.reason is not a string (got ${JSON.stringify(obj.reason)})` };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// painType: narrow to literal union without `as`.
|
|
146
|
+
const painTypeRaw = obj.painType;
|
|
147
|
+
if (painTypeRaw !== 'tool_failure' && painTypeRaw !== 'dispatch_error'
|
|
148
|
+
&& painTypeRaw !== 'subagent_error' && painTypeRaw !== 'user_frustration') {
|
|
149
|
+
return { valid: false, error: `painData.painType is not a valid pain type (got ${JSON.stringify(painTypeRaw)})` };
|
|
150
|
+
}
|
|
151
|
+
// After the narrowing check above, TypeScript infers painTypeRaw as the literal union.
|
|
152
|
+
const painType: PainDetectedData['painType'] = painTypeRaw === 'dispatch_error' ? 'tool_failure' : painTypeRaw;
|
|
153
|
+
|
|
154
|
+
// Build the typed object field-by-field. Optional fields are added only when present and valid.
|
|
155
|
+
const data: PainDetectedData = {
|
|
156
|
+
painId: obj.painId,
|
|
157
|
+
painType,
|
|
158
|
+
source: obj.source,
|
|
159
|
+
reason: obj.reason,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (Object.hasOwn(obj, 'score')) {
|
|
163
|
+
if (typeof obj.score !== 'number' || !Number.isFinite(obj.score)) {
|
|
164
|
+
return { valid: false, error: `painData.score is not a finite number (got ${JSON.stringify(obj.score)})` };
|
|
165
|
+
}
|
|
166
|
+
data.score = obj.score;
|
|
167
|
+
}
|
|
168
|
+
if (Object.hasOwn(obj, 'sessionId')) {
|
|
169
|
+
if (typeof obj.sessionId !== 'string') return { valid: false, error: 'painData.sessionId is not a string' };
|
|
170
|
+
data.sessionId = obj.sessionId;
|
|
171
|
+
}
|
|
172
|
+
if (Object.hasOwn(obj, 'agentId')) {
|
|
173
|
+
if (typeof obj.agentId !== 'string') return { valid: false, error: 'painData.agentId is not a string' };
|
|
174
|
+
data.agentId = obj.agentId;
|
|
175
|
+
}
|
|
176
|
+
if (Object.hasOwn(obj, 'taskId')) {
|
|
177
|
+
if (typeof obj.taskId !== 'string') return { valid: false, error: 'painData.taskId is not a string' };
|
|
178
|
+
data.taskId = obj.taskId;
|
|
179
|
+
}
|
|
180
|
+
if (Object.hasOwn(obj, 'traceId')) {
|
|
181
|
+
if (typeof obj.traceId !== 'string') return { valid: false, error: 'painData.traceId is not a string' };
|
|
182
|
+
data.traceId = obj.traceId;
|
|
183
|
+
}
|
|
184
|
+
if (Object.hasOwn(obj, 'provenance')) {
|
|
185
|
+
const prov = obj.provenance;
|
|
186
|
+
if (prov !== 'openclaw_context_bound' && prov !== 'owner_reported_no_host_trace' && prov !== 'automatic_hook') {
|
|
187
|
+
return { valid: false, error: `painData.provenance is not a known literal (got ${JSON.stringify(prov)})` };
|
|
188
|
+
}
|
|
189
|
+
data.provenance = prov;
|
|
190
|
+
}
|
|
191
|
+
if (Object.hasOwn(obj, 'evidence')) {
|
|
192
|
+
if (!Array.isArray(obj.evidence)) {
|
|
193
|
+
return { valid: false, error: 'painData.evidence is not an array' };
|
|
194
|
+
}
|
|
195
|
+
// Validate each entry has the PainEvidenceEntry shape { sourceRef: string, note: string }.
|
|
196
|
+
const evidence: { sourceRef: string; note: string }[] = [];
|
|
197
|
+
for (let i = 0; i < obj.evidence.length; i++) {
|
|
198
|
+
const entry = obj.evidence[i];
|
|
199
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
200
|
+
return { valid: false, error: `painData.evidence[${i}] is not an object` };
|
|
201
|
+
}
|
|
202
|
+
const e = entry as Record<string, unknown>;
|
|
203
|
+
if (typeof e.sourceRef !== 'string' || typeof e.note !== 'string') {
|
|
204
|
+
return { valid: false, error: `painData.evidence[${i}] missing string fields sourceRef/note` };
|
|
205
|
+
}
|
|
206
|
+
evidence.push({ sourceRef: e.sourceRef, note: e.note });
|
|
207
|
+
}
|
|
208
|
+
data.evidence = evidence;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { valid: true, data };
|
|
212
|
+
}
|
|
213
|
+
|
|
95
214
|
/** Output a refused/not_found result, respecting --json mode. Exits with code 1. */
|
|
96
|
-
function refuseExit(opts: PainRetryOptions, payload: { status?: string; painId: string; taskId?: string; reason: string; message?: string; nextAction: string }):
|
|
215
|
+
function refuseExit(opts: PainRetryOptions, payload: { status?: string; painId: string; taskId?: string; reason: string; message?: string; nextAction: string }): void {
|
|
97
216
|
if (opts.json) {
|
|
98
217
|
console.log(JSON.stringify({
|
|
99
218
|
status: payload.status ?? 'refused',
|
|
@@ -117,8 +236,9 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
117
236
|
|
|
118
237
|
// Step 1: Resolve painId → taskId
|
|
119
238
|
const resolution = resolveTaskIdFromPainId(opts.painId);
|
|
239
|
+
// eslint-disable-next-line no-restricted-syntax -- 'in' required for discriminated union narrowing (taskId vs reason/nextAction)
|
|
120
240
|
if ('reason' in resolution) {
|
|
121
|
-
refuseExit(opts, { painId: opts.painId, reason: resolution.reason, nextAction: resolution.nextAction });
|
|
241
|
+
return refuseExit(opts, { painId: opts.painId, reason: resolution.reason, nextAction: resolution.nextAction });
|
|
122
242
|
}
|
|
123
243
|
|
|
124
244
|
const { taskId } = resolution;
|
|
@@ -130,19 +250,39 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
130
250
|
await stateManager.initialize();
|
|
131
251
|
|
|
132
252
|
const task = await stateManager.getTask(taskId);
|
|
253
|
+
|
|
254
|
+
// Dead letter replay: when no task exists, check if the pain signal was
|
|
255
|
+
// persisted to dead_letter_pains (rc-9: no silent failure in pain.ts).
|
|
256
|
+
// If found, replay it through PainSignalBridge.onPainDetected.
|
|
257
|
+
let deadLetterReplay: { data: PainDetectedData; row: DeadLetterRow } | null = null;
|
|
133
258
|
if (!task) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
259
|
+
const dlStore = new SqliteDeadLetterStore(stateManager.connection);
|
|
260
|
+
const dlRow = dlStore.getByPainId(opts.painId);
|
|
261
|
+
if (!dlRow) {
|
|
262
|
+
return refuseExit(opts, {
|
|
263
|
+
status: 'not_found',
|
|
264
|
+
painId: opts.painId,
|
|
265
|
+
taskId,
|
|
266
|
+
reason: 'task_not_found',
|
|
267
|
+
message: `No task found for painId '${opts.painId}' (looked for taskId '${taskId}') and no dead letter found`,
|
|
268
|
+
nextAction: `Verify the painId is correct. Use 'pd task list --kind diagnostician' to see all diagnostician tasks.`,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
const parseResult = parsePainDetectedData(dlRow.painData);
|
|
272
|
+
if (!parseResult.valid) {
|
|
273
|
+
return refuseExit(opts, {
|
|
274
|
+
painId: opts.painId,
|
|
275
|
+
taskId,
|
|
276
|
+
reason: 'dead_letter_corrupt',
|
|
277
|
+
message: `Dead letter for painId '${opts.painId}' has corrupt painData: ${parseResult.error}`,
|
|
278
|
+
nextAction: `The dead letter row cannot be replayed. Inspect the dead_letter_pains table directly: SELECT * FROM dead_letter_pains WHERE pain_id = '${opts.painId}'`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
deadLetterReplay = { data: parseResult.data, row: dlRow };
|
|
142
282
|
}
|
|
143
283
|
|
|
144
|
-
if (task.taskKind !== 'diagnostician') {
|
|
145
|
-
refuseExit(opts, {
|
|
284
|
+
if (task && task.taskKind !== 'diagnostician') {
|
|
285
|
+
return refuseExit(opts, {
|
|
146
286
|
painId: opts.painId,
|
|
147
287
|
taskId,
|
|
148
288
|
reason: 'wrong_task_kind',
|
|
@@ -151,11 +291,13 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
151
291
|
});
|
|
152
292
|
}
|
|
153
293
|
|
|
154
|
-
|
|
155
|
-
|
|
294
|
+
// For dead letter replay there is no prior task; synthesize a status so the
|
|
295
|
+
// existing logging/output paths have a non-null value to print.
|
|
296
|
+
const previousTaskStatus: PDTaskStatus | 'dead_lettered' = task ? task.status : 'dead_lettered';
|
|
297
|
+
const previousLastError = task ? (task.lastError ?? null) : null;
|
|
156
298
|
|
|
157
|
-
if (task.status === 'succeeded' && !opts.force) {
|
|
158
|
-
refuseExit(opts, {
|
|
299
|
+
if (task && task.status === 'succeeded' && !opts.force) {
|
|
300
|
+
return refuseExit(opts, {
|
|
159
301
|
painId: opts.painId,
|
|
160
302
|
taskId,
|
|
161
303
|
reason: 'already_succeeded',
|
|
@@ -164,8 +306,8 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
164
306
|
});
|
|
165
307
|
}
|
|
166
308
|
|
|
167
|
-
if (!RETRYABLE_STATUSES.has(task.status) && task.status !== 'succeeded') {
|
|
168
|
-
refuseExit(opts, {
|
|
309
|
+
if (task && !RETRYABLE_STATUSES.has(task.status) && task.status !== 'succeeded') {
|
|
310
|
+
return refuseExit(opts, {
|
|
169
311
|
painId: opts.painId,
|
|
170
312
|
taskId,
|
|
171
313
|
reason: 'status_not_retryable',
|
|
@@ -178,7 +320,7 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
178
320
|
// P1 fix: --openclaw-local and --openclaw-gateway are mutually exclusive.
|
|
179
321
|
// Must output JSON when --json is set (CLI operator gate).
|
|
180
322
|
if (opts.openclawLocal && opts.openclawGateway) {
|
|
181
|
-
refuseExit(opts, {
|
|
323
|
+
return refuseExit(opts, {
|
|
182
324
|
painId: opts.painId,
|
|
183
325
|
taskId,
|
|
184
326
|
reason: 'conflicting_flags',
|
|
@@ -200,7 +342,7 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
200
342
|
}
|
|
201
343
|
|
|
202
344
|
if (!runtimeKind) {
|
|
203
|
-
refuseExit(opts, {
|
|
345
|
+
return refuseExit(opts, {
|
|
204
346
|
painId: opts.painId,
|
|
205
347
|
taskId,
|
|
206
348
|
reason: 'missing_runtime',
|
|
@@ -216,14 +358,14 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
216
358
|
const resolved = resolveRuntimeFromPdConfig(workspaceDir);
|
|
217
359
|
const configResult = resolved.result;
|
|
218
360
|
if (isRuntimeConfigError(configResult)) {
|
|
219
|
-
refuseExit(opts, { painId: opts.painId, taskId, reason: configResult.reason, message: configResult.message, nextAction: configResult.nextAction });
|
|
361
|
+
return refuseExit(opts, { painId: opts.painId, taskId, reason: configResult.reason, message: configResult.message, nextAction: configResult.nextAction });
|
|
220
362
|
}
|
|
221
363
|
const { openclawMode } = configResult;
|
|
222
364
|
// CLI flags override config (PRI-393)
|
|
223
365
|
const flagMode = opts.openclawLocal ? 'local' as const : opts.openclawGateway ? 'gateway' as const : undefined;
|
|
224
366
|
const effectiveMode = flagMode ?? openclawMode;
|
|
225
367
|
if (!effectiveMode) {
|
|
226
|
-
refuseExit(opts, {
|
|
368
|
+
return refuseExit(opts, {
|
|
227
369
|
painId: opts.painId,
|
|
228
370
|
taskId,
|
|
229
371
|
reason: 'missing_openclaw_mode',
|
|
@@ -279,6 +421,7 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
279
421
|
const apiKeyEnv = opts.apiKeyEnv ?? policyConfig?.apiKeyEnv;
|
|
280
422
|
const baseUrl = opts.baseUrl ?? policyConfig?.baseUrl;
|
|
281
423
|
const maxRetries = opts.maxRetries ?? policyConfig?.maxRetries;
|
|
424
|
+
const maxTokens = opts.maxTokens ?? policyConfig?.maxTokens;
|
|
282
425
|
const effectiveTimeoutMs = opts.timeoutMs ?? policyConfig?.timeoutMs;
|
|
283
426
|
|
|
284
427
|
// Validate required string fields: must be non-blank strings
|
|
@@ -287,7 +430,7 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
287
430
|
if (readNonBlankString(model) === null) missing.push('model');
|
|
288
431
|
if (readNonBlankString(apiKeyEnv) === null) missing.push('apiKeyEnv');
|
|
289
432
|
if (missing.length > 0) {
|
|
290
|
-
refuseExit(opts, {
|
|
433
|
+
return refuseExit(opts, {
|
|
291
434
|
painId: opts.painId,
|
|
292
435
|
taskId,
|
|
293
436
|
reason: `missing_required_config: ${missing.join(', ')}`,
|
|
@@ -304,12 +447,15 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
304
447
|
if (effectiveTimeoutMs !== undefined && effectiveTimeoutMs !== null && !(Number.isFinite(effectiveTimeoutMs) && effectiveTimeoutMs > 0)) {
|
|
305
448
|
invalidNumeric.push(`timeoutMs (got: ${effectiveTimeoutMs})`);
|
|
306
449
|
}
|
|
450
|
+
if (maxTokens !== undefined && maxTokens !== null && !(Number.isFinite(maxTokens) && maxTokens > 0)) {
|
|
451
|
+
invalidNumeric.push(`maxTokens (got: ${maxTokens})`);
|
|
452
|
+
}
|
|
307
453
|
if (invalidNumeric.length > 0) {
|
|
308
|
-
refuseExit(opts, {
|
|
454
|
+
return refuseExit(opts, {
|
|
309
455
|
painId: opts.painId,
|
|
310
456
|
taskId,
|
|
311
457
|
reason: `invalid_numeric_config: ${invalidNumeric.join(', ')}`,
|
|
312
|
-
message: `Invalid numeric pi-ai config: ${invalidNumeric.join(', ')}. maxRetries must be a non-negative integer; timeoutMs must be
|
|
458
|
+
message: `Invalid numeric pi-ai config: ${invalidNumeric.join(', ')}. maxRetries must be a non-negative integer; timeoutMs and maxTokens must be positive numbers.`,
|
|
313
459
|
nextAction: 'Fix the numeric values and retry.',
|
|
314
460
|
});
|
|
315
461
|
}
|
|
@@ -319,7 +465,7 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
319
465
|
const validModel = readNonBlankString(model);
|
|
320
466
|
const validApiKeyEnv = readNonBlankString(apiKeyEnv);
|
|
321
467
|
if (validProvider === null || validModel === null || validApiKeyEnv === null) {
|
|
322
|
-
refuseExit(opts, {
|
|
468
|
+
return refuseExit(opts, {
|
|
323
469
|
painId: opts.painId,
|
|
324
470
|
taskId,
|
|
325
471
|
reason: 'internal_validation_error',
|
|
@@ -329,7 +475,7 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
329
475
|
}
|
|
330
476
|
|
|
331
477
|
if (!process.env[validApiKeyEnv]) {
|
|
332
|
-
refuseExit(opts, {
|
|
478
|
+
return refuseExit(opts, {
|
|
333
479
|
painId: opts.painId,
|
|
334
480
|
taskId,
|
|
335
481
|
reason: 'missing_api_key',
|
|
@@ -344,11 +490,12 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
344
490
|
apiKeyEnv: validApiKeyEnv,
|
|
345
491
|
baseUrl,
|
|
346
492
|
maxRetries,
|
|
493
|
+
maxTokens,
|
|
347
494
|
timeoutMs: effectiveTimeoutMs,
|
|
348
495
|
workspace: workspaceDir,
|
|
349
496
|
});
|
|
350
497
|
} else {
|
|
351
|
-
refuseExit(opts, {
|
|
498
|
+
return refuseExit(opts, {
|
|
352
499
|
painId: opts.painId,
|
|
353
500
|
taskId,
|
|
354
501
|
reason: `unknown_runtime: '${runtimeKind}'`,
|
|
@@ -378,21 +525,37 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
378
525
|
const featureFlags = computeFlagsFromLoadResult(configLoadResult);
|
|
379
526
|
const isSplitPipeline = isFeatureEnabled(featureFlags, 'diagnostician_split_pipeline');
|
|
380
527
|
const pipelineTimeoutMs = isSplitPipeline ? SPLIT_PIPELINE_TOTAL_TIMEOUT_MS : 300_000;
|
|
528
|
+
// BUG-1 (PRI-442): extract effectiveConfig so ADR-0019 LLM rate-limit
|
|
529
|
+
// degradation (isDegradationEnabled in base-peer-runner) can read the
|
|
530
|
+
// diagnostician_llm_degradation feature flag. Without this, the runners
|
|
531
|
+
// receive no effectiveConfig and degradation silently never fires.
|
|
532
|
+
// CR-1 (CodeRabbit P2, rc-9): warn when config load failed so the
|
|
533
|
+
// fallback to defaults is observable — no silent degradation.
|
|
534
|
+
if (!configLoadResult.ok) {
|
|
535
|
+
const errSummary = configLoadResult.errors
|
|
536
|
+
.map((e) => `${e.path}: ${e.reason}`)
|
|
537
|
+
.join('; ');
|
|
538
|
+
console.warn(
|
|
539
|
+
`[pd pain retry] .pd/config.yaml at ${configLoadResult.configPath} is malformed — falling back to default feature flags. Errors: ${errSummary}. Next action: ${configLoadResult.errors[0]?.nextAction ?? 'fix config errors and retry'}`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const effectiveConfig = configLoadResult.ok ? configLoadResult.effective : configLoadResult.defaults;
|
|
381
543
|
|
|
382
544
|
let runner: DiagnosticianRunnerLike;
|
|
383
545
|
if (isSplitPipeline) {
|
|
384
546
|
const resolvedKind = typeof runtimeAdapter.kind === 'function' ? runtimeAdapter.kind() : runtimeKind;
|
|
547
|
+
const perStageTimeoutMs = pipelineTimeoutMs / 3;
|
|
385
548
|
const rootCauseRunner = new DiagRootCauseRunner(
|
|
386
|
-
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDiagRootCauseValidator(), contextAssembler },
|
|
387
|
-
{ owner: 'pd-cli-pain-retry', runtimeKind: resolvedKind, outputLanguage },
|
|
549
|
+
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDiagRootCauseValidator(), contextAssembler, contentHashFn },
|
|
550
|
+
{ owner: 'pd-cli-pain-retry', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs, effectiveConfig },
|
|
388
551
|
);
|
|
389
552
|
const distillerRunner = new DiagDistillerRunner(
|
|
390
|
-
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDiagDistillerValidator() },
|
|
391
|
-
{ owner: 'pd-cli-pain-retry', runtimeKind: resolvedKind, outputLanguage },
|
|
553
|
+
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDiagDistillerValidator(), contentHashFn },
|
|
554
|
+
{ owner: 'pd-cli-pain-retry', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs, effectiveConfig },
|
|
392
555
|
);
|
|
393
556
|
const routerRunner = new DiagRouterRunner(
|
|
394
|
-
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, committer },
|
|
395
|
-
{ owner: 'pd-cli-pain-retry', runtimeKind: resolvedKind, outputLanguage },
|
|
557
|
+
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, committer, contentHashFn },
|
|
558
|
+
{ owner: 'pd-cli-pain-retry', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs, effectiveConfig },
|
|
396
559
|
);
|
|
397
560
|
|
|
398
561
|
runner = new SplitDiagnosticianRunner({
|
|
@@ -401,12 +564,120 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
401
564
|
routerRunner,
|
|
402
565
|
stateManager,
|
|
403
566
|
committer,
|
|
404
|
-
perStageTimeoutMs
|
|
567
|
+
perStageTimeoutMs,
|
|
405
568
|
});
|
|
406
569
|
} else {
|
|
407
570
|
runner = new DisabledDiagnosticianRunner();
|
|
408
571
|
}
|
|
409
572
|
|
|
573
|
+
// ── Dead letter replay branch ──────────────────────────────────────────
|
|
574
|
+
// When task was null but a dead letter was found, replay the pain signal
|
|
575
|
+
// through PainSignalBridge.onPainDetected instead of calling diagnoseRun.
|
|
576
|
+
// On success/failure, markRetried updates the dead_letter_pains row.
|
|
577
|
+
if (deadLetterReplay) {
|
|
578
|
+
const dlStore = new SqliteDeadLetterStore(stateManager.connection);
|
|
579
|
+
if (!opts.json) {
|
|
580
|
+
console.log(`\nReplaying dead letter pain: ${opts.painId}`);
|
|
581
|
+
console.log(` Task ID: ${taskId}`);
|
|
582
|
+
console.log(` Dead Letter ID: ${deadLetterReplay.row.id}`);
|
|
583
|
+
console.log(` Failed At: ${deadLetterReplay.row.failedAt}`);
|
|
584
|
+
console.log(` Retry Count: ${deadLetterReplay.row.retryCount}`);
|
|
585
|
+
console.log(` Runtime: ${runtimeKind}`);
|
|
586
|
+
console.log(` Workspace: ${workspaceDir}\n`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const ledgerAdapter = new PrincipleTreeLedgerAdapter({ stateDir: path.join(workspaceDir, '.state') });
|
|
590
|
+
const intakeService = new CandidateIntakeService({ stateManager, ledgerAdapter });
|
|
591
|
+
const bridge = new PainSignalBridge({
|
|
592
|
+
stateManager,
|
|
593
|
+
runner,
|
|
594
|
+
intakeService,
|
|
595
|
+
ledgerAdapter,
|
|
596
|
+
owner: 'pd-cli-pain-retry-dead-letter',
|
|
597
|
+
workspaceDir,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
let bridgeResult: PainSignalBridgeResult;
|
|
601
|
+
try {
|
|
602
|
+
bridgeResult = await bridge.onPainDetected(deadLetterReplay.data);
|
|
603
|
+
} catch (err: unknown) {
|
|
604
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
605
|
+
const markFail = dlStore.markRetried(opts.painId, false);
|
|
606
|
+
if (opts.json) {
|
|
607
|
+
console.log(JSON.stringify({
|
|
608
|
+
status: 'failed',
|
|
609
|
+
painId: opts.painId,
|
|
610
|
+
taskId,
|
|
611
|
+
source: 'dead_letter',
|
|
612
|
+
deadLetterId: deadLetterReplay.row.id,
|
|
613
|
+
runtimeKind,
|
|
614
|
+
errorCategory: 'execution_failed',
|
|
615
|
+
message: errorMessage,
|
|
616
|
+
markRetriedOk: markFail.ok,
|
|
617
|
+
nextAction: 'Replay threw; retry_count incremented. Adjust parameters and run pd pain retry --pain-id again.',
|
|
618
|
+
}, null, 2));
|
|
619
|
+
} else {
|
|
620
|
+
console.error(`error: ${errorMessage}`);
|
|
621
|
+
console.error(`nextAction: Replay threw; retry_count incremented. Adjust parameters and try again.`);
|
|
622
|
+
}
|
|
623
|
+
process.exit(1);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const success = bridgeResult.status === 'succeeded';
|
|
628
|
+
const markResult = dlStore.markRetried(opts.painId, success);
|
|
629
|
+
|
|
630
|
+
if (opts.json) {
|
|
631
|
+
// cli-1-strict-json: exactly one parseable JSON object on stdout.
|
|
632
|
+
console.log(JSON.stringify({
|
|
633
|
+
status: success ? 'succeeded' : 'failed',
|
|
634
|
+
painId: opts.painId,
|
|
635
|
+
taskId,
|
|
636
|
+
source: 'dead_letter',
|
|
637
|
+
deadLetterId: deadLetterReplay.row.id,
|
|
638
|
+
runtimeKind,
|
|
639
|
+
bridgeStatus: bridgeResult.status,
|
|
640
|
+
candidateIds: bridgeResult.candidateIds,
|
|
641
|
+
ledgerEntryIds: bridgeResult.ledgerEntryIds,
|
|
642
|
+
markRetriedOk: markResult.ok,
|
|
643
|
+
message: bridgeResult.message ?? null,
|
|
644
|
+
nextAction: success
|
|
645
|
+
? (bridgeResult.candidateIds.length > 0
|
|
646
|
+
? `Dead letter replayed. Internalize candidates:\n ${bridgeResult.candidateIds.map((id) => `pd candidate internalize --candidate-id ${id} --workspace "${workspaceDir}"`).join('\n ')}`
|
|
647
|
+
: 'Dead letter replayed. No candidates generated.')
|
|
648
|
+
: `Replay did not succeed (status=${bridgeResult.status}). The dead letter remains available for future retry.`,
|
|
649
|
+
}, null, 2));
|
|
650
|
+
if (!success) {
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Text output
|
|
657
|
+
console.log(`\nReplay ${success ? 'succeeded' : 'failed'}:`);
|
|
658
|
+
console.log(` Pain ID: ${opts.painId}`);
|
|
659
|
+
console.log(` Task ID: ${taskId}`);
|
|
660
|
+
console.log(` Dead Letter ID: ${deadLetterReplay.row.id}`);
|
|
661
|
+
console.log(` Bridge Status: ${bridgeResult.status}`);
|
|
662
|
+
if (bridgeResult.message) {
|
|
663
|
+
console.log(` Message: ${bridgeResult.message}`);
|
|
664
|
+
}
|
|
665
|
+
if (success) {
|
|
666
|
+
console.log(` Candidates: ${bridgeResult.candidateIds.length}`);
|
|
667
|
+
if (bridgeResult.candidateIds.length > 0) {
|
|
668
|
+
console.log(` Ledger Entries: ${bridgeResult.ledgerEntryIds.length}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
console.log(` Mark Retried: ${markResult.ok ? 'ok' : `FAILED: ${markResult.error}`}`);
|
|
672
|
+
if (!success) {
|
|
673
|
+
console.log(`\n Next Action:`);
|
|
674
|
+
console.log(` Replay did not succeed. The dead letter remains available for future retry.`);
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ── Normal retry branch ─────────────────────────────────────────────────
|
|
410
681
|
if (!opts.json) {
|
|
411
682
|
console.log(`\nRetrying diagnosis for pain: ${opts.painId}`);
|
|
412
683
|
console.log(` Task ID: ${taskId}`);
|
|
@@ -463,6 +734,23 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
463
734
|
const intakeService = new CandidateIntakeService({ stateManager, ledgerAdapter });
|
|
464
735
|
|
|
465
736
|
for (const candidate of candidates) {
|
|
737
|
+
// PRI-503: admission gate check — refuse non-admitted candidates before
|
|
738
|
+
// intake, mirroring candidate.ts (intake/repair/backfill). Without this,
|
|
739
|
+
// `pd pain retry` could bypass the gate and intake unreviewed candidates.
|
|
740
|
+
// cli-5-failure-no-mutation: refusal path does not call intake(), does
|
|
741
|
+
// not update candidate status, does not write ledger.
|
|
742
|
+
// cli-6-output-next-action: refusal result carries admissionBlock.nextAction.
|
|
743
|
+
const admissionBlock = checkAdmissionGate(candidate);
|
|
744
|
+
if (admissionBlock) {
|
|
745
|
+
intakeFailed = true;
|
|
746
|
+
intakeResults.push({
|
|
747
|
+
candidateId: candidate.candidateId,
|
|
748
|
+
status: 'intake_failed',
|
|
749
|
+
error: `Admission gate refused: ${admissionBlock.reason}`,
|
|
750
|
+
nextAction: admissionBlock.nextAction,
|
|
751
|
+
});
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
466
754
|
try {
|
|
467
755
|
const entry = await intakeService.intake(candidate.candidateId);
|
|
468
756
|
if (candidate.status !== 'consumed') {
|
|
@@ -548,6 +836,7 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
548
836
|
|
|
549
837
|
if (intakeFailed) {
|
|
550
838
|
process.exit(1);
|
|
839
|
+
return;
|
|
551
840
|
}
|
|
552
841
|
} catch (error: unknown) {
|
|
553
842
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -568,7 +857,36 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
|
568
857
|
console.error(`error: ${message} (${errorCategory})`);
|
|
569
858
|
}
|
|
570
859
|
process.exit(1);
|
|
860
|
+
return;
|
|
571
861
|
} finally {
|
|
572
862
|
await stateManager.close();
|
|
573
863
|
}
|
|
574
864
|
}
|
|
865
|
+
|
|
866
|
+
/** Register the production `pd pain retry` command and its complete option contract. */
|
|
867
|
+
export function registerPainRetryCommand(
|
|
868
|
+
painCmd: Command,
|
|
869
|
+
handler: (opts: PainRetryOptions) => Promise<void> = handlePainRetry,
|
|
870
|
+
): Command {
|
|
871
|
+
return painCmd
|
|
872
|
+
.command('retry')
|
|
873
|
+
.description('Retry a failed diagnosis by pain ID')
|
|
874
|
+
.requiredOption('-p, --pain-id <painId>', 'Pain ID to retry diagnosis for')
|
|
875
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
876
|
+
.option('-r, --runtime <kind>', "Runtime kind: 'openclaw-cli', 'test-double', 'pi-ai'")
|
|
877
|
+
.option('--openclaw-local', 'Use local OpenClaw (mutually exclusive with --openclaw-gateway)')
|
|
878
|
+
.option('--openclaw-gateway', 'Use gateway OpenClaw (mutually exclusive with --openclaw-local)')
|
|
879
|
+
.option('-a, --agent <agentId>', 'Agent ID to invoke')
|
|
880
|
+
.option('--provider <name>', 'LLM provider (e.g., openrouter) — for pi-ai, falls back to policy')
|
|
881
|
+
.option('--model <id>', 'Model ID (e.g., anthropic/claude-sonnet-4) — for pi-ai, falls back to policy')
|
|
882
|
+
.option('--apiKeyEnv <name>', 'Env var name for API key — for pi-ai, falls back to policy')
|
|
883
|
+
.option('--baseUrl <url>', 'Custom base URL — for pi-ai, falls back to policy')
|
|
884
|
+
.option('--maxRetries <n>', 'Max retry attempts for LLM failures — for pi-ai, falls back to policy', parseInt)
|
|
885
|
+
.option('--maxTokens <n>', 'Max output tokens (max_tokens) for pi-ai LLM calls — for pi-ai, falls back to policy', parseInt)
|
|
886
|
+
.option('--timeoutMs <ms>', 'Timeout in milliseconds — for pi-ai, falls back to policy', parseInt)
|
|
887
|
+
.option('--force', 'Allow retry of already-succeeded tasks')
|
|
888
|
+
.option('--json', 'Output raw JSON')
|
|
889
|
+
.action(async (opts: PainRetryOptions) => {
|
|
890
|
+
await handler(opts);
|
|
891
|
+
});
|
|
892
|
+
}
|
|
@@ -101,8 +101,10 @@ export async function handleProvenChannelBaseline(opts: ProvenChannelBaselineCli
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
if (summary.status !== 'passed') {
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
if (!opts.json) {
|
|
105
|
+
console.error('');
|
|
106
|
+
console.error(`FAIL: status=${summary.status}`);
|
|
107
|
+
}
|
|
106
108
|
process.exitCode = 1;
|
|
107
109
|
}
|
|
108
110
|
} finally {
|