toga-ai 1.0.788 → 1.0.790

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.
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-08
9
+ updated: 2026-09-09
10
10
  owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
@@ -257,6 +257,45 @@ again, the sections still could **not advance** — the value was in the **wrong
257
257
  This is the same SuiteQL-vs-REST clock trap as the date-only-format bug above — see the
258
258
  [SuiteQL/REST shim doc](../../library/features/netsuite-suiteql-rest-shim.md).
259
259
 
260
+ ### ⚠ …and the cursor was READ on the wrong clock too — `strtotime()` (Chicago) shifted the fetch window +1h → SILENT record skips across ALL 6 sections + ALL clients (found + fixed 2026-09-09, NYCHH prod)
261
+
262
+ The deepest of the cursor-clock bugs, and the one that actually **dropped records that never came
263
+ back**. The two clock bugs above are on the **write** side of the cursor; this one is on the
264
+ **read** side of the same value, and it was silent — no crash, no alert, no frozen section. It
265
+ hit **every section and every client**, not just the by-reference ones.
266
+
267
+ - **Root cause — read clock ≠ fetch clock.** The cursor value `"<lastmoddate>|<netsuiteId>"` stores
268
+ its datetime in the NetSuite account timezone = **America/New_York (Eastern)** — the same clock the
269
+ list-fetch `WHERE` bounds render in (`App_Api_Netsuite_Rest::listItemFulfillments` et al. call
270
+ `->setTimezone('America/New_York')`). But `startModeIteration()`
271
+ (`common_sync_togasupply.php` ~L622) read it back with **`strtotime()`**, which parses in the
272
+ **script timezone America/Chicago** (set in library `_.php`). Chicago is 1h behind Eastern, so the
273
+ same digits produced an epoch **1 hour later**; the list fetch then re-rendered that bound in
274
+ Eastern, shifting the window's **lower bound +1 hour**.
275
+ - **Effect — a silent fetch-level skip.** Any record whose Eastern lastmod fell in that **1-hour
276
+ shadow** right after the previous checkpoint was **excluded from the fetch**; the window read empty,
277
+ and the `|0` empty-window jump advanced the cursor **past** it. Nothing threw — a throw still
278
+ freezes the section loudly; this is the quiet path that loses data. Combined with the giant-record
279
+ 900s timeouts (below) that historically froze sections and were then manually stepped over, the
280
+ gaps compounded over time.
281
+ - **Proven on NYCHH prod** (item fulfillment `6138135`, lastmod **2025-09-04 17:26:58 Eastern**): the
282
+ +1h bound (`> 17:29:03`) returned **0** records; the correct bound (`> 16:29:03`) returned **1**
283
+ (that record). The cursor trace showed the `|0` empty-window jump right over its time slot.
284
+ - **Fix (worker only, deployed + verified):** parse the cursor as Eastern in `startModeIteration` —
285
+ `$epochLastIntegration = (new \DateTime($dtLastIntegration, new \DateTimeZone('America/New_York')))->getTimestamp();`
286
+ — and change all **6** empty-window `|0` jump writes (~L831/938/1024/1193/1281/1369) from
287
+ `App_Date::convertToSQLDate(...)` (renders Chicago) to
288
+ `App_Date::convertToTimezone($epoch, 'Y-m-d H:i:s', 'America/New_York')`, so the **whole** cursor
289
+ pipeline — read + per-record write + empty-window write — runs on **one** clock (Eastern). DST-safe
290
+ (named IANA zone). php-reviewer + cto both **AGREE**; cto flagged (and we applied) the
291
+ empty-window-write hardening to stop a permanent 1h backward bias building up on idle runs. Verified
292
+ after setting NYCHH's fulfillment cursor to `2025-09-04 00:00:00|0`: `6138135` imported.
293
+ - **The rule (now proven THREE times — this bug + the two write-clock bugs above):** every touch of
294
+ the cursor value — the **read**, the per-record **write**, and the empty-window **write** — must use
295
+ the NetSuite account timezone (**Eastern**), never the script's Chicago clock and never the REST
296
+ GET's UTC. `strtotime()` and `App_Date::convertToSQLDate()` both use the script tz (Chicago) and are
297
+ the trap. Any manual cursor rewind value you set by hand must also be **Eastern**.
298
+
260
299
  ### ⚠ Per-record isolation is SECTION-SPECIFIC — and SALES_ORDERS has NONE (corrected 2026-08-17)
