@principles/pd-cli 1.132.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.
Files changed (212) 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 +40 -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/pain-flood-simulation-runner.d.ts.map +1 -1
  107. package/dist/services/pain-flood-simulation-runner.js +9 -0
  108. package/dist/services/pain-flood-simulation-runner.js.map +1 -1
  109. package/dist/services/pd-config-loader.js +2 -2
  110. package/dist/services/pd-config-loader.js.map +1 -1
  111. package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
  112. package/dist/services/proven-channel-baseline-runner.js +10 -2
  113. package/dist/services/proven-channel-baseline-runner.js.map +1 -1
  114. package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
  115. package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
  116. package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
  117. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  118. package/dist/services/rulehost-pipeline-runner.js +111 -7
  119. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  120. package/dist/services/rulehost-readiness.js.map +1 -1
  121. package/dist/services/runtime-adapter-resolver.d.ts +1 -0
  122. package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
  123. package/dist/services/runtime-adapter-resolver.js +3 -0
  124. package/dist/services/runtime-adapter-resolver.js.map +1 -1
  125. package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
  126. package/dist/services/synthetic-baseline-runner.js +11 -0
  127. package/dist/services/synthetic-baseline-runner.js.map +1 -1
  128. package/dist/utils/production-workspace-guard.d.ts +4 -4
  129. package/dist/utils/production-workspace-guard.d.ts.map +1 -1
  130. package/dist/utils/production-workspace-guard.js +19 -16
  131. package/dist/utils/production-workspace-guard.js.map +1 -1
  132. package/dist/utils/production-workspace-guard.test.js +54 -19
  133. package/dist/utils/production-workspace-guard.test.js.map +1 -1
  134. package/package.json +8 -5
  135. package/scripts/migrate-illegal-expected-decision.ts +252 -0
  136. package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
  137. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
  138. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
  139. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
  140. package/src/commands/admission-gate.ts +30 -0
  141. package/src/commands/candidate.ts +74 -6
  142. package/src/commands/console.ts +40 -0
  143. package/src/commands/demo-story-a.ts +4 -2
  144. package/src/commands/diagnose.ts +158 -10
  145. package/src/commands/errors-list.ts +440 -0
  146. package/src/commands/health-codex.ts +249 -0
  147. package/src/commands/health.ts +60 -0
  148. package/src/commands/intent.ts +98 -29
  149. package/src/commands/pain-evidence.ts +6 -0
  150. package/src/commands/pain-record.ts +21 -0
  151. package/src/commands/pain-retry.ts +353 -35
  152. package/src/commands/proven-channel-baseline.ts +4 -2
  153. package/src/commands/runtime-activation.ts +657 -12
  154. package/src/commands/runtime-canary.ts +31 -5
  155. package/src/commands/runtime-health-snapshot.ts +4 -2
  156. package/src/commands/runtime-init.ts +305 -0
  157. package/src/commands/runtime-internalization-context-trace.ts +445 -0
  158. package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
  159. package/src/commands/runtime-internalization-integrity.ts +4 -2
  160. package/src/commands/runtime-internalization-run-once.ts +20 -5
  161. package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
  162. package/src/commands/runtime-uat.guard.test.ts +14 -16
  163. package/src/commands/runtime-uat.ts +4 -3
  164. package/src/config-reader.ts +1 -1
  165. package/src/index.ts +69 -78
  166. package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
  167. package/src/services/config-doctor.ts +9 -2
  168. package/src/services/pain-flood-simulation-runner.ts +9 -0
  169. package/src/services/pd-config-loader.ts +2 -2
  170. package/src/services/proven-channel-baseline-runner.ts +10 -3
  171. package/src/services/rulehost-pipeline-runner.ts +157 -6
  172. package/src/services/runtime-adapter-resolver.ts +4 -0
  173. package/src/services/synthetic-baseline-runner.ts +11 -0
  174. package/src/utils/production-workspace-guard.test.ts +62 -20
  175. package/src/utils/production-workspace-guard.ts +19 -16
  176. package/tests/bdd/cli-contract.steps.ts +343 -0
  177. package/tests/bdd/context-trace.steps.ts +229 -0
  178. package/tests/bdd/support/gherkin-loader.ts +147 -0
  179. package/tests/bdd/support/repo-root.ts +62 -0
  180. package/tests/bdd/support/vitest-bdd.ts +179 -0
  181. package/tests/commands/admission-gate-coverage.test.ts +79 -0
  182. package/tests/commands/candidate-audit-repair.test.ts +77 -0
  183. package/tests/commands/candidate-intake.test.ts +155 -10
  184. package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
  185. package/tests/commands/cli-help-snapshot.test.ts +15 -1
  186. package/tests/commands/console-open.test.ts +19 -0
  187. package/tests/commands/diagnose.test.ts +619 -14
  188. package/tests/commands/errors-list.test.ts +678 -0
  189. package/tests/commands/health-codex.test.ts +243 -0
  190. package/tests/commands/health-host-wiring.test.ts +76 -0
  191. package/tests/commands/intent.test.ts +4 -4
  192. package/tests/commands/pain-record.test.ts +80 -0
  193. package/tests/commands/pain-retry.test.ts +94 -3
  194. package/tests/commands/run-rulehost-handler.test.ts +133 -0
  195. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
  196. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
  197. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
  198. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
  199. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
  200. package/tests/commands/runtime-activation.test.ts +1090 -5
  201. package/tests/commands/runtime-canary.test.ts +50 -0
  202. package/tests/commands/runtime-features.test.ts +2 -0
  203. package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
  204. package/tests/commands/runtime-init.test.ts +346 -0
  205. package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
  206. package/tests/commands/runtime-internalization.test.ts +2 -2
  207. package/tests/config-reader.test.ts +6 -5
  208. package/tests/e2e/cross-package-acceptance.test.ts +55 -13
  209. package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
  210. package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
  211. package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
  212. package/vitest.config.ts +1 -1
