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.
Files changed (129) hide show
  1. package/README.md +13 -5
  2. package/openclaw.plugin.json +4 -4
  3. package/package.json +1 -1
  4. package/src/commands/archive-impl.ts +3 -3
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +3 -3
  7. package/src/commands/disable-impl.ts +1 -1
  8. package/src/commands/evolution-status.ts +2 -2
  9. package/src/commands/focus.ts +2 -2
  10. package/src/commands/nocturnal-train.ts +6 -6
  11. package/src/commands/pain.ts +4 -4
  12. package/src/commands/pd-reflect.ts +87 -0
  13. package/src/commands/rollback-impl.ts +4 -4
  14. package/src/commands/rollback.ts +2 -2
  15. package/src/commands/samples.ts +2 -2
  16. package/src/commands/workflow-debug.ts +1 -1
  17. package/src/config/errors.ts +1 -1
  18. package/src/core/adaptive-thresholds.ts +1 -1
  19. package/src/core/code-implementation-storage.ts +2 -2
  20. package/src/core/config.ts +1 -1
  21. package/src/core/diagnostician-task-store.ts +2 -2
  22. package/src/core/empathy-keyword-matcher.ts +3 -3
  23. package/src/core/event-log.ts +5 -5
  24. package/src/core/evolution-engine.ts +4 -4
  25. package/src/core/evolution-logger.ts +1 -1
  26. package/src/core/evolution-reducer.ts +3 -3
  27. package/src/core/evolution-types.ts +5 -5
  28. package/src/core/external-training-contract.ts +1 -1
  29. package/src/core/focus-history.ts +14 -14
  30. package/src/core/hygiene/tracker.ts +1 -1
  31. package/src/core/init.ts +2 -2
  32. package/src/core/model-deployment-registry.ts +2 -2
  33. package/src/core/model-training-registry.ts +2 -2
  34. package/src/core/nocturnal-arbiter.ts +1 -1
  35. package/src/core/nocturnal-artificer.ts +2 -2
  36. package/src/core/nocturnal-candidate-scoring.ts +2 -2
  37. package/src/core/nocturnal-compliance.ts +3 -3
  38. package/src/core/nocturnal-dataset.ts +3 -3
  39. package/src/core/nocturnal-export.ts +4 -4
  40. package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
  41. package/src/core/nocturnal-snapshot-contract.ts +112 -0
  42. package/src/core/nocturnal-trajectory-extractor.ts +7 -5
  43. package/src/core/nocturnal-trinity.ts +27 -28
  44. package/src/core/pain-context-extractor.ts +3 -3
  45. package/src/core/pain.ts +124 -11
  46. package/src/core/path-resolver.ts +4 -4
  47. package/src/core/pd-task-reconciler.ts +10 -10
  48. package/src/core/pd-task-service.ts +1 -1
  49. package/src/core/pd-task-store.ts +1 -1
  50. package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
  51. package/src/core/principle-training-state.ts +2 -2
  52. package/src/core/principle-tree-ledger.ts +7 -7
  53. package/src/core/promotion-gate.ts +9 -9
  54. package/src/core/replay-engine.ts +12 -12
  55. package/src/core/risk-calculator.ts +1 -1
  56. package/src/core/rule-host-types.ts +2 -2
  57. package/src/core/rule-host.ts +5 -5
  58. package/src/core/schema/db-types.ts +1 -1
  59. package/src/core/schema/schema-definitions.ts +1 -1
  60. package/src/core/session-tracker.ts +96 -4
  61. package/src/core/shadow-observation-registry.ts +3 -3
  62. package/src/core/system-logger.ts +2 -2
  63. package/src/core/thinking-os-parser.ts +1 -1
  64. package/src/core/training-program.ts +2 -2
  65. package/src/core/trajectory.ts +8 -8
  66. package/src/core/workspace-context.ts +2 -2
  67. package/src/core/workspace-dir-service.ts +85 -0
  68. package/src/core/workspace-dir-validation.ts +30 -107
  69. package/src/hooks/bash-risk.ts +3 -3
  70. package/src/hooks/edit-verification.ts +4 -4
  71. package/src/hooks/gate-block-helper.ts +4 -4
  72. package/src/hooks/gate.ts +10 -10
  73. package/src/hooks/gfi-gate.ts +7 -7
  74. package/src/hooks/lifecycle.ts +2 -2
  75. package/src/hooks/llm.ts +1 -1
  76. package/src/hooks/pain.ts +25 -5
  77. package/src/hooks/progressive-trust-gate.ts +7 -7
  78. package/src/hooks/prompt.ts +24 -5
  79. package/src/hooks/subagent.ts +2 -2
  80. package/src/hooks/thinking-checkpoint.ts +2 -2
  81. package/src/hooks/trajectory-collector.ts +1 -1
  82. package/src/http/principles-console-route.ts +14 -6
  83. package/src/i18n/commands.ts +4 -0
  84. package/src/index.ts +181 -185
  85. package/src/service/central-health-service.ts +1 -1
  86. package/src/service/central-overview-service.ts +3 -3
  87. package/src/service/evolution-query-service.ts +1 -1
  88. package/src/service/evolution-worker.ts +209 -104
  89. package/src/service/health-query-service.ts +27 -17
  90. package/src/service/monitoring-query-service.ts +3 -3
  91. package/src/service/nocturnal-runtime.ts +4 -4
  92. package/src/service/nocturnal-service.ts +40 -23
  93. package/src/service/nocturnal-target-selector.ts +2 -2
  94. package/src/service/runtime-summary-service.ts +1 -1
  95. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
  96. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
  97. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
  98. package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
  99. package/src/service/subagent-workflow/types.ts +4 -4
  100. package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
  101. package/src/service/subagent-workflow/workflow-store.ts +2 -2
  102. package/src/tools/critique-prompt.ts +2 -3
  103. package/src/tools/deep-reflect.ts +17 -16
  104. package/src/tools/model-index.ts +1 -1
  105. package/src/utils/file-lock.ts +1 -1
  106. package/src/utils/io.ts +7 -2
  107. package/src/utils/nlp.ts +1 -1
  108. package/src/utils/plugin-logger.ts +2 -2
  109. package/src/utils/retry.ts +3 -2
  110. package/src/utils/subagent-probe.ts +20 -33
  111. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
  112. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
  113. package/templates/pain_settings.json +1 -1
  114. package/tests/build-artifacts.test.ts +4 -58
  115. package/tests/commands/pd-reflect.test.ts +49 -0
  116. package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
  117. package/tests/core/pain-auto-repair.test.ts +96 -0
  118. package/tests/core/pain-integration.test.ts +483 -0
  119. package/tests/core/pain.test.ts +5 -4
  120. package/tests/core/workspace-dir-service.test.ts +68 -0
  121. package/tests/core/workspace-dir-validation.test.ts +56 -192
  122. package/tests/hooks/pain.test.ts +20 -0
  123. package/tests/http/principles-console-route.test.ts +42 -20
  124. package/tests/integration/empathy-workflow-integration.test.ts +1 -2
  125. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
  126. package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
  127. package/tests/service/evolution-worker.nocturnal.test.ts +562 -6
  128. package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
  129. package/tests/utils/subagent-probe.test.ts +32 -0
