principles-disciple 1.72.0 → 1.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/INSTALL.md +1 -3
  2. package/openclaw.plugin.json +10 -5
  3. package/package.json +17 -19
  4. package/scripts/acceptance-test.mjs +16 -73
  5. package/scripts/sync-plugin.mjs +382 -77
  6. package/src/commands/archive-impl.ts +2 -1
  7. package/src/commands/capabilities.ts +2 -2
  8. package/src/commands/context.ts +2 -2
  9. package/src/commands/disable-impl.ts +2 -1
  10. package/src/commands/evolution-status.ts +16 -16
  11. package/src/commands/export.ts +12 -67
  12. package/src/commands/pain.ts +91 -1
  13. package/src/commands/principle-rollback.ts +2 -1
  14. package/src/commands/promote-impl.ts +7 -43
  15. package/src/commands/rollback-impl.ts +2 -1
  16. package/src/commands/rollback.ts +2 -1
  17. package/src/commands/samples.ts +2 -1
  18. package/src/commands/thinking-os.ts +2 -1
  19. package/src/config/errors.ts +18 -2
  20. package/src/constants/diagnostician.ts +2 -2
  21. package/src/constants/tools.ts +2 -1
  22. package/src/core/__tests__/focus-history.test.ts +210 -0
  23. package/src/core/config.ts +1 -1
  24. package/src/core/correction-cue-learner.ts +2 -136
  25. package/src/core/correction-types.ts +16 -88
  26. package/src/core/dictionary.ts +19 -20
  27. package/src/core/empathy-keyword-matcher.ts +17 -289
  28. package/src/core/empathy-types.ts +18 -229
  29. package/src/core/event-log.ts +29 -132
  30. package/src/core/evolution-reducer.ts +21 -2
  31. package/src/core/evolution-types.ts +76 -464
  32. package/src/core/file-store.ts +80 -0
  33. package/src/core/focus-history.ts +228 -955
  34. package/src/core/local-worker-routing.ts +34 -314
  35. package/src/core/merge-gate-audit.ts +0 -195
  36. package/src/core/migration.ts +0 -1
  37. package/src/core/pain-diagnostic-gate.ts +154 -0
  38. package/src/core/pain-signal.ts +21 -138
  39. package/src/core/pain.ts +15 -88
  40. package/src/core/path-resolver.ts +0 -1
  41. package/src/core/paths.ts +0 -1
  42. package/src/core/pd-task-reconciler.ts +26 -115
  43. package/src/core/pd-task-service.ts +9 -9
  44. package/src/core/pd-task-types.ts +23 -127
  45. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  46. package/src/core/principle-compiler/code-validator.ts +15 -42
  47. package/src/core/principle-compiler/compiler.ts +100 -15
  48. package/src/core/principle-compiler/index.ts +5 -2
  49. package/src/core/principle-compiler/template-generator.ts +4 -104
  50. package/src/core/principle-injection.ts +10 -202
  51. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  52. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  53. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  54. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  55. package/src/core/principle-tree-ledger.ts +8 -6
  56. package/src/core/reflection/reflection-context.ts +14 -109
  57. package/src/core/replay-engine.ts +8 -500
  58. package/src/core/rule-host-helpers.ts +5 -35
  59. package/src/core/rule-host-types.ts +10 -82
  60. package/src/core/rule-host.ts +6 -63
  61. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  62. package/src/core/session-tracker.ts +87 -101
  63. package/src/core/shadow-observation-registry.ts +19 -48
  64. package/src/core/trajectory.ts +3 -1
  65. package/src/core/workflow-funnel-loader.ts +62 -68
  66. package/src/core/workspace-context.ts +46 -0
  67. package/src/core/workspace-dir-service.ts +1 -1
  68. package/src/core/workspace-dir-validation.ts +18 -9
  69. package/src/hooks/AGENTS.md +1 -1
  70. package/src/hooks/gate-block-helper.ts +71 -64
  71. package/src/hooks/gate.ts +183 -31
  72. package/src/hooks/lifecycle.ts +30 -32
  73. package/src/hooks/llm.ts +60 -32
  74. package/src/hooks/pain.ts +297 -103
  75. package/src/hooks/prompt.ts +400 -440
  76. package/src/hooks/subagent.ts +2 -29
  77. package/src/i18n/commands.ts +2 -10
  78. package/src/index.ts +95 -85
  79. package/src/openclaw-sdk.ts +311 -0
  80. package/src/service/central-database.ts +8 -4
  81. package/src/service/evolution-queue-migration.ts +2 -1
  82. package/src/service/evolution-worker.ts +163 -1786
  83. package/src/service/internalization-trigger-adapter.ts +302 -0
  84. package/src/service/keyword-optimization-service.ts +4 -4
  85. package/src/service/monitoring-query-service.ts +1 -215
  86. package/src/service/queue-io.ts +60 -331
  87. package/src/service/runtime-summary-service.ts +59 -16
  88. package/src/service/subagent-workflow/index.ts +0 -41
  89. package/src/service/subagent-workflow/types.ts +9 -120
  90. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  91. package/src/service/workflow-watchdog.ts +0 -43
  92. package/src/types/event-payload.ts +16 -74
  93. package/src/types/event-types.ts +38 -547
  94. package/src/types/hygiene-types.ts +7 -30
  95. package/src/types/principle-tree-schema.ts +20 -222
  96. package/src/types/queue.ts +15 -70
  97. package/src/types/runtime-summary.ts +5 -49
  98. package/src/utils/io.ts +8 -20
  99. package/src/utils/retry.ts +1 -1
  100. package/src/utils/shadow-fingerprint.ts +2 -2
  101. package/src/utils/workspace-resolver.ts +50 -0
  102. package/templates/langs/en/core/AGENTS.md +7 -7
  103. package/templates/langs/en/core/BOOT.md +1 -1
  104. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  105. package/templates/langs/en/principles/THINKING_OS.md +3 -2
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  107. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  108. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  109. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  110. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  111. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  112. package/templates/langs/en/skills/evolve-task/SKILL.md +3 -3
  113. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  114. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  115. package/templates/langs/en/skills/pd-mentor/SKILL.md +2 -3
  116. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  117. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  118. package/templates/langs/zh/core/AGENTS.md +7 -7
  119. package/templates/langs/zh/core/BOOT.md +1 -1
  120. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  121. package/templates/langs/zh/principles/THINKING_OS.md +3 -2
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  127. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  128. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  129. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  130. package/templates/langs/zh/skills/evolve-task/SKILL.md +4 -4
  131. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  132. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  133. package/templates/langs/zh/skills/pd-mentor/SKILL.md +2 -3
  134. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  135. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  136. package/tests/build-artifacts.test.ts +1 -3
  137. package/tests/commands/evolution-status.test.ts +0 -118
  138. package/tests/core/bootstrap-rules.test.ts +1 -1
  139. package/tests/core/config.test.ts +1 -1
  140. package/tests/core/event-log.test.ts +35 -0
  141. package/tests/core/evolution-engine.test.ts +610 -0
  142. package/tests/core/file-store.test.ts +102 -0
  143. package/tests/core/focus-history.test.ts +203 -11
  144. package/tests/core/merge-gate-audit.test.ts +2 -169
  145. package/tests/core/migration.test.ts +7 -7
  146. package/tests/core/model-deployment-registry.test.ts +7 -1
  147. package/tests/core/model-training-registry.test.ts +19 -0
  148. package/tests/core/observability.test.ts +0 -1
  149. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  150. package/tests/core/pain.test.ts +0 -1
  151. package/tests/core/path-resolver.test.ts +1 -1
  152. package/tests/core/paths-refactor.test.ts +0 -22
  153. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  154. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  155. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  156. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  157. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  158. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  159. package/tests/core/reflection-context.test.ts +0 -14
  160. package/tests/core/replay-engine.test.ts +127 -215
  161. package/tests/core/rule-host-helpers.test.ts +2 -2
  162. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  163. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  164. package/tests/core/workspace-context.test.ts +2 -2
  165. package/tests/core/workspace-dir-validation.test.ts +8 -1
  166. package/tests/core-anti-growth.test.ts +191 -0
  167. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  168. package/tests/hooks/confirm-first-removal.test.ts +188 -0
  169. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  170. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  171. package/tests/hooks/gate-no-path-write-tool.test.ts +172 -0
  172. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  173. package/tests/hooks/pain.test.ts +269 -12
  174. package/tests/hooks/prompt-characterization.test.ts +500 -0
  175. package/tests/hooks/prompt-size-guard.test.ts +32 -17
  176. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  177. package/tests/index.test.ts +94 -1
  178. package/tests/integration/auto-entry-gate.test.ts +248 -0
  179. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  180. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  181. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  182. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  183. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  184. package/tests/service/central-database.test.ts +457 -0
  185. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  186. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  187. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  188. package/tests/service/monitoring-query-service.test.ts +1 -47
  189. package/tests/service/queue-io.test.ts +1 -62
  190. package/tests/service/runtime-summary-service.test.ts +3 -1
  191. package/tests/service/workflow-watchdog.test.ts +0 -91
  192. package/tests/utils/file-lock.test.ts +5 -3
  193. package/tests/utils/session-key.test.ts +52 -0
  194. package/tests/utils/subagent-probe.test.ts +48 -1
  195. package/vitest.config.ts +4 -11
  196. package/.planning/codebase/ARCHITECTURE.md +0 -157
  197. package/.planning/codebase/CONCERNS.md +0 -145
  198. package/.planning/codebase/CONVENTIONS.md +0 -148
  199. package/.planning/codebase/INTEGRATIONS.md +0 -81
  200. package/.planning/codebase/STACK.md +0 -87
  201. package/.planning/codebase/STRUCTURE.md +0 -193
  202. package/.planning/codebase/TESTING.md +0 -243
  203. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  204. package/docs/COMMAND_REFERENCE.md +0 -76
  205. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  206. package/scripts/build-web.mjs +0 -46
  207. package/scripts/diagnose-nocturnal.mjs +0 -537
  208. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  209. package/src/commands/nocturnal-review.ts +0 -322
  210. package/src/commands/nocturnal-rollout.ts +0 -790
  211. package/src/commands/nocturnal-train.ts +0 -986
  212. package/src/commands/pd-reflect.ts +0 -88
  213. package/src/core/adaptive-thresholds.ts +0 -478
  214. package/src/core/diagnostician-task-store.ts +0 -192
  215. package/src/core/nocturnal-arbiter.ts +0 -715
  216. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  217. package/src/core/nocturnal-artificer.ts +0 -257
  218. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  219. package/src/core/nocturnal-compliance.ts +0 -1146
  220. package/src/core/nocturnal-dataset.ts +0 -763
  221. package/src/core/nocturnal-executability.ts +0 -428
  222. package/src/core/nocturnal-export.ts +0 -499
  223. package/src/core/nocturnal-paths.ts +0 -240
  224. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  225. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  226. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  227. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  228. package/src/core/nocturnal-trinity-types.ts +0 -218
  229. package/src/core/nocturnal-trinity.ts +0 -2680
  230. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  231. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  232. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  233. package/src/http/principles-console-route.ts +0 -709
  234. package/src/service/central-health-service.ts +0 -49
  235. package/src/service/central-overview-service.ts +0 -138
  236. package/src/service/control-ui-query-service.ts +0 -900
  237. package/src/service/cooldown-strategy.ts +0 -97
  238. package/src/service/evolution-pain-context.ts +0 -79
  239. package/src/service/evolution-query-service.ts +0 -407
  240. package/src/service/health-query-service.ts +0 -1038
  241. package/src/service/nocturnal-config.ts +0 -214
  242. package/src/service/nocturnal-runtime.ts +0 -734
  243. package/src/service/nocturnal-service.ts +0 -1605
  244. package/src/service/nocturnal-target-selector.ts +0 -545
  245. package/src/service/sleep-cycle.ts +0 -157
  246. package/src/service/startup-reconciler.ts +0 -112
  247. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  248. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  249. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  250. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  251. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  252. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  253. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  254. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  255. package/src/tools/write-pain-flag.ts +0 -215
  256. package/templates/langs/en/skills/plan-script/SKILL.md +0 -32
  257. package/templates/langs/zh/skills/plan-script/SKILL.md +0 -32
  258. package/tests/commands/nocturnal-review.test.ts +0 -448
  259. package/tests/commands/nocturnal-train.test.ts +0 -97
  260. package/tests/commands/pd-reflect.test.ts +0 -49
  261. package/tests/core/adaptive-thresholds.test.ts +0 -261
  262. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  263. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  264. package/tests/core/nocturnal-artificer.test.ts +0 -241
  265. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  266. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  267. package/tests/core/nocturnal-compliance.test.ts +0 -646
  268. package/tests/core/nocturnal-dataset.test.ts +0 -892
  269. package/tests/core/nocturnal-e2e.test.ts +0 -234
  270. package/tests/core/nocturnal-executability.test.ts +0 -357
  271. package/tests/core/nocturnal-export.test.ts +0 -517
  272. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  273. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  274. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  275. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  276. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  277. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  278. package/tests/core/pain-auto-repair.test.ts +0 -96
  279. package/tests/core/pain-integration.test.ts +0 -510
  280. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  281. package/tests/http/principles-console-route.test.ts +0 -162
  282. package/tests/integration/chaos-resilience.test.ts +0 -348
  283. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  284. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  285. package/tests/service/control-ui-query-service.test.ts +0 -121
  286. package/tests/service/cooldown-strategy.test.ts +0 -164
  287. package/tests/service/data-endpoints-regression.test.ts +0 -834
  288. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  289. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  290. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  291. package/tests/service/nocturnal-runtime.test.ts +0 -473
  292. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  293. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  294. package/tests/service/startup-reconciler.test.ts +0 -148
  295. package/tests/tools/write-pain-flag.test.ts +0 -358
  296. package/ui/src/App.tsx +0 -45
  297. package/ui/src/api.ts +0 -220
  298. package/ui/src/charts.tsx +0 -955
  299. package/ui/src/components/ErrorState.tsx +0 -6
  300. package/ui/src/components/Loading.tsx +0 -13
  301. package/ui/src/components/ProtectedRoute.tsx +0 -12
  302. package/ui/src/components/Shell.tsx +0 -91
  303. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  304. package/ui/src/components/index.ts +0 -5
  305. package/ui/src/context/auth.tsx +0 -80
  306. package/ui/src/context/theme.tsx +0 -66
  307. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  308. package/ui/src/i18n/ui.ts +0 -473
  309. package/ui/src/main.tsx +0 -16
  310. package/ui/src/pages/EvolutionPage.tsx +0 -333
  311. package/ui/src/pages/FeedbackPage.tsx +0 -138
  312. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  313. package/ui/src/pages/LoginPage.tsx +0 -89
  314. package/ui/src/pages/OverviewPage.tsx +0 -599
  315. package/ui/src/pages/SamplesPage.tsx +0 -174
  316. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  317. package/ui/src/styles.css +0 -2020
  318. package/ui/src/types.ts +0 -384
  319. package/ui/src/utils/format.ts +0 -15
