principles-disciple 1.7.6 → 1.7.8

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 (106) hide show
  1. package/dist/commands/context.js +5 -15
  2. package/dist/commands/evolution-status.js +2 -9
  3. package/dist/commands/export.js +61 -8
  4. package/dist/commands/nocturnal-review.d.ts +24 -0
  5. package/dist/commands/nocturnal-review.js +265 -0
  6. package/dist/commands/nocturnal-rollout.d.ts +27 -0
  7. package/dist/commands/nocturnal-rollout.js +671 -0
  8. package/dist/commands/nocturnal-train.d.ts +25 -0
  9. package/dist/commands/nocturnal-train.js +919 -0
  10. package/dist/commands/pain.js +8 -21
  11. package/dist/constants/tools.d.ts +2 -2
  12. package/dist/constants/tools.js +1 -1
  13. package/dist/core/adaptive-thresholds.d.ts +186 -0
  14. package/dist/core/adaptive-thresholds.js +300 -0
  15. package/dist/core/config.d.ts +2 -38
  16. package/dist/core/config.js +6 -61
  17. package/dist/core/event-log.d.ts +1 -2
  18. package/dist/core/event-log.js +0 -3
  19. package/dist/core/evolution-engine.js +1 -21
  20. package/dist/core/evolution-reducer.d.ts +7 -1
  21. package/dist/core/evolution-reducer.js +56 -4
  22. package/dist/core/evolution-types.d.ts +61 -9
  23. package/dist/core/evolution-types.js +31 -9
  24. package/dist/core/external-training-contract.d.ts +276 -0
  25. package/dist/core/external-training-contract.js +269 -0
  26. package/dist/core/local-worker-routing.d.ts +175 -0
  27. package/dist/core/local-worker-routing.js +525 -0
  28. package/dist/core/model-deployment-registry.d.ts +218 -0
  29. package/dist/core/model-deployment-registry.js +503 -0
  30. package/dist/core/model-training-registry.d.ts +295 -0
  31. package/dist/core/model-training-registry.js +475 -0
  32. package/dist/core/nocturnal-arbiter.d.ts +159 -0
  33. package/dist/core/nocturnal-arbiter.js +534 -0
  34. package/dist/core/nocturnal-candidate-scoring.d.ts +137 -0
  35. package/dist/core/nocturnal-candidate-scoring.js +266 -0
  36. package/dist/core/nocturnal-compliance.d.ts +175 -0
  37. package/dist/core/nocturnal-compliance.js +824 -0
  38. package/dist/core/nocturnal-dataset.d.ts +224 -0
  39. package/dist/core/nocturnal-dataset.js +443 -0
  40. package/dist/core/nocturnal-executability.d.ts +85 -0
  41. package/dist/core/nocturnal-executability.js +331 -0
  42. package/dist/core/nocturnal-export.d.ts +124 -0
  43. package/dist/core/nocturnal-export.js +275 -0
  44. package/dist/core/nocturnal-paths.d.ts +124 -0
  45. package/dist/core/nocturnal-paths.js +214 -0
  46. package/dist/core/nocturnal-trajectory-extractor.d.ts +242 -0
  47. package/dist/core/nocturnal-trajectory-extractor.js +307 -0
  48. package/dist/core/nocturnal-trinity.d.ts +311 -0
  49. package/dist/core/nocturnal-trinity.js +880 -0
  50. package/dist/core/paths.d.ts +6 -0
  51. package/dist/core/paths.js +6 -0
  52. package/dist/core/principle-training-state.d.ts +121 -0
  53. package/dist/core/principle-training-state.js +321 -0
  54. package/dist/core/promotion-gate.d.ts +238 -0
  55. package/dist/core/promotion-gate.js +529 -0
  56. package/dist/core/session-tracker.d.ts +10 -0
  57. package/dist/core/session-tracker.js +14 -0
  58. package/dist/core/shadow-observation-registry.d.ts +217 -0
  59. package/dist/core/shadow-observation-registry.js +308 -0
  60. package/dist/core/training-program.d.ts +233 -0
  61. package/dist/core/training-program.js +433 -0
  62. package/dist/core/trajectory.d.ts +95 -1
  63. package/dist/core/trajectory.js +220 -6
  64. package/dist/core/workspace-context.d.ts +0 -6
  65. package/dist/core/workspace-context.js +0 -12
  66. package/dist/hooks/bash-risk.d.ts +6 -6
  67. package/dist/hooks/bash-risk.js +8 -8
  68. package/dist/hooks/gate-block-helper.js +1 -1
  69. package/dist/hooks/gate.d.ts +1 -1
  70. package/dist/hooks/gate.js +2 -2
  71. package/dist/hooks/gfi-gate.d.ts +3 -3
  72. package/dist/hooks/gfi-gate.js +15 -14
  73. package/dist/hooks/pain.js +6 -9
  74. package/dist/hooks/progressive-trust-gate.d.ts +21 -49
  75. package/dist/hooks/progressive-trust-gate.js +51 -204
  76. package/dist/hooks/prompt.d.ts +11 -11
  77. package/dist/hooks/prompt.js +158 -72
  78. package/dist/hooks/subagent.js +43 -6
  79. package/dist/i18n/commands.js +8 -8
  80. package/dist/index.js +129 -28
  81. package/dist/service/evolution-worker.d.ts +42 -4
  82. package/dist/service/evolution-worker.js +321 -13
  83. package/dist/service/nocturnal-runtime.d.ts +183 -0
  84. package/dist/service/nocturnal-runtime.js +352 -0
  85. package/dist/service/nocturnal-service.d.ts +163 -0
  86. package/dist/service/nocturnal-service.js +787 -0
  87. package/dist/service/nocturnal-target-selector.d.ts +145 -0
  88. package/dist/service/nocturnal-target-selector.js +315 -0
  89. package/dist/service/phase3-input-filter.d.ts +2 -23
  90. package/dist/service/phase3-input-filter.js +3 -27
  91. package/dist/service/runtime-summary-service.d.ts +0 -10
  92. package/dist/service/runtime-summary-service.js +1 -54
  93. package/dist/tools/deep-reflect.js +2 -1
  94. package/dist/types/event-types.d.ts +2 -10
  95. package/dist/types/runtime-summary.d.ts +1 -8
  96. package/dist/types.d.ts +0 -3
  97. package/dist/types.js +0 -2
  98. package/openclaw.plugin.json +1 -1
  99. package/package.json +1 -1
  100. package/templates/langs/en/skills/pd-mentor/SKILL.md +5 -5
  101. package/templates/langs/zh/skills/pd-mentor/SKILL.md +5 -5
  102. package/templates/pain_settings.json +0 -6
  103. package/dist/commands/trust.d.ts +0 -4
  104. package/dist/commands/trust.js +0 -78
  105. package/dist/core/trust-engine.d.ts +0 -96
  106. package/dist/core/trust-engine.js +0 -286
