principles-disciple 1.11.0 → 1.13.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 (233) 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 +250 -6
  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 +21 -47
  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 +110 -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 +319 -61
  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 +733 -0
  77. package/src/core/principle-tree-migration.ts +195 -0
  78. package/src/core/profile.ts +3 -1
  79. package/src/core/promotion-gate.ts +14 -18
  80. package/src/core/replay-engine.ts +562 -0
  81. package/src/core/risk-calculator.ts +6 -4
  82. package/src/core/rule-host-helpers.ts +39 -0
  83. package/src/core/rule-host-types.ts +82 -0
  84. package/src/core/rule-host.ts +245 -0
  85. package/src/core/rule-implementation-runtime.ts +38 -0
  86. package/src/core/schema/db-types.ts +16 -0
  87. package/src/core/schema/index.ts +26 -0
  88. package/src/core/schema/migration-runner.ts +207 -0
  89. package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
  90. package/src/core/schema/migrations/002-init-central.ts +122 -0
  91. package/src/core/schema/migrations/003-init-workflow.ts +55 -0
  92. package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
  93. package/src/core/schema/migrations/index.ts +31 -0
  94. package/src/core/schema/schema-definitions.ts +650 -0
  95. package/src/core/session-tracker.ts +6 -4
  96. package/src/core/shadow-observation-registry.ts +6 -3
  97. package/src/core/system-logger.ts +2 -2
  98. package/src/core/thinking-models.ts +182 -46
  99. package/src/core/thinking-os-parser.ts +156 -0
  100. package/src/core/training-program.ts +7 -7
  101. package/src/core/trajectory.ts +42 -36
  102. package/src/core/workspace-context.ts +77 -11
  103. package/src/core/workspace-dir-validation.ts +152 -0
  104. package/src/hooks/AGENTS.md +31 -0
  105. package/src/hooks/bash-risk.ts +3 -1
  106. package/src/hooks/edit-verification.ts +9 -5
  107. package/src/hooks/gate-block-helper.ts +5 -1
  108. package/src/hooks/gate.ts +152 -5
  109. package/src/hooks/gfi-gate.ts +9 -2
  110. package/src/hooks/lifecycle-routing.ts +124 -0
  111. package/src/hooks/lifecycle.ts +12 -12
  112. package/src/hooks/llm.ts +17 -109
  113. package/src/hooks/message-sanitize.ts +5 -3
  114. package/src/hooks/pain.ts +19 -15
  115. package/src/hooks/progressive-trust-gate.ts +7 -1
  116. package/src/hooks/prompt.ts +169 -60
  117. package/src/hooks/subagent.ts +5 -4
  118. package/src/hooks/thinking-checkpoint.ts +2 -0
  119. package/src/hooks/trajectory-collector.ts +15 -12
  120. package/src/http/principles-console-route.ts +31 -68
  121. package/src/i18n/commands.ts +2 -2
  122. package/src/index.ts +130 -40
  123. package/src/service/central-database.ts +131 -43
  124. package/src/service/central-health-service.ts +47 -0
  125. package/src/service/central-overview-service.ts +135 -0
  126. package/src/service/central-sync-service.ts +87 -0
  127. package/src/service/control-ui-query-service.ts +46 -36
  128. package/src/service/event-log-auditor.ts +261 -0
  129. package/src/service/evolution-query-service.ts +23 -22
  130. package/src/service/evolution-worker.ts +565 -261
  131. package/src/service/health-query-service.ts +213 -36
  132. package/src/service/nocturnal-runtime.ts +8 -4
  133. package/src/service/nocturnal-service.ts +503 -59
  134. package/src/service/nocturnal-target-selector.ts +5 -7
  135. package/src/service/runtime-summary-service.ts +2 -1
  136. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
  137. package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
  138. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
  139. package/src/service/subagent-workflow/index.ts +2 -0
  140. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
  141. package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
  142. package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
  143. package/src/service/subagent-workflow/types.ts +9 -4
  144. package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
  145. package/src/service/subagent-workflow/workflow-store.ts +71 -11
  146. package/src/service/trajectory-service.ts +2 -1
  147. package/src/tools/critique-prompt.ts +1 -1
  148. package/src/tools/deep-reflect.ts +175 -209
  149. package/src/tools/model-index.ts +2 -1
  150. package/src/types/event-types.ts +2 -2
  151. package/src/types/principle-tree-schema.ts +29 -23
  152. package/src/utils/file-lock.ts +5 -3
  153. package/src/utils/io.ts +5 -2
  154. package/src/utils/nlp.ts +5 -46
  155. package/src/utils/node-vm-polyfill.ts +11 -0
  156. package/src/utils/plugin-logger.ts +2 -0
  157. package/src/utils/retry.ts +572 -0
  158. package/src/utils/subagent-probe.ts +1 -1
  159. package/templates/langs/en/core/AGENTS.md +0 -13
  160. package/templates/langs/en/core/SOUL.md +1 -31
  161. package/templates/langs/en/core/TOOLS.md +0 -4
  162. package/templates/langs/en/principles/THINKING_OS.md +77 -0
  163. package/templates/langs/en/skills/admin/SKILL.md +0 -1
  164. package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
  165. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
  166. package/templates/langs/zh/core/AGENTS.md +0 -22
  167. package/templates/langs/zh/core/SOUL.md +1 -31
  168. package/templates/langs/zh/core/TOOLS.md +0 -4
  169. package/templates/langs/zh/principles/THINKING_OS.md +77 -0
  170. package/templates/langs/zh/skills/admin/SKILL.md +0 -1
  171. package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
  172. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
  173. package/tests/commands/evolution-status.test.ts +119 -0
  174. package/tests/commands/implementation-lifecycle.test.ts +362 -0
  175. package/tests/core/code-implementation-storage.test.ts +398 -0
  176. package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
  177. package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
  178. package/tests/core/nocturnal-artificer.test.ts +241 -0
  179. package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
  180. package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
  181. package/tests/core/pd-task-store.test.ts +126 -0
  182. package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
  183. package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
  184. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
  185. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
  186. package/tests/core/principle-training-state.test.ts +228 -1
  187. package/tests/core/principle-tree-ledger.test.ts +423 -0
  188. package/tests/core/regression-v1-9-1.test.ts +265 -0
  189. package/tests/core/replay-engine.test.ts +234 -0
  190. package/tests/core/rule-host-helpers.test.ts +120 -0
  191. package/tests/core/rule-host.test.ts +389 -0
  192. package/tests/core/rule-implementation-runtime.test.ts +64 -0
  193. package/tests/core/workspace-context.test.ts +53 -0
  194. package/tests/core/workspace-dir-validation.test.ts +272 -0
  195. package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
  196. package/tests/hooks/pain.test.ts +74 -10
  197. package/tests/hooks/prompt.test.ts +63 -1
  198. package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
  199. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
  200. package/tests/service/data-endpoints-regression.test.ts +834 -0
  201. package/tests/service/evolution-worker.test.ts +0 -123
  202. package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
  203. package/tests/utils/nlp.test.ts +1 -19
  204. package/tests/utils/retry.test.ts +327 -0
  205. package/ui/src/App.tsx +1 -1
  206. package/ui/src/api.ts +4 -0
  207. package/ui/src/charts.tsx +366 -0
  208. package/ui/src/components/WorkspaceConfig.tsx +107 -75
  209. package/ui/src/i18n/ui.ts +89 -31
  210. package/ui/src/pages/EvolutionPage.tsx +1 -1
  211. package/ui/src/pages/OverviewPage.tsx +441 -81
  212. package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
  213. package/ui/src/styles.css +43 -0
  214. package/ui/src/types.ts +17 -1
  215. package/src/agents/nocturnal-dreamer.md +0 -152
  216. package/src/agents/nocturnal-philosopher.md +0 -138
  217. package/src/agents/nocturnal-reflector.md +0 -126
  218. package/src/agents/nocturnal-scribe.md +0 -164
  219. package/templates/workspace/.principles/00-kernel.md +0 -51
  220. package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
  221. package/templates/workspace/.principles/PRINCIPLES.md +0 -20
  222. package/templates/workspace/.principles/PROFILE.json +0 -54
  223. package/templates/workspace/.principles/PROFILE.schema.json +0 -56
  224. package/templates/workspace/.principles/THINKING_OS.md +0 -64
  225. package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
  226. package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
  227. package/templates/workspace/.principles/models/_INDEX.md +0 -27
  228. package/templates/workspace/.principles/models/first_principles.md +0 -62
  229. package/templates/workspace/.principles/models/marketing_4p.md +0 -52
  230. package/templates/workspace/.principles/models/porter_five.md +0 -63
  231. package/templates/workspace/.principles/models/swot.md +0 -60
  232. package/templates/workspace/.principles/models/user_story_map.md +0 -63
  233. 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,22 @@ 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
