@principles/pd-cli 1.73.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 (298) hide show
  1. package/README.md +90 -0
  2. package/dist/commands/artifact.d.ts +14 -0
  3. package/dist/commands/artifact.d.ts.map +1 -0
  4. package/dist/commands/artifact.js +67 -0
  5. package/dist/commands/artifact.js.map +1 -0
  6. package/dist/commands/candidate.d.ts +83 -0
  7. package/dist/commands/candidate.d.ts.map +1 -0
  8. package/dist/commands/candidate.js +891 -0
  9. package/dist/commands/candidate.js.map +1 -0
  10. package/dist/commands/central-sync.d.ts +10 -0
  11. package/dist/commands/central-sync.d.ts.map +1 -0
  12. package/dist/commands/central-sync.js +32 -0
  13. package/dist/commands/central-sync.js.map +1 -0
  14. package/dist/commands/console.d.ts +9 -0
  15. package/dist/commands/console.d.ts.map +1 -0
  16. package/dist/commands/console.js +114 -0
  17. package/dist/commands/console.js.map +1 -0
  18. package/dist/commands/context.d.ts +7 -0
  19. package/dist/commands/context.d.ts.map +1 -0
  20. package/dist/commands/context.js +55 -0
  21. package/dist/commands/context.js.map +1 -0
  22. package/dist/commands/demo-story-a.d.ts +12 -0
  23. package/dist/commands/demo-story-a.d.ts.map +1 -0
  24. package/dist/commands/demo-story-a.js +175 -0
  25. package/dist/commands/demo-story-a.js.map +1 -0
  26. package/dist/commands/diagnose.d.ts +35 -0
  27. package/dist/commands/diagnose.d.ts.map +1 -0
  28. package/dist/commands/diagnose.js +390 -0
  29. package/dist/commands/diagnose.js.map +1 -0
  30. package/dist/commands/evolution-tasks-list.d.ts +15 -0
  31. package/dist/commands/evolution-tasks-list.d.ts.map +1 -0
  32. package/dist/commands/evolution-tasks-list.js +34 -0
  33. package/dist/commands/evolution-tasks-list.js.map +1 -0
  34. package/dist/commands/evolution-tasks-show.d.ts +14 -0
  35. package/dist/commands/evolution-tasks-show.d.ts.map +1 -0
  36. package/dist/commands/evolution-tasks-show.js +52 -0
  37. package/dist/commands/evolution-tasks-show.js.map +1 -0
  38. package/dist/commands/flow.d.ts +7 -0
  39. package/dist/commands/flow.d.ts.map +1 -0
  40. package/dist/commands/flow.js +57 -0
  41. package/dist/commands/flow.js.map +1 -0
  42. package/dist/commands/health.d.ts +16 -0
  43. package/dist/commands/health.d.ts.map +1 -0
  44. package/dist/commands/health.js +150 -0
  45. package/dist/commands/health.js.map +1 -0
  46. package/dist/commands/history.d.ts +11 -0
  47. package/dist/commands/history.d.ts.map +1 -0
  48. package/dist/commands/history.js +50 -0
  49. package/dist/commands/history.js.map +1 -0
  50. package/dist/commands/legacy-cleanup.d.ts +27 -0
  51. package/dist/commands/legacy-cleanup.d.ts.map +1 -0
  52. package/dist/commands/legacy-cleanup.js +171 -0
  53. package/dist/commands/legacy-cleanup.js.map +1 -0
  54. package/dist/commands/legacy-import.d.ts +7 -0
  55. package/dist/commands/legacy-import.d.ts.map +1 -0
  56. package/dist/commands/legacy-import.js +86 -0
  57. package/dist/commands/legacy-import.js.map +1 -0
  58. package/dist/commands/pain-record.d.ts +10 -0
  59. package/dist/commands/pain-record.d.ts.map +1 -0
  60. package/dist/commands/pain-record.js +162 -0
  61. package/dist/commands/pain-record.js.map +1 -0
  62. package/dist/commands/proven-channel-baseline.d.ts +12 -0
  63. package/dist/commands/proven-channel-baseline.d.ts.map +1 -0
  64. package/dist/commands/proven-channel-baseline.js +97 -0
  65. package/dist/commands/proven-channel-baseline.js.map +1 -0
  66. package/dist/commands/remediation-output.d.ts +40 -0
  67. package/dist/commands/remediation-output.d.ts.map +1 -0
  68. package/dist/commands/remediation-output.js +23 -0
  69. package/dist/commands/remediation-output.js.map +1 -0
  70. package/dist/commands/run.d.ts +10 -0
  71. package/dist/commands/run.d.ts.map +1 -0
  72. package/dist/commands/run.js +68 -0
  73. package/dist/commands/run.js.map +1 -0
  74. package/dist/commands/runtime-activation.d.ts +11 -0
  75. package/dist/commands/runtime-activation.d.ts.map +1 -0
  76. package/dist/commands/runtime-activation.js +150 -0
  77. package/dist/commands/runtime-activation.js.map +1 -0
  78. package/dist/commands/runtime-canary.d.ts +30 -0
  79. package/dist/commands/runtime-canary.d.ts.map +1 -0
  80. package/dist/commands/runtime-canary.js +343 -0
  81. package/dist/commands/runtime-canary.js.map +1 -0
  82. package/dist/commands/runtime-diagnostics-export.d.ts +20 -0
  83. package/dist/commands/runtime-diagnostics-export.d.ts.map +1 -0
  84. package/dist/commands/runtime-diagnostics-export.js +177 -0
  85. package/dist/commands/runtime-diagnostics-export.js.map +1 -0
  86. package/dist/commands/runtime-features.d.ts +26 -0
  87. package/dist/commands/runtime-features.d.ts.map +1 -0
  88. package/dist/commands/runtime-features.js +70 -0
  89. package/dist/commands/runtime-features.js.map +1 -0
  90. package/dist/commands/runtime-gfi-snapshot.d.ts +7 -0
  91. package/dist/commands/runtime-gfi-snapshot.d.ts.map +1 -0
  92. package/dist/commands/runtime-gfi-snapshot.js +101 -0
  93. package/dist/commands/runtime-gfi-snapshot.js.map +1 -0
  94. package/dist/commands/runtime-health-snapshot.d.ts +7 -0
  95. package/dist/commands/runtime-health-snapshot.d.ts.map +1 -0
  96. package/dist/commands/runtime-health-snapshot.js +93 -0
  97. package/dist/commands/runtime-health-snapshot.js.map +1 -0
  98. package/dist/commands/runtime-idle-trigger.d.ts +12 -0
  99. package/dist/commands/runtime-idle-trigger.d.ts.map +1 -0
  100. package/dist/commands/runtime-idle-trigger.js +102 -0
  101. package/dist/commands/runtime-idle-trigger.js.map +1 -0
  102. package/dist/commands/runtime-internalization-enqueue-successors.d.ts +9 -0
  103. package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -0
  104. package/dist/commands/runtime-internalization-enqueue-successors.js +393 -0
  105. package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -0
  106. package/dist/commands/runtime-internalization-integrity-repair.d.ts +9 -0
  107. package/dist/commands/runtime-internalization-integrity-repair.d.ts.map +1 -0
  108. package/dist/commands/runtime-internalization-integrity-repair.js +54 -0
  109. package/dist/commands/runtime-internalization-integrity-repair.js.map +1 -0
  110. package/dist/commands/runtime-internalization-integrity.d.ts +7 -0
  111. package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -0
  112. package/dist/commands/runtime-internalization-integrity.js +53 -0
  113. package/dist/commands/runtime-internalization-integrity.js.map +1 -0
  114. package/dist/commands/runtime-internalization-queue.d.ts +7 -0
  115. package/dist/commands/runtime-internalization-queue.d.ts.map +1 -0
  116. package/dist/commands/runtime-internalization-queue.js +85 -0
  117. package/dist/commands/runtime-internalization-queue.js.map +1 -0
  118. package/dist/commands/runtime-internalization-run-once.d.ts +12 -0
  119. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -0
  120. package/dist/commands/runtime-internalization-run-once.js +546 -0
  121. package/dist/commands/runtime-internalization-run-once.js.map +1 -0
  122. package/dist/commands/runtime-internalization-wake-once.d.ts +8 -0
  123. package/dist/commands/runtime-internalization-wake-once.d.ts.map +1 -0
  124. package/dist/commands/runtime-internalization-wake-once.js +72 -0
  125. package/dist/commands/runtime-internalization-wake-once.js.map +1 -0
  126. package/dist/commands/runtime-pain-flood-simulation.d.ts +10 -0
  127. package/dist/commands/runtime-pain-flood-simulation.d.ts.map +1 -0
  128. package/dist/commands/runtime-pain-flood-simulation.js +104 -0
  129. package/dist/commands/runtime-pain-flood-simulation.js.map +1 -0
  130. package/dist/commands/runtime-pruning.d.ts +45 -0
  131. package/dist/commands/runtime-pruning.d.ts.map +1 -0
  132. package/dist/commands/runtime-pruning.js +355 -0
  133. package/dist/commands/runtime-pruning.js.map +1 -0
  134. package/dist/commands/runtime-recovery.d.ts +9 -0
  135. package/dist/commands/runtime-recovery.d.ts.map +1 -0
  136. package/dist/commands/runtime-recovery.js +94 -0
  137. package/dist/commands/runtime-recovery.js.map +1 -0
  138. package/dist/commands/runtime-synthetic-baseline.d.ts +7 -0
  139. package/dist/commands/runtime-synthetic-baseline.d.ts.map +1 -0
  140. package/dist/commands/runtime-synthetic-baseline.js +59 -0
  141. package/dist/commands/runtime-synthetic-baseline.js.map +1 -0
  142. package/dist/commands/runtime-uat.d.ts +52 -0
  143. package/dist/commands/runtime-uat.d.ts.map +1 -0
  144. package/dist/commands/runtime-uat.js +274 -0
  145. package/dist/commands/runtime-uat.js.map +1 -0
  146. package/dist/commands/runtime.d.ts +20 -0
  147. package/dist/commands/runtime.d.ts.map +1 -0
  148. package/dist/commands/runtime.js +256 -0
  149. package/dist/commands/runtime.js.map +1 -0
  150. package/dist/commands/samples-list.d.ts +11 -0
  151. package/dist/commands/samples-list.d.ts.map +1 -0
  152. package/dist/commands/samples-list.js +37 -0
  153. package/dist/commands/samples-list.js.map +1 -0
  154. package/dist/commands/samples-review.d.ts +14 -0
  155. package/dist/commands/samples-review.d.ts.map +1 -0
  156. package/dist/commands/samples-review.js +22 -0
  157. package/dist/commands/samples-review.js.map +1 -0
  158. package/dist/commands/task.d.ts +14 -0
  159. package/dist/commands/task.d.ts.map +1 -0
  160. package/dist/commands/task.js +92 -0
  161. package/dist/commands/task.js.map +1 -0
  162. package/dist/commands/trace.d.ts +19 -0
  163. package/dist/commands/trace.d.ts.map +1 -0
  164. package/dist/commands/trace.js +154 -0
  165. package/dist/commands/trace.js.map +1 -0
  166. package/dist/commands/trajectory.d.ts +11 -0
  167. package/dist/commands/trajectory.d.ts.map +1 -0
  168. package/dist/commands/trajectory.js +47 -0
  169. package/dist/commands/trajectory.js.map +1 -0
  170. package/dist/index.d.ts +9 -0
  171. package/dist/index.d.ts.map +1 -0
  172. package/dist/index.js +736 -0
  173. package/dist/index.js.map +1 -0
  174. package/dist/legacy/legacy-import.d.ts +15 -0
  175. package/dist/legacy/legacy-import.d.ts.map +1 -0
  176. package/dist/legacy/legacy-import.js +141 -0
  177. package/dist/legacy/legacy-import.js.map +1 -0
  178. package/dist/legacy/session-history-import.d.ts +26 -0
  179. package/dist/legacy/session-history-import.d.ts.map +1 -0
  180. package/dist/legacy/session-history-import.js +151 -0
  181. package/dist/legacy/session-history-import.js.map +1 -0
  182. package/dist/principle-tree-ledger-adapter.d.ts +12 -0
  183. package/dist/principle-tree-ledger-adapter.d.ts.map +1 -0
  184. package/dist/principle-tree-ledger-adapter.js +12 -0
  185. package/dist/principle-tree-ledger-adapter.js.map +1 -0
  186. package/dist/resolve-workspace.d.ts +12 -0
  187. package/dist/resolve-workspace.d.ts.map +1 -0
  188. package/dist/resolve-workspace.js +20 -0
  189. package/dist/resolve-workspace.js.map +1 -0
  190. package/dist/services/demo-story-a-runner.d.ts +8 -0
  191. package/dist/services/demo-story-a-runner.d.ts.map +1 -0
  192. package/dist/services/demo-story-a-runner.js +369 -0
  193. package/dist/services/demo-story-a-runner.js.map +1 -0
  194. package/dist/services/feature-flag-loader.d.ts +6 -0
  195. package/dist/services/feature-flag-loader.d.ts.map +1 -0
  196. package/dist/services/feature-flag-loader.js +54 -0
  197. package/dist/services/feature-flag-loader.js.map +1 -0
  198. package/dist/services/pain-flood-simulation-runner.d.ts +10 -0
  199. package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -0
  200. package/dist/services/pain-flood-simulation-runner.js +289 -0
  201. package/dist/services/pain-flood-simulation-runner.js.map +1 -0
  202. package/dist/services/proven-channel-baseline-runner.d.ts +12 -0
  203. package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -0
  204. package/dist/services/proven-channel-baseline-runner.js +114 -0
  205. package/dist/services/proven-channel-baseline-runner.js.map +1 -0
  206. package/dist/services/synthetic-baseline-runner.d.ts +8 -0
  207. package/dist/services/synthetic-baseline-runner.d.ts.map +1 -0
  208. package/dist/services/synthetic-baseline-runner.js +251 -0
  209. package/dist/services/synthetic-baseline-runner.js.map +1 -0
  210. package/package.json +35 -0
  211. package/src/commands/artifact.ts +82 -0
  212. package/src/commands/candidate.ts +1117 -0
  213. package/src/commands/central-sync.ts +44 -0
  214. package/src/commands/console.ts +121 -0
  215. package/src/commands/context.ts +72 -0
  216. package/src/commands/demo-story-a.ts +195 -0
  217. package/src/commands/diagnose.ts +452 -0
  218. package/src/commands/evolution-tasks-list.ts +44 -0
  219. package/src/commands/evolution-tasks-show.ts +60 -0
  220. package/src/commands/flow.ts +60 -0
  221. package/src/commands/health.ts +189 -0
  222. package/src/commands/history.ts +63 -0
  223. package/src/commands/legacy-cleanup.ts +206 -0
  224. package/src/commands/legacy-import.ts +104 -0
  225. package/src/commands/pain-record.ts +167 -0
  226. package/src/commands/proven-channel-baseline.ts +113 -0
  227. package/src/commands/remediation-output.ts +66 -0
  228. package/src/commands/run.ts +89 -0
  229. package/src/commands/runtime-activation.ts +176 -0
  230. package/src/commands/runtime-canary.ts +371 -0
  231. package/src/commands/runtime-diagnostics-export.ts +229 -0
  232. package/src/commands/runtime-features.ts +103 -0
  233. package/src/commands/runtime-gfi-snapshot.ts +135 -0
  234. package/src/commands/runtime-health-snapshot.ts +106 -0
  235. package/src/commands/runtime-internalization-enqueue-successors.ts +479 -0
  236. package/src/commands/runtime-internalization-integrity-repair.ts +69 -0
  237. package/src/commands/runtime-internalization-integrity.ts +63 -0
  238. package/src/commands/runtime-internalization-queue.ts +106 -0
  239. package/src/commands/runtime-internalization-run-once.ts +658 -0
  240. package/src/commands/runtime-internalization-wake-once.ts +87 -0
  241. package/src/commands/runtime-pain-flood-simulation.ts +121 -0
  242. package/src/commands/runtime-pruning.ts +438 -0
  243. package/src/commands/runtime-recovery.ts +107 -0
  244. package/src/commands/runtime-synthetic-baseline.ts +70 -0
  245. package/src/commands/runtime-uat.ts +339 -0
  246. package/src/commands/runtime.ts +281 -0
  247. package/src/commands/samples-list.ts +43 -0
  248. package/src/commands/samples-review.ts +32 -0
  249. package/src/commands/task.ts +130 -0
  250. package/src/commands/trace.ts +174 -0
  251. package/src/commands/trajectory.ts +64 -0
  252. package/src/index.ts +829 -0
  253. package/src/legacy/legacy-import.ts +179 -0
  254. package/src/legacy/session-history-import.ts +231 -0
  255. package/src/principle-tree-ledger-adapter.ts +13 -0
  256. package/src/resolve-workspace.ts +20 -0
  257. package/src/services/demo-story-a-runner.ts +472 -0
  258. package/src/services/feature-flag-loader.ts +73 -0
  259. package/src/services/pain-flood-simulation-runner.ts +354 -0
  260. package/src/services/proven-channel-baseline-runner.ts +150 -0
  261. package/src/services/synthetic-baseline-runner.ts +291 -0
  262. package/tests/commands/candidate-audit-repair.test.ts +338 -0
  263. package/tests/commands/candidate-intake.test.ts +589 -0
  264. package/tests/commands/candidate-internalization-backfill.test.ts +480 -0
  265. package/tests/commands/candidate-internalize.test.ts +272 -0
  266. package/tests/commands/candidate-route.test.ts +328 -0
  267. package/tests/commands/candidate-show.test.ts +95 -0
  268. package/tests/commands/cli-command-tree.test.ts +64 -0
  269. package/tests/commands/context.test.ts +114 -0
  270. package/tests/commands/demo-story-a.test.ts +255 -0
  271. package/tests/commands/diagnose.test.ts +792 -0
  272. package/tests/commands/health.test.ts +330 -0
  273. package/tests/commands/pain-record.test.ts +316 -0
  274. package/tests/commands/plugin-config-resolution-cutover.test.ts +220 -0
  275. package/tests/commands/proven-channel-baseline.test.ts +441 -0
  276. package/tests/commands/runtime-activation.test.ts +168 -0
  277. package/tests/commands/runtime-canary.test.ts +369 -0
  278. package/tests/commands/runtime-diagnostics-export.test.ts +170 -0
  279. package/tests/commands/runtime-features.test.ts +114 -0
  280. package/tests/commands/runtime-health-snapshot.test.ts +357 -0
  281. package/tests/commands/runtime-internalization-enqueue-successors.test.ts +803 -0
  282. package/tests/commands/runtime-internalization-integrity-repair.test.ts +169 -0
  283. package/tests/commands/runtime-internalization-integrity.test.ts +102 -0
  284. package/tests/commands/runtime-internalization-queue.test.ts +252 -0
  285. package/tests/commands/runtime-internalization-run-once.test.ts +1318 -0
  286. package/tests/commands/runtime-internalization-wake-once.test.ts +170 -0
  287. package/tests/commands/runtime-internalization.test.ts +52 -0
  288. package/tests/commands/runtime-pain-flood-simulation.test.ts +418 -0
  289. package/tests/commands/runtime-pruning.test.ts +693 -0
  290. package/tests/commands/runtime-recovery.test.ts +96 -0
  291. package/tests/commands/runtime-synthetic-baseline.test.ts +249 -0
  292. package/tests/commands/runtime-uat.test.ts +397 -0
  293. package/tests/commands/runtime.test.ts +262 -0
  294. package/tests/commands/trace.test.ts +314 -0
  295. package/tests/e2e/candidate-intake-e2e.test.ts +316 -0
  296. package/tests/services/feature-flag-loader.test.ts +207 -0
  297. package/tests/services/proven-channel-baseline-runner.test.ts +30 -0
  298. package/tsconfig.json +26 -0
