toga-ai 1.0.293 → 1.0.295

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,12 +6,13 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-07
10
- owners: ["bala"]
9
+ updated: 2026-07-09
10
+ owners: ["bala", "mhammontree"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
13
  related:
14
14
  - ../architecture.md
15
+ - ../../../clients/aig/features/entitlement-intake.md
15
16
  ---
16
17
 
17
18
  ## Summary
@@ -57,6 +58,37 @@ rows on every write.
57
58
  If you need to link by a business key (like an employee XID), **resolve that key to a uuid in your
58
59
  caller first** (e.g. build a `key → uuid` map), then send `{uuid}`.
59
60
 
61
+ ## Per-API overrides (`Apis_RecordFields`)
62
+
63
+ The base identifier flags above (`Core.RecordFields.isIdentifier`) can be **overridden per API**
64
+ via rows in a tenant's `Apis_RecordFields` table, read by the V2 resolver's override block
65
+ (`V2.php` ~6926–6936; child policy in `getChildPolicy()` ~8019–8041; loader ~8526–8538). Two
66
+ columns matter:
67
+
68
+ - **`overrideIsIdentifier`** — overrides whether the field is a searchable identifier for that API.
69
+ - **`overrideChildPolicy`** — overrides link-vs-create for the nested record (e.g. `MATCH_CREATE`
70
+ = match an existing child by identifier, else create it).
71
+
72
+ > **Trap: a `NULL` `overrideIsIdentifier` is read as "not an identifier", not "no opinion".**
73
+ > The override block evaluates `!$row->overrideIsIdentifier`, so a `NULL` **unsets** the field
74
+ > from the API's searchable identifiers even when the base `Core.RecordFields` row has
75
+ > `isIdentifier=1`. Any `Apis_RecordFields` row created **only** to set a child policy
76
+ > (leaving `overrideIsIdentifier` NULL) will silently strip that field as an identifier. The
77
+ > nested object then has no searchable identifier and the write fails **EV-12 VALIDATION**
78
+ > (`V2.php:7094`) — the `MATCH_CREATE` that should have saved it never runs on that path.
79
+ > Correct the data with an explicit `overrideIsIdentifier = 1`, or (systemic fix, not yet done)
80
+ > change the code so only an explicit `0` removes an identifier and `NULL` means "no override".
81
+
82
+ **Build divergence warning.** The `Apis_RecordFields` override feature (reading
83
+ `overrideIsIdentifier`/`overrideChildPolicy`) was introduced 2026-03-20 (api2 commit `624146d`
84
+ "Cache custom fields and API override lookups"). Environments on **older** builds ignore the
85
+ override and keep the base identifier, so a tenant with identical Core metadata + override rows +
86
+ data can behave differently across environments purely by build version. A NULL-`overrideIsIdentifier`
87
+ override row that works on prod (older build) will start throwing EV-12 once prod gets the newer
88
+ build — audit such rows before/with the deploy. First seen in AIG (TRUE-79978): prod resolved,
89
+ beta/QA threw EV-12 on the injected `entitlementFulfillmentType` — see
90
+ [AIG entitlement intake](../../../clients/aig/features/entitlement-intake.md).
91
+
60
92
  ## Gotcha
61
93
 
62
94
  - **Nested write with only a non-identifier field silently creates duplicates.** This is a
@@ -68,6 +100,11 @@ caller first** (e.g. build a `key → uuid` map), then send `{uuid}`.
68
100
 
69
101
  ## Change history
70
102
 
103
+ - 2026-07-09 — Documented the per-API `Apis_RecordFields` override layer (`overrideIsIdentifier`,
104
+ `overrideChildPolicy`) and the trap that a `NULL` `overrideIsIdentifier` is read as "not an
105
+ identifier" (`!$row->overrideIsIdentifier`) — an override row set only for a child policy
106
+ silently strips the field as an identifier → EV-12 at `V2.php:7094`. Added the build-divergence
107
+ warning (override feature added 2026-03-20, commit `624146d`). Surfaced by AIG TRUE-79978. (mhammontree)
71
108
  - 2026-07-07 — Documented that api2 nested-relationship writes match an existing child **only** by
72
109
  identifier (`uuid`); a non-unique custom field is not a match key and forces a new-record insert.
73
110
  Correct pattern is `contact: {uuid}` (resolve business keys to uuid in the caller). Surfaced by
@@ -78,3 +115,5 @@ caller first** (e.g. build a `key → uuid` map), then send `{uuid}`.
78
115
  - [api2 architecture](../architecture.md) — the V2 CRUD engine (`processRoutePairs`).
79
116
  - [Prudential device import + contact linking](../../../clients/prudential/features/device-information-import-and-contact-linking.md)
80
117
  — the production incident that exposed this behavior.
118
+ - [AIG entitlement intake](../../../clients/aig/features/entitlement-intake.md) — the TRUE-79978
119
+ EV-12 that exposed the `Apis_RecordFields` NULL-`overrideIsIdentifier` trap + build divergence.
@@ -5,12 +5,13 @@ project: API
5
5
  client: aig
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-06-19
8
+ updated: 2026-07-09
9
9
  owners: ["mhammontree"]
10
10
  files:
11
11
  - _underscore/Model/Aig/Entitlement.php
12
12
  - dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql
13
13
  related:
14
+ - 2.0/apps/api2/features/nested-relationship-writes.md
14
15
  - 2.0/apps/api2/architecture.md
15
16
  - 2.0/apps/dbchanges2/architecture.md
16
17
  - 2.0/apps/_underscore/architecture.md
@@ -42,7 +43,9 @@ the FK can't resolve and intake fails with **"Missing AIG item ID."** Keeping
42
43
  - reads `partNumber = payload->saleItem->partNumber` and runs
43
44
  `SELECT id FROM Items WHERE partNumber = '{partNumber}' LIMIT 1` to get `$itemId`;
44
45
  - uses `$itemId` to load fulfillment **types** (`Items_EntitlementFulfillmentTypes`) and
45
- **methods** (`Items_EntitlementFulfillmentMethods`) and injects them into the payload.
46
+ **methods** (`Items_EntitlementFulfillmentMethods`) and injects them into the payload as
47
+ **name-only** related objects (e.g. `entitlementFulfillmentType: {name: "Repair"}`). AIG
48
+ never sends `entitlementFulfillmentType`; it is populated entirely by this interceptor.
46
49
  3. The V2 engine resolves `Entitlement.saleItemId` (FK → `_Model_Client_Item`) from that same
47
50
  `saleItem.partNumber`. **No matching `Items` row → unresolvable FK → "Missing AIG item ID".**
48
51
  4. `postPost` then emails the contact a Staples Protection Plan registration link
@@ -118,9 +121,28 @@ this interceptor or use this dual-purpose Items pattern.
118
121
  codes are **not** in the SaleItemID spreadsheet and must be sourced separately if missing.
119
122
  - **No unique key on `Items.partNumber`** — use an anti-join for idempotent loads, not
120
123
  `INSERT IGNORE`.
124
+ - **An `entitlementFulfillmentType` EV-12 VALIDATION error is NOT a bad client payload —
125
+ it's this interceptor's injected field failing to resolve.** AIG never sends that field;
126
+ the interceptor injects it as `{name: "Repair"}`. The error means the V2 resolver could not
127
+ find a searchable identifier for the injected object. Root cause (TRUE-79978) was an
128
+ `Apis_RecordFields` override row that stripped `name` as an identifier — see
129
+ [api2 nested-relationship writes → Per-API overrides](../../../2.0/apps/api2/features/nested-relationship-writes.md).
130
+ - **Empty `saleItem.partNumber` silently resolves to the wrong item.** The lookup is
131
+ `WHERE partNumber = '{$partNumber}' LIMIT 1`; when `partNumber` is `""`, it matches item
132
+ `id=10` ("2YR Tablet") — the first row with an empty part number — which carries a "Repair"
133
+ fulfillment type. So a blank part number does not fail loudly; it injects a spurious
134
+ name-only fulfillment type. The interceptor should guard an empty/failed lookup before
135
+ injecting (not yet fixed).
121
136
 
122
137
  ## Change history
123
138
 
139
+ - 2026-07-09 — TRUE-79978: root-caused an `entitlementFulfillmentType` EV-12 on beta/QA. The
140
+ field is injected (name-only) by `prePost`; the beta `Client_Aig.Apis_RecordFields` override
141
+ row (apiId=2, recordFieldId=1202) had `overrideIsIdentifier=NULL` + `overrideChildPolicy=MATCH_CREATE`,
142
+ which the newer V2 build reads as "not an identifier", stripping `name` → no searchable id →
143
+ EV-12. Unblocked with a manual one-row beta data fix (`SET overrideIsIdentifier=1`); prod runs
144
+ an older build without the override feature so it still resolves. See the api2 feature doc for
145
+ the shared resolver behavior. (mhammontree)
124
146
  - 2026-06-19 — Documented intake flow; loaded 2,200 new SaleItem codes (1,700 STS `ASI-*`/`SM-*`
125
147
  + 500 unique numeric `SA`) into `Client_Aig.Items` via TRUE-79534, fixing "Missing AIG item ID"
126
148
  on the new `ASI-*` scheme. Follow-up to TRUE-79441. (mhammontree)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.293",
3
+ "version": "1.0.295",
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",
@@ -1,16 +1,18 @@
1
1
  ---
2
2
  name: work-ticket
3
- description: Work an APPROVED ClickUp ticket plan end-to-end. Invoke as `/work-ticket <TICKET-ID>` (e.g. `/work-ticket TRUE-79868`) AFTER `/plan-ticket` has pushed an approved plan to the ticket's Pseudocode field. Loads the plan from the ticket's Pseudocode field, primes framework context via /kickoff (self-answered), implements the plan phase-by-phase with TOGA reviewers, then per repo creates a bare ticket-ID branch, commits, pushes, opens a PR, and posts the PR links as a comment on the ticket. Stops after PRs are open for your review — never changes ClickUp ticket status. Trigger on "/work-ticket", "work the ticket", "execute the plan for <ticket>", "build and PR <ticket>", "ship <ticket>".
3
+ description: Work an APPROVED ClickUp ticket plan end-to-end. Invoke as `/work-ticket <TICKET-ID>` (e.g. `/work-ticket TRUE-79868`) AFTER `/plan-ticket` has pushed an approved plan to the ticket's Pseudocode field. Loads the plan from the ticket's Pseudocode field, primes framework context via /kickoff (self-answered), per repo creates and checks out a bare ticket-ID branch from the fresh remote default BEFORE any edits, implements the plan phase-by-phase with TOGA reviewers, then commits, pushes, opens a PR, and posts the PR links as a comment on the ticket. Stops after PRs are open for your review — never changes ClickUp ticket status. Trigger on "/work-ticket", "work the ticket", "execute the plan for <ticket>", "build and PR <ticket>", "ship <ticket>".
4
4
  ---
5
5
 
6
- # work-ticket — approved plan → codebranch → commit → PR → ClickUp GitHub tab
6
+ # work-ticket — approved plan → branchcode → commit → PR → ClickUp GitHub tab
7
7
 
8
8
  The downstream companion to **`/plan-ticket`**. Given a ticket whose plan has already been
9
9
  approved and pushed to its **📝 Pseudocode** field, this skill executes that plan: it primes
10
- context, writes the code, then for **each repo the plan touches** creates a branch named the
11
- **bare ticket id**, commits, pushes, opens a PR, and posts the PR links as a ticket comment.
10
+ context, then for **each repo the plan touches** creates and checks out a branch named the
11
+ **bare ticket id** (cut from the freshly-fetched remote default) **BEFORE writing any code**,
12
+ writes the code on that branch, commits, pushes, opens a PR, and posts the PR links as a
13
+ ticket comment.
12
14
  ClickUp's native GitHub integration *may* surface those PRs in the ticket's **GitHub tab** when
13
- the repo is connected in ClickUp's GitHub settings (see Step 5b — not guaranteed), because the
15
+ the repo is connected in ClickUp's GitHub settings (see Step 6b — not guaranteed), because the
14
16
  ticket id is in the
15
17
  branch name (and PR title/body).
16
18
 
@@ -62,14 +64,52 @@ Pass them as the trailing argument so kickoff goes straight to preflight + primi
62
64
  /kickoff <framework> <layer>, repos: <repos>, client: <client> — execute <TICKET> <title>
63
65
  ```
64
66
 
65
- Carry the `context-primer` briefing (framework rules, gotchas, client variations) into Step 3
67
+ Carry the `context-primer` briefing (framework rules, gotchas, client variations) into Step 4
66
68
  it directly informs how each phase is implemented. Only stop to ask the developer if a kickoff
67
69
  gate answer is genuinely ambiguous after reading the plan header.
68
70
 
69
- ## Step 3 — Execute the plan, phase by phase
71
+ ## Step 3 — Per repo: create & checkout the ticket branch BEFORE any edits
72
+
73
+ **Branch FIRST, edit SECOND — never write a single line while sitting on `_main`/`_production`
74
+ or on another ticket's branch.** Two reasons this ordering is mandatory:
75
+
76
+ 1. **Safety** — if work-in-progress gets committed (by you, a hook, or the developer), it lands
77
+ on the ticket branch, never on a default branch that deploys to prod.
78
+ 2. **Fresh base** — edits are made against the CURRENT remote default, not a stale local
79
+ checkout. (Real incident: a local `_production` was 12 commits behind and the target file had
80
+ changed upstream — editing before branching would have based the PR on stale code and
81
+ silently reverted the upstream changes.)
82
+
83
+ For **each repo in the plan's `Repos:` header**, run inside that repo's path:
84
+
85
+ ```bash
86
+ # 0. Working tree must be clean before starting — surface any stray edits to the developer
87
+ git -C "<repo-path>" status --porcelain
88
+ # 1. Resolve the repo's REAL default branch (e.g. _production for app repos, _main for dbchanges2)
89
+ DEFAULT=$(gh repo view <owner/repo> --json defaultBranchRef -q .defaultBranchRef.name)
90
+ # 2. Fetch so the base is current
91
+ git -C "<repo-path>" fetch origin --quiet
92
+ # 3. Reuse an existing TRUE-XXXX branch if present; otherwise cut a fresh one from origin/<default>
93
+ git -C "<repo-path>" rev-parse --verify TRUE-XXXX 2>/dev/null \
94
+ && git -C "<repo-path>" checkout TRUE-XXXX \
95
+ || git -C "<repo-path>" checkout -b TRUE-XXXX "origin/$DEFAULT"
96
+ ```
97
+
98
+ - Branch name = **bare ticket id** (team convention; what ClickUp's GitHub integration links on).
99
+ - If `git status` shows uncommitted changes from a prior task, **stop and ask the developer**
100
+ before touching that repo — do not stash someone's work silently.
101
+ - If reusing an existing `TRUE-XXXX` branch, still fetch and consider rebasing onto
102
+ `origin/<default>` so the PR diff stays current.
103
+ - **Never branch off / commit to `_main`/`_production` directly**, and **never force-push**
104
+ (per `git-workflow.md`).
105
+
106
+ Only after every in-scope repo is sitting on its ticket branch do you start writing code.
107
+
108
+ ## Step 4 — Execute the plan, phase by phase
70
109
 
71
110
  Implement the plan against the **real repo paths** (resolve each from the `repo-path-<repo>`
72
- memory kickoff established; never guess paths). For non-trivial work drive it as implement
111
+ memory kickoff established; never guess paths) each repo already checked out on its ticket
112
+ branch from Step 3. For non-trivial work drive it as implement →
73
113
  verify with subagents so the conversation stays the conductor:
74
114
 
75
115
  1. For each **phase** in the plan, an implementer subagent writes that phase's code at the
@@ -94,34 +134,13 @@ verify with subagents so the conversation stays the conductor:
94
134
 
95
135
  Do **not** push anything to a remote or open a PR until the code is implemented and clean.
96
136
 
97
- ## Step 4 — Per repo: branch (bare ticket id), commit, push
137
+ ## Step 5 — Per repo: commit & push
98
138
 
99
139
  For **each repo in the plan's `Repos:` header** that has changes, run inside that repo's path:
100
140
 
101
- 1. **Branch bare ticket id, based on the CURRENT remote default.** This matches the team
102
- convention (existing branches are `TRUE-79142`, `TRUE-77219`) and is exactly what ClickUp's
103
- GitHub integration auto-links on. **The branch MUST be cut from freshly-fetched
104
- `origin/<default>` — never from whatever local branch the repo happens to be sitting on.** A
105
- working checkout is frequently parked on a *different, unmerged ticket's branch* (or a stale
106
- local default); branching off that silently bases your PR on someone else's unmerged work and
107
- produces a wrong, conflict-prone diff. So always **fetch first, then branch off the remote
108
- default**, carrying your uncommitted Step 3 changes onto the new branch:
109
- ```bash
110
- # Resolve the repo's REAL default branch (e.g. _production for app repos, _main for dbchanges2)
111
- DEFAULT=$(gh repo view <owner/repo> --json defaultBranchRef -q .defaultBranchRef.name)
112
- git -C "<repo-path>" fetch origin --quiet
113
- # Reuse an existing TRUE-XXXX branch if present; otherwise cut a fresh one from origin/<default>.
114
- git -C "<repo-path>" rev-parse --verify TRUE-XXXX 2>/dev/null \
115
- && git -C "<repo-path>" checkout TRUE-XXXX \
116
- || git -C "<repo-path>" checkout -b TRUE-XXXX "origin/$DEFAULT"
117
- ```
118
- `git checkout -b … origin/$DEFAULT` keeps your uncommitted working-tree edits and re-bases them
119
- onto the latest remote default in one step. **First confirm `git status` shows only the files
120
- your plan changed** (no stray edits from a prior ticket) before branching. If the checkout is
121
- refused because a file you edited also changed on the default, `git stash` → `checkout -b … origin/$DEFAULT`
122
- → `git stash pop` and resolve. **Never branch off / commit to `_main`/`_production` directly**,
123
- and **never force-push** (per `git-workflow.md`). If already on the `TRUE-XXXX` branch, still
124
- `git fetch` and consider rebasing onto `origin/<default>` so the PR diff stays current.
141
+ 1. **Verify the branch.** Confirm `git branch --show-current` is the `TRUE-XXXX` branch from
142
+ Step 3 if the repo somehow ended up back on a default branch, go redo Step 3 for it before
143
+ committing anything.
125
144
  2. **Commit.** Stage only the files the plan changed. Message format `type: short description`
126
145
  (`feat`/`fix`/`refactor`/`docs`/`test`/`chore`, lowercase, present tense, ≤72 chars). Add a
127
146
  body paragraph for substantial changes. **Pre-commit check:** `php -l` passes on changed PHP,
@@ -130,7 +149,7 @@ For **each repo in the plan's `Repos:` header** that has changes, run inside tha
130
149
 
131
150
  Repeat for every in-scope repo — one branch per repo, all named the same bare ticket id.
132
151
 
133
- ## Step 5 — Open a PR per repo (auto-links to the ClickUp GitHub tab)
152
+ ## Step 6 — Open a PR per repo (auto-links to the ClickUp GitHub tab)
134
153
 
135
154
  For each pushed repo, open a PR with `gh` from that repo's path:
136
155
 
@@ -158,7 +177,7 @@ gh pr create --repo <owner/repo> --base <DEFAULT-BRANCH> --head <TICKET> \
158
177
  - End the PR body with the standard footer:
159
178
  `🤖 Generated with [Claude Code](https://claude.com/claude-code)`.
160
179
 
161
- ### Step 5b — Post the PR links as a ClickUp comment (the RELIABLE link — do NOT skip)
180
+ ### Step 6b — Post the PR links as a ClickUp comment (the RELIABLE link — do NOT skip)
162
181
 
163
182
  **The native GitHub↔ClickUp tab is NOT reliable in this workspace.** It only populates if each
164
183
  repo is connected under ClickUp → Settings → Integrations → GitHub (a one-time workspace OAuth
@@ -181,12 +200,12 @@ fetch("https://api.clickup.com/api/v2/task/<TICKET>/comment?custom_task_ids=true
181
200
  (A comment does NOT populate the GitHub *tab* — it lands in the ticket's activity/comments —
182
201
  but it is the dependable way for the developer to reach the PRs from the ticket.)
183
202
 
184
- ## Step 6 — Report & stop (do NOT touch ClickUp status)
203
+ ## Step 7 — Report & stop (do NOT touch ClickUp status)
185
204
 
186
205
  Summarize for the developer:
187
206
  - Ticket title and the repos that got changes.
188
207
  - For each repo: the branch name (`<TICKET>`) and the **PR url** `gh` returned.
189
- - Confirm the Step 5b comment was posted (the reliable link). State plainly that the GitHub
208
+ - Confirm the Step 6b comment was posted (the reliable link). State plainly that the GitHub
190
209
  **tab** populates only if the repos are connected in ClickUp's GitHub integration settings —
191
210
  do not claim it will appear automatically.
192
211
  - **Remind the developer the next step is theirs:** review/approve the PR(s), then **manually
@@ -207,7 +226,7 @@ Offer `/capture` if a durable, KB-worthy finding emerged while implementing.
207
226
  - **Linking is NOT automatic.** The native GitHub tab requires a per-repo connection in ClickUp's
208
227
  GitHub integration settings (workspace OAuth, not doable from `gh`). The branch name + bare-id
209
228
  PR title + `CU-<internalId>` in the body are necessary but **not sufficient** — if the repo
210
- isn't connected, the tab stays empty. Step 5b's comment is the reliable link; always post it.
229
+ isn't connected, the tab stays empty. Step 6b's comment is the reliable link; always post it.
211
230
  - **Autonomy boundary:** autonomous through PR creation; stops there. Moving the ClickUp ticket
212
231
  is always a manual developer action.
213
232
  - Run AFTER `/plan-ticket` has produced and the developer has approved a plan. This skill does