toga-ai 1.0.1014 → 1.0.1016

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.
package/CLAUDE.md CHANGED
@@ -19,7 +19,7 @@ as `/skill-name` inside Claude Code.
19
19
  | Skill | Command | When to use |
20
20
  |-------|---------|-------------|
21
21
  | **start** | `/start TRUE-1234` | **Start of every session.** ClickUp ticket id only. Reads the ticket (apps, clients, task type, pseudocode), checks branches, then primes the framework architecture, coding standards, repo knowledge, and client context. Edits are blocked until it runs. |
22
- | **commit** | `/commit` | Commit only the files this session changed, with the session notes in the message. Never pushes. |
22
+ | **commit** | `/commit` | Commit only the files this session changed, with the session notes in the message, then pushes the ticket branch. |
23
23
  | **pseudocode** | `/pseudocode` | Major tickets only. Writes the agreed approach as plain text to the ticket's Pseudocode field, after approval. |
24
24
  | **finish** | `/finish` | **End of every session.** Records what was built or changed into the knowledge base, checks the ticket fields, and when the ticket is done pushes, opens the PRs, comments on the ticket, and sets it to development finished. |
25
25
  | **toga-loop** | `/toga-loop` | Orchestrate a bigger or iterative change (multi-file feature, refactor, migration, sweep, or "keep going until X") as an explore→implement→verify loop driven by subagents, with progress tracked on a state file so it survives compaction and can resume. Run after `/start`. |
@@ -251,7 +251,7 @@ ticket — edits are blocked until you run it.
251
251
  ```
252
252
  /commit
253
253
  ```
254
- Commits only the files this session changed, with the session notes. It never pushes.
254
+ Commits only the files this session changed, with the session notes, then pushes the ticket branch (never force).
255
255
 
256
256
  **End of every session — run this before closing:**
