principles-disciple 1.7.5 → 1.7.8

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 (129) hide show
  1. package/dist/commands/context.js +5 -15
  2. package/dist/commands/evolution-status.js +29 -48
  3. package/dist/commands/export.js +61 -8
  4. package/dist/commands/nocturnal-review.d.ts +24 -0
  5. package/dist/commands/nocturnal-review.js +265 -0
  6. package/dist/commands/nocturnal-rollout.d.ts +27 -0
  7. package/dist/commands/nocturnal-rollout.js +671 -0
  8. package/dist/commands/nocturnal-train.d.ts +25 -0
  9. package/dist/commands/nocturnal-train.js +919 -0
  10. package/dist/commands/pain.js +8 -21
  11. package/dist/config/defaults/runtime.d.ts +40 -0
  12. package/dist/config/defaults/runtime.js +44 -0
  13. package/dist/config/errors.d.ts +84 -0
  14. package/dist/config/errors.js +94 -0
  15. package/dist/config/index.d.ts +7 -0
  16. package/dist/config/index.js +7 -0
  17. package/dist/constants/diagnostician.d.ts +0 -4
  18. package/dist/constants/diagnostician.js +0 -4
  19. package/dist/constants/tools.d.ts +2 -2
  20. package/dist/constants/tools.js +1 -1
  21. package/dist/core/adaptive-thresholds.d.ts +186 -0
  22. package/dist/core/adaptive-thresholds.js +300 -0
  23. package/dist/core/config.d.ts +2 -38
  24. package/dist/core/config.js +6 -61
  25. package/dist/core/control-ui-db.d.ts +27 -0
  26. package/dist/core/control-ui-db.js +18 -0
  27. package/dist/core/event-log.d.ts +1 -2
  28. package/dist/core/event-log.js +0 -3
  29. package/dist/core/evolution-engine.js +1 -21
  30. package/dist/core/evolution-reducer.d.ts +7 -1
  31. package/dist/core/evolution-reducer.js +56 -4
  32. package/dist/core/evolution-types.d.ts +61 -9
  33. package/dist/core/evolution-types.js +31 -9
  34. package/dist/core/external-training-contract.d.ts +276 -0
  35. package/dist/core/external-training-contract.js +269 -0
  36. package/dist/core/local-worker-routing.d.ts +175 -0
  37. package/dist/core/local-worker-routing.js +525 -0
  38. package/dist/core/model-deployment-registry.d.ts +218 -0
  39. package/dist/core/model-deployment-registry.js +503 -0
  40. package/dist/core/model-training-registry.d.ts +295 -0
  41. package/dist/core/model-training-registry.js +475 -0
  42. package/dist/core/nocturnal-arbiter.d.ts +159 -0
  43. package/dist/core/nocturnal-arbiter.js +534 -0
  44. package/dist/core/nocturnal-candidate-scoring.d.ts +137 -0
  45. package/dist/core/nocturnal-candidate-scoring.js +266 -0
  46. package/dist/core/nocturnal-compliance.d.ts +175 -0
  47. package/dist/core/nocturnal-compliance.js +824 -0
  48. package/dist/core/nocturnal-dataset.d.ts +224 -0
  49. package/dist/core/nocturnal-dataset.js +443 -0
  50. package/dist/core/nocturnal-executability.d.ts +85 -0
  51. package/dist/core/nocturnal-executability.js +331 -0
  52. package/dist/core/nocturnal-export.d.ts +124 -0
  53. package/dist/core/nocturnal-export.js +275 -0
  54. package/dist/core/nocturnal-paths.d.ts +124 -0
  55. package/dist/core/nocturnal-paths.js +214 -0
  56. package/dist/core/nocturnal-trajectory-extractor.d.ts +242 -0
  57. package/dist/core/nocturnal-trajectory-extractor.js +307 -0
  58. package/dist/core/nocturnal-trinity.d.ts +311 -0
  59. package/dist/core/nocturnal-trinity.js +880 -0
  60. package/dist/core/path-resolver.js +2 -1
  61. package/dist/core/paths.d.ts +6 -0
  62. package/dist/core/paths.js +6 -0
  63. package/dist/core/principle-training-state.d.ts +121 -0
  64. package/dist/core/principle-training-state.js +321 -0
  65. package/dist/core/promotion-gate.d.ts +238 -0
  66. package/dist/core/promotion-gate.js +529 -0
  67. package/dist/core/session-tracker.d.ts +10 -0
  68. package/dist/core/session-tracker.js +14 -0
  69. package/dist/core/shadow-observation-registry.d.ts +217 -0
  70. package/dist/core/shadow-observation-registry.js +308 -0
  71. package/dist/core/training-program.d.ts +233 -0
  72. package/dist/core/training-program.js +433 -0
  73. package/dist/core/trajectory.d.ts +155 -1
  74. package/dist/core/trajectory.js +292 -8
  75. package/dist/core/workspace-context.d.ts +0 -6
  76. package/dist/core/workspace-context.js +0 -12
  77. package/dist/hooks/bash-risk.d.ts +57 -0
  78. package/dist/hooks/bash-risk.js +137 -0
  79. package/dist/hooks/edit-verification.d.ts +62 -0
  80. package/dist/hooks/edit-verification.js +256 -0
  81. package/dist/hooks/gate-block-helper.d.ts +44 -0
  82. package/dist/hooks/gate-block-helper.js +119 -0
  83. package/dist/hooks/gate.d.ts +18 -0
  84. package/dist/hooks/gate.js +62 -751
  85. package/dist/hooks/gfi-gate.d.ts +40 -0
  86. package/dist/hooks/gfi-gate.js +113 -0
  87. package/dist/hooks/pain.js +6 -9
  88. package/dist/hooks/progressive-trust-gate.d.ts +51 -0
  89. package/dist/hooks/progressive-trust-gate.js +89 -0
  90. package/dist/hooks/prompt.d.ts +11 -11
  91. package/dist/hooks/prompt.js +167 -77
  92. package/dist/hooks/subagent.js +43 -6
  93. package/dist/hooks/thinking-checkpoint.d.ts +37 -0
  94. package/dist/hooks/thinking-checkpoint.js +51 -0
  95. package/dist/http/principles-console-route.js +13 -3
  96. package/dist/i18n/commands.js +8 -8
  97. package/dist/index.js +129 -28
  98. package/dist/service/central-database.js +2 -1
  99. package/dist/service/control-ui-query-service.d.ts +1 -1
  100. package/dist/service/control-ui-query-service.js +3 -3
  101. package/dist/service/evolution-query-service.d.ts +1 -1
  102. package/dist/service/evolution-query-service.js +5 -5
  103. package/dist/service/evolution-worker.d.ts +52 -4
  104. package/dist/service/evolution-worker.js +328 -16
  105. package/dist/service/nocturnal-runtime.d.ts +183 -0
  106. package/dist/service/nocturnal-runtime.js +352 -0
  107. package/dist/service/nocturnal-service.d.ts +163 -0
  108. package/dist/service/nocturnal-service.js +787 -0
  109. package/dist/service/nocturnal-target-selector.d.ts +145 -0
  110. package/dist/service/nocturnal-target-selector.js +315 -0
  111. package/dist/service/phase3-input-filter.d.ts +48 -12
  112. package/dist/service/phase3-input-filter.js +84 -18
  113. package/dist/service/runtime-summary-service.d.ts +34 -10
  114. package/dist/service/runtime-summary-service.js +87 -48
  115. package/dist/tools/deep-reflect.js +2 -1
  116. package/dist/types/event-types.d.ts +4 -10
  117. package/dist/types/runtime-summary.d.ts +47 -0
  118. package/dist/types/runtime-summary.js +1 -0
  119. package/dist/types.d.ts +0 -3
  120. package/dist/types.js +0 -2
  121. package/openclaw.plugin.json +1 -1
  122. package/package.json +1 -1
  123. package/templates/langs/en/skills/pd-mentor/SKILL.md +5 -5
  124. package/templates/langs/zh/skills/pd-mentor/SKILL.md +5 -5
  125. package/templates/pain_settings.json +0 -6
  126. package/dist/commands/trust.d.ts +0 -4
  127. package/dist/commands/trust.js +0 -78
  128. package/dist/core/trust-engine.d.ts +0 -96
  129. package/dist/core/trust-engine.js +0 -286
