toga-ai 1.0.965 → 1.0.967

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.
@@ -172,6 +172,16 @@
172
172
  "timeout": 3000
173
173
  }
174
174
  ]
175
+ },
176
+ {
177
+ "matcher": "Write|Edit|MultiEdit",
178
+ "hooks": [
179
+ {
180
+ "type": "command",
181
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/memory-scope-guard.js\"",
182
+ "timeout": 3000
183
+ }
184
+ ]
175
185
  }
176
186
  ],
177
187
  "SessionStart": [
@@ -6,8 +6,8 @@ project: Test
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-22
10
- owners: [jcardinal]
9
+ updated: 2026-10-07
10
+ owners: [jcardinal, mhammontree]
11
11
  files:
12
12
  - test/team/2.0 deployment/generate_client_sql.php
13
13
  - test/team/2.0 deployment/INPUT.sql
@@ -31,6 +31,12 @@ Use [Tools Multi-Client SQL](../../tools/features/multi-client-sql.md) instead (
31
31
  2. Reads one static list file; switch lists by commenting between `Clients_Db.txt` / `Logs_Clients_Db.txt` / `NetSuite_Clients_Db.txt` in the script.
32
32
  3. Per DB: banner comment, `` USE `<db>`; ``, then the SQL. Prints the combined script with a header and DB count.
33
33
 
34
+ ## The three list files (verified prod 2026-10-07)
35
+
36
+ - **`Clients_Db.txt` — 35 entries, and all 35 match the live tenant databases exactly. It is NOT stale.** The 11 extra `Core.Clients` rows have no database (see [per-client DB connections](../../../../2.0/apps/_underscore/features/per-client-database-connections.md)); adding them would point the fan-out at databases that do not exist.
37
+ - **`NetSuite_Clients_Db.txt` — 22 clients, an exact duplicate** of the clients whose `dbchanges2/Client_<Name>/_modules.txt` names `netsuite`. Same list kept twice; `_modules.txt` is the better source because it sits next to the client it describes.
38
+ - Retiring these files means retiring `generate_client_sql.php` (it reads all three, including a commented-out `Logs_Clients_Db.txt`) — not just deleting text files. Production already uses tools `/developers/multi-client-sql`.
39
+
34
40
  ## Gotchas
35
41
 
36
42
  - Lists are static — a stale/missing DB is silently skipped. The Tools tool reads them live.
@@ -42,4 +48,6 @@ Use [Tools Multi-Client SQL](../../tools/features/multi-client-sql.md) instead (
42
48
 
43
49
  ## Change history
44
50
 
51
+ - 2026-10-07 — Verified the list files: `Clients_Db.txt` is accurate (35/35); `NetSuite_Clients_Db.txt` duplicates dbchanges2 `_modules.txt`. (mhammontree)
52
+
45
53
  - 2026-09-22 — Marked superseded by Tools Multi-Client SQL; corrected input file (`INPUT.sql`) and list-switching. (jcardinal)
@@ -6,8 +6,8 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-24
10
- owners: [jcardinal]
9
+ updated: 2026-10-07
10
+ owners: [jcardinal, mhammontree]
11
11
  files:
12
12
  - tools/_/app/developers/multiclientsql.php
13
13
  - tools/mvc/developers/multi-client-sql/get.php
@@ -38,8 +38,19 @@ Replaces `test/team/2.0 deployment/generate_client_sql.php`. Development Team pe
38
38
  5. Every list returns a `skipped` array, shown on the page ("Skipped N (database not built — no DatabaseHosts row): …").
39
39
  6. Every DB/folder name is checked against `^[A-Za-z0-9_]{1,64}$` before it goes into GraphQL or a `USE` line. Page writes via `textContent`/`.value` only.
40
40
 
41
+ ## Reusing it from another tools page
42
+
43
+ - `token()` is already `public static` and reads `GITHUB_TOKEN` from `getenv()` + `$_SERVER` + `$_ENV`. That token already has read access to `agilantsolutions/dbchanges2` in production — a second page needs **no new PAT and no new EB property**.
44
+ - `graphql(string $fields, string $step)` is `private static` and carries the whole transport (curl, host pinned to `api.github.com`, bearer token, 30s timeout, one retry on a dropped connection, GraphQL error handling, and never logging the request because it holds the Authorization header). Making it `public static` is a one-keyword change with no behavior change — do that rather than re-implementing curl.
45
+ - `GITHUB_BRANCH` is a const `_main` baked into expressions inside private methods. A page needing another branch builds its own field strings and borrows only the transport — do **not** thread a `$branch` parameter through the shipped class.
46
+ - `DB_NAME_PATTERN` is `^[A-Za-z0-9_]{1,64}$` and **rejects dbchanges2 filenames** (`2026-06-03- BLANK_CLIENT_DATABASE.sql` has dashes, spaces, a dot). A page listing files needs its own filename pattern.
47
+ - `treeFolders()` keeps only `type === 'tree'` — folders only, never files.
48
+ - The `_modules.txt` parse (lowercase, trim, skip blank lines) already tolerates the trailing blank lines in `Client_Spglobal/_modules.txt`.
49
+
41
50
  ## Gotchas
42
51
 
52
+ - Nested module folders are not handled: `_modules/Ai/Bdr/` makes the module list show `Ai` with zero member clients. Pre-existing live limitation, not a regression.
53
+
43
54
  - `Databases` is a MySQL reserved word: `FROM Databases` / `JOIN Databases AS X` fails with error 1064. Backtick it (`` `Databases` ``) or qualify it (`Core.Databases`).
44
55
  - dbchanges2 has **no** `_production` branch; its deployed branch is `_main`.
45
56
  - A missing `[database_...]` config section is not catchable in 1.0 — `App_Error` renders its HTML error page and exits. The tool pre-checks `config['database_toga2core']` and throws a user-safe message. Local `config.dev-*.ini` files may lack that section.
@@ -52,5 +63,7 @@ Replaces `test/team/2.0 deployment/generate_client_sql.php`. Development Team pe
52
63
 
53
64
  ## Change history
54
65
 
66
+ - 2026-10-07 — Added the reuse map for other tools pages (`token()`, `graphql()`, the branch const and name patterns) and the nested `_modules/Ai/Bdr` limitation. (mhammontree)
67
+
55
68
  - 2026-09-22 — Created; shipped and verified in production. (jcardinal)
56
69
  - 2026-09-24 — Lists filtered by `Core.DatabaseHosts` for `ENVIRONMENT`; `skipped` list shown; verified prod 35 of 46. (jcardinal)
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-02
9
+ updated: 2026-10-07
10
10
  owners: [jcardinal, ajean, mhammontree]
11
11
  files:
12
12
  - tools/_/app/auth.php
@@ -88,6 +88,7 @@ The platform's rotating key/value parameter store, on the **2.0 PROD Core DB** (
88
88
  - **Failure path also fataled → fixed (2026-06-30).** `tools_ssoFail()` called `http_response_code(401)` to render the "Authentication failed" page, which itself fataled with "headers already sent" and the error handler escalated it to an uncaught `ErrorException` → a bare **500** instead of the intended 401 page. Root cause: `App_FrameworkIndex::render()` turns on the preloader and runs `App_Page::flushCapture()` (`ob_end_flush + flush`, `library/app/page.php:213`) **before** `body()/App_MVC::loadFile()` runs the mvc page. Fixed with the same `if (!headers_sent()) { http_response_code(401); }` guard (matches `App_MVC::routeTo`'s own check).
89
89
  - **`App_Sso::initiate()` hard-requires a non-empty `apiSecretAccessToken`** (`library/app/sso.php:64-65`, `empty()` guard → `InvalidArgumentException`; the token encrypts the RelayState). An empty/absent token surfaces as a redirect to **`/login?error=1` ("Sign in failed. Please try again.")** — the initiate leg catches the exception (`sso/initiate/get.php:30-34`) and routes to `/login?error=1`. This fails **before** the IdP round-trip, so it never reaches the consumer's **"Authentication failed"** (`tools_ssoFail`) page. **The two error UIs tell you which leg failed:** `/login?error=1` = initiate (RelayState encryption / token), "Authentication failed" = consumer (decrypt). This was the 2026-06-30 regression — removing the config token per the (wrong) earlier note made initiate throw.
90
90
  - **Both legs depend on `db_toga2core` + a populated `Core.Parameters` token.** If `[database_toga2core]` is not configured, `currentApiSecret()`/`handoffKeys()` runs `App_Database::query` against an unregistered alias and **throws**. Because initiate only catches `InvalidArgumentException`/`RuntimeException`, that surfaces as a **500** — not the login banner. **Diagnostic:** a 500 at initiate = missing `db_toga2core`; `/login?error=1` = empty/absent `API_SECRET_ACCESS_TOKEN`.
91
+ - **Local dev-bypass login fails with a bare "Sign in failed." — the cause is usually a missing config section, not SSO.** `tools/mvc/login/post.php` routed an invalid/empty email **and** a not-found user to `/login?error=1` with no logging, so three different causes looked identical. Each failure branch now writes one `error_log` line naming which branch fired (the not-found branch points at `[database_true]`). Root cause in practice: `config.dev-<name>.ini` is missing **`[database_true]`** and/or **`[database_toga2core]`**. Those two sections are commonly absent from local `config.dev-*.ini` files; without `[database_toga2core]` the Multi-Client SQL and Central IDs pages also fail. Copy them from a config that has both.
91
92
  - **No app-side replay defense.** The handoff token carries no nonce/timestamp the app verifies. Recommend the gateway embed `iat` + `jti`.
92
93
 
93
94
  ## Related
@@ -99,4 +100,6 @@ The platform's rotating key/value parameter store, on the **2.0 PROD Core DB** (
99
100
  - [TOGa Desk staff SSO (same pattern)](../../togadesk/features/staff-sso-login.md)
100
101
 
101
102
  ## Change history
103
+
104
+ - 2026-10-07 — Dev-bypass login now logs which failure branch fired; documented the missing `[database_true]` / `[database_toga2core]` local-config cause. (mhammontree)
102
105
  - 2026-10-02 — Clarified sign-in is one click on an SSO button; no auto-detect of a Microsoft session (mhammontree)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-09-23
9
+ updated: 2026-10-07
10
10
  owners: ["jcardinal", "rgirish", "mhammontree", "ajean"]
11
11
  files:
12
12
  - _underscore/_underscore.php
@@ -177,22 +177,32 @@ string** has a single leading capital, rest lower-case (e.g. `Towfoundation`):
177
177
  | Tenant logs | `Logs_<Tenant>` | Per-client activity/audit logs. |
178
178
  | Framework logs | `Logs` | Generic 2.0 logs not tied to any client. |
179
179
 
180
- **Resolving a tenant string from a client name:** query `Core.Clients`. The
181
- `clientIdentifier` field **is** the exact suffix used in `Client_`, `Archive_`, and
182
- `Logs_` database names; the `name` field is the human-readable label. Look up by either
183
- to find the other:
180
+ **Resolving a tenant's real database names:** query `Core.Clients`, then follow the id
181
+ columns into `Core.Databases` — **never build the name as `'Client_' . $clientIdentifier`.**
184
182
 
185
183
  ```sql
186
184
  SELECT
187
- name,
188
- clientIdentifier
189
- FROM Clients
185
+ c.name,
186
+ c.clientIdentifier,
187
+ cd.name AS clientDatabase,
188
+ ld.name AS logDatabase,
189
+ ad.name AS archiveDatabase
190
+ FROM Clients AS c
191
+ LEFT JOIN `Databases` AS cd ON cd.id = c.clientDatabaseId
192
+ LEFT JOIN `Databases` AS ld ON ld.id = c.logDatabaseId
193
+ LEFT JOIN `Databases` AS ad ON ad.id = c.archiveDatabaseId
190
194
  WHERE
191
- name LIKE '%<keyword>%'
195
+ c.name LIKE '%<keyword>%'
192
196
  ```
193
197
 
194
- A client whose `clientIdentifier` is `Towfoundation` therefore lives in
195
- `Client_Towfoundation`, `Archive_Towfoundation`, and `Logs_Towfoundation`.
198
+ `clientIdentifier` usually matches the suffix (`Towfoundation` → `Client_Towfoundation`,
199
+ `Archive_Towfoundation`, `Logs_Towfoundation`), but that is a **convention, not a rule**.
200
+ Verified in prod: `Compass_Usa` → `Client_Compass` / `Logs_Compass` / `Archive_Compass`, and
201
+ `Compass_Canada` → `Client_CompassCanada` / `Logs_CompassCanada` / `Archive_CompassCanada`.
202
+ There is no `Client_Compass_Usa`. A string-built name passes local testing and breaks in
203
+ production for exactly those clients. Precedent that does it right:
204
+ `App_Developers_MultiClientSql::coreLists()` in 1.0 `tools`. See
205
+ [per-client database connections](features/per-client-database-connections.md).
196
206
 
197
207
  #### Non-production environments
198
208
 
@@ -428,3 +438,7 @@ multi-file UI components (`.php`/`.html`/`.css`/`.js`) invoked as `<_ComponentNa
428
438
  message/trace to the client if `display_errors` is on unconditionally. Confirm `display_errors` is
429
439
  disabled in production and that the framework's error surface returns a sanitized envelope (not a raw
430
440
  stack trace) to API consumers. Not yet done — tracked as a follow-up.
441
+
442
+ ## Change history
443
+
444
+ - 2026-10-07 — Corrected tenant DB-name resolution: resolve `Core.Clients` id columns into `Core.Databases`; `clientIdentifier` is not the suffix (Compass). (mhammontree)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-06
9
+ updated: 2026-10-07
10
10
  owners: ["dfranks", "jcardinal", "mhammontree", "apeterson", "kyalamarthi", "bala"]
11
11
  files:
12
12
  - _underscore/Database.php
@@ -59,7 +59,7 @@ When `_underscore` serves a request for a client it opens **three distinct per-c
59
59
 
60
60
  ## Gotchas
61
61
 
62
- - **`Core.Clients` has rows for clients whose DBs were never built** (prod 2026-09-24: 46 rows, 35 built). "DB exists" = a `Core.DatabaseHosts` row (`databaseId` + `environmentId` [+ `regionId`]) for the current `Environments.slug` — the same rows `registerClientDatabases()` resolves. Any fan-out over all clients must filter `EXISTS` on `DatabaseHosts` for the env, not `Core.Clients` alone; `isActive`-style flags are wrong (inactive clients still have DBs). Host-row count varies (e.g. `Archive_Nychh` has 1, others 4) — still built. Example: [Tools Multi-Client SQL](../../../../1.0/apps/tools/features/multi-client-sql.md).
62
+ - **`Core.Clients` has rows for clients whose DBs were never built** (prod 2026-09-24: 46 rows, 35 built). "DB exists" = a `Core.DatabaseHosts` row (`databaseId` + `environmentId` [+ `regionId`]) for the current `Environments.slug` — the same rows `registerClientDatabases()` resolves. Any fan-out over all clients must filter `EXISTS` on `DatabaseHosts` for the env, not `Core.Clients` alone; `isActive`-style flags are wrong (inactive clients still have DBs). Host-row count varies (e.g. `Archive_Nychh` has 1, others 4) — still built. The 11 registered-but-never-provisioned clients (prod 2026-10-07): **Agilant, Athena, Cloudcsp, Companycom, Eclinicalworks, Ims, Newegg, Officedepot, Staples, Synnex, Walmart** — each has a `Core.Databases` row naming `Client_<Name>`, but no schema. Do **not** add them to any fan-out list. Also on the prod client cluster: a stray `Client_Compass_20260831_120000` schema (looks like an Aug-31 manual backup copy) — confirm with the owner before dropping. Example: [Tools Multi-Client SQL](../../../../1.0/apps/tools/features/multi-client-sql.md).
63
63
  - **⚠ NEVER read `LAST_INSERT_ID()` (or any other per-connection session value) through `_Query`.** `_Query` defaults `_isReadHostEnabled = true` (`Query.php:8`) and routes SELECTs to `CONNECTION_READ` (`Query.php:273-276`). `LAST_INSERT_ID()` is scoped to the connection that did the INSERT, so a read-host SELECT returns **0** — silently wrong, and now an FK violation wherever the id is used as a foreign key. Use `mysqli_insert_id` on the write/transaction connection and reject `<= 0`. It is not biting today only because most client registrations leave `writeHost`/`readHost` null, and `Database.php:488` then points READ and WRITE at the **same** `mysqli` handle (so raw `mysqli_query` and `new _Query(...)` share one connection and one transaction). Configuring a real read host for any client would break every such call at once. Worked example: [Compass PEOPLE-file user lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
64
64
  - **Locks and just-committed rows: force the writer and skip the cache.** A `_Query` SELECT goes to the read host and may be served from the query cache. For `GET_LOCK`/`RELEASE_LOCK` (session-scoped — must be on the writer) or reading a row another job just committed, call `$query->setisReadHostEnabled(false)` and `_Database::useQueryCache(false)`. Worked example: NYCDOE `Sftp/Import` per-checksum lock in [2.0 ASN SFTP Ingestion](../../../../clients/nycdoe/features/asn-sftp-ingestion-2-0.md).
65
65
  - **`_Database::transactionCommit()` / `transactionRollback()` with NO database argument leave each alias's "started" flag `true`.** A later `transactionStart()` in the same PHP process then never begins a transaction — writes autocommit and a rollback undoes nothing. Prod worker jobs and api2 requests are safe (one job/request = fresh process state); it bites long-lived processes and test harnesses. Reset with `\_Database::$_transactionStarts = []` before `transactionStart()` (see `runAsWorkerJob` in [worker2 Codeception tests](../../worker2/features/codeception-integration-tests.md)).
@@ -94,6 +94,7 @@ Methodology lesson — why the wrong conclusion was reached: the model-class com
94
94
  - [Re-pointing a DB alias mid-request](./database-alias-repointing.md)
95
95
 
96
96
  ## Change history
97
+ - 2026-10-07 — Named the 11 registered-but-unprovisioned prod clients and the stray `Client_Compass_20260831_120000` backup schema. (mhammontree)
97
98
  - 2026-10-06 — Writer+no-cache reads for locks/just-committed rows; no-arg commit/rollback leaves the transaction-started flag set (mhammontree)
98
99
  - 2026-09-23 — Added the read-host routing trap: `_Query` sends SELECTs to `CONNECTION_READ`, so `LAST_INSERT_ID()` read through `_Query` can return 0; noted that null `writeHost`/`readHost` currently collapses both to one handle, and that raw `mysqli_query` needs a manual `resetQueryCache()`. (mhammontree)
99
100
  - 2026-09-24 — Added gotcha: filter client fan-outs by `DatabaseHosts` for the environment; `Core.Clients` includes never-built clients. (jcardinal)
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-07
10
10
  owners: ["mhammontree"]
11
11
  files:
12
12
  - Client/
@@ -51,6 +51,21 @@ Order matters — each step assumes the prior one ran.
51
51
  - **api2 / _underscore** — the API the client calls; verify here.
52
52
  - **Bastion** — production deploy path (upload SQL, source per cluster).
53
53
 
54
+ ## Planned onboarding tool — settled design (TRUE-79864)
55
+
56
+ Platform-level and host-independent. Decided 2026-10-07; **host moved to Desk 2.0 (`toga25-desk`)**, which already plans an "Add Client" button. The 1.0 `tools` hosting plan is dead (no `App_Devops_*` classes, no 1.0 MVC page, no persona nav entry, and `App_Developers_MultiClientSql` is a 1.0 class Desk cannot reach). Builds on the 2026-06-22 Jeff design spec (ticket TRUE-79702) at `2.0-new-client-onboarding-consolidation.md` — read that file for the spec; it is not copied here.
57
+
58
+ - **Generates SQL and guides. It never executes SQL and never writes to a repository.** Because it never writes, the "two onboardings at once" branch-lock problem does not exist.
59
+ - **Output is split by cluster:** (A) `CREATE DATABASE` Client_/Logs_/Archive_; (B) the apply order for `Client_<Id>` — newest-dated blank, then loose `Client/*.sql` in filename order, then the chosen modules' files, **file names and order only, not the SQL text**; (C) the `Logs_Client/` blank for `Logs_<Id>`; (D) Core registration inserts (secret-free, committable); (E) `Apis` + `Apis_Roles` (secrets, shown once, never committed).
60
+ - **`Logs_<Name>` is IN scope** even though the 2026-06-22 spec deferred it: the API logs there and checks the auth rate limit against it, so deferring it leaves a client that registers fine and then cannot authenticate.
61
+ - **Branch is picked from a drop-down**, excluding the repo default branch (`_main`) and any protected branch — onboarding always runs against a ticket branch. Read the default branch name from the API; never hardcode it.
62
+ - **Modules: selection only** (spec step 5 — read `_modules/`, tick what the client uses, fold its files into the apply order, write the client's `_modules.txt` line). Module **consolidation** (spec step 4) is OUT; it is a write. **Blank consolidation is also OUT** — `Client/HISTORIC` shows ~15 rewrites over ~2 years, so it is periodic maintenance on its own clock, not part of onboarding.
63
+ - **The `Apis` secret is generated in the browser** with `crypto.getRandomValues` and the INSERT is built client-side, so plaintext never reaches the server or a log. (`crypto.randomUUID` and `crypto.subtle` are undefined on a non-secure origin; `getRandomValues` is not.)
64
+ - **Run history: two new prod `Core` tables** — `ClientOnboardings` (client, branch, commit SHA, blank filename, loose-file count, ticket ref, who, status, email domains, modules, the ordered file list as TEXT) and `ClientOnboardingEnvironments` (FK to `Core.Environments.id`, isApplied, dtApplied, appliedByEmail). `Core.Environments` exists in prod with 27 rows (production = id 1). Two constraints: **no FK to the user** (`Client_True` is on the client cluster, `Core` on the core cluster), and **never store the `Apis` secret**.
65
+ - **The internal API-key row is named `TOGA Technology`, not `Agilant`** — the old company name is not used for new work. The existing generator still emits `Agilant`.
66
+ - **The one real correctness risk is a stale local checkout.** If the developer's checkout is ahead of the branch the tool reads, the tool lists *fewer* files than exist and the new client silently misses migrations, with no error. Mitigation: print the branch, the commit SHA, the file count, each file's blob SHA, and one verify command.
67
+ - **Open question:** Desk is React talking to api2, so the GitHub read must move server-side into api2 or worker2 — a long read inside a web request hits a timeout problem.
68
+
54
69
  ## Gotchas
55
70
 
56
71
  - **🚨 The blank can be WRONG, not just behind — an open example (2026-08-28).** `Client/2026-06-03- BLANK_CLIENT_DATABASE.sql` creates `TransferOrderItems` (`CREATE` at ~line 9462) **without `dtCreated`/`dtUpdated`**, but `_Model_Client_TransferOrderItem` declares them as `FIELD_DATETIME_CREATED`/`FIELD_DATETIME_UPDATED`, so `_Model` puts them in **every SELECT it builds**. Every tenant provisioned from this blank has a table the model cannot read — nothing in a request has to ask for the columns. `Client/2026-08-28b - TransferOrderItemsTimestamps.sql` repairs existing tenants; **the blank itself is still unfixed**, so the next onboarding reproduces it. When a model-vs-blank mismatch is found, fix **both**: a catch-up `Client/` file for live tenants *and* the blank.
@@ -69,3 +84,7 @@ Order matters — each step assumes the prior one ran.
69
84
  - [TOGa 2.0 client onboarding SQL generator](../../../1.0/apps/test/features/toga2-client-onboarding-sql.md)
70
85
  - [TOGa 2.0 Client Onboarding Wizard](../../../1.0/apps/test/features/toga2-onboarding-wizard.md)
71
86
  - [Tickets API](../../api2/features/tickets-api.md)
87
+
88
+ ## Change history
89
+
90
+ - 2026-10-07 — Recorded the settled design for the onboarding tool (TRUE-79864) and the decision to host it in Desk 2.0, not 1.0 tools. (mhammontree)
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-10-06
8
+ updated: 2026-10-07
9
9
  owners: [jcardinal, mhammontree, dfranks, bala, ajean]
10
10
  files: []
11
11
  related:
@@ -666,6 +666,9 @@ can follow. Do not type the child alone.
666
666
 
667
667
  * Use comments to explain the "why" behind the code, not the "how".
668
668
  * Inline comments should be used sparingly.
669
+ * Keep a file docblock short: what the file is, plus the one or two traps that bite the next
670
+ editor. Design history, scope decisions, reuse notes and rejected approaches go to the
671
+ knowledge base at `/capture`, not into the file — they bury the code and go stale.
669
672
 
670
673
  #### **DocBlocks**
671
674
 
@@ -1023,3 +1026,7 @@ Existing static-key paths (ZoomInfo, VipSupport, `_Cloud` CLI) are grandfathered
1023
1026
  is deferred, not sanctioned for new work. Never record access-key values anywhere but the ini.
1024
1027
 
1025
1028
  See: 2.0/apps/worker2/features/cross-account-aws-access.md
1029
+
1030
+ ## Change history
1031
+
1032
+ - 2026-10-07 — Added the short-file-docblock rule (design history belongs in the knowledge base). (mhammontree)
@@ -174,6 +174,9 @@ Every fact has exactly one home — never duplicate it into a second place.
174
174
  they're copied elsewhere. Kickoff/capture may be told to *read* one; never to *paste* it in.
175
175
  - **Everything a person must know lives in the team KB** — decisions, rules, gotchas,
176
176
  architecture. Not a generated file, not a stale wiki page.
177
+ - **Never only in local Claude memory.** Memory is private to one machine: it holds talking
178
+ style, local paths, and personal habits only. A team rule goes here (or the platform apps'
179
+ docs) and memory keeps at most `Lives in: <where>` — see `rules/common/memory-scope.md`.
177
180
  - **App docs lifecycle (pre-MVP → launch → post-launch):**
178
181
  1. **Before MVP**, an in-development app's knowledge lives in the app repo's own `docs/`.
179
182
  The KB carries only a `registry.json` entry + a small `architecture.md` **stub**
@@ -45,7 +45,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
45
45
  - **togatech** (TOGA Technology Website) — 8 doc(s) → [standalone/apps/togatech/INDEX.md](standalone/apps/togatech/INDEX.md)
46
46
  - **websocket** (WebSocket Server) — 2 doc(s) → [standalone/apps/websocket/INDEX.md](standalone/apps/websocket/INDEX.md)
47
47
  - **forward** (Forwarder) — 3 doc(s) → [standalone/apps/forward/INDEX.md](standalone/apps/forward/INDEX.md)
48
- - **claude** (Claude Harness) — 8 doc(s) → [standalone/apps/claude/INDEX.md](standalone/apps/claude/INDEX.md)
48
+ - **claude** (Claude Harness) — 9 doc(s) → [standalone/apps/claude/INDEX.md](standalone/apps/claude/INDEX.md)
49
49
  - **powerbi** (Power BI) — 0 doc(s) → [standalone/apps/powerbi/INDEX.md](standalone/apps/powerbi/INDEX.md)
50
50
 
51
51
  ## Clients
@@ -2,6 +2,7 @@
2
2
 
3
3
  | Doc | Summary |
4
4
  |-----|---------|
5
+ | [Memory Scope Guard — Keep Team Rules Out of Local Claude Memory](features/memory-scope-guard.md) | Rule + PostToolUse warn hook + `/capture` sweep that keep team build rules in the KB, not in one developer's private Claude memory. |
5
6
  | [Review-Enforcement Hooks — Mandatory sql-reviewer / php-reviewer Routing on Write](features/review-enforcement-hooks.md) | PostToolUse hooks that force a specialist review (`sql-reviewer` / `php-reviewer`) after SQL/PHP writes; open before turning either into a pre-write gate. |
6
7
  | [Harness Distribution — How a knowledge.js Fix Reaches Teammates](workflows/harness-distribution.md) | How a `knowledge.js` fix reaches teammates — two copies exist, only the git clone matters; `npx toga-ai` pulls it. |
7
8
  | [Knowledge Base Publish / Push Pipeline](workflows/knowledge-publish-pipeline.md) | How `/capture` and `/session-save` push knowledge docs to `agilantsolutions/claude` `_main` — the git sequence, retry, and the `47fbdc4` PUSH_FAILED fix. |
@@ -0,0 +1,54 @@
1
+ ---
2
+ title: Memory Scope Guard — Keep Team Rules Out of Local Claude Memory
3
+ framework: "standalone"
4
+ repo: claude
5
+ project: Claude Harness
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-06
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - claude/rules/common/memory-scope.md
13
+ - claude/scripts/hooks/memory-scope-guard.js
14
+ - claude/skills/capture/SKILL.md
15
+ - claude/.claude/settings.json
16
+ related:
17
+ - standalone/apps/claude/features/review-enforcement-hooks.md
18
+ ---
19
+
20
+ Rule + PostToolUse warn hook + `/capture` sweep that keep team build rules in the KB, not in one developer's private Claude memory. Open when the guard fires or a rule "only one dev knows".
21
+
22
+ ## Summary
23
+
24
+ Local Claude memory (`~/.claude/projects/<project>/memory/*.md`, index `MEMORY.md`) is private to one machine. A scripted-API rule and ~80 build rules once lived only there, invisible to teammates. Three parts stop that:
25
+
26
+ 1. **Rule** `rules/common/memory-scope.md` (always-on, installed to `.claude/rules/toga/common/`): memory holds only personal things — talking style, local paths, personal habits (`type: user`). Any rule about how we build goes to the KB (platform-app rules: the architecture docs of every platform app — `toga25-desk`, `toga25-iq`, `toga2-template`) in the same change; memory keeps at most `Lives in: <where>`.
27
+ 2. **Hook** `scripts/hooks/memory-scope-guard.js` — warns, never blocks.
28
+ 3. **`/capture` Step 2b** — sweeps this session's memory notes, moves team rules into the KB, shrinks each note to its `Lives in:` pointer.
29
+
30
+ `communication.md`'s per-person profile still lives in memory — the rule names it as personal.
31
+
32
+ ## How the hook works
33
+
34
+ - Wired in the template `.claude/settings.json` → `PostToolUse`, matcher `Write|Edit|MultiEdit`.
35
+ - Fires only when the target path is inside `.claude/projects/<x>/memory/` or `.claude/memory/` (either slash style) and ends `.md`.
36
+ - Reads the note from disk (falls back to the written text) and warns when **one sentence or clause states a build rule**: a directive word (never, always, must, do not, don't, should, required) **and** a build term (SQL, dbchanges2, api2, _underscore, interceptor, ACL, RecordFields, Surface — capitalised only, blox, migration, endpoint, scripted, worker2, NetSuite, schema, component). Any `type:` except `user`.
37
+ - Ignored text: `**Why:**` lines (they quote the reason), `(...)` asides, `[[note-name]]` links.
38
+ - Never warns on: `type: user`; any note with a `Lives in:` line; names `user-*`, `repo-path-*`, `habit-*`, `*-profile`, or with `path|location|dir|folder` in the name. Lookup notes (paths, URLs, profiles, registries) and status/reminder notes stay quiet because they rarely pair a directive with a build term.
39
+ - `MEMORY.md`: only added lines that are not `- [name](file.md)` pointers and name a build term.
40
+ - Output: JSON `hookSpecificOutput.additionalContext` (the PostToolUse channel that reaches the model). Once per file per session (state in OS temp, `toga-memory-scope-<session>.json`). Always exit 0, fail-open.
41
+ - Off switch: `TOGA_MEMORY_SCOPE_GUARD_DISABLED=1`.
42
+
43
+ ## Gotchas
44
+
45
+ - Heuristic, not exact. It misses a rule worded without a directive word ("lock it into the app docs"). A personal habit saved as `type: feedback` that says "always ... SQL" will warn — save habits as `type: user` or name them `habit-*`.
46
+ - Tuned 2026-10-06 against one developer's 140 notes: the first version fired on 14 (lookups and status notes); the current one fires on 0.
47
+ - Plain stdout from a PostToolUse hook is not reliably shown to the model; this hook uses `additionalContext` on purpose.
48
+
49
+ ## Change history
50
+ - 2026-10-06 — Created: memory-scope rule, guard hook, `/capture` Step 2b sweep; heuristic tuned to "directive + build term" to cut false positives (jcardinal)
51
+
52
+ ## Related
53
+
54
+ - [Review-Enforcement Hooks](review-enforcement-hooks.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.965",
3
+ "version": "1.0.967",
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",
@@ -142,3 +142,6 @@ How to keep it:
142
142
  the developer's known style and habits before you write a word.
143
143
  - **This profile is private and per-person. Never put it in the team knowledge base** — it lives
144
144
  only in that developer's local memory, on their machine.
145
+ - **A personal habit is not a team rule.** "Run php -l for me" is a habit (memory, `type: user`).
146
+ "Record hooks go in api2 interceptors" is how we build (team KB). See
147
+ [memory-scope.md](memory-scope.md).
@@ -0,0 +1,46 @@
1
+ # Memory Scope — what goes in local memory vs the team KB
2
+
3
+ A developer's local Claude memory (`~/.claude/projects/<project>/memory/*.md`, index
4
+ `MEMORY.md`) is private to one machine. Teammates never see it. So it holds **only personal
5
+ things**. Team rules go where the team can read them.
6
+
7
+ ## Local memory — personal only
8
+
9
+ 1. How this developer likes to be talked to (`user-communication-profile`).
10
+ 2. Their local machine paths (`repo-path-<repo>`, `team-repo-path`).
11
+ 3. Their personal work habits ("run php -l for me", "don't commit unless I say so").
12
+
13
+ Save habit and profile notes as `type: user` (or name them `user-*`).
14
+
15
+ ## Team KB — every rule about HOW we build
16
+
17
+ Anything about how we build goes into the team KB in the **same change** — never only into
18
+ memory. That includes:
19
+
20
+ - architecture and where code lives,
21
+ - API shape (api2 endpoints, interceptors, scripted vs table APIs),
22
+ - SQL and `dbchanges2`,
23
+ - ACL and roles,
24
+ - data model (Records, RecordFields, Surface/settings),
25
+ - UI and design process, reuse (blox),
26
+ - sync and integration behavior (NetSuite, worker2),
27
+ - the harness itself (skills, hooks, rules).
28
+
29
+ **Platform apps** (`toga25-desk`, `toga25-iq`, `toga2-template`): an app-build rule goes into
30
+ the architecture docs of **every** platform app, not just the one you are in.
31
+
32
+ ## The rules
33
+
34
+ 1. **When a developer states a team rule, write it to the KB / app docs first.** Then, if
35
+ useful, keep a one-line pointer in memory.
36
+ 2. **Memory may keep at most a pointer** for a team rule:
37
+ `Lives in: <KB doc path or app doc § section>`. No copy of the rule body.
38
+ 3. **Unsure?** Ask: "would a teammate building the same thing need this?" Yes → KB.
39
+ 4. **At `/capture`**, move any team rule found in this session's memory notes into the KB and
40
+ shrink the note to its `Lives in:` pointer.
41
+
42
+ ## How this is enforced
43
+
44
+ `scripts/hooks/memory-scope-guard.js` runs after every Write/Edit into a memory folder. If the
45
+ note looks like a team rule, it warns (never blocks). Silence it with
46
+ `TOGA_MEMORY_SCOPE_GUARD_DISABLED=1`.
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * memory-scope-guard.js — WARN when a team rule is being written into a developer's
6
+ * LOCAL Claude memory instead of the team knowledge base.
7
+ *
8
+ * Why this exists: local memory (~/.claude/projects/<project>/memory/*.md, index
9
+ * MEMORY.md) is private to one machine. Teammates never see it. A scripted-API rule and
10
+ * ~80 build rules once lived only in one developer's memory. The rule
11
+ * (rules/common/memory-scope.md) says memory holds ONLY personal things — talking style,
12
+ * local paths, personal habits. Team rules go to the KB (or a platform app's docs), and
13
+ * memory keeps at most a one-line "Lives in:" pointer.
14
+ *
15
+ * How it works: registered on PostToolUse for Write|Edit|MultiEdit. It fires only when
16
+ * the target sits inside a Claude memory folder (`.claude/projects/<x>/memory/` or
17
+ * `.claude/memory/`, either slash style). It reads the note as written to disk and warns
18
+ * when the note STATES A BUILD RULE: one sentence holds both a directive word (never /
19
+ * always / must / do not / should / required) and a build term (SQL, api2, ACL, ...).
20
+ * Lookup notes (paths, URLs, profiles, registries) and status/reminder notes rarely pair
21
+ * the two, so they stay quiet. "Why:" lines are ignored (they quote the reason).
22
+ * Skipped: `type: user`; any note with a "Lives in:" pointer line; notes named user-*,
23
+ * repo-path-*, habit-*, *-profile, or with path/location/dir/folder in the name.
24
+ * MEMORY.md (the index) is checked only for added lines that are not "- [name](file.md)"
25
+ * pointers and that name build terms.
26
+ * The warning goes through the PostToolUse additionalContext channel. Once per file per
27
+ * session (state file in the OS temp dir, keyed on session id).
28
+ *
29
+ * It never blocks. It always exits 0 and fails open on any error.
30
+ *
31
+ * Escape hatch: set TOGA_MEMORY_SCOPE_GUARD_DISABLED=1 to silence the warning.
32
+ */
33
+
34
+ const fs = require('fs');
35
+ const os = require('os');
36
+ const path = require('path');
37
+
38
+ const RULE_DOC = 'rules/common/memory-scope.md';
39
+ const MEMORY_INDEX_NAME = 'memory.md';
40
+ const MAX_SEEN_PATHS = 500;
41
+
42
+ const MEMORY_DIR_RE = /(^|\/)\.claude\/(projects\/[^/]+\/)?memory\//i;
43
+ const PERSONAL_NAME_RE = /^(user-.*|repo-path-.*|habit-.*|.*-profile)$/i;
44
+ const LOCATION_NAME_RE = /(^|-)(path|paths|location|dir|folder)(-|$)/i;
45
+ const DIRECTIVE_RE = /\b(never|always|must(?: not)?|do not|don't|should(?: not)?|required)\b/i;
46
+ const WHY_LINE_RE = /^\s*\**why\**\s*:/i;
47
+ // Sentence or clause: split on . ! ? ; and line breaks.
48
+ const SENTENCE_SPLIT_RE = /(?<=[.!?;])\s+|\r?\n/;
49
+ // Asides in (...) describe, they do not set rules — e.g. "(so don't break it)".
50
+ const PARENTHETICAL_RE = /\([^()]*\)/g;
51
+ // [[note-name]] links name other notes; their words are not this note's rule.
52
+ const WIKI_LINK_RE = /\[\[[^\]]*\]\]/g;
53
+ const LIVES_IN_RE = /^\s*\**lives in\**\s*:/im;
54
+ const INDEX_POINTER_RE = /^\s*-\s*\[[^\]]+\]\([^)]+\)/;
55
+ const BUILD_TERMS_RE = new RegExp(
56
+ '\\b(' + [
57
+ 'SQL', 'dbchanges2', 'api2', '_underscore', 'interceptors?', 'ACL', 'RecordFields',
58
+ 'blox', 'migrations?', 'endpoints?', 'scripted', 'worker2', 'NetSuite',
59
+ 'schema', 'components?',
60
+ ].join('|') + ')\\b',
61
+ 'i'
62
+ );
63
+ // "surface" is also a common verb; only the capitalised platform layer name counts.
64
+ const SURFACE_TERM_RE = /\b(Surface)\b/;
65
+
66
+ function findBuildTerm(text) {
67
+ return text.match(BUILD_TERMS_RE) || text.match(SURFACE_TERM_RE);
68
+ }
69
+
70
+ function readPayload() {
71
+ let raw = '';
72
+ try { raw = fs.readFileSync(0, 'utf8'); } catch (e) { /* no stdin */ }
73
+ let data = {};
74
+ if (raw && raw.trim()) {
75
+ try { data = JSON.parse(raw); } catch (e) { data = {}; }
76
+ }
77
+ if (!data.tool_name && process.env.CLAUDE_TOOL_NAME) data.tool_name = process.env.CLAUDE_TOOL_NAME;
78
+ if (!data.tool_input && process.env.CLAUDE_TOOL_INPUT) {
79
+ try { data.tool_input = JSON.parse(process.env.CLAUDE_TOOL_INPUT); } catch (e) { /* ignore */ }
80
+ }
81
+ if (!data.session_id) data.session_id = process.env.CLAUDE_SESSION_ID || process.env.CLAUDE_CODE_SESSION_ID;
82
+ return data;
83
+ }
84
+
85
+ /* Text this tool call added: Write content, Edit new_string, MultiEdit edits[].new_string. */
86
+ function collectNewText(input) {
87
+ const parts = [];
88
+ if (typeof input.content === 'string') parts.push(input.content);
89
+ if (typeof input.new_string === 'string') parts.push(input.new_string);
90
+ if (Array.isArray(input.edits)) {
91
+ for (const e of input.edits) if (e && typeof e.new_string === 'string') parts.push(e.new_string);
92
+ }
93
+ return parts.join('\n');
94
+ }
95
+
96
+ /* Split a note into { front, body }. `front` is the raw frontmatter text ('' if none). */
97
+ function splitFrontmatter(text) {
98
+ const m = text.match(/^\uFEFF?---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
99
+ return m ? { front: m[1], body: m[2] } : { front: '', body: text };
100
+ }
101
+
102
+ /* `type:` may sit at the top level or nested under `metadata:`. */
103
+ function frontmatterValue(front, key) {
104
+ const m = front.match(new RegExp('^\\s*' + key + '\\s*:\\s*["\']?([^"\'\\r\\n]*)', 'im'));
105
+ return m ? m[1].trim() : '';
106
+ }
107
+
108
+ /* First sentence that states a rule about build work: a directive word (never / always /
109
+ * must / do not ...) AND a build term in the same sentence. Returns { directive, term,
110
+ * sentence } or null. Lookups (paths, profiles, registries) and status/reminder notes
111
+ * rarely pair the two, so they stay quiet. "Why:" lines quote the reason, not the rule. */
112
+ function findRuleSentence(body) {
113
+ const lines = body.split(/\r?\n/).filter((l) => !WHY_LINE_RE.test(l));
114
+ const text = lines.join('\n').replace(WIKI_LINK_RE, ' ').replace(PARENTHETICAL_RE, ' ');
115
+ for (const sentence of text.split(SENTENCE_SPLIT_RE)) {
116
+ const directive = sentence.match(DIRECTIVE_RE);
117
+ if (!directive) continue;
118
+ const term = findBuildTerm(sentence);
119
+ if (term) return { directive: directive[1], term: term[1], sentence: sentence.trim() };
120
+ }
121
+ return null;
122
+ }
123
+
124
+ /* Returns a short reason string when the note looks like a team rule, else ''. */
125
+ function teamRuleReason(fileText, fileBase) {
126
+ const { front, body } = splitFrontmatter(fileText);
127
+ const name = frontmatterValue(front, 'name') || fileBase;
128
+ if (PERSONAL_NAME_RE.test(name) || PERSONAL_NAME_RE.test(fileBase)) return '';
129
+ if (LOCATION_NAME_RE.test(name) || LOCATION_NAME_RE.test(fileBase)) return '';
130
+
131
+ const type = frontmatterValue(front, 'type').toLowerCase();
132
+ if (type === 'user') return '';
133
+ if (LIVES_IN_RE.test(body)) return '';
134
+
135
+ const hit = findRuleSentence(body);
136
+ if (!hit) return '';
137
+ return '`type: ' + (type || 'none') + '` note states a build rule ("' + hit.directive +
138
+ '" + "' + hit.term + '")';
139
+ }
140
+
141
+ /* MEMORY.md: warn only for added lines that are not index pointers and name build terms. */
142
+ function indexRuleReason(newText) {
143
+ for (const line of newText.split(/\r?\n/)) {
144
+ const t = line.trim();
145
+ if (!t || t.startsWith('#') || INDEX_POINTER_RE.test(t)) continue;
146
+ const term = findBuildTerm(t);
147
+ if (term) return 'MEMORY.md line that reads like a rule, not a pointer ("' + term[1] + '")';
148
+ }
149
+ return '';
150
+ }
151
+
152
+ function sessionKey(data) {
153
+ const sid = (data && data.session_id) || String(process.ppid || process.pid);
154
+ return String(sid).replace(/[^A-Za-z0-9_-]/g, '').slice(0, 64) || 'x';
155
+ }
156
+
157
+ /* True the first time this path is seen this session; records it. */
158
+ function firstTimeThisSession(data, normPath) {
159
+ const sf = path.join(os.tmpdir(), 'toga-memory-scope-' + sessionKey(data) + '.json');
160
+ let seen = [];
161
+ try { seen = JSON.parse(fs.readFileSync(sf, 'utf8')) || []; } catch (e) { seen = []; }
162
+ if (!Array.isArray(seen)) seen = [];
163
+ if (seen.includes(normPath)) return false;
164
+ seen.push(normPath);
165
+ try { fs.writeFileSync(sf, JSON.stringify(seen.slice(-MAX_SEEN_PATHS))); } catch (e) { /* non-fatal */ }
166
+ return true;
167
+ }
168
+
169
+ function warning(filePath, reason) {
170
+ return [
171
+ '<system-reminder>',
172
+ 'MEMORY SCOPE: ' + path.basename(filePath) + ' looks like a TEAM RULE (' + reason + '),',
173
+ 'but local memory is private to this machine — teammates never see it.',
174
+ 'Put the rule in the team KB (or, for a platform app, its architecture docs in every',
175
+ 'platform app: toga25-desk, toga25-iq, toga2-template) in this same change.',
176
+ 'Keep only a one-line pointer in memory, e.g. "Lives in: <KB doc or app doc § section>".',
177
+ 'If this really is personal (talking style, local path, personal habit), ignore this.',
178
+ 'Rule: ' + RULE_DOC + '.',
179
+ '</system-reminder>',
180
+ ].join('\n');
181
+ }
182
+
183
+ function main() {
184
+ if (process.env.TOGA_MEMORY_SCOPE_GUARD_DISABLED === '1') return;
185
+
186
+ const data = readPayload();
187
+ if (!/^(Write|Edit|MultiEdit)$/.test(data.tool_name || '')) return;
188
+
189
+ const input = data.tool_input || {};
190
+ const filePath = typeof input.file_path === 'string' ? input.file_path : '';
191
+ if (!filePath) return;
192
+
193
+ const norm = filePath.replace(/\\/g, '/');
194
+ if (!MEMORY_DIR_RE.test(norm) || !/\.md$/i.test(norm)) return;
195
+
196
+ const base = path.basename(norm);
197
+ let reason;
198
+ if (base.toLowerCase() === MEMORY_INDEX_NAME) {
199
+ reason = indexRuleReason(collectNewText(input));
200
+ } else {
201
+ let text = '';
202
+ try { text = fs.readFileSync(path.resolve(data.cwd || process.cwd(), filePath), 'utf8'); } catch (e) {
203
+ text = collectNewText(input); // file unreadable — judge what was written
204
+ }
205
+ reason = teamRuleReason(text, base.replace(/\.md$/i, ''));
206
+ }
207
+ if (!reason) return;
208
+
209
+ if (!firstTimeThisSession(data, norm.toLowerCase())) return;
210
+
211
+ console.log(JSON.stringify({
212
+ hookSpecificOutput: {
213
+ hookEventName: 'PostToolUse',
214
+ additionalContext: warning(filePath, reason),
215
+ },
216
+ }));
217
+ }
218
+
219
+ try { main(); } catch (e) { /* fail-open: a warning hook must never brick a session */ }
220
+ process.exitCode = 0; // always 0; exitCode (not exit()) lets stdout flush on Windows pipes
@@ -121,6 +121,26 @@ That is the entire main-thread analysis job: summarize, do not investigate the K
121
121
  Never write `toga25-blox`'s inventory contents into the KB — it is generated-from-code and
122
122
  lives only in the repo (see CONVENTIONS → "Where knowledge lives").
123
123
 
124
+ ## Step 2b — Sweep this session's memory notes for team rules (main thread)
125
+
126
+ Local memory is private; team rules belong in the KB (see `rules/common/memory-scope.md`).
127
+
128
+ 1. List the memory notes added or changed **this session**: files in the project's memory
129
+ folder (`~/.claude/projects/<project>/memory/*.md`) whose frontmatter `originSessionId`
130
+ matches this session, or whose `modified` time / file time is inside this session.
131
+ Skip `MEMORY.md`, `type: user` notes, and `user-*` / `repo-path-*` / `*-profile` notes.
132
+ 2. For each note that holds a **team rule** (how we build: architecture, API shape,
133
+ SQL/dbchanges2, ACL, data model, UI/design process, reuse, sync, harness) and has no
134
+ `Lives in:` line, add it to the digest's `changes[]` as **Decided**, with the rule text and
135
+ `files:` = the code/doc area it governs. For a platform-app rule, say it must go into the
136
+ architecture docs of every platform app (`toga25-desk`, `toga25-iq`, `toga2-template`) and
137
+ write those app docs yourself in this step (they live in the app repos, not the KB).
138
+ 3. **After Step 5** (once you know where each rule landed), shrink each of those memory notes
139
+ to its frontmatter plus one line: `Lives in: <KB doc path or app doc § section>`. Keep the
140
+ `MEMORY.md` pointer line.
141
+
142
+ If no memory note this session holds a team rule, skip this step.
143
+
124
144
  ## Step 3 — Delegate to the `session-capture` subagent
125
145
 
126
146
  Spawn the subagent via the **Agent tool** with `subagent_type: session-capture`, passing the
@@ -177,7 +197,8 @@ person across sessions" in `rules/common/communication.md`.)
177
197
  unless I say so", "investigate before asking me", "run the DB query yourself").
178
198
  2. Update their profile in **local Claude memory** — talking style in a `user`-type note named
179
199
  `user-communication-profile`; each working habit as its own memory note (with its `MEMORY.md`
180
- pointer). Add any clear new signal; don't duplicate what's already there.
200
+ pointer). Add any clear new signal; don't duplicate what's already there. Save these as
201
+ `type: user`. A rule about how we build is **not** a habit — it goes to the KB (Step 2b).
181
202
  3. **Only record clear signals** — prefer what they said directly over what you infer. Keep the
182
203
  notes short. If nothing new came up, do nothing.
183
204