principles-disciple 1.15.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 (136) hide show
  1. package/README.md +13 -5
  2. package/openclaw.plugin.json +4 -4
  3. package/package.json +4 -2
  4. package/scripts/bootstrap-rules.mjs +66 -0
  5. package/scripts/validate-live-path.ts +356 -0
  6. package/src/commands/archive-impl.ts +3 -3
  7. package/src/commands/capabilities.ts +1 -1
  8. package/src/commands/context.ts +3 -3
  9. package/src/commands/disable-impl.ts +1 -1
  10. package/src/commands/evolution-status.ts +2 -2
  11. package/src/commands/focus.ts +2 -2
  12. package/src/commands/nocturnal-train.ts +6 -6
  13. package/src/commands/pain.ts +4 -4
  14. package/src/commands/pd-reflect.ts +87 -0
  15. package/src/commands/rollback-impl.ts +4 -4
  16. package/src/commands/rollback.ts +2 -2
  17. package/src/commands/samples.ts +2 -2
  18. package/src/commands/workflow-debug.ts +1 -1
  19. package/src/config/errors.ts +1 -1
  20. package/src/core/adaptive-thresholds.ts +1 -1
  21. package/src/core/bootstrap-rules.ts +177 -0
  22. package/src/core/code-implementation-storage.ts +2 -2
  23. package/src/core/config.ts +1 -1
  24. package/src/core/diagnostician-task-store.ts +2 -2
  25. package/src/core/empathy-keyword-matcher.ts +3 -3
  26. package/src/core/event-log.ts +5 -5
  27. package/src/core/evolution-engine.ts +4 -4
  28. package/src/core/evolution-logger.ts +1 -1
  29. package/src/core/evolution-reducer.ts +3 -3
  30. package/src/core/evolution-types.ts +5 -5
  31. package/src/core/external-training-contract.ts +1 -1
  32. package/src/core/focus-history.ts +14 -14
  33. package/src/core/hygiene/tracker.ts +1 -1
  34. package/src/core/init.ts +2 -2
  35. package/src/core/model-deployment-registry.ts +2 -2
  36. package/src/core/model-training-registry.ts +2 -2
  37. package/src/core/nocturnal-arbiter.ts +1 -1
  38. package/src/core/nocturnal-artificer.ts +2 -2
  39. package/src/core/nocturnal-candidate-scoring.ts +2 -2
  40. package/src/core/nocturnal-compliance.ts +3 -3
  41. package/src/core/nocturnal-dataset.ts +3 -3
  42. package/src/core/nocturnal-export.ts +4 -4
  43. package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
  44. package/src/core/nocturnal-snapshot-contract.ts +112 -0
  45. package/src/core/nocturnal-trajectory-extractor.ts +7 -5
  46. package/src/core/nocturnal-trinity.ts +27 -28
  47. package/src/core/pain-context-extractor.ts +3 -3
  48. package/src/core/pain.ts +124 -11
  49. package/src/core/path-resolver.ts +4 -4
  50. package/src/core/pd-task-reconciler.ts +10 -10
  51. package/src/core/pd-task-service.ts +1 -1
  52. package/src/core/pd-task-store.ts +1 -1
  53. package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
  54. package/src/core/principle-training-state.ts +2 -2
  55. package/src/core/principle-tree-ledger.ts +7 -7
  56. package/src/core/promotion-gate.ts +9 -9
  57. package/src/core/replay-engine.ts +12 -12
  58. package/src/core/risk-calculator.ts +1 -1
  59. package/src/core/rule-host-types.ts +2 -2
  60. package/src/core/rule-host.ts +5 -5
  61. package/src/core/schema/db-types.ts +1 -1
  62. package/src/core/schema/schema-definitions.ts +1 -1
  63. package/src/core/session-tracker.ts +96 -4
  64. package/src/core/shadow-observation-registry.ts +3 -3
  65. package/src/core/system-logger.ts +2 -2
  66. package/src/core/thinking-os-parser.ts +1 -1
  67. package/src/core/training-program.ts +2 -2
  68. package/src/core/trajectory.ts +8 -8
  69. package/src/core/workspace-context.ts +2 -2
  70. package/src/core/workspace-dir-service.ts +85 -0
  71. package/src/core/workspace-dir-validation.ts +30 -107
  72. package/src/hooks/bash-risk.ts +3 -3
  73. package/src/hooks/edit-verification.ts +4 -4
  74. package/src/hooks/gate-block-helper.ts +4 -4
  75. package/src/hooks/gate.ts +10 -10
  76. package/src/hooks/gfi-gate.ts +7 -7
  77. package/src/hooks/lifecycle.ts +2 -2
  78. package/src/hooks/llm.ts +1 -1
  79. package/src/hooks/pain.ts +25 -5
  80. package/src/hooks/progressive-trust-gate.ts +7 -7
  81. package/src/hooks/prompt.ts +24 -5
  82. package/src/hooks/subagent.ts +2 -2
  83. package/src/hooks/thinking-checkpoint.ts +2 -2
  84. package/src/hooks/trajectory-collector.ts +1 -1
  85. package/src/http/principles-console-route.ts +14 -6
  86. package/src/i18n/commands.ts +4 -0
  87. package/src/index.ts +181 -185
  88. package/src/service/central-health-service.ts +1 -1
  89. package/src/service/central-overview-service.ts +3 -3
  90. package/src/service/evolution-query-service.ts +1 -1
  91. package/src/service/evolution-worker.ts +209 -104
  92. package/src/service/health-query-service.ts +27 -17
  93. package/src/service/monitoring-query-service.ts +3 -3
  94. package/src/service/nocturnal-runtime.ts +4 -4
  95. package/src/service/nocturnal-service.ts +40 -23
  96. package/src/service/nocturnal-target-selector.ts +2 -2
  97. package/src/service/runtime-summary-service.ts +1 -1
  98. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
  99. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
  100. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
  101. package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
  102. package/src/service/subagent-workflow/types.ts +4 -4
  103. package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
  104. package/src/service/subagent-workflow/workflow-store.ts +2 -2
  105. package/src/tools/critique-prompt.ts +2 -3
  106. package/src/tools/deep-reflect.ts +17 -16
  107. package/src/tools/model-index.ts +1 -1
  108. package/src/utils/file-lock.ts +1 -1
  109. package/src/utils/io.ts +7 -2
  110. package/src/utils/nlp.ts +1 -1
  111. package/src/utils/plugin-logger.ts +2 -2
  112. package/src/utils/retry.ts +3 -2
  113. package/src/utils/subagent-probe.ts +20 -33
  114. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
  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/bootstrap-rules.test.ts +582 -0
  120. package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
  121. package/tests/core/pain-auto-repair.test.ts +96 -0
  122. package/tests/core/pain-integration.test.ts +483 -0
  123. package/tests/core/pain.test.ts +5 -4
  124. package/tests/core/workspace-dir-service.test.ts +68 -0
  125. package/tests/core/workspace-dir-validation.test.ts +56 -192
  126. package/tests/hooks/pain.test.ts +20 -0
  127. package/tests/http/principles-console-route.test.ts +42 -20
  128. package/tests/integration/empathy-workflow-integration.test.ts +1 -2
  129. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
  130. package/tests/scripts/validate-live-path.test.ts +286 -0
  131. package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
  132. package/tests/service/evolution-worker.nocturnal.test.ts +562 -6
  133. package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
  134. package/tests/utils/subagent-probe.test.ts +32 -0
  135. package/ui/src/charts.tsx +4 -1
  136. package/ui/src/pages/ThinkingModelsPage.tsx +9 -1
