@principles/pd-cli 1.152.12 → 1.152.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/package.json +13 -7
  2. package/CHANGELOG.md +0 -23
  3. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts +0 -9
  4. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts.map +0 -1
  5. package/dist/commands/__tests__/intent-flag-wiring.test.js +0 -237
  6. package/dist/commands/__tests__/intent-flag-wiring.test.js.map +0 -1
  7. package/dist/commands/__tests__/legacy-cleanup.test.d.ts +0 -18
  8. package/dist/commands/__tests__/legacy-cleanup.test.d.ts.map +0 -1
  9. package/dist/commands/__tests__/legacy-cleanup.test.js +0 -467
  10. package/dist/commands/__tests__/legacy-cleanup.test.js.map +0 -1
  11. package/dist/commands/__tests__/mvp-smoke.test.d.ts +0 -15
  12. package/dist/commands/__tests__/mvp-smoke.test.d.ts.map +0 -1
  13. package/dist/commands/__tests__/mvp-smoke.test.js +0 -262
  14. package/dist/commands/__tests__/mvp-smoke.test.js.map +0 -1
  15. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +0 -2
  16. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +0 -1
  17. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +0 -60
  18. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +0 -1
  19. package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts +0 -21
  20. package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts.map +0 -1
  21. package/dist/commands/__tests__/rulecode-flag-wiring.test.js +0 -186
  22. package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +0 -1
  23. package/dist/commands/__tests__/rulecode-handler.test.d.ts +0 -16
  24. package/dist/commands/__tests__/rulecode-handler.test.d.ts.map +0 -1
  25. package/dist/commands/__tests__/rulecode-handler.test.js +0 -285
  26. package/dist/commands/__tests__/rulecode-handler.test.js.map +0 -1
  27. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts +0 -24
  28. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts.map +0 -1
  29. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +0 -232
  30. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +0 -1
  31. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts +0 -15
  32. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts.map +0 -1
  33. package/dist/commands/__tests__/runtime-activation-nextaction.test.js +0 -73
  34. package/dist/commands/__tests__/runtime-activation-nextaction.test.js.map +0 -1
  35. package/dist/commands/__tests__/runtime-probe-config.test.d.ts +0 -20
  36. package/dist/commands/__tests__/runtime-probe-config.test.d.ts.map +0 -1
  37. package/dist/commands/__tests__/runtime-probe-config.test.js +0 -389
  38. package/dist/commands/__tests__/runtime-probe-config.test.js.map +0 -1
  39. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts +0 -9
  40. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts.map +0 -1
  41. package/dist/commands/__tests__/telemetry-flag-wiring.test.js +0 -76
  42. package/dist/commands/__tests__/telemetry-flag-wiring.test.js.map +0 -1
  43. package/dist/commands/__tests__/version.test.d.ts +0 -2
  44. package/dist/commands/__tests__/version.test.d.ts.map +0 -1
  45. package/dist/commands/__tests__/version.test.js +0 -238
  46. package/dist/commands/__tests__/version.test.js.map +0 -1
  47. package/dist/commands/runtime-uat.guard.test.d.ts +0 -2
  48. package/dist/commands/runtime-uat.guard.test.d.ts.map +0 -1
  49. package/dist/commands/runtime-uat.guard.test.js +0 -153
  50. package/dist/commands/runtime-uat.guard.test.js.map +0 -1
  51. package/dist/services/__tests__/cli-output.test.d.ts +0 -18
  52. package/dist/services/__tests__/cli-output.test.d.ts.map +0 -1
  53. package/dist/services/__tests__/cli-output.test.js +0 -103
  54. package/dist/services/__tests__/cli-output.test.js.map +0 -1
  55. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +0 -2
  56. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +0 -1
  57. package/dist/services/__tests__/evaluator-runner-deps.test.js +0 -343
  58. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +0 -1
  59. package/dist/services/__tests__/rulehost-readiness.test.d.ts +0 -2
  60. package/dist/services/__tests__/rulehost-readiness.test.d.ts.map +0 -1
  61. package/dist/services/__tests__/rulehost-readiness.test.js +0 -311
  62. package/dist/services/__tests__/rulehost-readiness.test.js.map +0 -1
  63. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts +0 -18
  64. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts.map +0 -1
  65. package/dist/services/__tests__/runtime-adapter-resolver.test.js +0 -673
  66. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +0 -1
  67. package/dist/utils/production-workspace-guard.test.d.ts +0 -2
  68. package/dist/utils/production-workspace-guard.test.d.ts.map +0 -1
  69. package/dist/utils/production-workspace-guard.test.js +0 -137
  70. package/dist/utils/production-workspace-guard.test.js.map +0 -1
  71. package/scripts/llm-dogfood.ts +0 -456
  72. package/scripts/migrate-illegal-expected-decision.ts +0 -252
  73. package/src/commands/__tests__/intent-flag-wiring.test.ts +0 -282
  74. package/src/commands/__tests__/legacy-cleanup.test.ts +0 -602
  75. package/src/commands/__tests__/mvp-smoke.test.ts +0 -300
  76. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +0 -73
  77. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +0 -237
  78. package/src/commands/__tests__/rulecode-handler.test.ts +0 -369
  79. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +0 -292
  80. package/src/commands/__tests__/runtime-activation-nextaction.test.ts +0 -81
  81. package/src/commands/__tests__/runtime-probe-config.test.ts +0 -432
  82. package/src/commands/__tests__/telemetry-flag-wiring.test.ts +0 -84
  83. package/src/commands/__tests__/version.test.ts +0 -251
  84. package/src/commands/admission-gate.ts +0 -30
  85. package/src/commands/artifact.ts +0 -82
  86. package/src/commands/build-trajectory-evidence.ts +0 -298
  87. package/src/commands/candidate.ts +0 -1250
  88. package/src/commands/codex-ingest-catchup.ts +0 -114
  89. package/src/commands/codex-ingest-quarantine.ts +0 -127
  90. package/src/commands/codex-reconcile.ts +0 -84
  91. package/src/commands/codex-setup.ts +0 -476
  92. package/src/commands/codex-worker.ts +0 -185
  93. package/src/commands/command-helpers.ts +0 -24
  94. package/src/commands/config-doctor.ts +0 -193
  95. package/src/commands/console.ts +0 -797
  96. package/src/commands/context.ts +0 -72
  97. package/src/commands/demo-story-a.ts +0 -241
  98. package/src/commands/diagnose.ts +0 -756
  99. package/src/commands/errors-list.ts +0 -441
  100. package/src/commands/evolution-tasks-list.ts +0 -48
  101. package/src/commands/evolution-tasks-show.ts +0 -62
  102. package/src/commands/flow.ts +0 -60
  103. package/src/commands/health-codex.ts +0 -576
  104. package/src/commands/health.ts +0 -315
  105. package/src/commands/history.ts +0 -63
  106. package/src/commands/intent.ts +0 -457
  107. package/src/commands/legacy-cleanup.ts +0 -531
  108. package/src/commands/legacy-import.ts +0 -104
  109. package/src/commands/mvp-smoke.ts +0 -160
  110. package/src/commands/pain-evidence.ts +0 -226
  111. package/src/commands/pain-list.ts +0 -239
  112. package/src/commands/pain-record.ts +0 -612
  113. package/src/commands/pain-retry.ts +0 -916
  114. package/src/commands/principles-stats.ts +0 -700
  115. package/src/commands/proven-channel-baseline.ts +0 -115
  116. package/src/commands/quality-scorecard.ts +0 -272
  117. package/src/commands/remediation-output.ts +0 -66
  118. package/src/commands/rulecode.ts +0 -465
  119. package/src/commands/run.ts +0 -89
  120. package/src/commands/runtime-activation.ts +0 -1629
  121. package/src/commands/runtime-artifact-repair.ts +0 -524
  122. package/src/commands/runtime-canary.ts +0 -395
  123. package/src/commands/runtime-compatibility-scan.ts +0 -105
  124. package/src/commands/runtime-diagnostics-export.ts +0 -229
  125. package/src/commands/runtime-features.ts +0 -153
  126. package/src/commands/runtime-gfi-snapshot.ts +0 -135
  127. package/src/commands/runtime-health-snapshot.ts +0 -108
  128. package/src/commands/runtime-init.ts +0 -429
  129. package/src/commands/runtime-internalization-enqueue-successors.ts +0 -530
  130. package/src/commands/runtime-internalization-integrity-repair.ts +0 -79
  131. package/src/commands/runtime-internalization-integrity.ts +0 -107
  132. package/src/commands/runtime-internalization-queue.ts +0 -135
  133. package/src/commands/runtime-internalization-retry.ts +0 -191
  134. package/src/commands/runtime-internalization-run-once.ts +0 -791
  135. package/src/commands/runtime-internalization-run-rulehost.ts +0 -749
  136. package/src/commands/runtime-internalization-wake-once.ts +0 -87
  137. package/src/commands/runtime-pain-flood-simulation.ts +0 -121
  138. package/src/commands/runtime-pruning.ts +0 -438
  139. package/src/commands/runtime-recovery-failed-tasks.ts +0 -258
  140. package/src/commands/runtime-recovery.ts +0 -109
  141. package/src/commands/runtime-synthetic-baseline.ts +0 -70
  142. package/src/commands/runtime-uat.guard.test.ts +0 -175
  143. package/src/commands/runtime-uat.ts +0 -380
  144. package/src/commands/runtime.ts +0 -473
  145. package/src/commands/samples-list.ts +0 -39
  146. package/src/commands/samples-review.ts +0 -36
  147. package/src/commands/task.ts +0 -271
  148. package/src/commands/telemetry.ts +0 -349
  149. package/src/commands/trace.ts +0 -174
  150. package/src/commands/trajectory-db-unavailable.ts +0 -19
  151. package/src/commands/trajectory.ts +0 -64
  152. package/src/commands/version.ts +0 -49
  153. package/src/config-reader.ts +0 -122
  154. package/src/index.ts +0 -1276
  155. package/src/legacy/legacy-import.ts +0 -179
  156. package/src/legacy/session-history-import.ts +0 -203
  157. package/src/resolve-workspace.ts +0 -106
  158. package/src/services/__tests__/cli-output.test.ts +0 -130
  159. package/src/services/__tests__/evaluator-runner-deps.test.ts +0 -382
  160. package/src/services/__tests__/rulehost-readiness.test.ts +0 -363
  161. package/src/services/__tests__/runtime-adapter-resolver.test.ts +0 -793
  162. package/src/services/cli-output.ts +0 -95
  163. package/src/services/config-doctor.ts +0 -697
  164. package/src/services/console-launcher.ts +0 -537
  165. package/src/services/demo-rule-compiler.ts +0 -40
  166. package/src/services/demo-story-a-runner.ts +0 -471
  167. package/src/services/mainline-snapshot-assembler.ts +0 -579
  168. package/src/services/pain-flood-simulation-runner.ts +0 -462
  169. package/src/services/pd-config-loader.ts +0 -433
  170. package/src/services/proven-channel-baseline-runner.ts +0 -157
  171. package/src/services/quality-scorecard/data-extractor.ts +0 -150
  172. package/src/services/quality-scorecard/local-evaluator.ts +0 -142
  173. package/src/services/quality-scorecard/strong-model-gate.ts +0 -202
  174. package/src/services/resolve-runtime-from-pd-config.ts +0 -230
  175. package/src/services/rulehost-pipeline-runner.ts +0 -1214
  176. package/src/services/rulehost-readiness.ts +0 -326
  177. package/src/services/runtime-adapter-resolver.ts +0 -364
  178. package/src/services/synthetic-baseline-runner.ts +0 -400
  179. package/src/services/version-report.ts +0 -253
  180. package/src/services/workspace-tool-semantics.ts +0 -26
  181. package/src/utils/path-security.ts +0 -96
  182. package/src/utils/production-workspace-guard.test.ts +0 -157
  183. package/src/utils/production-workspace-guard.ts +0 -210
  184. package/tests/bdd/cli-contract.steps.ts +0 -346
  185. package/tests/bdd/principles-stats.steps.ts +0 -211
  186. package/tests/bdd/support/gherkin-loader.ts +0 -147
  187. package/tests/bdd/support/repo-root.ts +0 -62
  188. package/tests/bdd/support/vitest-bdd.ts +0 -179
  189. package/tests/commands/admission-gate-coverage.test.ts +0 -79
  190. package/tests/commands/build-trajectory-evidence.test.ts +0 -508
  191. package/tests/commands/candidate-audit-repair.test.ts +0 -421
  192. package/tests/commands/candidate-intake.test.ts +0 -732
  193. package/tests/commands/candidate-internalization-backfill.test.ts +0 -548
  194. package/tests/commands/candidate-internalize-lineage.test.ts +0 -628
  195. package/tests/commands/candidate-internalize.test.ts +0 -299
  196. package/tests/commands/candidate-route.test.ts +0 -329
  197. package/tests/commands/candidate-show.test.ts +0 -95
  198. package/tests/commands/cli-command-tree.test.ts +0 -144
  199. package/tests/commands/cli-help-snapshot.test.ts +0 -155
  200. package/tests/commands/cli-skill-contract.test.ts +0 -121
  201. package/tests/commands/codex-consent-reversibility.steps.test.ts +0 -299
  202. package/tests/commands/codex-ingest-quarantine.test.ts +0 -136
  203. package/tests/commands/codex-setup.test.ts +0 -297
  204. package/tests/commands/codex-worker-daemon.test.ts +0 -95
  205. package/tests/commands/codex-worker-registration.test.ts +0 -207
  206. package/tests/commands/config-doctor.test.ts +0 -336
  207. package/tests/commands/console-launcher-edge-cases.test.ts +0 -388
  208. package/tests/commands/console-open.test.ts +0 -1381
  209. package/tests/commands/context.test.ts +0 -114
  210. package/tests/commands/demo-story-a.test.ts +0 -318
  211. package/tests/commands/diagnose.test.ts +0 -1803
  212. package/tests/commands/errors-list.test.ts +0 -679
  213. package/tests/commands/evolution-tasks-list.test.ts +0 -41
  214. package/tests/commands/evolution-tasks-show.test.ts +0 -46
  215. package/tests/commands/health-codex.test.ts +0 -349
  216. package/tests/commands/health-host-wiring.test.ts +0 -76
  217. package/tests/commands/health-reliability.test.ts +0 -99
  218. package/tests/commands/health.test.ts +0 -413
  219. package/tests/commands/intent.test.ts +0 -321
  220. package/tests/commands/legacy-cleanup.test.ts +0 -148
  221. package/tests/commands/pain-evidence.test.ts +0 -518
  222. package/tests/commands/pain-list.test.ts +0 -219
  223. package/tests/commands/pain-record-async.test.ts +0 -335
  224. package/tests/commands/pain-record-session-parser.test.ts +0 -228
  225. package/tests/commands/pain-record.test.ts +0 -851
  226. package/tests/commands/pain-retry.test.ts +0 -1121
  227. package/tests/commands/plugin-config-resolution-cutover.test.ts +0 -229
  228. package/tests/commands/pri-393-runtime-config-unification.test.ts +0 -443
  229. package/tests/commands/principles-stats-wiring.test.ts +0 -135
  230. package/tests/commands/principles-stats.test.ts +0 -274
  231. package/tests/commands/product-path-regression.test.ts +0 -86
  232. package/tests/commands/proven-channel-baseline.test.ts +0 -441
  233. package/tests/commands/rulecode.test.ts +0 -177
  234. package/tests/commands/run-rulehost-handler.test.ts +0 -908
  235. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +0 -227
  236. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +0 -167
  237. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +0 -262
  238. package/tests/commands/runtime-activation-governance-audit.test.ts +0 -95
  239. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +0 -193
  240. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +0 -248
  241. package/tests/commands/runtime-activation-shadow-telemetry.test.ts +0 -130
  242. package/tests/commands/runtime-activation.test.ts +0 -2052
  243. package/tests/commands/runtime-artifact-repair-registration.test.ts +0 -74
  244. package/tests/commands/runtime-artifact-repair.test.ts +0 -404
  245. package/tests/commands/runtime-canary.test.ts +0 -430
  246. package/tests/commands/runtime-compatibility-scan.test.ts +0 -146
  247. package/tests/commands/runtime-diagnostics-export.test.ts +0 -175
  248. package/tests/commands/runtime-features.test.ts +0 -249
  249. package/tests/commands/runtime-health-snapshot.test.ts +0 -358
  250. package/tests/commands/runtime-init-empty-workspace.test.ts +0 -252
  251. package/tests/commands/runtime-init.test.ts +0 -420
  252. package/tests/commands/runtime-internalization-enqueue-successors.test.ts +0 -782
  253. package/tests/commands/runtime-internalization-integrity-repair.test.ts +0 -239
  254. package/tests/commands/runtime-internalization-integrity.test.ts +0 -140
  255. package/tests/commands/runtime-internalization-queue.test.ts +0 -380
  256. package/tests/commands/runtime-internalization-retry-owner-authority.test.ts +0 -431
  257. package/tests/commands/runtime-internalization-run-once-evaluator-parity.test.ts +0 -269
  258. package/tests/commands/runtime-internalization-run-once-language.test.ts +0 -303
  259. package/tests/commands/runtime-internalization-run-once-real-config.test.ts +0 -237
  260. package/tests/commands/runtime-internalization-run-once-rollout-parity.test.ts +0 -706
  261. package/tests/commands/runtime-internalization-run-once.test.ts +0 -1791
  262. package/tests/commands/runtime-internalization-wake-once.test.ts +0 -171
  263. package/tests/commands/runtime-internalization.test.ts +0 -53
  264. package/tests/commands/runtime-pain-flood-simulation.test.ts +0 -418
  265. package/tests/commands/runtime-pruning.test.ts +0 -694
  266. package/tests/commands/runtime-recovery-failed-tasks.test.ts +0 -328
  267. package/tests/commands/runtime-recovery.test.ts +0 -120
  268. package/tests/commands/runtime-synthetic-baseline.test.ts +0 -249
  269. package/tests/commands/runtime-uat.test.ts +0 -397
  270. package/tests/commands/runtime.test.ts +0 -392
  271. package/tests/commands/samples-list.test.ts +0 -43
  272. package/tests/commands/samples-review.test.ts +0 -41
  273. package/tests/commands/task.test.ts +0 -153
  274. package/tests/commands/telemetry.test.ts +0 -190
  275. package/tests/commands/trace.test.ts +0 -316
  276. package/tests/config-reader.test.ts +0 -143
  277. package/tests/e2e/candidate-intake-e2e.test.ts +0 -338
  278. package/tests/e2e/cli-full-flow.test.ts +0 -233
  279. package/tests/e2e/cross-package-acceptance.test.ts +0 -635
  280. package/tests/helpers/pd-cli-path.ts +0 -5
  281. package/tests/pd-cli-smoke.test.ts +0 -13
  282. package/tests/resolve-workspace.test.ts +0 -196
  283. package/tests/services/config-doctor-signal-health.test.ts +0 -109
  284. package/tests/services/console-launcher.test.ts +0 -114
  285. package/tests/services/demo-rule-compiler.test.ts +0 -242
  286. package/tests/services/mainline-snapshot-assembler.test.ts +0 -572
  287. package/tests/services/pd-config-loader.test.ts +0 -505
  288. package/tests/services/proven-channel-baseline-runner.test.ts +0 -30
  289. package/tests/services/quality-scorecard/strong-model-gate.test.ts +0 -133
  290. package/tests/services/resolve-runtime-from-pd-config.test.ts +0 -485
  291. package/tests/services/rulehost-pipeline-e2e.test.ts +0 -538
  292. package/tests/services/rulehost-pipeline-runner.test.ts +0 -1242
  293. package/tests/services/workspace-tool-semantics.test.ts +0 -76
  294. package/tests/utils/path-security.test.ts +0 -180
  295. package/tsconfig.json +0 -26
  296. 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
- });