principles-disciple 1.16.0 → 1.17.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 (129) hide show
  1. package/README.md +13 -5
  2. package/openclaw.plugin.json +4 -4
  3. package/package.json +1 -1
  4. package/src/commands/archive-impl.ts +3 -3
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +3 -3
  7. package/src/commands/disable-impl.ts +1 -1
  8. package/src/commands/evolution-status.ts +2 -2
  9. package/src/commands/focus.ts +2 -2
  10. package/src/commands/nocturnal-train.ts +6 -6
  11. package/src/commands/pain.ts +4 -4
  12. package/src/commands/pd-reflect.ts +87 -0
  13. package/src/commands/rollback-impl.ts +4 -4
  14. package/src/commands/rollback.ts +2 -2
  15. package/src/commands/samples.ts +2 -2
  16. package/src/commands/workflow-debug.ts +1 -1
  17. package/src/config/errors.ts +1 -1
  18. package/src/core/adaptive-thresholds.ts +1 -1
  19. package/src/core/code-implementation-storage.ts +2 -2
  20. package/src/core/config.ts +1 -1
  21. package/src/core/diagnostician-task-store.ts +2 -2
  22. package/src/core/empathy-keyword-matcher.ts +3 -3
  23. package/src/core/event-log.ts +5 -5
  24. package/src/core/evolution-engine.ts +4 -4
  25. package/src/core/evolution-logger.ts +1 -1
  26. package/src/core/evolution-reducer.ts +3 -3
  27. package/src/core/evolution-types.ts +5 -5
  28. package/src/core/external-training-contract.ts +1 -1
  29. package/src/core/focus-history.ts +14 -14
  30. package/src/core/hygiene/tracker.ts +1 -1
  31. package/src/core/init.ts +2 -2
  32. package/src/core/model-deployment-registry.ts +2 -2
  33. package/src/core/model-training-registry.ts +2 -2
  34. package/src/core/nocturnal-arbiter.ts +1 -1
  35. package/src/core/nocturnal-artificer.ts +2 -2
  36. package/src/core/nocturnal-candidate-scoring.ts +2 -2
  37. package/src/core/nocturnal-compliance.ts +3 -3
  38. package/src/core/nocturnal-dataset.ts +3 -3
  39. package/src/core/nocturnal-export.ts +4 -4
  40. package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
  41. package/src/core/nocturnal-snapshot-contract.ts +112 -0
  42. package/src/core/nocturnal-trajectory-extractor.ts +7 -5
  43. package/src/core/nocturnal-trinity.ts +27 -28
  44. package/src/core/pain-context-extractor.ts +3 -3
  45. package/src/core/pain.ts +124 -11
  46. package/src/core/path-resolver.ts +4 -4
  47. package/src/core/pd-task-reconciler.ts +10 -10
  48. package/src/core/pd-task-service.ts +1 -1
  49. package/src/core/pd-task-store.ts +1 -1
  50. package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
  51. package/src/core/principle-training-state.ts +2 -2
  52. package/src/core/principle-tree-ledger.ts +7 -7
  53. package/src/core/promotion-gate.ts +9 -9
  54. package/src/core/replay-engine.ts +12 -12
  55. package/src/core/risk-calculator.ts +1 -1
  56. package/src/core/rule-host-types.ts +2 -2
  57. package/src/core/rule-host.ts +5 -5
  58. package/src/core/schema/db-types.ts +1 -1
  59. package/src/core/schema/schema-definitions.ts +1 -1
  60. package/src/core/session-tracker.ts +96 -4
  61. package/src/core/shadow-observation-registry.ts +3 -3
  62. package/src/core/system-logger.ts +2 -2
  63. package/src/core/thinking-os-parser.ts +1 -1
  64. package/src/core/training-program.ts +2 -2
  65. package/src/core/trajectory.ts +8 -8
  66. package/src/core/workspace-context.ts +2 -2
  67. package/src/core/workspace-dir-service.ts +85 -0
  68. package/src/core/workspace-dir-validation.ts +30 -107
  69. package/src/hooks/bash-risk.ts +3 -3
  70. package/src/hooks/edit-verification.ts +4 -4
  71. package/src/hooks/gate-block-helper.ts +4 -4
  72. package/src/hooks/gate.ts +10 -10
  73. package/src/hooks/gfi-gate.ts +7 -7
  74. package/src/hooks/lifecycle.ts +2 -2
  75. package/src/hooks/llm.ts +1 -1
  76. package/src/hooks/pain.ts +25 -5
  77. package/src/hooks/progressive-trust-gate.ts +7 -7
  78. package/src/hooks/prompt.ts +24 -5
  79. package/src/hooks/subagent.ts +2 -2
  80. package/src/hooks/thinking-checkpoint.ts +2 -2
  81. package/src/hooks/trajectory-collector.ts +1 -1
  82. package/src/http/principles-console-route.ts +14 -6
  83. package/src/i18n/commands.ts +4 -0
  84. package/src/index.ts +181 -185
  85. package/src/service/central-health-service.ts +1 -1
  86. package/src/service/central-overview-service.ts +3 -3
  87. package/src/service/evolution-query-service.ts +1 -1
  88. package/src/service/evolution-worker.ts +209 -104
  89. package/src/service/health-query-service.ts +27 -17
  90. package/src/service/monitoring-query-service.ts +3 -3
  91. package/src/service/nocturnal-runtime.ts +4 -4
  92. package/src/service/nocturnal-service.ts +40 -23
  93. package/src/service/nocturnal-target-selector.ts +2 -2
  94. package/src/service/runtime-summary-service.ts +1 -1
  95. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
  96. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
  97. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
  98. package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
  99. package/src/service/subagent-workflow/types.ts +4 -4
  100. package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
  101. package/src/service/subagent-workflow/workflow-store.ts +2 -2
  102. package/src/tools/critique-prompt.ts +2 -3
  103. package/src/tools/deep-reflect.ts +17 -16
  104. package/src/tools/model-index.ts +1 -1
  105. package/src/utils/file-lock.ts +1 -1
  106. package/src/utils/io.ts +7 -2
  107. package/src/utils/nlp.ts +1 -1
  108. package/src/utils/plugin-logger.ts +2 -2
  109. package/src/utils/retry.ts +3 -2
  110. package/src/utils/subagent-probe.ts +20 -33
  111. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
  112. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
  113. package/templates/pain_settings.json +1 -1
  114. package/tests/build-artifacts.test.ts +4 -58
  115. package/tests/commands/pd-reflect.test.ts +49 -0
  116. package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
  117. package/tests/core/pain-auto-repair.test.ts +96 -0
  118. package/tests/core/pain-integration.test.ts +483 -0
  119. package/tests/core/pain.test.ts +5 -4
  120. package/tests/core/workspace-dir-service.test.ts +68 -0
  121. package/tests/core/workspace-dir-validation.test.ts +56 -192
  122. package/tests/hooks/pain.test.ts +20 -0
  123. package/tests/http/principles-console-route.test.ts +42 -20
  124. package/tests/integration/empathy-workflow-integration.test.ts +1 -2
  125. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
  126. package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
  127. package/tests/service/evolution-worker.nocturnal.test.ts +562 -6
  128. package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
  129. package/tests/utils/subagent-probe.test.ts +32 -0
