erne-universal 0.9.0 → 0.10.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/CLAUDE.md +48 -0
- package/bin/cli.js +9 -0
- package/commands/worker.md +63 -0
- package/dashboard/server.js +38 -1
- package/lib/audit-cli.js +1 -1
- package/lib/worker.js +122 -0
- package/package.json +2 -2
- package/scripts/validate-all.js +1 -1
package/CLAUDE.md
CHANGED
|
@@ -125,6 +125,54 @@ When a user's message matches these signals, automatically use the corresponding
|
|
|
125
125
|
|
|
126
126
|
Use `/plan`, `/code-review`, `/tdd`, `/build-fix`, `/perf`, `/upgrade`, `/native-module`, `/navigate`, `/animate`, `/deploy`, `/component`, `/debug`, `/debug-visual`, `/audit`, `/quality-gate`, `/code`, `/feature`, `/learn`, `/retrospective`, `/setup-device` for guided workflows.
|
|
127
127
|
|
|
128
|
+
## Worker Mode (Autonomous Ticket Execution)
|
|
129
|
+
|
|
130
|
+
ERNE can run as an autonomous worker that polls a ticket provider, picks up ready tasks, and executes the full pipeline without human intervention.
|
|
131
|
+
|
|
132
|
+
### Quick Start
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
erne worker --config worker.json
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### How It Works
|
|
139
|
+
|
|
140
|
+
1. Polls a configured provider (ClickUp, GitHub Issues, Linear, Jira, or local JSON) for tickets marked as ready
|
|
141
|
+
2. Validates each ticket has sufficient detail (title, description, acceptance criteria)
|
|
142
|
+
3. Scores confidence (0-100) — skips tickets below the configured threshold
|
|
143
|
+
4. Generates an implementation plan from ticket context + project audit data
|
|
144
|
+
5. Executes in an isolated git worktree to avoid disrupting the main branch
|
|
145
|
+
6. Runs the test suite and verifies no regressions
|
|
146
|
+
7. Performs automated self-review against ERNE coding standards
|
|
147
|
+
8. Compares audit health score before and after changes
|
|
148
|
+
9. Creates a pull request with summary, test results, and ticket link
|
|
149
|
+
|
|
150
|
+
### Quality Gates
|
|
151
|
+
|
|
152
|
+
Each ticket must pass these gates before a PR is created:
|
|
153
|
+
- Confidence score above threshold (default: 70)
|
|
154
|
+
- All existing tests pass
|
|
155
|
+
- No audit score regression
|
|
156
|
+
- Self-review finds no critical issues
|
|
157
|
+
|
|
158
|
+
### Configuration
|
|
159
|
+
|
|
160
|
+
See `worker.example.json` for a full template. Key options:
|
|
161
|
+
- `provider.type` — clickup, github, linear, jira, local
|
|
162
|
+
- `provider.poll_interval_seconds` — How often to check for new tickets (default: 60)
|
|
163
|
+
- `erne.min_confidence` — Minimum confidence score to attempt a ticket (default: 70)
|
|
164
|
+
- `erne.hook_profile` — ERNE hook profile to use (minimal, standard, strict)
|
|
165
|
+
- `repo.path` — Absolute path to the local repository
|
|
166
|
+
- `repo.base_branch` — Branch to create worktrees from (default: main)
|
|
167
|
+
|
|
168
|
+
### CLI Options
|
|
169
|
+
|
|
170
|
+
| Flag | Description |
|
|
171
|
+
|------|-------------|
|
|
172
|
+
| `--config <path>` | Path to worker config JSON (required) |
|
|
173
|
+
| `--dry-run` | Fetch and display tickets without executing |
|
|
174
|
+
| `--once` | Process one ticket then exit |
|
|
175
|
+
|
|
128
176
|
## Available Skills
|
|
129
177
|
|
|
130
178
|
Skills in `skills/` activate automatically:
|
package/bin/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ const COMMANDS = {
|
|
|
18
18
|
doctor: () => require('../lib/doctor'),
|
|
19
19
|
status: () => require('../lib/status'),
|
|
20
20
|
audit: () => require('../lib/audit-cli'),
|
|
21
|
+
worker: () => require('../lib/worker'),
|
|
21
22
|
'sync-configs': () => require('../lib/sync-configs'),
|
|
22
23
|
sync: () => require('../lib/sync-configs'),
|
|
23
24
|
version: () => {
|
|
@@ -41,10 +42,16 @@ const COMMANDS = {
|
|
|
41
42
|
start Init project and start dashboard
|
|
42
43
|
doctor Check project health and ERNE setup
|
|
43
44
|
status Show current ERNE configuration
|
|
45
|
+
worker Run autonomous ticket execution agent
|
|
44
46
|
sync-configs Sync IDE config files from CLAUDE.md (alias: sync)
|
|
45
47
|
version Show installed version
|
|
46
48
|
help Show this help message
|
|
47
49
|
|
|
50
|
+
Worker options:
|
|
51
|
+
--config <path> Path to worker config JSON (required)
|
|
52
|
+
--dry-run Fetch tickets without executing
|
|
53
|
+
--once Process one ticket and exit
|
|
54
|
+
|
|
48
55
|
Add-agent options:
|
|
49
56
|
--room <name> Agent room: development, code-review, testing, conference (default: development)
|
|
50
57
|
|
|
@@ -63,6 +70,8 @@ const COMMANDS = {
|
|
|
63
70
|
npx erne-universal init -p minimal --no-mcp -y
|
|
64
71
|
npx erne-universal add-agent api-specialist
|
|
65
72
|
npx erne-universal add-agent database-expert --room testing
|
|
73
|
+
npx erne-universal worker --config worker.json
|
|
74
|
+
npx erne-universal worker --config worker.json --dry-run
|
|
66
75
|
|
|
67
76
|
Website: https://erne.dev
|
|
68
77
|
`);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: worker
|
|
3
|
+
description: Autonomous ticket execution — polls a provider, picks up ready tickets, and runs the full ERNE pipeline (validate, plan, code, test, review, PR).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /worker — Autonomous Ticket Execution
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
erne worker --config <path-to-worker.json>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Options
|
|
15
|
+
|
|
16
|
+
| Flag | Description |
|
|
17
|
+
|------|-------------|
|
|
18
|
+
| `--config <path>` | Path to worker configuration JSON (required) |
|
|
19
|
+
| `--dry-run` | Fetch tickets and print them without executing |
|
|
20
|
+
| `--once` | Process one ticket and exit |
|
|
21
|
+
|
|
22
|
+
## Pipeline Steps
|
|
23
|
+
|
|
24
|
+
1. **Poll** — Fetch ready tickets from the configured provider
|
|
25
|
+
2. **Validate** — Check ticket has enough detail (title, description, acceptance criteria)
|
|
26
|
+
3. **Confidence Score** — Estimate likelihood of autonomous success (0-100)
|
|
27
|
+
4. **Context Resolve** — Load project stack, audit data, and relevant files
|
|
28
|
+
5. **Plan** — Generate implementation plan from ticket + context
|
|
29
|
+
6. **Execute** — Run Claude Code in an isolated git worktree
|
|
30
|
+
7. **Test** — Run test suite, verify no regressions
|
|
31
|
+
8. **Self-Review** — Automated code review against ERNE standards
|
|
32
|
+
9. **Health Delta** — Compare audit score before/after
|
|
33
|
+
10. **PR** — Create pull request with full summary and link to ticket
|
|
34
|
+
|
|
35
|
+
## Supported Providers
|
|
36
|
+
|
|
37
|
+
- **clickup** — ClickUp tasks (API token + list ID)
|
|
38
|
+
- **github** — GitHub Issues (repo + labels)
|
|
39
|
+
- **linear** — Linear issues (API key + team)
|
|
40
|
+
- **jira** — Jira issues (API token + project)
|
|
41
|
+
- **local** — JSON file with ticket definitions (for testing)
|
|
42
|
+
|
|
43
|
+
## Configuration Reference
|
|
44
|
+
|
|
45
|
+
See `worker.example.json` at the project root for a full example. Key sections:
|
|
46
|
+
|
|
47
|
+
- `provider` — Type, credentials, poll interval, filters
|
|
48
|
+
- `repo` — Local path, base branch, remote
|
|
49
|
+
- `erne` — Hook profile, quality gates, confidence threshold
|
|
50
|
+
- `log` — File path and log level
|
|
51
|
+
|
|
52
|
+
## Examples
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Dry run to see available tickets
|
|
56
|
+
erne worker --config worker.json --dry-run
|
|
57
|
+
|
|
58
|
+
# Process one ticket and stop
|
|
59
|
+
erne worker --config worker.json --once
|
|
60
|
+
|
|
61
|
+
# Continuous polling mode
|
|
62
|
+
erne worker --config worker.json
|
|
63
|
+
```
|
package/dashboard/server.js
CHANGED
|
@@ -176,6 +176,7 @@ const persistHistory = () => {
|
|
|
176
176
|
|
|
177
177
|
let lastActiveAgent = null;
|
|
178
178
|
let lastAudit = null;
|
|
179
|
+
let lastWorkerState = null;
|
|
179
180
|
|
|
180
181
|
const handleEvent = (event) => {
|
|
181
182
|
const { type, agent, agents: agentList, task } = event;
|
|
@@ -213,6 +214,35 @@ const handleEvent = (event) => {
|
|
|
213
214
|
return { ok: true };
|
|
214
215
|
}
|
|
215
216
|
|
|
217
|
+
// Worker events — update worker state and broadcast
|
|
218
|
+
if (type && type.startsWith('worker:')) {
|
|
219
|
+
const now = new Date().toISOString();
|
|
220
|
+
if (type === 'worker:start') {
|
|
221
|
+
lastWorkerState = { status: 'polling', provider: event.provider, repo: event.repo, interval: event.interval, startedAt: now, lastEvent: now, currentTicket: null };
|
|
222
|
+
} else if (type === 'worker:task-start') {
|
|
223
|
+
if (lastWorkerState) {
|
|
224
|
+
lastWorkerState.status = 'working';
|
|
225
|
+
lastWorkerState.currentTicket = { identifier: event.identifier, title: event.title, startedAt: now };
|
|
226
|
+
lastWorkerState.lastEvent = now;
|
|
227
|
+
}
|
|
228
|
+
} else if (type === 'worker:task-complete') {
|
|
229
|
+
if (lastWorkerState) {
|
|
230
|
+
lastWorkerState.status = 'polling';
|
|
231
|
+
lastWorkerState.currentTicket = null;
|
|
232
|
+
lastWorkerState.lastEvent = now;
|
|
233
|
+
lastWorkerState.lastCompleted = { identifier: event.identifier, title: event.title, result: event.result, completedAt: now };
|
|
234
|
+
}
|
|
235
|
+
} else if (type === 'worker:idle') {
|
|
236
|
+
if (lastWorkerState) {
|
|
237
|
+
lastWorkerState.status = 'polling';
|
|
238
|
+
lastWorkerState.lastEvent = now;
|
|
239
|
+
}
|
|
240
|
+
} else if (lastWorkerState) {
|
|
241
|
+
lastWorkerState.lastEvent = now;
|
|
242
|
+
}
|
|
243
|
+
return { ok: true };
|
|
244
|
+
}
|
|
245
|
+
|
|
216
246
|
if (!agent || !agentState[agent]) {
|
|
217
247
|
return { error: `Unknown agent: ${agent}` };
|
|
218
248
|
}
|
|
@@ -603,6 +633,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
603
633
|
return;
|
|
604
634
|
}
|
|
605
635
|
|
|
636
|
+
if (req.method === 'GET' && req.url === '/api/worker') {
|
|
637
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
638
|
+
res.end(JSON.stringify(lastWorkerState || { status: 'offline' }));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
606
642
|
if (req.method === 'POST' && req.url === '/api/audit/run') {
|
|
607
643
|
try {
|
|
608
644
|
const projectDir = process.env.ERNE_PROJECT_DIR || process.cwd();
|
|
@@ -702,7 +738,8 @@ wss.on('connection', (ws) => {
|
|
|
702
738
|
|
|
703
739
|
// Validate event shape before processing
|
|
704
740
|
if (!data || typeof data !== 'object' || typeof data.type !== 'string') return;
|
|
705
|
-
const VALID_TYPES = ['agent:start', 'agent:complete', 'planning:start', 'planning:end', 'audit:complete'
|
|
741
|
+
const VALID_TYPES = ['agent:start', 'agent:complete', 'planning:start', 'planning:end', 'audit:complete',
|
|
742
|
+
'worker:start', 'worker:poll', 'worker:task-start', 'worker:task-complete', 'worker:idle'];
|
|
706
743
|
if (!VALID_TYPES.includes(data.type)) return;
|
|
707
744
|
|
|
708
745
|
const result = handleEvent(data);
|
package/lib/audit-cli.js
CHANGED
|
@@ -283,7 +283,7 @@ Full data: \`erne-docs/audit-data.json\`
|
|
|
283
283
|
function postDashboardEvent(data) {
|
|
284
284
|
try {
|
|
285
285
|
const payload = JSON.stringify({
|
|
286
|
-
type: 'audit
|
|
286
|
+
type: 'audit:complete',
|
|
287
287
|
timestamp: new Date().toISOString(),
|
|
288
288
|
summary: {
|
|
289
289
|
totalFiles: data.meta ? data.meta.totalSourceFiles : 0,
|
package/lib/worker.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
module.exports = async function worker() {
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const configIdx = args.indexOf('--config');
|
|
7
|
+
const configPath = configIdx !== -1 ? args[configIdx + 1] : null;
|
|
8
|
+
const dryRun = args.includes('--dry-run');
|
|
9
|
+
const once = args.includes('--once');
|
|
10
|
+
|
|
11
|
+
if (!configPath) {
|
|
12
|
+
console.error(' Usage: erne worker --config <path.json>');
|
|
13
|
+
console.error(' Options: --dry-run, --once');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { loadConfig, validateConfig } = require('../worker/config');
|
|
18
|
+
const { createProvider } = require('../worker/providers/factory');
|
|
19
|
+
const { createPoller } = require('../worker/poller');
|
|
20
|
+
const { processTicket } = require('../worker/scheduler');
|
|
21
|
+
const { createLogger } = require('../worker/logger');
|
|
22
|
+
const { publishDashboardEvent } = require('../worker/dashboard-events');
|
|
23
|
+
|
|
24
|
+
// 1. Load config
|
|
25
|
+
const fullPath = path.resolve(configPath);
|
|
26
|
+
const config = loadConfig(fullPath);
|
|
27
|
+
const validation = validateConfig(config);
|
|
28
|
+
if (!validation.valid) {
|
|
29
|
+
console.error(' Config errors:');
|
|
30
|
+
validation.errors.forEach(e => console.error(' \u2717 ' + e));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2. Create logger
|
|
35
|
+
const logger = createLogger({ file: config.log?.file, level: config.log?.level || 'info' });
|
|
36
|
+
|
|
37
|
+
// 3. Detect project and scan (if audit-scanner available)
|
|
38
|
+
let stackInfo = { layers: [] };
|
|
39
|
+
let auditData = {};
|
|
40
|
+
try {
|
|
41
|
+
const { detectProject } = require('./detect');
|
|
42
|
+
stackInfo = detectProject(config.repo.path);
|
|
43
|
+
} catch { /* detect not critical */ }
|
|
44
|
+
try {
|
|
45
|
+
const { runScan } = require('./audit-scanner');
|
|
46
|
+
if (config.erne?.audit_refresh !== false) {
|
|
47
|
+
auditData = runScan(config.repo.path, { skipDepHealth: true, maxFiles: 500 });
|
|
48
|
+
}
|
|
49
|
+
} catch { /* scan not critical */ }
|
|
50
|
+
|
|
51
|
+
// 4. Create provider
|
|
52
|
+
const provider = createProvider(config, logger);
|
|
53
|
+
|
|
54
|
+
// 5. Print banner
|
|
55
|
+
const pollSec = config.provider.poll_interval_seconds || 60;
|
|
56
|
+
console.log(`
|
|
57
|
+
erne worker — Autonomous Agent
|
|
58
|
+
|
|
59
|
+
Provider: ${config.provider.type}
|
|
60
|
+
Repo: ${config.repo.path}
|
|
61
|
+
Profile: ${config.erne?.hook_profile || 'standard'}
|
|
62
|
+
Polling every ${pollSec}s
|
|
63
|
+
${dryRun ? ' Mode: DRY RUN (no execution)\n' : ''}
|
|
64
|
+
Waiting for tickets...
|
|
65
|
+
`);
|
|
66
|
+
|
|
67
|
+
// 6. Dry run mode
|
|
68
|
+
if (dryRun) {
|
|
69
|
+
logger.info('Dry run — fetching tickets...');
|
|
70
|
+
try {
|
|
71
|
+
const tickets = await provider.fetchReadyTickets();
|
|
72
|
+
if (tickets.length === 0) {
|
|
73
|
+
console.log(' No ready tickets found.');
|
|
74
|
+
} else {
|
|
75
|
+
console.log(` ${tickets.length} ticket(s) found:`);
|
|
76
|
+
tickets.forEach(t => console.log(` - ${t.identifier}: ${t.title}`));
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error(' Error fetching tickets:', err.message);
|
|
80
|
+
}
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 7. Publish start event
|
|
85
|
+
publishDashboardEvent('worker:start', { provider: config.provider.type, repo: config.repo.path, interval: pollSec });
|
|
86
|
+
|
|
87
|
+
// 8. Create poller
|
|
88
|
+
const poller = createPoller({
|
|
89
|
+
provider,
|
|
90
|
+
intervalMs: pollSec * 1000,
|
|
91
|
+
logger,
|
|
92
|
+
onTicket: async (ticket) => {
|
|
93
|
+
await processTicket({ ticket, provider, config, auditData, stackInfo, logger });
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// 9. Handle shutdown
|
|
98
|
+
const shutdown = () => {
|
|
99
|
+
logger.info('Shutting down worker...');
|
|
100
|
+
poller.stop();
|
|
101
|
+
};
|
|
102
|
+
process.on('SIGINT', shutdown);
|
|
103
|
+
process.on('SIGTERM', shutdown);
|
|
104
|
+
|
|
105
|
+
// 10. Start (once mode or continuous)
|
|
106
|
+
if (once) {
|
|
107
|
+
logger.info('Once mode — processing first available ticket');
|
|
108
|
+
try {
|
|
109
|
+
const tickets = await provider.fetchReadyTickets();
|
|
110
|
+
if (tickets.length > 0) {
|
|
111
|
+
await processTicket({ ticket: tickets[0], provider, config, auditData, stackInfo, logger });
|
|
112
|
+
} else {
|
|
113
|
+
logger.info('No ready tickets found');
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
logger.error('Error:', { error: err.message });
|
|
117
|
+
}
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await poller.start();
|
|
122
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "erne-universal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Complete AI coding agent harness for React Native and Expo development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"node": ">=18"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
|
-
"test": "node --test tests/*.test.js tests/hooks/*.test.js tests/context/*.test.js",
|
|
61
|
+
"test": "node --test tests/*.test.js tests/hooks/*.test.js tests/context/*.test.js tests/worker/*.test.js",
|
|
62
62
|
"lint": "eslint lib/ scripts/ bin/",
|
|
63
63
|
"lint:agents": "node scripts/lint-agents.js",
|
|
64
64
|
"lint:content": "node scripts/lint-content.js",
|
package/scripts/validate-all.js
CHANGED
|
@@ -68,7 +68,7 @@ for (const f of agentFiles) {
|
|
|
68
68
|
|
|
69
69
|
// Commands
|
|
70
70
|
console.log(' Commands:');
|
|
71
|
-
validateCount('commands', '.md',
|
|
71
|
+
validateCount('commands', '.md', 21, 'commands/');
|
|
72
72
|
const cmdFiles = fs.readdirSync('commands').filter(f => f.endsWith('.md'));
|
|
73
73
|
for (const f of cmdFiles) {
|
|
74
74
|
validateFrontmatter(path.join('commands', f), ['name', 'description']);
|