261
300
 
262
301
  Whether a bad record is "stepped over" or freezes the section depends entirely on whether **that
@@ -454,6 +493,19 @@ See the [tracking-number bridge doc](../../../2.0/apps/_underscore/features/trac
454
493
  0 of their fulfillments resolve an upstream sales order). `/units` writes and all three
455
494
  tracking-bridge models have **no** interceptor, and **DELETE fires none**.
456
495
 
496
+ - **The two `GET /sales-orders` calls use a narrow `fields` projection, NOT `depth` (2026-09-09).**
497
+ Both the sales-order lookup and the missing-SO refetch in `syncItemFulfillmentFromNetsuite`
498
+ (`toga2.php` ~L4644-4706) request only `uuid`,
499
+ `salesOrderItems.{uuid,lineNumber,quantity,c_netsuiteLineUniqueKey}`, and
500
+ `salesOrderItems.item.{uuid,partNumber}` — the only fields the line-matcher reads. They previously
501
+ sent `depth=5`, whose huge nested read (units, tracking, PO links) made api2 exceed the worker's
502
+ **900s cURL** limit and threw **before** the `ItemFulfillments` row was written, freezing the
503
+ section (`Logs.Issue` 666, `apitransaction.php:418`). SO GETs dropped 100s+ → ~1s. Same class of fix
504
+ as the 2026-09-08 inventory-adjustments `depth=5→4` change; a `fields` projection is the proven
505
+ pattern here (already used by the `/item-fulfillments` GET in this same method) and dodges the api2
506
+ max-depth FK omission. Keep new fields explicit in the projection — a needed field left out of the
507
+ list silently reads `null`, not an error.
508
+
457
509
  ### Single-tracking-number fan-out to items (318) and units (319) — corrected 2026-08-26
458
510
 
459
511
  The earlier "maintains all three bridges" claim was **only true for 317 (header)**: in prod
@@ -1387,6 +1439,30 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
1387
1439
 
1388
1440
  ## Change history
1389
1441
 
1442
+ - 2026-09-09 — **The resume cursor was READ on the wrong clock — a silent +1h window skip dropped
1443
+ records across ALL 6 sections and ALL clients (worker, deployed + verified).** `startModeIteration`
1444
+ (`common_sync_togasupply.php` ~L622) parsed the Eastern-stored cursor with `strtotime()` (script tz
1445
+ America/Chicago), so the fetch's lower bound rendered **1 hour high** and any record in that 1-hour
1446
+ shadow was excluded; the empty window then `|0`-jumped past it — no crash, no alert. Fixed by parsing
1447
+ the cursor as `America/New_York` and rewriting the 6 empty-window `|0` jump writes with
1448
+ `App_Date::convertToTimezone(..., 'America/New_York')`, so read + per-record write + empty-window
1449
+ write all run on one clock (Eastern). Proven on NYCHH IF `6138135` (Eastern 2025-09-04 17:26:58): the
1450
+ +1h bound returned 0 records, the correct bound returned 1. php-reviewer + cto AGREE; cto's
1451
+ empty-window-write hardening applied. Now the THIRD cursor-clock bug — the standing rule is every
1452
+ touch of the cursor value uses Eastern, never Chicago (`strtotime`/`convertToSQLDate`) and never the
1453
+ REST GET's UTC. See the read-clock gotcha in *How it works*. (jcardinal)
1454
+ - 2026-09-09 — **Fulfillment sales-order GET at `depth=5` hit the worker's 900s cURL timeout and froze
1455
+ the section (library, deployed + verified).** Both `GET /sales-orders` calls in
1456
+ `App_Api_Toga2::syncItemFulfillmentFromNetsuite` (`toga2.php` ~L4644-4706 — the lookup + the
1457
+ missing-SO refetch) changed from `depth=5` to a narrow `fields` projection (uuid;
1458
+ `salesOrderItems.{uuid,lineNumber,quantity,c_netsuiteLineUniqueKey}`;
1459
+ `salesOrderItems.item.{uuid,partNumber}` — the only fields the matcher reads), dropping the huge
1460
+ nested read (units, tracking, PO links) that pushed api2 past 900s and threw before the
1461
+ `ItemFulfillments` row was written (`Logs.Issue` 666, `apitransaction.php:418`). Same class as the
1462
+ 2026-09-08 inventory-adjustments depth fix; the `fields` projection is the proven pattern (already
1463
+ used by the `/item-fulfillments` GET in this method). Fulfillment SO GETs dropped 100s+ → ~1s. Both
1464
+ fixes address the NYCHH missing-fulfillment investigation; library + worker still deploy together for
1465
+ this sync path. (jcardinal)
1390
1466
  - 2026-09-08 - **A `Closed` NetSuite order never reaches the transfer-order sync** - the
