@pleaseai/work 0.0.0 → 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/LICENSE +112 -0
- package/README.md +563 -0
- package/dist/index.js +210 -20
- package/package.json +12 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Functional Source License, Version 1.1, MIT Future License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Passion Factory, Inc
|
|
4
|
+
|
|
5
|
+
## Abbreviation
|
|
6
|
+
|
|
7
|
+
FSL-1.1-MIT
|
|
8
|
+
|
|
9
|
+
## Notice
|
|
10
|
+
|
|
11
|
+
Copyright 2026 Passion Factory, Inc
|
|
12
|
+
|
|
13
|
+
## Terms and Conditions
|
|
14
|
+
|
|
15
|
+
### Licensor ("We")
|
|
16
|
+
|
|
17
|
+
The party offering the Software under these Terms and Conditions.
|
|
18
|
+
|
|
19
|
+
### The Software
|
|
20
|
+
|
|
21
|
+
The "Software" is each version of the software that we make available under
|
|
22
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
23
|
+
Conditions with the Software.
|
|
24
|
+
|
|
25
|
+
### License Grant
|
|
26
|
+
|
|
27
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
28
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
29
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
30
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
31
|
+
|
|
32
|
+
### Permitted Purpose
|
|
33
|
+
|
|
34
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
35
|
+
means making the Software available to others in a commercial product or
|
|
36
|
+
service that:
|
|
37
|
+
|
|
38
|
+
1. substitutes for the Software;
|
|
39
|
+
|
|
40
|
+
2. substitutes for any other product or service we offer using the Software
|
|
41
|
+
that exists as of the date we make the Software available; or
|
|
42
|
+
|
|
43
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
44
|
+
|
|
45
|
+
Permitted Purposes specifically include using the Software:
|
|
46
|
+
|
|
47
|
+
1. for your internal use and access;
|
|
48
|
+
|
|
49
|
+
2. for non-commercial education;
|
|
50
|
+
|
|
51
|
+
3. for non-commercial research; and
|
|
52
|
+
|
|
53
|
+
4. in connection with professional services that you provide to a licensee
|
|
54
|
+
using the Software in accordance with these Terms and Conditions.
|
|
55
|
+
|
|
56
|
+
### Patents
|
|
57
|
+
|
|
58
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
59
|
+
patents, the license grant above includes a license under our patents. If you
|
|
60
|
+
make a claim against any party that the Software infringes or contributes to
|
|
61
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
62
|
+
immediately.
|
|
63
|
+
|
|
64
|
+
### Redistribution
|
|
65
|
+
|
|
66
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
67
|
+
the Software.
|
|
68
|
+
|
|
69
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
70
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
71
|
+
remove any copyright notices provided in or with the Software.
|
|
72
|
+
|
|
73
|
+
### Disclaimer
|
|
74
|
+
|
|
75
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
76
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
77
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
78
|
+
|
|
79
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
80
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
81
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
82
|
+
|
|
83
|
+
### Trademarks
|
|
84
|
+
|
|
85
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
86
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
87
|
+
trademarks, trade names, service marks or product names.
|
|
88
|
+
|
|
89
|
+
## Grant of Future License
|
|
90
|
+
|
|
91
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
92
|
+
the MIT License that is effective on 2028-03-13. On or after that date, you
|
|
93
|
+
may use the Software under the MIT License, in which case the following will
|
|
94
|
+
apply:
|
|
95
|
+
|
|
96
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
97
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
98
|
+
in the Software without restriction, including without limitation the rights
|
|
99
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
100
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
101
|
+
furnished to do so, subject to the following conditions:
|
|
102
|
+
|
|
103
|
+
The above copyright notice and this permission notice shall be included in all
|
|
104
|
+
copies or substantial portions of the Software.
|
|
105
|
+
|
|
106
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
107
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
108
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
109
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
110
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
111
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
112
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
# Work Please
|
|
2
|
+
|
|
3
|
+
[](https://sonarcloud.io/summary/new_code?id=pleaseai_work-please) [](https://sonarcloud.io/summary/new_code?id=pleaseai_work-please) [](https://sonarcloud.io/summary/new_code?id=pleaseai_work-please) [](https://sonarcloud.io/summary/new_code?id=pleaseai_work-please)
|
|
4
|
+
[](https://codecov.io/gh/pleaseai/work-please)
|
|
5
|
+
|
|
6
|
+
Work Please turns issue tracker tasks into isolated, autonomous implementation runs — managing work
|
|
7
|
+
instead of supervising coding agents.
|
|
8
|
+
|
|
9
|
+
> **Warning**: Work Please is an engineering preview for use in trusted environments.
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Overview](#overview)
|
|
14
|
+
- [Key Differences from Symphony](#key-differences-from-symphony)
|
|
15
|
+
- [Features](#features)
|
|
16
|
+
- [Architecture](#architecture)
|
|
17
|
+
- [Quick Start](#quick-start)
|
|
18
|
+
- [Prerequisites](#prerequisites)
|
|
19
|
+
- [Install](#install)
|
|
20
|
+
- [Configure](#configure)
|
|
21
|
+
- [Run](#run)
|
|
22
|
+
- [WORKFLOW.md Configuration](#workflowmd-configuration)
|
|
23
|
+
- [Full Front Matter Schema](#full-front-matter-schema)
|
|
24
|
+
- [Template Variables](#template-variables)
|
|
25
|
+
- [CLI Usage](#cli-usage)
|
|
26
|
+
- [GitHub App Authentication](#github-app-authentication)
|
|
27
|
+
- [Setting up GitHub App credentials](#setting-up-github-app-credentials)
|
|
28
|
+
- [Validation](#validation)
|
|
29
|
+
- [Trust and Safety](#trust-and-safety)
|
|
30
|
+
- [Permission Modes](#permission-modes)
|
|
31
|
+
- [Workspace Isolation](#workspace-isolation)
|
|
32
|
+
- [Recommendations](#recommendations)
|
|
33
|
+
- [License](#license)
|
|
34
|
+
|
|
35
|
+
## Overview
|
|
36
|
+
|
|
37
|
+
Work Please is a long-running TypeScript service that:
|
|
38
|
+
|
|
39
|
+
1. Polls an issue tracker (Asana or GitHub Projects v2) for tasks in configured active states.
|
|
40
|
+
2. Creates an isolated workspace directory for each eligible issue.
|
|
41
|
+
3. Launches a Claude Code agent session inside that workspace with a rendered prompt.
|
|
42
|
+
4. Monitors the session, handles retries, and reconciles issue state on each poll cycle.
|
|
43
|
+
|
|
44
|
+
It is a TypeScript implementation of the [Symphony specification](vendor/symphony/SPEC.md),
|
|
45
|
+
adapted for Asana / GitHub Projects v2 and Claude Code instead of Linear and Codex.
|
|
46
|
+
|
|
47
|
+
For full technical details, see [SPEC.md](SPEC.md).
|
|
48
|
+
|
|
49
|
+
## Key Differences from Symphony
|
|
50
|
+
|
|
51
|
+
| | Symphony (reference) | Work Please |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| Issue Tracker | Linear | Asana & GitHub Projects v2 |
|
|
54
|
+
| Coding Agent | Codex (app-server mode) | Claude Code CLI |
|
|
55
|
+
| Language | Elixir/OTP | TypeScript + Bun |
|
|
56
|
+
| Tracker Auth | `LINEAR_API_KEY` | `ASANA_ACCESS_TOKEN`, `GITHUB_TOKEN`, or GitHub App credentials |
|
|
57
|
+
| Project Config | `project_slug` | `project_gid` (Asana) or `owner` + `project_number` (GitHub Projects v2) |
|
|
58
|
+
| Issue States | Linear workflow states | Asana sections / GitHub Projects v2 Status field |
|
|
59
|
+
| Agent Protocol | JSON-RPC over stdio | `@anthropic-ai/claude-agent-sdk` |
|
|
60
|
+
| Permission Model | Codex approval/sandbox policies | Claude Code `--permission-mode` |
|
|
61
|
+
|
|
62
|
+
## Features
|
|
63
|
+
|
|
64
|
+
- **Multi-tracker support** — Dispatch work from Asana tasks or GitHub Projects v2 items on a
|
|
65
|
+
fixed cadence.
|
|
66
|
+
- **GitHub App authentication** — Authenticate the GitHub tracker with a GitHub App installation
|
|
67
|
+
token (`app_id` + `private_key` + `installation_id`) instead of a PAT, for fine-grained
|
|
68
|
+
permissions and higher API rate limits.
|
|
69
|
+
- **Assignee & label filters** — Filter eligible issues by assignee and/or label. Multiple values
|
|
70
|
+
within each filter use OR logic; assignee and label filters are ANDed when both are specified.
|
|
71
|
+
Applies at dispatch time only — already-running issues are unaffected. Configured per-tracker
|
|
72
|
+
in `WORKFLOW.md`.
|
|
73
|
+
- **Isolated workspaces** — Each issue gets a dedicated directory; workspaces persist across runs.
|
|
74
|
+
- **`WORKFLOW.md` config** — Version agent prompt and runtime settings alongside your code.
|
|
75
|
+
- **Bounded concurrency** — Global and per-state concurrent agent limits.
|
|
76
|
+
- **Retry with backoff** — Exponential backoff on failures; short continuation retry on clean exit.
|
|
77
|
+
- **Dynamic config reload** — Edit `WORKFLOW.md` and changes apply without restarting the service.
|
|
78
|
+
- **Workspace hooks** — Shell scripts run at `after_create`, `before_run`, `after_run`, and
|
|
79
|
+
`before_remove` lifecycle events.
|
|
80
|
+
- **Structured logging** — Operator-visible logs with stable `key=value` format.
|
|
81
|
+
- **Optional HTTP dashboard** — Enable with `--port` for runtime status and JSON API.
|
|
82
|
+
|
|
83
|
+
## Architecture
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
WORKFLOW.md
|
|
87
|
+
|
|
|
88
|
+
v
|
|
89
|
+
Config Layer ──> Orchestrator ──> Workspace Manager ──> Agent Runner (Claude Code)
|
|
90
|
+
| |
|
|
91
|
+
v v
|
|
92
|
+
Issue Tracker Client Isolated workspace/
|
|
93
|
+
(Asana REST API or per-issue directory
|
|
94
|
+
GitHub GraphQL API,
|
|
95
|
+
polling + reconciliation)
|
|
96
|
+
|
|
|
97
|
+
v
|
|
98
|
+
Status Surface (optional HTTP dashboard / structured logs)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Components:
|
|
102
|
+
|
|
103
|
+
- **Workflow Loader** — Parses `WORKFLOW.md` YAML front matter and prompt template body.
|
|
104
|
+
- **Config Layer** — Typed getters with env-var indirection and built-in defaults.
|
|
105
|
+
- **Issue Tracker Client** — Fetches candidate issues, reconciles running-issue states. Supports
|
|
106
|
+
Asana (REST API) and GitHub Projects v2 (GraphQL API) adapters.
|
|
107
|
+
- **Orchestrator** — Owns in-memory state; drives the poll/dispatch/retry loop.
|
|
108
|
+
- **Workspace Manager** — Creates, reuses, and cleans per-issue workspaces; runs hooks.
|
|
109
|
+
- **Agent Runner** — Launches Claude Code, streams events back to the orchestrator.
|
|
110
|
+
- **Status Surface** — Optional terminal view and HTTP API for operator visibility.
|
|
111
|
+
|
|
112
|
+
See [SPEC.md](SPEC.md) for the full specification.
|
|
113
|
+
|
|
114
|
+
## Quick Start
|
|
115
|
+
|
|
116
|
+
### Prerequisites
|
|
117
|
+
|
|
118
|
+
- **Bun** (see [bun.sh](https://bun.sh) for installation)
|
|
119
|
+
- **Claude Code CLI** (see the [official installation guide](https://docs.anthropic.com/en/docs/claude-code))
|
|
120
|
+
- **Asana access token** (`ASANA_ACCESS_TOKEN`) **or** **GitHub token** (`GITHUB_TOKEN`) with
|
|
121
|
+
access to the target project, **or** **GitHub App credentials** (`GITHUB_APP_ID`,
|
|
122
|
+
`GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_INSTALLATION_ID`) — see [GitHub App Authentication](#github-app-authentication)
|
|
123
|
+
|
|
124
|
+
### Install
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
git clone https://github.com/chatbot-pf/work-please.git
|
|
128
|
+
cd work-please
|
|
129
|
+
bun install
|
|
130
|
+
bun run build
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Configure
|
|
134
|
+
|
|
135
|
+
Create a `WORKFLOW.md` in your target repository. Two examples are shown below.
|
|
136
|
+
|
|
137
|
+
#### Asana
|
|
138
|
+
|
|
139
|
+
```markdown
|
|
140
|
+
---
|
|
141
|
+
tracker:
|
|
142
|
+
kind: asana
|
|
143
|
+
api_key: $ASANA_ACCESS_TOKEN
|
|
144
|
+
project_gid: "1234567890123456"
|
|
145
|
+
active_sections:
|
|
146
|
+
- In Progress
|
|
147
|
+
terminal_sections:
|
|
148
|
+
- Done
|
|
149
|
+
- Cancelled
|
|
150
|
+
|
|
151
|
+
polling:
|
|
152
|
+
interval_ms: 30000
|
|
153
|
+
|
|
154
|
+
workspace:
|
|
155
|
+
root: ~/work-please_workspaces
|
|
156
|
+
|
|
157
|
+
hooks:
|
|
158
|
+
after_create: |
|
|
159
|
+
git clone https://github.com/your-org/your-repo.git .
|
|
160
|
+
bun install
|
|
161
|
+
|
|
162
|
+
agent:
|
|
163
|
+
max_concurrent_agents: 3
|
|
164
|
+
max_turns: 20
|
|
165
|
+
|
|
166
|
+
claude:
|
|
167
|
+
permission_mode: acceptEdits
|
|
168
|
+
turn_timeout_ms: 3600000
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
You are working on an Asana task for the project.
|
|
172
|
+
|
|
173
|
+
Task: {{ issue.title }}
|
|
174
|
+
|
|
175
|
+
{{ issue.description }}
|
|
176
|
+
|
|
177
|
+
{% if issue.blocked_by.size > 0 %}
|
|
178
|
+
Blocked by:
|
|
179
|
+
{% for blocker in issue.blocked_by %}
|
|
180
|
+
- {{ blocker.identifier }} ({{ blocker.state }})
|
|
181
|
+
{% endfor %}
|
|
182
|
+
{% endif %}
|
|
183
|
+
|
|
184
|
+
{% if attempt %}
|
|
185
|
+
This is attempt #{{ attempt }}. Review any prior work in the workspace before continuing.
|
|
186
|
+
{% endif %}
|
|
187
|
+
|
|
188
|
+
Your task:
|
|
189
|
+
1. Understand the task requirements.
|
|
190
|
+
2. Implement the requested changes.
|
|
191
|
+
3. Write or update tests as needed.
|
|
192
|
+
4. Open a pull request and move this task to the review section.
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### GitHub Projects v2 (PAT)
|
|
196
|
+
|
|
197
|
+
```markdown
|
|
198
|
+
---
|
|
199
|
+
tracker:
|
|
200
|
+
kind: github_projects
|
|
201
|
+
api_key: $GITHUB_TOKEN
|
|
202
|
+
owner: your-org
|
|
203
|
+
project_number: 42
|
|
204
|
+
active_statuses:
|
|
205
|
+
- In Progress
|
|
206
|
+
terminal_statuses:
|
|
207
|
+
- Done
|
|
208
|
+
- Cancelled
|
|
209
|
+
|
|
210
|
+
polling:
|
|
211
|
+
interval_ms: 30000
|
|
212
|
+
|
|
213
|
+
workspace:
|
|
214
|
+
root: ~/work-please_workspaces
|
|
215
|
+
|
|
216
|
+
hooks:
|
|
217
|
+
after_create: |
|
|
218
|
+
git clone https://github.com/your-org/your-repo.git .
|
|
219
|
+
bun install
|
|
220
|
+
|
|
221
|
+
agent:
|
|
222
|
+
max_concurrent_agents: 3
|
|
223
|
+
max_turns: 20
|
|
224
|
+
|
|
225
|
+
claude:
|
|
226
|
+
permission_mode: acceptEdits
|
|
227
|
+
turn_timeout_ms: 3600000
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
You are working on a GitHub issue for the repository `your-org/your-repo`.
|
|
231
|
+
|
|
232
|
+
Issue {{ issue.identifier }}: {{ issue.title }}
|
|
233
|
+
|
|
234
|
+
{{ issue.description }}
|
|
235
|
+
|
|
236
|
+
{% if issue.blocked_by.size > 0 %}
|
|
237
|
+
Blocked by:
|
|
238
|
+
{% for blocker in issue.blocked_by %}
|
|
239
|
+
- {{ blocker.identifier }} ({{ blocker.state }})
|
|
240
|
+
{% endfor %}
|
|
241
|
+
{% endif %}
|
|
242
|
+
|
|
243
|
+
{% if attempt %}
|
|
244
|
+
This is attempt #{{ attempt }}. Review any prior work in the workspace before continuing.
|
|
245
|
+
{% endif %}
|
|
246
|
+
|
|
247
|
+
Your task:
|
|
248
|
+
1. Understand the issue requirements.
|
|
249
|
+
2. Implement the requested changes.
|
|
250
|
+
3. Write or update tests as needed.
|
|
251
|
+
4. Open a pull request and move this issue to the review status.
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### GitHub Projects v2 (GitHub App)
|
|
255
|
+
|
|
256
|
+
Use GitHub App credentials instead of a PAT for fine-grained permissions and higher API rate limits:
|
|
257
|
+
|
|
258
|
+
```markdown
|
|
259
|
+
---
|
|
260
|
+
tracker:
|
|
261
|
+
kind: github_projects
|
|
262
|
+
app_id: $GITHUB_APP_ID
|
|
263
|
+
private_key: $GITHUB_APP_PRIVATE_KEY
|
|
264
|
+
installation_id: $GITHUB_APP_INSTALLATION_ID
|
|
265
|
+
owner: your-org
|
|
266
|
+
project_number: 42
|
|
267
|
+
active_statuses:
|
|
268
|
+
- In Progress
|
|
269
|
+
terminal_statuses:
|
|
270
|
+
- Done
|
|
271
|
+
- Cancelled
|
|
272
|
+
|
|
273
|
+
polling:
|
|
274
|
+
interval_ms: 30000
|
|
275
|
+
|
|
276
|
+
workspace:
|
|
277
|
+
root: ~/work-please_workspaces
|
|
278
|
+
|
|
279
|
+
hooks:
|
|
280
|
+
after_create: |
|
|
281
|
+
git clone https://github.com/your-org/your-repo.git .
|
|
282
|
+
bun install
|
|
283
|
+
|
|
284
|
+
agent:
|
|
285
|
+
max_concurrent_agents: 3
|
|
286
|
+
max_turns: 20
|
|
287
|
+
|
|
288
|
+
claude:
|
|
289
|
+
permission_mode: acceptEdits
|
|
290
|
+
turn_timeout_ms: 3600000
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
You are working on a GitHub issue for the repository `your-org/your-repo`.
|
|
294
|
+
|
|
295
|
+
Issue {{ issue.identifier }}: {{ issue.title }}
|
|
296
|
+
|
|
297
|
+
{{ issue.description }}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Run
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
# Set your tracker token
|
|
304
|
+
export ASANA_ACCESS_TOKEN=your_token_here
|
|
305
|
+
# or (GitHub PAT)
|
|
306
|
+
export GITHUB_TOKEN=ghp_your_token_here
|
|
307
|
+
# or (GitHub App)
|
|
308
|
+
export GITHUB_APP_ID=12345
|
|
309
|
+
export GITHUB_APP_PRIVATE_KEY="$(cat path/to/private-key.pem)"
|
|
310
|
+
export GITHUB_APP_INSTALLATION_ID=67890
|
|
311
|
+
|
|
312
|
+
# Run Work Please against a WORKFLOW.md in the current directory
|
|
313
|
+
bunx work-please
|
|
314
|
+
|
|
315
|
+
# Or specify a WORKFLOW.md path
|
|
316
|
+
bunx work-please /path/to/WORKFLOW.md
|
|
317
|
+
|
|
318
|
+
# Enable the optional HTTP dashboard on port 3000
|
|
319
|
+
bunx work-please --port 3000
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## WORKFLOW.md Configuration
|
|
323
|
+
|
|
324
|
+
`WORKFLOW.md` is the single source of truth for Work Please's runtime behavior. It combines a YAML
|
|
325
|
+
front matter configuration block with a Markdown prompt template body.
|
|
326
|
+
|
|
327
|
+
### Full Front Matter Schema
|
|
328
|
+
|
|
329
|
+
```yaml
|
|
330
|
+
---
|
|
331
|
+
tracker:
|
|
332
|
+
kind: asana # Required: "asana" or "github_projects"
|
|
333
|
+
|
|
334
|
+
# --- Asana fields (when kind == "asana") ---
|
|
335
|
+
api_key: $ASANA_ACCESS_TOKEN # Required: token or $ENV_VAR
|
|
336
|
+
endpoint: https://app.asana.com/api/1.0 # Optional: override Asana API base URL
|
|
337
|
+
project_gid: "1234567890123456" # Required: Asana project GID
|
|
338
|
+
active_sections: # Optional: default ["To Do", "In Progress"]
|
|
339
|
+
- In Progress
|
|
340
|
+
terminal_sections: # Optional: default ["Done", "Cancelled"]
|
|
341
|
+
- Done
|
|
342
|
+
- Cancelled
|
|
343
|
+
|
|
344
|
+
# --- GitHub Projects v2 fields (when kind == "github_projects") ---
|
|
345
|
+
# api_key: $GITHUB_TOKEN # Required: token or $ENV_VAR
|
|
346
|
+
# endpoint: https://api.github.com # Optional: override GitHub API base URL
|
|
347
|
+
# owner: your-org # Required: GitHub organization or user login
|
|
348
|
+
# project_number: 42 # Required: GitHub Projects v2 project number
|
|
349
|
+
# project_id: PVT_kwDOxxxxx # Optional: project node ID (bypasses owner+project_number lookup)
|
|
350
|
+
# active_statuses: # Optional: default ["Todo", "In Progress"]
|
|
351
|
+
# - In Progress
|
|
352
|
+
# terminal_statuses: # Optional: default ["Done", "Cancelled"]
|
|
353
|
+
# - Done
|
|
354
|
+
# - Cancelled
|
|
355
|
+
# GitHub App authentication (alternative to api_key — all three required together):
|
|
356
|
+
# app_id: $GITHUB_APP_ID # Optional: GitHub App ID (integer or $ENV_VAR)
|
|
357
|
+
# private_key: $GITHUB_APP_PRIVATE_KEY # Optional: GitHub App private key PEM or $ENV_VAR
|
|
358
|
+
# installation_id: $GITHUB_APP_INSTALLATION_ID # Optional: installation ID (integer or $ENV_VAR)
|
|
359
|
+
|
|
360
|
+
# --- Shared filter fields (both trackers) ---
|
|
361
|
+
# filter:
|
|
362
|
+
# assignee: user1, user2 # Optional: CSV or YAML array; case-insensitive OR match
|
|
363
|
+
# # (unassigned issues are excluded when this filter is set)
|
|
364
|
+
# label: bug, feature # Optional: CSV or YAML array; case-insensitive OR match
|
|
365
|
+
# Both filters AND together when both are specified. Applies at dispatch time only.
|
|
366
|
+
|
|
367
|
+
polling:
|
|
368
|
+
interval_ms: 30000 # Optional: poll cadence in ms, default 30000
|
|
369
|
+
|
|
370
|
+
workspace:
|
|
371
|
+
root: ~/work-please_workspaces # Optional: default <tmpdir>/work-please_workspaces
|
|
372
|
+
|
|
373
|
+
hooks:
|
|
374
|
+
after_create: | # Optional: run once when workspace is first created
|
|
375
|
+
git clone https://github.com/your-org/your-repo.git .
|
|
376
|
+
before_run: | # Optional: run before each agent attempt
|
|
377
|
+
git pull --rebase
|
|
378
|
+
after_run: | # Optional: run after each agent attempt
|
|
379
|
+
echo "Run completed"
|
|
380
|
+
before_remove: | # Optional: run before workspace deletion
|
|
381
|
+
echo "Cleaning up"
|
|
382
|
+
timeout_ms: 60000 # Optional: hook timeout in ms, default 60000
|
|
383
|
+
|
|
384
|
+
agent:
|
|
385
|
+
max_concurrent_agents: 10 # Optional: global concurrency limit, default 10
|
|
386
|
+
max_retry_backoff_ms: 300000 # Optional: max retry delay in ms, default 300000
|
|
387
|
+
max_concurrent_agents_by_state: # Optional: per-state concurrency limits
|
|
388
|
+
in progress: 5
|
|
389
|
+
|
|
390
|
+
claude:
|
|
391
|
+
command: claude # Optional: Claude Code CLI command, default "claude"
|
|
392
|
+
permission_mode: acceptEdits # Optional: one of 'default', 'acceptEdits', 'bypassPermissions'. Defaults to 'bypassPermissions'.
|
|
393
|
+
allowed_tools: # Optional: restrict available tools
|
|
394
|
+
- Read
|
|
395
|
+
- Write
|
|
396
|
+
- Bash
|
|
397
|
+
turn_timeout_ms: 3600000 # Optional: per-turn timeout in ms, default 3600000
|
|
398
|
+
read_timeout_ms: 5000 # Optional: initial subprocess read timeout in ms, default 5000
|
|
399
|
+
stall_timeout_ms: 300000 # Optional: stall detection timeout, default 300000
|
|
400
|
+
|
|
401
|
+
server:
|
|
402
|
+
port: 3000 # Optional: enable HTTP dashboard on this port
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
Your prompt template goes here. Available variables:
|
|
406
|
+
|
|
407
|
+
- {{ issue.id }} — Tracker-internal issue ID
|
|
408
|
+
- {{ issue.identifier }} — Human-readable identifier (e.g. "#42" or task GID)
|
|
409
|
+
- {{ issue.title }} — Issue title
|
|
410
|
+
- {{ issue.description }} — Issue body/description
|
|
411
|
+
- {{ issue.state }} — Current tracker state name
|
|
412
|
+
- {{ issue.url }} — Issue URL
|
|
413
|
+
- {{ issue.assignee }} — Primary assignee login (GitHub) or email (Asana), or null if unassigned
|
|
414
|
+
- {{ issue.labels }} — Array of label strings (normalized to lowercase)
|
|
415
|
+
- {{ issue.blocked_by }} — Array of blocker refs (each has id, identifier, state)
|
|
416
|
+
- {{ issue.priority }} — Numeric priority or null
|
|
417
|
+
- {{ issue.created_at }} — ISO-8601 creation timestamp
|
|
418
|
+
- {{ issue.updated_at }} — ISO-8601 last-updated timestamp
|
|
419
|
+
- {{ attempt }} — Retry attempt number (null on first run)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Template Variables
|
|
423
|
+
|
|
424
|
+
The prompt template uses Liquid-compatible syntax. All `issue` fields are available:
|
|
425
|
+
|
|
426
|
+
```markdown
|
|
427
|
+
{{ issue.identifier }}: {{ issue.title }}
|
|
428
|
+
|
|
429
|
+
{{ issue.description }}
|
|
430
|
+
|
|
431
|
+
State: {{ issue.state }}
|
|
432
|
+
|
|
433
|
+
{% if issue.blocked_by.size > 0 %}
|
|
434
|
+
Blocked by:
|
|
435
|
+
{% for blocker in issue.blocked_by %}
|
|
436
|
+
- {{ blocker.identifier }} ({{ blocker.state }})
|
|
437
|
+
{% endfor %}
|
|
438
|
+
{% endif %}
|
|
439
|
+
|
|
440
|
+
{% if attempt %}
|
|
441
|
+
Retry attempt: {{ attempt }}
|
|
442
|
+
{% endif %}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## CLI Usage
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
# Basic usage (reads WORKFLOW.md from current directory)
|
|
449
|
+
work-please
|
|
450
|
+
|
|
451
|
+
# Specify WORKFLOW.md path (positional argument)
|
|
452
|
+
work-please ./WORKFLOW.md
|
|
453
|
+
|
|
454
|
+
# Enable HTTP dashboard
|
|
455
|
+
work-please --port 3000
|
|
456
|
+
|
|
457
|
+
# Initialize a new GitHub Projects v2 project and scaffold WORKFLOW.md
|
|
458
|
+
# (Requires GITHUB_TOKEN environment variable to be set)
|
|
459
|
+
work-please init --owner <org-or-user> --title "My Project"
|
|
460
|
+
|
|
461
|
+
# Alternatively, provide the token via a flag:
|
|
462
|
+
work-please init --owner <org-or-user> --title "My Project" --token <your-github-token>
|
|
463
|
+
|
|
464
|
+
# Show help
|
|
465
|
+
work-please --help
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## GitHub App Authentication
|
|
469
|
+
|
|
470
|
+
The `github_projects` tracker supports two authentication methods:
|
|
471
|
+
|
|
472
|
+
| Method | Config fields | When to use |
|
|
473
|
+
|--------|--------------|-------------|
|
|
474
|
+
| **PAT** | `api_key` | Personal access tokens — quick setup |
|
|
475
|
+
| **GitHub App** | `app_id`, `private_key`, `installation_id` | Organizations — fine-grained permissions, higher rate limits |
|
|
476
|
+
|
|
477
|
+
When both are present, `api_key` (PAT) takes precedence.
|
|
478
|
+
|
|
479
|
+
### Setting up GitHub App credentials
|
|
480
|
+
|
|
481
|
+
1. Create a GitHub App with the following permissions:
|
|
482
|
+
- **Repository permissions**:
|
|
483
|
+
- `Contents`: Read-only
|
|
484
|
+
- `Issues`: Read & write
|
|
485
|
+
- `Pull requests`: Read & write
|
|
486
|
+
- **Organization permissions**:
|
|
487
|
+
- `Projects`: Read & write
|
|
488
|
+
2. Install the app on your organization and note the **installation ID** (visible in the app's
|
|
489
|
+
installation settings URL).
|
|
490
|
+
3. Generate a **private key** (`.pem` file) from the app's settings page.
|
|
491
|
+
4. Set the environment variables:
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
export GITHUB_APP_ID=12345
|
|
495
|
+
export GITHUB_APP_PRIVATE_KEY="$(cat /path/to/private-key.pem)"
|
|
496
|
+
export GITHUB_APP_INSTALLATION_ID=67890
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
5. Reference them in `WORKFLOW.md`:
|
|
500
|
+
|
|
501
|
+
```yaml
|
|
502
|
+
tracker:
|
|
503
|
+
kind: github_projects
|
|
504
|
+
app_id: $GITHUB_APP_ID
|
|
505
|
+
private_key: $GITHUB_APP_PRIVATE_KEY
|
|
506
|
+
installation_id: $GITHUB_APP_INSTALLATION_ID
|
|
507
|
+
owner: your-org
|
|
508
|
+
project_number: 42
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
The values can also be inlined directly (not recommended for secrets):
|
|
512
|
+
|
|
513
|
+
```yaml
|
|
514
|
+
app_id: 12345
|
|
515
|
+
private_key: "-----BEGIN RSA PRIVATE KEY-----\n..."
|
|
516
|
+
installation_id: 67890
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Validation
|
|
520
|
+
|
|
521
|
+
Work Please validates GitHub App config at startup:
|
|
522
|
+
|
|
523
|
+
| Scenario | Result |
|
|
524
|
+
|----------|--------|
|
|
525
|
+
| `api_key` set | PAT auth — app fields ignored |
|
|
526
|
+
| All three app fields set (`app_id`, `private_key`, `installation_id`) | App auth |
|
|
527
|
+
| Only some app fields set | `incomplete_github_app_config` error |
|
|
528
|
+
| No auth configured | `missing_tracker_api_key` error |
|
|
529
|
+
|
|
530
|
+
## Trust and Safety
|
|
531
|
+
|
|
532
|
+
Work Please runs Claude Code autonomously. Understand the trust implications before deploying.
|
|
533
|
+
|
|
534
|
+
### Permission Modes
|
|
535
|
+
|
|
536
|
+
| Mode | Behavior | Recommended For |
|
|
537
|
+
|---|---|---|
|
|
538
|
+
| `default` | Interactive approval for sensitive operations | Development, unknown repositories |
|
|
539
|
+
| `acceptEdits` | Auto-approve file edits; prompt for shell commands | Trusted codebases |
|
|
540
|
+
| `bypassPermissions` | Auto-approve all operations | Sandboxed CI environments |
|
|
541
|
+
|
|
542
|
+
Start with `default` or `acceptEdits` unless you are running in a fully isolated environment.
|
|
543
|
+
|
|
544
|
+
### Workspace Isolation
|
|
545
|
+
|
|
546
|
+
- Each issue runs in a dedicated directory under `workspace.root`.
|
|
547
|
+
- Claude Code's working directory is validated against the workspace path before launch.
|
|
548
|
+
- Workspace paths are sanitized to prevent path traversal attacks.
|
|
549
|
+
|
|
550
|
+
### Recommendations
|
|
551
|
+
|
|
552
|
+
- Use `acceptEdits` permission mode as a baseline for most deployments.
|
|
553
|
+
- Use `bypassPermissions` only in network-isolated CI runners or Docker containers.
|
|
554
|
+
- Set `agent.max_concurrent_agents` conservatively when first testing.
|
|
555
|
+
- Monitor agent runs via the HTTP dashboard (`--port`) or structured logs.
|
|
556
|
+
- Keep API tokens scoped to the minimum required permissions.
|
|
557
|
+
|
|
558
|
+
## License
|
|
559
|
+
|
|
560
|
+
Apache License 2.0. See [LICENSE](vendor/symphony/LICENSE) for details.
|
|
561
|
+
|
|
562
|
+
Work Please is a TypeScript implementation based on the
|
|
563
|
+
[Symphony specification](vendor/symphony/SPEC.md) by OpenAI (Apache 2.0).
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
1
2
|
// @bun
|
|
2
3
|
var __create = Object.create;
|
|
3
4
|
var __getProtoOf = Object.getPrototypeOf;
|
|
@@ -31840,6 +31841,8 @@ function buildConfig(workflow) {
|
|
|
31840
31841
|
};
|
|
31841
31842
|
}
|
|
31842
31843
|
function buildTrackerConfig(kind, tracker) {
|
|
31844
|
+
const label_prefix = stringValue(tracker.label_prefix) ?? null;
|
|
31845
|
+
const filter = buildFilterConfig(sectionMap(tracker, "filter"));
|
|
31843
31846
|
if (kind === "asana") {
|
|
31844
31847
|
return {
|
|
31845
31848
|
kind,
|
|
@@ -31847,7 +31850,9 @@ function buildTrackerConfig(kind, tracker) {
|
|
|
31847
31850
|
api_key: resolveEnvValue(stringValue(tracker.api_key), process4.env.ASANA_ACCESS_TOKEN),
|
|
31848
31851
|
project_gid: stringValue(tracker.project_gid) ?? null,
|
|
31849
31852
|
active_sections: csvValue(tracker.active_sections) ?? csvValue(tracker.active_states) ?? DEFAULTS2.ASANA_ACTIVE_SECTIONS,
|
|
31850
|
-
terminal_sections: csvValue(tracker.terminal_sections) ?? csvValue(tracker.terminal_states) ?? DEFAULTS2.ASANA_TERMINAL_SECTIONS
|
|
31853
|
+
terminal_sections: csvValue(tracker.terminal_sections) ?? csvValue(tracker.terminal_states) ?? DEFAULTS2.ASANA_TERMINAL_SECTIONS,
|
|
31854
|
+
label_prefix,
|
|
31855
|
+
filter
|
|
31851
31856
|
};
|
|
31852
31857
|
}
|
|
31853
31858
|
if (kind === "github_projects") {
|
|
@@ -31862,13 +31867,23 @@ function buildTrackerConfig(kind, tracker) {
|
|
|
31862
31867
|
terminal_statuses: csvValue(tracker.terminal_statuses) ?? csvValue(tracker.terminal_states) ?? DEFAULTS2.GITHUB_TERMINAL_STATUSES,
|
|
31863
31868
|
app_id: resolveEnvValue(stringValue(tracker.app_id), process4.env.GITHUB_APP_ID),
|
|
31864
31869
|
private_key: resolveEnvValue(stringValue(tracker.private_key), process4.env.GITHUB_APP_PRIVATE_KEY),
|
|
31865
|
-
installation_id: resolveInstallationId(tracker.installation_id)
|
|
31870
|
+
installation_id: resolveInstallationId(tracker.installation_id),
|
|
31871
|
+
label_prefix,
|
|
31872
|
+
filter
|
|
31866
31873
|
};
|
|
31867
31874
|
}
|
|
31868
31875
|
return {
|
|
31869
31876
|
kind,
|
|
31870
31877
|
endpoint: stringValue(tracker.endpoint) ?? "",
|
|
31871
|
-
api_key: resolveEnvValue(stringValue(tracker.api_key), undefined)
|
|
31878
|
+
api_key: resolveEnvValue(stringValue(tracker.api_key), undefined),
|
|
31879
|
+
label_prefix,
|
|
31880
|
+
filter
|
|
31881
|
+
};
|
|
31882
|
+
}
|
|
31883
|
+
function buildFilterConfig(filter) {
|
|
31884
|
+
return {
|
|
31885
|
+
assignee: csvValue(filter.assignee) ?? [],
|
|
31886
|
+
label: csvValue(filter.label) ?? []
|
|
31872
31887
|
};
|
|
31873
31888
|
}
|
|
31874
31889
|
function validateConfig(config2) {
|
|
@@ -32062,6 +32077,120 @@ function normalizeTrackerKind(kind) {
|
|
|
32062
32077
|
return normalized || null;
|
|
32063
32078
|
}
|
|
32064
32079
|
|
|
32080
|
+
// src/label.ts
|
|
32081
|
+
var LABEL_COLORS = {
|
|
32082
|
+
dispatched: "1d76db",
|
|
32083
|
+
done: "0e8a16",
|
|
32084
|
+
failed: "d93f0b"
|
|
32085
|
+
};
|
|
32086
|
+
var LABEL_TIMEOUT_MS = 1e4;
|
|
32087
|
+
var GITHUB_ISSUE_URL_RE = /https?:\/\/[^/]+\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)/;
|
|
32088
|
+
function parseGitHubIssueUrl(url2) {
|
|
32089
|
+
const match = url2.match(GITHUB_ISSUE_URL_RE);
|
|
32090
|
+
if (!match)
|
|
32091
|
+
return null;
|
|
32092
|
+
const number4 = Number.parseInt(match[3], 10);
|
|
32093
|
+
if (Number.isNaN(number4))
|
|
32094
|
+
return null;
|
|
32095
|
+
return { owner: match[1], repo: match[2], number: number4 };
|
|
32096
|
+
}
|
|
32097
|
+
function formatLabelName(prefix, state) {
|
|
32098
|
+
return `${prefix}: ${state}`;
|
|
32099
|
+
}
|
|
32100
|
+
function createLabelService(config2) {
|
|
32101
|
+
const { kind, label_prefix } = config2.tracker;
|
|
32102
|
+
if (!label_prefix)
|
|
32103
|
+
return null;
|
|
32104
|
+
if (kind !== "github_projects")
|
|
32105
|
+
return null;
|
|
32106
|
+
const apiKey = config2.tracker.api_key;
|
|
32107
|
+
const endpoint2 = config2.tracker.endpoint;
|
|
32108
|
+
const prefix = label_prefix;
|
|
32109
|
+
const headers = {
|
|
32110
|
+
Authorization: `bearer ${apiKey ?? ""}`,
|
|
32111
|
+
"Content-Type": "application/json",
|
|
32112
|
+
Accept: "application/vnd.github+json"
|
|
32113
|
+
};
|
|
32114
|
+
return {
|
|
32115
|
+
async setLabel(issue2, state) {
|
|
32116
|
+
if (!issue2.url)
|
|
32117
|
+
return;
|
|
32118
|
+
const parsed = parseGitHubIssueUrl(issue2.url);
|
|
32119
|
+
if (!parsed)
|
|
32120
|
+
return;
|
|
32121
|
+
const ctx = { endpoint: endpoint2, owner: parsed.owner, repo: parsed.repo, headers };
|
|
32122
|
+
try {
|
|
32123
|
+
const labelName = formatLabelName(prefix, state);
|
|
32124
|
+
await ensureLabelExists(ctx, labelName, state);
|
|
32125
|
+
await removeExistingPrefixLabels(ctx, parsed.number, prefix);
|
|
32126
|
+
await addLabel(ctx, parsed.number, labelName);
|
|
32127
|
+
} catch (err) {
|
|
32128
|
+
console.warn(`[label] error setting label issue_url=${issue2.url}: ${err}`);
|
|
32129
|
+
}
|
|
32130
|
+
}
|
|
32131
|
+
};
|
|
32132
|
+
}
|
|
32133
|
+
async function fetchWithTimeout(url2, options) {
|
|
32134
|
+
const controller = new AbortController;
|
|
32135
|
+
const timer = setTimeout(() => controller.abort(), LABEL_TIMEOUT_MS);
|
|
32136
|
+
try {
|
|
32137
|
+
return await fetch(url2, { ...options, signal: controller.signal });
|
|
32138
|
+
} finally {
|
|
32139
|
+
clearTimeout(timer);
|
|
32140
|
+
}
|
|
32141
|
+
}
|
|
32142
|
+
async function ensureLabelExists(ctx, name, state) {
|
|
32143
|
+
const { endpoint: endpoint2, owner, repo, headers } = ctx;
|
|
32144
|
+
const url2 = `${endpoint2}/repos/${owner}/${repo}/labels`;
|
|
32145
|
+
const response = await fetchWithTimeout(url2, {
|
|
32146
|
+
method: "POST",
|
|
32147
|
+
headers,
|
|
32148
|
+
body: JSON.stringify({ name, color: LABEL_COLORS[state] })
|
|
32149
|
+
});
|
|
32150
|
+
if (!response.ok && response.status !== 422) {
|
|
32151
|
+
console.warn(`[label] failed to ensure label exists label_name=${name} owner=${owner} repo=${repo}: HTTP ${response.status}`);
|
|
32152
|
+
}
|
|
32153
|
+
}
|
|
32154
|
+
async function removeExistingPrefixLabels(ctx, number4, prefix) {
|
|
32155
|
+
const { endpoint: endpoint2, owner, repo, headers } = ctx;
|
|
32156
|
+
const url2 = `${endpoint2}/repos/${owner}/${repo}/issues/${number4}/labels`;
|
|
32157
|
+
const response = await fetchWithTimeout(url2, { method: "GET", headers });
|
|
32158
|
+
if (!response.ok) {
|
|
32159
|
+
console.warn(`[label] failed to fetch existing labels owner=${owner} repo=${repo} issue_number=${number4}: HTTP ${response.status}`);
|
|
32160
|
+
return;
|
|
32161
|
+
}
|
|
32162
|
+
let labels;
|
|
32163
|
+
try {
|
|
32164
|
+
labels = await response.json();
|
|
32165
|
+
} catch {
|
|
32166
|
+
console.warn(`[label] failed to parse label list response for issue_number=${number4}`);
|
|
32167
|
+
return;
|
|
32168
|
+
}
|
|
32169
|
+
const toRemove = labels.filter((l) => l.name.startsWith(`${prefix}: `));
|
|
32170
|
+
for (const label of toRemove) {
|
|
32171
|
+
const deleteUrl = `${url2}/${encodeURIComponent(label.name)}`;
|
|
32172
|
+
const deleteResponse = await fetchWithTimeout(deleteUrl, { method: "DELETE", headers }).catch((err) => {
|
|
32173
|
+
console.warn(`[label] failed to remove label "${label.name}" issue_number=${number4}: ${err}`);
|
|
32174
|
+
return null;
|
|
32175
|
+
});
|
|
32176
|
+
if (deleteResponse && !deleteResponse.ok) {
|
|
32177
|
+
console.warn(`[label] failed to remove label "${label.name}" owner=${owner} repo=${repo} issue_number=${number4}: HTTP ${deleteResponse.status}`);
|
|
32178
|
+
}
|
|
32179
|
+
}
|
|
32180
|
+
}
|
|
32181
|
+
async function addLabel(ctx, number4, name) {
|
|
32182
|
+
const { endpoint: endpoint2, owner, repo, headers } = ctx;
|
|
32183
|
+
const url2 = `${endpoint2}/repos/${owner}/${repo}/issues/${number4}/labels`;
|
|
32184
|
+
const response = await fetchWithTimeout(url2, {
|
|
32185
|
+
method: "POST",
|
|
32186
|
+
headers,
|
|
32187
|
+
body: JSON.stringify({ labels: [name] })
|
|
32188
|
+
});
|
|
32189
|
+
if (!response.ok) {
|
|
32190
|
+
console.warn(`[label] failed to add label label_name=${name} owner=${owner} repo=${repo} issue_number=${number4}: HTTP ${response.status}`);
|
|
32191
|
+
}
|
|
32192
|
+
}
|
|
32193
|
+
|
|
32065
32194
|
// ../../node_modules/.bun/liquidjs@10.25.0/node_modules/liquidjs/dist/liquid.node.mjs
|
|
32066
32195
|
import { PassThrough } from "stream";
|
|
32067
32196
|
import { sep as sep3, extname, resolve as resolve$1, dirname as dirname$1 } from "path";
|
|
@@ -36756,6 +36885,28 @@ function issueToTemplateVars(issue2) {
|
|
|
36756
36885
|
};
|
|
36757
36886
|
}
|
|
36758
36887
|
|
|
36888
|
+
// src/filter.ts
|
|
36889
|
+
function matchesFilter(issue2, filter2) {
|
|
36890
|
+
if (filter2.assignee.length > 0) {
|
|
36891
|
+
const filterAssignees = new Set(filter2.assignee.map((a2) => a2.toLowerCase()));
|
|
36892
|
+
if (!issue2.assignees.some((a2) => filterAssignees.has(a2.toLowerCase()))) {
|
|
36893
|
+
return false;
|
|
36894
|
+
}
|
|
36895
|
+
}
|
|
36896
|
+
if (filter2.label.length > 0) {
|
|
36897
|
+
const filterLabels = new Set(filter2.label.map((l) => l.toLowerCase()));
|
|
36898
|
+
if (!issue2.labels.some((l) => filterLabels.has(l.toLowerCase()))) {
|
|
36899
|
+
return false;
|
|
36900
|
+
}
|
|
36901
|
+
}
|
|
36902
|
+
return true;
|
|
36903
|
+
}
|
|
36904
|
+
|
|
36905
|
+
// src/tracker/types.ts
|
|
36906
|
+
function isTrackerError(val) {
|
|
36907
|
+
return typeof val === "object" && val !== null && "code" in val;
|
|
36908
|
+
}
|
|
36909
|
+
|
|
36759
36910
|
// src/tracker/asana.ts
|
|
36760
36911
|
var PAGE_SIZE = 50;
|
|
36761
36912
|
var NETWORK_TIMEOUT_MS3 = 30000;
|
|
@@ -36764,6 +36915,7 @@ function createAsanaAdapter(config2) {
|
|
|
36764
36915
|
const apiKey = config2.tracker.api_key;
|
|
36765
36916
|
const projectGid = config2.tracker.project_gid ?? "";
|
|
36766
36917
|
const activeSections = config2.tracker.active_sections ?? ["To Do", "In Progress"];
|
|
36918
|
+
const filter2 = config2.tracker.filter;
|
|
36767
36919
|
function headers() {
|
|
36768
36920
|
return {
|
|
36769
36921
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -36809,7 +36961,7 @@ function createAsanaAdapter(config2) {
|
|
|
36809
36961
|
async function fetchTasksInSection(sectionGid, sectionName) {
|
|
36810
36962
|
const issues = [];
|
|
36811
36963
|
let offset2 = null;
|
|
36812
|
-
const fields = "gid,name,notes,dependencies,tags,created_at,modified_at,custom_fields,memberships.section.name";
|
|
36964
|
+
const fields = "gid,name,notes,dependencies,tags,created_at,modified_at,custom_fields,memberships.section.name,assignee,assignee.email";
|
|
36813
36965
|
do {
|
|
36814
36966
|
const url2 = offset2 ? `${endpoint2}/sections/${sectionGid}/tasks?opt_fields=${fields}&limit=${PAGE_SIZE}&offset=${encodeURIComponent(offset2)}` : `${endpoint2}/sections/${sectionGid}/tasks?opt_fields=${fields}&limit=${PAGE_SIZE}`;
|
|
36815
36967
|
const result = await fetchJson(url2);
|
|
@@ -36828,7 +36980,10 @@ function createAsanaAdapter(config2) {
|
|
|
36828
36980
|
}
|
|
36829
36981
|
return {
|
|
36830
36982
|
async fetchCandidateIssues() {
|
|
36831
|
-
|
|
36983
|
+
const issues = await fetchTasks(activeSections);
|
|
36984
|
+
if (isTrackerError(issues))
|
|
36985
|
+
return issues;
|
|
36986
|
+
return issues.filter((issue2) => matchesFilter(issue2, filter2));
|
|
36832
36987
|
},
|
|
36833
36988
|
async fetchIssuesByStates(states) {
|
|
36834
36989
|
if (states.length === 0)
|
|
@@ -36859,6 +37014,7 @@ function createAsanaAdapter(config2) {
|
|
|
36859
37014
|
state: state ?? "",
|
|
36860
37015
|
branch_name: null,
|
|
36861
37016
|
url: null,
|
|
37017
|
+
assignees: [],
|
|
36862
37018
|
labels: [],
|
|
36863
37019
|
blocked_by: [],
|
|
36864
37020
|
created_at: null,
|
|
@@ -36877,6 +37033,8 @@ function normalizeAsanaTask(task, sectionName) {
|
|
|
36877
37033
|
identifier: String(dep.gid ?? ""),
|
|
36878
37034
|
state: null
|
|
36879
37035
|
})) : [];
|
|
37036
|
+
const assigneeObj = task.assignee;
|
|
37037
|
+
const assignees = assigneeObj?.email ? [assigneeObj.email] : [];
|
|
36880
37038
|
return {
|
|
36881
37039
|
id: gid,
|
|
36882
37040
|
identifier: gid,
|
|
@@ -36886,6 +37044,7 @@ function normalizeAsanaTask(task, sectionName) {
|
|
|
36886
37044
|
state: sectionName,
|
|
36887
37045
|
branch_name: null,
|
|
36888
37046
|
url: null,
|
|
37047
|
+
assignees,
|
|
36889
37048
|
labels,
|
|
36890
37049
|
blocked_by: blockedBy,
|
|
36891
37050
|
created_at: task.created_at ? new Date(String(task.created_at)) : null,
|
|
@@ -36911,6 +37070,7 @@ function createGitHubAdapter(config2) {
|
|
|
36911
37070
|
const projectNumber = config2.tracker.project_number ?? 0;
|
|
36912
37071
|
const projectId = config2.tracker.project_id ?? null;
|
|
36913
37072
|
const activeStatuses = config2.tracker.active_statuses ?? ["Todo", "In Progress"];
|
|
37073
|
+
const filter2 = config2.tracker.filter;
|
|
36914
37074
|
const octokit = createAuthenticatedGraphql(config2);
|
|
36915
37075
|
async function runGraphql2(query, variables = {}) {
|
|
36916
37076
|
try {
|
|
@@ -36928,11 +37088,11 @@ function createGitHubAdapter(config2) {
|
|
|
36928
37088
|
}
|
|
36929
37089
|
}
|
|
36930
37090
|
const PROJECT_ITEMS_QUERY = `
|
|
36931
|
-
query($owner: String!, $number: Int!, $cursor: String) {
|
|
37091
|
+
query($owner: String!, $number: Int!, $cursor: String, $search: String) {
|
|
36932
37092
|
repositoryOwner(login: $owner) {
|
|
36933
37093
|
... on Organization {
|
|
36934
37094
|
projectV2(number: $number) {
|
|
36935
|
-
items(first: ${PAGE_SIZE2}, after: $cursor) {
|
|
37095
|
+
items(first: ${PAGE_SIZE2}, after: $cursor, query: $search) {
|
|
36936
37096
|
pageInfo { hasNextPage endCursor }
|
|
36937
37097
|
nodes {
|
|
36938
37098
|
id
|
|
@@ -36948,11 +37108,13 @@ function createGitHubAdapter(config2) {
|
|
|
36948
37108
|
... on Issue {
|
|
36949
37109
|
number title body url
|
|
36950
37110
|
labels(first: 20) { nodes { name } }
|
|
37111
|
+
assignees(first: 10) { nodes { login } }
|
|
36951
37112
|
createdAt updatedAt
|
|
36952
37113
|
}
|
|
36953
37114
|
... on PullRequest {
|
|
36954
37115
|
number title body url
|
|
36955
37116
|
labels(first: 20) { nodes { name } }
|
|
37117
|
+
assignees(first: 10) { nodes { login } }
|
|
36956
37118
|
createdAt updatedAt
|
|
36957
37119
|
}
|
|
36958
37120
|
}
|
|
@@ -36962,7 +37124,7 @@ function createGitHubAdapter(config2) {
|
|
|
36962
37124
|
}
|
|
36963
37125
|
... on User {
|
|
36964
37126
|
projectV2(number: $number) {
|
|
36965
|
-
items(first: ${PAGE_SIZE2}, after: $cursor) {
|
|
37127
|
+
items(first: ${PAGE_SIZE2}, after: $cursor, query: $search) {
|
|
36966
37128
|
pageInfo { hasNextPage endCursor }
|
|
36967
37129
|
nodes {
|
|
36968
37130
|
id
|
|
@@ -36978,11 +37140,13 @@ function createGitHubAdapter(config2) {
|
|
|
36978
37140
|
... on Issue {
|
|
36979
37141
|
number title body url
|
|
36980
37142
|
labels(first: 20) { nodes { name } }
|
|
37143
|
+
assignees(first: 10) { nodes { login } }
|
|
36981
37144
|
createdAt updatedAt
|
|
36982
37145
|
}
|
|
36983
37146
|
... on PullRequest {
|
|
36984
37147
|
number title body url
|
|
36985
37148
|
labels(first: 20) { nodes { name } }
|
|
37149
|
+
assignees(first: 10) { nodes { login } }
|
|
36986
37150
|
createdAt updatedAt
|
|
36987
37151
|
}
|
|
36988
37152
|
}
|
|
@@ -36994,10 +37158,10 @@ function createGitHubAdapter(config2) {
|
|
|
36994
37158
|
}
|
|
36995
37159
|
`;
|
|
36996
37160
|
const PROJECT_BY_ID_QUERY = `
|
|
36997
|
-
query($projectId: ID!, $cursor: String) {
|
|
37161
|
+
query($projectId: ID!, $cursor: String, $search: String) {
|
|
36998
37162
|
node(id: $projectId) {
|
|
36999
37163
|
... on ProjectV2 {
|
|
37000
|
-
items(first: ${PAGE_SIZE2}, after: $cursor) {
|
|
37164
|
+
items(first: ${PAGE_SIZE2}, after: $cursor, query: $search) {
|
|
37001
37165
|
pageInfo { hasNextPage endCursor }
|
|
37002
37166
|
nodes {
|
|
37003
37167
|
id
|
|
@@ -37013,11 +37177,13 @@ function createGitHubAdapter(config2) {
|
|
|
37013
37177
|
... on Issue {
|
|
37014
37178
|
number title body url
|
|
37015
37179
|
labels(first: 20) { nodes { name } }
|
|
37180
|
+
assignees(first: 10) { nodes { login } }
|
|
37016
37181
|
createdAt updatedAt
|
|
37017
37182
|
}
|
|
37018
37183
|
... on PullRequest {
|
|
37019
37184
|
number title body url
|
|
37020
37185
|
labels(first: 20) { nodes { name } }
|
|
37186
|
+
assignees(first: 10) { nodes { login } }
|
|
37021
37187
|
createdAt updatedAt
|
|
37022
37188
|
}
|
|
37023
37189
|
}
|
|
@@ -37048,11 +37214,11 @@ function createGitHubAdapter(config2) {
|
|
|
37048
37214
|
}
|
|
37049
37215
|
}
|
|
37050
37216
|
`;
|
|
37051
|
-
async function fetchAllItems(statusFilter) {
|
|
37217
|
+
async function fetchAllItems(statusFilter, search2 = "") {
|
|
37052
37218
|
const issues = [];
|
|
37053
37219
|
let cursor = null;
|
|
37054
37220
|
do {
|
|
37055
|
-
const result = projectId ? await runGraphql2(PROJECT_BY_ID_QUERY, { projectId, cursor }) : await runGraphql2(PROJECT_ITEMS_QUERY, { owner, number: projectNumber, cursor });
|
|
37221
|
+
const result = projectId ? await runGraphql2(PROJECT_BY_ID_QUERY, { projectId, cursor, search: search2 }) : await runGraphql2(PROJECT_ITEMS_QUERY, { owner, number: projectNumber, cursor, search: search2 });
|
|
37056
37222
|
if ("code" in result)
|
|
37057
37223
|
return result;
|
|
37058
37224
|
const payload = result.data;
|
|
@@ -37072,8 +37238,8 @@ function createGitHubAdapter(config2) {
|
|
|
37072
37238
|
const status = extractStatus(node);
|
|
37073
37239
|
if (!status)
|
|
37074
37240
|
continue;
|
|
37075
|
-
const
|
|
37076
|
-
if (
|
|
37241
|
+
const statusMatches = statusFilter.length === 0 || statusFilter.some((s2) => normalizeState(s2) === normalizeState(status));
|
|
37242
|
+
if (statusMatches) {
|
|
37077
37243
|
issues.push(normalizeProjectItem(node, status));
|
|
37078
37244
|
}
|
|
37079
37245
|
}
|
|
@@ -37091,7 +37257,7 @@ function createGitHubAdapter(config2) {
|
|
|
37091
37257
|
}
|
|
37092
37258
|
return {
|
|
37093
37259
|
async fetchCandidateIssues() {
|
|
37094
|
-
return fetchAllItems(activeStatuses);
|
|
37260
|
+
return fetchAllItems(activeStatuses, buildQueryString(filter2));
|
|
37095
37261
|
},
|
|
37096
37262
|
async fetchIssuesByStates(states) {
|
|
37097
37263
|
if (states.length === 0)
|
|
@@ -37138,11 +37304,21 @@ function extractStatus(node) {
|
|
|
37138
37304
|
}
|
|
37139
37305
|
return null;
|
|
37140
37306
|
}
|
|
37307
|
+
function buildQueryString(filter2) {
|
|
37308
|
+
const parts = [];
|
|
37309
|
+
if (filter2.assignee.length > 0)
|
|
37310
|
+
parts.push(`assignee:${filter2.assignee.join(",")}`);
|
|
37311
|
+
if (filter2.label.length > 0)
|
|
37312
|
+
parts.push(`label:${filter2.label.join(",")}`);
|
|
37313
|
+
return parts.join(" ");
|
|
37314
|
+
}
|
|
37141
37315
|
function normalizeProjectItem(node, status) {
|
|
37142
37316
|
const content = node.content;
|
|
37143
37317
|
const number4 = content?.number;
|
|
37144
37318
|
const identifier = number4 ? `#${number4}` : String(node.id ?? "");
|
|
37145
37319
|
const labels = Array.isArray(content?.labels?.nodes) ? content.labels.nodes.map((l) => (l.name ?? "").toLowerCase()).filter(Boolean) : [];
|
|
37320
|
+
const assigneeNodes = content?.assignees?.nodes;
|
|
37321
|
+
const assignees = Array.isArray(assigneeNodes) ? assigneeNodes.map((n2) => n2.login ?? "").filter(Boolean) : [];
|
|
37146
37322
|
return {
|
|
37147
37323
|
id: String(node.id ?? ""),
|
|
37148
37324
|
identifier,
|
|
@@ -37152,6 +37328,7 @@ function normalizeProjectItem(node, status) {
|
|
|
37152
37328
|
state: status,
|
|
37153
37329
|
branch_name: null,
|
|
37154
37330
|
url: content?.url ? String(content.url) : null,
|
|
37331
|
+
assignees,
|
|
37155
37332
|
labels,
|
|
37156
37333
|
blocked_by: [],
|
|
37157
37334
|
created_at: content?.createdAt ? new Date(String(content.createdAt)) : null,
|
|
@@ -37159,11 +37336,6 @@ function normalizeProjectItem(node, status) {
|
|
|
37159
37336
|
};
|
|
37160
37337
|
}
|
|
37161
37338
|
|
|
37162
|
-
// src/tracker/types.ts
|
|
37163
|
-
function isTrackerError(val) {
|
|
37164
|
-
return typeof val === "object" && val !== null && "code" in val;
|
|
37165
|
-
}
|
|
37166
|
-
|
|
37167
37339
|
// src/tracker/index.ts
|
|
37168
37340
|
function createTrackerAdapter(config2) {
|
|
37169
37341
|
const { kind } = config2.tracker;
|
|
@@ -40171,6 +40343,7 @@ class Orchestrator {
|
|
|
40171
40343
|
workflowPath;
|
|
40172
40344
|
pollTimer = null;
|
|
40173
40345
|
fileWatcher = null;
|
|
40346
|
+
labelService = null;
|
|
40174
40347
|
constructor(workflowPath) {
|
|
40175
40348
|
this.workflowPath = workflowPath;
|
|
40176
40349
|
const wf = loadWorkflow(workflowPath);
|
|
@@ -40179,6 +40352,7 @@ class Orchestrator {
|
|
|
40179
40352
|
}
|
|
40180
40353
|
this.workflow = wf;
|
|
40181
40354
|
this.config = buildConfig(wf);
|
|
40355
|
+
this.labelService = createLabelService(this.config);
|
|
40182
40356
|
this.state = {
|
|
40183
40357
|
poll_interval_ms: this.config.polling.interval_ms,
|
|
40184
40358
|
max_concurrent_agents: this.config.agent.max_concurrent_agents,
|
|
@@ -40309,6 +40483,9 @@ class Orchestrator {
|
|
|
40309
40483
|
};
|
|
40310
40484
|
this.state.running.set(issue2.id, entry);
|
|
40311
40485
|
console.warn(`[orchestrator] dispatching issue_id=${issue2.id} issue_identifier=${issue2.identifier} attempt=${attempt ?? "first"}`);
|
|
40486
|
+
this.labelService?.setLabel(issue2, "dispatched").catch((err) => {
|
|
40487
|
+
console.warn(`[orchestrator] label service error issue_id=${issue2.id}: ${err}`);
|
|
40488
|
+
});
|
|
40312
40489
|
this.runWorker(issue2, attempt).catch((err) => {
|
|
40313
40490
|
console.error(`[orchestrator] worker uncaught error issue_id=${issue2.id}: ${err}`);
|
|
40314
40491
|
});
|
|
@@ -40428,9 +40605,15 @@ class Orchestrator {
|
|
|
40428
40605
|
this.state.agent_totals.output_tokens += running.agent_output_tokens;
|
|
40429
40606
|
this.state.agent_totals.total_tokens += running.agent_total_tokens;
|
|
40430
40607
|
if (reason === "normal") {
|
|
40608
|
+
this.labelService?.setLabel(running.issue, "done").catch((err) => {
|
|
40609
|
+
console.warn(`[orchestrator] label service error issue_id=${issueId}: ${err}`);
|
|
40610
|
+
});
|
|
40431
40611
|
this.state.completed.add(issueId);
|
|
40432
40612
|
this.scheduleRetry(issueId, running.identifier, 1, null, "continuation");
|
|
40433
40613
|
} else {
|
|
40614
|
+
this.labelService?.setLabel(running.issue, "failed").catch((err) => {
|
|
40615
|
+
console.warn(`[orchestrator] label service error issue_id=${issueId}: ${err}`);
|
|
40616
|
+
});
|
|
40434
40617
|
const nextAttempt = nextAttemptFrom(running.retry_attempt);
|
|
40435
40618
|
this.scheduleRetry(issueId, running.identifier, nextAttempt, error48, "failure");
|
|
40436
40619
|
}
|
|
@@ -40520,6 +40703,12 @@ class Orchestrator {
|
|
|
40520
40703
|
const isActive = activeStates.some((s2) => normalizeState(s2) === normalizedState);
|
|
40521
40704
|
if (isTerminal) {
|
|
40522
40705
|
console.warn(`[orchestrator] issue terminal, stopping worker issue_id=${issue2.id} state=${issue2.state}`);
|
|
40706
|
+
const runningEntry = this.state.running.get(issue2.id);
|
|
40707
|
+
if (runningEntry) {
|
|
40708
|
+
this.labelService?.setLabel(runningEntry.issue, "done").catch((err) => {
|
|
40709
|
+
console.warn(`[orchestrator] label service error issue_id=${issue2.id}: ${err}`);
|
|
40710
|
+
});
|
|
40711
|
+
}
|
|
40523
40712
|
this.terminateRunningIssue(issue2.id, true);
|
|
40524
40713
|
} else if (isActive) {
|
|
40525
40714
|
const entry = this.state.running.get(issue2.id);
|
|
@@ -40585,6 +40774,7 @@ class Orchestrator {
|
|
|
40585
40774
|
}
|
|
40586
40775
|
this.workflow = wf;
|
|
40587
40776
|
this.config = newConfig;
|
|
40777
|
+
this.labelService = createLabelService(newConfig);
|
|
40588
40778
|
this.state.poll_interval_ms = newConfig.polling.interval_ms;
|
|
40589
40779
|
this.state.max_concurrent_agents = newConfig.agent.max_concurrent_agents;
|
|
40590
40780
|
console.warn("[orchestrator] workflow reloaded successfully");
|
package/package.json
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pleaseai/work",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"description": "Symphony-spec orchestrator for Claude Code + Asana/GitHub Projects v2",
|
|
6
6
|
"license": "FSL-1.1-MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/
|
|
9
|
+
"url": "https://github.com/pleaseai/work-please.git",
|
|
10
10
|
"directory": "apps/work-please"
|
|
11
11
|
},
|
|
12
|
-
"files": [
|
|
13
|
-
"dist",
|
|
14
|
-
"LICENSE"
|
|
15
|
-
],
|
|
16
12
|
"bin": {
|
|
17
13
|
"work-please": "./dist/index.js"
|
|
18
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md",
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
19
20
|
"scripts": {
|
|
20
|
-
"
|
|
21
|
+
"prepublishOnly": "cp ../../LICENSE ../../README.md .",
|
|
22
|
+
"postpublish": "rm -f LICENSE README.md",
|
|
23
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target bun && bun run scripts/add-shebang.ts",
|
|
21
24
|
"dev": "bun run --watch src/index.ts",
|
|
22
25
|
"lint": "eslint .",
|
|
23
26
|
"lint:fix": "eslint . --fix",
|
|
24
27
|
"check": "tsc --noEmit",
|
|
25
|
-
"test": "bun test"
|
|
28
|
+
"test": "bun test",
|
|
29
|
+
"test:coverage": "bun test --coverage --coverage-reporter=lcov"
|
|
26
30
|
},
|
|
27
31
|
"dependencies": {
|
|
28
32
|
"@anthropic-ai/claude-agent-sdk": "^0.2.72",
|