@atollhq/skill-claude 0.4.16 → 0.4.18

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
@@ -26,6 +26,11 @@ Restart Claude Code and the `atoll` skill is available.
26
26
 
27
27
  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.
28
28
 
29
+ The installer writes credential-bearing files atomically, refuses symbolic-link
30
+ targets, uses `0600` for credential files, and uses `0700` for dedicated
31
+ credential directories. Concurrent installer runs are serialized so profile and
32
+ settings updates are not lost.
33
+
29
34
  ## Using the skill
30
35
 
31
36
  Once installed, ask Claude anything task-related:
package/bin/install.mjs CHANGED
@@ -1,8 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
4
- import { join, dirname } from 'node:path'
3
+ import {
4
+ chmodSync,
5
+ closeSync,
6
+ cpSync,
7
+ existsSync,
8
+ lstatSync,
9
+ mkdirSync,
10
+ openSync,
11
+ readFileSync,
12
+ renameSync,
13
+ rmSync,
14
+ writeFileSync,
15
+ } from 'node:fs'
16
+ import { basename, dirname, join } from 'node:path'
5
17
  import { homedir } from 'node:os'
18
+ import fsExt from 'fs-ext-extra-prebuilt'
6
19
  import { fileURLToPath } from 'node:url'
7
20
 
8
21
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -86,21 +99,137 @@ if (!args.key.startsWith('sk_atoll_')) {
86
99
  process.exit(1)
87
100
  }
88
101
 
102
+ function lstatIfExists(path) {
103
+ try {
104
+ return lstatSync(path)
105
+ } catch (error) {
106
+ if (error?.code === 'ENOENT') return undefined
107
+ throw error
108
+ }
109
+ }
110
+
111
+ function assertSafeFile(path) {
112
+ const stat = lstatIfExists(path)
113
+ if (!stat) return
114
+ if (stat.isSymbolicLink()) {
115
+ throw new Error(`Refusing symbolic link for credential file: ${path}`)
116
+ }
117
+ if (!stat.isFile()) {
118
+ throw new Error(`Refusing non-file credential path: ${path}`)
119
+ }
120
+ }
121
+
122
+ function ensurePrivateDirectory(path) {
123
+ const stat = lstatIfExists(path)
124
+ if (stat) {
125
+ if (stat.isSymbolicLink()) {
126
+ throw new Error(`Refusing symbolic link for credential directory: ${path}`)
127
+ }
128
+ if (!stat.isDirectory()) {
129
+ throw new Error(`Refusing non-directory credential path: ${path}`)
130
+ }
131
+ } else {
132
+ mkdirSync(path, { recursive: true, mode: 0o700 })
133
+ }
134
+ chmodSync(path, 0o700)
135
+ }
136
+
89
137
  function readJson(path) {
138
+ assertSafeFile(path)
90
139
  if (!existsSync(path)) return {}
91
140
  try {
92
141
  return JSON.parse(readFileSync(path, 'utf-8'))
93
- } catch {
142
+ } catch (error) {
143
+ if (!(error instanceof SyntaxError)) throw error
94
144
  console.error(`Warning: could not parse ${path}, creating fresh config`)
95
145
  return {}
96
146
  }
97
147
  }
98
148
 