package/README.md CHANGED
@@ -17,12 +17,21 @@ This plugin integrates with OpenClaw to provide an evolutionary programming fram
17
17
 
18
18
  ### Slash Commands
19
19
 
20
+ All commands support **short aliases** for easier input:
21
+
22
+ | Short | Full Command | Description |
23
+ |-------|--------------|-------------|
24
+ | `/pdi` | `/pd-init` | Initialize workspace |
25
+ | `/pdb` | `/pd-bootstrap` | Scan environment tools |
26
+ | `/pdr` | `/pd-research` | Research tools and capabilities |
27
+ | `/pdt` | `/pd-thinking` | Manage thinking models |
28
+ | `/pdrl` | `/pd-reflect` | Manually trigger nocturnal reflection |
29
+ | `/pdd` | `/pd-daily` | Configure and send daily report |
30
+ | `/pdg` | `/pd-grooming` | Workspace cleanup |
31
+ | `/pdh` | `/pd-help` | Show command reference |
32
+
20
33
  | Command | Description |
21
34
  |---------|-------------|
22
- | `/pd-init` | Initialize workspace |
23
- | `/pd-bootstrap` | Scan environment tools |
24
- | `/pd-research` | Research tools and capabilities |
25
- | `/pd-thinking` | Manage thinking models |
26
35
  | `/pd-status` | View evolution status |
