principles-disciple 1.16.0 → 1.17.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/README.md +13 -5
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/commands/archive-impl.ts +3 -3
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +3 -3
- package/src/commands/disable-impl.ts +1 -1
- package/src/commands/evolution-status.ts +2 -2
- package/src/commands/focus.ts +2 -2
- package/src/commands/nocturnal-train.ts +6 -6
- package/src/commands/pain.ts +4 -4
- package/src/commands/pd-reflect.ts +87 -0
- package/src/commands/rollback-impl.ts +4 -4
- package/src/commands/rollback.ts +2 -2
- package/src/commands/samples.ts +2 -2
- package/src/commands/workflow-debug.ts +1 -1
- package/src/config/errors.ts +1 -1
- package/src/core/adaptive-thresholds.ts +1 -1
- package/src/core/code-implementation-storage.ts +2 -2
- package/src/core/config.ts +1 -1
- package/src/core/diagnostician-task-store.ts +2 -2
- package/src/core/empathy-keyword-matcher.ts +3 -3
- package/src/core/event-log.ts +5 -5
- package/src/core/evolution-engine.ts +4 -4
- package/src/core/evolution-logger.ts +1 -1
- package/src/core/evolution-reducer.ts +3 -3
- package/src/core/evolution-types.ts +5 -5
- package/src/core/external-training-contract.ts +1 -1
- package/src/core/focus-history.ts +14 -14
- package/src/core/hygiene/tracker.ts +1 -1
- package/src/core/init.ts +2 -2
- package/src/core/model-deployment-registry.ts +2 -2
- package/src/core/model-training-registry.ts +2 -2
- package/src/core/nocturnal-arbiter.ts +1 -1
- package/src/core/nocturnal-artificer.ts +2 -2
- package/src/core/nocturnal-candidate-scoring.ts +2 -2
- package/src/core/nocturnal-compliance.ts +3 -3
- package/src/core/nocturnal-dataset.ts +3 -3
- package/src/core/nocturnal-export.ts +4 -4
- package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
- package/src/core/nocturnal-snapshot-contract.ts +112 -0
- package/src/core/nocturnal-trajectory-extractor.ts +7 -5
- package/src/core/nocturnal-trinity.ts +27 -28
- package/src/core/pain-context-extractor.ts +3 -3
- package/src/core/pain.ts +124 -11
- package/src/core/path-resolver.ts +4 -4
- package/src/core/pd-task-reconciler.ts +10 -10
- package/src/core/pd-task-service.ts +1 -1
- package/src/core/pd-task-store.ts +1 -1
- package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
- package/src/core/principle-training-state.ts +2 -2
- package/src/core/principle-tree-ledger.ts +7 -7
- package/src/core/promotion-gate.ts +9 -9
- package/src/core/replay-engine.ts +12 -12
- package/src/core/risk-calculator.ts +1 -1
- package/src/core/rule-host-types.ts +2 -2
- package/src/core/rule-host.ts +5 -5
- package/src/core/schema/db-types.ts +1 -1
- package/src/core/schema/schema-definitions.ts +1 -1
- package/src/core/session-tracker.ts +96 -4
- package/src/core/shadow-observation-registry.ts +3 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-os-parser.ts +1 -1
- package/src/core/training-program.ts +2 -2
- package/src/core/trajectory.ts +8 -8
- package/src/core/workspace-context.ts +2 -2
- package/src/core/workspace-dir-service.ts +85 -0
- package/src/core/workspace-dir-validation.ts +30 -107
- package/src/hooks/bash-risk.ts +3 -3
- package/src/hooks/edit-verification.ts +4 -4
- package/src/hooks/gate-block-helper.ts +4 -4
- package/src/hooks/gate.ts +10 -10
- package/src/hooks/gfi-gate.ts +7 -7
- package/src/hooks/lifecycle.ts +2 -2
- package/src/hooks/llm.ts +1 -1
- package/src/hooks/pain.ts +25 -5
- package/src/hooks/progressive-trust-gate.ts +7 -7
- package/src/hooks/prompt.ts +24 -5
- package/src/hooks/subagent.ts +2 -2
- package/src/hooks/thinking-checkpoint.ts +2 -2
- package/src/hooks/trajectory-collector.ts +1 -1
- package/src/http/principles-console-route.ts +14 -6
- package/src/i18n/commands.ts +4 -0
- package/src/index.ts +181 -185
- package/src/service/central-health-service.ts +1 -1
- package/src/service/central-overview-service.ts +3 -3
- package/src/service/evolution-query-service.ts +1 -1
- package/src/service/evolution-worker.ts +209 -104
- package/src/service/health-query-service.ts +27 -17
- package/src/service/monitoring-query-service.ts +3 -3
- package/src/service/nocturnal-runtime.ts +4 -4
- package/src/service/nocturnal-service.ts +40 -23
- package/src/service/nocturnal-target-selector.ts +2 -2
- package/src/service/runtime-summary-service.ts +1 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
- package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
- package/src/service/subagent-workflow/types.ts +4 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
- package/src/service/subagent-workflow/workflow-store.ts +2 -2
- package/src/tools/critique-prompt.ts +2 -3
- package/src/tools/deep-reflect.ts +17 -16
- package/src/tools/model-index.ts +1 -1
- package/src/utils/file-lock.ts +1 -1
- package/src/utils/io.ts +7 -2
- package/src/utils/nlp.ts +1 -1
- package/src/utils/plugin-logger.ts +2 -2
- package/src/utils/retry.ts +3 -2
- package/src/utils/subagent-probe.ts +20 -33
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/pain_settings.json +1 -1
- package/tests/build-artifacts.test.ts +4 -58
- package/tests/commands/pd-reflect.test.ts +49 -0
- package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
- package/tests/core/pain-auto-repair.test.ts +96 -0
- package/tests/core/pain-integration.test.ts +483 -0
- package/tests/core/pain.test.ts +5 -4
- package/tests/core/workspace-dir-service.test.ts +68 -0
- package/tests/core/workspace-dir-validation.test.ts +56 -192
- package/tests/hooks/pain.test.ts +20 -0
- package/tests/http/principles-console-route.test.ts +42 -20
- package/tests/integration/empathy-workflow-integration.test.ts +1 -2
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
- package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
- package/tests/service/evolution-worker.nocturnal.test.ts +562 -6
- package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
- package/tests/utils/subagent-probe.test.ts +32 -0
|
@@ -232,7 +232,7 @@ export class WorkflowStore {
|
|
|
232
232
|
`).all() as WorkflowRow[];
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
|
|
235
|
+
|
|
236
236
|
recordEvent(
|
|
237
237
|
workflowId: string,
|
|
238
238
|
eventType: string,
|
|
@@ -268,7 +268,7 @@ export class WorkflowStore {
|
|
|
268
268
|
* idempotencyKey must be unique per (workflow_id, stage). If a row with the
|
|
269
269
|
* same idempotency_key already exists, this is a no-op (idempotent).
|
|
270
270
|
*/
|
|
271
|
-
|
|
271
|
+
|
|
272
272
|
recordStageOutput(
|
|
273
273
|
workflowId: string,
|
|
274
274
|
stage: 'dreamer' | 'philosopher',
|
|
@@ -28,11 +28,10 @@ export function buildCritiquePromptV2(
|
|
|
28
28
|
): string {
|
|
29
29
|
const { context, depth = 2, workspaceDir, api } = params;
|
|
30
30
|
|
|
31
|
-
// 1. 确定工作区目录 (优先级:显式传入 > api.config > official API
|
|
31
|
+
// 1. 确定工作区目录 (优先级:显式传入 > api.config > official API)
|
|
32
32
|
const effectiveWorkspaceDir = workspaceDir
|
|
33
33
|
|| (api?.config?.workspaceDir as string)
|
|
34
|
-
|| resolveWorkspaceDirFromApi(api)
|
|
35
|
-
|| api?.resolvePath?.('.');
|
|
34
|
+
|| resolveWorkspaceDirFromApi(api);
|
|
36
35
|
|
|
37
36
|
if (!effectiveWorkspaceDir) {
|
|
38
37
|
throw new Error('Workspace directory is required for deep reflection.');
|
|
@@ -4,6 +4,7 @@ import * as fs from 'fs';
|
|
|
4
4
|
import { EventLogService } from '../core/event-log.js';
|
|
5
5
|
import { resolvePdPath } from '../core/paths.js';
|
|
6
6
|
import { resolveWorkspaceDirFromApi } from '../core/path-resolver.js';
|
|
7
|
+
import { WorkspaceNotFoundError } from '../config/index.js';
|
|
7
8
|
import {
|
|
8
9
|
DeepReflectWorkflowManager,
|
|
9
10
|
deepReflectWorkflowSpec,
|
|
@@ -106,11 +107,8 @@ export function createDeepReflectTool(api: OpenClawPluginApi) {
|
|
|
106
107
|
return { content: [{ type: 'text', text: '❌ 错误: 必须提供反思上下文 (context)。' }] };
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
|
|
110
|
+
|
|
110
111
|
const effectiveWorkspaceDir = resolveReflectionWorkspace(api);
|
|
111
|
-
if (!effectiveWorkspaceDir) {
|
|
112
|
-
return { content: [{ type: 'text', text: '❌ 反思执行失败: Workspace directory is required for deep reflection。请检查 API 配置或网络连接。' }] };
|
|
113
|
-
}
|
|
114
112
|
|
|
115
113
|
const config = loadConfig(effectiveWorkspaceDir, api);
|
|
116
114
|
if (config.mode === 'disabled' || !config.enabled) {
|
|
@@ -122,10 +120,10 @@ export function createDeepReflectTool(api: OpenClawPluginApi) {
|
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
try {
|
|
125
|
-
|
|
123
|
+
|
|
126
124
|
return await executeReflectionWorkflow(effectiveWorkspaceDir, config, context, depth, model_id, api);
|
|
127
125
|
} catch (err) {
|
|
128
|
-
|
|
126
|
+
|
|
129
127
|
return handleReflectionError(err, context, depth, model_id, effectiveWorkspaceDir, api);
|
|
130
128
|
}
|
|
131
129
|
}
|
|
@@ -135,16 +133,19 @@ export function createDeepReflectTool(api: OpenClawPluginApi) {
|
|
|
135
133
|
/**
|
|
136
134
|
* Resolve workspace directory for deep reflection tool.
|
|
137
135
|
*/
|
|
138
|
-
function resolveReflectionWorkspace(api: OpenClawPluginApi): string
|
|
139
|
-
|
|
140
|
-
|| resolveWorkspaceDirFromApi(api)
|
|
141
|
-
|
|
136
|
+
function resolveReflectionWorkspace(api: OpenClawPluginApi): string {
|
|
137
|
+
const dir = (api.config?.workspaceDir as string)
|
|
138
|
+
|| resolveWorkspaceDirFromApi(api);
|
|
139
|
+
if (!dir) {
|
|
140
|
+
throw new WorkspaceNotFoundError('deep-reflect: workspace directory could not be resolved via API or config');
|
|
141
|
+
}
|
|
142
|
+
return dir;
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
/**
|
|
145
146
|
* Execute the deep reflection workflow: start, poll, collect results.
|
|
146
147
|
*/
|
|
147
|
-
|
|
148
|
+
|
|
148
149
|
async function executeReflectionWorkflow(
|
|
149
150
|
effectiveWorkspaceDir: string,
|
|
150
151
|
config: DeepReflectionConfig,
|
|
@@ -175,7 +176,7 @@ async function executeReflectionWorkflow(
|
|
|
175
176
|
|
|
176
177
|
const startTime = Date.now();
|
|
177
178
|
const timeoutMs = config.timeout_ms ?? 60000;
|
|
178
|
-
|
|
179
|
+
|
|
179
180
|
return await pollReflectionCompletion(manager, handle, timeoutMs, startTime, eventLog, effectiveWorkspaceDir, context, model_id, depth);
|
|
180
181
|
} finally {
|
|
181
182
|
manager.dispose();
|
|
@@ -185,7 +186,7 @@ async function executeReflectionWorkflow(
|
|
|
185
186
|
/**
|
|
186
187
|
* Poll the reflection workflow until completion, timeout, or error.
|
|
187
188
|
*/
|
|
188
|
-
|
|
189
|
+
|
|
189
190
|
async function pollReflectionCompletion(
|
|
190
191
|
manager: DeepReflectWorkflowManager,
|
|
191
192
|
handle: { workflowId: string; childSessionKey: string },
|
|
@@ -205,7 +206,7 @@ async function pollReflectionCompletion(
|
|
|
205
206
|
if (!workflowState) break;
|
|
206
207
|
|
|
207
208
|
if (workflowState === 'completed') {
|
|
208
|
-
|
|
209
|
+
|
|
209
210
|
return formatReflectionSuccess(handle, context, depth, model_id, startTime, eventLog, workspaceDir);
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -220,7 +221,7 @@ async function pollReflectionCompletion(
|
|
|
220
221
|
/**
|
|
221
222
|
* Format the success response from a completed reflection.
|
|
222
223
|
*/
|
|
223
|
-
|
|
224
|
+
|
|
224
225
|
function formatReflectionSuccess(
|
|
225
226
|
handle: { childSessionKey: string },
|
|
226
227
|
context: string,
|
|
@@ -273,7 +274,7 @@ ${insights || '反思完成,详见 REFLECTION_LOG。'}
|
|
|
273
274
|
/**
|
|
274
275
|
* Handle reflection errors and format error response.
|
|
275
276
|
*/
|
|
276
|
-
|
|
277
|
+
|
|
277
278
|
function handleReflectionError(
|
|
278
279
|
err: unknown,
|
|
279
280
|
context: string,
|
package/src/tools/model-index.ts
CHANGED
|
@@ -61,7 +61,7 @@ export function loadModelIndex(
|
|
|
61
61
|
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
62
62
|
const customConfig = loadCustomConfig(wctx);
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
let modelsDir: string;
|
|
66
66
|
if (customConfig?.modelsDir) {
|
|
67
67
|
modelsDir = path.isAbsolute(customConfig.modelsDir)
|
package/src/utils/file-lock.ts
CHANGED
|
@@ -334,7 +334,7 @@ export async function withAsyncLock<T>(
|
|
|
334
334
|
let queue = asyncLockQueues.get(lockPath);
|
|
335
335
|
|
|
336
336
|
// 创建新的 Promise 链
|
|
337
|
-
|
|
337
|
+
|
|
338
338
|
let resolveRelease: () => void;
|
|
339
339
|
const releasePromise = new Promise<void>(resolve => {
|
|
340
340
|
resolveRelease = resolve;
|
package/src/utils/io.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function normalizePath(filePath: string, projectDir: string): string {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
let rel: string;
|
|
21
21
|
if (projectIsWin) {
|
|
22
22
|
const projectAbs = path.resolve(projectDir);
|
|
@@ -80,6 +80,11 @@ export function serializeKvLines(data: Record<string, any>): string {
|
|
|
80
80
|
const keys = Object.keys(data).sort();
|
|
81
81
|
for (const k of keys) {
|
|
82
82
|
const v = data[k];
|
|
83
|
+
// Skip empty/undefined values — prevents writing blank lines that cause
|
|
84
|
+
// agent confusion (SKILL.md lists fields that aren't actually present on disk)
|
|
85
|
+
if (v === '' || v === undefined || v === null) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
83
88
|
if (Array.isArray(v)) {
|
|
84
89
|
lines.push(`${k}: ${v.join(',')}`);
|
|
85
90
|
} else if (typeof v === 'object' && v !== null) {
|
|
@@ -105,7 +110,7 @@ export function planStatus(projectDir: string): string {
|
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
|
-
/* eslint-disable @typescript-eslint/no-unused-vars
|
|
113
|
+
/* eslint-disable @typescript-eslint/no-unused-vars -- Reason: Error is intentionally ignored for graceful degradation */
|
|
109
114
|
} catch (_e) {
|
|
110
115
|
// Ignore read errors
|
|
111
116
|
}
|
package/src/utils/nlp.ts
CHANGED
|
@@ -19,7 +19,7 @@ export function extractCommonPhrases(samples: string[], minOccurrence = 3): stri
|
|
|
19
19
|
|
|
20
20
|
// Filter phrases that meet the threshold
|
|
21
21
|
return Array.from(phrases.entries())
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
.filter(([_elem, count]) => count >= minOccurrence)
|
|
24
24
|
.map(([phrase, _count]) => phrase);
|
|
25
25
|
}
|
|
@@ -2,12 +2,12 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
|
|
4
4
|
export interface PluginLogger {
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
info(message: string, meta?: Record<string, unknown>): void;
|
|
7
7
|
warn(message: string, meta?: Record<string, unknown>): void;
|
|
8
8
|
error(message: string, meta?: Record<string, unknown>): void;
|
|
9
9
|
debug(message: string, meta?: Record<string, unknown>): void;
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface PluginLoggerConfig {
|
package/src/utils/retry.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface RetryOptions {
|
|
|
34
34
|
operation?: string;
|
|
35
35
|
/** Logger instance (optional, defaults to console) */
|
|
36
36
|
logger?: RetryLogger;
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
isRetryable?: (_error: unknown) => boolean;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -424,7 +424,7 @@ export function computeDynamicTimeout(
|
|
|
424
424
|
if (history.length < MIN_SAMPLES) {
|
|
425
425
|
// Not enough data — use the spec's static timeout
|
|
426
426
|
const fallback = clampTimeout(defaultTimeout);
|
|
427
|
-
//
|
|
427
|
+
// eslint-disable-next-line no-console -- Monitoring output for fallback diagnostics
|
|
428
428
|
console.info(`[PD:DynamicTimeout] Insufficient samples (${history.length} < ${MIN_SAMPLES}) for '${workflowType}', falling back to static timeout: ${fallback}ms`);
|
|
429
429
|
return fallback;
|
|
430
430
|
}
|
|
@@ -432,6 +432,7 @@ export function computeDynamicTimeout(
|
|
|
432
432
|
const p95 = percentile(history, 95);
|
|
433
433
|
const adaptive = p95 * SAFETY_MULTIPLIER;
|
|
434
434
|
const result = clampTimeout(adaptive);
|
|
435
|
+
// eslint-disable-next-line no-console -- Monitoring output for adaptive timeout diagnostics
|
|
435
436
|
console.info(`[PD:DynamicTimeout] Computed adaptive timeout for '${workflowType}': P95=${p95}ms (from ${history.length} samples) × ${SAFETY_MULTIPLIER} = ${result}ms`);
|
|
436
437
|
return result;
|
|
437
438
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subagent Runtime Availability Probe
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* This utility provides a reliable way to detect which mode we're in.
|
|
4
|
+
* This utility intentionally avoids inferring runtime availability from
|
|
5
|
+
* JavaScript implementation details like constructor names. The only contract
|
|
6
|
+
* we trust here is whether a callable `run` entrypoint exists.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
import type { OpenClawPluginApi } from '../openclaw-sdk.js';
|
|
@@ -29,46 +27,35 @@ function getGlobalGatewaySubagent(): SubagentRuntime | null {
|
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* In embedded mode, subagent.run is a regular Function that throws synchronously.
|
|
36
|
-
*
|
|
37
|
-
* We use constructor check first because it's fast and has no side effects.
|
|
30
|
+
* Return a small, explicit availability assessment for the subagent runtime.
|
|
31
|
+
* This is a shape check only. Actual invocation failures are classified by the
|
|
32
|
+
* caller as runtime-unavailable vs downstream task failures.
|
|
38
33
|
*
|
|
39
34
|
* @param subagent - The subagent runtime object from api.runtime.subagent
|
|
40
|
-
* @returns
|
|
35
|
+
* @returns availability status and reason
|
|
41
36
|
*/
|
|
42
|
-
export function
|
|
37
|
+
export function getSubagentRuntimeAvailability(
|
|
43
38
|
subagent: { run?: unknown } | undefined
|
|
44
|
-
): boolean {
|
|
45
|
-
if (!subagent) return false;
|
|
39
|
+
): { available: boolean; reason: 'missing_runtime' | 'missing_run' | 'callable' } {
|
|
40
|
+
if (!subagent) return { available: false, reason: 'missing_runtime' };
|
|
46
41
|
|
|
47
42
|
try {
|
|
48
43
|
const runFn = subagent.run;
|
|
49
|
-
if (typeof runFn !== 'function')
|
|
50
|
-
|
|
51
|
-
// In gateway mode, methods are AsyncFunction instances
|
|
52
|
-
// In embedded mode, methods are regular Function instances that throw
|
|
53
|
-
const isAsync = runFn.constructor?.name === 'AsyncFunction';
|
|
54
|
-
|
|
55
|
-
if (isAsync) return true;
|
|
56
|
-
|
|
57
|
-
// Fallback: Check if it's a Proxy that might late-bind to the gateway subagent
|
|
58
|
-
// This handles the case where the plugin was loaded with allowGatewaySubagentBinding
|
|
59
|
-
// but the proxy hasn't resolved yet
|
|
60
|
-
const globalGateway = getGlobalGatewaySubagent();
|
|
61
|
-
if (globalGateway && typeof globalGateway.run === 'function') {
|
|
62
|
-
return globalGateway.run.constructor?.name === 'AsyncFunction';
|
|
44
|
+
if (typeof runFn !== 'function') {
|
|
45
|
+
return { available: false, reason: 'missing_run' };
|
|
63
46
|
}
|
|
64
|
-
|
|
65
|
-
return false;
|
|
47
|
+
return { available: true, reason: 'callable' };
|
|
66
48
|
} catch {
|
|
67
|
-
|
|
68
|
-
return false;
|
|
49
|
+
return { available: false, reason: 'missing_run' };
|
|
69
50
|
}
|
|
70
51
|
}
|
|
71
52
|
|
|
53
|
+
export function isSubagentRuntimeAvailable(
|
|
54
|
+
subagent: { run?: unknown } | undefined
|
|
55
|
+
): boolean {
|
|
56
|
+
return getSubagentRuntimeAvailability(subagent).available;
|
|
57
|
+
}
|
|
58
|
+
|
|
72
59
|
/**
|
|
73
60
|
* Get the actual subagent runtime, preferring the global gateway subagent
|
|
74
61
|
* if the passed one is not available.
|
|
@@ -12,7 +12,7 @@ You are now the "Manual Intervention Pain" component.
|
|
|
12
12
|
1. Write the user's feedback `$ARGUMENTS` as a **high-priority** pain signal to `.state/.pain_flag`.
|
|
13
13
|
2. Inform the user that the signal has been injected, and suggest waiting for the next Hook trigger (e.g., Stop or PreCompact) or manually running `/reflection-log`.
|
|
14
14
|
|
|
15
|
-
**Write Format** (must use this KV format,
|
|
15
|
+
**Write Format** (must use this KV format, fields sorted alphabetically):
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
agent_id: <current agent ID, e.g., main/builder/diagnostician>
|
|
@@ -22,16 +22,17 @@ score: 80
|
|
|
22
22
|
session_id: <current session ID>
|
|
23
23
|
source: human_intervention
|
|
24
24
|
time: <ISO 8601 timestamp>
|
|
25
|
-
trace_id:
|
|
26
|
-
trigger_text_preview:
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
**
|
|
27
|
+
**Required fields** (4):
|
|
30
28
|
- `source`: Fixed as `human_intervention`
|
|
31
29
|
- `score`: Default `80` for manual intervention (high priority)
|
|
30
|
+
- `time`: ISO 8601 timestamp
|
|
31
|
+
- `reason`: User's feedback verbatim
|
|
32
|
+
|
|
33
|
+
**Optional fields** (auto-filled by system, but must be provided for manual injection):
|
|
34
|
+
- `agent_id`: Current agent ID (e.g., main/builder/diagnostician)
|
|
32
35
|
- `session_id`: Current session ID (from context)
|
|
33
|
-
- `agent_id`: Current agent ID (from context)
|
|
34
36
|
- `is_risky`: Fixed as `false`
|
|
35
|
-
- `trace_id` / `trigger_text_preview`: Leave empty
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
**Note**: `trace_id` and `trigger_text_preview` are auto-generated by the system — do NOT include them when manually injecting pain signals.
|
|
@@ -12,7 +12,7 @@ disable-model-invocation: true
|
|
|
12
12
|
1. 将用户的反馈 `$ARGUMENTS` 作为一条**高优先级**的痛苦信号,写入 `.state/.pain_flag`。
|
|
13
13
|
2. 告知用户信号已注入,并建议其等待下一个 Hook 触发(如 Stop 或 PreCompact)或手动运行 `/reflection-log`。
|
|
14
14
|
|
|
15
|
-
**写入格式**(必须使用以下 KV
|
|
15
|
+
**写入格式**(必须使用以下 KV 格式,字段按字母排序):
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
agent_id: <当前 agent ID,如 main/builder/diagnostician>
|
|
@@ -22,16 +22,17 @@ score: 80
|
|
|
22
22
|
session_id: <当前 session ID>
|
|
23
23
|
source: human_intervention
|
|
24
24
|
time: <ISO 8601 时间>
|
|
25
|
-
trace_id:
|
|
26
|
-
trigger_text_preview:
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
**必填字段**(4 个):
|
|
30
28
|
- `source`: 固定为 `human_intervention`
|
|
31
29
|
- `score`: 人工干预信号默认设为 `80`(高优先级)
|
|
30
|
+
- `time`: ISO 8601 时间戳
|
|
31
|
+
- `reason`: 用户反馈的原文
|
|
32
|
+
|
|
33
|
+
**可选字段**(自动写入时由系统填充,人工注入时必须填写):
|
|
34
|
+
- `agent_id`: 当前智能体 ID(如 main/builder/diagnostician)
|
|
32
35
|
- `session_id`: 当前会话 ID(从上下文中获取)
|
|
33
|
-
- `agent_id`: 当前智能体 ID(从上下文中获取)
|
|
34
36
|
- `is_risky`: 固定为 `false`
|
|
35
|
-
- `trace_id` / `trigger_text_preview`: 留空即可
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
**注意**: `trace_id` 和 `trigger_text_preview` 由系统自动生成,人工注入时**不需要**写这两个字段。
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect } from 'vitest';
|
|
11
|
-
import { existsSync, readdirSync
|
|
11
|
+
import { existsSync, readdirSync } from 'fs';
|
|
12
12
|
import { join, dirname } from 'path';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
|
|
@@ -35,45 +35,9 @@ describe('Build Artifacts', () => {
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'explorer',
|
|
42
|
-
'auditor',
|
|
43
|
-
'planner',
|
|
44
|
-
'implementer',
|
|
45
|
-
'reviewer',
|
|
46
|
-
'reporter',
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
it('should have agents directory with correct files', () => {
|
|
50
|
-
const agentsDir = join(packageRoot, 'dist/agents');
|
|
51
|
-
|
|
52
|
-
if (!existsSync(agentsDir)) {
|
|
53
|
-
// Skip test if dist/agents doesn't exist (not a production build)
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const files = readdirSync(agentsDir)
|
|
58
|
-
.filter(f => f.endsWith('.md'))
|
|
59
|
-
.map(f => f.replace('.md', ''));
|
|
60
|
-
|
|
61
|
-
// At least 5 agents should be present
|
|
62
|
-
expect(files.length, 'Should have at least 5 agent definitions').toBeGreaterThanOrEqual(5);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
for (const agent of expectedAgents) {
|
|
66
|
-
it(`should include ${agent}.md`, () => {
|
|
67
|
-
const agentPath = join(packageRoot, `dist/agents/${agent}.md`);
|
|
68
|
-
|
|
69
|
-
if (!existsSync(join(packageRoot, 'dist/agents'))) {
|
|
70
|
-
return; // Skip if directory doesn't exist
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
expect(existsSync(agentPath), `${agent}.md should exist in dist/agents`).toBe(true);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
});
|
|
38
|
+
// NOTE: agents/ directory was removed in favor of embedded prompts.
|
|
39
|
+
// All role prompts are now inlined in nocturnal-trinity.ts at build time.
|
|
40
|
+
// See: nocturnal-trinity.ts NOCTURNAL_DREAMER_PROMPT, etc.
|
|
77
41
|
|
|
78
42
|
describe('Templates', () => {
|
|
79
43
|
it('should have templates directory with subdirectories', () => {
|
|
@@ -91,21 +55,3 @@ describe('Build Artifacts', () => {
|
|
|
91
55
|
});
|
|
92
56
|
});
|
|
93
57
|
|
|
94
|
-
describe('Agent Loader Integration', () => {
|
|
95
|
-
it('should be able to import agent-loader from dist', async () => {
|
|
96
|
-
const distPath = join(packageRoot, 'dist/core/agent-loader.js');
|
|
97
|
-
|
|
98
|
-
if (!existsSync(distPath)) {
|
|
99
|
-
return; // Skip if dist not built
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Dynamic import from dist
|
|
103
|
-
const { listAvailableAgents } = await import(join(packageRoot, 'dist/core/agent-loader.js'));
|
|
104
|
-
|
|
105
|
-
const agents = listAvailableAgents();
|
|
106
|
-
|
|
107
|
-
expect(agents.length, 'listAvailableAgents should return at least 5 agents').toBeGreaterThanOrEqual(5);
|
|
108
|
-
expect(agents, 'Should include diagnostician').toContain('diagnostician');
|
|
109
|
-
expect(agents, 'Should include explorer').toContain('explorer');
|
|
110
|
-
});
|
|
111
|
-
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { handlePdReflect } from '../../src/commands/pd-reflect.js';
|
|
6
|
+
|
|
7
|
+
describe('pd-reflect command', () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
let workspaceDir: string;
|
|
10
|
+
let queuePath: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-reflect-'));
|
|
14
|
+
workspaceDir = path.join(tempDir, 'workspace-a');
|
|
15
|
+
fs.mkdirSync(path.join(workspaceDir, '.state'), { recursive: true });
|
|
16
|
+
queuePath = path.join(workspaceDir, '.state', 'evolution_queue.json');
|
|
17
|
+
fs.writeFileSync(queuePath, '[]', 'utf8');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('requires an explicit resolved workspace directory', async () => {
|
|
21
|
+
const result = await handlePdReflect.handler({} as any);
|
|
22
|
+
expect(result.isError).toBe(true);
|
|
23
|
+
expect(result.text).toContain('Cannot determine workspace directory');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('enqueues into the provided active workspace', async () => {
|
|
27
|
+
const result = await handlePdReflect.handler({ workspaceDir } as any);
|
|
28
|
+
expect(result.isError).toBeUndefined();
|
|
29
|
+
|
|
30
|
+
const queue = JSON.parse(fs.readFileSync(queuePath, 'utf8')) as Array<Record<string, unknown>>;
|
|
31
|
+
expect(queue).toHaveLength(1);
|
|
32
|
+
expect(queue[0].taskKind).toBe('sleep_reflection');
|
|
33
|
+
expect(result.text).toContain('Nocturnal reflection task enqueued');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('does not hardcode another workspace when context already provides one', async () => {
|
|
37
|
+
const otherWorkspace = path.join(tempDir, 'workspace-b');
|
|
38
|
+
fs.mkdirSync(path.join(otherWorkspace, '.state'), { recursive: true });
|
|
39
|
+
const otherQueuePath = path.join(otherWorkspace, '.state', 'evolution_queue.json');
|
|
40
|
+
fs.writeFileSync(otherQueuePath, '[]', 'utf8');
|
|
41
|
+
|
|
42
|
+
await handlePdReflect.handler({ workspaceDir } as any);
|
|
43
|
+
|
|
44
|
+
const activeQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8')) as Array<Record<string, unknown>>;
|
|
45
|
+
const otherQueue = JSON.parse(fs.readFileSync(otherQueuePath, 'utf8')) as Array<Record<string, unknown>>;
|
|
46
|
+
expect(activeQueue).toHaveLength(1);
|
|
47
|
+
expect(otherQueue).toHaveLength(0);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { validateNocturnalSnapshotIngress } from '../../src/core/nocturnal-snapshot-contract.js';
|
|
3
|
+
|
|
4
|
+
describe('validateNocturnalSnapshotIngress', () => {
|
|
5
|
+
it('accepts a fully shaped runtime snapshot', () => {
|
|
6
|
+
const result = validateNocturnalSnapshotIngress({
|
|
7
|
+
sessionId: 'session-1',
|
|
8
|
+
startedAt: '2026-04-10T00:00:00.000Z',
|
|
9
|
+
updatedAt: '2026-04-10T00:01:00.000Z',
|
|
10
|
+
assistantTurns: [],
|
|
11
|
+
userTurns: [],
|
|
12
|
+
toolCalls: [],
|
|
13
|
+
painEvents: [],
|
|
14
|
+
gateBlocks: [],
|
|
15
|
+
stats: {
|
|
16
|
+
totalAssistantTurns: 1,
|
|
17
|
+
totalToolCalls: 2,
|
|
18
|
+
totalPainEvents: 0,
|
|
19
|
+
totalGateBlocks: 0,
|
|
20
|
+
failureCount: 0,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(result.status).toBe('valid');
|
|
25
|
+
expect(result.snapshot?.sessionId).toBe('session-1');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('rejects reduced pseudo-snapshots that omit canonical fields', () => {
|
|
29
|
+
const result = validateNocturnalSnapshotIngress({
|
|
30
|
+
sessionId: 'session-1',
|
|
31
|
+
sessionStart: '2026-04-10T00:00:00.000Z',
|
|
32
|
+
stats: {
|
|
33
|
+
totalAssistantTurns: 1,
|
|
34
|
+
totalToolCalls: 2,
|
|
35
|
+
totalPainEvents: 0,
|
|
36
|
+
totalGateBlocks: 0,
|
|
37
|
+
failureCount: 0,
|
|
38
|
+
},
|
|
39
|
+
recentPain: [],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(result.status).toBe('invalid');
|
|
43
|
+
expect(result.reasons).toContain('snapshot.startedAt must be a non-empty string');
|
|
44
|
+
expect(result.reasons).toContain('snapshot.assistantTurns must be an array');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('rejects fallback snapshots with no pain signal', () => {
|
|
48
|
+
const result = validateNocturnalSnapshotIngress({
|
|
49
|
+
sessionId: 'session-1',
|
|
50
|
+
startedAt: '2026-04-10T00:00:00.000Z',
|
|
51
|
+
updatedAt: '2026-04-10T00:00:00.000Z',
|
|
52
|
+
assistantTurns: [],
|
|
53
|
+
userTurns: [],
|
|
54
|
+
toolCalls: [],
|
|
55
|
+
painEvents: [],
|
|
56
|
+
gateBlocks: [],
|
|
57
|
+
stats: {
|
|
58
|
+
totalAssistantTurns: null,
|
|
59
|
+
totalToolCalls: null,
|
|
60
|
+
totalPainEvents: 0,
|
|
61
|
+
totalGateBlocks: null,
|
|
62
|
+
failureCount: null,
|
|
63
|
+
},
|
|
64
|
+
_dataSource: 'pain_context_fallback',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result.status).toBe('invalid');
|
|
68
|
+
expect(result.reasons).toContain('fallback snapshot must contain at least one pain signal');
|
|
69
|
+
});
|
|
70
|
+
});
|