principles-disciple 1.41.0 → 1.43.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 (95) hide show
  1. package/.planning/codebase/ARCHITECTURE.md +157 -0
  2. package/.planning/codebase/CONCERNS.md +145 -0
  3. package/.planning/codebase/CONVENTIONS.md +148 -0
  4. package/.planning/codebase/INTEGRATIONS.md +81 -0
  5. package/.planning/codebase/STACK.md +87 -0
  6. package/.planning/codebase/STRUCTURE.md +193 -0
  7. package/.planning/codebase/TESTING.md +243 -0
  8. package/openclaw.plugin.json +1 -1
  9. package/package.json +1 -1
  10. package/src/commands/archive-impl.ts +5 -3
  11. package/src/commands/context.ts +1 -0
  12. package/src/commands/disable-impl.ts +1 -1
  13. package/src/commands/evolution-status.ts +2 -2
  14. package/src/commands/pain.ts +12 -5
  15. package/src/commands/principle-rollback.ts +1 -1
  16. package/src/commands/promote-impl.ts +13 -7
  17. package/src/commands/rollback.ts +10 -4
  18. package/src/commands/samples.ts +1 -1
  19. package/src/commands/thinking-os.ts +1 -0
  20. package/src/commands/workflow-debug.ts +1 -1
  21. package/src/core/config.ts +1 -0
  22. package/src/core/dictionary.ts +1 -0
  23. package/src/core/event-log.ts +8 -6
  24. package/src/core/evolution-types.ts +33 -1
  25. package/src/core/external-training-contract.ts +1 -1
  26. package/src/core/merge-gate-audit.ts +3 -3
  27. package/src/core/nocturnal-arbiter.ts +1 -1
  28. package/src/core/nocturnal-compliance.ts +21 -21
  29. package/src/core/nocturnal-executability.ts +1 -1
  30. package/src/core/nocturnal-reasoning-deriver.ts +4 -4
  31. package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
  32. package/src/core/nocturnal-snapshot-contract.ts +1 -1
  33. package/src/core/pain-context-extractor.ts +2 -2
  34. package/src/core/path-resolver.ts +1 -0
  35. package/src/core/pd-task-reconciler.ts +1 -0
  36. package/src/core/pd-task-service.ts +1 -1
  37. package/src/core/pd-task-store.ts +1 -0
  38. package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
  39. package/src/core/principle-internalization/principle-lifecycle-service.ts +1 -1
  40. package/src/core/principle-training-state.ts +2 -2
  41. package/src/core/principle-tree-migration.ts +1 -1
  42. package/src/core/replay-engine.ts +1 -0
  43. package/src/core/risk-calculator.ts +2 -1
  44. package/src/core/rule-host.ts +1 -1
  45. package/src/core/session-tracker.ts +1 -0
  46. package/src/core/shadow-observation-registry.ts +1 -1
  47. package/src/core/thinking-models.ts +1 -1
  48. package/src/core/thinking-os-parser.ts +1 -1
  49. package/src/core/trajectory.ts +2 -0
  50. package/src/hooks/bash-risk.ts +2 -2
  51. package/src/hooks/edit-verification.ts +3 -3
  52. package/src/hooks/gate.ts +8 -8
  53. package/src/hooks/gfi-gate.ts +2 -2
  54. package/src/hooks/lifecycle-routing.ts +1 -1
  55. package/src/hooks/message-sanitize.ts +18 -5
  56. package/src/hooks/pain.ts +2 -2
  57. package/src/hooks/progressive-trust-gate.ts +3 -3
  58. package/src/hooks/prompt.ts +17 -4
  59. package/src/hooks/subagent.ts +2 -3
  60. package/src/hooks/thinking-checkpoint.ts +1 -1
  61. package/src/http/principles-console-route.ts +21 -4
  62. package/src/service/central-database.ts +3 -2
  63. package/src/service/central-health-service.ts +2 -1
  64. package/src/service/central-overview-service.ts +3 -2
  65. package/src/service/control-ui-query-service.ts +2 -2
  66. package/src/service/event-log-auditor.ts +2 -2
  67. package/src/service/evolution-query-service.ts +1 -1
  68. package/src/service/evolution-worker.ts +96 -370
  69. package/src/service/health-query-service.ts +11 -10
  70. package/src/service/monitoring-query-service.ts +4 -4
  71. package/src/service/nocturnal-target-selector.ts +2 -2
  72. package/src/service/queue-io.ts +375 -0
  73. package/src/service/queue-migration.ts +122 -0
  74. package/src/service/runtime-summary-service.ts +1 -1
  75. package/src/service/sleep-cycle.ts +157 -0
  76. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +1 -0
  77. package/src/service/subagent-workflow/runtime-direct-driver.ts +1 -1
  78. package/src/service/subagent-workflow/subagent-error-utils.ts +1 -1
  79. package/src/service/subagent-workflow/workflow-store.ts +3 -2
  80. package/src/service/workflow-watchdog.ts +168 -0
  81. package/src/tools/critique-prompt.ts +1 -1
  82. package/src/tools/deep-reflect.ts +22 -11
  83. package/src/tools/model-index.ts +1 -1
  84. package/src/types/event-payload.ts +80 -0
  85. package/src/types/queue.ts +70 -0
  86. package/src/utils/file-lock.ts +2 -2
  87. package/src/utils/io.ts +11 -3
  88. package/tests/core/evolution-migration.test.ts +325 -1
  89. package/tests/core/queue-purge.test.ts +337 -0
  90. package/tests/fixtures/legacy-queue-v1.json +74 -0
  91. package/tests/queue/async-lock.test.ts +200 -0
  92. package/tests/service/evolution-worker.queue.test.ts +296 -0
  93. package/tests/service/queue-io.test.ts +229 -0
  94. package/tests/service/queue-migration.test.ts +147 -0
  95. package/tests/service/workflow-watchdog.test.ts +372 -0
