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.
@@ -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
+ }
@@ -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. Working Memory (preserved from last compaction)
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;
@@ -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 → thinking_os
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;
@@ -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.150.0",
5
+ "version": "1.152.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.150.0",
3
+ "version": "1.152.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",