principles-disciple 1.71.0 → 1.73.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 (309) hide show
  1. package/openclaw.plugin.json +10 -5
  2. package/package.json +17 -19
  3. package/scripts/acceptance-test.mjs +16 -73
  4. package/scripts/sync-plugin.mjs +382 -77
  5. package/src/commands/archive-impl.ts +2 -1
  6. package/src/commands/capabilities.ts +2 -2
  7. package/src/commands/context.ts +2 -2
  8. package/src/commands/disable-impl.ts +2 -1
  9. package/src/commands/evolution-status.ts +16 -16
  10. package/src/commands/export.ts +12 -67
  11. package/src/commands/pain.ts +91 -1
  12. package/src/commands/principle-rollback.ts +2 -1
  13. package/src/commands/promote-impl.ts +7 -43
  14. package/src/commands/rollback-impl.ts +2 -1
  15. package/src/commands/rollback.ts +2 -1
  16. package/src/commands/samples.ts +2 -1
  17. package/src/commands/thinking-os.ts +2 -1
  18. package/src/config/errors.ts +18 -2
  19. package/src/constants/diagnostician.ts +2 -2
  20. package/src/constants/tools.ts +2 -1
  21. package/src/core/__tests__/focus-history.test.ts +210 -0
  22. package/src/core/config.ts +1 -1
  23. package/src/core/confirm-first-gate.ts +255 -0
  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 +38 -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/pain-diagnostic-gate.ts +154 -0
  37. package/src/core/pain-signal.ts +21 -138
  38. package/src/core/pain.ts +15 -88
  39. package/src/core/pd-task-reconciler.ts +26 -115
  40. package/src/core/pd-task-service.ts +9 -9
  41. package/src/core/pd-task-types.ts +23 -127
  42. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  43. package/src/core/principle-compiler/code-validator.ts +15 -42
  44. package/src/core/principle-compiler/compiler.ts +100 -15
  45. package/src/core/principle-compiler/index.ts +5 -2
  46. package/src/core/principle-compiler/template-generator.ts +4 -104
  47. package/src/core/principle-injection.ts +10 -202
  48. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  49. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  50. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  51. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  52. package/src/core/principle-tree-ledger.ts +8 -6
  53. package/src/core/reflection/reflection-context.ts +14 -109
  54. package/src/core/replay-engine.ts +8 -500
  55. package/src/core/rule-host-helpers.ts +5 -35
  56. package/src/core/rule-host-types.ts +10 -82
  57. package/src/core/rule-host.ts +6 -63
  58. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  59. package/src/core/session-tracker.ts +87 -101
  60. package/src/core/shadow-observation-registry.ts +19 -48
  61. package/src/core/trajectory.ts +3 -1
  62. package/src/core/workflow-funnel-loader.ts +62 -68
  63. package/src/core/workspace-context.ts +46 -0
  64. package/src/core/workspace-dir-service.ts +1 -1
  65. package/src/core/workspace-dir-validation.ts +18 -9
  66. package/src/hooks/AGENTS.md +1 -1
  67. package/src/hooks/gate-block-helper.ts +46 -44
  68. package/src/hooks/gate.ts +207 -7
  69. package/src/hooks/lifecycle.ts +30 -32
  70. package/src/hooks/llm.ts +60 -32
  71. package/src/hooks/pain.ts +297 -103
  72. package/src/hooks/prompt.ts +469 -339
  73. package/src/hooks/subagent.ts +2 -29
  74. package/src/i18n/commands.ts +2 -10
  75. package/src/index.ts +95 -85
  76. package/src/openclaw-sdk.ts +311 -0
  77. package/src/service/central-database.ts +8 -4
  78. package/src/service/evolution-queue-migration.ts +2 -1
  79. package/src/service/evolution-worker.ts +163 -1786
  80. package/src/service/internalization-trigger-adapter.ts +302 -0
  81. package/src/service/keyword-optimization-service.ts +4 -4
  82. package/src/service/monitoring-query-service.ts +1 -215
  83. package/src/service/queue-io.ts +60 -331
  84. package/src/service/runtime-summary-service.ts +115 -18
  85. package/src/service/subagent-workflow/index.ts +0 -41
  86. package/src/service/subagent-workflow/types.ts +9 -120
  87. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  88. package/src/service/workflow-watchdog.ts +0 -43
  89. package/src/types/event-payload.ts +16 -74
  90. package/src/types/event-types.ts +39 -547
  91. package/src/types/hygiene-types.ts +7 -30
  92. package/src/types/principle-tree-schema.ts +20 -222
  93. package/src/types/queue.ts +15 -70
  94. package/src/types/runtime-summary.ts +5 -49
  95. package/src/utils/io.ts +10 -0
  96. package/src/utils/retry.ts +1 -1
  97. package/src/utils/shadow-fingerprint.ts +2 -2
  98. package/src/utils/workspace-resolver.ts +50 -0
  99. package/templates/langs/en/core/AGENTS.md +2 -2
  100. package/templates/langs/en/core/BOOT.md +1 -1
  101. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  102. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  103. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  104. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  105. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  107. package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  108. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  109. package/templates/langs/en/skills/evolve-task/SKILL.md +1 -1
  110. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  111. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  112. package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -1
  113. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  114. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  115. package/templates/langs/zh/core/AGENTS.md +2 -2
  116. package/templates/langs/zh/core/BOOT.md +1 -1
  117. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  118. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  119. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  120. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  121. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  127. package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
  128. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  129. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  130. package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -1
  131. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  132. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  133. package/tests/build-artifacts.test.ts +1 -3
  134. package/tests/commands/evolution-status.test.ts +0 -118
  135. package/tests/core/bootstrap-rules.test.ts +1 -1
  136. package/tests/core/config.test.ts +1 -1
  137. package/tests/core/event-log.test.ts +35 -0
  138. package/tests/core/evolution-engine.test.ts +610 -0
  139. package/tests/core/file-store.test.ts +102 -0
  140. package/tests/core/focus-history.test.ts +203 -11
  141. package/tests/core/merge-gate-audit.test.ts +2 -169
  142. package/tests/core/model-deployment-registry.test.ts +7 -1
  143. package/tests/core/model-training-registry.test.ts +19 -0
  144. package/tests/core/observability.test.ts +0 -1
  145. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  146. package/tests/core/pain.test.ts +0 -1
  147. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  148. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  149. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  150. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  151. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  152. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  153. package/tests/core/reflection-context.test.ts +0 -14
  154. package/tests/core/replay-engine.test.ts +127 -215
  155. package/tests/core/rule-host-helpers.test.ts +2 -2
  156. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  157. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  158. package/tests/core/workspace-dir-validation.test.ts +8 -1
  159. package/tests/core-anti-growth.test.ts +192 -0
  160. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  161. package/tests/hooks/confirm-first-gate.test.ts +333 -0
  162. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  163. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  164. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  165. package/tests/hooks/pain.test.ts +269 -12
  166. package/tests/hooks/prompt-characterization.test.ts +500 -0
  167. package/tests/hooks/prompt-size-guard.test.ts +329 -0
  168. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  169. package/tests/index.test.ts +94 -1
  170. package/tests/integration/auto-entry-gate.test.ts +248 -0
  171. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  172. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  173. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  174. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  175. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  176. package/tests/service/central-database.test.ts +457 -0
  177. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  178. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  179. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  180. package/tests/service/monitoring-query-service.test.ts +1 -47
  181. package/tests/service/queue-io.test.ts +1 -62
  182. package/tests/service/runtime-summary-service.test.ts +184 -3
  183. package/tests/service/workflow-watchdog.test.ts +0 -91
  184. package/tests/utils/file-lock.test.ts +5 -3
  185. package/tests/utils/session-key.test.ts +52 -0
  186. package/tests/utils/subagent-probe.test.ts +48 -1
  187. package/vitest.config.ts +4 -11
  188. package/.planning/codebase/ARCHITECTURE.md +0 -157
  189. package/.planning/codebase/CONCERNS.md +0 -145
  190. package/.planning/codebase/CONVENTIONS.md +0 -148
  191. package/.planning/codebase/INTEGRATIONS.md +0 -81
  192. package/.planning/codebase/STACK.md +0 -87
  193. package/.planning/codebase/STRUCTURE.md +0 -193
  194. package/.planning/codebase/TESTING.md +0 -243
  195. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  196. package/docs/COMMAND_REFERENCE.md +0 -76
  197. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  198. package/scripts/build-web.mjs +0 -46
  199. package/scripts/diagnose-nocturnal.mjs +0 -537
  200. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  201. package/src/commands/nocturnal-review.ts +0 -322
  202. package/src/commands/nocturnal-rollout.ts +0 -790
  203. package/src/commands/nocturnal-train.ts +0 -986
  204. package/src/commands/pd-reflect.ts +0 -88
  205. package/src/core/adaptive-thresholds.ts +0 -478
  206. package/src/core/diagnostician-task-store.ts +0 -192
  207. package/src/core/nocturnal-arbiter.ts +0 -715
  208. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  209. package/src/core/nocturnal-artificer.ts +0 -257
  210. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  211. package/src/core/nocturnal-compliance.ts +0 -1146
  212. package/src/core/nocturnal-dataset.ts +0 -763
  213. package/src/core/nocturnal-executability.ts +0 -428
  214. package/src/core/nocturnal-export.ts +0 -499
  215. package/src/core/nocturnal-paths.ts +0 -240
  216. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  217. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  218. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  219. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  220. package/src/core/nocturnal-trinity-types.ts +0 -218
  221. package/src/core/nocturnal-trinity.ts +0 -2680
  222. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  223. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  224. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  225. package/src/http/principles-console-route.ts +0 -709
  226. package/src/service/central-health-service.ts +0 -49
  227. package/src/service/central-overview-service.ts +0 -138
  228. package/src/service/control-ui-query-service.ts +0 -900
  229. package/src/service/cooldown-strategy.ts +0 -97
  230. package/src/service/evolution-pain-context.ts +0 -79
  231. package/src/service/evolution-query-service.ts +0 -407
  232. package/src/service/health-query-service.ts +0 -1038
  233. package/src/service/nocturnal-config.ts +0 -214
  234. package/src/service/nocturnal-runtime.ts +0 -734
  235. package/src/service/nocturnal-service.ts +0 -1605
  236. package/src/service/nocturnal-target-selector.ts +0 -545
  237. package/src/service/sleep-cycle.ts +0 -157
  238. package/src/service/startup-reconciler.ts +0 -112
  239. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  240. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  241. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  242. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  243. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  244. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  245. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  246. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  247. package/src/tools/write-pain-flag.ts +0 -215
  248. package/tests/commands/nocturnal-review.test.ts +0 -448
  249. package/tests/commands/nocturnal-train.test.ts +0 -97
  250. package/tests/commands/pd-reflect.test.ts +0 -49
  251. package/tests/core/adaptive-thresholds.test.ts +0 -261
  252. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  253. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  254. package/tests/core/nocturnal-artificer.test.ts +0 -241
  255. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  256. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  257. package/tests/core/nocturnal-compliance.test.ts +0 -646
  258. package/tests/core/nocturnal-dataset.test.ts +0 -892
  259. package/tests/core/nocturnal-e2e.test.ts +0 -234
  260. package/tests/core/nocturnal-executability.test.ts +0 -357
  261. package/tests/core/nocturnal-export.test.ts +0 -517
  262. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  263. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  264. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  265. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  266. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  267. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  268. package/tests/core/pain-auto-repair.test.ts +0 -96
  269. package/tests/core/pain-integration.test.ts +0 -510
  270. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  271. package/tests/http/principles-console-route.test.ts +0 -162
  272. package/tests/integration/chaos-resilience.test.ts +0 -348
  273. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  274. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  275. package/tests/service/control-ui-query-service.test.ts +0 -121
  276. package/tests/service/cooldown-strategy.test.ts +0 -164
  277. package/tests/service/data-endpoints-regression.test.ts +0 -834
  278. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  279. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  280. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  281. package/tests/service/nocturnal-runtime.test.ts +0 -473
  282. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  283. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  284. package/tests/service/startup-reconciler.test.ts +0 -148
  285. package/tests/tools/write-pain-flag.test.ts +0 -358
  286. package/ui/src/App.tsx +0 -45
  287. package/ui/src/api.ts +0 -220
  288. package/ui/src/charts.tsx +0 -955
  289. package/ui/src/components/ErrorState.tsx +0 -6
  290. package/ui/src/components/Loading.tsx +0 -13
  291. package/ui/src/components/ProtectedRoute.tsx +0 -12
  292. package/ui/src/components/Shell.tsx +0 -91
  293. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  294. package/ui/src/components/index.ts +0 -5
  295. package/ui/src/context/auth.tsx +0 -80
  296. package/ui/src/context/theme.tsx +0 -66
  297. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  298. package/ui/src/i18n/ui.ts +0 -473
  299. package/ui/src/main.tsx +0 -16
  300. package/ui/src/pages/EvolutionPage.tsx +0 -333
  301. package/ui/src/pages/FeedbackPage.tsx +0 -138
  302. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  303. package/ui/src/pages/LoginPage.tsx +0 -89
  304. package/ui/src/pages/OverviewPage.tsx +0 -599
  305. package/ui/src/pages/SamplesPage.tsx +0 -174
  306. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  307. package/ui/src/styles.css +0 -2020
  308. package/ui/src/types.ts +0 -384
  309. 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
+ });