@@ -1,4 +1,5 @@
1
1
  import * as path from 'path';
2
+ import type { Command } from 'commander';
2
3
  import {
3
4
  RuntimeStateManager,
4
5
  ActivationDispatcher,
@@ -9,11 +10,30 @@ import {
9
10
  SqliteActivationStateStore,
10
11
  SqliteApprovalQueueStore,
11
12
  SqlitePIArtifactStore,
13
+ ApprovalQueue,
14
+ ApprovalCompletionService,
12
15
  isArtifactRevisionOf,
16
+ extractEvidenceRefs,
17
+ extractPrincipleId,
13
18
  } from '@principles/core/runtime-v2';
14
- import type { ActivationDecision, PIArtifactSnapshot, RolloutActivationDecision } from '@principles/core/runtime-v2';
15
- import type { PIArtifactRecord } from '@principles/core/runtime-v2';
19
+ import type {
20
+ ActivationDecision,
21
+ PIArtifactSnapshot,
22
+ RolloutActivationDecision,
23
+ ApprovalDecisionResult,
24
+ ApprovalCompletionResult,
25
+ } from '@principles/core/runtime-v2';
26
+ import type { PIArtifactRecord, ActivationStatusRecord } from '@principles/core/runtime-v2';
16
27
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
28
+ import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
29
+
30
+ /**
31
+ * Type guard for parsed JSON objects (rc-2-no-as-bypass).
32
+ * Replaces `as Record<string, unknown>` casts on untrusted contentJson.
33
+ */
34
+ function isRecord(value: unknown): value is Record<string, unknown> {
35
+ return value !== null && value !== undefined && typeof value === 'object' && !Array.isArray(value);
36
+ }
17
37
 
18
38
  interface ActivationDispatchOptions {
19
39
  workspace?: string;
@@ -150,6 +170,7 @@ export async function handleRuntimeActivationDispatch(opts: ActivationDispatchOp
150
170
 
151
171
  const activationStateStore = new SqliteActivationStateStore(stateManager.connection);
152
172
  const approvalQueueStore = new SqliteApprovalQueueStore(stateManager.connection);
173
+ const featureFlags = computeFlagsFromLoadResult(loadPdConfig(workspaceDir));
153
174
  // Wire all three MVP-Core writers, including RuleHostWriter for code_tool_hook.
154
175
  // PRI-408: fixes P0 breakpoint where code_tool_hook channel could not activate.
155
176
  const dispatcher = new ActivationDispatcher(
@@ -158,7 +179,10 @@ export async function handleRuntimeActivationDispatch(opts: ActivationDispatchOp
158
179
  {
159
180
  writers: [
160
181
  new PromptWriter(),
161
- new RuleHostWriter({ gateDeps: createProductionGateDeps() }),
182
+ new RuleHostWriter({
183
+ gateDeps: createProductionGateDeps(),
184
+ featureFlagProbe: (flagId) => featureFlags.flags[flagId]?.enabled === true,
185
+ }),
162
186
  new DeferArchiveWriter(),
163
187
  ],
164
188
  approvalQueueStore,
@@ -210,7 +234,7 @@ export async function handleRuntimeActivationDeactivate(opts: ActivationDeactiva
210
234
  ok: false,
211
235
  activationId: '',
212
236
  reason: 'activation_id_required',
213
- nextAction: 'Provide --activation-id <id> from `pd runtime activation list`',
237
+ nextAction: 'Provide --activation-id <id> from `pd activation list`',
214
238
  };
215
239
  if (opts.json) {
216
240
  console.log(JSON.stringify(result, null, 2));
@@ -240,7 +264,7 @@ export async function handleRuntimeActivationDeactivate(opts: ActivationDeactiva
240
264
  ok: false,
241
265
  activationId: opts.activationId,
242
266
  reason: 'not_found_or_already_deactivated',
243
- nextAction: 'Check activation ID with `pd runtime activation list`, or it may already be deactivated',
267
+ nextAction: 'Check activation ID with `pd activation list`, or it may already be deactivated',
244
268
  };
245
269
 
246
270
  if (opts.json) {
@@ -281,6 +305,114 @@ export async function handleRuntimeActivationDeactivate(opts: ActivationDeactiva
281
305
  }
282
306
  }
283
307
 
308
+ export interface ActivationPromoteOptions {
309
+ workspace?: string;
310
+ activationId?: string;
311
+ dryRun?: boolean;
312
+ confirm?: boolean;
313
+ json?: boolean;
314
+ }
315
+
316
+ export interface ActivationPromoteResult {
317
+ ok: boolean;
318
+ decision: 'would_promote' | 'promoted' | 'refused';
319
+ activationId: string;
320
+ promotedAt?: string;
321
+ reason?: string;
322
+ nextAction?: string;
323
+ }
324
+
325
+ export async function handleRuntimeActivationPromote(opts: ActivationPromoteOptions): Promise<void> {
326
+ const activationId = opts.activationId?.trim() ?? '';
327
+ const refuse = (reason: string, nextAction: string): void => {
328
+ const result: ActivationPromoteResult = { ok: false, decision: 'refused', activationId, reason, nextAction };
329
+ if (opts.json) console.log(JSON.stringify(result));
330
+ else {
331
+ console.error(`Promotion refused: ${reason}`);
332
+ console.error(`Next action: ${nextAction}`);
333
+ }
334
+ process.exitCode = 1;
335
+ };
336
+
337
+ if (!activationId) {
338
+ refuse('activation_id_required', 'Provide --activation-id from `pd activation list --channel code_tool_hook`.');
339
+ return;
340
+ }
341
+ if (opts.dryRun === true && opts.confirm === true) {
342
+ refuse('dry_run_confirm_mutually_exclusive', 'Choose either --dry-run or --confirm.');
343
+ return;
344
+ }
345
+
346
+ let stateManager: RuntimeStateManager | undefined;
347
+ try {
348
+ const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
349
+ stateManager = new RuntimeStateManager({ workspaceDir });
350
+ await stateManager.initialize();
351
+ const store = new SqliteActivationStateStore(stateManager.connection);
352
+ const activeHooks = await store.listCodeToolHookActivations(false);
353
+ // CodeRabbit PR2 Comment 3: dry-run must apply the same eligibility checks
354
+ // as the real promote path. `promoteActivation` runs a COUNT guard inside a
355
+ // BEGIN IMMEDIATE transaction and refuses when the count of matching shadow
356
+ // rows is not exactly 1. The previous dry-run branch used `find()` (returns
357
+ // the first match) and reported `would_promote` even when duplicates would
358
+ // make the confirm path throw. Mirror the store's uniqueness check here so
359
+ // dry-run and confirm agree (cli-5: failure paths must not mutate state;
360
+ // cli-6: degraded/refused results carry a structured reason + nextAction).
361
+ const matchingShadows = activeHooks.filter(
362
+ (record) => record.activationId === activationId && record.action === 'code_tool_hook_shadow_activate',
363
+ );
364
+ if (matchingShadows.length === 0) {
365
+ refuse(
366
+ 'not_found_inactive_or_not_shadow',
367
+ 'Refresh `pd activation list --channel code_tool_hook`; only active shadow activations can be promoted.',
368
+ );
369
+ return;
370
+ }
371
+ if (matchingShadows.length > 1) {
372
+ refuse(
373
+ 'duplicate_shadow_activations',
374
+ `${matchingShadows.length} shadow activations share activation_id=${activationId}; resolve duplicates before promoting.`,
375
+ );
376
+ return;
377
+ }
378
+
379
+ if (opts.confirm !== true) {
380
+ const result: ActivationPromoteResult = {
381
+ ok: true,
382
+ decision: 'would_promote',
383
+ activationId,
384
+ nextAction: `Run pd activation promote --activation-id ${activationId} --confirm to enable live blocking.`,
385
+ };
386
+ if (opts.json) console.log(JSON.stringify(result));
387
+ else {
388
+ console.log(`Would promote: ${activationId}`);
389
+ console.log(` nextAction: ${result.nextAction}`);
390
+ }
391
+ return;
392
+ }
393
+
394
+ const promotedAt = new Date().toISOString();
395
+ const promoted = await store.promoteActivation(activationId, promotedAt);
396
+ if (!promoted) {
397
+ refuse('promotion_precondition_changed', 'Refresh the activation list; the activation changed before promotion.');
398
+ return;
399
+ }
400
+ const result: ActivationPromoteResult = { ok: true, decision: 'promoted', activationId, promotedAt };
401
+ if (opts.json) console.log(JSON.stringify(result));
402
+ else {
403
+ console.log(`Promoted live: ${activationId}`);
404
+ console.log(` promotedAt: ${promotedAt}`);
405
+ }
406
+ } catch (err: unknown) {
407
+ refuse(
408
+ `promotion_failed: ${err instanceof Error ? err.message : String(err)}`,
409
+ 'Check workspace configuration and database integrity; no promotion was applied.',
410
+ );
411
+ } finally {
412
+ await stateManager?.close();
413
+ }
414
+ }
415
+
284
416
  // ── List Activations Command (PRI-408 Contract D — observability) ────────────
285
417
 
286
418
  interface ActivationListOptions {
@@ -335,23 +467,180 @@ export async function handleRuntimeActivationList(opts: ActivationListOptions):
335
467
  ? records
336
468
  : records.filter(r => r.deactivatedAt === null);
337
469
 
470
+ // F9-1: Validate artifact_id → pi_artifacts.artifact_id reference for each
471
+ // activation. A dangling reference means the activation cannot function
472
+ // (RuleHost cannot load implementationCode, dispatcher cannot verify
473
+ // lineage). Emit degraded status with reason + nextAction instead of
474
+ // silently returning corrupted records (rc-9-no-silent-fallback,
475
+ // rc-6-lineage-consistency; related ERR: ERR-002).
476
+ const {piArtifactStore} = stateManager;
477
+ const uniqueArtifactIds = new Set(filtered.map(r => r.artifactId));
478
+ const danglingArtifactIds = new Set<string>();
479
+ // PRI-491: Map artifactId to { contextVersion, evidenceRefs } extracted
480
+ // from contentJson. rc-1/rc-2: contentJson is parsed as unknown and
481
+ // type-narrowed; never `as`-cast without prior typeof check.
482
+ const artifactMetadata = new Map<string, { contextVersion: 'v1' | 'v2'; evidenceRefs: string[] | null; principleId: string | null }>();
483
+ for (const artifactId of uniqueArtifactIds) {
484
+ try {
485
+ const artifact = await piArtifactStore.getArtifactById(artifactId);
486
+ if (!artifact) {
487
+ danglingArtifactIds.add(artifactId);
488
+ continue;
489
+ }
490
+ // PRI-491: Parse contentJson to extract contextVersion + evidenceRefs.
491
+ // rc-1: treat parsed JSON as unknown; rc-2: narrow with typeof before use.
492
+ let parsedContent: Record<string, unknown> | null = null;
493
+ try {
494
+ const parsed: unknown = JSON.parse(artifact.contentJson);
495
+ if (isRecord(parsed)) {
496
+ parsedContent = parsed;
497
+ }
498
+ } catch {
499
+ // Malformed contentJson - treat as no metadata (not dangling, just unreadable)
500
+ }
501
+ const requiresCtxV2 = parsedContent !== null
502
+ && Object.hasOwn(parsedContent, 'requiresContextVersion')
503
+ && parsedContent.requiresContextVersion === 2;
504
+ const contextVersion: 'v1' | 'v2' = requiresCtxV2 ? 'v2' : 'v1';
505
+ const evidenceRefs = parsedContent !== null ? extractEvidenceRefs(parsedContent) : null;
506
+ // PRI-500: extract principleId to match Console's ActivationRecord field.
507
+ // Uses extractPrincipleId (4-step fallback) so dreamer artifacts whose
508
+ // sourcePrincipleId was stripped still resolve via contentJson.
509
+ const principleId = extractPrincipleId(toSnapshot(artifact));
510
+ artifactMetadata.set(artifactId, { contextVersion, evidenceRefs, principleId });
511
+ } catch {
512
+ // Treat lookup failure as dangling - fail loud rather than silent (rc-9).
513
+ danglingArtifactIds.add(artifactId);
514
+ }
515
+ }
516
+ const hasDangling = danglingArtifactIds.size > 0;
517
+
518
+ // PRI-491: Probe rulecode_context_v2 flag to determine suspended_by_flag
519
+ // status for v2 activations. When the flag is off, v2 activations are
520
+ // suspended (not executing) even though they remain active in the DB.
521
+ const featureFlags = computeFlagsFromLoadResult(loadPdConfig(workspaceDir));
522
+ const v2FlagEnabled = featureFlags.flags.rulecode_context_v2?.enabled === true;
523
+
524
+ // PRI-491: Enriched activation record with owner-observable fields.
525
+ // mode/status/contextVersion/evidenceRefs/nextAction let the owner
526
+ // understand rule state without reading SQLite or logs.
527
+ interface AnnotatedActivation extends ActivationStatusRecord {
528
+ principleId?: string;
529
+ mode?: 'shadow' | 'live';
530
+ status: 'active' | 'deactivated' | 'suspended_by_flag';
531
+ contextVersion?: 'v1' | 'v2';
532
+ evidenceRefs?: string[];
533
+ evidenceSummary?: string;
534
+ nextAction?: string;
535
+ warning?: string;
536
+ }
537
+ const annotated: AnnotatedActivation[] = filtered.map(r => {
538
+ // Derive mode from action (shadow_activate -> shadow, live_activate -> live)
539
+ const mode: 'shadow' | 'live' | undefined = r.action === 'code_tool_hook_shadow_activate'
540
+ ? 'shadow'
541
+ : r.action === 'code_tool_hook_live_activate'
542
+ ? 'live'
543
+ : undefined;
544
+
545
+ // Look up artifact metadata (contextVersion, evidenceRefs)
546
+ const meta = artifactMetadata.get(r.artifactId);
547
+ const contextVersion = meta?.contextVersion;
548
+ const evidenceRefs = meta?.evidenceRefs ?? undefined;
549
+ const evidenceSummary = evidenceRefs && evidenceRefs.length > 0
550
+ ? `${evidenceRefs.length} evidence ref(s): ${evidenceRefs.slice(0, 3).join(', ')}${evidenceRefs.length > 3 ? '...' : ''}`
551
+ : undefined;
552
+
553
+ // Derive status: deactivated > suspended_by_flag > active
554
+ let status: 'active' | 'deactivated' | 'suspended_by_flag';
555
+ let nextAction: string | undefined;
556
+ if (r.deactivatedAt) {
557
+ status = 'deactivated';
558
+ nextAction = undefined;
559
+ } else if (contextVersion === 'v2' && !v2FlagEnabled) {
560
+ status = 'suspended_by_flag';
561
+ nextAction = `Enable rulecode_context_v2 flag or deactivate: pd activation deactivate --activation-id ${r.activationId} --confirm`;
562
+ } else {
563
+ status = 'active';
564
+ if (mode === 'shadow') {
565
+ nextAction = `pd activation promote --activation-id ${r.activationId} --confirm`;
566
+ } else if (mode === 'live') {
567
+ nextAction = `pd activation deactivate --activation-id ${r.activationId} --confirm`;
568
+ } else {
569
+ nextAction = undefined;
570
+ }
571
+ }
572
+
573
+ const record: AnnotatedActivation = {
574
+ ...r,
575
+ principleId: meta?.principleId ?? 'unlinked',
576
+ mode,
577
+ status,
578
+ contextVersion,
579
+ evidenceRefs,
580
+ evidenceSummary,
581
+ nextAction,
582
+ };
583
+ if (danglingArtifactIds.has(r.artifactId)) {
584
+ record.warning = `artifact_id "${r.artifactId}" does not exist in pi_artifacts - activation is orphaned`;
585
+ }
586
+ return record;
587
+ });
588
+
338
589
  if (opts.json) {
339
- // Strict JSON mode: exactly one parseable JSON object on stdout
340
- console.log(JSON.stringify({ activations: filtered }, null, 2));
590
+ // Strict JSON mode: exactly one parseable JSON object on stdout.
591
+ // When dangling references are detected, emit degraded status with
592
+ // reason + nextAction so operators can act on the corruption (rc-9,
593
+ // cli-6-output-next-action).
594
+ const payload: Record<string, unknown> = { activations: annotated };
595
+ if (hasDangling) {
596
+ payload.status = 'degraded';
597
+ payload.reason = `${danglingArtifactIds.size} activation(s) reference non-existent artifact_id(s): ${Array.from(danglingArtifactIds).join(', ')}`;
598
+ payload.nextAction = 'Run `pd runtime internalization integrity` for full chain diagnostics. Consider deactivating orphaned activations via `pd runtime activation deactivate` or restoring the missing artifacts.';
599
+ } else {
600
+ payload.status = 'ok';
601
+ }
602
+ console.log(JSON.stringify(payload, null, 2));
341
603
  } else {
342
- if (filtered.length === 0) {
604
+ if (annotated.length === 0) {
343
605
  console.log('No active activations found.');
344
606
  } else {
345
- for (const r of filtered) {
346
- const status = r.deactivatedAt ? `[DEACTIVATED ${r.deactivatedAt}]` : '[ACTIVE]';
347
- console.log(`${status} ${r.activationId}`);
607
+ for (const r of annotated) {
608
+ // PRI-491: Text output shows mode/status/contextVersion so the owner
609
+ // can tell at a glance whether a rule will block now.
610
+ const statusLabel = r.deactivatedAt
611
+ ? `[DEACTIVATED ${r.deactivatedAt}]`
612
+ : r.status === 'suspended_by_flag'
613
+ ? `[SUSPENDED by flag]`
614
+ : `[ACTIVE]`;
615
+ const modeLabel = r.mode ? ` (${r.mode})` : '';
616
+ console.log(`${statusLabel}${modeLabel} ${r.activationId}`);
617
+ console.log(` principleId: ${r.principleId ?? 'unlinked'}`);
348
618
  console.log(` artifactId: ${r.artifactId}`);
349
619
  console.log(` channel: ${r.channel}`);
350
620
  console.log(` action: ${r.action}`);
351
621
  console.log(` targetRef: ${r.targetRef}`);
352
622
  console.log(` activatedAt: ${r.activatedAt}`);
623
+ if (r.promotedAt) {
624
+ console.log(` promotedAt: ${r.promotedAt}`);
625
+ }
626
+ if (r.contextVersion) {
627
+ console.log(` contextVersion: ${r.contextVersion}`);
628
+ }
629
+ if (r.evidenceSummary) {
630
+ console.log(` evidence: ${r.evidenceSummary}`);
631
+ }
632
+ if (r.nextAction) {
633
+ console.log(` nextAction: ${r.nextAction}`);
634
+ }
635
+ if (r.warning) {
636
+ console.log(` WARNING: ${r.warning}`);
637
+ }
353
638
  console.log('');
354
639
  }
640
+ if (hasDangling) {
641
+ console.error(`Warning: ${danglingArtifactIds.size} activation(s) reference non-existent artifact_id(s).`);
642
+ console.error(`Next action: Run \`pd runtime internalization integrity\` for full chain diagnostics. Consider deactivating orphaned activations or restoring missing artifacts.`);
643
+ }
355
644
  }
356
645
  }
357
646
  } catch (err: unknown) {
@@ -610,7 +899,7 @@ export async function handleRuntimeActivationEdit(opts: ActivationEditOptions):
610
899
  console.log(` previousArtifactId: ${result.previousArtifactId}`);
611
900
  }
612
901
  console.log(` editedAt: ${result.editedAt}`);
613
- console.log('Next action: review the new artifact, then approve via Console or `pd runtime activation dispatch`');
902
+ console.log('Next action: review the new artifact, then run `pd activation approve --approval-id <id>` to approve and dispatch');
614
903
  }
615
904
  } catch (err: unknown) {
616
905
  // P2 #5: initialize/DB exceptions must not break --json contract.
@@ -632,3 +921,359 @@ export async function handleRuntimeActivationEdit(opts: ActivationEditOptions):
632
921
  await stateManager.close();
633
922
  }
634
923
  }
924
+
925
+ // ── Approve Command (Bug-M fix: CLI closed loop) ──────────────────────────────
926
+ //
927
+ // Bug-M root cause: `pd activation` had dispatch/list/edit/deactivate but no
928
+ // `approve`. Owners who completed `dispatch → edit` via CLI had to switch to
929
+ // Console to approve. This handler closes the CLI loop by reusing the same
930
+ // ApprovalQueue + ApprovalCompletionService that the Console model uses,
931
+ // without depending on the pd-console package (which is a private Web UI).
932
+
933
+ interface ActivationApproveOptions {
934
+ workspace?: string;
935
+ approvalId?: string;
936
+ decidedBy?: string;
937
+ note?: string;
938
+ json?: boolean;
939
+ }
940
+
941
+ interface ApproveResult {
942
+ ok: boolean;
943
+ approvalId: string;
944
+ activationId?: string;
945
+ decision?: string;
946
+ reason?: string;
947
+ nextAction?: string;
948
+ approvalRolledBack?: boolean;
949
+ }
950
+
951
+ export async function handleActivationApprove(opts: ActivationApproveOptions): Promise<void> {
952
+ if (!opts.approvalId) {
953
+ const result: ApproveResult = {
954
+ ok: false,
955
+ approvalId: '',
956
+ reason: 'approval_id_required',
957
+ nextAction: 'Provide --approval-id <id>. Run `pd approval list` to see pending approvals.',
958
+ };
959
+ if (opts.json) {
960
+ console.log(JSON.stringify(result, null, 2));
961
+ } else {
962
+ console.error('Error: --approval-id is required');
963
+ console.error(`Next action: ${result.nextAction}`);
964
+ }
965
+ process.exitCode = 1;
966
+ return;
967
+ }
968
+
969
+ const decidedBy = opts.decidedBy ?? 'cli-operator';
970
+ // CodeRabbit review fix (cli-1-strict-json): declare stateManager outside
971
+ // try so the finally block can close it, but resolve workspace INSIDE try
972
+ // so resolveWorkspaceDir() failures are caught and routed through --json.
973
+ let stateManager: RuntimeStateManager | null = null;
974
+
975
+ try {
976
+ const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
977
+ stateManager = new RuntimeStateManager({ workspaceDir });
978
+ await stateManager.initialize();
979
+ const sqliteConn = stateManager.connection;
980
+ const approvalQueueStore = new SqliteApprovalQueueStore(sqliteConn);
981
+ const queue = new ApprovalQueue(approvalQueueStore);
982
+
983
+ // Step 1: approve the pending approval record.
984
+ let approvalResult: ApprovalDecisionResult;
985
+ try {
986
+ approvalResult = await queue.approve(opts.approvalId, decidedBy, opts.note);
987
+ } catch (err) {
988
+ const errMsg = err instanceof Error ? err.message : String(err);
989
+ const result: ApproveResult = {
990
+ ok: false,
991
+ approvalId: opts.approvalId,
992
+ reason: `approve_write_failed: ${errMsg}`,
993
+ nextAction: 'Check workspace DB integrity. Try `pd runtime diagnostics`.',
994
+ };
995
+ if (opts.json) {
996
+ console.log(JSON.stringify(result, null, 2));
997
+ } else {
998
+ console.error(`Error: ${result.reason}`);
999
+ console.error(`Next action: ${result.nextAction}`);
1000
+ }
1001
+ process.exitCode = 1;
1002
+ return;
1003
+ }
1004
+
1005
+ if (!approvalResult.ok) {
1006
+ // Map ApprovalDecisionResult errors to ApproveResult (rc-3 fail-loud).
1007
+ // ApprovalDecisionResult.error is the discriminated union 'not_found' | 'already_decided';
1008
+ // both branches are exhaustive, so no third case is reachable at runtime.
1009
+ const reason = approvalResult.error === 'not_found'
1010
+ ? 'approval_not_found'
1011
+ : `already_decided: status is ${approvalResult.status ?? 'unknown'}`;
1012
+ const nextAction = approvalResult.error === 'not_found'
1013
+ ? 'Check the approval ID. Run `pd approval list` to see pending approvals.'
1014
+ : 'Only pending approvals can be approved. The approval is already decided.';
1015
+ const result: ApproveResult = {
1016
+ ok: false,
1017
+ approvalId: opts.approvalId,
1018
+ reason,
1019
+ nextAction,
1020
+ };
1021
+ if (opts.json) {
1022
+ console.log(JSON.stringify(result, null, 2));
1023
+ } else {
1024
+ console.error(`Error: ${result.reason}`);
1025
+ console.error(`Next action: ${result.nextAction}`);
1026
+ }
1027
+ process.exitCode = 1;
1028
+ return;
1029
+ }
1030
+
1031
+ // Step 2: dispatch activation via ApprovalCompletionService (handles
1032
+ // idempotency, feature flag, and rollback on failure).
1033
+ const activationStateStore = new SqliteActivationStateStore(sqliteConn);
1034
+ const piArtifactStore = new SqlitePIArtifactStore(sqliteConn);
1035
+ const artifactReadModel = {
1036
+ getArtifactById: async (id: string): Promise<PIArtifactSnapshot | null> => {
1037
+ const rec = await piArtifactStore.getArtifactById(id);
1038
+ return rec ? toSnapshot(rec) : null;
1039
+ },
1040
+ };
1041
+ // PRI-489: inject the real workspace `rulecode_context_v2` feature flag
1042
+ // probe into the approve path's RuleHostWriter — same wiring as the
1043
+ // dispatch path (handleRuntimeActivationDispatch) and the Console model
1044
+ // (ApprovalsConsoleModel.dispatchActivationAfterApproval). Previously
1045
+ // this path constructed RuleHostWriter without the probe, so a v2
1046
+ // artifact in a flag-off workspace would pass canActivate here while
1047
+ // being rejected by dispatch/Console — an inconsistent contract that
1048
+ // violated ERR-024 (validator wired in one enforcement path but not
1049
+ // another) and ERR-089 (sibling approval path diverged from dispatch).
1050
+ const featureFlags = computeFlagsFromLoadResult(loadPdConfig(workspaceDir));
1051
+ const dispatcher = new ActivationDispatcher(
1052
+ artifactReadModel,
1053
+ activationStateStore,
1054
+ {
1055
+ writers: [
1056
+ new PromptWriter(),
1057
+ new RuleHostWriter({
1058
+ gateDeps: createProductionGateDeps(),
1059
+ featureFlagProbe: (flagId) => featureFlags.flags[flagId]?.enabled === true,
1060
+ }),
1061
+ new DeferArchiveWriter(),
1062
+ ],
1063
+ approvalQueueStore,
1064
+ },
1065
+ );
1066
+ const completionService = new ApprovalCompletionService(
1067
+ approvalQueueStore,
1068
+ dispatcher,
1069
+ activationStateStore,
1070
+ );
1071
+
1072
+ let completionResult: ApprovalCompletionResult;
1073
+ try {
1074
+ completionResult = await completionService.completeApproval({
1075
+ approvalId: opts.approvalId,
1076
+ actor: { kind: 'human', userId: decidedBy },
1077
+ now: new Date().toISOString(),
1078
+ });
1079
+ } catch (err) {
1080
+ // CodeRabbit review fix (cli-5-failure-no-mutation): approval was
1081
+ // written but completion threw unexpectedly. Roll back the approval to
1082
+ // 'pending' so the operator can retry without a stale 'approved' record
1083
+ // that has no activation. Mirrors ApprovalsConsoleModel.approve() L168-182.
1084
+ const errMsg = err instanceof Error ? err.message : String(err);
1085
+ let approvalRolledBack = false;
1086
+ try {
1087
+ const rollbackResult = await queue.resetToPending(opts.approvalId);
1088
+ approvalRolledBack = rollbackResult.ok;
1089
+ } catch { /* best-effort rollback */ }
1090
+ const result: ApproveResult = {
1091
+ ok: false,
1092
+ approvalId: opts.approvalId,
1093
+ reason: `activation_completion_failed: ${errMsg}`,
1094
+ nextAction: approvalRolledBack
1095
+ ? 'Approval rolled back to pending. Fix the issue and re-run `pd activation approve --approval-id <id>`.'
1096
+ : `Approval remains 'approved'. Run \`pd activation dispatch --artifact-id ${approvalResult.record.artifactId} --confirm\` to retry activation.`,
1097
+ approvalRolledBack,
1098
+ };
1099
+ if (opts.json) {
1100
+ console.log(JSON.stringify(result, null, 2));
1101
+ } else {
1102
+ console.error(`Error: ${result.reason}`);
1103
+ console.error(`Next action: ${result.nextAction}`);
1104
+ }
1105
+ process.exitCode = 1;
1106
+ return;
1107
+ }
1108
+
1109
+ if (!completionResult.ok) {
1110
+ // CodeRabbit review fix (cli-5-failure-no-mutation): activation
1111
+ // returned !ok. Roll back the approval to 'pending' so the operator
1112
+ // can retry without a stale 'approved' record. Mirrors
1113
+ // ApprovalsConsoleModel.approve() L168-182.
1114
+ let approvalRolledBack = false;
1115
+ try {
1116
+ const rollbackResult = await queue.resetToPending(opts.approvalId);
1117
+ approvalRolledBack = rollbackResult.ok;
1118
+ } catch { /* best-effort rollback */ }
1119
+ const result: ApproveResult = {
1120
+ ok: false,
1121
+ approvalId: opts.approvalId,
1122
+ reason: `activation_failed: ${completionResult.reason}`,
1123
+ nextAction: approvalRolledBack
1124
+ ? `Approval rolled back to pending. ${completionResult.nextAction ?? 'Fix the issue and re-run `pd activation approve --approval-id <id>`.'}`
1125
+ : `Approval remains 'approved'. ${completionResult.nextAction ?? 'Check the artifact validation status and retry.'}`,
1126
+ approvalRolledBack,
1127
+ };
1128
+ if (opts.json) {
1129
+ console.log(JSON.stringify(result, null, 2));
1130
+ } else {
1131
+ console.error(`Error: ${result.reason}`);
1132
+ console.error(`Next action: ${result.nextAction}`);
1133
+ }
1134
+ process.exitCode = 1;
1135
+ return;
1136
+ }
1137
+
1138
+ // Step 3: success — surface activation result.
1139
+ const result: ApproveResult = {
1140
+ ok: true,
1141
+ approvalId: opts.approvalId,
1142
+ activationId: completionResult.activationId,
1143
+ decision: completionResult.decision.decision,
1144
+ nextAction: 'pd activation list',
1145
+ };
1146
+ if (opts.json) {
1147
+ console.log(JSON.stringify(result, null, 2));
1148
+ } else {
1149
+ console.log(`Approval approved: ${result.approvalId}`);
1150
+ console.log(` activationId: ${result.activationId ?? 'N/A'}`);
1151
+ console.log(` decision: ${result.decision}`);
1152
+ console.log(`Next action: ${result.nextAction}`);
1153
+ }
1154
+ } catch (err: unknown) {
1155
+ // P2 #5: initialize/DB exceptions must not break --json contract.
1156
+ const errMsg = err instanceof Error ? err.message : String(err);
1157
+ const result: ApproveResult = {
1158
+ ok: false,
1159
+ approvalId: opts.approvalId ?? '',
1160
+ reason: `initialize_failed: ${errMsg}`,
1161
+ nextAction: 'Check workspace directory and DB integrity. Try `pd runtime diagnostics`.',
1162
+ };
1163
+ if (opts.json) {
1164
+ console.log(JSON.stringify(result, null, 2));
1165
+ } else {
1166
+ console.error(`Error: ${result.reason}`);
1167
+ console.error(`Next action: ${result.nextAction}`);
1168
+ }
1169
+ process.exitCode = 1;
1170
+ } finally {
1171
+ // stateManager may be null if resolveWorkspaceDir() threw before assignment.
1172
+ await stateManager?.close();
1173
+ }
1174
+ }
1175
+
1176
+ // ── Commander Wiring (CLI gate rule 7: test the real command wiring) ─────────
1177
+ //
1178
+ // Extracted so parser-level tests can exercise the actual flag registration
1179
+ // without importing all of `index.ts`. Mirrors `registerRunRuleHostCommand`.
1180
+
1181
+ export function registerRuntimeActivationPromoteCommand(parent: Command): Command {
1182
+ return parent
1183
+ .command('promote')
1184
+ .description('Promote a code_tool_hook activation from shadow observation to live blocking')
1185
+ .requiredOption('--activation-id <id>', 'Shadow activation ID to promote')
1186
+ .option('-w, --workspace <path>', 'Workspace directory')
1187
+ .option('--dry-run', 'Validate eligibility without changing activation state')
1188
+ .option('--confirm', 'Confirm promotion to live blocking')
1189
+ .option('--json', 'Output raw JSON')
1190
+ .action(async (opts) => {
1191
+ await handleRuntimeActivationPromote({
1192
+ activationId: opts.activationId,
1193
+ workspace: opts.workspace,
1194
+ dryRun: opts.dryRun,
1195
+ confirm: opts.confirm,
1196
+ json: opts.json,
1197
+ });
1198
+ });
1199
+ }
1200
+
1201
+ // PRI-493: Extracted helpers for parser-level flag-wiring tests (cli-7).
1202
+ // Mirrors registerRuntimeActivationPromoteCommand — single source of truth
1203
+ // shared with index.ts. Flag typos in production surface at parseAsync time.
1204
+
1205
+ export function registerRuntimeActivationDeactivateCommand(parent: Command): Command {
1206
+ return parent
1207
+ .command('deactivate')
1208
+ .description('Deactivate an activation by activation ID')
1209
+ .requiredOption('--activation-id <id>', 'Activation ID to deactivate')
1210
+ .option('-w, --workspace <path>', 'Workspace directory')
1211
+ .option('--json', 'Output raw JSON')
1212
+ .action(async (opts) => {
1213
+ await handleRuntimeActivationDeactivate({
1214
+ activationId: opts.activationId,
1215
+ workspace: opts.workspace,
1216
+ json: opts.json,
1217
+ });
1218
+ });
1219
+ }
1220
+
1221
+ export function registerRuntimeActivationListCommand(parent: Command): Command {
1222
+ return parent
1223
+ .command('list')
1224
+ .description('List all activations for a workspace')
1225
+ .option('-w, --workspace <path>', 'Workspace directory')
1226
+ .option('-c, --channel <channel>', 'Filter by channel (prompt|code_tool_hook)')
1227
+ .option('--include-deactivated', 'Include deactivated records in output')
1228
+ .option('--json', 'Output raw JSON')
1229
+ .action(async (opts) => {
1230
+ await handleRuntimeActivationList({
1231
+ workspace: opts.workspace,
1232
+ channel: opts.channel,
1233
+ includeDeactivated: opts.includeDeactivated,
1234
+ json: opts.json,
1235
+ });
1236
+ });
1237
+ }
1238
+
1239
+ export function registerRuntimeActivationDispatchCommand(parent: Command): Command {
1240
+ return parent
1241
+ .command('dispatch')
1242
+ .description('Dispatch an activation for a rollout-reviewed artifact')
1243
+ .option('-a, --artifact-id <id>', 'PIArtifact ID to activate')
1244
+ .option('-w, --workspace <path>', 'Workspace directory')
1245
+ .option('-c, --channel <channel>', 'Activation channel (prompt|defer_archive)', 'prompt')
1246
+ .option('--dry-run', 'Dry-run mode (default, no writes)')
1247
+ .option('--confirm', 'Confirm and write activation record')
1248
+ .option('--json', 'Output raw JSON')
1249
+ .action(async (opts) => {
1250
+ await handleRuntimeActivationDispatch({
1251
+ workspace: opts.workspace,
1252
+ artifactId: opts.artifactId,
1253
+ channel: opts.channel,
1254
+ dryRun: opts.dryRun,
1255
+ confirm: opts.confirm,
1256
+ json: opts.json,
1257
+ });
1258
+ });
1259
+ }
1260
+
1261
+ export function registerRuntimeActivationApproveCommand(parent: Command): Command {
1262
+ return parent
1263
+ .command('approve')
1264
+ .description('Approve a pending approval and dispatch its activation')
1265
+ .requiredOption('-a, --approval-id <id>', 'Approval ID to approve')
1266
+ .option('--decided-by <user>', 'Reviewer name (default: cli-operator)')
1267
+ .option('--note <text>', 'Optional approval note')
1268
+ .option('-w, --workspace <path>', 'Workspace directory')
1269
+ .option('--json', 'Output raw JSON')
1270
+ .action(async (opts) => {
1271
+ await handleActivationApprove({
1272
+ approvalId: opts.approvalId,
1273
+ decidedBy: opts.decidedBy,
1274
+ note: opts.note,
1275
+ workspace: opts.workspace,
1276
+ json: opts.json,
1277
+ });
1278
+ });
1279
+ }