+ const apiAvailable = !!(this.runtimeAdapter as any).api;
180
+ const runtimeAvailable = !!(this.runtimeAdapter as any).api?.runtime;
181
+ const subagentAvailable = isSubagentRuntimeAvailable(subagent);
182
+
183
+ this.logger.info(`[PD:NocturnalWorkflow] Subagent availability check: api=${apiAvailable}, runtime=${runtimeAvailable}, subagent=${subagentAvailable}`);
184
+
185
+ if (!subagentAvailable) {
186
+ this.logger.warn(`[PD:NocturnalWorkflow] Subagent runtime unavailable (api=${apiAvailable}, runtime=${runtimeAvailable}), skipping workflow`);
187
+ throw new Error(`NocturnalWorkflowManager: subagent runtime unavailable`);
188
+ }
189
+
190
+ const workflowId = NocturnalWorkflowManager.generateWorkflowId();
265
191
  const now = Date.now();
266
192
 
267
193
  const metadata: WorkflowMetadata = {
@@ -286,13 +212,16 @@ export class NocturnalWorkflowManager implements WorkflowManager {
286
212
  state: 'active',
287
213
  created_at: now,
288
214
  updated_at: now,
215
+ duration_ms: null,
289
216
  metadata_json: JSON.stringify(metadata),
290
217
  });