@@ -1,152 +1,75 @@
1
1
  /**
2
2
  * WorkspaceDir Validation Utilities
3
3
  *
4
- * Provides runtime validation of workspaceDir to catch OpenClaw context bugs early.
5
- * When a hook receives an invalid workspaceDir, we warn immediately rather than
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
- // Check if it looks like a resolved '.' that went wrong
36
- // Common bad patterns:
26
+
27
+ const escapedHome = homeDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
37
28
  const badPatterns = [
38
- // Directly under home without a workspace subdirectory
39
- { pattern: new RegExp(`^${homeDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`), desc: 'is home directory itself' },
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; // Looks valid
38
+
39
+ return null;
50
40
  }
51
41
 
52
- /**
53
- * Try to resolve workspaceDir from agentId.
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
- onWarning: (msg: string) => void,
63
- ): string | null {
64
- try {
65
- const resolved = api.runtime.agent.resolveAgentWorkspaceDir(api.config, agentId);
66
- const issue = validateWorkspaceDir(resolved);
67
- if (issue) {
68
- onWarning(`agentId resolution returned invalid: "${resolved}" (${issue})`);
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
- * Validate a fallback workspaceDir and warn if invalid.
80
- * Returns the path regardless (it's the last resort).
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
- options?: { source?: string; onWarning?: (msg: string) => void },
110
- ): string {
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
  }
@@ -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
- /* eslint-disable @typescript-eslint/max-params -- Reason: Bash risk analysis requires command + pattern lists - refactoring to options object would be breaking API change */
41
+
42
42
  export function analyzeBashCommand(
43
43
  command: string,
44
44
  safePatterns: string[],
45
45
  dangerousPatterns: string[],
46
- logger?: { warn?: (/* eslint-disable-line no-unused-vars -- Reason: callback parameter, unused by design */_message: string) => void }
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
- // eslint-disable-next-line no-misleading-character-class -- Reason: zero-width character class ranges are intentional - documented in comment above
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
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: Hook handler signature requires event + context + config - refactoring would break plugin interface
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
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in try, catch has early return
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, no-unused-vars -- Reason: intentionally unused - let it fail naturally on path resolution error
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
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in try, all catch paths return early
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
- /* eslint-disable no-unused-vars -- Reason: type-only callback parameters in logger type */
51
+
52
52
  logger: { warn?: (_message: string) => void; error?: (_message: string) => void; info?: (_message: string) => void }
53
- /* eslint-enable no-unused-vars */
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
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function is defined later but called in this helper for retry logic
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
- /* eslint-disable @typescript-eslint/max-params, no-unused-vars -- Reason: Function requires all params for retry scheduling */
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
- // eslint-disable-next-line @typescript-eslint/prefer-destructuring -- Reason: mutationMatch[1] assigned to reassignable outer let - destructuring would shadow outer variable
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
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
170
+
171
171
  paramsSummary: _extractParamsSummary(event.params),
172
172
  },
