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.
- package/.claude/settings.json +30 -0
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +71 -4
- package/knowledge/2.0/apps/dbchanges2/architecture.md +14 -0
- package/package.json +1 -1
- package/rules/common/auto-review.md +48 -0
- package/rules/common/communication.md +31 -18
- package/rules/common/security.md +19 -0
- package/scripts/hooks/dbchanges2-file-sprawl.js +106 -0
- package/scripts/hooks/plain-talk.js +6 -4
- package/scripts/hooks/remind-frontend-review.js +39 -0
- package/scripts/hooks/scope-counter.js +20 -1
- package/scripts/hooks/sql-cross-cluster-check.js +118 -0
- package/skills/capture/SKILL.md +23 -18
- package/skills/kickoff/SKILL.md +10 -1
package/.claude/settings.json
CHANGED
|
@@ -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-
|
|
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
|
|
92
|
-
never *tune the window size down*. (Corollary: a window that has grown back to
|
|
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
|
@@ -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
|
-
|
|
80
|
-
they never have to tell you twice.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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.
|
package/rules/common/security.md
CHANGED
|
@@ -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
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
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
|
-
|
|
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 */ }
|
package/skills/capture/SKILL.md
CHANGED
|
@@ -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
|
|
159
|
-
|
|
160
|
-
Capture is also when you record **how the developer likes to be communicated with
|
|
161
|
-
next session already knows. This is a **main-thread, LOCAL memory**
|
|
162
|
-
`knowledge/` and is never pushed to the team repo. (See "Remember each
|
|
163
|
-
sessions" in `rules/common/communication.md`.)
|
|
164
|
-
|
|
165
|
-
1. Look back at
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
package/skills/kickoff/SKILL.md
CHANGED
|
@@ -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.
|
|
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.
|