@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,1242 +0,0 @@
1
- /**
2
- * runRuleHostPipeline unit tests (PRI-429).
3
- *
4
- * Covers the atomic capability contract (ArtificerL2 + Evaluator both run or
5
- * neither runs) and exact sourcePainId matching (D fix: pain-1 must NOT match
6
- * pain-10).
7
- *
8
- * ERR refs considered:
9
- * - ERR-001: parsed JSON treated as unknown
10
- * - ERR-005/007: no `as` bypass; type narrowing via typeof + Object.hasOwn
11
- * - ERR-009: missing sourcePainId = no match (fail loud)
12
- * - ERR-013: Object.hasOwn for untrusted key checks
13
- */
14
- /* eslint-disable @typescript-eslint/no-non-null-assertion, @typescript-eslint/class-methods-use-this, @typescript-eslint/require-await */
15
- import { describe, it, expect, afterEach, vi } from 'vitest';
16
- import * as os from 'node:os';
17
- import * as path from 'node:path';
18
- import * as fs from 'node:fs';
19
- import { runRuleHostPipeline, backfillScribeIdentity } from '../../src/services/rulehost-pipeline-runner.js';
20
- import type { CodeRuleCapability } from '../../src/services/rulehost-pipeline-runner.js';
21
- import type { PDRuntimeAdapter, RunHandle, RunStatus, PIArtifactStore, RuntimeCapabilities, RuntimeHealth, RuntimeArtifactRef, ContextItem, StructuredRunOutput, StartRunInput } from '@principles/core/runtime-v2';
22
- import { RuntimeStateManager, createPITaskDiagnosticJson, SqliteApprovalQueueStore } from '@principles/core/runtime-v2';
23
- import { addPrincipleToLedger, updatePrinciple } from '@principles/core/principle-tree-ledger';
24
- import { saveHostToolDeclaration } from '@principles/host-runtime';
25
-
26
- type StageFactory = (taskId: string, priorArtifactId?: string) => unknown;
27
- type EvaluatorFactory = (taskId: string, artificerArtifactId: string) => unknown;
28
-
29
- class ScriptedAdapter implements PDRuntimeAdapter {
30
- readonly startRunCalls: { taskId: string }[] = [];
31
- readonly startRunInputs = new Map<string, StartRunInput>();
32
- artifactStore: PIArtifactStore | null = null;
33
- constructor(private readonly factories: { dreamer: StageFactory; philosopher: StageFactory; scribe: StageFactory; artificer: StageFactory; evaluator: EvaluatorFactory }) {}
34
-
35
- private kindFor(taskId: string): 'dreamer' | 'philosopher' | 'scribe' | 'artificer' | 'evaluator' {
36
- if (taskId.startsWith('dreamer')) return 'dreamer';
37
- if (taskId.includes('philosopher')) return 'philosopher';
38
- if (taskId.includes('scribe')) return 'scribe';
39
- if (taskId.includes('artificer')) return 'artificer';
40
- return 'evaluator';
41
- }
42
-
43
- private async priorArtifactId(priorKind: string): Promise<string | undefined> {
44
- const calls = this.startRunCalls.filter((c) => this.kindFor(c.taskId) === priorKind);
45
- const last = calls[calls.length - 1];
46
- if (!last || !this.artifactStore) return undefined;
47
- const arts = await this.artifactStore.listBySourceTaskId(last.taskId);
48
- return arts[0]?.artifactId;
49
- }
50
-
51
- async startRun(input: StartRunInput): Promise<RunHandle> {
52
- this.startRunCalls.push({ taskId: input.taskRef.taskId });
53
- const runId = `run-${input.taskRef.taskId}`;
54
- this.startRunInputs.set(runId, input);
55
- return { runId, runtimeKind: 'test-double', startedAt: new Date().toISOString() };
56
- }
57
- async pollRun(_runId: string): Promise<RunStatus> { return { status: 'succeeded', runId: 'run-x' }; }
58
- async fetchOutput(runId: string): Promise<StructuredRunOutput | null> {
59
- const taskId = runId.replace(/^run-/, '');
60
- const kind = this.kindFor(taskId);
61
- let payload: unknown;
62
- if (kind === 'dreamer') payload = this.factories.dreamer(taskId);
63
- else if (kind === 'philosopher') payload = this.factories.philosopher(taskId, await this.priorArtifactId('dreamer'));
64
- else if (kind === 'scribe') payload = this.factories.scribe(taskId, await this.priorArtifactId('philosopher'));
65
- else if (kind === 'artificer') payload = this.factories.artificer(taskId, await this.priorArtifactId('scribe'));
66
- else {
67
- let artificerArtifactId = await this.priorArtifactId('artificer');
68
- if (!artificerArtifactId) {
69
- const inputPayload = this.startRunInputs.get(runId)?.inputPayload;
70
- if (typeof inputPayload === 'string') {
71
- const parsed: unknown = JSON.parse(inputPayload);
72
- if (parsed !== null && typeof parsed === 'object' && Object.hasOwn(parsed, 'sourceArtificerArtifactId')) {
73
- const candidate = Reflect.get(parsed, 'sourceArtificerArtifactId');
74
- if (typeof candidate === 'string') artificerArtifactId = candidate;
75
- }
76
- }
77
- }
78
- payload = this.factories.evaluator(taskId, requireLineage(artificerArtifactId, 'sourceArtificerArtifactId'));
79
- }
80
- return { runId, payload };
81
- }
82
- async cancelRun(_runId: string): Promise<void> { /* noop */ }
83
- async getCapabilities(): Promise<RuntimeCapabilities> {
84
- return {
85
- supportsStructuredJsonOutput: false,
86
- supportsToolUse: false,
87
- supportsWorkingDirectory: false,
88
- supportsModelSelection: false,
89
- supportsLongRunningSessions: false,
90
- supportsCancellation: false,
91
- supportsArtifactWriteBack: false,
92
- supportsConcurrentRuns: false,
93
- supportsStreaming: false,
94
- };
95
- }
96
- async healthCheck(): Promise<RuntimeHealth> {
97
- return { healthy: true, degraded: false, warnings: [], lastCheckedAt: new Date().toISOString() };
98
- }
99
- async fetchArtifacts(_runId: string): Promise<RuntimeArtifactRef[]> { return []; }
100
- async appendContext(_runId: string, _items: ContextItem[]): Promise<void> { /* noop */ }
101
- async refreshCapabilities(): Promise<RuntimeCapabilities> { return this.getCapabilities(); }
102
- kind(): 'test-double' { return 'test-double'; }
103
- }
104
-
105
- // ── Output factories ─────────────────────────────────────────────────────────
106
-
107
- function dreamerOut(taskId: string, painId: string): unknown {
108
- return {
109
- valid: true, taskId,
110
- candidates: [{ candidateIndex: 0, badDecision: 'Wrote to /etc/passwd', betterDecision: 'Block system path writes', rationale: 'OS damage risk', confidence: 0.85, riskLevel: 'low' as const, strategicPerspective: 'safety' }],
111
- sourcePainId: painId, contextRefs: [], generatedAt: new Date().toISOString(),
112
- };
113
- }
114
-
115
- function requireLineage(id: string | undefined, field: string): string {
116
- if (!id) throw new Error(`missing required lineage field: ${field}`);
117
- return id;
118
- }
119
-
120
- function philosopherOut(taskId: string, priorId?: string): unknown {
121
- return {
122
- taskId, sourceDreamerArtifactId: requireLineage(priorId, 'sourceDreamerArtifactId'),
123
- thesis: 'System path writes must be blocked',
124
- principleCandidate: { title: 'Block system path writes', rationale: 'OS corruption risk', scope: 'write ops', confidence: 0.9 },
125
- risks: [], generatedAt: new Date().toISOString(),
126
- };
127
- }
128
-
129
- function scribeOut(taskId: string, priorId?: string): unknown {
130
- return {
131
- taskId, sourcePhilosopherArtifactId: requireLineage(priorId, 'sourcePhilosopherArtifactId'),
132
- principleDraft: { title: 'Block system path writes', statement: 'Writes to /etc, /boot, /sys must be blocked.', rationale: 'OS corruption risk', applicability: ['write_file'], antiPatterns: ['Hardcoded allow'], confidence: 0.9 },
133
- sourceTrace: { philosopherArtifactId: requireLineage(priorId, 'sourceTrace.philosopherArtifactId') },
134
- risks: [], generatedAt: new Date().toISOString(),
135
- };
136
- }
137
-
138
- // PRI-780: v2-only generation — the pipeline requires an Owner-labelled pack
139
- // and the artificer output must satisfy the v2 contract (case parity incl.
140
- // ruleContext + verbatim evidenceRefs).
141
- const PIPE_RULE_CONTEXT = {
142
- version: 2 as const,
143
- history: { status: 'available' as const, truncated: false, calls: [] },
144
- facts: { priorReadOfTarget: 'unknown' as const, readCount: 0, writeCount: 0, uniqueWritePathCount: 0, sameActionBlockCount: null },
145
- };
146
- const PIPE_POS_CASE = { caseId: 'pos-1', kind: 'positive' as const, toolName: 'write_file', params: { path: '/project/f.txt' }, expectedDecision: 'allow' as const, ruleContext: PIPE_RULE_CONTEXT };
147
- const PIPE_NEG_CASE = { caseId: 'neg-1', kind: 'negative' as const, toolName: 'write_file', params: { path: '/etc/passwd' }, expectedDecision: 'block' as const, ruleContext: PIPE_RULE_CONTEXT };
148
- const PIPE_PACK = {
149
- sourceNegativeCase: PIPE_NEG_CASE,
150
- ownerDesiredOutcome: 'block system path writes',
151
- positiveCounterexamples: [PIPE_POS_CASE],
152
- evidenceRefs: ['pain://pipe-1'],
153
- redactionNotes: [],
154
- };
155
-
156
- function artificerV2(taskId: string, priorId?: string): unknown {
157
- return {
158
- taskId, sourceScribeArtifactId: requireLineage(priorId, 'sourceScribeArtifactId'),
159
- requiresContextVersion: 2,
160
- evidenceRefs: ['pain://pipe-1'],
161
- implementationPlan: { summary: 'Block /etc writes', targetSurface: 'rule-host', changes: ['matcher'], tests: ['unit'], rolloutNotes: ['shadow'], confidence: 0.85 },
162
- implementationCode: 'function evaluate(input, helpers) { const p = String(input?.action?.paramsSummary?.path ?? input?.action?.normalizedPath ?? ""); if (p.startsWith("/etc")) return { decision: "block", matched: true, reason: "system path" }; const ctx = input?.context; if (ctx && ctx.facts && ctx.facts.priorReadOfTarget === "no") { return { decision: "block", matched: true, reason: "no prior read" }; } return { decision: "allow", matched: false, reason: "ok" }; }',
163
- implementationSummary: 'Block system path writes',
164
- goldenTraceCases: [PIPE_POS_CASE, PIPE_NEG_CASE],
165
- affectedTools: ['write_file'],
166
- sourceTrace: { scribeArtifactId: requireLineage(priorId, 'sourceTrace.scribeArtifactId') },
167
- risks: [], generatedAt: new Date().toISOString(),
168
- };
169
- }
170
-
171
- function evaluatorApproved(taskId: string, artificerArtifactId: string): unknown {
172
- return {
173
- taskId, sourceArtificerArtifactId: artificerArtifactId,
174
- evaluation: { decision: 'approved', summary: 'approved', score: 0.9, strengths: [], concerns: [], requiredChanges: [] },
175
- sourceTrace: { artificerArtifactId },
176
- risks: [], generatedAt: new Date().toISOString(),
177
- codeReview: { intentConsistency: { aligned: true, explanation: 'ok' }, scopePrecision: { verdict: 'precise' as const, explanation: 'ok' }, traceCoverage: { sufficient: true, gaps: [], explanation: 'ok' } },
178
- adversarialCases: [{ caseId: 'adv-1', attackType: 'boundary' as const, toolName: 'write_file', params: { path: '/etc/shadow' }, expectedDecision: 'block' as const, rationale: 'system path' }],
179
- adversarialResult: { passed: true, failedCases: [] },
180
- };
181
- }
182
-
183
- function evaluatorRejected(taskId: string, artificerArtifactId: string): unknown {
184
- return {
185
- taskId, sourceArtificerArtifactId: artificerArtifactId,
186
- evaluation: { decision: 'rejected', summary: 'rejected: code too narrow', score: 0.3, strengths: [], concerns: ['misses /boot path'], requiredChanges: ['add /boot matcher'] },
187
- sourceTrace: { artificerArtifactId },
188
- risks: [], generatedAt: new Date().toISOString(),
189
- codeReview: { intentConsistency: { aligned: false, explanation: 'misses /boot' }, scopePrecision: { verdict: 'imprecise' as const, explanation: 'narrow' }, traceCoverage: { sufficient: false, gaps: ['/boot'], explanation: 'missing' } },
190
- adversarialCases: [{ caseId: 'adv-1', attackType: 'boundary' as const, toolName: 'write_file', params: { path: '/boot/grub' }, expectedDecision: 'block' as const, rationale: 'system path' }],
191
- adversarialResult: { passed: false, failedCases: [{ caseId: 'adv-1', errorType: 'wrong_decision', message: 'expected block, got allow' }] },
192
- };
193
- }
194
-
195
- function evaluatorNeedsRevision(taskId: string, artificerArtifactId: string): unknown {
196
- return {
197
- taskId, sourceArtificerArtifactId: artificerArtifactId,
198
- evaluation: { decision: 'needs_revision', summary: 'needs revision: adversarial replay failed', score: 0.4, strengths: [], concerns: ['adversarial case failed'], requiredChanges: ['fix matcher'] },
199
- sourceTrace: { artificerArtifactId },
200
- risks: [], generatedAt: new Date().toISOString(),
201
- codeReview: { intentConsistency: { aligned: false, explanation: 'misses system paths' }, scopePrecision: { verdict: 'too_narrow' as const, explanation: 'narrow' }, traceCoverage: { sufficient: false, gaps: [], explanation: 'missing' } },
202
- adversarialCases: [{ caseId: 'adv-1', attackType: 'boundary' as const, toolName: 'write_file', params: { path: '/etc/shadow' }, expectedDecision: 'block' as const, rationale: 'system path' }],
203
- adversarialResult: { passed: false, failedCases: [{ caseId: 'adv-1', attackType: 'boundary' as const, actualDecision: 'allow', expectedDecision: 'block', rationale: 'system path' }] },
204
- };
205
- }
206
-
207
- // ── Helpers ──────────────────────────────────────────────────────────────────
208
-
209
- let tmpDir = '';
210
-
211
- function makeTmpDir(): string {
212
- // mkdtempSync's random suffix keeps the path unpredictable (CodeQL
213
- // js/insecure-temporary-file: the pipeline language tests WRITE
214
- // .pd/config.yaml under this dir).
215
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pipe-'));
216
- // PRI-661: the pipeline's evaluator replay resolves the production gate
217
- // context from durable workspace provenance — seed a declaration like every
218
- // real host does on startup.
219
- saveHostToolDeclaration(dir, {
220
- version: 1,
221
- hostKind: 'testhost',
222
- mappings: [
223
- { rawToolName: 'Write', canonicalKind: 'write' },
224
- { rawToolName: 'Edit', canonicalKind: 'write' },
225
- { rawToolName: 'Bash', canonicalKind: 'execute' },
226
- ],
227
- declaredAt: new Date().toISOString(),
228
- });
229
- return dir;
230
- }
231
-
232
- function makeAdapter(opts?: { evaluator?: EvaluatorFactory }): ScriptedAdapter {
233
- return new ScriptedAdapter({
234
- dreamer: (taskId) => dreamerOut(taskId, 'pain-test-001'),
235
- philosopher: philosopherOut,
236
- scribe: scribeOut,
237
- artificer: artificerV2,
238
- evaluator: opts?.evaluator ?? evaluatorApproved,
239
- });
240
- }
241
-
242
- /**
243
- * Seed a dreamer task with sourcePainId embedded as a TOP-LEVEL key in
244
- * diagnosticJson (outside the pi_metadata envelope). This mirrors the pattern
245
- * from source-trace-locator.test.ts and PainSignalBridge.
246
- */
247
- async function seedDreamerWithId(
248
- sm: RuntimeStateManager,
249
- taskId: string,
250
- painId: string,
251
- channel: 'prompt' | 'code_tool_hook' | 'defer_archive' = 'code_tool_hook',
252
- candidateId?: string,
253
- ): Promise<void> {
254
- const baseMetadata = JSON.parse(createPITaskDiagnosticJson({
255
- dependencyTaskIds: [], channel, timeoutMs: 1000, inputArtifactRefs: [], outputArtifactRefs: [],
256
- })) as Record<string, unknown>;
257
- const diagnosticJson = JSON.stringify({ ...baseMetadata, sourcePainId: painId, ...(candidateId ? { candidateId } : {}) });
258
- await sm.createTask({ taskId, taskKind: 'dreamer', status: 'pending', attemptCount: 0, maxAttempts: 3, diagnosticJson });
259
- }
260
-
261
- /** Seed a dreamer task with raw diagnosticJson (for missing/wrong-type tests). */
262
- async function seedDreamerRaw(sm: RuntimeStateManager, taskId: string, diagnosticJson: string): Promise<void> {
263
- await sm.createTask({ taskId, taskKind: 'dreamer', status: 'pending', attemptCount: 0, maxAttempts: 3, diagnosticJson });
264
- }
265
-
266
- // ── Tests ────────────────────────────────────────────────────────────────────
267
-
268
- describe('runRuleHostPipeline (PRI-429) — atomic capability + exact pain match', () => {
269
- afterEach(() => {
270
- vi.restoreAllMocks();
271
- if (tmpDir) { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } tmpDir = ''; }
272
- });
273
-
274
- // ── Test 1: Capability ON + approved → candidate_ready_for_owner_review ──
275
- it('capability ON + evaluator approved → candidate_ready_for_owner_review; scribe identity backfilled on the rule path too (EP002-R4 follow-up #2)', async () => {
276
- tmpDir = makeTmpDir();
277
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
278
- await sm.initialize();
279
- await seedDreamerWithId(sm, 'dreamer-seeded-001', 'pain-test-001', 'code_tool_hook', 'cand-001');
280
- await sm.close();
281
- addPrincipleToLedger(path.join(tmpDir, '.state'), {
282
- id: 'ledger-principle-001', version: 1, text: 'test ledger principle', triggerPattern: '', action: '',
283
- status: 'candidate', evaluability: 'weak_heuristic', priority: 'P1', scope: 'general',
284
- valueScore: 0, adherenceRate: 0, painPreventedCount: 0, derivedFromPainIds: ['cand-001'],
285
- ruleIds: [], conflictsWithPrincipleIds: [], createdAt: '2026-09-15T00:00:00.000Z', updatedAt: '2026-09-15T00:00:00.000Z',
286
- });
287
-
288
- const adapter = makeAdapter();
289
- const capability: CodeRuleCapability = { enabled: true, artificerAdapter: adapter };
290
- const result = await runRuleHostPipeline({
291
- workspaceDir: tmpDir, painId: 'pain-test-001', runtimeAdapter: adapter,
292
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
293
- codeRuleCapability: capability,
294
- behaviorExamplePack: PIPE_PACK,
295
- onStoreReady: (store) => { adapter.artifactStore = store; },
296
- });
297
-
298
- expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
299
- expect(result.stages.map((s) => s.name)).toEqual(['pain_lookup', 'dreamer', 'philosopher', 'scribe', 'adversarial_loop']);
300
- expect(result.ruleArtifactId).not.toBeNull();
301
- // P1 #1 fix: candidate should be auto-enqueued into the ApprovalQueue
302
- expect(result.approvalId).not.toBeNull();
303
-
304
- // EP002-R4 follow-up #2: the scribe artifact carries the LEDGER UUID (not
305
- // a title) so the evaluator propagates it into the rule artifact and the
306
- // Console groups the approval by an id the detail page can resolve.
307
- const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
308
- await verify.initialize();
309
- try {
310
- const scribeArt = await verify.piArtifactStore.getArtifactById(
311
- (await verify.piArtifactStore.listBySourceTaskId(result.stages.find((s) => s.name === 'scribe')!.taskId!))
312
- .find((a) => a.artifactKind === 'principle')!.artifactId,
313
- );
314
- expect(scribeArt?.sourcePrincipleId).toBe('ledger-principle-001');
315
- const ruleArt = await verify.piArtifactStore.getArtifactById(result.ruleArtifactId!);
316
- expect(ruleArt?.sourcePrincipleId).toBe('ledger-principle-001');
317
- } finally {
318
- await verify.close();
319
- }
320
- }, 60_000);
321
-
322
- it('adversarial feedback loop drives a second artificer round before creating a candidate', async () => {
323
- tmpDir = makeTmpDir();
324
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
325
- await sm.initialize();
326
- await seedDreamerWithId(sm, 'dreamer-feedback-001', 'pain-feedback-001');
327
- await sm.close();
328
-
329
- let adapter: ScriptedAdapter;
330
- let artificerCallCount = 0;
331
- const artificerPrompts: string[] = [];
332
- adapter = new ScriptedAdapter({
333
- dreamer: (taskId) => dreamerOut(taskId, 'pain-feedback-001'),
334
- philosopher: philosopherOut,
335
- scribe: scribeOut,
336
- artificer: (taskId, priorId) => {
337
- artificerCallCount++;
338
- const runId = `run-${taskId}`;
339
- const inputPayload = adapter.startRunInputs.get(runId)?.inputPayload;
340
- if (typeof inputPayload === 'string') artificerPrompts.push(inputPayload);
341
-
342
- const base = artificerV2(taskId, priorId);
343
- if (artificerCallCount === 1) {
344
- // Round 1: code fails the adversarial replay, forcing needs_revision.
345
- return {
346
- ...base,
347
- implementationCode: 'function evaluate() { return { decision: "allow", matched: false, reason: "bug" }; }',
348
- };
349
- }
350
- // Round 2: fixed code passes the evaluator.
351
- return base;
352
- },
353
- evaluator: (taskId, artificerArtifactId) => {
354
- if (artificerCallCount === 1) {
355
- return evaluatorNeedsRevision(taskId, artificerArtifactId);
356
- }
357
- return evaluatorApproved(taskId, artificerArtifactId);
358
- },
359
- });
360
-
361
- const result = await runRuleHostPipeline({
362
- workspaceDir: tmpDir,
363
- painId: 'pain-feedback-001',
364
- runtimeAdapter: adapter,
365
- codeRuleCapability: { enabled: true, artificerAdapter: adapter },
366
- behaviorExamplePack: PIPE_PACK,
367
- channel: 'code_tool_hook',
368
- pollIntervalMs: 5,
369
- timeoutMs: 1000,
370
- onStoreReady: (store) => { adapter.artifactStore = store; },
371
- });
372
-
373
- expect(artificerCallCount).toBe(2);
374
- expect(artificerPrompts).toHaveLength(2);
375
- expect(artificerPrompts[1]).toContain('Prior adversarial replay failures');
376
- expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
377
- expect(result.ruleArtifactId).toMatch(/^pi-rule-/);
378
- // Owner Decision Experience v1 Phase A: this seed carries no candidateId and
379
- // no ledger principle, so identity binding is unverified (candidate_unresolved)
380
- // and the governance publication boundary refuses the approval enqueue — the
381
- // two-round adversarial loop itself is unaffected.
382
- expect(result.approvalId).toBeNull();
383
- expect(result.degradationReason ?? '').toContain('identity_binding_unverified');
384
- }, 60_000);
385
-
386
- // ── Test 2: Capability OFF (explicitly disabled) → text_principle_only ──
387
- it('capability OFF (explicitly disabled) → text_principle_only + prompt approval enqueue + identity backfill (PRI-804)', async () => {
388
- tmpDir = makeTmpDir();
389
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
390
- await sm.initialize();
391
- await seedDreamerWithId(sm, 'dreamer-seeded-002', 'pain-test-001', 'code_tool_hook', 'cand-002');
392
- await sm.close();
393
- // PRI-804(a): seed the ledger with the principle this chain's candidate maps
394
- // to, mirroring what the intake bridge creates in production workspaces.
395
- addPrincipleToLedger(path.join(tmpDir, '.state'), {
396
- id: 'ledger-principle-002', version: 1, text: 'test ledger principle', triggerPattern: '', action: '',
397
- status: 'candidate', evaluability: 'weak_heuristic', priority: 'P1', scope: 'general',
398
- valueScore: 0, adherenceRate: 0, painPreventedCount: 0, derivedFromPainIds: ['cand-002'],
399
- ruleIds: [], conflictsWithPrincipleIds: [], createdAt: '2026-09-15T00:00:00.000Z', updatedAt: '2026-09-15T00:00:00.000Z',
400
- });
401
-
402
- const adapter = makeAdapter();
403
- const capability: CodeRuleCapability = { enabled: false, disabledReason: 'artificer agent disabled in config' };
404
- const result = await runRuleHostPipeline({
405
- workspaceDir: tmpDir, painId: 'pain-test-001', runtimeAdapter: adapter,
406
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
407
- codeRuleCapability: capability,
408
- behaviorExamplePack: PIPE_PACK,
409
- onStoreReady: (store) => { adapter.artifactStore = store; },
410
- });
411
-
412
- expect(result.decision).toBe('text_principle_only');
413
- expect(result.ruleArtifactId).toBeNull();
414
- // Scribe ran, so a principle artifact should exist.
415
- expect(result.principleArtifactId).not.toBeNull();
416
- expect(result.degradationReason).toContain('code_rule_capability_off');
417
- // Adversarial loop stage should be skipped (not present or skipped status).
418
- const advStage = result.stages.find((s) => s.name === 'adversarial_loop');
419
- expect(advStage?.status).toBe('skipped');
420
-
421
- // PRI-804: the text principle must NOT dead-end — it is enqueued into the
422
- // EXISTING approval queue on the prompt channel for Owner Console review.
423
- expect(result.approvalId).not.toBeNull();
424
- expect(result.approvalId).toBe(`apr_prompt_${result.principleArtifactId}`);
425
-
426
- // Verify the persisted approval row, the PRI-804(a) identity backfill, and
427
- // the terminal validation marking (real store, real SQLite, real ledger).
428
- const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
429
- await verify.initialize();
430
- try {
431
- const approvalStore = new SqliteApprovalQueueStore(verify.connection);
432
- const record = await approvalStore.getById(result.approvalId!);
433
- expect(record).not.toBeNull();
434
- expect(record!.status).toBe('pending');
435
- expect(record!.channel).toBe('prompt');
436
- expect(record!.artifactId).toBe(result.principleArtifactId);
437
- const artifact = await verify.piArtifactStore.getArtifactById(result.principleArtifactId!);
438
- expect(artifact?.sourcePrincipleId).toBe('ledger-principle-002');
439
- expect(artifact?.validationStatus).toBe('validated');
440
- } finally {
441
- await verify.close();
442
- }
443
- }, 60_000);
444
-
445
- // ── Test 3: Capability ON + evaluator rejected → generation_rejected ──
446
- it('capability ON + evaluator rejected → generation_rejected', async () => {
447
- tmpDir = makeTmpDir();
448
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
449
- await sm.initialize();
450
- await seedDreamerWithId(sm, 'dreamer-seeded-003', 'pain-test-001');
451
- await sm.close();
452
-
453
- const adapter = makeAdapter({ evaluator: evaluatorRejected });
454
- const capability: CodeRuleCapability = { enabled: true, artificerAdapter: adapter };
455
- const result = await runRuleHostPipeline({
456
- workspaceDir: tmpDir, painId: 'pain-test-001', runtimeAdapter: adapter,
457
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
458
- codeRuleCapability: capability,
459
- behaviorExamplePack: PIPE_PACK,
460
- onStoreReady: (store) => { adapter.artifactStore = store; },
461
- });
462
-
463
- expect(result.decision).toBe('generation_rejected');
464
- expect(result.ruleArtifactId).toBeNull();
465
- }, 60_000);
466
-
467
- // ── Test 4: Capability OFF by default (not provided) → text_principle_only ──
468
- it('capability OFF by default (not provided) → text_principle_only', async () => {
469
- tmpDir = makeTmpDir();
470
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
471
- await sm.initialize();
472
- await seedDreamerWithId(sm, 'dreamer-seeded-004', 'pain-test-001');
473
- await sm.close();
474
-
475
- const adapter = makeAdapter();
476
- // No codeRuleCapability provided — defaults to OFF.
477
- const result = await runRuleHostPipeline({
478
- workspaceDir: tmpDir, painId: 'pain-test-001', runtimeAdapter: adapter,
479
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
480
- onStoreReady: (store) => { adapter.artifactStore = store; },
481
- });
482
-
483
- expect(result.decision).toBe('text_principle_only');
484
- expect(result.ruleArtifactId).toBeNull();
485
- expect(result.degradationReason).toContain('code_rule_capability');
486
- }, 60_000);
487
-
488
- // ── Test 5: Exact sourcePainId match — pain-1 must NOT match pain-10 ──
489
- it('exact sourcePainId match: pain-1 does NOT match pain-10', async () => {
490
- tmpDir = makeTmpDir();
491
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
492
- await sm.initialize();
493
- // Seed dreamer for pain-10 but query for pain-1 — must NOT match.
494
- await seedDreamerWithId(sm, 'dreamer-seeded-pain-10', 'pain-10');
495
- await sm.close();
496
-
497
- const adapter = makeAdapter();
498
- const result = await runRuleHostPipeline({
499
- workspaceDir: tmpDir, painId: 'pain-1', runtimeAdapter: adapter,
500
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
501
- onStoreReady: (store) => { adapter.artifactStore = store; },
502
- });
503
-
504
- expect(result.decision).toBe('generation_rejected');
505
- expect(result.degradationReason).toContain('no_dreamer_task_seeded');
506
- }, 60_000);
507
-
508
- // ── Test 6: No dreamer task at all → generation_rejected ──
509
- it('no dreamer task seeded for pain → generation_rejected', async () => {
510
- tmpDir = makeTmpDir();
511
- const adapter = makeAdapter();
512
- const result = await runRuleHostPipeline({
513
- workspaceDir: tmpDir, painId: 'pain-nonexistent', runtimeAdapter: adapter,
514
- pollIntervalMs: 5, timeoutMs: 1000,
515
- });
516
-
517
- expect(result.decision).toBe('generation_rejected');
518
- expect(result.degradationReason).toContain('no_dreamer_task_seeded');
519
- expect(result.stages[0]!.name).toBe('pain_lookup');
520
- expect(result.stages[0]!.status).toBe('failed');
521
- });
522
-
523
- // ── Test 7: Missing or wrong-type sourcePainId → no match ──
524
- it('missing or wrong-type sourcePainId in diagnosticJson → no match', async () => {
525
- tmpDir = makeTmpDir();
526
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
527
- await sm.initialize();
528
- // Seed a dreamer task WITHOUT sourcePainId (only pi_metadata).
529
- await seedDreamerRaw(sm, 'dreamer-no-painid-001', JSON.stringify({ pi_metadata: { channel: 'prompt' } }));
530
- // Seed a dreamer task with WRONG-TYPE sourcePainId (number, not string).
531
- await seedDreamerRaw(sm, 'dreamer-wrong-type-001', JSON.stringify({ pi_metadata: { channel: 'prompt' }, sourcePainId: 12345 }));
532
- await sm.close();
533
-
534
- const adapter = makeAdapter();
535
- const result = await runRuleHostPipeline({
536
- workspaceDir: tmpDir, painId: '12345', runtimeAdapter: adapter,
537
- pollIntervalMs: 5, timeoutMs: 1000,
538
- });
539
-
540
- // Neither task should match — missing sourcePainId and wrong-type sourcePainId
541
- // are both skipped (not silently matched).
542
- expect(result.decision).toBe('generation_rejected');
543
- expect(result.degradationReason).toContain('no_dreamer_task_seeded');
544
- }, 60_000);
545
-
546
- // ── PRI-866: comparison boundary normalizes BOTH sides via the canonical reader ──
547
- it('matches a historical row whose stored sourcePainId carries surrounding whitespace', async () => {
548
- tmpDir = makeTmpDir();
549
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
550
- await sm.initialize();
551
- // Pre-existing rows may carry untrimmed sourcePainId (writer-side drift).
552
- await seedDreamerWithId(sm, 'dreamer-padded-stored', ' pain-whitespace-1 ');
553
- await sm.close();
554
-
555
- const adapter = makeAdapter();
556
- const result = await runRuleHostPipeline({
557
- workspaceDir: tmpDir, painId: 'pain-whitespace-1', runtimeAdapter: adapter,
558
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
559
- onStoreReady: (store) => { adapter.artifactStore = store; },
560
- });
561
-
562
- expect(result.stages[0]).toMatchObject({
563
- name: 'pain_lookup',
564
- status: 'succeeded',
565
- taskId: 'dreamer-padded-stored',
566
- });
567
- }, 60_000);
568
-
569
- it('matches when the queried painId itself carries surrounding whitespace', async () => {
570
- tmpDir = makeTmpDir();
571
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
572
- await sm.initialize();
573
- await seedDreamerWithId(sm, 'dreamer-padded-query', 'pain-whitespace-2');
574
- await sm.close();
575
-
576
- const adapter = makeAdapter();
577
- const result = await runRuleHostPipeline({
578
- workspaceDir: tmpDir, painId: ' pain-whitespace-2 ', runtimeAdapter: adapter,
579
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
580
- onStoreReady: (store) => { adapter.artifactStore = store; },
581
- });
582
-
583
- expect(result.stages[0]).toMatchObject({
584
- name: 'pain_lookup',
585
- status: 'succeeded',
586
- taskId: 'dreamer-padded-query',
587
- });
588
- }, 60_000);
589
-
590
- it('blank query never matches and unresolvable rows are skipped, not crashed', async () => {
591
- tmpDir = makeTmpDir();
592
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
593
- await sm.initialize();
594
- // Rows the canonical reader resolves to null (absent / wrong-type key) —
595
- // malformed JSON itself cannot be inserted (the store validates it).
596
- await seedDreamerRaw(sm, 'dreamer-no-key-866', JSON.stringify({ pi_metadata: { channel: 'code_tool_hook' } }));
597
- await seedDreamerRaw(sm, 'dreamer-wrong-type-866', JSON.stringify({ pi_metadata: { channel: 'code_tool_hook' }, sourcePainId: 12345 }));
598
- await seedDreamerWithId(sm, 'dreamer-normal-866', 'pain-not-blank');
599
- await sm.close();
600
-
601
- const result = await runRuleHostPipeline({
602
- workspaceDir: tmpDir, painId: ' ', runtimeAdapter: makeAdapter(),
603
- pollIntervalMs: 5, timeoutMs: 1000,
604
- });
605
-
606
- // A blank query normalizes to '' — it must not match any stored pain id,
607
- // and unresolvable rows are skipped via the canonical reader (rc-9: the
608
- // caller still reports the loud no-match reason).
609
- expect(result.decision).toBe('generation_rejected');
610
- expect(result.degradationReason).toContain('no_dreamer_task_seeded');
611
- }, 60_000);
612
-
613
- it('rejects ambiguous lineage when multiple runnable Dreamer tasks have the same sourcePainId', async () => {
614
- tmpDir = makeTmpDir();
615
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
616
- await sm.initialize();
617
- await seedDreamerWithId(sm, 'dreamer-ambiguous-a', 'pain-ambiguous');
618
- await seedDreamerWithId(sm, 'dreamer-ambiguous-b', 'pain-ambiguous');
619
- await sm.close();
620
-
621
- const result = await runRuleHostPipeline({
622
- workspaceDir: tmpDir,
623
- painId: 'pain-ambiguous',
624
- runtimeAdapter: makeAdapter(),
625
- pollIntervalMs: 5,
626
- timeoutMs: 1000,
627
- });
628
-
629
- expect(result.decision).toBe('generation_rejected');
630
- expect(result.degradationReason).toContain('ambiguous_dreamer_tasks_for_pain');
631
- expect(result.stages[0]?.status).toBe('failed');
632
- }, 60_000);
633
-
634
- it('selects the Dreamer task matching the requested activation channel', async () => {
635
- tmpDir = makeTmpDir();
636
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
637
- await sm.initialize();
638
- await seedDreamerWithId(sm, 'dreamer-prompt', 'pain-multi-channel', 'prompt');
639
- await seedDreamerWithId(sm, 'dreamer-code-hook', 'pain-multi-channel', 'code_tool_hook');
640
- await sm.close();
641
-
642
- const adapter = makeAdapter();
643
- const result = await runRuleHostPipeline({
644
- workspaceDir: tmpDir,
645
- painId: 'pain-multi-channel',
646
- runtimeAdapter: adapter,
647
- channel: 'code_tool_hook',
648
- pollIntervalMs: 5,
649
- timeoutMs: 1000,
650
- onStoreReady: (store) => { adapter.artifactStore = store; },
651
- });
652
-
653
- expect(result.stages[0]).toMatchObject({
654
- name: 'pain_lookup',
655
- status: 'succeeded',
656
- taskId: 'dreamer-code-hook',
657
- });
658
- }, 60_000);
659
-
660
- it('uses the sole pain-linked Dreamer seed when its source channel differs from the target channel', async () => {
661
- tmpDir = makeTmpDir();
662
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
663
- await sm.initialize();
664
- await seedDreamerWithId(sm, 'dreamer-prompt-seed', 'pain-cross-channel', 'prompt');
665
- await sm.close();
666
-
667
- const adapter = makeAdapter();
668
- const result = await runRuleHostPipeline({
669
- workspaceDir: tmpDir,
670
- painId: 'pain-cross-channel',
671
- runtimeAdapter: adapter,
672
- channel: 'code_tool_hook',
673
- pollIntervalMs: 5,
674
- timeoutMs: 1000,
675
- onStoreReady: (store) => { adapter.artifactStore = store; },
676
- });
677
-
678
- expect(result.stages[0]).toMatchObject({
679
- name: 'pain_lookup',
680
- status: 'succeeded',
681
- taskId: 'dreamer-prompt-seed',
682
- });
683
- }, 60_000);
684
-
685
- it('resumes from a succeeded Dreamer seed instead of trying to lease it again', async () => {
686
- tmpDir = makeTmpDir();
687
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
688
- await sm.initialize();
689
- await seedDreamerWithId(sm, 'dreamer-resume', 'pain-resume');
690
- await sm.close();
691
-
692
- const firstAdapter = makeAdapter();
693
- await runRuleHostPipeline({
694
- workspaceDir: tmpDir, painId: 'pain-resume', runtimeAdapter: firstAdapter,
695
- pollIntervalMs: 5, timeoutMs: 1000,
696
- onStoreReady: (store) => { firstAdapter.artifactStore = store; },
697
- });
698
-
699
- const resumedAdapter = makeAdapter();
700
- const resumed = await runRuleHostPipeline({
701
- workspaceDir: tmpDir, painId: 'pain-resume', runtimeAdapter: resumedAdapter,
702
- pollIntervalMs: 5, timeoutMs: 1000,
703
- onStoreReady: (store) => { resumedAdapter.artifactStore = store; },
704
- });
705
-
706
- expect(resumed.stages.slice(0, 2)).toEqual([
707
- { name: 'pain_lookup', status: 'succeeded', taskId: 'dreamer-resume' },
708
- { name: 'dreamer', taskId: 'dreamer-resume', status: 'succeeded' },
709
- ]);
710
- expect(resumed.degradationReason).not.toContain('lease');
711
- }, 60_000);
712
-
713
- // ── Test 8 (E fix): retried status is NOT terminal — bounded retry succeeds ──
714
- it('retried status triggers bounded retry and eventually succeeds (E fix)', async () => {
715
- tmpDir = makeTmpDir();
716
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
717
- await sm.initialize();
718
- await seedDreamerWithId(sm, 'dreamer-retry-001', 'pain-retry-001');
719
- await sm.close();
720
-
721
- // Adapter that returns retried on first dreamer call, then succeeds.
722
- let dreamerCallCount = 0;
723
- const adapter = new ScriptedAdapter({
724
- dreamer: (taskId) => {
725
- dreamerCallCount++;
726
- if (dreamerCallCount === 1) {
727
- // First call: return a payload that will cause the runner to mark
728
- // the task as retried. We simulate this by throwing — the base
729
- // runner catches errors and may mark as retried.
730
- throw new Error('transient LLM error');
731
- }
732
- return dreamerOut(taskId, 'pain-retry-001');
733
- },
734
- philosopher: philosopherOut,
735
- scribe: scribeOut,
736
- artificer: artificerV2,
737
- evaluator: evaluatorApproved,
738
- });
739
- const capability: CodeRuleCapability = { enabled: true, artificerAdapter: adapter };
740
-
741
- const result = await runRuleHostPipeline({
742
- workspaceDir: tmpDir, painId: 'pain-retry-001', runtimeAdapter: adapter,
743
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
744
- maxStageRetries: 2,
745
- codeRuleCapability: capability,
746
- behaviorExamplePack: PIPE_PACK,
747
- onStoreReady: (store) => { adapter.artifactStore = store; },
748
- });
749
-
750
- // The retry contract is exact: one transient failure, one fresh successful
751
- // attempt, then the full pipeline succeeds.
752
- const dreamerStage = result.stages.find((s) => s.name === 'dreamer');
753
- expect(dreamerCallCount).toBe(2);
754
- expect(dreamerStage?.status).toBe('succeeded');
755
- expect(result.decision).toBe('candidate_ready_for_owner_review');
756
- // Owner Decision Experience v1 Phase A: this seed carries NO candidateId,
757
- // so the chain cannot bind a ledger principle (candidate_unresolved). The
758
- // rule artifact is produced and preserved, but the governance publication
759
- // boundary REFUSES to create an Owner approval subject for it.
760
- expect(result.approvalId).toBeNull();
761
- expect(result.degradationReason ?? '').toContain('identity_binding_unverified');
762
- expect(result.degradationReason ?? '').toContain('candidate_unresolved');
763
- expect(result.ruleArtifactId).not.toBeNull();
764
- }, 60_000);
765
-
766
- // ── Test 9 (E fix): retried status exhausted → stage marked 'degraded' ──
767
- it('retried status exhausted marks stage as degraded, not failed (E fix)', async () => {
768
- tmpDir = makeTmpDir();
769
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
770
- await sm.initialize();
771
- await seedDreamerWithId(sm, 'dreamer-exhaust-001', 'pain-exhaust-001');
772
- await sm.close();
773
-
774
- // Adapter that always throws on dreamer — simulating persistent transient
775
- // errors. The runner will mark the task as retried each time until
776
- // maxStageRetries is exhausted.
777
- const adapter = new ScriptedAdapter({
778
- dreamer: () => { throw new Error('persistent transient error'); },
779
- philosopher: philosopherOut,
780
- scribe: scribeOut,
781
- artificer: artificerV2,
782
- evaluator: evaluatorApproved,
783
- });
784
-
785
- const result = await runRuleHostPipeline({
786
- workspaceDir: tmpDir, painId: 'pain-exhaust-001', runtimeAdapter: adapter,
787
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
788
- maxStageRetries: 1, // Only 1 retry to keep the test fast
789
- });
790
-
791
- // Pipeline must reject — dreamer never succeeded.
792
- expect(result.decision).toBe('generation_rejected');
793
- expect(result.degradationReason).toContain('dreamer_failed');
794
- // The stage must be precisely degraded after the initial attempt + one retry.
795
- const dreamerStage = result.stages.find((s) => s.name === 'dreamer');
796
- expect(adapter.startRunCalls.filter((call) => call.taskId === 'dreamer-exhaust-001')).toHaveLength(2);
797
- expect(dreamerStage?.status).toBe('degraded');
798
- // The reason must be present (Runtime Contract Rule 9: no silent degradation).
799
- expect(dreamerStage?.reason).toBeTruthy();
800
- }, 60_000);
801
- });
802
-
803
- // ── PRI-714 (review fix #3): language wiring ─────────────────────────────────
804
- //
805
- // The PR's original fault class was "the parameter exists but is not wired".
806
- // These regressions drive the REAL pipeline and assert the LANGUAGE DIRECTIVE
807
- // on the actual systemPrompt handed to adapter.startRun (PRI-633: the
808
- // instruction left the payload for the system channel):
809
- // - explicit `principles.outputLanguage: 'en'` in the workspace config
810
- // overrides the zh-CN default;
811
- // - no config → the zh-CN default applies.
812
- // Deleting the pipeline's language resolution makes these fail.
813
-
814
- function writeLanguageConfig(dir: string, outputLanguage?: 'en'): void {
815
- fs.mkdirSync(path.join(dir, '.pd'), { recursive: true });
816
- // Full valid-config shape (mirrors pd-config-loader.test.ts) so validation
817
- // keeps `principles` intact instead of degrading to defaults.
818
- const config: Record<string, unknown> = {
819
- version: 1,
820
- features: {
821
- prompt: { category: 'core', enabled: true },
822
- code_tool_hook: { category: 'core', enabled: true },
823
- defer_archive: { category: 'core', enabled: true },
824
- correction_observer: { category: 'quiet', enabled: false },
825
- gfi: { category: 'quiet', enabled: false },
826
- },
827
- runtimeProfiles: {
828
- 'openclaw.default': { type: 'openclaw', source: 'default' },
829
- },
830
- internalAgents: {
831
- defaultRuntime: 'openclaw.default',
832
- agents: {
833
- diagnostician: { enabled: true, runtimeProfile: 'openclaw.default' },
834
- dreamer: { enabled: true },
835
- scribe: { enabled: true },
836
- artificer: { enabled: true },
837
- philosopher: { enabled: false },
838
- evaluator: { enabled: false },
839
- rolloutReviewer: { enabled: false },
840
- correctionObserver: { enabled: false },
841
- },
842
- },
843
- ui: { diagnostics: { mode: 'simple' } },
844
- };
845
- if (outputLanguage !== undefined) config.principles = { outputLanguage };
846
- // JSON is valid YAML — same trick the shared executor tests use.
847
- fs.writeFileSync(path.join(dir, '.pd', 'config.yaml'), JSON.stringify(config));
848
- }
849
-
850
- function collectStartRunInputs(adapter: ScriptedAdapter): Array<{ taskId: string; outputSchemaRef?: string; payload: string; systemPrompt?: string }> {
851
- const inputs: Array<{ taskId: string; outputSchemaRef?: string; payload: string; systemPrompt?: string }> = [];
852
- for (const input of adapter.startRunInputs.values()) {
853
- inputs.push({
854
- taskId: input.taskRef.taskId,
855
- outputSchemaRef: input.outputSchemaRef,
856
- payload: typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload),
857
- systemPrompt: input.systemPrompt,
858
- });
859
- }
860
- return inputs;
861
- }
862
-
863
- describe('runRuleHostPipeline — outputLanguage reaches adapter.startRun messages (PRI-714 review fix)', () => {
864
- afterEach(() => {
865
- vi.restoreAllMocks();
866
- if (tmpDir) { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } tmpDir = ''; }
867
- });
868
-
869
- it('explicit principles.outputLanguage=en overrides the zh-CN default on stage messages', async () => {
870
- tmpDir = makeTmpDir();
871
- writeLanguageConfig(tmpDir, 'en');
872
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
873
- await sm.initialize();
874
- await seedDreamerWithId(sm, 'dreamer-lang-en-001', 'pain-lang-en');
875
- await sm.close();
876
-
877
- const adapter = makeAdapter();
878
- const result = await runRuleHostPipeline({
879
- workspaceDir: tmpDir, painId: 'pain-lang-en', runtimeAdapter: adapter,
880
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
881
- codeRuleCapability: { enabled: true, artificerAdapter: adapter },
882
- behaviorExamplePack: PIPE_PACK,
883
- onStoreReady: (store) => { adapter.artifactStore = store; },
884
- });
885
- expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
886
-
887
- const inputs = collectStartRunInputs(adapter);
888
- // Dreamer: dreamer-subject field list, English directive on the system channel.
889
- const dreamer = inputs.find((i) => i.taskId.startsWith('dreamer'));
890
- expect(dreamer).toBeDefined();
891
- expect(dreamer?.systemPrompt).toContain('LANGUAGE DIRECTIVE');
892
- expect(dreamer?.systemPrompt).toContain('English');
893
- expect(dreamer?.systemPrompt).toContain('(candidates[].badDecision, candidates[].betterDecision, candidates[].rationale, candidates[].strategicPerspective)');
894
- expect(dreamer?.payload).toContain('dreamer-lang-en-001');
895
- expect(dreamer?.payload).not.toContain('LANGUAGE DIRECTIVE');
896
- // Philosopher: philosopher-subject field list, English directive.
897
- const philosopher = inputs.find((i) => i.taskId.includes('philosopher'));
898
- expect(philosopher).toBeDefined();
899
- expect(philosopher?.systemPrompt).toContain('LANGUAGE DIRECTIVE');
900
- expect(philosopher?.systemPrompt).toContain('English');
901
- expect(philosopher?.systemPrompt).toContain('(thesis, principleCandidate.title, principleCandidate.rationale, principleCandidate.scope, risks[])');
902
- // Evaluator: review-subject explicit nested paths + PRI-630 ledger echo rule.
903
- const evaluator = inputs.find((i) => i.outputSchemaRef === 'evaluator-output-v1');
904
- expect(evaluator).toBeDefined();
905
- expect(evaluator?.systemPrompt).toContain('LANGUAGE DIRECTIVE');
906
- expect(evaluator?.systemPrompt).toContain('English');
907
- expect(evaluator?.systemPrompt).toContain('codeReview.traceCoverage.gaps');
908
- expect(evaluator?.systemPrompt).toContain('adversarialCases[].rationale');
909
- expect(evaluator?.systemPrompt).toContain('requirementLedger[].statement MUST be copied verbatim');
910
- // The explicit en must have fully replaced the zh-CN default everywhere.
911
- for (const i of inputs) {
912
- expect(i.systemPrompt ?? '', 'Simplified Chinese leaked into an en-configured pipeline').not.toContain('Simplified Chinese');
913
- expect(i.payload, 'Simplified Chinese leaked into an en-configured pipeline').not.toContain('Simplified Chinese');
914
- }
915
- }, 60_000);
916
-
917
- it('no config file resolves the zh-CN default on the dreamer message', async () => {
918
- tmpDir = makeTmpDir();
919
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
920
- await sm.initialize();
921
- await seedDreamerWithId(sm, 'dreamer-lang-zh-001', 'pain-lang-zh');
922
- await sm.close();
923
-
924
- const adapter = makeAdapter();
925
- const result = await runRuleHostPipeline({
926
- workspaceDir: tmpDir, painId: 'pain-lang-zh', runtimeAdapter: adapter,
927
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
928
- codeRuleCapability: { enabled: true, artificerAdapter: adapter },
929
- behaviorExamplePack: PIPE_PACK,
930
- onStoreReady: (store) => { adapter.artifactStore = store; },
931
- });
932
- expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
933
-
934
- const inputs = collectStartRunInputs(adapter);
935
- const dreamer = inputs.find((i) => i.taskId.startsWith('dreamer'));
936
- expect(dreamer).toBeDefined();
937
- expect(dreamer?.systemPrompt).toContain('LANGUAGE DIRECTIVE');
938
- expect(dreamer?.systemPrompt).toContain('Simplified Chinese');
939
- expect(dreamer?.payload).toContain('dreamer-lang-zh-001');
940
- }, 60_000);
941
- });
942
-
943
- // ── Owner Decision Experience v1 Phase A — identity fix-forward (SPEC §9) ────
944
- //
945
- // §9.4 deterministic acceptance slice, exercised against the REAL production
946
- // path (real RuntimeStateManager, real SQLite, real ledger adapter):
947
- // success / ledger missing / ambiguous / conflict / write failure / replay.
948
- // Failure slices must prove: NO approval enqueue, NO activation, NO duplicate
949
- // ledger — while the generated artifacts stay preserved for later replay.
950
-
951
- describe('runRuleHostPipeline — Phase A governance publication boundary (identity fix-forward)', () => {
952
- afterEach(() => {
953
- vi.restoreAllMocks();
954
- if (tmpDir) { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } tmpDir = ''; }
955
- });
956
-
957
- function seedLedger(dir: string, id: string, candidateId: string): void {
958
- addPrincipleToLedger(path.join(dir, '.state'), {
959
- id, version: 1, text: `principle ${id}`, triggerPattern: '', action: '',
960
- status: 'candidate', evaluability: 'weak_heuristic', priority: 'P1', scope: 'general',
961
- valueScore: 0, adherenceRate: 0, painPreventedCount: 0, derivedFromPainIds: [candidateId],
962
- ruleIds: [], conflictsWithPrincipleIds: [], createdAt: '2026-09-15T00:00:00.000Z', updatedAt: '2026-09-15T00:00:00.000Z',
963
- });
964
- }
965
-
966
- async function countApprovalsForArtifact(dir: string, artifactId: string): Promise<number> {
967
- const verify = new RuntimeStateManager({ workspaceDir: dir });
968
- await verify.initialize();
969
- try {
970
- const rows = verify.connection.getDb()
971
- .prepare('SELECT COUNT(*) AS n FROM approvals WHERE artifact_id = ?').all(artifactId);
972
- const first = rows[0] as { n: number } | undefined;
973
- return first?.n ?? 0;
974
- } finally {
975
- await verify.close();
976
- }
977
- }
978
-
979
- async function countActivations(dir: string): Promise<number> {
980
- const verify = new RuntimeStateManager({ workspaceDir: dir });
981
- await verify.initialize();
982
- try {
983
- const rows = verify.connection.getDb().prepare('SELECT COUNT(*) AS n FROM activations').all();
984
- const first = rows[0] as { n: number } | undefined;
985
- return first?.n ?? 0;
986
- } finally {
987
- await verify.close();
988
- }
989
- }
990
-
991
- async function ledgerPrincipleCount(dir: string): Promise<number> {
992
- const ledgerPath = path.join(dir, '.state', 'principle_training_state.json');
993
- if (!fs.existsSync(ledgerPath)) return 0; // no ledger file yet = zero principles
994
- const parsed = JSON.parse(fs.readFileSync(ledgerPath, 'utf8')) as { _tree?: { principles?: Record<string, unknown> } };
995
- return Object.keys(parsed._tree?.principles ?? {}).length;
996
- }
997
-
998
- it('ledger_missing: rule artifact preserved, but NO approval subject, NO activation, ledger untouched', async () => {
999
- tmpDir = makeTmpDir();
1000
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
1001
- await sm.initialize();
1002
- await seedDreamerWithId(sm, 'dreamer-id-missing', 'pain-id-missing', 'code_tool_hook', 'cand-no-ledger');
1003
- await sm.close();
1004
- // Deliberately NO ledger principle for cand-no-ledger.
1005
-
1006
- const adapter = makeAdapter();
1007
- const result = await runRuleHostPipeline({
1008
- workspaceDir: tmpDir, painId: 'pain-id-missing', runtimeAdapter: adapter,
1009
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
1010
- codeRuleCapability: { enabled: true, artificerAdapter: adapter },
1011
- behaviorExamplePack: PIPE_PACK,
1012
- onStoreReady: (store) => { adapter.artifactStore = store; },
1013
- });
1014
-
1015
- expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
1016
- // The generation chain produced and PRESERVED its artifacts…
1017
- expect(result.ruleArtifactId).not.toBeNull();
1018
- // …but the publication boundary refused to create an Owner approval subject.
1019
- expect(result.approvalId).toBeNull();
1020
- expect(result.degradationReason ?? '').toContain('identity_binding_unverified');
1021
- expect(result.degradationReason ?? '').toContain('ledger_missing');
1022
- expect(await countApprovalsForArtifact(tmpDir, result.ruleArtifactId!)).toBe(0);
1023
- expect(await countActivations(tmpDir)).toBe(0);
1024
- expect(await ledgerPrincipleCount(tmpDir)).toBe(0);
1025
- }, 60_000);
1026
-
1027
- it('ledger_ambiguous: two ledger targets for one candidate refuse publication and refuse a silent first-match binding', async () => {
1028
- tmpDir = makeTmpDir();
1029
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
1030
- await sm.initialize();
1031
- await seedDreamerWithId(sm, 'dreamer-id-ambiguous', 'pain-id-ambiguous', 'code_tool_hook', 'cand-ambiguous');
1032
- await sm.close();
1033
- seedLedger(tmpDir, 'ledger-A', 'cand-ambiguous');
1034
- seedLedger(tmpDir, 'ledger-B', 'cand-ambiguous');
1035
-
1036
- const adapter = makeAdapter();
1037
- const result = await runRuleHostPipeline({
1038
- workspaceDir: tmpDir, painId: 'pain-id-ambiguous', runtimeAdapter: adapter,
1039
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
1040
- codeRuleCapability: { enabled: true, artificerAdapter: adapter },
1041
- behaviorExamplePack: PIPE_PACK,
1042
- onStoreReady: (store) => { adapter.artifactStore = store; },
1043
- });
1044
-
1045
- expect(result.decision).toBe('candidate_ready_for_owner_review');
1046
- expect(result.approvalId).toBeNull();
1047
- expect(result.degradationReason ?? '').toContain('ledger_ambiguous');
1048
- expect(await countApprovalsForArtifact(tmpDir, result.ruleArtifactId!)).toBe(0);
1049
- // The scribe artifact must NOT be silently bound to the first match.
1050
- const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
1051
- await verify.initialize();
1052
- try {
1053
- const scribeTaskId = result.stages.find((s) => s.name === 'scribe')!.taskId!;
1054
- const scribeArt = (await verify.piArtifactStore.listBySourceTaskId(scribeTaskId)).find((a) => a.artifactKind === 'principle');
1055
- expect(scribeArt?.sourcePrincipleId).toBeUndefined();
1056
- } finally {
1057
- await verify.close();
1058
- }
1059
- }, 60_000);
1060
-
1061
- it('identity_conflict: an existing binding to a different ledger target is never overwritten (§9.1.7)', async () => {
1062
- tmpDir = makeTmpDir();
1063
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
1064
- await sm.initialize();
1065
- await seedDreamerWithId(sm, 'dreamer-id-conflict', 'pain-id-conflict', 'code_tool_hook', 'cand-conflict');
1066
- await sm.close();
1067
- seedLedger(tmpDir, 'ledger-original', 'cand-conflict');
1068
-
1069
- const adapter = makeAdapter();
1070
- const first = await runRuleHostPipeline({
1071
- workspaceDir: tmpDir, painId: 'pain-id-conflict', runtimeAdapter: adapter,
1072
- channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
1073
- codeRuleCapability: { enabled: true, artificerAdapter: adapter },
1074
- behaviorExamplePack: PIPE_PACK,
1075
- onStoreReady: (store) => { adapter.artifactStore = store; },
1076
- });
1077
- expect(first.approvalId).not.toBeNull();
1078
-
1079
- // Repoint the candidate: detach it from the original principle so the
1080
- // chain now resolves to a DIFFERENT (still unique) target. Otherwise the
1081
- // plural lookup would correctly report ledger_ambiguous, not a conflict.
1082
- updatePrinciple(path.join(tmpDir, '.state'), 'ledger-original', { derivedFromPainIds: [] });
1083
- seedLedger(tmpDir, 'ledger-replacement', 'cand-conflict');
1084
- const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
1085
- await verify.initialize();
1086
- try {
1087
- const scribeTaskId = first.stages.find((s) => s.name === 'scribe')!.taskId!;
1088
- const binding = await backfillScribeIdentity({
1089
- stateManager: verify,
1090
- artifactStore: verify.piArtifactStore,
1091
- scribeTaskId,
1092
- dreamerTaskId: 'dreamer-id-conflict',
1093
- workspaceDir: tmpDir,
1094
- now: new Date().toISOString(),
1095
- });
1096
- expect(binding).toEqual({ status: 'identity_conflict', existingPrincipleId: 'ledger-original', resolvedPrincipleId: 'ledger-replacement' });
1097
- const scribeArt = (await verify.piArtifactStore.listBySourceTaskId(scribeTaskId)).find((a) => a.artifactKind === 'principle');
1098
- expect(scribeArt?.sourcePrincipleId).toBe('ledger-original');
1099
- } finally {
1100
- await verify.close();
1101
- }
1102
- }, 60_000);
1103
-
1104
- it('binding_write_failed / binding_readback_failed: store failures are structured, never silently accepted', async () => {
1105
- tmpDir = makeTmpDir();
1106
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
1107
- await sm.initialize();
1108
- await seedDreamerWithId(sm, 'dreamer-id-wf', 'pain-id-wf', 'code_tool_hook', 'cand-wf');
1109
- await sm.close();
1110
- seedLedger(tmpDir, 'ledger-wf', 'cand-wf');
1111
-
1112
- // Run once through the real path so a REAL scribe artifact exists.
1113
- const adapter = makeAdapter();
1114
- const run = await runRuleHostPipeline({
1115
- workspaceDir: tmpDir, painId: 'pain-id-wf', runtimeAdapter: adapter,
1116
- channel: 'prompt', pollIntervalMs: 5, timeoutMs: 1000,
1117
- onStoreReady: (store) => { adapter.artifactStore = store; },
1118
- });
1119
- expect(run.decision).toBe('text_principle_only');
1120
-
1121
- const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
1122
- await verify.initialize();
1123
- const scribeTaskId = run.stages.find((s) => s.name === 'scribe')!.taskId!;
1124
- try {
1125
- // The real run already bound + verified — replay must be idempotent.
1126
- const idempotent = await backfillScribeIdentity({
1127
- stateManager: verify, artifactStore: verify.piArtifactStore, scribeTaskId,
1128
- dreamerTaskId: 'dreamer-id-wf', workspaceDir: tmpDir, now: new Date().toISOString(),
1129
- });
1130
- expect(idempotent).toEqual({ status: 'already_bound', principleId: 'ledger-wf' });
1131
-
1132
- // Write failure: upsert throws.
1133
- const throwingStore = new Proxy(verify.piArtifactStore, {
1134
- get(target, prop, receiver) {
1135
- if (prop === 'upsertArtifact') {
1136
- return async () => { throw new Error('sqlite locked (simulated)'); };
1137
- }
1138
- return Reflect.get(target, prop, receiver);
1139
- },
1140
- });
1141
- // For the write-failure slice we need an UNBOUND artifact: use a second,
1142
- // unbound scribe-shaped artifact created directly in the store.
1143
- const taskId2 = 'scribe-unbound-task';
1144
- await verify.createTask({ taskId: taskId2, taskKind: 'scribe', status: 'succeeded', attemptCount: 0, maxAttempts: 3, diagnosticJson: JSON.stringify({ pi_metadata: { channel: 'prompt', dependencyTaskIds: [], timeoutMs: 1000 } }) });
1145
- await verify.piArtifactStore.createArtifact({
1146
- artifactId: 'pi-art-unbound-1', artifactKind: 'principle', sourceTaskId: taskId2,
1147
- lineageArtifactIds: [], validationStatus: 'pending', contentJson: '{}',
1148
- createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
1149
- });
1150
- const failed = await backfillScribeIdentity({
1151
- stateManager: verify, artifactStore: throwingStore, scribeTaskId: taskId2,
1152
- dreamerTaskId: 'dreamer-id-wf', workspaceDir: tmpDir, now: new Date().toISOString(),
1153
- });
1154
- expect(failed.status).toBe('binding_write_failed');
1155
-
1156
- // Read-back failure: upsert "succeeds" but the store returns a row without
1157
- // the binding (write lost / wrong row).
1158
- const staleStore = new Proxy(verify.piArtifactStore, {
1159
- get(target, prop, receiver) {
1160
- if (prop === 'getArtifactById') {
1161
- return async () => null;
1162
- }
1163
- return Reflect.get(target, prop, receiver);
1164
- },
1165
- });
1166
- const stale = await backfillScribeIdentity({
1167
- stateManager: verify, artifactStore: staleStore, scribeTaskId: taskId2,
1168
- dreamerTaskId: 'dreamer-id-wf', workspaceDir: tmpDir, now: new Date().toISOString(),
1169
- });
1170
- expect(stale.status).toBe('binding_readback_failed');
1171
- } finally {
1172
- await verify.close();
1173
- }
1174
- }, 60_000);
1175
-
1176
- it('text path: unverified binding preserves the principle artifact but creates NO approval (candidate_unresolved)', async () => {
1177
- tmpDir = makeTmpDir();
1178
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
1179
- await sm.initialize();
1180
- // No candidateId on the seed — candidate_unresolved.
1181
- await seedDreamerWithId(sm, 'dreamer-text-noid', 'pain-text-noid', 'prompt');
1182
- await sm.close();
1183
-
1184
- const adapter = makeAdapter();
1185
- const result = await runRuleHostPipeline({
1186
- workspaceDir: tmpDir, painId: 'pain-text-noid', runtimeAdapter: adapter,
1187
- channel: 'prompt', pollIntervalMs: 5, timeoutMs: 1000,
1188
- onStoreReady: (store) => { adapter.artifactStore = store; },
1189
- });
1190
-
1191
- expect(result.decision).toBe('text_principle_only');
1192
- expect(result.principleArtifactId).not.toBeNull();
1193
- expect(result.approvalId).toBeNull();
1194
- expect(result.degradationReason ?? '').toContain('identity_binding_unverified');
1195
- expect(result.degradationReason ?? '').toContain('candidate_unresolved');
1196
- expect(await countApprovalsForArtifact(tmpDir, result.principleArtifactId!)).toBe(0);
1197
- // The artifact is still marked validated — preserved, publishable later.
1198
- const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
1199
- await verify.initialize();
1200
- try {
1201
- const artifact = await verify.piArtifactStore.getArtifactById(result.principleArtifactId!);
1202
- expect(artifact?.validationStatus).toBe('validated');
1203
- expect(artifact?.sourcePrincipleId).toBeUndefined();
1204
- } finally {
1205
- await verify.close();
1206
- }
1207
- }, 60_000);
1208
-
1209
- it('replay after identity repair publishes: seed ledger mid-flight is unnecessary — repair-then-rerun enqueues (§9.1.8)', async () => {
1210
- tmpDir = makeTmpDir();
1211
- const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
1212
- await sm.initialize();
1213
- await seedDreamerWithId(sm, 'dreamer-repair', 'pain-repair', 'prompt', 'cand-repair');
1214
- await sm.close();
1215
-
1216
- const adapter1 = makeAdapter();
1217
- const refused = await runRuleHostPipeline({
1218
- workspaceDir: tmpDir, painId: 'pain-repair', runtimeAdapter: adapter1,
1219
- channel: 'prompt', pollIntervalMs: 5, timeoutMs: 1000,
1220
- onStoreReady: (store) => { adapter1.artifactStore = store; },
1221
- });
1222
- expect(refused.approvalId).toBeNull();
1223
-
1224
- // Identity repair: the ledger entry for the candidate appears (e.g. intake
1225
- // completed or the ledger was restored). Replay publishes.
1226
- seedLedger(tmpDir, 'ledger-repaired', 'cand-repair');
1227
- const adapter2 = makeAdapter();
1228
- // Run 2 RESUMES run 1's succeeded dreamer seed (no new dreamer startRun) —
1229
- // pre-register that call so the scripted philosopher can resolve its
1230
- // sourceDreamerArtifactId lineage from the shared store.
1231
- adapter2.startRunCalls.push({ taskId: 'dreamer-repair' });
1232
- const published = await runRuleHostPipeline({
1233
- workspaceDir: tmpDir, painId: 'pain-repair', runtimeAdapter: adapter2,
1234
- channel: 'prompt', pollIntervalMs: 5, timeoutMs: 1000,
1235
- onStoreReady: (store) => { adapter2.artifactStore = store; },
1236
- });
1237
- expect(published.approvalId, JSON.stringify(published, null, 1)).not.toBeNull();
1238
- expect(published.principleArtifactId).not.toBe(refused.principleArtifactId);
1239
- // Exactly one ledger principle exists — the pipeline never creates one.
1240
- expect(await ledgerPrincipleCount(tmpDir)).toBe(1);
1241
- }, 60_000);
1242
- });