toga-ai 1.0.97 → 1.0.99

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.
@@ -5,3 +5,4 @@
5
5
  | [Worker (1.0 Framework) Architecture](architecture.md) | `worker` is the legacy (**1.0** `App_` framework) **background-job tier**. | worker/index.php, worker/_/app/framework.php, worker/crons/, worker/schedules/, worker/ebs/cron.worker.php, worker/.ebextensions/035_cron.worker.config |
6
6
  | [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/reconcile_netsuite_totals.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
7
7
  | [Prudential: Send Shipments for the Day report (daily cron)](features/send-shipments-for-the-day.md) | Daily cron (9:00 PM) that emails Prudential and Dell stakeholders an Excel report of all devices shipped that day, including tracking number, serial number, emp | worker/crons/notifications/reports/send_shipments_for_the_day.php |
8
+ | [Onboarding a Client to the NetSuite TOGa Supply Sync](workflows/onboarding-client-to-netsuite-togasupply-sync.md) | How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer (`worker/crons/toga2/netsuite/`). | worker/crons/toga2/netsuite/sync_togasupply.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql |
@@ -37,6 +37,11 @@ which **self-elects a distinct role** (`notification`, `database`, `infrastructu
37
37
  > than run inside a web request. It builds on the `library` core (see
38
38
  > [Library architecture](../library/architecture.md)).
39
39
 
40
+ **Critical rules:** all classes use the `App_` prefix (1.0 framework). CRON scripts are plain PHP
41
+ under `crons/`; the schedule (cron registration) is the source of truth for what runs — a script
42
+ that isn't scheduled never executes. Client integrations live under `crons/toga2/<client>/`.
43
+ Use prepared statements for all SQL; never interpolate input.
44
+
40
45
  ## How a job becomes a cron (the dispatch pipeline)
41
46
 
42
47
  There is **no in-process scheduler**. Jobs are plain PHP files invoked by the OS `crontab`,
@@ -0,0 +1,116 @@
1
+ ---
2
+ title: Onboarding a Client to the NetSuite TOGa Supply Sync
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-06-16
10
+ owners: ["dfranks"]
11
+ files:
12
+ - worker/crons/toga2/netsuite/sync_togasupply.php
13
+ - worker/crons/toga2/netsuite/common_sync_togasupply.php
14
+ - worker/schedules/cron.worker.sync.json
15
+ - dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql
16
+ related: []
17
+ ---
18
+
19
+ ## Summary
20
+ How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer
21
+ (`worker/crons/toga2/netsuite/`). Each client gets a thin wrapper cron that sets its
22
+ `CLIENT_CONFIGURATION` + `IS_ENABLED_*` flags and then `require_once`s the shared
23
+ `common_sync_togasupply.php`, which builds all lookups and runs the 6 sync sections
24
+ (Sales Orders, Purchase Orders, Invoices, Item Receipts, Item Fulfillments, Inventory
25
+ Adjustments). Reads come from production NetSuite (REST/SuiteQL); writes go to the TOGa2
26
+ API (`App_Api_Toga2::send`), which lands in that client's `Client_<Name>` database.
27
+
28
+ ## Steps
29
+ 1. **Create the wrapper** `worker/crons/toga2/netsuite/sync_togasupply_<client>.php`,
30
+ modeled exactly on an existing one (e.g. `sync_togasupply_compass_usa.php`). Set
31
+ `CLIENT_CONFIGURATION` (`client`, `isParentCustomer`, `api`, `secret`,
32
+ `importCustomerPurchaseOrdersOnVendor`) and the constants. Shared constants are the same
33
+ across clients: `NETSUITE_CUSTOM_FIELD_ID__END_USER_CUSTOMER = 3149`,
34
+ `SHIPPING_LOCATION_TYPE_UUID = 527a3995-…`, `WAREHOUSE_LOCATION_TYPE_UUID = 78c24f51-…`,
35
+ and `importCustomerPurchaseOrdersOnVendor = 10ed18ec-0999-53d9-f9db-5892d90f09d4`.
36
+ Get `client` from `Core.Clients` (by name); get `api`/`secret` from
37
+ `Client_<Name>.Apis`. Lint with `C:\xampp7\php\php.exe -l` (prod is PHP 7.2).
38
+ 2. **Determine `isParentCustomer` by probing NetSuite — do not assume.** It controls whether
39
+ the common body calls `listChildCustomers()` (parent) or `fetchCustomerById()` (flat).
40
+ `listChildCustomers(N)` runs `SELECT id FROM customer WHERE parent = N`. If the client's
41
+ NetSuite customer has **zero children**, `isParentCustomer` MUST be `false`, or the sync
42
+ builds an empty customer list, registers nothing, and imports nothing (silent no-op). See
43
+ Gotchas.
44
+ 3. **Seed the Parameters table** (see the dedicated section below) — REQUIRED or the sync
45
+ aborts on its first parameter read.
46
+ 4. **Add the schedule entry** in `worker/schedules/cron.worker.sync.json`: all
47
+ `sync_togasupply_*` jobs run `*/5 * * * *`, `active: 1`. The cron only runs once a
48
+ schedule entry references the file and the worker is redeployed.
49
+ 5. **Deploy order matters:** the Parameters seed must hit prod `Client_<Name>` **before or
50
+ with** the cron going live, or the sync 404-aborts (and throws a Sentry error) every 5
51
+ minutes.
52
+
53
+ ## Parameters seed (the required, easily-missed step)
54
+ The sync reads/writes per-client sync state via the TOGa2 API `/parameters` endpoint, which
55
+ is **update-only** — a `GET`/`PUT` on a non-existent key returns `404 EV-6`, and because
56
+ `App_Api_Toga2::send()` defaults `$throwExceptionOnApiError = true`, that 404 **throws and
57
+ aborts the entire run** before any record is processed. The `startModeIteration()` ternary
58
+ (`isSuccess ? value : MIN_DATETIME`) looks like it tolerates a missing key, but it never
59
+ gets there because `send()` throws first. Every live client (e.g. Compass) was seeded at
60
+ onboarding; a new client has none.
61
+
62
+ The `Parameters` *table* already exists in every `Client_<Name>` DB — you only seed **rows**.
63
+ Add a `dbchanges2/Client_<Name>/<date> - NetsuiteSyncParameters.sql` migration modeled on
64
+ `dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql`, inserting all **12** keys:
65
+ - 6 × `NETSUITE_LAST_SYNC_DATETIME_{SALES_ORDERS,PURCHASE_ORDERS,INVOICES,ITEM_RECEIPTS,ITEM_FULFILLMENTS,INVENTORY_ADJUSTMENTS}`
66
+ → a start datetime. This sets how far back the initial backfill pulls (5-day windows, every
67
+ 5 min). Pick deliberately: `2018-01-01` = full history; a recent date = go-forward only.
68
+ - 6 × `NETSUITE_EXECUTION_MODE_*` → `864000-IDLE` (the interval is clamped to
69
+ `MAX_TIME_WINDOW_TO_FETCH_FROM_NETSUITE_SECONDS = 432000` on first run, so the exact
70
+ seed interval is not critical; `IDLE` is what matters).
71
+
72
+ Use `(UUID(), '<key>', '<value>')` rows — `Parameters.uuid` is required and unique.
73
+ dbchanges2 files use the unqualified `Parameters` table name (deployer selects the DB from
74
+ the directory; no `USE`).
75
+
76
+ ## Systems involved
77
+ - `worker` cron tier (1.0) — the wrapper + `common_sync_togasupply.php`.
78
+ - Production NetSuite (account `1095849`) via `App_Api_Netsuite_Rest` (SuiteQL/REST) — reads.
79
+ - TOGa2 API (`App_Api_Toga2`) — writes to `Client_<Name>` (+ logs to `Logs_<Name>`).
80
+ - `dbchanges2/Client_<Name>/` — the Parameters seed migration.
81
+
82
+ ## Local testing
83
+ - The worker's `[api] _` and api2's `[database]/[databaseClient]/[database1]` on the dev
84
+ laptop point at local (`http://api2/v2` → local Apache/api2 → localhost MySQL). NetSuite is
85
+ the **production** account but reads are read-only.
86
+ - A local end-to-end run needs the client's schemas present locally: import both
87
+ `Client_<Name>` AND `Logs_<Name>` from the read replicas (client2 / logs clusters) into
88
+ local XAMPP. `Logs_<Name>` only needs the **schema** (`mysqldump --no-data`) — its API-log
89
+ history can be many GB; api2 connects to it (link `ClientLogs`) on every request and 500s
90
+ with "Unknown database 'logs_<name>'" if absent.
91
+ - Run the cron in place with `C:\xampp8\php\php.exe` from the `worker/` dir; the autoloader
92
+ walks up to `worker/_` for `__APPROOT__`. `App_Api_Netsuite_Rest::authenticate()` +
93
+ `App_ApiTransaction->setLogging(false)` is the laptop-safe NetSuite pattern (see
94
+ `test/@dave/nsq.php`, a read-only SuiteQL runner).
95
+ - Verified behavior: empty window → 0 writes, checkpoint advances (clean no-op); record
96
+ already present → `PUT` update (idempotent, keyed on `c_netsuiteInternalSalesOrderId`, no
97
+ dupes); record absent → `POST` insert. Checkpoints (`NETSUITE_LAST_SYNC_DATETIME_*`)
98
+ advance by one window and `NETSUITE_EXECUTION_MODE_*` flips back to `…-IDLE` when a section
99
+ finishes cleanly.
100
+
101
+ ## Edge cases & escalation
102
+ - **`isParentCustomer` wrong** → silent no-op (true on a childless customer) or missed
103
+ end-user matching. Always probe NetSuite first.
104
+ - **Parameters not seeded in prod** → sync throws every 5 min; surfaces as a `worker1` Sentry
105
+ error and zero imports.
106
+ - **Missing catalog item** on an incoming order may throw mid-run depending on the section;
107
+ the checkpoint does not advance past a throwing window, so it self-heals once the dependency
108
+ exists.
109
+
110
+ ## Change history
111
+ - 2026-06-16 — Documented the onboarding process after adding Quad (TRUE-79575): wrapper +
112
+ schedule + the required 12-key Parameters seed; captured the `isParentCustomer` NetSuite
113
+ probe and the local Client_/Logs_ schema requirement. (dfranks)
114
+
115
+ ## Related docs
116
+ - [Forecast2 NetSuite reconciliation](../features/forecast2-netsuite-reconciliation.md)
@@ -1,6 +1,23 @@
1
+ ---
2
+ title: "Framework 1.0 (App_) Rules"
3
+ framework: "1.0"
4
+ project: Library
5
+ client: shared
6
+ type: standard
7
+ status: active
8
+ updated: 2026-06-16
9
+ owners: [jcardinal]
10
+ files: []
11
+ related:
12
+ - ../apps/library/architecture.md
13
+ - ../apps/worker/architecture.md
14
+ ---
15
+
1
16
  # Framework 1.0 (App_) Rules
2
17
 
3
- These rules apply to all repos under `C:\WWW\1.0`: `library` (core) and `worker` (app).
18
+ These rules apply to all 1.0 repos: `library` (core) and `worker` (app). They are loaded
19
+ on-demand by `/kickoff` when a 1.0 repo is in scope (not always-on) — see the rules-scoping
20
+ note in `CONVENTIONS.md`.
4
21
 
5
22
  ## Class naming
6
23
 
@@ -28,6 +28,11 @@ configuration, ACL, and third-party integrations. Every 2.0 project depends on i
28
28
  Minimum PHP 8.1 (enforced in `_underscore.php`). The current architecture is
29
29
  **decoupled**: React handles the frontend; `_underscore` serves as the backend API layer.
30
30
 
31
+ **Critical rules:** framework classes use a leading-underscore prefix (`_Model_*`, `_Controller_*`,
32
+ `_Worker_*`). Always use parameterized queries / the `_Db` layer — never interpolate input into SQL.
33
+ Beware the **lazy-transaction gotcha**: writes issued outside an explicitly committed transaction
34
+ can be silently dropped — confirm commit semantics before relying on a write.
35
+
31
36
  ## Entry point & boot sequence
32
37
 
33
38
  Every 2.0 project's `index.php` is just `<?php require '_underscore.php';`.
@@ -31,6 +31,10 @@ Runtime: PHP 8.2+, AWS Elastic Beanstalk (Apache/httpd → PHP-FPM). The `_under
31
31
  framework is **pulled at deploy, not vendored**. Composer deps: `sentry/sentry`,
32
32
  `aws/aws-sdk-php`, `robrichards/xmlseclibs`, `phpmailer/phpmailer`.
33
33
 
34
+ **Critical rules:** every action method must return the exact envelope
35
+ `['success' => bool, 'data' => mixed, 'errors' => array]` — never a raw string or bare array.
36
+ On error: `success=false`, `data=null`, `errors=[...]`. Deviating breaks API consumers silently.
37
+
34
38
  ## Dependencies
35
39
 
36
40
  - **`_underscore` (framework core)** — this API is essentially a metadata-driven layer over
@@ -33,6 +33,11 @@ Because execution order is purely alphabetical, **file naming is the contract.**
33
33
  named with a leading **`YYYY-MM-DD`** date so alphabetical sort = chronological order, which
34
34
  is the intended execution order.
35
35
 
36
+ **Critical rules:** every new `.sql` file **must** be named `YYYY-MM-DD<letter> - <Description>.sql`
37
+ with a **mandatory lower-case letter** right after the date (`a` for the first file of that day
38
+ in a folder, then `b`, `c`, …). One folder per database; place client changes under the matching
39
+ `Client_<Name>` folder. Never edit an already-run migration — add a new dated file instead.
40
+
36
41
  ## File naming convention (the execution contract)
37
42
 
38
43
  1. **Date prefix, ISO format:** every file starts `YYYY-MM-DD`. ISO ordering means
@@ -1,6 +1,24 @@
1
+ ---
2
+ title: "Framework 2.0 (_underscore) Rules"
3
+ framework: "2.0"
4
+ project: _Underscore
5
+ client: shared
6
+ type: standard
7
+ status: active
8
+ updated: 2026-06-16
9
+ owners: [jcardinal]
10
+ files: []
11
+ related:
12
+ - ../apps/_underscore/architecture.md
13
+ - ../apps/worker2/architecture.md
14
+ - ../apps/api2/architecture.md
15
+ ---
16
+
1
17
  # Framework 2.0 (_underscore) Rules
2
18
 
3
- These rules apply to all repos under `C:\WWW\2.0`: `_underscore` (core), `worker2` (app), and `api2` (app).
19
+ These rules apply to all 2.0 repos: `_underscore` (core), `worker2`, and `api2`. They are
20
+ loaded on-demand by `/kickoff` when a 2.0 repo is in scope (not always-on) — see the
21
+ rules-scoping note in `CONVENTIONS.md`.
4
22
 
5
23
  ## Class naming
6
24
 
@@ -1,8 +1,24 @@
1
+ ---
2
+ title: "Python Rules"
3
+ framework: "2.0"
4
+ project: Talos
5
+ client: shared
6
+ type: standard
7
+ status: active
8
+ updated: 2026-06-16
9
+ owners: [jcardinal]
10
+ files: []
11
+ related:
12
+ - ../apps/talos/architecture.md
13
+ - ../apps/voice-to-voice/architecture.md
14
+ ---
15
+
1
16
  # Python Rules (applies to all TOGA Python repos)
2
17
 
3
18
  Applies to every TOGA Python project: **talos** (`aegra-api`, `react_agent`,
4
- `mcp-servers/*`) and **voice-to-voice** (`core/`, `clients/*`,
5
- `lambda_updated/`). Future Python repos inherit these by default.
19
+ `mcp-servers/*`) and **voice-to-voice** (`core/`, `clients/*`, `lambda_updated/`). Future
20
+ Python repos inherit these by default. Loaded on-demand by `/kickoff` only when a Python repo
21
+ (registry `language: python`) is in scope — see the rules-scoping note in `CONVENTIONS.md`.
6
22
 
7
23
  ## Toolchain
8
24
 
@@ -168,6 +168,37 @@ those three doc types fails the build (and therefore blocks `capture`'s publish)
168
168
  `architecture`, and `standard` docs are exempt. Keep the list terse and cap it at ~10 entries,
169
169
  folding older ones away.
170
170
 
171
+ ## Rules scoping (universal always-on vs framework on-demand)
172
+
173
+ `rules/` carries only **universal** always-on rules (security, coding-style, git-workflow,
174
+ testing) — installed to `.claude/rules/toga/common/` and loaded every session.
175
+
176
+ **Framework-specific** rules live in the knowledge base as `type: standard` docs and load
177
+ **on-demand via `/kickoff`** for the framework in scope:
178
+
179
+ - `knowledge/1.0/standards/framework-rules.md` — 1.0 `App_` conventions (loads when a 1.0 repo is chosen)
180
+ - `knowledge/2.0/standards/framework-rules.md` — 2.0 `_underscore` conventions (loads when a 2.0 repo is chosen)
181
+ - `knowledge/2.0/standards/python.md` — Python rules (loads only when a repo with registry
182
+ `language: python` — e.g. `talos`, `voice-to-voice` — is in scope)
183
+
184
+ `kickoff-preflight` emits these in its `standards` list; kickoff reads them like any other
185
+ standard. This keeps a session from carrying rules for a framework it isn't working in. When
186
+ adding a Python repo, set `"language": "python"` on its `registry.json` entry so its rules load.
187
+
188
+ ## Architecture `## Summary` — must carry "Critical rules"
189
+
190
+ Every `architecture.md` opens with a `## Summary` section. Kickoff loads this summary (not the
191
+ full doc) for any repo pulled in as **awareness only** — e.g. a client-app the developer didn't
192
+ explicitly name (see [Client app-scope](#client-app-scope-apps-on-profilemd)). So the summary is
193
+ often the *only* thing a session sees for that repo.
194
+
195
+ Therefore each `## Summary` must end with a **`**Critical rules:**`** line capturing the few
196
+ load-bearing, must-not-violate rules for that repo — the things that would cause a real defect if
197
+ missed even when the full doc is never opened. Examples: dbchanges2's mandatory `YYYY-MM-DD<letter>`
198
+ filename suffix; api2's exact `{success,data,errors}` response envelope; `_underscore`'s
199
+ lazy-transaction write-drop gotcha. Keep it to 2–4 sentences. This is what lets summaries stay cheap
200
+ without losing safety.
201
+
171
202
  ## INDEX.md files
172
203
 
173
204
  Never hand-edited. `node knowledge.js index` regenerates the master `INDEX.md`, each
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
5
5
  ## 1.0 framework
6
6
 
7
7
  - **library** (Library) _(framework core)_ — 4 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 4 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
8
+ - **worker** (Worker) — 5 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **togadesk** (TOGa Desk) — 7 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
10
10
  - **togaview** (TOGa View) — 5 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
11
11
  - **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
@@ -14,8 +14,8 @@
14
14
  { "repo": "togatech", "project": "TOGA Technology Website", "framework": "standalone", "role": "app", "dependsOn": [] },
15
15
  { "repo": "webhook", "project": "Webhook", "framework": "1.0", "role": "app", "dependsOn": ["library"] },
16
16
  { "repo": "walmarttechservices", "project": "Walmart Tech Services", "framework": "1.0", "role": "app", "dependsOn": ["library"] },
17
- { "repo": "talos", "project": "TOGa IQ", "framework": "2.0", "role": "app", "dependsOn": [] },
17
+ { "repo": "talos", "project": "TOGa IQ", "framework": "2.0", "role": "app", "dependsOn": [], "language": "python" },
18
18
  { "repo": "test", "project": "Test", "framework": "1.0", "role": "app", "dependsOn": ["library"] },
19
- { "repo": "voice-to-voice", "project": "TOGa Voice", "framework": "2.0", "role": "app", "dependsOn": [] },
19
+ { "repo": "voice-to-voice", "project": "TOGa Voice", "framework": "2.0", "role": "app", "dependsOn": [], "language": "python" },
20
20
  { "repo": "ai-bdr", "project": "AI-BDR", "framework": "2.0", "role": "app", "dependsOn": [] }
21
21
  ]
package/knowledge.js CHANGED
@@ -213,22 +213,34 @@ function cmdPreflight(args) {
213
213
  clientApps = (Array.isArray(data.apps) ? data.apps : []).filter(r => registry.find(x => x.repo === r));
214
214
  }
215
215
  }
216
- const chosen = [...new Set([...devChosen, ...clientApps])];
217
- if (!chosen.length) {
216
+ // Dev-named repos load FULL (deep work). Client apps load as SUMMARIES only
217
+ // (awareness) unless the dev also explicitly named them — this keeps a client
218
+ // kickoff cheap: a teammate who only mentions "Compass" gets a one-paragraph
219
+ // summary of every client app (incl. its critical rules) without paying the
220
+ // full-architecture cost of repos they aren't touching. (P1)
221
+ const chosen = [...new Set(devChosen)];
222
+ const scope = [...new Set([...chosen, ...clientApps])]; // full ∪ awareness
223
+ if (!scope.length) {
218
224
  console.log(JSON.stringify({ error: 'PREFLIGHT: provide --repos=<repo,repo> and/or --client=<slug> whose profile lists apps' }));
219
225
  process.exitCode = 1;
220
226
  return;
221
227
  }
222
228
  const layer = String(args.layer || 'hybrid').toLowerCase();
223
229
  const q = args.q ? String(args.q).toLowerCase() : null;
224
- const unknown = chosen.filter(r => !registry.find(x => x.repo === r));
230
+ const unknown = scope.filter(r => !registry.find(x => x.repo === r));
225
231
 
226
- // frameworks involved (from the chosen repos)
232
+ // Standards load per framework the dev is ACTUALLY working in (chosen). If the
233
+ // dev named nothing (client-only kickoff), fall back to every in-scope framework.
227
234
  const fwSet = new Set();
228
- for (const r of chosen) { const e = registry.find(x => x.repo === r); if (e) fwSet.add(e.framework); }
235
+ const fwSource = chosen.length ? chosen : scope;
236
+ for (const r of fwSource) { const e = registry.find(x => x.repo === r); if (e) fwSet.add(e.framework); }
229
237
  const frameworks = [...fwSet];
230
238
 
231
- // dependency load-set: framework core(s) first, then transitive dependsOn, then chosen
239
+ // dependency load-set: framework core(s) first, then transitive dependsOn, then
240
+ // everything in scope (chosen full + client-app awareness, both as load-set entries;
241
+ // only `chosen` get full reads below — the rest are summarized).
242
+ const scopeFwSet = new Set();
243
+ for (const r of scope) { const e = registry.find(x => x.repo === r); if (e) scopeFwSet.add(e.framework); }
232
244
  const seen = new Set();
233
245
  const loadSet = [];
234
246
  const visit = (r) => {
@@ -237,8 +249,8 @@ function cmdPreflight(args) {
237
249
  for (const d of (e.dependsOn || [])) visit(d);
238
250
  if (!seen.has(r)) { seen.add(r); loadSet.push(r); }
239
251
  };
240
- for (const fw of frameworks) for (const core of coreReposFor(registry, fw)) visit(core);
241
- for (const r of chosen) visit(r);
252
+ for (const fw of scopeFwSet) for (const core of coreReposFor(registry, fw)) visit(core);
253
+ for (const r of scope) visit(r);
242
254
 
243
255
  const docs = allDocs();
244
256
  const chosenSet = new Set(chosen);
@@ -277,6 +289,21 @@ function cmdPreflight(args) {
277
289
  for (const fw of frameworks) {
278
290
  if (wantBackend) standards.push({ ...fileInfo(`${fw}/standards/backend-php.md`), framework: fw });
279
291
  if (wantFrontend) standards.push({ ...fileInfo(`${fw}/standards/frontend.md`), framework: fw });
292
+ // Framework-specific convention rules (App_ / _underscore). These used to load
293
+ // always-on for every session; they now ride with the framework's standards so a
294
+ // session only carries the rules for the framework it is actually working in. (P2)
295
+ if (wantBackend) {
296
+ const fr = fileInfo(`${fw}/standards/framework-rules.md`);
297
+ if (fr.exists) standards.push({ ...fr, framework: fw });
298
+ }
299
+ }
300
+ // Python rules load only when a Python repo (registry `language: python`) is in scope —
301
+ // not for every 2.0 PHP session. (P2)
302
+ const pythonRepos = scope.filter(r => { const e = registry.find(x => x.repo === r); return e && e.language === 'python'; });
303
+ if (wantBackend && pythonRepos.length) {
304
+ const pe = registry.find(x => x.repo === pythonRepos[0]);
305
+ const py = fileInfo(`${pe.framework}/standards/python.md`);
306
+ if (py.exists) standards.push({ ...py, framework: pe.framework, language: 'python' });
280
307
  }
281
308
 
282
309
  // client docs (profile + features/workflows filtered to involved frameworks)
@@ -324,7 +351,28 @@ function cmdPreflight(args) {
324
351
  // profile.md `apps:` (vs. named by the developer) so it can show "loaded because you
325
352
  // mentioned <client>" and the dev can still add/remove.
326
353
  const clientScope = { client: slug, fromClient: clientApps, fromDev: devChosen };
327
- console.log(JSON.stringify({ frameworks, loadSet, unknown, clientScope, repos: repoOut, standards, client, reads }));
354
+
355
+ // Load-size estimate (P6): rough word/token cost of everything kickoff will read,
356
+ // so the skill can warn when a scope is unusually heavy (e.g. too many full repos)
357
+ // and the dev can narrow `--repos`. Full reads count the doc body; lazy summaries
358
+ // count only the inlined summary text. ~1.3 tokens/word is a coarse heuristic.
359
+ const wc = s => (String(s || '').trim().match(/\S+/g) || []).length;
360
+ const docByRel = new Map(docs.map(d => [d.rel, d]));
361
+ let fullWords = 0, summaryWords = 0;
362
+ for (const r of reads) {
363
+ if (!r.exists) continue;
364
+ if (r.lazy) { summaryWords += wc(r.summary); continue; }
365
+ const d = docByRel.get(r.path);
366
+ if (d) fullWords += wc(d.body);
367
+ }
368
+ const words = fullWords + summaryWords;
369
+ const estimate = {
370
+ words, tokensApprox: Math.round(words * 1.3),
371
+ fullReads: reads.filter(r => r.exists && !r.lazy).length,
372
+ summaries: reads.filter(r => r.exists && r.lazy).length,
373
+ heavy: words > 8000, // kickoff should suggest narrowing --repos above this
374
+ };
375
+ console.log(JSON.stringify({ frameworks, loadSet, unknown, clientScope, estimate, repos: repoOut, standards, client, reads }));
328
376
  }
329
377
 
330
378
  /* ------------------------------------------------------------------ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.97",
3
+ "version": "1.0.99",
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",
package/rules/README.md CHANGED
@@ -1,26 +1,34 @@
1
1
  # Rules Directory
2
2
 
3
- Rules are **always-follow guidelines** that are loaded automatically into every Claude Code session. Unlike skills (which are on-demand slash commands), rules are passive context — Claude reads them and applies them without being asked.
3
+ Rules here are the **universal, always-on guidelines** loaded automatically into every Claude Code session. Unlike skills (on-demand slash commands), rules are passive context — Claude reads them and applies them without being asked.
4
+
5
+ > **Scoping (P2):** only **universal** rules live here and load always-on. **Framework-specific**
6
+ > rules (1.0 `App_`, 2.0 `_underscore`, Python) were moved into the knowledge base under
7
+ > `knowledge/<fw>/standards/framework-rules.md` (and `2.0/standards/python.md`) and are loaded
8
+ > **on-demand by `/kickoff`** for the framework actually in scope. This keeps every session from
9
+ > carrying rules for frameworks it isn't touching. See `knowledge/CONVENTIONS.md`.
4
10
 
5
11
  ## What belongs here
6
12
 
7
- Rules files contain direct imperatives: coding style, security requirements, git workflow, framework conventions. They do not contain architecture documentation (that goes in `knowledge/`) or how-to instructions (that goes in `skills/`).
13
+ Only **language- and framework-agnostic** imperatives: coding style, security, git workflow,
14
+ testing. Framework-specific conventions belong in `knowledge/<fw>/standards/`. Architecture
15
+ documentation goes in `knowledge/`; how-to instructions go in `skills/`.
8
16
 
9
17
  ## Directory structure
10
18
 
11
19
  ```
12
20
  rules/
13
- ├── README.md ← this file
14
- ├── common/ ← apply to all repos and both frameworks
15
- ├── coding-style.md ← language-agnostic style rules
16
- ├── git-workflow.md ← branching, commit messages, PR rules
17
- ├── security.md ← security rules (critical — always followed)
18
- └── testing.md ← test requirements
19
- └── php/ ← PHP-specific rules
20
- ├── app-framework.md ← 1.0 App_ framework rules
21
- └── underscore-framework.md ← 2.0 _underscore framework rules
21
+ ├── README.md ← this file (not installed)
22
+ └── common/ ← UNIVERSAL — apply to all repos, all frameworks (always-on)
23
+ ├── coding-style.md ← language-agnostic style rules
24
+ ├── git-workflow.md ← branching, commit messages, PR rules
25
+ ├── security.md ← security rules (critical — always followed)
26
+ └── testing.md ← test requirements
22
27
  ```
23
28
 
29
+ Framework-specific rules are NOT here — see `knowledge/1.0/standards/framework-rules.md`,
30
+ `knowledge/2.0/standards/framework-rules.md`, and `knowledge/2.0/standards/python.md`.
31
+
24
32
  ## How to install
25
33
 
26
34
  Run from the team knowledge repo root:
@@ -29,7 +37,8 @@ Run from the team knowledge repo root:
29
37
  node scripts/install.js
30
38
  ```
31
39
 
32
- This copies all files from `rules/` into `.claude/rules/toga/` in the target project. Claude Code automatically loads rules from `.claude/rules/`.
40
+ This copies `rules/common/` into `.claude/rules/toga/common/` in the target project (and prunes
41
+ any stale pre-P2 framework rules). Claude Code automatically loads rules from `.claude/rules/`.
33
42
 
34
43
  If you want to install into a specific project:
35
44
 
@@ -39,7 +48,8 @@ node scripts/install.js /path/to/project
39
48
 
40
49
  ## How to add new rules
41
50
 
42
- 1. Create a new `.md` file in the appropriate subdirectory (`common/` or `php/`).
51
+ 1. Universal rule? Add a `.md` under `common/`. Framework-specific? Add it to the matching
52
+ `knowledge/<fw>/standards/framework-rules.md` (or `python.md`) instead — do not put it here.
43
53
  2. Write direct imperatives — "Never do X", "Always do Y". No meta-commentary.
44
54
  3. Keep each file under 200 lines.
45
55
  4. Run `node scripts/install.js` to push the new rule to all installed projects.
@@ -411,37 +411,17 @@ If you close without running /capture, the knowledge is lost.
411
411
 
412
412
  ---
413
413
 
414
- ### Available Skills (slash commands)
414
+ ### Skills & agents
415
415
 
416
- ${skillNames.map((s) => `- \`/${s}\``).join('\n')}
416
+ Your Claude Code session already lists every available \`/skill\` and specialist agent on each
417
+ launch — this file intentionally does **not** duplicate that catalog (a static copy only drifts
418
+ out of date). The rules that matter:
417
419
 
418
- ---
419
-
420
- ### Agents (Claude spawns these automatically — you can also invoke them explicitly)
421
-
422
- Each agent is a specialist. Claude delegates to them when the task fits. You can
423
- also tell Claude "use the php-reviewer agent on this file" explicitly.
424
-
425
- | Agent | When it activates |
426
- |---|---|
427
- | **php-reviewer** | Any PHP file is written or edited |
428
- | **sql-reviewer** | SQL queries detected in PHP or migration files |
429
- | **framework-pattern-checker** | New PHP class created — validates App_ vs _underscore conventions |
430
- | **php-build-resolver** | PHP fatal error or parse error in output |
431
- | **planner** | New feature request — produces phased implementation plan |
432
- | **knowledge-writer** | You ask Claude to document something — writes correct frontmatter |
433
- | **session-capture** | Runs inside /capture — extracts and categorizes session learnings |
434
- | **harness-optimizer** | Run /harness-audit — scores and improves this harness setup |
435
-
436
- **Multi-agent workflow example:**
437
- When you say "build the order refund endpoint", Claude will:
438
- 1. Spawn **planner** → get phased implementation plan
439
- 2. Write code → **framework-pattern-checker** validates conventions automatically
440
- 3. Write SQL → **sql-reviewer** checks for injection and N+1 patterns
441
- 4. After editing PHP → **php-reviewer** runs security and style checks
442
- 5. At /capture → **session-capture** extracts decisions for the knowledge base
443
-
444
- You do not need to manage this manually. The agents are wired to the right triggers.
420
+ - **\`/kickoff\` first, \`/capture\` last** — every session (see above).
421
+ - Specialist agents (php-reviewer, sql-reviewer, framework-pattern-checker, planner,
422
+ knowledge-writer, session-capture, php-build-resolver, harness-optimizer) fire **automatically**
423
+ on the matching file or operation. You can also invoke one explicitly — e.g. "use the
424
+ php-reviewer agent on this file."
445
425
 
446
426
  ---
447
427
 
@@ -629,14 +609,24 @@ function main() {
629
609
  }
630
610
  console.log(' ✓ Skills (' + skillNames.length + '): ' + fmtStats(skillStats));
631
611
 
632
- // Rules — update if content changed (team standards should always be current)
633
- const rulesSrc = path.join(harnessDir, 'rules');
634
- const rulesDest = path.join(claudeDir, 'rules', 'toga');
612
+ // Rules — only the UNIVERSAL set (rules/common) installs always-on. Framework-specific
613
+ // rules (1.0 App_, 2.0 _underscore, Python) now live in the knowledge base under
614
+ // <fw>/standards/ and load on-demand via /kickoff per the framework in scope (P2), so a
615
+ // session never carries rules for a framework it isn't working in.
616
+ const rulesToga = path.join(claudeDir, 'rules', 'toga');
617
+ const rulesSrc = path.join(harnessDir, 'rules', 'common');
618
+ const rulesDest = path.join(rulesToga, 'common');
635
619
  let rulesStats = { added: 0, updated: 0, unchanged: 0 };
636
620
  if (fs.existsSync(rulesSrc)) {
637
621
  try { rulesStats = copyDir(rulesSrc, rulesDest, { updateIfChanged: true }); }
638
622
  catch (e) { errors.push('rules: ' + e.message); }
639
623
  }
624
+ // Prune framework-specific rules + the meta README left behind by pre-P2 installs so
625
+ // they stop loading always-on (their content now lives in <fw>/standards/).
626
+ for (const stale of ['php', 'python', 'README.md']) {
627
+ const p = path.join(rulesToga, stale);
628
+ try { if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true }); } catch (_) { /* best-effort cleanup */ }
629
+ }
640
630
  console.log(' ✓ Rules (' + countMd(rulesDest) + '): ' + fmtStats(rulesStats));
