principles-disciple 1.12.0 → 1.13.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/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/scripts/sync-plugin.mjs +156 -1
- package/src/commands/nocturnal-train.ts +11 -12
- package/src/core/evolution-reducer.ts +31 -4
- package/src/core/nocturnal-trinity.ts +19 -4
- package/src/core/principle-tree-ledger.ts +27 -7
- package/src/core/principle-tree-migration.ts +195 -0
- package/src/core/thinking-os-parser.ts +36 -44
- package/src/index.ts +7 -3
- package/src/service/nocturnal-service.ts +11 -7
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +18 -3
- package/templates/langs/en/principles/THINKING_OS.md +13 -0
- package/templates/langs/zh/principles/THINKING_OS.md +13 -0
- package/ui/src/i18n/ui.ts +34 -9
- package/ui/src/pages/EvolutionPage.tsx +1 -1
- package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Principle Tree Migration — Migrates trainingStore to tree.principles
|
|
3
|
+
*
|
|
4
|
+
* This migration handles the Phase 11 gap: existing principles in trainingStore
|
|
5
|
+
* were never written to tree.principles, blocking the Rule/Implementation layer.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* - Called automatically by migratePrincipleTree() during plugin initialization
|
|
9
|
+
* - Or run manually: node scripts/migrate-principle-tree.mjs <workspace-dir>
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import {
|
|
15
|
+
loadLedger,
|
|
16
|
+
createPrinciple,
|
|
17
|
+
type LedgerPrinciple,
|
|
18
|
+
} from './principle-tree-ledger.js';
|
|
19
|
+
import type { LegacyPrincipleTrainingState } from './principle-tree-ledger.js';
|
|
20
|
+
import { SystemLogger } from './system-logger.js';
|
|
21
|
+
|
|
22
|
+
export interface PrincipleTreeMigrationResult {
|
|
23
|
+
migratedCount: number;
|
|
24
|
+
skippedCount: number;
|
|
25
|
+
errorCount: number;
|
|
26
|
+
details: Array<{
|
|
27
|
+
principleId: string;
|
|
28
|
+
status: 'migrated' | 'skipped' | 'error';
|
|
29
|
+
reason?: string;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if migration is needed by comparing trainingStore and tree.principles
|
|
35
|
+
*/
|
|
36
|
+
export function needsMigration(stateDir: string): boolean {
|
|
37
|
+
const ledger = loadLedger(stateDir);
|
|
38
|
+
const trainingStoreCount = Object.keys(ledger.trainingStore).length;
|
|
39
|
+
const treePrinciplesCount = Object.keys(ledger.tree.principles).length;
|
|
40
|
+
|
|
41
|
+
// Migration needed if trainingStore has more principles than tree.principles
|
|
42
|
+
return trainingStoreCount > treePrinciplesCount;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a minimal LedgerPrinciple from LegacyPrincipleTrainingState
|
|
47
|
+
*/
|
|
48
|
+
function trainingStateToTreePrinciple(
|
|
49
|
+
principleId: string,
|
|
50
|
+
state: LegacyPrincipleTrainingState,
|
|
51
|
+
now: string
|
|
52
|
+
): LedgerPrinciple {
|
|
53
|
+
return {
|
|
54
|
+
id: principleId,
|
|
55
|
+
version: 1,
|
|
56
|
+
text: `Principle ${principleId}`, // Minimal text, will be enriched from PRINCIPLES.md if available
|
|
57
|
+
triggerPattern: '', // Unknown from legacy data
|
|
58
|
+
action: '', // Unknown from legacy data
|
|
59
|
+
status: mapInternalizationStatusToPrincipleStatus(state.internalizationStatus),
|
|
60
|
+
priority: 'P1', // Default priority
|
|
61
|
+
scope: 'general',
|
|
62
|
+
evaluability: state.evaluability,
|
|
63
|
+
valueScore: 0,
|
|
64
|
+
adherenceRate: state.complianceRate * 100, // Convert 0-1 to 0-100
|
|
65
|
+
painPreventedCount: 0,
|
|
66
|
+
derivedFromPainIds: [],
|
|
67
|
+
ruleIds: [],
|
|
68
|
+
conflictsWithPrincipleIds: [],
|
|
69
|
+
createdAt: now,
|
|
70
|
+
updatedAt: now,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Map internalization status to principle status
|
|
76
|
+
*/
|
|
77
|
+
function mapInternalizationStatusToPrincipleStatus(
|
|
78
|
+
status: LegacyPrincipleTrainingState['internalizationStatus']
|
|
79
|
+
): 'candidate' | 'active' | 'deprecated' {
|
|
80
|
+
switch (status) {
|
|
81
|
+
case 'internalized':
|
|
82
|
+
case 'deployed_pending_eval':
|
|
83
|
+
return 'active';
|
|
84
|
+
case 'regressed':
|
|
85
|
+
case 'needs_training':
|
|
86
|
+
return 'candidate';
|
|
87
|
+
case 'prompt_only':
|
|
88
|
+
case 'in_training':
|
|
89
|
+
return 'candidate';
|
|
90
|
+
default:
|
|
91
|
+
return 'candidate';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Migrate trainingStore principles to tree.principles
|
|
97
|
+
*
|
|
98
|
+
* This function is idempotent: it only migrates principles that don't exist
|
|
99
|
+
* in tree.principles yet.
|
|
100
|
+
*/
|
|
101
|
+
export function migratePrincipleTree(
|
|
102
|
+
stateDir: string,
|
|
103
|
+
workspaceDir?: string
|
|
104
|
+
): PrincipleTreeMigrationResult {
|
|
105
|
+
const result: PrincipleTreeMigrationResult = {
|
|
106
|
+
migratedCount: 0,
|
|
107
|
+
skippedCount: 0,
|
|
108
|
+
errorCount: 0,
|
|
109
|
+
details: [],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const ledger = loadLedger(stateDir);
|
|
114
|
+
const now = new Date().toISOString();
|
|
115
|
+
|
|
116
|
+
for (const [principleId, state] of Object.entries(ledger.trainingStore)) {
|
|
117
|
+
// Skip if already exists in tree.principles
|
|
118
|
+
if (ledger.tree.principles[principleId]) {
|
|
119
|
+
result.skippedCount++;
|
|
120
|
+
result.details.push({
|
|
121
|
+
principleId,
|
|
122
|
+
status: 'skipped',
|
|
123
|
+
reason: 'Already exists in tree.principles',
|
|
124
|
+
});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const treePrinciple = trainingStateToTreePrinciple(principleId, state, now);
|
|
130
|
+
createPrinciple(stateDir, treePrinciple);
|
|
131
|
+
|
|
132
|
+
result.migratedCount++;
|
|
133
|
+
result.details.push({
|
|
134
|
+
principleId,
|
|
135
|
+
status: 'migrated',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (workspaceDir) {
|
|
139
|
+
SystemLogger.log(
|
|
140
|
+
workspaceDir,
|
|
141
|
+
'PRINCIPLE_TREE_MIGRATED',
|
|
142
|
+
`Migrated ${principleId} from trainingStore to tree.principles`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
result.errorCount++;
|
|
147
|
+
result.details.push({
|
|
148
|
+
principleId,
|
|
149
|
+
status: 'error',
|
|
150
|
+
reason: String(err),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (workspaceDir) {
|
|
154
|
+
SystemLogger.log(
|
|
155
|
+
workspaceDir,
|
|
156
|
+
'PRINCIPLE_TREE_MIGRATION_ERROR',
|
|
157
|
+
`Failed to migrate ${principleId}: ${String(err)}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (workspaceDir && result.migratedCount > 0) {
|
|
164
|
+
SystemLogger.log(
|
|
165
|
+
workspaceDir,
|
|
166
|
+
'PRINCIPLE_TREE_MIGRATION_COMPLETE',
|
|
167
|
+
`Migrated ${result.migratedCount} principles to tree.principles (${result.skippedCount} skipped, ${result.errorCount} errors)`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (workspaceDir) {
|
|
172
|
+
SystemLogger.log(
|
|
173
|
+
workspaceDir,
|
|
174
|
+
'PRINCIPLE_TREE_MIGRATION_FAILED',
|
|
175
|
+
`Migration failed: ${String(err)}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Run migration if needed (called during plugin initialization)
|
|
185
|
+
*/
|
|
186
|
+
export function runMigrationIfNeeded(
|
|
187
|
+
stateDir: string,
|
|
188
|
+
workspaceDir?: string
|
|
189
|
+
): PrincipleTreeMigrationResult | null {
|
|
190
|
+
if (!needsMigration(stateDir)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return migratePrincipleTree(stateDir, workspaceDir);
|
|
195
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Parses THINKING_OS.md to extract directive definitions.
|
|
5
5
|
* THINKING_OS.md is the single source of truth for thinking models.
|
|
6
6
|
*
|
|
7
|
-
* XML structure:
|
|
7
|
+
* Required XML structure:
|
|
8
8
|
* <directive id="T-01" name="MAP_BEFORE_TERRITORY">
|
|
9
9
|
* <trigger>...</trigger>
|
|
10
10
|
* <must>...</must>
|
|
@@ -37,47 +37,42 @@ function extractTag(content: string, tagName: string): string {
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Parse THINKING_OS.md content and extract all <directive> blocks.
|
|
40
|
-
* Returns empty array if no directives found.
|
|
40
|
+
* Returns empty array if no XML directives found.
|
|
41
41
|
*/
|
|
42
42
|
export function parseThinkingOsMd(content: string): ThinkingOsDirective[] {
|
|
43
43
|
const directives: ThinkingOsDirective[] = [];
|
|
44
|
-
|
|
45
|
-
// Match all <directive ...> ... </directive> blocks
|
|
46
44
|
const directiveRegex = /<directive\s+([^>]*)>([\s\S]*?)<\/directive>/gi;
|
|
47
|
-
|
|
45
|
+
|
|
48
46
|
let match: RegExpExecArray | null = null;
|
|
49
47
|
|
|
50
48
|
while ((match = directiveRegex.exec(content)) !== null) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const idMatch = /id="([^"]+)"/i
|
|
54
|
-
const nameMatch = /name="([^"]+)"/i
|
|
55
|
-
|
|
49
|
+
const attrs = match[1];
|
|
50
|
+
const body = match[2];
|
|
51
|
+
const idMatch = attrs.match(/id="([^"]+)"/i);
|
|
52
|
+
const nameMatch = attrs.match(/name="([^"]+)"/i);
|
|
56
53
|
if (!idMatch) continue;
|
|
57
54
|
|
|
58
|
-
|
|
55
|
+
directives.push({
|
|
59
56
|
id: idMatch[1],
|
|
60
57
|
name: nameMatch ? nameMatch[1] : '',
|
|
61
58
|
trigger: extractTag(body, 'trigger'),
|
|
62
59
|
must: extractTag(body, 'must'),
|
|
63
60
|
forbidden: extractTag(body, 'forbidden'),
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
directives.push(directive);
|
|
61
|
+
});
|
|
67
62
|
}
|
|
68
63
|
|
|
69
64
|
return directives;
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
/**
|
|
73
|
-
* Load THINKING_OS.md from the
|
|
74
|
-
* Falls back to
|
|
68
|
+
* Load THINKING_OS.md from the workspace.
|
|
69
|
+
* Falls back to plugin templates if workspace file doesn't exist or has no XML directives.
|
|
75
70
|
*/
|
|
76
71
|
export function loadThinkingOsFromWorkspace(
|
|
77
72
|
workspaceDir: string,
|
|
78
73
|
language = 'zh',
|
|
79
74
|
): ThinkingOsDirective[] {
|
|
80
|
-
// Priority 1: workspace
|
|
75
|
+
// Priority 1: workspace THINKING_OS.md
|
|
81
76
|
const workspacePath = resolvePdPath(workspaceDir, 'THINKING_OS');
|
|
82
77
|
if (fs.existsSync(workspacePath)) {
|
|
83
78
|
try {
|
|
@@ -89,39 +84,21 @@ export function loadThinkingOsFromWorkspace(
|
|
|
89
84
|
}
|
|
90
85
|
}
|
|
91
86
|
|
|
92
|
-
// ES Module compatible __dirname (must be inside function for bundler)
|
|
93
|
-
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
94
|
-
|
|
95
87
|
// Priority 2: plugin template for the given language
|
|
96
|
-
const templatePath =
|
|
97
|
-
|
|
98
|
-
'templates',
|
|
99
|
-
'langs',
|
|
100
|
-
language,
|
|
101
|
-
'principles',
|
|
102
|
-
'THINKING_OS.md',
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
if (fs.existsSync(templatePath)) {
|
|
88
|
+
const templatePath = resolveTemplatePath(language);
|
|
89
|
+
if (templatePath) {
|
|
106
90
|
try {
|
|
107
91
|
const content = fs.readFileSync(templatePath, 'utf-8');
|
|
108
|
-
|
|
92
|
+
const directives = parseThinkingOsMd(content);
|
|
93
|
+
if (directives.length > 0) return directives;
|
|
109
94
|
} catch {
|
|
110
95
|
// Fall through to zh template
|
|
111
96
|
}
|
|
112
97
|
}
|
|
113
98
|
|
|
114
99
|
// Priority 3: zh template as ultimate fallback
|
|
115
|
-
const zhPath =
|
|
116
|
-
|
|
117
|
-
'templates',
|
|
118
|
-
'langs',
|
|
119
|
-
'zh',
|
|
120
|
-
'principles',
|
|
121
|
-
'THINKING_OS.md',
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
if (fs.existsSync(zhPath)) {
|
|
100
|
+
const zhPath = resolveTemplatePath('zh');
|
|
101
|
+
if (zhPath) {
|
|
125
102
|
try {
|
|
126
103
|
const content = fs.readFileSync(zhPath, 'utf-8');
|
|
127
104
|
return parseThinkingOsMd(content);
|
|
@@ -133,6 +110,22 @@ export function loadThinkingOsFromWorkspace(
|
|
|
133
110
|
return [];
|
|
134
111
|
}
|
|
135
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the THINKING_OS.md template path for a given language.
|
|
115
|
+
*/
|
|
116
|
+
function resolveTemplatePath(language: string): string | null {
|
|
117
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
118
|
+
const templatePath = path.join(
|
|
119
|
+
path.dirname(path.dirname(path.dirname(currentDir))),
|
|
120
|
+
'templates',
|
|
121
|
+
'langs',
|
|
122
|
+
language,
|
|
123
|
+
'principles',
|
|
124
|
+
'THINKING_OS.md',
|
|
125
|
+
);
|
|
126
|
+
return fs.existsSync(templatePath) ? templatePath : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
136
129
|
/**
|
|
137
130
|
* Extract meaningful detection keywords from a trigger string.
|
|
138
131
|
* Returns an array of regex patterns.
|
|
@@ -142,14 +135,14 @@ export function generateDetectionPatterns(trigger: string): RegExp[] {
|
|
|
142
135
|
|
|
143
136
|
const patterns: string[] = [];
|
|
144
137
|
|
|
145
|
-
// Extract Chinese phrases: 3-8 character sequences
|
|
138
|
+
// Extract Chinese phrases: 3-8 character sequences
|
|
146
139
|
const chinesePattern = /[\u4e00-\u9fff]{3,8}/g;
|
|
147
140
|
const chineseMatches = trigger.match(chinesePattern) ?? [];
|
|
148
141
|
for (const phrase of chineseMatches) {
|
|
149
142
|
patterns.push(phrase);
|
|
150
143
|
}
|
|
151
144
|
|
|
152
|
-
// Extract English words/phrases
|
|
145
|
+
// Extract English words/phrases
|
|
153
146
|
const englishPattern = /[a-zA-Z]{3,20}(?:\s+[a-zA-Z]{3,20}){0,3}/g;
|
|
154
147
|
const englishMatches = trigger.match(englishPattern) ?? [];
|
|
155
148
|
for (const phrase of englishMatches) {
|
|
@@ -159,6 +152,5 @@ export function generateDetectionPatterns(trigger: string): RegExp[] {
|
|
|
159
152
|
}
|
|
160
153
|
}
|
|
161
154
|
|
|
162
|
-
// Convert to case-insensitive regexes
|
|
163
155
|
return patterns.map(p => new RegExp(p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'));
|
|
164
156
|
}
|
package/src/index.ts
CHANGED
|
@@ -54,6 +54,7 @@ import { PDTaskService } from './core/pd-task-service.js';
|
|
|
54
54
|
import { CentralSyncService } from './service/central-sync-service.js';
|
|
55
55
|
import { ensureWorkspaceTemplates } from './core/init.js';
|
|
56
56
|
import { migrateDirectoryStructure } from './core/migration.js';
|
|
57
|
+
import { runMigrationIfNeeded } from './core/principle-tree-migration.js';
|
|
57
58
|
import { SystemLogger } from './core/system-logger.js';
|
|
58
59
|
import { createDeepReflectTool } from './tools/deep-reflect.js';
|
|
59
60
|
import { PathResolver, resolveWorkspaceDirFromApi } from './core/path-resolver.js';
|
|
@@ -125,6 +126,9 @@ const plugin = {
|
|
|
125
126
|
const workspaceDir = ctx.workspaceDir || api.resolvePath('.');
|
|
126
127
|
if (!workspaceInitialized && workspaceDir) {
|
|
127
128
|
migrateDirectoryStructure(api, workspaceDir);
|
|
129
|
+
// Phase 11: Migrate trainingStore principles to tree.principles
|
|
130
|
+
const { stateDir } = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
131
|
+
runMigrationIfNeeded(stateDir, workspaceDir);
|
|
128
132
|
ensureWorkspaceTemplates(api, workspaceDir, language);
|
|
129
133
|
SystemLogger.log(workspaceDir, 'SYSTEM_BOOT', `Principles Disciple online. Language: ${language}`);
|
|
130
134
|
workspaceInitialized = true;
|
|
@@ -231,7 +235,7 @@ const plugin = {
|
|
|
231
235
|
try {
|
|
232
236
|
const workspaceDir = resolveToolHookWorkspaceDir(ctx, api, 'trajectory.after_tool_call');
|
|
233
237
|
TrajectoryCollector.handleAfterToolCall(event, { ...ctx, workspaceDir });
|
|
234
|
-
// eslint-disable-next-line
|
|
238
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Reason: catch binding intentionally unused
|
|
235
239
|
} catch (_err) {
|
|
236
240
|
// Non-critical: don't log, just skip
|
|
237
241
|
}
|
|
@@ -244,7 +248,7 @@ const plugin = {
|
|
|
244
248
|
try {
|
|
245
249
|
const workspaceDir = resolveToolHookWorkspaceDir(ctx as unknown as Record<string, unknown>, api, 'trajectory.llm_output');
|
|
246
250
|
TrajectoryCollector.handleLlmOutput(event, { ...ctx, workspaceDir });
|
|
247
|
-
// eslint-disable-next-line
|
|
251
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Reason: catch binding intentionally unused
|
|
248
252
|
} catch (_err) {
|
|
249
253
|
// Non-critical: don't log, just skip
|
|
250
254
|
}
|
|
@@ -254,7 +258,7 @@ const plugin = {
|
|
|
254
258
|
// ── Hook: Subagent Loop Closure ──
|
|
255
259
|
api.on(
|
|
256
260
|
'subagent_spawning',
|
|
257
|
-
|
|
261
|
+
|
|
258
262
|
(event: PluginHookSubagentSpawningEvent, _ctx: PluginHookSubagentContext): void | PluginHookSubagentSpawningResult => {
|
|
259
263
|
try {
|
|
260
264
|
// Resolve workspace via official API, falling back to PathResolver
|
|
@@ -1146,13 +1146,14 @@ async function executeNocturnalReflectionWithAdapter(
|
|
|
1146
1146
|
// eslint-disable-next-line no-useless-assignment -- Reason: initial value unused due to immediate reassignment in all branches
|
|
1147
1147
|
let snapshot: NocturnalSessionSnapshot | null = null;
|
|
1148
1148
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1149
|
+
if (options.principleIdOverride && options.snapshotOverride) {
|
|
1150
|
+
// Skip Selector: use provided principleId and snapshot directly
|
|
1151
|
+
selectedPrincipleId = options.principleIdOverride;
|
|
1152
|
+
selectedSessionId = options.snapshotOverride.sessionId;
|
|
1153
|
+
snapshot = options.snapshotOverride;
|
|
1154
|
+
console.log(`[nocturnal-service] Using override: principleId=${selectedPrincipleId}, sessionId=${selectedSessionId}`);
|
|
1155
|
+
// Calculate violation density from snapshot stats for meaningful diagnostics
|
|
1156
|
+
const snapStats = options.snapshotOverride.stats;
|
|
1156
1157
|
const totalToolCalls = snapStats?.totalToolCalls ?? 0;
|
|
1157
1158
|
const failureCount = snapStats?.failureCount ?? 0;
|
|
1158
1159
|
const violationDensity = totalToolCalls > 0 ? failureCount / totalToolCalls : 0;
|
|
@@ -1177,6 +1178,7 @@ async function executeNocturnalReflectionWithAdapter(
|
|
|
1177
1178
|
diagnostics.idle = { isIdle: true, mostRecentActivityAt: 0, idleForMs: 0, userActiveSessions: 0, abandonedSessionIds: [], trajectoryGuardrailConfirmsIdle: true, reason: 'selector skipped (override provided)' };
|
|
1178
1179
|
} else {
|
|
1179
1180
|
// Normal Selector path
|
|
1181
|
+
console.log(`[nocturnal-service] Step 2/7: Target selection (normal path)`);
|
|
1180
1182
|
const extractor = createNocturnalTrajectoryExtractor(workspaceDir, stateDir);
|
|
1181
1183
|
const selector = new NocturnalTargetSelector(workspaceDir, stateDir, extractor, {
|
|
1182
1184
|
idleCheckOverride: options.idleCheckOverride,
|
|
@@ -1185,8 +1187,10 @@ async function executeNocturnalReflectionWithAdapter(
|
|
|
1185
1187
|
|
|
1186
1188
|
const selection = selector.select();
|
|
1187
1189
|
diagnostics.selection = selection;
|
|
1190
|
+
console.log(`[nocturnal-service] Selector result: decision=${selection.decision}, skipReason=${selection.skipReason ?? 'none'}`);
|
|
1188
1191
|
|
|
1189
1192
|
if (selection.decision === 'skip') {
|
|
1193
|
+
console.warn(`[nocturnal-service] Target selection skipped: ${selection.skipReason}`);
|
|
1190
1194
|
return {
|
|
1191
1195
|
success: false,
|
|
1192
1196
|
noTargetSelected: true,
|
|
@@ -176,8 +176,14 @@ export class NocturnalWorkflowManager implements WorkflowManager {
|
|
|
176
176
|
// Other workflow managers (empathy, deep-reflect) have this check
|
|
177
177
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: TrinityRuntimeAdapter interface doesn't expose api.runtime.subagent, but OpenClawTrinityRuntimeAdapter has it
|
|
178
178
|
const subagent = (this.runtimeAdapter as any).api?.runtime?.subagent;
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
const apiAvailable = !!(this.runtimeAdapter as any).api;
|
|
180
|
+
const runtimeAvailable = !!(this.runtimeAdapter as any).api?.runtime;
|
|
181
|
+
const subagentAvailable = isSubagentRuntimeAvailable(subagent);
|
|
182
|
+
|
|
183
|
+
this.logger.info(`[PD:NocturnalWorkflow] Subagent availability check: api=${apiAvailable}, runtime=${runtimeAvailable}, subagent=${subagentAvailable}`);
|
|
184
|
+
|
|
185
|
+
if (!subagentAvailable) {
|
|
186
|
+
this.logger.warn(`[PD:NocturnalWorkflow] Subagent runtime unavailable (api=${apiAvailable}, runtime=${runtimeAvailable}), skipping workflow`);
|
|
181
187
|
throw new Error(`NocturnalWorkflowManager: subagent runtime unavailable`);
|
|
182
188
|
}
|
|
183
189
|
|
|
@@ -234,12 +240,15 @@ export class NocturnalWorkflowManager implements WorkflowManager {
|
|
|
234
240
|
// When principleId is provided, we pass it as principleIdOverride to skip Selector.
|
|
235
241
|
// When principleId is missing, Selector will choose a principle from training store.
|
|
236
242
|
this.logger.info(`[PD:NocturnalWorkflow] Calling executeNocturnalReflectionAsync for full pipeline (principleId=${principleId ?? 'auto-select'})`);
|
|
243
|
+
const pipelineStart = Date.now();
|
|
237
244
|
|
|
238
245
|
// #213: Wrap fire-and-forget Promise with .catch() to prevent
|
|
239
246
|
// unhandled promise rejections if anything throws outside the try-catch
|
|
240
247
|
// (e.g., during parameter construction or environment errors).
|
|
241
248
|
Promise.resolve().then(async () => {
|
|
242
249
|
try {
|
|
250
|
+
this.logger.info(`[PD:NocturnalWorkflow] [${workflowId}] Pipeline step 1/4: Starting executeNocturnalReflectionAsync`);
|
|
251
|
+
|
|
243
252
|
const result = await executeNocturnalReflectionAsync(
|
|
244
253
|
this.workspaceDir,
|
|
245
254
|
this.stateDir,
|
|
@@ -262,20 +271,26 @@ export class NocturnalWorkflowManager implements WorkflowManager {
|
|
|
262
271
|
}
|
|
263
272
|
);
|
|
264
273
|
|
|
274
|
+
const pipelineDuration = Date.now() - pipelineStart;
|
|
275
|
+
this.logger.info(`[PD:NocturnalWorkflow] [${workflowId}] Pipeline completed in ${pipelineDuration}ms, success=${result.success}`);
|
|
276
|
+
|
|
265
277
|
if (result.success) {
|
|
278
|
+
this.logger.info(`[PD:NocturnalWorkflow] [${workflowId}] Pipeline step 4/4: Completed successfully, artifactId=${result.diagnostics?.persistedPath}`);
|
|
266
279
|
this.store.recordEvent(workflowId, 'nocturnal_completed', null, 'completed', 'Full pipeline completed via executeNocturnalReflectionAsync', {
|
|
267
280
|
artifactId: result.diagnostics?.persistedPath,
|
|
268
281
|
});
|
|
269
282
|
this.completedWorkflows.set(workflowId, Date.now());
|
|
270
283
|
} else {
|
|
271
284
|
const reason = result.noTargetSelected ? 'no_target_selected' : 'validation_failed';
|
|
285
|
+
this.logger.warn(`[PD:NocturnalWorkflow] [${workflowId}] Pipeline failed: reason=${reason}, noTargetSelected=${result.noTargetSelected}, skipReason=${result.skipReason ?? 'none'}, validationFailures=${result.validationFailures?.length ?? 0}`);
|
|
272
286
|
this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', reason, {
|
|
273
287
|
failures: result.validationFailures,
|
|
274
288
|
skipReason: result.skipReason,
|
|
275
289
|
});
|
|
276
290
|
}
|
|
277
291
|
} catch (err) {
|
|
278
|
-
|
|
292
|
+
const errDuration = Date.now() - pipelineStart;
|
|
293
|
+
this.logger.error(`[PD:NocturnalWorkflow] [${workflowId}] executeNocturnalReflectionAsync threw after ${errDuration}ms: ${String(err)}`);
|
|
279
294
|
this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', String(err), { workflowId });
|
|
280
295
|
}
|
|
281
296
|
}).catch((err) => {
|
|
@@ -61,4 +61,17 @@ LLMs are highly sensitive to XML tags; this structure is designed to boost instr
|
|
|
61
61
|
<must>Maintain extreme digital cleanliness. The project root is SACRED. Use strict `kebab-case` for all naming. Clean up all test scripts and debug artifacts after the task.</must>
|
|
62
62
|
<forbidden>Creating arbitrary temporary files (e.g., `test.txt`, `temp.md`, `debug.log`) in the project root directory.</forbidden>
|
|
63
63
|
</directive>
|
|
64
|
+
|
|
65
|
+
<!-- 复杂任务分解与记忆外化 (Complex Task Decomposition & Memory Externalization) -->
|
|
66
|
+
<directive id="T-09" name="DIVIDE_AND_CONQUER">
|
|
67
|
+
<trigger>When facing a complex task with multiple interdependent steps or large-scale refactoring.</trigger>
|
|
68
|
+
<must>Break the work into smallest meaningful units. Execute in dependency order. Validate each unit before proceeding.</must>
|
|
69
|
+
<forbidden>Tackle complex tasks as a single monolithic operation. Mix unrelated changes in one edit.</forbidden>
|
|
70
|
+
</directive>
|
|
71
|
+
|
|
72
|
+
<directive id="T-10" name="MEMORY_EXTERNALIZATION">
|
|
73
|
+
<trigger>When drawing conclusions, completing analysis, or about to switch context.</trigger>
|
|
74
|
+
<must>Write conclusions to a file (plan.md, scratchpad, memory) before proceeding. Preserve reasoning for future reference.</must>
|
|
75
|
+
<forbidden>Keep important conclusions only in conversation context. Lose state between turns.</forbidden>
|
|
76
|
+
</directive>
|
|
64
77
|
</thinking_os_core_directives>
|
|
@@ -61,4 +61,17 @@
|
|
|
61
61
|
<must>保持极致的数字洁癖。项目根目录是神圣的。所有命名必须严格使用 `kebab-case`。任务结束后清理所有的测试脚本和 Debug 遗留物。</must>
|
|
62
62
|
<forbidden>在项目根目录下随意创建临时文件(如 `test.txt`、`temp.md`、`debug.log`)。</forbidden>
|
|
63
63
|
</directive>
|
|
64
|
+
|
|
65
|
+
<!-- 复杂任务分解与记忆外化 (Complex Task Decomposition & Memory Externalization) -->
|
|
66
|
+
<directive id="T-09" name="DIVIDE_AND_CONQUER">
|
|
67
|
+
<trigger>面对包含多个相互依赖步骤的复杂任务或大规模重构时。</trigger>
|
|
68
|
+
<must>将工作拆分为最小有意义的单元。按依赖顺序执行。在继续之前验证每个单元。</must>
|
|
69
|
+
<forbidden>将复杂任务当作单一操作处理。在一次编辑中混合不相关的变更。</forbidden>
|
|
70
|
+
</directive>
|
|
71
|
+
|
|
72
|
+
<directive id="T-10" name="MEMORY_EXTERNALIZATION">
|
|
73
|
+
<trigger>得出结论、完成分析或即将切换上下文时。</trigger>
|
|
74
|
+
<must>将结论写入文件(plan.md、scratchpad、memory)后再继续。保留推理过程供未来参考。</must>
|
|
75
|
+
<forbidden>将重要结论仅保留在对话上下文中。在会话切换后丢失状态。</forbidden>
|
|
76
|
+
</directive>
|
|
64
77
|
</thinking_os_core_directives>
|
package/ui/src/i18n/ui.ts
CHANGED
|
@@ -225,21 +225,46 @@ export const i18n = {
|
|
|
225
225
|
dormant: { zh: '休眠', en: 'Dormant' },
|
|
226
226
|
effective: { zh: '有效', en: 'Effective' },
|
|
227
227
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
// Charts and sections
|
|
229
|
+
coverageTrend: { zh: '覆盖率趋势', en: 'Coverage Trend' },
|
|
230
|
+
emptyCoverageTrend: { zh: '今日暂无覆盖率记录', en: 'No coverage data yet' },
|
|
231
|
+
emptyCoverageTrendDesc: { zh: '当 AI 开始执行任务后,覆盖率会自动记录。', en: 'Coverage is tracked automatically once AI starts working.' },
|
|
232
|
+
scenarioHeatmap: { zh: '场景热力图', en: 'Scenario Heatmap' },
|
|
233
|
+
emptyScenarioMatrix: { zh: '暂无场景数据', en: 'No scenario data yet' },
|
|
234
|
+
emptyScenarioMatrixDesc: { zh: '模型触发场景后会在此显示。', en: 'Scenarios will appear here when models are triggered.' },
|
|
235
|
+
emptyAllActive: { zh: '所有模型都在使用中', en: 'All models are active' },
|
|
236
|
+
emptyAllActiveDesc: { zh: '没有休眠模型。', en: 'No dormant models.' },
|
|
237
|
+
noModelsYet: { zh: '暂无思维模型数据', en: 'No thinking model data yet' },
|
|
238
|
+
noModelsYetDesc: { zh: 'AI 开始使用后,这里会显示思维模型的使用情况。', en: 'Thinking model usage will appear here once AI starts working.' },
|
|
239
|
+
|
|
240
|
+
// Recommendations
|
|
241
|
+
dormantModels: { zh: '休眠模型', en: 'Dormant Models' },
|
|
242
|
+
reinforce: { zh: '保持', en: 'Reinforce' },
|
|
243
|
+
rework: { zh: '重构', en: 'Rework' },
|
|
244
|
+
archive: { zh: '归档', en: 'Archive' },
|
|
245
|
+
filterByRec: { zh: '按推荐过滤', en: 'Filter by Rec' },
|
|
231
246
|
|
|
232
247
|
// Detail sections
|
|
233
|
-
outcomeStats: { zh: '
|
|
234
|
-
|
|
235
|
-
recentEvents: { zh: '最近事件', en: 'Recent Events' },
|
|
236
|
-
noScenariosYet: { zh: '暂无场景', en: 'No scenarios yet' },
|
|
237
|
-
|
|
238
|
-
// Outcome stats
|
|
248
|
+
outcomeStats: { zh: '效果统计', en: 'Outcome Stats' },
|
|
249
|
+
usageTrend: { zh: '使用趋势', en: 'Usage Trend' },
|
|
239
250
|
success: { zh: '成功', en: 'Success' },
|
|
240
251
|
failure: { zh: '失败', en: 'Failure' },
|
|
241
252
|
pain: { zh: '痛点', en: 'Pain' },
|
|
242
253
|
correction: { zh: '纠正', en: 'Correction' },
|
|
254
|
+
scenarioDistribution: { zh: '场景分布', en: 'Scenario Distribution' },
|
|
255
|
+
recentEvents: { zh: '最近事件', en: 'Recent Events' },
|
|
256
|
+
toolContext: { zh: '工具上下文', en: 'Tool Context' },
|
|
257
|
+
painContext: { zh: '痛点上下文', en: 'Pain Context' },
|
|
258
|
+
principleContext: { zh: '原则上下文', en: 'Principle Context' },
|
|
259
|
+
trigger: { zh: '触发条件', en: 'Trigger' },
|
|
260
|
+
antiPattern: { zh: '禁止行为', en: 'Anti-Pattern' },
|
|
261
|
+
thinkingOsSource: { zh: '思维模型定义来源', en: 'Thinking Model Source' },
|
|
262
|
+
|
|
263
|
+
// Empty state
|
|
264
|
+
emptyTitle: { zh: '选择一个思维模型', en: 'Select a thinking model' },
|
|
265
|
+
emptyDesc: { zh: '点击左侧列表中的模型,查看场景分布和最近事件', en: 'Click a model from the list to inspect scenario coverage and recent events' },
|
|
266
|
+
noDataTitle: { zh: '思维模型定义', en: 'Thinking Model Definitions' },
|
|
267
|
+
noDataDesc: { zh: '以下是 10 个思维模型的定义。当 AI 开始使用后,这里会显示每个模型的使用统计。', en: 'Below are 10 thinking model definitions. Usage statistics will appear once the AI starts working.' },
|
|
243
268
|
|
|
244
269
|
// Table
|
|
245
270
|
hits: { zh: '命中', en: 'Hits' },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { ChevronLeft } from 'lucide-react';
|
|
2
|
+
import { ChevronLeft, Clock, Activity, Shield, Zap, BookOpen } from 'lucide-react';
|
|
3
3
|
import { api } from '../api';
|
|
4
4
|
import type {
|
|
5
5
|
OverviewResponse,
|