@atollhq/skill-codex 0.4.16 → 0.4.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,6 +38,11 @@ For profile mode, Codex has the Atoll skill immediately, global Codex guidance p
38
38
 
39
39
  Use `@latest` in the `npx` command so npm does not reuse a stale cached installer. In profile mode, the installer prints its package version and a verification command; run `atoll --profile agent-a agent-context --json` if you need to confirm the profile was created.
40
40
 
41
+ The installer writes credential-bearing files atomically, refuses symbolic-link
42
+ targets, uses `0600` for credential files, and uses `0700` for dedicated
43
+ credential directories. Concurrent installer runs are serialized, and env-var
44
+ mode shell-quotes every value written to the shell profile.
45
+
41
46
  ## Using the integration
42
47
 
43
48
  Once installed, ask Codex anything task-related:
package/bin/install.mjs CHANGED
@@ -1,8 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, statSync, renameSync, rmSync } from 'node:fs'
3
+ import {
4
+ chmodSync,
5
+ closeSync,
6
+ copyFileSync,
7
+ cpSync,
8
+ existsSync,
9
+ lstatSync,
10
+ mkdirSync,
11
+ openSync,
12
+ readFileSync,
13
+ renameSync,
14
+ rmSync,
15
+ statSync,
16
+ writeFileSync,
17
+ } from 'node:fs'
4
18
  import { join, dirname, resolve, basename } from 'node:path'
5
19
  import { homedir } from 'node:os'
20
+ import fsExt from 'fs-ext-extra-prebuilt'
6
21
  import { fileURLToPath } from 'node:url'
7
22
 
8
23
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -114,21 +129,142 @@ if (!args.key.startsWith('sk_atoll_')) {
114
129
  process.exit(1)
115
130
  }
116
131
 
132
+ function lstatIfExists(path) {
133
+ try {
134
+ return lstatSync(path)
135
+ } catch (error) {
136
+ if (error?.code === 'ENOENT') return undefined
137
+ throw error
138
+ }
139
+ }
140
+
141
+ function assertSafeFile(path) {
142
+ const stat = lstatIfExists(path)
143
+ if (!stat) return
144
+ if (stat.isSymbolicLink()) {
145
+ throw new Error(`Refusing symbolic link for credential file: ${path}`)
146
+ }
147
+ if (!stat.isFile()) {
148
+ throw new Error(`Refusing non-file credential path: ${path}`)
149
+ }
150
+ }
151
+
152
+ function ensurePrivateDirectory(path) {
153
+ const stat = lstatIfExists(path)
154
+ if (stat) {
155
+ if (stat.isSymbolicLink()) {
156
+ throw new Error(`Refusing symbolic link for credential directory: ${path}`)
157
+ }
158
+ if (!stat.isDirectory()) {
159
+ throw new Error(`Refusing non-directory credential path: ${path}`)
160
+ }
161
+ } else {
162
+ mkdirSync(path, { recursive: true, mode: 0o700 })
163
+ }
164
+ chmodSync(path, 0o700)
165
+ }
166
+
117
167
  function readJson(path) {
168
+ assertSafeFile(path)
118
169
  if (!existsSync(path)) return {}
119
170
  try {
120
171
  return JSON.parse(readFileSync(path, 'utf-8'))
121
- } catch {
172
+ } catch (error) {
173
+ if (!(error instanceof SyntaxError)) throw error
122
174
  console.error(`Warning: could not parse ${path}, creating fresh config`)
123
175
  return {}
124
176
  }
125
177
  }
126
178
 
