principles-disciple 1.34.2 → 1.36.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 (35) hide show
  1. package/.dependency-cruiser.json +19 -0
  2. package/openclaw.plugin.json +1 -1
  3. package/package.json +6 -3
  4. package/src/config/defaults/runtime.ts +100 -24
  5. package/src/core/correction-cue-learner.ts +23 -8
  6. package/src/core/event-log.ts +87 -20
  7. package/src/core/init.ts +2 -2
  8. package/src/core/nocturnal-candidate-scoring.ts +6 -6
  9. package/src/core/nocturnal-trinity-types.ts +94 -0
  10. package/src/core/nocturnal-trinity.ts +35 -99
  11. package/src/core/session-tracker.ts +7 -6
  12. package/src/core/system-logger.ts +104 -12
  13. package/src/core/workspace-dir-service.ts +40 -6
  14. package/src/core/workspace-dir-validation.ts +5 -37
  15. package/src/hooks/prompt.ts +3 -3
  16. package/src/hooks/trajectory-collector.ts +7 -7
  17. package/src/index.ts +8 -68
  18. package/src/service/central-sync-service.ts +3 -8
  19. package/src/service/correction-observer-workflow-manager.ts +2 -2
  20. package/src/service/evolution-worker.ts +13 -22
  21. package/src/service/keyword-optimization-service.ts +2 -2
  22. package/src/service/nocturnal-service.ts +62 -43
  23. package/src/service/subagent-workflow/correction-observer-types.ts +69 -0
  24. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +246 -0
  25. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +4 -4
  26. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +4 -4
  27. package/src/service/subagent-workflow/index.ts +13 -0
  28. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +2 -2
  29. package/src/service/subagent-workflow/types.ts +69 -3
  30. package/src/utils/shadow-fingerprint.ts +42 -0
  31. package/src/utils/workspace-resolver.ts +54 -0
  32. package/tests/core/correction-cue-learner.test.ts +345 -0
  33. package/tests/core/workspace-dir-validation.test.ts +1 -1
  34. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +3 -3
  35. package/vitest.config.ts +53 -6
package/src/index.ts CHANGED
@@ -16,9 +16,7 @@ import type {
16
16
  PluginHookSubagentSpawningResult,
17
17
  PluginHookSubagentContext,
18
18
  } from './openclaw-sdk.js';
19
- import * as crypto from 'crypto';
20
19
  import * as path from 'path';
21
- import type { WorkerProfile } from './core/model-deployment-registry.js';
22
20
  import { classifyTask } from './core/local-worker-routing.js';
23
21
  import { completeShadowObservation, recordShadowRouting } from './core/shadow-observation-registry.js';
24
22
  import { getCommandDescription } from './i18n/commands.js';
@@ -59,83 +57,25 @@ import { migrateDirectoryStructure } from './core/migration.js';
59
57
  import { SystemLogger } from './core/system-logger.js';
60
58
  import { createDeepReflectTool } from './tools/deep-reflect.js';
61
59
  import { createWritePainFlagTool } from './tools/write-pain-flag.js';
62
- import { PathResolver, resolveWorkspaceDirFromApi } from './core/path-resolver.js';
63
- import { validateWorkspaceDir } from './core/workspace-dir-validation.js';
64
- import { resolveRequiredWorkspaceDir, resolveWorkspaceDir, type WorkspaceResolutionContext } from './core/workspace-dir-service.js';
60
+ import { PathResolver } from './core/path-resolver.js';
65
61
  import { createPrinciplesConsoleRoute } from './http/principles-console-route.js';
66
62
  import { extractAgentIdFromSessionKey } from './utils/session-key.js';
63
+ import { resolveCommandWorkspaceDir, resolveToolHookWorkspaceDirSafe } from './utils/workspace-resolver.js';
64
+ import { computeRuntimeShadowTaskFingerprint, PD_LOCAL_PROFILES } from './utils/shadow-fingerprint.js';
65
+ import type { WorkerProfile } from './core/model-deployment-registry.js';
66
+ import { validateWorkspaceDir } from './core/workspace-dir-validation.js';
67
+ import { resolveWorkspaceDirFromApi } from './core/path-resolver.js';
68
+ import { resolveRequiredWorkspaceDir } from './core/workspace-dir-service.js';
67
69
 
68
70
  // Track initialization to avoid repeated calls
69
71
  let workspaceInitialized = false;
70
72
  // Track started evolution workers — one per workspace
71
73
  const startedWorkspaces = new Set<string>();
72
74
 
