principles-disciple 1.7.6 → 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 (106) hide show
  1. package/dist/commands/context.js +5 -15
  2. package/dist/commands/evolution-status.js +2 -9
  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/constants/tools.d.ts +2 -2
  12. package/dist/constants/tools.js +1 -1
  13. package/dist/core/adaptive-thresholds.d.ts +186 -0
  14. package/dist/core/adaptive-thresholds.js +300 -0
  15. package/dist/core/config.d.ts +2 -38
  16. package/dist/core/config.js +6 -61
  17. package/dist/core/event-log.d.ts +1 -2
  18. package/dist/core/event-log.js +0 -3
  19. package/dist/core/evolution-engine.js +1 -21
  20. package/dist/core/evolution-reducer.d.ts +7 -1
  21. package/dist/core/evolution-reducer.js +56 -4
  22. package/dist/core/evolution-types.d.ts +61 -9
  23. package/dist/core/evolution-types.js +31 -9
  24. package/dist/core/external-training-contract.d.ts +276 -0
  25. package/dist/core/external-training-contract.js +269 -0
  26. package/dist/core/local-worker-routing.d.ts +175 -0
  27. package/dist/core/local-worker-routing.js +525 -0
  28. package/dist/core/model-deployment-registry.d.ts +218 -0
  29. package/dist/core/model-deployment-registry.js +503 -0
  30. package/dist/core/model-training-registry.d.ts +295 -0
  31. package/dist/core/model-training-registry.js +475 -0
  32. package/dist/core/nocturnal-arbiter.d.ts +159 -0
  33. package/dist/core/nocturnal-arbiter.js +534 -0
  34. package/dist/core/nocturnal-candidate-scoring.d.ts +137 -0
  35. package/dist/core/nocturnal-candidate-scoring.js +266 -0
  36. package/dist/core/nocturnal-compliance.d.ts +175 -0
  37. package/dist/core/nocturnal-compliance.js +824 -0
  38. package/dist/core/nocturnal-dataset.d.ts +224 -0
  39. package/dist/core/nocturnal-dataset.js +443 -0
  40. package/dist/core/nocturnal-executability.d.ts +85 -0
  41. package/dist/core/nocturnal-executability.js +331 -0
  42. package/dist/core/nocturnal-export.d.ts +124 -0
  43. package/dist/core/nocturnal-export.js +275 -0
  44. package/dist/core/nocturnal-paths.d.ts +124 -0
  45. package/dist/core/nocturnal-paths.js +214 -0
  46. package/dist/core/nocturnal-trajectory-extractor.d.ts +242 -0
  47. package/dist/core/nocturnal-trajectory-extractor.js +307 -0
  48. package/dist/core/nocturnal-trinity.d.ts +311 -0
  49. package/dist/core/nocturnal-trinity.js +880 -0
  50. package/dist/core/paths.d.ts +6 -0
  51. package/dist/core/paths.js +6 -0
  52. package/dist/core/principle-training-state.d.ts +121 -0
  53. package/dist/core/principle-training-state.js +321 -0
  54. package/dist/core/promotion-gate.d.ts +238 -0
  55. package/dist/core/promotion-gate.js +529 -0
  56. package/dist/core/session-tracker.d.ts +10 -0
  57. package/dist/core/session-tracker.js +14 -0
  58. package/dist/core/shadow-observation-registry.d.ts +217 -0
  59. package/dist/core/shadow-observation-registry.js +308 -0
  60. package/dist/core/training-program.d.ts +233 -0
  61. package/dist/core/training-program.js +433 -0
  62. package/dist/core/trajectory.d.ts +95 -1
  63. package/dist/core/trajectory.js +220 -6
  64. package/dist/core/workspace-context.d.ts +0 -6
  65. package/dist/core/workspace-context.js +0 -12
  66. package/dist/hooks/bash-risk.d.ts +6 -6
  67. package/dist/hooks/bash-risk.js +8 -8
  68. package/dist/hooks/gate-block-helper.js +1 -1
  69. package/dist/hooks/gate.d.ts +1 -1
  70. package/dist/hooks/gate.js +2 -2
  71. package/dist/hooks/gfi-gate.d.ts +3 -3
  72. package/dist/hooks/gfi-gate.js +15 -14
  73. package/dist/hooks/pain.js +6 -9
  74. package/dist/hooks/progressive-trust-gate.d.ts +21 -49
  75. package/dist/hooks/progressive-trust-gate.js +51 -204
  76. package/dist/hooks/prompt.d.ts +11 -11
  77. package/dist/hooks/prompt.js +158 -72
  78. package/dist/hooks/subagent.js +43 -6
  79. package/dist/i18n/commands.js +8 -8
  80. package/dist/index.js +129 -28
  81. package/dist/service/evolution-worker.d.ts +42 -4
  82. package/dist/service/evolution-worker.js +321 -13
  83. package/dist/service/nocturnal-runtime.d.ts +183 -0
  84. package/dist/service/nocturnal-runtime.js +352 -0
  85. package/dist/service/nocturnal-service.d.ts +163 -0
  86. package/dist/service/nocturnal-service.js +787 -0
  87. package/dist/service/nocturnal-target-selector.d.ts +145 -0
  88. package/dist/service/nocturnal-target-selector.js +315 -0
  89. package/dist/service/phase3-input-filter.d.ts +2 -23
  90. package/dist/service/phase3-input-filter.js +3 -27
  91. package/dist/service/runtime-summary-service.d.ts +0 -10
  92. package/dist/service/runtime-summary-service.js +1 -54
  93. package/dist/tools/deep-reflect.js +2 -1
  94. package/dist/types/event-types.d.ts +2 -10
  95. package/dist/types/runtime-summary.d.ts +1 -8
  96. package/dist/types.d.ts +0 -3
  97. package/dist/types.js +0 -2
  98. package/openclaw.plugin.json +1 -1
  99. package/package.json +1 -1
  100. package/templates/langs/en/skills/pd-mentor/SKILL.md +5 -5
  101. package/templates/langs/zh/skills/pd-mentor/SKILL.md +5 -5
  102. package/templates/pain_settings.json +0 -6
  103. package/dist/commands/trust.d.ts +0 -4
  104. package/dist/commands/trust.js +0 -78
  105. package/dist/core/trust-engine.d.ts +0 -96
  106. package/dist/core/trust-engine.js +0 -286
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Nocturnal Runtime Service — Idle Detection Source of Truth
3
+ * ===========================================================
4
+ *
5
+ * This module is the authoritative source for workspace idle state used by the
6
+ * nocturnal reflection pipeline. It must NOT use `.last_active.json` as the primary
7
+ * source of truth.
8
+ *
9
+ * SOURCE OF TRUTH HIERARCHY (ordered by priority):
10
+ * 1. SessionState.lastActivityAt — via listSessions(workspaceDir)
11
+ * 2. trajectory timestamps — secondary guardrail only, NOT primary
12
+ * 3. nocturnal-runtime.json — cooldown/quota bookkeeping (ephemeral state)
13
+ *
14
+ * DESIGN CONSTRAINTS:
15
+ * - No `.last_active.json` as primary idle source
16
+ * - trajectory timestamps are a guardrail, not the primary source
17
+ * - cooldown/quota state is persisted in nocturnal-runtime.json
18
+ * - abandoned sessions (>2h inactive) must not block nocturnal flow
19
+ */
20
+ import * as fs from 'fs';
21
+ import * as path from 'path';
22
+ import { listSessions } from '../core/session-tracker.js';
23
+ import { withLockAsync } from '../utils/file-lock.js';
24
+ // ---------------------------------------------------------------------------
25
+ // Constants
26
+ // ---------------------------------------------------------------------------
27
+ /** File name for nocturnal runtime bookkeeping */
28
+ export const NOCTURNAL_RUNTIME_FILE = 'nocturnal-runtime.json';
29
+ /** Default idle threshold: workspace is considered idle if no activity for this duration (ms) */
30
+ export const DEFAULT_IDLE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
31
+ /** Default cooldown between nocturnal runs (ms) */
32
+ export const DEFAULT_GLOBAL_COOLDOWN_MS = 60 * 60 * 1000; // 1 hour
33
+ /** Default per-principle cooldown (ms) */
34
+ export const DEFAULT_PRINCIPLE_COOLDOWN_MS = 6 * 60 * 60 * 1000; // 6 hours
35
+ /** Default maximum nocturnal runs per quota window */
36
+ export const DEFAULT_MAX_RUNS_PER_WINDOW = 3;
37
+ /** Default quota window size (ms) */
38
+ export const DEFAULT_QUOTA_WINDOW_MS = 24 * 60 * 60 * 1000; // 24 hours
39
+ /** Abandoned session threshold: sessions inactive for longer than this are ignored (ms) */
40
+ export const DEFAULT_ABANDONED_THRESHOLD_MS = 2 * 60 * 60 * 1000; // 2 hours
41
+ // ---------------------------------------------------------------------------
42
+ // Default State
43
+ // ---------------------------------------------------------------------------
44
+ function createDefaultState() {
45
+ return {
46
+ principleCooldowns: {},
47
+ recentRunTimestamps: [],
48
+ };
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // File Operations (with locking)
52
+ // ---------------------------------------------------------------------------
53
+ async function readState(stateDir) {
54
+ const filePath = path.join(stateDir, NOCTURNAL_RUNTIME_FILE);
55
+ if (!fs.existsSync(filePath)) {
56
+ return createDefaultState();
57
+ }
58
+ try {
59
+ const raw = fs.readFileSync(filePath, 'utf-8');
60
+ const parsed = JSON.parse(raw);
61
+ // Ensure required fields exist (migration-safe)
62
+ return {
63
+ principleCooldowns: parsed.principleCooldowns ?? {},
64
+ recentRunTimestamps: parsed.recentRunTimestamps ?? [],
65
+ lastRunAt: parsed.lastRunAt,
66
+ lastSuccessfulRunAt: parsed.lastSuccessfulRunAt,
67
+ globalCooldownUntil: parsed.globalCooldownUntil,
68
+ lastRunMeta: parsed.lastRunMeta,
69
+ };
70
+ }
71
+ catch {
72
+ // Corrupted file — start fresh
73
+ return createDefaultState();
74
+ }
75
+ }
76
+ function readStateSync(stateDir) {
77
+ const filePath = path.join(stateDir, NOCTURNAL_RUNTIME_FILE);
78
+ if (!fs.existsSync(filePath)) {
79
+ return createDefaultState();
80
+ }
81
+ try {
82
+ const raw = fs.readFileSync(filePath, 'utf-8');
83
+ const parsed = JSON.parse(raw);
84
+ return {
85
+ principleCooldowns: parsed.principleCooldowns ?? {},
86
+ recentRunTimestamps: parsed.recentRunTimestamps ?? [],
87
+ lastRunAt: parsed.lastRunAt,
88
+ lastSuccessfulRunAt: parsed.lastSuccessfulRunAt,
89
+ globalCooldownUntil: parsed.globalCooldownUntil,
90
+ lastRunMeta: parsed.lastRunMeta,
91
+ };
92
+ }
93
+ catch (err) {
94
+ console.warn(`[nocturnal-runtime] State file corrupted, resetting: ${err instanceof Error ? err.message : String(err)}`);
95
+ return createDefaultState();
96
+ }
97
+ }
98
+ async function writeState(stateDir, state) {
99
+ const filePath = path.join(stateDir, NOCTURNAL_RUNTIME_FILE);
100
+ const stateDirPath = path.dirname(filePath);
101
+ if (!fs.existsSync(stateDirPath)) {
102
+ fs.mkdirSync(stateDirPath, { recursive: true });
103
+ }
104
+ await withLockAsync(filePath, async () => {
105
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2), 'utf-8');
106
+ });
107
+ }
108
+ // ---------------------------------------------------------------------------
109
+ // Idle Detection
110
+ // ---------------------------------------------------------------------------
111
+ /**
112
+ * Check if the workspace is currently idle based on session activity.
113
+ *
114
+ * IDLE DETERMINATION LOGIC:
115
+ * - Collect all sessions for the workspace via listSessions()
116
+ * - Filter out abandoned sessions (inactive > abandonedThresholdMs)
117
+ * - Workspace is idle if: no active sessions OR all active sessions have lastActivityAt older than idleThresholdMs
118
+ * - Abandoned sessions do NOT contribute to idle determination
119
+ *
120
+ * @param workspaceDir - Workspace directory to check
121
+ * @param options.idleThresholdMs - Consider idle if no activity for this duration (default: 30 min)
122
+ * @param options.abandonedThresholdMs - Consider session abandoned if inactive for this duration (default: 2 hr)
123
+ * @param trajectoryLastActivityAt - Optional trajectory timestamp as secondary guardrail
124
+ * @returns IdleCheckResult with full diagnostic information
125
+ */
126
+ export function checkWorkspaceIdle(workspaceDir, options = {}, trajectoryLastActivityAt) {
127
+ const { idleThresholdMs = DEFAULT_IDLE_THRESHOLD_MS, abandonedThresholdMs = DEFAULT_ABANDONED_THRESHOLD_MS, } = options;
128
+ const now = Date.now();
129
+ const sessions = listSessions(workspaceDir);
130
+ // Separate active vs abandoned sessions
131
+ const abandonedSessions = [];
132
+ let mostRecentActivityAt = 0;
133
+ let activeSessionCount = 0;
134
+ for (const session of sessions) {
135
+ const inactiveFor = now - session.lastActivityAt;
136
+ if (inactiveFor > abandonedThresholdMs) {
137
+ abandonedSessions.push(session.sessionId);
138
+ }
139
+ else {
140
+ activeSessionCount++;
141
+ if (session.lastActivityAt > mostRecentActivityAt) {
142
+ mostRecentActivityAt = session.lastActivityAt;
143
+ }
144
+ }
145
+ }
146
+ const idleForMs = mostRecentActivityAt > 0 ? now - mostRecentActivityAt : now;
147
+ const isIdle = mostRecentActivityAt === 0 || idleForMs > idleThresholdMs;
148
+ // Trajectory guardrail: only used as a secondary check
149
+ // If trajectory says there's recent activity but session state says idle,
150
+ // that's a discrepancy we should note but still trust session state as primary
151
+ let trajectoryGuardrailConfirmsIdle = true;
152
+ if (trajectoryLastActivityAt !== undefined) {
153
+ const trajectoryIdleFor = now - trajectoryLastActivityAt;
154
+ // Guardrail confirms if trajectory also shows idle or near-idle (>80% of threshold)
155
+ trajectoryGuardrailConfirmsIdle = trajectoryIdleFor > idleThresholdMs * 0.8;
156
+ }
157
+ let reason;
158
+ if (mostRecentActivityAt === 0) {
159
+ reason = 'No active sessions found — workspace is idle';
160
+ }
161
+ else if (isIdle) {
162
+ reason = `Most recent activity ${idleForMs}ms ago (>${idleThresholdMs}ms threshold)`;
163
+ }
164
+ else {
165
+ reason = `Recent activity ${idleForMs}ms ago (<${idleThresholdMs}ms threshold)`;
166
+ }
167
+ if (abandonedSessions.length > 0) {
168
+ reason += `; ${abandonedSessions.length} abandoned session(s) ignored`;
169
+ }
170
+ return {
171
+ isIdle,
172
+ mostRecentActivityAt,
173
+ idleForMs,
174
+ activeSessionCount,
175
+ abandonedSessionIds: abandonedSessions,
176
+ trajectoryGuardrailConfirmsIdle,
177
+ reason,
178
+ };
179
+ }
180
+ // ---------------------------------------------------------------------------
181
+ // Cooldown Management
182
+ // ---------------------------------------------------------------------------
183
+ /**
184
+ * Check if the workspace is currently in a cooldown period.
185
+ *
186
+ * @param stateDir - State directory
187
+ * @param principleId - Optional principle ID to check per-principle cooldown
188
+ * @param options - Cooldown configuration options
189
+ * @returns CooldownCheckResult
190
+ */
191
+ export function checkCooldown(stateDir, principleId, options = {}) {
192
+ const { globalCooldownMs = DEFAULT_GLOBAL_COOLDOWN_MS, principleCooldownMs = DEFAULT_PRINCIPLE_COOLDOWN_MS, maxRunsPerWindow = DEFAULT_MAX_RUNS_PER_WINDOW, quotaWindowMs = DEFAULT_QUOTA_WINDOW_MS, } = options;
193
+ const now = Date.now();
194
+ const state = readStateSync(stateDir);
195
+ // Global cooldown check
196
+ let globalCooldownActive = false;
197
+ let globalCooldownRemainingMs = 0;
198
+ let globalCooldownUntil = null;
199
+ if (state.globalCooldownUntil) {
200
+ const cooldownEnd = new Date(state.globalCooldownUntil).getTime();
201
+ if (cooldownEnd > now) {
202
+ globalCooldownActive = true;
203
+ globalCooldownRemainingMs = cooldownEnd - now;
204
+ globalCooldownUntil = state.globalCooldownUntil;
205
+ }
206
+ }
207
+ // Principle-specific cooldown check
208
+ let principleCooldownActive = false;
209
+ let principleCooldownRemainingMs = 0;
210
+ let principleCooldownUntil = null;
211
+ if (principleId && state.principleCooldowns[principleId]) {
212
+ const cooldownEnd = new Date(state.principleCooldowns[principleId]).getTime();
213
+ if (cooldownEnd > now) {
214
+ principleCooldownActive = true;
215
+ principleCooldownRemainingMs = cooldownEnd - now;
216
+ principleCooldownUntil = state.principleCooldowns[principleId];
217
+ }
218
+ }
219
+ // Quota check: count runs in sliding window
220
+ const windowStart = now - quotaWindowMs;
221
+ const recentRuns = state.recentRunTimestamps
222
+ .map(ts => new Date(ts).getTime())
223
+ .filter(ts => ts > windowStart);
224
+ const quotaExhausted = recentRuns.length >= maxRunsPerWindow;
225
+ const runsRemaining = Math.max(0, maxRunsPerWindow - recentRuns.length);
226
+ return {
227
+ globalCooldownActive,
228
+ globalCooldownUntil,
229
+ globalCooldownRemainingMs,
230
+ principleCooldownActive,
231
+ principleCooldownUntil,
232
+ principleCooldownRemainingMs,
233
+ quotaExhausted,
234
+ runsRemaining,
235
+ };
236
+ }
237
+ /**
238
+ * Record that a nocturnal run has started.
239
+ * Updates global cooldown and quota tracking.
240
+ *
241
+ * @param stateDir - State directory
242
+ * @param principleId - Target principle ID for this run
243
+ */
244
+ export async function recordRunStart(stateDir, principleId) {
245
+ const state = await readState(stateDir);
246
+ const now = new Date().toISOString();
247
+ state.lastRunAt = now;
248
+ state.lastRunMeta = {
249
+ targetPrincipleId: principleId,
250
+ status: 'skipped', // Will be updated on completion
251
+ };
252
+ // Set global cooldown
253
+ const cooldownUntil = new Date(Date.now() + DEFAULT_GLOBAL_COOLDOWN_MS).toISOString();
254
+ state.globalCooldownUntil = cooldownUntil;
255
+ // Add to recent runs for quota tracking
256
+ state.recentRunTimestamps.push(now);
257
+ // Prune old timestamps outside the quota window
258
+ const windowStart = Date.now() - DEFAULT_QUOTA_WINDOW_MS;
259
+ state.recentRunTimestamps = state.recentRunTimestamps
260
+ .map(ts => new Date(ts).getTime())
261
+ .filter(ts => ts > windowStart)
262
+ .map(ts => new Date(ts).toISOString());
263
+ await writeState(stateDir, state);
264
+ }
265
+ /**
266
+ * Record the outcome of a nocturnal run.
267
+ *
268
+ * @param stateDir - State directory
269
+ * @param outcome - 'success', 'failed', or 'skipped'
270
+ * @param details - Optional details about the run
271
+ */
272
+ export async function recordRunEnd(stateDir, outcome, details) {
273
+ const state = await readState(stateDir);
274
+ const now = new Date().toISOString();
275
+ if (outcome === 'success') {
276
+ state.lastSuccessfulRunAt = now;
277
+ // Also set per-principle cooldown if we know which principle was targeted
278
+ if (state.lastRunMeta?.targetPrincipleId) {
279
+ const pid = state.lastRunMeta.targetPrincipleId;
280
+ state.principleCooldowns[pid] = new Date(Date.now() + DEFAULT_PRINCIPLE_COOLDOWN_MS).toISOString();
281
+ }
282
+ }
283
+ // Update run metadata
284
+ state.lastRunMeta = {
285
+ ...state.lastRunMeta,
286
+ status: outcome,
287
+ sampleCount: details?.sampleCount ?? state.lastRunMeta?.sampleCount,
288
+ reason: details?.reason ?? state.lastRunMeta?.reason,
289
+ };
290
+ // Note: global cooldown remains active (set at run start) - we don't clear it on failure
291
+ // This prevents rapid retry loops
292
+ await writeState(stateDir, state);
293
+ }
294
+ /**
295
+ * Clear all cooldowns (for testing or admin reset).
296
+ *
297
+ * @param stateDir - State directory
298
+ */
299
+ export async function clearAllCooldowns(stateDir) {
300
+ const state = await readState(stateDir);
301
+ state.globalCooldownUntil = undefined;
302
+ state.principleCooldowns = {};
303
+ state.recentRunTimestamps = [];
304
+ state.lastRunMeta = undefined;
305
+ await writeState(stateDir, state);
306
+ }
307
+ /**
308
+ * Get the current runtime state (for debugging/inspection).
309
+ *
310
+ * @param stateDir - State directory
311
+ * @returns The current NocturnalRuntimeState
312
+ */
313
+ export async function getRuntimeState(stateDir) {
314
+ return readState(stateDir);
315
+ }
316
+ /**
317
+ * Combined pre-flight check for whether a nocturnal run should proceed.
318
+ * Integrates idle + cooldown + quota checks.
319
+ *
320
+ * @param workspaceDir - Workspace directory
321
+ * @param stateDir - State directory
322
+ * @param principleId - Target principle ID
323
+ * @param trajectoryLastActivityAt - Optional trajectory timestamp as secondary guardrail
324
+ * @param idleCheckOverride - Optional override for idle check result (for testing)
325
+ */
326
+ export function checkPreflight(workspaceDir, stateDir, principleId, trajectoryLastActivityAt, idleCheckOverride) {
327
+ const idle = idleCheckOverride ?? checkWorkspaceIdle(workspaceDir, {}, trajectoryLastActivityAt);
328
+ const cooldown = checkCooldown(stateDir, principleId);
329
+ const blockers = [];
330
+ if (!idle.isIdle) {
331
+ blockers.push(`Workspace not idle (active for ${idle.idleForMs}ms, threshold=${DEFAULT_IDLE_THRESHOLD_MS}ms)`);
332
+ }
333
+ if (cooldown.globalCooldownActive) {
334
+ blockers.push(`Global cooldown active until ${cooldown.globalCooldownUntil}`);
335
+ }
336
+ if (cooldown.principleCooldownActive) {
337
+ blockers.push(`Principle cooldown active until ${cooldown.principleCooldownUntil}`);
338
+ }
339
+ if (cooldown.quotaExhausted) {
340
+ blockers.push(`Quota exhausted (${DEFAULT_MAX_RUNS_PER_WINDOW} runs per ${DEFAULT_QUOTA_WINDOW_MS / 3600000}h window)`);
341
+ }
342
+ if (idle.abandonedSessionIds.length > 0 && idle.activeSessionCount === 0) {
343
+ // Only block if ALL sessions are abandoned (meaning workspace truly has no activity)
344
+ // If some sessions are active, we trust the session-based idle check
345
+ }
346
+ return {
347
+ canRun: blockers.length === 0,
348
+ idle,
349
+ cooldown,
350
+ blockers,
351
+ };
352
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Nocturnal Service — Trinity Reflection Pipeline Orchestrator
3
+ * ============================================================
4
+ *
5
+ * PURPOSE: Orchestrate the complete nocturnal reflection pipeline:
6
+ * 1. Workspace idle check
7
+ * 2. Target selection (principle + session)
8
+ * 3. Trajectory snapshot extraction
9
+ * 4. Trinity artifact generation (Dreamer -> Philosopher -> Scribe)
10
+ * OR single-reflector fallback (if Trinity disabled or fails)
11
+ * 5. Arbiter validation
12
+ * 6. Executability check
13
+ * 7. Artifact persistence
14
+ * 8. Cooldown recording
15
+ *
16
+ * DESIGN CONSTRAINTS (Phase 6):
17
+ * - Trinity is configurable (useTrinity flag)
18
+ * - Single-reflector fallback preserved if Trinity fails
19
+ * - All stage I/O is structured JSON contracts
20
+ * - Any malformed stage output fails the entire chain closed
21
+ * - Final artifact still passes arbiter + executability validation
22
+ * - Telemetry records chain mode, stage outcomes, candidate counts
23
+ * - No real training export (Phase 3+ only)
24
+ * - No auto-deployment
25
+ * - Approved artifacts go to .state/nocturnal/samples/{artifactId}.json
26
+ * - Cooldown recorded via nocturnal-runtime.ts
27
+ *
28
+ * THIS IS THE MAIN ORCHESTRATOR — all other nocturnal modules are called from here.
29
+ */
30
+ import { type NocturnalSessionSnapshot } from '../core/nocturnal-trajectory-extractor.js';
31
+ import { type NocturnalSelectionResult, type SkipReason } from './nocturnal-target-selector.js';
32
+ import { type NocturnalArtifact, type ArbiterResult } from '../core/nocturnal-arbiter.js';
33
+ import { type TrinityConfig, type TrinityResult, type TrinityRuntimeAdapter } from '../core/nocturnal-trinity.js';
34
+ import { type BoundedAction } from '../core/nocturnal-executability.js';
35
+ import { type IdleCheckResult, type PreflightCheckResult } from './nocturnal-runtime.js';
36
+ /**
37
+ * Result of a complete nocturnal reflection run.
38
+ */
39
+ export interface NocturnalRunResult {
40
+ /** Whether the run produced an approved artifact */
41
+ success: boolean;
42
+ /** The approved artifact (if success === true) */
43
+ artifact?: NocturnalArtifact & {
44
+ boundedAction?: BoundedAction;
45
+ };
46
+ /** Skip reason (if success === false because nothing to do) */
47
+ skipReason?: SkipReason;
48
+ /** Whether the selector found no target */
49
+ noTargetSelected: boolean;
50
+ /** Whether the reflector rejected or artifact failed validation */
51
+ validationFailed: boolean;
52
+ /** Validation failure reasons */
53
+ validationFailures: string[];
54
+ /** Snapshot used for reflection */
55
+ snapshot?: NocturnalSessionSnapshot;
56
+ /** Diagnostics from each pipeline stage */
57
+ diagnostics: NocturnalRunDiagnostics;
58
+ /** Trinity telemetry (if Trinity was used) */
59
+ trinityTelemetry?: TrinityResult['telemetry'];
60
+ }
61
+ /**
62
+ * Diagnostics from each pipeline stage.
63
+ */
64
+ export interface NocturnalRunDiagnostics {
65
+ /** Pre-flight check result */
66
+ preflight: PreflightCheckResult | null;
67
+ /** Selection result */
68
+ selection: NocturnalSelectionResult | null;
69
+ /** Idle check result */
70
+ idle: IdleCheckResult | null;
71
+ /** Whether Trinity chain was attempted */
72
+ trinityAttempted: boolean;
73
+ /** Trinity result (if trinityAttempted === true) */
74
+ trinityResult: TrinityResult | null;
75
+ /** Which chain mode was used */
76
+ chainModeUsed: 'trinity' | 'single-reflector' | null;
77
+ /** Arbiter validation result */
78
+ arbiterResult: ArbiterResult | null;
79
+ /** Executability validation result (if arbiter passed) */
80
+ executabilityResult: {
81
+ executable: boolean;
82
+ failures: string[];
83
+ } | null;
84
+ /** Whether artifact was persisted */
85
+ persisted: boolean;
86
+ /** Persistence path (if persisted) */
87
+ persistedPath?: string;
88
+ }
89
+ /**
90
+ * Configuration for the nocturnal service.
91
+ */
92
+ export interface NocturnalServiceOptions {
93
+ /**
94
+ * Whether to skip the reflector (for testing arbiter/executability in isolation).
95
+ * Default: false (reflector runs normally).
96
+ */
97
+ skipReflector?: boolean;
98
+ /**
99
+ * Override the reflector output (for testing).
100
+ * If provided, this JSON string is used instead of calling the stub reflector.
101
+ */
102
+ reflectorOutputOverride?: string;
103
+ /**
104
+ * Override idle check (for testing).
105
+ * If provided, this result is used instead of calling checkWorkspaceIdle.
106
+ */
107
+ idleCheckOverride?: IdleCheckResult;
108
+ /**
109
+ * Trinity chain configuration.
110
+ * Default: { useTrinity: true, maxCandidates: 3, useStubs: false }
111
+ */
112
+ trinityConfig?: Partial<TrinityConfig>;
113
+ /**
114
+ * Runtime adapter for real subagent execution.
115
+ * When provided, Trinity stages are invoked via the adapter's async methods.
116
+ * Ignored when trinityConfig.useStubs is true.
117
+ */
118
+ runtimeAdapter?: TrinityRuntimeAdapter;
119
+ /**
120
+ * Override the Trinity result (for testing).
121
+ * If provided, this result is used instead of running the Trinity chain.
122
+ */
123
+ trinityResultOverride?: TrinityResult;
124
+ /**
125
+ * Recent pain context from the evolution queue.
126
+ * When provided, the target selector uses it for ranking bias and diagnostics enrichment.
127
+ * This threads recent pain signals into sleep_reflection targeting without merging task kinds.
128
+ */
129
+ painContext?: import('../service/evolution-worker.js').RecentPainContext;
130
+ }
131
+ /**
132
+ * Execute a complete nocturnal reflection run.
133
+ *
134
+ * Pipeline:
135
+ * 1. Pre-flight check (idle + cooldown + quota)
136
+ * 2. Target selection (principle + violating session)
137
+ * 3. Trajectory snapshot extraction
138
+ * 4. Reflector (stub) → JSON artifact
139
+ * 5. Arbiter validation
140
+ * 6. Executability check
141
+ * 7. Artifact persistence
142
+ * 8. Cooldown recording
143
+ *
144
+ * @param workspaceDir - Workspace directory
145
+ * @param stateDir - State directory
146
+ * @param options - Service configuration options
147
+ * @returns NocturnalRunResult
148
+ */
149
+ export declare function executeNocturnalReflection(workspaceDir: string, stateDir: string, options?: NocturnalServiceOptions): NocturnalRunResult;
150
+ /**
151
+ * Async wrapper for executeNocturnalReflection.
152
+ * When runtimeAdapter is provided in options, uses runTrinityAsync for real subagent execution.
153
+ * Otherwise falls back to synchronous executeNocturnalReflection.
154
+ */
155
+ export declare function executeNocturnalReflectionAsync(workspaceDir: string, stateDir: string, options?: NocturnalServiceOptions): Promise<NocturnalRunResult>;
156
+ /**
157
+ * List all approved nocturnal artifacts for a workspace.
158
+ * Returns artifacts sorted by createdAt (newest first).
159
+ */
160
+ export declare function listApprovedNocturnalArtifacts(workspaceDir: string): Array<NocturnalArtifact & {
161
+ persistedAt: string;
162
+ boundedAction?: BoundedAction;
163
+ }>;