toga-ai 1.0.756 → 1.0.758

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.
@@ -112,6 +112,36 @@
112
112
  "timeout": 3000
113
113
  }
114
114
  ]
115
+ },
116
+ {
117
+ "matcher": "Write|Edit|MultiEdit",
118
+ "hooks": [
119
+ {
120
+ "type": "command",
121
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/remind-frontend-review.js\"",
122
+ "timeout": 3000
123
+ }
124
+ ]
125
+ },
126
+ {
127
+ "matcher": "Write|Edit|MultiEdit",
128
+ "hooks": [
129
+ {
130
+ "type": "command",
131
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/sql-cross-cluster-check.js\"",
132
+ "timeout": 3000
133
+ }
134
+ ]
135
+ },
136
+ {
137
+ "matcher": "Write|Edit|MultiEdit",
138
+ "hooks": [
139
+ {
140
+ "type": "command",
141
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/dbchanges2-file-sprawl.js\"",
142
+ "timeout": 3000
143
+ }
144
+ ]
115
145
  }
116
146
  ],
117
147
  "SessionStart": [
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-02
9
+ updated: 2026-09-03
10
10
  owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
@@ -88,9 +88,27 @@ The single most expensive misreading of this engine. `startModeIteration()` writ
88
88
  `<seconds>-RUNNING` at the **start** of every section and `finishModeIteration()` writes `-IDLE` at
89
89
  the end; the ÷3 `REDUCE_INTERVAL_FACTOR` shrink fires only because the *previous* run never reached
90
90
  its IDLE write. **There is no timeout, memory cap, or resource limit anywhere in the script** — so
91
- "didn't finish" can only mean **it threw**. A collapsing window therefore means *go read Sentry*,
92
- never *tune the window size down*. (Corollary: a window that has grown back to the cap is positive
93
- evidence the section is completing.)
91
+ "didn't finish" can only mean **it threw**. A collapsing window therefore means *go read the error
92
+ in prod* (see below), never *tune the window size down*. (Corollary: a window that has grown back to
93
+ the cap is positive evidence the section is completing.)
94
+
95
+ **Where the error is: prod `Logs.Issue` / `Logs.Event`, NOT Sentry.** Read worker errors from the
96
+ production log DB by default — you do not need a developer to point you there. The base `Logs`
97
+ schema (prod-logs cluster) holds `Issue` (deduped, with `errorMessage` + `trace`) and `Event` (each
98
+ occurrence, with `clientId`, `errorMessage`, `trace`, and a `context` JSON carrying `errorFile` +
99
+ `errorLine`). Find a sync crash like this (NYCHH `clientId = 3`):
100
+
101
+ ```sql
102
+ SELECT e.dtCreated, e.issueId, e.errorMessage, e.context
103
+ FROM Event e
104
+ WHERE e.clientId = 3 AND e.dtCreated > NOW() - INTERVAL 2 DAY
105
+ ORDER BY e.dtCreated DESC;
106
+ ```
107
+
108
+ Filter on the `context.REFERENCE.areaPath` = `crons/toga2/netsuite`. **The aggregated `Issue.trace`
109
+ can show an OLD line; trust the newest `Event.context.errorLine` for the real spot.** `context` also
110
+ gives `memoryPeakMb` — a small peak (e.g. 20 MB) rules out "giant record / OOM" and points at a
111
+ plain thrown warning instead.
94
112
 
95
113
  ### Per-record resume cursor — checkpoint after EACH record, not window-end (LIVE 2026-09-02)
96
114
 
@@ -151,6 +169,39 @@ back in the account **display** format (here `M/D/YYYY`, date-only). Proven: `SE
151
169
  but the same trap if either ever becomes one.
152
170
  - Deployed 2026-09-02 (worker + library re-deploy; both files had changed).
153
171
 
172
+ ### ⚠ The by-reference refetch dropped `lastModifiedDate` → ITEM_FULFILLMENTS + INVENTORY_ADJUSTMENTS froze (found 2026-09-03, NYCHH prod)
173
+
174
+ The per-record cursor introduced a trap specific to the two sections whose `sync*FromNetsuite()`
175
+ **refetches the record by reference**. `syncItemFulfillmentFromNetsuite(&$nsItemFulfillment, …)`
176
+ (`library/app/api/toga2.php:4535`) and `syncInventoryAdjustmentFromNetsuite(&$nsInventoryAdjustment,
177
+ …)` (`:5655`) each **replace the caller's object** with a fetch-by-id shim
178
+ (`fetchItemFulfillmentById` / `fetchInventoryAdjustmentById`). Those two fetch shims **did not set
179
+ `->lastModifiedDate`**. So the per-record cursor PUT right after the call
180
+ (`common_sync_togasupply.php:1235` fulfillments, `:1323` inventory adjustments) read
181
+ `$record->lastModifiedDate` on the **replaced** object → *Undefined property: stdClass::$lastModifiedDate*
182
+ → 1.0 `App_Error::handleError` → `exit` → the section died **before** its cursor PUT and
183
+ `finishModeIteration()`, freezing it at RUNNING with the watermark stuck years back. Confirmed on
184
+ NYCHH prod (`Logs.Issue` 649, `clientId 3`): fulfillments frozen ~2023-01-23, inventory adjustments
185
+ ~2022-12-29, both windows shrinking. `memoryPeakMb` was 20 — a fast thrown warning, **not** a
186
+ giant-record/OOM. **GroWrk was the reported alert** (ITEM_FULFILLMENTS re-throwing every 5 min
187
+ since the 2026-09-02 per-record cursor deploy). Because the fix is in **shared library code**, all
188
+ four active togasupply clients that hit these sections were affected: **GroWrk, NYCHH,
189
+ Prudential-EDI, S&P Global**.
190
+
191
+ - **ITEM_RECEIPTS was never affected**: `fetchItemReceiptById` already set `lastModifiedDate`, so its
192
+ cursor PUT succeeded (a real `…|<internalId>` value) while the other two carried the migration's
193
+ `…|0` and never moved.
194
+ - **The `|0` is NOT the bug.** It is the intended empty-window marker (see the per-record cursor
195
+ section). The frozen sections showed `|0` only because they never completed a record since the
196
+ 2026-09-02 rename.
197
+ - **Fix (library only, `php -l` clean):** set `$shim->lastModifiedDate` in **both**
198
+ `fetchItemFulfillmentById` (from the REST header's `lastModifiedDate`) and
199
+ `fetchInventoryAdjustmentById` (add `lastmodifieddate` to its SuiteQL SELECT, then set the shim).
200
+ No worker change, no SQL, fail-loud preserved. After deploy, the two sections resume and backfill
201
+ the gap on their own (idempotent) — rewinding the cursor is optional to speed catch-up.
202
+ - **Lesson:** any `sync*FromNetsuite()` that takes the record `&$byReference` and refetches must have
203
+ its fetch-by-id shim carry **every** field the caller reads afterward — `lastModifiedDate` first.
204
+
154
205
  ### ⚠ Per-record isolation is SECTION-SPECIFIC — and SALES_ORDERS has NONE (corrected 2026-08-17)
155
206
 
156
207
  Whether a bad record is "stepped over" or freezes the section depends entirely on whether **that
@@ -1124,6 +1175,22 @@ the code deploy).
1124
1175
 
1125
1176
  ## Change history
1126
1177
 
1178
+ - 2026-09-03 — **By-reference refetch dropped `lastModifiedDate` → ITEM_FULFILLMENTS + INVENTORY_ADJUSTMENTS
1179
+ froze (fixed, `library` only).** GroWrk's ITEM_FULFILLMENTS was re-throwing every 5 min since the
1180
+ 2026-09-02 per-record cursor deploy (`Undefined property: stdClass::$lastModifiedDate` at
1181
+ `common_sync_togasupply.php:1235`). Root cause: `syncItemFulfillmentFromNetsuite(&$rec)` and
1182
+ `syncInventoryAdjustmentFromNetsuite(&$rec)` **replace** the by-ref record with a fetch-by-id shim
1183
+ (`fetchItemFulfillmentById` / `fetchInventoryAdjustmentById` in `rest.php`), and neither shim set
1184
+ `->lastModifiedDate` — so the per-record cursor PUT that reads it after the call threw → `App_Error`
1185
+ `exit()` → section died before `finishModeIteration()` → frozen RUNNING, window ÷3. `fetchItemReceiptById`
1186
+ already set it, so ITEM_RECEIPTS was never hit; INVENTORY_ADJUSTMENTS was latent (no client had processed
1187
+ one since the rename). Shared code → all four active togasupply clients affected (GroWrk, NYCHH,
1188
+ Prudential-EDI, S&P Global); no data lost (the record write commits each run), self-heals after deploy.
1189
+ Fix: set `$shim->lastModifiedDate` in both shims (add `lastmodifieddate` to InventoryAdjustment's SuiteQL
1190
+ SELECT). `php -l` clean; no worker/SQL change. Lesson: any `sync*FromNetsuite()` that refetches its
1191
+ `&$byReference` record must have its fetch-by-id shim carry every field the caller reads afterward. Full
1192
+ detail in *How it works* above; shim-side entry on the
1193
+ [SuiteQL/REST shim doc](../../library/features/netsuite-suiteql-rest-shim.md). (jcardinal)
1127
1194
  - 2026-09-02 — **Per-record cursor was storing a date-ONLY, US-format value — FIXED + DEPLOYED (shared:
1128
1195
  `worker` + `library`).** The datetime half was `M/D/YYYY` with no time (e.g. `9/2/2026|6302323`), so the
1129
1196
  cursor sat at midnight and the same-second id tiebreak never matched — every run re-fetched the whole day.
@@ -72,6 +72,20 @@ team-maintained `id`s** — ask the developer for the next value before insertin
72
72
  > trailing-space quirk like `2026-06-03- BLANK_CLIENT_DATABASE.sql`). The **only** hard rule
73
73
  > is the leading `YYYY-MM-DD<letter>` so ordering holds.
74
74
 
75
+ ## One file per type per build (keep change files consolidated)
76
+
77
+ **Prefer a single `.sql` file per database type (folder) per build.** When a session produces
78
+ several changes for the same target — e.g. three separate files under `Core/`, or four under
79
+ `Client/` — **consolidate them into one dated file** for that folder instead. Multiple same-type
80
+ files in one build are not forbidden, but they have become too common; keep it to one per type
81
+ unless they genuinely must be separate (for example, an ordering gap where another folder's file
82
+ must run between them).
83
+
84
+ - Combine same-folder statements into one `YYYY-MM-DD<letter> - <Description>.sql`.
85
+ - Different folders (types) stay in their own files — that split is required (see isolation).
86
+ - The `dbchanges2-file-sprawl` hook warns when a second file of the same type is created in a
87
+ session, as a reminder to consolidate.
88
+
75
89
  ## Folder → database mapping
76
90
 
77
91
  Each top-level folder targets a database or a **fan-out set** of databases. Tenant naming
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.756",
3
+ "version": "1.0.758",
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",
@@ -0,0 +1,48 @@
1
+ # Auto-Review Rules
2
+
3
+ After you write or change code, run the right reviewer yourself — **as a sub-agent** — before
4
+ you call the work done. The developer should not have to ask. Running reviews off the main
5
+ thread also keeps the main conversation small.
6
+
7
+ ## Run the matching reviewer — always, automatically
8
+
9
+ | You wrote… | Run this reviewer (as a sub-agent) |
10
+ |---|---|
11
+ | PHP | `php-reviewer` (TOGA) |
12
+ | A **new** PHP class | also `framework-pattern-checker` (TOGA) |
13
+ | SQL — a `.sql` file, or SQL inside PHP | `sql-reviewer` (TOGA) |
14
+ | React — `.jsx` / `.tsx` | `ecc:react-reviewer` |
15
+ | TypeScript (non-React) — `.ts` | `ecc:typescript-reviewer` |
16
+ | Anything touching auth, user input, credentials, or queries | also `cso` (security) |
17
+
18
+ Rules:
19
+
20
+ - **Run it in a sub-agent, not the main thread.** Run independent reviews at the same time
21
+ (one message, several agents) so the main conversation stays lean.
22
+ - **Run it before you commit, ship, or say "done".** If it finds real problems, fix them and
23
+ re-run until it is clean.
24
+ - **The reviewer must check the code against our team standards** — the rules in `rules/` and
25
+ the standards in the knowledge base. Point it at the standard for that language. For SQL that
26
+ includes the escaping rules and the "never mix two database families in one query" rule
27
+ (see `security.md` and the `_underscore` / `dbchanges2` architecture docs).
28
+ - **Opt-out:** if the developer says "skip review", skip it for that change. Otherwise it runs
29
+ on its own.
30
+
31
+ ## Get a CTO second opinion for moderate or bigger changes
32
+
33
+ Run the `cto` agent for an independent second opinion — on your own, in a sub-agent — when a
34
+ change is **moderate or complex**. That means any of:
35
+
36
+ - it touches more than ~2 files,
37
+ - it is a schema or migration change (`dbchanges` / `dbchanges2`),
38
+ - it is a new feature, or
39
+ - it is hard to undo (a data migration, an API or contract change, an architecture choice).
40
+
41
+ Do this **before you finalize the approach** — for a design choice, before you write the code.
42
+ The developer should not have to ask for it.
43
+
44
+ ## Why
45
+
46
+ These reviews and the CTO opinion catch framework breaks, security holes, and bad calls before
47
+ they ship. Making them automatic — and running them off the main thread — means the developer
48
+ gets the safety without asking, and the main conversation stays small.
@@ -73,21 +73,34 @@ back to dense wording and teammates missed critical details. To stop that, a hoo
73
73
  - The hook is wired in `.claude/settings.json` and ships to every repo on `npx toga-ai`.
74
74
  - It never blocks anything; it only nudges. To silence it, set `TOGA_PLAIN_TALK_DISABLED=1`.
75
75
 
76
- ## Remember each person across sessions
77
-
78
- The "mirror the person" rule (rule 7) only sees the current session. That is not enough —
79
- you should learn how each developer likes to be talked to and remember it **for good**, so
80
- they never have to tell you twice. Use the developer's **local Claude memory** for this.
81
-
82
- - **Keep a per-person communication profile in local memory** — e.g. a `user`-type memory
83
- named `user-communication-profile`. It loads at the start of every session, so you apply
84
- their known style before you write a word.
85
- - **Save a preference the moment they state it — do not wait to be asked.** If a developer
86
- says "speak plainly", "don't use the word X", "keep it short", or corrects your wording,
87
- write it into their profile right then. An explicit preference is the strongest signal.
88
- - **At `/capture`, review and update the profile** with any clear new pattern in how they
89
- communicated this session (see the capture skill's step for this).
90
- - **Only record clear signals.** Prefer what they say directly over what you infer. Keep the
91
- note short and readable; a wrong guess is worse than no guess.
92
- - **This profile is private and per-person. Never put it in the team knowledge base** — it
93
- lives only in that developer's local memory, on their machine.
76
+ ## Remember each person across sessions — style AND how they like to work
77
+
78
+ The "mirror the person" rule (rule 7) only sees the current session. That is not enough.
79
+ Actively learn how each developer likes to be talked to **and how they like you to work**, and
80
+ remember it **for good**, so they never have to tell you twice. Store it in the developer's
81
+ **local Claude memory**. Build this profile on purpose — do not wait to be asked to remember.
82
+
83
+ Capture **two** kinds of things:
84
+
85
+ 1. **How they like to be talked to** — plain vs detailed, short vs long, words to avoid, tone.
86
+ 2. **How they like you to work** — the standing habits they expect every session. For example:
87
+ - checks they always want run (a lint, a specific review, a security pass, a test),
88
+ - tools or commands they prefer, or forbid,
89
+ - how they want results shown (deploy steps first, SQL file + order, and so on),
90
+ - workflow do's and don'ts ("don't commit unless I say so", "investigate before asking me",
91
+ "always run the DB query yourself").
92
+
93
+ How to keep it:
94
+
95
+ - **Save it the moment they state it — don't wait to be asked.** An explicit preference, or a
96
+ correction of something you did, is the strongest signal. Write it down right then.
97
+ - **At `/capture`, review and update** with any clear new pattern from the session (see the
98
+ capture skill's step for this).
99
+ - **Only record clear signals.** Prefer what they say directly over what you infer. Keep each
100
+ note short; a wrong guess is worse than no guess.
101
+ - **Store it as local memory, per person.** A `user`-type note like `user-communication-profile`
102
+ holds the talking style; work habits are their own memory notes (one habit per note, the way
103
+ the memory system already loads them every session). Both load at session start, so you apply
104
+ the developer's known style and habits before you write a word.
105
+ - **This profile is private and per-person. Never put it in the team knowledge base** — it lives
106
+ only in that developer's local memory, on their machine.
@@ -73,6 +73,25 @@ One caveat on 2.0's helpers: `_Database::escape()` is the correct one. Do **not*
73
73
  `_Database::protect()`, which *strips* `% \ / * " '` and the literal ` or` — lossy filtering, not
74
74
  escaping, and it will corrupt data you meant to store intact.
75
75
 
76
+ ## Never query across database clusters (2.0)
77
+
78
+ In 2.0 the database families live on **separate production clusters**: `Core` / `Forecast` /
79
+ `Team` (prod-core), `Client_<Tenant>` (prod-client), `Archive_<Tenant>` (prod-archive), and
80
+ `Logs` / `Logs_<Tenant>` (prod-logs). A single SQL statement that joins or sub-selects across
81
+ two families **cannot run in production** — the other database is on a different server. It
82
+ works locally only because every database shares one endpoint there, so the break shows up
83
+ only after release.
84
+
85
+ - **Never mix two database families (e.g. `Core` and a `Client_<Tenant>` DB) in one query.**
86
+ Keep every query inside one database.
87
+ - To use data from another family, reference it by a stable **id / uuid / slug** and assemble
88
+ the result in **PHP across two `_Database` connections** — one per family — never in one SQL
89
+ statement.
90
+ - In `dbchanges2` this is a **hard rule**, blocked by the `dbchanges2-cluster-isolation` hook.
91
+ In app code (hand-written SQL) it is not blocked, but the `sql-cross-cluster-check` hook
92
+ warns, and the `sql-reviewer` must flag it. See the `_underscore` and `dbchanges2`
93
+ architecture docs.
94
+
76
95
  ## XSS prevention
77
96
 
78
97
  Never output user-supplied data to HTML without escaping.
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * dbchanges2-file-sprawl.js — WARNS when more than one dbchanges2 .sql file of the
6
+ * SAME type (same target folder) is created in a session.
7
+ *
8
+ * WHY: we have seen builds emit 3–4 separate Client (or Core) change files in one
9
+ * conversation. It is not fatal, but it is too much, too often. The standard now
10
+ * asks for one file per type per build — consolidate same-type changes into a
11
+ * single dated file unless they truly must be separate. This hook nudges toward
12
+ * that; it never blocks.
13
+ *
14
+ * "Type" = the top-level folder under dbchanges2 (Core, Logs, Client,
15
+ * Client_<Name>, Logs_Client, _modules/<module>) — the same buckets the isolation
16
+ * hook uses. Two different Client_<Name> folders are different types (different
17
+ * client databases); two files in Core/ are the same type.
18
+ *
19
+ * Session state is a small JSON file in the OS temp dir keyed by the parent pid,
20
+ * mapping type -> [file paths seen]. Fail-open; never blocks.
21
+ *
22
+ * Escape hatch: DBCHANGES2_SPRAWL_DISABLED=1
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const os = require('os');
27
+ const path = require('path');
28
+
29
+ function readPayload() {
30
+ let raw = '';
31
+ try { raw = fs.readFileSync(0, 'utf8'); } catch (e) { /* no stdin */ }
32
+ let data = {};
33
+ if (raw && raw.trim()) {
34
+ try { data = JSON.parse(raw); } catch (e) { data = {}; }
35
+ }
36
+ if (!data.tool_name && process.env.CLAUDE_TOOL_NAME) data.tool_name = process.env.CLAUDE_TOOL_NAME;
37
+ if (!data.tool_input && process.env.CLAUDE_TOOL_INPUT) {
38
+ try { data.tool_input = JSON.parse(process.env.CLAUDE_TOOL_INPUT); } catch (e) {}
39
+ }
40
+ if (!data.session_id) data.session_id = process.env.CLAUDE_SESSION_ID || process.env.CLAUDE_CODE_SESSION_ID;
41
+ return data;
42
+ }
43
+
44
+ /* Key session state on the CC session id (stable for the whole session), not
45
+ * process.ppid — on Windows git-bash ppid varies per spawn, so it is a last resort. */
46
+ function sessionKey(data) {
47
+ const sid = (data && data.session_id) || String(process.ppid || process.pid);
48
+ return String(sid).replace(/[^A-Za-z0-9_-]/g, '').slice(0, 64) || 'x';
49
+ }
50
+
51
+ /* Type bucket = first path segment after /dbchanges2/. Null if not a dbchanges2 file. */
52
+ function resolveType(filePath) {
53
+ const norm = filePath.replace(/\\/g, '/');
54
+ const m = norm.match(/(?:^|\/)dbchanges2\/(.+)$/i);
55
+ if (!m) return null;
56
+ const rel = m[1];
57
+ if (/\/HISTORIC\//i.test(norm)) return null; // archive, never executed
58
+ const seg = rel.split('/')[0];
59
+ if (!seg || seg === rel) return null; // file at repo root, no folder
60
+ if (/^_modules$/i.test(seg)) {
61
+ const parts = rel.split('/');
62
+ return parts[1] ? '_modules/' + parts[1] : '_modules';
63
+ }
64
+ return seg;
65
+ }
66
+
67
+ function stateFile(key) {
68
+ return path.join(os.tmpdir(), 'toga-dbchanges2-sprawl-' + key + '.json');
69
+ }
70
+
71
+ function main() {
72
+ if (process.env.DBCHANGES2_SPRAWL_DISABLED === '1') return;
73
+
74
+ const data = readPayload();
75
+ if (!/^(Write|Edit|MultiEdit)$/.test(data.tool_name || '')) return;
76
+
77
+ const input = data.tool_input || {};
78
+ const filePath = typeof input.file_path === 'string' ? input.file_path : '';
79
+ if (!/\.sql$/i.test(filePath)) return;
80
+
81
+ const type = resolveType(filePath);
82
+ if (!type) return;
83
+
84
+ const normPath = filePath.replace(/\\/g, '/');
85
+ const sf = stateFile(sessionKey(data));
86
+ let state = {};
87
+ try { state = JSON.parse(fs.readFileSync(sf, 'utf8')) || {}; } catch (e) { state = {}; }
88
+
89
+ const seen = Array.isArray(state[type]) ? state[type] : [];
90
+ if (seen.includes(normPath)) return; // same file touched again — not a new file
91
+ seen.push(normPath);
92
+ state[type] = seen;
93
+ try { fs.writeFileSync(sf, JSON.stringify(state)); } catch (e) { /* non-fatal */ }
94
+
95
+ if (seen.length >= 2) {
96
+ console.log(
97
+ 'DBCHANGES2 FILE SPRAWL: this is the ' + seen.length + ' "' + type + '" change file this ' +
98
+ 'session. Standard is ONE file per type per build — prefer consolidating same-type changes ' +
99
+ 'into a single dated file unless they genuinely must be separate (e.g. ordering that needs a ' +
100
+ 'gap). Files so far for ' + type + ':\n' + seen.map((p) => ' ' + p.split('/').pop()).join('\n') +
101
+ '\nSee knowledge/2.0/apps/dbchanges2/architecture.md → "One file per type per build".'
102
+ );
103
+ }
104
+ }
105
+
106
+ try { main(); } catch (e) { /* fail-open */ }
@@ -57,10 +57,12 @@ function reminder() {
57
57
  'register. Reuse their own terms for things. Match their length: if they write short,',
58
58
  'you write short. Never fake misspellings or bad grammar; be natural, just familiar.',
59
59
  '',
60
- 'REMEMBER THEM ACROSS SESSIONS: this developer may have a saved communication profile in',
61
- 'their local Claude memory (e.g. user-communication-profile) — apply it. If they state a',
62
- 'preference (a word to avoid, a length, a tone), save it to that profile right away so it',
63
- 'persists to the next session; do not make them tell you twice.',
60
+ 'REMEMBER THEM ACROSS SESSIONS: this developer has a saved profile in their local Claude',
61
+ 'memory — how they like to be talked to (e.g. user-communication-profile) AND how they like',
62
+ 'you to work (standing checks, preferred/forbidden tools, workflow do\'s and don\'ts, saved',
63
+ 'as memory notes). Apply it. If they state any preference — a word to avoid, a length, a',
64
+ 'tone, a check they always want, a habit — save it to memory right away so it persists to',
65
+ 'the next session; do not make them tell you twice.',
64
66
  '',
65
67
  'Before you send: could you cut half the words and keep the meaning? Is any word too',
66
68
  'rare for a non-native reader? Is the main point on the first line?',
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * PostToolUse (Write|Edit|MultiEdit) hook: front-end review reminder.
5
+ *
6
+ * PHP and SQL already have review reminders (remind-php-review.js,
7
+ * remind-sql-review.js). This covers the gap: React and TypeScript. When a write
8
+ * touches a React (.jsx/.tsx) or TypeScript (.ts) file, it emits a directive to
9
+ * run the matching reviewer AS A SUB-AGENT before the change is done. A
10
+ * deterministic hook cannot spawn the (LLM) reviewer itself, so it forces the
11
+ * routing instead. See rules/common/auto-review.md.
12
+ *
13
+ * Plain .js/.mjs/.cjs is intentionally NOT matched — in this codebase that is
14
+ * mostly tooling, config, and hook scripts, so reminding on it would be noise.
15
+ *
16
+ * Escape hatch: TOGA_FRONTEND_REVIEW_DISABLED=1
17
+ */
18
+
19
+ if (process.env.TOGA_FRONTEND_REVIEW_DISABLED === '1') process.exit(0);
20
+
21
+ const input = (() => {
22
+ try { return JSON.parse(process.env.CLAUDE_TOOL_INPUT || '{}'); } catch { return {}; }
23
+ })();
24
+
25
+ const filePath = typeof input.file_path === 'string' ? input.file_path : '';
26
+
27
+ if (/\.(jsx|tsx)$/i.test(filePath)) {
28
+ console.log(
29
+ 'REACT file edited (' + filePath + '). You MUST run the ecc:react-reviewer agent ' +
30
+ '(as a sub-agent) on this change before treating it as done — hook correctness, render ' +
31
+ 'performance, server/client boundaries, and our team standards. See rules/common/auto-review.md.'
32
+ );
33
+ } else if (/\.ts$/i.test(filePath)) {
34
+ console.log(
35
+ 'TYPESCRIPT file edited (' + filePath + '). You MUST run the ecc:typescript-reviewer agent ' +
36
+ '(as a sub-agent) on this change before treating it as done — type safety, async correctness, ' +
37
+ 'and our team standards. See rules/common/auto-review.md.'
38
+ );
39
+ }
@@ -2,11 +2,30 @@
2
2
  const os = require('os');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const scopeFile = path.join(os.tmpdir(), 'toga-scope-' + (process.ppid || process.pid) + '.count');
5
+
6
+ /* Key the per-session counter on the CC session id — stable for the whole session
7
+ * and passed to hooks on stdin (and often in env). process.ppid is NOT reliable as
8
+ * a session key (on Windows git-bash it varies per spawn), so it is only the last
9
+ * resort. */
10
+ function sessionKey() {
11
+ let sid = '';
12
+ try {
13
+ const raw = fs.readFileSync(0, 'utf8');
14
+ if (raw && raw.trim()) sid = JSON.parse(raw).session_id || '';
15
+ } catch (e) { /* no stdin */ }
16
+ sid = sid || process.env.CLAUDE_SESSION_ID || process.env.CLAUDE_CODE_SESSION_ID ||
17
+ String(process.ppid || process.pid);
18
+ return sid.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 64) || 'x';
19
+ }
20
+
21
+ const scopeFile = path.join(os.tmpdir(), 'toga-scope-' + sessionKey() + '.count');
6
22
  let count = 0;
7
23
  try { count = parseInt(fs.readFileSync(scopeFile, 'utf8').trim(), 10) || 0; } catch {}
8
24
  count += 1;
9
25
  try { fs.writeFileSync(scopeFile, String(count)); } catch {}
26
+ if (count === 3) {
27
+ console.log('MODERATE CHANGE: 3 files modified this session. Per rules/common/auto-review.md, for a moderate or bigger code change get a CTO second opinion on the approach (cto agent, as a sub-agent) before finalizing — unless the developer said to skip it.');
28
+ }
10
29
  if (count >= 20) {
11
30
  console.log('SCOPE WARNING: ' + count + ' files modified this session. Consider whether changes are too scattered.');
12
31
  }
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * sql-cross-cluster-check.js — best-effort WARNING when one piece of code
6
+ * references two different 2.0 database families in what looks like a single
7
+ * query.
8
+ *
9
+ * WHY: in production the 2.0 database families live on SEPARATE clusters — Core /
10
+ * Forecast / Team, Client_<Tenant>, Archive_<Tenant>, Logs / Logs_<Tenant>, Cache.
11
+ * A query that joins or sub-selects across two of them cannot resolve in
12
+ * production; it only works locally, where every database shares one endpoint. So
13
+ * a query must stay inside ONE database; cross-family data is assembled in PHP
14
+ * across two connections, referencing the other side by id / uuid / slug.
15
+ *
16
+ * SCOPE: this covers HAND-WRITTEN SQL in app code (.php) and .sql files OUTSIDE
17
+ * dbchanges2. dbchanges2 .sql files are HARD-BLOCKED by
18
+ * dbchanges2-cluster-isolation.js — this hook deliberately skips them. Here a hard
19
+ * block would be too blunt (a .php file legitimately runs separate queries on
20
+ * separate connections), so this only WARNS. The sql-reviewer is still expected to
21
+ * confirm.
22
+ *
23
+ * Fail-open; never blocks. Disable: SQL_CROSS_CLUSTER_DISABLED=1
24
+ */
25
+
26
+ const fs = require('fs');
27
+
28
+ function readPayload() {
29
+ let raw = '';
30
+ try { raw = fs.readFileSync(0, 'utf8'); } catch (e) { /* no stdin */ }
31
+ let data = {};
32
+ if (raw && raw.trim()) {
33
+ try { data = JSON.parse(raw); } catch (e) { data = {}; }
34
+ }
35
+ if (!data.tool_name && process.env.CLAUDE_TOOL_NAME) data.tool_name = process.env.CLAUDE_TOOL_NAME;
36
+ if (!data.tool_input && process.env.CLAUDE_TOOL_INPUT) {
37
+ try { data.tool_input = JSON.parse(process.env.CLAUDE_TOOL_INPUT); } catch (e) {}
38
+ }
39
+ return data;
40
+ }
41
+
42
+ const DB_FAMILIES = ['Core', 'Client', 'Logs', 'Archive', 'Cache', 'Team', 'Forecast'];
43
+
44
+ /* Blank ONLY comments — NOT string literals. In PHP the SQL we need to inspect
45
+ * lives inside quoted strings, so stripping strings (as the dbchanges2 .sql hook
46
+ * does) would erase the very query we are checking. Keeping strings can add a rare
47
+ * false positive from a DB name mentioned in an unrelated string, which is fine for
48
+ * a warning. Newlines preserved so line numbers still match. */
49
+ function stripComments(sql) {
50
+ const blanked = (m) => m.replace(/[^\n]/g, ' ');
51
+ return sql
52
+ .replace(/\/\*[\s\S]*?\*\//g, blanked) // /* block */
53
+ .replace(/\/\/[^\n]*/g, blanked) // // line (php)
54
+ .replace(/--[ \t][^\n]*/g, blanked) // -- line (sql)
55
+ .replace(/(^|[^\w$])#[^\n]*/g, (m) => (m[0] === '#' ? blanked(m) : m[0] + blanked(m.slice(1)))); // # line
56
+ }
57
+
58
+ /* Distinct qualified database references, e.g. `Core`.`Records`, Client_Aig.SalesOrders. */
59
+ function findQualifiedDbs(sql) {
60
+ const code = stripComments(sql);
61
+ const names = DB_FAMILIES.join('|');
62
+ const re = new RegExp(
63
+ '`?\\b((?:' + names + ')(?:_[A-Za-z0-9]+)?)`?\\s*\\.\\s*`?[A-Za-z_][A-Za-z0-9_]*`?',
64
+ 'g'
65
+ );
66
+ const seen = new Map(); // full db name -> first matched text
67
+ let hit;
68
+ while ((hit = re.exec(code)) !== null) {
69
+ if (!seen.has(hit[1])) seen.set(hit[1], hit[0].trim());
70
+ }
71
+ return seen;
72
+ }
73
+
74
+ function collectNewText(inp) {
75
+ const parts = [];
76
+ if (typeof inp.content === 'string') parts.push(inp.content);
77
+ if (typeof inp.new_string === 'string') parts.push(inp.new_string);
78
+ if (Array.isArray(inp.edits)) {
79
+ for (const e of inp.edits) if (e && typeof e.new_string === 'string') parts.push(e.new_string);
80
+ }
81
+ return parts.join('\n');
82
+ }
83
+
84
+ function main() {
85
+ if (process.env.SQL_CROSS_CLUSTER_DISABLED === '1') return;
86
+
87
+ const data = readPayload();
88
+ if (!/^(Write|Edit|MultiEdit)$/.test(data.tool_name || '')) return;
89
+
90
+ const input = data.tool_input || {};
91
+ const filePath = typeof input.file_path === 'string' ? input.file_path : '';
92
+ const norm = filePath.replace(/\\/g, '/');
93
+
94
+ if (/(?:^|\/)dbchanges2\//i.test(norm)) return; // hard-blocked elsewhere
95
+ if (!/\.(php|sql)$/i.test(norm)) return;
96
+
97
+ const text = collectNewText(input);
98
+ if (!text.trim()) return;
99
+
100
+ const dbs = findQualifiedDbs(text);
101
+ if (dbs.size < 2) return; // one (or zero) DB referenced — fine
102
+
103
+ const list = [...dbs.entries()].slice(0, 10)
104
+ .map(([db, ex]) => ' ' + ex + ' ← database ' + db)
105
+ .join('\n');
106
+
107
+ console.log(
108
+ 'POSSIBLE CROSS-CLUSTER SQL in ' + (filePath || 'this edit') + '. This code references ' +
109
+ dbs.size + ' different database families:\n' + list + '\n' +
110
+ 'In production these families are on SEPARATE clusters, so a single query spanning them ' +
111
+ 'cannot resolve there — it only works locally. VERIFY each query stays inside ONE database. ' +
112
+ 'If you need data from another family, reference it by id / uuid / slug and assemble the ' +
113
+ 'result in PHP across two _Database connections — never in one SQL statement. Then run the ' +
114
+ 'sql-reviewer agent to confirm. See security.md → "Never query across database clusters".'
115
+ );
116
+ }
117
+
118
+ try { main(); } catch (e) { /* fail-open */ }
@@ -155,24 +155,29 @@ Relay the subagent's final report to the developer: what was written, the publis
155
155
  standards/architecture). If the subagent reported everything was NO-OP, relay: "Nothing here
156
156
  is durable team knowledge — KB unchanged."
157
157
 
158
- ## Step 6 — Update how to talk to this developer (local memory, private — never pushed)
159
-
160
- Capture is also when you record **how the developer likes to be communicated with**, so the
161
- next session already knows. This is a **main-thread, LOCAL memory** action — it never touches
162
- `knowledge/` and is never pushed to the team repo. (See "Remember each person across
163
- sessions" in `rules/common/communication.md`.)
164
-
165
- 1. Look back at how the developer communicated this session — the words they used, the length
166
- and tone they preferred, and any preference they stated ("speak plainly", "don't use the
167
- word X", "keep it short") or any wording of yours they corrected.
168
- 2. Update their per-person profile in **local Claude memory** (a `user`-type memory named
169
- `user-communication-profile`, plus its `MEMORY.md` pointer). Add any clear new signal;
170
- don't duplicate what's already there.
171
- 3. **Only record clear signals** — prefer what they said directly over what you infer. Keep
172
- the note short and readable. If nothing new came up, do nothing.
173
-
174
- An explicit preference should already have been saved the moment it was stated; this step is
175
- the backstop that also catches patterns.
158
+ ## Step 6 — Update the developer's profile: how to talk to them AND how they like to work (local memory, private — never pushed)
159
+
160
+ Capture is also when you record **how the developer likes to be communicated with, and how they
161
+ like you to work**, so the next session already knows. This is a **main-thread, LOCAL memory**
162
+ action — it never touches `knowledge/` and is never pushed to the team repo. (See "Remember each
163
+ person across sessions" in `rules/common/communication.md`.)
164
+
165
+ 1. Look back at the session for **two** kinds of signal:
166
+ - **Talking style** — the words they used, the length and tone they preferred, any preference
167
+ they stated ("speak plainly", "don't use the word X", "keep it short") or any wording of
168
+ yours they corrected.
169
+ - **Working habits** — standing things they expect every session: checks they always want run
170
+ (a lint, a review, a test), tools or commands they prefer or forbid, how they want results
171
+ shown (deploy steps first, SQL file + order), and workflow do's and don'ts ("don't commit
172
+ unless I say so", "investigate before asking me", "run the DB query yourself").
173
+ 2. Update their profile in **local Claude memory** — talking style in a `user`-type note named
174
+ `user-communication-profile`; each working habit as its own memory note (with its `MEMORY.md`
175
+ pointer). Add any clear new signal; don't duplicate what's already there.
176
+ 3. **Only record clear signals** — prefer what they said directly over what you infer. Keep the
177
+ notes short. If nothing new came up, do nothing.
178
+
179
+ Anything they stated explicitly should already have been saved the moment it was said; this step
180
+ is the backstop that also catches patterns.
176
181
 
177
182
  ### After Capture
178
183
 
@@ -410,7 +410,16 @@ loaded here.
410
410
  After priming, analyze what the developer is building and **propose a concrete plan
411
411
  before writing code**. The plan's *shape* depends on size — and the default for anything
412
412
  non-trivial is to **orchestrate specialist subagents**, not to do everything in this
413
- thread. This keeps the main conversation as the orchestrator (light, durable) while the
413
+ thread.
414
+
415
+ > **Standing directive — auto-review and CTO (see `rules/common/auto-review.md`).** After you
416
+ > write code, run the matching reviewer yourself **as a sub-agent**, before you call it done —
417
+ > PHP → `php-reviewer`, SQL → `sql-reviewer`, React (`.jsx`/`.tsx`) → `ecc:react-reviewer`,
418
+ > TypeScript (`.ts`) → `ecc:typescript-reviewer`, a new PHP class → also
419
+ > `framework-pattern-checker`; anything touching auth/input/credentials/queries → also `cso`.
420
+ > For a **moderate or bigger** change (more than ~2 files, a schema/migration, a new feature,
421
+ > or a hard-to-undo choice) get a `cto` second opinion on the approach first. The developer
422
+ > should not have to ask. Keep all of this off the main thread. This keeps the main conversation as the orchestrator (light, durable) while the
414
423
  heavy reading/writing/verifying happens in subagents — the same discipline as Step 4.
415
424
 
416
425
  The developer does not know what "sub-agents" or "loops" are — never use that jargon.