@@ -0,0 +1,1318 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import * as path from 'path';
3
+
4
+ const VALID_DREAMER_CANDIDATES = [{ candidateIndex: 0, badDecision: 'Ignored validation', betterDecision: 'Validate inputs', rationale: 'Prevents errors', confidence: 0.9, riskLevel: 'low' as const, strategicPerspective: 'defensive-programming' }];
5
+
6
+ const mockWakeOnce = vi.fn();
7
+ const mockRun = vi.fn();
8
+ const mockCommitNextTaskProposal = vi.fn().mockResolvedValue({ decision: 'no_successor', sourceTaskId: '', reason: '' });
9
+ const mockClose = vi.fn().mockResolvedValue(undefined);
10
+ const mockInitialize = vi.fn().mockResolvedValue(undefined);
11
+ const mockPiArtifactStore = {
12
+ createArtifact: vi.fn().mockResolvedValue({}),
13
+ upsertArtifact: vi.fn().mockResolvedValue({}),
14
+ getArtifactById: vi.fn().mockResolvedValue(null),
15
+ listBySourceTaskId: vi.fn().mockResolvedValue([]),
16
+ listLineage: vi.fn().mockResolvedValue([]),
17
+ };
18
+
19
+ vi.mock('../../src/resolve-workspace.js', () => ({
20
+ resolveWorkspaceDir: vi.fn().mockReturnValue('/fake/workspace'),
21
+ }));
22
+
23
+ vi.mock('@principles/core/runtime-v2', () => ({
24
+ RuntimeStateManager: vi.fn().mockImplementation(function () {
25
+ return {
26
+ initialize: mockInitialize,
27
+ close: mockClose,
28
+ connection: {},
29
+ taskStore: {},
30
+ runStore: {},
31
+ piArtifactStore: mockPiArtifactStore,
32
+ };
33
+ }),
34
+ InternalizationOrchestrator: vi.fn().mockImplementation(function () {
35
+ return { wakeOnce: mockWakeOnce, commitNextTaskProposal: mockCommitNextTaskProposal };
36
+ }),
37
+ DreamerRunner: vi.fn().mockImplementation(function () {
38
+ return { run: mockRun };
39
+ }),
40
+ PhilosopherRunner: vi.fn().mockImplementation(function () {
41
+ return { run: mockRun };
42
+ }),
43
+ ScribeRunner: vi.fn().mockImplementation(function () {
44
+ return { run: mockRun };
45
+ }),
46
+ ArtificerRunner: vi.fn().mockImplementation(function () {
47
+ return { run: mockRun };
48
+ }),
49
+ EvaluatorRunner: vi.fn().mockImplementation(function () {
50
+ return { run: mockRun };
51
+ }),
52
+ RolloutReviewerRunner: vi.fn().mockImplementation(function () {
53
+ return { run: mockRun };
54
+ }),
55
+ StoreEventEmitter: vi.fn().mockImplementation(function () {
56
+ return { emitTelemetry: vi.fn() };
57
+ }),
58
+ PassThroughDreamerValidator: vi.fn().mockImplementation(function () {
59
+ return { validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }) };
60
+ }),
61
+ DefaultDreamerValidator: vi.fn().mockImplementation(function () {
62
+ return { validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }) };
63
+ }),
64
+ DefaultPhilosopherValidator: vi.fn().mockImplementation(function () {
65
+ return { validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }) };
66
+ }),
67
+ DefaultScribeValidator: vi.fn().mockImplementation(function () {
68
+ return { validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }) };
69
+ }),
70
+ DefaultArtificerValidator: vi.fn().mockImplementation(function () {
71
+ return { validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }) };
72
+ }),
73
+ DefaultEvaluatorValidator: vi.fn().mockImplementation(function () {
74
+ return { validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }) };
75
+ }),
76
+ DefaultRolloutReviewerValidator: vi.fn().mockImplementation(function () {
77
+ return { validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }) };
78
+ }),
79
+ TestDoubleRuntimeAdapter: vi.fn().mockImplementation(function () {
80
+ return {
81
+ kind: vi.fn().mockReturnValue('test-double'),
82
+ getCapabilities: vi.fn(),
83
+ healthCheck: vi.fn(),
84
+ startRun: vi.fn().mockResolvedValue({ runId: 'run-test-001', runtimeKind: 'test-double', startedAt: new Date().toISOString() }),
85
+ pollRun: vi.fn().mockResolvedValue({ runId: 'run-test-001', status: 'succeeded' }),
86
+ cancelRun: vi.fn().mockResolvedValue(undefined),
87
+ fetchOutput: vi.fn().mockResolvedValue({ runId: 'run-test-001', payload: {} }),
88
+ fetchArtifacts: vi.fn().mockResolvedValue([]),
89
+ };
90
+ }),
91
+ PiAiRuntimeAdapter: vi.fn().mockImplementation(function () {
92
+ return {
93
+ kind: vi.fn().mockReturnValue('pi-ai'),
94
+ getCapabilities: vi.fn(),
95
+ healthCheck: vi.fn(),
96
+ startRun: vi.fn().mockResolvedValue({ runId: 'run-pi-001', runtimeKind: 'pi-ai', startedAt: new Date().toISOString() }),
97
+ pollRun: vi.fn().mockResolvedValue({ runId: 'run-pi-001', status: 'succeeded' }),
98
+ cancelRun: vi.fn().mockResolvedValue(undefined),
99
+ fetchOutput: vi.fn().mockResolvedValue({ runId: 'run-pi-001', payload: {} }),
100
+ fetchArtifacts: vi.fn().mockResolvedValue([]),
101
+ };
102
+ }),
103
+ OpenClawCliRuntimeAdapter: vi.fn().mockImplementation(function () {
104
+ return {
105
+ kind: vi.fn().mockReturnValue('openclaw-cli'),
106
+ getCapabilities: vi.fn(),
107
+ healthCheck: vi.fn(),
108
+ startRun: vi.fn().mockResolvedValue({ runId: 'run-oc-001', runtimeKind: 'openclaw-cli', startedAt: new Date().toISOString() }),
109
+ pollRun: vi.fn().mockResolvedValue({ runId: 'run-oc-001', status: 'succeeded' }),
110
+ cancelRun: vi.fn().mockResolvedValue(undefined),
111
+ fetchOutput: vi.fn().mockResolvedValue({ runId: 'run-oc-001', payload: {} }),
112
+ fetchArtifacts: vi.fn().mockResolvedValue([]),
113
+ };
114
+ }),
115
+ resolveRuntimeConfig: vi.fn().mockReturnValue({
116
+ runtimeKind: 'pi-ai',
117
+ timeoutMs: 300_000,
118
+ agentId: 'main',
119
+ provider: 'test-provider',
120
+ model: 'test-model',
121
+ apiKeyEnv: 'TEST_API_KEY',
122
+ }),
123
+ isRuntimeConfigError: vi.fn().mockReturnValue(false),
124
+ validateRuntimeConfig: vi.fn(),
125
+ }));
126
+
127
+ import { handleRuntimeInternalizationRunOnce } from '../../src/commands/runtime-internalization-run-once.js';
128
+
129
+ const WS = '/fake/workspace';
130
+
131
+ describe('handleRuntimeInternalizationRunOnce', () => {
132
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>;
133
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
134
+
135
+ beforeEach(() => {
136
+ vi.clearAllMocks();
137
+ process.exitCode = 0;
138
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
139
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
140
+ });
141
+
142
+ afterEach(() => {
143
+ process.exitCode = 0;
144
+ consoleLogSpy.mockRestore();
145
+ consoleErrorSpy.mockRestore();
146
+ });
147
+
148
+ it('test-double without --allow-test-double: refuses to execute and exits 1', async () => {
149
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'test-double', allowTestDouble: false });
150
+
151
+ expect(process.exitCode).toBe(1);
152
+ expect(consoleErrorSpy.mock.calls.some((c: string[]) => c[0].includes('test-double runtime mutates real queue state'))).toBe(true);
153
+ expect(mockWakeOnce).not.toHaveBeenCalled();
154
+ });
155
+
156
+ it('default runtime (config) does not require --allow-test-double', async () => {
157
+ mockWakeOnce.mockResolvedValue({
158
+ decision: 'would_lease',
159
+ taskId: 'task-dreamer-001',
160
+ taskKind: 'dreamer',
161
+ });
162
+
163
+ mockRun.mockResolvedValue({
164
+ status: 'succeeded',
165
+ taskId: 'task-dreamer-001',
166
+ runId: 'run-001',
167
+ artifactId: 'pi-art-task-dreamer-001-run-001',
168
+ resultRef: 'dreamer://run-001',
169
+ contextHash: 'ctx-abc',
170
+ output: { valid: true, taskId: 'task-dreamer-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
171
+ attemptCount: 1,
172
+ });
173
+
174
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, json: true });
175
+
176
+ expect(mockWakeOnce).toHaveBeenCalled();
177
+ });
178
+
179
+ it('test-double with --allow-test-double: proceeds normally', async () => {
180
+ mockWakeOnce.mockResolvedValue({
181
+ decision: 'would_lease',
182
+ taskId: 'task-dreamer-001',
183
+ taskKind: 'dreamer',
184
+ });
185
+
186
+ mockRun.mockResolvedValue({
187
+ status: 'succeeded',
188
+ taskId: 'task-dreamer-001',
189
+ runId: 'run-001',
190
+ artifactId: 'pi-art-task-dreamer-001-run-001',
191
+ resultRef: 'dreamer://run-001',
192
+ contextHash: 'ctx-abc',
193
+ output: { valid: true, taskId: 'task-dreamer-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
194
+ attemptCount: 1,
195
+ });
196
+
197
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'test-double', allowTestDouble: true, json: true });
198
+
199
+ expect(mockWakeOnce).toHaveBeenCalled();
200
+ });
201
+
202
+ it('unsupported runner kind: exits 1 with error', async () => {
203
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'invalid-runner', runtime: 'test-double', allowTestDouble: true });
204
+
205
+ expect(process.exitCode).toBe(1);
206
+ expect(consoleErrorSpy.mock.calls.some((c: string[]) => c[0].includes('unsupported runner kind'))).toBe(true);
207
+ });
208
+
209
+ it('no_ready_tasks: reports no leasable task and exits 1', async () => {
210
+ mockWakeOnce.mockResolvedValue({
211
+ decision: 'no_ready_tasks',
212
+ inspectedCount: 3,
213
+ reason: 'all_blocked',
214
+ });
215
+
216
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'test-double', allowTestDouble: true, json: true });
217
+
218
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
219
+ expect(output.decision).toBe('no_ready_tasks');
220
+ expect(process.exitCode).toBe(1);
221
+ });
222
+
223
+ it('would_lease dreamer task: uses dryRun wakeOnce then DreamerRunner.run leases and executes', async () => {
224
+ mockWakeOnce.mockResolvedValue({
225
+ decision: 'would_lease',
226
+ taskId: 'task-dreamer-001',
227
+ taskKind: 'dreamer',
228
+ });
229
+
230
+ mockRun.mockResolvedValue({
231
+ status: 'succeeded',
232
+ taskId: 'task-dreamer-001',
233
+ runId: 'run-001',
234
+ artifactId: 'pi-art-task-dreamer-001-run-001',
235
+ resultRef: 'dreamer://run-001',
236
+ contextHash: 'ctx-abc',
237
+ output: { valid: true, taskId: 'task-dreamer-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
238
+ attemptCount: 1,
239
+ });
240
+
241
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'test-double', allowTestDouble: true, json: true });
242
+
243
+ expect(mockRun).toHaveBeenCalledWith('task-dreamer-001');
244
+
245
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
246
+ expect(output.decision).toBe('would_lease');
247
+ expect(output.taskId).toBe('task-dreamer-001');
248
+ expect(output.runId).toBe('run-001');
249
+ expect(output.artifactId).toBe('pi-art-task-dreamer-001-run-001');
250
+ expect(output.resultRef).toBe('dreamer://run-001');
251
+ expect(output.runnerResult.status).toBe('succeeded');
252
+ });
253
+
254
+ it('would_lease dreamer task with runner failure: reports failed result', async () => {
255
+ mockWakeOnce.mockResolvedValue({
256
+ decision: 'would_lease',
257
+ taskId: 'task-dreamer-002',
258
+ taskKind: 'dreamer',
259
+ });
260
+
261
+ mockRun.mockResolvedValue({
262
+ status: 'failed',
263
+ taskId: 'task-dreamer-002',
264
+ errorCategory: 'execution_failed',
265
+ failureReason: 'Runtime unavailable',
266
+ attemptCount: 1,
267
+ });
268
+
269
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'test-double', allowTestDouble: true, json: true });
270
+
271
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
272
+ expect(output.runnerResult.status).toBe('failed');
273
+ expect(output.runnerResult.errorCategory).toBe('execution_failed');
274
+ });
275
+
276
+ it('text output for succeeded dreamer run includes runId/artifactId/resultRef', async () => {
277
+ mockWakeOnce.mockResolvedValue({
278
+ decision: 'would_lease',
279
+ taskId: 'task-dreamer-003',
280
+ taskKind: 'dreamer',
281
+ });
282
+
283
+ mockRun.mockResolvedValue({
284
+ status: 'succeeded',
285
+ taskId: 'task-dreamer-003',
286
+ runId: 'run-003',
287
+ artifactId: 'pi-art-task-dreamer-003-run-003',
288
+ resultRef: 'dreamer://run-003',
289
+ contextHash: 'ctx-def',
290
+ output: { valid: true, taskId: 'task-dreamer-003', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
291
+ attemptCount: 1,
292
+ });
293
+
294
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'test-double', allowTestDouble: true, json: false });
295
+
296
+ const text = consoleLogSpy.mock.calls.map((c: string[]) => c[0]).join('\n');
297
+ expect(text).toContain('task-dreamer-003');
298
+ expect(text).toContain('succeeded');
299
+ expect(text).toContain('runId: run-003');
300
+ expect(text).toContain('artifactId: pi-art-task-dreamer-003-run-003');
301
+ expect(text).toContain('resultRef: dreamer://run-003');
302
+ });
303
+
304
+ it('lease_conflict: reports conflict and exits 1', async () => {
305
+ mockWakeOnce.mockResolvedValue({
306
+ decision: 'lease_conflict',
307
+ taskId: 'task-dreamer-004',
308
+ conflictReason: 'Already leased by another runner',
309
+ });
310
+
311
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'test-double', allowTestDouble: true, json: true });
312
+
313
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
314
+ expect(output.decision).toBe('lease_conflict');
315
+ expect(process.exitCode).toBe(1);
316
+ });
317
+
318
+ it('orchestrator error: exits 1 with error message', async () => {
319
+ mockWakeOnce.mockRejectedValue(new Error('store unavailable'));
320
+
321
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'test-double', allowTestDouble: true, json: true });
322
+
323
+ expect(process.exitCode).toBe(1);
324
+ });
325
+
326
+ it('uses stateManager.piArtifactStore (durable SQLite) not MemoryPIArtifactStore', async () => {
327
+ mockWakeOnce.mockResolvedValue({
328
+ decision: 'would_lease',
329
+ taskId: 'task-dreamer-005',
330
+ taskKind: 'dreamer',
331
+ });
332
+
333
+ mockRun.mockResolvedValue({
334
+ status: 'succeeded',
335
+ taskId: 'task-dreamer-005',
336
+ runId: 'run-005',
337
+ artifactId: 'pi-art-task-dreamer-005-run-005',
338
+ resultRef: 'dreamer://run-005',
339
+ attemptCount: 1,
340
+ });
341
+
342
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'test-double', allowTestDouble: true, json: true });
343
+
344
+ const DreamerRunnerMock = vi.mocked(
345
+ await import('@principles/core/runtime-v2').then(m => m.DreamerRunner),
346
+ );
347
+ const lastCall = DreamerRunnerMock.mock.calls[DreamerRunnerMock.mock.calls.length - 1];
348
+ if (lastCall) {
349
+ const deps = lastCall[0] as { artifactStore?: unknown };
350
+ expect(deps.artifactStore).toBe(mockPiArtifactStore);
351
+ }
352
+ });
353
+
354
+ it('--runtime pi-ai resolves PiAiRuntimeAdapter', async () => {
355
+ mockWakeOnce.mockResolvedValue({
356
+ decision: 'would_lease',
357
+ taskId: 'task-dreamer-006',
358
+ taskKind: 'dreamer',
359
+ });
360
+
361
+ mockRun.mockResolvedValue({
362
+ status: 'succeeded',
363
+ taskId: 'task-dreamer-006',
364
+ runId: 'run-006',
365
+ artifactId: 'pi-art-task-dreamer-006-run-006',
366
+ resultRef: 'dreamer://run-006',
367
+ attemptCount: 1,
368
+ });
369
+
370
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'pi-ai', json: true });
371
+
372
+ const PiAiMock = vi.mocked(
373
+ await import('@principles/core/runtime-v2').then(m => m.PiAiRuntimeAdapter),
374
+ );
375
+ expect(PiAiMock).toHaveBeenCalled();
376
+ });
377
+
378
+ it('--runtime openclaw-cli resolves OpenClawCliRuntimeAdapter', async () => {
379
+ const { resolveRuntimeConfig } = await import('@principles/core/runtime-v2');
380
+ vi.mocked(resolveRuntimeConfig).mockReturnValue({
381
+ runtimeKind: 'openclaw-cli',
382
+ openclawMode: 'local',
383
+ timeoutMs: 300_000,
384
+ agentId: 'main',
385
+ });
386
+
387
+ mockWakeOnce.mockResolvedValue({
388
+ decision: 'would_lease',
389
+ taskId: 'task-dreamer-007',
390
+ taskKind: 'dreamer',
391
+ });
392
+
393
+ mockRun.mockResolvedValue({
394
+ status: 'succeeded',
395
+ taskId: 'task-dreamer-007',
396
+ runId: 'run-007',
397
+ artifactId: 'pi-art-task-dreamer-007-run-007',
398
+ resultRef: 'dreamer://run-007',
399
+ attemptCount: 1,
400
+ });
401
+
402
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'openclaw-cli', json: true });
403
+
404
+ const OpenClawMock = vi.mocked(
405
+ await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
406
+ );
407
+ expect(OpenClawMock).toHaveBeenCalled();
408
+ });
409
+
410
+ it('--runtime config resolves adapter from workflows.yaml', async () => {
411
+ mockWakeOnce.mockResolvedValue({
412
+ decision: 'would_lease',
413
+ taskId: 'task-dreamer-008',
414
+ taskKind: 'dreamer',
415
+ });
416
+
417
+ mockRun.mockResolvedValue({
418
+ status: 'succeeded',
419
+ taskId: 'task-dreamer-008',
420
+ runId: 'run-008',
421
+ artifactId: 'pi-art-task-dreamer-008-run-008',
422
+ resultRef: 'dreamer://run-008',
423
+ attemptCount: 1,
424
+ });
425
+
426
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'config', json: true });
427
+
428
+ const ResolveConfigMock = vi.mocked(
429
+ await import('@principles/core/runtime-v2').then(m => m.resolveRuntimeConfig),
430
+ );
431
+ expect(ResolveConfigMock).toHaveBeenCalled();
432
+ });
433
+
434
+ it('--runtime config reads from workspaceDir/.state (not .pd)', async () => {
435
+ mockWakeOnce.mockResolvedValue({
436
+ decision: 'would_lease',
437
+ taskId: 'task-dreamer-009',
438
+ taskKind: 'dreamer',
439
+ });
440
+
441
+ mockRun.mockResolvedValue({
442
+ status: 'succeeded',
443
+ taskId: 'task-dreamer-009',
444
+ runId: 'run-009',
445
+ artifactId: 'pi-art-task-dreamer-009-run-009',
446
+ resultRef: 'dreamer://run-009',
447
+ attemptCount: 1,
448
+ });
449
+
450
+ const customWs = '/tmp/test-workspace';
451
+ await handleRuntimeInternalizationRunOnce({ workspace: customWs, runtime: 'config', json: true });
452
+
453
+ const ResolveConfigMock = vi.mocked(
454
+ await import('@principles/core/runtime-v2').then(m => m.resolveRuntimeConfig),
455
+ );
456
+ const resolvedWorkspace = path.resolve(customWs);
457
+ const expectedStateDir = path.join(resolvedWorkspace, '.state');
458
+ expect(ResolveConfigMock).toHaveBeenCalledWith(expectedStateDir, { requestedRuntimeKind: 'config' });
459
+ });
460
+
461
+ it('--runner philosopher dispatches PhilosopherRunner', async () => {
462
+ mockWakeOnce.mockResolvedValue({
463
+ decision: 'would_lease',
464
+ taskId: 'task-phil-001',
465
+ taskKind: 'philosopher',
466
+ });
467
+
468
+ mockRun.mockResolvedValue({
469
+ status: 'succeeded',
470
+ taskId: 'task-phil-001',
471
+ runId: 'run-phil-001',
472
+ artifactId: 'pi-art-task-phil-001-run-phil-001',
473
+ resultRef: 'philosopher://run-phil-001',
474
+ contextHash: 'ctx-phil-abc',
475
+ output: {
476
+ taskId: 'task-phil-001',
477
+ sourceDreamerArtifactId: 'pi-art-dreamer-001',
478
+ thesis: 'Test thesis',
479
+ principleCandidate: { title: 'T', rationale: 'R', scope: 'S', confidence: 0.9 },
480
+ risks: [],
481
+ generatedAt: new Date().toISOString(),
482
+ },
483
+ attemptCount: 1,
484
+ });
485
+
486
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'philosopher', runtime: 'test-double', allowTestDouble: true, json: true });
487
+
488
+ const PhilosopherRunnerMock = vi.mocked(
489
+ await import('@principles/core/runtime-v2').then(m => m.PhilosopherRunner),
490
+ );
491
+ expect(PhilosopherRunnerMock).toHaveBeenCalled();
492
+ expect(mockRun).toHaveBeenCalledWith('task-phil-001');
493
+
494
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
495
+ expect(output.runnerKind).toBe('philosopher');
496
+ expect(output.taskId).toBe('task-phil-001');
497
+ expect(output.runId).toBe('run-phil-001');
498
+ expect(output.artifactId).toBe('pi-art-task-phil-001-run-phil-001');
499
+ expect(output.resultRef).toBe('philosopher://run-phil-001');
500
+ expect(output.runnerResult.status).toBe('succeeded');
501
+ });
502
+
503
+ it('--runner philosopher with text output includes key IDs', async () => {
504
+ mockWakeOnce.mockResolvedValue({
505
+ decision: 'would_lease',
506
+ taskId: 'task-phil-002',
507
+ taskKind: 'philosopher',
508
+ });
509
+
510
+ mockRun.mockResolvedValue({
511
+ status: 'succeeded',
512
+ taskId: 'task-phil-002',
513
+ runId: 'run-phil-002',
514
+ artifactId: 'pi-art-task-phil-002-run-phil-002',
515
+ resultRef: 'philosopher://run-phil-002',
516
+ contextHash: 'ctx-phil-def',
517
+ output: {
518
+ taskId: 'task-phil-002',
519
+ sourceDreamerArtifactId: 'pi-art-dreamer-002',
520
+ thesis: 'Test thesis',
521
+ principleCandidate: { title: 'T', rationale: 'R', scope: 'S', confidence: 0.9 },
522
+ risks: [],
523
+ generatedAt: new Date().toISOString(),
524
+ },
525
+ attemptCount: 1,
526
+ });
527
+
528
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'philosopher', runtime: 'test-double', allowTestDouble: true, json: false });
529
+
530
+ const text = consoleLogSpy.mock.calls.map((c: string[]) => c[0]).join('\n');
531
+ expect(text).toContain('task-phil-002');
532
+ expect(text).toContain('succeeded');
533
+ expect(text).toContain('runId: run-phil-002');
534
+ expect(text).toContain('artifactId: pi-art-task-phil-002-run-phil-002');
535
+ expect(text).toContain('resultRef: philosopher://run-phil-002');
536
+ });
537
+
538
+ it('successful dreamer + --enqueue-next returns successorTaskId', async () => {
539
+ mockWakeOnce.mockResolvedValue({
540
+ decision: 'would_lease',
541
+ taskId: 'task-dreamer-enq-001',
542
+ taskKind: 'dreamer',
543
+ });
544
+
545
+ mockRun.mockResolvedValue({
546
+ status: 'succeeded',
547
+ taskId: 'task-dreamer-enq-001',
548
+ runId: 'run-enq-001',
549
+ artifactId: 'pi-art-enq-001',
550
+ resultRef: 'dreamer://run-enq-001',
551
+ contextHash: 'ctx-enq',
552
+ output: { valid: true, taskId: 'task-dreamer-enq-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
553
+ attemptCount: 1,
554
+ });
555
+
556
+ mockCommitNextTaskProposal.mockResolvedValue({
557
+ decision: 'successor_created',
558
+ sourceTaskId: 'task-dreamer-enq-001',
559
+ successorTaskId: 'task-phil-enq-001',
560
+ successorKind: 'philosopher',
561
+ });
562
+
563
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, enqueueNext: true, json: true });
564
+
565
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
566
+ expect(output.enqueueDecision).toBe('successor_created');
567
+ expect(output.successorTaskId).toBe('task-phil-enq-001');
568
+ expect(output.successorKind).toBe('philosopher');
569
+ });
570
+
571
+ it('repeated --enqueue-next returns existing successorTaskId', async () => {
572
+ mockWakeOnce.mockResolvedValue({
573
+ decision: 'would_lease',
574
+ taskId: 'task-dreamer-enq-002',
575
+ taskKind: 'dreamer',
576
+ });
577
+
578
+ mockRun.mockResolvedValue({
579
+ status: 'succeeded',
580
+ taskId: 'task-dreamer-enq-002',
581
+ runId: 'run-enq-002',
582
+ artifactId: 'pi-art-enq-002',
583
+ resultRef: 'dreamer://run-enq-002',
584
+ contextHash: 'ctx-enq2',
585
+ output: { valid: true, taskId: 'task-dreamer-enq-002', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
586
+ attemptCount: 1,
587
+ });
588
+
589
+ mockCommitNextTaskProposal.mockResolvedValue({
590
+ decision: 'successor_exists',
591
+ sourceTaskId: 'task-dreamer-enq-002',
592
+ successorTaskId: 'task-phil-enq-002',
593
+ successorKind: 'philosopher',
594
+ });
595
+
596
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, enqueueNext: true, json: true });
597
+
598
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
599
+ expect(output.enqueueDecision).toBe('successor_exists');
600
+ expect(output.successorTaskId).toBe('task-phil-enq-002');
601
+ expect(output.successorKind).toBe('philosopher');
602
+ });
603
+
604
+ it('--enqueue-next with no_successor does not set successorTaskId', async () => {
605
+ mockWakeOnce.mockResolvedValue({
606
+ decision: 'would_lease',
607
+ taskId: 'task-dreamer-enq-003',
608
+ taskKind: 'dreamer',
609
+ });
610
+
611
+ mockRun.mockResolvedValue({
612
+ status: 'succeeded',
613
+ taskId: 'task-dreamer-enq-003',
614
+ runId: 'run-enq-003',
615
+ artifactId: 'pi-art-enq-003',
616
+ resultRef: 'dreamer://run-enq-003',
617
+ contextHash: 'ctx-enq3',
618
+ output: { valid: true, taskId: 'task-dreamer-enq-003', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
619
+ attemptCount: 1,
620
+ });
621
+
622
+ mockCommitNextTaskProposal.mockResolvedValue({
623
+ decision: 'no_successor',
624
+ sourceTaskId: 'task-dreamer-enq-003',
625
+ reason: 'terminal runner',
626
+ });
627
+
628
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, enqueueNext: true, json: true });
629
+
630
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
631
+ expect(output.enqueueDecision).toBe('no_successor');
632
+ expect(output.successorTaskId).toBeUndefined();
633
+ });
634
+
635
+ it('--enqueue-next with failed run does not call commitNextTaskProposal', async () => {
636
+ mockWakeOnce.mockResolvedValue({
637
+ decision: 'would_lease',
638
+ taskId: 'task-dreamer-enq-004',
639
+ taskKind: 'dreamer',
640
+ });
641
+
642
+ mockRun.mockResolvedValue({
643
+ status: 'failed',
644
+ taskId: 'task-dreamer-enq-004',
645
+ errorCategory: 'execution_failed',
646
+ failureReason: 'Runtime unavailable',
647
+ attemptCount: 1,
648
+ });
649
+
650
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, enqueueNext: true, json: true });
651
+
652
+ expect(mockCommitNextTaskProposal).not.toHaveBeenCalled();
653
+ });
654
+
655
+ it('--enqueue-next without --allow-test-double still blocked', async () => {
656
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', enqueueNext: true });
657
+
658
+ expect(process.exitCode).toBe(1);
659
+ expect(mockWakeOnce).not.toHaveBeenCalled();
660
+ });
661
+
662
+ it('test-double with --runner philosopher requires --allow-test-double', async () => {
663
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'philosopher', runtime: 'test-double' });
664
+
665
+ expect(process.exitCode).toBe(1);
666
+ expect(consoleErrorSpy.mock.calls.some((c: string[]) => c[0].includes('test-double runtime mutates real queue state'))).toBe(true);
667
+ expect(mockWakeOnce).not.toHaveBeenCalled();
668
+ });
669
+
670
+ it('JSON output includes runnerKind field', async () => {
671
+ mockWakeOnce.mockResolvedValue({
672
+ decision: 'would_lease',
673
+ taskId: 'task-dreamer-rk',
674
+ taskKind: 'dreamer',
675
+ });
676
+
677
+ mockRun.mockResolvedValue({
678
+ status: 'succeeded',
679
+ taskId: 'task-dreamer-rk',
680
+ runId: 'run-rk',
681
+ artifactId: 'pi-art-rk',
682
+ resultRef: 'dreamer://run-rk',
683
+ contextHash: 'ctx-rk',
684
+ output: { valid: true, taskId: 'task-dreamer-rk', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
685
+ attemptCount: 1,
686
+ });
687
+
688
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: true });
689
+
690
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
691
+ expect(output.runnerKind).toBe('dreamer');
692
+ expect(output.decision).toBe('would_lease');
693
+ expect(output.taskId).toBe('task-dreamer-rk');
694
+ expect(output.runId).toBe('run-rk');
695
+ expect(output.artifactId).toBe('pi-art-rk');
696
+ expect(output.resultRef).toBe('dreamer://run-rk');
697
+ });
698
+
699
+ it('--timeout-ms passes effectiveTimeoutMs to DreamerRunner and output', async () => {
700
+ mockWakeOnce.mockResolvedValue({
701
+ decision: 'would_lease',
702
+ taskId: 'task-dreamer-tm',
703
+ taskKind: 'dreamer',
704
+ });
705
+
706
+ mockRun.mockResolvedValue({
707
+ status: 'succeeded',
708
+ taskId: 'task-dreamer-tm',
709
+ runId: 'run-tm',
710
+ artifactId: 'pi-art-tm',
711
+ resultRef: 'dreamer://run-tm',
712
+ contextHash: 'ctx-tm',
713
+ output: { valid: true, taskId: 'task-dreamer-tm', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
714
+ attemptCount: 1,
715
+ });
716
+
717
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, timeoutMs: 180_000, json: true });
718
+
719
+ const DreamerRunnerMock = vi.mocked(
720
+ await import('@principles/core/runtime-v2').then(m => m.DreamerRunner),
721
+ );
722
+ const lastCall = DreamerRunnerMock.mock.calls[DreamerRunnerMock.mock.calls.length - 1];
723
+ if (lastCall) {
724
+ const opts = lastCall[1] as { timeoutMs?: number };
725
+ expect(opts.timeoutMs).toBe(180_000);
726
+ }
727
+
728
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
729
+ expect(output.effectiveTimeoutMs).toBe(180_000);
730
+ });
731
+
732
+ it('default timeoutMs is 300000 when --timeout-ms not provided', async () => {
733
+ mockWakeOnce.mockResolvedValue({
734
+ decision: 'would_lease',
735
+ taskId: 'task-dreamer-dt',
736
+ taskKind: 'dreamer',
737
+ });
738
+
739
+ mockRun.mockResolvedValue({
740
+ status: 'succeeded',
741
+ taskId: 'task-dreamer-dt',
742
+ runId: 'run-dt',
743
+ artifactId: 'pi-art-dt',
744
+ resultRef: 'dreamer://run-dt',
745
+ contextHash: 'ctx-dt',
746
+ output: { valid: true, taskId: 'task-dreamer-dt', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
747
+ attemptCount: 1,
748
+ });
749
+
750
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: true });
751
+
752
+ const DreamerRunnerMock = vi.mocked(
753
+ await import('@principles/core/runtime-v2').then(m => m.DreamerRunner),
754
+ );
755
+ const lastCall = DreamerRunnerMock.mock.calls[DreamerRunnerMock.mock.calls.length - 1];
756
+ if (lastCall) {
757
+ const opts = lastCall[1] as { timeoutMs?: number };
758
+ expect(opts.timeoutMs).toBe(300_000);
759
+ }
760
+
761
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
762
+ expect(output.effectiveTimeoutMs).toBe(300_000);
763
+ });
764
+
765
+ it('timeout source extracted from failureReason on timeout', async () => {
766
+ mockWakeOnce.mockResolvedValue({
767
+ decision: 'would_lease',
768
+ taskId: 'task-dreamer-ts',
769
+ taskKind: 'dreamer',
770
+ });
771
+
772
+ mockRun.mockResolvedValue({
773
+ status: 'failed',
774
+ taskId: 'task-dreamer-ts',
775
+ errorCategory: 'timeout',
776
+ failureReason: '[timeout] LLM request timed out after 300000ms (timeoutSource=provider_request)',
777
+ attemptCount: 1,
778
+ });
779
+
780
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, timeoutMs: 300_000, json: true });
781
+
782
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
783
+ expect(output.timeoutSource).toBe('provider_request');
784
+ expect(output.effectiveTimeoutMs).toBe(300_000);
785
+ });
786
+
787
+ it('timeout source extracted as runner_poll from abort timeout', async () => {
788
+ mockWakeOnce.mockResolvedValue({
789
+ decision: 'would_lease',
790
+ taskId: 'task-dreamer-rp',
791
+ taskKind: 'dreamer',
792
+ });
793
+
794
+ mockRun.mockResolvedValue({
795
+ status: 'failed',
796
+ taskId: 'task-dreamer-rp',
797
+ errorCategory: 'timeout',
798
+ failureReason: '[timeout] LLM request timed out after 300000ms (timeoutSource=runner_poll)',
799
+ attemptCount: 1,
800
+ });
801
+
802
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: true });
803
+
804
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
805
+ expect(output.timeoutSource).toBe('runner_poll');
806
+ });
807
+
808
+ it('--timeout-ms overrides workflows.yaml timeoutMs for PiAiRuntimeAdapter', async () => {
809
+ mockWakeOnce.mockResolvedValue({
810
+ decision: 'would_lease',
811
+ taskId: 'task-dreamer-ov',
812
+ taskKind: 'dreamer',
813
+ });
814
+
815
+ mockRun.mockResolvedValue({
816
+ status: 'succeeded',
817
+ taskId: 'task-dreamer-ov',
818
+ runId: 'run-ov',
819
+ artifactId: 'pi-art-ov',
820
+ resultRef: 'dreamer://run-ov',
821
+ contextHash: 'ctx-ov',
822
+ output: { valid: true, taskId: 'task-dreamer-ov', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
823
+ attemptCount: 1,
824
+ });
825
+
826
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'config', timeoutMs: 240_000, json: true });
827
+
828
+ const PiAiMock = vi.mocked(
829
+ await import('@principles/core/runtime-v2').then(m => m.PiAiRuntimeAdapter),
830
+ );
831
+ const lastCall = PiAiMock.mock.calls[PiAiMock.mock.calls.length - 1];
832
+ if (lastCall) {
833
+ const config = lastCall[0] as { timeoutMs?: number };
834
+ expect(config.timeoutMs).toBe(240_000);
835
+ }
836
+
837
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
838
+ expect(output.effectiveTimeoutMs).toBe(240_000);
839
+ });
840
+
841
+ it('--runner scribe dispatches ScribeRunner', async () => {
842
+ mockWakeOnce.mockResolvedValue({
843
+ decision: 'would_lease',
844
+ taskId: 'task-scribe-001',
845
+ taskKind: 'scribe',
846
+ });
847
+
848
+ mockRun.mockResolvedValue({
849
+ status: 'succeeded',
850
+ taskId: 'task-scribe-001',
851
+ runId: 'run-scribe-001',
852
+ artifactId: 'pi-art-task-scribe-001-run-scribe-001',
853
+ resultRef: 'scribe://run-scribe-001',
854
+ contextHash: 'ctx-scribe-abc',
855
+ output: {
856
+ taskId: 'task-scribe-001',
857
+ sourcePhilosopherArtifactId: 'pi-art-phil-001',
858
+ principleDraft: { title: 'T', statement: 'S', rationale: 'R', applicability: [], antiPatterns: [], confidence: 0.9 },
859
+ sourceTrace: { philosopherArtifactId: 'pi-art-phil-001' },
860
+ risks: [],
861
+ generatedAt: new Date().toISOString(),
862
+ },
863
+ attemptCount: 1,
864
+ });
865
+
866
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'scribe', runtime: 'test-double', allowTestDouble: true, json: true });
867
+
868
+ const ScribeRunnerMock = vi.mocked(
869
+ await import('@principles/core/runtime-v2').then(m => m.ScribeRunner),
870
+ );
871
+ expect(ScribeRunnerMock).toHaveBeenCalled();
872
+ expect(mockRun).toHaveBeenCalledWith('task-scribe-001');
873
+
874
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
875
+ expect(output.runnerKind).toBe('scribe');
876
+ expect(output.taskId).toBe('task-scribe-001');
877
+ expect(output.runId).toBe('run-scribe-001');
878
+ expect(output.artifactId).toBe('pi-art-task-scribe-001-run-scribe-001');
879
+ expect(output.resultRef).toBe('scribe://run-scribe-001');
880
+ expect(output.runnerResult.status).toBe('succeeded');
881
+ });
882
+
883
+ it('--runner scribe --enqueue-next creates artificer successor', async () => {
884
+ mockWakeOnce.mockResolvedValue({
885
+ decision: 'would_lease',
886
+ taskId: 'task-scribe-enq-001',
887
+ taskKind: 'scribe',
888
+ });
889
+
890
+ mockRun.mockResolvedValue({
891
+ status: 'succeeded',
892
+ taskId: 'task-scribe-enq-001',
893
+ runId: 'run-scribe-enq-001',
894
+ artifactId: 'pi-art-scribe-enq-001',
895
+ resultRef: 'scribe://run-scribe-enq-001',
896
+ contextHash: 'ctx-enq',
897
+ output: {
898
+ taskId: 'task-scribe-enq-001',
899
+ sourcePhilosopherArtifactId: 'pi-art-phil-enq-001',
900
+ principleDraft: { title: 'T', statement: 'S', rationale: 'R', applicability: [], antiPatterns: [], confidence: 0.9 },
901
+ sourceTrace: { philosopherArtifactId: 'pi-art-phil-enq-001' },
902
+ risks: [],
903
+ generatedAt: new Date().toISOString(),
904
+ },
905
+ attemptCount: 1,
906
+ });
907
+
908
+ mockCommitNextTaskProposal.mockResolvedValue({
909
+ decision: 'successor_created',
910
+ sourceTaskId: 'task-scribe-enq-001',
911
+ successorTaskId: 'task-artificer-enq-001',
912
+ successorKind: 'artificer',
913
+ });
914
+
915
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'scribe', runtime: 'test-double', allowTestDouble: true, enqueueNext: true, json: true });
916
+
917
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
918
+ expect(output.enqueueDecision).toBe('successor_created');
919
+ expect(output.successorTaskId).toBe('task-artificer-enq-001');
920
+ expect(output.successorKind).toBe('artificer');
921
+ });
922
+
923
+ it('run-once source imports EvaluatorRunner for dispatch', async () => {
924
+ const { existsSync, readFileSync } = await import('node:fs');
925
+ const { resolve } = await import('node:path');
926
+ const srcPath = resolve(__dirname, '../../src/commands/runtime-internalization-run-once.ts');
927
+ if (!existsSync(srcPath)) return;
928
+ const src = readFileSync(srcPath, 'utf-8');
929
+ expect(src).toContain('EvaluatorRunner');
930
+ expect(src).toContain('DefaultEvaluatorValidator');
931
+ });
932
+
933
+ it('--runner artificer dispatches ArtificerRunner', async () => {
934
+ mockWakeOnce.mockResolvedValue({
935
+ decision: 'would_lease',
936
+ taskId: 'task-artificer-001',
937
+ taskKind: 'artificer',
938
+ });
939
+
940
+ mockRun.mockResolvedValue({
941
+ status: 'succeeded',
942
+ taskId: 'task-artificer-001',
943
+ runId: 'run-artificer-001',
944
+ artifactId: 'pi-art-task-artificer-001-run-artificer-001',
945
+ resultRef: 'artificer://run-artificer-001',
946
+ contextHash: 'ctx-artificer-abc',
947
+ output: {
948
+ taskId: 'task-artificer-001',
949
+ sourceScribeArtifactId: 'pi-art-scribe-001',
950
+ implementationPlan: {
951
+ summary: 'Test implementation summary',
952
+ targetSurface: 'src/test/*.ts',
953
+ changes: ['Add validation'],
954
+ tests: ['Unit test for validation'],
955
+ rolloutNotes: ['Deploy behind feature flag'],
956
+ confidence: 0.8,
957
+ },
958
+ sourceTrace: { scribeArtifactId: 'pi-art-scribe-001' },
959
+ risks: [],
960
+ generatedAt: new Date().toISOString(),
961
+ },
962
+ attemptCount: 1,
963
+ });
964
+
965
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'artificer', runtime: 'test-double', allowTestDouble: true, json: true });
966
+
967
+ const ArtificerRunnerMock = vi.mocked(
968
+ await import('@principles/core/runtime-v2').then(m => m.ArtificerRunner),
969
+ );
970
+ expect(ArtificerRunnerMock).toHaveBeenCalled();
971
+ expect(mockRun).toHaveBeenCalledWith('task-artificer-001');
972
+
973
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
974
+ expect(output.runnerKind).toBe('artificer');
975
+ expect(output.taskId).toBe('task-artificer-001');
976
+ expect(output.runId).toBe('run-artificer-001');
977
+ expect(output.artifactId).toBe('pi-art-task-artificer-001-run-artificer-001');
978
+ expect(output.resultRef).toBe('artificer://run-artificer-001');
979
+ expect(output.runnerResult.status).toBe('succeeded');
980
+ });
981
+
982
+ it('--runner artificer --enqueue-next returns successor decision', async () => {
983
+ mockWakeOnce.mockResolvedValue({
984
+ decision: 'would_lease',
985
+ taskId: 'task-artificer-enq-001',
986
+ taskKind: 'artificer',
987
+ });
988
+
989
+ mockRun.mockResolvedValue({
990
+ status: 'succeeded',
991
+ taskId: 'task-artificer-enq-001',
992
+ runId: 'run-artificer-enq-001',
993
+ artifactId: 'pi-art-artificer-enq-001',
994
+ resultRef: 'artificer://run-artificer-enq-001',
995
+ contextHash: 'ctx-enq',
996
+ output: {
997
+ taskId: 'task-artificer-enq-001',
998
+ sourceScribeArtifactId: 'pi-art-scribe-enq-001',
999
+ implementationPlan: {
1000
+ summary: 'Test implementation summary',
1001
+ targetSurface: 'src/test/*.ts',
1002
+ changes: ['Add validation'],
1003
+ tests: ['Unit test for validation'],
1004
+ rolloutNotes: ['Deploy behind feature flag'],
1005
+ confidence: 0.8,
1006
+ },
1007
+ sourceTrace: { scribeArtifactId: 'pi-art-scribe-enq-001' },
1008
+ risks: [],
1009
+ generatedAt: new Date().toISOString(),
1010
+ },
1011
+ attemptCount: 1,
1012
+ });
1013
+
1014
+ mockCommitNextTaskProposal.mockResolvedValue({
1015
+ decision: 'successor_created',
1016
+ sourceTaskId: 'task-artificer-enq-001',
1017
+ successorTaskId: 'task-evaluator-enq-001',
1018
+ successorKind: 'evaluator',
1019
+ });
1020
+
1021
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'artificer', runtime: 'test-double', allowTestDouble: true, enqueueNext: true, json: true });
1022
+
1023
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
1024
+ expect(output.enqueueDecision).toBe('successor_created');
1025
+ expect(output.successorTaskId).toBe('task-evaluator-enq-001');
1026
+ expect(output.successorKind).toBe('evaluator');
1027
+ });
1028
+
1029
+ it('wakeOnce is called with runnerKind as taskKind filter', async () => {
1030
+ mockWakeOnce.mockResolvedValue({
1031
+ decision: 'would_lease',
1032
+ taskId: 'task-artificer-filter',
1033
+ taskKind: 'artificer',
1034
+ });
1035
+
1036
+ mockRun.mockResolvedValue({
1037
+ status: 'succeeded',
1038
+ taskId: 'task-artificer-filter',
1039
+ runId: 'run-filter',
1040
+ artifactId: 'pi-art-filter',
1041
+ resultRef: 'artificer://run-filter',
1042
+ contextHash: 'ctx-filter',
1043
+ output: {
1044
+ taskId: 'task-artificer-filter',
1045
+ sourceScribeArtifactId: 'pi-art-scribe-filter',
1046
+ implementationPlan: {
1047
+ summary: 'Test',
1048
+ targetSurface: 'src/test.ts',
1049
+ changes: [],
1050
+ tests: [],
1051
+ rolloutNotes: [],
1052
+ confidence: 0.8,
1053
+ },
1054
+ sourceTrace: { scribeArtifactId: 'pi-art-scribe-filter' },
1055
+ risks: [],
1056
+ generatedAt: new Date().toISOString(),
1057
+ },
1058
+ attemptCount: 1,
1059
+ });
1060
+
1061
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'artificer', runtime: 'test-double', allowTestDouble: true, json: true });
1062
+
1063
+ expect(mockWakeOnce).toHaveBeenCalledWith('artificer');
1064
+ });
1065
+
1066
+ it('--runner evaluator dispatches EvaluatorRunner', async () => {
1067
+ mockWakeOnce.mockResolvedValue({
1068
+ decision: 'would_lease',
1069
+ taskId: 'task-evaluator-001',
1070
+ taskKind: 'evaluator',
1071
+ });
1072
+
1073
+ mockRun.mockResolvedValue({
1074
+ status: 'succeeded',
1075
+ taskId: 'task-evaluator-001',
1076
+ runId: 'run-evaluator-001',
1077
+ artifactId: 'pi-art-task-evaluator-001-run-evaluator-001',
1078
+ resultRef: 'evaluator://run-evaluator-001',
1079
+ contextHash: 'ctx-evaluator-abc',
1080
+ output: {
1081
+ taskId: 'task-evaluator-001',
1082
+ sourceArtificerArtifactId: 'pi-art-artificer-001',
1083
+ evaluation: {
1084
+ decision: 'approved',
1085
+ summary: 'Test evaluation summary',
1086
+ score: 0.85,
1087
+ strengths: ['Well-structured plan'],
1088
+ concerns: [],
1089
+ requiredChanges: [],
1090
+ },
1091
+ sourceTrace: { artificerArtifactId: 'pi-art-artificer-001' },
1092
+ risks: [],
1093
+ generatedAt: new Date().toISOString(),
1094
+ },
1095
+ attemptCount: 1,
1096
+ });
1097
+
1098
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'evaluator', runtime: 'test-double', allowTestDouble: true, json: true });
1099
+
1100
+ const EvaluatorRunnerMock = vi.mocked(
1101
+ await import('@principles/core/runtime-v2').then(m => m.EvaluatorRunner),
1102
+ );
1103
+ expect(EvaluatorRunnerMock).toHaveBeenCalled();
1104
+ expect(mockRun).toHaveBeenCalledWith('task-evaluator-001');
1105
+
1106
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
1107
+ expect(output.runnerKind).toBe('evaluator');
1108
+ expect(output.taskId).toBe('task-evaluator-001');
1109
+ expect(output.runId).toBe('run-evaluator-001');
1110
+ expect(output.artifactId).toBe('pi-art-task-evaluator-001-run-evaluator-001');
1111
+ expect(output.resultRef).toBe('evaluator://run-evaluator-001');
1112
+ expect(output.runnerResult.status).toBe('succeeded');
1113
+ });
1114
+
1115
+ it('--runner evaluator --enqueue-next returns successor decision', async () => {
1116
+ mockWakeOnce.mockResolvedValue({
1117
+ decision: 'would_lease',
1118
+ taskId: 'task-evaluator-enq-001',
1119
+ taskKind: 'evaluator',
1120
+ });
1121
+
1122
+ mockRun.mockResolvedValue({
1123
+ status: 'succeeded',
1124
+ taskId: 'task-evaluator-enq-001',
1125
+ runId: 'run-evaluator-enq-001',
1126
+ artifactId: 'pi-art-evaluator-enq-001',
1127
+ resultRef: 'evaluator://run-evaluator-enq-001',
1128
+ contextHash: 'ctx-enq',
1129
+ output: {
1130
+ taskId: 'task-evaluator-enq-001',
1131
+ sourceArtificerArtifactId: 'pi-art-artificer-enq-001',
1132
+ evaluation: {
1133
+ decision: 'approved',
1134
+ summary: 'Test evaluation summary',
1135
+ score: 0.85,
1136
+ strengths: ['Well-structured plan'],
1137
+ concerns: [],
1138
+ requiredChanges: [],
1139
+ },
1140
+ sourceTrace: { artificerArtifactId: 'pi-art-artificer-enq-001' },
1141
+ risks: [],
1142
+ generatedAt: new Date().toISOString(),
1143
+ },
1144
+ attemptCount: 1,
1145
+ });
1146
+
1147
+ mockCommitNextTaskProposal.mockResolvedValue({
1148
+ decision: 'successor_created',
1149
+ sourceTaskId: 'task-evaluator-enq-001',
1150
+ successorTaskId: 'task-rollout-reviewer-enq-001',
1151
+ successorKind: 'rollout_reviewer',
1152
+ });
1153
+
1154
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'evaluator', runtime: 'test-double', allowTestDouble: true, enqueueNext: true, json: true });
1155
+
1156
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
1157
+ expect(output.enqueueDecision).toBe('successor_created');
1158
+ expect(output.successorTaskId).toBe('task-rollout-reviewer-enq-001');
1159
+ expect(output.successorKind).toBe('rollout_reviewer');
1160
+ });
1161
+
1162
+ it('--runner rollout_reviewer dispatches RolloutReviewerRunner', async () => {
1163
+ mockWakeOnce.mockResolvedValue({
1164
+ decision: 'would_lease',
1165
+ taskId: 'task-rollout-reviewer-001',
1166
+ taskKind: 'rollout_reviewer',
1167
+ });
1168
+
1169
+ mockRun.mockResolvedValue({
1170
+ status: 'succeeded',
1171
+ taskId: 'task-rollout-reviewer-001',
1172
+ runId: 'run-rollout-reviewer-001',
1173
+ artifactId: 'pi-art-task-rollout-reviewer-001-run-rollout-reviewer-001',
1174
+ resultRef: 'rollout-reviewer://run-rollout-reviewer-001',
1175
+ contextHash: 'ctx-rr-abc',
1176
+ output: {
1177
+ taskId: 'task-rollout-reviewer-001',
1178
+ sourceEvaluatorArtifactId: 'pi-art-evaluator-001',
1179
+ review: {
1180
+ decision: 'approve_rollout',
1181
+ summary: 'Test rollout review summary',
1182
+ confidence: 0.9,
1183
+ requiredChanges: [],
1184
+ rolloutRisks: [],
1185
+ safetyChecks: ['Verify feature flag is properly configured'],
1186
+ },
1187
+ sourceTrace: { evaluatorArtifactId: 'pi-art-evaluator-001' },
1188
+ risks: [],
1189
+ generatedAt: new Date().toISOString(),
1190
+ },
1191
+ attemptCount: 1,
1192
+ });
1193
+
1194
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'rollout_reviewer', runtime: 'test-double', allowTestDouble: true, json: true });
1195
+
1196
+ const RolloutReviewerRunnerMock = vi.mocked(
1197
+ await import('@principles/core/runtime-v2').then(m => m.RolloutReviewerRunner),
1198
+ );
1199
+ expect(RolloutReviewerRunnerMock).toHaveBeenCalled();
1200
+ expect(mockRun).toHaveBeenCalledWith('task-rollout-reviewer-001');
1201
+
1202
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
1203
+ expect(output.runnerKind).toBe('rollout_reviewer');
1204
+ expect(output.taskId).toBe('task-rollout-reviewer-001');
1205
+ expect(output.runId).toBe('run-rollout-reviewer-001');
1206
+ expect(output.artifactId).toBe('pi-art-task-rollout-reviewer-001-run-rollout-reviewer-001');
1207
+ expect(output.resultRef).toBe('rollout-reviewer://run-rollout-reviewer-001');
1208
+ expect(output.runnerResult.status).toBe('succeeded');
1209
+ });
1210
+
1211
+ it('--runner rollout_reviewer --enqueue-next returns no_successor for prompt channel', async () => {
1212
+ mockWakeOnce.mockResolvedValue({
1213
+ decision: 'would_lease',
1214
+ taskId: 'task-rollout-reviewer-enq-001',
1215
+ taskKind: 'rollout_reviewer',
1216
+ });
1217
+
1218
+ mockRun.mockResolvedValue({
1219
+ status: 'succeeded',
1220
+ taskId: 'task-rollout-reviewer-enq-001',
1221
+ runId: 'run-rollout-reviewer-enq-001',
1222
+ artifactId: 'pi-art-rollout-reviewer-enq-001',
1223
+ resultRef: 'rollout-reviewer://run-rollout-reviewer-enq-001',
1224
+ contextHash: 'ctx-enq',
1225
+ output: {
1226
+ taskId: 'task-rollout-reviewer-enq-001',
1227
+ sourceEvaluatorArtifactId: 'pi-art-evaluator-enq-001',
1228
+ review: {
1229
+ decision: 'approve_rollout',
1230
+ summary: 'Test rollout review summary',
1231
+ confidence: 0.9,
1232
+ requiredChanges: [],
1233
+ rolloutRisks: [],
1234
+ safetyChecks: ['Verify feature flag is properly configured'],
1235
+ },
1236
+ sourceTrace: { evaluatorArtifactId: 'pi-art-evaluator-enq-001' },
1237
+ risks: [],
1238
+ generatedAt: new Date().toISOString(),
1239
+ },
1240
+ attemptCount: 1,
1241
+ });
1242
+
1243
+ mockCommitNextTaskProposal.mockResolvedValue({
1244
+ decision: 'no_successor',
1245
+ sourceTaskId: 'task-rollout-reviewer-enq-001',
1246
+ reason: 'No valid successor in job graph for this task kind and channel',
1247
+ });
1248
+
1249
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'rollout_reviewer', runtime: 'test-double', allowTestDouble: true, enqueueNext: true, json: true });
1250
+
1251
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
1252
+ expect(output.enqueueDecision).toBe('no_successor');
1253
+ });
1254
+
1255
+ it('--runtime config with missing config outputs structured JSON error', async () => {
1256
+ const { resolveRuntimeConfig, isRuntimeConfigError } = await import('@principles/core/runtime-v2');
1257
+ vi.mocked(resolveRuntimeConfig).mockReturnValue({
1258
+ ok: false,
1259
+ reason: 'explicit_config_missing',
1260
+ message: 'runtime=config requested but no workflows.yaml funnel policy found',
1261
+ nextAction: 'Create a pd-runtime-v2-diagnosis funnel policy in workflows.yaml',
1262
+ });
1263
+ vi.mocked(isRuntimeConfigError).mockReturnValue(true);
1264
+
1265
+ mockWakeOnce.mockResolvedValue({
1266
+ decision: 'would_lease',
1267
+ taskId: 'task-dreamer-cfg-err',
1268
+ taskKind: 'dreamer',
1269
+ });
1270
+
1271
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'config', json: true });
1272
+
1273
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
1274
+ expect(output.decision).toBe('config_error');
1275
+ expect(output.reason).toContain('explicit_config_missing');
1276
+ expect(output.nextAction).toBeTruthy();
1277
+ expect(process.exitCode).toBe(1);
1278
+ });
1279
+
1280
+ it('--runtime config with missing config outputs text error', async () => {
1281
+ const { resolveRuntimeConfig, isRuntimeConfigError } = await import('@principles/core/runtime-v2');
1282
+ vi.mocked(resolveRuntimeConfig).mockReturnValue({
1283
+ ok: false,
1284
+ reason: 'explicit_config_missing',
1285
+ message: 'runtime=config requested but no workflows.yaml funnel policy found',
1286
+ nextAction: 'Create a pd-runtime-v2-diagnosis funnel policy in workflows.yaml',
1287
+ });
1288
+ vi.mocked(isRuntimeConfigError).mockReturnValue(true);
1289
+
1290
+ mockWakeOnce.mockResolvedValue({
1291
+ decision: 'would_lease',
1292
+ taskId: 'task-dreamer-cfg-err2',
1293
+ taskKind: 'dreamer',
1294
+ });
1295
+
1296
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runtime: 'config', json: false });
1297
+
1298
+ expect(consoleErrorSpy.mock.calls.some((c: string[]) => c[0].includes('explicit_config_missing'))).toBe(true);
1299
+ expect(process.exitCode).toBe(1);
1300
+ });
1301
+
1302
+ it('runtime execution error outputs runtime_error in JSON mode', async () => {
1303
+ mockWakeOnce.mockResolvedValue({
1304
+ decision: 'would_lease',
1305
+ taskId: 'task-dreamer-rt-err',
1306
+ taskKind: 'dreamer',
1307
+ });
1308
+ mockRun.mockRejectedValueOnce(new Error('artifact write failed: disk full'));
1309
+
1310
+ await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: true });
1311
+
1312
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
1313
+ expect(output.decision).toBe('runtime_error');
1314
+ expect(output.reason).toContain('artifact write failed');
1315
+ expect(output.nextAction).toBeTruthy();
1316
+ expect(process.exitCode).toBe(1);
1317
+ });
1318
+ });