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.
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.20.0",
5
+ "version": "1.22.0",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.20.0",
3
+ "version": "1.22.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -126,7 +126,11 @@ function _handleArchiveImpl(
126
126
  updateImplementation(stateDir, implId, {
127
127
  archivedAt: new Date().toISOString(),
128
128
  });
129
- refreshPrincipleLifecycle(workspaceDir, stateDir);
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
- refreshPrincipleLifecycle(workspaceDir, stateDir);
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
- refreshPrincipleLifecycle(workspaceDir, stateDir);
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
- refreshPrincipleLifecycle(workspaceDir, stateDir);
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
- refreshPrincipleLifecycle(workspaceDir, stateDir);
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: 95,
128
+ confidence: 50,
129
129
  reasonCodes,
130
130
  evidenceSummary,
131
- nextAction: 'Define at least one concrete rule before choosing an internalization route.',
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
- refreshPrincipleLifecycle(workspaceDir, stateDir);
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
  }