@@ -45,9 +45,8 @@ import {
45
45
  // ---------------------------------------------------------------------------
46
46
  // Embedded Role Prompts
47
47
  // ---------------------------------------------------------------------------
48
- // These prompts are embedded at build time to eliminate file system dependency.
49
- // Previously loaded from src/agents/*.md at runtime fragile because esbuild
50
- // did not copy the agents/ directory into the bundle.
48
+ // These prompts are embedded at build time. The agents/ directory was removed
49
+ // to eliminate fragile runtime file dependencies on the file system.
51
50
 
52
51
  const NOCTURNAL_DREAMER_PROMPT = `# Nocturnal Dreamer — Candidate Generation
53
52
 
@@ -276,7 +275,7 @@ If you cannot synthesize an artifact:
276
275
  * Interface for Trinity stage invocation.
277
276
  * Implementations can use real subagent runtimes or stubs.
278
277
  */
279
- /* eslint-disable no-unused-vars -- Reason: interface method params are type signatures, not implementations */
278
+
280
279
  export interface TrinityRuntimeAdapter {
281
280
  /**
282
281
  * Invoke the Dreamer stage.
@@ -327,7 +326,7 @@ export interface TrinityRuntimeAdapter {
327
326
  */
328
327
  close?(): Promise<void>;
329
328
  }
330
- /* eslint-enable no-unused-vars */
329
+
331
330
 
332
331
  // ---------------------------------------------------------------------------
333
332
  // OpenClaw Runtime Adapter
@@ -339,7 +338,7 @@ export interface TrinityRuntimeAdapter {
339
338
  * Does NOT depend on OpenClaw internals.
340
339
  */
341
340
  export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
342
- /* eslint-disable no-unused-vars -- Reason: type-level function parameter names are documentation */
341
+
343
342
  private readonly api: {
344
343
  runtime: {
345
344
  subagent: {
@@ -366,7 +365,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
366
365
  };
367
366
  };
368
367
  };
369
- /* eslint-enable no-unused-vars */
368
+
370
369
 
371
370
  private readonly stageTimeoutMs: number;
372
371
 
@@ -472,14 +471,14 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
472
471
  }
473
472
  }
474
473
 
475
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: scribe invocation requires all context parameters - refactoring would break API
474
+
476
475
  async invokeScribe(
477
476
  dreamerOutput: DreamerOutput,
478
477
  philosopherOutput: PhilosopherOutput,
479
478
  snapshot: NocturnalSessionSnapshot,
480
479
  principleId: string,
481
480
  telemetry: TrinityTelemetry,
482
- // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: interface requires this param but implementation doesn't use it
481
+
483
482
  _config: TrinityConfig
484
483
  ): Promise<TrinityDraftArtifact | null> {
485
484
  const sessionKey = `agent:main:subagent:ne-scribe-${randomUUID()}`;
@@ -519,7 +518,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
519
518
  }
520
519
  }
521
520
 
522
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: interface-required close() with no-op implementation
521
+
523
522
  async close(): Promise<void> {
524
523
  // Nothing to clean up in this implementation
525
524
  }
@@ -528,7 +527,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
528
527
  // Private Helper Methods
529
528
  // ---------------------------------------------------------------------------
530
529
 
531
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: pure utility function that doesn't need instance state
530
+
532
531
  private extractAssistantText(
533
532
  messages: { role: string; text?: string; content?: string }[]
534
533
  ): string {
@@ -541,7 +540,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
541
540
  return '';
542
541
  }
543
542
 
544
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: pure utility function that doesn't need instance state
543
+
545
544
  private buildDreamerPrompt(
546
545
  snapshot: NocturnalSessionSnapshot,
547
546
  principleId: string,
@@ -551,18 +550,18 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
551
550
 
552
551
  Session Snapshot:
553
552
  - Session ID: ${snapshot.sessionId}
554
- - Assistant Turns: ${snapshot.stats.totalAssistantTurns}
555
- - Tool Calls: ${snapshot.stats.totalToolCalls}
556
- - Failures: ${snapshot.stats.failureCount}
553
+ - Assistant Turns: ${snapshot.stats.totalAssistantTurns ?? 'N/A (data unavailable)'}
554
+ - Tool Calls: ${snapshot.stats.totalToolCalls ?? 'N/A (data unavailable)'}
555
+ - Failures: ${snapshot.stats.failureCount ?? 'N/A (data unavailable)'}
557
556
  - Pain Events: ${snapshot.stats.totalPainEvents}
558
- - Gate Blocks: ${snapshot.stats.totalGateBlocks}
557
+ - Gate Blocks: ${snapshot.stats.totalGateBlocks ?? 'N/A (data unavailable)'}
559
558
 
560
559
  Please analyze this session and generate ${maxCandidates} candidate corrections. Each candidate should identify a bad decision and propose a better alternative grounded in the target principle.
561
560
 
562
561
  Respond with ONLY a valid JSON object matching the DreamerOutput contract.`;
563
562
  }
