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
@@ -156,7 +156,8 @@ export class HealthQueryService {
156
156
  const queue = this.readQueueStats();
157
157
  const painFlag = this.readPainFlag();
158
158
 
159
- // GFI: always read from SQLite (synced from session JSON at construction time)
159
+ // GFI: Re-sync from session JSON on every request for real-time data
160
+ this.syncGfiFromSession();
160
161
  const gfiData = this.readGfiFromDb();
161
162
  const {currentGfi} = gfiData;
162
163
  const peakToday = gfiData.dailyGfiPeak;
@@ -277,7 +278,8 @@ export class HealthQueryService {
277
278
  ORDER BY total DESC
278
279
  `, today);
279
280
 
280
- // GFI: read from SQLite (synced from session JSON at construction time)
281
+ // GFI: Re-sync from session JSON for real-time data
282
+ this.syncGfiFromSession();
281
283
  const gfiData = this.readGfiFromDb();
282
284
 
283
285
  return {
@@ -551,7 +553,7 @@ export class HealthQueryService {
551
553
  const streamPath = resolvePdPath(this.workspaceDir, 'EVOLUTION_STREAM');
552
554
  if (!fs.existsSync(streamPath)) return [];
553
555
 
554
- // eslint-disable-next-line no-useless-assignment -- Reason: initial value unused due to immediate reassignment
556
+
555
557
  let lines: string[] = [];
556
558
  try {
557
559
  const raw = fs.readFileSync(streamPath, 'utf8').trim();
@@ -563,7 +565,7 @@ export class HealthQueryService {
563
565
 
564
566
  const records: RecentPrincipleChange[] = [];
565
567
  for (const line of lines) {
566
- // eslint-disable-next-line no-useless-assignment -- Reason: initial value unused due to immediate reassignment in try/catch
568
+
567
569
  let event: EvolutionStreamRecord | null = null;
568
570
  try {
569
571
  event = JSON.parse(line) as EvolutionStreamRecord;
@@ -781,7 +783,7 @@ export class HealthQueryService {
781
783
  return [];
782
784
  }
783
785
 
784
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
786
+
785
787
  private getEventDedupKey(entry: EventLogEntry): string {
786
788
  const eventId = typeof entry.data?.eventId === 'string' ? entry.data.eventId : null;
787
789
  if (eventId) {
@@ -851,7 +853,7 @@ export class HealthQueryService {
851
853
  return fallbackStage;
852
854
  }
853
855
 
854
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
856
+
855
857
  private resolveGateType(row: GateBlockRow): string {
856
858
  if (typeof row.gate_type === 'string' && row.gate_type.trim().length > 0) {
857
859
  return row.gate_type;
@@ -875,14 +877,14 @@ export class HealthQueryService {
875
877
  return cached.has(columnName);
876
878
  }
877
879
 
878
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
880
+
879
881
  private scoreToStatus(score: number): string {
880
882
  if (score >= 70) return 'healthy';
881
883
  if (score >= 40) return 'warning';
882
884
  return 'critical';
883
885
  }
884
886
 
885
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
887
+
886
888
  private evolutionToStatus(tier: string, points: number): string {
887
889
  const lower = tier.toLowerCase();
888
890
  if (lower === 'forest' || lower === 'tree') return 'healthy';
@@ -890,7 +892,7 @@ export class HealthQueryService {
890
892
  return 'critical';
891
893
  }
892
894
 
893
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this, no-unused-vars -- Reason: Private helper doesn't use instance state
895
+
894
896
  private safeListFiles(dirPath: string, predicate: (_name: string) => boolean): string[] {
895
897
  if (!fs.existsSync(dirPath)) return [];
896
898
  try {
@@ -902,7 +904,7 @@ export class HealthQueryService {
902
904
  }
903
905
  }
904
906
 
905
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
907
+
906
908
  private readJsonFile<T>(filePath: string, fallback: T): T {
907
909
  if (!fs.existsSync(filePath)) return fallback;
908
910
  try {
@@ -912,12 +914,12 @@ export class HealthQueryService {
912
914
  }
913
915
  }
914
916
 
915
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
917
+
916
918
  private asNumber(value: unknown, fallback: number): number {
917
919
  return Number.isFinite(value) ? Number(value) : fallback;
918
920
  }
919
921
 
920
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
922
+
921
923
  private asNullableNumber(value: unknown): number | null {
922
924
  if (Number.isFinite(value)) return Number(value);
923
925
  if (typeof value === 'string' && value.trim().length > 0) {
@@ -953,7 +955,7 @@ export class HealthQueryService {
953
955
  today,
954
956
  );
955
957
  } catch (err) {
956
- console.warn('[HealthQueryService] Failed to sync GFI from session:', err);
958
+ // Non-critical: GFI sync failure should not block queries
957
959
  }
958
960
  }
959
961
 
@@ -981,11 +983,15 @@ export class HealthQueryService {
981
983
  */
982
984
  private readLatestSessionFromFile(): SessionState | null {
983
985
  const sessionsDir = path.join(this.stateDir, 'sessions');
984
- if (!fs.existsSync(sessionsDir)) return null;
986
+ if (!fs.existsSync(sessionsDir)) {
987
+ return null;
988
+ }
985
989
 
986
990
  try {
987
991
  const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.json'));
988
- if (files.length === 0) return null;
992
+ if (files.length === 0) {
993
+ return null;
994
+ }
989
995
 
990
996
  let latest: SessionState | null = null;
991
997
  let latestTs = 0;
@@ -994,7 +1000,10 @@ export class HealthQueryService {
994
1000
  try {
995
1001
  const content = fs.readFileSync(path.join(sessionsDir, file), 'utf-8');
996
1002
  const state = JSON.parse(content) as SessionState;
997
- if (state.workspaceDir && state.workspaceDir !== this.workspaceDir) continue;
1003
+ // Skip sessions from different workspaces
1004
+ if (state.workspaceDir && state.workspaceDir !== this.workspaceDir) {
1005
+ continue;
1006
+ }
998
1007
  const ts = Number(state.lastControlActivityAt ?? state.lastActivityAt ?? 0);
999
1008
  if (ts > latestTs) {
1000
1009
  latestTs = ts;
@@ -1006,7 +1015,8 @@ export class HealthQueryService {
1006
1015
  }
1007
1016
 
1008
1017
  return latest;
1009
- } catch {
1018
+ } catch (err) {
1019
+ // Non-critical: failure to read session files should not crash the service
1010
1020
  return null;
1011
1021
  }
1012
1022
  }
@@ -83,9 +83,9 @@ export class MonitoringQueryService {
83
83
  const failedEvent = events.find(e => e.event_type === `trinity_${stage}_failed`);
84
84
 
85
85
  // Determine status
86
- // eslint-disable-next-line @typescript-eslint/init-declarations
86
+
87
87
  let status: 'pending' | 'running' | 'completed' | 'failed';
88
- // eslint-disable-next-line @typescript-eslint/init-declarations
88
+
89
89
  let reason: string | undefined;
90
90
 
91
91
  if (!startEvent) {
@@ -106,7 +106,7 @@ export class MonitoringQueryService {
106
106
  const outputCount = stageOutputs.filter(so => so.stage === stage).length;
107
107
 
108
108
  // Calculate duration if stage started and completed/failed
109
- // eslint-disable-next-line @typescript-eslint/init-declarations
109
+
110
110
  let duration: number | undefined;
111
111
  if (startEvent && (completeEvent || failedEvent)) {
112
112
  const endEvent = completeEvent || failedEvent;
@@ -304,7 +304,7 @@ export function checkWorkspaceIdle(
304
304
  trajectoryGuardrailConfirmsIdle = trajectoryIdleFor > idleThresholdMs * 0.8;
305
305
  }
306
306
 
307
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all if/else branches
307
+
308
308
  let reason: string;
309
309
  if (mostRecentActivityAt === 0) {
310
310
  reason = 'No active sessions found — workspace is idle';
@@ -352,7 +352,7 @@ export function checkCooldown(
352
352
  } = {}
353
353
  ): CooldownCheckResult {
354
354
  const {
355
- /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: Cooldown parameters reserved for future quota enforcement */
355
+ /* eslint-disable @typescript-eslint/no-unused-vars -- Reason: Cooldown parameters reserved for future quota enforcement */
356
356
  globalCooldownMs: _globalCooldownMs = DEFAULT_GLOBAL_COOLDOWN_MS,
357
357
  principleCooldownMs: _principleCooldownMs = DEFAULT_PRINCIPLE_COOLDOWN_MS,
358
358
  maxRunsPerWindow = DEFAULT_MAX_RUNS_PER_WINDOW,
@@ -372,7 +372,7 @@ export function checkCooldown(
372
372
  if (cooldownEnd > now) {
373
373
  globalCooldownActive = true;
374
374
  globalCooldownRemainingMs = cooldownEnd - now;
375
- globalCooldownUntil = state.globalCooldownUntil; // eslint-disable-line @typescript-eslint/prefer-destructuring -- Reason: globalCooldownUntil is reassignable outer let - destructuring would shadow
375
+ globalCooldownUntil = state.globalCooldownUntil;
376
376
  }
377
377
  }
378
378
 
@@ -540,7 +540,7 @@ export interface PreflightCheckResult {
540
540
  * @param trajectoryLastActivityAt - Optional trajectory timestamp as secondary guardrail
541
541
  * @param idleCheckOverride - Optional override for idle check result (for testing)
542
542
  */
543
- /* eslint-disable @typescript-eslint/max-params -- Reason: Preflight check requires workspace, state, principleId, trajectory timestamp, and idle override */
543
+
544
544
  export function checkPreflight(
545
545
  workspaceDir: string,
546
546
  stateDir: string,
@@ -99,6 +99,7 @@ import {
99
99
  import { NocturnalPathResolver } from '../core/nocturnal-paths.js';
100
100
  import { registerSample } from '../core/nocturnal-dataset.js';
101
101
  import type { Implementation } from '../types/principle-tree-schema.js';
102
+ import { validateNocturnalSnapshotIngress } from '../core/nocturnal-snapshot-contract.js';
102
103
 
103
104
  // ---------------------------------------------------------------------------
104
105
  // Types
@@ -266,16 +267,16 @@ function invokeStubReflector(
266
267
 
267
268
  // Build a plausible bad/better decision pair based on available snapshot data.
268
269
  // This is synthetic — real reflection would come from subagent analysis.
269
- const hasFailures = snapshot.stats.failureCount > 0;
270
+ const hasFailures = (snapshot.stats.failureCount ?? 0) > 0;
270
271
  const hasPain = snapshot.stats.totalPainEvents > 0;
271
- const hasGateBlocks = snapshot.stats.totalGateBlocks > 0;
272
+ const hasGateBlocks = (snapshot.stats.totalGateBlocks ?? 0) > 0;
272
273
 
273
274
  // Detect what kind of signal is available and craft appropriate artifact
274
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all if/else branches
275
+
275
276
  let badDecision: string;
276
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all if/else branches
277
+
277
278
  let betterDecision: string;
278
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all if/else branches
279
+
279
280
  let rationale: string;
280
281
 
281
282
  if (hasGateBlocks) {
@@ -362,7 +363,7 @@ function buildGateBlockRefs(snapshot: NocturnalSessionSnapshot): string[] {
362
363
  );
363
364
  }
364
365
 
365
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function signature requires all parameters for type-safe artifact construction */
366
+
366
367
  function buildDefaultArtificerOutput(
367
368
  ruleId: string,
368
369
  artifact: NocturnalArtifact,
@@ -411,7 +412,7 @@ function buildDefaultArtificerOutput(
411
412
  };
412
413
  }
413
414
 
414
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function signature requires all parameters for type-safe candidate persistence */
415
+
415
416
  function persistCodeCandidate(
416
417
  workspaceDir: string,
417
418
  stateDir: string,
@@ -503,7 +504,7 @@ function persistCodeCandidate(
503
504
  }
504
505
  }
505
506
 
506
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function signature requires all parameters for type-safe candidate persistence */
507
+
507
508
  function maybePersistArtificerCandidate(
508
509
  workspaceDir: string,
509
510
  stateDir: string,
@@ -743,10 +744,10 @@ export function executeNocturnalReflection(
743
744
  // -------------------------------------------------------------------------
744
745
  // Step 5: Artifact generation (Trinity or single-reflector)
745
746
  // -------------------------------------------------------------------------
746
- // eslint-disable-next-line no-useless-assignment -- Reason: initial value unused due to immediate reassignment in all branches
747
+
747
748
  let trinityArtifact: TrinityDraftArtifact | null = null;
748
749
  let trinityResult: TrinityResult | null = null;
749
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all branches before use at line 884
750
+
750
751
  let rawJson: string;
751
752
 
752
753
  if (options.skipReflector) {
@@ -966,7 +967,7 @@ export function executeNocturnalReflection(
966
967
  boundedAction: execResult.boundedAction,
967
968
  };
968
969
 
969
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in try, catch has early return
970
+
970
971
  let persistedPath: string;
971
972
  try {
972
973
  persistedPath = persistArtifact(workspaceDir, artifactWithBoundedAction);
@@ -1090,7 +1091,7 @@ export async function executeNocturnalReflectionAsync(
1090
1091
 
1091
1092
  // If runtime adapter is provided, use async Trinity path
1092
1093
  if (options.runtimeAdapter) {
1093
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
1094
+
1094
1095
  return executeNocturnalReflectionWithAdapter(workspaceDir, stateDir, options);
1095
1096
  }
1096
1097
 
@@ -1139,20 +1140,36 @@ async function executeNocturnalReflectionWithAdapter(
1139
1140
  }
1140
1141
 
1141
1142
  // Step 2: Target selection (or use override to skip)
1142
- // eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned immediately in all branches before use
1143
+
1143
1144
  let selectedPrincipleId: string | undefined;
1144
- // eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned immediately in all branches before use
1145
+
1145
1146
  let selectedSessionId: string | undefined;
1146
- // eslint-disable-next-line no-useless-assignment -- Reason: initial value unused due to immediate reassignment in all branches
1147
+
1147
1148
  let snapshot: NocturnalSessionSnapshot | null = null;
1148
1149
 
1149
1150
  if (options.principleIdOverride && options.snapshotOverride) {
1151
+ const snapshotValidation = validateNocturnalSnapshotIngress(options.snapshotOverride);
1152
+ if (snapshotValidation.status !== 'valid' || !snapshotValidation.snapshot) {
1153
+ return {
1154
+ success: false,
1155
+ skipReason: 'insufficient_snapshot_data',
1156
+ noTargetSelected: true,
1157
+ validationFailed: true,
1158
+ validationFailures: snapshotValidation.reasons.length > 0
1159
+ ? snapshotValidation.reasons
1160
+ : ['invalid snapshot override'],
1161
+ snapshot: undefined,
1162
+ diagnostics,
1163
+ trinityTelemetry: undefined,
1164
+ };
1165
+ }
1166
+
1150
1167
  // Skip Selector: use provided principleId and snapshot directly
1151
1168
  selectedPrincipleId = options.principleIdOverride;
1152
- selectedSessionId = options.snapshotOverride.sessionId;
1153
- snapshot = options.snapshotOverride;
1169
+ selectedSessionId = snapshotValidation.snapshot.sessionId;
1170
+ snapshot = snapshotValidation.snapshot;
1154
1171
  // Calculate violation density from snapshot stats for meaningful diagnostics
1155
- const snapStats = options.snapshotOverride.stats;
1172
+ const snapStats = snapshotValidation.snapshot.stats;
1156
1173
  const totalToolCalls = snapStats?.totalToolCalls ?? 0;
1157
1174
  const failureCount = snapStats?.failureCount ?? 0;
1158
1175
  const violationDensity = totalToolCalls > 0 ? failureCount / totalToolCalls : 0;
@@ -1205,9 +1222,9 @@ async function executeNocturnalReflectionWithAdapter(
1205
1222
  };
1206
1223
  }
1207
1224
 
1208
- // eslint-disable-next-line @typescript-eslint/prefer-destructuring -- Reason: selectedPrincipleId/selectedSessionId are reassignable outer lets - destructuring would shadow
1225
+
1209
1226
  selectedPrincipleId = selection.selectedPrincipleId;
1210
- // eslint-disable-next-line @typescript-eslint/prefer-destructuring -- Reason: selectedPrincipleId/selectedSessionId are reassignable outer lets - destructuring would shadow
1227
+
1211
1228
  selectedSessionId = selection.selectedSessionId;
1212
1229
 
1213
1230
  if (!selectedPrincipleId || !selectedSessionId) {
@@ -1240,10 +1257,10 @@ async function executeNocturnalReflectionWithAdapter(
1240
1257
  });
1241
1258
 
1242
1259
  // Step 4: Trinity execution via adapter (async)
1243
- // eslint-disable-next-line no-useless-assignment -- Reason: initial value unused due to immediate reassignment in all branches
1260
+
1244
1261
  let trinityArtifact: TrinityDraftArtifact | null = null;
1245
1262
  let trinityResult: TrinityResult | null = null;
1246
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all branches before use
1263
+
1247
1264
  let rawJson: string;
1248
1265
 
1249
1266
  if (options.skipReflector) {
@@ -1348,7 +1365,7 @@ async function executeNocturnalReflectionWithAdapter(
1348
1365
 
1349
1366
  // Step 7: Persist artifact
1350
1367
  const artifactWithBoundedAction = { ...arbiterResult.artifact, boundedAction: execResult.boundedAction };
1351
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in try, catch has early return
1368
+
1352
1369
  let persistedPath: string;
1353
1370
  try {
1354
1371
  persistedPath = persistArtifact(workspaceDir, artifactWithBoundedAction);
@@ -286,7 +286,7 @@ export class NocturnalTargetSelector {
286
286
  recentMaxPainScore: number;
287
287
  };
288
288
 
289
- /* eslint-disable @typescript-eslint/max-params -- Reason: Constructor requires all parameters for proper initialization */
289
+
290
290
  constructor(
291
291
  workspaceDir: string,
292
292
  stateDir: string,
@@ -518,7 +518,7 @@ export class NocturnalTargetSelector {
518
518
  *
519
519
  * This is a convenience wrapper for the common case.
520
520
  */
521
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function signature requires all parameters for type-safe selection */
521
+
522
522
  export function selectNocturnalTarget(
523
523
  workspaceDir: string,
524
524
  stateDir: string,
@@ -418,7 +418,7 @@ export class RuntimeSummaryService {
418
418
  * NOT a truth source for Phase 3 eligibility or decisions.
419
419
  * Queue is the only authoritative execution truth source.
420
420
  */
421
- /* eslint-disable @typescript-eslint/max-params -- Reason: Directive summary requires queue, directive, timestamp, and warnings */
421
+
422
422
  private static buildDirectiveSummary(
423
423
  queue: QueueItem[] | null,
424
424
  directive: DirectiveFile | null,
@@ -69,7 +69,7 @@ export class DeepReflectWorkflowManager extends WorkflowManagerBase {
69
69
  return super.startWorkflow(spec, options);
70
70
  }
71
71
 
72
- /* eslint-disable @typescript-eslint/class-methods-use-this -- Reason: Subclass overrides id generation pattern */
72
+
73
73
  protected override generateWorkflowId(): string {
74
74
  return `wf_dr_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
75
75
  }
@@ -70,7 +70,7 @@ export class EmpathyObserverWorkflowManager extends WorkflowManagerBase {
70
70
  return super.startWorkflow(spec, options);
71
71
  }
72
72
 
73
- /* eslint-disable @typescript-eslint/class-methods-use-this -- Reason: Subclass stores metadata via spec, not class fields */
73
+
74
74
  protected override createWorkflowMetadata<TResult>(
75
75
  spec: SubagentWorkflowSpec<TResult>,
76
76
  options: {
@@ -103,7 +103,7 @@ export class EmpathyObserverWorkflowManager extends WorkflowManagerBase {
103
103
  ].join('\n');
104
104
  }
105
105
 
106
- /* eslint-disable @typescript-eslint/class-methods-use-this -- Reason: Subclass overrides id generation pattern */
106
+
107
107
  protected override generateWorkflowId(): string {
108
108
  return `wf_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
109
109
  }
@@ -185,7 +185,7 @@ export const empathyObserverWorkflowSpec: SubagentWorkflowSpec<EmpathyResult> =
185
185
  ttlMs: 300_000,
186
186
  shouldDeleteSessionAfterFinalize: true,
187
187
 
188
- /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: interface method signature, unused parameter for spec compliance */
188
+
189
189
  buildPrompt(taskInput: unknown, _metadata: WorkflowMetadata): string {
190
190
  const userMessage = String(taskInput).trim();
191
191
  return [
@@ -41,6 +41,7 @@ import type { RecentPainContext } from '../evolution-worker.js';
41
41
  import * as fs from 'fs';
42
42
  import * as path from 'path';
43
43
  import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
44
+ import { validateNocturnalSnapshotIngress } from '../../core/nocturnal-snapshot-contract.js';
44
45
 
45
46
  // ─────────────────────────────────────────────────────────────────────────────
46
47
  // NocturnalResult Type Alias
@@ -82,7 +83,7 @@ export interface NocturnalWorkflowOptions {
82
83
  * - timeoutMs: 15 minutes (900000ms)
83
84
  * - ttlMs: 30 minutes (1800000ms)
84
85
  */
85
- /* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: spec methods intentionally receive unused params per interface contract */
86
+
86
87
  export const nocturnalWorkflowSpec: SubagentWorkflowSpec<NocturnalResult> = {
87
88
  workflowType: 'nocturnal',
88
89
  transport: 'runtime_direct',
@@ -115,7 +116,7 @@ export const nocturnalWorkflowSpec: SubagentWorkflowSpec<NocturnalResult> = {
115
116
  return status === 'ok';
116
117
  },
117
118
  };
118
- /* eslint-enable no-unused-vars */
119
+
119
120
 
120
121
  // ─────────────────────────────────────────────────────────────────────────────
121
122
  // NocturnalWorkflowManager
@@ -212,15 +213,17 @@ export class NocturnalWorkflowManager implements WorkflowManager {
212
213
  this.store.recordEvent(workflowId, 'nocturnal_started', null, 'active', 'TrinityRuntimeAdapter invoked', { workflowType: 'nocturnal' });
213
214
 
214
215
  // Extract snapshot and principleId from taskInput.metadata (NOC-07: Trinity async path)
215
- const snapshot = options.metadata?.snapshot as NocturnalSessionSnapshot | undefined;
216
+ const snapshotValidation = validateNocturnalSnapshotIngress(options.metadata?.snapshot);
217
+ const snapshot = snapshotValidation.snapshot;
216
218
  const principleId = options.metadata?.principleId as string | undefined;
217
219
  // Extract painContext for Selector ranking bias
218
220
  const painContext = options.metadata?.painContext as RecentPainContext | undefined;
219
221
 
220
- // Validate required metadata (prevent runtime crashes from undefined snapshot)
221
- if (!snapshot?.sessionId) {
222
- this.logger.warn(`[PD:NocturnalWorkflow] Missing snapshot.sessionId in metadata for workflow=${workflowId}, terminating`);
223
- this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', 'Missing required metadata: snapshot.sessionId', { workflowId });
222
+ if (snapshotValidation.status !== 'valid' || !snapshot) {
223
+ const reason = `Invalid snapshot ingress: ${snapshotValidation.reasons.join('; ') || 'missing snapshot'}`;
224
+ this.logger.warn(`[PD:NocturnalWorkflow] ${reason} workflow=${workflowId}`);
225
+ this.store.updateWorkflowState(workflowId, 'terminal_error');
226
+ this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', reason, { workflowId });
224
227
  return {
225
228
  workflowId,
226
229
  childSessionKey: `nocturnal:internal:${workflowId}`,
@@ -350,13 +353,13 @@ export class NocturnalWorkflowManager implements WorkflowManager {
350
353
  this.markCompleted(workflowId);
351
354
  }
352
355
 
353
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: interface contract requires this method signature, implementation is a no-op per D-10
356
+
354
357
  async notifyLifecycleEvent(
355
- // eslint-disable-next-line no-unused-vars -- Reason: interface contract requires these params but NocturnalWorkflowManager is a no-op per D-10
358
+
356
359
  _workflowId: string,
357
- // eslint-disable-next-line no-unused-vars -- Reason: interface contract requires these params but NocturnalWorkflowManager is a no-op per D-10
360
+
358
361
  _event: 'subagent_spawned' | 'subagent_ended',
359
- // eslint-disable-next-line no-unused-vars -- Reason: interface contract requires these params but NocturnalWorkflowManager is a no-op per D-10
362
+
360
363
  _data?: Record<string, unknown>
361
364
  ): Promise<void> {
362
365
  // D-10: No-op. NocturnalWorkflowManager does not use the wait-on-run pattern.
@@ -411,13 +414,13 @@ export class NocturnalWorkflowManager implements WorkflowManager {
411
414
  maxAgeMs = 30 * 60 * 1000,
412
415
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: subagentRuntime param is intentionally any for backward compatibility with callers
413
416
  subagentRuntime?: any,
414
- /* eslint-disable no-unused-vars -- Reason: type parameter names are documentation, not actual variables */
417
+
415
418
  agentSession?: {
416
419
  resolveStorePath: () => string;
417
420
  loadSessionStore: (storePath: string, opts?: { skipCache?: boolean }) => Record<string, unknown>;
418
421
  saveSessionStore: (storePath: string, store: Record<string, unknown>) => Promise<void>;
419
422
  },
420
- /* eslint-enable no-unused-vars */
423
+
421
424
  ): Promise<number> {
422
425
  const expired = this.store.getExpiredWorkflows(maxAgeMs);
423
426
 
@@ -8,12 +8,12 @@ import type {
8
8
  import { isExpectedSubagentError } from './subagent-error-utils.js';
9
9
 
10
10
  export interface TransportDriver {
11
- /* eslint-disable no-unused-vars -- Reason: interface method params are type signatures, implementations use actual values */
11
+
12
12
  run(params: RunParams): Promise<RunResult>;
13
13
  wait(params: WaitParams): Promise<WaitResult>;
14
14
  getResult(params: GetResultParams): Promise<GetResultResult>;
15
15
  cleanup(params: CleanupParams): Promise<void>;
16
- /* eslint-enable no-unused-vars */
16
+
17
17
  }
18
18
 
19
19
  export interface RunParams {
@@ -55,7 +55,7 @@ export interface CleanupParams {
55
55
  deleteTranscript?: boolean;
56
56
  }
57
57
 
58
- /* eslint-disable no-unused-vars -- Reason: type method params are type signatures, implementations use actual values */
58
+
59
59
  type PluginRuntimeSubagent = {
60
60
  run: (params: {
61
61
  sessionKey: string;
@@ -79,21 +79,21 @@ type PluginRuntimeSubagent = {
79
79
  deleteTranscript?: boolean;
80
80
  }) => Promise<void>;
81
81
  };
82
- /* eslint-enable no-unused-vars */
82
+
83
83
 
84
84
  /**
85
85
  * OpenClaw plugin SDK's agent.session namespace — always available (not gateway-scoped).
86
86
  * These functions are imported directly from OpenClaw's session store module.
87
87
  */
88
88
  export type AgentSessionAPI = {
89
- /* eslint-disable no-unused-vars -- Reason: type method params are type signatures, implementations use actual values */
89
+
90
90
  resolveStorePath: () => string;
91
91
  loadSessionStore: (storePath: string, opts?: { skipCache?: boolean }) => Record<string, unknown>;
92
92
  saveSessionStore: (storePath: string, store: Record<string, unknown>) => Promise<void>;
93
93
  resolveSessionFilePath: (sessionKey: string) => string;
94
94
  /** Optional: OpenClaw config object needed for session path resolution */
95
95
  config?: unknown;
96
- /* eslint-enable no-unused-vars */
96
+
97
97
  };
98
98
 
99
99
  export class RuntimeDirectDriver implements TransportDriver {
@@ -206,6 +206,10 @@ export class RuntimeDirectDriver implements TransportDriver {
206
206
  this.logger.info(`[PD:RuntimeDirectDriver] Gateway-scoped cleanup unavailable (${errMsg.split(':')[0]}), falling back to agent.session`);
207
207
  await this.cleanupViaAgentSession(params.sessionKey);
208
208
  this.logger.info(`[PD:RuntimeDirectDriver] Fallback cleanup succeeded`);
209
+ } else if (isNonGatewayContext && !this.agentSession) {
210
+ // #232: Both cleanup paths unavailable — this is an OpenClaw environment limitation,
211
+ // not a PD bug. Session will be cleaned up on next gateway request or TTL expiry.
212
+ this.logger.warn(`[PD:RuntimeDirectDriver] Session cleanup skipped (${errMsg.split(':')[0]}): no gateway context and agentSession unavailable — session will expire naturally`);
209
213
  } else {
210
214
  this.logger.error(`[PD:RuntimeDirectDriver] Cleanup failed: ${errMsg}`);
211
215
  throw error;
@@ -145,7 +145,7 @@ export interface WorkflowHandle {
145
145
  * ```
146
146
  */
147
147
  export interface SubagentWorkflowSpec<TResult> {
148
- /* eslint-disable no-unused-vars -- Reason: interface method params are type signatures, implementations use actual values */
148
+
149
149
  /** Unique identifier for this workflow type */
150
150
  workflowType: string;
151
151
  /** Which transport mechanism to use */
@@ -172,7 +172,7 @@ export interface SubagentWorkflowSpec<TResult> {
172
172
  * For runtime_direct: typically finalize only on 'ok', skip on 'timeout'/'error'.
173
173
  */
174
174
  shouldFinalizeOnWaitStatus: (status: 'ok' | 'error' | 'timeout') => boolean;
175
- /* eslint-enable no-unused-vars */
175
+
176
176
  }
177
177
 
178
178
  // ── Empathy Observer Specific Types ──────────────────────────────────────────
@@ -229,7 +229,7 @@ export interface EmpathyObserverWorkflowSpec extends SubagentWorkflowSpec<Empath
229
229
  * This is what the helper exposes to business modules.
230
230
  */
231
231
  export interface WorkflowManager {
232
- /* eslint-disable no-unused-vars -- Reason: interface method params are type signatures, implementations use actual values */
232
+
233
233
  /**
234
234
  * Start a new workflow.
235
235
  * Creates workflow state, spawns subagent, and returns handle.
@@ -289,7 +289,7 @@ export interface WorkflowManager {
289
289
  * Release resources (DB connections, timers).
290
290
  */
291
291
  dispose: () => void;
292
- /* eslint-enable no-unused-vars */
292
+
293
293
  }
294
294
 
295
295
  // ── Workflow Store (for SQLite persistence) ──────────────────────────────────
@@ -155,7 +155,7 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
155
155
  * Create workflow metadata for store.createWorkflow().
156
156
  * Subclasses override to add type-specific fields.
157
157
  */
158
- /* eslint-disable @typescript-eslint/class-methods-use-this -- Reason: Subclass hook that returns value via spec, not class state */
158
+
159
159
  protected createWorkflowMetadata<TResult>(
160
160
  spec: SubagentWorkflowSpec<TResult>,
161
161
  options: {
@@ -181,7 +181,7 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
181
181
  * Called after driver.run() succeeds.
182
182
  * Subclasses override to call store.createWorkflow() with type-specific metadata.
183
183
  */
184
- /* eslint-disable @typescript-eslint/max-params -- Reason: Interface hook requires all params from caller context */
184
+
185
185
  protected async createWorkflowRecord<TResult>(
186
186
  workflowId: string,
187
187
  childSessionKey: string,
@@ -213,7 +213,7 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
213
213
 
214
214
  // ── Protected Helpers ────────────────────────────────────────────────────
215
215
 
216
- /* eslint-disable @typescript-eslint/class-methods-use-this -- Reason: Helper method that delegates to spec.buildPrompt and driver.run */
216
+
217
217
  protected buildRunParams<TResult>(
218
218
  spec: SubagentWorkflowSpec<TResult>,
219
219
  options: {
@@ -311,11 +311,11 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
311
311
  status: 'ok' | 'error' | 'timeout',
312
312
  error?: string
313
313
  ): Promise<void> {
314
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in try, catch has early returns
314
+
315
315
  let workflow;
316
316
  try {
317
317
  workflow = this.store.getWorkflow(workflowId);
318
- /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: Error is handled via early returns based on status, not error value */
318
+ /* eslint-disable @typescript-eslint/no-unused-vars -- Reason: Error is handled via early returns based on status, not error value */
319
319
  } catch (_dbError) {
320
320
  // Database connection closed (e.g., by lifecycle notification dispose).
321
321
  // If subagent succeeded, this is a known race condition — the workflow