principles-disciple 1.5.4

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 (189) hide show
  1. package/dist/commands/capabilities.d.ts +3 -0
  2. package/dist/commands/capabilities.js +73 -0
  3. package/dist/commands/evolver.d.ts +9 -0
  4. package/dist/commands/evolver.js +26 -0
  5. package/dist/commands/pain.d.ts +5 -0
  6. package/dist/commands/pain.js +114 -0
  7. package/dist/commands/strategy.d.ts +3 -0
  8. package/dist/commands/strategy.js +29 -0
  9. package/dist/commands/thinking-os.d.ts +2 -0
  10. package/dist/commands/thinking-os.js +162 -0
  11. package/dist/commands/trust.d.ts +4 -0
  12. package/dist/commands/trust.js +95 -0
  13. package/dist/core/agent-loader.d.ts +44 -0
  14. package/dist/core/agent-loader.js +147 -0
  15. package/dist/core/config-service.d.ts +15 -0
  16. package/dist/core/config-service.js +26 -0
  17. package/dist/core/config.d.ts +103 -0
  18. package/dist/core/config.js +186 -0
  19. package/dist/core/detection-funnel.d.ts +33 -0
  20. package/dist/core/detection-funnel.js +100 -0
  21. package/dist/core/detection-service.d.ts +15 -0
  22. package/dist/core/detection-service.js +28 -0
  23. package/dist/core/dictionary-service.d.ts +15 -0
  24. package/dist/core/dictionary-service.js +26 -0
  25. package/dist/core/dictionary.d.ts +36 -0
  26. package/dist/core/dictionary.js +136 -0
  27. package/dist/core/event-log.d.ts +53 -0
  28. package/dist/core/event-log.js +196 -0
  29. package/dist/core/evolution-engine.d.ts +119 -0
  30. package/dist/core/evolution-engine.js +542 -0
  31. package/dist/core/evolution-types.d.ts +126 -0
  32. package/dist/core/evolution-types.js +56 -0
  33. package/dist/core/hygiene/tracker.d.ts +22 -0
  34. package/dist/core/hygiene/tracker.js +106 -0
  35. package/dist/core/init.d.ts +12 -0
  36. package/dist/core/init.js +117 -0
  37. package/dist/core/migration.d.ts +6 -0
  38. package/dist/core/migration.js +90 -0
  39. package/dist/core/pain.d.ts +4 -0
  40. package/dist/core/pain.js +70 -0
  41. package/dist/core/path-resolver.d.ts +43 -0
  42. package/dist/core/path-resolver.js +259 -0
  43. package/dist/core/paths.d.ts +60 -0
  44. package/dist/core/paths.js +67 -0
  45. package/dist/core/profile.d.ts +62 -0
  46. package/dist/core/profile.js +210 -0
  47. package/dist/core/risk-calculator.d.ts +7 -0
  48. package/dist/core/risk-calculator.js +39 -0
  49. package/dist/core/session-tracker.d.ts +76 -0
  50. package/dist/core/session-tracker.js +286 -0
  51. package/dist/core/system-logger.d.ts +8 -0
  52. package/dist/core/system-logger.js +31 -0
  53. package/dist/core/trust-engine.d.ts +91 -0
  54. package/dist/core/trust-engine.js +284 -0
  55. package/dist/core/workspace-context.d.ts +64 -0
  56. package/dist/core/workspace-context.js +134 -0
  57. package/dist/hooks/gate.d.ts +6 -0
  58. package/dist/hooks/gate.js +487 -0
  59. package/dist/hooks/lifecycle.d.ts +5 -0
  60. package/dist/hooks/lifecycle.js +180 -0
  61. package/dist/hooks/llm.d.ts +4 -0
  62. package/dist/hooks/llm.js +153 -0
  63. package/dist/hooks/pain.d.ts +5 -0
  64. package/dist/hooks/pain.js +173 -0
  65. package/dist/hooks/prompt.d.ts +38 -0
  66. package/dist/hooks/prompt.js +285 -0
  67. package/dist/hooks/subagent.d.ts +2 -0
  68. package/dist/hooks/subagent.js +70 -0
  69. package/dist/i18n/commands.d.ts +26 -0
  70. package/dist/i18n/commands.js +88 -0
  71. package/dist/index.d.ts +7 -0
  72. package/dist/index.js +204 -0
  73. package/dist/service/evolution-worker.d.ts +17 -0
  74. package/dist/service/evolution-worker.js +293 -0
  75. package/dist/tools/agent-spawn.d.ts +33 -0
  76. package/dist/tools/agent-spawn.js +170 -0
  77. package/dist/tools/critique-prompt.d.ts +14 -0
  78. package/dist/tools/critique-prompt.js +81 -0
  79. package/dist/tools/deep-reflect.d.ts +19 -0
  80. package/dist/tools/deep-reflect.js +174 -0
  81. package/dist/tools/model-index.d.ts +9 -0
  82. package/dist/tools/model-index.js +82 -0
  83. package/dist/types/event-types.d.ts +229 -0
  84. package/dist/types/event-types.js +73 -0
  85. package/dist/types/hygiene-types.d.ts +20 -0
  86. package/dist/types/hygiene-types.js +12 -0
  87. package/dist/types.d.ts +1 -0
  88. package/dist/types.js +1 -0
  89. package/dist/utils/file-lock.d.ts +64 -0
  90. package/dist/utils/file-lock.js +270 -0
  91. package/dist/utils/glob-match.d.ts +28 -0
  92. package/dist/utils/glob-match.js +49 -0
  93. package/dist/utils/hashing.d.ts +9 -0
  94. package/dist/utils/hashing.js +25 -0
  95. package/dist/utils/io.d.ts +6 -0
  96. package/dist/utils/io.js +106 -0
  97. package/dist/utils/nlp.d.ts +9 -0
  98. package/dist/utils/nlp.js +59 -0
  99. package/dist/utils/plugin-logger.d.ts +39 -0
  100. package/dist/utils/plugin-logger.js +70 -0
  101. package/openclaw.plugin.json +46 -0
  102. package/package.json +63 -0
  103. package/templates/langs/en/core/AGENTS.md +206 -0
  104. package/templates/langs/en/core/BOOT.md +60 -0
  105. package/templates/langs/en/core/BOOTSTRAP.md +250 -0
  106. package/templates/langs/en/core/HEARTBEAT.md +74 -0
  107. package/templates/langs/en/core/IDENTITY.md +8 -0
  108. package/templates/langs/en/core/PRINCIPLES.md +10 -0
  109. package/templates/langs/en/core/SOUL.md +76 -0
  110. package/templates/langs/en/core/TOOLS.md +53 -0
  111. package/templates/langs/en/core/USER.md +10 -0
  112. package/templates/langs/en/pain/00_seed_samples.md +23 -0
  113. package/templates/langs/en/pain_dictionary.json +22 -0
  114. package/templates/langs/en/skills/admin/SKILL.md +40 -0
  115. package/templates/langs/en/skills/bootstrap-tools/SKILL.md +53 -0
  116. package/templates/langs/en/skills/deductive-audit/SKILL.md +36 -0
  117. package/templates/langs/en/skills/evolution-framework-update/SKILL.md +31 -0
  118. package/templates/langs/en/skills/evolve-system/SKILL.md +46 -0
  119. package/templates/langs/en/skills/evolve-task/SKILL.md +83 -0
  120. package/templates/langs/en/skills/feedback/SKILL.md +51 -0
  121. package/templates/langs/en/skills/init-strategy/SKILL.md +54 -0
  122. package/templates/langs/en/skills/inject-rule/SKILL.md +19 -0
  123. package/templates/langs/en/skills/manage-okr/SKILL.md +96 -0
  124. package/templates/langs/en/skills/pain/SKILL.md +19 -0
  125. package/templates/langs/en/skills/pd-daily/SKILL.md +199 -0
  126. package/templates/langs/en/skills/pd-grooming/SKILL.md +46 -0
  127. package/templates/langs/en/skills/pd-mentor/SKILL.md +230 -0
  128. package/templates/langs/en/skills/plan-script/SKILL.md +32 -0
  129. package/templates/langs/en/skills/profile/SKILL.md +24 -0
  130. package/templates/langs/en/skills/reflection/SKILL.md +40 -0
  131. package/templates/langs/en/skills/reflection-log/SKILL.md +37 -0
  132. package/templates/langs/en/skills/report/SKILL.md +13 -0
  133. package/templates/langs/en/skills/root-cause/SKILL.md +33 -0
  134. package/templates/langs/en/skills/triage/SKILL.md +29 -0
  135. package/templates/langs/en/skills/watch-evolution/SKILL.md +33 -0
  136. package/templates/langs/zh/core/AGENTS.md +207 -0
  137. package/templates/langs/zh/core/BOOT.md +60 -0
  138. package/templates/langs/zh/core/BOOTSTRAP.md +250 -0
  139. package/templates/langs/zh/core/HEARTBEAT.md +74 -0
  140. package/templates/langs/zh/core/IDENTITY.md +8 -0
  141. package/templates/langs/zh/core/SOUL.md +76 -0
  142. package/templates/langs/zh/core/TOOLS.md +53 -0
  143. package/templates/langs/zh/core/USER.md +10 -0
  144. package/templates/langs/zh/pain/00_seed_samples.md +24 -0
  145. package/templates/langs/zh/pain_dictionary.json +18 -0
  146. package/templates/langs/zh/skills/admin/SKILL.md +42 -0
  147. package/templates/langs/zh/skills/bootstrap-tools/SKILL.md +52 -0
  148. package/templates/langs/zh/skills/deductive-audit/SKILL.md +36 -0
  149. package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +31 -0
  150. package/templates/langs/zh/skills/evolve-system/SKILL.md +46 -0
  151. package/templates/langs/zh/skills/evolve-task/SKILL.md +83 -0
  152. package/templates/langs/zh/skills/feedback/SKILL.md +53 -0
  153. package/templates/langs/zh/skills/init-strategy/SKILL.md +54 -0
  154. package/templates/langs/zh/skills/inject-rule/SKILL.md +19 -0
  155. package/templates/langs/zh/skills/manage-okr/SKILL.md +109 -0
  156. package/templates/langs/zh/skills/pain/SKILL.md +19 -0
  157. package/templates/langs/zh/skills/pd-daily/SKILL.md +199 -0
  158. package/templates/langs/zh/skills/pd-grooming/SKILL.md +46 -0
  159. package/templates/langs/zh/skills/pd-mentor/SKILL.md +230 -0
  160. package/templates/langs/zh/skills/plan-script/SKILL.md +32 -0
  161. package/templates/langs/zh/skills/profile/SKILL.md +24 -0
  162. package/templates/langs/zh/skills/reflection/SKILL.md +40 -0
  163. package/templates/langs/zh/skills/reflection-log/SKILL.md +37 -0
  164. package/templates/langs/zh/skills/report/SKILL.md +13 -0
  165. package/templates/langs/zh/skills/root-cause/SKILL.md +33 -0
  166. package/templates/langs/zh/skills/triage/SKILL.md +29 -0
  167. package/templates/langs/zh/skills/watch-evolution/SKILL.md +33 -0
  168. package/templates/pain_dictionary.json +36 -0
  169. package/templates/pain_settings.json +77 -0
  170. package/templates/workspace/.principles/00-kernel.md +51 -0
  171. package/templates/workspace/.principles/DECISION_POLICY.json +44 -0
  172. package/templates/workspace/.principles/PRINCIPLES.md +20 -0
  173. package/templates/workspace/.principles/PROFILE.json +52 -0
  174. package/templates/workspace/.principles/PROFILE.schema.json +56 -0
  175. package/templates/workspace/.principles/THINKING_OS.md +64 -0
  176. package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +7 -0
  177. package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +9 -0
  178. package/templates/workspace/.principles/models/_INDEX.md +27 -0
  179. package/templates/workspace/.principles/models/first_principles.md +62 -0
  180. package/templates/workspace/.principles/models/marketing_4p.md +52 -0
  181. package/templates/workspace/.principles/models/porter_five.md +63 -0
  182. package/templates/workspace/.principles/models/swot.md +60 -0
  183. package/templates/workspace/.principles/models/user_story_map.md +63 -0
  184. package/templates/workspace/.state/WORKBOARD.json +4 -0
  185. package/templates/workspace/AUDIT.md +15 -0
  186. package/templates/workspace/PLAN.md +2 -0
  187. package/templates/workspace/okr/RECOVERY_PROTOCOL.md +56 -0
  188. package/templates/workspace/okr/TASK_CHANGES.jsonl +6 -0
  189. package/templates/workspace/okr/WEEK_TASKS.json +6 -0
