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
@@ -1,20 +1,19 @@
1
1
  import type { PluginLogger } from '../../openclaw-sdk.js';
2
2
  import type {
3
- WorkflowManager,
4
- WorkflowHandle,
5
3
  SubagentWorkflowSpec,
6
4
  WorkflowMetadata,
7
- WorkflowDebugSummary,
8
5
  EmpathyObserverPayload,
9
6
  EmpathyResult,
10
7
  WorkflowResultContext,
11
8
  WorkflowPersistContext,
9
+ WorkflowHandle,
12
10
  } from './types.js';
13
- import { RuntimeDirectDriver, type RunParams } from './runtime-direct-driver.js';
14
- import { WorkflowStore } from './workflow-store.js';
15
- import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
11
+ import type { RuntimeDirectDriver } from './runtime-direct-driver.js';
16
12
  import { WorkspaceContext } from '../../core/workspace-context.js';
17
13
  import { trackFriction } from '../../core/session-tracker.js';
14
+ import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
15
+ import { WorkflowManagerBase } from './workflow-manager-base.js';
16
+ import { normalizeSeverity } from '../../core/empathy-types.js';
18
17
 
19
18
  const WORKFLOW_SESSION_PREFIX = 'agent:main:subagent:workflow-';
20
19
 
@@ -25,25 +24,24 @@ export interface EmpathyObserverWorkflowOptions {
25
24
  workspaceDir: string;
26
25
  logger: PluginLogger;
27
26
  subagent: RuntimeDirectDriver['subagent'];
27
+ /** Pass api.runtime.agent.session to enable heartbeat-safe cleanup (#188) */
28
+ agentSession?: RuntimeDirectDriver['agentSession'];
28
29
  }
29
30
 
