principles-disciple 1.72.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 +459 -439
  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 +59 -16
  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 +32 -17
  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 +3 -1
  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
@@ -1,118 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import * as fs from 'fs';
3
- import * as os from 'os';
4
- import * as path from 'path';
5
-
6
- const { mockExecuteNocturnalReflectionAsync } = vi.hoisted(() => ({
7
- mockExecuteNocturnalReflectionAsync: vi.fn(),
8
- }));
9
-
10
- vi.mock('../../src/service/nocturnal-service.js', () => ({
11
- executeNocturnalReflectionAsync: mockExecuteNocturnalReflectionAsync,
12
- }));
13
-
14
- vi.mock('../../src/utils/subagent-probe.js', () => ({
15
- isSubagentRuntimeAvailable: vi.fn(() => true),
16
- }));
17
-
18
- vi.mock('../../src/core/nocturnal-paths.js', () => ({
19
- resolveNocturnalDir: vi.fn((workspaceDir: string) => path.join(workspaceDir, '.state', 'nocturnal', 'samples')),
20
- }));
21
-
22
- import { NocturnalWorkflowManager, nocturnalWorkflowSpec } from '../../src/service/subagent-workflow/nocturnal-workflow-manager.js';
23
- import { safeRmDir } from '../test-utils.js';
24
-
25
- describe('NocturnalWorkflowManager runtime hardening', () => {
26
- let workspaceDir: string;
27
- let stateDir: string;
28
-
29
- beforeEach(() => {
30
- vi.clearAllMocks();
31
- workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-wf-'));
32
- stateDir = path.join(workspaceDir, '.state');
33
- fs.mkdirSync(stateDir, { recursive: true });
34
- });
35
-
36
- afterEach(() => {
37
- safeRmDir(workspaceDir);
38
- });
39
-
40
- it('marks workflow terminal_error when async pipeline throws a gateway-only runtime error', async () => {
41
- mockExecuteNocturnalReflectionAsync.mockRejectedValue(
42
- new Error('Plugin runtime subagent methods are only available during a gateway request.')
43
- );
44
-
45
- const manager = new NocturnalWorkflowManager({
46
- workspaceDir,
47
- stateDir,
48
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as any,
49
- runtimeAdapter: {} as any,
50
- });
51
-
52
- const handle = await manager.startWorkflow(nocturnalWorkflowSpec, {
53
- parentSessionId: 'sleep_reflection:test',
54
- taskInput: {},
55
- metadata: {
56
- snapshot: {
57
- sessionId: 'session-1',
58
- startedAt: '2026-04-10T00:00:00.000Z',
59
- updatedAt: '2026-04-10T00:01:00.000Z',
60
- assistantTurns: [],
61
- userTurns: [],
62
- toolCalls: [],
63
- painEvents: [],
64
- gateBlocks: [],
65
- stats: {
66
- totalAssistantTurns: 1,
67
- totalToolCalls: 1,
68
- totalPainEvents: 0,
69
- totalGateBlocks: 0,
70
- failureCount: 0,
71
- },
72
- },
73
- },
74
- });
75
-
76
- await Promise.resolve();
77
- await Promise.resolve();
78
-
79
- const summary = await manager.getWorkflowDebugSummary(handle.workflowId);
80
- expect(summary?.state).toBe('terminal_error');
81
- expect(summary?.recentEvents.some((event) => event.eventType === 'nocturnal_failed')).toBe(true);
82
-
83
- manager.dispose();
84
- });
85
-
86
- it('rejects malformed snapshot ingress before starting the async pipeline', async () => {
87
- const manager = new NocturnalWorkflowManager({
88
- workspaceDir,
89
- stateDir,
90
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as any,
91
- runtimeAdapter: {} as any,
92
- });
93
-
94
- const handle = await manager.startWorkflow(nocturnalWorkflowSpec, {
95
- parentSessionId: 'sleep_reflection:test',
96
- taskInput: {},
97
- metadata: {
98
- snapshot: {
99
- sessionId: 'session-1',
100
- stats: {
101
- totalAssistantTurns: 1,
102
- totalToolCalls: 1,
103
- totalPainEvents: 0,
104
- totalGateBlocks: 0,
105
- failureCount: 0,
106
- },
107
- },
108
- },
109
- });
110
-
111
- const summary = await manager.getWorkflowDebugSummary(handle.workflowId);
112
- expect(summary?.state).toBe('terminal_error');
113
- expect(summary?.recentEvents.some((event) => event.eventType === 'nocturnal_failed')).toBe(true);
114
- expect(mockExecuteNocturnalReflectionAsync).not.toHaveBeenCalled();
115
-
116
- manager.dispose();
117
- });
118
- });
@@ -1,473 +0,0 @@
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
- // TODO: Fix - abandonedSessionIds not being populated correctly
84
- it.skip('should treat abandoned sessions as not contributing to idle check', () => {
85
- // Session active 3 hours ago — should be treated as abandoned
86
- vi.setSystemTime(new Date('2026-03-27T09:00:00.000Z')); // 3 hours before "now"
87
- trackToolRead('session-abandoned', 'src/main.ts', workspaceDir);
88
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z')); // reset to "now"
89
-
90
- const result = checkWorkspaceIdle(workspaceDir, {
91
- idleThresholdMs: 30 * 60 * 1000,
92
- abandonedThresholdMs: 2 * 60 * 60 * 1000,
93
- });
94
-
95
- expect(result.isIdle).toBe(true); // No active sessions, so idle
96
- expect(result.abandonedSessionIds).toContain('session-abandoned');
97
- expect(result.userActiveSessions).toBe(0);
98
- expect(result.reason).toContain('abandoned session(s) ignored');
99
- });
100
-
101
- // TODO: Fix - abandonedSessionIds not being populated correctly
102
- it.skip('should ignore ancient sessions but still detect recent activity from other sessions', () => {
103
- // Ancient session (4 hours ago — abandoned)
104
- vi.setSystemTime(new Date('2026-03-27T08:00:00.000Z'));
105
- trackToolRead('session-ancient', 'src/main.ts', workspaceDir);
106
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
107
-
108
- // Recent session (5 minutes ago — still active)
109
- vi.setSystemTime(new Date('2026-03-27T11:55:00.000Z'));
110
- trackToolRead('session-active', 'src/main.ts', workspaceDir);
111
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
112
-
113
- const result = checkWorkspaceIdle(workspaceDir, {
114
- idleThresholdMs: 30 * 60 * 1000,
115
- abandonedThresholdMs: 2 * 60 * 60 * 1000,
116
- });
117
-
118
- expect(result.isIdle).toBe(false); // Recent activity 5 min ago
119
- expect(result.abandonedSessionIds).toContain('session-ancient');
120
- expect(result.userActiveSessions).toBe(1);
121
- });
122
-
123
- it('should use trajectory timestamp as secondary guardrail', () => {
124
- // No sessions, trajectory shows recent activity
125
- const trajectoryRecent = Date.now() - 5 * 60 * 1000; // 5 min ago
126
-
127
- const result = checkWorkspaceIdle(workspaceDir, {}, trajectoryRecent);
128
- expect(result.isIdle).toBe(true); // Still idle (no sessions is primary)
129
- expect(result.trajectoryGuardrailConfirmsIdle).toBe(false); // But trajectory disagrees
130
- });
131
-
132
- it('should confirm idle when both session state and trajectory agree', () => {
133
- // No sessions, trajectory also shows idle (>80% of threshold)
134
- const trajectoryOld = Date.now() - 40 * 60 * 1000; // 40 min ago (>24 min = 80% of 30min)
135
-
136
- const result = checkWorkspaceIdle(workspaceDir, {}, trajectoryOld);
137
- expect(result.isIdle).toBe(true);
138
- expect(result.trajectoryGuardrailConfirmsIdle).toBe(true);
139
- });
140
-
141
- it('should report idleForMs correctly', () => {
142
- // Session active 15 min ago
143
- vi.setSystemTime(new Date('2026-03-27T11:45:00.000Z'));
144
- trackToolRead('session-active', 'src/main.ts', workspaceDir);
145
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
146
-
147
- const result = checkWorkspaceIdle(workspaceDir, { idleThresholdMs: 30 * 60 * 1000 });
148
- expect(result.idleForMs).toBe(15 * 60 * 1000);
149
- expect(result.isIdle).toBe(false);
150
- });
151
- });
152
-
153
- // -------------------------------------------------------------------------
154
- // Cooldown Management Tests
155
- // -------------------------------------------------------------------------
156
-
157
- describe('checkCooldown', () => {
158
- it('should return no active cooldowns when state is empty', () => {
159
- const result = checkCooldown(tempDir);
160
- expect(result.globalCooldownActive).toBe(false);
161
- expect(result.principleCooldownActive).toBe(false);
162
- expect(result.quotaExhausted).toBe(false);
163
- expect(result.runsRemaining).toBe(3); // DEFAULT_MAX_RUNS_PER_WINDOW
164
- });
165
-
166
- it('should detect active global cooldown', async () => {
167
- await recordRunStart(tempDir, 'T-01');
168
-
169
- const result = checkCooldown(tempDir);
170
- expect(result.globalCooldownActive).toBe(true);
171
- expect(result.globalCooldownRemainingMs).toBe(DEFAULT_GLOBAL_COOLDOWN_MS);
172
- expect(result.globalCooldownUntil).toBeTruthy();
173
- });
174
-
175
- it('should detect expired global cooldown', async () => {
176
- await recordRunStart(tempDir, 'T-01');
177
-
178
- // Advance time past the global cooldown
179
- vi.advanceTimersByTime(DEFAULT_GLOBAL_COOLDOWN_MS + 1000);
180
-
181
- const result = checkCooldown(tempDir);
182
- expect(result.globalCooldownActive).toBe(false);
183
- expect(result.globalCooldownRemainingMs).toBe(0);
184
- });
185
-
186
- it('should detect principle-specific cooldown after successful run', async () => {
187
- await recordRunStart(tempDir, 'T-01');
188
- await recordRunEnd(tempDir, 'success', { sampleCount: 5 });
189
-
190
- const result = checkCooldown(tempDir, 'T-01');
191
- expect(result.principleCooldownActive).toBe(true);
192
- expect(result.principleCooldownRemainingMs).toBe(DEFAULT_PRINCIPLE_COOLDOWN_MS);
193
- });
194
-
195
- it('should not trigger principle cooldown on failed run', async () => {
196
- await recordRunStart(tempDir, 'T-01');
197
- await recordRunEnd(tempDir, 'failed', { reason: 'No violating sessions' });
198
-
199
- // Global cooldown still active, but no principle cooldown
200
- const result = checkCooldown(tempDir, 'T-01');
201
- expect(result.globalCooldownActive).toBe(true);
202
- expect(result.principleCooldownActive).toBe(false);
203
- });
204
-
205
- it('should not trigger principle cooldown on skipped run', async () => {
206
- await recordRunStart(tempDir, 'T-01');
207
- await recordRunEnd(tempDir, 'skipped', { reason: 'Idle check failed' });
208
-
209
- const result = checkCooldown(tempDir, 'T-01');
210
- expect(result.principleCooldownActive).toBe(false);
211
- });
212
-
213
- it('should enforce quota limit', async () => {
214
- // Run max number of times
215
- for (let i = 0; i < 3; i++) {
216
- await recordRunStart(tempDir, 'T-01');
217
- await recordRunEnd(tempDir, 'success', { sampleCount: 1 });
218
- // Advance past global cooldown for each run
219
- vi.advanceTimersByTime(DEFAULT_GLOBAL_COOLDOWN_MS + 1000);
220
- }
221
-
222
- const result = checkCooldown(tempDir);
223
- expect(result.quotaExhausted).toBe(true);
224
- expect(result.runsRemaining).toBe(0);
225
- });
226
-
227
- it('should reset quota after window expires', async () => {
228
- // Run max times
229
- for (let i = 0; i < 3; i++) {
230
- await recordRunStart(tempDir, 'T-01');
231
- await recordRunEnd(tempDir, 'success', { sampleCount: 1 });
232
- vi.advanceTimersByTime(DEFAULT_GLOBAL_COOLDOWN_MS + 1000);
233
- }
234
-
235
- // Advance past the quota window
236
- vi.advanceTimersByTime(24 * 60 * 60 * 1000 + 1000);
237
-
238
- const result = checkCooldown(tempDir);
239
- expect(result.quotaExhausted).toBe(false);
240
- expect(result.runsRemaining).toBe(3);
241
- });
242
-
243
- it('should only cooldown specific principles, not others', async () => {
244
- await recordRunStart(tempDir, 'T-01');
245
- await recordRunEnd(tempDir, 'success');
246
-
247
- const resultT01 = checkCooldown(tempDir, 'T-01');
248
- const resultT02 = checkCooldown(tempDir, 'T-02');
249
-
250
- expect(resultT01.principleCooldownActive).toBe(true);
251
- expect(resultT02.principleCooldownActive).toBe(false);
252
- });
253
- });
254
-
255
- // -------------------------------------------------------------------------
256
- // Run Recording Tests
257
- // -------------------------------------------------------------------------
258
-
259
- describe('recordRunStart / recordRunEnd', () => {
260
- it('should record run start timestamp', async () => {
261
- await recordRunStart(tempDir, 'T-01');
262
- const state = await getRuntimeState(tempDir);
263
-
264
- expect(state.lastRunAt).toBeTruthy();
265
- expect(state.lastRunMeta?.targetPrincipleId).toBe('T-01');
266
- expect(state.lastRunMeta?.status).toBe('skipped');
267
- expect(state.globalCooldownUntil).toBeTruthy();
268
- });
269
-
270
- it('should record successful run with sample count', async () => {
271
- await recordRunStart(tempDir, 'T-01');
272
- await recordRunEnd(tempDir, 'success', { sampleCount: 7 });
273
-
274
- const state = await getRuntimeState(tempDir);
275
- expect(state.lastSuccessfulRunAt).toBeTruthy();
276
- expect(state.lastRunMeta?.status).toBe('success');
277
- expect(state.lastRunMeta?.sampleCount).toBe(7);
278
- expect(state.principleCooldowns['T-01']).toBeTruthy();
279
- });
280
-
281
- it('should preserve failed run reason without setting successful timestamp', async () => {
282
- await recordRunStart(tempDir, 'T-02');
283
- await recordRunEnd(tempDir, 'failed', { reason: 'No violating sessions found' });
284
-
285
- const state = await getRuntimeState(tempDir);
286
- expect(state.lastSuccessfulRunAt).toBeUndefined();
287
- expect(state.lastRunMeta?.status).toBe('failed');
288
- expect(state.lastRunMeta?.reason).toBe('No violating sessions found');
289
- expect(state.principleCooldowns['T-02']).toBeUndefined(); // No principle cooldown on failure
290
- });
291
-
292
- it('should add timestamp to recentRunTimestamps for quota tracking', async () => {
293
- await recordRunStart(tempDir, 'T-01');
294
- const state = await getRuntimeState(tempDir);
295
-
296
- expect(state.recentRunTimestamps.length).toBe(1);
297
- });
298
- });
299
-
300
- // -------------------------------------------------------------------------
301
- // clearAllCooldowns Tests
302
- // -------------------------------------------------------------------------
303
-
304
- describe('clearAllCooldowns', () => {
305
- it('should clear all cooldown state', async () => {
306
- await recordRunStart(tempDir, 'T-01');
307
- await recordRunEnd(tempDir, 'success');
308
-
309
- await clearAllCooldowns(tempDir);
310
-
311
- const state = await getRuntimeState(tempDir);
312
- expect(state.globalCooldownUntil).toBeUndefined();
313
- expect(state.principleCooldowns).toEqual({});
314
- expect(state.recentRunTimestamps).toEqual([]);
315
- expect(state.lastRunMeta).toBeUndefined();
316
- });
317
- });
318
-
319
- // -------------------------------------------------------------------------
320
- // Preflight Check Tests
321
- // -------------------------------------------------------------------------
322
-
323
- describe('checkPreflight', () => {
324
- it('should return canRun=true when workspace is idle and no cooldowns', () => {
325
- // No sessions = idle
326
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
327
- expect(result.canRun).toBe(true);
328
- expect(result.blockers).toEqual([]);
329
- });
330
-
331
- it('should block when workspace is not idle', () => {
332
- // Create recent session
333
- trackToolRead('session-active', 'src/main.ts', workspaceDir);
334
-
335
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
336
- expect(result.canRun).toBe(false);
337
- expect(result.blockers.some(b => b.includes('not idle'))).toBe(true);
338
- });
339
-
340
- it('should block when global cooldown is active', async () => {
341
- await recordRunStart(tempDir, 'T-01');
342
-
343
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
344
- expect(result.canRun).toBe(false);
345
- expect(result.blockers.some(b => b.includes('Global cooldown'))).toBe(true);
346
- });
347
-
348
- it('should block when quota is exhausted', async () => {
349
- // Exhaust quota
350
- for (let i = 0; i < 3; i++) {
351
- await recordRunStart(tempDir, 'T-01');
352
- await recordRunEnd(tempDir, 'success', { sampleCount: 1 });
353
- vi.advanceTimersByTime(DEFAULT_GLOBAL_COOLDOWN_MS + 1000);
354
- }
355
-
356
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
357
- expect(result.canRun).toBe(false);
358
- expect(result.blockers.some(b => b.includes('Quota exhausted'))).toBe(true);
359
- });
360
-
361
- it('should report all blockers when multiple conditions block', async () => {
362
- // Create recent session AND set global cooldown
363
- trackToolRead('session-active', 'src/main.ts', workspaceDir);
364
- await recordRunStart(tempDir, 'T-01');
365
-
366
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
367
- expect(result.canRun).toBe(false);
368
- expect(result.blockers.length).toBeGreaterThanOrEqual(2);
369
- });
370
-
371
- it('should include idle info in preflight result', () => {
372
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
373
- expect(result.idle).toBeDefined();
374
- expect(result.idle.isIdle).toBe(true); // No sessions
375
- });
376
-
377
- it('should include cooldown info in preflight result', () => {
378
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
379
- expect(result.cooldown).toBeDefined();
380
- expect(result.cooldown.globalCooldownActive).toBe(false);
381
- });
382
- });
383
-
384
- // -------------------------------------------------------------------------
385
- // Abandoned Session Tests (dedicated)
386
- // -------------------------------------------------------------------------
387
-
388
- describe('abandoned sessions', () => {
389
- it('should not block nocturnal flow when all sessions are abandoned but workspace otherwise empty', () => {
390
- // Create only abandoned sessions (no recent activity)
391
- vi.setSystemTime(new Date('2026-03-27T09:00:00.000Z')); // 3 hours ago
392
- trackToolRead('session-abandoned', 'src/main.ts', workspaceDir);
393
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
394
-
395
- const result = checkWorkspaceIdle(workspaceDir, {
396
- idleThresholdMs: 30 * 60 * 1000,
397
- abandonedThresholdMs: 2 * 60 * 60 * 1000,
398
- });
399
-
400
- // Workspace should be considered idle (all sessions abandoned = effectively no sessions)
401
- expect(result.isIdle).toBe(true);
402
- expect(result.userActiveSessions).toBe(0);
403
- });
404
-
405
- // TODO: Fix - abandonedSessionIds not being populated correctly
406
- it.skip('should not incorrectly block when there are abandoned AND active sessions', () => {
407
- // Abandoned session (3 hours ago)
408
- vi.setSystemTime(new Date('2026-03-27T09:00:00.000Z'));
409
- trackToolRead('session-abandoned', 'src/main.ts', workspaceDir);
410
-
411
- // Recent session (5 min ago)
412
- vi.setSystemTime(new Date('2026-03-27T11:55:00.000Z'));
413
- trackToolRead('session-active', 'src/main.ts', workspaceDir);
414
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
415
-
416
- const idleResult = checkWorkspaceIdle(workspaceDir, {
417
- idleThresholdMs: 30 * 60 * 1000,
418
- abandonedThresholdMs: 2 * 60 * 60 * 1000,
419
- });
420
-
421
- // Should NOT be idle because there's a recent active session
422
- expect(idleResult.isIdle).toBe(false);
423
- expect(idleResult.userActiveSessions).toBe(1);
424
- expect(idleResult.abandonedSessionIds).toContain('session-abandoned');
425
- });
426
-
427
- it('should persist abandoned sessions do not create cooldown state', async () => {
428
- // Create abandoned session
429
- vi.setSystemTime(new Date('2026-03-27T09:00:00.000Z'));
430
- trackToolRead('session-abandoned', 'src/main.ts', workspaceDir);
431
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
432
-
433
- // Workspace is idle, preflight should pass
434
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
435
- expect(result.canRun).toBe(true);
436
- });
437
- });
438
-
439
- // -------------------------------------------------------------------------
440
- // File Persistence Tests
441
- // -------------------------------------------------------------------------
442
-
443
- describe('file persistence', () => {
444
- it('should create nocturnal-runtime.json on first write', async () => {
445
- const filePath = path.join(tempDir, NOCTURNAL_RUNTIME_FILE);
446
- expect(fs.existsSync(filePath)).toBe(false);
447
-
448
- await recordRunStart(tempDir, 'T-01');
449
- expect(fs.existsSync(filePath)).toBe(true);
450
- });
451
-
452
- it('should survive corrupted JSON (start fresh)', async () => {
453
- const filePath = path.join(tempDir, NOCTURNAL_RUNTIME_FILE);
454
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
455
- fs.writeFileSync(filePath, '{ corrupted json }', 'utf-8');
456
-
457
- const state = await getRuntimeState(tempDir);
458
- // Should return default state, not throw
459
- expect(state.principleCooldowns).toEqual({});
460
- expect(state.recentRunTimestamps).toEqual([]);
461
- });
462
-
463
- it('should read persisted cooldown on restart', async () => {
464
- await recordRunStart(tempDir, 'T-01');
465
- await recordRunEnd(tempDir, 'success');
466
-
467
- // Simulate restart by re-reading
468
- const state = await getRuntimeState(tempDir);
469
- expect(state.lastSuccessfulRunAt).toBeTruthy();
470
- expect(state.principleCooldowns['T-01']).toBeTruthy();
471
- });
472
- });
473
- });