@principles/pd-cli 1.133.0 → 1.135.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/dist/commands/__tests__/intent-flag-wiring.test.js +74 -3
  2. package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
  3. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +2 -0
  4. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +1 -0
  5. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +60 -0
  6. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +1 -0
  7. package/dist/commands/__tests__/rulecode-flag-wiring.test.js +11 -4
  8. package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +1 -1
  9. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +9 -0
  10. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -1
  11. package/dist/commands/admission-gate.d.ts +24 -0
  12. package/dist/commands/admission-gate.d.ts.map +1 -0
  13. package/dist/commands/admission-gate.js +29 -0
  14. package/dist/commands/admission-gate.js.map +1 -0
  15. package/dist/commands/candidate.d.ts.map +1 -1
  16. package/dist/commands/candidate.js +72 -6
  17. package/dist/commands/candidate.js.map +1 -1
  18. package/dist/commands/console.d.ts.map +1 -1
  19. package/dist/commands/console.js +42 -0
  20. package/dist/commands/console.js.map +1 -1
  21. package/dist/commands/demo-story-a.d.ts.map +1 -1
  22. package/dist/commands/demo-story-a.js +4 -2
  23. package/dist/commands/demo-story-a.js.map +1 -1
  24. package/dist/commands/diagnose.d.ts.map +1 -1
  25. package/dist/commands/diagnose.js +155 -8
  26. package/dist/commands/diagnose.js.map +1 -1
  27. package/dist/commands/errors-list.d.ts +25 -0
  28. package/dist/commands/errors-list.d.ts.map +1 -0
  29. package/dist/commands/errors-list.js +368 -0
  30. package/dist/commands/errors-list.js.map +1 -0
  31. package/dist/commands/health-codex.d.ts +7 -0
  32. package/dist/commands/health-codex.d.ts.map +1 -0
  33. package/dist/commands/health-codex.js +209 -0
  34. package/dist/commands/health-codex.js.map +1 -0
  35. package/dist/commands/health.d.ts +24 -0
  36. package/dist/commands/health.d.ts.map +1 -1
  37. package/dist/commands/health.js +48 -0
  38. package/dist/commands/health.js.map +1 -1
  39. package/dist/commands/intent.d.ts +21 -4
  40. package/dist/commands/intent.d.ts.map +1 -1
  41. package/dist/commands/intent.js +88 -27
  42. package/dist/commands/intent.js.map +1 -1
  43. package/dist/commands/pain-evidence.d.ts +6 -0
  44. package/dist/commands/pain-evidence.d.ts.map +1 -1
  45. package/dist/commands/pain-evidence.js +6 -0
  46. package/dist/commands/pain-evidence.js.map +1 -1
  47. package/dist/commands/pain-record.d.ts.map +1 -1
  48. package/dist/commands/pain-record.js +21 -0
  49. package/dist/commands/pain-record.js.map +1 -1
  50. package/dist/commands/pain-retry.d.ts +4 -0
  51. package/dist/commands/pain-retry.d.ts.map +1 -1
  52. package/dist/commands/pain-retry.js +327 -32
  53. package/dist/commands/pain-retry.js.map +1 -1
  54. package/dist/commands/proven-channel-baseline.d.ts.map +1 -1
  55. package/dist/commands/proven-channel-baseline.js +4 -2
  56. package/dist/commands/proven-channel-baseline.js.map +1 -1
  57. package/dist/commands/runtime-activation.d.ts +30 -0
  58. package/dist/commands/runtime-activation.d.ts.map +1 -1
  59. package/dist/commands/runtime-activation.js +567 -11
  60. package/dist/commands/runtime-activation.js.map +1 -1
  61. package/dist/commands/runtime-canary.d.ts.map +1 -1
  62. package/dist/commands/runtime-canary.js +32 -7
  63. package/dist/commands/runtime-canary.js.map +1 -1
  64. package/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
  65. package/dist/commands/runtime-health-snapshot.js +4 -2
  66. package/dist/commands/runtime-health-snapshot.js.map +1 -1
  67. package/dist/commands/runtime-init.d.ts +43 -0
  68. package/dist/commands/runtime-init.d.ts.map +1 -0
  69. package/dist/commands/runtime-init.js +264 -0
  70. package/dist/commands/runtime-init.js.map +1 -0
  71. package/dist/commands/runtime-internalization-context-trace.d.ts +25 -0
  72. package/dist/commands/runtime-internalization-context-trace.d.ts.map +1 -0
  73. package/dist/commands/runtime-internalization-context-trace.js +374 -0
  74. package/dist/commands/runtime-internalization-context-trace.js.map +1 -0
  75. package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
  76. package/dist/commands/runtime-internalization-enqueue-successors.js +3 -1
  77. package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
  78. package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
  79. package/dist/commands/runtime-internalization-integrity.js +4 -2
  80. package/dist/commands/runtime-internalization-integrity.js.map +1 -1
  81. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  82. package/dist/commands/runtime-internalization-run-once.js +20 -5
  83. package/dist/commands/runtime-internalization-run-once.js.map +1 -1
  84. package/dist/commands/runtime-internalization-run-rulehost.d.ts +1 -0
  85. package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
  86. package/dist/commands/runtime-internalization-run-rulehost.js +149 -6
  87. package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
  88. package/dist/commands/runtime-uat.d.ts.map +1 -1
  89. package/dist/commands/runtime-uat.guard.test.js +13 -15
  90. package/dist/commands/runtime-uat.guard.test.js.map +1 -1
  91. package/dist/commands/runtime-uat.js +4 -3
  92. package/dist/commands/runtime-uat.js.map +1 -1
  93. package/dist/config-reader.js +1 -1
  94. package/dist/config-reader.js.map +1 -1
  95. package/dist/index.js +54 -78
  96. package/dist/index.js.map +1 -1
  97. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +2 -0
  98. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +1 -0
  99. package/dist/services/__tests__/evaluator-runner-deps.test.js +304 -0
  100. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -0
  101. package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
  102. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
  103. package/dist/services/config-doctor.d.ts.map +1 -1
  104. package/dist/services/config-doctor.js +9 -2
  105. package/dist/services/config-doctor.js.map +1 -1
  106. package/dist/services/console-launcher.d.ts +6 -0
  107. package/dist/services/console-launcher.d.ts.map +1 -1
  108. package/dist/services/console-launcher.js.map +1 -1
  109. package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
  110. package/dist/services/pain-flood-simulation-runner.js +9 -0
  111. package/dist/services/pain-flood-simulation-runner.js.map +1 -1
  112. package/dist/services/pd-config-loader.js +2 -2
  113. package/dist/services/pd-config-loader.js.map +1 -1
  114. package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
  115. package/dist/services/proven-channel-baseline-runner.js +10 -2
  116. package/dist/services/proven-channel-baseline-runner.js.map +1 -1
  117. package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
  118. package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
  119. package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
  120. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  121. package/dist/services/rulehost-pipeline-runner.js +111 -7
  122. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  123. package/dist/services/rulehost-readiness.js.map +1 -1
  124. package/dist/services/runtime-adapter-resolver.d.ts +1 -0
  125. package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
  126. package/dist/services/runtime-adapter-resolver.js +3 -0
  127. package/dist/services/runtime-adapter-resolver.js.map +1 -1
  128. package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
  129. package/dist/services/synthetic-baseline-runner.js +11 -0
  130. package/dist/services/synthetic-baseline-runner.js.map +1 -1
  131. package/dist/utils/production-workspace-guard.d.ts +4 -4
  132. package/dist/utils/production-workspace-guard.d.ts.map +1 -1
  133. package/dist/utils/production-workspace-guard.js +19 -16
  134. package/dist/utils/production-workspace-guard.js.map +1 -1
  135. package/dist/utils/production-workspace-guard.test.js +54 -19
  136. package/dist/utils/production-workspace-guard.test.js.map +1 -1
  137. package/package.json +8 -5
  138. package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
  139. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
  140. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
  141. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
  142. package/src/commands/admission-gate.ts +30 -0
  143. package/src/commands/candidate.ts +74 -6
  144. package/src/commands/console.ts +41 -0
  145. package/src/commands/demo-story-a.ts +4 -2
  146. package/src/commands/diagnose.ts +158 -10
  147. package/src/commands/errors-list.ts +440 -0
  148. package/src/commands/health-codex.ts +249 -0
  149. package/src/commands/health.ts +60 -0
  150. package/src/commands/intent.ts +98 -29
  151. package/src/commands/pain-evidence.ts +6 -0
  152. package/src/commands/pain-record.ts +21 -0
  153. package/src/commands/pain-retry.ts +353 -35
  154. package/src/commands/proven-channel-baseline.ts +4 -2
  155. package/src/commands/runtime-activation.ts +657 -12
  156. package/src/commands/runtime-canary.ts +31 -5
  157. package/src/commands/runtime-health-snapshot.ts +4 -2
  158. package/src/commands/runtime-init.ts +305 -0
  159. package/src/commands/runtime-internalization-context-trace.ts +445 -0
  160. package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
  161. package/src/commands/runtime-internalization-integrity.ts +4 -2
  162. package/src/commands/runtime-internalization-run-once.ts +20 -5
  163. package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
  164. package/src/commands/runtime-uat.guard.test.ts +14 -16
  165. package/src/commands/runtime-uat.ts +4 -3
  166. package/src/config-reader.ts +1 -1
  167. package/src/index.ts +69 -78
  168. package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
  169. package/src/services/config-doctor.ts +9 -2
  170. package/src/services/console-launcher.ts +6 -0
  171. package/src/services/pain-flood-simulation-runner.ts +9 -0
  172. package/src/services/pd-config-loader.ts +2 -2
  173. package/src/services/proven-channel-baseline-runner.ts +10 -3
  174. package/src/services/rulehost-pipeline-runner.ts +157 -6
  175. package/src/services/runtime-adapter-resolver.ts +4 -0
  176. package/src/services/synthetic-baseline-runner.ts +11 -0
  177. package/src/utils/production-workspace-guard.test.ts +62 -20
  178. package/src/utils/production-workspace-guard.ts +19 -16
  179. package/tests/bdd/cli-contract.steps.ts +343 -0
  180. package/tests/bdd/context-trace.steps.ts +229 -0
  181. package/tests/bdd/support/gherkin-loader.ts +147 -0
  182. package/tests/bdd/support/repo-root.ts +62 -0
  183. package/tests/bdd/support/vitest-bdd.ts +179 -0
  184. package/tests/commands/admission-gate-coverage.test.ts +79 -0
  185. package/tests/commands/candidate-audit-repair.test.ts +77 -0
  186. package/tests/commands/candidate-intake.test.ts +155 -10
  187. package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
  188. package/tests/commands/cli-help-snapshot.test.ts +15 -1
  189. package/tests/commands/console-open.test.ts +229 -32
  190. package/tests/commands/diagnose.test.ts +619 -14
  191. package/tests/commands/errors-list.test.ts +678 -0
  192. package/tests/commands/health-codex.test.ts +243 -0
  193. package/tests/commands/health-host-wiring.test.ts +76 -0
  194. package/tests/commands/intent.test.ts +4 -4
  195. package/tests/commands/pain-record.test.ts +80 -0
  196. package/tests/commands/pain-retry.test.ts +94 -3
  197. package/tests/commands/run-rulehost-handler.test.ts +133 -0
  198. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
  199. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
  200. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
  201. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
  202. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
  203. package/tests/commands/runtime-activation.test.ts +1090 -5
  204. package/tests/commands/runtime-canary.test.ts +50 -0
  205. package/tests/commands/runtime-features.test.ts +2 -0
  206. package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
  207. package/tests/commands/runtime-init.test.ts +346 -0
  208. package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
  209. package/tests/commands/runtime-internalization.test.ts +2 -2
  210. package/tests/config-reader.test.ts +6 -5
  211. package/tests/e2e/cross-package-acceptance.test.ts +55 -13
  212. package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
  213. package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
  214. package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
  215. package/vitest.config.ts +1 -1
@@ -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
- const runtimeKind = opts.runtime ?? 'test-double';
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
- const internalizeNextAction = candidateIds.length > 0
492
- ? `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 ')}`
493
- : 'No candidates were generated from this diagnosis.';
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
+ }