@liangjie559567/ultrapower 5.1.0 → 5.2.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 (78) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/__tests__/skills.test.js +3 -2
  3. package/dist/__tests__/skills.test.js.map +1 -1
  4. package/dist/hooks/nexus/__tests__/config.test.d.ts +2 -0
  5. package/dist/hooks/nexus/__tests__/config.test.d.ts.map +1 -0
  6. package/dist/hooks/nexus/__tests__/config.test.js +38 -0
  7. package/dist/hooks/nexus/__tests__/config.test.js.map +1 -0
  8. package/dist/hooks/nexus/__tests__/consciousness-sync.test.d.ts +2 -0
  9. package/dist/hooks/nexus/__tests__/consciousness-sync.test.d.ts.map +1 -0
  10. package/dist/hooks/nexus/__tests__/consciousness-sync.test.js +28 -0
  11. package/dist/hooks/nexus/__tests__/consciousness-sync.test.js.map +1 -0
  12. package/dist/hooks/nexus/__tests__/data-collector.test.d.ts +2 -0
  13. package/dist/hooks/nexus/__tests__/data-collector.test.d.ts.map +1 -0
  14. package/dist/hooks/nexus/__tests__/data-collector.test.js +61 -0
  15. package/dist/hooks/nexus/__tests__/data-collector.test.js.map +1 -0
  16. package/dist/hooks/nexus/__tests__/improvement-puller.test.d.ts +2 -0
  17. package/dist/hooks/nexus/__tests__/improvement-puller.test.d.ts.map +1 -0
  18. package/dist/hooks/nexus/__tests__/improvement-puller.test.js +49 -0
  19. package/dist/hooks/nexus/__tests__/improvement-puller.test.js.map +1 -0
  20. package/dist/hooks/nexus/__tests__/session-end-hook.test.d.ts +2 -0
  21. package/dist/hooks/nexus/__tests__/session-end-hook.test.d.ts.map +1 -0
  22. package/dist/hooks/nexus/__tests__/session-end-hook.test.js +39 -0
  23. package/dist/hooks/nexus/__tests__/session-end-hook.test.js.map +1 -0
  24. package/dist/hooks/nexus/config.d.ts +5 -0
  25. package/dist/hooks/nexus/config.d.ts.map +1 -0
  26. package/dist/hooks/nexus/config.js +35 -0
  27. package/dist/hooks/nexus/config.js.map +1 -0
  28. package/dist/hooks/nexus/consciousness-sync.d.ts +8 -0
  29. package/dist/hooks/nexus/consciousness-sync.d.ts.map +1 -0
  30. package/dist/hooks/nexus/consciousness-sync.js +57 -0
  31. package/dist/hooks/nexus/consciousness-sync.js.map +1 -0
  32. package/dist/hooks/nexus/data-collector.d.ts +4 -0
  33. package/dist/hooks/nexus/data-collector.d.ts.map +1 -0
  34. package/dist/hooks/nexus/data-collector.js +23 -0
  35. package/dist/hooks/nexus/data-collector.js.map +1 -0
  36. package/dist/hooks/nexus/improvement-puller.d.ts +9 -0
  37. package/dist/hooks/nexus/improvement-puller.d.ts.map +1 -0
  38. package/dist/hooks/nexus/improvement-puller.js +42 -0
  39. package/dist/hooks/nexus/improvement-puller.js.map +1 -0
  40. package/dist/hooks/nexus/session-end-hook.d.ts +16 -0
  41. package/dist/hooks/nexus/session-end-hook.d.ts.map +1 -0
  42. package/dist/hooks/nexus/session-end-hook.js +49 -0
  43. package/dist/hooks/nexus/session-end-hook.js.map +1 -0
  44. package/dist/hooks/nexus/types.d.ts +54 -0
  45. package/dist/hooks/nexus/types.d.ts.map +1 -0
  46. package/dist/hooks/nexus/types.js +2 -0
  47. package/dist/hooks/nexus/types.js.map +1 -0
  48. package/dist/hooks/session-end/__tests__/nexus-integration.test.d.ts +2 -0
  49. package/dist/hooks/session-end/__tests__/nexus-integration.test.d.ts.map +1 -0
  50. package/dist/hooks/session-end/__tests__/nexus-integration.test.js +30 -0
  51. package/dist/hooks/session-end/__tests__/nexus-integration.test.js.map +1 -0
  52. package/dist/hooks/session-end/index.d.ts.map +1 -1
  53. package/dist/hooks/session-end/index.js +15 -0
  54. package/dist/hooks/session-end/index.js.map +1 -1
  55. package/docs/CLAUDE.md +1 -1
  56. package/docs/README.codex.md +6 -0
  57. package/docs/plans/2026-01-17-visual-brainstorming.md +571 -0
  58. package/docs/plans/2026-02-26-nexus-design.md +354 -0
  59. package/docs/plans/2026-02-26-nexus-implementation-plan.md +2538 -0
  60. package/docs/plans/2026-02-26-phase2-active-learning-design.md +377 -0
  61. package/docs/standards/contribution-guide.md +52 -1
  62. package/docs/superpowers/plans/2026-01-22-document-review-system.md +301 -0
  63. package/docs/superpowers/plans/2026-02-19-visual-brainstorming-refactor.md +523 -0
  64. package/docs/superpowers/specs/2026-01-22-document-review-system-design.md +136 -0
  65. package/docs/superpowers/specs/2026-02-19-visual-brainstorming-refactor-design.md +162 -0
  66. package/hooks/run-hook.cmd +32 -29
  67. package/package.json +4 -2
  68. package/skills/brainstorming/spec-document-reviewer-prompt.md +50 -0
  69. package/skills/brainstorming/visual-companion.md +249 -0
  70. package/skills/nexus/SKILL.md +35 -0
  71. package/skills/nexus/nexus-evolve/SKILL.md +31 -0
  72. package/skills/nexus/nexus-review/SKILL.md +39 -0
  73. package/skills/nexus/nexus-status/SKILL.md +31 -0
  74. package/skills/release/SKILL.md +3 -0
  75. package/skills/requesting-code-review/SKILL.md +1 -1
  76. package/skills/using-superpowers/references/codex-tools.md +25 -0
  77. package/skills/writing-plans/plan-document-reviewer-prompt.md +52 -0
  78. /package/hooks/{session-start.sh → session-start} +0 -0