291
218
  this.store.recordEvent(workflowId, 'nocturnal_started', null, 'active', 'TrinityRuntimeAdapter invoked', { workflowType: 'nocturnal' });
292
219
 
293
220
  // 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;
221
+ const snapshot = options.metadata?.snapshot as NocturnalSessionSnapshot | undefined;
295
222
  const principleId = options.metadata?.principleId as string | undefined;
223
+ // Extract painContext for Selector ranking bias
224
+ const painContext = options.metadata?.painContext as RecentPainContext | undefined;
296
225
 
297
226
  // Validate required metadata (prevent runtime crashes from undefined snapshot)
298
227
  if (!snapshot?.sessionId) {
@@ -304,186 +233,77 @@ export class NocturnalWorkflowManager implements WorkflowManager {
304
233
  state: 'terminal_error' as const,
305
234
  };
306
235
  }
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'
236
+ // #205: Always use executeNocturnalReflectionAsync for unified pipeline
237
+ // This ensures the full post-processing flow is always executed:
238
+ // Selector Trinity Arbiter Executability Persist → Register
239
+ //
240
+ // When principleId is provided, we pass it as principleIdOverride to skip Selector.
241
+ // When principleId is missing, Selector will choose a principle from training store.
242
+ this.logger.info(`[PD:NocturnalWorkflow] Calling executeNocturnalReflectionAsync for full pipeline (principleId=${principleId ?? 'auto-select'})`);
243
+ const pipelineStart = Date.now();
244
+
245
+ // #213: Wrap fire-and-forget Promise with .catch() to prevent
246
+ // unhandled promise rejections if anything throws outside the try-catch
247
+ // (e.g., during parameter construction or environment errors).
340
248
  Promise.resolve().then(async () => {
341
249
  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
- }
250
+ this.logger.info(`[PD:NocturnalWorkflow] [${workflowId}] Pipeline step 1/4: Starting executeNocturnalReflectionAsync`);
251
+
252
+ const result = await executeNocturnalReflectionAsync(
253
+ this.workspaceDir,
254
+ this.stateDir,
255
+ {
256
+ runtimeAdapter: this.runtimeAdapter,
257
+ trinityConfig: {
258
+ useTrinity: true,
259
+ maxCandidates: 3,
260
+ useStubs: false,
261
+ runtimeAdapter: this.runtimeAdapter,
262
+ stateDir: this.stateDir,
263
+ },
264
+ // Pass painContext for Selector ranking bias
265
+ painContext,
266
+ // Skip Selector if principleId and snapshot are provided
267
+ ...(principleId && snapshot ? {
268
+ principleIdOverride: principleId,
269
+ snapshotOverride: snapshot,
270
+ } : {}),
384
271
  }
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
- }
417
- }
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
272
  );
