principles-disciple 1.111.0 → 1.112.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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/index.ts +44 -0
- package/src/service/internalization-auto-consumer-service.ts +293 -0
- package/tests/evolution-worker-slimming.test.ts +2 -0
- package/tests/integration/mvp-surface-registry-guard.test.ts +1 -0
- package/tests/internalization-auto-consumer-gate.test.ts +158 -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.112.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { handleSamplesCommand } from './commands/samples.js';
|
|
|
52
52
|
import { handleWorkflowDebugCommand } from './commands/workflow-debug.js';
|
|
53
53
|
import { EvolutionWorkerService } from './service/evolution-worker.js';
|
|
54
54
|
import { CorrectionObserverService } from './service/correction-observer-service.js';
|
|
55
|
+
import { InternalizationAutoConsumerService } from './service/internalization-auto-consumer-service.js';
|
|
55
56
|
import { TrajectoryService } from './service/trajectory-service.js';
|
|
56
57
|
import { PDTaskService } from './core/pd-task-service.js';
|
|
57
58
|
import { CentralSyncService } from './service/central-sync-service.js';
|
|
@@ -149,6 +150,30 @@ export function shouldStartCorrectionObserver(
|
|
|
149
150
|
return { shouldStart: false, flagSource: flag.source, disabledInfo };
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
export interface InternalizationAutoConsumerGateResult {
|
|
154
|
+
shouldStart: boolean;
|
|
155
|
+
flagSource: string;
|
|
156
|
+
disabledInfo: string | null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function shouldStartInternalizationAutoConsumer(
|
|
160
|
+
workspaceDir: string,
|
|
161
|
+
logger: { info?: (msg: string) => void; warn?: (msg: string) => void },
|
|
162
|
+
): InternalizationAutoConsumerGateResult {
|
|
163
|
+
const flag = loadFeatureFlagFromWorkspace(workspaceDir, 'internalization_auto_consumer', logger);
|
|
164
|
+
if (flag.enabled) {
|
|
165
|
+
return { shouldStart: true, flagSource: flag.source, disabledInfo: null };
|
|
166
|
+
}
|
|
167
|
+
const disabledInfo = JSON.stringify({
|
|
168
|
+
reason: 'internalization_auto_consumer_disabled',
|
|
169
|
+
nextAction: `pd runtime internalization run-once --workspace "${workspaceDir}" --runner dreamer --runtime config --json`,
|
|
170
|
+
featureFlag: 'internalization_auto_consumer',
|
|
171
|
+
boundedContext: 'internalization_auto_consumer',
|
|
172
|
+
flagSource: flag.source,
|
|
173
|
+
});
|
|
174
|
+
return { shouldStart: false, flagSource: flag.source, disabledInfo };
|
|
175
|
+
}
|
|
176
|
+
|
|
152
177
|
const plugin = {
|
|
153
178
|
name: "Principles Disciple",
|
|
154
179
|
description: "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
|
|
@@ -258,6 +283,23 @@ const plugin = {
|
|
|
258
283
|
api.logger.info(`[PD] CorrectionObserver NOT started for workspace: ${workspaceDir}. ${corrGate.disabledInfo}`);
|
|
259
284
|
SystemLogger.log(workspaceDir, 'CORRECTION_OBSERVER_DISABLED', corrGate.disabledInfo ?? '');
|
|
260
285
|
}
|
|
286
|
+
|
|
287
|
+
// ── Start InternalizationAutoConsumer for THIS workspace ──
|
|
288
|
+
// PRI-381: Bounded auto-consumer for dreamer ready tasks.
|
|
289
|
+
// Default ON for dogfood; kill switch via features.internalization_auto_consumer.enabled=false.
|
|
290
|
+
const autoConsGate = shouldStartInternalizationAutoConsumer(workspaceDir, api.logger);
|
|
291
|
+
if (autoConsGate.shouldStart) {
|
|
292
|
+
InternalizationAutoConsumerService.start({
|
|
293
|
+
config: api.config,
|
|
294
|
+
workspaceDir,
|
|
295
|
+
stateDir: path.join(workspaceDir, '.state'),
|
|
296
|
+
logger: api.logger,
|
|
297
|
+
});
|
|
298
|
+
api.logger.info(`[PD] InternalizationAutoConsumer started for workspace: ${workspaceDir} (flag source: ${autoConsGate.flagSource})`);
|
|
299
|
+
} else {
|
|
300
|
+
api.logger.info(`[PD] InternalizationAutoConsumer NOT started for workspace: ${workspaceDir}. ${autoConsGate.disabledInfo}`);
|
|
301
|
+
SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_DISABLED', autoConsGate.disabledInfo ?? '');
|
|
302
|
+
}
|
|
261
303
|
}
|
|
262
304
|
|
|
263
305
|
const result = await handleBeforePromptBuild(event, { ...ctx, api: api as Parameters<typeof handleBeforePromptBuild>[1]['api'], workspaceDir });
|
|
@@ -562,6 +604,8 @@ const plugin = {
|
|
|
562
604
|
if (guardedPdTask) api.registerService(guardedPdTask);
|
|
563
605
|
const guardedCentralSync = guardService('service:central-sync', CentralSyncService, api.logger);
|
|
564
606
|
if (guardedCentralSync) api.registerService(guardedCentralSync);
|
|
607
|
+
const guardedAutoConsumer = guardService('service:internalization-auto-consumer', InternalizationAutoConsumerService, api.logger);
|
|
608
|
+
if (guardedAutoConsumer) api.registerService(guardedAutoConsumer);
|
|
565
609
|
} catch (err) {
|
|
566
610
|
api.logger.error(`[PD] Failed to register services: ${String(err)}`);
|
|
567
611
|
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import type { OpenClawPluginServiceContext, PluginLogger } from '../openclaw-sdk.js';
|
|
2
|
+
import {
|
|
3
|
+
createRuntimeStateHandle,
|
|
4
|
+
InternalizationOrchestrator,
|
|
5
|
+
DreamerRunner,
|
|
6
|
+
DefaultDreamerValidator,
|
|
7
|
+
PiAiRuntimeAdapter,
|
|
8
|
+
storeEmitter,
|
|
9
|
+
resolveRuntimeConfigFromPdConfig,
|
|
10
|
+
isRuntimeConfigError,
|
|
11
|
+
computeConsumerDecision,
|
|
12
|
+
InternalizationQueueReadModel,
|
|
13
|
+
MVP_CORE_TASK_KINDS,
|
|
14
|
+
} from '@principles/core/runtime-v2';
|
|
15
|
+
import { loadPdConfigForPlugin, loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
|
|
16
|
+
import { SystemLogger } from '../core/system-logger.js';
|
|
17
|
+
|
|
18
|
+
const INTERNALIZATION_AUTO_CONSUMER_INTERVAL_MS = 120_000;
|
|
19
|
+
const INTERNALIZATION_AUTO_CONSUMER_INITIAL_DELAY_MS = 30_000;
|
|
20
|
+
const INTERNALIZATION_AUTO_CONSUMER_FLAG_ID = 'internalization_auto_consumer';
|
|
21
|
+
|
|
22
|
+
export interface InternalizationAutoConsumerServiceShape {
|
|
23
|
+
id: string;
|
|
24
|
+
start: (ctx: OpenClawPluginServiceContext) => void;
|
|
25
|
+
stop?: (ctx: OpenClawPluginServiceContext) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface WorkspaceConsumerState {
|
|
29
|
+
stopped: boolean;
|
|
30
|
+
timeoutId: ReturnType<typeof setTimeout> | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const workspaceStates = new Map<string, WorkspaceConsumerState>();
|
|
34
|
+
|
|
35
|
+
function getWorkspaceState(workspaceDir: string): WorkspaceConsumerState {
|
|
36
|
+
let state = workspaceStates.get(workspaceDir);
|
|
37
|
+
if (!state) {
|
|
38
|
+
state = { stopped: false, timeoutId: null };
|
|
39
|
+
workspaceStates.set(workspaceDir, state);
|
|
40
|
+
}
|
|
41
|
+
return state;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatRunOnceCommand(workspaceDir: string): string {
|
|
45
|
+
return `pd runtime internalization run-once --workspace "${workspaceDir}" --runner dreamer --runtime config --json`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function runConsumerCycle(
|
|
49
|
+
workspaceDir: string,
|
|
50
|
+
logger: PluginLogger,
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
const flag = loadFeatureFlagFromConfig(workspaceDir, INTERNALIZATION_AUTO_CONSUMER_FLAG_ID, {
|
|
53
|
+
info: (msg: string) => logger.info(msg),
|
|
54
|
+
warn: (msg: string) => logger.warn(msg),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!flag.enabled) {
|
|
58
|
+
const disabledInfo = JSON.stringify({
|
|
59
|
+
reason: 'internalization_auto_consumer_disabled',
|
|
60
|
+
nextAction: formatRunOnceCommand(workspaceDir),
|
|
61
|
+
flagSource: flag.source,
|
|
62
|
+
});
|
|
63
|
+
SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_SKIP', disabledInfo);
|
|
64
|
+
logger.info(`[PD:AutoConsumer] Cycle skipped: auto-consumer disabled. Source: ${flag.source}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const configResult = loadPdConfigForPlugin(workspaceDir);
|
|
69
|
+
if (!configResult.ok) {
|
|
70
|
+
const malformedInfo = JSON.stringify({
|
|
71
|
+
reason: 'config_malformed',
|
|
72
|
+
nextAction: configResult.errors[0]?.nextAction ?? 'Fix .pd/config.yaml and retry',
|
|
73
|
+
errors: configResult.errors.map((e) => e.reason),
|
|
74
|
+
});
|
|
75
|
+
SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_SKIP', malformedInfo);
|
|
76
|
+
logger.warn(`[PD:AutoConsumer] Config malformed, skipping cycle.`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const runtimeConfigResult = resolveRuntimeConfigFromPdConfig(
|
|
81
|
+
configResult.effective,
|
|
82
|
+
(name: string) => process.env[name],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (isRuntimeConfigError(runtimeConfigResult)) {
|
|
86
|
+
const rtInfo = JSON.stringify({
|
|
87
|
+
reason: 'runtime_config_error',
|
|
88
|
+
message: runtimeConfigResult.message,
|
|
89
|
+
nextAction: runtimeConfigResult.nextAction,
|
|
90
|
+
});
|
|
91
|
+
SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_SKIP', rtInfo);
|
|
92
|
+
logger.warn(`[PD:AutoConsumer] Runtime config error: ${runtimeConfigResult.message}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let handle: Awaited<ReturnType<typeof createRuntimeStateHandle>> | null = null;
|
|
97
|
+
try {
|
|
98
|
+
handle = await createRuntimeStateHandle({ workspaceDir, readonly: false });
|
|
99
|
+
const { stateManager } = handle;
|
|
100
|
+
|
|
101
|
+
const readModel = new InternalizationQueueReadModel(stateManager);
|
|
102
|
+
readModel.setPolicy({
|
|
103
|
+
enabledChannels: new Set(['prompt', 'code_tool_hook', 'defer_archive']),
|
|
104
|
+
actionableTaskKinds: new Set(MVP_CORE_TASK_KINDS),
|
|
105
|
+
});
|
|
106
|
+
const snapshot = await readModel.getSnapshot();
|
|
107
|
+
|
|
108
|
+
const decision = computeConsumerDecision({
|
|
109
|
+
autoConsumerEnabled: true,
|
|
110
|
+
readyTaskCount: snapshot.readyTasks.length,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!decision.shouldConsume) {
|
|
114
|
+
SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_SKIP', JSON.stringify({
|
|
115
|
+
reason: decision.reason,
|
|
116
|
+
readyTaskCount: snapshot.readyTasks.length,
|
|
117
|
+
}));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const orchestrator = new InternalizationOrchestrator(
|
|
122
|
+
{ stateManager },
|
|
123
|
+
{ owner: 'auto-consumer', runtimeKind: 'config', dryRun: false },
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const wakeResult = await orchestrator.wakeOnce('dreamer');
|
|
127
|
+
|
|
128
|
+
if (wakeResult.decision !== 'leased') {
|
|
129
|
+
const skipPayload: Record<string, unknown> = {
|
|
130
|
+
decision: wakeResult.decision,
|
|
131
|
+
};
|
|
132
|
+
if (wakeResult.decision === 'no_ready_tasks') {
|
|
133
|
+
skipPayload.reason = wakeResult.reason;
|
|
134
|
+
}
|
|
135
|
+
SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_SKIP', JSON.stringify(skipPayload));
|
|
136
|
+
logger.info(`[PD:AutoConsumer] No task to consume: ${wakeResult.decision}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const adapter = new PiAiRuntimeAdapter({
|
|
141
|
+
provider: runtimeConfigResult.provider ?? 'openai',
|
|
142
|
+
model: runtimeConfigResult.model ?? 'gpt-4o',
|
|
143
|
+
apiKeyEnv: runtimeConfigResult.apiKeyEnv ?? 'OPENAI_API_KEY',
|
|
144
|
+
maxRetries: runtimeConfigResult.maxRetries,
|
|
145
|
+
timeoutMs: runtimeConfigResult.timeoutMs,
|
|
146
|
+
baseUrl: runtimeConfigResult.baseUrl,
|
|
147
|
+
workspace: workspaceDir,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const validator = new DefaultDreamerValidator();
|
|
151
|
+
const runner = new DreamerRunner(
|
|
152
|
+
{
|
|
153
|
+
stateManager,
|
|
154
|
+
runtimeAdapter: adapter,
|
|
155
|
+
eventEmitter: storeEmitter,
|
|
156
|
+
artifactStore: stateManager.piArtifactStore,
|
|
157
|
+
validator,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
owner: 'auto-consumer',
|
|
161
|
+
runtimeKind: 'config',
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const taskId = wakeResult.taskId;
|
|
166
|
+
logger.info(`[PD:AutoConsumer] Running dreamer task: ${taskId}`);
|
|
167
|
+
SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_RUN', JSON.stringify({
|
|
168
|
+
taskId,
|
|
169
|
+
taskKind: 'dreamer',
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
const runResult = await runner.run(taskId);
|
|
173
|
+
|
|
174
|
+
if (runResult.status === 'succeeded') {
|
|
175
|
+
const commitResult = await orchestrator.commitNextTaskProposal(taskId);
|
|
176
|
+
SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_SUCCESS', JSON.stringify({
|
|
177
|
+
taskId,
|
|
178
|
+
status: runResult.status,
|
|
179
|
+
successorDecision: commitResult.decision,
|
|
180
|
+
}));
|
|
181
|
+
logger.info(
|
|
182
|
+
`[PD:AutoConsumer] Task ${taskId} succeeded. Successor: ${commitResult.decision}`,
|
|
183
|
+
);
|
|
184
|
+
} else {
|
|
185
|
+
SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_TASK_FAILED', JSON.stringify({
|
|
186
|
+
taskId,
|
|
187
|
+
status: runResult.status,
|
|
188
|
+
}));
|
|
189
|
+
logger.warn(`[PD:AutoConsumer] Task ${taskId} status: ${runResult.status}`);
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_ERROR', String(err));
|
|
193
|
+
logger.error(`[PD:AutoConsumer] Cycle error: ${String(err)}`);
|
|
194
|
+
} finally {
|
|
195
|
+
if (handle) {
|
|
196
|
+
await handle.close().catch(() => {});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const InternalizationAutoConsumerService: InternalizationAutoConsumerServiceShape = {
|
|
202
|
+
id: 'principles-internalization-auto-consumer',
|
|
203
|
+
|
|
204
|
+
start(ctx: OpenClawPluginServiceContext): void {
|
|
205
|
+
const maybeWorkspaceDir = ctx?.workspaceDir;
|
|
206
|
+
const logger = ctx?.logger || console;
|
|
207
|
+
|
|
208
|
+
if (!maybeWorkspaceDir) {
|
|
209
|
+
logger.warn('[PD:AutoConsumer] No workspace directory, not starting.');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const workspaceDir: string = maybeWorkspaceDir;
|
|
214
|
+
const state = getWorkspaceState(workspaceDir);
|
|
215
|
+
|
|
216
|
+
if (!state.stopped && state.timeoutId !== null) {
|
|
217
|
+
logger.info(`[PD:AutoConsumer] Already started for workspace: ${workspaceDir}`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const flag = loadFeatureFlagFromConfig(workspaceDir, INTERNALIZATION_AUTO_CONSUMER_FLAG_ID, {
|
|
222
|
+
info: (msg: string) => logger.info(msg),
|
|
223
|
+
warn: (msg: string) => logger.warn(msg),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (!flag.enabled) {
|
|
227
|
+
const disabledInfo = JSON.stringify({
|
|
228
|
+
reason: 'internalization_auto_consumer_disabled',
|
|
229
|
+
nextAction: formatRunOnceCommand(workspaceDir),
|
|
230
|
+
flagSource: flag.source,
|
|
231
|
+
});
|
|
232
|
+
SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_DISABLED', disabledInfo);
|
|
233
|
+
logger.info(
|
|
234
|
+
`[PD:AutoConsumer] NOT started for workspace: ${workspaceDir}. Disabled (source: ${flag.source}).`,
|
|
235
|
+
);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
state.stopped = false;
|
|
240
|
+
|
|
241
|
+
const interval = INTERNALIZATION_AUTO_CONSUMER_INTERVAL_MS;
|
|
242
|
+
|
|
243
|
+
function scheduleNext(): void {
|
|
244
|
+
if (state.stopped) return;
|
|
245
|
+
state.timeoutId = setTimeout(runCycle, interval);
|
|
246
|
+
state.timeoutId?.unref();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function runCycle(): Promise<void> {
|
|
250
|
+
if (state.stopped) return;
|
|
251
|
+
await runConsumerCycle(workspaceDir, logger);
|
|
252
|
+
scheduleNext();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
state.timeoutId = setTimeout(() => {
|
|
256
|
+
void runCycle().catch((err: unknown) => {
|
|
257
|
+
logger.error(`[PD:AutoConsumer] Startup cycle failed: ${String(err)}`);
|
|
258
|
+
if (state.stopped) return;
|
|
259
|
+
state.timeoutId = setTimeout(runCycle, interval);
|
|
260
|
+
state.timeoutId?.unref();
|
|
261
|
+
});
|
|
262
|
+
}, INTERNALIZATION_AUTO_CONSUMER_INITIAL_DELAY_MS);
|
|
263
|
+
state.timeoutId?.unref();
|
|
264
|
+
|
|
265
|
+
SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_STARTED', JSON.stringify({
|
|
266
|
+
intervalMs: interval,
|
|
267
|
+
initialDelayMs: INTERNALIZATION_AUTO_CONSUMER_INITIAL_DELAY_MS,
|
|
268
|
+
}));
|
|
269
|
+
logger.info(
|
|
270
|
+
`[PD:AutoConsumer] Started for workspace: ${workspaceDir} (interval: ${interval}ms, initial delay: ${INTERNALIZATION_AUTO_CONSUMER_INITIAL_DELAY_MS}ms)`,
|
|
271
|
+
);
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
stop(ctx: OpenClawPluginServiceContext): void {
|
|
275
|
+
const workspaceDir = ctx?.workspaceDir;
|
|
276
|
+
if (workspaceDir) {
|
|
277
|
+
const state = workspaceStates.get(workspaceDir);
|
|
278
|
+
if (state) {
|
|
279
|
+
state.stopped = true;
|
|
280
|
+
if (state.timeoutId) clearTimeout(state.timeoutId);
|
|
281
|
+
state.timeoutId = null;
|
|
282
|
+
}
|
|
283
|
+
workspaceStates.delete(workspaceDir);
|
|
284
|
+
} else {
|
|
285
|
+
for (const [, state] of workspaceStates) {
|
|
286
|
+
state.stopped = true;
|
|
287
|
+
if (state.timeoutId) clearTimeout(state.timeoutId);
|
|
288
|
+
state.timeoutId = null;
|
|
289
|
+
}
|
|
290
|
+
workspaceStates.clear();
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
};
|
|
@@ -174,6 +174,8 @@ describe('PRI-294: Surface registry coverage audit', () => {
|
|
|
174
174
|
'startup:workspace-init',
|
|
175
175
|
'startup:evolution-worker',
|
|
176
176
|
'startup:correction-observer',
|
|
177
|
+
'service:internalization-auto-consumer', // PRI-381: bounded auto-consumer
|
|
178
|
+
'startup:internalization-auto-consumer', // PRI-381: auto-consumer startup
|
|
177
179
|
];
|
|
178
180
|
const allowedIds = new Set([...usedSet, ...additionallyRegistered]);
|
|
179
181
|
const unaccounted = PLUGIN_SURFACE_REGISTRY
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as yaml from 'js-yaml';
|
|
6
|
+
|
|
7
|
+
vi.mock('../src/core/dictionary-service.js', () => ({
|
|
8
|
+
DictionaryService: { get: vi.fn(() => ({ flush: vi.fn() })) },
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('../src/core/session-tracker.js', () => ({
|
|
12
|
+
initPersistence: vi.fn(),
|
|
13
|
+
flushAllSessions: vi.fn(),
|
|
14
|
+
listSessions: vi.fn(() => []),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('../src/core/workspace-context.js', () => {
|
|
18
|
+
const mockCtx = {
|
|
19
|
+
stateDir: '',
|
|
20
|
+
workspaceDir: '',
|
|
21
|
+
config: { get: vi.fn() },
|
|
22
|
+
eventLog: { recordHookExecution: vi.fn() },
|
|
23
|
+
dictionary: { flush: vi.fn() },
|
|
24
|
+
resolve: vi.fn((key: string) => `/mock/${key}`),
|
|
25
|
+
trajectory: null,
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
WorkspaceContext: {
|
|
29
|
+
fromHookContext: vi.fn(() => mockCtx),
|
|
30
|
+
clearCache: vi.fn(),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
shouldStartInternalizationAutoConsumer,
|
|
37
|
+
loadFeatureFlagFromWorkspace,
|
|
38
|
+
} from '../src/index.js';
|
|
39
|
+
|
|
40
|
+
function createTempWorkspace(): string {
|
|
41
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-auto-consumer-'));
|
|
42
|
+
fs.mkdirSync(path.join(dir, '.pd'), { recursive: true });
|
|
43
|
+
fs.mkdirSync(path.join(dir, '.state'), { recursive: true });
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeConfigYaml(workspaceDir: string, featureOverrides: Record<string, unknown>): void {
|
|
48
|
+
const configPath = path.join(workspaceDir, '.pd', 'config.yaml');
|
|
49
|
+
const defaultFeatures: Record<string, unknown> = {
|
|
50
|
+
prompt: { category: 'core', enabled: true },
|
|
51
|
+
code_tool_hook: { category: 'core', enabled: true },
|
|
52
|
+
defer_archive: { category: 'core', enabled: true },
|
|
53
|
+
correction_observer: { category: 'quiet', enabled: false },
|
|
54
|
+
empathy_observer: { category: 'quiet', enabled: false },
|
|
55
|
+
evolution_worker: { category: 'quiet', enabled: false },
|
|
56
|
+
internalization_auto_consumer: { category: 'quiet', enabled: true },
|
|
57
|
+
nocturnal: { category: 'gone', enabled: false },
|
|
58
|
+
};
|
|
59
|
+
const config = {
|
|
60
|
+
version: 1,
|
|
61
|
+
features: Object.assign({}, defaultFeatures, featureOverrides),
|
|
62
|
+
runtimeProfiles: {
|
|
63
|
+
'openclaw.default': { type: 'openclaw', source: 'default' },
|
|
64
|
+
},
|
|
65
|
+
internalAgents: {
|
|
66
|
+
defaultRuntime: 'openclaw.default',
|
|
67
|
+
agents: {
|
|
68
|
+
diagnostician: { enabled: true },
|
|
69
|
+
dreamer: { enabled: true },
|
|
70
|
+
scribe: { enabled: true },
|
|
71
|
+
artificer: { enabled: true },
|
|
72
|
+
philosopher: { enabled: false },
|
|
73
|
+
evaluator: { enabled: false },
|
|
74
|
+
rolloutReviewer: { enabled: false },
|
|
75
|
+
trainer: { enabled: false },
|
|
76
|
+
correctionObserver: { enabled: false },
|
|
77
|
+
empathyObserver: { enabled: false },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const content = yaml.dump(config, { schema: yaml.JSON_SCHEMA });
|
|
82
|
+
fs.writeFileSync(configPath, content, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function createMockLogger() {
|
|
86
|
+
return {
|
|
87
|
+
info: vi.fn(),
|
|
88
|
+
warn: vi.fn(),
|
|
89
|
+
error: vi.fn(),
|
|
90
|
+
debug: vi.fn(),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('PRI-381: InternalizationAutoConsumer gate', () => {
|
|
95
|
+
let workspaceDir: string;
|
|
96
|
+
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
workspaceDir = createTempWorkspace();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
try {
|
|
103
|
+
fs.rmSync(workspaceDir, { recursive: true, force: true });
|
|
104
|
+
} catch { /* best-effort */ }
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('shouldStartInternalizationAutoConsumer', () => {
|
|
108
|
+
it('returns shouldStart=true with defaults (quiet flag — default enabled)', () => {
|
|
109
|
+
const logger = createMockLogger();
|
|
110
|
+
const result = shouldStartInternalizationAutoConsumer(workspaceDir, logger);
|
|
111
|
+
expect(result.shouldStart).toBe(true);
|
|
112
|
+
expect(result.flagSource).toBeDefined();
|
|
113
|
+
expect(result.disabledInfo).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns shouldStart=false when config disables the flag, with reason and nextAction', () => {
|
|
117
|
+
writeConfigYaml(workspaceDir, {
|
|
118
|
+
internalization_auto_consumer: { category: 'quiet', enabled: false },
|
|
119
|
+
});
|
|
120
|
+
const logger = createMockLogger();
|
|
121
|
+
const result = shouldStartInternalizationAutoConsumer(workspaceDir, logger);
|
|
122
|
+
|
|
123
|
+
expect(result.shouldStart).toBe(false);
|
|
124
|
+
expect(result.disabledInfo).not.toBeNull();
|
|
125
|
+
const info = JSON.parse(result.disabledInfo ?? '{}');
|
|
126
|
+
expect(info.reason).toBe('internalization_auto_consumer_disabled');
|
|
127
|
+
expect(info.nextAction).toContain('pd runtime internalization run-once');
|
|
128
|
+
expect(info.flagSource).toBeDefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('returns shouldStart=true with explicit config enabling', () => {
|
|
132
|
+
writeConfigYaml(workspaceDir, {
|
|
133
|
+
internalization_auto_consumer: { category: 'quiet', enabled: true },
|
|
134
|
+
});
|
|
135
|
+
const logger = createMockLogger();
|
|
136
|
+
const result = shouldStartInternalizationAutoConsumer(workspaceDir, logger);
|
|
137
|
+
expect(result.shouldStart).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('loadFeatureFlagFromWorkspace for auto-consumer', () => {
|
|
142
|
+
it('returns enabled=true when no config.yaml exists (default)', () => {
|
|
143
|
+
const logger = createMockLogger();
|
|
144
|
+
const result = loadFeatureFlagFromWorkspace(workspaceDir, 'internalization_auto_consumer', logger);
|
|
145
|
+
expect(result.enabled).toBe(true);
|
|
146
|
+
expect(result.source).toBe('defaults');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns enabled=false when config disables the flag (quiet flag can be disabled)', () => {
|
|
150
|
+
writeConfigYaml(workspaceDir, {
|
|
151
|
+
internalization_auto_consumer: { category: 'quiet', enabled: false },
|
|
152
|
+
});
|
|
153
|
+
const logger = createMockLogger();
|
|
154
|
+
const result = loadFeatureFlagFromWorkspace(workspaceDir, 'internalization_auto_consumer', logger);
|
|
155
|
+
expect(result.enabled).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|