@@ -1,986 +0,0 @@
1
- /**
2
- * Nocturnal Training Command Handler
3
- * ==================================
4
- *
5
- * Plugin command handler for nocturnal training operations.
6
- * Provides commands for:
7
- * - create-experiment: Create a new training experiment
8
- * - show-experiment: Show experiment details
9
- * - import-result: Import trainer result
10
- * - attach-eval: Attach benchmark eval to checkpoint
11
- * - show-lineage: Show checkpoint lineage
12
- * - list-experiments: List all experiments
13
- * - list-checkpoints: List all checkpoints
14
- *
15
- * Usage:
16
- * /nocturnal-train create-experiment --backend=peft-trl-orpo --family=<model-family> [--hyperparams=...]
17
- * /nocturnal-train show-experiment <experimentId>
18
- * /nocturnal-train import-result <experimentId> --result=<path-or-json>
19
- * /nocturnal-train attach-eval <checkpointId> --benchmark-id=<id> --delta=<number> --verdict=<pass|fail>
20
- * /nocturnal-train show-lineage <checkpointId>
21
- * /nocturnal-train list-experiments
22
- * /nocturnal-train list-checkpoints
23
- */
24
-
25
- import * as path from 'path';
26
- import * as fs from 'fs';
27
- import { execFileSync, spawn } from 'child_process';
28
- import { fileURLToPath } from 'url';
29
- import { atomicWriteFileSync } from '../utils/io.js';
30
- import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
31
- import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
32
- import {
33
- type TrainerBackendKind,
34
- type HardwareTier,
35
- type TrainingExperimentResult,
36
- } from '../core/external-training-contract.js';
37
- import {
38
- TrainingProgram,
39
- } from '../core/training-program.js';
40
- import {
41
- listTrainingRuns,
42
- getTrainingRun,
43
- listCheckpoints,
44
- getCheckpoint,
45
- getCheckpointLineage,
46
- getTrainingRegistryStats,
47
- } from '../core/model-training-registry.js';
48
- import { getDeployment } from '../core/model-deployment-registry.js';
49
-
50
- function isZh(ctx: PluginCommandContext): boolean {
51
- return String(ctx.config?.language || 'en').startsWith('zh');
52
- }
53
-
54
- const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
55
- const REPO_ROOT = path.resolve(MODULE_DIR, '..', '..', '..', '..');
56
- const TRAINER_SCRIPTS_DIR = path.join(REPO_ROOT, 'scripts', 'nocturnal', 'trainer');
57
- const BENCHMARK_SCRIPT_PATH = path.join(REPO_ROOT, 'scripts', 'nocturnal', 'run-benchmark.ts');
58
-
59
- /**
60
- * Parse backend from argument string.
61
- */
62
- function parseBackend(arg: string | undefined): TrainerBackendKind {
63
- if (!arg) return 'peft-trl-orpo';
64
- const valid: TrainerBackendKind[] = ['peft-trl-orpo', 'unsloth-orpo', 'dry-run'];
65
- if (valid.includes(arg as TrainerBackendKind)) {
66
- return arg as TrainerBackendKind;
67
- }
68
- return 'peft-trl-orpo';
69
- }
70
-
71
- /**
72
- * Parse hardware tier from argument string.
73
- */
74
- function parseHardwareTier(arg: string | undefined): HardwareTier {
75
- if (!arg) return 'consumer-gpu';
76
- const valid: HardwareTier[] = ['consumer-gpu', 'small-gpu', 'cpu-experimental'];
77
- if (valid.includes(arg as HardwareTier)) {
78
- return arg as HardwareTier;
79
- }
80
- return 'consumer-gpu';
81
- }
82
-
83
- /**
84
- * Format training run for display.
85
- */
86
- function formatTrainingRun(run: ReturnType<typeof getTrainingRun>, zh: boolean): string {
87
- if (!run) return zh ? '未找到' : 'Not found';
88
- const lines = [
89
- `ID: ${run.trainRunId.substring(0, 8)}...`,
90
- `Family: ${run.targetModelFamily}`,
91
- `Status: ${run.status}`,
92
- `Dataset FP: ${run.datasetFingerprint.substring(0, 12)}...`,
93
- `Created: ${new Date(run.createdAt).toLocaleString()}`,
94
- ];
95
- if (run.completedAt) lines.push(`Completed: ${new Date(run.completedAt).toLocaleString()}`);
96
- if (run.failureReason) lines.push(`Failure: ${run.failureReason}`);
97
- if (run.checkpointIds.length > 0) {
98
- lines.push(`Checkpoints: ${run.checkpointIds.length}`);
99
- }
100
- return lines.join('\n ');
101
- }
102
-
103
- export async function handleNocturnalTrainCommand(ctx: PluginCommandContext): Promise<PluginCommandResult> {
104
- const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'nocturnal-train');
105
- const zh = isZh(ctx);
106
- const args = (ctx.args || '').trim();
107
- const parts = args.split(/\s+/).filter(Boolean);
108
- const [subcommand = 'help'] = parts;
109
-
110
- const backendArg = parts.find((p) => p.startsWith('--backend='))?.split('=')[1];
111
- const familyArg = parts.find((p) => p.startsWith('--family='))?.split('=')[1];
112
- const hardwareTierArg = parts.find((p) => p.startsWith('--tier='))?.split('=')[1];
113
- const datasetExportIdArg = parts.find((p) => p.startsWith('--dataset='))?.split('=')[1];
114
- const benchmarkExportIdArg = parts.find((p) => p.startsWith('--benchmark='))?.split('=')[1];
115
- const resultArg = parts.find((p) => p.startsWith('--result='))?.split('=')[1];
116
- const checkpointIdArg = parts.find((p) => p.startsWith('--checkpoint-id='))?.split('=')[1];
117
- const benchmarkIdArg = parts.find((p) => p.startsWith('--benchmark-id='))?.split('=')[1];
118
- const deltaArg = parts.find((p) => p.startsWith('--delta='))?.split('=')[1];
119
- const verdictArg = parts.find((p) => p.startsWith('--verdict='))?.split('=')[1];
120
- const modeArg = parts.find((p) => p.startsWith('--mode='))?.split('=')[1];
121
- const baselineScoreArg = parts.find((p) => p.startsWith('--baseline='))?.split('=')[1];
122
- const candidateScoreArg = parts.find((p) => p.startsWith('--candidate='))?.split('=')[1];
123
-
124
- try {
125
- // ── Help ────────────────────────────────────────────────────────────────
126
- if (subcommand === 'help' || subcommand === '--help') {
127
- return {
128
- text: zh
129
- ? ` nocturnal-train 命令帮助
130
-
131
- 用法:
132
- /nocturnal-train create-experiment --backend=<backend> --family=<model-family> [--dataset=<export-id>] [--benchmark=<export-id>] [--run]
133
- /nocturnal-train show-experiment <experimentId>
134
- /nocturnal-train import-result <experimentId> --result=<path-or-json>
135
- /nocturnal-train attach-eval <checkpointId> --benchmark-id=<id> [--baseline-ref=<checkpointId>] [--delta=<number>] [--verdict=<pass|fail>] [--run-benchmark]
136
- /nocturnal-train show-lineage <checkpointId>
137
- /nocturnal-train list-experiments
138
- /nocturnal-train list-checkpoints [--family=<model-family>]
139
-
140
- 示例:
141
- /nocturnal-train create-experiment --backend=peft-trl-orpo --family=qwen2.5-7b-reader --dataset=export-123 --benchmark=bench-456 --run
142
- /nocturnal-train show-experiment exp-abc123
143
- /nocturnal-train import-result exp-abc123 --result=.state/nocturnal/evals/result-exp-abc123.json
144
- /nocturnal-train attach-eval ckpt-xyz --benchmark-id=bench-001 --delta=0.08 --verdict=pass --run-benchmark
145
- /nocturnal-train show-lineage ckpt-xyz
146
- /nocturnal-train list-checkpoints --family=qwen2.5-7b-reader
147
-
148
- 后端选项:
149
- peft-trl-orpo - PEFT + TRL ORPO (生产用)
150
- unsloth-orpo - Unsloth 加速 ORPO
151
- dry-run - 仅验证,不实际训练
152
-
153
- 硬件层级:
154
- consumer-gpu - RTX 4090 24GB (默认)
155
- small-gpu - 8-16GB VRAM
156
- cpu-experimental - 仅 dry-run`
157
- : ` nocturnal-train command help
158
-
159
- Usage:
160
- /nocturnal-train create-experiment --backend=<backend> --family=<model-family> [--dataset=<export-id>] [--benchmark=<export-id>]
161
- /nocturnal-train show-experiment <experimentId>
162
- /nocturnal-train import-result <experimentId> --result=<path-or-json>
163
- /nocturnal-train attach-eval <checkpointId> --benchmark-id=<id> --delta=<number> --verdict=<pass|fail> [--baseline=<score>] [--candidate=<score>]
164
- /nocturnal-train show-lineage <checkpointId>
165
- /nocturnal-train list-experiments
166
- /nocturnal-train list-checkpoints [--family=<model-family>]
167
-
168
- Examples:
169
- /nocturnal-train create-experiment --backend=peft-trl-orpo --family=qwen2.5-7b-reader --dataset=export-123 --benchmark=bench-456
170
- /nocturnal-train show-experiment exp-abc123
171
- /nocturnal-train import-result exp-abc123 --result=.state/nocturnal/evals/result-exp-abc123.json
172
- /nocturnal-train attach-eval ckpt-xyz --benchmark-id=bench-001 --delta=0.08 --verdict=pass
173
- /nocturnal-train show-lineage ckpt-xyz
174
- /nocturnal-train list-checkpoints --family=qwen2.5-7b-reader
175
-
176
- Backend options:
177
- peft-trl-orpo - PEFT + TRL ORPO (production)
178
- unsloth-orpo - Unsloth accelerated ORPO
179
- dry-run - Validation only, no real training
180
-
181
- Hardware tiers:
182
- consumer-gpu - RTX 4090 24GB (default)
183
- small-gpu - 8-16GB VRAM
184
- cpu-experimental - dry-run only`,
185
- };
186
- }
187
-
188
- // ── Create Experiment ─────────────────────────────────────────────────
189
- if (subcommand === 'create-experiment') {
190
- if (!familyArg) {
191
- return { text: zh ? '错误: 需要 --family 参数' : 'Error: --family is required' };
192
- }
193
-
194
- const backend = parseBackend(backendArg);
195
- const hardwareTier = parseHardwareTier(hardwareTierArg);
196
- const runNow = args.includes('--run');
197
-
198
- // Find ORPO export if dataset not specified
199
- let datasetExportId = datasetExportIdArg;
200
- let datasetExportPath = '';
201
- if (!datasetExportId) {
202
- // Try to find latest ORPO export
203
- const exportsDir = path.join(workspaceDir, '.state', 'exports', 'orpo');
204
- if (fs.existsSync(exportsDir)) {
205
- const files = fs.readdirSync(exportsDir).filter((f) => f.endsWith('-manifest.json'));
206
- if (files.length > 0) {
207
- const manifest = JSON.parse(fs.readFileSync(path.join(exportsDir, files[0]), 'utf-8'));
208
- datasetExportId = manifest.exportId;
209
- datasetExportPath = manifest.exportPath;
210
- }
211
- }
212
- if (!datasetExportId) {
213
- return {
214
- text: zh
215
- ? '错误: 未找到 ORPO 导出。请先运行 /pd-nocturnal-review 导出数据。'
216
- : 'Error: No ORPO export found. Run /pd-nocturnal-review to export data first.',
217
- };
218
- }
219
- } else {
220
- datasetExportPath = path.join(workspaceDir, '.state', 'exports', 'orpo', `${datasetExportId}.jsonl`);
221
- }
222
-
223
- // Get dataset fingerprint
224
- let datasetFingerprint = 'unknown';
225
- const manifestPath = path.join(workspaceDir, '.state', 'exports', 'orpo', `${datasetExportId}-manifest.json`);
226
- if (fs.existsSync(manifestPath)) {
227
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
228
- if (!manifest.datasetFingerprint) {
229
- return {
230
- text: zh
231
- ? `错误: manifest 文件缺少 datasetFingerprint: ${manifestPath}`
232
- : `Error: manifest missing datasetFingerprint: ${manifestPath}`,
233
- };
234
- }
235
- ({ datasetFingerprint } = manifest);
236
- }
237
-
238
- const benchmarkExportId = benchmarkExportIdArg || datasetExportId || 'benchmark-default';
239
- const outputDir = path.join(workspaceDir, '.state', 'nocturnal', 'checkpoints');
240
-
241
- const program = new TrainingProgram(workspaceDir);
242
- const createResult = program.createExperiment({
243
- backend,
244
- targetWorkerProfile: 'local-reader', // Phase 7 only
245
- targetModelFamily: familyArg,
246
- hardwareTier,
247
- datasetExportId,
248
- datasetExportPath,
249
- datasetFingerprint,
250
- benchmarkExportId,
251
- outputDir,
252
- });
253
-
254
- // --- Write spec files for the manual chain ---
255
- // The trainer reads spec from scripts/nocturnal/trainer/experiment-<id>.json.
256
- // import-result reads spec from .state/nocturnal/checkpoints/experiment-<id>.json.
257
- // Both must be written so the manual create-experiment -> trainer -> import-result chain works.
258
- const {spec} = createResult;
259
- const trainerSpecPath = path.join(TRAINER_SCRIPTS_DIR, `experiment-${spec.experimentId}.json`);
260
- const workspaceSpecPath = path.join(workspaceDir, '.state', 'nocturnal', 'checkpoints', `experiment-${spec.experimentId}.json`);
261
- const trainerSpecDir = path.dirname(trainerSpecPath);
262
- const workspaceCheckpointsDir = path.dirname(workspaceSpecPath);
263
- if (!fs.existsSync(trainerSpecDir)) {
264
- fs.mkdirSync(trainerSpecDir, { recursive: true });
265
- }
266
- if (!fs.existsSync(workspaceCheckpointsDir)) {
267
- fs.mkdirSync(workspaceCheckpointsDir, { recursive: true });
268
- }
269
- atomicWriteFileSync(trainerSpecPath, JSON.stringify(spec, null, 2));
270
- atomicWriteFileSync(workspaceSpecPath, JSON.stringify(spec, null, 2));
271
-
272
- // --- Auto-run mode: execute trainer immediately ---
273
- // This closes the gap in the create-experiment -> trainer -> import-result chain.
274
- // NOTE: This blocks until training completes (could be minutes).
275
- if (runNow) {
276
- const baseDir = TRAINER_SCRIPTS_DIR;
277
- const scriptPath = path.join(baseDir, 'main.py');
278
- const specPath = path.join(baseDir, `experiment-${spec.experimentId}.json`);
279
-
280
- const resultFilePath = path.join(spec.outputDir, `result-${spec.experimentId}.json`);
281
-
282
- // Write spec file
283
- const specDir = path.dirname(specPath);
284
- if (!fs.existsSync(specDir)) {
285
- fs.mkdirSync(specDir, { recursive: true });
286
- }
287
- atomicWriteFileSync(specPath, JSON.stringify(spec, null, 2));
288
-
289
-
290
-
291
- let trainerResult!: TrainingExperimentResult;
292
-
293
- try {
294
- if (spec.backend === 'dry-run') {
295
- trainerResult = {
296
- experimentId: spec.experimentId,
297
- backend: 'dry-run',
298
- status: 'dry_run' as const,
299
- targetWorkerProfile: spec.targetWorkerProfile,
300
- targetModelFamily: spec.targetModelFamily,
301
- datasetFingerprint: spec.datasetFingerprint,
302
- configFingerprint: spec.configFingerprint,
303
- codeHash: spec.codeHash,
304
- createdAt: new Date().toISOString(),
305
- };
306
- } else {
307
- // Execute trainer using spawn (streaming, no full log buffering).
308
- // stdout is collected into a fixed-size buffer (1MB) to avoid OOM.
309
- // stderr is piped directly to parent stderr to avoid memory accumulation.
310
- const timeoutMs = (spec.budget.maxWallClockMinutes * 60 * 1000) + 30000;
311
- const pythonExe = process.platform === 'win32' ? 'python' : 'python3';
312
- const MAX_STDOUT_BUFFER = 1 * 1024 * 1024; // 1MB cap
313
-
314
- trainerResult = await new Promise((resolve, reject) => {
315
- const proc = spawn(pythonExe, [scriptPath, '--spec', specPath, '--output-dir', outputDir], {
316
- timeout: timeoutMs,
317
- });
318
-
319
- // Collect stdout with size cap to prevent OOM
320
- const stdoutChunks: Buffer[] = [];
321
- let stdoutSize = 0;
322
-
323
- proc.stdout.on('data', (chunk: Buffer) => {
324
- const remaining = MAX_STDOUT_BUFFER - stdoutSize;
325
- if (remaining > 0) {
326
- stdoutChunks.push(chunk.slice(0, remaining));
327
- stdoutSize += Math.min(chunk.length, remaining);
328
- }
329
- });
330
-
331
- // Pipe stderr directly — training logs can be large, don't buffer
332
- proc.stderr.pipe(process.stderr);
333
-
334
- const timer = setTimeout(() => {
335
- proc.kill();
336
- reject(new Error(`Trainer timed out after ${timeoutMs}ms`));
337
- }, timeoutMs);
338
- timer.unref(); // Don't keep process alive for timeout
339
-
340
- proc.on('close', (code) => {
341
- clearTimeout(timer);
342
- if (code === 0) {
343
- const stdout = Buffer.concat(stdoutChunks).toString('utf-8');
344
- const trimmed = stdout.trim();
345
- if (trimmed) {
346
- try {
347
- resolve(JSON.parse(trimmed));
348
- return;
349
- } catch {
350
- // fall through to result file
351
- }
352
- }
353
- // Fallback to result file
354
- if (fs.existsSync(resultFilePath)) {
355
- try {
356
- resolve(JSON.parse(fs.readFileSync(resultFilePath, 'utf-8')));
357
- return;
358
- } catch {
359
- reject(new Error(`Trainer stdout was not valid JSON and result file also invalid: ${resultFilePath}`));
360
- return;
361
- }
362
- }
363
- reject(new Error(`Trainer produced no parseable stdout and no result file found at: ${resultFilePath}`));
364
- } else {
365
- // Non-zero exit: try result file as last resort
366
- if (fs.existsSync(resultFilePath)) {
367
- try {
368
- resolve(JSON.parse(fs.readFileSync(resultFilePath, 'utf-8')));
369
- } catch {
370
- reject(new Error(`Trainer exited with code ${code} and result file was invalid`));
371
- }
372
- } else {
373
- reject(new Error(`Trainer exited with code ${code} and no result file found`));
374
- }
375
- }
376
- });
377
-
378
- proc.on('error', (err) => {
379
- clearTimeout(timer);
380
- reject(new Error(`Trainer spawn failed: ${err instanceof Error ? err.message : String(err)}`));
381
- });
382
- });
383
- }
384
- } catch (err: unknown) {
385
- return {
386
- text: zh
387
- ? `❌ 训练执行失败: ${err instanceof Error ? err.message : String(err)}\n\n训练 Run ID: ${createResult.trainRunId}\n请检查 trainer 输出或使用 dry-run 后端重试。`
388
- : `❌ Trainer execution failed: ${err instanceof Error ? err.message : String(err)}\n\nTraining Run ID: ${createResult.trainRunId}\nCheck trainer output or retry with --backend=dry-run.`,
389
- };
390
- } finally {
391
- // Clean up spec file
392
- if (fs.existsSync(specPath)) {
393
- fs.unlinkSync(specPath);
394
- }
395
- }
396
-
397
- // Process trainer result (register checkpoint)
398
- // dry_run returns null (no checkpoint); other statuses throw on error
399
-
400
-
401
- let processed: { checkpointId: string; checkpointRef: string } | null;
402
- try {
403
- processed = program.processResult({
404
- spec: createResult.spec,
405
- trainRunId: createResult.trainRunId,
406
- result: trainerResult,
407
- });
408
- } catch (err: unknown) {
409
- return {
410
- text: zh
411
- ? `❌ 结果导入失败: ${err instanceof Error ? err.message : String(err)}`
412
- : `❌ Result import failed: ${err instanceof Error ? err.message : String(err)}`,
413
- };
414
- }
415
-
416
- if (processed === null) {
417
- // dry_run completed with no checkpoint — this is a non-error outcome
418
- return {
419
- text: zh
420
- ? `✅ Dry-run 完成(未产生 checkpoint)
421
- 实验 ID: ${createResult.spec.experimentId}
422
- 训练 Run ID: ${createResult.trainRunId}
423
- 状态: ${trainerResult.status}
424
-
425
- 下一步:
426
- 若需产生可部署的 checkpoint,请使用 --backend=peft-trl-orpo 或 --backend=unsloth-orpo 重试。`
427
- : `✅ Dry-run complete (no checkpoint produced)
428
- Experiment ID: ${createResult.spec.experimentId}
429
- Training Run ID: ${createResult.trainRunId}
430
- Status: ${trainerResult.status}
431
-
432
- Next steps:
433
- To produce a deployable checkpoint, retry with --backend=peft-trl-orpo or --backend=unsloth-orpo.`,
434
- };
435
- }
436
-
437
- return {
438
- text: zh
439
- ? `✅ 训练完成
440
- 实验 ID: ${createResult.spec.experimentId}
441
- 训练 Run ID: ${createResult.trainRunId}
442
- Checkpoint ID: ${processed.checkpointId}
443
- 状态: ${trainerResult.status}
444
- ${trainerResult.failureReason ? `失败原因: ${trainerResult.failureReason}` : ''}
445
-
446
- 下一步:
447
- 1. 运行评估: /nocturnal-train attach-eval ${processed.checkpointId} --benchmark-id=<id> --delta=<number> --verdict=<pass|fail> --run-benchmark
448
- 2. 查看检查点: /nocturnal-train show-lineage ${processed.checkpointId}`
449
- : `✅ Training complete
450
- Experiment ID: ${createResult.spec.experimentId}
451
- Training Run ID: ${createResult.trainRunId}
452
- Checkpoint ID: ${processed.checkpointId}
453
- Status: ${trainerResult.status}
454
- ${trainerResult.failureReason ? `Failure: ${trainerResult.failureReason}` : ''}
455
-
456
- Next steps:
457
- 1. Run eval: /nocturnal-train attach-eval ${processed.checkpointId} --benchmark-id=<id> --delta=<number> --verdict=<pass|fail> --run-benchmark
458
- 2. View checkpoint: /nocturnal-train show-lineage ${processed.checkpointId}`,
459
- };
460
- }
461
-
462
- return {
463
- text: zh
464
- ? `✅ 实验已创建
465
- 实验 ID: ${createResult.spec.experimentId}
466
- 后端: ${createResult.spec.backend}
467
- 模型家族: ${createResult.spec.targetModelFamily}
468
- 硬件层级: ${createResult.spec.hardwareTier}
469
- 数据集: ${createResult.spec.datasetExportId}
470
- 输出目录: ${createResult.spec.outputDir}
471
- 训练 Run ID: ${createResult.trainRunId}
472
-
473
- 下一步:
474
- 1. 运行外部训练器: python "${path.join(TRAINER_SCRIPTS_DIR, 'main.py')}" --spec "${trainerSpecPath}" --output-dir ${outputDir}
475
- 2. 导入结果: /nocturnal-train import-result ${createResult.spec.experimentId} --result=<path>
476
- 3. 附加评估: /nocturnal-train attach-eval <checkpointId> --benchmark-id=<id> --delta=<number> --verdict=<pass|fail>
477
- 4. 手动链路 spec 已写入:
478
- - ${trainerSpecPath}
479
- - ${workspaceSpecPath}`
480
- : `✅ Experiment created
481
- Experiment ID: ${createResult.spec.experimentId}
482
- Backend: ${createResult.spec.backend}
483
- Model Family: ${createResult.spec.targetModelFamily}
484
- Hardware Tier: ${createResult.spec.hardwareTier}
485
- Dataset: ${createResult.spec.datasetExportId}
486
- Output Dir: ${createResult.spec.outputDir}
487
- Training Run ID: ${createResult.trainRunId}
488
-
489
- Next steps:
490
- 1. Run external trainer: python "${path.join(TRAINER_SCRIPTS_DIR, 'main.py')}" --spec "${trainerSpecPath}" --output-dir ${outputDir}
491
- 2. Import result: /nocturnal-train import-result ${createResult.spec.experimentId} --result=<path>
492
- 3. Attach eval: /nocturnal-train attach-eval <checkpointId> --benchmark-id=<id> --delta=<number> --verdict=<pass|fail>
493
- 4. Durable spec files written to:
494
- - ${trainerSpecPath}
495
- - ${workspaceSpecPath}`,
496
- };
497
- }
498
-
499
- // ── Show Experiment ───────────────────────────────────────────────────
500
- if (subcommand === 'show-experiment') {
501
- const [, experimentId] = parts;
502
- if (!experimentId) {
503
- return { text: zh ? '错误: 需要实验 ID' : 'Error: experiment ID required' };
504
- }
505
-
506
- const runs = listTrainingRuns(workspaceDir, { targetModelFamily: undefined });
507
- const run = runs.find((r) => r.trainRunId.startsWith(experimentId) || r.trainRunId === experimentId);
508
-
509
- if (!run) {
510
- return { text: zh ? `未找到实验: ${experimentId}` : `Experiment not found: ${experimentId}` };
511
- }
512
-
513
- return { text: formatTrainingRun(run, zh) };
514
- }
515
-
516
- // ── Import Result ─────────────────────────────────────────────────────
517
- if (subcommand === 'import-result') {
518
- const [, experimentId] = parts;
519
- if (!experimentId) {
520
- return { text: zh ? '错误: 需要实验 ID' : 'Error: experiment ID required' };
521
- }
522
-
523
- // Get result from argument or file
524
- let resultJson = resultArg;
525
- if (resultJson && fs.existsSync(resultJson)) {
526
- resultJson = fs.readFileSync(resultJson, 'utf-8');
527
- }
528
- if (!resultJson) {
529
- // Try to find result file
530
- const resultPath = path.join(workspaceDir, '.state', 'nocturnal', 'checkpoints', `result-${experimentId}.json`);
531
- if (fs.existsSync(resultPath)) {
532
- resultJson = fs.readFileSync(resultPath, 'utf-8');
533
- } else {
534
- return {
535
- text: zh
536
- ? `错误: 未找到结果文件。请使用 --result 参数指定路径或 JSON 内容。`
537
- : `Error: Result not found. Use --result to specify path or JSON content.`,
538
- };
539
- }
540
- }
541
-
542
-
543
- let result: any;
544
- try {
545
- result = JSON.parse(resultJson);
546
- } catch {
547
- return { text: zh ? '错误: 无效的 JSON 格式' : 'Error: Invalid JSON format' };
548
- }
549
-
550
- // Find the training run
551
- const runs = listTrainingRuns(workspaceDir);
552
- const run = runs.find(
553
- (r) => r.trainRunId === result.trainRunId || r.trainRunId.startsWith(experimentId)
554
- );
555
-
556
- if (!run) {
557
- return { text: zh ? `错误: 未找到训练 Run: ${result.trainRunId}` : `Error: Training run not found: ${result.trainRunId}` };
558
- }
559
-
560
- // Validate spec exists
561
- const specPath = path.join(workspaceDir, '.state', 'nocturnal', 'checkpoints', `experiment-${experimentId}.json`);
562
- if (!fs.existsSync(specPath)) {
563
- return {
564
- text: zh
565
- ? `错误: 未找到实验 spec 文件: ${specPath}`
566
- : `Error: Experiment spec not found: ${specPath}`,
567
- };
568
- }
569
- const spec = JSON.parse(fs.readFileSync(specPath, 'utf-8'));
570
-
571
- // Process the result
572
- const program = new TrainingProgram(workspaceDir);
573
-
574
-
575
- let processed: { checkpointId: string; checkpointRef: string } | null;
576
- try {
577
- processed = program.processResult({
578
- spec,
579
- trainRunId: run.trainRunId,
580
- result,
581
- });
582
- } catch (err: unknown) {
583
- return {
584
- text: zh
585
- ? `❌ 导入失败: ${err instanceof Error ? err.message : String(err)}`
586
- : `❌ Import failed: ${err instanceof Error ? err.message : String(err)}`,
587
- };
588
- }
589
-
590
- if (processed === null) {
591
- // dry_run: non-error outcome with no checkpoint
592
- return {
593
- text: zh
594
- ? `✅ Dry-run 结果已导入(无 checkpoint)
595
- Status: ${result.status}
596
- 训练 Run: ${run.trainRunId}
597
-
598
- 若需产生可部署的 checkpoint,请使用 --backend=peft-trl-orpo 或 --backend=unsloth-orpo 重试。`
599
- : `✅ Dry-run result imported (no checkpoint)
600
- Status: ${result.status}
601
- Training Run: ${run.trainRunId}
602
-
603
- To produce a deployable checkpoint, retry with --backend=peft-trl-orpo or --backend=unsloth-orpo.`,
604
- };
605
- }
606
-
607
- return {
608
- text: zh
609
- ? `✅ 结果已导入
610
- Status: ${result.status}
611
- Checkpoint ID: ${processed.checkpointId}
612
- Checkpoint Ref: ${processed.checkpointRef}
613
- ${result.artifact ? `Artifact: ${result.artifact.artifactPath}` : ''}
614
- ${result.metrics ? `Wall Time: ${result.metrics.wallClockMinutes} min` : ''}
615
- ${result.failureReason ? `Failure: ${result.failureReason}` : ''}
616
-
617
- 下一步:
618
- 1. 运行评估: /nocturnal-train attach-eval ${processed.checkpointId} --benchmark-id=<id> --delta=<number> --verdict=<pass|fail>
619
- 2. 查看详情: /nocturnal-train show-lineage ${processed.checkpointId}`
620
- : `✅ Result imported
621
- Status: ${result.status}
622
- Checkpoint ID: ${processed.checkpointId}
623
- Checkpoint Ref: ${processed.checkpointRef}
624
- ${result.artifact ? `Artifact: ${result.artifact.artifactPath}` : ''}
625
- ${result.metrics ? `Wall Time: ${result.metrics.wallClockMinutes} min` : ''}
626
- ${result.failureReason ? `Failure: ${result.failureReason}` : ''}
627
-
628
- Next steps:
629
- 1. Run eval: /nocturnal-train attach-eval ${processed.checkpointId} --benchmark-id=<id> --delta=<number> --verdict=<pass|fail>
630
- 2. View details: /nocturnal-train show-lineage ${processed.checkpointId}`,
631
- };
632
- }
633
-
634
- // ── Attach Eval ──────────────────────────────────────────────────────
635
- if (subcommand === 'attach-eval') {
636
- const checkpointId = parts[1] || checkpointIdArg;
637
- if (!checkpointId) {
638
- return { text: zh ? '错误: 需要 checkpointId' : 'Error: checkpointId required' };
639
- }
640
-
641
- const runBenchmark = args.includes('--run-benchmark');
642
- const baselineRefArg = parts.find((p) => p.startsWith('--baseline-ref='))?.split('=')[1];
643
-
644
- const program = new TrainingProgram(workspaceDir);
645
- const checkpoint = getCheckpoint(workspaceDir, checkpointId);
646
-
647
- if (!checkpoint) {
648
- return { text: zh ? `错误: Checkpoint 未找到: ${checkpointId}` : `Error: Checkpoint not found: ${checkpointId}` };
649
- }
650
-
651
- let benchmarkId = benchmarkIdArg || `bench-${Date.now()}`;
652
- let delta = deltaArg ? parseFloat(deltaArg) : NaN;
653
- let verdict: 'fail' | 'pass' | 'compare_only' = verdictArg === 'pass' || verdictArg === 'fail' || verdictArg === 'compare_only'
654
- ? (verdictArg)
655
- : 'compare_only';
656
- let baselineScore = baselineScoreArg ? parseFloat(baselineScoreArg) : 0.5;
657
- let candidateScore = candidateScoreArg ? parseFloat(candidateScoreArg) : 0.5;
658
- const mode: 'prompt_assisted' | 'reduced_prompt' = (modeArg === 'prompt_assisted' ? 'prompt_assisted' : 'reduced_prompt');
659
-
660
- // --- Run benchmark mode: execute real benchmark to get scores ---
661
- // This closes the gap in the attach-eval command chain.
662
- if (runBenchmark) {
663
- // Determine baseline checkpoint ref
664
- let baselineRef = baselineRefArg;
665
- if (!baselineRef) {
666
- // Try to auto-detect from deployment registry: use the currently active checkpoint as baseline
667
- const deployment = getDeployment(workspaceDir, 'local-reader');
668
- if (deployment?.activeCheckpointId && deployment.activeCheckpointId !== checkpointId) {
669
- baselineRef = deployment.activeCheckpointId;
670
- }
671
- }
672
- if (!baselineRef) {
673
- return {
674
- text: zh
675
- ? `错误: --run-benchmark 需要 --baseline-ref 参数指定基线检查点,或当前需要有已启用的 local-reader 部署。`
676
- : `Error: --run-benchmark requires --baseline-ref to specify the baseline checkpoint, or an active local-reader deployment must exist.`,
677
- };
678
- }
679
-
680
- // Resolve both checkpoint refs to artifact paths for the scorer.
681
- // The scorer (resolveCheckpointPath) expects filesystem paths to PEFT adapters,
682
- // not checkpoint registry IDs. Look them up from the registry.
683
- const baselineCheckpoint = getCheckpoint(workspaceDir, baselineRef);
684
- if (!baselineCheckpoint) {
685
- return {
686
- text: zh
687
- ? `错误: Baseline 检查点未找到: ${baselineRef}`
688
- : `Error: Baseline checkpoint not found: ${baselineRef}`,
689
- };
690
- }
691
- // Candidate checkpoint was already validated above (line 550)
692
-
693
- // Find the export ID from the parent training run
694
- let exportId = checkpointId;
695
- if (checkpoint.trainRunId) {
696
- const run = getTrainingRun(workspaceDir, checkpoint.trainRunId);
697
- if (run?.exportId) {
698
- ({ exportId } = run);
699
- }
700
- }
701
- const scorerType = 'local-model'; // Use real model scorer
702
-
703
- // Run benchmark via ts-node subprocess
704
- const benchmarkScript = BENCHMARK_SCRIPT_PATH;
705
- const outputDir = path.join(workspaceDir, '.state', 'nocturnal', 'evals');
706
-
707
- // Build the compare command - pass ARTIFACT PATHS as separate arguments
708
- // to avoid shell injection when paths contain special characters.
709
- // Use execFileSync to pass arguments as an array (no shell interpolation).
710
- const cmdArgs = [
711
- '--yes',
712
- 'ts-node',
713
- benchmarkScript,
714
- 'compare',
715
- `--export-id=${exportId}`,
716
- `--baseline=${baselineCheckpoint.artifactPath}`,
717
- `--candidate=${checkpoint.artifactPath}`,
718
- `--mode=${mode}`,
719
- `--scorer=${scorerType}`,
720
- `--output-dir=${outputDir}`,
721
- ];
722
-
723
- let benchmarkResult: {
724
- delta: { delta: number; baselineScore: number; candidateScore: number };
725
- verdict: 'pass' | 'fail' | 'compare_only';
726
- benchmarkId: string;
727
- } | null = null;
728
- let benchmarkError = '';
729
-
730
- try {
731
- // Use execFileSync to avoid shell injection — paths are passed as args, not interpolated
732
- const stdout = execFileSync('npx', cmdArgs, {
733
- cwd: process.cwd(),
734
- timeout: 300000, // 5 min timeout
735
- encoding: 'utf-8',
736
- });
737
- // stdout is the JSON result from run-benchmark
738
- try {
739
- benchmarkResult = JSON.parse(stdout.trim());
740
- } catch {
741
- benchmarkError = `Failed to parse benchmark output: ${stdout.substring(0, 200)}`;
742
- }
743
- } catch (err: unknown) {
744
- // execSync throws on non-zero exit code; stdout may contain partial data
745
- const stdout = ((err as { stdout?: string }).stdout) ?? '';
746
- try {
747
- benchmarkResult = JSON.parse(stdout.trim());
748
- } catch {
749
- benchmarkError = `Benchmark failed: ${err instanceof Error ? err.message : String(err)}. stdout: ${stdout.substring(0, 200)}`;
750
- }
751
- }
752
-
753
- if (benchmarkError || !benchmarkResult) {
754
- return {
755
- text: zh
756
- ? `❌ Benchmark 执行失败: ${benchmarkError || '无法解析结果'}`
757
- : `❌ Benchmark execution failed: ${benchmarkError || 'Could not parse result'}`,
758
- };
759
- }
760
-
761
- // Destructure benchmark result - delta property contains the actual delta value
762
-
763
- delta = benchmarkResult.delta.delta;
764
-
765
- baselineScore = benchmarkResult.delta.baselineScore;
766
-
767
- candidateScore = benchmarkResult.delta.candidateScore;
768
-
769
- benchmarkId = benchmarkResult.benchmarkId;
770
-
771
- verdict = benchmarkResult.verdict;
772
- } else {
773
- // Manual mode: require explicit delta and verdict
774
- if (!deltaArg || !verdictArg) {
775
- return {
776
- text: zh
777
- ? '错误: 需要 --benchmark-id, --delta, --verdict 参数(或使用 --run-benchmark 自动运行)'
778
- : 'Error: --benchmark-id, --delta, --verdict are required (or use --run-benchmark to auto-run)',
779
- };
780
- }
781
- if (isNaN(delta)) {
782
- return { text: zh ? '错误: delta 必须是数字' : 'Error: delta must be a number' };
783
- }
784
- }
785
-
786
- const evalSummary = {
787
- evalId: `eval-${Date.now()}`,
788
- checkpointId,
789
- benchmarkId,
790
- targetModelFamily: checkpoint.targetModelFamily,
791
- mode,
792
- baselineScore,
793
- candidateScore,
794
- delta,
795
- verdict,
796
- };
797
-
798
- try {
799
- program.attachEvalAndMarkDeployable(checkpointId, evalSummary);
800
- const deployable = verdict === 'pass' || verdict === 'compare_only';
801
-
802
- return {
803
- text: zh
804
- ? `✅ 评估已附加${runBenchmark ? '(自动 Benchmark)' : ''}
805
- Checkpoint: ${checkpointId.substring(0, 8)}...
806
- Benchmark: ${benchmarkId}
807
- 基线分数: ${baselineScore.toFixed(4)}
808
- 候选分数: ${candidateScore.toFixed(4)}
809
- Delta: ${delta >= 0 ? '+' : ''}${delta.toFixed(4)}
810
- Verdict: ${verdict}
811
- Mode: ${mode}
812
- Deployable: ${deployable ? 'Yes' : 'No'}
813
-
814
- 下一步:
815
- 1. 评估晋升: /nocturnal-rollout evaluate-promotion ${checkpointId}
816
- 2. 绑定部署: /nocturnal-rollout bind ${checkpointId} --profile=local-reader`
817
- : `✅ Eval attached${runBenchmark ? ' (auto benchmark)' : ''}
818
- Checkpoint: ${checkpointId.substring(0, 8)}...
819
- Benchmark: ${benchmarkId}
820
- Baseline Score: ${baselineScore.toFixed(4)}
821
- Candidate Score: ${candidateScore.toFixed(4)}
822
- Delta: ${delta >= 0 ? '+' : ''}${delta.toFixed(4)}
823
- Verdict: ${verdict}
824
- Mode: ${mode}
825
- Deployable: ${deployable ? 'Yes' : 'No'}
826
-
827
- Next steps:
828
- 1. Evaluate promotion: /nocturnal-rollout evaluate-promotion ${checkpointId}
829
- 2. Bind deployment: /nocturnal-rollout bind ${checkpointId} --profile=local-reader`,
830
- };
831
- } catch (err: unknown) {
832
- return {
833
- text: zh
834
- ? `❌ 附加评估失败: ${err instanceof Error ? err.message : String(err)}`
835
- : `❌ Attach eval failed: ${err instanceof Error ? err.message : String(err)}`,
836
- };
837
- }
838
- }
839
-
840
- // ── Show Lineage ─────────────────────────────────────────────────────
841
- if (subcommand === 'show-lineage') {
842
- const [, checkpointId] = parts;
843
- if (!checkpointId) {
844
- return { text: zh ? '错误: 需要 checkpointId' : 'Error: checkpointId required' };
845
- }
846
-
847
- const lineage = getCheckpointLineage(workspaceDir, checkpointId);
848
- if (!lineage) {
849
- return { text: zh ? `未找到 lineage: ${checkpointId}` : `Lineage not found: ${checkpointId}` };
850
- }
851
-
852
- const { run, checkpoint, eval: eval_ } = lineage;
853
-
854
- let text = zh
855
- ? `=== Checkpoint Lineage ===
856
- Checkpoint: ${checkpoint.checkpointId}
857
- Family: ${checkpoint.targetModelFamily}
858
- Deployable: ${checkpoint.deployable}
859
- Artifact: ${checkpoint.artifactPath}
860
-
861
- --- Training Run ---
862
- ${formatTrainingRun(run, zh)}
863
-
864
- --- Eval Summary ---`
865
- : `=== Checkpoint Lineage ===
866
- Checkpoint: ${checkpoint.checkpointId}
867
- Family: ${checkpoint.targetModelFamily}
868
- Deployable: ${checkpoint.deployable}
869
- Artifact: ${checkpoint.artifactPath}
870
-
871
- --- Training Run ---
872
- ${formatTrainingRun(run, zh)}
873
-
874
- --- Eval Summary ---`;
875
-
876
- if (eval_) {
877
- text += `
878
- ID: ${eval_.evalId}
879
- Mode: ${eval_.mode}
880
- Delta: ${eval_.delta >= 0 ? '+' : ''}${eval_.delta.toFixed(4)}
881
- Baseline: ${eval_.baselineScore.toFixed(3)}
882
- Candidate: ${eval_.candidateScore.toFixed(3)}
883
- Verdict: ${eval_.verdict}`;
884
- } else {
885
- text += zh ? '\n(无)' : '\n(None)';
886
- }
887
-
888
- return { text };
889
- }
890
-
891
- // ── List Experiments ──────────────────────────────────────────────────
892
- if (subcommand === 'list-experiments') {
893
- const runs = listTrainingRuns(workspaceDir);
894
- if (runs.length === 0) {
895
- return { text: zh ? '没有训练实验' : 'No training experiments' };
896
- }
897
-
898
- const lines = runs.slice(0, 20).map((run) => {
899
- const date = new Date(run.createdAt).toLocaleDateString();
900
- return `${run.trainRunId.substring(0, 8)}... | ${run.status} | ${run.targetModelFamily} | ${date} | ${run.checkpointIds.length} ckpts`;
901
- });
902
-
903
- return {
904
- text: zh
905
- ? `训练实验 (${runs.length}):
906
- ${lines.join('\n')}`
907
- : `Training experiments (${runs.length}):
908
- ${lines.join('\n')}`,
909
- };
910
- }
911
-
912
- // ── List Checkpoints ─────────────────────────────────────────────────
913
- if (subcommand === 'list-checkpoints') {
914
- const checkpoints = listCheckpoints(workspaceDir);
915
- if (checkpoints.length === 0) {
916
- return { text: zh ? '没有 Checkpoint' : 'No checkpoints' };
917
- }
918
-
919
- const filtered = familyArg
920
- ? checkpoints.filter((cp) => cp.targetModelFamily.includes(familyArg))
921
- : checkpoints;
922
-
923
- if (filtered.length === 0) {
924
- return { text: zh ? '没有匹配的 Checkpoint' : 'No matching checkpoints' };
925
- }
926
-
927
- const lines = filtered.slice(0, 20).map((cp) => {
928
- const date = new Date(cp.createdAt).toLocaleDateString();
929
- return `${cp.checkpointId.substring(0, 8)}... | ${cp.deployable ? 'deployable' : 'not-deployable'} | ${cp.targetModelFamily} | ${date}`;
930
- });
931
-
932
- return {
933
- text: zh
934
- ? `Checkpoints (${filtered.length}):
935
- ${lines.join('\n')}`
936
- : `Checkpoints (${filtered.length}):
937
- ${lines.join('\n')}`,
938
- };
939
- }
940
-
941
- // ── Stats ────────────────────────────────────────────────────────────
942
- if (subcommand === 'stats') {
943
- const stats = getTrainingRegistryStats(workspaceDir);
944
- return {
945
- text: zh
946
- ? `=== 训练注册统计 ===
947
- 总实验数: ${stats.totalRuns}
948
- 完成: ${stats.completedRuns}
949
- 失败: ${stats.failedRuns}
950
- 进行中: ${stats.pendingRuns + stats.runningRuns}
951
-
952
- 总 Checkpoint: ${stats.totalCheckpoints}
953
- 可部署: ${stats.deployableCheckpoints}
954
-
955
- 总评估: ${stats.totalEvals}
956
- 通过: ${stats.passingEvals}
957
- 失败: ${stats.failingEvals}`
958
- : `=== Training Registry Stats ===
959
- Total runs: ${stats.totalRuns}
960
- Completed: ${stats.completedRuns}
961
- Failed: ${stats.failedRuns}
962
- In progress: ${stats.pendingRuns + stats.runningRuns}
963
-
964
- Total checkpoints: ${stats.totalCheckpoints}
965
- Deployable: ${stats.deployableCheckpoints}
966
-
967
- Total evals: ${stats.totalEvals}
968
- Passing: ${stats.passingEvals}
969
- Failing: ${stats.failingEvals}`,
970
- };
971
- }
972
-
973
- // Unknown subcommand
974
- return {
975
- text: zh
976
- ? `未知子命令: ${subcommand}。运行 /nocturnal-train help 查看帮助。`
977
- : `Unknown subcommand: ${subcommand}. Run /nocturnal-train help for usage.`,
978
- };
979
- } catch (err: unknown) {
980
- return {
981
- text: zh
982
- ? `❌ 命令失败: ${err instanceof Error ? err.message : String(err)}`
983
- : `❌ Command failed: ${err instanceof Error ? err.message : String(err)}`,
984
- };
985
- }
986
- }