toga-ai 1.0.284 → 1.0.286

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,6 +6,7 @@
6
6
  | [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Page.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql |
7
7
  | [Address Validation (carrier waterfall + validateAddress scripted endpoint)](features/address-validation.md) | `_Model_Client_Address::validateAddress` verifies a US address against a **carrier waterfall (USPS → FedEx → UPS)** and returns a single canonical, carrier-norm | _underscore/Model/Client/Address.php |
8
8
  | [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
9
+ | [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
9
10
  | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
10
11
  | [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
11
12
  | [Error Reporting — Issue/Event Aggregation (exceptionHandler)](features/error-reporting-issue-event.md) | `_underscore`'s global exception handler persists every uncaught exception into a two-table **Issue / Event** model in the **shared Core Logs DB** (`_underscore | _underscore/Error.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, dbchanges2/Logs/2026-07-06 - Issue and Event tables.sql |
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-07-02
9
+ updated: 2026-07-07
10
10
  owners: ["jcardinal", "rgirish", "mhammontree"]
11
11
  files:
12
12
  - _underscore/_underscore.php
@@ -119,6 +119,16 @@ multi-tenant client-DB lookup from Core. `Query.php` wraps MySQLi with read/writ
119
119
  auto-routing and result helpers: `fetchRows()`, `fetchRow()`, `fetchOne()`,
120
120
  `fetchRowsAssocArray()`, `fetchKeyValuePairs()`, `fetchValues()`.
121
121
 
122
+ `_Query` also supports a **writes-only async mode** (`isAsync` — 3rd constructor arg /
123
+ `setIsAsync()`): a slow WRITE is dispatched to the Worker tier via `_Worker::runTask()`
124
+ instead of running on the request connection, so a long write cannot hold a request-scoped
125
+ connection open long enough to hit **"MySQL server has gone away."** See
126
+ [features/async-query-execution.md](./features/async-query-execution.md). This rides on the
127
+ **SQS-first** `_Worker::runTask()` enqueue path (below): `runTask()` no longer INSERTs the
128
+ `WorkerJobs` row on the caller's connection — it sends `{uuid, action, parameters}` to the
129
+ worker SQS queue and the worker tier creates/tracks the row on its own fresh connection at
130
+ pickup (see the worker2 architecture doc's SQS-first decision).
131
+
122
132
  ### Physical infrastructure
123
133
 
124
134
  Each **environment** is one or more MySQL clusters. Every cluster has a single
@@ -338,3 +348,4 @@ multi-file UI components (`.php`/`.html`/`.css`/`.js`) invoked as `<_ComponentNa
338
348
  - 2026-06-25 — Added the Surface platform UI presentation/configuration layer (DB-driven UI config replacing `Page::meta()`, CTO-reviewed AGREE-WITH-ADJUSTMENTS) (jcardinal)
339
349
  - 2026-06-29 — Surface made the enforced (un-flagged) presentation path; reserved id blocks renumbered to Records 333–341 / RecordFields 2246–2433 (known seed-vs-provisioned drift). (jcardinal)
340
350
  - 2026-07-02 — Fixed framework-wide `FIELD_STORAGE` write-drop and folder-read bugs in `Model.php`; noted the remaining unconditional-refetch dirty-read follow-up. (mhammontree)
351
+ - 2026-07-07 — Documented `_Query` writes-only async mode (`isAsync` → Worker `Infrastructure/Database/Query`) in the database-architecture section, linked `features/async-query-execution.md`, and noted the SQS-first `_Worker::runTask()` enqueue departure (caller does no MySQL; worker tier creates the row on pickup). (jcardinal)
@@ -0,0 +1,89 @@
1
+ ---
2
+ title: Asynchronous Query Execution (writes-only, via Worker)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-07
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - _underscore/Query.php
13
+ - worker2/Worker/Infrastructure/Database.php
14
+ - worker2/Worker/Team/Transcripts.php
15
+ related:
16
+ - ../architecture.md
17
+ - ./per-client-database-connections.md
18
+ ---
19
+
20
+ ## Summary
21
+
22
+ `_Query` can run a **write** query asynchronously so a long/slow write does not hold a
23
+ request-scoped DB connection open long enough to hit **"MySQL server has gone away"**.
24
+ Instead of executing on the local connection, the query is dispatched as a Worker task
25
+ (`Infrastructure/Database/Query`) that opens its own connection on the target DB, runs the
26
+ write, and commits. It is **writes-only by design** — an async SELECT captures no result set
27
+ and is a harmless no-op.
28
+
29
+ ## Key files / entry points
30
+
31
+ - **`_underscore/Query.php`** — `_Query` gained an `isAsync` flag: constructor 3rd param
32
+ `bool $isAsync = false`, a `setIsAsync()` setter, and an `_isAsync` property. When async,
33
+ `execute()` resolves the target DB connection config and dispatches a Worker task instead of
34
+ running locally.
35
+ - **`worker2/Worker/Infrastructure/Database.php`** — `_Worker_Infrastructure_Database::Query()`
36
+ is the worker-side action that runs the write on the target DB and commits.
37
+ - **`worker2/Worker/Team/Transcripts.php`** — live example caller in `SyncKnowledgeBases()`
38
+ (lines ~911 / ~925).
39
+
40
+ ## How it works
41
+
42
+ 1. **Opt in.** Construct with the third arg true:
43
+ `new _Query($sql, _underscore::DB_TEAM, true);` (or call `setIsAsync(true)` before the
44
+ query is set).
45
+ 2. **Dispatch (in `execute()`).** When `_isAsync` is true, `execute()` does **not** run the
46
+ query locally. It resolves the target DB connection config — from `_Database::$_registers`
47
+ or the `_Config` `DATABASE` group (direct hostname, or `writehost`/`readhost` + `backuphost`
48
+ params) — and calls
49
+ `_Worker::runTask(action: 'Infrastructure/Database/Query', parameters: {hostname, database,
50
+ username, password, query})`.
51
+ 3. **Worker side.** `_Worker_Infrastructure_Database::Query()` calls `_Database::register(...)`
52
+ under the alias `'Query'`, runs `new _Query($query, 'Query')` to execute the write on the
53
+ worker's own connection, then commits with `_Database::transactionCommit('Query')`.
54
+ 4. **Writes only.** Async SELECT captures no result set (fire-and-forget) and is treated as a
55
+ harmless no-op — do not use async for reads.
56
+
57
+ ## Correctness gotchas (all fixed this session — do not regress)
58
+
59
+ - **Constructor order (critical).** `setIsAsync()` must run **before** `setQuery()`, because
60
+ `setQuery()` executes non-SELECT queries immediately. With the original order (setQuery first)
61
+ every async WRITE ran synchronously and the async path was never taken. The constructor was
62
+ reordered to set the async flag first.
63
+ - **Worker-side commit (critical).** `_Database::register()` with an alias auto-starts a lazy
64
+ transaction; the write flips autocommit off and is rolled back when the worker connection
65
+ closes unless explicitly committed. The worker action must call
66
+ `_Database::transactionCommit('Query')` after the write. This is the known _underscore
67
+ [lazy-transaction gotcha](../architecture.md) — the same trap that silently discarded Rate
68
+ SAML user INSERTs.
69
+ - **Double-dispatch.** The async branch leaves `_results` null, and `execute()` is guarded by
70
+ `is_null(_results)`, so `__destruct()`/getters re-fire the job. Fixed by setting a sentinel
71
+ (`_results = true`) at the end of the async branch.
72
+ - **Dangling local transaction.** The original async check ran **after** the
73
+ write-connection/transaction setup, so a dispatching async write opened a local writer
74
+ connection and started a local transaction it never committed. Fixed by moving the async
75
+ dispatch to the **top** of `execute()`, before any read/write connection or transaction setup.
76
+
77
+ ## Known open items
78
+
79
+ - **SECURITY (open, developer aware).** DB username/password are currently sent in **plaintext**
80
+ through the Worker queue payload (SQS + `WorkerJobs` table). Being handled separately: the
81
+ worker should resolve credentials from config by database name rather than receiving them in
82
+ the payload. Do not build on the plaintext-credential path.
83
+
84
+ ## Change history
85
+
86
+ - 2026-07-07 — Built writes-only async query execution (`_Query` `isAsync` → Worker
87
+ `Infrastructure/Database/Query` action); fixed four correctness bugs found in review
88
+ (constructor order, worker-side commit, double-dispatch sentinel, dangling local
89
+ transaction). Credential-in-payload security item left open. (jcardinal)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-06-08
9
+ updated: 2026-07-07
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - worker2/Controller/Index.php
@@ -15,6 +15,7 @@ files:
15
15
  - _underscore/Worker.php
16
16
  related:
17
17
  - ./features/creating-worker-actions.md
18
+ - ../_underscore/features/async-query-execution.md
18
19
  ---
19
20
 
20
21
  ## Summary
@@ -92,8 +93,29 @@ New path (`{workerJobId}` present):
92
93
  5. Call `initialize()` if present, then the action with spread parameters.
93
94
  6. **Check-out:** `UPDATE dtCompleted, executionTime, isSuccess, failureReason`. Return 200.
94
95
 
95
- Legacy path (`action` present, no `workerJobId`): used by `_Worker::runTask()` debug mode
96
- only; runs directly with no `WorkerJobs` tracking.
96
+ Direct-payload path (`action` present, no `workerJobId`): formerly the debug-only "legacy"
97
+ path (untracked, could return HTTP 500). As of 2026-07-07 it is a **first-class tracked
98
+ path** — it is how the SQS-first `_Worker::runTask()` (below) delivers production jobs. The
99
+ message carries `{uuid, action, parameters}`; the worker creates and tracks the `WorkerJobs`
100
+ row on its own fresh connection and **always returns HTTP 200** (no DLQ + 3600s visibility
101
+ timeout means any 500 = poison-message storm):
102
+
103
+ 1. **Dedupe guard.** SELECT `WorkerJobs` by `uuid`. Skip (200) **only if `isSuccess IS NOT
104
+ NULL`** (already completed). A row that exists but is incomplete (prior crash / in-flight)
105
+ is **re-run**, not dropped — mirrors the `workerJobId` guard. (SQS is at-least-once.)
106
+ 2. **New job → INSERT** `(uuid, jobType='ACTION', action, parameters, dtQueued=NOW(),
107
+ dtStarted=NOW(), instanceId)`. A UNIQUE index on `WorkerJobs.uuid` (added in `dbchanges2`)
108
+ backstops the SELECT→INSERT TOCTOU race.
109
+ 3. **INSERT-failure disambiguation** is by **re-resolving the uuid**, not by parsing the
110
+ error code: if the row now exists it was a benign concurrent duplicate (skip 200);
111
+ otherwise the write genuinely failed — log CRITICAL + `\Sentry\captureException`, still
112
+ return 200.
113
+ 4. `dtQueued=NOW()` at insert (not NULL) so JobScheduler's `dtQueued IS NULL` recovery does
114
+ not re-queue these rows; the watchdog still covers them via `dtStarted`.
115
+
116
+ > **Known follow-up (not done):** the ~100-line log-DB-registration + `initialize()` blocks
117
+ > are duplicated between the `workerJobId` path and the direct-payload path — candidate for
118
+ > extraction to shared private helpers.
97
119
 
98
120
  ## Action → class/method routing
99
121
 
@@ -103,9 +125,17 @@ file `Worker/Team/Github.php`.
103
125
 
104
126
  ## Programmatic invocation & manual retry
105
127
 
106
- - **`_Worker::runTask(action, parameters)`** (`_underscore/Worker.php`) — INSERT
107
- `WorkerJobs` **`transactionCommit(DB_CORE)` before SQS** send `{workerJobId}`.
108
- Debug mode posts synchronously with no tracking.
128
+ - **`_Worker::runTask(action, parameters)`** (`_underscore/Worker.php`) — **SQS-first as of
129
+ 2026-07-07:** the production branch does **no MySQL** (no INSERT, no commit). It generates a
130
+ client-side `uuid` and sends `{uuid, action, parameters}` to the worker SQS queue; the worker
131
+ tier creates+tracks the `WorkerJobs` row on pickup (see direct-payload path above). Payload is
132
+ capped at `_Worker::SQS_MAX_MESSAGE_BYTES = 262144` — `runTask()` **throws** if the JSON
133
+ exceeds the SQS 256KB limit (loud failure, not a silent drop); oversized params must be
134
+ offloaded to S3 by the caller. Debug branch unchanged (synchronous POST). **Why:** long-running
135
+ scripts hit "MySQL server has gone away" on the caller's stale Core connection during the old
136
+ INSERT; SQS sends do not depend on the DB connection, so row creation moved off the caller
137
+ entirely. This is the enqueue path used by `_Query` writes-only async mode. See the SQS-first
138
+ decision + trade-off under *Key design decisions*.
109
139
  - **`_Worker_Infrastructure_Worker::Retry($workerJobId)`** — resets the job fields,
110
140
  commits before SQS, sends `{workerJobId}` immediately. Sets `dtQueued=NOW()` (not NULL)
111
141
  so JobScheduler doesn't double-pick it.
@@ -118,17 +148,56 @@ file `Worker/Team/Github.php`.
118
148
  ## Critical transaction pattern
119
149
 
120
150
  **Any INSERT/UPDATE that must be visible to another connection before an SQS message is
121
- delivered must be immediately committed.** Three sites: `Retry()`, `_Worker::runTask()`,
122
- and the `Controller/Index.php` check-in. Reason: the caller's MySQL transaction is still
151
+ delivered must be immediately committed.** Sites: `Retry()` and the `Controller/Index.php`
152
+ check-in. (`_Worker::runTask()` no longer applies as of 2026-07-07 it does no MySQL; the
153
+ worker tier creates the row itself on pickup.) Reason: the caller's MySQL transaction is still
123
154
  open when SQS delivers; the worker reads in a separate connection and, if uncommitted, its
124
155
  SELECT returns nothing and the guard fires ("already processed or not found — skipping").
125
156
 
126
157
  ## Key design decisions
127
158
 
128
- MySQL-first (SQS on success) prevents lost jobs · always HTTP 200 (WorkerJobs tracks
129
- failures, SQS retry unwanted) · 3600s visibility timeout for hour-long jobs · no DLQ
130
- (developer-controlled `Retry()` preferred) · all fallback payloads to S3 (no size split) ·
131
- SQS failure → reset `dtQueued=NULL` (rescheduled next minute) · `maxExecutionTime` on
132
- CronJobs, env var for ACTION jobs · `/Webhook` appended enforces method-name convention ·
133
- API Gateway in front of Lambda (SCP blocks Function URLs) · `workerJobId` lookup keeps SQS
134
- messages tiny · commit before SQS avoids the delivery-before-commit race.
159
+ MySQL-first for the **Lambda/webhook/cron ingestion paths** (SQS on success) prevents lost
160
+ jobs · always HTTP 200 (WorkerJobs tracks failures, SQS retry unwanted) · 3600s visibility
161
+ timeout for hour-long jobs · no DLQ (developer-controlled `Retry()` preferred) · all fallback
162
+ payloads to S3 (no size split) · SQS failure → reset `dtQueued=NULL` (rescheduled next
163
+ minute) · `maxExecutionTime` on CronJobs, env var for ACTION jobs · `/Webhook` appended
164
+ enforces method-name convention · API Gateway in front of Lambda (SCP blocks Function URLs) ·
165
+ `workerJobId` lookup keeps SQS messages tiny · commit before SQS avoids the
166
+ delivery-before-commit race.
167
+
168
+ ### `_Worker::runTask()` is SQS-first, NOT MySQL-first (2026-07-07 — intentional departure)
169
+
170
+ For **programmatic** enqueue via `_Worker::runTask()`, the MySQL-first rule was deliberately
171
+ reversed. Long-running PHP scripts were hitting "MySQL server has gone away" on the caller's
172
+ `WorkerJobs` INSERT because the caller's Core connection had gone **stale** (not because the
173
+ server was dying). Approach chosen (**A — SQS-first**): the caller does no MySQL; it sends
174
+ `{uuid, action, parameters}` to SQS and the worker tier creates+tracks the row on its own
175
+ fresh connection at pickup.
176
+
177
+ **Rejected alternative (CTO-recommended):** Approach C/D — reconnect-on-stale inside
178
+ `_Database`, keeping the durable id-based (MySQL-first) pipeline. The CTO review advised
179
+ **against** Approach A because the failure is a stale PHP connection rather than a dying
180
+ server, and because promoting the debug-only untracked path risked poison-message storms and
181
+ duplicate execution. The developer chose Approach A anyway, **accepting the durability
182
+ trade-off**: a bare SQS-send failure loses the job, and jobs are not visible in `WorkerJobs`
183
+ until worker pickup. The CTO review's known-danger mitigations were then implemented — uuid
184
+ dedupe that re-runs incomplete rows, UNIQUE `uuid` index for the TOCTOU race, uuid
185
+ re-resolution to disambiguate INSERT failures (CRITICAL log + Sentry on genuine failure),
186
+ always-200, and a 256KB payload cap that throws. Record kept so a future reader understands
187
+ the MySQL-first design was departed from **on purpose** here.
188
+
189
+ **Gotchas from this change:**
190
+ - `dtQueued` on these rows now reflects worker pickup/row-creation time, **not** true SQS
191
+ enqueue time — semantic drift for any queue-latency metric.
192
+ - Debug/legacy messages without a `uuid` get a random one, so dedupe does not apply to them —
193
+ an HTTP-layer retry of a debug call could double-run.
194
+
195
+ ## Change history
196
+
197
+ - 2026-06-08 — Initial worker2 architecture doc. (jcardinal)
198
+ - 2026-07-07 — `_Worker::runTask()` moved to SQS-first (caller does no MySQL; worker tier
199
+ creates/tracks the row on pickup); the former debug-only direct-payload path in
200
+ `Controller/Index.php` promoted to a first-class tracked path with uuid dedupe, UNIQUE-index
201
+ TOCTOU backstop, uuid re-resolution on INSERT failure, and a 256KB payload cap. Documented as
202
+ an intentional departure from MySQL-first (CTO recommended against; trade-off accepted).
203
+ Related: `_underscore` `_Query` writes-only async mode. (jcardinal)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-23
9
+ updated: 2026-07-08
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - worker2/Worker/Clickup.php
@@ -69,8 +69,26 @@ None — internal team/sprint tooling, uniform across clients.
69
69
  - **No PHPUnit harness** in worker2. The regression test
70
70
  `Tests/Worker/ClickupWorkTypeTest.php` is a plain-PHP script (reflection on
71
71
  `isStatusComplete`) run with `php Tests/Worker/ClickupWorkTypeTest.php`.
72
+ - **ClickUp dropdown custom-field `value` is the option ORDERINDEX, not its name.** The
73
+ webhook payload for a dropdown/labels custom field delivers the selected option's integer
74
+ `orderindex`, *not* the option's display name. Any name-keyed lookup (e.g.
75
+ `$lookupPreviousSprintWorkTypeByName`) will silently never match if fed the raw `value`.
76
+ Resolve the name first via the field's own `type_config->options` — build a map keyed by
77
+ `orderindex` (`$workTypesByIndex[$opt->orderindex] = $opt`) and read `->name` from it. This
78
+ is the proven pattern in `_Worker_Team_Sprint::SprintLock` (`Worker/Team/Sprint.php`, the
79
+ Work Type resolve ~L3894 and the POST ~L3971). Guard the lookup with
80
+ `isset($workTypesByIndex[$value])` so a stale/deleted option orderindex doesn't warn.
72
81
 
73
82
  ## Change history
83
+ - 2026-07-08 — Fixed **Previous Sprint Work Type** never being set on tasks created after a
84
+ sprint launched (`taskCreated` case, `Worker/Clickup.php` ~L715-751). Root cause: the handler
85
+ assigned `$workType = $customField->value` (the ClickUp option orderindex, an int) then looked
86
+ it up in a name-keyed map, so the `isset()` guard never matched and the POST silently never
87
+ fired. Now resolves orderindex→name via the WORKTYPE field's `type_config->options`
88
+ (`$workTypesByIndex`), matching the `SprintLock` convention, with an added
89
+ `isset($workTypesByIndex[$value])` guard. Also added: empty Work Type now defaults to
90
+ `Unplanned` before the Previous Sprint Work Type lookup so newly-created tasks still get a
91
+ value. `php -l` clean; php-reviewer approved. (jcardinal)
74
92
  - 2026-06-23 — Fixed dependents staying Conditional after a blocker completed: completion check
75
93
  now treats `closed` as terminal alongside `done`, and `taskStatusUpdated` re-evaluates
76
94
  dependents (gated to terminal statuses). Added `COMPLETE_STATUS_TYPES`, `isStatusComplete()`,
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
17
17
 
18
18
  ## 2.0 framework
19
19
 
20
- - **_underscore** (_Underscore) _(framework core)_ — 24 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
20
+ - **_underscore** (_Underscore) _(framework core)_ — 25 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
21
  - **worker2** (Worker) — 27 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
22
  - **api2** (API) — 10 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
23
23
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.284",
3
+ "version": "1.0.286",
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",