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.
@@ -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
- /* eslint-disable @typescript-eslint/init-declarations, @typescript-eslint/no-use-before-define, @typescript-eslint/prefer-destructuring, no-useless-assignment, @typescript-eslint/no-unused-vars */
45
+
48
46
  let match: RegExpExecArray | null = null;
49
47
 
50
48
  while ((match = directiveRegex.exec(content)) !== null) {
51
- const [, attrs, body] = match;
52
-
53
- const idMatch = /id="([^"]+)"/i.exec(attrs);
54
- const nameMatch = /name="([^"]+)"/i.exec(attrs);
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
- const directive: ThinkingOsDirective = {
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 plugin templates for a given language.
74
- * Falls back to the workspace THINKING_OS.md if it exists.
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's own THINKING_OS.md
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 = path.join(
97
- path.dirname(path.dirname(path.dirname(currentDir))),
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
- return parseThinkingOsMd(content);
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 = path.join(
116
- path.dirname(path.dirname(path.dirname(currentDir))),
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 that are meaningful
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: sequences of letters
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 no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: catch binding intentionally unused
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 no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: catch binding intentionally unused
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
- // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: ctx param required by hook callback signature but not used in this handler
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
- 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
- // Calculate violation density from snapshot stats for meaningful diagnostics
1155
- const snapStats = options.snapshotOverride.stats;
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
- if (!isSubagentRuntimeAvailable(subagent)) {
180
- this.logger.warn(`[PD:NocturnalWorkflow] Subagent runtime unavailable, skipping workflow`);
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
- this.logger.error(`[PD:NocturnalWorkflow] executeNocturnalReflectionAsync threw: ${String(err)}`);
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
- // Empty state
229
- emptyTitle: { zh: '选择一个思维模型', en: 'Select a thinking model' },
230
- emptyDesc: { zh: '点击左侧列表中的模型,查看场景分布和最近事件', en: 'Click a model from the list to inspect scenario coverage and recent events' },
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: '结果统计', en: 'Outcome Stats' },
234
- scenarioDistribution: { zh: '场景分布', en: 'Scenario Distribution' },
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,