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.
@@ -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-22
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.921",
3
+ "version": "1.0.923",
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",
@@ -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