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.
Files changed (129) hide show
  1. package/README.md +13 -5
  2. package/openclaw.plugin.json +4 -4
  3. package/package.json +1 -1
  4. package/src/commands/archive-impl.ts +3 -3
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +3 -3
  7. package/src/commands/disable-impl.ts +1 -1
  8. package/src/commands/evolution-status.ts +2 -2
  9. package/src/commands/focus.ts +2 -2
  10. package/src/commands/nocturnal-train.ts +6 -6
  11. package/src/commands/pain.ts +4 -4
  12. package/src/commands/pd-reflect.ts +87 -0
  13. package/src/commands/rollback-impl.ts +4 -4
  14. package/src/commands/rollback.ts +2 -2
  15. package/src/commands/samples.ts +2 -2
  16. package/src/commands/workflow-debug.ts +1 -1
  17. package/src/config/errors.ts +1 -1
  18. package/src/core/adaptive-thresholds.ts +1 -1
  19. package/src/core/code-implementation-storage.ts +2 -2
  20. package/src/core/config.ts +1 -1
  21. package/src/core/diagnostician-task-store.ts +2 -2
  22. package/src/core/empathy-keyword-matcher.ts +3 -3
  23. package/src/core/event-log.ts +5 -5
  24. package/src/core/evolution-engine.ts +4 -4
  25. package/src/core/evolution-logger.ts +1 -1
  26. package/src/core/evolution-reducer.ts +3 -3
  27. package/src/core/evolution-types.ts +5 -5
  28. package/src/core/external-training-contract.ts +1 -1
  29. package/src/core/focus-history.ts +14 -14
  30. package/src/core/hygiene/tracker.ts +1 -1
  31. package/src/core/init.ts +2 -2
  32. package/src/core/model-deployment-registry.ts +2 -2
  33. package/src/core/model-training-registry.ts +2 -2
  34. package/src/core/nocturnal-arbiter.ts +1 -1
  35. package/src/core/nocturnal-artificer.ts +2 -2
  36. package/src/core/nocturnal-candidate-scoring.ts +2 -2
  37. package/src/core/nocturnal-compliance.ts +3 -3
  38. package/src/core/nocturnal-dataset.ts +3 -3
  39. package/src/core/nocturnal-export.ts +4 -4
  40. package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
  41. package/src/core/nocturnal-snapshot-contract.ts +112 -0
  42. package/src/core/nocturnal-trajectory-extractor.ts +7 -5
  43. package/src/core/nocturnal-trinity.ts +27 -28
  44. package/src/core/pain-context-extractor.ts +3 -3
  45. package/src/core/pain.ts +124 -11
  46. package/src/core/path-resolver.ts +4 -4
  47. package/src/core/pd-task-reconciler.ts +10 -10
  48. package/src/core/pd-task-service.ts +1 -1
  49. package/src/core/pd-task-store.ts +1 -1
  50. package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
  51. package/src/core/principle-training-state.ts +2 -2
  52. package/src/core/principle-tree-ledger.ts +7 -7
  53. package/src/core/promotion-gate.ts +9 -9
  54. package/src/core/replay-engine.ts +12 -12
  55. package/src/core/risk-calculator.ts +1 -1
  56. package/src/core/rule-host-types.ts +2 -2
  57. package/src/core/rule-host.ts +5 -5
  58. package/src/core/schema/db-types.ts +1 -1
  59. package/src/core/schema/schema-definitions.ts +1 -1
  60. package/src/core/session-tracker.ts +96 -4
  61. package/src/core/shadow-observation-registry.ts +3 -3
  62. package/src/core/system-logger.ts +2 -2
  63. package/src/core/thinking-os-parser.ts +1 -1
  64. package/src/core/training-program.ts +2 -2
  65. package/src/core/trajectory.ts +8 -8
  66. package/src/core/workspace-context.ts +2 -2
  67. package/src/core/workspace-dir-service.ts +85 -0
  68. package/src/core/workspace-dir-validation.ts +30 -107
  69. package/src/hooks/bash-risk.ts +3 -3
  70. package/src/hooks/edit-verification.ts +4 -4
  71. package/src/hooks/gate-block-helper.ts +4 -4
  72. package/src/hooks/gate.ts +10 -10
  73. package/src/hooks/gfi-gate.ts +7 -7
  74. package/src/hooks/lifecycle.ts +2 -2
  75. package/src/hooks/llm.ts +1 -1
  76. package/src/hooks/pain.ts +25 -5
  77. package/src/hooks/progressive-trust-gate.ts +7 -7
  78. package/src/hooks/prompt.ts +24 -5
  79. package/src/hooks/subagent.ts +2 -2
  80. package/src/hooks/thinking-checkpoint.ts +2 -2
  81. package/src/hooks/trajectory-collector.ts +1 -1
  82. package/src/http/principles-console-route.ts +14 -6
  83. package/src/i18n/commands.ts +4 -0
  84. package/src/index.ts +181 -185
  85. package/src/service/central-health-service.ts +1 -1
  86. package/src/service/central-overview-service.ts +3 -3
  87. package/src/service/evolution-query-service.ts +1 -1
  88. package/src/service/evolution-worker.ts +209 -104
  89. package/src/service/health-query-service.ts +27 -17
  90. package/src/service/monitoring-query-service.ts +3 -3
  91. package/src/service/nocturnal-runtime.ts +4 -4
  92. package/src/service/nocturnal-service.ts +40 -23
  93. package/src/service/nocturnal-target-selector.ts +2 -2
  94. package/src/service/runtime-summary-service.ts +1 -1
  95. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
  96. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
  97. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
  98. package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
  99. package/src/service/subagent-workflow/types.ts +4 -4
  100. package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
  101. package/src/service/subagent-workflow/workflow-store.ts +2 -2
  102. package/src/tools/critique-prompt.ts +2 -3
  103. package/src/tools/deep-reflect.ts +17 -16
  104. package/src/tools/model-index.ts +1 -1
  105. package/src/utils/file-lock.ts +1 -1
  106. package/src/utils/io.ts +7 -2
  107. package/src/utils/nlp.ts +1 -1
  108. package/src/utils/plugin-logger.ts +2 -2
  109. package/src/utils/retry.ts +3 -2
  110. package/src/utils/subagent-probe.ts +20 -33
  111. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
  112. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
  113. package/templates/pain_settings.json +1 -1
  114. package/tests/build-artifacts.test.ts +4 -58
  115. package/tests/commands/pd-reflect.test.ts +49 -0
  116. package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
  117. package/tests/core/pain-auto-repair.test.ts +96 -0
  118. package/tests/core/pain-integration.test.ts +483 -0
  119. package/tests/core/pain.test.ts +5 -4
  120. package/tests/core/workspace-dir-service.test.ts +68 -0
  121. package/tests/core/workspace-dir-validation.test.ts +56 -192
  122. package/tests/hooks/pain.test.ts +20 -0
  123. package/tests/http/principles-console-route.test.ts +42 -20
  124. package/tests/integration/empathy-workflow-integration.test.ts +1 -2
  125. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
  126. package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
  127. package/tests/service/evolution-worker.nocturnal.test.ts +562 -6
  128. package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
  129. 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
