@arungeorgesaji/assembly 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/.env.example +7 -0
- package/LICENSE +674 -0
- package/README.md +455 -0
- package/ngrok-example.yml +9 -0
- package/package.json +23 -0
- package/src/agent-profiles.js +167 -0
- package/src/agent-runner.js +37 -0
- package/src/approval.js +45 -0
- package/src/cli.js +381 -0
- package/src/config.js +90 -0
- package/src/context-builder.js +91 -0
- package/src/doctor.js +151 -0
- package/src/file-updates.js +89 -0
- package/src/follow-up.js +33 -0
- package/src/git-worktree.js +35 -0
- package/src/github-webhooks.js +159 -0
- package/src/github.js +261 -0
- package/src/init.js +91 -0
- package/src/job-store.js +97 -0
- package/src/job-worker.js +390 -0
- package/src/models.js +55 -0
- package/src/openai-agent-runner.js +141 -0
- package/src/orchestrator.js +221 -0
- package/src/patch.js +78 -0
- package/src/planner.js +279 -0
- package/src/repo-inspector.js +88 -0
- package/src/report.js +46 -0
- package/src/result-validation.js +55 -0
- package/src/review-agent-runner.js +127 -0
- package/src/root.js +51 -0
- package/src/run-store.js +104 -0
- package/src/scope.js +107 -0
- package/src/slack-thread-store.js +34 -0
- package/src/slack-webhooks.js +120 -0
- package/src/slack.js +31 -0
- package/src/validation.js +33 -0
- package/src/verification-runner.js +51 -0
- package/src/webhook-server.js +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# Assembly
|
|
2
|
+
|
|
3
|
+
Assembly is a coordination layer for AI software engineering teams.
|
|
4
|
+
|
|
5
|
+
Modern coding agents are powerful, but they usually work alone with broad access to an entire codebase. Assembly introduces the structure used by effective engineering organizations: planning, ownership, specialization, review, testing, traceability, and human-controlled delivery.
|
|
6
|
+
|
|
7
|
+
Assembly decomposes software development work into focused tasks, assigns those tasks to specialized agents, validates their outputs, and produces merge-ready pull requests with complete execution history.
|
|
8
|
+
|
|
9
|
+
## What Assembly Does
|
|
10
|
+
|
|
11
|
+
Assembly turns a requested software change into a structured engineering workflow.
|
|
12
|
+
|
|
13
|
+
1. A Planner Agent analyzes the repository and the requested change.
|
|
14
|
+
2. The planner creates an execution plan with tasks, dependencies, file ownership, and acceptance criteria.
|
|
15
|
+
3. Specialized agents receive only the context required for their assigned work.
|
|
16
|
+
4. Agents produce patches, reports, tests, review notes, and other artifacts.
|
|
17
|
+
5. Assembly validates outputs against the original plan and routes them to downstream agents.
|
|
18
|
+
6. Testing and review agents verify behavior, regressions, code quality, architecture, and security.
|
|
19
|
+
7. Assembly creates a merge-ready pull request with a full summary of changes and findings.
|
|
20
|
+
|
|
21
|
+
Human engineers remain in control of the final merge decision.
|
|
22
|
+
|
|
23
|
+
## Why Assembly
|
|
24
|
+
|
|
25
|
+
AI engineering work needs more than code generation. It needs coordination.
|
|
26
|
+
|
|
27
|
+
Without structure, agents can make unrelated edits, miss dependencies, duplicate work, overlook regressions, or lose the reasoning behind implementation decisions. Assembly is designed to give AI agents the same operating model that makes human engineering teams effective:
|
|
28
|
+
|
|
29
|
+
- Clear task ownership
|
|
30
|
+
- Limited and relevant context
|
|
31
|
+
- Explicit dependencies
|
|
32
|
+
- Acceptance criteria
|
|
33
|
+
- Structured handoffs
|
|
34
|
+
- Test and review gates
|
|
35
|
+
- Traceable decisions
|
|
36
|
+
- Human approval before merge
|
|
37
|
+
|
|
38
|
+
## Core Concepts
|
|
39
|
+
|
|
40
|
+
### Planner Agent
|
|
41
|
+
|
|
42
|
+
The Planner Agent is responsible for understanding the repository, interpreting the requested change, and producing an execution plan. The plan defines what needs to happen, which agents should do the work, what files or systems they own, and how success will be verified.
|
|
43
|
+
|
|
44
|
+
### Specialized Agents
|
|
45
|
+
|
|
46
|
+
Assembly assigns work to agents with focused responsibilities, such as:
|
|
47
|
+
|
|
48
|
+
- Frontend development
|
|
49
|
+
- Backend implementation
|
|
50
|
+
- Database migrations
|
|
51
|
+
- Infrastructure changes
|
|
52
|
+
- Test generation and execution
|
|
53
|
+
- Security review
|
|
54
|
+
- Documentation
|
|
55
|
+
- Code review
|
|
56
|
+
|
|
57
|
+
Each agent receives scoped context for its task instead of unrestricted access to the entire project history and plan.
|
|
58
|
+
|
|
59
|
+
### Structured Contracts
|
|
60
|
+
|
|
61
|
+
Agents do not coordinate through unstructured back-and-forth communication. Assembly uses structured contracts: tasks, dependencies, artifacts, patches, reports, acceptance criteria, and validation results.
|
|
62
|
+
|
|
63
|
+
This keeps collaboration auditable and reduces unintended changes across unrelated parts of the codebase.
|
|
64
|
+
|
|
65
|
+
### Validation And Review
|
|
66
|
+
|
|
67
|
+
Assembly validates agent outputs before they are passed forward. Testing agents verify acceptance criteria and regressions. Review agents inspect code quality, architecture, security concerns, and conflicts between changes from different agents.
|
|
68
|
+
|
|
69
|
+
### Real-Time Visibility
|
|
70
|
+
|
|
71
|
+
Assembly provides a live view of:
|
|
72
|
+
|
|
73
|
+
- Current plan
|
|
74
|
+
- Task ownership
|
|
75
|
+
- Agent progress
|
|
76
|
+
- Dependencies
|
|
77
|
+
- Changed files
|
|
78
|
+
- Test status
|
|
79
|
+
- Review results
|
|
80
|
+
- Blockers
|
|
81
|
+
- Decisions and rationale
|
|
82
|
+
|
|
83
|
+
Developers can see what each agent is doing, why a decision was made, and how the system arrived at the final pull request.
|
|
84
|
+
|
|
85
|
+
## Slack Workflow
|
|
86
|
+
|
|
87
|
+
Assembly integrates with Slack for teams that already coordinate engineering work there.
|
|
88
|
+
|
|
89
|
+
Developers can request changes, ask questions, or provide feedback from Slack. Assembly routes those requests to the relevant agents, updates the execution plan, generates new changes, refreshes validation, and updates the pull request while preserving traceability.
|
|
90
|
+
|
|
91
|
+
## Pull Request Feedback
|
|
92
|
+
|
|
93
|
+
Developers can also request changes directly from the pull request. Assembly treats PR comments and review feedback as part of the active workflow, routes them to the relevant agents, updates the plan when needed, generates follow-up changes, reruns validation, and refreshes the pull request with the latest results.
|
|
94
|
+
|
|
95
|
+
## Pull Request Output
|
|
96
|
+
|
|
97
|
+
When work is complete, Assembly prepares a merge-ready pull request that includes:
|
|
98
|
+
|
|
99
|
+
- Summary of changes
|
|
100
|
+
- Agent contributions
|
|
101
|
+
- Files changed
|
|
102
|
+
- Test results
|
|
103
|
+
- Security findings
|
|
104
|
+
- Review recommendations
|
|
105
|
+
- Known risks or blockers
|
|
106
|
+
- Links to relevant artifacts and decisions
|
|
107
|
+
|
|
108
|
+
The pull request is intended to be understandable, auditable, and ready for human review.
|
|
109
|
+
|
|
110
|
+
## Project Status
|
|
111
|
+
|
|
112
|
+
Assembly is under active development.
|
|
113
|
+
|
|
114
|
+
The goal is not to replace engineers. The goal is to make AI-assisted development more structured, accountable, reviewable, and safe for real software teams.
|
|
115
|
+
|
|
116
|
+
## Getting Started
|
|
117
|
+
|
|
118
|
+
Assembly ships as a Node.js CLI. The deterministic stub runner works without API keys; provider-backed implementation and review require an OpenAI API key.
|
|
119
|
+
|
|
120
|
+
Prerequisites:
|
|
121
|
+
|
|
122
|
+
- Node.js 20 or newer
|
|
123
|
+
|
|
124
|
+
Install globally:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm install -g @arungeorgesaji/assembly
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Initialize a target repository:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
cd my-repo
|
|
134
|
+
assembly init
|
|
135
|
+
assembly doctor
|
|
136
|
+
assembly webhook --port 3000
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Assembly resolves the target to the Git repository root, even when launched from a subdirectory. Use `--repo <path>` to target another checkout explicitly.
|
|
140
|
+
|
|
141
|
+
For local development on Assembly itself:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm install
|
|
145
|
+
npm link
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`assembly init` creates `.assembly/` and a starter `.env`. Fill in values as needed:
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
ASSEMBLY_AGENT_PROVIDER=openai
|
|
152
|
+
ASSEMBLY_APPROVAL_MODE=auto
|
|
153
|
+
OPENAI_API_KEY=your_api_key
|
|
154
|
+
OPENAI_MODEL=gpt-4.1-mini
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Leave `ASSEMBLY_AGENT_PROVIDER=stub` to run without an API key.
|
|
158
|
+
|
|
159
|
+
Check missing local setup:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
assembly doctor
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Create a plan with npm:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
npm run plan -- "Add Slack workflow support" --pretty
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Create a persisted local run:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
npm run run -- "Add Slack workflow support" --pretty
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Check a run:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
assembly status <run-id>
|
|
181
|
+
assembly inspect <run-id> --pretty
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Each run writes:
|
|
185
|
+
|
|
186
|
+
```text
|
|
187
|
+
.assembly/runs/<run-id>/
|
|
188
|
+
request.json
|
|
189
|
+
plan.json
|
|
190
|
+
state.json
|
|
191
|
+
events.jsonl
|
|
192
|
+
final-report.md
|
|
193
|
+
artifacts/
|
|
194
|
+
<task-id>/
|
|
195
|
+
result.json
|
|
196
|
+
patch.diff
|
|
197
|
+
file-updates.json
|
|
198
|
+
verification/
|
|
199
|
+
result.json
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Run tests:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
npm test
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Current Implementation
|
|
209
|
+
|
|
210
|
+
The first working slice includes:
|
|
211
|
+
|
|
212
|
+
- A structured execution plan model
|
|
213
|
+
- Task ownership, dependencies, acceptance criteria, risks, and verification steps
|
|
214
|
+
- Task folder/file scopes with allowlists and denylists
|
|
215
|
+
- A deterministic request-aware planner for turning a change request into a task graph
|
|
216
|
+
- Request-specific task shapes for documentation, tests, refactors, and general code changes
|
|
217
|
+
- Repository-aware planning that inspects source, test, documentation, config files, package scripts, and narrows task scopes when request terms identify likely targets
|
|
218
|
+
- Lightweight repository inspection for package type and verification commands
|
|
219
|
+
- Plan validation for missing owners, missing acceptance criteria, and invalid dependencies
|
|
220
|
+
- A CLI that emits plan JSON
|
|
221
|
+
- A local run store under `.assembly/runs/<run-id>/`
|
|
222
|
+
- A stub agent runner that produces per-task artifacts
|
|
223
|
+
- Optional OpenAI agent runner selected with `ASSEMBLY_AGENT_PROVIDER=openai`
|
|
224
|
+
- Optional OpenAI review agent that inspects completed task results and verification output
|
|
225
|
+
- Local `.env` loading for `OPENAI_API_KEY` and `OPENAI_MODEL`
|
|
226
|
+
- Approval gate before applying edits, defaulting to `ASSEMBLY_APPROVAL_MODE=auto`
|
|
227
|
+
- Agent result validation for task id, terminal status, summary, changed files, artifacts, and risks
|
|
228
|
+
- Changed-file validation against each task's assigned scope
|
|
229
|
+
- Final git diff validation before PR creation or update to reject files that were not owned by the completed run
|
|
230
|
+
- Unified diff patch validation and local application through `git apply`
|
|
231
|
+
- Structured full-file updates for reliable local edits when patch generation is too brittle
|
|
232
|
+
- Additive change policy for `Add ...` requests to prevent accidental line removals
|
|
233
|
+
- Post-run verification command execution with stored stdout, stderr, and exit codes
|
|
234
|
+
- Blocked and failed run handling
|
|
235
|
+
- Final report generation for every run
|
|
236
|
+
- Status and inspect commands for persisted runs
|
|
237
|
+
|
|
238
|
+
Stronger provider-backed coding behavior, richer Slack command syntax, and production deployment hardening are planned next layers.
|
|
239
|
+
|
|
240
|
+
## Agent Result Contract
|
|
241
|
+
|
|
242
|
+
Agents must return structured results:
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"taskId": "task-id",
|
|
247
|
+
"status": "complete",
|
|
248
|
+
"summary": "What happened.",
|
|
249
|
+
"changedFiles": [],
|
|
250
|
+
"artifacts": ["result.json"],
|
|
251
|
+
"risks": [],
|
|
252
|
+
"patch": "",
|
|
253
|
+
"fileUpdates": []
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Valid statuses are `complete`, `blocked`, and `failed`. A completed task must include at least one artifact. If `patch` is present, it must be a unified diff whose changed files are all listed in `changedFiles` and allowed by the task scope. Agents can also return `fileUpdates` entries with full replacement file contents. If the result does not match the dispatched task or fails validation, Assembly marks the task and run as failed.
|
|
258
|
+
|
|
259
|
+
## Task Scope Contract
|
|
260
|
+
|
|
261
|
+
Each task includes a scope:
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"scope": {
|
|
266
|
+
"paths": ["src/", "tests/"],
|
|
267
|
+
"allowlist": ["package.json"],
|
|
268
|
+
"denylist": [".env", ".git/", ".assembly/"]
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Every reported changed file must be a safe relative path inside `paths` or explicitly listed in `allowlist`. Denylisted paths always fail validation. Absolute paths and `../` traversal are rejected.
|
|
274
|
+
|
|
275
|
+
## Local Code Editing Flow
|
|
276
|
+
|
|
277
|
+
For local execution, an agent can return a unified diff in `patch`. Assembly then:
|
|
278
|
+
|
|
279
|
+
1. extracts changed files from the patch
|
|
280
|
+
2. validates those files against the task scope
|
|
281
|
+
3. verifies every patch file is listed in `changedFiles`
|
|
282
|
+
4. writes `artifacts/<task-id>/patch.diff`
|
|
283
|
+
5. applies the patch with `git apply --check` followed by `git apply`
|
|
284
|
+
6. runs detected verification commands such as `npm test`
|
|
285
|
+
7. writes verification output to `artifacts/verification/result.json`
|
|
286
|
+
|
|
287
|
+
For model-generated edits, Assembly also supports `fileUpdates`:
|
|
288
|
+
|
|
289
|
+
```json
|
|
290
|
+
{
|
|
291
|
+
"fileUpdates": [
|
|
292
|
+
{
|
|
293
|
+
"path": "README.md",
|
|
294
|
+
"content": "full replacement file content"
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Assembly validates each update path against task scope before writing it. For requests that begin with `Add`, implementation tasks use an additive change policy: existing file lines must remain in the same order, so accidental rewrites or removals fail validation.
|
|
301
|
+
|
|
302
|
+
## Planning And Review
|
|
303
|
+
|
|
304
|
+
The local planner is deterministic but request-aware:
|
|
305
|
+
|
|
306
|
+
- documentation requests create a documentation task scoped to `README.md`
|
|
307
|
+
- test requests create a test task scoped to `tests/`
|
|
308
|
+
- refactor requests create a refactor task scoped to implementation files
|
|
309
|
+
- general code requests create an implementation task scoped to `src/`, `tests/`, and selected project files
|
|
310
|
+
|
|
311
|
+
When `ASSEMBLY_AGENT_PROVIDER=openai` is enabled, OpenAI currently handles implementation and review tasks. Planner tasks remain deterministic. Review runs after implementation verification, inspects the run state and artifacts, and can mark the workflow `complete`, `blocked`, or `failed`.
|
|
312
|
+
|
|
313
|
+
Plans also include dynamic agent profiles. These are not predefined personas; Assembly derives them from each task's owner, scope, denylist, and change policy. Tasks keep a stable `owner` for dispatch compatibility and reference an `agentProfileId` for the scoped delegation contract. The profile is passed to implementation and review agents so they can honor the exact owned paths without requiring a hardcoded list such as frontend/test/docs agents.
|
|
314
|
+
|
|
315
|
+
## Approval Modes
|
|
316
|
+
|
|
317
|
+
Assembly validates agent output before any edit is applied, then passes the result through an approval gate:
|
|
318
|
+
|
|
319
|
+
- `auto`: apply validated edits immediately
|
|
320
|
+
- `manual`: pause the run before applying edits and mark it blocked
|
|
321
|
+
- `never`: dry-run mode; write artifacts but do not apply edits
|
|
322
|
+
|
|
323
|
+
Local CLI defaults to `auto`. Slack and GitHub flows can use `manual` later for human approval before delivery.
|
|
324
|
+
|
|
325
|
+
## Follow-Up And GitHub Flow
|
|
326
|
+
|
|
327
|
+
Create a local follow-up run from feedback:
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
assembly follow-up <run-id> "Address this feedback" --pretty
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Create a GitHub pull request from a completed run:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
assembly github create-pr <run-id>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
The GitHub PR command uses `gh` and `git`. It creates and pushes a branch named `assembly/<run-id>`, opens a PR using the run report, then switches your local checkout back to the branch you started from.
|
|
340
|
+
|
|
341
|
+
Before creating a PR, Assembly requires:
|
|
342
|
+
|
|
343
|
+
- the run status is `complete`
|
|
344
|
+
- verification passed
|
|
345
|
+
- review completed successfully
|
|
346
|
+
- the run has owned changed files
|
|
347
|
+
- the working tree has no unrelated dirty files
|
|
348
|
+
|
|
349
|
+
Only files recorded by the completed run are staged. Assembly does not use `git add .` for PR creation.
|
|
350
|
+
|
|
351
|
+
Turn a GitHub issue or PR comment into a follow-up run:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
assembly github comment-to-follow-up <run-id> <comment-id>
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Authenticate GitHub CLI first:
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
gh auth login
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Webhook Flow
|
|
364
|
+
|
|
365
|
+
Run the webhook server:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
assembly webhook --port 3000
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Expose it with a tunnel such as:
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
ngrok http 3000
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Configure a GitHub webhook:
|
|
378
|
+
|
|
379
|
+
- Payload URL: `https://<your-tunnel>/github/webhook`
|
|
380
|
+
- Content type: `application/json`
|
|
381
|
+
- Secret: same value as `GITHUB_WEBHOOK_SECRET`
|
|
382
|
+
- Events:
|
|
383
|
+
- issues
|
|
384
|
+
- issue comments
|
|
385
|
+
- pull request review comments
|
|
386
|
+
- pull request reviews
|
|
387
|
+
|
|
388
|
+
Configure a Slack app:
|
|
389
|
+
|
|
390
|
+
- Enable Event Subscriptions
|
|
391
|
+
- Request URL: `https://<your-tunnel>/slack/events`
|
|
392
|
+
- Subscribe to bot events:
|
|
393
|
+
- `app_mention`
|
|
394
|
+
- `message.im`
|
|
395
|
+
- Install the app to your workspace
|
|
396
|
+
- Copy the signing secret to `SLACK_SIGNING_SECRET`
|
|
397
|
+
- Copy the bot token to `SLACK_BOT_TOKEN`
|
|
398
|
+
|
|
399
|
+
Set local environment:
|
|
400
|
+
|
|
401
|
+
```text
|
|
402
|
+
GITHUB_WEBHOOK_SECRET=your_webhook_secret
|
|
403
|
+
SLACK_SIGNING_SECRET=your_slack_signing_secret
|
|
404
|
+
SLACK_BOT_TOKEN=xoxb-your-slack-bot-token
|
|
405
|
+
ASSEMBLY_AGENT_PROVIDER=openai
|
|
406
|
+
ASSEMBLY_APPROVAL_MODE=auto
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Before exposing the webhook, run:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
assembly doctor
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
GitHub webhook events must include `@assembly` in the issue, comment, or review body. Slack requests can come from an app mention or direct message.
|
|
416
|
+
|
|
417
|
+
Supported events:
|
|
418
|
+
|
|
419
|
+
- `issues.opened` / `issues.edited` on normal issues: creates a new run and opens a new PR
|
|
420
|
+
- `issue_comment.created` on normal issues: creates a new run and opens a new PR
|
|
421
|
+
- `issue_comment.created` on PRs: creates a follow-up run, updates the existing PR branch, refreshes the PR body with the latest run report, and posts a completion comment
|
|
422
|
+
- `pull_request_review_comment.created`: creates a follow-up run with inline file/line context, updates the existing PR branch, refreshes the PR body, and posts a completion comment
|
|
423
|
+
- `pull_request_review.submitted`: creates a follow-up run from the review body, updates the existing PR branch, refreshes the PR body, and posts a completion comment
|
|
424
|
+
|
|
425
|
+
Webhook handling is asynchronous:
|
|
426
|
+
|
|
427
|
+
1. verify GitHub signature
|
|
428
|
+
2. ignore unsupported events or comments without `@assembly`
|
|
429
|
+
3. ignore duplicate GitHub delivery IDs that already have a job
|
|
430
|
+
4. enqueue a job under `.assembly/jobs/`
|
|
431
|
+
5. respond to GitHub quickly
|
|
432
|
+
6. process the job in the background
|
|
433
|
+
7. create a temporary git worktree so webhook jobs do not touch your dirty local checkout
|
|
434
|
+
8. create a run or follow-up run
|
|
435
|
+
9. copy the run record back into `.assembly/runs/`
|
|
436
|
+
10. push owned changes to a new PR branch or existing PR branch
|
|
437
|
+
11. refresh the PR body with the latest run report for PR follow-ups
|
|
438
|
+
12. comment back on the issue or PR
|
|
439
|
+
|
|
440
|
+
If a GitHub job fails, Assembly comments back on the issue or PR with the job id, run id when one was created, retryability, the failure reason, and a suggested next action.
|
|
441
|
+
|
|
442
|
+
Slack handling verifies Slack signatures, answers URL verification challenges, deduplicates Slack event ids, queues `slack.request` jobs, and immediately replies in the originating thread with an "On it" job acknowledgement. The first request in a Slack thread creates an Assembly run, opens a GitHub PR, stores the Slack thread to PR mapping under `.assembly/slack-threads/`, and replies again with the PR link. Later requests in that same Slack thread become follow-up runs against the same PR branch, refresh the PR body, and reply in the thread. If the run fails or blocks, Assembly stops before PR creation or update and replies with the actual run failure. If a Slack job fails, Assembly replies in the thread with the job id, run id when one was created, retryability, the failure reason, and a suggested next action.
|
|
443
|
+
|
|
444
|
+
For manual retry/debugging:
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
assembly job list
|
|
448
|
+
assembly job inspect <job-id> --pretty
|
|
449
|
+
assembly job process <job-id>
|
|
450
|
+
assembly job retry <job-id>
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## License
|
|
454
|
+
|
|
455
|
+
See [LICENSE](LICENSE).
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arungeorgesaji/assembly",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A coordination layer for AI software engineering teams.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"assembly": "src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/*.js",
|
|
11
|
+
".env.example",
|
|
12
|
+
"ngrok-example.yml"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test",
|
|
16
|
+
"plan": "node src/cli.js plan",
|
|
17
|
+
"run": "node src/cli.js run"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT"
|
|
23
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function attachAgentProfiles(tasks) {
|
|
4
|
+
const profileByKey = new Map();
|
|
5
|
+
const profiledTasks = tasks.map((task) => {
|
|
6
|
+
const key = profileKey(task);
|
|
7
|
+
let profile = profileByKey.get(key);
|
|
8
|
+
if (!profile) {
|
|
9
|
+
profile = createAgentProfile(task, key);
|
|
10
|
+
profileByKey.set(key, profile);
|
|
11
|
+
}
|
|
12
|
+
profile.taskIds.push(task.id);
|
|
13
|
+
return {
|
|
14
|
+
...task,
|
|
15
|
+
agentProfileId: profile.id,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
tasks: profiledTasks,
|
|
21
|
+
agentProfiles: [...profileByKey.values()],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateAgentProfiles(plan) {
|
|
26
|
+
const errors = [];
|
|
27
|
+
const profiles = plan.agentProfiles ?? [];
|
|
28
|
+
const profileIds = new Set();
|
|
29
|
+
const taskIds = new Set(plan.tasks.map((task) => task.id));
|
|
30
|
+
|
|
31
|
+
for (const profile of profiles) {
|
|
32
|
+
if (!profile.id) {
|
|
33
|
+
errors.push("agent profile id cannot be empty");
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (profileIds.has(profile.id)) {
|
|
37
|
+
errors.push(`agent profile ${profile.id} is duplicated`);
|
|
38
|
+
}
|
|
39
|
+
profileIds.add(profile.id);
|
|
40
|
+
|
|
41
|
+
if (!profile.dispatchOwner) {
|
|
42
|
+
errors.push(`agent profile ${profile.id} must include a dispatchOwner`);
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(profile.taskIds) || profile.taskIds.length === 0) {
|
|
45
|
+
errors.push(`agent profile ${profile.id} must include taskIds`);
|
|
46
|
+
} else {
|
|
47
|
+
for (const taskId of profile.taskIds) {
|
|
48
|
+
if (!taskIds.has(taskId)) {
|
|
49
|
+
errors.push(`agent profile ${profile.id} references unknown task ${taskId}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const ownedPath of profile.ownedPaths ?? []) {
|
|
54
|
+
if (!isSafeRelativePath(ownedPath)) {
|
|
55
|
+
errors.push(`agent profile ${profile.id} ownedPaths contains unsafe path ${ownedPath}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const deniedPath of profile.deniedPaths ?? []) {
|
|
59
|
+
if (!isSafeRelativePath(deniedPath)) {
|
|
60
|
+
errors.push(`agent profile ${profile.id} deniedPaths contains unsafe path ${deniedPath}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const task of plan.tasks) {
|
|
66
|
+
if (!task.agentProfileId) {
|
|
67
|
+
errors.push(`task ${task.id} must reference an agentProfileId`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const profile = profiles.find((candidate) => candidate.id === task.agentProfileId);
|
|
71
|
+
if (!profile) {
|
|
72
|
+
errors.push(`task ${task.id} references unknown agent profile ${task.agentProfileId}`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (profile.dispatchOwner !== task.owner) {
|
|
76
|
+
errors.push(`task ${task.id} owner does not match agent profile ${profile.id}`);
|
|
77
|
+
}
|
|
78
|
+
if (!profile.taskIds.includes(task.id)) {
|
|
79
|
+
errors.push(`agent profile ${profile.id} does not list task ${task.id}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return errors;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createAgentProfile(task, key) {
|
|
87
|
+
const ownedPaths = unique([...(task.scope?.paths ?? []), ...(task.scope?.allowlist ?? [])]).sort();
|
|
88
|
+
const deniedPaths = unique(task.scope?.denylist ?? []).sort();
|
|
89
|
+
const focus = ownedPaths.length > 0 ? ownedPaths.join(", ") : task.title;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
id: `agent-${slugify(task.owner)}-${hashKey(key)}`,
|
|
93
|
+
label: `${task.owner}: ${focus}`,
|
|
94
|
+
dispatchOwner: task.owner,
|
|
95
|
+
focus,
|
|
96
|
+
ownedPaths,
|
|
97
|
+
deniedPaths,
|
|
98
|
+
changePolicy: task.changePolicy,
|
|
99
|
+
tags: inferTags(ownedPaths),
|
|
100
|
+
taskIds: [],
|
|
101
|
+
instructions: [
|
|
102
|
+
`Work only inside: ${ownedPaths.join(", ") || "the task scope"}.`,
|
|
103
|
+
`Do not modify: ${deniedPaths.join(", ") || "paths outside the task scope"}.`,
|
|
104
|
+
`Use change policy: ${task.changePolicy}.`,
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function profileKey(task) {
|
|
110
|
+
return JSON.stringify({
|
|
111
|
+
owner: task.owner,
|
|
112
|
+
changePolicy: task.changePolicy,
|
|
113
|
+
paths: unique([...(task.scope?.paths ?? []), ...(task.scope?.allowlist ?? [])]).sort(),
|
|
114
|
+
denylist: unique(task.scope?.denylist ?? []).sort(),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function inferTags(ownedPaths) {
|
|
119
|
+
const tags = new Set();
|
|
120
|
+
for (const ownedPath of ownedPaths) {
|
|
121
|
+
const normalized = ownedPath.replaceAll("\\", "/");
|
|
122
|
+
const [area] = normalized.split("/");
|
|
123
|
+
if (area) {
|
|
124
|
+
tags.add(`area:${area.replace(/\/$/, "")}`);
|
|
125
|
+
}
|
|
126
|
+
const extension = path.posix.extname(normalized).slice(1);
|
|
127
|
+
if (extension) {
|
|
128
|
+
tags.add(`ext:${extension}`);
|
|
129
|
+
}
|
|
130
|
+
for (const token of normalized.toLowerCase().split(/[^a-z0-9]+/).filter((part) => part.length >= 3)) {
|
|
131
|
+
tags.add(`term:${token}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return [...tags].sort();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isSafeRelativePath(value) {
|
|
138
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const normalized = value.replaceAll("\\", "/");
|
|
142
|
+
if (path.posix.isAbsolute(normalized)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const collapsed = path.posix.normalize(normalized);
|
|
146
|
+
return collapsed !== "." && collapsed !== ".." && !collapsed.startsWith("../");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function slugify(value) {
|
|
150
|
+
return String(value ?? "")
|
|
151
|
+
.toLowerCase()
|
|
152
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
153
|
+
.replace(/^-|-$/g, "")
|
|
154
|
+
.slice(0, 48) || "agent";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function hashKey(value) {
|
|
158
|
+
let hash = 0;
|
|
159
|
+
for (const char of value) {
|
|
160
|
+
hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
|
|
161
|
+
}
|
|
162
|
+
return hash.toString(36);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function unique(values) {
|
|
166
|
+
return [...new Set(values.filter(Boolean))];
|
|
167
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getAgentProvider } from "./config.js";
|
|
2
|
+
import { createOpenAIAgentRunner } from "./openai-agent-runner.js";
|
|
3
|
+
import { createOpenAIReviewRunner } from "./review-agent-runner.js";
|
|
4
|
+
|
|
5
|
+
export async function runStubAgent(task) {
|
|
6
|
+
return {
|
|
7
|
+
taskId: task.id,
|
|
8
|
+
status: "complete",
|
|
9
|
+
summary: `${task.owner} completed placeholder work for "${task.title}".`,
|
|
10
|
+
changedFiles: [],
|
|
11
|
+
artifacts: ["result.json"],
|
|
12
|
+
risks: [
|
|
13
|
+
"Stub agent did not modify repository files. Replace with provider-backed execution before real delivery.",
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createAgentRunner() {
|
|
19
|
+
const provider = getAgentProvider();
|
|
20
|
+
if (provider === "stub") {
|
|
21
|
+
return runStubAgent;
|
|
22
|
+
}
|
|
23
|
+
if (provider === "openai") {
|
|
24
|
+
const openAIAgentRunner = createOpenAIAgentRunner();
|
|
25
|
+
const openAIReviewRunner = createOpenAIReviewRunner();
|
|
26
|
+
return async (task, context) => {
|
|
27
|
+
if (task.owner === "implementation-agent") {
|
|
28
|
+
return openAIAgentRunner(task, context);
|
|
29
|
+
}
|
|
30
|
+
if (task.owner === "review-agent") {
|
|
31
|
+
return openAIReviewRunner(task, context);
|
|
32
|
+
}
|
|
33
|
+
return runStubAgent(task);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`unknown ASSEMBLY_AGENT_PROVIDER: ${provider}`);
|
|
37
|
+
}
|