@pleaseai/work 0.0.0 → 0.1.3

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.
Files changed (4) hide show
  1. package/LICENSE +112 -0
  2. package/README.md +563 -0
  3. package/dist/index.js +240 -25
  4. 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
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=pleaseai_work-please&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=pleaseai_work-please) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=pleaseai_work-please&metric=bugs)](https://sonarcloud.io/summary/new_code?id=pleaseai_work-please) [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=pleaseai_work-please&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=pleaseai_work-please) [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=pleaseai_work-please&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=pleaseai_work-please)
4
+ [![codecov](https://codecov.io/gh/pleaseai/work-please/graph/badge.svg?token=do858Z1lsI)](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,53 @@ 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
+ function serializeCause(cause) {
36910
+ if (cause instanceof Error)
36911
+ return cause.message;
36912
+ if (typeof cause === "string")
36913
+ return cause;
36914
+ try {
36915
+ return JSON.stringify(cause) ?? String(cause);
36916
+ } catch {
36917
+ return String(cause);
36918
+ }
36919
+ }
36920
+ function formatTrackerError(err) {
36921
+ switch (err.code) {
36922
+ case "github_projects_api_status":
36923
+ case "asana_api_status":
36924
+ return `${err.code} (HTTP ${err.status})`;
36925
+ case "github_projects_graphql_errors":
36926
+ return `${err.code}: ${JSON.stringify(err.errors)}`;
36927
+ case "github_projects_api_request":
36928
+ case "asana_api_request":
36929
+ return `${err.code}: ${serializeCause(err.cause)}`;
36930
+ default:
36931
+ return err.code;
36932
+ }
36933
+ }
36934
+
36759
36935
  // src/tracker/asana.ts
36760
36936
  var PAGE_SIZE = 50;
36761
36937
  var NETWORK_TIMEOUT_MS3 = 30000;
@@ -36764,6 +36940,7 @@ function createAsanaAdapter(config2) {
36764
36940
  const apiKey = config2.tracker.api_key;
36765
36941
  const projectGid = config2.tracker.project_gid ?? "";
36766
36942
  const activeSections = config2.tracker.active_sections ?? ["To Do", "In Progress"];
36943
+ const filter2 = config2.tracker.filter;
36767
36944
  function headers() {
36768
36945
  return {
36769
36946
  Authorization: `Bearer ${apiKey}`,
@@ -36809,7 +36986,7 @@ function createAsanaAdapter(config2) {
36809
36986
  async function fetchTasksInSection(sectionGid, sectionName) {
36810
36987
  const issues = [];
36811
36988
  let offset2 = null;
36812
- const fields = "gid,name,notes,dependencies,tags,created_at,modified_at,custom_fields,memberships.section.name";
36989
+ const fields = "gid,name,notes,dependencies,tags,created_at,modified_at,custom_fields,memberships.section.name,assignee,assignee.email";
36813
36990
  do {
36814
36991
  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
36992
  const result = await fetchJson(url2);
@@ -36828,7 +37005,10 @@ function createAsanaAdapter(config2) {
36828
37005
  }
36829
37006
  return {
36830
37007
  async fetchCandidateIssues() {
36831
- return fetchTasks(activeSections);
37008
+ const issues = await fetchTasks(activeSections);
37009
+ if (isTrackerError(issues))
37010
+ return issues;
37011
+ return issues.filter((issue2) => matchesFilter(issue2, filter2));
36832
37012
  },
36833
37013
  async fetchIssuesByStates(states) {
36834
37014
  if (states.length === 0)
@@ -36859,6 +37039,7 @@ function createAsanaAdapter(config2) {
36859
37039
  state: state ?? "",
36860
37040
  branch_name: null,
36861
37041
  url: null,
37042
+ assignees: [],
36862
37043
  labels: [],
36863
37044
  blocked_by: [],
36864
37045
  created_at: null,
@@ -36877,6 +37058,8 @@ function normalizeAsanaTask(task, sectionName) {
36877
37058
  identifier: String(dep.gid ?? ""),
36878
37059
  state: null
36879
37060
  })) : [];
37061
+ const assigneeObj = task.assignee;
37062
+ const assignees = assigneeObj?.email ? [assigneeObj.email] : [];
36880
37063
  return {
36881
37064
  id: gid,
36882
37065
  identifier: gid,
@@ -36886,6 +37069,7 @@ function normalizeAsanaTask(task, sectionName) {
36886
37069
  state: sectionName,
36887
37070
  branch_name: null,
36888
37071
  url: null,
37072
+ assignees,
36889
37073
  labels,
36890
37074
  blocked_by: blockedBy,
36891
37075
  created_at: task.created_at ? new Date(String(task.created_at)) : null,
@@ -36911,6 +37095,7 @@ function createGitHubAdapter(config2) {
36911
37095
  const projectNumber = config2.tracker.project_number ?? 0;
36912
37096
  const projectId = config2.tracker.project_id ?? null;
36913
37097
  const activeStatuses = config2.tracker.active_statuses ?? ["Todo", "In Progress"];
37098
+ const filter2 = config2.tracker.filter;
36914
37099
  const octokit = createAuthenticatedGraphql(config2);
36915
37100
  async function runGraphql2(query, variables = {}) {
36916
37101
  try {
@@ -36928,11 +37113,11 @@ function createGitHubAdapter(config2) {
36928
37113
  }
36929
37114
  }
36930
37115
  const PROJECT_ITEMS_QUERY = `
36931
- query($owner: String!, $number: Int!, $cursor: String) {
37116
+ query($owner: String!, $number: Int!, $cursor: String, $search: String) {
36932
37117
  repositoryOwner(login: $owner) {
36933
37118
  ... on Organization {
36934
37119
  projectV2(number: $number) {
36935
- items(first: ${PAGE_SIZE2}, after: $cursor) {
37120
+ items(first: ${PAGE_SIZE2}, after: $cursor, query: $search) {
36936
37121
  pageInfo { hasNextPage endCursor }
36937
37122
  nodes {
36938
37123
  id
@@ -36948,11 +37133,13 @@ function createGitHubAdapter(config2) {
36948
37133
  ... on Issue {
36949
37134
  number title body url
36950
37135
  labels(first: 20) { nodes { name } }
37136
+ assignees(first: 10) { nodes { login } }
36951
37137
  createdAt updatedAt
36952
37138
  }
36953
37139
  ... on PullRequest {
36954
37140
  number title body url
36955
37141
  labels(first: 20) { nodes { name } }
37142
+ assignees(first: 10) { nodes { login } }
36956
37143
  createdAt updatedAt
36957
37144
  }
36958
37145
  }
@@ -36962,7 +37149,7 @@ function createGitHubAdapter(config2) {
36962
37149
  }
36963
37150
  ... on User {
36964
37151
  projectV2(number: $number) {
36965
- items(first: ${PAGE_SIZE2}, after: $cursor) {
37152
+ items(first: ${PAGE_SIZE2}, after: $cursor, query: $search) {
36966
37153
  pageInfo { hasNextPage endCursor }
36967
37154
  nodes {
36968
37155
  id
@@ -36978,11 +37165,13 @@ function createGitHubAdapter(config2) {
36978
37165
  ... on Issue {
36979
37166
  number title body url
36980
37167
  labels(first: 20) { nodes { name } }
37168
+ assignees(first: 10) { nodes { login } }
36981
37169
  createdAt updatedAt
36982
37170
  }
36983
37171
  ... on PullRequest {
36984
37172
  number title body url
36985
37173
  labels(first: 20) { nodes { name } }
37174
+ assignees(first: 10) { nodes { login } }
36986
37175
  createdAt updatedAt
36987
37176
  }
36988
37177
  }
@@ -36994,10 +37183,10 @@ function createGitHubAdapter(config2) {
36994
37183
  }
36995
37184
  `;
36996
37185
  const PROJECT_BY_ID_QUERY = `
36997
- query($projectId: ID!, $cursor: String) {
37186
+ query($projectId: ID!, $cursor: String, $search: String) {
36998
37187
  node(id: $projectId) {
36999
37188
  ... on ProjectV2 {
37000
- items(first: ${PAGE_SIZE2}, after: $cursor) {
37189
+ items(first: ${PAGE_SIZE2}, after: $cursor, query: $search) {
37001
37190
  pageInfo { hasNextPage endCursor }
37002
37191
  nodes {
37003
37192
  id
@@ -37013,11 +37202,13 @@ function createGitHubAdapter(config2) {
37013
37202
  ... on Issue {
37014
37203
  number title body url
37015
37204
  labels(first: 20) { nodes { name } }
37205
+ assignees(first: 10) { nodes { login } }
37016
37206
  createdAt updatedAt
37017
37207
  }
37018
37208
  ... on PullRequest {
37019
37209
  number title body url
37020
37210
  labels(first: 20) { nodes { name } }
37211
+ assignees(first: 10) { nodes { login } }
37021
37212
  createdAt updatedAt
37022
37213
  }
37023
37214
  }
@@ -37048,11 +37239,11 @@ function createGitHubAdapter(config2) {
37048
37239
  }
37049
37240
  }
37050
37241
  `;
37051
- async function fetchAllItems(statusFilter) {
37242
+ async function fetchAllItems(statusFilter, search2 = "") {
37052
37243
  const issues = [];
37053
37244
  let cursor = null;
37054
37245
  do {
37055
- const result = projectId ? await runGraphql2(PROJECT_BY_ID_QUERY, { projectId, cursor }) : await runGraphql2(PROJECT_ITEMS_QUERY, { owner, number: projectNumber, cursor });
37246
+ 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
37247
  if ("code" in result)
37057
37248
  return result;
37058
37249
  const payload = result.data;
@@ -37072,8 +37263,8 @@ function createGitHubAdapter(config2) {
37072
37263
  const status = extractStatus(node);
37073
37264
  if (!status)
37074
37265
  continue;
37075
- const matchesFilter = statusFilter.length === 0 || statusFilter.some((s2) => normalizeState(s2) === normalizeState(status));
37076
- if (matchesFilter) {
37266
+ const statusMatches = statusFilter.length === 0 || statusFilter.some((s2) => normalizeState(s2) === normalizeState(status));
37267
+ if (statusMatches) {
37077
37268
  issues.push(normalizeProjectItem(node, status));
37078
37269
  }
37079
37270
  }
@@ -37091,7 +37282,7 @@ function createGitHubAdapter(config2) {
37091
37282
  }
37092
37283
  return {
37093
37284
  async fetchCandidateIssues() {
37094
- return fetchAllItems(activeStatuses);
37285
+ return fetchAllItems(activeStatuses, buildQueryString(filter2));
37095
37286
  },
37096
37287
  async fetchIssuesByStates(states) {
37097
37288
  if (states.length === 0)
@@ -37138,11 +37329,21 @@ function extractStatus(node) {
37138
37329
  }
37139
37330
  return null;
37140
37331
  }
37332
+ function buildQueryString(filter2) {
37333
+ const parts = [];
37334
+ if (filter2.assignee.length > 0)
37335
+ parts.push(`assignee:${filter2.assignee.join(",")}`);
37336
+ if (filter2.label.length > 0)
37337
+ parts.push(`label:${filter2.label.join(",")}`);
37338
+ return parts.join(" ");
37339
+ }
37141
37340
  function normalizeProjectItem(node, status) {
37142
37341
  const content = node.content;
37143
37342
  const number4 = content?.number;
37144
37343
  const identifier = number4 ? `#${number4}` : String(node.id ?? "");
37145
37344
  const labels = Array.isArray(content?.labels?.nodes) ? content.labels.nodes.map((l) => (l.name ?? "").toLowerCase()).filter(Boolean) : [];
37345
+ const assigneeNodes = content?.assignees?.nodes;
37346
+ const assignees = Array.isArray(assigneeNodes) ? assigneeNodes.map((n2) => n2.login ?? "").filter(Boolean) : [];
37146
37347
  return {
37147
37348
  id: String(node.id ?? ""),
37148
37349
  identifier,
@@ -37152,6 +37353,7 @@ function normalizeProjectItem(node, status) {
37152
37353
  state: status,
37153
37354
  branch_name: null,
37154
37355
  url: content?.url ? String(content.url) : null,
37356
+ assignees,
37155
37357
  labels,
37156
37358
  blocked_by: [],
37157
37359
  created_at: content?.createdAt ? new Date(String(content.createdAt)) : null,
@@ -37159,11 +37361,6 @@ function normalizeProjectItem(node, status) {
37159
37361
  };
37160
37362
  }
37161
37363
 
37162
- // src/tracker/types.ts
37163
- function isTrackerError(val) {
37164
- return typeof val === "object" && val !== null && "code" in val;
37165
- }
37166
-
37167
37364
  // src/tracker/index.ts
37168
37365
  function createTrackerAdapter(config2) {
37169
37366
  const { kind } = config2.tracker;
@@ -40171,6 +40368,7 @@ class Orchestrator {
40171
40368
  workflowPath;
40172
40369
  pollTimer = null;
40173
40370
  fileWatcher = null;
40371
+ labelService = null;
40174
40372
  constructor(workflowPath) {
40175
40373
  this.workflowPath = workflowPath;
40176
40374
  const wf = loadWorkflow(workflowPath);
@@ -40179,6 +40377,7 @@ class Orchestrator {
40179
40377
  }
40180
40378
  this.workflow = wf;
40181
40379
  this.config = buildConfig(wf);
40380
+ this.labelService = createLabelService(this.config);
40182
40381
  this.state = {
40183
40382
  poll_interval_ms: this.config.polling.interval_ms,
40184
40383
  max_concurrent_agents: this.config.agent.max_concurrent_agents,
@@ -40241,13 +40440,13 @@ class Orchestrator {
40241
40440
  }
40242
40441
  const adapter = createTrackerAdapter(this.config);
40243
40442
  if (isTrackerError(adapter)) {
40244
- console.error(`[orchestrator] tracker adapter error: ${adapter.code}`);
40443
+ console.error(`[orchestrator] tracker adapter error: ${formatTrackerError(adapter)}`);
40245
40444
  this.scheduleTick(this.state.poll_interval_ms);
40246
40445
  return;
40247
40446
  }
40248
40447
  const candidatesResult = await adapter.fetchCandidateIssues();
40249
40448
  if (isTrackerError(candidatesResult)) {
40250
- console.error(`[orchestrator] tracker fetch failed: ${candidatesResult.code}`);
40449
+ console.error(`[orchestrator] tracker fetch failed: ${formatTrackerError(candidatesResult)}`);
40251
40450
  this.scheduleTick(this.state.poll_interval_ms);
40252
40451
  return;
40253
40452
  }
@@ -40309,6 +40508,9 @@ class Orchestrator {
40309
40508
  };
40310
40509
  this.state.running.set(issue2.id, entry);
40311
40510
  console.warn(`[orchestrator] dispatching issue_id=${issue2.id} issue_identifier=${issue2.identifier} attempt=${attempt ?? "first"}`);
40511
+ this.labelService?.setLabel(issue2, "dispatched").catch((err) => {
40512
+ console.warn(`[orchestrator] label service error issue_id=${issue2.id}: ${err}`);
40513
+ });
40312
40514
  this.runWorker(issue2, attempt).catch((err) => {
40313
40515
  console.error(`[orchestrator] worker uncaught error issue_id=${issue2.id}: ${err}`);
40314
40516
  });
@@ -40428,9 +40630,15 @@ class Orchestrator {
40428
40630
  this.state.agent_totals.output_tokens += running.agent_output_tokens;
40429
40631
  this.state.agent_totals.total_tokens += running.agent_total_tokens;
40430
40632
  if (reason === "normal") {
40633
+ this.labelService?.setLabel(running.issue, "done").catch((err) => {
40634
+ console.warn(`[orchestrator] label service error issue_id=${issueId}: ${err}`);
40635
+ });
40431
40636
  this.state.completed.add(issueId);
40432
40637
  this.scheduleRetry(issueId, running.identifier, 1, null, "continuation");
40433
40638
  } else {
40639
+ this.labelService?.setLabel(running.issue, "failed").catch((err) => {
40640
+ console.warn(`[orchestrator] label service error issue_id=${issueId}: ${err}`);
40641
+ });
40434
40642
  const nextAttempt = nextAttemptFrom(running.retry_attempt);
40435
40643
  this.scheduleRetry(issueId, running.identifier, nextAttempt, error48, "failure");
40436
40644
  }
@@ -40509,7 +40717,7 @@ class Orchestrator {
40509
40717
  return;
40510
40718
  const refreshed = await adapter.fetchIssueStatesByIds(runningIds);
40511
40719
  if (isTrackerError(refreshed)) {
40512
- console.warn(`[orchestrator] state refresh failed: ${refreshed.code} \u2014 keeping workers running`);
40720
+ console.warn(`[orchestrator] state refresh failed: ${formatTrackerError(refreshed)} \u2014 keeping workers running`);
40513
40721
  return;
40514
40722
  }
40515
40723
  const activeStates = getActiveStates(this.config);
@@ -40520,6 +40728,12 @@ class Orchestrator {
40520
40728
  const isActive = activeStates.some((s2) => normalizeState(s2) === normalizedState);
40521
40729
  if (isTerminal) {
40522
40730
  console.warn(`[orchestrator] issue terminal, stopping worker issue_id=${issue2.id} state=${issue2.state}`);
40731
+ const runningEntry = this.state.running.get(issue2.id);
40732
+ if (runningEntry) {
40733
+ this.labelService?.setLabel(runningEntry.issue, "done").catch((err) => {
40734
+ console.warn(`[orchestrator] label service error issue_id=${issue2.id}: ${err}`);
40735
+ });
40736
+ }
40523
40737
  this.terminateRunningIssue(issue2.id, true);
40524
40738
  } else if (isActive) {
40525
40739
  const entry = this.state.running.get(issue2.id);
@@ -40547,12 +40761,12 @@ class Orchestrator {
40547
40761
  const terminalStates = getTerminalStates(this.config);
40548
40762
  const adapter = createTrackerAdapter(this.config);
40549
40763
  if (isTrackerError(adapter)) {
40550
- console.warn(`[orchestrator] startup cleanup: adapter error ${adapter.code}`);
40764
+ console.warn(`[orchestrator] startup cleanup: adapter error ${formatTrackerError(adapter)}`);
40551
40765
  return;
40552
40766
  }
40553
40767
  const result = await adapter.fetchIssuesByStates(terminalStates);
40554
40768
  if (isTrackerError(result)) {
40555
- console.warn(`[orchestrator] startup terminal cleanup failed: ${result.code}`);
40769
+ console.warn(`[orchestrator] startup terminal cleanup failed: ${formatTrackerError(result)}`);
40556
40770
  return;
40557
40771
  }
40558
40772
  for (const issue2 of result) {
@@ -40585,6 +40799,7 @@ class Orchestrator {
40585
40799
  }
40586
40800
  this.workflow = wf;
40587
40801
  this.config = newConfig;
40802
+ this.labelService = createLabelService(newConfig);
40588
40803
  this.state.poll_interval_ms = newConfig.polling.interval_ms;
40589
40804
  this.state.max_concurrent_agents = newConfig.agent.max_concurrent_agents;
40590
40805
  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.0.0",
4
+ "version": "0.1.3",
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/chatbot-pf/work-please.git",
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
- "build": "bun build ./src/index.ts --outdir ./dist --target bun",
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",