@@ -6,6 +6,21 @@ const INTERNAL_TAG_PATTERNS = [
6
6
  /<empathy\s+[^>]*\/?>(?:<\/empathy>)?/gi,
7
7
  ];
8
8
 
9
+ /**
10
+ * Type predicate: true if msg is an assistant message with content.
11
+ * Used for safe narrowing after spread operations on message union.
12
+ */
13
+ function isAssistantMessageWithContent(
14
+ msg: unknown
15
+ ): msg is { role: 'assistant'; content: string } {
16
+ return (
17
+ typeof msg === 'object' &&
18
+ msg !== null &&
19
+ (msg as { role?: string }).role === 'assistant' &&
20
+ typeof (msg as { content?: unknown }).content === 'string'
21
+ );
22
+ }
23
+
9
24
  export function sanitizeAssistantText(text: string): string {
10
25
  let result = text;
11
26
  for (const pattern of INTERNAL_TAG_PATTERNS) {
@@ -23,11 +38,10 @@ export function handleBeforeMessageWrite(
23
38
  const msg = event.message as { role?: string; content?: unknown } | undefined;
24
39
  if (!msg || msg.role !== 'assistant') return;
25
40
 
26
- if (typeof msg.content === 'string') {
41
+ if (isAssistantMessageWithContent(msg)) {
27
42
  const sanitized = sanitizeAssistantText(msg.content);
28
43
  if (sanitized !== msg.content) {
29
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: message content is dynamically modified, type preserved from event.message union
30
- return { message: { ...msg, content: sanitized } as any };
44
+ return { message: { ...msg, content: sanitized } };
31
45
  }
32
46
  return;
33
47
  }
@@ -39,8 +53,7 @@ export function handleBeforeMessageWrite(
39
53
  }
40
54
  return part;
41
55
  });
42
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: message content is dynamically modified, type preserved from event.message union
43
- return { message: { ...msg, content: next } as any };
56
+ return { message: { ...msg, content: next } };
44
57
  }
45
58
 
46
59
  return;
package/src/hooks/pain.ts CHANGED
@@ -131,7 +131,7 @@ export function handleAfterToolCall(
131
131
 
132
132
  // ── Trust Engine: Record failure ──
133
133
 
134
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
134
+
135
135
  const errorType = extractErrorType(event.error || errorText);
136
136
  const filePath = params.file_path || params.path || params.file;
137
137
  const relPath = typeof filePath === 'string' ? normalizePath(filePath, effectiveWorkspaceDir) : 'unknown';
@@ -194,7 +194,7 @@ export function handleAfterToolCall(
194
194
  const session = getSession(sessionId);
195
195
  const toolFailureGfi = session?.gfiBySource?.tool_failure || 0;
196
196
 
197
- // eslint-disable-next-line @typescript-eslint/init-declarations
197
+
198
198
  let resetState: SessionState;
199
199
  if (toolFailureGfi > 0) {
200
200
  // Reduce tool_failure source by 50% (relief from successful tool execution)
@@ -87,7 +87,7 @@ export function buildEvolutionGateReason(
87
87
  * Internal helper to call the shared block helper with progressive-trust-gate source tag.
88
88
  */
89
89
 
90
- // eslint-disable-next-line @typescript-eslint/max-params
90
+
91
91
  function block(
92
92
  filePath: string,
93
93
  reason: string,
@@ -121,7 +121,7 @@ function block(
121
121
  * @returns PluginHookBeforeToolCallResult to block, or undefined to allow
122
122
  */
123
123
 
124
- // eslint-disable-next-line @typescript-eslint/max-params
124
+
125
125
  export function checkProgressiveTrustGate(
126
126
  event: PluginHookBeforeToolCallEvent,
127
127
  wctx: WorkspaceContext,
@@ -154,7 +154,7 @@ export function checkProgressiveTrustGate(
154
154
 
155
155
  const currentTier = epDecision.currentTier ?? 1;
156
156
 
157
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
157
+
158
158
  const tierName = getTierName(currentTier);
159
159
 
160
160
  logger.info?.(`[PD_GATE] EP Gate: Tier ${currentTier} (${tierName}), Tool: ${event.toolName}, Risk: ${risky}, Allowed: ${epDecision.allowed}`);
@@ -1,3 +1,5 @@
1
+
2
+
1
3
  import * as fs from 'fs';
2
4
  import * as path from 'path';
3
5
  import type { PluginHookBeforePromptBuildEvent, PluginHookAgentContext, PluginHookBeforePromptBuildResult, PluginLogger, OpenClawPluginApi } from '../openclaw-sdk.js';
@@ -20,6 +22,17 @@ import {
20
22
  } from '../core/empathy-keyword-matcher.js';
21
23
  import { severityToPenalty, DEFAULT_EMPATHY_KEYWORD_CONFIG } from '../core/empathy-types.js';
22
24
  import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
25
+ import type { PluginRuntimeSubagent } from '../service/subagent-workflow/runtime-direct-driver.js';
26
+
27
+ /**
28
+ * Type assertion: OpenClaw SDK subagent -> workflow manager subagent type.
29
+ * Both types are structurally identical but come from different import paths.
30
+ */
31
+ function toWorkflowSubagent(
32
+ subagent: NonNullable<OpenClawPluginApi['runtime']>['subagent']
33
+ ): PluginRuntimeSubagent {
34
+ return subagent as unknown as PluginRuntimeSubagent;
35
+ }
23
36
 
24
37
  // ---------------------------------------------------------------------------
25
38
  // Static file cache — avoids re-reading rarely-changing files every message
@@ -590,8 +603,8 @@ The empathy observer subagent handles pain detection independently.
590
603
  const empathyManager = new EmpathyObserverWorkflowManager({
591
604
  workspaceDir,
592
605
  logger: api.logger ?? console,
593
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: runtimeSubagent has structurally compatible shape but differs from workflow manager's subagent type
594
- subagent: runtimeSubagent as any,
606
+
607
+ subagent: toWorkflowSubagent(runtimeSubagent),
595
608
  });
596
609
  empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
597
610
  parentSessionId: sessionId,
@@ -626,8 +639,8 @@ The empathy observer subagent handles pain detection independently.
626
639
  const empathyManager = new EmpathyObserverWorkflowManager({
627
640
  workspaceDir,
628
641
  logger: api.logger ?? console,
629
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: api.runtime.subagent has structurally compatible shape but differs from workflow manager's subagent type
630
- subagent: api.runtime.subagent as any,
642
+
643
+ subagent: toWorkflowSubagent(api.runtime.subagent),
631
644
  });
632
645
 
633
646
  empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
@@ -14,7 +14,7 @@ import type { WorkflowManager } from '../service/subagent-workflow/types.js';
14
14
  * Used by the subagent_ended hook to dispatch lifecycle recovery to the right manager.
15
15
  */
16
16
 
17
- // eslint-disable-next-line @typescript-eslint/max-params
17
+
18
18
  function createWorkflowManagerForType(
19
19
  workflowType: string,
20
20
  workspaceDir: string,
@@ -25,9 +25,8 @@ function createWorkflowManagerForType(
25
25
  info: (m: string) => logger.info(String(m)),
26
26
  warn: (m: string) => logger.warn(String(m)),
27
27
  error: (m: string) => logger.error(String(m)),
28
-
29
28
  debug: () => { /* no-op */ },
30
- } as unknown as PluginLogger;
29
+ };
31
30
 
32
31
  switch (workflowType) {
33
32
  case 'empathy-observer':
@@ -41,7 +41,7 @@ export interface ThinkingCheckpointConfig {
41
41
  * @returns Block result if thinking required, undefined otherwise
42
42
  */
43
43
 
44
- // eslint-disable-next-line @typescript-eslint/max-params
44
+
45
45
  export function checkThinkingCheckpoint(
46
46
  event: PluginHookBeforeToolCallEvent,
47
47
  config: ThinkingCheckpointConfig,
@@ -1,3 +1,4 @@
1
+ import * as crypto from 'crypto';
1
2
  import fs from 'fs';
2
3
  import path from 'path';
3
4
  import type { IncomingMessage, ServerResponse } from 'node:http';
@@ -96,7 +97,7 @@ function createService(api: OpenClawPluginApi): ControlUiQueryService {
96
97
  }
97
98
 
98
99
 
99
- // eslint-disable-next-line @typescript-eslint/max-params
100
+
100
101
  function handleApiRoute(
101
102
  api: OpenClawPluginApi,
102
103
  pathname: string,
@@ -105,13 +106,13 @@ function handleApiRoute(
105
106
  ): Promise<boolean> | boolean {
106
107
  // Check authentication for API routes
107
108
 
108
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
109
+
109
110
  if (!validateGatewayAuth(req)) {
110
111
  json(res, 401, { error: 'unauthorized', message: 'Valid Gateway token required.' });
111
112
  return true;
112
113
  }
113
114
 
114
- // eslint-disable-next-line @typescript-eslint/init-declarations
115
+
115
116
  let service: ControlUiQueryService;
116
117
  try {
117
118
  service = createService(api);
@@ -566,7 +567,23 @@ function validateGatewayAuth(req: IncomingMessage): boolean {
566
567
  const authHeader = (req.headers?.authorization as string) || '';
567
568
  const tokenMatch = /^Bearer\s+(.+)$/i.exec(authHeader);
568
569
  const providedToken = tokenMatch?.[1];
569
- return providedToken === gatewayToken;
570
+
571
+ if (!providedToken) {
572
+ return false;
573
+ }
574
+
575
+ // Constant-time comparison to prevent timing attacks (per D-07)
576
+ // Use Buffer comparison — both tokens must be same length for timingSafeEqual
577
+ const providedBuffer = Buffer.from(providedToken, 'utf8');
578
+ const expectedBuffer = Buffer.from(gatewayToken, 'utf8');
579
+
580
+ if (providedBuffer.length !== expectedBuffer.length) {
581
+ // Length mismatch — fail fast but without timing leak
582
+ // Return false immediately rather than letting timingSafeEqual throw
583
+ return false;
584
+ }
585
+
586
+ return crypto.timingSafeEqual(providedBuffer, expectedBuffer);
570
587
  }
571
588
 
572
589
  /**
@@ -1,3 +1,4 @@
1
+
1
2
  import Database from 'better-sqlite3';
2
3
  import fs from 'fs';
3
4
  import path from 'path';
@@ -200,7 +201,7 @@ export class CentralDatabase {
200
201
  /**
201
202
  * Sync data from a single workspace into the central database
202
203
  */
203
- // eslint-disable-next-line complexity -- complexity 12, refactor candidate
204
+
204
205
  syncWorkspace(workspaceName: string): number {
205
206
  const workspace = this.workspaces.find(w => w.name === workspaceName);
206
207
  if (!workspace) {
@@ -714,7 +715,7 @@ export class CentralDatabase {
714
715
  syncEnabled: c.sync_enabled === 1,
715
716
  }));
716
717
  }
717
- // eslint-disable-next-line complexity -- complexity 12, refactor candidate
718
+
718
719
 
719
720
  updateWorkspaceConfig(
720
721
  workspaceName: string,
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-console */
1
2
  import { getCentralDatabase } from './central-database.js';
2
3
  import { HealthQueryService } from './health-query-service.js';
3
4
 
@@ -18,7 +19,7 @@ export interface CentralHealthResponse {
18
19
  */
19
20
  export class CentralHealthService {
20
21
 
21
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
22
+
22
23
  getAllWorkspaceHealth(): CentralHealthResponse {
23
24
  const centralDb = getCentralDatabase();
24
25
  const workspaces: WorkspaceHealthEntry[] = [];
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-console */
1
2
  import { getCentralDatabase, type CentralDatabase } from './central-database.js';
2
3
  import { getThinkingModelDefinitions } from '../core/thinking-models.js';
3
4
  import type { OverviewResponse } from './control-ui-query-service.js';
@@ -24,7 +25,7 @@ export class CentralOverviewService {
24
25
  }
25
26
 
26
27
 
27
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
28
+
28
29
  dispose(): void {
29
30
  // Do NOT dispose centralDb — it's a singleton shared across all requests.
30
31
  // Individual services that open per-request connections (e.g. HealthQueryService)
@@ -62,7 +63,7 @@ export class CentralOverviewService {
62
63
 
63
64
  // D-06: sampleQueue.counters from aggregated_correction_samples GROUP BY review_status
64
65
 
65
- // eslint-disable-next-line no-useless-assignment
66
+
66
67
  let sampleCounters: Record<string, number> = {};
67
68
  try {
68
69
  sampleCounters = this.centralDb.getSampleCountersByStatus();
@@ -255,7 +255,7 @@ export class ControlUiQueryService {
255
255
  this.uiDb.dispose();
256
256
  }
257
257
 
258
- // eslint-disable-next-line complexity -- complexity 14, refactor candidate
258
+
259
259
  getOverview(days = 30): OverviewResponse {
260
260
  const stats = this.trajectory.getDataStats();
261
261
  const regressionRows = this.uiDb.all<{
@@ -400,7 +400,7 @@ export class ControlUiQueryService {
400
400
  };
401
401
  }
402
402
 
403
- // eslint-disable-next-line complexity -- complexity 13, refactor candidate
403
+
404
404
  listSamples(filters: SampleListFilters = {}): SamplesResponse {
405
405
  const page = Math.max(1, Number(filters.page ?? 1));
406
406
  const pageSize = clampPageSize(filters.pageSize);
@@ -137,7 +137,7 @@ function countAllHooks(filePath: string): Record<string, number> {
137
137
  * @param openclawDir - Base OpenClaw directory (e.g., ~/.openclaw)
138
138
  * @param expectedToolHooks - Hook names that should appear in the primary workspace
139
139
  */
140
- // eslint-disable-next-line complexity -- complexity 11, slightly over threshold
140
+
141
141
  export async function auditEventLogs(
142
142
  openclawDir: string,
143
143
  expectedToolHooks: string[] = ['before_tool_call', 'after_tool_call'],
@@ -210,7 +210,7 @@ export async function auditEventLogs(
210
210
  /**
211
211
  * Format audit report for display.
212
212
  */
213
- // eslint-disable-next-line complexity -- complexity 13, refactor candidate
213
+
214
214
  export function formatAuditReport(report: AuditReport): string {
215
215
  const lines: string[] = [];
216
216
 
@@ -155,7 +155,7 @@ export class EvolutionQueryService {
155
155
  * 注意:不关闭 trajectory,因为它是单例由 TrajectoryRegistry 管理
156
156
  */
157
157
 
158
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
158
+
159
159
  dispose(): void {
160
160
  // EvolutionQueryService 不拥有 trajectory,所以不关闭它
161
161
  // trajectory 是由 TrajectoryRegistry 管理的单例