@@ -0,0 +1,153 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { trackLlmOutput, recordThinkingCheckpoint } from '../core/session-tracker.js';
4
+ import { writePainFlag } from '../core/pain.js';
5
+ import { DetectionService } from '../core/detection-service.js';
6
+ import { WorkspaceContext } from '../core/workspace-context.js';
7
+ export function handleLlmOutput(event, ctx) {
8
+ if (!ctx.workspaceDir || !ctx.sessionId)
9
+ return;
10
+ const wctx = WorkspaceContext.fromHookContext(ctx);
11
+ const config = wctx.config;
12
+ const eventLog = wctx.eventLog;
13
+ // Track this turn in the core session memory
14
+ const state = trackLlmOutput(ctx.sessionId, event.usage, config, ctx.workspaceDir);
15
+ // We need actual assistant text to analyze
16
+ if (!event.assistantTexts || event.assistantTexts.length === 0)
17
+ return;
18
+ const text = event.assistantTexts.join('\n');
19
+ // ── Track B: Semantic Pain Detection (V1.3.0 Funnel) ──
20
+ const detectionService = DetectionService.get(wctx.stateDir);
21
+ const detection = detectionService.detect(text);
22
+ if (detection.detected) {
23
+ eventLog.recordRuleMatch(ctx.sessionId, {
24
+ ruleId: detection.ruleId || detection.source,
25
+ layer: detection.source === 'l1_exact' ? 'L1' : (detection.source === 'l2_cache' ? 'L2' : 'L3'),
26
+ severity: detection.severity || 0,
27
+ textPreview: text.substring(0, 100)
28
+ });
29
+ }
30
+ let painScore = detection.detected ? (detection.severity || 0) : 0;
31
+ let source = detection.detected
32
+ ? (detection.ruleId ? `llm_${detection.ruleId.toLowerCase()}` : `llm_${detection.source}`)
33
+ : '';
34
+ let matchedReason = detection.detected
35
+ ? `Agent triggered pain detection (Source: ${detection.source}${detection.ruleId ? `, Rule: ${detection.ruleId}` : ''})`
36
+ : '';
37
+ // 3. Paralysis Check (from session state tracker)
38
+ const stuckThreshold = config.get('thresholds.stuck_loops_trigger') || 3;
39
+ const inputThreshold = config.get('thresholds.cognitive_paralysis_input') || 4000;
40
+ const paralysisScore = config.get('scores.paralysis') || 40;
41
+ if (state.stuckLoops >= stuckThreshold && state.totalInputTokens > inputThreshold && painScore < paralysisScore) {
42
+ painScore = paralysisScore;
43
+ source = 'llm_paralysis';
44
+ matchedReason = `Agent is stuck in low-output loops (${state.stuckLoops} consecutive turns with tiny output but huge context), indicating cognitive paralysis.`;
45
+ }
46
+ // If a pain threshold is crossed, write the autonomous pain flag
47
+ const painTriggerThreshold = config.get('thresholds.pain_trigger') || 30;
48
+ if (painScore >= painTriggerThreshold) {
49
+ // Inject the actual text snippet that triggered this for the diagnostician to read later
50
+ const snippet = text.length > 200 ? text.substring(0, 100) + '...' + text.substring(text.length - 100) : text;
51
+ writePainFlag(ctx.workspaceDir, {
52
+ source,
53
+ score: String(painScore),
54
+ time: new Date().toISOString(),
55
+ reason: matchedReason,
56
+ is_risky: 'false',
57
+ trigger_text_preview: snippet
58
+ });
59
+ eventLog.recordPainSignal(ctx.sessionId, {
60
+ score: painScore,
61
+ source: source,
62
+ reason: matchedReason,
63
+ isRisky: false
64
+ });
65
+ }
66
+ // ═══ Thinking OS: Mental Model Usage Tracking ═══
67
+ trackThinkingModelUsage(text, wctx, ctx.sessionId);
68
+ }
69
+ const THINKING_MODEL_SIGNALS = {
70
+ 'T-01': [
71
+ /let me (first )?(understand|map|outline|survey|review the (structure|architecture|dependencies))/i,
72
+ /让我先(梳理|了解|画出|理解|查看)(一下)?(结构|架构|依赖|全貌)/,
73
+ ],
74
+ 'T-02': [
75
+ /(type|test|contract|schema|interface) (constraint|requirement|check|validation)/i,
76
+ /we (must|need to) (respect|follow|adhere to) the/i,
77
+ /(必须|需要).*?(遵守|符合|满足).*?(类型|测试|契约|接口|规范)/,
78
+ ],
79
+ 'T-03': [
80
+ /based on (the |this )?(evidence|logs?|output|error|stack trace|test result)/i,
81
+ /let me (check|verify|confirm|read|look at) (the |)(actual|source|code|file|log)/i,
82
+ /根据(日志|证据|输出|报错|堆栈|测试结果)/,
83
+ ],
84
+ 'T-04': [
85
+ /this (is|would be) (irreversible|destructive|permanent|not easily undone)/i,
86
+ /(reversible|can be undone|safely roll back)/i,
87
+ /(不可逆|破坏性|永久的|无法回滚|可以回滚|安全地撤销)/,
88
+ ],
89
+ 'T-05': [
90
+ /we (must|should) (not|never|avoid|prevent|ensure we don't)/i,
91
+ /(critical|important) (not to|that we don't|to avoid)/i,
92
+ /(绝不能|必须避免|不可以|禁止|确保不会)/,
93
+ ],
94
+ 'T-06': [
95
+ /(simpl(er|est|ify)|minimal|straightforward|lean) (approach|solution|fix|implementation)/i,
96
+ /(simple is better|keep it simple|no need to over)/i,
97
+ /(最简(单|洁)|精简|没有必要(过度|额外))/,
98
+ ],
99
+ 'T-07': [
100
+ /(minimal|smallest|narrowest|least) (change|diff|modification|impact)/i,
101
+ /only (change|modify|touch|edit) (the |what)/i,
102
+ /(最小(改动|变更|修改)|只(改|动|修))/,
103
+ ],
104
+ 'T-08': [
105
+ /this (error|failure|issue) (tells us|indicates|signals|suggests|means)/i,
106
+ /let me (stop|pause|step back|reconsider|rethink)/i,
107
+ /这个(错误|失败|问题)(告诉我们|表明|说明|意味)/,
108
+ /让我(停下|暂停|退一步|重新(考虑|思考|审视))/,
109
+ ],
110
+ 'T-09': [
111
+ /(break|split|decompose|divide) (this |the task |it )?(into|down)/i,
112
+ /(step 1|first,? (we|i|let's)|phase 1)/i,
113
+ /(拆分|分解|分步|分阶段|第一步)/,
114
+ ],
115
+ };
116
+ function trackThinkingModelUsage(text, wctx, sessionId) {
117
+ const logPath = wctx.resolve('THINKING_OS_USAGE');
118
+ const logDir = path.dirname(logPath);
119
+ if (!fs.existsSync(logDir))
120
+ fs.mkdirSync(logDir, { recursive: true });
121
+ let usageLog = {};
122
+ if (fs.existsSync(logPath)) {
123
+ try {
124
+ usageLog = JSON.parse(fs.readFileSync(logPath, 'utf8'));
125
+ }
126
+ catch (e) {
127
+ console.error(`[PD:LLM] Failed to parse thinking OS usage log: ${String(e)}`);
128
+ }
129
+ }
130
+ let anyMatch = false;
131
+ for (const [modelId, patterns] of Object.entries(THINKING_MODEL_SIGNALS)) {
132
+ for (const pattern of patterns) {
133
+ if (pattern.test(text)) {
134
+ usageLog[modelId] = (usageLog[modelId] || 0) + 1;
135
+ anyMatch = true;
136
+ break;
137
+ }
138
+ }
139
+ }
140
+ usageLog['_total_turns'] = (usageLog['_total_turns'] || 0) + 1;
141
+ if (anyMatch) {
142
+ // Record thinking checkpoint for gate enforcement
143
+ if (sessionId) {
144
+ recordThinkingCheckpoint(sessionId, wctx.workspaceDir);
145
+ }
146
+ try {
147
+ fs.writeFileSync(logPath, JSON.stringify(usageLog, null, 2), 'utf8');
148
+ }
149
+ catch (e) {
150
+ console.error(`[PD:LLM] Failed to write thinking OS usage log: ${String(e)}`);
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,5 @@
1
+ import type { PluginHookAfterToolCallEvent, PluginHookToolContext, OpenClawPluginApi } from '../openclaw-sdk.js';
2
+ export declare function handleAfterToolCall(event: PluginHookAfterToolCallEvent, ctx: PluginHookToolContext & {
3
+ workspaceDir?: string;
4
+ pluginConfig?: Record<string, unknown>;
5
+ }, api?: OpenClawPluginApi): void;
@@ -0,0 +1,173 @@
1
+ import * as fs from 'fs';
2
+ import { isRisky, normalizePath } from '../utils/io.js';
3
+ import { normalizeProfile } from '../core/profile.js';
4
+ import { computePainScore, writePainFlag } from '../core/pain.js';
5
+ import { trackFriction, resetFriction } from '../core/session-tracker.js';
6
+ import { denoiseError, computeHash } from '../utils/hashing.js';
7
+ import { SystemLogger } from '../core/system-logger.js';
8
+ import { WorkspaceContext } from '../core/workspace-context.js';
9
+ const WRITE_TOOLS = ['write', 'edit', 'apply_patch', 'write_file', 'edit_file', 'replace'];
10
+ export function handleAfterToolCall(event, ctx, api) {
11
+ const effectiveWorkspaceDir = ctx.workspaceDir || api?.workspaceDir || api?.resolvePath?.('.');
12
+ if (!effectiveWorkspaceDir) {
13
+ return;
14
+ }
15
+ const wctx = WorkspaceContext.fromHookContext({ ...ctx, workspaceDir: effectiveWorkspaceDir });
16
+ const config = wctx.config;
17
+ const eventLog = wctx.eventLog;
18
+ const trust = wctx.trust;
19
+ const sessionId = ctx.sessionId || 'unknown';
20
+ const params = event.params;
21
+ // ── Track A: Empirical Friction (GFI) ──
22
+ // 0. Special Case: Manual Pain Intervention
23
+ if (event.toolName === 'pain' || event.toolName === 'skill:pain') {
24
+ const reason = params.input || params.arguments || 'Manual intervention';
25
+ trackFriction(sessionId, 100, 'manual_pain', effectiveWorkspaceDir);
26
+ SystemLogger.log(effectiveWorkspaceDir, 'MANUAL_PAIN', `User manually triggered pain: ${reason}`);
27
+ eventLog.recordPainSignal(sessionId, {
28
+ score: 100,
29
+ source: 'manual',
30
+ reason: `User intervention: ${reason}`,
31
+ isRisky: true
32
+ });
33
+ return;
34
+ }
35
+ // 1. Determine if this was a failure
36
+ const exitCode = (event.result && typeof event.result === 'object') ? event.result.exitCode : 0;
37
+ const isFailure = !!event.error || (exitCode !== 0 && exitCode !== undefined);
38
+ if (isFailure) {
39
+ const errorText = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result));
40
+ const denoised = denoiseError(errorText);
41
+ const hash = computeHash(denoised);
42
+ const deltaF = config.get('scores.tool_failure_friction') || 30;
43
+ const updatedState = trackFriction(sessionId, deltaF, hash, effectiveWorkspaceDir);
44
+ // ── Trust Engine: Record failure ──
45
+ const errorType = extractErrorType(event.error || errorText);
46
+ const filePath = params.file_path || params.path || params.file;
47
+ const relPath = typeof filePath === 'string' ? normalizePath(filePath, effectiveWorkspaceDir) : 'unknown';
48
+ // Load profile for risk_paths check
49
+ const profilePath = wctx.resolve('PROFILE');
50
+ let profile = normalizeProfile({});
51
+ if (fs.existsSync(profilePath)) {
52
+ try {
53
+ profile = normalizeProfile(JSON.parse(fs.readFileSync(profilePath, 'utf8')));
54
+ }
55
+ catch (_e) { }
56
+ }
57
+ const isRisk = isRisky(relPath, profile.risk_paths);
58
+ trust.recordFailure(isRisk ? 'risky' : 'tool', {
59
+ sessionId,
60
+ api,
61
+ toolName: event.toolName // 👈 NEW: Pass toolName for classification
62
+ });
63
+ // Record tool call failure event
64
+ eventLog.recordToolCall(sessionId, {
65
+ toolName: event.toolName,
66
+ filePath: typeof filePath === 'string' ? filePath : undefined,
67
+ error: event.error ? String(event.error).substring(0, 200) : undefined,
68
+ errorType,
69
+ gfi: updatedState.currentGfi,
70
+ consecutiveErrors: updatedState.consecutiveErrors,
71
+ exitCode,
72
+ });
73
+ }
74
+ else {
75
+ // ── SUCCESS BRANCH ──
76
+ resetFriction(sessionId, effectiveWorkspaceDir);
77
+ // 👈 Record success to reset failure streak and earn minor trust (if constructive)
78
+ trust.recordSuccess('tool_success', {
79
+ sessionId,
80
+ api,
81
+ toolName: event.toolName // 👈 NEW: Pass toolName for classification
82
+ });
83
+ if (WRITE_TOOLS.includes(event.toolName)) {
84
+ const filePath = params.file_path || params.path || params.file;
85
+ eventLog.recordToolCall(sessionId, {
86
+ toolName: event.toolName,
87
+ filePath: typeof filePath === 'string' ? filePath : undefined,
88
+ gfi: 0,
89
+ });
90
+ // ── Hygiene Tracking: Record persistence actions ──
91
+ if (typeof filePath === 'string') {
92
+ const normalized = filePath.replace(/\\/g, '/');
93
+ const isMemory = /(?:^|\/)memory\//.test(normalized) || normalized.endsWith('/MEMORY.md') || normalized === 'MEMORY.md';
94
+ const isPlan = normalized === 'PLAN.md' || normalized.endsWith('/PLAN.md');
95
+ if (isMemory || isPlan) {
96
+ const content = params.content || params.new_string || '';
97
+ wctx.hygiene.recordPersistence({
98
+ ts: new Date().toISOString(),
99
+ tool: event.toolName,
100
+ path: filePath,
101
+ type: isMemory ? 'memory' : 'plan',
102
+ contentLength: content.length,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ // Special case for memory_store tool (Success only)
108
+ if (event.toolName === 'memory_store') {
109
+ const text = params.text || '';
110
+ wctx.hygiene.recordPersistence({
111
+ ts: new Date().toISOString(),
112
+ tool: event.toolName,
113
+ path: 'DATABASE',
114
+ type: 'memory',
115
+ contentLength: text.length,
116
+ });
117
+ }
118
+ }
119
+ // ── Legacy/Risky Write Pain Logic (Unified WRITE_TOOLS) ──
120
+ if (!WRITE_TOOLS.includes(event.toolName) || !isFailure) {
121
+ return;
122
+ }
123
+ const filePath = params.file_path || params.path || params.file;
124
+ const relPath = typeof filePath === 'string' ? normalizePath(filePath, effectiveWorkspaceDir) : 'unknown';
125
+ const profilePath = wctx.resolve('PROFILE');
126
+ let profile = normalizeProfile({});
127
+ if (fs.existsSync(profilePath)) {
128
+ try {
129
+ profile = normalizeProfile(JSON.parse(fs.readFileSync(profilePath, 'utf8')));
130
+ }
131
+ catch (_e) { }
132
+ }
133
+ const isRisk = isRisky(relPath, profile.risk_paths);
134
+ const painScore = computePainScore(1, false, false, isRisk ? 20 : 0, effectiveWorkspaceDir);
135
+ const painData = {
136
+ score: String(painScore),
137
+ source: 'tool_failure',
138
+ time: new Date().toISOString(),
139
+ reason: `Tool ${event.toolName} failed on ${relPath}. Error: ${event.error ?? 'Non-zero exit code'}`,
140
+ is_risky: String(isRisk),
141
+ };
142
+ writePainFlag(effectiveWorkspaceDir, painData);
143
+ eventLog.recordPainSignal(sessionId, {
144
+ score: painScore,
145
+ source: 'tool_failure',
146
+ reason: `Tool ${event.toolName} failed on ${relPath}`,
147
+ isRisky: isRisk,
148
+ });
149
+ }
150
+ function extractErrorType(error) {
151
+ if (!error)
152
+ return 'Unknown';
153
+ const msg = String(error);
154
+ if (msg.includes('EACCES') || msg.includes('permission denied'))
155
+ return 'EACCES';
156
+ if (msg.includes('ENOENT') || msg.includes('no such file'))
157
+ return 'ENOENT';
158
+ if (msg.includes('EISDIR'))
159
+ return 'EISDIR';
160
+ if (msg.includes('ENOSPC'))
161
+ return 'ENOSPC';
162
+ if (msg.includes('SyntaxError'))
163
+ return 'SyntaxError';
164
+ if (msg.includes('TypeError'))
165
+ return 'TypeError';
166
+ if (msg.includes('ReferenceError'))
167
+ return 'ReferenceError';
168
+ if (msg.includes('timeout') || msg.includes('ETIMEDOUT'))
169
+ return 'Timeout';
170
+ if (msg.includes('network') || msg.includes('ECONNREFUSED'))
171
+ return 'Network';
172
+ return 'Other';
173
+ }
@@ -0,0 +1,38 @@
1
+ import type { PluginHookBeforePromptBuildEvent, PluginHookAgentContext, PluginHookBeforePromptBuildResult, PluginLogger } from '../openclaw-sdk.js';
2
+ /**
3
+ * 代理默认配置
4
+ */
5
+ interface AgentsDefaultsConfig {
6
+ model?: unknown;
7
+ subagents?: {
8
+ model?: unknown;
9
+ };
10
+ }
11
+ /**
12
+ * OpenClaw API 接口定义(Prompt Hook 所需部分)
13
+ */
14
+ interface PromptHookApi {
15
+ config?: {
16
+ agents?: {
17
+ defaults?: AgentsDefaultsConfig;
18
+ };
19
+ };
20
+ logger: PluginLogger;
21
+ }
22
+ /**
23
+ * 从 OpenClaw 配置中解析模型选择
24
+ * 支持 string 或 { primary, fallbacks } 格式
25
+ * @internal 导出仅供测试使用
26
+ */
27
+ export declare function resolveModelFromConfig(modelConfig: unknown, logger?: PluginLogger): string | null;
28
+ /**
29
+ * 获取诊断子智能体应使用的模型
30
+ * 优先级:subagents.model > 主模型
31
+ * 如果都没有配置,抛出错误
32
+ * @internal 导出仅供测试使用
33
+ */
34
+ export declare function getDiagnosticianModel(api: PromptHookApi | null, logger?: PluginLogger): string;
35
+ export declare function handleBeforePromptBuild(event: PluginHookBeforePromptBuildEvent, ctx: PluginHookAgentContext & {
36
+ api?: PromptHookApi;
37
+ }): Promise<PluginHookBeforePromptBuildResult | void>;
38
+ export {};
@@ -0,0 +1,285 @@
1
+ import * as fs from 'fs';
2
+ import { getSession, resetFriction } from '../core/session-tracker.js';
3
+ import { WorkspaceContext } from '../core/workspace-context.js';
4
+ /**
5
+ * 验证模型字符串格式是否为 "provider/model"
6
+ */
7
+ function isValidModelFormat(model) {
8
+ // 格式: "provider/model" 或 "provider/model-variant"
9
+ // provider: 字母数字和连字符,不能以连字符开头/结尾
10
+ // model: 字母数字、连字符、点号、下划线
11
+ const MODEL_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]\/[a-zA-Z0-9._-]+$/;
12
+ return MODEL_PATTERN.test(model);
13
+ }
14
+ /**
15
+ * 从 OpenClaw 配置中解析模型选择
16
+ * 支持 string 或 { primary, fallbacks } 格式
17
+ * @internal 导出仅供测试使用
18
+ */
19
+ export function resolveModelFromConfig(modelConfig, logger) {
20
+ if (!modelConfig)
21
+ return null;
22
+ // 格式 1: "provider/model" 字符串
23
+ if (typeof modelConfig === 'string') {
24
+ const trimmed = modelConfig.trim();
25
+ if (!trimmed)
26
+ return null;
27
+ if (!isValidModelFormat(trimmed)) {
28
+ logger?.warn(`[PD:Prompt] Invalid model format: "${trimmed}". Expected "provider/model" format.`);
29
+ return null;
30
+ }
31
+ return trimmed;
32
+ }
33
+ // 格式 2: { primary: "provider/model", fallbacks: [...] } 对象
34
+ if (typeof modelConfig === 'object' && modelConfig !== null && !Array.isArray(modelConfig)) {
35
+ const cfg = modelConfig;
36
+ if (cfg.primary && typeof cfg.primary === 'string') {
37
+ const trimmed = cfg.primary.trim();
38
+ if (!trimmed)
39
+ return null;
40
+ if (!isValidModelFormat(trimmed)) {
41
+ logger?.warn(`[PD:Prompt] Invalid primary model format: "${trimmed}". Expected "provider/model" format.`);
42
+ return null;
43
+ }
44
+ return trimmed;
45
+ }
46
+ }
47
+ // 格式 3: 数组格式(不支持,发出警告)
48
+ if (Array.isArray(modelConfig)) {
49
+ console.warn(`[PD:Prompt] Array model config not supported. Expected "provider/model" string or { primary: "..." } object.`);
50
+ logger?.warn(`[PD:Prompt] Array model config not supported. Expected "provider/model" string or { primary: "..." } object.`);
51
+ return null;
52
+ }
53
+ return null;
54
+ }
55
+ /**
56
+ * 获取诊断子智能体应使用的模型
57
+ * 优先级:subagents.model > 主模型
58
+ * 如果都没有配置,抛出错误
59
+ * @internal 导出仅供测试使用
60
+ */
61
+ export function getDiagnosticianModel(api, logger) {
62
+ // 兼容两种调用方式:
63
+ // 1. 新方式:getDiagnosticianModel(api) - api 包含 logger
64
+ // 2. 旧方式:getDiagnosticianModel(api, logger) - 分离参数
65
+ const effectiveLogger = api?.logger || logger;
66
+ if (!effectiveLogger) {
67
+ throw new Error('[PD:Prompt] ERROR: Logger not available for getDiagnosticianModel');
68
+ }
69
+ const agentsConfig = api?.config?.agents?.defaults;
70
+ // 优先使用子智能体专用模型
71
+ const subagentModel = resolveModelFromConfig(agentsConfig?.subagents?.model, effectiveLogger);
72
+ if (subagentModel) {
73
+ effectiveLogger.info(`[PD:Prompt] Using subagents.model for diagnostician: ${subagentModel}`);
74
+ return subagentModel;
75
+ }
76
+ // 备选:使用主智能体模型
77
+ const primaryModel = resolveModelFromConfig(agentsConfig?.model, effectiveLogger);
78
+ if (primaryModel) {
79
+ effectiveLogger.info(`[PD:Prompt] Using primary model for diagnostician (subagents.model not set): ${primaryModel}`);
80
+ return primaryModel;
81
+ }
82
+ // 没有配置任何模型,报错
83
+ const errorMsg = `[PD:Prompt] ERROR: No model configured for diagnostician subagent. ` +
84
+ `Please set 'agents.defaults.subagents.model' or 'agents.defaults.model' in OpenClaw config.`;
85
+ effectiveLogger.error(errorMsg);
86
+ throw new Error(errorMsg);
87
+ }
88
+ export async function handleBeforePromptBuild(event, ctx) {
89
+ const workspaceDir = ctx.workspaceDir;
90
+ if (!workspaceDir)
91
+ return;
92
+ const wctx = WorkspaceContext.fromHookContext(ctx);
93
+ const { trigger, sessionId, api } = ctx;
94
+ const logger = api?.logger; // 统一获取 logger
95
+ // Minimal mode: heartbeat and subagents skip project context/system caps to reduce tokens
96
+ // SessionId format: "agent:main:subagent:{type}-{id}" for subagents, "agent:main:..." for main
97
+ const isMinimalMode = trigger === "heartbeat" || sessionId?.includes(":subagent:") === true;
98
+ const focusPath = wctx.resolve('CURRENT_FOCUS');
99
+ const painFlagPath = wctx.resolve('PAIN_FLAG');
100
+ const capsPath = wctx.resolve('SYSTEM_CAPABILITIES');
101
+ const config = wctx.config;
102
+ const session = sessionId ? getSession(sessionId) : undefined;
103
+ let prependSystemContext = '';
104
+ let prependContext = '';
105
+ let appendSystemContext = '';
106
+ // ═══ LAYER 0 (道之源): Core Principles - Highest Priority ═══
107
+ const principlesPath = wctx.resolve('PRINCIPLES');
108
+ if (fs.existsSync(principlesPath)) {
109
+ try {
110
+ const principles = fs.readFileSync(principlesPath, 'utf8');
111
+ if (principles.trim()) {
112
+ prependSystemContext = `<core_principles>\n${principles.trim()}\n</core_principles>`;
113
+ }
114
+ }
115
+ catch (e) {
116
+ logger?.error(`[PD:Prompt] Failed to read PRINCIPLES: ${String(e)}`);
117
+ }
118
+ }
119
+ // 0. Manual Pain Clearance
120
+ if (trigger === 'user' && sessionId && session && session.currentGfi >= 100) {
121
+ resetFriction(sessionId, workspaceDir);
122
+ }
123
+ // ═══ LAYER 3 (道): Thinking OS + Reflection Checkpoint ═══
124
+ // Both are static, cacheable content - put in prependSystemContext for provider caching
125
+ const thinkingOsPath = wctx.resolve('THINKING_OS');
126
+ if (fs.existsSync(thinkingOsPath)) {
127
+ try {
128
+ const thinkingOs = fs.readFileSync(thinkingOsPath, 'utf8');
129
+ if (thinkingOs.trim()) {
130
+ prependSystemContext += `\n<thinking_os>\n${thinkingOs.trim()}\n</thinking_os>`;
131
+ }
132
+ }
133
+ catch (e) {
134
+ logger?.error(`[PD:Prompt] Failed to read THINKING_OS: ${String(e)}`);
135
+ }
136
+ }
137
+ // 1. Critical Reflection Logic (High Priority - Prompt Injection)
138
+ const reflectionLogPath = wctx.resolve('REFLECTION_LOG');
139
+ if (fs.existsSync(reflectionLogPath)) {
140
+ try {
141
+ const reflectionLog = fs.readFileSync(reflectionLogPath, 'utf8');
142
+ if (reflectionLog.trim()) {
143
+ prependContext += `\n<reflection_log>\n${reflectionLog.trim()}\n</reflection_log>\n`;
144
+ }
145
+ }
146
+ catch (e) {
147
+ logger?.error(`[PD:Prompt] Failed to read REFLECTION_LOG: ${String(e)}`);
148
+ }
149
+ }
150
+ // 2. Strategic focus (skip in minimal mode)
151
+ if (!isMinimalMode) {
152
+ if (fs.existsSync(focusPath)) {
153
+ try {
154
+ const currentFocus = fs.readFileSync(focusPath, 'utf8');
155
+ if (currentFocus.trim()) {
156
+ prependContext += `\n<project_context>\n--- Strategic Focus ---\n${currentFocus.trim()}\n--- End of Strategic Focus ---\n</project_context>\n`;
157
+ }
158
+ }
159
+ catch (e) {
160
+ logger?.error(`[PD:Prompt] Failed to read CURRENT_FOCUS: ${String(e)}`);
161
+ }
162
+ }
163
+ }
164
+ // 3. Background Evolution Directives
165
+ let evolutionDirective = ''; // 用于存储进化指令,避免 return 导致上下文丢失
166
+ const queuePath = wctx.resolve('EVOLUTION_QUEUE');
167
+ if (fs.existsSync(queuePath)) {
168
+ try {
169
+ const queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
170
+ const inProgressTask = queue.find((t) => t.status === 'in_progress');
171
+ if (inProgressTask) {
172
+ // High-intensity directive to force the agent to work on the evolution task
173
+ let diagnosticianModel = null;
174
+ try {
175
+ if (api) {
176
+ diagnosticianModel = getDiagnosticianModel(api);
177
+ }
178
+ }
179
+ catch (err) {
180
+ // 模型配置缺失,记录错误但不 return,继续注入其他上下文
181
+ logger?.error(`[PD:Prompt] Failed to resolve diagnostician model: ${String(err)}`);
182
+ }
183
+ if (diagnosticianModel) {
184
+ // 转义任务内容,防止注入攻击
185
+ // 需要转义:反引号、$、反斜杠、双引号、换行符
186
+ const escapedTask = inProgressTask.task
187
+ .replace(/\\/g, '\\\\') // 先转义反斜杠
188
+ .replace(/`/g, '\\`') // 转义反引号(模板字符串)
189
+ .replace(/\$/g, '\\$') // 转义 $(模板字符串插值)
190
+ .replace(/"/g, '\\"') // 转义双引号
191
+ .replace(/\n/g, '\\n'); // 转义换行符
192
+ logger?.info(`[PD:Prompt] Injecting SYSTEM OVERRIDE for evolution task: ${inProgressTask.id}`);
193
+ evolutionDirective = `\n[🚨 SYSTEM OVERRIDE 🚨]\n` +
194
+ `A critical evolution task is assigned to you. YOU MUST PRIORITIZE THIS TASK.\n` +
195
+ `TASK: "${escapedTask}"\n\n` +
196
+ `ACTION REQUIRED:\n` +
197
+ `Reply ONLY with "[EVOLUTION_ACK]". Then immediately invoke the \`sessions_spawn\` tool:\n` +
198
+ `\`\`\`\n` +
199
+ `sessions_spawn target="diagnostician" message="${escapedTask}" model="${diagnosticianModel}"\n` +
200
+ `\`\`\`\n` +
201
+ `NO OTHER ACTIONS PERMITTED.`;
202
+ }
203
+ }
204
+ }
205
+ catch (e) {
206
+ logger?.error(`[PD:Prompt] Failed to parse EVOLUTION_QUEUE: ${String(e)}`);
207
+ }
208
+ }
209
+ // 4. Perceptive awareness: System Capabilities (skip in minimal mode)
210
+ if (!isMinimalMode) {
211
+ if (fs.existsSync(capsPath)) {
212
+ try {
213
+ const caps = fs.readFileSync(capsPath, 'utf8');
214
+ prependContext += `\n<system_capabilities>\n${caps}\n</system_capabilities>\n`;
215
+ }
216
+ catch (e) {
217
+ logger?.error(`[PD:Prompt] Failed to read SYSTEM_CAPABILITIES: ${String(e)}`);
218
+ }
219
+ }
220
+ }
221
+ // 5. Heartbeat-specific active checklist
222
+ if (trigger === 'heartbeat') {
223
+ const heartbeatPath = wctx.resolve('HEARTBEAT');
224
+ if (fs.existsSync(heartbeatPath)) {
225
+ try {
226
+ const heartbeatChecklist = fs.readFileSync(heartbeatPath, 'utf8');
227
+ prependContext += `\n<heartbeat_checklist>\n${heartbeatChecklist}\n\nDIRECTIVE: Perform a system-wide self-audit now. If everything is stable, strictly reply with "HEARTBEAT_OK" to minimize token usage.\n</heartbeat_checklist>\n`;
228
+ }
229
+ catch (e) {
230
+ logger?.error(`[PD:Prompt] Failed to read HEARTBEAT: ${String(e)}`);
231
+ }
232
+ }
233
+ }
234
+ // 6. Security Layer: Trust & Permission Awareness (Dynamic Content)
235
+ // 这些是动态内容,放入 <pd:internal_context> 以便 prependSystemContext 保持纯静态
236
+ const trustScore = wctx.trust.getScore();
237
+ const stage = wctx.trust.getStage();
238
+ const hygiene = wctx.hygiene.getStats();
239
+ // 1. 数值安全校验:防止异常值
240
+ // safeScore 范围: 0-100,safeStage 范围: 1-4(四个信任阶段)
241
+ const safeScore = Math.max(0, Math.min(100, Number(trustScore) || 0));
242
+ const safeStage = Math.max(1, Math.min(4, Number(stage) || 1));
243
+ // 2. 构建动态内部上下文(重命名 internalContext → dynamicContext)
244
+ let dynamicContext = '';
245
+ dynamicContext += `[CURRENT TRUST SCORE: ${safeScore}/100 (Stage ${safeStage})]\n`;
246
+ dynamicContext += `[COGNITIVE HYGIENE: ${hygiene.persistenceCount} persists today]\n`;
247
+ // 3. 视觉层次改进:Stage 1 使用更醒目的格式
248
+ if (safeStage === 1) {
249
+ dynamicContext += `\n[!CRITICAL!] Your trust score is critical. You are in read-only mode. Use diagnostician sub-agents to recover trust.\n`;
250
+ }
251
+ if (hygiene.persistenceCount === 0 && trigger === 'user') {
252
+ dynamicContext += `⚠️ ADVISORY: You haven't persisted any state today. To prevent "Goldfish Memory", consider updating PLAN.md or writing notes to memory/ if this session is becoming complex.\n`;
253
+ }
254
+ // 4. 使用命名空间前缀 (pd:internal_context)
255
+ if (dynamicContext.trim()) {
256
+ prependContext = `\n<pd:internal_context>\n${dynamicContext.trim()}\n</pd:internal_context>\n` + prependContext;
257
+ }
258
+ // 注入进化指令(如果有),放在 prependContext 最前面(高优先级)
259
+ if (evolutionDirective) {
260
+ prependContext = evolutionDirective + prependContext;
261
+ }
262
+ // ═══ SIZE GUARD: Prevent token explosion ═══
263
+ const totalSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
264
+ const MAX_SIZE = 10000;
265
+ if (totalSize > MAX_SIZE) {
266
+ const originalSize = totalSize;
267
+ // Truncate <project_context> to first 50 lines
268
+ const projectContextMatch = prependContext.match(/(<project_context>[\s\S]*?<\/project_context>)/);
269
+ if (projectContextMatch) {
270
+ const originalBlock = projectContextMatch[1];
271
+ const lines = originalBlock.split('\n');
272
+ if (lines.length > 50) {
273
+ const truncatedBlock = lines.slice(0, 50).join('\n') + '\n...[truncated]';
274
+ prependContext = prependContext.replace(originalBlock, truncatedBlock);
275
+ }
276
+ }
277
+ const newSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
278
+ logger?.warn(`[PD:Prompt] Injection size exceeded: ${originalSize} chars (limit: ${MAX_SIZE}), truncated to ${newSize} chars (${newSize - originalSize} saved)`);
279
+ }
280
+ return {
281
+ prependSystemContext,
282
+ prependContext,
283
+ appendSystemContext
284
+ };
285
+ }
@@ -0,0 +1,2 @@
1
+ import { PluginHookSubagentEndedEvent, PluginHookAgentContext } from '../openclaw-sdk.js';
2
+ export declare function handleSubagentEnded(event: PluginHookSubagentEndedEvent, ctx: PluginHookAgentContext): Promise<void>;