@@ -12,6 +12,9 @@ export declare const PD_DIRS: {
12
12
  SESSIONS: string;
13
13
  PAIN_SAMPLES: string;
14
14
  LOCKS: string;
15
+ NOCTURNAL_SAMPLES: string;
16
+ NOCTURNAL_MEMORY: string;
17
+ NOCTURNAL_EXPORTS: string;
15
18
  };
16
19
  /**
17
20
  * Standard File Path Mappings
@@ -39,6 +42,9 @@ export declare const PD_FILES: {
39
42
  SESSION_DIR: string;
40
43
  DICTIONARY: string;
41
44
  PRINCIPLE_BLACKLIST: string;
45
+ NOCTURNAL_SAMPLES_DIR: string;
46
+ NOCTURNAL_MEMORY_DIR: string;
47
+ NOCTURNAL_EXPORTS_DIR: string;
42
48
  PLAN: string;
43
49
  MEMORY_MD: string;
44
50
  HEARTBEAT: string;
@@ -26,6 +26,9 @@ export const PD_DIRS = {
26
26
  SESSIONS: posixJoin('.state', 'sessions'),
27
27
  PAIN_SAMPLES: posixJoin('memory', 'pain'),
28
28
  LOCKS: posixJoin('memory', '.locks'),
29
+ NOCTURNAL_SAMPLES: posixJoin('.state', 'nocturnal', 'samples'),
30
+ NOCTURNAL_MEMORY: posixJoin('.state', 'nocturnal', 'memory'),
31
+ NOCTURNAL_EXPORTS: posixJoin('.state', 'exports', 'orpo'),
29
32
  };
30
33
  /**
31
34
  * Standard File Path Mappings
@@ -53,6 +56,9 @@ export const PD_FILES = {
53
56
  SESSION_DIR: PD_DIRS.SESSIONS,
54
57
  DICTIONARY: posixJoin(PD_DIRS.STATE, 'pain_dictionary.json'),
55
58
  PRINCIPLE_BLACKLIST: posixJoin(PD_DIRS.STATE, 'principle_blacklist.json'),
59
+ NOCTURNAL_SAMPLES_DIR: PD_DIRS.NOCTURNAL_SAMPLES,
60
+ NOCTURNAL_MEMORY_DIR: PD_DIRS.NOCTURNAL_MEMORY,
61
+ NOCTURNAL_EXPORTS_DIR: PD_DIRS.NOCTURNAL_EXPORTS,
56
62
  PLAN: 'PLAN.md',
57
63
  MEMORY_MD: 'MEMORY.md',
58
64
  HEARTBEAT: 'HEARTBEAT.md',
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Principle Training State Store
3
+ * ================================================================
4
+ *
5
+ * Independent persistence layer for principle internalization tracking.
6
+ * Clearly separates the four truth states defined in the architecture:
7
+ * 1. sample_generated — reflection pair produced and passed arbiter
8
+ * 2. sample_included_in_train_run — training run consumed the sample
9
+ * 3. checkpoint_deployed — adapter/checkpoint is routable in OpenClaw
10
+ * 4. behavior_internalized — deployed worker improves on holdout eval
11
+ *
12
+ * DESIGN CONSTRAINTS (Phase 1):
13
+ * - No runtime target selection (Task 1.2 scope)
14
+ * - No training logic (Phase 2+ scope)
15
+ * - No evolution-reducer modifications
16
+ *
17
+ * FILE: {stateDir}/principle_training_state.json
18
+ */
19
+ import type { PrincipleEvaluatorLevel } from './evolution-types.js';
20
+ /** File name for principle training state persistence */
21
+ export declare const PRINCIPLE_TRAINING_FILE = "principle_training_state.json";
22
+ /**
23
+ * Internalization status — tracks progress through the four truth states:
24
+ * sample_generated → sample_included_in_train_run → checkpoint_deployed → behavior_internalized
25
+ */
26
+ export type InternalizationStatus = 'prompt_only' | 'needs_training' | 'in_training' | 'deployed_pending_eval' | 'internalized' | 'regressed';
27
+ /**
28
+ * Per-principle training state record.
29
+ * Tracks the full lineage from sample generation through deployment.
30
+ */
31
+ export interface PrincipleTrainingState {
32
+ /** Principle identifier (e.g., "T-01", "P_write_before_delete") */
33
+ principleId: string;
34
+ /** Evaluability classification — controls whether automatic targeting is allowed */
35
+ evaluability: PrincipleEvaluatorLevel;
36
+ /** Number of applicable decision-point opportunities observed */
37
+ applicableOpportunityCount: number;
38
+ /** Number of violations of this principle observed */
39
+ observedViolationCount: number;
40
+ /** Observed compliance rate (0.0 – 1.0) */
41
+ complianceRate: number;
42
+ /** Trend direction for violations (+1 = improving, 0 = stable, -1 = worsening) */
43
+ violationTrend: number;
44
+ /** Number of reflection samples generated for this principle */
45
+ generatedSampleCount: number;
46
+ /** Number of generated samples approved by arbiter */
47
+ approvedSampleCount: number;
48
+ /** Training run IDs that included samples from this principle */
49
+ includedTrainRunIds: string[];
50
+ /** Deployed checkpoint IDs for this principle */
51
+ deployedCheckpointIds: string[];
52
+ /** Last holdout evaluation score (0.0 – 1.0), if available */
53
+ lastEvalScore?: number;
54
+ /** Current internalization status */
55
+ internalizationStatus: InternalizationStatus;
56
+ }
57
+ /**
58
+ * The full principle training store — a map of principleId -> state.
59
+ * Stored as a single JSON object in principle_training_state.json.
60
+ */
61
+ export type PrincipleTrainingStore = Record<string, PrincipleTrainingState>;
62
+ /**
63
+ * Creates a default principle training state for a newly tracked principle.
64
+ * Safe defaults: evaluability='manual_only' (requires explicit upgrade to auto-training),
65
+ * internalizationStatus='prompt_only' (starts as prompt-only, not eligible for
66
+ * automatic training until upgraded).
67
+ */
68
+ export declare function createDefaultPrincipleState(principleId: string): PrincipleTrainingState;
69
+ /**
70
+ * Loads the full principle training store from disk.
71
+ * Returns an empty store if the file does not exist or is corrupted.
72
+ */
73
+ export declare function loadStore(stateDir: string): PrincipleTrainingStore;
74
+ /**
75
+ * Synchronously saves the full principle training store to disk.
76
+ * Uses file locking to prevent concurrent write corruption.
77
+ */
78
+ export declare function saveStore(stateDir: string, store: PrincipleTrainingStore): void;
79
+ /**
80
+ * Asynchronously loads the full principle training store from disk.
81
+ * Returns an empty store if the file does not exist or is corrupted.
82
+ */
83
+ export declare function loadStoreAsync(stateDir: string): Promise<PrincipleTrainingStore>;
84
+ /**
85
+ * Asynchronously saves the full principle training store to disk.
86
+ * Uses file locking to prevent concurrent write corruption.
87
+ */
88
+ export declare function saveStoreAsync(stateDir: string, store: PrincipleTrainingStore): Promise<void>;
89
+ /**
90
+ * Gets the training state for a single principle.
91
+ * Returns a default state if the principle is not yet tracked.
92
+ */
93
+ export declare function getPrincipleState(stateDir: string, principleId: string): PrincipleTrainingState;
94
+ /**
95
+ * Updates or inserts the training state for a single principle.
96
+ * Persists the full store after the update.
97
+ *
98
+ * Uses file locking around the entire read-modify-write sequence to prevent
99
+ * concurrent updates from causing lost writes.
100
+ */
101
+ export declare function setPrincipleState(stateDir: string, state: PrincipleTrainingState): void;
102
+ /**
103
+ * Removes a principle from the training store.
104
+ * Does nothing if the principle is not tracked.
105
+ *
106
+ * Uses file locking around the entire read-modify-write sequence.
107
+ */
108
+ export declare function removePrincipleState(stateDir: string, principleId: string): void;
109
+ /**
110
+ * Returns all principles currently tracked in the store.
111
+ */
112
+ export declare function listPrincipleIds(stateDir: string): string[];
113
+ /**
114
+ * Returns all principles matching a given internalization status.
115
+ */
116
+ export declare function listPrinciplesByStatus(stateDir: string, status: InternalizationStatus): PrincipleTrainingState[];
117
+ /**
118
+ * Returns all principles with 'deterministic' or 'weak_heuristic' evaluability
119
+ * that are eligible for automatic nocturnal targeting.
120
+ */
121
+ export declare function listEvaluablePrinciples(stateDir: string): PrincipleTrainingState[];
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Principle Training State Store
3
+ * ================================================================
4
+ *
5
+ * Independent persistence layer for principle internalization tracking.
6
+ * Clearly separates the four truth states defined in the architecture:
7
+ * 1. sample_generated — reflection pair produced and passed arbiter
8
+ * 2. sample_included_in_train_run — training run consumed the sample
9
+ * 3. checkpoint_deployed — adapter/checkpoint is routable in OpenClaw
10
+ * 4. behavior_internalized — deployed worker improves on holdout eval
11
+ *
12
+ * DESIGN CONSTRAINTS (Phase 1):
13
+ * - No runtime target selection (Task 1.2 scope)
14
+ * - No training logic (Phase 2+ scope)
15
+ * - No evolution-reducer modifications
16
+ *
17
+ * FILE: {stateDir}/principle_training_state.json
18
+ */
19
+ import * as fs from 'fs';
20
+ import * as path from 'path';
21
+ import { withLock, withLockAsync } from '../utils/file-lock.js';
22
+ // ---------------------------------------------------------------------------
23
+ // Constants
24
+ // ---------------------------------------------------------------------------
25
+ /** File name for principle training state persistence */
26
+ export const PRINCIPLE_TRAINING_FILE = 'principle_training_state.json';
27
+ // ---------------------------------------------------------------------------
28
+ // Valid Status Values (for validation)
29
+ // ---------------------------------------------------------------------------
30
+ const VALID_EVALUABILITIES = ['deterministic', 'weak_heuristic', 'manual_only'];
31
+ const VALID_INTERNALIZATION_STATUSES = [
32
+ 'prompt_only',
33
+ 'needs_training',
34
+ 'in_training',
35
+ 'deployed_pending_eval',
36
+ 'internalized',
37
+ 'regressed',
38
+ ];
39
+ // ---------------------------------------------------------------------------
40
+ // Schema Version
41
+ // ---------------------------------------------------------------------------
42
+ // Factory
43
+ // ---------------------------------------------------------------------------
44
+ /**
45
+ * Creates a default principle training state for a newly tracked principle.
46
+ * Safe defaults: evaluability='manual_only' (requires explicit upgrade to auto-training),
47
+ * internalizationStatus='prompt_only' (starts as prompt-only, not eligible for
48
+ * automatic training until upgraded).
49
+ */
50
+ export function createDefaultPrincipleState(principleId) {
51
+ return {
52
+ principleId,
53
+ evaluability: 'manual_only', // Safe default: requires explicit upgrade
54
+ applicableOpportunityCount: 0,
55
+ observedViolationCount: 0,
56
+ complianceRate: 0,
57
+ violationTrend: 0,
58
+ generatedSampleCount: 0,
59
+ approvedSampleCount: 0,
60
+ includedTrainRunIds: [],
61
+ deployedCheckpointIds: [],
62
+ internalizationStatus: 'prompt_only', // Safe default: starts as prompt-only
63
+ };
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // File Operations
67
+ // ---------------------------------------------------------------------------
68
+ function getFilePath(stateDir) {
69
+ return path.join(stateDir, PRINCIPLE_TRAINING_FILE);
70
+ }
71
+ /**
72
+ * Applies migration-safe defaults to a raw parsed store.
73
+ * Handles:
74
+ * - Missing top-level entries (principles added since last load)
75
+ * - Missing fields on existing entries (schema evolution)
76
+ * - Invalid enum values (falls back to safe defaults)
77
+ * - NaN / out-of-range numeric values (clamped or defaulted)
78
+ */
79
+ function applyMigrationDefaults(raw) {
80
+ if (!raw || typeof raw !== 'object') {
81
+ return {};
82
+ }
83
+ const store = raw;
84
+ const result = {};
85
+ for (const [principleId, state] of Object.entries(store)) {
86
+ if (!state || typeof state !== 'object') {
87
+ // Corrupted entry — skip
88
+ continue;
89
+ }
90
+ const s = state;
91
+ // evaluability — validate enum
92
+ const rawEval = s.evaluability;
93
+ const evaluability = VALID_EVALUABILITIES.includes(rawEval)
94
+ ? rawEval
95
+ : 'manual_only';
96
+ // internalizationStatus — validate enum
97
+ const rawStatus = s.internalizationStatus;
98
+ const internalizationStatus = VALID_INTERNALIZATION_STATUSES.includes(rawStatus)
99
+ ? rawStatus
100
+ : 'prompt_only';
101
+ // Numeric fields — clamp to valid ranges
102
+ const applicableOpportunityCount = clampInt(s.applicableOpportunityCount, 0, Infinity, 0);
103
+ const observedViolationCount = clampInt(s.observedViolationCount, 0, Infinity, 0);
104
+ const complianceRate = clampFloat(s.complianceRate, 0, 1, 0);
105
+ const violationTrend = clampFloat(s.violationTrend, -1, 1, 0);
106
+ const generatedSampleCount = clampInt(s.generatedSampleCount, 0, Infinity, 0);
107
+ const approvedSampleCount = clampInt(s.approvedSampleCount, 0, Infinity, 0);
108
+ // Optional float — only set if in range [0, 1]
109
+ const rawLastEval = s.lastEvalScore;
110
+ let lastEvalScore;
111
+ if (rawLastEval != null && typeof rawLastEval === 'number') {
112
+ const clamped = Math.max(0, Math.min(1, rawLastEval));
113
+ if (Number.isFinite(clamped)) {
114
+ lastEvalScore = clamped;
115
+ }
116
+ }
117
+ // Arrays — ensure always arrays
118
+ const includedTrainRunIds = Array.isArray(s.includedTrainRunIds)
119
+ ? s.includedTrainRunIds.filter((id) => typeof id === 'string')
120
+ : [];
121
+ const deployedCheckpointIds = Array.isArray(s.deployedCheckpointIds)
122
+ ? s.deployedCheckpointIds.filter((id) => typeof id === 'string')
123
+ : [];
124
+ // Skip entries where the stored principleId doesn't match the map key.
125
+ // This indicates a corrupted or tampered entry.
126
+ const storedPrincipleId = s.principleId;
127
+ if (typeof storedPrincipleId !== 'string' || storedPrincipleId !== principleId) {
128
+ continue;
129
+ }
130
+ result[principleId] = {
131
+ principleId,
132
+ evaluability,
133
+ applicableOpportunityCount,
134
+ observedViolationCount,
135
+ complianceRate,
136
+ violationTrend,
137
+ generatedSampleCount,
138
+ approvedSampleCount,
139
+ includedTrainRunIds,
140
+ deployedCheckpointIds,
141
+ lastEvalScore,
142
+ internalizationStatus,
143
+ };
144
+ }
145
+ return result;
146
+ }
147
+ /** Clamp an unknown value to a float range, returning default if invalid */
148
+ function clampFloat(value, min, max, fallback) {
149
+ if (typeof value !== 'number' || !Number.isFinite(value))
150
+ return fallback;
151
+ return Math.max(min, Math.min(max, value));
152
+ }
153
+ /** Clamp an unknown value to an integer range, returning default if invalid */
154
+ function clampInt(value, min, max, fallback) {
155
+ if (typeof value !== 'number' || !Number.isFinite(value))
156
+ return fallback;
157
+ const rounded = Math.round(value);
158
+ return Math.max(min, Math.min(max, rounded));
159
+ }
160
+ // ---------------------------------------------------------------------------
161
+ // Synchronous Read/Write
162
+ // ---------------------------------------------------------------------------
163
+ /**
164
+ * Loads the full principle training store from disk.
165
+ * Returns an empty store if the file does not exist or is corrupted.
166
+ */
167
+ export function loadStore(stateDir) {
168
+ const filePath = getFilePath(stateDir);
169
+ if (!fs.existsSync(filePath)) {
170
+ return {};
171
+ }
172
+ try {
173
+ const raw = fs.readFileSync(filePath, 'utf-8');
174
+ const parsed = JSON.parse(raw);
175
+ return applyMigrationDefaults(parsed);
176
+ }
177
+ catch (err) {
178
+ // Corrupted file — fail-safe, return empty store
179
+ console.warn(`[principle-training-state] Failed to load store from "${filePath}": ${err instanceof Error ? err.message : String(err)}. Returning empty store.`);
180
+ return {};
181
+ }
182
+ }
183
+ /**
184
+ * Synchronously saves the full principle training store to disk.
185
+ * Uses file locking to prevent concurrent write corruption.
186
+ */
187
+ export function saveStore(stateDir, store) {
188
+ const filePath = getFilePath(stateDir);
189
+ const dir = path.dirname(filePath);
190
+ if (!fs.existsSync(dir)) {
191
+ fs.mkdirSync(dir, { recursive: true });
192
+ }
193
+ withLock(filePath, () => {
194
+ fs.writeFileSync(filePath, JSON.stringify(store, null, 2), 'utf-8');
195
+ });
196
+ }
197
+ // ---------------------------------------------------------------------------
198
+ // Async Read/Write (for use in async contexts)
199
+ // ---------------------------------------------------------------------------
200
+ /**
201
+ * Asynchronously loads the full principle training store from disk.
202
+ * Returns an empty store if the file does not exist or is corrupted.
203
+ */
204
+ export async function loadStoreAsync(stateDir) {
205
+ const filePath = getFilePath(stateDir);
206
+ if (!fs.existsSync(filePath)) {
207
+ return {};
208
+ }
209
+ try {
210
+ const raw = await fs.promises.readFile(filePath, 'utf-8');
211
+ const parsed = JSON.parse(raw);
212
+ return applyMigrationDefaults(parsed);
213
+ }
214
+ catch (err) {
215
+ console.warn(`[principle-training-state] Failed to load store asynchronously from "${filePath}": ${err instanceof Error ? err.message : String(err)}. Returning empty store.`);
216
+ return {};
217
+ }
218
+ }
219
+ /**
220
+ * Asynchronously saves the full principle training store to disk.
221
+ * Uses file locking to prevent concurrent write corruption.
222
+ */
223
+ export async function saveStoreAsync(stateDir, store) {
224
+ const filePath = getFilePath(stateDir);
225
+ const dir = path.dirname(filePath);
226
+ if (!fs.existsSync(dir)) {
227
+ await fs.promises.mkdir(dir, { recursive: true });
228
+ }
229
+ await withLockAsync(filePath, async () => {
230
+ await fs.promises.writeFile(filePath, JSON.stringify(store, null, 2), 'utf-8');
231
+ });
232
+ }
233
+ // ---------------------------------------------------------------------------
234
+ // Single-Principle Accessors
235
+ // ---------------------------------------------------------------------------
236
+ /**
237
+ * Gets the training state for a single principle.
238
+ * Returns a default state if the principle is not yet tracked.
239
+ */
240
+ export function getPrincipleState(stateDir, principleId) {
241
+ const store = loadStore(stateDir);
242
+ return store[principleId] ?? createDefaultPrincipleState(principleId);
243
+ }
244
+ /**
245
+ * Updates or inserts the training state for a single principle.
246
+ * Persists the full store after the update.
247
+ *
248
+ * Uses file locking around the entire read-modify-write sequence to prevent
249
+ * concurrent updates from causing lost writes.
250
+ */
251
+ export function setPrincipleState(stateDir, state) {
252
+ const filePath = getFilePath(stateDir);
253
+ withLock(filePath, () => {
254
+ // Read current store while holding the lock
255
+ const store = loadStoreUnlocked(filePath);
256
+ store[state.principleId] = state;
257
+ // Write directly — no nested lock needed (we hold the outer lock)
258
+ const dir = path.dirname(filePath);
259
+ if (!fs.existsSync(dir)) {
260
+ fs.mkdirSync(dir, { recursive: true });
261
+ }
262
+ fs.writeFileSync(filePath, JSON.stringify(store, null, 2), 'utf-8');
263
+ });
264
+ }
265
+ /**
266
+ * Internal: loads store from a specific file path without acquiring a lock.
267
+ * Caller must hold the lock. Use only inside locked sections.
268
+ */
269
+ function loadStoreUnlocked(filePath) {
270
+ if (!fs.existsSync(filePath)) {
271
+ return {};
272
+ }
273
+ try {
274
+ const raw = fs.readFileSync(filePath, 'utf-8');
275
+ return applyMigrationDefaults(JSON.parse(raw));
276
+ }
277
+ catch {
278
+ return {};
279
+ }
280
+ }
281
+ /**
282
+ * Removes a principle from the training store.
283
+ * Does nothing if the principle is not tracked.
284
+ *
285
+ * Uses file locking around the entire read-modify-write sequence.
286
+ */
287
+ export function removePrincipleState(stateDir, principleId) {
288
+ const filePath = getFilePath(stateDir);
289
+ withLock(filePath, () => {
290
+ const store = loadStoreUnlocked(filePath);
291
+ if (Object.prototype.hasOwnProperty.call(store, principleId)) {
292
+ delete store[principleId];
293
+ const dir = path.dirname(filePath);
294
+ if (!fs.existsSync(dir)) {
295
+ fs.mkdirSync(dir, { recursive: true });
296
+ }
297
+ fs.writeFileSync(filePath, JSON.stringify(store, null, 2), 'utf-8');
298
+ }
299
+ });
300
+ }
301
+ /**
302
+ * Returns all principles currently tracked in the store.
303
+ */
304
+ export function listPrincipleIds(stateDir) {
305
+ return Object.keys(loadStore(stateDir));
306
+ }
307
+ /**
308
+ * Returns all principles matching a given internalization status.
309
+ */
310
+ export function listPrinciplesByStatus(stateDir, status) {
311
+ const store = loadStore(stateDir);
312
+ return Object.values(store).filter((s) => s.internalizationStatus === status);
313
+ }
314
+ /**
315
+ * Returns all principles with 'deterministic' or 'weak_heuristic' evaluability
316
+ * that are eligible for automatic nocturnal targeting.
317
+ */
318
+ export function listEvaluablePrinciples(stateDir) {
319
+ const store = loadStore(stateDir);
320
+ return Object.values(store).filter((s) => s.evaluability !== 'manual_only' && s.internalizationStatus !== 'prompt_only');
321
+ }