641
631
 
642
632
  // Agents — update if content changed
@@ -343,6 +343,13 @@ related: []
343
343
  ---
344
344
 
345
345
  ## Summary
346
+ <2–4 sentence overview of the repo. **End the Summary with a `**Critical rules:**` line** — the
347
+ few load-bearing must-knows that have to ride along when only this summary is loaded (kickoff
348
+ loads the Summary, not the full doc, for repos pulled in as client-app awareness). See
349
+ `CONVENTIONS.md` → *Architecture Summary*. Examples: dbchanges2's mandatory `YYYY-MM-DD<letter>`
350
+ filename suffix; api2's exact `{success,data,errors}` envelope; `_underscore`'s lazy-transaction
351
+ write-drop gotcha.>
352
+
346
353
  ## <system sections: components, data, flows, key decisions>
347
354
  ```
348
355
 
@@ -35,16 +35,13 @@ description: Start-of-session context loader for TOGA Technology projects. Run t
35
35
 
36
36
  ## Arguments — text passed after `/kickoff` never skips any step
37
37
 
38
- `/kickoff` may be invoked with trailing text (e.g. `/kickoff worker2 backend fix for Compass`),
39
- including a long, specific paragraph naming exact files, line numbers, and a concrete
40
- change. That text is the developer's description of today's work it is **not** permission
41
- to shortcut the flow, no matter how actionable it looks.
38
+ Trailing text (e.g. `/kickoff worker2 backend fix for Compass`, even a detailed paragraph
39
+ naming files and line numbers) is the developer's description of today's work — **input to the
40
+ Step 2 interview, not permission to shortcut the flow** (the STOP gate above governs this).
42
41
 
43
42
  - **Step 0 (auto-update check) ALWAYS runs first**, with or without arguments.
44
- - Use the argument text to **pre-fill answers** to the Step 2 interview (framework, layer,
45
- repo, client, task). Only ask about whatever is still missing or ambiguous.
46
- - Never treat the argument as an instruction to start coding — or even to start *reading
47
- the named files* — before Steps 0–6 complete. Investigation IS work; it waits for priming.
43
+ - Use the argument text to **pre-fill** Step 2 answers (framework, layer, repo, client, task);
44
+ ask only about what's still missing or ambiguous.
48
45
 
49
46
  ## Step 0 — Auto-update check (runs before anything else, even with arguments)
50
47
 
@@ -247,6 +244,14 @@ Tell the developer concisely:
247
244
  say so explicitly: "No knowledge captured yet for X — `capture` will build it as you work."
248
245
  - Confirm you're primed and ready for their first task.
249
246
 
247
+ **Heavy-load check (from preflight `estimate`).** Preflight returns
248
+ `estimate {words, tokensApprox, fullReads, summaries, heavy}`. If `estimate.heavy` is true
249
+ (load > ~8k words — usually too many full repos), tell the developer and suggest narrowing:
250
+ > "This scope loads ~`tokensApprox` tokens across `fullReads` full repos. If you're only
251
+ > touching one, name just that repo (`also drop <repo>`) so the rest stay as summaries."
252
+ Client `apps` load as cheap summaries by default, so a large client rarely trips this — it's
253
+ usually too many repos passed to `--repos`.
254
+
250
255
  ## New-repo onboarding (when the developer names a repo not in `registry.json`)
251
256
 
252
257
  Ask **all** of the following — assume nothing (you may offer a suggested default, e.g.