179
+ function readPrivateFile(path) {
180
+ assertSafeFile(path)
181
+ return readFileSync(path, 'utf-8')
182
+ }
183
+
184
+ function writePrivateFile(path, content) {
185
+ assertSafeFile(path)
186
+ const tmpPath = join(
187
+ dirname(path),
188
+ `.${basename(path)}.atoll-${process.pid}-${Date.now()}.tmp`,
189
+ )
190
+ try {
191
+ writeFileSync(tmpPath, content, { flag: 'wx', mode: 0o600 })
192
+ chmodSync(tmpPath, 0o600)
193
+ renameSync(tmpPath, path)
194
+ chmodSync(path, 0o600)
195
+ } catch (error) {
196
+ if (existsSync(tmpPath)) rmSync(tmpPath, { force: true })
197
+ throw error
198
+ }
199
+ }
200
+
201
+ function acquireInstallerLock() {
202
+ const atollDir = join(homedir(), '.atoll')
203
+ const target = join(atollDir, 'installer.lock-target')
204
+ ensurePrivateDirectory(atollDir)
205
+ try {
206
+ writeFileSync(target, '', { flag: 'wx', mode: 0o600 })
207
+ } catch (error) {
208
+ if (error?.code !== 'EEXIST') throw error
209
+ assertSafeFile(target)
210
+ }
211
+ chmodSync(target, 0o600)
212
+ const fd = openSync(target, 'r+')
213
+ const deadline = Date.now() + 10_000
214
+ const waitState = new Int32Array(new SharedArrayBuffer(4))
215
+ while (true) {
216
+ try {
217
+ if (process.platform === 'win32') {
218
+ fsExt.lockFileExSync(
219
+ fd,
220
+ fsExt.constants.LOCKFILE_EXCLUSIVE_LOCK
221
+ | fsExt.constants.LOCKFILE_FAIL_IMMEDIATELY,
222
+ 0,
223
+ 0,
224
+ 1,
225
+ 0,
226
+ )
227
+ } else {
228
+ fsExt.fcntlSync(fd, 'setlk', fsExt.constants.F_WRLCK, 0, 0)
229
+ }
230
+ break
231
+ } catch (error) {
232
+ if (!['EACCES', 'EAGAIN', 'EBUSY', 'EWOULDBLOCK'].includes(error?.code)) {
233
+ closeSync(fd)
234
+ throw error
235
+ }
236
+ if (Date.now() >= deadline) {
237
+ closeSync(fd)
238
+ throw new Error(`Timed out waiting for installer lock: ${target}`)
239
+ }
240
+ Atomics.wait(waitState, 0, 0, 25)
241
+ }
242
+ }
243
+
244
+ let released = false
245
+ return () => {
246
+ if (released) return
247
+ released = true
248
+ try {
249
+ if (process.platform === 'win32') {
250
+ fsExt.unlockFileExSync(fd, 0, 0, 1, 0)
251
+ } else {
252
+ fsExt.fcntlSync(fd, 'setlk', fsExt.constants.F_UNLCK, 0, 0)
253
+ }
254
+ } finally {
255
+ closeSync(fd)
256
+ }
257
+ }
258
+ }
259
+
260
+ const releaseInstallerLock = acquireInstallerLock()
261
+
127
262
  function writeAtollProfile() {
128
263
  if (!args.profile) return null
129
264
 
130
265
  const atollDir = join(homedir(), '.atoll')
131
266
  const configPath = join(atollDir, 'config.json')
267
+ ensurePrivateDirectory(atollDir)
132
268
  const config = readJson(configPath)
133
269
  config.profiles ??= {}
134
270
  config.profiles[args.profile] ??= {}
@@ -144,8 +280,7 @@ function writeAtollProfile() {
144
280
  if (args.baseUrl) profile.baseUrl = args.baseUrl
145
281
  else delete profile.baseUrl
146
282
 
147
- mkdirSync(atollDir, { recursive: true })
148
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n')
283
+ writePrivateFile(configPath, JSON.stringify(config, null, 2) + '\n')
149
284
  console.log(`Configured Atoll CLI profile "${args.profile}" in ${configPath}`)
150
285
  return configPath
151
286
  }
@@ -322,7 +457,7 @@ const profilePath = shell.includes('zsh')
322
457
  // 4. In profile mode, keep credentials scoped to the Atoll CLI profile.
323
458
  if (args.profile) {
324
459
  if (existsSync(profilePath)) {
325
- let profile = readFileSync(profilePath, 'utf-8')
460
+ let profile = readPrivateFile(profilePath)
326
461
  profile = removeExport(profile, 'ATOLL_PROFILE')
327
462
  profile = removeExport(profile, 'ATOLL_API_KEY')
328
463
  profile = removeExport(profile, 'ATOLL_ORG_ID')
@@ -330,7 +465,7 @@ if (args.profile) {
330
465
  profile = removeExport(profile, 'ATOLL_TEAM')
331
466
  profile = removeExport(profile, 'ATOLL_BASE_URL')
332
467
  profile = removeExport(profile, 'ATOLL_ENV_MODE')
333
- writeFileSync(profilePath, profile)
468
+ writePrivateFile(profilePath, profile)
334
469
  console.log(`Removed stale Atoll shell exports from ${profilePath}`)
335
470
  }
336
471
  writeAtollProfile()
@@ -339,14 +474,19 @@ if (args.profile) {
339
474
  console.log(`Run profile-scoped commands with: atoll --profile ${args.profile} ...`)
340
475
  console.log(`Verify the profile with: atoll --profile ${args.profile} agent-context --json`)
341
476
  console.log(`\nDone! Codex has the Atoll skill installed, and the Atoll CLI profile "${args.profile}" is configured.`)
477
+ releaseInstallerLock()
342
478
  process.exit(0)
343
479
  }
344
480
 
345
481
  // 4. Set env vars in shell profile for env-var mode.
482
+ function shellQuote(value) {
483
+ return `'${String(value).replaceAll("'", "'\"'\"'")}'`
484
+ }
485
+
346
486
  function upsertExport(profile, name, value) {
347
- const line = `export ${name}="${value}"`
487
+ const line = `export ${name}=${shellQuote(value)}`
348
488
  const pattern = new RegExp(`^export ${name}=.*$`, 'm')
349
- if (pattern.test(profile)) return profile.replace(pattern, line)
489
+ if (pattern.test(profile)) return profile.replace(pattern, () => line)
350
490
  return `${profile.trimEnd()}\n${line}\n`
351
491
  }
352
492
 
@@ -356,7 +496,7 @@ function removeExport(profile, name) {
356
496
  }
357
497
 
358
498
  if (existsSync(profilePath)) {
359
- let profile = readFileSync(profilePath, 'utf-8')
499
+ let profile = readPrivateFile(profilePath)
360
500
  if (!profile.includes('# Atoll (added by @atollhq/skill-codex)')) {
361
501
  profile = `${profile.trimEnd()}\n\n# Atoll (added by @atollhq/skill-codex)\n`
362
502
  }
@@ -370,7 +510,7 @@ if (existsSync(profilePath)) {
370
510
  if (args.baseUrl) profile = upsertExport(profile, 'ATOLL_BASE_URL', args.baseUrl)
371
511
  else profile = removeExport(profile, 'ATOLL_BASE_URL')
372
512
  profile = upsertExport(profile, 'ATOLL_ENV_MODE', '1')
373
- writeFileSync(profilePath, profile)
513
+ writePrivateFile(profilePath, profile)
374
514
 
375
515
  const configuredVars = []
376
516
  configuredVars.push('ATOLL_API_KEY', 'ATOLL_ORG_ID', 'ATOLL_ENV_MODE')
@@ -381,16 +521,17 @@ if (existsSync(profilePath)) {
381
521
  } else {
382
522
  const envLines = [
383
523
  '# Atoll (added by @atollhq/skill-codex)',
384
- `export ATOLL_API_KEY="${args.key}"`,
385
- `export ATOLL_ORG_ID="${args.org}"`,
386
- args.project ? `export ATOLL_PROJECT="${args.project}"` : null,
387
- args.team ? `export ATOLL_TEAM="${args.team}"` : null,
388
- args.baseUrl ? `export ATOLL_BASE_URL="${args.baseUrl}"` : null,
389
- 'export ATOLL_ENV_MODE="1"',
524
+ `export ATOLL_API_KEY=${shellQuote(args.key)}`,
525
+ `export ATOLL_ORG_ID=${shellQuote(args.org)}`,
526
+ args.project ? `export ATOLL_PROJECT=${shellQuote(args.project)}` : null,
527
+ args.team ? `export ATOLL_TEAM=${shellQuote(args.team)}` : null,
528
+ args.baseUrl ? `export ATOLL_BASE_URL=${shellQuote(args.baseUrl)}` : null,
529
+ `export ATOLL_ENV_MODE=${shellQuote('1')}`,
390
530
  ].filter(Boolean)
391
531
  const envBlock = `\n${envLines.join('\n')}\n`
392
- writeFileSync(profilePath, envBlock)
532
+ writePrivateFile(profilePath, envBlock)
393
533
  console.log(`Created ${profilePath} with Atoll env vars`)
394
534
  }
395
535
 
396
536
  console.log(`\nDone! Restart your shell, then Codex will have access to the Atoll API.`)
537
+ releaseInstallerLock()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atollhq/skill-codex",
3
- "version": "0.4.16",
3
+ "version": "0.4.19",
4
4
  "description": "Install the Atoll project management integration for Codex CLI",
5
5
  "bin": {
6
6
  "skill-codex": "bin/install.mjs"
@@ -21,6 +21,9 @@
21
21
  "url": "git+https://github.com/atollhq/atoll.git",
22
22
  "directory": "packages/skill-codex"
23
23
  },
24
+ "dependencies": {
25
+ "fs-ext-extra-prebuilt": "2.2.9"
26
+ },
24
27
  "license": "MIT",
25
28
  "type": "module"
26
29
  }
package/skill/SKILL.md CHANGED
@@ -20,7 +20,7 @@ Goals (directional objectives with deadlines)
20
20
 
21
21
  This means an agent can reason: "We're off pace on paying_customers → the Content Pipeline initiative should drive signups but has stalled issues → unblocking those is the highest-leverage action right now."
22
22
 
23
- Agents are org members with the same API, same permissions, same ability to create goals, update KPIs, propose initiatives, and execute work. The system does not distinguish between human and agent actions.
23
+ Agents are organization members using the same API and authorization model as humans. Effective organization role and project scope still govern each action; agent identity does not bypass those checks.
24
24
 
25
25
  ## Authentication
26
26
 
@@ -98,8 +98,15 @@ Profiles can store default org ID, project, team, and base URL values. For named
98
98
 
99
99
  Env vars remain supported for CI, containers, and one-off runtime usage, but persistent developer/agent machines should prefer profiles. When a profile is selected, ambient `ATOLL_*` env vars do not silently override profile context; conflicting env values fail before network calls. Pass `--profile`, use repo-local `.atoll/context.json`, or opt into env mode with `--env-mode` / `ATOLL_ENV_MODE=1`.
100
100
 
101
+ Repo-local `baseUrl` values cannot reuse a saved profile key unless that same base URL is stored in the profile. Set `ATOLL_TRUST_REPO_BASE_URL=1` only for a single process after verifying both the repository and destination host.
102
+
101
103
  `atoll issue list` and `atoll issue create` apply the selected default team unless a command-level `--team` override is passed. Issue command `--project` flags accept a project ID, slug, or exact name, including list and bulk defaults. In bulk JSON items, `project` accepts those references while `projectId` and `project_id` are canonical IDs. `--milestone` accepts a milestone ID, or an exact milestone name when a project is selected with `--project` or the active profile's default project.
102
104
 
105
+ `atoll issue list --open` excludes terminal statuses `done` and `cancelled`,
106
+ plus archived issues, while preserving every custom and other non-terminal
107
+ status. It composes with other list filters, ordering, pagination, and JSON,
108
+ and cannot be combined with `--include-archived`.
109
+
103
110
  Common commands:
104
111
 
105
112
  ```bash
@@ -112,6 +119,7 @@ atoll agent-context
112
119
 
113
120
  # List tasks
114
121
  atoll issue list --json
122
+ atoll issue list --open
115
123
  atoll issue list --status todo --priority 1 --limit 25
116
124
  atoll issue list --scope blocked --initiative initiative-uuid --order-by due_date --order-dir asc
117
125
 
@@ -123,6 +131,7 @@ atoll issue view ATOLL-42 # alias kept for humans
123
131
  atoll issue create --title "Fix login bug" --status todo --priority 1
124
132
  atoll issue create --title "Plan rollout" --project project-slug --milestone "Launch"
125
133
  atoll issue create --title "Weekly status review" --due-date 2026-07-06 --recurrence weekly
134
+ atoll issue create --title "MWF status review" --due-date 2026-07-06 --recurrence weekly --recurrence-days mon,wed,fri
126
135
  atoll issue upsert --match-title --project <project-id> --title "Fix login bug" --status todo
127
136
  atoll issue bulk-create --file ./issues.json --continue-on-error
128
137
 
@@ -150,6 +159,12 @@ atoll label list
150
159
  atoll label add ATOLL-42 bug
151
160
  atoll notification list --json
152
161
  atoll notification ack notification-uuid
162
+ atoll inbox list --json
163
+ atoll inbox view email-uuid --json
164
+ atoll inbox triage email-uuid --category support --priority 1 --status action_required
165
+ atoll inbox resolve email-uuid --note "Handled in ATOLL-123"
166
+ # Draft only; this does not send:
167
+ atoll inbox draft email-uuid --from support@atollhq.com --to user@example.com --subject "Re: Help" --body-file ./reply.txt
153
168
  atoll subtask create ATOLL-42 --title "Verify recurrence"
154
169
  atoll activity issue ATOLL-42
155
170
 
@@ -174,6 +189,7 @@ atoll feedback "The status error should list custom board statuses"
174
189
 
175
190
  # Projects & milestones
176
191
  atoll project list
192
+ atoll board-column create --project <project> --key review --label "In Review" --description "Ready for review"
177
193
  atoll project delete <project-id> --confirm DELETE
178
194
  atoll milestone list --project <project-id>
179
195
  atoll milestone upsert --project <project-id> --name "v1.0" --date 2026-06-01
@@ -205,8 +221,10 @@ CLI JSON conventions:
205
221
  - Project-scoped `atoll issue list --json` includes `project_context`; `atoll issue get/view --json` includes `status_column` plus `project_context` when available.
206
222
  - For initiative execution context via API, `GET /api/orgs/{id}/initiatives/{initiativeId}/issues?details=1` returns accessible task details from linked projects, direct issue links, and linked milestones.
207
223
  - Diagnostics and errors go to stderr.
224
+ - Machine-readable JSON preserves API strings exactly; human terminal output removes ANSI/VT, control, and bidirectional formatting characters from API-supplied strings.
208
225
  - Interactive CLI update notices also go to stderr and are suppressed for JSON/non-TTY/CI/completion flows.
209
226
  - `atoll agent-context` returns a versioned command/flag manifest, available profile context, and structured `cli.update_available` metadata.
227
+ - Weekly issue recurrence accepts unique selected weekdays with `--recurrence weekly --recurrence-days mon,wed,fri`. Read JSON exposes normalized `recurrence_days` and `recurrence_schedule`; unrelated updates preserve the schedule.
210
228
  - `atoll heartbeat --json` includes the same structured `cli` update metadata for agents, plus `attention_items`, `attention_summary`, and `recommended_action` when Atoll can propose one concrete strategy-backed next action. `atoll heartbeat --signals-only --json` preserves filtered `signals`, `attention_items`, `attention_summary`, and `recommended_action` for short polling. Handle direct attention items first, then call each handled item's `ack_endpoint`. Follow `recommended_action.usage_guidance`: prefer `suggested_write.operation` when it still matches the board, preserve KPI/initiative/initiative_target/why-now/expected-impact/first-step/success-criteria evidence, and avoid copying deferred busywork into issue or comment payloads. If a `start_work` recommendation uses `issue.update` with a body, update the issue status and preserve that body as an issue comment; `PATCH /issues/{issueId}` accepts `comment_body` for this same-request progress note.
211
229
  - `atoll plan validate/apply` consumes `schemaVersion: "atoll.plan.v1"` files with `milestones`, `issues`, `dependencies`, `initiativeLinks`, and `milestoneLinks`; local `key` values can be referenced by `milestoneKey`, `issueKey`, `dependsOn`, `blockedBy`, or `blocks`.
212
230
 
@@ -214,7 +232,7 @@ CLI JSON conventions:
214
232
 
215
233
  When a human asks you to help automate a KPI from a third-party API, use this Atoll skill. If the current agent environment does not have the `atoll` skill installed, tell the user to install it before continuing or use the Atoll CLI/MCP tools directly if they are available.
216
234
 
217
- Agents may create draft syncs and validate proposed configs only after a human admin has allowlisted the exact destination host in Atoll. Human admins must create or review the draft in Settings > Integrations > KPI syncs, edit supported request/extraction fields and secrets through structured UI, dry-run, publish, disable, or run-now with snapshot writing.
235
+ Organization-wide non-guest agents may create draft syncs and validate proposed configs for KPIs they can read, but only after a human admin has allowlisted the exact destination host in Atoll. Guest and project-scoped agents cannot use the KPI or nested sync routes. Human admins must create or review the draft in Settings > Integrations > KPI syncs, edit supported request/extraction fields and secrets through structured UI, dry-run, publish, disable, or run-now with snapshot writing.
218
236
 
219
237
  ```bash
220
238
  atoll kpi sync validate <kpi-id> \
@@ -240,7 +258,11 @@ npm install -g @atollhq/mcp-server
240
258
  PORT=8787 atoll-mcp
241
259
  ```
242
260
 
243
- Remote MCP clients call `POST /mcp` with Streamable HTTP and should send `Authorization: Bearer sk_atoll_...` per request. Single-tenant deployments can set `ATOLL_API_KEY` and `ATOLL_ORG_ID` as environment variables.
261
+ HTTP mode binds to `127.0.0.1` by default. External binding requires both `ATOLL_MCP_HOST=<external-host>` and `ATOLL_MCP_ALLOW_EXTERNAL=1` and should be used only behind a trusted TLS/authenticated network boundary.
262
+
263
+ Remote MCP clients call `POST /mcp` with Streamable HTTP and must send `Authorization: Bearer sk_atoll_...` per request. HTTP requests never fall back to a process-level `ATOLL_API_KEY`; that fallback is available only in explicit `--stdio` mode. HTTP deployments may set `ATOLL_ORG_ID` and `ATOLL_BASE_URL` as defaults.
264
+
265
+ The server validates each HTTP bearer token through `/api/auth/me` before MCP dispatch and rejects request bodies over 1 MiB, including chunked requests.
244
266
 
245
267
  The MCP server mirrors core CLI workflows with tools such as `atoll_get_heartbeat`, issue/project/goal/KPI/initiative/milestone tools, dependency tools, webhook tools, `atoll_send_feedback`, and `atoll_api_request` for advanced endpoints. `atoll_add_comment` accepts structured mentions, `reply_to_comment_id`, and explicit agent `source_metadata`; it does not infer harness thread IDs. `atoll_update_issue` accepts `comment_body` for durable progress comments.
246
268
 
@@ -260,7 +282,7 @@ atoll issue list --json --limit 10
260
282
 
261
283
  If the user is setting up Atoll in another AI tool, give them a copyable prompt. Keep secrets out of chat: tell the user to run auth commands locally and never ask them to paste `sk_atoll_...` keys into a model conversation unless they explicitly choose that risk.
262
284
 
263
- If the user is in Atoll's first-run setup wizard, the key may be setup-scoped. In that mode, inspect the repo or interview the user, then create or revise the setup proposal only. Do not try to create projects, goals, KPIs, initiatives, or issues directly, and do not approve/apply the proposal. The human reviews the editable proposal in Atoll and approves it there.
285
+ If the user is in Atoll's first-run setup wizard, the key may be setup-scoped. In that mode, inspect the repo or interview the user, then create or revise the setup proposal only. Do not try to create projects, goals, KPIs, initiatives, or issues directly, and do not approve/apply the proposal. The human reviews the editable proposal in Atoll and approves it there. Treat the setup key as temporary: it expires after 24 hours and Atoll revokes it when setup is applied, skipped, or failed. Continued use requires a separately minted ordinary key.
264
286
 
265
287
  ### Prompt: Create the First Board
266
288
 
@@ -350,7 +372,9 @@ The primary pattern for autonomous agents. Prefer `atoll heartbeat --json` when
350
372
  - **Project context**: relevant board columns, including optional descriptions that explain stage criteria for agents. Project-scoped guests receive every explicitly accessible board while idle; personal agents retain relevant inherited-project context.
351
373
  - **Signals** sorted by severity — the agent's prioritized to-do list
352
374
  - **Attention items**: direct current-member notifications such as mentions, assignments, assignee comments, and creator-visible status changes, with an `ack_endpoint` to call after handling
353
- - **Recommended action**: one deterministic strategy-backed next action when Atoll has enough evidence (`create_work`, `start_work`, `escalate_blocker`, or `refresh_metric`), including why-now, expected impact, first step, success criteria, quality warnings, and any suggested write.
375
+ - **Recommended action**: one deterministic strategy-backed next action when Atoll has enough evidence (`create_work`, `start_work`, `escalate_blocker`, `refresh_metric`, or `investigate`), including why-now, expected impact, first step, success criteria, quality warnings, and any suggested write. An investigation can use `suggested_write.operation: "none"` when heartbeat lacks enough detail for a safe write.
376
+
377
+ Recommendation ordering keeps blockers and urgent initiative targets first, followed by executable work for off-pace KPIs and in-progress work linked to stale KPIs. Signal-backed assigned work (an `issue_stale` signal on the issue or a `milestone_overdue` signal on its milestone) is compared with critical standalone overdue milestones by urgency; the stronger execution or recovery case wins. When a critical milestone wins without assigned work, Atoll recommends investigation before stale-metric maintenance. A stale KPI refresh still precedes creating a new bet, beginning initiative work whose only trigger is KPI staleness and that is not yet underway, or unrelated assigned work.
354
378
 
355
379
  Heartbeat is org-scoped, but project-bound payload details are filtered by the caller's project access. Owners/admins receive full org context; members/guests only receive project-bound strategy, work health, assigned work, milestone signals, and board context for accessible projects. Project-scoped guests receive every explicitly accessible board while idle; personal agents retain relevant inherited-project context. Non-guest members can also see unprojected org-level strategy. Shared initiatives can appear with counts and signals based only on accessible work.
356
380
 
@@ -411,13 +435,33 @@ atoll kpi snapshot add paying_customers --value 42 --initiative "Content pipelin
411
435
  atoll kpi snapshot list paying_customers --include-attribution --json
412
436
  ```
413
437
 
414
- Project-scoped agent profiles apply their default project to `atoll initiative list` and `atoll initiative create`. Use `--project <id-or-slug>` to override that project, or `--org-wide` to intentionally suppress the default project. API callers can pass `project_id` or `projectId` on create, and `?project_id=...` on list; guest/project-scoped callers must use a project they can access, and create requires edit/admin project access.
438
+ Project-scoped agent profiles apply their default project to `atoll initiative list` and `atoll initiative create`. Use `--project <id-or-slug>` to override that project, or `--org-wide` to intentionally suppress the default project. API callers can pass `project_id` or `projectId` on create, and `?project_id=...` on list; guest/project-scoped callers must use a project they can access, and create requires edit/admin project access. Projectless organization-wide initiative creation requires an organization owner/admin.
439
+
440
+ Project-linked initiative reads require access to at least one linked project.
441
+ The authoritative set includes explicit project links and projects inferred
442
+ from direct issue/milestone links. Updating an initiative or mutating its issue,
443
+ milestone, or target links requires edit/admin access to every linked project;
444
+ a requested issue or milestone project must already be linked when it is
445
+ project-bound. Eligible non-guests may link and unlink writable projectless
446
+ issues; projectless milestones are unsupported. KPI-impact reads omit
447
+ unreadable KPIs and KPI-impact writes require owner/admin Strategy access.
448
+ Projectless initiative writes require an organization owner/admin.
449
+ Treat `404` as concealed absence or unreadable scope and `403` as insufficient
450
+ write access to a readable initiative.
451
+
452
+ KPIs are organization-wide Strategy resources. Owners/admins may read and
453
+ write; other non-guest organization members may read values, snapshots, and
454
+ redacted per-KPI sync metadata but cannot create, update, delete, or record
455
+ snapshots. Guest/project-scoped agents receive `403` for the collection and
456
+ concealed `404` responses for direct KPI, snapshot, and per-KPI sync
457
+ read/draft routes. Verify the active profile's organization-wide role before
458
+ running KPI commands.
415
459
 
416
460
  Every KPI snapshot can be attributed to an initiative or issue, building a record of *what actually moved the numbers*. Keep KPI-to-initiative impact links separate from snapshot attribution: an initiative link means the initiative is expected to move the KPI, while snapshot attribution records the source of one measurement. Heartbeat reports one canonical status per KPI and can explain a KPI with `atoll heartbeat --explain-kpi <kpi> --json`.
417
461
 
418
462
  ### Audit and improve the strategy
419
463
 
420
- Use the audit to review the whole strategy chain at a high level and fix structural problems — the common one being initiatives created without a goal.
464
+ Use the audit to review the strategy chain visible to the caller at a high level and fix structural problems — the common one being initiatives created without a goal.
421
465
 
422
466
  ```bash
423
467
  atoll strategy audit # human-readable, grouped by severity
@@ -426,6 +470,12 @@ atoll strategy audit --json # findings[] for programmatic remediation
426
470
 
427
471
  `GET /api/orgs/{id}/strategy/audit` returns `findings[]` (each with a `type`, `severity`, the relevant entity id, and a concrete `suggested_fix`) plus `summary` counts. It diagnoses; you remediate with the normal write endpoints. Typical loop:
428
472
 
473
+ The audit follows the caller's project access. Owners/admins receive
474
+ organization-wide execution evidence. Other non-guests receive project-bound
475
+ issues, milestones, target links, and target findings only for readable
476
+ projects. A restricted caller with no readable projects receives no issue or
477
+ target execution evidence. Guests cannot run the audit.
478
+
429
479
  1. `atoll strategy audit --json` to get findings.
430
480
  2. For each finding, apply its `suggested_fix`, e.g.:
431
481
  - `initiative_orphaned` → `atoll initiative update "<initiative>" --goal "<goal>"` (or `PATCH .../initiatives/{id} { goal_id }`)
@@ -458,9 +508,11 @@ Config sessions and unused manual connect tokens expire after 10 minutes. Sessio
458
508
  Webhook creation returns a raw `whsec_...` secret once. Delivery requests include:
459
509
 
460
510
  - `X-Atoll-Signature`: `sha256=` plus an HMAC-SHA256 over the raw body, keyed by the SHA-256 hex digest of the raw secret.
511
+ - `X-Atoll-Signature-Version`: the primary signing-key version.
512
+ - `X-Atoll-Signatures`: versioned signatures during a bounded key-overlap window.
461
513
  - `X-Atoll-Delivery-Id`: stable delivery id for receiver-side deduplication.
462
514
 
463
- Delivery rows expose `delivery_id`, `status`, and `next_retry_at`. Network failures and 5xx responses retry quickly in-process, then persist `status: retry_pending` with `next_retry_at`; an internal drain retries due deliveries every 15 minutes.
515
+ Webhook administration is owner/admin only. Lists return an origin-only `destination_display`; paths, queries, and signing material are never returned. Payload schema version `2` is allowlisted and omits descriptions, comment bodies, and raw change values. Delivery rows expose safe `delivery_id`, `status`, `status_code`, `error_code`, and retry timing, but not payloads, receiver response bodies, or raw errors. Network failures and 5xx responses retry quickly in-process, then persist `status: retry_pending` with `next_retry_at`; an internal drain retries due deliveries every 15 minutes.
464
516
 
465
517
  ### Billing and plan limits
466
518
 
@@ -486,6 +538,7 @@ Full endpoint tables and field schemas:
486
538
  | Initiatives | POST `.../initiatives` (`project_id`/`projectId` optional; required for guests) | GET `.../initiatives` (`project_id` optional; required for guests) | PATCH `.../initiatives/{id}` | DELETE `.../initiatives/{id}` |
487
539
  | Milestones | POST `.../milestones` | GET `.../milestones` | PATCH `.../milestones/{id}` | DELETE `.../milestones/{id}` |
488
540
  | Comments | POST `.../comments` with `{ body, mentions?, reply_to_comment_id?, source_metadata? }` | GET `.../comments` or `.../comments/{id}` | PATCH `.../comments/{id}` | DELETE `.../comments/{id}` |
541
+ | Attachments | POST `.../attachments` | GET `.../attachments` or `.../attachments/{id}/content` | — | DELETE `.../attachments/{id}` |
489
542
  | Subtasks | POST `.../subtasks` | GET `.../subtasks` | PATCH `.../subtasks/{id}` | DELETE `.../subtasks/{id}` |
490
543
 
491
544
  Initiative create accepts `title` or legacy `name`, plus camelCase aliases `goalId`, `ownerId`, and `targetDate`.
@@ -494,6 +547,29 @@ All endpoints are under `/api/orgs/{orgId}/...`.
494
547
 
495
548
  Issue comments inherit issue project permissions: listing comments requires access to the issue's project, comment writes (add, edit, delete) require write access to that project, edit/delete still require comment authorship, and guests cannot access comments on unprojected issues.
496
549
 
550
+ Project-bound milestone, status-update, board-column, issue-activity, and PR-link
551
+ reads require effective project access. Milestone create/update, status-update
552
+ create, board-column mutations, and project-bound PR-link create require `edit`
553
+ or `admin`; eligible non-guests may read issue activity and read or attach PR
554
+ links for projectless issues. Milestone delete remains organization
555
+ owner/admin-only. Issue activity is read-only. Organization activity and
556
+ analytics are limited to the caller's accessible projects, with eligible
557
+ non-guests also receiving projectless data; project-health contains accessible
558
+ projects only. Do not treat org membership alone as project authorization.
559
+
560
+ Issue templates follow the same effective-project boundary: project-template
561
+ reads require project access and writes require `edit`/`admin`.
562
+ Organization-wide templates are readable by non-guests and manageable only by
563
+ organization owners/admins; guest/project-scoped agents never receive them.
564
+ Avatar mutations require both caller and target to belong to the organization
565
+ in the request path. Avatar pointer changes use compare-and-set semantics;
566
+ concurrent changes return `409`, and successful mutations with durable Storage
567
+ cleanup still queued return `202` with `cleanup_pending: true`. A conflict can
568
+ also include `cleanup_pending: true` when cleanup of a staged or retired object
569
+ remains queued. An authenticated 15-minute worker drains due jobs
570
+ independently, with avatar requests providing an additional opportunistic
571
+ sweep.
572
+
497
573
  Comment bodies accept Markdown/plain text or existing rich-text HTML. Atoll stores and returns comment bodies as sanitized HTML. If sanitization leaves no visible text or safe media, the request returns `400` with `body is required` for direct comments or `comment_body is required` for issue updates with `comment_body`.
498
574
 
499
575
  Structured mentions are recommended for agents and integrations. Direct comment requests accept `mentions: [{ "member_id": "member-id" }]`; issue updates that create comments accept `comment_mentions: [{ "member_id": "member-id" }]`. `member_id` is the stable Atoll org member ID, not an auth user ID or display name. Markdown and HTML `atoll:member` links remain backward-compatible.
@@ -504,6 +580,12 @@ Agent-authored direct comments may include explicit `source_metadata` with `harn
504
580
 
505
581
  Responses that create comments include `mentions: { requested, created, skipped }`. Each `skipped[]` entry includes `member_id` and `reason`; reasons are `invalid_member_id`, `not_found`, `self_mention`, `no_project_access`, `guest_unprojected_issue`, `unsupported_member_type`, and `mentions_muted`.
506
582
 
583
+ Issue attachments inherit the same issue permissions. Project-scoped reads require project access; upload and delete require `edit` or `admin`. Guests cannot access attachments on unprojected issues, while non-guests follow the org-level issue rule.
584
+
585
+ Attachment metadata contains `id`, `filename`, `file_size`, `mime_type`, `uploaded_by`, `created_at`, and a relative `url`. Resolve `url` against the Atoll base URL and resend the bearer credential or browser session. It is an authenticated API path, not a public or transferable storage URL; clients that consumed the former absolute public URLs must migrate.
586
+
587
+ Uploads use multipart field `file`, must be non-empty, and are limited to 10 MiB (`413` when exceeded). Declared images must be signature-valid PNG, JPEG, GIF, or WebP; SVG and other declared image types are rejected. Other files are accepted but forced to download as `application/octet-stream`.
588
+
507
589
  † `DELETE /issues/{id}` requires `owner` or `admin` role — any caller without that role (including member-role agents) gets `403`. If you just need to remove a task, use `POST /api/orgs/{orgId}/issues/{issueId}/archive` (soft delete, no role gate); reverse with `DELETE` on the same path (unarchive). In the CLI, prefer `atoll issue archive <id>`. Permanent `atoll issue delete <id>` requires `--force` and supports `--dry-run`.
508
590
 
509
591
  ### Quick enum reference
@@ -555,6 +637,6 @@ atoll feedback resend fb_123
555
637
  - Request bodies accept camelCase; responses use snake_case
556
638
  - Descriptions support Markdown; comment bodies accept Markdown/plain text or rich-text HTML and are stored as sanitized HTML
557
639
  - All timestamps are ISO 8601 UTC
558
- - Board statuses are customizable per project -- query `/board-columns` for available values and optional column descriptions
640
+ - Board statuses are customizable per project -- query `/board-columns` for available values and optional column descriptions; append a column with `atoll board-column create`, using `--description` or `--description-file` for agent guidance
559
641
  - API changes appear in real-time on the web board
560
642
  - List endpoints support `limit` (default 25, max 100), `offset` pagination, and optional `shape=envelope` / `response_shape=cli` for `{ resource, items, total, limit, offset, nextOffset, truncated, hint }`
@@ -2,7 +2,12 @@
2
2
 
3
3
  Base URL: `https://atollhq.com`
4
4
 
5
- All endpoints require `Authorization: Bearer sk_atoll_...` header.
5
+ Endpoints require `Authorization: Bearer sk_atoll_...` unless an endpoint
6
+ explicitly documents a different server-to-server credential.
7
+
8
+ Directly requested unreadable project-bound resources return `404` without
9
+ disclosing whether they exist. A readable project or resource with insufficient
10
+ write access returns `403`; collection reads may omit unreadable linked rows.
6
11
 
7
12
  ## Table of Contents
8
13
 
@@ -65,20 +70,25 @@ do not require key rotation.
65
70
  | POST | `/api/orgs` | Create an org (`{ name }`) |
66
71
  | GET | `/api/orgs/{id}` | Get org details |
67
72
  | PATCH | `/api/orgs/{id}` | Update org |
68
- | DELETE | `/api/orgs/{id}` | Delete org |
73
+ | DELETE | `/api/orgs/{id}` | Delete org (owner only; durably queues attachment object cleanup) |
69
74
 
70
75
  ## Projects
71
76
 
72
77
  | Method | Endpoint | Description |
73
78
  |--------|----------|-------------|
74
79
  | GET | `/api/orgs/{id}/projects` | List projects (visibility-filtered) |
75
- | POST | `/api/orgs/{id}/projects` | Create project (`{ name, description?, visibility?, color?, icon?, github_repo? }`) |
80
+ | POST | `/api/orgs/{id}/projects` | Create project and default views atomically (`{ name, description?, visibility?, color?, icon?, github_repo? }`, owner/admin) |
76
81
  | GET | `/api/orgs/{id}/projects/{projectId}` | Get project with issues |
77
82
  | PATCH | `/api/orgs/{id}/projects/{projectId}` | Update project (`{ name?, description?, status?, visibility?, color?, icon? }`) |
78
83
  | DELETE | `/api/orgs/{id}/projects/{projectId}` | Permanently delete project and all tasks in it (owner/admin; body must include `{ "confirmation": "DELETE" }`) |
79
84
 
80
85
  Guest users only see projects they are assigned to.
81
86
 
87
+ A successful project create also creates Backlog, Todo, In Progress, and Done
88
+ columns; a Default board view containing those columns; and All Tasks, My
89
+ Tasks, and Recently Updated custom views. If any default cannot be created, the
90
+ transaction rolls back and no partial project remains.
91
+
82
92
  ## Project Members
83
93
 
84
94
  | Method | Endpoint | Description |
@@ -125,7 +135,13 @@ Plan limits are enforced when creating projects, human members, agents/integrati
125
135
  | POST | `/api/orgs/{id}/issues/{issueId}/initiatives` | Link task to initiative (`{ initiative_id }`) |
126
136
  | DELETE | `/api/orgs/{id}/issues/{issueId}/initiatives/{initiativeId}` | Unlink task from initiative |
127
137
 
128
- Issue-centric initiative links follow task project permissions: reading links requires access to the task's project; linking and unlinking require edit/admin access to that project. Guest callers may link only to initiatives already linked to the same accessible project.
138
+ Issue-centric initiative links follow both resource boundaries. The collection
139
+ read requires access to the task, omits linked initiatives the caller cannot
140
+ read, and returns `200`. For project-bound tasks, linking and unlinking require
141
+ edit/admin access to the task project, which must already be linked to the
142
+ initiative. Eligible non-guests may link or unlink writable projectless tasks.
143
+ Every mutation also requires edit/admin access to every project linked to the
144
+ initiative. Directly requested unreadable mutations are concealed as `404`.
129
145
 
130
146
  **List filters** (query params):
131
147
  - `status` -- `backlog`, `todo`, `in_progress`, `done`, `cancelled`
@@ -133,6 +149,7 @@ Issue-centric initiative links follow task project permissions: reading links re
133
149
  - `projectId`, `assigneeId`, `teamId`, `milestoneId`
134
150
  - `q` -- full issue lists search title and description (case-insensitive)
135
151
  - Compact views (`view=board` or `view=list`) also support `assignee` (member ID or `unassigned`, including multi-assignee links), `initiativeId`, `scope` (`mine` or `blocked`), and `q` over title plus issue number
152
+ - `open` -- `true` excludes terminal statuses `done` and `cancelled`, plus archived tasks; custom and other non-terminal statuses remain included. Takes precedence over `includeArchived`.
136
153
  - `includeArchived` -- `true` to include archived tasks
137
154
  - `orderBy` -- `created_at` (default), `updated_at`, `priority`, `due_date`, `title`, `status`
138
155
  - `orderDir` -- `asc` or `desc` (default)
@@ -140,7 +157,7 @@ Issue-centric initiative links follow task project permissions: reading links re
140
157
  - `offset` -- pagination offset
141
158
  - `shape=envelope` or `response_shape=cli` -- opt into CLI-compatible list responses: `{ resource, items, total, limit, offset, nextOffset, truncated, hint }`
142
159
 
143
- **GET task detail** returns enriched data: `milestone`, `creator`, `assignee`, `assignees`, `sub_tasks`, `issue_labels`, and `isBlocked`.
160
+ **GET task detail** returns enriched data: `milestone`, `creator`, `assignee`, `assignees`, `sub_tasks`, `issue_labels`, and `isBlocked`. Recurring tasks also return normalized `recurrence_days` and `recurrence_schedule`. Create, update, and bulk-create accept `recurrenceDays` only with `recurrenceType: "weekly"`; values must be unique weekdays from `mon` through `sun`.
144
161
 
145
162
  ## Dependencies
146
163
 
@@ -194,6 +211,8 @@ Responses that create comments include `mentions: { requested, created, skipped
194
211
 
195
212
  Roles: `owner`, `admin`, `member`, `guest`.
196
213
 
214
+ Member `PATCH` and `DELETE` can return `409` when the actor's or target member's authorization changes before the atomic mutation commits. Refetch the member and current permissions before retrying, and retry only if the action remains authorized.
215
+
197
216
  ## Milestones
198
217
 
199
218
  | Method | Endpoint | Description |
@@ -204,6 +223,10 @@ Roles: `owner`, `admin`, `member`, `guest`.
204
223
  | PATCH | `/api/orgs/{id}/milestones/{milestoneId}` | Update milestone |
205
224
  | DELETE | `/api/orgs/{id}/milestones/{milestoneId}` | Delete milestone |
206
225
 
226
+ Project-bound reads require effective project access. Create and update require
227
+ `edit` or `admin` access. Unreadable milestones are concealed as `404`.
228
+ Milestone deletion remains organization owner/admin-only.
229
+
207
230
  ## Goals
208
231
 
209
232
  | Method | Endpoint | Description |
@@ -218,20 +241,20 @@ Roles: `owner`, `admin`, `member`, `guest`.
218
241
 
219
242
  | Method | Endpoint | Description |
220
243
  |--------|----------|-------------|
221
- | GET | `/api/orgs/{id}/kpis` | List KPIs (optional `?goal_id=...`) |
222
- | POST | `/api/orgs/{id}/kpis` | Create KPI |
223
- | GET | `/api/orgs/{id}/kpis/{kpiId}` | Get KPI |
224
- | PATCH | `/api/orgs/{id}/kpis/{kpiId}` | Update KPI |
244
+ | GET | `/api/orgs/{id}/kpis` | List KPIs (optional `?goal_id=...`); non-guest Strategy read access required |
245
+ | POST | `/api/orgs/{id}/kpis` | Create KPI; owner/admin Strategy write access required |
246
+ | GET | `/api/orgs/{id}/kpis/{kpiId}` | Get KPI; non-guest Strategy read access required |
247
+ | PATCH | `/api/orgs/{id}/kpis/{kpiId}` | Update KPI; owner/admin Strategy write access required |
225
248
  | DELETE | `/api/orgs/{id}/kpis/{kpiId}` | Delete KPI (admin/owner only) |
226
- | GET | `/api/orgs/{id}/kpis/{kpiId}/snapshots` | List snapshots (optional `?limit=50`) |
227
- | POST | `/api/orgs/{id}/kpis/{kpiId}/snapshots` | Record a snapshot |
249
+ | GET | `/api/orgs/{id}/kpis/{kpiId}/snapshots` | List snapshots (optional `?limit=50`); non-guest Strategy read access required |
250
+ | POST | `/api/orgs/{id}/kpis/{kpiId}/snapshots` | Record a snapshot; owner/admin Strategy write access required |
228
251
  | GET | `/api/orgs/{id}/kpi-http-sync-policy` | List exact-host KPI HTTP sync allowlist policy |
229
252
  | POST | `/api/orgs/{id}/kpi-http-sync-policy` | Add an allowed exact host (human admin only) |
230
253
  | GET | `/api/orgs/{id}/kpi-http-syncs` | List org-wide KPI HTTP sync review rows for Settings; admins get config/secret metadata, members get redacted status rows |
231
- | GET | `/api/orgs/{id}/kpis/{kpiId}/http-syncs` | List KPI HTTP syncs |
232
- | POST | `/api/orgs/{id}/kpis/{kpiId}/http-syncs` | Create a draft KPI HTTP sync |
233
- | PUT | `/api/orgs/{id}/kpis/{kpiId}/http-syncs` | Validate a proposed KPI HTTP sync config without storing or running it |
234
- | GET | `/api/orgs/{id}/kpis/{kpiId}/http-syncs/{syncId}` | Get a KPI HTTP sync |
254
+ | GET | `/api/orgs/{id}/kpis/{kpiId}/http-syncs` | List KPI HTTP syncs; readable KPI required |
255
+ | POST | `/api/orgs/{id}/kpis/{kpiId}/http-syncs` | Create a draft KPI HTTP sync; readable KPI required |
256
+ | PUT | `/api/orgs/{id}/kpis/{kpiId}/http-syncs` | Validate a proposed KPI HTTP sync config without storing or running it; readable KPI required |
257
+ | GET | `/api/orgs/{id}/kpis/{kpiId}/http-syncs/{syncId}` | Get a KPI HTTP sync; readable KPI required |
235
258
  | PATCH | `/api/orgs/{id}/kpis/{kpiId}/http-syncs/{syncId}` | Update a KPI HTTP sync draft (human admin only) |
236
259
  | POST | `/api/orgs/{id}/kpis/{kpiId}/http-syncs/{syncId}/validate` | Validate a stored sync (human admin only) |
237
260
  | GET | `/api/orgs/{id}/kpis/{kpiId}/http-syncs/{syncId}/secrets` | List sanitized secret metadata (human admin only) |
@@ -254,15 +277,28 @@ Roles: `owner`, `admin`, `member`, `guest`.
254
277
  | POST | `/api/orgs/{id}/initiatives/{initiativeId}/projects` | Add project to initiative |
255
278
  | DELETE | `/api/orgs/{id}/initiatives/{initiativeId}/projects` | Remove project from initiative |
256
279
 
257
- Create accepts `title` or legacy `name`, plus camelCase aliases `goalId`, `ownerId`, and `targetDate`.
280
+ Create accepts `title` or legacy `name`, plus camelCase aliases `goalId`,
281
+ `ownerId`, and `targetDate`. Projectless creation requires an organization
282
+ owner/admin.
283
+
284
+ Project-linked initiative collections, project-bound enrichment, and
285
+ issue/milestone/target links are filtered to projects the caller can read.
286
+ Authoritative scope includes explicit project links plus projects inferred from
287
+ direct issue and milestone links. A read is allowed when at least one linked
288
+ project is readable, but write operations require edit/admin access to every
289
+ project linked to the initiative. Projectless initiatives are readable by
290
+ non-guest organization members and writable only by owners/admins. KPI-impact
291
+ reads omit unreadable KPIs; KPI-impact writes additionally require owner/admin
292
+ Strategy access. Unreadable directly requested resources return `404`; readable
293
+ resources without sufficient write access return `403`.
258
294
 
259
295
  ## Initiative Links
260
296
 
261
297
  | Method | Endpoint | Description |
262
298
  |--------|----------|-------------|
263
- | GET | `.../initiatives/{id}/kpi-impacts` | List KPI impact links |
264
- | POST | `.../initiatives/{id}/kpi-impacts` | Add (`{ kpi_id, expected_impact? }`) |
265
- | DELETE | `.../initiatives/{id}/kpi-impacts/{impactId}` | Remove link |
299
+ | GET | `.../initiatives/{id}/kpi-impacts` | List KPI impact links whose KPIs are readable |
300
+ | POST | `.../initiatives/{id}/kpi-impacts` | Add (`{ kpi_id, expected_impact? }`); owner/admin KPI Strategy write access required |
301
+ | DELETE | `.../initiatives/{id}/kpi-impacts/{impactId}` | Remove link; owner/admin KPI Strategy write access required |
266
302
  | GET | `.../initiatives/{id}/issues` | List linked issue links; add `?details=1` for accessible task details from linked projects, direct issue links, and linked milestones |
267
303
  | POST | `.../initiatives/{id}/issues` | Link issue (`{ issue_id }`) |
268
304
  | DELETE | `.../initiatives/{id}/issues/{issueId}` | Unlink issue |
@@ -274,12 +310,12 @@ Create accepts `title` or legacy `name`, plus camelCase aliases `goalId`, `owner
274
310
  | GET | `.../initiatives/{id}/targets/{targetId}` | Get target |
275
311
  | PATCH | `.../initiatives/{id}/targets/{targetId}` | Update target |
276
312
  | DELETE | `.../initiatives/{id}/targets/{targetId}` | Delete target |
277
- | GET | `.../initiatives/{id}/targets/{targetId}/issues` | List target issue links |
278
- | POST | `.../initiatives/{id}/targets/{targetId}/issues` | Link issue to target (`{ issue_id }`) |
279
- | DELETE | `.../initiatives/{id}/targets/{targetId}/issues/{issueId}` | Unlink issue from target |
280
- | GET | `.../initiatives/{id}/targets/{targetId}/milestones` | List target milestone links |
281
- | POST | `.../initiatives/{id}/targets/{targetId}/milestones` | Link milestone to target (`{ milestone_id }`) |
282
- | DELETE | `.../initiatives/{id}/targets/{targetId}/milestones/{milestoneId}` | Unlink milestone from target |
313
+ | GET | `.../initiatives/{id}/targets/{targetId}/issues` | List readable target issue links, including readable projectless issues for non-guests |
314
+ | POST | `.../initiatives/{id}/targets/{targetId}/issues` | Link issue to target (`{ issue_id }`); a project-bound issue's project must already be linked to the initiative, while eligible non-guests may link writable projectless issues |
315
+ | DELETE | `.../initiatives/{id}/targets/{targetId}/issues/{issueId}` | Unlink issue from target; a project-bound issue's project must already be linked to the initiative, while eligible non-guests may unlink writable projectless issues |
316
+ | GET | `.../initiatives/{id}/targets/{targetId}/milestones` | List readable project-bound target milestone links; projectless milestones are unsupported |
317
+ | POST | `.../initiatives/{id}/targets/{targetId}/milestones` | Link milestone to target (`{ milestone_id }`); its project must already be linked to the initiative, and projectless milestones are unsupported |
318
+ | DELETE | `.../initiatives/{id}/targets/{targetId}/milestones/{milestoneId}` | Unlink milestone from target; its project must already be linked to the initiative, and projectless milestones are unsupported |
283
319
 
284
320
  Targets are initiative-level commitments. Use `mode: "progress"` for normal output tracking and `mode: "gate"` for launch blockers or prerequisites where KPI pace language would be misleading. Targets do not create KPI snapshots.
285
321
 
@@ -289,7 +325,7 @@ Targets are initiative-level commitments. Use `mode: "progress"` for normal outp
289
325
  |--------|----------|-------------|
290
326
  | GET | `/api/orgs/{id}/strategy/audit` | Audit the strategy chain for structural gaps + health issues, each with a suggested fix |
291
327
 
292
- Returns findings only (not the full graph). Use it for a high-level review — orphaned initiatives/KPIs (no goal), goals with no KPI or no initiative, dangling initiative execution links, KPIs missing targets/stale/off-pace, initiatives missing impact/execution or stalled, blocked/overdue work — then remediate with the goal/KPI/initiative write endpoints above. Forbidden for guests. CLI: `atoll strategy audit [--severity critical|warning|info] [--json]`.
328
+ Returns findings only (not the full graph). Use it for a high-level review — orphaned initiatives/KPIs (no goal), goals with no KPI or no initiative, dangling initiative execution links, KPIs missing targets/stale/off-pace, initiatives missing impact/execution or stalled, blocked/overdue work — then remediate with the goal/KPI/initiative write endpoints above. Owners/admins receive organization-wide execution evidence. Other non-guests receive project-bound issues, milestones, target links, and target findings only for readable projects. A restricted member with no readable projects receives no issue or target execution evidence. Forbidden for guests. CLI: `atoll strategy audit [--severity critical|warning|info] [--json]`.
293
329
 
294
330
  ## Heartbeat
295
331
 
@@ -299,6 +335,8 @@ Returns findings only (not the full graph). Use it for a high-level review — o
299
335
 
300
336
  Returns computed briefing with goal status, KPI pace/trend, initiative progress, assigned work, direct `attention_items`, `attention_summary`, signals, and a deterministic `recommended_action` when Atoll can propose one concrete strategy-backed next action. The endpoint is org-scoped, but project-bound payload details are filtered by the caller's project access. Project-scoped guests receive every explicitly accessible board while idle; personal agents retain relevant inherited-project context. Non-guest members can also see unprojected org-level strategy, and shared initiatives can appear with counts and signals based only on accessible work.
301
337
 
338
+ Recommendation ordering keeps blockers and urgent initiative targets first, followed by executable work for off-pace KPIs and in-progress work linked to stale KPIs. Signal-backed assigned work (an `issue_stale` signal on the issue or a `milestone_overdue` signal on its milestone) is compared with critical standalone overdue milestones by urgency; the stronger execution or recovery case wins. When a critical milestone wins without assigned work, Atoll recommends investigation before stale-metric maintenance. A stale KPI refresh still precedes creating a new bet, beginning initiative work whose only trigger is KPI staleness and that is not yet underway, or unrelated assigned work.
339
+
302
340
  Signal types: `kpi_off_pace`, `kpi_stale`, `issue_stale`, `issue_blocked`, `milestone_overdue`, `initiative_stalled`, `webhook_failing`. Severity: `info`, `warning`, `critical`.
303
341
 
304
342
  CLI equivalent:
@@ -322,6 +360,10 @@ KPI stale/off-pace signal metadata includes `linked_initiatives` and `recent_att
322
360
 
323
361
  Filters: `by_me` = your actions; `mine` = activity on issues assigned to you.
324
362
 
363
+ Organization activity is limited to accessible projects; eligible non-guests may
364
+ also receive projectless activity. Project-bound issue activity requires project
365
+ access; eligible non-guests may also read projectless issue activity.
366
+
325
367
  ## Teams
326
368
 
327
369
  | Method | Endpoint | Description |
@@ -351,11 +393,18 @@ Custom statuses per project. Each column defines a valid status value and may in
351
393
  |--------|----------|-------------|
352
394
  | GET | `/api/orgs/{id}/projects/{projectId}/board-columns` | List columns (ordered by position) |
353
395
  | GET | `/api/orgs/{id}/projects/{projectId}/board-context` | Get board milestone and initiative focus context |
354
- | POST | `/api/orgs/{id}/projects/{projectId}/board-columns` | Create column (`{ key, label, description?, color?, position? }`) |
355
- | PATCH | `/api/orgs/{id}/projects/{projectId}/board-columns/{columnId}` | Update column (`{ label?, description?, color?, position? }`) |
356
- | DELETE | `/api/orgs/{id}/projects/{projectId}/board-columns/{columnId}` | Delete column (requires `reassignTo` in body) |
396
+ | POST | `/api/orgs/{id}/projects/{projectId}/board-columns` | Append column (`{ key, label, description?, color? }`) |
397
+ | PATCH | `/api/orgs/{id}/projects/{projectId}/board-columns/{columnId}` | Update column (`{ label?, description?, color? }`) |
398
+ | DELETE | `/api/orgs/{id}/projects/{projectId}/board-columns/{columnId}` | Delete column (`?reassignTo={columnId}` is required when the source contains issues) |
357
399
  | PUT | `/api/orgs/{id}/projects/{projectId}/board-columns/reorder` | Bulk reorder (`{ columns: [{id, position}] }`) |
358
400
 
401
+ Reads require effective project access; mutations require `edit` or `admin`.
402
+ Delete-with-reassignment and reorder are atomic, the final column cannot be
403
+ deleted, reorder requires the complete current column set, and cross-project
404
+ targets, duplicate positions, and negative or non-integer positions are
405
+ rejected. Creation appends; direct `position` changes on create or patch are
406
+ rejected.
407
+
359
408
  ## Board Views
360
409
 
361
410
  | Method | Endpoint | Description |
@@ -383,14 +432,41 @@ Custom statuses per project. Each column defines a valid status value and may in
383
432
  | PATCH | `/api/orgs/{id}/templates/{templateId}` | Update template |
384
433
  | DELETE | `/api/orgs/{id}/templates/{templateId}` | Delete template |
385
434
 
435
+ Project-template reads require effective project access; create/update/delete
436
+ require `edit` or `admin`. Organization-wide templates are readable by
437
+ non-guests and manageable only by organization owners/admins. Guests and
438
+ project-scoped agents never receive organization-wide templates. Unreadable or
439
+ cross-organization IDs return `404`; readable view-only projects return `403`
440
+ for writes.
441
+
386
442
  ## Attachments
387
443
 
388
444
  | Method | Endpoint | Description |
389
445
  |--------|----------|-------------|
390
- | GET | `/api/orgs/{id}/issues/{issueId}/attachments` | List private attachments with signed URLs (`url_expires_in: 3600`) |
391
- | POST | `/api/orgs/{id}/issues/{issueId}/attachments` | Upload private file (multipart, `file` field, max 10MB) |
392
- | GET | `/api/orgs/{id}/issues/{issueId}/attachments/{attachmentId}` | Redirect authorized requests to a fresh signed URL |
393
- | DELETE | `/api/orgs/{id}/issues/{issueId}/attachments/{attachmentId}` | Delete private attachment |
446
+ | GET | `/api/orgs/{id}/issues/{issueId}/attachments` | List metadata with authenticated content URLs |
447
+ | POST | `/api/orgs/{id}/issues/{issueId}/attachments` | Upload non-empty file (multipart `file`, max 10 MiB) |
448
+ | GET | `/api/orgs/{id}/issues/{issueId}/attachments/{attachmentId}/content` | Read private content |
449
+ | DELETE | `/api/orgs/{id}/issues/{issueId}/attachments/{attachmentId}` | Delete attachment |
450
+
451
+ Attachment `url` values are stable authenticated API paths, not public storage
452
+ URLs. Resolve them against the Atoll base URL and resend the bearer/session
453
+ credential; do not expect storage fields or cache/share the URL as public.
454
+ Project-scoped reads require project access and writes require edit/admin.
455
+ Guests cannot access unprojected issue attachments; non-guests follow the
456
+ org-level issue rule. Empty files return `400` and files over 10 MiB return
457
+ `413`. PNG, JPEG, GIF, and WebP are signature-checked and served inline; other
458
+ declared images are rejected, while non-image files are forced to download.
459
+ Upload durably prepares exact reconciliation before Storage, activates it after
460
+ upload, and only then attempts the row. Unverified outcomes remain queued.
461
+ Cleanup is tombstoned under the same object lock as creation before removal.
462
+ User deletion retires surviving create work atomically; tombstone expiry makes
463
+ one final idempotent Storage removal.
464
+ Direct attachment deletion and permanent issue, project, or organization
465
+ deletion commit the attachment-row or parent cascade first and atomically queue
466
+ both transitional and private bucket paths for cleanup. A service-authenticated
467
+ worker processes bounded due jobs every 15 minutes and retries failures. Direct
468
+ deletion returns `202` with `"cleanup_pending": true` when immediate cleanup is
469
+ deferred; parent deletion returns after durable queueing.
394
470
 
395
471
  ## Profile Images
396
472
 
@@ -399,6 +475,18 @@ Custom statuses per project. Each column defines a valid status value and may in
399
475
  | POST | `/api/orgs/{id}/members/{memberId}/avatar` | Upload avatar to public `avatars` bucket (multipart, max 2MB, JPEG/PNG/WebP/GIF) |
400
476
  | DELETE | `/api/orgs/{id}/members/{memberId}/avatar` | Remove avatar |
401
477
 
478
+ Members may manage their own avatar; organization owners/admins may manage
479
+ another member only inside the same path organization. Cross-organization
480
+ caller or target IDs return `404`. Upload returns
481
+ `{ "member": { "id": "...", "avatar_url": "..." } }` with no other member
482
+ metadata. Avatar updates use compare-and-set semantics: concurrent changes
483
+ return `409`, while a successful mutation with durable Storage cleanup still
484
+ queued returns `202` and includes `"cleanup_pending": true`. A conflict body is
485
+ `{ "error": "Avatar changed concurrently" }` and may add
486
+ `"cleanup_pending": true` only for queued staged or retired object cleanup.
487
+ An authenticated 15-minute worker drains due jobs independently, while avatar
488
+ requests also sweep a small due batch. Uploads over 2MB return `413`.
489
+
402
490
  ## PR Links
403
491
 
404
492
  | Method | Endpoint | Description |
@@ -408,6 +496,11 @@ Custom statuses per project. Each column defines a valid status value and may in
408
496
 
409
497
  Attach PRs manually with a canonical GitHub pull request URL such as `https://github.com/owner/repo/pull/123`; malformed or non-PR URLs return `400`. On attach, Atoll refreshes GitHub metadata when available so title/status/head SHA reflect the PR instead of only the submitted URL. PR links can also be created or refreshed automatically via the GitHub webhook integration.
410
498
 
499
+ For project-bound issues, listing requires project access and attaching requires
500
+ `edit` or `admin` access. Eligible non-guests may list and attach links for
501
+ projectless issues. Authorization is bound to the issue's current parent before
502
+ child reads or writes and occurs before URL parsing or GitHub metadata lookup.
503
+
411
504
  ## Project Status Updates
412
505
 
413
506
  | Method | Endpoint | Description |
@@ -417,19 +510,32 @@ Attach PRs manually with a canonical GitHub pull request URL such as `https://gi
417
510
 
418
511
  Status values: `on_track`, `at_risk`, `off_track`.
419
512
 
513
+ Reads require effective project access; creation requires `edit` or `admin`.
514
+
420
515
  ## Project Health
421
516
 
422
517
  | Method | Endpoint | Description |
423
518
  |--------|----------|-------------|
424
519
  | GET | `/api/orgs/{id}/project-health` | Latest health status per project |
425
520
 
521
+ Only accessible projects are returned. Empty project scope returns empty health.
522
+
426
523
  ## Analytics
427
524
 
428
525
  | Method | Endpoint | Description |
429
526
  |--------|----------|-------------|
430
527
  | GET | `/api/orgs/{id}/analytics?from=...&to=...` | Get analytics data |
431
528
 
432
- Required: `from`, `to` (dates). Optional: `projectId`, `teamId`.
529
+ Required: `from`, `to`. Each must be either a calendar-valid `YYYY-MM-DD` date
530
+ or a timezone-qualified RFC 3339 timestamp (`Z` or an explicit UTC offset).
531
+ The ordered range may span no more than 366 days; partial dates,
532
+ timezone-less timestamps, normalized invalid dates, reversed ranges, and
533
+ longer ranges return `400`.
534
+ Optional: `projectId`, `teamId`.
535
+
536
+ All aggregates are limited to accessible projects; eligible non-guests may also
537
+ receive projectless work. An inaccessible explicit `projectId` is concealed as
538
+ `404`; empty guest scope returns empty aggregates.
433
539
 
434
540
  ## Automation Rules
435
541
 
@@ -449,14 +555,36 @@ Trigger events: `issue.created`, `issue.status_changed`, `issue.assigned`, `issu
449
555
 
450
556
  | Method | Endpoint | Description |
451
557
  |--------|----------|-------------|
452
- | GET | `/api/webhooks?orgId=...` | List webhooks |
558
+ | GET | `/api/webhooks?orgId=...` | List redacted webhooks (owner/admin) |
453
559
  | POST | `/api/webhooks?orgId=...` | Create webhook (owner/admin) |
454
560
  | DELETE | `/api/webhooks/{id}` | Delete webhook (owner/admin) |
455
- | GET | `/api/webhooks/{id}/deliveries` | List recent deliveries (last 50) |
456
- | POST | `/api/webhooks/{id}/redeliver/{deliveryId}` | Redeliver a past payload |
457
- | POST | `/api/webhooks/{id}/test` | Send ping test event |
561
+ | GET | `/api/webhooks/{id}/deliveries` | List safe delivery metadata (owner/admin, last 50) |
562
+ | POST | `/api/webhooks/{id}/redeliver/{deliveryId}` | Redeliver a past payload (owner/admin) |
563
+ | POST | `/api/webhooks/{id}/test` | Send ping test event (owner/admin) |
564
+
565
+ URL must be an HTTPS DNS hostname. IP literals, `localhost`, and `.local` hosts are rejected at creation; delivery refuses non-public DNS results and does not follow redirects. Returns webhook record plus `secret` for HMAC verification. Store the secret immediately; it is shown only once. Later lists expose only `destination_display` such as `https://example.com/…`.
566
+
567
+ Payload schema version `2` is allowlisted and omits descriptions, comment bodies, and raw change values. Delivery requests include `X-Atoll-Signature`, `X-Atoll-Signature-Version`, versioned `X-Atoll-Signatures`, and `X-Atoll-Delivery-Id`. Delivery history returns safe status, `error_code`, and retry timing only—not payloads, receiver response bodies, or raw errors. Atoll retries network failures and 5xx responses after 5s and 30s, then records `status: retry_pending` with `next_retry_at`; an internal cron drains due retries every 15 minutes.
568
+
569
+ ## Private Inbound Email Inbox
570
+
571
+ | Method | Endpoint | Description |
572
+ |--------|----------|-------------|
573
+ | POST | `/api/webhooks/resend/inbound` | Receive a signed Resend `email.received` event |
574
+ | GET | `/api/orgs/{id}/inbox` | List private inbox mail; defaults to `status=untriaged` |
575
+ | GET | `/api/orgs/{id}/inbox/{emailId}` | Read one message, attachment metadata, audit actions, and drafts |
576
+ | PATCH | `/api/orgs/{id}/inbox/{emailId}` | Triage, classify, resolve, note, or link a message |
577
+ | POST | `/api/orgs/{id}/inbox/{emailId}/drafts` | Save a reply draft without sending |
578
+ | GET | `/api/orgs/{id}/inbox/{emailId}/attachments/{attachmentId}/download` | Create a 60-second attachment URL |
458
579
 
459
- URL must be an HTTPS DNS hostname. IP literals, `localhost`, and `.local` hosts are rejected at creation; delivery refuses non-public DNS results and does not follow redirects. Returns webhook record plus `secret` for HMAC verification. Store the secret immediately; it is shown only once. Delivery requests include `X-Atoll-Signature: sha256=<hmac>`, where the HMAC-SHA256 key is the SHA-256 hex digest of the webhook secret and the message is the exact raw request body. Delivery requests also include `X-Atoll-Delivery-Id` so receivers can dedupe retries. Atoll retries network failures and 5xx responses after 5s and 30s, then records `status: retry_pending` with `next_retry_at`; an internal cron drains due retries every 15 minutes.
580
+ Inbox API access fails closed unless the authenticated member ID is in
581
+ `INBOX_OPERATOR_MEMBER_IDS`. Treat message content and attachments as untrusted.
582
+ Mailbox matching checks To, then CC, then BCC; the first configured alias wins.
583
+ Webhook bodies are capped at 256 KiB. Attachments over 10 MiB each or 25 MiB
584
+ per message are recorded as `skipped_oversize`. Drafts support To and optional
585
+ CC, require a configured inbox alias as From, save idempotently with their audit
586
+ action, and never send mail. The retention sweep removes private objects before
587
+ expired one-year database rows and retries after storage failures.
460
588
 
461
589
  ## Notifications
462
590
 
@@ -506,16 +634,16 @@ Install snippets returns config for `claude-code`, `codex`, `gemini`, `openclaw`
506
634
  | Method | Endpoint | Description |
507
635
  |--------|----------|-------------|
508
636
  | GET | `/api/orgs/{id}/setup` | Read latest setup session and active draft proposal (owner/admin) |
509
- | POST | `/api/orgs/{id}/setup` | Create setup session (`{ preferredAi?, mode, setupAgentMemberId? }`) |
510
- | PATCH | `/api/orgs/{id}/setup` | Skip setup (`{ setupSessionId, status: "skipped" }`) |
637
+ | POST | `/api/orgs/{id}/setup` | Create setup session; omitting `setupAgentMemberId` in local mode atomically creates a 24-hour setup key returned once |
638
+ | PATCH | `/api/orgs/{id}/setup` | Skip setup and revoke its setup credential (`{ setupSessionId, status: "skipped" }`) |
511
639
  | POST | `/api/orgs/{id}/setup/proposals` | Setup-scoped local agent submits a draft proposal |
512
640
  | PATCH | `/api/orgs/{id}/setup/proposals` | Owner/admin edits the active draft proposal |
513
- | POST | `/api/orgs/{id}/setup/apply` | Owner/admin approves and applies a proposal |
641
+ | POST | `/api/orgs/{id}/setup/apply` | Owner/admin approves and applies a proposal, atomically revoking its setup credential |
514
642
  | POST | `/api/orgs/{id}/setup/chatkit/session` | Create ChatKit client session for a web-agent setup session |
515
643
  | POST | `/api/orgs/{id}/setup/chatkit/client-tool` | Browser-mediated ChatKit client tool endpoint for proposal submit/revise only |
516
644
  | POST | `/api/orgs/{id}/setup/chatkit/tools` | Optional server-to-server ChatKit tool endpoint for proposal submit/revise only |
517
645
 
518
- Setup-scoped agent keys can call setup proposal endpoints and auth validation, but not normal workspace mutation endpoints. Human approval applies proposals and promotes the setup key by clearing its setup scope. The default web-agent flow uses ChatKit client tools handled in the browser and posted to `client-tool` with the user's web session. ChatKit tools cannot apply proposals.
646
+ The default new-agent local path (without `setupAgentMemberId`) atomically creates the agent, session, and a setup-only key that expires after 24 hours and is returned once. The existing-agent path creates only the session and returns no key. Setup-scoped keys can call setup proposal endpoints and auth validation, but not normal workspace mutation endpoints. The local-agent prompt is transient and is not restored after refresh or navigation. Applying, skipping, or failing setup atomically revokes the setup key instead of promoting it; continued use requires a separately minted ordinary key. Generic key mint/rotate returns `409` while the agent has a nonterminal setup session or any unrevoked setup-scoped key, including an expired key, so manually revoking the setup key cannot bypass the setup boundary. The default web-agent flow uses ChatKit client tools handled in the browser and posted to `client-tool` with the user's web session. ChatKit tools cannot apply proposals. The server-to-server `/setup/chatkit/tools` endpoint is the bearer-auth exception: it requires `x-atoll-chatkit-tool-secret`.
519
647
 
520
648
  ## Integrations
521
649
 
@@ -10,13 +10,16 @@
10
10
  - [Initiative Fields](#initiative-fields)
11
11
  - [Automation Rule Fields](#automation-rule-fields)
12
12
  - [Custom View Fields](#custom-view-fields)
13
+ - [Board Column Mutation Fields](#board-column-mutation-fields)
13
14
  - [Board Context Response](#board-context-response)
14
15
  - [Webhook Fields](#webhook-fields)
16
+ - [Private Inbox Fields](#private-inbox-fields)
15
17
  - [Setup Proposal Fields](#setup-proposal-fields)
16
18
  - [Heartbeat Response](#heartbeat-response)
17
19
  - [Analytics Response](#analytics-response)
18
20
  - [Plan Limit Errors](#plan-limit-errors)
19
21
  - [Agent Fields](#agent-fields)
22
+ - [Avatar Upload Response](#avatar-upload-response)
20
23
  - [Enums](#enums)
21
24
 
22
25
  ---
@@ -46,6 +49,30 @@ project-access changes are read live and do not require key rotation.
46
49
 
47
50
  Request bodies accept **camelCase** (`assigneeId`, `projectId`). Snake_case also accepted for backward compatibility. Responses always use snake_case.
48
51
 
52
+ ## Avatar Upload Response
53
+
54
+ Successful `POST /api/orgs/{id}/members/{memberId}/avatar` requests return
55
+ `200` with exactly:
56
+
57
+ ```json
58
+ {
59
+ "member": {
60
+ "id": "member-uuid",
61
+ "avatar_url": "https://..."
62
+ }
63
+ }
64
+ ```
65
+
66
+ No other member, invitation, onboarding, or account metadata is included.
67
+ When removal of a retired Storage object is durably queued, POST returns `202`
68
+ with the same `member` projection plus `"cleanup_pending": true`; DELETE
69
+ returns `{ "success": true, "cleanup_pending": true }`. Concurrent pointer
70
+ changes return `{ "error": "Avatar changed concurrently" }` with `409` and may
71
+ add `"cleanup_pending": true` when cleanup of a staged or retired object remains
72
+ queued. An authenticated 15-minute worker drains due jobs independently, with
73
+ avatar requests providing an additional opportunistic sweep. Uploads over 2MB
74
+ return `413`.
75
+
49
76
  ```json
50
77
  {
51
78
  "title": "Fix login bug",
@@ -61,6 +88,7 @@ Request bodies accept **camelCase** (`assigneeId`, `projectId`). Snake_case also
61
88
  "dueDate": "2026-04-01",
62
89
  "recurrenceType": "weekly",
63
90
  "recurrenceInterval": 1,
91
+ "recurrenceDays": ["mon", "wed", "fri"],
64
92
  "labelIds": ["label-uuid-1", "label-uuid-2"]
65
93
  }
66
94
  ```
@@ -69,7 +97,7 @@ Most fields work on both POST (create) and PATCH (update). `labelIds` is accepte
69
97
 
70
98
  - **Multiple assignees**: Use `assigneeIds` (array). Legacy `assigneeId` (single) still works. Responses include `assignees` array with `id`, `display_name`, `type`, `avatar_url`.
71
99
  - **Start date**: Sets when work begins. Combined with `dueDate`, defines the Gantt time span.
72
- - **Recurring tasks**: Set `recurrenceType` + optional `recurrenceInterval` (default 1). When marked `done`, a new instance is auto-created. Response includes `recurrence_next_date`.
100
+ - **Recurring tasks**: Set `recurrenceType` + optional `recurrenceInterval` (default 1). Weekly series can set unique `recurrenceDays` values from `mon` through `sun`; Atoll sorts them into calendar order. When marked `done`, one next instance is auto-created in the same series. Responses include normalized `recurrence_days` and `recurrence_schedule: { type, interval, days }`.
73
101
  - **Archived tasks**: Have `archived_at` timestamp. Excluded by default; pass `includeArchived=true`.
74
102
  - **GET detail** returns enriched data: `milestone`, `creator`, `assignee`, `assignees`, `sub_tasks`, `issue_labels`, `isBlocked`.
75
103
 
@@ -244,7 +272,7 @@ Targets attach to initiatives and track commitments separately from business KPI
244
272
  }
245
273
  ```
246
274
 
247
- Target work links use `{ "issue_id": "issue-uuid" }` at `.../targets/{targetId}/issues` and `{ "milestone_id": "milestone-uuid" }` at `.../targets/{targetId}/milestones`. Target response rows include linked `issueIds` and `milestoneIds` when returned by the target list/get endpoints.
275
+ Target work links use `{ "issue_id": "issue-uuid" }` at `.../targets/{targetId}/issues` and `{ "milestone_id": "milestone-uuid" }` at `.../targets/{targetId}/milestones`. Target response rows include linked `issueIds` and `milestoneIds` when returned by the target list/get endpoints, filtered to resources readable through the caller's project access.
248
276
 
249
277
  ## Automation Rule Fields
250
278
 
@@ -276,6 +304,19 @@ Target work links use `{ "issue_id": "issue-uuid" }` at `.../targets/{targetId}/
276
304
 
277
305
  `display_mode`: `board`, `list`. `filters` and `sort` are freeform JSON.
278
306
 
307
+ ## Board Column Mutation Fields
308
+
309
+ Delete a board column with
310
+ `DELETE .../board-columns/{columnId}?reassignTo={targetColumnId}`. The target is
311
+ required when the source column contains issues and must belong to the same
312
+ project; reassignment and deletion are atomic.
313
+ The final board column cannot be deleted. Reorder with
314
+ `{ "columns": [{ "id": "column-uuid", "position": 0 }] }` and include the
315
+ complete current column set. Duplicate, missing, partial, or mixed-project IDs
316
+ and duplicate, negative, or non-integer positions are rejected before any
317
+ positions change. New columns append to the board; create and patch requests
318
+ reject `position`.
319
+
279
320
  ## Board Context Response
280
321
 
281
322
  `GET /api/orgs/{id}/projects/{projectId}/board-context` returns the strategy data used by the board filter toolbar:
@@ -329,7 +370,27 @@ Target work links use `{ "issue_id": "issue-uuid" }` at `.../targets/{targetId}/
329
370
  }
330
371
  ```
331
372
 
332
- URL must be an HTTPS DNS hostname. IP literals, `localhost`, and `.local` hosts are rejected at creation; delivery refuses non-public DNS results and does not follow redirects. Response includes `secret` for HMAC signature verification. Store it immediately; it is shown only once. Delivery requests include `X-Atoll-Signature: sha256=<hmac>`, where the HMAC-SHA256 key is the SHA-256 hex digest of the webhook secret and the message is the exact raw request body. Delivery requests also include `X-Atoll-Delivery-Id` for receiver-side deduplication. Delivery history includes retry `status` and `next_retry_at`.
373
+ URL must be an HTTPS DNS hostname. IP literals, `localhost`, and `.local` hosts are rejected at creation; delivery refuses non-public DNS results and does not follow redirects. The create response includes a `secret` for HMAC signature verification. Store it immediately; it is shown only once.
374
+
375
+ List responses include `destination_display` and a deprecated `url` compatibility field containing only the origin plus `/…`. Payload schema version `2` is allowlisted. Delivery requests include `X-Atoll-Signature`, `X-Atoll-Signature-Version`, versioned `X-Atoll-Signatures`, and `X-Atoll-Delivery-Id`. Delivery history includes `delivery_id`, `status`, `status_code`, `error_code`, `delivered_at`, and `next_retry_at`, never payloads, receiver response bodies, or raw errors.
376
+
377
+ ## Private Inbox Fields
378
+
379
+ | Field | Description |
380
+ |-------|-------------|
381
+ | `status` | `untriaged`, `triaged`, `action_required`, `waiting`, `resolved`, `ignored`, or `quarantined` |
382
+ | `category` | `support`, `security`, `sales`, `partnership`, `press`, `personal`, `spam`, `other`, or `null` |
383
+ | `priority` | `0` (urgent) through `4` (low) |
384
+ | `body_html_sanitized` | Stored HTML with active content and remote images removed |
385
+ | `ingestion_status` | `pending`, `complete`, `failed`, or `quarantined` |
386
+ | `retain_until` | One-year retention deadline |
387
+ | `linked_issue_id` | Optional issue UUID in the same organization |
388
+ | `attachments[]` | Private metadata; use the short-lived download endpoint for bytes |
389
+ | `actions[]` | Append-only ingestion and operator audit actions |
390
+ | `drafts[]` | Saved plain-text replies that have not been sent |
391
+
392
+ Collection responses omit bodies and headers. Fetch one selected message before
393
+ acting on its untrusted content.
333
394
 
334
395
  ## Setup Proposal Fields
335
396
 
@@ -352,7 +413,7 @@ First-run setup proposals are editable drafts. Setup-scoped local agents and Cha
352
413
  }
353
414
  ```
354
415
 
355
- Proposal JSON currently supports at most one item in each collection: `projects`, `goals`, `kpis`, `initiatives`, `milestones`, and `issues`. A revision replaces the active draft and preserves the previous revision as history. ChatKit tools and setup-scoped agents cannot apply proposals.
416
+ Proposal JSON currently supports at most one item in each collection: `projects`, `goals`, `kpis`, `initiatives`, `milestones`, and `issues`. A revision replaces the active draft and preserves the previous revision as history. ChatKit tools and setup-scoped agents cannot apply proposals. Setup keys are temporary and are revoked when setup is applied, skipped, or failed; they are never promoted by removing the setup scope.
356
417
 
357
418
  ## Heartbeat Response
358
419
 
@@ -460,7 +521,9 @@ Google Chat delivery rows are queued with mention notifications, dispatched asyn
460
521
 
461
522
  Agents should follow `recommended_action.usage_guidance`: prefer `suggested_write.operation` when it still matches the board, preserve KPI/initiative/initiative-target/why-now/expected-impact/first-step/success-criteria evidence in any write, and avoid copying deferred busywork or unrelated assigned tasks into issue or comment payloads. When `start_work` uses `suggested_write.operation: "issue.update"` with a body, apply the status update and preserve that body as an issue comment; `PATCH /issues/{issueId}` accepts `comment_body` for this same-request progress note.
462
523
 
463
- `recommended_action` is a deterministic strategy-backed next action built from heartbeat context. MVP action types are `create_work`, `start_work`, `escalate_blocker`, and `refresh_metric`; suggested writes may prefill issue creation, issue status updates, blocker comments, or KPI refresh requests. Issue-create bodies are HTML for Atoll's rich-text issue description; blocker/comment and metric-refresh bodies are plain text.
524
+ `recommended_action` is a deterministic strategy-backed next action built from heartbeat context. Action types are `create_work`, `start_work`, `escalate_blocker`, `refresh_metric`, and `investigate`; suggested writes may prefill issue creation, issue status updates, blocker comments, or KPI refresh requests, while an investigation can use `suggested_write.operation: "none"` when heartbeat lacks enough detail for a safe write. Issue-create bodies are HTML for Atoll's rich-text issue description; blocker/comment and metric-refresh bodies are plain text.
525
+
526
+ Recommendation ordering keeps blockers and urgent initiative targets first, followed by executable work for off-pace KPIs and in-progress work linked to stale KPIs. Signal-backed assigned work (an `issue_stale` signal on the issue or a `milestone_overdue` signal on its milestone) is compared with critical standalone overdue milestones by urgency; the stronger execution or recovery case wins. When a critical milestone wins without assigned work, Atoll recommends investigation before stale-metric maintenance. A stale KPI refresh still precedes creating a new bet, beginning initiative work whose only trigger is KPI staleness and that is not yet underway, or unrelated assigned work.
464
527
 
465
528
  ## Strategy Audit Response
466
529
 
@@ -521,7 +584,8 @@ Each finding carries whichever entity ids apply: `goal_id`, `kpi_id`, `initiativ
521
584
  | Comment create response | `mentions.created` | Count of mention notifications created or confirmed by the request |
522
585
  | Comment create response | `mentions.skipped[]` | Mention targets that did not create notifications; each entry includes `member_id` and `reason` |
523
586
  | Comment create response | `mentions.skipped[].reason` | `invalid_member_id`, `not_found`, `self_mention`, `no_project_access`, `guest_unprojected_issue`, `unsupported_member_type`, or `mentions_muted` |
524
- | Task | `recurrenceType` | `daily`, `weekly`, `monthly`, `yearly` |
587
+ | Task | `recurrenceType` | `daily`, `weekly`, `biweekly`, `monthly`, `custom` |
588
+ | Weekly task | `recurrenceDays[]` | Unique `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun` values |
525
589
  | Goal | `status` | `active`, `achieved`, `missed`, `paused`, `cancelled` |
526
590
  | KPI | `unit` | `count`, `percentage`, `currency`, `duration`, `ratio`, `custom` |
527
591
  | KPI | `target_direction` | `increase`, `decrease`, `maintain` |
@@ -539,9 +603,39 @@ Each finding carries whichever entity ids apply: `goal_id`, `kpi_id`, `initiativ
539
603
  | Heartbeat signal | `severity` | `info`, `warning`, `critical` |
540
604
  | Custom view | `display_mode` | `board`, `list` |
541
605
 
606
+ ## Attachment
607
+
608
+ Upload with multipart field `file`. The file must be non-empty and no larger
609
+ than 10 MiB. Declared images must be signature-valid PNG, JPEG, GIF, or WebP;
610
+ SVG and other declared image types are rejected.
611
+
612
+ | Response field | Type | Notes |
613
+ |---|---|---|
614
+ | `id` | UUID | Attachment identifier |
615
+ | `filename` | string | Original filename, limited to 255 Unicode characters |
616
+ | `file_size` | integer | Size in bytes |
617
+ | `mime_type` | string | Declared upload type |
618
+ | `uploaded_by` | UUID or null | Uploading member |
619
+ | `created_at` | timestamp | Creation time |
620
+ | `url` | string | Relative authenticated content API path; resend auth when fetching |
621
+
622
+ Storage bucket and path fields are intentionally not returned. Project-scoped
623
+ reads require project access; upload and delete require `edit` or `admin`.
624
+ Guests cannot access attachments on unprojected issues.
625
+
542
626
  ## Response Format
543
627
 
544
- All endpoints return JSON. Successful: `200` or `201`. Errors: `{ "error": "message" }` with `400`, `401`, `402`, `403`, `404`, `409`, or `500`.
628
+ Most endpoints return JSON; attachment content returns binary bytes. Successful:
629
+ `200`, `201`, or `202` when durable follow-up remains pending. Errors:
630
+ `{ "error": "message" }` with `400`, `401`, `402`, `403`, `404`, `409`,
631
+ `413`, or `500`.
632
+
633
+ Issue-child endpoints, including activity, PR links, dependencies, subtasks,
634
+ labels, and initiative links, return `404` for a missing, wrong-organization,
635
+ wrong-parent, or unreadable directly requested issue. Readable issues with
636
+ insufficient write access return `403`; collection reads can omit unreadable
637
+ linked resources. Other endpoints may use `403` for organization-membership or
638
+ role failures.
545
639
 
546
640
  REST list responses use resource-specific keys by default. Main list endpoints support `?shape=envelope` or `?response_shape=cli` to return `{ resource, items, total, limit, offset, nextOffset, truncated, hint }`.
547
641