principles-disciple 1.58.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.
Files changed (46) hide show
  1. package/openclaw.plugin.json +4 -4
  2. package/package.json +1 -1
  3. package/src/commands/archive-impl.ts +2 -1
  4. package/src/commands/capabilities.ts +2 -1
  5. package/src/commands/context.ts +3 -5
  6. package/src/commands/disable-impl.ts +2 -1
  7. package/src/commands/evolution-status.ts +18 -1
  8. package/src/commands/export.ts +2 -1
  9. package/src/commands/focus.ts +2 -5
  10. package/src/commands/nocturnal-review.ts +2 -1
  11. package/src/commands/nocturnal-rollout.ts +2 -1
  12. package/src/commands/nocturnal-train.ts +2 -1
  13. package/src/commands/pain.ts +2 -1
  14. package/src/commands/pd-reflect.ts +5 -7
  15. package/src/commands/principle-rollback.ts +2 -1
  16. package/src/commands/promote-impl.ts +2 -1
  17. package/src/commands/rollback-impl.ts +2 -1
  18. package/src/commands/rollback.ts +2 -1
  19. package/src/commands/samples.ts +2 -1
  20. package/src/commands/strategy.ts +3 -2
  21. package/src/commands/thinking-os.ts +2 -1
  22. package/src/commands/workflow-debug.ts +2 -1
  23. package/src/core/event-log.ts +42 -3
  24. package/src/core/init.ts +2 -2
  25. package/src/core/principle-compiler/ledger-registrar.ts +11 -2
  26. package/src/core/rule-host-types.ts +4 -0
  27. package/src/core/rule-host.ts +7 -1
  28. package/src/hooks/gate.ts +15 -0
  29. package/src/hooks/prompt.ts +13 -0
  30. package/src/index.ts +13 -4
  31. package/src/service/evolution-worker.ts +30 -0
  32. package/src/service/runtime-summary-service.ts +38 -0
  33. package/src/tools/critique-prompt.ts +4 -5
  34. package/src/tools/deep-reflect.ts +4 -3
  35. package/src/types/event-types.ts +73 -3
  36. package/src/utils/workspace-resolver.ts +44 -3
  37. package/tests/commands/pd-reflect.test.ts +1 -1
  38. package/tests/core/bootstrap-rules.test.ts +14 -0
  39. package/tests/core/evolution-reducer.compilation-retry.test.ts +2 -1
  40. package/tests/core/ledger-registrar.test.ts +5 -2
  41. package/tests/core/principle-compiler.test.ts +4 -2
  42. package/tests/core/regression-v1-9-1.test.ts +2 -1
  43. package/tests/integration/gate-real-io.e2e.test.ts +5 -8
  44. package/tests/integration/pain-id-chain-e2e.test.ts +12 -6
  45. package/tests/integration/principle-compiler-e2e.test.ts +28 -9
  46. package/tests/integration/principle-lifecycle.e2e.test.ts +2 -1
