agentic-orchestrator 0.1.26 → 0.1.28

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 (172) hide show
  1. package/AGENTS.md +2 -2
  2. package/CLAUDE.md +2 -2
  3. package/README.md +47 -14
  4. package/agentic/orchestrator/agents.yaml +13 -0
  5. package/agentic/orchestrator/policy.yaml +3 -0
  6. package/agentic/orchestrator/schemas/agents.schema.json +76 -0
  7. package/agentic/orchestrator/schemas/policy.schema.json +16 -0
  8. package/agentic/orchestrator/schemas/policy.user.schema.json +16 -0
  9. package/agentic/orchestrator/schemas/state.schema.json +53 -0
  10. package/apps/control-plane/src/application/configuration-service.ts +181 -0
  11. package/apps/control-plane/src/application/kernel-tool-wiring.ts +292 -0
  12. package/apps/control-plane/src/application/services/checkpoint-service.ts +523 -0
  13. package/apps/control-plane/src/application/services/feature-send-message-service.ts +132 -0
  14. package/apps/control-plane/src/application/services/patch-service.ts +29 -5
  15. package/apps/control-plane/src/application/services/repo-operations-service.ts +276 -0
  16. package/apps/control-plane/src/application/services/worktree-watchdog-service.ts +156 -0
  17. package/apps/control-plane/src/cli/cli-argument-parser.ts +12 -0
  18. package/apps/control-plane/src/cli/help-command-handler.ts +17 -0
  19. package/apps/control-plane/src/cli/init-command-handler.ts +31 -0
  20. package/apps/control-plane/src/cli/resume-command-handler.ts +31 -4
  21. package/apps/control-plane/src/cli/rollback-command-handler.ts +217 -0
  22. package/apps/control-plane/src/cli/run-command-handler.ts +8 -0
  23. package/apps/control-plane/src/cli/types.ts +3 -0
  24. package/apps/control-plane/src/core/kernel-types.ts +55 -0
  25. package/apps/control-plane/src/core/kernel.ts +61 -878
  26. package/apps/control-plane/src/core/tool-caller.ts +10 -0
  27. package/apps/control-plane/src/core/utils/field-readers.ts +38 -0
  28. package/apps/control-plane/src/core/utils/index-normalizer.ts +119 -0
  29. package/apps/control-plane/src/core/utils/path-normalizers.ts +22 -0
  30. package/apps/control-plane/src/interfaces/cli/bootstrap.ts +15 -0
  31. package/apps/control-plane/src/providers/api-worker-provider.ts +14 -12
  32. package/apps/control-plane/src/providers/cli-worker-provider.ts +82 -12
  33. package/apps/control-plane/src/providers/providers.ts +45 -24
  34. package/apps/control-plane/src/providers/worker-provider-factory.ts +36 -1
  35. package/apps/control-plane/src/supervisor/run-coordinator.ts +91 -36
  36. package/apps/control-plane/src/supervisor/runtime.ts +107 -1
  37. package/apps/control-plane/src/supervisor/types.ts +9 -0
  38. package/apps/control-plane/src/supervisor/worker-decision-loop.ts +253 -14
  39. package/apps/control-plane/test/checkpoint-service.spec.ts +537 -0
  40. package/apps/control-plane/test/cli-helpers.spec.ts +28 -0
  41. package/apps/control-plane/test/cli.unit.spec.ts +52 -0
  42. package/apps/control-plane/test/configuration-service.spec.ts +466 -0
  43. package/apps/control-plane/test/dashboard-api.integration.spec.ts +537 -0
  44. package/apps/control-plane/test/dashboard-client.spec.ts +233 -0
  45. package/apps/control-plane/test/feature-send-message-service.spec.ts +314 -0
  46. package/apps/control-plane/test/init-wizard.spec.ts +35 -0
  47. package/apps/control-plane/test/path-normalizers.spec.ts +41 -0
  48. package/apps/control-plane/test/repo-operations-service.spec.ts +339 -0
  49. package/apps/control-plane/test/resume-command.spec.ts +33 -0
  50. package/apps/control-plane/test/review-workspace-logic.spec.ts +130 -0
  51. package/apps/control-plane/test/rollback-command.spec.ts +208 -0
  52. package/apps/control-plane/test/run-coordinator.spec.ts +119 -0
  53. package/apps/control-plane/test/worker-decision-loop.spec.ts +209 -0
  54. package/apps/control-plane/test/worker-provider-adapters.spec.ts +102 -0
  55. package/apps/control-plane/test/worker-provider-factory.spec.ts +14 -0
  56. package/apps/control-plane/test/worktree-watchdog-service.spec.ts +147 -0
  57. package/config/agentic/orchestrator/agents.yaml +13 -0
  58. package/dist/apps/control-plane/application/configuration-service.d.ts +19 -0
  59. package/dist/apps/control-plane/application/configuration-service.js +123 -0
  60. package/dist/apps/control-plane/application/configuration-service.js.map +1 -0
  61. package/dist/apps/control-plane/application/kernel-tool-wiring.d.ts +39 -0
  62. package/dist/apps/control-plane/application/kernel-tool-wiring.js +38 -0
  63. package/dist/apps/control-plane/application/kernel-tool-wiring.js.map +1 -0
  64. package/dist/apps/control-plane/application/services/checkpoint-service.d.ts +84 -0
  65. package/dist/apps/control-plane/application/services/checkpoint-service.js +367 -0
  66. package/dist/apps/control-plane/application/services/checkpoint-service.js.map +1 -0
  67. package/dist/apps/control-plane/application/services/feature-send-message-service.d.ts +25 -0
  68. package/dist/apps/control-plane/application/services/feature-send-message-service.js +105 -0
  69. package/dist/apps/control-plane/application/services/feature-send-message-service.js.map +1 -0
  70. package/dist/apps/control-plane/application/services/patch-service.d.ts +6 -0
  71. package/dist/apps/control-plane/application/services/patch-service.js +11 -2
  72. package/dist/apps/control-plane/application/services/patch-service.js.map +1 -1
  73. package/dist/apps/control-plane/application/services/repo-operations-service.d.ts +70 -0
  74. package/dist/apps/control-plane/application/services/repo-operations-service.js +213 -0
  75. package/dist/apps/control-plane/application/services/repo-operations-service.js.map +1 -0
  76. package/dist/apps/control-plane/application/services/worktree-watchdog-service.d.ts +23 -0
  77. package/dist/apps/control-plane/application/services/worktree-watchdog-service.js +119 -0
  78. package/dist/apps/control-plane/application/services/worktree-watchdog-service.js.map +1 -0
  79. package/dist/apps/control-plane/cli/cli-argument-parser.js +12 -0
  80. package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
  81. package/dist/apps/control-plane/cli/help-command-handler.js +17 -0
  82. package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
  83. package/dist/apps/control-plane/cli/init-command-handler.js +23 -0
  84. package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
  85. package/dist/apps/control-plane/cli/resume-command-handler.js +25 -5
  86. package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
  87. package/dist/apps/control-plane/cli/rollback-command-handler.d.ts +6 -0
  88. package/dist/apps/control-plane/cli/rollback-command-handler.js +177 -0
  89. package/dist/apps/control-plane/cli/rollback-command-handler.js.map +1 -0
  90. package/dist/apps/control-plane/cli/run-command-handler.js +7 -1
  91. package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
  92. package/dist/apps/control-plane/cli/types.d.ts +3 -0
  93. package/dist/apps/control-plane/cli/types.js +1 -0
  94. package/dist/apps/control-plane/cli/types.js.map +1 -1
  95. package/dist/apps/control-plane/core/configuration-service.d.ts +25 -0
  96. package/dist/apps/control-plane/core/configuration-service.js +130 -0
  97. package/dist/apps/control-plane/core/configuration-service.js.map +1 -0
  98. package/dist/apps/control-plane/core/kernel-tool-wiring.d.ts +50 -0
  99. package/dist/apps/control-plane/core/kernel-tool-wiring.js +44 -0
  100. package/dist/apps/control-plane/core/kernel-tool-wiring.js.map +1 -0
  101. package/dist/apps/control-plane/core/kernel-types.d.ts +48 -0
  102. package/dist/apps/control-plane/core/kernel-types.js +2 -0
  103. package/dist/apps/control-plane/core/kernel-types.js.map +1 -0
  104. package/dist/apps/control-plane/core/kernel.d.ts +17 -48
  105. package/dist/apps/control-plane/core/kernel.js +44 -539
  106. package/dist/apps/control-plane/core/kernel.js.map +1 -1
  107. package/dist/apps/control-plane/core/tool-caller.d.ts +10 -0
  108. package/dist/apps/control-plane/core/utils/error-normalizer.d.ts +2 -0
  109. package/dist/apps/control-plane/core/utils/error-normalizer.js +51 -0
  110. package/dist/apps/control-plane/core/utils/error-normalizer.js.map +1 -0
  111. package/dist/apps/control-plane/core/utils/field-readers.d.ts +9 -0
  112. package/dist/apps/control-plane/core/utils/field-readers.js +30 -0
  113. package/dist/apps/control-plane/core/utils/field-readers.js.map +1 -0
  114. package/dist/apps/control-plane/core/utils/index-normalizer.d.ts +7 -0
  115. package/dist/apps/control-plane/core/utils/index-normalizer.js +92 -0
  116. package/dist/apps/control-plane/core/utils/index-normalizer.js.map +1 -0
  117. package/dist/apps/control-plane/core/utils/path-normalizers.d.ts +2 -0
  118. package/dist/apps/control-plane/core/utils/path-normalizers.js +17 -0
  119. package/dist/apps/control-plane/core/utils/path-normalizers.js.map +1 -0
  120. package/dist/apps/control-plane/interfaces/cli/bootstrap.js +13 -1
  121. package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
  122. package/dist/apps/control-plane/providers/api-worker-provider.d.ts +4 -13
  123. package/dist/apps/control-plane/providers/api-worker-provider.js +10 -0
  124. package/dist/apps/control-plane/providers/api-worker-provider.js.map +1 -1
  125. package/dist/apps/control-plane/providers/cli-worker-provider.d.ts +11 -13
  126. package/dist/apps/control-plane/providers/cli-worker-provider.js +64 -0
  127. package/dist/apps/control-plane/providers/cli-worker-provider.js.map +1 -1
  128. package/dist/apps/control-plane/providers/providers.d.ts +31 -24
  129. package/dist/apps/control-plane/providers/providers.js +10 -0
  130. package/dist/apps/control-plane/providers/providers.js.map +1 -1
  131. package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +11 -0
  132. package/dist/apps/control-plane/providers/worker-provider-factory.js +20 -1
  133. package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -1
  134. package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +3 -0
  135. package/dist/apps/control-plane/supervisor/run-coordinator.js +81 -33
  136. package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
  137. package/dist/apps/control-plane/supervisor/runtime.d.ts +8 -1
  138. package/dist/apps/control-plane/supervisor/runtime.js +90 -0
  139. package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
  140. package/dist/apps/control-plane/supervisor/types.d.ts +11 -0
  141. package/dist/apps/control-plane/supervisor/types.js.map +1 -1
  142. package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +21 -1
  143. package/dist/apps/control-plane/supervisor/worker-decision-loop.js +207 -13
  144. package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
  145. package/package.json +1 -1
  146. package/packages/web-dashboard/package.json +2 -0
  147. package/packages/web-dashboard/src/app/analytics/page.tsx +83 -2
  148. package/packages/web-dashboard/src/app/api/actions/route.ts +92 -1
  149. package/packages/web-dashboard/src/app/api/analytics/route.ts +5 -2
  150. package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/[checkpointId]/diff/route.ts +43 -0
  151. package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/compare/route.ts +45 -0
  152. package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/stream/route.ts +170 -0
  153. package/packages/web-dashboard/src/app/api/features/[id]/file-diff/route.ts +144 -0
  154. package/packages/web-dashboard/src/app/api/features/[id]/log-stream/route.ts +167 -0
  155. package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/[filename]/route.ts +65 -0
  156. package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/route.ts +63 -0
  157. package/packages/web-dashboard/src/app/api/features/[id]/timeline/route.ts +60 -0
  158. package/packages/web-dashboard/src/app/feature/[id]/page.tsx +32 -11
  159. package/packages/web-dashboard/src/app/globals.css +2 -0
  160. package/packages/web-dashboard/src/components/detail-panel.tsx +483 -0
  161. package/packages/web-dashboard/src/components/review-workspace.tsx +1162 -0
  162. package/packages/web-dashboard/src/lib/aop-client.ts +725 -0
  163. package/packages/web-dashboard/src/lib/review-contracts.ts +182 -0
  164. package/packages/web-dashboard/src/lib/review-workspace-logic.ts +64 -0
  165. package/packages/web-dashboard/src/lib/types.ts +131 -0
  166. package/packages/web-dashboard/src/styles/dashboard.module.css +333 -0
  167. package/spec-files/completed/agentic_orchestrator_execution_mode_spec.md +1905 -0
  168. package/spec-files/outstanding/agentic_orchestrator_runtime_inspection_spec.md +940 -0
  169. package/spec-files/outstanding/execution_mode_critical_review.md +355 -0
  170. package/spec-files/outstanding/shadow_workspace_implementation_spec.md +1271 -0
  171. package/spec-files/outstanding/shadow_workspace_spec_summary.md +222 -0
  172. package/spec-files/progress.md +269 -1
