@principles/pd-cli 1.152.11 → 1.152.13

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 (303) hide show
  1. package/dist/commands/candidate.d.ts +3 -3
  2. package/dist/commands/candidate.d.ts.map +1 -1
  3. package/dist/commands/candidate.js +9 -21
  4. package/dist/commands/candidate.js.map +1 -1
  5. package/dist/index.js +0 -0
  6. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  7. package/dist/services/rulehost-pipeline-runner.js +4 -17
  8. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  9. package/package.json +13 -7
  10. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts +0 -9
  11. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts.map +0 -1
  12. package/dist/commands/__tests__/intent-flag-wiring.test.js +0 -237
  13. package/dist/commands/__tests__/intent-flag-wiring.test.js.map +0 -1
  14. package/dist/commands/__tests__/legacy-cleanup.test.d.ts +0 -18
  15. package/dist/commands/__tests__/legacy-cleanup.test.d.ts.map +0 -1
  16. package/dist/commands/__tests__/legacy-cleanup.test.js +0 -467
  17. package/dist/commands/__tests__/legacy-cleanup.test.js.map +0 -1
  18. package/dist/commands/__tests__/mvp-smoke.test.d.ts +0 -15
  19. package/dist/commands/__tests__/mvp-smoke.test.d.ts.map +0 -1
  20. package/dist/commands/__tests__/mvp-smoke.test.js +0 -262
  21. package/dist/commands/__tests__/mvp-smoke.test.js.map +0 -1
  22. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +0 -2
  23. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +0 -1
  24. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +0 -60
  25. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +0 -1
  26. package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts +0 -21
  27. package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts.map +0 -1
  28. package/dist/commands/__tests__/rulecode-flag-wiring.test.js +0 -186
  29. package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +0 -1
  30. package/dist/commands/__tests__/rulecode-handler.test.d.ts +0 -16
  31. package/dist/commands/__tests__/rulecode-handler.test.d.ts.map +0 -1
  32. package/dist/commands/__tests__/rulecode-handler.test.js +0 -285
  33. package/dist/commands/__tests__/rulecode-handler.test.js.map +0 -1
  34. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts +0 -24
  35. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts.map +0 -1
  36. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +0 -232
  37. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +0 -1
  38. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts +0 -15
  39. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts.map +0 -1
  40. package/dist/commands/__tests__/runtime-activation-nextaction.test.js +0 -73
  41. package/dist/commands/__tests__/runtime-activation-nextaction.test.js.map +0 -1
  42. package/dist/commands/__tests__/runtime-probe-config.test.d.ts +0 -20
  43. package/dist/commands/__tests__/runtime-probe-config.test.d.ts.map +0 -1
  44. package/dist/commands/__tests__/runtime-probe-config.test.js +0 -389
  45. package/dist/commands/__tests__/runtime-probe-config.test.js.map +0 -1
  46. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts +0 -9
  47. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts.map +0 -1
  48. package/dist/commands/__tests__/telemetry-flag-wiring.test.js +0 -76
  49. package/dist/commands/__tests__/telemetry-flag-wiring.test.js.map +0 -1
  50. package/dist/commands/__tests__/version.test.d.ts +0 -2
  51. package/dist/commands/__tests__/version.test.d.ts.map +0 -1
  52. package/dist/commands/__tests__/version.test.js +0 -238
  53. package/dist/commands/__tests__/version.test.js.map +0 -1
  54. package/dist/commands/runtime-uat.guard.test.d.ts +0 -2
  55. package/dist/commands/runtime-uat.guard.test.d.ts.map +0 -1
  56. package/dist/commands/runtime-uat.guard.test.js +0 -153
  57. package/dist/commands/runtime-uat.guard.test.js.map +0 -1
  58. package/dist/services/__tests__/cli-output.test.d.ts +0 -18
  59. package/dist/services/__tests__/cli-output.test.d.ts.map +0 -1
  60. package/dist/services/__tests__/cli-output.test.js +0 -103
  61. package/dist/services/__tests__/cli-output.test.js.map +0 -1
  62. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +0 -2
  63. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +0 -1
  64. package/dist/services/__tests__/evaluator-runner-deps.test.js +0 -343
  65. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +0 -1
  66. package/dist/services/__tests__/rulehost-readiness.test.d.ts +0 -2
  67. package/dist/services/__tests__/rulehost-readiness.test.d.ts.map +0 -1
  68. package/dist/services/__tests__/rulehost-readiness.test.js +0 -311
  69. package/dist/services/__tests__/rulehost-readiness.test.js.map +0 -1
  70. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts +0 -18
  71. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts.map +0 -1
  72. package/dist/services/__tests__/runtime-adapter-resolver.test.js +0 -673
  73. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +0 -1
  74. package/dist/utils/production-workspace-guard.test.d.ts +0 -2
  75. package/dist/utils/production-workspace-guard.test.d.ts.map +0 -1
  76. package/dist/utils/production-workspace-guard.test.js +0 -137
  77. package/dist/utils/production-workspace-guard.test.js.map +0 -1
  78. package/scripts/llm-dogfood.ts +0 -456
  79. package/scripts/migrate-illegal-expected-decision.ts +0 -252
  80. package/src/commands/__tests__/intent-flag-wiring.test.ts +0 -282
  81. package/src/commands/__tests__/legacy-cleanup.test.ts +0 -602
  82. package/src/commands/__tests__/mvp-smoke.test.ts +0 -300
  83. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +0 -73
  84. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +0 -237
  85. package/src/commands/__tests__/rulecode-handler.test.ts +0 -369
  86. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +0 -292
  87. package/src/commands/__tests__/runtime-activation-nextaction.test.ts +0 -81
  88. package/src/commands/__tests__/runtime-probe-config.test.ts +0 -432
  89. package/src/commands/__tests__/telemetry-flag-wiring.test.ts +0 -84
  90. package/src/commands/__tests__/version.test.ts +0 -251
  91. package/src/commands/admission-gate.ts +0 -30
  92. package/src/commands/artifact.ts +0 -82
  93. package/src/commands/build-trajectory-evidence.ts +0 -298
  94. package/src/commands/candidate.ts +0 -1260
  95. package/src/commands/codex-ingest-catchup.ts +0 -114
  96. package/src/commands/codex-ingest-quarantine.ts +0 -127
  97. package/src/commands/codex-reconcile.ts +0 -84
  98. package/src/commands/codex-setup.ts +0 -476
  99. package/src/commands/codex-worker.ts +0 -185
  100. package/src/commands/command-helpers.ts +0 -24
  101. package/src/commands/config-doctor.ts +0 -193
  102. package/src/commands/console.ts +0 -797
  103. package/src/commands/context.ts +0 -72
  104. package/src/commands/demo-story-a.ts +0 -241
  105. package/src/commands/diagnose.ts +0 -756
  106. package/src/commands/errors-list.ts +0 -441
  107. package/src/commands/evolution-tasks-list.ts +0 -48
  108. package/src/commands/evolution-tasks-show.ts +0 -62
  109. package/src/commands/flow.ts +0 -60
  110. package/src/commands/health-codex.ts +0 -576
  111. package/src/commands/health.ts +0 -315
  112. package/src/commands/history.ts +0 -63
  113. package/src/commands/intent.ts +0 -457
  114. package/src/commands/legacy-cleanup.ts +0 -531
  115. package/src/commands/legacy-import.ts +0 -104
  116. package/src/commands/mvp-smoke.ts +0 -160
  117. package/src/commands/pain-evidence.ts +0 -226
  118. package/src/commands/pain-list.ts +0 -239
  119. package/src/commands/pain-record.ts +0 -612
  120. package/src/commands/pain-retry.ts +0 -916
  121. package/src/commands/principles-stats.ts +0 -700
  122. package/src/commands/proven-channel-baseline.ts +0 -115
  123. package/src/commands/quality-scorecard.ts +0 -272
  124. package/src/commands/remediation-output.ts +0 -66
  125. package/src/commands/rulecode.ts +0 -465
  126. package/src/commands/run.ts +0 -89
  127. package/src/commands/runtime-activation.ts +0 -1629
  128. package/src/commands/runtime-artifact-repair.ts +0 -524
  129. package/src/commands/runtime-canary.ts +0 -395
  130. package/src/commands/runtime-compatibility-scan.ts +0 -105
  131. package/src/commands/runtime-diagnostics-export.ts +0 -229
  132. package/src/commands/runtime-features.ts +0 -153
  133. package/src/commands/runtime-gfi-snapshot.ts +0 -135
  134. package/src/commands/runtime-health-snapshot.ts +0 -108
  135. package/src/commands/runtime-init.ts +0 -429
  136. package/src/commands/runtime-internalization-enqueue-successors.ts +0 -530
  137. package/src/commands/runtime-internalization-integrity-repair.ts +0 -79
  138. package/src/commands/runtime-internalization-integrity.ts +0 -107
  139. package/src/commands/runtime-internalization-queue.ts +0 -135
  140. package/src/commands/runtime-internalization-retry.ts +0 -191
  141. package/src/commands/runtime-internalization-run-once.ts +0 -791
  142. package/src/commands/runtime-internalization-run-rulehost.ts +0 -749
  143. package/src/commands/runtime-internalization-wake-once.ts +0 -87
  144. package/src/commands/runtime-pain-flood-simulation.ts +0 -121
  145. package/src/commands/runtime-pruning.ts +0 -438
  146. package/src/commands/runtime-recovery-failed-tasks.ts +0 -258
  147. package/src/commands/runtime-recovery.ts +0 -109
  148. package/src/commands/runtime-synthetic-baseline.ts +0 -70
  149. package/src/commands/runtime-uat.guard.test.ts +0 -175
  150. package/src/commands/runtime-uat.ts +0 -380
  151. package/src/commands/runtime.ts +0 -473
  152. package/src/commands/samples-list.ts +0 -39
  153. package/src/commands/samples-review.ts +0 -36
  154. package/src/commands/task.ts +0 -271
  155. package/src/commands/telemetry.ts +0 -349
  156. package/src/commands/trace.ts +0 -174
  157. package/src/commands/trajectory-db-unavailable.ts +0 -19
  158. package/src/commands/trajectory.ts +0 -64
  159. package/src/commands/version.ts +0 -49
  160. package/src/config-reader.ts +0 -122
  161. package/src/index.ts +0 -1276
  162. package/src/legacy/legacy-import.ts +0 -179
  163. package/src/legacy/session-history-import.ts +0 -203
  164. package/src/resolve-workspace.ts +0 -106
  165. package/src/services/__tests__/cli-output.test.ts +0 -130
  166. package/src/services/__tests__/evaluator-runner-deps.test.ts +0 -382
  167. package/src/services/__tests__/rulehost-readiness.test.ts +0 -363
  168. package/src/services/__tests__/runtime-adapter-resolver.test.ts +0 -793
  169. package/src/services/cli-output.ts +0 -95
  170. package/src/services/config-doctor.ts +0 -697
  171. package/src/services/console-launcher.ts +0 -537
  172. package/src/services/demo-rule-compiler.ts +0 -40
  173. package/src/services/demo-story-a-runner.ts +0 -471
  174. package/src/services/mainline-snapshot-assembler.ts +0 -579
  175. package/src/services/pain-flood-simulation-runner.ts +0 -462
  176. package/src/services/pd-config-loader.ts +0 -433
  177. package/src/services/proven-channel-baseline-runner.ts +0 -157
  178. package/src/services/quality-scorecard/data-extractor.ts +0 -150
  179. package/src/services/quality-scorecard/local-evaluator.ts +0 -142
  180. package/src/services/quality-scorecard/strong-model-gate.ts +0 -202
  181. package/src/services/resolve-runtime-from-pd-config.ts +0 -230
  182. package/src/services/rulehost-pipeline-runner.ts +0 -1216
  183. package/src/services/rulehost-readiness.ts +0 -326
  184. package/src/services/runtime-adapter-resolver.ts +0 -364
  185. package/src/services/synthetic-baseline-runner.ts +0 -400
  186. package/src/services/version-report.ts +0 -253
  187. package/src/services/workspace-tool-semantics.ts +0 -26
  188. package/src/utils/path-security.ts +0 -96
  189. package/src/utils/production-workspace-guard.test.ts +0 -157
  190. package/src/utils/production-workspace-guard.ts +0 -210
  191. package/tests/bdd/cli-contract.steps.ts +0 -346
  192. package/tests/bdd/principles-stats.steps.ts +0 -211
  193. package/tests/bdd/support/gherkin-loader.ts +0 -147
  194. package/tests/bdd/support/repo-root.ts +0 -62
  195. package/tests/bdd/support/vitest-bdd.ts +0 -179
  196. package/tests/commands/admission-gate-coverage.test.ts +0 -79
  197. package/tests/commands/build-trajectory-evidence.test.ts +0 -508
  198. package/tests/commands/candidate-audit-repair.test.ts +0 -421
  199. package/tests/commands/candidate-intake.test.ts +0 -732
  200. package/tests/commands/candidate-internalization-backfill.test.ts +0 -548
  201. package/tests/commands/candidate-internalize-lineage.test.ts +0 -565
  202. package/tests/commands/candidate-internalize.test.ts +0 -299
  203. package/tests/commands/candidate-route.test.ts +0 -329
  204. package/tests/commands/candidate-show.test.ts +0 -95
  205. package/tests/commands/cli-command-tree.test.ts +0 -144
  206. package/tests/commands/cli-help-snapshot.test.ts +0 -155
  207. package/tests/commands/cli-skill-contract.test.ts +0 -121
  208. package/tests/commands/codex-consent-reversibility.steps.test.ts +0 -299
  209. package/tests/commands/codex-ingest-quarantine.test.ts +0 -136
  210. package/tests/commands/codex-setup.test.ts +0 -297
  211. package/tests/commands/codex-worker-daemon.test.ts +0 -95
  212. package/tests/commands/codex-worker-registration.test.ts +0 -207
  213. package/tests/commands/config-doctor.test.ts +0 -336
  214. package/tests/commands/console-launcher-edge-cases.test.ts +0 -388
  215. package/tests/commands/console-open.test.ts +0 -1381
  216. package/tests/commands/context.test.ts +0 -114
  217. package/tests/commands/demo-story-a.test.ts +0 -318
  218. package/tests/commands/diagnose.test.ts +0 -1803
  219. package/tests/commands/errors-list.test.ts +0 -679
  220. package/tests/commands/evolution-tasks-list.test.ts +0 -41
  221. package/tests/commands/evolution-tasks-show.test.ts +0 -46
  222. package/tests/commands/health-codex.test.ts +0 -349
  223. package/tests/commands/health-host-wiring.test.ts +0 -76
  224. package/tests/commands/health-reliability.test.ts +0 -99
  225. package/tests/commands/health.test.ts +0 -413
  226. package/tests/commands/intent.test.ts +0 -321
  227. package/tests/commands/legacy-cleanup.test.ts +0 -148
  228. package/tests/commands/pain-evidence.test.ts +0 -518
  229. package/tests/commands/pain-list.test.ts +0 -219
  230. package/tests/commands/pain-record-async.test.ts +0 -335
  231. package/tests/commands/pain-record-session-parser.test.ts +0 -228
  232. package/tests/commands/pain-record.test.ts +0 -851
  233. package/tests/commands/pain-retry.test.ts +0 -1121
  234. package/tests/commands/plugin-config-resolution-cutover.test.ts +0 -229
  235. package/tests/commands/pri-393-runtime-config-unification.test.ts +0 -443
  236. package/tests/commands/principles-stats-wiring.test.ts +0 -135
  237. package/tests/commands/principles-stats.test.ts +0 -274
  238. package/tests/commands/product-path-regression.test.ts +0 -86
  239. package/tests/commands/proven-channel-baseline.test.ts +0 -441
  240. package/tests/commands/rulecode.test.ts +0 -177
  241. package/tests/commands/run-rulehost-handler.test.ts +0 -908
  242. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +0 -227
  243. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +0 -167
  244. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +0 -262
  245. package/tests/commands/runtime-activation-governance-audit.test.ts +0 -95
  246. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +0 -193
  247. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +0 -248
  248. package/tests/commands/runtime-activation-shadow-telemetry.test.ts +0 -130
  249. package/tests/commands/runtime-activation.test.ts +0 -2052
  250. package/tests/commands/runtime-artifact-repair-registration.test.ts +0 -74
  251. package/tests/commands/runtime-artifact-repair.test.ts +0 -404
  252. package/tests/commands/runtime-canary.test.ts +0 -430
  253. package/tests/commands/runtime-compatibility-scan.test.ts +0 -146
  254. package/tests/commands/runtime-diagnostics-export.test.ts +0 -175
  255. package/tests/commands/runtime-features.test.ts +0 -249
  256. package/tests/commands/runtime-health-snapshot.test.ts +0 -358
  257. package/tests/commands/runtime-init-empty-workspace.test.ts +0 -252
  258. package/tests/commands/runtime-init.test.ts +0 -420
  259. package/tests/commands/runtime-internalization-enqueue-successors.test.ts +0 -782
  260. package/tests/commands/runtime-internalization-integrity-repair.test.ts +0 -239
  261. package/tests/commands/runtime-internalization-integrity.test.ts +0 -140
  262. package/tests/commands/runtime-internalization-queue.test.ts +0 -380
  263. package/tests/commands/runtime-internalization-retry-owner-authority.test.ts +0 -431
  264. package/tests/commands/runtime-internalization-run-once-evaluator-parity.test.ts +0 -269
  265. package/tests/commands/runtime-internalization-run-once-language.test.ts +0 -303
  266. package/tests/commands/runtime-internalization-run-once-real-config.test.ts +0 -237
  267. package/tests/commands/runtime-internalization-run-once-rollout-parity.test.ts +0 -706
  268. package/tests/commands/runtime-internalization-run-once.test.ts +0 -1791
  269. package/tests/commands/runtime-internalization-wake-once.test.ts +0 -171
  270. package/tests/commands/runtime-internalization.test.ts +0 -53
  271. package/tests/commands/runtime-pain-flood-simulation.test.ts +0 -418
  272. package/tests/commands/runtime-pruning.test.ts +0 -694
  273. package/tests/commands/runtime-recovery-failed-tasks.test.ts +0 -328
  274. package/tests/commands/runtime-recovery.test.ts +0 -120
  275. package/tests/commands/runtime-synthetic-baseline.test.ts +0 -249
  276. package/tests/commands/runtime-uat.test.ts +0 -397
  277. package/tests/commands/runtime.test.ts +0 -392
  278. package/tests/commands/samples-list.test.ts +0 -43
  279. package/tests/commands/samples-review.test.ts +0 -41
  280. package/tests/commands/task.test.ts +0 -153
  281. package/tests/commands/telemetry.test.ts +0 -190
  282. package/tests/commands/trace.test.ts +0 -316
  283. package/tests/config-reader.test.ts +0 -143
  284. package/tests/e2e/candidate-intake-e2e.test.ts +0 -338
  285. package/tests/e2e/cli-full-flow.test.ts +0 -233
  286. package/tests/e2e/cross-package-acceptance.test.ts +0 -635
  287. package/tests/helpers/pd-cli-path.ts +0 -5
  288. package/tests/pd-cli-smoke.test.ts +0 -13
  289. package/tests/resolve-workspace.test.ts +0 -196
  290. package/tests/services/config-doctor-signal-health.test.ts +0 -109
  291. package/tests/services/console-launcher.test.ts +0 -114
  292. package/tests/services/demo-rule-compiler.test.ts +0 -242
  293. package/tests/services/mainline-snapshot-assembler.test.ts +0 -572
  294. package/tests/services/pd-config-loader.test.ts +0 -505
  295. package/tests/services/proven-channel-baseline-runner.test.ts +0 -30
  296. package/tests/services/quality-scorecard/strong-model-gate.test.ts +0 -133
  297. package/tests/services/resolve-runtime-from-pd-config.test.ts +0 -485
  298. package/tests/services/rulehost-pipeline-e2e.test.ts +0 -538
  299. package/tests/services/rulehost-pipeline-runner.test.ts +0 -1175
  300. package/tests/services/workspace-tool-semantics.test.ts +0 -76
  301. package/tests/utils/path-security.test.ts +0 -180
  302. package/tsconfig.json +0 -26
  303. package/vitest.config.ts +0 -9
