principles-disciple 1.5.4 → 1.7.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 (88) hide show
  1. package/dist/commands/context.d.ts +5 -0
  2. package/dist/commands/context.js +312 -0
  3. package/dist/commands/evolution-status.d.ts +4 -0
  4. package/dist/commands/evolution-status.js +138 -0
  5. package/dist/commands/export.d.ts +2 -0
  6. package/dist/commands/export.js +45 -0
  7. package/dist/commands/focus.d.ts +14 -0
  8. package/dist/commands/focus.js +582 -0
  9. package/dist/commands/pain.js +143 -6
  10. package/dist/commands/principle-rollback.d.ts +4 -0
  11. package/dist/commands/principle-rollback.js +22 -0
  12. package/dist/commands/rollback.d.ts +19 -0
  13. package/dist/commands/rollback.js +119 -0
  14. package/dist/commands/samples.d.ts +2 -0
  15. package/dist/commands/samples.js +55 -0
  16. package/dist/core/config.d.ts +37 -0
  17. package/dist/core/config.js +47 -0
  18. package/dist/core/control-ui-db.d.ts +68 -0
  19. package/dist/core/control-ui-db.js +274 -0
  20. package/dist/core/detection-funnel.d.ts +1 -1
  21. package/dist/core/detection-funnel.js +4 -0
  22. package/dist/core/dictionary.d.ts +2 -0
  23. package/dist/core/dictionary.js +13 -0
  24. package/dist/core/event-log.d.ts +22 -1
  25. package/dist/core/event-log.js +319 -0
  26. package/dist/core/evolution-engine.d.ts +5 -5
  27. package/dist/core/evolution-engine.js +18 -18
  28. package/dist/core/evolution-migration.d.ts +5 -0
  29. package/dist/core/evolution-migration.js +65 -0
  30. package/dist/core/evolution-reducer.d.ts +69 -0
  31. package/dist/core/evolution-reducer.js +369 -0
  32. package/dist/core/evolution-types.d.ts +103 -0
  33. package/dist/core/focus-history.d.ts +65 -0
  34. package/dist/core/focus-history.js +266 -0
  35. package/dist/core/init.js +30 -7
  36. package/dist/core/migration.js +0 -2
  37. package/dist/core/path-resolver.d.ts +3 -0
  38. package/dist/core/path-resolver.js +90 -31
  39. package/dist/core/paths.d.ts +7 -8
  40. package/dist/core/paths.js +48 -40
  41. package/dist/core/profile.js +1 -1
  42. package/dist/core/session-tracker.d.ts +4 -0
  43. package/dist/core/session-tracker.js +15 -0
  44. package/dist/core/thinking-models.d.ts +38 -0
  45. package/dist/core/thinking-models.js +170 -0
  46. package/dist/core/trajectory.d.ts +184 -0
  47. package/dist/core/trajectory.js +817 -0
  48. package/dist/core/trust-engine.d.ts +2 -0
  49. package/dist/core/trust-engine.js +30 -4
  50. package/dist/core/workspace-context.d.ts +13 -0
  51. package/dist/core/workspace-context.js +50 -7
  52. package/dist/hooks/gate.js +301 -30
  53. package/dist/hooks/llm.d.ts +8 -0
  54. package/dist/hooks/llm.js +347 -69
  55. package/dist/hooks/message-sanitize.d.ts +3 -0
  56. package/dist/hooks/message-sanitize.js +37 -0
  57. package/dist/hooks/pain.js +105 -5
  58. package/dist/hooks/prompt.d.ts +20 -11
  59. package/dist/hooks/prompt.js +558 -158
  60. package/dist/hooks/subagent.d.ts +9 -2
  61. package/dist/hooks/subagent.js +40 -3
  62. package/dist/http/principles-console-route.d.ts +2 -0
  63. package/dist/http/principles-console-route.js +257 -0
  64. package/dist/i18n/commands.js +48 -20
  65. package/dist/index.js +264 -8
  66. package/dist/service/control-ui-query-service.d.ts +217 -0
  67. package/dist/service/control-ui-query-service.js +537 -0
  68. package/dist/service/empathy-observer-manager.d.ts +42 -0
  69. package/dist/service/empathy-observer-manager.js +147 -0
  70. package/dist/service/evolution-worker.d.ts +10 -0
  71. package/dist/service/evolution-worker.js +156 -24
  72. package/dist/service/trajectory-service.d.ts +2 -0
  73. package/dist/service/trajectory-service.js +15 -0
  74. package/dist/tools/agent-spawn.d.ts +27 -6
  75. package/dist/tools/agent-spawn.js +339 -87
  76. package/dist/tools/deep-reflect.d.ts +27 -7
  77. package/dist/tools/deep-reflect.js +282 -113
  78. package/dist/types/event-types.d.ts +84 -2
  79. package/dist/types/event-types.js +33 -0
  80. package/dist/types.d.ts +52 -0
  81. package/dist/types.js +24 -1
  82. package/openclaw.plugin.json +43 -11
  83. package/package.json +16 -6
  84. package/templates/langs/zh/core/HEARTBEAT.md +28 -4
  85. package/templates/langs/zh/skills/pd-daily/SKILL.md +97 -13
  86. package/templates/pain_settings.json +54 -2
  87. package/templates/workspace/.principles/PROFILE.json +2 -0
  88. package/templates/workspace/okr/CURRENT_FOCUS.md +57 -0
