principles-disciple 1.16.0 → 1.18.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 (132) 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 +4 -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 +480 -158
  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 +221 -109
  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 +11 -4
  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/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +111 -0
  113. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +1 -1
  114. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +1 -1
  115. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
  116. package/templates/pain_settings.json +1 -1
  117. package/tests/build-artifacts.test.ts +4 -58
  118. package/tests/commands/pd-reflect.test.ts +49 -0
  119. package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
  120. package/tests/core/pain-auto-repair.test.ts +96 -0
  121. package/tests/core/pain-integration.test.ts +483 -0
  122. package/tests/core/pain.test.ts +5 -4
  123. package/tests/core/workspace-dir-service.test.ts +68 -0
  124. package/tests/core/workspace-dir-validation.test.ts +56 -192
  125. package/tests/hooks/pain.test.ts +20 -0
  126. package/tests/http/principles-console-route.test.ts +42 -20
  127. package/tests/integration/empathy-workflow-integration.test.ts +1 -2
  128. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
  129. package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
  130. package/tests/service/evolution-worker.nocturnal.test.ts +118 -109
  131. package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
  132. package/tests/utils/subagent-probe.test.ts +32 -0
@@ -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) => {
@@ -70,7 +70,7 @@ export const DEFAULT_ALLOWED_MARGIN = 0.05;
70
70
  * Allowed worker profiles for Phase 7 shadow rollout.
71
71
  * Only bounded local workers eligible. local-reader first, local-editor deferred.
72
72
  */
73
- // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: reserved for Phase 7 shadow rollout profile validation
73
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Reason: reserved for Phase 7 shadow rollout profile validation
74
74
  const ALLOWED_ROLLOUT_PROFILES: readonly TrainableWorkerProfile[] = ['local-reader'];
75
75
 
76
76
  /**
@@ -233,7 +233,7 @@ function writeRegistry(stateDir: string, registry: PromotionRegistry): void {
233
233
  */
234
234
  function withPromotionRegistryLock<T>(
235
235
  stateDir: string,
236
- // eslint-disable-next-line no-unused-vars -- Reason: callback parameter name is type documentation, actual value passed at call site
236
+
237
237
  fn: (_registry: PromotionRegistry) => T
238
238
  ): T {
239
239
  const registryPath = getRegistryPath(stateDir);
@@ -318,7 +318,7 @@ export function evaluatePromotionGate(
318
318
  ): PromotionGateResult {
319
319
  const {
320
320
  checkpointId,
321
- // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: reserved for Phase 7 profile-based targeting
321
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Reason: reserved for Phase 7 profile-based targeting
322
322
  targetProfile: _targetProfile,
323
323
  baselineMetrics,
324
324
  minDelta = DEFAULT_MIN_DELTA,
@@ -388,9 +388,9 @@ export function evaluatePromotionGate(
388
388
  // PREFER real shadow evidence over eval verdict proxy
389
389
  // Shadow evidence comes from actual runtime routing decisions
390
390
  const shadowStats = computeShadowStats(stateDir, { checkpointId });
391
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
391
+
392
392
  let arbiterRejectRate: number;
393
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
393
+
394
394
  let arbiterRejectSource: 'shadow' | 'eval-proxy';
395
395
 
396
396
  if (shadowStats && shadowStats.isStatisticallySignificant) {
@@ -424,9 +424,9 @@ export function evaluatePromotionGate(
424
424
 
425
425
  // --- Check 6: Executability reject rate constraint ---
426
426
  // PREFER real shadow evidence: escalation rate + profile rejection rate
427
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
427
+
428
428
  let executabilityRejectRate: number;
429
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
429
+
430
430
  let executabilityRejectSource: 'shadow' | 'eval-proxy';
431
431
 
432
432
  if (shadowStats && shadowStats.isStatisticallySignificant) {
@@ -484,7 +484,7 @@ export function evaluatePromotionGate(
484
484
  qualityCheck.passed;
485
485
 
486
486
  // --- Suggest state based on checks ---
487
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all branches
487
+
488
488
  let suggestedState: PromotionState | undefined;
489
489
  if (allPassed) {
490
490
  suggestedState = 'candidate_only';
@@ -585,7 +585,7 @@ export function advancePromotion(
585
585
  // - rejected → candidate_only/shadow_ready: allowed via re-evaluation
586
586
  // (new eval data may reverse a previous rejection)
587
587
  //
588
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all branches
588
+
589
589
  let targetState: PromotionState;
590
590
  if (!gateResult.passes) {
591
591
  targetState = 'rejected';
@@ -69,9 +69,9 @@ export interface ReplayReport {
69
69
  }
70
70
 
71
71
  export interface CandidateEvaluator {
72
- /* eslint-disable no-unused-vars -- Reason: interface method params are type signatures */
72
+
73
73
  evaluate(sample: unknown): { passed: boolean; reason?: string; decision: string };
74
- /* eslint-enable no-unused-vars */
74
+
75
75
  }
76
76
 
77
77
  export class ReplayEngine {
@@ -112,7 +112,7 @@ export class ReplayEngine {
112
112
  return samples;
113
113
  }
114
114
 
115
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Public API method that delegates to evaluator, no instance state needed
115
+
116
116
  runSingleSample(sample: ReplaySample, evaluator: CandidateEvaluator): ReplayResult {
117
117
  const evaluation = evaluator.evaluate(sample);
118
118
  return {
@@ -202,12 +202,12 @@ export class ReplayEngine {
202
202
  throw new Error(`Implementation ${implementation.id} does not export evaluate().`);
203
203
  }
204
204
 
205
- /* eslint-disable no-unused-vars -- Reason: type-only parameters in type cast, not used at runtime */
205
+
206
206
  const evaluate = moduleExports.evaluate as (
207
207
  _input: RuleHostInput,
208
208
  _helpers: RuleHostHelpers,
209
209
  ) => RuleHostResult;
210
- /* eslint-enable no-unused-vars */
210
+
211
211
 
212
212
  return {
213
213
  evaluate: (sample: unknown) => {
@@ -284,7 +284,7 @@ export class ReplayEngine {
284
284
  };
285
285
  }
286
286
 
287
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
287
+
288
288
  private _selectToolCall(
289
289
  snapshot: NocturnalSessionSnapshot,
290
290
  classification: SampleClassification,
@@ -314,7 +314,7 @@ export class ReplayEngine {
314
314
  return byNewest[0] ?? null;
315
315
  }
316
316
 
317
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
317
+
318
318
  private _matchGateBlock(
319
319
  gateBlocks: NocturnalGateBlock[],
320
320
  toolCall: NocturnalToolCall,
@@ -351,7 +351,7 @@ export class ReplayEngine {
351
351
  }
352
352
  }
353
353
 
354
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
354
+
355
355
  private _estimateLineChanges(toolCall: NocturnalToolCall): number {
356
356
  if (toolCall.toolName === 'edit' || toolCall.toolName === 'write') {
357
357
  return 20;
@@ -359,7 +359,7 @@ export class ReplayEngine {
359
359
  return 0;
360
360
  }
361
361
 
362
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
362
+
363
363
  private _inferBashRisk(toolCall: NocturnalToolCall): 'safe' | 'normal' | 'dangerous' | 'unknown' {
364
364
  if (toolCall.toolName !== 'bash' && toolCall.toolName !== 'run_shell_command') {
365
365
  return 'unknown';
@@ -371,7 +371,7 @@ export class ReplayEngine {
371
371
  return toolCall.outcome === 'success' ? 'safe' : 'normal';
372
372
  }
373
373
 
374
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
374
+
375
375
  private _scoreEvaluation(
376
376
  sample: ReplaySample,
377
377
  result: RuleHostResult,
@@ -465,7 +465,7 @@ export class ReplayEngine {
465
465
  };
466
466
  }
467
467
 
468
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
468
+
469
469
  private _determineDecision(
470
470
  pain: ClassificationSummary,
471
471
  success: ClassificationSummary,
@@ -495,7 +495,7 @@ export class ReplayEngine {
495
495
  });
496
496
  }
497
497
 
498
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
498
+
499
499
  private _deriveExpectedOutcome(
500
500
  record: NocturnalDatasetRecord,
501
501
  ): ReplaySample['expectedOutcome'] {
@@ -93,7 +93,7 @@ export function getTargetFileLineCount(absoluteFilePath: string): number | null
93
93
  * @param maxLines - Optional upper bound to prevent misconfiguration
94
94
  * @returns Maximum allowed lines (at least minLines, at most maxLines if provided)
95
95
  */
96
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: percentage threshold calculation requires all 4 params - refactoring would break API
96
+
97
97
  export function calculatePercentageThreshold(
98
98
  targetLineCount: number,
99
99
  percentage: number,
@@ -76,7 +76,7 @@ export interface LoadedImplementation {
76
76
  implId: string;
77
77
  ruleId: string;
78
78
  meta: RuleHostMeta;
79
- /* eslint-disable no-unused-vars -- Reason: _input parameter name in interface type definition intentionally unused - actual implementation uses different param names */
79
+
80
80
  evaluate: (_input: RuleHostInput) => RuleHostResult;
81
- /* eslint-enable no-unused-vars */
81
+
82
82
  }
@@ -36,9 +36,9 @@ import type {
36
36
  import type { Implementation } from '../types/principle-tree-schema.js';
37
37
 
38
38
  export interface RuleHostLogger {
39
- /* eslint-disable no-unused-vars -- Reason: logger callback param name intentionally unused - callback only invoked for side effects */
39
+
40
40
  warn?: (_message: string) => void;
41
- /* eslint-enable no-unused-vars */
41
+
42
42
  }
43
43
 
44
44
  export class RuleHost {
@@ -69,7 +69,7 @@ export class RuleHost {
69
69
  }
70
70
 
71
71
  // Merge decisions from all active implementations
72
- // eslint-disable-next-line @typescript-eslint/init-declarations -- undefined is valid zero value, checked before use
72
+
73
73
  let blocked: RuleHostResult | undefined;
74
74
  const approvals: RuleHostResult[] = [];
75
75
 
@@ -218,12 +218,12 @@ export class RuleHost {
218
218
 
219
219
  // Return a loaded implementation that wraps the compiled evaluate
220
220
  // with the actual helpers from the input at evaluation time
221
- /* eslint-disable no-unused-vars -- Reason: type cast params intentionally unused - they're just type annotations, actual function uses different params */
221
+
222
222
  const rawEvaluate = moduleExports.evaluate as (
223
223
  _input: RuleHostInput,
224
224
  _helpers: ReturnType<typeof createRuleHostHelpers>
225
225
  ) => RuleHostResult;
226
- /* eslint-enable no-unused-vars */
226
+
227
227
 
228
228
  return {
229
229
  implId: impl.id,
@@ -3,7 +3,7 @@
3
3
  * Kept separate to avoid circular dependencies between schema-definitions and migration-runner.
4
4
  */
5
5
 
6
- /* eslint-disable no-unused-vars -- Reason: interface method param names are part of type signature */
6
+
7
7
 
8
8
  /** Minimal interface for better-sqlite3 Database instances. */
9
9
  export interface Db {
@@ -35,7 +35,7 @@ export interface Migration {
35
35
  name: string;
36
36
  /** Which database file this migration applies to */
37
37
  db: DbType;
38
- /* eslint-disable no-unused-vars -- Reason: interface callback params are part of type signature */
38
+
39
39
  /** Apply this migration */
40
40
  up: (_db: Db) => void;
41
41
  /** Revert this migration */