principles-disciple 1.59.0 → 1.60.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.
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.59.0",
5
+ "version": "1.60.0",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
@@ -76,8 +76,8 @@
76
76
  }
77
77
  },
78
78
  "buildFingerprint": {
79
- "gitSha": "f37ee1dec538",
80
- "bundleMd5": "f578c460a9849e9b6e6c573d13e48f12",
81
- "builtAt": "2026-04-18T07:51:05.292Z"
79
+ "gitSha": "b300ef0761c5",
80
+ "bundleMd5": "3c16a5198f3559b097d028b6e987cf5b",
81
+ "builtAt": "2026-04-18T11:02:10.525Z"
82
82
  }
83
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.59.0",
3
+ "version": "1.60.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -19,6 +19,7 @@ import {
19
19
  } from '../core/principle-tree-ledger.js';
20
20
  import type { Implementation, ImplementationLifecycleState } from '../types/principle-tree-schema.js';
21
21
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
22
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
22
23
 
23
24
  /**
24
25
  * Get all implementations from the ledger.
@@ -43,7 +44,7 @@ function canArchive(state: ImplementationLifecycleState): boolean {
43
44
  * /pd-archive-impl list - List archivable implementations
44
45
  */
45
46
  export function handleArchiveImplCommand(ctx: PluginCommandContext): PluginCommandResult {
46
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
47
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'archive-impl');
47
48
  const {stateDir} = WorkspaceContext.fromHookContext({ ...ctx, workspaceDir });
48
49
  const lang = (ctx.config?.language as string) || 'en';
49
50
  const isZh = lang === 'zh';
@@ -4,6 +4,7 @@ import * as path from 'path';
4
4
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
5
5
  import { WorkspaceContext } from '../core/workspace-context.js';
6
6
  import { atomicWriteFileSync } from '../utils/io.js';
7
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
7
8
 
