@principles/pd-cli 1.133.0 → 1.134.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 +40 -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/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 +40 -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/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 +19 -0
- 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
package/src/commands/diagnose.ts
CHANGED
|
@@ -29,13 +29,23 @@ import {
|
|
|
29
29
|
run as diagnoseRun,
|
|
30
30
|
status as diagnoseStatus,
|
|
31
31
|
PrincipleTreeLedgerAdapter,
|
|
32
|
+
buildDreamerSeedFromCandidate,
|
|
33
|
+
CANDIDATE_KIND_TO_ROUTE,
|
|
34
|
+
ROUTE_CHANNEL_MAP,
|
|
35
|
+
MVP_ENABLED_CHANNELS,
|
|
32
36
|
} from '@principles/core/runtime-v2';
|
|
33
37
|
import type { PDRuntimeAdapter, OutputLanguage } from '@principles/core/runtime-v2';
|
|
34
38
|
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
35
39
|
import { readOutputLanguageFromWorkspace } from '../config-reader.js';
|
|
36
40
|
import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
|
|
37
41
|
import { isFeatureEnabled, SPLIT_PIPELINE_TOTAL_TIMEOUT_MS } from '@principles/core/runtime-v2';
|
|
42
|
+
import { createHash } from 'node:crypto';
|
|
43
|
+
/** Layer 0 content-hash (design §6.1); injected so diag writers can attach predecessorSummary hashes. */
|
|
44
|
+
const contentHashFn = (input: string): string => createHash('sha256').update(input).digest('hex');
|
|
38
45
|
import { resolveRuntimeAdapterFromConfig, ConfigResolutionError } from '../services/runtime-adapter-resolver.js';
|
|
46
|
+
import { resolveRuntimeFromPdConfig } from '../services/resolve-runtime-from-pd-config.js';
|
|
47
|
+
import { checkAdmissionGate } from './admission-gate.js';
|
|
48
|
+
import { resolveSourcePainIdFromDiagnostician } from './candidate.js';
|
|
39
49
|
import * as path from 'path';
|
|
40
50
|
|
|
41
51
|
function validateStalledThreshold(val: unknown): number | undefined {
|
|
@@ -198,7 +208,38 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
|
|
|
198
208
|
return;
|
|
199
209
|
}
|
|
200
210
|
|
|
201
|
-
|
|
211
|
+
// Resolve runtime kind. P1 fix (mirrors pain-retry.ts): pd diagnose run
|
|
212
|
+
// must NOT default to test-double. Without --runtime, the split pipeline
|
|
213
|
+
// would validate the test-double's stale DiagnosticianOutputV1-shaped
|
|
214
|
+
// payload against DiagRootCauseOutputV1Schema and fail with
|
|
215
|
+
// max_attempts_exceeded — silently producing failed/fake diagnostic data
|
|
216
|
+
// in a real workspace. Fall back to .pd/config.yaml first; refuse if
|
|
217
|
+
// nothing is bound. (rc-9-no-silent-fallback / ERR-002; EP-03, EP-04)
|
|
218
|
+
let runtimeKind = opts.runtime;
|
|
219
|
+
if (!runtimeKind) {
|
|
220
|
+
const resolved = resolveRuntimeFromPdConfig(workspaceDir);
|
|
221
|
+
if (!isRuntimeConfigError(resolved.result) && resolved.result.runtimeKind) {
|
|
222
|
+
({ runtimeKind } = resolved.result);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!runtimeKind) {
|
|
227
|
+
const missingRuntime = {
|
|
228
|
+
ok: false,
|
|
229
|
+
reason: 'missing_runtime',
|
|
230
|
+
message:
|
|
231
|
+
'No --runtime specified and no .pd/config.yaml runtime binding found. pd diagnose run must not default to test-double to prevent fake data in real workspaces.',
|
|
232
|
+
nextAction: `Specify --runtime explicitly: pd diagnose run --task-id ${opts.taskId} --runtime pi-ai --provider <provider> --model <model> --apiKeyEnv <ENV>`,
|
|
233
|
+
};
|
|
234
|
+
if (opts.json) {
|
|
235
|
+
console.log(JSON.stringify(missingRuntime, null, 2));
|
|
236
|
+
} else {
|
|
237
|
+
console.error(`error: ${missingRuntime.message}`);
|
|
238
|
+
console.error(`Next action: ${missingRuntime.nextAction}`);
|
|
239
|
+
}
|
|
240
|
+
process.exit(1);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
202
243
|
|
|
203
244
|
const stateManager = new RuntimeStateManager({ workspaceDir });
|
|
204
245
|
|
|
@@ -353,22 +394,37 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
|
|
|
353
394
|
const featureFlags = computeFlagsFromLoadResult(configLoadResult);
|
|
354
395
|
const isSplitPipeline = isFeatureEnabled(featureFlags, 'diagnostician_split_pipeline');
|
|
355
396
|
const pipelineTimeoutMs = isSplitPipeline ? SPLIT_PIPELINE_TOTAL_TIMEOUT_MS : 300_000;
|
|
397
|
+
// BUG-1 (PRI-442): extract effectiveConfig so ADR-0019 LLM rate-limit
|
|
398
|
+
// degradation (isDegradationEnabled in base-peer-runner) can read the
|
|
399
|
+
// diagnostician_llm_degradation feature flag. Without this, the runners
|
|
400
|
+
// receive no effectiveConfig and degradation silently never fires.
|
|
401
|
+
// CR-1 (CodeRabbit P2, rc-9): warn when config load failed so the
|
|
402
|
+
// fallback to defaults is observable — no silent degradation.
|
|
403
|
+
if (!configLoadResult.ok) {
|
|
404
|
+
const errSummary = configLoadResult.errors
|
|
405
|
+
.map((e) => `${e.path}: ${e.reason}`)
|
|
406
|
+
.join('; ');
|
|
407
|
+
console.warn(
|
|
408
|
+
`[pd diagnose] .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'}`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const effectiveConfig = configLoadResult.ok ? configLoadResult.effective : configLoadResult.defaults;
|
|
356
412
|
|
|
357
413
|
let runner: DiagnosticianRunnerLike;
|
|
358
414
|
if (isSplitPipeline) {
|
|
359
415
|
const resolvedKind = typeof runtimeAdapter.kind === 'function' ? runtimeAdapter.kind() : runtimeKind;
|
|
360
416
|
const perStageTimeoutMs = pipelineTimeoutMs / 3;
|
|
361
417
|
const rootCauseRunner = new DiagRootCauseRunner(
|
|
362
|
-
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDiagRootCauseValidator(), contextAssembler },
|
|
363
|
-
{ owner: 'pd-cli-diagnose', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs },
|
|
418
|
+
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDiagRootCauseValidator(), contextAssembler, contentHashFn },
|
|
419
|
+
{ owner: 'pd-cli-diagnose', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs, effectiveConfig },
|
|
364
420
|
);
|
|
365
421
|
const distillerRunner = new DiagDistillerRunner(
|
|
366
|
-
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDiagDistillerValidator() },
|
|
367
|
-
{ owner: 'pd-cli-diagnose', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs },
|
|
422
|
+
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDiagDistillerValidator(), contentHashFn },
|
|
423
|
+
{ owner: 'pd-cli-diagnose', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs, effectiveConfig },
|
|
368
424
|
);
|
|
369
425
|
const routerRunner = new DiagRouterRunner(
|
|
370
|
-
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, committer },
|
|
371
|
-
{ owner: 'pd-cli-diagnose', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs },
|
|
426
|
+
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, committer, contentHashFn },
|
|
427
|
+
{ owner: 'pd-cli-diagnose', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs, effectiveConfig },
|
|
372
428
|
);
|
|
373
429
|
|
|
374
430
|
runner = new SplitDiagnosticianRunner({
|
|
@@ -463,6 +519,23 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
|
|
|
463
519
|
const intakeService = new CandidateIntakeService({ stateManager, ledgerAdapter });
|
|
464
520
|
|
|
465
521
|
for (const candidate of candidates) {
|
|
522
|
+
// PRI-503: admission gate check — refuse non-admitted candidates before
|
|
523
|
+
// intake, mirroring candidate.ts (intake/repair/backfill). Without this,
|
|
524
|
+
// `pd diagnose --intake` could bypass the gate and intake unreviewed
|
|
525
|
+
// candidates. cli-5-failure-no-mutation: refusal path does not call
|
|
526
|
+
// intake(), does not update candidate status, does not write ledger.
|
|
527
|
+
// cli-6-output-next-action: refusal result carries admissionBlock.nextAction.
|
|
528
|
+
const admissionBlock = checkAdmissionGate(candidate);
|
|
529
|
+
if (admissionBlock) {
|
|
530
|
+
intakeFailed = true;
|
|
531
|
+
intakeResults.push({
|
|
532
|
+
candidateId: candidate.candidateId,
|
|
533
|
+
status: 'intake_failed',
|
|
534
|
+
error: `Admission gate refused: ${admissionBlock.reason}`,
|
|
535
|
+
nextAction: admissionBlock.nextAction,
|
|
536
|
+
});
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
466
539
|
try {
|
|
467
540
|
const entry = await intakeService.intake(candidate.candidateId);
|
|
468
541
|
if (candidate.status !== 'consumed') {
|
|
@@ -486,11 +559,79 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
|
|
|
486
559
|
}
|
|
487
560
|
}
|
|
488
561
|
|
|
562
|
+
// Defect-004 fix: seed dreamer task for each consumed candidate whose
|
|
563
|
+
// recommendation_kind routes to an MVP-enabled internalization channel.
|
|
564
|
+
// Without this, the chain stops at 'consumed' and never reaches internalization
|
|
565
|
+
// (11 of 18 consumed candidates hit this in production). Mirrors the pattern
|
|
566
|
+
// in PainSignalBridge.onDiagnosisComplete (pain-signal-bridge.ts L310-338).
|
|
567
|
+
//
|
|
568
|
+
// DEFECT-005 (PRI-514): the dreamer seed loop is gated ONLY by `opts.intake`,
|
|
569
|
+
// NOT by a global `intakeFailed` flag. A single defer candidate whose intake
|
|
570
|
+
// is refused by the admission gate must NOT poison dreamer seed for sibling
|
|
571
|
+
// candidates that were successfully consumed (EP-03 / ERR-089 sibling-branch
|
|
572
|
+
// defect). Per-candidate eligibility is checked inside the loop via the
|
|
573
|
+
// intake result's status. `intakeFailed` is retained solely for the
|
|
574
|
+
// nextAction/exit-code signaling below (partial failure still exits non-zero).
|
|
575
|
+
if (opts.intake !== false) {
|
|
576
|
+
for (const candidate of candidates) {
|
|
577
|
+
const kind = candidate.recommendationKind;
|
|
578
|
+
if (kind === 'defer' || kind === 'implementation') continue;
|
|
579
|
+
const intakeResult = intakeResults.find((r) => r.candidateId === candidate.candidateId);
|
|
580
|
+
if (!intakeResult || intakeResult.status !== 'consumed') continue;
|
|
581
|
+
try {
|
|
582
|
+
const route = CANDIDATE_KIND_TO_ROUTE[kind ?? ''];
|
|
583
|
+
if (!route) continue;
|
|
584
|
+
const channel = ROUTE_CHANNEL_MAP[route];
|
|
585
|
+
const ready = !!channel && MVP_ENABLED_CHANNELS.has(channel);
|
|
586
|
+
// BUG-2 (PRI-442): resolve sourcePainId from the diagnostician task
|
|
587
|
+
// chain (rc-6 lineage consistency). The candidate itself doesn't carry
|
|
588
|
+
// sourcePainId; it must be resolved from the diagnostician task's
|
|
589
|
+
// diagnosticJson. ERR-004: never invent lineage.
|
|
590
|
+
const sourcePainId = await resolveSourcePainIdFromDiagnostician(stateManager, candidate);
|
|
591
|
+
const seed = buildDreamerSeedFromCandidate(candidate, { route, ready, sourcePainId: sourcePainId ?? undefined });
|
|
592
|
+
// eslint-disable-next-line no-restricted-syntax -- 'in' required for discriminated union narrowing (BridgeTaskSeed | BridgeDecision)
|
|
593
|
+
if ('decision' in seed) continue; // not_internalizable or invalid — skip
|
|
594
|
+
const existingTask = await stateManager.getTask(seed.taskId);
|
|
595
|
+
if (!existingTask) {
|
|
596
|
+
await stateManager.createTask({
|
|
597
|
+
taskId: seed.taskId,
|
|
598
|
+
taskKind: seed.taskKind,
|
|
599
|
+
inputRef: '',
|
|
600
|
+
status: seed.status,
|
|
601
|
+
attemptCount: seed.attemptCount,
|
|
602
|
+
maxAttempts: seed.maxAttempts,
|
|
603
|
+
diagnosticJson: seed.diagnosticJson,
|
|
604
|
+
});
|
|
605
|
+
intakeResults.push({
|
|
606
|
+
candidateId: candidate.candidateId,
|
|
607
|
+
status: 'dreamer_seeded',
|
|
608
|
+
nextAction: `pd task show ${seed.taskId} | pd dreamer run --task-id ${seed.taskId}`,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
} catch (seedErr) {
|
|
612
|
+
intakeResults.push({
|
|
613
|
+
candidateId: candidate.candidateId,
|
|
614
|
+
status: 'dreamer_seed_failed',
|
|
615
|
+
error: seedErr instanceof Error ? seedErr.message : String(seedErr),
|
|
616
|
+
nextAction: `pd candidate internalize --candidate-id ${candidate.candidateId}`,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
489
622
|
if (opts.json) {
|
|
490
623
|
const candidateIds = candidates.map((c) => c.candidateId);
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
624
|
+
// CodeRabbit review fix: reflect dreamer seed status in nextAction so
|
|
625
|
+
// the owner knows whether internalization has already started or still
|
|
626
|
+
// needs manual candidate internalize. Previously the message always
|
|
627
|
+
// said "NOT started automatically" even when dreamer tasks were seeded.
|
|
628
|
+
const dreamerSeededCount = intakeResults.filter((r) => r.status === 'dreamer_seeded').length;
|
|
629
|
+
const dreamerSeedFailedCount = intakeResults.filter((r) => r.status === 'dreamer_seed_failed').length;
|
|
630
|
+
const internalizeNextAction = dreamerSeededCount > 0
|
|
631
|
+
? `Dreamer tasks seeded automatically for ${dreamerSeededCount} candidate(s). To continue, run the dreamer tasks shown in intake.candidates[].${dreamerSeedFailedCount > 0 ? ` (${dreamerSeedFailedCount} candidate(s) failed seeding — see intake.candidates[] for retry guidance.)` : ''}`
|
|
632
|
+
: candidateIds.length > 0
|
|
633
|
+
? `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 ')}`
|
|
634
|
+
: 'No candidates were generated from this diagnosis.';
|
|
494
635
|
const jsonOutput = {
|
|
495
636
|
...result,
|
|
496
637
|
intake: {
|
|
@@ -534,6 +675,13 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
|
|
|
534
675
|
} else if (ir.status === 'intake_failed') {
|
|
535
676
|
console.log(` ${ir.candidateId}: INTAKE FAILED — ${ir.error}`);
|
|
536
677
|
console.log(` Next action: pd candidate intake --candidate-id ${ir.candidateId} --workspace "${workspaceDir}"`);
|
|
678
|
+
} else if (ir.status === 'dreamer_seeded') {
|
|
679
|
+
// CodeRabbit review fix: surface dreamer seed success in TTY output
|
|
680
|
+
console.log(` ${ir.candidateId}: dreamer seeded — ${ir.nextAction}`);
|
|
681
|
+
} else if (ir.status === 'dreamer_seed_failed') {
|
|
682
|
+
// CodeRabbit review fix: surface dreamer seed failure in TTY output
|
|
683
|
+
console.log(` ${ir.candidateId}: DREAMER SEED FAILED — ${ir.error}`);
|
|
684
|
+
console.log(` Next action: ${ir.nextAction}`);
|
|
537
685
|
}
|
|
538
686
|
}
|
|
539
687
|
}
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pd errors list — List PD pipeline errors (failed tasks + worker errors).
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* pd errors list [--json] [--kind <runnerKind>] [--since <hours>] [--limit <n>] [--workspace <path>]
|
|
6
|
+
*
|
|
7
|
+
* Aggregates two error sources into a single operator-facing read-only view:
|
|
8
|
+
* 1. SQLite state.db tasks where status IN ('failed', 'needs_human_review')
|
|
9
|
+
* (via SqliteTaskStore.listFailedTasks — Task 8).
|
|
10
|
+
* 2. worker-status.json `errors` array written by EvolutionWorker
|
|
11
|
+
* (Task 2 extended the entry shape to { at, kind, principleId, error } | string).
|
|
12
|
+
*
|
|
13
|
+
* CLI gate compliance:
|
|
14
|
+
* - cli-1-strict-json: --json stdout is exactly one parseable JSON object.
|
|
15
|
+
* - cli-2-exit-stops: every process.exit(...) is immediately followed by return.
|
|
16
|
+
* - cli-6-output-next-action: empty result includes a `nextAction` field.
|
|
17
|
+
* - cli-4-dry-run-confirm-mutex: N/A (read-only command).
|
|
18
|
+
*
|
|
19
|
+
* Runtime contract compliance:
|
|
20
|
+
* - rc-1-treat-as-unknown: worker-status.json parsed content is treated as unknown.
|
|
21
|
+
* - rc-2-no-as-bypass: no `as` casts; runtime validation via typeof / Array.isArray.
|
|
22
|
+
* - rc-4-validate-array-elements: errors array elements are validated element-wise.
|
|
23
|
+
* - rc-5-object-hasown-not-in: Object.hasOwn used for untrusted object key checks.
|
|
24
|
+
* - rc-9-no-silent-fallback: missing worker-status.json → workerErrors=[] with
|
|
25
|
+
* a `workerStatusMissing` note; parse failures surface a `workerStatusWarning`.
|
|
26
|
+
*/
|
|
27
|
+
import * as path from 'path';
|
|
28
|
+
import * as fs from 'fs';
|
|
29
|
+
import type { Command } from 'commander';
|
|
30
|
+
import { SqliteConnection, SqliteTaskStore } from '@principles/core';
|
|
31
|
+
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Options accepted by the `pd errors list` command.
|
|
35
|
+
*
|
|
36
|
+
* `since` is in hours (CLI surface); converted to a Unix-ms timestamp when
|
|
37
|
+
* passed to SqliteTaskStore.listFailedTasks.
|
|
38
|
+
*/
|
|
39
|
+
export interface ErrorsListOptions {
|
|
40
|
+
workspace?: string;
|
|
41
|
+
json?: boolean;
|
|
42
|
+
kind?: string;
|
|
43
|
+
since?: number; // hours
|
|
44
|
+
limit?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Default and maximum values for --limit. */
|
|
48
|
+
const DEFAULT_LIMIT = 50;
|
|
49
|
+
const MAX_LIMIT = 200;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Type guard: narrow `unknown` to a plain record (rc-2: no `as` bypass).
|
|
53
|
+
* Returns true only for non-null, non-array objects.
|
|
54
|
+
*/
|
|
55
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
56
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normalized worker-status.json error entry. WorkerStatusErrorEntry in
|
|
61
|
+
* evolution-worker.ts is `{ at, kind, principleId, error }`; legacy entries
|
|
62
|
+
* are bare strings. We normalize both shapes to a structured form for output.
|
|
63
|
+
*/
|
|
64
|
+
interface WorkerErrorEntry {
|
|
65
|
+
at: string | null;
|
|
66
|
+
kind: string | null;
|
|
67
|
+
principleId: string | null;
|
|
68
|
+
error: string;
|
|
69
|
+
rawForm: 'object' | 'string';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Aggregated result returned by the command (JSON or text-rendered). */
|
|
73
|
+
interface ErrorsListResult {
|
|
74
|
+
tasks: unknown[];
|
|
75
|
+
workerErrors: WorkerErrorEntry[];
|
|
76
|
+
total: number;
|
|
77
|
+
workerStatusPath: string;
|
|
78
|
+
workerStatusMissing?: boolean;
|
|
79
|
+
workerStatusWarning?: string;
|
|
80
|
+
nextAction?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate and normalize a single worker-status.json errors entry.
|
|
85
|
+
*
|
|
86
|
+
* rc-1: input is `unknown` (parsed from JSON).
|
|
87
|
+
* rc-2: no `as` — uses typeof / Object.hasOwn guards.
|
|
88
|
+
* rc-5: Object.hasOwn for key checks (not `in`).
|
|
89
|
+
*
|
|
90
|
+
* Accepts:
|
|
91
|
+
* - string → { error: <string>, rawForm: 'string', others null }
|
|
92
|
+
* - { at, kind, principleId, error } → normalized object
|
|
93
|
+
* Anything else → null (skipped; caller records a warning).
|
|
94
|
+
*/
|
|
95
|
+
function normalizeWorkerErrorEntry(entry: unknown): WorkerErrorEntry | null {
|
|
96
|
+
if (typeof entry === 'string') {
|
|
97
|
+
return {
|
|
98
|
+
at: null,
|
|
99
|
+
kind: null,
|
|
100
|
+
principleId: null,
|
|
101
|
+
error: entry,
|
|
102
|
+
rawForm: 'string',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// rc-2: type guard instead of `as` cast (isRecord narrows unknown → record).
|
|
106
|
+
if (!isRecord(entry)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
// rc-5: use Object.hasOwn, not `in`, for untrusted object key checks.
|
|
110
|
+
const at = Object.hasOwn(entry, 'at') && typeof entry.at === 'string' ? entry.at : null;
|
|
111
|
+
const kind = Object.hasOwn(entry, 'kind') && typeof entry.kind === 'string' ? entry.kind : null;
|
|
112
|
+
const principleId =
|
|
113
|
+
Object.hasOwn(entry, 'principleId') && typeof entry.principleId === 'string' ? entry.principleId : null;
|
|
114
|
+
// `error` is the only required field for the object form; if missing or
|
|
115
|
+
// non-string, fall back to JSON.stringify so we don't lose the entry.
|
|
116
|
+
let errorStr: string;
|
|
117
|
+
if (Object.hasOwn(entry, 'error') && typeof entry.error === 'string') {
|
|
118
|
+
errorStr = entry.error;
|
|
119
|
+
} else {
|
|
120
|
+
try {
|
|
121
|
+
errorStr = JSON.stringify(entry);
|
|
122
|
+
} catch {
|
|
123
|
+
errorStr = '[unserializable worker-status error entry]';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
at,
|
|
128
|
+
kind,
|
|
129
|
+
principleId,
|
|
130
|
+
error: errorStr,
|
|
131
|
+
rawForm: 'object',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Read and validate the worker-status.json `errors` array.
|
|
137
|
+
*
|
|
138
|
+
* Returns `{ entries, missing, warning }`:
|
|
139
|
+
* - `missing: true` if the file does not exist (not an error — worker hasn't run).
|
|
140
|
+
* - `warning: string` if the file exists but is unparseable / wrong shape
|
|
141
|
+
* (rc-9: surface the reason, do not silently swallow).
|
|
142
|
+
* - `entries: WorkerErrorEntry[]` validated element-wise (rc-4).
|
|
143
|
+
*/
|
|
144
|
+
function readWorkerStatusErrors(stateDir: string): {
|
|
145
|
+
entries: WorkerErrorEntry[];
|
|
146
|
+
missing: boolean;
|
|
147
|
+
warning: string | null;
|
|
148
|
+
} {
|
|
149
|
+
const statusPath = path.join(stateDir, 'worker-status.json');
|
|
150
|
+
if (!fs.existsSync(statusPath)) {
|
|
151
|
+
return { entries: [], missing: true, warning: null };
|
|
152
|
+
}
|
|
153
|
+
let raw: string;
|
|
154
|
+
try {
|
|
155
|
+
raw = fs.readFileSync(statusPath, 'utf8');
|
|
156
|
+
} catch (err) {
|
|
157
|
+
return {
|
|
158
|
+
entries: [],
|
|
159
|
+
missing: false,
|
|
160
|
+
warning: `Failed to read worker-status.json: ${err instanceof Error ? err.message : String(err)}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// rc-1: parsed JSON is unknown.
|
|
164
|
+
let parsed: unknown;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(raw);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return {
|
|
169
|
+
entries: [],
|
|
170
|
+
missing: false,
|
|
171
|
+
warning: `Failed to parse worker-status.json: ${err instanceof Error ? err.message : String(err)}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (!isRecord(parsed)) {
|
|
175
|
+
return {
|
|
176
|
+
entries: [],
|
|
177
|
+
missing: false,
|
|
178
|
+
warning: 'worker-status.json root is not a JSON object',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// rc-5: Object.hasOwn for untrusted key check.
|
|
182
|
+
if (!Object.hasOwn(parsed, 'errors')) {
|
|
183
|
+
// No `errors` field — treat as empty (older worker-status.json may lack it).
|
|
184
|
+
return { entries: [], missing: false, warning: null };
|
|
185
|
+
}
|
|
186
|
+
const errorsField = parsed.errors;
|
|
187
|
+
if (!Array.isArray(errorsField)) {
|
|
188
|
+
return {
|
|
189
|
+
entries: [],
|
|
190
|
+
missing: false,
|
|
191
|
+
warning: 'worker-status.json `errors` field is not an array',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// rc-4: validate array element types; skip malformed entries.
|
|
195
|
+
const entries: WorkerErrorEntry[] = [];
|
|
196
|
+
let skipped = 0;
|
|
197
|
+
for (const item of errorsField) {
|
|
198
|
+
const normalized = normalizeWorkerErrorEntry(item);
|
|
199
|
+
if (normalized === null) {
|
|
200
|
+
skipped++;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
entries.push(normalized);
|
|
204
|
+
}
|
|
205
|
+
const warning =
|
|
206
|
+
skipped > 0
|
|
207
|
+
? `Skipped ${skipped} malformed worker-status.json error entr${skipped === 1 ? 'y' : 'ies'}`
|
|
208
|
+
: null;
|
|
209
|
+
return { entries, missing: false, warning };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function formatTextOutput(result: ErrorsListResult, workspaceDir: string): string {
|
|
213
|
+
const lines: string[] = [];
|
|
214
|
+
lines.push('PD Errors List');
|
|
215
|
+
lines.push(` workspace: ${workspaceDir}`);
|
|
216
|
+
lines.push(` total: ${result.total} (tasks: ${result.tasks.length}, workerErrors: ${result.workerErrors.length})`);
|
|
217
|
+
lines.push('');
|
|
218
|
+
if (result.workerStatusMissing) {
|
|
219
|
+
lines.push(` workerStatus: (missing — ${result.workerStatusPath} not found)`);
|
|
220
|
+
} else if (result.workerStatusWarning) {
|
|
221
|
+
lines.push(` workerStatus: WARNING — ${result.workerStatusWarning}`);
|
|
222
|
+
}
|
|
223
|
+
if (result.tasks.length > 0) {
|
|
224
|
+
lines.push('');
|
|
225
|
+
lines.push('Failed tasks:');
|
|
226
|
+
for (const t of result.tasks) {
|
|
227
|
+
// rc-2: type guard instead of `as` cast — tasks come from SqliteTaskStore
|
|
228
|
+
// which returns FailedTaskSummary[], but we typed tasks as unknown[] to
|
|
229
|
+
// keep the result interface decoupled from core's type exports.
|
|
230
|
+
const task = isRecord(t) ? t : null;
|
|
231
|
+
const taskId = task && typeof task.taskId === 'string' ? task.taskId : '?';
|
|
232
|
+
const taskKind = task && typeof task.taskKind === 'string' ? task.taskKind : '?';
|
|
233
|
+
const status = task && typeof task.status === 'string' ? task.status : '?';
|
|
234
|
+
const lastError = task && typeof task.lastError === 'string' ? task.lastError : '-';
|
|
235
|
+
const attemptCount = task && typeof task.attemptCount === 'number' ? task.attemptCount : 0;
|
|
236
|
+
const painId = task && typeof task.painId === 'string' ? task.painId : '-';
|
|
237
|
+
const lastAttemptAt = task && typeof task.lastAttemptAt === 'string' ? task.lastAttemptAt : '-';
|
|
238
|
+
lines.push(` - ${taskId} (${taskKind}) [${status}]`);
|
|
239
|
+
lines.push(` painId: ${painId}`);
|
|
240
|
+
lines.push(` lastError: ${lastError}`);
|
|
241
|
+
lines.push(` attemptCount: ${attemptCount}`);
|
|
242
|
+
lines.push(` lastAttemptAt: ${lastAttemptAt}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (result.workerErrors.length > 0) {
|
|
246
|
+
lines.push('');
|
|
247
|
+
lines.push('Worker errors:');
|
|
248
|
+
for (const e of result.workerErrors) {
|
|
249
|
+
const at = e.at ?? '-';
|
|
250
|
+
const kind = e.kind ?? '-';
|
|
251
|
+
const pid = e.principleId ?? '-';
|
|
252
|
+
lines.push(` - [${at}] kind=${kind} principleId=${pid}`);
|
|
253
|
+
lines.push(` error: ${e.error}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (result.total === 0) {
|
|
257
|
+
lines.push('');
|
|
258
|
+
lines.push('No errors found. PD pipeline is healthy.');
|
|
259
|
+
}
|
|
260
|
+
return lines.join('\n');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function handleErrorsList(opts: ErrorsListOptions): Promise<void> {
|
|
264
|
+
const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
|
|
265
|
+
const stateDir = path.join(workspaceDir, '.pd');
|
|
266
|
+
const stateDbPath = path.join(stateDir, 'state.db');
|
|
267
|
+
|
|
268
|
+
// Pre-check: if state.db does not exist, fail loud (cli-2-exit-stops + rc-9).
|
|
269
|
+
if (!fs.existsSync(stateDbPath)) {
|
|
270
|
+
const msg = `Error: state.db not found at ${stateDbPath}. Workspace may not be initialized.`;
|
|
271
|
+
if (opts.json) {
|
|
272
|
+
// cli-1: even error JSON is a single object on stdout? No — cli-2 says
|
|
273
|
+
// failure paths write to stderr. JSON error envelope goes to stderr too
|
|
274
|
+
// so stdout stays clean (cli-1-strict-json guarantees stdout is parseable).
|
|
275
|
+
process.stderr.write(
|
|
276
|
+
JSON.stringify(
|
|
277
|
+
{
|
|
278
|
+
ok: false,
|
|
279
|
+
reason: msg,
|
|
280
|
+
nextAction: 'Run "pd runtime init --confirm --workspace <dir>" to initialize databases, then retry.',
|
|
281
|
+
},
|
|
282
|
+
null,
|
|
283
|
+
2,
|
|
284
|
+
) + '\n',
|
|
285
|
+
);
|
|
286
|
+
} else {
|
|
287
|
+
process.stderr.write(msg + '\n');
|
|
288
|
+
}
|
|
289
|
+
process.exitCode = 1;
|
|
290
|
+
return; // cli-2-exit-stops
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Resolve and validate --limit.
|
|
294
|
+
let limit = DEFAULT_LIMIT;
|
|
295
|
+
if (opts.limit !== undefined) {
|
|
296
|
+
if (!Number.isFinite(opts.limit) || opts.limit <= 0) {
|
|
297
|
+
const msg = `Error: --limit must be a positive integer, got ${opts.limit}`;
|
|
298
|
+
// cli-6-output-next-action: mirror the state.db missing branch — when
|
|
299
|
+
// opts.json is true, emit a structured { ok:false, reason, nextAction }
|
|
300
|
+
// envelope to stderr instead of plain text.
|
|
301
|
+
if (opts.json) {
|
|
302
|
+
process.stderr.write(
|
|
303
|
+
JSON.stringify(
|
|
304
|
+
{
|
|
305
|
+
ok: false,
|
|
306
|
+
reason: msg,
|
|
307
|
+
nextAction: 'Pass --limit as a positive integer (e.g. --limit 50). Maximum is 500.',
|
|
308
|
+
},
|
|
309
|
+
null,
|
|
310
|
+
2,
|
|
311
|
+
) + '\n',
|
|
312
|
+
);
|
|
313
|
+
} else {
|
|
314
|
+
process.stderr.write(msg + '\n');
|
|
315
|
+
}
|
|
316
|
+
process.exitCode = 1;
|
|
317
|
+
return; // cli-2-exit-stops
|
|
318
|
+
}
|
|
319
|
+
limit = Math.min(opts.limit, MAX_LIMIT);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Resolve --since (hours → Unix-ms timestamp).
|
|
323
|
+
let sinceMs: number | undefined;
|
|
324
|
+
if (opts.since !== undefined) {
|
|
325
|
+
if (!Number.isFinite(opts.since) || opts.since < 0) {
|
|
326
|
+
const msg = `Error: --since must be a non-negative number of hours, got ${opts.since}`;
|
|
327
|
+
// cli-6-output-next-action: same JSON envelope pattern as --limit above.
|
|
328
|
+
if (opts.json) {
|
|
329
|
+
process.stderr.write(
|
|
330
|
+
JSON.stringify(
|
|
331
|
+
{
|
|
332
|
+
ok: false,
|
|
333
|
+
reason: msg,
|
|
334
|
+
nextAction: 'Pass --since as a non-negative number of hours (e.g. --since 24 for the last 24h).',
|
|
335
|
+
},
|
|
336
|
+
null,
|
|
337
|
+
2,
|
|
338
|
+
) + '\n',
|
|
339
|
+
);
|
|
340
|
+
} else {
|
|
341
|
+
process.stderr.write(msg + '\n');
|
|
342
|
+
}
|
|
343
|
+
process.exitCode = 1;
|
|
344
|
+
return; // cli-2-exit-stops
|
|
345
|
+
}
|
|
346
|
+
sinceMs = Date.now() - opts.since * 3600 * 1000;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const connection = new SqliteConnection({ workspaceDir, readonly: true });
|
|
350
|
+
try {
|
|
351
|
+
const taskStore = new SqliteTaskStore(connection);
|
|
352
|
+
const tasks = await taskStore.listFailedTasks({
|
|
353
|
+
kind: opts.kind,
|
|
354
|
+
since: sinceMs,
|
|
355
|
+
limit,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const { entries: workerErrors, missing, warning } = readWorkerStatusErrors(stateDir);
|
|
359
|
+
|
|
360
|
+
const total = tasks.length + workerErrors.length;
|
|
361
|
+
const result: ErrorsListResult = {
|
|
362
|
+
tasks,
|
|
363
|
+
workerErrors,
|
|
364
|
+
total,
|
|
365
|
+
workerStatusPath: path.join(stateDir, 'worker-status.json'),
|
|
366
|
+
};
|
|
367
|
+
if (missing) {
|
|
368
|
+
result.workerStatusMissing = true;
|
|
369
|
+
}
|
|
370
|
+
if (warning) {
|
|
371
|
+
result.workerStatusWarning = warning;
|
|
372
|
+
}
|
|
373
|
+
// cli-6-output-next-action: include nextAction only when there are no errors.
|
|
374
|
+
if (total === 0) {
|
|
375
|
+
result.nextAction = 'No errors found. PD pipeline is healthy.';
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (opts.json) {
|
|
379
|
+
// cli-1-strict-json: stdout is exactly one parseable JSON object.
|
|
380
|
+
console.log(JSON.stringify(result, null, 2));
|
|
381
|
+
} else {
|
|
382
|
+
console.log(formatTextOutput(result, workspaceDir));
|
|
383
|
+
}
|
|
384
|
+
} catch (err: unknown) {
|
|
385
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
386
|
+
if (opts.json) {
|
|
387
|
+
process.stderr.write(
|
|
388
|
+
JSON.stringify(
|
|
389
|
+
{
|
|
390
|
+
ok: false,
|
|
391
|
+
reason: `Failed to list errors: ${message}`,
|
|
392
|
+
nextAction: 'Check workspace path and state.db readability, then retry.',
|
|
393
|
+
},
|
|
394
|
+
null,
|
|
395
|
+
2,
|
|
396
|
+
) + '\n',
|
|
397
|
+
);
|
|
398
|
+
} else {
|
|
399
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
400
|
+
}
|
|
401
|
+
process.exitCode = 1;
|
|
402
|
+
return; // cli-2-exit-stops
|
|
403
|
+
} finally {
|
|
404
|
+
connection.close();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Register the `errors` command group with `list` subcommand on a Commander
|
|
410
|
+
* program. Used by both production CLI (src/index.ts) and parser-level tests
|
|
411
|
+
* (cli-7-test-wiring).
|
|
412
|
+
*
|
|
413
|
+
* The `errors` command group is hidden from `pd --help` (operator diagnostic,
|
|
414
|
+
* same convention as `legacy` / `artifact` / `runtime recovery`).
|
|
415
|
+
*/
|
|
416
|
+
export function registerErrorsListCommand(program: Command): Command {
|
|
417
|
+
const errorsCmd = program
|
|
418
|
+
.command('errors', { hidden: true })
|
|
419
|
+
.description('PD pipeline error inspection (failed tasks + worker errors)');
|
|
420
|
+
|
|
421
|
+
errorsCmd
|
|
422
|
+
.command('list')
|
|
423
|
+
.description('List PD pipeline errors — failed tasks + worker-status.json errors')
|
|
424
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
425
|
+
.option('--json', 'Output raw JSON (cli-1-strict-json: stdout is exactly one JSON object)')
|
|
426
|
+
.option('--kind <runnerKind>', 'Filter failed tasks by task_kind (e.g. diagnostician, dreamer)')
|
|
427
|
+
.option('--since <hours>', 'Only show errors from the last N hours (non-negative number)', parseInt)
|
|
428
|
+
.option('--limit <n>', 'Max failed tasks to return (default: 50, max: 200)', parseInt)
|
|
429
|
+
.action(async (opts) => {
|
|
430
|
+
await handleErrorsList({
|
|
431
|
+
workspace: opts.workspace,
|
|
432
|
+
json: opts.json === true,
|
|
433
|
+
kind: opts.kind,
|
|
434
|
+
since: opts.since,
|
|
435
|
+
limit: opts.limit,
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
return errorsCmd;
|
|
440
|
+
}
|