agent-orchestrator-kit 0.1.13 → 0.2.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/CHANGELOG.md +16 -0
- package/README.md +18 -11
- package/bin/agent-orchestrator.js +558 -2
- package/package.json +2 -2
- package/profiles/generic/orchestrator.yaml +3 -0
- package/profiles/mvp/orchestrator.yaml +3 -0
- package/profiles/node/orchestrator.yaml +3 -0
- package/profiles/vue3/orchestrator.yaml +3 -0
- package/templates/.agents/amp.settings.json.example +2 -5
- package/templates/.agents/commands/opsx-apply.md +10 -3
- package/templates/.agents/commands/opsx-archive.md +10 -3
- package/templates/.agents/commands/opsx-design.md +10 -3
- package/templates/.agents/commands/opsx-explore.md +10 -3
- package/templates/.agents/commands/opsx-propose.md +10 -3
- package/templates/.agents/commands/opsx-quick.md +10 -3
- package/templates/.agents/commands/opsx-review.md +10 -3
- package/templates/.agents/mcp.json.example +2 -5
- package/templates/.agents/rules/agent-orchestration.mdc +28 -68
- package/templates/.agents/rules/cli-via-npm.mdc +2 -1
- package/templates/.agents/rules/figma-token-setup.mdc +1 -1
- package/templates/.agents/rules/memory-mcp-autosetup.mdc +4 -54
- package/templates/.agents/rules/session-handoff.mdc +28 -0
- package/templates/.agents/skills/agent-orchestration/SKILL.md +36 -19
- package/templates/.agents/subagents/session-handoff.md +48 -0
- package/templates/.agents/subagents/setup-doctor.md +1 -1
- package/templates/AGENTS.md +22 -119
- package/templates/CLAUDE.md +6 -65
- package/templates/orchestrator.yaml +6 -0
- package/templates/scripts/memory-mcp-launcher.cjs +46 -0
- package/templates/scripts/sync-local-agent-skills.sh +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { program } from 'commander';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import { readFileSync, existsSync, mkdirSync, copyFileSync, readdirSync, statSync, writeFileSync, rmSync } from 'fs';
|
|
5
|
-
import { join, dirname, basename } from 'path';
|
|
5
|
+
import { join, dirname, basename, resolve } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
8
|
|
|
@@ -71,12 +71,35 @@ const GITIGNORE_LINES = [
|
|
|
71
71
|
const FIGMA_ENV_REL = join('.agents', 'figma.local.env');
|
|
72
72
|
const FIGMA_ENV_EXAMPLE_REL = join('.agents', 'figma.local.env.example');
|
|
73
73
|
const FIGMA_LAUNCHER_REL = join('scripts', 'figma-mcp-launcher.cjs');
|
|
74
|
+
const MEMORY_LAUNCHER_REL = join('scripts', 'memory-mcp-launcher.cjs');
|
|
75
|
+
const MEMORY_FILE_REL = join('.cursor', 'memory.json');
|
|
74
76
|
const FIGMA_MANAGED_PATHS = [
|
|
75
77
|
FIGMA_ENV_EXAMPLE_REL,
|
|
76
78
|
FIGMA_LAUNCHER_REL,
|
|
77
79
|
join('.agents', 'mcp.json.example'),
|
|
78
80
|
join('.agents', 'amp.settings.json.example'),
|
|
79
81
|
];
|
|
82
|
+
const MEMORY_MANAGED_PATHS = [
|
|
83
|
+
MEMORY_LAUNCHER_REL,
|
|
84
|
+
join('.agents', 'mcp.json.example'),
|
|
85
|
+
join('.agents', 'amp.settings.json.example'),
|
|
86
|
+
];
|
|
87
|
+
const AMP_SPAWN_PREAMBLE =
|
|
88
|
+
'CRITICAL (Amp / Cursor / Claude): Parent MUST spawn this skill as an isolated subagent with fresh context. Do not execute it in the main thread. If spawn is unavailable, STOP and report blocked — do not perform this specialist\'s work in the parent. Return only the structured subagent report.';
|
|
89
|
+
const HANDOFF_REQUIRED_SECTIONS = ['Closed role', 'Done', 'Next command'];
|
|
90
|
+
const HANDOFF_SECTIONS = [
|
|
91
|
+
'Closed role',
|
|
92
|
+
'Change',
|
|
93
|
+
'Done',
|
|
94
|
+
'Decisions',
|
|
95
|
+
'Blocked',
|
|
96
|
+
'Next command',
|
|
97
|
+
'Next role',
|
|
98
|
+
'Attach',
|
|
99
|
+
'Subagents to spawn',
|
|
100
|
+
'Constraints',
|
|
101
|
+
'Prompt',
|
|
102
|
+
];
|
|
80
103
|
|
|
81
104
|
const log = {
|
|
82
105
|
info: (msg) => console.log(pc.cyan(' →'), msg),
|
|
@@ -237,6 +260,396 @@ function ensureFigmaMcpEntry(projectDir) {
|
|
|
237
260
|
}
|
|
238
261
|
}
|
|
239
262
|
|
|
263
|
+
function memoryServerConfig() {
|
|
264
|
+
return { command: 'node', args: [MEMORY_LAUNCHER_REL.replace(/\\/g, '/')] };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function isMemoryLauncher(server) {
|
|
268
|
+
const args = server?.args || [];
|
|
269
|
+
return server?.command === 'node' && args.some((arg) => String(arg).includes('memory-mcp-launcher.cjs'));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function refreshMemoryManagedFiles(projectDir) {
|
|
273
|
+
const templateDir = join(KIT_ROOT, 'templates');
|
|
274
|
+
for (const rel of MEMORY_MANAGED_PATHS) {
|
|
275
|
+
const src = join(templateDir, rel);
|
|
276
|
+
const dest = join(projectDir, rel);
|
|
277
|
+
if (!existsSync(src)) continue;
|
|
278
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
279
|
+
copyFileSync(src, dest);
|
|
280
|
+
log.ok(rel);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function writeJsonFile(filePath, value) {
|
|
285
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
286
|
+
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function upsertMemoryServer(cfg, key, label) {
|
|
290
|
+
const servers = cfg[key] || {};
|
|
291
|
+
const current = servers.memory;
|
|
292
|
+
if (isMemoryLauncher(current)) {
|
|
293
|
+
log.ok(`${label} already uses memory launcher`);
|
|
294
|
+
cfg[key] = servers;
|
|
295
|
+
return cfg;
|
|
296
|
+
}
|
|
297
|
+
servers.memory = memoryServerConfig();
|
|
298
|
+
cfg[key] = servers;
|
|
299
|
+
log.ok(`${label} ← memory launcher (absolute MEMORY_FILE_PATH)`);
|
|
300
|
+
return cfg;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function ensureMemoryMcpEntry(projectDir) {
|
|
304
|
+
mkdirSync(join(projectDir, '.cursor'), { recursive: true });
|
|
305
|
+
const memoryFile = join(projectDir, MEMORY_FILE_REL);
|
|
306
|
+
if (!existsSync(memoryFile)) {
|
|
307
|
+
writeFileSync(memoryFile, '');
|
|
308
|
+
log.ok('.cursor/memory.json created');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const examplePath = join(projectDir, '.agents', 'mcp.json.example');
|
|
312
|
+
const cursorPath = join(projectDir, '.mcp.json');
|
|
313
|
+
if (!existsSync(cursorPath) && existsSync(examplePath)) {
|
|
314
|
+
copyFileSync(examplePath, cursorPath);
|
|
315
|
+
log.ok('.mcp.json created from example');
|
|
316
|
+
}
|
|
317
|
+
if (existsSync(cursorPath)) {
|
|
318
|
+
try {
|
|
319
|
+
const cfg = upsertMemoryServer(JSON.parse(readFileSync(cursorPath, 'utf-8')), 'mcpServers', '.mcp.json');
|
|
320
|
+
writeJsonFile(cursorPath, cfg);
|
|
321
|
+
} catch {
|
|
322
|
+
log.warn('.mcp.json present but invalid JSON — merge memory launcher from .agents/mcp.json.example');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
mkdirSync(join(projectDir, '.amp'), { recursive: true });
|
|
327
|
+
const ampExample = join(projectDir, '.agents', 'amp.settings.json.example');
|
|
328
|
+
const ampPath = join(projectDir, '.amp', 'settings.json');
|
|
329
|
+
if (!existsSync(ampPath) && existsSync(ampExample)) {
|
|
330
|
+
copyFileSync(ampExample, ampPath);
|
|
331
|
+
log.ok('.amp/settings.json created from example');
|
|
332
|
+
}
|
|
333
|
+
if (existsSync(ampPath)) {
|
|
334
|
+
try {
|
|
335
|
+
const cfg = upsertMemoryServer(JSON.parse(readFileSync(ampPath, 'utf-8')), 'amp.mcpServers', '.amp/settings.json');
|
|
336
|
+
writeJsonFile(ampPath, cfg);
|
|
337
|
+
} catch {
|
|
338
|
+
log.warn('.amp/settings.json present but invalid JSON — merge memory launcher manually');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function readOrchestratorMeta(projectDir) {
|
|
344
|
+
const orchPath = join(projectDir, '.agents', 'orchestrator.yaml');
|
|
345
|
+
if (!existsSync(orchPath)) return { agentLanguage: 'en' };
|
|
346
|
+
const content = readFileSync(orchPath, 'utf-8');
|
|
347
|
+
const lang = content.match(/agent_language:\s*["']?([A-Za-z_-]+)/);
|
|
348
|
+
return { agentLanguage: lang ? lang[1] : 'en' };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function parseHandoffMarkdown(content) {
|
|
352
|
+
const sections = {};
|
|
353
|
+
const parts = String(content || '').split(/^## /m);
|
|
354
|
+
for (const part of parts.slice(1)) {
|
|
355
|
+
const nl = part.indexOf('\n');
|
|
356
|
+
const title = (nl === -1 ? part : part.slice(0, nl)).trim();
|
|
357
|
+
const body = nl === -1 ? '' : part.slice(nl + 1).trim();
|
|
358
|
+
sections[title] = body;
|
|
359
|
+
}
|
|
360
|
+
return sections;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function firstLineCommand(value) {
|
|
364
|
+
const match = String(value || '').match(/\/opsx:[^\s`]+(?:\s+[^\s`]+)?/);
|
|
365
|
+
if (match) return match[0].trim();
|
|
366
|
+
return String(value || '').replace(/^[`\s]+|[`\s]+$/g, '').split('\n')[0].trim();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function firstSpawnName(value) {
|
|
370
|
+
const tick = String(value || '').match(/`([a-z0-9-]+)`/i);
|
|
371
|
+
if (tick) return tick[1];
|
|
372
|
+
const word = String(value || '').match(/\b([a-z][a-z0-9-]{2,})\b/i);
|
|
373
|
+
return word ? word[1] : '';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function sectionOr(sections, title, fallback = '') {
|
|
377
|
+
const value = sections[title];
|
|
378
|
+
return value && value.trim() ? value.trim() : fallback;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function buildHandoffMarkdown(fields) {
|
|
382
|
+
const prompt = fields.prompt ? `\n\n## Prompt\n\n\`\`\`text\n${fields.prompt}\n\`\`\`` : '';
|
|
383
|
+
return `# Session Handoff
|
|
384
|
+
|
|
385
|
+
## Closed role
|
|
386
|
+
${fields.closedRole}
|
|
387
|
+
|
|
388
|
+
## Change
|
|
389
|
+
${fields.change}
|
|
390
|
+
|
|
391
|
+
## Done
|
|
392
|
+
${fields.done}
|
|
393
|
+
|
|
394
|
+
## Decisions
|
|
395
|
+
${fields.decisions}
|
|
396
|
+
|
|
397
|
+
## Blocked
|
|
398
|
+
${fields.blocked}
|
|
399
|
+
|
|
400
|
+
## Next command
|
|
401
|
+
\`${fields.nextCommand}\`
|
|
402
|
+
|
|
403
|
+
## Next role
|
|
404
|
+
${fields.nextRole}
|
|
405
|
+
|
|
406
|
+
## Attach
|
|
407
|
+
${fields.attach}
|
|
408
|
+
|
|
409
|
+
## Subagents to spawn
|
|
410
|
+
${fields.spawn}
|
|
411
|
+
|
|
412
|
+
## Constraints
|
|
413
|
+
${fields.constraints}${prompt}
|
|
414
|
+
`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function fieldsFromSections(changeName, sections, extra = {}) {
|
|
418
|
+
const nextCommand = extra.nextCommand || firstLineCommand(sectionOr(sections, 'Next command'));
|
|
419
|
+
return {
|
|
420
|
+
changeName,
|
|
421
|
+
closedRole: extra.closedRole || sectionOr(sections, 'Closed role', extra.closedRole || ''),
|
|
422
|
+
change: extra.change || sectionOr(sections, 'Change', `- name: ${changeName}`),
|
|
423
|
+
done: extra.done || extra.summary || sectionOr(sections, 'Done', extra.done || ''),
|
|
424
|
+
decisions: extra.decisions || sectionOr(sections, 'Decisions', 'none'),
|
|
425
|
+
blocked: extra.blocked || sectionOr(sections, 'Blocked', 'none'),
|
|
426
|
+
nextCommand,
|
|
427
|
+
nextRole: extra.nextRole || sectionOr(sections, 'Next role', ''),
|
|
428
|
+
attach: extra.attach || sectionOr(sections, 'Attach', `- \`openspec/changes/${changeName}/\``),
|
|
429
|
+
spawn: extra.spawn || sectionOr(sections, 'Subagents to spawn', ''),
|
|
430
|
+
constraints: extra.constraints || sectionOr(sections, 'Constraints', ''),
|
|
431
|
+
status: extra.status || '',
|
|
432
|
+
tasks: extra.tasks || '',
|
|
433
|
+
review: extra.review || '',
|
|
434
|
+
sessionCount: extra.sessionCount || '',
|
|
435
|
+
summary: extra.summary || extra.done || sectionOr(sections, 'Done', ''),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function missingHandoffFields(fields) {
|
|
440
|
+
const missing = [];
|
|
441
|
+
if (!fields.closedRole) missing.push('Closed role');
|
|
442
|
+
if (!fields.done) missing.push('Done');
|
|
443
|
+
if (!fields.nextCommand) missing.push('Next command');
|
|
444
|
+
return missing;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function isUkLang(lang) {
|
|
448
|
+
const value = String(lang || 'en').toLowerCase();
|
|
449
|
+
return value === 'uk' || value.startsWith('uk');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function buildNextSessionPrompt(fields, agentLanguage) {
|
|
453
|
+
const name = fields.changeName;
|
|
454
|
+
const cmd = firstLineCommand(fields.nextCommand);
|
|
455
|
+
const spawnName = firstSpawnName(fields.spawn) || firstSpawnName(fields.nextRole);
|
|
456
|
+
const ampWrapper = spawnName ? `subagent-${spawnName}` : 'subagent-<phase-specialist>';
|
|
457
|
+
const uk = isUkLang(agentLanguage);
|
|
458
|
+
const languageName = uk ? 'українська' : 'English';
|
|
459
|
+
|
|
460
|
+
if (uk) {
|
|
461
|
+
return `${cmd}
|
|
462
|
+
|
|
463
|
+
Ти — conductor наступної рольової сесії для зміни \`${name}\`.
|
|
464
|
+
Мова відповіді: ${languageName} (\`project.agent_language: ${agentLanguage}\`).
|
|
465
|
+
НЕ змішуй фази. НЕ починай наступну роль у цьому ж чаті, доки ця фаза не закрита за HARD STOP.
|
|
466
|
+
|
|
467
|
+
## Хто ти і що робити
|
|
468
|
+
- Команда цієї сесії: \`${cmd}\`
|
|
469
|
+
- Наступна роль / субагент фази: \`${spawnName || fields.nextRole || 'див. таблицю маршрутизації'}\`
|
|
470
|
+
- Amp: заспавни isolated skill \`${ampWrapper}\` зі свіжим контекстом. Виконувати тіло спеціаліста в головному треді Amp — порушення протоколу.
|
|
471
|
+
- Cursor / Claude: заспавни \`.cursor/agents/${spawnName || '<name>'}.md\` / \`.claude/agents/${spawnName || '<name>'}.md\`.
|
|
472
|
+
- Батьківська сесія — лише conductor: перевіряє звіт, не виконує роботу спеціаліста.
|
|
473
|
+
|
|
474
|
+
## Обов'язковий старт (до будь-якої роботи спеціаліста)
|
|
475
|
+
1. Виконай pasted-команду \`${cmd}\` і оголоси роль.
|
|
476
|
+
2. \`npx agent-orchestrator-kit status\`
|
|
477
|
+
3. \`npx agent-orchestrator-kit handoff ${name} --restore\`
|
|
478
|
+
4. Прочитай Memory MCP: \`Change:${name}\`, \`Handoff:${name}\`, \`Decision:*\`.
|
|
479
|
+
5. Якщо Memory порожнє або MCP недоступний — прочитай \`openspec/changes/${name}/handoff.md\`. Відсутність Memory НЕ блокує сесію, коли є файл.
|
|
480
|
+
6. Заспавни \`session-handoff\` у режимі restore, якщо брифінг неповний (Amp: isolated \`subagent-session-handoff\`).
|
|
481
|
+
7. Лише після цього заспавни субагента фази. Free-form «продовжуй» / «далі» при одній активній зміні = \`Handoff.next_command\`.
|
|
482
|
+
|
|
483
|
+
## Повний контекст попередньої сесії (самодостатній — не покладайся лише на Memory)
|
|
484
|
+
- Закрита роль: ${fields.closedRole || 'не вказано'}
|
|
485
|
+
- Зміна: ${fields.change || name}
|
|
486
|
+
- Зроблено:
|
|
487
|
+
${fields.done || 'не вказано'}
|
|
488
|
+
- Рішення:
|
|
489
|
+
${fields.decisions || 'none'}
|
|
490
|
+
- Блокери:
|
|
491
|
+
${fields.blocked || 'none'}
|
|
492
|
+
- Attach:
|
|
493
|
+
${fields.attach || `- \`openspec/changes/${name}/\``}
|
|
494
|
+
- Субагенти цієї сесії:
|
|
495
|
+
${fields.spawn || `- \`${spawnName || 'phase specialist'}\``}
|
|
496
|
+
- Обмеження:
|
|
497
|
+
${fields.constraints || 'не змішувати фази; не писати поза дозволеними шляхами ролі'}
|
|
498
|
+
${fields.status ? `- status: ${fields.status}` : ''}
|
|
499
|
+
${fields.tasks ? `- tasks: ${fields.tasks}` : ''}
|
|
500
|
+
${fields.review ? `- review: ${fields.review}` : ''}
|
|
501
|
+
|
|
502
|
+
## HARD STOP на виході (ти НЕ закінчив, поки це не виконано)
|
|
503
|
+
1. Заспавни \`session-handoff\` у режимі persist (Amp: isolated \`subagent-session-handoff\`). Якщо spawn недоступний — зроби persist сам, ніколи не пропускай.
|
|
504
|
+
2. Запиши \`openspec/changes/${name}/handoff.md\` з усіма секціями шаблону.
|
|
505
|
+
3. \`npx agent-orchestrator-kit handoff ${name}\` — exit 0 обов'язковий. CLI записує Memory JSON абсолютним шляхом і друкує розширений промпт у stdout.
|
|
506
|
+
4. Якщо Memory MCP живий — онови \`Change:${name}\`, \`Handoff:${name}\`, \`Decision:*\` відповідно до файлу.
|
|
507
|
+
5. Встав stdout CLI у чат одним fenced-блоком. Не скорочуй. Без службового ярлика. Перший рядок — \`/opsx:…\`.
|
|
508
|
+
6. Зупинись. Наступна роль починається в НОВОМУ чаті з цим промптом.
|
|
509
|
+
|
|
510
|
+
OpenSpec-файли — source of truth для вимог і тасків. Memory і handoff.md — індекс фази. Цей промпт — повний операційний бриф наступного треду, навіть якщо Amp проігнорує Memory MCP.`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return `${cmd}
|
|
514
|
+
|
|
515
|
+
You are the conductor for the next role session of change \`${name}\`.
|
|
516
|
+
Reply language: ${languageName} (\`project.agent_language: ${agentLanguage}\`).
|
|
517
|
+
Do not mix phases. Do not start the following role in this chat until this phase is closed via HARD STOP.
|
|
518
|
+
|
|
519
|
+
## Who you are and what to do
|
|
520
|
+
- This session command: \`${cmd}\`
|
|
521
|
+
- Next role / phase subagent: \`${spawnName || fields.nextRole || 'see routing table'}\`
|
|
522
|
+
- Amp: spawn isolated skill \`${ampWrapper}\` with fresh context. Running the specialist body in Amp's main thread is a protocol violation.
|
|
523
|
+
- Cursor / Claude: spawn \`.cursor/agents/${spawnName || '<name>'}.md\` / \`.claude/agents/${spawnName || '<name>'}.md\`.
|
|
524
|
+
- The parent session is conductor-only: verify the report, do not do the specialist's work.
|
|
525
|
+
|
|
526
|
+
## Mandatory start (before any specialist work)
|
|
527
|
+
1. Honor the pasted \`${cmd}\` command and announce the role.
|
|
528
|
+
2. \`npx agent-orchestrator-kit status\`
|
|
529
|
+
3. \`npx agent-orchestrator-kit handoff ${name} --restore\`
|
|
530
|
+
4. Read Memory MCP: \`Change:${name}\`, \`Handoff:${name}\`, \`Decision:*\`.
|
|
531
|
+
5. If Memory is empty or MCP is down, read \`openspec/changes/${name}/handoff.md\`. Missing Memory does not block the session when the file exists.
|
|
532
|
+
6. Spawn \`session-handoff\` in restore mode if the briefing is incomplete (Amp: isolated \`subagent-session-handoff\`).
|
|
533
|
+
7. Only then spawn the phase specialist. Free-form "continue" / "next" with one active change means \`Handoff.next_command\`.
|
|
534
|
+
|
|
535
|
+
## Full previous-session context (self-contained — do not rely on Memory alone)
|
|
536
|
+
- Closed role: ${fields.closedRole || 'not set'}
|
|
537
|
+
- Change: ${fields.change || name}
|
|
538
|
+
- Done:
|
|
539
|
+
${fields.done || 'not set'}
|
|
540
|
+
- Decisions:
|
|
541
|
+
${fields.decisions || 'none'}
|
|
542
|
+
- Blocked:
|
|
543
|
+
${fields.blocked || 'none'}
|
|
544
|
+
- Attach:
|
|
545
|
+
${fields.attach || `- \`openspec/changes/${name}/\``}
|
|
546
|
+
- Subagents for this session:
|
|
547
|
+
${fields.spawn || `- \`${spawnName || 'phase specialist'}\``}
|
|
548
|
+
- Constraints:
|
|
549
|
+
${fields.constraints || 'do not mix phases; do not write outside the role allowed paths'}
|
|
550
|
+
${fields.status ? `- status: ${fields.status}` : ''}
|
|
551
|
+
${fields.tasks ? `- tasks: ${fields.tasks}` : ''}
|
|
552
|
+
${fields.review ? `- review: ${fields.review}` : ''}
|
|
553
|
+
|
|
554
|
+
## Exit HARD STOP (you are NOT done until this succeeds)
|
|
555
|
+
1. Spawn \`session-handoff\` in persist mode (Amp: isolated \`subagent-session-handoff\`). If spawn is unavailable, persist yourself — never skip.
|
|
556
|
+
2. Write \`openspec/changes/${name}/handoff.md\` with every template section.
|
|
557
|
+
3. \`npx agent-orchestrator-kit handoff ${name}\` — exit 0 is required. The CLI upserts Memory JSON with an absolute path and prints the expanded prompt on stdout.
|
|
558
|
+
4. If Memory MCP tools work, also update \`Change:${name}\`, \`Handoff:${name}\`, \`Decision:*\` to match the file.
|
|
559
|
+
5. Paste CLI stdout into chat as one fenced block. Do not shorten it. No service banner. First line is \`/opsx:…\`.
|
|
560
|
+
6. Stop. The next role starts in a NEW chat with that prompt.
|
|
561
|
+
|
|
562
|
+
OpenSpec files are the source of truth for requirements and tasks. Memory and handoff.md index the phase. This prompt is the next thread's full operating brief even if Amp ignores Memory MCP.`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function loadMemoryItems(filePath) {
|
|
566
|
+
if (!existsSync(filePath)) return [];
|
|
567
|
+
const raw = readFileSync(filePath, 'utf-8').trim();
|
|
568
|
+
if (!raw) return [];
|
|
569
|
+
if (raw.startsWith('{')) {
|
|
570
|
+
try {
|
|
571
|
+
const parsed = JSON.parse(raw);
|
|
572
|
+
const entities = (parsed.entities || []).map((entity) => ({ type: 'entity', ...entity }));
|
|
573
|
+
const relations = (parsed.relations || []).map((relation) => ({ type: 'relation', ...relation }));
|
|
574
|
+
return [...entities, ...relations];
|
|
575
|
+
} catch {
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return raw
|
|
580
|
+
.split('\n')
|
|
581
|
+
.map((line) => line.trim())
|
|
582
|
+
.filter(Boolean)
|
|
583
|
+
.map((line) => JSON.parse(line));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function saveMemoryItems(filePath, items) {
|
|
587
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
588
|
+
const body = items.map((item) => JSON.stringify(item)).join('\n');
|
|
589
|
+
writeFileSync(filePath, body ? `${body}\n` : '');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function upsertMemoryEntity(items, name, entityType, observations) {
|
|
593
|
+
const entity = { type: 'entity', name, entityType, observations };
|
|
594
|
+
const idx = items.findIndex((item) => item.type === 'entity' && item.name === name);
|
|
595
|
+
if (idx >= 0) items[idx] = entity;
|
|
596
|
+
else items.push(entity);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function parseDecisionItems(text) {
|
|
600
|
+
return String(text || '')
|
|
601
|
+
.split('\n')
|
|
602
|
+
.map((line) => line.replace(/^\s*[-*]\s*/, '').trim())
|
|
603
|
+
.filter((line) => line && !/^none$/i.test(line));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function persistMemoryFromHandoff(projectDir, fields) {
|
|
607
|
+
const filePath = resolve(projectDir, MEMORY_FILE_REL);
|
|
608
|
+
const items = loadMemoryItems(filePath);
|
|
609
|
+
const name = fields.changeName;
|
|
610
|
+
const changeObs = [
|
|
611
|
+
fields.status ? `status: ${fields.status}` : null,
|
|
612
|
+
fields.tasks ? `tasks: ${fields.tasks}` : null,
|
|
613
|
+
fields.closedRole ? `last_role: ${fields.closedRole}` : null,
|
|
614
|
+
fields.review ? `review: ${fields.review}` : null,
|
|
615
|
+
fields.summary ? `summary: ${fields.summary}` : null,
|
|
616
|
+
].filter(Boolean);
|
|
617
|
+
if (changeObs.length) upsertMemoryEntity(items, `Change:${name}`, 'Change', changeObs);
|
|
618
|
+
|
|
619
|
+
const handoffObs = [
|
|
620
|
+
fields.nextRole ? `next_role: ${fields.nextRole}` : null,
|
|
621
|
+
fields.nextCommand ? `next_command: ${fields.nextCommand}` : null,
|
|
622
|
+
fields.sessionCount ? `session_count: ${fields.sessionCount}` : null,
|
|
623
|
+
fields.summary ? `summary: ${fields.summary}` : null,
|
|
624
|
+
fields.blocked ? `blocked: ${fields.blocked}` : null,
|
|
625
|
+
].filter(Boolean);
|
|
626
|
+
if (handoffObs.length) upsertMemoryEntity(items, `Handoff:${name}`, 'Handoff', handoffObs);
|
|
627
|
+
|
|
628
|
+
for (const decision of parseDecisionItems(fields.decisions)) {
|
|
629
|
+
const topicMatch = decision.match(/^([^:]+):/);
|
|
630
|
+
const topic = (topicMatch ? topicMatch[1] : decision).trim().slice(0, 80) || decision.slice(0, 80);
|
|
631
|
+
upsertMemoryEntity(items, `Decision:${topic}`, 'Decision', [`chosen: ${decision}`]);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
saveMemoryItems(filePath, items);
|
|
635
|
+
return filePath;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function resolveHandoffChange(projectDir, requested) {
|
|
639
|
+
if (requested) return requested;
|
|
640
|
+
const changes = listActiveChanges(projectDir);
|
|
641
|
+
if (changes.length === 1) return changes[0];
|
|
642
|
+
if (changes.length === 0) return null;
|
|
643
|
+
return { ambiguous: changes };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function readHandoffFields(projectDir, changeName) {
|
|
647
|
+
const filePath = join(projectDir, 'openspec', 'changes', changeName, 'handoff.md');
|
|
648
|
+
if (!existsSync(filePath)) return { filePath, fields: null };
|
|
649
|
+
const sections = parseHandoffMarkdown(readFileSync(filePath, 'utf-8'));
|
|
650
|
+
return { filePath, fields: fieldsFromSections(changeName, sections) };
|
|
651
|
+
}
|
|
652
|
+
|
|
240
653
|
function parseFigmaUrl(url) {
|
|
241
654
|
try {
|
|
242
655
|
const parsed = new URL(url);
|
|
@@ -643,7 +1056,7 @@ function generateAmpSubagentSkills(projectDir) {
|
|
|
643
1056
|
'',
|
|
644
1057
|
`<!-- AUTO-GENERATED from .agents/subagents/${file} — edit the source file, then run: npx agent-orchestrator-kit sync -->`,
|
|
645
1058
|
'',
|
|
646
|
-
|
|
1059
|
+
AMP_SPAWN_PREAMBLE,
|
|
647
1060
|
'',
|
|
648
1061
|
parsed[2].trim(),
|
|
649
1062
|
'',
|
|
@@ -774,6 +1187,10 @@ program
|
|
|
774
1187
|
log.title('Updating .gitignore');
|
|
775
1188
|
mergeGitignore(projectDir, GITIGNORE_LINES);
|
|
776
1189
|
|
|
1190
|
+
log.title('Configuring Memory MCP');
|
|
1191
|
+
refreshMemoryManagedFiles(projectDir);
|
|
1192
|
+
ensureMemoryMcpEntry(projectDir);
|
|
1193
|
+
|
|
777
1194
|
log.title('Done');
|
|
778
1195
|
log.ok(`agent-orchestrator-kit v${KIT_VERSION} installed`);
|
|
779
1196
|
printNextSteps(profile, projectDir, ci, specVerify);
|
|
@@ -825,6 +1242,9 @@ program
|
|
|
825
1242
|
|
|
826
1243
|
log.title('Refreshing Figma setup templates');
|
|
827
1244
|
refreshFigmaManagedFiles(projectDir);
|
|
1245
|
+
log.title('Configuring Memory MCP');
|
|
1246
|
+
refreshMemoryManagedFiles(projectDir);
|
|
1247
|
+
ensureMemoryMcpEntry(projectDir);
|
|
828
1248
|
mergeGitignore(projectDir, GITIGNORE_LINES);
|
|
829
1249
|
|
|
830
1250
|
log.ok(`Updated to v${KIT_VERSION}`);
|
|
@@ -889,6 +1309,9 @@ program
|
|
|
889
1309
|
syncAmp(projectDir);
|
|
890
1310
|
}
|
|
891
1311
|
|
|
1312
|
+
log.title('Configuring Memory MCP');
|
|
1313
|
+
ensureMemoryMcpEntry(projectDir);
|
|
1314
|
+
|
|
892
1315
|
mergeGitignore(projectDir, GITIGNORE_LINES);
|
|
893
1316
|
|
|
894
1317
|
log.ok('Sync complete');
|
|
@@ -1139,4 +1562,137 @@ program
|
|
|
1139
1562
|
}
|
|
1140
1563
|
});
|
|
1141
1564
|
|
|
1565
|
+
program
|
|
1566
|
+
.command('memory-setup')
|
|
1567
|
+
.description('Install memory MCP launcher and rewrite Cursor/Amp configs to use an absolute MEMORY_FILE_PATH')
|
|
1568
|
+
.action(() => {
|
|
1569
|
+
const projectDir = process.cwd();
|
|
1570
|
+
log.title('agent-orchestrator memory-setup');
|
|
1571
|
+
refreshMemoryManagedFiles(projectDir);
|
|
1572
|
+
mergeGitignore(projectDir, GITIGNORE_LINES);
|
|
1573
|
+
ensureMemoryMcpEntry(projectDir);
|
|
1574
|
+
log.ok(`Memory file: ${resolve(projectDir, MEMORY_FILE_REL)}`);
|
|
1575
|
+
log.info('Restart Cursor / Amp after this change');
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
program
|
|
1579
|
+
.command('handoff [change-name]')
|
|
1580
|
+
.description('Persist or restore session handoff: write handoff.md, upsert Memory JSON, print the expanded next-thread prompt')
|
|
1581
|
+
.option('--restore', 'Print the restore briefing instead of persisting', false)
|
|
1582
|
+
.option('--closed-role <role>', 'Closed role for persist')
|
|
1583
|
+
.option('--next-command <command>', 'Next /opsx:* command')
|
|
1584
|
+
.option('--next-role <role>', 'Next role or subagent name')
|
|
1585
|
+
.option('--summary <text>', 'Persisted summary (also fills Done when Done is empty)')
|
|
1586
|
+
.option('--done <text>', 'Done section')
|
|
1587
|
+
.option('--decisions <text>', 'Decisions section')
|
|
1588
|
+
.option('--blocked <text>', 'Blocked section')
|
|
1589
|
+
.option('--attach <text>', 'Attach section')
|
|
1590
|
+
.option('--spawn <text>', 'Subagents to spawn')
|
|
1591
|
+
.option('--constraints <text>', 'Constraints section')
|
|
1592
|
+
.option('--status <status>', 'Change status observation')
|
|
1593
|
+
.option('--tasks <progress>', 'Task progress n/m')
|
|
1594
|
+
.option('--review <verdict>', 'Review verdict')
|
|
1595
|
+
.option('--session-count <n>', 'Handoff session_count')
|
|
1596
|
+
.action((changeName, opts) => {
|
|
1597
|
+
const projectDir = process.cwd();
|
|
1598
|
+
const resolved = resolveHandoffChange(projectDir, changeName);
|
|
1599
|
+
if (!resolved) {
|
|
1600
|
+
log.err('No active change found. Pass a name: npx agent-orchestrator-kit handoff <name>');
|
|
1601
|
+
process.exitCode = 1;
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (resolved.ambiguous) {
|
|
1605
|
+
log.err(`Multiple active changes: ${resolved.ambiguous.join(', ')}. Pass the change name argument.`);
|
|
1606
|
+
process.exitCode = 1;
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
const name = resolved;
|
|
1611
|
+
const { agentLanguage } = readOrchestratorMeta(projectDir);
|
|
1612
|
+
const changeDir = join(projectDir, 'openspec', 'changes', name);
|
|
1613
|
+
|
|
1614
|
+
if (opts.restore) {
|
|
1615
|
+
log.title(`handoff restore ${name}`);
|
|
1616
|
+
const { filePath, fields } = readHandoffFields(projectDir, name);
|
|
1617
|
+
const memoryPath = resolve(projectDir, MEMORY_FILE_REL);
|
|
1618
|
+
const memoryItems = loadMemoryItems(memoryPath).filter(
|
|
1619
|
+
(item) => item.type === 'entity' && String(item.name || '').includes(name),
|
|
1620
|
+
);
|
|
1621
|
+
if (!fields && memoryItems.length === 0) {
|
|
1622
|
+
log.err(`No handoff.md or Memory entities for ${name}`);
|
|
1623
|
+
log.info(`Expected: ${filePath}`);
|
|
1624
|
+
process.exitCode = 1;
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
if (fields) {
|
|
1628
|
+
log.ok(`handoff.md: ${filePath}`);
|
|
1629
|
+
console.log(`next_command: ${fields.nextCommand || '(missing)'}`);
|
|
1630
|
+
console.log(`next_role: ${fields.nextRole || '(missing)'}`);
|
|
1631
|
+
console.log(`closed_role: ${fields.closedRole || '(missing)'}`);
|
|
1632
|
+
console.log('');
|
|
1633
|
+
console.log(fields.done || '');
|
|
1634
|
+
} else {
|
|
1635
|
+
log.warn('handoff.md missing — using Memory JSON only');
|
|
1636
|
+
}
|
|
1637
|
+
if (memoryItems.length) {
|
|
1638
|
+
log.ok(`Memory entities: ${memoryItems.length} (${memoryPath})`);
|
|
1639
|
+
} else {
|
|
1640
|
+
log.warn(`Memory JSON empty or missing at ${memoryPath}`);
|
|
1641
|
+
}
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
console.error(pc.bold(pc.white(`\nhandoff persist ${name}`)));
|
|
1646
|
+
if (!existsSync(changeDir)) {
|
|
1647
|
+
log.err(`change not found: ${name}`);
|
|
1648
|
+
process.exitCode = 1;
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const existing = readHandoffFields(projectDir, name);
|
|
1653
|
+
const extra = {
|
|
1654
|
+
closedRole: opts.closedRole,
|
|
1655
|
+
nextCommand: opts.nextCommand,
|
|
1656
|
+
nextRole: opts.nextRole,
|
|
1657
|
+
summary: opts.summary,
|
|
1658
|
+
done: opts.done,
|
|
1659
|
+
decisions: opts.decisions,
|
|
1660
|
+
blocked: opts.blocked,
|
|
1661
|
+
attach: opts.attach,
|
|
1662
|
+
spawn: opts.spawn,
|
|
1663
|
+
constraints: opts.constraints,
|
|
1664
|
+
status: opts.status,
|
|
1665
|
+
tasks: opts.tasks,
|
|
1666
|
+
review: opts.review,
|
|
1667
|
+
sessionCount: opts.sessionCount,
|
|
1668
|
+
};
|
|
1669
|
+
const sections = existing.fields ? parseHandoffMarkdown(readFileSync(existing.filePath, 'utf-8')) : {};
|
|
1670
|
+
const fields = fieldsFromSections(name, sections, extra);
|
|
1671
|
+
const missing = missingHandoffFields(fields);
|
|
1672
|
+
if (missing.length) {
|
|
1673
|
+
log.err(`handoff.md incomplete — missing: ${missing.join(', ')}`);
|
|
1674
|
+
log.err('Write the file sections or pass --closed-role, --done/--summary, and --next-command');
|
|
1675
|
+
process.exitCode = 1;
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
const progress = parseTasksProgress(changeDir);
|
|
1680
|
+
if (!fields.tasks && progress) fields.tasks = `${progress.done}/${progress.total}`;
|
|
1681
|
+
if (!fields.review) fields.review = parseReviewVerdict(changeDir) || '';
|
|
1682
|
+
if (!fields.status) {
|
|
1683
|
+
fields.status = fields.review && /^APPROVE/i.test(fields.review) ? 'spec-approved' : 'in-progress';
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
const prompt = buildNextSessionPrompt(fields, agentLanguage).replace(/^\n+|\n+$/g, '');
|
|
1687
|
+
fields.prompt = prompt;
|
|
1688
|
+
writeFileSync(existing.filePath, `${buildHandoffMarkdown(fields).trim()}\n`);
|
|
1689
|
+
console.error(pc.green(' ✓'), existing.filePath.replace(`${projectDir}/`, ''));
|
|
1690
|
+
|
|
1691
|
+
const memoryPath = persistMemoryFromHandoff(projectDir, fields);
|
|
1692
|
+
console.error(pc.green(' ✓'), `Memory JSON upserted: ${memoryPath}`);
|
|
1693
|
+
|
|
1694
|
+
console.error(pc.dim('Copy the prompt below into the next chat as one fenced block. Do not include this line.'));
|
|
1695
|
+
process.stdout.write(`${prompt}\n`);
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1142
1698
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Universal AI agent orchestration kit for Cursor, Claude Code, and Amp Code — spec-driven OpenSpec pipeline, conductor subagents, session handoff, and optional local Figma PAT setup",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Universal AI agent orchestration kit for Cursor, Claude Code, and Amp Code — spec-driven OpenSpec pipeline, conductor subagents, durable session handoff CLI, and optional local Figma PAT setup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agent",
|
|
7
7
|
"cursor",
|
|
@@ -47,10 +47,13 @@ handoff:
|
|
|
47
47
|
restore_on_start: true
|
|
48
48
|
persist_on_exit: true
|
|
49
49
|
emit_next_session_prompt: true
|
|
50
|
+
prompt_self_contained: true
|
|
51
|
+
spawn_handoff_subagent: true
|
|
50
52
|
|
|
51
53
|
memory:
|
|
52
54
|
enabled: true
|
|
53
55
|
file: .cursor/memory.json
|
|
56
|
+
launcher: scripts/memory-mcp-launcher.cjs
|
|
54
57
|
|
|
55
58
|
mcp:
|
|
56
59
|
baseline:
|
|
@@ -54,10 +54,13 @@ handoff:
|
|
|
54
54
|
restore_on_start: true
|
|
55
55
|
persist_on_exit: true
|
|
56
56
|
emit_next_session_prompt: true
|
|
57
|
+
prompt_self_contained: true
|
|
58
|
+
spawn_handoff_subagent: true
|
|
57
59
|
|
|
58
60
|
memory:
|
|
59
61
|
enabled: true
|
|
60
62
|
file: .cursor/memory.json
|
|
63
|
+
launcher: scripts/memory-mcp-launcher.cjs
|
|
61
64
|
|
|
62
65
|
mcp:
|
|
63
66
|
baseline:
|
|
@@ -52,10 +52,13 @@ handoff:
|
|
|
52
52
|
restore_on_start: true
|
|
53
53
|
persist_on_exit: true
|
|
54
54
|
emit_next_session_prompt: true
|
|
55
|
+
prompt_self_contained: true
|
|
56
|
+
spawn_handoff_subagent: true
|
|
55
57
|
|
|
56
58
|
memory:
|
|
57
59
|
enabled: true
|
|
58
60
|
file: .cursor/memory.json
|
|
61
|
+
launcher: scripts/memory-mcp-launcher.cjs
|
|
59
62
|
|
|
60
63
|
mcp:
|
|
61
64
|
baseline:
|
|
@@ -51,10 +51,13 @@ handoff:
|
|
|
51
51
|
restore_on_start: true
|
|
52
52
|
persist_on_exit: true
|
|
53
53
|
emit_next_session_prompt: true
|
|
54
|
+
prompt_self_contained: true
|
|
55
|
+
spawn_handoff_subagent: true
|
|
54
56
|
|
|
55
57
|
memory:
|
|
56
58
|
enabled: true
|
|
57
59
|
file: .cursor/memory.json
|
|
60
|
+
launcher: scripts/memory-mcp-launcher.cjs
|
|
58
61
|
|
|
59
62
|
mcp:
|
|
60
63
|
baseline:
|