257
257
  ```
@@ -59,7 +59,7 @@ You are the maker; you never sign off your own work.
59
59
  - `sql-reviewer` on any SQL (file or inline).
60
60
  - `cso` on anything touching auth, user input, credentials, or queries.
61
61
  Fix what they find and re-run until clean.
62
- 5. **Stop at the working tree.** Do not commit or push — report what changed and wait.
62
+ 5. **Report what changed.** Commit and push happen through `/commit` (ticket branch, never force).
63
63
 
64
64
  ## Output
65
65
 
@@ -59,7 +59,7 @@ reviewer checks your work, not you.
59
59
  stale bundle until a full restart; don't trust a live browser check until then.
60
60
  4. **Review (required, via the Agent tool, off your thread):** `ecc:react-reviewer` on all
61
61
  `.jsx`/`.tsx`; `ecc:typescript-reviewer` on non-React `.ts`. Fix and re-run until clean.
62
- 5. **Stop at the working tree** — do not commit or push. Report what changed.
62
+ 5. **Report what changed.** Commit and push happen through `/commit` (ticket branch, never force).
63
63
 
64
64
  ## Output
65
65
 
@@ -10,7 +10,7 @@
10
10
  | [_ApiRequest — JSON encode/decode & api-logging behavior](features/apirequest-json-content-type.md) | `_ApiRequest`, the 2.0 outbound HTTP client — JSON encode/decode, retry, and api-logging traps; open before calling it or reading `Logs.Api`. |
11
11
  | [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment names in multiple languages via an `AssortmentTranslations` sidecar table on the metadata-driven translation layer; open when translating asso |
12
12
  | [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | Run a slow WRITE asynchronously via the Worker tier so a long request write cannot hold a stale connection and hit "MySQL server has gone away"; open before usi |
13
- | [FIELD_AUTOINCREMENT record numbering (Parameters counter, race-safe, NULL-only trigger)](features/autoincrement-record-numbering.md) | ORM-generated record numbers (`SA100001`, `SR131021`) via `FIELD_AUTOINCREMENT` and the per-tenant `Parameters` counter that makes them race-safe; open when add |
13
+ | [Record numbering (platform-wide FIELD_AUTOINCREMENT: Core.Records prefix + counter, scrambled)](features/autoincrement-record-numbering.md) | ORM record numbers (`SA1000000`, `AJ1419`, `CL519`) from one platform-wide counter per record type in `Core.Records`; open when adding a numbered model/prefix, |
14
14
  | [Calculated-column filter options (`<field>FilterOptions` model static → table meta)](features/calculated-column-filter-options.md) | Lets a calculated (`FIELD_SQL`) table column supply its own filter dropdown options via a `<field>FilterOptions` model static; open when adding a data-driven co |
15
15
  | [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent |
16
16
  | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind Fulfill & Ship: buying/persisting a carrier label (UPS/FedEx) and creating the NetSuite Item Fulfillment; open for label, reprint, retu |
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-10-07
9
+ updated: 2026-10-13
10
10
  owners: ["jcardinal", "rgirish", "mhammontree", "ajean"]
11
11
  files:
12
12
  - _underscore/_underscore.php
@@ -282,9 +282,7 @@ Columns are declared as `public` properties set to field-type constants:
282
282
  - **Calculated:** `FIELD_SQL` (see below).
283
283
 
284
284
  **Field options** (the `FIELDOPT_*` array keys that tune a field): `FIELDOPT_DEFAULT` (default
285
- value), `FIELDOPT_FOREIGNKEY_MODEL` (target model class), `FIELDOPT_AUTOINCREMENT_PREFIX` /
286
- `FIELDOPT_AUTOINCREMENT_FIRSTNUMBER` / `FIELDOPT_AUTOINCREMENT_SUFFIX` (numbering scheme, see the
287
- autoincrement feature), and `FIELDOPT_SQL_STORED` / `FIELDOPT_SQL_TYPE` (calculated fields, below).
285
+ value), `FIELDOPT_FOREIGNKEY_MODEL` (target model class), and `FIELDOPT_SQL_STORED` / `FIELDOPT_SQL_TYPE` (calculated fields, below).
288
286
 
289
287
  **Foreign keys** are arrays with the target model:
290
288
 
@@ -1,85 +1,72 @@
1
1
  ---
2
- title: "FIELD_AUTOINCREMENT record numbering (Parameters counter, race-safe, NULL-only trigger)"
2
+ title: "Record numbering (platform-wide FIELD_AUTOINCREMENT: Core.Records prefix + counter, scrambled)"
3
3
  framework: "2.0"
4
4
  repo: _underscore
5
5
  project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-05
10
- owners: ["bala", "rgirish"]
9
+ updated: 2026-10-13
10
+ owners: ["bala", "rgirish", "jcardinal"]
11
11
  files:
12
12
  - _underscore/Model.php
13
13
  - _underscore/Database.php
14
14
  - _underscore/Model/Client/SalesOrder.php
15
+ - _underscore/Model/Client/ServiceRequest.php
15
16
  - _underscore/Model/Client/TransferOrder.php
16
- - _underscore/Model/Prudential/ServiceRequest.php
17
+ - _underscore/Model/Compass/Canada/SalesOrder.php
18
+ - _underscore/Model/Core/Client.php
19
+ - dbchanges2/Core/2026-10-13d - RecordNumbering.sql
17
20
  related:
18
21
  - ../architecture.md
22
+ - ../../dbchanges2/architecture.md
19
23
  - ../../api2/features/v2-deadlock-retry.md
20
- - ../../api2/features/environment-variable-drives-underscore-branch.md
21
24
  - ../../worker2/features/netsuite-transferorder-outbound-push.md
22
25
  - ../../worker2/features/netsuite-salesorder-outbound-push.md
23
26
  ---
24
27
 
25
- ORM-generated record numbers (`SA100001`, `SR131021`) via `FIELD_AUTOINCREMENT` and the per-tenant `Parameters` counter that makes them race-safe; open when adding a prefix, debugging a duplicate-number 1062, or load-testing creates.
28
+ ORM record numbers (`SA1000000`, `AJ1419`, `CL519`) from one platform-wide counter per record type in `Core.Records`; open when adding a numbered model/prefix, debugging a 1062 on `number`, or touching `formatRecordNumber`.
26
29
 
27
30
  ## Summary
28
31
 
29
- A record number is generated by the ORM, not MySQL. A model declares:
30
-
31
- ```php
32
- public $number = [
33
- self::FIELD_AUTOINCREMENT,
34
- self::FIELDOPT_AUTOINCREMENT_PREFIX => 'TA',
35
- self::FIELDOPT_AUTOINCREMENT_FIRSTNUMBER => 100000,
36
- ];
37
- ```
38
-
39
- 12 models use it (SalesOrder `SA`, PurchaseOrder `P`, ServiceRequest `SR`, TransferOrder `TA`, Ticket, ItemReceipt, InventoryAdjustment, ...). For `DB_CLIENT` models the next number comes from a **counter row in the tenant's `Parameters` table**, taken on its own connection, so concurrent creates never get the same number.
40
-
41
- **Branch state (verified 2026-10-02):** counter code (TRUE-82209, `1760b58a` + caps-key follow-up `bf22215b`) is on `_underscore` `_beta`, `_sandbox-dev`, `TRUE-82209`. **Not** on `_production` or `_sandbox-client` — prod still runs the old `MAX()+1` path and the Prudential `usleep` stopgap.
32
+ A model declares `public $number = [self::FIELD_AUTOINCREMENT];` — nothing else (the old `FIELDOPT_AUTOINCREMENT_PREFIX/FIRSTNUMBER/SUFFIX` options, the `MAX()+1` path and the per-tenant `Parameters` counter / PR `_underscore#756` are gone; PR 756 must NOT be merged). Prefix and counter live in `Core.Records`, so numbers are unique across all clients. TRUE-82715.
42
33
 
43
34
  ## How it works
44
35
 