149
+ function writePrivateFile(path, content) {
150
+ assertSafeFile(path)
151
+ const tmpPath = join(
152
+ dirname(path),
153
+ `.${basename(path)}.atoll-${process.pid}-${Date.now()}.tmp`,
154
+ )
155
+ try {
156
+ writeFileSync(tmpPath, content, { flag: 'wx', mode: 0o600 })
157
+ chmodSync(tmpPath, 0o600)
158
+ renameSync(tmpPath, path)
159
+ chmodSync(path, 0o600)
160
+ } catch (error) {
161
+ if (existsSync(tmpPath)) rmSync(tmpPath, { force: true })
162
+ throw error
163
+ }
164
+ }
165
+
166
+ function acquireInstallerLock() {
167
+ const atollDir = join(homedir(), '.atoll')
168
+ const target = join(atollDir, 'installer.lock-target')
169
+ ensurePrivateDirectory(atollDir)
170
+ try {
171
+ writeFileSync(target, '', { flag: 'wx', mode: 0o600 })
172
+ } catch (error) {
173
+ if (error?.code !== 'EEXIST') throw error
174
+ assertSafeFile(target)
175
+ }
176
+ chmodSync(target, 0o600)
177
+ const fd = openSync(target, 'r+')
178
+ const deadline = Date.now() + 10_000
179
+ const waitState = new Int32Array(new SharedArrayBuffer(4))
180
+ while (true) {
181
+ try {
182
+ if (process.platform === 'win32') {
183
+ fsExt.lockFileExSync(
184
+ fd,
185
+ fsExt.constants.LOCKFILE_EXCLUSIVE_LOCK
186
+ | fsExt.constants.LOCKFILE_FAIL_IMMEDIATELY,
187
+ 0,
188
+ 0,
189
+ 1,
190
+ 0,
191
+ )
192
+ } else {
193
+ fsExt.fcntlSync(fd, 'setlk', fsExt.constants.F_WRLCK, 0, 0)
194
+ }
195
+ break
196
+ } catch (error) {
197
+ if (!['EACCES', 'EAGAIN', 'EBUSY', 'EWOULDBLOCK'].includes(error?.code)) {
198
+ closeSync(fd)
199
+ throw error
200
+ }
201
+ if (Date.now() >= deadline) {
202
+ closeSync(fd)
203
+ throw new Error(`Timed out waiting for installer lock: ${target}`)
204
+ }
205
+ Atomics.wait(waitState, 0, 0, 25)
206
+ }
207
+ }
208
+
209
+ let released = false
210
+ return () => {
211
+ if (released) return
212
+ released = true
213
+ try {
214
+ if (process.platform === 'win32') {
215
+ fsExt.unlockFileExSync(fd, 0, 0, 1, 0)
216
+ } else {
217
+ fsExt.fcntlSync(fd, 'setlk', fsExt.constants.F_UNLCK, 0, 0)
218
+ }
219
+ } finally {
220
+ closeSync(fd)
221
+ }
222
+ }
223
+ }
224
+
225
+ const releaseInstallerLock = acquireInstallerLock()
226
+
99
227
  function writeAtollProfile() {
100
228
  if (!args.profile) return
101
229
 
102
230
  const atollDir = join(homedir(), '.atoll')
103
231
  const configPath = join(atollDir, 'config.json')
232
+ ensurePrivateDirectory(atollDir)
104
233
  const config = readJson(configPath)
105
234
  config.profiles ??= {}
106
235
  config.profiles[args.profile] ??= {}
@@ -116,8 +245,7 @@ function writeAtollProfile() {
116
245
  if (args.baseUrl) profile.baseUrl = args.baseUrl
117
246
  else delete profile.baseUrl
118
247
 
119
- mkdirSync(atollDir, { recursive: true })
120
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n')
248
+ writePrivateFile(configPath, JSON.stringify(config, null, 2) + '\n')
121
249
  console.log(`Configured Atoll CLI profile "${args.profile}" in ${configPath}`)
122
250
  }
123
251
 
@@ -125,6 +253,7 @@ function writeAtollProfile() {
125
253
  const skillSrc = join(__dirname, '..', 'skill')
126
254
  const skillDest = join(homedir(), '.claude', 'skills', 'atoll')
127
255
  const legacySkillDest = join(homedir(), '.claude', 'skills', 'atoll-api')
256
+ ensurePrivateDirectory(join(homedir(), '.claude'))
128
257
 
129
258
  mkdirSync(join(skillDest, 'references'), { recursive: true })
130
259
  cpSync(skillSrc, skillDest, { recursive: true })
@@ -162,7 +291,7 @@ if (args.profile) {
162
291
  settings.env.ATOLL_ENV_MODE = '1'
163
292
  }
164
293
 
165
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n')
294
+ writePrivateFile(settingsPath, JSON.stringify(settings, null, 2) + '\n')
166
295
  writeAtollProfile()
167
296
 
168
297
  const configuredVars = []
@@ -183,3 +312,4 @@ if (args.profile) {
183
312
 
184
313
  console.log(`\nDone! Start Claude Code and the atoll skill will be available.`)
185
314
  console.log(`Try: "List my Atoll tasks" or "Check my heartbeat"`)
315
+ releaseInstallerLock()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atollhq/skill-claude",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "description": "Install the Atoll project management skill for Claude Code",
5
5
  "bin": {
6
6
  "skill-claude": "bin/install.mjs"
@@ -22,6 +22,9 @@
22
22
  "url": "git+https://github.com/atollhq/atoll.git",
23
23
  "directory": "packages/skill-claude"
24
24
  },
25
+ "dependencies": {
26
+ "fs-ext-extra-prebuilt": "2.2.9"
27
+ },
25
28
  "license": "MIT",
26
29
  "type": "module"
27
30
  }
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
 
@@ -205,8 +220,10 @@ CLI JSON conventions:
205
220
  - Project-scoped `atoll issue list --json` includes `project_context`; `atoll issue get/view --json` includes `status_column` plus `project_context` when available.
206
221
  - 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
222
  - Diagnostics and errors go to stderr.
223
+ - Machine-readable JSON preserves API strings exactly; human terminal output removes ANSI/VT, control, and bidirectional formatting characters from API-supplied strings.
208
224
  - Interactive CLI update notices also go to stderr and are suppressed for JSON/non-TTY/CI/completion flows.
209
225
  - `atoll agent-context` returns a versioned command/flag manifest, available profile context, and structured `cli.update_available` metadata.
226
+ - 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
227
  - `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
228
  - `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
229
 
@@ -214,7 +231,7 @@ CLI JSON conventions:
214
231
 
215
232
  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
233
 
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.
234
+ 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
235
 
219
236
  ```bash
220
237
  atoll kpi sync validate <kpi-id> \
@@ -240,7 +257,11 @@ npm install -g @atollhq/mcp-server
240
257
  PORT=8787 atoll-mcp
241
258
  ```
242
259
 
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.
260
+ 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.
261
+
262
+ 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.
263
+
264
+ 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
265
 
245
266
  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
267
 
@@ -260,7 +281,7 @@ atoll issue list --json --limit 10
260
281
 
261
282
  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
283
 
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.
284
+ 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
285
 
265
286
  ### Prompt: Create the First Board
266
287
 
@@ -350,7 +371,9 @@ The primary pattern for autonomous agents. Prefer `atoll heartbeat --json` when
350
371
  - **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
372
  - **Signals** sorted by severity — the agent's prioritized to-do list
352
373
  - **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.
374
+ - **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.
375
+
376
+ 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
377
 
355
378
  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
379
 
@@ -411,13 +434,33 @@ atoll kpi snapshot add paying_customers --value 42 --initiative "Content pipelin
411
434
  atoll kpi snapshot list paying_customers --include-attribution --json
412
435
  ```
413
436
 
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.
437
+ 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.
438
+
439
+ Project-linked initiative reads require access to at least one linked project.
440
+ The authoritative set includes explicit project links and projects inferred
441
+ from direct issue/milestone links. Updating an initiative or mutating its issue,
442
+ milestone, or target links requires edit/admin access to every linked project;
443
+ a requested issue or milestone project must already be linked when it is
444
+ project-bound. Eligible non-guests may link and unlink writable projectless
445
+ issues; projectless milestones are unsupported. KPI-impact reads omit
446
+ unreadable KPIs and KPI-impact writes require owner/admin Strategy access.
447
+ Projectless initiative writes require an organization owner/admin.
448
+ Treat `404` as concealed absence or unreadable scope and `403` as insufficient
449
+ write access to a readable initiative.
450
+
451
+ KPIs are organization-wide Strategy resources. Owners/admins may read and
452
+ write; other non-guest organization members may read values, snapshots, and
453
+ redacted per-KPI sync metadata but cannot create, update, delete, or record
454
+ snapshots. Guest/project-scoped agents receive `403` for the collection and
455
+ concealed `404` responses for direct KPI, snapshot, and per-KPI sync
456
+ read/draft routes. Verify the active profile's organization-wide role before
457
+ running KPI commands.
415
458
 
416
459
  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
460
 
418
461
  ### Audit and improve the strategy
419
462
 
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.
463
+ 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
464
 
422
465
  ```bash
423
466
  atoll strategy audit # human-readable, grouped by severity
@@ -426,6 +469,12 @@ atoll strategy audit --json # findings[] for programmatic remediation
426
469
 
427
470
  `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
471
 
472
+ The audit follows the caller's project access. Owners/admins receive
473
+ organization-wide execution evidence. Other non-guests receive project-bound
474
+ issues, milestones, target links, and target findings only for readable
475
+ projects. A restricted caller with no readable projects receives no issue or
476
+ target execution evidence. Guests cannot run the audit.
477
+
429
478
  1. `atoll strategy audit --json` to get findings.
430
479
  2. For each finding, apply its `suggested_fix`, e.g.:
431
480
  - `initiative_orphaned` → `atoll initiative update "<initiative>" --goal "<goal>"` (or `PATCH .../initiatives/{id} { goal_id }`)
@@ -458,9 +507,11 @@ Config sessions and unused manual connect tokens expire after 10 minutes. Sessio
458
507
  Webhook creation returns a raw `whsec_...` secret once. Delivery requests include:
459
508
 
460
509
  - `X-Atoll-Signature`: `sha256=` plus an HMAC-SHA256 over the raw body, keyed by the SHA-256 hex digest of the raw secret.
510
+ - `X-Atoll-Signature-Version`: the primary signing-key version.
511
+ - `X-Atoll-Signatures`: versioned signatures during a bounded key-overlap window.
461
512
  - `X-Atoll-Delivery-Id`: stable delivery id for receiver-side deduplication.
462
513
 
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.
514
+ 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
515
 
465
516
  ### Billing and plan limits
466
517
 
@@ -486,6 +537,7 @@ Full endpoint tables and field schemas:
486
537
  | 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
538
  | Milestones | POST `.../milestones` | GET `.../milestones` | PATCH `.../milestones/{id}` | DELETE `.../milestones/{id}` |
488
539
  | Comments | POST `.../comments` with `{ body, mentions?, reply_to_comment_id?, source_metadata? }` | GET `.../comments` or `.../comments/{id}` | PATCH `.../comments/{id}` | DELETE `.../comments/{id}` |
540
+ | Attachments | POST `.../attachments` | GET `.../attachments` or `.../attachments/{id}/content` | — | DELETE `.../attachments/{id}` |
489
541
  | Subtasks | POST `.../subtasks` | GET `.../subtasks` | PATCH `.../subtasks/{id}` | DELETE `.../subtasks/{id}` |
490
542
 
491
543
  Initiative create accepts `title` or legacy `name`, plus camelCase aliases `goalId`, `ownerId`, and `targetDate`.
@@ -494,6 +546,29 @@ All endpoints are under `/api/orgs/{orgId}/...`.
494
546
 
495
547
  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
548
 
549
+ Project-bound milestone, status-update, board-column, issue-activity, and PR-link
550
+ reads require effective project access. Milestone create/update, status-update
551
+ create, board-column mutations, and project-bound PR-link create require `edit`
552
+ or `admin`; eligible non-guests may read issue activity and read or attach PR
553
+ links for projectless issues. Milestone delete remains organization
554
+ owner/admin-only. Issue activity is read-only. Organization activity and
555
+ analytics are limited to the caller's accessible projects, with eligible
556
+ non-guests also receiving projectless data; project-health contains accessible
557
+ projects only. Do not treat org membership alone as project authorization.
558
+
559
+ Issue templates follow the same effective-project boundary: project-template
560
+ reads require project access and writes require `edit`/`admin`.
561
+ Organization-wide templates are readable by non-guests and manageable only by
562
+ organization owners/admins; guest/project-scoped agents never receive them.
563
+ Avatar mutations require both caller and target to belong to the organization
564
+ in the request path. Avatar pointer changes use compare-and-set semantics;
565
+ concurrent changes return `409`, and successful mutations with durable Storage
566
+ cleanup still queued return `202` with `cleanup_pending: true`. A conflict can
567
+ also include `cleanup_pending: true` when cleanup of a staged or retired object
568
+ remains queued. An authenticated 15-minute worker drains due jobs
569
+ independently, with avatar requests providing an additional opportunistic
570
+ sweep.
571
+
497
572
  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
573
 
499
574
  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 +579,12 @@ Agent-authored direct comments may include explicit `source_metadata` with `harn
504
579
 
505
580
  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
581
 
582
+ 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.
583
+
584
+ 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.
585
+
586
+ 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`.
587
+
507
588
  † `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
589
 
509
590
  ### Quick enum reference
@@ -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