8
9
  const TOOLS_TO_SCAN = [
9
10
  { name: 'rg', cmd: ['rg', '--version'] },
@@ -50,7 +51,7 @@ function scanEnvironment(wctx: WorkspaceContext): any {
50
51
  }
51
52
 
52
53
  export function handleBootstrapTools(ctx: PluginCommandContext): PluginCommandResult {
53
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
54
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'capabilities');
54
55
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
55
56
 
56
57
  try {
@@ -2,6 +2,7 @@
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
5
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
5
6
  import { atomicWriteFileSync } from '../utils/io.js';
6
7
  import type { ContextInjectionConfig} from '../types.js';
7
8
  import { defaultContextConfig } from '../types.js';
@@ -11,11 +12,8 @@ import { loadContextInjectionConfig } from '../hooks/prompt.js';
11
12
  * Get workspace directory from context
12
13
  */
13
14
  function getWorkspaceDir(ctx: PluginCommandContext): string {
14
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
15
- if (!workspaceDir) {
16
- throw new Error('[PD:Context] workspaceDir is required but not provided');
17
- }
18
- return workspaceDir;
15
+ // resolvePluginCommandWorkspaceDir throws on failure never returns falsy
16
+ return resolvePluginCommandWorkspaceDir(ctx, 'context');
19
17
  }
20
18
 
21
19
  /**
@@ -21,6 +21,7 @@ import {
21
21
  } from '../core/principle-tree-ledger.js';
22
22
  import type { Implementation, ImplementationLifecycleState } from '../types/principle-tree-schema.js';
23
23
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
24
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
24
25
 
25
26
  /**
26
27
  * Get all implementations from the ledger.
@@ -133,7 +134,7 @@ function _handleDisableImpl(
133
134
  */
134
135
 
135
136
  export function handleDisableImplCommand(ctx: PluginCommandContext): PluginCommandResult {
136
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
137
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'disable-impl');
137
138
  const {stateDir} = WorkspaceContext.fromHookContext({ ...ctx, workspaceDir });
138
139
  const lang = (ctx.config?.language as string) || 'en';
139
140
  const isZh = lang === 'zh';
@@ -4,6 +4,7 @@ import { WorkspaceContext } from '../core/workspace-context.js';
4
4
  import { normalizeLanguage } from '../i18n/commands.js';
5
5
  import type { PluginCommandContext } from '../openclaw-sdk.js';
6
6
  import { RuntimeSummaryService } from '../service/runtime-summary-service.js';
7
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
7
8
 
8
9
  function formatNumber(value: number | null): string {
9
10
  if (value === null || Number.isNaN(value)) {
@@ -168,7 +169,7 @@ function buildChineseOutput(
168
169
  }
169
170
 
170
171
  export function handleEvolutionStatusCommand(ctx: PluginCommandContext): { text: string } {
171
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
172
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'evolution-status');
172
173
  const sessionId = (ctx as { sessionId?: string | null }).sessionId ?? null;
173
174
  // #207/#210: Use WorkspaceContext to get evolutionReducer with stateDir
174
175
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
@@ -1,5 +1,6 @@
1
1
  import { WorkspaceContext } from '../core/workspace-context.js';
2
2
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
3
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
3
4
  import { exportORPOSamples, listExports } from '../core/nocturnal-export.js';
4
5
 
5
6
  function isZh(ctx: PluginCommandContext): boolean {
@@ -7,7 +8,7 @@ function isZh(ctx: PluginCommandContext): boolean {
7
8
  }
8
9
 
9
10
  export function handleExportCommand(ctx: PluginCommandContext): PluginCommandResult {
10
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
11
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'export');
11
12
  const zh = isZh(ctx);
12
13
  const args = (ctx.args || '').trim();
13
14
  const parts = args.split(/\s+/).filter(Boolean);
@@ -13,6 +13,7 @@ import * as path from 'path';
13
13
  import type { PluginCommandContext, PluginCommandResult, OpenClawPluginApi } from '../openclaw-sdk.js';
14
14
  import { WorkspaceContext } from '../core/workspace-context.js';
15
15
  import { atomicWriteFileSync } from '../utils/io.js';
16
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
16
17
  import {
17
18
  getHistoryDir,
18
19
  backupToHistory,
@@ -28,11 +29,7 @@ import {
28
29
  * 获取工作区目录
29
30
  */
30
31
  function getWorkspaceDir(ctx: PluginCommandContext): string {
31
- const workspaceDir = ctx.config?.workspaceDir as string | undefined;
32
- if (!workspaceDir) {
33
- throw new Error('[PD:Focus] workspaceDir is required but not provided');
34
- }
35
- return workspaceDir;
32
+ return resolvePluginCommandWorkspaceDir(ctx, 'focus');
36
33
  }
37
34
 
38
35
  /**
@@ -34,6 +34,7 @@ import {
34
34
  } from '../core/nocturnal-dataset.js';
35
35
  import { getPrincipleState, setPrincipleState } from '../core/principle-training-state.js';
36
36
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
37
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
37
38
 
38
39
  function isZh(ctx: PluginCommandContext): boolean {
39
40
  return String(ctx.config?.language || 'en').startsWith('zh');
@@ -91,7 +92,7 @@ function statusLabel(status: NocturnalReviewStatus, zh: boolean): string {
91
92
  }
92
93
 
93
94
  export function handleNocturnalReviewCommand(ctx: PluginCommandContext): PluginCommandResult {
94
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
95
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'nocturnal-review');
95
96
  const zh = isZh(ctx);
96
97
  const args = (ctx.args || '').trim();
97
98
  const parts = args.split(/\s+/).filter(Boolean);
@@ -25,6 +25,7 @@
25
25
  */
26
26
 
27
27
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
28
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
28
29
  import {
29
30
  evaluatePromotionGate,
30
31
  advancePromotion,
@@ -99,7 +100,7 @@ function formatConstraintCheck(
99
100
  }
100
101
 
101
102
  export function handleNocturnalRolloutCommand(ctx: PluginCommandContext): PluginCommandResult {
102
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
103
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'nocturnal-rollout');
103
104
  const zh = isZh(ctx);
104
105
  const args = (ctx.args || '').trim();
105
106
  const parts = args.split(/\s+/).filter(Boolean);
@@ -28,6 +28,7 @@ import { execFileSync, spawn } from 'child_process';
28
28
  import { fileURLToPath } from 'url';
29
29
  import { atomicWriteFileSync } from '../utils/io.js';
30
30
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
31
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
31
32
  import {
32
33
  type TrainerBackendKind,
33
34
  type HardwareTier,
@@ -100,7 +101,7 @@ function formatTrainingRun(run: ReturnType<typeof getTrainingRun>, zh: boolean):
100
101
  }
101
102
 
102
103
  export async function handleNocturnalTrainCommand(ctx: PluginCommandContext): Promise<PluginCommandResult> {
103
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
104
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'nocturnal-train');
104
105
  const zh = isZh(ctx);
105
106
  const args = (ctx.args || '').trim();
106
107
  const parts = args.split(/\s+/).filter(Boolean);
@@ -1,6 +1,7 @@
1
1
  import { resetFriction, getSession } from '../core/session-tracker.js';
2
2
  import { WorkspaceContext } from '../core/workspace-context.js';
3
3
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
4
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
4
5
  import type { EmpathyEventStats } from '../types/event-types.js';
5
6
 
6
7
  /**
@@ -92,7 +93,7 @@ interface SessionAwareCommandContext extends PluginCommandContext {
92
93
  * Handles the /pd-status command
93
94
  */
94
95
  export function handlePainCommand(ctx: PluginCommandContext): PluginCommandResult {
95
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
96
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'pain');
96
97
 
97
98
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
98
99
  const lang = (ctx.config?.language as string) || 'en';
@@ -8,6 +8,7 @@
8
8
  import type { PluginCommandDefinition, PluginCommandContext, PluginCommandResult, OpenClawPluginApi } from '../openclaw-sdk.js';
9
9
  import { acquireQueueLock, EVOLUTION_QUEUE_LOCK_SUFFIX } from '../service/evolution-worker.js';
10
10
  import { atomicWriteFileSync } from '../utils/io.js';
11
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
11
12
  import * as fs from 'fs';
12
13
  import * as path from 'path';
13
14
 
@@ -23,11 +24,7 @@ export const handlePdReflect: PluginCommandDefinition = {
23
24
  requireAuth: false,
24
25
  handler: async (ctx: PdReflectContext): Promise<PluginCommandResult> => {
25
26
  try {
26
-
27
- const workspaceDir = ctx.workspaceDir;
28
- if (!workspaceDir) {
29
- return { text: 'Cannot determine workspace directory. Ensure you are in an active workspace.', isError: true };
30
- }
27
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'pd-reflect');
31
28
 
32
29
  const stateDir = path.join(workspaceDir, '.state');
33
30
  const queuePath = path.join(stateDir, 'evolution_queue.json');
@@ -80,9 +77,10 @@ export const handlePdReflect: PluginCommandDefinition = {
80
77
  return {
81
78
  text: `Nocturnal reflection task enqueued: \`${taskId}\`\n\nIt will be processed in the next evolution worker cycle (~15s). Check .state/nocturnal/samples/ for results.`,
82
79
  };
83
- } catch (error) {
80
+ } catch (err: unknown) {
81
+ const message = err instanceof Error ? err.message : String(err);
84
82
  return {
85
- text: `Failed to trigger reflection: ${String(error)}`,
83
+ text: `Failed to trigger reflection: ${message}`,
86
84
  isError: true,
87
85
  };
88
86
  }
@@ -1,9 +1,10 @@
1
1
  import { WorkspaceContext } from '../core/workspace-context.js';
2
2
  import type { PluginCommandContext } from '../openclaw-sdk.js';
3
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
3
4
 
4
5
 
5
6
  export function handlePrincipleRollbackCommand(ctx: PluginCommandContext): { text: string } {
6
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
7
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'principle-rollback');
7
8
  const argText = (ctx.args || '').trim();
8
9
  const [principleId = '', ...reasonParts] = argText.split(/\s+/);
9
10
  const reason = (reasonParts.join(' ') || 'manual rollback').trim();
@@ -28,6 +28,7 @@ import {
28
28
  } from '../core/principle-tree-ledger.js';
29
29
  import { WorkspaceContext } from '../core/workspace-context.js';
30
30
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
31
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
31
32
  import type { Implementation, ImplementationLifecycleState } from '../types/principle-tree-schema.js';
32
33
  import { withLock } from '../utils/file-lock.js';
33
34
  import { atomicWriteFileSync } from '../utils/io.js';
@@ -250,7 +251,7 @@ function _handlePromoteImpl(options: PromoteImplOptions): PluginCommandResult {
250
251
  }
251
252
 
252
253
  export function handlePromoteImplCommand(ctx: PluginCommandContext): PluginCommandResult {
253
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
254
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'promote-impl');
254
255
  const {stateDir} = WorkspaceContext.fromHookContext({ ...ctx, workspaceDir });
255
256
  const lang = (ctx.config?.language as string) || 'en';
256
257
  const isZh = lang === 'zh';
@@ -25,6 +25,7 @@ import {
25
25
  } from '../core/principle-tree-ledger.js';
26
26
  import type { Implementation } from '../types/principle-tree-schema.js';
27
27
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
28
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
28
29
 
29
30
  /**
30
31
  * Get all implementations from the ledger.
@@ -44,7 +45,7 @@ function getAllImplementations(stateDir: string): Implementation[] {
44
45
  */
45
46
 
46
47
  export function handleRollbackImplCommand(ctx: PluginCommandContext): PluginCommandResult {
47
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
48
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'rollback-impl');
48
49
  const {stateDir} = WorkspaceContext.fromHookContext({ ...ctx, workspaceDir });
49
50
  const lang = (ctx.config?.language as string) || 'en';
50
51
  const isZh = lang === 'zh';
@@ -1,6 +1,7 @@
1
1
  import { WorkspaceContext } from '../core/workspace-context.js';
2
2
  import { resetFriction } from '../core/session-tracker.js';
3
3
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
4
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
4
5
 
5
6
  /**
6
7
  * Extended context interface that includes sessionId injected by the plugin framework.
@@ -18,7 +19,7 @@ interface SessionAwareCommandContext extends PluginCommandContext {
18
19
  * /pd-rollback last - Rollback the last empathy event in current session
19
20
  */
20
21
  export function handleRollbackCommand(ctx: PluginCommandContext): PluginCommandResult {
21
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
22
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'rollback');
22
23
 
23
24
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
24
25
  const lang = (ctx.config?.language as string) || 'en';
@@ -1,12 +1,13 @@
1
1
  import { WorkspaceContext } from '../core/workspace-context.js';
2
2
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
3
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
3
4
 
4
5
  function isZh(ctx: PluginCommandContext): boolean {
5
6
  return String(ctx.config?.language || 'en').startsWith('zh');
6
7
  }
7
8
 
8
9
  export function handleSamplesCommand(ctx: PluginCommandContext): PluginCommandResult {
9
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
10
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'samples');
10
11
  const zh = isZh(ctx);
11
12
  const args = (ctx.args || '').trim();
12
13
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
@@ -1,8 +1,9 @@
1
1
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
2
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
2
3
  import { WorkspaceContext } from '../core/workspace-context.js';
3
4
 
4
5
  export function handleInitStrategy(ctx: PluginCommandContext): PluginCommandResult {
5
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
6
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'strategy');
6
7
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
7
8
 
8
9
  const okrDir = wctx.resolve('OKR_DIR').replace(workspaceDir, '').replace(/^\/+/, '');
@@ -20,7 +21,7 @@ export function handleInitStrategy(ctx: PluginCommandContext): PluginCommandResu
20
21
  }
21
22
 
22
23
  export function handleManageOkr(ctx: PluginCommandContext): PluginCommandResult {
23
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
24
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'strategy:manageOkr');
24
25
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
25
26
 
26
27
  const focusPath = wctx.resolve('CURRENT_FOCUS').replace(workspaceDir, '').replace(/^\/+/, '');
@@ -1,10 +1,11 @@
1
1
 
2
2
  import * as fs from 'fs';
3
3
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
4
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
4
5
  import { WorkspaceContext } from '../core/workspace-context.js';
5
6
 
6
7
  function getWorkspaceDir(ctx: PluginCommandContext): string {
7
- return (ctx.config?.workspaceDir as string) || process.cwd();
8
+ return resolvePluginCommandWorkspaceDir(ctx, 'thinking-os');
8
9
  }
9
10
 
10
11
  function getModels(wctx: WorkspaceContext): Record<string, string> {
@@ -1,5 +1,6 @@
1
1
  import { WorkflowStore } from '../service/subagent-workflow/workflow-store.js';
2
2
  import type { PluginCommandContext } from '../openclaw-sdk.js';
3
+ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js';
3
4
 
4
5
  function formatTimestamp(ts: number | null | undefined): string {
5
6
  if (!ts) return '--';
@@ -85,7 +86,7 @@ function buildOutput(
85
86
  export function handleWorkflowDebugCommand(
86
87
  ctx: PluginCommandContext & { args?: string }
87
88
  ): { text: string } {
88
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
89
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'workflow-debug');
89
90
 
90
91
  // Parse workflow ID from args
91
92
  const args = (ctx as { args?: string }).args?.trim() || '';
@@ -4,7 +4,7 @@
4
4
  * Shared helpers for resolving workspace directories across commands and hooks.
5
5
  */
6
6
 
7
- import type { OpenClawPluginApi } from '../openclaw-sdk.js';
7
+ import type { OpenClawPluginApi, PluginCommandContext } from '../openclaw-sdk.js';
8
8
  import { validateWorkspaceDir, type WorkspaceResolutionContext } from '../core/workspace-dir-validation.js';
9
9
  import { resolveWorkspaceDir } from '../core/workspace-dir-service.js';
10
10
  import { resolveWorkspaceDirFromApi } from '../core/path-resolver.js';
@@ -25,7 +25,10 @@ export function resolveCommandWorkspaceDir(
25
25
  if (ctx.workspaceDir) {
26
26
  const issue = validateWorkspaceDir(ctx.workspaceDir);
27
27
  if (!issue) return ctx.workspaceDir;
28
- api.logger.error(`[PD:Command] ctx.workspaceDir="${ctx.workspaceDir}" is invalid: ${issue}`);
28
+ // Validation failed fail immediately, do not silently fall back
29
+ const errorMsg = `[PD:Command] ctx.workspaceDir="${ctx.workspaceDir}" is invalid: ${issue}`;
30
+ api.logger.error(errorMsg);
31
+ throw new Error(errorMsg);
29
32
  }
30
33
 
31
34
  // 2. Official OpenClaw API → env vars → config file
@@ -34,13 +37,51 @@ export function resolveCommandWorkspaceDir(
34
37
 
35
38
  // CRITICAL FAILURE: Cannot determine workspace directory
36
39
  const errorMsg = `[PD:Command] CRITICAL: Cannot resolve workspace directory. ` +
37
- `ctx.workspaceDir="${ctx.workspaceDir}" is invalid, and all fallbacks failed. ` +
40
+ `ctx.workspaceDir="${ctx.workspaceDir ?? ''}" is invalid, and all fallbacks failed. ` +
38
41
  `Commands will NOT execute to prevent data corruption.`;
39
42
  api.logger.error(errorMsg);
40
43
 
41
44
  throw new Error(errorMsg);
42
45
  }
43
46
 
47
+ /**
48
+ * Resolve workspace directory for plugin command execution.
49
+ *
50
+ * Chain: ctx.workspaceDir (canonical) → ctx.config.workspaceDir (dispatcher fallback)
51
+ *
52
+ * CRITICAL: Throws if workspaceDir cannot be resolved. Commands must NEVER silently
53
+ * fall back to process.cwd() as this masks configuration errors and can corrupt
54
+ * the wrong workspace.
55
+ *
56
+ * @param ctx - Plugin command context (has workspaceDir + config properties)
57
+ * @param source - Source label for error messages (e.g. 'evolution-status', 'pain')
58
+ */
59
+ export function resolvePluginCommandWorkspaceDir(
60
+ ctx: PluginCommandContext,
61
+ source: string,
62
+ ): string {
63
+ // 1. Canonical workspaceDir field (set by OpenClaw command dispatcher)
64
+ if (ctx.workspaceDir) {
65
+ const issue = validateWorkspaceDir(ctx.workspaceDir);
66
+ if (!issue) return ctx.workspaceDir;
67
+ throw new Error(`[PD:Command:${source}] ctx.workspaceDir="${ctx.workspaceDir}" is invalid: ${issue}`);
68
+ }
69
+
70
+ // 2. Dispatcher may also put workspaceDir in config (legacy/alternative path)
71
+ const configWorkspaceDir = ctx.config?.workspaceDir as string | undefined;
72
+ if (configWorkspaceDir) {
73
+ const issue = validateWorkspaceDir(configWorkspaceDir);
74
+ if (!issue) return configWorkspaceDir;
75
+ throw new Error(`[PD:Command:${source}] ctx.config.workspaceDir="${configWorkspaceDir}" is invalid: ${issue}`);
76
+ }
77
+
78
+ // CRITICAL FAILURE: No workspace directory available
79
+ throw new Error(
80
+ `[PD:Command:${source}] CRITICAL: workspaceDir is not set in ctx.workspaceDir or ctx.config.workspaceDir. ` +
81
+ `Commands cannot execute without a valid workspace. Set OPENCLAW_WORKSPACE_DIR env var or ensure the workspace is properly initialized.`,
82
+ );
83
+ }
84
+
44
85
  /**
45
86
  * Resolve workspace directory for tool hook execution (safe version).
46
87
  * Returns undefined instead of throwing if resolution fails.
@@ -20,7 +20,7 @@ describe('pd-reflect command', () => {
20
20
  it('requires an explicit resolved workspace directory', async () => {
21
21
  const result = await handlePdReflect.handler({} as any);
22
22
  expect(result.isError).toBe(true);
23
- expect(result.text).toContain('Cannot determine workspace directory');
23
+ expect(result.text).toContain('workspaceDir is not set');
24
24
  });
25
25
 
26
26
  it('enqueues into the provided active workspace', async () => {