173
173
  workspace: {
174
174
  isRiskPath: risky,
175
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
175
+
176
176
  planStatus: _getPlanStatus(ctx.workspaceDir),
177
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
177
+
178
178
  hasPlanFile: _hasPlanFile(ctx.workspaceDir),
179
179
  },
180
180
  session: {
181
181
  sessionId: ctx.sessionId,
182
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
182
+
183
183
  currentGfi: _getCurrentGfi(ctx.sessionId),
184
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
184
+
185
185
  recentThinking: _hasRecentThinking(ctx.sessionId),
186
186
  },
187
187
  evolution: {
188
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
188
+
189
189
  epTier: _getEpTier(wctx.workspaceDir),
190
190
  },
191
191
  derived: {
192
192
  estimatedLineChanges: estimateLineChanges({ toolName: event.toolName, params: event.params }),
193
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
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
- /* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: type-only parameter not used at runtime */
340
+
341
341
  function _getBashRisk(
342
342
  event: PluginHookBeforeToolCallEvent,
343
343
  _profile: { risk_paths: string[] }
344
344
  ): 'safe' | 'normal' | 'dangerous' | 'unknown' {
345
- /* eslint-enable no-unused-vars, @typescript-eslint/no-unused-vars */
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 || '');
@@ -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
- /* eslint-disable @typescript-eslint/max-params -- Reason: Helper function requires all params for block recording */
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
- /* eslint-disable no-unused-vars -- Reason: type-only callback params in logger type */
57
+
58
58
  logger?: { info?: (message: string) => void; warn?: (message: string) => void; error?: (message: string) => void }
59
- /* eslint-enable no-unused-vars */
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 || // eslint-disable-next-line @typescript-eslint/no-empty-function -- empty warn/error no-op
67
+ }, logger ||
68
68
  { warn: () => {}, error: () => {} } as const);