30
- export class EmpathyObserverWorkflowManager implements WorkflowManager {
31
- private readonly store: WorkflowStore;
32
- private readonly driver: RuntimeDirectDriver;
33
- private readonly logger: PluginLogger;
34
- private readonly workspaceDir: string;
35
-
36
- private activeWorkflows = new Map<string, NodeJS.Timeout>();
37
- private completedWorkflows = new Map<string, number>();
38
- private workflowSpecs = new Map<string, SubagentWorkflowSpec<unknown>>();
39
-
31
+ export class EmpathyObserverWorkflowManager extends WorkflowManagerBase {
40
32
  constructor(opts: EmpathyObserverWorkflowOptions) {
41
- this.workspaceDir = opts.workspaceDir;
42
- this.logger = opts.logger;
43
- this.store = new WorkflowStore({ workspaceDir: opts.workspaceDir });
44
- this.driver = new RuntimeDirectDriver({ subagent: opts.subagent, logger: opts.logger });
33
+ super({
34
+ workspaceDir: opts.workspaceDir,
35
+ logger: opts.logger,
36
+ subagent: opts.subagent,
37
+ agentSession: opts.agentSession,
38
+ workflowType: 'empathy-observer',
39
+ sessionPrefix: WORKFLOW_SESSION_PREFIX,
40
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
41
+ defaultTtlMs: DEFAULT_TTL_MS,
42
+ });
45
43
  }
46
-
44
+
47
45
  async startWorkflow<TResult>(
48
46
  spec: SubagentWorkflowSpec<TResult>,
49
47
  options: {
@@ -53,66 +51,27 @@ export class EmpathyObserverWorkflowManager implements WorkflowManager {
53
51
  metadata?: Record<string, unknown>;
54
52
  }
55
53
  ): Promise<WorkflowHandle> {
56
- const workflowId = this.generateWorkflowId();
57
- const childSessionKey = this.buildChildSessionKey(options.parentSessionId);
58
- const now = Date.now();
59
-
60
- const metadata: WorkflowMetadata = {
61
- parentSessionId: options.parentSessionId,
62
- workspaceDir: options.workspaceDir,
63
- taskInput: options.taskInput,
64
- startedAt: now,
65
- workflowType: spec.workflowType,
66
- ...options.metadata,
67
- };
68
-
69
- this.logger.info(`[PD:EmpathyObserverWorkflow] Starting workflow: workflowId=${workflowId}, type=${spec.workflowType}`);
70
-
71
54
  // Surface degrade: skip boot sessions (they run outside gateway request context)
72
55
  if (options.parentSessionId.startsWith('boot-')) {
73
56
  this.logger.info(`[PD:EmpathyObserverWorkflow] Skipping workflow: boot session (gateway request context unavailable)`);
74
57
  throw new Error(`EmpathyObserverWorkflowManager: cannot start workflow for boot session`);
75
58
  }
76
-
59
+
77
60
  // Surface degrade: check subagent runtime availability before calling run()
78
61
  if (!isSubagentRuntimeAvailable(this.driver.getSubagent())) {
79
62
  this.logger.info(`[PD:EmpathyObserverWorkflow] Skipping workflow: subagent runtime unavailable`);
80
63
  throw new Error(`EmpathyObserverWorkflowManager: subagent runtime unavailable`);
81
64
  }
82
-
65
+
83
66
  if (spec.transport !== 'runtime_direct') {
84
67
  throw new Error(`EmpathyObserverWorkflowManager only supports runtime_direct transport`);
85
68
  }
86
-
87
- const runParams = this.buildRunParams(spec, options, childSessionKey);
88
- const runResult = await this.driver.run(runParams);
89
-
90
- this.store.createWorkflow({
91
- workflow_id: workflowId,
92
- workflow_type: spec.workflowType,
93
- transport: spec.transport,
94
- parent_session_id: options.parentSessionId,
95
- child_session_key: childSessionKey,
96
- run_id: runResult.runId,
97
- state: 'active',
98
- created_at: now,
99
- updated_at: now,
100
- metadata_json: JSON.stringify(metadata),
101
- });
102
- this.store.recordEvent(workflowId, 'spawned', null, 'active', 'subagent spawned', { runId: runResult.runId });
103
- this.workflowSpecs.set(workflowId, spec as SubagentWorkflowSpec<unknown>);
104
-
105
- this.scheduleWaitPoll(workflowId, spec.timeoutMs ?? DEFAULT_TIMEOUT_MS, runResult.runId);
106
-
107
- return {
108
- workflowId,
109
- childSessionKey,
110
- runId: runResult.runId,
111
- state: 'active',
112
- };
69
+
70
+ return super.startWorkflow(spec, options);
113
71
  }
114
-
115
- private buildRunParams<TResult>(
72
+
73
+ /* eslint-disable @typescript-eslint/class-methods-use-this -- Reason: Subclass stores metadata via spec, not class fields */
74
+ protected override createWorkflowMetadata<TResult>(
116
75
  spec: SubagentWorkflowSpec<TResult>,
117
76
  options: {
118
77
  parentSessionId: string;
@@ -120,29 +79,21 @@ export class EmpathyObserverWorkflowManager implements WorkflowManager {
120
79
  taskInput: unknown;
121
80
  metadata?: Record<string, unknown>;
122
81
  },
123
- childSessionKey: string
124
- ): RunParams {
125
- const message = spec.buildPrompt(options.taskInput, {
82
+ now: number
83
+ ): WorkflowMetadata {
84
+ // EmpathyObserver stores standard metadata
85
+ return {
126
86
  parentSessionId: options.parentSessionId,
127
87
  workspaceDir: options.workspaceDir,
128
88
  taskInput: options.taskInput,
129
- startedAt: Date.now(),
89
+ startedAt: now,
130
90
  workflowType: spec.workflowType,
131
- ...(options.metadata ?? {}),
132
- });
133
-
134
- return {
135
- sessionKey: childSessionKey,
136
- message,
137
- lane: 'subagent',
138
- deliver: false,
139
- idempotencyKey: options.parentSessionId
140
- ? `${options.parentSessionId}:${Date.now()}`
141
- : `pd:${childSessionKey}:${Date.now()}`,
142
- expectsCompletionMessage: true,
91
+ ...options.metadata,
143
92
  };
144
93
  }
145
-
94
+
95
+ // ── EmpathyObserver-Specific Methods ─────────────────────────────────────
96
+
146
97
  static buildEmpathyPrompt(userMessage: string): string {
147
98
  return [
148
99
  'You are an empathy observer.',
@@ -151,302 +102,23 @@ export class EmpathyObserverWorkflowManager implements WorkflowManager {
151
102
  `User message: ${JSON.stringify(userMessage.trim())}`,
152
103
  ].join('\n');
153
104
  }
154
-
155
- private scheduleWaitPoll(
156
- workflowId: string,
157
- timeoutMs: number,
158
- runId: string
159
- ): void {
160
- const effectiveTimeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
161
-
162
- const timeout = setTimeout(async () => {
163
- try {
164
- const result = await this.driver.wait({ runId, timeoutMs: effectiveTimeoutMs });
165
- await this.notifyWaitResult(workflowId, result.status, result.error);
166
- } catch (error) {
167
- this.logger.error(`[PD:EmpathyObserverWorkflow] Wait poll failed: ${String(error)}`);
168
- await this.notifyWaitResult(workflowId, 'error', String(error));
169
- }
170
- }, 100);
171
-
172
- this.activeWorkflows.set(workflowId, timeout);
173
- }
174
-
175
- async notifyWaitResult(
176
- workflowId: string,
177
- status: 'ok' | 'error' | 'timeout',
178
- error?: string
179
- ): Promise<void> {
180
- const workflow = this.store.getWorkflow(workflowId);
181
- if (!workflow) {
182
- this.logger.warn(`[PD:EmpathyObserverWorkflow] notifyWaitResult: workflow not found: ${workflowId}`);
183
- return;
184
- }
185
-
186
- if (workflow.state === 'completed' || workflow.state === 'terminal_error' || workflow.state === 'expired') {
187
- this.logger.info(`[PD:EmpathyObserverWorkflow] notifyWaitResult: ignoring terminal workflow: ${workflowId}, state=${workflow.state}`);
188
- return;
189
- }
190
-
191
- this.logger.info(`[PD:EmpathyObserverWorkflow] notifyWaitResult: workflowId=${workflowId}, status=${status}`);
192
-
193
- const previousState = workflow.state;
194
- this.store.updateWorkflowState(workflowId, 'wait_result');
195
- this.store.recordEvent(workflowId, 'wait_result', previousState, 'wait_result', `wait completed: ${status}`, { error });
196
-
197
- const spec = this.workflowSpecs.get(workflowId);
198
- const shouldFinalize = spec ? spec.shouldFinalizeOnWaitStatus(status) : status === 'ok';
199
-
200
- if (shouldFinalize) {
201
- await this.finalizeOnce(workflowId);
202
- } else {
203
- this.store.updateWorkflowState(workflowId, 'terminal_error');
204
- this.store.recordEvent(workflowId, 'finalize_skipped', 'wait_result', 'terminal_error', `wait status: ${status}`, { error });
205
- }
206
- }
207
-
208
- async notifyLifecycleEvent(
209
- workflowId: string,
210
- event: 'subagent_spawned' | 'subagent_ended',
211
- data?: { outcome?: 'ok' | 'error' | 'timeout' | 'killed' | 'reset' | 'deleted'; error?: string }
212
- ): Promise<void> {
213
- this.logger.info(`[PD:EmpathyObserverWorkflow] notifyLifecycleEvent: workflowId=${workflowId}, event=${event}`);
214
-
215
- if (event === 'subagent_ended' && data?.outcome) {
216
- await this.notifyWaitResult(workflowId, data.outcome === 'ok' ? 'ok' : data.outcome === 'error' ? 'error' : 'timeout', data.error);
217
- }
218
- }
219
-
220
- async finalizeOnce(workflowId: string): Promise<void> {
221
- const workflow = this.store.getWorkflow(workflowId);
222
- if (!workflow) {
223
- this.logger.warn(`[PD:EmpathyObserverWorkflow] finalizeOnce: workflow not found: ${workflowId}`);
224
- return;
225
- }
226
-
227
- const spec = this.workflowSpecs.get(workflowId);
228
- if (!spec) {
229
- throw new Error(`Workflow spec not registered for ${workflowId}`);
230
- }
231
-
232
- if (this.isCompleted(workflowId)) {
233
- this.logger.info(`[PD:EmpathyObserverWorkflow] finalizeOnce: already completed: ${workflowId}`);
234
- return;
235
- }
236
-
237
- this.logger.info(`[PD:EmpathyObserverWorkflow] Finalizing workflow: ${workflowId}`);
238
-
239
- this.store.updateWorkflowState(workflowId, 'finalizing');
240
-
241
- try {
242
- const result = await this.driver.getResult({ sessionKey: workflow.child_session_key, limit: 20 });
243
-
244
- const metadata = JSON.parse(workflow.metadata_json) as WorkflowMetadata;
245
- const parsed = await spec.parseResult({
246
- messages: result.messages,
247
- assistantTexts: result.assistantTexts,
248
- metadata,
249
- waitStatus: 'ok',
250
- });
251
-
252
- if (!parsed) {
253
- this.store.updateWorkflowState(workflowId, 'terminal_error');
254
- this.store.recordEvent(workflowId, 'parse_failed', 'finalizing', 'terminal_error', 'spec.parseResult returned null', {});
255
- return;
256
- }
257
-
258
- await spec.persistResult({
259
- result: parsed,
260
- metadata,
261
- workspaceDir: this.workspaceDir,
262
- });
263
- this.store.recordEvent(workflowId, 'persisted', 'finalizing', 'finalizing', 'result persisted', {});
264
-
265
- if (spec.shouldDeleteSessionAfterFinalize && workflow.run_id) {
266
- try {
267
- await this.driver.cleanup({ sessionKey: workflow.child_session_key });
268
- this.store.updateCleanupState(workflowId, 'completed');
269
- } catch (cleanupError) {
270
- this.logger.error(`[PD:EmpathyObserverWorkflow] cleanup failed after persistence: ${String(cleanupError)}`);
271
- this.store.updateCleanupState(workflowId, 'failed');
272
- this.store.updateWorkflowState(workflowId, 'cleanup_pending');
273
- this.store.recordEvent(workflowId, 'cleanup_failed', 'finalizing', 'cleanup_pending', String(cleanupError), {});
274
- this.markCompleted(workflowId);
275
- return;
276
- }
277
- }
278
-
279
- this.store.updateWorkflowState(workflowId, 'completed');
280
- this.store.recordEvent(workflowId, 'finalized', 'finalizing', 'completed', 'success', {});
281
- this.markCompleted(workflowId);
282
-
283
- } catch (error) {
284
- this.logger.error(`[PD:EmpathyObserverWorkflow] finalizeOnce failed: ${String(error)}`);
285
- this.store.updateWorkflowState(workflowId, 'terminal_error');
286
- this.store.recordEvent(workflowId, 'finalize_error', 'finalizing', 'terminal_error', String(error), {});
287
- throw error;
288
- }
289
- }
290
-
291
- async sweepExpiredWorkflows(maxAgeMs = DEFAULT_TTL_MS): Promise<number> {
292
- const expired = this.store.getExpiredWorkflows(maxAgeMs);
293
-
294
- this.logger.info(`[PD:EmpathyObserverWorkflow] sweepExpiredWorkflows: found ${expired.length} expired`);
295
-
296
- for (const workflow of expired) {
297
- try {
298
- this.logger.info(`[PD:EmpathyObserverWorkflow] Sweeping expired workflow: ${workflow.workflow_id}`);
299
-
300
- await this.driver.cleanup({ sessionKey: workflow.child_session_key });
301
- this.store.updateCleanupState(workflow.workflow_id, 'completed');
302
- this.store.updateWorkflowState(workflow.workflow_id, 'expired');
303
- this.store.recordEvent(workflow.workflow_id, 'swept', workflow.state, 'expired', 'TTL expired', {});
304
-
305
- } catch (error) {
306
- this.logger.error(`[PD:EmpathyObserverWorkflow] Sweep cleanup failed for ${workflow.workflow_id}: ${String(error)}`);
307
- this.store.updateCleanupState(workflow.workflow_id, 'failed');
308
- }
309
- }
310
105
 
311
- // Clean up memory Maps to prevent leaks
312
- const cutoff = Date.now() - 60_000; // 1 minute dedup window
313
- for (const [id, timestamp] of this.completedWorkflows) {
314
- if (timestamp < cutoff) {
315
- this.completedWorkflows.delete(id);
316
- }
317
- }
318
- for (const [id, timeout] of this.activeWorkflows) {
319
- const wf = this.store.getWorkflow(id);
320
- if (!wf || wf.state === 'expired' || wf.state === 'completed' || wf.state === 'terminal_error') {
321
- clearTimeout(timeout);
322
- this.activeWorkflows.delete(id);
323
- }
324
- }
325
-
326
- return expired.length;
327
- }
328
-
329
- async getWorkflowDebugSummary(workflowId: string, eventLimit = 10): Promise<WorkflowDebugSummary | null> {
330
- const workflow = this.store.getWorkflow(workflowId);
331
- if (!workflow) return null;
332
-
333
- const metadata = JSON.parse(workflow.metadata_json) as WorkflowMetadata;
334
- const recentEvents = this.store
335
- .getEvents(workflowId)
336
- .slice(-eventLimit)
337
- .map((event) => ({
338
- eventType: event.event_type,
339
- fromState: event.from_state,
340
- toState: event.to_state,
341
- reason: event.reason,
342
- createdAt: event.created_at,
343
- payload: JSON.parse(event.payload_json || '{}') as Record<string, unknown>,
344
- }));
345
-
346
- return {
347
- workflowId: workflow.workflow_id,
348
- workflowType: workflow.workflow_type,
349
- transport: workflow.transport,
350
- parentSessionId: workflow.parent_session_id,
351
- childSessionKey: workflow.child_session_key,
352
- runId: workflow.run_id,
353
- state: workflow.state,
354
- cleanupState: workflow.cleanup_state,
355
- lastObservedAt: workflow.last_observed_at ?? null,
356
- metadata,
357
- recentEvents,
358
- };
359
- }
360
-
361
- private generateWorkflowId(): string {
106
+ /* eslint-disable @typescript-eslint/class-methods-use-this -- Reason: Subclass overrides id generation pattern */
107
+ protected override generateWorkflowId(): string {
362
108
  return `wf_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
363
109
  }
364
-
365
- private buildChildSessionKey(parentSessionId: string): string {
366
- const safeParentSessionId = parentSessionId
367
- .replace(/[^a-zA-Z0-9_-]/g, '_')
368
- .substring(0, 64);
369
- const timestamp = Date.now();
370
- return `${WORKFLOW_SESSION_PREFIX}${safeParentSessionId}-${timestamp}`;
371
- }
372
-
373
- private extractAssistantText(messages: unknown[], assistantTexts?: string[]): string {
374
- if (assistantTexts && assistantTexts.length > 0) {
375
- return assistantTexts[assistantTexts.length - 1] || '';
376
- }
377
-
378
- for (let i = messages.length - 1; i >= 0; i--) {
379
- const msg = messages[i] as { role?: string; content?: unknown };
380
- if (msg?.role !== 'assistant') continue;
381
- if (typeof msg.content === 'string') return msg.content;
382
- if (Array.isArray(msg.content)) {
383
- const txt = msg.content
384
- .filter((part: any) => part?.type === 'text' && typeof part.text === 'string')
385
- .map((part: any) => part.text)
386
- .join('\n');
387
- if (txt) return txt;
388
- }
389
- }
390
-
391
- return '';
392
- }
393
-
394
- parseEmpathyPayload(rawText: string): EmpathyObserverPayload | null {
395
- if (!rawText?.trim()) return null;
396
-
397
- try {
398
- return JSON.parse(rawText.trim()) as EmpathyObserverPayload;
399
- } catch {
400
- const match = rawText.match(/\{[\s\S]*\}/);
401
- if (!match) {
402
- this.logger.warn('[PD:EmpathyObserverWorkflow] Observer payload is not valid JSON');
403
- return null;
404
- }
405
- try {
406
- return JSON.parse(match[0]) as EmpathyObserverPayload;
407
- } catch {
408
- this.logger.warn('[PD:EmpathyObserverWorkflow] Failed to parse observer JSON payload');
409
- return null;
410
- }
411
- }
412
- }
413
-
414
- private isCompleted(workflowId: string): boolean {
415
- const timestamp = this.completedWorkflows.get(workflowId);
416
- if (!timestamp) return false;
417
- if (Date.now() - timestamp > 5 * 60 * 1000) {
418
- this.completedWorkflows.delete(workflowId);
419
- return false;
420
- }
421
- return true;
422
- }
423
-
424
- private markCompleted(workflowId: string): void {
425
- this.completedWorkflows.set(workflowId, Date.now());
426
- this.workflowSpecs.delete(workflowId);
427
-
428
- const timeout = this.activeWorkflows.get(workflowId);
429
- if (timeout) {
430
- clearTimeout(timeout);
431
- this.activeWorkflows.delete(workflowId);
432
- }
433
- }
434
-
435
- dispose(): void {
436
- for (const timeout of this.activeWorkflows.values()) {
437
- clearTimeout(timeout);
438
- }
439
- this.activeWorkflows.clear();
440
- this.store.dispose();
441
- }
442
110
  }
443
111
 
112
+ // ─── Factory ─────────────────────────────────────────────────────────────────
113
+
444
114
  export function createEmpathyObserverWorkflowManager(
445
115
  opts: EmpathyObserverWorkflowOptions
446
116
  ): EmpathyObserverWorkflowManager {
447
117
  return new EmpathyObserverWorkflowManager(opts);
448
118
  }
449
119
 
120
+ // ─── Helper Functions ─────────────────────────────────────────────────────────
121
+
450
122
  /**
451
123
  * Extract raw assistant text from messages or assistantTexts array.
452
124
  */
@@ -460,8 +132,8 @@ function extractAssistantTextForSpec(messages: unknown[], assistantTexts?: strin
460
132
  if (typeof msg.content === 'string') return msg.content;
461
133
  if (Array.isArray(msg.content)) {
462
134
  const txt = msg.content
463
- .filter((part: any) => part?.type === 'text' && typeof part.text === 'string')
464
- .map((part: any) => part.text)
135
+ .filter((part: unknown) => part && typeof part === 'object' && (part as { type?: string }).type === 'text' && typeof (part as { text?: unknown }).text === 'string')
136
+ .map((part: unknown) => (part as { text: string }).text)
465
137
  .join('\n');
466
138
  if (txt) return txt;
467
139
  }
@@ -477,7 +149,7 @@ function parseEmpathyPayloadForSpec(rawText: string): EmpathyObserverPayload | n
477
149
  try {
478
150
  return JSON.parse(rawText.trim()) as EmpathyObserverPayload;
479
151
  } catch {
480
- const match = rawText.match(/\{[\s\S]*\}/);
152
+ const match = /\{[\s\S]*\}/.exec(rawText);
481
153
  if (!match) return null;
482
154
  try {
483
155
  return JSON.parse(match[0]) as EmpathyObserverPayload;
@@ -487,15 +159,6 @@ function parseEmpathyPayloadForSpec(rawText: string): EmpathyObserverPayload | n
487
159
  }
488
160
  }
489
161
 
490
- /**
491
- * Normalize severity to valid enum.
492
- */
493
- function normalizeSeverityForSpec(severity: string | undefined): 'mild' | 'moderate' | 'severe' {
494
- if (severity === 'severe') return 'severe';
495
- if (severity === 'moderate') return 'moderate';
496
- return 'mild';
497
- }
498
-
499
162
  /**
500
163
  * Normalize confidence to [0, 1] range.
501
164
  */
@@ -513,10 +176,8 @@ function scoreFromSeverityForSpec(severity: string | undefined, wctx: WorkspaceC
513
176
  return Number(wctx.config.get('empathy_engine.penalties.mild') ?? 10);
514
177
  }
515
178
 
516
- /**
517
- * EmpathyObserver workflow specification.
518
- * This spec drives EmpathyObserverWorkflowManager for the empathy observer workflow.
519
- */
179
+ // ─── Workflow Spec ─────────────────────────────────────────────────────────────
180
+
520
181
  export const empathyObserverWorkflowSpec: SubagentWorkflowSpec<EmpathyResult> = {
521
182
  workflowType: 'empathy-observer',
522
183
  transport: 'runtime_direct',
@@ -524,6 +185,7 @@ export const empathyObserverWorkflowSpec: SubagentWorkflowSpec<EmpathyResult> =
524
185
  ttlMs: 300_000,
525
186
  shouldDeleteSessionAfterFinalize: true,
526
187
 
188
+ /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: interface method signature, unused parameter for spec compliance */
527
189
  buildPrompt(taskInput: unknown, _metadata: WorkflowMetadata): string {
528
190
  const userMessage = String(taskInput).trim();
529
191
  return [
@@ -541,7 +203,7 @@ export const empathyObserverWorkflowSpec: SubagentWorkflowSpec<EmpathyResult> =
541
203
 
542
204
  return {
543
205
  damageDetected: payload.damageDetected ?? false,
544
- severity: normalizeSeverityForSpec(payload.severity),
206
+ severity: normalizeSeverity(payload.severity),
545
207
  confidence: normalizeConfidenceForSpec(payload.confidence),
546
208
  reason: payload.reason ?? '',
547
209
  painScore: 0,
@@ -10,6 +10,8 @@ export {
10
10
  type CleanupParams,
11
11
  } from './runtime-direct-driver.js';
12
12
 
13
+ export { isExpectedSubagentError } from './subagent-error-utils.js';
14
+
13
15
  export { WorkflowStore, type WorkflowStoreOptions } from './workflow-store.js';
14
16
 
15
17
  export {