toga-ai 1.0.755 → 1.0.757
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/tools/features/oneuptime-monitor-status-panel.md +15 -1
- 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: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-03
|
|
10
10
|
owners: [kyalamarthi, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- tools/assets/clickup/sprint-dashboard.html
|
|
@@ -119,6 +119,15 @@ Status bucketing is **derived, not hardcoded**: `isOperationalState` → good; w
|
|
|
119
119
|
non-operational → offline/critical; anything between → degraded. A renamed or new status still
|
|
120
120
|
buckets correctly.
|
|
121
121
|
|
|
122
|
+
**Display label — the UI never shows the word "Offline"; it shows "Outage".** OneUptime's API
|
|
123
|
+
still returns the status name "Offline" and the good/degraded/critical bucketing (from
|
|
124
|
+
`isOperationalState`, not the name) is untouched — the swap is a **display-layer** transform only.
|
|
125
|
+
A helper `outageLabel(name)` replaces the whole word "Offline" (case-insensitive) with "Outage"
|
|
126
|
+
at the per-monitor subtext render (`r.statusName`), so live and dev mock-mode rows match. The two
|
|
127
|
+
hardcoded critical-state labels also read **"OUTAGE"**: the alert popup title (was "OFFLINE") and
|
|
128
|
+
the top-right main band. "DEGRADED" (warning) and "OPERATIONAL" (good) are unchanged. Do not
|
|
129
|
+
rename the raw OneUptime status or the server proxy data to achieve this.
|
|
130
|
+
|
|
122
131
|
### Panel presentation — state-adaptive (supersedes the solid status circle)
|
|
123
132
|
|
|
124
133
|
The old design (a solid `MonitorCircle`, a per-status legend with counts + percentages, and a
|
|
@@ -258,6 +267,11 @@ so a reduced-motion viewer never lands on a fully-invisible glow.
|
|
|
258
267
|
`.alert-flash`, this same rule also covers the slow `alert-breathe` breathe.
|
|
259
268
|
|
|
260
269
|
## Change history
|
|
270
|
+
- 2026-09-03 — UI now shows **"Outage" instead of "Offline"** everywhere. Added a display-layer
|
|
271
|
+
`outageLabel(name)` helper (whole-word, case-insensitive) applied at the per-monitor subtext
|
|
272
|
+
render (`r.statusName`, two spots, covers dev mock rows); set the alert popup title "OFFLINE" →
|
|
273
|
+
"OUTAGE" (top-right band already "OUTAGE"). Raw OneUptime status names and the good/degraded/
|
|
274
|
+
critical role bucketing (from `isOperationalState`) are untouched — display-only. (jcardinal)
|
|
261
275
|
- 2026-08-19 — Removed the monitor panel header entirely. The `mon-head` block — the "Status"
|
|
262
276
|
title (`mon-title`) and the "live · updated Xs ago" meta line (`mon-meta`) — was dropped from
|
|
263
277
|
`MonitorPanel`, together with the now-unused `meta` computation and the `okAt` prop. `now =
|
|
@@ -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.
|