principles-disciple 1.5.4 → 1.7.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 (88) hide show
  1. package/dist/commands/context.d.ts +5 -0
  2. package/dist/commands/context.js +312 -0
  3. package/dist/commands/evolution-status.d.ts +4 -0
  4. package/dist/commands/evolution-status.js +138 -0
  5. package/dist/commands/export.d.ts +2 -0
  6. package/dist/commands/export.js +45 -0
  7. package/dist/commands/focus.d.ts +14 -0
  8. package/dist/commands/focus.js +582 -0
  9. package/dist/commands/pain.js +143 -6
  10. package/dist/commands/principle-rollback.d.ts +4 -0
  11. package/dist/commands/principle-rollback.js +22 -0
  12. package/dist/commands/rollback.d.ts +19 -0
  13. package/dist/commands/rollback.js +119 -0
  14. package/dist/commands/samples.d.ts +2 -0
  15. package/dist/commands/samples.js +55 -0
  16. package/dist/core/config.d.ts +37 -0
  17. package/dist/core/config.js +47 -0
  18. package/dist/core/control-ui-db.d.ts +68 -0
  19. package/dist/core/control-ui-db.js +274 -0
  20. package/dist/core/detection-funnel.d.ts +1 -1
  21. package/dist/core/detection-funnel.js +4 -0
  22. package/dist/core/dictionary.d.ts +2 -0
  23. package/dist/core/dictionary.js +13 -0
  24. package/dist/core/event-log.d.ts +22 -1
  25. package/dist/core/event-log.js +319 -0
  26. package/dist/core/evolution-engine.d.ts +5 -5
  27. package/dist/core/evolution-engine.js +18 -18
  28. package/dist/core/evolution-migration.d.ts +5 -0
  29. package/dist/core/evolution-migration.js +65 -0
  30. package/dist/core/evolution-reducer.d.ts +69 -0
  31. package/dist/core/evolution-reducer.js +369 -0
  32. package/dist/core/evolution-types.d.ts +103 -0
  33. package/dist/core/focus-history.d.ts +65 -0
  34. package/dist/core/focus-history.js +266 -0
  35. package/dist/core/init.js +30 -7
  36. package/dist/core/migration.js +0 -2
  37. package/dist/core/path-resolver.d.ts +3 -0
  38. package/dist/core/path-resolver.js +90 -31
  39. package/dist/core/paths.d.ts +7 -8
  40. package/dist/core/paths.js +48 -40
  41. package/dist/core/profile.js +1 -1
  42. package/dist/core/session-tracker.d.ts +4 -0
  43. package/dist/core/session-tracker.js +15 -0
  44. package/dist/core/thinking-models.d.ts +38 -0
  45. package/dist/core/thinking-models.js +170 -0
  46. package/dist/core/trajectory.d.ts +184 -0
  47. package/dist/core/trajectory.js +817 -0
  48. package/dist/core/trust-engine.d.ts +2 -0
  49. package/dist/core/trust-engine.js +30 -4
  50. package/dist/core/workspace-context.d.ts +13 -0
  51. package/dist/core/workspace-context.js +50 -7
  52. package/dist/hooks/gate.js +301 -30
  53. package/dist/hooks/llm.d.ts +8 -0
  54. package/dist/hooks/llm.js +347 -69
  55. package/dist/hooks/message-sanitize.d.ts +3 -0
  56. package/dist/hooks/message-sanitize.js +37 -0
  57. package/dist/hooks/pain.js +105 -5
  58. package/dist/hooks/prompt.d.ts +20 -11
  59. package/dist/hooks/prompt.js +558 -158
  60. package/dist/hooks/subagent.d.ts +9 -2
  61. package/dist/hooks/subagent.js +40 -3
  62. package/dist/http/principles-console-route.d.ts +2 -0
  63. package/dist/http/principles-console-route.js +257 -0
  64. package/dist/i18n/commands.js +48 -20
  65. package/dist/index.js +264 -8
  66. package/dist/service/control-ui-query-service.d.ts +217 -0
  67. package/dist/service/control-ui-query-service.js +537 -0
  68. package/dist/service/empathy-observer-manager.d.ts +42 -0
  69. package/dist/service/empathy-observer-manager.js +147 -0
  70. package/dist/service/evolution-worker.d.ts +10 -0
  71. package/dist/service/evolution-worker.js +156 -24
  72. package/dist/service/trajectory-service.d.ts +2 -0
  73. package/dist/service/trajectory-service.js +15 -0
  74. package/dist/tools/agent-spawn.d.ts +27 -6
  75. package/dist/tools/agent-spawn.js +339 -87
  76. package/dist/tools/deep-reflect.d.ts +27 -7
  77. package/dist/tools/deep-reflect.js +282 -113
  78. package/dist/types/event-types.d.ts +84 -2
  79. package/dist/types/event-types.js +33 -0
  80. package/dist/types.d.ts +52 -0
  81. package/dist/types.js +24 -1
  82. package/openclaw.plugin.json +43 -11
  83. package/package.json +16 -6
  84. package/templates/langs/zh/core/HEARTBEAT.md +28 -4
  85. package/templates/langs/zh/skills/pd-daily/SKILL.md +97 -13
  86. package/templates/pain_settings.json +54 -2
  87. package/templates/workspace/.principles/PROFILE.json +2 -0
  88. package/templates/workspace/okr/CURRENT_FOCUS.md +57 -0
