principles-disciple 1.149.0 → 1.151.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 +0 -4
- package/dist/core/event-log.js +0 -5
- package/dist/core/intent-doc-reader.d.ts +80 -0
- package/dist/core/intent-doc-reader.js +189 -0
- package/dist/hooks/prompt-helpers.d.ts +5 -1
- package/dist/hooks/prompt-helpers.js +13 -2
- package/dist/hooks/prompt-types.d.ts +8 -0
- package/dist/hooks/prompt.js +37 -4
- package/dist/service/runtime-summary-service.js +1 -1
- package/dist/types/event-types.d.ts +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -2
- package/scripts/acceptance-test.mjs +3 -3
- package/scripts/validate-live-path.ts +0 -462
package/README.md
CHANGED
|
@@ -25,7 +25,6 @@ All commands support **short aliases** for easier input:
|
|
|
25
25
|
| `/pdb` | `/pd-bootstrap` | Scan environment tools |
|
|
26
26
|
| `/pdr` | `/pd-research` | Research tools and capabilities |
|
|
27
27
|
| `/pdt` | `/pd-thinking` | Manage thinking models |
|
|
28
|
-
| `/pdrl` | `/pd-reflect` | Manually trigger nocturnal reflection |
|
|
29
28
|
| `/pdd` | `/pd-daily` | Configure and send daily report |
|
|
30
29
|
| `/pdg` | `/pd-grooming` | Workspace cleanup |
|
|
31
30
|
| `/pdh` | `/pd-help` | Show command reference |
|
|
@@ -39,9 +38,6 @@ All commands support **short aliases** for easier input:
|
|
|
39
38
|
| `/pd-rollback` | Rollback to previous state |
|
|
40
39
|
| `/pd-export` | Export trajectory/correction data |
|
|
41
40
|
| `/pd-samples` | Review correction samples |
|
|
42
|
-
| `/pd-nocturnal-review` | Review nocturnal training samples |
|
|
43
|
-
| `/nocturnal-train` | Nocturnal training operations |
|
|
44
|
-
| `/nocturnal-rollout` | Nocturnal rollout and promotion |
|
|
45
41
|
| `/pd-workflow-debug` | Debug workflow state |
|
|
46
42
|
|
|
47
43
|
### Configuration
|
package/dist/core/event-log.js
CHANGED
|
@@ -69,8 +69,6 @@ export class EventLog {
|
|
|
69
69
|
recordPainSignal(sessionId, data) {
|
|
70
70
|
this.record('pain_signal', 'detected', sessionId, data);
|
|
71
71
|
}
|
|
72
|
-
// recordRuleMatch removed (PRI-451 Wave 1): dead code. Its only effect was
|
|
73
|
-
// incrementing stats.pain.rulesMatched (also dead, removed in Wave 1.5).
|
|
74
72
|
recordRulePromotion(data) {
|
|
75
73
|
this.record('rule_promotion', 'promoted', undefined, data);
|
|
76
74
|
}
|
|
@@ -350,9 +348,6 @@ export class EventLog {
|
|
|
350
348
|
stats.empathy.rollbackCount++;
|
|
351
349
|
stats.empathy.rolledBackScore += data.originalScore || 0;
|
|
352
350
|
}
|
|
353
|
-
// rule_match handler removed (PRI-451 Wave 1.5): recordRuleMatch is gone
|
|
354
|
-
// (Wave 1.1), so no rule_match events are emitted; stats.pain.rulesMatched
|
|
355
|
-
// was its only consumer and is also removed.
|
|
356
351
|
else if (entry.type === 'rule_promotion') {
|
|
357
352
|
// stats.pain.candidatesPromoted removed (PRI-451 Wave 1.5): dead counter.
|
|
358
353
|
stats.evolution.rulesPromoted++;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-467 — safeReadIntentDoc: plugin I/O wrapper for reading INTENT.md.
|
|
3
|
+
*
|
|
4
|
+
* Reads `.principles/INTENT.md` with:
|
|
5
|
+
* - Feature flag check FIRST (SPEC §12: flag off → flag_disabled without fs)
|
|
6
|
+
* - TTL + mtime cache (60s TTL, mtime check) mirroring prompt.ts cachedReadFile
|
|
7
|
+
* - 32KB size cap (INTENT_MAX_BYTES)
|
|
8
|
+
* - Never throws — all errors return structured `reason` + `nextAction`
|
|
9
|
+
*
|
|
10
|
+
* Trust boundary (SPEC §12.2):
|
|
11
|
+
* - Raw content is returned for the pure builder to escape; this reader does
|
|
12
|
+
* NOT escape or bound the content for prompt injection. The pure builder
|
|
13
|
+
* `buildIntentFrictionBlock` handles escaping + bounding.
|
|
14
|
+
*
|
|
15
|
+
* ERR checklist:
|
|
16
|
+
* EP-01 / ERR-001, ERR-005: raw content validated with typeof, never `as`
|
|
17
|
+
* EP-02 / ERR-025, ERR-070: production path; plugin I/O file in the whitelist
|
|
18
|
+
* EP-03 / ERR-002, ERR-014: every degraded path returns structured reason + nextAction
|
|
19
|
+
* EP-09: tests use real fs writes in temp dirs
|
|
20
|
+
*
|
|
21
|
+
* Architecture: this file is I/O (fs, path). It is whitelisted in
|
|
22
|
+
* architecture-regression.test.ts KNOWN_PLUGIN_CORE_FILES.
|
|
23
|
+
*/
|
|
24
|
+
import { type IntentDocSections, type IntentDocWarning } from '@principles/core/runtime-v2';
|
|
25
|
+
export type SafeReadIntentDocReason = 'flag_disabled' | 'not_found' | 'oversized' | 'read_error';
|
|
26
|
+
export interface IntentDoc {
|
|
27
|
+
/** Raw INTENT.md content (unescaped — caller escapes for prompt injection). */
|
|
28
|
+
raw: string;
|
|
29
|
+
/** Parsed sections from the raw content. */
|
|
30
|
+
sections: IntentDocSections;
|
|
31
|
+
/** SHA-256 content hash for deduplication and audit. */
|
|
32
|
+
contentHash: string;
|
|
33
|
+
/** Absolute path to the INTENT.md file. */
|
|
34
|
+
path: string;
|
|
35
|
+
/** ISO timestamp when the doc was read. */
|
|
36
|
+
readAt: string;
|
|
37
|
+
/** Validation warnings (missing/empty/too_vague sections). */
|
|
38
|
+
warnings: IntentDocWarning[];
|
|
39
|
+
}
|
|
40
|
+
export interface SafeReadIntentDocResult {
|
|
41
|
+
/** True when the doc was successfully read and parsed. */
|
|
42
|
+
ok: boolean;
|
|
43
|
+
/** True when the INTENT.md file exists on disk. */
|
|
44
|
+
found: boolean;
|
|
45
|
+
/** True when the intent_engineering flag is enabled. */
|
|
46
|
+
flagEnabled: boolean;
|
|
47
|
+
/** The parsed IntentDoc, present only when ok=true. */
|
|
48
|
+
doc?: IntentDoc;
|
|
49
|
+
/** Structured reason for a degraded path (present when ok=false). */
|
|
50
|
+
reason?: SafeReadIntentDocReason;
|
|
51
|
+
/** Next action for the operator (present when ok=false). */
|
|
52
|
+
nextAction?: string;
|
|
53
|
+
/** Validation warnings (always present, empty when no warnings). */
|
|
54
|
+
warnings: IntentDocWarning[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Reset the intent doc cache for test isolation.
|
|
58
|
+
* Call in beforeEach() to ensure tests don't pollute each other.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resetIntentDocCacheForTest(workspaceDir?: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* Safely read INTENT.md for prompt injection.
|
|
63
|
+
*
|
|
64
|
+
* Contract (SPEC §12):
|
|
65
|
+
* 1. Flag check FIRST via loadFeatureFlagFromConfig. Flag off → flag_disabled
|
|
66
|
+
* WITHOUT any fs access to INTENT.md.
|
|
67
|
+
* 2. Flag on → check TTL + mtime cache. If cached and fresh → return cached.
|
|
68
|
+
* 3. Otherwise → stat the file (oversized check), read, parse, validate, cache.
|
|
69
|
+
* 4. Never throws — all errors return structured reason + nextAction.
|
|
70
|
+
*
|
|
71
|
+
* @param workspaceDir - Absolute path to the workspace root
|
|
72
|
+
* @param options - Optional logger for debug-level diagnostics
|
|
73
|
+
* @returns SafeReadIntentDocResult (never throws)
|
|
74
|
+
*/
|
|
75
|
+
export declare function safeReadIntentDoc(workspaceDir: string, options?: {
|
|
76
|
+
logger?: {
|
|
77
|
+
debug?: (msg: string) => void;
|
|
78
|
+
warn?: (msg: string) => void;
|
|
79
|
+
};
|
|
80
|
+
}): SafeReadIntentDocResult;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-467 — safeReadIntentDoc: plugin I/O wrapper for reading INTENT.md.
|
|
3
|
+
*
|
|
4
|
+
* Reads `.principles/INTENT.md` with:
|
|
5
|
+
* - Feature flag check FIRST (SPEC §12: flag off → flag_disabled without fs)
|
|
6
|
+
* - TTL + mtime cache (60s TTL, mtime check) mirroring prompt.ts cachedReadFile
|
|
7
|
+
* - 32KB size cap (INTENT_MAX_BYTES)
|
|
8
|
+
* - Never throws — all errors return structured `reason` + `nextAction`
|
|
9
|
+
*
|
|
10
|
+
* Trust boundary (SPEC §12.2):
|
|
11
|
+
* - Raw content is returned for the pure builder to escape; this reader does
|
|
12
|
+
* NOT escape or bound the content for prompt injection. The pure builder
|
|
13
|
+
* `buildIntentFrictionBlock` handles escaping + bounding.
|
|
14
|
+
*
|
|
15
|
+
* ERR checklist:
|
|
16
|
+
* EP-01 / ERR-001, ERR-005: raw content validated with typeof, never `as`
|
|
17
|
+
* EP-02 / ERR-025, ERR-070: production path; plugin I/O file in the whitelist
|
|
18
|
+
* EP-03 / ERR-002, ERR-014: every degraded path returns structured reason + nextAction
|
|
19
|
+
* EP-09: tests use real fs writes in temp dirs
|
|
20
|
+
*
|
|
21
|
+
* Architecture: this file is I/O (fs, path). It is whitelisted in
|
|
22
|
+
* architecture-regression.test.ts KNOWN_PLUGIN_CORE_FILES.
|
|
23
|
+
*/
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import { INTENT_MAX_BYTES, parseIntentDocSections, computeIntentContentHash, validateIntentDocSections, } from '@principles/core/runtime-v2';
|
|
27
|
+
import { loadFeatureFlagFromConfig } from './pd-config-loader.js';
|
|
28
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
29
|
+
const INTENT_FILENAME = 'INTENT.md';
|
|
30
|
+
const INTENT_DIR = '.principles';
|
|
31
|
+
const INTENT_CACHE_TTL_MS = 60_000; // 1 minute (SPEC §12.1)
|
|
32
|
+
// ── Module-level cache (per-workspace) ───────────────────────────────────────
|
|
33
|
+
const _intentDocCache = new Map();
|
|
34
|
+
/**
|
|
35
|
+
* Reset the intent doc cache for test isolation.
|
|
36
|
+
* Call in beforeEach() to ensure tests don't pollute each other.
|
|
37
|
+
*/
|
|
38
|
+
export function resetIntentDocCacheForTest(workspaceDir) {
|
|
39
|
+
if (workspaceDir) {
|
|
40
|
+
_intentDocCache.delete(workspaceDir);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
_intentDocCache.clear();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
47
|
+
function getIntentFilePath(workspaceDir) {
|
|
48
|
+
return path.join(workspaceDir, INTENT_DIR, INTENT_FILENAME);
|
|
49
|
+
}
|
|
50
|
+
function buildDocFromRaw(raw, filePath, readAt) {
|
|
51
|
+
const sections = parseIntentDocSections(raw);
|
|
52
|
+
const warnings = validateIntentDocSections(sections);
|
|
53
|
+
const contentHash = computeIntentContentHash(raw);
|
|
54
|
+
return {
|
|
55
|
+
raw,
|
|
56
|
+
sections,
|
|
57
|
+
contentHash,
|
|
58
|
+
path: filePath,
|
|
59
|
+
readAt,
|
|
60
|
+
warnings,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// ── Main entrypoint ──────────────────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* Safely read INTENT.md for prompt injection.
|
|
66
|
+
*
|
|
67
|
+
* Contract (SPEC §12):
|
|
68
|
+
* 1. Flag check FIRST via loadFeatureFlagFromConfig. Flag off → flag_disabled
|
|
69
|
+
* WITHOUT any fs access to INTENT.md.
|
|
70
|
+
* 2. Flag on → check TTL + mtime cache. If cached and fresh → return cached.
|
|
71
|
+
* 3. Otherwise → stat the file (oversized check), read, parse, validate, cache.
|
|
72
|
+
* 4. Never throws — all errors return structured reason + nextAction.
|
|
73
|
+
*
|
|
74
|
+
* @param workspaceDir - Absolute path to the workspace root
|
|
75
|
+
* @param options - Optional logger for debug-level diagnostics
|
|
76
|
+
* @returns SafeReadIntentDocResult (never throws)
|
|
77
|
+
*/
|
|
78
|
+
export function safeReadIntentDoc(workspaceDir, options) {
|
|
79
|
+
// SPEC §12 — Flag check FIRST. Flag off → no fs access, no cache access.
|
|
80
|
+
const flagResult = loadFeatureFlagFromConfig(workspaceDir, 'intent_engineering', options?.logger);
|
|
81
|
+
if (!flagResult.enabled) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
found: false,
|
|
85
|
+
flagEnabled: false,
|
|
86
|
+
reason: 'flag_disabled',
|
|
87
|
+
nextAction: 'Enable the intent_engineering feature flag in .pd/config.yaml to read INTENT.md.',
|
|
88
|
+
warnings: [],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const filePath = getIntentFilePath(workspaceDir);
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
// Check cache freshness (TTL + mtime)
|
|
94
|
+
const cached = _intentDocCache.get(workspaceDir);
|
|
95
|
+
if (cached && (now - cached.loadedAt) < INTENT_CACHE_TTL_MS) {
|
|
96
|
+
// Cache is within TTL. Verify mtime hasn't changed.
|
|
97
|
+
try {
|
|
98
|
+
const stat = fs.statSync(filePath);
|
|
99
|
+
if (stat.mtimeMs === cached.mtime) {
|
|
100
|
+
// Cache hit — return cached doc
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
found: true,
|
|
104
|
+
flagEnabled: true,
|
|
105
|
+
doc: cached.doc,
|
|
106
|
+
warnings: cached.doc.warnings,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// File may have been deleted since caching. Fall through to not_found path.
|
|
112
|
+
// Don't return cached doc if the file no longer exists.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Fresh read path
|
|
116
|
+
try {
|
|
117
|
+
// Check existence first
|
|
118
|
+
if (!fs.existsSync(filePath)) {
|
|
119
|
+
// Invalidate stale cache entry if any
|
|
120
|
+
_intentDocCache.delete(workspaceDir);
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
found: false,
|
|
124
|
+
flagEnabled: true,
|
|
125
|
+
reason: 'not_found',
|
|
126
|
+
nextAction: 'Create .principles/INTENT.md using "pd intent init".',
|
|
127
|
+
warnings: [],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const stat = fs.statSync(filePath);
|
|
131
|
+
// SPEC §12 — 32KB size cap
|
|
132
|
+
if (stat.size > INTENT_MAX_BYTES) {
|
|
133
|
+
_intentDocCache.delete(workspaceDir);
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
found: true,
|
|
137
|
+
flagEnabled: true,
|
|
138
|
+
reason: 'oversized',
|
|
139
|
+
nextAction: `INTENT.md exceeds ${INTENT_MAX_BYTES} bytes (${stat.size} bytes). Reduce content.`,
|
|
140
|
+
warnings: [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
144
|
+
// PRI-467 review fix (P2): TOCTOU guard — re-check actual byte length
|
|
145
|
+
// after readFileSync. The file may have grown between statSync() and
|
|
146
|
+
// readFileSync(), bypassing the stat.size oversized check. Without this,
|
|
147
|
+
// an oversized file would enter parse/hash/cache as ok:true.
|
|
148
|
+
const actualBytes = Buffer.byteLength(raw, 'utf8');
|
|
149
|
+
if (actualBytes > INTENT_MAX_BYTES) {
|
|
150
|
+
_intentDocCache.delete(workspaceDir);
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
found: true,
|
|
154
|
+
flagEnabled: true,
|
|
155
|
+
reason: 'oversized',
|
|
156
|
+
nextAction: `INTENT.md exceeds ${INTENT_MAX_BYTES} bytes (${actualBytes} bytes after read). Reduce content.`,
|
|
157
|
+
warnings: [],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const doc = buildDocFromRaw(raw, filePath, new Date(now).toISOString());
|
|
161
|
+
// Update cache
|
|
162
|
+
_intentDocCache.set(workspaceDir, {
|
|
163
|
+
doc,
|
|
164
|
+
mtime: stat.mtimeMs,
|
|
165
|
+
loadedAt: now,
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
found: true,
|
|
170
|
+
flagEnabled: true,
|
|
171
|
+
doc,
|
|
172
|
+
warnings: doc.warnings,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
// ERR-002 — graceful degradation with reason + nextAction
|
|
177
|
+
_intentDocCache.delete(workspaceDir);
|
|
178
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
179
|
+
options?.logger?.warn?.(`[PD:Intent] safeReadIntentDoc failed: workspace=${workspaceDir}, error=${message}`);
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
found: false,
|
|
183
|
+
flagEnabled: true,
|
|
184
|
+
reason: 'read_error',
|
|
185
|
+
nextAction: 'Check filesystem permissions for .principles/INTENT.md.',
|
|
186
|
+
warnings: [],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -76,9 +76,13 @@ export declare function formatEvolutionPrinciples(active: EvolutionPrincipleEntr
|
|
|
76
76
|
* Assemble appendSystemContext from ordered parts.
|
|
77
77
|
*
|
|
78
78
|
* Content order (most important last):
|
|
79
|
-
* behavioral_constraints → project_context → working_memory →
|
|
79
|
+
* behavioral_constraints → project_context → intent_block → working_memory →
|
|
80
80
|
* thinking_os → evolution_principles → core_principles
|
|
81
81
|
*
|
|
82
|
+
* PRI-467: intent_block (INTENT.md reference) sits after project_context
|
|
83
|
+
* (stable reference data, lower priority than principles) and before
|
|
84
|
+
* working_memory (volatile per-session state).
|
|
85
|
+
*
|
|
82
86
|
* Pure logic — string assembly only, no I/O.
|
|
83
87
|
*
|
|
84
88
|
* @param parts Ordered content parts (empty/undefined parts are skipped)
|
|
@@ -187,9 +187,13 @@ export function formatEvolutionPrinciples(active, probation) {
|
|
|
187
187
|
* Assemble appendSystemContext from ordered parts.
|
|
188
188
|
*
|
|
189
189
|
* Content order (most important last):
|
|
190
|
-
* behavioral_constraints → project_context → working_memory →
|
|
190
|
+
* behavioral_constraints → project_context → intent_block → working_memory →
|
|
191
191
|
* thinking_os → evolution_principles → core_principles
|
|
192
192
|
*
|
|
193
|
+
* PRI-467: intent_block (INTENT.md reference) sits after project_context
|
|
194
|
+
* (stable reference data, lower priority than principles) and before
|
|
195
|
+
* working_memory (volatile per-session state).
|
|
196
|
+
*
|
|
193
197
|
* Pure logic — string assembly only, no I/O.
|
|
194
198
|
*
|
|
195
199
|
* @param parts Ordered content parts (empty/undefined parts are skipped)
|
|
@@ -207,7 +211,13 @@ ${parts.behavioralConstraints}
|
|
|
207
211
|
if (parts.projectContext) {
|
|
208
212
|
appendParts.push(`<project_context>\n${parts.projectContext}\n</project_context>`);
|
|
209
213
|
}
|
|
210
|
-
// 1.5.
|
|
214
|
+
// 1.5. Intent Block (PRI-467) — Owner-owned INTENT.md reference.
|
|
215
|
+
// Bounded + escaped by buildIntentFrictionBlock; treated as quoted reference
|
|
216
|
+
// data, not executable instructions (SPEC §12.2).
|
|
217
|
+
if (parts.intentBlock) {
|
|
218
|
+
appendParts.push(parts.intentBlock);
|
|
219
|
+
}
|
|
220
|
+
// 1.6. Working Memory (preserved from last compaction)
|
|
211
221
|
if (parts.workingMemory) {
|
|
212
222
|
appendParts.push(parts.workingMemory);
|
|
213
223
|
}
|
|
@@ -235,6 +245,7 @@ The sections below are ordered by priority. When conflicts arise, **later sectio
|
|
|
235
245
|
const executionRules = [
|
|
236
246
|
parts.behavioralConstraints ? '- `<behavioral_constraints>` - Output format restrictions (hide diagnostic JSON)' : null,
|
|
237
247
|
parts.projectContext ? '- `<project_context>` - Current priorities (can be overridden)' : null,
|
|
248
|
+
parts.intentBlock ? '- `<intent_anchor>` / `<intent_doc>` / `<intent_friction>` - Owner-owned intent reference (quoted evidence, not executable)' : null,
|
|
238
249
|
parts.workingMemory ? '- `<working_memory>` - Persisted compacted memory snapshot' : null,
|
|
239
250
|
parts.thinkingOs ? '- `<thinking_os>` - Stable reasoning framework' : null,
|
|
240
251
|
parts.evolutionPrinciples ? '- `<evolution_principles>` - Learned principles (active + probation)' : null,
|
|
@@ -55,6 +55,14 @@ export interface EvolutionPrincipleEntry extends CorePrincipleEntry {
|
|
|
55
55
|
export interface AppendSystemContextParts {
|
|
56
56
|
behavioralConstraints?: string;
|
|
57
57
|
projectContext?: string;
|
|
58
|
+
/**
|
|
59
|
+
* PRI-467: Intent Engineering friction block. Bounded + escaped INTENT.md
|
|
60
|
+
* reference. Positioned after projectContext (stable reference data, lower
|
|
61
|
+
* priority than principles) and before workingMemory (volatile per-session
|
|
62
|
+
* state). Only populated when intent_engineering flag is on and INTENT.md
|
|
63
|
+
* is valid.
|
|
64
|
+
*/
|
|
65
|
+
intentBlock?: string;
|
|
58
66
|
workingMemory?: string;
|
|
59
67
|
thinkingOs?: string;
|
|
60
68
|
evolutionPrinciples?: string;
|
package/dist/hooks/prompt.js
CHANGED
|
@@ -20,6 +20,8 @@ import { isSharedCooldownActive, markSharedEpisodeAsDiagnosed } from './trigger-
|
|
|
20
20
|
import { buildEmpathyObservation, resolveSourceKind } from './raw-observation-adapter.js';
|
|
21
21
|
import { evaluateEvidenceTriage } from './triage-adapter.js';
|
|
22
22
|
import { loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
|
|
23
|
+
import { safeReadIntentDoc, resetIntentDocCacheForTest } from '../core/intent-doc-reader.js';
|
|
24
|
+
import { buildIntentFrictionBlock } from '@principles/core/runtime-v2';
|
|
23
25
|
import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
|
|
24
26
|
import { detectCorrectionCue as coreDetectCorrectionCue, escapeXml, extractMessageContent, isMinimalTrigger, } from '@principles/core/prompt-builder';
|
|
25
27
|
import { sanitizeForEvidence } from './message-sanitize.js';
|
|
@@ -103,10 +105,12 @@ export function resetPromptStateForTest(workspaceDir) {
|
|
|
103
105
|
if (workspaceDir) {
|
|
104
106
|
_staticFileCache.delete(workspaceDir);
|
|
105
107
|
_empathyState.delete(workspaceDir);
|
|
108
|
+
resetIntentDocCacheForTest(workspaceDir);
|
|
106
109
|
}
|
|
107
110
|
else {
|
|
108
111
|
_staticFileCache.clear();
|
|
109
112
|
_empathyState.clear();
|
|
113
|
+
resetIntentDocCacheForTest();
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
function parseContextInjectionConfig(value) {
|
|
@@ -871,11 +875,38 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
871
875
|
catch (e) {
|
|
872
876
|
logger?.warn?.(`[PD:RuntimeV2] Failed to read Runtime V2 prompt activations: ${String(e)}`);
|
|
873
877
|
}
|
|
878
|
+
// ── PRI-467: Intent Engineering — INTENT.md friction block injection ──
|
|
879
|
+
// SPEC §5: flag off → no fs access, no cache access, no injection, no telemetry.
|
|
880
|
+
// SPEC §13.1: inject only when flag on + INTENT.md exists + safeReadIntentDoc ok=true.
|
|
881
|
+
// SPEC §14.1 Mode A: prompt-only — no output-hook capture, no check_emitted counter.
|
|
882
|
+
let intentBlockContent;
|
|
883
|
+
try {
|
|
884
|
+
const intentFlag = loadFeatureFlagFromConfig(workspaceDir, 'intent_engineering', logger);
|
|
885
|
+
if (intentFlag.enabled) {
|
|
886
|
+
const intentResult = safeReadIntentDoc(workspaceDir, { logger });
|
|
887
|
+
if (intentResult.ok && intentResult.doc) {
|
|
888
|
+
const block = buildIntentFrictionBlock({ rawIntentMd: intentResult.doc.raw });
|
|
889
|
+
if (block.length > 0) {
|
|
890
|
+
intentBlockContent = block;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
else if (!intentResult.ok && intentResult.reason !== 'not_found') {
|
|
894
|
+
// SPEC §13.1: missing file is silent (no debug noise). Other degraded
|
|
895
|
+
// paths (oversized, read_error, flag_disabled) log a debug reason.
|
|
896
|
+
logger?.debug?.(`[PD:Intent] INTENT.md injection skipped: reason=${intentResult.reason ?? 'unknown'}, nextAction=${intentResult.nextAction ?? 'none'}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
catch (intentErr) {
|
|
901
|
+
// ERR-002 — fail-open: never let INTENT injection break the prompt hook.
|
|
902
|
+
logger?.warn?.(`[PD:Intent] INTENT injection failed, skipping: ${String(intentErr)}`);
|
|
903
|
+
}
|
|
874
904
|
// Build appendSystemContext with recency effect
|
|
875
|
-
// Content order (most important last): behavioral_constraints -> project_context -> working_memory -> reflection_log -> thinking_os -> principles
|
|
905
|
+
// Content order (most important last): behavioral_constraints -> project_context -> intent_block -> working_memory -> reflection_log -> thinking_os -> principles
|
|
876
906
|
appendSystemContext = assembleAppendSystemContext({
|
|
877
907
|
behavioralConstraints: shouldInjectBehavioralConstraints ? empathySilenceConstraint : undefined,
|
|
878
908
|
projectContext: projectContextContent || undefined,
|
|
909
|
+
intentBlock: intentBlockContent,
|
|
879
910
|
workingMemory: workingMemoryContent || undefined,
|
|
880
911
|
thinkingOs: thinkingOsContent || undefined,
|
|
881
912
|
evolutionPrinciples: evolutionPrinciplesContent || undefined,
|
|
@@ -892,10 +923,12 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
892
923
|
// routing helpers deleted per PRI-448. No routing-related content is injected.
|
|
893
924
|
// ──── 8. SIZE GUARD ────
|
|
894
925
|
// Delegates to @principles/core/prompt-builder/truncateInjectionToBudget
|
|
895
|
-
// which handles priority stripping: project_context →
|
|
896
|
-
// evolution_principles → reflection_log → reason: truncation → fallback.
|
|
926
|
+
// which handles priority stripping: project_context → intent_block →
|
|
927
|
+
// thinking_os → evolution_principles → reflection_log → reason: truncation → fallback.
|
|
928
|
+
// PRI-467: intentBlockContent is passed so the guard can strip INTENT by
|
|
929
|
+
// exact match before falling back to the nuclear option.
|
|
897
930
|
const result = truncateInjectionToBudget(prependSystemContext, prependContext, appendSystemContext, {
|
|
898
|
-
blocks: { projectContextContent, thinkingOsContent, evolutionPrinciplesContent },
|
|
931
|
+
blocks: { projectContextContent, intentBlockContent, thinkingOsContent, evolutionPrinciplesContent },
|
|
899
932
|
});
|
|
900
933
|
prependSystemContext = result.prependSystemContext;
|
|
901
934
|
prependContext = result.prependContext;
|
|
@@ -16,7 +16,7 @@ function pushWarning(warnings, message) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* YAML-SSOT-03: resolve a dot-path (e.g. 'evolution.
|
|
19
|
+
* YAML-SSOT-03: resolve a dot-path (e.g. 'evolution.rulehostEvaluated') from dailyStats.
|
|
20
20
|
* Returns { count, resolvable } to distinguish "field not found / non-numeric" from "legitimate zero".
|
|
21
21
|
*/
|
|
22
22
|
function resolveStatsField(stats, dotPath) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { EventType, EventCategory, EventLogEntry, ToolCallEventData, PainSignalEventData,
|
|
1
|
+
export type { EventType, EventCategory, EventLogEntry, ToolCallEventData, PainSignalEventData, RulePromotionEventData, HookExecutionEventData, GateBlockEventData, GateBypassEventData, PlanApprovalEventData, EvolutionTaskEventData, EmpathyRollbackEventData, HeartbeatDiagnosisEventData, DiagnosisTaskEventData, DiagnosticianReportEventData, PrincipleCandidateEventData, RuleEnforcedEventData, RuleHostEvaluatedEventData, RuleHostBlockedEventData, RuleHostRequireApprovalEventData, RuleHostAutoCorrectProposedEventData, RuleHostAutoCorrectAppliedEventData, RuntimeV2PromptActivationsInjectedEventData, RuleHostUnhealthyEventData, ToolCallStats, ErrorStats, EmpathyEventStats, GfiStats, EvolutionStats as EventEvolutionStats, HookStats, DailyStats, } from '@principles/core/runtime-v2';
|
|
2
2
|
export { createEmptyDailyStats, } from '@principles/core/runtime-v2';
|
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.151.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "principles-disciple",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.151.0",
|
|
4
4
|
"description": "Native OpenClaw plugin for Principles Disciple",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/bundle.js",
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
"lint": "eslint \"src/**/*.ts\"",
|
|
48
48
|
"bootstrap-rules": "node scripts/bootstrap-rules.mjs",
|
|
49
49
|
"compile-principles": "node scripts/compile-principles.mjs",
|
|
50
|
-
"validate-live-path": "tsx scripts/validate-live-path.ts",
|
|
51
50
|
"sync-plugin": "node scripts/sync-plugin.mjs"
|
|
52
51
|
},
|
|
53
52
|
"devDependencies": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* PD Pipeline — End-to-End Acceptance Test
|
|
5
5
|
*
|
|
6
|
-
* Verifies that all components of the
|
|
6
|
+
* Verifies that all components of the PD reflection pipeline
|
|
7
7
|
* work correctly in a real environment (not unit tests).
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
@@ -71,7 +71,7 @@ function main() {
|
|
|
71
71
|
process.exit(1);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
console.log('\n🧪
|
|
74
|
+
console.log('\n🧪 PD Pipeline Acceptance Test');
|
|
75
75
|
console.log('═'.repeat(55));
|
|
76
76
|
console.log(`Workspace: ${workspaceDir}`);
|
|
77
77
|
console.log(`Database: ${dbPath}\n`);
|
|
@@ -1,462 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Validate Live Path Script (Phase 18) — with Data Flow Monitoring
|
|
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
|
-
* - Monitors data flow: queue state → workflow state → artifact persistence
|
|
15
|
-
* - Outputs summary and exits 0 on success, non-zero on failure
|
|
16
|
-
*
|
|
17
|
-
* Usage:
|
|
18
|
-
* tsx scripts/validate-live-path.ts [--verbose]
|
|
19
|
-
*
|
|
20
|
-
* Environment:
|
|
21
|
-
* WORKSPACE_DIR - Optional workspace directory (defaults to process.cwd())
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import * as Database from 'better-sqlite3';
|
|
25
|
-
import * as fs from 'fs';
|
|
26
|
-
import * as path from 'path';
|
|
27
|
-
|
|
28
|
-
// ─── Constants ───────────────────────────────────────────────────────────
|
|
29
|
-
const POLL_INTERVAL_MS = 5_000;
|
|
30
|
-
const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
31
|
-
const LOCK_SUFFIX = '.lock';
|
|
32
|
-
const LOCK_MAX_RETRIES = 50;
|
|
33
|
-
const LOCK_RETRY_DELAY_MS = 50;
|
|
34
|
-
const LOCK_STALE_MS = 30_000;
|
|
35
|
-
const WORKSPACE_DIR = process.env.WORKSPACE_DIR || process.cwd();
|
|
36
|
-
const STATE_DIR = path.join(WORKSPACE_DIR, '.state');
|
|
37
|
-
const QUEUE_PATH = path.join(STATE_DIR, 'EVOLUTION_QUEUE');
|
|
38
|
-
const LEDGER_PATH = path.join(STATE_DIR, 'principle_training_state.json');
|
|
39
|
-
const DB_PATH = path.join(STATE_DIR, 'subagent_workflows.db');
|
|
40
|
-
const SAMPLES_DIR = path.join(STATE_DIR, 'nocturnal', 'samples');
|
|
41
|
-
|
|
42
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
43
|
-
function timestamp(): string {
|
|
44
|
-
return new Date().toISOString();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function logStep(step: string, detail: string): void {
|
|
48
|
-
console.log(`[${timestamp()}] ▸ ${step}: ${detail}`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function logData(label: string, data: unknown): void {
|
|
52
|
-
const display = typeof data === 'string' ? data : JSON.stringify(data).slice(0, 300);
|
|
53
|
-
console.log(`[${timestamp()}] 📦 ${label}: ${display}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function safeReadJson(filePath: string): unknown {
|
|
57
|
-
try {
|
|
58
|
-
if (!fs.existsSync(filePath)) return null;
|
|
59
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
60
|
-
} catch { return null; }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ─── Types ───────────────────────────────────────────────────────────────
|
|
64
|
-
interface LedgerRule {
|
|
65
|
-
id: string;
|
|
66
|
-
principleId: string;
|
|
67
|
-
action: string;
|
|
68
|
-
type: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface HybridLedgerStore {
|
|
72
|
-
tree: {
|
|
73
|
-
rules: Record<string, LedgerRule>;
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
interface WorkflowRow {
|
|
78
|
-
workflow_id: string;
|
|
79
|
-
workflow_type: string;
|
|
80
|
-
state: string;
|
|
81
|
-
metadata_json: string;
|
|
82
|
-
created_at: number;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
interface QueueItem {
|
|
86
|
-
id: string;
|
|
87
|
-
taskKind: string;
|
|
88
|
-
status: string;
|
|
89
|
-
resolution?: string;
|
|
90
|
-
resultRef?: string;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
interface LockContext {
|
|
94
|
-
lockPath: string;
|
|
95
|
-
pid: number;
|
|
96
|
-
release: () => void;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ─── File Lock Functions (simplified from file-lock.ts) ──────────────────
|
|
100
|
-
async function acquireLockAsync(filePath: string, options: {
|
|
101
|
-
lockSuffix?: string;
|
|
102
|
-
maxRetries?: number;
|
|
103
|
-
baseRetryDelayMs?: number;
|
|
104
|
-
lockStaleMs?: number;
|
|
105
|
-
} = {}): Promise<LockContext> {
|
|
106
|
-
const lockSuffix = options.lockSuffix ?? LOCK_SUFFIX;
|
|
107
|
-
const maxRetries = options.maxRetries ?? LOCK_MAX_RETRIES;
|
|
108
|
-
const baseRetryDelayMs = options.baseRetryDelayMs ?? LOCK_RETRY_DELAY_MS;
|
|
109
|
-
const lockStaleMs = options.lockStaleMs ?? LOCK_STALE_MS;
|
|
110
|
-
const { pid } = process;
|
|
111
|
-
const lockPath = filePath + lockSuffix;
|
|
112
|
-
|
|
113
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
114
|
-
try {
|
|
115
|
-
// Check if lock file exists and is stale
|
|
116
|
-
if (fs.existsSync(lockPath)) {
|
|
117
|
-
const lockContent = fs.readFileSync(lockPath, 'utf8');
|
|
118
|
-
const lockPid = parseInt(lockContent, 10);
|
|
119
|
-
const lockStats = fs.statSync(lockPath);
|
|
120
|
-
const lockAge = Date.now() - lockStats.mtimeMs;
|
|
121
|
-
|
|
122
|
-
// Clean up stale lock
|
|
123
|
-
if (lockAge > lockStaleMs) {
|
|
124
|
-
fs.unlinkSync(lockPath);
|
|
125
|
-
} else if (lockPid !== pid) {
|
|
126
|
-
// Lock held by another process
|
|
127
|
-
await new Promise(resolve => setTimeout(resolve, baseRetryDelayMs));
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Acquire lock
|
|
133
|
-
fs.writeFileSync(lockPath, pid.toString(), { flag: 'wx' });
|
|
134
|
-
return {
|
|
135
|
-
lockPath,
|
|
136
|
-
pid,
|
|
137
|
-
release: () => {
|
|
138
|
-
try {
|
|
139
|
-
if (fs.existsSync(lockPath)) {
|
|
140
|
-
fs.unlinkSync(lockPath);
|
|
141
|
-
}
|
|
142
|
-
} catch {
|
|
143
|
-
// Ignore errors during release
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
} catch (error: unknown) {
|
|
148
|
-
const err = error as { code?: string };
|
|
149
|
-
if (err.code === 'EEXIST') {
|
|
150
|
-
if (attempt < maxRetries - 1) {
|
|
151
|
-
await new Promise(resolve => setTimeout(resolve, baseRetryDelayMs));
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const lockError = new Error(`Failed to acquire lock for ${filePath}: ${String(error)}`);
|
|
156
|
-
lockError.cause = error;
|
|
157
|
-
throw lockError;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
throw new Error(`Failed to acquire lock for ${filePath} after ${maxRetries} attempts`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function releaseLock(ctx: LockContext): void {
|
|
165
|
-
ctx.release();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ─── Step 1: Check bootstrapped rules ─────────────────────────────────────
|
|
169
|
-
function loadBootstrappedRules(): LedgerRule[] {
|
|
170
|
-
if (!fs.existsSync(LEDGER_PATH)) {
|
|
171
|
-
throw new Error('FAIL: principle_training_state.json not found. Run Phase 17 bootstrap first: npm run bootstrap-rules');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const ledger: HybridLedgerStore = JSON.parse(fs.readFileSync(LEDGER_PATH, 'utf8'));
|
|
175
|
-
const bootstrappedRules = Object.values(ledger.tree.rules).filter(r =>
|
|
176
|
-
r.id.endsWith('_stub_bootstrap')
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
return bootstrappedRules;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ─── Step 2: Build synthetic snapshot ──────────────────────────────────────
|
|
183
|
-
function buildSyntheticSnapshot(taskId: string) {
|
|
184
|
-
return {
|
|
185
|
-
sessionId: `validation-${taskId}`,
|
|
186
|
-
startedAt: new Date().toISOString(),
|
|
187
|
-
updatedAt: new Date().toISOString(),
|
|
188
|
-
assistantTurns: [],
|
|
189
|
-
userTurns: [],
|
|
190
|
-
toolCalls: [],
|
|
191
|
-
painEvents: [],
|
|
192
|
-
gateBlocks: [],
|
|
193
|
-
stats: {
|
|
194
|
-
totalAssistantTurns: 0,
|
|
195
|
-
totalToolCalls: 0,
|
|
196
|
-
failureCount: 0,
|
|
197
|
-
totalPainEvents: 1,
|
|
198
|
-
totalGateBlocks: 0,
|
|
199
|
-
},
|
|
200
|
-
recentPain: [{
|
|
201
|
-
source: 'live-validation',
|
|
202
|
-
score: 50,
|
|
203
|
-
severity: 'moderate',
|
|
204
|
-
reason: 'Synthetic snapshot for live path validation',
|
|
205
|
-
createdAt: new Date().toISOString(),
|
|
206
|
-
}],
|
|
207
|
-
_dataSource: 'pain_context_fallback',
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// ─── Step 3: Enqueue sleep_reflection task with proper file locking ──────────
|
|
212
|
-
// Uses acquireLockAsync to prevent TOCTOU race conditions (T-18-01 mitigation)
|
|
213
|
-
async function enqueueSleepReflectionTask(taskId: string): Promise<void> {
|
|
214
|
-
let lockCtx: LockContext | null = null;
|
|
215
|
-
try {
|
|
216
|
-
// Acquire lock before reading queue file (T-18-01 mitigation)
|
|
217
|
-
lockCtx = await acquireLockAsync(QUEUE_PATH, {
|
|
218
|
-
lockSuffix: LOCK_SUFFIX,
|
|
219
|
-
maxRetries: LOCK_MAX_RETRIES,
|
|
220
|
-
baseRetryDelayMs: LOCK_RETRY_DELAY_MS,
|
|
221
|
-
lockStaleMs: LOCK_STALE_MS,
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
let queue: QueueItem[] = [];
|
|
225
|
-
if (fs.existsSync(QUEUE_PATH)) {
|
|
226
|
-
const queueContent = fs.readFileSync(QUEUE_PATH, 'utf8');
|
|
227
|
-
queue = JSON.parse(queueContent);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
queue.push({
|
|
231
|
-
id: taskId,
|
|
232
|
-
taskKind: 'sleep_reflection',
|
|
233
|
-
status: 'pending',
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
fs.writeFileSync(QUEUE_PATH, JSON.stringify(queue, null, 2), 'utf8');
|
|
237
|
-
} finally {
|
|
238
|
-
if (lockCtx) {
|
|
239
|
-
releaseLock(lockCtx);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// ─── Step 4: Poll workflow store (raw SQLite, no WorkflowStore import) ─────
|
|
245
|
-
// Uses better-sqlite3 directly to avoid WorkflowStore async initialization issues in standalone script
|
|
246
|
-
function listNocturnalWorkflows(): WorkflowRow[] {
|
|
247
|
-
if (!fs.existsSync(DB_PATH)) {
|
|
248
|
-
return [];
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const db = new Database(DB_PATH, { readonly: true });
|
|
252
|
-
const rows = db.prepare(`
|
|
253
|
-
SELECT workflow_id, workflow_type, state, metadata_json, created_at
|
|
254
|
-
FROM subagent_workflows
|
|
255
|
-
WHERE workflow_type = 'nocturnal'
|
|
256
|
-
ORDER BY created_at DESC
|
|
257
|
-
`).all() as WorkflowRow[];
|
|
258
|
-
db.close();
|
|
259
|
-
return rows;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// ─── Step 5: Correlate and verify ─────────────────────────────────────────
|
|
263
|
-
function verifyWorkflowCompletion(taskId: string): {
|
|
264
|
-
workflowId: string;
|
|
265
|
-
state: string;
|
|
266
|
-
resolution: string;
|
|
267
|
-
} | null {
|
|
268
|
-
const workflows = listNocturnalWorkflows();
|
|
269
|
-
|
|
270
|
-
for (const wf of workflows) {
|
|
271
|
-
const meta = JSON.parse(wf.metadata_json);
|
|
272
|
-
if (meta.taskId !== taskId) continue;
|
|
273
|
-
if (wf.state !== 'completed') continue;
|
|
274
|
-
|
|
275
|
-
// Read resolution from queue (resolution is on queue item, not on WorkflowRow)
|
|
276
|
-
let queue: QueueItem[] = [];
|
|
277
|
-
try {
|
|
278
|
-
if (fs.existsSync(QUEUE_PATH)) {
|
|
279
|
-
const queueContent = fs.readFileSync(QUEUE_PATH, 'utf8');
|
|
280
|
-
queue = JSON.parse(queueContent);
|
|
281
|
-
}
|
|
282
|
-
} catch {
|
|
283
|
-
// Queue file missing or corrupted — resolution unknown
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const queueItem = queue.find(q => q.id === taskId);
|
|
287
|
-
const resolution = queueItem?.resolution;
|
|
288
|
-
|
|
289
|
-
return {
|
|
290
|
-
workflowId: wf.workflow_id,
|
|
291
|
-
state: wf.state,
|
|
292
|
-
resolution: resolution || 'MISSING',
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ─── Main ─────────────────────────────────────────────────────────────────
|
|
300
|
-
async function main() {
|
|
301
|
-
const verbose = process.argv.includes('--verbose');
|
|
302
|
-
|
|
303
|
-
console.log('╔══════════════════════════════════════════════════════════╗');
|
|
304
|
-
console.log('║ Nocturnal Live Path Validation + Data Flow Monitor ║');
|
|
305
|
-
console.log('╚══════════════════════════════════════════════════════════╝');
|
|
306
|
-
logStep('WORKSPACE', WORKSPACE_DIR);
|
|
307
|
-
|
|
308
|
-
// 0. Baseline: snapshot current state
|
|
309
|
-
logStep('BASELINE', 'Capturing current state before validation');
|
|
310
|
-
const queueBefore = safeReadJson(QUEUE_PATH) as QueueItem[] | null;
|
|
311
|
-
logData('EVOLUTION_QUEUE (before)', queueBefore?.length ?? 0);
|
|
312
|
-
if (fs.existsSync(SAMPLES_DIR)) {
|
|
313
|
-
const samplesBefore = fs.readdirSync(SAMPLES_DIR).length;
|
|
314
|
-
logData('nocturnal/samples/', `${samplesBefore} files`);
|
|
315
|
-
} else {
|
|
316
|
-
logData('nocturnal/samples/', 'directory not present');
|
|
317
|
-
}
|
|
318
|
-
if (fs.existsSync(DB_PATH)) {
|
|
319
|
-
const wfCount = listNocturnalWorkflows().length;
|
|
320
|
-
logData('subagent_workflows.db', `${wfCount} nocturnal workflows`);
|
|
321
|
-
} else {
|
|
322
|
-
logData('subagent_workflows.db', 'not present');
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// 1. Check bootstrapped rules
|
|
326
|
-
// eslint-disable-next-line @typescript-eslint/init-declarations
|
|
327
|
-
let rules: LedgerRule[];
|
|
328
|
-
try {
|
|
329
|
-
rules = loadBootstrappedRules();
|
|
330
|
-
logStep('STEP 1', `Found ${rules.length} bootstrapped rule(s)`);
|
|
331
|
-
} catch {
|
|
332
|
-
console.error('FAIL: principle_training_state.json not found. Run Phase 17 bootstrap first: npm run bootstrap-rules');
|
|
333
|
-
process.exit(1);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (rules.length === 0) {
|
|
337
|
-
console.error('FAIL: No _stub_bootstrap rules found. Run Phase 17 bootstrap first: npm run bootstrap-rules');
|
|
338
|
-
process.exit(1);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (verbose) {
|
|
342
|
-
for (const rule of rules) {
|
|
343
|
-
console.log(` - ${rule.id} (principleId=${rule.principleId}, action=${rule.action})`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// 2. Generate task ID
|
|
348
|
-
const taskId = `validation-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
349
|
-
|
|
350
|
-
// 3. Build synthetic snapshot for validation
|
|
351
|
-
const snapshot = buildSyntheticSnapshot(taskId);
|
|
352
|
-
logStep('STEP 2', `Synthetic snapshot: sessionId=${snapshot.sessionId}`);
|
|
353
|
-
logData('snapshot.recentPain', JSON.stringify(snapshot.recentPain));
|
|
354
|
-
|
|
355
|
-
// 4. Enqueue task (with lock acquisition)
|
|
356
|
-
try {
|
|
357
|
-
await enqueueSleepReflectionTask(taskId);
|
|
358
|
-
logStep('STEP 3', `Enqueued sleep_reflection task: ${taskId}`);
|
|
359
|
-
|
|
360
|
-
// Post-enqueue: verify queue state
|
|
361
|
-
const queueAfter = safeReadJson(QUEUE_PATH) as QueueItem[] | null;
|
|
362
|
-
const taskItem = queueAfter?.find(q => q.id === taskId);
|
|
363
|
-
logData('EVOLUTION_QUEUE (after)', `${queueAfter?.length ?? 0} tasks`);
|
|
364
|
-
logData(`task[${taskId}]`, taskItem ? JSON.stringify(taskItem) : 'NOT FOUND');
|
|
365
|
-
} catch (error: unknown) {
|
|
366
|
-
console.error('FAIL: Failed to enqueue sleep_reflection task:', String(error));
|
|
367
|
-
process.exit(1);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// 5. Poll for completion — with data flow monitoring
|
|
371
|
-
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
372
|
-
let pollCount = 0;
|
|
373
|
-
let lastQueueStatus = 'unknown';
|
|
374
|
-
let lastWorkflowState = 'none';
|
|
375
|
-
logStep('STEP 4', `Polling for workflow completion (timeout: ${POLL_TIMEOUT_MS / 1000 / 60}min, interval: ${POLL_INTERVAL_MS / 1000}s)`);
|
|
376
|
-
|
|
377
|
-
while (Date.now() < deadline) {
|
|
378
|
-
pollCount++;
|
|
379
|
-
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
380
|
-
|
|
381
|
-
// Capture queue state
|
|
382
|
-
const queueNow = safeReadJson(QUEUE_PATH) as QueueItem[] | null;
|
|
383
|
-
const taskNow = queueNow?.find(q => q.id === taskId);
|
|
384
|
-
const currentQueueStatus = taskNow?.status ?? 'not_in_queue';
|
|
385
|
-
|
|
386
|
-
// Capture workflow DB state
|
|
387
|
-
const workflows = listNocturnalWorkflows();
|
|
388
|
-
const matchingWf = workflows.find(w => {
|
|
389
|
-
try {
|
|
390
|
-
const meta = JSON.parse(w.metadata_json);
|
|
391
|
-
return meta.taskId === taskId;
|
|
392
|
-
} catch { return false; }
|
|
393
|
-
});
|
|
394
|
-
const currentWorkflowState = matchingWf?.state ?? 'not_in_db';
|
|
395
|
-
|
|
396
|
-
// Log state changes
|
|
397
|
-
if (currentQueueStatus !== lastQueueStatus || currentWorkflowState !== lastWorkflowState) {
|
|
398
|
-
logStep(`POLL #${pollCount}`, `queue=${currentQueueStatus}, workflow=${currentWorkflowState}`);
|
|
399
|
-
if (taskNow) logData('queue item', JSON.stringify({ status: taskNow.status, resolution: taskNow.resolution }));
|
|
400
|
-
if (matchingWf) logData('workflow', JSON.stringify({ state: matchingWf.state, type: matchingWf.workflow_type }));
|
|
401
|
-
lastQueueStatus = currentQueueStatus;
|
|
402
|
-
lastWorkflowState = currentWorkflowState;
|
|
403
|
-
} else if (verbose) {
|
|
404
|
-
process.stdout.write('.');
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Check for completion
|
|
408
|
-
const result = verifyWorkflowCompletion(taskId);
|
|
409
|
-
if (result) {
|
|
410
|
-
console.log(''); // newline if dots were printed
|
|
411
|
-
logStep('STEP 5', `Workflow completed!`);
|
|
412
|
-
logData('RESULT', `workflowId=${result.workflowId} state=${result.state} resolution=${result.resolution}`);
|
|
413
|
-
|
|
414
|
-
// Check artifact persistence
|
|
415
|
-
if (fs.existsSync(SAMPLES_DIR)) {
|
|
416
|
-
const newSamples = fs.readdirSync(SAMPLES_DIR).filter(f => {
|
|
417
|
-
const stat = fs.statSync(path.join(SAMPLES_DIR, f));
|
|
418
|
-
return stat.isFile() && f.endsWith('.json') && (Date.now() - stat.mtimeMs) < 60000; // created in last minute
|
|
419
|
-
});
|
|
420
|
-
if (newSamples.length > 0) {
|
|
421
|
-
logData('new artifacts', newSamples.join(', '));
|
|
422
|
-
const firstArtifact = safeReadJson(path.join(SAMPLES_DIR, newSamples[0]));
|
|
423
|
-
if (firstArtifact) logData('artifact content (first)', JSON.stringify(firstArtifact).slice(0, 300));
|
|
424
|
-
} else {
|
|
425
|
-
logData('new artifacts', 'none created in last 60s');
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (result.resolution === 'MISSING' || result.resolution === 'expired') {
|
|
430
|
-
console.error('FAIL: resolution not explicit');
|
|
431
|
-
process.exit(1);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
console.log('PASS: Live path validation successful');
|
|
435
|
-
process.exit(0);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Timeout — dump final state for debugging
|
|
440
|
-
console.log('');
|
|
441
|
-
logStep('TIMEOUT', `Poll timeout after ${pollCount} polls (${POLL_TIMEOUT_MS / 1000 / 60}min)`);
|
|
442
|
-
logData('FINAL queue status', lastQueueStatus);
|
|
443
|
-
logData('FINAL workflow state', lastWorkflowState);
|
|
444
|
-
|
|
445
|
-
// Dump full queue for debugging
|
|
446
|
-
const finalQueue = safeReadJson(QUEUE_PATH);
|
|
447
|
-
if (finalQueue) logData('FINAL queue dump', JSON.stringify(finalQueue).slice(0, 500));
|
|
448
|
-
|
|
449
|
-
// Dump full workflow DB for debugging
|
|
450
|
-
const finalWorkflows = listNocturnalWorkflows();
|
|
451
|
-
if (finalWorkflows.length > 0) {
|
|
452
|
-
logData('FINAL workflows', finalWorkflows.map(w => `${w.workflow_id}: state=${w.state}`).join(', '));
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
console.error('FAIL: No completed nocturnal workflow found for taskId');
|
|
456
|
-
process.exit(1);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
main().catch(err => {
|
|
460
|
-
console.error('FAIL:', err);
|
|
461
|
-
process.exit(1);
|
|
462
|
-
});
|