@@ -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.58.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": "70500e1475ef",
80
- "bundleMd5": "607cfbcb4534d2cfdc45cb0cd019cd0b",
81
- "builtAt": "2026-04-16T03:41:17.317Z"
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.58.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)) {
@@ -74,6 +75,14 @@ function buildEnglishOutput(
74
75
  `- Phase 3: ready ${summary.phase3.phase3ShadowEligible ? 'yes' : 'no'}, queueTruthReady ${summary.phase3.queueTruthReady ? 'yes' : 'no'}, eligible ${summary.phase3.evolutionEligible}, reference_only ${summary.phase3.evolutionReferenceOnly}, rejected ${summary.phase3.evolutionRejected}${summary.phase3.evolutionReferenceOnlyReasons.length > 0 ? ` (reference ${summary.phase3.evolutionReferenceOnlyReasons.slice(0, 2).join(', ')})` : ''}${summary.phase3.evolutionRejectedReasons.length > 0 ? ` (${summary.phase3.evolutionRejectedReasons.slice(0, 3).join(', ')})` : ''}`,
75
76
  `- Phase 3 Legacy Directive File: ${summary.phase3.directiveStatus} (${summary.phase3.directiveIgnoredReason})`,
76
77
  '',
78
+ // D: Heartbeat Diagnostician chain — separated from evolution/nocturnal
79
+ 'Heartbeat Diagnostician (Pain → Principle)',
80
+ `- Pending tasks: ${summary.heartbeatDiagnosis.pendingTasks}`,
81
+ `- Tasks written today: ${summary.heartbeatDiagnosis.tasksWrittenToday}`,
82
+ `- Reports written today: ${summary.heartbeatDiagnosis.reportsWrittenToday}`,
83
+ `- Candidates created today: ${summary.heartbeatDiagnosis.candidatesCreatedToday}`,
84
+ `- Heartbeats injected today: ${summary.heartbeatDiagnosis.heartbeatsInjectedToday}`,
85
+ '',
77
86
  'Principles',
78
87
  `- candidate principles: ${stats.candidateCount}`,
79
88
  `- probation principles: ${stats.probationCount}`,
@@ -127,6 +136,14 @@ function buildChineseOutput(
127
136
  `- Phase 3: ready ${summary.phase3.phase3ShadowEligible ? 'yes' : 'no'},queueTruthReady ${summary.phase3.queueTruthReady ? 'yes' : 'no'},eligible ${summary.phase3.evolutionEligible},reference_only ${summary.phase3.evolutionReferenceOnly},rejected ${summary.phase3.evolutionRejected}${summary.phase3.evolutionReferenceOnlyReasons.length > 0 ? ` (reference ${summary.phase3.evolutionReferenceOnlyReasons.slice(0, 2).join(', ')})` : ''}${summary.phase3.evolutionRejectedReasons.length > 0 ? ` (${summary.phase3.evolutionRejectedReasons.slice(0, 3).join(', ')})` : ''}`,
128
137
  `- Phase 3 Legacy Directive File: ${summary.phase3.directiveStatus} (${summary.phase3.directiveIgnoredReason})`,
129
138
  '',
139
+ // D: Heartbeat Diagnostician chain — separated from evolution/nocturnal
140
+ '心跳诊断链路(Pain → 原则)',
141
+ `- 等待处理: ${summary.heartbeatDiagnosis.pendingTasks}`,
142
+ `- 今日写入任务: ${summary.heartbeatDiagnosis.tasksWrittenToday}`,
143
+ `- 今日写入报告: ${summary.heartbeatDiagnosis.reportsWrittenToday}`,
144
+ `- 今日创建候选: ${summary.heartbeatDiagnosis.candidatesCreatedToday}`,
145
+ `- 今日心跳注入: ${summary.heartbeatDiagnosis.heartbeatsInjectedToday}`,
146
+ '',
130
147
  '原则统计',
131
148
  `- 候选原则: ${stats.candidateCount}`,
132
149
  `- 观察期原则: ${stats.probationCount}`,
@@ -152,7 +169,7 @@ function buildChineseOutput(
152
169
  }
153
170
 
154
171
  export function handleEvolutionStatusCommand(ctx: PluginCommandContext): { text: string } {
155
- const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
172
+ const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'evolution-status');
156
173
  const sessionId = (ctx as { sessionId?: string | null }).sessionId ?? null;
157
174
  // #207/#210: Use WorkspaceContext to get evolutionReducer with stateDir
158
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() || '';
@@ -18,6 +18,12 @@ import type {
18
18
  EvolutionTaskEventData,
19
19
  DeepReflectionEventData,
20
20
  EmpathyRollbackEventData,
21
+ // C: New event data types
22
+ DiagnosisTaskEventData,
23
+ HeartbeatDiagnosisEventData,
24
+ DiagnosticianReportEventData,
25
+ PrincipleCandidateEventData,
26
+ RuleEnforcedEventData,
21
27
  } from '../types/event-types.js';
22
28
  import { createEmptyDailyStats } from '../types/event-types.js';
23
29
  import { atomicWriteFileSync } from '../utils/io.js';
@@ -180,9 +186,28 @@ export class EventLog {
180
186
  recordWarn(sessionId: string | undefined, message: string, context?: Record<string, unknown>): void {
181
187
  this.record('warn', 'failure', sessionId, { message, ...context });
182
188
  }
183
-
184
-
185
-
189
+
190
+ // C: Diagnostician heartbeat chain event recorders
191
+ recordDiagnosisTask(data: DiagnosisTaskEventData): void {
192
+ this.record('diagnosis_task', 'written', undefined, data);
193
+ }
194
+
195
+ recordHeartbeatDiagnosis(data: HeartbeatDiagnosisEventData): void {
196
+ this.record('heartbeat_diagnosis', 'injected', undefined, data);
197
+ }
198
+
199
+ recordDiagnosticianReport(data: DiagnosticianReportEventData): void {
200
+ this.record('diagnostician_report', data.success ? 'completed' : 'failure', undefined, data);
201
+ }
202
+
203
+ recordPrincipleCandidate(data: PrincipleCandidateEventData): void {
204
+ this.record('principle_candidate', 'created', undefined, data);
205
+ }
206
+
207
+ recordRuleEnforced(data: RuleEnforcedEventData): void {
208
+ this.record('rule_enforced', 'matched', undefined, data);
209
+ }
210
+
186
211
  private record(
187
212
  type: EventType,
188
213
  category: EventCategory,
@@ -325,6 +350,20 @@ export class EventLog {
325
350
  stats.evolution.tasksEnqueued++;
326
351
  }
327
352
  }
353
+ // C: Diagnostician heartbeat chain event counters
354
+ else if (entry.type === 'diagnosis_task') {
355
+ stats.evolution.diagnosisTasksWritten++;
356
+ } else if (entry.type === 'heartbeat_diagnosis') {
357
+ stats.evolution.heartbeatsInjected++;
358
+ } else if (entry.type === 'diagnostician_report') {
359
+ if (entry.category === 'completed') {
360
+ stats.evolution.diagnosticianReportsWritten++;
361
+ }
362
+ } else if (entry.type === 'principle_candidate') {
363
+ stats.evolution.principleCandidatesCreated++;
364
+ } else if (entry.type === 'rule_enforced') {
365
+ stats.evolution.rulesEnforced++;
366
+ }
328
367
  }
329
368
 
330
369
  private startFlushTimer(): void {
package/src/core/init.ts CHANGED
@@ -192,7 +192,7 @@ export function ensureCorePrinciples(stateDir: string, logger: PluginLogger): bo
192
192
  for (const model of CORE_THINKING_MODELS) {
193
193
  const state: PrincipleTrainingState = {
194
194
  principleId: model.id,
195
- evaluability: 'manual_only',
195
+ evaluability: 'deterministic',
196
196
  applicableOpportunityCount: 0,
197
197
  observedViolationCount: 0,
198
198
  complianceRate: 0,
@@ -217,7 +217,7 @@ export function ensureCorePrinciples(stateDir: string, logger: PluginLogger): bo
217
217
  status: 'active',
218
218
  priority: 'P1',
219
219
  scope: 'general',
220
- evaluability: 'manual_only',
220
+ evaluability: 'deterministic',
221
221
  valueScore: 0,
222
222
  adherenceRate: 0,
223
223
  painPreventedCount: 0,
@@ -52,6 +52,11 @@ export function registerCompiledRule(stateDir: string, input: RegisterInput): Re
52
52
  const now = new Date().toISOString();
53
53
 
54
54
  // Step 1: Create the rule
55
+ // FIX: Auto-generated rules default to 'warn' enforcement (not 'block') until:
56
+ // - replay evaluation passes
57
+ // - coverage confirmation
58
+ // - human approval
59
+ // This prevents P_001-style false positives from blocking normal edits.
55
60
  const rule: LedgerRule = {
56
61
  id: ruleId,
57
62
  version: 1,
@@ -59,7 +64,7 @@ export function registerCompiledRule(stateDir: string, input: RegisterInput): Re
59
64
  description: `Automatically compiled gate rule generated from principle ${principleId}`,
60
65
  type: 'gate',
61
66
  triggerCondition: coversCondition,
62
- enforcement: 'block',
67
+ enforcement: 'warn',
63
68
  action: codeContent,
64
69
  principleId,
65
70
  status: 'proposed',
@@ -82,7 +87,11 @@ export function registerCompiledRule(stateDir: string, input: RegisterInput): Re
82
87
  version: '1',
83
88
  coversCondition,
84
89
  coveragePercentage: 100,
85
- lifecycleState: 'active' as const,
90
+ // FIX: Start as 'candidate' instead of 'active'.
91
+ // RuleHost only loads lifecycleState='active' implementations.
92
+ // This means auto-generated rules will NOT block until explicitly
93
+ // promoted to 'active' after replay evaluation + human approval.
94
+ lifecycleState: 'candidate' as const,
86
95
  createdAt: now,
87
96
  updatedAt: now,
88
97
  };
@@ -66,6 +66,10 @@ export interface RuleHostResult {
66
66
  matched: boolean;
67
67
  reason: string;
68
68
  diagnostics?: Record<string, unknown>;
69
+ /** C: Rule ID that produced this result (for observability events) */
70
+ ruleId?: string;
71
+ /** C: Principle ID that this rule implements (for observability events) */
72
+ principleId?: string;
69
73
  }
70
74
 
71
75
  // ---------------------------------------------------------------------------
@@ -234,7 +234,13 @@ export class RuleHost {
234
234
  meta,
235
235
  evaluate: (input: RuleHostInput): RuleHostResult => {
236
236
  const frozenHelpers = createRuleHostHelpers(input);
237
- return rawEvaluate(input, frozenHelpers);
237
+ const result = rawEvaluate(input, frozenHelpers);
238
+ // C: Enrich result with rule/principle IDs for observability
239
+ if (result.matched && (result.decision === 'block' || result.decision === 'requireApproval')) {
240
+ result.ruleId = impl.ruleId;
241
+ result.principleId = meta.ruleId ?? impl.ruleId;
242
+ }
243
+ return result;
238
244
  },
239
245
  };
240
246
  } catch (compileError: unknown) {
package/src/hooks/gate.ts CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  } from '../constants/tools.js';
40
40
  import { getSession, hasRecentThinking } from '../core/session-tracker.js';
41
41
  import { getEvolutionEngine } from '../core/evolution-engine.js';
42
+ import { EventLogService } from '../core/event-log.js';
42
43
 
43
44
  export function handleBeforeToolCall(
44
45
  event: PluginHookBeforeToolCallEvent,
@@ -205,6 +206,20 @@ export function handleBeforeToolCall(
205
206
 
206
207
  const hostResult = ruleHost.evaluate(hostInput);
207
208
  if (hostResult?.decision === 'block' || hostResult?.decision === 'requireApproval') {
209
+ // C: Record rule_enforced event for matched rules
210
+ try {
211
+ const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
212
+ eventLog.recordRuleEnforced({
213
+ ruleId: hostResult.ruleId || 'unknown',
214
+ principleId: hostResult.principleId || 'unknown',
215
+ enforcement: hostResult.decision === 'requireApproval' ? 'requireApproval' : 'block',
216
+ toolName: event.toolName,
217
+ filePath: relPath,
218
+ });
219
+ } catch (evErr) {
220
+ logger?.warn?.(`[PD_GATE] Failed to record rule_enforced event: ${String(evErr)}`);
221
+ }
222
+
208
223
  const reason = hostResult.decision === 'requireApproval'
209
224
  ? `[Rule Host] Approval required: ${hostResult.reason}`
210
225
  : hostResult.reason;
@@ -23,6 +23,7 @@ import {
23
23
  } from '../core/empathy-keyword-matcher.js';
24
24
  import { severityToPenalty, DEFAULT_EMPATHY_KEYWORD_CONFIG } from '../core/empathy-types.js';
25
25
  import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
26
+ import { EventLogService } from '../core/event-log.js';
26
27
  import type { PluginRuntimeSubagent } from '../service/subagent-workflow/runtime-direct-driver.js';
27
28
 
28
29
  /**
@@ -755,6 +756,18 @@ ${taskBlocks}${processingNote}
755
756
  </diagnostician_tasks>\n`;
756
757
 
757
758
  logger?.info?.(`[PD:Prompt] Injected ${Math.min(pendingCount, 3)}/${pendingCount} pending diagnostician task(s) into heartbeat prompt`);
759
+
760
+ // C: Record heartbeat_diagnosis event for observability
761
+ try {
762
+ const eventLog = EventLogService.get(wctx.stateDir, logger);
763
+ eventLog.recordHeartbeatDiagnosis({
764
+ taskCount: pendingCount,
765
+ taskIds: pendingTasks.slice(0, 3).map(t => t.id),
766
+ trigger: 'heartbeat',
767
+ });
768
+ } catch (evErr) {
769
+ logger?.warn?.(`[PD:Prompt] Failed to record heartbeat_diagnosis event: ${String(evErr)}`);
770
+ }
758
771
  }
759
772
  } catch (e) {
760
773
  logger?.warn?.(`[PD:Prompt] Failed to read diagnostician tasks: ${String(e)}`);