27
36
  | `/pd-context` | Control context injection |
28
37
  | `/pd-focus` | Focus file management |
@@ -34,7 +43,6 @@ This plugin integrates with OpenClaw to provide an evolutionary programming fram
34
43
  | `/nocturnal-train` | Nocturnal training operations |
35
44
  | `/nocturnal-rollout` | Nocturnal rollout and promotion |
36
45
  | `/pd-workflow-debug` | Debug workflow state |
37
- | `/pd-help` | Show command reference |
38
46
 
39
47
  ### Tools
40
48
 
@@ -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.15.0",
5
+ "version": "1.17.0",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
@@ -76,8 +76,8 @@
76
76
  }
77
77
  },
78
78
  "buildFingerprint": {
79
- "gitSha": "a320b37a2418",
80
- "bundleMd5": "6a5838169d98e4e6419e736dd034d93a",
81
- "builtAt": "2026-04-09T13:57:36.081Z"
79
+ "gitSha": "bce835db37a0",
80
+ "bundleMd5": "9e44177badb37ac423669fd187bf2667",
81
+ "builtAt": "2026-04-10T14:01:23.050Z"
82
82
  }
83
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -32,7 +32,9 @@
32
32
  "build:production": "node esbuild.config.js --production && node scripts/build-web.mjs --production && node scripts/verify-build.mjs",
33
33
  "test": "vitest run",
34
34
  "test:coverage": "vitest run --coverage",
35
- "lint": "eslint src/"
35
+ "lint": "eslint src/",
36
+ "bootstrap-rules": "node scripts/bootstrap-rules.mjs",
37
+ "validate-live-path": "tsx scripts/validate-live-path.ts"
36
38
  },