45
- 1. **Fires only when the field is NULL.** A supplied number always wins (NetSuite importer's `tranId`, Compass PO `mitsSalesOrder`), and bypasses the counter. A front-end that posts `number` silently disables the generator.
46
- 2. **Counter key:** `NEXT_NUMBER_<TABLE_IN_SNAKE_CASE>_<PREFIX><SUFFIX>`, all caps — e.g. `NEXT_NUMBER_SERVICE_REQUESTS_SR`, `NEXT_NUMBER_SALES_ORDERS_SA`, `NEXT_NUMBER_PURCHASE_ORDERS_P`, `NEXT_NUMBER_SALES_ORDERS_SAC` (Compass Canada). One row per table + prefix + suffix. `value` = last number issued.
47
- 3. **Allocate** (`_Model::allocateAutoincrementNumber()`): `UPDATE Parameters SET value = LAST_INSERT_ID(CAST(value AS UNSIGNED)+1) WHERE key=...`, read back with `getInsertId()`, then **commit at once**. Runs on `_Database::DB_CLIENT_SEQUENCE` (`'ClientSequence'`), a separate connection — the api2 request transaction commits only at request end, so taking the row lock there would serialize every create.
48
- 4. **`DB_CLIENT_SEQUENCE` is automatic.** `_Database::register()` points it at the same DB whenever the alias is `_underscore::DB_CLIENT` (covers api2, worker2, saml, cXML, Nychh worker), and drops its connection when DB_CLIENT is re-pointed to another tenant. No app change needed.
49
- 5. **Self-seeding.** Row missing → run the old `MAX()` SQL on the sequence connection (writer, not read host), `INSERT ... ON DUPLICATE KEY UPDATE value = GREATEST(...)`, commit between each step. No per-tenant SQL or seed script.
50
- 6. **Collision retry in `save()`.** If the INSERT fails 1062 **and** the message contains the number this save generated (a hand-set number took it), resync the counter past `MAX()` and retry, up to `MAX_AUTOINCREMENT_ATTEMPTS = 5`. A 1062 on any other unique key (uuid) is re-thrown. Any failed counter step rolls back the sequence connection.
51
- 7. **Unchanged:** Core-DB models and `search()` keep the old `MAX()+1` path. **No zero padding** (prefix + integer). **Gaps are possible** (rolled-back request still used its number) — same as before.
36
+ 1. **Columns:** `Core.Records.numberPrefix CHAR(2) UNIQUE`, `nextNumber BIGINT UNSIGNED`. No `RecordFields` rows on purpose → not editable through the API.
37
+ 2. **Row lookup:** `_Model` walks its class + parents against `Records.model` (stored with leading backslash, UNIQUE). Client overrides (`_Model_Compass_Canada_SalesOrder`) use the base row (SA). No row / NULL prefix / NULL counter → plain `Exception` (fail loud).
38
+ 3. **Allocate:** `UPDATE Records SET nextNumber = LAST_INSERT_ID(nextNumber + 1) WHERE id = X` on `_Database::DB_CORE_SEQUENCE` (`'CoreSequence'`), committed at once; counter = insertId − 1. CoreSequence follows `DB_CORE` when registered (api2), else falls back to the config path of the physical Core DB (worker2 prod, saml). Re-registering Core with other settings closes the CoreSequence link.
39
+ 4. **Format:** `prefix + formatRecordNumber(counter)` — no hyphen, no check digit. d = digit count of counter, c = offset within that width: `shown = 10^(d-1) + (c * 387419) % (9*10^(d-1))`. Bijection per width (multiplier coprime to 2, 3, 5) → no uniqueness check or retry loop; same digit count as the counter; length grows on its own.
40
+ 5. **Start counters** (dbchanges2 seed): SA(14), SR(35), TK(99), ET(191) at 1000000 — 7 digits so they never equal old 6-digit numbers; PU(17), TR(312), RC(78), FF(28), AJ(154), CS(222) at 1000; CL(51) at 100 + client count. `Core.Clients.number VARCHAR(16) NOT NULL UNIQUE` added + backfilled in id order (RecordFields 2740).
41
+ 6. **Hand-set number wins** (counter untouched). 1062 retry up to 5 only when the dup is the number this save generated. `forSearching` with null number skips the field (keeps `load()`'s empty-WHERE guard).
42
+
43
+ **Format rules (jcardinal):** 2-letter prefix, stored in `Core.Records` not the model; letters O, I, B banned (look like 0, 1, 8), L allowed; validate in PHP (no CHECK — see [dbchanges2 rule 15](../../dbchanges2/architecture.md)). TA → TR.
52
44
 
53
- **Rejected alternatives (2026-10-02):**
54
- - Whole-request replay via `DEADLOCK_RETRIABLE_ROUTES` for every numbered route — unsafe per tenant: Adyen / Elite / Growrk `postPost` hooks have external side effects. See [V2 request retry](../../api2/features/v2-deadlock-retry.md).
55
- - Derive number from the AUTO_INCREMENT `id` — `number` is NOT NULL UNIQUE and hand-set numbers break it.
56
- - `GET_LOCK` — the lock is held to request end, so creates serialize.
45
+ **Rejected:** plain counter (type unreadable); numeric million bands (break at 1M); client id/date in number (long, leaks); check digit; Crockford base32 (hard to read aloud); random + exists-check loop; per-type multiplier column (edits break numbers); `numberMinDigits` column (start counter sets length). Block allocation of 1,000 deferred — one write per create is fine for now.
57
46
 
58
47
  ## Gotchas
59
48
 
60
- - **⚠ Old path race (still live on prod until this ships):** `SELECT MAX+1` then a separate INSERT → concurrent creates pick one number → loser gets 1062 `Duplicate entry 'SR131021' for key 'ServiceRequests.number'` → HTTP 400 `EV-10`, record lost. The number in the error belongs to the **winner**. **Staggering or delaying callers does NOT fix it** — the unsafe window is wider than a second: on dev-sandbox 2026-09-30 a losing request read at `11:45:02` while the winner committed at `11:45:03`. Prod rate on the old path: **8 failed `POST /v2/service-requests` for Prudential between 2026-09-10 and 2026-09-21**. Only the counter removes it.
61
- - **⚠ A prefix on a SHARED model is not unique across clients** — every tenant starts at `TA100000`. Never key a cross-client id on `number`; NetSuite account 1095849 is shared, so pushes key `externalId` on uuid (`toga-so-<uuid>` / `toga-to-<uuid>`).
62
- - **SA / TA numbers are placeholders** — overwritten by NetSuite `tranId` on write-back (~12s). Never build a key or match on `number`. Keeping a Toga number permanently needs **two** changes: a guard in `writeBackNetSuiteId()` **and** dropping `number` from the PUT in `library/app/api/toga2.php` (~L4295) and `_underscore/Trait/Netsuite/TransferOrder.php` (tried 2026-09-02, reverted).
63
- - **`FIELD_AUTOINCREMENT` is special-cased in `buildSqlFieldValue()`**, so a null value does not collapse a `load()` WHERE the way a null `FIELD_CHAR` does — see [_underscore architecture](../architecture.md).
64
- - **Adding the option renumbers nothing**; existing rows are untouched.
65
- - **Error class depends on boot.** Real boot calls `mysqli_report(MYSQLI_REPORT_STRICT)` (`Error.php:217`), so a failed query returns false and `_Query` throws `_Exception_Database` (which `save()` catches). A test harness that skips this boot gets raw `mysqli_sql_exception` instead — boot the framework fully when testing.
66
- - **Reproducing the race needs distinct rows per call.** Each concurrent test request must use a **different contact** (employee_xid / email) **and unit serialNumber**. Reusing one makes requests queue on that row lock and the race never shows (old code "passed" 10/10). Release all calls at once with the gate-file pattern ([V2 request retry](../../api2/features/v2-deadlock-retry.md)). Prudential `POST /v2/service-requests` also needs `customer.uuid`. Network latency is irrelevant.
67
- - **Beta can't take big bursts:** sandbox-dev DB `max_connections = 90`; ~15+ simultaneous requests → `Too many connections` → 500 `EO-1`. Keep bursts at ~10. See [ENVIRONMENT decides the branch](../../api2/features/environment-variable-drives-underscore-branch.md).
68
- - **Run `php -l Model.php Database.php` after resolving merge conflicts on env branches.** The TRUE-82209 merge into `_sandbox-dev`/`_beta` dropped `public static $_aliases` / `private static $_parked` in `Database.php` and duplicated `private $_model_generatedAutoincrementFields` in `Model.php` → beta outage (`Access to undeclared static property _Database::$_aliases`). The error page also dumped `$_SERVER`.
49
+ - **⚠ NEVER change `AUTOINCREMENT_MULTIPLIER = 387419`** — every issued number depends on it.
50
+ - **New numbered model needs a `Core.Records` row with `numberPrefix` + `nextNumber`** or creates throw. `Model/Client/Scope.php` (Desk branch TRUE-82615, prefix `'SC-'`) has no row: on merge make it plain `FIELD_AUTOINCREMENT` + seed a Records row prefix `SC`.
51
+ - **Local DB with an old `Clients.number` (CL1…)** → the migration's ADD COLUMN fails; fix by hand locally. Prod had no such column though `_Model_Core_Client` declared one.
52
+ - **SA / TR numbers are placeholders** — NetSuite `tranId` overwrites them on write-back (accepted). Never key a match on `number`; pushes key `externalId` on uuid.
53
+ - **Old `MAX()+1` race (why this exists):** concurrent creates got one number → 1062 → HTTP 400 `EV-10`, record lost (Prudential SR, TRUE-82209). Staggering callers did not fix it.
54
+ - **Gaps are possible** (a rolled-back request still used its number).
55
+ - **Reproducing a race needs distinct rows per call** (different contact + serial) and a gate-file release ([V2 request retry](../../api2/features/v2-deadlock-retry.md)); sandbox-dev `max_connections = 90` → keep bursts ~10.
56
+ - **Run `php -l Model.php Database.php` after merge conflicts on env branches** — a past merge dropped `_Database::$_aliases` → beta outage.
69
57
 
70
58
  ## Verification
71
59
 
72
- Beta (`api.beta.togahub.com` → sandbox-dev), 2026-10-02: before fix 8/10 concurrent creates failed 1062; after fix 100/100 (10 rounds of 10), 116 distinct numbers, counter matched `MAX()`.
60
+ 2026-10-13 local (Client_Elite): single create AJ1419 for counter 1000; 10 concurrent creates distinct, counter +10, no 1062; hand-set `TEST-HAND-1` kept, counter unchanged; Compass Canada → row 14 SA. 2,000,000-counter uniqueness test passed.
73
61
 
74
62
  ## Related
75
63
 
76
64
  - [_underscore architecture](../architecture.md)
77
- - [V2 request retry](../../api2/features/v2-deadlock-retry.md)
65
+ - [dbchanges2 architecture](../../dbchanges2/architecture.md)
78
66
  - [NetSuite TransferOrder outbound push](../../worker2/features/netsuite-transferorder-outbound-push.md)
79
67
  - [NetSuite SalesOrder outbound push](../../worker2/features/netsuite-salesorder-outbound-push.md)
80
68
 
81
69
  ## Change history
82
- - 2026-10-05 — Noted the old `MAX()+1` window exceeds one second, so staggering callers is not a workaround; added the prod failure rate still seen on `_production`/`_sandbox-client`. (bala)
83
- - 2026-10-02 — Replaced unlocked `MAX()+1` with a per-tenant `Parameters` counter on its own committed connection (`DB_CLIENT_SEQUENCE`), self-seeding, 1062 resync retry; removed Prudential `usleep` stopgap. On `_beta`/`_sandbox-dev` only. (rgirish)
84
- - 2026-09-22 — Recorded that the `MAX+1` generator is not concurrency-safe. (rgirish)
85
- - 2026-09-02 — `TA` / 100000 added to the shared `_Model_Client_TransferOrder`. (bala)
70
+ - 2026-10-13 — Replaced per-tenant Parameters counter (PR 756, not merged) with platform-wide prefix + counter in `Core.Records`, scrambled format; removed FIELDOPT_AUTOINCREMENT_* options and Prudential usleep. TRUE-82715. (jcardinal)
71
+ - 2026-10-02 — Per-tenant Parameters counter on `_beta` (superseded). (rgirish)
72
+ - 2026-09-02 — `TA` added to shared TransferOrder (now TR). (bala)
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-10-06
9
+ updated: 2026-10-13
10
10
  owners: [jcardinal, mhammontree, bala, ajean]
11
11
  files:
12
12
  - Core/
@@ -258,6 +258,7 @@ This lifecycle is automated by a local browser tool — see the [TOGa 2.0 Client
258
258
  - No prose preamble in a migration file: one short line per statement; narrative goes to the ticket.
259
259
  - Before running on prod, confirm each tenant's existing natural-key values match EXACTLY, or a mismatch inserts a duplicate instead of a no-op.
260
260
  14. **Never assume a migration has or has not run — verify (ask, or SELECT the live schema) before claiming state or editing a file.** An applied file is immutable: to undo it, RESTORE the original file to match what ran, then add a NEW dated forward-only file that drops/changes it (fresh envs run both and converge; run envs run only the new one). MySQL has no `DROP COLUMN IF EXISTS`, so restoring the add is what makes the later drop safe everywhere.
261
+ 15. **Never add MySQL `CHECK` constraints in schema changes** — they caused problems with database backups (jcardinal, 2026-10-13). Validate allowed values in PHP instead.
261
262
 
262
263
  ## Bulk data loads — batch, and stage large sets in a temp table
263
264
 
@@ -393,3 +394,7 @@ The extra `SELECT … FROM ( … ) existing` layer both dodges error 1093 and pi
393
394
  - **A `Client/` fan-out file that hardcodes a `customRecordFieldId` wires the wrong field for 29 of 30 tenants with no error** (rule #10). Custom-field ids are per-client `AUTO_INCREMENT`; keep custom-field work in the tenant's own folder.
394
395
  - **An unguarded registration/seed insert can double-register** on a fresh environment (rule #7) — guard on the semantic identity, not the description.
395
396
  - **The `dbchanges2` folder name IS the target database.** Running a `Core/` file against a client DB fails as `Table 'Client_X.<Table>' doesn't exist` (files are unqualified by design) — which names a missing table, not the real mistake. See [targeting the right database](workflows/targeting-the-right-database.md).
397
+
398
+ ## Change history
399
+
400
+ - 2026-10-13 — Added rule 15: no MySQL `CHECK` constraints (TRUE-82715). (jcardinal)
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-10-12
8
+ updated: 2026-10-13
9
9
  owners: [jcardinal, mhammontree, dfranks, bala, ajean]
10
10
  files: []
11
11
  related:
@@ -51,6 +51,8 @@ conventions.
51
51
  10. Text/Blob fields
52
52
  11. Custom fields
53
53
 
54
+ **No `CHECK` constraints.** Never add MySQL `CHECK` constraints in schema changes; they caused problems with database backups. Validate allowed values in PHP. See [dbchanges2 rule 15](../apps/dbchanges2/architecture.md).
55
+
54
56
  ### UUIDs
55
57
 
56
58
  * All records inserted in the 2.0 database must be given a random UUID in the second column of each table called `uuid`
@@ -1026,5 +1028,6 @@ See: 2.0/apps/worker2/features/cross-account-aws-access.md (role model),
1026
1028
 
1027
1029
  ## Change history
1028
1030
 
1031
+ - 2026-10-13 — Table Design: no MySQL `CHECK` constraints; validate in PHP (TRUE-82715). (jcardinal)
1029
1032
  - 2026-10-12 — AWS access rule now covers 1.0 + 2.0: no IAM keys, assume WorkloadsRuntime; static-key grandfathering removed (TRUE-82705). (jcardinal)
1030
1033
  - 2026-10-07 — Added the short-file-docblock rule (design history belongs in the knowledge base). (mhammontree)
@@ -17,7 +17,7 @@ project: _Underscore
17
17
  client: prudential
18
18
  type: profile
19
19
  status: active
20
- updated: 2026-10-02
20
+ updated: 2026-10-13
21
21
  owners: ["jcardinal", "rgirish", "bala", "mhammontree"]
22
22
  files: []
23
23
  related:
@@ -120,11 +120,10 @@ The order-created cron branches on this id; the shipped/delivered crons branch o
120
120
  The 15-minute `prudential_auth_rejection_alert.php` cron now alerts on them — see
121
121
  features/service-request-auth-rejection-alert.md.
122
122
 
123
- - **SR number race (ServiceNow bursts → 1062 `Duplicate entry 'SR…'` → 400 `EV-10`, SR lost).** Fixed in
124
- the framework by a `Parameters` counter (`NEXT_NUMBER_SERVICE_REQUESTS_SR`); the `usleep(0.5–2s)` stopgap in
125
- `_Model_Prudential_ServiceRequest::prePost()` is removed. Both on `_underscore` `_beta`/`_sandbox-dev` only
126
- (2026-10-02) — prod still has the race and the stopgap. Load-test with a distinct contact + serial per call and
127
- `customer.uuid` set. See [FIELD_AUTOINCREMENT record numbering](../../2.0/apps/_underscore/features/autoincrement-record-numbering.md).
123
+ - **SR number race (TRUE-82209: ServiceNow bursts → 1062 `Duplicate entry 'SR…'` → 400 `EV-10`, SR lost).** Fixed by
124
+ platform-wide record numbers (TRUE-82715); the `usleep` stopgap in `_Model_Prudential_ServiceRequest::prePost()`
125
+ is removed. Load-test with a distinct contact + serial per call and `customer.uuid` set. See
126
+ [Record numbering](../../2.0/apps/_underscore/features/autoincrement-record-numbering.md).
128
127
 
129
128
  ## Monitoring
130
129
  - `Monitor/Prudential/{SalesOrderGenerationBacklog,DellPurchaseOrderTransmissionBacklog,OrderShippedUpdateBacklog}`
@@ -6,7 +6,7 @@ project: Claude Harness
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-10-09
9
+ updated: 2026-10-13
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - claude/skills/start/SKILL.md
@@ -43,11 +43,12 @@ writes, and agents until the session has a ticket. These replaced `/kickoff`, `/
43
43
  KB, moves the status `to do` -> `in progress`, and loads PR review feedback if an open PR
44
44
  exists (rework: same branch, same PR). If ClickUp can't be read, it stops — no offline mode.
45
45
  2. **`/commit`** — commits only this session's files, ticket from the session file, then the
46
- branch. Never pushes.
46
+ branch. Then pushes the ticket branch (`git push -u origin <TICKET>`, never force).
47
47
  3. **`/pseudocode`** — major tickets only. Writes the agreed approach to the Pseudocode field
48
48
  after the developer approves. Touches no other field.
49
49
  4. **`/finish`** — always writes the team KB first. Then one approval list for ticket fields
50
- (Application, Stakeholders, Task Type, title, Has UI Changes). If the ticket is done: push each
50
+ (Application, Stakeholders, Task Type, title, Has UI Changes). Then it asks "Is this ticket finished?" — a yes is the permission (no extra asks): commit any
51
+ uncommitted session files, push each
51
52
  repo with work, open or update a PR per repo, post a PM comment, set status
52
53
  `development finished`. The PM comment is plain bullets for a non-technical PM: "What
53
54
  changed" always; "Manual steps", "Still open", "Ticket fields changed by Claude" only when
@@ -177,6 +178,8 @@ All tested 2026-10-08:
177
178
  - 2026-10-09 — TRUE-82605: `/start` names the session `<TICKET>: <short title>` (set-title +
178
179
  SessionStart `sessionTitle`; `/rename` to paste). (jcardinal)
179
180
 
181
+ - 2026-10-13 — `/commit` now pushes the ticket branch; `/finish`'s "finished" yes covers commit,
182
+ push, and PR with no extra asks; Claude may switch to the ticket branch in normal work. (jcardinal, TRUE-82715)
180
183
  ## Related
181
184
 
182
185
  - [Ticket-branch creation safety](./ticket-branch-creation-safety.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1014",
3
+ "version": "1.0.1016",
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",
@@ -9,37 +9,25 @@ Start every session with `/start TRUE-1234` (the ClickUp ticket id only). The `s
9
9
  blocks edits, writes, and agents until a session has a ticket. The ticket drives the branch name,
10
10
  the commit prefix, the PR title, and the ticket updates at `/finish`.
11
11
 
12
- ## Claude never commits or pushes on its own
13
-
14
- Claude **must never** run `git commit`, `git push`, `git merge`, or open/merge a PR against a
15
- **project repo** unless the developer explicitly asked for it in that conversation.
16
-
17
- Writing and editing files is expected. **Publishing them is not.** After making changes, Claude
18
- stops at the working tree, reports what changed, and waits. The developer decides when it lands.
19
-
20
- Two exceptions, and only two:
21
-
22
- 1. **The developer explicitly told Claude to** — "commit this", "push it", "open the PR". A
23
- general "go ahead" on an implementation task is **not** commit authorization; it authorizes
24
- the code changes, not the publish.
25
- 2. **The team knowledge base repo** (`agilantsolutions/claude`, cloned to `~/toga-tech`) — the
26
- `/finish` skill's KB capture commits and pushes to `_main`. That remains automatic.
27
-
28
- A standing directive from an earlier session does not carry forward. Blanket phrases like "you
29
- have full access" grant tool access, not publish authority. When in doubt: **don't commit, ask.**
30
-
31
- Invoking one of these skills **is** explicit authorization, but only for what that skill
32
- describes. It does not authorize publishing anything else in the session.
33
-
34
- - `/commit` — local commits of only the files this session changed. Never pushes.
35
- - `/finish` — the KB push to `_main`, the ticket-branch pushes, the PRs, and the ticket writes.
36
- If this session's files are uncommitted, `/finish` asks once and, on "yes", runs the
37
- `/commit` steps itself (same rules as `/commit`). Claude writes and posts the PM comment,
38
- PR subject/body, commit notes, and KB summary itself — no approval prompt (secret scan
39
- still runs). Ticket field changes still need the one approval list; pushes still ask.
40
- - `/ship`, `/land-and-deploy` — the commits, pushes, and PRs they describe.
41
-
42
- Force-pushing is never automatic in any repo, including the knowledge repo.
12
+ ## Commit, push, and PR — what Claude does (decided 2026-10-13, TRUE-82715)
13
+
14
+ 1. **`/commit`** commits only this session's files **and pushes the ticket branch**
15
+ (`git push -u origin <TICKET>`). Never force.
16
+ 2. **`/finish`** does the KB capture, then asks one question: **"Is this ticket finished?"**
17
+ On yes it: commits any uncommitted session files, pushes, opens (or updates) one PR per
18
+ repo, comments on the ticket, and sets the status. No extra "may I push?" or "may I open
19
+ the PR?" questions — that yes is the permission. Ticket field changes still use the one
20
+ approval list.
21
+ 3. **Branches.** Claude may create or switch to the ticket branch as part of normal work.
22
+ 4. `/ship`, `/land-and-deploy` — the commits, pushes, and PRs they describe.
23
+ 5. The KB repo (`~/toga-tech`) — `/finish` commits and pushes to `_main`.
24
+
25
+ Always, in every repo:
26
+
27
+ - Never force-push.
28
+ - Never stash, and never discard someone's uncommitted changes.
29
+ - Never commit straight to `_main` or the default branch of an app repo.
30
+ - Never commit secrets, `.env` files, or local paths (secret scan still runs).
43
31
 
44
32
  ## Claude never creates git worktrees
45
33
 
@@ -63,25 +51,12 @@ that repo, Claude may run `start-helper.js create-branch` to create (from the de
63
51
  or check out the ticket branch `<TICKET>`. Nothing else: no worktree, no stash, never on a
64
52
  repo with uncommitted changes, never force-push. See *Branch naming*.
65
53
 
66
- ## Claude never rewrites code because a branch changed
67
-
68
- Many developers work across many projects and switch branches constantly. A change that is
69
- "missing" from the working tree is almost always **on another branch** — not lost.
70
-
71
- If Claude notices that work it made earlier in the conversation is gone from the working tree,
72
- or sees it in the git history on a different branch, it **must stop and ask**. It must not:
54
+ ## Missing work is usually on another branch
73
55
 
74
- - Re-implement or re-write the code from memory
75
- - Switch branches itself (`git checkout`, `git switch`) to "restore" the work
76
- - Stash, cherry-pick, merge, or reset to recover it
77
- - Assume the developer wants it back
78
-
79
- The correct response is to **pause the conversation** and say what it observed — which changes
80
- appear absent, and which branch they seem to live on — then ask the developer to switch to that
81
- branch so the existing code can be reviewed. Reviewing what is actually there beats regenerating
82
- it: a rewrite silently discards whatever the developer (or a teammate) changed in the meantime.
83
-
84
- Only rewrite when explicitly told to.
56
+ If work Claude made earlier is gone from the working tree, it is almost always on another
57
+ branch — not lost. Do not re-write it from memory. Find the branch it lives on (`git log
58
+ --all`), say so, and switch to it if the working tree is clean. If the tree has uncommitted
59
+ changes, stop and ask — never stash or discard them.
85
60
 
86
61
  ## Check git before stating commit state
87
62
 
@@ -124,13 +99,17 @@ Never force-push to `_main` under any circumstances.
124
99
  The branch is the bare ClickUp ticket id — e.g. `TRUE-82600`. No prefix, no description.
125
100
  ClickUp may create it, or `/start` creates it. One branch per ticket per repo.
126
101
 
102
+ **Base branch.** Only the new platform app repos (today `toga25-desk` and `toga25-iq`) branch from
103
+ `toga25` and open their PR back to `toga25`. Every other repo (`_underscore`, `api2`, `worker2`,
104
+ `dbchanges2`, …) uses its own default branch, even in the same session.
105
+
127
106
  `/start` checks that every repo on the ticket (from its Application values) is on the ticket
128
107
  branch. On a mismatch it asks once per repo: "Create/switch to `<TICKET>` in `<repo>` from
129
108
  `<base>`?" On yes it runs `start-helper.js create-branch` (checks out `origin/<TICKET>` or the
130
109
  local branch if one exists, else creates it from the default branch and pushes it with
131
110
  `git push -u`, never force). A repo with uncommitted changes is refused — Claude stops and
132
- says so. This is the only case where Claude creates or switches a branch; otherwise Claude
133
- never switches branches itself.
111
+ says so. Claude may also create or switch to the ticket branch later in normal work, with the
112
+ same limits.
134
113
 
135
114
  The KB/harness repo (`toga-tech`) is the exception: work there goes on `_main`.
136
115
 
@@ -44,5 +44,5 @@ statement so its view is genuinely independent.
44
44
  3. A lead implements (`backend-lead` / `frontend-lead` / `database-architect`).
45
45
  4. Reviewers + `qa-engineer` verify (maker ≠ checker).
46
46
  5. `documentation-engineer` / `knowledge-writer` capture what's durable.
47
- 6. Stop at the working tree. Claude never commits or pushes a project repo unless the
48
- developer explicitly says so this session.
47
+ 6. `/commit` commits and pushes the ticket branch; `/finish` opens the PRs once the developer
48
+ says the ticket is finished. Never force-push.
@@ -588,7 +588,7 @@ ticket — edits are blocked until you run it.
588
588
  \`\`\`
589
589
  /commit
590
590
  \`\`\`
591
- Commits only the files this session changed, with the session notes. It never pushes.
591
+ Commits only the files this session changed, with the session notes, then pushes the ticket branch (never force).
592
592
 
593
593
  **End of every session — run this before closing:**
594
594
  \`\`\`
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: commit
3
- description: Commit ONLY the files this Claude Code session changed, in every repo it touched, and write the session's notes (why the task exists, what was found, approach, turned-down options, who decided) into the commit message so the PR reviewer can build the PR summary from it. The ticket comes from the /start session ticket, then the branch. Safe when several sessions work on the same branch in the same checkout. Never pushes — /finish pushes and opens the PRs. Trigger on "/commit", "commit this", "commit my changes".
3
+ description: Commit ONLY the files this Claude Code session changed, in every repo it touched, and write the session's notes (why the task exists, what was found, approach, turned-down options, who decided) into the commit message so the PR reviewer can build the PR summary from it. The ticket comes from the /start session ticket, then the branch. Safe when several sessions work on the same branch in the same checkout. Then pushes the ticket branch (never force); /finish opens the PRs. Trigger on "/commit", "commit this", "commit my changes".
4
4
  ---
5
5
 
6
6
  # /commit — commit this session's files, with the session's notes
@@ -13,9 +13,11 @@ ticket comes from the `/start` session ticket file
13
13
 
14
14
  ## Step 0 — Rules
15
15
 
16
- - Running `/commit` IS the developer's explicit commit authorization — **for these commits only**.
17
- - Never push. Never `git add -A` or `git add .`. Never `--no-verify`. Never `--amend`.
18
- Never switch branches. Never commit a file the developer did not approve.
16
+ - Running `/commit` IS the authorization to commit these files and push the ticket branch.
17
+ - Push only the ticket branch: `git -C <repo> push -u origin <TICKET>`. Never force-push. Never
18
+ push or commit straight to `_main` / the default branch of an app repo.
19
+ - Never `git add -A` or `git add .`. Never `--no-verify`. Never `--amend`.
20
+ Never stash or discard uncommitted changes. Never commit a file the developer did not approve.
19
21
 
20
22
  ## Step 1 — Find your own session id
21
23
 
@@ -129,7 +131,8 @@ Then, per repo:
129
131
 
130
132
  1. `helper forget --session=<id> --repo=<repo> --files=<committed files, comma-separated>`
131
133
  2. Report per repo: short hash, subject, file count.
132
- 3. Say: "Committed locally. /commit never pushes — run /finish when the ticket is done; it
133
- pushes and opens the PRs."
134
- 4. Say: changes made by hand outside Claude can still be committed the normal way in VS Code;
134
+ 3. Push: `git -C <repo> push -u origin <TICKET>` (never force). If the push is rejected, stop
135
+ and report it — do not force or rebase on your own.
136
+ 4. Say: "Committed and pushed. Run /finish when the ticket is done; it opens the PRs."
137
+ 5. Say: changes made by hand outside Claude can still be committed the normal way in VS Code;
135
138
  the PR reviewer will show those as manual changes.
@@ -13,7 +13,8 @@ ClickUp reads run in the `ticket-reader` subagent. The main thread distills and
13
13
 
14
14
  Claude writes and posts all summary text itself — PM comment, PR subject and body, commit
15
15
  notes, KB capture summary. Never ask the developer to approve that wording. Approvals are only
16
- for real decisions: ticket field changes (one list), ELEVATED KB docs, and pushes.
16
+ for real decisions: ticket field changes (one list), ELEVATED KB docs, and "Is this ticket finished?". A yes to
17
+ that question is the permission to commit, push, and open the PRs — no extra asks.
17
18
 
18
19
  ## Step 0 — Rules, helpers, session ticket
19
20
 
@@ -349,10 +350,9 @@ skip Steps 12–14 and go to Step 15 (PM comment) and Step 16 (status).
349
350
  `origin/<TICKET>` if pushed). This includes commits from other sessions — that is intended.
350
351
  Keep for each: repo path, `owner/repo`, ahead count, pushed or not.
351
352
  2. `COMMIT status --session=<sid> --root="$CLAUDE_PROJECT_DIR"`. If any repo has `toCommit`
352
- files: ask once — "These files aren't committed: <list>. Run /commit for them now?"
353
- **Yes** → run the `/commit` skill's steps yourself (same rules as `skills/commit/SKILL.md`:
353
+ files: do not ask again — the "finished" yes covers it. Run the `/commit` skill's steps yourself (same rules as `skills/commit/SKILL.md`:
354
354
  only this session's files, the `Claude-Notes: v1` format), then re-run step 1 and continue.
355
- **No** → stop and say "Run /finish again when they are committed." (Changes outside
355
+ (Changes outside
356
356
  Claude's list, `otherChanged`, are the developer's — mention them, do not block.)
357
357
  3. If `gh auth status` fails, say exactly: "Run `gh auth login` in a terminal, then tell me
358
358
  'done'". When they say done, run `gh auth status` again and continue only if it passes.
@@ -158,8 +158,9 @@ developer has not named the session.
158
158
  Branches named with the ticket id link to the ClickUp ticket through the GitHub
159
159
  integration — on the first real run, confirm the link shows on the ticket.
160
160
 
161
- This is the **only** time Claude creates or switches a branch, and only after the developer's
162
- yes (`rules/common/git-workflow.md`). On no, say: "Switch these repos to `<T>`, then tell me.
161
+ Here Claude asks first. Later in the session Claude may create or switch to the ticket branch
162
+ as part of normal work, with the same limits — never stash, never discard changes, never
163
+ force (`rules/common/git-workflow.md`). On no, say: "Switch these repos to `<T>`, then tell me.
163
164
  Or say 'continue anyway'." Only on an explicit "continue anyway":
164
165
 
165
166
  ```