toga-ai 1.0.1024 → 1.0.1025

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.
@@ -14,6 +14,7 @@
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 |
17
+ | [Case status is derived from stage/owner; stage changes pass a reason-note gate](features/case-status-and-stage-gate.md) | `_Model_Client_Case::_status` is DERIVED (never set) and stage writes via api2 are gated by a `case-set-stage` Surface rule; open before writing Cases from code |
17
18
  | [Running 2.0 code from a bare CLI script (bootstrap + transactions)](features/cli-script-bootstrap.md) | Minimum recipe + traps for running real 2.0 framework code (`_Model`/`_Query`/`_Database`) from a bare `php script.php`; open before writing a CLI harness/backf |
18
19
  | [Client Users require a contactId — the postPost backfill interceptor & every creation path](features/client-user-contactid-required.md) | Every `Client_<Tenant>.Users` row must have a non-null `contactId`; the shared `_Model_Client_User::postPost` interceptor guarantees it for api2-created users, |
19
20
  | [_Cloud credentials (WorkloadsRuntime) + S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` credentials (assumes `WorkloadsRuntime`, no IAM keys) and S3 helpers (copy/get/delete/list, per-bucket region, 5 GB single-PUT limit); open before any |
@@ -0,0 +1,46 @@
1
+ ---
2
+ title: "Case status is derived from stage/owner; stage changes pass a reason-note gate"
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-15
10
+ owners: [jcardinal]
11
+ files:
12
+ - _underscore/Model/Client/Case.php
13
+ - _underscore/Model/Client/CaseNote.php
14
+ - _underscore/Model/Client/CaseStage.php
15
+ - _underscore/Model/Client/Ticket.php
16
+ related:
17
+ - ../../api2/features/api-payload-interceptors.md
18
+ - ./note-record-authorship.md
19
+ - ../../../../clients/nycdoe/features/asn-sftp-ingestion-2-0.md
20
+ ---
21
+
22
+ `_Model_Client_Case::_status` is DERIVED (never set) and stage writes via api2 are gated by a `case-set-stage` Surface rule; open before writing Cases from code, an integration, or the API.
23
+
24
+ ## Summary
25
+
26
+ Stages are set; statuses are derived. A case's `_status` slug comes from its stage, else its owner. Moving a case to a blocker stage needs a stored reason note in the same request. Built for TOGa Desk Case Blocker (TRUE-82615, Desk ADR 0144). Shared core: applies to every 2.0 client. **On `_underscore` branch `TRUE-82615` (toga25 line) as of 2026-10-15 — not on `_production`.**
27
+
28
+ ## How it works
29
+
30
+ - **`_status`** (plain `FIELD_SQL`, text slug): stage's linked `CaseStatuses.slug` if `caseStageId` set → else `ownerUserId` set → `in-progress` → else `open`. Same rule as `_Model_Client_Ticket::_status`. Never add `FIELDOPT_SQL_TYPE` (a cast zeroes the slug).
31
+ - **Blocker stages** (`roadblocked`, `on-hold`, `awaiting-user`) are `CaseStages` rows keyed by the new `CaseStages.slug`. Resume = set `caseStageId` to NULL (status falls back to owner/open).
32
+ - **`Client.CaseNotes` / `_Model_Client_CaseNote`**: write-once reason notes (`caseId`, `caseStageId`, `note`). `authorUserId` is stamped in `::save()` from `_Model::getRequestUserId()`, never from the payload. ACL: create + read only.
33
+ - **Set-stage gate**: Case `prePut`/`prePatch` capture the before-stage; `postPut`/`postPatch`/`postPost` read the `case-set-stage` Surface `requiredRule` for the new stage slug and its status slug. If the rule needs a reason, a `CaseNotes` row for this case + stage must have been stored in this request (`CaseNote::countCreatedFor`) — else `_Exception_Validation` (400) and the whole request rolls back. Wired by 5 `Core.ApiPayloadInterceptors` rows on cases (recordId 222).
34
+ - **`Ticket::createCase`** (`{"createCase":{}}` on PUT /tickets/{uuid}) now creates the case with **no stage** (it used to force the Open stage), so status derives.
35
+ - Central ids: `Core.Records` 377 (case-notes); `Core.RecordFields` 2743-2750. SQL: dbchanges2 `Client/2026-10-14b`, `Client/2026-10-14c`, `Core/2026-10-14b`, `Client/2026-10-15a` (case-notes ACL), `Client/2026-10-15b` (cases-stages read ACL).
36
+
37
+ ## Gotchas
38
+
39
+ - **Do not write `_status` or pick an "Open" stage to mean open.** Leave `caseStageId` NULL; status derives.
40
+ - **Direct `_Model` writes skip the gate** (interceptors run only in api2). Nycdoe SFTP `Parse.php` creates cases this way; its cases now read `open` / `in-progress`.
41
+ - **PUT/PATCH a case without a route uuid → 400** — the gate needs `$api->recordUuid`.
42
+ - **Keep Cases FK child policies `MATCH`.** `MATCH_UPDATE` / `UPSERT` / `CREATE` write a case through a parent's FK object and bypass the gate.
43
+ - **The reason must be a stored row, not payload text** — api2 can swallow a failed nested note and still return 200.
44
+
45
+ ## Change history
46
+ - 2026-10-15 — Created: derived case status, blocker stages, CaseNotes, set-stage gate, createCase without stage (TRUE-82615). (jcardinal)
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: [apeterson]
9
+ updated: 2026-10-15
10
+ owners: [apeterson, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/SalesOrderNote.php
13
13
  - _underscore/Model/Client/PurchaseOrderNote.php
@@ -18,6 +18,7 @@ files:
18
18
  - _underscore/Model/Client/PartRequestNote.php
19
19
  related:
20
20
  - ../../toga-blox/features/add-notes-template.md
21
+ - ./case-status-and-stage-gate.md
21
22
  - ../../toga25-supply/features/transfer-orders-page.md
22
23
  - ../features/sales-order-denial-reason.md
23
24
  - ../../../clients/compass-usa/features/approval-decision-flow.md
@@ -62,8 +63,12 @@ Before building anything that shows or filters by note author:
62
63
 
63
64
  - **`contactId` on a `*Notes` model is an unwritten FK — treat it as always NULL.** Seven models, zero writers.
64
65
  - **A notes list UI has no author to show.** Design for it, or the field silently renders blank for every row.
66
+ - **Exception — `CaseNotes` (2026-10-15) has a real author:** `authorUserId` stamped in `_Model_Client_CaseNote::save()` from `_Model::getRequestUserId()`. Copy that pattern for the real fix. See [Case status + stage gate](./case-status-and-stage-gate.md).
65
67
  - **`dtStamp` is `FIELD_DATETIME_CREATED`** — stamped by the ORM on insert, the only provenance a note actually carries.
66
68
 
69
+ ## Change history
70
+ - 2026-10-15 — Noted CaseNotes as the first note model with a server-stamped author. (jcardinal)
71
+
67
72
  ## Related
68
73
 
69
74
  - [Add Notes template](../../toga-blox/features/add-notes-template.md)
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-08
10
- owners: [dfranks]
9
+ updated: 2026-10-15
10
+ owners: [dfranks, jcardinal]
11
11
  files: []
12
12
  related:
13
13
  - ../../../../standalone/apps/claude/workflows/clickup-ticket-workflow.md
@@ -24,7 +24,7 @@ How ClickUp surfaces branches/PRs/commits in a ticket's GitHub tab and the branc
24
24
 
25
25
  - ClickUp's **native GitHub integration** auto-surfaces a branch, PR, or commit in a ticket's GitHub tab **whenever the ticket id appears in the branch name and/or the PR title/body**. Match is on the textual ticket id (e.g. `TRUE-79868`).
26
26
  - **No ClickUp API call creates this link.** Linking is purely a side effect of the ticket id appearing in Git metadata.
27
- - Durable rule for ticket work: name the **git branch as the BARE ticket id** (e.g. `TRUE-79868`), **one branch per repo**, PR title starts with the ticket id (`TRUE-79868 - <subject>`). PR base = the repo's default branch (usually `_production`, but it varies — resolve it with `gh repo view`). Confirmed working 2026-10-08.
27
+ - Durable rule for ticket work: name the **git branch as the BARE ticket id** (e.g. `TRUE-79868`), **one branch per repo**, PR title starts with the ticket id (`TRUE-79868 - <subject>`). PR base is set by the ticket's project — see `rules/common/git-workflow.md` → *Base branch*. Confirmed working 2026-10-08.
28
28
 
29
29
  ## Gotchas
30
30
 
@@ -38,6 +38,7 @@ How ClickUp surfaces branches/PRs/commits in a ticket's GitHub tab and the branc
38
38
 
39
39
  ## Change history
40
40
 
41
+ - 2026-10-15 — PR base now follows the ticket's project (Desk/IQ → `toga25`); links to git-workflow.md instead of restating. (jcardinal)
41
42
  - 2026-10-08 — Added reconnect fix, Development-space repo linking, and settings to leave alone. (jcardinal)
42
43
  - 2026-10-08 — `/work-ticket` and `/plan-ticket` retired; `/finish` opens the PRs. PR title = ticket id first; base = repo default branch; git-workflow now uses bare ticket-id branches. (jcardinal)
43
44
 
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
21
21
 
22
22
  ## 2.0 framework
23
23
 
24
- - **_underscore** (_Underscore) _(framework core)_ — 95 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
24
+ - **_underscore** (_Underscore) _(framework core)_ — 96 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
25
25
  - **worker2** (Worker) — 86 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
26
26
  - **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
27
27
  - **dbchanges2** (Database Changes) _(framework core)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: nycdoe
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-10-06
10
- owners: [mhammontree]
9
+ updated: 2026-10-15
10
+ owners: [mhammontree, jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Sftp/Import.php
13
13
  - worker2/Worker/Sftp/Parse.php
@@ -23,6 +23,7 @@ related:
23
23
  - ../profile.md
24
24
  - ../../../2.0/apps/worker2/features/codeception-integration-tests.md
25
25
  - ../../../2.0/apps/_underscore/features/cli-script-bootstrap.md
26
+ - ../../../2.0/apps/_underscore/features/case-status-and-stage-gate.md
26
27
  ---
27
28
 
28
29
  2.0 rewrite of DOE's vendor-SFTP ASN intake (TRUE-80519 Phases 1–2): per-manufacturer fetch to `Files`, header-validated parse into one Case per customer PO + one ServiceRequest per line; open for any 2.0 DOE ASN/SFTP work.
@@ -112,8 +113,10 @@ PRs open 2026-10-06: library, worker2, dbchanges2, _underscore (worker optional
112
113
  - **`connect()` must not chain the phpseclib exception** — phpseclib `login()` frames carry the first 15 chars of the password, and worker2 stores job traces in `Core.WorkerJobs`. It rethrows a fresh exception with no `previous`.
113
114
  - **Locks and just-committed rows must read the writer** — see the read-host gotcha in [Per-Client Database Connections](../../../2.0/apps/_underscore/features/per-client-database-connections.md).
114
115
  - **Schema drift: `_Model_Client_Case` declares `longDescription` but `Client_Nycdoe.Cases` lacked the column** → any direct-model Case load/save 1054s. Fixed for DOE (Client_Nycdoe schema file). Other tenants' `Cases` may have the same gap — unchecked (ask Rohan).
116
+ - **Parsed Cases get no stage; `_status` is derived** (`open`, or `in-progress` once an owner is set) since TRUE-82615. Direct model writes skip the api2 set-stage gate. Never set a stage to mean "open" — see [Case status + stage gate](../../../2.0/apps/_underscore/features/case-status-and-stage-gate.md).
115
117
 
116
118
  ## Change history
119
+ - 2026-10-15 — Gotcha: parsed Cases now derive status (no stage) after the shared Case model change. (jcardinal)
117
120
  - 2026-10-06 — 9 dbchanges2 files merged into one Client_Nycdoe + one Core file; runbook Part A updated (mhammontree)
118
121
  - 2026-10-06 — Activation/deployment runbook (Part A inert, Part B shadow mode, rollback, failed-file release) (mhammontree)
119
122
  - 2026-10-06 — Gated delete built (RunForFile/DeleteSource, Parse claim + status-outside-transaction fix, retired names, `released` status); open cutover items (mhammontree)
@@ -6,7 +6,7 @@ project: Claude Harness
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-10-13
9
+ updated: 2026-10-15
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - claude/skills/start/SKILL.md
@@ -134,8 +134,9 @@ All tested 2026-10-08:
134
134
  .scss .less .html`, a React app's `src/`, blox components), else unchecks it.
135
135
  - **Branch = bare ticket id**, created by ClickUp or by `/start`'s `create-branch`. A branch
136
136
  named with the ticket id should link to the ticket through the GitHub integration — confirm on
137
- the first real `create-branch` run. PR base = the repo's default branch (usually
138
- `_production`, varies) — resolve with `gh repo view <owner/repo> --json defaultBranchRef`.
137
+ the first real `create-branch` run. Base branch is set by the ticket's project (TOGa Desk / TOGa IQ ticket → every repo from
138
+ `toga25`; else each repo's default branch) — rule lives in `rules/common/git-workflow.md`
139
+ → *Base branch*.
139
140
  - **The KB repo (`toga-tech`) works on `_main`.** No branch check and no PRs there; `/finish`
140
141
  still does the ticket check.
141
142
  - **Re-check the branch, never repeat it.** When the developer says they switched (or before
@@ -157,6 +158,7 @@ All tested 2026-10-08:
157
158
 
158
159
  ## Change history
159
160
 
161
+ - 2026-10-15 — Base branch now follows the ticket's project (Desk/IQ → `toga25`); points at git-workflow.md. (jcardinal)
160
162
  - 2026-10-08 — Created: ClickUp-tied sessions (`/start`, `/commit`, `/pseudocode`, `/finish`)
161
163
  replace `/kickoff`, `/capture`, `/plan-ticket`, `/work-ticket`, `/rework-ticket`. (jcardinal)
162
164
  - 2026-10-08 — Branch claims always re-checked fresh; `/finish` runs `/commit` on yes and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1024",
3
+ "version": "1.0.1025",
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",