principles-disciple 1.20.0 → 1.22.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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/commands/archive-impl.ts +5 -1
- package/src/commands/disable-impl.ts +5 -1
- package/src/commands/nocturnal-review.ts +12 -0
- package/src/commands/nocturnal-rollout.ts +35 -0
- package/src/commands/promote-impl.ts +10 -2
- package/src/commands/rollback-impl.ts +5 -1
- package/src/core/principle-internalization/internalization-routing-policy.ts +3 -3
- package/src/core/principle-internalization/lifecycle-metrics.ts +3 -0
- package/src/service/nocturnal-service.ts +28 -3
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -126,7 +126,11 @@ function _handleArchiveImpl(
|
|
|
126
126
|
updateImplementation(stateDir, implId, {
|
|
127
127
|
archivedAt: new Date().toISOString(),
|
|
128
128
|
});
|
|
129
|
-
|
|
129
|
+
try {
|
|
130
|
+
refreshPrincipleLifecycle(workspaceDir, stateDir);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.warn('[archive-impl] Lifecycle refresh failed:', err instanceof Error ? err.stack : err);
|
|
133
|
+
}
|
|
130
134
|
|
|
131
135
|
return {
|
|
132
136
|
text: isZh
|
|
@@ -109,7 +109,11 @@ function _handleDisableImpl(
|
|
|
109
109
|
disabledBy: sessionId || 'manual',
|
|
110
110
|
disabledReason: reasonText,
|
|
111
111
|
});
|
|
112
|
-
|
|
112
|
+
try {
|
|
113
|
+
refreshPrincipleLifecycle(workspaceDir, stateDir);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.warn('[disable-impl] Lifecycle refresh failed:', err instanceof Error ? err.stack : err);
|
|
116
|
+
}
|
|
113
117
|
|
|
114
118
|
return {
|
|
115
119
|
text: isZh
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import * as fs from 'fs';
|
|
25
|
+
import * as path from 'path';
|
|
25
26
|
import {
|
|
26
27
|
listDatasetRecords,
|
|
27
28
|
getDatasetRecord,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
type NocturnalDatasetRecord,
|
|
32
33
|
type NocturnalReviewStatus,
|
|
33
34
|
} from '../core/nocturnal-dataset.js';
|
|
35
|
+
import { getPrincipleState, setPrincipleState } from '../core/principle-training-state.js';
|
|
34
36
|
import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
|
|
35
37
|
|
|
36
38
|
function isZh(ctx: PluginCommandContext): boolean {
|
|
@@ -194,6 +196,16 @@ export function handleNocturnalReviewCommand(ctx: PluginCommandContext): PluginC
|
|
|
194
196
|
|
|
195
197
|
const updated = updateReviewStatus(workspaceDir, fingerprint, 'approved_for_training', reason);
|
|
196
198
|
|
|
199
|
+
// #251: Sync approvedSampleCount in training store
|
|
200
|
+
try {
|
|
201
|
+
const stateDir = path.join(workspaceDir, '.state');
|
|
202
|
+
const state = getPrincipleState(stateDir, updated.principleId);
|
|
203
|
+
state.approvedSampleCount += 1;
|
|
204
|
+
setPrincipleState(stateDir, state);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.warn(`[nocturnal-review] Failed to sync approvedSampleCount for ${updated.principleId}:`, err instanceof Error ? err.stack : err);
|
|
207
|
+
}
|
|
208
|
+
|
|
197
209
|
return {
|
|
198
210
|
text: zh
|
|
199
211
|
? `样本已批准用于训练:\n fingerprint: ${updated.sampleFingerprint.substring(0, 16)}...\n principleId: ${updated.principleId}\n targetModelFamily: ${updated.targetModelFamily ?? '(none)'}\n reason: ${updated.reviewReason}`
|
|
@@ -56,6 +56,11 @@ import {
|
|
|
56
56
|
import {
|
|
57
57
|
getCheckpoint,
|
|
58
58
|
} from '../core/model-training-registry.js';
|
|
59
|
+
import {
|
|
60
|
+
runMergeGateAudit,
|
|
61
|
+
formatMergeGateAuditReport,
|
|
62
|
+
} from '../core/merge-gate-audit.js';
|
|
63
|
+
import { resolvePdPath } from '../core/paths.js';
|
|
59
64
|
|
|
60
65
|
function isZh(ctx: PluginCommandContext): boolean {
|
|
61
66
|
return String(ctx.config?.language || 'en').startsWith('zh');
|
|
@@ -257,6 +262,14 @@ ${result.passes
|
|
|
257
262
|
return { text };
|
|
258
263
|
}
|
|
259
264
|
|
|
265
|
+
// ── Merge-Gate Audit ──────────────────────────────────────────────────
|
|
266
|
+
if (subcommand === 'audit') {
|
|
267
|
+
const stateDir = resolvePdPath(workspaceDir, 'STATE_DIR');
|
|
268
|
+
const report = runMergeGateAudit(workspaceDir, stateDir);
|
|
269
|
+
const formatted = formatMergeGateAuditReport(report);
|
|
270
|
+
return { text: formatted };
|
|
271
|
+
}
|
|
272
|
+
|
|
260
273
|
// ── Advance Promotion ─────────────────────────────────────────────────
|
|
261
274
|
if (subcommand === 'advance-promotion') {
|
|
262
275
|
const checkpointId = parts[1] || checkpointIdArg;
|
|
@@ -267,6 +280,28 @@ ${result.passes
|
|
|
267
280
|
const profile = parseProfile(profileArg);
|
|
268
281
|
const hasReview = args.includes('--review');
|
|
269
282
|
const noteArg = parts.find((p) => p.startsWith('--note='))?.split('=')[1];
|
|
283
|
+
const skipAudit = args.includes('--skip-audit');
|
|
284
|
+
|
|
285
|
+
// ── Merge-gate auto-gate: block advance-promotion if audit is BLOCK ──
|
|
286
|
+
if (!skipAudit) {
|
|
287
|
+
const stateDir = resolvePdPath(workspaceDir, 'STATE_DIR');
|
|
288
|
+
const auditReport = runMergeGateAudit(workspaceDir, stateDir);
|
|
289
|
+
if (auditReport.overallStatus === 'block') {
|
|
290
|
+
return {
|
|
291
|
+
text: zh
|
|
292
|
+
? `❌ Merge-Gate 审计阻止了晋升:发现 ${auditReport.counts.block} 个阻断项
|
|
293
|
+
|
|
294
|
+
${formatMergeGateAuditReport(auditReport)}
|
|
295
|
+
|
|
296
|
+
如需强制晋升,请添加 --skip-audit 标志。`
|
|
297
|
+
: `❌ Merge-Gate audit blocked promotion: ${auditReport.counts.block} blocking issue(s) found
|
|
298
|
+
|
|
299
|
+
${formatMergeGateAuditReport(auditReport)}
|
|
300
|
+
|
|
301
|
+
To force promotion, add --skip-audit flag.`,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
270
305
|
|
|
271
306
|
try {
|
|
272
307
|
const promotion = advancePromotion(workspaceDir, {
|
|
@@ -172,7 +172,11 @@ function _handlePromoteImpl(options: PromoteImplOptions): PluginCommandResult {
|
|
|
172
172
|
disabledBy: undefined,
|
|
173
173
|
disabledReason: undefined,
|
|
174
174
|
});
|
|
175
|
-
|
|
175
|
+
try {
|
|
176
|
+
refreshPrincipleLifecycle(workspaceDir, stateDir);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.warn('[promote-impl] Lifecycle refresh failed (re-enable):', err instanceof Error ? err.stack : err);
|
|
179
|
+
}
|
|
176
180
|
|
|
177
181
|
output += isZh
|
|
178
182
|
? `\n✅ 实现已重新启用: ${implId}\n 状态: disabled -> active`
|
|
@@ -225,7 +229,11 @@ function _handlePromoteImpl(options: PromoteImplOptions): PluginCommandResult {
|
|
|
225
229
|
withLock(eventPath, () => {
|
|
226
230
|
fs.writeFileSync(eventPath, JSON.stringify(promotionEvent, null, 2), 'utf-8');
|
|
227
231
|
});
|
|
228
|
-
|
|
232
|
+
try {
|
|
233
|
+
refreshPrincipleLifecycle(workspaceDir, stateDir);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
console.warn('[promote-impl] Lifecycle refresh failed (promotion):', err instanceof Error ? err.stack : err);
|
|
236
|
+
}
|
|
229
237
|
|
|
230
238
|
output += isZh
|
|
231
239
|
? `\n\n✅ 实现已晋升: ${implId}\n 状态: candidate -> active`
|
|
@@ -185,7 +185,11 @@ function _handleRollbackImpl(
|
|
|
185
185
|
withLock(rollbackPath, () => {
|
|
186
186
|
fs.writeFileSync(rollbackPath, JSON.stringify(rollbackRecord, null, 2), 'utf-8');
|
|
187
187
|
});
|
|
188
|
-
|
|
188
|
+
try {
|
|
189
|
+
refreshPrincipleLifecycle(workspaceDir, stateDir);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.warn('[rollback-impl] Lifecycle refresh failed:', err instanceof Error ? err.stack : err);
|
|
192
|
+
}
|
|
189
193
|
|
|
190
194
|
let output = isZh
|
|
191
195
|
? `\n\u2705 \u56de\u6eda\u5b8c\u6210: ${implId}\n \u72b6\u6001: active -> disabled\n \u539f\u56e0: ${reasonText}`
|
|
@@ -121,14 +121,14 @@ export function recommendInternalizationRoute(
|
|
|
121
121
|
principle.summary.repeatedErrorSignal === 0;
|
|
122
122
|
|
|
123
123
|
if (principle.rules.length === 0) {
|
|
124
|
-
reasonCodes.push('no_material_rules');
|
|
124
|
+
reasonCodes.push('insufficient_data', 'no_material_rules');
|
|
125
125
|
return {
|
|
126
126
|
principleId: principle.principle.id,
|
|
127
127
|
route: 'defer',
|
|
128
|
-
confidence:
|
|
128
|
+
confidence: 50,
|
|
129
129
|
reasonCodes,
|
|
130
130
|
evidenceSummary,
|
|
131
|
-
nextAction: '
|
|
131
|
+
nextAction: 'No rules defined for this principle. Create at least one rule via pain→principle→rule pipeline before internalization routing can produce meaningful recommendations.',
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -11,6 +11,8 @@ export interface RuleMetricResult {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface PrincipleAdherenceResult {
|
|
14
|
+
/** True when no rules exist — all numeric fields are defaults, not computed values */
|
|
15
|
+
insufficientData?: boolean;
|
|
14
16
|
adherenceRate: number;
|
|
15
17
|
averageRuleCoverage: number;
|
|
16
18
|
averageFalsePositiveRate: number;
|
|
@@ -108,6 +110,7 @@ export function computePrincipleAdherence(
|
|
|
108
110
|
|
|
109
111
|
if (principle.rules.length === 0) {
|
|
110
112
|
return {
|
|
113
|
+
insufficientData: true,
|
|
111
114
|
adherenceRate: 0,
|
|
112
115
|
averageRuleCoverage: 0,
|
|
113
116
|
averageFalsePositiveRate: 0,
|
|
@@ -98,9 +98,24 @@ import {
|
|
|
98
98
|
} from './nocturnal-runtime.js';
|
|
99
99
|
import { NocturnalPathResolver } from '../core/nocturnal-paths.js';
|
|
100
100
|
import { registerSample } from '../core/nocturnal-dataset.js';
|
|
101
|
+
import { getPrincipleState, setPrincipleState } from '../core/principle-training-state.js';
|
|
101
102
|
import type { Implementation } from '../types/principle-tree-schema.js';
|
|
102
103
|
import { validateNocturnalSnapshotIngress } from '../core/nocturnal-snapshot-contract.js';
|
|
103
104
|
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// #251: Sync trainingStore sample counts after registration
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
function incrementGeneratedSampleCount(stateDir: string, principleId: string): void {
|
|
110
|
+
try {
|
|
111
|
+
const state = getPrincipleState(stateDir, principleId);
|
|
112
|
+
state.generatedSampleCount += 1;
|
|
113
|
+
setPrincipleState(stateDir, state);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.warn(`[nocturnal-service] Failed to sync generatedSampleCount for ${principleId}:`, err instanceof Error ? err.stack : err);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
104
119
|
// ---------------------------------------------------------------------------
|
|
105
120
|
// Types
|
|
106
121
|
// ---------------------------------------------------------------------------
|
|
@@ -467,7 +482,11 @@ function persistCodeCandidate(
|
|
|
467
482
|
implementationId,
|
|
468
483
|
createdAt: now,
|
|
469
484
|
});
|
|
470
|
-
|
|
485
|
+
try {
|
|
486
|
+
refreshPrincipleLifecycle(workspaceDir, stateDir);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
console.warn('[nocturnal-service] Lifecycle refresh failed after code candidate persistence:', err instanceof Error ? err.stack : err);
|
|
489
|
+
}
|
|
471
490
|
return {
|
|
472
491
|
status: 'persisted_candidate',
|
|
473
492
|
ruleResolution: {
|
|
@@ -993,7 +1012,10 @@ export function executeNocturnalReflection(
|
|
|
993
1012
|
// Approved artifacts must enter the dataset registry so they can be reviewed
|
|
994
1013
|
// before export. Without this, new samples never appear in the review queue.
|
|
995
1014
|
try {
|
|
996
|
-
registerSample(workspaceDir, arbiterResult.artifact, persistedPath, null);
|
|
1015
|
+
const regResult = registerSample(workspaceDir, arbiterResult.artifact, persistedPath, null);
|
|
1016
|
+
if (regResult.isNew) {
|
|
1017
|
+
incrementGeneratedSampleCount(stateDir, arbiterResult.artifact.principleId);
|
|
1018
|
+
}
|
|
997
1019
|
} catch (err) {
|
|
998
1020
|
// Non-fatal: artifact is persisted, registry is secondary.
|
|
999
1021
|
// Log but don't fail the run.
|
|
@@ -1380,7 +1402,10 @@ async function executeNocturnalReflectionWithAdapter(
|
|
|
1380
1402
|
|
|
1381
1403
|
// Step 8: Register in dataset lineage
|
|
1382
1404
|
try {
|
|
1383
|
-
registerSample(workspaceDir, arbiterResult.artifact, persistedPath, null);
|
|
1405
|
+
const regResult = registerSample(workspaceDir, arbiterResult.artifact, persistedPath, null);
|
|
1406
|
+
if (regResult.isNew) {
|
|
1407
|
+
incrementGeneratedSampleCount(stateDir, arbiterResult.artifact.principleId);
|
|
1408
|
+
}
|
|
1384
1409
|
} catch (err) {
|
|
1385
1410
|
console.warn(`[nocturnal-service] Failed to register sample in dataset registry: ${String(err)}`);
|
|
1386
1411
|
}
|