toga-ai 1.0.927 → 1.0.929

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/knowledge/1.0/apps/library/features/netsuite-suiteql-api-reference.md +25 -2
  2. package/knowledge/1.0/apps/tools/INDEX.md +1 -0
  3. package/knowledge/1.0/apps/tools/features/force-logout.md +46 -0
  4. package/knowledge/1.0/apps/tools/features/worker2-job-bridge.md +13 -2
  5. package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +28 -1
  6. package/knowledge/2.0/apps/dbchanges2/architecture.md +2 -2
  7. package/knowledge/2.0/apps/toga2-commerce/features/force-logout-on-deployment.md +5 -0
  8. package/knowledge/2.0/apps/toga25-supply/features/force-logout-on-deployment.md +5 -1
  9. package/knowledge/2.0/apps/worker2/INDEX.md +3 -3
  10. package/knowledge/2.0/apps/worker2/features/clickup-design-sprint-automation.md +9 -4
  11. package/knowledge/2.0/apps/worker2/features/clickup-general-automation.md +47 -28
  12. package/knowledge/2.0/apps/worker2/features/clickup-work-type-automation.md +32 -8
  13. package/knowledge/2.0/apps/worker2/features/committed-secrets-and-config-debt.md +14 -3
  14. package/knowledge/2.0/apps/worker2/features/netsuite-opportunity-sync.md +7 -1
  15. package/knowledge/2.0/apps/worker2/features/qa-qc-review-pipeline.md +52 -16
  16. package/knowledge/2.0/apps/worker2/features/team-sprint-management.md +92 -69
  17. package/knowledge/2.0/apps/worker2/workflows/sprint-clickup-folder-repoint.md +26 -15
  18. package/knowledge/INDEX.md +2 -2
  19. package/knowledge/clients/nychh/INDEX.md +2 -2
  20. package/knowledge/clients/nychh/features/transfer-order-notification-emails.md +80 -1
  21. package/knowledge/clients/nychh/profile.md +2 -2
  22. package/package.json +1 -1
@@ -6,8 +6,8 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-30
10
- owners: ["dfranks", "jcardinal", "kyalamarthi", "ajean", "rgirish"]
9
+ updated: 2026-10-01
10
+ owners: ["dfranks", "jcardinal", "kyalamarthi", "ajean", "rgirish", "bala"]
11
11
  files:
12
12
  - library/app/api/netsuite/rest.php
13
13
  - library/ssl/netsuite_ec_key.pem
@@ -106,6 +106,7 @@ Type codes (`transaction.type`):
106
106
 
107
107
  Status codes (`transaction.status` letter):
108
108
  **Sales Orders:** `A` Pending Approval · `B` Pending Fulfillment · `C` Cancelled · `D` Partially Fulfilled · `E` Pending Billing/Partially Fulfilled · `F` Pending Billing · `G` Billed · `H` Closed. **"Open" sales orders = `B`, `D`, `E`, `F`.** Note `listSalesOrders()` returns **all** statuses for a window (no server-side status filter) — callers must filter.
109
+ **Item Fulfillments (`ItemShip`):** `A` Picked · `B` Packed · `C` Shipped.
109
110
  **Invoices:** `A` Open · `B` Paid In Full. **Credit Memos:** `A` Open · `B` Fully Applied. **Cash Sales:** `A` Unapproved Payment · `C` Deposited. **Cash Refunds:** `Y` (the only status; empty string in SOAP).
110
111
 
111
112
  Header columns present (✓ confirmed in SuiteQL): `id`, `tranid`, `trandate`, `lastmodifieddate`, `type`, `status`, `entity`, `employee`, `leadsource`, `terms`, `memo`, `otherrefnum`, `foreigntotal`/`total` (both return the same value), `foreignamountunpaid` (AR balance — invoices only; NULL for SO/credit memo/cash sale), `duedate`, `shipmethod`, `opportunity`, `shippingaddress` (numeric id → join `transactionShippingAddress`), and custom fields (`CUSTBODY_END_CUSTOMER`, `CUSTBODY_STOCKING_ORDER`, …).
@@ -114,6 +115,25 @@ Header fields NOT in SuiteQL (✗ — require a per-id REST GET): `shippingCost`
114
115
 
115
116
  Tracking numbers in SuiteQL (verified 2026-09-28): `transaction.trackingnumberlist` (on the IF) / `linkedtrackingnumberlist` (on the SO) return **`trackingnumber` record ids**, not the numbers — resolve with `SELECT id, trackingnumber FROM trackingnumber WHERE id IN (...)`. `trackingnumbermap` returned nothing. The REST per-id GET below also has them.
116
117
 
118
+ ### Order → the transactions created FROM it: `nexttransactionlink`
119
+
120
+ **`createdfrom` is a `transactionline` column — SuiteQL rejects it on `transaction`.** There is no
121
+ header-level createdfrom to filter on. To find an order's fulfillments (or any child document), join
122
+ `nexttransactionlink`:
123
+
124
+ ```sql
125
+ SELECT ntl.previousdoc, ntl.nextdoc, t.tranid, t.type, t.status
126
+ FROM nexttransactionlink ntl
127
+ JOIN transaction t ON t.id = ntl.nextdoc
128
+ WHERE ntl.previousdoc IN (<order internal ids>)
129
+ ```
130
+
131
+ Pair it with the `ItemShip` status letters above to see how far each fulfillment got. This is how a
132
+ **NYCHH transfer order** is traced: `Client_Nychh.TransferOrders.c_netsuiteInternalSalesOrderId`
133
+ holds a NetSuite **sales order** internal id (a $0 SO *is* the transfer order — see
134
+ [the outbound push](../../../2.0/apps/worker2/features/netsuite-transferorder-outbound-push.md)), so
135
+ `previousdoc` is that id and the `ItemShip` rows that come back are its fulfillments.
136
+
117
137
  ### `transactionline` table
118
138
 
119
139
  Key columns (confirmed present): `transaction`, `linesequencenumber`, `uniquekey`, `id`, `item`, `itemtype`, `mainline`, `quantity`, `foreignamount`, `rate`, `costestimate`, `class`, `memo`, `quantitybilled`, `quantitycommitted`, `quantitypicked`, `quantityshiprecv`, `createdfrom`, `entity` (line-level customer), `location`, `subsidiary`.
@@ -271,4 +291,7 @@ SalesOrd per-id GET counts by `lastmodifieddate` window: ~16,600 (90d) / ~61,900
271
291
  - `../../worker/features/forecast2-netsuite-reconciliation.md` — the audit/trueup tooling that consumes this API.
272
292
 
273
293
  ## Change history
294
+ - 2026-10-01 — Added `nexttransactionlink` as the order → child-document join (`createdfrom` is a
295
+ `transactionline` column and is rejected on `transaction`), and the `ItemShip` status letters
296
+ (`A` picked / `B` packed / `C` shipped). Found tracing NYCHH transfer orders. (bala)
274
297
  - 2026-09-30 — Gotcha: `inventorybalance.quantityavailable` ignores SO commitments; use `aggregateitemlocation`. (rgirish)
@@ -12,6 +12,7 @@
12
12
  | [Tools — Developers Folder (UUID & Password Generators)](features/developer-tools.md) | Tools "Developers" folder — the Generate UUID and Generate Password tools (reference example of the one-tool-per-`mvc/<route>/get.php` pattern); open for the ge |
13
13
  | [/emails Email Queue Review (Tools → 2.0 Logs_<Client>.Email)](features/email-queue-review.md) | Staff page to browse any 2.0 client's `Logs_<Client>.Email` queue and release/reject `HELD` review-hold emails. |
14
14
  | [/errors Curation Console (Tools → shared Core Logs DB)](features/errors-curation-console.md) | Internal-only 1.0 Tools triage/curation screen (`/errors`) for the 2.0 Issue/Event error pipeline, reading the shared Core Logs DB via `db_toga2logs`; curates, |
