@principles/pd-cli 1.152.12 → 1.152.14

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