@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/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).
@@ -0,0 +1,9 @@
1
+ version: "3"
2
+ agent:
3
+ authtoken: AUTHTOKEN
4
+
5
+ tunnels:
6
+ assembly:
7
+ proto: http
8
+ addr: 3000
9
+ domain: URL
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
+ }