564
563
 
565
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: utility method doesn't require this - pure prompt building function
564
+
566
565
  private buildPhilosopherPrompt(
567
566
  dreamerOutput: DreamerOutput,
568
567
  principleId: string
@@ -576,7 +575,7 @@ ${candidatesJson}
576
575
  Please evaluate each candidate and rank them by principle alignment, specificity, and actionability. Respond with ONLY a valid JSON object matching the PhilosopherOutput contract.`;
577
576
  }
578
577
 
579
- /* eslint-disable @typescript-eslint/class-methods-use-this, @typescript-eslint/max-params -- Reason: helper function needs all 4 context params and doesn't use instance state */
578
+
580
579
  private buildScribePrompt(
581
580
  dreamerOutput: DreamerOutput,
582
581
  philosopherOutput: PhilosopherOutput,
@@ -596,7 +595,7 @@ ${judgmentsJson}
596
595
 
597
596
  Select the best candidate (Philosopher's rank 1) and synthesize it into a final TrinityDraftArtifact. Respond with ONLY a valid JSON object.`;
598
597
  }
599
- /* eslint-enable @typescript-eslint/class-methods-use-this, @typescript-eslint/max-params */
598
+
600
599
 
601
600
  private parseDreamerOutput(text: string): DreamerOutput {
602
601
  const json = this.extractJson(text);
@@ -694,12 +693,12 @@ Select the best candidate (Philosopher's rank 1) and synthesize it into a final
694
693
  }
695
694
  }
696
695
 
