@principles/pd-cli 1.152.12 → 1.152.14
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/candidate.d.ts.map +1 -1
- package/dist/commands/candidate.js +34 -7
- package/dist/commands/candidate.js.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +23 -7
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +21 -7
- package/dist/commands/pain-retry.js.map +1 -1
- package/package.json +13 -7
- package/CHANGELOG.md +0 -23
- package/dist/commands/__tests__/intent-flag-wiring.test.d.ts +0 -9
- package/dist/commands/__tests__/intent-flag-wiring.test.d.ts.map +0 -1
- package/dist/commands/__tests__/intent-flag-wiring.test.js +0 -237
- package/dist/commands/__tests__/intent-flag-wiring.test.js.map +0 -1
- package/dist/commands/__tests__/legacy-cleanup.test.d.ts +0 -18
- package/dist/commands/__tests__/legacy-cleanup.test.d.ts.map +0 -1
- package/dist/commands/__tests__/legacy-cleanup.test.js +0 -467
- package/dist/commands/__tests__/legacy-cleanup.test.js.map +0 -1
- package/dist/commands/__tests__/mvp-smoke.test.d.ts +0 -15
- package/dist/commands/__tests__/mvp-smoke.test.d.ts.map +0 -1
- package/dist/commands/__tests__/mvp-smoke.test.js +0 -262
- package/dist/commands/__tests__/mvp-smoke.test.js.map +0 -1
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +0 -2
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +0 -1
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +0 -60
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +0 -1
- package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts +0 -21
- package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts.map +0 -1
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js +0 -186
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +0 -1
- package/dist/commands/__tests__/rulecode-handler.test.d.ts +0 -16
- package/dist/commands/__tests__/rulecode-handler.test.d.ts.map +0 -1
- package/dist/commands/__tests__/rulecode-handler.test.js +0 -285
- package/dist/commands/__tests__/rulecode-handler.test.js.map +0 -1
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts +0 -24
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts.map +0 -1
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +0 -232
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +0 -1
- package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts +0 -15
- package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts.map +0 -1
- package/dist/commands/__tests__/runtime-activation-nextaction.test.js +0 -73
- package/dist/commands/__tests__/runtime-activation-nextaction.test.js.map +0 -1
- package/dist/commands/__tests__/runtime-probe-config.test.d.ts +0 -20
- package/dist/commands/__tests__/runtime-probe-config.test.d.ts.map +0 -1
- package/dist/commands/__tests__/runtime-probe-config.test.js +0 -389
- package/dist/commands/__tests__/runtime-probe-config.test.js.map +0 -1
- package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts +0 -9
- package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts.map +0 -1
- package/dist/commands/__tests__/telemetry-flag-wiring.test.js +0 -76
- package/dist/commands/__tests__/telemetry-flag-wiring.test.js.map +0 -1
- package/dist/commands/__tests__/version.test.d.ts +0 -2
- package/dist/commands/__tests__/version.test.d.ts.map +0 -1
- package/dist/commands/__tests__/version.test.js +0 -238
- package/dist/commands/__tests__/version.test.js.map +0 -1
- package/dist/commands/runtime-uat.guard.test.d.ts +0 -2
- package/dist/commands/runtime-uat.guard.test.d.ts.map +0 -1
- package/dist/commands/runtime-uat.guard.test.js +0 -153
- package/dist/commands/runtime-uat.guard.test.js.map +0 -1
- package/dist/services/__tests__/cli-output.test.d.ts +0 -18
- package/dist/services/__tests__/cli-output.test.d.ts.map +0 -1
- package/dist/services/__tests__/cli-output.test.js +0 -103
- package/dist/services/__tests__/cli-output.test.js.map +0 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +0 -2
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +0 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.js +0 -343
- package/dist/services/__tests__/evaluator-runner-deps.test.js.map +0 -1
- package/dist/services/__tests__/rulehost-readiness.test.d.ts +0 -2
- package/dist/services/__tests__/rulehost-readiness.test.d.ts.map +0 -1
- package/dist/services/__tests__/rulehost-readiness.test.js +0 -311
- package/dist/services/__tests__/rulehost-readiness.test.js.map +0 -1
- package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts +0 -18
- package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts.map +0 -1
- package/dist/services/__tests__/runtime-adapter-resolver.test.js +0 -673
- package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +0 -1
- package/dist/utils/production-workspace-guard.test.d.ts +0 -2
- package/dist/utils/production-workspace-guard.test.d.ts.map +0 -1
- package/dist/utils/production-workspace-guard.test.js +0 -137
- package/dist/utils/production-workspace-guard.test.js.map +0 -1
- package/scripts/llm-dogfood.ts +0 -456
- package/scripts/migrate-illegal-expected-decision.ts +0 -252
- package/src/commands/__tests__/intent-flag-wiring.test.ts +0 -282
- package/src/commands/__tests__/legacy-cleanup.test.ts +0 -602
- package/src/commands/__tests__/mvp-smoke.test.ts +0 -300
- package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +0 -73
- package/src/commands/__tests__/rulecode-flag-wiring.test.ts +0 -237
- package/src/commands/__tests__/rulecode-handler.test.ts +0 -369
- package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +0 -292
- package/src/commands/__tests__/runtime-activation-nextaction.test.ts +0 -81
- package/src/commands/__tests__/runtime-probe-config.test.ts +0 -432
- package/src/commands/__tests__/telemetry-flag-wiring.test.ts +0 -84
- package/src/commands/__tests__/version.test.ts +0 -251
- package/src/commands/admission-gate.ts +0 -30
- package/src/commands/artifact.ts +0 -82
- package/src/commands/build-trajectory-evidence.ts +0 -298
- package/src/commands/candidate.ts +0 -1250
- package/src/commands/codex-ingest-catchup.ts +0 -114
- package/src/commands/codex-ingest-quarantine.ts +0 -127
- package/src/commands/codex-reconcile.ts +0 -84
- package/src/commands/codex-setup.ts +0 -476
- package/src/commands/codex-worker.ts +0 -185
- package/src/commands/command-helpers.ts +0 -24
- package/src/commands/config-doctor.ts +0 -193
- package/src/commands/console.ts +0 -797
- package/src/commands/context.ts +0 -72
- package/src/commands/demo-story-a.ts +0 -241
- package/src/commands/diagnose.ts +0 -756
- package/src/commands/errors-list.ts +0 -441
- package/src/commands/evolution-tasks-list.ts +0 -48
- package/src/commands/evolution-tasks-show.ts +0 -62
- package/src/commands/flow.ts +0 -60
- package/src/commands/health-codex.ts +0 -576
- package/src/commands/health.ts +0 -315
- package/src/commands/history.ts +0 -63
- package/src/commands/intent.ts +0 -457
- package/src/commands/legacy-cleanup.ts +0 -531
- package/src/commands/legacy-import.ts +0 -104
- package/src/commands/mvp-smoke.ts +0 -160
- package/src/commands/pain-evidence.ts +0 -226
- package/src/commands/pain-list.ts +0 -239
- package/src/commands/pain-record.ts +0 -612
- package/src/commands/pain-retry.ts +0 -916
- package/src/commands/principles-stats.ts +0 -700
- package/src/commands/proven-channel-baseline.ts +0 -115
- package/src/commands/quality-scorecard.ts +0 -272
- package/src/commands/remediation-output.ts +0 -66
- package/src/commands/rulecode.ts +0 -465
- package/src/commands/run.ts +0 -89
- package/src/commands/runtime-activation.ts +0 -1629
- package/src/commands/runtime-artifact-repair.ts +0 -524
- package/src/commands/runtime-canary.ts +0 -395
- package/src/commands/runtime-compatibility-scan.ts +0 -105
- package/src/commands/runtime-diagnostics-export.ts +0 -229
- package/src/commands/runtime-features.ts +0 -153
- package/src/commands/runtime-gfi-snapshot.ts +0 -135
- package/src/commands/runtime-health-snapshot.ts +0 -108
- package/src/commands/runtime-init.ts +0 -429
- package/src/commands/runtime-internalization-enqueue-successors.ts +0 -530
- package/src/commands/runtime-internalization-integrity-repair.ts +0 -79
- package/src/commands/runtime-internalization-integrity.ts +0 -107
- package/src/commands/runtime-internalization-queue.ts +0 -135
- package/src/commands/runtime-internalization-retry.ts +0 -191
- package/src/commands/runtime-internalization-run-once.ts +0 -791
- package/src/commands/runtime-internalization-run-rulehost.ts +0 -749
- package/src/commands/runtime-internalization-wake-once.ts +0 -87
- package/src/commands/runtime-pain-flood-simulation.ts +0 -121
- package/src/commands/runtime-pruning.ts +0 -438
- package/src/commands/runtime-recovery-failed-tasks.ts +0 -258
- package/src/commands/runtime-recovery.ts +0 -109
- package/src/commands/runtime-synthetic-baseline.ts +0 -70
- package/src/commands/runtime-uat.guard.test.ts +0 -175
- package/src/commands/runtime-uat.ts +0 -380
- package/src/commands/runtime.ts +0 -473
- package/src/commands/samples-list.ts +0 -39
- package/src/commands/samples-review.ts +0 -36
- package/src/commands/task.ts +0 -271
- package/src/commands/telemetry.ts +0 -349
- package/src/commands/trace.ts +0 -174
- package/src/commands/trajectory-db-unavailable.ts +0 -19
- package/src/commands/trajectory.ts +0 -64
- package/src/commands/version.ts +0 -49
- package/src/config-reader.ts +0 -122
- package/src/index.ts +0 -1276
- package/src/legacy/legacy-import.ts +0 -179
- package/src/legacy/session-history-import.ts +0 -203
- package/src/resolve-workspace.ts +0 -106
- package/src/services/__tests__/cli-output.test.ts +0 -130
- package/src/services/__tests__/evaluator-runner-deps.test.ts +0 -382
- package/src/services/__tests__/rulehost-readiness.test.ts +0 -363
- package/src/services/__tests__/runtime-adapter-resolver.test.ts +0 -793
- package/src/services/cli-output.ts +0 -95
- package/src/services/config-doctor.ts +0 -697
- package/src/services/console-launcher.ts +0 -537
- package/src/services/demo-rule-compiler.ts +0 -40
- package/src/services/demo-story-a-runner.ts +0 -471
- package/src/services/mainline-snapshot-assembler.ts +0 -579
- package/src/services/pain-flood-simulation-runner.ts +0 -462
- package/src/services/pd-config-loader.ts +0 -433
- package/src/services/proven-channel-baseline-runner.ts +0 -157
- package/src/services/quality-scorecard/data-extractor.ts +0 -150
- package/src/services/quality-scorecard/local-evaluator.ts +0 -142
- package/src/services/quality-scorecard/strong-model-gate.ts +0 -202
- package/src/services/resolve-runtime-from-pd-config.ts +0 -230
- package/src/services/rulehost-pipeline-runner.ts +0 -1214
- package/src/services/rulehost-readiness.ts +0 -326
- package/src/services/runtime-adapter-resolver.ts +0 -364
- package/src/services/synthetic-baseline-runner.ts +0 -400
- package/src/services/version-report.ts +0 -253
- package/src/services/workspace-tool-semantics.ts +0 -26
- package/src/utils/path-security.ts +0 -96
- package/src/utils/production-workspace-guard.test.ts +0 -157
- package/src/utils/production-workspace-guard.ts +0 -210
- package/tests/bdd/cli-contract.steps.ts +0 -346
- package/tests/bdd/principles-stats.steps.ts +0 -211
- package/tests/bdd/support/gherkin-loader.ts +0 -147
- package/tests/bdd/support/repo-root.ts +0 -62
- package/tests/bdd/support/vitest-bdd.ts +0 -179
- package/tests/commands/admission-gate-coverage.test.ts +0 -79
- package/tests/commands/build-trajectory-evidence.test.ts +0 -508
- package/tests/commands/candidate-audit-repair.test.ts +0 -421
- package/tests/commands/candidate-intake.test.ts +0 -732
- package/tests/commands/candidate-internalization-backfill.test.ts +0 -548
- package/tests/commands/candidate-internalize-lineage.test.ts +0 -628
- package/tests/commands/candidate-internalize.test.ts +0 -299
- package/tests/commands/candidate-route.test.ts +0 -329
- package/tests/commands/candidate-show.test.ts +0 -95
- package/tests/commands/cli-command-tree.test.ts +0 -144
- package/tests/commands/cli-help-snapshot.test.ts +0 -155
- package/tests/commands/cli-skill-contract.test.ts +0 -121
- package/tests/commands/codex-consent-reversibility.steps.test.ts +0 -299
- package/tests/commands/codex-ingest-quarantine.test.ts +0 -136
- package/tests/commands/codex-setup.test.ts +0 -297
- package/tests/commands/codex-worker-daemon.test.ts +0 -95
- package/tests/commands/codex-worker-registration.test.ts +0 -207
- package/tests/commands/config-doctor.test.ts +0 -336
- package/tests/commands/console-launcher-edge-cases.test.ts +0 -388
- package/tests/commands/console-open.test.ts +0 -1381
- package/tests/commands/context.test.ts +0 -114
- package/tests/commands/demo-story-a.test.ts +0 -318
- package/tests/commands/diagnose.test.ts +0 -1803
- package/tests/commands/errors-list.test.ts +0 -679
- package/tests/commands/evolution-tasks-list.test.ts +0 -41
- package/tests/commands/evolution-tasks-show.test.ts +0 -46
- package/tests/commands/health-codex.test.ts +0 -349
- package/tests/commands/health-host-wiring.test.ts +0 -76
- package/tests/commands/health-reliability.test.ts +0 -99
- package/tests/commands/health.test.ts +0 -413
- package/tests/commands/intent.test.ts +0 -321
- package/tests/commands/legacy-cleanup.test.ts +0 -148
- package/tests/commands/pain-evidence.test.ts +0 -518
- package/tests/commands/pain-list.test.ts +0 -219
- package/tests/commands/pain-record-async.test.ts +0 -335
- package/tests/commands/pain-record-session-parser.test.ts +0 -228
- package/tests/commands/pain-record.test.ts +0 -851
- package/tests/commands/pain-retry.test.ts +0 -1121
- package/tests/commands/plugin-config-resolution-cutover.test.ts +0 -229
- package/tests/commands/pri-393-runtime-config-unification.test.ts +0 -443
- package/tests/commands/principles-stats-wiring.test.ts +0 -135
- package/tests/commands/principles-stats.test.ts +0 -274
- package/tests/commands/product-path-regression.test.ts +0 -86
- package/tests/commands/proven-channel-baseline.test.ts +0 -441
- package/tests/commands/rulecode.test.ts +0 -177
- package/tests/commands/run-rulehost-handler.test.ts +0 -908
- package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +0 -227
- package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +0 -167
- package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +0 -262
- package/tests/commands/runtime-activation-governance-audit.test.ts +0 -95
- package/tests/commands/runtime-activation-list-flag-wiring.test.ts +0 -193
- package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +0 -248
- package/tests/commands/runtime-activation-shadow-telemetry.test.ts +0 -130
- package/tests/commands/runtime-activation.test.ts +0 -2052
- package/tests/commands/runtime-artifact-repair-registration.test.ts +0 -74
- package/tests/commands/runtime-artifact-repair.test.ts +0 -404
- package/tests/commands/runtime-canary.test.ts +0 -430
- package/tests/commands/runtime-compatibility-scan.test.ts +0 -146
- package/tests/commands/runtime-diagnostics-export.test.ts +0 -175
- package/tests/commands/runtime-features.test.ts +0 -249
- package/tests/commands/runtime-health-snapshot.test.ts +0 -358
- package/tests/commands/runtime-init-empty-workspace.test.ts +0 -252
- package/tests/commands/runtime-init.test.ts +0 -420
- package/tests/commands/runtime-internalization-enqueue-successors.test.ts +0 -782
- package/tests/commands/runtime-internalization-integrity-repair.test.ts +0 -239
- package/tests/commands/runtime-internalization-integrity.test.ts +0 -140
- package/tests/commands/runtime-internalization-queue.test.ts +0 -380
- package/tests/commands/runtime-internalization-retry-owner-authority.test.ts +0 -431
- package/tests/commands/runtime-internalization-run-once-evaluator-parity.test.ts +0 -269
- package/tests/commands/runtime-internalization-run-once-language.test.ts +0 -303
- package/tests/commands/runtime-internalization-run-once-real-config.test.ts +0 -237
- package/tests/commands/runtime-internalization-run-once-rollout-parity.test.ts +0 -706
- package/tests/commands/runtime-internalization-run-once.test.ts +0 -1791
- package/tests/commands/runtime-internalization-wake-once.test.ts +0 -171
- package/tests/commands/runtime-internalization.test.ts +0 -53
- package/tests/commands/runtime-pain-flood-simulation.test.ts +0 -418
- package/tests/commands/runtime-pruning.test.ts +0 -694
- package/tests/commands/runtime-recovery-failed-tasks.test.ts +0 -328
- package/tests/commands/runtime-recovery.test.ts +0 -120
- package/tests/commands/runtime-synthetic-baseline.test.ts +0 -249
- package/tests/commands/runtime-uat.test.ts +0 -397
- package/tests/commands/runtime.test.ts +0 -392
- package/tests/commands/samples-list.test.ts +0 -43
- package/tests/commands/samples-review.test.ts +0 -41
- package/tests/commands/task.test.ts +0 -153
- package/tests/commands/telemetry.test.ts +0 -190
- package/tests/commands/trace.test.ts +0 -316
- package/tests/config-reader.test.ts +0 -143
- package/tests/e2e/candidate-intake-e2e.test.ts +0 -338
- package/tests/e2e/cli-full-flow.test.ts +0 -233
- package/tests/e2e/cross-package-acceptance.test.ts +0 -635
- package/tests/helpers/pd-cli-path.ts +0 -5
- package/tests/pd-cli-smoke.test.ts +0 -13
- package/tests/resolve-workspace.test.ts +0 -196
- package/tests/services/config-doctor-signal-health.test.ts +0 -109
- package/tests/services/console-launcher.test.ts +0 -114
- package/tests/services/demo-rule-compiler.test.ts +0 -242
- package/tests/services/mainline-snapshot-assembler.test.ts +0 -572
- package/tests/services/pd-config-loader.test.ts +0 -505
- package/tests/services/proven-channel-baseline-runner.test.ts +0 -30
- package/tests/services/quality-scorecard/strong-model-gate.test.ts +0 -133
- package/tests/services/resolve-runtime-from-pd-config.test.ts +0 -485
- package/tests/services/rulehost-pipeline-e2e.test.ts +0 -538
- package/tests/services/rulehost-pipeline-runner.test.ts +0 -1242
- package/tests/services/workspace-tool-semantics.test.ts +0 -76
- package/tests/utils/path-security.test.ts +0 -180
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -10
|
@@ -1,916 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pd pain retry command — Retry a failed diagnosis by pain ID.
|
|
3
|
-
*
|
|
4
|
-
* Looks up the diagnostician task for a given painId, validates retry eligibility,
|
|
5
|
-
* then delegates to the existing diagnose run logic.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* pd pain retry --pain-id <painId> --workspace <path> --runtime <kind> [runtime flags] [--json] [--force]
|
|
9
|
-
*
|
|
10
|
-
* IMPORTANT: --runtime is required (no default). test-double would generate fake
|
|
11
|
-
* candidates/ledger in a real workspace, so it must be explicitly requested.
|
|
12
|
-
*/
|
|
13
|
-
import {
|
|
14
|
-
RuntimeStateManager,
|
|
15
|
-
SqliteHistoryQuery,
|
|
16
|
-
SqliteContextAssembler,
|
|
17
|
-
SqliteDiagnosticianCommitter,
|
|
18
|
-
SqliteTrajectoryLocator,
|
|
19
|
-
SqliteSourceTraceLocator,
|
|
20
|
-
StoreEventEmitter,
|
|
21
|
-
SplitDiagnosticianRunner,
|
|
22
|
-
DiagRootCauseRunner,
|
|
23
|
-
DiagDistillerRunner,
|
|
24
|
-
DiagRouterRunner,
|
|
25
|
-
DefaultDiagRootCauseValidator,
|
|
26
|
-
DefaultDiagDistillerValidator,
|
|
27
|
-
type DiagnosticianRunnerLike,
|
|
28
|
-
TestDoubleRuntimeAdapter,
|
|
29
|
-
OpenClawCliRuntimeAdapter,
|
|
30
|
-
PiAiRuntimeAdapter,
|
|
31
|
-
PDRuntimeError,
|
|
32
|
-
isRuntimeConfigError,
|
|
33
|
-
CandidateIntakeService,
|
|
34
|
-
run as diagnoseRun,
|
|
35
|
-
SPLIT_PIPELINE_TOTAL_TIMEOUT_MS,
|
|
36
|
-
resolveDiagnosticianCapability,
|
|
37
|
-
PrincipleTreeLedgerAdapter,
|
|
38
|
-
SqliteDeadLetterStore,
|
|
39
|
-
PainSignalBridge,
|
|
40
|
-
type PainDetectedData,
|
|
41
|
-
type PainSignalBridgeResult,
|
|
42
|
-
type DeadLetterRow,
|
|
43
|
-
} from '@principles/core/runtime-v2';
|
|
44
|
-
import type { PDRuntimeAdapter, RuntimeConfig, OutputLanguage } from '@principles/core/runtime-v2';
|
|
45
|
-
import type { Command } from 'commander';
|
|
46
|
-
import { loadPdConfig, resolvePromptFullPipelineSeedMode } from '../services/pd-config-loader.js';
|
|
47
|
-
import { resolveRuntimeFromPdConfig } from '../services/resolve-runtime-from-pd-config.js';
|
|
48
|
-
import type { PDTaskStatus } from '@principles/core/runtime-v2';
|
|
49
|
-
import { readOutputLanguageFromWorkspace } from '../config-reader.js';
|
|
50
|
-
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
51
|
-
import { checkAdmissionGate } from './admission-gate.js';
|
|
52
|
-
import * as path from 'path';
|
|
53
|
-
|
|
54
|
-
// ── Types ──────────────────────────────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
interface PainRetryOptions {
|
|
57
|
-
painId: string;
|
|
58
|
-
workspace?: string;
|
|
59
|
-
json?: boolean;
|
|
60
|
-
force?: boolean;
|
|
61
|
-
runtime?: string;
|
|
62
|
-
openclawLocal?: boolean;
|
|
63
|
-
openclawGateway?: boolean;
|
|
64
|
-
agent?: string;
|
|
65
|
-
provider?: string;
|
|
66
|
-
model?: string;
|
|
67
|
-
apiKeyEnv?: string;
|
|
68
|
-
baseUrl?: string;
|
|
69
|
-
maxRetries?: number;
|
|
70
|
-
maxTokens?: number;
|
|
71
|
-
timeoutMs?: number;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Allowed task statuses for retry without --force. */
|
|
75
|
-
const RETRYABLE_STATUSES: ReadonlySet<PDTaskStatus> = new Set([
|
|
76
|
-
'retry_wait',
|
|
77
|
-
'failed',
|
|
78
|
-
'needs_human_review',
|
|
79
|
-
]);
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Resolve a painId to a diagnostician taskId.
|
|
83
|
-
*
|
|
84
|
-
* Convention: task_id = `diagnosis_<painId>` (see PainToPrincipleService).
|
|
85
|
-
* If the painId already has the `diagnosis_` prefix, reject to avoid double-prefixing.
|
|
86
|
-
*/
|
|
87
|
-
function resolveTaskIdFromPainId(painId: string): { taskId: string } | { reason: string; nextAction: string } {
|
|
88
|
-
if (painId.startsWith('diagnosis_')) {
|
|
89
|
-
return {
|
|
90
|
-
reason: `painId '${painId}' already has 'diagnosis_' prefix — this looks like a taskId, not a painId`,
|
|
91
|
-
nextAction: `Use the raw painId (without 'diagnosis_' prefix), or use 'pd diagnose run --task-id ${painId}' directly`,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
return { taskId: `diagnosis_${painId}` };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Return value as a non-blank string if it is one, otherwise null. */
|
|
98
|
-
function readNonBlankString(value: unknown): string | null {
|
|
99
|
-
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Validate that an unknown value (read from dead_letter_pains.pain_data) has
|
|
104
|
-
* the shape of PainDetectedData.
|
|
105
|
-
*
|
|
106
|
-
* rc-1: Treat parsed JSON as unknown.
|
|
107
|
-
* rc-2: No `as` bypass — use typeof checks and literal narrowing.
|
|
108
|
-
* rc-3: Required fields (painId, painType, source, reason) fail loud.
|
|
109
|
-
* rc-5: Use Object.hasOwn for field presence checks on untrusted objects.
|
|
110
|
-
*/
|
|
111
|
-
function parsePainDetectedData(value: unknown):
|
|
112
|
-
| { valid: true; data: PainDetectedData }
|
|
113
|
-
| { valid: false; error: string } {
|
|
114
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
115
|
-
return {
|
|
116
|
-
valid: false,
|
|
117
|
-
error: `painData is not an object (got ${value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value})`,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
const obj = value as Record<string, unknown>;
|
|
121
|
-
|
|
122
|
-
// Detect dead-letter error envelopes from SqliteDeadLetterStore parse/serialize failures.
|
|
123
|
-
if (Object.hasOwn(obj, '__deadLetterParseError') || Object.hasOwn(obj, '__deadLetterSerializeError')) {
|
|
124
|
-
return {
|
|
125
|
-
valid: false,
|
|
126
|
-
error: `painData contains dead-letter error envelope: ${JSON.stringify(obj)}`,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Required string fields
|
|
131
|
-
if (typeof obj.painId !== 'string' || obj.painId.trim().length === 0) {
|
|
132
|
-
return { valid: false, error: `painData.painId is not a non-blank string (got ${JSON.stringify(obj.painId)})` };
|
|
133
|
-
}
|
|
134
|
-
if (typeof obj.source !== 'string') {
|
|
135
|
-
return { valid: false, error: `painData.source is not a string (got ${JSON.stringify(obj.source)})` };
|
|
136
|
-
}
|
|
137
|
-
if (typeof obj.reason !== 'string') {
|
|
138
|
-
return { valid: false, error: `painData.reason is not a string (got ${JSON.stringify(obj.reason)})` };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// painType: narrow to literal union without `as`.
|
|
142
|
-
const painTypeRaw = obj.painType;
|
|
143
|
-
if (painTypeRaw !== 'tool_failure' && painTypeRaw !== 'dispatch_error'
|
|
144
|
-
&& painTypeRaw !== 'subagent_error' && painTypeRaw !== 'user_frustration') {
|
|
145
|
-
return { valid: false, error: `painData.painType is not a valid pain type (got ${JSON.stringify(painTypeRaw)})` };
|
|
146
|
-
}
|
|
147
|
-
// After the narrowing check above, TypeScript infers painTypeRaw as the literal union.
|
|
148
|
-
const painType: PainDetectedData['painType'] = painTypeRaw === 'dispatch_error' ? 'tool_failure' : painTypeRaw;
|
|
149
|
-
|
|
150
|
-
// Build the typed object field-by-field. Optional fields are added only when present and valid.
|
|
151
|
-
const data: PainDetectedData = {
|
|
152
|
-
painId: obj.painId,
|
|
153
|
-
painType,
|
|
154
|
-
source: obj.source,
|
|
155
|
-
reason: obj.reason,
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
if (Object.hasOwn(obj, 'score')) {
|
|
159
|
-
if (typeof obj.score !== 'number' || !Number.isFinite(obj.score)) {
|
|
160
|
-
return { valid: false, error: `painData.score is not a finite number (got ${JSON.stringify(obj.score)})` };
|
|
161
|
-
}
|
|
162
|
-
data.score = obj.score;
|
|
163
|
-
}
|
|
164
|
-
if (Object.hasOwn(obj, 'sessionId')) {
|
|
165
|
-
if (typeof obj.sessionId !== 'string') return { valid: false, error: 'painData.sessionId is not a string' };
|
|
166
|
-
data.sessionId = obj.sessionId;
|
|
167
|
-
}
|
|
168
|
-
if (Object.hasOwn(obj, 'agentId')) {
|
|
169
|
-
if (typeof obj.agentId !== 'string') return { valid: false, error: 'painData.agentId is not a string' };
|
|
170
|
-
data.agentId = obj.agentId;
|
|
171
|
-
}
|
|
172
|
-
if (Object.hasOwn(obj, 'taskId')) {
|
|
173
|
-
if (typeof obj.taskId !== 'string') return { valid: false, error: 'painData.taskId is not a string' };
|
|
174
|
-
data.taskId = obj.taskId;
|
|
175
|
-
}
|
|
176
|
-
if (Object.hasOwn(obj, 'traceId')) {
|
|
177
|
-
if (typeof obj.traceId !== 'string') return { valid: false, error: 'painData.traceId is not a string' };
|
|
178
|
-
data.traceId = obj.traceId;
|
|
179
|
-
}
|
|
180
|
-
if (Object.hasOwn(obj, 'provenance')) {
|
|
181
|
-
const prov = obj.provenance;
|
|
182
|
-
// host_context_bound is the current value (SPEC §12); the legacy
|
|
183
|
-
// openclaw_context_bound spelling remains valid for replay of old dead letters.
|
|
184
|
-
if (prov !== 'host_context_bound' && prov !== 'openclaw_context_bound' && prov !== 'owner_reported_no_host_trace' && prov !== 'automatic_hook') {
|
|
185
|
-
return { valid: false, error: `painData.provenance is not a known literal (got ${JSON.stringify(prov)})` };
|
|
186
|
-
}
|
|
187
|
-
data.provenance = prov === 'openclaw_context_bound' ? 'host_context_bound' : prov;
|
|
188
|
-
}
|
|
189
|
-
if (Object.hasOwn(obj, 'evidence')) {
|
|
190
|
-
if (!Array.isArray(obj.evidence)) {
|
|
191
|
-
return { valid: false, error: 'painData.evidence is not an array' };
|
|
192
|
-
}
|
|
193
|
-
// Validate each entry has the PainEvidenceEntry shape { sourceRef: string, note: string }.
|
|
194
|
-
const evidence: { sourceRef: string; note: string }[] = [];
|
|
195
|
-
for (let i = 0; i < obj.evidence.length; i++) {
|
|
196
|
-
const entry = obj.evidence[i];
|
|
197
|
-
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
198
|
-
return { valid: false, error: `painData.evidence[${i}] is not an object` };
|
|
199
|
-
}
|
|
200
|
-
const e = entry as Record<string, unknown>;
|
|
201
|
-
if (typeof e.sourceRef !== 'string' || typeof e.note !== 'string') {
|
|
202
|
-
return { valid: false, error: `painData.evidence[${i}] missing string fields sourceRef/note` };
|
|
203
|
-
}
|
|
204
|
-
evidence.push({ sourceRef: e.sourceRef, note: e.note });
|
|
205
|
-
}
|
|
206
|
-
data.evidence = evidence;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return { valid: true, data };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Output a refused/not_found result, respecting --json mode. Exits with code 1. */
|
|
213
|
-
function refuseExit(opts: PainRetryOptions, payload: { status?: string; painId: string; taskId?: string; reason: string; message?: string; nextAction: string }): void {
|
|
214
|
-
if (opts.json) {
|
|
215
|
-
console.log(JSON.stringify({
|
|
216
|
-
status: payload.status ?? 'refused',
|
|
217
|
-
painId: payload.painId,
|
|
218
|
-
taskId: payload.taskId ?? null,
|
|
219
|
-
reason: payload.reason,
|
|
220
|
-
...(payload.message ? { message: payload.message } : {}),
|
|
221
|
-
nextAction: payload.nextAction,
|
|
222
|
-
}));
|
|
223
|
-
} else {
|
|
224
|
-
console.error(`error: ${payload.message ?? payload.reason}`);
|
|
225
|
-
// PRI-638: the reason must be visible in text mode too — an Owner kill
|
|
226
|
-
// switch (capability_disabled) must not read like a runtime fault.
|
|
227
|
-
console.error(`reason: ${payload.reason}`);
|
|
228
|
-
console.error(`nextAction: ${payload.nextAction}`);
|
|
229
|
-
}
|
|
230
|
-
process.exit(1);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// ── Handler ────────────────────────────────────────────────────────────────────
|
|
234
|
-
|
|
235
|
-
export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
|
|
236
|
-
const workspaceDir = resolveWorkspaceDir(opts.workspace);
|
|
237
|
-
|
|
238
|
-
// Step 1: Resolve painId → taskId
|
|
239
|
-
const resolution = resolveTaskIdFromPainId(opts.painId);
|
|
240
|
-
// eslint-disable-next-line no-restricted-syntax -- 'in' required for discriminated union narrowing (taskId vs reason/nextAction)
|
|
241
|
-
if ('reason' in resolution) {
|
|
242
|
-
return refuseExit(opts, { painId: opts.painId, reason: resolution.reason, nextAction: resolution.nextAction });
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const { taskId } = resolution;
|
|
246
|
-
|
|
247
|
-
// PRI-638: the capability gate comes BEFORE runtime resolution. Previously an
|
|
248
|
-
// Owner-disabled Diagnostician surfaced here as `missing_runtime` ("no
|
|
249
|
-
// .pd/config.yaml runtime binding found"), which told the Owner their config
|
|
250
|
-
// was broken when they had deliberately switched the agent off. This reads the
|
|
251
|
-
// same canonical authority (`internalAgents.agents.diagnostician.enabled`) the
|
|
252
|
-
// runtime factory uses — the CLI owns no kill switch of its own.
|
|
253
|
-
const capability = resolveDiagnosticianCapability(
|
|
254
|
-
(() => {
|
|
255
|
-
const loaded = loadPdConfig(workspaceDir);
|
|
256
|
-
return loaded.ok ? loaded.effective : loaded.defaults;
|
|
257
|
-
})(),
|
|
258
|
-
);
|
|
259
|
-
if (!capability.available) {
|
|
260
|
-
return refuseExit(opts, {
|
|
261
|
-
painId: opts.painId,
|
|
262
|
-
taskId,
|
|
263
|
-
reason: capability.reason,
|
|
264
|
-
message: capability.message,
|
|
265
|
-
nextAction: capability.nextAction,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Step 2: Look up task and validate
|
|
270
|
-
const stateManager = new RuntimeStateManager({ workspaceDir });
|
|
271
|
-
|
|
272
|
-
try {
|
|
273
|
-
await stateManager.initialize();
|
|
274
|
-
|
|
275
|
-
const task = await stateManager.getTask(taskId);
|
|
276
|
-
|
|
277
|
-
// Dead letter replay: when no task exists, check if the pain signal was
|
|
278
|
-
// persisted to dead_letter_pains (rc-9: no silent failure in pain.ts).
|
|
279
|
-
// If found, replay it through PainSignalBridge.onPainDetected.
|
|
280
|
-
let deadLetterReplay: { data: PainDetectedData; row: DeadLetterRow } | null = null;
|
|
281
|
-
if (!task) {
|
|
282
|
-
const dlStore = new SqliteDeadLetterStore(stateManager.connection);
|
|
283
|
-
const dlRow = dlStore.getByPainId(opts.painId);
|
|
284
|
-
if (!dlRow) {
|
|
285
|
-
return refuseExit(opts, {
|
|
286
|
-
status: 'not_found',
|
|
287
|
-
painId: opts.painId,
|
|
288
|
-
taskId,
|
|
289
|
-
reason: 'task_not_found',
|
|
290
|
-
message: `No task found for painId '${opts.painId}' (looked for taskId '${taskId}') and no dead letter found`,
|
|
291
|
-
nextAction: `Verify the painId is correct. Use 'pd task list --kind diagnostician' to see all diagnostician tasks.`,
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
const parseResult = parsePainDetectedData(dlRow.painData);
|
|
295
|
-
if (!parseResult.valid) {
|
|
296
|
-
return refuseExit(opts, {
|
|
297
|
-
painId: opts.painId,
|
|
298
|
-
taskId,
|
|
299
|
-
reason: 'dead_letter_corrupt',
|
|
300
|
-
message: `Dead letter for painId '${opts.painId}' has corrupt painData: ${parseResult.error}`,
|
|
301
|
-
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}'`,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
deadLetterReplay = { data: parseResult.data, row: dlRow };
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (task && task.taskKind !== 'diagnostician') {
|
|
308
|
-
return refuseExit(opts, {
|
|
309
|
-
painId: opts.painId,
|
|
310
|
-
taskId,
|
|
311
|
-
reason: 'wrong_task_kind',
|
|
312
|
-
message: `Task '${taskId}' is not a diagnostician task (taskKind='${task.taskKind}')`,
|
|
313
|
-
nextAction: `pd pain retry only retries diagnostician tasks. Use 'pd diagnose run --task-id ${taskId}' for other task kinds.`,
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// For dead letter replay there is no prior task; synthesize a status so the
|
|
318
|
-
// existing logging/output paths have a non-null value to print.
|
|
319
|
-
const previousTaskStatus: PDTaskStatus | 'dead_lettered' = task ? task.status : 'dead_lettered';
|
|
320
|
-
const previousLastError = task ? (task.lastError ?? null) : null;
|
|
321
|
-
|
|
322
|
-
if (task && task.status === 'succeeded' && !opts.force) {
|
|
323
|
-
return refuseExit(opts, {
|
|
324
|
-
painId: opts.painId,
|
|
325
|
-
taskId,
|
|
326
|
-
reason: 'already_succeeded',
|
|
327
|
-
message: `Task '${taskId}' already succeeded. Use --force to re-run a succeeded task.`,
|
|
328
|
-
nextAction: `Add --force to retry: pd pain retry --pain-id ${opts.painId} --force`,
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (task && !RETRYABLE_STATUSES.has(task.status) && task.status !== 'succeeded') {
|
|
333
|
-
return refuseExit(opts, {
|
|
334
|
-
painId: opts.painId,
|
|
335
|
-
taskId,
|
|
336
|
-
reason: 'status_not_retryable',
|
|
337
|
-
message: `Task '${taskId}' has status '${task.status}' which is not retryable. Retryable statuses: ${[...RETRYABLE_STATUSES].join(', ')}`,
|
|
338
|
-
nextAction: `Wait for the task to reach a terminal state, or use 'pd diagnose run --task-id ${taskId}' directly.`,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Step 3: Resolve runtime kind
|
|
343
|
-
// P1 fix: --openclaw-local and --openclaw-gateway are mutually exclusive.
|
|
344
|
-
// Must output JSON when --json is set (CLI operator gate).
|
|
345
|
-
if (opts.openclawLocal && opts.openclawGateway) {
|
|
346
|
-
return refuseExit(opts, {
|
|
347
|
-
painId: opts.painId,
|
|
348
|
-
taskId,
|
|
349
|
-
reason: 'conflicting_flags',
|
|
350
|
-
message: '--openclaw-local and --openclaw-gateway are mutually exclusive',
|
|
351
|
-
nextAction: 'Provide exactly one of --openclaw-local or --openclaw-gateway, not both.',
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// P1 fix: pd pain retry must NOT default to test-double.
|
|
356
|
-
// This command is for real workspace pain fixes — test-double would generate
|
|
357
|
-
// fake candidates/ledger in a real .pd/state.db. Require explicit --runtime
|
|
358
|
-
// or fall back to .pd/config.yaml.
|
|
359
|
-
let runtimeKind = opts.runtime;
|
|
360
|
-
if (!runtimeKind) {
|
|
361
|
-
const resolved = resolveRuntimeFromPdConfig(workspaceDir);
|
|
362
|
-
if (!isRuntimeConfigError(resolved.result) && resolved.result.runtimeKind) {
|
|
363
|
-
({ runtimeKind } = resolved.result);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (!runtimeKind) {
|
|
368
|
-
return refuseExit(opts, {
|
|
369
|
-
painId: opts.painId,
|
|
370
|
-
taskId,
|
|
371
|
-
reason: 'missing_runtime',
|
|
372
|
-
message: 'No --runtime specified and no .pd/config.yaml runtime binding found. pd pain retry must not default to test-double to prevent fake data in real workspaces.',
|
|
373
|
-
nextAction: `Specify --runtime explicitly: pd pain retry --pain-id ${opts.painId} --runtime pi-ai --provider <provider> --model <model> --apiKeyEnv <ENV>`,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Step 4: Build runtime adapter
|
|
378
|
-
let runtimeAdapter: PDRuntimeAdapter;
|
|
379
|
-
|
|
380
|
-
if (runtimeKind === 'openclaw-cli') {
|
|
381
|
-
const resolved = resolveRuntimeFromPdConfig(workspaceDir);
|
|
382
|
-
const configResult = resolved.result;
|
|
383
|
-
if (isRuntimeConfigError(configResult)) {
|
|
384
|
-
return refuseExit(opts, { painId: opts.painId, taskId, reason: configResult.reason, message: configResult.message, nextAction: configResult.nextAction });
|
|
385
|
-
}
|
|
386
|
-
const { openclawMode } = configResult;
|
|
387
|
-
// CLI flags override config (PRI-393)
|
|
388
|
-
const flagMode = opts.openclawLocal ? 'local' as const : opts.openclawGateway ? 'gateway' as const : undefined;
|
|
389
|
-
const effectiveMode = flagMode ?? openclawMode;
|
|
390
|
-
if (!effectiveMode) {
|
|
391
|
-
return refuseExit(opts, {
|
|
392
|
-
painId: opts.painId,
|
|
393
|
-
taskId,
|
|
394
|
-
reason: 'missing_openclaw_mode',
|
|
395
|
-
message: 'runtimeKind is openclaw-cli but no mode resolved',
|
|
396
|
-
nextAction: 'Provide --openclaw-local or --openclaw-gateway',
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
runtimeAdapter = new OpenClawCliRuntimeAdapter({
|
|
401
|
-
runtimeMode: effectiveMode,
|
|
402
|
-
workspaceDir,
|
|
403
|
-
agentId: opts.agent ?? 'main',
|
|
404
|
-
});
|
|
405
|
-
} else if (runtimeKind === 'test-double') {
|
|
406
|
-
runtimeAdapter = new TestDoubleRuntimeAdapter({
|
|
407
|
-
onPollRun: (_runId: string) => ({
|
|
408
|
-
runId: _runId,
|
|
409
|
-
status: 'succeeded',
|
|
410
|
-
startedAt: new Date().toISOString(),
|
|
411
|
-
endedAt: new Date().toISOString(),
|
|
412
|
-
}),
|
|
413
|
-
onFetchOutput: (_runId: string) => ({
|
|
414
|
-
runId: _runId,
|
|
415
|
-
payload: {
|
|
416
|
-
valid: true,
|
|
417
|
-
diagnosisId: `diag-retry-${Date.now()}`,
|
|
418
|
-
taskId,
|
|
419
|
-
summary: 'CLI retry test diagnosis — validate tool arguments before execution',
|
|
420
|
-
rootCause: 'Test root cause — missing argument validation',
|
|
421
|
-
violatedPrinciples: [],
|
|
422
|
-
evidence: [],
|
|
423
|
-
recommendations: [
|
|
424
|
-
{ kind: 'principle', description: 'Always validate tool arguments before execution to prevent silent failures' },
|
|
425
|
-
{ kind: 'rule', description: 'Use schema validation for external inputs' },
|
|
426
|
-
],
|
|
427
|
-
confidence: 0.9,
|
|
428
|
-
},
|
|
429
|
-
}),
|
|
430
|
-
});
|
|
431
|
-
} else if (runtimeKind === 'pi-ai') {
|
|
432
|
-
const resolved = resolveRuntimeFromPdConfig(workspaceDir);
|
|
433
|
-
for (const w of resolved.legacyWarnings) console.warn(`[pd pain retry] ${w}`);
|
|
434
|
-
|
|
435
|
-
let policyConfig: RuntimeConfig | null = null;
|
|
436
|
-
if (!isRuntimeConfigError(resolved.result)) {
|
|
437
|
-
policyConfig = resolved.result;
|
|
438
|
-
} else {
|
|
439
|
-
console.warn(`[pd pain retry] .pd/config.yaml resolution failed: ${resolved.result.message}. Using CLI flags if provided.`);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const provider = opts.provider ?? policyConfig?.provider;
|
|
443
|
-
const model = opts.model ?? policyConfig?.model;
|
|
444
|
-
const apiKeyEnv = opts.apiKeyEnv ?? policyConfig?.apiKeyEnv;
|
|
445
|
-
const baseUrl = opts.baseUrl ?? policyConfig?.baseUrl;
|
|
446
|
-
const maxRetries = opts.maxRetries ?? policyConfig?.maxRetries;
|
|
447
|
-
const maxTokens = opts.maxTokens ?? policyConfig?.maxTokens;
|
|
448
|
-
const effectiveTimeoutMs = opts.timeoutMs ?? policyConfig?.timeoutMs;
|
|
449
|
-
|
|
450
|
-
// Validate required string fields: must be non-blank strings
|
|
451
|
-
const missing: string[] = [];
|
|
452
|
-
if (readNonBlankString(provider) === null) missing.push('provider');
|
|
453
|
-
if (readNonBlankString(model) === null) missing.push('model');
|
|
454
|
-
if (readNonBlankString(apiKeyEnv) === null) missing.push('apiKeyEnv');
|
|
455
|
-
if (missing.length > 0) {
|
|
456
|
-
return refuseExit(opts, {
|
|
457
|
-
painId: opts.painId,
|
|
458
|
-
taskId,
|
|
459
|
-
reason: `missing_required_config: ${missing.join(', ')}`,
|
|
460
|
-
message: `Missing or blank required pi-ai config: ${missing.join(', ')}`,
|
|
461
|
-
nextAction: `Pass via --flag or add to .pd/config.yaml. Example: pd pain retry --pain-id ${opts.painId} --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY`,
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Validate numeric options: must be finite, integer, non-negative if provided
|
|
466
|
-
const invalidNumeric: string[] = [];
|
|
467
|
-
if (maxRetries !== undefined && maxRetries !== null && !(Number.isFinite(maxRetries) && Number.isInteger(maxRetries) && maxRetries >= 0)) {
|
|
468
|
-
invalidNumeric.push(`maxRetries (got: ${maxRetries})`);
|
|
469
|
-
}
|
|
470
|
-
if (effectiveTimeoutMs !== undefined && effectiveTimeoutMs !== null && !(Number.isFinite(effectiveTimeoutMs) && effectiveTimeoutMs > 0)) {
|
|
471
|
-
invalidNumeric.push(`timeoutMs (got: ${effectiveTimeoutMs})`);
|
|
472
|
-
}
|
|
473
|
-
if (maxTokens !== undefined && maxTokens !== null && !(Number.isFinite(maxTokens) && maxTokens > 0)) {
|
|
474
|
-
invalidNumeric.push(`maxTokens (got: ${maxTokens})`);
|
|
475
|
-
}
|
|
476
|
-
if (invalidNumeric.length > 0) {
|
|
477
|
-
return refuseExit(opts, {
|
|
478
|
-
painId: opts.painId,
|
|
479
|
-
taskId,
|
|
480
|
-
reason: `invalid_numeric_config: ${invalidNumeric.join(', ')}`,
|
|
481
|
-
message: `Invalid numeric pi-ai config: ${invalidNumeric.join(', ')}. maxRetries must be a non-negative integer; timeoutMs and maxTokens must be positive numbers.`,
|
|
482
|
-
nextAction: 'Fix the numeric values and retry.',
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// After validation, these are guaranteed non-blank strings.
|
|
487
|
-
const validProvider = readNonBlankString(provider);
|
|
488
|
-
const validModel = readNonBlankString(model);
|
|
489
|
-
const validApiKeyEnv = readNonBlankString(apiKeyEnv);
|
|
490
|
-
if (validProvider === null || validModel === null || validApiKeyEnv === null) {
|
|
491
|
-
return refuseExit(opts, {
|
|
492
|
-
painId: opts.painId,
|
|
493
|
-
taskId,
|
|
494
|
-
reason: 'internal_validation_error',
|
|
495
|
-
message: 'Internal error: validated string fields became null after validation.',
|
|
496
|
-
nextAction: 'This should not happen. Please report this bug.',
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (!process.env[validApiKeyEnv]) {
|
|
501
|
-
return refuseExit(opts, {
|
|
502
|
-
painId: opts.painId,
|
|
503
|
-
taskId,
|
|
504
|
-
reason: 'missing_api_key',
|
|
505
|
-
message: `Environment variable '${validApiKeyEnv}' is not set`,
|
|
506
|
-
nextAction: `Set the environment variable: export ${validApiKeyEnv}=<your-api-key>`,
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
runtimeAdapter = new PiAiRuntimeAdapter({
|
|
511
|
-
provider: validProvider,
|
|
512
|
-
model: validModel,
|
|
513
|
-
apiKeyEnv: validApiKeyEnv,
|
|
514
|
-
baseUrl,
|
|
515
|
-
maxRetries,
|
|
516
|
-
maxTokens,
|
|
517
|
-
timeoutMs: effectiveTimeoutMs,
|
|
518
|
-
workspace: workspaceDir,
|
|
519
|
-
// PRI-633: profile systemPrompt rides as the append layer (config-only;
|
|
520
|
-
// no CLI flag — same sourcing as the other profile fields' fallback).
|
|
521
|
-
...(policyConfig?.systemPrompt ? { systemPrompt: policyConfig.systemPrompt } : {}),
|
|
522
|
-
});
|
|
523
|
-
} else {
|
|
524
|
-
return refuseExit(opts, {
|
|
525
|
-
painId: opts.painId,
|
|
526
|
-
taskId,
|
|
527
|
-
reason: `unknown_runtime: '${runtimeKind}'`,
|
|
528
|
-
message: `Unknown runtime kind '${runtimeKind}'`,
|
|
529
|
-
nextAction: 'Supported runtimes: openclaw-cli, test-double, pi-ai',
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Step 5: Build runner and execute (same as diagnose run)
|
|
534
|
-
const sqliteConn = stateManager.connection;
|
|
535
|
-
const { taskStore } = stateManager;
|
|
536
|
-
const { runStore } = stateManager;
|
|
537
|
-
const historyQuery = new SqliteHistoryQuery(sqliteConn);
|
|
538
|
-
const trajectoryLocator = new SqliteTrajectoryLocator(sqliteConn);
|
|
539
|
-
const sourceTraceLocator = new SqliteSourceTraceLocator(taskStore, trajectoryLocator);
|
|
540
|
-
const contextAssembler = new SqliteContextAssembler(taskStore, historyQuery, runStore, { sourceTraceLocator });
|
|
541
|
-
|
|
542
|
-
const eventEmitter = new StoreEventEmitter();
|
|
543
|
-
const committer = new SqliteDiagnosticianCommitter(sqliteConn);
|
|
544
|
-
|
|
545
|
-
// PRI-336: Read outputLanguage from workspace config
|
|
546
|
-
const outputLangResult = readOutputLanguageFromWorkspace(workspaceDir);
|
|
547
|
-
const outputLanguage: OutputLanguage | undefined = outputLangResult.outputLanguage;
|
|
548
|
-
|
|
549
|
-
// PRI-638: implementation selection is gone. The split pipeline is the only
|
|
550
|
-
// Diagnostician implementation, so it always runs with its documented
|
|
551
|
-
// 3-stage budget; `diagnostician_split_pipeline` no longer selects a runner
|
|
552
|
-
// nor disables capability (the capability gate above owns that).
|
|
553
|
-
const pipelineTimeoutMs = SPLIT_PIPELINE_TOTAL_TIMEOUT_MS;
|
|
554
|
-
// BUG-1 (PRI-442): extract effectiveConfig so ADR-0019 LLM rate-limit
|
|
555
|
-
// degradation (isDegradationEnabled in base-peer-runner) can read the
|
|
556
|
-
// diagnostician_llm_degradation feature flag. Without this, the runners
|
|
557
|
-
// receive no effectiveConfig and degradation silently never fires.
|
|
558
|
-
// CR-1 (CodeRabbit P2, rc-9): warn when config load failed so the
|
|
559
|
-
// fallback to defaults is observable — no silent degradation.
|
|
560
|
-
const configLoadResult = loadPdConfig(workspaceDir);
|
|
561
|
-
if (!configLoadResult.ok) {
|
|
562
|
-
const errSummary = configLoadResult.errors
|
|
563
|
-
.map((e) => `${e.path}: ${e.reason}`)
|
|
564
|
-
.join('; ');
|
|
565
|
-
console.warn(
|
|
566
|
-
`[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'}`,
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
const effectiveConfig = configLoadResult.ok ? configLoadResult.effective : configLoadResult.defaults;
|
|
570
|
-
|
|
571
|
-
const resolvedKind = typeof runtimeAdapter.kind === 'function' ? runtimeAdapter.kind() : runtimeKind;
|
|
572
|
-
const perStageTimeoutMs = pipelineTimeoutMs / 3;
|
|
573
|
-
const rootCauseRunner = new DiagRootCauseRunner(
|
|
574
|
-
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDiagRootCauseValidator(), contextAssembler },
|
|
575
|
-
{ owner: 'pd-cli-pain-retry', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs, effectiveConfig },
|
|
576
|
-
);
|
|
577
|
-
const distillerRunner = new DiagDistillerRunner(
|
|
578
|
-
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDiagDistillerValidator() },
|
|
579
|
-
{ owner: 'pd-cli-pain-retry', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs, effectiveConfig },
|
|
580
|
-
);
|
|
581
|
-
const routerRunner = new DiagRouterRunner(
|
|
582
|
-
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, committer },
|
|
583
|
-
{ owner: 'pd-cli-pain-retry', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs, effectiveConfig },
|
|
584
|
-
);
|
|
585
|
-
|
|
586
|
-
const runner: DiagnosticianRunnerLike = new SplitDiagnosticianRunner({
|
|
587
|
-
rootCauseRunner,
|
|
588
|
-
distillerRunner,
|
|
589
|
-
routerRunner,
|
|
590
|
-
stateManager,
|
|
591
|
-
committer,
|
|
592
|
-
perStageTimeoutMs,
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
// ── Dead letter replay branch ──────────────────────────────────────────
|
|
596
|
-
// When task was null but a dead letter was found, replay the pain signal
|
|
597
|
-
// through PainSignalBridge.onPainDetected instead of calling diagnoseRun.
|
|
598
|
-
// On success/failure, markRetried updates the dead_letter_pains row.
|
|
599
|
-
if (deadLetterReplay) {
|
|
600
|
-
const dlStore = new SqliteDeadLetterStore(stateManager.connection);
|
|
601
|
-
if (!opts.json) {
|
|
602
|
-
console.log(`\nReplaying dead letter pain: ${opts.painId}`);
|
|
603
|
-
console.log(` Task ID: ${taskId}`);
|
|
604
|
-
console.log(` Dead Letter ID: ${deadLetterReplay.row.id}`);
|
|
605
|
-
console.log(` Failed At: ${deadLetterReplay.row.failedAt}`);
|
|
606
|
-
console.log(` Retry Count: ${deadLetterReplay.row.retryCount}`);
|
|
607
|
-
console.log(` Runtime: ${runtimeKind}`);
|
|
608
|
-
console.log(` Workspace: ${workspaceDir}\n`);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const ledgerAdapter = new PrincipleTreeLedgerAdapter({ stateDir: path.join(workspaceDir, '.state') });
|
|
612
|
-
const intakeService = new CandidateIntakeService({ stateManager, ledgerAdapter });
|
|
613
|
-
const bridge = new PainSignalBridge({
|
|
614
|
-
stateManager,
|
|
615
|
-
runner,
|
|
616
|
-
intakeService,
|
|
617
|
-
ledgerAdapter,
|
|
618
|
-
owner: 'pd-cli-pain-retry-dead-letter',
|
|
619
|
-
workspaceDir,
|
|
620
|
-
// PRI-720: honor the Owner's full-prompt-pipeline switch on replay seeds.
|
|
621
|
-
fullPipelinePromptSeeds: resolvePromptFullPipelineSeedMode(workspaceDir) === 'full_chain',
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
let bridgeResult: PainSignalBridgeResult;
|
|
625
|
-
try {
|
|
626
|
-
bridgeResult = await bridge.onPainDetected(deadLetterReplay.data);
|
|
627
|
-
} catch (err: unknown) {
|
|
628
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
629
|
-
const markFail = dlStore.markRetried(opts.painId, false);
|
|
630
|
-
if (opts.json) {
|
|
631
|
-
console.log(JSON.stringify({
|
|
632
|
-
status: 'failed',
|
|
633
|
-
painId: opts.painId,
|
|
634
|
-
taskId,
|
|
635
|
-
source: 'dead_letter',
|
|
636
|
-
deadLetterId: deadLetterReplay.row.id,
|
|
637
|
-
runtimeKind,
|
|
638
|
-
errorCategory: 'execution_failed',
|
|
639
|
-
message: errorMessage,
|
|
640
|
-
markRetriedOk: markFail.ok,
|
|
641
|
-
nextAction: 'Replay threw; retry_count incremented. Adjust parameters and run pd pain retry --pain-id again.',
|
|
642
|
-
}, null, 2));
|
|
643
|
-
} else {
|
|
644
|
-
console.error(`error: ${errorMessage}`);
|
|
645
|
-
console.error(`nextAction: Replay threw; retry_count incremented. Adjust parameters and try again.`);
|
|
646
|
-
}
|
|
647
|
-
process.exit(1);
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const success = bridgeResult.status === 'succeeded';
|
|
652
|
-
const markResult = dlStore.markRetried(opts.painId, success);
|
|
653
|
-
|
|
654
|
-
if (opts.json) {
|
|
655
|
-
// cli-1-strict-json: exactly one parseable JSON object on stdout.
|
|
656
|
-
console.log(JSON.stringify({
|
|
657
|
-
status: success ? 'succeeded' : 'failed',
|
|
658
|
-
painId: opts.painId,
|
|
659
|
-
taskId,
|
|
660
|
-
source: 'dead_letter',
|
|
661
|
-
deadLetterId: deadLetterReplay.row.id,
|
|
662
|
-
runtimeKind,
|
|
663
|
-
bridgeStatus: bridgeResult.status,
|
|
664
|
-
candidateIds: bridgeResult.candidateIds,
|
|
665
|
-
ledgerEntryIds: bridgeResult.ledgerEntryIds,
|
|
666
|
-
markRetriedOk: markResult.ok,
|
|
667
|
-
message: bridgeResult.message ?? null,
|
|
668
|
-
nextAction: success
|
|
669
|
-
? (bridgeResult.candidateIds.length > 0
|
|
670
|
-
? `Dead letter replayed. Internalize candidates:\n ${bridgeResult.candidateIds.map((id) => `pd candidate internalize --candidate-id ${id} --workspace "${workspaceDir}"`).join('\n ')}`
|
|
671
|
-
: 'Dead letter replayed. No candidates generated.')
|
|
672
|
-
: `Replay did not succeed (status=${bridgeResult.status}). The dead letter remains available for future retry.`,
|
|
673
|
-
}, null, 2));
|
|
674
|
-
if (!success) {
|
|
675
|
-
process.exit(1);
|
|
676
|
-
}
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Text output
|
|
681
|
-
console.log(`\nReplay ${success ? 'succeeded' : 'failed'}:`);
|
|
682
|
-
console.log(` Pain ID: ${opts.painId}`);
|
|
683
|
-
console.log(` Task ID: ${taskId}`);
|
|
684
|
-
console.log(` Dead Letter ID: ${deadLetterReplay.row.id}`);
|
|
685
|
-
console.log(` Bridge Status: ${bridgeResult.status}`);
|
|
686
|
-
if (bridgeResult.message) {
|
|
687
|
-
console.log(` Message: ${bridgeResult.message}`);
|
|
688
|
-
}
|
|
689
|
-
if (success) {
|
|
690
|
-
console.log(` Candidates: ${bridgeResult.candidateIds.length}`);
|
|
691
|
-
if (bridgeResult.candidateIds.length > 0) {
|
|
692
|
-
console.log(` Ledger Entries: ${bridgeResult.ledgerEntryIds.length}`);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
console.log(` Mark Retried: ${markResult.ok ? 'ok' : `FAILED: ${markResult.error}`}`);
|
|
696
|
-
if (!success) {
|
|
697
|
-
console.log(`\n Next Action:`);
|
|
698
|
-
console.log(` Replay did not succeed. The dead letter remains available for future retry.`);
|
|
699
|
-
process.exit(1);
|
|
700
|
-
}
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// ── Normal retry branch ─────────────────────────────────────────────────
|
|
705
|
-
if (!opts.json) {
|
|
706
|
-
console.log(`\nRetrying diagnosis for pain: ${opts.painId}`);
|
|
707
|
-
console.log(` Task ID: ${taskId}`);
|
|
708
|
-
console.log(` Previous: ${previousTaskStatus}${previousLastError ? ` (${previousLastError})` : ''}`);
|
|
709
|
-
console.log(` Runtime: ${runtimeKind}`);
|
|
710
|
-
console.log(` Workspace: ${workspaceDir}\n`);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const result = await diagnoseRun({
|
|
714
|
-
taskId,
|
|
715
|
-
stateManager,
|
|
716
|
-
runner,
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
if (result.status !== 'succeeded') {
|
|
720
|
-
if (opts.json) {
|
|
721
|
-
console.log(JSON.stringify({
|
|
722
|
-
status: 'failed',
|
|
723
|
-
painId: opts.painId,
|
|
724
|
-
taskId,
|
|
725
|
-
runId: null,
|
|
726
|
-
runtimeKind,
|
|
727
|
-
previousTaskStatus,
|
|
728
|
-
previousLastError,
|
|
729
|
-
newTaskStatus: result.status,
|
|
730
|
-
errorCategory: result.errorCategory ?? null,
|
|
731
|
-
failureReason: result.failureReason ?? null,
|
|
732
|
-
nextAction: result.errorCategory === 'output_invalid'
|
|
733
|
-
? 'The LLM output failed validation. Try a different model or provider.'
|
|
734
|
-
: 'Check the error category and retry with adjusted parameters.',
|
|
735
|
-
}, null, 2));
|
|
736
|
-
} else {
|
|
737
|
-
console.log(`\nRetry failed:`);
|
|
738
|
-
console.log(` Status: ${result.status}`);
|
|
739
|
-
console.log(` Task ID: ${result.taskId}`);
|
|
740
|
-
if (result.errorCategory) {
|
|
741
|
-
console.log(` Error Category: ${result.errorCategory}`);
|
|
742
|
-
}
|
|
743
|
-
if (result.failureReason) {
|
|
744
|
-
console.log(` Failure Reason: ${result.failureReason}`);
|
|
745
|
-
}
|
|
746
|
-
console.log('');
|
|
747
|
-
}
|
|
748
|
-
process.exit(1);
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Step 6: Intake candidates
|
|
753
|
-
const candidates = await stateManager.getCandidatesByTaskId(taskId);
|
|
754
|
-
const intakeResults: { candidateId: string; ledgerEntryId?: string; status: string; error?: string; nextAction?: string }[] = [];
|
|
755
|
-
let intakeFailed = false;
|
|
756
|
-
|
|
757
|
-
const ledgerAdapter = new PrincipleTreeLedgerAdapter({ stateDir: path.join(workspaceDir, '.state') });
|
|
758
|
-
const intakeService = new CandidateIntakeService({ stateManager, ledgerAdapter });
|
|
759
|
-
|
|
760
|
-
for (const candidate of candidates) {
|
|
761
|
-
// PRI-503: admission gate check — refuse non-admitted candidates before
|
|
762
|
-
// intake, mirroring candidate.ts (intake/repair/backfill). Without this,
|
|
763
|
-
// `pd pain retry` could bypass the gate and intake unreviewed candidates.
|
|
764
|
-
// cli-5-failure-no-mutation: refusal path does not call intake(), does
|
|
765
|
-
// not update candidate status, does not write ledger.
|
|
766
|
-
// cli-6-output-next-action: refusal result carries admissionBlock.nextAction.
|
|
767
|
-
const admissionBlock = checkAdmissionGate(candidate);
|
|
768
|
-
if (admissionBlock) {
|
|
769
|
-
intakeFailed = true;
|
|
770
|
-
intakeResults.push({
|
|
771
|
-
candidateId: candidate.candidateId,
|
|
772
|
-
status: 'intake_failed',
|
|
773
|
-
error: `Admission gate refused: ${admissionBlock.reason}`,
|
|
774
|
-
nextAction: admissionBlock.nextAction,
|
|
775
|
-
});
|
|
776
|
-
continue;
|
|
777
|
-
}
|
|
778
|
-
try {
|
|
779
|
-
const entry = await intakeService.intake(candidate.candidateId);
|
|
780
|
-
if (candidate.status !== 'consumed') {
|
|
781
|
-
await stateManager.updateCandidateStatus(candidate.candidateId, { status: 'consumed' });
|
|
782
|
-
}
|
|
783
|
-
intakeResults.push({
|
|
784
|
-
candidateId: candidate.candidateId,
|
|
785
|
-
ledgerEntryId: entry.id,
|
|
786
|
-
status: 'consumed',
|
|
787
|
-
});
|
|
788
|
-
} catch (intakeErr: unknown) {
|
|
789
|
-
intakeFailed = true;
|
|
790
|
-
const intakeErrorMessage = intakeErr instanceof Error ? intakeErr.message : String(intakeErr);
|
|
791
|
-
intakeResults.push({
|
|
792
|
-
candidateId: candidate.candidateId,
|
|
793
|
-
status: 'intake_failed',
|
|
794
|
-
error: intakeErrorMessage,
|
|
795
|
-
nextAction: `pd candidate intake --candidate-id ${candidate.candidateId} --workspace "${workspaceDir}"`,
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const candidateIds = candidates.map((c) => c.candidateId);
|
|
801
|
-
const ledgerEntryIds = intakeResults
|
|
802
|
-
.filter((ir): ir is { candidateId: string; ledgerEntryId: string; status: string } => ir.status === 'consumed' && typeof ir.ledgerEntryId === 'string')
|
|
803
|
-
.map((ir) => ir.ledgerEntryId);
|
|
804
|
-
|
|
805
|
-
// Build nextAction: candidates generated but internalization not automatic
|
|
806
|
-
const internalizeNextAction = candidateIds.length > 0
|
|
807
|
-
? `Candidates generated but internalization has NOT started automatically. To begin internalization, run:\n ${candidateIds.map((id) => `pd candidate internalize --candidate-id ${id} --workspace "${workspaceDir}"`).join('\n ')}`
|
|
808
|
-
: 'No candidates were generated from this diagnosis.';
|
|
809
|
-
|
|
810
|
-
if (opts.json) {
|
|
811
|
-
// Strict single JSON object output
|
|
812
|
-
console.log(JSON.stringify({
|
|
813
|
-
status: 'succeeded',
|
|
814
|
-
painId: opts.painId,
|
|
815
|
-
taskId,
|
|
816
|
-
runId: null,
|
|
817
|
-
runtimeKind,
|
|
818
|
-
previousTaskStatus,
|
|
819
|
-
previousLastError,
|
|
820
|
-
newTaskStatus: 'succeeded',
|
|
821
|
-
candidateIds,
|
|
822
|
-
ledgerEntryIds,
|
|
823
|
-
intake: {
|
|
824
|
-
candidates: intakeResults,
|
|
825
|
-
},
|
|
826
|
-
nextAction: internalizeNextAction,
|
|
827
|
-
}, null, 2));
|
|
828
|
-
if (intakeFailed) {
|
|
829
|
-
process.exit(1);
|
|
830
|
-
}
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// Text output
|
|
835
|
-
console.log(`\nRetry succeeded:`);
|
|
836
|
-
console.log(` Pain ID: ${opts.painId}`);
|
|
837
|
-
console.log(` Task ID: ${taskId}`);
|
|
838
|
-
console.log(` Previous Status: ${previousTaskStatus}${previousLastError ? ` (${previousLastError})` : ''}`);
|
|
839
|
-
console.log(` New Status: succeeded`);
|
|
840
|
-
console.log(` Candidates: ${candidateIds.length}`);
|
|
841
|
-
if (result.contextHash) {
|
|
842
|
-
console.log(` Context Hash: ${result.contextHash.substring(0, 16)}...`);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
if (intakeResults.length > 0) {
|
|
846
|
-
console.log(`\n Candidate Intake:`);
|
|
847
|
-
for (const ir of intakeResults) {
|
|
848
|
-
if (ir.status === 'consumed') {
|
|
849
|
-
console.log(` ${ir.candidateId}: consumed (ledger: ${ir.ledgerEntryId})`);
|
|
850
|
-
} else if (ir.status === 'intake_failed') {
|
|
851
|
-
console.log(` ${ir.candidateId}: INTAKE FAILED — ${ir.error}`);
|
|
852
|
-
console.log(` Next action: ${ir.nextAction}`);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
console.log(`\n Next Action:`);
|
|
858
|
-
console.log(` ${internalizeNextAction}`);
|
|
859
|
-
console.log('');
|
|
860
|
-
|
|
861
|
-
if (intakeFailed) {
|
|
862
|
-
process.exit(1);
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
} catch (error: unknown) {
|
|
866
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
867
|
-
let errorCategory = 'execution_failed';
|
|
868
|
-
if (error instanceof PDRuntimeError) {
|
|
869
|
-
errorCategory = error.category;
|
|
870
|
-
}
|
|
871
|
-
if (opts.json) {
|
|
872
|
-
console.log(JSON.stringify({
|
|
873
|
-
status: 'failed',
|
|
874
|
-
painId: opts.painId,
|
|
875
|
-
taskId: `diagnosis_${opts.painId}`,
|
|
876
|
-
errorCategory,
|
|
877
|
-
message,
|
|
878
|
-
nextAction: 'Check the error message and retry with adjusted parameters.',
|
|
879
|
-
}, null, 2));
|
|
880
|
-
} else {
|
|
881
|
-
console.error(`error: ${message} (${errorCategory})`);
|
|
882
|
-
}
|
|
883
|
-
process.exit(1);
|
|
884
|
-
return;
|
|
885
|
-
} finally {
|
|
886
|
-
await stateManager.close();
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
/** Register the production `pd pain retry` command and its complete option contract. */
|
|
891
|
-
export function registerPainRetryCommand(
|
|
892
|
-
painCmd: Command,
|
|
893
|
-
handler: (opts: PainRetryOptions) => Promise<void> = handlePainRetry,
|
|
894
|
-
): Command {
|
|
895
|
-
return painCmd
|
|
896
|
-
.command('retry')
|
|
897
|
-
.description('Retry a failed diagnosis by pain ID')
|
|
898
|
-
.requiredOption('-p, --pain-id <painId>', 'Pain ID to retry diagnosis for')
|
|
899
|
-
.option('-w, --workspace <path>', 'Workspace directory')
|
|
900
|
-
.option('-r, --runtime <kind>', "Runtime kind: 'openclaw-cli', 'test-double', 'pi-ai'")
|
|
901
|
-
.option('--openclaw-local', 'Use local OpenClaw (mutually exclusive with --openclaw-gateway)')
|
|
902
|
-
.option('--openclaw-gateway', 'Use gateway OpenClaw (mutually exclusive with --openclaw-local)')
|
|
903
|
-
.option('-a, --agent <agentId>', 'Agent ID to invoke')
|
|
904
|
-
.option('--provider <name>', 'LLM provider (e.g., openrouter) — for pi-ai, falls back to policy')
|
|
905
|
-
.option('--model <id>', 'Model ID (e.g., anthropic/claude-sonnet-4) — for pi-ai, falls back to policy')
|
|
906
|
-
.option('--apiKeyEnv <name>', 'Env var name for API key — for pi-ai, falls back to policy')
|
|
907
|
-
.option('--baseUrl <url>', 'Custom base URL — for pi-ai, falls back to policy')
|
|
908
|
-
.option('--maxRetries <n>', 'Max retry attempts for LLM failures — for pi-ai, falls back to policy', parseInt)
|
|
909
|
-
.option('--maxTokens <n>', 'Max output tokens (max_tokens) for pi-ai LLM calls — for pi-ai, falls back to policy', parseInt)
|
|
910
|
-
.option('--timeoutMs <ms>', 'Timeout in milliseconds — for pi-ai, falls back to policy', parseInt)
|
|
911
|
-
.option('--force', 'Allow retry of already-succeeded tasks')
|
|
912
|
-
.option('--json', 'Output raw JSON')
|
|
913
|
-
.action(async (opts: PainRetryOptions) => {
|
|
914
|
-
await handler(opts);
|
|
915
|
-
});
|
|
916
|
-
}
|