@agent-relay/factory 0.1.1 → 0.1.2
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 +146 -205
- package/dist/cli/fleet.d.ts +5 -1
- package/dist/cli/fleet.d.ts.map +1 -1
- package/dist/cli/fleet.js +86 -15
- package/dist/cli/fleet.js.map +1 -1
- package/dist/config/schema.d.ts +4 -4
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +13 -2
- package/dist/config/schema.js.map +1 -1
- package/dist/constants/linear.d.ts +2 -0
- package/dist/constants/linear.d.ts.map +1 -1
- package/dist/constants/linear.js +6 -0
- package/dist/constants/linear.js.map +1 -1
- package/dist/dispatch/templates.d.ts +18 -2
- package/dist/dispatch/templates.d.ts.map +1 -1
- package/dist/dispatch/templates.js +18 -10
- package/dist/dispatch/templates.js.map +1 -1
- package/dist/fleet/create-fleet.d.ts +3 -2
- package/dist/fleet/create-fleet.d.ts.map +1 -1
- package/dist/fleet/create-fleet.js +3 -2
- package/dist/fleet/create-fleet.js.map +1 -1
- package/dist/fleet/ensure-relay-broker.d.ts +24 -0
- package/dist/fleet/ensure-relay-broker.d.ts.map +1 -0
- package/dist/fleet/ensure-relay-broker.js +56 -0
- package/dist/fleet/ensure-relay-broker.js.map +1 -0
- package/dist/fleet/internal-fleet-client.d.ts +8 -3
- package/dist/fleet/internal-fleet-client.d.ts.map +1 -1
- package/dist/fleet/internal-fleet-client.js +32 -5
- package/dist/fleet/internal-fleet-client.js.map +1 -1
- package/dist/fleet/relay-fleet-client.d.ts +90 -13
- package/dist/fleet/relay-fleet-client.d.ts.map +1 -1
- package/dist/fleet/relay-fleet-client.js +496 -20
- package/dist/fleet/relay-fleet-client.js.map +1 -1
- package/dist/github/index.d.ts +4 -4
- package/dist/github/index.js +2 -2
- package/dist/github/probe-closer.d.ts +1 -1
- package/dist/github/probe-closer.js +1 -1
- package/dist/index.d.ts +33 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -15
- package/dist/index.js.map +1 -1
- package/dist/linear/state-resolver.d.ts +3 -2
- package/dist/linear/state-resolver.d.ts.map +1 -1
- package/dist/linear/state-resolver.js +74 -10
- package/dist/linear/state-resolver.js.map +1 -1
- package/dist/mount/local-mount-preflight.d.ts +1 -0
- package/dist/mount/local-mount-preflight.d.ts.map +1 -1
- package/dist/mount/local-mount-preflight.js +64 -12
- package/dist/mount/local-mount-preflight.js.map +1 -1
- package/dist/mount/relayfile-binary.d.ts +2 -0
- package/dist/mount/relayfile-binary.d.ts.map +1 -1
- package/dist/mount/relayfile-binary.js +39 -5
- package/dist/mount/relayfile-binary.js.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.d.ts +2 -2
- package/dist/mount/relayfile-cloud-mount-client.js +1 -1
- package/dist/node/factory-node.d.ts +59 -0
- package/dist/node/factory-node.d.ts.map +1 -0
- package/dist/node/factory-node.js +351 -0
- package/dist/node/factory-node.js.map +1 -0
- package/dist/node/factory.node.d.ts +3 -0
- package/dist/node/factory.node.d.ts.map +1 -0
- package/dist/node/factory.node.js +5 -0
- package/dist/node/factory.node.js.map +1 -0
- package/dist/orchestrator/batch-tracker.d.ts +2 -2
- package/dist/orchestrator/factory.d.ts +5 -2
- package/dist/orchestrator/factory.d.ts.map +1 -1
- package/dist/orchestrator/factory.js +413 -59
- package/dist/orchestrator/factory.js.map +1 -1
- package/dist/orchestrator/index.d.ts +4 -4
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/index.js +3 -3
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/process-identity.d.ts +1 -1
- package/dist/orchestrator/reaper.d.ts +3 -3
- package/dist/orchestrator/reaper.js +3 -3
- package/dist/ports/fleet.d.ts +8 -2
- package/dist/ports/fleet.d.ts.map +1 -1
- package/dist/ports/index.d.ts +4 -4
- package/dist/ports/state.d.ts +3 -3
- package/dist/ports/writeback.d.ts +1 -1
- package/dist/safety/factory-scope.d.ts +2 -2
- package/dist/state/in-memory-state-store.d.ts +1 -1
- package/dist/state/in-memory-state-store.js +1 -1
- package/dist/subscriptions/event-client.d.ts +1 -1
- package/dist/subscriptions/event-client.js +2 -2
- package/dist/subscriptions/globs.js +1 -1
- package/dist/subscriptions/index.d.ts +7 -7
- package/dist/subscriptions/index.js +4 -4
- package/dist/subscriptions/linear-filter.d.ts +1 -1
- package/dist/subscriptions/linear-filter.js +1 -1
- package/dist/subscriptions/specs.d.ts +1 -1
- package/dist/subscriptions/specs.js +2 -2
- package/dist/testing/fakes.d.ts +1 -1
- package/dist/testing/fakes.js +1 -1
- package/dist/testing/fakes.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/triage/heuristic.d.ts +8 -4
- package/dist/triage/heuristic.d.ts.map +1 -1
- package/dist/triage/heuristic.js +70 -6
- package/dist/triage/heuristic.js.map +1 -1
- package/dist/triage/index.d.ts +6 -6
- package/dist/triage/index.d.ts.map +1 -1
- package/dist/triage/index.js +4 -4
- package/dist/triage/index.js.map +1 -1
- package/dist/triage/llm.d.ts +1 -1
- package/dist/triage/llm.d.ts.map +1 -1
- package/dist/triage/llm.js +5 -3
- package/dist/triage/llm.js.map +1 -1
- package/dist/triage/schema.d.ts +133 -29
- package/dist/triage/schema.d.ts.map +1 -1
- package/dist/triage/schema.js +6 -3
- package/dist/triage/schema.js.map +1 -1
- package/dist/triage/tiered.d.ts +1 -1
- package/dist/triage/tiered.d.ts.map +1 -1
- package/dist/triage/tiered.js +9 -2
- package/dist/triage/tiered.js.map +1 -1
- package/dist/types.d.ts +10 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/writeback/github.d.ts +2 -2
- package/dist/writeback/github.js +1 -1
- package/dist/writeback/index.d.ts +5 -5
- package/dist/writeback/index.js +3 -3
- package/dist/writeback/linear.d.ts +3 -3
- package/dist/writeback/linear.d.ts.map +1 -1
- package/dist/writeback/linear.js +57 -14
- package/dist/writeback/linear.js.map +1 -1
- package/dist/writeback/slack.d.ts +1 -1
- package/dist/writeback/slack.js +2 -2
- package/package.json +10 -2
- package/scripts/com.agentrelay.factory-canary.plist.example +53 -0
- package/scripts/factory-canary.sh +89 -0
package/README.md
CHANGED
|
@@ -1,248 +1,189 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Agent Workforce Factory
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
dispatches agents to implement fixes, opens PRs, drives them to completion through
|
|
5
|
-
a merge gate, and closes the issues — all under a hard safety scope.
|
|
3
|
+
**Turn issues into reviewed pull requests, automatically.**
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Point the factory at your ticketing system (Linear, GitHub, Shortcut, Asana etc) and it does the loop a human
|
|
6
|
+
otherwise babysits: it discovers the issues that are ready, decides how to tackle
|
|
7
|
+
each one, spawns coding agents to implement and review the change, opens a PR,
|
|
8
|
+
drives it through a merge gate, and closes the issue — all inside a safety scope
|
|
9
|
+
you define, so it only ever acts on work you've explicitly opted in.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
## Why use it
|
|
12
|
+
|
|
13
|
+
- **Clear the small-but-real backlog.** The well-scoped fixes and chores that pile
|
|
14
|
+
up get done without a person shepherding each one.
|
|
15
|
+
- **It only touches what you allow.** A safety gate (title prefix + team) means
|
|
16
|
+
the factory dispatches *exactly* the issues you mark for it and ignores
|
|
17
|
+
everything else — opt-in by construction.
|
|
18
|
+
- **Real PRs, not blind merges.** Every change goes through an implement → review
|
|
19
|
+
→ merge-gate flow and lands as a normal PR. It defaults to *never* auto-merging
|
|
20
|
+
until you turn that on.
|
|
21
|
+
- **You stay in the loop.** It posts threaded status to Slack and can ask a human
|
|
22
|
+
for clarification mid-task when an issue is ambiguous.
|
|
23
|
+
- **Drop in by label.** Hand it new work just by labeling a Linear or GitHub
|
|
24
|
+
issue — no new tooling in your day-to-day.
|
|
25
|
+
|
|
26
|
+
A good fit when you have a steady stream of scoped issues and want them turned
|
|
27
|
+
into PRs without standing up your own agent orchestration.
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
13
30
|
|
|
14
|
-
node bin/factory.mjs factory <action> --config <cfg>
|
|
15
31
|
```
|
|
32
|
+
discover ready issues → triage (how to do it) → dispatch agents (implement + review)
|
|
33
|
+
→ open PR → merge gate → close issue
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Each step is gated by your config and the safety scope. Issues outside the scope
|
|
37
|
+
are pulled but never dispatched.
|
|
16
38
|
|
|
17
|
-
|
|
39
|
+
## Install
|
|
18
40
|
|
|
19
41
|
```bash
|
|
20
|
-
|
|
42
|
+
npm install @agent-relay/factory
|
|
21
43
|
```
|
|
22
44
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
The factory talks to a relay broker through the **`agent-relay`** sidecar; install
|
|
46
|
+
and sign in to that separately (it's a peer of this package). Once installed, the
|
|
47
|
+
CLI is available as `factory`:
|
|
26
48
|
|
|
27
|
-
|
|
49
|
+
```bash
|
|
50
|
+
factory factory run-once --config ./factory.config.json --dry-run
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> Yes, `factory factory` — the `factory` CLI groups its commands under a `factory`
|
|
54
|
+
> subcommand. From a source checkout instead of an npm install, run
|
|
55
|
+
> `npm ci && npm run build` first, then `node bin/factory.mjs factory <action> …`.
|
|
28
56
|
|
|
29
57
|
## Quick start
|
|
30
58
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# Add --dry-run to plan without writing or spawning anything.
|
|
34
|
-
factory factory run-once --config ./factory.config.json --dry-run
|
|
59
|
+
1. **Write a minimal config** (`factory.config.json`). Only `workspaceId` and a
|
|
60
|
+
repo route are required:
|
|
35
61
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"workspaceId": "your-workspace-id",
|
|
65
|
+
"repos": {
|
|
66
|
+
"byLabel": { "pear": "AgentWorkforce/pear" },
|
|
67
|
+
"clonePaths": { "AgentWorkforce/pear": "/path/to/your/pear/checkout" },
|
|
68
|
+
"default": "AgentWorkforce/pear"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`workspaceId` is your relay workspace; `repos.byLabel` maps a Linear label to a
|
|
74
|
+
repo; `clonePaths` tells the agent where that repo lives locally so it has
|
|
75
|
+
somewhere to make changes.
|
|
76
|
+
|
|
77
|
+
2. **Plan a cycle without touching anything** — `--dry-run` discovers and triages
|
|
78
|
+
but writes nothing and spawns no agents:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
factory factory run-once --config ./factory.config.json --dry-run
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
3. **Let it work for real:**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# One discovery→dispatch cycle, then exit.
|
|
88
|
+
factory factory run-once --config ./factory.config.json
|
|
89
|
+
|
|
90
|
+
# Or run continuously as a daemon (the production form).
|
|
91
|
+
factory factory start --mode live --config ./factory.config.json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> **Pulled some issues but dispatched none?** That's the safety gate doing its
|
|
95
|
+
> job — the issues are real but outside your scope. See
|
|
96
|
+
> [Tell it what to work on](#tell-it-what-to-work-on).
|
|
39
97
|
|
|
40
98
|
## Commands
|
|
41
99
|
|
|
42
100
|
| Command | What it does |
|
|
43
101
|
|---|---|
|
|
44
|
-
| `factory run-once` | One
|
|
45
|
-
| `factory loop` |
|
|
46
|
-
| `factory start --mode live` | Long-lived daemon
|
|
47
|
-
| `factory status` | Print
|
|
48
|
-
| `factory
|
|
49
|
-
| `factory
|
|
50
|
-
| `factory
|
|
51
|
-
| `factory triage <KEY\|path>` | Triage a single issue and print the decision. |
|
|
52
|
-
| `factory dispatch <KEY\|path>` | Triage + dispatch a single issue. Honors `--dry-run`. |
|
|
53
|
-
| `factory close-probe <PR#> --repo <owner/repo> --issue <KEY>` | Manually close a synthetic E2E probe PR. |
|
|
54
|
-
|
|
55
|
-
Global options (accepted anywhere in the args): `--config <path>`,
|
|
56
|
-
`--dry-run`, `--backend <internal\|relay>`.
|
|
102
|
+
| `factory run-once` | One discover→triage→dispatch cycle, then exit. Honors `--dry-run`. |
|
|
103
|
+
| `factory loop` | A bounded multi-iteration loop, then exit. |
|
|
104
|
+
| `factory start --mode live` | Long-lived daemon — the production entrypoint. Runs until you stop it. |
|
|
105
|
+
| `factory status` | Print current factory status as JSON. |
|
|
106
|
+
| `factory triage <KEY\|path>` | Triage one issue and print the decision. |
|
|
107
|
+
| `factory dispatch <KEY\|path>` | Triage + dispatch one issue. Honors `--dry-run`. |
|
|
108
|
+
| `factory canary <KEY\|path>` | Assert a known "Ready for Agent" issue is dispatch-ready by the real dry-run triage path. Prints `{ok,issue,status,reason}`; exits non-zero (with the skip reason) if it isn't. |
|
|
57
109
|
|
|
58
|
-
|
|
110
|
+
Global options work anywhere in the args: `--config <path>`, `--dry-run`,
|
|
111
|
+
`--backend <internal|relay>`. The internal backend reuses a relay broker that's
|
|
112
|
+
already running for your workspace, and starts one if none is.
|
|
59
113
|
|
|
60
|
-
|
|
114
|
+
(There are a few more operational commands — `loop-status`, `kill-loop`,
|
|
115
|
+
`reap-orphans`, `close-probe` — for running the daemon in production.)
|
|
61
116
|
|
|
62
|
-
|
|
63
|
-
loop and refreshes a heartbeat file.
|
|
64
|
-
2. **External reaper** — a *scheduled* `factory factory reap-orphans --config <live>`
|
|
65
|
-
that acts as a crash backstop, cleaning up orphaned agent pairs if the daemon dies.
|
|
117
|
+
### Scheduled sync-fidelity canary
|
|
66
118
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
119
|
+
`factory canary` is the regression detector for upstream sync drift: if a synced
|
|
120
|
+
issue stops carrying enough state to be dispatchable (e.g. the Linear sync
|
|
121
|
+
regresses to records without `state.id`), a known-good issue flips from
|
|
122
|
+
dispatch-ready to skipped. Run it on a schedule against a standing "Ready for
|
|
123
|
+
Agent" canary issue and alert on failure.
|
|
70
124
|
|
|
71
|
-
|
|
125
|
+
`scripts/factory-canary.sh` wraps the command for cron/launchd: it runs from your
|
|
126
|
+
deployment dir (reusing the running relay broker), bounds a hung run, and posts a
|
|
127
|
+
Slack alert via `FACTORY_CANARY_SLACK_WEBHOOK` on failure. See
|
|
128
|
+
`scripts/com.agentrelay.factory-canary.plist.example` for an every-6h launchd
|
|
129
|
+
template.
|
|
72
130
|
|
|
73
|
-
|
|
74
|
-
while the cloud GitHub→mount PR-sync is degraded; a `gh` auth drop halts completion.
|
|
75
|
-
- Run the real binary path (`factory` / `node bin/factory.mjs`), not a shim.
|
|
131
|
+
## Tell it what to work on
|
|
76
132
|
|
|
77
|
-
|
|
133
|
+
Two ways to hand the factory an issue — both are just labeling/titling, nothing
|
|
134
|
+
to install:
|
|
78
135
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
136
|
+
| Source | What you do | Result |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| **Linear** | Title it `[factory] <task>`, set the team + a repo label, move it to **Ready for Agent** | dispatched directly |
|
|
139
|
+
| **GitHub** | Add the **`factory`** label to the issue | mirrored into a `[factory]` Linear issue, then dispatched |
|
|
82
140
|
|
|
83
|
-
The
|
|
84
|
-
|
|
85
|
-
|
|
141
|
+
The **safety gate** is what keeps this opt-in: by default the factory only
|
|
142
|
+
dispatches an issue whose **title starts with your configured prefix** *and* whose
|
|
143
|
+
**team matches** your configured team. Everything else is ignored. Loosen it
|
|
144
|
+
deliberately — it's the main guardrail.
|
|
86
145
|
|
|
87
|
-
|
|
146
|
+
> Tip: `[factory-e2e]` is reserved for the factory's own self-test soak (its PRs
|
|
147
|
+
> auto-close). For real work you want to keep, use the `[factory]` prefix.
|
|
88
148
|
|
|
89
|
-
|
|
90
|
-
has a default:
|
|
149
|
+
## Run it as a fleet node (optional)
|
|
91
150
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"repos": {
|
|
96
|
-
"byLabel": { "pear": "AgentWorkforce/pear" },
|
|
97
|
-
"default": "AgentWorkforce/pear"
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
```
|
|
151
|
+
The package also ships a fleet **node definition** so a machine can advertise
|
|
152
|
+
`spawn:claude` / `spawn:codex` / `workflow:run` to the cloud and run agents in the
|
|
153
|
+
checkouts it owns:
|
|
101
154
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
A realistic live config, showing the fields you'll actually want to set (comments
|
|
105
|
-
are illustrative — strip them; JSON has no comments):
|
|
106
|
-
|
|
107
|
-
```jsonc
|
|
108
|
-
{
|
|
109
|
-
// relayfile cloud mount workspace id — the workspace whose /linear and /slack
|
|
110
|
-
// trees the factory reads. Same id the Pear app uses for this workspace.
|
|
111
|
-
"workspaceId": "ws_abc123",
|
|
112
|
-
|
|
113
|
-
// Which Linear issues to pull. Empty arrays = no filter on that dimension.
|
|
114
|
-
// Values are Linear team keys / project names / label names / assignee ids.
|
|
115
|
-
"subscription": {
|
|
116
|
-
"teams": ["AR"],
|
|
117
|
-
"labels": ["pear"],
|
|
118
|
-
"projects": [],
|
|
119
|
-
"assignees": []
|
|
120
|
-
},
|
|
121
|
-
|
|
122
|
-
// Issue → repo routing. Precedence (first match wins):
|
|
123
|
-
// byLabel → byProject → keywordRules (regex on title/desc) → default
|
|
124
|
-
// No match and no default → the issue is escalated, never dispatched.
|
|
125
|
-
"repos": {
|
|
126
|
-
"byLabel": { "pear": "AgentWorkforce/pear", "cloud": "AgentWorkforce/cloud" },
|
|
127
|
-
"byProject": { "Pear": "AgentWorkforce/pear" },
|
|
128
|
-
"keywordRules": [{ "pattern": "relayfile|mount", "repo": "AgentWorkforce/relayfile" }],
|
|
129
|
-
// Where each repo is checked out locally for the agent to work in.
|
|
130
|
-
"clonePaths": { "AgentWorkforce/pear": "/Users/you/Projects/pear" },
|
|
131
|
-
"default": "AgentWorkforce/pear"
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
// SAFETY GATE — an issue is only dispatched if BOTH hold (see below):
|
|
135
|
-
"safety": {
|
|
136
|
-
"requireTitlePrefix": "[factory-e2e]",
|
|
137
|
-
"requireTeamKey": "AR"
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
"batchSize": 5, // max issues dispatched per cycle
|
|
141
|
-
"dispatch": { "maxAttempts": 2, "errorCooldownMs": 60000 },
|
|
142
|
-
"mergePolicy": "never", // see mergePolicy note below
|
|
143
|
-
"models": { // optional per-role model overrides
|
|
144
|
-
"implementer": "claude-opus-4-8",
|
|
145
|
-
"reviewer": "claude-opus-4-8",
|
|
146
|
-
"triage": "claude-haiku-4-5-20251001"
|
|
147
|
-
},
|
|
148
|
-
"slack": { "channel": "C0B902XR6PN" }, // optional threaded status updates
|
|
149
|
-
"loop": {
|
|
150
|
-
"heartbeatPath": "/tmp/factory-run/factory-loop-heartbeat.json",
|
|
151
|
-
"registryPath": "/tmp/factory-run/factory-loop-registry.json",
|
|
152
|
-
"heartbeatStaleMs": 60000
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
+
```bash
|
|
156
|
+
agent-relay fleet serve @agent-relay/factory/node
|
|
155
157
|
```
|
|
156
158
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
- **`workspaceId`** *(required)* — the relayfile cloud mount workspace id for this
|
|
160
|
-
workspace (the same one the Pear app mounts `/linear` and `/slack` under).
|
|
161
|
-
- **`repos.byLabel`** *(required)* — map a Linear label → `owner/repo`. The other
|
|
162
|
-
routing maps are optional; **precedence is `byLabel` → `byProject` →
|
|
163
|
-
`keywordRules` → `default`**, else the issue is escalated (never dispatched).
|
|
164
|
-
- **`repos.clonePaths`** — `owner/repo` → local working-tree path. Without an entry
|
|
165
|
-
the agent has nowhere to apply changes for that repo, so set it for every repo
|
|
166
|
-
you actually dispatch to.
|
|
167
|
-
- **`stateIds`** — the Linear workflow-state UUIDs for `readyForAgent`,
|
|
168
|
-
`agentImplementing`, `done`, `inPlanning`, and optionally `humanReview`.
|
|
169
|
-
Defaults cover the **AR team's** core states (see
|
|
170
|
-
[`src/constants/linear.ts`](src/constants/linear.ts)). `humanReview` is
|
|
171
|
-
intentionally omitted from the schema default so legacy configs fall back to
|
|
172
|
-
direct-to-Done behavior unless they opt into a review state. If you run against
|
|
173
|
-
a different Linear team, override the state UUIDs with that team's values (read
|
|
174
|
-
them from the Linear API / the issue JSON's `state.id`).
|
|
175
|
-
- **`safety`** — the scope gate (below). Defaults `[factory-e2e]` + team `AR`;
|
|
176
|
-
GitHub issue mirrors created by the factory use `[factory]` and are accepted
|
|
177
|
-
by the same gate.
|
|
178
|
-
|
|
179
|
-
### The safety gate (what actually gets dispatched)
|
|
180
|
-
|
|
181
|
-
`isInFactoryScope` ([`src/safety/factory-scope.ts`](src/safety/factory-scope.ts))
|
|
182
|
-
dispatches an issue only when **both** are true:
|
|
183
|
-
|
|
184
|
-
1. The issue **title starts with `safety.requireTitlePrefix`** — exactly
|
|
185
|
-
`[factory-e2e]`, or `[factory-e2e] <rest>` by default. Factory-created
|
|
186
|
-
GitHub mirrors titled `[factory] <GitHub title>` are also accepted. Anything
|
|
187
|
-
else is out of scope.
|
|
188
|
-
2. The issue's **team key equals `safety.requireTeamKey`** (`AR`).
|
|
189
|
-
|
|
190
|
-
This is why `factory run-once` may pull issues but dispatch none — they're real
|
|
191
|
-
issues that fall outside the gate. Loosen the gate deliberately; it's the primary
|
|
192
|
-
guardrail against the factory acting on issues it shouldn't.
|
|
193
|
-
|
|
194
|
-
> **⚠️ `[factory-e2e]` is the synthetic self-test prefix — its PRs auto-close.**
|
|
195
|
-
> The default `requireTitlePrefix` is `[factory-e2e]`, which is *also* the marker
|
|
196
|
-
> the factory uses to identify synthetic E2E soak issues. A `[factory-e2e]`-titled
|
|
197
|
-
> issue's PR is **auto-closed, never merged** (`#isSyntheticProbeIssue`) — great
|
|
198
|
-
> for self-test soaks, wrong for real work. **For real issues you want to keep,
|
|
199
|
-
> set `requireTitlePrefix` to `[factory]`** and title issues `[factory] <task>`;
|
|
200
|
-
> reserve `[factory-e2e]` for the soak. The synthetic check matches `[factory-e2e]`
|
|
201
|
-
> exactly, so `[factory]` issues are not auto-closed. (Note: `[factory-e2e]` does
|
|
202
|
-
> **not** satisfy a `[factory]` gate — the prefixes differ at the `]`/`-`, so don't
|
|
203
|
-
> mix them.)
|
|
204
|
-
|
|
205
|
-
### Ingesting GitHub issues (label `factory`)
|
|
206
|
-
|
|
207
|
-
The factory also picks up **GitHub issues labeled `factory`** (case-insensitive),
|
|
208
|
-
read from the mounted `/github/repos/**` tree (pushed in by the GitHub→relayfile
|
|
209
|
-
sync — no API polling). For each, it creates one Linear mirror titled
|
|
210
|
-
`[factory] <GitHub title>` in **Ready for Agent**, routes it via `repos.byLabel`,
|
|
211
|
-
then dispatches it like any other Linear issue. Mirrors are deduped (one per
|
|
212
|
-
GitHub issue) and set to `done` when the GitHub issue closes.
|
|
213
|
-
|
|
214
|
-
So to hand the factory a GitHub issue you **just add the `factory` label** — you
|
|
215
|
-
do **not** put `[factory]` in the GitHub title (the factory adds that to the
|
|
216
|
-
Linear mirror). This is the cross-repo path: label, say, a `relay` repo issue
|
|
217
|
-
`factory` and it flows in, routed to `AgentWorkforce/relay`.
|
|
159
|
+
It reads its node config from `./factory.node.json` (or `$FACTORY_NODE_CONFIG`).
|
|
160
|
+
Prefer to build the definition yourself?
|
|
218
161
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
| Linear (direct) | title `[factory] <task>`, team AR, label = repo, Ready for Agent | dispatched directly |
|
|
222
|
-
| GitHub (any configured repo) | add the **`factory` label** | mirrored to a `[factory]` Linear issue, then dispatched |
|
|
162
|
+
```ts
|
|
163
|
+
import { createFactoryNodeDefinition, readFactoryNodeConfigSync } from '@agent-relay/factory'
|
|
223
164
|
|
|
224
|
-
|
|
165
|
+
export default createFactoryNodeDefinition({ config: readFactoryNodeConfigSync() })
|
|
166
|
+
```
|
|
225
167
|
|
|
226
|
-
|
|
227
|
-
autonomous merge on green + approved review. **Stays `never` until the flip is
|
|
228
|
-
thrown** (issue #321 §6).
|
|
229
|
-
- `loop.heartbeatPath` / `loop.registryPath` / `loop.heartbeatStaleMs` (`60000`) —
|
|
230
|
-
daemon/reaper coupling (the reaper must point at the same paths via the same
|
|
231
|
-
`--config`).
|
|
232
|
-
- `batchSize` (`5`), `dispatch.maxAttempts` (`2`), `dispatch.errorCooldownMs`
|
|
233
|
-
(`60000`), `models.{implementer,reviewer,triage}`, `slack.channel`,
|
|
234
|
-
`subscription.*`, `liveSubscription.*`, `stateIds.*`.
|
|
168
|
+
## Configuration
|
|
235
169
|
|
|
236
|
-
|
|
170
|
+
Pass a JSON file via `--config`. Beyond the two required fields above, useful
|
|
171
|
+
knobs include issue **routing** (`repos.byLabel` / `byProject` / `keywordRules` /
|
|
172
|
+
`default`), the **safety gate** (`safety.requireTitlePrefix`, `safety.requireTeamKey`),
|
|
173
|
+
`mergePolicy` (defaults to `never`), per-role **model** overrides, and an optional
|
|
174
|
+
**Slack** channel for status threads.
|
|
237
175
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
176
|
+
The full schema — every field and default — is validated by Zod at load time, so
|
|
177
|
+
an invalid config fails fast with a field-level error. See
|
|
178
|
+
[`src/config/schema.ts`](src/config/schema.ts) for the authoritative reference,
|
|
179
|
+
and [`test/fixtures/factory.config.json`](test/fixtures/factory.config.json) for a
|
|
180
|
+
worked example (including offline fixture mode).
|
|
241
181
|
|
|
242
182
|
## Notes
|
|
243
183
|
|
|
244
|
-
- The daemon is headless by design
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
184
|
+
- The daemon is headless by design; tools like Pear can consume this package and
|
|
185
|
+
wrap it, but the published CLI is `factory`.
|
|
186
|
+
- The published `dist/` is plain ESM, runnable directly by Node (`node bin/factory.mjs`)
|
|
187
|
+
and importable by ESM consumers.
|
|
188
|
+
- For production operation (the live-daemon + reaper backstop model, heartbeats,
|
|
189
|
+
and `gh`-auth preconditions), see the operations notes alongside the config schema.
|
package/dist/cli/fleet.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { createFactory, createFleet, type Capability, type Factory, type FactoryConfig, type FleetBackend, type FleetClient, type GhRunner, type FactoryStateResolution, type MountClient, type ProbeCloser, type RelayfileCloudMountClientConfig, type ResolvedFactoryWorkspace } from '../index';
|
|
1
|
+
import { createFactory, createFleet, ensureRelayBroker, type Capability, type Factory, type FactoryConfig, type FleetBackend, type FleetClient, type GhRunner, type FactoryStateResolution, type MountClient, type ProbeCloser, type RelayfileCloudMountClientConfig, type ResolvedFactoryWorkspace } from '../index.js';
|
|
2
2
|
interface FleetCliDeps {
|
|
3
3
|
fleet?: FleetClient;
|
|
4
4
|
mount?: MountClient;
|
|
5
5
|
createFactory?: typeof createFactory;
|
|
6
6
|
createFleet?: typeof createFleet;
|
|
7
|
+
ensureRelayBroker?: typeof ensureRelayBroker;
|
|
7
8
|
cloudMountFromConfig?: (config?: RelayfileCloudMountClientConfig) => Promise<MountClient>;
|
|
8
9
|
resolveWorkspace?: () => Promise<ResolvedFactoryWorkspace>;
|
|
9
10
|
resolveStates?: (mount: MountClient, config: FactoryConfig) => Promise<FactoryStateResolution>;
|
|
@@ -49,6 +50,9 @@ type ParsedCommand = {
|
|
|
49
50
|
kind: 'factory';
|
|
50
51
|
action: 'start';
|
|
51
52
|
mode?: 'live';
|
|
53
|
+
} | {
|
|
54
|
+
kind: 'factory-canary';
|
|
55
|
+
issue: string;
|
|
52
56
|
} | {
|
|
53
57
|
kind: 'factory-triage';
|
|
54
58
|
issue: string;
|
package/dist/cli/fleet.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleet.d.ts","sourceRoot":"","sources":["../../src/cli/fleet.ts"],"names":[],"mappings":"AAKA,OAAO,EAKL,aAAa,EACb,WAAW,
|
|
1
|
+
{"version":3,"file":"fleet.d.ts","sourceRoot":"","sources":["../../src/cli/fleet.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/fleet.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
|
-
import { ensureLocalMount } from '../mount/local-mount-preflight';
|
|
5
|
-
import { FactoryConfigSchema, RelayfileCloudMountClient, checkFactoryLoopLiveness, closeProbePr, createFactory, createFleet, defaultGhRunner, isInFactoryScope,
|
|
6
|
-
import { FakeFleetClient, FakeMountClient } from '../testing';
|
|
4
|
+
import { ensureLocalMount } from '../mount/local-mount-preflight.js';
|
|
5
|
+
import { FactoryConfigSchema, RelayfileCloudMountClient, checkFactoryLoopLiveness, closeProbePr, createFactory, createFleet, ensureRelayBroker, defaultGhRunner, isInFactoryScope, readLinearIssueWithCanonicalFallback, reapFactoryOrphansOnce, readFactoryLoopHeartbeat, resolveFactoryStates, resolveFactoryWorkspace, } from '../index.js';
|
|
6
|
+
import { FakeFleetClient, FakeMountClient } from '../testing/index.js';
|
|
7
7
|
export async function runFleetCli(argv, deps = {}) {
|
|
8
8
|
const out = deps.stdout ?? process.stdout;
|
|
9
9
|
const err = deps.stderr ?? process.stderr;
|
|
@@ -53,6 +53,7 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
53
53
|
writeJson(out, { released: command.name });
|
|
54
54
|
return 0;
|
|
55
55
|
case 'factory':
|
|
56
|
+
case 'factory-canary':
|
|
56
57
|
case 'factory-triage':
|
|
57
58
|
case 'factory-dispatch': {
|
|
58
59
|
if (!loaded)
|
|
@@ -112,7 +113,7 @@ export function parseFleetCommand(args) {
|
|
|
112
113
|
if (verb === 'spawn') {
|
|
113
114
|
const [capability, ...flags] = rest;
|
|
114
115
|
if (!isCapability(capability)) {
|
|
115
|
-
throw new Error('fleet spawn requires capability spawn:codex or
|
|
116
|
+
throw new Error('fleet spawn requires capability spawn:codex, spawn:claude, or workflow:run');
|
|
116
117
|
}
|
|
117
118
|
const parsed = parseFlags(flags);
|
|
118
119
|
return {
|
|
@@ -246,6 +247,12 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
246
247
|
}
|
|
247
248
|
return 0;
|
|
248
249
|
}
|
|
250
|
+
if (command.kind === 'factory-canary') {
|
|
251
|
+
const report = await factory.runOnce({ dryRun: true });
|
|
252
|
+
const result = evaluateFactoryCanary(report, command.issue);
|
|
253
|
+
writeJson(out, result);
|
|
254
|
+
return result.ok ? 0 : 1;
|
|
255
|
+
}
|
|
249
256
|
const issue = await readIssueArg(mount, command.issue);
|
|
250
257
|
const decision = await factory.triageIssue(issue);
|
|
251
258
|
if (command.kind === 'factory-triage') {
|
|
@@ -263,6 +270,11 @@ function parseFactoryCommand(args) {
|
|
|
263
270
|
if (action === 'run-once' || action === 'loop' || action === 'status' || action === 'loop-status' || action === 'kill-loop' || action === 'reap-orphans') {
|
|
264
271
|
return { kind: 'factory', action };
|
|
265
272
|
}
|
|
273
|
+
if (action === 'canary') {
|
|
274
|
+
if (!issueOrPr)
|
|
275
|
+
throw new Error('fleet factory canary requires an issue key or path');
|
|
276
|
+
return { kind: 'factory-canary', issue: issueOrPr };
|
|
277
|
+
}
|
|
266
278
|
if (action === 'triage') {
|
|
267
279
|
if (!issueOrPr)
|
|
268
280
|
throw new Error('fleet factory triage requires an issue key or path');
|
|
@@ -284,6 +296,41 @@ function parseFactoryCommand(args) {
|
|
|
284
296
|
}
|
|
285
297
|
throw new Error(`Unknown fleet factory action: ${action ?? ''}`);
|
|
286
298
|
}
|
|
299
|
+
// Canary: assert a known "Ready for Agent" issue is classified dispatch-ready
|
|
300
|
+
// by the REAL triage path against the live mount. This is the regression
|
|
301
|
+
// detector for sync-fidelity drift (sparse records / stub primaries) — if it
|
|
302
|
+
// flips to skipped, the adapter/sync contract broke. Exits non-zero with the
|
|
303
|
+
// offending skip reason so CI/cron can alert.
|
|
304
|
+
function evaluateFactoryCanary(report, issueArg) {
|
|
305
|
+
const wantKey = issueArg.startsWith('/')
|
|
306
|
+
? (issueArg.split('/').at(-1) ?? '').replace(/\.json$/u, '').split('__')[0]
|
|
307
|
+
: issueArg;
|
|
308
|
+
const matches = (ref) => ref.key === wantKey;
|
|
309
|
+
if (report.dispatched.some((d) => matches(d.issue))) {
|
|
310
|
+
return { ok: true, issue: wantKey, status: 'dispatched' };
|
|
311
|
+
}
|
|
312
|
+
if (report.triaged.some((t) => matches(t.issue))) {
|
|
313
|
+
return { ok: true, issue: wantKey, status: 'triaged' };
|
|
314
|
+
}
|
|
315
|
+
const skipped = report.skipped.find((s) => matches(s.issue));
|
|
316
|
+
if (skipped) {
|
|
317
|
+
return { ok: false, issue: wantKey, status: 'skipped', reason: skipped.reason };
|
|
318
|
+
}
|
|
319
|
+
if (!report.pulled.some(matches)) {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
issue: wantKey,
|
|
323
|
+
status: 'not-found',
|
|
324
|
+
reason: 'issue was not enumerated from the mount (sync may be missing it or it is in-flight)',
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
issue: wantKey,
|
|
330
|
+
status: 'unknown',
|
|
331
|
+
reason: 'issue pulled but neither dispatched, triaged, nor skipped',
|
|
332
|
+
};
|
|
333
|
+
}
|
|
287
334
|
function parseFactoryStartFlags(args) {
|
|
288
335
|
let mode;
|
|
289
336
|
const flags = args.filter((arg) => Boolean(arg));
|
|
@@ -315,11 +362,36 @@ async function buildFleet(globals, loaded, deps) {
|
|
|
315
362
|
return deps.fleet;
|
|
316
363
|
if (globals.backend === 'internal' && hasExplicitFixtureFiles(loaded))
|
|
317
364
|
return new FakeFleetClient();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
365
|
+
const cwd = process.cwd();
|
|
366
|
+
const connectionPath = resolveBrokerConnectionPath(cwd);
|
|
367
|
+
// An injected createFleet owns fleet construction entirely (tests), so skip the
|
|
368
|
+
// real broker bootstrap.
|
|
369
|
+
if (deps.createFleet) {
|
|
370
|
+
return deps.createFleet({ backend: globals.backend, cwd, connectionPath });
|
|
371
|
+
}
|
|
372
|
+
// The internal backend talks to a relay broker. Reuse the one already running
|
|
373
|
+
// for this workspace, or start one if none is up, so `factory <run-once|loop>`
|
|
374
|
+
// works without a separately-started broker. The relay backend manages its own
|
|
375
|
+
// connection and needs no local broker.
|
|
376
|
+
if (globals.backend === 'internal') {
|
|
377
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
378
|
+
const logger = {
|
|
379
|
+
info: (message, ...args) => stderr.write(`${message}${formatLogArgs(args)}\n`),
|
|
380
|
+
};
|
|
381
|
+
const { client, started } = await (deps.ensureRelayBroker ?? ensureRelayBroker)({ cwd, connectionPath, logger });
|
|
382
|
+
return createFleet({ backend: 'internal', cwd, connectionPath }, { harnessClient: client, ownsBroker: started });
|
|
383
|
+
}
|
|
384
|
+
return createFleet({ backend: globals.backend, cwd, connectionPath });
|
|
385
|
+
}
|
|
386
|
+
function formatLogArgs(args) {
|
|
387
|
+
if (args.length === 0)
|
|
388
|
+
return '';
|
|
389
|
+
try {
|
|
390
|
+
return ` ${args.map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg))).join(' ')}`;
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return '';
|
|
394
|
+
}
|
|
323
395
|
}
|
|
324
396
|
export function resolveBrokerConnectionPath(startCwd = process.cwd()) {
|
|
325
397
|
let current = resolve(startCwd);
|
|
@@ -365,7 +437,7 @@ async function isAllowedFactoryDraft(path, content, opts, mount, config) {
|
|
|
365
437
|
if (nestedComment) {
|
|
366
438
|
const issuePath = `/linear/issues/${nestedComment[1]}.json`;
|
|
367
439
|
try {
|
|
368
|
-
const issue =
|
|
440
|
+
const issue = await readLinearIssueWithCanonicalFallback(mount, issuePath);
|
|
369
441
|
return isInFactoryScope(issue, config.safety);
|
|
370
442
|
}
|
|
371
443
|
catch {
|
|
@@ -376,7 +448,7 @@ async function isAllowedFactoryDraft(path, content, opts, mount, config) {
|
|
|
376
448
|
if (isInFactoryScope(scopeIssueFromDraftContent(content), config.safety))
|
|
377
449
|
return true;
|
|
378
450
|
try {
|
|
379
|
-
const issue =
|
|
451
|
+
const issue = await readLinearIssueWithCanonicalFallback(mount, path);
|
|
380
452
|
return isInFactoryScope(issue, config.safety);
|
|
381
453
|
}
|
|
382
454
|
catch {
|
|
@@ -397,8 +469,7 @@ const scopeIssueFromDraftContent = (content) => ({
|
|
|
397
469
|
});
|
|
398
470
|
async function readIssueArg(mount, issueArg) {
|
|
399
471
|
const path = issueArg.startsWith('/') ? issueArg : await findIssuePath(mount, issueArg);
|
|
400
|
-
|
|
401
|
-
return parseLinearIssue(path, content);
|
|
472
|
+
return readLinearIssueWithCanonicalFallback(mount, path);
|
|
402
473
|
}
|
|
403
474
|
async function findIssuePath(mount, key) {
|
|
404
475
|
const matches = (await mount.listTree('/linear/issues/'))
|
|
@@ -443,10 +514,10 @@ function flushWritable(stream) {
|
|
|
443
514
|
});
|
|
444
515
|
}
|
|
445
516
|
function isCapability(value) {
|
|
446
|
-
return value === 'spawn:codex' || value === 'spawn:claude';
|
|
517
|
+
return value === 'spawn:codex' || value === 'spawn:claude' || value === 'workflow:run';
|
|
447
518
|
}
|
|
448
519
|
function defaultAgentName(capability, now) {
|
|
449
|
-
return `fleet-${capability.replace('spawn:', '')}-${now}`;
|
|
520
|
+
return `fleet-${capability.replace('spawn:', '').replace(':', '-')}-${now}`;
|
|
450
521
|
}
|
|
451
522
|
export function installFactoryStopSignalHandlers(factory, opts = {}) {
|
|
452
523
|
const exit = opts.exit ?? ((code) => process.exit(code));
|