73
- /**
74
- * Resolve workspaceDir for slash commands.
75
- * Chain: ctx.workspaceDir → resolveWorkspaceDirFromApi (official OpenClaw API + env vars)
76
- *
77
- * CRITICAL: Throws if workspaceDir cannot be resolved. Silent failures are dangerous
78
- * because commands might operate on the wrong directory.
79
- */
80
- function resolveCommandWorkspaceDir(
81
- api: OpenClawPluginApi,
82
- ctx: { workspaceDir?: string },
83
- ): string {
84
- // 1. Direct from command context (most reliable — set by OpenClaw for current session)
85
- if (ctx.workspaceDir) {
86
- const issue = validateWorkspaceDir(ctx.workspaceDir);
87
- if (!issue) return ctx.workspaceDir;
88
- api.logger.error(`[PD:Command] ctx.workspaceDir="${ctx.workspaceDir}" is invalid: ${issue}`);
89
- }
90
-
91
- // 2. Official OpenClaw API → env vars → config file
92
- const resolved = resolveWorkspaceDirFromApi(api);
93
- if (resolved) return resolved;
94
-
95
- // CRITICAL FAILURE: Cannot determine workspace directory
96
- const errorMsg = `[PD:Command] CRITICAL: Cannot resolve workspace directory. ` +
97
- `ctx.workspaceDir="${ctx.workspaceDir}" is invalid, and all fallbacks failed. ` +
98
- `Commands will NOT execute to prevent data corruption.`;
99
- api.logger.error(errorMsg);
100
-
101
- throw new Error(errorMsg);
102
- }
103
-
104
75
  // Map from childSessionKey → shadowObservationId
105
76
  // Used to complete shadow observations when subagent ends
106
77
  const pendingShadowObservations = new Map<string, string>();
107
78
 
