toga-ai 1.0.921 → 1.0.923
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
CHANGED
|
@@ -90,6 +90,16 @@
|
|
|
90
90
|
"timeout": 3000
|
|
91
91
|
}
|
|
92
92
|
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"matcher": "Write|Edit",
|
|
96
|
+
"hooks": [
|
|
97
|
+
{
|
|
98
|
+
"type": "command",
|
|
99
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/platform-reuse-reminder.js\"",
|
|
100
|
+
"timeout": 3000
|
|
101
|
+
}
|
|
102
|
+
]
|
|
93
103
|
}
|
|
94
104
|
],
|
|
95
105
|
"PostToolUse": [
|
|
@@ -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-30
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Monitor/Clickup.php
|
|
@@ -35,6 +35,7 @@ Distinct from the sibling email watchdog [`_Worker_Clickup_Health`](./clickup-co
|
|
|
35
35
|
- `HIGH` (full failure / Offline) — **blind read**: the ClickUp call threw, OR a monitored `togahub.com` webhook has no readable `health.status`. "We don't know the status."
|
|
36
36
|
- `DEGRADED` — a monitored `togahub.com` webhook has `health.status = "suspended"`, OR no `togahub.com` subscription exists at all. A known outage we can see.
|
|
37
37
|
- `OK` — every monitored webhook is readable and not suspended (so `active`, `failing`, any other readable status → OK).
|
|
38
|
+
- **Coverage check** (`evaluateCoverage()`, added 2026-09-25) also → `DEGRADED`: a watched space/event has no covering webhook, a duplicate covering webhook, or a covering webhook with `health.status = "suspended"` (`self::SUSPENDED_STATUS`). `failing` is OK here too (ClickUp is still retrying). Unreadable status is not flagged by coverage — the main loop already makes it `HIGH`.
|
|
38
39
|
- Precedence: `HIGH` > `DEGRADED` > `OK`. `probe` is `OK` on any successful read (even when `alarm=HIGH` from a blind webhook) and `DEGRADED` only when the ClickUp read itself failed.
|
|
39
40
|
- **OneUptime criteria must match the tokens** (config lives outside the repo): Request Body Contains `"alarm":"HIGH"` → Offline + incident; Contains `"alarm":"DEGRADED"` → Degraded; received in 1 min AND Contains `"alarm":"OK"` → Online (OK-gated recovery).
|
|
40
41
|
- **Push URL is an in-file `$ONEUPTIME_URL` local, not `[oneuptime]` config** — the established team convention for every `Worker/Monitor/*` file (Operations, Staples, Compass, Aig, Prudential, Nycdoe, Fleet). A cso review suggested reading it from config; the developer rejected that on purpose — the heartbeat URL is treated as low-sensitivity and stays inline, matching the siblings. See the credential note in [OneUptime push-metric monitors](./oneuptime-worker2-monitoring.md). The `$push` guard still soft-fails on a blank URL so the job can deploy inert.
|
|
@@ -47,6 +48,7 @@ Distinct from the sibling email watchdog [`_Worker_Clickup_Health`](./clickup-co
|
|
|
47
48
|
- Keep the metric POST non-fatal — a monitoring outage must never break the worker job.
|
|
48
49
|
|
|
49
50
|
## Change history
|
|
51
|
+
- 2026-09-30 — Coverage check flagged any covering webhook with status ≠ `active` as degraded, so `failing` (fail_count 1) made the monitor Degraded. Now flags only `suspended`; const `HEALTHY_STATUS` → `SUSPENDED_STATUS`, `CheckWebhook()` uses `self::SUSPENDED_STATUS` (duplicate local removed). `Worker/Clickup/Project.php` (EnsureWebhooks) only reads `degradedHealth` for a message — unaffected. Not yet committed/deployed at capture. (jcardinal)
|
|
50
52
|
- 2026-09-22 — Reworked the alarm rule from "any status ≠ active → DEGRADED" to three levels: `HIGH` on a blind read (ClickUp call failed, or a webhook has no readable `health.status`), `DEGRADED` on `"suspended"` or no `togahub.com` subscription, `OK` for everything else (`active`/`failing`/other). Original was too sensitive. Also hardened: blind-path OneUptime push wrapped in try/catch (non-fatal), `health` guarded against a non-object, stale "Left BLANK" URL comment removed (URL is live). Payload fields changed: `activeCount` → `okCount`, added `suspendedCount`/`blindCount`. OneUptime monitor criteria must be set to match `"alarm":"HIGH"` and `"alarm":"DEGRADED"`. (jcardinal)
|
|
51
53
|
- 2026-09-21 — Created `_Worker_Monitor_Clickup::CheckWebhook` (OneUptime, `*/10`, stricter than the email watchdog). Recreated the inbound webhook under the service account (`pk_87469444`, new id `bd6168ab-…`, deleting the old OAuth-owned one) so both this monitor and `_Worker_Clickup_Health` can see it — the two were previously blind to it (token-ownership gotcha above). (jcardinal)
|
|
52
54
|
|
package/package.json
CHANGED
|
@@ -29,7 +29,8 @@ not a script. When a principle and a real constraint collide, reason it out and
|
|
|
29
29
|
- **Data-drive the UI.** Labels, colors, options, config come from Surface/settings and
|
|
30
30
|
`labelRecordFieldId` — not hardcoded. Model anything data-drivable through those layers.
|
|
31
31
|
- **Reuse before build.** A fundamental shared component (the table first) belongs in `blox`,
|
|
32
|
-
themed per app — not copied.
|
|
32
|
+
themed per app — not copied. Nudged by `scripts/hooks/platform-reuse-reminder.js` on every
|
|
33
|
+
new `src/` file in a platform app or blox (silence: `TOGA_REUSE_REMINDER_DISABLED=1`).
|
|
33
34
|
- **Standard before custom.** Discuss standard-vs-custom before adding any custom field.
|
|
34
35
|
|
|
35
36
|
## Reversibility gate
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* platform-reuse-reminder.js — PRE-CREATE nudge to reuse before building in the 2.0
|
|
6
|
+
* platform apps.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: the platform has ~10 apps sharing one component library
|
|
9
|
+
* (toga25-blox). New pages keep getting fresh components, hooks and styles that already
|
|
10
|
+
* exist in blox or in another app. The "reuse before build" rule
|
|
11
|
+
* (rules/common/architecture-principles.md) is passive; this puts it in context at the
|
|
12
|
+
* moment a new file is about to be created.
|
|
13
|
+
*
|
|
14
|
+
* How it works: registered on PreToolUse for Write|Edit. It fires only when ALL hold:
|
|
15
|
+
* - the target file does NOT exist yet (a new file),
|
|
16
|
+
* - it ends .tsx / .ts / .css (not .test.ts / .d.ts),
|
|
17
|
+
* - it sits under the `src/` of a platform app — a folder whose vite.config.ts calls
|
|
18
|
+
* defineTogaApp — or under the `src/` of the toga25-blox repo itself.
|
|
19
|
+
* Then it adds a short reminder through the PreToolUse additionalContext channel.
|
|
20
|
+
* Once per file path per session (state file in the OS temp dir, keyed on session id).
|
|
21
|
+
*
|
|
22
|
+
* It never blocks. It always exits 0 and fails open on any error.
|
|
23
|
+
*
|
|
24
|
+
* Escape hatch: set TOGA_REUSE_REMINDER_DISABLED=1 to silence the reminder.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
// Single-source platform consolidation rule (lives in toga25-blox).
|
|
32
|
+
const CONSOLIDATION_DOC = 'toga25-blox/docs/consolidation.md';
|
|
33
|
+
const BLOX_INVENTORY = '../toga25-blox/INVENTORY.md';
|
|
34
|
+
const APP_REUSE_DOC = 'docs/architecture/reusable-components.md';
|
|
35
|
+
const BLOX_PACKAGE_NAME = '@toga/blox';
|
|
36
|
+
const BLOX_DIR_NAME = 'toga25-blox';
|
|
37
|
+
const MAX_WALK_UP_LEVELS = 15;
|
|
38
|
+
const MAX_SEEN_PATHS = 500;
|
|
39
|
+
|
|
40
|
+
function readPayload() {
|
|
41
|
+
let raw = '';
|
|
42
|
+
try { raw = fs.readFileSync(0, 'utf8'); } catch (e) { /* no stdin */ }
|
|
43
|
+
let data = {};
|
|
44
|
+
if (raw && raw.trim()) {
|
|
45
|
+
try { data = JSON.parse(raw); } catch (e) { data = {}; }
|
|
46
|
+
}
|
|
47
|
+
if (!data.tool_name && process.env.CLAUDE_TOOL_NAME) data.tool_name = process.env.CLAUDE_TOOL_NAME;
|
|
48
|
+
if (!data.tool_input && process.env.CLAUDE_TOOL_INPUT) {
|
|
49
|
+
try { data.tool_input = JSON.parse(process.env.CLAUDE_TOOL_INPUT); } catch (e) { /* ignore */ }
|
|
50
|
+
}
|
|
51
|
+
if (!data.session_id) data.session_id = process.env.CLAUDE_SESSION_ID || process.env.CLAUDE_CODE_SESSION_ID;
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isWatchedExtension(filePath) {
|
|
56
|
+
const p = filePath.toLowerCase();
|
|
57
|
+
if (p.endsWith('.d.ts') || p.endsWith('.test.ts') || p.endsWith('.test.tsx')) return false;
|
|
58
|
+
return /\.(tsx|ts|css)$/.test(p);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isBloxRoot(dir) {
|
|
62
|
+
if (path.basename(dir).toLowerCase() !== BLOX_DIR_NAME) {
|
|
63
|
+
try {
|
|
64
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
65
|
+
return pkg && pkg.name === BLOX_PACKAGE_NAME;
|
|
66
|
+
} catch (e) { return false; }
|
|
67
|
+
}
|
|
68
|
+
return fs.existsSync(path.join(dir, 'package.json'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isPlatformAppRoot(dir) {
|
|
72
|
+
try {
|
|
73
|
+
return fs.readFileSync(path.join(dir, 'vite.config.ts'), 'utf8').includes('defineTogaApp');
|
|
74
|
+
} catch (e) { return false; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Walk up from the file to the nearest project root. Returns { root, isBlox } when the
|
|
78
|
+
* nearest root is a platform app or blox, else null. The file must be under root/src/. */
|
|
79
|
+
function findPlatformRoot(filePath) {
|
|
80
|
+
let dir = path.dirname(filePath);
|
|
81
|
+
for (let i = 0; i < MAX_WALK_UP_LEVELS; i++) {
|
|
82
|
+
const app = isPlatformAppRoot(dir);
|
|
83
|
+
const blox = !app && isBloxRoot(dir);
|
|
84
|
+
if (app || blox) {
|
|
85
|
+
const rel = path.relative(dir, filePath).replace(/\\/g, '/');
|
|
86
|
+
return rel.startsWith('src/') ? { root: dir, isBlox: blox } : null;
|
|
87
|
+
}
|
|
88
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) return null; // some other project
|
|
89
|
+
const parent = path.dirname(dir);
|
|
90
|
+
if (parent === dir) return null;
|
|
91
|
+
dir = parent;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sessionKey(data) {
|
|
97
|
+
const sid = (data && data.session_id) || String(process.ppid || process.pid);
|
|
98
|
+
return String(sid).replace(/[^A-Za-z0-9_-]/g, '').slice(0, 64) || 'x';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* True the first time this path is seen this session; records it. */
|
|
102
|
+
function firstTimeThisSession(data, normPath) {
|
|
103
|
+
const sf = path.join(os.tmpdir(), 'toga-reuse-reminder-' + sessionKey(data) + '.json');
|
|
104
|
+
let seen = [];
|
|
105
|
+
try { seen = JSON.parse(fs.readFileSync(sf, 'utf8')) || []; } catch (e) { seen = []; }
|
|
106
|
+
if (!Array.isArray(seen)) seen = [];
|
|
107
|
+
if (seen.includes(normPath)) return false;
|
|
108
|
+
seen.push(normPath);
|
|
109
|
+
try { fs.writeFileSync(sf, JSON.stringify(seen.slice(-MAX_SEEN_PATHS))); } catch (e) { /* non-fatal */ }
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function reminder(isBlox) {
|
|
114
|
+
const where = isBlox
|
|
115
|
+
? 'New file in toga25-blox. Before creating it: check INVENTORY.md in this repo'
|
|
116
|
+
: 'New file in a platform app. Before creating it: check `' + BLOX_INVENTORY + '` and this app\'s `' +
|
|
117
|
+
APP_REUSE_DOC + '`';
|
|
118
|
+
return [
|
|
119
|
+
'<system-reminder>',
|
|
120
|
+
where + ' — reuse or extend an existing piece first.',
|
|
121
|
+
'If this duplicates something in another page or app, consolidate instead',
|
|
122
|
+
'(rule: `' + CONSOLIDATION_DOC + '`).',
|
|
123
|
+
'</system-reminder>',
|
|
124
|
+
].join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function main() {
|
|
128
|
+
if (process.env.TOGA_REUSE_REMINDER_DISABLED === '1') return;
|
|
129
|
+
|
|
130
|
+
const data = readPayload();
|
|
131
|
+
if (!/^(Write|Edit)$/.test(data.tool_name || '')) return;
|
|
132
|
+
|
|
133
|
+
const input = data.tool_input || {};
|
|
134
|
+
const filePath = typeof input.file_path === 'string' ? input.file_path : '';
|
|
135
|
+
if (!filePath || !isWatchedExtension(filePath)) return;
|
|
136
|
+
|
|
137
|
+
const abs = path.resolve(data.cwd || process.cwd(), filePath);
|
|
138
|
+
if (fs.existsSync(abs)) return; // only new files
|
|
139
|
+
|
|
140
|
+
const hit = findPlatformRoot(abs);
|
|
141
|
+
if (!hit) return;
|
|
142
|
+
|
|
143
|
+
if (!firstTimeThisSession(data, abs.replace(/\\/g, '/').toLowerCase())) return;
|
|
144
|
+
|
|
145
|
+
console.log(JSON.stringify({
|
|
146
|
+
hookSpecificOutput: {
|
|
147
|
+
hookEventName: 'PreToolUse',
|
|
148
|
+
additionalContext: reminder(hit.isBlox),
|
|
149
|
+
},
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try { main(); } catch (e) { /* fail-open: never block or brick an edit */ }
|
|
154
|
+
process.exitCode = 0; // always 0; exitCode (not exit()) lets stdout flush on Windows pipes
|