69
69
  }
70
70
 
71
- /* eslint-disable @typescript-eslint/max-params -- Reason: Gate function requires all params for comprehensive checks */
71
+
72
72
  export function checkGfiGate(
73
73
  event: PluginHookBeforeToolCallEvent,
74
74
  wctx: WorkspaceContext,
75
75
  sessionId: string | undefined,
76
76
  config: GfiGateConfig,
77
- /* eslint-disable no-unused-vars -- Reason: type-only callback params in logger type */
77
+
78
78
  logger?: { info?: (message: string) => void; warn?: (message: string) => void }
79
- /* eslint-enable no-unused-vars */
79
+
80
80
  ): PluginHookBeforeToolCallResult | undefined {
81
81
  if (!config || config.enabled === false || !sessionId) {
82
82
  return undefined;
@@ -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, no-unused-vars -- Reason: intentionally unused - cleanup errors ignored
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
- /* eslint-disable @typescript-eslint/no-use-before-define -- Reason: extractAndSaveWorkingMemory is defined later in this file */
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
- /* eslint-disable @typescript-eslint/no-use-before-define -- Reason: trackThinkingModelUsage is defined later in this file */
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 = ctx.workspaceDir || (api as unknown as { workspaceDir?: string })?.workspaceDir || api?.resolvePath?.('.');
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
- /* eslint-disable @typescript-eslint/no-use-before-define -- Reason: extractErrorType is defined later in this file */
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
- const resetState = resetFriction(sessionId, effectiveWorkspaceDir);
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
- /* eslint-disable @typescript-eslint/max-params -- Reason: Helper function requires all parameters for block recording */
89
+
90
90
  function block(
91
91
  filePath: string,
92
92
  reason: string,
93
93
  wctx: WorkspaceContext,
94
94
  toolName: string,
95
- /* eslint-disable no-unused-vars -- Reason: type-only callback params in logger type */
95
+
96
96
  logger: { warn?: (message: string) => void; error?: (message: string) => void },
97
- /* eslint-enable no-unused-vars */
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
- /* eslint-disable @typescript-eslint/max-params -- Reason: Gate function requires all parameters for comprehensive checks */
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
- /* eslint-disable no-unused-vars -- Reason: type-only callback params in logger type */
129
+
130
130
  logger: { warn?: (message: string) => void; error?: (message: string) => void; info?: (message: string) => void },
131
- /* eslint-enable no-unused-vars */
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
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
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}`);
@@ -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
- // eslint-disable-next-line no-useless-assignment -- Reason: initial value unused due to immediate reassignment
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
- // eslint-disable-next-line no-useless-assignment -- Reason: initial value unused due to immediate reassignment
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
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in while loop condition
875
+
857
876
  let _m;
858
877
  const r = new RegExp(pattern.source, pattern.flags);
859
- /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: regex exec side effect used, match variable intentionally unused */
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
  });
@@ -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
- /* eslint-disable @typescript-eslint/max-params -- Reason: Factory function requires workflowType, workspaceDir, logger, and subagent */
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
- // eslint-disable-next-line @typescript-eslint/no-empty-function -- debug logger no-op
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
- /* eslint-disable @typescript-eslint/max-params -- Reason: Checkpoint function requires event, config, sessionId, and logger */
43
+
44
44
  export function checkThinkingCheckpoint(
45
45
  event: PluginHookBeforeToolCallEvent,
46
46
  config: ThinkingCheckpointConfig,
47
47
  sessionId: string | undefined,
48
- /* eslint-disable no-unused-vars -- Reason: callback parameter in type annotation */
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
- /* eslint-disable @typescript-eslint/prefer-destructuring */
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)) {