principles-disciple 1.150.0 → 1.152.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/dist/commands/pain.js +2 -0
- package/dist/core/intent-doc-reader-adapter.d.ts +28 -0
- package/dist/core/intent-doc-reader-adapter.js +56 -0
- package/dist/core/intent-doc-reader.d.ts +80 -0
- package/dist/core/intent-doc-reader.js +189 -0
- package/dist/hooks/pain.js +5 -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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/commands/pain.js
CHANGED
|
@@ -5,6 +5,7 @@ import { resolvePluginCommandWorkspaceDir } from '../utils/workspace-resolver.js
|
|
|
5
5
|
import { computeHash } from '../utils/hashing.js';
|
|
6
6
|
import { PainToPrincipleService, PrincipleTreeLedgerAdapter } from '@principles/core/runtime-v2';
|
|
7
7
|
import { loadPdConfigForPlugin } from '../core/pd-config-loader.js';
|
|
8
|
+
import { createIntentDocReader } from '../core/intent-doc-reader-adapter.js';
|
|
8
9
|
/**
|
|
9
10
|
* Creates a visual progress bar (e.g., [██████░░░░])
|
|
10
11
|
*/
|
|
@@ -274,6 +275,7 @@ export async function handlePainReportCommand(ctx) {
|
|
|
274
275
|
autoIntakeEnabled: true,
|
|
275
276
|
effectiveConfig: configResult.effective,
|
|
276
277
|
getEnvVar: (name) => process.env[name],
|
|
278
|
+
intentDocReader: createIntentDocReader(wctx.workspaceDir),
|
|
277
279
|
});
|
|
278
280
|
const result = await service.recordPain({
|
|
279
281
|
painId: painData.painId,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-468 — Plugin adapter implementing the core `IntentDocReader` port.
|
|
3
|
+
*
|
|
4
|
+
* Bridges the plugin-owned `safeReadIntentDoc()` I/O function to the
|
|
5
|
+
* core-owned `IntentDocReader` interface so Stage A (in core) can read
|
|
6
|
+
* INTENT.md without core performing any filesystem I/O.
|
|
7
|
+
*
|
|
8
|
+
* This adapter only MAPS types — it adds no new I/O, no new flag checks,
|
|
9
|
+
* and no new telemetry. The underlying `safeReadIntentDoc()` already
|
|
10
|
+
* performs the flag-first check (SPEC §12), TTL+mtime cache, and size cap.
|
|
11
|
+
*
|
|
12
|
+
* ERR checklist:
|
|
13
|
+
* EP-01 / ERR-001: never `as` — field-by-field mapping with typeof checks
|
|
14
|
+
* EP-02 / ERR-025: plugin I/O file, whitelisted in architecture-regression
|
|
15
|
+
* EP-03 / ERR-002: every degraded path flows through with reason + nextAction
|
|
16
|
+
*
|
|
17
|
+
* Architecture: this file is I/O (delegates to fs-reading safeReadIntentDoc).
|
|
18
|
+
* It will be added to KNOWN_PLUGIN_CORE_FILES in architecture-regression.test.ts.
|
|
19
|
+
*/
|
|
20
|
+
import type { IntentDocReader } from '@principles/core/runtime-v2';
|
|
21
|
+
/**
|
|
22
|
+
* Create an IntentDocReader bound to a specific workspace.
|
|
23
|
+
*
|
|
24
|
+
* The returned reader is stateless beyond the workspace binding — each
|
|
25
|
+
* `readIntentDoc()` call delegates to `safeReadIntentDoc()`, which owns
|
|
26
|
+
* the TTL+mtime cache.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createIntentDocReader(workspaceDir: string): IntentDocReader;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-468 — Plugin adapter implementing the core `IntentDocReader` port.
|
|
3
|
+
*
|
|
4
|
+
* Bridges the plugin-owned `safeReadIntentDoc()` I/O function to the
|
|
5
|
+
* core-owned `IntentDocReader` interface so Stage A (in core) can read
|
|
6
|
+
* INTENT.md without core performing any filesystem I/O.
|
|
7
|
+
*
|
|
8
|
+
* This adapter only MAPS types — it adds no new I/O, no new flag checks,
|
|
9
|
+
* and no new telemetry. The underlying `safeReadIntentDoc()` already
|
|
10
|
+
* performs the flag-first check (SPEC §12), TTL+mtime cache, and size cap.
|
|
11
|
+
*
|
|
12
|
+
* ERR checklist:
|
|
13
|
+
* EP-01 / ERR-001: never `as` — field-by-field mapping with typeof checks
|
|
14
|
+
* EP-02 / ERR-025: plugin I/O file, whitelisted in architecture-regression
|
|
15
|
+
* EP-03 / ERR-002: every degraded path flows through with reason + nextAction
|
|
16
|
+
*
|
|
17
|
+
* Architecture: this file is I/O (delegates to fs-reading safeReadIntentDoc).
|
|
18
|
+
* It will be added to KNOWN_PLUGIN_CORE_FILES in architecture-regression.test.ts.
|
|
19
|
+
*/
|
|
20
|
+
import { safeReadIntentDoc } from './intent-doc-reader.js';
|
|
21
|
+
/**
|
|
22
|
+
* Create an IntentDocReader bound to a specific workspace.
|
|
23
|
+
*
|
|
24
|
+
* The returned reader is stateless beyond the workspace binding — each
|
|
25
|
+
* `readIntentDoc()` call delegates to `safeReadIntentDoc()`, which owns
|
|
26
|
+
* the TTL+mtime cache.
|
|
27
|
+
*/
|
|
28
|
+
export function createIntentDocReader(workspaceDir) {
|
|
29
|
+
return {
|
|
30
|
+
readIntentDoc() {
|
|
31
|
+
const result = safeReadIntentDoc(workspaceDir);
|
|
32
|
+
if (result.ok && result.doc) {
|
|
33
|
+
const doc = result.doc;
|
|
34
|
+
const reference = {
|
|
35
|
+
raw: typeof doc.raw === 'string' ? doc.raw : '',
|
|
36
|
+
contentHash: typeof doc.contentHash === 'string' ? doc.contentHash : '',
|
|
37
|
+
path: typeof doc.path === 'string' ? doc.path : '',
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
found: true,
|
|
42
|
+
flagEnabled: true,
|
|
43
|
+
doc: reference,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Degraded path — preserve structured reason + nextAction (EP-03 / ERR-002)
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
found: result.found,
|
|
50
|
+
flagEnabled: result.flagEnabled,
|
|
51
|
+
reason: result.reason,
|
|
52
|
+
nextAction: result.nextAction,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/hooks/pain.js
CHANGED
|
@@ -25,6 +25,7 @@ import { resolveWorkspaceDirForRuntimeV2 } from '../utils/workspace-resolver.js'
|
|
|
25
25
|
import { PainToPrincipleService, PrincipleTreeLedgerAdapter } from '@principles/core/runtime-v2';
|
|
26
26
|
import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
|
|
27
27
|
import { loadPdConfigForPlugin, loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
|
|
28
|
+
import { createIntentDocReader } from '../core/intent-doc-reader-adapter.js';
|
|
28
29
|
import { evaluateTriggerController } from '@principles/core/runtime-v2';
|
|
29
30
|
import { isSharedCooldownActive, markSharedEpisodeAsDiagnosed } from './trigger-cooldown-tracker.js';
|
|
30
31
|
import { buildManualPainObservation, resolveSourceKind } from './raw-observation-adapter.js';
|
|
@@ -44,6 +45,10 @@ function createPainToPrincipleService(wctx) {
|
|
|
44
45
|
autoIntakeEnabled: true,
|
|
45
46
|
effectiveConfig: configResult.effective,
|
|
46
47
|
getEnvVar: (name) => process.env[name],
|
|
48
|
+
// PRI-468: wire INTENT.md reader so Stage A can produce intentTension
|
|
49
|
+
// when intent_engineering flag is on. Adapter performs no I/O beyond
|
|
50
|
+
// delegating to safeReadIntentDoc (which owns the flag-first check).
|
|
51
|
+
intentDocReader: createIntentDocReader(wctx.workspaceDir),
|
|
47
52
|
});
|
|
48
53
|
}
|
|
49
54
|
// buildTrajectoryEvidence is in ./trajectory-evidence.ts (re-exported above)
|
|
@@ -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;
|
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.152.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|