@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
@@ -1,6 +1,14 @@
1
1
  import * as path from 'path';
2
- import { RuntimeStateManager, ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, createProductionGateDeps, SqliteActivationStateStore, SqliteApprovalQueueStore, SqlitePIArtifactStore, isArtifactRevisionOf, } from '@principles/core/runtime-v2';
2
+ import { RuntimeStateManager, ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, createProductionGateDeps, SqliteActivationStateStore, SqliteApprovalQueueStore, SqlitePIArtifactStore, ApprovalQueue, ApprovalCompletionService, isArtifactRevisionOf, extractEvidenceRefs, extractPrincipleId, } from '@principles/core/runtime-v2';
3
3
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
4
+ import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
5
+ /**
6
+ * Type guard for parsed JSON objects (rc-2-no-as-bypass).
7
+ * Replaces `as Record<string, unknown>` casts on untrusted contentJson.
8
+ */
9
+ function isRecord(value) {
10
+ return value !== null && value !== undefined && typeof value === 'object' && !Array.isArray(value);
11
+ }
4
12
  function mapRolloutDecision(reviewDecision) {
5
13
  if (!reviewDecision)
6
14
  return 'require_approval';
@@ -124,12 +132,16 @@ export async function handleRuntimeActivationDispatch(opts) {
124
132
  };
125
133
  const activationStateStore = new SqliteActivationStateStore(stateManager.connection);
126
134
  const approvalQueueStore = new SqliteApprovalQueueStore(stateManager.connection);
135
+ const featureFlags = computeFlagsFromLoadResult(loadPdConfig(workspaceDir));
127
136
  // Wire all three MVP-Core writers, including RuleHostWriter for code_tool_hook.
128
137
  // PRI-408: fixes P0 breakpoint where code_tool_hook channel could not activate.
129
138
  const dispatcher = new ActivationDispatcher(artifactReadModel, activationStateStore, {
130
139
  writers: [
131
140
  new PromptWriter(),
132
- new RuleHostWriter({ gateDeps: createProductionGateDeps() }),
141
+ new RuleHostWriter({
142
+ gateDeps: createProductionGateDeps(),
143
+ featureFlagProbe: (flagId) => featureFlags.flags[flagId]?.enabled === true,
144
+ }),
133
145
  new DeferArchiveWriter(),
134
146
  ],
135
147
  approvalQueueStore,
@@ -162,7 +174,7 @@ export async function handleRuntimeActivationDeactivate(opts) {
162
174
  ok: false,
163
175
  activationId: '',
164
176
  reason: 'activation_id_required',
165
- nextAction: 'Provide --activation-id <id> from `pd runtime activation list`',
177
+ nextAction: 'Provide --activation-id <id> from `pd activation list`',
166
178
  };
167
179
  if (opts.json) {
168
180
  console.log(JSON.stringify(result, null, 2));
@@ -189,7 +201,7 @@ export async function handleRuntimeActivationDeactivate(opts) {
189
201
  ok: false,
190
202
  activationId: opts.activationId,
191
203
  reason: 'not_found_or_already_deactivated',
192
- nextAction: 'Check activation ID with `pd runtime activation list`, or it may already be deactivated',
204
+ nextAction: 'Check activation ID with `pd activation list`, or it may already be deactivated',
193
205
  };
194
206
  if (opts.json) {
195
207
  // Strict JSON mode: exactly one parseable JSON object on stdout
@@ -232,6 +244,86 @@ export async function handleRuntimeActivationDeactivate(opts) {
232
244
  await stateManager.close();
233
245
  }
234
246
  }
247
+ export async function handleRuntimeActivationPromote(opts) {
248
+ const activationId = opts.activationId?.trim() ?? '';
249
+ const refuse = (reason, nextAction) => {
250
+ const result = { ok: false, decision: 'refused', activationId, reason, nextAction };
251
+ if (opts.json)
252
+ console.log(JSON.stringify(result));
253
+ else {
254
+ console.error(`Promotion refused: ${reason}`);
255
+ console.error(`Next action: ${nextAction}`);
256
+ }
257
+ process.exitCode = 1;
258
+ };
259
+ if (!activationId) {
260
+ refuse('activation_id_required', 'Provide --activation-id from `pd activation list --channel code_tool_hook`.');
261
+ return;
262
+ }
263
+ if (opts.dryRun === true && opts.confirm === true) {
264
+ refuse('dry_run_confirm_mutually_exclusive', 'Choose either --dry-run or --confirm.');
265
+ return;
266
+ }
267
+ let stateManager;
268
+ try {
269
+ const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
270
+ stateManager = new RuntimeStateManager({ workspaceDir });
271
+ await stateManager.initialize();
272
+ const store = new SqliteActivationStateStore(stateManager.connection);
273
+ const activeHooks = await store.listCodeToolHookActivations(false);
274
+ // CodeRabbit PR2 Comment 3: dry-run must apply the same eligibility checks
275
+ // as the real promote path. `promoteActivation` runs a COUNT guard inside a
276
+ // BEGIN IMMEDIATE transaction and refuses when the count of matching shadow
277
+ // rows is not exactly 1. The previous dry-run branch used `find()` (returns
278
+ // the first match) and reported `would_promote` even when duplicates would
279
+ // make the confirm path throw. Mirror the store's uniqueness check here so
280
+ // dry-run and confirm agree (cli-5: failure paths must not mutate state;
281
+ // cli-6: degraded/refused results carry a structured reason + nextAction).
282
+ const matchingShadows = activeHooks.filter((record) => record.activationId === activationId && record.action === 'code_tool_hook_shadow_activate');
283
+ if (matchingShadows.length === 0) {
284
+ refuse('not_found_inactive_or_not_shadow', 'Refresh `pd activation list --channel code_tool_hook`; only active shadow activations can be promoted.');
285
+ return;
286
+ }
287
+ if (matchingShadows.length > 1) {
288
+ refuse('duplicate_shadow_activations', `${matchingShadows.length} shadow activations share activation_id=${activationId}; resolve duplicates before promoting.`);
289
+ return;
290
+ }
291
+ if (opts.confirm !== true) {
292
+ const result = {
293
+ ok: true,
294
+ decision: 'would_promote',
295
+ activationId,
296
+ nextAction: `Run pd activation promote --activation-id ${activationId} --confirm to enable live blocking.`,
297
+ };
298
+ if (opts.json)
299
+ console.log(JSON.stringify(result));
300
+ else {
301
+ console.log(`Would promote: ${activationId}`);
302
+ console.log(` nextAction: ${result.nextAction}`);
303
+ }
304
+ return;
305
+ }
306
+ const promotedAt = new Date().toISOString();
307
+ const promoted = await store.promoteActivation(activationId, promotedAt);
308
+ if (!promoted) {
309
+ refuse('promotion_precondition_changed', 'Refresh the activation list; the activation changed before promotion.');
310
+ return;
311
+ }
312
+ const result = { ok: true, decision: 'promoted', activationId, promotedAt };
313
+ if (opts.json)
314
+ console.log(JSON.stringify(result));
315
+ else {
316
+ console.log(`Promoted live: ${activationId}`);
317
+ console.log(` promotedAt: ${promotedAt}`);
318
+ }
319
+ }
320
+ catch (err) {
321
+ refuse(`promotion_failed: ${err instanceof Error ? err.message : String(err)}`, 'Check workspace configuration and database integrity; no promotion was applied.');
322
+ }
323
+ finally {
324
+ await stateManager?.close();
325
+ }
326
+ }
235
327
  export async function handleRuntimeActivationList(opts) {
236
328
  // P2 #5 fix: fail loud on invalid channel instead of silently listing all.
237
329
  const VALID_CHANNELS = new Set(['prompt', 'code_tool_hook', undefined]);
@@ -275,25 +367,170 @@ export async function handleRuntimeActivationList(opts) {
275
367
  const filtered = opts.includeDeactivated
276
368
  ? records
277
369
  : records.filter(r => r.deactivatedAt === null);
370
+ // F9-1: Validate artifact_id → pi_artifacts.artifact_id reference for each
371
+ // activation. A dangling reference means the activation cannot function
372
+ // (RuleHost cannot load implementationCode, dispatcher cannot verify
373
+ // lineage). Emit degraded status with reason + nextAction instead of
374
+ // silently returning corrupted records (rc-9-no-silent-fallback,
375
+ // rc-6-lineage-consistency; related ERR: ERR-002).
376
+ const { piArtifactStore } = stateManager;
377
+ const uniqueArtifactIds = new Set(filtered.map(r => r.artifactId));
378
+ const danglingArtifactIds = new Set();
379
+ // PRI-491: Map artifactId to { contextVersion, evidenceRefs } extracted
380
+ // from contentJson. rc-1/rc-2: contentJson is parsed as unknown and
381
+ // type-narrowed; never `as`-cast without prior typeof check.
382
+ const artifactMetadata = new Map();
383
+ for (const artifactId of uniqueArtifactIds) {
384
+ try {
385
+ const artifact = await piArtifactStore.getArtifactById(artifactId);
386
+ if (!artifact) {
387
+ danglingArtifactIds.add(artifactId);
388
+ continue;
389
+ }
390
+ // PRI-491: Parse contentJson to extract contextVersion + evidenceRefs.
391
+ // rc-1: treat parsed JSON as unknown; rc-2: narrow with typeof before use.
392
+ let parsedContent = null;
393
+ try {
394
+ const parsed = JSON.parse(artifact.contentJson);
395
+ if (isRecord(parsed)) {
396
+ parsedContent = parsed;
397
+ }
398
+ }
399
+ catch {
400
+ // Malformed contentJson - treat as no metadata (not dangling, just unreadable)
401
+ }
402
+ const requiresCtxV2 = parsedContent !== null
403
+ && Object.hasOwn(parsedContent, 'requiresContextVersion')
404
+ && parsedContent.requiresContextVersion === 2;
405
+ const contextVersion = requiresCtxV2 ? 'v2' : 'v1';
406
+ const evidenceRefs = parsedContent !== null ? extractEvidenceRefs(parsedContent) : null;
407
+ // PRI-500: extract principleId to match Console's ActivationRecord field.
408
+ // Uses extractPrincipleId (4-step fallback) so dreamer artifacts whose
409
+ // sourcePrincipleId was stripped still resolve via contentJson.
410
+ const principleId = extractPrincipleId(toSnapshot(artifact));
411
+ artifactMetadata.set(artifactId, { contextVersion, evidenceRefs, principleId });
412
+ }
413
+ catch {
414
+ // Treat lookup failure as dangling - fail loud rather than silent (rc-9).
415
+ danglingArtifactIds.add(artifactId);
416
+ }
417
+ }
418
+ const hasDangling = danglingArtifactIds.size > 0;
419
+ // PRI-491: Probe rulecode_context_v2 flag to determine suspended_by_flag
420
+ // status for v2 activations. When the flag is off, v2 activations are
421
+ // suspended (not executing) even though they remain active in the DB.
422
+ const featureFlags = computeFlagsFromLoadResult(loadPdConfig(workspaceDir));
423
+ const v2FlagEnabled = featureFlags.flags.rulecode_context_v2?.enabled === true;
424
+ const annotated = filtered.map(r => {
425
+ // Derive mode from action (shadow_activate -> shadow, live_activate -> live)
426
+ const mode = r.action === 'code_tool_hook_shadow_activate'
427
+ ? 'shadow'
428
+ : r.action === 'code_tool_hook_live_activate'
429
+ ? 'live'
430
+ : undefined;
431
+ // Look up artifact metadata (contextVersion, evidenceRefs)
432
+ const meta = artifactMetadata.get(r.artifactId);
433
+ const contextVersion = meta?.contextVersion;
434
+ const evidenceRefs = meta?.evidenceRefs ?? undefined;
435
+ const evidenceSummary = evidenceRefs && evidenceRefs.length > 0
436
+ ? `${evidenceRefs.length} evidence ref(s): ${evidenceRefs.slice(0, 3).join(', ')}${evidenceRefs.length > 3 ? '...' : ''}`
437
+ : undefined;
438
+ // Derive status: deactivated > suspended_by_flag > active
439
+ let status;
440
+ let nextAction;
441
+ if (r.deactivatedAt) {
442
+ status = 'deactivated';
443
+ nextAction = undefined;
444
+ }
445
+ else if (contextVersion === 'v2' && !v2FlagEnabled) {
446
+ status = 'suspended_by_flag';
447
+ nextAction = `Enable rulecode_context_v2 flag or deactivate: pd activation deactivate --activation-id ${r.activationId} --confirm`;
448
+ }
449
+ else {
450
+ status = 'active';
451
+ if (mode === 'shadow') {
452
+ nextAction = `pd activation promote --activation-id ${r.activationId} --confirm`;
453
+ }
454
+ else if (mode === 'live') {
455
+ nextAction = `pd activation deactivate --activation-id ${r.activationId} --confirm`;
456
+ }
457
+ else {
458
+ nextAction = undefined;
459
+ }
460
+ }
461
+ const record = {
462
+ ...r,
463
+ principleId: meta?.principleId ?? 'unlinked',
464
+ mode,
465
+ status,
466
+ contextVersion,
467
+ evidenceRefs,
468
+ evidenceSummary,
469
+ nextAction,
470
+ };
471
+ if (danglingArtifactIds.has(r.artifactId)) {
472
+ record.warning = `artifact_id "${r.artifactId}" does not exist in pi_artifacts - activation is orphaned`;
473
+ }
474
+ return record;
475
+ });
278
476
  if (opts.json) {
279
- // Strict JSON mode: exactly one parseable JSON object on stdout
280
- console.log(JSON.stringify({ activations: filtered }, null, 2));
477
+ // Strict JSON mode: exactly one parseable JSON object on stdout.
478
+ // When dangling references are detected, emit degraded status with
479
+ // reason + nextAction so operators can act on the corruption (rc-9,
480
+ // cli-6-output-next-action).
481
+ const payload = { activations: annotated };
482
+ if (hasDangling) {
483
+ payload.status = 'degraded';
484
+ payload.reason = `${danglingArtifactIds.size} activation(s) reference non-existent artifact_id(s): ${Array.from(danglingArtifactIds).join(', ')}`;
485
+ 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.';
486
+ }
487
+ else {
488
+ payload.status = 'ok';
489
+ }
490
+ console.log(JSON.stringify(payload, null, 2));
281
491
  }
282
492
  else {
283
- if (filtered.length === 0) {
493
+ if (annotated.length === 0) {
284
494
  console.log('No active activations found.');
285
495
  }
286
496
  else {
287
- for (const r of filtered) {
288
- const status = r.deactivatedAt ? `[DEACTIVATED ${r.deactivatedAt}]` : '[ACTIVE]';
289
- console.log(`${status} ${r.activationId}`);
497
+ for (const r of annotated) {
498
+ // PRI-491: Text output shows mode/status/contextVersion so the owner
499
+ // can tell at a glance whether a rule will block now.
500
+ const statusLabel = r.deactivatedAt
501
+ ? `[DEACTIVATED ${r.deactivatedAt}]`
502
+ : r.status === 'suspended_by_flag'
503
+ ? `[SUSPENDED by flag]`
504
+ : `[ACTIVE]`;
505
+ const modeLabel = r.mode ? ` (${r.mode})` : '';
506
+ console.log(`${statusLabel}${modeLabel} ${r.activationId}`);
507
+ console.log(` principleId: ${r.principleId ?? 'unlinked'}`);
290
508
  console.log(` artifactId: ${r.artifactId}`);
291
509
  console.log(` channel: ${r.channel}`);
292
510
  console.log(` action: ${r.action}`);
293
511
  console.log(` targetRef: ${r.targetRef}`);
294
512
  console.log(` activatedAt: ${r.activatedAt}`);
513
+ if (r.promotedAt) {
514
+ console.log(` promotedAt: ${r.promotedAt}`);
515
+ }
516
+ if (r.contextVersion) {
517
+ console.log(` contextVersion: ${r.contextVersion}`);
518
+ }
519
+ if (r.evidenceSummary) {
520
+ console.log(` evidence: ${r.evidenceSummary}`);
521
+ }
522
+ if (r.nextAction) {
523
+ console.log(` nextAction: ${r.nextAction}`);
524
+ }
525
+ if (r.warning) {
526
+ console.log(` WARNING: ${r.warning}`);
527
+ }
295
528
  console.log('');
296
529
  }
530
+ if (hasDangling) {
531
+ console.error(`Warning: ${danglingArtifactIds.size} activation(s) reference non-existent artifact_id(s).`);
532
+ console.error(`Next action: Run \`pd runtime internalization integrity\` for full chain diagnostics. Consider deactivating orphaned activations or restoring missing artifacts.`);
533
+ }
297
534
  }
298
535
  }
299
536
  }
@@ -536,7 +773,7 @@ export async function handleRuntimeActivationEdit(opts) {
536
773
  console.log(` previousArtifactId: ${result.previousArtifactId}`);
537
774
  }
538
775
  console.log(` editedAt: ${result.editedAt}`);
539
- console.log('Next action: review the new artifact, then approve via Console or `pd runtime activation dispatch`');
776
+ console.log('Next action: review the new artifact, then run `pd activation approve --approval-id <id>` to approve and dispatch');
540
777
  }
541
778
  }
542
779
  catch (err) {
@@ -561,4 +798,323 @@ export async function handleRuntimeActivationEdit(opts) {
561
798
  await stateManager.close();
562
799
  }
563
800
  }
801
+ export async function handleActivationApprove(opts) {
802
+ if (!opts.approvalId) {
803
+ const result = {
804
+ ok: false,
805
+ approvalId: '',
806
+ reason: 'approval_id_required',
807
+ nextAction: 'Provide --approval-id <id>. Run `pd approval list` to see pending approvals.',
808
+ };
809
+ if (opts.json) {
810
+ console.log(JSON.stringify(result, null, 2));
811
+ }
812
+ else {
813
+ console.error('Error: --approval-id is required');
814
+ console.error(`Next action: ${result.nextAction}`);
815
+ }
816
+ process.exitCode = 1;
817
+ return;
818
+ }
819
+ const decidedBy = opts.decidedBy ?? 'cli-operator';
820
+ // CodeRabbit review fix (cli-1-strict-json): declare stateManager outside
821
+ // try so the finally block can close it, but resolve workspace INSIDE try
822
+ // so resolveWorkspaceDir() failures are caught and routed through --json.
823
+ let stateManager = null;
824
+ try {
825
+ const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
826
+ stateManager = new RuntimeStateManager({ workspaceDir });
827
+ await stateManager.initialize();
828
+ const sqliteConn = stateManager.connection;
829
+ const approvalQueueStore = new SqliteApprovalQueueStore(sqliteConn);
830
+ const queue = new ApprovalQueue(approvalQueueStore);
831
+ // Step 1: approve the pending approval record.
832
+ let approvalResult;
833
+ try {
834
+ approvalResult = await queue.approve(opts.approvalId, decidedBy, opts.note);
835
+ }
836
+ catch (err) {
837
+ const errMsg = err instanceof Error ? err.message : String(err);
838
+ const result = {
839
+ ok: false,
840
+ approvalId: opts.approvalId,
841
+ reason: `approve_write_failed: ${errMsg}`,
842
+ nextAction: 'Check workspace DB integrity. Try `pd runtime diagnostics`.',
843
+ };
844
+ if (opts.json) {
845
+ console.log(JSON.stringify(result, null, 2));
846
+ }
847
+ else {
848
+ console.error(`Error: ${result.reason}`);
849
+ console.error(`Next action: ${result.nextAction}`);
850
+ }
851
+ process.exitCode = 1;
852
+ return;
853
+ }
854
+ if (!approvalResult.ok) {
855
+ // Map ApprovalDecisionResult errors to ApproveResult (rc-3 fail-loud).
856
+ // ApprovalDecisionResult.error is the discriminated union 'not_found' | 'already_decided';
857
+ // both branches are exhaustive, so no third case is reachable at runtime.
858
+ const reason = approvalResult.error === 'not_found'
859
+ ? 'approval_not_found'
860
+ : `already_decided: status is ${approvalResult.status ?? 'unknown'}`;
861
+ const nextAction = approvalResult.error === 'not_found'
862
+ ? 'Check the approval ID. Run `pd approval list` to see pending approvals.'
863
+ : 'Only pending approvals can be approved. The approval is already decided.';
864
+ const result = {
865
+ ok: false,
866
+ approvalId: opts.approvalId,
867
+ reason,
868
+ nextAction,
869
+ };
870
+ if (opts.json) {
871
+ console.log(JSON.stringify(result, null, 2));
872
+ }
873
+ else {
874
+ console.error(`Error: ${result.reason}`);
875
+ console.error(`Next action: ${result.nextAction}`);
876
+ }
877
+ process.exitCode = 1;
878
+ return;
879
+ }
880
+ // Step 2: dispatch activation via ApprovalCompletionService (handles
881
+ // idempotency, feature flag, and rollback on failure).
882
+ const activationStateStore = new SqliteActivationStateStore(sqliteConn);
883
+ const piArtifactStore = new SqlitePIArtifactStore(sqliteConn);
884
+ const artifactReadModel = {
885
+ getArtifactById: async (id) => {
886
+ const rec = await piArtifactStore.getArtifactById(id);
887
+ return rec ? toSnapshot(rec) : null;
888
+ },
889
+ };
890
+ // PRI-489: inject the real workspace `rulecode_context_v2` feature flag
891
+ // probe into the approve path's RuleHostWriter — same wiring as the
892
+ // dispatch path (handleRuntimeActivationDispatch) and the Console model
893
+ // (ApprovalsConsoleModel.dispatchActivationAfterApproval). Previously
894
+ // this path constructed RuleHostWriter without the probe, so a v2
895
+ // artifact in a flag-off workspace would pass canActivate here while
896
+ // being rejected by dispatch/Console — an inconsistent contract that
897
+ // violated ERR-024 (validator wired in one enforcement path but not
898
+ // another) and ERR-089 (sibling approval path diverged from dispatch).
899
+ const featureFlags = computeFlagsFromLoadResult(loadPdConfig(workspaceDir));
900
+ const dispatcher = new ActivationDispatcher(artifactReadModel, activationStateStore, {
901
+ writers: [
902
+ new PromptWriter(),
903
+ new RuleHostWriter({
904
+ gateDeps: createProductionGateDeps(),
905
+ featureFlagProbe: (flagId) => featureFlags.flags[flagId]?.enabled === true,
906
+ }),
907
+ new DeferArchiveWriter(),
908
+ ],
909
+ approvalQueueStore,
910
+ });
911
+ const completionService = new ApprovalCompletionService(approvalQueueStore, dispatcher, activationStateStore);
912
+ let completionResult;
913
+ try {
914
+ completionResult = await completionService.completeApproval({
915
+ approvalId: opts.approvalId,
916
+ actor: { kind: 'human', userId: decidedBy },
917
+ now: new Date().toISOString(),
918
+ });
919
+ }
920
+ catch (err) {
921
+ // CodeRabbit review fix (cli-5-failure-no-mutation): approval was
922
+ // written but completion threw unexpectedly. Roll back the approval to
923
+ // 'pending' so the operator can retry without a stale 'approved' record
924
+ // that has no activation. Mirrors ApprovalsConsoleModel.approve() L168-182.
925
+ const errMsg = err instanceof Error ? err.message : String(err);
926
+ let approvalRolledBack = false;
927
+ try {
928
+ const rollbackResult = await queue.resetToPending(opts.approvalId);
929
+ approvalRolledBack = rollbackResult.ok;
930
+ }
931
+ catch { /* best-effort rollback */ }
932
+ const result = {
933
+ ok: false,
934
+ approvalId: opts.approvalId,
935
+ reason: `activation_completion_failed: ${errMsg}`,
936
+ nextAction: approvalRolledBack
937
+ ? 'Approval rolled back to pending. Fix the issue and re-run `pd activation approve --approval-id <id>`.'
938
+ : `Approval remains 'approved'. Run \`pd activation dispatch --artifact-id ${approvalResult.record.artifactId} --confirm\` to retry activation.`,
939
+ approvalRolledBack,
940
+ };
941
+ if (opts.json) {
942
+ console.log(JSON.stringify(result, null, 2));
943
+ }
944
+ else {
945
+ console.error(`Error: ${result.reason}`);
946
+ console.error(`Next action: ${result.nextAction}`);
947
+ }
948
+ process.exitCode = 1;
949
+ return;
950
+ }
951
+ if (!completionResult.ok) {
952
+ // CodeRabbit review fix (cli-5-failure-no-mutation): activation
953
+ // returned !ok. Roll back the approval to 'pending' so the operator
954
+ // can retry without a stale 'approved' record. Mirrors
955
+ // ApprovalsConsoleModel.approve() L168-182.
956
+ let approvalRolledBack = false;
957
+ try {
958
+ const rollbackResult = await queue.resetToPending(opts.approvalId);
959
+ approvalRolledBack = rollbackResult.ok;
960
+ }
961
+ catch { /* best-effort rollback */ }
962
+ const result = {
963
+ ok: false,
964
+ approvalId: opts.approvalId,
965
+ reason: `activation_failed: ${completionResult.reason}`,
966
+ nextAction: approvalRolledBack
967
+ ? `Approval rolled back to pending. ${completionResult.nextAction ?? 'Fix the issue and re-run `pd activation approve --approval-id <id>`.'}`
968
+ : `Approval remains 'approved'. ${completionResult.nextAction ?? 'Check the artifact validation status and retry.'}`,
969
+ approvalRolledBack,
970
+ };
971
+ if (opts.json) {
972
+ console.log(JSON.stringify(result, null, 2));
973
+ }
974
+ else {
975
+ console.error(`Error: ${result.reason}`);
976
+ console.error(`Next action: ${result.nextAction}`);
977
+ }
978
+ process.exitCode = 1;
979
+ return;
980
+ }
981
+ // Step 3: success — surface activation result.
982
+ const result = {
983
+ ok: true,
984
+ approvalId: opts.approvalId,
985
+ activationId: completionResult.activationId,
986
+ decision: completionResult.decision.decision,
987
+ nextAction: 'pd activation list',
988
+ };
989
+ if (opts.json) {
990
+ console.log(JSON.stringify(result, null, 2));
991
+ }
992
+ else {
993
+ console.log(`Approval approved: ${result.approvalId}`);
994
+ console.log(` activationId: ${result.activationId ?? 'N/A'}`);
995
+ console.log(` decision: ${result.decision}`);
996
+ console.log(`Next action: ${result.nextAction}`);
997
+ }
998
+ }
999
+ catch (err) {
1000
+ // P2 #5: initialize/DB exceptions must not break --json contract.
1001
+ const errMsg = err instanceof Error ? err.message : String(err);
1002
+ const result = {
1003
+ ok: false,
1004
+ approvalId: opts.approvalId ?? '',
1005
+ reason: `initialize_failed: ${errMsg}`,
1006
+ nextAction: 'Check workspace directory and DB integrity. Try `pd runtime diagnostics`.',
1007
+ };
1008
+ if (opts.json) {
1009
+ console.log(JSON.stringify(result, null, 2));
1010
+ }
1011
+ else {
1012
+ console.error(`Error: ${result.reason}`);
1013
+ console.error(`Next action: ${result.nextAction}`);
1014
+ }
1015
+ process.exitCode = 1;
1016
+ }
1017
+ finally {
1018
+ // stateManager may be null if resolveWorkspaceDir() threw before assignment.
1019
+ await stateManager?.close();
1020
+ }
1021
+ }
1022
+ // ── Commander Wiring (CLI gate rule 7: test the real command wiring) ─────────
1023
+ //
1024
+ // Extracted so parser-level tests can exercise the actual flag registration
1025
+ // without importing all of `index.ts`. Mirrors `registerRunRuleHostCommand`.
1026
+ export function registerRuntimeActivationPromoteCommand(parent) {
1027
+ return parent
1028
+ .command('promote')
1029
+ .description('Promote a code_tool_hook activation from shadow observation to live blocking')
1030
+ .requiredOption('--activation-id <id>', 'Shadow activation ID to promote')
1031
+ .option('-w, --workspace <path>', 'Workspace directory')
1032
+ .option('--dry-run', 'Validate eligibility without changing activation state')
1033
+ .option('--confirm', 'Confirm promotion to live blocking')
1034
+ .option('--json', 'Output raw JSON')
1035
+ .action(async (opts) => {
1036
+ await handleRuntimeActivationPromote({
1037
+ activationId: opts.activationId,
1038
+ workspace: opts.workspace,
1039
+ dryRun: opts.dryRun,
1040
+ confirm: opts.confirm,
1041
+ json: opts.json,
1042
+ });
1043
+ });
1044
+ }
1045
+ // PRI-493: Extracted helpers for parser-level flag-wiring tests (cli-7).
1046
+ // Mirrors registerRuntimeActivationPromoteCommand — single source of truth
1047
+ // shared with index.ts. Flag typos in production surface at parseAsync time.
1048
+ export function registerRuntimeActivationDeactivateCommand(parent) {
1049
+ return parent
1050
+ .command('deactivate')
1051
+ .description('Deactivate an activation by activation ID')
1052
+ .requiredOption('--activation-id <id>', 'Activation ID to deactivate')
1053
+ .option('-w, --workspace <path>', 'Workspace directory')
1054
+ .option('--json', 'Output raw JSON')
1055
+ .action(async (opts) => {
1056
+ await handleRuntimeActivationDeactivate({
1057
+ activationId: opts.activationId,
1058
+ workspace: opts.workspace,
1059
+ json: opts.json,
1060
+ });
1061
+ });
1062
+ }
1063
+ export function registerRuntimeActivationListCommand(parent) {
1064
+ return parent
1065
+ .command('list')
1066
+ .description('List all activations for a workspace')
1067
+ .option('-w, --workspace <path>', 'Workspace directory')
1068
+ .option('-c, --channel <channel>', 'Filter by channel (prompt|code_tool_hook)')
1069
+ .option('--include-deactivated', 'Include deactivated records in output')
1070
+ .option('--json', 'Output raw JSON')
1071
+ .action(async (opts) => {
1072
+ await handleRuntimeActivationList({
1073
+ workspace: opts.workspace,
1074
+ channel: opts.channel,
1075
+ includeDeactivated: opts.includeDeactivated,
1076
+ json: opts.json,
1077
+ });
1078
+ });
1079
+ }
1080
+ export function registerRuntimeActivationDispatchCommand(parent) {
1081
+ return parent
1082
+ .command('dispatch')
1083
+ .description('Dispatch an activation for a rollout-reviewed artifact')
1084
+ .option('-a, --artifact-id <id>', 'PIArtifact ID to activate')
1085
+ .option('-w, --workspace <path>', 'Workspace directory')
1086
+ .option('-c, --channel <channel>', 'Activation channel (prompt|defer_archive)', 'prompt')
1087
+ .option('--dry-run', 'Dry-run mode (default, no writes)')
1088
+ .option('--confirm', 'Confirm and write activation record')
1089
+ .option('--json', 'Output raw JSON')
1090
+ .action(async (opts) => {
1091
+ await handleRuntimeActivationDispatch({
1092
+ workspace: opts.workspace,
1093
+ artifactId: opts.artifactId,
1094
+ channel: opts.channel,
1095
+ dryRun: opts.dryRun,
1096
+ confirm: opts.confirm,
1097
+ json: opts.json,
1098
+ });
1099
+ });
1100
+ }
1101
+ export function registerRuntimeActivationApproveCommand(parent) {
1102
+ return parent
1103
+ .command('approve')
1104
+ .description('Approve a pending approval and dispatch its activation')
1105
+ .requiredOption('-a, --approval-id <id>', 'Approval ID to approve')
1106
+ .option('--decided-by <user>', 'Reviewer name (default: cli-operator)')
1107
+ .option('--note <text>', 'Optional approval note')
1108
+ .option('-w, --workspace <path>', 'Workspace directory')
1109
+ .option('--json', 'Output raw JSON')
1110
+ .action(async (opts) => {
1111
+ await handleActivationApprove({
1112
+ approvalId: opts.approvalId,
1113
+ decidedBy: opts.decidedBy,
1114
+ note: opts.note,
1115
+ workspace: opts.workspace,
1116
+ json: opts.json,
1117
+ });
1118
+ });
1119
+ }
564
1120
  //# sourceMappingURL=runtime-activation.js.map