@@ -0,0 +1,147 @@
1
+ import { WorkspaceContext } from '../core/workspace-context.js';
2
+ import { trackFriction } from '../core/session-tracker.js';
3
+ const OBSERVER_SESSION_PREFIX = 'empathy_obs:';
4
+ export class EmpathyObserverManager {
5
+ static instance;
6
+ sessionLocks = new Map();
7
+ constructor() { }
8
+ static getInstance() {
9
+ if (!EmpathyObserverManager.instance) {
10
+ EmpathyObserverManager.instance = new EmpathyObserverManager();
11
+ }
12
+ return EmpathyObserverManager.instance;
13
+ }
14
+ shouldTrigger(api, sessionId) {
15
+ if (!api || !sessionId)
16
+ return false;
17
+ const enabled = api.config?.empathy_engine?.enabled !== false;
18
+ if (!enabled)
19
+ return false;
20
+ return !this.sessionLocks.has(sessionId);
21
+ }
22
+ async spawn(api, sessionId, userMessage) {
23
+ if (!api)
24
+ return null;
25
+ if (!this.shouldTrigger(api, sessionId))
26
+ return null;
27
+ if (!userMessage?.trim())
28
+ return null;
29
+ const timestamp = Date.now();
30
+ const sessionKey = `${OBSERVER_SESSION_PREFIX}${sessionId}:${timestamp}`;
31
+ this.sessionLocks.set(sessionId, sessionKey);
32
+ const prompt = [
33
+ 'You are an empathy observer.',
34
+ 'Analyze ONLY the user message and return strict JSON (no markdown):',
35
+ '{"damageDetected": boolean, "severity": "mild|moderate|severe", "confidence": number, "reason": string}',
36
+ `User message: ${JSON.stringify(userMessage.trim())}`,
37
+ ].join('\n');
38
+ try {
39
+ await api.runtime.subagent.run({
40
+ sessionKey,
41
+ message: prompt,
42
+ lane: 'subagent',
43
+ deliver: false,
44
+ idempotencyKey: `${sessionId}:${timestamp}`,
45
+ });
46
+ api.logger.info(`[PD:EmpathyObserver] Spawned observer ${sessionKey}`);
47
+ return sessionKey;
48
+ }
49
+ catch (error) {
50
+ this.sessionLocks.delete(sessionId);
51
+ api.logger.warn(`[PD:EmpathyObserver] Failed to spawn observer for ${sessionId}: ${String(error)}`);
52
+ return null;
53
+ }
54
+ }
55
+ async reap(api, targetSessionKey, workspaceDir) {
56
+ if (!api || !workspaceDir || !this.isObserverSession(targetSessionKey))
57
+ return;
58
+ const sessionId = this.extractParentSessionId(targetSessionKey);
59
+ const unlock = () => {
60
+ if (sessionId && this.sessionLocks.get(sessionId) === targetSessionKey) {
61
+ this.sessionLocks.delete(sessionId);
62
+ }
63
+ };
64
+ try {
65
+ const messages = await api.runtime.subagent.getSessionMessages({
66
+ sessionKey: targetSessionKey,
67
+ limit: 20,
68
+ });
69
+ const rawText = this.extractAssistantText(messages.messages, messages.assistantTexts);
70
+ const parsed = this.parseJsonPayload(rawText, api.logger);
71
+ if (parsed?.damageDetected && sessionId) {
72
+ const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
73
+ const score = this.scoreFromSeverity(parsed.severity, wctx.config);
74
+ trackFriction(sessionId, score, `observer_empathy_${parsed.severity || 'mild'}`, workspaceDir);
75
+ api.logger.info(`[PD:EmpathyObserver] Applied GFI +${score} for ${sessionId}`);
76
+ }
77
+ }
78
+ catch (error) {
79
+ api.logger.warn(`[PD:EmpathyObserver] Failed to reap ${targetSessionKey}: ${String(error)}`);
80
+ }
81
+ finally {
82
+ unlock();
83
+ }
84
+ }
85
+ isObserverSession(sessionKey) {
86
+ return typeof sessionKey === 'string' && sessionKey.startsWith(OBSERVER_SESSION_PREFIX);
87
+ }
88
+ extractParentSessionId(sessionKey) {
89
+ if (!this.isObserverSession(sessionKey))
90
+ return null;
91
+ const rest = sessionKey.slice(OBSERVER_SESSION_PREFIX.length);
92
+ const marker = rest.lastIndexOf(':');
93
+ if (marker <= 0)
94
+ return null;
95
+ return rest.slice(0, marker);
96
+ }
97
+ parseJsonPayload(rawText, logger) {
98
+ if (!rawText?.trim())
99
+ return null;
100
+ try {
101
+ return JSON.parse(rawText.trim());
102
+ }
103
+ catch {
104
+ const match = rawText.match(/\{[\s\S]*\}/);
105
+ if (!match) {
106
+ logger?.warn('[PD:EmpathyObserver] Observer payload is not valid JSON, skipping.');
107
+ return null;
108
+ }
109
+ try {
110
+ return JSON.parse(match[0]);
111
+ }
112
+ catch {
113
+ logger?.warn('[PD:EmpathyObserver] Failed to parse observer JSON payload, skipping.');
114
+ return null;
115
+ }
116
+ }
117
+ }
118
+ extractAssistantText(messages, assistantTexts) {
119
+ if (assistantTexts && assistantTexts.length > 0) {
120
+ return assistantTexts[assistantTexts.length - 1] || '';
121
+ }
122
+ for (let i = messages.length - 1; i >= 0; i--) {
123
+ const msg = messages[i];
124
+ if (msg?.role !== 'assistant')
125
+ continue;
126
+ if (typeof msg.content === 'string')
127
+ return msg.content;
128
+ if (Array.isArray(msg.content)) {
129
+ const txt = msg.content
130
+ .filter((part) => part?.type === 'text' && typeof part.text === 'string')
131
+ .map((part) => part.text)
132
+ .join('\n');
133
+ if (txt)
134
+ return txt;
135
+ }
136
+ }
137
+ return '';
138
+ }
139
+ scoreFromSeverity(severity, config) {
140
+ if (severity === 'severe')
141
+ return Number(config.get('empathy_engine.penalties.severe') ?? 40);
142
+ if (severity === 'moderate')
143
+ return Number(config.get('empathy_engine.penalties.moderate') ?? 25);
144
+ return Number(config.get('empathy_engine.penalties.mild') ?? 10);
145
+ }
146
+ }
147
+ export const empathyObserverManager = EmpathyObserverManager.getInstance();
@@ -1,6 +1,7 @@
1
1
  import type { OpenClawPluginServiceContext, OpenClawPluginApi } from '../openclaw-sdk.js';
