principles-disciple 1.41.0 → 1.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.planning/codebase/ARCHITECTURE.md +157 -0
- package/.planning/codebase/CONCERNS.md +145 -0
- package/.planning/codebase/CONVENTIONS.md +148 -0
- package/.planning/codebase/INTEGRATIONS.md +81 -0
- package/.planning/codebase/STACK.md +87 -0
- package/.planning/codebase/STRUCTURE.md +193 -0
- package/.planning/codebase/TESTING.md +243 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/commands/pain.ts +12 -5
- package/src/commands/promote-impl.ts +13 -7
- package/src/commands/rollback.ts +10 -3
- package/src/core/event-log.ts +8 -6
- package/src/core/evolution-types.ts +33 -1
- package/src/hooks/message-sanitize.ts +18 -5
- package/src/hooks/prompt.ts +15 -4
- package/src/hooks/subagent.ts +2 -3
- package/src/http/principles-console-route.ts +21 -4
- package/src/service/evolution-worker.ts +89 -365
- package/src/service/queue-io.ts +375 -0
- package/src/service/queue-migration.ts +122 -0
- package/src/service/sleep-cycle.ts +157 -0
- package/src/service/subagent-workflow/runtime-direct-driver.ts +1 -1
- package/src/service/workflow-watchdog.ts +168 -0
- package/src/tools/deep-reflect.ts +22 -11
- package/src/types/event-payload.ts +80 -0
- package/src/types/queue.ts +70 -0
- package/src/utils/file-lock.ts +2 -2
- package/src/utils/io.ts +11 -3
- package/tests/core/evolution-migration.test.ts +325 -1
- package/tests/core/queue-purge.test.ts +337 -0
- package/tests/fixtures/legacy-queue-v1.json +74 -0
- package/tests/queue/async-lock.test.ts +200 -0
- package/tests/service/evolution-worker.queue.test.ts +296 -0
- package/tests/service/queue-io.test.ts +229 -0
- package/tests/service/queue-migration.test.ts +147 -0
- package/tests/service/workflow-watchdog.test.ts +372 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Codebase Structure
|
|
2
|
+
|
|
3
|
+
**Analysis Date:** 2026-04-15
|
|
4
|
+
|
|
5
|
+
## Directory Layout
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
openclaw-plugin/
|
|
9
|
+
├── src/ # Main source code
|
|
10
|
+
│ ├── commands/ # Slash command implementations
|
|
11
|
+
│ ├── config/ # Configuration defaults and errors
|
|
12
|
+
│ ├── constants/ # Shared constants
|
|
13
|
+
│ ├── core/ # Core business logic
|
|
14
|
+
│ │ ├── hygiene/ # Hygiene tracking
|
|
15
|
+
│ │ ├── principle-internalization/ # Principle lifecycle
|
|
16
|
+
│ │ └── schema/ # Database schema and migrations
|
|
17
|
+
│ ├── hooks/ # OpenClaw hook handlers
|
|
18
|
+
│ ├── http/ # HTTP route handlers
|
|
19
|
+
│ ├── i18n/ # Internationalization
|
|
20
|
+
│ ├── service/ # Background services
|
|
21
|
+
│ │ └── subagent-workflow/ # Workflow managers
|
|
22
|
+
│ ├── tools/ # Plugin tools
|
|
23
|
+
│ ├── types/ # TypeScript type definitions
|
|
24
|
+
│ └── utils/ # Utility functions
|
|
25
|
+
├── ui/ # React UI
|
|
26
|
+
│ └── src/
|
|
27
|
+
│ ├── components/ # React components
|
|
28
|
+
│ ├── context/ # React contexts
|
|
29
|
+
│ ├── hooks/ # React hooks
|
|
30
|
+
│ └── pages/ # Page components
|
|
31
|
+
├── tests/ # Test suite
|
|
32
|
+
│ ├── commands/ # Command tests
|
|
33
|
+
│ ├── core/ # Core module tests
|
|
34
|
+
│ ├── fixtures/ # Test fixtures
|
|
35
|
+
│ ├── hooks/ # Hook tests
|
|
36
|
+
│ ├── integration/ # Integration tests
|
|
37
|
+
│ ├── service/ # Service tests
|
|
38
|
+
│ └── utils/ # Utility tests
|
|
39
|
+
├── templates/ # Workspace templates
|
|
40
|
+
│ ├── langs/ # Language-specific templates
|
|
41
|
+
│ └── workspace/ # Workspace structure templates
|
|
42
|
+
├── dist/ # Build output
|
|
43
|
+
├── scripts/ # Build scripts
|
|
44
|
+
├── .state/ # Runtime state (gitignored)
|
|
45
|
+
└── .tmp/ # Temporary files (gitignored)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Directory Purposes
|
|
49
|
+
|
|
50
|
+
**src/commands/:**
|
|
51
|
+
- Purpose: Slash command implementations
|
|
52
|
+
- Contains: 20+ command handlers (strategy, focus, pain, rollback, nocturnal-review, nocturnal-train, etc.)
|
|
53
|
+
- Key files: `strategy.ts`, `focus.ts`, `nocturnal-train.ts`, `nocturnal-rollout.ts`
|
|
54
|
+
|
|
55
|
+
**src/core/:**
|
|
56
|
+
- Purpose: Core business logic (evolution, trajectory, pain, training, rules)
|
|
57
|
+
- Contains: 70+ core modules including `evolution-engine.ts`, `trajectory.ts`, `nocturnal-trinity.ts`, `pain.ts`, `principle-tree-ledger.ts`
|
|
58
|
+
- Key files: `evolution-engine.ts`, `nocturnal-trinity.ts`, `rule-host.ts`
|
|
59
|
+
|
|
60
|
+
**src/hooks/:**
|
|
61
|
+
- Purpose: OpenClaw hook handlers for intercepting agent behavior
|
|
62
|
+
- Contains: `prompt.ts`, `gate.ts`, `pain.ts`, `llm.ts`, `lifecycle.ts`, `subagent.ts`, `trajectory-collector.ts`
|
|
63
|
+
- Key files: `gate.ts` (security), `prompt.ts` (context injection)
|
|
64
|
+
|
|
65
|
+
**src/service/:**
|
|
66
|
+
- Purpose: Background worker services
|
|
67
|
+
- Contains: `evolution-worker.ts` (main worker), `nocturnal-service.ts`, `trajectory-service.ts`, `central-database.ts`
|
|
68
|
+
- Key files: `evolution-worker.ts` (144KB, main async processor)
|
|
69
|
+
|
|
70
|
+
**src/service/subagent-workflow/:**
|
|
71
|
+
- Purpose: Workflow orchestration for complex subagent operations
|
|
72
|
+
- Contains: `nocturnal-workflow-manager.ts`, `deep-reflect-workflow-manager.ts`, `empathy-observer-workflow-manager.ts`
|
|
73
|
+
- Key files: `workflow-manager-base.ts`, `dynamic-timeout.ts`
|
|
74
|
+
|
|
75
|
+
**src/utils/:**
|
|
76
|
+
- Purpose: Shared utility functions
|
|
77
|
+
- Contains: `io.ts` (atomic writes), `plugin-logger.ts`, `retry.ts`, `hashing.ts`, `file-lock.ts`
|
|
78
|
+
- Key files: `io.ts` (critical for safe file operations)
|
|
79
|
+
|
|
80
|
+
**src/schema/:**
|
|
81
|
+
- Purpose: SQLite database schema and migrations
|
|
82
|
+
- Contains: `schema-definitions.ts`, `migration-runner.ts`, `migrations/*.ts`
|
|
83
|
+
- Migrations: 4 migrations (001-004)
|
|
84
|
+
|
|
85
|
+
**ui/src/:**
|
|
86
|
+
- Purpose: React-based plugin UI
|
|
87
|
+
- Contains: Pages (Overview, Evolution, Feedback, GateMonitor), components (Shell, ProtectedRoute)
|
|
88
|
+
- Key files: `App.tsx`, `pages/EvolutionPage.tsx`, `pages/FeedbackPage.tsx`
|
|
89
|
+
|
|
90
|
+
## Key File Locations
|
|
91
|
+
|
|
92
|
+
**Entry Points:**
|
|
93
|
+
- `src/index.ts`: Plugin entry point, registers all hooks/commands/tools
|
|
94
|
+
|
|
95
|
+
**Configuration:**
|
|
96
|
+
- `src/core/config.ts`: PainSettings defaults
|
|
97
|
+
- `src/core/paths.ts`: Directory and file path constants (PD_DIRS, PD_FILES)
|
|
98
|
+
- `openclaw.plugin.json`: Plugin manifest
|
|
99
|
+
|
|
100
|
+
**Core Logic:**
|
|
101
|
+
- `src/core/evolution-engine.ts`: Evolution processing (18KB)
|
|
102
|
+
- `src/core/nocturnal-trinity.ts`: Nocturnal training orchestration (87KB - largest file)
|
|
103
|
+
- `src/core/trajectory.ts`: Trajectory tracking (64KB)
|
|
104
|
+
- `src/core/principle-tree-ledger.ts`: Principle lifecycle management (22KB)
|
|
105
|
+
- `src/core/rule-host.ts`: Sandboxed rule execution (7KB)
|
|
106
|
+
|
|
107
|
+
**Service Layer:**
|
|
108
|
+
- `src/service/evolution-worker.ts`: Background evolution worker (144KB - largest file)
|
|
109
|
+
- `src/service/nocturnal-service.ts`: Nocturnal training service (59KB)
|
|
110
|
+
- `src/service/nocturnal-runtime.ts`: Runtime for nocturnal operations (24KB)
|
|
111
|
+
|
|
112
|
+
**Testing:**
|
|
113
|
+
- `tests/`: Test suite with unit and integration layers
|
|
114
|
+
- `vitest.config.ts`: Test configuration with unit/integration project separation
|
|
115
|
+
|
|
116
|
+
## Naming Conventions
|
|
117
|
+
|
|
118
|
+
**Files:**
|
|
119
|
+
- TypeScript: `kebab-case.ts` or `camelCase.ts` depending on module type
|
|
120
|
+
- Commands: `kebab-case.ts` (e.g., `nocturnal-review.ts`)
|
|
121
|
+
- Core modules: `camelCase.ts` (e.g., `evolutionEngine.ts`)
|
|
122
|
+
- React components: `PascalCase.tsx`
|
|
123
|
+
|
|
124
|
+
**Directories:**
|
|
125
|
+
- kebab-case: `subagent-workflow`, `principle-internalization`
|
|
126
|
+
|
|
127
|
+
**Types:**
|
|
128
|
+
- Interfaces: `PascalCase` (e.g., `PainSettings`, `EvolutionContext`)
|
|
129
|
+
- Type aliases: `PascalCase`
|
|
130
|
+
- Enums: `PascalCase`
|
|
131
|
+
|
|
132
|
+
## Where to Add New Code
|
|
133
|
+
|
|
134
|
+
**New Command:**
|
|
135
|
+
- Primary code: `src/commands/<name>.ts`
|
|
136
|
+
- Handler registration: `src/index.ts` in `registerCommandWithAlias()` or `api.registerCommand()`
|
|
137
|
+
- Tests: `tests/commands/<name>.test.ts`
|
|
138
|
+
|
|
139
|
+
**New Hook Handler:**
|
|
140
|
+
- Implementation: `src/hooks/<name>.ts`
|
|
141
|
+
- Registration: `src/index.ts` in `api.on('<hook_name>', ...)` call
|
|
142
|
+
- Tests: `tests/hooks/<name>.test.ts`
|
|
143
|
+
|
|
144
|
+
**New Core Service:**
|
|
145
|
+
- Implementation: `src/core/<name>.ts` or `src/service/<name>.ts`
|
|
146
|
+
- Registration: `src/index.ts` in `api.registerService()`
|
|
147
|
+
- Tests: `tests/core/<name>.test.ts` or `tests/service/<name>.test.ts`
|
|
148
|
+
|
|
149
|
+
**New Workflow Manager:**
|
|
150
|
+
- Implementation: `src/service/subagent-workflow/<name>-workflow-manager.ts`
|
|
151
|
+
- Base class: `src/service/subagent-workflow/workflow-manager-base.ts`
|
|
152
|
+
- Tests: `tests/service/subagent-workflow/<name>.test.ts`
|
|
153
|
+
|
|
154
|
+
**New Database Migration:**
|
|
155
|
+
- Implementation: `src/core/schema/migrations/<number>-<description>.ts`
|
|
156
|
+
- Registration: `src/core/schema/migrations/index.ts`
|
|
157
|
+
- Tests: Integration test in `tests/core/control-ui-db.test.ts`
|
|
158
|
+
|
|
159
|
+
**New Utility:**
|
|
160
|
+
- Shared: `src/utils/<name>.ts`
|
|
161
|
+
- Tests: `tests/utils/<name>.test.ts`
|
|
162
|
+
|
|
163
|
+
## Special Directories
|
|
164
|
+
|
|
165
|
+
**.state/:**
|
|
166
|
+
- Purpose: Runtime state (per workspace)
|
|
167
|
+
- Generated: Yes (created at runtime)
|
|
168
|
+
- Committed: No (gitignored)
|
|
169
|
+
|
|
170
|
+
**.tmp/:**
|
|
171
|
+
- Purpose: Temporary files during build/dev
|
|
172
|
+
- Generated: Yes
|
|
173
|
+
- Committed: No (gitignored)
|
|
174
|
+
|
|
175
|
+
**dist/:**
|
|
176
|
+
- Purpose: Build output
|
|
177
|
+
- Generated: Yes (by `npm run build`)
|
|
178
|
+
- Committed: Yes (in some branches)
|
|
179
|
+
|
|
180
|
+
**templates/:**
|
|
181
|
+
- Purpose: Workspace template files copied on init
|
|
182
|
+
- Generated: No
|
|
183
|
+
- Committed: Yes
|
|
184
|
+
- Contains: Language-specific templates (en, zh), workspace structure
|
|
185
|
+
|
|
186
|
+
**node_modules/:**
|
|
187
|
+
- Purpose: Dependencies
|
|
188
|
+
- Generated: Yes (by pnpm install)
|
|
189
|
+
- Committed: No
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
*Structure analysis: 2026-04-15*
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Testing Patterns
|
|
2
|
+
|
|
3
|
+
**Analysis Date:** 2026-04-15
|
|
4
|
+
|
|
5
|
+
## Test Framework
|
|
6
|
+
|
|
7
|
+
**Runner:** Vitest 4.1.0
|
|
8
|
+
- Config: `vitest.config.ts`
|
|
9
|
+
- Environment: `node`
|
|
10
|
+
- Pool: `threads` (required for `better-sqlite3` native handle cleanup)
|
|
11
|
+
|
|
12
|
+
**Assertion Library:** Vitest built-in (`expect`)
|
|
13
|
+
|
|
14
|
+
**Coverage:** `@vitest/coverage-v8`
|
|
15
|
+
- Thresholds: lines 70%, functions 70%, branches 60%, statements 70%
|
|
16
|
+
- Excludes: `tests/**`
|
|
17
|
+
|
|
18
|
+
**Run Commands:**
|
|
19
|
+
```bash
|
|
20
|
+
npm test # Run unit tests (fast, parallel)
|
|
21
|
+
npm run test:unit # Alias for above
|
|
22
|
+
npm run test:integration # Run integration tests only
|
|
23
|
+
npm run test:coverage # Run with coverage report
|
|
24
|
+
npm run test:all # Run all tests (unit + integration)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Test File Organization
|
|
28
|
+
|
|
29
|
+
**Location:**
|
|
30
|
+
- Tests co-located in `tests/` directory, mirroring `src/` structure
|
|
31
|
+
- `tests/core/`, `tests/commands/`, `tests/hooks/`, `tests/service/`, `tests/utils/`
|
|
32
|
+
- Integration tests in `tests/integration/`
|
|
33
|
+
|
|
34
|
+
**Naming:**
|
|
35
|
+
- `*.test.ts` suffix for all test files
|
|
36
|
+
- Example: `tests/core/detection-service.test.ts`
|
|
37
|
+
|
|
38
|
+
**Structure:**
|
|
39
|
+
```
|
|
40
|
+
tests/
|
|
41
|
+
├── commands/ # Command handlers
|
|
42
|
+
├── core/ # Core services and logic
|
|
43
|
+
├── fixtures/ # Shared fixtures
|
|
44
|
+
├── hooks/ # Hook handlers
|
|
45
|
+
├── integration/ # End-to-end tests
|
|
46
|
+
├── scripts/ # Script tests
|
|
47
|
+
└── service/ # Service layer
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Test Structure
|
|
51
|
+
|
|
52
|
+
**Suite Organization:**
|
|
53
|
+
```typescript
|
|
54
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
55
|
+
|
|
56
|
+
describe('DetectionService', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
vi.clearAllMocks();
|
|
59
|
+
// Reset state between tests
|
|
60
|
+
DetectionService.reset();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should create a new instance on first get', () => {
|
|
64
|
+
// Test implementation
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Patterns:**
|
|
70
|
+
- `beforeEach` for setup, `afterEach` for teardown
|
|
71
|
+
- `vi.clearAllMocks()` between tests (not `vi.resetAllMocks()`)
|
|
72
|
+
- `vi.useFakeTimers()` / `vi.useRealTimers()` for time-sensitive tests
|
|
73
|
+
|
|
74
|
+
## Mocking
|
|
75
|
+
|
|
76
|
+
**Framework:** Vitest's `vi.fn()` and `vi.mock()`
|
|
77
|
+
|
|
78
|
+
**Module Mocking:**
|
|
79
|
+
```typescript
|
|
80
|
+
vi.mock('../../src/core/dictionary-service.js');
|
|
81
|
+
vi.mock('../../src/core/detection-funnel.js');
|
|
82
|
+
|
|
83
|
+
// Then configure mock implementations
|
|
84
|
+
vi.mocked(DictionaryService.get).mockReturnValue(mockDict as any);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Built-in Module Mocking:**
|
|
88
|
+
```typescript
|
|
89
|
+
vi.mock('fs');
|
|
90
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
91
|
+
vi.mocked(fs.readFileSync).mockImplementation((p) => {
|
|
92
|
+
if (p.toString() === configPath) return JSON.stringify(mockConfig);
|
|
93
|
+
return '';
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Mock Reset:**
|
|
98
|
+
- `vi.clearAllMocks()` clears call history but keeps implementations
|
|
99
|
+
- `vi.resetAllMocks()` clears both (use with caution)
|
|
100
|
+
- Reset singleton state: `DetectionService.reset()`, `WorkspaceContext.clearCache()`
|
|
101
|
+
|
|
102
|
+
**Partial Mocks:**
|
|
103
|
+
```typescript
|
|
104
|
+
vi.mocked(fs.existsSync).mockImplementation((p) => p.toString() === configPath);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Fixtures and Factories
|
|
108
|
+
|
|
109
|
+
**Temp Directory Pattern (integration tests):**
|
|
110
|
+
```typescript
|
|
111
|
+
const tempDirs: string[] = [];
|
|
112
|
+
|
|
113
|
+
function makeWorkspace(): string {
|
|
114
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-runtime-summary-'));
|
|
115
|
+
tempDirs.push(dir);
|
|
116
|
+
fs.mkdirSync(path.join(dir, '.state', 'sessions'), { recursive: true });
|
|
117
|
+
return dir;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
for (const dir of tempDirs.splice(0)) {
|
|
122
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**JSON Fixture Writing:**
|
|
128
|
+
```typescript
|
|
129
|
+
function writeJson(filePath: string, value: unknown): void {
|
|
130
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
131
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf8');
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Session File Helper:**
|
|
136
|
+
```typescript
|
|
137
|
+
function writeSession(workspace: string, sessionId: string, payload: Record<string, unknown>): void {
|
|
138
|
+
writeJson(path.join(workspace, '.state', 'sessions', `${sessionId}.json`), {
|
|
139
|
+
sessionId,
|
|
140
|
+
...payload,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Integration Tests
|
|
146
|
+
|
|
147
|
+
**Requirements:**
|
|
148
|
+
- Real SQLite database via `better-sqlite3`
|
|
149
|
+
- Thread pool required (not `vm` pool) due to native handle cleanup issues
|
|
150
|
+
- Explicit file list in `vitest.config.ts` integration array
|
|
151
|
+
|
|
152
|
+
**Integration Test Files:**
|
|
153
|
+
```typescript
|
|
154
|
+
const integrationTests = [
|
|
155
|
+
'tests/core/control-ui-db.test.ts',
|
|
156
|
+
'tests/core/evolution-logger.test.ts',
|
|
157
|
+
'tests/core/nocturnal-e2e.test.ts',
|
|
158
|
+
// ...
|
|
159
|
+
];
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Common Patterns
|
|
163
|
+
|
|
164
|
+
**Async Testing with Fake Timers:**
|
|
165
|
+
```typescript
|
|
166
|
+
beforeEach(() => {
|
|
167
|
+
vi.useFakeTimers();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
afterEach(() => {
|
|
171
|
+
vi.useRealTimers();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should retry on retryable error', async () => {
|
|
175
|
+
const fn = vi.fn()
|
|
176
|
+
.mockRejectedValueOnce(new Error('ETIMEDOUT'))
|
|
177
|
+
.mockResolvedValue('success');
|
|
178
|
+
|
|
179
|
+
const resultPromise = retryAsync(fn, { initialDelayMs: 100 });
|
|
180
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
181
|
+
const result = await resultPromise;
|
|
182
|
+
expect(result).toBe('success');
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Error Testing:**
|
|
187
|
+
```typescript
|
|
188
|
+
it('should throw after max retries exceeded', async () => {
|
|
189
|
+
vi.useRealTimers();
|
|
190
|
+
const fn = vi.fn().mockRejectedValue(new Error('ETIMEDOUT'));
|
|
191
|
+
|
|
192
|
+
await expect(retryAsync(fn, { maxRetries: 1, initialDelayMs: 1, logger: { warn: vi.fn() } }))
|
|
193
|
+
.rejects.toThrow('ETIMEDOUT');
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Singleton Reset Pattern:**
|
|
198
|
+
```typescript
|
|
199
|
+
// Many services use singleton pattern requiring reset between tests
|
|
200
|
+
DetectionService.reset();
|
|
201
|
+
WorkspaceContext.clearCache();
|
|
202
|
+
clearSession('live-session');
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Test Naming and Documentation
|
|
206
|
+
|
|
207
|
+
**Descriptive Test Names:**
|
|
208
|
+
```typescript
|
|
209
|
+
it('should block risk path writes at Seed tier (EP system)', () => { ... });
|
|
210
|
+
it('should return default values if file does not exist', () => { ... });
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Test Comments for Context:**
|
|
214
|
+
```typescript
|
|
215
|
+
// Task 4: Default Values Consistency Tests
|
|
216
|
+
describe('Gate Default Values Consistency', () => {
|
|
217
|
+
/**
|
|
218
|
+
* PURPOSE: Prove that gate.ts inline defaults match PROFILE_DEFAULTS.
|
|
219
|
+
* If gate.ts has inline defaults that differ from normalizeProfile(),
|
|
220
|
+
* this is a bug - the defaults should come from a single source of truth.
|
|
221
|
+
*/
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Test Isolation
|
|
226
|
+
|
|
227
|
+
**Environment Variable Injection:**
|
|
228
|
+
```typescript
|
|
229
|
+
// Set env before module load
|
|
230
|
+
process.env.PD_TEST_AGENTS_DIR = TEST_AGENTS_DIR;
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Path Traversal Protection Tests:**
|
|
234
|
+
```typescript
|
|
235
|
+
it('rejects path traversal session IDs', async () => {
|
|
236
|
+
const result = await extractRecentConversation('../../etc/passwd', 'main');
|
|
237
|
+
expect(result).toBe('');
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
*Testing analysis: 2026-04-15*
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/commands/pain.ts
CHANGED
|
@@ -80,6 +80,14 @@ function formatEmpathyCard(stats: EmpathyEventStats, range: string, isZh: boolea
|
|
|
80
80
|
return lines.join('\n');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Extended context interface that includes sessionId injected by the plugin framework.
|
|
85
|
+
* PluginCommandContext does not include sessionId in its type definition.
|
|
86
|
+
*/
|
|
87
|
+
interface SessionAwareCommandContext extends PluginCommandContext {
|
|
88
|
+
sessionId: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
83
91
|
/**
|
|
84
92
|
* Handles the /pd-status command
|
|
85
93
|
*/
|
|
@@ -89,15 +97,14 @@ export function handlePainCommand(ctx: PluginCommandContext): PluginCommandResul
|
|
|
89
97
|
const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
|
|
90
98
|
const lang = (ctx.config?.language as string) || 'en';
|
|
91
99
|
const isZh = lang === 'zh';
|
|
92
|
-
|
|
93
|
-
const {sessionId} = (ctx as any);
|
|
100
|
+
const { sessionId } = ctx as SessionAwareCommandContext;
|
|
94
101
|
|
|
95
102
|
const args = (ctx.args || '').trim();
|
|
96
103
|
|
|
97
104
|
// Handle empathy subcommand
|
|
98
105
|
if (args.startsWith('empathy')) {
|
|
99
106
|
|
|
100
|
-
|
|
107
|
+
|
|
101
108
|
return handleEmpathySubcommand(wctx, args, sessionId, isZh);
|
|
102
109
|
}
|
|
103
110
|
|
|
@@ -138,7 +145,7 @@ export function handlePainCommand(ctx: PluginCommandContext): PluginCommandResul
|
|
|
138
145
|
|
|
139
146
|
// Determine health status based on GFI
|
|
140
147
|
|
|
141
|
-
|
|
148
|
+
|
|
142
149
|
let healthLabel: string;
|
|
143
150
|
let suggestionText = '';
|
|
144
151
|
|
|
@@ -218,7 +225,7 @@ export function handlePainCommand(ctx: PluginCommandContext): PluginCommandResul
|
|
|
218
225
|
* Handle /pd-status empathy subcommand
|
|
219
226
|
*/
|
|
220
227
|
|
|
221
|
-
|
|
228
|
+
|
|
222
229
|
function handleEmpathySubcommand(
|
|
223
230
|
wctx: WorkspaceContext,
|
|
224
231
|
args: string,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from '../core/principle-tree-ledger.js';
|
|
29
29
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
30
30
|
import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
|
|
31
|
-
import type { Implementation } from '../types/principle-tree-schema.js';
|
|
31
|
+
import type { Implementation, ImplementationLifecycleState } from '../types/principle-tree-schema.js';
|
|
32
32
|
import { withLock } from '../utils/file-lock.js';
|
|
33
33
|
import { atomicWriteFileSync } from '../utils/io.js';
|
|
34
34
|
|
|
@@ -37,16 +37,23 @@ function getAllImplementations(stateDir: string): Implementation[] {
|
|
|
37
37
|
return Object.values(ledger.tree.implementations);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Type predicate: true if impl has lifecycleState of 'candidate' or 'disabled'.
|
|
42
|
+
* The ledger adds lifecycleState at runtime beyond what's in the manifest interface.
|
|
43
|
+
*/
|
|
44
|
+
function isCandidateOrDisabled(
|
|
45
|
+
impl: Implementation
|
|
46
|
+
): impl is Implementation & { lifecycleState: ImplementationLifecycleState } {
|
|
47
|
+
return impl.lifecycleState === 'candidate' || impl.lifecycleState === 'disabled';
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
function _handleListCandidates(
|
|
41
51
|
stateDir: string,
|
|
42
52
|
isZh: boolean,
|
|
43
53
|
): PluginCommandResult {
|
|
44
54
|
const engine = new ReplayEngine('', stateDir);
|
|
45
55
|
const allImpls = getAllImplementations(stateDir);
|
|
46
|
-
const candidates = allImpls.filter(
|
|
47
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: lifecycleState is a dynamic property added by the system - type not in official interface
|
|
48
|
-
(impl) => (impl as any).lifecycleState === 'candidate',
|
|
49
|
-
);
|
|
56
|
+
const candidates = allImpls.filter(isCandidateOrDisabled);
|
|
50
57
|
|
|
51
58
|
if (candidates.length === 0) {
|
|
52
59
|
return {
|
|
@@ -141,8 +148,7 @@ function _handlePromoteImpl(options: PromoteImplOptions): PluginCommandResult {
|
|
|
141
148
|
};
|
|
142
149
|
}
|
|
143
150
|
|
|
144
|
-
|
|
145
|
-
const currentState = (candidate as any).lifecycleState || 'candidate';
|
|
151
|
+
const currentState = candidate.lifecycleState || 'candidate';
|
|
146
152
|
|
|
147
153
|
if (currentState !== 'candidate' && currentState !== 'disabled') {
|
|
148
154
|
return {
|
package/src/commands/rollback.ts
CHANGED
|
@@ -2,6 +2,14 @@ import { WorkspaceContext } from '../core/workspace-context.js';
|
|
|
2
2
|
import { resetFriction } from '../core/session-tracker.js';
|
|
3
3
|
import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Extended context interface that includes sessionId injected by the plugin framework.
|
|
7
|
+
* PluginCommandContext does not include sessionId in its type definition.
|
|
8
|
+
*/
|
|
9
|
+
interface SessionAwareCommandContext extends PluginCommandContext {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
/**
|
|
6
14
|
* Handles the /pd-rollback command
|
|
7
15
|
*
|
|
@@ -15,8 +23,7 @@ export function handleRollbackCommand(ctx: PluginCommandContext): PluginCommandR
|
|
|
15
23
|
const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
|
|
16
24
|
const lang = (ctx.config?.language as string) || 'en';
|
|
17
25
|
const isZh = lang === 'zh';
|
|
18
|
-
|
|
19
|
-
const {sessionId} = (ctx as any);
|
|
26
|
+
const { sessionId } = ctx as SessionAwareCommandContext;
|
|
20
27
|
|
|
21
28
|
const args = (ctx.args || '').trim();
|
|
22
29
|
|
|
@@ -45,7 +52,7 @@ Usage:
|
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
|
|
49
56
|
let eventId: string | null;
|
|
50
57
|
|
|
51
58
|
const _triggerMethod = 'user_command' as const;
|
package/src/core/event-log.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
import * as fs from 'fs';
|
|
2
3
|
import * as path from 'path';
|
|
3
4
|
import type {
|
|
@@ -94,7 +95,7 @@ export class EventLog {
|
|
|
94
95
|
/**
|
|
95
96
|
* Clean up event files older than EVENT_LOG_RETENTION_DAYS.
|
|
96
97
|
*/
|
|
97
|
-
private cleanupOldEventFiles(
|
|
98
|
+
private cleanupOldEventFiles(_today: string): void {
|
|
98
99
|
if (EVENT_LOG_RETENTION_DAYS <= 0) return;
|
|
99
100
|
|
|
100
101
|
try {
|
|
@@ -110,8 +111,8 @@ export class EventLog {
|
|
|
110
111
|
fs.unlinkSync(filePath);
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
|
-
} catch {
|
|
114
|
-
|
|
114
|
+
} catch (err) {
|
|
115
|
+
this.logger?.debug?.(`[PD] Event file cleanup failed (non-blocking): ${String(err)}`);
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
|
|
@@ -220,6 +221,7 @@ export class EventLog {
|
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
|
|
224
|
+
/* eslint-disable complexity */
|
|
223
225
|
private updateStats(entry: EventLogEntry): void {
|
|
224
226
|
let stats = this.statsCache.get(entry.date);
|
|
225
227
|
if (!stats) {
|
|
@@ -228,8 +230,6 @@ export class EventLog {
|
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
if (entry.type === 'tool_call') {
|
|
231
|
-
|
|
232
|
-
const _data = entry.data as unknown as ToolCallEventData;
|
|
233
233
|
stats.tools.total++;
|
|
234
234
|
if (entry.category === 'success') stats.tools.success++;
|
|
235
235
|
else stats.tools.failure++;
|
|
@@ -349,7 +349,8 @@ export class EventLog {
|
|
|
349
349
|
.map((line) => {
|
|
350
350
|
try {
|
|
351
351
|
return JSON.parse(line) as EventLogEntry;
|
|
352
|
-
} catch {
|
|
352
|
+
} catch (err) {
|
|
353
|
+
this.logger?.warn?.(`[PD] Corrupted event line skipped: ${String(err).slice(0, 100)}`);
|
|
353
354
|
return null;
|
|
354
355
|
}
|
|
355
356
|
})
|
|
@@ -499,6 +500,7 @@ export class EventLog {
|
|
|
499
500
|
/**
|
|
500
501
|
* Aggregate empathy stats for a specific session.
|
|
501
502
|
*/
|
|
503
|
+
/* eslint-disable complexity */
|
|
502
504
|
private aggregateSessionEmpathy(sessionId: string, result: EmpathyEventStats): void {
|
|
503
505
|
for (const entry of this.getMergedEvents()) {
|
|
504
506
|
if (entry.sessionId === sessionId && entry.type === 'pain_signal') {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Evolution Points System V2.0 - MVP
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Core Philosophy: Growth-driven替代Penalty-driven
|
|
5
5
|
* - 起点0分,只能增加,不扣分
|
|
6
6
|
* - 失败记录教训,不扣分
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* - 5级成长路径:Seed → Forest
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
// V2 queue types require TaskKind/TaskPriority from trajectory-types
|
|
12
|
+
import type { TaskKind, TaskPriority } from './trajectory-types.js';
|
|
13
|
+
|
|
11
14
|
// ===== 等级定义 =====
|
|
12
15
|
|
|
13
16
|
|
|
@@ -464,3 +467,32 @@ export type EvolutionLoopEvent =
|
|
|
464
467
|
| { ts: string; type: 'principle_rolled_back'; data: PrincipleRolledBackData }
|
|
465
468
|
| { ts: string; type: 'circuit_breaker_opened'; data: CircuitBreakerOpenedData }
|
|
466
469
|
| { ts: string; type: 'legacy_import'; data: LegacyImportData };
|
|
470
|
+
|
|
471
|
+
// V2 Queue Types (moved from evolution-worker.ts for shared use)
|
|
472
|
+
export type QueueStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'canceled';
|
|
473
|
+
export type TaskResolution = 'marker_detected' | 'auto_completed_timeout' | 'failed_max_retries' | 'runtime_unavailable' | 'canceled' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'stub_fallback' | 'skipped_thin_violation' | 'success' | 'failure' | 'skipped';
|
|
474
|
+
|
|
475
|
+
export interface EvolutionQueueItem {
|
|
476
|
+
id: string;
|
|
477
|
+
taskKind: TaskKind;
|
|
478
|
+
priority: TaskPriority;
|
|
479
|
+
source: string;
|
|
480
|
+
traceId?: string;
|
|
481
|
+
task?: string;
|
|
482
|
+
score: number;
|
|
483
|
+
reason: string;
|
|
484
|
+
timestamp: string;
|
|
485
|
+
enqueued_at?: string;
|
|
486
|
+
started_at?: string;
|
|
487
|
+
completed_at?: string;
|
|
488
|
+
assigned_session_key?: string;
|
|
489
|
+
trigger_text_preview?: string;
|
|
490
|
+
status: QueueStatus;
|
|
491
|
+
resolution?: TaskResolution;
|
|
492
|
+
session_id?: string;
|
|
493
|
+
agent_id?: string;
|
|
494
|
+
retryCount: number;
|
|
495
|
+
maxRetries: number;
|
|
496
|
+
lastError?: string;
|
|
497
|
+
resultRef?: string;
|
|
498
|
+
}
|