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.
- package/.claude/settings.json +10 -0
- package/knowledge/1.0/apps/test/features/2-0-deployment-client-sql.md +10 -2
- package/knowledge/1.0/apps/tools/features/multi-client-sql.md +15 -2
- package/knowledge/1.0/apps/tools/features/saml-sso-auth.md +4 -1
- package/knowledge/2.0/apps/_underscore/architecture.md +25 -11
- package/knowledge/2.0/apps/_underscore/features/per-client-database-connections.md +3 -2
- package/knowledge/2.0/apps/dbchanges2/workflows/client-onboarding.md +20 -1
- package/knowledge/2.0/standards/backend-php.md +8 -1
- package/knowledge/CONVENTIONS.md +3 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/standalone/apps/claude/INDEX.md +1 -0
- package/knowledge/standalone/apps/claude/features/memory-scope-guard.md +54 -0
- package/package.json +1 -1
- package/rules/common/communication.md +3 -0
- package/rules/common/memory-scope.md +46 -0
- package/scripts/hooks/memory-scope-guard.js +220 -0
- package/skills/capture/SKILL.md +22 -1
package/.claude/settings.json
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
181
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
`
|
|
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-
|
|
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-
|
|
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-
|
|
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)
|
package/knowledge/CONVENTIONS.md
CHANGED
|
@@ -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**
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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) —
|
|
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
|
@@ -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
|
package/skills/capture/SKILL.md
CHANGED
|
@@ -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
|
|