speexor 0.1.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/API-REFERENCE.md +201 -0
- package/ARCHITECTURE.md +548 -0
- package/CHANGELOG.md +52 -0
- package/CODE-OF-CONDUCT.md +83 -0
- package/CONTRIBUTING.md +98 -0
- package/FAQ.md +105 -0
- package/LICENSE.md +21 -0
- package/PUBLISH.md +77 -0
- package/README.md +179 -0
- package/REFACTOR-LOG.md +40 -0
- package/ROADMAP.md +78 -0
- package/SECURITY.md +79 -0
- package/SUMMARY.md +46 -0
- package/TESTING.md +140 -0
- package/dist/agent-5D3BVWNK.js +37 -0
- package/dist/agent-5D3BVWNK.js.map +1 -0
- package/dist/chunk-2F66BZYJ.js +212 -0
- package/dist/chunk-2F66BZYJ.js.map +1 -0
- package/dist/chunk-5NA2TFPG.js +3 -0
- package/dist/chunk-5NA2TFPG.js.map +1 -0
- package/dist/chunk-B7WLHC4W.js +666 -0
- package/dist/chunk-B7WLHC4W.js.map +1 -0
- package/dist/chunk-SXALZEOJ.js +345 -0
- package/dist/chunk-SXALZEOJ.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +287 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/index.d.ts +31 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +205 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/index.d.ts +6 -0
- package/dist/plugins/index.js +3 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/types-0q_okI2g.d.ts +205 -0
- package/docs/PRD01.md +264 -0
- package/docs/PRD02.md +299 -0
- package/docs/PRD03.md +0 -0
- package/docs/PRD04.md +349 -0
- package/docs/PRD05.md +312 -0
- package/docs/SETUP.md +94 -0
- package/docs/TROUBLESHOOTING.md +113 -0
- package/examples/basic.yaml +61 -0
- package/package.json +102 -0
- package/schema/config.schema.json +119 -0
- package/speexor.config.yaml.example +30 -0
package/SUMMARY.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Speexor — Project Summary
|
|
2
|
+
|
|
3
|
+
> Agent Orchestrator for multi-AI coding agent orchestration across repositories.
|
|
4
|
+
|
|
5
|
+
## Project Status
|
|
6
|
+
- **Version:** 0.1.0 (pre-release)
|
|
7
|
+
- **License:** MIT
|
|
8
|
+
- **Language:** TypeScript (ESM, ES2022)
|
|
9
|
+
- **Node.js:** >= 18.0.0
|
|
10
|
+
|
|
11
|
+
## What It Does
|
|
12
|
+
Speexor is an orchestration layer that spawns and manages multiple AI coding agents in parallel across one or more git repositories. Each agent runs in an isolated git worktree with its own runtime session, handles one task autonomously, and can automatically respond to CI failures and PR review comments.
|
|
13
|
+
|
|
14
|
+
## Scope
|
|
15
|
+
- **Source files:** 31 TypeScript files (~2,430 lines)
|
|
16
|
+
- **Plugins:** 10 implementations across 6 plugin slots
|
|
17
|
+
- **CLI commands:** 6
|
|
18
|
+
- **AI adapters:** OpenCode, Claude Code, Aider, Codex
|
|
19
|
+
- **Runtime backends:** tmux (Unix), Process (Windows)
|
|
20
|
+
|
|
21
|
+
## Architecture Highlights
|
|
22
|
+
- Plugin-based: all capabilities are extensions of 7 defined slots
|
|
23
|
+
- Agent-agnostic: no vendor lock to a single AI provider
|
|
24
|
+
- Git-provider agnostic: GitHub as first implementation, interface open for GitLab/Gitea
|
|
25
|
+
- Dashboard built-in: no external dependencies for monitoring
|
|
26
|
+
|
|
27
|
+
## Milestones
|
|
28
|
+
|
|
29
|
+
| Milestone | Description | Status |
|
|
30
|
+
|-----------|-------------|--------|
|
|
31
|
+
| M0 — Foundation | Monorepo, core types, CLI skeleton, plugin contracts | ✅ Done |
|
|
32
|
+
| M1 — Single Agent E2E | OpenCode adapter + tmux + worktree, E2E flow | ✅ Done |
|
|
33
|
+
| M2 — GitHub Integration | Tracker + SCM + reaction engine | ✅ Done |
|
|
34
|
+
| M3 — Multi-Agent | Claude Code, Aider, Codex adapters | ✅ Done |
|
|
35
|
+
| M4 — Dashboard MVP | REST API + HTML dashboard | ✅ Done |
|
|
36
|
+
| M5 — Cost/Provider | Multi-provider routing config | 🔄 In Progress |
|
|
37
|
+
| M6 — Polish & Docs | Documentation, examples, open-source readiness | 🔄 In Progress |
|
|
38
|
+
|
|
39
|
+
## Quick Links
|
|
40
|
+
- [README](./README.md)
|
|
41
|
+
- [Architecture](./ARCHITECTURE.md)
|
|
42
|
+
- [Changelog](./CHANGELOG.md)
|
|
43
|
+
- [Roadmap](./ROADMAP.md)
|
|
44
|
+
- [Contributing](./CONTRIBUTING.md)
|
|
45
|
+
- [Security](./SECURITY.md)
|
|
46
|
+
- [Testing](./TESTING.md)
|
package/TESTING.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Testing Guide
|
|
2
|
+
|
|
3
|
+
> Testing strategy and guidelines for Speexor — Agent Orchestrator.
|
|
4
|
+
|
|
5
|
+
## Test Framework
|
|
6
|
+
|
|
7
|
+
Speexor uses [Vitest](https://vitest.dev/) as the test framework with `@vitest/coverage-v8` for coverage reporting.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Run all tests
|
|
11
|
+
pnpm test
|
|
12
|
+
|
|
13
|
+
# Watch mode
|
|
14
|
+
pnpm test:watch
|
|
15
|
+
|
|
16
|
+
# With coverage
|
|
17
|
+
pnpm test:coverage
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Test Categories
|
|
21
|
+
|
|
22
|
+
### Unit Tests
|
|
23
|
+
Test individual modules in isolation:
|
|
24
|
+
- **Core types** — Verify interface contracts and type guards
|
|
25
|
+
- **Config validation** — Test Zod schemas with valid/invalid YAML
|
|
26
|
+
- **Event bus** — Test emit/on/off/once behavior
|
|
27
|
+
- **Plugin contracts** — Verify plugins implement required interfaces
|
|
28
|
+
|
|
29
|
+
### Integration Tests
|
|
30
|
+
Test module interactions:
|
|
31
|
+
- **Lifecycle + plugins** — Verify plugin registration, agent spawn flow
|
|
32
|
+
- **Config + lifecycle** — Verify config loading → lifecycle initialization
|
|
33
|
+
- **Dashboard state** — Verify state mutations reflect correctly
|
|
34
|
+
- **Session store** — Verify persistence round-trip
|
|
35
|
+
|
|
36
|
+
### Plugin Tests
|
|
37
|
+
Each plugin should be tested against its interface contract:
|
|
38
|
+
- **Agent plugins** — Test spawn/sendInput/getStatus/kill lifecycle
|
|
39
|
+
- **Runtime plugins** — Test createSession/destroySession flow
|
|
40
|
+
- **Workspace plugin** — Test worktree lifecycle with mock git repo
|
|
41
|
+
- **Tracker/SCM** — Test API calls (mock gh CLI)
|
|
42
|
+
- **Notifier** — Test notification dispatch
|
|
43
|
+
|
|
44
|
+
### E2E Tests (Future)
|
|
45
|
+
Full end-to-end tests with real agent CLIs:
|
|
46
|
+
- Requires installed agent CLI (opencode, claude-code, etc.)
|
|
47
|
+
- Requires GitHub CLI authentication
|
|
48
|
+
- Creates real worktrees and branches (clean up after)
|
|
49
|
+
|
|
50
|
+
## Coverage Requirements
|
|
51
|
+
|
|
52
|
+
| Category | Target |
|
|
53
|
+
|----------|--------|
|
|
54
|
+
| Core types & config | ≥90% |
|
|
55
|
+
| CLI commands | ≥80% |
|
|
56
|
+
| Plugin loader | ≥90% |
|
|
57
|
+
| Agent adapters | ≥75% |
|
|
58
|
+
| Runtime adapters | ≥80% |
|
|
59
|
+
| Dashboard | ≥70% |
|
|
60
|
+
| Reaction engine | ≥85% |
|
|
61
|
+
| Session store | ≥90% |
|
|
62
|
+
|
|
63
|
+
## Test Structure
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
tests/
|
|
67
|
+
├── unit/
|
|
68
|
+
│ ├── core/
|
|
69
|
+
│ │ ├── config.test.ts
|
|
70
|
+
│ │ ├── event-bus.test.ts
|
|
71
|
+
│ │ └── lifecycle.test.ts
|
|
72
|
+
│ ├── plugins/
|
|
73
|
+
│ │ ├── opencode.test.ts
|
|
74
|
+
│ │ ├── tmux.test.ts
|
|
75
|
+
│ │ └── git-worktree.test.ts
|
|
76
|
+
│ └── cli/
|
|
77
|
+
│ └── commands.test.ts
|
|
78
|
+
├── integration/
|
|
79
|
+
│ ├── lifecycle-plugins.test.ts
|
|
80
|
+
│ └── config-lifecycle.test.ts
|
|
81
|
+
└── fixtures/
|
|
82
|
+
├── valid-config.yaml
|
|
83
|
+
├── invalid-config.yaml
|
|
84
|
+
└── mock-gh-responses/
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Writing Tests
|
|
88
|
+
|
|
89
|
+
### Vitest Setup
|
|
90
|
+
```typescript
|
|
91
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Example: Testing Config Validation
|
|
95
|
+
```typescript
|
|
96
|
+
import { validateConfig } from '../src/core/config.js'
|
|
97
|
+
|
|
98
|
+
describe('Config Validation', () => {
|
|
99
|
+
it('accepts valid minimal config', () => {
|
|
100
|
+
const config = {
|
|
101
|
+
version: '1',
|
|
102
|
+
projects: [
|
|
103
|
+
{
|
|
104
|
+
name: 'test',
|
|
105
|
+
repository: 'https://github.com/user/repo',
|
|
106
|
+
provider: { primary: 'opencode' },
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
}
|
|
110
|
+
expect(() => validateConfig(config)).not.toThrow()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('rejects config without version', () => {
|
|
114
|
+
expect(() => validateConfig({ projects: [] })).toThrow()
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Example: Testing Plugin Contract
|
|
120
|
+
```typescript
|
|
121
|
+
import { OpenCodeAgent } from '../src/plugins/agent/opencode.js'
|
|
122
|
+
import { createEventBus } from '../src/core/event-bus.js'
|
|
123
|
+
|
|
124
|
+
describe('OpenCodeAgent', () => {
|
|
125
|
+
it('implements AgentPlugin interface', () => {
|
|
126
|
+
const agent = new OpenCodeAgent()
|
|
127
|
+
expect(agent.name).toBe('opencode-agent')
|
|
128
|
+
expect(agent.type).toBe('agent')
|
|
129
|
+
expect(agent.spawn).toBeInstanceOf(Function)
|
|
130
|
+
expect(agent.kill).toBeInstanceOf(Function)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Mocking Guidelines
|
|
136
|
+
|
|
137
|
+
- **gh CLI** — Use `vi.mock('node:child_process')` to mock execSync
|
|
138
|
+
- **tmux** — Mock process execution, don't require tmux installation
|
|
139
|
+
- **File system** — Use `vi.mock('node:fs')` for config/store tests
|
|
140
|
+
- **Event bus** — Use real EventBus (not mocked) for integration tests
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { loadConfig, SpeexorLifecycle } from './chunk-2F66BZYJ.js';
|
|
2
|
+
import { loadAllPlugins } from './chunk-B7WLHC4W.js';
|
|
3
|
+
import Debug from 'debug';
|
|
4
|
+
|
|
5
|
+
var debug = Debug("speexor:agent");
|
|
6
|
+
async function agentSpawnCommand(options) {
|
|
7
|
+
const config = loadConfig();
|
|
8
|
+
const lifecycle = new SpeexorLifecycle(config);
|
|
9
|
+
await lifecycle.initialize();
|
|
10
|
+
const plugins = loadAllPlugins();
|
|
11
|
+
for (const plugin of plugins) {
|
|
12
|
+
lifecycle.registerPlugin(plugin);
|
|
13
|
+
}
|
|
14
|
+
const project = config.projects[0];
|
|
15
|
+
if (!project) {
|
|
16
|
+
throw new Error("No project configured");
|
|
17
|
+
}
|
|
18
|
+
const task = {
|
|
19
|
+
id: options.task,
|
|
20
|
+
title: options.task,
|
|
21
|
+
description: `Task ${options.task}`,
|
|
22
|
+
repository: project.repository,
|
|
23
|
+
branch: `speexor/${options.task}`,
|
|
24
|
+
provider: options.agent || project.provider.primary
|
|
25
|
+
};
|
|
26
|
+
debug(`Spawning agent for task ${task.id} using ${task.provider}`);
|
|
27
|
+
const session = await lifecycle.spawnAgent(task);
|
|
28
|
+
console.log(`
|
|
29
|
+
\u2705 Agent spawned: ${session.id}`);
|
|
30
|
+
console.log(` \u{1F916} Provider: ${session.provider}`);
|
|
31
|
+
console.log(` \u{1F4CB} Task: ${task.id}
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { agentSpawnCommand };
|
|
36
|
+
//# sourceMappingURL=agent-5D3BVWNK.js.map
|
|
37
|
+
//# sourceMappingURL=agent-5D3BVWNK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/agent.ts"],"names":[],"mappings":";;;;AAMA,IAAM,KAAA,GAAQ,MAAM,eAAe,CAAA;AAOnC,eAAsB,kBAAkB,OAAA,EAAuB;AAC7D,EAAA,MAAM,SAAS,UAAA,EAAW;AAC1B,EAAA,MAAM,SAAA,GAAY,IAAI,gBAAA,CAAiB,MAAM,CAAA;AAC7C,EAAA,MAAM,UAAU,UAAA,EAAW;AAE3B,EAAA,MAAM,UAAU,cAAA,EAAe;AAC/B,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,SAAA,CAAU,eAAe,MAAM,CAAA;AAAA,EACjC;AAEA,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA;AACjC,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,MAAM,IAAI,MAAM,uBAAuB,CAAA;AAAA,EACzC;AAEA,EAAA,MAAM,IAAA,GAAkB;AAAA,IACtB,IAAI,OAAA,CAAQ,IAAA;AAAA,IACZ,OAAO,OAAA,CAAQ,IAAA;AAAA,IACf,WAAA,EAAa,CAAA,KAAA,EAAQ,OAAA,CAAQ,IAAI,CAAA,CAAA;AAAA,IACjC,YAAY,OAAA,CAAQ,UAAA;AAAA,IACpB,MAAA,EAAQ,CAAA,QAAA,EAAW,OAAA,CAAQ,IAAI,CAAA,CAAA;AAAA,IAC/B,QAAA,EAAW,OAAA,CAAQ,KAAA,IAA2B,OAAA,CAAQ,QAAA,CAAS;AAAA,GACjE;AAEA,EAAA,KAAA,CAAM,2BAA2B,IAAA,CAAK,EAAE,CAAA,OAAA,EAAU,IAAA,CAAK,QAAQ,CAAA,CAAE,CAAA;AAEjE,EAAA,MAAM,OAAA,GAAU,MAAM,SAAA,CAAU,UAAA,CAAW,IAAI,CAAA;AAC/C,EAAA,OAAA,CAAQ,GAAA,CAAI;AAAA,wBAAA,EAAwB,OAAA,CAAQ,EAAE,CAAA,CAAE,CAAA;AAChD,EAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,sBAAA,EAAkB,OAAA,CAAQ,QAAQ,CAAA,CAAE,CAAA;AAChD,EAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,kBAAA,EAAc,IAAA,CAAK,EAAE;AAAA,CAAI,CAAA;AACvC","file":"agent-5D3BVWNK.js","sourcesContent":["import { loadConfig } from '../core/config.js'\nimport { SpeexorLifecycle } from '../core/lifecycle.js'\nimport { loadAllPlugins } from '../plugins/index.js'\nimport type { AgentTask, AgentProvider } from '../core/types.js'\nimport Debug from 'debug'\n\nconst debug = Debug('speexor:agent')\n\ninterface SpawnOptions {\n task: string\n agent: string\n}\n\nexport async function agentSpawnCommand(options: SpawnOptions) {\n const config = loadConfig()\n const lifecycle = new SpeexorLifecycle(config)\n await lifecycle.initialize()\n\n const plugins = loadAllPlugins()\n for (const plugin of plugins) {\n lifecycle.registerPlugin(plugin)\n }\n\n const project = config.projects[0]\n if (!project) {\n throw new Error('No project configured')\n }\n\n const task: AgentTask = {\n id: options.task,\n title: options.task,\n description: `Task ${options.task}`,\n repository: project.repository,\n branch: `speexor/${options.task}`,\n provider: (options.agent as AgentProvider) || project.provider.primary,\n }\n\n debug(`Spawning agent for task ${task.id} using ${task.provider}`)\n\n const session = await lifecycle.spawnAgent(task)\n console.log(`\\n ✅ Agent spawned: ${session.id}`)\n console.log(` 🤖 Provider: ${session.provider}`)\n console.log(` 📋 Task: ${task.id}\\n`)\n}\n"]}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { EventEmitter } from 'eventemitter3';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import Debug from 'debug';
|
|
7
|
+
|
|
8
|
+
// src/core/event-bus.ts
|
|
9
|
+
function createEventBus() {
|
|
10
|
+
const emitter = new EventEmitter();
|
|
11
|
+
return {
|
|
12
|
+
emit(event, data) {
|
|
13
|
+
emitter.emit(event, data);
|
|
14
|
+
},
|
|
15
|
+
on(event, handler) {
|
|
16
|
+
emitter.on(event, handler);
|
|
17
|
+
},
|
|
18
|
+
off(event, handler) {
|
|
19
|
+
emitter.off(event, handler);
|
|
20
|
+
},
|
|
21
|
+
once(event, handler) {
|
|
22
|
+
emitter.once(event, handler);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
var reactionRuleSchema = z.object({
|
|
27
|
+
auto: z.boolean(),
|
|
28
|
+
action: z.enum(["fix", "notify", "escalate", "skip"]),
|
|
29
|
+
retries: z.number().int().min(0).max(10),
|
|
30
|
+
escalateAfter: z.number().int().min(1).max(1440)
|
|
31
|
+
});
|
|
32
|
+
var providerRoutingSchema = z.object({
|
|
33
|
+
primary: z.enum(["opencode", "claude-code", "aider", "codex"]),
|
|
34
|
+
fallback: z.array(z.enum(["opencode", "claude-code", "aider", "codex"])).optional(),
|
|
35
|
+
concurrentLimit: z.number().int().min(1).max(20).optional(),
|
|
36
|
+
costLimit: z.number().int().optional()
|
|
37
|
+
});
|
|
38
|
+
var projectSchema = z.object({
|
|
39
|
+
name: z.string().min(1),
|
|
40
|
+
repository: z.string().min(1),
|
|
41
|
+
path: z.string().optional(),
|
|
42
|
+
branch: z.string().optional(),
|
|
43
|
+
provider: providerRoutingSchema,
|
|
44
|
+
reactions: z.object({
|
|
45
|
+
"ci-failed": reactionRuleSchema.optional(),
|
|
46
|
+
"changes-requested": reactionRuleSchema.optional(),
|
|
47
|
+
"approved-and-green": reactionRuleSchema.optional()
|
|
48
|
+
}).optional(),
|
|
49
|
+
plugins: z.object({
|
|
50
|
+
tracker: z.string().optional(),
|
|
51
|
+
scm: z.string().optional(),
|
|
52
|
+
runtime: z.string().optional(),
|
|
53
|
+
notifier: z.string().optional()
|
|
54
|
+
}).optional()
|
|
55
|
+
});
|
|
56
|
+
var configSchema = z.object({
|
|
57
|
+
version: z.literal("1"),
|
|
58
|
+
projects: z.array(projectSchema).min(1)
|
|
59
|
+
});
|
|
60
|
+
var DEFAULT_REACTION_RULES = {
|
|
61
|
+
"ci-failed": { auto: true, action: "fix", retries: 3, escalateAfter: 30 },
|
|
62
|
+
"changes-requested": { auto: true, action: "fix", retries: 2, escalateAfter: 60 },
|
|
63
|
+
"approved-and-green": { auto: false, action: "notify", retries: 0, escalateAfter: 0 }
|
|
64
|
+
};
|
|
65
|
+
function loadConfig(cwd) {
|
|
66
|
+
const dir = cwd ?? process.cwd();
|
|
67
|
+
const candidates = [
|
|
68
|
+
join(dir, "speexor.config.yaml"),
|
|
69
|
+
join(dir, "speexor.config.yml"),
|
|
70
|
+
join(dir, ".speexor.yaml"),
|
|
71
|
+
join(dir, ".speexor.yml"),
|
|
72
|
+
join(dir, "speexor.yaml")
|
|
73
|
+
];
|
|
74
|
+
let configPath;
|
|
75
|
+
for (const candidate of candidates) {
|
|
76
|
+
if (existsSync(candidate)) {
|
|
77
|
+
configPath = candidate;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!configPath) {
|
|
82
|
+
throw new Error(`speexor.config.yaml not found in ${dir}. Run \`speexor start <repo>\` to initialize.`);
|
|
83
|
+
}
|
|
84
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
85
|
+
const parsed = parse(raw);
|
|
86
|
+
return validateConfig(parsed);
|
|
87
|
+
}
|
|
88
|
+
function validateConfig(raw) {
|
|
89
|
+
const result = configSchema.safeParse(raw);
|
|
90
|
+
if (!result.success) {
|
|
91
|
+
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
92
|
+
throw new Error(`Invalid speexor config:
|
|
93
|
+
${issues}`);
|
|
94
|
+
}
|
|
95
|
+
for (const project of result.data.projects) {
|
|
96
|
+
if (!project.reactions) {
|
|
97
|
+
project.reactions = { ...DEFAULT_REACTION_RULES };
|
|
98
|
+
} else {
|
|
99
|
+
project.reactions = {
|
|
100
|
+
"ci-failed": project.reactions["ci-failed"] ?? DEFAULT_REACTION_RULES["ci-failed"],
|
|
101
|
+
"changes-requested": project.reactions["changes-requested"] ?? DEFAULT_REACTION_RULES["changes-requested"],
|
|
102
|
+
"approved-and-green": project.reactions["approved-and-green"] ?? DEFAULT_REACTION_RULES["approved-and-green"]
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return result.data;
|
|
107
|
+
}
|
|
108
|
+
function generateDefaultConfig(repoUrl, projectName) {
|
|
109
|
+
const name = projectName ?? repoUrl.split("/").pop()?.replace(".git", "") ?? "my-project";
|
|
110
|
+
return {
|
|
111
|
+
version: "1",
|
|
112
|
+
projects: [
|
|
113
|
+
{
|
|
114
|
+
name,
|
|
115
|
+
repository: repoUrl,
|
|
116
|
+
provider: {
|
|
117
|
+
primary: "opencode"
|
|
118
|
+
},
|
|
119
|
+
reactions: { ...DEFAULT_REACTION_RULES }
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
var debug = Debug("speexor:lifecycle");
|
|
125
|
+
var SpeexorLifecycle = class {
|
|
126
|
+
config;
|
|
127
|
+
eventBus;
|
|
128
|
+
plugins = /* @__PURE__ */ new Map();
|
|
129
|
+
sessions = /* @__PURE__ */ new Map();
|
|
130
|
+
status = "initializing";
|
|
131
|
+
constructor(config) {
|
|
132
|
+
this.config = config;
|
|
133
|
+
this.eventBus = createEventBus();
|
|
134
|
+
}
|
|
135
|
+
async initialize() {
|
|
136
|
+
debug("Initializing Speexor lifecycle");
|
|
137
|
+
this.status = "active";
|
|
138
|
+
this.eventBus.emit("lifecycle:initialized", { timestamp: /* @__PURE__ */ new Date() });
|
|
139
|
+
}
|
|
140
|
+
registerPlugin(plugin) {
|
|
141
|
+
const existing = this.plugins.get(plugin.type) ?? [];
|
|
142
|
+
existing.push(plugin);
|
|
143
|
+
this.plugins.set(plugin.type, existing);
|
|
144
|
+
debug(`Registered plugin: ${plugin.name} (${plugin.type})`);
|
|
145
|
+
}
|
|
146
|
+
getPlugins(slot) {
|
|
147
|
+
return this.plugins.get(slot) ?? [];
|
|
148
|
+
}
|
|
149
|
+
getFirstPlugin(slot) {
|
|
150
|
+
return this.getPlugins(slot)[0];
|
|
151
|
+
}
|
|
152
|
+
async spawnAgent(task) {
|
|
153
|
+
const agentPlugin = this.getFirstPlugin("agent");
|
|
154
|
+
if (!agentPlugin) throw new Error("No agent plugin registered");
|
|
155
|
+
const workspacePlugin = this.getFirstPlugin("workspace");
|
|
156
|
+
if (!workspacePlugin) throw new Error("No workspace plugin registered");
|
|
157
|
+
const runtimePlugin = this.getFirstPlugin("runtime");
|
|
158
|
+
if (!runtimePlugin) throw new Error("No runtime plugin registered");
|
|
159
|
+
const worktree = await workspacePlugin.createWorktree(task);
|
|
160
|
+
this.eventBus.emit("worktree:created", { taskId: task.id, path: worktree.path });
|
|
161
|
+
const runtimeSession = await runtimePlugin.createSession(worktree.path);
|
|
162
|
+
const session = await agentPlugin.spawn(task, runtimeSession);
|
|
163
|
+
this.sessions.set(session.id, session);
|
|
164
|
+
this.eventBus.emit("session:created", { session, task });
|
|
165
|
+
debug(`Agent spawned: ${session.id} for task ${task.id}`);
|
|
166
|
+
return session;
|
|
167
|
+
}
|
|
168
|
+
async stopSession(sessionId) {
|
|
169
|
+
const session = this.sessions.get(sessionId);
|
|
170
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
171
|
+
const agentPlugin = this.getFirstPlugin("agent");
|
|
172
|
+
if (agentPlugin) {
|
|
173
|
+
await agentPlugin.kill(sessionId);
|
|
174
|
+
}
|
|
175
|
+
this.sessions.delete(sessionId);
|
|
176
|
+
this.eventBus.emit("session:completed", { sessionId });
|
|
177
|
+
debug(`Session stopped: ${sessionId}`);
|
|
178
|
+
}
|
|
179
|
+
getSession(sessionId) {
|
|
180
|
+
return this.sessions.get(sessionId);
|
|
181
|
+
}
|
|
182
|
+
listSessions() {
|
|
183
|
+
return Array.from(this.sessions.values());
|
|
184
|
+
}
|
|
185
|
+
async destroy() {
|
|
186
|
+
this.status = "completed";
|
|
187
|
+
for (const [id] of this.sessions) {
|
|
188
|
+
await this.stopSession(id).catch(() => {
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const slots = ["terminal", "notifier", "scm", "tracker", "workspace", "runtime", "agent"];
|
|
192
|
+
for (const slot of slots) {
|
|
193
|
+
const plugins = this.plugins.get(slot) ?? [];
|
|
194
|
+
for (const plugin of plugins.reverse()) {
|
|
195
|
+
await plugin.destroy().catch(() => {
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
this.eventBus.emit("lifecycle:destroyed", { timestamp: /* @__PURE__ */ new Date() });
|
|
200
|
+
debug("Lifecycle destroyed");
|
|
201
|
+
}
|
|
202
|
+
getConfig() {
|
|
203
|
+
return this.config;
|
|
204
|
+
}
|
|
205
|
+
getStatus() {
|
|
206
|
+
return this.status;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export { DEFAULT_REACTION_RULES, SpeexorLifecycle, createEventBus, generateDefaultConfig, loadConfig, validateConfig };
|
|
211
|
+
//# sourceMappingURL=chunk-2F66BZYJ.js.map
|
|
212
|
+
//# sourceMappingURL=chunk-2F66BZYJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/event-bus.ts","../src/core/config.ts","../src/core/lifecycle.ts"],"names":[],"mappings":"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","file":"chunk-2F66BZYJ.js","sourcesContent":["import { EventEmitter } from 'eventemitter3'\nimport type { EventBus } from './types.js'\n\nexport function createEventBus(): EventBus {\n const emitter = new EventEmitter()\n\n return {\n emit(event, data) {\n emitter.emit(event, data)\n },\n on(event, handler) {\n emitter.on(event, handler)\n },\n off(event, handler) {\n emitter.off(event, handler)\n },\n once(event, handler) {\n emitter.once(event, handler)\n },\n }\n}\n","import { readFileSync, existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { parse } from 'yaml'\nimport { z } from 'zod'\nimport type { SpeexorConfig, ReactionConfig } from './types.js'\n\nconst reactionRuleSchema = z.object({\n auto: z.boolean(),\n action: z.enum(['fix', 'notify', 'escalate', 'skip']),\n retries: z.number().int().min(0).max(10),\n escalateAfter: z.number().int().min(1).max(1440),\n})\n\nconst providerRoutingSchema = z.object({\n primary: z.enum(['opencode', 'claude-code', 'aider', 'codex']),\n fallback: z.array(z.enum(['opencode', 'claude-code', 'aider', 'codex'])).optional(),\n concurrentLimit: z.number().int().min(1).max(20).optional(),\n costLimit: z.number().int().optional(),\n})\n\nconst projectSchema = z.object({\n name: z.string().min(1),\n repository: z.string().min(1),\n path: z.string().optional(),\n branch: z.string().optional(),\n provider: providerRoutingSchema,\n reactions: z\n .object({\n 'ci-failed': reactionRuleSchema.optional(),\n 'changes-requested': reactionRuleSchema.optional(),\n 'approved-and-green': reactionRuleSchema.optional(),\n })\n .optional(),\n plugins: z\n .object({\n tracker: z.string().optional(),\n scm: z.string().optional(),\n runtime: z.string().optional(),\n notifier: z.string().optional(),\n })\n .optional(),\n})\n\nconst configSchema = z.object({\n version: z.literal('1'),\n projects: z.array(projectSchema).min(1),\n})\n\nexport const DEFAULT_REACTION_RULES: ReactionConfig = {\n 'ci-failed': { auto: true, action: 'fix', retries: 3, escalateAfter: 30 },\n 'changes-requested': { auto: true, action: 'fix', retries: 2, escalateAfter: 60 },\n 'approved-and-green': { auto: false, action: 'notify', retries: 0, escalateAfter: 0 },\n}\n\nexport function loadConfig(cwd?: string): SpeexorConfig {\n const dir = cwd ?? process.cwd()\n\n const candidates = [\n join(dir, 'speexor.config.yaml'),\n join(dir, 'speexor.config.yml'),\n join(dir, '.speexor.yaml'),\n join(dir, '.speexor.yml'),\n join(dir, 'speexor.yaml'),\n ]\n\n let configPath: string | undefined\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n configPath = candidate\n break\n }\n }\n\n if (!configPath) {\n throw new Error(`speexor.config.yaml not found in ${dir}. ` + 'Run `speexor start <repo>` to initialize.')\n }\n\n const raw = readFileSync(configPath, 'utf-8')\n const parsed = parse(raw)\n\n return validateConfig(parsed)\n}\n\nexport function validateConfig(raw: unknown): SpeexorConfig {\n const result = configSchema.safeParse(raw)\n if (!result.success) {\n const issues = result.error.issues.map((i) => ` - ${i.path.join('.')}: ${i.message}`).join('\\n')\n throw new Error(`Invalid speexor config:\\n${issues}`)\n }\n\n for (const project of result.data.projects) {\n if (!project.reactions) {\n ;(project as { reactions: ReactionConfig }).reactions = { ...DEFAULT_REACTION_RULES }\n } else {\n project.reactions = {\n 'ci-failed': project.reactions['ci-failed'] ?? DEFAULT_REACTION_RULES['ci-failed'],\n 'changes-requested': project.reactions['changes-requested'] ?? DEFAULT_REACTION_RULES['changes-requested'],\n 'approved-and-green': project.reactions['approved-and-green'] ?? DEFAULT_REACTION_RULES['approved-and-green'],\n }\n }\n }\n\n return result.data as SpeexorConfig\n}\n\nexport function generateDefaultConfig(repoUrl: string, projectName?: string): SpeexorConfig {\n const name = projectName ?? repoUrl.split('/').pop()?.replace('.git', '') ?? 'my-project'\n return {\n version: '1',\n projects: [\n {\n name,\n repository: repoUrl,\n provider: {\n primary: 'opencode',\n },\n reactions: { ...DEFAULT_REACTION_RULES },\n },\n ],\n }\n}\n","import type {\n SpeexorConfig,\n AgentPlugin,\n RuntimePlugin,\n WorkspacePlugin,\n AgentTask,\n AgentSession,\n SessionStatus,\n EventBus,\n PluginModule,\n PluginSlot,\n} from './types.js'\nimport { createEventBus } from './event-bus.js'\nimport Debug from 'debug'\n\nconst debug = Debug('speexor:lifecycle')\n\nexport class SpeexorLifecycle {\n private config: SpeexorConfig\n public eventBus: EventBus\n private plugins: Map<PluginSlot, PluginModule[]> = new Map()\n private sessions: Map<string, AgentSession> = new Map()\n private status: SessionStatus = 'initializing'\n\n constructor(config: SpeexorConfig) {\n this.config = config\n this.eventBus = createEventBus()\n }\n\n async initialize(): Promise<void> {\n debug('Initializing Speexor lifecycle')\n this.status = 'active'\n this.eventBus.emit('lifecycle:initialized', { timestamp: new Date() })\n }\n\n registerPlugin(plugin: PluginModule): void {\n const existing = this.plugins.get(plugin.type) ?? []\n existing.push(plugin)\n this.plugins.set(plugin.type, existing)\n debug(`Registered plugin: ${plugin.name} (${plugin.type})`)\n }\n\n getPlugins<T extends PluginModule>(slot: PluginSlot): T[] {\n return (this.plugins.get(slot) ?? []) as T[]\n }\n\n getFirstPlugin<T extends PluginModule>(slot: PluginSlot): T | undefined {\n return this.getPlugins<T>(slot)[0]\n }\n\n async spawnAgent(task: AgentTask): Promise<AgentSession> {\n const agentPlugin = this.getFirstPlugin<AgentPlugin>('agent')\n if (!agentPlugin) throw new Error('No agent plugin registered')\n\n const workspacePlugin = this.getFirstPlugin<WorkspacePlugin>('workspace')\n if (!workspacePlugin) throw new Error('No workspace plugin registered')\n\n const runtimePlugin = this.getFirstPlugin<RuntimePlugin>('runtime')\n if (!runtimePlugin) throw new Error('No runtime plugin registered')\n\n const worktree = await workspacePlugin.createWorktree(task)\n this.eventBus.emit('worktree:created', { taskId: task.id, path: worktree.path })\n\n const runtimeSession = await runtimePlugin.createSession(worktree.path)\n\n const session = await agentPlugin.spawn(task, runtimeSession)\n this.sessions.set(session.id, session)\n\n this.eventBus.emit('session:created', { session, task })\n debug(`Agent spawned: ${session.id} for task ${task.id}`)\n\n return session\n }\n\n async stopSession(sessionId: string): Promise<void> {\n const session = this.sessions.get(sessionId)\n if (!session) throw new Error(`Session ${sessionId} not found`)\n\n const agentPlugin = this.getFirstPlugin<AgentPlugin>('agent')\n if (agentPlugin) {\n await agentPlugin.kill(sessionId)\n }\n\n this.sessions.delete(sessionId)\n this.eventBus.emit('session:completed', { sessionId })\n debug(`Session stopped: ${sessionId}`)\n }\n\n getSession(sessionId: string): AgentSession | undefined {\n return this.sessions.get(sessionId)\n }\n\n listSessions(): AgentSession[] {\n return Array.from(this.sessions.values())\n }\n\n async destroy(): Promise<void> {\n this.status = 'completed'\n\n for (const [id] of this.sessions) {\n await this.stopSession(id).catch(() => {})\n }\n\n const slots: PluginSlot[] = ['terminal', 'notifier', 'scm', 'tracker', 'workspace', 'runtime', 'agent']\n for (const slot of slots) {\n const plugins = this.plugins.get(slot) ?? []\n for (const plugin of plugins.reverse()) {\n await plugin.destroy().catch(() => {})\n }\n }\n\n this.eventBus.emit('lifecycle:destroyed', { timestamp: new Date() })\n debug('Lifecycle destroyed')\n }\n\n getConfig(): SpeexorConfig {\n return this.config\n }\n\n getStatus(): SessionStatus {\n return this.status\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-5NA2TFPG.js"}
|