principles-disciple 1.8.0 → 1.8.2

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 (460) hide show
  1. package/ADVANCED_CONFIG_ZH.md +97 -0
  2. package/AGENT_INSTALL.md +173 -0
  3. package/AGENT_INSTALL_EN.md +173 -0
  4. package/INSTALL.md +256 -0
  5. package/SKILL.md +63 -0
  6. package/docs/COMMAND_REFERENCE.md +76 -0
  7. package/docs/COMMAND_REFERENCE_EN.md +79 -0
  8. package/esbuild.config.js +75 -0
  9. package/openclaw.plugin.json +6 -1
  10. package/package.json +13 -15
  11. package/scripts/build-web.mjs +46 -0
  12. package/scripts/install-dependencies.cjs +47 -0
  13. package/scripts/sync-plugin.mjs +802 -0
  14. package/scripts/verify-build.mjs +109 -0
  15. package/src/agents/nocturnal-dreamer.md +152 -0
  16. package/src/agents/nocturnal-philosopher.md +138 -0
  17. package/src/agents/nocturnal-reflector.md +126 -0
  18. package/src/agents/nocturnal-scribe.md +164 -0
  19. package/src/commands/capabilities.ts +85 -0
  20. package/{dist/commands/context.js → src/commands/context.ts} +78 -38
  21. package/src/commands/evolution-status.ts +146 -0
  22. package/src/commands/export.ts +111 -0
  23. package/src/commands/focus.ts +533 -0
  24. package/src/commands/nocturnal-review.ts +311 -0
  25. package/src/commands/nocturnal-rollout.ts +763 -0
  26. package/src/commands/nocturnal-train.ts +1002 -0
  27. package/{dist/commands/pain.js → src/commands/pain.ts} +68 -49
  28. package/src/commands/principle-rollback.ts +27 -0
  29. package/{dist/commands/rollback.js → src/commands/rollback.ts} +44 -12
  30. package/src/commands/samples.ts +60 -0
  31. package/src/commands/strategy.ts +38 -0
  32. package/{dist/commands/thinking-os.js → src/commands/thinking-os.ts} +59 -36
  33. package/src/commands/workflow-debug.ts +128 -0
  34. package/{dist/config/defaults/runtime.js → src/config/defaults/runtime.ts} +12 -5
  35. package/src/config/errors.ts +163 -0
  36. package/{dist/config/index.d.ts → src/config/index.ts} +2 -1
  37. package/src/constants/diagnostician.ts +66 -0
  38. package/src/constants/tools.ts +62 -0
  39. package/src/core/adaptive-thresholds.ts +476 -0
  40. package/{dist/core/config-service.js → src/core/config-service.ts} +7 -4
  41. package/{dist/core/config.js → src/core/config.ts} +158 -46
  42. package/src/core/control-ui-db.ts +435 -0
  43. package/{dist/core/detection-funnel.js → src/core/detection-funnel.ts} +36 -21
  44. package/{dist/core/detection-service.js → src/core/detection-service.ts} +7 -4
  45. package/{dist/core/dictionary-service.js → src/core/dictionary-service.ts} +7 -4
  46. package/{dist/core/dictionary.js → src/core/dictionary.ts} +57 -34
  47. package/src/core/empathy-keyword-matcher.ts +327 -0
  48. package/src/core/empathy-types.ts +218 -0
  49. package/src/core/event-log.ts +544 -0
  50. package/src/core/evolution-engine.ts +612 -0
  51. package/src/core/evolution-logger.ts +353 -0
  52. package/src/core/evolution-migration.ts +77 -0
  53. package/src/core/evolution-reducer.ts +731 -0
  54. package/src/core/evolution-types.ts +456 -0
  55. package/src/core/external-training-contract.ts +527 -0
  56. package/src/core/focus-history.ts +1458 -0
  57. package/src/core/hygiene/tracker.ts +117 -0
  58. package/{dist/core/init.js → src/core/init.ts} +39 -26
  59. package/src/core/local-worker-routing.ts +617 -0
  60. package/{dist/core/migration.js → src/core/migration.ts} +18 -11
  61. package/src/core/model-deployment-registry.ts +722 -0
  62. package/src/core/model-training-registry.ts +813 -0
  63. package/src/core/nocturnal-arbiter.ts +706 -0
  64. package/src/core/nocturnal-candidate-scoring.ts +392 -0
  65. package/src/core/nocturnal-compliance.ts +1075 -0
  66. package/src/core/nocturnal-dataset.ts +668 -0
  67. package/src/core/nocturnal-executability.ts +428 -0
  68. package/src/core/nocturnal-export.ts +390 -0
  69. package/{dist/core/nocturnal-paths.js → src/core/nocturnal-paths.ts} +49 -23
  70. package/src/core/nocturnal-trajectory-extractor.ts +484 -0
  71. package/src/core/nocturnal-trinity.ts +1384 -0
  72. package/src/core/pain.ts +122 -0
  73. package/{dist/core/path-resolver.js → src/core/path-resolver.ts} +157 -36
  74. package/{dist/core/paths.js → src/core/paths.ts} +13 -4
  75. package/src/core/principle-training-state.ts +450 -0
  76. package/src/core/profile.ts +226 -0
  77. package/src/core/promotion-gate.ts +822 -0
  78. package/{dist/core/risk-calculator.js → src/core/risk-calculator.ts} +42 -16
  79. package/{dist/core/session-tracker.js → src/core/session-tracker.ts} +185 -63
  80. package/src/core/shadow-observation-registry.ts +534 -0
  81. package/{dist/core/system-logger.js → src/core/system-logger.ts} +9 -5
  82. package/src/core/thinking-models.ts +217 -0
  83. package/src/core/training-program.ts +630 -0
  84. package/src/core/trajectory-types.ts +243 -0
  85. package/src/core/trajectory.ts +1673 -0
  86. package/{dist/core/workspace-context.js → src/core/workspace-context.ts} +57 -32
  87. package/src/hooks/bash-risk.ts +171 -0
  88. package/src/hooks/edit-verification.ts +295 -0
  89. package/src/hooks/gate-block-helper.ts +160 -0
  90. package/src/hooks/gate.ts +210 -0
  91. package/src/hooks/gfi-gate.ts +177 -0
  92. package/src/hooks/lifecycle.ts +326 -0
  93. package/{dist/hooks/llm.js → src/hooks/llm.ts} +166 -139
  94. package/src/hooks/message-sanitize.ts +45 -0
  95. package/src/hooks/pain.ts +384 -0
  96. package/src/hooks/progressive-trust-gate.ts +174 -0
  97. package/src/hooks/prompt.ts +920 -0
  98. package/src/hooks/subagent.ts +207 -0
  99. package/src/hooks/thinking-checkpoint.ts +73 -0
  100. package/src/hooks/trajectory-collector.ts +290 -0
  101. package/src/http/principles-console-route.ts +716 -0
  102. package/src/i18n/commands.ts +117 -0
  103. package/src/index.ts +694 -0
  104. package/src/service/central-database.ts +831 -0
  105. package/src/service/control-ui-query-service.ts +888 -0
  106. package/src/service/evolution-query-service.ts +405 -0
  107. package/src/service/evolution-worker.ts +1646 -0
  108. package/src/service/health-query-service.ts +836 -0
  109. package/{dist/service/nocturnal-runtime.js → src/service/nocturnal-runtime.ts} +263 -36
  110. package/src/service/nocturnal-service.ts +1015 -0
  111. package/src/service/nocturnal-target-selector.ts +532 -0
  112. package/src/service/phase3-input-filter.ts +237 -0
  113. package/src/service/runtime-summary-service.ts +757 -0
  114. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +513 -0
  115. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +603 -0
  116. package/src/service/subagent-workflow/index.ts +51 -0
  117. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +856 -0
  118. package/src/service/subagent-workflow/runtime-direct-driver.ts +166 -0
  119. package/src/service/subagent-workflow/types.ts +378 -0
  120. package/src/service/subagent-workflow/workflow-store.ts +328 -0
  121. package/src/service/trajectory-service.ts +15 -0
  122. package/{dist/tools/critique-prompt.js → src/tools/critique-prompt.ts} +25 -8
  123. package/src/tools/deep-reflect.ts +349 -0
  124. package/{dist/tools/model-index.js → src/tools/model-index.ts} +33 -17
  125. package/src/types/event-types.ts +453 -0
  126. package/src/types/hygiene-types.ts +31 -0
  127. package/src/types/principle-tree-schema.ts +244 -0
  128. package/src/types/runtime-summary.ts +49 -0
  129. package/src/types.ts +74 -0
  130. package/src/utils/file-lock.ts +391 -0
  131. package/{dist/utils/glob-match.js → src/utils/glob-match.ts} +21 -20
  132. package/{dist/utils/hashing.js → src/utils/hashing.ts} +6 -4
  133. package/src/utils/io.ts +110 -0
  134. package/{dist/utils/nlp.js → src/utils/nlp.ts} +19 -12
  135. package/{dist/utils/plugin-logger.js → src/utils/plugin-logger.ts} +33 -8
  136. package/src/utils/subagent-probe.ts +94 -0
  137. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +70 -1
  138. package/templates/pain_settings.json +2 -1
  139. package/tests/README.md +120 -0
  140. package/tests/build-artifacts.test.ts +111 -0
  141. package/tests/commands/evolution-status.test.ts +222 -0
  142. package/tests/commands/evolver.test.ts +22 -0
  143. package/tests/commands/export.test.ts +78 -0
  144. package/tests/commands/nocturnal-review.test.ts +448 -0
  145. package/tests/commands/nocturnal-train.test.ts +97 -0
  146. package/tests/commands/pain.test.ts +108 -0
  147. package/tests/commands/samples.test.ts +65 -0
  148. package/tests/commands/strategy.test.ts +34 -0
  149. package/tests/commands/thinking-os.test.ts +88 -0
  150. package/tests/core/adaptive-thresholds.test.ts +261 -0
  151. package/tests/core/config-service.test.ts +89 -0
  152. package/tests/core/config.test.ts +90 -0
  153. package/tests/core/control-ui-db.test.ts +75 -0
  154. package/tests/core/core-template-guidance.test.ts +21 -0
  155. package/tests/core/detection-funnel.test.ts +63 -0
  156. package/tests/core/detection-service.test.ts +50 -0
  157. package/tests/core/dictionary-service.test.ts +116 -0
  158. package/tests/core/dictionary.test.ts +168 -0
  159. package/tests/core/empathy-keyword-matcher.test.ts +209 -0
  160. package/tests/core/event-log.test.ts +181 -0
  161. package/tests/core/evolution-e2e.test.ts +58 -0
  162. package/tests/core/evolution-engine-gate-integration.test.ts +543 -0
  163. package/tests/core/evolution-engine.test.ts +562 -0
  164. package/tests/core/evolution-logger.test.ts +148 -0
  165. package/tests/core/evolution-migration.test.ts +50 -0
  166. package/tests/core/evolution-paths.test.ts +21 -0
  167. package/tests/core/evolution-reducer.detector-metadata.test.ts +602 -0
  168. package/tests/core/evolution-reducer.test.ts +180 -0
  169. package/tests/core/evolution-types-loop.test.ts +48 -0
  170. package/tests/core/evolution-user-stories.e2e.test.ts +249 -0
  171. package/tests/core/external-training-contract.test.ts +463 -0
  172. package/tests/core/focus-history.test.ts +682 -0
  173. package/tests/core/init-flatten.test.ts +69 -0
  174. package/tests/core/init-refactor.test.ts +87 -0
  175. package/tests/core/init-v1.3.test.ts +46 -0
  176. package/tests/core/init.test.ts +190 -0
  177. package/tests/core/local-worker-routing.test.ts +757 -0
  178. package/tests/core/migration.test.ts +84 -0
  179. package/tests/core/model-deployment-registry.test.ts +845 -0
  180. package/tests/core/model-training-registry.test.ts +889 -0
  181. package/tests/core/nocturnal-arbiter.test.ts +494 -0
  182. package/tests/core/nocturnal-candidate-scoring.test.ts +400 -0
  183. package/tests/core/nocturnal-compliance.test.ts +646 -0
  184. package/tests/core/nocturnal-dataset.test.ts +892 -0
  185. package/tests/core/nocturnal-executability.test.ts +357 -0
  186. package/tests/core/nocturnal-export.test.ts +462 -0
  187. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +428 -0
  188. package/tests/core/nocturnal-trajectory-extractor.test.ts +634 -0
  189. package/tests/core/nocturnal-trinity.test.ts +953 -0
  190. package/tests/core/pain.test.ts +33 -0
  191. package/tests/core/path-resolver.test.ts +57 -0
  192. package/tests/core/paths-refactor.test.ts +42 -0
  193. package/tests/core/phase7-rollout-integration.test.ts +477 -0
  194. package/tests/core/principle-training-state.test.ts +712 -0
  195. package/tests/core/profile.test.ts +56 -0
  196. package/tests/core/promotion-gate.test.ts +556 -0
  197. package/tests/core/risk-calculator.test.ts +168 -0
  198. package/tests/core/session-tracker.test.ts +191 -0
  199. package/tests/core/training-program.test.ts +472 -0
  200. package/tests/core/trajectory.test.ts +265 -0
  201. package/tests/core/workspace-context-factory.test.ts +18 -0
  202. package/tests/core/workspace-context.test.ts +134 -0
  203. package/tests/fixtures/nocturnal-reviewed-subset.json +183 -0
  204. package/tests/fixtures/production-compatibility.test.ts +147 -0
  205. package/tests/fixtures/production-mock-generator.ts +282 -0
  206. package/tests/hooks/bash-risk-integration.test.ts +137 -0
  207. package/tests/hooks/bash-risk.test.ts +81 -0
  208. package/tests/hooks/edit-verification.test.ts +678 -0
  209. package/tests/hooks/gate-edit-verification-p1.test.ts +632 -0
  210. package/tests/hooks/gate-edit-verification.test.ts +435 -0
  211. package/tests/hooks/gate-pipeline-integration.test.ts +404 -0
  212. package/tests/hooks/gate.test.ts +271 -0
  213. package/tests/hooks/gfi-gate-unit.test.ts +422 -0
  214. package/tests/hooks/gfi-gate.test.ts +669 -0
  215. package/tests/hooks/lifecycle.test.ts +248 -0
  216. package/tests/hooks/llm.test.ts +308 -0
  217. package/tests/hooks/message-sanitize.test.ts +36 -0
  218. package/tests/hooks/pain.test.ts +141 -0
  219. package/tests/hooks/progressive-trust-gate.test.ts +277 -0
  220. package/tests/hooks/prompt.test.ts +1411 -0
  221. package/tests/hooks/subagent.test.ts +467 -0
  222. package/tests/hooks/thinking-gate.test.ts +313 -0
  223. package/tests/http/principles-console-route.test.ts +140 -0
  224. package/tests/hygiene-tracker.test.ts +77 -0
  225. package/tests/index.integration.test.ts +179 -0
  226. package/tests/index.shadow-routing.integration.test.ts +140 -0
  227. package/tests/index.test.ts +9 -0
  228. package/tests/integration/empathy-workflow-integration.test.ts +627 -0
  229. package/tests/service/control-ui-query-service.test.ts +121 -0
  230. package/tests/service/empathy-observer-workflow-manager.test.ts +176 -0
  231. package/tests/service/evolution-worker.test.ts +585 -0
  232. package/tests/service/nocturnal-runtime.test.ts +470 -0
  233. package/tests/service/nocturnal-service.test.ts +577 -0
  234. package/tests/service/nocturnal-target-selector.test.ts +615 -0
  235. package/tests/service/nocturnal-workflow-manager.test.ts +439 -0
  236. package/tests/service/phase3-input-filter.test.ts +289 -0
  237. package/tests/service/runtime-summary-service.test.ts +919 -0
  238. package/tests/task-compliance.test.ts +166 -0
  239. package/tests/test-utils.ts +48 -0
  240. package/tests/tools/critique-prompt.test.ts +260 -0
  241. package/tests/tools/deep-reflect.test.ts +232 -0
  242. package/tests/tools/model-index.test.ts +246 -0
  243. package/tests/ui/app.test.tsx +114 -0
  244. package/tests/utils/file-lock.test.ts +407 -0
  245. package/tests/utils/hashing.test.ts +32 -0
  246. package/tests/utils/io.test.ts +39 -0
  247. package/tests/utils/nlp.test.ts +53 -0
  248. package/tests/utils/plugin-logger.test.ts +156 -0
  249. package/tsconfig.json +16 -0
  250. package/tsconfig.tsbuildinfo +1 -0
  251. package/ui/src/App.tsx +45 -0
  252. package/ui/src/api.ts +216 -0
  253. package/ui/src/charts.tsx +586 -0
  254. package/ui/src/components/ErrorState.tsx +6 -0
  255. package/ui/src/components/Loading.tsx +13 -0
  256. package/ui/src/components/ProtectedRoute.tsx +12 -0
  257. package/ui/src/components/Shell.tsx +91 -0
  258. package/ui/src/components/WorkspaceConfig.tsx +146 -0
  259. package/ui/src/components/index.ts +5 -0
  260. package/ui/src/context/auth.tsx +80 -0
  261. package/ui/src/context/theme.tsx +66 -0
  262. package/ui/src/hooks/useAutoRefresh.ts +39 -0
  263. package/ui/src/i18n/ui.ts +363 -0
  264. package/ui/src/main.tsx +16 -0
  265. package/ui/src/pages/EvolutionPage.tsx +352 -0
  266. package/ui/src/pages/FeedbackPage.tsx +140 -0
  267. package/ui/src/pages/GateMonitorPage.tsx +136 -0
  268. package/ui/src/pages/LoginPage.tsx +88 -0
  269. package/ui/src/pages/OverviewPage.tsx +238 -0
  270. package/ui/src/pages/SamplesPage.tsx +174 -0
  271. package/ui/src/pages/ThinkingModelsPage.tsx +127 -0
  272. package/ui/src/styles.css +1661 -0
  273. package/ui/src/types.ts +368 -0
  274. package/ui/src/utils/format.ts +15 -0
  275. package/vitest.config.ts +23 -0
  276. package/dist/commands/capabilities.d.ts +0 -3
  277. package/dist/commands/capabilities.js +0 -73
  278. package/dist/commands/context.d.ts +0 -5
  279. package/dist/commands/evolution-status.d.ts +0 -4
  280. package/dist/commands/evolution-status.js +0 -117
  281. package/dist/commands/evolver.d.ts +0 -9
  282. package/dist/commands/evolver.js +0 -26
  283. package/dist/commands/export.d.ts +0 -2
  284. package/dist/commands/export.js +0 -98
  285. package/dist/commands/focus.d.ts +0 -14
  286. package/dist/commands/focus.js +0 -457
  287. package/dist/commands/nocturnal-review.d.ts +0 -24
  288. package/dist/commands/nocturnal-review.js +0 -265
  289. package/dist/commands/nocturnal-rollout.d.ts +0 -27
  290. package/dist/commands/nocturnal-rollout.js +0 -671
  291. package/dist/commands/nocturnal-train.d.ts +0 -25
  292. package/dist/commands/nocturnal-train.js +0 -919
  293. package/dist/commands/pain.d.ts +0 -5
  294. package/dist/commands/principle-rollback.d.ts +0 -4
  295. package/dist/commands/principle-rollback.js +0 -22
  296. package/dist/commands/rollback.d.ts +0 -19
  297. package/dist/commands/samples.d.ts +0 -2
  298. package/dist/commands/samples.js +0 -55
  299. package/dist/commands/strategy.d.ts +0 -3
  300. package/dist/commands/strategy.js +0 -29
  301. package/dist/commands/thinking-os.d.ts +0 -2
  302. package/dist/config/defaults/runtime.d.ts +0 -40
  303. package/dist/config/errors.d.ts +0 -84
  304. package/dist/config/errors.js +0 -94
  305. package/dist/config/index.js +0 -7
  306. package/dist/constants/diagnostician.d.ts +0 -12
  307. package/dist/constants/diagnostician.js +0 -56
  308. package/dist/constants/tools.d.ts +0 -17
  309. package/dist/constants/tools.js +0 -54
  310. package/dist/core/adaptive-thresholds.d.ts +0 -186
  311. package/dist/core/adaptive-thresholds.js +0 -300
  312. package/dist/core/config-service.d.ts +0 -15
  313. package/dist/core/config.d.ts +0 -127
  314. package/dist/core/control-ui-db.d.ts +0 -95
  315. package/dist/core/control-ui-db.js +0 -292
  316. package/dist/core/detection-funnel.d.ts +0 -33
  317. package/dist/core/detection-service.d.ts +0 -15
  318. package/dist/core/dictionary-service.d.ts +0 -15
  319. package/dist/core/dictionary.d.ts +0 -38
  320. package/dist/core/event-log.d.ts +0 -82
  321. package/dist/core/event-log.js +0 -463
  322. package/dist/core/evolution-engine.d.ts +0 -118
  323. package/dist/core/evolution-engine.js +0 -464
  324. package/dist/core/evolution-logger.d.ts +0 -137
  325. package/dist/core/evolution-logger.js +0 -256
  326. package/dist/core/evolution-migration.d.ts +0 -5
  327. package/dist/core/evolution-migration.js +0 -65
  328. package/dist/core/evolution-reducer.d.ts +0 -98
  329. package/dist/core/evolution-reducer.js +0 -465
  330. package/dist/core/evolution-types.d.ts +0 -287
  331. package/dist/core/evolution-types.js +0 -78
  332. package/dist/core/external-training-contract.d.ts +0 -276
  333. package/dist/core/external-training-contract.js +0 -269
  334. package/dist/core/focus-history.d.ts +0 -210
  335. package/dist/core/focus-history.js +0 -1185
  336. package/dist/core/hygiene/tracker.d.ts +0 -22
  337. package/dist/core/hygiene/tracker.js +0 -106
  338. package/dist/core/init.d.ts +0 -12
  339. package/dist/core/local-worker-routing.d.ts +0 -175
  340. package/dist/core/local-worker-routing.js +0 -525
  341. package/dist/core/migration.d.ts +0 -6
  342. package/dist/core/model-deployment-registry.d.ts +0 -218
  343. package/dist/core/model-deployment-registry.js +0 -503
  344. package/dist/core/model-training-registry.d.ts +0 -295
  345. package/dist/core/model-training-registry.js +0 -475
  346. package/dist/core/nocturnal-arbiter.d.ts +0 -159
  347. package/dist/core/nocturnal-arbiter.js +0 -534
  348. package/dist/core/nocturnal-candidate-scoring.d.ts +0 -137
  349. package/dist/core/nocturnal-candidate-scoring.js +0 -266
  350. package/dist/core/nocturnal-compliance.d.ts +0 -175
  351. package/dist/core/nocturnal-compliance.js +0 -824
  352. package/dist/core/nocturnal-dataset.d.ts +0 -224
  353. package/dist/core/nocturnal-dataset.js +0 -443
  354. package/dist/core/nocturnal-executability.d.ts +0 -85
  355. package/dist/core/nocturnal-executability.js +0 -331
  356. package/dist/core/nocturnal-export.d.ts +0 -124
  357. package/dist/core/nocturnal-export.js +0 -275
  358. package/dist/core/nocturnal-paths.d.ts +0 -124
  359. package/dist/core/nocturnal-trajectory-extractor.d.ts +0 -242
  360. package/dist/core/nocturnal-trajectory-extractor.js +0 -307
  361. package/dist/core/nocturnal-trinity.d.ts +0 -311
  362. package/dist/core/nocturnal-trinity.js +0 -880
  363. package/dist/core/pain.d.ts +0 -4
  364. package/dist/core/pain.js +0 -70
  365. package/dist/core/path-resolver.d.ts +0 -46
  366. package/dist/core/paths.d.ts +0 -65
  367. package/dist/core/principle-training-state.d.ts +0 -121
  368. package/dist/core/principle-training-state.js +0 -321
  369. package/dist/core/profile.d.ts +0 -62
  370. package/dist/core/profile.js +0 -210
  371. package/dist/core/promotion-gate.d.ts +0 -238
  372. package/dist/core/promotion-gate.js +0 -529
  373. package/dist/core/risk-calculator.d.ts +0 -22
  374. package/dist/core/session-tracker.d.ts +0 -99
  375. package/dist/core/shadow-observation-registry.d.ts +0 -217
  376. package/dist/core/shadow-observation-registry.js +0 -308
  377. package/dist/core/system-logger.d.ts +0 -8
  378. package/dist/core/thinking-models.d.ts +0 -38
  379. package/dist/core/thinking-models.js +0 -170
  380. package/dist/core/training-program.d.ts +0 -233
  381. package/dist/core/training-program.js +0 -433
  382. package/dist/core/trajectory.d.ts +0 -411
  383. package/dist/core/trajectory.js +0 -1307
  384. package/dist/core/workspace-context.d.ts +0 -71
  385. package/dist/hooks/bash-risk.d.ts +0 -57
  386. package/dist/hooks/bash-risk.js +0 -137
  387. package/dist/hooks/edit-verification.d.ts +0 -62
  388. package/dist/hooks/edit-verification.js +0 -256
  389. package/dist/hooks/gate-block-helper.d.ts +0 -44
  390. package/dist/hooks/gate-block-helper.js +0 -119
  391. package/dist/hooks/gate.d.ts +0 -24
  392. package/dist/hooks/gate.js +0 -173
  393. package/dist/hooks/gfi-gate.d.ts +0 -40
  394. package/dist/hooks/gfi-gate.js +0 -113
  395. package/dist/hooks/lifecycle.d.ts +0 -5
  396. package/dist/hooks/lifecycle.js +0 -284
  397. package/dist/hooks/llm.d.ts +0 -12
  398. package/dist/hooks/message-sanitize.d.ts +0 -3
  399. package/dist/hooks/message-sanitize.js +0 -37
  400. package/dist/hooks/pain.d.ts +0 -5
  401. package/dist/hooks/pain.js +0 -301
  402. package/dist/hooks/progressive-trust-gate.d.ts +0 -51
  403. package/dist/hooks/progressive-trust-gate.js +0 -89
  404. package/dist/hooks/prompt.d.ts +0 -47
  405. package/dist/hooks/prompt.js +0 -884
  406. package/dist/hooks/subagent.d.ts +0 -10
  407. package/dist/hooks/subagent.js +0 -387
  408. package/dist/hooks/thinking-checkpoint.d.ts +0 -37
  409. package/dist/hooks/thinking-checkpoint.js +0 -51
  410. package/dist/hooks/trajectory-collector.d.ts +0 -32
  411. package/dist/hooks/trajectory-collector.js +0 -256
  412. package/dist/http/principles-console-route.d.ts +0 -9
  413. package/dist/http/principles-console-route.js +0 -567
  414. package/dist/i18n/commands.d.ts +0 -26
  415. package/dist/i18n/commands.js +0 -116
  416. package/dist/index.d.ts +0 -7
  417. package/dist/index.js +0 -581
  418. package/dist/service/central-database.d.ts +0 -104
  419. package/dist/service/central-database.js +0 -649
  420. package/dist/service/control-ui-query-service.d.ts +0 -221
  421. package/dist/service/control-ui-query-service.js +0 -543
  422. package/dist/service/empathy-observer-manager.d.ts +0 -52
  423. package/dist/service/empathy-observer-manager.js +0 -229
  424. package/dist/service/evolution-query-service.d.ts +0 -155
  425. package/dist/service/evolution-query-service.js +0 -258
  426. package/dist/service/evolution-worker.d.ts +0 -101
  427. package/dist/service/evolution-worker.js +0 -974
  428. package/dist/service/nocturnal-runtime.d.ts +0 -183
  429. package/dist/service/nocturnal-service.d.ts +0 -163
  430. package/dist/service/nocturnal-service.js +0 -787
  431. package/dist/service/nocturnal-target-selector.d.ts +0 -145
  432. package/dist/service/nocturnal-target-selector.js +0 -315
  433. package/dist/service/phase3-input-filter.d.ts +0 -73
  434. package/dist/service/phase3-input-filter.js +0 -172
  435. package/dist/service/runtime-summary-service.d.ts +0 -122
  436. package/dist/service/runtime-summary-service.js +0 -485
  437. package/dist/service/trajectory-service.d.ts +0 -2
  438. package/dist/service/trajectory-service.js +0 -15
  439. package/dist/tools/critique-prompt.d.ts +0 -14
  440. package/dist/tools/deep-reflect.d.ts +0 -39
  441. package/dist/tools/deep-reflect.js +0 -350
  442. package/dist/tools/model-index.d.ts +0 -9
  443. package/dist/types/event-types.d.ts +0 -306
  444. package/dist/types/event-types.js +0 -106
  445. package/dist/types/hygiene-types.d.ts +0 -20
  446. package/dist/types/hygiene-types.js +0 -12
  447. package/dist/types/runtime-summary.d.ts +0 -47
  448. package/dist/types/runtime-summary.js +0 -1
  449. package/dist/types.d.ts +0 -50
  450. package/dist/types.js +0 -22
  451. package/dist/utils/file-lock.d.ts +0 -71
  452. package/dist/utils/file-lock.js +0 -309
  453. package/dist/utils/glob-match.d.ts +0 -28
  454. package/dist/utils/hashing.d.ts +0 -9
  455. package/dist/utils/io.d.ts +0 -6
  456. package/dist/utils/io.js +0 -106
  457. package/dist/utils/nlp.d.ts +0 -9
  458. package/dist/utils/plugin-logger.d.ts +0 -39
  459. package/dist/utils/subagent-probe.d.ts +0 -34
  460. package/dist/utils/subagent-probe.js +0 -81
