principles-disciple 1.162.0 → 1.163.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,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,
@@ -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
- * Create an IntentDocReader bound to a specific workspace.
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
- * The returned reader is stateless beyond the workspace binding — each
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
- * Create an IntentDocReader bound to a specific workspace.
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
- * The returned reader is stateless beyond the workspace binding — each
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
- * Call in beforeEach() to ensure tests don't pollute each other.
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
- * Call in beforeEach() to ensure tests don't pollute each other.
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, INTENT_FILENAME);
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: 'Enable the intent_engineering feature flag in .pd/config.yaml to read INTENT.md.',
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 filePath = getIntentFilePath(workspaceDir);
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(workspaceDir);
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(workspaceDir);
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: 'Create .principles/INTENT.md using "pd intent init".',
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(workspaceDir);
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: `INTENT.md exceeds ${INTENT_MAX_BYTES} bytes (${stat.size} bytes). Reduce content.`,
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(workspaceDir);
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: `INTENT.md exceeds ${INTENT_MAX_BYTES} bytes (${actualBytes} bytes after read). Reduce content.`,
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(workspaceDir, {
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(workspaceDir);
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: 'Check filesystem permissions for .principles/INTENT.md.',
206
+ nextAction: `Check filesystem permissions for .principles/${getIntentFilename(lang)}.`,
186
207
  warnings: [],
187
208
  };
188
209
  }
@@ -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)
@@ -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) {
@@ -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.162.0",
5
+ "version": "1.163.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.162.0",
3
+ "version": "1.163.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",