@@ -3,9 +3,14 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { afterEach, describe, expect, it } from 'vitest';
5
5
  import {
6
+ compareFeatureCheckpoints,
6
7
  readDashboardStatus,
8
+ readFeatureCheckpointDiff,
9
+ readFeatureCheckpoints,
7
10
  readFeatureCost,
11
+ readFeatureDetail,
8
12
  readFeatureLog,
13
+ readInteractivePerformanceMetrics,
9
14
  readFeatureQaTestIndex,
10
15
  readFeatureReviewBrief,
11
16
  } from '@/lib/aop-client.js';
@@ -359,4 +364,232 @@ describe('dashboard aop client mapping', () => {
359
364
  contract_risk_summary: 'Risk',
360
365
  });
361
366
  });
367
+
368
+ it('GIVEN_state_with_execution_mode_and_checkpoints_WHEN_readFeatureDetail_THEN_returns_timeline_with_validation_status', async () => {
369
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-dash-client-'));
370
+ tempRoots.push(repoRoot);
371
+ const featureId = 'feature_checkpoints';
372
+ const featureRoot = path.join(repoRoot, '.aop', 'features', featureId);
373
+ await fs.mkdir(path.join(featureRoot, 'evidence'), { recursive: true });
374
+ await fs.writeFile(
375
+ path.join(featureRoot, 'state.md'),
376
+ [
377
+ '---',
378
+ `feature_id: ${featureId}`,
379
+ 'version: 1',
380
+ 'status: qa',
381
+ 'branch: feature/feature_checkpoints',
382
+ 'worktree_path: /tmp/worktrees/feature_checkpoints',
383
+ 'execution_mode: interactive',
384
+ 'checkpoints:',
385
+ ' - checkpoint_id: checkpoint-002',
386
+ ' timestamp: 2026-03-06T10:02:00.000Z',
387
+ ' files_changed: [src/a.ts]',
388
+ ' validation_status: invalid',
389
+ ' violations: [src/a.ts: forbidden_area]',
390
+ ' severity: error',
391
+ ' diff_snapshot: .aop/features/feature_checkpoints/checkpoints/checkpoint-002.diff',
392
+ ' - checkpoint_id: checkpoint-001',
393
+ ' timestamp: 2026-03-06T10:01:00.000Z',
394
+ ' files_changed: [src/b.ts]',
395
+ ' validation_status: valid',
396
+ ' violations: []',
397
+ ' diff_snapshot: .aop/features/feature_checkpoints/checkpoints/checkpoint-001.diff',
398
+ '---',
399
+ '',
400
+ ].join('\n'),
401
+ 'utf8',
402
+ );
403
+ await fs.writeFile(
404
+ path.join(featureRoot, 'plan.json'),
405
+ JSON.stringify({ plan_version: 1 }),
406
+ 'utf8',
407
+ );
408
+ await fs.writeFile(path.join(featureRoot, 'evidence', 'diff.patch'), '', 'utf8');
409
+
410
+ const detail = await readFeatureDetail(featureId, repoRoot);
411
+ expect(detail).not.toBeNull();
412
+ expect(detail?.execution_mode).toBe('interactive');
413
+ expect(detail?.checkpoints).toHaveLength(2);
414
+ expect(detail?.checkpoints?.[0]).toMatchObject({
415
+ checkpoint_id: 'checkpoint-002',
416
+ validation_status: 'invalid',
417
+ severity: 'error',
418
+ });
419
+ expect(detail?.checkpoints?.[1]).toMatchObject({
420
+ checkpoint_id: 'checkpoint-001',
421
+ validation_status: 'valid',
422
+ });
423
+ });
424
+
425
+ it('GIVEN_checkpoint_state_and_diff_artifacts_WHEN_read_checkpoint_helpers_THEN_returns_snapshot_and_comparison', async () => {
426
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-dash-client-'));
427
+ tempRoots.push(repoRoot);
428
+ const featureId = 'feature_checkpoint_compare';
429
+ const featureRoot = path.join(repoRoot, '.aop', 'features', featureId);
430
+ const checkpointDir = path.join(featureRoot, 'checkpoints');
431
+ await fs.mkdir(checkpointDir, { recursive: true });
432
+ await fs.writeFile(
433
+ path.join(featureRoot, 'state.md'),
434
+ [
435
+ '---',
436
+ `feature_id: ${featureId}`,
437
+ 'version: 1',
438
+ 'status: qa',
439
+ 'execution_mode: interactive',
440
+ 'checkpoints:',
441
+ ' - checkpoint_id: checkpoint-002',
442
+ ' timestamp: 2026-03-06T10:02:00.000Z',
443
+ ' files_changed: [src/a.ts, src/b.ts]',
444
+ ' validation_status: valid',
445
+ ' violations: []',
446
+ ' diff_snapshot: .aop/features/feature_checkpoint_compare/checkpoints/checkpoint-002.diff',
447
+ ' - checkpoint_id: checkpoint-001',
448
+ ' timestamp: 2026-03-06T10:01:00.000Z',
449
+ ' files_changed: [src/a.ts]',
450
+ ' validation_status: valid',
451
+ ' violations: []',
452
+ ' diff_snapshot: .aop/features/feature_checkpoint_compare/checkpoints/checkpoint-001.diff',
453
+ '---',
454
+ '',
455
+ ].join('\n'),
456
+ 'utf8',
457
+ );
458
+ await fs.writeFile(
459
+ path.join(checkpointDir, 'checkpoint-001.diff'),
460
+ [
461
+ 'diff --git a/src/a.ts b/src/a.ts',
462
+ 'index 1111111..2222222 100644',
463
+ '--- a/src/a.ts',
464
+ '+++ b/src/a.ts',
465
+ '@@ -1 +1 @@',
466
+ '-const value = 1;',
467
+ '+const value = 2;',
468
+ ].join('\n'),
469
+ 'utf8',
470
+ );
471
+ await fs.writeFile(
472
+ path.join(checkpointDir, 'checkpoint-002.diff'),
473
+ [
474
+ 'diff --git a/src/a.ts b/src/a.ts',
475
+ 'index 1111111..3333333 100644',
476
+ '--- a/src/a.ts',
477
+ '+++ b/src/a.ts',
478
+ '@@ -1 +1 @@',
479
+ '-const value = 1;',
480
+ '+const value = 3;',
481
+ 'diff --git a/src/b.ts b/src/b.ts',
482
+ 'new file mode 100644',
483
+ '--- /dev/null',
484
+ '+++ b/src/b.ts',
485
+ '@@ -0,0 +1 @@',
486
+ '+export const b = true;',
487
+ ].join('\n'),
488
+ 'utf8',
489
+ );
490
+
491
+ const checkpoints = await readFeatureCheckpoints(featureId, repoRoot);
492
+ expect(checkpoints?.execution_mode).toBe('interactive');
493
+ expect(checkpoints?.checkpoints).toHaveLength(2);
494
+
495
+ const snapshot = await readFeatureCheckpointDiff(featureId, 'checkpoint-001', repoRoot);
496
+ expect(snapshot?.checkpoint.checkpoint_id).toBe('checkpoint-001');
497
+ expect(snapshot?.diff).toContain('diff --git');
498
+
499
+ const comparison = await compareFeatureCheckpoints(
500
+ featureId,
501
+ 'checkpoint-001',
502
+ 'checkpoint-002',
503
+ repoRoot,
504
+ );
505
+ expect(comparison).not.toBeNull();
506
+ expect(comparison?.summary.changed).toBeGreaterThanOrEqual(1);
507
+ expect(comparison?.summary.added).toBeGreaterThanOrEqual(1);
508
+ });
509
+
510
+ it('GIVEN_interactive_metrics_and_worker_events_WHEN_readInteractivePerformanceMetrics_THEN_returns_aggregated_snapshot', async () => {
511
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-dash-client-'));
512
+ tempRoots.push(repoRoot);
513
+ await fs.mkdir(path.join(repoRoot, '.aop', 'analytics'), { recursive: true });
514
+ await fs.mkdir(path.join(repoRoot, '.aop', 'runtime', 'worker-events'), { recursive: true });
515
+ await fs.writeFile(
516
+ path.join(repoRoot, '.aop', 'analytics', 'interactive-mode.json'),
517
+ JSON.stringify(
518
+ {
519
+ schema_version: 1,
520
+ updated_at: '2026-03-06T00:00:00.000Z',
521
+ totals: {
522
+ checkpoint_count: 3,
523
+ valid_count: 2,
524
+ invalid_count: 1,
525
+ skipped_count: 0,
526
+ files_changed_total: 9,
527
+ },
528
+ histories: {
529
+ checkpoint_latency_ms: [200, 300, 500],
530
+ validation_latency_ms: [90, 120, 150],
531
+ diff_capture_latency_ms: [40, 50, 60],
532
+ },
533
+ latest: {
534
+ recorded_at: '2026-03-06T00:00:00.000Z',
535
+ feature_id: 'feature_metrics',
536
+ checkpoint_id: 'checkpoint-003',
537
+ trigger: 'final',
538
+ validation_status: 'valid',
539
+ checkpoint_latency_ms: 500,
540
+ validation_latency_ms: 150,
541
+ diff_capture_latency_ms: 60,
542
+ files_changed: 4,
543
+ },
544
+ },
545
+ null,
546
+ 2,
547
+ ),
548
+ 'utf8',
549
+ );
550
+ await fs.writeFile(
551
+ path.join(repoRoot, '.aop', 'runtime', 'worker-events', 'run-test.jsonl'),
552
+ [
553
+ JSON.stringify({
554
+ ts: '2026-03-06T00:00:00.000Z',
555
+ role: 'builder',
556
+ output_types: ['PATCH'],
557
+ patch_count: 1,
558
+ plan_submission_count: 0,
559
+ request_count: 1,
560
+ note_count: 0,
561
+ valid: true,
562
+ error_code: null,
563
+ provider: 'codex',
564
+ model: 'gpt-5',
565
+ execution_mode: 'interactive',
566
+ elapsed_ms: 1200,
567
+ }),
568
+ JSON.stringify({
569
+ ts: '2026-03-06T00:00:10.000Z',
570
+ role: 'builder',
571
+ output_types: ['PATCH'],
572
+ patch_count: 1,
573
+ plan_submission_count: 0,
574
+ request_count: 4,
575
+ note_count: 0,
576
+ valid: true,
577
+ error_code: null,
578
+ provider: 'codex',
579
+ model: 'gpt-5',
580
+ execution_mode: 'deterministic',
581
+ elapsed_ms: 2400,
582
+ }),
583
+ ].join('\n'),
584
+ 'utf8',
585
+ );
586
+
587
+ const metrics = await readInteractivePerformanceMetrics(repoRoot);
588
+ expect(metrics.checkpoint_count).toBe(3);
589
+ expect(metrics.avg_checkpoint_latency_ms).toBeCloseTo(333.3, 1);
590
+ expect(metrics.execution_modes.interactive_iterations).toBe(1);
591
+ expect(metrics.execution_modes.deterministic_iterations).toBe(1);
592
+ expect(metrics.execution_modes.context_request_reduction_ratio).toBeCloseTo(0.75, 2);
593
+ expect(metrics.latest?.checkpoint_id).toBe('checkpoint-003');
594
+ });
362
595
  });