15
+ | [Force Logout (tools /developers/force-logout → worker2 sets META_LAST_REFRESH_DATETIME)](features/force-logout.md) | Tools page that signs every 2.0 front-end user out after a big deploy by changing Core parameter `META_LAST_REFRESH_DATETIME` through a worker2 job; open to tri |
15
16
  | [GitHub Audit (monthly code-churn report)](features/github-audit.md) | Internal monthly code-churn report at `/developers/github-audit` (lines added/removed across every `agilantsolutions` repo for one month); open for the two-phas |
16
17
  | [Legacy Email Notifier (tools /email-migration/notify)](features/legacy-email-notifier.md) | SSO-gated admin tool at `/email-migration/notify` that emails employees a digest of mail still hitting their legacy domains (asks senders to switch to `@togatec |
17
18
  | [Tools — Multi-Client SQL (per-client-DB SQL fan-out)](features/multi-client-sql.md) | Tools `/developers/multi-client-sql` — fans one SQL change-set across a live list of 2.0 client/log/archive/module DBs into a copy/download script (never execut |
@@ -0,0 +1,46 @@
1
+ ---
2
+ title: Force Logout (tools /developers/force-logout → worker2 sets META_LAST_REFRESH_DATETIME)
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-01
10
+ owners: [bala]
11
+ files:
12
+ - tools/_/app/developers/forcelogout.php
13
+ - tools/mvc/developers/force-logout/get.php
14
+ - tools/mvc/developers/force-logout/post.php
15
+ - tools/_/app/nav.php
16
+ - worker2/Worker/Platform/ForceLogout.php
17
+ related:
18
+ - ./worker2-job-bridge.md
19
+ - ./developer-tools.md
20
+ - ../../../2.0/apps/toga2-commerce/features/force-logout-on-deployment.md
21
+ - ../../../2.0/apps/toga25-supply/features/force-logout-on-deployment.md
22
+ ---
23
+
24
+ Tools page that signs every 2.0 front-end user out after a big deploy by changing Core parameter `META_LAST_REFRESH_DATETIME` through a worker2 job; open to trigger a force logout, or before touching `Core.Parameters` from code.
25
+
26
+ ## Summary
27
+
28
+ Page `/developers/force-logout` (nav: Developers → Force Logout, persona **Development Team** only). Dev picks a date/time, presses Update. Replaces the old hand-written INSERT/UPDATE on `Core.Parameters` in prod. toga2-commerce and toga25-supply poll that key and log the user out on **any** change; the value itself does not matter (see the two FE docs in `related`).
29
+
30
+ ## How it works
31
+
32
+ 1. `post.php` actions: `load` (current value), `queue-update`, `job-status`.
33
+ 2. `App_Developers_ForceLogout::queueLogoutTimeUpdate()` validates strict `Y-m-d H:i:s` and **refuses a value equal to the stored one** (no change = no logout). Logs via `error_log` with the user uuid (audit).
34
+ 3. Queues `Platform/ForceLogout/SetLogoutTime` with `App_Worker::enqueue` (see [worker2-job-bridge](./worker2-job-bridge.md)).
35
+ 4. Page polls `job-status` every 2s: reads `WorkerJobs.isSuccess/dtStarted/output` scoped to this action only; shows only the first line of a failure, file paths stripped.
36
+ 5. worker2 `_Worker_Platform_ForceLogout::SetLogoutTime(value, requestedByUserUuid = '')` loads `_Model_Core_Parameter` by the hardcoded key, sets `value`, `save()`. Turns the query cache off first, then calls `_Database::transactionCommit(database: _underscore::DB_CORE)` explicitly.
37
+
38
+ ## Gotchas
39
+
40
+ - **⚠ Never take the parameter key from input.** `Core.Parameters` also holds api2 secrets (`API_SECRET_ACCESS_TOKEN` etc). Key is a constant in both tools and worker2.
41
+ - **`createFromFormat` alone accepts `2026-02-31`** (rolls over). Validate with a round trip: `format()` of the parsed date must equal the input.
42
+ - **Works on prod tools only today.** Only `config.production.ini` has both `[worker]` (queue_url/queue_region) and `[database_toga2core]`. alpha has Core but no `[worker]`; no beta tools config has a worker queue.
43
+ - **Deploy worker2 before tools**, or the first job fails with an unknown class.
44
+
45
+ ## Change history
46
+ - 2026-10-01 — Built the page + worker2 action, replacing hand-run SQL on Core.Parameters. TRUE-82414. (bala)
@@ -6,11 +6,12 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-23
10
- owners: [mhammontree]
9
+ updated: 2026-10-01
10
+ owners: [mhammontree, bala]
11
11
  files:
12
12
  - tools/_/app/worker.php
13
13
  related:
14
+ - ./force-logout.md
14
15
  - ../../../clients/compass-usa/features/people-file-import-admin-page.md
15
16
  - ./mvc-data-access-patterns.md
16
17
  - ../../../2.0/apps/worker2/architecture.md
@@ -63,6 +64,16 @@ empty result — because the wrapper format is worker2's, not a contract.
63
64
  `maxExecutionTime`. Never make a page wait for long work — poll, don't block.
64
65
  - **Deploy worker2 first.** A Tools page that calls a new action is broken until the action
65
66
  exists.
67
+ - **Most tools configs cannot enqueue.** Only `config.production.ini` has a `[worker]` section
68
+ (queue_url/queue_region); alpha and beta tools have none.
69
+
70
+ ## Local testing (tools → worker2)
71
+
72
+ 1. Tools picks `config.<ENVIRONMENT>.ini` from the `ENVIRONMENT` env var; `config.dev-local.ini` is git-ignored and needs `[worker]` + the DB sections.
73
+ 2. Tools `composer install` fails on local PHP 8.1 (`symfony/filesystem` needs 8.2). Workaround: a git-ignored `tools/vendor/autoload.php` that requires worker2's vendor autoload.
74
+ 3. The AWS SDK ignores the host in `QueueUrl`. Set `AWS_ENDPOINT_URL_SQS` to a local fake SQS that POSTs the message body to `http://worker2/` (Laragon vhost).
75
+ 4. Dev-bypass login must use a `Client_True` email (e.g. `@togatech.com`, not `@goagilant.com`).
66
76
 
67
77
  ## Change history
78
+ - 2026-10-01 — Added the local tools→worker2 test setup and the prod-only `[worker]` config fact (TRUE-82414). (bala)
68
79
  - 2026-09-23 — Created from TRUE-82250: documented the three `enqueue()` outcomes, the log-line-wrapped `output`, and the CTO decision that tools never speaks SFTP (phpseclib 1.x sha1-only KEX + credential duplication). (mhammontree)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-30
9
+ updated: 2026-10-01
10
10
  owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
@@ -106,6 +106,10 @@ The value is `<seconds>-<STATE>`. Three readings, and only the first is a real f
106
106
 
107
107
  - **`1-RUNNING`** (window collapsed ÷3 to 1s) = throwing on **every** run = real freeze. Go read the
108
108
  error (`Logs.Event` by clientId → file:line → `Logs_<Client>.Api` on the transactionId).
109
+ **⚠ Nothing alerts on the way down.** `common_sync_togasupply.php:675-681` shrinks the window
110
+ ÷3 and the paired *"Interval is already at minimum but previous execution still failed to
111
+ finish"* throw is **commented out** (“so the script can continue”), so a phase degrades
112
+ 864000 → … → **1 second** silently and never climbs back. Only the `Parameters` value shows it.
109
113
  - **A large-window `*-RUNNING`** (e.g. `864000-RUNNING`, `288000-RUNNING`) = **usually** healthy
110
114
  mid-run or a deliberate pause/backfill. **⚠ CORRECTED 2026-09-18 — this is NOT sufficient on its
111
115
  own, and the old "NOT stuck, do not fix it" wording caused a real miss.** NYCHH showed
@@ -577,6 +581,23 @@ purchase orders, invoices and item receipts were disabled again on 2026-09-03**
577
581
  is exactly why every NYCHH cursor stops at **2026-09-03**. Before concluding "the sync is broken",
578
582
  check the launcher's toggles at the relevant commit.
579
583
 
584
+ ### ⚠ NYCHH syncs ONCE A DAY — and its live state is in the CLIENT db, not Core
585
+
586
+ Two traps that cost a diagnosis, both NYCHH-specific:
587
+
588
+ - **The `*/5` schedule is not the real cadence.** `cron.worker.sync.json` registers
589
+ `sync_togasupply_hh.php` every 5 minutes like everyone else, but the wrapper calls
590
+ **`App_Framework::cronInitialization(86400)`** (“THIS CRON JOB IS INTENDED TO BE RUN EVERY DAY”),
591
+ so 287 of every 288 invocations exit immediately. Anything NetSuite ships after the morning run
592
+ waits until the next day. Account for that before calling a late import “broken” — and when
593
+ rewinding a cursor, run the wrapper by hand rather than waiting (the 10-day ceiling above means a
594
+ deep rewind would otherwise take weeks).
595
+ - **Read `Client_Nychh.Parameters`, never the `NETSUITE_HH_*` keys in `Core`.** Core still carries
596
+ legacy `NETSUITE_HH_*` rows whose last-sync dates sit in **May 2026**; they are **stale leftovers
597
+ that nothing writes**. The live cursors and execution modes are
598
+ `NETSUITE_LAST_SYNC_CURSOR_*` / `NETSUITE_EXECUTION_MODE_*` in the client DB (see *Data model*).
599
+ Reading the Core copies leads straight to a wrong diagnosis.
600
+
580
601
  ### Replaying one order means rewinding the cursor by hand
581
602
 
582
603
  The cursor is a **seek** position (`<lastmodifieddate>|<internalId>`, see above), so an order whose
@@ -2044,6 +2065,12 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
2044
2065
  [NetSuite Sync Alert Monitor](../../library/features/netsuite-sync-alert-monitor.md).
2045
2066
 
2046
2067
  ## Change history
2068
+ - 2026-10-01 — NYCHH diagnosis traps recorded: the wrapper is gated to **once a day** by
2069
+ `cronInitialization(86400)` despite its `*/5` schedule entry, and the live cursors/modes live in
2070
+ `Client_Nychh.Parameters` — the `NETSUITE_HH_*` keys in `Core` are stale (May 2026) leftovers.
2071
+ Also noted that the “interval already at minimum” throw is **commented out**
2072
+ (`common_sync_togasupply.php:675-681`), so a phase can decay to a 1-second window with no alert.
2073
+ (bala)
2047
2074
  - 2026-09-30 — Closed-order rule v3: drop only when every line is closed and nothing was created from it ([doc](../../library/features/netsuite-closed-order-rule.md)). Sync never auto-creates warehouse Locations (`throwIfUnknownWarehouse`); unit location from the line, not `custitemnumber_location`. Added `process_netsuite_records.php` and the stuck-sync diagnosis path. (rgirish)
2048
2075
  - 2026-09-24 — TO↔PO links: two-direction reconcile (vendor POs no longer upstream; retireStale moves downstream SO links to 326/330); fixed SO downstream line-link deletes on PO re-sync; new `process_netsuite_order.php` + `NETSUITE_SYNC_LOOKUPS_ONLY`. (jcardinal)
2049
2076
  - 2026-09-22 — **The `Logs.Issue` 589 doom-loop's framework root cause is now fully explained and has
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-09-25
9
+ updated: 2026-10-01
10
10
  owners: [jcardinal, mhammontree, bala, ajean]
11
11
  files:
12
12
  - Core/
@@ -238,7 +238,7 @@ This lifecycle is automated by a local browser tool — see the [TOGa 2.0 Client
238
238
 
239
239
  **Every UUID must be freshly, fully random — never derive one from another.** Do **not** take a known uuid and bump a digit (`…-0001`, `…-0002`, …), copy-and-tweak, or otherwise pattern them. When a file needs several literals, generate each one independently at random. (Patterned uuids are guessable and defeat the uniqueness guarantee.) The one intended exception is a fan-out `Client/` grant, which **reuses the same** literal per row across tenants on purpose for safe rollback — deliberate reuse, not incrementing (see the feature doc).
240
240
 
241
- One earlier migration (`Core/2026-05-08 … CronJobs_WorkerCleanup_insert`) used `UUID()` — that is a known violation, not a precedent to copy. First applied correctly in `Core/2026-07-21b - Insert - SprintLockScheduled CronJob.sql`.
241
+ One earlier migration (`Core/2026-05-08 … CronJobs_WorkerCleanup_insert`) used `UUID()` — that is a known violation, not a precedent to copy. First applied correctly in `Core/2026-09-29a - Record sprint and design CronJobs.sql` (new rows 83–86 carry pre-generated v4 literals; the recorded prod rows keep their existing prod uuids, which predate this rule).
242
242
  7. **Guard every registration/seed insert with `NOT EXISTS` (or `INSERT IGNORE`).** Rows that register platform behavior — `ApiPayloadInterceptors`, `Core.RecordFields`/`CustomRecordFields`, cron jobs, ACL rows — must be inserted only if not already present. An **unguarded** registration is not merely non-idempotent: on a fresh environment it can run **alongside a second migration that registers the same behavior**, producing a **double registration** (e.g. two interceptor rows for the same record → the interceptor fires twice). A real instance: the Rate WH `prePost` interceptor is registered unguarded in `Client_Rate/2026-07-15 - PreInterceptor.sql` (record 191, `minDepth NULL`); a second guarded registration would have double-fired it. See `clients/rate/features/whole-home-warranty-purchase-guard.md`.
243
243
  8. **Never reference a database other than the folder's own target.** No `Core.Table` in a `Client_<Name>/` file, no `Client_X.Table` in a `Core/` file, and **no database qualifier at all** in the fan-out folders (`Client/`, `Logs_Client/`, `_modules/`). These databases are on **separate production clusters**; the query works locally and dies in production. Resolve foreign ids with a hardcoded v4 UUID literal or an in-database slug/natural key, or split the work into one file per database folder. See *Database isolation* above — enforced by the `dbchanges2-cluster-isolation` hook.
244
244
  9. **Inserting into `Core.Records` or `Core.RecordFields`? ASK the developer for the next `id`** (the developer takes it in Tools → Developers → Central IDs — the source of truth; never guess from MAX(id), branches hold unreleased ids). These are the **only two tables on the platform with team-maintained primary keys**. Write the assigned `id` as an explicit literal — **never** let `AUTO_INCREMENT` assign it. Conversely, **always hardcode** these `id`s where they are referenced, including in foreign keys (`recordId`, `recordFieldId`) from other databases. See *`Core.Records` / `Core.RecordFields`* above.
@@ -8,6 +8,7 @@ type: feature
8
8
  status: active
9
9
  updated: 2026-10-01
10
10
  owners: [apeterson]
11
+ owners: [apeterson, bala]
11
12
  files:
12
13
  - toga2-commerce/src/hooks/useDeploymentGuard.tsx
13
14
  - toga2-commerce/src/lib/deploymentVersion.ts
@@ -15,6 +16,7 @@ files:
15
16
  - toga2-commerce/src/components/AuthLayout/AuthLayout.tsx
16
17
  - toga2-commerce/src/lib/authStorage.ts
17
18
  related:
19
+ - ../../../../1.0/apps/tools/features/force-logout.md
18
20
  - ./sso-redirect-and-session-gating.md
19
21
  - ./catalog-cache-freshness.md
20
22
  - ../../toga25-supply/features/force-logout-on-deployment.md
@@ -32,6 +34,8 @@ Ported from toga25-supply (`force-logout-on-deployment`). Two commerce-only diff
32
34
 
33
35
  Reached commerce `_production` only in merge `26e2fae7` (2026-09-30, TRUE-81991); supply had it earlier. That gap is why one parameter bump logged supply users out and commerce users not at all.
34
36
 
37
+ **Trigger:** change the value with tools `/developers/force-logout` ([force-logout](../../../../1.0/apps/tools/features/force-logout.md)), not hand SQL.
38
+
35
39
  ## How it works
36
40
 
37
41
  1. One query on `META_LAST_REFRESH_DATETIME`, `enabled` only while authenticated, `staleTime: 0`, `refetchOnWindowFocus: true`, `refetchInterval` **5 minutes**.
@@ -76,4 +80,5 @@ To also fire on an in-app navigation: a `useLocation()` effect calling `refetch(
76
80
 
77
81
  ## Change history
78
82
  - 2026-10-01 — Documented the exact refetch triggers (mount / tab-visibility / 5-min poll; **not** route change), the background-pause behavior, the first-bump no-op, the ignored `key=` param, and the commerce-vs-supply query config difference. Route-change refetch deferred. (apeterson)
83
+ - 2026-10-01 — Trigger is now the tools Force Logout page (TRUE-82414). (bala)
79
84
  - 2026-09-25 — Built for commerce: moved out of `AuthContext` into `<DeploymentGuard />` so a toaster can be shown; added 5-min poll + focus refetch; baseline key added to `AUTH_STORAGE_KEYS`. TRUE-81991. (apeterson)
@@ -7,12 +7,13 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-10-01
10
- owners: [apeterson]
10
+ owners: [apeterson, bala]
11
11
  files:
12
12
  - toga25-supply/src/hooks/useDeploymentGuard.tsx
13
13
  - toga25-supply/src/App.tsx
14
14
  related:
15
15
  - ../../toga2-commerce/features/force-logout-on-deployment.md
16
+ - ../../../../1.0/apps/tools/features/force-logout.md
16
17
  ---
17
18
 
18
19
  Force-signs-out every user when a deploy bumps a Core parameter, so session + caches reset to the new build; open when debugging surprise logouts or `useDeploymentGuard`.
@@ -21,6 +22,8 @@ Force-signs-out every user when a deploy bumps a Core parameter, so session + ca
21
22
 
22
23
  On large deploys the backend bumps Core parameter `META_LAST_REFRESH_DATETIME`. The FE watches it and, when it changes mid-session, force-signs-out every logged-in user so session, `localStorage`, and React Query cache (including the persisted `supply-chain-query-cache`) reset cleanly. Implemented as a headless `<DeploymentGuard />` driven by `useDeploymentGuard`, mounted inside the `AuthProvider` / `ToasterProvider` tree in `App.tsx` — **above** `<AppRouter />`, so the hook cannot use `useLocation()`.
23
24
 
25
+ **Trigger:** change the value with tools `/developers/force-logout` ([force-logout](../../../../1.0/apps/tools/features/force-logout.md)), not hand SQL.
26
+
24
27
  ## How it works
25
28
 
26
29
  1. **Poll the Core parameter.** TanStack `useQuery` reads `META_LAST_REFRESH_DATETIME` via `apiGet("/core-parameters", { fields: ["value"], key: "META_LAST_REFRESH_DATETIME" })`, reading `response.data.coreParameters` (tolerant of an array or a single object). Query: `enabled` only while authenticated, `staleTime: 0`, `refetchOnWindowFocus: true`, `refetchInterval` 5 minutes.
@@ -32,5 +35,6 @@ On large deploys the backend bumps Core parameter `META_LAST_REFRESH_DATETIME`.
32
35
  **When it actually fires, the first-bump no-op, and the ignored `key=` param are identical here** — documented once in [commerce's force-logout doc](../../toga2-commerce/features/force-logout-on-deployment.md) (sections *What actually triggers a refetch* and *Gotchas*). Supply-only difference: `App.tsx` leaves `refetchOnWindowFocus` at React Query's default `true` app-wide, so every on-screen query refetches on tab visibility — not just this one. Sign-out timing is the same in both apps.
33
36
 
34
37
  ## Change history
38
+ - 2026-10-01 — Trigger is now the tools Force Logout page (TRUE-82414). (bala)
35
39
  - 2026-10-01 — Recorded that the guard sits above the router (blocks `useLocation()`); moved the shared trigger/gotcha detail to the commerce doc rather than duplicating it. (apeterson)
36
40
  - 2026-09-16 — Initial implementation. (apeterson)
@@ -11,14 +11,14 @@
11
11
  | [ClickUp Connectivity Watchdog](features/clickup-connectivity-watchdog.md) | Cron watchdog that emails when the ClickUp integration goes silent in business hours; open when touching ClickUp webhook health or `_Worker_Clickup_Health`. |
12
12
  | [ClickUp Custom-Field Conventions (constant IDs, runtime option values, Task Type migration)](features/clickup-custom-field-conventions.md) | How worker2 code must reference ClickUp custom fields; open before reading/writing any ClickUp custom field or touching the Task Type migration. |
13
13
  | [ClickUp Design Sprint Automation (Final Design Outcome)](features/clickup-design-sprint-automation.md) | `_Worker_Clickup_Design` drives the design-sprint workflow in ClickUp (FDO default on create, spawn iteration / complete task on Final Design Outcome, daily rev |
14
- | [ClickUp General Automation (Sub-Environment reservation, Has UI / Has Code roll-ups)](features/clickup-general-automation.md) | `_Worker_Clickup_GeneralAutomation` reacts to ClickUp custom-field changes on a Task; open when working on sub-environment reservation or Has UI / Has Code roll |
14
+ | [ClickUp General Automation (Sub-Environment reservation, Has UI / Has Code, review-field reset)](features/clickup-general-automation.md) | `_Worker_Clickup_GeneralAutomation` keeps QA/QC fields consistent on ClickUp field/status changes (sub-environment reservation, Has UI / Has Code, review-field |
15
15
  | [ClickUp GitHub-tab Auto-linking & Ticket-id Branch Naming](features/clickup-github-autolink.md) | How ClickUp surfaces branches/PRs/commits in a ticket's GitHub tab and the branch/PR-title naming that triggers it; open when naming branches for ticket work or |
16
16
  | [ClickUp Project & Opportunity Multi-List Routing](features/clickup-project-routing.md) | Routes ClickUp tasks into secondary multi-list memberships by custom-field values; open for opportunity/epic routing, the stage-is-a-list constraint, or webhook |
17
17
  | [ClickUp Rich-Text Custom Fields via Quill Delta (API)](features/clickup-richtext-api.md) | How to write formatted text into a ClickUp text/long-text custom field via the undocumented `value_richtext` Quill Delta key; open when a richtext ClickUp field |
18
18
  | [ClickUp Subtask Activity → Parent Opportunity/Epic Comments](features/clickup-subtask-activity.md) | Mirrors subtask status/comment activity up to its top-level Opportunity/Epic as one comment; open for the subtask fan-out, its dedup ledger, or structural ances |
19
19
  | [ClickUp Task Description Fluffer (Talos KB Investigation)](features/clickup-task-fluffer-talos.md) | Async worker that "fluffs" a new ClickUp task by running its name+description through the Talos `dev-core` KB agent and writing the report back; open for the fl |
20
20
  | [ClickUp Webhook Health Monitor → OneUptime (Monitor/Clickup/CheckWebhook)](features/clickup-webhook-health-monitor.md) | worker2 cron that checks the team's ClickUp webhook subscriptions and pages via a OneUptime Incoming-Request monitor; open when tuning the ClickUp webhook alarm |
21
- | [ClickUp Work Type Automation (Committed / Conditional / Stretch)](features/clickup-work-type-automation.md) | Auto-maintains each ClickUp task's Work Type (Committed/Conditional/Stretch) from its dependencies, status, and due date, and sets new-task defaults on taskCrea |
21
+ | [ClickUp Work Type Automation (Committed / Conditional / Stretch)](features/clickup-work-type-automation.md) | Auto-maintains each ClickUp task's Work Type (Committed/Conditional/Stretch) from its dependencies, status, and due date, and sets sprint-list defaults (Work Ty |
22
22
  | [worker2 Codeception integration tests (2.0 test-framework pilot)](features/codeception-integration-tests.md) | First 2.0 app-level automated test suite: Codeception 5 / PHPUnit 11 in worker2 (pilot, Jeff's direction), real-DB integration tests with hard local-only safety |
23
23
  | [worker2 — Committed Secrets & Config Exposure (debt register)](features/committed-secrets-and-config-debt.md) | Standing list of credentials committed to the worker2 repo (locations only); open before adding a credential to worker2 or assuming a config value is private. |
24
24
  | [Compass VIP Support Importer (worker2)](features/compass-vip-support-importer.md) | worker2 action that ingests Compass's quarterly VIP spreadsheet and sets each VIP's support tech (`Users.c_supportedByUserId` in `Client_Compass`); open when wo |
@@ -60,7 +60,7 @@
60
60
  | [Talos Meeting-Notes Integration & Token Auto-Refresh (consumer)](features/talos-meeting-notes-integration.md) | Consumer side of Talos: the endpoints, DevCore assistant, and JWT auto-refresh a worker2 dev tool needs to query the meeting-notes corpus; open when a tool must |
61
61
  | [Talos Pricing Automation (worker2 Cron — Usage Import, AWS Actuals, Margins, Profiles)](features/talos-pricing-automation.md) | Monthly worker2 crons that turn raw usage + AWS cost into a measured platform unit price, per-client cost, and margins; open for the pricing pipeline, its AWS c |
62
62
  | [Talos Transcript Ingestion Pipeline (worker2 → AWS Bedrock KBs)](features/talos-transcript-ingestion.md) | `_Worker_Team_Transcripts` cron pipeline ingesting Teams VTT transcripts into Talos Bedrock KBs + emailing organizer recaps; open for Export/Process/Retry/SyncK |
63
- | [Team Sprint Management & Reporting](features/team-sprint-management.md) | `_Worker_Team_Sprint` engine for TOGA's biweekly dev-sprint process + reporting (warnings, lock, daily dashboard, sprint-end scoring, AI release notes); open fo |
63
+ | [Team Sprint Management & Reporting](features/team-sprint-management.md) | `_Worker_Team_Sprint` engine for TOGA's biweekly dev-sprint process + reporting (warnings, lock, daily dashboard, sprint-end scoring, AI release notes, 7 PM In |
64
64
  | [Teams Meeting Transcript Export](features/teams-transcript-export.md) | How `Team/Transcripts/Export` polls Microsoft Graph for Teams transcripts; open for the Graph organizer-resolution, GUID-vs-UPN, and Entra-permission details (t |
65
65
  | [Centralized Tracking-Status Refresh (worker2 Sync cron, all clients)](features/tracking-status-refresh.md) | Platform-wide worker2 cron polling FedEx/USPS/UPS to keep `TrackingNumbers.status` current for every provisioned client via api2 PUT; open for the dispatcher/cu |
66
66
  | [VAPI Webhook Handler (worker2 — AI-BDR end-of-call processing)](features/vapi-webhook-handler.md) | worker2 handler for VAPI's end-of-call report (writes results onto the `ContactAttempt`, dispatches the post-call action) + the outbound dialer eligibility rule |
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-25
9
+ updated: 2026-10-01
10
10
  owners: ["ajean", "jcardinal"]
11
11
  files:
12
12
  - worker2/Worker/Clickup/Design.php
@@ -24,7 +24,7 @@ related:
24
24
 
25
25
  `_Worker_Clickup_Design` replaces a set of native ClickUp automations. When a task's **Final Design Outcome** (FDO) drop-down is set to an actionable value, the worker spawns an iteration task or completes the task; a daily cron reminds the Design Approver of pending reviews.
26
26
 
27
- **Current state (2026-09-25):** live on both design folders (`DESIGN_SPRINTS_FOLDER_IDS`: True Design Sprints `90115939842`, Design Sprints `90116585900`) + Backlog Design lists. Approve→complete and iterate→spawn verified working after the 9/21 multi-folder gate fix (commit `6dcebe0`); earlier misses on the new folder were the old single-folder gate. History of the 2026-07 no-op / no-spawn fixes (worker2 PR #98, #113 + _underscore PR #669) is in Gotchas. The native ClickUp automations masking the original no-op must stay disabled.
27
+ **Current state (2026-10-01):** live on Design Sprints `90116585900` (`DESIGN_SPRINTS_FOLDER_IDS`) + Backlog Design lists. The old True Design Sprints folder `90115939842` was **removed** from the gate (old folders get no automation; open tasks there are closed by hand). FDO-on-create, approve→complete and iterate→spawn verified working since Jeff's 9/25 deploy; failures reported after that predated it. History of the 2026-07 no-op / no-spawn fixes (worker2 PR #98, #113 + _underscore PR #669) is in Gotchas. The native ClickUp automations masking the original no-op must stay disabled.
28
28
 
29
29
  ## How it works
30
30
 
@@ -47,7 +47,7 @@ related:
47
47
  - `Worker/Clickup.php` — routes ClickUp webhooks.
48
48
  - `shouldEmailDesignApprover($payload, $taskDetails)` — the single predicate gating the "Action Needed: Design Review" email (requires history_items[0] is a Design Approver custom_field change, task status still `iterated`, and user-driven: `user.id != -1`).
49
49
  - `taskUpdated` path (~822) and `taskStatusUpdated` path (~230) both gate the email on this predicate.
50
- - `Controller/ClickupDesignTest.php` — regression tests reached via the `?controller=ClickupDesignTest` route (worker2 has **no PHPUnit harness**; mirrors the `CompassPeopleTest` convention). Uses `ReflectionMethod` to reach the private methods.
50
+ - `Controller/ClickupDesignTest.php` — regression tests (updated 2026-10-01: the new-task case now expects `in progress`) reached via the `?controller=ClickupDesignTest` route (worker2 has **no PHPUnit harness**; mirrors the `CompassPeopleTest` convention). Uses `ReflectionMethod` to reach the private methods.
51
51
 
52
52
  ### Flow
53
53
 
@@ -80,7 +80,7 @@ Configuration lives as ClickUp identifiers (folder IDs, custom-field IDs, drop-d
80
80
  - **Never use strict `===` when reading a ClickUp `drop_down` value/orderindex/id (ROOT CAUSE of the silent no-op).** ClickUp returns `value`/`orderindex` with **inconsistent int-vs-string typing** (`"2" !== 2`), so `resolveFinalDesignOutcomeOptionId` matched nothing, returned `null`, and `handleFinalDesignOutcome` hit its early return (job still `isSuccess=1`). Fixed by comparing **as strings**: `(string)$option->orderindex === (string)$field->value` (and the same for `$option->id`). **Every other** drop_down reader in `Worker/Clickup.php` relies on PHP **array-key coercion** (`$optionsByIndex[$option->orderindex]` then indexed by `$customField->value`) precisely because of this typing inconsistency — strict `===` anywhere reading a ClickUp drop_down value is a bug. The native automation `c25fd345` had been masking this no-op in prod.
81
81
  - **`taskStatusUpdated` switches on `before->status`, NOT `after->status`** (Clickup.php ~71). A `case '<status>'` in that switch fires when a task **LEAVES** that status — the in-code comment claiming "just changed TO iterated" is misleading. This is the trap behind the second spurious-email vector: the `iterated` case fired on `completeTask()`'s `iterated → complete` transition during `spawnIterationTask`. Anyone adding status-transition logic must account for this direction. The "entering iterated" trigger added this session is correctly placed under `switch ($historyItem->after->status)` (`case ...::STATUS_ITERATED` → `handleTaskEnteredIterated()`) — distinct from the `before->status` switch.
82
82
  - **Spurious "Design Review" email — fixed on a single predicate.** Both vectors (the `taskUpdated` Design-Approver-field path ~822 and the `taskStatusUpdated` `iterated` path ~230) now gate on `shouldEmailDesignApprover($payload, $taskDetails)`, which requires: `history_items[0]` is a Design Approver custom_field change, task status still `iterated`, and the change is **user-driven** (`user.id != -1`; ClickBot/automation writes carry `user.id == -1`). The dead `$isDesignApproverSet` block was removed. Consequence: a pure status change **into** iterated (approver already set, no field change) no longer emails from the status path — the legitimate review email flows from the `taskUpdated` field handler.
83
- - **Native automations must stay disabled once the worker is verified.** The worker replaces: `c25fd345`, `272f743e`, `a64c863a`, `7739cc41`, `08570f31`, `70b09962`. Keep sprint-readiness automations only: `10df1786`, `f6214678`. Re-enabling any of the replaced set reintroduces the duplicate+close and spurious-email behavior.
83
+ - **Native automations must stay disabled once the worker is verified.** The worker replaces: `c25fd345`, `272f743e`, `a64c863a`, `7739cc41`, `08570f31`, `70b09962`. Keep sprint-readiness automations only: `10df1786`, `f6214678` — these existed only on the old folder and **must be rebuilt by hand on Design Sprints** (open as of 2026-10-01). Re-enabling any of the replaced set reintroduces the duplicate+close and spurious-email behavior.
84
84
  - **The spawn gate must admit Backlog Design lists — folder + name, not just the sprint folder (ROOT CAUSE of the 2026-07-15 intermittent no-spawn).** `isDesignSprintTask()` originally accepted only the True Design Sprints folder or two hardcoded `DESIGN_LIST_IDS`, so a design task in a **Backlog Design list** was silently skipped (no iteration spawned). The gate now also admits `BACKLOG_FOLDER_ID = '90116192732'` when the list name matches `DESIGN_LIST_NAME_PATTERN`. **Regex trap:** the pattern is `/(?<!non-)\bdesign\b/i` — a bare `\bdesign\b` wrongly matched **"Non-Design Overflow"** because the hyphen is itself a word boundary, so the negative-lookbehind `(?<!non-)` is required; the `\b` boundary also correctly excludes **"Redesign"**. `SendReviewReminders()` applies the same folder+pattern so new backlog Design lists get reminders without a code change.
85
85
  - **Resolve the FDO from the webhook `after` value, NOT the per-process snapshot (read-after-write race — second root cause of the 2026-07-15 failure).** `handleFinalDesignOutcome` used to resolve the outcome from the cached `getTaskDetails` snapshot fetched early in the request, which can **predate** ClickUp reflecting the drop-down write → it read the OLD value → silent skip. The webhook's authoritative new value (`history_items[].after`) is now threaded through `handleFinalDesignOutcome(..., ?string $webhookFdoValue)` → `resolveFinalDesignOutcomeOptionId(..., ?string $overrideValue)` (override wins over the snapshot value; the option *list* is still read from the snapshot). `_Worker_Clickup::extractCustomFieldAfterValue()` normalizes the `after` shape and — critically — distinguishes a **missing** `after` (→ `null` → fall back to snapshot) from an **explicit null** `after` (→ `''` sentinel → field was cleared → resolve to no outcome; do NOT act on the stale previous option). The previously-silent resolver-null return now logs a `[ClickupDesign]` breadcrumb.
86
86
  - **`_Component_Api_Clickup::send()` throws on transient failures — it no longer silently returns NULL (shared-core, `_underscore` PR #669).** The old retry loop never actually retried and returned NULL on HTTP `>= 502`, **silently dropping** the API call. Now transient failures — HTTP 502/503/504, 429, and transport-level errors (`responseCode === 0` / cURL failure) — **throw** so the worker job fails loudly and the **queue** retries; other non-2xx still throw. Retry is intentionally left to the queue (no in-worker sleep-and-retry; CTO Option C). **It throws the base `Exception`** — the same type every other `_Component_Api` client throws and the framework error handler / Sentry already track — NOT a bespoke exception class. (A `_Exception_Api_Transient` subclass was prototyped and dropped precisely because a new type sits outside the existing Sentry/error path.) Exception messages never embed the full `$response` body (task/user PII); the private `summarizeApiError()` surfaces only ClickUp `err`/`ECODE` with a bounded/truncated fallback. Affects **every** caller of `send()` across the 2.0 apps — grep `_Component_Api_Clickup::send(` before assuming a caller tolerated the old null-on-failure return.
@@ -90,3 +90,8 @@ Configuration lives as ClickUp identifiers (folder IDs, custom-field IDs, drop-d
90
90
 
91
91
  - [ClickUp Work-Type Automation](./clickup-work-type-automation.md)
92
92
  - [team-sprint-management](./team-sprint-management.md)
93
+
94
+ ## Change history
95
+ - 2026-10-01 — Old True Design Sprints folder `90115939842` dropped from `DESIGN_SPRINTS_FOLDER_IDS`; natives 10df1786/f6214678 to rebuild on Design Sprints (ajean; worker2 PR #178)
96
+ - 2026-09-21 — Multi-folder gate (commit `6dcebe0`) (jcardinal)
97
+ - 2026-07-15 — Backlog Design lists + webhook `after` value fix (worker2 #113, _underscore #669) (ajean)
@@ -1,59 +1,78 @@
1
1
  ---
2
- title: ClickUp General Automation (Sub-Environment reservation, Has UI / Has Code roll-ups)
2
+ title: ClickUp General Automation (Sub-Environment reservation, Has UI / Has Code, review-field reset)
3
3
  framework: "2.0"
4
4
  repo: worker2
5
5
  project: Worker
6
6
  client: shared
7
7
  type: feature
8
- status: draft
9
- updated: 2026-09-16
10
- owners: [jcardinal]
8
+ status: active
9
+ updated: 2026-10-01
10
+ owners: ["jcardinal", "ajean"]
11
11
  files:
12
12
  - worker2/Worker/Clickup/GeneralAutomation.php
13
13
  - worker2/Worker/Clickup.php
14
+ - worker2/Worker/Github.php
14
15
  - _underscore/Component/Api/Clickup/Clickup.php
15
16
  related:
16
17
  - ./qa-qc-review-pipeline.md
17
18
  - ./clickup-work-type-automation.md
18
19
  - ./clickup-custom-field-conventions.md
20
+ - ./team-sprint-management.md
19
21
  ---
20
22
 
21
- `_Worker_Clickup_GeneralAutomation` reacts to ClickUp custom-field changes on a Task; open when working on sub-environment reservation or Has UI / Has Code roll-ups.
23
+ `_Worker_Clickup_GeneralAutomation` keeps QA/QC fields consistent on ClickUp field/status changes (sub-environment reservation, Has UI / Has Code, review-field reset to Needs Review); open when one of those fields is wrong or you add a QA/QC field rule.
22
24
 
23
25
  ## Summary
24
26
 
25
- **New** worker, fanned out from `_Worker_Clickup::Webhook` (shared ClickUp webhook entry). Part of the platform-wide QA/QC dev-process automation driven by the approved plan (`QA-QC Plan.txt`). **Not deployed yet** — lives on the `feature-qa-qc` branch working tree.
26
-
27
- Three reactions today:
28
- 1. **Sub-Environment auto-reservation** — when a Task needs a reserved environment.
29
- 2. **Has UI / Has Code roll-up** to the parent Feature.
30
- 3. **Has Code backfill** on "Development Finished" when a PR exists.
27
+ Action `Clickup/GeneralAutomation/Process`, fanned out from `_Worker_Clickup::Webhook` on
28
+ `taskUpdated` / `taskStatusUpdated`. Implements the "General Automation" section of the team QA/QC
29
+ plan as seven numbered rules (listed in the class docblock). Live in prod; Dev Sprints folder scope.
31
30
 
32
31
  ## How it works
33
32
 
34
- Entry: `_Worker_Clickup::Webhook` inspects the field change and fans out to `_Worker_Clickup_GeneralAutomation` (alongside the QA/QC review chain — see [qa-qc-review-pipeline](./qa-qc-review-pipeline.md)).
35
-
36
- ### Sub-Environment auto-reservation
37
-
38
- - Triggers when **Client Approval Required** is set **and** Task Type = **Feature**.
39
- - Reserves a Greek-letter sub-environment **atomically** from the `SubEnvironments` table: a **guarded `UPDATE`** claims a free row, and the worker **retries** if the guarded update claimed nothing (another task won the race). Safe pattern instead of select-then-update, which double-books under concurrent webhooks.
40
- - The reserved letter is written back to the Task.
41
-
42
- ### Has UI / Has Code roll-up to the parent Feature
43
-
44
- - When a child Task's **Has UI** or **Has Code** flag changes, the worker rolls the value up to the parent **Feature** task (a Feature "has code" if any child does, same for UI).
45
-
46
- ### Has Code backfill on Development Finished
47
-
48
- - When a Task **enters** "Development Finished", if a **PR exists** for it the worker backfills the **Has Code** flag. Covers tasks whose Has Code was never set by hand but that clearly produced code.
33
+ ### The rules (`GeneralAutomation.php`)
34
+
35
+ 1. **Client Approval Required** checked → check **Reserve Sub-Environment**.
36
+ 2. **Task Type = Feature** → check Reserve Sub-Environment.
37
+ 3. Reserve Sub-Environment checked → claim the next free Greek letter from `SubEnvironments` with a
38
+ **guarded `UPDATE` + retry** (never select-then-update) and set **Sub-Environment**.
39
+ 4. Reserve Sub-Environment unchecked → clear Sub-Environment, release the row.
40
+ 5. A subtask's **Has UI / Has Code Changes** → roll up onto the parent Feature (any child = parent).
41
+ 6. Status → **Development Finished**, a PR exists, Has Code Changes unchecked → check it.
42
+ 7. Status **enters** `ui review` / `front-end review` / `back-end review` → reset the matching field
43
+ **UI Review** (`c08e3ca2…`) / **Front-end Review** (`e21c888a…`) / **Back-End Review**
44
+ (`227480b6…`) to **Needs Review**. Dev Sprints only, not Feature tasks (`FeatureReview` owns
45
+ those); skipped if already Needs Review. `QaReview` / `FeatureReview` treat Needs Review as a
46
+ no-op, so the reset does not loop.
47
+
48
+ ### PR → "Has Code Changes"
49
+
50
+ - `hasPullRequest(string $customTaskId)` (public) — **exact** match on `PullRequests.title`, which
51
+ stores only the task custom id (`_Worker_Github::prTitleToCustomId`). A `LIKE` prefix match was a
52
+ false-positive risk (`TRUE-123` matched `TRUE-1234`). Shared with
53
+ `_Worker_Clickup::handleDevelopmentFinished`, which checks for a PR itself so it does not race rule 6.
54
+ - `Worker/Github.php` `setHasCodeChangesFromPr()` — checks Has Code Changes when a PR row is **first
55
+ recorded** (task resolved via `Team.Tasks` customClickupIdentifier → clickupIdentifier). Best-effort,
56
+ never throws: the GitHub webhook must keep returning 200.
57
+
58
+ Sprint-list defaults set on create / move (Condition, Has UI Changes, Work Type) live in
59
+ [ClickUp work-type automation](./clickup-work-type-automation.md#sprint-list-defaults-taskcreated--taskmoved).
49
60
 
50
61
  ## Gotchas
51
62
 
52
- - **Reserve the sub-environment with a guarded `UPDATE` + retry, never select-then-update.** Concurrent webhooks will otherwise hand the same Greek letter to two tasks.
53
- - **Custom-field option VALUES are resolved at runtime, IDs are constants.** Field IDs come from the `CLICK_UP_CUSTOM_FIELD_ID__*` constants in `_Component_Api_Clickup`; dropdown option values are read at runtime from `type_config->options` (by `orderindex`, set by option id) — never hardcode an option's numeric value. See [ClickUp custom-field conventions](./clickup-custom-field-conventions.md).
63
+ - **Reserve the sub-environment with a guarded `UPDATE` + retry, never select-then-update.**
64
+ Concurrent webhooks would hand the same letter to two tasks.
65
+ - **Field IDs are constants, option VALUES are resolved at runtime** — see
66
+ [ClickUp custom-field conventions](./clickup-custom-field-conventions.md).
67
+ - **Review fields always reset on entry (owner decision 2026-10-01).** Do not "keep" an old
68
+ Approved/Denied when a task re-enters a review status.
54
69
 
55
70
  ## Related
56
71
 
57
72
  - [QA/QC review pipeline](./qa-qc-review-pipeline.md)
58
73
  - [ClickUp Work-Type Automation](./clickup-work-type-automation.md)
59
74
  - [ClickUp custom-field conventions](./clickup-custom-field-conventions.md)
75
+
76
+ ## Change history
77
+ - 2026-10-01 — Rule 7 review-field reset; Has Code Changes on first PR record; `hasPullRequest()` exact match (ajean; worker2 PR #178)
78
+ - 2026-09-16 — Initial rules 1–6 (jcardinal)
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-25
10
- owners: ["jcardinal"]
9
+ updated: 2026-10-01
10
+ owners: ["jcardinal", "ajean"]
11
11
  files:
12
12
  - worker2/Worker/Clickup.php
13
13
  - worker2/Tests/Worker/ClickupWorkTypeTest.php
@@ -16,7 +16,7 @@ related:
16
16
  - ../workflows/clickup-credential-rotation.md
17
17
  ---
18
18
 
19
- Auto-maintains each ClickUp task's Work Type (Committed/Conditional/Stretch) from its dependencies, status, and due date, and sets new-task defaults on taskCreated; open when work-type promotion/demotion or taskCreated field writes misbehave.
19
+ Auto-maintains each ClickUp task's Work Type (Committed/Conditional/Stretch) from its dependencies, status, and due date, and sets sprint-list defaults (Work Type, Condition, Has UI Changes) on create/move; open when work-type promotion/demotion or those default writes misbehave.
20
20
 
21
21
  ## Summary
22
22
 
@@ -42,12 +42,29 @@ Flow:
42
42
  - `Stretch` with a due date inside the current sprint → set Committed/Conditional as above.
43
43
  6. When a task is depended on by another (`$dependency->depends_on == $taskId`), it recurses into the blocking direction so completing a task re-evaluates the tasks it was blocking. The `$dependsOnTaskId` argument breaks two-node cycles.
44
44
 
45
- ### New-task defaults (`taskCreated`)
45
+ ### Sprint-list defaults (`taskCreated` + `taskMoved`)
46
46
 
47
- - Every optional field write in `taskCreated` goes through private `setFieldSafely($taskId, $fieldLabel, callable)`: try/catch → `error_log` + Sentry. One refused field never aborts the job (the later Clickup/Fluffer/Process dispatch still runs).
48
- - **Work Type = Unplanned** when empty, only if the list name starts with `Sprint` AND `isTaskInQaQcScope()` (Dev Sprints folder). Option id resolved by name at runtime (`WORK_TYPE_UNPLANNED`). Non-`Sprint` lists get no Work Type write (jcardinal decision).
49
- - **Previous Sprint Work Type** = the Work Type name (or `Unplanned` when empty), resolved by name via the field's own options.
50
- - Design FDO default is dispatched from here too — see [design sprint automation](./clickup-design-sprint-automation.md).
47
+ Applied by `applySprintEntryDefaults()` when a task is **created in**, or **moved into**, a list whose
48
+ name starts with `Sprint` inside the Dev Sprints or Design Sprints folder
49
+ (`isInDevOrDesignSprintsFolder()`). Backlog and other lists get nothing.
50
+
51
+ - **Condition** = On Track if empty (`defaultConditionOnTrack()`), then **Previous Sprint Condition**
52
+ = Condition (`CLICK_UP_CUSTOM_FIELD_ID__PREVIOUS_CONDITION`).
53
+ - **Has UI Changes** checked (Dev Sprints only).
54
+ - **Work Type** = Unplanned if empty (Dev Sprints only; `defaultWorkTypeUnplanned()`, option resolved
55
+ by name).
56
+ - **Previous Sprint Work Type** = the Work Type name (or Unplanned) — **only on sprint lists** (it used
57
+ to fire on Backlog too).
58
+ - Every optional write goes through private `setFieldSafely($taskId, $fieldLabel, callable)`:
59
+ try/catch → `error_log` + Sentry, so one refused field never aborts the job (the later
60
+ Clickup/Fluffer/Process dispatch still runs). Dropdowns are set by option name
61
+ (`setDropdownByName()`).
62
+ - Design FDO default is dispatched from `taskCreated` too — see
63
+ [design sprint automation](./clickup-design-sprint-automation.md).
64
+
65
+ **Dev sprint data ingestion** (sprint tag, `ensureTaskExistsInCurrentSprint`, dependencies) requires
66
+ the task's **HOME** folder = Dev Sprints (`isHomeFolderInQaQcScope()`), not just any list membership —
67
+ so Design tasks multi-listed into a Dev list stay out of Dev reports (owner decision).
51
68
 
52
69
  Data model: reads/writes the `Team` database (`Tasks`, `Developers`, `Tasks_Developers`, `Sprints`) and the ClickUp REST API. The Work Type itself lives in ClickUp as a custom field (`CLICK_UP_CUSTOM_FIELD_ID__WORKTYPE`), not in the DB.
53
70
 
@@ -58,8 +75,15 @@ Data model: reads/writes the `Team` database (`Tasks`, `Developers`, `Tasks_Deve
58
75
  - **No PHPUnit harness** in worker2. The regression test `Tests/Worker/ClickupWorkTypeTest.php` is a plain-PHP script (reflection on `isStatusComplete`) run with `php Tests/Worker/ClickupWorkTypeTest.php`.
59
76
  - **ClickUp dropdown custom-field `value` is the option ORDERINDEX, not its name.** The webhook payload for a dropdown/labels custom field delivers the selected option's integer `orderindex`, *not* the option's display name. Any name-keyed lookup (e.g. `$lookupPreviousSprintWorkTypeByName`) will silently never match if fed the raw `value`. Resolve the name first via the field's own `type_config->options` — build a map keyed by `orderindex` (`$workTypesByIndex[$opt->orderindex] = $opt`) and read `->name` from it. This is the proven pattern in `_Worker_Team_Sprint::SprintLock` (`Worker/Team/Sprint.php`, the Work Type resolve ~L3894 and the POST ~L3971). Guard the lookup with `isset($workTypesByIndex[$value])` so a stale/deleted option orderindex doesn't warn.
60
77
 
78
+ - **Home folder vs. any list.** `isTaskInQaQcScope()` matches any list the task is in (good for QA/QC
79
+ gates); sprint **data ingestion** must use `isHomeFolderInQaQcScope()`. Mixing them up leaks Design
80
+ tasks into Dev sprint reports.
61
81
  - **2026-09-21..25 outage: every taskCreated failed (~113 jobs, 0 Fluffer jobs).** The automation user lacked edit rights on Previous Sprint Work Type → HTTP 401 `ACCESS_606`; the throw killed the rest of the job. Fixed by field permissions + `setFieldSafely`. Any new optional write must use `setFieldSafely`. Account-change check: [credential rotation](../workflows/clickup-credential-rotation.md).
62
82
 
63
83
  ## Related
64
84
 
65
85
  - [ClickUp Project & Opportunity Multi-List Routing](./clickup-project-routing.md)
86
+
87
+ ## Change history
88
+ - 2026-10-01 — Defaults also on taskMoved into a `Sprint*` list; Condition / Previous Sprint Condition / Has UI Changes added; Previous Sprint Work Type limited to sprint lists; ingestion gated on HOME folder (ajean; worker2 PR #178)
89
+ - 2026-09-25 — `setFieldSafely` after the ACCESS_606 taskCreated outage (jcardinal)
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-25
10
- owners: ["kyalamarthi"]
9
+ updated: 2026-10-01
10
+ owners: ["kyalamarthi", "ajean"]
11
11
  files:
12
12
  - worker2/.gitignore
13
13
  - worker2/Worker/Team/Github.php
@@ -43,7 +43,8 @@ the publish on a credential literal.
43
43
  |---|---|---|
44
44
  | GitHub personal access token, `agilantgit` account | `worker2/Worker/Team/Github.php:9` **and** `worker2/.ebextensions/git.json:4` (same token, two copies) | Committed since 2025-01-17 and present in **166 of 167 branches**, so scrubbing history is not a fix — **rotation is the only fix**. Org-wide reach. It is also interpolated into a `shell_exec` clone URL, so it shows up in the instance process list. |
45
45
  | NetSuite EC private key | `worker2/Config/netsuite_ec_key.pem` | Tracked in git as a file. |
46
- | TOGa Talos (`api.togaiq.com`) API key | `worker2/Worker/Team/Sprint.php:43`, used ~L422 | Still a class constant. There is **no `[togaiq]` config group yet**, so moving it means adding the config group first. |
46
+ | TOGa Talos (`api.togaiq.com`) API key — **old** release-notes key | git history only: `worker2/Worker/Team/Sprint.php` before PR #179 (still visible at commit `8613ace`) | Code no longer uses it, but it was committed, so it **must be revoked** in TOGa Talos. Open until revoked. |
47
+ | TOGa Talos API key — **current** (`[talos] api_key`) | `worker2/Config/production.ini` `[talos]` group (~L230) | Shared by release notes and the transcript pipeline. Committed like every `Config/*.ini` (see Summary). |
47
48
  | PayPal webhook signature verification **off** in production | `worker2/Config/production.ini:182` | Impact already written up — a caller who knows the URL can create a paid SalesOrder. See [Rate PayPal subscription webhook](../../../../clients/rate/features/paypal-subscription-purchase-webhook.md). |
48
49
 
49
50
  ### Fixed / no longer true
@@ -54,6 +55,12 @@ the publish on a credential literal.
54
55
  `Authorization` header written into `Logs.Api`, are separate open items — see
55
56
  [_ApiRequest gotchas](../../_underscore/features/apirequest-json-content-type.md#gotchas).
56
57
 
58
+ - **TOGa Talos key in `Sprint.php`** (2026-10-01, worker2 PR #179) — the `AI_API_KEY` constant is
59
+ **gone**. `aiApiKey()` reads `[talos] api_key`, the same `/api/ai/generate` key the transcript
60
+ pipeline already uses (no new `[togaiq]` group). This removed a **second** copy of a TOGa Talos key
61
+ from PHP — but `[talos] api_key` is itself committed in `production.ini` (table above), so treat it
62
+ as a refactor, not a fix.
63
+
57
64
  ## Gotchas
58
65
 
59
66
  - **Config is not private here.** Moving a secret from a PHP constant into
@@ -75,3 +82,7 @@ the publish on a credential literal.
75
82
  - [Team Sprint Management & Reporting](./team-sprint-management.md)
76
83
  - [_ApiRequest — encode/decode & api-logging behavior](../../_underscore/features/apirequest-json-content-type.md)
77
84
  - [Rate PayPal Subscription Purchase & Webhook Pipeline](../../../../clients/rate/features/paypal-subscription-purchase-webhook.md)
85
+
86
+ ## Change history
87
+ - 2026-10-01 — TOGa Talos release-notes key moved to `[talos] api_key`; old key listed for revocation (ajean)
88
+ - 2026-09-21 — Register created from audit (kyalamarthi)