@principles/pd-cli 1.112.0 → 1.114.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 (35) hide show
  1. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts +24 -0
  2. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts.map +1 -0
  3. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +223 -0
  4. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -0
  5. package/dist/commands/runtime-activation.d.ts +37 -0
  6. package/dist/commands/runtime-activation.d.ts.map +1 -1
  7. package/dist/commands/runtime-activation.js +416 -2
  8. package/dist/commands/runtime-activation.js.map +1 -1
  9. package/dist/commands/runtime-internalization-run-rulehost.d.ts +23 -0
  10. package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -0
  11. package/dist/commands/runtime-internalization-run-rulehost.js +364 -0
  12. package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -0
  13. package/dist/index.js +56 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/services/demo-rule-compiler.d.ts +24 -0
  16. package/dist/services/demo-rule-compiler.d.ts.map +1 -0
  17. package/dist/services/demo-rule-compiler.js +53 -0
  18. package/dist/services/demo-rule-compiler.js.map +1 -0
  19. package/dist/services/rulehost-pipeline-runner.d.ts +132 -0
  20. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -0
  21. package/dist/services/rulehost-pipeline-runner.js +376 -0
  22. package/dist/services/rulehost-pipeline-runner.js.map +1 -0
  23. package/package.json +1 -1
  24. package/scripts/llm-dogfood.ts +419 -0
  25. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +280 -0
  26. package/src/commands/runtime-activation.ts +459 -1
  27. package/src/commands/runtime-internalization-run-rulehost.ts +417 -0
  28. package/src/index.ts +60 -1
  29. package/src/services/demo-rule-compiler.ts +71 -0
  30. package/src/services/rulehost-pipeline-runner.ts +638 -0
  31. package/tests/commands/cli-command-tree.test.ts +14 -0
  32. package/tests/commands/runtime-activation.test.ts +553 -1
  33. package/tests/e2e/cross-package-acceptance.test.ts +549 -0
  34. package/tests/services/rulehost-pipeline-e2e.test.ts +477 -0
  35. package/tests/services/rulehost-pipeline-runner.test.ts +525 -0
@@ -4,8 +4,12 @@ import {
4
4
  ActivationDispatcher,
5
5
  PromptWriter,
6
6
  DeferArchiveWriter,
7
+ RuleHostWriter,
8
+ createProductionGateDeps,
7
9
  SqliteActivationStateStore,
8
10
  SqliteApprovalQueueStore,
11
+ SqlitePIArtifactStore,
12
+ isArtifactRevisionOf,
9
13
  } from '@principles/core/runtime-v2';
10
14
  import type { ActivationDecision, PIArtifactSnapshot, RolloutActivationDecision } from '@principles/core/runtime-v2';
11
15
  import type { PIArtifactRecord } from '@principles/core/runtime-v2';
