principles-disciple 1.73.0 → 1.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +1 -3
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/core/event-log.ts +0 -9
- package/src/core/migration.ts +0 -1
- package/src/core/path-resolver.ts +0 -1
- package/src/core/paths.ts +0 -1
- package/src/core/workspace-guidance-migrator.ts +179 -0
- package/src/hooks/gate-block-helper.ts +25 -20
- package/src/hooks/gate.ts +13 -61
- package/src/hooks/prompt.ts +1 -61
- package/src/index.ts +8 -12
- package/src/types/event-types.ts +0 -1
- package/src/utils/io.ts +0 -22
- package/templates/langs/en/core/AGENTS.md +5 -5
- package/templates/langs/en/core/BOOTSTRAP.md +1 -1
- package/templates/langs/en/principles/THINKING_OS.md +4 -3
- package/templates/langs/en/skills/admin/SKILL.md +2 -2
- package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
- package/templates/langs/en/skills/evolve-task/SKILL.md +2 -2
- package/templates/langs/en/skills/pd-grooming/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -2
- package/templates/langs/en/skills/reflection/SKILL.md +2 -2
- package/templates/langs/en/skills/report/SKILL.md +1 -1
- package/templates/langs/zh/core/AGENTS.md +5 -5
- package/templates/langs/zh/core/BOOTSTRAP.md +1 -1
- package/templates/langs/zh/principles/THINKING_OS.md +4 -3
- package/templates/langs/zh/skills/admin/SKILL.md +2 -2
- package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
- package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
- package/templates/langs/zh/skills/pd-grooming/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -2
- package/templates/langs/zh/skills/reflection/SKILL.md +2 -2
- package/templates/langs/zh/skills/report/SKILL.md +1 -1
- package/tests/core/migration.test.ts +7 -7
- package/tests/core/path-resolver.test.ts +1 -1
- package/tests/core/paths-refactor.test.ts +0 -22
- package/tests/core/workspace-context.test.ts +2 -2
- package/tests/core-anti-growth.test.ts +1 -1
- package/tests/hooks/confirm-first-removal.test.ts +188 -0
- package/tests/hooks/gate-no-path-write-tool.test.ts +172 -0
- package/src/core/confirm-first-gate.ts +0 -255
- package/templates/langs/en/skills/plan-script/SKILL.md +0 -32
- package/templates/langs/zh/skills/plan-script/SKILL.md +0 -32
- package/templates/workspace/PLAN.md +0 -2
- package/tests/hooks/confirm-first-gate.test.ts +0 -333
package/INSTALL.md
CHANGED
|
@@ -236,9 +236,7 @@ After installation, enable the PLAN whitelist feature:
|
|
|
236
236
|
}
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
-
2.
|
|
240
|
-
|
|
241
|
-
3. Restart your agent session
|
|
239
|
+
2. Restart your agent session
|
|
242
240
|
|
|
243
241
|
Now even Stage 1 agents can edit files when a READY plan exists!
|
|
244
242
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/core/event-log.ts
CHANGED
|
@@ -28,7 +28,6 @@ import type {
|
|
|
28
28
|
RuleHostAutoCorrectProposedEventData,
|
|
29
29
|
RuleHostAutoCorrectAppliedEventData,
|
|
30
30
|
RuntimeV2PromptActivationsInjectedEventData,
|
|
31
|
-
RuntimeV2ConfirmFirstGateEventData,
|
|
32
31
|
} from '../types/event-types.js';
|
|
33
32
|
import { createEmptyDailyStats } from '../types/event-types.js';
|
|
34
33
|
import { atomicWriteFileSync } from '../utils/io.js';
|
|
@@ -210,14 +209,6 @@ export class EventLog {
|
|
|
210
209
|
this.record('runtime_v2_prompt_activations_injected', 'injected', data.sessionId, data);
|
|
211
210
|
}
|
|
212
211
|
|
|
213
|
-
recordConfirmFirstGateBlocked(data: RuntimeV2ConfirmFirstGateEventData): void {
|
|
214
|
-
this.record('runtime_v2_confirm_first_gate_blocked', 'blocked', data.sessionId, data);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
recordConfirmFirstGateApproved(data: RuntimeV2ConfirmFirstGateEventData): void {
|
|
218
|
-
this.record('runtime_v2_confirm_first_gate_approved', 'approved', data.sessionId, data);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
212
|
private record(
|
|
222
213
|
type: EventType,
|
|
223
214
|
category: EventCategory,
|
package/src/core/migration.ts
CHANGED
|
@@ -19,7 +19,6 @@ export function migrateDirectoryStructure(api: OpenClawPluginApi, workspaceDir:
|
|
|
19
19
|
{ legacy: path.join(legacyDocsDir, 'PRINCIPLES.md'), newKey: 'PRINCIPLES' },
|
|
20
20
|
{ legacy: path.join(legacyDocsDir, 'THINKING_OS.md'), newKey: 'THINKING_OS' },
|
|
21
21
|
{ legacy: path.join(legacyDocsDir, 'DECISION_POLICY.json'), newKey: 'DECISION_POLICY' },
|
|
22
|
-
{ legacy: path.join(legacyDocsDir, 'PLAN.md'), newKey: 'PLAN' },
|
|
23
22
|
{ legacy: path.join(legacyDocsDir, 'evolution_queue.json'), newKey: 'EVOLUTION_QUEUE' },
|
|
24
23
|
{ legacy: path.join(legacyDocsDir, '.pain_flag'), newKey: 'PAIN_FLAG' },
|
|
25
24
|
{ legacy: path.join(legacyDocsDir, 'SYSTEM_CAPABILITIES.json'), newKey: 'SYSTEM_CAPABILITIES' },
|
|
@@ -306,7 +306,6 @@ export class PathResolver {
|
|
|
306
306
|
'THINKING_OS': workspacePath.join(workspace, '.principles', 'THINKING_OS.md'),
|
|
307
307
|
'DECISION_POLICY': workspacePath.join(workspace, '.principles', 'DECISION_POLICY.json'),
|
|
308
308
|
'MODELS_DIR': workspacePath.join(workspace, '.principles', 'models'),
|
|
309
|
-
'PLAN': workspacePath.join(workspace, 'PLAN.md'),
|
|
310
309
|
'AGENT_SCORECARD': workspacePath.join(state, 'AGENT_SCORECARD.json'),
|
|
311
310
|
'PAIN_FLAG': workspacePath.join(state, '.pain_flag'),
|
|
312
311
|
'EVOLUTION_QUEUE': workspacePath.join(state, 'evolution_queue.json'),
|
package/src/core/paths.ts
CHANGED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import type { OpenClawPluginApi } from '../openclaw-sdk.js';
|
|
4
|
+
import { migrateWorkspaceGuidance, containsStalePlanMdGuidance } from '@principles/core/runtime-v2';
|
|
5
|
+
|
|
6
|
+
const WORKSPACE_GUIDANCE_FILES = [
|
|
7
|
+
'AGENTS.md',
|
|
8
|
+
'MEMORY.md',
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
const PRINCIPLES_SUBDIR_FILES = [
|
|
12
|
+
'THINKING_OS.md',
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
const SKILLS_DIR = path.join('.principles', 'skills');
|
|
16
|
+
const PRINCIPLES_DIR = '.principles';
|
|
17
|
+
const BACKUP_SUFFIX = '.pre-pri286.bak';
|
|
18
|
+
|
|
19
|
+
interface MigrationError {
|
|
20
|
+
file: string;
|
|
21
|
+
error: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MigrationResult {
|
|
25
|
+
migratedFiles: string[];
|
|
26
|
+
skippedFiles: string[];
|
|
27
|
+
errors: MigrationError[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readFileContent(filePath: string): string | null {
|
|
31
|
+
try {
|
|
32
|
+
const raw: unknown = fs.readFileSync(filePath, 'utf-8');
|
|
33
|
+
if (typeof raw !== 'string') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return raw;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeBackup(filePath: string, content: string): boolean {
|
|
43
|
+
const backupPath = filePath + BACKUP_SUFFIX;
|
|
44
|
+
try {
|
|
45
|
+
fs.writeFileSync(backupPath, content, 'utf-8');
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface DiscoverResult {
|
|
53
|
+
files: string[];
|
|
54
|
+
error?: MigrationError;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function discoverSkillFiles(workspaceDir: string): DiscoverResult {
|
|
58
|
+
const skillsDir = path.join(workspaceDir, SKILLS_DIR);
|
|
59
|
+
if (!fs.existsSync(skillsDir)) {
|
|
60
|
+
return { files: [] };
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
64
|
+
const skillFiles: string[] = [];
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
const skillMd = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
68
|
+
if (fs.existsSync(skillMd)) {
|
|
69
|
+
skillFiles.push(skillMd);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { files: skillFiles };
|
|
74
|
+
} catch (err: unknown) {
|
|
75
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
76
|
+
return {
|
|
77
|
+
files: [],
|
|
78
|
+
error: {
|
|
79
|
+
file: SKILLS_DIR,
|
|
80
|
+
error: `Failed to enumerate skills directory: ${errMsg}`,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function collectCandidateFiles(workspaceDir: string, result: MigrationResult): string[] {
|
|
87
|
+
const candidates: string[] = [];
|
|
88
|
+
|
|
89
|
+
for (const filename of WORKSPACE_GUIDANCE_FILES) {
|
|
90
|
+
candidates.push(path.join(workspaceDir, filename));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const filename of PRINCIPLES_SUBDIR_FILES) {
|
|
94
|
+
candidates.push(path.join(workspaceDir, PRINCIPLES_DIR, filename));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const skillDiscovery = discoverSkillFiles(workspaceDir);
|
|
98
|
+
if (skillDiscovery.error) {
|
|
99
|
+
result.errors.push(skillDiscovery.error);
|
|
100
|
+
}
|
|
101
|
+
candidates.push(...skillDiscovery.files);
|
|
102
|
+
|
|
103
|
+
return candidates;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function migrateStaleWorkspaceGuidance(
|
|
107
|
+
api: OpenClawPluginApi,
|
|
108
|
+
workspaceDir: string,
|
|
109
|
+
): MigrationResult {
|
|
110
|
+
const result: MigrationResult = {
|
|
111
|
+
migratedFiles: [],
|
|
112
|
+
skippedFiles: [],
|
|
113
|
+
errors: [],
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const candidates = collectCandidateFiles(workspaceDir, result);
|
|
117
|
+
|
|
118
|
+
for (const filePath of candidates) {
|
|
119
|
+
const relativePath = path.relative(workspaceDir, filePath);
|
|
120
|
+
|
|
121
|
+
if (!fs.existsSync(filePath)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const content = readFileContent(filePath);
|
|
126
|
+
if (content === null) {
|
|
127
|
+
result.errors.push({
|
|
128
|
+
file: relativePath,
|
|
129
|
+
error: 'Failed to read file content',
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!containsStalePlanMdGuidance(content, relativePath)) {
|
|
135
|
+
result.skippedFiles.push(relativePath);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const migrationResult = migrateWorkspaceGuidance(content, relativePath);
|
|
140
|
+
if (!migrationResult.changed) {
|
|
141
|
+
result.skippedFiles.push(relativePath);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const migrated = migrationResult.migrated;
|
|
146
|
+
|
|
147
|
+
const backupOk = writeBackup(filePath, content);
|
|
148
|
+
if (!backupOk) {
|
|
149
|
+
result.errors.push({
|
|
150
|
+
file: relativePath,
|
|
151
|
+
error: 'Failed to create backup file before migration',
|
|
152
|
+
});
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
fs.writeFileSync(filePath, migrated, 'utf-8');
|
|
158
|
+
result.migratedFiles.push(relativePath);
|
|
159
|
+
api.logger.info(`[PD:GuidanceMigration] Migrated ${relativePath} (backup at ${relativePath}${BACKUP_SUFFIX})`);
|
|
160
|
+
} catch (writeErr: unknown) {
|
|
161
|
+
const errMsg = writeErr instanceof Error ? writeErr.message : String(writeErr);
|
|
162
|
+
result.errors.push({
|
|
163
|
+
file: relativePath,
|
|
164
|
+
error: `Failed to write migrated content: ${errMsg}`,
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
168
|
+
} catch {
|
|
169
|
+
api.logger.error(`[PD:GuidanceMigration] CRITICAL: Failed to restore original content for ${relativePath} after write failure`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (result.migratedFiles.length > 0) {
|
|
175
|
+
api.logger.info(`[PD:GuidanceMigration] Migration complete: ${result.migratedFiles.length} migrated, ${result.skippedFiles.length} skipped, ${result.errors.length} errors`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
@@ -149,34 +149,39 @@ export function recordGateBlockAndReturn(
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
// 6. Return consistent block result with operator guidance
|
|
152
|
+
// 6. Return consistent block result with contextual operator guidance
|
|
153
|
+
const blockMessage = buildContextualBlockMessage({ filePath, reason });
|
|
154
|
+
|
|
153
155
|
return {
|
|
154
156
|
block: true,
|
|
155
|
-
blockReason:
|
|
157
|
+
blockReason: blockMessage,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build contextual block message based on block source.
|
|
163
|
+
* - rule-host: principle-based guidance
|
|
164
|
+
* - default/gate: generic security gate message
|
|
165
|
+
*/
|
|
166
|
+
function buildContextualBlockMessage({
|
|
167
|
+
filePath,
|
|
168
|
+
reason,
|
|
169
|
+
}: {
|
|
170
|
+
filePath: string;
|
|
171
|
+
reason: string;
|
|
172
|
+
}): string {
|
|
173
|
+
// rule-host or generic gate blocks
|
|
174
|
+
return `[Principles Disciple] Security Gate Blocked this action.
|
|
156
175
|
File: ${filePath}
|
|
157
176
|
Reason: ${reason}
|
|
158
177
|
|
|
159
178
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
160
179
|
📋 How to unblock this operation:
|
|
161
180
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
- Target Files: ${filePath}
|
|
167
|
-
- Steps: What you want to do (be specific)
|
|
168
|
-
- Metrics: How to verify success
|
|
169
|
-
- Active Mental Models: Select 2 relevant models from .principles/THINKING_OS.md
|
|
170
|
-
- Rollback: How to restore if it fails
|
|
171
|
-
|
|
172
|
-
3. After completing the plan, set STATUS: READY in PLAN.md
|
|
173
|
-
|
|
174
|
-
4. Retry the operation
|
|
175
|
-
|
|
176
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
177
|
-
This is a mandatory security gate. The operation was blocked because the modification exceeds the allowed threshold for your current evolution tier.
|
|
178
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
179
|
-
};
|
|
181
|
+
This action was blocked by a Rule Host principle.
|
|
182
|
+
If the blocked path is correct and safe, explain the reasoning to the owner
|
|
183
|
+
and ask for explicit confirmation to proceed.
|
|
184
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`;
|
|
180
185
|
}
|
|
181
186
|
|
|
182
187
|
/**
|
package/src/hooks/gate.ts
CHANGED
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
* 2. Rule Host: Dynamic principle-based evaluation (sole gate)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import
|
|
13
|
-
import * as path from 'path';
|
|
14
|
-
import { normalizePath, planStatus } from '../utils/io.js';
|
|
12
|
+
import { normalizePath } from '../utils/io.js';
|
|
15
13
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
16
14
|
import { recordGateBlockAndReturn } from './gate-block-helper.js';
|
|
17
15
|
import { RuleHost } from '../core/rule-host.js';
|
|
@@ -19,7 +17,6 @@ import type { RuleHostInput } from '@principles/core/runtime-v2';
|
|
|
19
17
|
import { validateCorrectionProposal, validateProposedPathBounds } from '@principles/core/runtime-v2';
|
|
20
18
|
import type { PluginHookBeforeToolCallEvent, PluginHookToolContext, PluginHookBeforeToolCallResult, PluginLogger } from '../openclaw-sdk.js';
|
|
21
19
|
import { AGENT_TOOLS, BASH_TOOLS_SET, WRITE_TOOLS } from '../constants/tools.js';
|
|
22
|
-
import { evaluateConfirmFirstGateSync } from '../core/confirm-first-gate.js';
|
|
23
20
|
import { getSession, hasRecentThinking } from '../core/session-tracker.js';
|
|
24
21
|
import { getEvolutionEngine } from '../core/evolution-engine.js';
|
|
25
22
|
import { EventLogService } from '../core/event-log.js';
|
|
@@ -42,42 +39,6 @@ export function handleBeforeToolCall(
|
|
|
42
39
|
|
|
43
40
|
const wctx = WorkspaceContext.fromHookContext(ctx);
|
|
44
41
|
|
|
45
|
-
// 1.5. Confirm-First Gate — runs BEFORE filePath resolution to catch apply_patch/no-path cases
|
|
46
|
-
try {
|
|
47
|
-
const cfResult = evaluateConfirmFirstGateSync(
|
|
48
|
-
ctx.sessionId,
|
|
49
|
-
event.toolName,
|
|
50
|
-
event.params,
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
if (cfResult.action === 'block') {
|
|
54
|
-
const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
|
|
55
|
-
eventLog.recordConfirmFirstGateBlocked({
|
|
56
|
-
sessionId: ctx.sessionId ?? 'unknown',
|
|
57
|
-
workspaceDir: ctx.workspaceDir,
|
|
58
|
-
toolName: event.toolName,
|
|
59
|
-
reason: cfResult.reason ?? 'confirm_first_required',
|
|
60
|
-
principleId: cfResult.principleId ?? 'unknown',
|
|
61
|
-
nextAction: cfResult.nextAction ?? '',
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// Use safe placeholder when filePath is unavailable (e.g., apply_patch with no path)
|
|
65
|
-
const safePath = (event.params?.file_path || event.params?.path || event.params?.file || event.params?.target)
|
|
66
|
-
?? `<tool:${event.toolName}>`;
|
|
67
|
-
|
|
68
|
-
return recordGateBlockAndReturn(wctx, {
|
|
69
|
-
filePath: typeof safePath === 'string' ? safePath : `<tool:${event.toolName}>`,
|
|
70
|
-
reason: cfResult.reason ?? 'confirm_first_required',
|
|
71
|
-
toolName: event.toolName,
|
|
72
|
-
sessionId: ctx.sessionId,
|
|
73
|
-
blockSource: 'confirm-first-gate',
|
|
74
|
-
}, logger);
|
|
75
|
-
}
|
|
76
|
-
} catch (cfErr) {
|
|
77
|
-
// ERR-002: fail loud — log but do not crash the gate
|
|
78
|
-
logger?.warn?.(`[PD:ConfirmFirst] Gate evaluation failed (non-blocking): ${String(cfErr)}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
42
|
// 2. Resolve the target file path
|
|
82
43
|
let filePath = event.params?.file_path || event.params?.path || event.params?.file || event.params?.target;
|
|
83
44
|
|
|
@@ -94,6 +55,12 @@ export function handleBeforeToolCall(
|
|
|
94
55
|
}
|
|
95
56
|
}
|
|
96
57
|
|
|
58
|
+
// Write tools without a file path must still go through RuleHost evaluation.
|
|
59
|
+
// Use a synthetic path so RuleHost can evaluate and potentially block.
|
|
60
|
+
if (!filePath && isWriteTool) {
|
|
61
|
+
filePath = `<tool:${event.toolName}>`;
|
|
62
|
+
}
|
|
63
|
+
|
|
97
64
|
if (typeof filePath !== 'string') return;
|
|
98
65
|
|
|
99
66
|
const relPath = normalizePath(filePath, ctx.workspaceDir);
|
|
@@ -109,8 +76,12 @@ export function handleBeforeToolCall(
|
|
|
109
76
|
},
|
|
110
77
|
workspace: {
|
|
111
78
|
isRiskPath: false, // Rule Host determines risk dynamically
|
|
112
|
-
|
|
113
|
-
|
|
79
|
+
// DEPRECATED (PRI-286): planStatus/hasPlanFile are legacy compatibility fields.
|
|
80
|
+
// Live PD no longer reads or manages PLAN.md state. These fields must not be
|
|
81
|
+
// used for new MVP behavior. Future "plan-first" enforcement must come from
|
|
82
|
+
// owner-approved RuleHost/code_tool_hook activation, not built-in state.
|
|
83
|
+
planStatus: 'NONE' as const,
|
|
84
|
+
hasPlanFile: false,
|
|
114
85
|
},
|
|
115
86
|
session: {
|
|
116
87
|
sessionId: ctx.sessionId,
|
|
@@ -377,25 +348,6 @@ function _extractParamsSummary(params: Record<string, unknown>): Record<string,
|
|
|
377
348
|
return summary;
|
|
378
349
|
}
|
|
379
350
|
|
|
380
|
-
function _getPlanStatus(workspaceDir: string): 'NONE' | 'DRAFT' | 'READY' | 'UNKNOWN' {
|
|
381
|
-
try {
|
|
382
|
-
const status = planStatus(workspaceDir);
|
|
383
|
-
if (status === 'READY') return 'READY';
|
|
384
|
-
if (status === 'DRAFT') return 'DRAFT';
|
|
385
|
-
if (status === '') return 'NONE';
|
|
386
|
-
return 'UNKNOWN';
|
|
387
|
-
} catch {
|
|
388
|
-
return 'UNKNOWN';
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function _hasPlanFile(workspaceDir: string): boolean {
|
|
393
|
-
try {
|
|
394
|
-
return fs.existsSync(path.join(workspaceDir, 'PLAN.md'));
|
|
395
|
-
} catch {
|
|
396
|
-
return false;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
351
|
|
|
400
352
|
function _getCurrentGfi(sessionId?: string): number {
|
|
401
353
|
if (!sessionId) return 0;
|
package/src/hooks/prompt.ts
CHANGED
|
@@ -8,11 +8,10 @@ import { WorkspaceContext } from '../core/workspace-context.js';
|
|
|
8
8
|
import type { ContextInjectionConfig} from '../types.js';
|
|
9
9
|
import { defaultContextConfig } from '../types.js';
|
|
10
10
|
import { classifyTask, type RoutingInput } from '../core/local-worker-routing.js';
|
|
11
|
-
import { detectApprovalMarker, setConfirmFirstApproval, setConfirmFirstDirective, hydrateFromStore, pruneStoreStaleRows, setConfirmFirstStore, resetConfirmFirst } from '../core/confirm-first-gate.js';
|
|
12
11
|
import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection, autoCompressFocus, safeReadCurrentFocus } from '../core/focus-history.js';
|
|
13
12
|
import { PathResolver } from '../core/path-resolver.js';
|
|
14
13
|
import { selectPrinciplesForInjection, DEFAULT_PRINCIPLE_BUDGET } from '../core/principle-injection.js';
|
|
15
|
-
import { getCachedMaskedPrincipleSet, WorkflowFunnelLoader, PiAiRuntimeAdapter, EmpathyObserver, AgentScheduler
|
|
14
|
+
import { getCachedMaskedPrincipleSet, WorkflowFunnelLoader, PiAiRuntimeAdapter, EmpathyObserver, AgentScheduler } from '@principles/core/runtime-v2';
|
|
16
15
|
import { truncateInjectionToBudget } from '@principles/core/prompt-builder';
|
|
17
16
|
import { PromptActivationReader, RUNTIME_V2_PRINCIPLE_BUDGET } from '../core/runtime-v2-prompt-activation-reader.js';
|
|
18
17
|
import {
|
|
@@ -77,7 +76,6 @@ function cachedReadFile(filePath: string): string {
|
|
|
77
76
|
// Module-level empathy state — shared across calls to avoid per-turn I/O
|
|
78
77
|
let _empathyTurnCounter = 0;
|
|
79
78
|
let _empathyKeywordCache: { store: ReturnType<typeof loadKeywordStore>; lang: string } | null = null;
|
|
80
|
-
let _confirmFirstHydrationCounter = 0;
|
|
81
79
|
|
|
82
80
|
/**
|
|
83
81
|
* Model configuration with primary model and optional fallback models
|
|
@@ -265,19 +263,6 @@ export function getDiagnosticianModel(api: PromptHookApi | null, logger?: Plugin
|
|
|
265
263
|
*/
|
|
266
264
|
|
|
267
265
|
|
|
268
|
-
function ensureConfirmFirstStore(workspaceDir: string): void {
|
|
269
|
-
if (!_confirmFirstStoreInitialized) {
|
|
270
|
-
try {
|
|
271
|
-
const connection = new SqliteConnection({ workspaceDir, readonly: false });
|
|
272
|
-
setConfirmFirstStore(new SqliteConfirmFirstStateStore(connection));
|
|
273
|
-
_confirmFirstStoreInitialized = true;
|
|
274
|
-
} catch (err) {
|
|
275
|
-
console.warn(`[PD:ConfirmFirst] Failed to initialize store: ${String(err)}`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
let _confirmFirstStoreInitialized = false;
|
|
280
|
-
|
|
281
266
|
export async function handleBeforePromptBuild(
|
|
282
267
|
event: PluginHookBeforePromptBuildEvent,
|
|
283
268
|
ctx: PluginHookAgentContext & { api?: PromptHookApi }
|
|
@@ -297,18 +282,6 @@ export async function handleBeforePromptBuild(
|
|
|
297
282
|
wctx.trajectory?.recordSession?.({ sessionId });
|
|
298
283
|
}
|
|
299
284
|
|
|
300
|
-
if (sessionId) {
|
|
301
|
-
ensureConfirmFirstStore(workspaceDir);
|
|
302
|
-
hydrateFromStore(sessionId);
|
|
303
|
-
_confirmFirstHydrationCounter++;
|
|
304
|
-
if (_confirmFirstHydrationCounter % 100 === 0) {
|
|
305
|
-
const pruned = pruneStoreStaleRows();
|
|
306
|
-
if (pruned > 0) {
|
|
307
|
-
logger?.info?.(`[PD:ConfirmFirst] Pruned ${pruned} stale rows from confirm_first_state`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
285
|
if (sessionId && trigger === 'user' && Array.isArray(event.messages) && event.messages.length > 0) {
|
|
313
286
|
const latestUserIndex = [...event.messages]
|
|
314
287
|
.map((message, index) => ({ message, index }))
|
|
@@ -318,25 +291,6 @@ export async function handleBeforePromptBuild(
|
|
|
318
291
|
if (latestUserIndex) {
|
|
319
292
|
const userText = getTextContent(latestUserIndex.message);
|
|
320
293
|
|
|
321
|
-
// ── Confirm-first approval detection ──
|
|
322
|
-
// If user sends approval language, mark session as approved for confirm-first gate
|
|
323
|
-
if (sessionId && detectApprovalMarker(userText)) {
|
|
324
|
-
setConfirmFirstApproval(sessionId);
|
|
325
|
-
// P2: Emit approval telemetry for observability (ERR-002)
|
|
326
|
-
try {
|
|
327
|
-
wctx.eventLog.recordConfirmFirstGateApproved({
|
|
328
|
-
sessionId,
|
|
329
|
-
workspaceDir: wctx.workspaceDir,
|
|
330
|
-
toolName: '(approval)',
|
|
331
|
-
reason: 'user_approval_detected',
|
|
332
|
-
principleId: 'confirm-first',
|
|
333
|
-
nextAction: 'mutating tools now permitted',
|
|
334
|
-
});
|
|
335
|
-
} catch (logErr) {
|
|
336
|
-
logger?.warn?.(`[PD:ConfirmFirst] Failed to emit approval event: ${String(logErr)}`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
294
|
// Use CorrectionCueLearner for detection — supports learned keywords, not just hardcoded list
|
|
341
295
|
let correctionCue: string | null = null;
|
|
342
296
|
try {
|
|
@@ -990,22 +944,8 @@ ${heartbeatChecklist}
|
|
|
990
944
|
} catch (logErr) {
|
|
991
945
|
logger?.warn?.(`[PD:RuntimeV2] Failed to emit activation observability event: ${String(logErr)}`);
|
|
992
946
|
}
|
|
993
|
-
|
|
994
|
-
// ── Set confirm-first directive state for gate enforcement ──
|
|
995
|
-
if (sessionId) {
|
|
996
|
-
const cfPrinciple = dedupedV2.find(
|
|
997
|
-
(p) =>
|
|
998
|
-
p.principleId === 'princ-mvp-acceptance-confirm-first' ||
|
|
999
|
-
(p.text.toLowerCase().includes('confirm requirements') &&
|
|
1000
|
-
p.text.toLowerCase().includes('owner approval')),
|
|
1001
|
-
);
|
|
1002
|
-
setConfirmFirstDirective(sessionId, !!cfPrinciple, cfPrinciple?.principleId);
|
|
1003
|
-
}
|
|
1004
947
|
} catch (e) {
|
|
1005
948
|
logger?.warn?.(`[PD:RuntimeV2] Failed to read Runtime V2 prompt activations: ${String(e)}`);
|
|
1006
|
-
if (sessionId) {
|
|
1007
|
-
resetConfirmFirst(sessionId);
|
|
1008
|
-
}
|
|
1009
949
|
}
|
|
1010
950
|
|
|
1011
951
|
// Build appendSystemContext with recency effect
|
package/src/index.ts
CHANGED
|
@@ -51,6 +51,7 @@ import { PDTaskService } from './core/pd-task-service.js';
|
|
|
51
51
|
import { CentralSyncService } from './service/central-sync-service.js';
|
|
52
52
|
import { ensureWorkspaceTemplates } from './core/init.js';
|
|
53
53
|
import { migrateDirectoryStructure } from './core/migration.js';
|
|
54
|
+
import { migrateStaleWorkspaceGuidance } from './core/workspace-guidance-migrator.js';
|
|
54
55
|
import { SystemLogger } from './core/system-logger.js';
|
|
55
56
|
import { PathResolver } from './core/path-resolver.js';
|
|
56
57
|
import { resolveCommandWorkspaceDir, resolveToolHookWorkspaceDirSafe } from './utils/workspace-resolver.js';
|
|
@@ -59,9 +60,7 @@ import type { WorkerProfile } from './core/model-deployment-registry.js';
|
|
|
59
60
|
import { validateWorkspaceDir } from './core/workspace-dir-validation.js';
|
|
60
61
|
import { resolveWorkspaceDirFromApi } from './core/path-resolver.js';
|
|
61
62
|
|
|
62
|
-
// Track
|
|
63
|
-
let workspaceInitialized = false;
|
|
64
|
-
// Track started evolution workers — one per workspace
|
|
63
|
+
// Track started workspaces — one-time init + evolution worker per workspace
|
|
65
64
|
const startedWorkspaces = new Set<string>();
|
|
66
65
|
|
|
67
66
|
const HOOK_WORKSPACE_RESOLUTION_NEXT_ACTION =
|
|
@@ -113,19 +112,16 @@ const plugin = {
|
|
|
113
112
|
return;
|
|
114
113
|
}
|
|
115
114
|
try {
|
|
116
|
-
if (!
|
|
115
|
+
if (!startedWorkspaces.has(workspaceDir)) {
|
|
116
|
+
startedWorkspaces.add(workspaceDir);
|
|
117
117
|
migrateDirectoryStructure(api, workspaceDir);
|
|
118
|
+
migrateStaleWorkspaceGuidance(api, workspaceDir);
|
|
118
119
|
ensureWorkspaceTemplates(api, workspaceDir, language);
|
|
119
120
|
SystemLogger.log(workspaceDir, 'SYSTEM_BOOT', `Principles Disciple online. Language: ${language}`);
|
|
120
|
-
workspaceInitialized = true;
|
|
121
|
-
}
|
|
122
121
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// per workspace so each agent's pain signals are processed independently.
|
|
127
|
-
if (!startedWorkspaces.has(workspaceDir)) {
|
|
128
|
-
startedWorkspaces.add(workspaceDir);
|
|
122
|
+
// ── Start EvolutionWorker for THIS workspace ──
|
|
123
|
+
// One EvolutionWorker per workspace so each agent's pain signals
|
|
124
|
+
// are processed independently.
|
|
129
125
|
EvolutionWorkerService.api = api;
|
|
130
126
|
EvolutionWorkerService.start({
|
|
131
127
|
config: api.config,
|
package/src/types/event-types.ts
CHANGED
package/src/utils/io.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
|
-
import { resolvePdPath } from '../core/paths.js';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Atomic file write — write to temp then rename to prevent partial writes on crash.
|
|
@@ -148,27 +147,6 @@ export function serializeKvLines(data: Record<string, any>): string {
|
|
|
148
147
|
return lines.join('\n');
|
|
149
148
|
}
|
|
150
149
|
|
|
151
|
-
export function planStatus(projectDir: string): string {
|
|
152
|
-
const planPath = resolvePdPath(projectDir, 'PLAN');
|
|
153
|
-
try {
|
|
154
|
-
if (!fs.existsSync(planPath)) return '';
|
|
155
|
-
const content = fs.readFileSync(planPath, 'utf8');
|
|
156
|
-
const lines = content.split('\n');
|
|
157
|
-
for (const line of lines) {
|
|
158
|
-
if (line.startsWith('STATUS:')) {
|
|
159
|
-
const parts = line.split(':');
|
|
160
|
-
if (parts.length > 1) {
|
|
161
|
-
return parts[1].trim().split(/\s+/)[0] || '';
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
/* eslint-disable @typescript-eslint/no-unused-vars -- Reason: Error is intentionally ignored for graceful degradation */
|
|
166
|
-
} catch (_e) {
|
|
167
|
-
// Ignore read errors
|
|
168
|
-
}
|
|
169
|
-
return '';
|
|
170
|
-
}
|
|
171
|
-
|
|
172
150
|
/**
|
|
173
151
|
* Normalize command arguments from PluginCommandContext.args.
|
|
174
152
|
* Handles string | string[] | undefined union by joining arrays with spaces.
|
|
@@ -17,7 +17,6 @@ As Principles Disciple, you must distinguish between two physical spaces:
|
|
|
17
17
|
Make decisions based on relative paths in the **Project Battlefield**:
|
|
18
18
|
|
|
19
19
|
- **Strategic Focus**: `./memory/STRATEGY.md`
|
|
20
|
-
- **Physical Plan**: `./PLAN.md`
|
|
21
20
|
- **Pain Signal**: Runtime V2 `PainSignalBridge` (`pd pain record` for manual trigger; `.state/.pain_flag` is legacy compatibility only)
|
|
22
21
|
- **System Capabilities**: `./.state/SYSTEM_CAPABILITIES.json`
|
|
23
22
|
|
|
@@ -166,12 +165,13 @@ On platforms that support reactions (Discord, Slack), use emoji reactions natura
|
|
|
166
165
|
You default to architect mode.
|
|
167
166
|
|
|
168
167
|
- **L1 (Direct Execution)**: Single-file tweaks, doc maintenance → do it directly
|
|
169
|
-
- **L2 (Delegation Protocol)**: Major changes →
|
|
168
|
+
- **L2 (Delegation Protocol)**: Major changes → recommended to describe the plan and get owner confirmation before executing
|
|
170
169
|
|
|
171
|
-
###
|
|
170
|
+
### Planning Guidance
|
|
172
171
|
|
|
173
|
-
-
|
|
174
|
-
-
|
|
172
|
+
- For complex tasks, consider drafting a plan document and getting owner approval before making large changes
|
|
173
|
+
- This is a behavioral suggestion, not a built-in gate — PD does not enforce plan-before-action by default
|
|
174
|
+
- If an owner-approved RuleHost rule enforces planning behavior, that rule takes effect automatically
|
|
175
175
|
- **Prevent pollution**: Never write execution details back to strategic documents
|
|
176
176
|
|
|
177
177
|
---
|
|
@@ -183,7 +183,7 @@ openclaw cron add --name "pd-grooming-daily" \
|
|
|
183
183
|
openclaw cron add --name "health-check" \
|
|
184
184
|
--every 4h \
|
|
185
185
|
--session main \
|
|
186
|
-
--system-event 'Health check: Verify core tools (rg, node, python) are available. Check if
|
|
186
|
+
--system-event 'Health check: Verify core tools (rg, node, python) are available. Check if workspace state matches actual progress.'
|
|
187
187
|
```
|
|
188
188
|
|
|
189
189
|
### 3. Strategy Alignment (Daily at 9 AM)
|