principles-disciple 1.72.0 → 1.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/INSTALL.md +1 -3
  2. package/openclaw.plugin.json +10 -5
  3. package/package.json +17 -19
  4. package/scripts/acceptance-test.mjs +16 -73
  5. package/scripts/sync-plugin.mjs +382 -77
  6. package/src/commands/archive-impl.ts +2 -1
  7. package/src/commands/capabilities.ts +2 -2
  8. package/src/commands/context.ts +2 -2
  9. package/src/commands/disable-impl.ts +2 -1
  10. package/src/commands/evolution-status.ts +16 -16
  11. package/src/commands/export.ts +12 -67
  12. package/src/commands/pain.ts +91 -1
  13. package/src/commands/principle-rollback.ts +2 -1
  14. package/src/commands/promote-impl.ts +7 -43
  15. package/src/commands/rollback-impl.ts +2 -1
  16. package/src/commands/rollback.ts +2 -1
  17. package/src/commands/samples.ts +2 -1
  18. package/src/commands/thinking-os.ts +2 -1
  19. package/src/config/errors.ts +18 -2
  20. package/src/constants/diagnostician.ts +2 -2
  21. package/src/constants/tools.ts +2 -1
  22. package/src/core/__tests__/focus-history.test.ts +210 -0
  23. package/src/core/config.ts +1 -1
  24. package/src/core/correction-cue-learner.ts +2 -136
  25. package/src/core/correction-types.ts +16 -88
  26. package/src/core/dictionary.ts +19 -20
  27. package/src/core/empathy-keyword-matcher.ts +17 -289
  28. package/src/core/empathy-types.ts +18 -229
  29. package/src/core/event-log.ts +29 -132
  30. package/src/core/evolution-reducer.ts +21 -2
  31. package/src/core/evolution-types.ts +76 -464
  32. package/src/core/file-store.ts +80 -0
  33. package/src/core/focus-history.ts +228 -955
  34. package/src/core/local-worker-routing.ts +34 -314
  35. package/src/core/merge-gate-audit.ts +0 -195
  36. package/src/core/migration.ts +0 -1
  37. package/src/core/pain-diagnostic-gate.ts +154 -0
  38. package/src/core/pain-signal.ts +21 -138
  39. package/src/core/pain.ts +15 -88
  40. package/src/core/path-resolver.ts +0 -1
  41. package/src/core/paths.ts +0 -1
  42. package/src/core/pd-task-reconciler.ts +26 -115
  43. package/src/core/pd-task-service.ts +9 -9
  44. package/src/core/pd-task-types.ts +23 -127
  45. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  46. package/src/core/principle-compiler/code-validator.ts +15 -42
  47. package/src/core/principle-compiler/compiler.ts +100 -15
  48. package/src/core/principle-compiler/index.ts +5 -2
  49. package/src/core/principle-compiler/template-generator.ts +4 -104
  50. package/src/core/principle-injection.ts +10 -202
  51. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  52. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  53. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  54. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  55. package/src/core/principle-tree-ledger.ts +8 -6
  56. package/src/core/reflection/reflection-context.ts +14 -109
  57. package/src/core/replay-engine.ts +8 -500
  58. package/src/core/rule-host-helpers.ts +5 -35
  59. package/src/core/rule-host-types.ts +10 -82
  60. package/src/core/rule-host.ts +6 -63
  61. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  62. package/src/core/session-tracker.ts +87 -101
  63. package/src/core/shadow-observation-registry.ts +19 -48
  64. package/src/core/trajectory.ts +3 -1
  65. package/src/core/workflow-funnel-loader.ts +62 -68
  66. package/src/core/workspace-context.ts +46 -0
  67. package/src/core/workspace-dir-service.ts +1 -1
  68. package/src/core/workspace-dir-validation.ts +18 -9
  69. package/src/hooks/AGENTS.md +1 -1
  70. package/src/hooks/gate-block-helper.ts +71 -64
  71. package/src/hooks/gate.ts +183 -31
  72. package/src/hooks/lifecycle.ts +30 -32
  73. package/src/hooks/llm.ts +60 -32
  74. package/src/hooks/pain.ts +297 -103
  75. package/src/hooks/prompt.ts +400 -440
  76. package/src/hooks/subagent.ts +2 -29
  77. package/src/i18n/commands.ts +2 -10
  78. package/src/index.ts +95 -85
  79. package/src/openclaw-sdk.ts +311 -0
  80. package/src/service/central-database.ts +8 -4
  81. package/src/service/evolution-queue-migration.ts +2 -1
  82. package/src/service/evolution-worker.ts +163 -1786
  83. package/src/service/internalization-trigger-adapter.ts +302 -0
  84. package/src/service/keyword-optimization-service.ts +4 -4
  85. package/src/service/monitoring-query-service.ts +1 -215
  86. package/src/service/queue-io.ts +60 -331
  87. package/src/service/runtime-summary-service.ts +59 -16
  88. package/src/service/subagent-workflow/index.ts +0 -41
  89. package/src/service/subagent-workflow/types.ts +9 -120
  90. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  91. package/src/service/workflow-watchdog.ts +0 -43
  92. package/src/types/event-payload.ts +16 -74
  93. package/src/types/event-types.ts +38 -547
  94. package/src/types/hygiene-types.ts +7 -30
  95. package/src/types/principle-tree-schema.ts +20 -222
  96. package/src/types/queue.ts +15 -70
  97. package/src/types/runtime-summary.ts +5 -49
  98. package/src/utils/io.ts +8 -20
  99. package/src/utils/retry.ts +1 -1
  100. package/src/utils/shadow-fingerprint.ts +2 -2
  101. package/src/utils/workspace-resolver.ts +50 -0
  102. package/templates/langs/en/core/AGENTS.md +7 -7
  103. package/templates/langs/en/core/BOOT.md +1 -1
  104. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  105. package/templates/langs/en/principles/THINKING_OS.md +3 -2
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  107. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  108. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  109. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  110. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  111. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  112. package/templates/langs/en/skills/evolve-task/SKILL.md +3 -3
  113. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  114. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  115. package/templates/langs/en/skills/pd-mentor/SKILL.md +2 -3
  116. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  117. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  118. package/templates/langs/zh/core/AGENTS.md +7 -7
  119. package/templates/langs/zh/core/BOOT.md +1 -1
  120. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  121. package/templates/langs/zh/principles/THINKING_OS.md +3 -2
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  127. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  128. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  129. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  130. package/templates/langs/zh/skills/evolve-task/SKILL.md +4 -4
  131. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  132. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  133. package/templates/langs/zh/skills/pd-mentor/SKILL.md +2 -3
  134. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  135. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  136. package/tests/build-artifacts.test.ts +1 -3
  137. package/tests/commands/evolution-status.test.ts +0 -118
  138. package/tests/core/bootstrap-rules.test.ts +1 -1
  139. package/tests/core/config.test.ts +1 -1
  140. package/tests/core/event-log.test.ts +35 -0
  141. package/tests/core/evolution-engine.test.ts +610 -0
  142. package/tests/core/file-store.test.ts +102 -0
  143. package/tests/core/focus-history.test.ts +203 -11
  144. package/tests/core/merge-gate-audit.test.ts +2 -169
  145. package/tests/core/migration.test.ts +7 -7
  146. package/tests/core/model-deployment-registry.test.ts +7 -1
  147. package/tests/core/model-training-registry.test.ts +19 -0
  148. package/tests/core/observability.test.ts +0 -1
  149. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  150. package/tests/core/pain.test.ts +0 -1
  151. package/tests/core/path-resolver.test.ts +1 -1
  152. package/tests/core/paths-refactor.test.ts +0 -22
  153. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  154. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  155. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  156. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  157. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  158. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  159. package/tests/core/reflection-context.test.ts +0 -14
  160. package/tests/core/replay-engine.test.ts +127 -215
  161. package/tests/core/rule-host-helpers.test.ts +2 -2
  162. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  163. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  164. package/tests/core/workspace-context.test.ts +2 -2
  165. package/tests/core/workspace-dir-validation.test.ts +8 -1
  166. package/tests/core-anti-growth.test.ts +191 -0
  167. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  168. package/tests/hooks/confirm-first-removal.test.ts +188 -0
  169. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  170. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  171. package/tests/hooks/gate-no-path-write-tool.test.ts +172 -0
  172. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  173. package/tests/hooks/pain.test.ts +269 -12
  174. package/tests/hooks/prompt-characterization.test.ts +500 -0
  175. package/tests/hooks/prompt-size-guard.test.ts +32 -17
  176. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  177. package/tests/index.test.ts +94 -1
  178. package/tests/integration/auto-entry-gate.test.ts +248 -0
  179. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  180. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  181. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  182. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  183. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  184. package/tests/service/central-database.test.ts +457 -0
  185. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  186. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  187. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  188. package/tests/service/monitoring-query-service.test.ts +1 -47
  189. package/tests/service/queue-io.test.ts +1 -62
  190. package/tests/service/runtime-summary-service.test.ts +3 -1
  191. package/tests/service/workflow-watchdog.test.ts +0 -91
  192. package/tests/utils/file-lock.test.ts +5 -3
  193. package/tests/utils/session-key.test.ts +52 -0
  194. package/tests/utils/subagent-probe.test.ts +48 -1
  195. package/vitest.config.ts +4 -11
  196. package/.planning/codebase/ARCHITECTURE.md +0 -157
  197. package/.planning/codebase/CONCERNS.md +0 -145
  198. package/.planning/codebase/CONVENTIONS.md +0 -148
  199. package/.planning/codebase/INTEGRATIONS.md +0 -81
  200. package/.planning/codebase/STACK.md +0 -87
  201. package/.planning/codebase/STRUCTURE.md +0 -193
  202. package/.planning/codebase/TESTING.md +0 -243
  203. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  204. package/docs/COMMAND_REFERENCE.md +0 -76
  205. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  206. package/scripts/build-web.mjs +0 -46
  207. package/scripts/diagnose-nocturnal.mjs +0 -537
  208. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  209. package/src/commands/nocturnal-review.ts +0 -322
  210. package/src/commands/nocturnal-rollout.ts +0 -790
  211. package/src/commands/nocturnal-train.ts +0 -986
  212. package/src/commands/pd-reflect.ts +0 -88
  213. package/src/core/adaptive-thresholds.ts +0 -478
  214. package/src/core/diagnostician-task-store.ts +0 -192
  215. package/src/core/nocturnal-arbiter.ts +0 -715
  216. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  217. package/src/core/nocturnal-artificer.ts +0 -257
  218. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  219. package/src/core/nocturnal-compliance.ts +0 -1146
  220. package/src/core/nocturnal-dataset.ts +0 -763
  221. package/src/core/nocturnal-executability.ts +0 -428
  222. package/src/core/nocturnal-export.ts +0 -499
  223. package/src/core/nocturnal-paths.ts +0 -240
  224. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  225. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  226. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  227. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  228. package/src/core/nocturnal-trinity-types.ts +0 -218
  229. package/src/core/nocturnal-trinity.ts +0 -2680
  230. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  231. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  232. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  233. package/src/http/principles-console-route.ts +0 -709
  234. package/src/service/central-health-service.ts +0 -49
  235. package/src/service/central-overview-service.ts +0 -138
  236. package/src/service/control-ui-query-service.ts +0 -900
  237. package/src/service/cooldown-strategy.ts +0 -97
  238. package/src/service/evolution-pain-context.ts +0 -79
  239. package/src/service/evolution-query-service.ts +0 -407
  240. package/src/service/health-query-service.ts +0 -1038
  241. package/src/service/nocturnal-config.ts +0 -214
  242. package/src/service/nocturnal-runtime.ts +0 -734
  243. package/src/service/nocturnal-service.ts +0 -1605
  244. package/src/service/nocturnal-target-selector.ts +0 -545
  245. package/src/service/sleep-cycle.ts +0 -157
  246. package/src/service/startup-reconciler.ts +0 -112
  247. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  248. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  249. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  250. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  251. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  252. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  253. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  254. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  255. package/src/tools/write-pain-flag.ts +0 -215
  256. package/templates/langs/en/skills/plan-script/SKILL.md +0 -32
  257. package/templates/langs/zh/skills/plan-script/SKILL.md +0 -32
  258. package/tests/commands/nocturnal-review.test.ts +0 -448
  259. package/tests/commands/nocturnal-train.test.ts +0 -97
  260. package/tests/commands/pd-reflect.test.ts +0 -49
  261. package/tests/core/adaptive-thresholds.test.ts +0 -261
  262. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  263. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  264. package/tests/core/nocturnal-artificer.test.ts +0 -241
  265. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  266. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  267. package/tests/core/nocturnal-compliance.test.ts +0 -646
  268. package/tests/core/nocturnal-dataset.test.ts +0 -892
  269. package/tests/core/nocturnal-e2e.test.ts +0 -234
  270. package/tests/core/nocturnal-executability.test.ts +0 -357
  271. package/tests/core/nocturnal-export.test.ts +0 -517
  272. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  273. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  274. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  275. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  276. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  277. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  278. package/tests/core/pain-auto-repair.test.ts +0 -96
  279. package/tests/core/pain-integration.test.ts +0 -510
  280. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  281. package/tests/http/principles-console-route.test.ts +0 -162
  282. package/tests/integration/chaos-resilience.test.ts +0 -348
  283. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  284. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  285. package/tests/service/control-ui-query-service.test.ts +0 -121
  286. package/tests/service/cooldown-strategy.test.ts +0 -164
  287. package/tests/service/data-endpoints-regression.test.ts +0 -834
  288. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  289. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  290. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  291. package/tests/service/nocturnal-runtime.test.ts +0 -473
  292. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  293. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  294. package/tests/service/startup-reconciler.test.ts +0 -148
  295. package/tests/tools/write-pain-flag.test.ts +0 -358
  296. package/ui/src/App.tsx +0 -45
  297. package/ui/src/api.ts +0 -220
  298. package/ui/src/charts.tsx +0 -955
  299. package/ui/src/components/ErrorState.tsx +0 -6
  300. package/ui/src/components/Loading.tsx +0 -13
  301. package/ui/src/components/ProtectedRoute.tsx +0 -12
  302. package/ui/src/components/Shell.tsx +0 -91
  303. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  304. package/ui/src/components/index.ts +0 -5
  305. package/ui/src/context/auth.tsx +0 -80
  306. package/ui/src/context/theme.tsx +0 -66
  307. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  308. package/ui/src/i18n/ui.ts +0 -473
  309. package/ui/src/main.tsx +0 -16
  310. package/ui/src/pages/EvolutionPage.tsx +0 -333
  311. package/ui/src/pages/FeedbackPage.tsx +0 -138
  312. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  313. package/ui/src/pages/LoginPage.tsx +0 -89
  314. package/ui/src/pages/OverviewPage.tsx +0 -599
  315. package/ui/src/pages/SamplesPage.tsx +0 -174
  316. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  317. package/ui/src/styles.css +0 -2020
  318. package/ui/src/types.ts +0 -384
  319. package/ui/src/utils/format.ts +0 -15