@@ -0,0 +1,919 @@
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
+ import * as path from 'path';
25
+ import * as fs from 'fs';
26
+ import { execFileSync, spawn } from 'child_process';
27
+ import { fileURLToPath } from 'url';
28
+ import { TrainingProgram, } from '../core/training-program.js';
29
+ import { listTrainingRuns, getTrainingRun, listCheckpoints, getCheckpoint, getCheckpointLineage, getTrainingRegistryStats, } from '../core/model-training-registry.js';
30
+ import { getDeployment } from '../core/model-deployment-registry.js';
31
+ function isZh(ctx) {
32
+ return String(ctx.config?.language || 'en').startsWith('zh');
33
+ }
34
+ function zh(cond) {
35
+ return cond;
36
+ }
37
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
38
+ const REPO_ROOT = path.resolve(MODULE_DIR, '..', '..', '..', '..');
39
+ const TRAINER_SCRIPTS_DIR = path.join(REPO_ROOT, 'scripts', 'nocturnal', 'trainer');
40
+ const BENCHMARK_SCRIPT_PATH = path.join(REPO_ROOT, 'scripts', 'nocturnal', 'run-benchmark.ts');
41
+ /**
42
+ * Parse backend from argument string.
43
+ */
44
+ function parseBackend(arg) {
45
+ if (!arg)
46
+ return 'peft-trl-orpo';
47
+ const valid = ['peft-trl-orpo', 'unsloth-orpo', 'dry-run'];
48
+ if (valid.includes(arg)) {
49
+ return arg;
50
+ }
51
+ return 'peft-trl-orpo';
52
+ }
53
+ /**
54
+ * Parse hardware tier from argument string.
55
+ */
56
+ function parseHardwareTier(arg) {
57
+ if (!arg)
58
+ return 'consumer-gpu';
59
+ const valid = ['consumer-gpu', 'small-gpu', 'cpu-experimental'];
60
+ if (valid.includes(arg)) {
61
+ return arg;
62
+ }
63
+ return 'consumer-gpu';
64
+ }
65
+ /**
66
+ * Format training run for display.
67
+ */
68
+ function formatTrainingRun(run, zh) {
69
+ if (!run)
70
+ return zh ? '未找到' : 'Not found';
71
+ const lines = [
72
+ `ID: ${run.trainRunId.substring(0, 8)}...`,
73
+ `Family: ${run.targetModelFamily}`,
74
+ `Status: ${run.status}`,
75
+ `Dataset FP: ${run.datasetFingerprint.substring(0, 12)}...`,
76
+ `Created: ${new Date(run.createdAt).toLocaleString()}`,
77
+ ];
78
+ if (run.completedAt)
79
+ lines.push(`Completed: ${new Date(run.completedAt).toLocaleString()}`);
80
+ if (run.failureReason)
81
+ lines.push(`Failure: ${run.failureReason}`);
82
+ if (run.checkpointIds.length > 0) {
83
+ lines.push(`Checkpoints: ${run.checkpointIds.length}`);
84
+ }
85
+ return lines.join('\n ');
86
+ }
87
+ /**
88
+ * Format checkpoint for display.
89
+ */
90
+ function formatCheckpoint(cp, zh) {
91
+ if (!cp)
92
+ return zh ? '未找到' : 'Not found';
93
+ const lines = [
94
+ `ID: ${cp.checkpointId.substring(0, 8)}...`,
95
+ `Family: ${cp.targetModelFamily}`,
96
+ `Artifact: ${cp.artifactPath}`,
97
+ `Deployable: ${cp.deployable ? (zh ? '是' : 'Yes') : (zh ? '否' : 'No')}`,
98
+ `Created: ${new Date(cp.createdAt).toLocaleString()}`,
99
+ ];
100
+ if (cp.lastEvalSummaryRef) {
101
+ lines.push(`Eval: ${cp.lastEvalSummaryRef.substring(0, 12)}...`);
102
+ }
103
+ return lines.join('\n ');
104
+ }
105
+ export async function handleNocturnalTrainCommand(ctx) {
106
+ const workspaceDir = ctx.config?.workspaceDir || process.cwd();
107
+ const zh = isZh(ctx);
108
+ const args = (ctx.args || '').trim();
109
+ const parts = args.split(/\s+/).filter(Boolean);
110
+ const [subcommand = 'help'] = parts;
111
+ const backendArg = parts.find((p) => p.startsWith('--backend='))?.split('=')[1];
112
+ const familyArg = parts.find((p) => p.startsWith('--family='))?.split('=')[1];
113
+ const hardwareTierArg = parts.find((p) => p.startsWith('--tier='))?.split('=')[1];
114
+ const datasetExportIdArg = parts.find((p) => p.startsWith('--dataset='))?.split('=')[1];
115
+ const benchmarkExportIdArg = parts.find((p) => p.startsWith('--benchmark='))?.split('=')[1];
116
+ const resultArg = parts.find((p) => p.startsWith('--result='))?.split('=')[1];
117
+ const checkpointIdArg = parts.find((p) => p.startsWith('--checkpoint-id='))?.split('=')[1];
118
+ const benchmarkIdArg = parts.find((p) => p.startsWith('--benchmark-id='))?.split('=')[1];
119
+ const deltaArg = parts.find((p) => p.startsWith('--delta='))?.split('=')[1];
120
+ const verdictArg = parts.find((p) => p.startsWith('--verdict='))?.split('=')[1];
121
+ const modeArg = parts.find((p) => p.startsWith('--mode='))?.split('=')[1];
122
+ const baselineScoreArg = parts.find((p) => p.startsWith('--baseline='))?.split('=')[1];
123
+ const candidateScoreArg = parts.find((p) => p.startsWith('--candidate='))?.split('=')[1];
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
+ // ── Create Experiment ─────────────────────────────────────────────────
188
+ if (subcommand === 'create-experiment') {
189
+ if (!familyArg) {
190
+ return { text: zh ? '错误: 需要 --family 参数' : 'Error: --family is required' };
191
+ }
192
+ const backend = parseBackend(backendArg);
193
+ const hardwareTier = parseHardwareTier(hardwareTierArg);
194
+ const runNow = args.includes('--run');
195
+ // Find ORPO export if dataset not specified
196
+ let datasetExportId = datasetExportIdArg;
197
+ let datasetExportPath = '';
198
+ if (!datasetExportId) {
199
+ // Try to find latest ORPO export
200
+ const exportsDir = path.join(workspaceDir, '.state', 'exports', 'orpo');
201
+ if (fs.existsSync(exportsDir)) {
202
+ const files = fs.readdirSync(exportsDir).filter((f) => f.endsWith('-manifest.json'));
203
+ if (files.length > 0) {
204
+ const manifest = JSON.parse(fs.readFileSync(path.join(exportsDir, files[0]), 'utf-8'));
205
+ datasetExportId = manifest.exportId;
206
+ datasetExportPath = manifest.exportPath;
207
+ }
208
+ }
209
+ if (!datasetExportId) {
210
+ return {
211
+ text: zh
212
+ ? '错误: 未找到 ORPO 导出。请先运行 /pd-nocturnal-review 导出数据。'
213
+ : 'Error: No ORPO export found. Run /pd-nocturnal-review to export data first.',
214
+ };
215
+ }
216
+ }
217
+ else {
218
+ datasetExportPath = path.join(workspaceDir, '.state', 'exports', 'orpo', `${datasetExportId}.jsonl`);
219
+ }
220
+ // Get dataset fingerprint
221
+ let datasetFingerprint = 'unknown';
222
+ const manifestPath = path.join(workspaceDir, '.state', 'exports', 'orpo', `${datasetExportId}-manifest.json`);
223
+ if (fs.existsSync(manifestPath)) {
224
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
225
+ if (!manifest.datasetFingerprint) {
226
+ return {
227
+ text: zh
228
+ ? `错误: manifest 文件缺少 datasetFingerprint: ${manifestPath}`
229
+ : `Error: manifest missing datasetFingerprint: ${manifestPath}`,
230
+ };
231
+ }
232
+ datasetFingerprint = manifest.datasetFingerprint;
233
+ }
234
+ const benchmarkExportId = benchmarkExportIdArg || datasetExportId || 'benchmark-default';
235
+ const outputDir = path.join(workspaceDir, '.state', 'nocturnal', 'checkpoints');
236
+ const program = new TrainingProgram(workspaceDir);
237
+ const createResult = program.createExperiment({
238
+ backend,
239
+ targetWorkerProfile: 'local-reader', // Phase 7 only
240
+ targetModelFamily: familyArg,
241
+ hardwareTier,
242
+ datasetExportId,
243
+ datasetExportPath,
244
+ datasetFingerprint,
245
+ benchmarkExportId,
246
+ outputDir,
247
+ });
248
+ // --- Write spec files for the manual chain ---
249
+ // The trainer reads spec from scripts/nocturnal/trainer/experiment-<id>.json.
250
+ // import-result reads spec from .state/nocturnal/checkpoints/experiment-<id>.json.
251
+ // Both must be written so the manual create-experiment -> trainer -> import-result chain works.
252
+ const spec = createResult.spec;
253
+ const trainerSpecPath = path.join(TRAINER_SCRIPTS_DIR, `experiment-${spec.experimentId}.json`);
254
+ const workspaceSpecPath = path.join(workspaceDir, '.state', 'nocturnal', 'checkpoints', `experiment-${spec.experimentId}.json`);
255
+ const trainerSpecDir = path.dirname(trainerSpecPath);
256
+ const workspaceCheckpointsDir = path.dirname(workspaceSpecPath);
257
+ if (!fs.existsSync(trainerSpecDir)) {
258
+ fs.mkdirSync(trainerSpecDir, { recursive: true });
259
+ }
260
+ if (!fs.existsSync(workspaceCheckpointsDir)) {
261
+ fs.mkdirSync(workspaceCheckpointsDir, { recursive: true });
262
+ }
263
+ fs.writeFileSync(trainerSpecPath, JSON.stringify(spec, null, 2), 'utf-8');
264
+ fs.writeFileSync(workspaceSpecPath, JSON.stringify(spec, null, 2), 'utf-8');
265
+ // --- Auto-run mode: execute trainer immediately ---
266
+ // This closes the gap in the create-experiment -> trainer -> import-result chain.
267
+ // NOTE: This blocks until training completes (could be minutes).
268
+ if (runNow) {
269
+ const spec = createResult.spec;
270
+ const baseDir = TRAINER_SCRIPTS_DIR;
271
+ const scriptPath = path.join(baseDir, 'main.py');
272
+ const specPath = path.join(baseDir, `experiment-${spec.experimentId}.json`);
273
+ const outputDir = spec.outputDir;
274
+ const resultFilePath = path.join(outputDir, `result-${spec.experimentId}.json`);
275
+ // Write spec file
276
+ const specDir = path.dirname(specPath);
277
+ if (!fs.existsSync(specDir)) {
278
+ fs.mkdirSync(specDir, { recursive: true });
279
+ }
280
+ fs.writeFileSync(specPath, JSON.stringify(spec, null, 2), 'utf-8');
281
+ let trainerResult;
282
+ try {
283
+ if (spec.backend === 'dry-run') {
284
+ trainerResult = {
285
+ experimentId: spec.experimentId,
286
+ backend: 'dry-run',
287
+ status: 'dry_run',
288
+ targetWorkerProfile: spec.targetWorkerProfile,
289
+ targetModelFamily: spec.targetModelFamily,
290
+ datasetFingerprint: spec.datasetFingerprint,
291
+ configFingerprint: spec.configFingerprint,
292
+ codeHash: spec.codeHash,
293
+ createdAt: new Date().toISOString(),
294
+ };
295
+ }
296
+ else {
297
+ // Execute trainer using spawn (streaming, no full log buffering).
298
+ // stdout is collected into a fixed-size buffer (1MB) to avoid OOM.
299
+ // stderr is piped directly to parent stderr to avoid memory accumulation.
300
+ const timeoutMs = (spec.budget.maxWallClockMinutes * 60 * 1000) + 30000;
301
+ const pythonExe = process.platform === 'win32' ? 'python' : 'python3';
302
+ const MAX_STDOUT_BUFFER = 1 * 1024 * 1024; // 1MB cap
303
+ trainerResult = await new Promise((resolve, reject) => {
304
+ const proc = spawn(pythonExe, [scriptPath, '--spec', specPath, '--output-dir', outputDir], {
305
+ timeout: timeoutMs,
306
+ });
307
+ // Collect stdout with size cap to prevent OOM
308
+ const stdoutChunks = [];
309
+ let stdoutSize = 0;
310
+ proc.stdout.on('data', (chunk) => {
311
+ const remaining = MAX_STDOUT_BUFFER - stdoutSize;
312
+ if (remaining > 0) {
313
+ stdoutChunks.push(chunk.slice(0, remaining));
314
+ stdoutSize += Math.min(chunk.length, remaining);
315
+ }
316
+ });
317
+ // Pipe stderr directly — training logs can be large, don't buffer
318
+ proc.stderr.pipe(process.stderr);
319
+ const timer = setTimeout(() => {
320
+ proc.kill();
321
+ reject(new Error(`Trainer timed out after ${timeoutMs}ms`));
322
+ }, timeoutMs);
323
+ proc.on('close', (code) => {
324
+ clearTimeout(timer);
325
+ if (code === 0) {
326
+ const stdout = Buffer.concat(stdoutChunks).toString('utf-8');
327
+ const trimmed = stdout.trim();
328
+ if (trimmed) {
329
+ try {
330
+ resolve(JSON.parse(trimmed));
331
+ return;
332
+ }
333
+ catch {
334
+ // fall through to result file
335
+ }
336
+ }
337
+ // Fallback to result file
338
+ if (fs.existsSync(resultFilePath)) {
339
+ try {
340
+ resolve(JSON.parse(fs.readFileSync(resultFilePath, 'utf-8')));
341
+ return;
342
+ }
343
+ catch {
344
+ reject(new Error(`Trainer stdout was not valid JSON and result file also invalid: ${resultFilePath}`));
345
+ return;
346
+ }
347
+ }
348
+ reject(new Error(`Trainer produced no parseable stdout and no result file found at: ${resultFilePath}`));
349
+ }
350
+ else {
351
+ // Non-zero exit: try result file as last resort
352
+ if (fs.existsSync(resultFilePath)) {
353
+ try {
354
+ resolve(JSON.parse(fs.readFileSync(resultFilePath, 'utf-8')));
355
+ }
356
+ catch {
357
+ reject(new Error(`Trainer exited with code ${code} and result file was invalid`));
358
+ }
359
+ }
360
+ else {
361
+ reject(new Error(`Trainer exited with code ${code} and no result file found`));
362
+ }
363
+ }
364
+ });
365
+ proc.on('error', (err) => {
366
+ clearTimeout(timer);
367
+ reject(new Error(`Trainer spawn failed: ${err.message}`));
368
+ });
369
+ });
370
+ }
371
+ }
372
+ catch (err) {
373
+ return {
374
+ text: zh
375
+ ? `❌ 训练执行失败: ${err.message}\n\n训练 Run ID: ${createResult.trainRunId}\n请检查 trainer 输出或使用 dry-run 后端重试。`
376
+ : `❌ Trainer execution failed: ${err.message}\n\nTraining Run ID: ${createResult.trainRunId}\nCheck trainer output or retry with --backend=dry-run.`,
377
+ };
378
+ }
379
+ finally {
380
+ // Clean up spec file
381
+ if (fs.existsSync(specPath)) {
382
+ fs.unlinkSync(specPath);
383
+ }
384
+ }
385
+ // Process trainer result (register checkpoint)
386
+ // dry_run returns null (no checkpoint); other statuses throw on error
387
+ let processed;
388
+ try {
389
+ processed = program.processResult({
390
+ spec: createResult.spec,
391
+ trainRunId: createResult.trainRunId,
392
+ result: trainerResult,
393
+ });
394
+ }
395
+ catch (err) {
396
+ return {
397
+ text: zh
398
+ ? `❌ 结果导入失败: ${err.message}`
399
+ : `❌ Result import failed: ${err.message}`,
400
+ };
401
+ }
402
+ if (processed === null) {
403
+ // dry_run completed with no checkpoint — this is a non-error outcome
404
+ return {
405
+ text: zh
406
+ ? `✅ Dry-run 完成(未产生 checkpoint)
407
+ 实验 ID: ${createResult.spec.experimentId}
408
+ 训练 Run ID: ${createResult.trainRunId}
409
+ 状态: ${trainerResult.status}
410
+
411
+ 下一步:
412
+ 若需产生可部署的 checkpoint,请使用 --backend=peft-trl-orpo 或 --backend=unsloth-orpo 重试。`
413
+ : `✅ Dry-run complete (no checkpoint produced)
414
+ Experiment ID: ${createResult.spec.experimentId}
415
+ Training Run ID: ${createResult.trainRunId}
416
+ Status: ${trainerResult.status}
417
+
418
+ Next steps:
419
+ To produce a deployable checkpoint, retry with --backend=peft-trl-orpo or --backend=unsloth-orpo.`,
420
+ };
421
+ }
422
+ return {
423
+ text: zh
424
+ ? `✅ 训练完成
425
+ 实验 ID: ${createResult.spec.experimentId}
426
+ 训练 Run ID: ${createResult.trainRunId}
427
+ Checkpoint ID: ${processed.checkpointId}
428
+ 状态: ${trainerResult.status}
429
+ ${trainerResult.failureReason ? `失败原因: ${trainerResult.failureReason}` : ''}
430
+
431
+ 下一步:
432
+ 1. 运行评估: /nocturnal-train attach-eval ${processed.checkpointId} --benchmark-id=<id> --delta=<number> --verdict=<pass|fail> --run-benchmark
433
+ 2. 查看检查点: /nocturnal-train show-lineage ${processed.checkpointId}`
434
+ : `✅ Training complete
435
+ Experiment ID: ${createResult.spec.experimentId}
436
+ Training Run ID: ${createResult.trainRunId}
437
+ Checkpoint ID: ${processed.checkpointId}
438
+ Status: ${trainerResult.status}
439
+ ${trainerResult.failureReason ? `Failure: ${trainerResult.failureReason}` : ''}
440
+
441
+ Next steps:
442
+ 1. Run eval: /nocturnal-train attach-eval ${processed.checkpointId} --benchmark-id=<id> --delta=<number> --verdict=<pass|fail> --run-benchmark
443
+ 2. View checkpoint: /nocturnal-train show-lineage ${processed.checkpointId}`,
444
+ };
445
+ }
446
+ return {
447
+ text: zh
448
+ ? `✅ 实验已创建
449
+ 实验 ID: ${createResult.spec.experimentId}
450
+ 后端: ${createResult.spec.backend}
451
+ 模型家族: ${createResult.spec.targetModelFamily}
452
+ 硬件层级: ${createResult.spec.hardwareTier}
453
+ 数据集: ${createResult.spec.datasetExportId}
454
+ 输出目录: ${createResult.spec.outputDir}
455
+ 训练 Run ID: ${createResult.trainRunId}
456
+
457
+ 下一步:
458
+ 1. 运行外部训练器: python "${path.join(TRAINER_SCRIPTS_DIR, 'main.py')}" --spec "${trainerSpecPath}" --output-dir ${outputDir}
459
+ 2. 导入结果: /nocturnal-train import-result ${createResult.spec.experimentId} --result=<path>
460
+ 3. 附加评估: /nocturnal-train attach-eval <checkpointId> --benchmark-id=<id> --delta=<number> --verdict=<pass|fail>
461
+ 4. 手动链路 spec 已写入:
462
+ - ${trainerSpecPath}
463
+ - ${workspaceSpecPath}`
464
+ : `✅ Experiment created
465
+ Experiment ID: ${createResult.spec.experimentId}
466
+ Backend: ${createResult.spec.backend}
467
+ Model Family: ${createResult.spec.targetModelFamily}
468
+ Hardware Tier: ${createResult.spec.hardwareTier}
469
+ Dataset: ${createResult.spec.datasetExportId}
470
+ Output Dir: ${createResult.spec.outputDir}
471
+ Training Run ID: ${createResult.trainRunId}
472
+
473
+ Next steps:
474
+ 1. Run external trainer: python "${path.join(TRAINER_SCRIPTS_DIR, 'main.py')}" --spec "${trainerSpecPath}" --output-dir ${outputDir}
475
+ 2. Import result: /nocturnal-train import-result ${createResult.spec.experimentId} --result=<path>
476
+ 3. Attach eval: /nocturnal-train attach-eval <checkpointId> --benchmark-id=<id> --delta=<number> --verdict=<pass|fail>
477
+ 4. Durable spec files written to:
478
+ - ${trainerSpecPath}
479
+ - ${workspaceSpecPath}`,
480
+ };
481
+ }
482
+ // ── Show Experiment ───────────────────────────────────────────────────
483
+ if (subcommand === 'show-experiment') {
484
+ const experimentId = parts[1];
485
+ if (!experimentId) {
486
+ return { text: zh ? '错误: 需要实验 ID' : 'Error: experiment ID required' };
487
+ }
488
+ const runs = listTrainingRuns(workspaceDir, { targetModelFamily: undefined });
489
+ const run = runs.find((r) => r.trainRunId.startsWith(experimentId) || r.trainRunId === experimentId);
490
+ if (!run) {
491
+ return { text: zh ? `未找到实验: ${experimentId}` : `Experiment not found: ${experimentId}` };
492
+ }
493
+ return { text: formatTrainingRun(run, zh) };
494
+ }
495
+ // ── Import Result ─────────────────────────────────────────────────────
496
+ if (subcommand === 'import-result') {
497
+ const experimentId = parts[1];
498
+ if (!experimentId) {
499
+ return { text: zh ? '错误: 需要实验 ID' : 'Error: experiment ID required' };
500
+ }
501
+ // Get result from argument or file
502
+ let resultJson = resultArg;
503
+ if (resultJson && fs.existsSync(resultJson)) {
504
+ resultJson = fs.readFileSync(resultJson, 'utf-8');
505
+ }
506
+ if (!resultJson) {
507
+ // Try to find result file
508
+ const resultPath = path.join(workspaceDir, '.state', 'nocturnal', 'checkpoints', `result-${experimentId}.json`);
509
+ if (fs.existsSync(resultPath)) {
510
+ resultJson = fs.readFileSync(resultPath, 'utf-8');
511
+ }
512
+ else {
513
+ return {
514
+ text: zh
515
+ ? `错误: 未找到结果文件。请使用 --result 参数指定路径或 JSON 内容。`
516
+ : `Error: Result not found. Use --result to specify path or JSON content.`,
517
+ };
518
+ }
519
+ }
520
+ let result;
521
+ try {
522
+ result = JSON.parse(resultJson);
523
+ }
524
+ catch {
525
+ return { text: zh ? '错误: 无效的 JSON 格式' : 'Error: Invalid JSON format' };
526
+ }
527
+ // Find the training run
528
+ const runs = listTrainingRuns(workspaceDir);
529
+ const run = runs.find((r) => r.trainRunId === result.trainRunId || r.trainRunId.startsWith(experimentId));
530
+ if (!run) {
531
+ return { text: zh ? `错误: 未找到训练 Run: ${result.trainRunId}` : `Error: Training run not found: ${result.trainRunId}` };
532
+ }
533
+ // Validate spec exists
534
+ const specPath = path.join(workspaceDir, '.state', 'nocturnal', 'checkpoints', `experiment-${experimentId}.json`);
535
+ if (!fs.existsSync(specPath)) {
536
+ return {
537
+ text: zh
538
+ ? `错误: 未找到实验 spec 文件: ${specPath}`
539
+ : `Error: Experiment spec not found: ${specPath}`,
540
+ };
541
+ }
542
+ const spec = JSON.parse(fs.readFileSync(specPath, 'utf-8'));
543
+ // Process the result
544
+ const program = new TrainingProgram(workspaceDir);
545
+ let processed;
546
+ try {
547
+ processed = program.processResult({
548
+ spec,
549
+ trainRunId: run.trainRunId,
550
+ result,
551
+ });
552
+ }
553
+ catch (err) {
554
+ return {
555
+ text: zh
556
+ ? `❌ 导入失败: ${err.message}`
557
+ : `❌ Import failed: ${err.message}`,
558
+ };
559
+ }
560
+ if (processed === null) {
561
+ // dry_run: non-error outcome with no checkpoint
562
+ return {
563
+ text: zh
564
+ ? `✅ Dry-run 结果已导入(无 checkpoint)
565
+ Status: ${result.status}
566
+ 训练 Run: ${run.trainRunId}
567
+
568
+ 若需产生可部署的 checkpoint,请使用 --backend=peft-trl-orpo 或 --backend=unsloth-orpo 重试。`
569
+ : `✅ Dry-run result imported (no checkpoint)
570
+ Status: ${result.status}
571
+ Training Run: ${run.trainRunId}
572
+
573
+ To produce a deployable checkpoint, retry with --backend=peft-trl-orpo or --backend=unsloth-orpo.`,
574
+ };
575
+ }
576
+ return {
577
+ text: zh
578
+ ? `✅ 结果已导入
579
+ Status: ${result.status}
580
+ Checkpoint ID: ${processed.checkpointId}
581
+ Checkpoint Ref: ${processed.checkpointRef}
582
+ ${result.artifact ? `Artifact: ${result.artifact.artifactPath}` : ''}
583
+ ${result.metrics ? `Wall Time: ${result.metrics.wallClockMinutes} min` : ''}
584
+ ${result.failureReason ? `Failure: ${result.failureReason}` : ''}
585
+
586
+ 下一步:
587
+ 1. 运行评估: /nocturnal-train attach-eval ${processed.checkpointId} --benchmark-id=<id> --delta=<number> --verdict=<pass|fail>
588
+ 2. 查看详情: /nocturnal-train show-lineage ${processed.checkpointId}`
589
+ : `✅ Result imported
590
+ Status: ${result.status}
591
+ Checkpoint ID: ${processed.checkpointId}
592
+ Checkpoint Ref: ${processed.checkpointRef}
593
+ ${result.artifact ? `Artifact: ${result.artifact.artifactPath}` : ''}
594
+ ${result.metrics ? `Wall Time: ${result.metrics.wallClockMinutes} min` : ''}
595
+ ${result.failureReason ? `Failure: ${result.failureReason}` : ''}
596
+
597
+ Next steps:
598
+ 1. Run eval: /nocturnal-train attach-eval ${processed.checkpointId} --benchmark-id=<id> --delta=<number> --verdict=<pass|fail>
599
+ 2. View details: /nocturnal-train show-lineage ${processed.checkpointId}`,
600
+ };
601
+ }
602
+ // ── Attach Eval ──────────────────────────────────────────────────────
603
+ if (subcommand === 'attach-eval') {
604
+ const checkpointId = parts[1] || checkpointIdArg;
605
+ if (!checkpointId) {
606
+ return { text: zh ? '错误: 需要 checkpointId' : 'Error: checkpointId required' };
607
+ }
608
+ const runBenchmark = args.includes('--run-benchmark');
609
+ const baselineRefArg = parts.find((p) => p.startsWith('--baseline-ref='))?.split('=')[1];
610
+ const program = new TrainingProgram(workspaceDir);
611
+ const checkpoint = getCheckpoint(workspaceDir, checkpointId);
612
+ if (!checkpoint) {
613
+ return { text: zh ? `错误: Checkpoint 未找到: ${checkpointId}` : `Error: Checkpoint not found: ${checkpointId}` };
614
+ }
615
+ let benchmarkId = benchmarkIdArg || `bench-${Date.now()}`;
616
+ let delta = deltaArg ? parseFloat(deltaArg) : NaN;
617
+ let verdict = verdictArg === 'pass' || verdictArg === 'fail' || verdictArg === 'compare_only'
618
+ ? verdictArg
619
+ : 'compare_only';
620
+ let baselineScore = baselineScoreArg ? parseFloat(baselineScoreArg) : 0.5;
621
+ let candidateScore = candidateScoreArg ? parseFloat(candidateScoreArg) : 0.5;
622
+ const mode = (modeArg === 'prompt_assisted' ? 'prompt_assisted' : 'reduced_prompt');
623
+ // --- Run benchmark mode: execute real benchmark to get scores ---
624
+ // This closes the gap in the attach-eval command chain.
625
+ if (runBenchmark) {
626
+ // Determine baseline checkpoint ref
627
+ let baselineRef = baselineRefArg;
628
+ if (!baselineRef) {
629
+ // Try to auto-detect from deployment registry: use the currently active checkpoint as baseline
630
+ const deployment = getDeployment(workspaceDir, 'local-reader');
631
+ if (deployment?.activeCheckpointId && deployment.activeCheckpointId !== checkpointId) {
632
+ baselineRef = deployment.activeCheckpointId;
633
+ }
634
+ }
635
+ if (!baselineRef) {
636
+ return {
637
+ text: zh
638
+ ? `错误: --run-benchmark 需要 --baseline-ref 参数指定基线检查点,或当前需要有已启用的 local-reader 部署。`
639
+ : `Error: --run-benchmark requires --baseline-ref to specify the baseline checkpoint, or an active local-reader deployment must exist.`,
640
+ };
641
+ }
642
+ // Resolve both checkpoint refs to artifact paths for the scorer.
643
+ // The scorer (resolveCheckpointPath) expects filesystem paths to PEFT adapters,
644
+ // not checkpoint registry IDs. Look them up from the registry.
645
+ const baselineCheckpoint = getCheckpoint(workspaceDir, baselineRef);
646
+ if (!baselineCheckpoint) {
647
+ return {
648
+ text: zh
649
+ ? `错误: Baseline 检查点未找到: ${baselineRef}`
650
+ : `Error: Baseline checkpoint not found: ${baselineRef}`,
651
+ };
652
+ }
653
+ // Candidate checkpoint was already validated above (line 550)
654
+ // Find the export ID from the parent training run
655
+ let exportId = checkpointId;
656
+ if (checkpoint.trainRunId) {
657
+ const run = getTrainingRun(workspaceDir, checkpoint.trainRunId);
658
+ if (run?.exportId) {
659
+ exportId = run.exportId;
660
+ }
661
+ }
662
+ const scorerType = 'local-model'; // Use real model scorer
663
+ // Run benchmark via ts-node subprocess
664
+ const benchmarkScript = BENCHMARK_SCRIPT_PATH;
665
+ const outputDir = path.join(workspaceDir, '.state', 'nocturnal', 'evals');
666
+ // Build the compare command - pass ARTIFACT PATHS as separate arguments
667
+ // to avoid shell injection when paths contain special characters.
668
+ // Use execFileSync to pass arguments as an array (no shell interpolation).
669
+ const cmdArgs = [
670
+ '--yes',
671
+ 'ts-node',
672
+ benchmarkScript,
673
+ 'compare',
674
+ `--export-id=${exportId}`,
675
+ `--baseline=${baselineCheckpoint.artifactPath}`,
676
+ `--candidate=${checkpoint.artifactPath}`,
677
+ `--mode=${mode}`,
678
+ `--scorer=${scorerType}`,
679
+ `--output-dir=${outputDir}`,
680
+ ];
681
+ let benchmarkResult = null;
682
+ let benchmarkError = '';
683
+ try {
684
+ // Use execFileSync to avoid shell injection — paths are passed as args, not interpolated
685
+ const stdout = execFileSync('npx', cmdArgs, {
686
+ cwd: process.cwd(),
687
+ timeout: 300000, // 5 min timeout
688
+ encoding: 'utf-8',
689
+ });
690
+ // stdout is the JSON result from run-benchmark
691
+ try {
692
+ benchmarkResult = JSON.parse(stdout.trim());
693
+ }
694
+ catch {
695
+ benchmarkError = `Failed to parse benchmark output: ${stdout.substring(0, 200)}`;
696
+ }
697
+ }
698
+ catch (err) {
699
+ // execSync throws on non-zero exit code; stdout may contain partial data
700
+ const stdout = err.stdout ?? '';
701
+ try {
702
+ benchmarkResult = JSON.parse(stdout.trim());
703
+ }
704
+ catch {
705
+ benchmarkError = `Benchmark failed: ${err.message}. stdout: ${stdout.substring(0, 200)}`;
706
+ }
707
+ }
708
+ if (benchmarkError || !benchmarkResult) {
709
+ return {
710
+ text: zh
711
+ ? `❌ Benchmark 执行失败: ${benchmarkError || '无法解析结果'}`
712
+ : `❌ Benchmark execution failed: ${benchmarkError || 'Could not parse result'}`,
713
+ };
714
+ }
715
+ delta = benchmarkResult.delta.delta;
716
+ baselineScore = benchmarkResult.delta.baselineScore;
717
+ candidateScore = benchmarkResult.delta.candidateScore;
718
+ benchmarkId = benchmarkResult.benchmarkId;
719
+ verdict = benchmarkResult.verdict;
720
+ }
721
+ else {
722
+ // Manual mode: require explicit delta and verdict
723
+ if (!deltaArg || !verdictArg) {
724
+ return {
725
+ text: zh
726
+ ? '错误: 需要 --benchmark-id, --delta, --verdict 参数(或使用 --run-benchmark 自动运行)'
727
+ : 'Error: --benchmark-id, --delta, --verdict are required (or use --run-benchmark to auto-run)',
728
+ };
729
+ }
730
+ if (isNaN(delta)) {
731
+ return { text: zh ? '错误: delta 必须是数字' : 'Error: delta must be a number' };
732
+ }
733
+ }
734
+ const evalSummary = {
735
+ evalId: `eval-${Date.now()}`,
736
+ checkpointId,
737
+ benchmarkId,
738
+ targetModelFamily: checkpoint.targetModelFamily,
739
+ mode,
740
+ baselineScore,
741
+ candidateScore,
742
+ delta,
743
+ verdict,
744
+ };
745
+ try {
746
+ program.attachEvalAndMarkDeployable(checkpointId, evalSummary);
747
+ const deployable = verdict === 'pass' || verdict === 'compare_only';
748
+ return {
749
+ text: zh
750
+ ? `✅ 评估已附加${runBenchmark ? '(自动 Benchmark)' : ''}
751
+ Checkpoint: ${checkpointId.substring(0, 8)}...
752
+ Benchmark: ${benchmarkId}
753
+ 基线分数: ${baselineScore.toFixed(4)}
754
+ 候选分数: ${candidateScore.toFixed(4)}
755
+ Delta: ${delta >= 0 ? '+' : ''}${delta.toFixed(4)}
756
+ Verdict: ${verdict}
757
+ Mode: ${mode}
758
+ Deployable: ${deployable ? 'Yes' : 'No'}
759
+
760
+ 下一步:
761
+ 1. 评估晋升: /nocturnal-rollout evaluate-promotion ${checkpointId}
762
+ 2. 绑定部署: /nocturnal-rollout bind ${checkpointId} --profile=local-reader`
763
+ : `✅ Eval attached${runBenchmark ? ' (auto benchmark)' : ''}
764
+ Checkpoint: ${checkpointId.substring(0, 8)}...
765
+ Benchmark: ${benchmarkId}
766
+ Baseline Score: ${baselineScore.toFixed(4)}
767
+ Candidate Score: ${candidateScore.toFixed(4)}
768
+ Delta: ${delta >= 0 ? '+' : ''}${delta.toFixed(4)}
769
+ Verdict: ${verdict}
770
+ Mode: ${mode}
771
+ Deployable: ${deployable ? 'Yes' : 'No'}
772
+
773
+ Next steps:
774
+ 1. Evaluate promotion: /nocturnal-rollout evaluate-promotion ${checkpointId}
775
+ 2. Bind deployment: /nocturnal-rollout bind ${checkpointId} --profile=local-reader`,
776
+ };
777
+ }
778
+ catch (err) {
779
+ return {
780
+ text: zh
781
+ ? `❌ 附加评估失败: ${err.message}`
782
+ : `❌ Attach eval failed: ${err.message}`,
783
+ };
784
+ }
785
+ }
786
+ // ── Show Lineage ─────────────────────────────────────────────────────
787
+ if (subcommand === 'show-lineage') {
788
+ const checkpointId = parts[1];
789
+ if (!checkpointId) {
790
+ return { text: zh ? '错误: 需要 checkpointId' : 'Error: checkpointId required' };
791
+ }
792
+ const lineage = getCheckpointLineage(workspaceDir, checkpointId);
793
+ if (!lineage) {
794
+ return { text: zh ? `未找到 lineage: ${checkpointId}` : `Lineage not found: ${checkpointId}` };
795
+ }
796
+ const { run, checkpoint, eval: eval_ } = lineage;
797
+ let text = zh
798
+ ? `=== Checkpoint Lineage ===
799
+ Checkpoint: ${checkpoint.checkpointId}
800
+ Family: ${checkpoint.targetModelFamily}
801
+ Deployable: ${checkpoint.deployable}
802
+ Artifact: ${checkpoint.artifactPath}
803
+
804
+ --- Training Run ---
805
+ ${formatTrainingRun(run, zh)}
806
+
807
+ --- Eval Summary ---`
808
+ : `=== Checkpoint Lineage ===
809
+ Checkpoint: ${checkpoint.checkpointId}
810
+ Family: ${checkpoint.targetModelFamily}
811
+ Deployable: ${checkpoint.deployable}
812
+ Artifact: ${checkpoint.artifactPath}
813
+
814
+ --- Training Run ---
815
+ ${formatTrainingRun(run, zh)}
816
+
817
+ --- Eval Summary ---`;
818
+ if (eval_) {
819
+ text += `
820
+ ID: ${eval_.evalId}
821
+ Mode: ${eval_.mode}
822
+ Delta: ${eval_.delta >= 0 ? '+' : ''}${eval_.delta.toFixed(4)}
823
+ Baseline: ${eval_.baselineScore.toFixed(3)}
824
+ Candidate: ${eval_.candidateScore.toFixed(3)}
825
+ Verdict: ${eval_.verdict}`;
826
+ }
827
+ else {
828
+ text += zh ? '\n(无)' : '\n(None)';
829
+ }
830
+ return { text };
831
+ }
832
+ // ── List Experiments ──────────────────────────────────────────────────
833
+ if (subcommand === 'list-experiments') {
834
+ const runs = listTrainingRuns(workspaceDir);
835
+ if (runs.length === 0) {
836
+ return { text: zh ? '没有训练实验' : 'No training experiments' };
837
+ }
838
+ const lines = runs.slice(0, 20).map((run) => {
839
+ const date = new Date(run.createdAt).toLocaleDateString();
840
+ return `${run.trainRunId.substring(0, 8)}... | ${run.status} | ${run.targetModelFamily} | ${date} | ${run.checkpointIds.length} ckpts`;
841
+ });
842
+ return {
843
+ text: zh
844
+ ? `训练实验 (${runs.length}):
845
+ ${lines.join('\n')}`
846
+ : `Training experiments (${runs.length}):
847
+ ${lines.join('\n')}`,
848
+ };
849
+ }
850
+ // ── List Checkpoints ─────────────────────────────────────────────────
851
+ if (subcommand === 'list-checkpoints') {
852
+ const checkpoints = listCheckpoints(workspaceDir);
853
+ if (checkpoints.length === 0) {
854
+ return { text: zh ? '没有 Checkpoint' : 'No checkpoints' };
855
+ }
856
+ const filtered = familyArg
857
+ ? checkpoints.filter((cp) => cp.targetModelFamily.includes(familyArg))
858
+ : checkpoints;
859
+ if (filtered.length === 0) {
860
+ return { text: zh ? '没有匹配的 Checkpoint' : 'No matching checkpoints' };
861
+ }
862
+ const lines = filtered.slice(0, 20).map((cp) => {
863
+ const date = new Date(cp.createdAt).toLocaleDateString();
864
+ return `${cp.checkpointId.substring(0, 8)}... | ${cp.deployable ? 'deployable' : 'not-deployable'} | ${cp.targetModelFamily} | ${date}`;
865
+ });
866
+ return {
867
+ text: zh
868
+ ? `Checkpoints (${filtered.length}):
869
+ ${lines.join('\n')}`
870
+ : `Checkpoints (${filtered.length}):
871
+ ${lines.join('\n')}`,
872
+ };
873
+ }
874
+ // ── Stats ────────────────────────────────────────────────────────────
875
+ if (subcommand === 'stats') {
876
+ const stats = getTrainingRegistryStats(workspaceDir);
877
+ return {
878
+ text: zh
879
+ ? `=== 训练注册统计 ===
880
+ 总实验数: ${stats.totalRuns}
881
+ 完成: ${stats.completedRuns}
882
+ 失败: ${stats.failedRuns}
883
+ 进行中: ${stats.pendingRuns + stats.runningRuns}
884
+
885
+ 总 Checkpoint: ${stats.totalCheckpoints}
886
+ 可部署: ${stats.deployableCheckpoints}
887
+
888
+ 总评估: ${stats.totalEvals}
889
+ 通过: ${stats.passingEvals}
890
+ 失败: ${stats.failingEvals}`
891
+ : `=== Training Registry Stats ===
892
+ Total runs: ${stats.totalRuns}
893
+ Completed: ${stats.completedRuns}
894
+ Failed: ${stats.failedRuns}
895
+ In progress: ${stats.pendingRuns + stats.runningRuns}
896
+
897
+ Total checkpoints: ${stats.totalCheckpoints}
898
+ Deployable: ${stats.deployableCheckpoints}
899
+
900
+ Total evals: ${stats.totalEvals}
901
+ Passing: ${stats.passingEvals}
902
+ Failing: ${stats.failingEvals}`,
903
+ };
904
+ }
905
+ // Unknown subcommand
906
+ return {
907
+ text: zh
908
+ ? `未知子命令: ${subcommand}。运行 /nocturnal-train help 查看帮助。`
909
+ : `Unknown subcommand: ${subcommand}. Run /nocturnal-train help for usage.`,
910
+ };
911
+ }
912
+ catch (err) {
913
+ return {
914
+ text: zh
915
+ ? `❌ 命令失败: ${err.message}`
916
+ : `❌ Command failed: ${err.message}`,
917
+ };
918
+ }
919
+ }