429
273
 
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
- }
274
+ const pipelineDuration = Date.now() - pipelineStart;
275
+ this.logger.info(`[PD:NocturnalWorkflow] [${workflowId}] Pipeline completed in ${pipelineDuration}ms, success=${result.success}`);
441
276
 
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');
277
+ if (result.success) {
278
+ this.logger.info(`[PD:NocturnalWorkflow] [${workflowId}] Pipeline step 4/4: Completed successfully, artifactId=${result.diagnostics?.persistedPath}`);
279
+ this.store.recordEvent(workflowId, 'nocturnal_completed', null, 'completed', 'Full pipeline completed via executeNocturnalReflectionAsync', {
280
+ artifactId: result.diagnostics?.persistedPath,
281
+ });
282
+ this.completedWorkflows.set(workflowId, Date.now());
469
283
  } else {
470
- const errorMsg = failures.map(f => `${f.stage}: ${f.reason}`).join('; ');
471
- await this.notifyWaitResult(workflowId, 'error', errorMsg);
284
+ const reason = result.noTargetSelected ? 'no_target_selected' : 'validation_failed';
285
+ this.logger.warn(`[PD:NocturnalWorkflow] [${workflowId}] Pipeline failed: reason=${reason}, noTargetSelected=${result.noTargetSelected}, skipReason=${result.skipReason ?? 'none'}, validationFailures=${result.validationFailures?.length ?? 0}`);
286
+ this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', reason, {
287
+ failures: result.validationFailures,
288
+ skipReason: result.skipReason,
289
+ });
472
290
  }
473
291
  } 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);
292
+ const errDuration = Date.now() - pipelineStart;
293
+ this.logger.error(`[PD:NocturnalWorkflow] [${workflowId}] executeNocturnalReflectionAsync threw after ${errDuration}ms: ${String(err)}`);
294
+ this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', String(err), { workflowId });
478
295
  }
296
+ }).catch((err) => {
297
+ // #213: Outer catch — catches errors from the fire-and-forget promise
298
+ // itself (e.g., if the async callback throws before entering try).
299
+ this.logger.error(`[PD:NocturnalWorkflow] Unhandled error in async pipeline for ${workflowId}: ${String(err)}`);
300
+ this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', String(err), { workflowId });
479
301
  });
480
302
 
481
- // Return immediately with state='active' (NOC-07)
482
303
  return {
483
304
  workflowId,
484
305
  childSessionKey: `nocturnal:internal:${workflowId}`,
485
- runId: undefined,
486
- state: 'active',
306
+ state: 'active' as const,
487
307
  };
488
308
  }
489
309
 
@@ -537,13 +357,13 @@ export class NocturnalWorkflowManager implements WorkflowManager {
537
357
  this.markCompleted(workflowId);
538
358
  }
539
359
 
