toga-ai 1.0.927 → 1.0.928

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: 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.
@@ -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)
@@ -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: ["dfranks", "kyalamarthi", "ajean", "jcardinal"]
11
11
  files:
12
12
  - worker2/Worker/Netsuite.php
@@ -253,6 +253,11 @@ Stage/close/reopen rules: [stage sync](./netsuite-clickup-opportunity-bidirectio
253
253
 
254
254
  ## Gotchas
255
255
 
256
+ - **Unverified: the folder filter on `GET /team/{id}/task` may be wrong here.** ClickUp documents the
257
+ folder filter as `project_ids[]`; `Worker/Netsuite/Opportunity.php` (~L1345) and
258
+ `Worker/Netsuite/OpportunityDocuments.php` (~L456) send `folder_ids[]`. If ClickUp ignores it, the
259
+ query returns team-wide tasks, not just the Opportunities folder. Check before trusting those scans
260
+ (follow-up, flagged 2026-10-01). The newer sprint code uses `project_ids[]`.
256
261
  - **⚠ The `endCustomerName` 9-day total outage — a column-existence gate does NOT protect an ORM write (2026-09-01 → 2026-09-10).** Every `Netsuite/Opportunity/post|put` job threw `There is no field called 'endCustomerName' in the '_Model_Forecast_Opportunity' model.` (`_underscore/Model.php:498`) from 2026-09-01 12:01 to 2026-09-10 15:48 CDT. **The whole NetSuite→Forecast→ClickUp opportunity sync was dead for 9 days and every job was red.**
257
262
  - **Cause — a deploy split across TWO repos.** worker2 shipped the `$values['endCustomerName']` write (PR #158) while `_underscore/Model/Forecast/Opportunity.php` had not yet declared the property. `_Model` builds its field map from **declared properties** in the constructor, so `__set` rejected the unknown field. The DB column existed (the migration had been applied) — the missing half was the model declaration, in a different repo.
258
263
  - **Blast radius:** the throw lands in `importOpportunity()` **before** the Forecast save and **before** `maybeCreateClickupTask()`, so affected opportunities got **neither** a Forecast row **nor** a ClickUp task. Measured in prod `Core.WorkerJobs`: **611 failed jobs, 310 distinct opportunities, 264 still with no ClickUp task**, 1 (NS internalId 7442501) absent from Forecast.
@@ -322,4 +327,5 @@ Stage/close/reopen rules: [stage sync](./netsuite-clickup-opportunity-bidirectio
322
327
  - [MCP Tool Usage](../../../../standalone/apps/claude/workflows/mcp-tool-usage.md) — the `toga-db` query traps hit while running the verdict census.
323
328
 
324
329
  ## Change history
330
+ - 2026-10-01 — Flagged `folder_ids[]` vs documented `project_ids[]` filter (unverified) (ajean)
325
331
  - 2026-09-25 — Claim guards creation only; CU→NS routed by folder, link check, actor guard; orderindex-0 bug fixed (jcardinal)
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-29
10
- owners: [jcardinal]
9
+ updated: 2026-10-01
10
+ owners: ["jcardinal", "ajean"]
11
11
  files:
12
12
  - worker2/Worker/Clickup/QaReview.php
13
13
  - worker2/Worker/Clickup/QcReview.php
@@ -15,10 +15,12 @@ files:
15
15
  - worker2/Worker/Clickup/StageBatch.php
16
16
  - worker2/Worker/Clickup/FeatureReview.php
17
17
  - worker2/Worker/Clickup/BeReview.php
18
+ - worker2/Worker/Clickup/DevFinished.php
18
19
  - worker2/Worker/Clickup.php
19
20
  - worker2/Worker/Github.php
20
21
  - _underscore/Component/Api/Clickup/Clickup.php
21
22
  - dbchanges2/Core/2026-09-09a - Insert StageBatch CronJob.sql
23
+ - dbchanges2/Core/2026-09-29a - Record sprint and design CronJobs.sql
22
24
  - dbchanges2/Team/2026-09-09a - Add isApproved to PullRequests.sql
23
25
  related:
24
26
  - ./clickup-general-automation.md
@@ -40,8 +42,10 @@ production** (deployed on `_production`).
40
42
  - `_Worker_Clickup_QcReview` — QC defect review + client review; also handles the **Hotfix** defect
41
43
  gate and its `DeployHotfix` entry.
42
44
  - `_Worker_Clickup_QcBatch` — a weekday 4:30pm Central CRON that batches QA-Approved tasks into QC.
43
- - `_Worker_Clickup_StageBatch` — the **Wed 4:30pm Central** CRON that ships "To Deploy" tasks to
44
- production (the Task-flow prod-deploy piece; replaces the retired legacy status-driven merge).
45
+ - `_Worker_Clickup_StageBatch` — batch that ships "To Deploy" tasks to production. **Not scheduled in
46
+ prod: Stage deploys stay manual (owner decision 2026-10-01).**
47
+ - `_Worker_Clickup_DevFinished::CatchUp` — every-30-min safety net for missed Development Finished
48
+ webhooks.
45
49
  - `_Worker_Clickup_FeatureReview` — the **Feature** flow: rolls a multi-subtask feature through the
46
50
  same gates as a unit, advancing only when **all** subtasks clear a gate.
47
51
  - `_Worker_Clickup_BeReview` — **auto** BE-approve driven by GitHub PR reviews (removes the manual
@@ -79,17 +83,41 @@ when a task *left* a status; the QA/QC entry must be on entering the status.
79
83
  (matches `QaReview::changedCustomFieldIds`), each in its own try/catch; safe because each skips the
80
84
  other's task kind. Write failures are logged + Sentry, never thrown.
81
85
 
86
+ `handleDevelopmentFinished` checks for a PR itself (`_Worker_Clickup_GeneralAutomation::hasPullRequest`)
87
+ before picking the first review status, so it does not race the Has Code Changes backfill.
88
+
89
+ **Prod flow end to end:** Development Finished → UI / FE / BE review → QA Approved → (4:30 PM QC
90
+ Batch) QC Review → Stage or Client Review → To Deploy → **manual** deploy.
91
+
92
+ ### Catch-up for missed webhooks (`_Worker_Clickup_DevFinished::CatchUp`)
93
+
94
+ - Cron id 86, `*/30 * * * *`. Scans Dev Sprints (`GET /team/{id}/task?project_ids[]=…&statuses[]=development finished&subtasks=true`)
95
+ and re-runs `_Worker_Clickup::RunDevelopmentFinished` for tasks whose **Environment** is empty, or
96
+ set but stuck ≥ 30 min (`STUCK_THRESHOLD_SECONDS`).
97
+ - Fresh re-fetch per task (`getTaskDetails($id, true)`); skips tasks updated in the last 2 min
98
+ (`RECENT_UPDATE_GUARD_SECONDS`) so it does not race a live webhook.
99
+ - Per-task `catch (Exception)` is **on purpose** (CSO decision, CodeRabbit agreed): each task is
100
+ independent and is retried next run; code errors (`Error`) still fail loud. Rescans from page 0
101
+ after a trigger (moved tasks shift pages); `MAX_PAGES` caps fetches. Returns
102
+ `{isSuccess, checked, triggered, failed}`.
103
+ - First run 2026-10-01 moved 10 tasks stuck since the 9/12–9/20 webhook outage (all → `qa approved`,
104
+ since neither Has UI nor Has Code was checked).
105
+
82
106
  ### QA review chain (`_Worker_Clickup_QaReview`)
83
107
 
84
108
  - Three ordered review fields: **UI → FE (front-end) → BE (back-end)**.
85
- - **Approve** advances to the next review field.
109
+ - **Approve** advances to the next review field. Entering a review status resets its field to
110
+ **Needs Review** ([general automation](./clickup-general-automation.md) rule 7); `QaReview` /
111
+ `FeatureReview` ignore Needs Review, so there is no loop.
86
112
  - **Deny** sends the task to **Rework** and **reverts** the task's changes from the QA/QC branch.
87
113
  - **Unit-test gate is a STUB today.** `runUnitTests()` currently returns `true`. Real gating (Cypress /
88
114
  Postman / Codeception) is pending — do not treat the gate as enforcing anything yet.
89
115
 
90
116
  ### QC batch promotion (`_Worker_Clickup_QcBatch`)
91
117
 
92
- - Weekday **4:30pm Central** CRON (registered by `Core/2026-09-08e - Insert QcBatch CronJob.sql`).
118
+ - Weekday **4:30pm Central** CRON, prod id 75 (`Core/2026-09-08e - Insert QcBatch CronJob.sql`).
119
+ It was `isActive = 0` in prod until 2026-09-29a turned it on — until then QA Approved tasks never
120
+ moved to QC.
93
121
  - Moves **QA-Approved** tasks → **QC-Task**.
94
122
  - **Skips the run if QC is still busy** (does not pile a new batch on top of an in-progress one).
95
123
 
@@ -122,16 +150,15 @@ when a task *left* a status; the QA/QC entry must be on entering the status.
122
150
  safe. The actual prod merge goes through `MergeTaskToProduction` — see
123
151
  [QA/QC branch automation](./qa-qc-branch-automation.md).
124
152
 
125
- ### Stage cron (`_Worker_Clickup_StageBatch`)
153
+ ### Stage batch (`_Worker_Clickup_StageBatch`) — not scheduled
126
154
 
127
- - The **Wed 4:30pm Central** batch (`'30 16 * * 3'`, seeded by
128
- `Core/2026-09-09a - Insert StageBatch CronJob.sql`) that ships **"To Deploy"** tasks to production.
129
- - `Process` loops `Team.Tasks` "to deploy" rows and dispatches **one `DeployTask` per task**, so each
130
- stays under the 300s worker cap and a re-run **resumes via the deploy-claim** (no double deploy).
131
- `DeployTask` ships via `MergeTaskToProduction` then the shared finalize (`markProductionDeployed` +
132
- `releaseSubEnvironment`).
133
- - Replaces the **retired** legacy status-driven prod-merge (the old `to deploy` / `hotfix review`
134
- switch cases in `Worker/Clickup.php`).
155
+ - **Stage deploys stay manual (owner decision 2026-10-01).** Do not add or turn on a StageBatch cron
156
+ row without the owner, even though `Core/2026-09-09a - Insert StageBatch CronJob.sql` exists.
157
+ - When run: `Process` loops `Team.Tasks` "to deploy" rows and dispatches **one `DeployTask` per task**
158
+ (stays under the 300s worker cap; a re-run resumes via the deploy-claim). `DeployTask` ships via
159
+ `MergeTaskToProduction` then finalizes (`markProductionDeployed` + `releaseSubEnvironment`).
160
+ - Replaces the **retired** legacy status-driven prod-merge (old `to deploy` / `hotfix review` cases in
161
+ `Worker/Clickup.php`).
135
162
 
136
163
  ### Feature flow (`_Worker_Clickup_FeatureReview`)
137
164
 
@@ -157,7 +184,12 @@ when a task *left* a status; the QA/QC entry must be on entering the status.
157
184
  the fan-out, `handleDevelopmentFinished`, `QcBatch`, and the taskCreated Work Type default. A task
158
185
  outside that folder is ignored — verify the folder before expecting the flow to run.
159
186
  - **Dev Finished with neither Has UI Changes nor Has Code Changes checked → `qa approved` (skips
160
- review).** Intended (jcardinal confirmed); do not "fix".
187
+ review).** Intended (jcardinal confirmed); do not "fix". Sprint-list defaults now check Has UI
188
+ Changes on Dev tasks, and a recorded PR checks Has Code Changes, so this path is now rare.
189
+ - **A missed Development Finished webhook used to strand the task.** CatchUp now recovers it within
190
+ 30 min. It skips multi-listed tasks whose HOME folder is still the old True Dev Sprints folder —
191
+ same gap as `ResetInProgress`, see
192
+ [team-sprint-management](./team-sprint-management.md#gotchas).
161
193
  - **A Feature advances only when ALL subtasks clear a gate** — a single lagging subtask holds the whole
162
194
  feature. Subtasks carry no code; only the feature branch does.
163
195
  - **`_underscore CLICK_UP_CUSTOM_FIELD_ID__CLIENT_REVIEW` was repointed** to the Dev Sprints
@@ -186,3 +218,7 @@ when a task *left* a status; the QA/QC entry must be on entering the status.
186
218
  - [ClickUp general automation](./clickup-general-automation.md)
187
219
  - [QA/QC branch automation](./qa-qc-branch-automation.md)
188
220
  - [ClickUp custom-field conventions](./clickup-custom-field-conventions.md)
221
+
222
+ ## Change history
223
+ - 2026-10-01 — DevFinished CatchUp (cron 86); QC Batch turned on (id 75); review fields reset to Needs Review on entry; Stage deploys confirmed manual (ajean; worker2 PR #178, dbchanges2 PR #504)
224
+ - 2026-09-25 — Scope narrowed to Dev Sprints `90116585845` (jcardinal)
@@ -6,23 +6,25 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-25
10
- owners: ["jcardinal", "kyalamarthi"]
9
+ updated: 2026-10-01
10
+ owners: ["jcardinal", "kyalamarthi", "ajean"]
11
11
  files:
12
12
  - worker2/Worker/Team/Sprint.php
13
13
  - worker2/Config/production.ini
14
14
  - _underscore/Model/Team/Sprint.php
15
+ - dbchanges2/Core/2026-09-29a - Record sprint and design CronJobs.sql
15
16
  related:
16
17
  - ../architecture.md
17
18
  - ./creating-worker-actions.md
18
19
  - ./clickup-work-type-automation.md
19
20
  - ./clickup-project-routing.md
21
+ - ./clickup-general-automation.md
20
22
  - ./committed-secrets-and-config-debt.md
21
23
  - ../workflows/sprint-clickup-folder-repoint.md
22
24
  - ../workflows/php-runtime-upgrade-dependency-audit.md
23
25
  ---
24
26
 
25
- `_Worker_Team_Sprint` engine for TOGA's biweekly dev-sprint process + reporting (warnings, lock, daily dashboard, sprint-end scoring, AI release notes); open for sprint actions, the scoring model, or Power BI-parity dashboard SQL.
27
+ `_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 Progress reset); open for sprint actions, their cron schedule, the scoring model, or Power BI-parity dashboard SQL.
26
28
 
27
29
  ## Summary
28
30
 
@@ -50,11 +52,13 @@ everything.
50
52
  ### Key files / entry points
51
53
  - `Worker/Team/Sprint.php` — the entire feature (one abstract class, static methods).
52
54
  - `initialize()` — registers the `Logs_True` DB connection used for ClickUp API logging.
53
- - ClickUp config lives in class constants: `CLICKUP_TEAM_ID` and `CLICKUP_DEV_SPRINTS_FOLDER_ID` (the
54
- folder whose child lists are the sprints). The API token is **no longer** a constant here — all 6
55
- call sites use `_Component_Api_Clickup::accessToken()`, reading `[clickup] token` from config.
56
- - AI config constants: `AI_API_ENDPOINT` (TOGa Talos `…/api/ai/generate`), `AI_MODEL_ID`
57
- (`bedrock/us.anthropic.claude-haiku-4-5-…`), temperature, max-tokens, timeout.
55
+ - ClickUp config: `CLICKUP_TEAM_ID`; Dev Sprints folder via public `devSprintsFolderId()` — reads
56
+ `[clickup] sprint_folder_id`, falls back to `CLICKUP_DEV_SPRINTS_FOLDER_ID` (`90116585845`);
57
+ `CLICKUP_DESIGN_SPRINTS_FOLDER_ID` (`90116585900`, used only by `ResetInProgress`). Token via
58
+ `_Component_Api_Clickup::accessToken()` (`[clickup] token`).
59
+ - AI config: `AI_API_ENDPOINT` (TOGa Talos `…/api/ai/generate`), `AI_MODEL_ID`
60
+ (`bedrock/us.anthropic.claude-haiku-4-5-…`), temperature, max-tokens, timeout. Key via private
61
+ `aiApiKey()` → `[talos] api_key` (the same key the transcript pipeline uses); throws if missing.
58
62
  - Report distribution: `SEND_LEADERSHIP_REPORTS_TO` (leadership email list); `SEND_EMAILS_FROM` /
59
63
  `SEND_EMAILS_FROM_NAME`.
60
64
 
@@ -62,29 +66,38 @@ everything.
62
66
  `_Worker_Team_Sprint::SprintEnd`. New actions are only reachable once registered as a `CronJobs` row
63
67
  (or webhook route). See [creating-worker-actions](./creating-worker-actions.md).
64
68
 
65
- ### The sprint lifecycle (and which action runs when)
66
- A sprint runs ~14 days. Across the cycle these actions fire (scheduled as crons):
67
-
68
- 1. **Sprint launch day, 8:30 AM** — `SendSprintEndWarnings` and `SendSprintLockWarnings` email every
69
- developer who had a task in the latest sprint, reminding them to (a) finalize the *previous*
70
- sprint's task data before the 9:00 AM End capture, and (b) get their *new* sprint commitments ready
71
- to lock before the 9:45 AM launch ceremony.
72
- 2. **~9:00 AM** — `SprintEnd` captures and scores the just-finished sprint, emails reports, and
73
- (optionally) sends release notes.
74
- 3. **~9:45 AM (after launch ceremony)** — `SprintLock` freezes the new sprint's plan. In production
75
- this is now invoked via the scheduled self-gating wrapper `SprintLockScheduled` (see below), not by
76
- a cron that targets `SprintLock` directly — because the launch day is **every other Wednesday** and
77
- cron cannot express a biweekly schedule.
78
- 4. **Daily during the sprint** — `SprintDaily` emails leadership a progress dashboard.
79
-
80
- Most methods accept an optional `$sprint` number; when omitted they resolve the relevant sprint by
81
- date via `getSprint()` (which returns the sprint whose `dateStart…dateEnd` range contains today, or,
82
- for end-of-sprint, the previous sprint).
69
+ ### The sprint lifecycle — cron schedule (prod `Core.CronJobs`, Central time)
70
+ All rows are recorded in git by `dbchanges2/Core/2026-09-29a - Record sprint and design CronJobs.sql`
71
+ (PR #504, applied to prod). Sprints launch **every other Wednesday**; cron cannot say "biweekly", so
72
+ weekly rows **self-gate in PHP** against `Sprints`.
73
+
74
+ | Id | Action | Schedule | Gate |
75
+ |----|--------|----------|------|
76
+ | 9 | `Team/Sprint/SprintDaily` | `0 9 * * 1-5` | skips if no `Sprints` row covers today |
77
+ | 11 | `Team/Sprint/SprintEnd` | `0 9 * * 1` (Monday) | skips if previous sprint ended > 10 days ago (the mid-sprint Monday) |
78
+ | 40 | `Team/Sprint/SprintLockScheduled` | `0 10 * * 3` | runs only if a sprint ended yesterday |
79
+ | 83 | `Team/Sprint/SendSprintLockWarningsScheduled` | `30 8 * * 3` | same gate as SprintLockScheduled |
80
+ | 84 | `Team/Sprint/SendSprintEndWarningsScheduled` | `30 8 * * 1` | same gate as SprintEnd |
81
+ | 85 | `Team/Sprint/ResetInProgress` | `0 19 * * 1-5` | none (weekdays incl. holidays) |
82
+
83
+ (Id 21 = Design review reminders, id 86 = `Clickup/DevFinished/CatchUp` — see
84
+ [design automation](./clickup-design-sprint-automation.md) and
85
+ [QA/QC pipeline](./qa-qc-review-pipeline.md).)
86
+
87
+ Most methods accept an optional `$sprint` number; when omitted they resolve it via `getSprint()` —
88
+ the sprint whose `dateStart…dateEnd` contains today, `ORDER BY dateStart DESC, id DESC`. It returns
89
+ **null** when no row matches; every caller returns a skip message instead of fataling.
90
+
91
+ Every action returns a string to `Core.WorkerJobs.output` — skips say why; `SprintDaily`, `SprintEnd`,
92
+ and the warning actions return a JSON summary (e.g.
93
+ `{"isSuccess":true,"sprint":2,"tasksCaptured":74,"emailSent":true}`). Read the output column to tell
94
+ a good run from one that did nothing.
83
95
 
84
96
  ### Public actions (entry points)
85
97
 
86
- #### `SendSprintEndWarnings()` / `SendSprintLockWarnings()`
87
- Look up the latest `Sprints` row, find every distinct developer with a task in that sprint
98
+ #### `SendSprintEndWarnings()` / `SendSprintLockWarnings()` (+ `…Scheduled` wrappers)
99
+ The crons call the `…Scheduled` wrappers, which apply the gates in the table above and then call
100
+ these. Look up the latest `Sprints` row, find every distinct developer with a task in that sprint
88
101
  (`Developers ⋈ Tasks_Developers ⋈ Tasks`), and email each a templated reminder. End-warning = "clean
89
102
  up your data before the 9:00 AM End capture"; Lock-warning = "have your commitments ready before the
90
103
  9:45 AM lock; work added after lock is treated as unplanned/interrupt work." Both send via the
@@ -94,7 +107,10 @@ up your data before the 9:00 AM End capture"; Lock-warning = "have your commitme
94
107
  Freezes the sprint's plan at launch time. Optionally re-captures ClickUp data first, then for each
95
108
  task records the **"at lock" baseline** — `workTypeAtLock`, `sprintPointsAtLock`, `statusAtLock` — and
96
109
  initializes the per-sprint tracking counters (rework events, unjustified status/work-effort changes,
97
- in-progress time). These at-lock values are what later metrics measure *against*. If the target sprint
110
+ in-progress time). These at-lock values are what later metrics measure *against*. Per task it also
111
+ copies ClickUp **Condition → Previous Sprint Condition** (`CLICK_UP_CUSTOM_FIELD_ID__PREVIOUS_CONDITION`)
112
+ then resets **Condition = On Track** — each write in its own try/catch so one refused field does not
113
+ stop the lock. First lock running this code: 2026-10-14 (the 9/30 lock ran the old code). If the target sprint
98
114
  list cannot be found in ClickUp it emails a leadership alert. With `$generateSprintLockReport`, emits
99
115
  an Excel lock report of committed/conditional/unplanned/stretch points and tasks. When
100
116
  `$allowUnplannedTasks` is false, tasks appearing after lock with an UNPLANNED work type are flagged.
@@ -109,10 +125,8 @@ Otherwise it returns a skip message (captured to `Core.WorkerJobs.output`) and d
109
125
  `SprintLock()` itself is unchanged — this only decides *whether* to run it today. Action path
110
126
  `Team/Sprint/SprintLockScheduled` → `_Worker_Team_Sprint::SprintLockScheduled`.
111
127
 
112
- **Deploy coupling.** The worker2 code deploy and the `Core.CronJobs` row insert must land
113
- **together** (no committed migration seeds it — see Gotchas) — the cron row references the
114
- `Team/Sprint/SprintLockScheduled` action, which must exist in the deployed code or the scheduled job
115
- fails when it fires.
128
+ **Deploy coupling.** Deploy worker2 **before** applying any `CronJobs` row that names a new action —
129
+ otherwise the job fails when it fires.
116
130
 
117
131
  #### `SprintDaily($sprint = null, …)`
118
132
  In-sprint **leadership dashboard**, read-only with respect to metrics. Optionally re-captures ClickUp
@@ -127,14 +141,17 @@ Emailed to the leadership list as `TeamSprintDailyReport_Sprint{N}_{date}.xlsx`.
127
141
 
128
142
  #### `SprintEnd($sprint = null, …)`
129
143
  The largest method and the heart of the feature. Phases:
130
- 1. **Resolve** the target (previous) sprint and its developers; optionally call `CaptureSprintEnd()` to
131
- sync the final ClickUp state into `DB_TEAM`.
144
+ 1. **Resolve** the target (previous) sprint. The 10-day mid-sprint skip runs **before**
145
+ `CaptureSprintEnd()` — capture writes back to ClickUp, so it must not run on the middle Monday
146
+ (it used to, and overwrote the last sprint's data). Then optionally capture.
132
147
  2. **Score every developer** across the four metrics below.
133
148
  3. **Per-developer report workbook** (Dashboard, Team Summary, Sprint History, Task Detail, and a
134
149
  Score-Trends chart over the last `SPRINTS_BACK_FOR_AVERAGES` sprints), emailed to each developer.
135
150
  4. **Leadership summary workbook** (team-wide dashboard, all-developer summary with a *recommended
136
151
  commitment* for next sprint, and stalled-task detail), emailed to leadership.
137
- 5. Optionally call the release-notes generator.
152
+ 5. Optionally call `SendReleaseNotes()`. Its real result sets `releaseNotesSent` /
153
+ `releaseNotesError` (capped at 300 chars) in the JSON summary; a failure there is caught, so
154
+ SprintEnd still returns.
138
155
 
139
156
  #### `SendReleaseNotes($sprint = null)`
140
157
  Pulls every **completed** task in the sprint (`statusNow IN (…DONE)`) into a CSV of `workType,
@@ -143,6 +160,14 @@ with a detailed system prompt that rewrites technical task titles into business-
143
160
  stakeholder-grouped HTML release notes. The returned HTML (code fences stripped) is emailed to the
144
161
  leadership list as "Sprint N Release Notes".
145
162
 
163
+ #### `ResetInProgress(): string`
164
+ 7 PM weekdays: moves every **In Progress** task and subtask back to **To Do** in Dev Sprints
165
+ (`devSprintsFolderId()`) and Design Sprints (`90116585900`). No notification. Uses ClickUp
166
+ `GET /team/{id}/task` with `project_ids[]` (the documented folder filter — **not** `folder_ids[]`)
167
+ plus a home-folder check, rescans from page 0 after writes (moved tasks shift pages), throttles
168
+ writes (`RESET_IN_PROGRESS_THROTTLE_MICROSECONDS`), and caps page fetches per folder
169
+ (`RESET_IN_PROGRESS_MAX_ITERATIONS`).
170
+
146
171
  ### The scoring model (Sprint End)
147
172
  Each developer receives four 0–100 sub-scores and a weighted **Final Score**:
148
173
 
@@ -339,45 +364,38 @@ see [TOGa Talos Sprint Dashboard API](../../api2/features/sprint-dashboard-api.m
339
364
  **next** sprint's start (exclusive); the code stores `dateEnd` minus one day. **Proper fix for a
340
365
  future ticket:** take the number from the name but the dates from the list object's `start_date` /
341
366
  `due_date` (epoch ms), already fetched for the date-window match.
342
- - **The tooling cannot bootstrap from an empty `Sprints` table — it deadlocks.** `SprintDaily`
343
- (~L4806), `SprintEnd` (~L473-474) and `SendSprintEndWarnings` (~L218) all call `getSprint()`
344
- **before** the `Capture*` method that would create the `Sprints` row, and `getSprint()`
345
- (~L5702-5740) does `fetchRow()` with **no null check** — so on an empty table every entry point
346
- fatals with `Attempt to read property "id" on null`. The thing that creates the row is never
347
- reached. **Workaround (proven in production 2026-09-16):** `INSERT` one `Sprints` row by hand
348
- (`sprint`, `dateStart`, `dateEnd`); `CaptureSprintDaily`'s "ensure the sprint record exists" block
349
- (~L3478) self-heals from there. Anyone truncating these tables hits this — see
367
+ - **An empty `Sprints` table still cannot bootstrap itself.** Every entry point calls `getSprint()`
368
+ before the `Capture*` that would create the row. Since PR #178 it **skips with a message** instead
369
+ of fataling (`Attempt to read property "id" on null`), but it still never creates the row. Fix:
370
+ `INSERT` one `Sprints` row by hand (`sprint`, `dateStart`, `dateEnd`); `CaptureSprintDaily`
371
+ self-heals from there. See
350
372
  [Repointing the sprint tooling](../workflows/sprint-clickup-folder-repoint.md).
351
- - **`getSprint()` uses `LIMIT 1` with no `ORDER BY`** (~L5717 and ~L5721, both branches). With
352
- duplicate sprint numbers it returns an **arbitrary** row — a live violation of the
353
- deterministic-`LIMIT 1` rule in
354
- [backend-php standards](../../../standards/backend-php.md#a-limit-1-on-a-non-unique-key-must-have-a-deterministic-order-by).
373
+ - **`_underscore` `_Model_Team_Sprint` "last 4 sprints" math must not go below sprint 1.** After the
374
+ numbering restart, `sprint - 4` underflowed the unsigned sprint column (MySQL Error 1690) and
375
+ `SprintDaily` failed every day 9/22–9/30. Fixed on `_production` by `pastSprintsRange()`
376
+ (TRUE-82292). Any new "N sprints back" math must clamp at 1.
355
377
  - **`SprintLock` is NOT a safe way to repopulate empty tables.** It captures before it reads, so it
356
378
  looks like a bootstrap (its `getSprint()` call is gated behind `$generateSprintLockReport`), but
357
379
  its capture block (~L4016-4030) **writes back to ClickUp for every task** — sets Previous Sprint
358
- Work Type and sets "Added at sprint launch" = true. Run outside the real launch ceremony it
380
+ Work Type, "Added at sprint launch" = true, and the Condition copy/reset. Run outside the real launch ceremony it
359
381
  corrupts those fields across the whole sprint.
360
- - **The ClickUp "Condition" field (On Track) is retired (2026-09-25).** Not in the QA/QC plan, not in
361
- the Dev Sprints folder, never read by code. The `SprintLock` write was removed (it would fail on the
362
- new folder). `CLICK_UP_CUSTOM_FIELD_ID__CONDITION` / `CONDITION__ON_TRACK` still exist in
363
- `_underscore` but are unused — do not add new writes.
364
- - **`SprintDaily` returns nothing on success** — a production run reads `Successfully Executed
365
- (4.255 seconds): <no return message>`, so a run that worked is indistinguishable from one that
366
- never fired. Breaks the return-on-both-paths rule in the
367
- [worker2 architecture](../architecture.md); fix by returning a structured summary.
368
- - **The Team/Sprint cron rows are not in git.** Nothing in `dbchanges2/Core/` registers
369
- `Team/Sprint/%` (searched 2026-09-21), despite this doc previously listing a `CronJobs
370
- (SprintLockScheduled seed)` file. A concrete case of "a committed migration records intent, not
371
- deployed state" ([framework-rules](../../../standards/framework-rules.md)) — read the real
372
- schedule from `Core.CronJobs` in the target environment.
373
- - **The dev-sprints folder id lives in THREE places** — the `Sprint.php` constant, the
374
- `Monitor/Oneuptime.php` fallback, and `[clickup] sprint_folder_id` in `Config/production.ini`.
375
- They must move together, and `Worker/Clickup/Design.php`'s own Design-Sprints folder must not move
376
- with them. Procedure:
382
+ - **Condition and Previous Sprint Condition are live fields (2026-10-01).** The 9/25 "Condition
383
+ retired" call was reversed by the owner. Both fields now exist on the Dev Sprints and Design Sprints
384
+ lists (Previous Sprint Condition used to exist only on the old True Design Sprints folder). Writers:
385
+ `SprintLock` (above) and the webhook defaults in
386
+ [ClickUp general automation](./clickup-general-automation.md). Any task in a sprint list without
387
+ these fields (or **"Added at Sprint Launch"**) fails the lock write — the 9/30 lock first failed
388
+ with `[FIELD_115]` on that field, retry succeeded.
389
+ - **`ResetInProgress` skips multi-listed tasks whose HOME folder is the old True Dev Sprints folder**
390
+ (owner accepted for now). The same gap exists in `Clickup/DevFinished/CatchUp`. Fix: walk the
391
+ folder's lists with `include_timl=true` instead of the team-task filter.
392
+ - **The dev-sprints folder id: change `[clickup] sprint_folder_id` first.** `Sprint.php` reads it via
393
+ `devSprintsFolderId()`; the `Sprint.php` constant and the `Monitor/Oneuptime.php` fallback are only
394
+ fallbacks but should still move with it. Design folder ids must not move with them. Procedure:
377
395
  [Repointing the sprint tooling](../workflows/sprint-clickup-folder-repoint.md).
378
- - **Credentials.** The ClickUp token now comes from config (`_Component_Api_Clickup::accessToken()`);
379
- the TOGa Talos AI key is **still a hardcoded constant** at `Sprint.php:43`. Locations and remediation
380
- status: [committed secrets register](./committed-secrets-and-config-debt.md).
396
+ - **Credentials.** Both come from config now: ClickUp `[clickup] token`, TOGa Talos `[talos] api_key`
397
+ (PR #179). The old hardcoded TOGa Talos key is still in git history and must be revoked — see the
398
+ [committed secrets register](./committed-secrets-and-config-debt.md).
381
399
  - **Worker contract.** Like all 2.0 workers this must return cleanly and commit writes via the
382
400
  `_Db`/transaction layer to be durable — uncommitted captures can be silently dropped.
383
401
  - **ClickUp pagination returns duplicate tasks — the sync loop must be idempotent.** ClickUp's
@@ -406,3 +424,8 @@ see [TOGa Talos Sprint Dashboard API](../../api2/features/sprint-dashboard-api.m
406
424
  - [PHP Runtime Upgrade on Elastic Beanstalk](../workflows/php-runtime-upgrade-dependency-audit.md)
407
425
  - [TOGa Talos Sprint Dashboard API](../../api2/features/sprint-dashboard-api.md) — the productionized Record Scripts.
408
426
  - [Record Scripts](../../api2/features/record-scripts.md)
427
+
428
+ ## Change history
429
+ - 2026-10-01 — getSprint() deterministic + null-safe; JSON run summaries; SprintEnd skips before capture; release-notes key from `[talos] api_key`; SprintLock copies/resets Condition; new ResetInProgress + scheduled warning wrappers; cron rows recorded in dbchanges2 (ajean; worker2 PR #178, #179)
430
+ - 2026-09-25 — Condition write removed from SprintLock (reversed 2026-10-01) (jcardinal)
431
+ - 2026-09-16 — Repointed to the Dev Sprints folder; tables reset (kyalamarthi)
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-21
10
- owners: ["kyalamarthi"]
9
+ updated: 2026-10-01
10
+ owners: ["kyalamarthi", "ajean"]
11
11
  files:
12
12
  - worker2/Worker/Team/Sprint.php
13
13
  - worker2/Worker/Monitor/Oneuptime.php
@@ -33,15 +33,20 @@ constant edit. Done once on 2026-09-16 under TRUE-82052 (worker2 + dbchanges2).
33
33
  name with a regex that requires the year; a list created as `Sprint 1 (9/16 - 9/30)` does not
34
34
  match and the run fatals. See
35
35
  [team-sprint-management → gotchas](../features/team-sprint-management.md#gotchas).
36
- 2. **Update all three copies of the folder id — they must move together**, or the tooling
37
- split-brains:
38
- - `CLICKUP_DEV_SPRINTS_FOLDER_ID` — `worker2/Worker/Team/Sprint.php` (~L10)
39
- - `CLICKUP_SPRINTS_FOLDER_ID_FALLBACK` — `worker2/Worker/Monitor/Oneuptime.php` (~L25)
40
- - `[clickup] sprint_folder_id` — `worker2/Config/production.ini` (~L270); the escalation cron
41
- reads this and it **wins** over the Oneuptime fallback.
36
+ 2. **Update the folder id everywhere it lives:**
37
+ - `[clickup] sprint_folder_id` — `worker2/Config/production.ini`. **Primary:** `Sprint.php`
38
+ (`devSprintsFolderId()`, also used by ResetInProgress and DevFinished CatchUp) and the
39
+ escalation cron read it.
40
+ - `CLICKUP_DEV_SPRINTS_FOLDER_ID` — `worker2/Worker/Team/Sprint.php` (fallback only).
41
+ - `CLICKUP_SPRINTS_FOLDER_ID_FALLBACK` — `worker2/Worker/Monitor/Oneuptime.php` (fallback only).
42
+ - `_Worker_Clickup::QA_QC_FOLDER_IDS` — `worker2/Worker/Clickup.php` (QA/QC + sprint-default scope).
42
43
 
43
- Do **not** touch `DESIGN_SPRINTS_FOLDER_ID` in `worker2/Worker/Clickup/Design.php` — Design
44
- Sprints is a different folder with its own id.
44
+ Design Sprints is a different folder: `DESIGN_SPRINTS_FOLDER_IDS` (`Worker/Clickup/Design.php`)
45
+ and `CLICKUP_DESIGN_SPRINTS_FOLDER_ID` (`Sprint.php`) move only when Design moves.
46
+
47
+ **Also add the sprint custom fields to the new folder's lists** — at least Condition, Previous
48
+ Sprint Condition, Work Type, Previous Sprint Work Type, and "Added at Sprint Launch". A missing
49
+ field fails SprintLock (`[FIELD_115]`, seen 2026-09-30) and the webhook defaults.
45
50
  3. **Run the reset migration alone, on a fresh connection**, against the `Team` database:
46
51
  `dbchanges2/Team/2026-09-16a - Reset sprint tables for new ClickUp folder.sql`. It truncates
47
52
  child → parent (`Tasks_Developers` → `Tasks` → `Sprints`) wrapped in
@@ -55,8 +60,10 @@ constant edit. Done once on 2026-09-16 under TRUE-82052 (worker2 + dbchanges2).
55
60
  [creating-worker-actions](../features/creating-worker-actions.md)). From there
56
61
  `CaptureSprintDaily`'s own "ensure the sprint record exists" block (~L3478) keeps `Sprints`
57
62
  current on its own.
58
- 6. **Verify in the DB, not from the reply.** `SprintDaily` returns nothing on success, so a good run
59
- reads `Successfully Executed (…): <no return message>`. Confirm `Sprints` / `Tasks` repopulated.
63
+ 6. **Verify.** `SprintDaily` returns a JSON summary to `Core.WorkerJobs.output` (e.g.
64
+ `"tasksCaptured":74`); a skip message means no `Sprints` row covers today. Confirm `Sprints` /
65
+ `Tasks` repopulated.
66
+ 7. **Old folder:** no automation runs there. Close its open tasks by hand.
60
67
 
61
68
  ## Gotchas
62
69
 
@@ -68,12 +75,16 @@ constant edit. Done once on 2026-09-16 under TRUE-82052 (worker2 + dbchanges2).
68
75
  - **Truncate throws away sprint history.** The alternative is renumbering the old rows. On
69
76
  2026-09-16 the developer chose truncate and declined a backup — if history matters next time,
70
77
  renumber, or back up `Sprints` / `Tasks` / `Tasks_Developers` first.
71
- - **The cron rows are not in git.** No committed migration seeds the `Team/Sprint/%` `CronJobs`
72
- rows, so after a repoint the live schedule can only be read from `Core.CronJobs` in the target
73
- environment.
78
+ - **Cron rows do not need changing.** They name actions, not folders. Schedule is recorded in
79
+ `dbchanges2/Core/2026-09-29a - Record sprint and design CronJobs.sql` — see
80
+ [team-sprint-management](../features/team-sprint-management.md#the-sprint-lifecycle--cron-schedule-prod-corecronjobs-central-time).
74
81
 
75
82
  ## Related
76
83
 
77
84
  - [Team Sprint Management & Reporting](../features/team-sprint-management.md)
78
85
  - [Creating Worker Actions](../features/creating-worker-actions.md)
79
86
  - [dbchanges2 Architecture](../../dbchanges2/architecture.md)
87
+
88
+ ## Change history
89
+ - 2026-10-01 — Folder id now config-first (`devSprintsFolderId()`); field-setup step; cron rows now in git (ajean)
90
+ - 2026-09-16 — First repoint, TRUE-82052 (kyalamarthi)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.927",
3
+ "version": "1.0.928",
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",