@principles/pd-cli 1.80.0 → 1.82.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.
@@ -535,7 +535,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
535
535
  expect(text).toContain('resultRef: philosopher://run-phil-002');
536
536
  });
537
537
 
538
- it('successful dreamer + --enqueue-next returns successorTaskId', async () => {
538
+ it('auto-enqueue: successful dreamer returns successor info by default', async () => {
539
539
  mockWakeOnce.mockResolvedValue({
540
540
  decision: 'would_lease',
541
541
  taskId: 'task-dreamer-enq-001',
@@ -564,11 +564,11 @@ describe('handleRuntimeInternalizationRunOnce', () => {
564
564
 
565
565
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
566
566
  expect(output.enqueueDecision).toBe('successor_created');
567
- expect(output.successorTaskId).toBe('task-phil-enq-001');
567
+ expect(output.successorTaskIds![0]).toBe('task-phil-enq-001');
568
568
  expect(output.successorKind).toBe('philosopher');
569
569
  });
570
570
 
571
- it('repeated --enqueue-next returns existing successorTaskId', async () => {
571
+ it('auto-enqueue: repeated run returns existing successorTaskId', async () => {
572
572
  mockWakeOnce.mockResolvedValue({
573
573
  decision: 'would_lease',
574
574
  taskId: 'task-dreamer-enq-002',
@@ -597,11 +597,11 @@ describe('handleRuntimeInternalizationRunOnce', () => {
597
597
 
598
598
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
599
599
  expect(output.enqueueDecision).toBe('successor_exists');
600
- expect(output.successorTaskId).toBe('task-phil-enq-002');
600
+ expect(output.successorTaskIds![0]).toBe('task-phil-enq-002');
601
601
  expect(output.successorKind).toBe('philosopher');
602
602
  });
603
603
 
604
- it('--enqueue-next with no_successor does not set successorTaskId', async () => {
604
+ it('auto-enqueue: no_successor does not set successor info', async () => {
605
605
  mockWakeOnce.mockResolvedValue({
606
606
  decision: 'would_lease',
607
607
  taskId: 'task-dreamer-enq-003',
@@ -629,10 +629,10 @@ describe('handleRuntimeInternalizationRunOnce', () => {
629
629
 
630
630
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
631
631
  expect(output.enqueueDecision).toBe('no_successor');
632
- expect(output.successorTaskId).toBeUndefined();
632
+ expect(output.successorTaskIds).toEqual([]);
633
633
  });
634
634
 
635
- it('--enqueue-next with failed run does not call commitNextTaskProposal', async () => {
635
+ it('auto-enqueue: failed run does not call commitNextTaskProposal', async () => {
636
636
  mockWakeOnce.mockResolvedValue({
637
637
  decision: 'would_lease',
638
638
  taskId: 'task-dreamer-enq-004',
@@ -652,7 +652,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
652
652
  expect(mockCommitNextTaskProposal).not.toHaveBeenCalled();
653
653
  });
654
654
 
655
- it('--enqueue-next without --allow-test-double still blocked', async () => {
655
+ it('auto-enqueue without --allow-test-double still blocked', async () => {
656
656
  await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', enqueueNext: true });
657
657
 
658
658
  expect(process.exitCode).toBe(1);
@@ -880,7 +880,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
880
880
  expect(output.runnerResult.status).toBe('succeeded');
881
881
  });
882
882
 
883
- it('--runner scribe --enqueue-next creates artificer successor', async () => {
883
+ it('auto-enqueue: --runner scribe creates artificer successor', async () => {
884
884
  mockWakeOnce.mockResolvedValue({
885
885
  decision: 'would_lease',
886
886
  taskId: 'task-scribe-enq-001',
@@ -916,7 +916,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
916
916
 
917
917
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
918
918
  expect(output.enqueueDecision).toBe('successor_created');
919
- expect(output.successorTaskId).toBe('task-artificer-enq-001');
919
+ expect(output.successorTaskIds![0]).toBe('task-artificer-enq-001');
920
920
  expect(output.successorKind).toBe('artificer');
921
921
  });
922
922
 
@@ -979,7 +979,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
979
979
  expect(output.runnerResult.status).toBe('succeeded');
980
980
  });
981
981
 
982
- it('--runner artificer --enqueue-next returns successor decision', async () => {
982
+ it('auto-enqueue: --runner artificer returns successor decision', async () => {
983
983
  mockWakeOnce.mockResolvedValue({
984
984
  decision: 'would_lease',
985
985
  taskId: 'task-artificer-enq-001',
@@ -1022,7 +1022,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
1022
1022
 
1023
1023
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
1024
1024
  expect(output.enqueueDecision).toBe('successor_created');
1025
- expect(output.successorTaskId).toBe('task-evaluator-enq-001');
1025
+ expect(output.successorTaskIds![0]).toBe('task-evaluator-enq-001');
1026
1026
  expect(output.successorKind).toBe('evaluator');
1027
1027
  });
1028
1028
 
@@ -1112,7 +1112,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
1112
1112
  expect(output.runnerResult.status).toBe('succeeded');
1113
1113
  });
1114
1114
 
1115
- it('--runner evaluator --enqueue-next returns successor decision', async () => {
1115
+ it('auto-enqueue: --runner evaluator returns successor decision', async () => {
1116
1116
  mockWakeOnce.mockResolvedValue({
1117
1117
  decision: 'would_lease',
1118
1118
  taskId: 'task-evaluator-enq-001',
@@ -1155,7 +1155,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
1155
1155
 
1156
1156
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
1157
1157
  expect(output.enqueueDecision).toBe('successor_created');
1158
- expect(output.successorTaskId).toBe('task-rollout-reviewer-enq-001');
1158
+ expect(output.successorTaskIds![0]).toBe('task-rollout-reviewer-enq-001');
1159
1159
  expect(output.successorKind).toBe('rollout_reviewer');
1160
1160
  });
1161
1161
 
@@ -1208,7 +1208,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
1208
1208
  expect(output.runnerResult.status).toBe('succeeded');
1209
1209
  });
1210
1210
 
1211
- it('--runner rollout_reviewer --enqueue-next returns no_successor for prompt channel', async () => {
1211
+ it('auto-enqueue: --runner rollout_reviewer returns no_successor for prompt channel', async () => {
1212
1212
  mockWakeOnce.mockResolvedValue({
1213
1213
  decision: 'would_lease',
1214
1214
  taskId: 'task-rollout-reviewer-enq-001',
@@ -1315,4 +1315,276 @@ describe('handleRuntimeInternalizationRunOnce', () => {
1315
1315
  expect(output.nextAction).toBeTruthy();
1316
1316
  expect(process.exitCode).toBe(1);
1317
1317
  });
1318
+
1319
+ // === Default enqueue successor tests ===
1320
+
1321
+ it('default behavior (no --no-enqueue-next) auto-enqueues successor on runner success', async () => {
1322
+ mockWakeOnce.mockResolvedValue({
1323
+ decision: 'would_lease',
1324
+ taskId: 'task-dreamer-auto-001',
1325
+ taskKind: 'dreamer',
1326
+ });
1327
+
1328
+ mockRun.mockResolvedValue({
1329
+ status: 'succeeded',
1330
+ taskId: 'task-dreamer-auto-001',
1331
+ runId: 'run-auto-001',
1332
+ artifactId: 'pi-art-auto-001',
1333
+ resultRef: 'dreamer://run-auto-001',
1334
+ contextHash: 'ctx-auto',
1335
+ output: { valid: true, taskId: 'task-dreamer-auto-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
1336
+ attemptCount: 1,
1337
+ });
1338
+
1339
+ mockCommitNextTaskProposal.mockResolvedValue({
1340
+ decision: 'successor_created',
1341
+ sourceTaskId: 'task-dreamer-auto-001',
1342
+ successorTaskId: 'task-phil-auto-001',
1343
+ successorKind: 'philosopher',
1344
+ });
1345
+
1346
+ // No enqueueNext specified — default should auto-enqueue
1347
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: true });
1348
+
1349
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
1350
+ expect(output.successorEnqueueAttempted).toBe(true);
1351
+ expect(output.successorTasksCreated).toBe(1);
1352
+ expect(output.successorTaskIds).toContain('task-phil-auto-001');
1353
+ expect(output.enqueueDecision).toBe('successor_created');
1354
+ expect(output.successorKind).toBe('philosopher');
1355
+ expect(mockCommitNextTaskProposal).toHaveBeenCalledWith('task-dreamer-auto-001');
1356
+ });
1357
+
1358
+ it('--no-enqueue-next (enqueueNext: false) skips successor enqueue', async () => {
1359
+ mockWakeOnce.mockResolvedValue({
1360
+ decision: 'would_lease',
1361
+ taskId: 'task-dreamer-skip-001',
1362
+ taskKind: 'dreamer',
1363
+ });
1364
+
1365
+ mockRun.mockResolvedValue({
1366
+ status: 'succeeded',
1367
+ taskId: 'task-dreamer-skip-001',
1368
+ runId: 'run-skip-001',
1369
+ artifactId: 'pi-art-skip-001',
1370
+ resultRef: 'dreamer://run-skip-001',
1371
+ contextHash: 'ctx-skip',
1372
+ output: { valid: true, taskId: 'task-dreamer-skip-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
1373
+ attemptCount: 1,
1374
+ });
1375
+
1376
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, enqueueNext: false, json: true });
1377
+
1378
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
1379
+ expect(output.successorEnqueueAttempted).toBe(false);
1380
+ expect(output.nextAction).toContain('--no-enqueue-next');
1381
+ expect(mockCommitNextTaskProposal).not.toHaveBeenCalled();
1382
+ });
1383
+
1384
+ it('successor enqueue failure outputs partial_success with reason and nextAction', async () => {
1385
+ mockWakeOnce.mockResolvedValue({
1386
+ decision: 'would_lease',
1387
+ taskId: 'task-dreamer-fail-001',
1388
+ taskKind: 'dreamer',
1389
+ });
1390
+
1391
+ mockRun.mockResolvedValue({
1392
+ status: 'succeeded',
1393
+ taskId: 'task-dreamer-fail-001',
1394
+ runId: 'run-fail-001',
1395
+ artifactId: 'pi-art-fail-001',
1396
+ resultRef: 'dreamer://run-fail-001',
1397
+ contextHash: 'ctx-fail',
1398
+ output: { valid: true, taskId: 'task-dreamer-fail-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
1399
+ attemptCount: 1,
1400
+ });
1401
+
1402
+ mockCommitNextTaskProposal.mockRejectedValue(new Error('database locked'));
1403
+
1404
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: true });
1405
+
1406
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
1407
+ expect(output.decision).toBe('partial_success');
1408
+ expect(output.successorEnqueueAttempted).toBe(true);
1409
+ expect(output.enqueueDecision).toBe('enqueue_failed');
1410
+ expect(output.enqueueReason).toContain('database locked');
1411
+ expect(output.nextAction).toContain('enqueue-successors');
1412
+ expect(output.successorTasksCreated).toBe(0);
1413
+ expect(output.successorTaskIds).toEqual([]);
1414
+ });
1415
+
1416
+ it('runner failure never enqueues successor (default behavior)', async () => {
1417
+ mockWakeOnce.mockResolvedValue({
1418
+ decision: 'would_lease',
1419
+ taskId: 'task-dreamer-nofail-001',
1420
+ taskKind: 'dreamer',
1421
+ });
1422
+
1423
+ mockRun.mockResolvedValue({
1424
+ status: 'failed',
1425
+ taskId: 'task-dreamer-nofail-001',
1426
+ errorCategory: 'execution_failed',
1427
+ failureReason: 'Runtime unavailable',
1428
+ attemptCount: 1,
1429
+ });
1430
+
1431
+ // Default behavior (no --no-enqueue-next)
1432
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: true });
1433
+
1434
+ expect(mockCommitNextTaskProposal).not.toHaveBeenCalled();
1435
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
1436
+ expect(output.successorEnqueueAttempted).toBeUndefined();
1437
+ });
1438
+
1439
+ it('JSON output is single parseable JSON with successor fields', async () => {
1440
+ mockWakeOnce.mockResolvedValue({
1441
+ decision: 'would_lease',
1442
+ taskId: 'task-dreamer-json-001',
1443
+ taskKind: 'dreamer',
1444
+ });
1445
+
1446
+ mockRun.mockResolvedValue({
1447
+ status: 'succeeded',
1448
+ taskId: 'task-dreamer-json-001',
1449
+ runId: 'run-json-001',
1450
+ artifactId: 'pi-art-json-001',
1451
+ resultRef: 'dreamer://run-json-001',
1452
+ contextHash: 'ctx-json',
1453
+ output: { valid: true, taskId: 'task-dreamer-json-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
1454
+ attemptCount: 1,
1455
+ });
1456
+
1457
+ mockCommitNextTaskProposal.mockResolvedValue({
1458
+ decision: 'successor_created',
1459
+ sourceTaskId: 'task-dreamer-json-001',
1460
+ successorTaskId: 'task-phil-json-001',
1461
+ successorKind: 'philosopher',
1462
+ });
1463
+
1464
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: true });
1465
+
1466
+ const rawOutput = consoleLogSpy.mock.calls[0][0];
1467
+ // Must be single parseable JSON
1468
+ const output = JSON.parse(rawOutput);
1469
+ expect(output).toHaveProperty('successorEnqueueAttempted');
1470
+ expect(output).toHaveProperty('successorTasksCreated');
1471
+ expect(output).toHaveProperty('successorTaskIds');
1472
+ // nextAction may be undefined when successor is successfully created
1473
+ // but must be present on partial_success / no_successor / skipped
1474
+ });
1475
+
1476
+ it('text output for auto-enqueue shows successor info', async () => {
1477
+ mockWakeOnce.mockResolvedValue({
1478
+ decision: 'would_lease',
1479
+ taskId: 'task-dreamer-text-001',
1480
+ taskKind: 'dreamer',
1481
+ });
1482
+
1483
+ mockRun.mockResolvedValue({
1484
+ status: 'succeeded',
1485
+ taskId: 'task-dreamer-text-001',
1486
+ runId: 'run-text-001',
1487
+ artifactId: 'pi-art-text-001',
1488
+ resultRef: 'dreamer://run-text-001',
1489
+ contextHash: 'ctx-text',
1490
+ output: { valid: true, taskId: 'task-dreamer-text-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
1491
+ attemptCount: 1,
1492
+ });
1493
+
1494
+ mockCommitNextTaskProposal.mockResolvedValue({
1495
+ decision: 'successor_created',
1496
+ sourceTaskId: 'task-dreamer-text-001',
1497
+ successorTaskId: 'task-phil-text-001',
1498
+ successorKind: 'philosopher',
1499
+ });
1500
+
1501
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: false });
1502
+
1503
+ const text = consoleLogSpy.mock.calls.map((c: string[]) => c[0]).join('\n');
1504
+ expect(text).toContain('successor: task-phil-text-001');
1505
+ expect(text).toContain('enqueue_attempted: true');
1506
+ expect(text).toContain('successors_created: 1');
1507
+ });
1508
+ });
1509
+
1510
+ // === Commander parser-level tests for --no-enqueue-next ===
1511
+ // These tests verify that Commander correctly maps --no-enqueue-next to opts.enqueueNext.
1512
+ // They exercise the real Commander parsing path, NOT the handler directly.
1513
+ // See ERR-063: previous code used opts.noEnqueueNext (always undefined) instead of opts.enqueueNext.
1514
+
1515
+ describe('Commander --no-enqueue-next parser wiring', () => {
1516
+ function buildRunOnceCommand(capturedOpts: Record<string, unknown>) {
1517
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1518
+ const { Command } = require('commander') as typeof import('commander');
1519
+ const program = new Command();
1520
+ program.exitOverride(); // prevent process.exit during tests
1521
+
1522
+ const internalizationCmd = program.command('internalization');
1523
+ internalizationCmd
1524
+ .command('run-once')
1525
+ .description('Wake-and-run: lease the next PI task and execute it')
1526
+ .option('-w, --workspace <path>', 'Workspace directory')
1527
+ .option('--runner <kind>', 'Runner kind', 'dreamer')
1528
+ .option('--runtime <kind>', 'Runtime adapter kind', 'config')
1529
+ .option('--allow-test-double', 'Acknowledge test-double runtime')
1530
+ .option('--no-enqueue-next', 'Skip successor enqueue after successful runner')
1531
+ .option('--timeout-ms <ms>', 'Runner timeout', parseInt)
1532
+ .option('--json', 'Output raw JSON')
1533
+ .action(async (opts) => {
1534
+ Object.assign(capturedOpts, opts);
1535
+ });
1536
+
1537
+ return program;
1538
+ }
1539
+
1540
+ it('with --no-enqueue-next, Commander sets enqueueNext=false', async () => {
1541
+ const captured: Record<string, unknown> = {};
1542
+ const program = buildRunOnceCommand(captured);
1543
+
1544
+ await program.parseAsync([
1545
+ 'node', 'pd', 'internalization', 'run-once',
1546
+ '--no-enqueue-next',
1547
+ '--workspace', '/tmp/test',
1548
+ '--runtime', 'test-double',
1549
+ '--allow-test-double',
1550
+ '--json',
1551
+ ]);
1552
+
1553
+ expect(captured).toHaveProperty('enqueueNext', false);
1554
+ expect(captured).not.toHaveProperty('noEnqueueNext');
1555
+ });
1556
+
1557
+ it('without --no-enqueue-next, Commander sets enqueueNext=true (default)', async () => {
1558
+ const captured: Record<string, unknown> = {};
1559
+ const program = buildRunOnceCommand(captured);
1560
+
1561
+ await program.parseAsync([
1562
+ 'node', 'pd', 'internalization', 'run-once',
1563
+ '--workspace', '/tmp/test',
1564
+ '--runtime', 'test-double',
1565
+ '--allow-test-double',
1566
+ '--json',
1567
+ ]);
1568
+
1569
+ expect(captured).toHaveProperty('enqueueNext', true);
1570
+ });
1571
+
1572
+ it('opts has no noEnqueueNext property regardless of flag presence', async () => {
1573
+ const capturedWith: Record<string, unknown> = {};
1574
+ const programWith = buildRunOnceCommand(capturedWith);
1575
+ await programWith.parseAsync([
1576
+ 'node', 'pd', 'internalization', 'run-once',
1577
+ '--no-enqueue-next', '--workspace', '/tmp/test',
1578
+ ]);
1579
+
1580
+ const capturedWithout: Record<string, unknown> = {};
1581
+ const programWithout = buildRunOnceCommand(capturedWithout);
1582
+ await programWithout.parseAsync([
1583
+ 'node', 'pd', 'internalization', 'run-once',
1584
+ '--workspace', '/tmp/test',
1585
+ ]);
1586
+
1587
+ expect(capturedWith).not.toHaveProperty('noEnqueueNext');
1588
+ expect(capturedWithout).not.toHaveProperty('noEnqueueNext');
1589
+ });
1318
1590
  });