@@ -0,0 +1,582 @@
1
+ /**
2
+ * /pd-focus 命令 - 箔理 CURRENT_FOCUS.md
3
+ *
4
+ * 功能:
5
+ * - status: ęŸ„ēœ‹å½“å‰ēŠ¶ę€å’ŒåŽ†å²ē‰ˆęœ¬
6
+ * - compress: ę‰‹åŠØåŽ‹ē¼©å¹¶å¤‡ä»½
7
+ * - history: ęŸ„ēœ‹åŽ†å²ē‰ˆęœ¬åˆ—č”Ø
8
+ * - rollback: å›žę»šåˆ°ęŒ‡å®šåŽ†å²ē‰ˆęœ¬
9
+ */
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import { randomUUID } from 'node:crypto';
13
+ import { WorkspaceContext } from '../core/workspace-context.js';
14
+ import { getHistoryDir, backupToHistory, cleanupHistory, extractVersion, extractDate, } from '../core/focus-history.js';
15
+ import { createAgentSpawnTool } from '../tools/agent-spawn.js';
16
+ /**
17
+ * 清理 Markdown ä»£ē å—å›“ę 
18
+ * ē§»é™¤å¼€å¤“ēš„ ```lang å’Œē»“å°¾ēš„ ```
19
+ */
20
+ function stripMarkdownFence(content) {
21
+ let result = content.trim();
22
+ // ē§»é™¤å¼€å¤“ēš„ä»£ē å—ę ‡č®°ļ¼ˆå¦‚ ```markdown, ```text 等)
23
+ result = result.replace(/^```[\w]*\n?/, '');
24
+ // ē§»é™¤ē»“å°¾ēš„ä»£ē å—ę ‡č®°
25
+ result = result.replace(/\n?```$/, '');
26
+ return result.trim();
27
+ }
28
+ /**
29
+ * čŽ·å–å·„ä½œåŒŗē›®å½•
30
+ */
31
+ function getWorkspaceDir(ctx) {
32
+ const workspaceDir = ctx.config?.workspaceDir;
33
+ if (!workspaceDir) {
34
+ throw new Error('[PD:Focus] workspaceDir is required but not provided');
35
+ }
36
+ return workspaceDir;
37
+ }
38
+ /**
39
+ * åŽ‹ē¼© CURRENT_FOCUS内容
40
+ *
41
+ * č§„åˆ™ļ¼š
42
+ * - äæē•™ļ¼šę ‡é¢˜ć€å…ƒę•°ę®ć€ēŠ¶ę€åæ«ē…§
43
+ * - äæē•™ļ¼šäø‹äø€ę­„ē« čŠ‚ļ¼ˆå®Œę•“ļ¼‰
44
+ * - äæē•™ļ¼šå½“å‰ä»»åŠ”äø­ęœŖå®Œęˆēš„é”¹ļ¼ˆ- [ ])
45
+ * - ē§»é™¤ļ¼šå½“å‰ä»»åŠ”äø­å·²å®Œęˆēš„é”¹ļ¼ˆ- [x])超过 3 äøŖę—¶
46
+ * - ē§»é™¤ļ¼šP0 ē« čŠ‚å¦‚ęžœå…ØéƒØå®Œęˆ
47
+ * - äæē•™ļ¼šå‚č€ƒē« čŠ‚
48
+ */
49
+ function compressFocusContent(content) {
50
+ const lines = content.split('\n');
51
+ const result = [];
52
+ let currentSection = '';
53
+ let inP0Section = false;
54
+ let p0AllCompleted = true;
55
+ let p0Lines = [];
56
+ let completedCount = 0;
57
+ // č¾…åŠ©å‡½ę•°ļ¼šåˆ·ę–° P0 ē« čŠ‚ē¼“å­˜
58
+ const flushP0Lines = (skipIfCompleted) => {
59
+ if (inP0Section && p0Lines.length > 0) {
60
+ if (!skipIfCompleted || !p0AllCompleted) {
61
+ // P0 ęœ‰ęœŖå®Œęˆä»»åŠ”ļ¼Œäæē•™ P0 内容
62
+ result.push(...p0Lines);
63
+ }
64
+ // é‡ē½®ēŠ¶ę€
65
+ p0Lines = [];
66
+ inP0Section = false;
67
+ p0AllCompleted = true;
68
+ }
69
+ };
70
+ for (let i = 0; i < lines.length; i++) {
71
+ const line = lines[i];
72
+ const trimmedLine = line.trim();
73
+ // čÆ†åˆ«ē« čŠ‚
74
+ if (/^#{1,3}\s*.*ēŠ¶ę€åæ«ē…§|šŸ“/.test(trimmedLine)) {
75
+ flushP0Lines(true); // P0 å®Œęˆę—¶č·³čæ‡
76
+ currentSection = 'snapshot';
77
+ }
78
+ else if (/^###\s*P0/i.test(trimmedLine)) {
79
+ currentSection = 'current_p0';
80
+ inP0Section = true;
81
+ p0AllCompleted = true;
82
+ p0Lines = [line];
83
+ continue;
84
+ }
85
+ else if (/^###\s*P[1-9]/i.test(trimmedLine)) {
86
+ // 离开 P0 ē« čŠ‚
87
+ flushP0Lines(true); // P0 å®Œęˆę—¶č·³čæ‡ļ¼ŒęœŖå®Œęˆę—¶äæē•™
88
+ currentSection = 'current';
89
+ result.push(line);
90
+ continue;
91
+ }
92
+ else if (/^#{1,3}\s*.*当前任劔|šŸ”„/.test(trimmedLine)) {
93
+ flushP0Lines(true); // P0 å®Œęˆę—¶č·³čæ‡
94
+ currentSection = 'current';
95
+ }
96
+ else if (/^#{1,3}\s*.*下一歄|āž”ļø/.test(trimmedLine)) {
97
+ flushP0Lines(false); // äæē•™ęœŖå®Œęˆēš„ P0
98
+ currentSection = 'nextSteps';
99
+ }
100
+ else if (/^#{1,3}\s*.*å‚č€ƒ|šŸ“Ž/.test(trimmedLine)) {
101
+ flushP0Lines(false); // äæē•™ęœŖå®Œęˆēš„ P0
102
+ currentSection = 'reference';
103
+ }
104
+ // 处理P0ē« čŠ‚
105
+ if (inP0Section) {
106
+ p0Lines.push(line);
107
+ // ę£€ęŸ„ę˜Æå¦ęœ‰ęœŖå®Œęˆä»»åŠ”
108
+ if (/^-\s*\[\s*\]/.test(trimmedLine)) {
109
+ p0AllCompleted = false;
110
+ }
111
+ continue;
112
+ }
113
+ // å¤„ē†å½“å‰ä»»åŠ”ē« čŠ‚äø­å·²å®Œęˆēš„é”¹
114
+ if (currentSection === 'current') {
115
+ if (/^-\s*\[x\]/i.test(trimmedLine)) {
116
+ completedCount++;
117
+ // å¦‚ęžœå·²å®Œęˆé”¹č¶…čæ‡ 3 äøŖļ¼Œč·³čæ‡
118
+ if (completedCount > 3) {
119
+ continue;
120
+ }
121
+ }
122
+ }
123
+ result.push(line);
124
+ }
125
+ // å¾ŖēŽÆē»“ęŸåŽļ¼Œåˆ·ę–°å‰©ä½™ēš„ P0 ē« čŠ‚
126
+ flushP0Lines(false); // äæē•™ęœŖå®Œęˆēš„ P0
127
+ return result.join('\n');
128
+ }
129
+ /**
130
+ * 显示 CURRENT_FOCUS ēŠ¶ę€
131
+ */
132
+ function showStatus(workspaceDir, isZh) {
133
+ const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
134
+ const focusPath = wctx.resolve('CURRENT_FOCUS');
135
+ const historyDir = getHistoryDir(focusPath);
136
+ if (!fs.existsSync(focusPath)) {
137
+ return isZh
138
+ ? 'āš ļø CURRENT_FOCUS.md äøå­˜åœØ\n\nšŸ’” čÆ·å…ˆčæč”Œ `/pd-init` åˆå§‹åŒ–å·„ä½œåŒŗ'
139
+ : 'āš ļø CURRENT_FOCUS.md does not exist\n\nšŸ’” Run `/pd-init` to initialize workspace first';
140
+ }
141
+ const content = fs.readFileSync(focusPath, 'utf-8');
142
+ const version = extractVersion(content);
143
+ const date = extractDate(content);
144
+ const lines = content.split('\n').length;
145
+ // ē»Ÿč®”åŽ†å²ē‰ˆęœ¬
146
+ let historyCount = 0;
147
+ if (fs.existsSync(historyDir)) {
148
+ historyCount = fs.readdirSync(historyDir).filter(f => f.startsWith('CURRENT_FOCUS.v')).length;
149
+ }
150
+ if (isZh) {
151
+ return `šŸ“„ **CURRENT_FOCUS.md ēŠ¶ę€**
152
+
153
+ | å±žę€§ | 值 |
154
+ |------|-----|
155
+ | ē‰ˆęœ¬ | v${version} |
156
+ | ę›“ę–°ę—„ęœŸ | ${date} |
157
+ | č”Œę•° | ${lines} 蔌 |
158
+ | åŽ†å²ē‰ˆęœ¬ | ${historyCount} äøŖ |
159
+
160
+ šŸ’” 输兄 \`/pd-focus history\` ęŸ„ēœ‹åŽ†å²ē‰ˆęœ¬
161
+ šŸ’” 输兄 \`/pd-focus compress\` ę‰‹åŠØåŽ‹ē¼©`;
162
+ }
163
+ return `šŸ“„ **CURRENT_FOCUS.md Status**
164
+
165
+ | Property | Value |
166
+ |----------|-------|
167
+ | Version | v${version} |
168
+ | Updated | ${date} |
169
+ | Lines | ${lines} lines |
170
+ | History | ${historyCount} versions |
171
+
172
+ šŸ’” Type \`/pd-focus history\` to view history
173
+ šŸ’” Type \`/pd-focus compress\` to compress manually`;
174
+ }
175
+ /**
176
+ * ę˜¾ē¤ŗåŽ†å²ē‰ˆęœ¬åˆ—č”Ø
177
+ */
178
+ function showHistory(workspaceDir, isZh) {
179
+ const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
180
+ const focusPath = wctx.resolve('CURRENT_FOCUS');
181
+ const historyDir = getHistoryDir(focusPath);
182
+ if (!fs.existsSync(historyDir)) {
183
+ return isZh
184
+ ? 'šŸ“­ ęš‚ę— åŽ†å²ē‰ˆęœ¬\n\nšŸ’” åŽ†å²ē‰ˆęœ¬åœØåŽ‹ē¼© CURRENT_FOCUS.md ę—¶č‡ŖåŠØåˆ›å»ŗ'
185
+ : 'šŸ“­ No history versions yet\n\nšŸ’” History is created when CURRENT_FOCUS.md is compressed';
186
+ }
187
+ const files = fs.readdirSync(historyDir)
188
+ .filter(f => f.startsWith('CURRENT_FOCUS.v') && f.endsWith('.md'))
189
+ .map(f => {
190
+ const filePath = path.join(historyDir, f);
191
+ const stat = fs.statSync(filePath);
192
+ const content = fs.readFileSync(filePath, 'utf-8');
193
+ return {
194
+ name: f,
195
+ version: extractVersion(content),
196
+ date: extractDate(content),
197
+ mtime: stat.mtime,
198
+ lines: content.split('\n').length,
199
+ };
200
+ })
201
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
202
+ if (files.length === 0) {
203
+ return isZh ? 'šŸ“­ ęš‚ę— åŽ†å²ē‰ˆęœ¬' : 'šŸ“­ No history versions';
204
+ }
205
+ const lines = files.slice(0, 10).map((f, i) => {
206
+ const num = i + 1;
207
+ return isZh
208
+ ? `${num}. \`${f.name}\` - v${f.version} (${f.date}, ${f.lines} 蔌)`
209
+ : `${num}. \`${f.name}\` - v${f.version} (${f.date}, ${f.lines} lines)`;
210
+ });
211
+ const header = isZh
212
+ ? `šŸ“š **åŽ†å²ē‰ˆęœ¬åˆ—č”Ø** (ęœ€čæ‘ ${Math.min(files.length, 10)} äøŖ)\n`
213
+ : `šŸ“š **History Versions** (Last ${Math.min(files.length, 10)})\n`;
214
+ const footer = isZh
215
+ ? `\n\nšŸ’” 输兄 \`/pd-focus rollback <åŗå·>\` å›žę»šåˆ°ęŒ‡å®šē‰ˆęœ¬`
216
+ : `\n\nšŸ’” Type \`/pd-focus rollback <number>\` to rollback`;
217
+ return header + '\n' + lines.join('\n') + footer;
218
+ }
219
+ /**
220
+ * ę‰‹åŠØåŽ‹ē¼© CURRENT_FOCUS.mdļ¼ˆä½æē”Øå­ę™ŗčƒ½ä½“ļ¼‰
221
+ */
222
+ async function compressFocus(workspaceDir, isZh, api) {
223
+ const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
224
+ const focusPath = wctx.resolve('CURRENT_FOCUS');
225
+ if (!fs.existsSync(focusPath)) {
226
+ return isZh
227
+ ? 'āŒ CURRENT_FOCUS.md äøå­˜åœØ'
228
+ : 'āŒ CURRENT_FOCUS.md does not exist';
229
+ }
230
+ const oldContent = fs.readFileSync(focusPath, 'utf-8');
231
+ const oldVersion = extractVersion(oldContent);
232
+ const oldDate = extractDate(oldContent);
233
+ const oldLines = oldContent.split('\n').length;
234
+ // ę£€ęŸ„ę˜Æå¦éœ€č¦åŽ‹ē¼©
235
+ if (oldLines <= 40) {
236
+ return isZh
237
+ ? `āœ… å½“å‰ę–‡ä»¶ä»… ${oldLines} č”Œļ¼Œę— éœ€åŽ‹ē¼©\n\nšŸ’” ę–‡ä»¶å°‘äŗŽ 40 č”Œę—¶äøå»ŗč®®åŽ‹ē¼©`
238
+ : `āœ… Current file has only ${oldLines} lines, no need to compress\n\nšŸ’” Compression not recommended for files under 40 lines`;
239
+ }
240
+ // å¤‡ä»½å½“å‰ē‰ˆęœ¬
241
+ const backupPath = backupToHistory(focusPath, oldContent);
242
+ // ęø…ē†čæ‡ęœŸåŽ†å²
243
+ cleanupHistory(focusPath);
244
+ // ä½æē”Øå­ę™ŗčƒ½ä½“čæ›č”Œę™ŗčƒ½åŽ‹ē¼©
245
+ // čŽ·å– MEMORY.md 路径
246
+ const memoryPath = wctx.resolve('MEMORY_MD');
247
+ const compressPrompt = isZh
248
+ ? `ä½ ę˜Æäø€äøŖäø“äøšēš„é”¹ē›®ę–‡ę”£åŽ‹ē¼©åŠ©ę‰‹ć€‚čÆ·åŽ‹ē¼©ä»„äø‹ CURRENT_FOCUS.md 文件内容。
249
+
250
+ **é‡č¦ļ¼šåœØåŽ‹ē¼©å‰ļ¼Œä½ éœ€č¦ęå–é‡č¦é‡ŒēØ‹ē¢‘äæ”ęÆļ¼**
251
+
252
+ **ē¬¬äø€ę­„ļ¼šęå–é‡ŒēØ‹ē¢‘**
253
+ ä»ŽåŽŸå§‹å†…å®¹äø­čÆ†åˆ«å·²å®Œęˆēš„é‡ŒēØ‹ē¢‘ļ¼Œčæ™äŗ›äæ”ęÆéœ€č¦äæå­˜åˆ°č®°åæ†ę–‡ä»¶äø­ć€‚
254
+
255
+ **ē¬¬äŗŒę­„ļ¼šåŽ‹ē¼©ę–‡ä»¶**
256
+ åŽ‹ē¼©č§„åˆ™ļ¼š
257
+ 1. äæē•™ę ‡é¢˜ć€å…ƒę•°ę®č”Œļ¼ˆē‰ˆęœ¬ć€ēŠ¶ę€ć€ę—„ęœŸļ¼‰
258
+ 2. äæē•™"šŸ“ ēŠ¶ę€åæ«ē…§"ē« čŠ‚ļ¼ˆå®Œę•“ļ¼‰
259
+ 3. äæē•™"āž”ļø 下一歄"ē« čŠ‚ļ¼ˆå®Œę•“ļ¼Œčæ™ę˜Æęœ€é‡č¦ēš„äæ”ęÆļ¼‰
260
+ 4. äæē•™"šŸ“Ž å‚č€ƒ"ē« čŠ‚ļ¼ˆå®Œę•“ļ¼‰
261
+ 5. åÆ¹äŗŽ"šŸ”„ 当前任劔"ē« čŠ‚ļ¼š
262
+ - å¦‚ęžœ P0/P1 ē­‰å­ē« čŠ‚å…ØéƒØå®Œęˆļ¼Œåˆå¹¶äøŗē®€ēŸ­"å·²å®Œęˆé‡ŒēØ‹ē¢‘"åˆ—č”Øļ¼ˆęœ€å¤š5锹)
263
+ - äæē•™ę‰€ęœ‰ęœŖå®Œęˆä»»åŠ”ļ¼ˆ- [ ])
264
+ - å·²å®Œęˆä»»åŠ”ęœ€å¤šäæē•™ 3 äøŖęœ€čæ‘ēš„ļ¼Œå…¶ä½™ē§»é™¤
265
+ 6. ē§»é™¤é‡å¤äæ”ęÆå’Œå†—ä½™ęčæ°
266
+ 7. äæęŒ Markdown ę ¼å¼å’ŒčÆ­ä¹‰čæžč“Æ
267
+
268
+ **ē›®ę ‡ļ¼š** å°†ę–‡ä»¶åŽ‹ē¼©åˆ° 40 č”Œä»„å†…ć€‚
269
+
270
+ **åŽŸå§‹å†…å®¹ļ¼š**
271
+ \`\`\`markdown
272
+ ${oldContent}
273
+ \`\`\`
274
+
275
+ **č¾“å‡ŗę ¼å¼ļ¼ˆåæ…é”»äø„ę ¼éµå¾Ŗļ¼‰ļ¼š**
276
+ \`\`\`
277
+ ===MEMORY===
278
+ [éœ€č¦čæ½åŠ åˆ° memory/MEMORY.md ēš„é‡ŒēØ‹ē¢‘å†…å®¹ļ¼Œę ¼å¼ļ¼š]
279
+ ## {YYYY-MM-DD} é‡ŒēØ‹ē¢‘
280
+ - [é‡ŒēØ‹ē¢‘1]
281
+ - [é‡ŒēØ‹ē¢‘2]
282
+ ...
283
+ ===COMPRESSED===
284
+ [åŽ‹ē¼©åŽēš„ CURRENT_FOCUS.md 内容]
285
+ \`\`\`
286
+
287
+ å¦‚ęžœę²”ęœ‰éœ€č¦č®°å½•ēš„é‡ŒēØ‹ē¢‘ļ¼Œ===MEMORY=== éƒØåˆ†ē•™ē©ŗć€‚`
288
+ : `You are a professional project document compression assistant. Please compress the following CURRENT_FOCUS.md file.
289
+
290
+ **IMPORTANT: Extract milestones before compression!**
291
+
292
+ **Step 1: Extract Milestones**
293
+ Identify completed milestones from the original content that should be saved to memory.
294
+
295
+ **Step 2: Compress File**
296
+ Compression Rules:
297
+ 1. Keep title, metadata lines (version, status, date)
298
+ 2. Keep "šŸ“ Status Snapshot" section (complete)
299
+ 3. Keep "āž”ļø Next Steps" section (complete, this is the most important)
300
+ 4. Keep "šŸ“Ž References" section (complete)
301
+ 5. For "šŸ”„ Current Tasks" section:
302
+ - If P0/P1 subsections are all completed, merge into a short "Completed Milestones" list (max 5 items)
303
+ - Keep all incomplete tasks (- [ ])
304
+ - Keep at most 3 recent completed tasks, remove the rest
305
+ 6. Remove duplicate info and redundant descriptions
306
+ 7. Maintain Markdown format and semantic coherence
307
+
308
+ **Goal:** Compress the file to under 40 lines.
309
+
310
+ **Original Content:**
311
+ \`\`\`markdown
312
+ ${oldContent}
313
+ \`\`\`
314
+
315
+ **Output Format (must follow strictly):**
316
+ \`\`\`
317
+ ===MEMORY===
318
+ [Content to append to memory/MEMORY.md, format:]
319
+ ## {YYYY-MM-DD} Milestones
320
+ - [milestone 1]
321
+ - [milestone 2]
322
+ ...
323
+ ===COMPRESSED===
324
+ [Compressed CURRENT_FOCUS.md content]
325
+ \`\`\`
326
+
327
+ If no milestones to record, leave ===MEMORY=== section empty.`;
328
+ let compressedContent;
329
+ let usedAI = false;
330
+ let memoryUpdated = false;
331
+ try {
332
+ // č°ƒē”Øå­ę™ŗčƒ½ä½“čæ›č”ŒåŽ‹ē¼©
333
+ const tool = createAgentSpawnTool(api);
334
+ const result = await tool.execute(`focus-compress-${randomUUID()}`, {
335
+ agentType: 'reporter', // 使用 reporter ē±»åž‹ļ¼Œé€‚åˆę€»ē»“å’ŒåŽ‹ē¼©
336
+ task: compressPrompt,
337
+ });
338
+ // č§£ęžč¾“å‡ŗļ¼Œęå– MEMORY 和 COMPRESSED éƒØåˆ†
339
+ const resultText = result?.content?.[0]?.text || '';
340
+ if (resultText.trim()) {
341
+ const memoryMatch = resultText.match(/===MEMORY===([\s\S]*?)===COMPRESSED===/);
342
+ const compressedMatch = resultText.match(/===COMPRESSED===([\s\S]*?)$/);
343
+ if (compressedMatch && compressedMatch[1].trim()) {
344
+ // 清理 Markdown ä»£ē å—å›“ę 
345
+ compressedContent = stripMarkdownFence(compressedMatch[1]);
346
+ usedAI = true;
347
+ // å†™å…„č®°åæ†ę–‡ä»¶ļ¼ˆMEMORY.md åœØę ¹ē›®å½•ļ¼Œę— éœ€åˆ›å»ŗē›®å½•ļ¼‰
348
+ if (memoryMatch && memoryMatch[1].trim()) {
349
+ // 清理 Markdown ä»£ē å—å›“ę 
350
+ const memoryContent = stripMarkdownFence(memoryMatch[1]);
351
+ // 追加到 MEMORY.md
352
+ const existingMemory = fs.existsSync(memoryPath)
353
+ ? fs.readFileSync(memoryPath, 'utf-8')
354
+ : '';
355
+ const newMemory = existingMemory
356
+ ? `${existingMemory}\n\n${memoryContent}`
357
+ : memoryContent;
358
+ fs.writeFileSync(memoryPath, newMemory, 'utf-8');
359
+ memoryUpdated = true;
360
+ }
361
+ }
362
+ else {
363
+ // ę— ę³•č§£ęžč¾“å‡ŗļ¼Œå›žé€€åˆ°ē®€å•åŽ‹ē¼©
364
+ compressedContent = compressFocusContent(oldContent);
365
+ }
366
+ }
367
+ else {
368
+ // å­ę™ŗčƒ½ä½“čæ”å›žē©ŗļ¼Œå›žé€€åˆ°ē®€å•åŽ‹ē¼©
369
+ compressedContent = compressFocusContent(oldContent);
370
+ }
371
+ }
372
+ catch (error) {
373
+ // å­ę™ŗčƒ½ä½“å¤±č“„ļ¼Œå›žé€€åˆ°ē®€å•åŽ‹ē¼©
374
+ api.logger?.error(`[PD:Focus] AI compression failed, falling back to simple compression: ${String(error)}`);
375
+ compressedContent = compressFocusContent(oldContent);
376
+ }
377
+ // ę›“ę–°ē‰ˆęœ¬å·å’Œę—„ęœŸ
378
+ const versionParts = oldVersion.split('.');
379
+ const majorVersion = parseInt(versionParts[0], 10) || 1;
380
+ const newVersion = `${majorVersion + 1}`;
381
+ const today = new Date().toISOString().split('T')[0];
382
+ const newContent = compressedContent
383
+ .replace(/\*\*ē‰ˆęœ¬\*\*:\s*v[\d.]+/i, `**ē‰ˆęœ¬**: v${newVersion}`)
384
+ .replace(/\*\*ꛓꖰ\*\*:\s*\d{4}-\d{2}-\d{2}/, `**ꛓꖰ**: ${today}`);
385
+ const newLines = newContent.split('\n').length;
386
+ const savedLines = oldLines - newLines;
387
+ fs.writeFileSync(focusPath, newContent, 'utf-8');
388
+ const methodNote = usedAI
389
+ ? isZh
390
+ ? 'šŸ¤– 使用 AI ę™ŗčƒ½åŽ‹ē¼©'
391
+ : 'šŸ¤– AI-powered compression'
392
+ : isZh
393
+ ? 'šŸ“‹ ä½æē”Øč§„åˆ™åŽ‹ē¼©'
394
+ : 'šŸ“‹ Rule-based compression';
395
+ const memoryNote = memoryUpdated
396
+ ? isZh
397
+ ? 'šŸ“ å·²å°†é‡ŒēØ‹ē¢‘å†™å…„ MEMORY.md'
398
+ : 'šŸ“ Milestones saved to MEMORY.md'
399
+ : '';
400
+ if (isZh) {
401
+ return `āœ… **åŽ‹ē¼©å®Œęˆ**
402
+
403
+ | ę“ä½œ | čÆ¦ęƒ… |
404
+ |------|------|
405
+ | ę—§ē‰ˆęœ¬ | v${oldVersion} (${oldDate}) |
406
+ | ę–°ē‰ˆęœ¬ | v${newVersion} (${today}) |
407
+ | åŽ‹ē¼©å‰ | ${oldLines} 蔌 |
408
+ | åŽ‹ē¼©åŽ | ${newLines} 蔌 |
409
+ | čŠ‚ēœ | ${savedLines} 蔌 |
410
+ | 备份文件 | ${backupPath ? path.basename(backupPath) : '已存在'} |
411
+
412
+ ${methodNote}${memoryNote ? `\n${memoryNote}` : ''}
413
+
414
+ šŸ’” å·²åŽ‹ē¼©ē‰ˆęœ¬å·²å¤‡ä»½åˆ°åŽ†å²ē›®å½•
415
+ šŸ’” 输兄 \`/pd-focus history\` ęŸ„ēœ‹ę‰€ęœ‰åŽ†å²ē‰ˆęœ¬`;
416
+ }
417
+ return `āœ… **Compression Complete**
418
+
419
+ | Action | Details |
420
+ |--------|---------|
421
+ | Old Version | v${oldVersion} (${oldDate}) |
422
+ | New Version | v${newVersion} (${today}) |
423
+ | Before | ${oldLines} lines |
424
+ | After | ${newLines} lines |
425
+ | Saved | ${savedLines} lines |
426
+ | Backup File | ${backupPath ? path.basename(backupPath) : 'exists'} |
427
+
428
+ ${methodNote}${memoryNote ? `\n${memoryNote}` : ''}
429
+
430
+ šŸ’” Compressed version backed up to history
431
+ šŸ’” Type \`/pd-focus history\` to view all versions`;
432
+ }
433
+ /**
434
+ * å›žę»šåˆ°åŽ†å²ē‰ˆęœ¬
435
+ */
436
+ function rollbackFocus(workspaceDir, index, isZh) {
437
+ const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
438
+ const focusPath = wctx.resolve('CURRENT_FOCUS');
439
+ const historyDir = getHistoryDir(focusPath);
440
+ if (!fs.existsSync(historyDir)) {
441
+ return isZh ? 'āŒ ęš‚ę— åŽ†å²ē‰ˆęœ¬åÆå›žę»š' : 'āŒ No history versions to rollback';
442
+ }
443
+ const files = fs.readdirSync(historyDir)
444
+ .filter(f => f.startsWith('CURRENT_FOCUS.v') && f.endsWith('.md'))
445
+ .map(f => ({
446
+ name: f,
447
+ path: path.join(historyDir, f),
448
+ mtime: fs.statSync(path.join(historyDir, f)).mtime.getTime(),
449
+ }))
450
+ .sort((a, b) => b.mtime - a.mtime);
451
+ if (index < 1 || index > files.length) {
452
+ return isZh
453
+ ? `āŒ ę— ę•ˆēš„åŗå·: ${index}\n\nšŸ’” 请输兄 1-${files.length} ä¹‹é—“ēš„ę•°å­—`
454
+ : `āŒ Invalid index: ${index}\n\nšŸ’” Please enter a number between 1-${files.length}`;
455
+ }
456
+ const targetFile = files[index - 1];
457
+ const historyContent = fs.readFileSync(targetFile.path, 'utf-8');
458
+ // å¤‡ä»½å½“å‰ē‰ˆęœ¬
459
+ const currentContent = fs.existsSync(focusPath)
460
+ ? fs.readFileSync(focusPath, 'utf-8')
461
+ : '';
462
+ if (currentContent) {
463
+ backupToHistory(focusPath, currentContent);
464
+ }
465
+ // ę¢å¤åŽ†å²ē‰ˆęœ¬
466
+ const restoredVersion = extractVersion(historyContent);
467
+ const restoredDate = extractDate(historyContent);
468
+ const today = new Date().toISOString().split('T')[0];
469
+ // čŽ·å–ęœ€å¤§ē‰ˆęœ¬å·ļ¼ˆä»Žå½“å‰ę–‡ä»¶ęˆ–åŽ†å²ę–‡ä»¶äø­ļ¼‰
470
+ let maxVersion = parseFloat(restoredVersion) || 1;
471
+ if (currentContent) {
472
+ const currentVersion = parseFloat(extractVersion(currentContent)) || 1;
473
+ if (currentVersion > maxVersion) {
474
+ maxVersion = currentVersion;
475
+ }
476
+ }
477
+ // ę­£åøøé€’å¢žē‰ˆęœ¬å·
478
+ const newVersion = `${maxVersion + 1}`;
479
+ // ę·»åŠ å›žę»šę ‡č®°åˆ°ēŠ¶ę€å­—ę®µ
480
+ const restoredContent = historyContent
481
+ .replace(/\*\*ē‰ˆęœ¬\*\*:\s*v[\d.]+/i, `**ē‰ˆęœ¬**: v${newVersion}`)
482
+ .replace(/\*\*ꛓꖰ\*\*:\s*\d{4}-\d{2}-\d{2}/, `**ꛓꖰ**: ${today}`)
483
+ .replace(/\*\*ēŠ¶ę€\*\*:\s*[A-Z]+/i, `**ēŠ¶ę€**: ROLLBACK (from v${restoredVersion})`);
484
+ fs.writeFileSync(focusPath, restoredContent, 'utf-8');
485
+ if (isZh) {
486
+ return `āœ… **å›žę»šęˆåŠŸ**
487
+
488
+ | ę“ä½œ | čÆ¦ęƒ… |
489
+ |------|------|
490
+ | ę¢å¤č‡Ŗ | ${targetFile.name} |
491
+ | åŽŸē‰ˆęœ¬ | v${restoredVersion} (${restoredDate}) |
492
+ | ę–°ē‰ˆęœ¬ | v${newVersion} (${today}) |
493
+
494
+ šŸ’” å½“å‰ē‰ˆęœ¬å·²å¤‡ä»½ļ¼ŒåÆå†ę¬”å›žę»š`;
495
+ }
496
+ return `āœ… **Rollback Complete**
497
+
498
+ | Action | Details |
499
+ |--------|---------|
500
+ | Restored From | ${targetFile.name} |
501
+ | Original Version | v${restoredVersion} (${restoredDate}) |
502
+ | New Version | v${newVersion} (${today}) |
503
+
504
+ šŸ’” Current version backed up, you can rollback again`;
505
+ }
506
+ /**
507
+ * 显示帮助
508
+ */
509
+ function showHelp(isZh) {
510
+ if (isZh) {
511
+ return `šŸ“– **/pd-focus å‘½ä»¤åø®åŠ©**
512
+
513
+ \`/pd-focus status\` - ęŸ„ēœ‹ CURRENT_FOCUS.md ēŠ¶ę€
514
+ \`/pd-focus history\` - ęŸ„ēœ‹åŽ†å²ē‰ˆęœ¬åˆ—č”Ø
515
+ \`/pd-focus compress\` - ę‰‹åŠØåŽ‹ē¼©å¹¶å¤‡ä»½
516
+ \`/pd-focus rollback <åŗå·>\` - å›žę»šåˆ°ęŒ‡å®šåŽ†å²ē‰ˆęœ¬
517
+
518
+ **åŠŸčƒ½čÆ“ę˜Žļ¼š**
519
+ - åŽ†å²ē‰ˆęœ¬åœØåŽ‹ē¼©ę—¶č‡ŖåŠØåˆ›å»ŗ
520
+ - ęœ€å¤šäæē•™ 10 äøŖåŽ†å²ē‰ˆęœ¬
521
+ - Full ęØ”å¼ä¼ščÆ»å–å½“å‰ē‰ˆęœ¬ + ęœ€čæ‘ 3 äøŖåŽ†å²ē‰ˆęœ¬`;
522
+ }
523
+ return `šŸ“– **/pd-focus Command Help**
524
+
525
+ \`/pd-focus status\` - Show CURRENT_FOCUS.md status
526
+ \`/pd-focus history\` - View history versions list
527
+ \`/pd-focus compress\` - Manually compress and backup
528
+ \`/pd-focus rollback <number>\` - Rollback to specified version
529
+
530
+ **Features:**
531
+ - History versions created during compression
532
+ - Maximum 10 history versions retained
533
+ - Full mode reads current + last 3 history versions`;
534
+ }
535
+ /**
536
+ * 处理 /pd-focus 命令
537
+ */
538
+ export async function handleFocusCommand(ctx, api) {
539
+ const workspaceDir = getWorkspaceDir(ctx);
540
+ const args = ctx.args || [];
541
+ const subCommand = args[0]?.toLowerCase() || 'status';
542
+ // ę£€ęµ‹čÆ­čØ€ļ¼ˆäøŽ context.ts äæęŒäø€č‡“ļ¼‰
543
+ const isZh = ctx.config?.language === 'zh';
544
+ let result;
545
+ switch (subCommand) {
546
+ case 'status':
547
+ result = showStatus(workspaceDir, isZh);
548
+ break;
549
+ case 'history':
550
+ case 'hist':
551
+ result = showHistory(workspaceDir, isZh);
552
+ break;
553
+ case 'compress':
554
+ case 'cp':
555
+ result = await compressFocus(workspaceDir, isZh, api);
556
+ break;
557
+ case 'rollback':
558
+ case 'rb':
559
+ const index = parseInt(args[1], 10);
560
+ if (isNaN(index)) {
561
+ result = isZh
562
+ ? 'āŒ čÆ·ęŒ‡å®šč¦å›žę»šēš„ē‰ˆęœ¬åŗå·\n\nšŸ’” 输兄 `/pd-focus history` ęŸ„ēœ‹åÆē”Øē‰ˆęœ¬'
563
+ : 'āŒ Please specify version number to rollback\n\nšŸ’” Type `/pd-focus history` to see available versions';
564
+ }
565
+ else {
566
+ result = rollbackFocus(workspaceDir, index, isZh);
567
+ }
568
+ break;
569
+ case 'help':
570
+ case '--help':
571
+ case '-h':
572
+ result = showHelp(isZh);
573
+ break;
574
+ default:
575
+ result = isZh
576
+ ? `āŒ ęœŖēŸ„å‘½ä»¤: ${subCommand}\n\n${showHelp(isZh)}`
577
+ : `āŒ Unknown command: ${subCommand}\n\n${showHelp(isZh)}`;
578
+ }
579
+ return {
580
+ text: result,
581
+ };
582
+ }