principles-disciple 1.34.2 → 1.36.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 (35) hide show
  1. package/.dependency-cruiser.json +19 -0
  2. package/openclaw.plugin.json +1 -1
  3. package/package.json +6 -3
  4. package/src/config/defaults/runtime.ts +100 -24
  5. package/src/core/correction-cue-learner.ts +23 -8
  6. package/src/core/event-log.ts +87 -20
  7. package/src/core/init.ts +2 -2
  8. package/src/core/nocturnal-candidate-scoring.ts +6 -6
  9. package/src/core/nocturnal-trinity-types.ts +94 -0
  10. package/src/core/nocturnal-trinity.ts +35 -99
  11. package/src/core/session-tracker.ts +7 -6
  12. package/src/core/system-logger.ts +104 -12
  13. package/src/core/workspace-dir-service.ts +40 -6
  14. package/src/core/workspace-dir-validation.ts +5 -37
  15. package/src/hooks/prompt.ts +3 -3
  16. package/src/hooks/trajectory-collector.ts +7 -7
  17. package/src/index.ts +8 -68
  18. package/src/service/central-sync-service.ts +3 -8
  19. package/src/service/correction-observer-workflow-manager.ts +2 -2
  20. package/src/service/evolution-worker.ts +13 -22
  21. package/src/service/keyword-optimization-service.ts +2 -2
  22. package/src/service/nocturnal-service.ts +62 -43
  23. package/src/service/subagent-workflow/correction-observer-types.ts +69 -0
  24. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +246 -0
  25. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +4 -4
  26. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +4 -4
  27. package/src/service/subagent-workflow/index.ts +13 -0
  28. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +2 -2
  29. package/src/service/subagent-workflow/types.ts +69 -3
  30. package/src/utils/shadow-fingerprint.ts +42 -0
  31. package/src/utils/workspace-resolver.ts +54 -0
  32. package/tests/core/correction-cue-learner.test.ts +345 -0
  33. package/tests/core/workspace-dir-validation.test.ts +1 -1
  34. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +3 -3
  35. package/vitest.config.ts +53 -6
