principles-disciple 1.10.0 → 1.12.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 (230) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +66 -0
  3. package/esbuild.config.js +1 -1
  4. package/openclaw.plugin.json +4 -4
  5. package/package.json +2 -3
  6. package/run-nocturnal.mjs +30 -0
  7. package/scripts/db-migrate.mjs +170 -0
  8. package/scripts/sync-plugin.mjs +94 -5
  9. package/src/commands/archive-impl.ts +136 -0
  10. package/src/commands/capabilities.ts +4 -2
  11. package/src/commands/context.ts +5 -1
  12. package/src/commands/disable-impl.ts +151 -0
  13. package/src/commands/evolution-status.ts +64 -19
  14. package/src/commands/export.ts +8 -6
  15. package/src/commands/focus.ts +8 -20
  16. package/src/commands/nocturnal-review.ts +5 -7
  17. package/src/commands/nocturnal-rollout.ts +1 -12
  18. package/src/commands/nocturnal-train.ts +17 -42
  19. package/src/commands/pain.ts +10 -5
  20. package/src/commands/principle-rollback.ts +4 -2
  21. package/src/commands/promote-impl.ts +274 -0
  22. package/src/commands/rollback-impl.ts +234 -0
  23. package/src/commands/rollback.ts +6 -3
  24. package/src/commands/samples.ts +2 -0
  25. package/src/commands/thinking-os.ts +3 -4
  26. package/src/commands/workflow-debug.ts +2 -1
  27. package/src/config/errors.ts +1 -0
  28. package/src/core/AGENTS.md +34 -0
  29. package/src/core/adaptive-thresholds.ts +4 -3
  30. package/src/core/code-implementation-storage.ts +241 -0
  31. package/src/core/config.ts +5 -2
  32. package/src/core/control-ui-db.ts +29 -10
  33. package/src/core/detection-funnel.ts +12 -7
  34. package/src/core/diagnostician-task-store.ts +156 -0
  35. package/src/core/dictionary.ts +4 -4
  36. package/src/core/empathy-keyword-matcher.ts +7 -3
  37. package/src/core/empathy-types.ts +13 -2
  38. package/src/core/event-log.ts +14 -6
  39. package/src/core/evolution-engine.ts +27 -31
  40. package/src/core/evolution-logger.ts +3 -2
  41. package/src/core/evolution-reducer.ts +83 -31
  42. package/src/core/evolution-types.ts +10 -0
  43. package/src/core/external-training-contract.ts +1 -0
  44. package/src/core/focus-history.ts +38 -24
  45. package/src/core/hygiene/tracker.ts +10 -6
  46. package/src/core/init.ts +5 -2
  47. package/src/core/migration.ts +3 -3
  48. package/src/core/model-deployment-registry.ts +6 -4
  49. package/src/core/model-training-registry.ts +5 -3
  50. package/src/core/nocturnal-arbiter.ts +13 -14
  51. package/src/core/nocturnal-artifact-lineage.ts +117 -0
  52. package/src/core/nocturnal-artificer.ts +257 -0
  53. package/src/core/nocturnal-candidate-scoring.ts +4 -2
  54. package/src/core/nocturnal-compliance.ts +67 -19
  55. package/src/core/nocturnal-dataset.ts +95 -2
  56. package/src/core/nocturnal-executability.ts +2 -3
  57. package/src/core/nocturnal-export.ts +6 -3
  58. package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
  59. package/src/core/nocturnal-trajectory-extractor.ts +10 -3
  60. package/src/core/nocturnal-trinity.ts +300 -57
  61. package/src/core/pain-context-extractor.ts +29 -15
  62. package/src/core/pain.ts +7 -5
  63. package/src/core/path-resolver.ts +16 -15
  64. package/src/core/paths.ts +2 -1
  65. package/src/core/pd-task-reconciler.ts +463 -0
  66. package/src/core/pd-task-service.ts +42 -0
  67. package/src/core/pd-task-store.ts +77 -0
  68. package/src/core/pd-task-types.ts +128 -0
  69. package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
  70. package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
  71. package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
  72. package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
  73. package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
  74. package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
  75. package/src/core/principle-training-state.ts +95 -370
  76. package/src/core/principle-tree-ledger.ts +713 -0
  77. package/src/core/profile.ts +3 -1
  78. package/src/core/promotion-gate.ts +14 -18
  79. package/src/core/replay-engine.ts +562 -0
  80. package/src/core/risk-calculator.ts +6 -4
  81. package/src/core/rule-host-helpers.ts +39 -0
  82. package/src/core/rule-host-types.ts +82 -0
  83. package/src/core/rule-host.ts +245 -0
  84. package/src/core/rule-implementation-runtime.ts +38 -0
  85. package/src/core/schema/db-types.ts +16 -0
  86. package/src/core/schema/index.ts +26 -0
  87. package/src/core/schema/migration-runner.ts +207 -0
  88. package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
  89. package/src/core/schema/migrations/002-init-central.ts +122 -0
  90. package/src/core/schema/migrations/003-init-workflow.ts +55 -0
  91. package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
  92. package/src/core/schema/migrations/index.ts +31 -0
  93. package/src/core/schema/schema-definitions.ts +650 -0
  94. package/src/core/session-tracker.ts +6 -4
  95. package/src/core/shadow-observation-registry.ts +6 -3
  96. package/src/core/system-logger.ts +2 -2
  97. package/src/core/thinking-models.ts +182 -46
  98. package/src/core/thinking-os-parser.ts +164 -0
  99. package/src/core/training-program.ts +7 -7
  100. package/src/core/trajectory.ts +42 -36
  101. package/src/core/workspace-context.ts +77 -11
  102. package/src/core/workspace-dir-validation.ts +152 -0
  103. package/src/hooks/AGENTS.md +31 -0
  104. package/src/hooks/bash-risk.ts +3 -1
  105. package/src/hooks/edit-verification.ts +9 -5
  106. package/src/hooks/gate-block-helper.ts +5 -1
  107. package/src/hooks/gate.ts +152 -5
  108. package/src/hooks/gfi-gate.ts +9 -2
  109. package/src/hooks/lifecycle-routing.ts +124 -0
  110. package/src/hooks/lifecycle.ts +12 -12
  111. package/src/hooks/llm.ts +17 -109
  112. package/src/hooks/message-sanitize.ts +5 -3
  113. package/src/hooks/pain.ts +19 -15
  114. package/src/hooks/progressive-trust-gate.ts +7 -1
  115. package/src/hooks/prompt.ts +169 -60
  116. package/src/hooks/subagent.ts +5 -4
  117. package/src/hooks/thinking-checkpoint.ts +2 -0
  118. package/src/hooks/trajectory-collector.ts +15 -12
  119. package/src/http/principles-console-route.ts +31 -68
  120. package/src/i18n/commands.ts +2 -2
  121. package/src/index.ts +126 -40
  122. package/src/service/central-database.ts +131 -43
  123. package/src/service/central-health-service.ts +47 -0
  124. package/src/service/central-overview-service.ts +135 -0
  125. package/src/service/central-sync-service.ts +87 -0
  126. package/src/service/control-ui-query-service.ts +46 -36
  127. package/src/service/event-log-auditor.ts +261 -0
  128. package/src/service/evolution-query-service.ts +23 -22
  129. package/src/service/evolution-worker.ts +565 -261
  130. package/src/service/health-query-service.ts +213 -36
  131. package/src/service/nocturnal-runtime.ts +8 -4
  132. package/src/service/nocturnal-service.ts +499 -59
  133. package/src/service/nocturnal-target-selector.ts +5 -7
  134. package/src/service/runtime-summary-service.ts +2 -1
  135. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
  136. package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
  137. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
  138. package/src/service/subagent-workflow/index.ts +2 -0
  139. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +155 -285
  140. package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
  141. package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
  142. package/src/service/subagent-workflow/types.ts +9 -4
  143. package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
  144. package/src/service/subagent-workflow/workflow-store.ts +71 -11
  145. package/src/service/trajectory-service.ts +2 -1
  146. package/src/tools/critique-prompt.ts +1 -1
  147. package/src/tools/deep-reflect.ts +175 -209
  148. package/src/tools/model-index.ts +2 -1
  149. package/src/types/event-types.ts +2 -2
  150. package/src/types/principle-tree-schema.ts +29 -23
  151. package/src/utils/file-lock.ts +5 -3
  152. package/src/utils/io.ts +5 -2
  153. package/src/utils/nlp.ts +5 -46
  154. package/src/utils/node-vm-polyfill.ts +11 -0
  155. package/src/utils/plugin-logger.ts +2 -0
  156. package/src/utils/retry.ts +572 -0
  157. package/src/utils/subagent-probe.ts +1 -1
  158. package/templates/langs/en/core/AGENTS.md +0 -13
  159. package/templates/langs/en/core/SOUL.md +1 -31
  160. package/templates/langs/en/core/TOOLS.md +0 -4
  161. package/templates/langs/en/principles/THINKING_OS.md +64 -0
  162. package/templates/langs/en/skills/admin/SKILL.md +0 -1
  163. package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
  164. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
  165. package/templates/langs/zh/core/AGENTS.md +0 -22
  166. package/templates/langs/zh/core/SOUL.md +1 -31
  167. package/templates/langs/zh/core/TOOLS.md +0 -4
  168. package/templates/langs/zh/principles/THINKING_OS.md +64 -0
  169. package/templates/langs/zh/skills/admin/SKILL.md +0 -1
  170. package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
  171. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
  172. package/tests/commands/evolution-status.test.ts +119 -0
  173. package/tests/commands/implementation-lifecycle.test.ts +362 -0
  174. package/tests/core/code-implementation-storage.test.ts +398 -0
  175. package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
  176. package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
  177. package/tests/core/nocturnal-artificer.test.ts +241 -0
  178. package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
  179. package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
  180. package/tests/core/pd-task-store.test.ts +126 -0
  181. package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
  182. package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
  183. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
  184. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
  185. package/tests/core/principle-training-state.test.ts +228 -1
  186. package/tests/core/principle-tree-ledger.test.ts +423 -0
  187. package/tests/core/regression-v1-9-1.test.ts +265 -0
  188. package/tests/core/replay-engine.test.ts +234 -0
  189. package/tests/core/rule-host-helpers.test.ts +120 -0
  190. package/tests/core/rule-host.test.ts +389 -0
  191. package/tests/core/rule-implementation-runtime.test.ts +64 -0
  192. package/tests/core/workspace-context.test.ts +53 -0
  193. package/tests/core/workspace-dir-validation.test.ts +272 -0
  194. package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
  195. package/tests/hooks/pain.test.ts +74 -10
  196. package/tests/hooks/prompt.test.ts +63 -1
  197. package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
  198. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
  199. package/tests/service/data-endpoints-regression.test.ts +834 -0
  200. package/tests/service/evolution-worker.test.ts +0 -123
  201. package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
  202. package/tests/utils/nlp.test.ts +1 -19
  203. package/tests/utils/retry.test.ts +327 -0
  204. package/ui/src/App.tsx +1 -1
  205. package/ui/src/api.ts +4 -0
  206. package/ui/src/charts.tsx +366 -0
  207. package/ui/src/components/WorkspaceConfig.tsx +107 -75
  208. package/ui/src/i18n/ui.ts +55 -22
  209. package/ui/src/pages/OverviewPage.tsx +441 -81
  210. package/ui/src/styles.css +43 -0
  211. package/ui/src/types.ts +17 -1
  212. package/src/agents/nocturnal-dreamer.md +0 -152
  213. package/src/agents/nocturnal-philosopher.md +0 -138
  214. package/src/agents/nocturnal-reflector.md +0 -126
  215. package/src/agents/nocturnal-scribe.md +0 -164
  216. package/templates/workspace/.principles/00-kernel.md +0 -51
  217. package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
  218. package/templates/workspace/.principles/PRINCIPLES.md +0 -20
  219. package/templates/workspace/.principles/PROFILE.json +0 -54
  220. package/templates/workspace/.principles/PROFILE.schema.json +0 -56
  221. package/templates/workspace/.principles/THINKING_OS.md +0 -64
  222. package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
  223. package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
  224. package/templates/workspace/.principles/models/_INDEX.md +0 -27
  225. package/templates/workspace/.principles/models/first_principles.md +0 -62
  226. package/templates/workspace/.principles/models/marketing_4p.md +0 -52
  227. package/templates/workspace/.principles/models/porter_five.md +0 -63
  228. package/templates/workspace/.principles/models/swot.md +0 -60
  229. package/templates/workspace/.principles/models/user_story_map.md +0 -63
  230. package/templates/workspace/.state/WORKBOARD.json +0 -4
