agent-pool-mcp 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/group-lead.md +30 -0
- package/src/scheduler/daemon.js +5 -5
- package/src/scheduler/pipeline.js +4 -4
- package/src/scheduler/scheduler.js +3 -3
- package/src/server.js +117 -2
- package/src/tool-definitions.js +49 -1
- package/src/tools/groups.js +124 -0
package/README.md
CHANGED
|
@@ -282,7 +282,7 @@ src/
|
|
|
282
282
|
- **Live Events**: Progress polling uses a ring buffer to show the latest activity without overwhelming context.
|
|
283
283
|
- **Depth Tracking**: Nested orchestration support with optional `AGENT_POOL_MAX_DEPTH` limit.
|
|
284
284
|
- **Adaptive Polling**: Pipeline daemon uses 3s intervals when active, 30s when idle.
|
|
285
|
-
- **File-Based Communication**: Pipeline agents communicate through `.
|
|
285
|
+
- **File-Based Communication**: Pipeline agents communicate through `.agents/runs/` JSON files — each Gemini process has its own MCP server instance but shares state via filesystem.
|
|
286
286
|
|
|
287
287
|
## License
|
|
288
288
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: group-lead
|
|
3
|
+
description: Fractal group leader — breaks down tasks and delegates to group members via delegate_to_group.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Group Lead
|
|
7
|
+
|
|
8
|
+
You are a group leader in a fractal agent orchestration system. Your role is to:
|
|
9
|
+
|
|
10
|
+
1. **Analyze** the incoming task and break it into independent subtasks
|
|
11
|
+
2. **Create groups** if they don't exist yet, with appropriate skills and policies
|
|
12
|
+
3. **Delegate** subtasks to group members using `delegate_to_group`
|
|
13
|
+
4. **Collect** results from all agents using `get_task_result`
|
|
14
|
+
5. **Synthesize** a final report combining all results
|
|
15
|
+
|
|
16
|
+
## Rules
|
|
17
|
+
|
|
18
|
+
- Break tasks into pieces that can run in parallel
|
|
19
|
+
- Each subtask should be self-contained (agent can complete it without external context)
|
|
20
|
+
- Set appropriate policies: use `read-only` for analysis, `safe-edit` for code changes
|
|
21
|
+
- Always set `max_agents` to prevent runaway spawning
|
|
22
|
+
- Collect ALL results before synthesizing — don't skip slow agents
|
|
23
|
+
- Report both successes and failures
|
|
24
|
+
|
|
25
|
+
## Output Format
|
|
26
|
+
|
|
27
|
+
After collecting all results, produce a structured summary:
|
|
28
|
+
1. Task overview (what was asked)
|
|
29
|
+
2. Subtask results (one per agent)
|
|
30
|
+
3. Synthesized conclusion
|
package/src/scheduler/daemon.js
CHANGED
|
@@ -17,9 +17,9 @@ import { join, dirname } from 'node:path';
|
|
|
17
17
|
import { matchesCron } from './cron.js';
|
|
18
18
|
|
|
19
19
|
const POLL_INTERVAL_MS = 30_000; // Check schedules every 30 seconds
|
|
20
|
-
const PID_FILE = '.
|
|
21
|
-
const SCHEDULE_FILE = '.
|
|
22
|
-
const RESULTS_DIR = '.
|
|
20
|
+
const PID_FILE = '.agents/scheduler.pid';
|
|
21
|
+
const SCHEDULE_FILE = '.agents/schedule.json';
|
|
22
|
+
const RESULTS_DIR = '.agents/scheduled-results';
|
|
23
23
|
|
|
24
24
|
/** @type {string} */
|
|
25
25
|
const cwd = process.argv[2] || process.cwd();
|
|
@@ -163,8 +163,8 @@ function executeSchedule(schedule) {
|
|
|
163
163
|
|
|
164
164
|
import { readdirSync } from 'node:fs';
|
|
165
165
|
|
|
166
|
-
const PIPELINES_DIR = '.
|
|
167
|
-
const RUNS_DIR = '.
|
|
166
|
+
const PIPELINES_DIR = '.agents/pipelines';
|
|
167
|
+
const RUNS_DIR = '.agents/runs';
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
170
|
* Spawn a Gemini CLI agent for a pipeline step.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pipeline management — CRUD for pipeline definitions and run state.
|
|
3
3
|
*
|
|
4
|
-
* Pipelines are stored as JSON templates in .
|
|
5
|
-
* Each execution creates a run state in .
|
|
4
|
+
* Pipelines are stored as JSON templates in .agents/pipelines/.
|
|
5
|
+
* Each execution creates a run state in .agents/runs/.
|
|
6
6
|
*
|
|
7
7
|
* @module agent-pool/scheduler/pipeline
|
|
8
8
|
*/
|
|
@@ -12,8 +12,8 @@ import { join, dirname } from 'node:path';
|
|
|
12
12
|
import { randomUUID } from 'node:crypto';
|
|
13
13
|
import { ensureDaemon } from './scheduler.js';
|
|
14
14
|
|
|
15
|
-
const PIPELINES_DIR = '.
|
|
16
|
-
const RUNS_DIR = '.
|
|
15
|
+
const PIPELINES_DIR = '.agents/pipelines';
|
|
16
|
+
const RUNS_DIR = '.agents/runs';
|
|
17
17
|
|
|
18
18
|
// ─── Helpers ────────────────────────────────────────────────
|
|
19
19
|
|
|
@@ -15,9 +15,9 @@ import { nextCronRun } from './cron.js';
|
|
|
15
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
const DAEMON_SCRIPT = join(__dirname, 'daemon.js');
|
|
17
17
|
|
|
18
|
-
const SCHEDULE_FILE = '.
|
|
19
|
-
const RESULTS_DIR = '.
|
|
20
|
-
const PID_FILE = '.
|
|
18
|
+
const SCHEDULE_FILE = '.agents/schedule.json';
|
|
19
|
+
const RESULTS_DIR = '.agents/scheduled-results';
|
|
20
|
+
const PID_FILE = '.agents/scheduler.pid';
|
|
21
21
|
|
|
22
22
|
// ─── Schedule CRUD ──────────────────────────────────────────
|
|
23
23
|
|
package/src/server.js
CHANGED
|
@@ -22,6 +22,7 @@ import { listSkills, createSkill, deleteSkill, installSkill, provisionSkill } fr
|
|
|
22
22
|
import { consultPeer } from './tools/consult.js';
|
|
23
23
|
import { addSchedule, listSchedules, removeSchedule, getScheduledResults, getDaemonStatus } from './scheduler/scheduler.js';
|
|
24
24
|
import { createPipeline, listPipelines, runPipeline, getRun, listRuns, cancelRun, signalStepComplete, bounceBack } from './scheduler/pipeline.js';
|
|
25
|
+
import { createGroup, listGroups, getGroup } from './tools/groups.js';
|
|
25
26
|
|
|
26
27
|
import { TOOL_DEFINITIONS } from './tool-definitions.js';
|
|
27
28
|
|
|
@@ -68,6 +69,7 @@ Docs: https://github.com/google-gemini/gemini-cli`
|
|
|
68
69
|
/** Tools that require Gemini CLI to be installed */
|
|
69
70
|
const GEMINI_TOOLS = new Set([
|
|
70
71
|
'delegate_task', 'delegate_task_readonly', 'consult_peer', 'list_sessions',
|
|
72
|
+
'delegate_to_group',
|
|
71
73
|
]);
|
|
72
74
|
|
|
73
75
|
// ─── Depth tracking (for nested orchestration) ──────────────
|
|
@@ -110,7 +112,7 @@ export function createServer() {
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
const server = new Server(
|
|
113
|
-
{ name: 'agent-pool', version: '1.
|
|
115
|
+
{ name: 'agent-pool', version: '1.5.0' },
|
|
114
116
|
{ capabilities: { tools: {}, resources: {} } },
|
|
115
117
|
);
|
|
116
118
|
|
|
@@ -200,6 +202,12 @@ export function createServer() {
|
|
|
200
202
|
response = handleBounceBack(args); break;
|
|
201
203
|
case 'get_usage_guide':
|
|
202
204
|
response = handleGetUsageGuide(args); break;
|
|
205
|
+
case 'create_group':
|
|
206
|
+
response = handleCreateGroup(args); break;
|
|
207
|
+
case 'list_groups':
|
|
208
|
+
response = handleListGroups(args); break;
|
|
209
|
+
case 'delegate_to_group':
|
|
210
|
+
response = handleDelegateToGroup(args); break;
|
|
203
211
|
default:
|
|
204
212
|
response = { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
205
213
|
}
|
|
@@ -422,7 +430,7 @@ function handleScheduleTask(args) {
|
|
|
422
430
|
return {
|
|
423
431
|
content: [{
|
|
424
432
|
type: 'text',
|
|
425
|
-
text: `⏰ Task scheduled.\n\n- **Schedule ID**: \`${result.scheduleId}\`\n- **Cron**: \`${args.cron}\`\n- **Next run**: ${result.nextRun || 'unknown'}\n- **Prompt**: ${args.prompt.substring(0, 100)}...\n\nDaemon is running in the background. Results will be saved to \`.
|
|
433
|
+
text: `⏰ Task scheduled.\n\n- **Schedule ID**: \`${result.scheduleId}\`\n- **Cron**: \`${args.cron}\`\n- **Next run**: ${result.nextRun || 'unknown'}\n- **Prompt**: ${args.prompt.substring(0, 100)}...\n\nDaemon is running in the background. Results will be saved to \`.agents/scheduled-results/\`.\nUse \`list_schedules\` to see all schedules, \`get_scheduled_results\` to read outputs.`,
|
|
426
434
|
}],
|
|
427
435
|
};
|
|
428
436
|
} catch (error) {
|
|
@@ -688,3 +696,110 @@ function handleGetUsageGuide(args) {
|
|
|
688
696
|
return { content: [{ type: 'text', text: topicContent.join('\n').trim() }] };
|
|
689
697
|
}
|
|
690
698
|
|
|
699
|
+
// ─── Group Handlers ─────────────────────────────────────────────
|
|
700
|
+
|
|
701
|
+
/** @param {object} args */
|
|
702
|
+
function handleCreateGroup(args) {
|
|
703
|
+
const cwd = args.cwd ?? defaultCwd;
|
|
704
|
+
const result = createGroup(cwd, {
|
|
705
|
+
name: args.name,
|
|
706
|
+
runner: args.runner,
|
|
707
|
+
skill: args.skill,
|
|
708
|
+
policy: args.policy,
|
|
709
|
+
max_agents: args.max_agents,
|
|
710
|
+
include_dirs: args.include_dirs,
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
const configParts = [];
|
|
714
|
+
if (args.runner) configParts.push(`runner: ${args.runner}`);
|
|
715
|
+
if (args.skill) configParts.push(`skill: ${args.skill}`);
|
|
716
|
+
if (args.policy) configParts.push(`policy: ${args.policy}`);
|
|
717
|
+
if (args.max_agents) configParts.push(`max: ${args.max_agents}`);
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
content: [{
|
|
721
|
+
type: 'text',
|
|
722
|
+
text: `✅ Group ${result.created ? 'created' : 'updated'}: \`${args.name}\`${configParts.length > 0 ? `\n- ${configParts.join('\n- ')}` : ''}\n\nUse \`delegate_to_group\` to send tasks to this group.`,
|
|
723
|
+
}],
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** @param {object} args */
|
|
728
|
+
function handleListGroups(args) {
|
|
729
|
+
const cwd = args.cwd ?? defaultCwd;
|
|
730
|
+
const groups = listGroups(cwd);
|
|
731
|
+
|
|
732
|
+
if (groups.length === 0) {
|
|
733
|
+
return { content: [{ type: 'text', text: 'No groups defined. Use `create_group` to create one.' }] };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const lines = groups.map((g) => {
|
|
737
|
+
const parts = [];
|
|
738
|
+
if (g.runner) parts.push(`runner: ${g.runner}`);
|
|
739
|
+
if (g.skill) parts.push(`skill: ${g.skill}`);
|
|
740
|
+
if (g.policy) parts.push(`policy: ${g.policy}`);
|
|
741
|
+
if (g.max_agents) parts.push(`max: ${g.max_agents}`);
|
|
742
|
+
return `- **${g.name}** — ${parts.join(', ') || 'no config'}`;
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
content: [{ type: 'text', text: `## Agent Groups (${groups.length})\n\n${lines.join('\n')}` }],
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** @param {object} args */
|
|
751
|
+
function handleDelegateToGroup(args) {
|
|
752
|
+
const cwd = args.cwd ?? defaultCwd;
|
|
753
|
+
const group = getGroup(cwd, args.group);
|
|
754
|
+
|
|
755
|
+
if (!group) {
|
|
756
|
+
return {
|
|
757
|
+
content: [{ type: 'text', text: `❌ Group \`${args.group}\` not found. Use \`list_groups\` to see available groups.` }],
|
|
758
|
+
isError: true,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const count = args.count ?? 1;
|
|
763
|
+
|
|
764
|
+
// Check max_agents limit
|
|
765
|
+
if (group.max_agents && count > group.max_agents) {
|
|
766
|
+
return {
|
|
767
|
+
content: [{
|
|
768
|
+
type: 'text',
|
|
769
|
+
text: `❌ Requested ${count} agents but group \`${args.group}\` allows max ${group.max_agents}.`,
|
|
770
|
+
}],
|
|
771
|
+
isError: true,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const taskIds = [];
|
|
776
|
+
|
|
777
|
+
for (let i = 0; i < count; i++) {
|
|
778
|
+
const delegateArgs = {
|
|
779
|
+
prompt: count > 1 ? `[Agent ${i + 1}/${count} in group "${args.group}"]\n\n${args.prompt}` : args.prompt,
|
|
780
|
+
cwd,
|
|
781
|
+
runner: group.runner || undefined,
|
|
782
|
+
skill: group.skill || undefined,
|
|
783
|
+
policy: group.policy || undefined,
|
|
784
|
+
include_dirs: group.include_dirs || undefined,
|
|
785
|
+
timeout: args.timeout,
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
const result = handleDelegate(delegateArgs, {
|
|
789
|
+
approvalMode: DEFAULT_APPROVAL_MODE,
|
|
790
|
+
emoji: '👥',
|
|
791
|
+
label: `Group task (${args.group} #${i + 1})`,
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// Extract task_id from response text
|
|
795
|
+
const match = result.content[0].text.match(/`([0-9a-f-]{36})`/);
|
|
796
|
+
if (match) taskIds.push(match[1]);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
content: [{
|
|
801
|
+
type: 'text',
|
|
802
|
+
text: `👥 Delegated to group \`${args.group}\` — ${count} agent(s) spawned.\n\n${taskIds.map((id, i) => `- Agent ${i + 1}: \`${id}\``).join('\n')}\n\nUse \`get_task_result\` to check each agent's status.`,
|
|
803
|
+
}],
|
|
804
|
+
};
|
|
805
|
+
}
|
package/src/tool-definitions.js
CHANGED
|
@@ -199,7 +199,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
199
199
|
description: [
|
|
200
200
|
'Schedule a Gemini CLI agent to run on a cron schedule or as a delayed one-shot.',
|
|
201
201
|
'Spawns a persistent daemon that survives IDE/CLI restarts.',
|
|
202
|
-
'Results are saved to .
|
|
202
|
+
'Results are saved to .agents/scheduled-results/ and can be retrieved with get_scheduled_results.',
|
|
203
203
|
'',
|
|
204
204
|
'Cron format: standard 5-field (minute hour day month weekday).',
|
|
205
205
|
'Examples: "*/30 * * * *" (every 30 min), "0 9 * * MON-FRI" (9am weekdays), "0 */2 * * *" (every 2 hours).',
|
|
@@ -360,5 +360,53 @@ export const TOOL_DEFINITIONS = [
|
|
|
360
360
|
required: ['step_name', 'reason'],
|
|
361
361
|
},
|
|
362
362
|
},
|
|
363
|
+
// ─── Group Tools ────────────────────────────────────────
|
|
364
|
+
{
|
|
365
|
+
name: 'create_group',
|
|
366
|
+
description: 'Create a named agent group with shared config (runner, skill, policy). Groups are reusable presets for fractal orchestration.',
|
|
367
|
+
inputSchema: {
|
|
368
|
+
type: 'object',
|
|
369
|
+
properties: {
|
|
370
|
+
name: { type: 'string', description: 'Group name (e.g. "backend-team", "qa-group").' },
|
|
371
|
+
runner: { type: 'string', description: 'Default runner for agents in this group.' },
|
|
372
|
+
skill: { type: 'string', description: 'Default skill activated for all agents in this group.' },
|
|
373
|
+
policy: { type: 'string', description: 'Default policy restricting agent permissions.' },
|
|
374
|
+
max_agents: { type: 'number', description: 'Max concurrent agents allowed in this group.' },
|
|
375
|
+
include_dirs: { type: 'array', items: { type: 'string' }, description: 'Additional directories agents in this group can access.' },
|
|
376
|
+
cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
|
|
377
|
+
},
|
|
378
|
+
required: ['name'],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: 'list_groups',
|
|
383
|
+
description: 'List all registered agent groups with their config.',
|
|
384
|
+
inputSchema: {
|
|
385
|
+
type: 'object',
|
|
386
|
+
properties: {
|
|
387
|
+
cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: 'delegate_to_group',
|
|
393
|
+
description: [
|
|
394
|
+
'Delegate a task to a named agent group. Spawns agents with the group\'s shared config (runner, skill, policy).',
|
|
395
|
+
'Use `count` to launch multiple agents in parallel (fractal orchestration).',
|
|
396
|
+
'',
|
|
397
|
+
'Returns array of task_ids immediately (non-blocking). Use get_task_result to check each.',
|
|
398
|
+
].join('\n'),
|
|
399
|
+
inputSchema: {
|
|
400
|
+
type: 'object',
|
|
401
|
+
properties: {
|
|
402
|
+
group: { type: 'string', description: 'Group name to delegate to.' },
|
|
403
|
+
prompt: { type: 'string', description: 'Task description for the agents.' },
|
|
404
|
+
count: { type: 'number', description: 'Number of agents to spawn. Default: 1.' },
|
|
405
|
+
cwd: { type: 'string', description: 'Working directory. Defaults to current working directory.' },
|
|
406
|
+
timeout: { type: 'number', description: 'Timeout in seconds. Overrides group default.' },
|
|
407
|
+
},
|
|
408
|
+
required: ['group', 'prompt'],
|
|
409
|
+
},
|
|
410
|
+
},
|
|
363
411
|
];
|
|
364
412
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Groups — named config presets for fractal orchestration.
|
|
3
|
+
* Groups bundle runner, skill, policy, and max_agents into a reusable team config.
|
|
4
|
+
*
|
|
5
|
+
* @module agent-pool/tools/groups
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
const GROUPS_DIR = '.agents';
|
|
12
|
+
const GROUPS_FILE = 'groups.json';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get path to groups.json for a project.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} cwd - Project directory
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function getGroupsPath(cwd) {
|
|
21
|
+
return path.join(cwd, GROUPS_DIR, GROUPS_FILE);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load all groups from disk.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} cwd - Project directory
|
|
28
|
+
* @returns {Object<string, object>}
|
|
29
|
+
*/
|
|
30
|
+
function loadGroups(cwd) {
|
|
31
|
+
const filePath = getGroupsPath(cwd);
|
|
32
|
+
if (!fs.existsSync(filePath)) return {};
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Save groups to disk.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} cwd - Project directory
|
|
44
|
+
* @param {Object<string, object>} groups
|
|
45
|
+
*/
|
|
46
|
+
function saveGroups(cwd, groups) {
|
|
47
|
+
const dirPath = path.join(cwd, GROUPS_DIR);
|
|
48
|
+
if (!fs.existsSync(dirPath)) {
|
|
49
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
fs.writeFileSync(getGroupsPath(cwd), JSON.stringify(groups, null, 2));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create or update a group.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} cwd - Project directory
|
|
58
|
+
* @param {object} config - Group configuration
|
|
59
|
+
* @param {string} config.name - Group name (e.g. "backend-team")
|
|
60
|
+
* @param {string} [config.runner] - Default runner for agents in this group
|
|
61
|
+
* @param {string} [config.skill] - Default skill for agents in this group
|
|
62
|
+
* @param {string} [config.policy] - Default policy for agents in this group
|
|
63
|
+
* @param {number} [config.max_agents] - Max concurrent agents in this group
|
|
64
|
+
* @param {string[]} [config.include_dirs] - Additional directories agents can access
|
|
65
|
+
* @returns {{ name: string, created: boolean }}
|
|
66
|
+
*/
|
|
67
|
+
export function createGroup(cwd, config) {
|
|
68
|
+
const groups = loadGroups(cwd);
|
|
69
|
+
const existed = !!groups[config.name];
|
|
70
|
+
|
|
71
|
+
groups[config.name] = {
|
|
72
|
+
runner: config.runner || null,
|
|
73
|
+
skill: config.skill || null,
|
|
74
|
+
policy: config.policy || null,
|
|
75
|
+
max_agents: config.max_agents || null,
|
|
76
|
+
include_dirs: config.include_dirs || null,
|
|
77
|
+
created_at: existed ? groups[config.name].created_at : new Date().toISOString(),
|
|
78
|
+
updated_at: new Date().toISOString(),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
saveGroups(cwd, groups);
|
|
82
|
+
return { name: config.name, created: !existed };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* List all groups.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} cwd - Project directory
|
|
89
|
+
* @returns {Array<{ name: string, runner: string|null, skill: string|null, policy: string|null, max_agents: number|null }>}
|
|
90
|
+
*/
|
|
91
|
+
export function listGroups(cwd) {
|
|
92
|
+
const groups = loadGroups(cwd);
|
|
93
|
+
return Object.entries(groups).map(([name, config]) => ({
|
|
94
|
+
name,
|
|
95
|
+
...config,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get a single group by name.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} cwd - Project directory
|
|
103
|
+
* @param {string} name - Group name
|
|
104
|
+
* @returns {object|null}
|
|
105
|
+
*/
|
|
106
|
+
export function getGroup(cwd, name) {
|
|
107
|
+
const groups = loadGroups(cwd);
|
|
108
|
+
return groups[name] || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Delete a group.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} cwd - Project directory
|
|
115
|
+
* @param {string} name - Group name
|
|
116
|
+
* @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
export function deleteGroup(cwd, name) {
|
|
119
|
+
const groups = loadGroups(cwd);
|
|
120
|
+
if (!groups[name]) return false;
|
|
121
|
+
delete groups[name];
|
|
122
|
+
saveGroups(cwd, groups);
|
|
123
|
+
return true;
|
|
124
|
+
}
|