@@ -0,0 +1,246 @@
1
+ /**
2
+ * CorrectionObserverWorkflowManager
3
+ *
4
+ * Workflow manager that dispatches an LLM subagent to optimize correction
5
+ * keywords based on recent match performance data and user feedback.
6
+ *
7
+ * Follows the established WorkflowManagerBase pattern from EmpathyObserverWorkflowManager.
8
+ */
9
+
10
+ import type { PluginLogger } from '../../openclaw-sdk.js';
11
+ import type {
12
+ SubagentWorkflowSpec,
13
+ WorkflowMetadata,
14
+ WorkflowResultContext,
15
+ WorkflowPersistContext,
16
+ WorkflowHandle,
17
+ } from './types.js';
18
+ import type { RuntimeDirectDriver } from './runtime-direct-driver.js';
19
+ import { WorkflowManagerBase } from './workflow-manager-base.js';
20
+ import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
21
+ import type {
22
+ CorrectionObserverPayload,
23
+ CorrectionObserverResult,
24
+ } from './correction-observer-types.js';
25
+
26
+ const WORKFLOW_SESSION_PREFIX = 'agent:main:subagent:workflow-correction-';
27
+
28
+ const DEFAULT_TIMEOUT_MS = 30_000;
29
+ const DEFAULT_TTL_MS = 5 * 60 * 1000;
30
+
31
+ // Prompt formatting constants
32
+ const MAX_TRAJECTORY_MESSAGE_LENGTH = 80;
33
+
34
+ // ── Options ─────────────────────────────────────────────────────────────────
35
+
36
+ export interface CorrectionObserverWorkflowOptions {
37
+ workspaceDir: string;
38
+ logger: PluginLogger;
39
+ subagent: RuntimeDirectDriver['subagent'];
40
+ /** Pass api.runtime.agent.session to enable heartbeat-safe cleanup (#188) */
41
+ agentSession?: RuntimeDirectDriver['agentSession'];
42
+ }
43
+
44
+ // ── Helper Functions ─────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Extract raw assistant text from messages or assistantTexts array.
48
+ */
49
+ function extractAssistantTextForSpec(messages: unknown[], assistantTexts?: string[]): string {
50
+ if (assistantTexts && assistantTexts.length > 0) {
51
+ return assistantTexts[assistantTexts.length - 1] || '';
52
+ }
53
+ for (let i = messages.length - 1; i >= 0; i--) {
54
+ const msg = messages[i] as { role?: string; content?: unknown };
55
+ if (msg?.role !== 'assistant') continue;
56
+ if (typeof msg.content === 'string') return msg.content;
57
+ if (Array.isArray(msg.content)) {
58
+ const txt = msg.content
59
+ .filter((part: unknown) => part && typeof part === 'object' && (part as { type?: string }).type === 'text' && typeof (part as { text?: unknown }).text === 'string')
60
+ .map((part: unknown) => (part as { text: string }).text)
61
+ .join('\n');
62
+ if (txt) return txt;
63
+ }
64
+ }
65
+ return '';
66
+ }
67
+
68
+ /**
69
+ * Parse correction observer JSON payload from raw text.
70
+ */
71
+ function parseCorrectionObserverPayload(rawText: string): CorrectionObserverResult | null {
72
+ if (!rawText?.trim()) return null;
73
+ try {
74
+ return JSON.parse(rawText.trim()) as CorrectionObserverResult;
75
+ } catch {
76
+ const match = /\{[\s\S]*\}/.exec(rawText);
77
+ if (!match) return null;
78
+ try {
79
+ return JSON.parse(match[0]) as CorrectionObserverResult;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+ }
85
+
86
+ // ── Workflow Spec ─────────────────────────────────────────────────────────────
87
+
88
+ export const correctionObserverWorkflowSpec: SubagentWorkflowSpec<CorrectionObserverResult> = {
89
+ workflowType: 'correction_observer',
90
+ transport: 'runtime_direct',
91
+ timeoutMs: 30_000,
92
+ ttlMs: 300_000,
93
+ shouldDeleteSessionAfterFinalize: true,
94
+
95
+ buildPrompt(taskInput: unknown, _metadata: WorkflowMetadata): string {
96
+ const payload = taskInput as CorrectionObserverPayload;
97
+ const { keywordStoreSummary, recentMessages, trajectoryHistory } = payload;
98
+
99
+ const termsList = keywordStoreSummary.terms
100
+ .map(t => ` - term="${t.term}", weight=${t.weight}, hits=${t.hitCount}, TP=${t.truePositiveCount}, FP=${t.falsePositiveCount}`)
101
+ .join('\n');
102
+
103
+ const messages = recentMessages.length > 0
104
+ ? recentMessages.map(m => ` - ${JSON.stringify(m)}`).join('\n')
105
+ : ' (none)';
106
+
107
+ const trajectory = trajectoryHistory.length > 0
108
+ ? trajectoryHistory.map(t => ` - [${t.sessionId}] ${t.term} (${t.timestamp}): ${t.userMessage.substring(0, MAX_TRAJECTORY_MESSAGE_LENGTH)}`)
109
+ .join('\n')
110
+ : ' (none)';
111
+
112
+ return [
113
+ 'You are a correction keyword optimizer.',
114
+ '',
115
+ '## TASK',
116
+ 'Analyze the current correction keyword store and recent user messages.',
117
+ 'Recommend ADD/UPDATE/REMOVE actions to improve correction cue accuracy.',
118
+ '',
119
+ '## Current Keyword Store (' + keywordStoreSummary.totalKeywords + ' terms):',
120
+ termsList,
121
+ '',
122
+ '## Recent User Messages (' + recentMessages.length + ' messages):',
123
+ messages,
124
+ '',
125
+ '## Correction Trajectory (recent confirmed corrections, D-40-08):',
126
+ trajectory,
127
+ '',
128
+ '## Rules:',
129
+ '- ADD: If a correction pattern is detected in messages but not in store',
130
+ '- UPDATE: If a term\'s weight should change based on TP/FP ratio',
131
+ '- REMOVE: If a term has 0 hits after many uses AND high false positive rate (>0.3)',
132
+ '- Keep reasoning concise (max 100 chars)',
133
+ '- Weight range: 0.1-0.9',
134
+ '',
135
+ 'Return strict JSON (no markdown):',
136
+ '{"updated": boolean, "updates": {...}, "summary": string}',
137
+ ].join('\n');
138
+ },
139
+
140
+ async parseResult(ctx: WorkflowResultContext): Promise<CorrectionObserverResult | null> {
141
+ const rawText = extractAssistantTextForSpec(ctx.messages, ctx.assistantTexts);
142
+ return parseCorrectionObserverPayload(rawText);
143
+ },
144
+
145
+ async persistResult(_ctx: WorkflowPersistContext<CorrectionObserverResult>): Promise<void> {
146
+ // Result persistence is handled by the caller (evolution-worker.ts)
147
+ // which reads the result and applies keyword store updates.
148
+ // This spec handles only the LLM dispatch and result parsing.
149
+ },
150
+
151
+ shouldFinalizeOnWaitStatus(status: 'ok' | 'error' | 'timeout'): boolean {
152
+ return status === 'ok';
153
+ },
154
+ };
155
+
156
+ // ── Manager Class ─────────────────────────────────────────────────────────────
157
+
158
+ export class CorrectionObserverWorkflowManager extends WorkflowManagerBase {
159
+ constructor(opts: CorrectionObserverWorkflowOptions) {
160
+ super({
161
+ workspaceDir: opts.workspaceDir,
162
+ logger: opts.logger,
163
+ subagent: opts.subagent,
164
+ agentSession: opts.agentSession,
165
+ workflowType: 'correction_observer',
166
+ sessionPrefix: WORKFLOW_SESSION_PREFIX,
167
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
168
+ defaultTtlMs: DEFAULT_TTL_MS,
169
+ });
170
+ }
171
+
172
+ async startWorkflow<TResult>(
173
+ spec: SubagentWorkflowSpec<TResult>,
174
+ options: {
175
+ parentSessionId: string;
176
+ workspaceDir?: string;
177
+ taskInput: unknown;
178
+ metadata?: Record<string, unknown>;
179
+ }
180
+ ): Promise<WorkflowHandle> {
181
+ // Surface degrade: skip boot sessions
182
+ if (options.parentSessionId.startsWith('boot-')) {
183
+ this.logger.info(`[PD:CorrectionObserver] Skipping workflow: boot session`);
184
+ throw new Error(`CorrectionObserverWorkflowManager: cannot start workflow for boot session`);
185
+ }
186
+
187
+ // Surface degrade: check subagent runtime availability
188
+ if (!isSubagentRuntimeAvailable(this.driver.getSubagent())) {
189
+ this.logger.info(`[PD:CorrectionObserver] Skipping workflow: subagent runtime unavailable`);
190
+ throw new Error(`CorrectionObserverWorkflowManager: subagent runtime unavailable`);
191
+ }
192
+
193
+ if (spec.transport !== 'runtime_direct') {
194
+ throw new Error(`CorrectionObserverWorkflowManager only supports runtime_direct transport`);
195
+ }
196
+
197
+ return super.startWorkflow(spec, options);
198
+ }
199
+
200
+ /**
201
+ * Get the parsed workflow result for a completed workflow.
202
+ * Used by callers (evolution-worker.ts) to retrieve LLM optimization results
203
+ * after the workflow completes, so mutations can be applied to the keyword store.
204
+ */
205
+ async getWorkflowResult(workflowId: string): Promise<CorrectionObserverResult | null> {
206
+ const workflow = this.store.getWorkflow(workflowId);
207
+ if (!workflow) return null;
208
+
209
+ const result = await this.driver.getResult({ sessionKey: workflow.child_session_key, limit: 20 });
210
+ return correctionObserverWorkflowSpec.parseResult({
211
+ messages: result.messages,
212
+ assistantTexts: result.assistantTexts,
213
+ metadata: JSON.parse(workflow.metadata_json) as WorkflowMetadata,
214
+ waitStatus: 'ok',
215
+ });
216
+ }
217
+
218
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
219
+ protected override createWorkflowMetadata<TResult>(
220
+ spec: SubagentWorkflowSpec<TResult>,
221
+ options: {
222
+ parentSessionId: string;
223
+ workspaceDir?: string;
224
+ taskInput: unknown;
225
+ metadata?: Record<string, unknown>;
226
+ },
227
+ now: number
228
+ ): WorkflowMetadata {
229
+ return {
230
+ parentSessionId: options.parentSessionId,
231
+ workspaceDir: options.workspaceDir,
232
+ taskInput: options.taskInput,
233
+ startedAt: now,
234
+ workflowType: spec.workflowType,
235
+ ...options.metadata,
236
+ };
237
+ }
238
+ }
239
+
240
+ // ── Factory ─────────────────────────────────────────────────────────────────
241
+
242
+ export function createCorrectionObserverWorkflowManager(
243
+ opts: CorrectionObserverWorkflowOptions
244
+ ): CorrectionObserverWorkflowManager {
245
+ return new CorrectionObserverWorkflowManager(opts);
246
+ }
@@ -13,11 +13,11 @@ import type { RuntimeDirectDriver } from './runtime-direct-driver.js';
13
13
  import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
14
14
  import { buildCritiquePromptV2 } from '../../tools/critique-prompt.js';
15
15
  import { WorkflowManagerBase } from './workflow-manager-base.js';
16
+ import { DEEP_REFLECT_TTL_MS } from '../../config/defaults/runtime.js';
16
17
 
17
18
  const WORKFLOW_SESSION_PREFIX = 'agent:main:subagent:workflow-';
18
19
 
19
20
  const DEFAULT_TIMEOUT_MS = 60_000; // Deep-reflect needs more time than empathy
20
- const DEFAULT_TTL_MS = 10 * 60 * 1000;
21
21
 
22
22
  export interface DeepReflectWorkflowOptions {
23
23
  workspaceDir: string;
@@ -37,7 +37,7 @@ export class DeepReflectWorkflowManager extends WorkflowManagerBase {
37
37
  workflowType: 'deep-reflect',
38
38
  sessionPrefix: WORKFLOW_SESSION_PREFIX,
39
39
  defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
40
- defaultTtlMs: DEFAULT_TTL_MS,
40
+ defaultTtlMs: DEEP_REFLECT_TTL_MS,
41
41
  });
42
42
  }
43
43
 
@@ -70,7 +70,7 @@ export class DeepReflectWorkflowManager extends WorkflowManagerBase {
70
70
  }
71
71
 
72
72
 
73
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
73
+
74
74
  protected override generateWorkflowId(): string {
75
75
  return `wf_dr_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
76
76
  }
@@ -98,7 +98,7 @@ export const deepReflectWorkflowSpec: SubagentWorkflowSpec<DeepReflectResult> =
98
98
  workflowType: 'deep-reflect',
99
99
  transport: 'runtime_direct',
100
100
  timeoutMs: 60_000,
101
- ttlMs: 10 * 60 * 1000,
101
+ ttlMs: DEEP_REFLECT_TTL_MS,
102
102
  shouldDeleteSessionAfterFinalize: true,
103
103
 
104
104
  buildPrompt(taskInput: unknown, ctx: DeepReflectBuildPromptContext): string {
@@ -14,11 +14,11 @@ import { trackFriction } from '../../core/session-tracker.js';
14
14
  import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
15
15
  import { WorkflowManagerBase } from './workflow-manager-base.js';
16
16
  import { normalizeSeverity } from '../../core/empathy-types.js';
17
+ import { WORKFLOW_TTL_MS } from '../../config/defaults/runtime.js';
17
18
 
18
19
  const WORKFLOW_SESSION_PREFIX = 'agent:main:subagent:workflow-';
19
20
 
20
21
  const DEFAULT_TIMEOUT_MS = 30_000;
21
- const DEFAULT_TTL_MS = 5 * 60 * 1000;
22
22
 
23
23
  export interface EmpathyObserverWorkflowOptions {
24
24
  workspaceDir: string;
@@ -38,7 +38,7 @@ export class EmpathyObserverWorkflowManager extends WorkflowManagerBase {
38
38
  workflowType: 'empathy-observer',
39
39
  sessionPrefix: WORKFLOW_SESSION_PREFIX,
40
40
  defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
41
- defaultTtlMs: DEFAULT_TTL_MS,
41
+ defaultTtlMs: WORKFLOW_TTL_MS,
42
42
  });
43
43
  }
44
44
 
@@ -71,7 +71,7 @@ export class EmpathyObserverWorkflowManager extends WorkflowManagerBase {
71
71
  }
72
72
 
73
73
 
74
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
74
+
75
75
  protected override createWorkflowMetadata<TResult>(
76
76
  spec: SubagentWorkflowSpec<TResult>,
77
77
  options: {
@@ -105,7 +105,7 @@ export class EmpathyObserverWorkflowManager extends WorkflowManagerBase {
105
105
  }
106
106
 
107
107
 
108
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
108
+
109
109
  protected override generateWorkflowId(): string {
110
110
  return `wf_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
111
111
  }
@@ -65,3 +65,16 @@ export type {
65
65
  WorkflowEventRow,
66
66
  WorkflowDebugSummary,
67
67
  } from './types.js';
68
+
69
+ export {
70
+ CorrectionObserverWorkflowManager,
71
+ createCorrectionObserverWorkflowManager,
72
+ correctionObserverWorkflowSpec,
73
+ type CorrectionObserverWorkflowOptions,
74
+ } from './correction-observer-workflow-manager.js';
75
+
76
+ export type {
77
+ CorrectionObserverPayload,
78
+ CorrectionObserverResult,
79
+ CorrectionObserverWorkflowSpec,
80
+ } from './correction-observer-types.js';
@@ -36,7 +36,7 @@ import {
36
36
  } from '../nocturnal-service.js';
37
37
  import { type TrinityStageFailure, type TrinityResult } from '../../core/nocturnal-trinity.js';
38
38
  import type { TrinityRuntimeAdapter } from '../../core/nocturnal-trinity.js';
39
- import type { RecentPainContext } from '../evolution-worker.js';
39
+ import type { RecentPainContext } from './types.js';
40
40
  import * as fs from 'fs';
41
41
  import * as path from 'path';
42
42
  import { validateNocturnalSnapshotIngress } from '../../core/nocturnal-snapshot-contract.js';
@@ -394,7 +394,7 @@ export class NocturnalWorkflowManager implements WorkflowManager {
394
394
  }
395
395
 
396
396
 
397
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
397
+
398
398
  async notifyLifecycleEvent(
399
399
 
400
400
  _workflowId: string,
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type {
13
13
  NocturnalArtifact,
14
+ ArbiterResult,
14
15
  } from '../../core/nocturnal-arbiter.js';
15
16
  import type {
16
17
  BoundedAction,
@@ -18,12 +19,16 @@ import type {
18
19
  import type {
19
20
  NocturnalSessionSnapshot,
20
21
  } from '../../core/nocturnal-trajectory-extractor.js';
21
- import type {
22
- NocturnalRunDiagnostics,
23
- } from '../nocturnal-service.js';
24
22
  import type {
25
23
  TrinityResult,
26
24
  } from '../../core/nocturnal-trinity.js';
25
+ import type {
26
+ IdleCheckResult,
27
+ PreflightCheckResult,
28
+ } from '../nocturnal-runtime.js';
29
+ import type {
30
+ NocturnalSelectionResult,
31
+ } from '../nocturnal-target-selector.js';
27
32
 
28
33
  // ── Workflow Transport ────────────────────────────────────────────────────────
29
34
 
@@ -366,6 +371,27 @@ export type { PluginLogger } from '../../openclaw-sdk.js';
366
371
 
367
372
  // ── Nocturnal Workflow Types ───────────────────────────────────────────────────
368
373
 
374
+ /**
375
+ * Recent pain context for sleep_reflection tasks.
376
+ * Used by target selector for ranking bias and context enrichment.
377
+ * Originally from evolution-worker.ts, moved here to break circular dependency.
378
+ */
379
+ export interface RecentPainContext {
380
+ /** Most recent unresolved pain event */
381
+ mostRecent: {
382
+ score: number;
383
+ source: string;
384
+ reason: string;
385
+ timestamp: string;
386
+ /** Session ID where the pain occurred */
387
+ sessionId: string;
388
+ } | null;
389
+ /** Count of pain events in the recent window (for signal strength) */
390
+ recentPainCount: number;
391
+ /** Highest pain score in the recent window */
392
+ recentMaxPainScore: number;
393
+ }
394
+
369
395
  /**
370
396
  * Nocturnal workflow result type.
371
397
  * Mirrors NocturnalRunResult from nocturnal-service.ts (per D-02).
@@ -381,3 +407,43 @@ export type NocturnalResult = {
381
407
  diagnostics: NocturnalRunDiagnostics;
382
408
  trinityTelemetry?: TrinityResult['telemetry'];
383
409
  };
410
+
411
+ /**
412
+ * Diagnostics from each pipeline stage.
413
+ * Duplicated from nocturnal-service.ts to break circular dependency.
414
+ */
415
+ export interface NocturnalRunDiagnostics {
416
+ /** Pre-flight check result */
417
+ preflight: PreflightCheckResult | null;
418
+ /** Selection result */
419
+ selection: NocturnalSelectionResult | null;
420
+ /** Idle check result */
421
+ idle: IdleCheckResult | null;
422
+ /** Whether Trinity chain was attempted */
423
+ trinityAttempted: boolean;
424
+ /** Trinity result (if trinityAttempted === true) */
425
+ trinityResult: TrinityResult | null;
426
+ /** Which chain mode was used */
427
+ chainModeUsed: 'trinity' | 'single-reflector' | null;
428
+ /** Arbiter validation result */
429
+ arbiterResult: ArbiterResult | null;
430
+ /** Executability validation result (if arbiter passed) */
431
+ executabilityResult: { executable: boolean; failures: string[] } | null;
432
+ /** Whether artifact was persisted */
433
+ persisted: boolean;
434
+ /** Persistence path (if persisted) */
435
+ persistedPath?: string;
436
+ /** Code-candidate sidecar diagnostics */
437
+ artificer: NocturnalArtificerDiagnostics;
438
+ }
439
+
440
+ export interface NocturnalArtificerDiagnostics {
441
+ status: 'skipped' | 'validation_failed' | 'persisted_candidate';
442
+ reason?:
443
+ | 'behavioral_artifact_unavailable'
444
+ | 'no_deterministic_rule'
445
+ | 'persistence_failed'
446
+ | 'cancelled';
447
+ validationErrors?: string[];
448
+ persistedPath?: string;
449
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Shadow Observation Fingerprint Utilities
3
+ *
4
+ * Computes fingerprints for shadow task routing and tracking.
5
+ */
6
+
7
+ import * as crypto from 'crypto';
8
+ import type { PluginHookSubagentSpawningEvent } from '../openclaw-sdk.js';
9
+
10
+ /**
11
+ * PD local worker profiles that are managed by the shadow routing policy.
12
+ */
13
+ const _pdLocalProfiles = new Set(['local-reader', 'local-editor']);
14
+ export const PD_LOCAL_PROFILES: ReadonlySet<string> = _pdLocalProfiles;
15
+
16
+ /**
17
+ * Check if a profile is a local PD profile.
18
+ */
19
+ export function isLocalProfile(profile: string): boolean {
20
+ return _pdLocalProfiles.has(profile);
21
+ }
22
+
23
+ const RUNTIME_SHADOW_FINGERPRINT_HEX_LENGTH = 16;
24
+
25
+ /**
26
+ * Compute a fingerprint for runtime shadow task tracking.
27
+ * Used to correlate shadow routing decisions with subagent lifecycle events.
28
+ */
29
+ export function computeRuntimeShadowTaskFingerprint(
30
+ event: PluginHookSubagentSpawningEvent,
31
+ ): string {
32
+ const payload = {
33
+ childSessionKey: event.childSessionKey,
34
+ agentId: event.agentId,
35
+ label: event.label ?? '',
36
+ mode: event.mode,
37
+ threadRequested: event.threadRequested,
38
+ requesterChannel: event.requester?.channel ?? '',
39
+ requesterThreadId: event.requester?.threadId ?? '',
40
+ };
41
+ return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, RUNTIME_SHADOW_FINGERPRINT_HEX_LENGTH);
42
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Workspace Directory Resolution Utilities
3
+ *
4
+ * Shared helpers for resolving workspace directories across commands and hooks.
5
+ */
6
+
7
+ import type { OpenClawPluginApi } from '../openclaw-sdk.js';
8
+ import { validateWorkspaceDir, type WorkspaceResolutionContext } from '../core/workspace-dir-validation.js';
9
+ import { resolveWorkspaceDir } from '../core/workspace-dir-service.js';
10
+ import { resolveWorkspaceDirFromApi } from '../core/path-resolver.js';
11
+
12
+ /**
13
+ * Resolve workspace directory for command execution.
14
+ *
15
+ * Chain: ctx.workspaceDir → resolveWorkspaceDirFromApi (official OpenClaw API + env vars)
16
+ *
17
+ * CRITICAL: Throws if workspaceDir cannot be resolved. Silent failures are dangerous
18
+ * because commands might operate on the wrong directory.
19
+ */
20
+ export function resolveCommandWorkspaceDir(
21
+ api: OpenClawPluginApi,
22
+ ctx: { workspaceDir?: string },
23
+ ): string {
24
+ // 1. Direct from command context (most reliable — set by OpenClaw for current session)
25
+ if (ctx.workspaceDir) {
26
+ const issue = validateWorkspaceDir(ctx.workspaceDir);
27
+ if (!issue) return ctx.workspaceDir;
28
+ api.logger.error(`[PD:Command] ctx.workspaceDir="${ctx.workspaceDir}" is invalid: ${issue}`);
29
+ }
30
+
31
+ // 2. Official OpenClaw API → env vars → config file
32
+ const resolved = resolveWorkspaceDirFromApi(api);
33
+ if (resolved) return resolved;
34
+
35
+ // CRITICAL FAILURE: Cannot determine workspace directory
36
+ const errorMsg = `[PD:Command] CRITICAL: Cannot resolve workspace directory. ` +
37
+ `ctx.workspaceDir="${ctx.workspaceDir}" is invalid, and all fallbacks failed. ` +
38
+ `Commands will NOT execute to prevent data corruption.`;
39
+ api.logger.error(errorMsg);
40
+
41
+ throw new Error(errorMsg);
42
+ }
43
+
44
+ /**
45
+ * Resolve workspace directory for tool hook execution (safe version).
46
+ * Returns undefined instead of throwing if resolution fails.
47
+ */
48
+ export function resolveToolHookWorkspaceDirSafe(
49
+ ctx: WorkspaceResolutionContext,
50
+ api: OpenClawPluginApi,
51
+ source: string,
52
+ ): string | undefined {
53
+ return resolveWorkspaceDir(api, ctx, { source });
54
+ }