@@ -35,11 +35,12 @@ import {
35
35
  type NocturnalRunResult,
36
36
  } from '../nocturnal-service.js';
37
37
  import { type TrinityStageFailure, type TrinityResult } from '../../core/nocturnal-trinity.js';
38
- import type { TrinityRuntimeAdapter, TrinityConfig, DreamerOutput, PhilosopherOutput, TrinityTelemetry } from '../../core/nocturnal-trinity.js';
38
+ import type { TrinityRuntimeAdapter } from '../../core/nocturnal-trinity.js';
39
39
  import type { NocturnalSessionSnapshot } from '../../core/nocturnal-trajectory-extractor.js';
40
- import { createHash } from 'crypto';
40
+ import type { RecentPainContext } from '../evolution-worker.js';
41
41
  import * as fs from 'fs';
42
42
  import * as path from 'path';
43
+ import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
43
44
 
44
45
  // ─────────────────────────────────────────────────────────────────────────────
45
46
  // NocturnalResult Type Alias
@@ -51,50 +52,6 @@ import * as path from 'path';
51
52
  */
52
53
  export type NocturnalResult = NocturnalRunResult;
53
54
 
54
- // ─────────────────────────────────────────────────────────────────────────────
55
- // Idempotency Key Computation (D-18)
56
- // ─────────────────────────────────────────────────────────────────────────────
57
-
58
- /**
59
- * Compute idempotency key for the Dreamer stage (D-18).
60
- * Key = SHA-256(workflowId + stage + inputDigest)
61
- * inputDigest = SHA-256(snapshot.sessionId + principleId + maxCandidates)
62
- * Uses simple concatenation (no delimiters) per D-18 spec.
63
- */
64
- function computeDreamerIdempotencyKey(
65
- workflowId: string,
66
- snapshot: import('../../core/nocturnal-trajectory-extractor.js').NocturnalSessionSnapshot,
67
- principleId: string,
68
- maxCandidates: number
69
- ): string {
70
- // Use delimiters to prevent collision (e.g., "sess10"+"2"+"3" vs "sess1"+"02"+"3")
71
- const inputDigest = createHash('sha256')
72
- .update(`${snapshot.sessionId}::${principleId}::${maxCandidates}`)
73
- .digest('hex');
74
- return createHash('sha256')
75
- .update(`${workflowId}::dreamer::${inputDigest}`)
76
- .digest('hex');
77
- }
78
-
79
- /**
80
- * Compute idempotency key for the Philosopher stage (D-18).
81
- * Key = SHA-256(workflowId + stage + inputDigest)
82
- * inputDigest = SHA-256(workflowId + dreamerOutputJson)
83
- * Uses simple concatenation (no delimiters) per D-18 spec.
84
- */
85
- function computePhilosopherIdempotencyKey(
86
- workflowId: string,
87
- dreamerOutput: import('../../core/nocturnal-trinity.js').DreamerOutput
88
- ): string {
89
- const dreamerOutputJson = JSON.stringify(dreamerOutput);
90
- const inputDigest = createHash('sha256')
91
- .update(workflowId + dreamerOutputJson)
92
- .digest('hex');
93
- return createHash('sha256')
94
- .update(workflowId + 'philosopher' + inputDigest)
95
- .digest('hex');
96
- }
97
-
98
55
  // ─────────────────────────────────────────────────────────────────────────────
