principles-disciple 1.59.0 → 1.61.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.
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/commands/archive-impl.ts +2 -1
- package/src/commands/capabilities.ts +2 -1
- package/src/commands/context.ts +3 -5
- package/src/commands/disable-impl.ts +2 -1
- package/src/commands/evolution-status.ts +2 -1
- package/src/commands/export.ts +2 -1
- package/src/commands/focus.ts +2 -5
- package/src/commands/nocturnal-review.ts +2 -1
- package/src/commands/nocturnal-rollout.ts +2 -1
- package/src/commands/nocturnal-train.ts +2 -1
- package/src/commands/pain.ts +2 -1
- package/src/commands/pd-reflect.ts +5 -7
- package/src/commands/principle-rollback.ts +2 -1
- package/src/commands/promote-impl.ts +2 -1
- package/src/commands/rollback-impl.ts +2 -1
- package/src/commands/rollback.ts +2 -1
- package/src/commands/samples.ts +2 -1
- package/src/commands/strategy.ts +3 -2
- package/src/commands/thinking-os.ts +2 -1
- package/src/commands/workflow-debug.ts +2 -1
- package/src/core/diagnostician-task-store.ts +38 -1
- package/src/core/event-log.ts +21 -3
- package/src/service/evolution-worker.ts +89 -9
- package/src/service/runtime-summary-service.ts +8 -0
- package/src/types/event-types.ts +10 -1
- package/src/utils/workspace-resolver.ts +44 -3
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +98 -15
- package/tests/commands/pd-reflect.test.ts +1 -1
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.61.0",
|
|
6
6
|
"skills": [
|
|
7
7
|
"./skills"
|
|
8
8
|
],
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
"buildFingerprint": {
|
|
79
|
-
"gitSha": "
|
|
80
|
-
"bundleMd5": "
|
|
81
|
-
"builtAt": "2026-04-
|
|
79
|
+
"gitSha": "b300ef0761c5",
|
|
80
|
+
"bundleMd5": "3c16a5198f3559b097d028b6e987cf5b",
|
|
81
|
+
"builtAt": "2026-04-18T11:02:10.525Z"
|
|
82
82
|
}
|
|
83
83
|
}
|
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
54
|
+
const workspaceDir = resolvePluginCommandWorkspaceDir(ctx, 'capabilities');
|
|
54
55
|
const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
|
|
55
56
|
|
|
56
57
|
try {
|
package/src/commands/context.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
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
|
|
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
|
|
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 });
|
package/src/commands/export.ts
CHANGED
|
@@ -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
|
|
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);
|
package/src/commands/focus.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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);
|
package/src/commands/pain.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
80
|
+
} catch (err: unknown) {
|
|
81
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
84
82
|
return {
|
|
85
|
-
text: `Failed to trigger reflection: ${
|
|
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
|
|
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
|
|
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
|
|
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';
|
package/src/commands/rollback.ts
CHANGED
|
@@ -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
|
|
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';
|
package/src/commands/samples.ts
CHANGED
|
@@ -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
|
|
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 });
|
package/src/commands/strategy.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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() || '';
|
|
@@ -37,6 +37,8 @@ export interface DiagnosticianTask {
|
|
|
37
37
|
prompt: string;
|
|
38
38
|
createdAt: string;
|
|
39
39
|
status: 'pending' | 'completed';
|
|
40
|
+
/** Number of times task was retried due to marker exists but JSON report missing (#366) */
|
|
41
|
+
reportMissingRetries?: number;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export interface DiagnosticianTaskStore {
|
|
@@ -86,10 +88,12 @@ export async function addDiagnosticianTask(
|
|
|
86
88
|
|
|
87
89
|
|
|
88
90
|
const store = readTaskStoreSync(filePath);
|
|
91
|
+
const existing = store.tasks[taskId];
|
|
89
92
|
store.tasks[taskId] = {
|
|
90
93
|
prompt,
|
|
91
|
-
createdAt: new Date().toISOString(),
|
|
94
|
+
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
92
95
|
status: 'pending',
|
|
96
|
+
reportMissingRetries: existing?.reportMissingRetries ?? 0,
|
|
93
97
|
};
|
|
94
98
|
atomicWriteFileSync(filePath, JSON.stringify(store, null, 2));
|
|
95
99
|
});
|
|
@@ -153,3 +157,36 @@ export function hasPendingDiagnosticianTasks(stateDir: string): boolean {
|
|
|
153
157
|
const store = readTaskStore(stateDir);
|
|
154
158
|
return Object.values(store.tasks).some(t => t.status === 'pending');
|
|
155
159
|
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Re-queue a diagnostician task with an incremented reportMissingRetries counter.
|
|
163
|
+
* Used when a task has a marker file but no JSON report — the worker re-injects
|
|
164
|
+
* the task for the LLM to retry (up to MAX_REPORT_MISSING_RETRIES times).
|
|
165
|
+
*
|
|
166
|
+
* Idempotent: if the task doesn't exist, does nothing.
|
|
167
|
+
*/
|
|
168
|
+
export async function requeueDiagnosticianTask(
|
|
169
|
+
stateDir: string,
|
|
170
|
+
taskId: string,
|
|
171
|
+
maxRetries = 3,
|
|
172
|
+
): Promise<{ requeued: boolean; maxRetriesReached: boolean }> {
|
|
173
|
+
const filePath = resolveTasksPath(stateDir);
|
|
174
|
+
return withLockAsync(filePath, async () => {
|
|
175
|
+
const store = readTaskStoreSync(filePath);
|
|
176
|
+
const existing = store.tasks[taskId];
|
|
177
|
+
if (!existing) {
|
|
178
|
+
return { requeued: false, maxRetriesReached: false };
|
|
179
|
+
}
|
|
180
|
+
const retries = (existing.reportMissingRetries ?? 0) + 1;
|
|
181
|
+
if (retries > maxRetries) {
|
|
182
|
+
return { requeued: false, maxRetriesReached: true };
|
|
183
|
+
}
|
|
184
|
+
store.tasks[taskId] = {
|
|
185
|
+
...existing,
|
|
186
|
+
status: 'pending',
|
|
187
|
+
reportMissingRetries: retries,
|
|
188
|
+
};
|
|
189
|
+
atomicWriteFileSync(filePath, JSON.stringify(store, null, 2));
|
|
190
|
+
return { requeued: true, maxRetriesReached: false };
|
|
191
|
+
});
|
|
192
|
+
}
|
package/src/core/event-log.ts
CHANGED
|
@@ -197,7 +197,14 @@ export class EventLog {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
recordDiagnosticianReport(data: DiagnosticianReportEventData): void {
|
|
200
|
-
|
|
200
|
+
// Map three-state category to EventCategory
|
|
201
|
+
// Both missing_json and incomplete_fields map to 'failure' in EventCategory
|
|
202
|
+
const categoryMap: Record<DiagnosticianReportEventData['category'], EventCategory> = {
|
|
203
|
+
success: 'completed',
|
|
204
|
+
missing_json: 'failure',
|
|
205
|
+
incomplete_fields: 'failure',
|
|
206
|
+
};
|
|
207
|
+
this.record('diagnostician_report', categoryMap[data.category], undefined, data);
|
|
201
208
|
}
|
|
202
209
|
|
|
203
210
|
recordPrincipleCandidate(data: PrincipleCandidateEventData): void {
|
|
@@ -356,8 +363,19 @@ export class EventLog {
|
|
|
356
363
|
} else if (entry.type === 'heartbeat_diagnosis') {
|
|
357
364
|
stats.evolution.heartbeatsInjected++;
|
|
358
365
|
} else if (entry.type === 'diagnostician_report') {
|
|
359
|
-
|
|
360
|
-
|
|
366
|
+
const data = entry.data as unknown as DiagnosticianReportEventData;
|
|
367
|
+
// Backward compat: handle old events with success:boolean and new events with category:string
|
|
368
|
+
if ('category' in data) {
|
|
369
|
+
// New format: category is 'success' | 'missing_json' | 'incomplete_fields'
|
|
370
|
+
if (data.category === 'success' || data.category === 'incomplete_fields') {
|
|
371
|
+
stats.evolution.diagnosticianReportsWritten++;
|
|
372
|
+
}
|
|
373
|
+
if (data.category === 'missing_json') {
|
|
374
|
+
stats.evolution.reportsMissingJson++;
|
|
375
|
+
}
|
|
376
|
+
if (data.category === 'incomplete_fields') {
|
|
377
|
+
stats.evolution.reportsIncompleteFields++;
|
|
378
|
+
}
|
|
361
379
|
}
|
|
362
380
|
} else if (entry.type === 'principle_candidate') {
|
|
363
381
|
stats.evolution.principleCandidatesCreated++;
|
|
@@ -12,7 +12,7 @@ import { SystemLogger } from '../core/system-logger.js';
|
|
|
12
12
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
13
13
|
import type { EventLog } from '../core/event-log.js';
|
|
14
14
|
import { initPersistence, flushAllSessions } from '../core/session-tracker.js';
|
|
15
|
-
import { addDiagnosticianTask, completeDiagnosticianTask } from '../core/diagnostician-task-store.js';
|
|
15
|
+
import { addDiagnosticianTask, completeDiagnosticianTask, requeueDiagnosticianTask } from '../core/diagnostician-task-store.js';
|
|
16
16
|
import { getEvolutionLogger } from '../core/evolution-logger.js';
|
|
17
17
|
import type { TaskKind, TaskPriority } from '../core/trajectory-types.js';
|
|
18
18
|
import type { PrincipleEvaluability } from '../types/principle-tree-schema.js';
|
|
@@ -923,12 +923,49 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
923
923
|
|
|
924
924
|
let principlesGenerated = 0;
|
|
925
925
|
// C: Track report success for event recording
|
|
926
|
+
// FIX: Use reportParsed flag so reportSuccess=false when JSON is missing/garbled
|
|
926
927
|
let reportSuccess = false;
|
|
928
|
+
let reportParsed = false;
|
|
927
929
|
// Create principle from the diagnostician's JSON report.
|
|
928
930
|
const reportPath = path.join(wctx.stateDir, `.diagnostician_report_${task.id}.json`);
|
|
929
931
|
if (fs.existsSync(reportPath)) {
|
|
930
932
|
try {
|
|
931
|
-
const
|
|
933
|
+
const raw = fs.readFileSync(reportPath, 'utf8');
|
|
934
|
+
if (!raw || raw.trim().length === 0) {
|
|
935
|
+
throw new Error('Report file is empty');
|
|
936
|
+
}
|
|
937
|
+
const reportData = JSON.parse(raw);
|
|
938
|
+
if (!reportData) {
|
|
939
|
+
throw new Error('JSON parsed but content is null/undefined');
|
|
940
|
+
}
|
|
941
|
+
// Report is valid JSON — mark as parsed
|
|
942
|
+
reportParsed = true;
|
|
943
|
+
|
|
944
|
+
// FIX: Validate phase completeness before accepting the report
|
|
945
|
+
// A report missing critical phases is considered failed (not silently accepted).
|
|
946
|
+
// The diagnostician must produce all 4 diagnostic phases.
|
|
947
|
+
const phases = reportData?.phases || reportData?.diagnosis_report?.phases || {};
|
|
948
|
+
const requiredPhases = [
|
|
949
|
+
'evidence_gathering',
|
|
950
|
+
'causal_chain',
|
|
951
|
+
'root_cause_classification',
|
|
952
|
+
'principle_extraction',
|
|
953
|
+
];
|
|
954
|
+
const presentPhases = requiredPhases.filter(p =>
|
|
955
|
+
phases && Object.keys(phases).length > 0 && phases[p]
|
|
956
|
+
);
|
|
957
|
+
if (presentPhases.length < requiredPhases.length) {
|
|
958
|
+
const missing = requiredPhases.filter(p => !phases[p]);
|
|
959
|
+
if (logger) logger.warn(`[PD:EvolutionWorker] Report for task ${task.id} incomplete — missing phases: ${missing.join(', ')} (present: ${presentPhases.length}/${requiredPhases.length})`);
|
|
960
|
+
// Treat as retryable failure: don't mark success, let retry logic kick in
|
|
961
|
+
reportParsed = false;
|
|
962
|
+
// Also delete the incomplete marker so next heartbeat re-runs the diagnostician
|
|
963
|
+
try { fs.unlinkSync(completeMarker); } catch { /* ignore if already gone */ }
|
|
964
|
+
task.status = 'pending';
|
|
965
|
+
task.resolution = undefined;
|
|
966
|
+
queueChanged = true;
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
932
969
|
|
|
933
970
|
// ── Step 3: Noise Classification Filter ──
|
|
934
971
|
// Skip principle creation for low-value noise categories that don't represent
|
|
@@ -1048,15 +1085,52 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1048
1085
|
} catch (err) {
|
|
1049
1086
|
logger.warn(`[PD:EvolutionWorker] Failed to parse diagnostician report for task ${task.id}: ${String(err)}`);
|
|
1050
1087
|
}
|
|
1051
|
-
//
|
|
1052
|
-
reportSuccess
|
|
1088
|
+
// FIX: Only mark success if JSON was actually parsed and non-empty
|
|
1089
|
+
// If JSON was missing, garbled, or empty — reportSuccess stays false
|
|
1090
|
+
reportSuccess = reportParsed;
|
|
1053
1091
|
} else {
|
|
1054
|
-
|
|
1092
|
+
// ── #366: Marker exists but JSON report missing — retry logic ──
|
|
1093
|
+
// Do NOT mark completed yet. Re-inject the task for the next heartbeat cycle.
|
|
1094
|
+
// Read retry count from marker file content.
|
|
1095
|
+
const MAX_REPORT_MISSING_RETRIES = 3;
|
|
1096
|
+
let markerRetries = 0;
|
|
1097
|
+
try {
|
|
1098
|
+
const markerContent = fs.readFileSync(completeMarker, 'utf8');
|
|
1099
|
+
const match = markerContent.match(/report_missing_retries:(\d+)/);
|
|
1100
|
+
if (match) markerRetries = parseInt(match[1], 10);
|
|
1101
|
+
} catch { /* marker may not be readable, use 0 */ }
|
|
1102
|
+
|
|
1103
|
+
if (markerRetries < MAX_REPORT_MISSING_RETRIES) {
|
|
1104
|
+
// Re-inject: keep task in queue (don't mark completed), update marker with incremented count
|
|
1105
|
+
const newRetries = markerRetries + 1;
|
|
1106
|
+
if (logger) logger.info(`[PD:EvolutionWorker] Task ${task.id}: marker found but report missing — re-queuing (retry ${newRetries}/${MAX_REPORT_MISSING_RETRIES})`);
|
|
1107
|
+
// FIX: Update store's reportMissingRetries BEFORE deleting the marker.
|
|
1108
|
+
// This ensures the store's retry count is persisted even if the
|
|
1109
|
+
// diagnostician session crashes before re-adding the task.
|
|
1110
|
+
await requeueDiagnosticianTask(wctx.stateDir, task.id, MAX_REPORT_MISSING_RETRIES);
|
|
1111
|
+
// Also update the task in the main queue to keep it alive
|
|
1112
|
+
task.status = 'pending';
|
|
1113
|
+
task.resolution = undefined;
|
|
1114
|
+
queueChanged = true;
|
|
1115
|
+
// Delete the marker so the next heartbeat sees no marker
|
|
1116
|
+
// and re-processes the task as a fresh diagnostician run.
|
|
1117
|
+
try {
|
|
1118
|
+
fs.unlinkSync(completeMarker);
|
|
1119
|
+
} catch { /* ignore if already deleted */ }
|
|
1120
|
+
// Skip the completion/unlink block below — task is still pending
|
|
1121
|
+
continue;
|
|
1122
|
+
} else {
|
|
1123
|
+
// Max retries reached — accept that no report was produced
|
|
1124
|
+
if (logger) logger.warn(`[PD:EvolutionWorker] Task ${task.id}: max retries (${MAX_REPORT_MISSING_RETRIES}) reached — marking as failed_max_retries`);
|
|
1125
|
+
task.status = 'completed';
|
|
1126
|
+
task.completed_at = new Date().toISOString();
|
|
1127
|
+
task.resolution = 'failed_max_retries';
|
|
1128
|
+
}
|
|
1055
1129
|
}
|
|
1056
1130
|
|
|
1057
|
-
|
|
1058
|
-
task.
|
|
1059
|
-
|
|
1131
|
+
// Only reached if JSON existed or max retries reached:
|
|
1132
|
+
task.status = task.status || 'completed';
|
|
1133
|
+
task.completed_at = task.completed_at || new Date().toISOString();
|
|
1060
1134
|
if (!task.resolution) task.resolution = 'marker_detected';
|
|
1061
1135
|
try {
|
|
1062
1136
|
fs.unlinkSync(completeMarker);
|
|
@@ -1073,10 +1147,16 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1073
1147
|
|
|
1074
1148
|
// C: Record diagnostician_report event for observability
|
|
1075
1149
|
if (eventLog) {
|
|
1150
|
+
// Map to three-state category:
|
|
1151
|
+
// - reportSuccess=true → 'success' (JSON exists, parsed, principle found)
|
|
1152
|
+
// - reportSuccess=false, reportParsed=true → 'incomplete_fields' (JSON existed but principle missing)
|
|
1153
|
+
// - reportSuccess=false, reportParsed=false → 'missing_json' (JSON never existed)
|
|
1154
|
+
const reportCategory: 'success' | 'missing_json' | 'incomplete_fields' =
|
|
1155
|
+
reportSuccess ? 'success' : reportParsed ? 'incomplete_fields' : 'missing_json';
|
|
1076
1156
|
eventLog.recordDiagnosticianReport({
|
|
1077
1157
|
taskId: task.id,
|
|
1078
1158
|
reportPath,
|
|
1079
|
-
|
|
1159
|
+
category: reportCategory,
|
|
1080
1160
|
});
|
|
1081
1161
|
}
|
|
1082
1162
|
|
|
@@ -69,6 +69,10 @@ export interface RuntimeSummary {
|
|
|
69
69
|
tasksWrittenToday: number;
|
|
70
70
|
/** Total diagnostician reports written (today from event log) */
|
|
71
71
|
reportsWrittenToday: number;
|
|
72
|
+
/** Total diagnostician reports that were missing JSON (category=missing_json) */
|
|
73
|
+
reportsMissingJsonToday: number;
|
|
74
|
+
/** Total diagnostician reports with incomplete fields (category=incomplete_fields) */
|
|
75
|
+
reportsIncompleteFieldsToday: number;
|
|
72
76
|
/** Total principle candidates created from heartbeat chain (today from event log) */
|
|
73
77
|
candidatesCreatedToday: number;
|
|
74
78
|
/** Heartbeats that injected diagnostician tasks (today from event log) */
|
|
@@ -194,6 +198,8 @@ export class RuntimeSummaryService {
|
|
|
194
198
|
evolution?: {
|
|
195
199
|
diagnosisTasksWritten?: number;
|
|
196
200
|
diagnosticianReportsWritten?: number;
|
|
201
|
+
reportsMissingJson?: number;
|
|
202
|
+
reportsIncompleteFields?: number;
|
|
197
203
|
principleCandidatesCreated?: number;
|
|
198
204
|
heartbeatsInjected?: number;
|
|
199
205
|
[key: string]: unknown;
|
|
@@ -265,6 +271,8 @@ export class RuntimeSummaryService {
|
|
|
265
271
|
pendingTasks: pendingDiagTasks.length,
|
|
266
272
|
tasksWrittenToday: diagDailyStats?.diagnosisTasksWritten ?? 0,
|
|
267
273
|
reportsWrittenToday: diagDailyStats?.diagnosticianReportsWritten ?? 0,
|
|
274
|
+
reportsMissingJsonToday: diagDailyStats?.reportsMissingJson ?? 0,
|
|
275
|
+
reportsIncompleteFieldsToday: diagDailyStats?.reportsIncompleteFields ?? 0,
|
|
268
276
|
candidatesCreatedToday: diagDailyStats?.principleCandidatesCreated ?? 0,
|
|
269
277
|
heartbeatsInjectedToday: diagDailyStats?.heartbeatsInjected ?? 0,
|
|
270
278
|
};
|
package/src/types/event-types.ts
CHANGED
|
@@ -209,7 +209,12 @@ export interface DiagnosisTaskEventData {
|
|
|
209
209
|
export interface DiagnosticianReportEventData {
|
|
210
210
|
taskId: string;
|
|
211
211
|
reportPath: string;
|
|
212
|
-
success
|
|
212
|
+
/** Three-state category replacing boolean success field.
|
|
213
|
+
* - 'success': JSON exists and has principle field
|
|
214
|
+
* - 'missing_json': marker exists but JSON does not (Issue #366, LLM output truncation)
|
|
215
|
+
* - 'incomplete_fields': JSON exists but missing principle field
|
|
216
|
+
*/
|
|
217
|
+
category: 'success' | 'missing_json' | 'incomplete_fields';
|
|
213
218
|
}
|
|
214
219
|
|
|
215
220
|
/**
|
|
@@ -326,6 +331,8 @@ export interface EvolutionStats {
|
|
|
326
331
|
diagnosisTasksWritten: number;
|
|
327
332
|
heartbeatsInjected: number;
|
|
328
333
|
diagnosticianReportsWritten: number;
|
|
334
|
+
reportsMissingJson: number;
|
|
335
|
+
reportsIncompleteFields: number;
|
|
329
336
|
principleCandidatesCreated: number;
|
|
330
337
|
rulesEnforced: number;
|
|
331
338
|
}
|
|
@@ -490,6 +497,8 @@ export function createEmptyDailyStats(date: string): DailyStats {
|
|
|
490
497
|
diagnosisTasksWritten: 0,
|
|
491
498
|
heartbeatsInjected: 0,
|
|
492
499
|
diagnosticianReportsWritten: 0,
|
|
500
|
+
reportsMissingJson: 0,
|
|
501
|
+
reportsIncompleteFields: 0,
|
|
493
502
|
principleCandidatesCreated: 0,
|
|
494
503
|
rulesEnforced: 0,
|
|
495
504
|
},
|
|
@@ -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
|
-
|
|
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.
|
|
@@ -6,7 +6,7 @@ disable-model-invocation: true
|
|
|
6
6
|
|
|
7
7
|
# Diagnostician - Root Cause Analysis Agent
|
|
8
8
|
|
|
9
|
-
You are a professional root cause analysis expert. You MUST strictly follow the **
|
|
9
|
+
You are a professional root cause analysis expert. You MUST strictly follow the **six-phase protocol** (Phase 0 optional + Phase 1-5 mandatory) below to execute analysis and **immediately write results to the report file after each Phase completes**.
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -106,6 +106,20 @@ You are a professional root cause analysis expert. You MUST strictly follow the
|
|
|
106
106
|
}
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
**⚠️ Write Report File Immediately After Phase 1**:
|
|
110
|
+
Once Phase 1 is complete, **immediately** write the result to the report file (do NOT wait until the end):
|
|
111
|
+
```
|
|
112
|
+
write: .state/.diagnostician_report_<TASK_ID>.json
|
|
113
|
+
content: {
|
|
114
|
+
"taskId": "<TASK_ID>",
|
|
115
|
+
"completedAt": "<ISO timestamp>",
|
|
116
|
+
"phases": {
|
|
117
|
+
"evidence_gathering": { ...Phase 1 result... }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
If the file already exists (a previous Phase was already written), read the existing content, merge the new Phase result into it, then overwrite.
|
|
122
|
+
|
|
109
123
|
---
|
|
110
124
|
|
|
111
125
|
### Phase 2: Causal Chain Construction [Required]
|
|
@@ -145,6 +159,9 @@ You are a professional root cause analysis expert. You MUST strictly follow the
|
|
|
145
159
|
}
|
|
146
160
|
```
|
|
147
161
|
|
|
162
|
+
**⚠️ Write Report File Immediately After Phase 2**:
|
|
163
|
+
Once Phase 2 is complete, **immediately** merge the result into the report file (overwrite, do not lose Phase 1 content).
|
|
164
|
+
|
|
148
165
|
---
|
|
149
166
|
|
|
150
167
|
### Phase 3: Root Cause Classification [Required]
|
|
@@ -178,6 +195,9 @@ You are a professional root cause analysis expert. You MUST strictly follow the
|
|
|
178
195
|
}
|
|
179
196
|
```
|
|
180
197
|
|
|
198
|
+
**⚠️ Write Report File Immediately After Phase 3**:
|
|
199
|
+
Once Phase 3 is complete, **immediately** merge the result into the report file.
|
|
200
|
+
|
|
181
201
|
---
|
|
182
202
|
|
|
183
203
|
### Phase 4: Principle Extraction [Required]
|
|
@@ -264,9 +284,32 @@ You are a professional root cause analysis expert. You MUST strictly follow the
|
|
|
264
284
|
- "External dependency availability must be validated before invocation"
|
|
265
285
|
- "Code modifications must go through Issue process, ensuring traceability and rollback"
|
|
266
286
|
|
|
267
|
-
**
|
|
268
|
-
|
|
269
|
-
|
|
287
|
+
**Phase 4 Output Fields** (also write immediately after completing Phase 4 — merge with previous Phases):
|
|
288
|
+
```json
|
|
289
|
+
{
|
|
290
|
+
"taskId": "<TASK_ID>",
|
|
291
|
+
"completedAt": "<ISO timestamp>",
|
|
292
|
+
"phases": {
|
|
293
|
+
"context_extraction": { ... Phase 0 result ... },
|
|
294
|
+
"evidence_gathering": { ... Phase 1 result ... },
|
|
295
|
+
"causal_chain": { ... Phase 2 result ... },
|
|
296
|
+
"root_cause_classification": { ... Phase 3 result ... },
|
|
297
|
+
"principle_extraction": {
|
|
298
|
+
"phase": "principle_extraction",
|
|
299
|
+
"classification": {
|
|
300
|
+
"category": "development_transient|user_error|Design|Tooling|...",
|
|
301
|
+
"confidence": "high|medium|low",
|
|
302
|
+
"reproducible": true|false,
|
|
303
|
+
"severity": "high|medium|low"
|
|
304
|
+
},
|
|
305
|
+
"principle": { ... }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**⚠️ Write Report File Immediately After Phase 4**:
|
|
312
|
+
Once Phase 4 is complete, **immediately** merge the result (with `classification` and `principle`) into the report file. This is the final write — all Phases must now be present.
|
|
270
313
|
|
|
271
314
|
---
|
|
272
315
|
|
|
@@ -285,20 +328,27 @@ Your diagnostic report will be **auto-parsed as JSON**. Any format errors will c
|
|
|
285
328
|
|
|
286
329
|
**Self-check method**: Before outputting, mentally verify: every `"` must have matching `"` after it, if content contains `"` it must be escaped as `\"`.
|
|
287
330
|
|
|
288
|
-
|
|
331
|
+
The final report (written incrementally by each Phase) should look like:
|
|
289
332
|
|
|
290
333
|
```json
|
|
291
334
|
{
|
|
292
|
-
"
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
"
|
|
296
|
-
"
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
"
|
|
301
|
-
"
|
|
335
|
+
"taskId": "<TASK_ID>",
|
|
336
|
+
"completedAt": "2026-03-24T10:30:00Z",
|
|
337
|
+
"phases": {
|
|
338
|
+
"context_extraction": { "phase": "context_extraction", "session_id": "...", "context_source": "...", "conversation_summary": "..." },
|
|
339
|
+
"evidence_gathering": { "phase": "evidence_gathering", "evidence": { ... } },
|
|
340
|
+
"causal_chain": { "phase": "causal_chain", "chain": [...], "terminated_at": 3, "termination_reason": "..." },
|
|
341
|
+
"root_cause_classification": { "phase": "root_cause_classification", "root_cause": "...", "category": "Design", "guardrail_analysis": { ... } },
|
|
342
|
+
"principle_extraction": {
|
|
343
|
+
"phase": "principle_extraction",
|
|
344
|
+
"classification": { "category": "Design", "confidence": "high", "reproducible": false, "severity": "low" },
|
|
345
|
+
"principle": {
|
|
346
|
+
"trigger_pattern": "...",
|
|
347
|
+
"action": "...",
|
|
348
|
+
"abstracted_principle": "...",
|
|
349
|
+
"duplicate": false,
|
|
350
|
+
"coreAxiomId": "T-02"
|
|
351
|
+
}
|
|
302
352
|
}
|
|
303
353
|
}
|
|
304
354
|
}
|
|
@@ -306,6 +356,38 @@ Merge outputs from all four phases into one JSON object:
|
|
|
306
356
|
|
|
307
357
|
---
|
|
308
358
|
|
|
359
|
+
## ✅ Completion Protocol
|
|
360
|
+
|
|
361
|
+
### ✅ Checklist (ALL must be satisfied before writing marker)
|
|
362
|
+
|
|
363
|
+
Before writing the marker file, you MUST confirm all of the following:
|
|
364
|
+
|
|
365
|
+
1. **Report file exists**: `.diagnostician_report_<TASK_ID>.json` has been written to disk
|
|
366
|
+
2. **All Phase fields present**:
|
|
367
|
+
- [ ] `phases.context_extraction` ✅
|
|
368
|
+
- [ ] `phases.evidence_gathering` ✅
|
|
369
|
+
- [ ] `phases.causal_chain` ✅
|
|
370
|
+
- [ ] `phases.root_cause_classification` ✅
|
|
371
|
+
- [ ] `phases.principle_extraction` ✅
|
|
372
|
+
3. **Report is valid JSON**: Use read tool to verify file content parses correctly
|
|
373
|
+
|
|
374
|
+
### ✅ Write Marker (Final Step)
|
|
375
|
+
|
|
376
|
+
**ONLY after confirming all conditions above are satisfied**, write the marker file:
|
|
377
|
+
```
|
|
378
|
+
write: .state/.evolution_complete_<TASK_ID>
|
|
379
|
+
content: diagnostic_completed: <ISO timestamp>
|
|
380
|
+
outcome: <one-sentence summary>
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### ❌ Forbidden
|
|
384
|
+
|
|
385
|
+
- **NEVER write marker before JSON** — marker means diagnosis is complete, JSON report must exist
|
|
386
|
+
- **NEVER skip any Phase** — even if a Phase seems inapplicable, write empty `{}`
|
|
387
|
+
- **NEVER use non-ASCII quotes in JSON** — must use `"`, not `"` `"`
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
309
391
|
## ⚠️ Execution Constraints
|
|
310
392
|
|
|
311
393
|
1. **NO skipping phases**: MUST attempt Phase 0 (context acquisition), then execute Phase 1 → 2 → 3 → 4 in order
|
|
@@ -313,6 +395,7 @@ Merge outputs from all four phases into one JSON object:
|
|
|
313
395
|
3. **NO vague conclusions**: Root cause must be specific and fixable
|
|
314
396
|
4. **NO skipping principle extraction**: Even for simple issues, extract principles
|
|
315
397
|
5. **NO skipping deduplication**: `duplicate` field MUST appear in principle_extraction output
|
|
398
|
+
6. **NO writing marker before all Phases complete**: Marker comes LAST, only after every Phase is written to JSON
|
|
316
399
|
|
|
317
400
|
---
|
|
318
401
|
|
|
@@ -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('
|
|
23
|
+
expect(result.text).toContain('workspaceDir is not set');
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
it('enqueues into the provided active workspace', async () => {
|