principles-disciple 1.16.0 → 1.17.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/README.md +13 -5
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/commands/archive-impl.ts +3 -3
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +3 -3
- package/src/commands/disable-impl.ts +1 -1
- package/src/commands/evolution-status.ts +2 -2
- package/src/commands/focus.ts +2 -2
- package/src/commands/nocturnal-train.ts +6 -6
- package/src/commands/pain.ts +4 -4
- package/src/commands/pd-reflect.ts +87 -0
- package/src/commands/rollback-impl.ts +4 -4
- package/src/commands/rollback.ts +2 -2
- package/src/commands/samples.ts +2 -2
- package/src/commands/workflow-debug.ts +1 -1
- package/src/config/errors.ts +1 -1
- package/src/core/adaptive-thresholds.ts +1 -1
- package/src/core/code-implementation-storage.ts +2 -2
- package/src/core/config.ts +1 -1
- package/src/core/diagnostician-task-store.ts +2 -2
- package/src/core/empathy-keyword-matcher.ts +3 -3
- package/src/core/event-log.ts +5 -5
- package/src/core/evolution-engine.ts +4 -4
- package/src/core/evolution-logger.ts +1 -1
- package/src/core/evolution-reducer.ts +3 -3
- package/src/core/evolution-types.ts +5 -5
- package/src/core/external-training-contract.ts +1 -1
- package/src/core/focus-history.ts +14 -14
- package/src/core/hygiene/tracker.ts +1 -1
- package/src/core/init.ts +2 -2
- package/src/core/model-deployment-registry.ts +2 -2
- package/src/core/model-training-registry.ts +2 -2
- package/src/core/nocturnal-arbiter.ts +1 -1
- package/src/core/nocturnal-artificer.ts +2 -2
- package/src/core/nocturnal-candidate-scoring.ts +2 -2
- package/src/core/nocturnal-compliance.ts +3 -3
- package/src/core/nocturnal-dataset.ts +3 -3
- package/src/core/nocturnal-export.ts +4 -4
- package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
- package/src/core/nocturnal-snapshot-contract.ts +112 -0
- package/src/core/nocturnal-trajectory-extractor.ts +7 -5
- package/src/core/nocturnal-trinity.ts +27 -28
- package/src/core/pain-context-extractor.ts +3 -3
- package/src/core/pain.ts +124 -11
- package/src/core/path-resolver.ts +4 -4
- package/src/core/pd-task-reconciler.ts +10 -10
- package/src/core/pd-task-service.ts +1 -1
- package/src/core/pd-task-store.ts +1 -1
- package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
- package/src/core/principle-training-state.ts +2 -2
- package/src/core/principle-tree-ledger.ts +7 -7
- package/src/core/promotion-gate.ts +9 -9
- package/src/core/replay-engine.ts +12 -12
- package/src/core/risk-calculator.ts +1 -1
- package/src/core/rule-host-types.ts +2 -2
- package/src/core/rule-host.ts +5 -5
- package/src/core/schema/db-types.ts +1 -1
- package/src/core/schema/schema-definitions.ts +1 -1
- package/src/core/session-tracker.ts +96 -4
- package/src/core/shadow-observation-registry.ts +3 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-os-parser.ts +1 -1
- package/src/core/training-program.ts +2 -2
- package/src/core/trajectory.ts +8 -8
- package/src/core/workspace-context.ts +2 -2
- package/src/core/workspace-dir-service.ts +85 -0
- package/src/core/workspace-dir-validation.ts +30 -107
- package/src/hooks/bash-risk.ts +3 -3
- package/src/hooks/edit-verification.ts +4 -4
- package/src/hooks/gate-block-helper.ts +4 -4
- package/src/hooks/gate.ts +10 -10
- package/src/hooks/gfi-gate.ts +7 -7
- package/src/hooks/lifecycle.ts +2 -2
- package/src/hooks/llm.ts +1 -1
- package/src/hooks/pain.ts +25 -5
- package/src/hooks/progressive-trust-gate.ts +7 -7
- package/src/hooks/prompt.ts +24 -5
- package/src/hooks/subagent.ts +2 -2
- package/src/hooks/thinking-checkpoint.ts +2 -2
- package/src/hooks/trajectory-collector.ts +1 -1
- package/src/http/principles-console-route.ts +14 -6
- package/src/i18n/commands.ts +4 -0
- package/src/index.ts +181 -185
- package/src/service/central-health-service.ts +1 -1
- package/src/service/central-overview-service.ts +3 -3
- package/src/service/evolution-query-service.ts +1 -1
- package/src/service/evolution-worker.ts +209 -104
- package/src/service/health-query-service.ts +27 -17
- package/src/service/monitoring-query-service.ts +3 -3
- package/src/service/nocturnal-runtime.ts +4 -4
- package/src/service/nocturnal-service.ts +40 -23
- package/src/service/nocturnal-target-selector.ts +2 -2
- package/src/service/runtime-summary-service.ts +1 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
- package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
- package/src/service/subagent-workflow/types.ts +4 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
- package/src/service/subagent-workflow/workflow-store.ts +2 -2
- package/src/tools/critique-prompt.ts +2 -3
- package/src/tools/deep-reflect.ts +17 -16
- package/src/tools/model-index.ts +1 -1
- package/src/utils/file-lock.ts +1 -1
- package/src/utils/io.ts +7 -2
- package/src/utils/nlp.ts +1 -1
- package/src/utils/plugin-logger.ts +2 -2
- package/src/utils/retry.ts +3 -2
- package/src/utils/subagent-probe.ts +20 -33
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/pain_settings.json +1 -1
- package/tests/build-artifacts.test.ts +4 -58
- package/tests/commands/pd-reflect.test.ts +49 -0
- package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
- package/tests/core/pain-auto-repair.test.ts +96 -0
- package/tests/core/pain-integration.test.ts +483 -0
- package/tests/core/pain.test.ts +5 -4
- package/tests/core/workspace-dir-service.test.ts +68 -0
- package/tests/core/workspace-dir-validation.test.ts +56 -192
- package/tests/hooks/pain.test.ts +20 -0
- package/tests/http/principles-console-route.test.ts +42 -20
- package/tests/integration/empathy-workflow-integration.test.ts +1 -2
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
- package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
- package/tests/service/evolution-worker.nocturnal.test.ts +562 -6
- package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
- package/tests/utils/subagent-probe.test.ts +32 -0
|
@@ -1,152 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WorkspaceDir Validation Utilities
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* silently writing to the wrong directory.
|
|
4
|
+
* This module only validates candidate workspace directories and delegates
|
|
5
|
+
* actual resolution policy to workspace-dir-service.ts.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
/* eslint-disable no-unused-vars -- Reason: type definitions require param names that implementations may not use */
|
|
10
|
-
|
|
11
8
|
import * as os from 'os';
|
|
12
9
|
import type { PluginLogger } from '../openclaw-sdk.js';
|
|
10
|
+
import { resolveWorkspaceDir, type WorkspaceResolutionContext } from './workspace-dir-service.js';
|
|
13
11
|
|
|
14
|
-
/**
|
|
15
|
-
* Check if a path looks like a home directory (not a real workspace).
|
|
16
|
-
* Returns the reason if suspicious, or null if it looks valid.
|
|
17
|
-
*/
|
|
18
12
|
export function validateWorkspaceDir(dir: string | undefined): string | null {
|
|
19
13
|
if (!dir) {
|
|
20
14
|
return 'workspaceDir is undefined/null';
|
|
21
15
|
}
|
|
22
|
-
|
|
16
|
+
|
|
23
17
|
const homeDir = os.homedir();
|
|
24
|
-
|
|
25
|
-
// Home directory itself is not a valid workspace
|
|
18
|
+
|
|
26
19
|
if (dir === homeDir) {
|
|
27
20
|
return `workspaceDir equals home directory (${homeDir}), likely missing context field`;
|
|
28
21
|
}
|
|
29
|
-
|
|
30
|
-
// Root directory is definitely not a workspace
|
|
22
|
+
|
|
31
23
|
if (dir === '/' || dir === '') {
|
|
32
24
|
return `workspaceDir is root or empty: "${dir}"`;
|
|
33
25
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Common bad patterns:
|
|
26
|
+
|
|
27
|
+
const escapedHome = homeDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
37
28
|
const badPatterns = [
|
|
38
|
-
|
|
39
|
-
{ pattern: new RegExp(`^${
|
|
40
|
-
{ pattern: new RegExp(`^${homeDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/$`), desc: 'is home directory with trailing slash' },
|
|
29
|
+
{ pattern: new RegExp(`^${escapedHome}$`), desc: 'is home directory itself' },
|
|
30
|
+
{ pattern: new RegExp(`^${escapedHome}/$`), desc: 'is home directory with trailing slash' },
|
|
41
31
|
];
|
|
42
|
-
|
|
32
|
+
|
|
43
33
|
for (const { pattern, desc } of badPatterns) {
|
|
44
34
|
if (pattern.test(dir)) {
|
|
45
35
|
return `workspaceDir ${desc}: "${dir}"`;
|
|
46
36
|
}
|
|
47
37
|
}
|
|
48
|
-
|
|
49
|
-
return null;
|
|
38
|
+
|
|
39
|
+
return null;
|
|
50
40
|
}
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* Returns the resolved path if successful and valid, or null.
|
|
55
|
-
*/
|
|
56
|
-
function tryResolveFromAgentId(
|
|
57
|
-
agentId: string,
|
|
42
|
+
export function resolveValidWorkspaceDir(
|
|
43
|
+
ctx: WorkspaceResolutionContext,
|
|
58
44
|
api: {
|
|
59
45
|
runtime: { agent: { resolveAgentWorkspaceDir: (config: unknown, agentId: string) => string } };
|
|
60
46
|
config: unknown;
|
|
47
|
+
logger: PluginLogger;
|
|
61
48
|
},
|
|
62
|
-
|
|
63
|
-
): string |
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
return resolved;
|
|
72
|
-
} catch (err) {
|
|
73
|
-
onWarning(`failed to resolve from agentId: ${String(err)}`);
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
49
|
+
options?: { source?: string; fallbackAgentId?: string },
|
|
50
|
+
): string | undefined {
|
|
51
|
+
return resolveWorkspaceDir(api as never, ctx, {
|
|
52
|
+
source: options?.source,
|
|
53
|
+
fallbackAgentId: options?.fallbackAgentId,
|
|
54
|
+
logger: api.logger,
|
|
55
|
+
});
|
|
76
56
|
}
|
|
77
57
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*/
|
|
82
|
-
function validateFallback(path: string, onWarning: (msg: string) => void): string {
|
|
83
|
-
const issue = validateWorkspaceDir(path);
|
|
84
|
-
if (issue) {
|
|
85
|
-
onWarning(`FINAL FALLBACK "${path}" is also invalid: ${issue}. Events will be written to wrong location!`);
|
|
86
|
-
}
|
|
87
|
-
return path;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Resolve workspaceDir with validation and warning.
|
|
92
|
-
*
|
|
93
|
-
* Usage:
|
|
94
|
-
* const workspaceDir = resolveValidWorkspaceDir(ctx, api, { source: 'after_tool_call' });
|
|
95
|
-
*
|
|
96
|
-
* Fallback chain:
|
|
97
|
-
* 1. ctx.workspaceDir (validated)
|
|
98
|
-
* 2. api.runtime.agent.resolveAgentWorkspaceDir(config, ctx.agentId)
|
|
99
|
-
* 3. api.resolvePath('.') (last resort, warns loudly)
|
|
100
|
-
*/
|
|
101
|
-
export function resolveValidWorkspaceDir(
|
|
102
|
-
ctx: { workspaceDir?: string; agentId?: string },
|
|
58
|
+
export function logWorkspaceDirHealth(
|
|
59
|
+
ctx: WorkspaceResolutionContext,
|
|
60
|
+
source: string,
|
|
103
61
|
api: {
|
|
104
62
|
runtime: { agent: { resolveAgentWorkspaceDir: (config: unknown, agentId: string) => string } };
|
|
105
63
|
config: unknown;
|
|
106
|
-
resolvePath: (input: string) => string;
|
|
107
64
|
logger: PluginLogger;
|
|
108
65
|
},
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const source = options?.source || 'unknown';
|
|
112
|
-
const onWarning = options?.onWarning || ((msg: string) => api.logger.warn(`[PD:workspaceDir] ${msg}`));
|
|
113
|
-
|
|
114
|
-
// 1. Try ctx.workspaceDir
|
|
115
|
-
if (ctx.workspaceDir) {
|
|
116
|
-
const issue = validateWorkspaceDir(ctx.workspaceDir);
|
|
117
|
-
if (issue) {
|
|
118
|
-
onWarning(`${source}: ctx.workspaceDir="${ctx.workspaceDir}" is invalid: ${issue}`);
|
|
119
|
-
} else {
|
|
120
|
-
return ctx.workspaceDir;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 2. Try agentId resolution
|
|
125
|
-
if (ctx.agentId) {
|
|
126
|
-
const fromAgent = tryResolveFromAgentId(ctx.agentId, api, onWarning);
|
|
127
|
-
if (fromAgent) return fromAgent;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 3. Final fallback
|
|
131
|
-
return validateFallback(api.resolvePath('.'), onWarning);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Log workspaceDir resolution for debugging.
|
|
136
|
-
* Call this once during plugin startup to verify hook contexts.
|
|
137
|
-
*/
|
|
138
|
-
export function logWorkspaceDirHealth(ctx: { workspaceDir?: string; agentId?: string }, source: string, api: {
|
|
139
|
-
runtime: { agent: { resolveAgentWorkspaceDir: (config: unknown, agentId: string) => string } };
|
|
140
|
-
config: unknown;
|
|
141
|
-
resolvePath: (input: string) => string;
|
|
142
|
-
logger: PluginLogger;
|
|
143
|
-
}): void {
|
|
144
|
-
const resolved = resolveValidWorkspaceDir(ctx, api, { source });
|
|
66
|
+
): void {
|
|
67
|
+
const resolved = resolveValidWorkspaceDir(ctx, api, { source, fallbackAgentId: 'main' });
|
|
145
68
|
const issue = validateWorkspaceDir(resolved);
|
|
146
|
-
|
|
69
|
+
|
|
147
70
|
if (issue) {
|
|
148
71
|
api.logger.error(`[PD:health] ${source}: workspaceDir="${resolved}" - ${issue}`);
|
|
149
72
|
} else {
|
|
150
|
-
api.logger.info(`[PD:health] ${source}: workspaceDir="${resolved}"
|
|
73
|
+
api.logger.info(`[PD:health] ${source}: workspaceDir="${resolved}" OK`);
|
|
151
74
|
}
|
|
152
75
|
}
|
package/src/hooks/bash-risk.ts
CHANGED
|
@@ -38,12 +38,12 @@ export type BashRiskLevel = 'safe' | 'dangerous' | 'normal';
|
|
|
38
38
|
* @param logger - Optional logger for warnings about invalid patterns
|
|
39
39
|
* @returns The risk level: 'safe', 'dangerous', or 'normal'
|
|
40
40
|
*/
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
export function analyzeBashCommand(
|
|
43
43
|
command: string,
|
|
44
44
|
safePatterns: string[],
|
|
45
45
|
dangerousPatterns: string[],
|
|
46
|
-
logger?: { warn?: (
|
|
46
|
+
logger?: { warn?: ( _message: string) => void }
|
|
47
47
|
): BashRiskLevel {
|
|
48
48
|
let normalizedCmd = command.trim().toLowerCase();
|
|
49
49
|
|
|
@@ -65,7 +65,7 @@ export function analyzeBashCommand(
|
|
|
65
65
|
// - Zero-width joiner (U+200D)
|
|
66
66
|
// - Word joiner (U+2060)
|
|
67
67
|
// - Zero-width invisible separator (U+FEFF)
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
const ZERO_WIDTH_CHARS = /[\u200B\u200C\u200D\u2060\uFEFF]/g;
|
|
70
70
|
if (ZERO_WIDTH_CHARS.test(command)) {
|
|
71
71
|
logger?.warn?.(`[PD_GATE] Bash command contains zero-width characters — blocking as dangerous`);
|
|
@@ -126,7 +126,7 @@ This is enforced by P-03 (精确匹配前验证原则).`;
|
|
|
126
126
|
* Handle edit tool verification before allowing operation
|
|
127
127
|
* This enforces P-03 at the tool layer
|
|
128
128
|
*/
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
export function handleEditVerification(
|
|
131
131
|
event: PluginHookBeforeToolCallEvent,
|
|
132
132
|
wctx: WorkspaceContext,
|
|
@@ -155,11 +155,11 @@ export function handleEditVerification(
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
// 2. Resolve and read file
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
let absolutePath: string;
|
|
160
160
|
try {
|
|
161
161
|
absolutePath = wctx.resolve(filePath);
|
|
162
|
-
} catch (_error) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
162
|
+
} catch (_error) { // eslint-disable-line @typescript-eslint/no-unused-vars -- Reason: intentionally unused - let it fail naturally on path resolution error
|
|
163
163
|
// Path resolution error, let it fail naturally
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
@@ -222,7 +222,7 @@ export function handleEditVerification(
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// 3. Read current file content with improved error handling
|
|
225
|
-
|
|
225
|
+
|
|
226
226
|
let currentContent: string;
|
|
227
227
|
try {
|
|
228
228
|
currentContent = fs.readFileSync(absolutePath, 'utf-8');
|
|
@@ -48,9 +48,9 @@ export interface BlockContext {
|
|
|
48
48
|
export function recordGateBlockAndReturn(
|
|
49
49
|
wctx: WorkspaceContext,
|
|
50
50
|
blockCtx: BlockContext,
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
logger: { warn?: (_message: string) => void; error?: (_message: string) => void; info?: (_message: string) => void }
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
): PluginHookBeforeToolCallResult {
|
|
55
55
|
const { filePath, reason, toolName, sessionId, blockSource } = blockCtx;
|
|
56
56
|
|
|
@@ -93,7 +93,7 @@ export function recordGateBlockAndReturn(
|
|
|
93
93
|
wctx.trajectory?.recordGateBlock?.(trajectoryPayload);
|
|
94
94
|
} catch (error: unknown) {
|
|
95
95
|
logWarn(`[PD_GATE] Failed to record trajectory gate block: ${String(error)}`);
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
scheduleTrajectoryGateBlockRetry(wctx, trajectoryPayload, 1, logWarn, logError);
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -133,7 +133,7 @@ This is a mandatory security gate. The operation was blocked because the modific
|
|
|
133
133
|
* Uses exponential backoff with max retries.
|
|
134
134
|
* Failures are logged but do not affect the runtime block decision.
|
|
135
135
|
*/
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
function scheduleTrajectoryGateBlockRetry(
|
|
138
138
|
wctx: WorkspaceContext,
|
|
139
139
|
payload: {
|
package/src/hooks/gate.ts
CHANGED
|
@@ -134,7 +134,7 @@ export function handleBeforeToolCall(
|
|
|
134
134
|
const mutationMatch = /(?:>|>>|sed\s+-i|rm|mv|mkdir|touch|cp)\s+(?:-[a-zA-Z]+\s+)*([^\s;&|<>]+)/.exec(command);
|
|
135
135
|
|
|
136
136
|
if (mutationMatch) {
|
|
137
|
-
|
|
137
|
+
|
|
138
138
|
filePath = mutationMatch[1];
|
|
139
139
|
} else {
|
|
140
140
|
const hasRiskPath = profile.risk_paths.some(rp => command.includes(rp));
|
|
@@ -167,30 +167,30 @@ export function handleBeforeToolCall(
|
|
|
167
167
|
action: {
|
|
168
168
|
toolName: event.toolName,
|
|
169
169
|
normalizedPath: relPath,
|
|
170
|
-
|
|
170
|
+
|
|
171
171
|
paramsSummary: _extractParamsSummary(event.params),
|
|
172
172
|
},
|
|
173
173
|
workspace: {
|
|
174
174
|
isRiskPath: risky,
|
|
175
|
-
|
|
175
|
+
|
|
176
176
|
planStatus: _getPlanStatus(ctx.workspaceDir),
|
|
177
|
-
|
|
177
|
+
|
|
178
178
|
hasPlanFile: _hasPlanFile(ctx.workspaceDir),
|
|
179
179
|
},
|
|
180
180
|
session: {
|
|
181
181
|
sessionId: ctx.sessionId,
|
|
182
|
-
|
|
182
|
+
|
|
183
183
|
currentGfi: _getCurrentGfi(ctx.sessionId),
|
|
184
|
-
|
|
184
|
+
|
|
185
185
|
recentThinking: _hasRecentThinking(ctx.sessionId),
|
|
186
186
|
},
|
|
187
187
|
evolution: {
|
|
188
|
-
|
|
188
|
+
|
|
189
189
|
epTier: _getEpTier(wctx.workspaceDir),
|
|
190
190
|
},
|
|
191
191
|
derived: {
|
|
192
192
|
estimatedLineChanges: estimateLineChanges({ toolName: event.toolName, params: event.params }),
|
|
193
|
-
|
|
193
|
+
|
|
194
194
|
bashRisk: _getBashRisk(event, profile),
|
|
195
195
|
},
|
|
196
196
|
};
|
|
@@ -337,12 +337,12 @@ function _getEpTier(workspaceDir: string): number {
|
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
|
|
340
|
+
|
|
341
341
|
function _getBashRisk(
|
|
342
342
|
event: PluginHookBeforeToolCallEvent,
|
|
343
343
|
_profile: { risk_paths: string[] }
|
|
344
344
|
): 'safe' | 'normal' | 'dangerous' | 'unknown' {
|
|
345
|
-
|
|
345
|
+
|
|
346
346
|
if (!BASH_TOOLS_SET.has(event.toolName)) return 'unknown';
|
|
347
347
|
try {
|
|
348
348
|
const command = String(event.params.command || event.params.args || '');
|
package/src/hooks/gfi-gate.ts
CHANGED
|
@@ -47,16 +47,16 @@ export interface GfiGateConfig {
|
|
|
47
47
|
/**
|
|
48
48
|
* Internal helper to call the shared block helper with gfi-gate source tag.
|
|
49
49
|
*/
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
function block(
|
|
52
52
|
wctx: WorkspaceContext,
|
|
53
53
|
filePath: string,
|
|
54
54
|
reason: string,
|
|
55
55
|
toolName: string,
|
|
56
56
|
sessionId: string | undefined,
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
logger?: { info?: (message: string) => void; warn?: (message: string) => void; error?: (message: string) => void }
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
): PluginHookBeforeToolCallResult {
|
|
61
61
|
return recordGateBlockAndReturn(wctx, {
|
|
62
62
|
filePath,
|
|
@@ -64,19 +64,19 @@ function block(
|
|
|
64
64
|
toolName,
|
|
65
65
|
sessionId,
|
|
66
66
|
blockSource: 'gfi-gate',
|
|
67
|
-
}, logger ||
|
|
67
|
+
}, logger ||
|
|
68
68
|
{ warn: () => {}, error: () => {} } as const);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
export function checkGfiGate(
|
|
73
73
|
event: PluginHookBeforeToolCallEvent,
|
|
74
74
|
wctx: WorkspaceContext,
|
|
75
75
|
sessionId: string | undefined,
|
|
76
76
|
config: GfiGateConfig,
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
logger?: { info?: (message: string) => void; warn?: (message: string) => void }
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
): PluginHookBeforeToolCallResult | undefined {
|
|
81
81
|
if (!config || config.enabled === false || !sessionId) {
|
|
82
82
|
return undefined;
|
package/src/hooks/lifecycle.ts
CHANGED
|
@@ -122,7 +122,7 @@ export async function extractPainFromSessionFile(sessionFile: string, ctx: Plugi
|
|
|
122
122
|
try {
|
|
123
123
|
rl.close();
|
|
124
124
|
fileStream.destroy();
|
|
125
|
-
} catch (_e) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
125
|
+
} catch (_e) { // eslint-disable-line @typescript-eslint/no-unused-vars -- Reason: intentionally unused - cleanup errors ignored
|
|
126
126
|
// Ignore cleanup errors
|
|
127
127
|
}
|
|
128
128
|
}
|
|
@@ -196,7 +196,7 @@ export async function handleBeforeCompaction(
|
|
|
196
196
|
await extractPainFromSessionFile(event.sessionFile, ctx);
|
|
197
197
|
|
|
198
198
|
// 新增:提取并保存工作记忆
|
|
199
|
-
|
|
199
|
+
|
|
200
200
|
await extractAndSaveWorkingMemory(event.sessionFile, ctx, wctx);
|
|
201
201
|
}
|
|
202
202
|
}
|
package/src/hooks/llm.ts
CHANGED
|
@@ -255,7 +255,7 @@ export function handleLlmOutput(
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// ═══ Thinking OS: Mental Model Usage Tracking ═══
|
|
258
|
-
|
|
258
|
+
|
|
259
259
|
trackThinkingModelUsage({
|
|
260
260
|
text,
|
|
261
261
|
wctx,
|
package/src/hooks/pain.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import { isRisky, normalizePath } from '../utils/io.js';
|
|
3
3
|
import { normalizeProfile } from '../core/profile.js';
|
|
4
4
|
import { computePainScore, buildPainFlag, writePainFlag, trackPrincipleValue } from '../core/pain.js';
|
|
5
|
-
import { getSession, trackFriction, resetFriction, getInjectedProbationIds, clearInjectedProbationIds } from '../core/session-tracker.js';
|
|
5
|
+
import { getSession, trackFriction, resetFriction, getInjectedProbationIds, clearInjectedProbationIds, type SessionState } from '../core/session-tracker.js';
|
|
6
6
|
import { denoiseError, computeHash } from '../utils/hashing.js';
|
|
7
7
|
import { SystemLogger } from '../core/system-logger.js';
|
|
8
8
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
@@ -10,6 +10,8 @@ import { getEvolutionLogger, createTraceId } from '../core/evolution-logger.js';
|
|
|
10
10
|
import { recordEvolutionSuccess, recordEvolutionFailure } from '../core/evolution-engine.js';
|
|
11
11
|
import type { EvolutionLoopEvent } from '../core/evolution-types.js';
|
|
12
12
|
import type { PluginHookAfterToolCallEvent, PluginHookToolContext, OpenClawPluginApi } from '../openclaw-sdk.js';
|
|
13
|
+
import { validateWorkspaceDir } from '../core/workspace-dir-validation.js';
|
|
14
|
+
import { resolveWorkspaceDir } from '../core/workspace-dir-service.js';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Interface for tool parameters to avoid 'any'
|
|
@@ -49,7 +51,9 @@ export function handleAfterToolCall(
|
|
|
49
51
|
ctx: PluginHookToolContext & { workspaceDir?: string; pluginConfig?: Record<string, unknown> },
|
|
50
52
|
api?: OpenClawPluginApi
|
|
51
53
|
): void {
|
|
52
|
-
const effectiveWorkspaceDir =
|
|
54
|
+
const effectiveWorkspaceDir = api
|
|
55
|
+
? resolveWorkspaceDir(api, ctx, { source: 'after_tool_call' })
|
|
56
|
+
: validateWorkspaceDir(ctx.workspaceDir) ? undefined : ctx.workspaceDir;
|
|
53
57
|
if (!effectiveWorkspaceDir) {
|
|
54
58
|
return;
|
|
55
59
|
}
|
|
@@ -123,10 +127,10 @@ export function handleAfterToolCall(
|
|
|
123
127
|
const hash = computeHash(denoised);
|
|
124
128
|
|
|
125
129
|
const deltaF = config.get('scores.tool_failure_friction') || 30;
|
|
126
|
-
const updatedState = trackFriction(sessionId, deltaF, hash, effectiveWorkspaceDir);
|
|
130
|
+
const updatedState = trackFriction(sessionId, deltaF, hash, effectiveWorkspaceDir, { source: 'tool_failure' });
|
|
127
131
|
|
|
128
132
|
// ── Trust Engine: Record failure ──
|
|
129
|
-
|
|
133
|
+
|
|
130
134
|
const errorType = extractErrorType(event.error || errorText);
|
|
131
135
|
const filePath = params.file_path || params.path || params.file;
|
|
132
136
|
const relPath = typeof filePath === 'string' ? normalizePath(filePath, effectiveWorkspaceDir) : 'unknown';
|
|
@@ -184,7 +188,23 @@ export function handleAfterToolCall(
|
|
|
184
188
|
clearInjectedProbationIds(sessionId, effectiveWorkspaceDir);
|
|
185
189
|
} else {
|
|
186
190
|
// ── SUCCESS BRANCH ──
|
|
187
|
-
|
|
191
|
+
// Only reduce tool_failure source GFI by 50%, preserve user_empathy and other sources
|
|
192
|
+
// This prevents "read file success" from wiping user frustration signals
|
|
193
|
+
const session = getSession(sessionId);
|
|
194
|
+
const toolFailureGfi = session?.gfiBySource?.['tool_failure'] || 0;
|
|
195
|
+
|
|
196
|
+
let resetState: SessionState;
|
|
197
|
+
if (toolFailureGfi > 0) {
|
|
198
|
+
// Reduce tool_failure source by 50% (relief from successful tool execution)
|
|
199
|
+
const reliefAmount = toolFailureGfi * 0.5;
|
|
200
|
+
resetState = resetFriction(sessionId, effectiveWorkspaceDir, {
|
|
201
|
+
source: 'tool_failure',
|
|
202
|
+
amount: reliefAmount,
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
// No tool_failure GFI to reduce, just get current state
|
|
206
|
+
resetState = session || resetFriction(sessionId, effectiveWorkspaceDir);
|
|
207
|
+
}
|
|
188
208
|
|
|
189
209
|
recordEvolutionSuccess(effectiveWorkspaceDir, event.toolName, {
|
|
190
210
|
sessionId,
|
|
@@ -86,15 +86,15 @@ export function buildEvolutionGateReason(
|
|
|
86
86
|
/**
|
|
87
87
|
* Internal helper to call the shared block helper with progressive-trust-gate source tag.
|
|
88
88
|
*/
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
function block(
|
|
91
91
|
filePath: string,
|
|
92
92
|
reason: string,
|
|
93
93
|
wctx: WorkspaceContext,
|
|
94
94
|
toolName: string,
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
logger: { warn?: (message: string) => void; error?: (message: string) => void },
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
sessionId?: string
|
|
99
99
|
): PluginHookBeforeToolCallResult {
|
|
100
100
|
return recordGateBlockAndReturn(wctx, {
|
|
@@ -119,16 +119,16 @@ function block(
|
|
|
119
119
|
* @param profile - Gate profile containing risk_paths config
|
|
120
120
|
* @returns PluginHookBeforeToolCallResult to block, or undefined to allow
|
|
121
121
|
*/
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
export function checkProgressiveTrustGate(
|
|
124
124
|
event: PluginHookBeforeToolCallEvent,
|
|
125
125
|
wctx: WorkspaceContext,
|
|
126
126
|
relPath: string,
|
|
127
127
|
risky: boolean,
|
|
128
128
|
lineChanges: number,
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
logger: { warn?: (message: string) => void; error?: (message: string) => void; info?: (message: string) => void },
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
ctx: { workspaceDir?: string; sessionId?: string },
|
|
133
133
|
profile?: { risk_paths: string[]; core_governance_files?: string[] }
|
|
134
134
|
): PluginHookBeforeToolCallResult | void {
|
|
@@ -151,7 +151,7 @@ export function checkProgressiveTrustGate(
|
|
|
151
151
|
});
|
|
152
152
|
|
|
153
153
|
const currentTier = epDecision.currentTier ?? 1;
|
|
154
|
-
|
|
154
|
+
|
|
155
155
|
const tierName = getTierName(currentTier);
|
|
156
156
|
|
|
157
157
|
logger.info?.(`[PD_GATE] EP Gate: Tier ${currentTier} (${tierName}), Tool: ${event.toolName}, Risk: ${risky}, Allowed: ${epDecision.allowed}`);
|
package/src/hooks/prompt.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import type { PluginHookBeforePromptBuildEvent, PluginHookAgentContext, PluginHookBeforePromptBuildResult, PluginLogger, OpenClawPluginApi } from '../openclaw-sdk.js';
|
|
4
|
-
import { clearInjectedProbationIds, getSession, resetFriction, setInjectedProbationIds, trackFriction } from '../core/session-tracker.js';
|
|
4
|
+
import { clearInjectedProbationIds, getSession, resetFriction, setInjectedProbationIds, trackFriction, decayGfi, getGfiDecayElapsed } from '../core/session-tracker.js';
|
|
5
5
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
6
6
|
import type { ContextInjectionConfig} from '../types.js';
|
|
7
7
|
import { defaultContextConfig } from '../types.js';
|
|
@@ -308,6 +308,13 @@ export async function handleBeforePromptBuild(
|
|
|
308
308
|
return;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
// ──── DEBUG: Verify subagent availability in this context ────
|
|
312
|
+
const subagent = ctx.api?.runtime?.subagent;
|
|
313
|
+
logger?.info?.(`[PD:DEBUG:SubagentCheck] trigger=${ctx.trigger}, subagent_exists=${!!subagent}, subagent.run_exists=${!!subagent?.run}`);
|
|
314
|
+
if (subagent?.run) {
|
|
315
|
+
logger?.info?.('[PD:DEBUG:SubagentCheck] run entrypoint is callable');
|
|
316
|
+
}
|
|
317
|
+
|
|
311
318
|
const wctx = WorkspaceContext.fromHookContext(ctx);
|
|
312
319
|
const { trigger, sessionId, api } = ctx;
|
|
313
320
|
if (sessionId) {
|
|
@@ -358,7 +365,7 @@ export async function handleBeforePromptBuild(
|
|
|
358
365
|
// appendSystemContext: Principles + Thinking OS + reflection_log + project_context (cacheable, WebUI-hidden)
|
|
359
366
|
// prependContext: Only short dynamic directives: evolutionDirective + heartbeat
|
|
360
367
|
|
|
361
|
-
|
|
368
|
+
|
|
362
369
|
let prependSystemContext = '';
|
|
363
370
|
let prependContext = '';
|
|
364
371
|
let appendSystemContext = '';
|
|
@@ -612,6 +619,18 @@ The empathy observer subagent handles pain detection independently.
|
|
|
612
619
|
|
|
613
620
|
// ──── 4. Heartbeat-specific checklist ────
|
|
614
621
|
if (trigger === 'heartbeat') {
|
|
622
|
+
// ──── 4a. GFI Time-based Decay ────
|
|
623
|
+
// Apply segmented exponential decay to GFI on each heartbeat
|
|
624
|
+
if (sessionId) {
|
|
625
|
+
const elapsedMinutes = getGfiDecayElapsed(sessionId);
|
|
626
|
+
if (elapsedMinutes >= 1) {
|
|
627
|
+
const decayedState = decayGfi(sessionId, elapsedMinutes);
|
|
628
|
+
if (decayedState) {
|
|
629
|
+
logger?.info?.(`[PD:GFI] Heartbeat decay applied: ${elapsedMinutes}min elapsed, GFI now ${decayedState.currentGfi.toFixed(1)}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
615
634
|
const heartbeatPath = wctx.resolve('HEARTBEAT');
|
|
616
635
|
if (fs.existsSync(heartbeatPath)) {
|
|
617
636
|
try {
|
|
@@ -628,7 +647,7 @@ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
|
|
|
628
647
|
}
|
|
629
648
|
|
|
630
649
|
// ──── 6. Dynamic Attitude Matrix (based on GFI) ────
|
|
631
|
-
|
|
650
|
+
|
|
632
651
|
let attitudeDirective = '';
|
|
633
652
|
const currentGfi = session?.currentGfi || 0;
|
|
634
653
|
|
|
@@ -853,10 +872,10 @@ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
|
|
|
853
872
|
const filePattern = /\b([a-zA-Z]:\\?[^\s,]+\.[a-z]{2,10}|[./][^\s,]+\.[a-z]{2,10})\b/gi;
|
|
854
873
|
const toolMatches = toolPatterns.flatMap(({ pattern, tool }) => {
|
|
855
874
|
const matches: string[] = [];
|
|
856
|
-
|
|
875
|
+
|
|
857
876
|
let _m;
|
|
858
877
|
const r = new RegExp(pattern.source, pattern.flags);
|
|
859
|
-
/* eslint-disable @typescript-eslint/no-unused-vars
|
|
878
|
+
/* eslint-disable @typescript-eslint/no-unused-vars -- Reason: regex exec side effect used, match variable intentionally unused */
|
|
860
879
|
while ((_m = r.exec(latestUserText)) !== null) matches.push(tool);
|
|
861
880
|
return matches;
|
|
862
881
|
});
|
package/src/hooks/subagent.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { WorkflowManager } from '../service/subagent-workflow/types.js';
|
|
|
12
12
|
* Factory to create the appropriate WorkflowManager by workflow_type string.
|
|
13
13
|
* Used by the subagent_ended hook to dispatch lifecycle recovery to the right manager.
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
function createWorkflowManagerForType(
|
|
17
17
|
workflowType: string,
|
|
18
18
|
workspaceDir: string,
|
|
@@ -23,7 +23,7 @@ function createWorkflowManagerForType(
|
|
|
23
23
|
info: (m: string) => logger.info(String(m)),
|
|
24
24
|
warn: (m: string) => logger.warn(String(m)),
|
|
25
25
|
error: (m: string) => logger.error(String(m)),
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
debug: () => {},
|
|
28
28
|
} as unknown as PluginLogger;
|
|
29
29
|
|
|
@@ -40,12 +40,12 @@ export interface ThinkingCheckpointConfig {
|
|
|
40
40
|
* @param logger - Optional logger for info messages
|
|
41
41
|
* @returns Block result if thinking required, undefined otherwise
|
|
42
42
|
*/
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
export function checkThinkingCheckpoint(
|
|
45
45
|
event: PluginHookBeforeToolCallEvent,
|
|
46
46
|
config: ThinkingCheckpointConfig,
|
|
47
47
|
sessionId: string | undefined,
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
logger?: { info?: (message: string) => void }
|
|
50
50
|
): PluginHookBeforeToolCallResult | undefined {
|
|
51
51
|
const enabled = config.enabled ?? false;
|
|
@@ -228,7 +228,7 @@ export function handleBeforeMessageWrite(
|
|
|
228
228
|
// 提取文本内容
|
|
229
229
|
let content = '';
|
|
230
230
|
if (typeof msg.content === 'string') {
|
|
231
|
-
|
|
231
|
+
|
|
232
232
|
// Reason: msg.content is string | ContentPart[]; destructuring would require renaming in the else branch
|
|
233
233
|
content = msg.content;
|
|
234
234
|
} else if (Array.isArray(msg.content)) {
|