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.
Files changed (46) hide show
  1. package/INSTALL.md +1 -3
  2. package/openclaw.plugin.json +1 -1
  3. package/package.json +1 -1
  4. package/src/core/event-log.ts +0 -9
  5. package/src/core/migration.ts +0 -1
  6. package/src/core/path-resolver.ts +0 -1
  7. package/src/core/paths.ts +0 -1
  8. package/src/core/workspace-guidance-migrator.ts +179 -0
  9. package/src/hooks/gate-block-helper.ts +25 -20
  10. package/src/hooks/gate.ts +13 -61
  11. package/src/hooks/prompt.ts +1 -61
  12. package/src/index.ts +8 -12
  13. package/src/types/event-types.ts +0 -1
  14. package/src/utils/io.ts +0 -22
  15. package/templates/langs/en/core/AGENTS.md +5 -5
  16. package/templates/langs/en/core/BOOTSTRAP.md +1 -1
  17. package/templates/langs/en/principles/THINKING_OS.md +4 -3
  18. package/templates/langs/en/skills/admin/SKILL.md +2 -2
  19. package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  20. package/templates/langs/en/skills/evolve-task/SKILL.md +2 -2
  21. package/templates/langs/en/skills/pd-grooming/SKILL.md +1 -1
  22. package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -2
  23. package/templates/langs/en/skills/reflection/SKILL.md +2 -2
  24. package/templates/langs/en/skills/report/SKILL.md +1 -1
  25. package/templates/langs/zh/core/AGENTS.md +5 -5
  26. package/templates/langs/zh/core/BOOTSTRAP.md +1 -1
  27. package/templates/langs/zh/principles/THINKING_OS.md +4 -3
  28. package/templates/langs/zh/skills/admin/SKILL.md +2 -2
  29. package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  30. package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
  31. package/templates/langs/zh/skills/pd-grooming/SKILL.md +1 -1
  32. package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -2
  33. package/templates/langs/zh/skills/reflection/SKILL.md +2 -2
  34. package/templates/langs/zh/skills/report/SKILL.md +1 -1
  35. package/tests/core/migration.test.ts +7 -7
  36. package/tests/core/path-resolver.test.ts +1 -1
  37. package/tests/core/paths-refactor.test.ts +0 -22
  38. package/tests/core/workspace-context.test.ts +2 -2
  39. package/tests/core-anti-growth.test.ts +1 -1
  40. package/tests/hooks/confirm-first-removal.test.ts +188 -0
  41. package/tests/hooks/gate-no-path-write-tool.test.ts +172 -0
  42. package/src/core/confirm-first-gate.ts +0 -255
  43. package/templates/langs/en/skills/plan-script/SKILL.md +0 -32
  44. package/templates/langs/zh/skills/plan-script/SKILL.md +0 -32
  45. package/templates/workspace/PLAN.md +0 -2
  46. 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. Ensure `docs/PLAN.md` has `STATUS: READY`
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
 
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.73.0",
5
+ "version": "1.75.0",
6
6
  "activation": {
7
7
  "onCapabilities": [
8
8
  "hook"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.73.0",
3
+ "version": "1.75.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -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,
@@ -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
@@ -68,7 +68,6 @@ export const PD_FILES = {
68
68
  NOCTURNAL_EXPORTS_DIR: PD_DIRS.NOCTURNAL_EXPORTS,
69
69
  IMPL_CODE_DIR: PD_DIRS.IMPL_CODE_DIR,
70
70
 
71
- PLAN: 'PLAN.md',
72
71
  MEMORY_MD: 'MEMORY.md',
73
72
  HEARTBEAT: 'HEARTBEAT.md',
74
73
 
@@ -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: `[Principles Disciple] Security Gate Blocked this action.
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
- 1. Use the plan-script skill to create a PLAN.md:
163
- Invoke: skill:plan-script
164
-
165
- 2. Fill in the plan with:
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 * as fs from 'fs';
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
- planStatus: _getPlanStatus(ctx.workspaceDir),
113
- hasPlanFile: _hasPlanFile(ctx.workspaceDir),
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;
@@ -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, SqliteConfirmFirstStateStore, SqliteConnection } from '@principles/core/runtime-v2';
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 initialization to avoid repeated calls
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 (!workspaceInitialized) {
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
- // ── Start EvolutionWorker for THIS workspace ──
124
- // Each agent has its own heartbeat task. When before_prompt_build fires,
125
- // it fires for the current agent's workspaceDir. Start one EvolutionWorker
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,
@@ -23,7 +23,6 @@ export type {
23
23
  RuleHostAutoCorrectProposedEventData,
24
24
  RuleHostAutoCorrectAppliedEventData,
25
25
  RuntimeV2PromptActivationsInjectedEventData,
26
- RuntimeV2ConfirmFirstGateEventData,
27
26
  ToolCallStats,
28
27
  ErrorStats,
29
28
  PainStats,
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 → **MUST** update `./PLAN.md` and use `pd_spawn_agent` tool
168
+ - **L2 (Delegation Protocol)**: Major changes → recommended to describe the plan and get owner confirmation before executing
170
169
 
171
- ### State Machine Gating
170
+ ### Planning Guidance
172
171
 
173
- - **Single source of truth**: `./PLAN.md`
174
- - **Physical interception**: Plugin activated. If `PLAN.md` is not `READY` and you attempt to modify risk paths, calls will be blocked
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 PLAN.md state matches actual progress.'
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)