principles-disciple 1.7.3 → 1.7.5
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 +4 -2
- package/dist/commands/focus.js +30 -155
- package/dist/constants/diagnostician.d.ts +16 -0
- package/dist/constants/diagnostician.js +60 -0
- package/dist/constants/tools.d.ts +2 -2
- package/dist/constants/tools.js +1 -1
- package/dist/core/config.d.ts +23 -0
- package/dist/core/config.js +26 -1
- package/dist/core/evolution-engine.js +1 -1
- package/dist/core/evolution-logger.d.ts +137 -0
- package/dist/core/evolution-logger.js +256 -0
- package/dist/core/evolution-reducer.d.ts +23 -0
- package/dist/core/evolution-reducer.js +73 -29
- package/dist/core/evolution-types.d.ts +6 -0
- package/dist/core/focus-history.d.ts +145 -0
- package/dist/core/focus-history.js +919 -0
- package/dist/core/init.js +24 -0
- package/dist/core/profile.js +1 -1
- package/dist/core/risk-calculator.d.ts +15 -0
- package/dist/core/risk-calculator.js +48 -0
- package/dist/core/trajectory.d.ts +73 -0
- package/dist/core/trajectory.js +206 -0
- package/dist/hooks/gate.js +130 -20
- package/dist/hooks/lifecycle.js +104 -0
- package/dist/hooks/pain.js +31 -0
- package/dist/hooks/prompt.js +136 -38
- package/dist/hooks/subagent.d.ts +1 -0
- package/dist/hooks/subagent.js +200 -18
- package/dist/http/principles-console-route.d.ts +7 -0
- package/dist/http/principles-console-route.js +301 -1
- package/dist/index.js +0 -2
- package/dist/service/central-database.d.ts +104 -0
- package/dist/service/central-database.js +648 -0
- package/dist/service/control-ui-query-service.d.ts +2 -0
- package/dist/service/control-ui-query-service.js +4 -0
- package/dist/service/empathy-observer-manager.d.ts +8 -0
- package/dist/service/empathy-observer-manager.js +40 -0
- package/dist/service/evolution-query-service.d.ts +155 -0
- package/dist/service/evolution-query-service.js +258 -0
- package/dist/service/evolution-worker.d.ts +4 -0
- package/dist/service/evolution-worker.js +185 -63
- package/dist/service/phase3-input-filter.d.ts +37 -0
- package/dist/service/phase3-input-filter.js +106 -0
- package/dist/service/runtime-summary-service.d.ts +15 -0
- package/dist/service/runtime-summary-service.js +111 -23
- package/dist/tools/deep-reflect.js +8 -2
- package/dist/utils/subagent-probe.d.ts +34 -0
- package/dist/utils/subagent-probe.js +81 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -4
- package/templates/langs/en/core/AGENTS.md +15 -3
- package/templates/langs/en/core/BOOTSTRAP.md +24 -1
- package/templates/langs/en/core/TOOLS.md +9 -0
- package/templates/langs/zh/core/AGENTS.md +15 -3
- package/templates/langs/zh/core/BOOTSTRAP.md +24 -1
- package/templates/langs/zh/core/TOOLS.md +9 -0
- package/templates/langs/zh/skills/pd-auditor/SKILL.md +61 -0
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +287 -0
- package/templates/langs/zh/skills/pd-explorer/SKILL.md +65 -0
- package/templates/langs/zh/skills/pd-implementer/SKILL.md +68 -0
- package/templates/langs/zh/skills/pd-planner/SKILL.md +65 -0
- package/templates/langs/zh/skills/pd-reporter/SKILL.md +78 -0
- package/templates/langs/zh/skills/pd-reviewer/SKILL.md +66 -0
- 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 -445
package/dist/core/init.js
CHANGED
|
@@ -11,6 +11,24 @@ const DEFAULT_PROFILE = {
|
|
|
11
11
|
version: "1.0.0",
|
|
12
12
|
contextInjection: defaultContextConfig
|
|
13
13
|
};
|
|
14
|
+
const CORE_GUIDANCE_VERSION = 'pd-core-guidance-v2';
|
|
15
|
+
const CORE_GUIDANCE_FILES = new Set(['AGENTS.md', 'TOOLS.md']);
|
|
16
|
+
function getCoreGuidanceVersionMarker() {
|
|
17
|
+
return `pd-core-guidance-version: ${CORE_GUIDANCE_VERSION}`;
|
|
18
|
+
}
|
|
19
|
+
function hasOutdatedCoreGuidance(file, content) {
|
|
20
|
+
if (!CORE_GUIDANCE_FILES.has(file))
|
|
21
|
+
return false;
|
|
22
|
+
if (content.includes(getCoreGuidanceVersionMarker()))
|
|
23
|
+
return false;
|
|
24
|
+
if (content.includes('pd_spawn_agent'))
|
|
25
|
+
return true;
|
|
26
|
+
if (!content.includes('subagents'))
|
|
27
|
+
return true;
|
|
28
|
+
if (!content.includes('sessions_spawn'))
|
|
29
|
+
return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
14
32
|
/**
|
|
15
33
|
* Ensures that the workspace has the necessary template files for Principles Disciple.
|
|
16
34
|
* This function flattens 'core' templates to the root so OpenClaw can find them.
|
|
@@ -45,6 +63,12 @@ export function ensureWorkspaceTemplates(api, workspaceDir, language = 'en') {
|
|
|
45
63
|
fs.copyFileSync(srcPath, destPath);
|
|
46
64
|
api.logger.info(`[PD] Initialized core file: ${file}`);
|
|
47
65
|
}
|
|
66
|
+
else if (CORE_GUIDANCE_FILES.has(file)) {
|
|
67
|
+
const existingContent = fs.readFileSync(destPath, 'utf8');
|
|
68
|
+
if (hasOutdatedCoreGuidance(file, existingContent)) {
|
|
69
|
+
api.logger.warn(`[PD] Outdated core guidance detected in ${file}. Review the latest template guidance for peer sessions, subagents, and sessions_spawn routing.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
48
72
|
}
|
|
49
73
|
}
|
|
50
74
|
// 3. Copy pain memory seed files
|
package/dist/core/profile.js
CHANGED
|
@@ -51,7 +51,7 @@ export const PROFILE_DEFAULTS = {
|
|
|
51
51
|
thinking_checkpoint: {
|
|
52
52
|
enabled: false, // Default OFF to avoid blocking new users
|
|
53
53
|
window_ms: 5 * 60 * 1000, // 5 minute window
|
|
54
|
-
high_risk_tools: ['run_shell_command', 'delete_file', 'move_file'
|
|
54
|
+
high_risk_tools: ['run_shell_command', 'delete_file', 'move_file'],
|
|
55
55
|
},
|
|
56
56
|
custom_guards: [],
|
|
57
57
|
};
|
|
@@ -5,3 +5,18 @@ export interface FileModification {
|
|
|
5
5
|
}
|
|
6
6
|
export declare function estimateLineChanges(modification: FileModification): number;
|
|
7
7
|
export declare function assessRiskLevel(filePath: string, modification: FileModification, riskPaths: string[]): RiskLevel;
|
|
8
|
+
/**
|
|
9
|
+
* Get the total line count of a target file.
|
|
10
|
+
* @param absoluteFilePath - Absolute path to the file
|
|
11
|
+
* @returns File line count, or null if file doesn't exist or can't be read
|
|
12
|
+
*/
|
|
13
|
+
export declare function getTargetFileLineCount(absoluteFilePath: string): number | null;
|
|
14
|
+
/**
|
|
15
|
+
* Calculate the effective line limit based on percentage of target file.
|
|
16
|
+
* @param targetLineCount - Total lines in target file
|
|
17
|
+
* @param percentage - Allowed percentage (0-100)
|
|
18
|
+
* @param minLines - Absolute minimum threshold
|
|
19
|
+
* @param maxLines - Optional upper bound to prevent misconfiguration
|
|
20
|
+
* @returns Maximum allowed lines (at least minLines, at most maxLines if provided)
|
|
21
|
+
*/
|
|
22
|
+
export declare function calculatePercentageThreshold(targetLineCount: number, percentage: number, minLines: number, maxLines?: number): number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
1
2
|
import { isRisky } from '../utils/io.js';
|
|
2
3
|
export function estimateLineChanges(modification) {
|
|
3
4
|
const { toolName, params } = modification;
|
|
@@ -37,3 +38,50 @@ export function assessRiskLevel(filePath, modification, riskPaths) {
|
|
|
37
38
|
return 'LOW';
|
|
38
39
|
}
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Get the total line count of a target file.
|
|
43
|
+
* @param absoluteFilePath - Absolute path to the file
|
|
44
|
+
* @returns File line count, or null if file doesn't exist or can't be read
|
|
45
|
+
*/
|
|
46
|
+
export function getTargetFileLineCount(absoluteFilePath) {
|
|
47
|
+
try {
|
|
48
|
+
if (!fs.existsSync(absoluteFilePath)) {
|
|
49
|
+
return null; // File genuinely doesn't exist
|
|
50
|
+
}
|
|
51
|
+
const stats = fs.statSync(absoluteFilePath);
|
|
52
|
+
if (!stats.isFile()) {
|
|
53
|
+
return null; // Not a regular file (directory, device, etc.)
|
|
54
|
+
}
|
|
55
|
+
const content = fs.readFileSync(absoluteFilePath, 'utf-8');
|
|
56
|
+
return content.split('\n').length;
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
// Log error before falling back to null - this is intentional for security gates
|
|
60
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
61
|
+
const errorCode = e.code;
|
|
62
|
+
console.error(`[PD:RISK_CALC] Failed to read file for line count: ${absoluteFilePath}`, {
|
|
63
|
+
code: errorCode,
|
|
64
|
+
message: error.message,
|
|
65
|
+
});
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Calculate the effective line limit based on percentage of target file.
|
|
71
|
+
* @param targetLineCount - Total lines in target file
|
|
72
|
+
* @param percentage - Allowed percentage (0-100)
|
|
73
|
+
* @param minLines - Absolute minimum threshold
|
|
74
|
+
* @param maxLines - Optional upper bound to prevent misconfiguration
|
|
75
|
+
* @returns Maximum allowed lines (at least minLines, at most maxLines if provided)
|
|
76
|
+
*/
|
|
77
|
+
export function calculatePercentageThreshold(targetLineCount, percentage, minLines, maxLines) {
|
|
78
|
+
// Clamp percentage to valid range [0, 100]
|
|
79
|
+
const clampedPercentage = Math.max(0, Math.min(100, percentage));
|
|
80
|
+
const calculated = Math.round(targetLineCount * (clampedPercentage / 100));
|
|
81
|
+
let effectiveLimit = Math.max(calculated, minLines);
|
|
82
|
+
// Apply optional upper bound
|
|
83
|
+
if (maxLines !== undefined && maxLines > 0) {
|
|
84
|
+
effectiveLimit = Math.min(effectiveLimit, maxLines);
|
|
85
|
+
}
|
|
86
|
+
return effectiveLimit;
|
|
87
|
+
}
|
|
@@ -89,6 +89,63 @@ export interface TrajectorySessionInput {
|
|
|
89
89
|
sessionId: string;
|
|
90
90
|
startedAt?: string;
|
|
91
91
|
}
|
|
92
|
+
export interface EvolutionTaskInput {
|
|
93
|
+
taskId: string;
|
|
94
|
+
traceId: string;
|
|
95
|
+
source: string;
|
|
96
|
+
reason?: string | null;
|
|
97
|
+
score?: number;
|
|
98
|
+
status?: string;
|
|
99
|
+
enqueuedAt?: string | null;
|
|
100
|
+
startedAt?: string | null;
|
|
101
|
+
completedAt?: string | null;
|
|
102
|
+
resolution?: string | null;
|
|
103
|
+
createdAt?: string;
|
|
104
|
+
updatedAt?: string;
|
|
105
|
+
}
|
|
106
|
+
export interface EvolutionEventInput {
|
|
107
|
+
traceId: string;
|
|
108
|
+
taskId?: string | null;
|
|
109
|
+
stage: string;
|
|
110
|
+
level?: string;
|
|
111
|
+
message: string;
|
|
112
|
+
summary?: string | null;
|
|
113
|
+
metadata?: unknown;
|
|
114
|
+
createdAt?: string;
|
|
115
|
+
}
|
|
116
|
+
export interface EvolutionTaskRecord {
|
|
117
|
+
id: number;
|
|
118
|
+
taskId: string;
|
|
119
|
+
traceId: string;
|
|
120
|
+
source: string;
|
|
121
|
+
reason: string | null;
|
|
122
|
+
score: number;
|
|
123
|
+
status: string;
|
|
124
|
+
enqueuedAt: string | null;
|
|
125
|
+
startedAt: string | null;
|
|
126
|
+
completedAt: string | null;
|
|
127
|
+
resolution: string | null;
|
|
128
|
+
createdAt: string;
|
|
129
|
+
updatedAt: string;
|
|
130
|
+
}
|
|
131
|
+
export interface EvolutionEventRecord {
|
|
132
|
+
id: number;
|
|
133
|
+
traceId: string;
|
|
134
|
+
taskId: string | null;
|
|
135
|
+
stage: string;
|
|
136
|
+
level: string;
|
|
137
|
+
message: string;
|
|
138
|
+
summary: string | null;
|
|
139
|
+
metadata: Record<string, unknown>;
|
|
140
|
+
createdAt: string;
|
|
141
|
+
}
|
|
142
|
+
export interface EvolutionTaskFilters {
|
|
143
|
+
status?: string;
|
|
144
|
+
dateFrom?: string;
|
|
145
|
+
dateTo?: string;
|
|
146
|
+
limit?: number;
|
|
147
|
+
offset?: number;
|
|
148
|
+
}
|
|
92
149
|
export interface AssistantTurnRecord {
|
|
93
150
|
id: number;
|
|
94
151
|
sessionId: string;
|
|
@@ -145,6 +202,22 @@ export declare class TrajectoryDatabase {
|
|
|
145
202
|
recordTrustChange(input: TrajectoryTrustChangeInput): void;
|
|
146
203
|
recordPrincipleEvent(input: TrajectoryPrincipleEventInput): void;
|
|
147
204
|
recordTaskOutcome(input: TrajectoryTaskOutcomeInput): void;
|
|
205
|
+
recordEvolutionTask(input: EvolutionTaskInput): void;
|
|
206
|
+
updateEvolutionTask(taskId: string, updates: Partial<Omit<EvolutionTaskInput, 'taskId' | 'traceId' | 'source'>>): void;
|
|
207
|
+
recordEvolutionEvent(input: EvolutionEventInput): void;
|
|
208
|
+
listEvolutionTasks(filters?: EvolutionTaskFilters): EvolutionTaskRecord[];
|
|
209
|
+
listEvolutionEvents(traceId?: string, filters?: {
|
|
210
|
+
limit?: number;
|
|
211
|
+
offset?: number;
|
|
212
|
+
}): EvolutionEventRecord[];
|
|
213
|
+
getEvolutionTaskByTraceId(traceId: string): EvolutionTaskRecord | null;
|
|
214
|
+
getEvolutionStats(): {
|
|
215
|
+
total: number;
|
|
216
|
+
pending: number;
|
|
217
|
+
inProgress: number;
|
|
218
|
+
completed: number;
|
|
219
|
+
failed: number;
|
|
220
|
+
};
|
|
148
221
|
listAssistantTurns(sessionId: string): AssistantTurnRecord[];
|
|
149
222
|
listCorrectionSamples(status?: CorrectionSampleReviewStatus): CorrectionSampleRecord[];
|
|
150
223
|
reviewCorrectionSample(sampleId: string, status: Exclude<CorrectionSampleReviewStatus, 'pending'>, note?: string): CorrectionSampleRecord;
|
package/dist/core/trajectory.js
CHANGED
|
@@ -161,6 +161,181 @@ export class TrajectoryDatabase {
|
|
|
161
161
|
`).run(input.sessionId, input.taskId ?? null, input.outcome, input.summary ?? null, safeJson(input.principleIdsJson), input.createdAt ?? nowIso());
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
|
+
recordEvolutionTask(input) {
|
|
165
|
+
const now = nowIso();
|
|
166
|
+
this.withWrite(() => {
|
|
167
|
+
this.db.prepare(`
|
|
168
|
+
INSERT INTO evolution_tasks (
|
|
169
|
+
task_id, trace_id, source, reason, score, status,
|
|
170
|
+
enqueued_at, started_at, completed_at, resolution, created_at, updated_at
|
|
171
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
172
|
+
ON CONFLICT(task_id) DO UPDATE SET
|
|
173
|
+
status = excluded.status,
|
|
174
|
+
started_at = excluded.started_at,
|
|
175
|
+
completed_at = excluded.completed_at,
|
|
176
|
+
resolution = excluded.resolution,
|
|
177
|
+
updated_at = excluded.updated_at
|
|
178
|
+
`).run(input.taskId, input.traceId, input.source, input.reason ?? null, input.score ?? 0, input.status ?? 'pending', input.enqueuedAt ?? null, input.startedAt ?? null, input.completedAt ?? null, input.resolution ?? null, input.createdAt ?? now, input.updatedAt ?? now);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
updateEvolutionTask(taskId, updates) {
|
|
182
|
+
const now = nowIso();
|
|
183
|
+
this.withWrite(() => {
|
|
184
|
+
const setClauses = ['updated_at = ?'];
|
|
185
|
+
const values = [now];
|
|
186
|
+
if (updates.status !== undefined) {
|
|
187
|
+
setClauses.push('status = ?');
|
|
188
|
+
values.push(updates.status);
|
|
189
|
+
}
|
|
190
|
+
if (updates.startedAt !== undefined) {
|
|
191
|
+
setClauses.push('started_at = ?');
|
|
192
|
+
values.push(updates.startedAt);
|
|
193
|
+
}
|
|
194
|
+
if (updates.completedAt !== undefined) {
|
|
195
|
+
setClauses.push('completed_at = ?');
|
|
196
|
+
values.push(updates.completedAt);
|
|
197
|
+
}
|
|
198
|
+
if (updates.resolution !== undefined) {
|
|
199
|
+
setClauses.push('resolution = ?');
|
|
200
|
+
values.push(updates.resolution);
|
|
201
|
+
}
|
|
202
|
+
if (updates.score !== undefined) {
|
|
203
|
+
setClauses.push('score = ?');
|
|
204
|
+
values.push(updates.score);
|
|
205
|
+
}
|
|
206
|
+
values.push(taskId);
|
|
207
|
+
this.db.prepare(`
|
|
208
|
+
UPDATE evolution_tasks SET ${setClauses.join(', ')} WHERE task_id = ?
|
|
209
|
+
`).run(...values);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
recordEvolutionEvent(input) {
|
|
213
|
+
this.withWrite(() => {
|
|
214
|
+
this.db.prepare(`
|
|
215
|
+
INSERT INTO evolution_events (trace_id, task_id, stage, level, message, summary, metadata_json, created_at)
|
|
216
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
217
|
+
`).run(input.traceId, input.taskId ?? null, input.stage, input.level ?? 'info', input.message, input.summary ?? null, safeJson(input.metadata), input.createdAt ?? nowIso());
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
listEvolutionTasks(filters = {}) {
|
|
221
|
+
const conditions = [];
|
|
222
|
+
const values = [];
|
|
223
|
+
if (filters.status) {
|
|
224
|
+
conditions.push('status = ?');
|
|
225
|
+
values.push(filters.status);
|
|
226
|
+
}
|
|
227
|
+
if (filters.dateFrom) {
|
|
228
|
+
conditions.push('created_at >= ?');
|
|
229
|
+
values.push(filters.dateFrom);
|
|
230
|
+
}
|
|
231
|
+
if (filters.dateTo) {
|
|
232
|
+
conditions.push('created_at <= ?');
|
|
233
|
+
values.push(filters.dateTo);
|
|
234
|
+
}
|
|
235
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
236
|
+
const limit = filters.limit ?? 50;
|
|
237
|
+
const offset = filters.offset ?? 0;
|
|
238
|
+
const rows = this.db.prepare(`
|
|
239
|
+
SELECT id, task_id, trace_id, source, reason, score, status,
|
|
240
|
+
enqueued_at, started_at, completed_at, resolution, created_at, updated_at
|
|
241
|
+
FROM evolution_tasks
|
|
242
|
+
${whereClause}
|
|
243
|
+
ORDER BY created_at DESC
|
|
244
|
+
LIMIT ? OFFSET ?
|
|
245
|
+
`).all(...values, limit, offset);
|
|
246
|
+
return rows.map((row) => ({
|
|
247
|
+
id: Number(row.id),
|
|
248
|
+
taskId: String(row.task_id),
|
|
249
|
+
traceId: String(row.trace_id),
|
|
250
|
+
source: String(row.source),
|
|
251
|
+
reason: row.reason ? String(row.reason) : null,
|
|
252
|
+
score: Number(row.score ?? 0),
|
|
253
|
+
status: String(row.status),
|
|
254
|
+
enqueuedAt: row.enqueued_at ? String(row.enqueued_at) : null,
|
|
255
|
+
startedAt: row.started_at ? String(row.started_at) : null,
|
|
256
|
+
completedAt: row.completed_at ? String(row.completed_at) : null,
|
|
257
|
+
resolution: row.resolution ? String(row.resolution) : null,
|
|
258
|
+
createdAt: String(row.created_at),
|
|
259
|
+
updatedAt: String(row.updated_at),
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
listEvolutionEvents(traceId, filters = {}) {
|
|
263
|
+
const limit = filters.limit ?? 100;
|
|
264
|
+
const offset = filters.offset ?? 0;
|
|
265
|
+
let rows;
|
|
266
|
+
if (traceId) {
|
|
267
|
+
rows = this.db.prepare(`
|
|
268
|
+
SELECT id, trace_id, task_id, stage, level, message, summary, metadata_json, created_at
|
|
269
|
+
FROM evolution_events
|
|
270
|
+
WHERE trace_id = ?
|
|
271
|
+
ORDER BY created_at ASC
|
|
272
|
+
LIMIT ? OFFSET ?
|
|
273
|
+
`).all(traceId, limit, offset);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
rows = this.db.prepare(`
|
|
277
|
+
SELECT id, trace_id, task_id, stage, level, message, summary, metadata_json, created_at
|
|
278
|
+
FROM evolution_events
|
|
279
|
+
ORDER BY created_at DESC
|
|
280
|
+
LIMIT ? OFFSET ?
|
|
281
|
+
`).all(limit, offset);
|
|
282
|
+
}
|
|
283
|
+
return rows.map((row) => ({
|
|
284
|
+
id: Number(row.id),
|
|
285
|
+
traceId: String(row.trace_id),
|
|
286
|
+
taskId: row.task_id ? String(row.task_id) : null,
|
|
287
|
+
stage: String(row.stage),
|
|
288
|
+
level: String(row.level ?? 'info'),
|
|
289
|
+
message: String(row.message),
|
|
290
|
+
summary: row.summary ? String(row.summary) : null,
|
|
291
|
+
metadata: JSON.parse(String(row.metadata_json ?? '{}')),
|
|
292
|
+
createdAt: String(row.created_at),
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
getEvolutionTaskByTraceId(traceId) {
|
|
296
|
+
const row = this.db.prepare(`
|
|
297
|
+
SELECT id, task_id, trace_id, source, reason, score, status,
|
|
298
|
+
enqueued_at, started_at, completed_at, resolution, created_at, updated_at
|
|
299
|
+
FROM evolution_tasks
|
|
300
|
+
WHERE trace_id = ?
|
|
301
|
+
LIMIT 1
|
|
302
|
+
`).get(traceId);
|
|
303
|
+
if (!row)
|
|
304
|
+
return null;
|
|
305
|
+
return {
|
|
306
|
+
id: Number(row.id),
|
|
307
|
+
taskId: String(row.task_id),
|
|
308
|
+
traceId: String(row.trace_id),
|
|
309
|
+
source: String(row.source),
|
|
310
|
+
reason: row.reason ? String(row.reason) : null,
|
|
311
|
+
score: Number(row.score ?? 0),
|
|
312
|
+
status: String(row.status),
|
|
313
|
+
enqueuedAt: row.enqueued_at ? String(row.enqueued_at) : null,
|
|
314
|
+
startedAt: row.started_at ? String(row.started_at) : null,
|
|
315
|
+
completedAt: row.completed_at ? String(row.completed_at) : null,
|
|
316
|
+
resolution: row.resolution ? String(row.resolution) : null,
|
|
317
|
+
createdAt: String(row.created_at),
|
|
318
|
+
updatedAt: String(row.updated_at),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
getEvolutionStats() {
|
|
322
|
+
const rows = this.db.prepare(`
|
|
323
|
+
SELECT status, COUNT(*) as count FROM evolution_tasks GROUP BY status
|
|
324
|
+
`).all();
|
|
325
|
+
const stats = { total: 0, pending: 0, inProgress: 0, completed: 0, failed: 0 };
|
|
326
|
+
for (const row of rows) {
|
|
327
|
+
stats.total += row.count;
|
|
328
|
+
if (row.status === 'pending')
|
|
329
|
+
stats.pending = row.count;
|
|
330
|
+
else if (row.status === 'in_progress')
|
|
331
|
+
stats.inProgress = row.count;
|
|
332
|
+
else if (row.status === 'completed')
|
|
333
|
+
stats.completed = row.count;
|
|
334
|
+
else if (row.status === 'failed')
|
|
335
|
+
stats.failed = row.count;
|
|
336
|
+
}
|
|
337
|
+
return stats;
|
|
338
|
+
}
|
|
164
339
|
listAssistantTurns(sessionId) {
|
|
165
340
|
const rows = this.db.prepare(`
|
|
166
341
|
SELECT id, session_id, run_id, provider, model, raw_text, sanitized_text, blob_ref, created_at
|
|
@@ -457,6 +632,32 @@ export class TrajectoryDatabase {
|
|
|
457
632
|
row_count INTEGER NOT NULL,
|
|
458
633
|
created_at TEXT NOT NULL
|
|
459
634
|
);
|
|
635
|
+
CREATE TABLE IF NOT EXISTS evolution_tasks (
|
|
636
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
637
|
+
task_id TEXT UNIQUE NOT NULL,
|
|
638
|
+
trace_id TEXT NOT NULL,
|
|
639
|
+
source TEXT NOT NULL,
|
|
640
|
+
reason TEXT,
|
|
641
|
+
score INTEGER DEFAULT 0,
|
|
642
|
+
status TEXT DEFAULT 'pending',
|
|
643
|
+
enqueued_at TEXT,
|
|
644
|
+
started_at TEXT,
|
|
645
|
+
completed_at TEXT,
|
|
646
|
+
resolution TEXT,
|
|
647
|
+
created_at TEXT NOT NULL,
|
|
648
|
+
updated_at TEXT NOT NULL
|
|
649
|
+
);
|
|
650
|
+
CREATE TABLE IF NOT EXISTS evolution_events (
|
|
651
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
652
|
+
trace_id TEXT NOT NULL,
|
|
653
|
+
task_id TEXT,
|
|
654
|
+
stage TEXT NOT NULL,
|
|
655
|
+
level TEXT DEFAULT 'info',
|
|
656
|
+
message TEXT NOT NULL,
|
|
657
|
+
summary TEXT,
|
|
658
|
+
metadata_json TEXT,
|
|
659
|
+
created_at TEXT NOT NULL
|
|
660
|
+
);
|
|
460
661
|
CREATE VIEW IF NOT EXISTS v_error_clusters AS
|
|
461
662
|
SELECT tool_name, COALESCE(error_type, 'unknown') AS error_type, COUNT(*) AS occurrences
|
|
462
663
|
FROM tool_calls
|
|
@@ -480,6 +681,11 @@ export class TrajectoryDatabase {
|
|
|
480
681
|
CREATE INDEX IF NOT EXISTS idx_tool_calls_created_at ON tool_calls(created_at);
|
|
481
682
|
CREATE INDEX IF NOT EXISTS idx_pain_events_session_id ON pain_events(session_id);
|
|
482
683
|
CREATE INDEX IF NOT EXISTS idx_correction_samples_review_status ON correction_samples(review_status);
|
|
684
|
+
CREATE INDEX IF NOT EXISTS idx_evolution_tasks_trace_id ON evolution_tasks(trace_id);
|
|
685
|
+
CREATE INDEX IF NOT EXISTS idx_evolution_tasks_status ON evolution_tasks(status);
|
|
686
|
+
CREATE INDEX IF NOT EXISTS idx_evolution_tasks_created_at ON evolution_tasks(created_at);
|
|
687
|
+
CREATE INDEX IF NOT EXISTS idx_evolution_events_trace_id ON evolution_events(trace_id);
|
|
688
|
+
CREATE INDEX IF NOT EXISTS idx_evolution_events_created_at ON evolution_events(created_at);
|
|
483
689
|
`);
|
|
484
690
|
const row = this.db.prepare('SELECT version FROM schema_version LIMIT 1').get();
|
|
485
691
|
this.migrateSchema(row?.version);
|
package/dist/hooks/gate.js
CHANGED
|
@@ -4,16 +4,18 @@ import { isRisky, normalizePath, planStatus as getPlanStatus } from '../utils/io
|
|
|
4
4
|
import { matchesAnyPattern } from '../utils/glob-match.js';
|
|
5
5
|
import { normalizeProfile } from '../core/profile.js';
|
|
6
6
|
import { trackBlock, hasRecentThinking, getSession } from '../core/session-tracker.js';
|
|
7
|
-
import { assessRiskLevel, estimateLineChanges } from '../core/risk-calculator.js';
|
|
7
|
+
import { assessRiskLevel, estimateLineChanges, getTargetFileLineCount, calculatePercentageThreshold } from '../core/risk-calculator.js';
|
|
8
8
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
9
9
|
import { checkEvolutionGate } from '../core/evolution-engine.js';
|
|
10
10
|
import { EventLogService } from '../core/event-log.js';
|
|
11
11
|
import { AGENT_TOOLS, BASH_TOOLS_SET, HIGH_RISK_TOOLS, LOW_RISK_WRITE_TOOLS, WRITE_TOOLS, } from '../constants/tools.js';
|
|
12
|
+
const TRAJECTORY_GATE_BLOCK_RETRY_DELAY_MS = 250;
|
|
13
|
+
const TRAJECTORY_GATE_BLOCK_MAX_RETRIES = 3;
|
|
12
14
|
// ═══ GFI Gate Tool Tiers ═══
|
|
13
15
|
// TIER 0: 只读工具 - 永不拦截
|
|
14
16
|
// TIER 1: 低风险修改 - GFI >= low_risk_block 时拦截
|
|
15
|
-
// 注意:
|
|
16
|
-
//
|
|
17
|
+
// 注意:sessions_spawn、task 是 Agent 派生工具,常规阈值不拦截
|
|
18
|
+
// 但极高 GFI (>=90) 时仍会拦截,防止极端情况下失控
|
|
17
19
|
// TIER 2: 高风险操作 - GFI >= high_risk_block 时拦截
|
|
18
20
|
// TIER 3: Bash 命令 - 根据内容判断
|
|
19
21
|
/**
|
|
@@ -132,7 +134,7 @@ export function handleBeforeToolCall(event, ctx) {
|
|
|
132
134
|
thinking_checkpoint: {
|
|
133
135
|
enabled: false, // Default OFF
|
|
134
136
|
window_ms: 5 * 60 * 1000,
|
|
135
|
-
high_risk_tools: ['run_shell_command', 'delete_file', 'move_file'
|
|
137
|
+
high_risk_tools: ['run_shell_command', 'delete_file', 'move_file'],
|
|
136
138
|
}
|
|
137
139
|
};
|
|
138
140
|
if (fs.existsSync(profilePath)) {
|
|
@@ -347,7 +349,13 @@ GFI: ${currentGfi}/100
|
|
|
347
349
|
const trustScore = trustEngine.getScore();
|
|
348
350
|
const stage = trustEngine.getStage();
|
|
349
351
|
const trustSettings = wctx.config.get('trust') || {
|
|
350
|
-
limits: {
|
|
352
|
+
limits: {
|
|
353
|
+
stage_2_max_lines: 50,
|
|
354
|
+
stage_3_max_lines: 300,
|
|
355
|
+
stage_2_max_percentage: 10,
|
|
356
|
+
stage_3_max_percentage: 15,
|
|
357
|
+
min_lines_fallback: 20,
|
|
358
|
+
}
|
|
351
359
|
};
|
|
352
360
|
const riskLevel = assessRiskLevel(relPath, { toolName: event.toolName, params: event.params }, profile.risk_paths);
|
|
353
361
|
const lineChanges = estimateLineChanges({ toolName: event.toolName, params: event.params });
|
|
@@ -421,17 +429,34 @@ GFI: ${currentGfi}/100
|
|
|
421
429
|
}
|
|
422
430
|
if (risky || riskLevel !== 'LOW') {
|
|
423
431
|
// Block if not approved by whitelist
|
|
424
|
-
return block(relPath, `Trust score too low (${trustScore}). Stage 1 agents cannot modify risk paths or perform non-trivial edits.`, wctx, event.toolName, ctx.sessionId);
|
|
432
|
+
return block(relPath, `Trust score too low (${trustScore}). Stage 1 agents cannot modify risk paths or perform non-trivial edits.`, wctx, event.toolName, logger, ctx.sessionId);
|
|
425
433
|
}
|
|
426
434
|
}
|
|
427
435
|
// Stage 2 (Editor): Block writes to risk paths. Block large changes.
|
|
428
436
|
if (stage === 2) {
|
|
429
437
|
if (risky) {
|
|
430
|
-
return block(relPath, `Stage 2 agents are not authorized to modify risk paths.`, wctx, event.toolName, ctx.sessionId);
|
|
438
|
+
return block(relPath, `Stage 2 agents are not authorized to modify risk paths.`, wctx, event.toolName, logger, ctx.sessionId);
|
|
439
|
+
}
|
|
440
|
+
// Percentage-based threshold calculation
|
|
441
|
+
const targetAbsolutePath = typeof filePath === 'string' ? path.join(ctx.workspaceDir, filePath) : null;
|
|
442
|
+
const targetLineCount = targetAbsolutePath ? getTargetFileLineCount(targetAbsolutePath) : null;
|
|
443
|
+
const minLinesFallback = trustSettings.limits?.min_lines_fallback ?? 20;
|
|
444
|
+
const stage2MaxPercentage = trustSettings.limits?.stage_2_max_percentage ?? 10;
|
|
445
|
+
const stage2FixedLimit = trustSettings.limits?.stage_2_max_lines ?? 50;
|
|
446
|
+
let effectiveLimit;
|
|
447
|
+
let limitType;
|
|
448
|
+
let actualPercentage = null;
|
|
449
|
+
if (targetLineCount !== null && targetLineCount > 0) {
|
|
450
|
+
effectiveLimit = calculatePercentageThreshold(targetLineCount, stage2MaxPercentage, minLinesFallback);
|
|
451
|
+
actualPercentage = Math.round((lineChanges / targetLineCount) * 100);
|
|
452
|
+
limitType = 'percentage';
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
effectiveLimit = stage2FixedLimit;
|
|
456
|
+
limitType = 'fixed';
|
|
431
457
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return block(relPath, `Modification too large (${lineChanges} lines) for Stage 2. Max allowed is ${stage2Limit}.`, wctx, event.toolName, ctx.sessionId);
|
|
458
|
+
if (lineChanges > effectiveLimit) {
|
|
459
|
+
return block(relPath, buildLineLimitReason(lineChanges, effectiveLimit, limitType, targetLineCount, actualPercentage, 2), wctx, event.toolName, logger, ctx.sessionId);
|
|
435
460
|
}
|
|
436
461
|
}
|
|
437
462
|
// Stage 3 (Developer): Allow normal writes. Require READY plan for risk paths.
|
|
@@ -439,12 +464,29 @@ GFI: ${currentGfi}/100
|
|
|
439
464
|
if (risky) {
|
|
440
465
|
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
441
466
|
if (planStatus !== 'READY') {
|
|
442
|
-
return block(relPath, `No READY plan found. Stage 3 requires a plan for risk path modifications.`, wctx, event.toolName, ctx.sessionId);
|
|
467
|
+
return block(relPath, `No READY plan found. Stage 3 requires a plan for risk path modifications.`, wctx, event.toolName, logger, ctx.sessionId);
|
|
443
468
|
}
|
|
444
469
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
470
|
+
// Percentage-based threshold calculation
|
|
471
|
+
const targetAbsolutePath = typeof filePath === 'string' ? path.join(ctx.workspaceDir, filePath) : null;
|
|
472
|
+
const targetLineCount = targetAbsolutePath ? getTargetFileLineCount(targetAbsolutePath) : null;
|
|
473
|
+
const minLinesFallback = trustSettings.limits?.min_lines_fallback ?? 20;
|
|
474
|
+
const stage3MaxPercentage = trustSettings.limits?.stage_3_max_percentage ?? 15;
|
|
475
|
+
const stage3FixedLimit = trustSettings.limits?.stage_3_max_lines ?? 300;
|
|
476
|
+
let effectiveLimit;
|
|
477
|
+
let limitType;
|
|
478
|
+
let actualPercentage = null;
|
|
479
|
+
if (targetLineCount !== null && targetLineCount > 0) {
|
|
480
|
+
effectiveLimit = calculatePercentageThreshold(targetLineCount, stage3MaxPercentage, minLinesFallback);
|
|
481
|
+
actualPercentage = Math.round((lineChanges / targetLineCount) * 100);
|
|
482
|
+
limitType = 'percentage';
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
effectiveLimit = stage3FixedLimit;
|
|
486
|
+
limitType = 'fixed';
|
|
487
|
+
}
|
|
488
|
+
if (lineChanges > effectiveLimit) {
|
|
489
|
+
return block(relPath, buildLineLimitReason(lineChanges, effectiveLimit, limitType, targetLineCount, actualPercentage, 3), wctx, event.toolName, logger, ctx.sessionId);
|
|
448
490
|
}
|
|
449
491
|
}
|
|
450
492
|
// Stage 4 (Architect): Full bypass
|
|
@@ -473,7 +515,7 @@ GFI: ${currentGfi}/100
|
|
|
473
515
|
if (risky && profile.gate?.require_plan_for_risk_paths) {
|
|
474
516
|
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
475
517
|
if (planStatus !== 'READY') {
|
|
476
|
-
return block(relPath, `No READY plan found in PLAN.md.`, wctx, event.toolName, ctx.sessionId);
|
|
518
|
+
return block(relPath, `No READY plan found in PLAN.md.`, wctx, event.toolName, logger, ctx.sessionId);
|
|
477
519
|
}
|
|
478
520
|
}
|
|
479
521
|
}
|
|
@@ -494,12 +536,32 @@ GFI: ${currentGfi}/100
|
|
|
494
536
|
}
|
|
495
537
|
}
|
|
496
538
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
539
|
+
/**
|
|
540
|
+
* Build a detailed reason message for line limit blocks.
|
|
541
|
+
*/
|
|
542
|
+
function buildLineLimitReason(lineChanges, effectiveLimit, limitType, targetLineCount, actualPercentage, stage) {
|
|
543
|
+
if (limitType === 'percentage' && targetLineCount !== null && actualPercentage !== null) {
|
|
544
|
+
return `Modification too large: ${lineChanges} lines (${actualPercentage}% of ${targetLineCount} lines). ` +
|
|
545
|
+
`Stage ${stage} limit is ${effectiveLimit} lines (${limitType}). ` +
|
|
546
|
+
`Threshold calculation: min(${targetLineCount} × ${actualPercentage}%, ${effectiveLimit} lines).`;
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
return `Modification too large: ${lineChanges} lines. ` +
|
|
550
|
+
`Stage ${stage} limit is ${effectiveLimit} lines (fixed threshold). ` +
|
|
551
|
+
`Note: Could not read target file to calculate percentage-based limit. Check file permissions and encoding.`;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function block(filePath, reason, wctx, toolName, logger, sessionId) {
|
|
555
|
+
logger.error?.(`[PD_GATE] BLOCKED: ${filePath}. Reason: ${reason}`);
|
|
500
556
|
if (sessionId) {
|
|
501
557
|
trackBlock(sessionId);
|
|
502
558
|
}
|
|
559
|
+
const trajectoryPayload = {
|
|
560
|
+
sessionId: sessionId ?? null,
|
|
561
|
+
toolName,
|
|
562
|
+
filePath,
|
|
563
|
+
reason,
|
|
564
|
+
};
|
|
503
565
|
try {
|
|
504
566
|
wctx.eventLog.recordGateBlock(sessionId, {
|
|
505
567
|
toolName,
|
|
@@ -508,13 +570,61 @@ function block(filePath, reason, wctx, toolName, sessionId) {
|
|
|
508
570
|
});
|
|
509
571
|
}
|
|
510
572
|
catch (error) {
|
|
511
|
-
logger.warn(`[PD_GATE] Failed to record gate block event: ${String(error)}`);
|
|
573
|
+
logger.warn?.(`[PD_GATE] Failed to record gate block event: ${String(error)}`);
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
wctx.trajectory?.recordGateBlock?.(trajectoryPayload);
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
logger.warn?.(`[PD_GATE] Failed to record trajectory gate block: ${String(error)}`);
|
|
580
|
+
scheduleTrajectoryGateBlockRetry(wctx, trajectoryPayload, 1, {
|
|
581
|
+
warn: (message) => logger.warn?.(message),
|
|
582
|
+
error: (message) => logger.error?.(message),
|
|
583
|
+
});
|
|
512
584
|
}
|
|
513
585
|
return {
|
|
514
586
|
block: true,
|
|
515
|
-
blockReason: `[Principles Disciple] Security Gate Blocked this action
|
|
587
|
+
blockReason: `[Principles Disciple] Security Gate Blocked this action.
|
|
588
|
+
File: ${filePath}
|
|
589
|
+
Reason: ${reason}
|
|
590
|
+
|
|
591
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
592
|
+
📋 How to unblock this operation:
|
|
593
|
+
|
|
594
|
+
1. Use the plan-script skill to create a PLAN.md:
|
|
595
|
+
→ Invoke: skill:plan-script
|
|
596
|
+
|
|
597
|
+
2. Fill in the plan with:
|
|
598
|
+
- Target Files: ${filePath}
|
|
599
|
+
- Steps: What you want to do (be specific)
|
|
600
|
+
- Metrics: How to verify success
|
|
601
|
+
- Active Mental Models: Select 2 relevant models from .principles/THINKING_OS.md
|
|
602
|
+
- Rollback: How to restore if it fails
|
|
603
|
+
|
|
604
|
+
3. After completing the plan, set STATUS: READY in PLAN.md
|
|
605
|
+
|
|
606
|
+
4. Retry the operation
|
|
607
|
+
|
|
608
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
609
|
+
This is a mandatory security gate. The operation was blocked because the modification exceeds the allowed threshold for your current trust stage.
|
|
610
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
516
611
|
};
|
|
517
612
|
}
|
|
613
|
+
function scheduleTrajectoryGateBlockRetry(wctx, payload, attempt, logger) {
|
|
614
|
+
if (attempt > TRAJECTORY_GATE_BLOCK_MAX_RETRIES) {
|
|
615
|
+
logger.error?.(`[PD_GATE] Failed to persist trajectory gate block after ${TRAJECTORY_GATE_BLOCK_MAX_RETRIES} retries: ${payload.toolName} ${payload.filePath}`);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
setTimeout(() => {
|
|
619
|
+
try {
|
|
620
|
+
wctx.trajectory?.recordGateBlock?.(payload);
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
logger.warn(`[PD_GATE] Retrying trajectory gate block persistence: ${String(error)}`);
|
|
624
|
+
scheduleTrajectoryGateBlockRetry(wctx, payload, attempt + 1, logger);
|
|
625
|
+
}
|
|
626
|
+
}, TRAJECTORY_GATE_BLOCK_RETRY_DELAY_MS);
|
|
627
|
+
}
|
|
518
628
|
// ═══════════════════════════════════════════════════════════════
|
|
519
629
|
// P-03: Edit Tool Force Verification
|
|
520
630
|
// ═══════════════════════════════════════════════════════════════
|