1391
1467
  `status === 'Closed'` `continue` at `common_sync_togasupply.php` ~L746 sits **before** the
1392
1468
  `isTransferOrder()` branch, so `syncTransferOrderFromNetsuite`'s `case 'Closed'` was dead code and
@@ -45,7 +45,7 @@
45
45
  | [OneUptime push-metric monitors for 2.0 workers](features/oneuptime-worker2-monitoring.md) | A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, ported from the 1.0 `App_SystemMonitor_Compass` monitors. |
46
46
  | [Platform Cache Cleanup (_Worker_Platform_Cache — Clean + Truncate)](features/platform-cache-cleanup.md) | `_Worker_Platform_Cache` owns maintenance of the shared **Cache** cluster that backs api2's [multi-client data retrieval](../../api2/features/cross-client-data- |
47
47
  | [QA/QC Branch Automation (auto-mirror + rebuild-from-ledger revert)](features/qa-qc-branch-automation.md) | Two CTO-approved (AGREE) pieces of GitHub branch automation, centralized in **worker2** rather than per-repo GitHub Actions. |
48
- | [QA/QC Review Pipeline (ClickUp review chain, QC batch + defect/client-review)](features/qa-qc-review-pipeline.md) | The ClickUp-driven **QA then QC** review chain for the platform-wide QA/QC dev process (per the approved `QA-QC Plan.txt`). |
48
+ | [QA/QC Review Pipeline (ClickUp review chain, QC batch + defect/client-review)](features/qa-qc-review-pipeline.md) | The ClickUp-driven review + promotion chain for the platform-wide QA/QC dev process (per the approved `QA-QC Plan.txt`), moving a task through **Development → Q |
49
49
  | [Service Request → Sales Order → Purchase Order generation (Sync/ServiceRequest)](features/service-request-sales-order-generation.md) | `_Worker_Sync_ServiceRequest` turns a **Service Request into a Sales Order, and then into one Purchase Order per vendor**, for **any** tenant. |
50
50
  | [SSO Stability Monitor (Monitor/Operations/SsoStability)](features/sso-stability-monitor.md) | `_Worker_Monitor_Operations::SsoStability(): string` is the **reporter half** of the SAML uptime monitor. |
