toga-ai 1.0.511 → 1.0.513
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/knowledge/1.0/apps/tools/features/errors-curation-console.md +63 -0
- package/knowledge/2.0/apps/_underscore/features/error-reporting-issue-event.md +18 -0
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -1
- package/knowledge/2.0/apps/worker2/features/error-escalation-cron.md +173 -2
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/INDEX.md +1 -0
- package/knowledge/clients/compass-usa/features/mits-sales-order-transmission-alerting.md +89 -0
- package/knowledge/clients/compass-usa/profile.md +7 -2
- package/knowledge/standalone/apps/claude/INDEX.md +1 -0
- package/knowledge/standalone/apps/claude/workflows/ticket-branch-creation-safety.md +91 -0
- package/package.json +1 -1
|
@@ -49,6 +49,26 @@ happening and were deliberately never ticketed. Nobody had that view before.
|
|
|
49
49
|
Filesystem routing (1.0 Tools MVC): `mvc/errors/` = **`/errors`** (the listing) and
|
|
50
50
|
`mvc/errors/issue/` = **`/errors/issue`** (the detail page).
|
|
51
51
|
|
|
52
|
+
**`/errors/issue` takes two parameters, one meaning each** (2026-08-04):
|
|
53
|
+
|
|
54
|
+
| Parameter | Means | Use |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `?issue=4KP` | `Issue.reference` — the identifier everything else quotes (ClickUp title, escalation email, neglect digest, production error page) | **all new links**, including the listing rows and worker2's `buildToolsIssueUrl()` |
|
|
57
|
+
| `?issueId=9` | `Issue.id` | the original form, kept working **indefinitely** |
|
|
58
|
+
|
|
59
|
+
`?issueId=` is never removed: links built from it are already sitting in ClickUp tasks and emails
|
|
60
|
+
sent months ago that cannot be rewritten.
|
|
61
|
+
|
|
62
|
+
> **Why two parameters instead of one that accepts either.** References are **base 32 over an
|
|
63
|
+
> alphabet that includes digits**, so an ordinary reference — `'68'`, `'137'`, `'400'` — is all
|
|
64
|
+
> digits and **cannot be distinguished from a primary key by inspection**. A combined parameter
|
|
65
|
+
> would have to guess; naming the parameter after the column removes the ambiguity. An earlier
|
|
66
|
+
> combined `?issue=` with reference-first/id-fallback precedence was written and then replaced.
|
|
67
|
+
|
|
68
|
+
`?issue=` also accepts the **event** form (`?issue=4KP-12` strips the event half), because that is
|
|
69
|
+
what people copy out of an error page or ClickUp comment. An unknown reference leaves the id at 0
|
|
70
|
+
and renders the existing "no such issue" view.
|
|
71
|
+
|
|
52
72
|
**Listing** — 25 per page with a filtered `COUNT` (it was previously a silent `LIMIT 200`, so
|
|
53
73
|
older issues simply did not exist as far as a curator could tell); bold monospace Issue ID; an
|
|
54
74
|
**EB Environment** column read from the newest event's context; a **Type** column showing
|
|
@@ -75,6 +95,22 @@ curated text, affected clients) right, with an always-present Type chip.
|
|
|
75
95
|
- Header chip order is **Issue / Environment / Type / Urgency** — Type was moved ahead of Urgency so
|
|
76
96
|
urgency finishes the row at the far right.
|
|
77
97
|
|
|
98
|
+
### Neglect indicator (2026-08-04)
|
|
99
|
+
|
|
100
|
+
Both pages now surface the **neglect** contribution to urgency: the listing computes
|
|
101
|
+
`openTaskAgeHours` as a correlated subquery and renders a `$describeNeglect` amber note
|
|
102
|
+
(`.neglect-note`, `#8a5a00`) **under the urgency badge**; the detail page's *Acknowledged* fact row
|
|
103
|
+
reads "not yet — waiting 3 days" alongside a NORMAL/HIGH neglect chip.
|
|
104
|
+
|
|
105
|
+
**Why:** the urgency badge alone is genuinely misleading. A **HIGH beside "Lifetime 1"** is a
|
|
106
|
+
statement about *response time*, not about the error, and nothing on the page let a reader tell
|
|
107
|
+
those apart. **Nothing renders while the clock is running but under 24h** — below that the clock
|
|
108
|
+
contributes nothing to urgency, so saying "neglect" would overstate it.
|
|
109
|
+
|
|
110
|
+
**Deliberately scoped to the neglect contribution only.** The console never claims *which* axis is
|
|
111
|
+
winning: computing the full driving axis in Tools would mean copying the volume, sustained and
|
|
112
|
+
release-band thresholds too, and that much duplicated policy would rot.
|
|
113
|
+
|
|
78
114
|
### Table presentation rules (established 2026-08-04)
|
|
79
115
|
|
|
80
116
|
**Stacked datetimes in every table on both pages** — listing *Last seen*; detail *Events → When*,
|
|
@@ -143,6 +179,17 @@ They were renamed from plural on 2026-08-01, after the pipeline was already in p
|
|
|
143
179
|
|
|
144
180
|
## Gotchas / known issues
|
|
145
181
|
|
|
182
|
+
- **⚠ OPEN BUG (found 2026-08-04, deliberately left unfixed pending the developer's go-ahead):
|
|
183
|
+
`tools/mvc/errors/post.php` ~line 181, the merge-fingerprint action, will merge into the WRONG
|
|
184
|
+
issue.** It takes a reference **or** a numeric id in **one** field and disambiguates with
|
|
185
|
+
`ctype_digit()`. That was safe under the old **base-26** scheme, where every reference contained a
|
|
186
|
+
letter. Under the new **base-32** alphabet an all-digit reference like `'68'` is treated as **id
|
|
187
|
+
68** and silently repoints the fingerprint at the wrong Issue. Fix is either reference-first-then-id
|
|
188
|
+
precedence or two separate inputs (as `/errors/issue` now does).
|
|
189
|
+
- **The 24/48 neglect thresholds are duplicated as plain numbers in both Tools views.** Tools is
|
|
190
|
+
1.0 and cannot read worker2's `NEGLECT_NORMAL_HOURS` / `NEGLECT_HIGH_HOURS`; both sites carry a
|
|
191
|
+
comment naming those constants as the source of truth. **Changing the policy means changing three
|
|
192
|
+
places** (worker2 + both Tools views).
|
|
146
193
|
- **A 2.0 table rename breaks this page silently until it is run.** This console has **no model
|
|
147
194
|
layer** — `get.php` and `post.php` interpolate table names into hand-written `App_Database`
|
|
148
195
|
SQL (40+ literal occurrences between them). Nothing here fails at deploy time; it fails when
|
|
@@ -170,6 +217,22 @@ They were renamed from plural on 2026-08-01, after the pipeline was already in p
|
|
|
170
217
|
|
|
171
218
|
## Change history
|
|
172
219
|
|
|
220
|
+
- 2026-08-04 (latest, **uncommitted/undeployed** at time of writing) — **Built:** a **neglect
|
|
221
|
+
indicator** on both pages — listing gained an `openTaskAgeHours` correlated subquery and a
|
|
222
|
+
`$describeNeglect` amber note (`.neglect-note`, `#8a5a00`) under the urgency badge; the detail
|
|
223
|
+
page's *Acknowledged* row now reads "not yet — waiting 3 days" with a NORMAL/HIGH neglect chip.
|
|
224
|
+
The urgency badge alone was misleading (a HIGH beside "Lifetime 1" describes response time, not
|
|
225
|
+
the error) and nothing distinguished the two; nothing renders under 24h since the clock is
|
|
226
|
+
contributing nothing yet. Scoped to the neglect contribution only — computing the winning axis
|
|
227
|
+
would mean duplicating the volume/sustained/release-band thresholds as well. **Built:**
|
|
228
|
+
`/errors/issue` now accepts **`?issue=<reference>`** (also tolerating the `4KP-12` event form) and
|
|
229
|
+
the listing links by reference; `?issueId=<id>` is kept working **indefinitely** for links already
|
|
230
|
+
sitting in old ClickUp tasks and emails. Two parameters rather than one, because base-32
|
|
231
|
+
references can be all digits and are indistinguishable from a primary key. **Recorded two
|
|
232
|
+
maintenance hazards:** the 24/48 thresholds are now duplicated as literals in both Tools views
|
|
233
|
+
(Tools is 1.0 and cannot read worker2's constants — three places to change), and an **open bug** in
|
|
234
|
+
`post.php`'s merge-fingerprint `ctype_digit()` disambiguation, which under base-32 will merge into
|
|
235
|
+
the wrong Issue (flagged, left unfixed). (jcardinal)
|
|
173
236
|
- 2026-08-04 (later, **uncommitted/undeployed** at time of writing) — UI pass on both `/errors`
|
|
174
237
|
pages: **stacked datetimes** in every table via a shared `$datetime` closure emitting
|
|
175
238
|
`.dt`/`.dt-date`/`.dt-time` (a naturally wrapping datetime broke inside the day of the month), and
|
|
@@ -364,6 +364,18 @@ in `_underscore` and `library` in the **same** release, and the `/errors` consol
|
|
|
364
364
|
`dbchanges2/Logs_Client/2026-08-01a - Drop Error table.sql`. Do not reintroduce it.
|
|
365
365
|
- **Shared-schema migration collision.** Because these tables live in the *shared* Core Logs
|
|
366
366
|
DB, only **one** migration may `CREATE` them; later migrations only `ALTER`.
|
|
367
|
+
- **The `dbchanges2/Logs/` migration files LAG the deployed schema — read the live cluster, not the
|
|
368
|
+
migrations.** `dbchanges2/Logs/2026-07-30a - Error reporting Issues and Events.sql` does **not**
|
|
369
|
+
describe the live `Logs.Issue`: notably the identity column is **`issueKey` (varchar 128)**, not
|
|
370
|
+
`code`. Live columns include `reference`, `dtLastOccurred`, `isManaged`, `urgency`,
|
|
371
|
+
`minimumUrgency`, `consecutiveQuietWindows`, `subject`, `errorMessage`, `occurrences`,
|
|
372
|
+
`totalOccurrences`, `recurrenceCount`, `dtLastNotified`, `notificationCount`, `dtAcknowledged`,
|
|
373
|
+
`dtMutedUntil`, `createsClickupTask`, `clickupPriority`, `clickupAssigneeId`. Query
|
|
374
|
+
`information_schema` (or `SHOW TABLES` / `SHOW COLUMNS`) against the live Logs cluster **before**
|
|
375
|
+
writing any SQL against this feature — reading the migrations instead produced confidently wrong
|
|
376
|
+
conclusions twice on 2026-08-04. Same trap for the **singular** table names
|
|
377
|
+
(`Issue`/`Event`/`IssueFingerprint`/`IssueAreaOwner`/`IssueClickupTask`/`IssueEmailAddress`,
|
|
378
|
+
renamed by `2026-08-01a` and confirmed applied in prod).
|
|
367
379
|
- **Pre-existing committed secrets in this area (not fixed — out of scope, report only).**
|
|
368
380
|
A ClickUp API token at `_underscore/Component/Api/Clickup/Clickup.php:5`; SES SMTP
|
|
369
381
|
credentials at `_underscore/Email.php:11-12`; a named individual's Sentry personal access
|
|
@@ -418,6 +430,12 @@ clientUserId). **Neither was built.** As built instead:
|
|
|
418
430
|
|
|
419
431
|
## Change history
|
|
420
432
|
|
|
433
|
+
- 2026-08-04 — **Correction/diagnostic:** recorded that the `dbchanges2/Logs/` migration files lag
|
|
434
|
+
the deployed schema — `2026-07-30a` describes a `code` identity column, but live `Logs.Issue` is
|
|
435
|
+
keyed on **`issueKey` (varchar 128)** — and listed the live column set. Query
|
|
436
|
+
`information_schema` against the live Logs cluster before writing SQL here; trusting the
|
|
437
|
+
migrations (and forgetting the applied singular rename) produced two confidently wrong
|
|
438
|
+
conclusions in one session. No code change. (dfranks)
|
|
421
439
|
- 2026-08-04 (later, **uncommitted/undeployed** at time of writing) — **Reference encoding reworked
|
|
422
440
|
to base 32** over `'0123456789ACDEFGHJKMNPQRSTUVWXYZ'` (all digits kept; `O`/`I`/`L`/`B` dropped
|
|
423
441
|
as impersonators of `0`/`1`/`1`/`8`), replacing the base-26 decimal-counter+letter scheme, with
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
| [Compass VIP Support Importer (worker2)](features/compass-vip-support-importer.md) | A worker2 action that ingests Compass's quarterly VIP spreadsheet and assigns each VIP user's support technician by setting `Users.c_supportedByUserId` in `Clie | worker2/Worker/Client/Compass/VipSupport.php |
|
|
19
19
|
| [Creating Worker Actions](features/creating-worker-actions.md) | How to add a new callable Worker action — a PHP class whose `public static` methods are invoked as background jobs (via webhook, cron, or `_Worker::runTask()`). | worker2/Worker/, worker2/Controller/Index.php, _underscore/Worker.php |
|
|
20
20
|
| [Elite Freshservice Sync (worker2)](features/elite-freshservice-sync.md) | `_Worker_Elite` processes Freshservice webhook events and syncs them into TOGA 2. | worker2/Worker/Elite.php, worker2/Config/dev-kmaramreddy-laptop.ini |
|
|
21
|
-
| [Error Escalation Cron (Errors::Escalate → ClickUp / email)](features/error-escalation-cron.md) | `_Worker_Infrastructure_Errors::Escalate` (renamed from `SyncWithClickup`) is the sole owner of **escalation, de-escalation, ClickUp ticketing, reminders, busin | worker2/Worker/Infrastructure/Errors.php, worker2/Worker/Notification/Email.php, worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, worker2/Worker/Clickup/ErrorTask.php, worker2/Worker/Clickup.php, worker2/Controller/Index.php, worker2/Config/production.ini, _underscore/Model/Core/Logs/Issue.php, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql, dbchanges2/Logs/2026-08-03a - Issue clickupPriority.sql |
|
|
21
|
+
| [Error Escalation Cron (Errors::Escalate → ClickUp / email)](features/error-escalation-cron.md) | `_Worker_Infrastructure_Errors::Escalate` (renamed from `SyncWithClickup`) is the sole owner of **escalation, de-escalation, ClickUp ticketing, reminders, busin | worker2/Worker/Infrastructure/Errors.php, worker2/Worker/Notification/Email.php, worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, worker2/Worker/Clickup/ErrorTask.php, worker2/Worker/Clickup.php, worker2/Controller/Index.php, worker2/Config/production.ini, _underscore/Model/Core/Logs/Issue.php, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql, dbchanges2/Logs/2026-08-03a - Issue clickupPriority.sql, dbchanges2/Core/2026-08-04a - Error neglect digest cron job.sql |
|
|
22
22
|
| [Etilize Catalog Item Import & Refresh](features/etilize-catalog-item-import.md) | Client-generic catalog onboarding from an S3 CSV plus an Etilize re-pull. | worker2/Worker/Etilize/Items.php |
|
|
23
23
|
| [Etilize Item Translation Import](features/etilize-item-translation-import.md) | The abstract worker class `_Worker_Etilize_ItemTranslations` imports **non-English** item text from Etilize into the client's `ItemTranslations` table. | worker2/Worker/Etilize/ItemTranslations.php |
|
|
24
24
|
| [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Monitors/RateEntitlement.php, worker2/Worker/Notification/Email.php, worker2/Worker/Rate.php, dbchanges2/Core/2026-05-21 - Monitors.sql, dbchanges2/Core/2026-06-29a - Rate Entitlement Contract Monitor.sql |
|
|
@@ -7,7 +7,7 @@ client: shared
|
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-08-04
|
|
10
|
-
owners: ["jcardinal"]
|
|
10
|
+
owners: ["jcardinal", "dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Infrastructure/Errors.php
|
|
13
13
|
- worker2/Worker/Notification/Email.php
|
|
@@ -20,6 +20,7 @@ files:
|
|
|
20
20
|
- _underscore/Model/Core/Logs/Issue.php
|
|
21
21
|
- dbchanges2/Core/2026-07-30a - Error escalation cron job.sql
|
|
22
22
|
- dbchanges2/Logs/2026-08-03a - Issue clickupPriority.sql
|
|
23
|
+
- dbchanges2/Core/2026-08-04a - Error neglect digest cron job.sql
|
|
23
24
|
related:
|
|
24
25
|
- ../../_underscore/features/error-reporting-issue-event.md
|
|
25
26
|
- ../../../1.0/apps/tools/features/errors-curation-console.md
|
|
@@ -28,6 +29,7 @@ related:
|
|
|
28
29
|
- ./clickup-project-routing.md
|
|
29
30
|
- ./notification-email.md
|
|
30
31
|
- ./notification-email-template.md
|
|
32
|
+
- ../../../clients/compass-usa/features/mits-sales-order-transmission-alerting.md
|
|
31
33
|
---
|
|
32
34
|
|
|
33
35
|
## Summary
|
|
@@ -46,13 +48,93 @@ through this one action, so there is exactly one place to mute or debug alerting
|
|
|
46
48
|
- **Volume** — occurrences in a **1-hour** window: **NORMAL 5 / HIGH 10 / URGENT 20** (retuned
|
|
47
49
|
2026-08-04 from 20/50/100 over 10 minutes — a 10-minute window never fired for the
|
|
48
50
|
low-and-steady failures that actually matter).
|
|
49
|
-
- **Neglect** — hours an *unacknowledged* ClickUp task has sat: **
|
|
51
|
+
- **Neglect** — hours an *unacknowledged* ClickUp task has sat: **NORMAL 24 / HIGH 48. Capped at
|
|
52
|
+
HIGH — there is no URGENT rung.**
|
|
50
53
|
- **Sustained-24h** — an issue still occurring a day later escalates on duration alone.
|
|
51
54
|
- **Floor** — the Issue's declared `minimumUrgency` applies under *all* axes.
|
|
52
55
|
|
|
53
56
|
The neglect axis exists because volume alone cannot see a rare-but-serious error, nor a serious
|
|
54
57
|
error that everyone is ignoring.
|
|
55
58
|
|
|
59
|
+
#### ⚠ Neglect is capped at HIGH and slowed to 24h/48h (decided + fixed 2026-08-04)
|
|
60
|
+
|
|
61
|
+
`NEGLECT_NORMAL_HOURS` 1 → **24**, `NEGLECT_HIGH_HOURS` 4 → **48**, and
|
|
62
|
+
**`NEGLECT_URGENT_HOURS` and its branch were removed** from `resolveNeglectUrgency()`. This
|
|
63
|
+
**supersedes** the earlier 1/4/24 ladder with an URGENT rung.
|
|
64
|
+
|
|
65
|
+
**Why:** neglect answers *"is anyone on this"* — a question about the **team's response**, never
|
|
66
|
+
about how much damage the error is doing. Only the volume axes measure damage, so only they may
|
|
67
|
+
claim URGENT. Left uncapped on the old clock, any error that fired once and went unacknowledged
|
|
68
|
+
for an afternoon became HIGH, then URGENT the next day. That is how the queue filled up:
|
|
69
|
+
**48 of 68 issues sat at HIGH**, and issue **3H (id 60) was HIGH off a single lifetime
|
|
70
|
+
occurrence** — volume 0 in the 60-minute window, sustained 1 against a threshold of 50,
|
|
71
|
+
`minimumUrgency` LOW, with the entire HIGH coming from its ClickUp task sitting unacknowledged
|
|
72
|
+
for 4.93 hours.
|
|
73
|
+
|
|
74
|
+
**The neglect axis is a FLOOR, not a ceiling.** `resolveNeglectUrgency()` is one of four inputs
|
|
75
|
+
to `highestUrgency()`, which takes the **max** — so a neglected issue that starts firing still
|
|
76
|
+
escalates on volume: two days unacknowledged contributes HIGH, and 20 occurrences in the window
|
|
77
|
+
take it straight to URGENT regardless. Capping the axis lowers the floor *neglect alone* can
|
|
78
|
+
raise; it takes nothing away from the occurrence count. Upward moves apply the same run (only
|
|
79
|
+
**downward** moves go through the `REQUIRED_QUIET_RUNS` dwell), so volume takeover is same-run.
|
|
80
|
+
|
|
81
|
+
**Intended side effect, accepted:** `SPRINT_PROMOTION_URGENCY` is `'URGENT'`, so with neglect
|
|
82
|
+
capped at HIGH, **neglect alone can no longer promote an issue into the sprint** — only a genuine
|
|
83
|
+
occurrence rate can.
|
|
84
|
+
|
|
85
|
+
**Deploy effect.** All 50 then-HIGH issues had open unacknowledged tasks younger than 24h, so
|
|
86
|
+
their neglect contribution becomes LOW and they recompute from volume/sustained/floor within ~3
|
|
87
|
+
runs of the worker2 deploy. Expect a **burst of ClickUp priority PUTs** on the first runs as
|
|
88
|
+
`clickupPriority` re-syncs.
|
|
89
|
+
|
|
90
|
+
### Daily neglect digest email (`Infrastructure/Errors/NeglectDigest`)
|
|
91
|
+
|
|
92
|
+
A second worker action — `public NeglectDigest()`, with `loadNeglectedIssues()` and
|
|
93
|
+
`buildNeglectDigestBody()` — registered in `Core.CronJobs` as action
|
|
94
|
+
`Infrastructure/Errors/NeglectDigest` at **`0 8 * * *`** (08:00 CT: after the overnight cleanup
|
|
95
|
+
jobs, before the working day), `maxExecutionTime` 300. Migration:
|
|
96
|
+
`dbchanges2/Core/2026-08-04a - Error neglect digest cron job.sql`.
|
|
97
|
+
|
|
98
|
+
**Why a digest exists at all — the governing principle:** neglect means *"nobody is looking"*, so
|
|
99
|
+
**every signal that requires somebody to look is self-defeating.** It is also the necessary
|
|
100
|
+
counterpart to capping the axis: with neglect slowed and capped, an ignored issue no longer
|
|
101
|
+
announces itself by climbing the urgency scale, so something has to come and tell you.
|
|
102
|
+
|
|
103
|
+
Sends via `_Worker::runTask('Notification/EmailTemplate/Send')` with the TOGA Technology stored
|
|
104
|
+
template — the **same path as the technical escalation email**. Recipient is `[_underscore]
|
|
105
|
+
error_escalation_email`, gated by the same `error_escalation_enabled` key, so non-production mails
|
|
106
|
+
nobody. The body is a **fragment** for the template's `{body}` (600px cell; the template owns font
|
|
107
|
+
and width), exactly like `buildTechnicalEmailBody()`.
|
|
108
|
+
|
|
109
|
+
Three design decisions, each load-bearing:
|
|
110
|
+
|
|
111
|
+
- **It sends even when the list is empty.** An empty digest states *"nothing is neglected"*;
|
|
112
|
+
silence cannot be distinguished from a broken notifier — which is precisely the failure that hid
|
|
113
|
+
the escalation email being dead for days while every run reported success. The **subject
|
|
114
|
+
reflects the state** ("No neglected error issues" vs "2 error issues nobody has picked up") so
|
|
115
|
+
it is triageable from the inbox list without opening it.
|
|
116
|
+
- **One email covering every issue, never one per issue.** A per-issue alert for a condition that
|
|
117
|
+
by nature affects many issues at once is how a channel becomes wallpaper — and the reminder
|
|
118
|
+
ladder already carries the per-issue nagging.
|
|
119
|
+
- **Floored at `NEGLECT_NORMAL_HOURS`**, not "everything open". That constant is where the cron
|
|
120
|
+
itself starts treating an issue as neglected, so the digest reports only things that have
|
|
121
|
+
crossed a line. A task opened this morning is not neglected, it is new. Ordered **oldest
|
|
122
|
+
first** so the worst case is the first line read.
|
|
123
|
+
|
|
124
|
+
Verified against production: the query returns **2 rows** today (1B and 1I, both 24h
|
|
125
|
+
unacknowledged).
|
|
126
|
+
|
|
127
|
+
**Rejected alternatives:**
|
|
128
|
+
|
|
129
|
+
- *Mutating the ClickUp task **title** to say "neglected"* — the title is deliberately just the
|
|
130
|
+
reference and is never rewritten; rewriting churns the activity feed and breaks the
|
|
131
|
+
quote-the-reference contract, and it only helps someone already looking at the task.
|
|
132
|
+
- *A new `Issue` column (`dtNeglectedSince`, `neglectLevel`)* — fully derivable from
|
|
133
|
+
`IssueClickupTask.dtCreated` + `Issue.dtAcknowledged`, so storing it is denormalised state that
|
|
134
|
+
can drift, and it still needs something to read it out.
|
|
135
|
+
- *A ClickUp custom field* — **deferred, not rejected**: it writes every run and duplicates what
|
|
136
|
+
the digest says. Revisit only if the digest proves insufficient.
|
|
137
|
+
|
|
56
138
|
### Fast up, slow down
|
|
57
139
|
|
|
58
140
|
Escalation applies **the same run** a band is crossed. De-escalation requires the count to sit
|
|
@@ -107,6 +189,15 @@ ClickUp is the acknowledge/resolve system — developers already live there, and
|
|
|
107
189
|
Tools would just be ignored. **Any** status change on an error task counts as acknowledgement,
|
|
108
190
|
which is safe only because the automation never sets status itself.
|
|
109
191
|
|
|
192
|
+
> **`dtAcknowledged` is set by a status change OR by an assignee being added** — both, and this was
|
|
193
|
+
> already implemented (confirmed 2026-08-04, no change needed). `worker2/Worker/Clickup.php:856`
|
|
194
|
+
> routes the ClickUp `taskUpdated` webhook into
|
|
195
|
+
> `_Worker_Clickup_ErrorTask::handleAssigneeChange()`, which calls `markAcknowledged()` (and also
|
|
196
|
+
> feeds the per-area ownership learner). Both routes are subject to the 60-second creation grace
|
|
197
|
+
> window below. Production: 8 of 71 issues acknowledged. **Worth stating explicitly, because the
|
|
198
|
+
> acknowledgement definition is what both the neglect axis and the daily digest key off**, and it
|
|
199
|
+
> is not visible from the escalation-cron side.
|
|
200
|
+
|
|
110
201
|
> **60-second creation grace window (fix, 2026-08-04).** ClickUp fires a status webhook for a
|
|
111
202
|
> task's *initial* status, so the automation was acknowledging its **own** newly created tasks —
|
|
112
203
|
> stamping `dtAcknowledged` ~1s after `dtCreated` and permanently disabling the neglect axis for
|
|
@@ -175,6 +266,32 @@ healthy — cannot recur silently.
|
|
|
175
266
|
- **Occurrences** and **Last Occurrence** custom fields are synced, and task bodies open with a
|
|
176
267
|
**"Debug in Tools"** deep link.
|
|
177
268
|
|
|
269
|
+
#### Tools deep links address the issue by REFERENCE, not id
|
|
270
|
+
|
|
271
|
+
`TOOLS_ISSUE_PATH` changed from `'/errors/issue?issueId='` to **`'/errors/issue?issue='`**, and
|
|
272
|
+
`buildToolsIssueUrl()` now emits the **reference**, `rawurlencode`d, falling back to the id only
|
|
273
|
+
when a row has no reference. **Two parameters, one meaning each** — deliberately not one parameter
|
|
274
|
+
that accepts either:
|
|
275
|
+
|
|
276
|
+
| Parameter | Means | Use |
|
|
277
|
+
| --- | --- | --- |
|
|
278
|
+
| `?issue=4KP` | `Issue.reference` — what everything else quotes (ClickUp title, escalation email, digest, production error page) | **all new links** |
|
|
279
|
+
| `?issueId=9` | `Issue.id` | the original form, kept working **indefinitely** |
|
|
280
|
+
|
|
281
|
+
`?issueId=` stays supported forever because links built from it are already sitting in ClickUp
|
|
282
|
+
tasks and emails sent months ago that cannot be rewritten.
|
|
283
|
+
|
|
284
|
+
> **Why separate parameters rather than one that accepts either.** References are **base 32 over
|
|
285
|
+
> an alphabet that includes digits**, so an ordinary reference — `'68'`, `'137'`, `'400'` — is all
|
|
286
|
+
> digits and **cannot be told apart from a primary key by inspection**. A single combined parameter
|
|
287
|
+
> would have to guess; naming the parameter after the column removes the ambiguity entirely. (An
|
|
288
|
+
> earlier combined `?issue=` with reference-first / id-fallback precedence was written and then
|
|
289
|
+
> replaced by this.)
|
|
290
|
+
|
|
291
|
+
`?issue=` also accepts the **event** form — `?issue=4KP-12` strips the event half and lands on the
|
|
292
|
+
issue — because that is what people copy out of an error page or a ClickUp comment. An unknown
|
|
293
|
+
reference leaves the id at 0 and renders the existing "no such issue" view.
|
|
294
|
+
|
|
178
295
|
### Area-level owner learning (observation only)
|
|
179
296
|
|
|
180
297
|
`IssueAreaOwner` learns an owner after **5 in a row** and ships **observation-only** — a hint in
|
|
@@ -231,6 +348,12 @@ this controller needs the same treatment.**
|
|
|
231
348
|
|
|
232
349
|
## Gotchas / known issues
|
|
233
350
|
|
|
351
|
+
- **An issue held at HIGH purely by neglect never de-escalates on its own.**
|
|
352
|
+
`applyDeEscalationDwell()` needs the computed target to sit **below** the stored urgency for
|
|
353
|
+
`REQUIRED_QUIET_RUNS` (3) consecutive runs, but the neglect axis keeps recomputing the target
|
|
354
|
+
**at** the stored level — so the dwell counter never starts. That is why issue 3H showed
|
|
355
|
+
`consecutiveQuietWindows = 0` after five hours of total silence. **Acknowledging the ClickUp task
|
|
356
|
+
is what releases it**, and it then drops within 3 cron runs (~3 min).
|
|
234
357
|
- **Never send email inline inside this cron's open transaction — enqueue it.** `_Email::send()`
|
|
235
358
|
registers a second connection and toggles the read host, and a throw from in there was only ever
|
|
236
359
|
`error_log`'d. Use `_Worker::runTask` so a failure becomes a visible failed `WorkerJobs` row.
|
|
@@ -263,6 +386,22 @@ this controller needs the same treatment.**
|
|
|
263
386
|
issue gets a **neutral** subject rather than another client's raw error text.
|
|
264
387
|
- **`IssueEmailAddress.clientId = 0` means "all clients"** and requires an explicit
|
|
265
388
|
confirmation flag in the Tools console; list views render it as **"ALL CLIENTS"**.
|
|
389
|
+
- **A recipient scoped to a specific `clientId` delivers NOTHING for occurrences recorded without
|
|
390
|
+
client attribution.** `loadRecentClientIds()` maps an occurrence whose `clientId` is NULL to
|
|
391
|
+
**`0`**, and `resolveRecipientsForClient()` resolves scope `0` **plus** the erroring client. So
|
|
392
|
+
the rule is: scope to **`0`** when the `issueKey` is raised from a **single client-specific code
|
|
393
|
+
path** (a Compass-only cron, say); **never** scope to `0` for an `issueKey` raised from shared
|
|
394
|
+
code, which would leak one client's alerts to another's recipients.
|
|
395
|
+
- **Configuring a recipient is never retroactive.** `shouldNotify()` notifies on first occurrence
|
|
396
|
+
(`notificationCount` 0), on escalation, or when the backed-off reminder interval has elapsed —
|
|
397
|
+
and returns **false when the current window has zero events**. The first email therefore lands on
|
|
398
|
+
the **next live occurrence**, not on the backlog. Do not read silence right after a config change
|
|
399
|
+
as a failure.
|
|
400
|
+
- **Adding a business recipient is purely additive — it cannot switch ClickUp ticketing off.**
|
|
401
|
+
Routing is **both**, not either: `if ($recipients !== null)` emails the business recipients and
|
|
402
|
+
`if ($issue->createsClickupTask)` tickets the dev team, evaluated **independently**. The in-code
|
|
403
|
+
comment marking this records the earlier `elseif` form, where adding one recipient silently
|
|
404
|
+
disabled an issue's dev-team ticket.
|
|
266
405
|
- **The ClickUp webhook has no HMAC signature verification** (pre-existing, unfixed). A forged
|
|
267
406
|
POST can mark issues acknowledged — freezing the neglect axis — or resolve episodes. That is
|
|
268
407
|
alert suppression against the error-reporting system itself. Fix before relying on this
|
|
@@ -273,6 +412,38 @@ this controller needs the same treatment.**
|
|
|
273
412
|
|
|
274
413
|
## Change history
|
|
275
414
|
|
|
415
|
+
- 2026-08-04 (verified against `origin/_production` + the prod Logs cluster) — Recorded the
|
|
416
|
+
**recipient-routing semantics** a developer needs before configuring a business alert: routing is
|
|
417
|
+
**both** business email and dev-team ClickUp, evaluated independently (adding a recipient is
|
|
418
|
+
purely additive); `IssueEmailAddress.clientId` scoping means a client-scoped row delivers nothing
|
|
419
|
+
for occurrences with NULL `clientId` (mapped to `0`), so single-client `issueKey`s scope to `0`
|
|
420
|
+
and shared-code `issueKey`s never do; and `shouldNotify()` makes configuration
|
|
421
|
+
**non-retroactive** — the first email lands on the next live occurrence. Also noted that prod
|
|
422
|
+
`Logs.IssueEmailAddress` was still empty, making Compass's MITS rejection the platform's first
|
|
423
|
+
business-routed issue. No code change. (dfranks)
|
|
424
|
+
- 2026-08-04 (latest, **uncommitted/undeployed** at time of writing) — **Decided + fixed: the
|
|
425
|
+
neglect urgency axis is capped at HIGH and slowed to 24h/48h**, superseding the 1/4/24 ladder
|
|
426
|
+
recorded earlier the same day (`NEGLECT_NORMAL_HOURS` 1→24, `NEGLECT_HIGH_HOURS` 4→48,
|
|
427
|
+
`NEGLECT_URGENT_HOURS` and its branch removed). Neglect measures the **team's response**, not the
|
|
428
|
+
damage the error is doing, so only the volume axes may claim URGENT; uncapped on the old clock a
|
|
429
|
+
single-occurrence error became HIGH in an afternoon and URGENT the next day — 48 of 68 issues were
|
|
430
|
+
at HIGH and issue 3H (id 60) was HIGH off one lifetime occurrence. The axis remains a **floor, not
|
|
431
|
+
a ceiling** (`highestUrgency()` takes the max, so volume still escalates immediately). Accepted
|
|
432
|
+
side effect: `SPRINT_PROMOTION_URGENCY` is URGENT, so neglect alone can no longer promote into the
|
|
433
|
+
sprint. **Built:** a **daily neglect digest** (`Infrastructure/Errors/NeglectDigest`,
|
|
434
|
+
`Core.CronJobs` at `0 8 * * *`, migration `dbchanges2/Core/2026-08-04a`) — the counterpart to the
|
|
435
|
+
cap, since a capped axis no longer announces an ignored issue; it **sends even when empty** (a
|
|
436
|
+
broken notifier is indistinguishable from silence), is **one email for all issues**, and floors at
|
|
437
|
+
`NEGLECT_NORMAL_HOURS`, oldest first. Rejected: rewriting the ClickUp title, a derived
|
|
438
|
+
`dtNeglectedSince`/`neglectLevel` column; deferred: a ClickUp custom field. **Built:**
|
|
439
|
+
`TOOLS_ISSUE_PATH` now emits `?issue=<reference>` (rawurlencoded, id fallback) with `?issueId=`
|
|
440
|
+
kept working indefinitely — separate parameters because base-32 references can be all digits and
|
|
441
|
+
are indistinguishable from a primary key. **Discovered:** assignee-based acknowledgement was
|
|
442
|
+
already implemented (`Clickup.php:856` → `handleAssigneeChange()` → `markAcknowledged()`), and
|
|
443
|
+
that a neglect-held HIGH never de-escalates until the task is acknowledged. Verification:
|
|
444
|
+
`php -l` clean on every changed file; the digest SQL run against production (2 rows); neglect
|
|
445
|
+
impact quantified across all 68 issues; `0 8 * * *` checked against existing daily job
|
|
446
|
+
conventions. (jcardinal)
|
|
276
447
|
- 2026-08-04 (later, **uncommitted/undeployed** at time of writing) — **Fixed: escalation email had
|
|
277
448
|
never been sent, ever**, for two independent reasons. (A) `processTechnicalIssue()` returned early
|
|
278
449
|
when the ClickUp task would read identically, and that return sat *above* the `if ($shouldNotify)`
|
package/knowledge/INDEX.md
CHANGED
|
@@ -39,7 +39,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
39
39
|
- **togatech** (TOGA Technology Website) — 4 doc(s) → [standalone/apps/togatech/INDEX.md](standalone/apps/togatech/INDEX.md)
|
|
40
40
|
- **websocket** (WebSocket Server) — 2 doc(s) → [standalone/apps/websocket/INDEX.md](standalone/apps/websocket/INDEX.md)
|
|
41
41
|
- **forward** (Forwarder) — 3 doc(s) → [standalone/apps/forward/INDEX.md](standalone/apps/forward/INDEX.md)
|
|
42
|
-
- **claude** (Claude Harness) —
|
|
42
|
+
- **claude** (Claude Harness) — 4 doc(s) → [standalone/apps/claude/INDEX.md](standalone/apps/claude/INDEX.md)
|
|
43
43
|
|
|
44
44
|
## Clients
|
|
45
45
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
| [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql, dbchanges2/Client_Compass/2026-06-15a - FixItemFulfillmentTrackingNumberJoins.sql, dbchanges2/Client/2026-07-15a - ExcludeFeeItemsFromItemFulfillmentsForSalesOrdersView.sql |
|
|
9
9
|
| [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
|
|
10
10
|
| [Compass MITS PO Transmission to Vendors](features/mits-po-transmission-to-vendors.md) | 2.0 | The 1.0 worker cron `2_transmit_mits_purchase_orders_to_vendors.php` transmits Compass PurchaseOrders to their vendors (Office Depot, Strategic Systems, Compass | worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, worker/crons/toga2/compasscanada/workflow/2_transmit_mits_purchase_orders_to_vendors.php, library/app/client/compass.php |
|
|
11
|
+
| [Compass MITS Sales-Order Transmission — Rejection Alerting (Issue/Event, not email-in-cron)](features/mits-sales-order-transmission-alerting.md) | 2.0 | When MITS **rejects** a Compass sales order transmitted by the 1.0 cron `1_transmit_compass_sales_orders_to_mits.php`, the alert is no longer an email built ins | worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php, worker2/Worker/Infrastructure/Errors.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, dbchanges2/Logs/2026-08-04a - MITS rejection business recipients.sql |
|
|
11
12
|
| [Compass MR/MA Order Auto-Approval & Status Gate](features/mr-ma-order-approval-and-status.md) | 2.0 | Compass **MR** and **MA** sales orders are system-generated from the MITS / Office Depot EDI pipeline (they do not originate as user-entered SA orders) and must | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
|
|
12
13
|
| [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
|
|
13
14
|
| [Compass Cross-Kit Bundle Corruption — Detection & Repair](workflows/cross-kit-bundle-corruption.md) | 2.0 | A frontend regression in `toga2-commerce`'s edit-order bundle submission mis-attributed bundle (kit) line items and **fees/warranties** to the **wrong kit**, pe | src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts |
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Compass MITS Sales-Order Transmission — Rejection Alerting (Issue/Event, not email-in-cron)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
project: _Underscore
|
|
5
|
+
client: compass-usa
|
|
6
|
+
type: client-feature
|
|
7
|
+
status: active
|
|
8
|
+
updated: 2026-08-04
|
|
9
|
+
owners: ["dfranks"]
|
|
10
|
+
files:
|
|
11
|
+
- worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
|
|
12
|
+
- worker2/Worker/Infrastructure/Errors.php
|
|
13
|
+
- _underscore/Model/Core/Logs/Issue.php
|
|
14
|
+
- _underscore/Model/Core/Logs/IssueEmailAddress.php
|
|
15
|
+
- dbchanges2/Logs/2026-08-04a - MITS rejection business recipients.sql
|
|
16
|
+
related:
|
|
17
|
+
- mits-po-transmission-to-vendors.md
|
|
18
|
+
- ../../../2.0/apps/worker2/features/error-escalation-cron.md
|
|
19
|
+
- ../../../2.0/apps/_underscore/features/error-reporting-issue-event.md
|
|
20
|
+
- ../../../1.0/apps/library/features/error-capture-1-0.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
When MITS **rejects** a Compass sales order transmitted by the 1.0 cron
|
|
26
|
+
`1_transmit_compass_sales_orders_to_mits.php`, the alert is no longer an email built inside the
|
|
27
|
+
cron. It is a **business exception** raised into the shared Logs Issue/Event pipeline, and its
|
|
28
|
+
recipients are **rows in `Logs.IssueEmailAddress`**.
|
|
29
|
+
|
|
30
|
+
**Consequence for anyone handed a ticket like "add <person> to the MITS error emails": there is
|
|
31
|
+
no PHP to change.** It is a one-row data change delivered as a `dbchanges2/Logs/` migration.
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
- The cron raises
|
|
36
|
+
`throw new App_Exception_Business('COMPASS_SALES_ORDER_MITS_TRANSMIT_REJECTED', …, URGENCY_HIGH)`
|
|
37
|
+
on a MITS rejection. (Before 2026-08-03 this block was a hardcoded `new App_Email_Agilant()`
|
|
38
|
+
with `addTo('devteam@togatech.com')`.)
|
|
39
|
+
- Capture creates/dedupes a `Logs.Issue` keyed on the `issueKey`, and the worker2
|
|
40
|
+
`Infrastructure/Errors/Escalate` cron does all outbound notification — business email to the
|
|
41
|
+
configured recipients **and**, independently, the dev-team ClickUp task. See
|
|
42
|
+
[error-escalation-cron](../../../2.0/apps/worker2/features/error-escalation-cron.md).
|
|
43
|
+
- Recipients live in `Logs.IssueEmailAddress` (FK `issueId` → `Logs.Issue`), scoped by
|
|
44
|
+
`clientId` (`0` = all clients).
|
|
45
|
+
|
|
46
|
+
### Adding a recipient (the correct pattern)
|
|
47
|
+
|
|
48
|
+
Write a `dbchanges2/Logs/YYYY-MM-DD<letter>` migration that:
|
|
49
|
+
|
|
50
|
+
1. **Looks the issue up by `issueKey`**, never by its `id`. The id is assigned on the issue's
|
|
51
|
+
**first occurrence** and is therefore not stable across environments.
|
|
52
|
+
2. Uses **`clientId = 0`**. This `issueKey` is raised from a Compass-only code path, and an
|
|
53
|
+
occurrence recorded without client attribution resolves against scope `0` only — a row scoped
|
|
54
|
+
to the Compass `clientId` can deliver nothing. (Never use `0` for an `issueKey` raised from
|
|
55
|
+
shared code — that is a cross-tenant leak.)
|
|
56
|
+
3. Is **idempotent** via `INSERT … WHERE NOT EXISTS`, mirroring the unique key
|
|
57
|
+
`uq_issue_client_address_type`.
|
|
58
|
+
|
|
59
|
+
Configuration is **never retroactive**: `shouldNotify()` fires on the first occurrence, on
|
|
60
|
+
escalation, or when the backed-off reminder interval elapses, and returns false for a window with
|
|
61
|
+
zero events. The first email lands on the **next live rejection**, not on existing history.
|
|
62
|
+
|
|
63
|
+
## Gotchas / known issues
|
|
64
|
+
|
|
65
|
+
- **Do not "modernize" this alert back into the cron.** Migrating it to
|
|
66
|
+
`_Model_Client_EmailTemplate::send` (or any in-cron email) reverts the move to the Issue/Event
|
|
67
|
+
pipeline and removes the recipient configuration surface entirely.
|
|
68
|
+
- **Never re-introduce the payload dump into the alert.** The original email included the
|
|
69
|
+
transmission payload, which carries ship-to addressee, attention line, street address and phone
|
|
70
|
+
number. The Issue message is stored in a table **shared across every client**, displayed to
|
|
71
|
+
internal staff in the Tools `/errors` console, pushed to ClickUp, and emailed — so the payload
|
|
72
|
+
was deliberately removed. Alerts here name the order, not the person.
|
|
73
|
+
- **The MITS cron holds its API credentials inline in the file.** Treat that file as
|
|
74
|
+
secret-bearing when copying snippets anywhere (tickets, PR bodies, knowledge docs).
|
|
75
|
+
- **Adding a recipient does not turn ClickUp ticketing off.** Routing is both-and, not
|
|
76
|
+
either-or — see the escalation-cron doc for the `elseif` bug this used to be.
|
|
77
|
+
- As of 2026-08-04 prod `Logs.IssueEmailAddress` is empty, so this is the **first business-routed
|
|
78
|
+
issue on the platform**: `processBusinessIssue()` will execute in production for the first time
|
|
79
|
+
when this issue next fires. Watch that run rather than assuming it works.
|
|
80
|
+
|
|
81
|
+
## Change history
|
|
82
|
+
- 2026-08-04 — Documented that MITS rejection alerting moved (worker `5521854b`, 2026-08-03) from a
|
|
83
|
+
hardcoded in-cron `App_Email_Agilant` to `App_Exception_Business` +
|
|
84
|
+
`Logs.IssueEmailAddress` recipients, so adding an alert recipient is a data change, not a code
|
|
85
|
+
change. Recorded the deliberate removal of the PII-bearing payload dump from the alert, the
|
|
86
|
+
look-up-by-`issueKey` / `clientId = 0` / `NOT EXISTS` migration pattern, and that recipient
|
|
87
|
+
configuration only takes effect on the next live occurrence. (dfranks)
|
|
88
|
+
</content>
|
|
89
|
+
</invoke>
|
|
@@ -16,10 +16,11 @@ project: _Underscore
|
|
|
16
16
|
client: compass-usa
|
|
17
17
|
type: profile
|
|
18
18
|
status: active
|
|
19
|
-
updated: 2026-
|
|
20
|
-
owners: [jcardinal, bala, tcox, apeterson]
|
|
19
|
+
updated: 2026-08-04
|
|
20
|
+
owners: [jcardinal, bala, tcox, apeterson, dfranks]
|
|
21
21
|
files: []
|
|
22
22
|
related:
|
|
23
|
+
- features/mits-sales-order-transmission-alerting.md
|
|
23
24
|
- features/asn-to-item-fulfillment.md
|
|
24
25
|
- features/cost-centers.md
|
|
25
26
|
- features/approval-decision-flow.md
|
|
@@ -75,6 +76,10 @@ separate, related client (see its own profile).
|
|
|
75
76
|
`worker/crons/toga2/compass/workflow/3b_import_strategic_systems_advance_shipping_notices.php`.
|
|
76
77
|
|
|
77
78
|
## Key features (this client)
|
|
79
|
+
- [MITS Sales-Order Transmission — Rejection Alerting](features/mits-sales-order-transmission-alerting.md)
|
|
80
|
+
— MITS rejection alerts are `App_Exception_Business` Issues with recipients in
|
|
81
|
+
`Logs.IssueEmailAddress`, **not** an email in the cron: adding an alert recipient is a
|
|
82
|
+
`dbchanges2/Logs/` data change with no PHP.
|
|
78
83
|
- [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) — the
|
|
79
84
|
`_Model_Compass_AdvanceShippingNotice::postPost` handler.
|
|
80
85
|
- [Cost Centers (Unit Locations, numeric-only)](features/cost-centers.md) — what a Compass
|
|
@@ -5,3 +5,4 @@
|
|
|
5
5
|
| [Harness Distribution — How a knowledge.js Fix Reaches Teammates](workflows/harness-distribution.md) | There are **two copies** of `knowledge.js` on a developer's machine, and the skills use only one of them. | claude/scripts/install.js, claude/knowledge.js |
|
|
6
6
|
| [Knowledge Base Publish / Push Pipeline](workflows/knowledge-publish-pipeline.md) | How `/capture` and `/session-save` get knowledge docs from your working tree into `agilantsolutions/claude` on `_main`. | claude/knowledge.js |
|
|
7
7
|
| [MCP Tool Usage — "Talos" Means the Internal Knowledge Base, and Run Your Own SELECTs](workflows/mcp-tool-usage.md) | Two MCP connections are available in every session, and **Claude is expected to reach for them on its own judgement — nobody should have to say the word "MCP".* | claude/.claude/skills/kickoff/SKILL.md, claude/.claude/skills/capture/SKILL.md |
|
|
8
|
+
| [Ticket-Branch Creation Safety — How /work-ticket Armed Pushes to Production Branches](workflows/ticket-branch-creation-safety.md) | For roughly a month, `/work-ticket` created ticket branches whose **git upstream was the production branch**. | claude/skills/work-ticket/SKILL.md |
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Ticket-Branch Creation Safety — How /work-ticket Armed Pushes to Production Branches
|
|
3
|
+
framework: "standalone"
|
|
4
|
+
repo: claude
|
|
5
|
+
project: Claude Harness
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-04
|
|
10
|
+
owners: ["dfranks"]
|
|
11
|
+
files:
|
|
12
|
+
- claude/skills/work-ticket/SKILL.md
|
|
13
|
+
related:
|
|
14
|
+
- ./harness-distribution.md
|
|
15
|
+
- ../../../2.0/apps/worker2/workflows/ticket-to-pseudocode-planning.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
For roughly a month, `/work-ticket` created ticket branches whose **git upstream was the
|
|
21
|
+
production branch**. Claude's own pushes were unaffected, but a developer using VS Code's
|
|
22
|
+
**Commit + Sync** mid-session pushed ticket code straight to `_main`/`_production` — no PR, no
|
|
23
|
+
review. Two such pushes landed in two days before it was found.
|
|
24
|
+
|
|
25
|
+
The mechanism is generic git behavior, so it is worth understanding beyond this one skill.
|
|
26
|
+
|
|
27
|
+
## Root cause
|
|
28
|
+
|
|
29
|
+
Branch creation was `git checkout -b TRUE-XXXX "origin/$DEFAULT"`. Git's
|
|
30
|
+
**`branch.autoSetupMerge` defaults to `true`**, so branching from a *remote-tracking ref* adopts
|
|
31
|
+
that ref as the new branch's **upstream**. The ticket branch therefore tracked
|
|
32
|
+
`origin/_main` / `origin/_production` until the skill's later `git push -u` re-pointed it.
|
|
33
|
+
|
|
34
|
+
**Trigger asymmetry is why it hid.** Claude pushes with `git push -u origin <TICKET>` — an
|
|
35
|
+
explicit refspec, which ignores the upstream and was always safe. VS Code's Commit + Sync resolves
|
|
36
|
+
the destination **from the upstream**, so any developer commit made before the skill's first push
|
|
37
|
+
went to the production branch.
|
|
38
|
+
|
|
39
|
+
**A safety fix created the exposure.** Commit `6d155f7` (2026-07-09) moved branch creation from
|
|
40
|
+
*after* implementation to *before* any code is written — itself a genuine fix, because a stale
|
|
41
|
+
local `_production` 12 commits behind nearly reverted upstream work. But it turned a seconds-long
|
|
42
|
+
armed window into the **entire implementation phase**. Correctness fixes can lengthen an existing
|
|
43
|
+
window; check what else depends on the timing you moved.
|
|
44
|
+
|
|
45
|
+
## The fixes
|
|
46
|
+
|
|
47
|
+
1. **Skill:** branch creation uses **`--no-track`** *and* publishes the empty branch immediately
|
|
48
|
+
(`git push -u origin TRUE-XXXX`), so the branch has a correct upstream before any code exists.
|
|
49
|
+
The later push is a plain `git push origin <TICKET>`.
|
|
50
|
+
2. **Global git config:** `branch.autoSetupMerge=simple` (a checkout can no longer adopt a
|
|
51
|
+
production upstream, even outside the skill) plus `push.autoSetupRemote=true`.
|
|
52
|
+
3. **`pre-push` hook** in each app repo rejecting
|
|
53
|
+
`refs/heads/(_main|_production|_beta|_alpha|master)`, override `ALLOW_PROD_PUSH=1`. This also
|
|
54
|
+
catches Claude, because Claude shells out to real `git`.
|
|
55
|
+
4. **Deny rules** in `.claude/settings.json`.
|
|
56
|
+
5. Existing branches still tracking a production branch were **disarmed** individually.
|
|
57
|
+
|
|
58
|
+
`rework-ticket` was never affected — it checks out an existing remote branch.
|
|
59
|
+
|
|
60
|
+
## Recovery: getting an unreviewed commit off an unprotected default branch
|
|
61
|
+
|
|
62
|
+
Do **not** force-push. Instead:
|
|
63
|
+
|
|
64
|
+
1. **Revert via PR** on the default branch.
|
|
65
|
+
2. **Rebuild the ticket branch as a NEW commit** on the post-revert base.
|
|
66
|
+
|
|
67
|
+
> **The revert-remerge trap:** if you keep the original commit and merge that branch later, the
|
|
68
|
+
> original commit is already an **ancestor** of the default branch, so the merge is a silent no-op
|
|
69
|
+
> and the change **never comes back**. The revert wins and nothing warns you.
|
|
70
|
+
|
|
71
|
+
## Gotchas / known issues
|
|
72
|
+
|
|
73
|
+
- **Everything installed here is local and does not survive a fresh clone.** Hooks and git config
|
|
74
|
+
are per-machine; a teammate or a new laptop is unprotected until they redo it.
|
|
75
|
+
- **Standing exposure (not fixed):** `dbchanges2/_main`, `worker/_production`,
|
|
76
|
+
`worker2/_production` and `api2/_production` have **no branch protection and no rulesets**.
|
|
77
|
+
Server-side protection is the only durable fix and needs an admin GitHub account.
|
|
78
|
+
- **Diagnosing this class of incident:** `git reflog` on the remote-tracking branch shows
|
|
79
|
+
`update by push` entries — that is how both incidents were confirmed and how the other four
|
|
80
|
+
repos were cleared. Pre-existing `_beta` pushes are normal manual practice; check dates before
|
|
81
|
+
calling something an incident.
|
|
82
|
+
- Auditing "did this ever happen elsewhere?" requires sweeping **every** repo the skill touches,
|
|
83
|
+
not just the one that surfaced the bug.
|
|
84
|
+
|
|
85
|
+
## Change history
|
|
86
|
+
- 2026-08-04 — Documented the `branch.autoSetupMerge` root cause, the Claude-vs-VS-Code trigger
|
|
87
|
+
asymmetry that hid it for a month, and the fixes (`--no-track` + immediate branch publish,
|
|
88
|
+
global `autoSetupMerge=simple`, per-repo `pre-push` hook, settings deny rules). Recorded the
|
|
89
|
+
revert-then-rebuild recovery pattern and its revert-remerge no-op trap, plus the standing lack
|
|
90
|
+
of server-side branch protection on four app repos. (dfranks)
|
|
91
|
+
</content>
|
package/package.json
CHANGED