@@ -1,1216 +0,0 @@
1
- /**
2
- * RuleHost Pipeline Runner — full-chain internalization driver (PRI-429).
3
- *
4
- * Drives a pain signal all the way to a validated rule artifact in ONE call:
5
- * pain → dreamer → philosopher → scribe → artificer↔evaluator adversarial loop
6
- *
7
- * This is the "last mile" wiring that makes `runAdversarialLoop` reachable
8
- * from a real host entry point (the PRI-428 pr-review finding: the loop had no
9
- * production caller). Operators invoke it via
10
- * `pd runtime internalization run-rulehost --pain-id <id>`
11
- *
12
- * Design:
13
- * - Does NOT reuse the lease-based InternalizationOrchestrator. Instead it
14
- * chains runners directly (mirrors full-chain-real-llm.test.ts), because
15
- * the lease model is single-step + successor-proposal and cannot host a
16
- * synchronous multi-round loop.
17
- * - The artificer↔evaluator stage delegates to `runAdversarialLoop` (PRI-428),
18
- * which never throws and degrades to { decision: 'rejected' } with a reason.
19
- * - gateDeps (the sandbox adapter) is built from `compileDemoRule` +
20
- * `evaluateInRefinerSandbox`. `compileDemoRule` delegates to the canonical
21
- * hardened replay evaluator in @principles/core (PRI-809), so pre-activation
22
- * replay crosses the same vm trust boundary as the production gate.
23
- *
24
- * @see docs/plans/rulehost-mvp-activation.md
25
- * @see runAdversarialLoop in @principles/core/runtime-v2
26
- */
27
- import {
28
- RuntimeStateManager,
29
- StoreEventEmitter,
30
- DreamerRunner,
31
- DefaultDreamerValidator,
32
- PhilosopherRunner,
33
- DefaultPhilosopherValidator,
34
- ScribeRunner,
35
- DefaultScribeValidator,
36
- ArtificerRunner,
37
- DefaultArtificerValidator,
38
- EvaluatorRunner,
39
- DefaultEvaluatorValidator,
40
- createPITaskDiagnosticJson,
41
- parsePITaskMetadata,
42
- artificerRepairTaskId,
43
- runAdversarialLoop,
44
- evaluateInRefinerSandbox,
45
- DEFAULT_MAX_ROUNDS,
46
- SqliteApprovalQueueStore,
47
- getChannelRiskLevel,
48
- // PRI-510: feature-flag resolvers imported from the core barrel (EP-02:
49
- // pd-config-loader.ts only re-exports loadPdConfig + computeFlagsFromLoadResult;
50
- // computeFeatureFlagsFromConfig / isFeatureEnabled live in core).
51
- computeFeatureFlagsFromConfig,
52
- isFeatureEnabled,
53
- // PRI-714: resolve outputLanguage from the same effective config the
54
- // runners already receive (EP-07: canonical resolved value, not raw input).
55
- resolveOutputLanguage,
56
- buildArtificerHostSemanticContext,
57
- // PRI-804(a): ledger identity lookup for the source_principle_id backfill.
58
- PrincipleTreeLedgerAdapter,
59
- } from '@principles/core/runtime-v2';
60
- import type {
61
- AdversarialLoopResult,
62
- PDRuntimeAdapter,
63
- PeerRunnerResult,
64
- RefinerRuleHostGateDeps,
65
- PIArtifactStore,
66
- ApprovalRecord,
67
- BehaviorExamplePack,
68
- // PRI-510: type-only import for the repair-loop deps contract (EP-02 wiring).
69
- // RuntimeStateManager / StoreEventEmitter are already imported as values
70
- // above; EvaluatorValidator is type-only.
71
- EvaluatorRunnerDeps,
72
- SeedArtificerRepairParams,
73
- EvaluatorValidator,
74
- } from '@principles/core/runtime-v2';
75
- import * as path from 'node:path';
76
- import { loadPdConfig } from './pd-config-loader.js';
77
- import { createEvaluatorRuntimeContext } from '@principles/host-runtime';
78
- /* eslint-disable @typescript-eslint/no-use-before-define -- helpers declared after main, matching codebase convention */
79
- import { compileDemoRule } from './demo-rule-compiler.js';
80
-
81
- // ── Types ────────────────────────────────────────────────────────────────────
82
-
83
- /**
84
- * Code-rule capability (atomic: ArtificerL2 + Evaluator).
85
- *
86
- * Per the user correction (2026-06-18): ArtificerL2 and Evaluator are atomic —
87
- * both must run or neither runs. When enabled, the adversarial loop runs with
88
- * the artificerAdapter (L2 write-test-fix). When disabled or not provided, the
89
- * pipeline degrades to text-principle-only.
90
- *
91
- * The caller (CLI handler) resolves per-agent config (enabled/runtimeProfile)
92
- * and constructs the ArtificerL2Adapter when both artificer + evaluator are
93
- * enabled. The pipeline runner branches on `enabled` — it does NOT do config
94
- * resolution itself (separation of concerns: service vs I/O).
95
- */
96
- export interface CodeRuleCapability {
97
- /** Whether the capability is enabled (atomic: both artificer + evaluator enabled in config). */
98
- readonly enabled: boolean;
99
- /**
100
- * The ArtificerL2Adapter (or test-double) for the artificer stage. Required
101
- * when enabled. When enabled, this adapter replaces the base runtimeAdapter
102
- * for the ArtificerRunner only; EvaluatorRunner still uses the base adapter.
103
- * Ignored when disabled (may be omitted or set to the base adapter).
104
- */
105
- readonly artificerAdapter?: PDRuntimeAdapter;
106
- /** Structured reason when disabled (for degradation reporting). Required when disabled. */
107
- readonly disabledReason?: string;
108
- }
109
-
110
- export interface RuleHostAgentAdapters {
111
- readonly dreamer: PDRuntimeAdapter;
112
- readonly philosopher: PDRuntimeAdapter;
113
- readonly scribe: PDRuntimeAdapter;
114
- readonly evaluator: PDRuntimeAdapter;
115
- }
116
-
117
- export interface RuleHostPipelineOptions {
118
- /** Workspace directory containing .state/ (SQLite stores). */
119
- readonly workspaceDir: string;
120
- /** Pain ID whose internalization chain to drive. */
121
- readonly painId: string;
122
- /**
123
- * Runtime adapter (LLM). Caller constructs it (PiAi / test-double) so the
124
- * service stays runtime-agnostic. Mirrors full-chain-real-llm.test.ts.
125
- *
126
- * Used for: dreamer, philosopher, scribe, and evaluator (when capability ON).
127
- * The artificer stage uses codeRuleCapability.artificerAdapter when enabled.
128
- */
129
- readonly runtimeAdapter: PDRuntimeAdapter;
130
- /** Production per-agent adapters. Tests may omit this to use runtimeAdapter for every stage. */
131
- readonly agentAdapters?: RuleHostAgentAdapters;
132
- /**
133
- * Code-rule capability (atomic: ArtificerL2 + Evaluator). When omitted, the
134
- * capability is treated as OFF with reason 'code_rule_capability not provided'.
135
- */
136
- readonly codeRuleCapability?: CodeRuleCapability;
137
- /**
138
- * PRI-780: required whenever code-rule generation runs — v2 is the only
139
- * Artificer contract (no v1 fallback). Assembled from Owner-labelled
140
- * production evidence; a missing pack refuses the adversarial loop loud.
141
- */
142
- readonly behaviorExamplePack?: BehaviorExamplePack;
143
- /** Internalization channel for created tasks (default 'code_tool_hook'). */
144
- readonly channel?: 'prompt' | 'code_tool_hook' | 'defer_archive';
145
- /** Max adversarial rounds (PRD cap = 2). */
146
- readonly maxRounds?: number;
147
- /** Per-LLM-call timeout (default 300_000). */
148
- readonly timeoutMs?: number;
149
- /** Runner poll interval (default 100). */
150
- readonly pollIntervalMs?: number;
151
- /**
152
- * Max stage retry attempts for transient `retried` status (default 2).
153
- * Each retry gets fresh state from the state manager (Runtime Contract Rule 7).
154
- * When exhausted, the stage is marked 'degraded' with a structured reason.
155
- */
156
- readonly maxStageRetries?: number;
157
- /** Correlation prefix for task IDs. */
158
- readonly correlationId?: string;
159
- /** Progress callback (stage start/complete). Optional. */
160
- readonly onProgress?: (stage: string, status: 'start' | 'succeeded' | 'failed' | 'degraded' | 'skipped', detail?: string) => void;
161
- /**
162
- * Called once after the internal RuntimeStateManager + artifactStore are
163
- * constructed, so the caller (e.g. a test-double adapter) can wire the store
164
- * for artifactId resolution. Optional.
165
- */
166
- readonly onStoreReady?: (store: PIArtifactStore) => void;
167
- }
168
-
169
- export type RuleHostPipelineStageStatus = 'succeeded' | 'failed' | 'skipped' | 'degraded';
170
-
171
- export interface RuleHostPipelineStage {
172
- readonly name: 'pain_lookup' | 'dreamer' | 'philosopher' | 'scribe' | 'adversarial_loop';
173
- readonly status: RuleHostPipelineStageStatus;
174
- readonly taskId?: string;
175
- readonly reason?: string;
176
- }
177
-
178
- /**
179
- * Final pipeline decision.
180
- *
181
- * - `candidate_ready_for_owner_review`: adversarial loop approved. A validated
182
- * rule artifact exists and is WAITING for owner review. This is NOT owner
183
- * approval — it means the candidate is ready for the owner to review.
184
- * - `text_principle_only`: code-rule capability OFF (artificer or evaluator
185
- * disabled). No rule artifact; a text principle artifact is produced for the
186
- * prompt channel and enqueued into the existing approval queue (PRI-804).
187
- * - `generation_rejected`: pipeline failed (no dreamer task, stage failure, or
188
- * evaluator rejected the candidate). No rule artifact.
189
- */
190
- export interface RuleHostPipelineResult {
191
- readonly decision: 'candidate_ready_for_owner_review' | 'text_principle_only' | 'generation_rejected';
192
- readonly painId: string;
193
- readonly stages: RuleHostPipelineStage[];
194
- /** Scribe task that fed the adversarial loop (or text-principle path). */
195
- readonly scribeTaskId: string | null;
196
- /** From the adversarial loop result, when reached. */
197
- readonly adversarialLoop?: AdversarialLoopResult;
198
- /** Rule artifact ID when candidate_ready_for_owner_review; null otherwise. */
199
- readonly ruleArtifactId: string | null;
200
- /** Principle artifact ID (always present when scribe ran). */
201
- readonly principleArtifactId: string | null;
202
- /**
203
- * Approval ID when the candidate was auto-enqueued into the ApprovalQueue.
204
- * Present for decision='candidate_ready_for_owner_review' (rule artifact,
205
- * code_tool_hook channel) AND for decision='text_principle_only' (principle
206
- * artifact, prompt channel — PRI-804 wiring). Null when the enqueue itself
207
- * failed — check degradationReason for the structured reason.
208
- */
209
- readonly approvalId: string | null;
210
- /** Structured reason when decision is not candidate_ready_for_owner_review. */
211
- readonly degradationReason?: string;
212
- }
213
-
214
- // ── gateDeps builder ─────────────────────────────────────────────────────────
215
-
216
- /**
217
- * Build the sandbox gate deps for adversarial replay.
218
- *
219
- * `compileDemoRule` delegates to the canonical hardened replay evaluator in
220
- * @principles/core (PRI-809) — capability-identical across packages without a
221
- * duplicate vm implementation. See demo-rule-compiler.ts header comment.
222
- */
223
- export function createSandboxGateDeps(): RefinerRuleHostGateDeps {
224
- return {
225
- evaluateInSandbox: (code, goldenTrace, opts) => {
226
- // compileDemoRule throws on bad code. The throw propagates out of
227
- // evaluateInSandbox; EvaluatorRunner (and runAdversarialLoop's
228
- // evaluator_run_threw catch) classifies it as a sandbox failure, so it
229
- // surfaces as a rejected round rather than an uncaught exception.
230
- // evaluateInRefinerSandbox separately catches its own runtime throws
231
- // (from executing the evaluate function) and classifies them as
232
- // validation_failed / runtime_error.
233
- const evaluateCode = compileDemoRule(code, 'rulehost-pipeline');
234
- return evaluateInRefinerSandbox(code, goldenTrace, { evaluateCode, ...opts });
235
- },
236
- };
237
- }
238
-
239
- // ── Pipeline ─────────────────────────────────────────────────────────────────
240
-
241
- const DEFAULT_TIMEOUT_MS = 300_000;
242
- const DEFAULT_POLL_MS = 100;
243
- const DEFAULT_MAX_STAGE_RETRIES = 2;
244
-
245
- export async function runRuleHostPipeline(opts: RuleHostPipelineOptions): Promise<RuleHostPipelineResult> {
246
- const channel = opts.channel ?? 'code_tool_hook';
247
- const maxRounds = opts.maxRounds ?? DEFAULT_MAX_ROUNDS;
248
- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
249
- const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_MS;
250
- const maxStageRetries = opts.maxStageRetries ?? DEFAULT_MAX_STAGE_RETRIES;
251
- const correlation = opts.correlationId ?? `rulehost-${opts.painId}`;
252
- const onProgress = opts.onProgress ?? (() => { /* noop */ });
253
-
254
- // ── Resolve code-rule capability (atomic: ArtificerL2 + Evaluator) ──
255
- // Per user correction (2026-06-18): both must be enabled or neither runs.
256
- // When OFF, degrade to text-principle-only after scribe. When ON, run the
257
- // adversarial loop with the artificerAdapter (L2 write-test-fix).
258
- const capability = opts.codeRuleCapability;
259
- const capabilityEnabled = capability?.enabled === true;
260
- const capabilityDisabledReason = capability?.enabled === false
261
- ? (capability.disabledReason ?? 'code_rule_capability is disabled')
262
- : 'code_rule_capability not provided — default OFF (set codeRuleCapability.enabled=true when both artificer + evaluator agents are enabled)';
263
-
264
- const stages: RuleHostPipelineStage[] = [];
265
- const stateManager = new RuntimeStateManager({ workspaceDir: opts.workspaceDir });
266
- await stateManager.initialize();
267
-
268
- try {
269
- const artifactStore = stateManager.piArtifactStore;
270
- const eventEmitter = new StoreEventEmitter();
271
- // Issue 2: load effective config once so every stage runner can resolve
272
- // feature flags (e.g. `artificer_output_retry`). Mirrors
273
- // createEvaluatorRunnerDeps (rc-9: malformed config → fallback defaults).
274
- const configLoad = loadPdConfig(opts.workspaceDir);
275
- const effectiveConfig = configLoad.ok ? configLoad.effective : configLoad.defaults;
276
- // PRI-714: resolve outputLanguage once from the effective config so every
277
- // stage runner (dreamer/philosopher/scribe/artificer/evaluator) receives
278
- // the same language value. Mirrors pain-signal-runtime-factory (EP-07).
279
- // Default config also resolves (to zh-CN) — pipeline stages always
280
- // declare the owner's language instead of leaving review artifacts English.
281
- const {outputLanguage} = resolveOutputLanguage(effectiveConfig.config.principles?.outputLanguage);
282
- // Allow the caller's adapter to resolve real artifactIds (needed by
283
- // test-double adapters whose scripted outputs must match store-assigned IDs).
284
- opts.onStoreReady?.(artifactStore);
285
- const owner = 'rulehost-pipeline';
286
- const agentAdapters = opts.agentAdapters ?? {
287
- dreamer: opts.runtimeAdapter,
288
- philosopher: opts.runtimeAdapter,
289
- scribe: opts.runtimeAdapter,
290
- evaluator: opts.runtimeAdapter,
291
- };
292
- const runnerOptsFor = (adapter: PDRuntimeAdapter) => ({ owner, runtimeKind: adapter.kind(), pollIntervalMs, timeoutMs, effectiveConfig, outputLanguage });
293
-
294
- // ── Stage: pain lookup ──
295
- // Find a dreamer task already seeded for this pain (the pain→dreamer bridge
296
- // runs via `pd pain record` → PainSignalBridge.onPainDetected). We need the
297
- // dreamer task to start the chain. If none exists, the operator must run
298
- // `pd pain record` first — fail loud with guidance.
299
- //
300
- // D fix (PRI-429): exact sourcePainId match via Object.hasOwn on parsed
301
- // diagnosticJson. No substring matching (pain-1 must NOT match pain-10).
302
- onProgress('pain_lookup', 'start', `painId=${opts.painId}`);
303
- const dreamerLookup = await findDreamerTaskForPain(stateManager, opts.painId, channel);
304
- if (dreamerLookup.status === 'ambiguous') {
305
- const reason = `ambiguous_dreamer_tasks_for_pain: ${dreamerLookup.taskIds.join(',')}`;
306
- stages.push({ name: 'pain_lookup', status: 'failed', reason });
307
- onProgress('pain_lookup', 'failed', reason);
308
- return rejectedResult(opts.painId, stages, reason);
309
- }
310
- if (dreamerLookup.status === 'not_found') {
311
- stages.push({ name: 'pain_lookup', status: 'failed', reason: 'no_dreamer_task_seeded_for_pain' });
312
- onProgress('pain_lookup', 'failed', 'no dreamer task seeded');
313
- return rejectedResult(opts.painId, stages, 'no_dreamer_task_seeded_for_pain: run `pd pain record` first');
314
- }
315
- const dreamerSeedTaskId = dreamerLookup.taskId;
316
- stages.push({ name: 'pain_lookup', status: 'succeeded', taskId: dreamerSeedTaskId });
317
- onProgress('pain_lookup', 'succeeded', `dreamerTaskId=${dreamerSeedTaskId}`);
318
-
319
- // ── Stage: dreamer ──
320
- onProgress('dreamer', 'start');
321
- if (dreamerLookup.taskStatus === 'succeeded') {
322
- stages.push({ name: 'dreamer', taskId: dreamerSeedTaskId, status: 'succeeded' });
323
- } else {
324
- const dreamerRunner = new DreamerRunner(
325
- { stateManager, runtimeAdapter: agentAdapters.dreamer, eventEmitter, validator: new DefaultDreamerValidator(), artifactStore },
326
- runnerOptsFor(agentAdapters.dreamer),
327
- );
328
- const dreamerResult = await runStage(dreamerRunner, dreamerSeedTaskId, { maxStageRetries, pollIntervalMs });
329
- stages.push(stageFromResult('dreamer', dreamerSeedTaskId, dreamerResult));
330
- if (dreamerResult.status !== 'succeeded') {
331
- onProgress('dreamer', 'failed', dreamerResult.failureReason);
332
- return rejectedResult(opts.painId, stages, `dreamer_failed: ${dreamerResult.failureReason ?? dreamerResult.status}`);
333
- }
334
- }
335
- onProgress('dreamer', 'succeeded');
336
-
337
- // ── Stage: philosopher ──
338
- onProgress('philosopher', 'start');
339
- const philosopherTaskId = `${correlation}-philosopher-${Date.now().toString(36)}`;
340
- await createInternalizationTask(stateManager, philosopherTaskId, 'philosopher', [dreamerSeedTaskId], channel, timeoutMs);
341
- const philosopherRunner = new PhilosopherRunner(
342
- { stateManager, runtimeAdapter: agentAdapters.philosopher, eventEmitter, validator: new DefaultPhilosopherValidator(), artifactStore },
343
- runnerOptsFor(agentAdapters.philosopher),
344
- );
345
- const philosopherResult = await runStage(philosopherRunner, philosopherTaskId, { maxStageRetries, pollIntervalMs });
346
- stages.push(stageFromResult('philosopher', philosopherTaskId, philosopherResult));
347
- if (philosopherResult.status !== 'succeeded') {
348
- onProgress('philosopher', 'failed', philosopherResult.failureReason);
349
- return rejectedResult(opts.painId, stages, `philosopher_failed: ${philosopherResult.failureReason ?? philosopherResult.status}`);
350
- }
351
- onProgress('philosopher', 'succeeded');
352
-
353
- // ── Stage: scribe ──
354
- onProgress('scribe', 'start');
355
- const scribeTaskId = `${correlation}-scribe-${Date.now().toString(36)}`;
356
- await createInternalizationTask(stateManager, scribeTaskId, 'scribe', [philosopherTaskId], channel, timeoutMs);
357
- const scribeRunner = new ScribeRunner(
358
- { stateManager, runtimeAdapter: agentAdapters.scribe, eventEmitter, validator: new DefaultScribeValidator(), artifactStore },
359
- runnerOptsFor(agentAdapters.scribe),
360
- );
361
- const scribeResult = await runStage(scribeRunner, scribeTaskId, { maxStageRetries, pollIntervalMs });
362
- stages.push(stageFromResult('scribe', scribeTaskId, scribeResult));
363
- if (scribeResult.status !== 'succeeded') {
364
- onProgress('scribe', 'failed', scribeResult.failureReason);
365
- return rejectedResult(opts.painId, stages, `scribe_failed: ${scribeResult.failureReason ?? scribeResult.status}`);
366
- }
367
- onProgress('scribe', 'succeeded');
368
-
369
- // EP002-R4 follow-up #2 + Owner Decision Experience v1 Phase A: backfill
370
- // the LEDGER principle UUID on the scribe artifact for BOTH paths (rule +
371
- // text). Downstream identity resolution (extractPrincipleId's column-first
372
- // order) then binds the rule artifact, the approval grouping, and the
373
- // post-approve ledger upgrade to the ledger UUID instead of falling back
374
- // to principleDraft.title — the title namespace is what left rule-channel
375
- // approvals stranded as title-keyed groups the Console detail page cannot
376
- // reach.
377
- //
378
- // Phase A (SPEC §9.1): the binding outcome is STRUCTURED. An unverified
379
- // binding no longer silently continues into Owner approval publication —
380
- // the enqueue sites below refuse to create an approval subject for an
381
- // artifact whose stable ledger identity is not established, while the
382
- // generation chain and its artifacts are preserved for later replay.
383
- const identityBinding = await backfillScribeIdentity({
384
- stateManager, artifactStore, scribeTaskId, dreamerTaskId: dreamerSeedTaskId,
385
- workspaceDir: opts.workspaceDir, now: new Date().toISOString(),
386
- });
387
- if (scribeIdentityNote(identityBinding) !== '') {
388
- const scribeStage = stages.find((s) => s.name === 'scribe');
389
- if (scribeStage) (scribeStage as { reason?: string }).reason = scribeIdentityNote(identityBinding).replace(/^; /, '');
390
- }
391
-
392
- // ── Atomic capability branching ──
393
- // Per user correction (2026-06-18): ArtificerL2 + Evaluator are atomic.
394
- // When OFF (or not provided), skip the adversarial loop entirely and
395
- // degrade to text-principle-only. The scribe's principle artifact remains
396
- // for prompt-channel fallback.
397
- if (!capabilityEnabled) {
398
- stages.push({ name: 'adversarial_loop', status: 'skipped', reason: capabilityDisabledReason });
399
- onProgress('adversarial_loop', 'skipped', capabilityDisabledReason);
400
- // PRI-804: text principles enter the existing prompt-channel approval
401
- // queue (Owner governance, same gate as RuleCode) instead of dead-ending.
402
- const approvalStore = new SqliteApprovalQueueStore(stateManager.connection);
403
- return await textPrincipleOnlyResult({
404
- painId: opts.painId, stages, scribeTaskId, disabledReason: capabilityDisabledReason,
405
- artifactStore, approvalStore, now: new Date().toISOString(),
406
- identityBinding,
407
- });
408
- }
409
-
410
- // ── Stage: adversarial loop (artificer↔evaluator) ──
411
- // Capability ON: use the artificerAdapter (L2 write-test-fix) for the
412
- // artificer stage, and the base runtimeAdapter for the evaluator stage.
413
- if (!capability?.artificerAdapter) {
414
- // Contract violation: enabled but no adapter provided. Fail loud.
415
- stages.push({ name: 'adversarial_loop', status: 'failed', reason: 'artificerAdapter not provided' });
416
- onProgress('adversarial_loop', 'failed', 'artificerAdapter not provided');
417
- return rejectedResult(opts.painId, stages, 'code_rule_capability enabled but artificerAdapter not provided');
418
- }
419
- // PRI-780: v2-only generation — an enabled code-rule capability without
420
- // an Owner-labelled BehaviorExamplePack refuses BEFORE any artificer LLM
421
- // stage runs. Never degrade to an action-only (v1) rule.
422
- if (!opts.behaviorExamplePack) {
423
- const reason = 'behavior_example_pack_missing: v2 is the only RuleCode generation contract (PRI-780); provide Owner-labelled behavior examples via --behavior-examples';
424
- stages.push({ name: 'adversarial_loop', status: 'failed', reason });
425
- onProgress('adversarial_loop', 'failed', reason);
426
- return rejectedResult(opts.painId, stages, reason);
427
- }
428
- onProgress('adversarial_loop', 'start');
429
- // PRI-661: the loop's evaluator replay must use the SAME production gate
430
- // context as the consumer cycle and the activation gate (workspace root +
431
- // durable host tool registry). The previous sandbox-only gateDeps could
432
- // pass generation-time replay and fail the production activation gate on
433
- // the same workspace — the drift class PRI-634-F closed elsewhere.
434
- // Unresolvable provenance fails the loop LOUD (never a silent baseline
435
- // fallback — ERR-114), mirroring the artificerAdapter-not-provided failure
436
- // precedent below.
437
- const evaluatorContext = createEvaluatorRuntimeContext({ workspaceDir: opts.workspaceDir });
438
- if (!evaluatorContext.ok) {
439
- const refusalReason = `${evaluatorContext.reason} — ${evaluatorContext.nextAction}`;
440
- stages.push({ name: 'adversarial_loop', status: 'failed', reason: refusalReason });
441
- onProgress('adversarial_loop', 'failed', refusalReason);
442
- return rejectedResult(opts.painId, stages, `evaluator_runtime_context_unresolvable: ${evaluatorContext.reason} (nextAction: ${evaluatorContext.nextAction})`);
443
- }
444
- // PRI-741: artificer generation + evaluator parity BOTH anchor on the ONE
445
- // registry snapshot evaluatorContext resolved above (review round: a
446
- // second independent resolveWorkspaceHostToolSemantics read could observe
447
- // a different declaration snapshot than the replay gate). Unresolvable
448
- // provenance already failed loud at the evaluatorContext refusal above.
449
- const hostSemanticContext = buildArtificerHostSemanticContext(evaluatorContext.registry, evaluatorContext.hostKinds ?? []);
450
- const artificerRunner = new ArtificerRunner(
451
- {
452
- stateManager, runtimeAdapter: capability.artificerAdapter, eventEmitter, validator: new DefaultArtificerValidator(), artifactStore,
453
- behaviorExamplePack: opts.behaviorExamplePack,
454
- },
455
- {
456
- ...runnerOptsFor(capability.artificerAdapter),
457
- hostSemanticContext,
458
- },
459
- );
460
- // PRI-510 (DEFECT-004): construct EvaluatorRunnerDeps via the centralized
461
- // helper so the repair-loop wiring (isRepairLoopEnabled + seeder) is
462
- // actually invoked in the production CLI path. EP-02: prior code passed
463
- // only the 5 base deps, leaving the repair loop as dead code at runtime.
464
- const evaluatorRunner = new EvaluatorRunner(
465
- createEvaluatorRunnerDeps({
466
- stateManager,
467
- runtimeAdapter: agentAdapters.evaluator,
468
- eventEmitter,
469
- validator: new DefaultEvaluatorValidator(),
470
- artifactStore,
471
- workspaceDir: opts.workspaceDir,
472
- }),
473
- // PRI-661: production gate context (workspace root + host tool registry
474
- // from durable provenance) — replaces the previous sandbox-only gate
475
- // deps so generation-time replay verdicts match the activation gate on
476
- // the same workspace. Options argument (2nd), never deps (1st).
477
- {
478
- ...runnerOptsFor(agentAdapters.evaluator),
479
- gateDeps: evaluatorContext.gateDeps,
480
- // PRI-741: host-name parity replay case from the SAME registry
481
- // snapshot as the gateDeps above (single resolution).
482
- hostSemanticContext,
483
- },
484
- );
485
-
486
- const loopResult = await runAdversarialLoop({
487
- artificerRunner,
488
- evaluatorRunner,
489
- stateManager,
490
- artifactStore,
491
- scribeTaskId,
492
- maxRounds,
493
- correlationId: correlation,
494
- channel,
495
- });
496
-
497
- const loopStatus: RuleHostPipelineStageStatus = loopResult.decision === 'approved' ? 'succeeded' : 'degraded';
498
- stages.push({ name: 'adversarial_loop', status: loopStatus, taskId: loopResult.finalEvaluatorTaskId, reason: loopResult.degradationReason });
499
- onProgress('adversarial_loop', loopResult.decision === 'approved' ? 'succeeded' : 'degraded', loopResult.degradationReason);
500
-
501
- // Map adversarial loop decision to pipeline decision:
502
- // 'approved' → candidate_ready_for_owner_review (NOT owner approval)
503
- // 'rejected' → generation_rejected
504
- const pipelineDecision = loopResult.decision === 'approved'
505
- ? 'candidate_ready_for_owner_review' as const
506
- : 'generation_rejected' as const;
507
-
508
- // P1 #1 fix: auto-enqueue the candidate into the ApprovalQueue so the
509
- // owner can review it. Without this, the pipeline produces a candidate
510
- // artifact but it never enters the approval queue — the production chain
511
- // is broken at step 2→3. Tests manually called enqueue(); production did not.
512
- //
513
- // Owner Decision Experience v1 Phase A (SPEC §9.1.4/§9.1.6): creating an
514
- // Owner approval subject is a governance publication — it requires a
515
- // verified stable ledger identity. When the chain's candidate could not be
516
- // bound to exactly one existing ledger principle, the rule artifact must
517
- // NOT enter the approvable set, even though the artifact itself (and every
518
- // intermediate artifact) is preserved. Recovery is replay: after the
519
- // ledger/identity repair, re-running the pipeline re-attempts the binding
520
- // idempotently and then publishes.
521
- let approvalId: string | null = null;
522
- if (pipelineDecision === 'candidate_ready_for_owner_review' && loopResult.ruleArtifactId) {
523
- const ruleIdentity = await resolveRuleArtifactIdentity(artifactStore, loopResult.ruleArtifactId, identityBinding);
524
- if (ruleIdentity === null) {
525
- const reasonCode = scribeIdentityReasonCode(identityBinding);
526
- return {
527
- decision: pipelineDecision,
528
- painId: opts.painId,
529
- stages,
530
- scribeTaskId,
531
- adversarialLoop: loopResult,
532
- ruleArtifactId: loopResult.ruleArtifactId,
533
- principleArtifactId: loopResult.principleArtifactId,
534
- approvalId: null,
535
- degradationReason: `identity_binding_unverified: ${reasonCode}. Rule artifact NOT enqueued for Owner approval (governance publication boundary). Artifacts preserved; repair the ledger identity and re-run this pipeline to publish.`,
536
- };
537
- }
538
- try {
539
- const approvalStore = new SqliteApprovalQueueStore(stateManager.connection);
540
- const riskLevel = getChannelRiskLevel(channel);
541
- const enqueuedRecord: ApprovalRecord = await approvalStore.enqueue({
542
- artifactId: loopResult.ruleArtifactId,
543
- channel,
544
- riskLevel,
545
- summary: `RuleHost pipeline candidate for pain ${opts.painId}`,
546
- triggerReason: `adversarial_loop_approved: pain=${opts.painId}, rule=${loopResult.ruleArtifactId}`,
547
- }, new Date().toISOString());
548
- const { approvalId: enqueuedApprovalId } = enqueuedRecord;
549
- approvalId = enqueuedApprovalId;
550
- onProgress('adversarial_loop', 'succeeded', `auto-enqueued as approval ${approvalId}`);
551
- } catch (err: unknown) {
552
- // Enqueue failed — the candidate artifact exists but is not in the
553
- // approval queue. Degrade gracefully with a structured reason (ERR-002).
554
- const enqueueErr = err instanceof Error ? err.message : String(err);
555
- const degradeReason = `candidate_approved_but_enqueue_failed: ${enqueueErr}. Manual enqueue required: pd activation dispatch --artifact-id ${loopResult.ruleArtifactId} --channel ${channel}`;
556
- return {
557
- decision: pipelineDecision,
558
- painId: opts.painId,
559
- stages,
560
- scribeTaskId,
561
- adversarialLoop: loopResult,
562
- ruleArtifactId: loopResult.ruleArtifactId,
563
- principleArtifactId: loopResult.principleArtifactId,
564
- approvalId: null,
565
- degradationReason: degradeReason,
566
- };
567
- }
568
- }
569
-
570
- return {
571
- decision: pipelineDecision,
572
- painId: opts.painId,
573
- stages,
574
- scribeTaskId,
575
- adversarialLoop: loopResult,
576
- ruleArtifactId: loopResult.ruleArtifactId,
577
- principleArtifactId: loopResult.principleArtifactId,
578
- approvalId,
579
- degradationReason: loopResult.degradationReason,
580
- };
581
- } finally {
582
- await stateManager.close();
583
- }
584
- }
585
-
586
- // ── Helpers ──────────────────────────────────────────────────────────────────
587
-
588
- /**
589
- * Find the dreamer task seeded for a given pain ID.
590
- *
591
- * D fix (PRI-429): exact sourcePainId match via Object.hasOwn on parsed
592
- * diagnosticJson. No substring matching — 'pain-1' must NOT match 'pain-10'.
593
- *
594
- * The sourcePainId is stored as a top-level key in diagnosticJson (outside the
595
- * pi_metadata envelope), mirroring the pattern in source-trace-locator.test.ts
596
- * and PainSignalBridge. Malformed JSON is skipped (not crashed). Missing
597
- * sourcePainId is skipped (no match).
598
- *
599
- * ERR refs:
600
- * - ERR-001: parsed JSON treated as unknown
601
- * - ERR-005/007: no `as` bypass; type narrowing via typeof + Object.hasOwn
602
- * - ERR-013: Object.hasOwn for untrusted key checks
603
- * - ERR-009: missing sourcePainId = no match (fail loud, not silent skip)
604
- */
605
- type DreamerTaskLookup =
606
- | { readonly status: 'found'; readonly taskId: string; readonly taskStatus: 'pending' | 'retry_wait' | 'succeeded' }
607
- | { readonly status: 'not_found' }
608
- | { readonly status: 'ambiguous'; readonly taskIds: readonly string[] };
609
-
610
- async function findDreamerTaskForPain(
611
- stateManager: RuntimeStateManager,
612
- painId: string,
613
- channel: 'prompt' | 'code_tool_hook' | 'defer_archive',
614
- ): Promise<DreamerTaskLookup> {
615
- const tasks = await stateManager.listTasks();
616
- const dreamerTasks = tasks.filter((t) =>
617
- t.taskKind === 'dreamer' && (t.status === 'pending' || t.status === 'retry_wait' || t.status === 'succeeded'),
618
- );
619
- const allMatches: typeof dreamerTasks = [];
620
- const channelMatches: typeof dreamerTasks = [];
621
- for (const t of dreamerTasks) {
622
- if (typeof t.diagnosticJson !== 'string') continue;
623
- const metadata = parsePITaskMetadata(t.diagnosticJson);
624
- if (!metadata) continue;
625
- let parsed: unknown;
626
- try {
627
- parsed = JSON.parse(t.diagnosticJson);
628
- } catch {
629
- // Malformed JSON — skip this task, don't crash (Runtime Contract Rule 9:
630
- // graceful degradation with observable behavior — the task is simply not
631
- // a match, and the caller will report no_dreamer_task_seeded_for_pain).
632
- continue;
633
- }
634
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) continue;
635
- if (!Object.hasOwn(parsed, 'sourcePainId')) continue;
636
- const stored = Reflect.get(parsed, 'sourcePainId');
637
- if (typeof stored === 'string' && stored === painId) {
638
- allMatches.push(t);
639
- if (metadata.channel === channel) channelMatches.push(t);
640
- }
641
- }
642
- const matches = channelMatches.length > 0 ? channelMatches : allMatches;
643
- matches.sort((left, right) => left.taskId.localeCompare(right.taskId));
644
- if (matches.length === 0) return { status: 'not_found' };
645
- if (matches.length > 1) return { status: 'ambiguous', taskIds: matches.map((task) => task.taskId) };
646
- const [match] = matches;
647
- if (!match || (match.status !== 'pending' && match.status !== 'retry_wait' && match.status !== 'succeeded')) {
648
- return { status: 'not_found' };
649
- }
650
- return { status: 'found', taskId: match.taskId, taskStatus: match.status };
651
- }
652
-
653
- // eslint-disable-next-line @typescript-eslint/max-params
654
- async function createInternalizationTask(
655
- stateManager: RuntimeStateManager,
656
- taskId: string,
657
- taskKind: string,
658
- dependencyTaskIds: string[],
659
- channel: 'prompt' | 'code_tool_hook' | 'defer_archive',
660
- timeoutMs: number,
661
- ): Promise<void> {
662
- await stateManager.createTask({
663
- taskId,
664
- taskKind,
665
- status: 'pending',
666
- attemptCount: 0,
667
- maxAttempts: 3,
668
- diagnosticJson: createPITaskDiagnosticJson({
669
- dependencyTaskIds,
670
- channel,
671
- timeoutMs,
672
- inputArtifactRefs: [],
673
- outputArtifactRefs: [],
674
- }),
675
- });
676
- }
677
-
678
- // ── PRI-510 (DEFECT-004): EvaluatorRunner repair-loop CLI wiring ────────────
679
- //
680
- // EP-02 (Production Path Wiring): PRI-509 added the `isRepairLoopEnabled` and
681
- // `seedArtificerRepairTask` deps to `EvaluatorRunnerDeps` (core), but the two
682
- // CLI production paths (runtime-internalization-run-once.ts and this file)
683
- // constructed `EvaluatorRunner` with only the base deps — the repair loop was
684
- // dead code at runtime (evaluator needs_revision seeded nothing).
685
- //
686
- // This helper centralizes the construction of the full deps so both CLI sites
687
- // stay in sync. The flag resolver reads `.pd/config.yaml` (the unified config
688
- // — ADR-0016); the seeder writes the repair task via `stateManager.createTask`
689
- // (the only sanctioned task-creation path; core peer runners are forbidden
690
- // from calling it directly — architecture-regression.test.ts enforces this).
691
-
692
- /**
693
- * Inputs for {@link createEvaluatorRunnerDeps}. Carries the base `PeerRunnerDeps`
694
- * fields plus `workspaceDir` (used to resolve the feature flag from
695
- * `.pd/config.yaml`).
696
- */
697
- export interface CreateEvaluatorRunnerDepsInputs {
698
- readonly stateManager: RuntimeStateManager;
699
- readonly runtimeAdapter: PDRuntimeAdapter;
700
- readonly eventEmitter: StoreEventEmitter;
701
- readonly validator: EvaluatorValidator;
702
- readonly artifactStore: PIArtifactStore;
703
- /** Workspace directory containing `.pd/config.yaml` (flag source). */
704
- readonly workspaceDir: string;
705
- }
706
-
707
- /**
708
- * Build the full `EvaluatorRunnerDeps` for CLI production paths, including the
709
- * PRI-509 repair-loop wiring (EP-02: production path must invoke core logic).
710
- *
711
- * - `isRepairLoopEnabled`: reads `.pd/config.yaml` and resolves the
712
- * `evaluator_artificer_repair_loop` flag. Defaults to false when the config
713
- * is missing or malformed (rc-9: fail safe — legacy path runs, never throws).
714
- * - `seedArtificerRepairTask`: creates a new `artificer` task carrying the
715
- * `repairPayload` in `diagnosticJson` (rc-1, rc-6: serialized via the
716
- * validated `createPITaskDiagnosticJson`, no `as` bypass). Reuses the
717
- * artificer runner (PRI-509 D1) rather than introducing a new task kind.
718
- *
719
- * Returns the newly created repair task's ID (UUIDv4, rc-7: fresh per call).
720
- */
721
- export function createEvaluatorRunnerDeps(inputs: CreateEvaluatorRunnerDepsInputs): EvaluatorRunnerDeps {
722
- const { stateManager, runtimeAdapter, eventEmitter, validator, artifactStore, workspaceDir } = inputs;
723
- return {
724
- stateManager,
725
- runtimeAdapter,
726
- eventEmitter,
727
- validator,
728
- artifactStore,
729
- isRepairLoopEnabled: (): boolean => {
730
- // rc-9: never throw on malformed config — fail safe to false so the
731
- // legacy (non-repair) path runs. The malformed config is already
732
- // surfaced by `pd config doctor` and CLI start-up warnings.
733
- try {
734
- const result = loadPdConfig(workspaceDir);
735
- const effective = result.ok ? result.effective : result.defaults;
736
- const flags = computeFeatureFlagsFromConfig(effective);
737
- return isFeatureEnabled(flags, 'evaluator_artificer_repair_loop');
738
- } catch {
739
- return false;
740
- }
741
- },
742
- seedArtificerRepairTask: async (params: SeedArtificerRepairParams): Promise<string> => {
743
- // rc-7: each call gets a fresh task ID — never reuse a cached ID.
744
- // P0-4: deterministic revision identity + reuse on replay.
745
- // PRI-718: id 约定收敛到 pitask-metadata 的单一 owner。
746
- const repairTaskId = artificerRepairTaskId(
747
- params.repairPayload.sourceEvaluatorTaskId,
748
- params.repairPayload.repairIteration,
749
- );
750
- const existing = await stateManager.getTask(repairTaskId);
751
- if (existing) {
752
- // PRI-718 (revise ≠ resume): terminal repair rounds are finished
753
- // corrective work — never vehicles for new corrective work. In-flight
754
- // rounds are the legitimate replay-reuse population.
755
- if (existing.status === 'succeeded' || existing.status === 'failed' || existing.status === 'needs_human_review') {
756
- throw new Error(
757
- `repair task ${repairTaskId} already reached terminal status ${existing.status}; refusing to reuse it as new corrective work (PRI-718 revise-ne-resume)`,
758
- );
759
- }
760
- return repairTaskId;
761
- }
762
- await stateManager.createTask({
763
- taskId: repairTaskId,
764
- // D1 (PRI-509): task kind is 'artificer' — reuses the artificer
765
- // runner, which detects repairPayload in diagnosticJson and
766
- // forwards it to the prompt builder as repairFeedback.
767
- taskKind: 'artificer',
768
- status: 'pending',
769
- attemptCount: 0,
770
- maxAttempts: 3,
771
- // rc-1, rc-6: serialize via the validated helper — repairPayload
772
- // is untrusted LLM output but `createPITaskDiagnosticJson` writes
773
- // it through `serializePITaskMetadata`, which `parsePITaskMetadata`
774
- // re-validates on read (defense in depth).
775
- diagnosticJson: createPITaskDiagnosticJson({
776
- // PITaskMetadata fields are mutable arrays (the metadata envelope
777
- // serializes them as JSON arrays). The SeedArtificerRepairParams
778
- // contract carries readonly arrays (rc-1: untrusted input from the
779
- // evaluator LLM). Spread to a fresh mutable array — no mutation of
780
- // the caller's data.
781
- dependencyTaskIds: [...params.inheritedDependencyTaskIds],
782
- channel: params.inheritedChannel,
783
- timeoutMs: params.inheritedTimeoutMs,
784
- inputArtifactRefs: [...params.inheritedInputArtifactRefs],
785
- outputArtifactRefs: [],
786
- repairPayload: params.repairPayload,
787
- }),
788
- });
789
- return repairTaskId;
790
- },
791
- };
792
- }
793
-
794
- /**
795
- * Run a single pipeline stage with bounded retry for transient `retried` status.
796
- *
797
- * Runtime Contract Rule 7: retry/repair loops must distinguish current, next,
798
- * and recorded state. Each iteration calls `runner.run(taskId)` fresh — the
799
- * runner reads from the state manager which has the updated task state
800
- * (including any `retry_wait` → `pending` transition from the previous
801
- * iteration). No stale state is carried across iterations.
802
- *
803
- * `retried` is NOT terminal — it means the runner hit a transient error and
804
- * marked the task for retry. The pipeline retries up to `maxStageRetries`
805
- * times (default 2). When exhausted, the stage is marked 'degraded' with a
806
- * structured reason, and the caller treats it as a non-success outcome.
807
- */
808
- interface RunStageOptions {
809
- readonly maxStageRetries: number;
810
- readonly pollIntervalMs: number;
811
- }
812
-
813
- async function runStage(
814
- runner: { run(id: string): Promise<PeerRunnerResult<unknown>> },
815
- taskId: string,
816
- opts: RunStageOptions,
817
- ): Promise<PeerRunnerResult<unknown>> {
818
- let attempt = 0;
819
- // First attempt is not a retry — it's the initial run.
820
- let result = await runner.run(taskId);
821
- while (result.status === 'retried' && attempt < opts.maxStageRetries) {
822
- attempt++;
823
- // Brief wait before retry to allow transient conditions to clear.
824
- // The state manager has already marked the task as retry_wait; the runner
825
- // will re-read it on the next .run() call (fresh state per iteration).
826
- await new Promise<void>((resolve) => setTimeout(resolve, opts.pollIntervalMs));
827
- result = await runner.run(taskId);
828
- }
829
- return result;
830
- }
831
-
832
- function stageFromResult(
833
- name: RuleHostPipelineStage['name'],
834
- taskId: string,
835
- result: PeerRunnerResult<unknown>,
836
- ): RuleHostPipelineStage {
837
- // `retried` after exhausting retries is a transient-exhausted state, not a
838
- // hard failure. Mark as 'degraded' so the caller can distinguish "stage
839
- // failed permanently" from "stage exhausted transient retries".
840
- if (result.status === 'succeeded') {
841
- return { name, taskId, status: 'succeeded' };
842
- }
843
- if (result.status === 'retried') {
844
- return {
845
- name,
846
- taskId,
847
- status: 'degraded',
848
- reason: `transient_retry_exhausted: ${result.failureReason ?? result.status}`,
849
- };
850
- }
851
- return {
852
- name,
853
- taskId,
854
- status: 'failed',
855
- reason: result.failureReason ?? result.status,
856
- };
857
- }
858
-
859
- function rejectedResult(painId: string, stages: RuleHostPipelineStage[], degradationReason: string): RuleHostPipelineResult {
860
- return {
861
- decision: 'generation_rejected',
862
- painId,
863
- stages,
864
- scribeTaskId: null,
865
- ruleArtifactId: null,
866
- principleArtifactId: null,
867
- approvalId: null,
868
- degradationReason,
869
- };
870
- }
871
-
872
- /**
873
- * Build a text-principle-only result when the code-rule capability is OFF.
874
- *
875
- * The scribe stage already produced a principle artifact (stored via the
876
- * artifact store). We look it up by the scribe task ID so the caller can
877
- * reference it for prompt-channel fallback.
878
- */
879
- interface TextPrincipleOnlyParams {
880
- readonly painId: string;
881
- readonly stages: RuleHostPipelineStage[];
882
- readonly scribeTaskId: string;
883
- readonly disabledReason: string;
884
- readonly artifactStore: PIArtifactStore;
885
- /** PRI-804: approval queue used to enqueue the text principle (prompt channel). */
886
- readonly approvalStore: SqliteApprovalQueueStore;
887
- readonly now: string;
888
- /** Phase A: structured identity outcome from the shared post-scribe backfill. */
889
- readonly identityBinding: ScribeIdentityBinding;
890
- }
891
-
892
- /**
893
- * Owner Decision Experience v1 Phase A (SPEC §9.1) — structured ledger
894
- * identity binding outcome for the internalization chain.
895
- *
896
- * `bound`/`already_bound` mean the scribe principle artifact carries a
897
- * source_principle_id that was READ BACK from the PI store after the write
898
- * and, when the chain candidate resolved, that it equals the unique existing
899
- * ledger principle. Every other status is a publication blocker: the reason
900
- * codes follow the SPEC's suggested vocabulary
901
- * (candidate_unresolved / ledger_missing / ledger_ambiguous /
902
- * identity_conflict / binding_write_failed).
903
- */
904
- export type ScribeIdentityBinding =
905
- | { status: 'bound'; principleId: string }
906
- | { status: 'already_bound'; principleId: string }
907
- | { status: 'no_principle_artifact' }
908
- | { status: 'candidate_unresolved' }
909
- | { status: 'ledger_missing'; candidateId: string }
910
- | { status: 'ledger_ambiguous'; candidateId: string; principleIds: string[] }
911
- | { status: 'identity_conflict'; existingPrincipleId: string; resolvedPrincipleId: string }
912
- | { status: 'bound_target_missing'; principleId: string }
913
- | { status: 'binding_write_failed'; errorMessage: string }
914
- | { status: 'binding_readback_failed'; expectedPrincipleId: string; errorMessage: string };
915
-
916
- /** Only these two statuses authorize governance publication (approval enqueue). */
917
- export function isIdentityBindingVerified(
918
- binding: ScribeIdentityBinding,
919
- ): binding is Extract<ScribeIdentityBinding, { status: 'bound' | 'already_bound' }> {
920
- return binding.status === 'bound' || binding.status === 'already_bound';
921
- }
922
-
923
- /** Stable machine reason code for observability/logs (SPEC §9.1 vocabulary). */
924
- export function scribeIdentityReasonCode(binding: ScribeIdentityBinding): string {
925
- switch (binding.status) {
926
- case 'bound':
927
- case 'already_bound':
928
- return 'bound';
929
- case 'no_principle_artifact':
930
- return 'no_principle_artifact';
931
- case 'candidate_unresolved':
932
- return 'candidate_unresolved';
933
- case 'ledger_missing':
934
- return 'ledger_missing';
935
- case 'ledger_ambiguous':
936
- return 'ledger_ambiguous';
937
- case 'identity_conflict':
938
- return 'identity_conflict';
939
- case 'bound_target_missing':
940
- return 'identity_conflict';
941
- case 'binding_write_failed':
942
- return 'binding_write_failed';
943
- case 'binding_readback_failed':
944
- return 'binding_write_failed';
945
- }
946
- }
947
-
948
- /** Human/telemetry note for the scribe stage; '' on a verified binding. */
949
- export function scribeIdentityNote(binding: ScribeIdentityBinding): string {
950
- switch (binding.status) {
951
- case 'bound':
952
- case 'already_bound':
953
- return '';
954
- case 'no_principle_artifact':
955
- return '; source_principle_id_backfill_skipped: no principle artifact from scribe';
956
- case 'candidate_unresolved':
957
- return '; source_principle_id_backfill_skipped: no candidateId on the dreamer seed (candidate_unresolved)';
958
- case 'ledger_missing':
959
- return `; source_principle_id_backfill_skipped: candidate ${binding.candidateId} matches no ledger principle (ledger_missing)`;
960
- case 'ledger_ambiguous':
961
- return `; source_principle_id_backfill_skipped: candidate ${binding.candidateId} matches ${binding.principleIds.length} ledger principles (ledger_ambiguous)`;
962
- case 'identity_conflict':
963
- return `; source_principle_id_backfill_refused: artifact already bound to ${binding.existingPrincipleId}, chain resolves to ${binding.resolvedPrincipleId} (identity_conflict)`;
964
- case 'bound_target_missing':
965
- return `; source_principle_id_backfill_refused: artifact bound to ${binding.principleId} which is absent from the ledger (identity_conflict)`;
966
- case 'binding_write_failed':
967
- return `; source_principle_id_backfill_failed: ${binding.errorMessage}`;
968
- case 'binding_readback_failed':
969
- return `; source_principle_id_backfill_unverified: read-back mismatch for ${binding.expectedPrincipleId}: ${binding.errorMessage}`;
970
- }
971
- }
972
-
973
- /**
974
- * PRI-804(a) + Phase A: resolve the UNIQUE ledger principle for the chain.
975
- *
976
- * The ledger principle is the identity the Console groups approvals by and the
977
- * one `upgradeLedgerPrinciple` activates after approval. The only durable link
978
- * from the internalization chain to the ledger is the candidateId carried in
979
- * the dreamer task's diagnosticJson (set by the intake bridge) — the ledger
980
- * entry stores it in derivedFromPainIds. Phase A hardens the lookup:
981
- * `listForCandidate` (plural, same adapter the intake bridge uses — one
982
- * authority, no second truth) rejects zero and multi matches instead of
983
- * silently taking the first hit.
984
- */
985
- async function resolveLedgerIdentity(
986
- stateManager: RuntimeStateManager,
987
- dreamerTaskId: string,
988
- workspaceDir: string,
989
- ): Promise<ScribeIdentityBinding | { status: 'resolved'; principleId: string }> {
990
- let candidateId: string | undefined;
991
- try {
992
- const dreamerTask = await stateManager.getTask(dreamerTaskId);
993
- if (typeof dreamerTask?.diagnosticJson === 'string') {
994
- const parsed: unknown = JSON.parse(dreamerTask.diagnosticJson);
995
- if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
996
- && Object.hasOwn(parsed, 'candidateId')) {
997
- const stored = Reflect.get(parsed, 'candidateId');
998
- if (typeof stored === 'string' && stored.length > 0) candidateId = stored;
999
- }
1000
- }
1001
- } catch {
1002
- return { status: 'candidate_unresolved' }; // malformed dreamer diagnosticJson
1003
- }
1004
- if (!candidateId) return { status: 'candidate_unresolved' };
1005
- let matches: { id: string }[];
1006
- try {
1007
- const ledger = new PrincipleTreeLedgerAdapter({ stateDir: path.join(workspaceDir, '.state') });
1008
- matches = ledger.listForCandidate(candidateId);
1009
- } catch {
1010
- // Ledger unreadable cannot be distinguished from "no ledger" here — the
1011
- // write attempt below will surface the concrete error; treat as missing
1012
- // with the skip note (same observable class as before Phase A).
1013
- return { status: 'ledger_missing', candidateId };
1014
- }
1015
- if (matches.length === 0) return { status: 'ledger_missing', candidateId };
1016
- if (matches.length > 1) {
1017
- return { status: 'ledger_ambiguous', candidateId, principleIds: matches.map((m) => m.id) };
1018
- }
1019
- const [unique] = matches;
1020
- if (unique === undefined) return { status: 'ledger_missing', candidateId };
1021
- return { status: 'resolved', principleId: unique.id };
1022
- }
1023
-
1024
- interface BackfillIdentityParams {
1025
- readonly stateManager: RuntimeStateManager;
1026
- readonly artifactStore: PIArtifactStore;
1027
- readonly scribeTaskId: string;
1028
- readonly dreamerTaskId: string;
1029
- readonly workspaceDir: string;
1030
- readonly now: string;
1031
- }
1032
-
1033
- /**
1034
- * EP002-R4 follow-up #2 + Phase A: backfill the scribe principle artifact's
1035
- * source_principle_id with the LEDGER principle UUID (shared by the rule and
1036
- * text paths — runs right after the scribe stage succeeds). Every downstream
1037
- * identity resolution reads this column FIRST (extractPrincipleId /
1038
- * EvaluatorRunner.extractPrincipleIdFromArtifact), so this single write is
1039
- * what binds approvals, Console grouping, ledger upgrades, and rule
1040
- * artifacts to the ledger UUID instead of the title namespace.
1041
- *
1042
- * Phase A contract changes vs the note-only version:
1043
- * - the outcome is structured (ScribeIdentityBinding), not a free string;
1044
- * - an existing binding that DISAGREES with the resolved ledger target is an
1045
- * identity_conflict — never overwritten;
1046
- * - a successful write is verified by reading the artifact back from the
1047
- * PI store (log success alone is not binding evidence — SPEC §9.1);
1048
- * - a pre-existing binding is only trusted when its target exists in the
1049
- * ledger read (listForCandidate is not applicable — we check presence by
1050
- * resolving through the same ledger load).
1051
- *
1052
- * Still never throws — identity enrichment must not break the generation
1053
- * chain; it only gates the later governance publication.
1054
- */
1055
- export async function backfillScribeIdentity(params: BackfillIdentityParams): Promise<ScribeIdentityBinding> {
1056
- const { stateManager, artifactStore, scribeTaskId, dreamerTaskId, workspaceDir, now } = params;
1057
- try {
1058
- const arts = await artifactStore.listBySourceTaskId(scribeTaskId);
1059
- const principleArt = arts.find((a) => a.artifactKind === 'principle');
1060
- if (!principleArt) return { status: 'no_principle_artifact' };
1061
- const resolution = await resolveLedgerIdentity(stateManager, dreamerTaskId, workspaceDir);
1062
- if (principleArt.sourcePrincipleId) {
1063
- // Already bound (rerun). Idempotent success only when the binding is
1064
- // consistent with what the chain resolves to now.
1065
- if (resolution.status === 'resolved') {
1066
- if (resolution.principleId === principleArt.sourcePrincipleId) {
1067
- return { status: 'already_bound', principleId: principleArt.sourcePrincipleId };
1068
- }
1069
- return {
1070
- status: 'identity_conflict',
1071
- existingPrincipleId: principleArt.sourcePrincipleId,
1072
- resolvedPrincipleId: resolution.principleId,
1073
- };
1074
- }
1075
- if (resolution.status === 'ledger_ambiguous') return resolution;
1076
- // Chain candidate unresolved/missing on this replay — the durable
1077
- // binding still stands only if its target is a real ledger principle.
1078
- const ledger = new PrincipleTreeLedgerAdapter({ stateDir: path.join(workspaceDir, '.state') });
1079
- if (ledger.hasPrinciple(principleArt.sourcePrincipleId)) {
1080
- return { status: 'already_bound', principleId: principleArt.sourcePrincipleId };
1081
- }
1082
- return { status: 'bound_target_missing', principleId: principleArt.sourcePrincipleId };
1083
- }
1084
- if (resolution.status !== 'resolved') return resolution;
1085
- const ledgerPrincipleId = resolution.principleId;
1086
- await artifactStore.upsertArtifact({ ...principleArt, sourcePrincipleId: ledgerPrincipleId, updatedAt: now });
1087
- // Read-back verification: the PI store must return the bound ID.
1088
- try {
1089
- const readBack = await artifactStore.getArtifactById(principleArt.artifactId);
1090
- if (readBack?.sourcePrincipleId !== ledgerPrincipleId) {
1091
- return {
1092
- status: 'binding_readback_failed',
1093
- expectedPrincipleId: ledgerPrincipleId,
1094
- errorMessage: `read-back sourcePrincipleId=${readBack?.sourcePrincipleId ?? 'null'}`,
1095
- };
1096
- }
1097
- } catch (readErr: unknown) {
1098
- const msg = readErr instanceof Error ? readErr.message : String(readErr);
1099
- return { status: 'binding_readback_failed', expectedPrincipleId: ledgerPrincipleId, errorMessage: msg };
1100
- }
1101
- return { status: 'bound', principleId: ledgerPrincipleId };
1102
- } catch (err: unknown) {
1103
- const msg = err instanceof Error ? err.message : String(err);
1104
- return { status: 'binding_write_failed', errorMessage: msg };
1105
- }
1106
- }
1107
-
1108
- /**
1109
- * Phase A (SPEC §9.1.6): before the rule artifact enters the approvable set,
1110
- * verify the chain's ledger identity is VERIFIED and the rule artifact carries
1111
- * exactly that binding. A chain without a verified unique ledger target must
1112
- * not publish, even if the artifact itself carries some id (e.g. a legacy
1113
- * title-namespace value the evaluator would have echoed). Returns the
1114
- * verified principle id, or null when publication must be refused.
1115
- */
1116
- async function resolveRuleArtifactIdentity(
1117
- artifactStore: PIArtifactStore,
1118
- ruleArtifactId: string,
1119
- identityBinding: ScribeIdentityBinding,
1120
- ): Promise<string | null> {
1121
- if (!isIdentityBindingVerified(identityBinding)) return null;
1122
- let artifactSourcePrincipleId: string | undefined;
1123
- try {
1124
- const ruleArt = await artifactStore.getArtifactById(ruleArtifactId);
1125
- artifactSourcePrincipleId = ruleArt?.sourcePrincipleId;
1126
- } catch {
1127
- return null;
1128
- }
1129
- if (artifactSourcePrincipleId === undefined || artifactSourcePrincipleId !== identityBinding.principleId) return null;
1130
- return artifactSourcePrincipleId;
1131
- }
1132
-
1133
- async function textPrincipleOnlyResult(
1134
- params: TextPrincipleOnlyParams,
1135
- ): Promise<RuleHostPipelineResult> {
1136
- const { painId, stages, scribeTaskId, disabledReason, artifactStore, approvalStore, now, identityBinding } = params;
1137
- // Look up the principle artifact produced by the scribe stage.
1138
- try {
1139
- const arts = await artifactStore.listBySourceTaskId(scribeTaskId);
1140
- const principleArt = arts.find((a) => a.artifactKind === 'principle');
1141
- // Identity backfill already ran on the shared post-scribe step
1142
- // (backfillScribeIdentity) for BOTH paths; nothing to do here.
1143
- // On the text path the evaluator never runs, so nothing else flips the
1144
- // validation status — mark it validated here (same store API the
1145
- // evaluator uses; scribe task success already implies its validator
1146
- // passed — peer-runner contract: output_invalid tasks never succeed).
1147
- let identityNote = '';
1148
- if (principleArt && principleArt.validationStatus !== 'validated') {
1149
- try {
1150
- await artifactStore.updateValidationStatus(principleArt.artifactId, 'validated');
1151
- } catch (err: unknown) {
1152
- const msg = err instanceof Error ? err.message : String(err);
1153
- identityNote = `; validation_status_mark_failed: ${msg}`;
1154
- }
1155
- }
1156
- // PRI-804: a text principle is a prompt-channel intervention and follows the
1157
- // same Owner governance as RuleCode (Owner decision 2026-09-15): enqueue it
1158
- // into the EXISTING approval queue so the Owner can approve it in Console.
1159
- // Deterministic approval id (apr_prompt_<artifactId>) + INSERT OR IGNORE make
1160
- // replays idempotent. Enqueue failure degrades observably (rc-9), never
1161
- // throwing away the artifact.
1162
- //
1163
- // Phase A governance publication boundary: an enqueue is only allowed when
1164
- // the artifact's ledger identity binding is VERIFIED. Unverified bindings
1165
- // keep the artifact (and its validation status) but must not create an
1166
- // Owner approval subject; recovery is a replay after identity repair.
1167
- let approvalId: string | null = null;
1168
- let enqueueNote = '';
1169
- if (!principleArt) {
1170
- enqueueNote = '; approval_enqueue_skipped: no principle artifact from scribe';
1171
- } else if (!isIdentityBindingVerified(identityBinding)) {
1172
- const reasonCode = scribeIdentityReasonCode(identityBinding);
1173
- enqueueNote = `; approval_enqueue_skipped: identity_binding_unverified (${reasonCode}). Principle artifact preserved; repair the ledger identity and re-run this pipeline to publish.`;
1174
- } else if (identityBinding.principleId !== principleArt.sourcePrincipleId) {
1175
- enqueueNote = `; approval_enqueue_skipped: identity_binding_unverified (identity_conflict: chain resolves to ${identityBinding.principleId}, artifact carries ${principleArt.sourcePrincipleId ?? 'no binding'}). Principle artifact preserved; repair the ledger identity and re-run this pipeline to publish.`;
1176
- } else {
1177
- try {
1178
- const record = await approvalStore.enqueue({
1179
- artifactId: principleArt.artifactId,
1180
- channel: 'prompt',
1181
- riskLevel: getChannelRiskLevel('prompt'),
1182
- summary: `Text principle candidate for pain ${painId}`,
1183
- triggerReason: `text_principle_only: pain=${painId}, principle=${principleArt.artifactId}; code-rule capability off (${disabledReason})`,
1184
- }, now);
1185
- const { approvalId: enqueuedApprovalId } = record;
1186
- approvalId = enqueuedApprovalId;
1187
- } catch (err: unknown) {
1188
- const msg = err instanceof Error ? err.message : String(err);
1189
- enqueueNote = `; approval_enqueue_failed: ${msg}. Manual enqueue required: pd activation dispatch --artifact-id ${principleArt.artifactId} --channel prompt`;
1190
- }
1191
- }
1192
- return {
1193
- decision: 'text_principle_only',
1194
- painId,
1195
- stages,
1196
- scribeTaskId,
1197
- ruleArtifactId: null,
1198
- principleArtifactId: principleArt?.artifactId ?? null,
1199
- approvalId,
1200
- degradationReason: `code_rule_capability_off: ${disabledReason}${identityNote}${enqueueNote}`,
1201
- };
1202
- } catch (error: unknown) {
1203
- const message = error instanceof Error ? error.message : String(error);
1204
- return {
1205
- decision: 'text_principle_only',
1206
- painId,
1207
- stages,
1208
- scribeTaskId,
1209
- ruleArtifactId: null,
1210
- principleArtifactId: null,
1211
- approvalId: null,
1212
- degradationReason: `code_rule_capability_off: ${disabledReason}; principle_artifact_lookup_failed: ${message}`,
1213
- };
1214
- }
1215
-
1216
- }