540
- // ─────────────────────────────────────────────────────────────────────────
541
- // WorkflowManager Interface: notifyLifecycleEvent (NOC-01, D-10: no-op)
542
- // ─────────────────────────────────────────────────────────────────────────
543
-
360
+ // 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
361
  async notifyLifecycleEvent(
362
+ // eslint-disable-next-line no-unused-vars -- Reason: interface contract requires these params but NocturnalWorkflowManager is a no-op per D-10
545
363
  _workflowId: string,
364
+ // eslint-disable-next-line no-unused-vars -- Reason: interface contract requires these params but NocturnalWorkflowManager is a no-op per D-10
546
365
  _event: 'subagent_spawned' | 'subagent_ended',
366
+ // eslint-disable-next-line no-unused-vars -- Reason: interface contract requires these params but NocturnalWorkflowManager is a no-op per D-10
547
367
  _data?: Record<string, unknown>
548
368
  ): Promise<void> {
549
369
  // D-10: No-op. NocturnalWorkflowManager does not use the wait-on-run pattern.
@@ -574,11 +394,38 @@ export class NocturnalWorkflowManager implements WorkflowManager {
574
394
  this.markCompleted(workflowId);
575
395
  }
576
396
 
397
+ /**
398
+ * Force-expire a specific workflow by ID.
399
+ * Used by evolution-worker when a sleep_reflection task times out (#214).
400
+ */
401
+ expireWorkflow(workflowId: string, reason: string): void {
402
+ this.store.updateWorkflowState(workflowId, 'expired');
403
+ this.store.recordEvent(
404
+ workflowId,
405
+ 'nocturnal_expired',
406
+ 'active',
407
+ 'expired',
408
+ reason,
409
+ { reason }
410
+ );
411
+ }
412
+
577
413
  // ─────────────────────────────────────────────────────────────────────────
578
414
  // WorkflowManager Interface: sweepExpiredWorkflows (NOC-05)
579
415
  // ─────────────────────────────────────────────────────────────────────────
580
416
 
581
- async sweepExpiredWorkflows(maxAgeMs = 30 * 60 * 1000): Promise<number> {
417
+ async sweepExpiredWorkflows(
418
+ maxAgeMs = 30 * 60 * 1000,
419
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: subagentRuntime param is intentionally any for backward compatibility with callers
420
+ subagentRuntime?: any,
421
+ /* eslint-disable no-unused-vars -- Reason: type parameter names are documentation, not actual variables */
422
+ agentSession?: {
423
+ resolveStorePath: () => string;
424
+ loadSessionStore: (storePath: string, opts?: { skipCache?: boolean }) => Record<string, unknown>;
425
+ saveSessionStore: (storePath: string, store: Record<string, unknown>) => Promise<void>;
426
+ },
427
+ /* eslint-enable no-unused-vars */
428
+ ): Promise<number> {
582
429
  const expired = this.store.getExpiredWorkflows(maxAgeMs);
583
430
 
584
431
  this.logger.info(`[PD:NocturnalWorkflow] sweepExpiredWorkflows: found ${expired.length} expired`);
@@ -591,6 +438,44 @@ export class NocturnalWorkflowManager implements WorkflowManager {
591
438
  this.store.updateWorkflowState(workflow.workflow_id, 'expired');
592
439
  this.store.recordEvent(workflow.workflow_id, 'nocturnal_expired', workflow.state, 'expired', 'TTL expired', { workflowId: workflow.workflow_id });
593
440
 
441
+ // #180 + #188: Cleanup subagent session with gateway-safe fallback
442
+ if (workflow.child_session_key) {
443
+ try {
444
+ if (subagentRuntime) {
445
+ await subagentRuntime.deleteSession({
446
+ sessionKey: workflow.child_session_key,
447
+ deleteTranscript: true,
448
+ });
449
+ this.logger.info(`[PD:NocturnalWorkflow] Cleaned up subagent session: ${workflow.child_session_key}`);
450
+ } else if (agentSession) {
451
+ // Heartbeat-safe fallback: directly manipulate session store
452
+ const storePath = agentSession.resolveStorePath();
453
+ const store = agentSession.loadSessionStore(storePath, { skipCache: true });
454
+ const normalizedKey = workflow.child_session_key.toLowerCase();
455
+ if (store[normalizedKey]) {
456
+ delete store[normalizedKey];
457
+ await agentSession.saveSessionStore(storePath, store);
458
+ this.logger.info(`[PD:NocturnalWorkflow] Cleaned up subagent session via agentSession fallback: ${workflow.child_session_key}`);
459
+ }
460
+ }
461
+ } catch (sessionErr) {
462
+ const errMsg = String(sessionErr);
463
+ // #188: If gateway-scoped method fails, try agentSession fallback
464
+ if (errMsg.includes('gateway request') && agentSession) {
465
+ const storePath = agentSession.resolveStorePath();
466
+ const store = agentSession.loadSessionStore(storePath, { skipCache: true });
467
+ const normalizedKey = workflow.child_session_key.toLowerCase();
468
+ if (store[normalizedKey]) {
469
+ delete store[normalizedKey];
470
+ await agentSession.saveSessionStore(storePath, store);
471
+ this.logger.info(`[PD:NocturnalWorkflow] Cleaned up subagent session via agentSession fallback after gateway error: ${workflow.child_session_key}`);
472
+ }
473
+ } else {
474
+ this.logger.warn(`[PD:NocturnalWorkflow] Failed to cleanup session ${workflow.child_session_key}: ${errMsg}`);
475
+ }
476
+ }
477
+ }
478
+
594
479
  // D-06: Clean partial artifact files by workflowId prefix
595
480
  const samplesDir = resolveNocturnalDir(this.workspaceDir, 'SAMPLES');
596
481
  if (fs.existsSync(samplesDir)) {
@@ -644,7 +529,7 @@ export class NocturnalWorkflowManager implements WorkflowManager {
644
529
  }));
645
530
 
646
531
  // NOC-16: Compute Trinity stage states from events
647
- const trinityStageStates = this.computeTrinityStageStates(allEvents);
532
+ const trinityStageStates = NocturnalWorkflowManager.computeTrinityStageStates(allEvents);
648
533
 
649
534
  return {
650
535
  workflowId: workflow.workflow_id,
@@ -674,7 +559,7 @@ export class NocturnalWorkflowManager implements WorkflowManager {
674
559
  // Helper Methods
675
560
  // ─────────────────────────────────────────────────────────────────────────
676
561
 
677
- private generateWorkflowId(): string {
562
+ private static generateWorkflowId(): string {
678
563
  return `wf_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
679
564
  }
680
565
 
@@ -700,19 +585,19 @@ export class NocturnalWorkflowManager implements WorkflowManager {
700
585
  * Compute Trinity stage states from workflow events (NOC-16).
701
586
  * Derives current/completed/failed state for each Trinity stage.
702
587
  */
703
- private computeTrinityStageStates(events: WorkflowEventRow[]): Array<{
588
+ private static computeTrinityStageStates(events: WorkflowEventRow[]): {
704
589
  stage: 'dreamer' | 'philosopher' | 'scribe';
705
590
  status: 'pending' | 'running' | 'completed' | 'failed';
706
591
  reason?: string;
707
592
  completedAt?: number;
708
- }> {
709
- const stages: Array<'dreamer' | 'philosopher' | 'scribe'> = ['dreamer', 'philosopher', 'scribe'];
710
- const result: Array<{
593
+ }[] {
594
+ const stages: ('dreamer' | 'philosopher' | 'scribe')[] = ['dreamer', 'philosopher', 'scribe'];
595
+ const result: {
711
596
  stage: 'dreamer' | 'philosopher' | 'scribe';
712
597
  status: 'pending' | 'running' | 'completed' | 'failed';
713
598
  reason?: string;
714
599
  completedAt?: number;
715
- }> = [];
600
+ }[] = [];
716
601
 
717
602
  for (const stage of stages) {
718
603
  const startEvent = events.find(e => e.event_type === `trinity_${stage}_start`);
@@ -725,7 +610,7 @@ export class NocturnalWorkflowManager implements WorkflowManager {
725
610
  } else if (failedEvent) {
726
611
  // Stage ran and failed
727
612
  const payload = JSON.parse(failedEvent.payload_json || '{}') as Record<string, unknown>;
728
- const failures = payload.failures as Array<{ reason?: string }> | undefined;
613
+ const failures = payload.failures as { reason?: string }[] | undefined;
729
614
  result.push({
730
615
  stage,
731
616
  status: 'failed',