@principles/pd-cli 1.147.10 → 1.147.12

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.
@@ -1,5 +1,10 @@
1
1
  import * as path from 'path';
2
- import { createRecoverySweepService, type RecoverySweepServiceHandle } from '@principles/core/runtime-v2';
2
+ import {
3
+ createRecoverySweepService,
4
+ isPeerRunnerKind,
5
+ isDiagnosticianStageKind,
6
+ type RecoverySweepServiceHandle,
7
+ } from '@principles/core/runtime-v2';
3
8
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
4
9
 
5
10
  interface RecoveryFailedTasksOptions {
@@ -21,6 +26,50 @@ interface TaskDetail {
21
26
  nextAction: string;
22
27
  }
23
28
 
29
+ /** Stage-task ID convention prefix → the remainder is the parent task ID. */
30
+ const DIAG_STAGE_ID_PREFIXES = ['diag_rootcause-', 'diag_distiller-', 'diag_router-'] as const;
31
+
32
+ /**
33
+ * PRI-674 review P2: per-kind execution guidance must point at commands that
34
+ * actually support the recovered task kind (cli-6-output-next-action).
35
+ *
36
+ * Verified against real command surfaces:
37
+ * - `pd runtime internalization run-once` supports ONLY the 6 peer runner
38
+ * kinds (SUPPORTED_RUNNERS in runtime-internalization-run-once.ts).
39
+ * - `pd diagnose run --task-id <id>` is the only diagnostician execution
40
+ * entry (SplitDiagnosticianRunner runs the full A→B→C pipeline from the
41
+ * PARENT task; running a diag_* stage as parent would create bogus nested
42
+ * stage tasks).
43
+ * - diag_* stage linkage: persisted inputRef = parent task ID
44
+ * (SplitDiagnosticianRunner.ensureSubTask), with the
45
+ * diag_<stage>-<parentTaskId> ID convention as fallback. When the parent
46
+ * cannot be resolved we say so instead of printing a guessed command.
47
+ */
48
+ function buildExecutionNextAction(t: { taskId: string; taskKind: string; inputRef: string | null }): string {
49
+ // Branch on the authoritative taskKind, never on taskId shape.
50
+ if (isDiagnosticianStageKind(t.taskKind)) {
51
+ // Parent from persisted inputRef; fallback: diag_<stage>-<parentTaskId>
52
+ // ID convention (plain prefix split — no regex needed for fixed literals).
53
+ const matchedPrefix = DIAG_STAGE_ID_PREFIXES.find((p) => t.taskId.startsWith(p));
54
+ const parentTaskId = t.inputRef && t.inputRef.trim() !== ''
55
+ ? t.inputRef
56
+ : matchedPrefix
57
+ ? t.taskId.slice(matchedPrefix.length).trim() || undefined
58
+ : undefined;
59
+ if (parentTaskId) {
60
+ return `Stage task of diagnostician parent "${parentTaskId}". Recover the parent, then run: pd diagnose run --task-id ${parentTaskId} (runs the full pipeline including this stage)`;
61
+ }
62
+ return `Stage task whose diagnostician parent could not be resolved (no inputRef linkage, task ID does not follow diag_<stage>-<parentTaskId>). Inspect: pd diagnose status --task-id ${t.taskId} — do not run a stage task directly as a parent`;
63
+ }
64
+ if (t.taskKind === 'diagnostician') {
65
+ return `Run: pd diagnose run --task-id ${t.taskId} (executes the full diag_rootcause→diag_distiller→diag_router pipeline)`;
66
+ }
67
+ if (isPeerRunnerKind(t.taskKind)) {
68
+ return `Run: pd runtime internalization run-once --runner ${t.taskKind} (executes the next ready ${t.taskKind} task)`;
69
+ }
70
+ return `Task kind ${t.taskKind} has no registered CLI execution entry. Inspect: pd diagnose status --task-id ${t.taskId}, or use the Console failed-tasks page`;
71
+ }
72
+
24
73
  export async function handleRuntimeRecoveryFailedTasks(opts: RecoveryFailedTasksOptions): Promise<void> {
25
74
  if (opts.dryRun && opts.confirm) {
26
75
  if (opts.json) {
@@ -78,7 +127,7 @@ export async function handleRuntimeRecoveryFailedTasks(opts: RecoveryFailedTasks
78
127
  reason: t.isExhausted
79
128
  ? `Task exhausted max attempts (${t.attemptCount}/${t.maxAttempts}) but --force specified — reset to pending`
80
129
  : `Task failed and attempts remain (${t.attemptCount}/${t.maxAttempts}) — reset to pending`,
81
- nextAction: 'Task recovered to pending. Run pd runtime internalization run-once to execute.',
130
+ nextAction: `Task recovered to pending. ${buildExecutionNextAction({ taskId: t.taskId, taskKind: t.taskKind, inputRef: t.inputRef })}`,
82
131
  });
83
132
  } else {
84
133
  skippedCount++;
@@ -117,7 +166,7 @@ export async function handleRuntimeRecoveryFailedTasks(opts: RecoveryFailedTasks
117
166
 
118
167
  if (isDryRun) {
119
168
  if (taskDetails.length === 0) {
120
- summaryReason = 'No failed internalization tasks found';
169
+ summaryReason = 'No failed internalization or diagnostician tasks found';
121
170
  summaryNextAction = 'Nothing to recover';
122
171
  } else {
123
172
  summaryReason = `Found ${recoveredCount} recoverable and ${skippedCount} exhausted failed tasks`;
@@ -125,9 +174,37 @@ export async function handleRuntimeRecoveryFailedTasks(opts: RecoveryFailedTasks
125
174
  }
126
175
  } else {
127
176
  summaryReason = `Successfully recovered ${recoveredCount} failed tasks, skipped ${skippedCount} tasks`;
128
- summaryNextAction = recoveredCount > 0
129
- ? 'Run pd runtime internalization run-once to execute recovered tasks'
130
- : 'No tasks recovered';
177
+ // PRI-674 review P2: mixed recoveries span different executors — split
178
+ // the summary by actual command surface instead of one command for all.
179
+ if (recoveredCount > 0) {
180
+ const diagParents: string[] = [];
181
+ const diagStages: string[] = [];
182
+ const peerTasks: string[] = [];
183
+ const otherTasks: string[] = [];
184
+ for (const t of taskDetails) {
185
+ if (t.action !== 'recovered') continue;
186
+ if (t.taskKind === 'diagnostician') diagParents.push(t.taskId);
187
+ else if (isDiagnosticianStageKind(t.taskKind)) diagStages.push(t.taskId);
188
+ else if (isPeerRunnerKind(t.taskKind)) peerTasks.push(t.taskId);
189
+ else otherTasks.push(t.taskId);
190
+ }
191
+ const lines: string[] = [];
192
+ if (diagParents.length > 0) {
193
+ lines.push(`For diagnostician parent task(s) (${diagParents.join(', ')}): pd diagnose run --task-id <taskId>`);
194
+ }
195
+ if (diagStages.length > 0) {
196
+ lines.push(`For diag_* stage task(s) (${diagStages.join(', ')}): recover the parent diagnostician task, then pd diagnose run --task-id <parentTaskId> (each stage's parent is in its per-task nextAction)`);
197
+ }
198
+ if (peerTasks.length > 0) {
199
+ lines.push(`For internalization task(s) (${peerTasks.join(', ')}): pd runtime internalization run-once --runner <kind>`);
200
+ }
201
+ if (otherTasks.length > 0) {
202
+ lines.push(`For other task(s) (${otherTasks.join(', ')}): no registered CLI execution entry — see per-task nextAction`);
203
+ }
204
+ summaryNextAction = `Tasks recovered to pending. Execute by kind: ${lines.join('; ')}`;
205
+ } else {
206
+ summaryNextAction = 'No tasks recovered';
207
+ }
131
208
  }
132
209
 
133
210
  if (opts.json) {
package/src/index.ts CHANGED
@@ -870,9 +870,12 @@ const diagnosticsCmd = runtimeCmd
870
870
  .command('diagnostics', { hidden: true })
871
871
  .description('Control plane diagnostic bundle operations');
872
872
 
873
+ // PRI-674: visible (was hidden) — the only recovery re-entry point for an
874
+ // Owner without Console access; hidden-ness left failed diagnostician tasks
875
+ // undiscoverable.
873
876
  const recoveryCmd = runtimeCmd
874
- .command('recovery', { hidden: true })
875
- .description('Runtime V2 lease recovery operations');
877
+ .command('recovery')
878
+ .description('Runtime V2 recovery operations (expired-lease sweep, failed-task recovery)');
876
879
 
877
880
  // PRI-555 phase 1: dry-run-only artifact identity drift repair planner.
878
881
  runtimeCmd
@@ -907,7 +910,7 @@ recoveryCmd
907
910
 
908
911
  recoveryCmd
909
912
  .command('failed-tasks')
910
- .description('Recover failed internalization tasks')
913
+ .description('Recover failed internalization and diagnostician tasks')
911
914
  .option('-w, --workspace <path>', 'Workspace directory')
912
915
  .option('--dry-run', 'Report only, no modifications (default)')
913
916
  .option('--confirm', 'Actually recover failed tasks')
@@ -81,12 +81,18 @@ describe('PRI-455: pd runtime --help shows only MVP owner subcommands', () => {
81
81
  expect(runtimeHelp).toMatch(/\bfeatures\b/);
82
82
  });
83
83
 
84
+ // PRI-674: recovery is now owner-facing — it is the only failed-task
85
+ // recovery re-entry point for an Owner without Console access, so hiding
86
+ // it made failed diagnostician tasks undiscoverable.
87
+ it('runtime --help shows recovery', () => {
88
+ expect(runtimeHelp).toMatch(/^\s+recovery\b/m);
89
+ });
90
+
84
91
  // Operator subcommands that should be hidden
85
92
  const HIDDEN_RUNTIME_SUBCOMMANDS = [
86
93
  'canary',
87
94
  'synthetic',
88
95
  'uat',
89
- 'recovery',
90
96
  'pruning',
91
97
  'diagnostics',
92
98
  'probe',