99
56
  // NocturnalWorkflowOptions
100
57
  // ─────────────────────────────────────────────────────────────────────────────
@@ -125,6 +82,7 @@ export interface NocturnalWorkflowOptions {
125
82
  * - timeoutMs: 15 minutes (900000ms)
126
83
  * - ttlMs: 30 minutes (1800000ms)
127
84
  */
85
+ /* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: spec methods intentionally receive unused params per interface contract */
128
86
  export const nocturnalWorkflowSpec: SubagentWorkflowSpec<NocturnalResult> = {
129
87
  workflowType: 'nocturnal',
130
88
  transport: 'runtime_direct',
@@ -132,7 +90,10 @@ export const nocturnalWorkflowSpec: SubagentWorkflowSpec<NocturnalResult> = {
132
90
  ttlMs: 30 * 60 * 1000, // D-04: 30 minutes
133
91
  shouldDeleteSessionAfterFinalize: false, // D-09: no external session to delete
134
92
 
135
- buildPrompt(_taskInput: unknown, _metadata: WorkflowMetadata): string {
93
+ buildPrompt(
94
+ _taskInput: unknown,
95
+ _metadata: WorkflowMetadata
96
+ ): string {
136
97
  // NocturnalWorkflowManager does not use prompt injection.
137
98
  // Execution is driven by executeNocturnalReflectionAsync.
138
99
  return '';
@@ -141,7 +102,7 @@ export const nocturnalWorkflowSpec: SubagentWorkflowSpec<NocturnalResult> = {
141
102
  async parseResult(ctx: WorkflowResultContext): Promise<NocturnalResult | null> {
142
103
  // NocturnalWorkflowManager handles execution directly in startWorkflow.
143
104
  // This is not called via the standard subagent message path.
144
- return (ctx.metadata['nocturnalResult'] as NocturnalResult) ?? null;
105
+ return (ctx.metadata.nocturnalResult as NocturnalResult) ?? null;
145
106
  },
146
107
 
147
108
  async persistResult(_ctx: WorkflowPersistContext<NocturnalResult>): Promise<void> {
@@ -154,57 +115,7 @@ export const nocturnalWorkflowSpec: SubagentWorkflowSpec<NocturnalResult> = {
154
115
  return status === 'ok';
155
116
  },
156
117
  };
157
-
158
- // ─────────────────────────────────────────────────────────────────────────────
159
- // Stub Fallback Runtime Adapter (NOC-15)
160
- // ─────────────────────────────────────────────────────────────────────────────
161
-
162
- /**
163
- * Stub fallback runtime adapter for Trinity.
164
- * Wraps a real adapter and provides stub implementations for fallback when stages fail.
165
- * Per NOC-15: Fallback degrades to stub, NOT to EmpathyObserver/DeepReflect.
166
- */
167
- class StubFallbackRuntimeAdapter implements TrinityRuntimeAdapter {
168
- constructor(
169
- private snapshot: NocturnalSessionSnapshot,
170
- private principleId: string,
171
- private maxCandidates: number
172
- ) {}
173
-
174
- async invokeDreamer(
175
- snapshot: NocturnalSessionSnapshot,
176
- principleId: string,
177
- maxCandidates: number
178
- ): Promise<DreamerOutput> {
179
- const { invokeStubDreamer } = await import('../../core/nocturnal-trinity.js');
180
- return invokeStubDreamer(snapshot, principleId, maxCandidates);
181
- }
182
-
183
- async invokePhilosopher(
184
- dreamerOutput: DreamerOutput,
185
- principleId: string
186
- ): Promise<PhilosopherOutput> {
187
- const { invokeStubPhilosopher } = await import('../../core/nocturnal-trinity.js');
188
- return invokeStubPhilosopher(dreamerOutput, principleId);
189
- }
190
-
191
- async invokeScribe(
192
- dreamerOutput: DreamerOutput,
193
- philosopherOutput: PhilosopherOutput,
194
- snapshot: NocturnalSessionSnapshot,
195
- principleId: string,
196
- telemetry: TrinityTelemetry,
197
- config: TrinityConfig
198
- ): Promise<import('../../core/nocturnal-trinity.js').TrinityDraftArtifact | null> {
199
- // Use stub Scribe
200
- const { invokeStubScribe } = await import('../../core/nocturnal-trinity.js');
201
- return invokeStubScribe(dreamerOutput, philosopherOutput, snapshot, principleId, telemetry, config);
202
- }
203
-
204
- async close(): Promise<void> {
205
- // No-op for stubs
206
- }
207
- }
118
+ /* eslint-enable no-unused-vars */
208
119
 
209
120
  // ─────────────────────────────────────────────────────────────────────────────
210
121
  // NocturnalWorkflowManager
@@ -230,15 +141,15 @@ export class NocturnalWorkflowManager implements WorkflowManager {
230
141
  private readonly store: WorkflowStore;
231
142
 
232
143
  /** Tracks completion timestamps for idempotency */
233
- private completedWorkflows = new Map<string, number>();
144
+ private readonly completedWorkflows = new Map<string, number>();
234
145
  /** Maps workflowId → spec (needed for finalizeOnce) */
235
- private workflowSpecs = new Map<string, SubagentWorkflowSpec<unknown>>();
146
+ private readonly workflowSpecs = new Map<string, SubagentWorkflowSpec<unknown>>();
236
147
  /** Maps workflowId → result (needed for finalizeOnce) */
237
- private executionResults = new Map<string, NocturnalResult>();
148
+ private readonly executionResults = new Map<string, NocturnalResult>();
238
149
  /** Maps workflowId → TrinityStageFailure[] (stored before async launch, used in notifyWaitResult) */
239
- private pendingTrinityFailures = new Map<string, TrinityStageFailure[]>();
150
+ private readonly pendingTrinityFailures = new Map<string, TrinityStageFailure[]>();
240
151
  /** Maps workflowId → TrinityResult (needed by notifyWaitResult for artifact persistence) */
241
- private pendingTrinityResults = new Map<string, TrinityResult>();
152
+ private readonly pendingTrinityResults = new Map<string, TrinityResult>();
242
153
 
243
154
  constructor(opts: NocturnalWorkflowOptions) {
244
155
  this.workspaceDir = opts.workspaceDir;
@@ -261,7 +172,16 @@ export class NocturnalWorkflowManager implements WorkflowManager {
261
172
  metadata?: Record<string, unknown>;
262
173
  }
263
174
  ): Promise<WorkflowHandle> {
264
- const workflowId = this.generateWorkflowId();
175
+ // #179: Check subagent runtime availability before starting
176
+ // Other workflow managers (empathy, deep-reflect) have this check
177
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: TrinityRuntimeAdapter interface doesn't expose api.runtime.subagent, but OpenClawTrinityRuntimeAdapter has it
178
+ const subagent = (this.runtimeAdapter as any).api?.runtime?.subagent;
179
+ if (!isSubagentRuntimeAvailable(subagent)) {
180
+ this.logger.warn(`[PD:NocturnalWorkflow] Subagent runtime unavailable, skipping workflow`);
181
+ throw new Error(`NocturnalWorkflowManager: subagent runtime unavailable`);
182
+ }
183
+
184
+ const workflowId = NocturnalWorkflowManager.generateWorkflowId();
265
185
  const now = Date.now();
266
186
 
267
187
  const metadata: WorkflowMetadata = {
@@ -286,13 +206,16 @@ export class NocturnalWorkflowManager implements WorkflowManager {
286
206
  state: 'active',
287
207
  created_at: now,
288
208
  updated_at: now,
209
+ duration_ms: null,
289
210
  metadata_json: JSON.stringify(metadata),
290
211
  });
291
212
  this.store.recordEvent(workflowId, 'nocturnal_started', null, 'active', 'TrinityRuntimeAdapter invoked', { workflowType: 'nocturnal' });
292
213
 
293
214
  // Extract snapshot and principleId from taskInput.metadata (NOC-07: Trinity async path)
294
- const snapshot = options.metadata?.snapshot as import('../../core/nocturnal-trajectory-extractor.js').NocturnalSessionSnapshot | undefined;
215
+ const snapshot = options.metadata?.snapshot as NocturnalSessionSnapshot | undefined;
295
216
  const principleId = options.metadata?.principleId as string | undefined;
217
+ // Extract painContext for Selector ranking bias
218
+ const painContext = options.metadata?.painContext as RecentPainContext | undefined;
296
219
 
297
220
  // Validate required metadata (prevent runtime crashes from undefined snapshot)
298
221
  if (!snapshot?.sessionId) {
@@ -304,186 +227,68 @@ export class NocturnalWorkflowManager implements WorkflowManager {
304
227
  state: 'terminal_error' as const,
305
228
  };
306
229
  }
307
- if (!principleId) {
308
- this.logger.warn(`[PD:NocturnalWorkflow] Missing principleId in metadata for workflow=${workflowId}, terminating`);
309
- this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', 'Missing required metadata: principleId', { workflowId });
310
- return {
311
- workflowId,
312
- childSessionKey: `nocturnal:internal:${workflowId}`,
313
- state: 'terminal_error' as const,
314
- };
315
- }
316
-
317
- // Configure Trinity for async execution (NOC-06, NOC-07)
318
- const trinityConfig: TrinityConfig = {
319
- useTrinity: true, // NOC-07: Trinity chain, not single-reflector
320
- maxCandidates: 3,
321
- useStubs: false,
322
- runtimeAdapter: this.runtimeAdapter,
323
- stateDir: this.stateDir,
324
- };
325
-
326
- // Create mutable telemetry object (passed to invokeScribe and mutated)
327
- const telemetry: TrinityTelemetry = {
328
- chainMode: 'trinity',
329
- usedStubs: false,
330
- dreamerPassed: false,
331
- philosopherPassed: false,
332
- scribePassed: false,
333
- candidateCount: 0,
334
- selectedCandidateIndex: -1,
335
- stageFailures: [],
336
- };
337
-
338
- // NOC-07: Launch Trinity async via Promise.resolve().then() WITHOUT awaiting
339
- // This offloads the async chain so startWorkflow returns immediately with state='active'
230
+ // #205: Always use executeNocturnalReflectionAsync for unified pipeline
231
+ // This ensures the full post-processing flow is always executed:
232
+ // Selector Trinity Arbiter Executability Persist → Register
233
+ //
234
+ // When principleId is provided, we pass it as principleIdOverride to skip Selector.
235
+ // When principleId is missing, Selector will choose a principle from training store.
236
+ this.logger.info(`[PD:NocturnalWorkflow] Calling executeNocturnalReflectionAsync for full pipeline (principleId=${principleId ?? 'auto-select'})`);
237
+
238
+ // #213: Wrap fire-and-forget Promise with .catch() to prevent
239
+ // unhandled promise rejections if anything throws outside the try-catch
240
+ // (e.g., during parameter construction or environment errors).
340
241
  Promise.resolve().then(async () => {
341
242
  try {
342
- // NOC-15: Track if stub fallback was used
343
- let fallbackUsed = false;
344
-
345
- // Step 1: Crash recovery — check for existing stage outputs (NOC-13)
346
- // Query WorkflowStore for any existing outputs for this workflowId
347
- const existingOutputs = this.store.getStageOutputs(workflowId);
348
- const recoveredDreamerOutput = existingOutputs.find(o => o.stage === 'dreamer')?.output as DreamerOutput | undefined;
349
- const recoveredPhilosopherOutput = existingOutputs.find(o => o.stage === 'philosopher')?.output as PhilosopherOutput | undefined;
350
-
351
- let dreamerOutput: DreamerOutput;
352
- let philosopherOutput: PhilosopherOutput;
353
-
354
- // Step 2: Dreamer skip if recovered (NOC-12 idempotency)
355
- if (recoveredDreamerOutput) {
356
- this.logger.info(`[PD:NocturnalWorkflow] Recovered Dreamer output for workflow=${workflowId}, skipping Dreamer stage`);
357
- dreamerOutput = recoveredDreamerOutput;
358
- } else {
359
- // Compute idempotency key BEFORE calling invokeDreamer
360
- const dreamerIdemKey = computeDreamerIdempotencyKey(workflowId, snapshot, principleId, trinityConfig.maxCandidates);
361
-
362
- // Check idempotency — another concurrent run may have completed this stage
363
- const existingDreamerByKey = this.store.getStageOutputByKey(dreamerIdemKey);
364
- if (existingDreamerByKey) {
365
- this.logger.info(`[PD:NocturnalWorkflow] Found existing Dreamer output by idempotency key for workflow=${workflowId}`);
366
- dreamerOutput = existingDreamerByKey.output as DreamerOutput;
367
- } else {
368
- dreamerOutput = await this.runtimeAdapter.invokeDreamer(snapshot, principleId, trinityConfig.maxCandidates);
369
- // NOC-15: Fallback to stub Dreamer if real Dreamer failed
370
- if (!dreamerOutput.valid || dreamerOutput.candidates.length === 0) {
371
- this.logger.info(`[PD:NocturnalWorkflow] Dreamer failed (${dreamerOutput.reason}), falling back to stub`);
372
- fallbackUsed = true;
373
- const stubAdapter = new StubFallbackRuntimeAdapter(
374
- snapshot,
375
- principleId,
376
- trinityConfig.maxCandidates
377
- );
378
- dreamerOutput = await stubAdapter.invokeDreamer(snapshot, principleId, trinityConfig.maxCandidates);
379
- }
380
- // Persist Dreamer output (NOC-11)
381
- if (dreamerOutput.valid) {
382
- this.store.recordStageOutput(workflowId, 'dreamer', dreamerOutput, dreamerIdemKey);
383
- }
384
- }
385
- }
386
-
387
- // Step 3: Philosopher — skip if recovered (NOC-12 idempotency)
388
- if (recoveredPhilosopherOutput) {
389
- this.logger.info(`[PD:NocturnalWorkflow] Recovered Philosopher output for workflow=${workflowId}, skipping Philosopher stage`);
390
- philosopherOutput = recoveredPhilosopherOutput;
391
- } else {
392
- // Compute idempotency key BEFORE calling invokePhilosopher
393
- const philosopherIdemKey = computePhilosopherIdempotencyKey(workflowId, dreamerOutput);
394
-
395
- // Check idempotency
396
- const existingPhilosopherByKey = this.store.getStageOutputByKey(philosopherIdemKey);
397
- if (existingPhilosopherByKey) {
398
- this.logger.info(`[PD:NocturnalWorkflow] Found existing Philosopher output by idempotency key for workflow=${workflowId}`);
399
- philosopherOutput = existingPhilosopherByKey.output as PhilosopherOutput;
400
- } else {
401
- philosopherOutput = await this.runtimeAdapter.invokePhilosopher(dreamerOutput, principleId);
402
- // NOC-15: Fallback to stub Philosopher if real Philosopher failed
403
- if (!philosopherOutput.valid || philosopherOutput.judgments.length === 0) {
404
- this.logger.info(`[PD:NocturnalWorkflow] Philosopher failed (${philosopherOutput.reason}), falling back to stub`);
405
- fallbackUsed = true;
406
- const stubAdapter = new StubFallbackRuntimeAdapter(
407
- snapshot,
408
- principleId,
409
- trinityConfig.maxCandidates
410
- );
411
- philosopherOutput = await stubAdapter.invokePhilosopher(dreamerOutput, principleId);
412
- }
413
- // Persist Philosopher output (NOC-11)
414
- if (philosopherOutput.valid) {
415
- this.store.recordStageOutput(workflowId, 'philosopher', philosopherOutput, philosopherIdemKey);
416
- }
243
+ const result = await executeNocturnalReflectionAsync(
244
+ this.workspaceDir,
245
+ this.stateDir,
246
+ {
247
+ runtimeAdapter: this.runtimeAdapter,
248
+ trinityConfig: {
249
+ useTrinity: true,
250
+ maxCandidates: 3,
251
+ useStubs: false,
252
+ runtimeAdapter: this.runtimeAdapter,
253
+ stateDir: this.stateDir,
254
+ },
255
+ // Pass painContext for Selector ranking bias
256
+ painContext,
257
+ // Skip Selector if principleId and snapshot are provided
258
+ ...(principleId && snapshot ? {
259
+ principleIdOverride: principleId,
260
+ snapshotOverride: snapshot,
261
+ } : {}),
417
262
  }
418
- }
419
-
420
- // Step 4: Scribe — always runs (no intermediate Scribe output to persist)
421
- const draftArtifact = await this.runtimeAdapter.invokeScribe(
422
- dreamerOutput,
423
- philosopherOutput,
424
- snapshot,
425
- principleId,
426
- telemetry,
427
- trinityConfig
428
263
  );
429
264
 
430
- // Step 5: Build TrinityResult from stage outcomes
431
- const failures: TrinityStageFailure[] = [];
432
- if (!dreamerOutput.valid || dreamerOutput.candidates.length === 0) {
433
- failures.push({ stage: 'dreamer', reason: dreamerOutput.reason ?? 'no valid candidates' });
434
- }
435
- if (!philosopherOutput.valid || philosopherOutput.judgments.length === 0) {
436
- failures.push({ stage: 'philosopher', reason: philosopherOutput.reason ?? 'no judgments produced' });
437
- }
438
- if (!draftArtifact) {
439
- failures.push({ stage: 'scribe', reason: 'Failed to synthesize artifact' });
440
- }
441
-
442
- const trinityResult: TrinityResult = {
443
- success: failures.length === 0 && !!draftArtifact,
444
- artifact: draftArtifact ?? undefined,
445
- telemetry: {
446
- chainMode: 'trinity',
447
- usedStubs: fallbackUsed, // NOC-15: reflect actual stub usage
448
- dreamerPassed: dreamerOutput.valid && dreamerOutput.candidates.length > 0,
449
- philosopherPassed: philosopherOutput.valid && philosopherOutput.judgments.length > 0,
450
- scribePassed: !!draftArtifact,
451
- candidateCount: dreamerOutput.candidates.length,
452
- selectedCandidateIndex: draftArtifact?.selectedCandidateIndex ?? -1,
453
- stageFailures: failures.map(f => `${f.stage}: ${f.reason}`),
454
- },
455
- failures,
456
- fallbackOccurred: fallbackUsed, // NOC-15: mark when fallback was triggered
457
- };
458
-
459
- // Store for notifyWaitResult and proceed with existing flow
460
- this.pendingTrinityResults.set(workflowId, trinityResult);
461
- this.pendingTrinityFailures.set(workflowId, failures);
462
-
463
- // Record stage events (NOC-08, already implemented in Phase 07)
464
- this.recordStageEvents(workflowId, trinityResult);
465
-
466
- // Drive state transitions (NOC-10)
467
- if (trinityResult.success) {
468
- await this.notifyWaitResult(workflowId, 'ok');
265
+ if (result.success) {
266
+ this.store.recordEvent(workflowId, 'nocturnal_completed', null, 'completed', 'Full pipeline completed via executeNocturnalReflectionAsync', {
267
+ artifactId: result.diagnostics?.persistedPath,
268
+ });
269
+ this.completedWorkflows.set(workflowId, Date.now());
469
270
  } else {
470
- const errorMsg = failures.map(f => `${f.stage}: ${f.reason}`).join('; ');
471
- await this.notifyWaitResult(workflowId, 'error', errorMsg);
271
+ const reason = result.noTargetSelected ? 'no_target_selected' : 'validation_failed';
272
+ this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', reason, {
273
+ failures: result.validationFailures,
274
+ skipReason: result.skipReason,
275
+ });
472
276
  }
473
277
  } catch (err) {
474
- // Unexpected error - treat as Trinity failure
475
- const errorMsg = err instanceof Error ? err.message : String(err);
476
- this.pendingTrinityFailures.set(workflowId, [{ stage: 'dreamer' as const, reason: errorMsg }]);
477
- await this.notifyWaitResult(workflowId, 'error', errorMsg);
278
+ this.logger.error(`[PD:NocturnalWorkflow] executeNocturnalReflectionAsync threw: ${String(err)}`);
279
+ this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', String(err), { workflowId });
478
280
  }
281
+ }).catch((err) => {
282
+ // #213: Outer catch — catches errors from the fire-and-forget promise
283
+ // itself (e.g., if the async callback throws before entering try).
284
+ this.logger.error(`[PD:NocturnalWorkflow] Unhandled error in async pipeline for ${workflowId}: ${String(err)}`);
285
+ this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', String(err), { workflowId });
479
286
  });
480
287
 
481
- // Return immediately with state='active' (NOC-07)
482
288
  return {
483
289
  workflowId,
484
290
  childSessionKey: `nocturnal:internal:${workflowId}`,
485
- runId: undefined,
486
- state: 'active',
291
+ state: 'active' as const,
487
292
  };
488
293
  }
489
294
 
@@ -537,13 +342,13 @@ export class NocturnalWorkflowManager implements WorkflowManager {
537
342
  this.markCompleted(workflowId);
538
343
  }
539
344
 
540
- // ─────────────────────────────────────────────────────────────────────────
541
- // WorkflowManager Interface: notifyLifecycleEvent (NOC-01, D-10: no-op)
542
- // ─────────────────────────────────────────────────────────────────────────
543
-
345
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: interface contract requires this method signature, implementation is a no-op per D-10
544
346
  async notifyLifecycleEvent(
347
+ // eslint-disable-next-line no-unused-vars -- Reason: interface contract requires these params but NocturnalWorkflowManager is a no-op per D-10
545
348
  _workflowId: string,
349
+ // eslint-disable-next-line no-unused-vars -- Reason: interface contract requires these params but NocturnalWorkflowManager is a no-op per D-10
546
350
  _event: 'subagent_spawned' | 'subagent_ended',
351
+ // eslint-disable-next-line no-unused-vars -- Reason: interface contract requires these params but NocturnalWorkflowManager is a no-op per D-10
547
352
  _data?: Record<string, unknown>
548
353
  ): Promise<void> {
549
354
  // D-10: No-op. NocturnalWorkflowManager does not use the wait-on-run pattern.
@@ -574,11 +379,38 @@ export class NocturnalWorkflowManager implements WorkflowManager {
574
379
  this.markCompleted(workflowId);
575
380
  }
576
381
 
382
+ /**
383
+ * Force-expire a specific workflow by ID.
384
+ * Used by evolution-worker when a sleep_reflection task times out (#214).
385
+ */
386
+ expireWorkflow(workflowId: string, reason: string): void {
387
+ this.store.updateWorkflowState(workflowId, 'expired');
388
+ this.store.recordEvent(
389
+ workflowId,
390
+ 'nocturnal_expired',
391
+ 'active',
392
+ 'expired',
393
+ reason,
394
+ { reason }
395
+ );
396
+ }
397
+
577
398
  // ─────────────────────────────────────────────────────────────────────────
578
399
  // WorkflowManager Interface: sweepExpiredWorkflows (NOC-05)
579
400
  // ─────────────────────────────────────────────────────────────────────────
580
401
 
581
- async sweepExpiredWorkflows(maxAgeMs = 30 * 60 * 1000): Promise<number> {
402
+ async sweepExpiredWorkflows(
403
+ maxAgeMs = 30 * 60 * 1000,
404
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: subagentRuntime param is intentionally any for backward compatibility with callers
405
+ subagentRuntime?: any,
406
+ /* eslint-disable no-unused-vars -- Reason: type parameter names are documentation, not actual variables */
407
+ agentSession?: {
408
+ resolveStorePath: () => string;
409
+ loadSessionStore: (storePath: string, opts?: { skipCache?: boolean }) => Record<string, unknown>;
410
+ saveSessionStore: (storePath: string, store: Record<string, unknown>) => Promise<void>;
411
+ },
412
+ /* eslint-enable no-unused-vars */
413
+ ): Promise<number> {
582
414
  const expired = this.store.getExpiredWorkflows(maxAgeMs);
583
415
 
584
416
  this.logger.info(`[PD:NocturnalWorkflow] sweepExpiredWorkflows: found ${expired.length} expired`);
@@ -591,6 +423,44 @@ export class NocturnalWorkflowManager implements WorkflowManager {
591
423
  this.store.updateWorkflowState(workflow.workflow_id, 'expired');
592
424
  this.store.recordEvent(workflow.workflow_id, 'nocturnal_expired', workflow.state, 'expired', 'TTL expired', { workflowId: workflow.workflow_id });
593
425
 
426
+ // #180 + #188: Cleanup subagent session with gateway-safe fallback
427
+ if (workflow.child_session_key) {
428
+ try {
429
+ if (subagentRuntime) {
430
+ await subagentRuntime.deleteSession({
431
+ sessionKey: workflow.child_session_key,
432
+ deleteTranscript: true,
433
+ });
434
+ this.logger.info(`[PD:NocturnalWorkflow] Cleaned up subagent session: ${workflow.child_session_key}`);
435
+ } else if (agentSession) {
436
+ // Heartbeat-safe fallback: directly manipulate session store
437
+ const storePath = agentSession.resolveStorePath();
438
+ const store = agentSession.loadSessionStore(storePath, { skipCache: true });
439
+ const normalizedKey = workflow.child_session_key.toLowerCase();
440
+ if (store[normalizedKey]) {
441
+ delete store[normalizedKey];
442
+ await agentSession.saveSessionStore(storePath, store);
443
+ this.logger.info(`[PD:NocturnalWorkflow] Cleaned up subagent session via agentSession fallback: ${workflow.child_session_key}`);
444
+ }
445
+ }
446
+ } catch (sessionErr) {
447
+ const errMsg = String(sessionErr);
448
+ // #188: If gateway-scoped method fails, try agentSession fallback
449
+ if (errMsg.includes('gateway request') && agentSession) {
450
+ const storePath = agentSession.resolveStorePath();
451
+ const store = agentSession.loadSessionStore(storePath, { skipCache: true });
452
+ const normalizedKey = workflow.child_session_key.toLowerCase();
453
+ if (store[normalizedKey]) {
454
+ delete store[normalizedKey];
455
+ await agentSession.saveSessionStore(storePath, store);
456
+ this.logger.info(`[PD:NocturnalWorkflow] Cleaned up subagent session via agentSession fallback after gateway error: ${workflow.child_session_key}`);
457
+ }
458
+ } else {
459
+ this.logger.warn(`[PD:NocturnalWorkflow] Failed to cleanup session ${workflow.child_session_key}: ${errMsg}`);
460
+ }
461
+ }
462
+ }
463
+
594
464
  // D-06: Clean partial artifact files by workflowId prefix
595
465
  const samplesDir = resolveNocturnalDir(this.workspaceDir, 'SAMPLES');
596
466
  if (fs.existsSync(samplesDir)) {
@@ -644,7 +514,7 @@ export class NocturnalWorkflowManager implements WorkflowManager {
644
514
  }));
645
515
 
646
516
  // NOC-16: Compute Trinity stage states from events
647
- const trinityStageStates = this.computeTrinityStageStates(allEvents);
517
+ const trinityStageStates = NocturnalWorkflowManager.computeTrinityStageStates(allEvents);
648
518
 
649
519
  return {
650
520
  workflowId: workflow.workflow_id,
@@ -674,7 +544,7 @@ export class NocturnalWorkflowManager implements WorkflowManager {
674
544
  // Helper Methods
675
545
  // ─────────────────────────────────────────────────────────────────────────
676
546
 
677
- private generateWorkflowId(): string {
547
+ private static generateWorkflowId(): string {
678
548
  return `wf_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
679
549
  }
680
550
 
@@ -700,19 +570,19 @@ export class NocturnalWorkflowManager implements WorkflowManager {
700
570
  * Compute Trinity stage states from workflow events (NOC-16).
701
571
  * Derives current/completed/failed state for each Trinity stage.
702
572
  */
703
- private computeTrinityStageStates(events: WorkflowEventRow[]): Array<{
573
+ private static computeTrinityStageStates(events: WorkflowEventRow[]): {
704
574
  stage: 'dreamer' | 'philosopher' | 'scribe';
705
575
  status: 'pending' | 'running' | 'completed' | 'failed';
706
576
  reason?: string;
707
577
  completedAt?: number;
708
- }> {
709
- const stages: Array<'dreamer' | 'philosopher' | 'scribe'> = ['dreamer', 'philosopher', 'scribe'];
710
- const result: Array<{
578
+ }[] {
579
+ const stages: ('dreamer' | 'philosopher' | 'scribe')[] = ['dreamer', 'philosopher', 'scribe'];
580
+ const result: {
711
581
  stage: 'dreamer' | 'philosopher' | 'scribe';
712
582
  status: 'pending' | 'running' | 'completed' | 'failed';
713
583
  reason?: string;
714
584
  completedAt?: number;
715
- }> = [];
585
+ }[] = [];
716
586
 
717
587
  for (const stage of stages) {
718
588
  const startEvent = events.find(e => e.event_type === `trinity_${stage}_start`);
@@ -725,7 +595,7 @@ export class NocturnalWorkflowManager implements WorkflowManager {
725
595
  } else if (failedEvent) {
726
596
  // Stage ran and failed
727
597
  const payload = JSON.parse(failedEvent.payload_json || '{}') as Record<string, unknown>;
728
- const failures = payload.failures as Array<{ reason?: string }> | undefined;
598
+ const failures = payload.failures as { reason?: string }[] | undefined;
729
599
  result.push({
730
600
  stage,
731
601
  status: 'failed',