51
51
  | [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. |
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: draft
9
- updated: 2026-09-08
9
+ updated: 2026-09-09
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Team/Github.php
@@ -25,9 +25,12 @@ than per-repo GitHub Actions. **Not deployed yet** — on the `feature-qa-qc` br
25
25
  1. **Branch auto-mirror** — keep certain branches a byte-for-byte copy of a source branch.
26
26
  2. **Rebuild-from-ledger revert** — undo one task's changes on a QA/QC branch by rebuilding the
27
27
  branch from its merge ledger, not by `git revert`.
28
+ 3. **The single guarded door to `_production`** — `MergeTaskToProduction`, the only path a merge
29
+ can reach `_production`, plus `FanProductionOut` and a hardened public `Merge()`.
28
30
 
29
- Both feed the QA/QC review flow: a denied review routes a task to Rework and reverts its code —
30
- see [QA/QC review pipeline](./qa-qc-review-pipeline.md).
31
+ All three feed the QA/QC review flow: a denied review routes a task to Rework and reverts its code;
32
+ an approved Task/Hotfix/Feature deploys through `MergeTaskToProduction` — see
33
+ [QA/QC review pipeline](./qa-qc-review-pipeline.md).
31
34
 
32
35
  ## How it works
33
36
 
@@ -54,6 +57,22 @@ see [QA/QC review pipeline](./qa-qc-review-pipeline.md).
54
57
  merge-commit revert (and revert-of-revert) break there — rebuild-from-ledger is the only
55
58
  correct undo.
56
59
 
60
+ ### The single guarded door to `_production` (`MergeTaskToProduction`)
61
+
62
+ - **`_production` is reachable ONLY through `_Worker_Team_GitHub::MergeTaskToProduction`.** It
63
+ groups a task's PRs by repo and merges each — used by the Hotfix flow and the Stage cron.
64
+ - Two layers stop a double-deploy when a webhook re-fires:
65
+ 1. a **per-repo `GET_LOCK`**, and
66
+ 2. a **persisted deploy-claim** — a row in `EnvironmentBranchMerges` with `branch='_production'`.
67
+ A re-fired webhook or a cron re-run sees the claim and resumes instead of re-merging.
68
+ - **Public `Merge()` is split** so no queue message can reach prod by accident:
69
+ - the public `Merge()` is a **thin wrapper that refuses any non-mirror merge to `_production`**;
70
+ - a private `performMerge()` is the actual engine.
71
+ - **`FanProductionOut`** merges `_production` **into the rebuildable branches** after a deploy, to
72
+ keep them current.
73
+ - **Reverts are rebuild-from-ledger, never a merge-commit revert** (see below) — this holds for
74
+ `_production`-carrying branches too.
75
+
57
76
  ### Safety rules (load-bearing)
58
77
 
59
78
  - **Force-push is gated to `REBUILDABLE_BRANCHES` only.** Mirror targets and `_production` are
@@ -70,13 +89,20 @@ see [QA/QC review pipeline](./qa-qc-review-pipeline.md).
70
89
  - **Never `git revert` a QA/QC env-branch merge.** Env branches carry `_production` merges too,
71
90
  so merge-commit revert / revert-of-revert break. Use `RebuildBranch` (reset + re-merge from
72
91
  the ledger).
92
+ - **Nothing merges to `_production` except `MergeTaskToProduction`.** Public `Merge()` refuses a
93
+ non-mirror `_production` merge, so a stray queue message can't deploy. Re-fires are stopped by
94
+ the per-repo `GET_LOCK` + the persisted deploy-claim (`EnvironmentBranchMerges` `branch='_production'`).
73
95
 
74
96
  ## Change history
97
+ - 2026-09-09 — Added the single guarded door to `_production` (draft, `feature-qa-qc`, not
98
+ deployed): `MergeTaskToProduction` (per-repo `GET_LOCK` + persisted deploy-claim in
99
+ `EnvironmentBranchMerges` `branch='_production'`, groups a task's PRs by repo); public `Merge()`
100
+ split into a wrapper that **refuses** any non-mirror `_production` merge + a private
101
+ `performMerge()` engine; `FanProductionOut` merges `_production` into the rebuildable branches.
102
+ (jcardinal)
75
103
  - 2026-09-08 — Initial (draft, on `feature-qa-qc`, not deployed): centralized GitHub branch
76
104
  automation in worker2. Auto-mirror via `MIRROR_MAP` (hard-reset + force-with-lease, push
77
105
  webhook-driven); rebuild-from-ledger revert (`RevertTask`/`RebuildBranch`/`recordMerge`,
78
106
  `REBUILDABLE_BRANCHES`, `Team.EnvironmentBranchMerges`) replacing `git revert -m 1`. Force-push
79
107
  gated to rebuildable branches; `GET_LOCK` serializes merge vs rebuild; conflicts skip+email.
80
108
  (jcardinal)
81
- </content>
82
- </invoke>
@@ -6,15 +6,20 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: draft
9
- updated: 2026-09-08
9
+ updated: 2026-09-09
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Clickup/QaReview.php
13
13
  - worker2/Worker/Clickup/QcReview.php
14
14
  - worker2/Worker/Clickup/QcBatch.php
15
+ - worker2/Worker/Clickup/StageBatch.php
16
+ - worker2/Worker/Clickup/FeatureReview.php
17
+ - worker2/Worker/Clickup/BeReview.php
15
18
  - worker2/Worker/Clickup.php
19
+ - worker2/Worker/Github.php
16
20
  - _underscore/Component/Api/Clickup/Clickup.php
17
- - dbchanges2/Core/2026-09-08e - Insert QcBatch CronJob.sql
21
+ - dbchanges2/Core/2026-09-09a - Insert StageBatch CronJob.sql
22
+ - dbchanges2/Team/2026-09-09a - Add isApproved to PullRequests.sql
18
23
  related:
19
24
  - ./clickup-general-automation.md
20
25
  - ./qa-qc-branch-automation.md
@@ -23,17 +28,31 @@ related:
23
28
 
24
29
  ## Summary
25
30
 
26
- The ClickUp-driven **QA then QC** review chain for the platform-wide QA/QC dev process (per the
27
- approved `QA-QC Plan.txt`). Three **new** workers plus a new entry point in the shared ClickUp
28
- webhook handler. **Not deployed yet** — on the `feature-qa-qc` branch working tree.
31
+ The ClickUp-driven review + promotion chain for the platform-wide QA/QC dev process (per the
32
+ approved `QA-QC Plan.txt`), moving a task through **Development → QA → QC → (Client Review) →
33
+ Stage → Production**. Several **new** workers plus entry points in the shared ClickUp webhook
34
+ handler. **Not deployed yet** — on the `feature-qa-qc` branch working tree.
29
35
 
30
- - `_Worker_Clickup_QaReview` — the UI → FE → BE review chain.
31
- - `_Worker_Clickup_QcReview` — QC defect review + client review.
36
+ - `_Worker_Clickup_QaReview` — the UI → FE → BE review chain (branches by Task Type; Hotfix
37
+ BE-approve jumps straight to QC-Hotfix).
38
+ - `_Worker_Clickup_QcReview` — QC defect review + client review; also handles the **Hotfix**
39
+ defect gate and its `DeployHotfix` entry.
32
40
  - `_Worker_Clickup_QcBatch` — a weekday 4:30pm Central CRON that batches QA-Approved tasks into QC.
41
+ - `_Worker_Clickup_StageBatch` — the **Wed 4:30pm Central** CRON that ships "To Deploy" tasks to
42
+ production (the Task-flow prod-deploy piece; replaces the retired legacy status-driven merge).
43
+ - `_Worker_Clickup_FeatureReview` — the **Feature** flow: rolls a multi-subtask feature through the
44
+ same gates as a unit, advancing only when **all** subtasks clear a gate.
45
+ - `_Worker_Clickup_BeReview` — **auto** BE-approve driven by GitHub PR reviews (removes the manual
46
+ BE-review step).
47
+
48
+ **Scope gate — only fires in one ClickUp folder.** The whole automation is gated to the dedicated
49
+ **"(TEST) True Dev Sprints"** folder (id `90116585845`, list `901111612373`) via
50
+ `_Worker_Clickup::QA_QC_FOLDER_ID` + `isTaskInQaQcScope`, so it never touches live sprints.
33
51
 
34
52
  When a review is **denied**, the task is routed to **Rework** and its code changes are **reverted**
35
53
  from the QA/QC branch — see [QA/QC branch automation](./qa-qc-branch-automation.md) for how the
36
- revert (rebuild-from-ledger) works.
54
+ revert (rebuild-from-ledger) works, and for the single guarded door to `_production`
55
+ (`MergeTaskToProduction`) that the Stage cron and Hotfix flow deploy through.
37
56
 
38
57
  ## How it works
39
58
 
@@ -63,6 +82,52 @@ code triggered when a task *left* a status; the QA/QC entry must be on entering
63
82
  - **any Show Stopper** → **Rework** + revert;
64
83
  - otherwise route to **Stage (To Deploy)** or **Client-<letter> (Client Review)**.
65
84
  - **Client review webhook:** **Approved** → Stage / To Deploy; **Denied** → **Rework** + revert.
85
+ - QC entry **sets Environment to `QC-<letter>`** (the plan's "clear" wording was a slip).
86
+
87
+ ### BE auto-approve from a GitHub PR (`_Worker_Clickup_BeReview::FromPr`)
88
+
89
+ - Removes the manual BE-review step. `worker2/Worker/Github.php` gains a `pull_request_review`
90
+ case: it records each PR's approval in `PullRequests.isApproved`, parses the task custom id from
91
+ the PR title (`prTitleToCustomId` — first token, colon stripped), and dispatches `BeReview/FromPr`.
92
+ - `FromPr` sets the task's **BE Review** field **Approved only once ALL of the task's `_production`
93
+ PRs are approved**; a changes-requested on any PR → **Denied**. Multi-repo task = one decision.
94
+ - **PR → task lookup:** `PullRequests.title` (custom id) maps to a ClickUp task via
95
+ `Team.Tasks.customClickupIdentifier ↔ clickupIdentifier`.
96
+ - Setting the BE Review field then drives `QaReview` (Task / Hotfix) or `FeatureReview` (Feature).
97
+
98
+ ### Hotfix flow (`QaReview` + `QcReview`)
99
+
100
+ - Expedites a **single** task straight to production, **skipping Stage**.
101
+ - `QaReview::handleBeReview` branches by Task Type; a **Hotfix** BE-approve → **QC-Hotfix**.
102
+ - `QcReview` handles the Hotfix defect gate and exposes a `DeployHotfix` entry.
103
+ - The merge to `_production` runs from the worker with **no human at the merge moment** — the
104
+ guard chain (task type + status + all-defects-reviewed + no-show-stopper + deploy-claim + lock)
105
+ makes that safe. The actual prod merge goes through `MergeTaskToProduction` — see
106
+ [QA/QC branch automation](./qa-qc-branch-automation.md).
107
+
108
+ ### Stage cron (`_Worker_Clickup_StageBatch`)
109
+
110
+ - The **Wed 4:30pm Central** batch (`'30 16 * * 3'`, seeded by
111
+ `Core/2026-09-09a - Insert StageBatch CronJob.sql`) that ships **"To Deploy"** tasks to production.
112
+ - `Process` loops `Team.Tasks` "to deploy" rows and dispatches **one `DeployTask` per task**, so
113
+ each stays under the 300s worker cap and a re-run **resumes via the deploy-claim** (no double
114
+ deploy). `DeployTask` ships via `MergeTaskToProduction` then the shared finalize
115
+ (`markProductionDeployed` + `releaseSubEnvironment`).
116
+ - Replaces the **retired** legacy status-driven prod-merge (the old `to deploy` / `hotfix review`
117
+ switch cases in `Worker/Clickup.php`).
118
+
119
+ ### Feature flow (`_Worker_Clickup_FeatureReview`)
120
+
121
+ - A large **Feature** is reviewed and deployed **as one unit**: it is **one feature branch**, with
122
+ PRs keyed by the **feature** custom_id; **subtasks are review-only (no code)**.
123
+ - 5 phases: DevFinished QA-entry; UI + FE roll-ups; BE roll-up → QC; QC-defect + Client-review
124
+ roll-ups. Each phase advances the **feature** only when **all subtasks** clear that gate.
125
+ - Runs under a **per-feature `GET_LOCK`** on the feature custom_id with a **fresh-in-lock status
126
+ re-fetch**, so two sibling subtask reviews can't double-advance.
127
+ - `QaReview` / `QcReview` **skip** a Feature task or a Feature subtask (via a `parentIsFeature`
128
+ check) so `FeatureReview` owns them; `StageBatch` ships a Feature as one feature branch and
129
+ finalizes its subtasks (and skips subtasks in the batch scan).
130
+ - The feature's own **BE Review is a MANUAL field for now** (jcardinal + CTO decision).
66
131
 
67
132
  ## Gotchas
68
133
 
@@ -71,12 +136,20 @@ code triggered when a task *left* a status; the QA/QC entry must be on entering
71
136
  - **The unit-test gate is a stub (`runUnitTests()` returns true).** It does not block anything yet.
72
137
  - **Custom-field option VALUES are resolved at runtime, IDs are constants** — see
73
138
  [ClickUp custom-field conventions](./clickup-custom-field-conventions.md).
139
+ - **The automation only fires inside the "(TEST) True Dev Sprints" folder.** `isTaskInQaQcScope`
140
+ gates the fan-out, `handleDevelopmentFinished`, and `QcBatch`. A task outside that folder is
141
+ ignored — verify the folder before expecting the flow to run.
142
+ - **A Feature advances only when ALL subtasks clear a gate** — a single lagging subtask holds the
143
+ whole feature. Subtasks carry no code; only the feature branch does.
144
+ - **`_underscore CLICK_UP_CUSTOM_FIELD_ID__CLIENT_REVIEW` was repointed** to the "(TEST) True Dev
145
+ Sprints" folder's field (`2c4ab51a-…`); all field ids were verified against that folder.
74
146
 
75
147
  ## Known security gaps (discovered, not yet fixed — separate tickets)
76
148
 
77
- - **ClickUp and GitHub webhooks are NOT signature-verified today.** Verification must live at the
78
- **ingestion Lambda over the raw request body** — the worker only ever sees a **transformed**
79
- payload and cannot re-verify a signature. Do not try to add verification in the worker.
149
+ - **ClickUp and GitHub webhooks are NOT signature-verified today.** Verification (incl. the GitHub
150
+ HMAC check) must live at the **ingestion Lambda over the raw request body** — the worker only
151
+ ever sees a **transformed** payload and cannot re-verify a signature. Do not try to add
152
+ verification in the worker.
80
153
  - **`_Component_Api_Clickup::ACCESS_TOKEN` is a hardcoded, committed ClickUp token.** It must be
81
154
  **rotated and moved to an environment variable**. (Only the location is recorded here — never the
82
155
  value.) See also [ClickUp credential rotation](../workflows/clickup-credential-rotation.md).
@@ -84,6 +157,16 @@ code triggered when a task *left* a status; the QA/QC entry must be on entering
84
157
  separate ticket to escape/cast them.
85
158
 
86
159
  ## Change history
160
+ - 2026-09-09 — Added the remaining flows (draft, on `feature-qa-qc`, not deployed): **Hotfix**
161
+ (QaReview branches by Task Type → QC-Hotfix; QcReview `DeployHotfix`), **Stage cron**
162
+ (`_Worker_Clickup_StageBatch`, Wed 4:30pm Central, one `DeployTask` per task, resumes via
163
+ deploy-claim; retired the legacy `to deploy`/`hotfix review` switch cases), **Feature** roll-up
164
+ (`_Worker_Clickup_FeatureReview`, one feature branch keyed by feature custom_id, subtasks
165
+ review-only, per-feature GET_LOCK + fresh-in-lock re-fetch), and **BE auto-approve from PR**
166
+ (`Github.php` `pull_request_review` → `BeReview::FromPr`; all `_production` PRs approved →
167
+ Approved; `PullRequests.isApproved`). Scoped the whole automation to the "(TEST) True Dev
168
+ Sprints" folder (`isTaskInQaQcScope`); repointed CLIENT_REVIEW field; QC entry sets
169
+ Environment `QC-<letter>`; UI Review N/A advances. (jcardinal)
87
170
  - 2026-09-08 — Initial (draft, on `feature-qa-qc`, not deployed): new QA chain (`_Worker_Clickup_QaReview`,
88
171
  UI→FE→BE, approve advances / deny → Rework+revert), QC batch (`_Worker_Clickup_QcBatch`, weekday
89
172
  4:30pm Central, QA-Approved → QC-Task, skips while QC busy) and QC defect/client review
@@ -18,7 +18,7 @@ project: _Underscore
18
18
  client: nychh
19
19
  type: profile
20
20
  status: active
21
- updated: 2026-09-08
21
+ updated: 2026-09-09
22
22
  owners: ["jcardinal", "apeterson", "bala", "akhokhani"]
23
23
  files:
24
24
  - dbchanges2/Client_Nychh/2026-09-02a - TransferOrderNetsuitePushInterceptor.sql
@@ -85,6 +85,27 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
85
85
  `NETSUITE_LAST_SYNC_CURSOR_ITEM_RECEIPTS` is stale at 2024-08-06**, so re-enabling that feed
86
86
  replays ~2 years. See
87
87
  [NYCHH NetSuite → TransferOrders import](./features/netsuite-transfer-order-import.md).
88
+ - **Missing NetSuite records (item fulfillments + others) root-caused and fixed (2026-09-09).** Two
89
+ separate bugs in the 1.0 NetSuite→TOGa Supply sync starved NYCHH imports, both deployed and verified
90
+ in prod: (1) the resume cursor was **read on the wrong clock** (`strtotime()` in Chicago vs the
91
+ Eastern-stored value), shifting the fetch window +1h and **silently skipping** any record in that
92
+ 1-hour shadow — this hit all 6 sections and all clients; and (2) the fulfillment `GET /sales-orders`
93
+ ran at `depth=5` and **timed out at 900s**, freezing the section before the fulfillment row was
94
+ written. Proven on IF `6138135` (Eastern 2025-09-04 17:26:58). Fixes and the standing "cursor is
95
+ always Eastern" rule live in the
96
+ [per-client sync feature doc](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
97
+ - **NYCHH sync scope + data-gap scale + backfill approach (2026-09-09).**
98
+ - **Scope = 22 NetSuite customers:** parent **28908** "NYC Health + Hospitals" + 21 hospital child
99
+ customers (`24145, 29273, 29276, 31584, 31910-31916, 31918-31925, 32229, 33674`).
100
+ - **Gap report (NetSuite scope vs `Client_Nychh` imported):** Item Fulfillments **2986 vs 2472**
101
+ (~514 missing, 17%); Sales+Transfer Orders 2121 NS vs 2098 TOGa (159 SalesOrders + 1939
102
+ TransferOrders — roughly complete); Invoices **2128 vs 213** (~1915 missing, 90% — but that is
103
+ mostly because the **invoices feed was toggled OFF on 2026-09-03**, not the timezone bug).
104
+ POs/Receipts/Adjustments are vendor/location-scoped (not measured) and several are toggled off.
105
+ - **Backfill approach:** prefer a **targeted re-import of the specific missing NetSuite ids** (diff
106
+ NetSuite scope vs TOGa) over a **blind cursor rewind** — a full rescan re-pulls thousands of heavy
107
+ serial records and stresses the shared NetSuite auth quota. Re-import is safe (matches on the
108
+ NetSuite internal id). Any manual cursor rewind value must be written in **Eastern**.
88
109
  - 🚩 **OPEN, not root-caused (2026-09-08): a duplicate item INSERT aborts the whole NYCHH sales-order
89
110
  sync run.** `Items` id 5429 (658-BFZH) was created and then re-inserted in the same run —
90
111
  `Duplicate entry '26-658-BFZH-1' for key 'Items.manufacturerId_partNumber_catalogId'` (EV-10). The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.788",
3
+ "version": "1.0.790",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",