- /* eslint-disable @typescript-eslint/max-params -- Reason: Store interface requires full event context (workflowId, fromState, toState, reason, payload) */
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
- /* eslint-disable @typescript-eslint/max-params -- Reason: Store interface requires workflowId, stage, output, and idempotencyKey */
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 > api.resolvePath)
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
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
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
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
123
+
126
124
  return await executeReflectionWorkflow(effectiveWorkspaceDir, config, context, depth, model_id, api);
127
125
  } catch (err) {
128
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
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 | undefined {
139
- return (api.config?.workspaceDir as string)
140
- || resolveWorkspaceDirFromApi(api)
141
- || api.resolvePath?.('.');
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
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function signature requires all parameters for type-safe reflection workflow */
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
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
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
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function signature requires all parameters for type-safe polling */
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
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
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
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function signature requires all parameters for type-safe formatting */
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
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function signature requires all parameters for type-safe error handling */
277
+
277
278
  function handleReflectionError(
278
279
  err: unknown,
279
280
  context: string,
@@ -61,7 +61,7 @@ export function loadModelIndex(
61
61
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
62
62
  const customConfig = loadCustomConfig(wctx);
63
63
 
64
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
64
+
65
65
  let modelsDir: string;
66
66
  if (customConfig?.modelsDir) {
67
67
  modelsDir = path.isAbsolute(customConfig.modelsDir)
@@ -334,7 +334,7 @@ export async function withAsyncLock<T>(
334
334
  let queue = asyncLockQueues.get(lockPath);
335
335
 
336
336
  // 创建新的 Promise 链
337
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in Promise executor before use
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
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
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, no-unused-vars -- Reason: Error is intentionally ignored for graceful degradation */
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
- /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: Destructuring with _placeholder for unused array element */
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
- /* eslint-disable no-unused-vars -- Reason: interface method params are type signatures, implementations use actual values */
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
- /* eslint-enable no-unused-vars */
10
+
11
11
  }
12
12
 
13
13
  export interface PluginLoggerConfig {
@@ -34,7 +34,7 @@ export interface RetryOptions {
34
34
  operation?: string;
35
35
  /** Logger instance (optional, defaults to console) */
36
36
  logger?: RetryLogger;
37
- /* eslint-disable no-unused-vars -- Reason: callback param names are part of type signature, unused implementations are valid */
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
- // Use console.info since we don't have logger access here; this appears in journalctl
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
- * OpenClaw has two runtime modes:
5
- * - Gateway mode: api.runtime.subagent methods are real async functions
6
- * - Embedded mode: api.runtime.subagent is a Proxy that throws synchronously
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
- * Check if the subagent runtime is actually functional.
33
- *
34
- * In gateway mode, subagent.run is an AsyncFunction (constructor.name === 'AsyncFunction').
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 true if the runtime is functional (gateway mode), false otherwise
35
+ * @returns availability status and reason
41
36
  */
42
- export function isSubagentRuntimeAvailable(
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') return false;
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
- // Any error means unavailable
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, consistent with auto-detection channels):
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
- **Field Notes**:
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
- **⚠️ Important**: Do NOT use other formats (like writing only Source/Reason/Time lines). Downstream diagnostic systems depend on the complete KV field set.
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
- **⚠️ 注意**: 不要使用其他格式(如只写 Source/Reason/Time 三行),下游诊断系统依赖完整的 KV 字段。
38
+ **注意**: `trace_id` `trigger_text_preview` 由系统自动生成,人工注入时**不需要**写这两个字段。
@@ -27,7 +27,7 @@
27
27
  "intervals": {
28
28
  "worker_poll_ms": 900000,
29
29
  "initial_delay_ms": 5000,
30
- "task_timeout_ms": 1800000
30
+ "task_timeout_ms": 3600000
31
31
  },
32
32
  "trust": {
33
33
  "stages": {
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { describe, it, expect } from 'vitest';
11
- import { existsSync, readdirSync, statSync } from 'fs';
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
- describe('Agent definitions', () => {
39
- const expectedAgents = [
40
- 'diagnostician',
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
+ });