697
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: output parsing requires text + snapshot + principleId + telemetry - refactoring would break API
696
+
698
697
  private parseScribeOutput(
699
698
  text: string,
700
699
  snapshot: NocturnalSessionSnapshot,
701
700
  principleId: string,
702
- // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: interface requires this param but implementation doesn't use it
701
+
703
702
  _telemetry: TrinityTelemetry
704
703
  ): TrinityDraftArtifact | null {
705
704
  const json = this.extractJson(text);
@@ -740,7 +739,7 @@ Select the best candidate (Philosopher's rank 1) and synthesize it into a final
740
739
  /**
741
740
  * Extract JSON object from text that may contain markdown code blocks.
742
741
  */
743
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: pure utility function that doesn't need instance state
742
+
744
743
  private extractJson(text: string): string | null {
745
744
  // Try direct parse first
746
745
  try {
@@ -996,9 +995,9 @@ export function invokeStubDreamer(
996
995
  principleId: string,
997
996
  maxCandidates: number
998
997
  ): DreamerOutput {
999
- const hasFailures = snapshot.stats.failureCount > 0;
998
+ const hasFailures = (snapshot.stats.failureCount ?? 0) > 0;
1000
999
  const hasPain = snapshot.stats.totalPainEvents > 0;
1001
- const hasGateBlocks = snapshot.stats.totalGateBlocks > 0;
1000
+ const hasGateBlocks = (snapshot.stats.totalGateBlocks ?? 0) > 0;
1002
1001
 
1003
1002
  // #219: Detect fallback data source - stats may be incomplete
1004
1003
  const isFallback = snapshot._dataSource === 'pain_context_fallback';
@@ -1126,7 +1125,7 @@ export function invokeStubDreamer(
1126
1125
  */
1127
1126
  export function invokeStubPhilosopher(
1128
1127
  dreamerOutput: DreamerOutput,
1129
- // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: interface requires this param but stub implementation doesn't use it
1128
+
1130
1129
  _principleId: string
1131
1130
  ): PhilosopherOutput {
1132
1131
  if (!dreamerOutput.valid || dreamerOutput.candidates.length === 0) {
@@ -1201,7 +1200,7 @@ export function invokeStubPhilosopher(
1201
1200
  * In production, this would call the actual Scribe subagent.
1202
1201
  * The stub uses tournament selection (scoring + thresholds) to pick the winner.
1203
1202
  */
1204
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: stub scribe requires all context parameters - refactoring would break API
1203
+
1205
1204
  export function invokeStubScribe(
1206
1205
  dreamerOutput: DreamerOutput,
1207
1206
  philosopherOutput: PhilosopherOutput,
@@ -1279,7 +1278,7 @@ export function runTrinity(options: RunTrinityOptions): TrinityResult {
1279
1278
 
1280
1279
  // Stub path: use synchronous stub implementations
1281
1280
  if (config.useStubs) {
1282
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function is defined later in file but callback order makes this necessary
1281
+
1283
1282
  return runTrinityWithStubs(snapshot, principleId, config);
1284
1283
  }
1285
1284
 
@@ -1318,7 +1317,7 @@ export async function runTrinityAsync(options: RunTrinityOptions): Promise<Trini
1318
1317
 
1319
1318
  if (config.useStubs) {
1320
1319
  // Stub path: use synchronous stubs
1321
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function is defined later in file but callback order makes this necessary
1320
+
1322
1321
  return runTrinityWithStubs(snapshot, principleId, config);
1323
1322
  }
1324
1323
 
@@ -53,7 +53,7 @@ function getAgentsDir(): string {
53
53
  async function safeTail(filePath: string): Promise<string[]> {
54
54
  try {
55
55
  // Check existence and stats asynchronously
56
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in try, catch has early return
56
+
57
57
  let stat: fs.Stats;
58
58
  try {
59
59
  stat = await fsPromises.stat(filePath);
@@ -235,8 +235,8 @@ export async function extractRecentConversation(
235
235
  /**
236
236
  * Extracts failed tool call context with argument correlation.
237
237
  */
238
- /* eslint-disable @typescript-eslint/default-param-last */ // Reason: breaking API change - default param must precede required params for type inference compatibility
239
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: context extraction requires session + agent + tool + path - refactoring would break API
238
+ // Reason: breaking API change - default param must precede required params for type inference compatibility
239
+
240
240
  export async function extractFailedToolContext(
241
241
  sessionId: string,
242
242
  agentId = 'main',
package/src/core/pain.ts CHANGED
@@ -3,6 +3,7 @@ import * as path from 'path';
3
3
  import { serializeKvLines, parseKvLines } from '../utils/io.js';
4
4
  import { resolvePdPath } from './paths.js';
5
5
  import { ConfigService } from './config-service.js';
6
+ import { SystemLogger } from './system-logger.js';
6
7
 
7
8
  // =========================================================================
8
9
  // Pain Flag Contract (Single Source of Truth)
@@ -36,6 +37,13 @@ export interface PainFlagData {
36
37
  trigger_text_preview?: string;
37
38
  }
38
39
 
40
+ export interface PainFlagContractResult {
41
+ status: 'missing' | 'valid' | 'invalid';
42
+ format: 'missing' | 'empty' | 'kv' | 'json' | 'invalid_json';
43
+ data: Record<string, string>;
44
+ missingFields: string[];
45
+ }
46
+
39
47
  /**
40
48
  * Factory function — the ONLY way to construct pain flag data.
41
49
  *
@@ -58,16 +66,18 @@ export function buildPainFlag(input: {
58
66
  trace_id?: string;
59
67
  trigger_text_preview?: string;
60
68
  }): PainFlagData {
69
+ // Omit optional fields when not provided — prevents writing empty lines to disk
70
+ // which causes agent confusion (SKILL.md vs reality drift)
61
71
  return {
62
72
  source: input.source,
63
73
  score: input.score,
64
74
  time: input.time || new Date().toISOString(),
65
75
  reason: input.reason,
66
- session_id: input.session_id || '',
67
- agent_id: input.agent_id || '',
76
+ session_id: input.session_id ?? '',
77
+ agent_id: input.agent_id ?? '',
68
78
  is_risky: input.is_risky ? 'true' : 'false',
69
- trace_id: input.trace_id || '',
70
- trigger_text_preview: input.trigger_text_preview || '',
79
+ trace_id: input.trace_id ?? '',
80
+ trigger_text_preview: input.trigger_text_preview ?? '',
71
81
  };
72
82
  }
73
83
 
@@ -77,7 +87,9 @@ export function buildPainFlag(input: {
77
87
  */
78
88
  export function validatePainFlag(data: Record<string, string>): string[] {
79
89
  const missing: string[] = [];
80
- const required = ['source', 'score', 'time', 'reason', 'session_id', 'agent_id'] as const;
90
+ // Only source/score/time/reason are truly required — session_id/agent_id
91
+ // may be empty in automated contexts (heartbeat, background workers)
92
+ const required = ['source', 'score', 'time', 'reason'] as const;
81
93
  for (const field of required) {
82
94
  if (!data[field] || data[field].trim() === '') {
83
95
  missing.push(field);
@@ -86,7 +98,6 @@ export function validatePainFlag(data: Record<string, string>): string[] {
86
98
  return missing;
87
99
  }
88
100
 
89
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: Score computation requires all 5 parameters - refactoring to options object would be breaking API change
90
101
  export function computePainScore(rc: number, isSpiral: boolean, missingTestCommand: boolean, softScore: number, projectDir?: string): number {
91
102
  let score = Math.max(0, softScore || 0);
92
103
 
@@ -146,26 +157,128 @@ export function writePainFlag(projectDir: string, painData: PainFlagData): void
146
157
  fs.writeFileSync(painFlagPath, serializeKvLines(painData), "utf-8");
147
158
  }
148
159
 
160
+ /**
161
+ * Converts a JSON pain flag object to KV format.
162
+ */
163
+ function convertJsonToKv(json: Record<string, unknown>): Record<string, string> {
164
+ const kvData: Record<string, string> = {};
165
+ const fieldMap: Record<string, string> = {
166
+ source: 'source',
167
+ score: 'score',
168
+ time: 'time',
169
+ timestamp: 'time',
170
+ reason: 'reason',
171
+ session_id: 'session_id',
172
+ sessionId: 'session_id',
173
+ agent_id: 'agent_id',
174
+ agentId: 'agent_id',
175
+ is_risky: 'is_risky',
176
+ isRisky: 'is_risky',
177
+ severity: 'severity',
178
+ painId: 'pain_id',
179
+ };
180
+ for (const [jsonKey, kvKey] of Object.entries(fieldMap)) {
181
+ if (json[jsonKey] !== undefined) {
182
+ kvData[kvKey] = String(json[jsonKey]);
183
+ }
184
+ }
185
+ for (const [key, value] of Object.entries(json)) {
186
+ if (fieldMap[key] === undefined && value !== undefined && value !== null) {
187
+ kvData[key] = String(value);
188
+ }
189
+ }
190
+ return kvData;
191
+ }
192
+
193
+ /**
194
+ * Reads and validates the pain flag file with auto-repair.
195
+ *
196
+ * - If file doesn't exist → returns {}
197
+ * - If file is JSON format (wrong) → converts to KV, logs warning, rewrites file
198
+ * - If file is KV format → validates required fields, logs warning if missing
199
+ * - If file has unknown fields → silently ignores them (forward-compatible)
200
+ */
149
201
  export function readPainFlagData(projectDir: string): Record<string, string> {
150
202
  const painFlagPath = resolvePdPath(projectDir, 'PAIN_FLAG');
151
203
  try {
152
204
  if (!fs.existsSync(painFlagPath)) {
153
205
  return {};
154
206
  }
155
- const content = fs.readFileSync(painFlagPath, "utf-8");
156
- return parseKvLines(content);
157
- } catch (e) { // eslint-disable-line @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: intentionally unused - returning empty object on error
207
+ const content = fs.readFileSync(painFlagPath, "utf-8").trim();
208
+ if (!content) {
209
+ return {};
210
+ }
211
+
212
+ // Detect JSON format (wrong — should be KV)
213
+ if (content.startsWith('{')) {
214
+ let json: Record<string, unknown>;
215
+ try {
216
+ json = JSON.parse(content);
217
+ } catch {
218
+ SystemLogger.log(projectDir, 'PAIN_FLAG_CORRUPT', 'Pain flag file contains invalid JSON');
219
+ return {};
220
+ }
221
+
222
+ // Auto-repair: convert JSON to KV format
223
+ const kvData = convertJsonToKv(json);
224
+
225
+ const repaired = serializeKvLines(kvData);
226
+ fs.writeFileSync(painFlagPath, repaired, 'utf-8');
227
+ SystemLogger.log(projectDir, 'PAIN_FLAG_AUTO_REPAIRED', `Auto-repaired pain flag from JSON to KV format (${Object.keys(json).length} fields)`);
228
+ return kvData;
229
+ }
230
+
231
+ // KV format — parse and validate
232
+ const data = parseKvLines(content);
233
+ const missing = validatePainFlag(data);
234
+ if (missing.length > 0) {
235
+ SystemLogger.log(projectDir, 'PAIN_FLAG_INCOMPLETE', `Pain flag missing required fields: ${missing.join(', ')}`);
236
+ }
237
+ return data;
238
+ } catch (e) {
239
+ SystemLogger.log(projectDir, 'PAIN_FLAG_READ_ERROR', `Failed to read pain flag: ${String(e)}`);
158
240
  return {};
159
241
  }
160
242
  }
161
243
 
244
+ export function readPainFlagContract(projectDir: string): PainFlagContractResult {
245
+ const data = readPainFlagData(projectDir);
246
+
247
+ if (Object.keys(data).length === 0) {
248
+ const painFlagPath = resolvePdPath(projectDir, 'PAIN_FLAG');
249
+ if (!fs.existsSync(painFlagPath)) {
250
+ return { status: 'missing', format: 'missing', data: {}, missingFields: [] };
251
+ }
252
+
253
+ const raw = fs.readFileSync(painFlagPath, 'utf-8').trim();
254
+ if (!raw) {
255
+ return { status: 'missing', format: 'empty', data: {}, missingFields: [] };
256
+ }
257
+
258
+ return {
259
+ status: 'invalid',
260
+ format: raw.startsWith('{') ? 'invalid_json' : 'kv',
261
+ data: {},
262
+ missingFields: ['unparseable'],
263
+ };
264
+ }
265
+
266
+ const missing = validatePainFlag(data);
267
+ return {
268
+ status: missing.length > 0 ? 'invalid' : 'valid',
269
+ format: 'kv',
270
+ data,
271
+ missingFields: missing,
272
+ };
273
+ }
274
+
162
275
  /**
163
276
  * Track principle value metrics when a pain signal is written.
164
277
  * This is observation-only — it does NOT affect the pain flag write flow.
165
278
  * If any principle matches the pain signal, its painPreventedCount is incremented.
166
279
  * Errors are silently ignored to avoid disrupting the pain pipeline.
167
280
  */
168
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: principle value tracking requires workspace + data + getters + updaters - refactoring would break API
281
+
169
282
  export function trackPrincipleValue(
170
283
  workspaceDir: string,
171
284
  painData: { reason?: string; source?: string; score?: string },
@@ -174,7 +287,7 @@ export function trackPrincipleValue(
174
287
  trigger: string;
175
288
  valueMetrics?: { painPreventedCount: number; lastPainPreventedAt?: string; calculatedAt: string };
176
289
  }[],
177
- updatePrincipleMetrics: (_id: string, _metrics: { painPreventedCount: number; lastPainPreventedAt: string; calculatedAt: string }) => void, // eslint-disable-line no-unused-vars -- Reason: callback params required by interface, actual values accessed via principle.id and principle.valueMetrics
290
+ updatePrincipleMetrics: (_id: string, _metrics: { painPreventedCount: number; lastPainPreventedAt: string; calculatedAt: string }) => void,
178
291
  ): void {
179
292
  try {
180
293
  const activePrinciples = getActivePrinciples();
@@ -8,12 +8,12 @@ export interface PathResolverOptions {
8
8
  workspaceDir?: string;
9
9
  normalizeWorkspace?: boolean;
10
10
  logger?: {
11
- /* eslint-disable no-unused-vars -- Reason: logger callback param names intentionally unused - callbacks only invoked for side effects */
11
+
12
12
  debug?: (_msg: string) => void;
13
13
  info?: (_msg: string) => void;
14
14
  warn?: (_msg: string) => void;
15
15
  error?: (_msg: string) => void;
16
- /* eslint-enable no-unused-vars */
16
+
17
17
  };
18
18
  }
19
19
 
@@ -420,9 +420,9 @@ export function resolveWorkspaceDirFromApi(
420
420
  if (!api) return undefined;
421
421
 
422
422
  // 1. Official API: api.runtime.agent.resolveAgentWorkspaceDir
423
- /* eslint-disable no-unused-vars -- Reason: type callback params cfg/id unused - actual values passed are api.config and agentId */
423
+
424
424
  const officialAgent = (api.runtime as { agent?: { resolveAgentWorkspaceDir?: (cfg: unknown, id: string) => string } }).agent;
425
- /* eslint-enable no-unused-vars */
425
+
426
426
  if (officialAgent?.resolveAgentWorkspaceDir) {
427
427
  try {
428
428
  return officialAgent.resolveAgentWorkspaceDir(api.config, agentId ?? 'main');
@@ -72,14 +72,14 @@ export interface ReconcileResult {
72
72
  export interface ReconcileOptions {
73
73
  dryRun?: boolean;
74
74
  workspaceDir: string;
75
- /* eslint-disable no-unused-vars -- Reason: logger callback param names intentionally unused - callbacks only invoked for side effects */
75
+
76
76
  logger?: { info?: (_: string) => void; warn?: (_: string) => void };
77
- /* eslint-enable no-unused-vars */
77
+
78
78
  }
79
79
 
80
- /* eslint-disable no-unused-vars -- Reason: logger callbacks have unused param names in type */
80
+
81
81
  async function readCronStore(logger?: { info?: (_: string) => void; warn?: (_: string) => void }): Promise<CronStoreFile> {
82
- /* eslint-enable no-unused-vars */
82
+
83
83
  if (!fs.existsSync(CRON_STORE_PATH)) {
84
84
  logger?.info?.(`[PD:Reconciler] cron/jobs.json not found, starting with empty store`);
85
85
  return { version: 1, jobs: [] };
@@ -143,7 +143,7 @@ function diff(declared: PDTaskSpec[], actual: CronJob[]): DiffAction[] {
143
143
  function buildCronJob(
144
144
  task: PDTaskSpec,
145
145
  nowMs: number,
146
- // eslint-disable-next-line no-unused-vars -- logger callback param unused in type
146
+
147
147
  logger?: { info?: (_: string) => void },
148
148
  ): CronJob {
149
149
  logger?.info?.(`[PD:Reconciler] Building cron job: ${task.name} (id=${task.id}, interval=${task.schedule.everyMs}ms)`);
@@ -158,7 +158,7 @@ function buildCronJob(
158
158
  wakeMode: 'now',
159
159
  payload: {
160
160
  kind: 'agentTurn',
161
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: buildTaskPrompt is defined later in this file, called here for organizational reasons
161
+
162
162
  message: buildTaskPrompt(task, logger),
163
163
  lightContext: task.execution.lightContext ?? true,
164
164
  timeoutSeconds: task.execution.timeoutSeconds ?? 120,
@@ -177,7 +177,7 @@ function buildCronJob(
177
177
  };
178
178
  }
179
179
 
180
- // eslint-disable-next-line no-unused-vars -- logger callback param unused in type
180
+
181
181
  function buildTaskPrompt(task: PDTaskSpec, logger?: { info?: (_: string) => void }): string {
182
182
  if (task.id === 'empathy-optimizer') {
183
183
  logger?.info?.(`[PD:Reconciler] Building empathy optimizer prompt`);
@@ -292,7 +292,7 @@ export async function reconcilePDTasks(
292
292
  });
293
293
 
294
294
  const cronStore = await readCronStore(logger);
295
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: healthCheck is defined later in this file, called here for organizational reasons
295
+
296
296
  const healthUpdated = healthCheck(declared, cronStore, logger);
297
297
  const actions = diff(healthUpdated, cronStore.jobs);
298
298
 
@@ -369,9 +369,9 @@ export async function reconcilePDTasks(
369
369
  function healthCheck(
370
370
  tasks: PDTaskSpec[],
371
371
  cronStore: CronStoreFile,
372
- /* eslint-disable no-unused-vars -- Reason: callback type signature parameters */
372
+
373
373
  logger: { info?: (_msg: string) => void; warn?: (_msg: string) => void },
374
- /* eslint-enable no-unused-vars */
374
+
375
375
  ): PDTaskSpec[] {
376
376
  const jobByName = new Map(cronStore.jobs.map((j) => [j.name, j]));
377
377
 
@@ -35,7 +35,7 @@ export const PDTaskService: OpenClawPluginService = {
35
35
  }
36
36
  },
37
37
 
38
- // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: stop method required by service interface but no cleanup needed
38
+
39
39
  stop(_ctx: OpenClawPluginServiceContext): void {
40
40
  /* intentionally empty - no cleanup required for this service */
41
41
  },
@@ -55,7 +55,7 @@ export function initTaskMeta(task: PDTaskSpec): PDTaskSpec {
55
55
  return task;
56
56
  }
57
57
 
58
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: sync meta update requires task + status + optional jobId + error - refactoring would break API
58
+
59
59
  export function updateSyncMeta(
60
60
  task: PDTaskSpec,
61
61
  status: 'ok' | 'error',
@@ -63,7 +63,7 @@ export function assessDeprecatedReadiness(
63
63
  adherence.repeatedErrorReductionScore * 0.15,
64
64
  );
65
65
 
66
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all if/else branches
66
+
67
67
  let status: DeprecatedReadinessStatus;
68
68
  if (blockingReasons.length === 0 && stableCoverageRatio === 1) {
69
69
  status = 'ready';
@@ -62,7 +62,7 @@ export function createDefaultPrincipleState(principleId: string): PrincipleTrain
62
62
  }
63
63
 
64
64
  export function loadStore(stateDir: string): PrincipleTrainingStore {
65
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: ledgerTrainingStore is defined later in this file, called here for organizational reasons
65
+
66
66
  return ledgerTrainingStore(stateDir);
67
67
  }
68
68
 
@@ -75,7 +75,7 @@ export function saveStore(stateDir: string, store: PrincipleTrainingStore): void
75
75
  }
76
76
 
77
77
  export async function loadStoreAsync(stateDir: string): Promise<PrincipleTrainingStore> {
78
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: ledgerTrainingStore is defined later in this file, called here for organizational reasons
78
+
79
79
  return ledgerTrainingStore(stateDir);
80
80
  }
81
81
 
@@ -77,7 +77,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
77
77
  return typeof value === 'object' && value !== null && !Array.isArray(value);
78
78
  }
79
79
 
80
- /* eslint-disable @typescript-eslint/max-params -- Reason: Clamp function requires all parameters for safe numeric conversion */
80
+
81
81
  function clampFloat(value: unknown, min: number, max: number, fallback: number): number {
82
82
  if (typeof value !== 'number' || !Number.isFinite(value)) {
83
83
  return fallback;
@@ -85,7 +85,7 @@ function clampFloat(value: unknown, min: number, max: number, fallback: number):
85
85
  return Math.max(min, Math.min(max, value));
86
86
  }
87
87
 
88
- /* eslint-disable @typescript-eslint/max-params -- Reason: Clamp function requires all parameters for safe numeric conversion */
88
+
89
89
  function clampInt(value: unknown, min: number, max: number, fallback: number): number {
90
90
  if (typeof value !== 'number' || !Number.isFinite(value)) {
91
91
  return fallback;
@@ -315,9 +315,9 @@ function writeLedgerUnlocked(filePath: string, store: HybridLedgerStore): void {
315
315
  fs.writeFileSync(filePath, serializeLedger(store), 'utf-8');
316
316
  }
317
317
 
318
- /* eslint-disable no-unused-vars -- Reason: callback parameter used via closure in inner function */
318
+
319
319
  function mutateLedger<T>(stateDir: string, mutate: (store: HybridLedgerStore) => T): T {
320
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function is defined later but called in this helper for consistency
320
+
321
321
  const filePath = getLedgerFilePath(stateDir);
322
322
  return withLock(filePath, () => {
323
323
  const store = readLedgerFromFile(filePath);
@@ -327,9 +327,9 @@ function mutateLedger<T>(stateDir: string, mutate: (store: HybridLedgerStore) =>
327
327
  });
328
328
  }
329
329
 
330
- /* eslint-disable no-unused-vars -- Reason: callback parameter used via closure in inner function */
330
+
331
331
  async function mutateLedgerAsync<T>(stateDir: string, mutate: (store: HybridLedgerStore) => Promise<T>): Promise<T> {
332
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function is defined later but called in this helper for consistency
332
+
333
333
  const filePath = getLedgerFilePath(stateDir);
334
334
  return withLockAsync(filePath, async () => {
335
335
  const store = readLedgerFromFile(filePath);
@@ -363,7 +363,7 @@ export async function saveLedgerAsync(stateDir: string, store: HybridLedgerStore
363
363
 
364
364
  export function updateTrainingStore(
365
365
  stateDir: string,
366
- /* eslint-disable no-unused-vars -- Reason: callback parameter is forwarded to mutateLedger callback */
366
+
367
367
  mutate: (store: LegacyPrincipleTrainingStore) => void,
368
368
  ): void {
369
369
  mutateLedger(stateDir, (store) => {