@@ -146,10 +150,19 @@ export async function handleRuntimeActivationDispatch(opts: ActivationDispatchOp
146
150
 
147
151
  const activationStateStore = new SqliteActivationStateStore(stateManager.connection);
148
152
  const approvalQueueStore = new SqliteApprovalQueueStore(stateManager.connection);
153
+ // Wire all three MVP-Core writers, including RuleHostWriter for code_tool_hook.
154
+ // PRI-408: fixes P0 breakpoint where code_tool_hook channel could not activate.
149
155
  const dispatcher = new ActivationDispatcher(
150
156
  artifactReadModel,
151
157
  activationStateStore,
152
- { writers: [new PromptWriter(), new DeferArchiveWriter()], approvalQueueStore },
158
+ {
159
+ writers: [
160
+ new PromptWriter(),
161
+ new RuleHostWriter({ gateDeps: createProductionGateDeps() }),
162
+ new DeferArchiveWriter(),
163
+ ],
164
+ approvalQueueStore,
165
+ },
153
166
  );
154
167
 
155
168
  const result = await dispatcher.dispatch({
@@ -174,3 +187,448 @@ export async function handleRuntimeActivationDispatch(opts: ActivationDispatchOp
174
187
  await stateManager.close();
175
188
  }
176
189
  }
190
+
191
+ // ── Deactivate Command (PRI-408 Contract E) ─────────────────────────────────
192
+
193
+ interface ActivationDeactivateOptions {
194
+ workspace?: string;
195
+ activationId?: string;
196
+ json?: boolean;
197
+ }
198
+
199
+ export interface DeactivateResult {
200
+ ok: boolean;
201
+ activationId: string;
202
+ deactivatedAt?: string;
203
+ reason?: string;
204
+ nextAction?: string;
205
+ }
206
+
207
+ export async function handleRuntimeActivationDeactivate(opts: ActivationDeactivateOptions): Promise<void> {
208
+ if (!opts.activationId) {
209
+ const result: DeactivateResult = {
210
+ ok: false,
211
+ activationId: '',
212
+ reason: 'activation_id_required',
213
+ nextAction: 'Provide --activation-id <id> from `pd runtime activation list`',
214
+ };
215
+ if (opts.json) {
216
+ console.log(JSON.stringify(result, null, 2));
217
+ } else {
218
+ console.error(`Error: --activation-id is required`);
219
+ console.error(`Next action: ${result.nextAction}`);
220
+ }
221
+ process.exitCode = 1;
222
+ return;
223
+ }
224
+
225
+ const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
226
+ const stateManager = new RuntimeStateManager({ workspaceDir });
227
+
228
+ try {
229
+ await stateManager.initialize();
230
+ const activationStateStore = new SqliteActivationStateStore(stateManager.connection);
231
+ const deactivatedAt = new Date().toISOString();
232
+
233
+ // Idempotent deactivate: returns false if already deactivated or not found.
234
+ // Both cases are safe to call repeatedly (Contract E: rollback must be idempotent).
235
+ const success = await activationStateStore.deactivateActivation(opts.activationId, deactivatedAt);
236
+
237
+ const result: DeactivateResult = success
238
+ ? { ok: true, activationId: opts.activationId, deactivatedAt }
239
+ : {
240
+ ok: false,
241
+ activationId: opts.activationId,
242
+ reason: 'not_found_or_already_deactivated',
243
+ nextAction: 'Check activation ID with `pd runtime activation list`, or it may already be deactivated',
244
+ };
245
+
246
+ if (opts.json) {
247
+ // Strict JSON mode: exactly one parseable JSON object on stdout
248
+ console.log(JSON.stringify(result, null, 2));
249
+ } else {
250
+ if (success) {
251
+ console.log(`Deactivated: ${opts.activationId}`);
252
+ console.log(` deactivatedAt: ${deactivatedAt}`);
253
+ } else {
254
+ console.log(`Not deactivated: ${opts.activationId}`);
255
+ console.log(` reason: ${result.reason}`);
256
+ console.log(` nextAction: ${result.nextAction}`);
257
+ }
258
+ }
259
+
260
+ if (!success) {
261
+ process.exitCode = 1;
262
+ }
263
+ } catch (err: unknown) {
264
+ // P2 #5: initialize/DB exceptions must not break --json contract.
265
+ const errMsg = err instanceof Error ? err.message : String(err);
266
+ const result: DeactivateResult = {
267
+ ok: false,
268
+ activationId: opts.activationId,
269
+ reason: `initialize_failed: ${errMsg}`,
270
+ nextAction: 'Check workspace directory and DB integrity. Try `pd runtime diagnostics`.',
271
+ };
272
+ if (opts.json) {
273
+ console.log(JSON.stringify(result, null, 2));
274
+ } else {
275
+ console.error(`Error: ${result.reason}`);
276
+ console.error(`Next action: ${result.nextAction}`);
277
+ }
278
+ process.exitCode = 1;
279
+ } finally {
280
+ await stateManager.close();
281
+ }
282
+ }
283
+
284
+ // ── List Activations Command (PRI-408 Contract D — observability) ────────────
285
+
286
+ interface ActivationListOptions {
287
+ workspace?: string;
288
+ channel?: string;
289
+ includeDeactivated?: boolean;
290
+ json?: boolean;
291
+ }
292
+
293
+ export async function handleRuntimeActivationList(opts: ActivationListOptions): Promise<void> {
294
+ // P2 #5 fix: fail loud on invalid channel instead of silently listing all.
295
+ const VALID_CHANNELS = new Set(['prompt', 'code_tool_hook', undefined]);
296
+ if (opts.channel !== undefined && !VALID_CHANNELS.has(opts.channel)) {
297
+ const result = {
298
+ ok: false,
299
+ reason: `invalid_channel: ${opts.channel}`,
300
+ nextAction: 'Use one of: prompt, code_tool_hook, or omit --channel to list all',
301
+ };
302
+ if (opts.json) {
303
+ console.log(JSON.stringify(result, null, 2));
304
+ } else {
305
+ console.error(`Error: invalid channel "${opts.channel}"`);
306
+ console.error(`Next action: ${result.nextAction}`);
307
+ }
308
+ process.exitCode = 1;
309
+ return;
310
+ }
311
+
312
+ const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
313
+ const stateManager = new RuntimeStateManager({ workspaceDir });
314
+
315
+ try {
316
+ await stateManager.initialize();
317
+ const activationStateStore = new SqliteActivationStateStore(stateManager.connection);
318
+
319
+ // P2 #5 fix: pass includeDeactivated to the store so channel-specific queries
320
+ // also return deactivated records when requested. Previously the SQL hardcoded
321
+ // `WHERE deactivated_at IS NULL`, making --include-deactivated a no-op for
322
+ // channel-filtered queries.
323
+ let records;
324
+ if (opts.channel === 'prompt') {
325
+ records = await activationStateStore.listPromptActivations(opts.includeDeactivated ?? false);
326
+ } else if (opts.channel === 'code_tool_hook') {
327
+ records = await activationStateStore.listCodeToolHookActivations(opts.includeDeactivated ?? false);
328
+ } else {
329
+ records = await activationStateStore.listAllActivations();
330
+ }
331
+
332
+ // For listAllActivations, still apply the includeDeactivated filter at the
333
+ // caller level since listAllActivations() does not take the parameter.
334
+ const filtered = opts.includeDeactivated
335
+ ? records
336
+ : records.filter(r => r.deactivatedAt === null);
337
+
338
+ if (opts.json) {
339
+ // Strict JSON mode: exactly one parseable JSON object on stdout
340
+ console.log(JSON.stringify({ activations: filtered }, null, 2));
341
+ } else {
342
+ if (filtered.length === 0) {
343
+ console.log('No active activations found.');
344
+ } else {
345
+ for (const r of filtered) {
346
+ const status = r.deactivatedAt ? `[DEACTIVATED ${r.deactivatedAt}]` : '[ACTIVE]';
347
+ console.log(`${status} ${r.activationId}`);
348
+ console.log(` artifactId: ${r.artifactId}`);
349
+ console.log(` channel: ${r.channel}`);
350
+ console.log(` action: ${r.action}`);
351
+ console.log(` targetRef: ${r.targetRef}`);
352
+ console.log(` activatedAt: ${r.activatedAt}`);
353
+ console.log('');
354
+ }
355
+ }
356
+ }
357
+ } catch (err: unknown) {
358
+ // P2 #5: initialize/DB exceptions must not break --json contract.
359
+ const errMsg = err instanceof Error ? err.message : String(err);
360
+ const result = {
361
+ ok: false,
362
+ reason: `initialize_failed: ${errMsg}`,
363
+ nextAction: 'Check workspace directory and DB integrity. Try `pd runtime diagnostics`.',
364
+ };
365
+ if (opts.json) {
366
+ console.log(JSON.stringify(result, null, 2));
367
+ } else {
368
+ console.error(`Error: ${result.reason}`);
369
+ console.error(`Next action: ${result.nextAction}`);
370
+ }
371
+ process.exitCode = 1;
372
+ } finally {
373
+ await stateManager.close();
374
+ }
375
+ }
376
+
377
+ // ── Edit Pending Approval Command (P1 #2 fix — Owner edit entry point) ──────
378
+
379
+ interface ActivationEditOptions {
380
+ workspace?: string;
381
+ approvalId?: string;
382
+ newArtifactId?: string;
383
+ editReason?: string;
384
+ json?: boolean;
385
+ }
386
+
387
+ export interface EditApprovalResult {
388
+ ok: boolean;
389
+ approvalId?: string;
390
+ newArtifactId?: string;
391
+ previousArtifactId?: string;
392
+ editedAt?: string;
393
+ reason?: string;
394
+ nextAction?: string;
395
+ }
396
+
397
+ export async function handleRuntimeActivationEdit(opts: ActivationEditOptions): Promise<void> {
398
+ if (!opts.approvalId) {
399
+ const result: EditApprovalResult = {
400
+ ok: false,
401
+ reason: 'approval_id_required',
402
+ nextAction: 'Provide --approval-id <id> from Console approvals page',
403
+ };
404
+ if (opts.json) {
405
+ console.log(JSON.stringify(result, null, 2));
406
+ } else {
407
+ console.error('Error: --approval-id is required');
408
+ console.error(`Next action: ${result.nextAction}`);
409
+ }
410
+ process.exitCode = 1;
411
+ return;
412
+ }
413
+
414
+ if (!opts.newArtifactId) {
415
+ const result: EditApprovalResult = {
416
+ ok: false,
417
+ reason: 'new_artifact_id_required',
418
+ nextAction: 'Create a new artifact first (e.g. via pd candidate intake), then pass its ID with --new-artifact-id',
419
+ };
420
+ if (opts.json) {
421
+ console.log(JSON.stringify(result, null, 2));
422
+ } else {
423
+ console.error('Error: --new-artifact-id is required');
424
+ console.error(`Next action: ${result.nextAction}`);
425
+ }
426
+ process.exitCode = 1;
427
+ return;
428
+ }
429
+
430
+ if (!opts.editReason) {
431
+ const result: EditApprovalResult = {
432
+ ok: false,
433
+ reason: 'edit_reason_required',
434
+ nextAction: 'Provide --edit-reason explaining why the artifact is being revised',
435
+ };
436
+ if (opts.json) {
437
+ console.log(JSON.stringify(result, null, 2));
438
+ } else {
439
+ console.error('Error: --edit-reason is required');
440
+ console.error(`Next action: ${result.nextAction}`);
441
+ }
442
+ process.exitCode = 1;
443
+ return;
444
+ }
445
+
446
+ const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
447
+ const stateManager = new RuntimeStateManager({ workspaceDir });
448
+
449
+ try {
450
+ await stateManager.initialize();
451
+ const approvalStore = new SqliteApprovalQueueStore(stateManager.connection);
452
+ const artifactStore = new SqlitePIArtifactStore(stateManager.connection);
453
+ const now = new Date().toISOString();
454
+
455
+ // P1 #2: validate the new artifact before swapping the approval pointer.
456
+ // The new artifact must exist, be validated, and have lineage consistent
457
+ // with the original approval's artifact (same sourceTaskId).
458
+ const existingApproval = await approvalStore.getById(opts.approvalId);
459
+ if (!existingApproval) {
460
+ const result: EditApprovalResult = {
461
+ ok: false,
462
+ approvalId: opts.approvalId,
463
+ reason: 'not_found',
464
+ nextAction: 'Check the approval ID on Console approvals page',
465
+ };
466
+ if (opts.json) {
467
+ console.log(JSON.stringify(result, null, 2));
468
+ } else {
469
+ console.error(`Edit refused: ${result.reason}`);
470
+ console.error(`Next action: ${result.nextAction}`);
471
+ }
472
+ process.exitCode = 1;
473
+ await stateManager.close();
474
+ return;
475
+ }
476
+ if (existingApproval.status !== 'pending') {
477
+ const result: EditApprovalResult = {
478
+ ok: false,
479
+ approvalId: opts.approvalId,
480
+ reason: 'already_decided',
481
+ nextAction: `Approval is already decided (status: ${existingApproval.status}). Only pending approvals can be edited.`,
482
+ };
483
+ if (opts.json) {
484
+ console.log(JSON.stringify(result, null, 2));
485
+ } else {
486
+ console.error(`Edit refused: ${result.reason}`);
487
+ console.error(`Next action: ${result.nextAction}`);
488
+ }
489
+ process.exitCode = 1;
490
+ await stateManager.close();
491
+ return;
492
+ }
493
+
494
+ const newArtifact = await artifactStore.getArtifactById(opts.newArtifactId);
495
+ if (!newArtifact) {
496
+ const result: EditApprovalResult = {
497
+ ok: false,
498
+ approvalId: opts.approvalId,
499
+ reason: 'artifact_not_found',
500
+ nextAction: `Artifact ${opts.newArtifactId} does not exist. Create it first via pd candidate intake.`,
501
+ };
502
+ if (opts.json) {
503
+ console.log(JSON.stringify(result, null, 2));
504
+ } else {
505
+ console.error(`Edit refused: ${result.reason}`);
506
+ console.error(`Next action: ${result.nextAction}`);
507
+ }
508
+ process.exitCode = 1;
509
+ await stateManager.close();
510
+ return;
511
+ }
512
+ if (newArtifact.validationStatus !== 'validated') {
513
+ const result: EditApprovalResult = {
514
+ ok: false,
515
+ approvalId: opts.approvalId,
516
+ reason: `artifact_not_validated: ${newArtifact.validationStatus}`,
517
+ nextAction: `Artifact ${opts.newArtifactId} has validationStatus '${newArtifact.validationStatus}'. Run the production gate to validate it first.`,
518
+ };
519
+ if (opts.json) {
520
+ console.log(JSON.stringify(result, null, 2));
521
+ } else {
522
+ console.error(`Edit refused: ${result.reason}`);
523
+ console.error(`Next action: ${result.nextAction}`);
524
+ }
525
+ process.exitCode = 1;
526
+ await stateManager.close();
527
+ return;
528
+ }
529
+ const originalArtifact = await artifactStore.getArtifactById(existingApproval.artifactId);
530
+ if (!originalArtifact || !isArtifactRevisionOf(newArtifact, originalArtifact)) {
531
+ const result: EditApprovalResult = {
532
+ ok: false,
533
+ approvalId: opts.approvalId,
534
+ reason: 'artifact_lineage_mismatch',
535
+ nextAction: originalArtifact
536
+ ? `Artifact ${opts.newArtifactId} must reference ${originalArtifact.artifactId} or its source principle.`
537
+ : `Original artifact ${existingApproval.artifactId} is missing; restore it before editing this approval.`,
538
+ };
539
+ if (opts.json) {
540
+ console.log(JSON.stringify(result, null, 2));
541
+ } else {
542
+ console.error(`Edit refused: ${result.reason}`);
543
+ console.error(`Next action: ${result.nextAction}`);
544
+ }
545
+ process.exitCode = 1;
546
+ await stateManager.close();
547
+ return;
548
+ }
549
+
550
+ let editResult;
551
+ try {
552
+ editResult = await approvalStore.edit({
553
+ approvalId: opts.approvalId,
554
+ editedBy: 'operator',
555
+ newArtifactId: opts.newArtifactId,
556
+ editReason: opts.editReason,
557
+ now,
558
+ });
559
+ } catch (err: unknown) {
560
+ const errMsg = err instanceof Error ? err.message : String(err);
561
+ const result: EditApprovalResult = {
562
+ ok: false,
563
+ approvalId: opts.approvalId,
564
+ reason: `edit_failed: ${errMsg}`,
565
+ nextAction: 'Check workspace DB integrity and that the approval + new artifact exist',
566
+ };
567
+ if (opts.json) {
568
+ console.log(JSON.stringify(result, null, 2));
569
+ } else {
570
+ console.error(`Edit failed: ${result.reason}`);
571
+ console.error(`Next action: ${result.nextAction}`);
572
+ }
573
+ process.exitCode = 1;
574
+ return;
575
+ }
576
+
577
+ if (!editResult.ok) {
578
+ const result: EditApprovalResult = {
579
+ ok: false,
580
+ approvalId: opts.approvalId,
581
+ reason: editResult.error,
582
+ nextAction: editResult.error === 'not_found'
583
+ ? 'Check the approval ID on Console approvals page'
584
+ : `Approval is already decided (status: ${editResult.status ?? 'unknown'}). Only pending approvals can be edited.`,
585
+ };
586
+ if (opts.json) {
587
+ console.log(JSON.stringify(result, null, 2));
588
+ } else {
589
+ console.error(`Edit refused: ${result.reason}`);
590
+ console.error(`Next action: ${result.nextAction}`);
591
+ }
592
+ process.exitCode = 1;
593
+ return;
594
+ }
595
+
596
+ const result: EditApprovalResult = {
597
+ ok: true,
598
+ approvalId: editResult.record.approvalId,
599
+ newArtifactId: editResult.record.artifactId,
600
+ previousArtifactId: editResult.record.previousArtifactId ?? undefined,
601
+ editedAt: editResult.record.editedAt ?? now,
602
+ };
603
+
604
+ if (opts.json) {
605
+ console.log(JSON.stringify(result, null, 2));
606
+ } else {
607
+ console.log(`Approval edited: ${result.approvalId}`);
608
+ console.log(` newArtifactId: ${result.newArtifactId}`);
609
+ if (result.previousArtifactId) {
610
+ console.log(` previousArtifactId: ${result.previousArtifactId}`);
611
+ }
612
+ console.log(` editedAt: ${result.editedAt}`);
613
+ console.log('Next action: review the new artifact, then approve via Console or `pd runtime activation dispatch`');
614
+ }
615
+ } catch (err: unknown) {
616
+ // P2 #5: initialize/DB exceptions must not break --json contract.
617
+ const errMsg = err instanceof Error ? err.message : String(err);
618
+ const result: EditApprovalResult = {
619
+ ok: false,
620
+ approvalId: opts.approvalId,
621
+ reason: `initialize_failed: ${errMsg}`,
622
+ nextAction: 'Check workspace directory and DB integrity. Try `pd runtime diagnostics`.',
623
+ };
624
+ if (opts.json) {
625
+ console.log(JSON.stringify(result, null, 2));
626
+ } else {
627
+ console.error(`Error: ${result.reason}`);
628
+ console.error(`Next action: ${result.nextAction}`);
629
+ }
630
+ process.exitCode = 1;
631
+ } finally {
632
+ await stateManager.close();
633
+ }
634
+ }