@@ -0,0 +1,869 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ import { SqliteConnection, SqliteActivationStateStore } from '@principles/core/runtime-v2';
6
+ import type { ActivationStatusRecord } from '@principles/core/runtime-v2';
7
+ import { PromptActivationReader, RUNTIME_V2_PRINCIPLE_BUDGET } from '../../src/core/runtime-v2-prompt-activation-reader.js';
8
+
9
+ const TEST_PRINCIPLE_TEXT = 'UNIQUE_RUNTIME_V2_TEST_PRINCIPLE_7x9k2';
10
+
11
+ let tempWorkspaceDir: string;
12
+ let tempStateDir: string;
13
+ let sqliteConn: SqliteConnection;
14
+
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ process.env.PD_LEGACY_PROMPT_DIAGNOSTICIAN_ENABLED = 'true';
18
+
19
+ const baseTmp = os.tmpdir();
20
+ tempWorkspaceDir = fs.mkdtempSync(path.join(baseTmp, 'pd-prompt-v2-'));
21
+ tempStateDir = path.join(tempWorkspaceDir, '.principles');
22
+ fs.mkdirSync(tempStateDir, { recursive: true });
23
+
24
+ const pdDir = path.join(tempWorkspaceDir, '.pd');
25
+ fs.mkdirSync(pdDir, { recursive: true });
26
+
27
+ sqliteConn = new SqliteConnection(tempWorkspaceDir);
28
+ sqliteConn.getDb();
29
+ });
30
+
31
+ afterEach(() => {
32
+ try {
33
+ sqliteConn?.close();
34
+ } catch {
35
+ // best-effort
36
+ }
37
+ try {
38
+ fs.rmSync(tempWorkspaceDir, { recursive: true, force: true });
39
+ } catch {
40
+ // best-effort on Windows
41
+ }
42
+ process.env.PD_LEGACY_PROMPT_DIAGNOSTICIAN_ENABLED = '';
43
+ });
44
+
45
+ vi.mock('../../src/core/diagnostician-task-store.js', async () => ({
46
+ getPendingDiagnosticianTasks: vi.fn().mockReturnValue([]),
47
+ }));
48
+
49
+ vi.mock('../../src/core/event-log.js', () => ({
50
+ EventLogService: {
51
+ get: vi.fn().mockReturnValue({
52
+ recordHeartbeatDiagnosis: vi.fn(),
53
+ recordRuntimeV2ActivationsInjected: vi.fn(),
54
+ }),
55
+ },
56
+ }));
57
+
58
+ vi.mock('../../src/core/workspace-context.js', async () => {
59
+ const { EventLogService } = await import('../../src/core/event-log.js');
60
+ const mockEventLog = EventLogService.get('/mock');
61
+ return {
62
+ WorkspaceContext: {
63
+ fromHookContext: vi.fn().mockImplementation(() => ({
64
+ workspaceDir: tempWorkspaceDir,
65
+ stateDir: tempStateDir,
66
+ resolve: (key: string) => path.join(tempWorkspaceDir, '.principles', key),
67
+ trajectory: { recordSession: vi.fn(), recordUserTurn: vi.fn() },
68
+ config: { get: vi.fn() },
69
+ eventLog: mockEventLog,
70
+ evolutionReducer: {
71
+ getActivePrinciples: vi.fn().mockReturnValue([]),
72
+ getProbationPrinciples: vi.fn().mockReturnValue([]),
73
+ },
74
+ })),
75
+ fromHookContextExplicit: vi.fn().mockImplementation(() => ({
76
+ workspaceDir: tempWorkspaceDir,
77
+ stateDir: tempStateDir,
78
+ resolve: (key: string) => path.join(tempWorkspaceDir, '.principles', key),
79
+ trajectory: { recordSession: vi.fn(), recordUserTurn: vi.fn() },
80
+ config: { get: vi.fn() },
81
+ eventLog: mockEventLog,
82
+ evolutionReducer: {
83
+ getActivePrinciples: vi.fn().mockReturnValue([]),
84
+ getProbationPrinciples: vi.fn().mockReturnValue([]),
85
+ },
86
+ })),
87
+ },
88
+ };
89
+ });
90
+
91
+ vi.mock('../../src/core/session-tracker.js', () => ({
92
+ getSession: vi.fn().mockReturnValue({ currentGfi: 20 }),
93
+ resetFriction: vi.fn(),
94
+ trackFriction: vi.fn(),
95
+ setInjectedProbationIds: vi.fn(),
96
+ clearInjectedProbationIds: vi.fn(),
97
+ decayGfi: vi.fn(),
98
+ getGfiDecayElapsed: vi.fn().mockReturnValue(0),
99
+ }));
100
+
101
+ vi.mock('../../src/core/path-resolver.js', () => ({
102
+ PathResolver: { getExtensionRoot: vi.fn().mockReturnValue('/fake/extension') },
103
+ }));
104
+
105
+ vi.mock('../../src/core/principle-injection.js', () => ({
106
+ selectPrinciplesForInjection: vi.fn().mockReturnValue({
107
+ selected: [],
108
+ wasTruncated: false,
109
+ breakdown: { p0: 0, p1: 0, p2: 0 },
110
+ totalChars: 0,
111
+ }),
112
+ DEFAULT_PRINCIPLE_BUDGET: 3000,
113
+ }));
114
+
115
+ vi.mock('../../src/core/empathy-keyword-matcher.js', () => ({
116
+ matchEmpathyKeywords: vi.fn().mockReturnValue({ score: 0, matched: null, severity: 'none', matchedTerms: [] }),
117
+ loadKeywordStore: vi.fn().mockReturnValue({ terms: {}, stats: { totalHits: 0 } }),
118
+ saveKeywordStore: vi.fn(),
119
+ shouldTriggerOptimization: vi.fn().mockReturnValue(false),
120
+ getKeywordStoreSummary: vi.fn().mockReturnValue({ totalTerms: 0, highFalsePositiveTerms: [] }),
121
+ }));
122
+
123
+ vi.mock('../../src/core/empathy-types.js', () => ({
124
+ severityToPenalty: vi.fn().mockReturnValue(5),
125
+ DEFAULT_EMPATHY_KEYWORD_CONFIG: {},
126
+ }));
127
+
128
+ vi.mock('../../src/core/correction-cue-learner.js', () => ({
129
+ CorrectionCueLearner: {
130
+ get: vi.fn().mockReturnValue({
131
+ match: vi.fn().mockReturnValue({ matched: null, matchedTerms: [], confidence: 0 }),
132
+ recordHits: vi.fn(),
133
+ recordTruePositive: vi.fn(),
134
+ flush: vi.fn(),
135
+ }),
136
+ },
137
+ }));
138
+
139
+ vi.mock('../../src/core/focus-history.js', () => ({
140
+ extractSummary: vi.fn().mockReturnValue(''),
141
+ getHistoryVersions: vi.fn().mockResolvedValue([]),
142
+ parseWorkingMemorySection: vi.fn().mockReturnValue(null),
143
+ workingMemoryToInjection: vi.fn().mockReturnValue(''),
144
+ autoCompressFocus: vi.fn().mockReturnValue({ compressed: false, reason: 'not_needed' }),
145
+ safeReadCurrentFocus: vi.fn().mockReturnValue({ content: '', recovered: false, validationErrors: [] }),
146
+ }));
147
+
148
+ vi.mock('../../src/service/subagent-workflow/index.js', () => ({
149
+ EmpathyObserverWorkflowManager: vi.fn(),
150
+ empathyObserverWorkflowSpec: {},
151
+ isExpectedSubagentError: vi.fn().mockReturnValue(false),
152
+ }));
153
+
154
+ vi.mock('../../src/utils/subagent-probe.js', () => ({
155
+ isSubagentRuntimeAvailable: vi.fn().mockReturnValue(false),
156
+ }));
157
+
158
+ vi.mock('../../src/core/local-worker-routing.js', () => ({
159
+ classifyTask: vi.fn().mockReturnValue({
160
+ decision: 'stay_main',
161
+ classification: 'unknown',
162
+ reason: 'mocked',
163
+ blockers: [],
164
+ }),
165
+ }));
166
+
167
+ function makeMinimalEvent(overrides: {
168
+ trigger?: string;
169
+ sessionId?: string;
170
+ } = {}) {
171
+ const { trigger = 'user', sessionId = 'test-session-v2' } = overrides;
172
+ return {
173
+ prompt: 'hello world',
174
+ messages: [],
175
+ trigger,
176
+ sessionId,
177
+ } as unknown as Parameters<typeof import('../../src/hooks/prompt.js').handleBeforePromptBuild>[0];
178
+ }
179
+
180
+ function makeCtx(overrides: {
181
+ workspaceDir?: string;
182
+ trigger?: string;
183
+ sessionId?: string;
184
+ } = {}) {
185
+ const {
186
+ workspaceDir = tempWorkspaceDir,
187
+ trigger = 'user',
188
+ sessionId = 'test-session-v2',
189
+ } = overrides;
190
+ return {
191
+ workspaceDir,
192
+ trigger,
193
+ sessionId,
194
+ api: {
195
+ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
196
+ runtime: {},
197
+ config: {},
198
+ },
199
+ } as unknown as Parameters<typeof import('../../src/hooks/prompt.js').handleBeforePromptBuild>[1];
200
+ }
201
+
202
+ async function insertPromptActivation(overrides: {
203
+ artifactId: string;
204
+ principleId: string;
205
+ channel?: string;
206
+ action?: string;
207
+ targetRef?: string;
208
+ }) {
209
+ const {
210
+ artifactId,
211
+ principleId,
212
+ channel = 'prompt',
213
+ action = 'prompt_activate',
214
+ targetRef = `ledger://${principleId}`,
215
+ } = overrides;
216
+
217
+ const activationStore = new SqliteActivationStateStore(sqliteConn);
218
+ const now = new Date().toISOString();
219
+ const idempotencyKey = `${artifactId}::${channel}`;
220
+
221
+ await activationStore.recordActivation({
222
+ activationId: `act_prompt_${principleId}`,
223
+ idempotencyKey,
224
+ artifactId,
225
+ channel: channel as ActivationStatusRecord['channel'],
226
+ action,
227
+ targetRef,
228
+ activatedAt: now,
229
+ });
230
+ }
231
+
232
+ function insertValidatedPrincipleArtifact(overrides: {
233
+ artifactId: string;
234
+ principleId: string;
235
+ text?: string;
236
+ validationStatus?: string;
237
+ contentJson?: string;
238
+ }) {
239
+ const {
240
+ artifactId,
241
+ principleId,
242
+ text = TEST_PRINCIPLE_TEXT,
243
+ validationStatus = 'validated',
244
+ contentJson,
245
+ } = overrides;
246
+
247
+ const db = sqliteConn.getDb();
248
+ const now = new Date().toISOString();
249
+
250
+ db.prepare(`
251
+ INSERT INTO pi_artifacts (artifact_id, artifact_kind, source_task_id, source_principle_id, source_rule_id, lineage_artifact_ids, validation_status, content_json, created_at, updated_at)
252
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
253
+ `).run(
254
+ artifactId,
255
+ 'principle',
256
+ `task_${principleId}`,
257
+ principleId,
258
+ null,
259
+ '[]',
260
+ validationStatus,
261
+ contentJson ?? JSON.stringify({ principleId, text }),
262
+ now,
263
+ now,
264
+ );
265
+ }
266
+
267
+ describe('Runtime V2 prompt activation injection', () => {
268
+ it('owner-approved activated principle changes future prompt', async () => {
269
+ const artifactId = 'art-v2-prompt-001';
270
+ const principleId = 'princ-v2-001';
271
+
272
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
273
+ await insertPromptActivation({ artifactId, principleId });
274
+
275
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
276
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
277
+
278
+ // Runtime V2 principles are now in prependSystemContext (highest attention)
279
+ expect(result?.prependSystemContext).toContain(TEST_PRINCIPLE_TEXT);
280
+ });
281
+
282
+ it('unactivated principle is not injected', async () => {
283
+ const artifactId = 'art-v2-no-act-002';
284
+ const principleId = 'princ-v2-no-act-002';
285
+
286
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
287
+
288
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
289
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
290
+
291
+ expect(result?.appendSystemContext).not.toContain(TEST_PRINCIPLE_TEXT);
292
+ });
293
+
294
+ it('non-prompt activation is not injected', async () => {
295
+ const artifactId = 'art-v2-defer-003';
296
+ const principleId = 'princ-v2-defer-003';
297
+
298
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
299
+ await insertPromptActivation({
300
+ artifactId,
301
+ principleId,
302
+ channel: 'defer_archive',
303
+ action: 'defer_archive',
304
+ targetRef: `ledger://${principleId}#archived`,
305
+ });
306
+
307
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
308
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
309
+
310
+ expect(result?.appendSystemContext).not.toContain(TEST_PRINCIPLE_TEXT);
311
+ });
312
+
313
+ it('prompt feature flag check is present — core flag cannot be disabled by config', async () => {
314
+ const artifactId = 'art-v2-flag-004';
315
+ const principleId = 'princ-v2-flag-004';
316
+
317
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
318
+ await insertPromptActivation({ artifactId, principleId });
319
+
320
+ const pdDir = path.join(tempWorkspaceDir, '.pd');
321
+ if (!fs.existsSync(pdDir)) {
322
+ fs.mkdirSync(pdDir, { recursive: true });
323
+ }
324
+ fs.writeFileSync(
325
+ path.join(pdDir, 'feature-flags.yaml'),
326
+ 'prompt:\n enabled: false\n',
327
+ 'utf8',
328
+ );
329
+
330
+ const infoSpy = vi.fn();
331
+ const ctx = {
332
+ workspaceDir: tempWorkspaceDir,
333
+ trigger: 'user',
334
+ sessionId: 'test-session-v2',
335
+ api: {
336
+ logger: { info: infoSpy, warn: vi.fn(), error: vi.fn() },
337
+ runtime: {},
338
+ config: {},
339
+ },
340
+ } as unknown as Parameters<typeof import('../../src/hooks/prompt.js').handleBeforePromptBuild>[1];
341
+
342
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
343
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), ctx);
344
+
345
+ expect(result?.prependSystemContext).toContain(TEST_PRINCIPLE_TEXT);
346
+
347
+ const infoCalls = infoSpy.mock.calls.map((c: unknown[]) => String(c[0]));
348
+ const hasCoreFlagWarning = infoCalls.some(
349
+ (c: string) => c.includes('core flag cannot be disabled') || c.includes('warnings'),
350
+ );
351
+ expect(hasCoreFlagWarning || result?.prependSystemContext).toBeTruthy();
352
+ });
353
+
354
+ it('missing activated artifact fails loud without crashing', async () => {
355
+ const artifactId = 'art-v2-missing-005';
356
+ const principleId = 'princ-v2-missing-005';
357
+
358
+ await insertPromptActivation({ artifactId, principleId });
359
+
360
+ const warnSpy = vi.fn();
361
+ const ctx = {
362
+ workspaceDir: tempWorkspaceDir,
363
+ trigger: 'user',
364
+ sessionId: 'test-session-v2',
365
+ api: {
366
+ logger: { info: vi.fn(), warn: warnSpy, error: vi.fn() },
367
+ runtime: {},
368
+ config: {},
369
+ },
370
+ } as unknown as Parameters<typeof import('../../src/hooks/prompt.js').handleBeforePromptBuild>[1];
371
+
372
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
373
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), ctx);
374
+
375
+ expect(result).toBeDefined();
376
+ expect(result?.appendSystemContext).not.toContain(TEST_PRINCIPLE_TEXT);
377
+ const warnCalls = warnSpy.mock.calls.map((c: unknown[]) => String(c[0]));
378
+ const hasActivationWarning = warnCalls.some((c: string) => c.includes('artifact_not_found') || c.includes('artifact_query_unexpected') || c.includes('activation'));
379
+ expect(hasActivationWarning).toBe(true);
380
+ });
381
+
382
+ it('malformed content_json in artifact fails loud without crashing', async () => {
383
+ const artifactId = 'art-v2-malformed-006';
384
+ const principleId = 'princ-v2-malformed-006';
385
+
386
+ insertValidatedPrincipleArtifact({
387
+ artifactId,
388
+ principleId,
389
+ contentJson: '{not valid json<<<',
390
+ });
391
+ await insertPromptActivation({ artifactId, principleId });
392
+
393
+ const warnSpy = vi.fn();
394
+ const ctx = {
395
+ workspaceDir: tempWorkspaceDir,
396
+ trigger: 'user',
397
+ sessionId: 'test-session-v2',
398
+ api: {
399
+ logger: { info: vi.fn(), warn: warnSpy, error: vi.fn() },
400
+ runtime: {},
401
+ config: {},
402
+ },
403
+ } as unknown as Parameters<typeof import('../../src/hooks/prompt.js').handleBeforePromptBuild>[1];
404
+
405
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
406
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), ctx);
407
+
408
+ expect(result).toBeDefined();
409
+ expect(result?.appendSystemContext).not.toContain(TEST_PRINCIPLE_TEXT);
410
+ const warnCalls = warnSpy.mock.calls.map((c: unknown[]) => String(c[0]));
411
+ const hasParseWarning = warnCalls.some((c: string) => c.includes('json_parse_error') || c.includes('activation'));
412
+ expect(hasParseWarning).toBe(true);
413
+ });
414
+
415
+ it('no legacy promotion is required for Runtime V2 injection', async () => {
416
+ const artifactId = 'art-v2-no-legacy-007';
417
+ const principleId = 'princ-v2-no-legacy-007';
418
+
419
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
420
+ await insertPromptActivation({ artifactId, principleId });
421
+
422
+ const { WorkspaceContext } = await import('../../src/core/workspace-context.js');
423
+ const mockWctx = (WorkspaceContext.fromHookContext as ReturnType<typeof vi.fn>).mock.results[0]?.value;
424
+ const promoteSpy = vi.fn();
425
+
426
+ if (mockWctx?.evolutionReducer) {
427
+ mockWctx.evolutionReducer.promote = promoteSpy;
428
+ }
429
+
430
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
431
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
432
+
433
+ expect(result?.prependSystemContext).toContain(TEST_PRINCIPLE_TEXT);
434
+ expect(promoteSpy).not.toHaveBeenCalled();
435
+ });
436
+ });
437
+
438
+ describe('Runtime V2 prompt activation — additional guard tests', () => {
439
+ it('rejected/pending artifact is not injected', async () => {
440
+ const artifactId = 'art-v2-rejected-101';
441
+ const principleId = 'princ-v2-rejected-101';
442
+
443
+ insertValidatedPrincipleArtifact({ artifactId, principleId, validationStatus: 'rejected' });
444
+ await insertPromptActivation({ artifactId, principleId });
445
+
446
+ const reader = new PromptActivationReader(tempWorkspaceDir);
447
+ const result = await reader.readActivatedPrinciples();
448
+
449
+ expect(result.principles).toHaveLength(0);
450
+ expect(result.warnings.some((w) => w.includes('artifact_not_validated'))).toBe(true);
451
+ });
452
+
453
+ it('prompt channel with wrong action is not injected', async () => {
454
+ const artifactId = 'art-v2-wrong-action-102';
455
+ const principleId = 'princ-v2-wrong-action-102';
456
+
457
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
458
+ await insertPromptActivation({
459
+ artifactId,
460
+ principleId,
461
+ channel: 'prompt',
462
+ action: 'prompt_deactivate',
463
+ });
464
+
465
+ const reader = new PromptActivationReader(tempWorkspaceDir);
466
+ const result = await reader.readActivatedPrinciples();
467
+
468
+ expect(result.principles).toHaveLength(0);
469
+ });
470
+
471
+ it('multiple or oversized Runtime V2 principles are trimmed to budget', async () => {
472
+ const longText = 'A'.repeat(800);
473
+ for (let i = 0; i < 5; i++) {
474
+ const artifactId = `art-v2-budget-${i}`;
475
+ const principleId = `princ-v2-budget-${i}`;
476
+ insertValidatedPrincipleArtifact({ artifactId, principleId, text: longText });
477
+ await insertPromptActivation({ artifactId, principleId });
478
+ }
479
+
480
+ const reader = new PromptActivationReader(tempWorkspaceDir);
481
+ const result = await reader.readActivatedPrinciples();
482
+
483
+ expect(result.principles.length).toBe(5);
484
+
485
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
486
+ const hookResult = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
487
+
488
+ // Runtime V2 principles are now in prependSystemContext
489
+ const injected = hookResult?.prependSystemContext ?? '';
490
+ const markerCount = (injected.match(/princ-v2-budget-/g) || []).length;
491
+ expect(markerCount).toBeLessThan(5);
492
+ expect(markerCount).toBeGreaterThan(0);
493
+ });
494
+
495
+ it('malformed DB/config input fails loud with warning', async () => {
496
+ const pdDir = path.join(tempWorkspaceDir, '.pd');
497
+ fs.writeFileSync(
498
+ path.join(pdDir, 'feature-flags.yaml'),
499
+ '__proto__:\n enabled: true\nprompt:\n enabled: true\nconstructor:\n enabled: false\n',
500
+ 'utf8',
501
+ );
502
+
503
+ const warnSpy = vi.fn();
504
+ const reader = new PromptActivationReader(tempWorkspaceDir, {
505
+ logger: { warn: warnSpy, info: vi.fn(), error: vi.fn() },
506
+ });
507
+ const result = await reader.readActivatedPrinciples();
508
+
509
+ const warnCalls = warnSpy.mock.calls.map((c: unknown[]) => String(c[0]));
510
+ const hasDangerousKeyWarning = warnCalls.some(
511
+ (c: string) => c.includes('dangerous key') || c.includes('__proto__') || c.includes('constructor'),
512
+ );
513
+ expect(hasDangerousKeyWarning).toBe(true);
514
+ expect(result.principles).toEqual([]);
515
+ });
516
+
517
+ it('reader uses normalized workspaceDir correctly', async () => {
518
+ const artifactId = 'art-v2-norm-105';
519
+ const principleId = 'princ-v2-norm-105';
520
+
521
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
522
+ await insertPromptActivation({ artifactId, principleId });
523
+
524
+ const reader = new PromptActivationReader(tempWorkspaceDir);
525
+ const result = await reader.readActivatedPrinciples();
526
+
527
+ expect(result.principles).toHaveLength(1);
528
+ expect(result.principles[0].principleId).toBe(principleId);
529
+ });
530
+
531
+ it('pending validation status artifact is not injected', async () => {
532
+ const artifactId = 'art-v2-pending-106';
533
+ const principleId = 'princ-v2-pending-106';
534
+
535
+ insertValidatedPrincipleArtifact({ artifactId, principleId, validationStatus: 'pending' });
536
+ await insertPromptActivation({ artifactId, principleId });
537
+
538
+ const reader = new PromptActivationReader(tempWorkspaceDir);
539
+ const result = await reader.readActivatedPrinciples();
540
+
541
+ expect(result.principles).toHaveLength(0);
542
+ expect(result.warnings.some((w) => w.includes('artifact_not_validated'))).toBe(true);
543
+ });
544
+
545
+ it('malformed activation row with empty artifact_id is rejected', async () => {
546
+ const db = sqliteConn.getDb();
547
+ const now = new Date().toISOString();
548
+ db.prepare(`
549
+ INSERT INTO activations (activation_id, idempotency_key, artifact_id, channel, action, target_ref, activated_at)
550
+ VALUES (?, ?, ?, ?, ?, ?, ?)
551
+ `).run('', 'idem-empty-artifact', '', 'prompt', 'prompt_activate', '', now);
552
+
553
+ const store = new SqliteActivationStateStore(sqliteConn);
554
+ const activations = await store.listPromptActivations();
555
+ const emptyArtifact = activations.find((a) => a.idempotencyKey === 'idem-empty-artifact');
556
+ expect(emptyArtifact).toBeUndefined();
557
+ });
558
+
559
+ it('malformed activation row with empty activation_id is rejected', async () => {
560
+ const db = sqliteConn.getDb();
561
+ const now = new Date().toISOString();
562
+ db.prepare(`
563
+ INSERT INTO activations (activation_id, idempotency_key, artifact_id, channel, action, target_ref, activated_at)
564
+ VALUES (?, ?, ?, ?, ?, ?, ?)
565
+ `).run('', 'idem-empty-actid', 'some-artifact', 'prompt', 'prompt_activate', '', now);
566
+
567
+ const store = new SqliteActivationStateStore(sqliteConn);
568
+ const activations = await store.listPromptActivations();
569
+ const emptyActId = activations.find((a) => a.idempotencyKey === 'idem-empty-actid');
570
+ expect(emptyActId).toBeUndefined();
571
+ });
572
+
573
+ it('malformed activation row with empty action is rejected', async () => {
574
+ const db = sqliteConn.getDb();
575
+ const now = new Date().toISOString();
576
+ db.prepare(`
577
+ INSERT INTO activations (activation_id, idempotency_key, artifact_id, channel, action, target_ref, activated_at)
578
+ VALUES (?, ?, ?, ?, ?, ?, ?)
579
+ `).run('act-malformed-action', 'idem-empty-action', 'some-artifact', 'prompt', '', '', now);
580
+
581
+ const store = new SqliteActivationStateStore(sqliteConn);
582
+ const activations = await store.listPromptActivations();
583
+ const emptyAction = activations.find((a) => a.idempotencyKey === 'idem-empty-action');
584
+ expect(emptyAction).toBeUndefined();
585
+ });
586
+
587
+ it('malformed activation row with empty activated_at is rejected', async () => {
588
+ const db = sqliteConn.getDb();
589
+ db.prepare(`
590
+ INSERT INTO activations (activation_id, idempotency_key, artifact_id, channel, action, target_ref, activated_at)
591
+ VALUES (?, ?, ?, ?, ?, ?, ?)
592
+ `).run('act-malformed-at', 'idem-empty-at', 'some-artifact', 'prompt', 'prompt_activate', '', '');
593
+
594
+ const store = new SqliteActivationStateStore(sqliteConn);
595
+ const activations = await store.listPromptActivations();
596
+ const emptyAt = activations.find((a) => a.idempotencyKey === 'idem-empty-at');
597
+ expect(emptyAt).toBeUndefined();
598
+ });
599
+
600
+ it('malformed activation row with invalid channel is rejected', async () => {
601
+ const db = sqliteConn.getDb();
602
+ const now = new Date().toISOString();
603
+ db.prepare(`
604
+ INSERT INTO activations (activation_id, idempotency_key, artifact_id, channel, action, target_ref, activated_at)
605
+ VALUES (?, ?, ?, ?, ?, ?, ?)
606
+ `).run('act-bad-channel', 'idem-bad-channel', 'some-artifact', 'invalid_channel', 'prompt_activate', '', now);
607
+
608
+ const store = new SqliteActivationStateStore(sqliteConn);
609
+ const activations = await store.listPromptActivations();
610
+ const badChannel = activations.find((a) => a.idempotencyKey === 'idem-bad-channel');
611
+ expect(badChannel).toBeUndefined();
612
+ });
613
+ });
614
+
615
+ describe('Runtime V2 prompt activation observability events', () => {
616
+ it('emits injected event with principleIds when valid activations exist', async () => {
617
+ const artifactId = 'art-v2-obs-001';
618
+ const principleId = 'princ-v2-obs-001';
619
+
620
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
621
+ await insertPromptActivation({ artifactId, principleId });
622
+
623
+ const { EventLogService } = await import('../../src/core/event-log.js');
624
+ const mockEventLog = (EventLogService.get as ReturnType<typeof vi.fn>)();
625
+ const spy = mockEventLog.recordRuntimeV2ActivationsInjected as ReturnType<typeof vi.fn>;
626
+
627
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
628
+ await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
629
+
630
+ expect(spy).toHaveBeenCalled();
631
+ const payload = spy.mock.calls[0][0];
632
+ expect(payload.principleIds).toContain(principleId);
633
+ expect(payload.artifactIds).toContain(artifactId);
634
+ expect(payload.activationIds).toContain(`act_prompt_${principleId}`);
635
+ expect(payload.injectedCount).toBe(1);
636
+ expect(payload.injectedCharCount).toBeGreaterThan(0);
637
+ expect(payload.budget).toBe(RUNTIME_V2_PRINCIPLE_BUDGET);
638
+ expect(payload.sessionId).toBe('test-session-v2');
639
+ expect(payload.workspaceDir).toBe(tempWorkspaceDir);
640
+ expect(payload.skippedWarnings).toEqual([]);
641
+ });
642
+
643
+ it('emits skipReason when no validated activations exist', async () => {
644
+ const { EventLogService } = await import('../../src/core/event-log.js');
645
+ const mockEventLog = (EventLogService.get as ReturnType<typeof vi.fn>)();
646
+ const spy = mockEventLog.recordRuntimeV2ActivationsInjected as ReturnType<typeof vi.fn>;
647
+ spy.mockClear();
648
+
649
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
650
+ await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
651
+
652
+ expect(spy).toHaveBeenCalled();
653
+ const payload = spy.mock.calls[0][0];
654
+ expect(payload.injectedCount).toBe(0);
655
+ expect(payload.principleIds).toEqual([]);
656
+ expect(payload.skipReason).toBe('no_validated_activations');
657
+ expect(payload.nextAction).toContain('activations table');
658
+ });
659
+
660
+ it('confirm-first marker appears in principleIds evidence', async () => {
661
+ const artifactId = 'art-mvp-acceptance-001';
662
+ const principleId = 'princ-mvp-acceptance-confirm-first';
663
+ const text = 'Before starting any coding task, the agent must first confirm requirements and present a plan for owner approval.';
664
+
665
+ insertValidatedPrincipleArtifact({ artifactId, principleId, text });
666
+ await insertPromptActivation({ artifactId, principleId });
667
+
668
+ const { EventLogService } = await import('../../src/core/event-log.js');
669
+ const mockEventLog = (EventLogService.get as ReturnType<typeof vi.fn>)();
670
+ const spy = mockEventLog.recordRuntimeV2ActivationsInjected as ReturnType<typeof vi.fn>;
671
+ spy.mockClear();
672
+
673
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
674
+ await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
675
+
676
+ expect(spy).toHaveBeenCalled();
677
+ const payload = spy.mock.calls[0][0];
678
+ expect(payload.principleIds).toContain('princ-mvp-acceptance-confirm-first');
679
+ expect(payload.injectedCount).toBeGreaterThanOrEqual(1);
680
+ });
681
+
682
+ it('warnings are preserved in skippedWarnings', async () => {
683
+ const artifactId = 'art-v2-obs-warn-003';
684
+ const principleId = 'princ-v2-obs-warn-003';
685
+
686
+ insertValidatedPrincipleArtifact({ artifactId, principleId, validationStatus: 'rejected' });
687
+ await insertPromptActivation({ artifactId, principleId });
688
+
689
+ const { EventLogService } = await import('../../src/core/event-log.js');
690
+ const mockEventLog = (EventLogService.get as ReturnType<typeof vi.fn>)();
691
+ const spy = mockEventLog.recordRuntimeV2ActivationsInjected as ReturnType<typeof vi.fn>;
692
+ spy.mockClear();
693
+
694
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
695
+ await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
696
+
697
+ expect(spy).toHaveBeenCalled();
698
+ const payload = spy.mock.calls[0][0];
699
+ expect(payload.skippedWarnings.length).toBeGreaterThan(0);
700
+ expect(payload.skippedWarnings.some((w: string) => w.includes('artifact_not_validated'))).toBe(true);
701
+ expect(payload.injectedCount).toBe(0);
702
+ });
703
+
704
+ it('no raw secrets or full giant prompt in telemetry payload', async () => {
705
+ const artifactId = 'art-v2-obs-safe-004';
706
+ const principleId = 'princ-v2-obs-safe-004';
707
+ const secretText = 'sk-proj-SECRET_KEY_12345_should_not_appear';
708
+
709
+ insertValidatedPrincipleArtifact({ artifactId, principleId, text: secretText });
710
+ await insertPromptActivation({ artifactId, principleId });
711
+
712
+ const { EventLogService } = await import('../../src/core/event-log.js');
713
+ const mockEventLog = (EventLogService.get as ReturnType<typeof vi.fn>)();
714
+ const spy = mockEventLog.recordRuntimeV2ActivationsInjected as ReturnType<typeof vi.fn>;
715
+ spy.mockClear();
716
+
717
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
718
+ await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
719
+
720
+ expect(spy).toHaveBeenCalled();
721
+ const payload = spy.mock.calls[0][0];
722
+ const serialized = JSON.stringify(payload);
723
+ // Should not contain the full principle text — only IDs and char count
724
+ expect(serialized).not.toContain(secretText);
725
+ // Should not contain the full prompt
726
+ expect(serialized).not.toContain('hello world');
727
+ });
728
+ });
729
+
730
+ describe('Runtime V2 owner-approved behavior directives section', () => {
731
+ beforeEach(() => {
732
+ vi.resetModules();
733
+ });
734
+
735
+ it('renders owner-approved directives in prependSystemContext when activations exist', async () => {
736
+ const artifactId = 'art-v2-directive-201';
737
+ const principleId = 'princ-v2-directive-201';
738
+
739
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
740
+ await insertPromptActivation({ artifactId, principleId });
741
+
742
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
743
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
744
+
745
+ expect(result?.prependSystemContext).toContain('OWNER-APPROVED BEHAVIOR DIRECTIVES');
746
+ expect(result?.prependSystemContext).toContain('<directive');
747
+ expect(result?.prependSystemContext).toContain('</directive>');
748
+ });
749
+
750
+ it('prependSystemContext contains MANDATORY framing', async () => {
751
+ const artifactId = 'art-v2-directive-202';
752
+ const principleId = 'princ-v2-directive-202';
753
+
754
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
755
+ await insertPromptActivation({ artifactId, principleId });
756
+
757
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
758
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
759
+
760
+ const ctx = result?.prependSystemContext ?? '';
761
+ expect(ctx).toContain('MANDATORY');
762
+ expect(ctx).toContain('Owner-approved');
763
+ expect(ctx).toContain('active behavior constraint');
764
+ expect(ctx).toContain('Do not treat this as background context');
765
+ });
766
+
767
+ it('prependSystemContext includes safety boundary disclaimer', async () => {
768
+ const artifactId = 'art-v2-directive-203';
769
+ const principleId = 'princ-v2-directive-203';
770
+
771
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
772
+ await insertPromptActivation({ artifactId, principleId });
773
+
774
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
775
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
776
+
777
+ const ctx = result?.prependSystemContext ?? '';
778
+ expect(ctx).toContain('do not override safety');
779
+ expect(ctx).toContain('do not override safety, security, or core system policy');
780
+ });
781
+
782
+ it('directives appear in prependSystemContext (before gateway system prompt)', async () => {
783
+ const artifactId = 'art-v2-directive-204';
784
+ const principleId = 'princ-v2-directive-204';
785
+
786
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
787
+ await insertPromptActivation({ artifactId, principleId });
788
+
789
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
790
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
791
+
792
+ // Directives should be in prependSystemContext, NOT in appendSystemContext
793
+ const prependCtx = result?.prependSystemContext ?? '';
794
+ const appendCtx = result?.appendSystemContext ?? '';
795
+ const directiveMarker = 'OWNER-APPROVED BEHAVIOR DIRECTIVES';
796
+ expect(prependCtx).toContain(directiveMarker);
797
+ // Should NOT be duplicated in appendSystemContext
798
+ expect(appendCtx).not.toContain(directiveMarker);
799
+ });
800
+
801
+ it('directives appear after AGENT IDENTITY in prependSystemContext', async () => {
802
+ const artifactId = 'art-v2-directive-205';
803
+ const principleId = 'princ-v2-directive-205';
804
+
805
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
806
+ await insertPromptActivation({ artifactId, principleId });
807
+
808
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
809
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
810
+
811
+ const ctx = result?.prependSystemContext ?? '';
812
+ const identityIdx = ctx.indexOf('AGENT IDENTITY');
813
+ const directiveIdx = ctx.indexOf('OWNER-APPROVED BEHAVIOR DIRECTIVES');
814
+ expect(identityIdx).toBeGreaterThanOrEqual(0);
815
+ expect(directiveIdx).toBeGreaterThan(identityIdx);
816
+ });
817
+
818
+ it('confirm-first principle rendered as directive with id attribute', async () => {
819
+ const artifactId = 'art-mvp-acceptance-001';
820
+ const principleId = 'princ-mvp-acceptance-confirm-first';
821
+ const text = 'Before starting any coding task, the agent must first confirm requirements and present a plan for owner approval.';
822
+
823
+ insertValidatedPrincipleArtifact({ artifactId, principleId, text });
824
+ await insertPromptActivation({ artifactId, principleId });
825
+
826
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
827
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
828
+
829
+ const ctx = result?.prependSystemContext ?? '';
830
+ expect(ctx).toContain(`<directive id="princ-mvp-acceptance-confirm-first" source="runtime_v2_activation">`);
831
+ expect(ctx).toContain('MANDATORY: Before starting any coding task');
832
+ });
833
+
834
+ it('no directive section when no activations exist', async () => {
835
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
836
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
837
+
838
+ const prependCtx = result?.prependSystemContext ?? '';
839
+ expect(prependCtx).not.toContain('OWNER-APPROVED BEHAVIOR DIRECTIVES');
840
+ });
841
+
842
+ it('existing evolution_principles behavior for legacy principles remains intact', async () => {
843
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
844
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
845
+
846
+ expect(result).toBeDefined();
847
+ expect(result?.appendSystemContext).toBeDefined();
848
+ });
849
+
850
+ it('feature flag disabled path still skips Runtime V2 directives with structured reason', async () => {
851
+ const artifactId = 'art-v2-flag-206';
852
+ const principleId = 'princ-v2-flag-206';
853
+
854
+ insertValidatedPrincipleArtifact({ artifactId, principleId });
855
+ await insertPromptActivation({ artifactId, principleId });
856
+
857
+ const pdDir = path.join(tempWorkspaceDir, '.pd');
858
+ fs.writeFileSync(
859
+ path.join(pdDir, 'feature-flags.yaml'),
860
+ 'prompt:\n enabled: false\n',
861
+ 'utf8',
862
+ );
863
+
864
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
865
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
866
+
867
+ expect(result).toBeDefined();
868
+ });
869
+ });