@pleaseai/work 0.1.5 → 0.1.7

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 (3) hide show
  1. package/README.md +94 -59
  2. package/dist/index.js +135 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -36,13 +36,13 @@ instead of supervising coding agents.
36
36
 
37
37
  Work Please is a long-running TypeScript service that:
38
38
 
39
- 1. Polls an issue tracker (Asana or GitHub Projects v2) for tasks in configured active states.
39
+ 1. Polls an issue tracker (GitHub Projects v2 or Asana) for tasks in configured active states.
40
40
  2. Creates an isolated workspace directory for each eligible issue.
41
41
  3. Launches a Claude Code agent session inside that workspace with a rendered prompt.
42
42
  4. Monitors the session, handles retries, and reconciles issue state on each poll cycle.
43
43
 
44
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.
45
+ adapted for GitHub Projects v2 / Asana and Claude Code instead of Linear and Codex.
46
46
 
47
47
  For full technical details, see [SPEC.md](SPEC.md).
48
48
 
@@ -50,18 +50,18 @@ For full technical details, see [SPEC.md](SPEC.md).
50
50
 
51
51
  | | Symphony (reference) | Work Please |
52
52
  |---|---|---|
53
- | Issue Tracker | Linear | Asana & GitHub Projects v2 |
53
+ | Issue Tracker | Linear | GitHub Projects v2 & Asana (under development) |
54
54
  | Coding Agent | Codex (app-server mode) | Claude Code CLI |
55
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 |
56
+ | Tracker Auth | `LINEAR_API_KEY` | `GITHUB_TOKEN`, GitHub App credentials, or `ASANA_ACCESS_TOKEN` |
57
+ | Project Config | `project_slug` | `owner` + `project_number` (GitHub Projects v2) or `project_gid` (Asana) |
58
+ | Issue States | Linear workflow states | GitHub Projects v2 Status field / Asana sections |
59
59
  | Agent Protocol | JSON-RPC over stdio | `@anthropic-ai/claude-agent-sdk` |
60
60
  | Permission Model | Codex approval/sandbox policies | Claude Code `--permission-mode` |
61
61
 
62
62
  ## Features
63
63
 
64
- - **Multi-tracker support** — Dispatch work from Asana tasks or GitHub Projects v2 items on a
64
+ - **Multi-tracker support** — Dispatch work from GitHub Projects v2 items or Asana tasks (under development) on a
65
65
  fixed cadence.
66
66
  - **GitHub App authentication** — Authenticate the GitHub tracker with a GitHub App installation
67
67
  token (`app_id` + `private_key` + `installation_id`) instead of a PAT, for fine-grained
@@ -90,8 +90,8 @@ Config Layer ──> Orchestrator ──> Workspace Manager ──> Agent Runner
90
90
  | |
91
91
  v v
92
92
  Issue Tracker Client Isolated workspace/