@@ -0,0 +1,2538 @@
1
+ # nexus 实现计划
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** 为 ultrapower 添加 nexus 双层自我提升系统——插件层(TypeScript hooks)+ VPS 守护进程(Python)
6
+
7
+ **Architecture:** 插件层通过 Git 同步将会话数据推送到 nexus-daemon 仓库;VPS 守护进程每分钟拉取新事件,运行进化引擎和意识循环,生成改进建议推回仓库;插件层拉取改进并按置信度自动或通过 Telegram 确认应用。
8
+
9
+ **Tech Stack:** TypeScript (plugin hooks), Python 3.11 (VPS daemon), Git sync (communication), Telegram Bot API (notifications), systemd (process management)
10
+
11
+ **Design Doc:** `docs/plans/2026-02-26-nexus-design.md`
12
+
13
+ ---
14
+
15
+ ## P0:核心基础设施
16
+
17
+ ### Task 1: nexus 配置类型和存储结构
18
+
19
+ **Files:**
20
+ - Create: `src/hooks/nexus/types.ts`
21
+ - Create: `src/hooks/nexus/config.ts`
22
+ - Create: `src/hooks/nexus/__tests__/config.test.ts`
23
+
24
+ **Step 1: 写失败测试**
25
+
26
+ ```typescript
27
+ // src/hooks/nexus/__tests__/config.test.ts
28
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
29
+ import { mkdirSync, rmSync, writeFileSync } from 'fs';
30
+ import { join } from 'path';
31
+ import { tmpdir } from 'os';
32
+ import { loadNexusConfig, DEFAULT_NEXUS_CONFIG } from '../config.js';
33
+
34
+ describe('loadNexusConfig', () => {
35
+ let tmpDir: string;
36
+
37
+ beforeEach(() => {
38
+ tmpDir = join(tmpdir(), `nexus-test-${Date.now()}`);
39
+ mkdirSync(tmpDir, { recursive: true });
40
+ });
41
+
42
+ afterEach(() => {
43
+ rmSync(tmpDir, { recursive: true, force: true });
44
+ });
45
+
46
+ it('returns default config when file does not exist', () => {
47
+ const config = loadNexusConfig(tmpDir);
48
+ expect(config.enabled).toBe(false);
49
+ expect(config.autoApplyThreshold).toBe(80);
50
+ expect(config.consciousnessInterval).toBe(300);
51
+ });
52
+
53
+ it('loads and merges config from .omc/nexus/config.json', () => {
54
+ const configDir = join(tmpDir, '.omc', 'nexus');
55
+ mkdirSync(configDir, { recursive: true });
56
+ writeFileSync(
57
+ join(configDir, 'config.json'),
58
+ JSON.stringify({ enabled: true, gitRemote: 'git@github.com:user/nexus-daemon.git' })
59
+ );
60
+ const config = loadNexusConfig(tmpDir);
61
+ expect(config.enabled).toBe(true);
62
+ expect(config.gitRemote).toBe('git@github.com:user/nexus-daemon.git');
63
+ expect(config.autoApplyThreshold).toBe(80); // default preserved
64
+ });
65
+
66
+ it('returns default config on malformed JSON', () => {
67
+ const configDir = join(tmpDir, '.omc', 'nexus');
68
+ mkdirSync(configDir, { recursive: true });
69
+ writeFileSync(join(configDir, 'config.json'), 'not json');
70
+ const config = loadNexusConfig(tmpDir);
71
+ expect(config.enabled).toBe(false);
72
+ });
73
+ });
74
+ ```
75
+
76
+ **Step 2: 运行测试确认失败**
77
+
78
+ ```bash
79
+ npm test -- src/hooks/nexus/__tests__/config.test.ts
80
+ ```
81
+ Expected: FAIL — `Cannot find module '../config.js'`
82
+
83
+ **Step 3: 实现 types.ts**
84
+
85
+ ```typescript
86
+ // src/hooks/nexus/types.ts
87
+ export interface NexusConfig {
88
+ enabled: boolean;
89
+ gitRemote: string;
90
+ telegramToken?: string;
91
+ telegramChatId?: string;
92
+ autoApplyThreshold: number; // 0-100, default 80
93
+ consciousnessInterval: number; // seconds, default 300
94
+ consciousnessBudgetPercent: number; // 0-100, default 10
95
+ }
96
+
97
+ export interface SessionEvent {
98
+ sessionId: string;
99
+ timestamp: string;
100
+ directory: string;
101
+ durationMs?: number;
102
+ toolCalls: ToolCallRecord[];
103
+ agentsSpawned: number;
104
+ agentsCompleted: number;
105
+ modesUsed: string[];
106
+ skillsInjected: string[];
107
+ patternsSeen: PatternRecord[];
108
+ }
109
+
110
+ export interface ToolCallRecord {
111
+ toolName: string;
112
+ agentRole?: string;
113
+ skillName?: string;
114
+ timestamp: number;
115
+ }
116
+
117
+ export interface PatternRecord {
118
+ problem: string;
119
+ solution: string;
120
+ confidence: number;
121
+ occurrences: number;
122
+ }
123
+
124
+ export interface ImprovementSuggestion {
125
+ id: string;
126
+ createdAt: string;
127
+ source: 'evolution_engine' | 'consciousness_loop' | 'self_evaluator';
128
+ type: 'skill_update' | 'agent_update' | 'hook_update' | 'trigger_update';
129
+ targetFile: string;
130
+ confidence: number;
131
+ diff: string;
132
+ reason: string;
133
+ evidence: Record<string, unknown>;
134
+ status: 'pending' | 'applied' | 'rejected' | 'failed';
135
+ testResult: string | null;
136
+ }
137
+ ```
138
+
139
+ **Step 4: 实现 config.ts**
140
+
141
+ ```typescript
142
+ // src/hooks/nexus/config.ts
143
+ import { existsSync, readFileSync } from 'fs';
144
+ import { join } from 'path';
145
+ import type { NexusConfig } from './types.js';
146
+
147
+ export const DEFAULT_NEXUS_CONFIG: NexusConfig = {
148
+ enabled: false,
149
+ gitRemote: '',
150
+ autoApplyThreshold: 80,
151
+ consciousnessInterval: 300,
152
+ consciousnessBudgetPercent: 10,
153
+ };
154
+
155
+ const CONFIG_RELATIVE_PATH = '.omc/nexus/config.json';
156
+
157
+ export function loadNexusConfig(directory: string): NexusConfig {
158
+ const configPath = join(directory, CONFIG_RELATIVE_PATH);
159
+ if (!existsSync(configPath)) {
160
+ return { ...DEFAULT_NEXUS_CONFIG };
161
+ }
162
+ try {
163
+ const raw = readFileSync(configPath, 'utf-8');
164
+ const partial = JSON.parse(raw) as Partial<NexusConfig>;
165
+ return { ...DEFAULT_NEXUS_CONFIG, ...partial };
166
+ } catch {
167
+ return { ...DEFAULT_NEXUS_CONFIG };
168
+ }
169
+ }
170
+
171
+ export function isNexusEnabled(directory: string): boolean {
172
+ return loadNexusConfig(directory).enabled;
173
+ }
174
+ ```
175
+
176
+ **Step 5: 运行测试确认通过**
177
+
178
+ ```bash
179
+ npm test -- src/hooks/nexus/__tests__/config.test.ts
180
+ ```
181
+ Expected: PASS (3 tests)
182
+
183
+ **Step 6: Commit**
184
+
185
+ ```bash
186
+ git add src/hooks/nexus/types.ts src/hooks/nexus/config.ts src/hooks/nexus/__tests__/config.test.ts
187
+ git commit -m "feat(nexus): add config types and loader"
188
+ ```
189
+
190
+ ---
191
+
192
+ ### Task 2: data-collector hook(收集会话数据)
193
+
194
+ **Files:**
195
+ - Create: `src/hooks/nexus/data-collector.ts`
196
+ - Create: `src/hooks/nexus/__tests__/data-collector.test.ts`
197
+
198
+ **Step 1: 写失败测试**
199
+
200
+ ```typescript
201
+ // src/hooks/nexus/__tests__/data-collector.test.ts
202
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
203
+ import { mkdirSync, rmSync, existsSync, readFileSync, readdirSync } from 'fs';
204
+ import { join } from 'path';
205
+ import { tmpdir } from 'os';
206
+ import { collectSessionEvent, getEventsDir } from '../data-collector.js';
207
+ import type { SessionEvent } from '../types.js';
208
+
209
+ describe('collectSessionEvent', () => {
210
+ let tmpDir: string;
211
+
212
+ beforeEach(() => {
213
+ tmpDir = join(tmpdir(), `nexus-dc-test-${Date.now()}`);
214
+ mkdirSync(tmpDir, { recursive: true });
215
+ // Enable nexus in config
216
+ const configDir = join(tmpDir, '.omc', 'nexus');
217
+ mkdirSync(configDir, { recursive: true });
218
+ writeFileSync(join(configDir, 'config.json'), JSON.stringify({ enabled: true }));
219
+ });
220
+
221
+ afterEach(() => {
222
+ rmSync(tmpDir, { recursive: true, force: true });
223
+ });
224
+
225
+ it('writes event JSON to .omc/nexus/events/', async () => {
226
+ const event: SessionEvent = {
227
+ sessionId: 'test-session-123',
228
+ timestamp: '2026-02-26T14:00:00Z',
229
+ directory: tmpDir,
230
+ toolCalls: [],
231
+ agentsSpawned: 2,
232
+ agentsCompleted: 2,
233
+ modesUsed: ['ultrawork'],
234
+ skillsInjected: [],
235
+ patternsSeen: [],
236
+ };
237
+ await collectSessionEvent(tmpDir, event);
238
+ const eventsDir = getEventsDir(tmpDir);
239
+ expect(existsSync(eventsDir)).toBe(true);
240
+ const files = readdirSync(eventsDir);
241
+ expect(files.length).toBe(1);
242
+ expect(files[0]).toMatch(/^test-session-123-\d+\.json$/);
243
+ const content = JSON.parse(readFileSync(join(eventsDir, files[0]), 'utf-8'));
244
+ expect(content.sessionId).toBe('test-session-123');
245
+ expect(content.agentsSpawned).toBe(2);
246
+ });
247
+
248
+ it('does nothing when nexus is disabled', async () => {
249
+ const disabledDir = join(tmpdir(), `nexus-disabled-${Date.now()}`);
250
+ mkdirSync(disabledDir, { recursive: true });
251
+ const event: SessionEvent = {
252
+ sessionId: 'test-session-456',
253
+ timestamp: '2026-02-26T14:00:00Z',
254
+ directory: disabledDir,
255
+ toolCalls: [],
256
+ agentsSpawned: 0,
257
+ agentsCompleted: 0,
258
+ modesUsed: [],
259
+ skillsInjected: [],
260
+ patternsSeen: [],
261
+ };
262
+ await collectSessionEvent(disabledDir, event);
263
+ const eventsDir = getEventsDir(disabledDir);
264
+ expect(existsSync(eventsDir)).toBe(false);
265
+ rmSync(disabledDir, { recursive: true, force: true });
266
+ });
267
+ });
268
+ ```
269
+
270
+ **Step 2: 运行测试确认失败**
271
+
272
+ ```bash
273
+ npm test -- src/hooks/nexus/__tests__/data-collector.test.ts
274
+ ```
275
+ Expected: FAIL — `Cannot find module '../data-collector.js'`
276
+
277
+ **Step 3: 实现 data-collector.ts**
278
+
279
+ ```typescript
280
+ // src/hooks/nexus/data-collector.ts
281
+ import { mkdirSync, writeFileSync } from 'fs';
282
+ import { join } from 'path';
283
+ import { isNexusEnabled } from './config.js';
284
+ import type { SessionEvent } from './types.js';
285
+
286
+ const EVENTS_SUBDIR = '.omc/nexus/events';
287
+
288
+ export function getEventsDir(directory: string): string {
289
+ return join(directory, EVENTS_SUBDIR);
290
+ }
291
+
292
+ export async function collectSessionEvent(
293
+ directory: string,
294
+ event: SessionEvent
295
+ ): Promise<void> {
296
+ try {
297
+ if (!isNexusEnabled(directory)) return;
298
+
299
+ const eventsDir = getEventsDir(directory);
300
+ mkdirSync(eventsDir, { recursive: true });
301
+
302
+ const timestamp = Date.now();
303
+ const filename = `${event.sessionId}-${timestamp}.json`;
304
+ const filePath = join(eventsDir, filename);
305
+
306
+ writeFileSync(filePath, JSON.stringify(event, null, 2), 'utf-8');
307
+ } catch {
308
+ // Silent failure — must never break main flow
309
+ }
310
+ }
311
+ ```
312
+
313
+ **Step 4: 运行测试确认通过**
314
+
315
+ ```bash
316
+ npm test -- src/hooks/nexus/__tests__/data-collector.test.ts
317
+ ```
318
+ Expected: PASS (2 tests)
319
+
320
+ **Step 5: Commit**
321
+
322
+ ```bash
323
+ git add src/hooks/nexus/data-collector.ts src/hooks/nexus/__tests__/data-collector.test.ts
324
+ git commit -m "feat(nexus): add data-collector for session events"
325
+ ```
326
+
327
+ ---
328
+
329
+ ### Task 3: consciousness-sync hook(SessionEnd 后 git push)
330
+
331
+ **Files:**
332
+ - Create: `src/hooks/nexus/consciousness-sync.ts`
333
+ - Create: `src/hooks/nexus/__tests__/consciousness-sync.test.ts`
334
+
335
+ **Step 1: 写失败测试**
336
+
337
+ ```typescript
338
+ // src/hooks/nexus/__tests__/consciousness-sync.test.ts
339
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
340
+ import { syncToRemote, buildGitCommitMessage } from '../consciousness-sync.js';
341
+
342
+ describe('buildGitCommitMessage', () => {
343
+ it('includes session ID in commit message', () => {
344
+ const msg = buildGitCommitMessage('abc123', 3);
345
+ expect(msg).toContain('abc123');
346
+ expect(msg).toContain('3');
347
+ });
348
+ });
349
+
350
+ describe('syncToRemote', () => {
351
+ it('returns false when nexus is disabled', async () => {
352
+ const result = await syncToRemote('/nonexistent/dir', 'sess-001');
353
+ expect(result.success).toBe(false);
354
+ });
355
+ });
356
+ ```
357
+
358
+ **Step 2: 运行测试确认失败**
359
+
360
+ ```bash
361
+ npm test -- src/hooks/nexus/__tests__/consciousness-sync.test.ts
362
+ ```
363
+ Expected: FAIL — `Cannot find module '../consciousness-sync.js'`
364
+
365
+ **Step 3: 实现 consciousness-sync.ts**
366
+
367
+ ```typescript
368
+ // src/hooks/nexus/consciousness-sync.ts
369
+ import { spawnSync } from 'child_process';
370
+ import { existsSync } from 'fs';
371
+ import { join } from 'path';
372
+ import { isNexusEnabled, loadNexusConfig } from './config.js';
373
+ import { getEventsDir } from './data-collector.js';
374
+
375
+ export interface SyncResult {
376
+ success: boolean;
377
+ error?: string;
378
+ filesCommitted?: number;
379
+ }
380
+
381
+ export function buildGitCommitMessage(sessionId: string, fileCount: number): string {
382
+ // Use only first 8 chars of sessionId (alphanumeric only) to avoid injection
383
+ const safeId = sessionId.replace(/[^a-zA-Z0-9-]/g, '').slice(0, 8);
384
+ return `nexus: sync ${fileCount} event(s) from session ${safeId}`;
385
+ }
386
+
387
+ function git(args: string[], cwd: string): { ok: boolean; stdout: string; stderr: string } {
388
+ const result = spawnSync('git', args, { cwd, encoding: 'utf-8' });
389
+ return {
390
+ ok: result.status === 0,
391
+ stdout: result.stdout ?? '',
392
+ stderr: result.stderr ?? '',
393
+ };
394
+ }
395
+
396
+ export async function syncToRemote(
397
+ directory: string,
398
+ sessionId: string
399
+ ): Promise<SyncResult> {
400
+ try {
401
+ if (!isNexusEnabled(directory)) {
402
+ return { success: false, error: 'nexus disabled' };
403
+ }
404
+
405
+ const config = loadNexusConfig(directory);
406
+ if (!config.gitRemote) {
407
+ return { success: false, error: 'gitRemote not configured' };
408
+ }
409
+
410
+ const eventsDir = getEventsDir(directory);
411
+ if (!existsSync(eventsDir)) {
412
+ return { success: true, filesCommitted: 0 };
413
+ }
414
+
415
+ // Stage only nexus events — args are array, no shell injection possible
416
+ git(['add', join('.omc', 'nexus', 'events')], directory);
417
+
418
+ // Check if there's anything to commit
419
+ const statusResult = git(['status', '--porcelain'], directory);
420
+ const nexusLines = statusResult.stdout.split('\n').filter(l => l.includes('.omc/nexus/events'));
421
+ if (nexusLines.length === 0) {
422
+ return { success: true, filesCommitted: 0 };
423
+ }
424
+
425
+ const msg = buildGitCommitMessage(sessionId, nexusLines.length);
426
+ const commitResult = git(['commit', '-m', msg], directory);
427
+ if (!commitResult.ok) {
428
+ return { success: false, error: commitResult.stderr };
429
+ }
430
+
431
+ const pushResult = git(['push', 'origin', 'HEAD'], directory);
432
+ if (!pushResult.ok) {
433
+ return { success: false, error: pushResult.stderr };
434
+ }
435
+
436
+ return { success: true, filesCommitted: nexusLines.length };
437
+ } catch (e) {
438
+ return { success: false, error: e instanceof Error ? e.message : String(e) };
439
+ }
440
+ }
441
+ ```
442
+
443
+ **Step 4: 运行测试确认通过**
444
+
445
+ ```bash
446
+ npm test -- src/hooks/nexus/__tests__/consciousness-sync.test.ts
447
+ ```
448
+ Expected: PASS (3 tests)
449
+
450
+ **Step 5: Commit**
451
+
452
+ ```bash
453
+ git add src/hooks/nexus/consciousness-sync.ts src/hooks/nexus/__tests__/consciousness-sync.test.ts
454
+ git commit -m "feat(nexus): add consciousness-sync for git push on session end"
455
+ ```
456
+
457
+ ---
458
+
459
+ ### Task 4: SessionEnd hook 集成(连接 data-collector + consciousness-sync)
460
+
461
+ **Files:**
462
+ - Create: `src/hooks/nexus/session-end-hook.ts`
463
+ - Modify: `src/hooks/index.ts`(注册新 hook)
464
+
465
+ **Step 1: 写失败测试**
466
+
467
+ ```typescript
468
+ // src/hooks/nexus/__tests__/session-end-hook.test.ts
469
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
470
+ import { mkdirSync, rmSync, writeFileSync } from 'fs';
471
+ import { join } from 'path';
472
+ import { tmpdir } from 'os';
473
+ import { handleNexusSessionEnd } from '../session-end-hook.js';
474
+
475
+ describe('handleNexusSessionEnd', () => {
476
+ let tmpDir: string;
477
+
478
+ beforeEach(() => {
479
+ tmpDir = join(tmpdir(), `nexus-se-test-${Date.now()}`);
480
+ mkdirSync(join(tmpDir, '.omc', 'nexus'), { recursive: true });
481
+ writeFileSync(
482
+ join(tmpDir, '.omc', 'nexus', 'config.json'),
483
+ JSON.stringify({ enabled: true, gitRemote: '' }) // no remote = no push
484
+ );
485
+ });
486
+
487
+ afterEach(() => {
488
+ rmSync(tmpDir, { recursive: true, force: true });
489
+ });
490
+
491
+ it('collects event and returns result', async () => {
492
+ const result = await handleNexusSessionEnd({
493
+ sessionId: 'test-sess-789',
494
+ directory: tmpDir,
495
+ durationMs: 60000,
496
+ agentsSpawned: 1,
497
+ agentsCompleted: 1,
498
+ modesUsed: ['ultrawork'],
499
+ });
500
+ expect(result.collected).toBe(true);
501
+ // sync skipped because gitRemote is empty
502
+ expect(result.synced).toBe(false);
503
+ });
504
+
505
+ it('returns not collected when nexus disabled', async () => {
506
+ writeFileSync(
507
+ join(tmpDir, '.omc', 'nexus', 'config.json'),
508
+ JSON.stringify({ enabled: false })
509
+ );
510
+ const result = await handleNexusSessionEnd({
511
+ sessionId: 'test-sess-000',
512
+ directory: tmpDir,
513
+ });
514
+ expect(result.collected).toBe(false);
515
+ });
516
+ });
517
+ ```
518
+
519
+ **Step 2: 运行测试确认失败**
520
+
521
+ ```bash
522
+ npm test -- src/hooks/nexus/__tests__/session-end-hook.test.ts
523
+ ```
524
+ Expected: FAIL — `Cannot find module '../session-end-hook.js'`
525
+
526
+ **Step 3: 实现 session-end-hook.ts**
527
+
528
+ ```typescript
529
+ // src/hooks/nexus/session-end-hook.ts
530
+ import { isNexusEnabled } from './config.js';
531
+ import { collectSessionEvent } from './data-collector.js';
532
+ import { syncToRemote } from './consciousness-sync.js';
533
+ import type { SessionEvent } from './types.js';
534
+
535
+ export interface NexusSessionEndInput {
536
+ sessionId: string;
537
+ directory: string;
538
+ durationMs?: number;
539
+ agentsSpawned?: number;
540
+ agentsCompleted?: number;
541
+ modesUsed?: string[];
542
+ skillsInjected?: string[];
543
+ }
544
+
545
+ export interface NexusSessionEndResult {
546
+ collected: boolean;
547
+ synced: boolean;
548
+ error?: string;
549
+ }
550
+
551
+ export async function handleNexusSessionEnd(
552
+ input: NexusSessionEndInput
553
+ ): Promise<NexusSessionEndResult> {
554
+ const work = async (): Promise<NexusSessionEndResult> => {
555
+ try {
556
+ if (!isNexusEnabled(input.directory)) {
557
+ return { collected: false, synced: false };
558
+ }
559
+
560
+ const event: SessionEvent = {
561
+ sessionId: input.sessionId,
562
+ timestamp: new Date().toISOString(),
563
+ directory: input.directory,
564
+ durationMs: input.durationMs,
565
+ toolCalls: [],
566
+ agentsSpawned: input.agentsSpawned ?? 0,
567
+ agentsCompleted: input.agentsCompleted ?? 0,
568
+ modesUsed: input.modesUsed ?? [],
569
+ skillsInjected: input.skillsInjected ?? [],
570
+ patternsSeen: [],
571
+ };
572
+
573
+ await collectSessionEvent(input.directory, event);
574
+
575
+ const syncResult = await syncToRemote(input.directory, input.sessionId);
576
+
577
+ return {
578
+ collected: true,
579
+ synced: syncResult.success && (syncResult.filesCommitted ?? 0) > 0,
580
+ };
581
+ } catch (e) {
582
+ return {
583
+ collected: false,
584
+ synced: false,
585
+ error: e instanceof Error ? e.message : String(e),
586
+ };
587
+ }
588
+ };
589
+
590
+ // 3-second timeout (same pattern as session-reflector.ts)
591
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
592
+ const timeout = new Promise<NexusSessionEndResult>(resolve => {
593
+ timeoutHandle = setTimeout(
594
+ () => resolve({ collected: false, synced: false, error: 'timeout' }),
595
+ 3000
596
+ );
597
+ });
598
+
599
+ try {
600
+ return await Promise.race([work(), timeout]);
601
+ } finally {
602
+ clearTimeout(timeoutHandle);
603
+ }
604
+ }
605
+ ```
606
+
607
+ **Step 4: 运行测试确认通过**
608
+
609
+ ```bash
610
+ npm test -- src/hooks/nexus/__tests__/session-end-hook.test.ts
611
+ ```
612
+ Expected: PASS (2 tests)
613
+
614
+ **Step 5: Commit**
615
+
616
+ ```bash
617
+ git add src/hooks/nexus/session-end-hook.ts src/hooks/nexus/__tests__/session-end-hook.test.ts
618
+ git commit -m "feat(nexus): add session-end hook integration"
619
+ ```
620
+
621
+ ---
622
+
623
+ ### Task 5: 注册 nexus hook 到 processSessionEnd
624
+
625
+ **Files:**
626
+ - Modify: `src/hooks/session-end/index.ts`(在 reflectOnSessionEnd 之后添加 nexus 调用)
627
+
628
+ **Step 1: 写失败测试**
629
+
630
+ ```typescript
631
+ // src/hooks/session-end/__tests__/nexus-integration.test.ts
632
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
633
+ import { mkdirSync, rmSync, writeFileSync } from 'fs';
634
+ import { join } from 'path';
635
+ import { tmpdir } from 'os';
636
+
637
+ describe('processSessionEnd nexus integration', () => {
638
+ let tmpDir: string;
639
+
640
+ beforeEach(() => {
641
+ tmpDir = join(tmpdir(), `nexus-se-int-${Date.now()}`);
642
+ mkdirSync(join(tmpDir, '.omc', 'nexus'), { recursive: true });
643
+ writeFileSync(
644
+ join(tmpDir, '.omc', 'nexus', 'config.json'),
645
+ JSON.stringify({ enabled: true, gitRemote: '' })
646
+ );
647
+ });
648
+
649
+ afterEach(() => {
650
+ rmSync(tmpDir, { recursive: true, force: true });
651
+ });
652
+
653
+ it('processSessionEnd completes without throwing when nexus enabled', async () => {
654
+ const { processSessionEnd } = await import('../index.js');
655
+ const result = await processSessionEnd({
656
+ session_id: 'test-nexus-int-001',
657
+ transcript_path: '',
658
+ cwd: tmpDir,
659
+ permission_mode: 'default',
660
+ hook_event_name: 'SessionEnd',
661
+ reason: 'other',
662
+ });
663
+ expect(result.continue).toBe(true);
664
+ });
665
+ });
666
+ ```
667
+
668
+ **Step 2: 运行测试确认失败**
669
+
670
+ ```bash
671
+ npm test -- src/hooks/session-end/__tests__/nexus-integration.test.ts
672
+ ```
673
+ Expected: FAIL — 因为 `../nexus/session-end-hook.js` 模块尚不存在,动态 import 会抛出 MODULE_NOT_FOUND 错误,导致测试失败。Step 3 添加该模块后测试才会通过。
674
+
675
+ **Step 3: 修改 processSessionEnd**
676
+
677
+ 在 `src/hooks/session-end/index.ts` 的 `reflectOnSessionEnd` 调用块之后添加:
678
+
679
+ ```typescript
680
+ // Nexus consciousness sync: collect session data and push to nexus-daemon (best-effort)
681
+ try {
682
+ const { handleNexusSessionEnd } = await import('../nexus/session-end-hook.js');
683
+ await handleNexusSessionEnd({
684
+ sessionId: input.session_id,
685
+ directory: resolveToWorktreeRoot(input.cwd),
686
+ durationMs: metrics.duration_ms,
687
+ agentsSpawned: metrics.agents_spawned,
688
+ agentsCompleted: metrics.agents_completed,
689
+ modesUsed: metrics.modes_used,
690
+ });
691
+ } catch {
692
+ // Nexus failures must never block session end
693
+ }
694
+ ```
695
+
696
+ **Step 4: 运行测试确认通过**
697
+
698
+ ```bash
699
+ npm test -- src/hooks/session-end/__tests__/nexus-integration.test.ts
700
+ npm test -- src/hooks/nexus/
701
+ ```
702
+ Expected: PASS
703
+
704
+ **Step 5: Commit**
705
+
706
+ ```bash
707
+ git add src/hooks/session-end/index.ts src/hooks/session-end/__tests__/nexus-integration.test.ts
708
+ git commit -m "feat(nexus): register nexus hook in processSessionEnd"
709
+ ```
710
+
711
+ ---
712
+
713
+
714
+ ### Task 6: nexus-daemon 基础框架(Python)
715
+
716
+ **Files:**
717
+ - Create: `nexus-daemon/daemon.py`
718
+ - Create: `nexus-daemon/requirements.txt`
719
+ - Create: `nexus-daemon/README.md`
720
+ - Create: `nexus-daemon/tests/test_daemon.py`
721
+
722
+ **Step 1: 写失败测试**
723
+
724
+ ```python
725
+ # nexus-daemon/tests/test_daemon.py
726
+ import pytest
727
+ import json
728
+ import os
729
+ from pathlib import Path
730
+ from unittest.mock import patch, MagicMock
731
+
732
+ # Add parent to path
733
+ import sys
734
+ sys.path.insert(0, str(Path(__file__).parent.parent))
735
+
736
+ from daemon import NexusDaemon, DaemonConfig, load_config
737
+
738
+ class TestLoadConfig:
739
+ def test_returns_default_when_no_file(self, tmp_path):
740
+ config = load_config(tmp_path / 'nonexistent.json')
741
+ assert config.poll_interval == 60
742
+ assert config.openrouter_api_key == ''
743
+
744
+ def test_loads_from_file(self, tmp_path):
745
+ config_file = tmp_path / 'config.json'
746
+ config_file.write_text(json.dumps({
747
+ 'poll_interval': 30,
748
+ 'openrouter_api_key': 'sk-test-123'
749
+ }))
750
+ config = load_config(config_file)
751
+ assert config.poll_interval == 30
752
+ assert config.openrouter_api_key == 'sk-test-123'
753
+
754
+ class TestNexusDaemon:
755
+ def test_init_creates_directories(self, tmp_path):
756
+ daemon = NexusDaemon(repo_path=tmp_path)
757
+ assert (tmp_path / 'events').exists()
758
+ assert (tmp_path / 'improvements').exists()
759
+ assert (tmp_path / 'consciousness').exists()
760
+ assert (tmp_path / 'evolution').exists()
761
+
762
+ def test_get_new_events_returns_empty_when_no_files(self, tmp_path):
763
+ daemon = NexusDaemon(repo_path=tmp_path)
764
+ events = daemon.get_new_events()
765
+ assert events == []
766
+
767
+ def test_get_new_events_returns_unprocessed_files(self, tmp_path):
768
+ daemon = NexusDaemon(repo_path=tmp_path)
769
+ event_file = tmp_path / 'events' / 'sess-001-1234567890.json'
770
+ event_file.write_text(json.dumps({
771
+ 'sessionId': 'sess-001',
772
+ 'timestamp': '2026-02-26T14:00:00Z',
773
+ 'toolCalls': [],
774
+ 'agentsSpawned': 2,
775
+ }))
776
+ events = daemon.get_new_events()
777
+ assert len(events) == 1
778
+ assert events[0]['sessionId'] == 'sess-001'
779
+
780
+ def test_mark_event_processed(self, tmp_path):
781
+ daemon = NexusDaemon(repo_path=tmp_path)
782
+ event_file = tmp_path / 'events' / 'sess-002-9999.json'
783
+ event_file.write_text('{}')
784
+ daemon.mark_event_processed('sess-002-9999.json')
785
+ # Second call should not return this file
786
+ events = daemon.get_new_events()
787
+ assert all(e.get('_filename') != 'sess-002-9999.json' for e in events)
788
+ ```
789
+
790
+ **Step 2: 运行测试确认失败**
791
+
792
+ ```bash
793
+ cd nexus-daemon && pip install pytest && python -m pytest tests/test_daemon.py -v
794
+ ```
795
+ Expected: FAIL — `ModuleNotFoundError: No module named 'daemon'`
796
+
797
+ **Step 3: 实现 daemon.py**
798
+
799
+ ```python
800
+ # nexus-daemon/daemon.py
801
+ """
802
+ nexus-daemon: VPS 守护进程
803
+ 每分钟 git pull 拉取新事件,运行进化引擎,生成改进建议推回仓库。
804
+ """
805
+ from __future__ import annotations
806
+
807
+ import asyncio
808
+ import json
809
+ import logging
810
+ import os
811
+ import subprocess
812
+ import sys
813
+ from dataclasses import dataclass, field
814
+ from pathlib import Path
815
+ from typing import Any
816
+
817
+ logging.basicConfig(
818
+ level=logging.INFO,
819
+ format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
820
+ )
821
+ logger = logging.getLogger('nexus-daemon')
822
+
823
+
824
+ @dataclass
825
+ class DaemonConfig:
826
+ poll_interval: int = 60 # seconds between git pull cycles
827
+ openrouter_api_key: str = ''
828
+ openrouter_model: str = 'anthropic/claude-3-5-haiku'
829
+ telegram_token: str = ''
830
+ telegram_chat_id: str = ''
831
+ consciousness_interval: int = 300 # seconds between consciousness loops
832
+ consciousness_budget_percent: int = 10
833
+
834
+
835
+ def load_config(config_path: Path) -> DaemonConfig:
836
+ """Load config from JSON file, return defaults on missing/error."""
837
+ try:
838
+ if not config_path.exists():
839
+ return DaemonConfig()
840
+ raw = json.loads(config_path.read_text())
841
+ return DaemonConfig(
842
+ poll_interval=raw.get('poll_interval', 60),
843
+ openrouter_api_key=raw.get('openrouter_api_key', ''),
844
+ openrouter_model=raw.get('openrouter_model', 'anthropic/claude-3-5-haiku'),
845
+ telegram_token=raw.get('telegram_token', ''),
846
+ telegram_chat_id=raw.get('telegram_chat_id', ''),
847
+ consciousness_interval=raw.get('consciousness_interval', 300),
848
+ consciousness_budget_percent=raw.get('consciousness_budget_percent', 10),
849
+ )
850
+ except Exception:
851
+ return DaemonConfig()
852
+
853
+
854
+ class NexusDaemon:
855
+ """Core daemon: git pull loop + event processing."""
856
+
857
+ PROCESSED_LOG = '.processed_events.json'
858
+
859
+ def __init__(self, repo_path: Path, config: DaemonConfig | None = None):
860
+ self.repo_path = Path(repo_path)
861
+ self.config = config or DaemonConfig()
862
+ self._processed: set[str] = set()
863
+ self._ensure_dirs()
864
+ self._load_processed_log()
865
+
866
+ def _ensure_dirs(self) -> None:
867
+ for d in ['events', 'improvements', 'consciousness', 'evolution']:
868
+ (self.repo_path / d).mkdir(parents=True, exist_ok=True)
869
+
870
+ def _processed_log_path(self) -> Path:
871
+ return self.repo_path / self.PROCESSED_LOG
872
+
873
+ def _load_processed_log(self) -> None:
874
+ try:
875
+ path = self._processed_log_path()
876
+ if path.exists():
877
+ self._processed = set(json.loads(path.read_text()))
878
+ except Exception:
879
+ self._processed = set()
880
+
881
+ def _save_processed_log(self) -> None:
882
+ try:
883
+ self._processed_log_path().write_text(
884
+ json.dumps(sorted(self._processed), indent=2)
885
+ )
886
+ except Exception:
887
+ pass
888
+
889
+ def git_pull(self) -> bool:
890
+ """Pull latest from remote. Returns True on success."""
891
+ try:
892
+ result = subprocess.run(
893
+ ['git', 'fetch', 'origin'],
894
+ cwd=self.repo_path,
895
+ capture_output=True,
896
+ text=True,
897
+ timeout=30,
898
+ )
899
+ if result.returncode != 0:
900
+ logger.warning('git fetch failed: %s', result.stderr.strip())
901
+ return False
902
+ result = subprocess.run(
903
+ ['git', 'rebase', 'origin/main'],
904
+ cwd=self.repo_path,
905
+ capture_output=True,
906
+ text=True,
907
+ timeout=30,
908
+ )
909
+ if result.returncode == 0:
910
+ logger.info('git pull: success')
911
+ return True
912
+ logger.warning('git pull failed: %s', result.stderr.strip())
913
+ return False
914
+ except Exception as e:
915
+ logger.error('git pull error: %s', e)
916
+ return False
917
+
918
+ def git_push(self, message: str) -> bool:
919
+ """Commit and push improvements. Returns True on success."""
920
+ try:
921
+ subprocess.run(['git', 'add', 'improvements/', 'evolution/'], cwd=self.repo_path,
922
+ capture_output=True, timeout=10)
923
+ result = subprocess.run(
924
+ ['git', 'commit', '-m', message],
925
+ cwd=self.repo_path, capture_output=True, text=True, timeout=10,
926
+ )
927
+ if result.returncode != 0:
928
+ return False # nothing to commit
929
+ subprocess.run(['git', 'push', 'origin', 'HEAD'],
930
+ cwd=self.repo_path, capture_output=True, timeout=30)
931
+ return True
932
+ except Exception as e:
933
+ logger.error('git push error: %s', e)
934
+ return False
935
+
936
+ def get_new_events(self) -> list[dict[str, Any]]:
937
+ """Return unprocessed event files from events/ directory."""
938
+ events_dir = self.repo_path / 'events'
939
+ result = []
940
+ for f in sorted(events_dir.glob('*.json')):
941
+ if f.name in self._processed:
942
+ continue
943
+ try:
944
+ data = json.loads(f.read_text())
945
+ data['_filename'] = f.name
946
+ result.append(data)
947
+ except Exception:
948
+ pass
949
+ return result
950
+
951
+ def mark_event_processed(self, filename: str) -> None:
952
+ self._processed.add(filename)
953
+ self._save_processed_log()
954
+
955
+ async def run_once(self) -> None:
956
+ """Single poll cycle: pull → process events → push."""
957
+ self.git_pull()
958
+ events = self.get_new_events()
959
+ if not events:
960
+ return
961
+ logger.info('Processing %d new event(s)', len(events))
962
+ for event in events:
963
+ try:
964
+ await self._process_event(event)
965
+ self.mark_event_processed(event['_filename'])
966
+ except Exception as e:
967
+ logger.error('Error processing event %s: %s', event.get('_filename'), e)
968
+
969
+ async def _process_event(self, event: dict[str, Any]) -> None:
970
+ """Placeholder: route to evolution engine."""
971
+ logger.info('Event received: session=%s agents=%s',
972
+ event.get('sessionId', '?'),
973
+ event.get('agentsSpawned', 0))
974
+
975
+ async def run(self) -> None:
976
+ """Main loop: poll every config.poll_interval seconds."""
977
+ logger.info('nexus-daemon started (poll_interval=%ds)', self.config.poll_interval)
978
+ while True:
979
+ try:
980
+ await self.run_once()
981
+ except Exception as e:
982
+ logger.error('run_once error: %s', e)
983
+ await asyncio.sleep(self.config.poll_interval)
984
+
985
+
986
+ def main() -> None:
987
+ repo_path = Path(os.environ.get('NEXUS_REPO_PATH', '.'))
988
+ config_path = repo_path / 'config.json'
989
+ config = load_config(config_path)
990
+ daemon = NexusDaemon(repo_path=repo_path, config=config)
991
+ asyncio.run(daemon.run())
992
+
993
+
994
+ if __name__ == '__main__':
995
+ main()
996
+ ```
997
+
998
+ **Step 4: 创建 requirements.txt**
999
+
1000
+ ```
1001
+ # nexus-daemon/requirements.txt
1002
+ aiohttp>=3.9.0
1003
+ pytest>=7.0.0
1004
+ pytest-asyncio>=0.23.0
1005
+ ```
1006
+
1007
+ **Step 5: 运行测试确认通过**
1008
+
1009
+ ```bash
1010
+ cd nexus-daemon && python -m pytest tests/test_daemon.py -v
1011
+ ```
1012
+ Expected: PASS (6 tests)
1013
+
1014
+ **Step 6: Commit**
1015
+
1016
+ ```bash
1017
+ git add nexus-daemon/
1018
+ git commit -m "feat(nexus): add nexus-daemon Python base framework"
1019
+ ```
1020
+
1021
+ ---
1022
+
1023
+ ## P1:进化引擎与改进拉取
1024
+
1025
+ ### Task 7: Evolution Engine MVP(模式检测 + knowledge_base 写入)
1026
+
1027
+ **Files:**
1028
+ - Create: `nexus-daemon/evolution_engine.py`
1029
+ - Create: `nexus-daemon/tests/test_evolution_engine.py`
1030
+ - Modify: `nexus-daemon/daemon.py`
1031
+
1032
+ **Step 1: 写失败测试**
1033
+
1034
+ ```python
1035
+ # nexus-daemon/tests/test_evolution_engine.py
1036
+ import pytest
1037
+ import json
1038
+ from pathlib import Path
1039
+ import sys
1040
+ sys.path.insert(0, str(Path(__file__).parent.parent))
1041
+
1042
+ from evolution_engine import EvolutionEngine, PatternRecord, detect_patterns
1043
+
1044
+ class TestDetectPatterns:
1045
+ def test_returns_empty_for_no_events(self):
1046
+ patterns = detect_patterns([])
1047
+ assert patterns == []
1048
+
1049
+ def test_detects_repeated_modes(self):
1050
+ events = [
1051
+ {'modesUsed': ['ultrawork'], 'agentsSpawned': 3},
1052
+ {'modesUsed': ['ultrawork'], 'agentsSpawned': 2},
1053
+ {'modesUsed': ['ultrawork'], 'agentsSpawned': 4},
1054
+ ]
1055
+ patterns = detect_patterns(events)
1056
+ mode_patterns = [p for p in patterns if p.pattern_type == 'mode_usage']
1057
+ assert any(p.value == 'ultrawork' and p.occurrences >= 3 for p in mode_patterns)
1058
+
1059
+ def test_pattern_below_threshold_not_promoted(self):
1060
+ events = [
1061
+ {'modesUsed': ['ralph'], 'agentsSpawned': 1},
1062
+ {'modesUsed': ['ralph'], 'agentsSpawned': 1},
1063
+ ]
1064
+ patterns = detect_patterns(events)
1065
+ promoted = [p for p in patterns if p.occurrences >= 3]
1066
+ assert len(promoted) == 0
1067
+
1068
+ class TestEvolutionEngine:
1069
+ def test_init_creates_knowledge_base(self, tmp_path):
1070
+ engine = EvolutionEngine(repo_path=tmp_path)
1071
+ assert (tmp_path / 'evolution' / 'knowledge_base.md').exists()
1072
+
1073
+ def test_process_events_updates_knowledge_base(self, tmp_path):
1074
+ engine = EvolutionEngine(repo_path=tmp_path)
1075
+ events = [
1076
+ {'sessionId': f'sess-{i}', 'modesUsed': ['ultrawork'],
1077
+ 'agentsSpawned': 3, 'agentsCompleted': 3, 'toolCalls': []}
1078
+ for i in range(3)
1079
+ ]
1080
+ engine.process_events(events)
1081
+ kb = (tmp_path / 'evolution' / 'knowledge_base.md').read_text()
1082
+ assert 'ultrawork' in kb
1083
+
1084
+ def test_process_events_writes_pattern_library(self, tmp_path):
1085
+ engine = EvolutionEngine(repo_path=tmp_path)
1086
+ events = [
1087
+ {'sessionId': f's{i}', 'modesUsed': ['ralph'],
1088
+ 'agentsSpawned': 2, 'agentsCompleted': 2, 'toolCalls': []}
1089
+ for i in range(3)
1090
+ ]
1091
+ engine.process_events(events)
1092
+ pl = (tmp_path / 'evolution' / 'pattern_library.md').read_text()
1093
+ assert 'ralph' in pl
1094
+ ```
1095
+
1096
+ **Step 2: 运行测试确认失败**
1097
+
1098
+ ```bash
1099
+ cd nexus-daemon && python -m pytest tests/test_evolution_engine.py -v
1100
+ ```
1101
+ Expected: FAIL — `ModuleNotFoundError: No module named 'evolution_engine'`
1102
+
1103
+ **Step 3: 实现 evolution_engine.py**
1104
+
1105
+ ```python
1106
+ # nexus-daemon/evolution_engine.py
1107
+ from __future__ import annotations
1108
+ import logging
1109
+ from collections import Counter
1110
+ from dataclasses import dataclass, field
1111
+ from datetime import datetime, timezone
1112
+ from pathlib import Path
1113
+ from typing import Any
1114
+
1115
+ logger = logging.getLogger('nexus.evolution')
1116
+ PATTERN_THRESHOLD = 3
1117
+
1118
+ @dataclass
1119
+ class PatternRecord:
1120
+ pattern_type: str
1121
+ value: str
1122
+ occurrences: int
1123
+ confidence: int
1124
+ first_seen: str
1125
+ last_seen: str
1126
+ evidence: list[str] = field(default_factory=list)
1127
+
1128
+ def detect_patterns(events: list[dict[str, Any]]) -> list[PatternRecord]:
1129
+ if not events:
1130
+ return []
1131
+ now = datetime.now(timezone.utc).isoformat()
1132
+ patterns: list[PatternRecord] = []
1133
+ mode_counter: Counter[str] = Counter()
1134
+ for event in events:
1135
+ for mode in event.get('modesUsed', []):
1136
+ mode_counter[mode] += 1
1137
+ for mode, count in mode_counter.items():
1138
+ confidence = min(100, 50 + count * 10)
1139
+ patterns.append(PatternRecord(
1140
+ pattern_type='mode_usage', value=mode, occurrences=count,
1141
+ confidence=confidence, first_seen=now, last_seen=now,
1142
+ evidence=[f'Appeared in {count} sessions'],
1143
+ ))
1144
+ return patterns
1145
+
1146
+ class EvolutionEngine:
1147
+ KNOWLEDGE_BASE = 'evolution/knowledge_base.md'
1148
+ PATTERN_LIBRARY = 'evolution/pattern_library.md'
1149
+
1150
+ def __init__(self, repo_path: Path):
1151
+ self.repo_path = Path(repo_path)
1152
+ self._ensure_files()
1153
+
1154
+ def _ensure_files(self) -> None:
1155
+ evo_dir = self.repo_path / 'evolution'
1156
+ evo_dir.mkdir(parents=True, exist_ok=True)
1157
+ kb = self.repo_path / self.KNOWLEDGE_BASE
1158
+ if not kb.exists():
1159
+ kb.write_text('# Knowledge Base\n\n_Auto-generated by nexus_\n\n')
1160
+ pl = self.repo_path / self.PATTERN_LIBRARY
1161
+ if not pl.exists():
1162
+ pl.write_text('# Pattern Library\n\n_Patterns promoted after >= 3 occurrences_\n\n')
1163
+
1164
+ def process_events(self, events: list[dict[str, Any]]) -> list[PatternRecord]:
1165
+ if not events:
1166
+ return []
1167
+ patterns = detect_patterns(events)
1168
+ promoted = [p for p in patterns if p.occurrences >= PATTERN_THRESHOLD]
1169
+ if promoted:
1170
+ self._update_knowledge_base(promoted, events)
1171
+ self._update_pattern_library(promoted)
1172
+ logger.info('Promoted %d pattern(s)', len(promoted))
1173
+ return promoted
1174
+
1175
+ def _update_knowledge_base(self, patterns: list[PatternRecord], events: list[dict]) -> None:
1176
+ kb_path = self.repo_path / self.KNOWLEDGE_BASE
1177
+ now = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')
1178
+ lines = [f'\n## Update: {now}\n\nProcessed {len(events)} session(s):\n\n']
1179
+ for p in patterns:
1180
+ lines.append(f'- **{p.pattern_type}** `{p.value}`: {p.occurrences} occurrences, confidence={p.confidence}\n')
1181
+ with open(kb_path, 'a', encoding='utf-8') as f:
1182
+ f.writelines(lines)
1183
+
1184
+ def _update_pattern_library(self, patterns: list[PatternRecord]) -> None:
1185
+ pl_path = self.repo_path / self.PATTERN_LIBRARY
1186
+ now = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')
1187
+ lines = [f'\n## {now}\n\n']
1188
+ for p in patterns:
1189
+ lines.append(
1190
+ f'### {p.pattern_type}: {p.value}\n'
1191
+ f'- Occurrences: {p.occurrences}\n'
1192
+ f'- Confidence: {p.confidence}\n\n'
1193
+ )
1194
+ with open(pl_path, 'a', encoding='utf-8') as f:
1195
+ f.writelines(lines)
1196
+ ```
1197
+
1198
+ **Step 4: 集成到 daemon.py**
1199
+
1200
+ 在 `NexusDaemon.__init__` 末尾添加:
1201
+
1202
+ ```python
1203
+ from evolution_engine import EvolutionEngine
1204
+ self._evolution = EvolutionEngine(repo_path=self.repo_path)
1205
+ self._pending_events: list[dict] = []
1206
+ ```
1207
+
1208
+ 将 `_process_event` 替换为:
1209
+
1210
+ ```python
1211
+ async def _process_event(self, event: dict[str, Any]) -> None:
1212
+ logger.info('Event: session=%s agents=%s', event.get('sessionId','?'), event.get('agentsSpawned',0))
1213
+ self._pending_events.append(event)
1214
+ ```
1215
+
1216
+ 在 `run_once` 的事件循环结束后追加:
1217
+
1218
+ ```python
1219
+ if self._pending_events:
1220
+ promoted = self._evolution.process_events(self._pending_events)
1221
+ if promoted:
1222
+ self.git_push(f'nexus: evolution promoted {len(promoted)} pattern(s)')
1223
+ self._pending_events = []
1224
+ ```
1225
+
1226
+ **Step 5: 运行测试确认通过**
1227
+
1228
+ ```bash
1229
+ cd nexus-daemon && python -m pytest tests/ -v
1230
+ ```
1231
+ Expected: PASS (10 tests)
1232
+
1233
+ **Step 6: Commit**
1234
+
1235
+ ```bash
1236
+ git add nexus-daemon/evolution_engine.py nexus-daemon/tests/test_evolution_engine.py nexus-daemon/daemon.py
1237
+ git commit -m "feat(nexus): add Evolution Engine MVP with pattern detection"
1238
+ ```
1239
+
1240
+ ---
1241
+
1242
+ ---
1243
+
1244
+ ### Task 8: improvement-puller hook(拉取并应用改进建议)
1245
+
1246
+ **Files:**
1247
+ - Create: `src/hooks/nexus/improvement-puller.ts`
1248
+ - Create: `src/hooks/nexus/__tests__/improvement-puller.test.ts`
1249
+
1250
+ **Step 1: 写失败测试**
1251
+
1252
+ ```typescript
1253
+ // src/hooks/nexus/__tests__/improvement-puller.test.ts
1254
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
1255
+ import { mkdirSync, rmSync, writeFileSync } from 'fs';
1256
+ import { join } from 'path';
1257
+ import { tmpdir } from 'os';
1258
+ import { getImprovementsDir, loadPendingImprovements } from '../improvement-puller.js';
1259
+ import type { ImprovementSuggestion } from '../types.js';
1260
+
1261
+ describe('loadPendingImprovements', () => {
1262
+ let tmpDir: string;
1263
+
1264
+ beforeEach(() => {
1265
+ tmpDir = join(tmpdir(), `nexus-ip-test-${Date.now()}`);
1266
+ mkdirSync(join(tmpDir, '.omc', 'nexus', 'improvements'), { recursive: true });
1267
+ });
1268
+
1269
+ afterEach(() => {
1270
+ rmSync(tmpDir, { recursive: true, force: true });
1271
+ });
1272
+
1273
+ it('returns empty array when no improvement files', () => {
1274
+ const improvements = loadPendingImprovements(tmpDir);
1275
+ expect(improvements).toEqual([]);
1276
+ });
1277
+
1278
+ it('loads pending improvement files', () => {
1279
+ const imp: ImprovementSuggestion = {
1280
+ id: 'imp-001',
1281
+ createdAt: '2026-02-26T14:00:00Z',
1282
+ source: 'evolution_engine',
1283
+ type: 'skill_update',
1284
+ targetFile: 'skills/learner/SKILL.md',
1285
+ confidence: 85,
1286
+ diff: '--- a/skills/learner/SKILL.md\n+++ b/skills/learner/SKILL.md\n@@ -1 +1 @@\n-old\n+new',
1287
+ reason: 'test reason',
1288
+ evidence: {},
1289
+ status: 'pending',
1290
+ testResult: null,
1291
+ };
1292
+ writeFileSync(
1293
+ join(tmpDir, '.omc', 'nexus', 'improvements', 'imp-001.json'),
1294
+ JSON.stringify(imp)
1295
+ );
1296
+ const improvements = loadPendingImprovements(tmpDir);
1297
+ expect(improvements.length).toBe(1);
1298
+ expect(improvements[0].id).toBe('imp-001');
1299
+ expect(improvements[0].confidence).toBe(85);
1300
+ });
1301
+
1302
+ it('skips non-pending improvements', () => {
1303
+ const imp: ImprovementSuggestion = {
1304
+ id: 'imp-002',
1305
+ createdAt: '2026-02-26T14:00:00Z',
1306
+ source: 'evolution_engine',
1307
+ type: 'skill_update',
1308
+ targetFile: 'skills/test/SKILL.md',
1309
+ confidence: 90,
1310
+ diff: '',
1311
+ reason: 'already applied',
1312
+ evidence: {},
1313
+ status: 'applied',
1314
+ testResult: 'PASS',
1315
+ };
1316
+ writeFileSync(
1317
+ join(tmpDir, '.omc', 'nexus', 'improvements', 'imp-002.json'),
1318
+ JSON.stringify(imp)
1319
+ );
1320
+ const improvements = loadPendingImprovements(tmpDir);
1321
+ expect(improvements).toEqual([]);
1322
+ });
1323
+ });
1324
+ ```
1325
+
1326
+ **Step 2: 运行测试确认失败**
1327
+
1328
+ ```bash
1329
+ npm test -- src/hooks/nexus/__tests__/improvement-puller.test.ts
1330
+ ```
1331
+ Expected: FAIL — `Cannot find module '../improvement-puller.js'`
1332
+
1333
+ **Step 3: 实现 improvement-puller.ts**
1334
+
1335
+ ```typescript
1336
+ // src/hooks/nexus/improvement-puller.ts
1337
+ import { existsSync, readdirSync, readFileSync } from 'fs';
1338
+ import { join } from 'path';
1339
+ import { isNexusEnabled } from './config.js';
1340
+ import type { ImprovementSuggestion } from './types.js';
1341
+
1342
+ const IMPROVEMENTS_SUBDIR = '.omc/nexus/improvements';
1343
+
1344
+ export function getImprovementsDir(directory: string): string {
1345
+ return join(directory, IMPROVEMENTS_SUBDIR);
1346
+ }
1347
+
1348
+ export function loadPendingImprovements(directory: string): ImprovementSuggestion[] {
1349
+ const improvementsDir = getImprovementsDir(directory);
1350
+ if (!existsSync(improvementsDir)) {
1351
+ return [];
1352
+ }
1353
+ const results: ImprovementSuggestion[] = [];
1354
+ for (const file of readdirSync(improvementsDir)) {
1355
+ if (!file.endsWith('.json')) continue;
1356
+ try {
1357
+ const raw = readFileSync(join(improvementsDir, file), 'utf-8');
1358
+ const imp = JSON.parse(raw) as ImprovementSuggestion;
1359
+ if (imp.status === 'pending') {
1360
+ results.push(imp);
1361
+ }
1362
+ } catch {
1363
+ // Skip malformed files
1364
+ }
1365
+ }
1366
+ return results;
1367
+ }
1368
+
1369
+ export interface PullResult {
1370
+ found: number;
1371
+ autoApplied: number;
1372
+ pendingReview: number;
1373
+ errors: string[];
1374
+ }
1375
+
1376
+ export async function pullImprovements(directory: string): Promise<PullResult> {
1377
+ const result: PullResult = { found: 0, autoApplied: 0, pendingReview: 0, errors: [] };
1378
+ try {
1379
+ if (!isNexusEnabled(directory)) return result;
1380
+ const improvements = loadPendingImprovements(directory);
1381
+ result.found = improvements.length;
1382
+ // Routing by confidence is handled by the caller (session-end-hook or PreToolUse)
1383
+ result.pendingReview = improvements.length;
1384
+ } catch (e) {
1385
+ result.errors.push(e instanceof Error ? e.message : String(e));
1386
+ }
1387
+ return result;
1388
+ }
1389
+ ```
1390
+
1391
+ **Step 4: 运行测试确认通过**
1392
+
1393
+ ```bash
1394
+ npm test -- src/hooks/nexus/__tests__/improvement-puller.test.ts
1395
+ ```
1396
+ Expected: PASS (3 tests)
1397
+
1398
+ **Step 5: Commit**
1399
+
1400
+ ```bash
1401
+ git add src/hooks/nexus/improvement-puller.ts src/hooks/nexus/__tests__/improvement-puller.test.ts
1402
+ git commit -m "feat(nexus): add improvement-puller hook"
1403
+ ```
1404
+
1405
+ ---
1406
+
1407
+ ### Task 9: Telegram Bot 通知(Python)
1408
+
1409
+ **Files:**
1410
+ - Create: `nexus-daemon/telegram_bot.py`
1411
+ - Create: `nexus-daemon/tests/test_telegram_bot.py`
1412
+ - Modify: `nexus-daemon/daemon.py`(集成 Telegram 通知)
1413
+
1414
+ **Step 1: 写失败测试**
1415
+
1416
+ ```python
1417
+ # nexus-daemon/tests/test_telegram_bot.py
1418
+ import pytest
1419
+ from unittest.mock import patch, AsyncMock, MagicMock
1420
+ import sys
1421
+ from pathlib import Path
1422
+ sys.path.insert(0, str(Path(__file__).parent.parent))
1423
+
1424
+ from telegram_bot import TelegramBot, format_improvement_message
1425
+
1426
+ class TestFormatImprovementMessage:
1427
+ def test_includes_id_and_confidence(self):
1428
+ imp = {
1429
+ 'id': 'imp-001',
1430
+ 'confidence': 85,
1431
+ 'type': 'skill_update',
1432
+ 'targetFile': 'skills/learner/SKILL.md',
1433
+ 'reason': 'test reason',
1434
+ }
1435
+ msg = format_improvement_message(imp)
1436
+ assert 'imp-001' in msg
1437
+ assert '85' in msg
1438
+ assert 'skill_update' in msg
1439
+
1440
+ def test_includes_auto_apply_label_when_high_confidence(self):
1441
+ imp = {
1442
+ 'id': 'imp-002',
1443
+ 'confidence': 90,
1444
+ 'type': 'skill_update',
1445
+ 'targetFile': 'skills/test/SKILL.md',
1446
+ 'reason': 'high confidence',
1447
+ }
1448
+ msg = format_improvement_message(imp)
1449
+ assert 'AUTO' in msg or 'auto' in msg.lower()
1450
+
1451
+ def test_includes_review_label_when_low_confidence(self):
1452
+ imp = {
1453
+ 'id': 'imp-003',
1454
+ 'confidence': 60,
1455
+ 'type': 'hook_update',
1456
+ 'targetFile': 'src/hooks/test.ts',
1457
+ 'reason': 'low confidence',
1458
+ }
1459
+ msg = format_improvement_message(imp)
1460
+ assert 'review' in msg.lower() or 'confirm' in msg.lower()
1461
+
1462
+ class TestTelegramBot:
1463
+ def test_init_disabled_when_no_token(self):
1464
+ bot = TelegramBot(token='', chat_id='')
1465
+ assert not bot.enabled
1466
+
1467
+ def test_init_enabled_when_token_provided(self):
1468
+ bot = TelegramBot(token='test-token', chat_id='12345')
1469
+ assert bot.enabled
1470
+
1471
+ @pytest.mark.asyncio
1472
+ async def test_send_message_returns_false_when_disabled(self):
1473
+ bot = TelegramBot(token='', chat_id='')
1474
+ result = await bot.send_message('test')
1475
+ assert result is False
1476
+ ```
1477
+
1478
+ **Step 2: 运行测试确认失败**
1479
+
1480
+ ```bash
1481
+ cd nexus-daemon && pip install pytest pytest-asyncio && python -m pytest tests/test_telegram_bot.py -v
1482
+ ```
1483
+ Expected: FAIL — `ModuleNotFoundError: No module named 'telegram_bot'`
1484
+
1485
+
1486
+
1487
+ **Step 3: 实现 telegram_bot.py**
1488
+
1489
+ ```python
1490
+ # nexus-daemon/telegram_bot.py
1491
+ """
1492
+ Telegram Bot: 发送改进通知,接收用户确认指令。
1493
+ """
1494
+ from __future__ import annotations
1495
+
1496
+ import logging
1497
+ from typing import Any
1498
+
1499
+ logger = logging.getLogger('nexus.telegram')
1500
+
1501
+ AUTO_APPLY_THRESHOLD = 80
1502
+
1503
+
1504
+ def format_improvement_message(imp: dict[str, Any], auto_apply_threshold: int = AUTO_APPLY_THRESHOLD) -> str:
1505
+ """格式化改进建议为 Telegram 消息。"""
1506
+ confidence = imp.get('confidence', 0)
1507
+ label = 'AUTO-APPLY' if confidence >= auto_apply_threshold else 'needs review/confirm'
1508
+ return (
1509
+ f"[{label}] nexus improvement\n"
1510
+ f"ID: {imp.get('id', '?')}\n"
1511
+ f"Type: {imp.get('type', '?')}\n"
1512
+ f"File: {imp.get('targetFile', '?')}\n"
1513
+ f"Confidence: {confidence}\n"
1514
+ f"Reason: {imp.get('reason', '?')}"
1515
+ )
1516
+
1517
+
1518
+ class TelegramBot:
1519
+ """Telegram Bot 客户端,用于发送通知和接收确认。"""
1520
+
1521
+ API_BASE = 'https://api.telegram.org/bot{token}/{method}'
1522
+
1523
+ def __init__(self, token: str, chat_id: str):
1524
+ self.enabled = bool(token and chat_id)
1525
+ self._token = token
1526
+ self._chat_id = chat_id
1527
+
1528
+ async def send_message(self, text: str) -> bool:
1529
+ """发送消息到 Telegram。返回 True 表示成功。"""
1530
+ if not self.enabled:
1531
+ return False
1532
+ try:
1533
+ import aiohttp
1534
+ url = self.API_BASE.format(token=self._token, method='sendMessage')
1535
+ payload = {'chat_id': self._chat_id, 'text': text, 'parse_mode': 'HTML'}
1536
+ async with aiohttp.ClientSession() as session:
1537
+ async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=10)) as resp:
1538
+ if resp.status == 200:
1539
+ logger.info('Telegram message sent')
1540
+ return True
1541
+ logger.warning('Telegram API error: %s', resp.status)
1542
+ return False
1543
+ except Exception as e:
1544
+ logger.error('Telegram send error: %s', e)
1545
+ return False
1546
+
1547
+ async def notify_improvement(self, imp: dict[str, Any]) -> bool:
1548
+ """发送改进建议通知。"""
1549
+ msg = format_improvement_message(imp)
1550
+ return await self.send_message(msg)
1551
+ ```
1552
+
1553
+ **Step 4: 集成到 daemon.py**
1554
+
1555
+ 在 `NexusDaemon.__init__` 末尾添加:
1556
+
1557
+ ```python
1558
+ from telegram_bot import TelegramBot
1559
+ self._telegram = TelegramBot(
1560
+ token=self.config.telegram_token,
1561
+ chat_id=self.config.telegram_chat_id,
1562
+ )
1563
+ self._notified_improvements: set[str] = set() # dedup: avoid re-notifying same improvement
1564
+ ```
1565
+
1566
+ 在 `run_once` 的进化引擎调用后追加:
1567
+
1568
+ ```python
1569
+ # Notify via Telegram for improvements needing review (with deduplication)
1570
+ if self._telegram.enabled:
1571
+ improvements_dir = self.repo_path / 'improvements'
1572
+ import json as _json
1573
+ for f in sorted(improvements_dir.glob('*.json')):
1574
+ imp_id = f.stem
1575
+ if imp_id in self._notified_improvements:
1576
+ continue # already notified this improvement
1577
+ try:
1578
+ imp = _json.loads(f.read_text())
1579
+ if imp.get('status') == 'pending':
1580
+ sent = await self._telegram.notify_improvement(imp)
1581
+ if sent:
1582
+ self._notified_improvements.add(imp_id)
1583
+ except Exception:
1584
+ pass
1585
+ ```
1586
+
1587
+ **Step 5: 运行测试确认通过**
1588
+
1589
+ ```bash
1590
+ cd nexus-daemon && python -m pytest tests/test_telegram_bot.py -v
1591
+ ```
1592
+ Expected: PASS (6 tests)
1593
+
1594
+ **Step 6: Commit**
1595
+
1596
+ ```bash
1597
+ git add nexus-daemon/telegram_bot.py nexus-daemon/tests/test_telegram_bot.py nexus-daemon/daemon.py
1598
+ git commit -m "feat(nexus): add Telegram Bot notifications"
1599
+ ```
1600
+
1601
+ ---
1602
+
1603
+
1604
+ ---
1605
+
1606
+ ### Task 10: Consciousness Loop(后台意识循环)
1607
+
1608
+ **Files:**
1609
+ - Create: `nexus-daemon/tests/test_consciousness_loop.py`
1610
+ - Create: `nexus-daemon/consciousness_loop.py`
1611
+ - Modify: `nexus-daemon/daemon.py`
1612
+
1613
+ **Step 1: 编写失败测试**
1614
+
1615
+ ```python
1616
+ # nexus-daemon/tests/test_consciousness_loop.py
1617
+ import pytest
1618
+ from pathlib import Path
1619
+ from unittest.mock import AsyncMock, MagicMock, patch
1620
+ import json
1621
+
1622
+ from consciousness_loop import ConsciousnessLoop, ConsciousnessConfig
1623
+
1624
+
1625
+ @pytest.fixture
1626
+ def tmp_repo(tmp_path):
1627
+ (tmp_path / 'consciousness').mkdir()
1628
+ (tmp_path / 'consciousness' / 'rounds').mkdir()
1629
+ return tmp_path
1630
+
1631
+
1632
+ def make_loop(tmp_repo):
1633
+ cfg = ConsciousnessConfig(
1634
+ interval_seconds=300,
1635
+ budget_percent=10,
1636
+ max_rounds_per_session=5,
1637
+ )
1638
+ return ConsciousnessLoop(repo_path=tmp_repo, config=cfg)
1639
+
1640
+
1641
+ def test_scratchpad_path(tmp_repo):
1642
+ loop = make_loop(tmp_repo)
1643
+ assert loop.scratchpad_path == tmp_repo / 'consciousness' / 'scratchpad.md'
1644
+
1645
+
1646
+ def test_rounds_dir(tmp_repo):
1647
+ loop = make_loop(tmp_repo)
1648
+ assert loop.rounds_dir == tmp_repo / 'consciousness' / 'rounds'
1649
+
1650
+
1651
+ def test_write_scratchpad(tmp_repo):
1652
+ loop = make_loop(tmp_repo)
1653
+ loop._write_scratchpad('test content')
1654
+ assert loop.scratchpad_path.read_text() == 'test content'
1655
+
1656
+
1657
+ def test_write_round_record(tmp_repo):
1658
+ loop = make_loop(tmp_repo)
1659
+ loop._write_round_record(round_num=1, content='round 1 thoughts')
1660
+ files = list(loop.rounds_dir.glob('round-*.md'))
1661
+ assert len(files) == 1
1662
+ assert 'round 1 thoughts' in files[0].read_text()
1663
+
1664
+
1665
+ def test_is_paused_when_busy(tmp_repo):
1666
+ loop = make_loop(tmp_repo)
1667
+ # Create a busy marker
1668
+ (tmp_repo / '.nexus-busy').write_text('1')
1669
+ assert loop.is_paused() is True
1670
+
1671
+
1672
+ def test_not_paused_when_idle(tmp_repo):
1673
+ loop = make_loop(tmp_repo)
1674
+ assert loop.is_paused() is False
1675
+ ```
1676
+
1677
+ **Step 2: 运行测试确认失败**
1678
+
1679
+ ```bash
1680
+ cd nexus-daemon && python -m pytest tests/test_consciousness_loop.py -v
1681
+ ```
1682
+ Expected: FAIL with `ModuleNotFoundError: No module named 'consciousness_loop'`
1683
+
1684
+ **Step 3: 实现 `nexus-daemon/consciousness_loop.py`**
1685
+
1686
+ ```python
1687
+ # nexus-daemon/consciousness_loop.py
1688
+ from __future__ import annotations
1689
+ import asyncio
1690
+ from dataclasses import dataclass, field
1691
+ from datetime import datetime
1692
+ from pathlib import Path
1693
+ from typing import Optional
1694
+
1695
+
1696
+ @dataclass
1697
+ class ConsciousnessConfig:
1698
+ interval_seconds: int = 300
1699
+ budget_percent: int = 10
1700
+ max_rounds_per_session: int = 5
1701
+
1702
+
1703
+ class ConsciousnessLoop:
1704
+ def __init__(self, repo_path: Path, config: ConsciousnessConfig):
1705
+ self.repo_path = repo_path
1706
+ self.config = config
1707
+ self._round_count = 0
1708
+
1709
+ @property
1710
+ def scratchpad_path(self) -> Path:
1711
+ return self.repo_path / 'consciousness' / 'scratchpad.md'
1712
+
1713
+ @property
1714
+ def rounds_dir(self) -> Path:
1715
+ return self.repo_path / 'consciousness' / 'rounds'
1716
+
1717
+ def is_paused(self) -> bool:
1718
+ return (self.repo_path / '.nexus-busy').exists()
1719
+
1720
+ def _write_scratchpad(self, content: str) -> None:
1721
+ self.scratchpad_path.parent.mkdir(parents=True, exist_ok=True)
1722
+ self.scratchpad_path.write_text(content)
1723
+
1724
+ def _write_round_record(self, round_num: int, content: str) -> None:
1725
+ self.rounds_dir.mkdir(parents=True, exist_ok=True)
1726
+ ts = datetime.utcnow().strftime('%Y%m%dT%H%M%SZ')
1727
+ path = self.rounds_dir / f'round-{round_num:04d}-{ts}.md'
1728
+ path.write_text(content)
1729
+
1730
+ async def run_once(self) -> Optional[str]:
1731
+ """Run one consciousness round. Returns scratchpad content or None if paused."""
1732
+ if self.is_paused():
1733
+ return None
1734
+ if self._round_count >= self.config.max_rounds_per_session:
1735
+ return None
1736
+
1737
+ self._round_count += 1
1738
+ ts = datetime.utcnow().isoformat()
1739
+ content = (
1740
+ f'# Consciousness Round {self._round_count}\n\n'
1741
+ f'**Timestamp:** {ts}\n\n'
1742
+ f'*Awaiting LLM reflection...*\n'
1743
+ )
1744
+ self._write_scratchpad(content)
1745
+ self._write_round_record(self._round_count, content)
1746
+ return content
1747
+
1748
+ async def run_loop(self) -> None:
1749
+ """Continuous loop: run_once every interval_seconds."""
1750
+ while True:
1751
+ await self.run_once()
1752
+ await asyncio.sleep(self.config.interval_seconds)
1753
+ ```
1754
+
1755
+ **Step 4: 集成到 `nexus-daemon/daemon.py`**
1756
+
1757
+ 在 `NexusDaemon.__init__` 中添加:
1758
+ ```python
1759
+ from consciousness_loop import ConsciousnessLoop, ConsciousnessConfig
1760
+
1761
+ # 在 __init__ 末尾添加:
1762
+ self._consciousness = ConsciousnessLoop(
1763
+ repo_path=self.repo_path,
1764
+ config=ConsciousnessConfig(
1765
+ interval_seconds=self.config.consciousness_interval,
1766
+ budget_percent=self.config.consciousness_budget_percent,
1767
+ ),
1768
+ )
1769
+ ```
1770
+
1771
+ 在 `run()` 方法中并行启动意识循环:
1772
+ ```python
1773
+ async def run(self) -> None:
1774
+ """Main daemon loop."""
1775
+ tasks = [
1776
+ asyncio.create_task(self._main_loop()),
1777
+ asyncio.create_task(self._consciousness.run_loop()),
1778
+ ]
1779
+ await asyncio.gather(*tasks)
1780
+
1781
+ async def _main_loop(self) -> None:
1782
+ """Git pull + evolution engine loop."""
1783
+ while True:
1784
+ await self.run_once()
1785
+ await asyncio.sleep(60)
1786
+ ```
1787
+
1788
+ **Step 5: 运行测试确认通过**
1789
+
1790
+ ```bash
1791
+ cd nexus-daemon && python -m pytest tests/test_consciousness_loop.py -v
1792
+ ```
1793
+ Expected: PASS (6 tests)
1794
+
1795
+ **Step 6: Commit**
1796
+
1797
+ ```bash
1798
+ git add nexus-daemon/consciousness_loop.py nexus-daemon/tests/test_consciousness_loop.py nexus-daemon/daemon.py
1799
+ git commit -m "feat(nexus): add Consciousness Loop background awareness"
1800
+ ```
1801
+
1802
+ ---
1803
+
1804
+ ### Task 11: Self-Evaluator(健康报告)
1805
+
1806
+ **Files:**
1807
+ - Create: `nexus-daemon/tests/test_self_evaluator.py`
1808
+ - Create: `nexus-daemon/self_evaluator.py`
1809
+ - Modify: `nexus-daemon/daemon.py`
1810
+
1811
+ **Step 1: 编写失败测试**
1812
+
1813
+ ```python
1814
+ # nexus-daemon/tests/test_self_evaluator.py
1815
+ import pytest
1816
+ from pathlib import Path
1817
+ import json
1818
+ from self_evaluator import SelfEvaluator, SkillStats, HealthReport
1819
+
1820
+
1821
+ @pytest.fixture
1822
+ def tmp_repo(tmp_path):
1823
+ events_dir = tmp_path / 'events'
1824
+ events_dir.mkdir()
1825
+ return tmp_path
1826
+
1827
+
1828
+ def make_event(session_id: str, skills_triggered: list[str]) -> dict:
1829
+ return {
1830
+ 'sessionId': session_id,
1831
+ 'timestamp': '2026-02-26T10:00:00Z',
1832
+ 'skillsTriggered': skills_triggered,
1833
+ }
1834
+
1835
+
1836
+ def test_empty_events_returns_empty_report(tmp_repo):
1837
+ ev = SelfEvaluator(repo_path=tmp_repo)
1838
+ report = ev.generate_report()
1839
+ assert isinstance(report, HealthReport)
1840
+ assert report.total_sessions == 0
1841
+ assert report.skill_stats == {}
1842
+
1843
+
1844
+ def test_counts_skill_usage(tmp_repo):
1845
+ (tmp_repo / 'events').mkdir(exist_ok=True)
1846
+ evt = make_event('s1', ['learner', 'autopilot'])
1847
+ (tmp_repo / 'events' / 's1.json').write_text(json.dumps(evt))
1848
+ ev = SelfEvaluator(repo_path=tmp_repo)
1849
+ report = ev.generate_report()
1850
+ assert report.skill_stats['learner'].trigger_count == 1
1851
+ assert report.skill_stats['autopilot'].trigger_count == 1
1852
+
1853
+
1854
+ def test_detects_zombie_skills(tmp_repo):
1855
+ (tmp_repo / 'events').mkdir(exist_ok=True)
1856
+ # 10 sessions, none trigger 'zombie-skill'
1857
+ for i in range(10):
1858
+ evt = make_event(f's{i}', ['learner'])
1859
+ (tmp_repo / 'events' / f's{i}.json').write_text(json.dumps(evt))
1860
+ ev = SelfEvaluator(repo_path=tmp_repo, zombie_threshold=5)
1861
+ report = ev.generate_report()
1862
+ assert 'zombie-skill' not in report.skill_stats
1863
+ assert report.total_sessions == 10
1864
+
1865
+
1866
+ def test_format_report_markdown(tmp_repo):
1867
+ ev = SelfEvaluator(repo_path=tmp_repo)
1868
+ report = HealthReport(total_sessions=3, skill_stats={}, zombie_skills=[])
1869
+ md = ev.format_report(report)
1870
+ assert '# nexus Health Report' in md
1871
+ assert 'Total sessions: 3' in md
1872
+ ```
1873
+
1874
+ **Step 2: 运行测试确认失败**
1875
+
1876
+ ```bash
1877
+ cd nexus-daemon && python -m pytest tests/test_self_evaluator.py -v
1878
+ ```
1879
+ Expected: FAIL with `ModuleNotFoundError: No module named 'self_evaluator'`
1880
+
1881
+ **Step 3: 实现 `nexus-daemon/self_evaluator.py`**
1882
+
1883
+ ```python
1884
+ # nexus-daemon/self_evaluator.py
1885
+ from __future__ import annotations
1886
+ import json
1887
+ from dataclasses import dataclass, field
1888
+ from datetime import datetime
1889
+ from pathlib import Path
1890
+ from typing import Optional
1891
+
1892
+
1893
+ @dataclass
1894
+ class SkillStats:
1895
+ trigger_count: int = 0
1896
+
1897
+
1898
+ @dataclass
1899
+ class HealthReport:
1900
+ total_sessions: int
1901
+ skill_stats: dict[str, SkillStats]
1902
+ zombie_skills: list[str] = field(default_factory=list)
1903
+ generated_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
1904
+
1905
+
1906
+ class SelfEvaluator:
1907
+ def __init__(self, repo_path: Path, zombie_threshold: int = 10):
1908
+ self.repo_path = repo_path
1909
+ self.zombie_threshold = zombie_threshold
1910
+
1911
+ def _load_events(self) -> list[dict]:
1912
+ events_dir = self.repo_path / 'events'
1913
+ if not events_dir.exists():
1914
+ return []
1915
+ events = []
1916
+ for f in events_dir.glob('*.json'):
1917
+ try:
1918
+ events.append(json.loads(f.read_text()))
1919
+ except Exception:
1920
+ pass
1921
+ return events
1922
+
1923
+ def generate_report(self) -> HealthReport:
1924
+ events = self._load_events()
1925
+ skill_stats: dict[str, SkillStats] = {}
1926
+
1927
+ for evt in events:
1928
+ for skill in evt.get('skillsTriggered', []):
1929
+ if skill not in skill_stats:
1930
+ skill_stats[skill] = SkillStats()
1931
+ skill_stats[skill].trigger_count += 1
1932
+
1933
+ # Detect zombie skills: sessions >= threshold but skill never triggered
1934
+ zombie_skills: list[str] = []
1935
+ if len(events) >= self.zombie_threshold:
1936
+ all_known = set()
1937
+ for evt in events:
1938
+ all_known.update(evt.get('skillsAvailable', []))
1939
+ for skill in all_known:
1940
+ if skill not in skill_stats or skill_stats[skill].trigger_count == 0:
1941
+ zombie_skills.append(skill)
1942
+
1943
+ return HealthReport(
1944
+ total_sessions=len(events),
1945
+ skill_stats=skill_stats,
1946
+ zombie_skills=sorted(zombie_skills),
1947
+ )
1948
+
1949
+ def format_report(self, report: HealthReport) -> str:
1950
+ lines = [
1951
+ '# nexus Health Report',
1952
+ f'',
1953
+ f'Generated: {report.generated_at}',
1954
+ f'Total sessions: {report.total_sessions}',
1955
+ '',
1956
+ '## Skill Usage',
1957
+ ]
1958
+ if not report.skill_stats:
1959
+ lines.append('No skill usage recorded.')
1960
+ else:
1961
+ for skill, stats in sorted(report.skill_stats.items()):
1962
+ lines.append(f'- `{skill}`: {stats.trigger_count} triggers')
1963
+ if report.zombie_skills:
1964
+ lines += ['', '## Zombie Skills (never triggered)', '']
1965
+ for z in report.zombie_skills:
1966
+ lines.append(f'- `{z}`')
1967
+ return '\n'.join(lines)
1968
+ ```
1969
+
1970
+ **Step 4: 集成到 `nexus-daemon/daemon.py`**
1971
+
1972
+ 在 `run_once()` 末尾添加每日健康报告逻辑:
1973
+ ```python
1974
+ from self_evaluator import SelfEvaluator
1975
+
1976
+ # 在 __init__ 末尾:
1977
+ self._evaluator = SelfEvaluator(repo_path=self.repo_path)
1978
+ self._last_report_date: str = ''
1979
+
1980
+ # 在 run_once() 末尾:
1981
+ today = datetime.utcnow().strftime('%Y-%m-%d')
1982
+ if today != self._last_report_date:
1983
+ self._last_report_date = today
1984
+ report = self._evaluator.generate_report()
1985
+ md = self._evaluator.format_report(report)
1986
+ report_path = self.repo_path / 'consciousness' / f'health-{today}.md'
1987
+ report_path.parent.mkdir(parents=True, exist_ok=True)
1988
+ report_path.write_text(md)
1989
+ if self._telegram.enabled:
1990
+ await self._telegram.send_message(f'Daily health report ready: {today}')
1991
+ ```
1992
+
1993
+ **Step 5: 运行测试确认通过**
1994
+
1995
+ ```bash
1996
+ cd nexus-daemon && python -m pytest tests/test_self_evaluator.py -v
1997
+ ```
1998
+ Expected: PASS (4 tests)
1999
+
2000
+ **Step 6: Commit**
2001
+
2002
+ ```bash
2003
+ git add nexus-daemon/self_evaluator.py nexus-daemon/tests/test_self_evaluator.py nexus-daemon/daemon.py
2004
+ git commit -m "feat(nexus): add Self-Evaluator health reports"
2005
+ ```
2006
+
2007
+ ---
2008
+
2009
+ ### Task 12: nexus 管理 Skills(nexus-status / nexus-evolve / nexus-review)
2010
+
2011
+ **Files:**
2012
+ - Create: `skills/nexus/nexus-status/SKILL.md`
2013
+ - Create: `skills/nexus/nexus-evolve/SKILL.md`
2014
+ - Create: `skills/nexus/nexus-review/SKILL.md`
2015
+
2016
+ **Step 1: 创建 `skills/nexus/nexus-status/SKILL.md`**
2017
+
2018
+ ```markdown
2019
+ ---
2020
+ name: nexus-status
2021
+ description: 查看 nexus 系统状态
2022
+ triggers:
2023
+ - nexus status
2024
+ - nexus-status
2025
+ - nexus health
2026
+ ---
2027
+
2028
+ # nexus Status
2029
+
2030
+ 显示 nexus 系统当前状态。
2031
+
2032
+ ## 执行步骤
2033
+
2034
+ 1. 读取 `.omc/nexus/config.json` 确认 nexus 是否启用
2035
+ 2. 检查 `.omc/nexus/events/` 目录,统计待推送事件数量
2036
+ 3. 检查 `.omc/nexus/improvements/` 目录,统计待审批改进数量
2037
+ 4. 输出状态摘要:
2038
+
2039
+ ```
2040
+ nexus Status
2041
+ ============
2042
+ Enabled: true/false
2043
+ Pending events: N
2044
+ Pending improvements: N
2045
+ Last sync: <timestamp or "never">
2046
+ ```
2047
+
2048
+ 如果 nexus 未启用,提示用户配置 `.omc/nexus/config.json`。
2049
+ ```
2050
+
2051
+ **Step 2: 创建 `skills/nexus/nexus-evolve/SKILL.md`**
2052
+
2053
+ ```markdown
2054
+ ---
2055
+ name: nexus-evolve
2056
+ description: 手动触发 nexus 进化引擎
2057
+ triggers:
2058
+ - nexus evolve
2059
+ - nexus-evolve
2060
+ - trigger evolution
2061
+ ---
2062
+
2063
+ # nexus Evolve
2064
+
2065
+ 手动触发 nexus 进化引擎,立即处理积累的会话数据。
2066
+
2067
+ ## 执行步骤
2068
+
2069
+ 1. 检查 `.omc/nexus/events/` 是否有未处理事件
2070
+ 2. 如果有,执行 git push 将事件推送到 nexus-daemon 仓库
2071
+ 3. 提示用户:进化引擎将在 VPS 端处理这些事件
2072
+ 4. 等待约 2 分钟后,执行 git pull 拉取改进建议
2073
+ 5. 如果有新的 `.omc/nexus/improvements/` 文件,显示摘要
2074
+
2075
+ ## 输出格式
2076
+
2077
+ ```
2078
+ nexus Evolve triggered
2079
+ ======================
2080
+ Events pushed: N
2081
+ Waiting for VPS processing...
2082
+ Improvements received: N
2083
+ Run /nexus-review to review pending improvements.
2084
+ ```
2085
+ ```
2086
+
2087
+ **Step 3: 创建 `skills/nexus/nexus-review/SKILL.md`**
2088
+
2089
+ ```markdown
2090
+ ---
2091
+ name: nexus-review
2092
+ description: 查看并审批 nexus 生成的改进建议
2093
+ triggers:
2094
+ - nexus review
2095
+ - nexus-review
2096
+ - review improvements
2097
+ ---
2098
+
2099
+ # nexus Review
2100
+
2101
+ 查看 nexus 系统生成的待审批改进建议。
2102
+
2103
+ ## 执行步骤
2104
+
2105
+ 1. 读取 `.omc/nexus/improvements/` 下所有 `status === "pending"` 的文件
2106
+ 2. 按置信度降序排列
2107
+ 3. 对每个改进建议显示:
2108
+ - ID、类型、目标文件
2109
+ - 置信度(≥80 标记为 AUTO-APPLY)
2110
+ - 原因和证据摘要
2111
+ - diff 内容
2112
+
2113
+ 4. 询问用户对每个建议的操作:
2114
+ - `apply`:应用改进,更新 status 为 `applied`
2115
+ - `skip`:跳过,更新 status 为 `rejected`
2116
+ - `auto`:自动应用所有置信度 ≥80 的建议
2117
+
2118
+ ## 输出格式
2119
+
2120
+ ```
2121
+ nexus Improvements (N pending)
2122
+ ==============================
2123
+ [1] skill_update | skills/learner/SKILL.md | confidence: 87 [AUTO-APPLY]
2124
+ Reason: 触发词 'learn' 在过去 10 次会话中出现 23 次但未触发
2125
+ Action? (apply/skip/auto):
2126
+ ```
2127
+ ```
2128
+
2129
+ **Step 4: 验证 skill 文件格式正确**
2130
+
2131
+ ```bash
2132
+ # 确认文件存在
2133
+ ls skills/nexus/nexus-status/SKILL.md skills/nexus/nexus-evolve/SKILL.md skills/nexus/nexus-review/SKILL.md
2134
+ ```
2135
+ Expected: 3 files listed without error
2136
+
2137
+ ```bash
2138
+ # 确认每个文件包含必要的 frontmatter 字段
2139
+ grep -l "^name:" skills/nexus/*/SKILL.md | wc -l
2140
+ grep -l "^triggers:" skills/nexus/*/SKILL.md | wc -l
2141
+ ```
2142
+ Expected: 两条命令均输出 `3`
2143
+
2144
+ **Step 5: Commit**
2145
+
2146
+ ```bash
2147
+ git add skills/nexus/
2148
+ git commit -m "feat(nexus): add nexus-status, nexus-evolve, nexus-review management skills"
2149
+ ```
2150
+
2151
+ ---
2152
+
2153
+ ### Task 13: systemd 服务文件(VPS 部署)
2154
+
2155
+ **Files:**
2156
+ - Create: `nexus-daemon/nexus-daemon.service`
2157
+ - Create: `nexus-daemon/install.sh`
2158
+
2159
+ **Step 1: 创建 `nexus-daemon/nexus-daemon.service`**
2160
+
2161
+ ```ini
2162
+ [Unit]
2163
+ Description=nexus Daemon - ultrapower self-improvement VPS service
2164
+ After=network.target
2165
+ Wants=network-online.target
2166
+
2167
+ [Service]
2168
+ Type=simple
2169
+ User=nexus
2170
+ WorkingDirectory=/opt/nexus-daemon
2171
+ ExecStart=/opt/nexus-daemon/venv/bin/python daemon.py
2172
+ Restart=on-failure
2173
+ RestartSec=10
2174
+ StandardOutput=journal
2175
+ StandardError=journal
2176
+ SyslogIdentifier=nexus-daemon
2177
+
2178
+ # Environment
2179
+ Environment=PYTHONUNBUFFERED=1
2180
+
2181
+ [Install]
2182
+ WantedBy=multi-user.target
2183
+ ```
2184
+
2185
+ **Step 2: 创建 `nexus-daemon/install.sh`**
2186
+
2187
+ ```bash
2188
+ #!/usr/bin/env bash
2189
+ set -euo pipefail
2190
+
2191
+ INSTALL_DIR=/opt/nexus-daemon
2192
+ SERVICE_FILE=/etc/systemd/system/nexus-daemon.service
2193
+
2194
+ echo "Installing nexus-daemon..."
2195
+
2196
+ # Create user
2197
+ id nexus &>/dev/null || useradd -r -s /bin/false nexus
2198
+
2199
+ # Copy files
2200
+ mkdir -p "$INSTALL_DIR"
2201
+ cp -r . "$INSTALL_DIR/"
2202
+ chown -R nexus:nexus "$INSTALL_DIR"
2203
+
2204
+ # Create virtualenv and install deps
2205
+ python3 -m venv "$INSTALL_DIR/venv"
2206
+ "$INSTALL_DIR/venv/bin/pip" install -r "$INSTALL_DIR/requirements.txt"
2207
+
2208
+ # Install systemd service
2209
+ cp nexus-daemon.service "$SERVICE_FILE"
2210
+ systemctl daemon-reload
2211
+ systemctl enable nexus-daemon
2212
+ systemctl start nexus-daemon
2213
+
2214
+ echo "nexus-daemon installed and started."
2215
+ echo "Check status: systemctl status nexus-daemon"
2216
+ ```
2217
+
2218
+ **Step 3: 验证文件内容**
2219
+
2220
+ ```bash
2221
+ cat nexus-daemon/nexus-daemon.service | grep -E "^(Description|ExecStart|Restart)"
2222
+ ```
2223
+ Expected:
2224
+ ```
2225
+ Description=nexus Daemon - ultrapower self-improvement VPS service
2226
+ ExecStart=/opt/nexus-daemon/venv/bin/python daemon.py
2227
+ Restart=on-failure
2228
+ ```
2229
+
2230
+ **Step 4: Commit**
2231
+
2232
+ ```bash
2233
+ git add nexus-daemon/nexus-daemon.service nexus-daemon/install.sh
2234
+ git commit -m "feat(nexus): add systemd service file and install script"
2235
+ ```
2236
+
2237
+ ---
2238
+
2239
+ ### Task 14: Self-Modifier(代码自动修改模块)
2240
+
2241
+ **Files:**
2242
+ - Create: `nexus-daemon/self_modifier.py`
2243
+ - Modify: `nexus-daemon/daemon.py`(在 `run_once` 中集成 Self-Modifier)
2244
+ - Create: `nexus-daemon/tests/test_self_modifier.py`
2245
+
2246
+ **Step 1: 写失败测试**
2247
+
2248
+ ```python
2249
+ # nexus-daemon/tests/test_self_modifier.py
2250
+ import pytest
2251
+ import json
2252
+ from pathlib import Path
2253
+ from unittest.mock import patch, MagicMock
2254
+ import tempfile
2255
+ import shutil
2256
+
2257
+ from self_modifier import SelfModifier, ModificationResult
2258
+
2259
+
2260
+ @pytest.fixture
2261
+ def repo(tmp_path):
2262
+ """Create a minimal fake ultrapower repo."""
2263
+ skills_dir = tmp_path / 'skills' / 'learner'
2264
+ skills_dir.mkdir(parents=True)
2265
+ skill_file = skills_dir / 'SKILL.md'
2266
+ skill_file.write_text(
2267
+ '---\nname: learner\ntriggers:\n - learn\n---\n# Learner\n'
2268
+ )
2269
+ # Init git
2270
+ import subprocess
2271
+ subprocess.run(['git', 'init'], cwd=tmp_path, capture_output=True)
2272
+ subprocess.run(['git', 'add', '.'], cwd=tmp_path, capture_output=True)
2273
+ subprocess.run(
2274
+ ['git', 'commit', '-m', 'init'],
2275
+ cwd=tmp_path, capture_output=True,
2276
+ env={**__import__('os').environ, 'GIT_AUTHOR_NAME': 'test',
2277
+ 'GIT_AUTHOR_EMAIL': 'test@test.com',
2278
+ 'GIT_COMMITTER_NAME': 'test',
2279
+ 'GIT_COMMITTER_EMAIL': 'test@test.com'},
2280
+ )
2281
+ return tmp_path
2282
+
2283
+
2284
+ def test_apply_skill_update_low_confidence_skipped(repo):
2285
+ """Improvements with confidence < 70 are skipped."""
2286
+ modifier = SelfModifier(repo_path=repo)
2287
+ improvement = {
2288
+ 'id': 'imp-001',
2289
+ 'type': 'skill_update',
2290
+ 'targetFile': 'skills/learner/SKILL.md',
2291
+ 'confidence': 60,
2292
+ 'diff': '--- a/skills/learner/SKILL.md\n+++ b/skills/learner/SKILL.md\n'
2293
+ '@@ -3,1 +3,2 @@\n - learn\n+ - study\n',
2294
+ 'reason': 'add synonym',
2295
+ }
2296
+ result = modifier.apply(improvement)
2297
+ assert result.status == 'skipped'
2298
+ assert result.reason == 'confidence below threshold'
2299
+
2300
+
2301
+ def test_apply_skill_update_high_confidence_applies(repo):
2302
+ """Improvements with confidence >= 70 on skill files are applied."""
2303
+ modifier = SelfModifier(repo_path=repo)
2304
+ new_content = (
2305
+ '---\nname: learner\ntriggers:\n - learn\n - study\n---\n# Learner\n'
2306
+ )
2307
+ improvement = {
2308
+ 'id': 'imp-002',
2309
+ 'type': 'skill_update',
2310
+ 'targetFile': 'skills/learner/SKILL.md',
2311
+ 'confidence': 75,
2312
+ 'newContent': new_content,
2313
+ 'reason': 'add synonym study',
2314
+ }
2315
+ result = modifier.apply(improvement)
2316
+ assert result.status == 'applied'
2317
+ applied_content = (repo / 'skills' / 'learner' / 'SKILL.md').read_text()
2318
+ assert 'study' in applied_content
2319
+
2320
+
2321
+ def test_apply_rejects_path_traversal(repo):
2322
+ """Path traversal in targetFile is rejected."""
2323
+ modifier = SelfModifier(repo_path=repo)
2324
+ improvement = {
2325
+ 'id': 'imp-003',
2326
+ 'type': 'skill_update',
2327
+ 'targetFile': '../../etc/passwd',
2328
+ 'confidence': 90,
2329
+ 'newContent': 'malicious',
2330
+ 'reason': 'test',
2331
+ }
2332
+ result = modifier.apply(improvement)
2333
+ assert result.status == 'rejected'
2334
+ assert 'path traversal' in result.reason.lower()
2335
+ ```
2336
+
2337
+ **Step 2: 运行测试确认失败**
2338
+
2339
+ ```bash
2340
+ cd nexus-daemon && python -m pytest tests/test_self_modifier.py -v
2341
+ ```
2342
+ Expected: FAIL — `ModuleNotFoundError: No module named 'self_modifier'`
2343
+
2344
+ **Step 3: 实现 `nexus-daemon/self_modifier.py`**
2345
+
2346
+ ```python
2347
+ # nexus-daemon/self_modifier.py
2348
+ """Self-Modifier: applies improvement suggestions to the ultrapower repo."""
2349
+ from __future__ import annotations
2350
+
2351
+ import logging
2352
+ from dataclasses import dataclass
2353
+ from pathlib import Path
2354
+ from typing import Any
2355
+
2356
+ logger = logging.getLogger(__name__)
2357
+
2358
+ # Only these path prefixes are allowed for auto-modification
2359
+ ALLOWED_PREFIXES = ('skills/', 'agents/')
2360
+ # Confidence threshold: below this, skip without applying
2361
+ CONFIDENCE_THRESHOLD = 70
2362
+
2363
+
2364
+ @dataclass
2365
+ class ModificationResult:
2366
+ status: str # 'applied' | 'skipped' | 'rejected' | 'error'
2367
+ reason: str
2368
+ improvement_id: str
2369
+
2370
+
2371
+ class SelfModifier:
2372
+ """Applies improvement suggestions to skill/agent Markdown files."""
2373
+
2374
+ def __init__(self, repo_path: Path) -> None:
2375
+ self.repo_path = Path(repo_path)
2376
+
2377
+ def _validate_target(self, target_file: str) -> str | None:
2378
+ """Return error message if target_file is invalid, else None."""
2379
+ # Reject path traversal
2380
+ try:
2381
+ resolved = (self.repo_path / target_file).resolve()
2382
+ resolved.relative_to(self.repo_path.resolve())
2383
+ except ValueError:
2384
+ return 'path traversal detected'
2385
+
2386
+ # Only allow skills/ and agents/ directories
2387
+ if not any(target_file.startswith(p) for p in ALLOWED_PREFIXES):
2388
+ return f'target outside allowed prefixes {ALLOWED_PREFIXES}'
2389
+
2390
+ # Only allow Markdown files
2391
+ if not target_file.endswith('.md'):
2392
+ return 'only .md files are allowed for auto-modification'
2393
+
2394
+ return None
2395
+
2396
+ def apply(self, improvement: dict[str, Any]) -> ModificationResult:
2397
+ """Apply a single improvement. Returns ModificationResult."""
2398
+ imp_id = improvement.get('id', 'unknown')
2399
+ confidence = improvement.get('confidence', 0)
2400
+ target_file = improvement.get('targetFile', '')
2401
+ new_content = improvement.get('newContent')
2402
+
2403
+ # Confidence gate
2404
+ if confidence < CONFIDENCE_THRESHOLD:
2405
+ return ModificationResult(
2406
+ status='skipped',
2407
+ reason='confidence below threshold',
2408
+ improvement_id=imp_id,
2409
+ )
2410
+
2411
+ # Path validation
2412
+ error = self._validate_target(target_file)
2413
+ if error:
2414
+ return ModificationResult(
2415
+ status='rejected',
2416
+ reason=error,
2417
+ improvement_id=imp_id,
2418
+ )
2419
+
2420
+ # Apply: write new content
2421
+ target_path = self.repo_path / target_file
2422
+ if new_content is None:
2423
+ return ModificationResult(
2424
+ status='error',
2425
+ reason='newContent field missing',
2426
+ improvement_id=imp_id,
2427
+ )
2428
+
2429
+ try:
2430
+ target_path.parent.mkdir(parents=True, exist_ok=True)
2431
+ target_path.write_text(new_content, encoding='utf-8')
2432
+ logger.info('Applied improvement %s to %s', imp_id, target_file)
2433
+ return ModificationResult(
2434
+ status='applied',
2435
+ reason=f'wrote {len(new_content)} bytes to {target_file}',
2436
+ improvement_id=imp_id,
2437
+ )
2438
+ except OSError as e:
2439
+ return ModificationResult(
2440
+ status='error',
2441
+ reason=str(e),
2442
+ improvement_id=imp_id,
2443
+ )
2444
+ ```
2445
+
2446
+ **Step 4: 运行测试确认通过**
2447
+
2448
+ ```bash
2449
+ cd nexus-daemon && python -m pytest tests/test_self_modifier.py -v
2450
+ ```
2451
+ Expected: PASS (3 tests)
2452
+
2453
+ **Step 5: 集成到 `daemon.py` 的 `run_once`**
2454
+
2455
+ 在 `NexusDaemon.__init__` 中添加:
2456
+
2457
+ ```python
2458
+ from self_modifier import SelfModifier
2459
+
2460
+ # 在 __init__ 末尾:
2461
+ self._modifier = SelfModifier(repo_path=self.repo_path)
2462
+ ```
2463
+
2464
+ 在 `run_once` 的改进通知循环中,在 `notify_improvement` 调用之后添加自动应用逻辑:
2465
+
2466
+ ```python
2467
+ # Auto-apply high-confidence improvements (confidence >= 80)
2468
+ if imp.get('confidence', 0) >= 80 and imp.get('status') == 'pending':
2469
+ result = self._modifier.apply(imp)
2470
+ if result.status == 'applied':
2471
+ imp['status'] = 'auto_applied'
2472
+ f.write_text(
2473
+ __import__('json').dumps(imp, indent=2, ensure_ascii=False),
2474
+ encoding='utf-8',
2475
+ )
2476
+ logger.info('Auto-applied improvement %s', imp_id)
2477
+ ```
2478
+
2479
+ **Step 6: 运行全量测试**
2480
+
2481
+ ```bash
2482
+ cd nexus-daemon && python -m pytest tests/ -v
2483
+ ```
2484
+ Expected: PASS (all tests)
2485
+
2486
+ **Step 7: Commit**
2487
+
2488
+ ```bash
2489
+ git add nexus-daemon/self_modifier.py nexus-daemon/tests/test_self_modifier.py nexus-daemon/daemon.py
2490
+ git commit -m "feat(nexus): add Self-Modifier module for auto-applying skill improvements"
2491
+ ```
2492
+
2493
+ ---
2494
+
2495
+ ## 实现完成检查清单
2496
+
2497
+ ### P0 核心功能(Tasks 1-5)
2498
+ - [ ] Task 1: nexus 配置类型和加载器
2499
+ - [ ] Task 2: data-collector hook(收集会话数据)
2500
+ - [ ] Task 3: consciousness-sync hook(SessionEnd 后 git push)
2501
+ - [ ] Task 4: session-end hook 集成
2502
+ - [ ] Task 5: processSessionEnd 注册 nexus hook
2503
+
2504
+ ### P1 重要功能(Tasks 6-9, 14)
2505
+ - [ ] Task 6: nexus-daemon Python 基础框架
2506
+ - [ ] Task 7: Evolution Engine MVP(模式检测 + knowledge_base)
2507
+ - [ ] Task 8: improvement-puller hook(拉取改进)
2508
+ - [ ] Task 9: Telegram Bot 通知
2509
+ - [ ] Task 14: Self-Modifier(代码自动修改模块)
2510
+
2511
+ ### P2 增强功能(Tasks 10-13)
2512
+ - [ ] Task 10: Consciousness Loop(后台意识循环)
2513
+ - [ ] Task 11: Self-Evaluator(健康报告)
2514
+ - [ ] Task 12: nexus 管理 Skills(nexus-status / nexus-evolve / nexus-review)
2515
+ - [ ] Task 13: systemd 服务文件(VPS 部署)
2516
+
2517
+ ### 验收标准(来自设计文档)
2518
+ - [ ] 会话结束后,数据自动推送到 nexus-daemon 仓库
2519
+ - [ ] VPS 守护进程每分钟拉取新事件并处理
2520
+ - [ ] 后台意识循环每 5 分钟运行一次,写入 scratchpad.md
2521
+ - [ ] 同类模式出现 ≥ 3 次后,Evolution Engine 生成改进建议
2522
+ - [ ] 置信度 ≥ 80 的改进自动通过测试后合并
2523
+ - [ ] 置信度 < 80 的改进通过 Telegram 发送确认请求
2524
+ - [ ] 所有代码修改必须通过 `tsc --noEmit && npm test`
2525
+ - [ ] 不破坏 ultrapower 现有任何功能
2526
+
2527
+ ### 最终验证命令
2528
+
2529
+ ```bash
2530
+ # TypeScript 层
2531
+ npm run build && npm test
2532
+
2533
+ # Python 层
2534
+ cd nexus-daemon && python -m pytest -v
2535
+
2536
+ # 检查所有 skill 文件存在
2537
+ ls skills/nexus/*/SKILL.md
2538
+ ```