@@ -0,0 +1,470 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ import {
6
+ checkWorkspaceIdle,
7
+ checkCooldown,
8
+ checkPreflight,
9
+ recordRunStart,
10
+ recordRunEnd,
11
+ clearAllCooldowns,
12
+ getRuntimeState,
13
+ DEFAULT_IDLE_THRESHOLD_MS,
14
+ DEFAULT_GLOBAL_COOLDOWN_MS,
15
+ DEFAULT_PRINCIPLE_COOLDOWN_MS,
16
+ DEFAULT_ABANDONED_THRESHOLD_MS,
17
+ NOCTURNAL_RUNTIME_FILE,
18
+ } from '../../src/service/nocturnal-runtime.js';
19
+ import { initPersistence, trackToolRead, clearSession, listSessions } from '../../src/core/session-tracker.js';
20
+ import { safeRmDir } from '../test-utils.js';
21
+
22
+ describe('NocturnalRuntime', () => {
23
+ let tempDir: string;
24
+ let workspaceDir: string;
25
+
26
+ beforeEach(() => {
27
+ vi.useFakeTimers();
28
+ // Use a fixed "now" for deterministic testing
29
+ vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
30
+
31
+ workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-ws-'));
32
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-'));
33
+
34
+ // Initialize session tracker persistence for the temp workspace
35
+ initPersistence(tempDir);
36
+ });
37
+
38
+ afterEach(() => {
39
+ vi.useRealTimers();
40
+ vi.clearAllMocks();
41
+ safeRmDir(workspaceDir);
42
+ safeRmDir(tempDir);
43
+ clearSession('session-active');
44
+ clearSession('session-stale');
45
+ clearSession('session-abandoned');
46
+ clearSession('session-ancient');
47
+ });
48
+
49
+ // -------------------------------------------------------------------------
50
+ // Idle Detection Tests
51
+ // -------------------------------------------------------------------------
52
+
53
+ describe('checkWorkspaceIdle', () => {
54
+ it('should return isIdle=true when no sessions exist', () => {
55
+ const result = checkWorkspaceIdle(workspaceDir);
56
+ expect(result.isIdle).toBe(true);
57
+ expect(result.userActiveSessions).toBe(0);
58
+ expect(result.abandonedSessionIds).toEqual([]);
59
+ expect(result.reason).toContain('No active sessions');
60
+ });
61
+
62
+ it('should return isIdle=false when a session is recent (within threshold)', () => {
63
+ // Create an active session with recent activity
64
+ trackToolRead('session-active', 'src/main.ts', workspaceDir);
65
+
66
+ const result = checkWorkspaceIdle(workspaceDir, { idleThresholdMs: 30 * 60 * 1000 });
67
+ expect(result.isIdle).toBe(false);
68
+ expect(result.userActiveSessions).toBe(1);
69
+ expect(result.abandonedSessionIds).toEqual([]);
70
+ });
71
+
72
+ it('should return isIdle=true when session is older than idle threshold', () => {
73
+ // Create a stale session (activity 45 min ago)
74
+ vi.setSystemTime(new Date('2026-03-27T11:15:00.000Z')); // 45 min before "now"
75
+ trackToolRead('session-stale', 'src/main.ts', workspaceDir);
76
+ vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z')); // reset to "now"
77
+
78
+ const result = checkWorkspaceIdle(workspaceDir, { idleThresholdMs: 30 * 60 * 1000 });
79
+ expect(result.isIdle).toBe(true);
80
+ expect(result.idleForMs).toBeGreaterThan(30 * 60 * 1000);
81
+ });
82
+
83
+ it('should treat abandoned sessions as not contributing to idle check', () => {
84
+ // Session active 3 hours ago — should be treated as abandoned
85
+ vi.setSystemTime(new Date('2026-03-27T09:00:00.000Z')); // 3 hours before "now"
86
+ trackToolRead('session-abandoned', 'src/main.ts', workspaceDir);
87
+ vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z')); // reset to "now"
88
+
89
+ const result = checkWorkspaceIdle(workspaceDir, {
90
+ idleThresholdMs: 30 * 60 * 1000,
91
+ abandonedThresholdMs: 2 * 60 * 60 * 1000,
92
+ });
93
+
94
+ expect(result.isIdle).toBe(true); // No active sessions, so idle
95
+ expect(result.abandonedSessionIds).toContain('session-abandoned');
96
+ expect(result.userActiveSessions).toBe(0);
97
+ expect(result.reason).toContain('abandoned session(s) ignored');
98
+ });
99
+
100
+ it('should ignore ancient sessions but still detect recent activity from other sessions', () => {
101
+ // Ancient session (4 hours ago — abandoned)
102
+ vi.setSystemTime(new Date('2026-03-27T08:00:00.000Z'));
103
+ trackToolRead('session-ancient', 'src/main.ts', workspaceDir);
104
+ vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
105
+
106
+ // Recent session (5 minutes ago — still active)
107
+ vi.setSystemTime(new Date('2026-03-27T11:55:00.000Z'));
108
+ trackToolRead('session-active', 'src/main.ts', workspaceDir);
109
+ vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
110
+
111
+ const result = checkWorkspaceIdle(workspaceDir, {
112
+ idleThresholdMs: 30 * 60 * 1000,
113
+ abandonedThresholdMs: 2 * 60 * 60 * 1000,
114
+ });
115
+
116
+ expect(result.isIdle).toBe(false); // Recent activity 5 min ago
117
+ expect(result.abandonedSessionIds).toContain('session-ancient');
118
+ expect(result.userActiveSessions).toBe(1);
119
+ });
120
+
121
+ it('should use trajectory timestamp as secondary guardrail', () => {
122
+ // No sessions, trajectory shows recent activity
123
+ const trajectoryRecent = Date.now() - 5 * 60 * 1000; // 5 min ago
124
+
125
+ const result = checkWorkspaceIdle(workspaceDir, {}, trajectoryRecent);
126
+ expect(result.isIdle).toBe(true); // Still idle (no sessions is primary)
127
+ expect(result.trajectoryGuardrailConfirmsIdle).toBe(false); // But trajectory disagrees
128
+ });
129
+
130
+ it('should confirm idle when both session state and trajectory agree', () => {
131
+ // No sessions, trajectory also shows idle (>80% of threshold)
132
+ const trajectoryOld = Date.now() - 40 * 60 * 1000; // 40 min ago (>24 min = 80% of 30min)
133
+
134
+ const result = checkWorkspaceIdle(workspaceDir, {}, trajectoryOld);
135
+ expect(result.isIdle).toBe(true);
136
+ expect(result.trajectoryGuardrailConfirmsIdle).toBe(true);
137
+ });
138
+
139
+ it('should report idleForMs correctly', () => {
140
+ // Session active 15 min ago
141
+ vi.setSystemTime(new Date('2026-03-27T11:45:00.000Z'));
142
+ trackToolRead('session-active', 'src/main.ts', workspaceDir);
143
+ vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
144
+
145
+ const result = checkWorkspaceIdle(workspaceDir, { idleThresholdMs: 30 * 60 * 1000 });
146
+ expect(result.idleForMs).toBe(15 * 60 * 1000);
147
+ expect(result.isIdle).toBe(false);
148
+ });
149
+ });
150
+
151
+ // -------------------------------------------------------------------------
152
+ // Cooldown Management Tests
153
+ // -------------------------------------------------------------------------
154
+
155
+ describe('checkCooldown', () => {
156
+ it('should return no active cooldowns when state is empty', () => {
157
+ const result = checkCooldown(tempDir);
158
+ expect(result.globalCooldownActive).toBe(false);
159
+ expect(result.principleCooldownActive).toBe(false);
160
+ expect(result.quotaExhausted).toBe(false);
161
+ expect(result.runsRemaining).toBe(3); // DEFAULT_MAX_RUNS_PER_WINDOW
162
+ });
163
+
164
+ it('should detect active global cooldown', async () => {
165
+ await recordRunStart(tempDir, 'T-01');
166
+
167
+ const result = checkCooldown(tempDir);
168
+ expect(result.globalCooldownActive).toBe(true);
169
+ expect(result.globalCooldownRemainingMs).toBe(DEFAULT_GLOBAL_COOLDOWN_MS);
170
+ expect(result.globalCooldownUntil).toBeTruthy();
171
+ });
172
+
173
+ it('should detect expired global cooldown', async () => {
174
+ await recordRunStart(tempDir, 'T-01');
175
+
176
+ // Advance time past the global cooldown
177
+ vi.advanceTimersByTime(DEFAULT_GLOBAL_COOLDOWN_MS + 1000);
178
+
179
+ const result = checkCooldown(tempDir);
180
+ expect(result.globalCooldownActive).toBe(false);
181
+ expect(result.globalCooldownRemainingMs).toBe(0);
182
+ });
183
+
184
+ it('should detect principle-specific cooldown after successful run', async () => {
185
+ await recordRunStart(tempDir, 'T-01');
186
+ await recordRunEnd(tempDir, 'success', { sampleCount: 5 });
187
+
188
+ const result = checkCooldown(tempDir, 'T-01');
189
+ expect(result.principleCooldownActive).toBe(true);
190
+ expect(result.principleCooldownRemainingMs).toBe(DEFAULT_PRINCIPLE_COOLDOWN_MS);
191
+ });
192
+
193
+ it('should not trigger principle cooldown on failed run', async () => {
194
+ await recordRunStart(tempDir, 'T-01');
195
+ await recordRunEnd(tempDir, 'failed', { reason: 'No violating sessions' });
196
+
197
+ // Global cooldown still active, but no principle cooldown
198
+ const result = checkCooldown(tempDir, 'T-01');
199
+ expect(result.globalCooldownActive).toBe(true);
200
+ expect(result.principleCooldownActive).toBe(false);
201
+ });
202
+
203
+ it('should not trigger principle cooldown on skipped run', async () => {
204
+ await recordRunStart(tempDir, 'T-01');
205
+ await recordRunEnd(tempDir, 'skipped', { reason: 'Idle check failed' });
206
+
207
+ const result = checkCooldown(tempDir, 'T-01');
208
+ expect(result.principleCooldownActive).toBe(false);
209
+ });
210
+
211
+ it('should enforce quota limit', async () => {
212
+ // Run max number of times
213
+ for (let i = 0; i < 3; i++) {
214
+ await recordRunStart(tempDir, 'T-01');
215
+ await recordRunEnd(tempDir, 'success', { sampleCount: 1 });
216
+ // Advance past global cooldown for each run
217
+ vi.advanceTimersByTime(DEFAULT_GLOBAL_COOLDOWN_MS + 1000);
218
+ }
219
+
220
+ const result = checkCooldown(tempDir);
221
+ expect(result.quotaExhausted).toBe(true);
222
+ expect(result.runsRemaining).toBe(0);
223
+ });
224
+
225
+ it('should reset quota after window expires', async () => {
226
+ // Run max times
227
+ for (let i = 0; i < 3; i++) {
228
+ await recordRunStart(tempDir, 'T-01');
229
+ await recordRunEnd(tempDir, 'success', { sampleCount: 1 });
230
+ vi.advanceTimersByTime(DEFAULT_GLOBAL_COOLDOWN_MS + 1000);
231
+ }
232
+
233
+ // Advance past the quota window
234
+ vi.advanceTimersByTime(24 * 60 * 60 * 1000 + 1000);
235
+
236
+ const result = checkCooldown(tempDir);
237
+ expect(result.quotaExhausted).toBe(false);
238
+ expect(result.runsRemaining).toBe(3);
239
+ });
240
+
241
+ it('should only cooldown specific principles, not others', async () => {
242
+ await recordRunStart(tempDir, 'T-01');
243
+ await recordRunEnd(tempDir, 'success');
244
+
245
+ const resultT01 = checkCooldown(tempDir, 'T-01');
246
+ const resultT02 = checkCooldown(tempDir, 'T-02');
247
+
248
+ expect(resultT01.principleCooldownActive).toBe(true);
249
+ expect(resultT02.principleCooldownActive).toBe(false);
250
+ });
251
+ });
252
+
253
+ // -------------------------------------------------------------------------
254
+ // Run Recording Tests
255
+ // -------------------------------------------------------------------------
256
+
257
+ describe('recordRunStart / recordRunEnd', () => {
258
+ it('should record run start timestamp', async () => {
259
+ await recordRunStart(tempDir, 'T-01');
260
+ const state = await getRuntimeState(tempDir);
261
+
262
+ expect(state.lastRunAt).toBeTruthy();
263
+ expect(state.lastRunMeta?.targetPrincipleId).toBe('T-01');
264
+ expect(state.lastRunMeta?.status).toBe('skipped');
265
+ expect(state.globalCooldownUntil).toBeTruthy();
266
+ });
267
+
268
+ it('should record successful run with sample count', async () => {
269
+ await recordRunStart(tempDir, 'T-01');
270
+ await recordRunEnd(tempDir, 'success', { sampleCount: 7 });
271
+
272
+ const state = await getRuntimeState(tempDir);
273
+ expect(state.lastSuccessfulRunAt).toBeTruthy();
274
+ expect(state.lastRunMeta?.status).toBe('success');
275
+ expect(state.lastRunMeta?.sampleCount).toBe(7);
276
+ expect(state.principleCooldowns['T-01']).toBeTruthy();
277
+ });
278
+
279
+ it('should preserve failed run reason without setting successful timestamp', async () => {
280
+ await recordRunStart(tempDir, 'T-02');
281
+ await recordRunEnd(tempDir, 'failed', { reason: 'No violating sessions found' });
282
+
283
+ const state = await getRuntimeState(tempDir);
284
+ expect(state.lastSuccessfulRunAt).toBeUndefined();
285
+ expect(state.lastRunMeta?.status).toBe('failed');
286
+ expect(state.lastRunMeta?.reason).toBe('No violating sessions found');
287
+ expect(state.principleCooldowns['T-02']).toBeUndefined(); // No principle cooldown on failure
288
+ });
289
+
290
+ it('should add timestamp to recentRunTimestamps for quota tracking', async () => {
291
+ await recordRunStart(tempDir, 'T-01');
292
+ const state = await getRuntimeState(tempDir);
293
+
294
+ expect(state.recentRunTimestamps.length).toBe(1);
295
+ });
296
+ });
297
+
298
+ // -------------------------------------------------------------------------
299
+ // clearAllCooldowns Tests
300
+ // -------------------------------------------------------------------------
301
+
302
+ describe('clearAllCooldowns', () => {
303
+ it('should clear all cooldown state', async () => {
304
+ await recordRunStart(tempDir, 'T-01');
305
+ await recordRunEnd(tempDir, 'success');
306
+
307
+ await clearAllCooldowns(tempDir);
308
+
309
+ const state = await getRuntimeState(tempDir);
310
+ expect(state.globalCooldownUntil).toBeUndefined();
311
+ expect(state.principleCooldowns).toEqual({});
312
+ expect(state.recentRunTimestamps).toEqual([]);
313
+ expect(state.lastRunMeta).toBeUndefined();
314
+ });
315
+ });
316
+
317
+ // -------------------------------------------------------------------------
318
+ // Preflight Check Tests
319
+ // -------------------------------------------------------------------------
320
+
321
+ describe('checkPreflight', () => {
322
+ it('should return canRun=true when workspace is idle and no cooldowns', () => {
323
+ // No sessions = idle
324
+ const result = checkPreflight(workspaceDir, tempDir, 'T-01');
325
+ expect(result.canRun).toBe(true);
326
+ expect(result.blockers).toEqual([]);
327
+ });
328
+
329
+ it('should block when workspace is not idle', () => {
330
+ // Create recent session
331
+ trackToolRead('session-active', 'src/main.ts', workspaceDir);
332
+
333
+ const result = checkPreflight(workspaceDir, tempDir, 'T-01');
334
+ expect(result.canRun).toBe(false);
335
+ expect(result.blockers.some(b => b.includes('not idle'))).toBe(true);
336
+ });
337
+
338
+ it('should block when global cooldown is active', async () => {
339
+ await recordRunStart(tempDir, 'T-01');
340
+
341
+ const result = checkPreflight(workspaceDir, tempDir, 'T-01');
342
+ expect(result.canRun).toBe(false);
343
+ expect(result.blockers.some(b => b.includes('Global cooldown'))).toBe(true);
344
+ });
345
+
346
+ it('should block when quota is exhausted', async () => {
347
+ // Exhaust quota
348
+ for (let i = 0; i < 3; i++) {
349
+ await recordRunStart(tempDir, 'T-01');
350
+ await recordRunEnd(tempDir, 'success', { sampleCount: 1 });
351
+ vi.advanceTimersByTime(DEFAULT_GLOBAL_COOLDOWN_MS + 1000);
352
+ }
353
+
354
+ const result = checkPreflight(workspaceDir, tempDir, 'T-01');
355
+ expect(result.canRun).toBe(false);
356
+ expect(result.blockers.some(b => b.includes('Quota exhausted'))).toBe(true);
357
+ });
358
+
359
+ it('should report all blockers when multiple conditions block', async () => {
360
+ // Create recent session AND set global cooldown
361
+ trackToolRead('session-active', 'src/main.ts', workspaceDir);
362
+ await recordRunStart(tempDir, 'T-01');
363
+
364
+ const result = checkPreflight(workspaceDir, tempDir, 'T-01');
365
+ expect(result.canRun).toBe(false);
366
+ expect(result.blockers.length).toBeGreaterThanOrEqual(2);
367
+ });
368
+
369
+ it('should include idle info in preflight result', () => {
370
+ const result = checkPreflight(workspaceDir, tempDir, 'T-01');
371
+ expect(result.idle).toBeDefined();
372
+ expect(result.idle.isIdle).toBe(true); // No sessions
373
+ });
374
+
375
+ it('should include cooldown info in preflight result', () => {
376
+ const result = checkPreflight(workspaceDir, tempDir, 'T-01');
377
+ expect(result.cooldown).toBeDefined();
378
+ expect(result.cooldown.globalCooldownActive).toBe(false);
379
+ });
380
+ });
381
+
382
+ // -------------------------------------------------------------------------
383
+ // Abandoned Session Tests (dedicated)
384
+ // -------------------------------------------------------------------------
385
+
386
+ describe('abandoned sessions', () => {
387
+ it('should not block nocturnal flow when all sessions are abandoned but workspace otherwise empty', () => {
388
+ // Create only abandoned sessions (no recent activity)
389
+ vi.setSystemTime(new Date('2026-03-27T09:00:00.000Z')); // 3 hours ago
390
+ trackToolRead('session-abandoned', 'src/main.ts', workspaceDir);
391
+ vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
392
+
393
+ const result = checkWorkspaceIdle(workspaceDir, {
394
+ idleThresholdMs: 30 * 60 * 1000,
395
+ abandonedThresholdMs: 2 * 60 * 60 * 1000,
396
+ });
397
+
398
+ // Workspace should be considered idle (all sessions abandoned = effectively no sessions)
399
+ expect(result.isIdle).toBe(true);
400
+ expect(result.userActiveSessions).toBe(0);
401
+ });
402
+
403
+ it('should not incorrectly block when there are abandoned AND active sessions', () => {
404
+ // Abandoned session (3 hours ago)
405
+ vi.setSystemTime(new Date('2026-03-27T09:00:00.000Z'));
406
+ trackToolRead('session-abandoned', 'src/main.ts', workspaceDir);
407
+
408
+ // Recent session (5 min ago)
409
+ vi.setSystemTime(new Date('2026-03-27T11:55:00.000Z'));
410
+ trackToolRead('session-active', 'src/main.ts', workspaceDir);
411
+ vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
412
+
413
+ const idleResult = checkWorkspaceIdle(workspaceDir, {
414
+ idleThresholdMs: 30 * 60 * 1000,
415
+ abandonedThresholdMs: 2 * 60 * 60 * 1000,
416
+ });
417
+
418
+ // Should NOT be idle because there's a recent active session
419
+ expect(idleResult.isIdle).toBe(false);
420
+ expect(idleResult.userActiveSessions).toBe(1);
421
+ expect(idleResult.abandonedSessionIds).toContain('session-abandoned');
422
+ });
423
+
424
+ it('should persist abandoned sessions do not create cooldown state', async () => {
425
+ // Create abandoned session
426
+ vi.setSystemTime(new Date('2026-03-27T09:00:00.000Z'));
427
+ trackToolRead('session-abandoned', 'src/main.ts', workspaceDir);
428
+ vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
429
+
430
+ // Workspace is idle, preflight should pass
431
+ const result = checkPreflight(workspaceDir, tempDir, 'T-01');
432
+ expect(result.canRun).toBe(true);
433
+ });
434
+ });
435
+
436
+ // -------------------------------------------------------------------------
437
+ // File Persistence Tests
438
+ // -------------------------------------------------------------------------
439
+
440
+ describe('file persistence', () => {
441
+ it('should create nocturnal-runtime.json on first write', async () => {
442
+ const filePath = path.join(tempDir, NOCTURNAL_RUNTIME_FILE);
443
+ expect(fs.existsSync(filePath)).toBe(false);
444
+
445
+ await recordRunStart(tempDir, 'T-01');
446
+ expect(fs.existsSync(filePath)).toBe(true);
447
+ });
448
+
449
+ it('should survive corrupted JSON (start fresh)', async () => {
450
+ const filePath = path.join(tempDir, NOCTURNAL_RUNTIME_FILE);
451
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
452
+ fs.writeFileSync(filePath, '{ corrupted json }', 'utf-8');
453
+
454
+ const state = await getRuntimeState(tempDir);
455
+ // Should return default state, not throw
456
+ expect(state.principleCooldowns).toEqual({});
457
+ expect(state.recentRunTimestamps).toEqual([]);
458
+ });
459
+
460
+ it('should read persisted cooldown on restart', async () => {
461
+ await recordRunStart(tempDir, 'T-01');
462
+ await recordRunEnd(tempDir, 'success');
463
+
464
+ // Simulate restart by re-reading
465
+ const state = await getRuntimeState(tempDir);
466
+ expect(state.lastSuccessfulRunAt).toBeTruthy();
467
+ expect(state.principleCooldowns['T-01']).toBeTruthy();
468
+ });
469
+ });
470
+ });