93
- (Asana REST API or per-issue directory
94
- GitHub GraphQL API,
93
+ (GitHub GraphQL API or per-issue directory
94
+ Asana REST API,
95
95
  polling + reconciliation)
96
96
  |
97
97
  v
@@ -103,7 +103,7 @@ Components:
103
103
  - **Workflow Loader** — Parses `WORKFLOW.md` YAML front matter and prompt template body.
104
104
  - **Config Layer** — Typed getters with env-var indirection and built-in defaults.
105
105
  - **Issue Tracker Client** — Fetches candidate issues, reconciles running-issue states. Supports
106
- Asana (REST API) and GitHub Projects v2 (GraphQL API) adapters.
106
+ GitHub Projects v2 (GraphQL API) and Asana (REST API) adapters.
107
107
  - **Orchestrator** — Owns in-memory state; drives the poll/dispatch/retry loop.
108
108
  - **Workspace Manager** — Creates, reuses, and cleans per-issue workspaces; runs hooks.
109
109
  - **Agent Runner** — Launches Claude Code, streams events back to the orchestrator.
@@ -117,9 +117,9 @@ See [SPEC.md](SPEC.md) for the full specification.
117
117
 
118
118
  - **Bun** (see [bun.sh](https://bun.sh) for installation)
119
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)
120
+ - **GitHub token** (`GITHUB_TOKEN`) with access to the target project, **or** **GitHub App credentials**
121
+ (`GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_INSTALLATION_ID`) see [GitHub App Authentication](#github-app-authentication),
122
+ **or** **Asana access token** (`ASANA_ACCESS_TOKEN`) (under development)
123
123
 
124
124
  ### Install
125
125
 
@@ -133,18 +133,22 @@ bun run build
133
133
  ### Configure
134
134
 
135
135
  Create a `WORKFLOW.md` in your target repository. Two examples are shown below.
136
+ See also the [example WORKFLOW.md](https://github.com/pleaseai/workflow/blob/main/WORKFLOW.md) for a real-world reference.
136
137
 
137
- #### Asana
138
+ #### GitHub Projects v2 (PAT)
139
+
140
+ See also the [example GitHub Project](https://github.com/orgs/pleaseai/projects/2) for a real-world reference.
138
141
 
139
142
  ```markdown
140
143
  ---
141
144
  tracker:
142
- kind: asana
143
- api_key: $ASANA_ACCESS_TOKEN
144
- project_gid: "1234567890123456"
145
- active_sections:
145
+ kind: github_projects
146
+ api_key: $GITHUB_TOKEN
147
+ owner: your-org
148
+ project_number: 42
149
+ active_statuses:
146
150
  - In Progress
147
- terminal_sections:
151
+ terminal_statuses:
148
152
  - Done
149
153
  - Cancelled
150
154
 
@@ -165,12 +169,13 @@ agent:
165
169
 
166
170
  claude:
167
171
  permission_mode: acceptEdits
172
+ # setting_sources: [] # default: [project, local, user]; set [] for SDK isolation mode
168
173
  turn_timeout_ms: 3600000
169
174
  ---
170
175
 
171
- You are working on an Asana task for the project.
176
+ You are working on a GitHub issue for the repository `your-org/your-repo`.
172
177
 
173
- Task: {{ issue.title }}
178
+ Issue {{ issue.identifier }}: {{ issue.title }}
174
179
 
175
180
  {{ issue.description }}
176
181
 
@@ -186,19 +191,23 @@ This is attempt #{{ attempt }}. Review any prior work in the workspace before co
186
191
  {% endif %}
187
192
 
188
193
  Your task:
189
- 1. Understand the task requirements.
194
+ 1. Understand the issue requirements.
190
195
  2. Implement the requested changes.
191
196
  3. Write or update tests as needed.
192
- 4. Open a pull request and move this task to the review section.
197
+ 4. Open a pull request and move this issue to the review status.
193
198
  ```
194
199
 
195
- #### GitHub Projects v2 (PAT)
200
+ #### GitHub Projects v2 (GitHub App)
201
+
202
+ Use GitHub App credentials instead of a PAT for fine-grained permissions and higher API rate limits:
196
203
 
197
204
  ```markdown
198
205
  ---
199
206
  tracker:
200
207
  kind: github_projects
201
- api_key: $GITHUB_TOKEN
208
+ app_id: $GITHUB_APP_ID
209
+ private_key: $GITHUB_APP_PRIVATE_KEY
210
+ installation_id: $GITHUB_APP_INSTALLATION_ID
202
211
  owner: your-org
203
212
  project_number: 42
204
213
  active_statuses:
@@ -224,6 +233,7 @@ agent:
224
233
 
225
234
  claude:
226
235
  permission_mode: acceptEdits
236
+ # setting_sources: [] # default: [project, local, user]; set [] for SDK isolation mode
227
237
  turn_timeout_ms: 3600000
228
238
  ---
229
239
 
@@ -251,22 +261,19 @@ Your task:
251
261
  4. Open a pull request and move this issue to the review status.
252
262
  ```
253
263
 
254
- #### GitHub Projects v2 (GitHub App)
264
+ #### Asana (under development)
255
265
 
256
- Use GitHub App credentials instead of a PAT for fine-grained permissions and higher API rate limits:
266
+ > **Note**: Asana support is under development. The configuration below is a preview and may change.
257
267
 
258
268
  ```markdown
259
269
  ---
260
270
  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:
271
+ kind: asana
272
+ api_key: $ASANA_ACCESS_TOKEN
273
+ project_gid: "1234567890123456"
274
+ active_sections:
268
275
  - In Progress
269
- terminal_statuses:
276
+ terminal_sections:
270
277
  - Done
271
278
  - Cancelled
272
279
 
@@ -287,27 +294,45 @@ agent:
287
294
 
288
295
  claude:
289
296
  permission_mode: acceptEdits
297
+ # setting_sources: [] # default: [project, local, user]; set [] for SDK isolation mode
290
298
  turn_timeout_ms: 3600000
291
299
  ---
292
300
 
293
- You are working on a GitHub issue for the repository `your-org/your-repo`.
301
+ You are working on an Asana task for the project.
294
302
 
295
- Issue {{ issue.identifier }}: {{ issue.title }}
303
+ Task: {{ issue.title }}
296
304
 
297
305
  {{ issue.description }}
306
+
307
+ {% if issue.blocked_by.size > 0 %}
308
+ Blocked by:
309
+ {% for blocker in issue.blocked_by %}
310
+ - {{ blocker.identifier }} ({{ blocker.state }})
311
+ {% endfor %}
312
+ {% endif %}
313
+
314
+ {% if attempt %}
315
+ This is attempt #{{ attempt }}. Review any prior work in the workspace before continuing.
316
+ {% endif %}
317
+
318
+ Your task:
319
+ 1. Understand the task requirements.
320
+ 2. Implement the requested changes.
321
+ 3. Write or update tests as needed.
322
+ 4. Open a pull request and move this task to the review section.
298
323
  ```
299
324
 
300
325
  ### Run
301
326
 
302
327
  ```bash
303
- # Set your tracker token
304
- export ASANA_ACCESS_TOKEN=your_token_here
305
- # or (GitHub PAT)
328
+ # Set your tracker token (GitHub PAT)
306
329
  export GITHUB_TOKEN=ghp_your_token_here
307
330
  # or (GitHub App)
308
331
  export GITHUB_APP_ID=12345
309
332
  export GITHUB_APP_PRIVATE_KEY="$(cat path/to/private-key.pem)"
310
333
  export GITHUB_APP_INSTALLATION_ID=67890
334
+ # or (Asana — under development)
335
+ export ASANA_ACCESS_TOKEN=your_token_here
311
336
 
312
337
  # Run Work Please against a WORKFLOW.md in the current directory
313
338
  bunx work-please
@@ -329,34 +354,34 @@ front matter configuration block with a Markdown prompt template body.
329
354
  ```yaml
330
355
  ---
331
356
  tracker:
332
- kind: asana # Required: "asana" or "github_projects"
357
+ kind: github_projects # Required: "github_projects" or "asana"
333
358
 
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"]
359
+ # --- GitHub Projects v2 fields (when kind == "github_projects") ---
360
+ api_key: $GITHUB_TOKEN # Required: token or $ENV_VAR
361
+ endpoint: https://api.github.com # Optional: override GitHub API base URL
362
+ owner: your-org # Required: GitHub organization or user login
363
+ project_number: 42 # Required: GitHub Projects v2 project number
364
+ project_id: PVT_kwDOxxxxx # Optional: project node ID (bypasses owner+project_number lookup)
365
+ active_statuses: # Optional: default ["Todo", "In Progress"]
339
366
  - In Progress
340
- terminal_sections: # Optional: default ["Done", "Cancelled"]
367
+ terminal_statuses: # Optional: default ["Done", "Cancelled"]
341
368
  - Done
342
369
  - 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
370
  # GitHub App authentication (alternative to api_key — all three required together):
356
371
  # app_id: $GITHUB_APP_ID # Optional: GitHub App ID (integer or $ENV_VAR)
357
372
  # private_key: $GITHUB_APP_PRIVATE_KEY # Optional: GitHub App private key PEM or $ENV_VAR
358
373
  # installation_id: $GITHUB_APP_INSTALLATION_ID # Optional: installation ID (integer or $ENV_VAR)
359
374
 
375
+ # --- Asana fields (when kind == "asana") --- UNDER DEVELOPMENT
376
+ # api_key: $ASANA_ACCESS_TOKEN # Required: token or $ENV_VAR
377
+ # endpoint: https://app.asana.com/api/1.0 # Optional: override Asana API base URL
378
+ # project_gid: "1234567890123456" # Required: Asana project GID
379
+ # active_sections: # Optional: default ["To Do", "In Progress"]
380
+ # - In Progress
381
+ # terminal_sections: # Optional: default ["Done", "Cancelled"]
382
+ # - Done
383
+ # - Cancelled
384
+
360
385
  # --- Shared filter fields (both trackers) ---
361
386
  # filter:
362
387
  # assignee: user1, user2 # Optional: CSV or YAML array; case-insensitive OR match
@@ -389,14 +414,24 @@ agent:
389
414
 
390
415
  claude:
391
416
  command: claude # Optional: Claude Code CLI command, default "claude"
417
+ effort: high # Optional: reasoning depth — 'low', 'medium', 'high', or 'max'. Default 'high'.
392
418
  permission_mode: acceptEdits # Optional: one of 'default', 'acceptEdits', 'bypassPermissions'. Defaults to 'bypassPermissions'.
393
419
  allowed_tools: # Optional: restrict available tools
394
420
  - Read
395
421
  - Write
396
422
  - Bash
423
+ setting_sources: # Optional: filesystem settings to load. Default: [project, local, user]
424
+ - project # load .claude/settings.json + CLAUDE.md from the workspace directory
425
+ - local # load .claude/settings.local.json from the workspace directory
426
+ - user # load ~/.claude/settings.json + global CLAUDE.md
427
+ # Only "project", "local", and "user" are valid — other values are ignored
397
428
  turn_timeout_ms: 3600000 # Optional: per-turn timeout in ms, default 3600000
398
429
  read_timeout_ms: 5000 # Optional: initial subprocess read timeout in ms, default 5000
399
430
  stall_timeout_ms: 300000 # Optional: stall detection timeout, default 300000
431
+ settings:
432
+ attribution:
433
+ commit: "🙏 Generated with [Work Please](https://github.com/pleaseai/work-please)" # Optional: appended to git commit messages. Defaults to Work Please link.
434
+ pr: "🙏 Generated with [Work Please](https://github.com/pleaseai/work-please)" # Optional: appended to PR descriptions. Defaults to Work Please link.
400
435
 
401
436
  server:
402
437
  port: 3000 # Optional: enable HTTP dashboard on this port
package/dist/index.js CHANGED
@@ -2166,7 +2166,7 @@ var {
2166
2166
  var package_default = {
2167
2167
  name: "@pleaseai/work",
2168
2168
  type: "module",
2169
- version: "0.1.5",
2169
+ version: "0.1.7",
2170
2170
  description: "Symphony-spec orchestrator for Claude Code + Asana/GitHub Projects v2",
2171
2171
  license: "FSL-1.1-MIT",
2172
2172
  repository: {
@@ -3088,6 +3088,13 @@ agent:
3088
3088
  max_turns: 20
3089
3089
  claude:
3090
3090
  permission_mode: bypassPermissions
3091
+ # claude.settings controls the attribution text written into .claude/settings.local.json
3092
+ # of each workspace. Omit to use the default Work Please attribution.
3093
+ # claude:
3094
+ # settings:
3095
+ # attribution:
3096
+ # commit: "\uD83D\uDE4F Generated with Work Please"
3097
+ # pr: "\uD83D\uDE4F Generated with Work Please"
3091
3098
  # server:
3092
3099
  # port: 3000
3093
3100
  ---
@@ -31679,7 +31686,11 @@ function createToolsMcpServer(config2) {
31679
31686
  }
31680
31687
 
31681
31688
  // src/agent-runner.ts
31689
+ var UUID_PATTERN = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i;
31690
+ var NEWLINE_PATTERN = /[\r\n]/g;
31691
+
31682
31692
  class AppServerClient {
31693
+ assignedSessionId = null;
31683
31694
  sessionId = null;
31684
31695
  abortController = null;
31685
31696
  workspace;
@@ -31690,11 +31701,20 @@ class AppServerClient {
31690
31701
  this.workspace = workspace;
31691
31702
  this.queryFn = queryFn;
31692
31703
  }
31693
- async startSession() {
31704
+ async startSession(sessionId) {
31705
+ this.assignedSessionId = null;
31706
+ this.sessionId = null;
31707
+ if (sessionId !== undefined && !UUID_PATTERN.test(sessionId)) {
31708
+ const preview = String(sessionId).slice(0, 64).replace(NEWLINE_PATTERN, " ");
31709
+ return new Error(`invalid_session_id: expected UUID format, got "${preview}"`);
31710
+ }
31694
31711
  const validationErr = this.validateWorkspaceCwd();
31695
31712
  if (validationErr)
31696
31713
  return validationErr;
31697
- return { threadId: randomUUID(), workspace: this.workspace };
31714
+ const id = sessionId ?? randomUUID();
31715
+ this.assignedSessionId = id;
31716
+ this.sessionId = sessionId ?? null;
31717
+ return { sessionId: id, workspace: this.workspace };
31698
31718
  }
31699
31719
  async runTurn(session, prompt, _issue, onMessage) {
31700
31720
  const controller = new AbortController;
@@ -31713,6 +31733,8 @@ class AppServerClient {
31713
31733
  }
31714
31734
  if (this.sessionId) {
31715
31735
  options.resume = this.sessionId;
31736
+ } else if (this.assignedSessionId) {
31737
+ options.sessionId = this.assignedSessionId;
31716
31738
  }
31717
31739
  if (this.config.claude.command !== "claude") {
31718
31740
  options.pathToClaudeCodeExecutable = this.config.claude.command;
@@ -31720,12 +31742,22 @@ class AppServerClient {
31720
31742
  if (this.config.claude.model) {
31721
31743
  options.model = this.config.claude.model;
31722
31744
  }
31745
+ const sp = this.config.claude.system_prompt;
31746
+ if (sp.type === "custom") {
31747
+ options.systemPrompt = sp.value;
31748
+ } else {
31749
+ options.systemPrompt = sp;
31750
+ }
31751
+ options.effort = this.config.claude.effort;
31723
31752
  const toolSpecs = getToolSpecs(this.config);
31724
31753
  if (toolSpecs.length > 0) {
31725
31754
  options.mcpServers = {
31726
31755
  "work-please-tools": createToolsMcpServer(this.config)
31727
31756
  };
31728
31757
  }
31758
+ if (this.config.claude.setting_sources.length > 0) {
31759
+ options.settingSources = this.config.claude.setting_sources;
31760
+ }
31729
31761
  const turnId = randomUUID();
31730
31762
  let sessionId = null;
31731
31763
  let gotError = false;
@@ -31737,11 +31769,11 @@ class AppServerClient {
31737
31769
  const initMsg = msg;
31738
31770
  sessionId = initMsg.session_id;
31739
31771
  this.sessionId = sessionId;
31772
+ this.assignedSessionId = null;
31740
31773
  onMessage({
31741
31774
  event: "session_started",
31742
31775
  timestamp: new Date,
31743
31776
  session_id: sessionId,
31744
- thread_id: session.threadId,
31745
31777
  turn_id: turnId
31746
31778
  });
31747
31779
  } else if (msg.type === "result") {
@@ -31794,12 +31826,18 @@ class AppServerClient {
31794
31826
  });
31795
31827
  return err;
31796
31828
  }
31797
- return { thread_id: session.threadId, turn_id: turnId, session_id: sessionId };
31829
+ return { turn_id: turnId, session_id: sessionId };
31798
31830
  } catch (err) {
31799
31831
  clearTimeout(timeoutHandle);
31800
31832
  const error48 = err instanceof Error ? err : new Error(String(err));
31833
+ if (!sessionId) {
31834
+ if (!options.resume) {
31835
+ this.sessionId = null;
31836
+ this.assignedSessionId = null;
31837
+ }
31838
+ }
31801
31839
  onMessage({
31802
- event: "startup_failed",
31840
+ event: sessionId ? "turn_failed" : "startup_failed",
31803
31841
  timestamp: new Date,
31804
31842
  payload: { reason: error48.message }
31805
31843
  });
@@ -31808,6 +31846,7 @@ class AppServerClient {
31808
31846
  }
31809
31847
  stopSession() {
31810
31848
  this.abortController?.abort();
31849
+ this.assignedSessionId = null;
31811
31850
  this.sessionId = null;
31812
31851
  this.abortController = null;
31813
31852
  }
@@ -31828,6 +31867,7 @@ import { tmpdir } from "os";
31828
31867
  import { join, sep as sep2 } from "path";
31829
31868
  import process4 from "process";
31830
31869
  var ENV_VAR_RE = /^\$([A-Z_]\w*)$/i;
31870
+ var VALID_SETTING_SOURCES = new Set(["user", "project", "local"]);
31831
31871
  var DEFAULTS2 = {
31832
31872
  POLL_INTERVAL_MS: 30000,
31833
31873
  WORKSPACE_ROOT: join(tmpdir(), "work-please_workspaces"),
@@ -31835,9 +31875,11 @@ var DEFAULTS2 = {
31835
31875
  MAX_CONCURRENT_AGENTS: 10,
31836
31876
  AGENT_MAX_TURNS: 20,
31837
31877
  MAX_RETRY_BACKOFF_MS: 300000,
31878
+ CLAUDE_EFFORT: "high",
31838
31879
  CLAUDE_COMMAND: "claude",
31839
31880
  CLAUDE_PERMISSION_MODE: "bypassPermissions",
31840
31881
  CLAUDE_ALLOWED_TOOLS: [],
31882
+ CLAUDE_SETTING_SOURCES: ["project", "local", "user"],
31841
31883
  CLAUDE_TURN_TIMEOUT_MS: 3600000,
31842
31884
  CLAUDE_READ_TIMEOUT_MS: 5000,
31843
31885
  CLAUDE_STALL_TIMEOUT_MS: 300000,
@@ -31879,20 +31921,34 @@ function buildConfig(workflow) {
31879
31921
  max_retry_backoff_ms: posIntValue(agent.max_retry_backoff_ms, DEFAULTS2.MAX_RETRY_BACKOFF_MS),
31880
31922
  max_concurrent_agents_by_state: stateLimitsValue(agent.max_concurrent_agents_by_state)
31881
31923
  },
31882
- claude: {
31883
- model: stringValue(claude.model),
31884
- command: commandValue(claude.command) ?? DEFAULTS2.CLAUDE_COMMAND,
31885
- permission_mode: stringValue(claude.permission_mode) ?? DEFAULTS2.CLAUDE_PERMISSION_MODE,
31886
- allowed_tools: stringArrayValue(claude.allowed_tools, DEFAULTS2.CLAUDE_ALLOWED_TOOLS),
31887
- turn_timeout_ms: intValue(claude.turn_timeout_ms, DEFAULTS2.CLAUDE_TURN_TIMEOUT_MS),
31888
- read_timeout_ms: intValue(claude.read_timeout_ms, DEFAULTS2.CLAUDE_READ_TIMEOUT_MS),
31889
- stall_timeout_ms: intValue(claude.stall_timeout_ms, DEFAULTS2.CLAUDE_STALL_TIMEOUT_MS)
31890
- },
31924
+ claude: buildClaudeConfig(claude),
31891
31925
  server: {
31892
31926
  port: nonNegIntOrNull(server.port)
31893
31927
  }
31894
31928
  };
31895
31929
  }
31930
+ function buildClaudeConfig(claude) {
31931
+ const settingsSec = sectionMap(claude, "settings");
31932
+ const attributionSec = sectionMap(settingsSec, "attribution");
31933
+ return {
31934
+ model: stringValue(claude.model),
31935
+ effort: effortValue(claude.effort, DEFAULTS2.CLAUDE_EFFORT),
31936
+ command: commandValue(claude.command) ?? DEFAULTS2.CLAUDE_COMMAND,
31937
+ permission_mode: stringValue(claude.permission_mode) ?? DEFAULTS2.CLAUDE_PERMISSION_MODE,
31938
+ allowed_tools: stringArrayValue(claude.allowed_tools, DEFAULTS2.CLAUDE_ALLOWED_TOOLS),
31939
+ setting_sources: stringArrayValue(claude.setting_sources, DEFAULTS2.CLAUDE_SETTING_SOURCES).filter((s2) => VALID_SETTING_SOURCES.has(s2)),
31940
+ turn_timeout_ms: intValue(claude.turn_timeout_ms, DEFAULTS2.CLAUDE_TURN_TIMEOUT_MS),
31941
+ read_timeout_ms: intValue(claude.read_timeout_ms, DEFAULTS2.CLAUDE_READ_TIMEOUT_MS),
31942
+ stall_timeout_ms: intValue(claude.stall_timeout_ms, DEFAULTS2.CLAUDE_STALL_TIMEOUT_MS),
31943
+ system_prompt: systemPromptValue(claude.system_prompt),
31944
+ settings: {
31945
+ attribution: {
31946
+ commit: stringValue(attributionSec.commit),
31947
+ pr: stringValue(attributionSec.pr)
31948
+ }
31949
+ }
31950
+ };
31951
+ }
31896
31952
  function buildTrackerConfig(kind, tracker) {
31897
31953
  const label_prefix = stringValue(tracker.label_prefix) ?? null;
31898
31954
  const filter = buildFilterConfig(sectionMap(tracker, "filter"));
@@ -32006,6 +32062,26 @@ function maxConcurrentForState(config2, state) {
32006
32062
  const byState = config2.agent.max_concurrent_agents_by_state;
32007
32063
  return byState[normalized] ?? config2.agent.max_concurrent_agents;
32008
32064
  }
32065
+ var DEFAULT_SYSTEM_PROMPT = { type: "preset", preset: "claude_code" };
32066
+ function systemPromptValue(val) {
32067
+ if (val == null)
32068
+ return DEFAULT_SYSTEM_PROMPT;
32069
+ if (typeof val === "string") {
32070
+ const trimmed = val.trim();
32071
+ return trimmed ? { type: "custom", value: trimmed } : DEFAULT_SYSTEM_PROMPT;
32072
+ }
32073
+ if (typeof val === "object" && !Array.isArray(val)) {
32074
+ const obj = val;
32075
+ if (obj.type === "preset" && obj.preset === "claude_code") {
32076
+ return typeof obj.append === "string" ? { type: "preset", preset: "claude_code", append: obj.append } : { type: "preset", preset: "claude_code" };
32077
+ }
32078
+ if (obj.type === "custom" && typeof obj.value === "string") {
32079
+ const trimmed = obj.value.trim();
32080
+ return trimmed ? { type: "custom", value: trimmed } : DEFAULT_SYSTEM_PROMPT;
32081
+ }
32082
+ }
32083
+ return DEFAULT_SYSTEM_PROMPT;
32084
+ }
32009
32085
  function sectionMap(raw, key) {
32010
32086
  const val = raw[key];
32011
32087
  return val && typeof val === "object" && !Array.isArray(val) ? val : {};
@@ -32047,6 +32123,18 @@ function hookScriptValue(val) {
32047
32123
  const trimmed = val.trimEnd();
32048
32124
  return trimmed === "" ? null : trimmed;
32049
32125
  }
32126
+ function effortValue(val, fallback) {
32127
+ const s2 = typeof val === "string" ? val.trim() : val;
32128
+ switch (s2) {
32129
+ case "low":
32130
+ case "medium":
32131
+ case "high":
32132
+ case "max":
32133
+ return s2;
32134
+ default:
32135
+ return fallback;
32136
+ }
32137
+ }
32050
32138
  function commandValue(val) {
32051
32139
  if (typeof val !== "string")
32052
32140
  return null;
@@ -32070,7 +32158,7 @@ function csvValue(val) {
32070
32158
  function stringArrayValue(val, fallback) {
32071
32159
  if (!Array.isArray(val))
32072
32160
  return fallback;
32073
- return val.filter((v) => typeof v === "string");
32161
+ return val.filter((v) => typeof v === "string" && v.trim().length > 0);
32074
32162
  }
32075
32163
  function stateLimitsValue(val) {
32076
32164
  if (!val || typeof val !== "object" || Array.isArray(val))
@@ -40173,9 +40261,28 @@ function isWorkflowError(result) {
40173
40261
  }
40174
40262
 
40175
40263
  // src/workspace.ts
40176
- import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync2, rmSync as rmSync2, statSync as statSync3 } from "fs";
40177
- import { join as join4, resolve as resolve4, sep as sep4 } from "path";
40264
+ import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
40265
+ import { dirname as dirname2, join as join4, resolve as resolve4, sep as sep4 } from "path";
40178
40266
  import process6 from "process";
40267
+ var CLAUDE_SETTINGS_PATH = ".claude/settings.local.json";
40268
+ var WORK_PLEASE_URL = "https://github.com/pleaseai/work-please";
40269
+ var ATTRIBUTION_TEXT = `\uD83D\uDE4F Generated with [Work Please](${WORK_PLEASE_URL})`;
40270
+ function generateClaudeSettings(attribution) {
40271
+ return `${JSON.stringify({
40272
+ attribution: {
40273
+ commit: attribution?.commit ?? ATTRIBUTION_TEXT,
40274
+ pr: attribution?.pr ?? ATTRIBUTION_TEXT
40275
+ }
40276
+ }, null, 2)}
40277
+ `;
40278
+ }
40279
+ function ensureClaudeSettings(wsPath, attribution) {
40280
+ const settingsPath = join4(wsPath, CLAUDE_SETTINGS_PATH);
40281
+ if (existsSync4(settingsPath))
40282
+ return;
40283
+ mkdirSync2(dirname2(settingsPath), { recursive: true });
40284
+ writeFileSync2(settingsPath, generateClaudeSettings(attribution), "utf-8");
40285
+ }
40179
40286
  var _git = {
40180
40287
  spawnSync: (args) => Bun.spawnSync(args)
40181
40288
  };
@@ -40308,6 +40415,11 @@ async function createWorkspace(config2, identifier, issue2) {
40308
40415
  if (hookErr)
40309
40416
  return hookErr;
40310
40417
  }
40418
+ try {
40419
+ ensureClaudeSettings(wtPath, config2.claude.settings.attribution);
40420
+ } catch (err) {
40421
+ return err instanceof Error ? err : new Error(String(err));
40422
+ }
40311
40423
  return { path: wtPath, workspace_key: key, created_now: createdNow };
40312
40424
  }
40313
40425
  }
@@ -40338,6 +40450,11 @@ async function createWorkspace(config2, identifier, issue2) {
40338
40450
  if (hookErr)
40339
40451
  return hookErr;
40340
40452
  }
40453
+ try {
40454
+ ensureClaudeSettings(wsPath, config2.claude.settings.attribution);
40455
+ } catch (err) {
40456
+ return err instanceof Error ? err : new Error(String(err));
40457
+ }
40341
40458
  return workspace;
40342
40459
  }
40343
40460
  async function removeWorkspace(config2, identifier, issue2) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pleaseai/work",
3
3
  "type": "module",
4
- "version": "0.1.5",
4
+ "version": "0.1.7",
5
5
  "description": "Symphony-spec orchestrator for Claude Code + Asana/GitHub Projects v2",
6
6
  "license": "FSL-1.1-MIT",
7
7
  "repository": {