principles-disciple 1.7.4 → 1.7.6
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/dist/commands/evolution-status.js +32 -44
- package/dist/commands/focus.js +30 -155
- package/dist/config/defaults/runtime.d.ts +40 -0
- package/dist/config/defaults/runtime.js +44 -0
- package/dist/config/errors.d.ts +84 -0
- package/dist/config/errors.js +94 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +7 -0
- package/dist/constants/diagnostician.d.ts +12 -0
- package/dist/constants/diagnostician.js +56 -0
- package/dist/constants/tools.d.ts +2 -2
- package/dist/constants/tools.js +1 -1
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.js +7 -0
- package/dist/core/control-ui-db.d.ts +27 -0
- package/dist/core/control-ui-db.js +18 -0
- package/dist/core/evolution-engine.js +1 -1
- package/dist/core/focus-history.d.ts +92 -0
- package/dist/core/focus-history.js +490 -0
- package/dist/core/init.js +2 -2
- package/dist/core/path-resolver.js +2 -1
- package/dist/core/profile.js +1 -1
- package/dist/core/trajectory.d.ts +60 -0
- package/dist/core/trajectory.js +72 -2
- package/dist/hooks/bash-risk.d.ts +57 -0
- package/dist/hooks/bash-risk.js +137 -0
- package/dist/hooks/edit-verification.d.ts +62 -0
- package/dist/hooks/edit-verification.js +256 -0
- package/dist/hooks/gate-block-helper.d.ts +44 -0
- package/dist/hooks/gate-block-helper.js +119 -0
- package/dist/hooks/gate.d.ts +18 -0
- package/dist/hooks/gate.js +63 -752
- package/dist/hooks/gfi-gate.d.ts +40 -0
- package/dist/hooks/gfi-gate.js +112 -0
- package/dist/hooks/progressive-trust-gate.d.ts +79 -0
- package/dist/hooks/progressive-trust-gate.js +242 -0
- package/dist/hooks/prompt.js +83 -28
- package/dist/hooks/subagent.js +1 -2
- package/dist/hooks/thinking-checkpoint.d.ts +37 -0
- package/dist/hooks/thinking-checkpoint.js +51 -0
- package/dist/http/principles-console-route.d.ts +7 -0
- package/dist/http/principles-console-route.js +255 -3
- package/dist/index.js +0 -2
- package/dist/service/central-database.d.ts +104 -0
- package/dist/service/central-database.js +649 -0
- package/dist/service/control-ui-query-service.d.ts +1 -1
- package/dist/service/control-ui-query-service.js +3 -3
- package/dist/service/evolution-query-service.d.ts +1 -1
- package/dist/service/evolution-query-service.js +5 -5
- package/dist/service/evolution-worker.d.ts +10 -0
- package/dist/service/evolution-worker.js +10 -6
- package/dist/service/phase3-input-filter.d.ts +57 -0
- package/dist/service/phase3-input-filter.js +93 -3
- package/dist/service/runtime-summary-service.d.ts +34 -0
- package/dist/service/runtime-summary-service.js +93 -1
- package/dist/tools/deep-reflect.js +1 -2
- package/dist/types/event-types.d.ts +2 -0
- package/dist/types/runtime-summary.d.ts +54 -0
- package/dist/types/runtime-summary.js +1 -0
- package/dist/utils/subagent-probe.d.ts +11 -0
- package/dist/utils/subagent-probe.js +46 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/templates/langs/en/core/AGENTS.md +1 -1
- package/templates/langs/en/core/TOOLS.md +1 -1
- package/templates/langs/zh/core/AGENTS.md +1 -1
- package/templates/langs/zh/core/TOOLS.md +1 -1
- package/{agents/auditor.md → templates/langs/zh/skills/pd-auditor/SKILL.md} +3 -3
- package/{agents/diagnostician.md → templates/langs/zh/skills/pd-diagnostician/SKILL.md} +39 -29
- package/{agents/explorer.md → templates/langs/zh/skills/pd-explorer/SKILL.md} +3 -3
- package/{agents/implementer.md → templates/langs/zh/skills/pd-implementer/SKILL.md} +3 -3
- package/{agents/planner.md → templates/langs/zh/skills/pd-planner/SKILL.md} +3 -3
- package/{agents/reporter.md → templates/langs/zh/skills/pd-reporter/SKILL.md} +3 -3
- package/{agents/reviewer.md → templates/langs/zh/skills/pd-reviewer/SKILL.md} +3 -3
- package/dist/core/agent-loader.d.ts +0 -44
- package/dist/core/agent-loader.js +0 -147
- package/dist/tools/agent-spawn.d.ts +0 -54
- package/dist/tools/agent-spawn.js +0 -456
package/dist/hooks/prompt.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as path from 'path';
|
|
|
3
3
|
import { clearInjectedProbationIds, getSession, resetFriction, setInjectedProbationIds } from '../core/session-tracker.js';
|
|
4
4
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
5
5
|
import { defaultContextConfig } from '../types.js';
|
|
6
|
-
import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection } from '../core/focus-history.js';
|
|
6
|
+
import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection, autoCompressFocus, safeReadCurrentFocus } from '../core/focus-history.js';
|
|
7
7
|
import { empathyObserverManager } from '../service/empathy-observer-manager.js';
|
|
8
8
|
import { PathResolver } from '../core/path-resolver.js';
|
|
9
9
|
/**
|
|
@@ -130,6 +130,8 @@ function resolveEvolutionTask(inProgressTask, messages, maxContextMessages = 4,
|
|
|
130
130
|
const preview = typeof inProgressTask.trigger_text_preview === 'string' && inProgressTask.trigger_text_preview.trim()
|
|
131
131
|
? inProgressTask.trigger_text_preview.trim()
|
|
132
132
|
: 'N/A';
|
|
133
|
+
const sessionId = typeof inProgressTask.session_id === 'string' ? inProgressTask.session_id.trim() : '';
|
|
134
|
+
const agentId = typeof inProgressTask.agent_id === 'string' ? inProgressTask.agent_id.trim() : '';
|
|
133
135
|
const conversationContext = includeConversationContext
|
|
134
136
|
? extractRecentConversationContext(messages, maxContextMessages, maxCharsPerMsg)
|
|
135
137
|
: '';
|
|
@@ -142,11 +144,27 @@ function resolveEvolutionTask(inProgressTask, messages, maxContextMessages = 4,
|
|
|
142
144
|
`;
|
|
143
145
|
taskDescription += `**Trigger Text**: "${preview}"
|
|
144
146
|
`;
|
|
147
|
+
if (sessionId) {
|
|
148
|
+
taskDescription += `**Session ID**: ${sessionId}
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
if (agentId) {
|
|
152
|
+
taskDescription += `**Agent ID**: ${agentId}
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
145
155
|
if (conversationContext) {
|
|
146
156
|
taskDescription += `
|
|
147
157
|
---
|
|
148
158
|
**Recent Conversation Context**:
|
|
149
159
|
${conversationContext}`;
|
|
160
|
+
}
|
|
161
|
+
else if (!sessionId) {
|
|
162
|
+
taskDescription += `
|
|
163
|
+
---
|
|
164
|
+
**Note**: 对话上下文不可用。请主动收集证据:
|
|
165
|
+
1. 从 Reason 字段提取关键词,搜索相关代码
|
|
166
|
+
2. 读取 .state/logs/events.jsonl 最近日志
|
|
167
|
+
3. 基于 Reason 中的文件路径定位问题`;
|
|
150
168
|
}
|
|
151
169
|
taskDescription += `
|
|
152
170
|
|
|
@@ -227,7 +245,6 @@ export function resolveModelFromConfig(modelConfig, logger) {
|
|
|
227
245
|
}
|
|
228
246
|
// 闁哄秶鍘х槐?3: 闁轰焦澹嗙划宥夊冀閻撳海纭€闁挎稑鐗呯粭澶愬绩椤栨稑鐦柨娑樿嫰瑜板倿宕欐ウ娆惧妳闁告稑顭槐?
|
|
229
247
|
if (Array.isArray(modelConfig)) {
|
|
230
|
-
console.warn(`[PD:Prompt] Array model config not supported. Expected "provider/model" string or { primary: "..." } object.`);
|
|
231
248
|
logger?.warn(`[PD:Prompt] Array model config not supported. Expected "provider/model" string or { primary: "..." } object.`);
|
|
232
249
|
return null;
|
|
233
250
|
}
|
|
@@ -388,8 +405,7 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
|
|
|
388
405
|
- Use the current session for the normal user reply.
|
|
389
406
|
- Use sessions_send for cross-session messaging.
|
|
390
407
|
- Use agents_list / sessions_list / sessions_spawn for peer-agent or peer-session orchestration.
|
|
391
|
-
- Use
|
|
392
|
-
- Use pd_run_worker only for Principles Disciple internal workers such as diagnostician/explorer.
|
|
408
|
+
- Use sessions_spawn with pd-diagnostician/pd-explorer/etc skills for internal worker tasks.
|
|
393
409
|
|
|
394
410
|
## 妫e啯鎯?INTERNAL SYSTEM LAYOUT
|
|
395
411
|
- Your core plugin logic is rooted at: ${PathResolver.getExtensionRoot() || 'EXTENSION_ROOT (unresolved)'}
|
|
@@ -407,7 +423,7 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
|
|
|
407
423
|
trustContext += `Hygiene: ${hygiene.persistenceCount} persists today\n`;
|
|
408
424
|
// Stage-based restrictions
|
|
409
425
|
if (safeStage === 1) {
|
|
410
|
-
trustContext += `ACTION CONSTRAINT: You are in READ-ONLY MODE. You MUST use
|
|
426
|
+
trustContext += `ACTION CONSTRAINT: You are in READ-ONLY MODE. You MUST use sessions_spawn with the pd-diagnostician skill to recover trust before writing files.\n`;
|
|
411
427
|
}
|
|
412
428
|
else if (safeStage === 2) {
|
|
413
429
|
trustContext += `ACTION CONSTRAINT: LIMITED MODE. You are restricted to a maximum of 50 lines per edit.\n`;
|
|
@@ -421,7 +437,11 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
|
|
|
421
437
|
prependContext += `<system_override:runtime_constraints>\n${trustContext.trim()}\n</system_override:runtime_constraints>\n`;
|
|
422
438
|
}
|
|
423
439
|
// 闁崇儤鍔忛弲鏌ュ煛?3. Evolution Directive (always on, highest priority) - stays in prependContext 闁崇儤鍔忛弲鏌ュ煛?
|
|
424
|
-
|
|
440
|
+
// NOTE: active evolution task prompt is injected from EVOLUTION_QUEUE for active tasks
|
|
441
|
+
// NOT used for Phase 3 eligibility decisions
|
|
442
|
+
// EVOLUTION_DIRECTIVE.json is a compatibility-only display artifact
|
|
443
|
+
// Phase 3 eligibility uses only queue and trust (see phase3-input-filter.ts)
|
|
444
|
+
let activeEvolutionTaskPrompt = '';
|
|
425
445
|
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
426
446
|
if (fs.existsSync(queuePath)) {
|
|
427
447
|
try {
|
|
@@ -441,32 +461,48 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
|
|
|
441
461
|
const escapedTask = JSON.stringify(resolvedTask);
|
|
442
462
|
logger?.info(`[PD:Prompt] Injecting EVOLUTION TASK for: ${inProgressTask.id}`);
|
|
443
463
|
if (trigger === 'user') {
|
|
444
|
-
|
|
464
|
+
activeEvolutionTaskPrompt = `<evolution_task priority="high">
|
|
445
465
|
TASK: ${escapedTask}
|
|
446
466
|
|
|
447
|
-
REQUIRED ACTION:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
467
|
+
REQUIRED ACTION (两阶段回复):
|
|
468
|
+
|
|
469
|
+
**阶段 1 - 立即回复用户:**
|
|
470
|
+
简要告知用户你检测到的问题,例如:
|
|
471
|
+
"我注意到刚才的操作遇到了一些问题(简述问题),我已安排诊断代理在后台分析根因。分析完成后会继续告诉你结果。"
|
|
472
|
+
|
|
473
|
+
然后调用:
|
|
474
|
+
sessions_spawn(task="使用 pd-diagnostician skill 执行诊断任务。\n\n任务: ${escapedTask}")
|
|
475
|
+
|
|
476
|
+
**阶段 2 - 收到子代理结果后:**
|
|
477
|
+
当收到诊断代理的完成消息后,用以下格式回复用户:
|
|
478
|
+
|
|
479
|
+
"**诊断完成** 🔍
|
|
480
|
+
|
|
481
|
+
基于后台分析结果:
|
|
482
|
+
- **根因**: [一句话总结]
|
|
483
|
+
- **分类**: [People/Design/Assumption/Tooling]
|
|
484
|
+
- **建议**: [可执行的改进建议]
|
|
485
|
+
|
|
486
|
+
相关原则已记录到 PRINCIPLES.md。"
|
|
487
|
+
|
|
488
|
+
IMPORTANT:
|
|
452
489
|
- Do NOT reply with "[EVOLUTION_ACK]".
|
|
453
|
-
-
|
|
490
|
+
- 子代理结果会作为新消息到达,届时再进行阶段2的回复。
|
|
454
491
|
</evolution_task>\n`;
|
|
455
492
|
}
|
|
456
493
|
else {
|
|
457
|
-
|
|
494
|
+
activeEvolutionTaskPrompt = `<evolution_task priority="critical">
|
|
458
495
|
TASK: ${escapedTask}
|
|
459
496
|
|
|
460
497
|
REQUIRED ACTION:
|
|
461
498
|
- Start diagnostics immediately by calling:
|
|
462
|
-
|
|
463
|
-
- Treat pd_run_worker as an internal Principles Disciple worker, not a peer-session messaging tool.
|
|
499
|
+
sessions_spawn(task="使用 pd-diagnostician skill 执行诊断任务。\n\n任务: ${escapedTask}")
|
|
464
500
|
- Do NOT reply with "[EVOLUTION_ACK]".
|
|
465
501
|
</evolution_task>\n`;
|
|
466
502
|
}
|
|
467
503
|
break;
|
|
468
504
|
}
|
|
469
|
-
if (!
|
|
505
|
+
if (!activeEvolutionTaskPrompt && inProgressTasks.length > 0) {
|
|
470
506
|
logger?.warn('[PD:Prompt] Skipping evolution task injection because task payload is invalid.');
|
|
471
507
|
}
|
|
472
508
|
}
|
|
@@ -475,8 +511,8 @@ REQUIRED ACTION:
|
|
|
475
511
|
}
|
|
476
512
|
}
|
|
477
513
|
// Inject queue-derived evolution task at the front of prependContext
|
|
478
|
-
if (
|
|
479
|
-
prependContext =
|
|
514
|
+
if (activeEvolutionTaskPrompt) {
|
|
515
|
+
prependContext = activeEvolutionTaskPrompt + prependContext;
|
|
480
516
|
}
|
|
481
517
|
// 鈺愨晲鈺?4. Empathy Observer Spawn (async sidecar) 鈺愨晲鈺?
|
|
482
518
|
// Skip if this is a subagent session or if the message indicates agent-to-agent communication
|
|
@@ -581,34 +617,53 @@ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
|
|
|
581
617
|
let workingMemoryContent = '';
|
|
582
618
|
if (!isMinimalMode && contextConfig.projectFocus !== 'off') {
|
|
583
619
|
const focusPath = wctx.resolve('CURRENT_FOCUS');
|
|
584
|
-
|
|
620
|
+
const extensionRoot = PathResolver.getExtensionRoot();
|
|
621
|
+
// 🔒 安全读取:自动验证格式,损坏时从模板恢复
|
|
622
|
+
const { content: currentFocus, recovered, validationErrors } = safeReadCurrentFocus(focusPath, extensionRoot || '', logger);
|
|
623
|
+
if (recovered) {
|
|
624
|
+
logger?.info?.(`[PD:Prompt] CURRENT_FOCUS.md was recovered from template`);
|
|
625
|
+
}
|
|
626
|
+
if (validationErrors.length > 0) {
|
|
627
|
+
logger?.warn?.(`[PD:Prompt] CURRENT_FOCUS validation errors: ${validationErrors.join(', ')}`);
|
|
628
|
+
}
|
|
629
|
+
if (currentFocus.trim()) {
|
|
585
630
|
try {
|
|
586
|
-
|
|
587
|
-
|
|
631
|
+
// 🚀 自动压缩门禁:检查文件大小,超过阈值自动压缩
|
|
632
|
+
const stateDir = wctx.stateDir;
|
|
633
|
+
const compressResult = autoCompressFocus(focusPath, workspaceDir, stateDir);
|
|
634
|
+
if (compressResult.compressed) {
|
|
635
|
+
logger?.info?.(`[PD:Prompt] Auto-compressed CURRENT_FOCUS: ${compressResult.oldLines} → ${compressResult.newLines} lines. Milestones archived: ${compressResult.milestonesArchived}`);
|
|
636
|
+
}
|
|
637
|
+
else if (compressResult.reason === 'Rate limited (24h interval)') {
|
|
638
|
+
logger?.debug?.(`[PD:Prompt] Auto-compress skipped: ${compressResult.reason}`);
|
|
639
|
+
}
|
|
640
|
+
// 重新读取(可能被压缩更新了)
|
|
641
|
+
const finalContent = fs.readFileSync(focusPath, 'utf8').trim();
|
|
642
|
+
if (finalContent) {
|
|
588
643
|
// 解析工作记忆部分(用于独立注入)
|
|
589
|
-
const workingMemorySnapshot = parseWorkingMemorySection(
|
|
644
|
+
const workingMemorySnapshot = parseWorkingMemorySection(finalContent);
|
|
590
645
|
if (workingMemorySnapshot) {
|
|
591
646
|
workingMemoryContent = workingMemoryToInjection(workingMemorySnapshot);
|
|
592
647
|
}
|
|
593
648
|
if (contextConfig.projectFocus === 'summary') {
|
|
594
649
|
// Summary mode: intelligent extraction prioritizing key sections
|
|
595
|
-
projectContextContent = extractSummary(
|
|
650
|
+
projectContextContent = extractSummary(finalContent, 30);
|
|
596
651
|
}
|
|
597
652
|
else {
|
|
598
653
|
// Full mode: current version + recent history (3 versions)
|
|
599
654
|
const historyVersions = getHistoryVersions(focusPath, 3);
|
|
600
655
|
if (historyVersions.length > 0) {
|
|
601
|
-
const historySections = historyVersions.map((v, i) => `\n---\n\n
|
|
602
|
-
projectContextContent = `${
|
|
656
|
+
const historySections = historyVersions.map((v, i) => `\n---\n\n**历史版本 v${historyVersions.length - i}**\n\n${v}`).join('');
|
|
657
|
+
projectContextContent = `${finalContent}${historySections}`;
|
|
603
658
|
}
|
|
604
659
|
else {
|
|
605
|
-
projectContextContent =
|
|
660
|
+
projectContextContent = finalContent;
|
|
606
661
|
}
|
|
607
662
|
}
|
|
608
663
|
}
|
|
609
664
|
}
|
|
610
665
|
catch (e) {
|
|
611
|
-
logger?.error(`[PD:Prompt] Failed to
|
|
666
|
+
logger?.error(`[PD:Prompt] Failed to process CURRENT_FOCUS: ${String(e)}`);
|
|
612
667
|
}
|
|
613
668
|
}
|
|
614
669
|
}
|
package/dist/hooks/subagent.js
CHANGED
|
@@ -3,7 +3,6 @@ import { writePainFlag } from '../core/pain.js';
|
|
|
3
3
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
4
4
|
import { empathyObserverManager } from '../service/empathy-observer-manager.js';
|
|
5
5
|
import { acquireQueueLock } from '../service/evolution-worker.js';
|
|
6
|
-
import { isSubagentRuntimeAvailable } from '../utils/subagent-probe.js';
|
|
7
6
|
const COMPLETION_RETRY_DELAY_MS = 250;
|
|
8
7
|
const COMPLETION_MAX_RETRIES = 3;
|
|
9
8
|
const COMPLETION_RETRY_TTL_MS = 60 * 60 * 1000; // 1 hour TTL for retry entries
|
|
@@ -252,7 +251,7 @@ export async function handleSubagentEnded(event, ctx) {
|
|
|
252
251
|
}
|
|
253
252
|
}
|
|
254
253
|
// Read diagnostician output and create principle with generalized pattern
|
|
255
|
-
if (completedTaskId &&
|
|
254
|
+
if (completedTaskId && ctx.api?.runtime?.subagent) {
|
|
256
255
|
try {
|
|
257
256
|
const messages = await ctx.api?.runtime?.subagent?.getSessionMessages?.({
|
|
258
257
|
sessionKey: targetSessionKey,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thinking Checkpoint Module
|
|
3
|
+
*
|
|
4
|
+
* Enforces P-10 deep reflection requirement for high-risk tool operations.
|
|
5
|
+
*
|
|
6
|
+
* **Responsibilities:**
|
|
7
|
+
* - Check if high-risk tools have recent deep thinking (T-01 through T-10)
|
|
8
|
+
* - Block high-risk operations without preceding deep reflection
|
|
9
|
+
* - Configurable time window for thinking validity (default 5 minutes)
|
|
10
|
+
* - Provide clear guidance on required action (deep_reflect tool usage)
|
|
11
|
+
*
|
|
12
|
+
* **Configuration:**
|
|
13
|
+
* - Thinking checkpoint settings from profile.thinking_checkpoint
|
|
14
|
+
* - Window duration for thinking validity
|
|
15
|
+
* - High-risk tool list
|
|
16
|
+
*/
|
|
17
|
+
import type { PluginHookBeforeToolCallEvent, PluginHookBeforeToolCallResult } from '../openclaw-sdk.js';
|
|
18
|
+
export interface ThinkingCheckpointConfig {
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
window_ms?: number;
|
|
21
|
+
high_risk_tools?: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Checks if a tool call requires a recent deep thinking checkpoint.
|
|
25
|
+
*
|
|
26
|
+
* This enforces P-10 (Thinking OS Checkpoint) - high-risk operations must
|
|
27
|
+
* be preceded by deep reflection within the configured time window.
|
|
28
|
+
*
|
|
29
|
+
* @param event - The before_tool_call event
|
|
30
|
+
* @param config - Thinking checkpoint configuration from profile
|
|
31
|
+
* @param sessionId - Current session ID
|
|
32
|
+
* @param logger - Optional logger for info messages
|
|
33
|
+
* @returns Block result if thinking required, undefined otherwise
|
|
34
|
+
*/
|
|
35
|
+
export declare function checkThinkingCheckpoint(event: PluginHookBeforeToolCallEvent, config: ThinkingCheckpointConfig, sessionId: string | undefined, logger?: {
|
|
36
|
+
info?: (message: string) => void;
|
|
37
|
+
}): PluginHookBeforeToolCallResult | undefined;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thinking Checkpoint Module
|
|
3
|
+
*
|
|
4
|
+
* Enforces P-10 deep reflection requirement for high-risk tool operations.
|
|
5
|
+
*
|
|
6
|
+
* **Responsibilities:**
|
|
7
|
+
* - Check if high-risk tools have recent deep thinking (T-01 through T-10)
|
|
8
|
+
* - Block high-risk operations without preceding deep reflection
|
|
9
|
+
* - Configurable time window for thinking validity (default 5 minutes)
|
|
10
|
+
* - Provide clear guidance on required action (deep_reflect tool usage)
|
|
11
|
+
*
|
|
12
|
+
* **Configuration:**
|
|
13
|
+
* - Thinking checkpoint settings from profile.thinking_checkpoint
|
|
14
|
+
* - Window duration for thinking validity
|
|
15
|
+
* - High-risk tool list
|
|
16
|
+
*/
|
|
17
|
+
import { hasRecentThinking } from '../core/session-tracker.js';
|
|
18
|
+
import { THINKING_CHECKPOINT_WINDOW_MS, THINKING_CHECKPOINT_DEFAULT_HIGH_RISK_TOOLS } from '../config/index.js';
|
|
19
|
+
/**
|
|
20
|
+
* Checks if a tool call requires a recent deep thinking checkpoint.
|
|
21
|
+
*
|
|
22
|
+
* This enforces P-10 (Thinking OS Checkpoint) - high-risk operations must
|
|
23
|
+
* be preceded by deep reflection within the configured time window.
|
|
24
|
+
*
|
|
25
|
+
* @param event - The before_tool_call event
|
|
26
|
+
* @param config - Thinking checkpoint configuration from profile
|
|
27
|
+
* @param sessionId - Current session ID
|
|
28
|
+
* @param logger - Optional logger for info messages
|
|
29
|
+
* @returns Block result if thinking required, undefined otherwise
|
|
30
|
+
*/
|
|
31
|
+
export function checkThinkingCheckpoint(event, config, sessionId, logger) {
|
|
32
|
+
const enabled = config.enabled ?? false;
|
|
33
|
+
const windowMs = config.window_ms ?? THINKING_CHECKPOINT_WINDOW_MS;
|
|
34
|
+
const highRiskTools = config.high_risk_tools ?? [...THINKING_CHECKPOINT_DEFAULT_HIGH_RISK_TOOLS];
|
|
35
|
+
if (!enabled || !sessionId) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const isHighRisk = highRiskTools.includes(event.toolName);
|
|
39
|
+
if (!isHighRisk) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const hasThinking = hasRecentThinking(sessionId, windowMs);
|
|
43
|
+
if (!hasThinking) {
|
|
44
|
+
logger?.info?.(`[PD:THINKING_GATE] High-risk tool "${event.toolName}" called without recent deep thinking`);
|
|
45
|
+
return {
|
|
46
|
+
block: true,
|
|
47
|
+
blockReason: `[Thinking OS Checkpoint] 高风险操作 "${event.toolName}" 需要先进行深度思考。\n\n请先使用 deep_reflect 工具分析当前情况,然后再尝试此操作。\n\n这是强制性检查点,目的是确保决策质量。\n\n提示:调用 deep_reflect 后,${Math.round(windowMs / 60000)}分钟内的操作将自动放行。\n\n可在PROFILE.json中设置 thinking_checkpoint.enabled: false 来禁用此检查。`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import type { OpenClawPluginApi, OpenClawPluginHttpRouteParams } from '../openclaw-sdk.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create routes for Principles Console.
|
|
4
|
+
* Returns an array of routes:
|
|
5
|
+
* 1. Static files route (no auth required for HTML/CSS/JS)
|
|
6
|
+
* 2. API route (gateway auth required)
|
|
7
|
+
*/
|
|
8
|
+
export declare function createPrinciplesConsoleRoutes(api: OpenClawPluginApi): OpenClawPluginHttpRouteParams[];
|
|
2
9
|
export declare function createPrinciplesConsoleRoute(api: OpenClawPluginApi): OpenClawPluginHttpRouteParams;
|
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { ControlUiQueryService } from '../service/control-ui-query-service.js';
|
|
4
4
|
import { getEvolutionQueryService } from '../service/evolution-query-service.js';
|
|
5
5
|
import { TrajectoryRegistry } from '../core/trajectory.js';
|
|
6
|
+
import { getCentralDatabase } from '../service/central-database.js';
|
|
6
7
|
const ROUTE_PREFIX = '/plugins/principles';
|
|
7
8
|
const API_PREFIX = `${ROUTE_PREFIX}/api`;
|
|
8
9
|
const ASSETS_PREFIX = `${ROUTE_PREFIX}/assets`;
|
|
@@ -82,6 +83,11 @@ function createService(api) {
|
|
|
82
83
|
return new ControlUiQueryService(workspaceDir);
|
|
83
84
|
}
|
|
84
85
|
function handleApiRoute(api, pathname, req, res) {
|
|
86
|
+
// Check authentication for API routes
|
|
87
|
+
if (!validateGatewayAuth(req)) {
|
|
88
|
+
json(res, 401, { error: 'unauthorized', message: 'Valid Gateway token required.' });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
85
91
|
const service = createService(api);
|
|
86
92
|
const url = new URL(req.url || pathname, 'http://127.0.0.1');
|
|
87
93
|
const method = (req.method || 'GET').toUpperCase();
|
|
@@ -100,8 +106,164 @@ function handleApiRoute(api, pathname, req, res) {
|
|
|
100
106
|
service.dispose();
|
|
101
107
|
}
|
|
102
108
|
};
|
|
109
|
+
// Helper to parse and clamp days parameter
|
|
110
|
+
const parseDays = (param) => {
|
|
111
|
+
const value = param ? Number(param) : 30;
|
|
112
|
+
if (!Number.isFinite(value) || value < 1)
|
|
113
|
+
return 30;
|
|
114
|
+
return Math.min(365, Math.max(1, Math.floor(value)));
|
|
115
|
+
};
|
|
103
116
|
if (pathname === `${API_PREFIX}/overview` && method === 'GET') {
|
|
104
|
-
|
|
117
|
+
const days = parseDays(url.searchParams.get('days'));
|
|
118
|
+
return done(() => service.getOverview(days));
|
|
119
|
+
}
|
|
120
|
+
if (pathname === `${API_PREFIX}/central/overview` && method === 'GET') {
|
|
121
|
+
const days = parseDays(url.searchParams.get('days'));
|
|
122
|
+
return done(() => {
|
|
123
|
+
const centralDb = getCentralDatabase();
|
|
124
|
+
const stats = centralDb.getOverviewStats();
|
|
125
|
+
const trend = centralDb.getDailyTrend(days);
|
|
126
|
+
const regressions = centralDb.getTopRegressions(5);
|
|
127
|
+
const thinkingStats = centralDb.getThinkingModelStats();
|
|
128
|
+
const workspaces = centralDb.getWorkspaces();
|
|
129
|
+
return {
|
|
130
|
+
workspaceDir: 'central',
|
|
131
|
+
generatedAt: new Date().toISOString(),
|
|
132
|
+
dataFreshness: workspaces.length > 0 ? (workspaces[0].lastSync ?? null) : null,
|
|
133
|
+
dataSource: 'central_aggregated_db',
|
|
134
|
+
runtimeControlPlaneSource: 'all_workspaces',
|
|
135
|
+
summary: {
|
|
136
|
+
repeatErrorRate: stats.totalToolCalls > 0
|
|
137
|
+
? stats.totalFailures / stats.totalToolCalls
|
|
138
|
+
: 0,
|
|
139
|
+
userCorrectionRate: stats.totalToolCalls > 0
|
|
140
|
+
? stats.totalCorrections / stats.totalToolCalls
|
|
141
|
+
: 0,
|
|
142
|
+
pendingSamples: stats.pendingSamples,
|
|
143
|
+
approvedSamples: stats.approvedSamples,
|
|
144
|
+
thinkingCoverageRate: stats.totalToolCalls > 0
|
|
145
|
+
? stats.totalThinkingEvents / stats.totalToolCalls
|
|
146
|
+
: 0,
|
|
147
|
+
painEvents: stats.totalPainEvents,
|
|
148
|
+
principleEventCount: 0,
|
|
149
|
+
gateBlocks: 0,
|
|
150
|
+
taskOutcomes: 0,
|
|
151
|
+
},
|
|
152
|
+
dailyTrend: trend,
|
|
153
|
+
topRegressions: regressions,
|
|
154
|
+
sampleQueue: {
|
|
155
|
+
counters: {
|
|
156
|
+
pending: stats.pendingSamples,
|
|
157
|
+
approved: stats.approvedSamples,
|
|
158
|
+
rejected: stats.rejectedSamples,
|
|
159
|
+
},
|
|
160
|
+
preview: [],
|
|
161
|
+
},
|
|
162
|
+
thinkingSummary: {
|
|
163
|
+
activeModels: thinkingStats.activeModels,
|
|
164
|
+
dormantModels: thinkingStats.totalModels - thinkingStats.activeModels,
|
|
165
|
+
effectiveModels: thinkingStats.models.filter(m => m.coverageRate > 0.1).length,
|
|
166
|
+
coverageRate: stats.totalToolCalls > 0
|
|
167
|
+
? stats.totalThinkingEvents / stats.totalToolCalls
|
|
168
|
+
: 0,
|
|
169
|
+
},
|
|
170
|
+
centralInfo: {
|
|
171
|
+
workspaceCount: stats.workspaceCount,
|
|
172
|
+
enabledWorkspaceCount: stats.enabledWorkspaceCount,
|
|
173
|
+
workspaces: stats.workspaceNames,
|
|
174
|
+
enabledWorkspaces: stats.enabledWorkspaceNames,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (pathname === `${API_PREFIX}/central/sync` && method === 'POST') {
|
|
180
|
+
return done(() => {
|
|
181
|
+
const centralDb = getCentralDatabase();
|
|
182
|
+
const results = centralDb.syncEnabled();
|
|
183
|
+
const summary = {};
|
|
184
|
+
results.forEach((count, name) => {
|
|
185
|
+
summary[name] = count;
|
|
186
|
+
});
|
|
187
|
+
return { synced: summary, timestamp: new Date().toISOString() };
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (pathname === `${API_PREFIX}/central/workspaces` && method === 'GET') {
|
|
191
|
+
return done(() => {
|
|
192
|
+
const centralDb = getCentralDatabase();
|
|
193
|
+
const configs = centralDb.getWorkspaceConfigs();
|
|
194
|
+
const workspaces = centralDb.getWorkspaces();
|
|
195
|
+
return {
|
|
196
|
+
configs,
|
|
197
|
+
workspaces: workspaces.map(ws => ({
|
|
198
|
+
name: ws.name,
|
|
199
|
+
path: ws.path,
|
|
200
|
+
lastSync: ws.lastSync,
|
|
201
|
+
config: configs.find(c => c.workspaceName === ws.name) ?? null,
|
|
202
|
+
})),
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const workspaceConfigMatch = pathname.match(/^\/plugins\/principles\/api\/central\/workspaces\/([^/]+)$/);
|
|
207
|
+
if (workspaceConfigMatch && method === 'GET') {
|
|
208
|
+
return done(() => {
|
|
209
|
+
const centralDb = getCentralDatabase();
|
|
210
|
+
const workspaceName = decodeURIComponent(workspaceConfigMatch[1]);
|
|
211
|
+
const configs = centralDb.getWorkspaceConfigs();
|
|
212
|
+
const config = configs.find(c => c.workspaceName === workspaceName);
|
|
213
|
+
return config ?? { workspaceName, enabled: true, displayName: workspaceName, syncEnabled: true };
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (workspaceConfigMatch && method === 'PATCH') {
|
|
217
|
+
return (async () => {
|
|
218
|
+
try {
|
|
219
|
+
const body = await readJsonBody(req);
|
|
220
|
+
const centralDb = getCentralDatabase();
|
|
221
|
+
const workspaceName = decodeURIComponent(workspaceConfigMatch[1]);
|
|
222
|
+
centralDb.updateWorkspaceConfig(workspaceName, {
|
|
223
|
+
enabled: body.enabled,
|
|
224
|
+
displayName: body.displayName,
|
|
225
|
+
syncEnabled: body.syncEnabled,
|
|
226
|
+
});
|
|
227
|
+
const configs = centralDb.getWorkspaceConfigs();
|
|
228
|
+
json(res, 200, configs.find(c => c.workspaceName === workspaceName));
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error instanceof Error && error.message === 'invalid_json') {
|
|
233
|
+
json(res, 400, { error: 'bad_request', message: 'Request body must be valid JSON.' });
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
api.logger.warn(`[PD:ControlUI] Workspace config update failed: ${String(error)}`);
|
|
237
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
})();
|
|
241
|
+
}
|
|
242
|
+
if (pathname === `${API_PREFIX}/central/workspaces` && method === 'POST') {
|
|
243
|
+
return (async () => {
|
|
244
|
+
try {
|
|
245
|
+
const body = await readJsonBody(req);
|
|
246
|
+
const name = typeof body.name === 'string' ? body.name : '';
|
|
247
|
+
const workspacePath = typeof body.path === 'string' ? body.path : '';
|
|
248
|
+
if (!name || !workspacePath) {
|
|
249
|
+
json(res, 400, { error: 'bad_request', message: 'name and path are required.' });
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
const centralDb = getCentralDatabase();
|
|
253
|
+
centralDb.addCustomWorkspace(name, workspacePath);
|
|
254
|
+
json(res, 201, { success: true, workspace: name });
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (error instanceof Error && error.message === 'invalid_json') {
|
|
259
|
+
json(res, 400, { error: 'bad_request', message: 'Request body must be valid JSON.' });
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
api.logger.warn(`[PD:ControlUI] Add workspace failed: ${String(error)}`);
|
|
263
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
})();
|
|
105
267
|
}
|
|
106
268
|
if (pathname === `${API_PREFIX}/samples` && method === 'GET') {
|
|
107
269
|
return done(() => service.listSamples({
|
|
@@ -217,9 +379,10 @@ function handleApiRoute(api, pathname, req, res) {
|
|
|
217
379
|
});
|
|
218
380
|
}
|
|
219
381
|
if (pathname === `${API_PREFIX}/evolution/stats` && method === 'GET') {
|
|
382
|
+
const days = parseDays(url.searchParams.get('days'));
|
|
220
383
|
return done(() => {
|
|
221
384
|
const evoService = evolutionService();
|
|
222
|
-
return evoService.getStats();
|
|
385
|
+
return evoService.getStats(days);
|
|
223
386
|
});
|
|
224
387
|
}
|
|
225
388
|
const evolutionTraceMatch = pathname.match(/^\/plugins\/principles\/api\/evolution\/trace\/([^/]+)$/);
|
|
@@ -275,10 +438,93 @@ function handleApiRoute(api, pathname, req, res) {
|
|
|
275
438
|
json(res, 404, { error: 'not_found', message: 'Unknown Principles Console API route.' });
|
|
276
439
|
return true;
|
|
277
440
|
}
|
|
441
|
+
function getGatewayToken() {
|
|
442
|
+
try {
|
|
443
|
+
const configPath = path.join(process.env.HOME || '', '.openclaw', 'openclaw.json');
|
|
444
|
+
if (!fs.existsSync(configPath))
|
|
445
|
+
return null;
|
|
446
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
447
|
+
return config?.gateway?.auth?.token || null;
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function validateGatewayAuth(req) {
|
|
454
|
+
const gatewayToken = getGatewayToken();
|
|
455
|
+
if (!gatewayToken) {
|
|
456
|
+
// No token configured, allow all requests
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
const authHeader = req.headers?.['authorization'] || '';
|
|
460
|
+
const tokenMatch = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
461
|
+
const providedToken = tokenMatch?.[1];
|
|
462
|
+
return providedToken === gatewayToken;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Create routes for Principles Console.
|
|
466
|
+
* Returns an array of routes:
|
|
467
|
+
* 1. Static files route (no auth required for HTML/CSS/JS)
|
|
468
|
+
* 2. API route (gateway auth required)
|
|
469
|
+
*/
|
|
470
|
+
export function createPrinciplesConsoleRoutes(api) {
|
|
471
|
+
// Route 1: Static files (HTML, CSS, JS) - no auth check
|
|
472
|
+
const staticRoute = {
|
|
473
|
+
path: ROUTE_PREFIX,
|
|
474
|
+
auth: 'plugin',
|
|
475
|
+
match: 'prefix',
|
|
476
|
+
async handler(req, res) {
|
|
477
|
+
const url = new URL(req.url || ROUTE_PREFIX, 'http://127.0.0.1');
|
|
478
|
+
const pathname = url.pathname;
|
|
479
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
480
|
+
// Skip API routes - they'll be handled by the API route
|
|
481
|
+
if (pathname.startsWith(API_PREFIX)) {
|
|
482
|
+
return false; // Let the API route handle this
|
|
483
|
+
}
|
|
484
|
+
// Serve assets
|
|
485
|
+
if (pathname.startsWith(ASSETS_PREFIX)) {
|
|
486
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
487
|
+
text(res, 405, 'Method Not Allowed');
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
const assetPath = safeStaticPath(api.rootDir, pathname);
|
|
491
|
+
if (!assetPath || !serveFile(res, assetPath)) {
|
|
492
|
+
text(res, 404, 'Asset Not Found');
|
|
493
|
+
}
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
// Serve index.html for the main route
|
|
497
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
498
|
+
text(res, 405, 'Method Not Allowed');
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
const indexPath = path.join(api.rootDir, 'dist', 'web', 'index.html');
|
|
502
|
+
if (!serveFile(res, indexPath)) {
|
|
503
|
+
text(res, 503, 'Principles Console UI is not built yet.');
|
|
504
|
+
}
|
|
505
|
+
return true;
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
// Route 2: API endpoints - gateway auth required
|
|
509
|
+
const apiRoute = {
|
|
510
|
+
path: API_PREFIX,
|
|
511
|
+
auth: 'gateway',
|
|
512
|
+
match: 'prefix',
|
|
513
|
+
async handler(req, res) {
|
|
514
|
+
const url = new URL(req.url || API_PREFIX, 'http://127.0.0.1');
|
|
515
|
+
const pathname = url.pathname;
|
|
516
|
+
return handleApiRoute(api, pathname, req, res);
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
return [staticRoute, apiRoute];
|
|
520
|
+
}
|
|
521
|
+
// Legacy export for backwards compatibility
|
|
278
522
|
export function createPrinciplesConsoleRoute(api) {
|
|
523
|
+
const routes = createPrinciplesConsoleRoutes(api);
|
|
524
|
+
// Return the combined behavior - this will be called from index.ts
|
|
279
525
|
return {
|
|
280
526
|
path: ROUTE_PREFIX,
|
|
281
|
-
auth: '
|
|
527
|
+
auth: 'plugin',
|
|
282
528
|
match: 'prefix',
|
|
283
529
|
async handler(req, res) {
|
|
284
530
|
const url = new URL(req.url || ROUTE_PREFIX, 'http://127.0.0.1');
|
|
@@ -287,9 +533,15 @@ export function createPrinciplesConsoleRoute(api) {
|
|
|
287
533
|
if (!pathname.startsWith(ROUTE_PREFIX)) {
|
|
288
534
|
return false;
|
|
289
535
|
}
|
|
536
|
+
// For API routes, check auth manually
|
|
290
537
|
if (pathname.startsWith(API_PREFIX)) {
|
|
538
|
+
if (!validateGatewayAuth(req)) {
|
|
539
|
+
json(res, 401, { error: 'unauthorized', message: 'Valid Gateway token required.' });
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
291
542
|
return handleApiRoute(api, pathname, req, res);
|
|
292
543
|
}
|
|
544
|
+
// Static files - no auth required
|
|
293
545
|
if (pathname.startsWith(ASSETS_PREFIX)) {
|
|
294
546
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
295
547
|
text(res, 405, 'Method Not Allowed');
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,6 @@ import { ensureWorkspaceTemplates } from './core/init.js';
|
|
|
26
26
|
import { migrateDirectoryStructure } from './core/migration.js';
|
|
27
27
|
import { SystemLogger } from './core/system-logger.js';
|
|
28
28
|
import { createDeepReflectTool } from './tools/deep-reflect.js';
|
|
29
|
-
import { createAgentSpawnTool } from './tools/agent-spawn.js';
|
|
30
29
|
import { PathResolver } from './core/path-resolver.js';
|
|
31
30
|
import { createPrinciplesConsoleRoute } from './http/principles-console-route.js';
|
|
32
31
|
// Track initialization to avoid repeated calls
|
|
@@ -476,7 +475,6 @@ const plugin = {
|
|
|
476
475
|
}
|
|
477
476
|
});
|
|
478
477
|
api.registerTool(createDeepReflectTool(api));
|
|
479
|
-
api.registerTool(createAgentSpawnTool(api));
|
|
480
478
|
}
|
|
481
479
|
};
|
|
482
480
|
export default plugin;
|