toga-ai 1.0.931 → 1.0.932

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.
@@ -32,7 +32,7 @@
32
32
  | [Etilize Item Translation Import](features/etilize-item-translation-import.md) | Imports non-English item + feature text from Etilize into the client's `ItemTranslations` (and feature sidecar tables), writing directly to `DB_CLIENT`; open wh |
33
33
  | [Logs retention cleanup worker (Infrastructure/Logs/Cleanup — Logs_<Tenant>.Api pruning)](features/logs-retention-cleanup.md) | Daily batched pruning of `Logs_<Tenant>.Api` (GET 30d, other methods 90d); open when changing log retention, adding a table to prune, or when an old Api log row |
34
34
  | [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | DB-driven monitoring framework: one orchestrator applies anti-flap + notification rules to any registered "is X healthy?" child check; open when adding a monito |
35
- | [Bidirectional NetSuite ↔ Forecast ↔ ClickUp opportunity stage sync — BUILT (stage map, echo guard, claim split)](features/netsuite-clickup-opportunity-bidirectional-design.md) | Two-way NetSuite↔ClickUp opportunity STAGE sync (shared `_Component_Opportunity_StageMap`, actor echo guard, claim split, stale-claim takeover); open before tou |
35
+ | [Bidirectional NetSuite ↔ Forecast ↔ ClickUp opportunity stage + title sync — BUILT (stage map, echo guard, claim split, NS-owned title)](features/netsuite-clickup-opportunity-bidirectional-design.md) | Two-way NetSuite↔ClickUp opportunity STAGE sync + one-way NS→CU TITLE sync (shared `_Component_Opportunity_StageMap`, actor echo guard, claim split, stale-claim |
36
36
  | [NetSuite Integrations Monitor (Monitor/Operations/NetsuiteIntegrations)](features/netsuite-integrations-monitor.md) | Cross-client health check that flags stuck NetSuite↔2.0 integrations; open when a NetSuite integration hangs or when building an all-client OneUptime sweep. |
37
37
  | [NetSuite item → client routing (there is NO client on the NetSuite item record)](features/netsuite-item-client-routing.md) | Evidence that no NetSuite field can route an item to a client; open before designing NetSuite item webhook phase 2 (TRUE-80553) so the "route by a NetSuite fiel |
38
38
  | [ClickUp Opportunity Client vs End-Customer Labels (Stakeholders / End Customer)](features/netsuite-opportunity-client-labels.md) | The NetSuite→ClickUp opportunity task's two customer labels (Stakeholders=client, End Customer=`custbody_end_customer`), their backfills/digest and the fuzzy-ma |
@@ -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: [jcardinal, ajean]
11
11
  files:
12
12
  - worker2/Worker/Clickup/Project.php
@@ -49,6 +49,8 @@ This single fact determines the shape of the whole NetSuite↔ClickUp opportunit
49
49
  | Closed Won | `901111987478` |
50
50
  | Closed Lost | `901111987480` |
51
51
 
52
+ **Won leaves the folder; Lost stays.** Setting Opportunity Stage = Opportunity Won makes a ClickUp native Automation move the task out of the Opportunities folder into Project Hub (space `90113939341`) → list "New Project" (`901112061579`), folder "hidden" (`90117098908`), status "new project", task type Epic. The API cannot undo it (home list). Closed Lost tasks stay in the folder (list `901111987480`). So code scoped to folder `90117060687` (e.g. the NS→CU title sync) does not see Won tasks.
53
+
52
54
  All 8 share **one status group** (`proj_90113928591`), so `opportunity won` and `closed lost` are valid statuses on **every** list — a status is not what distinguishes a stage.
53
55
 
54
56
  **ClickUp v2 cannot move a task's home list**, and it fails in two different ways — one silent. Recorded empirically at `worker2/Worker/Clickup/Project.md:14-26`:
@@ -143,4 +145,5 @@ Read-only / setup actions, invoked via `curl -X POST https://worker.togahub.com/
143
145
  - [Outbound third-party API rate budget](../../_underscore/features/outbound-api-rate-budget.md)
144
146
 
145
147
  ## Change history
148
+ - 2026-10-01 — Won tasks are moved to Project Hub "New Project" by Automation (ajean)
146
149
  - 2026-09-25 — Noted folder-gated opportunity dispatch, no early return (jcardinal)
@@ -1,12 +1,12 @@
1
1
  ---
2
- title: Bidirectional NetSuite ↔ Forecast ↔ ClickUp opportunity stage sync — BUILT (stage map, echo guard, claim split)
2
+ title: Bidirectional NetSuite ↔ Forecast ↔ ClickUp opportunity stage + title sync — BUILT (stage map, echo guard, claim split, NS-owned title)
3
3
  framework: "2.0"
4
4
  repo: worker2
5
5
  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/Component/Opportunity/StageMap/StageMap.php
@@ -25,16 +25,18 @@ related:
25
25
  - ../../../../1.0/apps/worker/features/forecast2-netsuite-reconciliation.md
26
26
  ---
27
27
 
28
- Two-way NetSuite↔ClickUp opportunity STAGE sync (shared `_Component_Opportunity_StageMap`, actor echo guard, claim split, stale-claim takeover); open before touching any opportunity stage/close/reopen logic or the stage map rules.
28
+ Two-way NetSuite↔ClickUp opportunity STAGE sync + one-way NS→CU TITLE sync (shared `_Component_Opportunity_StageMap`, actor echo guard, claim split, stale-claim takeover); open before touching any opportunity stage/close/reopen/title logic or the stage map rules.
29
29
 
30
30
  ## Summary
31
31
 
32
- Built 2026-09-24 (`jcardinal`, design by `ajean`). Code on worker2 `_production`; flags `SYNC_STAGE_TO_CLICKUP` (NS→CU) and `SYNC_CLOSE_TO_NETSUITE` (CU→NS close/reopen) now `true`. `UPDATE_CLICKUP` (full-task NS→CU rewrite) stays `false`. Live verification still pending — see Handoff. Notes/files are a separate job: [opportunity documents sync](./netsuite-opportunity-documents-sync.md).
32
+ Built 2026-09-24 (`jcardinal`, design by `ajean`). Code on worker2 `_production`; flags `SYNC_STAGE_TO_CLICKUP` (NS→CU), `SYNC_TITLE_TO_CLICKUP` (NS→CU title, PR #183, 2026-10-01) and `SYNC_CLOSE_TO_NETSUITE` (CU→NS close/reopen) all `true`. `UPDATE_CLICKUP` (full-task NS→CU rewrite) stays `false`. Live verification still pending — see Handoff. Notes/files are a separate job: [opportunity documents sync](./netsuite-opportunity-documents-sync.md).
33
33
 
34
34
  ## How it works
35
35
 
36
36
  ### Per-field ownership
37
- NetSuite owns: Opportunity Stage dropdown, close state. ClickUp owns: description (Fluffer output — never diffed from NS), assignees, presales fields. NS→CU writes ONLY the Opportunity Stage dropdown (`d8fd5819-d8f7-494b-8671-aff7fbdeb985`); the ClickUp Automation moves the task list from it ([routing](./clickup-project-routing.md)).
37
+ NetSuite owns: Opportunity Stage dropdown, close state, **opportunity title** (decided Aaron Jean 2026-10-01). ClickUp owns: description (Fluffer output — never diffed from NS), assignees, presales fields. NS→CU writes ONLY the Opportunity Stage dropdown (`d8fd5819-d8f7-494b-8671-aff7fbdeb985`) and the task name; the ClickUp Automation moves the task list from the dropdown ([routing](./clickup-project-routing.md)).
38
+
39
+ **Title rule:** CU→NS never writes title (not NetSuite, not `Forecast.title`). A human rename in ClickUp is reverted by the next NS event — accepted on purpose, so NetSuite stays the one record.
38
40
 
39
41
  ### Stage map — `_Component_Opportunity_StageMap` (PHP, one file, both directions)
40
42
  Replaced `SALES_STAGE_MAP` / `STAGE_OPTION_ORDERINDEX` constants (deleted). Rules from Aaron Jean + Eric, 2026-09-24:
@@ -61,18 +63,26 @@ Option orderindex is resolved LIVE from the field's `type_config.options` (fixes
61
63
 
62
64
  ### CU→NS (`Worker/Clickup/Opportunity.php`)
63
65
  - Routed from `Worker/Clickup.php` by Opportunities **folder** `90117060687` (all 8 stage lists), no early return — hub routing handlers still run.
66
+ - Writes stage/close only — never title (PR #183 removed the title PATCH, the `Forecast.title` write, `stripTaskNamePrefix()`, `CLICK_UP_CUSTOM_FIELD_ID__CUSTOMER_NUMBER`, `FORECAST_TITLE_MAX_LENGTH`). `clickupPostComment()` is public as an internal helper — not a queue action.
64
67
  - **Actor echo guard:** drop an event when every history item's actor is our config-token or component-token user (resolved via `GET /user`). Never drop actor `-1` (ClickUp Automation — a human list-move arrives as `taskUpdated` on the dropdown with actor `-1`). Value diff kept as backstop.
65
68
  - **Link check (security):** before any PATCH, require `Forecast.Opportunities.clickupTaskId === taskId`. `Opportunity #` is free text; anyone could point a task at another opp.
66
69
  - `logged()` commits `DB_LOGS`.
67
70
 
68
71
  ### NS→CU (`Worker/Netsuite/Opportunity.php`) — claim split
69
- - The `clickupTaskId` claim now guards **creation only**. Linked opportunity → `syncStageToClickup()`: fresh task GET, write dropdown only if the map says so; ClickUp 404 = skip.
72
+ - The `clickupTaskId` claim now guards **creation only**. Linked opportunity → `syncLinkedTaskToClickup()` (was `syncStageToClickup()`): one task GET, then title sync and stage sync run independently — both are tried, one combined exception if either fails. ClickUp 404 = `sync skipped (task X not found in ClickUp)`.
73
+ - **Title sync — `syncTitleToClickup()`** (kill switch `SYNC_TITLE_TO_CLICKUP`):
74
+ - Only tasks in Opportunities folder `90117060687`; Won tasks moved to Project Hub are left alone.
75
+ - Match = normalized name equals the NS title, or ends with ` — <title>` / ` - <title>`. Empty NS title → skip.
76
+ - Mismatch → PUT name from pure `buildTaskName()` (same function task creation uses, so names are byte-identical), then comment: "The opportunity title was changed in NetSuite, so the automation has updated it in ClickUp. Previous name: …".
77
+ - Post-PUT check: if ClickUp stored a different name → no comment, Sentry message, result `title rename did not stick`. Comment failure is non-fatal and never auto-retried.
78
+ - `setAutoRetry` on the name PUT and the stage POST. Result string ends `; title updated|unchanged|skipped (...)`.
70
79
  - **Stale PENDING takeover:** `PENDING` older than 30 min (or NULL `dtClickupClaimed`) can be re-claimed. `dtClickupClaimed` is raw-SQL only — deliberately NOT on either ORM model (1.0 or 2.0); code checks `SHOW COLUMNS` first. `endClaim()` / `loadClaimTimestamp()` are fenced on the claim timestamp so a slow job can't overwrite a newer owner.
71
80
 
72
81
  ### Reconciler
73
82
  The 1.0 `periodic_forecast_discrepancy_fix_opportunities.php` still INSERTs (Aaron's 2026-09-09 session kept and completed it) — the old "reconciler becomes a reporter" plan is dropped.
74
83
 
75
84
  ## Gotchas
85
+ - **NS→CU stage write echoes back as a human event.** The stage write makes the ClickUp Automation move the list; that event has actor `-1`, so the echo guard never drops it and CU→NS `Process` runs. Before PR #183 this PATCHed NetSuite title from the stale ClickUp name (old ` - ` format; `UPDATE_CLICKUP=false` meant NS title edits never reached ClickUp). 15 NetSuite titles were corrupted 2026-10-01 (e.g. "Pen test" → "68272 - 4502 Societe Generale Financial Corp"); each PATCH also re-fired the NS webhook (3 PUTs per opp). Restore list is held by Aaron, restore deferred. Lesson: anything CU→NS writes must be safe to run after every NS→CU write.
76
86
  - `taskMoved` is not subscribed for space `90113928591`; stage detection relies on the dropdown `taskUpdated`.
77
87
  - `Opportunities` FK children (`OpportunityDocuments`) use ON DELETE CASCADE because the 1.0 reconciler/fixer delete `Opportunities` rows.
78
88
  - worker2 has no `?controller=` URL routing — run the map test / probes via CLI runner.
@@ -90,5 +100,6 @@ The 1.0 `periodic_forecast_discrepancy_fix_opportunities.php` still INSERTs (Aar
90
100
  - [ClickUp routing](./clickup-project-routing.md) — stage-as-list Automation.
91
101
 
92
102
  ## Change history
103
+ - 2026-10-01 — NetSuite owns title: CU→NS title write removed, NS→CU title sync added (PR #183) (ajean)
93
104
  - 2026-09-25 — Built: StageMap component, actor guard, link check, claim split, stale takeover; flags on. Map stays PHP; reconciler keeps INSERT (jcardinal)
94
105
  - 2026-09-16 — Agreed design recorded, not built (ajean)
@@ -86,7 +86,7 @@ Outbound sync from NetSuite to TOGA for the record types the Forecast2 importer
86
86
 
87
87
  Shared helpers: `buildCustomFields()` (the field array, used by both create and update), `resolvePresalesUserId()` (cached `Client_True.Users.c_clickupUserIdentifier` lookup → else a `GET /team` roster scan that back-fills the cache via `cacheClickupUser()`).
88
88
 
89
- - **Task name** = `{opportunityNumber} — {customer} — {title}`, e.g. `60533 — 560 Endeavor Health — Data Center Facility Consolidation Services` (empty parts dropped).
89
+ - **Task name** = `{opportunityNumber} — {customer} — {title}` from pure `buildTaskName()` (also used by the NS→CU title sync), e.g. `60533 — 560 Endeavor Health — Data Center Facility Consolidation Services` (empty parts dropped).
90
90
  - **Task description** = a fixed structured block built by `buildDescription()`:
91
91
  ```
92
92
  Company: 560 Endeavor Health
@@ -123,7 +123,7 @@ Required a new `dbchanges2/Forecast/` ALTER adding the `clickupTaskId` column, a
123
123
 
124
124
  #### Claim guards creation only (since 2026-09-24)
125
125
 
126
- The claim used to double as "do anything at all", making the integration create-only. Now split: a linked opportunity goes to `syncStageToClickup()` (stage dropdown only, behind `SYNC_STAGE_TO_CLICKUP = true`). `updateTask()` is still dead behind `UPDATE_CLICKUP = false` — leave it. A stale `PENDING` (>30 min via `dtClickupClaimed`, or NULL timestamp) is now taken over automatically. Details: [stage sync](./netsuite-clickup-opportunity-bidirectional-design.md).
126
+ The claim used to double as "do anything at all", making the integration create-only. Now split: a linked opportunity goes to `syncLinkedTaskToClickup()` (stage dropdown + task name only, behind `SYNC_STAGE_TO_CLICKUP` / `SYNC_TITLE_TO_CLICKUP`). `updateTask()` is still dead behind `UPDATE_CLICKUP = false` — leave it. A stale `PENDING` (>30 min via `dtClickupClaimed`, or NULL timestamp) is now taken over automatically. Details: [stage sync](./netsuite-clickup-opportunity-bidirectional-design.md).
127
127
 
128
128
  `clickupTaskId` semantics: `NULL` = unclaimed; `PENDING` = claim in flight; else the real task id.
129
129
 
@@ -173,6 +173,13 @@ curl -X POST https://webhook.togahub.com/netsuite -H 'Content-Type: application/
173
173
  - An opportunity with no presales lead returns `skipped (no presales lead)` and stays queued.
174
174
  - `importOpportunity()` is self-healing, so the replay also repairs the Forecast row.
175
175
 
176
+ **Bulk replay rules (from the 2026-10-01 Won/Lost catch-up — 287 tasks, 149 → Closed Lost, 138 → Opportunity Won):**
177
+ - **Retroactive data fixes use the live-sync logic (webhook replay), never hand-set values in ClickUp** (decided Aaron Jean 2026-10-01).
178
+ - **Never bulk-replay without PR #183.** Before it, each replay's stage write echoed back through CU→NS and overwrote NetSuite titles (see [stage sync](./netsuite-clickup-opportunity-bidirectional-design.md) gotchas).
179
+ - **Space replays 3–5 s apart.** ClickUp returned HTTP 429 at 0.5 s (31 failures) and still 8 failures at 1 s with retries. A failed replay is safe to re-send.
180
+ - Candidate list: ClickUp folder `90117060687` tasks, match by opportunity number to `Forecast.Opportunities` + `PercentToCloseStatuses.netsuiteInternalId` (`124`/`130` = Won, `14` = Lost). `130` Alternate Quote goes to Won per StageMap even when NS status reads Closed - Lost.
181
+ - The ClickUp MCP filter rejects two filters on the same custom field ("use a single RANGE") — run `!= Won` and `= Lost` as separate queries.
182
+
176
183
  **A `404` on replay is the correct signal for a NetSuite-deleted opportunity, not a defect.** The single failure in that run (NS internalId **7442501**) was NetSuite REST HTTP 404 *"The opportunity does not exist"* — the record had been deleted in NetSuite, which is also why it was the one row missing from Forecast.
177
184
 
178
185
  #### Open item — item-sync watchdog timeouts (NOT investigated)
@@ -245,7 +252,7 @@ Three facts worth not rediscovering:
245
252
  1. Resolve NS opp from `Opportunity #` (`a5529cdc-8dbb-4165-8bc5-9f5cc691796a`, = `tranId`) via one-row SuiteQL.
246
253
  2. **Require `Forecast.Opportunities.clickupTaskId === taskId`** — else skip (free-text field; security).
247
254
  3. Drop echo events by actor (our token users; never actor `-1`); GET the opp with `expandSubResources=true`.
248
- 4. PATCH only differing NS-owned fields (`title`, stage/`entityStatus` per `_Component_Opportunity_StageMap`).
255
+ 4. PATCH only differing stage/`entityStatus` per `_Component_Opportunity_StageMap`. **Never title** — NetSuite owns it (PR #183, 2026-10-01).
249
256
 
250
257
  Stage/close/reopen rules: [stage sync](./netsuite-clickup-opportunity-bidirectional-design.md). Notes/files: [documents sync](./netsuite-opportunity-documents-sync.md).
251
258
 
@@ -327,5 +334,6 @@ Stage/close/reopen rules: [stage sync](./netsuite-clickup-opportunity-bidirectio
327
334
  - [MCP Tool Usage](../../../../standalone/apps/claude/workflows/mcp-tool-usage.md) — the `toga-db` query traps hit while running the verdict census.
328
335
 
329
336
  ## Change history
337
+ - 2026-10-01 — CU→NS no longer writes title; bulk replay rules from the Won/Lost catch-up (ajean)
330
338
  - 2026-10-01 — Flagged `folder_ids[]` vs documented `project_ids[]` filter (unverified) (ajean)
331
339
  - 2026-09-25 — Claim guards creation only; CU→NS routed by folder, link check, actor guard; orderindex-0 bug fixed (jcardinal)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.931",
3
+ "version": "1.0.932",
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",