37
39
  "devDependencies": {
38
40
  "@testing-library/react": "^16.3.0",
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Minimal Rule Bootstrap CLI (Phase 17)
5
+ *
6
+ * Seeds 1-3 stub Rule entities for high-value deterministic principles.
7
+ * Idempotent: re-running skips already-existing rules.
8
+ *
9
+ * Usage:
10
+ * npm run bootstrap-rules # default (3 principles)
11
+ * BOOTSTRAP_LIMIT=2 npm run bootstrap-rules # limit to 2 principles
12
+ * STATE_DIR=/path/to/state npm run bootstrap-rules # custom state dir
13
+ */
14
+
15
+ import { join, dirname } from 'path';
16
+ import { fileURLToPath } from 'url';
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = dirname(__filename);
20
+
21
+ const PROJECT_ROOT = join(__dirname, '..');
22
+ const STATE_DIR = process.env.STATE_DIR || join(PROJECT_ROOT, '..', '.state');
23
+ const LIMIT = parseInt(process.env.BOOTSTRAP_LIMIT || '3', 10);
24
+
25
+ async function run() {
26
+ let BootstrapModule;
27
+ try {
28
+ BootstrapModule = await import('../dist/core/bootstrap-rules.js');
29
+ } catch {
30
+ console.error('Bootstrap module not found in dist/. Build first: node esbuild.config.js');
31
+ process.exit(1);
32
+ }
33
+
34
+ const { bootstrapRules, validateBootstrap, selectPrinciplesForBootstrap } = BootstrapModule;
35
+
36
+ console.log('Selecting principles for bootstrap...');
37
+ console.log(` State dir: ${STATE_DIR}`);
38
+ console.log(` Limit: ${LIMIT}`);
39
+
40
+ try {
41
+ const selectedIds = selectPrinciplesForBootstrap(STATE_DIR, LIMIT);
42
+ console.log(` Selected ${selectedIds.length} principle(s): ${selectedIds.join(', ')}`);
43
+
44
+ console.log('\nRunning bootstrap...');
45
+ const results = bootstrapRules(STATE_DIR, LIMIT);
46
+
47
+ for (const r of results) {
48
+ console.log(` ${r.status === 'created' ? '+' : '='} ${r.principleId} -> ${r.ruleId} (${r.status})`);
49
+ }
50
+
51
+ const created = results.filter((r) => r.status === 'created');
52
+ const skipped = results.filter((r) => r.status === 'skipped');
53
+ console.log(`\nDone: ${created.length} created, ${skipped.length} skipped.`);
54
+
55
+ if (created.length > 0) {
56
+ console.log('\nValidating...');
57
+ const valid = validateBootstrap(STATE_DIR, selectedIds);
58
+ console.log(` Validation: ${valid ? 'PASS' : 'FAIL'}`);
59
+ }
60
+ } catch (err) {
61
+ console.error(`\nBootstrap failed: ${err.message}`);
62
+ process.exit(1);
63
+ }
64
+ }
65
+
66
+ run();
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Validate Live Path Script (Phase 18)
4
+ *
5
+ * Validates the end-to-end nocturnal workflow path with bootstrapped principles.
6
+ *
7
+ * Purpose:
8
+ * - Reads bootstrapped rules from principle_training_state.json
9
+ * - Creates synthetic snapshot with recentPain to pass hasUsableNocturnalSnapshot() guard
10
+ * - Enqueues sleep_reflection task with proper file locking
11
+ * - Polls subagent_workflows.db directly for nocturnal workflows
12
+ * - Correlates workflow to queue item via taskId
13
+ * - Verifies state='completed' and explicit resolution (not 'expired')
14
+ * - Outputs summary and exits 0 on success, non-zero on failure
15
+ *
16
+ * Usage:
17
+ * tsx scripts/validate-live-path.ts [--verbose]
18
+ *
19
+ * Environment:
20
+ * WORKSPACE_DIR - Optional workspace directory (defaults to process.cwd())
21
+ */
22
+
23
+ import * as fs from 'fs';
24
+ import * as path from 'path';
25
+
26
+ // ─── Constants ───────────────────────────────────────────────────────────
27
+ const POLL_INTERVAL_MS = 5_000;
28
+ const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
29
+ const LOCK_SUFFIX = '.lock';
30
+ const LOCK_MAX_RETRIES = 50;
31
+ const LOCK_RETRY_DELAY_MS = 50;
32
+ const LOCK_STALE_MS = 30_000;
33
+ const WORKSPACE_DIR = process.env.WORKSPACE_DIR || process.cwd();
34
+ const STATE_DIR = path.join(WORKSPACE_DIR, '.state');
35
+ const QUEUE_PATH = path.join(STATE_DIR, 'EVOLUTION_QUEUE');
36
+ const QUEUE_LOCK_PATH = QUEUE_PATH + LOCK_SUFFIX;
37
+ const LEDGER_PATH = path.join(STATE_DIR, 'principle_training_state.json');
38
+ const DB_PATH = path.join(STATE_DIR, 'subagent_workflows.db');
39
+
40
+ // ─── Types ───────────────────────────────────────────────────────────────
41
+ interface LedgerRule {
42
+ id: string;
43
+ principleId: string;
44
+ action: string;
45
+ type: string;
46
+ }
47
+
48
+ interface HybridLedgerStore {
49
+ tree: {
50
+ rules: Record<string, LedgerRule>;
51
+ };
52
+ }
53
+
54
+ interface WorkflowRow {
55
+ workflow_id: string;
56
+ workflow_type: string;
57
+ state: string;
58
+ metadata_json: string;
59
+ created_at: number;
60
+ }
61
+
62
+ interface QueueItem {
63
+ id: string;
64
+ taskKind: string;
65
+ status: string;
66
+ resolution?: string;
67
+ resultRef?: string;
68
+ }
69
+
70
+ interface LockContext {
71
+ lockPath: string;
72
+ pid: number;
73
+ release: () => void;
74
+ }
75
+
76
+ // ─── File Lock Functions (simplified from file-lock.ts) ──────────────────
77
+ async function acquireLockAsync(filePath: string, options: {
78
+ lockSuffix?: string;
79
+ maxRetries?: number;
80
+ baseRetryDelayMs?: number;
81
+ lockStaleMs?: number;
82
+ } = {}): Promise<LockContext> {
83
+ const opts = {
84
+ lockSuffix: LOCK_SUFFIX,
85
+ maxRetries: LOCK_MAX_RETRIES,
86
+ baseRetryDelayMs: LOCK_RETRY_DELAY_MS,
87
+ lockStaleMs: LOCK_STALE_MS,
88
+ ...options,
89
+ };
90
+ const { pid } = process;
91
+ const lockPath = filePath + opts.lockSuffix;
92
+
93
+ for (let attempt = 0; attempt < opts.maxRetries!; attempt++) {
94
+ try {
95
+ // Check if lock file exists and is stale
96
+ if (fs.existsSync(lockPath)) {
97
+ const lockContent = fs.readFileSync(lockPath, 'utf8');
98
+ const lockPid = parseInt(lockContent, 10);
99
+ const lockStats = fs.statSync(lockPath);
100
+ const lockAge = Date.now() - lockStats.mtimeMs;
101
+
102
+ // Clean up stale lock
103
+ if (lockAge > opts.lockStaleMs!) {
104
+ fs.unlinkSync(lockPath);
105
+ } else if (lockPid !== pid) {
106
+ // Lock held by another process
107
+ await new Promise(resolve => setTimeout(resolve, opts.baseRetryDelayMs!));
108
+ continue;
109
+ }
110
+ }
111
+
112
+ // Acquire lock
113
+ fs.writeFileSync(lockPath, pid.toString(), { flag: 'wx' });
114
+ return {
115
+ lockPath,
116
+ pid,
117
+ release: () => {
118
+ try {
119
+ if (fs.existsSync(lockPath)) {
120
+ fs.unlinkSync(lockPath);
121
+ }
122
+ } catch {
123
+ // Ignore errors during release
124
+ }
125
+ },
126
+ };
127
+ } catch (error: unknown) {
128
+ if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
129
+ if (attempt < opts.maxRetries! - 1) {
130
+ await new Promise(resolve => setTimeout(resolve, opts.baseRetryDelayMs!));
131
+ continue;
132
+ }
133
+ }
134
+ throw new Error(`Failed to acquire lock for ${filePath}: ${String(error)}`);
135
+ }
136
+ }
137
+
138
+ throw new Error(`Failed to acquire lock for ${filePath} after ${opts.maxRetries} attempts`);
139
+ }
140
+
141
+ function releaseLock(ctx: LockContext): void {
142
+ ctx.release();
143
+ }
144
+
145
+ // ─── Step 1: Check bootstrapped rules ─────────────────────────────────────
146
+ function loadBootstrappedRules(): LedgerRule[] {
147
+ if (!fs.existsSync(LEDGER_PATH)) {
148
+ throw new Error('FAIL: principle_training_state.json not found. Run Phase 17 bootstrap first: npm run bootstrap-rules');
149
+ }
150
+
151
+ const ledger: HybridLedgerStore = JSON.parse(fs.readFileSync(LEDGER_PATH, 'utf8'));
152
+ const bootstrappedRules = Object.values(ledger.tree.rules).filter(r =>
153
+ r.id.endsWith('_stub_bootstrap')
154
+ );
155
+
156
+ return bootstrappedRules;
157
+ }
158
+
159
+ // ─── Step 2: Build synthetic snapshot ──────────────────────────────────────
160
+ function buildSyntheticSnapshot(taskId: string) {
161
+ return {
162
+ sessionId: `validation-${taskId}`,
163
+ startedAt: new Date().toISOString(),
164
+ updatedAt: new Date().toISOString(),
165
+ assistantTurns: [],
166
+ userTurns: [],
167
+ toolCalls: [],
168
+ painEvents: [],
169
+ gateBlocks: [],
170
+ stats: {
171
+ totalAssistantTurns: 0,
172
+ totalToolCalls: 0,
173
+ failureCount: 0,
174
+ totalPainEvents: 1,
175
+ totalGateBlocks: 0,
176
+ },
177
+ recentPain: [{
178
+ source: 'live-validation',
179
+ score: 50,
180
+ severity: 'moderate',
181
+ reason: 'Synthetic snapshot for live path validation',
182
+ createdAt: new Date().toISOString(),
183
+ }],
184
+ _dataSource: 'pain_context_fallback',
185
+ };
186
+ }
187
+
188
+ // ─── Step 3: Enqueue sleep_reflection task with proper file locking ──────────
189
+ // Uses acquireLockAsync to prevent TOCTOU race conditions (T-18-01 mitigation)
190
+ async function enqueueSleepReflectionTask(taskId: string): Promise<void> {
191
+ let lockCtx: LockContext | null = null;
192
+ try {
193
+ // Acquire lock before reading queue file (T-18-01 mitigation)
194
+ lockCtx = await acquireLockAsync(QUEUE_PATH, {
195
+ lockSuffix: LOCK_SUFFIX,
196
+ maxRetries: LOCK_MAX_RETRIES,
197
+ baseRetryDelayMs: LOCK_RETRY_DELAY_MS,
198
+ lockStaleMs: LOCK_STALE_MS,
199
+ });
200
+
201
+ let queue: QueueItem[] = [];
202
+ if (fs.existsSync(QUEUE_PATH)) {
203
+ const queueContent = fs.readFileSync(QUEUE_PATH, 'utf8');
204
+ queue = JSON.parse(queueContent);
205
+ }
206
+
207
+ queue.push({
208
+ id: taskId,
209
+ taskKind: 'sleep_reflection',
210
+ status: 'pending',
211
+ });
212
+
213
+ fs.writeFileSync(QUEUE_PATH, JSON.stringify(queue, null, 2), 'utf8');
214
+ } finally {
215
+ if (lockCtx) {
216
+ releaseLock(lockCtx);
217
+ }
218
+ }
219
+ }
220
+
221
+ // ─── Step 4: Poll workflow store (raw SQLite, no WorkflowStore import) ─────
222
+ // Uses better-sqlite3 directly to avoid WorkflowStore async initialization issues in standalone script
223
+ function listNocturnalWorkflows(): WorkflowRow[] {
224
+ if (!fs.existsSync(DB_PATH)) {
225
+ return [];
226
+ }
227
+
228
+ const Database = require('better-sqlite3');
229
+ const db = new Database(DB_PATH, { readonly: true });
230
+ const rows = db.prepare(`
231
+ SELECT workflow_id, workflow_type, state, metadata_json, created_at
232
+ FROM subagent_workflows
233
+ WHERE workflow_type = 'nocturnal'
234
+ ORDER BY created_at DESC
235
+ `).all() as WorkflowRow[];
236
+ db.close();
237
+ return rows;
238
+ }
239
+
240
+ // ─── Step 5: Correlate and verify ─────────────────────────────────────────
241
+ function verifyWorkflowCompletion(taskId: string): {
242
+ workflowId: string;
243
+ state: string;
244
+ resolution: string;
245
+ } | null {
246
+ const workflows = listNocturnalWorkflows();
247
+
248
+ for (const wf of workflows) {
249
+ const meta = JSON.parse(wf.metadata_json);
250
+ if (meta.taskId !== taskId) continue;
251
+ if (wf.state !== 'completed') continue;
252
+
253
+ // Read resolution from queue (resolution is on queue item, not on WorkflowRow)
254
+ let queue: QueueItem[] = [];
255
+ try {
256
+ if (fs.existsSync(QUEUE_PATH)) {
257
+ const queueContent = fs.readFileSync(QUEUE_PATH, 'utf8');
258
+ queue = JSON.parse(queueContent);
259
+ }
260
+ } catch {
261
+ // Queue file missing or corrupted — resolution unknown
262
+ }
263
+
264
+ const queueItem = queue.find(q => q.id === taskId);
265
+ const resolution = queueItem?.resolution;
266
+
267
+ return {
268
+ workflowId: wf.workflow_id,
269
+ state: wf.state,
270
+ resolution: resolution || 'MISSING',
271
+ };
272
+ }
273
+
274
+ return null;
275
+ }
276
+
277
+ // ─── Main ─────────────────────────────────────────────────────────────────
278
+ async function main() {
279
+ const verbose = process.argv.includes('--verbose');
280
+
281
+ // 1. Check bootstrapped rules
282
+ let rules: LedgerRule[];
283
+ try {
284
+ rules = loadBootstrappedRules();
285
+ } catch {
286
+ console.error('FAIL: principle_training_state.json not found. Run Phase 17 bootstrap first: npm run bootstrap-rules');
287
+ process.exit(1);
288
+ }
289
+
290
+ if (rules.length === 0) {
291
+ console.error('FAIL: No _stub_bootstrap rules found. Run Phase 17 bootstrap first: npm run bootstrap-rules');
292
+ process.exit(1);
293
+ }
294
+
295
+ if (verbose) {
296
+ console.log(`Found ${rules.length} bootstrapped rule(s)`);
297
+ for (const rule of rules) {
298
+ console.log(` - ${rule.id} (principleId=${rule.principleId}, action=${rule.action})`);
299
+ }
300
+ }
301
+
302
+ // 2. Generate task ID
303
+ const taskId = `validation-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
304
+
305
+ // 3. Build synthetic snapshot for validation
306
+ const snapshot = buildSyntheticSnapshot(taskId);
307
+ if (verbose) {
308
+ console.log(`Created synthetic snapshot: sessionId=${snapshot.sessionId}`);
309
+ }
310
+
311
+ // 4. Enqueue task (with lock acquisition)
312
+ try {
313
+ await enqueueSleepReflectionTask(taskId);
314
+ if (verbose) {
315
+ console.log(`Enqueued sleep_reflection task: ${taskId}`);
316
+ }
317
+ } catch (error: unknown) {
318
+ console.error('FAIL: Failed to enqueue sleep_reflection task:', String(error));
319
+ process.exit(1);
320
+ }
321
+
322
+ // 5. Poll for completion
323
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
324
+ if (verbose) {
325
+ console.log('Polling for workflow completion...');
326
+ }
327
+
328
+ while (Date.now() < deadline) {
329
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
330
+
331
+ const result = verifyWorkflowCompletion(taskId);
332
+ if (result) {
333
+ console.log(`RESULT: workflow=${result.workflowId} state=${result.state} resolution=${result.resolution} taskId=${taskId}`);
334
+
335
+ if (result.resolution === 'MISSING' || result.resolution === 'expired') {
336
+ console.error('FAIL: resolution not explicit');
337
+ process.exit(1);
338
+ }
339
+
340
+ console.log('PASS: Live path validation successful');
341
+ process.exit(0);
342
+ }
343
+
344
+ if (verbose) {
345
+ process.stdout.write('.');
346
+ }
347
+ }
348
+
349
+ console.error('FAIL: Poll timeout — no completed nocturnal workflow found for taskId');
350
+ process.exit(1);
351
+ }
352
+
353
+ main().catch(err => {
354
+ console.error('FAIL:', err);
355
+ process.exit(1);
356
+ });
@@ -51,13 +51,13 @@ export function handleArchiveImplCommand(ctx: PluginCommandContext): PluginComma
51
51
 
52
52
  // Subcommand: list
53
53
  if (subcommand === 'list') {
54
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: Mutual recursion between helper functions - reordering would break logical grouping
54
+
55
55
  return _handleListArchivable(stateDir, isZh);
56
56
  }
57
57
 
58
58
  // Archive by ID
59
59
  const targetId = subcommand;
60
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: Mutual recursion between helper functions - reordering would break logical grouping
60
+
61
61
  return _handleArchiveImpl(workspaceDir, stateDir, targetId, isZh);
62
62
  }
63
63
 
@@ -94,7 +94,7 @@ function _handleListArchivable(
94
94
  return { text: output };
95
95
  }
96
96
 
97
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: Command handler signature must match OpenClaw plugin interface - breaking API change to options objects would affect public contracts
97
+
98
98
  function _handleArchiveImpl(
99
99
  workspaceDir: string,
100
100
  stateDir: string,
@@ -24,7 +24,7 @@ function scanEnvironment(wctx: WorkspaceContext): any {
24
24
  available: true,
25
25
  version: versionLine.trim(),
26
26
  };
27
- // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: catch parameter intentionally unused - we only care that the command failed
27
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Reason: catch parameter intentionally unused - we only care that the command failed
28
28
  } catch (_e) {
29
29
  tools[tool.name] = { available: false };
30
30
  }
@@ -98,7 +98,7 @@ function showStatus(workspaceDir: string, isZh: boolean): string {
98
98
  /**
99
99
  * Toggle a boolean setting
100
100
  */
101
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: Command handler signature requires specific params - refactoring would break public API contract
101
+
102
102
  function toggleSetting(
103
103
  workspaceDir: string,
104
104
  key: 'thinkingOs' | 'reflectionLog',
@@ -213,7 +213,7 @@ function applyPreset(
213
213
  preset: 'minimal' | 'standard' | 'full',
214
214
  isZh: boolean
215
215
  ): string {
216
- // eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned in switch block immediately after declaration
216
+
217
217
  let config: ContextInjectionConfig;
218
218
 
219
219
  switch (preset) {
@@ -314,7 +314,7 @@ export function handleContextCommand(ctx: PluginCommandContext): PluginCommandRe
314
314
  // Detect language from context
315
315
  const isZh = (ctx.config?.language as string) === 'zh';
316
316
 
317
- // eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned in switch block immediately after declaration
317
+
318
318
  let result: string;
319
319
 
320
320
  switch (subCommand) {
@@ -69,7 +69,7 @@ function _handleListActive(
69
69
  return { text: output };
70
70
  }
71
71
 
72
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: Command handler signature must match OpenClaw plugin interface - breaking API change to options objects would affect public contracts
72
+
73
73
  function _handleDisableImpl(
74
74
  workspaceDir: string,
75
75
  stateDir: string,
@@ -45,7 +45,7 @@ function formatRouteRecommendations(
45
45
  .join(', ');
46
46
  }
47
47
 
48
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: Command handler signature must match OpenClaw plugin interface - breaking API change to options objects would affect public contracts
48
+
49
49
  function buildEnglishOutput(
50
50
  workspaceDir: string,
51
51
  sessionId: string | null,
@@ -97,7 +97,7 @@ function buildEnglishOutput(
97
97
  return lines.join('\n');
98
98
  }
99
99
 
100
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: Command handler signature must match OpenClaw plugin interface - breaking API change to options objects would affect public contracts
100
+
101
101
  function buildChineseOutput(
102
102
  workspaceDir: string,
103
103
  sessionId: string | null,
@@ -281,7 +281,7 @@ async function compressFocus(
281
281
  cleanupHistory(focusPath);
282
282
 
283
283
  // 5. 压缩内容
284
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both try and catch blocks
284
+
285
285
  let compressedContent: string;
286
286
  try {
287
287
  compressedContent = compressFocusContent(oldContent, workspaceDir);
@@ -477,7 +477,7 @@ export async function handleFocusCommand(
477
477
  // 检测语言(与 context.ts 保持一致)
478
478
  const isZh = (ctx.config?.language as string) === 'zh';
479
479
 
480
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all switch cases
480
+
481
481
  let result: string;
482
482
 
483
483
  switch (subCommand) {
@@ -270,12 +270,12 @@ Hardware tiers:
270
270
  // This closes the gap in the create-experiment -> trainer -> import-result chain.
271
271
  // NOTE: This blocks until training completes (could be minutes).
272
272
  if (runNow) {
273
- // eslint-disable-next-line @typescript-eslint/no-shadow -- Reason: shadowing is intentional - inner block scoping for trainer execution
273
+
274
274
  const {spec} = createResult;
275
275
  const baseDir = TRAINER_SCRIPTS_DIR;
276
276
  const scriptPath = path.join(baseDir, 'main.py');
277
277
  const specPath = path.join(baseDir, `experiment-${spec.experimentId}.json`);
278
- // eslint-disable-next-line @typescript-eslint/no-shadow -- Reason: shadowing is intentional - inner block scoping for trainer output directory
278
+
279
279
  const {outputDir} = spec;
280
280
  const resultFilePath = path.join(outputDir, `result-${spec.experimentId}.json`);
281
281
 
@@ -286,7 +286,7 @@ Hardware tiers:
286
286
  }
287
287
  fs.writeFileSync(specPath, JSON.stringify(spec, null, 2), 'utf-8');
288
288
 
289
- // eslint-disable-next-line @typescript-eslint/init-declarations, @typescript-eslint/consistent-type-imports -- Reason: type assertion required - trainer result type from external contract module
289
+
290
290
  let trainerResult!: import('../core/external-training-contract.js').TrainingExperimentResult;
291
291
 
292
292
  try {
@@ -394,7 +394,7 @@ Hardware tiers:
394
394
 
395
395
  // Process trainer result (register checkpoint)
396
396
  // dry_run returns null (no checkpoint); other statuses throw on error
397
- // eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned in try block immediately after declaration
397
+
398
398
  let processed: { checkpointId: string; checkpointRef: string } | null;
399
399
  try {
400
400
  processed = program.processResult({
@@ -536,7 +536,7 @@ Next steps:
536
536
  }
537
537
  }
538
538
 
539
- // eslint-disable-next-line @typescript-eslint/init-declarations, @typescript-eslint/no-explicit-any -- Reason: JSON.parse returns dynamic JSON - type unknown at parse time, narrowed via type narrowing below
539
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: JSON.parse returns dynamic JSON - type unknown at parse time, narrowed via type narrowing below
540
540
  let result: any;
541
541
  try {
542
542
  result = JSON.parse(resultJson);
@@ -567,7 +567,7 @@ Next steps:
567
567
 
568
568
  // Process the result
569
569
  const program = new TrainingProgram(workspaceDir);
570
- // eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned in try block immediately after declaration
570
+
571
571
  let processed: { checkpointId: string; checkpointRef: string } | null;
572
572
  try {
573
573
  processed = program.processResult({