metame-cli 1.5.21 → 1.5.23
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 +3 -2
- package/index.js +25 -24
- package/package.json +1 -1
- package/scripts/agent-intent-shared.js +111 -0
- package/scripts/daemon-agent-commands.js +160 -354
- package/scripts/daemon-agent-intent.js +282 -0
- package/scripts/daemon-agent-lifecycle.js +243 -0
- package/scripts/daemon-agent-workflow.js +295 -0
- package/scripts/daemon-bridges.js +18 -5
- package/scripts/daemon-claude-engine.js +74 -75
- package/scripts/daemon-command-router.js +24 -294
- package/scripts/daemon-prompt-context.js +127 -0
- package/scripts/daemon-reactive-lifecycle.js +138 -6
- package/scripts/daemon-session-commands.js +16 -2
- package/scripts/daemon-session-store.js +104 -0
- package/scripts/daemon-team-workflow.js +146 -0
- package/scripts/daemon.js +14 -3
- package/scripts/docs/hook-config.md +41 -21
- package/scripts/docs/maintenance-manual.md +2 -2
- package/scripts/docs/orphan-files-review.md +1 -1
- package/scripts/docs/pointer-map.md +2 -2
- package/scripts/hooks/intent-agent-capability.js +51 -0
- package/scripts/hooks/intent-doc-router.js +23 -11
- package/scripts/hooks/intent-memory-recall.js +1 -3
- package/scripts/hooks/intent-team-dispatch.js +1 -1
- package/scripts/intent-registry.js +78 -14
- package/scripts/ops-mission-queue.js +101 -36
- package/scripts/ops-reactive-bootstrap.js +86 -0
- package/scripts/resolve-yaml.js +3 -0
- package/scripts/runtime-bootstrap.js +77 -0
- package/scripts/hooks/intent-engine.js +0 -75
- package/scripts/hooks/team-context.js +0 -143
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { resolveEngineModel } = require('./daemon-engine-runtime');
|
|
4
|
-
const {
|
|
4
|
+
const { createAgentIntentHandler } = require('./daemon-agent-intent');
|
|
5
|
+
const { rawChatId: extractOriginalChatId, isThreadChatId } = require('./core/thread-chat-id');
|
|
5
6
|
|
|
6
7
|
function createCommandRouter(deps) {
|
|
7
8
|
const {
|
|
@@ -135,82 +136,14 @@ function createCommandRouter(deps) {
|
|
|
135
136
|
return false;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
function extractQuotedContent(input) {
|
|
139
|
-
const m = String(input || '').match(/[“"'「](.+?)[”"'」]/);
|
|
140
|
-
return m ? m[1].trim() : '';
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function extractPathFromText(input) {
|
|
144
|
-
const m = String(input || '').match(/(?:~\/|\/|\.\/|\.\.\/)[^\s,。;;!!??"“”'‘’`]+/);
|
|
145
|
-
if (!m) return '';
|
|
146
|
-
return m[0].replace(/[,。;;!!??]+$/, '');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function extractAgentName(input) {
|
|
150
|
-
const text = String(input || '').trim();
|
|
151
|
-
const byNameField = text.match(/(?:名字|名称|叫做?|名为|named?)\s*(?:为)?\s*[“"'「]?([^\s,。;;!!??"“”'‘’`]+)[”"'」]?/i);
|
|
152
|
-
if (byNameField) return byNameField[1].trim();
|
|
153
|
-
const byBind = text.match(/(?:bind|绑定)\s*(?:到|为|成)?\s*[“"'「]?([a-zA-Z0-9_\-\u4e00-\u9fa5]+)[”"'」]?/i);
|
|
154
|
-
if (byBind) return byBind[1].trim();
|
|
155
|
-
return '';
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function deriveAgentName(input, workspaceDir) {
|
|
159
|
-
const explicit = extractAgentName(input);
|
|
160
|
-
if (explicit) return explicit;
|
|
161
|
-
if (workspaceDir) {
|
|
162
|
-
const basename = workspaceDir.split(/[/\\]/).filter(Boolean).pop();
|
|
163
|
-
if (basename) return basename;
|
|
164
|
-
}
|
|
165
|
-
return 'workspace-agent';
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function deriveRoleDelta(input) {
|
|
169
|
-
const text = String(input || '').trim();
|
|
170
|
-
const quoted = extractQuotedContent(text);
|
|
171
|
-
if (quoted) return quoted;
|
|
172
|
-
const byVerb = text.match(/(?:改成|改为|变成|设为|更新为)\s*[::]?\s*(.+)$/);
|
|
173
|
-
if (byVerb) return byVerb[1].trim();
|
|
174
|
-
return text;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function deriveCreateRoleDelta(input) {
|
|
178
|
-
const text = String(input || '').trim();
|
|
179
|
-
const quoted = extractQuotedContent(text);
|
|
180
|
-
if (quoted) return quoted;
|
|
181
|
-
const byRoleField = text.match(/(?:角色|职责|人设)\s*(?:是|为|:|:)?\s*(.+)$/i);
|
|
182
|
-
if (byRoleField) return byRoleField[1].trim();
|
|
183
|
-
return '';
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function inferAgentEngineFromText(input) {
|
|
187
|
-
const text = String(input || '').trim().toLowerCase();
|
|
188
|
-
if (!text) return null;
|
|
189
|
-
if (/\bcodex\b/.test(text) || /柯德|科德/.test(text)) return 'codex';
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function isLikelyDirectAgentAction(input) {
|
|
194
|
-
const text = String(input || '').trim();
|
|
195
|
-
return /^(?:请|帮我|麻烦|给我|给这个群|给当前群|在这个群|把这个群|把当前群|将这个群|这个群|当前群|本群|群里|我想|我要|我需要|创建|新建|新增|搞一个|加一个|create|bind|绑定|列出|查看|显示|有哪些|解绑|取消绑定|断开绑定|修改|调整)/i.test(text);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function looksLikeAgentIssueReport(input) {
|
|
199
|
-
const text = String(input || '').trim();
|
|
200
|
-
const hasIssueWords = /(用户反馈|反馈|报错|bug|问题|故障|异常|修复|改一下|修一下|任务|工单|代码)/i.test(text);
|
|
201
|
-
const hasAgentWords = /(agent|智能体|session|会话|目录|工作区|绑定|切换)/i.test(text);
|
|
202
|
-
return hasIssueWords && hasAgentWords;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function projectNameFromResult(data, fallbackName) {
|
|
206
|
-
if (data && data.project && data.project.name) return data.project.name;
|
|
207
|
-
if (data && data.projectKey) return data.projectKey;
|
|
208
|
-
return fallbackName || 'workspace-agent';
|
|
209
|
-
}
|
|
210
|
-
|
|
211
139
|
function projectKeyFromVirtualChatId(chatId) {
|
|
212
140
|
const v = String(chatId || '');
|
|
213
|
-
if (v.startsWith('_agent_'))
|
|
141
|
+
if (v.startsWith('_agent_')) {
|
|
142
|
+
const rest = v.slice(7);
|
|
143
|
+
const scopeIdx = rest.indexOf('::');
|
|
144
|
+
const key = scopeIdx >= 0 ? rest.slice(0, scopeIdx) : rest;
|
|
145
|
+
return key || null;
|
|
146
|
+
}
|
|
214
147
|
if (v.startsWith('_scope_')) {
|
|
215
148
|
const idx = v.lastIndexOf('__');
|
|
216
149
|
if (idx > 7 && idx + 2 < v.length) return v.slice(idx + 2);
|
|
@@ -222,6 +155,9 @@ function createCommandRouter(deps) {
|
|
|
222
155
|
const rawChatId = String(chatId || '');
|
|
223
156
|
const inferredKey = projectKey || projectKeyFromVirtualChatId(rawChatId);
|
|
224
157
|
if (rawChatId.startsWith('_agent_') || rawChatId.startsWith('_scope_')) return rawChatId;
|
|
158
|
+
// Feishu topics must keep per-thread isolation even when the thread is
|
|
159
|
+
// temporarily routed to a named agent/project via nickname or chat_agent_map.
|
|
160
|
+
if (isThreadChatId(rawChatId)) return rawChatId;
|
|
225
161
|
return inferredKey ? `_bound_${inferredKey}` : rawChatId;
|
|
226
162
|
}
|
|
227
163
|
|
|
@@ -407,226 +343,20 @@ function createCommandRouter(deps) {
|
|
|
407
343
|
});
|
|
408
344
|
}
|
|
409
345
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if (/让.*做分身|叫.*做分身|甲.*做分身/.test(text)) return true;
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function _detectNewAgentIntent(text) {
|
|
427
|
-
if (!text || text.startsWith('/') || text.length < 3) return false;
|
|
428
|
-
if (_detectCloneIntent(text)) return false;
|
|
429
|
-
if (_detectTeamIntent(text)) return false;
|
|
430
|
-
const agentKeywords = ['agent', '助手', '机器人', '小助手'];
|
|
431
|
-
const hasAgentKeyword = agentKeywords.some(k => text.toLowerCase().includes(k.toLowerCase()));
|
|
432
|
-
const actionKeywords = ['新建', '创建', '造', '做一个', '加一个', '增加', '添加', '开一个'];
|
|
433
|
-
const hasAction = actionKeywords.some(k => text.includes(k));
|
|
434
|
-
if (hasAgentKeyword && hasAction) {
|
|
435
|
-
const excludePatterns = [/已经/, /存在/, /有了/, /好了/, /完成/, /搞定/, /配置好/, /怎么建/, /如何建/, /方法/, /步骤/, /是什么/, /哪个/];
|
|
436
|
-
if (excludePatterns.some(p => p.test(text))) return false;
|
|
437
|
-
return true;
|
|
438
|
-
}
|
|
439
|
-
if (/^(给我|帮我|我要|我想|给我加|帮我加)/.test(text) && hasAgentKeyword) return true;
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function _detectTeamIntent(text) {
|
|
444
|
-
if (!text || text.startsWith('/') || text.length < 4) return false;
|
|
445
|
-
// Exclude: only mentioning team, no creation intent
|
|
446
|
-
if (/走team|用team|通过team|team里|team中|团队里|团队中|走团队|用团队|在team|在团队|team.*已经|团队.*已经|team.*讨论|团队.*讨论/.test(text)) return false;
|
|
447
|
-
// Positive match: team + action word
|
|
448
|
-
if ((text.includes('团队') || text.includes('工作组'))) {
|
|
449
|
-
if (/(新建|创建|造一个|加一个|组建|设置|建|搞)/.test(text)) {
|
|
450
|
-
if (/怎么|如何|方法|步骤/.test(text)) return false;
|
|
451
|
-
return true;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
// Pattern: "建个团队" / "搞个团队"
|
|
455
|
-
if (/^(新建|创建|建|搞).*团队/.test(text)) return true;
|
|
456
|
-
return false;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
async function tryHandleAgentIntent(bot, chatId, text, config) {
|
|
460
|
-
if (!agentTools || !text || text.startsWith('/')) return false;
|
|
461
|
-
const key = String(chatId);
|
|
462
|
-
if (hasFreshPendingFlow(key) || hasFreshPendingFlow(key + ':edit')) return false;
|
|
463
|
-
const input = text.trim();
|
|
464
|
-
if (!input) return false;
|
|
465
|
-
|
|
466
|
-
// Clone intent — route to /agent new clone wizard
|
|
467
|
-
if (_detectCloneIntent(input)) {
|
|
468
|
-
log('INFO', `[CloneIntent] "${input.slice(0, 80)}" → /agent new clone`);
|
|
469
|
-
await handleAgentCommand({ bot, chatId, text: '/agent new clone', config });
|
|
470
|
-
return true;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// New agent intent — route to /agent new wizard
|
|
474
|
-
if (_detectNewAgentIntent(input)) {
|
|
475
|
-
log('INFO', `[NewAgentIntent] "${input.slice(0, 80)}" → /agent new`);
|
|
476
|
-
await handleAgentCommand({ bot, chatId, text: '/agent new', config });
|
|
477
|
-
return true;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Team creation intent — route to /agent new team wizard
|
|
481
|
-
if (_detectTeamIntent(input)) {
|
|
482
|
-
log('INFO', `[TeamIntent] "${input.slice(0, 80)}" → /agent new team`);
|
|
483
|
-
await handleAgentCommand({ bot, chatId, text: '/agent new team', config });
|
|
484
|
-
return true;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const directAction = isLikelyDirectAgentAction(input);
|
|
488
|
-
const issueReport = looksLikeAgentIssueReport(input);
|
|
489
|
-
if (issueReport && !directAction) return false;
|
|
490
|
-
const workspaceDir = extractPathFromText(input);
|
|
491
|
-
const hasWorkspacePath = !!workspaceDir;
|
|
492
|
-
|
|
493
|
-
// Exclude third-party product context — "智能体" about other companies is NOT about our agents
|
|
494
|
-
// Requires BOTH a company name AND agent-related keyword to trigger, avoiding false positives on generic verbs
|
|
495
|
-
const _hasThirdPartyName = /(阿里|百度|腾讯|字节|谷歌|google|openai|微软|microsoft|deepseek|豆包|通义|文心|kimi)/i.test(input);
|
|
496
|
-
const _hasAgentWord = /(智能体|agent|助手|机器人)/i.test(input);
|
|
497
|
-
const _isAboutOurAgents = /(我的|我们的|当前|这个群|这里的|metame)/i.test(input);
|
|
498
|
-
if (_hasThirdPartyName && _hasAgentWord && !_isAboutOurAgents) return false;
|
|
499
|
-
|
|
500
|
-
const hasAgentContext = /(agent|智能体|工作区|人设|绑定|当前群|这个群|chat|workspace)/i.test(input);
|
|
501
|
-
const wantsList = /(列出|查看|显示|有哪些|list|show)/i.test(input) && /(agent|智能体|工作区|绑定)/i.test(input);
|
|
502
|
-
const wantsUnbind = /(解绑|取消绑定|断开绑定|unbind|unassign)/i.test(input) && hasAgentContext;
|
|
503
|
-
const wantsEditRole =
|
|
504
|
-
((/(角色|职责|人设)/i.test(input) && /(改|修改|调整|更新|变成|改成|改为)/i.test(input)) ||
|
|
505
|
-
/(把这个agent|把当前agent|当前群.*角色|当前群.*职责)/i.test(input));
|
|
506
|
-
const wantsCreate =
|
|
507
|
-
(/(创建|新建|新增|搞一个|加一个|create)/i.test(input) && /(agent|智能体|人设|工作区)/i.test(input) && (directAction || hasWorkspacePath));
|
|
508
|
-
const wantsBind =
|
|
509
|
-
!wantsCreate &&
|
|
510
|
-
(/(绑定|bind)/i.test(input) && hasAgentContext && (directAction || hasWorkspacePath));
|
|
511
|
-
|
|
512
|
-
if (!wantsList && !wantsUnbind && !wantsEditRole && !wantsCreate && !wantsBind) {
|
|
513
|
-
return false;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (wantsList) {
|
|
517
|
-
const res = await agentTools.listAllAgents(chatId);
|
|
518
|
-
if (!res.ok) {
|
|
519
|
-
await bot.sendMessage(chatId, `❌ 查询 Agent 失败: ${res.error}`);
|
|
520
|
-
return true;
|
|
521
|
-
}
|
|
522
|
-
const agents = res.data.agents || [];
|
|
523
|
-
if (agents.length === 0) {
|
|
524
|
-
await bot.sendMessage(chatId, '暂无已配置的 Agent。你可以直接说“给这个群创建一个 Agent,目录是 ~/xxx”。');
|
|
525
|
-
return true;
|
|
526
|
-
}
|
|
527
|
-
const lines = ['📋 当前 Agent 列表', ''];
|
|
528
|
-
for (const a of agents) {
|
|
529
|
-
const marker = a.key === res.data.boundKey ? ' ◀ 当前' : '';
|
|
530
|
-
lines.push(`${a.icon || '🤖'} ${a.name}${marker}`);
|
|
531
|
-
lines.push(`目录: ${a.cwd}`);
|
|
532
|
-
lines.push(`Key: ${a.key}`);
|
|
533
|
-
lines.push('');
|
|
534
|
-
}
|
|
535
|
-
await bot.sendMessage(chatId, lines.join('\n').trimEnd());
|
|
536
|
-
return true;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if (wantsUnbind) {
|
|
540
|
-
const res = await agentTools.unbindCurrentAgent(chatId);
|
|
541
|
-
if (!res.ok) {
|
|
542
|
-
await bot.sendMessage(chatId, `❌ 解绑失败: ${res.error}`);
|
|
543
|
-
return true;
|
|
544
|
-
}
|
|
545
|
-
if (res.data.unbound) {
|
|
546
|
-
await bot.sendMessage(chatId, `✅ 已解绑当前群(原 Agent: ${res.data.previousProjectKey})`);
|
|
547
|
-
} else {
|
|
548
|
-
await bot.sendMessage(chatId, '当前群没有绑定 Agent,无需解绑。');
|
|
549
|
-
}
|
|
550
|
-
return true;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
if (wantsEditRole) {
|
|
554
|
-
const freshCfg = loadConfig();
|
|
555
|
-
const bound = getBoundProjectForChat(chatId, freshCfg);
|
|
556
|
-
if (!bound.project || !bound.project.cwd) {
|
|
557
|
-
await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent。先说“给这个群绑定一个 Agent,目录是 ~/xxx”。');
|
|
558
|
-
return true;
|
|
559
|
-
}
|
|
560
|
-
// Lazy migration: ensure soul layer exists for agents created before this feature
|
|
561
|
-
if (agentTools && typeof agentTools.repairAgentSoul === 'function') {
|
|
562
|
-
await agentTools.repairAgentSoul(bound.project.cwd).catch(() => {});
|
|
563
|
-
}
|
|
564
|
-
const roleDelta = deriveRoleDelta(input);
|
|
565
|
-
const res = await agentTools.editAgentRoleDefinition(bound.project.cwd, roleDelta);
|
|
566
|
-
if (!res.ok) {
|
|
567
|
-
await bot.sendMessage(chatId, `❌ 更新角色失败: ${res.error}`);
|
|
568
|
-
return true;
|
|
569
|
-
}
|
|
570
|
-
await bot.sendMessage(chatId, res.data.created ? '✅ 已创建 CLAUDE.md 并写入角色定义' : '✅ 角色定义已更新到 CLAUDE.md');
|
|
571
|
-
return true;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
if (wantsCreate) {
|
|
575
|
-
if (!workspaceDir) {
|
|
576
|
-
await bot.sendMessage(chatId, [
|
|
577
|
-
'我可以帮你创建 Agent,还差一个工作目录。',
|
|
578
|
-
'例如:`给这个群创建一个 Agent,目录是 ~/projects/foo`',
|
|
579
|
-
'也可以直接回我一个路径(`~/`、`/`、`./`、`../` 开头都行)。',
|
|
580
|
-
].join('\n'));
|
|
581
|
-
return true;
|
|
582
|
-
}
|
|
583
|
-
const agentName = deriveAgentName(input, workspaceDir);
|
|
584
|
-
const roleDelta = deriveCreateRoleDelta(input);
|
|
585
|
-
const inferredEngine = inferAgentEngineFromText(input);
|
|
586
|
-
// Always skip binding creating chat — new group activates via /activate
|
|
587
|
-
const res = await agentTools.createNewWorkspaceAgent(agentName, workspaceDir, roleDelta, chatId, {
|
|
588
|
-
skipChatBinding: true,
|
|
589
|
-
engine: inferredEngine,
|
|
590
|
-
});
|
|
591
|
-
if (!res.ok) {
|
|
592
|
-
await bot.sendMessage(chatId, `❌ 创建 Agent 失败: ${res.error}`);
|
|
593
|
-
return true;
|
|
594
|
-
}
|
|
595
|
-
const data = res.data || {};
|
|
596
|
-
const projName = projectNameFromResult(data, agentName);
|
|
597
|
-
const engineTip = data.project && data.project.engine ? `\n引擎: ${data.project.engine}` : '';
|
|
598
|
-
if (data.projectKey && pendingActivations) {
|
|
599
|
-
pendingActivations.set(data.projectKey, {
|
|
600
|
-
agentKey: data.projectKey, agentName: projName, cwd: data.cwd,
|
|
601
|
-
createdByChatId: String(chatId), createdAt: Date.now(),
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
await bot.sendMessage(chatId,
|
|
605
|
-
`✅ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}${engineTip}\n\n` +
|
|
606
|
-
`**下一步**: 在新群里发送 \`/activate\` 完成绑定(30分钟内有效)`
|
|
607
|
-
);
|
|
608
|
-
return true;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
if (wantsBind) {
|
|
612
|
-
const agentName = deriveAgentName(input, workspaceDir);
|
|
613
|
-
const inferredEngine = inferAgentEngineFromText(input);
|
|
614
|
-
const res = await agentTools.bindAgentToChat(chatId, agentName, workspaceDir || null, { engine: inferredEngine });
|
|
615
|
-
if (!res.ok) {
|
|
616
|
-
await bot.sendMessage(chatId, `❌ 绑定失败: ${res.error}`);
|
|
617
|
-
return true;
|
|
618
|
-
}
|
|
619
|
-
const data = res.data || {};
|
|
620
|
-
const projName = projectNameFromResult(data, agentName);
|
|
621
|
-
if (data.cwd) attachOrCreateSession(chatId, normalizeCwd(data.cwd), projName, (data.project && data.project.engine) || getDefaultEngine());
|
|
622
|
-
await bot.sendMessage(chatId, `✅ 已绑定 Agent\n名称: ${projName}\n目录: ${data.cwd || '(未知)'}`);
|
|
623
|
-
return true;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
return false;
|
|
627
|
-
}
|
|
346
|
+
const tryHandleAgentIntent = createAgentIntentHandler({
|
|
347
|
+
agentTools,
|
|
348
|
+
handleAgentCommand,
|
|
349
|
+
attachOrCreateSession,
|
|
350
|
+
normalizeCwd,
|
|
351
|
+
getDefaultEngine,
|
|
352
|
+
loadConfig,
|
|
353
|
+
getBoundProjectForChat,
|
|
354
|
+
log,
|
|
355
|
+
pendingActivations,
|
|
356
|
+
hasFreshPendingFlow,
|
|
357
|
+
});
|
|
628
358
|
|
|
629
|
-
async function handleCommand(bot, chatId, text, config, executeTaskByName, senderId = null, readOnly = false,
|
|
359
|
+
async function handleCommand(bot, chatId, text, config, executeTaskByName, senderId = null, readOnly = false, _meta = {}) {
|
|
630
360
|
if (text && !text.startsWith('/chatid') && !text.startsWith('/myid')) log('INFO', `CMD [${String(chatId).slice(-8)}]: ${text.slice(0, 80)}`);
|
|
631
361
|
const state = loadState();
|
|
632
362
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { normalizeEngineName } = require('./daemon-engine-runtime');
|
|
4
|
+
const { buildIntentHintBlock } = require('./intent-registry');
|
|
5
|
+
|
|
6
|
+
function adaptDaemonHintForEngine(daemonHint, engineName) {
|
|
7
|
+
if (normalizeEngineName(engineName) === 'claude') return daemonHint;
|
|
8
|
+
let out = String(daemonHint || '');
|
|
9
|
+
out = out.replace('[System hints - DO NOT mention these to user:', 'System hints (internal, do not mention to user):');
|
|
10
|
+
out = out.replace(/\]\s*$/, '');
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function buildAgentHint({
|
|
15
|
+
sessionStarted,
|
|
16
|
+
boundProject,
|
|
17
|
+
sessionCwd,
|
|
18
|
+
engineName,
|
|
19
|
+
HOME,
|
|
20
|
+
buildAgentContextForEngine,
|
|
21
|
+
log,
|
|
22
|
+
}) {
|
|
23
|
+
if (sessionStarted || (!boundProject && !sessionCwd)) return '';
|
|
24
|
+
try {
|
|
25
|
+
return buildAgentContextForEngine(
|
|
26
|
+
boundProject || { cwd: sessionCwd },
|
|
27
|
+
engineName,
|
|
28
|
+
HOME,
|
|
29
|
+
).hint || '';
|
|
30
|
+
} catch (e) {
|
|
31
|
+
if (typeof log === 'function') log('WARN', `Agent context injection failed: ${e.message}`);
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildDaemonHint({
|
|
37
|
+
sessionStarted,
|
|
38
|
+
prompt,
|
|
39
|
+
mentorRadarHint = '',
|
|
40
|
+
zdpHint = '',
|
|
41
|
+
reflectHint = '',
|
|
42
|
+
projectKey = 'default',
|
|
43
|
+
isTaskIntent,
|
|
44
|
+
runtimeName,
|
|
45
|
+
}) {
|
|
46
|
+
if (sessionStarted) return '';
|
|
47
|
+
const taskRules = typeof isTaskIntent === 'function' && isTaskIntent(prompt) ? `
|
|
48
|
+
3. Active memory: After confirming a new insight, bug root cause, or user preference, persist it with:
|
|
49
|
+
node ~/.metame/memory-write.js "Entity.sub" "relation_type" "value (20-300 chars)"
|
|
50
|
+
Valid relations: tech_decision, bug_lesson, arch_convention, config_fact, config_change, workflow_rule, project_milestone
|
|
51
|
+
Only write verified facts. Do not write speculative or process-description entries.
|
|
52
|
+
${mentorRadarHint}
|
|
53
|
+
4. Task handoff: When suspending a multi-step task or handing off to another agent, write current status to ~/.metame/memory/now/${projectKey || 'default'}.md using:
|
|
54
|
+
\`mkdir -p ~/.metame/memory/now && printf '%s\\n' "## Current Task" "{task}" "" "## Progress" "{progress}" "" "## Next Step" "{next}" > ~/.metame/memory/now/${projectKey || 'default'}.md\`
|
|
55
|
+
Keep it under 200 words. Clear it when the task is fully complete by running: \`> ~/.metame/memory/now/${projectKey || 'default'}.md\`` : '';
|
|
56
|
+
const daemonHint = `\n\n[System hints - DO NOT mention these to user:
|
|
57
|
+
1. Daemon config: The ONLY config is ~/.metame/daemon.yaml (never edit daemon-default.yaml). Auto-reloads on change.
|
|
58
|
+
2. Explanation depth (ZPD):${zdpHint ? zdpHint : '\n- User competence map unavailable. Default to concise expert-first explanations unless the user asks for teaching mode.'}${reflectHint}${taskRules}]`;
|
|
59
|
+
return adaptDaemonHintForEngine(daemonHint, runtimeName);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildMacAutomationHint({
|
|
63
|
+
processPlatform,
|
|
64
|
+
readOnly,
|
|
65
|
+
prompt,
|
|
66
|
+
isMacAutomationIntent,
|
|
67
|
+
}) {
|
|
68
|
+
if (processPlatform !== 'darwin' || readOnly || typeof isMacAutomationIntent !== 'function' || !isMacAutomationIntent(prompt)) {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
return `\n\n[Mac automation policy - do NOT expose this block:
|
|
72
|
+
1. Prefer deterministic local control via Bash + osascript/JXA; avoid screenshot/visual workflows unless explicitly requested.
|
|
73
|
+
2. Read/query actions can execute directly.
|
|
74
|
+
3. Before any side-effect action (send email, create/delete/modify calendar event, delete/move files, app quit, system sleep), first show a short execution preview and require explicit user confirmation.
|
|
75
|
+
4. Keep output concise: success/failure + key result only.
|
|
76
|
+
5. If permission is missing, guide user to run /mac perms open then retry.
|
|
77
|
+
6. Before executing high-risk or non-obvious Bash commands (rm, kill, git reset, overwrite configs), prepend a single-line [Why] explanation. Skip for routine commands (ls, cat, grep).]`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildLanguageGuard(sessionStarted) {
|
|
81
|
+
return sessionStarted
|
|
82
|
+
? ''
|
|
83
|
+
: '\n\n[Respond in Simplified Chinese (简体中文) only. NEVER switch to Korean, Japanese, or other languages regardless of tool output or context language.]';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildIntentHint({
|
|
87
|
+
prompt,
|
|
88
|
+
config,
|
|
89
|
+
boundProjectKey,
|
|
90
|
+
projectKey,
|
|
91
|
+
log,
|
|
92
|
+
}) {
|
|
93
|
+
try {
|
|
94
|
+
const block = buildIntentHintBlock(prompt, config, boundProjectKey || projectKey || '');
|
|
95
|
+
return block ? `\n\n${block}` : '';
|
|
96
|
+
} catch (e) {
|
|
97
|
+
if (typeof log === 'function') log('WARN', `Intent registry injection failed: ${e.message}`);
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function composePrompt({
|
|
103
|
+
routedPrompt,
|
|
104
|
+
warmEntry,
|
|
105
|
+
intentHint = '',
|
|
106
|
+
daemonHint = '',
|
|
107
|
+
agentHint = '',
|
|
108
|
+
macAutomationHint = '',
|
|
109
|
+
summaryHint = '',
|
|
110
|
+
memoryHint = '',
|
|
111
|
+
mentorHint = '',
|
|
112
|
+
langGuard = '',
|
|
113
|
+
}) {
|
|
114
|
+
return warmEntry
|
|
115
|
+
? routedPrompt + intentHint
|
|
116
|
+
: routedPrompt + daemonHint + intentHint + agentHint + macAutomationHint + summaryHint + memoryHint + mentorHint + langGuard;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
adaptDaemonHintForEngine,
|
|
121
|
+
buildAgentHint,
|
|
122
|
+
buildDaemonHint,
|
|
123
|
+
buildMacAutomationHint,
|
|
124
|
+
buildLanguageGuard,
|
|
125
|
+
buildIntentHint,
|
|
126
|
+
composePrompt,
|
|
127
|
+
};
|
|
@@ -258,6 +258,79 @@ function runProjectVerifier(projectKey, config, deps) {
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
function sanitizeQueueId(id) {
|
|
262
|
+
return String(id || '').replace(/[^a-zA-Z0-9_-]/g, '');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function createMissionStartPrompt(title) {
|
|
266
|
+
return `New mission: "${title}"\n\nStart this mission. Read your CLAUDE.md for instructions, then decide on the first step using NEXT_DISPATCH.`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function loadMissionQueueState(projectKey, projectCwd, deps) {
|
|
270
|
+
const manifest = loadProjectManifest(projectCwd);
|
|
271
|
+
const scripts = resolveProjectScripts(projectCwd, manifest);
|
|
272
|
+
if (!fs.existsSync(scripts.missionQueue)) {
|
|
273
|
+
return { manifest, scripts, listResult: null, activeMission: null, nextMission: null };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const relQueue = path.relative(projectCwd, scripts.missionQueue);
|
|
277
|
+
const queueEnv = { ...process.env, MISSION_CWD: projectCwd, TOPICS_CWD: projectCwd };
|
|
278
|
+
let listResult = null;
|
|
279
|
+
try {
|
|
280
|
+
const listOut = execSync(`node "${relQueue}" list`, {
|
|
281
|
+
cwd: projectCwd, encoding: 'utf8', timeout: 10000, env: queueEnv,
|
|
282
|
+
}).trim();
|
|
283
|
+
listResult = JSON.parse(listOut);
|
|
284
|
+
} catch (e) {
|
|
285
|
+
deps.log('WARN', `Reactive: mission queue list failed for ${projectKey}: ${e.message}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const topics = Array.isArray(listResult && listResult.topics) ? listResult.topics : [];
|
|
289
|
+
return {
|
|
290
|
+
manifest,
|
|
291
|
+
scripts,
|
|
292
|
+
relQueue,
|
|
293
|
+
queueEnv,
|
|
294
|
+
listResult,
|
|
295
|
+
activeMission: topics.find(t => t.status === 'active') || null,
|
|
296
|
+
nextMission: topics.filter(t => t.status === 'pending').sort((a, b) => (a.priority || 999) - (b.priority || 999))[0] || null,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function activateQueuedMission(projectKey, projectCwd, deps) {
|
|
301
|
+
const queueState = loadMissionQueueState(projectKey, projectCwd, deps);
|
|
302
|
+
const { scripts, relQueue, queueEnv } = queueState;
|
|
303
|
+
if (!scripts || !fs.existsSync(scripts.missionQueue)) {
|
|
304
|
+
return { mission: null, prompt: null, missionId: null };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (queueState.activeMission) {
|
|
308
|
+
return {
|
|
309
|
+
mission: queueState.activeMission.title,
|
|
310
|
+
missionId: queueState.activeMission.id || '',
|
|
311
|
+
prompt: createMissionStartPrompt(queueState.activeMission.title),
|
|
312
|
+
reusedActive: true,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!queueState.nextMission) return { mission: null, prompt: null, missionId: null };
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
execSync(`node "${relQueue}" activate ${sanitizeQueueId(queueState.nextMission.id)}`, {
|
|
320
|
+
cwd: projectCwd, encoding: 'utf8', timeout: 10000, env: queueEnv,
|
|
321
|
+
});
|
|
322
|
+
} catch (e) {
|
|
323
|
+
deps.log('WARN', `Reactive: mission activate failed for ${projectKey}: ${e.message}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
mission: queueState.nextMission.title,
|
|
328
|
+
missionId: queueState.nextMission.id || '',
|
|
329
|
+
prompt: createMissionStartPrompt(queueState.nextMission.title),
|
|
330
|
+
reusedActive: false,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
261
334
|
/**
|
|
262
335
|
* Run project-level completion hooks (archive + topic pool).
|
|
263
336
|
* Platform only calls scripts if they exist — no business logic here.
|
|
@@ -298,8 +371,6 @@ function runCompletionHooks(projectKey, projectCwd, deps) {
|
|
|
298
371
|
if (result.archived && fs.existsSync(scripts.missionQueue)) {
|
|
299
372
|
const relQueue = path.relative(projectCwd, scripts.missionQueue);
|
|
300
373
|
const queueEnv = { ...process.env, MISSION_CWD: projectCwd, TOPICS_CWD: projectCwd };
|
|
301
|
-
// Sanitize topic IDs to prevent shell injection (only allow alphanumeric, dash, underscore)
|
|
302
|
-
const sanitizeId = (id) => String(id || '').replace(/[^a-zA-Z0-9_-]/g, '');
|
|
303
374
|
// 2a. Complete current active mission
|
|
304
375
|
try {
|
|
305
376
|
const listOut = execSync(`node "${relQueue}" list`, {
|
|
@@ -309,7 +380,7 @@ function runCompletionHooks(projectKey, projectCwd, deps) {
|
|
|
309
380
|
if (listResult.success && Array.isArray(listResult.topics)) {
|
|
310
381
|
const activeTopic = listResult.topics.find(t => t.status === 'active');
|
|
311
382
|
if (activeTopic) {
|
|
312
|
-
execSync(`node "${relQueue}" complete ${
|
|
383
|
+
execSync(`node "${relQueue}" complete ${sanitizeQueueId(activeTopic.id)}`, {
|
|
313
384
|
cwd: projectCwd, encoding: 'utf8', timeout: 10000, env: queueEnv,
|
|
314
385
|
});
|
|
315
386
|
deps.log('INFO', `Reactive: completed mission ${activeTopic.id}: ${activeTopic.title}`);
|
|
@@ -326,7 +397,7 @@ function runCompletionHooks(projectKey, projectCwd, deps) {
|
|
|
326
397
|
const nextResult = JSON.parse(nextOut);
|
|
327
398
|
if (nextResult.success && nextResult.topic) {
|
|
328
399
|
try {
|
|
329
|
-
execSync(`node "${relQueue}" activate ${
|
|
400
|
+
execSync(`node "${relQueue}" activate ${sanitizeQueueId(nextResult.topic.id)}`, {
|
|
330
401
|
cwd: projectCwd, encoding: 'utf8', timeout: 10000, env: queueEnv,
|
|
331
402
|
});
|
|
332
403
|
} catch (e) {
|
|
@@ -334,7 +405,7 @@ function runCompletionHooks(projectKey, projectCwd, deps) {
|
|
|
334
405
|
}
|
|
335
406
|
result.nextMission = nextResult.topic.title;
|
|
336
407
|
result.nextMissionId = nextResult.topic.id || '';
|
|
337
|
-
result.nextMissionPrompt =
|
|
408
|
+
result.nextMissionPrompt = createMissionStartPrompt(nextResult.topic.title);
|
|
338
409
|
deps.log('INFO', `Reactive: next mission for ${projectKey}: ${nextResult.topic.title}`);
|
|
339
410
|
}
|
|
340
411
|
} catch (e) {
|
|
@@ -347,6 +418,66 @@ function runCompletionHooks(projectKey, projectCwd, deps) {
|
|
|
347
418
|
return result;
|
|
348
419
|
}
|
|
349
420
|
|
|
421
|
+
function bootstrapReactiveProject(projectKey, config, deps) {
|
|
422
|
+
if (!isReactiveParent(projectKey, config)) {
|
|
423
|
+
return { started: false, reason: 'not_reactive' };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const st = deps.loadState();
|
|
427
|
+
const rs = getReactiveState(st, projectKey);
|
|
428
|
+
if (!deps.checkBudget(config, st)) {
|
|
429
|
+
setReactiveStatus(st, projectKey, 'paused', 'budget_exceeded');
|
|
430
|
+
deps.saveState(st);
|
|
431
|
+
appendEvent(projectKey, { type: 'BUDGET_LIMIT', action: 'bootstrap_skip' }, deps.metameDir);
|
|
432
|
+
return { started: false, reason: 'budget_exceeded' };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const staleMinutes = config.projects?.[projectKey]?.stale_timeout_minutes || 120;
|
|
436
|
+
const lastUpdateMs = new Date(rs.updated_at || 0).getTime();
|
|
437
|
+
const recentlyUpdated = Number.isFinite(lastUpdateMs) && (Date.now() - lastUpdateMs) < staleMinutes * 60 * 1000;
|
|
438
|
+
if (rs.status === 'running' && isReactiveExecutionActive(projectKey, config, deps)) {
|
|
439
|
+
return { started: false, reason: 'already_running' };
|
|
440
|
+
}
|
|
441
|
+
if (rs.status === 'running' && recentlyUpdated) {
|
|
442
|
+
return { started: false, reason: 'recently_running' };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const projectCwd = resolveProjectCwd(projectKey, config);
|
|
446
|
+
if (!projectCwd) return { started: false, reason: 'cwd_missing' };
|
|
447
|
+
|
|
448
|
+
const activation = activateQueuedMission(projectKey, projectCwd, deps);
|
|
449
|
+
if (!activation.mission || !activation.prompt) {
|
|
450
|
+
setReactiveStatus(st, projectKey, 'idle', '');
|
|
451
|
+
rs.depth = 0;
|
|
452
|
+
deps.saveState(st);
|
|
453
|
+
return { started: false, reason: 'no_pending_mission' };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
setReactiveStatus(st, projectKey, 'running', '');
|
|
457
|
+
rs.depth = 0;
|
|
458
|
+
rs.last_signal = 'MISSION_START';
|
|
459
|
+
deps.saveState(st);
|
|
460
|
+
appendEvent(projectKey, {
|
|
461
|
+
type: 'MISSION_START',
|
|
462
|
+
mission_id: activation.missionId || '',
|
|
463
|
+
mission_title: activation.mission,
|
|
464
|
+
bootstrap: true,
|
|
465
|
+
reused_active: !!activation.reusedActive,
|
|
466
|
+
}, deps.metameDir);
|
|
467
|
+
try { generateStateFile(projectKey, config, deps); } catch { /* non-critical */ }
|
|
468
|
+
|
|
469
|
+
deps.handleDispatchItem({
|
|
470
|
+
target: projectKey,
|
|
471
|
+
prompt: activation.prompt,
|
|
472
|
+
from: '_system',
|
|
473
|
+
_reactive: true,
|
|
474
|
+
_reactive_project: projectKey,
|
|
475
|
+
new_session: true,
|
|
476
|
+
}, config);
|
|
477
|
+
|
|
478
|
+
return { started: true, mission: activation.mission, missionId: activation.missionId || '' };
|
|
479
|
+
}
|
|
480
|
+
|
|
350
481
|
// ── Event Log (Event Sourcing) ──────────────────────────────────
|
|
351
482
|
|
|
352
483
|
/**
|
|
@@ -1195,9 +1326,10 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
1195
1326
|
}
|
|
1196
1327
|
|
|
1197
1328
|
module.exports = {
|
|
1329
|
+
bootstrapReactiveProject,
|
|
1198
1330
|
handleReactiveOutput,
|
|
1199
1331
|
parseReactiveSignals,
|
|
1200
1332
|
reconcilePerpetualProjects,
|
|
1201
1333
|
replayEventLog,
|
|
1202
|
-
__test: { runProjectVerifier, readPhaseFromState, resolveProjectCwd, appendEvent, projectProgressTsv, generateStateFile, loadProjectManifest, resolveProjectScripts, parseEventLog, buildRunningMemory, scanRelevantArtifacts, buildWorkingMemory, persistMemoryFiles, extractInlineFacts, extractOutputSummary, isReactiveExecutionActive, loadWorkingMemory },
|
|
1334
|
+
__test: { runProjectVerifier, readPhaseFromState, resolveProjectCwd, appendEvent, projectProgressTsv, generateStateFile, loadProjectManifest, resolveProjectScripts, parseEventLog, buildRunningMemory, scanRelevantArtifacts, buildWorkingMemory, persistMemoryFiles, extractInlineFacts, extractOutputSummary, isReactiveExecutionActive, loadWorkingMemory, activateQueuedMission, createMissionStartPrompt, sanitizeQueueId, loadMissionQueueState },
|
|
1203
1335
|
};
|