108
- // PD local worker profiles that are managed by the shadow routing policy
109
- const PD_LOCAL_PROFILES = new Set<WorkerProfile>(['local-reader', 'local-editor']);
110
-
111
- function computeRuntimeShadowTaskFingerprint(event: PluginHookSubagentSpawningEvent): string {
112
- const payload = {
113
- childSessionKey: event.childSessionKey,
114
- agentId: event.agentId,
115
- label: event.label ?? '',
116
- mode: event.mode,
117
- threadRequested: event.threadRequested,
118
- requesterChannel: event.requester?.channel ?? '',
119
- requesterThreadId: event.requester?.threadId ?? '',
120
- };
121
- return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 16);
122
- }
123
-
124
- function _resolveCommandWorkspaceDirStrict(
125
- api: OpenClawPluginApi,
126
- ctx: WorkspaceResolutionContext,
127
- ): string {
128
- return resolveRequiredWorkspaceDir(api, ctx, { source: 'command' });
129
- }
130
-
131
- function resolveToolHookWorkspaceDirSafe(
132
- ctx: WorkspaceResolutionContext,
133
- api: OpenClawPluginApi,
134
- source: string,
135
- ): string | undefined {
136
- return resolveWorkspaceDir(api, ctx, { source });
137
- }
138
-
139
79
  const plugin = {
140
80
  name: "Principles Disciple",
141
81
  description: "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
@@ -421,7 +361,7 @@ const plugin = {
421
361
 
422
362
  // ── Slash Commands ──
423
363
  // Register command with optional short alias
424
- // eslint-disable-next-line @typescript-eslint/max-params, @typescript-eslint/no-explicit-any
364
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
425
365
  const registerCommandWithAlias = (name: string, alias: string | null, desc: string, handler: any, opts?: { acceptsArgs?: boolean }) => {
426
366
  const base = {
427
367
  name,
@@ -7,18 +7,13 @@
7
7
 
8
8
  import type { OpenClawPluginService, OpenClawPluginServiceContext, PluginLogger } from '../openclaw-sdk.js';
9
9
  import { CentralDatabase } from './central-database.js';
10
+ import { WORKFLOW_TTL_MS } from '../config/defaults/runtime.js';
10
11
 
11
12
  let syncInterval: ReturnType<typeof setInterval> | null = null;
12
13
  let logger: PluginLogger | undefined = undefined;
13
14
  let centralDb: CentralDatabase | null = null;
14
15
 
15
- /**
16
- * Default sync interval: 5 minutes.
17
- * Can be overridden via config: intervals.central_sync_ms
18
- */
19
- const DEFAULT_SYNC_INTERVAL_MS = 5 * 60 * 1000;
20
-
21
- // eslint-disable-next-line complexity -- complexity 12, refactor candidate
16
+
22
17
  async function runSyncCycle(): Promise<void> {
23
18
  if (!centralDb) {
24
19
  logger?.warn?.('[PD:CentralSync] CentralDatabase not initialized, skipping sync');
@@ -51,7 +46,7 @@ export const CentralSyncService: OpenClawPluginService = {
51
46
  logger = ctxLogger;
52
47
 
53
48
  const { intervals } = config as { intervals?: { central_sync_ms?: number } };
54
- const intervalMs = intervals?.central_sync_ms ?? DEFAULT_SYNC_INTERVAL_MS;
49
+ const intervalMs = intervals?.central_sync_ms ?? WORKFLOW_TTL_MS;
55
50
 
56
51
  // Initialize CentralDatabase
57
52
  centralDb = new CentralDatabase();
@@ -22,11 +22,11 @@ import type {
22
22
  CorrectionObserverPayload,
23
23
  CorrectionObserverResult,
24
24
  } from './correction-observer-types.js';
25
+ import { WORKFLOW_TTL_MS } from '../config/defaults/runtime.js';
25
26
 
26
27
  const WORKFLOW_SESSION_PREFIX = 'agent:main:subagent:workflow-correction-';
27
28
 
28
29
  const DEFAULT_TIMEOUT_MS = 30_000;
29
- const DEFAULT_TTL_MS = 5 * 60 * 1000;
30
30
 
31
31
  // ── Options ─────────────────────────────────────────────────────────────────
32
32
 
@@ -162,7 +162,7 @@ export class CorrectionObserverWorkflowManager extends WorkflowManagerBase {
162
162
  workflowType: 'correction_observer',
163
163
  sessionPrefix: WORKFLOW_SESSION_PREFIX,
164
164
  defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
165
- defaultTtlMs: DEFAULT_TTL_MS,
165
+ defaultTtlMs: WORKFLOW_TTL_MS,
166
166
  });
167
167
  }
168
168
 
@@ -31,8 +31,8 @@ import {
31
31
  import { validateNocturnalSnapshotIngress } from '../core/nocturnal-snapshot-contract.js';
32
32
  import { isExpectedSubagentError } from './subagent-workflow/subagent-error-utils.js';
33
33
  import { readPainFlagContract } from '../core/pain.js';
34
- import { CorrectionObserverWorkflowManager, correctionObserverWorkflowSpec } from './correction-observer-workflow-manager.js';
35
- import type { CorrectionObserverPayload } from './correction-observer-types.js';
34
+ import { CorrectionObserverWorkflowManager, correctionObserverWorkflowSpec } from './subagent-workflow/correction-observer-workflow-manager.js';
35
+ import type { CorrectionObserverPayload } from './subagent-workflow/correction-observer-types.js';
36
36
  import { KeywordOptimizationService } from './keyword-optimization-service.js';
37
37
  import { TrajectoryRegistry } from '../core/trajectory.js';
38
38
  import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
@@ -40,15 +40,6 @@ import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
40
40
  const WORKFLOW_TTL_MS = 5 * 60 * 1000; // 5 minutes default TTL for helper workflows
41
41
  import { OpenClawTrinityRuntimeAdapter } from '../core/nocturnal-trinity.js';
42
42
 
43
- /**
44
- * Atomic file write — write to temp then rename to prevent partial writes on crash.
45
- */
46
- function atomicWriteFileSync(filePath: string, data: string): void {
47
- const tmpPath = filePath + '.tmp';
48
- fs.writeFileSync(tmpPath, data, 'utf8');
49
- fs.renameSync(tmpPath, filePath);
50
- }
51
-
52
43
  // ── Workflow Watchdog ────────────────────────────────────────────────────────
53
44
  // Detects stale/orphaned workflows, invalid results, and cleanup failures.
54
45
  // Runs every heartbeat cycle, catching bugs like:
@@ -692,7 +683,7 @@ function shouldSkipForDedup(
692
683
  * Load and migrate the evolution queue. Returns empty array if file doesn't exist.
693
684
  */
694
685
  function loadEvolutionQueue(queuePath: string): EvolutionQueueItem[] {
695
- // eslint-disable-next-line no-useless-assignment
686
+
696
687
  let rawQueue: RawQueueItem[] = [];
697
688
  try {
698
689
  rawQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
@@ -733,7 +724,7 @@ function enqueueNewSleepReflectionTask(
733
724
  recentPainContext,
734
725
  });
735
726
 
736
- atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
727
+ fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
737
728
  logger?.info?.(`[PD:EvolutionWorker] Enqueued sleep_reflection task ${taskId}`);
738
729
  }
739
730
 
@@ -878,7 +869,7 @@ async function doEnqueuePainTask(
878
869
  retryCount: 0, maxRetries: 3,
879
870
  });
880
871
 
881
- atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
872
+ fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
882
873
  fs.appendFileSync(painFlagPath, `\nstatus: queued\ntask_id: ${taskId}\n`, 'utf8');
883
874
  result.enqueued = true;
884
875
 
@@ -1667,7 +1658,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1667
1658
 
1668
1659
  // Write claimed state (includes any pain changes from above) and release lock
1669
1660
  if (queueChanged) {
1670
- atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
1661
+ fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
1671
1662
  }
1672
1663
  releaseLock();
1673
1664
  for (const sleepTask of sleepReflectionTasks) {
@@ -1921,7 +1912,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1921
1912
  freshQueue[idx] = sleepTask;
1922
1913
  }
1923
1914
  }
1924
- atomicWriteFileSync(queuePath, JSON.stringify(freshQueue, null, 2));
1915
+ fs.writeFileSync(queuePath, JSON.stringify(freshQueue, null, 2), 'utf8');
1925
1916
 
1926
1917
  // Log completions to EvolutionLogger
1927
1918
  for (const sleepTask of sleepReflectionTasks) {
@@ -2043,7 +2034,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
2043
2034
 
2044
2035
  if (parsedResult?.updated) {
2045
2036
  koService.applyResult(parsedResult);
2046
- await learner.recordOptimizationPerformed();
2037
+ learner.recordOptimizationPerformed();
2047
2038
  logger?.info?.(`[PD:EvolutionWorker] keyword_optimization applied mutations: ${parsedResult.summary}`);
2048
2039
  } else {
2049
2040
  logger?.info?.(`[PD:EvolutionWorker] keyword_optimization completed with no updates`);
@@ -2096,7 +2087,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
2096
2087
  freshQueue.push(koTask);
2097
2088
  }
2098
2089
  }
2099
- fs.writeFileSync(queuePath, JSON.stringify(freshQueue, null, 2));
2090
+ fs.writeFileSync(queuePath, JSON.stringify(freshQueue, null, 2), 'utf8');
2100
2091
  } catch (koResultErr) {
2101
2092
  logger?.warn?.(`[PD:EvolutionWorker] Failed to write keyword_optimization results: ${String(koResultErr)}`);
2102
2093
  } finally {
@@ -2106,7 +2097,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
2106
2097
  }
2107
2098
 
2108
2099
  if (queueChanged) {
2109
- atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
2100
+ fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
2110
2101
  }
2111
2102
 
2112
2103
  // Pipeline observability: log stage-level summary at end of cycle
@@ -2224,7 +2215,7 @@ export async function registerEvolutionTaskSession(
2224
2215
  if (!task.started_at) {
2225
2216
  task.started_at = new Date().toISOString();
2226
2217
  }
2227
- atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
2218
+ fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
2228
2219
  return true;
2229
2220
  } finally {
2230
2221
  releaseLock();
@@ -2264,7 +2255,7 @@ interface WorkerStatusReport {
2264
2255
  function writeWorkerStatus(stateDir: string, report: WorkerStatusReport): void {
2265
2256
  try {
2266
2257
  const statusPath = path.join(stateDir, 'worker-status.json');
2267
- atomicWriteFileSync(statusPath, JSON.stringify(report, null, 2));
2258
+ fs.writeFileSync(statusPath, JSON.stringify(report, null, 2), 'utf8');
2268
2259
  } catch (statusErr) {
2269
2260
  // Non-critical: worker-status.json is for monitoring, failure is acceptable
2270
2261
  // (no logger available in this standalone helper)
@@ -2295,7 +2286,7 @@ async function processEvolutionQueueWithResult(
2295
2286
  const purgeResult = purgeStaleFailedTasks(queue, logger);
2296
2287
  if (purgeResult.purged > 0) {
2297
2288
  // Write back the cleaned queue
2298
- atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
2289
+ fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
2299
2290
  }
2300
2291
 
2301
2292
  queueResult.total = queue.length;
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
10
- import type { CorrectionObserverResult } from './correction-observer-types.js';
10
+ import type { CorrectionObserverResult } from './subagent-workflow/correction-observer-types.js';
11
11
  import type { PluginLogger } from '../openclaw-sdk.js';
12
12
  import { TrajectoryRegistry } from '../core/trajectory.js';
13
13
 
@@ -137,4 +137,4 @@ export type TrajectoryHistoryEntry = {
137
137
  };
138
138
 
139
139
  /** Re-export CorrectionObserverPayload for convenience */
140
- export type { CorrectionObserverPayload } from './correction-observer-types.js';
140
+ export type { CorrectionObserverPayload } from './subagent-workflow/correction-observer-types.js';