@@ -0,0 +1,314 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ FeatureSendMessageService,
4
+ type FeatureSendMessageServicePort,
5
+ } from '../src/application/services/feature-send-message-service.js';
6
+ import { STATUS, GATE_RESULT } from '../src/core/constants.js';
7
+ import type { RuntimeSessionsSnapshot } from '../src/core/runtime-sessions.js';
8
+
9
+ function makeSendMessageMock() {
10
+ return vi.fn(async (_sessionId: string, _message: string) => {});
11
+ }
12
+
13
+ function makeRuntimeSessions(
14
+ overrides: Partial<RuntimeSessionsSnapshot> = {},
15
+ ): RuntimeSessionsSnapshot {
16
+ const now = new Date().toISOString();
17
+ return {
18
+ run_id: 'run1',
19
+ orchestrator_session_id: 'orch-session',
20
+ provider: 'codex',
21
+ model: 'gpt-4',
22
+ provider_config_ref_hash: 'hash',
23
+ owner_instance_id: 'owner1',
24
+ lease_id: 'lease1',
25
+ started_at: now,
26
+ last_heartbeat_at: now,
27
+ lease_expires_at: now,
28
+ feature_sessions: {
29
+ feat1: {
30
+ planner_session_id: 'planner-sess',
31
+ builder_session_id: 'builder-sess',
32
+ qa_session_id: 'qa-sess',
33
+ },
34
+ },
35
+ ...overrides,
36
+ };
37
+ }
38
+
39
+ function makePort(
40
+ overrides: Partial<FeatureSendMessageServicePort> = {},
41
+ ): FeatureSendMessageServicePort {
42
+ return {
43
+ readState: vi.fn(async () => ({ frontMatter: { status: '' }, body: '', raw: '' })),
44
+ getRuntimeSessions: vi.fn(async () => makeRuntimeSessions()),
45
+ getProvider: vi.fn(() => ({
46
+ sendMessage: makeSendMessageMock(),
47
+ })),
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ describe('FeatureSendMessageService', () => {
53
+ let sendMessageMock: ReturnType<typeof makeSendMessageMock>;
54
+
55
+ beforeEach(() => {
56
+ sendMessageMock = makeSendMessageMock();
57
+ vi.clearAllMocks();
58
+ });
59
+
60
+ it('GIVEN_null_featureId_WHEN_featureSendMessage_THEN_throws_INVALID_ARGUMENT', async () => {
61
+ const service = new FeatureSendMessageService(makePort());
62
+ await expect(service.featureSendMessage(null, 'hello')).rejects.toMatchObject({
63
+ normalizedResponse: expect.objectContaining({ ok: false }),
64
+ });
65
+ });
66
+
67
+ it('GIVEN_null_message_WHEN_featureSendMessage_THEN_throws_INVALID_ARGUMENT', async () => {
68
+ const service = new FeatureSendMessageService(makePort());
69
+ await expect(service.featureSendMessage('feat1', null)).rejects.toMatchObject({
70
+ normalizedResponse: expect.objectContaining({ ok: false }),
71
+ });
72
+ });
73
+
74
+ it('GIVEN_no_feature_session_WHEN_featureSendMessage_THEN_throws_session_not_found', async () => {
75
+ const port = makePort({
76
+ getRuntimeSessions: vi.fn(async () => makeRuntimeSessions({ feature_sessions: {} })),
77
+ });
78
+ const service = new FeatureSendMessageService(port);
79
+ await expect(service.featureSendMessage('feat1', 'hello')).rejects.toMatchObject({
80
+ normalizedResponse: { ok: false, error: { code: 'session_not_found' } },
81
+ });
82
+ });
83
+
84
+ it('GIVEN_no_provider_WHEN_featureSendMessage_THEN_throws_provider_unsupported', async () => {
85
+ const port = makePort({
86
+ getProvider: vi.fn(() => null),
87
+ });
88
+ const service = new FeatureSendMessageService(port);
89
+ await expect(service.featureSendMessage('feat1', 'hello')).rejects.toMatchObject({
90
+ normalizedResponse: { ok: false, error: { code: 'provider_unsupported' } },
91
+ });
92
+ });
93
+
94
+ it('GIVEN_status_PLANNING_WHEN_featureSendMessage_THEN_routes_to_planner', async () => {
95
+ const port = makePort({
96
+ readState: vi.fn(async () => ({
97
+ frontMatter: { status: STATUS.PLANNING },
98
+ body: '',
99
+ raw: '',
100
+ })),
101
+ getProvider: vi.fn(() => ({ sendMessage: sendMessageMock })),
102
+ });
103
+ const service = new FeatureSendMessageService(port);
104
+ const result = (await service.featureSendMessage('feat1', 'hello')) as Record<string, unknown>;
105
+ expect(result.target_role).toBe('planner');
106
+ expect(result.session_id).toBe('planner-sess');
107
+ expect(sendMessageMock).toHaveBeenCalledWith('planner-sess', 'hello');
108
+ });
109
+
110
+ it('GIVEN_status_BUILDING_WHEN_featureSendMessage_THEN_routes_to_builder', async () => {
111
+ const port = makePort({
112
+ readState: vi.fn(async () => ({
113
+ frontMatter: { status: STATUS.BUILDING },
114
+ body: '',
115
+ raw: '',
116
+ })),
117
+ getProvider: vi.fn(() => ({ sendMessage: sendMessageMock })),
118
+ });
119
+ const service = new FeatureSendMessageService(port);
120
+ const result = (await service.featureSendMessage('feat1', 'hello')) as Record<string, unknown>;
121
+ expect(result.target_role).toBe('builder');
122
+ expect(result.session_id).toBe('builder-sess');
123
+ });
124
+
125
+ it('GIVEN_status_QA_WHEN_featureSendMessage_THEN_routes_to_qa', async () => {
126
+ const port = makePort({
127
+ readState: vi.fn(async () => ({ frontMatter: { status: STATUS.QA }, body: '', raw: '' })),
128
+ getProvider: vi.fn(() => ({ sendMessage: sendMessageMock })),
129
+ });
130
+ const service = new FeatureSendMessageService(port);
131
+ const result = (await service.featureSendMessage('feat1', 'hello')) as Record<string, unknown>;
132
+ expect(result.target_role).toBe('qa');
133
+ expect(result.session_id).toBe('qa-sess');
134
+ });
135
+
136
+ it('GIVEN_status_READY_TO_MERGE_WHEN_featureSendMessage_THEN_routes_to_qa', async () => {
137
+ const port = makePort({
138
+ readState: vi.fn(async () => ({
139
+ frontMatter: { status: STATUS.READY_TO_MERGE },
140
+ body: '',
141
+ raw: '',
142
+ })),
143
+ getProvider: vi.fn(() => ({ sendMessage: sendMessageMock })),
144
+ });
145
+ const service = new FeatureSendMessageService(port);
146
+ const result = (await service.featureSendMessage('feat1', 'hello')) as Record<string, unknown>;
147
+ expect(result.target_role).toBe('qa');
148
+ });
149
+
150
+ it('GIVEN_status_BLOCKED_fast_fail_full_not_fail_WHEN_featureSendMessage_THEN_routes_to_builder', async () => {
151
+ const port = makePort({
152
+ readState: vi.fn(async () => ({
153
+ frontMatter: {
154
+ status: STATUS.BLOCKED,
155
+ gates: { fast: GATE_RESULT.FAIL, full: GATE_RESULT.PASS },
156
+ },
157
+ body: '',
158
+ raw: '',
159
+ })),
160
+ getProvider: vi.fn(() => ({ sendMessage: sendMessageMock })),
161
+ });
162
+ const service = new FeatureSendMessageService(port);
163
+ const result = (await service.featureSendMessage('feat1', 'hello')) as Record<string, unknown>;
164
+ expect(result.target_role).toBe('builder');
165
+ expect(result.session_id).toBe('builder-sess');
166
+ });
167
+
168
+ it('GIVEN_status_BLOCKED_full_fail_WHEN_featureSendMessage_THEN_routes_to_qa', async () => {
169
+ const port = makePort({
170
+ readState: vi.fn(async () => ({
171
+ frontMatter: {
172
+ status: STATUS.BLOCKED,
173
+ gates: { fast: GATE_RESULT.FAIL, full: GATE_RESULT.FAIL },
174
+ },
175
+ body: '',
176
+ raw: '',
177
+ })),
178
+ getProvider: vi.fn(() => ({ sendMessage: sendMessageMock })),
179
+ });
180
+ const service = new FeatureSendMessageService(port);
181
+ const result = (await service.featureSendMessage('feat1', 'hello')) as Record<string, unknown>;
182
+ expect(result.target_role).toBe('qa');
183
+ expect(result.session_id).toBe('qa-sess');
184
+ });
185
+
186
+ it('GIVEN_status_BLOCKED_only_fast_fail_no_full_WHEN_featureSendMessage_THEN_routes_to_builder', async () => {
187
+ const port = makePort({
188
+ readState: vi.fn(async () => ({
189
+ frontMatter: {
190
+ status: STATUS.BLOCKED,
191
+ gates: { fast: GATE_RESULT.FAIL },
192
+ },
193
+ body: '',
194
+ raw: '',
195
+ })),
196
+ getProvider: vi.fn(() => ({ sendMessage: sendMessageMock })),
197
+ });
198
+ const service = new FeatureSendMessageService(port);
199
+ const result = (await service.featureSendMessage('feat1', 'hello')) as Record<string, unknown>;
200
+ expect(result.target_role).toBe('builder');
201
+ });
202
+
203
+ it('GIVEN_unknown_status_WHEN_featureSendMessage_THEN_routes_to_orchestrator', async () => {
204
+ const port = makePort({
205
+ readState: vi.fn(async () => ({
206
+ frontMatter: { status: 'unknown_status' },
207
+ body: '',
208
+ raw: '',
209
+ })),
210
+ getProvider: vi.fn(() => ({ sendMessage: sendMessageMock })),
211
+ });
212
+ const service = new FeatureSendMessageService(port);
213
+ const result = (await service.featureSendMessage('feat1', 'hello')) as Record<string, unknown>;
214
+ expect(result.target_role).toBe('orchestrator');
215
+ expect(result.session_id).toBe('orch-session');
216
+ });
217
+
218
+ it('GIVEN_planner_session_unassigned_WHEN_featureSendMessage_THEN_falls_back_to_orchestrator', async () => {
219
+ const port = makePort({
220
+ readState: vi.fn(async () => ({
221
+ frontMatter: { status: STATUS.PLANNING },
222
+ body: '',
223
+ raw: '',
224
+ })),
225
+ getRuntimeSessions: vi.fn(async () =>
226
+ makeRuntimeSessions({
227
+ feature_sessions: {
228
+ feat1: {
229
+ planner_session_id: 'unassigned',
230
+ builder_session_id: 'builder-sess',
231
+ qa_session_id: 'qa-sess',
232
+ },
233
+ },
234
+ }),
235
+ ),
236
+ getProvider: vi.fn(() => ({ sendMessage: sendMessageMock })),
237
+ });
238
+ const service = new FeatureSendMessageService(port);
239
+ const result = (await service.featureSendMessage('feat1', 'hello')) as Record<string, unknown>;
240
+ expect(result.target_role).toBe('orchestrator');
241
+ expect(result.session_id).toBe('orch-session');
242
+ });
243
+
244
+ it('GIVEN_builder_session_unknown_WHEN_featureSendMessage_THEN_falls_back_to_orchestrator', async () => {
245
+ const port = makePort({
246
+ readState: vi.fn(async () => ({
247
+ frontMatter: { status: STATUS.BUILDING },
248
+ body: '',
249
+ raw: '',
250
+ })),
251
+ getRuntimeSessions: vi.fn(async () =>
252
+ makeRuntimeSessions({
253
+ feature_sessions: {
254
+ feat1: {
255
+ planner_session_id: 'planner-sess',
256
+ builder_session_id: 'unknown',
257
+ qa_session_id: 'qa-sess',
258
+ },
259
+ },
260
+ }),
261
+ ),
262
+ getProvider: vi.fn(() => ({ sendMessage: sendMessageMock })),
263
+ });
264
+ const service = new FeatureSendMessageService(port);
265
+ const result = (await service.featureSendMessage('feat1', 'hello')) as Record<string, unknown>;
266
+ expect(result.target_role).toBe('orchestrator');
267
+ });
268
+
269
+ it('GIVEN_successful_send_WHEN_featureSendMessage_THEN_returns_delivery_receipt', async () => {
270
+ const port = makePort({
271
+ readState: vi.fn(async () => ({
272
+ frontMatter: { status: STATUS.BUILDING },
273
+ body: '',
274
+ raw: '',
275
+ })),
276
+ getProvider: vi.fn(() => ({ sendMessage: sendMessageMock })),
277
+ });
278
+ const service = new FeatureSendMessageService(port);
279
+ const result = (await service.featureSendMessage('feat1', 'hello')) as Record<string, unknown>;
280
+ expect(result.delivered).toBe(true);
281
+ expect(result.feature_id).toBe('feat1');
282
+ });
283
+
284
+ it('GIVEN_provider_without_sendMessage_WHEN_featureSendMessage_THEN_throws_provider_unsupported', async () => {
285
+ const port = makePort({
286
+ getProvider: vi.fn(() => ({})),
287
+ });
288
+ const service = new FeatureSendMessageService(port);
289
+ await expect(service.featureSendMessage('feat1', 'hello')).rejects.toMatchObject({
290
+ normalizedResponse: { ok: false, error: { code: 'provider_unsupported' } },
291
+ });
292
+ });
293
+
294
+ it('GIVEN_provider_with_getSessionInfo_active_true_WHEN_waiting_THEN_resolves_quickly', async () => {
295
+ const getSessionInfoMock = vi.fn(async (_sessionId: string) => ({
296
+ active: true,
297
+ provider: 'codex',
298
+ }));
299
+ const port = makePort({
300
+ readState: vi.fn(async () => ({
301
+ frontMatter: { status: STATUS.BUILDING },
302
+ body: '',
303
+ raw: '',
304
+ })),
305
+ getProvider: vi.fn(() => ({
306
+ sendMessage: sendMessageMock,
307
+ getSessionInfo: getSessionInfoMock,
308
+ })),
309
+ });
310
+ const service = new FeatureSendMessageService(port);
311
+ const result = (await service.featureSendMessage('feat1', 'hello')) as Record<string, unknown>;
312
+ expect(result.delivered).toBe(true);
313
+ });
314
+ });
@@ -97,6 +97,9 @@ describe('InitCommandHandler', () => {
97
97
  expect(agentsContent).toContain('roles:');
98
98
  expect(agentsContent).toContain('default_provider: custom');
99
99
  expect(agentsContent).toContain('default_model: local-default');
100
+ expect(agentsContent).toContain('execution_mode: deterministic');
101
+ expect(agentsContent).toContain('interactive:');
102
+ expect(agentsContent).toContain('checkpoint_interval_ms: 30000');
100
103
  expect(agentsContent).not.toContain('provider_config_env:');
101
104
 
102
105
  const adaptersContent = await fs.readFile(
@@ -351,6 +354,7 @@ describe('InitCommandHandler', () => {
351
354
  );
352
355
  expect(agentsContent).toContain('default_provider: claude');
353
356
  expect(agentsContent).toContain('default_model: sonnet-4.5');
357
+ expect(agentsContent).toContain('execution_mode: deterministic');
354
358
  expect(agentsContent).not.toContain('provider_config_env:');
355
359
 
356
360
  const adaptersContent = await fs.readFile(
@@ -392,6 +396,37 @@ describe('InitCommandHandler', () => {
392
396
  'utf8',
393
397
  );
394
398
  expect(agentsContent).not.toContain('provider_config_env:');
399
+ expect(agentsContent).toContain('execution_mode: deterministic');
400
+ });
401
+
402
+ it('GIVEN_execution_mode_prompt_answered_interactive_WHEN_init_runs_THEN_agents_yaml_sets_interactive_runtime_mode', async () => {
403
+ await fs.mkdir(path.join(cwd, '.git'), { recursive: true });
404
+ await fs.writeFile(path.join(cwd, '.git', 'config'), '[core]\n', 'utf8');
405
+ execFileMock.mockResolvedValue({ stdout: 'origin/main\n', stderr: '' });
406
+
407
+ const prompts = makePromptFactory([
408
+ 'main',
409
+ 'codex',
410
+ 'codex-default',
411
+ 'github',
412
+ '3',
413
+ '3000',
414
+ 'none',
415
+ 'vitest',
416
+ 'yes',
417
+ 'interactive',
418
+ ]);
419
+ const handler = new InitCommandHandler(cwd, prompts.factory);
420
+ const result = await handler.execute({ auto: false });
421
+
422
+ expect(result.ok).toBe(true);
423
+ const agentsContent = await fs.readFile(
424
+ path.join(cwd, 'config', 'agentic', 'orchestrator', 'agents.yaml'),
425
+ 'utf8',
426
+ );
427
+ expect(agentsContent).toContain('execution_mode: interactive');
428
+ expect(agentsContent).toContain('interactive:');
429
+ expect(agentsContent).toContain('shadow_workspace:');
395
430
  });
396
431
 
397
432
  it('GIVEN_api_backed_mode_with_existing_env_var_WHEN_init_runs_THEN_agents_yaml_uses_that_env_var', async () => {
@@ -0,0 +1,41 @@
1
+ import path from 'node:path';
2
+ import { describe, expect, it } from 'vitest';
3
+ import {
4
+ normalizeFromWorktree,
5
+ normalizeRepoPathForState,
6
+ } from '../src/core/utils/path-normalizers.js';
7
+
8
+ describe('path-normalizers', () => {
9
+ it('normalizeRepoPathForState returns dot when absolute path is repo root', () => {
10
+ const repoRoot = '/repo';
11
+ expect(normalizeRepoPathForState(repoRoot, repoRoot)).toBe('.');
12
+ });
13
+
14
+ it('normalizeRepoPathForState returns repo-relative path for nested file', () => {
15
+ const repoRoot = '/repo';
16
+ const absolutePath = path.join(repoRoot, 'apps', 'control-plane', 'src', 'index.ts');
17
+ expect(normalizeRepoPathForState(repoRoot, absolutePath)).toBe(
18
+ 'apps/control-plane/src/index.ts',
19
+ );
20
+ });
21
+
22
+ it('normalizeFromWorktree keeps relative path when target is inside worktree', () => {
23
+ const repoRoot = '/repo';
24
+ const worktreePath = '/repo/.worktrees/feature-a';
25
+ const repoRelativeFromWorktree = '.worktrees/feature-a/apps/control-plane/src/aop.ts';
26
+
27
+ expect(normalizeFromWorktree(worktreePath, repoRoot, repoRelativeFromWorktree)).toBe(
28
+ 'apps/control-plane/src/aop.ts',
29
+ );
30
+ });
31
+
32
+ it('normalizeFromWorktree falls back to repo-relative when target is outside worktree', () => {
33
+ const repoRoot = '/repo';
34
+ const worktreePath = '/repo/.worktrees/feature-a';
35
+ const repoRelativeFromWorktree = 'README.md';
36
+
37
+ expect(normalizeFromWorktree(worktreePath, repoRoot, repoRelativeFromWorktree)).toBe(
38
+ 'README.md',
39
+ );
40
+ });
41
+ });