[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71STQTM5MEHRKQPDQG79AGT":3,"skill-same:01M71STQTM5MEHRKQPDQG79AGT":65},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":45,"commitSha":46,"skillDirectory":47,"forksCount":48,"repositoryHtmlUrl":49,"repositoryHomepageUrl":6,"ownerHtmlUrl":50,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":51,"install":52,"resources":64},"01M71STQTM5MEHRKQPDQG79AGT","pcxa",null,"PCXA construction intelligence platform CLI. Search\u002Fread files, manage tags and folders, manage activities\u002Fsteps\u002Fprogress\u002Fdependencies, manage form templates\u002Ffields\u002Fsubmissions, manage custom objects (field choices) with fuzzy \"did you mean?\" value matching, manage resources\u002Ftimesheets\u002Fcost-codes\u002Fbudgets, manage entity links between objects, and chat with the project's AI assistant. Use when the user asks about project files, documents, tasks, activities, forms, custom objects, resources, timesheets, entity links, project management, or wants to send messages to the AI chatbot.","PCX-Analytics\u002Fpcxa-skill","PCX-Analytics","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F275529346?v=4","01M71STM9KGHKAF926SNSQNJNX","PCX Analytics","01M71STMAMRM6EBAJ77TA5NFAX",0,"MIT","https:\u002F\u002Fgithub.com\u002FPCX-Analytics\u002Fpcxa-skill\u002Fblob\u002Fmain\u002Fskills\u002Fpcxa\u002FSKILL.md",[],[5,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"platform","activities","steps","send","value","links","you","form","resources","assistant","project","forms","objects","files","read","command","search","ai chatbot","documents","2026-11-10 13:02:02",false,"---\nname: pcxa\ndescription: PCXA construction intelligence platform CLI. Search\u002Fread files, manage tags and folders, manage activities\u002Fsteps\u002Fprogress\u002Fdependencies, manage form templates\u002Ffields\u002Fsubmissions, manage custom objects (field choices) with fuzzy \"did you mean?\" value matching, manage resources\u002Ftimesheets\u002Fcost-codes\u002Fbudgets, manage entity links between objects, and chat with the project's AI assistant. Use when the user asks about project files, documents, tasks, activities, forms, custom objects, resources, timesheets, entity links, project management, or wants to send messages to the AI chatbot.\nargument-hint: \u003Ccommand> [options]\nuser-invocable: true\ndisable-model-invocation: false\n---\n\n# PCXA CLI\n\n**Tool:** `pcxa`\n\nAll commands output JSON by default. Use `-f table` for human-readable. Use `--dry-run` on write operations. Run `pcxa \u003Ccommand> --help` for full options.\n\nWhen installed as a Claude Code plugin, `pcxa` is provided by the plugin `bin\u002F` directory. For direct terminal use outside Claude Code, ask the user to install it once with:\n```\npipx install git+https:\u002F\u002Fgithub.com\u002FPCX-Analytics\u002Fpcxa-skill.git\n```\nThen `pcxa update` self-upgrades from GitHub. The CLI prints a one-line notice to stderr (max once per 24h) when a newer release is available.\n\n\n## Setup & Authentication\n\nAlways start by running `pcxa whoami` to see the current state. The output tells you whether the user is authenticated and whether a project is set. Only run setup steps that are actually missing.\n\n### Step 1 — Authentication (drive it yourself, don't punt to a terminal)\n\nIf `whoami` says \"No profiles configured\" or a command fails with \"Profile not found\":\n\n```bash\npcxa login --no-setup\n```\n\nThe `--no-setup` flag is required when you (the agent) drive login: it skips the interactive company\u002Fproject picker that reads stdin, which would otherwise hang. The CLI prints two lines immediately:\n\n```\nOpening browser to authenticate...\n  If your browser does not open automatically, visit:\n  https:\u002F\u002Fwww.pcxa.app\u002Fauth\u002Fcli-auth?port=PORT&state=STATE\n```\n\n**Read those lines from stdout, then surface the URL to the user as a clickable markdown link.** The CLI tries `webbrowser.open()` but that's usually a no-op in WSL\u002Fheadless — the user opening the link manually is the normal path. They sign in (MFA\u002FSSO supported), the page redirects to a localhost callback, the CLI captures tokens, the command exits.\n\nRun with a generous bash timeout (e.g., 180s) since the user may take a moment to sign in. The CLI's own `--timeout` defaults to 120s; pass `--timeout 300` if you want longer.\n\nIf browser login isn't viable (rare — only if the host can't reach `pcxa.app`), fall back to password login: `pcxa setup -u USER_EMAIL` (prompts for password — only works if the user can run it themselves).\n\n### Step 2 — Pick a project\n\nAfter login (or if `whoami` shows `Project: not set`), drive the project picker yourself:\n\n```bash\npcxa projects -f table       # list all (company_id, project_id) the user has access to\n```\n\nShow that list to the user, ask which project they want, then:\n\n```bash\n# When the conversation\u002FCWD is inside a repo that maps 1:1 to a PCXA project,\n# pin it locally so future runs in this repo are auto-scoped:\npcxa set-project PROJECT_ID --company COMPANY_ID --local\n\n# Otherwise, set the global default for this user:\npcxa set-project PROJECT_ID --company COMPANY_ID\n```\n\nAlways pass `--company` when picking — `--local` writes a `.pcxa` file in CWD; without `--local`, the choice is saved in the global profile.\n\n### How resolution works\n\nProject scope resolves in this order: **repo `.pcxa` file** > **active profile default**. `.pcxa` is committed (no secrets — just `{ \"company\": 4, \"project\": 10, \"user\": \"alice@example.com\" }`). Different repos can pin different accounts via the `user` field; the CLI matches it against profile usernames in the active credentials file.\n\nCredentials resolve **folder-first**: a `.pcxa-credentials.json` found by walking up from CWD is used for both reads and writes (token refresh included); otherwise the global `~\u002F.pcxa\u002Fcredentials.json` is used. So `pcxa login` from inside a repo writes that repo's own `.pcxa-credentials.json` (at the git root, gitignored) and can't clobber another repo's tokens — pass `--global` to write the shared global file instead. Pre-0.3 global creds at `~\u002F.file_explorer\u002Fconfig.json` are auto-migrated to `~\u002F.pcxa\u002Fcredentials.json` on first run.\n\nState to surface to the user on the first turn: `whoami` shows `Active profile`, `User`, `Company`, `Project` (with `(from .pcxa)` annotation when applicable), `Creds:`, and `Repo pin:`. If you ran setup steps, echo the resulting scope back (\"Operating on project Acme Tower (4)\") so the user can correct you before any writes.\n\n## Project Metadata\n\n```bash\npcxa project get                                              # view project details\npcxa project members                                          # list members (name → user ID)\npcxa project members --search \"John\"                          # search by name\u002Fusername\npcxa project update --name \"New Name\"                         # update name\npcxa project update --description \"...\" --scope-statement \"...\" # set description & scope\npcxa project update --code \"T-FAB1\" --industry \"Construction\" # set code & industry\npcxa project update --start-date 2025-12-03 --end-date 2026-08-10\npcxa project update --rollup-method equal                     # equal|duration|cost|labor\npcxa project update --progress-input-method percentage        # status|percentage\n```\n\nFields: `name`, `code` (max 20), `description`, `scope-statement`, `industry`, `life-cycle`, `start-date`, `end-date`, `progress-input-method`, `rollup-method`.\n\n## File Search & Reading\n\n```bash\npcxa files list --ext PDF --search \"keyword\" --limit 50  # title trigram (fuzzy by default; exact first)\npcxa files list --search \"concret\" --exact               # tighter title match (still AND-matches words, NOT a phrase)\npcxa files query 'title:schedule AND precast AND delay'  # BOOLEAN: AND\u002FOR\u002FNOT + grouping (⚠ multi-term AND under-reports on projects without the fix — see below)\npcxa files query 'title:report AND (delay OR \"change order\")'   # grouping + quoted phrase\npcxa files query 'contract NOT draft' --ext PDF          # exclusion (impossible with --search\u002F--content)\npcxa files query 'title:\"Case Memo\"' --count-only        # true LITERAL phrase on the filename\npcxa files list --content \"IOCC-387\"                     # literal substring in file BODY — every match, paginated (stable id order)\npcxa files list --content \"IOCC-387\" --count-only        # exact total of content matches (count:0 = genuinely not located)\npcxa files list --tags \"urgent,review\" --tags-mode all   # AND: files with ALL tags\npcxa files search \"natural language query\"                    # hybrid keyword + semantic (same endpoint as the web UI)\npcxa files search \"query\" --scope file,activity               # restrict source types (csv: file,activity,drawing,photo)\npcxa files search \"query\" --ext PDF --limit 25                # narrow to file type, page-size up to 50\npcxa files content \"BRG report\" --ext PDF                     # alias of `files search` scoped to files (same hybrid endpoint)\npcxa files read FILE_ID --outline                             # section map\npcxa files read FILE_ID                                       # first 5 chunks\npcxa files read FILE_ID --start 5                             # next window\npcxa files batch-read 423 511 612 --window 3                  # multi-file in one call\npcxa files batch-read --chunk 423:7 --chunk 511:12            # excerpts at specific chunks\npcxa files batch-read 423 511 --outline                       # outlines, multi-file\npcxa files info FILE_ID                                       # metadata + versions\npcxa files stats                                              # project-wide counts\npcxa files aggregate file_type                                # group by dimension\npcxa files recent --limit 30\npcxa files download FILE_ID                              # download to current dir\npcxa files download FILE_ID -o report.pdf                # custom output path\npcxa files upload \u002Fpath\u002Fto\u002Ffile.pdf --folder 5 --title \"Report\" --tags \"final,2026\"\npcxa files upload \u002Fpath\u002Fto\u002Fdir\u002F --folder 5               # bulk upload all files in dir (flat)\npcxa files sync \u002Fpath\u002Fto\u002Ftree --folder 5                 # recursive mirror; creates subfolders; idempotent\npcxa files sync \u002Fpath\u002Fto\u002Ftree --folder 5 --manifest .pcxa-sync.json   # persist upload log for fast re-runs\npcxa files sync \u002Fpath\u002Fto\u002Ftree --folder 5 --include \"*.pdf\" --exclude \"draft_*\" --concurrency 16\npcxa files delete 123 124 --yes                          # mark for deletion (adds 'to_delete' tag)\npcxa files restore 123 124                               # remove 'to_delete' tag (undo)\npcxa files list --tags to_delete                         # list everything pending deletion\npcxa files set-index-mode 1 2 3 --mode none              # stop server-side indexing (BYOC prep)\npcxa files upload-chunks corpus.jsonl --manifest .pcxa-sync.json   # supply your own chunks + vectors\npcxa files upload-ocr ocr.jsonl --manifest .pcxa-sync.json --validate-only  # dry-run your own OCR\npcxa files upload-ocr ocr.jsonl --manifest .pcxa-sync.json         # supply your own OCR (text + boxes)\n```\n\n**Upload storage:** Small files are uploaded through the API. Larger files use a presigned upload flow handled by the CLI and API.\n\n## Bring your own chunks (`files upload-chunks`)\n\nFor a caller that runs its **own** extraction \u002F chunking \u002F embedding pipeline and\nwants PCXA to serve *its* index instead of re-deriving one. Files must exist\nfirst — chunks attach to them.\n\n> **If what you have is OCR page text, you want [`files\n> upload-ocr`](#bring-your-own-ocr-files-upload-ocr) instead.** Sending OCR\n> through this command succeeds — and silently discards your bounding boxes and\n> opts those files out of every future chunker improvement, because the rows are\n> marked `chunk_source=external` and are never re-derived.\n\n```bash\n# 1. upload the files (idempotent, resumable, records file_ids in the manifest)\npcxa files sync .\u002Fcorpus --folder 42 --manifest .pcxa-sync.json\n\n# 2. stop our chunker touching them (optional but recommended — see below)\npcxa files set-index-mode 101 102 103 --mode none\n\n# 3. supply chunks + embeddings\npcxa files upload-chunks .\u002Fchunks\u002F --manifest .pcxa-sync.json --state .pcxa-chunks.json\n\n# validate the whole corpus without sending anything\npcxa files upload-chunks .\u002Fchunks\u002F --dry-run\n```\n\n**Input is JSON-Lines, one record per file, streamed** — a multi-million-chunk\ncorpus never has to fit in memory. Pass `.jsonl` files or directories of them.\n\n```json\n{\"file_id\": 123, \"chunks\": [\n  {\"chunk_index\": 0, \"content\": \"...\", \"embedding\": [768 floats]},\n  {\"chunk_index\": 1, \"content\": \"...\", \"embedding\": [768 floats]}\n]}\n```\n\nPer-chunk optional keys: `content_hash` (sha256 hex), `page_number` (1-based),\n`metadata` (object). Per-file optional keys: `file_version_id`,\n`indexed_content_hash`, `document_summary`, `document_context_strategy`.\n\nInstead of `file_id` a record may carry `path` or `name`, resolved through\n`--manifest` against the manifest a prior `files sync` wrote — so the two\ncommands compose without you keeping your own id table. A filename that maps to\nmore than one file is **rejected, not guessed** (it would overwrite the wrong\ndocument's index); address those by `path` or `file_id`.\n\n### Embeddings\n\nSupply `embedding` on every chunk of a file and that file is marked **INDEXED** —\nour embedder never runs on it, and you pay nothing for embedding. Omit them\nentirely and the file lands **CHUNKED** for our embedder to pick up (billable,\nbut valid — useful if you have good chunk boundaries but no vectors).\n\n**Embeddings are all-or-nothing per file.** A file where only some chunks carry a\nvector is rejected client-side, because the server would demote the whole file to\nCHUNKED and re-embed it — silently costing you the thing you were avoiding.\n\nVectors must be exactly **768 dimensions** from `gemini-embedding-001`\n(`--embedding-model` to override, but the server requires an exact match). This\nis checked hard on purpose: every vector in the index is 768-dim, so another\n768-dim model's output validates fine and then simply retrieves badly, forever,\nwith nothing to detect it afterwards.\n\nYou do **not** need to supply identifier metadata — the server extracts\nRFI\u002FPCO\u002FCO\u002FNCR-style references from the `content` you send.\n\n### Pacing: a client-side default you can safely raise\n\n`--chunks-per-hour` defaults to **60,000**, which keeps a long backfill\ncomfortably under the endpoint's limit of **30 requests\u002Fminute per user**. This\nis client-side pacing only. It is **not** a durability constraint, and nothing\ndownstream requires the upload to be slow.\n\nRaise it for a large corpus, or pass `--chunks-per-hour 0` to upload as fast as\nthe rate limit allows — at the 5,000-chunk maximum per request that is a great\ndeal of headroom. Either way the client retries `429`s honouring `Retry-After`,\nand once a chunk is accepted the server is responsible for storing it durably;\nthere is no follow-up step for you to run.\n\n### Why `set-index-mode --mode none` first\n\nWithout it, our own chunker processes every file before your chunks arrive. That\nis *harmless* — your upload replaces whatever it produced, and the guards below\nthen protect it — but it is pure waste at corpus scale. `--mode none` skips it\nentirely; your upload still creates the index row.\n\n### After a successful upload, the server leaves your chunks alone\n\n- Server-side reprocessing will not re-chunk the file: retries and re-runs leave\n  your chunks in place.\n- Index-policy changes — including a project left at the default `selective` —\n  neither delete your chunks nor strip their vectors.\n- Uploading a **new version** of the file keeps your chunks and marks the index\n  **stale**: search keeps returning the *older* text until you re-upload for the\n  new version.\n\n### Operational notes\n\n- **Auth: project admin or company admin.** Chunk upload replaces a file's whole\n  indexed content, so it takes the same gate as every other bulk file operation.\n  Plain project membership gets `403`. Give an integration a service account with\n  project-admin on each target project.\n- **Resume:** `--state \u003Cpath>` records applied file ids; a re-run skips them.\n  Ctrl-C saves state before exiting.\n- **Batching** is automatic against the server caps — 50 files and 5,000 chunks\n  per request, 2,000 chunks per file, 16,000 chars per chunk. Values above the\n  caps are clamped rather than rejected.\n- **`--dry-run` validates everything** — dimensions, partial embeddings,\n  duplicate `chunk_index`, oversized content — without spending a request. Run it\n  over the full corpus before the real load.\n- **Failures are per-item.** One bad `file_id` doesn't abort the batch; bad\n  records are counted and logged (`--error-log` for one JSON line each).\n  `--max-failures` (default 100) aborts a run that is clearly misconfigured.\n- **Rate limits** (`429`) are retried honouring `Retry-After`.\n- **Exit code is non-zero** if anything failed, so a driving script can tell.\n\n## Bring your own OCR (`files upload-ocr`)\n\nFor a caller who **scanned the documents themselves**. You send page text —\nideally with word- and line-level bounding boxes — and PCXA treats it as the\nfile's authoritative text: it chunks it, extracts identifiers, indexes it for\nboolean search and embeds it, exactly as it would text from its own OCR\nprovider.\n\n### Which of the two upload commands you want\n\n**This is the most common mistake with these two endpoints, and nothing tells\nyou afterwards, because both succeed.**\n\n| | `upload-ocr` | `upload-chunks` |\n|---|---|---|\n| You have | page **text** from a scan | finished **chunks**, usually embedded |\n| Who chunks | **the server** | nobody — it stores what you send |\n| Geometry | preserved at full precision | no field for it |\n| Row marked | ordinary — re-derivable | `chunk_source=external`, never re-derived |\n| After acceptance | chunking → embedding, async | nothing |\n\nSend OCR through `upload-chunks` and you get a 200 and a searchable file — and\nyou have silently thrown away every bounding box and opted those files out of\nevery future chunker improvement. If you OCR'd a scan, use `upload-ocr`.\n\n(On a bring-your-own-chunks project `upload-ocr` refuses outright with\n`project_external_chunks_only`: those projects were promised the server never\nderives text server-side.)\n\n### Start with `--validate-only`\n\n```bash\n# 1. the dry run — writes nothing, reports refusals grouped by error_code\npcxa files upload-ocr .\u002Focr\u002F --manifest .pcxa-sync.json --validate-only\n\n# 2. the real load, resumable\npcxa files upload-ocr .\u002Focr\u002F --manifest .pcxa-sync.json --state .pcxa-ocr.json\n```\n\nThe dry run is **server-side**, which is the point: it reports envelope shape\nerrors *and* the state-machine refusals a client cannot see — whether each file\nis parked, already indexed, has an OCR batch in flight, belongs to a\nbring-your-own-chunks project. For a six-figure run that is the difference\nbetween discovering 8,000 unusable targets before scanning and after.\n\nIt exits **0** when the run completed: refusals are its *output*, not its\nfailure, so `validate && upload` is scriptable. Only client-side rejections and\ntransport failures make it non-zero. `--dry-run` is an accepted alias.\n\n### Input\n\nJSON-Lines, one record per file, streamed — a corpus never has to fit in memory.\n\n```json\n{\"file_id\": 123, \"envelope\": {...}}\n{\"path\": \"Sources\u002FRFI-142.pdf\", \"overwrite\": false, \"envelope\": {...}}\n```\n\nPer-record optional keys: `overwrite`, `file_version_id`, `provenance`. As with\n`upload-chunks`, `path`\u002F`name` resolve through `--manifest`, and an ambiguous\nfilename is rejected rather than guessed.\n\nThe `envelope` is one of **two shapes, and that is the whole set** — a closed\nunion. Do not invent a third, and do not send a layout envelope with null or\nfabricated boxes.\n\n**`pcxa.ocr_layout.v1`** — text with geometry. All coordinates 0–1 normalized,\norigin top-left. Positional arrays because this repeats per word across a corpus.\n\n```json\n{\"schema\": \"pcxa.ocr_layout.v1\",\n \"provenance\": {\"producer\": \"mxi-scanner\u002F1.4\", \"tool_version\": \"1.4.0\"},\n \"pages\": [{\"n\": 1, \"w\": 2550, \"h\": 3300,\n            \"lines\": [{\"t\": \"REINFORCED CONCRETE SLAB\",\n                       \"b\": [0.08, 0.10, 0.92, 0.14],\n                       \"c\": 0.97,\n                       \"words\": [[\"REINFORCED\", 0.08, 0.10, 0.24, 0.14, 0.99]]}]}]}\n```\n\n**`pcxa.text.v1`** — no geometry. Fully supported, and better than fabricated\nboxes; you lose only future geometry-dependent features, not retrieval.\n\n```json\n{\"schema\": \"pcxa.text.v1\",\n \"provenance\": {\"producer\": \"mxi-scanner\u002F1.4\"},\n \"pages\": [{\"n\": 1, \"text\": \"...\"}]}\n```\n\n**Send full precision — do not round.** The stored copy is the master and\neverything downstream is re-derived from it, so rounding is irreversible loss\ntaken to save storage that costs about $0.015\u002FGB-month.\n\n### `202` does not mean searchable\n\n**This is the one that gets reported as a bug.** Acceptance means the text is\ndurably stored. Chunking and embedding happen afterwards on a background queue,\nso the response cannot tell you the file is searchable and this command's exit\ndoes not mean \"indexed\". Read `pcxa files info \u003Cid>` \u002F the index status for that.\n`upload-chunks` returns a terminal state; this does not.\n\n### Refusals are per-item, and most are not retryable\n\nRefused files are reported and skipped — retrying them only burns the rate\nlimit. The end-of-run report is a **histogram by `error_code`**, not a wall of\nper-file lines (only the first 20 are echoed; `--error-log` gets every one).\n\nNot retryable: `index_parked` · `already_indexed` · `external_chunk_source` ·\n`project_external_chunks_only` · `index_excluded_by_policy` ·\n`file_version_mismatch` · `incomplete_page_coverage` · `has_server_text` ·\n`has_ocr_text` · `derivation_pending` · `not_found`.\n\nTransient, and worth retrying: `ocr_batch_in_flight` · `chunking_in_progress`.\nThese are deliberately **not** recorded in `--state`, so re-running with the\nsame state file retries exactly those.\n\nTwo will confuse a caller, so the CLI explains them inline:\n\n- **`index_parked` is a decision, not an error.** Someone excluded that file\n  from indexing on purpose, and this endpoint clears **no** park reasons. A\n  whole folder coming back parked means that folder is excluded deliberately.\n- **`incomplete_page_coverage`** only happens on a partially-OCR'd file, and\n  names the missing pages. Your upload must cover every page the server still\n  needs; it then stores only those, so text already extracted from the good\n  pages survives.\n\n`warnings` are advisory and are **not** failures. The common one — a word box\noutside its line box — is legitimate for rotated text.\n\n### Operational notes\n\n- **Auth: project admin or company admin**, same gate as `upload-chunks`. Give\n  the integration its own service account: every applied file is written to the\n  audit log with the acting user.\n- **Client-side validation mirrors the server** so a bad payload fails before it\n  costs a request: closed schema union, 1-indexed unique page numbers, bboxes\n  with `x1 > x0` \u002F `y1 > y0` and all coords in `[0,1]`, positive `w`\u002F`h`, and at\n  most 1,000 pages per file. `\"lines\": []` is a **legal blank page**; an empty\n  `pages` array is not.\n- **Confidence is optional, and `0` is rejected.** `0` asserts the text is\n  certainly wrong — it is a common exporter default, and a corpus carrying it\n  tells the ranker to distrust every line. Omit the field instead.\n- **Batching packs by bytes *and* file count** — 50 files\u002Frequest and a 10 MB\n  body cap, where the body cap is a `413` raised *before* the body is parsed, so\n  nothing in the response would say which file was responsible. `--max-bytes`\n  (default 9 MB) is the byte budget. A single file whose envelope exceeds it is\n  rejected client-side: an envelope is that file's text master and cannot be\n  split across requests.\n- **Pacing** `--pages-per-hour` defaults to 250,000. The endpoint allows **120\n  requests\u002Fminute** — its own throttle scope, *not* `upload-chunks`' 30\u002Fmin.\n  Client-side smoothing only: raise it, or pass `0` to disable. `429`s are\n  retried honouring `Retry-After` either way.\n- **Retries need no idempotency key.** Dedup is content-addressed and computed\n  server-side; a re-post of byte-identical content for the same file and version\n  returns `\"deduplicated\": true` and writes nothing new. Do **not** add an\n  `Idempotency-Key` header — the client cannot reproduce the server's canonical\n  JSON, so a client-computed key would be wrong in both directions.\n- **`--overwrite`** (default off) displaces text a file already has; a\n  per-record `\"overwrite\"` key wins over the flag. It never overrides\n  `already_indexed` or `index_parked`.\n- **Resume** with `--state`, exactly as `upload-chunks` does.\n\n**Not in scope:** producing the OCR. This command transports what a scanner\nemits.\n\n**Bulk tree sync (`files sync`):** Mirrors a local directory tree under a PCXA folder. Walks the tree, creates any missing subfolders to match, and uploads files in parallel via the same presign+PUT\u002Fmultipart path as `files upload`. Idempotent two ways: it lists each target folder once and skips local files whose name already exists there, and an optional `--manifest \u003Cpath>` persists `{relative_path → {size, file_id}}` so re-runs skip without hitting the API. Failures (network, register errors) are listed at the end and counted toward `error` rate. Progress is rendered live on stderr: a bar plus files-done, bytes-done\u002Ftotal, throughput, current concurrency (`c=N`), elapsed, ETA, and error count. Filters: `--include` and `--exclude` accept repeatable globs against filenames; dotfiles\u002Fdot-dirs are skipped by default (`--include-hidden` opts in).\n\n**Designed for TB-scale runs:**\n- **Auto-tuning concurrency** (default ON): an AIMD controller samples throughput and error rate every 10s and adjusts active workers via a runtime-resizable semaphore. Errors halve concurrency with a 30s cooldown; clean windows with rising throughput step it up. Bound by `--min-concurrency` \u002F `--max-concurrency` (default 1 \u002F 32). Pass `--no-auto-tune` to pin concurrency at `--concurrency` for the whole run. Tuner decisions print above the progress line so you can see what's happening.\n- **Failure-budget circuit breaker** (`--max-failures`, default 100): aborts the run cleanly if failure events hit the budget (one failed batch = one event; rows the server rejects count too, and no further batch is registered once it trips), so a misconfigured target folder doesn't burn hours of bandwidth. Exits `4`; files the abort never reached are reported as `not_attempted`, not as errors. Set `0` to disable.\n- **Exit status a script can trust** (0.8.0+): `0` every queued file registered · `1` bad input\u002Fstartup · `2` auth expired · `3` partial (some files errored, unrecognized, or not attempted — a Ctrl-C'd run included) · `4` aborted by `--max-failures`. Before 0.8.0 a sync with failed files exited `0`. The JSON summary satisfies `created + duplicate + error + unrecognized + not_attempted == to_upload`.\n- **`--trust-manifest` limits**: only server-confirmed rows (`created`\u002F`duplicate`) enter the manifest, so failed files are retried. The flag skips the server-side name check, so a file deleted on the server after being recorded is never re-uploaded — drop the flag when the target folder may have changed.\n- **Pre-flight folder check**: `GET \u002Ffolders\u002F{id}\u002F` runs before walking, so an inaccessible target ID fails fast instead of mid-run. Retried with backoff — a transport hiccup on this call is not evidence the folder is missing.\n- **Resilient folder resolution**: folder creates and per-parent subfolder lookups run with a 180s floor (raised further by `--timeout`, never lowered) and retry on timeouts, 429s, and 5xx. The root-level listing on a project-root sync inherits the ordinary default rather than the floor, so `--timeout` is still worth setting there. Because folders are identified by (name, parent), a retry re-resolves by name first and adopts the folder if a timed-out create actually landed server-side, instead of making a duplicate. Auth\u002Fpermission\u002Fvalidation errors still fail on the first attempt. Resolved folders are checkpointed into `--manifest` (`{relative_dir → folder_id}`) and reused on the next run, so a failure part-way through a wide tree doesn't throw away the whole run's setup.\n- **Adaptive multipart part-size**: per-file `part_size` is bumped automatically if the file would exceed R2's 10000-part cap (relevant for files > ~160 GB at the 16 MB default).\n- **Time-based manifest checkpoints**: manifest flushes every 50 uploads *or* every 30s, whichever comes first — a crash mid-run loses at most ~30s of progress.\n- **Resume messaging**: when the manifest already has entries, the run prints \"Resuming from manifest: N files already recorded.\" up front.\n- **`--part-concurrency`** (default 4) decouples parts-per-file parallelism from files-in-flight parallelism, so 16 concurrent multipart files don't spawn 16 × 16 = 256 PUTs.\n- **`--limit N`** stops after queueing N files (post-filtering). Useful for graduated smoke tests before committing to a multi-hour run; the manifest is still written so the next run resumes exactly where this one stopped.\n\n**HTTP read timeout (`--timeout`, `$PCXA_HTTP_TIMEOUT`):** every API call defaults to 30s. That is too tight for write endpoints on large projects — folder creates and bulk mutations regularly run past it, and the client can't distinguish \"slow\" from \"dead\". `pcxa --timeout \u003Cseconds> ...` (accepted before or after the subcommand on `files sync` and `files purge`) raises it for every call that goes through the API client. It does **not** change the upload\u002Fdownload\u002Fpresign helpers, which call the transport directly with their own longer timeouts already baked in (60–600s depending on the operation) — an explicit value there wins over the default, so `--timeout` neither raises nor lowers them. Prefer raising the timeout over shrinking `--chunk` when a bulk call times out: the server usually finished the work, so an abort leaves a silent partial success.\n\n**Deletion convention:** `pcxa files delete \u003Cids>` marks files for deletion by applying the `to_delete` tag. Use `pcxa files restore \u003Cids>` to undo before cleanup runs. Without `--yes`, `delete` prompts for confirmation.\n\nSearch results include `url` fields — always show these to users for document links.\n\n**`--search` is fuzzy by default (`files list`, `files aggregate`, `activities list`).** Backend uses PostgreSQL trigram similarity: exact substring matches surface first (similarity ~1.0), then typo-tolerant matches ranked by similarity DESC, in a single paginated response. `concret` finds `Concrete Pour`; `0314` ranks `RFI-0314` above `Document-031499`. Pass `--exact` to opt back into tight substring matching (rejects typos — useful when the query is a known-correct identifier and you don't want fuzzy noise). **`--exact` is not a phrase match:** the words are still AND-matched separately, so `--search \"Case Memo\" --exact` also matches `Case Assessment Memorandum`. When you need a literal adjacent phrase in the filename, use `pcxa files query 'title:\"Case Memo\"'`. The backend rate-limits fuzzy search to 100\u002Fmin per user; not normally a concern for agent use.\n\n**`files query` response shape:** `{query, parsed, results, total_files, count_exact, limit, offset}`. `parsed` is the canonical interpretation of your expression — surface it to the user when the result set matters. `count_exact: false` means `total_files` is a floor (ceiling reached), not a total. Rows carry `file_id`, `file_name`, `file_type`, `folder_path`, and a `url`.\n\n**Search response shape:** `pcxa files search` returns `{query, total_results, results, hybrid_enabled}` — a top-N reranked list (server-capped at 50). Each row carries `score`, `file_id`\u002F`activity_id`, `file_name`\u002F`title`, `folder_path`, `page_number`, `chunk_position`, and a `url`. Hybrid means semantic similarity and keyword matching over the project's chunk text are combined and reranked into a single ordering — the same path the web UI's search bar uses.\n\n**Four ways to find files — pick by what you need:**\n- **`files list --search \u003Cterm>`** — matches the **title\u002Ffilename** only (trigram). Paginated + countable. Use when you know part of the name.\n- **`files list --content \u003Cterm>`** — matches the indexed **body\u002Fcontents** as a **literal substring** (not ranked, not semantic). Paginated + **countable and exhaustive**: `--content \u003Cterm> --count-only` gives the *exact* total and you can page through *every* match in a stable order (by `id`). `count: 0` means the term is genuinely in no in-scope file — so this is the path for a \"not located\" \u002F completeness finding. Use it to find files by what's inside them — e.g. an eDiscovery estate where emails are named by bare Bates numbers (`YATES002119058`) and the term only appears in the body. Only indexed files match; scope with `--folder`\u002F`--ext`\u002F`--index-status`.\n- **`files search \u003Cterm>` \u002F `files content \u003Cterm>`** — hybrid semantic + keyword **ranking** (relevance-ordered), a **top-50 reranked sample, not a total**. Best for natural-language \u002F \"most relevant\" lookups. Do **not** use it to count or enumerate — it can't page past 50, and asking for more (`--limit 200`) is clamped to 50 with a notice pointing you at `--content`. For \"how many \u002F list them all\", use `--content`.\n\n- **`files query '\u003Cexpr>'`** — **boolean** search: `AND` \u002F `OR` \u002F `NOT`, parentheses for grouping, `title:` \u002F `content:` field scoping, and `\"quoted phrases\"` matched adjacently. Structurally expressive where `--content` is a single literal. Use it whenever the question has more than one condition — *\"schedule in the title AND both precast and delay in the body\"* is one call: `pcxa files query 'title:schedule AND precast AND delay'`. Bare terms search content; operators must be UPPERCASE (lowercase `and`\u002F`or` are ordinary words). Every response echoes `parsed` — **read it** to confirm the query was understood before trusting the results.\n\n  > ⚠️ **Multi-term `AND` is being fixed PER PROJECT — and nothing in the response tells you which behaviour you got.**\n  >\n  > **Old behaviour, still live on most projects:** two or more CONTENT terms joined by `AND` are chunk-scoped and UNDER-REPORT. The match requires the terms to fall in the *same passage* (~3,000 characters), not merely in the same file, so `precast AND delay` misses files where the words appear pages apart. Measured: **1,042 returned where 3,496 match** (~70% missed); on another shape, **3,738 returned where 9,342 match** (~60% missed).\n  >\n  > **New behaviour, rolling out:** `AND` is file-scoped, the count is correct, and totals are more often exact rather than capped.\n  >\n  > **The trap: `count_exact: true` appears in BOTH cases.** The old path returns a wrong number *and* labels it exact whenever it lands under the ceiling. So `count_exact` does not distinguish them, and neither does anything else in the payload.\n  >\n  > **So do not rest a completeness or \"not located\" finding on a multi-term `AND` unless you have confirmed the fix is live for that project.** Otherwise:\n  > - Run each content term as its own `files query` (or `files list --content \u003Cterm>`, which IS exhaustive per term) and intersect the `file_id`s yourself. Slower, correct on every project.\n  > - Or ask which projects have the file-scoped fix enabled, and say so when you rely on it.\n  >\n  > Using `AND` for the stricter \"these words near each other\" reading stays legitimate — just describe it that way rather than calling it exhaustive.\n  >\n  > A single content term, `title:` predicates, `OR`, and `NOT` are unaffected. When the fix reaches a project, counts on existing `AND` queries **go up** — that is the correction, not a regression.\n\n`--search` and `--content` compose (title AND body) and combine with `--tags`, `--folder`, `--ext`, dates, etc. For anything beyond a plain AND of one title term and one body term, reach for `files query` instead.\n\n**`files query` limits (all reported, never silent):** at most 8 OR branches, 4 levels of nesting, 16 terms. A bare `NOT` is rejected — negation needs something positive to search within (`contract NOT draft`, not `NOT draft`). An `OR` whose branches are **two or more very common** content words is rejected as too broad with an actionable message; narrow one branch or run them separately. `count_exact: false` means a ceiling was hit and the number is a floor, not a total.\n\n**Paging through the matches (`--limit` \u002F `--offset`).** Every `... list` subcommand — `files list`, `activities list`, `forms list`, `submissions list`, `custom-objects list`, `resources list`, `cost-codes list`, `budgets list`, `timesheets list` — returns **one page at a time**, defaulting to 25 or 50 rows. **A bare `list` call is never the complete set.** To enumerate everything, get the total first, then walk it:\n\n```bash\npcxa files list --content \"IOCC-387\" --count-only              # {\"count\": 137} ← the real total\npcxa files list --content \"IOCC-387\" --limit 100 --offset 0    # rows 1–100\npcxa files list --content \"IOCC-387\" --limit 100 --offset 100  # rows 101–137\n```\n\nIncrement `--offset` by `--limit` until you've collected `count` rows (or a page comes back short). With `--content` the order is by `id`, so paging is exhaustive — exactly `count` distinct ids, no repeats or skips. Report the number from `--count-only`, never the length of one page.\n\n**After search → read in batch.** Once `files search` returns the rows, prefer `files batch-read --chunk file_id:chunk_position` over N single `files read` calls. Each row carries a `chunk_position` — pass `file_id:chunk_position` as `--chunk` to read just the relevant excerpt + neighbors. One round trip instead of N. Use `--outline` for section maps when files are large and you need to plan further reads.\n\nFor folder-scoped browsing, use `pcxa files list --folder \u003Cid>`.\n\n## Tags & Folders\n\n```bash\npcxa tags list                                                # all tags with counts\npcxa tags add 1 2 3 --tags urgent,review                      # add (preserves existing)\npcxa tags remove 1 2 --tags draft                             # remove specific tags\npcxa tags set 1 2 --tags final,approved                       # replace all tags\npcxa tags bulk --file plan.json                               # different tags per file, one request\npcxa files bulk-patch --file plan.json                        # per-file metadata plan (tags + title\u002Fcategory\u002Fdescription)\npcxa folders tree --depth 2                                   # hierarchy\npcxa folders create \"Contracts\" --parent 5                    # new folder\npcxa folders rename 5 \"Legal\"                                 # rename\npcxa folders move 5 --parent 10                               # reparent\npcxa folders contents 5                                       # subfolders + files (paginated; --timeout for slow folders)\npcxa folders subfolders 5                                     # lightweight [{id,name}] list (fast on large folders)\npcxa folders delete 5                                         # delete + all contents\npcxa move 10 11 12 --folder 5                                 # bulk move files\npcxa categorize 10 11 --category \"Submittal\"                  # bulk set category\npcxa files update 10 --title \"New\" --tags a,b --folder 5      # single file update\n```\n\n**Bulk tag\u002Fmetadata plans (`files bulk-patch`, `tags bulk`):** `tags add\u002Fremove\u002Fset` apply the *same* tag set to a list of file ids. When each file needs *different* tags (or you're patching metadata exported from a spreadsheet), use the server-side `files\u002Fbulk_patch\u002F` endpoint instead of one request per file — up to 500 rows per request, auto-chunked for larger plans, per-row validated so one bad row doesn't fail the batch. Output reports `patched`, `modified` (rows that actually changed), and `failed` (per-row errors).\n\n`files bulk-patch --file plan.json` takes a JSON list of rows (or `{\"changes\": [...]}`), each row `{file_id, ...}` setting any subset of `title`, `category`, `description`, `tags` (with optional `tag_mode` ∈ `set`|`add`|`remove`, default `set`):\n\n```json\n{\"changes\": [\n  {\"file_id\": 123, \"tags\": [\"reviewed\", \"urgent\"], \"tag_mode\": \"add\"},\n  {\"file_id\": 124, \"tags\": [\"legal\"]},\n  {\"file_id\": 125, \"title\": \"ACME-0001.pdf\", \"category\": \"Contracts\", \"description\": \"Q3 amendment\"}\n]}\n```\n\n`tags bulk --file plan.json` is the tag-only view of the same endpoint — rows are `{file_id, tags, tag_mode}` and scalar fields (title\u002Fcategory\u002Fdescription) are rejected with a pointer to `files bulk-patch`. `folder` is not patchable via either command (folder moves recompute privacy\u002Faggregates — use `pcxa move`). Empty `tags` in `set` mode is refused client- and server-side to prevent an accidental mass tag-wipe; use `tag_mode: remove` to clear specific tags. Add `--dry-run` to preview the plan without sending. Both are `bulk_patch`-backed; the older `tags add\u002Fremove\u002Fset` and `bulk_update` path is now set-based server-side, but unchanged for callers.\n\n## Activities\n\n```bash\npcxa activities list --status in_progress --priority 3,4\npcxa activities list --search \"foundation\" --assignee 5 --sort -due_date  # fuzzy by default; add --exact for tight\npcxa activities list --tags \"structural,review\" --tags-mode all  # AND mode\npcxa activities list --after 2026-03-01 --before 2026-03-31       # updated in date range\npcxa activities list --after last_month                          # relative dates supported\npcxa activities list --created-after 2026-01-01 --created-before 2026-03-31\npcxa activities list --assignee 5 --after 2026-03-01 --before 2026-03-31  # user's work in period\npcxa activities list --wbs 1.4.2                              # exact WBS path\npcxa activities list --wbs-branch 1.4                         # 1.4 and everything under it\npcxa activities list --wbs-branch 1.4 --status completed      # composes with any other filter\npcxa activities get 123                                       # detail + steps + deps\npcxa activities create --title \"Review\" --priority 3 --type 5 --assignees 1,2\npcxa activities create --title \"Pour slab\" --custom-fields '{\"3\":\"Acme Corp\"}'  # custom-object value, fuzzy-validated\npcxa activities update 123 --status completed --percent 100\npcxa activities update 123 --outcome \"Grain is one row per charge per register.\"\npcxa activities update 123 --outcome \"\"                       # clear a wrong outcome\npcxa activities update 123 --custom-fields '{\"3\":\"Acme Corp\"}' --no-fuzzy        # write value as-is\npcxa activities delete 123 456                                # bulk delete\npcxa activities bulk-update 1 2 3 --status in_progress\npcxa activities types                                         # list templates\npcxa activities related 123                                   # linked files\u002Ffolders\u002Fphotos\u002Fforms\n```\n\n**Statuses:** `not_started`, `in_progress`, `completed` | **Priority:** 0=none, 1=low, 2=med, 3=high, 4=critical\n\n**Descriptions support Markdown** (headings, tables, bold, lists). Keep descriptions focused on:\n- **Scope\u002Fobjective** — what the activity is and what it produces\n- **Requestor** — who asked for it, with reference to source communication (e.g., `file:247764`)\n- **Business justification** — why this work exists\n\nDo NOT put in descriptions: processing details, scripts, output file lists, status updates, or progress notes. Those belong in **comments** (`pcxa comments add`) as dated narrative entries.\n\n**`--outcome` is the answer; `--description` is the question.** Description is the brief — what was asked and why. Outcome is what the activity concluded or produced: the decision, the finding, the deliverable. It is *not* production or quantity (that is `--percent`), and it is deliberately not called \"output\", which in this codebase means measurable production. Put the conclusion here rather than burying it in the newest comment — outcome is semantically indexed, so \"what did we decide\" stays findable.\n\nEvery edit is kept in the activity's history, so revising an outcome loses nothing. `--outcome \"\"` clears it.\n\nTwo limits worth knowing before you plan around them. Both are lifted by PCX-Analytics\u002Fpcxa#2863, so check whether that has deployed before designing around them:\n- **`bulk-update` has no `--outcome`.** The server's bulk allow-list rejects the field. Update them one at a time.\n- **`activities list --search` does not read outcomes.** Server-side search covers title, description and WBS code only. The outcome *is* indexed for semantic search, so `pcxa chat send` can find it when `--search` cannot.\n\n**WBS filters:** `--wbs 1.4.2` matches that one activity exactly. `--wbs-branch 1.4` returns 1.4 plus its whole subtree — 1.4.1, 1.4.2.7, at any depth — but **not** 1.40 or 1.41, which merely share a digit prefix. Both are server-side and compose with every other filter, and a `wbs` column is added to the table so you can confirm the scope rather than trust it.\n\nRequires an API with the WBS filters (PCX-Analytics\u002Fpcxa#2863). Against an older API the CLI **aborts rather than printing**: django-filter silently ignores parameters it does not know, so an unpatched server answers `--wbs-branch 1.4` with the entire project and a 200. The CLI re-checks every returned row and refuses a result it can prove is out of scope — you get a loud error, never a project-wide list mislabelled as a branch.\n\n**Date filters:** `--after`\u002F`--before` filter by last updated; `--created-after`\u002F`--created-before` filter by creation date. Accepts `YYYY-MM-DD` or relative keywords: `today`, `last_7_days`, `this_month`, `last_quarter`, etc.\n\n**Name resolution:** `--assignee` and `--owner` accept user IDs or names. Names are fuzzy-matched against project members:\n- Exact\u002Fsubstring match → resolves automatically with confirmation message\n- Multiple close matches → lists candidates with IDs for you to pick\n- No match → suggests `pcxa project members` to list all\n\n**Custom-object fields:** `--custom-fields` takes a JSON map of `{custom_field_id: value}` (custom fields are defined at the project level). On `update` the map merges into the activity's current values, and `null` clears one. Values targeting a custom-object-backed field are fuzzy-validated the same way form submissions are — see **Custom Objects**. Pass `--no-fuzzy` to write the raw value.\n\n**Invoicing workflow:** Query a user's activity in a billing period by name — no `--status` filter needed since `--after`\u002F`--before` captures any work (started, progressed, or completed):\n```bash\npcxa activities list --assignee \"John\" --after 2026-03-01 --before 2026-03-31\n```\n\n### Related items (`activities related`)\n\nWhat is attached to an activity — files, folders, photos, form submissions and\nrecords — in **one call, in both directions**. This is the CLI view of the web\napp's \"Related items\" rail.\n\n```bash\npcxa activities related 123                       # everything linked, either direction\npcxa activities related 123 --types file,folder   # only files and folders\npcxa activities related 123 --all                 # follow cursors to the end\npcxa activities related 123 --limit 100           # rows per page\n```\n\nThe `dir` column says which way the link points: `->` the activity is the\nlink's source, `\u003C-` it is the target.\n\nPrefer this over two `links list` calls. Links point either way, so the flat\nendpoint needs one query for `--source activity:123`, another for `--target\nactivity:123`, and a merge; this is anchored on the activity and resolves the\nfar end of every edge for you. It also runs on the project-scoped route, where\nthe company and project permission gates actually fire.\n\n**It does not report a total, and that is deliberate** — an exact count would\nmean enumerating and permission-filtering the whole neighbourhood. So the row\ncount is what was fetched, never what exists. Three notices tell you when the\nview is partial, and all three go to stderr:\n\n- **`... cannot be resolved by this endpoint and are NOT listed above`** — a\n  link of that type exists on the activity but this endpoint cannot hydrate it.\n  **Drawings are the common case**: `links create --target drawing:N` works and\n  the web app makes them, but `related` cannot show them. Use\n  `pcxa links list --source activity:123` to see those edges.\n- **`the server stopped scanning on its own budget`** — more links may exist\n  beyond what was scanned.\n- **`More results. Use --all, or --cursor …`** — pagination is cursor-based;\n  `--all` follows it, stopping after 50 pages and printing the cursor to resume.\n\n## Steps (Subtasks)\n\nWhen steps exist, activity progress auto-calculates from weighted step completion.\n\n```bash\npcxa steps list 123                                           # list steps\npcxa steps create 123 --name \"Draft review\" --weight 40\npcxa steps update 123 45 --percent 100                        # mark step complete\npcxa steps delete 123 45                                      # weights rebalance\npcxa steps from-template 123                                  # create from type template\n```\n\n## Progress\n\n```bash\npcxa progress list 123                                        # progress timeline\npcxa progress add 123 --percent 50 --notes \"Halfway\"\npcxa progress add 123 --percent 25 --date 2026-03-15          # backdate\npcxa progress delete 123 789                                  # manual entries only\n```\n\n## Comments\n\n```bash\npcxa comments list 3712                                       # list comments on activity\npcxa comments add 3712 --content \"Drafting initiated...\"\npcxa comments delete 3712 2952                                # delete comment by ID\npcxa comments bulk 3712 --file comments.json                  # bulk add from JSON file\n```\n\n**Bulk JSON format** (`comments.json`):\n```json\n[\n  {\"content\": \"Drafting initiated per weekly report #12\"},\n  {\"content\": \"Internal review completed, minor revisions needed\"}\n]\n```\n\nSupports both bare list and wrapper object with `\"comments\"` key.\n\n## Dependencies (CPM)\n\nTypes: `FS` (finish-to-start), `SS`, `FF`, `SF`. Lag in days (negative = lead).\n\n```bash\npcxa deps list --predecessor 10\npcxa deps create --predecessor 10 --successor 20 --type FS --lag 2\npcxa deps delete 456\n```\n\n## Tag-Filter Links (evidence sets on an activity)\n\nAttach a **saved tag query** to an activity as one link that stands for a whole *set* of files — the AND\u002FOR combination a plain object link can't express. \"Pay apps for Yates\" = tags `pay_app` **and** `yates` in `all` mode. The set is **dynamic**: it resolves live to whatever files currently carry the tags (`pcxa files list --tags pay_app,yates --tags-mode all`), and the web app renders each link as a chip that deep-links into the Files tab with the filter pre-applied.\n\n```bash\npcxa tag-filters list 5080                                          # links on activity 5080\npcxa tag-filters add 5080 --tags pay_app,yates --mode all           # AND: files with BOTH tags\npcxa tag-filters add 5080 --tags rfi,submittal --mode any --label \"Open items\"  # OR (default)\npcxa tag-filters delete 5080 12                                     # remove link 12\n```\n\n`--mode all` requires **every** tag (AND); `--mode any` (default) matches **any** tag (OR) — same semantics as `files list --tags-mode`. Up to 20 tags per link; `--label` overrides the default `tag1 + tag2` display label. This is the CLI surface for the backend's `ActivityTagFilterLink` (nested under `activities\u002F{id}\u002Ftag-filter-links\u002F`). Use it instead of `links create --target …` when the evidence is defined by a tag combination rather than a fixed object.\n\n## Gantt & WBS Tree\n\n```bash\npcxa gantt --status in_progress\npcxa tree --max-depth 3\n```\n\n## Forms (Templates)\n\nForm templates define reusable forms with typed fields. Submissions are instances of a form.\n\n```bash\npcxa forms list --category Safety                     # list templates\npcxa forms list --scope project --search \"inspection\"\npcxa forms get 1                                      # detail + fields\npcxa forms create --name \"Safety Inspection\" --category Safety --code-prefix SI --code-padding 3\npcxa forms update 1 --description \"Updated desc\" --reviewers 3,5\npcxa forms delete 1\n```\n\n**Scope:** `project` (default) or `company`. **Code settings:** `code-prefix` (e.g. RFI), `code-scope` (project\u002Fcompany), `code-separator` (default: -), `code-padding` (3=001).\n\n## Fields (Form Fields)\n\nFields define the structure of a form template. Field values are referenced by field ID in submissions.\n\n```bash\npcxa fields list 1                                    # list fields for form 1\npcxa fields create 1 --label \"Inspector\" --type text --required --order 1\npcxa fields create 1 --label \"Severity\" --type select --options '{\"choices\":[\"Low\",\"Med\",\"High\"]}'\npcxa fields create 1 --label \"Date\" --type date --required --order 2\npcxa fields create 1 --label \"Vendor\" --type choice --choice-id 21      # custom-object-backed\npcxa fields update 1 5 --label \"New Label\" --required true\npcxa fields delete 1 5\n```\n\n**Field types** (exact set): `text`, `textarea`, `number`, `date`, `datetime`, `checkbox`, `select`, `radio`, `choice`, `table`, `photo`, `file`, `location`.\n- `select`\u002F`radio`: inline options via `--options '{\"choices\":[\"A\",\"B\"]}'`.\n- `choice`: backed by a **custom object** (field-choice) — bind with `--choice-id \u003Cobject_id>`; submission values are fuzzy-validated (see **Custom Objects**).\n- `table`: a repeating grid of typed columns — see **Table fields** below.\n\n### Table fields\n\nA `table` field holds a list of rows, each with the same typed columns. **The columns go in `table_schema` (a JSON array), NOT `--options`** — the CLI exposes a dedicated `--table-schema` flag for this. `--options` is silently accepted by the API but ignored by the forms UI, so a table defined via `--options` renders with no columns.\n\n```bash\npcxa fields create 1 --label \"Line Items\" --type table --required \\\n  --table-schema '[\n    {\"name\":\"item\",\"field_type\":\"text\",\"label\":\"Item\"},\n    {\"name\":\"qty\",\"field_type\":\"number\",\"label\":\"Qty\"},\n    {\"name\":\"unit\",\"field_type\":\"select\",\"label\":\"Unit\",\"options\":[\"ea\",\"lf\",\"sf\"]}\n  ]' \\\n  --min-rows 1 --max-rows 20\n```\n\n- Each column needs a **`name`** (the key used in submission values), a **`field_type`**, and a **`label`**. Column `name` keys must be stable — submission row values are keyed by them.\n- `--min-rows` \u002F `--max-rows` bound the row count (optional).\n- `pcxa fields list 1` and `pcxa forms get 1` show a table field's columns (`cols: item, qty, …`) and a choice field's binding (`custom-object 21`).\n\n## Submissions (Form Submissions)\n\nSubmissions are instances of a form template, with field values keyed by field ID.\n\n```bash\npcxa submissions list --form 1                        # list for a specific form\npcxa submissions list --status draft                  # filter by status\npcxa submissions get 1 42                             # detail (form_id submission_id)\npcxa submissions create 1 --code SI-001 --values '{\"1\":\"John\",\"2\":\"2026-03-24\",\"3\":\"High\"}'\npcxa submissions update 1 42 --values '{\"3\":\"Critical\"}' --merge --tags urgent  # change only field 3\npcxa submissions delete 1 42\n```\n\n**Statuses:** `draft`, `submitted`, `closed`. `--values` is a JSON object mapping **field ID → value**. The value shape depends on the field type:\n\n| Field type | Value shape | Example (`\"3\"` = field ID) |\n|---|---|---|\n| text\u002Fnumber\u002Fdate\u002Fselect\u002Fradio\u002Fchoice | scalar | `{\"3\":\"High\"}`, `{\"3\":42}` |\n| checkbox | boolean | `{\"3\":true}` |\n| table | **array of row objects keyed by column `name`** | `{\"3\":[{\"item\":\"Rebar\",\"qty\":50},{\"item\":\"Concrete\",\"qty\":12}]}` |\n\nFor a **table** field, each row is an object whose keys are the column `name`s from the field's `table_schema`. Example creating a submission with a 2-row table in field `7`:\n\n```bash\npcxa submissions create 1 --code SI-002 \\\n  --values '{\"7\":[{\"item\":\"Rebar\",\"qty\":50,\"unit\":\"lf\"},{\"item\":\"Concrete\",\"qty\":12,\"unit\":\"sf\"}]}'\n```\n\n> ⚠️ **The API does NOT validate value shapes.** It accepts a wrong shape (a scalar where a table is expected, rows keyed by the wrong names, a `{\"rows\":[…]}` wrapper, etc.) without error — but the forms UI then renders the field broken. Match the field type and the column `name`s exactly. Use `pcxa fields list \u003Cform_id>` to confirm a table field's columns before submitting.\n\n> ⚠️ **`update --values` replaces the entire values dict by default.** Passing a partial `--values` (e.g. just `{\"3\":\"Critical\"}`) clears every other field on the submission. Pass **`--merge`** (alias `--patch`) to update only the keys you supply, leaving the rest intact. Without `--merge` the CLI prints a one-line overwrite warning to stderr.\n>\n> **`--merge` works at field-ID granularity only — it cannot merge *within* a table value.** To change one cell or add one row, resend the field's **entire** row array: `pcxa submissions get` the current value, edit the array, and send it back under that field's key with `--merge`.\n\nValues targeting a custom-object-backed (`choice`) field are fuzzy-validated on create\u002Fupdate — see **Custom Objects** below. Pass `--no-fuzzy` to skip. (Custom-object columns *inside* a table row are not yet fuzzy-validated — verify those values against the object's options yourself.)\n\n## Custom Objects (Field Choices)\n\nCustom objects are reusable, typed lookup tables (`field-choices` in the API). A custom object defines a `property_schema` (its columns — a JSON **list** of `{\"name\", \"type\"}` objects) and holds **options** (rows), each with a `label` and a `properties` JSON of column values. Form fields can be backed by a custom object so submissions pick from its options. Objects exist at **project** (default) or **company** scope; a company object can be surfaced into a project with `extend`.\n\n```bash\npcxa custom-objects list                                    # project-scoped objects\npcxa custom-objects list --scope company --search vendor     # company-scoped, filtered\npcxa custom-objects get 5                                   # schema + first options\npcxa custom-objects create --name \"Vendors\" --schema '[{\"name\":\"code\",\"type\":\"text\"}]'\npcxa custom-objects create --name \"Trades\" --scope company --extensible true\npcxa custom-objects update 5 --description \"Approved vendor list\"\npcxa custom-objects extend 5                                # company object → current project\npcxa custom-objects delete 5\n```\n\n### Options (rows)\n\n```bash\npcxa custom-objects options list 5                          # rows of object 5\npcxa custom-objects options create 5 --label \"Acme Corp\" --properties '{\"code\":\"A1\"}'\npcxa custom-objects options update 5 42 --label \"Acme Corporation\" --order 1\npcxa custom-objects options delete 5 42\npcxa custom-objects options bulk-create 5 --file options.json   # [{\"label\":..,\"properties\":..}, ...]\npcxa custom-objects options reorder 5 --order \"42,17,9\"      # option ids in desired order\n```\n\nPass `--scope company` to any object\u002Foption command when the object is company-scoped.\n\n### Fuzzy matching (\"did you mean?\")\n\nWhen a value should reference a custom object, the CLI fuzzy-matches it against that object's options:\n\n```bash\npcxa custom-objects resolve 5 \"Acme Crp\"\n# → No match for 'Acme Crp'. Did you mean 'Acme Corp' (option 10)?\n```\n\n`resolve` exits non-zero when there is no confident match. **Form submissions and activity custom fields run the same check automatically:** on `submissions create`\u002F`update` with `--values` or `activities create`\u002F`update` with `--custom-fields`, any value targeting a custom-object-backed field is validated — an exact label\u002Fid or a unique substring resolves silently; anything else blocks the write with a \"Did you mean Y?\" suggestion. Pass `--no-fuzzy` to write the raw value. Validation reads the field definitions (form fields, or activity custom fields at `activities\u002Fcustom-fields\u002F`) and the object's options (trying project then company scope); a permissions or network error degrades to a warning and never blocks an otherwise-valid write.\n\n## Resources\n\nResources represent people, equipment, consumables, or subcontractors assigned to activities.\n\n```bash\npcxa resources list --type personnel --active true\npcxa resources get 1                                  # detail with rates\npcxa resources create --name \"John Smith\" --type personnel --user 5\npcxa resources create --name \"Crane #3\" --type equipment --unit hours --capacity 10\npcxa resources update 1 --name \"John D. Smith\" --active false\npcxa resources delete 1\n```\n\n**Types:** `personnel`, `equipment`, `consumable`, `subcontractor` | **Units:** `hours`, `days`, `each`, `lump_sum`\n\nOnly `personnel` resources can link to a user (`--user`). Use `--user 0` to unlink.\n\n## Rates (Resource Rates)\n\nAppend-only rate history. Create new rates with new effective dates — no updates\u002Fdeletes.\n\n```bash\npcxa rates list 1                                     # rates for resource 1\npcxa rates create 1 --effective-date 2026-04-01 --standard-rate 75 --cost-rate 50 --bill-rate 100\npcxa rates create 1 --effective-date 2026-04-01 --standard-rate 75 --cost-rate 50 --bill-rate 100 --overtime-rate 112.50\n```\n\n## Assignments (Resource → Activity)\n\n```bash\npcxa assignments list 123                             # list for activity 123\npcxa assignments create 123 --resource 5 --planned-units 40 --role \"Lead Engineer\"\npcxa assignments create 123 --resource 8 --planned-units 80 --curve front_loaded --driving\npcxa assignments update 123 45 --remaining 20 --at-completion 50\npcxa assignments delete 123 45\n```\n\n**Curves:** `uniform`, `front_loaded`, `back_loaded`, `bell`\n\n## Cost Codes (Company-Scoped)\n\nHierarchical cost tracking codes shared across all projects.\n\n```bash\npcxa cost-codes list --root-only\npcxa cost-codes get 1                                 # detail with children\npcxa cost-codes create --code \"03.300\" --name \"Cast-in-Place Concrete\"\npcxa cost-codes create --code \"03.310\" --name \"Structural\" --parent 1\npcxa cost-codes update 1 --active false\npcxa cost-codes delete 1\n```\n\n## Budgets (Cost Code Budgets)\n\n```bash\npcxa budgets list\npcxa budgets create --cost-code 1 --amount 50000 --units 200\npcxa budgets update 1 --amount 75000\npcxa budgets delete 1\n```\n\n## Timesheets\n\nTime\u002Fcost tracking per resource with approval workflow: `draft` → `submitted` → `approved` | `rejected` → `draft`.\n\n```bash\npcxa timesheets list --status draft --resource 5\npcxa timesheets list --after 2026-03-01 --before 2026-03-31\npcxa timesheets get 1                                 # detail with all entries\npcxa timesheets create --resource 5 --period-start 2026-03-24 --period-end 2026-03-30 --period-type weekly\npcxa timesheets update 1 --period-end 2026-03-31\npcxa timesheets delete 1\npcxa timesheets submit 1                              # draft → submitted\npcxa timesheets approve 1                             # submitted → approved\npcxa timesheets reject 1 --reason \"Missing entries\"   # submitted → rejected\npcxa timesheets reopen 1                              # rejected → draft\n```\n\n**Period types:** `weekly`, `biweekly`, `monthly`. Only draft\u002Frejected timesheets are editable.\n\n## Time Entries (nested under timesheets)\n\n```bash\npcxa entries list 1                                   # entries for timesheet 1\npcxa entries list 1 --date 2026-03-25 --activity 123\npcxa entries create 1 --activity 123 --date 2026-03-25 --hours 8\npcxa entries create 1 --activity 123 --date 2026-03-25 --hours 2 --type overtime --cost-code 5\npcxa entries update 1 42 --hours 6 --description \"Half day\"\npcxa entries delete 1 42\n```\n\n**Types:** `regular`, `overtime`, `double_time` | Hours: 0.01–24\n\n## Cost Entries (nested under timesheets)\n\n```bash\npcxa cost-entries list 1\npcxa cost-entries create 1 --resource 8 --activity 123 --date 2026-03-25 --quantity 5 --unit-cost 200\npcxa cost-entries update 1 42 --quantity 10\npcxa cost-entries delete 1 42\n```\n\n`total_cost` auto-computed: `quantity × unit_cost`.\n\n## Time Period Locks\n\nLock date ranges to prevent edits.\n\n```bash\npcxa locks list\npcxa locks create --period-start 2026-03-01 --period-end 2026-03-15 --reason \"Month closed\"\npcxa locks delete 1\n```\n\n## Links (Entity Links)\n\nConnect any two objects (files, activities, photos, drawings) with contextual descriptions. Object references use `type:id` format.\n\n**Types:** `activity`, `file`, `folder`, `photo`, `drawing`, `source_document`, `markup`, `project`, `company`, `formsubmission`, `fieldchoiceoption`\n\nAn unknown type is rejected locally, with the valid set and a \"did you mean?\"\nsuggestion, instead of costing a round-trip and coming back as a bare 400.\n\nTo read an activity's links, prefer `pcxa activities related \u003Cid>` — one call,\nboth directions, far ends resolved. Note the two type sets differ: every type\nabove is *linkable*, but `related` can only resolve `activity`, `file`,\n`folder`, `photo`, `formsubmission` and `fieldchoiceoption`. Drawing links are\ncreatable and visible to `links list`, but `related` reports them as omitted\nrather than showing them.\n\n```bash\npcxa links list --source file:170106                  # links from a file\npcxa links list --target activity:3710                # links to an activity\npcxa links list --source file:170106 --target activity:3710  # specific pair\npcxa links create --source file:170106 --target file:170107 --type attachment\npcxa links create --source activity:3710 --target file:170106 --type deliverable\npcxa links create --source file:170129 --target file:170100 --type \"supersedes\" --description \"Corrected analysis\"\npcxa links delete 42\npcxa links bulk --file links.json                     # bulk create from JSON\n```\n\n**Bulk JSON format** (`links.json`):\n```json\n[\n  {\"source\": \"file:170106\", \"target\": \"file:170107\", \"type\": \"attachment\"},\n  {\"source\": \"activity:3710\", \"target\": \"file:170106\", \"description\": \"RFI analysis deliverable\"},\n  {\"source_type\": \"file\", \"source_id\": 170129, \"target_type\": \"file\", \"target_id\": 170100, \"type\": \"supersedes\"}\n]\n```\n\nThe `--type` flag sets the description field (the backend doesn't enforce typed relationships). Use `--description` for longer context. Both shorthand (`\"source\": \"file:123\"`) and explicit (`\"source_type\": \"file\", \"source_id\": 123`) formats work in bulk JSON.\n\n`links bulk` calls the server-side bulk endpoint (up to 500 links per request, auto-chunked for larger files) instead of one request per link — rows are validated independently server-side, so one bad row doesn't fail the batch. Output reports `created`, `exists` (already-present links, not an error), and `failed` (per-row errors with the row index).\n\n## AI Chat\n\nSend messages to the project's AI assistant and read back its replies. Useful for ad-hoc probing and for letting an agent evaluate chatbot response quality. Project-scoped — uses the `.pcxa` company\u002Fproject.\n\n```bash\npcxa chat send \"What is the status of the foundation work?\"          # uses current conversation, waits for reply\npcxa chat send \"...\" --new --title \"Eval run 1\"                      # fresh conversation\npcxa chat send \"...\" --conversation 123                              # continue an existing thread\npcxa chat send \"...\" --research                                      # enable file-search tools (research_mode)\npcxa chat send \"...\" --model gemini-2.5-pro                          # override model (see chat models)\npcxa chat send \"...\" --no-wait                                       # fire-and-forget; returns agent_task_id\npcxa chat send \"...\" --timeout 300                                   # polling timeout (default 180s)\n\npcxa chat ls --search \"RFI\"                                          # list conversations\npcxa chat get [ID]                                                    # show full transcript (default: current)\npcxa chat get 123 --show-tools                                        # include tool calls\npcxa chat new --title \"Probe\"                                         # create empty conversation\npcxa chat delete 123                                                  # soft-delete (archive)\npcxa chat models                                                      # list available models\n```\n\n**Default JSON output** (from `chat send`):\n```json\n{\n  \"conversation_id\": 42,\n  \"agent_task_id\": 9001,\n  \"agent_task_status\": \"completed\",\n  \"elapsed_seconds\": 14.2,\n  \"timed_out\": false,\n  \"user_message\": {\"id\": 500, \"content\": \"...\"},\n  \"assistant_message\": {\n    \"id\": 501, \"role\": \"assistant\", \"content\": \"...\",\n    \"tool_steps\": [...], \"thinking_steps\": [...], \"action_cards\": [...]\n  }\n}\n```\n\n**How it works:** `chat send` submits a message, waits for the platform's assistant task to finish, and returns the assistant response. Exit code 2 means the task failed. `--no-wait` skips polling and returns the task ID immediately.\n\n## Audit Log (company admins)\n\nWho opened, downloaded or was shown which files. **Company-scoped** (needs no project; `--company` overrides the profile\u002F`.pcxa` company) and readable by **company admins only** — anyone else gets a 403.\n\n```bash\npcxa audit list --user jane@example.com --since 2026-09-01 -f table    # one person\npcxa audit list --type file.presign.issued --since 2026-09-14           # downloads and previews\npcxa audit list --search \"site plan\" --until 2026-09-13 -f table        # label \u002F username \u002F event contains\npcxa audit list --user jane@example.com --csv jane.csv                  # every matching event to a spreadsheet\npcxa audit history 16457660 --csv file-16457660.csv                     # one file's full history, oldest first\npcxa audit history 812 --resource-type folder.Folder                    # any other resource\n```\n\n| Event type | Meaning |\n|---|---|\n| `files.get` | a file's record opened in the app or API |\n| `file.presign.issued` | file bytes handed out: a download or in-app preview |\n| `file.bulk_download.entry` | a file included in a bulk download |\n| `file.search_excerpt` | file text shown in a search result or to the assistant |\n| `file.ai_read` \u002F `file.ai_viewed_pages` \u002F `file.ai_quoted` | the assistant read, looked at, or quoted the file |\n| `file.email_preview` | an email previewed |\n| `file.container_entry` | a file opened inside a zip or mailbox |\n\nNot exhaustive: `event_type` is free text server-side. Assistant rows carry `is_ai_agent` and `conversation_id` (in `details`).\n\n**Read the output correctly:**\n\n- **`--user` takes an email.** The log stores an internal username that is *usually* the email prefix but gets a numeric suffix on collision (`jdoe`, `jdoe1`), so a guessed prefix can return nobody or someone else. An email is resolved against current company members; for a former member, pass the exact username (find it with `--search`).\n- **Dates are UTC.** `--since` is inclusive; `--until YYYY-MM-DD` includes that whole day. The server has no date filter, so the CLI pages newest-first and stops at `--since` — a wide window on a busy company reads many pages.\n- **`list` stops at 500 rows on screen**, and says so on stderr when more exist; `--csv` exports everything unless you pass `--limit`.\n- **An empty `history` is not proof of no access.** If the company has chain-of-custody auditing disabled, the CLI warns on stderr and JSON shows `\"audit_enabled\": false`. A file's history also includes folder downloads that carried it.\n- **`history` is rate-limited to 10 requests\u002Fhour per user**, shared with audit verification (one request per 1000 events). If the limit hits mid-history the CLI exits 1 and writes no partial file.\n- The CSV keeps `sequence_number` (the ledger's chain position) and the full `details` JSON.\n\n## Reporting API errors\n\nWhen a PCXa CLI command returns a non-2xx API response, surface the endpoint, status code, and response body to the user. Do not invent alternate data paths; ask the user how they want to proceed.\n\n## Invocation\n\n`\u002Fpcxa $ARGUMENTS` → `pcxa $ARGUMENTS` (if installed via pipx) or `python .claude\u002Fskills\u002Fpcxa\u002Fpcxa.py $ARGUMENTS`\n","# PCXA CLI\n\n**Tool:** `pcxa`\n\nAll commands output JSON by default. Use `-f table` for human-readable. Use `--dry-run` on write operations. Run `pcxa \u003Ccommand> --help` for full options.\n\nWhen installed as a Claude Code plugin, `pcxa` is provided by the plugin `bin\u002F` directory. For direct terminal use outside Claude Code, ask the user to install it once with:\n```\npipx install git+https:\u002F\u002Fgithub.com\u002FPCX-Analytics\u002Fpcxa-skill.git\n```\nThen `pcxa update` self-upgrades from GitHub. The CLI prints a one-line notice to stderr (max once per 24h) when a newer release is available.\n\n\n## Setup & Authentication\n\nAlways start by running `pcxa whoami` to see the current state. The output tells you whether the user is authenticated and whether a project is set. Only run setup steps that are actually missing.\n\n### Step 1 — Authentication (drive it yourself, don't punt to a terminal)\n\nIf `whoami` says \"No profiles configured\" or a command fails with \"Profile not found\":\n\n```bash\npcxa login --no-setup\n```\n\nThe `--no-setup` flag is required when you (the agent) drive login: it skips the interactive company\u002Fproject picker that reads stdin, which would otherwise hang. The CLI prints two lines immediately:\n\n```\nOpening browser to authenticate...\n  If your browser does not open automatically, visit:\n  https:\u002F\u002Fwww.pcxa.app\u002Fauth\u002Fcli-auth?port=PORT&state=STATE\n```\n\n**Read those lines from stdout, then surface the URL to the user as a clickable markdown link.** The CLI tries `webbrowser.open()` but that's usually a no-op in WSL\u002Fheadless — the user opening the link manually is the normal path. They sign in (MFA\u002FSSO supported), the page redirects to a localhost callback, the CLI captures tokens, the command exits.\n\nRun with a generous bash timeout (e.g., 180s) since the user may take a moment to sign in. The CLI's own `--timeout` defaults to 120s; pass `--timeout 300` if you want longer.\n\nIf browser login isn't viable (rare — only if the host can't reach `pcxa.app`), fall back to password login: `pcxa setup -u USER_EMAIL` (prompts for password — only works if the user can run it themselves).\n\n### Step 2 — Pick a project\n\nAfter login (or if `whoami` shows `Project: not set`), drive the project picker yourself:\n\n```bash\npcxa projects -f table       # list all (company_id, project_id) the user has access to\n```\n\nShow that list to the user, ask which project they want, then:\n\n```bash\n# When the conversation\u002FCWD is inside a repo that maps 1:1 to a PCXA project,\n# pin it locally so future runs in this repo are auto-scoped:\npcxa set-project PROJECT_ID --company COMPANY_ID --local\n\n# Otherwise, set the global default for this user:\npcxa set-project PROJECT_ID --company COMPANY_ID\n```\n\nAlways pass `--company` when picking — `--local` writes a `.pcxa` file in CWD; without `--local`, the choice is saved in the global profile.\n\n### How resolution works\n\nProject scope resolves in this order: **repo `.pcxa` file** > **active profile default**. `.pcxa` is committed (no secrets — just `{ \"company\": 4, \"project\": 10, \"user\": \"alice@example.com\" }`). Different repos can pin different accounts via the `user` field; the CLI matches it against profile usernames in the active credentials file.\n\nCredentials resolve **folder-first**: a `.pcxa-credentials.json` found by walking up from CWD is used for both reads and writes (token refresh included); otherwise the global `~\u002F.pcxa\u002Fcredentials.json` is used. So `pcxa login` from inside a repo writes that repo's own `.pcxa-credentials.json` (at the git root, gitignored) and can't clobber another repo's tokens — pass `--global` to write the shared global file instead. Pre-0.3 global creds at `~\u002F.file_explorer\u002Fconfig.json` are auto-migrated to `~\u002F.pcxa\u002Fcredentials.json` on first run.\n\nState to surface to the user on the first turn: `whoami` shows `Active profile`, `User`, `Company`, `Project` (with `(from .pcxa)` annotation when applicable), `Creds:`, and `Repo pin:`. If you ran setup steps, echo the resulting scope back (\"Operating on project Acme Tower (4)\") so the user can correct you before any writes.\n\n## Project Metadata\n\n```bash\npcxa project get                                              # view project details\npcxa project members                                          # list members (name → user ID)\npcxa project members --search \"John\"                          # search by name\u002Fusername\npcxa project update --name \"New Name\"                         # update name\npcxa project update --description \"...\" --scope-statement \"...\" # set description & scope\npcxa project update --code \"T-FAB1\" --industry \"Construction\" # set code & industry\npcxa project update --start-date 2025-12-03 --end-date 2026-08-10\npcxa project update --rollup-method equal                     # equal|duration|cost|labor\npcxa project update --progress-input-method percentage        # status|percentage\n```\n\nFields: `name`, `code` (max 20), `description`, `scope-statement`, `industry`, `life-cycle`, `start-date`, `end-date`, `progress-input-method`, `rollup-method`.\n\n## File Search & Reading\n\n```bash\npcxa files list --ext PDF --search \"keyword\" --limit 50  # title trigram (fuzzy by default; exact first)\npcxa files list --search \"concret\" --exact               # tighter title match (still AND-matches words, NOT a phrase)\npcxa files query 'title:schedule AND precast AND delay'  # BOOLEAN: AND\u002FOR\u002FNOT + grouping (⚠ multi-term AND under-reports on projects without the fix — see below)\npcxa files query 'title:report AND (delay OR \"change order\")'   # grouping + quoted phrase\npcxa files query 'contract NOT draft' --ext PDF          # exclusion (impossible with --search\u002F--content)\npcxa files query 'title:\"Case Memo\"' --count-only        # true LITERAL phrase on the filename\npcxa files list --content \"IOCC-387\"                     # literal substring in file BODY — every match, paginated (stable id order)\npcxa files list --content \"IOCC-387\" --count-only        # exact total of content matches (count:0 = genuinely not located)\npcxa files list --tags \"urgent,review\" --tags-mode all   # AND: files with ALL tags\npcxa files search \"natural language query\"                    # hybrid keyword + semantic (same endpoint as the web UI)\npcxa files search \"query\" --scope file,activity               # restrict source types (csv: file,activity,drawing,photo)\npcxa files search \"query\" --ext PDF --limit 25                # narrow to file type, page-size up to 50\npcxa files content \"BRG report\" --ext PDF                     # alias of `files search` scoped to files (same hybrid endpoint)\npcxa files read FILE_ID --outline                             # section map\npcxa files read FILE_ID                                       # first 5 chunks\npcxa files read FILE_ID --start 5                             # next window\npcxa files batch-read 423 511 612 --window 3                  # multi-file in one call\npcxa files batch-read --chunk 423:7 --chunk 511:12            # excerpts at specific chunks\npcxa files batch-read 423 511 --outline                       # outlines, multi-file\npcxa files info FILE_ID                                       # metadata + versions\npcxa files stats                                              # project-wide counts\npcxa files aggregate file_type                                # group by dimension\npcxa files recent --limit 30\npcxa files download FILE_ID                              # download to current dir\npcxa files download FILE_ID -o report.pdf                # custom output path\npcxa files upload \u002Fpath\u002Fto\u002Ffile.pdf --folder 5 --title \"Report\" --tags \"final,2026\"\npcxa files upload \u002Fpath\u002Fto\u002Fdir\u002F --folder 5               # bulk upload all files in dir (flat)\npcxa files sync \u002Fpath\u002Fto\u002Ftree --folder 5                 # recursive mirror; creates subfolders; idempotent\npcxa files sync \u002Fpath\u002Fto\u002Ftree --folder 5 --manifest .pcxa-sync.json   # persist upload log for fast re-runs\npcxa files sync \u002Fpath\u002Fto\u002Ftree --folder 5 --include \"*.pdf\" --exclude \"draft_*\" --concurrency 16\npcxa files delete 123 124 --yes                          # mark for deletion (adds 'to_delete' tag)\npcxa files restore 123 124                               # remove 'to_delete' tag (undo)\npcxa files list --tags to_delete                         # list everything pending deletion\npcxa files set-index-mode 1 2 3 --mode none              # stop server-side indexing (BYOC prep)\npcxa files upload-chunks corpus.jsonl --manifest .pcxa-sync.json   # supply your own chunks + vectors\npcxa files upload-ocr ocr.jsonl --manifest .pcxa-sync.json --validate-only  # dry-run your own OCR\npcxa files upload-ocr ocr.jsonl --manifest .pcxa-sync.json         # supply your own OCR (text + boxes)\n```\n\n**Upload storage:** Small files are uploaded through the API. Larger files use a presigned upload flow handled by the CLI and API.\n\n## Bring your own chunks (`files upload-chunks`)\n\nFor a caller that runs its **own** extraction \u002F chunking \u002F embedding pipeline and\nwants PCXA to serve *its* index instead of re-deriving one. Files must exist\nfirst — chunks attach to them.\n\n> **If what you have is OCR page text, you want [`files\n> upload-ocr`](#bring-your-own-ocr-files-upload-ocr) instead.** Sending OCR\n> through this command succeeds — and silently discards your bounding boxes and\n> opts those files out of every future chunker improvement, because the rows are\n> marked `chunk_source=external` and are never re-derived.\n\n```bash\n# 1. upload the files (idempotent, resumable, records file_ids in the manifest)\npcxa files sync .\u002Fcorpus --folder 42 --manifest .pcxa-sync.json\n\n# 2. stop our chunker touching them (optional but recommended — see below)\npcxa files set-index-mode 101 102 103 --mode none\n\n# 3. supply chunks + embeddings\npcxa files upload-chunks .\u002Fchunks\u002F --manifest .pcxa-sync.json --state .pcxa-chunks.json\n\n# validate the whole corpus without sending anything\npcxa files upload-chunks .\u002Fchunks\u002F --dry-run\n```\n\n**Input is JSON-Lines, one record per file, streamed** — a multi-million-chunk\ncorpus never has to fit in memory. Pass `.jsonl` files or directories of them.\n\n```json\n{\"file_id\": 123, \"chunks\": [\n  {\"chunk_index\": 0, \"content\": \"...\", \"embedding\": [768 floats]},\n  {\"chunk_index\": 1, \"content\": \"...\", \"embedding\": [768 floats]}\n]}\n```\n\nPer-chunk optional keys: `content_hash` (sha256 hex), `page_number` (1-based),\n`metadata` (object). Per-file optional keys: `file_version_id`,\n`indexed_content_hash`, `document_summary`, `document_context_strategy`.\n\nInstead of `file_id` a record may carry `path` or `name`, resolved through\n`--manifest` against the manifest a prior `files sync` wrote — so the two\ncommands compose without you keeping your own id table. A filename that maps to\nmore than one file is **rejected, not guessed** (it would overwrite the wrong\ndocument's index); address those by `path` or `file_id`.\n\n### Embeddings\n\nSupply `embedding` on every chunk of a file and that file is marked **INDEXED** —\nour embedder never runs on it, and you pay nothing for embedding. Omit them\nentirely and the file lands **CHUNKED** for our embedder to pick up (billable,\nbut valid — useful if you have good chunk boundaries but no vectors).\n\n**Embeddings are all-or-nothing per file.** A file where only some chunks carry a\nvector is rejected client-side, because the server would demote the whole file to\nCHUNKED and re-embed it — silently costing you the thing you were avoiding.\n\nVectors must be exactly **768 dimensions** from `gemini-embedding-001`\n(`--embedding-model` to override, but the server requires an exact match). This\nis checked hard on purpose: every vector in the index is 768-dim, so another\n768-dim model's output validates fine and then simply retrieves badly, forever,\nwith nothing to detect it afterwards.\n\nYou do **not** need to supply identifier metadata — the server extracts\nRFI\u002FPCO\u002FCO\u002FNCR-style references from the `content` you send.\n\n### Pacing: a client-side default you can safely raise\n\n`--chunks-per-hour` defaults to **60,000**, which keeps a long backfill\ncomfortably under the endpoint's limit of **30 requests\u002Fminute per user**. This\nis client-side pacing only. It is **not** a durability constraint, and nothing\ndownstream requires the upload to be slow.\n\nRaise it for a large corpus, or pass `--chunks-per-hour 0` to upload as fast as\nthe rate limit allows — at the 5,000-chunk maximum per request that is a great\ndeal of headroom. Either way the client retries `429`s honouring `Retry-After`,\nand once a chunk is accepted the server is responsible for storing it durably;\nthere is no follow-up step for you to run.\n\n### Why `set-index-mode --mode none` first\n\nWithout it, our own chunker processes every file before your chunks arrive. That\nis *harmless* — your upload replaces whatever it produced, and the guards below\nthen protect it — but it is pure waste at corpus scale. `--mode none` skips it\nentirely; your upload still creates the index row.\n\n### After a successful upload, the server leaves your chunks alone\n\n- Server-side reprocessing will not re-chunk the file: retries and re-runs leave\n  your chunks in place.\n- Index-policy changes — including a project left at the default `selective` —\n  neither delete your chunks nor strip their vectors.\n- Uploading a **new version** of the file keeps your chunks and marks the index\n  **stale**: search keeps returning the *older* text until you re-upload for the\n  new version.\n\n### Operational notes\n\n- **Auth: project admin or company admin.** Chunk upload replaces a file's whole\n  indexed content, so it takes the same gate as every other bulk file operation.\n  Plain project membership gets `403`. Give an integration a service account with\n  project-admin on each target project.\n- **Resume:** `--state \u003Cpath>` records applied file ids; a re-run skips them.\n  Ctrl-C saves state before exiting.\n- **Batching** is automatic against the server caps — 50 files and 5,000 chunks\n  per request, 2,000 chunks per file, 16,000 chars per chunk. Values above the\n  caps are clamped rather than rejected.\n- **`--dry-run` validates everything** — dimensions, partial embeddings,\n  duplicate `chunk_index`, oversized content — without spending a request. Run it\n  over the full corpus before the real load.\n- **Failures are per-item.** One bad `file_id` doesn't abort the batch; bad\n  records are counted and logged (`--error-log` for one JSON line each).\n  `--max-failures` (default 100) aborts a run that is clearly misconfigured.\n- **Rate limits** (`429`) are retried honouring `Retry-After`.\n- **Exit code is non-zero** if anything failed, so a driving script can tell.\n\n## Bring your own OCR (`files upload-ocr`)\n\nFor a caller who **scanned the documents themselves**. You send page text —\nideally with word- and line-level bounding boxes — and PCXA treats it as the\nfile's authoritative text: it chunks it, extracts identifiers, indexes it for\nboolean search and embeds it, exactly as it would text from its own OCR\nprovider.\n\n### Which of the two upload commands you want\n\n**This is the most common mistake with these two endpoints, and nothing tells\nyou afterwards, because both succeed.**\n\n| | `upload-ocr` | `upload-chunks` |\n|---|---|---|\n| You have | page **text** from a scan | finished **chunks**, usually embedded |\n| Who chunks | **the server** | nobody — it stores what you send |\n| Geometry | preserved at full precision | no field for it |\n| Row marked | ordinary — re-derivable | `chunk_source=external`, never re-derived |\n| After acceptance | chunking → embedding, async | nothing |\n\nSend OCR through `upload-chunks` and you get a 200 and a searchable file — and\nyou have silently thrown away every bounding box and opted those files out of\nevery future chunker improvement. If you OCR'd a scan, use `upload-ocr`.\n\n(On a bring-your-own-chunks project `upload-ocr` refuses outright with\n`project_external_chunks_only`: those projects were promised the server never\nderives text server-side.)\n\n### Start with `--validate-only`\n\n```bash\n# 1. the dry run — writes nothing, reports refusals grouped by error_code\npcxa files upload-ocr .\u002Focr\u002F --manifest .pcxa-sync.json --validate-only\n\n# 2. the real load, resumable\npcxa files upload-ocr .\u002Focr\u002F --manifest .pcxa-sync.json --state .pcxa-ocr.json\n```\n\nThe dry run is **server-side**, which is the point: it reports envelope shape\nerrors *and* the state-machine refusals a client cannot see — whether each file\nis parked, already indexed, has an OCR batch in flight, belongs to a\nbring-your-own-chunks project. For a six-figure run that is the difference\nbetween discovering 8,000 unusable targets before scanning and after.\n\nIt exits **0** when the run completed: refusals are its *output*, not its\nfailure, so `validate && upload` is scriptable. Only client-side rejections and\ntransport failures make it non-zero. `--dry-run` is an accepted alias.\n\n### Input\n\nJSON-Lines, one record per file, streamed — a corpus never has to fit in memory.\n\n```json\n{\"file_id\": 123, \"envelope\": {...}}\n{\"path\": \"Sources\u002FRFI-142.pdf\", \"overwrite\": false, \"envelope\": {...}}\n```\n\nPer-record optional keys: `overwrite`, `file_version_id`, `provenance`. As with\n`upload-chunks`, `path`\u002F`name` resolve through `--manifest`, and an ambiguous\nfilename is rejected rather than guessed.\n\nThe `envelope` is one of **two shapes, and that is the whole set** — a closed\nunion. Do not invent a third, and do not send a layout envelope with null or\nfabricated boxes.\n\n**`pcxa.ocr_layout.v1`** — text with geometry. All coordinates 0–1 normalized,\norigin top-left. Positional arrays because this repeats per word across a corpus.\n\n```json\n{\"schema\": \"pcxa.ocr_layout.v1\",\n \"provenance\": {\"producer\": \"mxi-scanner\u002F1.4\", \"tool_version\": \"1.4.0\"},\n \"pages\": [{\"n\": 1, \"w\": 2550, \"h\": 3300,\n            \"lines\": [{\"t\": \"REINFORCED CONCRETE SLAB\",\n                       \"b\": [0.08, 0.10, 0.92, 0.14],\n                       \"c\": 0.97,\n                       \"words\": [[\"REINFORCED\", 0.08, 0.10, 0.24, 0.14, 0.99]]}]}]}\n```\n\n**`pcxa.text.v1`** — no geometry. Fully supported, and better than fabricated\nboxes; you lose only future geometry-dependent features, not retrieval.\n\n```json\n{\"schema\": \"pcxa.text.v1\",\n \"provenance\": {\"producer\": \"mxi-scanner\u002F1.4\"},\n \"pages\": [{\"n\": 1, \"text\": \"...\"}]}\n```\n\n**Send full precision — do not round.** The stored copy is the master and\neverything downstream is re-derived from it, so rounding is irreversible loss\ntaken to save storage that costs about $0.015\u002FGB-month.\n\n### `202` does not mean searchable\n\n**This is the one that gets reported as a bug.** Acceptance means the text is\ndurably stored. Chunking and embedding happen afterwards on a background queue,\nso the response cannot tell you the file is searchable and this command's exit\ndoes not mean \"indexed\". Read `pcxa files info \u003Cid>` \u002F the index status for that.\n`upload-chunks` returns a terminal state; this does not.\n\n### Refusals are per-item, and most are not retryable\n\nRefused files are reported and skipped — retrying them only burns the rate\nlimit. The end-of-run report is a **histogram by `error_code`**, not a wall of\nper-file lines (only the first 20 are echoed; `--error-log` gets every one).\n\nNot retryable: `index_parked` · `already_indexed` · `external_chunk_source` ·\n`project_external_chunks_only` · `index_excluded_by_policy` ·\n`file_version_mismatch` · `incomplete_page_coverage` · `has_server_text` ·\n`has_ocr_text` · `derivation_pending` · `not_found`.\n\nTransient, and worth retrying: `ocr_batch_in_flight` · `chunking_in_progress`.\nThese are deliberately **not** recorded in `--state`, so re-running with the\nsame state file retries exactly those.\n\nTwo will confuse a caller, so the CLI explains them inline:\n\n- **`index_parked` is a decision, not an error.** Someone excluded that file\n  from indexing on purpose, and this endpoint clears **no** park reasons. A\n  whole folder coming back parked means that folder is excluded deliberately.\n- **`incomplete_page_coverage`** only happens on a partially-OCR'd file, and\n  names the missing pages. Your upload must cover every page the server still\n  needs; it then stores only those, so text already extracted from the good\n  pages survives.\n\n`warnings` are advisory and are **not** failures. The common one — a word box\noutside its line box — is legitimate for rotated text.\n\n### Operational notes\n\n- **Auth: project admin or company admin**, same gate as `upload-chunks`. Give\n  the integration its own service account: every applied file is written to the\n  audit log with the acting user.\n- **Client-side validation mirrors the server** so a bad payload fails before it\n  costs a request: closed schema union, 1-indexed unique page numbers, bboxes\n  with `x1 > x0` \u002F `y1 > y0` and all coords in `[0,1]`, positive `w`\u002F`h`, and at\n  most 1,000 pages per file. `\"lines\": []` is a **legal blank page**; an empty\n  `pages` array is not.\n- **Confidence is optional, and `0` is rejected.** `0` asserts the text is\n  certainly wrong — it is a common exporter default, and a corpus carrying it\n  tells the ranker to distrust every line. Omit the field instead.\n- **Batching packs by bytes *and* file count** — 50 files\u002Frequest and a 10 MB\n  body cap, where the body cap is a `413` raised *before* the body is parsed, so\n  nothing in the response would say which file was responsible. `--max-bytes`\n  (default 9 MB) is the byte budget. A single file whose envelope exceeds it is\n  rejected client-side: an envelope is that file's text master and cannot be\n  split across requests.\n- **Pacing** `--pages-per-hour` defaults to 250,000. The endpoint allows **120\n  requests\u002Fminute** — its own throttle scope, *not* `upload-chunks`' 30\u002Fmin.\n  Client-side smoothing only: raise it, or pass `0` to disable. `429`s are\n  retried honouring `Retry-After` either way.\n- **Retries need no idempotency key.** Dedup is content-addressed and computed\n  server-side; a re-post of byte-identical content for the same file and version\n  returns `\"deduplicated\": true` and writes nothing new. Do **not** add an\n  `Idempotency-Key` header — the client cannot reproduce the server's canonical\n  JSON, so a client-computed key would be wrong in both directions.\n- **`--overwrite`** (default off) displaces text a file already has; a\n  per-record `\"overwrite\"` key wins over the flag. It never overrides\n  `already_indexed` or `index_parked`.\n- **Resume** with `--state`, exactly as `upload-chunks` does.\n\n**Not in scope:** producing the OCR. This command transports what a scanner\nemits.\n\n**Bulk tree sync (`files sync`):** Mirrors a local directory tree under a PCXA folder. Walks the tree, creates any missing subfolders to match, and uploads files in parallel via the same presign+PUT\u002Fmultipart path as `files upload`. Idempotent two ways: it lists each target folder once and skips local files whose name already exists there, and an optional `--manifest \u003Cpath>` persists `{relative_path → {size, file_id}}` so re-runs skip without hitting the API. Failures (network, register errors) are listed at the end and counted toward `error` rate. Progress is rendered live on stderr: a bar plus files-done, bytes-done\u002Ftotal, throughput, current concurrency (`c=N`), elapsed, ETA, and error count. Filters: `--include` and `--exclude` accept repeatable globs against filenames; dotfiles\u002Fdot-dirs are skipped by default (`--include-hidden` opts in).\n\n**Designed for TB-scale runs:**\n- **Auto-tuning concurrency** (default ON): an AIMD controller samples throughput and error rate every 10s and adjusts active workers via a runtime-resizable semaphore. Errors halve concurrency with a 30s cooldown; clean windows with rising throughput step it up. Bound by `--min-concurrency` \u002F `--max-concurrency` (default 1 \u002F 32). Pass `--no-auto-tune` to pin concurrency at `--concurrency` for the whole run. Tuner decisions print above the progress line so you can see what's happening.\n- **Failure-budget circuit breaker** (`--max-failures`, default 100): aborts the run cleanly if failure events hit the budget (one failed batch = one event; rows the server rejects count too, and no further batch is registered once it trips), so a misconfigured target folder doesn't burn hours of bandwidth. Exits `4`; files the abort never reached are reported as `not_attempted`, not as errors. Set `0` to disable.\n- **Exit status a script can trust** (0.8.0+): `0` every queued file registered · `1` bad input\u002Fstartup · `2` auth expired · `3` partial (some files errored, unrecognized, or not attempted — a Ctrl-C'd run included) · `4` aborted by `--max-failures`. Before 0.8.0 a sync with failed files exited `0`. The JSON summary satisfies `created + duplicate + error + unrecognized + not_attempted == to_upload`.\n- **`--trust-manifest` limits**: only server-confirmed rows (`created`\u002F`duplicate`) enter the manifest, so failed files are retried. The flag skips the server-side name check, so a file deleted on the server after being recorded is never re-uploaded — drop the flag when the target folder may have changed.\n- **Pre-flight folder check**: `GET \u002Ffolders\u002F{id}\u002F` runs before walking, so an inaccessible target ID fails fast instead of mid-run. Retried with backoff — a transport hiccup on this call is not evidence the folder is missing.\n- **Resilient folder resolution**: folder creates and per-parent subfolder lookups run with a 180s floor (raised further by `--timeout`, never lowered) and retry on timeouts, 429s, and 5xx. The root-level listing on a project-root sync inherits the ordinary default rather than the floor, so `--timeout` is still worth setting there. Because folders are identified by (name, parent), a retry re-resolves by name first and adopts the folder if a timed-out create actually landed server-side, instead of making a duplicate. Auth\u002Fpermission\u002Fvalidation errors still fail on the first attempt. Resolved folders are checkpointed into `--manifest` (`{relative_dir → folder_id}`) and reused on the next run, so a failure part-way through a wide tree doesn't throw away the whole run's setup.\n- **Adaptive multipart part-size**: per-file `part_size` is bumped automatically if the file would exceed R2's 10000-part cap (relevant for files > ~160 GB at the 16 MB default).\n- **Time-based manifest checkpoints**: manifest flushes every 50 uploads *or* every 30s, whichever comes first — a crash mid-run loses at most ~30s of progress.\n- **Resume messaging**: when the manifest already has entries, the run prints \"Resuming from manifest: N files already recorded.\" up front.\n- **`--part-concurrency`** (default 4) decouples parts-per-file parallelism from files-in-flight parallelism, so 16 concurrent multipart files don't spawn 16 × 16 = 256 PUTs.\n- **`--limit N`** stops after queueing N files (post-filtering). Useful for graduated smoke tests before committing to a multi-hour run; the manifest is still written so the next run resumes exactly where this one stopped.\n\n**HTTP read timeout (`--timeout`, `$PCXA_HTTP_TIMEOUT`):** every API call defaults to 30s. That is too tight for write endpoints on large projects — folder creates and bulk mutations regularly run past it, and the client can't distinguish \"slow\" from \"dead\". `pcxa --timeout \u003Cseconds> ...` (accepted before or after the subcommand on `files sync` and `files purge`) raises it for every call that goes through the API client. It does **not** change the upload\u002Fdownload\u002Fpresign helpers, which call the transport directly with their own longer timeouts already baked in (60–600s depending on the operation) — an explicit value there wins over the default, so `--timeout` neither raises nor lowers them. Prefer raising the timeout over shrinking `--chunk` when a bulk call times out: the server usually finished the work, so an abort leaves a silent partial success.\n\n**Deletion convention:** `pcxa files delete \u003Cids>` marks files for deletion by applying the `to_delete` tag. Use `pcxa files restore \u003Cids>` to undo before cleanup runs. Without `--yes`, `delete` prompts for confirmation.\n\nSearch results include `url` fields — always show these to users for document links.\n\n**`--search` is fuzzy by default (`files list`, `files aggregate`, `activities list`).** Backend uses PostgreSQL trigram similarity: exact substring matches surface first (similarity ~1.0), then typo-tolerant matches ranked by similarity DESC, in a single paginated response. `concret` finds `Concrete Pour`; `0314` ranks `RFI-0314` above `Document-031499`. Pass `--exact` to opt back into tight substring matching (rejects typos — useful when the query is a known-correct identifier and you don't want fuzzy noise). **`--exact` is not a phrase match:** the words are still AND-matched separately, so `--search \"Case Memo\" --exact` also matches `Case Assessment Memorandum`. When you need a literal adjacent phrase in the filename, use `pcxa files query 'title:\"Case Memo\"'`. The backend rate-limits fuzzy search to 100\u002Fmin per user; not normally a concern for agent use.\n\n**`files query` response shape:** `{query, parsed, results, total_files, count_exact, limit, offset}`. `parsed` is the canonical interpretation of your expression — surface it to the user when the result set matters. `count_exact: false` means `total_files` is a floor (ceiling reached), not a total. Rows carry `file_id`, `file_name`, `file_type`, `folder_path`, and a `url`.\n\n**Search response shape:** `pcxa files search` returns `{query, total_results, results, hybrid_enabled}` — a top-N reranked list (server-capped at 50). Each row carries `score`, `file_id`\u002F`activity_id`, `file_name`\u002F`title`, `folder_path`, `page_number`, `chunk_position`, and a `url`. Hybrid means semantic similarity and keyword matching over the project's chunk text are combined and reranked into a single ordering — the same path the web UI's search bar uses.\n\n**Four ways to find files — pick by what you need:**\n- **`files list --search \u003Cterm>`** — matches the **title\u002Ffilename** only (trigram). Paginated + countable. Use when you know part of the name.\n- **`files list --content \u003Cterm>`** — matches the indexed **body\u002Fcontents** as a **literal substring** (not ranked, not semantic). Paginated + **countable and exhaustive**: `--content \u003Cterm> --count-only` gives the *exact* total and you can page through *every* match in a stable order (by `id`). `count: 0` means the term is genuinely in no in-scope file — so this is the path for a \"not located\" \u002F completeness finding. Use it to find files by what's inside them — e.g. an eDiscovery estate where emails are named by bare Bates numbers (`YATES002119058`) and the term only appears in the body. Only indexed files match; scope with `--folder`\u002F`--ext`\u002F`--index-status`.\n- **`files search \u003Cterm>` \u002F `files content \u003Cterm>`** — hybrid semantic + keyword **ranking** (relevance-ordered), a **top-50 reranked sample, not a total**. Best for natural-language \u002F \"most relevant\" lookups. Do **not** use it to count or enumerate — it can't page past 50, and asking for more (`--limit 200`) is clamped to 50 with a notice pointing you at `--content`. For \"how many \u002F list them all\", use `--content`.\n\n- **`files query '\u003Cexpr>'`** — **boolean** search: `AND` \u002F `OR` \u002F `NOT`, parentheses for grouping, `title:` \u002F `content:` field scoping, and `\"quoted phrases\"` matched adjacently. Structurally expressive where `--content` is a single literal. Use it whenever the question has more than one condition — *\"schedule in the title AND both precast and delay in the body\"* is one call: `pcxa files query 'title:schedule AND precast AND delay'`. Bare terms search content; operators must be UPPERCASE (lowercase `and`\u002F`or` are ordinary words). Every response echoes `parsed` — **read it** to confirm the query was understood before trusting the results.\n\n  > ⚠️ **Multi-term `AND` is being fixed PER PROJECT — and nothing in the response tells you which behaviour you got.**\n  >\n  > **Old behaviour, still live on most projects:** two or more CONTENT terms joined by `AND` are chunk-scoped and UNDER-REPORT. The match requires the terms to fall in the *same passage* (~3,000 characters), not merely in the same file, so `precast AND delay` misses files where the words appear pages apart. Measured: **1,042 returned where 3,496 match** (~70% missed); on another shape, **3,738 returned where 9,342 match** (~60% missed).\n  >\n  > **New behaviour, rolling out:** `AND` is file-scoped, the count is correct, and totals are more often exact rather than capped.\n  >\n  > **The trap: `count_exact: true` appears in BOTH cases.** The old path returns a wrong number *and* labels it exact whenever it lands under the ceiling. So `count_exact` does not distinguish them, and neither does anything else in the payload.\n  >\n  > **So do not rest a completeness or \"not located\" finding on a multi-term `AND` unless you have confirmed the fix is live for that project.** Otherwise:\n  > - Run each content term as its own `files query` (or `files list --content \u003Cterm>`, which IS exhaustive per term) and intersect the `file_id`s yourself. Slower, correct on every project.\n  > - Or ask which projects have the file-scoped fix enabled, and say so when you rely on it.\n  >\n  > Using `AND` for the stricter \"these words near each other\" reading stays legitimate — just describe it that way rather than calling it exhaustive.\n  >\n  > A single content term, `title:` predicates, `OR`, and `NOT` are unaffected. When the fix reaches a project, counts on existing `AND` queries **go up** — that is the correction, not a regression.\n\n`--search` and `--content` compose (title AND body) and combine with `--tags`, `--folder`, `--ext`, dates, etc. For anything beyond a plain AND of one title term and one body term, reach for `files query` instead.\n\n**`files query` limits (all reported, never silent):** at most 8 OR branches, 4 levels of nesting, 16 terms. A bare `NOT` is rejected — negation needs something positive to search within (`contract NOT draft`, not `NOT draft`). An `OR` whose branches are **two or more very common** content words is rejected as too broad with an actionable message; narrow one branch or run them separately. `count_exact: false` means a ceiling was hit and the number is a floor, not a total.\n\n**Paging through the matches (`--limit` \u002F `--offset`).** Every `... list` subcommand — `files list`, `activities list`, `forms list`, `submissions list`, `custom-objects list`, `resources list`, `cost-codes list`, `budgets list`, `timesheets list` — returns **one page at a time**, defaulting to 25 or 50 rows. **A bare `list` call is never the complete set.** To enumerate everything, get the total first, then walk it:\n\n```bash\npcxa files list --content \"IOCC-387\" --count-only              # {\"count\": 137} ← the real total\npcxa files list --content \"IOCC-387\" --limit 100 --offset 0    # rows 1–100\npcxa files list --content \"IOCC-387\" --limit 100 --offset 100  # rows 101–137\n```\n\nIncrement `--offset` by `--limit` until you've collected `count` rows (or a page comes back short). With `--content` the order is by `id`, so paging is exhaustive — exactly `count` distinct ids, no repeats or skips. Report the number from `--count-only`, never the length of one page.\n\n**After search → read in batch.** Once `files search` returns the rows, prefer `files batch-read --chunk file_id:chunk_position` over N single `files read` calls. Each row carries a `chunk_position` — pass `file_id:chunk_position` as `--chunk` to read just the relevant excerpt + neighbors. One round trip instead of N. Use `--outline` for section maps when files are large and you need to plan further reads.\n\nFor folder-scoped browsing, use `pcxa files list --folder \u003Cid>`.\n\n## Tags & Folders\n\n```bash\npcxa tags list                                                # all tags with counts\npcxa tags add 1 2 3 --tags urgent,review                      # add (preserves existing)\npcxa tags remove 1 2 --tags draft                             # remove specific tags\npcxa tags set 1 2 --tags final,approved                       # replace all tags\npcxa tags bulk --file plan.json                               # different tags per file, one request\npcxa files bulk-patch --file plan.json                        # per-file metadata plan (tags + title\u002Fcategory\u002Fdescription)\npcxa folders tree --depth 2                                   # hierarchy\npcxa folders create \"Contracts\" --parent 5                    # new folder\npcxa folders rename 5 \"Legal\"                                 # rename\npcxa folders move 5 --parent 10                               # reparent\npcxa folders contents 5                                       # subfolders + files (paginated; --timeout for slow folders)\npcxa folders subfolders 5                                     # lightweight [{id,name}] list (fast on large folders)\npcxa folders delete 5                                         # delete + all contents\npcxa move 10 11 12 --folder 5                                 # bulk move files\npcxa categorize 10 11 --category \"Submittal\"                  # bulk set category\npcxa files update 10 --title \"New\" --tags a,b --folder 5      # single file update\n```\n\n**Bulk tag\u002Fmetadata plans (`files bulk-patch`, `tags bulk`):** `tags add\u002Fremove\u002Fset` apply the *same* tag set to a list of file ids. When each file needs *different* tags (or you're patching metadata exported from a spreadsheet), use the server-side `files\u002Fbulk_patch\u002F` endpoint instead of one request per file — up to 500 rows per request, auto-chunked for larger plans, per-row validated so one bad row doesn't fail the batch. Output reports `patched`, `modified` (rows that actually changed), and `failed` (per-row errors).\n\n`files bulk-patch --file plan.json` takes a JSON list of rows (or `{\"changes\": [...]}`), each row `{file_id, ...}` setting any subset of `title`, `category`, `description`, `tags` (with optional `tag_mode` ∈ `set`|`add`|`remove`, default `set`):\n\n```json\n{\"changes\": [\n  {\"file_id\": 123, \"tags\": [\"reviewed\", \"urgent\"], \"tag_mode\": \"add\"},\n  {\"file_id\": 124, \"tags\": [\"legal\"]},\n  {\"file_id\": 125, \"title\": \"ACME-0001.pdf\", \"category\": \"Contracts\", \"description\": \"Q3 amendment\"}\n]}\n```\n\n`tags bulk --file plan.json` is the tag-only view of the same endpoint — rows are `{file_id, tags, tag_mode}` and scalar fields (title\u002Fcategory\u002Fdescription) are rejected with a pointer to `files bulk-patch`. `folder` is not patchable via either command (folder moves recompute privacy\u002Faggregates — use `pcxa move`). Empty `tags` in `set` mode is refused client- and server-side to prevent an accidental mass tag-wipe; use `tag_mode: remove` to clear specific tags. Add `--dry-run` to preview the plan without sending. Both are `bulk_patch`-backed; the older `tags add\u002Fremove\u002Fset` and `bulk_update` path is now set-based server-side, but unchanged for callers.\n\n## Activities\n\n```bash\npcxa activities list --status in_progress --priority 3,4\npcxa activities list --search \"foundation\" --assignee 5 --sort -due_date  # fuzzy by default; add --exact for tight\npcxa activities list --tags \"structural,review\" --tags-mode all  # AND mode\npcxa activities list --after 2026-03-01 --before 2026-03-31       # updated in date range\npcxa activities list --after last_month                          # relative dates supported\npcxa activities list --created-after 2026-01-01 --created-before 2026-03-31\npcxa activities list --assignee 5 --after 2026-03-01 --before 2026-03-31  # user's work in period\npcxa activities list --wbs 1.4.2                              # exact WBS path\npcxa activities list --wbs-branch 1.4                         # 1.4 and everything under it\npcxa activities list --wbs-branch 1.4 --status completed      # composes with any other filter\npcxa activities get 123                                       # detail + steps + deps\npcxa activities create --title \"Review\" --priority 3 --type 5 --assignees 1,2\npcxa activities create --title \"Pour slab\" --custom-fields '{\"3\":\"Acme Corp\"}'  # custom-object value, fuzzy-validated\npcxa activities update 123 --status completed --percent 100\npcxa activities update 123 --outcome \"Grain is one row per charge per register.\"\npcxa activities update 123 --outcome \"\"                       # clear a wrong outcome\npcxa activities update 123 --custom-fields '{\"3\":\"Acme Corp\"}' --no-fuzzy        # write value as-is\npcxa activities delete 123 456                                # bulk delete\npcxa activities bulk-update 1 2 3 --status in_progress\npcxa activities types                                         # list templates\npcxa activities related 123                                   # linked files\u002Ffolders\u002Fphotos\u002Fforms\n```\n\n**Statuses:** `not_started`, `in_progress`, `completed` | **Priority:** 0=none, 1=low, 2=med, 3=high, 4=critical\n\n**Descriptions support Markdown** (headings, tables, bold, lists). Keep descriptions focused on:\n- **Scope\u002Fobjective** — what the activity is and what it produces\n- **Requestor** — who asked for it, with reference to source communication (e.g., `file:247764`)\n- **Business justification** — why this work exists\n\nDo NOT put in descriptions: processing details, scripts, output file lists, status updates, or progress notes. Those belong in **comments** (`pcxa comments add`) as dated narrative entries.\n\n**`--outcome` is the answer; `--description` is the question.** Description is the brief — what was asked and why. Outcome is what the activity concluded or produced: the decision, the finding, the deliverable. It is *not* production or quantity (that is `--percent`), and it is deliberately not called \"output\", which in this codebase means measurable production. Put the conclusion here rather than burying it in the newest comment — outcome is semantically indexed, so \"what did we decide\" stays findable.\n\nEvery edit is kept in the activity's history, so revising an outcome loses nothing. `--outcome \"\"` clears it.\n\nTwo limits worth knowing before you plan around them. Both are lifted by PCX-Analytics\u002Fpcxa#2863, so check whether that has deployed before designing around them:\n- **`bulk-update` has no `--outcome`.** The server's bulk allow-list rejects the field. Update them one at a time.\n- **`activities list --search` does not read outcomes.** Server-side search covers title, description and WBS code only. The outcome *is* indexed for semantic search, so `pcxa chat send` can find it when `--search` cannot.\n\n**WBS filters:** `--wbs 1.4.2` matches that one activity exactly. `--wbs-branch 1.4` returns 1.4 plus its whole subtree — 1.4.1, 1.4.2.7, at any depth — but **not** 1.40 or 1.41, which merely share a digit prefix. Both are server-side and compose with every other filter, and a `wbs` column is added to the table so you can confirm the scope rather than trust it.\n\nRequires an API with the WBS filters (PCX-Analytics\u002Fpcxa#2863). Against an older API the CLI **aborts rather than printing**: django-filter silently ignores parameters it does not know, so an unpatched server answers `--wbs-branch 1.4` with the entire project and a 200. The CLI re-checks every returned row and refuses a result it can prove is out of scope — you get a loud error, never a project-wide list mislabelled as a branch.\n\n**Date filters:** `--after`\u002F`--before` filter by last updated; `--created-after`\u002F`--created-before` filter by creation date. Accepts `YYYY-MM-DD` or relative keywords: `today`, `last_7_days`, `this_month`, `last_quarter`, etc.\n\n**Name resolution:** `--assignee` and `--owner` accept user IDs or names. Names are fuzzy-matched against project members:\n- Exact\u002Fsubstring match → resolves automatically with confirmation message\n- Multiple close matches → lists candidates with IDs for you to pick\n- No match → suggests `pcxa project members` to list all\n\n**Custom-object fields:** `--custom-fields` takes a JSON map of `{custom_field_id: value}` (custom fields are defined at the project level). On `update` the map merges into the activity's current values, and `null` clears one. Values targeting a custom-object-backed field are fuzzy-validated the same way form submissions are — see **Custom Objects**. Pass `--no-fuzzy` to write the raw value.\n\n**Invoicing workflow:** Query a user's activity in a billing period by name — no `--status` filter needed since `--after`\u002F`--before` captures any work (started, progressed, or completed):\n```bash\npcxa activities list --assignee \"John\" --after 2026-03-01 --before 2026-03-31\n```\n\n### Related items (`activities related`)\n\nWhat is attached to an activity — files, folders, photos, form submissions and\nrecords — in **one call, in both directions**. This is the CLI view of the web\napp's \"Related items\" rail.\n\n```bash\npcxa activities related 123                       # everything linked, either direction\npcxa activities related 123 --types file,folder   # only files and folders\npcxa activities related 123 --all                 # follow cursors to the end\npcxa activities related 123 --limit 100           # rows per page\n```\n\nThe `dir` column says which way the link points: `->` the activity is the\nlink's source, `\u003C-` it is the target.\n\nPrefer this over two `links list` calls. Links point either way, so the flat\nendpoint needs one query for `--source activity:123`, another for `--target\nactivity:123`, and a merge; this is anchored on the activity and resolves the\nfar end of every edge for you. It also runs on the project-scoped route, where\nthe company and project permission gates actually fire.\n\n**It does not report a total, and that is deliberate** — an exact count would\nmean enumerating and permission-filtering the whole neighbourhood. So the row\ncount is what was fetched, never what exists. Three notices tell you when the\nview is partial, and all three go to stderr:\n\n- **`... cannot be resolved by this endpoint and are NOT listed above`** — a\n  link of that type exists on the activity but this endpoint cannot hydrate it.\n  **Drawings are the common case**: `links create --target drawing:N` works and\n  the web app makes them, but `related` cannot show them. Use\n  `pcxa links list --source activity:123` to see those edges.\n- **`the server stopped scanning on its own budget`** — more links may exist\n  beyond what was scanned.\n- **`More results. Use --all, or --cursor …`** — pagination is cursor-based;\n  `--all` follows it, stopping after 50 pages and printing the cursor to resume.\n\n## Steps (Subtasks)\n\nWhen steps exist, activity progress auto-calculates from weighted step completion.\n\n```bash\npcxa steps list 123                                           # list steps\npcxa steps create 123 --name \"Draft review\" --weight 40\npcxa steps update 123 45 --percent 100                        # mark step complete\npcxa steps delete 123 45                                      # weights rebalance\npcxa steps from-template 123                                  # create from type template\n```\n\n## Progress\n\n```bash\npcxa progress list 123                                        # progress timeline\npcxa progress add 123 --percent 50 --notes \"Halfway\"\npcxa progress add 123 --percent 25 --date 2026-03-15          # backdate\npcxa progress delete 123 789                                  # manual entries only\n```\n\n## Comments\n\n```bash\npcxa comments list 3712                                       # list comments on activity\npcxa comments add 3712 --content \"Drafting initiated...\"\npcxa comments delete 3712 2952                                # delete comment by ID\npcxa comments bulk 3712 --file comments.json                  # bulk add from JSON file\n```\n\n**Bulk JSON format** (`comments.json`):\n```json\n[\n  {\"content\": \"Drafting initiated per weekly report #12\"},\n  {\"content\": \"Internal review completed, minor revisions needed\"}\n]\n```\n\nSupports both bare list and wrapper object with `\"comments\"` key.\n\n## Dependencies (CPM)\n\nTypes: `FS` (finish-to-start), `SS`, `FF`, `SF`. Lag in days (negative = lead).\n\n```bash\npcxa deps list --predecessor 10\npcxa deps create --predecessor 10 --successor 20 --type FS --lag 2\npcxa deps delete 456\n```\n\n## Tag-Filter Links (evidence sets on an activity)\n\nAttach a **saved tag query** to an activity as one link that stands for a whole *set* of files — the AND\u002FOR combination a plain object link can't express. \"Pay apps for Yates\" = tags `pay_app` **and** `yates` in `all` mode. The set is **dynamic**: it resolves live to whatever files currently carry the tags (`pcxa files list --tags pay_app,yates --tags-mode all`), and the web app renders each link as a chip that deep-links into the Files tab with the filter pre-applied.\n\n```bash\npcxa tag-filters list 5080                                          # links on activity 5080\npcxa tag-filters add 5080 --tags pay_app,yates --mode all           # AND: files with BOTH tags\npcxa tag-filters add 5080 --tags rfi,submittal --mode any --label \"Open items\"  # OR (default)\npcxa tag-filters delete 5080 12                                     # remove link 12\n```\n\n`--mode all` requires **every** tag (AND); `--mode any` (default) matches **any** tag (OR) — same semantics as `files list --tags-mode`. Up to 20 tags per link; `--label` overrides the default `tag1 + tag2` display label. This is the CLI surface for the backend's `ActivityTagFilterLink` (nested under `activities\u002F{id}\u002Ftag-filter-links\u002F`). Use it instead of `links create --target …` when the evidence is defined by a tag combination rather than a fixed object.\n\n## Gantt & WBS Tree\n\n```bash\npcxa gantt --status in_progress\npcxa tree --max-depth 3\n```\n\n## Forms (Templates)\n\nForm templates define reusable forms with typed fields. Submissions are instances of a form.\n\n```bash\npcxa forms list --category Safety                     # list templates\npcxa forms list --scope project --search \"inspection\"\npcxa forms get 1                                      # detail + fields\npcxa forms create --name \"Safety Inspection\" --category Safety --code-prefix SI --code-padding 3\npcxa forms update 1 --description \"Updated desc\" --reviewers 3,5\npcxa forms delete 1\n```\n\n**Scope:** `project` (default) or `company`. **Code settings:** `code-prefix` (e.g. RFI), `code-scope` (project\u002Fcompany), `code-separator` (default: -), `code-padding` (3=001).\n\n## Fields (Form Fields)\n\nFields define the structure of a form template. Field values are referenced by field ID in submissions.\n\n```bash\npcxa fields list 1                                    # list fields for form 1\npcxa fields create 1 --label \"Inspector\" --type text --required --order 1\npcxa fields create 1 --label \"Severity\" --type select --options '{\"choices\":[\"Low\",\"Med\",\"High\"]}'\npcxa fields create 1 --label \"Date\" --type date --required --order 2\npcxa fields create 1 --label \"Vendor\" --type choice --choice-id 21      # custom-object-backed\npcxa fields update 1 5 --label \"New Label\" --required true\npcxa fields delete 1 5\n```\n\n**Field types** (exact set): `text`, `textarea`, `number`, `date`, `datetime`, `checkbox`, `select`, `radio`, `choice`, `table`, `photo`, `file`, `location`.\n- `select`\u002F`radio`: inline options via `--options '{\"choices\":[\"A\",\"B\"]}'`.\n- `choice`: backed by a **custom object** (field-choice) — bind with `--choice-id \u003Cobject_id>`; submission values are fuzzy-validated (see **Custom Objects**).\n- `table`: a repeating grid of typed columns — see **Table fields** below.\n\n### Table fields\n\nA `table` field holds a list of rows, each with the same typed columns. **The columns go in `table_schema` (a JSON array), NOT `--options`** — the CLI exposes a dedicated `--table-schema` flag for this. `--options` is silently accepted by the API but ignored by the forms UI, so a table defined via `--options` renders with no columns.\n\n```bash\npcxa fields create 1 --label \"Line Items\" --type table --required \\\n  --table-schema '[\n    {\"name\":\"item\",\"field_type\":\"text\",\"label\":\"Item\"},\n    {\"name\":\"qty\",\"field_type\":\"number\",\"label\":\"Qty\"},\n    {\"name\":\"unit\",\"field_type\":\"select\",\"label\":\"Unit\",\"options\":[\"ea\",\"lf\",\"sf\"]}\n  ]' \\\n  --min-rows 1 --max-rows 20\n```\n\n- Each column needs a **`name`** (the key used in submission values), a **`field_type`**, and a **`label`**. Column `name` keys must be stable — submission row values are keyed by them.\n- `--min-rows` \u002F `--max-rows` bound the row count (optional).\n- `pcxa fields list 1` and `pcxa forms get 1` show a table field's columns (`cols: item, qty, …`) and a choice field's binding (`custom-object 21`).\n\n## Submissions (Form Submissions)\n\nSubmissions are instances of a form template, with field values keyed by field ID.\n\n```bash\npcxa submissions list --form 1                        # list for a specific form\npcxa submissions list --status draft                  # filter by status\npcxa submissions get 1 42                             # detail (form_id submission_id)\npcxa submissions create 1 --code SI-001 --values '{\"1\":\"John\",\"2\":\"2026-03-24\",\"3\":\"High\"}'\npcxa submissions update 1 42 --values '{\"3\":\"Critical\"}' --merge --tags urgent  # change only field 3\npcxa submissions delete 1 42\n```\n\n**Statuses:** `draft`, `submitted`, `closed`. `--values` is a JSON object mapping **field ID → value**. The value shape depends on the field type:\n\n| Field type | Value shape | Example (`\"3\"` = field ID) |\n|---|---|---|\n| text\u002Fnumber\u002Fdate\u002Fselect\u002Fradio\u002Fchoice | scalar | `{\"3\":\"High\"}`, `{\"3\":42}` |\n| checkbox | boolean | `{\"3\":true}` |\n| table | **array of row objects keyed by column `name`** | `{\"3\":[{\"item\":\"Rebar\",\"qty\":50},{\"item\":\"Concrete\",\"qty\":12}]}` |\n\nFor a **table** field, each row is an object whose keys are the column `name`s from the field's `table_schema`. Example creating a submission with a 2-row table in field `7`:\n\n```bash\npcxa submissions create 1 --code SI-002 \\\n  --values '{\"7\":[{\"item\":\"Rebar\",\"qty\":50,\"unit\":\"lf\"},{\"item\":\"Concrete\",\"qty\":12,\"unit\":\"sf\"}]}'\n```\n\n> ⚠️ **The API does NOT validate value shapes.** It accepts a wrong shape (a scalar where a table is expected, rows keyed by the wrong names, a `{\"rows\":[…]}` wrapper, etc.) without error — but the forms UI then renders the field broken. Match the field type and the column `name`s exactly. Use `pcxa fields list \u003Cform_id>` to confirm a table field's columns before submitting.\n\n> ⚠️ **`update --values` replaces the entire values dict by default.** Passing a partial `--values` (e.g. just `{\"3\":\"Critical\"}`) clears every other field on the submission. Pass **`--merge`** (alias `--patch`) to update only the keys you supply, leaving the rest intact. Without `--merge` the CLI prints a one-line overwrite warning to stderr.\n>\n> **`--merge` works at field-ID granularity only — it cannot merge *within* a table value.** To change one cell or add one row, resend the field's **entire** row array: `pcxa submissions get` the current value, edit the array, and send it back under that field's key with `--merge`.\n\nValues targeting a custom-object-backed (`choice`) field are fuzzy-validated on create\u002Fupdate — see **Custom Objects** below. Pass `--no-fuzzy` to skip. (Custom-object columns *inside* a table row are not yet fuzzy-validated — verify those values against the object's options yourself.)\n\n## Custom Objects (Field Choices)\n\nCustom objects are reusable, typed lookup tables (`field-choices` in the API). A custom object defines a `property_schema` (its columns — a JSON **list** of `{\"name\", \"type\"}` objects) and holds **options** (rows), each with a `label` and a `properties` JSON of column values. Form fields can be backed by a custom object so submissions pick from its options. Objects exist at **project** (default) or **company** scope; a company object can be surfaced into a project with `extend`.\n\n```bash\npcxa custom-objects list                                    # project-scoped objects\npcxa custom-objects list --scope company --search vendor     # company-scoped, filtered\npcxa custom-objects get 5                                   # schema + first options\npcxa custom-objects create --name \"Vendors\" --schema '[{\"name\":\"code\",\"type\":\"text\"}]'\npcxa custom-objects create --name \"Trades\" --scope company --extensible true\npcxa custom-objects update 5 --description \"Approved vendor list\"\npcxa custom-objects extend 5                                # company object → current project\npcxa custom-objects delete 5\n```\n\n### Options (rows)\n\n```bash\npcxa custom-objects options list 5                          # rows of object 5\npcxa custom-objects options create 5 --label \"Acme Corp\" --properties '{\"code\":\"A1\"}'\npcxa custom-objects options update 5 42 --label \"Acme Corporation\" --order 1\npcxa custom-objects options delete 5 42\npcxa custom-objects options bulk-create 5 --file options.json   # [{\"label\":..,\"properties\":..}, ...]\npcxa custom-objects options reorder 5 --order \"42,17,9\"      # option ids in desired order\n```\n\nPass `--scope company` to any object\u002Foption command when the object is company-scoped.\n\n### Fuzzy matching (\"did you mean?\")\n\nWhen a value should reference a custom object, the CLI fuzzy-matches it against that object's options:\n\n```bash\npcxa custom-objects resolve 5 \"Acme Crp\"\n# → No match for 'Acme Crp'. Did you mean 'Acme Corp' (option 10)?\n```\n\n`resolve` exits non-zero when there is no confident match. **Form submissions and activity custom fields run the same check automatically:** on `submissions create`\u002F`update` with `--values` or `activities create`\u002F`update` with `--custom-fields`, any value targeting a custom-object-backed field is validated — an exact label\u002Fid or a unique substring resolves silently; anything else blocks the write with a \"Did you mean Y?\" suggestion. Pass `--no-fuzzy` to write the raw value. Validation reads the field definitions (form fields, or activity custom fields at `activities\u002Fcustom-fields\u002F`) and the object's options (trying project then company scope); a permissions or network error degrades to a warning and never blocks an otherwise-valid write.\n\n## Resources\n\nResources represent people, equipment, consumables, or subcontractors assigned to activities.\n\n```bash\npcxa resources list --type personnel --active true\npcxa resources get 1                                  # detail with rates\npcxa resources create --name \"John Smith\" --type personnel --user 5\npcxa resources create --name \"Crane #3\" --type equipment --unit hours --capacity 10\npcxa resources update 1 --name \"John D. Smith\" --active false\npcxa resources delete 1\n```\n\n**Types:** `personnel`, `equipment`, `consumable`, `subcontractor` | **Units:** `hours`, `days`, `each`, `lump_sum`\n\nOnly `personnel` resources can link to a user (`--user`). Use `--user 0` to unlink.\n\n## Rates (Resource Rates)\n\nAppend-only rate history. Create new rates with new effective dates — no updates\u002Fdeletes.\n\n```bash\npcxa rates list 1                                     # rates for resource 1\npcxa rates create 1 --effective-date 2026-04-01 --standard-rate 75 --cost-rate 50 --bill-rate 100\npcxa rates create 1 --effective-date 2026-04-01 --standard-rate 75 --cost-rate 50 --bill-rate 100 --overtime-rate 112.50\n```\n\n## Assignments (Resource → Activity)\n\n```bash\npcxa assignments list 123                             # list for activity 123\npcxa assignments create 123 --resource 5 --planned-units 40 --role \"Lead Engineer\"\npcxa assignments create 123 --resource 8 --planned-units 80 --curve front_loaded --driving\npcxa assignments update 123 45 --remaining 20 --at-completion 50\npcxa assignments delete 123 45\n```\n\n**Curves:** `uniform`, `front_loaded`, `back_loaded`, `bell`\n\n## Cost Codes (Company-Scoped)\n\nHierarchical cost tracking codes shared across all projects.\n\n```bash\npcxa cost-codes list --root-only\npcxa cost-codes get 1                                 # detail with children\npcxa cost-codes create --code \"03.300\" --name \"Cast-in-Place Concrete\"\npcxa cost-codes create --code \"03.310\" --name \"Structural\" --parent 1\npcxa cost-codes update 1 --active false\npcxa cost-codes delete 1\n```\n\n## Budgets (Cost Code Budgets)\n\n```bash\npcxa budgets list\npcxa budgets create --cost-code 1 --amount 50000 --units 200\npcxa budgets update 1 --amount 75000\npcxa budgets delete 1\n```\n\n## Timesheets\n\nTime\u002Fcost tracking per resource with approval workflow: `draft` → `submitted` → `approved` | `rejected` → `draft`.\n\n```bash\npcxa timesheets list --status draft --resource 5\npcxa timesheets list --after 2026-03-01 --before 2026-03-31\npcxa timesheets get 1                                 # detail with all entries\npcxa timesheets create --resource 5 --period-start 2026-03-24 --period-end 2026-03-30 --period-type weekly\npcxa timesheets update 1 --period-end 2026-03-31\npcxa timesheets delete 1\npcxa timesheets submit 1                              # draft → submitted\npcxa timesheets approve 1                             # submitted → approved\npcxa timesheets reject 1 --reason \"Missing entries\"   # submitted → rejected\npcxa timesheets reopen 1                              # rejected → draft\n```\n\n**Period types:** `weekly`, `biweekly`, `monthly`. Only draft\u002Frejected timesheets are editable.\n\n## Time Entries (nested under timesheets)\n\n```bash\npcxa entries list 1                                   # entries for timesheet 1\npcxa entries list 1 --date 2026-03-25 --activity 123\npcxa entries create 1 --activity 123 --date 2026-03-25 --hours 8\npcxa entries create 1 --activity 123 --date 2026-03-25 --hours 2 --type overtime --cost-code 5\npcxa entries update 1 42 --hours 6 --description \"Half day\"\npcxa entries delete 1 42\n```\n\n**Types:** `regular`, `overtime`, `double_time` | Hours: 0.01–24\n\n## Cost Entries (nested under timesheets)\n\n```bash\npcxa cost-entries list 1\npcxa cost-entries create 1 --resource 8 --activity 123 --date 2026-03-25 --quantity 5 --unit-cost 200\npcxa cost-entries update 1 42 --quantity 10\npcxa cost-entries delete 1 42\n```\n\n`total_cost` auto-computed: `quantity × unit_cost`.\n\n## Time Period Locks\n\nLock date ranges to prevent edits.\n\n```bash\npcxa locks list\npcxa locks create --period-start 2026-03-01 --period-end 2026-03-15 --reason \"Month closed\"\npcxa locks delete 1\n```\n\n## Links (Entity Links)\n\nConnect any two objects (files, activities, photos, drawings) with contextual descriptions. Object references use `type:id` format.\n\n**Types:** `activity`, `file`, `folder`, `photo`, `drawing`, `source_document`, `markup`, `project`, `company`, `formsubmission`, `fieldchoiceoption`\n\nAn unknown type is rejected locally, with the valid set and a \"did you mean?\"\nsuggestion, instead of costing a round-trip and coming back as a bare 400.\n\nTo read an activity's links, prefer `pcxa activities related \u003Cid>` — one call,\nboth directions, far ends resolved. Note the two type sets differ: every type\nabove is *linkable*, but `related` can only resolve `activity`, `file`,\n`folder`, `photo`, `formsubmission` and `fieldchoiceoption`. Drawing links are\ncreatable and visible to `links list`, but `related` reports them as omitted\nrather than showing them.\n\n```bash\npcxa links list --source file:170106                  # links from a file\npcxa links list --target activity:3710                # links to an activity\npcxa links list --source file:170106 --target activity:3710  # specific pair\npcxa links create --source file:170106 --target file:170107 --type attachment\npcxa links create --source activity:3710 --target file:170106 --type deliverable\npcxa links create --source file:170129 --target file:170100 --type \"supersedes\" --description \"Corrected analysis\"\npcxa links delete 42\npcxa links bulk --file links.json                     # bulk create from JSON\n```\n\n**Bulk JSON format** (`links.json`):\n```json\n[\n  {\"source\": \"file:170106\", \"target\": \"file:170107\", \"type\": \"attachment\"},\n  {\"source\": \"activity:3710\", \"target\": \"file:170106\", \"description\": \"RFI analysis deliverable\"},\n  {\"source_type\": \"file\", \"source_id\": 170129, \"target_type\": \"file\", \"target_id\": 170100, \"type\": \"supersedes\"}\n]\n```\n\nThe `--type` flag sets the description field (the backend doesn't enforce typed relationships). Use `--description` for longer context. Both shorthand (`\"source\": \"file:123\"`) and explicit (`\"source_type\": \"file\", \"source_id\": 123`) formats work in bulk JSON.\n\n`links bulk` calls the server-side bulk endpoint (up to 500 links per request, auto-chunked for larger files) instead of one request per link — rows are validated independently server-side, so one bad row doesn't fail the batch. Output reports `created`, `exists` (already-present links, not an error), and `failed` (per-row errors with the row index).\n\n## AI Chat\n\nSend messages to the project's AI assistant and read back its replies. Useful for ad-hoc probing and for letting an agent evaluate chatbot response quality. Project-scoped — uses the `.pcxa` company\u002Fproject.\n\n```bash\npcxa chat send \"What is the status of the foundation work?\"          # uses current conversation, waits for reply\npcxa chat send \"...\" --new --title \"Eval run 1\"                      # fresh conversation\npcxa chat send \"...\" --conversation 123                              # continue an existing thread\npcxa chat send \"...\" --research                                      # enable file-search tools (research_mode)\npcxa chat send \"...\" --model gemini-2.5-pro                          # override model (see chat models)\npcxa chat send \"...\" --no-wait                                       # fire-and-forget; returns agent_task_id\npcxa chat send \"...\" --timeout 300                                   # polling timeout (default 180s)\n\npcxa chat ls --search \"RFI\"                                          # list conversations\npcxa chat get [ID]                                                    # show full transcript (default: current)\npcxa chat get 123 --show-tools                                        # include tool calls\npcxa chat new --title \"Probe\"                                         # create empty conversation\npcxa chat delete 123                                                  # soft-delete (archive)\npcxa chat models                                                      # list available models\n```\n\n**Default JSON output** (from `chat send`):\n```json\n{\n  \"conversation_id\": 42,\n  \"agent_task_id\": 9001,\n  \"agent_task_status\": \"completed\",\n  \"elapsed_seconds\": 14.2,\n  \"timed_out\": false,\n  \"user_message\": {\"id\": 500, \"content\": \"...\"},\n  \"assistant_message\": {\n    \"id\": 501, \"role\": \"assistant\", \"content\": \"...\",\n    \"tool_steps\": [...], \"thinking_steps\": [...], \"action_cards\": [...]\n  }\n}\n```\n\n**How it works:** `chat send` submits a message, waits for the platform's assistant task to finish, and returns the assistant response. Exit code 2 means the task failed. `--no-wait` skips polling and returns the task ID immediately.\n\n## Audit Log (company admins)\n\nWho opened, downloaded or was shown which files. **Company-scoped** (needs no project; `--company` overrides the profile\u002F`.pcxa` company) and readable by **company admins only** — anyone else gets a 403.\n\n```bash\npcxa audit list --user jane@example.com --since 2026-09-01 -f table    # one person\npcxa audit list --type file.presign.issued --since 2026-09-14           # downloads and previews\npcxa audit list --search \"site plan\" --until 2026-09-13 -f table        # label \u002F username \u002F event contains\npcxa audit list --user jane@example.com --csv jane.csv                  # every matching event to a spreadsheet\npcxa audit history 16457660 --csv file-16457660.csv                     # one file's full history, oldest first\npcxa audit history 812 --resource-type folder.Folder                    # any other resource\n```\n\n| Event type | Meaning |\n|---|---|\n| `files.get` | a file's record opened in the app or API |\n| `file.presign.issued` | file bytes handed out: a download or in-app preview |\n| `file.bulk_download.entry` | a file included in a bulk download |\n| `file.search_excerpt` | file text shown in a search result or to the assistant |\n| `file.ai_read` \u002F `file.ai_viewed_pages` \u002F `file.ai_quoted` | the assistant read, looked at, or quoted the file |\n| `file.email_preview` | an email previewed |\n| `file.container_entry` | a file opened inside a zip or mailbox |\n\nNot exhaustive: `event_type` is free text server-side. Assistant rows carry `is_ai_agent` and `conversation_id` (in `details`).\n\n**Read the output correctly:**\n\n- **`--user` takes an email.** The log stores an internal username that is *usually* the email prefix but gets a numeric suffix on collision (`jdoe`, `jdoe1`), so a guessed prefix can return nobody or someone else. An email is resolved against current company members; for a former member, pass the exact username (find it with `--search`).\n- **Dates are UTC.** `--since` is inclusive; `--until YYYY-MM-DD` includes that whole day. The server has no date filter, so the CLI pages newest-first and stops at `--since` — a wide window on a busy company reads many pages.\n- **`list` stops at 500 rows on screen**, and says so on stderr when more exist; `--csv` exports everything unless you pass `--limit`.\n- **An empty `history` is not proof of no access.** If the company has chain-of-custody auditing disabled, the CLI warns on stderr and JSON shows `\"audit_enabled\": false`. A file's history also includes folder downloads that carried it.\n- **`history` is rate-limited to 10 requests\u002Fhour per user**, shared with audit verification (one request per 1000 events). If the limit hits mid-history the CLI exits 1 and writes no partial file.\n- The CSV keeps `sequence_number` (the ledger's chain position) and the full `details` JSON.\n\n## Reporting API errors\n\nWhen a PCXa CLI command returns a non-2xx API response, surface the endpoint, status code, and response body to the user. Do not invent alternate data paths; ask the user how they want to proceed.\n\n## Invocation\n\n`\u002Fpcxa $ARGUMENTS` → `pcxa $ARGUMENTS` (if installed via pipx) or `python .claude\u002Fskills\u002Fpcxa\u002Fpcxa.py $ARGUMENTS`\n",{"name":5,"description":7,"argument-hint":43,"user-invocable":44,"disable-model-invocation":39},"\u003Ccommand> [options]",true,"main","0f080053bfb327c2461055d23a0fffac72993988","skills\u002Fpcxa",2,"https:\u002F\u002Fgithub.com\u002FPCX-Analytics\u002Fpcxa-skill","https:\u002F\u002Fgithub.com\u002FPCX-Analytics","2026-11-05 00:33:57",{"source":8,"skillName":5,"commands":53,"prompt":63,"zipAvailable":44},[54,57,60],{"tool":55,"command":56},"npx","npx skills add PCX-Analytics\u002Fpcxa-skill --skill pcxa",{"tool":58,"command":59},"bunx","bunx skills add PCX-Analytics\u002Fpcxa-skill --skill pcxa",{"tool":61,"command":62},"pnpm","pnpm dlx skills add PCX-Analytics\u002Fpcxa-skill --skill pcxa","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FPCX-Analytics\u002Fpcxa-skill\u002Fblob\u002Fmain\u002Fskills\u002Fpcxa\u002FSKILL.md",[],[66],{"skillNo":67,"name":68,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":69,"sourceHtmlUrl":70,"categories":71,"keywords":72,"publishedAt":38},"01M71STNYFCMTNX7W4SHRBKYC3","pcxa-skill","https:\u002F\u002Fgithub.com\u002FPCX-Analytics\u002Fpcxa-skill\u002Fblob\u002Fmain\u002FLICENSE","https:\u002F\u002Fgithub.com\u002FPCX-Analytics\u002Fpcxa-skill\u002Fblob\u002Fmain\u002FSKILL.md",[],[73],"pcxa skill"]