principles-disciple 1.162.0 → 1.164.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 -2
- package/dist/core/behavior-example-pack-assembler.d.ts +43 -0
- package/dist/core/behavior-example-pack-assembler.js +247 -0
- package/dist/core/intent-doc-reader-adapter.d.ts +13 -5
- package/dist/core/intent-doc-reader-adapter.js +25 -5
- package/dist/core/intent-doc-reader.d.ts +13 -4
- package/dist/core/intent-doc-reader.js +45 -24
- package/dist/core/trajectory.d.ts +23 -0
- package/dist/core/trajectory.js +62 -0
- package/dist/hooks/pain.js +2 -2
- package/dist/hooks/prompt.js +2 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/commands/pain.js
CHANGED
|
@@ -5,7 +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
|
+
import { createIntentDocReader, resolveIntentLang } from '../core/intent-doc-reader-adapter.js';
|
|
9
9
|
/**
|
|
10
10
|
* Creates a visual progress bar (e.g., [██████░░░░])
|
|
11
11
|
*/
|
|
@@ -275,7 +275,7 @@ export async function handlePainReportCommand(ctx) {
|
|
|
275
275
|
autoIntakeEnabled: true,
|
|
276
276
|
effectiveConfig: configResult.effective,
|
|
277
277
|
getEnvVar: (name) => process.env[name],
|
|
278
|
-
intentDocReader: createIntentDocReader(wctx.workspaceDir),
|
|
278
|
+
intentDocReader: createIntentDocReader(wctx.workspaceDir, resolveIntentLang(wctx.workspaceDir)),
|
|
279
279
|
});
|
|
280
280
|
const result = await service.recordPain({
|
|
281
281
|
painId: painData.painId,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { BehaviorExamplePack } from '@principles/core/runtime-v2';
|
|
2
|
+
export interface BehaviorExamplePackAssemblerOptions {
|
|
3
|
+
/** Workspace directory — used to obtain the shared TrajectoryDatabase. */
|
|
4
|
+
readonly workspaceDir: string;
|
|
5
|
+
/**
|
|
6
|
+
* State directory (unused for direct DB access but required by the
|
|
7
|
+
* WorkspaceContext facade pattern; future-proofing for redaction config).
|
|
8
|
+
*/
|
|
9
|
+
readonly stateDir: string;
|
|
10
|
+
}
|
|
11
|
+
export interface BehaviorExamplePackAssemblerInput {
|
|
12
|
+
/** Canonical pain ID (e.g. pain_<ts>_<hash>) — entry point into pain lineage. */
|
|
13
|
+
readonly sourcePainId: string;
|
|
14
|
+
/** Owner's natural-language desired outcome — must be non-empty. */
|
|
15
|
+
readonly ownerDesiredOutcome: string;
|
|
16
|
+
/**
|
|
17
|
+
* Project directory, used to detect absolute paths that should be redacted
|
|
18
|
+
* to relative/basename form in the assembled pack.
|
|
19
|
+
*/
|
|
20
|
+
readonly projectDir: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Plugin I/O assembler: pain lineage + trajectory → validated BehaviorExamplePack.
|
|
24
|
+
*
|
|
25
|
+
* Lifecycle: cheap to construct; obtains TrajectoryDatabase via the registry.
|
|
26
|
+
* Dispose is the registry's responsibility (singleton per workspace).
|
|
27
|
+
*/
|
|
28
|
+
export declare class BehaviorExamplePackAssembler {
|
|
29
|
+
private readonly db;
|
|
30
|
+
constructor(opts: BehaviorExamplePackAssemblerOptions);
|
|
31
|
+
/**
|
|
32
|
+
* Assemble a BehaviorExamplePack from the pain lineage anchored at
|
|
33
|
+
* `input.sourcePainId`.
|
|
34
|
+
*
|
|
35
|
+
* Fail-loud contract (spec §7.2, ERR-069):
|
|
36
|
+
* - pain event not found → throws
|
|
37
|
+
* - empty trajectory → throws
|
|
38
|
+
* - no failing tool call to anchor sourceNegativeCase → throws
|
|
39
|
+
* - no successful tool call for positiveCounterexamples → throws
|
|
40
|
+
* - assembled pack fails validateBehaviorExamplePack → throws
|
|
41
|
+
*/
|
|
42
|
+
assemble(input: BehaviorExamplePackAssemblerInput): BehaviorExamplePack;
|
|
43
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-484 — Phase 5 Task 28 — BehaviorExamplePackAssembler (plugin I/O)
|
|
3
|
+
*
|
|
4
|
+
* Reads pain lineage + trajectory rows from TrajectoryDatabase and assembles
|
|
5
|
+
* a validated BehaviorExamplePack for the Artificer. Fail loud on any gap
|
|
6
|
+
* (missing pain, empty trajectory, invalid pack) — no silent fallback.
|
|
7
|
+
*
|
|
8
|
+
* Err-prevention:
|
|
9
|
+
* - ERR-001: every DB row field validated as `unknown` (no `as` bypass on
|
|
10
|
+
* parsed params_json or row casts).
|
|
11
|
+
* - ERR-069: Artificer shared schema — invalid pack must fail loud.
|
|
12
|
+
* - ERR-026: reuses production TrajectoryDatabase (no hand-written DDL).
|
|
13
|
+
* - rc-2: structural type guards, no `as T` on parsed input.
|
|
14
|
+
* - rc-5: Object.hasOwn over `in`.
|
|
15
|
+
* - rc-9: every failure path throws a structured Error with a clear reason.
|
|
16
|
+
*
|
|
17
|
+
* Spec: docs/superpowers/specs/2026-06-27-rulecode-context-vision-design.md §7.2
|
|
18
|
+
*
|
|
19
|
+
* Boundary: this module is I/O. Pure logic (type + validator) lives in
|
|
20
|
+
* `@principles/core/runtime-v2` → `behavior-example-pack.ts`.
|
|
21
|
+
*/
|
|
22
|
+
import { validateBehaviorExamplePack, } from '@principles/core/runtime-v2';
|
|
23
|
+
import { TrajectoryRegistry } from './trajectory.js';
|
|
24
|
+
// ── internal constants ─────────────────────────────────────────────────────
|
|
25
|
+
const MAX_POSITIVES = 3;
|
|
26
|
+
const MAX_EVIDENCE_REFS = 5;
|
|
27
|
+
const HISTORY_LIMIT = 100;
|
|
28
|
+
const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
29
|
+
// ── type guards (rc-2: no `as` bypass) ─────────────────────────────────────
|
|
30
|
+
function isPlainObject(value) {
|
|
31
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
function hasNoProtoKeys(value) {
|
|
34
|
+
for (const key of Object.keys(value)) {
|
|
35
|
+
if (PROTO_KEYS.has(key))
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
// ── BehaviorExamplePackAssembler ───────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Plugin I/O assembler: pain lineage + trajectory → validated BehaviorExamplePack.
|
|
43
|
+
*
|
|
44
|
+
* Lifecycle: cheap to construct; obtains TrajectoryDatabase via the registry.
|
|
45
|
+
* Dispose is the registry's responsibility (singleton per workspace).
|
|
46
|
+
*/
|
|
47
|
+
export class BehaviorExamplePackAssembler {
|
|
48
|
+
db;
|
|
49
|
+
constructor(opts) {
|
|
50
|
+
// TrajectoryRegistry.get returns the workspace-wide singleton; do NOT
|
|
51
|
+
// dispose it here. The caller (typically a workspace lifecycle hook)
|
|
52
|
+
// owns dispose.
|
|
53
|
+
this.db = TrajectoryRegistry.get(opts.workspaceDir);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Assemble a BehaviorExamplePack from the pain lineage anchored at
|
|
57
|
+
* `input.sourcePainId`.
|
|
58
|
+
*
|
|
59
|
+
* Fail-loud contract (spec §7.2, ERR-069):
|
|
60
|
+
* - pain event not found → throws
|
|
61
|
+
* - empty trajectory → throws
|
|
62
|
+
* - no failing tool call to anchor sourceNegativeCase → throws
|
|
63
|
+
* - no successful tool call for positiveCounterexamples → throws
|
|
64
|
+
* - assembled pack fails validateBehaviorExamplePack → throws
|
|
65
|
+
*/
|
|
66
|
+
assemble(input) {
|
|
67
|
+
// ── 1. Look up the anchoring pain event ──
|
|
68
|
+
const pain = this.db.getPainEventByCanonicalId(input.sourcePainId);
|
|
69
|
+
if (!pain) {
|
|
70
|
+
throw new Error(`[BehaviorExamplePackAssembler] pain event not found for canonical_pain_id="${input.sourcePainId}" (ERR-069 fail loud)`);
|
|
71
|
+
}
|
|
72
|
+
// ── 2. Pull the recent trajectory for the pain's session ──
|
|
73
|
+
// getRuleHostContextRows returns rows WITH params_json (which
|
|
74
|
+
// listToolCallsForSession does not), so we use it here.
|
|
75
|
+
const ctxResult = this.db.getRuleHostContextRows(pain.sessionId, HISTORY_LIMIT);
|
|
76
|
+
const rows = ctxResult.rows;
|
|
77
|
+
if (rows.length === 0) {
|
|
78
|
+
throw new Error(`[BehaviorExamplePackAssembler] empty trajectory (no tool calls) for sessionId="${pain.sessionId}" (pain=${input.sourcePainId}, ERR-069 fail loud)`);
|
|
79
|
+
}
|
|
80
|
+
// ── 3. Partition into failures (negative candidates) and successes (positives) ──
|
|
81
|
+
const failures = [];
|
|
82
|
+
const successes = [];
|
|
83
|
+
for (const row of rows) {
|
|
84
|
+
if (row.outcome === 'failure') {
|
|
85
|
+
failures.push(row);
|
|
86
|
+
}
|
|
87
|
+
else if (row.outcome === 'success') {
|
|
88
|
+
successes.push(row);
|
|
89
|
+
}
|
|
90
|
+
// 'blocked' rows are not used as either positive or negative cases.
|
|
91
|
+
}
|
|
92
|
+
if (failures.length === 0) {
|
|
93
|
+
throw new Error(`[BehaviorExamplePackAssembler] no failing tool call in sessionId="${pain.sessionId}" — cannot build sourceNegativeCase (pain=${input.sourcePainId}, ERR-069 fail loud)`);
|
|
94
|
+
}
|
|
95
|
+
if (successes.length === 0) {
|
|
96
|
+
throw new Error(`[BehaviorExamplePackAssembler] no successful tool call in sessionId="${pain.sessionId}" — cannot build positiveCounterexamples (pain=${input.sourcePainId}, ERR-069 fail loud)`);
|
|
97
|
+
}
|
|
98
|
+
// ── 4. Build sourceNegativeCase from the most recent failure ──
|
|
99
|
+
const sourceFailure = failures[failures.length - 1];
|
|
100
|
+
if (!sourceFailure) {
|
|
101
|
+
// Defensive: should be unreachable due to the failures.length === 0 check
|
|
102
|
+
// above, but noUncheckedIndexedAccess requires the guard.
|
|
103
|
+
throw new Error(`[BehaviorExamplePackAssembler] internal error: sourceFailure undefined (pain=${input.sourcePainId})`);
|
|
104
|
+
}
|
|
105
|
+
const sourceNegativeCase = buildCaseFromRow(sourceFailure, 'negative', 'block', input.projectDir);
|
|
106
|
+
// ── 5. Build positiveCounterexamples from successes (≤3, most recent first) ──
|
|
107
|
+
const positiveRows = successes.slice(-MAX_POSITIVES); // most recent ≤3
|
|
108
|
+
const positiveCounterexamples = [];
|
|
109
|
+
for (const row of positiveRows) {
|
|
110
|
+
positiveCounterexamples.push(buildCaseFromRow(row, 'positive', 'allow', input.projectDir));
|
|
111
|
+
}
|
|
112
|
+
// ── 6. Build evidenceRefs from pain events + gate blocks (≤5) ──
|
|
113
|
+
const evidenceRefs = [];
|
|
114
|
+
const sessionPainEvents = this.db.listPainEventsForSession(pain.sessionId);
|
|
115
|
+
for (const p of sessionPainEvents) {
|
|
116
|
+
if (evidenceRefs.length >= MAX_EVIDENCE_REFS)
|
|
117
|
+
break;
|
|
118
|
+
evidenceRefs.push(`pain:${p.id}`);
|
|
119
|
+
}
|
|
120
|
+
const gateBlocks = this.db.listGateBlocksForSession(pain.sessionId);
|
|
121
|
+
for (const g of gateBlocks) {
|
|
122
|
+
if (evidenceRefs.length >= MAX_EVIDENCE_REFS)
|
|
123
|
+
break;
|
|
124
|
+
evidenceRefs.push(`gate:${g.id}`);
|
|
125
|
+
}
|
|
126
|
+
if (evidenceRefs.length === 0) {
|
|
127
|
+
// The anchor pain event itself always provides at least one evidenceRef.
|
|
128
|
+
evidenceRefs.push(`pain:${pain.id}`);
|
|
129
|
+
}
|
|
130
|
+
// ── 7. Apply redaction (spec §7.2: "经过脱敏") ──
|
|
131
|
+
const redactionResult = redactParams(sourceNegativeCase.params, input.projectDir);
|
|
132
|
+
const redactedNegativeCase = {
|
|
133
|
+
...sourceNegativeCase,
|
|
134
|
+
params: redactionResult.redacted,
|
|
135
|
+
};
|
|
136
|
+
const redactedPositives = positiveCounterexamples.map((c) => {
|
|
137
|
+
const r = redactParams(c.params, input.projectDir);
|
|
138
|
+
return { ...c, params: r.redacted };
|
|
139
|
+
});
|
|
140
|
+
const redactionNotes = [...redactionResult.notes];
|
|
141
|
+
// ── 8. Assemble the pack ──
|
|
142
|
+
const pack = {
|
|
143
|
+
sourceNegativeCase: redactedNegativeCase,
|
|
144
|
+
ownerDesiredOutcome: input.ownerDesiredOutcome,
|
|
145
|
+
positiveCounterexamples: redactedPositives,
|
|
146
|
+
evidenceRefs,
|
|
147
|
+
redactionNotes,
|
|
148
|
+
};
|
|
149
|
+
// ── 9. Validate — fail loud (ERR-069) ──
|
|
150
|
+
const validation = validateBehaviorExamplePack(pack);
|
|
151
|
+
if (!validation.valid) {
|
|
152
|
+
throw new Error(`[BehaviorExamplePackAssembler] assembled pack failed validateBehaviorExamplePack: ${validation.errors.join('; ')} (ERR-069 fail loud)`);
|
|
153
|
+
}
|
|
154
|
+
return pack;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ── internal: case builder ─────────────────────────────────────────────────
|
|
158
|
+
/**
|
|
159
|
+
* Build a GoldenTraceCaseInput from a trajectory tool_call row.
|
|
160
|
+
*
|
|
161
|
+
* ERR-001: parses params_json as unknown and structurally validates (must be
|
|
162
|
+
* a plain object without proto-pollution keys). Throws on malformed input
|
|
163
|
+
* (rc-9: no silent fallback).
|
|
164
|
+
*/
|
|
165
|
+
function buildCaseFromRow(row, kind, expectedDecision, _projectDir) {
|
|
166
|
+
if (typeof row.toolName !== 'string' || row.toolName.length === 0) {
|
|
167
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: toolName must be a non-empty string (rc-9 fail loud)`);
|
|
168
|
+
}
|
|
169
|
+
if (typeof row.paramsJson !== 'string') {
|
|
170
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: paramsJson must be a string (rc-9 fail loud)`);
|
|
171
|
+
}
|
|
172
|
+
let parsedParams;
|
|
173
|
+
try {
|
|
174
|
+
parsedParams = JSON.parse(row.paramsJson);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: paramsJson is not valid JSON (rc-9 fail loud)`);
|
|
178
|
+
}
|
|
179
|
+
if (!isPlainObject(parsedParams)) {
|
|
180
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: paramsJson must parse to a non-array object (rc-9 fail loud)`);
|
|
181
|
+
}
|
|
182
|
+
if (!hasNoProtoKeys(parsedParams)) {
|
|
183
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: paramsJson must not carry prototype-pollution keys (ERR-076, rc-9 fail loud)`);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
caseId: `case-${kind}-${row.id}`,
|
|
187
|
+
kind,
|
|
188
|
+
toolName: row.toolName,
|
|
189
|
+
params: parsedParams,
|
|
190
|
+
expectedDecision,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Redact sensitive fields from tool call params before they enter the pack.
|
|
195
|
+
*
|
|
196
|
+
* First-version policy:
|
|
197
|
+
* - Absolute paths outside the project tree → replaced with `<redacted:absolute-path>`
|
|
198
|
+
* - String values that look like secrets (long base64 / AKIA-style keys / etc.)
|
|
199
|
+
* → replaced with `<redacted:secret>`
|
|
200
|
+
*
|
|
201
|
+
* Records each redaction in `notes` so the Artificer and the owner can see
|
|
202
|
+
* what was stripped (spec §7.2: redactionNotes is part of the pack contract).
|
|
203
|
+
*/
|
|
204
|
+
function redactParams(params, projectDir) {
|
|
205
|
+
const notes = [];
|
|
206
|
+
const redacted = {};
|
|
207
|
+
for (const key of Object.keys(params)) {
|
|
208
|
+
if (PROTO_KEYS.has(key))
|
|
209
|
+
continue; // defense in depth (ERR-076)
|
|
210
|
+
const value = params[key];
|
|
211
|
+
if (typeof value !== 'string') {
|
|
212
|
+
redacted[key] = value;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// Path-like values
|
|
216
|
+
if (looksLikeAbsolutePath(value) && !value.startsWith(projectDir)) {
|
|
217
|
+
redacted[key] = '<redacted:absolute-path>';
|
|
218
|
+
notes.push(`redacted absolute path in field "${key}" (outside project tree)`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
// Secret-like values (AWS keys, long base64 tokens, etc.)
|
|
222
|
+
if (looksLikeSecret(value)) {
|
|
223
|
+
redacted[key] = '<redacted:secret>';
|
|
224
|
+
notes.push(`redacted possible secret in field "${key}"`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
redacted[key] = value;
|
|
228
|
+
}
|
|
229
|
+
return { redacted, notes };
|
|
230
|
+
}
|
|
231
|
+
function looksLikeAbsolutePath(value) {
|
|
232
|
+
// POSIX absolute path or Windows drive path
|
|
233
|
+
return value.startsWith('/') || /^[A-Za-z]:[\\/]/.test(value);
|
|
234
|
+
}
|
|
235
|
+
const SECRET_PATTERNS = [
|
|
236
|
+
/AKIA[0-9A-Z]{16}/, // AWS access key ID
|
|
237
|
+
/[A-Za-z0-9+/]{40,}/, // long base64-ish token (≥40 chars, no spaces)
|
|
238
|
+
];
|
|
239
|
+
function looksLikeSecret(value) {
|
|
240
|
+
if (value.length < 20)
|
|
241
|
+
return false; // too short to be a real secret
|
|
242
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
243
|
+
if (pattern.test(value))
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
@@ -17,12 +17,20 @@
|
|
|
17
17
|
* Architecture: this file is I/O (delegates to fs-reading safeReadIntentDoc).
|
|
18
18
|
* It will be added to KNOWN_PLUGIN_CORE_FILES in architecture-regression.test.ts.
|
|
19
19
|
*/
|
|
20
|
-
import type { IntentDocReader } from '@principles/core/runtime-v2';
|
|
20
|
+
import type { IntentDocReader, IntentLang } from '@principles/core/runtime-v2';
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Detect which language's INTENT file exists on disk.
|
|
23
|
+
* Priority: zh-CN first, then en. Defaults to zh-CN if neither exists.
|
|
23
24
|
*
|
|
24
|
-
*
|
|
25
|
+
* This lets hooks avoid hardcoding a single language — the Owner may have
|
|
26
|
+
* created either INTENT.zh-CN.md or INTENT.en.md.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveIntentLang(workspaceDir: string): IntentLang;
|
|
29
|
+
/**
|
|
30
|
+
* Create an IntentDocReader bound to a specific workspace and language.
|
|
31
|
+
*
|
|
32
|
+
* The returned reader is stateless beyond the (workspace, lang) binding — each
|
|
25
33
|
* `readIntentDoc()` call delegates to `safeReadIntentDoc()`, which owns
|
|
26
|
-
* the TTL+mtime cache
|
|
34
|
+
* the TTL+mtime cache keyed by `${workspaceDir}:${lang}`.
|
|
27
35
|
*/
|
|
28
|
-
export declare function createIntentDocReader(workspaceDir: string): IntentDocReader;
|
|
36
|
+
export declare function createIntentDocReader(workspaceDir: string, lang: IntentLang): IntentDocReader;
|
|
@@ -17,18 +17,38 @@
|
|
|
17
17
|
* Architecture: this file is I/O (delegates to fs-reading safeReadIntentDoc).
|
|
18
18
|
* It will be added to KNOWN_PLUGIN_CORE_FILES in architecture-regression.test.ts.
|
|
19
19
|
*/
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import * as path from 'node:path';
|
|
20
22
|
import { safeReadIntentDoc } from './intent-doc-reader.js';
|
|
23
|
+
import { getIntentFilename, } from '@principles/core/runtime-v2';
|
|
24
|
+
const INTENT_DIR = '.principles';
|
|
21
25
|
/**
|
|
22
|
-
*
|
|
26
|
+
* Detect which language's INTENT file exists on disk.
|
|
27
|
+
* Priority: zh-CN first, then en. Defaults to zh-CN if neither exists.
|
|
23
28
|
*
|
|
24
|
-
*
|
|
29
|
+
* This lets hooks avoid hardcoding a single language — the Owner may have
|
|
30
|
+
* created either INTENT.zh-CN.md or INTENT.en.md.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveIntentLang(workspaceDir) {
|
|
33
|
+
const zhPath = path.join(workspaceDir, INTENT_DIR, getIntentFilename('zh-CN'));
|
|
34
|
+
if (fs.existsSync(zhPath))
|
|
35
|
+
return 'zh-CN';
|
|
36
|
+
const enPath = path.join(workspaceDir, INTENT_DIR, getIntentFilename('en'));
|
|
37
|
+
if (fs.existsSync(enPath))
|
|
38
|
+
return 'en';
|
|
39
|
+
return 'zh-CN';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create an IntentDocReader bound to a specific workspace and language.
|
|
43
|
+
*
|
|
44
|
+
* The returned reader is stateless beyond the (workspace, lang) binding — each
|
|
25
45
|
* `readIntentDoc()` call delegates to `safeReadIntentDoc()`, which owns
|
|
26
|
-
* the TTL+mtime cache
|
|
46
|
+
* the TTL+mtime cache keyed by `${workspaceDir}:${lang}`.
|
|
27
47
|
*/
|
|
28
|
-
export function createIntentDocReader(workspaceDir) {
|
|
48
|
+
export function createIntentDocReader(workspaceDir, lang) {
|
|
29
49
|
return {
|
|
30
50
|
readIntentDoc() {
|
|
31
|
-
const result = safeReadIntentDoc(workspaceDir);
|
|
51
|
+
const result = safeReadIntentDoc(workspaceDir, lang);
|
|
32
52
|
if (result.ok && result.doc) {
|
|
33
53
|
const doc = result.doc;
|
|
34
54
|
const reference = {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Architecture: this file is I/O (fs, path). It is whitelisted in
|
|
22
22
|
* architecture-regression.test.ts KNOWN_PLUGIN_CORE_FILES.
|
|
23
23
|
*/
|
|
24
|
-
import { type IntentDocSections, type IntentDocWarning } from '@principles/core/runtime-v2';
|
|
24
|
+
import { type IntentDocSections, type IntentDocWarning, type IntentLang } from '@principles/core/runtime-v2';
|
|
25
25
|
export type SafeReadIntentDocReason = 'flag_disabled' | 'not_found' | 'oversized' | 'read_error';
|
|
26
26
|
export interface IntentDoc {
|
|
27
27
|
/** Raw INTENT.md content (unescaped — caller escapes for prompt injection). */
|
|
@@ -55,9 +55,17 @@ export interface SafeReadIntentDocResult {
|
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
57
|
* Reset the intent doc cache for test isolation.
|
|
58
|
-
*
|
|
58
|
+
*
|
|
59
|
+
* - No args: clear the whole cache.
|
|
60
|
+
* - workspaceDir only: delete all entries for that workspace (any lang).
|
|
61
|
+
* Keys are formatted as `${workspaceDir}:${lang}`, so we match by prefix.
|
|
62
|
+
* - workspaceDir + lang: delete the single `${workspaceDir}:${lang}` entry.
|
|
63
|
+
*
|
|
64
|
+
* Note: prompt.ts resetPromptStateForTest passes workspaceDir only — the
|
|
65
|
+
* previous signature treated it as a literal cache key, which never matched
|
|
66
|
+
* the `${workspaceDir}:${lang}` format and silently left entries behind.
|
|
59
67
|
*/
|
|
60
|
-
export declare function resetIntentDocCacheForTest(workspaceDir?: string): void;
|
|
68
|
+
export declare function resetIntentDocCacheForTest(workspaceDir?: string, lang?: IntentLang): void;
|
|
61
69
|
/**
|
|
62
70
|
* Safely read INTENT.md for prompt injection.
|
|
63
71
|
*
|
|
@@ -69,10 +77,11 @@ export declare function resetIntentDocCacheForTest(workspaceDir?: string): void;
|
|
|
69
77
|
* 4. Never throws — all errors return structured reason + nextAction.
|
|
70
78
|
*
|
|
71
79
|
* @param workspaceDir - Absolute path to the workspace root
|
|
80
|
+
* @param lang - Mandatory language code ('zh-CN' | 'en'); selects INTENT.${lang}.md
|
|
72
81
|
* @param options - Optional logger for debug-level diagnostics
|
|
73
82
|
* @returns SafeReadIntentDocResult (never throws)
|
|
74
83
|
*/
|
|
75
|
-
export declare function safeReadIntentDoc(workspaceDir: string, options?: {
|
|
84
|
+
export declare function safeReadIntentDoc(workspaceDir: string, lang: IntentLang, options?: {
|
|
76
85
|
logger?: {
|
|
77
86
|
debug?: (msg: string) => void;
|
|
78
87
|
warn?: (msg: string) => void;
|
|
@@ -23,29 +23,48 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import * as fs from 'node:fs';
|
|
25
25
|
import * as path from 'node:path';
|
|
26
|
-
import { INTENT_MAX_BYTES, parseIntentDocSections, computeIntentContentHash, validateIntentDocSections, } from '@principles/core/runtime-v2';
|
|
26
|
+
import { INTENT_MAX_BYTES, getIntentFilename, parseIntentDocSections, computeIntentContentHash, validateIntentDocSections, } from '@principles/core/runtime-v2';
|
|
27
27
|
import { loadFeatureFlagFromConfig } from './pd-config-loader.js';
|
|
28
28
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
29
|
-
const INTENT_FILENAME = 'INTENT.md';
|
|
30
29
|
const INTENT_DIR = '.principles';
|
|
31
30
|
const INTENT_CACHE_TTL_MS = 60_000; // 1 minute (SPEC §12.1)
|
|
32
31
|
// ── Module-level cache (per-workspace) ───────────────────────────────────────
|
|
33
32
|
const _intentDocCache = new Map();
|
|
34
33
|
/**
|
|
35
34
|
* Reset the intent doc cache for test isolation.
|
|
36
|
-
*
|
|
35
|
+
*
|
|
36
|
+
* - No args: clear the whole cache.
|
|
37
|
+
* - workspaceDir only: delete all entries for that workspace (any lang).
|
|
38
|
+
* Keys are formatted as `${workspaceDir}:${lang}`, so we match by prefix.
|
|
39
|
+
* - workspaceDir + lang: delete the single `${workspaceDir}:${lang}` entry.
|
|
40
|
+
*
|
|
41
|
+
* Note: prompt.ts resetPromptStateForTest passes workspaceDir only — the
|
|
42
|
+
* previous signature treated it as a literal cache key, which never matched
|
|
43
|
+
* the `${workspaceDir}:${lang}` format and silently left entries behind.
|
|
37
44
|
*/
|
|
38
|
-
export function resetIntentDocCacheForTest(workspaceDir) {
|
|
39
|
-
if (workspaceDir) {
|
|
40
|
-
_intentDocCache.delete(workspaceDir);
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
45
|
+
export function resetIntentDocCacheForTest(workspaceDir, lang) {
|
|
46
|
+
if (workspaceDir === undefined) {
|
|
43
47
|
_intentDocCache.clear();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (lang !== undefined) {
|
|
51
|
+
_intentDocCache.delete(`${workspaceDir}:${lang}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// workspaceDir only — clear all langs for this workspace by prefix.
|
|
55
|
+
const prefix = `${workspaceDir}:`;
|
|
56
|
+
for (const key of _intentDocCache.keys()) {
|
|
57
|
+
if (key.startsWith(prefix)) {
|
|
58
|
+
_intentDocCache.delete(key);
|
|
59
|
+
}
|
|
44
60
|
}
|
|
45
61
|
}
|
|
46
62
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
47
|
-
function getIntentFilePath(workspaceDir) {
|
|
48
|
-
return path.join(workspaceDir, INTENT_DIR,
|
|
63
|
+
function getIntentFilePath(workspaceDir, lang) {
|
|
64
|
+
return path.join(workspaceDir, INTENT_DIR, getIntentFilename(lang));
|
|
65
|
+
}
|
|
66
|
+
function getCacheKey(workspaceDir, lang) {
|
|
67
|
+
return `${workspaceDir}:${lang}`;
|
|
49
68
|
}
|
|
50
69
|
function buildDocFromRaw(raw, filePath, readAt) {
|
|
51
70
|
const sections = parseIntentDocSections(raw);
|
|
@@ -72,10 +91,11 @@ function buildDocFromRaw(raw, filePath, readAt) {
|
|
|
72
91
|
* 4. Never throws — all errors return structured reason + nextAction.
|
|
73
92
|
*
|
|
74
93
|
* @param workspaceDir - Absolute path to the workspace root
|
|
94
|
+
* @param lang - Mandatory language code ('zh-CN' | 'en'); selects INTENT.${lang}.md
|
|
75
95
|
* @param options - Optional logger for debug-level diagnostics
|
|
76
96
|
* @returns SafeReadIntentDocResult (never throws)
|
|
77
97
|
*/
|
|
78
|
-
export function safeReadIntentDoc(workspaceDir, options) {
|
|
98
|
+
export function safeReadIntentDoc(workspaceDir, lang, options) {
|
|
79
99
|
// SPEC §12 — Flag check FIRST. Flag off → no fs access, no cache access.
|
|
80
100
|
const flagResult = loadFeatureFlagFromConfig(workspaceDir, 'intent_engineering', options?.logger);
|
|
81
101
|
if (!flagResult.enabled) {
|
|
@@ -84,14 +104,15 @@ export function safeReadIntentDoc(workspaceDir, options) {
|
|
|
84
104
|
found: false,
|
|
85
105
|
flagEnabled: false,
|
|
86
106
|
reason: 'flag_disabled',
|
|
87
|
-
nextAction:
|
|
107
|
+
nextAction: `Enable the intent_engineering feature flag in .pd/config.yaml to read ${getIntentFilename(lang)}.`,
|
|
88
108
|
warnings: [],
|
|
89
109
|
};
|
|
90
110
|
}
|
|
91
|
-
const
|
|
111
|
+
const cacheKey = getCacheKey(workspaceDir, lang);
|
|
112
|
+
const filePath = getIntentFilePath(workspaceDir, lang);
|
|
92
113
|
const now = Date.now();
|
|
93
114
|
// Check cache freshness (TTL + mtime)
|
|
94
|
-
const cached = _intentDocCache.get(
|
|
115
|
+
const cached = _intentDocCache.get(cacheKey);
|
|
95
116
|
if (cached && (now - cached.loadedAt) < INTENT_CACHE_TTL_MS) {
|
|
96
117
|
// Cache is within TTL. Verify mtime hasn't changed.
|
|
97
118
|
try {
|
|
@@ -117,26 +138,26 @@ export function safeReadIntentDoc(workspaceDir, options) {
|
|
|
117
138
|
// Check existence first
|
|
118
139
|
if (!fs.existsSync(filePath)) {
|
|
119
140
|
// Invalidate stale cache entry if any
|
|
120
|
-
_intentDocCache.delete(
|
|
141
|
+
_intentDocCache.delete(cacheKey);
|
|
121
142
|
return {
|
|
122
143
|
ok: false,
|
|
123
144
|
found: false,
|
|
124
145
|
flagEnabled: true,
|
|
125
146
|
reason: 'not_found',
|
|
126
|
-
nextAction:
|
|
147
|
+
nextAction: `Create .principles/${getIntentFilename(lang)} using "pd intent init".`,
|
|
127
148
|
warnings: [],
|
|
128
149
|
};
|
|
129
150
|
}
|
|
130
151
|
const stat = fs.statSync(filePath);
|
|
131
152
|
// SPEC §12 — 32KB size cap
|
|
132
153
|
if (stat.size > INTENT_MAX_BYTES) {
|
|
133
|
-
_intentDocCache.delete(
|
|
154
|
+
_intentDocCache.delete(cacheKey);
|
|
134
155
|
return {
|
|
135
156
|
ok: false,
|
|
136
157
|
found: true,
|
|
137
158
|
flagEnabled: true,
|
|
138
159
|
reason: 'oversized',
|
|
139
|
-
nextAction:
|
|
160
|
+
nextAction: `${getIntentFilename(lang)} exceeds ${INTENT_MAX_BYTES} bytes (${stat.size} bytes). Reduce content.`,
|
|
140
161
|
warnings: [],
|
|
141
162
|
};
|
|
142
163
|
}
|
|
@@ -147,19 +168,19 @@ export function safeReadIntentDoc(workspaceDir, options) {
|
|
|
147
168
|
// an oversized file would enter parse/hash/cache as ok:true.
|
|
148
169
|
const actualBytes = Buffer.byteLength(raw, 'utf8');
|
|
149
170
|
if (actualBytes > INTENT_MAX_BYTES) {
|
|
150
|
-
_intentDocCache.delete(
|
|
171
|
+
_intentDocCache.delete(cacheKey);
|
|
151
172
|
return {
|
|
152
173
|
ok: false,
|
|
153
174
|
found: true,
|
|
154
175
|
flagEnabled: true,
|
|
155
176
|
reason: 'oversized',
|
|
156
|
-
nextAction:
|
|
177
|
+
nextAction: `${getIntentFilename(lang)} exceeds ${INTENT_MAX_BYTES} bytes (${actualBytes} bytes after read). Reduce content.`,
|
|
157
178
|
warnings: [],
|
|
158
179
|
};
|
|
159
180
|
}
|
|
160
181
|
const doc = buildDocFromRaw(raw, filePath, new Date(now).toISOString());
|
|
161
182
|
// Update cache
|
|
162
|
-
_intentDocCache.set(
|
|
183
|
+
_intentDocCache.set(cacheKey, {
|
|
163
184
|
doc,
|
|
164
185
|
mtime: stat.mtimeMs,
|
|
165
186
|
loadedAt: now,
|
|
@@ -174,15 +195,15 @@ export function safeReadIntentDoc(workspaceDir, options) {
|
|
|
174
195
|
}
|
|
175
196
|
catch (err) {
|
|
176
197
|
// ERR-002 — graceful degradation with reason + nextAction
|
|
177
|
-
_intentDocCache.delete(
|
|
198
|
+
_intentDocCache.delete(cacheKey);
|
|
178
199
|
const message = err instanceof Error ? err.message : String(err);
|
|
179
|
-
options?.logger?.warn?.(`[PD:Intent] safeReadIntentDoc failed: workspace=${workspaceDir}, error=${message}`);
|
|
200
|
+
options?.logger?.warn?.(`[PD:Intent] safeReadIntentDoc failed: workspace=${workspaceDir}, lang=${lang}, error=${message}`);
|
|
180
201
|
return {
|
|
181
202
|
ok: false,
|
|
182
203
|
found: false,
|
|
183
204
|
flagEnabled: true,
|
|
184
205
|
reason: 'read_error',
|
|
185
|
-
nextAction:
|
|
206
|
+
nextAction: `Check filesystem permissions for .principles/${getIntentFilename(lang)}.`,
|
|
186
207
|
warnings: [],
|
|
187
208
|
};
|
|
188
209
|
}
|
|
@@ -146,6 +146,29 @@ export declare class TrajectoryDatabase {
|
|
|
146
146
|
confidence: number | null;
|
|
147
147
|
createdAt: string;
|
|
148
148
|
}[];
|
|
149
|
+
/**
|
|
150
|
+
* PRI-484 Phase 5 — Look up a single pain event by canonical_pain_id.
|
|
151
|
+
*
|
|
152
|
+
* Used by BehaviorExamplePackAssembler to anchor a pain lineage without
|
|
153
|
+
* already knowing the session_id. Returns null when not found.
|
|
154
|
+
*
|
|
155
|
+
* ERR-001 (rc-1, rc-2): row fields validated as unknown — no `as` bypass.
|
|
156
|
+
* Uses a type-guard predicate to narrow the DB row structurally.
|
|
157
|
+
*/
|
|
158
|
+
getPainEventByCanonicalId(canonicalPainId: string): {
|
|
159
|
+
id: number;
|
|
160
|
+
sessionId: string;
|
|
161
|
+
source: string;
|
|
162
|
+
score: number;
|
|
163
|
+
reason: string | null;
|
|
164
|
+
severity: string | null;
|
|
165
|
+
origin: string | null;
|
|
166
|
+
confidence: number | null;
|
|
167
|
+
text: string | null;
|
|
168
|
+
canonicalPainId: string;
|
|
169
|
+
runtimeTaskId: string | null;
|
|
170
|
+
createdAt: string;
|
|
171
|
+
} | null;
|
|
149
172
|
/**
|
|
150
173
|
* List user turns for a session.
|
|
151
174
|
* Returns sanitized/reduced fields for nocturnal use — NO raw text.
|
package/dist/core/trajectory.js
CHANGED
|
@@ -26,6 +26,34 @@ const SCHEMA_VERSION = 1;
|
|
|
26
26
|
function hasNameField(row) {
|
|
27
27
|
return Object.hasOwn(row, 'name');
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Pure structural narrowing from `unknown` to `Record<string, unknown>` with
|
|
31
|
+
* no `as` cast (ERR-001 / rc-2). The returned type allows indexed access in
|
|
32
|
+
* subsequent code without further casts.
|
|
33
|
+
*/
|
|
34
|
+
function isStringRecord(value) {
|
|
35
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
function isPainEventRow(value) {
|
|
38
|
+
if (!isStringRecord(value))
|
|
39
|
+
return false;
|
|
40
|
+
if (typeof value.id !== 'number')
|
|
41
|
+
return false;
|
|
42
|
+
if (typeof value.session_id !== 'string')
|
|
43
|
+
return false;
|
|
44
|
+
// The remaining fields are accessed via String()/Number() which tolerate any
|
|
45
|
+
// primitive; presence check only (rc-5: Object.hasOwn over `in`).
|
|
46
|
+
return (Object.hasOwn(value, 'source')
|
|
47
|
+
&& Object.hasOwn(value, 'score')
|
|
48
|
+
&& Object.hasOwn(value, 'reason')
|
|
49
|
+
&& Object.hasOwn(value, 'severity')
|
|
50
|
+
&& Object.hasOwn(value, 'origin')
|
|
51
|
+
&& Object.hasOwn(value, 'confidence')
|
|
52
|
+
&& Object.hasOwn(value, 'text')
|
|
53
|
+
&& Object.hasOwn(value, 'canonical_pain_id')
|
|
54
|
+
&& Object.hasOwn(value, 'runtime_task_id')
|
|
55
|
+
&& Object.hasOwn(value, 'created_at'));
|
|
56
|
+
}
|
|
29
57
|
/**
|
|
30
58
|
* Apply the full trajectory.db schema (tables + indexes + views + migrations) to an open
|
|
31
59
|
* Database handle. Used by TrajectoryDatabase.initSchema() and exported via
|
|
@@ -957,6 +985,40 @@ export class TrajectoryDatabase {
|
|
|
957
985
|
createdAt: String(row.created_at),
|
|
958
986
|
}));
|
|
959
987
|
}
|
|
988
|
+
/**
|
|
989
|
+
* PRI-484 Phase 5 — Look up a single pain event by canonical_pain_id.
|
|
990
|
+
*
|
|
991
|
+
* Used by BehaviorExamplePackAssembler to anchor a pain lineage without
|
|
992
|
+
* already knowing the session_id. Returns null when not found.
|
|
993
|
+
*
|
|
994
|
+
* ERR-001 (rc-1, rc-2): row fields validated as unknown — no `as` bypass.
|
|
995
|
+
* Uses a type-guard predicate to narrow the DB row structurally.
|
|
996
|
+
*/
|
|
997
|
+
getPainEventByCanonicalId(canonicalPainId) {
|
|
998
|
+
const rawRow = this.db.prepare(`
|
|
999
|
+
SELECT id, session_id, source, score, reason, severity, origin, confidence,
|
|
1000
|
+
text, canonical_pain_id, runtime_task_id, created_at
|
|
1001
|
+
FROM pain_events
|
|
1002
|
+
WHERE canonical_pain_id = ?
|
|
1003
|
+
LIMIT 1
|
|
1004
|
+
`).get(canonicalPainId);
|
|
1005
|
+
if (!isPainEventRow(rawRow))
|
|
1006
|
+
return null;
|
|
1007
|
+
return {
|
|
1008
|
+
id: rawRow.id,
|
|
1009
|
+
sessionId: rawRow.session_id,
|
|
1010
|
+
source: String(rawRow.source),
|
|
1011
|
+
score: Number(rawRow.score),
|
|
1012
|
+
reason: rawRow.reason ? String(rawRow.reason) : null,
|
|
1013
|
+
severity: rawRow.severity ? String(rawRow.severity) : null,
|
|
1014
|
+
origin: rawRow.origin ? String(rawRow.origin) : null,
|
|
1015
|
+
confidence: rawRow.confidence != null ? Number(rawRow.confidence) : null,
|
|
1016
|
+
text: rawRow.text ? String(rawRow.text) : null,
|
|
1017
|
+
canonicalPainId: String(rawRow.canonical_pain_id),
|
|
1018
|
+
runtimeTaskId: rawRow.runtime_task_id ? String(rawRow.runtime_task_id) : null,
|
|
1019
|
+
createdAt: String(rawRow.created_at),
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
960
1022
|
/**
|
|
961
1023
|
* List user turns for a session.
|
|
962
1024
|
* Returns sanitized/reduced fields for nocturnal use — NO raw text.
|
package/dist/hooks/pain.js
CHANGED
|
@@ -25,7 +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
|
+
import { createIntentDocReader, resolveIntentLang } from '../core/intent-doc-reader-adapter.js';
|
|
29
29
|
import { evaluateTriggerController } from '@principles/core/runtime-v2';
|
|
30
30
|
import { isSharedCooldownActive, markSharedEpisodeAsDiagnosed } from './trigger-cooldown-tracker.js';
|
|
31
31
|
import { buildManualPainObservation, resolveSourceKind } from './raw-observation-adapter.js';
|
|
@@ -48,7 +48,7 @@ function createPainToPrincipleService(wctx) {
|
|
|
48
48
|
// PRI-468: wire INTENT.md reader so Stage A can produce intentTension
|
|
49
49
|
// when intent_engineering flag is on. Adapter performs no I/O beyond
|
|
50
50
|
// delegating to safeReadIntentDoc (which owns the flag-first check).
|
|
51
|
-
intentDocReader: createIntentDocReader(wctx.workspaceDir),
|
|
51
|
+
intentDocReader: createIntentDocReader(wctx.workspaceDir, resolveIntentLang(wctx.workspaceDir)),
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
// buildTrajectoryEvidence is in ./trajectory-evidence.ts (re-exported above)
|
package/dist/hooks/prompt.js
CHANGED
|
@@ -21,6 +21,7 @@ import { buildEmpathyObservation, resolveSourceKind } from './raw-observation-ad
|
|
|
21
21
|
import { evaluateEvidenceTriage } from './triage-adapter.js';
|
|
22
22
|
import { loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
|
|
23
23
|
import { safeReadIntentDoc, resetIntentDocCacheForTest } from '../core/intent-doc-reader.js';
|
|
24
|
+
import { resolveIntentLang } from '../core/intent-doc-reader-adapter.js';
|
|
24
25
|
import { buildIntentFrictionBlock } from '@principles/core/runtime-v2';
|
|
25
26
|
import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
|
|
26
27
|
import { detectCorrectionCue as coreDetectCorrectionCue, escapeXml, extractMessageContent, isMinimalTrigger, } from '@principles/core/prompt-builder';
|
|
@@ -909,7 +910,7 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
909
910
|
try {
|
|
910
911
|
const intentFlag = loadFeatureFlagFromConfig(workspaceDir, 'intent_engineering', logger);
|
|
911
912
|
if (intentFlag.enabled) {
|
|
912
|
-
const intentResult = safeReadIntentDoc(workspaceDir, { logger });
|
|
913
|
+
const intentResult = safeReadIntentDoc(workspaceDir, resolveIntentLang(workspaceDir), { logger });
|
|
913
914
|
if (intentResult.ok && intentResult.doc) {
|
|
914
915
|
const block = buildIntentFrictionBlock({ rawIntentMd: intentResult.doc.raw });
|
|
915
916
|
if (block.length > 0) {
|
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.164.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|