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.
- package/README.md +13 -5
- package/openclaw.plugin.json +4 -4
- package/package.json +4 -2
- package/scripts/bootstrap-rules.mjs +66 -0
- package/scripts/validate-live-path.ts +356 -0
- package/src/commands/archive-impl.ts +3 -3
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +3 -3
- package/src/commands/disable-impl.ts +1 -1
- package/src/commands/evolution-status.ts +2 -2
- package/src/commands/focus.ts +2 -2
- package/src/commands/nocturnal-train.ts +6 -6
- package/src/commands/pain.ts +4 -4
- package/src/commands/pd-reflect.ts +87 -0
- package/src/commands/rollback-impl.ts +4 -4
- package/src/commands/rollback.ts +2 -2
- package/src/commands/samples.ts +2 -2
- package/src/commands/workflow-debug.ts +1 -1
- package/src/config/errors.ts +1 -1
- package/src/core/adaptive-thresholds.ts +1 -1
- package/src/core/bootstrap-rules.ts +177 -0
- package/src/core/code-implementation-storage.ts +2 -2
- package/src/core/config.ts +1 -1
- package/src/core/diagnostician-task-store.ts +2 -2
- package/src/core/empathy-keyword-matcher.ts +3 -3
- package/src/core/event-log.ts +5 -5
- package/src/core/evolution-engine.ts +4 -4
- package/src/core/evolution-logger.ts +1 -1
- package/src/core/evolution-reducer.ts +3 -3
- package/src/core/evolution-types.ts +5 -5
- package/src/core/external-training-contract.ts +1 -1
- package/src/core/focus-history.ts +14 -14
- package/src/core/hygiene/tracker.ts +1 -1
- package/src/core/init.ts +2 -2
- package/src/core/model-deployment-registry.ts +2 -2
- package/src/core/model-training-registry.ts +2 -2
- package/src/core/nocturnal-arbiter.ts +1 -1
- package/src/core/nocturnal-artificer.ts +2 -2
- package/src/core/nocturnal-candidate-scoring.ts +2 -2
- package/src/core/nocturnal-compliance.ts +3 -3
- package/src/core/nocturnal-dataset.ts +3 -3
- package/src/core/nocturnal-export.ts +4 -4
- package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
- package/src/core/nocturnal-snapshot-contract.ts +112 -0
- package/src/core/nocturnal-trajectory-extractor.ts +7 -5
- package/src/core/nocturnal-trinity.ts +27 -28
- package/src/core/pain-context-extractor.ts +3 -3
- package/src/core/pain.ts +124 -11
- package/src/core/path-resolver.ts +4 -4
- package/src/core/pd-task-reconciler.ts +10 -10
- package/src/core/pd-task-service.ts +1 -1
- package/src/core/pd-task-store.ts +1 -1
- package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
- package/src/core/principle-training-state.ts +2 -2
- package/src/core/principle-tree-ledger.ts +7 -7
- package/src/core/promotion-gate.ts +9 -9
- package/src/core/replay-engine.ts +12 -12
- package/src/core/risk-calculator.ts +1 -1
- package/src/core/rule-host-types.ts +2 -2
- package/src/core/rule-host.ts +5 -5
- package/src/core/schema/db-types.ts +1 -1
- package/src/core/schema/schema-definitions.ts +1 -1
- package/src/core/session-tracker.ts +96 -4
- package/src/core/shadow-observation-registry.ts +3 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-os-parser.ts +1 -1
- package/src/core/training-program.ts +2 -2
- package/src/core/trajectory.ts +8 -8
- package/src/core/workspace-context.ts +2 -2
- package/src/core/workspace-dir-service.ts +85 -0
- package/src/core/workspace-dir-validation.ts +30 -107
- package/src/hooks/bash-risk.ts +3 -3
- package/src/hooks/edit-verification.ts +4 -4
- package/src/hooks/gate-block-helper.ts +4 -4
- package/src/hooks/gate.ts +10 -10
- package/src/hooks/gfi-gate.ts +7 -7
- package/src/hooks/lifecycle.ts +2 -2
- package/src/hooks/llm.ts +1 -1
- package/src/hooks/pain.ts +25 -5
- package/src/hooks/progressive-trust-gate.ts +7 -7
- package/src/hooks/prompt.ts +24 -5
- package/src/hooks/subagent.ts +2 -2
- package/src/hooks/thinking-checkpoint.ts +2 -2
- package/src/hooks/trajectory-collector.ts +1 -1
- package/src/http/principles-console-route.ts +14 -6
- package/src/i18n/commands.ts +4 -0
- package/src/index.ts +181 -185
- package/src/service/central-health-service.ts +1 -1
- package/src/service/central-overview-service.ts +3 -3
- package/src/service/evolution-query-service.ts +1 -1
- package/src/service/evolution-worker.ts +209 -104
- package/src/service/health-query-service.ts +27 -17
- package/src/service/monitoring-query-service.ts +3 -3
- package/src/service/nocturnal-runtime.ts +4 -4
- package/src/service/nocturnal-service.ts +40 -23
- package/src/service/nocturnal-target-selector.ts +2 -2
- package/src/service/runtime-summary-service.ts +1 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
- package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
- package/src/service/subagent-workflow/types.ts +4 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
- package/src/service/subagent-workflow/workflow-store.ts +2 -2
- package/src/tools/critique-prompt.ts +2 -3
- package/src/tools/deep-reflect.ts +17 -16
- package/src/tools/model-index.ts +1 -1
- package/src/utils/file-lock.ts +1 -1
- package/src/utils/io.ts +7 -2
- package/src/utils/nlp.ts +1 -1
- package/src/utils/plugin-logger.ts +2 -2
- package/src/utils/retry.ts +3 -2
- package/src/utils/subagent-probe.ts +20 -33
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/pain_settings.json +1 -1
- package/tests/build-artifacts.test.ts +4 -58
- package/tests/commands/pd-reflect.test.ts +49 -0
- package/tests/core/bootstrap-rules.test.ts +582 -0
- package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
- package/tests/core/pain-auto-repair.test.ts +96 -0
- package/tests/core/pain-integration.test.ts +483 -0
- package/tests/core/pain.test.ts +5 -4
- package/tests/core/workspace-dir-service.test.ts +68 -0
- package/tests/core/workspace-dir-validation.test.ts +56 -192
- package/tests/hooks/pain.test.ts +20 -0
- package/tests/http/principles-console-route.test.ts +42 -20
- package/tests/integration/empathy-workflow-integration.test.ts +1 -2
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
- package/tests/scripts/validate-live-path.test.ts +286 -0
- package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
- package/tests/service/evolution-worker.nocturnal.test.ts +562 -6
- package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
- package/tests/utils/subagent-probe.test.ts +32 -0
- package/ui/src/charts.tsx +4 -1
- 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
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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": "
|
|
80
|
-
"bundleMd5": "
|
|
81
|
-
"builtAt": "2026-04-
|
|
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.
|
|
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
|
-
|
|
54
|
+
|
|
55
55
|
return _handleListArchivable(stateDir, isZh);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// Archive by ID
|
|
59
59
|
const targetId = subcommand;
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/commands/context.ts
CHANGED
|
@@ -98,7 +98,7 @@ function showStatus(workspaceDir: string, isZh: boolean): string {
|
|
|
98
98
|
/**
|
|
99
99
|
* Toggle a boolean setting
|
|
100
100
|
*/
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
+
|
|
101
101
|
function buildChineseOutput(
|
|
102
102
|
workspaceDir: string,
|
|
103
103
|
sessionId: string | null,
|
package/src/commands/focus.ts
CHANGED
|
@@ -281,7 +281,7 @@ async function compressFocus(
|
|
|
281
281
|
cleanupHistory(focusPath);
|
|
282
282
|
|
|
283
283
|
// 5. 压缩内容
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
570
|
+
|
|
571
571
|
let processed: { checkpointId: string; checkpointRef: string } | null;
|
|
572
572
|
try {
|
|
573
573
|
processed = program.processResult({
|