2
2
  export interface EvolutionQueueItem {
3
3
  id: string;
4
+ task?: string;
4
5
  score: number;
5
6
  source: string;
6
7
  reason: string;
@@ -8,6 +9,15 @@ export interface EvolutionQueueItem {
8
9
  trigger_text_preview?: string;
9
10
  status: 'pending' | 'in_progress' | 'completed';
10
11
  }
12
+ export declare function hasRecentDuplicateTask(queue: EvolutionQueueItem[], source: string, preview: string, now: number): boolean;
13
+ export declare function hasEquivalentPromotedRule(dictionary: {
14
+ getAllRules(): Record<string, {
15
+ type: string;
16
+ phrases?: string[];
17
+ pattern?: string;
18
+ status: string;
19
+ }>;
20
+ }, phrase: string): boolean;
11
21
  export interface ExtendedEvolutionWorkerService {
12
22
  id: string;
13
23
  api: OpenClawPluginApi | null;
@@ -8,6 +8,101 @@ import { SystemLogger } from '../core/system-logger.js';
8
8
  import { WorkspaceContext } from '../core/workspace-context.js';
9
9
  import { initPersistence, flushAllSessions } from '../core/session-tracker.js';
10
10
  let intervalId = null;
11
+ const PAIN_QUEUE_DEDUP_WINDOW_MS = 30 * 60 * 1000;
12
+ // P0 fix: File lock constants and helper for queue operations (prevents TOCTOU race)
13
+ const EVOLUTION_QUEUE_LOCK_SUFFIX = '.lock';
14
+ const LOCK_MAX_RETRIES = 50;
15
+ const LOCK_RETRY_DELAY_MS = 50;
16
+ const LOCK_STALE_MS = 30_000;
17
+ /**
18
+ * Acquire an exclusive file lock for the given resource.
19
+ * Returns a release function. Uses 'wx' flag for atomic exclusive create.
20
+ * Detects stale locks by checking PID and mtime.
21
+ */
22
+ function acquireQueueLock(lockPath, logger) {
23
+ let retries = 0;
24
+ while (retries < LOCK_MAX_RETRIES) {
25
+ try {
26
+ const fd = fs.openSync(lockPath, 'wx');
27
+ fs.writeSync(fd, `${process.pid}\n${Date.now()}`);
28
+ fs.closeSync(fd);
29
+ return () => {
30
+ try {
31
+ fs.unlinkSync(lockPath);
32
+ }
33
+ catch { /* ignore */ }
34
+ };
35
+ }
36
+ catch (err) {
37
+ if (err.code === 'EEXIST') {
38
+ // Check if lock is stale
39
+ try {
40
+ const stat = fs.statSync(lockPath);
41
+ const content = fs.readFileSync(lockPath, 'utf8').trim();
42
+ const pid = parseInt(content.split('\n')[0] || '0', 10);
43
+ let isStale = false;
44
+ if (pid > 0) {
45
+ try {
46
+ process.kill(pid, 0);
47
+ }
48
+ catch (e) {
49
+ if (e.code === 'ESRCH')
50
+ isStale = true;
51
+ }
52
+ if (!isStale && Date.now() - stat.mtimeMs > LOCK_STALE_MS)
53
+ isStale = true;
54
+ }
55
+ else if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
56
+ isStale = true;
57
+ }
58
+ if (isStale) {
59
+ fs.unlinkSync(lockPath);
60
+ retries++;
61
+ const start = Date.now();
62
+ while (Date.now() - start < LOCK_RETRY_DELAY_MS) { /* spin */ }
63
+ continue;
64
+ }
65
+ }
66
+ catch { /* stat/read failed, treat as busy */ }
67
+ retries++;
68
+ const start = Date.now();
69
+ while (Date.now() - start < LOCK_RETRY_DELAY_MS) { /* spin */ }
70
+ continue;
71
+ }
72
+ throw err;
73
+ }
74
+ }
75
+ logger?.warn?.(`[PD:EvolutionWorker] Failed to acquire lock after ${LOCK_MAX_RETRIES} retries: ${lockPath}`);
76
+ return null;
77
+ }
78
+ function normalizePainDedupKey(source, preview) {
79
+ return `${source.trim().toLowerCase()}::${preview.trim().toLowerCase()}`;
80
+ }
81
+ export function hasRecentDuplicateTask(queue, source, preview, now) {
82
+ const key = normalizePainDedupKey(source, preview);
83
+ return queue.some((task) => {
84
+ if (task.status === 'completed')
85
+ return false;
86
+ const taskTime = new Date(task.timestamp).getTime();
87
+ if (!Number.isFinite(taskTime) || (now - taskTime) > PAIN_QUEUE_DEDUP_WINDOW_MS)
88
+ return false;
89
+ return normalizePainDedupKey(task.source, task.trigger_text_preview || '') === key;
90
+ });
91
+ }
92
+ export function hasEquivalentPromotedRule(dictionary, phrase) {
93
+ const normalizedPhrase = phrase.trim().toLowerCase();
94
+ return Object.values(dictionary.getAllRules()).some((rule) => {
95
+ if (rule.status !== 'active')
96
+ return false;
97
+ if (rule.type === 'exact_match' && Array.isArray(rule.phrases)) {
98
+ return rule.phrases.some((candidate) => candidate.trim().toLowerCase() === normalizedPhrase);
99
+ }
100
+ if (rule.type === 'regex' && typeof rule.pattern === 'string') {
101
+ return rule.pattern.trim().toLowerCase() === normalizedPhrase;
102
+ }
103
+ return false;
104
+ });
105
+ }
11
106
  function checkPainFlag(wctx, logger) {
12
107
  try {
13
108
  const painFlagPath = wctx.resolve('PAIN_FLAG');
@@ -37,28 +132,43 @@ function checkPainFlag(wctx, logger) {
37
132
  if (logger)
38
133
  logger.info(`[PD:EvolutionWorker] Detected pain flag (score: ${score}, source: ${source}). Enqueueing evolution task.`);
39
134
  const queuePath = wctx.resolve('EVOLUTION_QUEUE');
40
- let queue = [];
41
- if (fs.existsSync(queuePath)) {
42
- try {
43
- queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
135
+ const lockPath = queuePath + EVOLUTION_QUEUE_LOCK_SUFFIX;
136
+ const releaseLock = acquireQueueLock(lockPath, logger);
137
+ if (!releaseLock)
138
+ return; // Could not acquire lock
139
+ try {
140
+ let queue = [];
141
+ if (fs.existsSync(queuePath)) {
142
+ try {
143
+ queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
144
+ }
145
+ catch (e) {
146
+ if (logger)
147
+ logger.error(`[PD:EvolutionWorker] Failed to parse evolution queue: ${String(e)}`);
148
+ }
44
149
  }
45
- catch (e) {
46
- if (logger)
47
- logger.error(`[PD:EvolutionWorker] Failed to parse evolution queue: ${String(e)}`);
150
+ const now = Date.now();
151
+ if (hasRecentDuplicateTask(queue, source, preview, now)) {
152
+ logger?.info?.(`[PD:EvolutionWorker] Duplicate pain task skipped for source=${source} preview=${preview || 'N/A'}`);
153
+ fs.appendFileSync(painFlagPath, `\nstatus: queued\n`, 'utf8');
154
+ return;
48
155
  }
156
+ const taskId = createHash('md5').update(`${source}:${score}:${preview}`).digest('hex').substring(0, 8);
157
+ queue.push({
158
+ id: taskId,
159
+ score,
160
+ source,
161
+ reason,
162
+ trigger_text_preview: preview,
163
+ timestamp: new Date(now).toISOString(),
164
+ status: 'pending'
165
+ });
166
+ fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
167
+ fs.appendFileSync(painFlagPath, '\nstatus: queued\n', 'utf8');
168
+ }
169
+ finally {
170
+ releaseLock();
49
171
  }
50
- const taskId = createHash('md5').update(`${source}:${score}:${new Date().toISOString()}`).digest('hex').substring(0, 8);
51
- queue.push({
52
- id: taskId,
53
- score,
54
- source,
55
- reason,
56
- trigger_text_preview: preview,
57
- timestamp: new Date().toISOString(),
58
- status: 'pending'
59
- });
60
- fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
61
- fs.appendFileSync(painFlagPath, '\nstatus: queued\n', 'utf8');
62
172
  }
63
173
  catch (err) {
64
174
  if (logger)
@@ -66,11 +176,23 @@ function checkPainFlag(wctx, logger) {
66
176
  }
67
177
  }
68
178
  function processEvolutionQueue(wctx, logger, eventLog) {
179
+ const queuePath = wctx.resolve('EVOLUTION_QUEUE');
180
+ if (!fs.existsSync(queuePath))
181
+ return;
182
+ const lockPath = queuePath + EVOLUTION_QUEUE_LOCK_SUFFIX;
183
+ const releaseLock = acquireQueueLock(lockPath, logger);
184
+ if (!releaseLock)
185
+ return; // Could not acquire lock
69
186
  try {
70
- const queuePath = wctx.resolve('EVOLUTION_QUEUE');
71
- if (!fs.existsSync(queuePath))
187
+ let queue = [];
188
+ try {
189
+ queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
190
+ }
191
+ catch (e) {
192
+ if (logger)
193
+ logger.error(`[PD:EvolutionWorker] Failed to parse evolution queue: ${String(e)}`);
72
194
  return;
73
- const queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
195
+ }
74
196
  let queueChanged = false;
75
197
  const config = wctx.config;
76
198
  const timeout = config.get('intervals.task_timeout_ms') || (30 * 60 * 1000);
@@ -89,13 +211,15 @@ function processEvolutionQueue(wctx, logger, eventLog) {
89
211
  if (pendingTasks.length > 0) {
90
212
  const directivePath = wctx.resolve('EVOLUTION_DIRECTIVE');
91
213
  const highestScoreTask = pendingTasks.sort((a, b) => b.score - a.score)[0];
214
+ const taskDescription = `Diagnose systemic pain [ID: ${highestScoreTask.id}]. Source: ${highestScoreTask.source}. Reason: ${highestScoreTask.reason}. ` +
215
+ `Trigger text: "${highestScoreTask.trigger_text_preview || 'N/A'}"`;
92
216
  const directive = {
93
217
  active: true,
94
- task: `Diagnose systemic pain [ID: ${highestScoreTask.id}]. Source: ${highestScoreTask.source}. Reason: ${highestScoreTask.reason}. ` +
95
- `Trigger text: "${highestScoreTask.trigger_text_preview || 'N/A'}"`,
218
+ task: taskDescription,
96
219
  timestamp: new Date().toISOString()
97
220
  };
98
221
  fs.writeFileSync(directivePath, JSON.stringify(directive, null, 2), 'utf8');
222
+ highestScoreTask.task = taskDescription;
99
223
  highestScoreTask.status = 'in_progress';
100
224
  queueChanged = true;
101
225
  if (eventLog) {
@@ -114,6 +238,9 @@ function processEvolutionQueue(wctx, logger, eventLog) {
114
238
  if (logger)
115
239
  logger.warn(`[PD:EvolutionWorker] Error processing evolution queue: ${String(err)}`);
116
240
  }
241
+ finally {
242
+ releaseLock();
243
+ }
117
244
  }
118
245
  async function processDetectionQueue(wctx, api, eventLog) {
119
246
  const logger = api.logger;
@@ -213,6 +340,11 @@ function processPromotion(wctx, logger, eventLog) {
213
340
  if (commonPhrases.length > 0) {
214
341
  const phrase = commonPhrases[0];
215
342
  const ruleId = `P_PROMOTED_${fingerprint.toUpperCase()}`;
343
+ if (hasEquivalentPromotedRule(dictionary, phrase)) {
344
+ cand.status = 'duplicate';
345
+ logger?.info?.(`[PD:EvolutionWorker] Skipping duplicate promoted rule for candidate ${fingerprint}: ${phrase}`);
346
+ continue;
347
+ }
216
348
  if (logger)
217
349
  logger.info(`[PD:EvolutionWorker] Promoting candidate ${fingerprint} to formal rule: ${ruleId}`);
218
350
  SystemLogger.log(wctx.workspaceDir, 'RULE_PROMOTED', `Candidate ${fingerprint} promoted to rule ${ruleId}`);
@@ -0,0 +1,2 @@
1
+ import type { OpenClawPluginService } from '../openclaw-sdk.js';
2
+ export declare const TrajectoryService: OpenClawPluginService;
@@ -0,0 +1,15 @@
1
+ import { TrajectoryRegistry } from '../core/trajectory.js';
2
+ import { WorkspaceContext } from '../core/workspace-context.js';
3
+ export const TrajectoryService = {
4
+ id: 'principles-disciple-trajectory',
5
+ start(ctx) {
6
+ if (!ctx.workspaceDir)
7
+ return;
8
+ WorkspaceContext.fromHookContext(ctx).trajectory;
9
+ },
10
+ stop(ctx) {
11
+ if (!ctx.workspaceDir)
12
+ return;
13
+ TrajectoryRegistry.dispose(ctx.workspaceDir);
14
+ },
15
+ };
@@ -6,22 +6,43 @@
6
6
  */
7
7
  import type { OpenClawPluginApi } from '../openclaw-sdk.js';
8
8
  /**
9
- * Agent Spawn Tool definition
9
+ * Create Agent Spawn Tool
10
+ *
11
+ * Uses factory pattern to capture `api` in closure, following OpenClaw plugin SDK conventions.
12
+ * The execute signature must be: async (_toolCallId: string, rawParams: Record<string, unknown>)
10
13
  */
11
- export declare const agentSpawnTool: {
14
+ export declare function createAgentSpawnTool(api: OpenClawPluginApi): {
12
15
  name: string;
13
16
  description: string;
14
17
  parameters: import("@sinclair/typebox").TObject<{
15
18
  agentType: import("@sinclair/typebox").TString;
16
19
  task: import("@sinclair/typebox").TString;
20
+ runInBackground: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
17
21
  }>;
18
22
  /**
19
23
  * Execution logic for the agent spawn tool
24
+ *
25
+ * OpenClaw tool execute signature:
26
+ * - First parameter: _toolCallId (string) - the tool call ID
27
+ * - Second parameter: rawParams (Record<string, unknown>) - the actual parameters
28
+ * - Third parameter (optional): signal (AbortSignal) - for cancellation
20
29
  */
21
- execute(params: {
22
- agentType: string;
23
- task: string;
24
- }, api: OpenClawPluginApi, _workspaceDir?: string): Promise<string>;
30
+ execute(_toolCallId: string, rawParams: Record<string, unknown>): Promise<{
31
+ content: Array<{
32
+ type: string;
33
+ text: string;
34
+ }>;
35
+ }>;
36
+ };
37
+ export declare const agentSpawnTool: {
38
+ name: string;
39
+ description: string;
40
+ parameters: import("@sinclair/typebox").TObject<{
41
+ agentType: import("@sinclair/typebox").TString;
42
+ task: import("@sinclair/typebox").TString;
43
+ runInBackground: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
44
+ }>;
45
+ execute: () => never;
25
46
  };
26
47
  /**
27
48
  * Batch spawn multiple agents in sequence