From f8e8b7dbcc45dfed26509d5def7f7d65db7ae4c6 Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Fri, 14 Aug 2026 11:55:33 -0400 Subject: [PATCH] fix(hygiene): the Task validator rejected the repo's own ZetaId and 57% of real usage `TASK_RE` in validate-agencysignature-pr-body.ts accepted ticket-shaped ids only. It was written before this repo retired sequential B-NNNN ids, and it cannot express a ZetaId at all - 26 chars, Crockford base32, digit-initial, no dash. Counted over every `Task:` value in the last 300 commits on main (363 values): 143 passed the old pattern 12 were bare ZetaIds - the canonical work-item key that .claude/rules/workitems-mint-with-zetaid.md mandates 208 were slugs naming the work, or a ZetaId joined to one So it rejected 57% of established practice, including the repo's own mandated id format. #10594 wired this validator to CI unchanged, so at the cutover it would have reddened the majority of correct commits - and a gate that fires mostly on correct work gets switched off, which is strictly worse than no gate: a disabled check still occupies the slot where a real one would go. Every Task value I used today would have failed, and so would the ZetaIds in agent PRs. The field's job is to NAME the work, so a slug always was a legitimate name and shape was never the property worth enforcing. What must stay caught is the unfilled template, so that is now explicit and checked FIRST, independently of shape: PLACEHOLDER_TASK_RE rejects todo / tbd / xxx / task / placeholder / fixme / n/a / bare dashes / <...>. That complements the existing PLACEHOLDER_RE rather than duplicating it - that one catches the angle-bracket skeleton across every required key, while these are the words someone types when they have not decided yet, none of which contain angle brackets and all of which would have satisfied the widened shape. The dashed alternative is case-INSENSITIVE deliberately. The commonest real form is a ZetaId joined to a slug (081KZZYWBN2087G0R003NAQQAF-exact-cyclotomic-amplitude-carrier, and the reverse order too); a lowercase-only slug rule left 23 of those still failing, which my own test caught before commit. A second self-caught regression: dropping the explicit `task-#?\d+` alternative broke `task-#12`, because `#` is not alphanumeric. Verified: replaying all 363 historical values through the new patterns gives 0 failures; every legacy form (none, Otto-343, task-#12, #99, FOO-12) still passes; all ten placeholder spellings are rejected; 18 validator unit tests pass; lint-typescript exits 0. Found by an agent while filing its own PR - it hit the rejection, worked around it with `Task: none`, and flagged the regex rather than treating the workaround as the answer. Agency-Signature-Version: 1 Agent: Otto Agent-Runtime: Claude Code Agent-Model: claude-opus-5 Credential-Identity: AceHack via gh Credential-Mode: shared Human-Review: pending Human-Review-Evidence: chat Action-Mode: autonomous-fail-open Task: fix-task-regex-rejects-zetaids Co-authored-by: Claude Opus 5 --- .../validate-agencysignature-pr-body.ts | 57 ++++++++++++++++++- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/src/Core.TypeScript/hygiene/validate-agencysignature-pr-body.ts b/src/Core.TypeScript/hygiene/validate-agencysignature-pr-body.ts index 8155b04536..8fd514d635 100644 --- a/src/Core.TypeScript/hygiene/validate-agencysignature-pr-body.ts +++ b/src/Core.TypeScript/hygiene/validate-agencysignature-pr-body.ts @@ -58,8 +58,47 @@ const REQUIRED_KEYS: readonly string[] = [ const FENCE_RE = /^[\t ]*```[A-Za-z]*[\t ]*$/; const BLANK_RE = /^[\t ]*$/; -const TASK_RE = - /^(?:none|Otto-\d+|task-#?\d+|#?\d+|[A-Za-z][A-Za-z0-9]*-\d+)$/; +/** + * What may name the work a commit belongs to. + * + * WIDENED 2026-08-14, from measurement rather than taste. The previous pattern was + * `^(?:none|Otto-\d+|task-#?\d+|#?\d+|[A-Za-z][A-Za-z0-9]*-\d+)$` — ticket-shaped ids only, + * written before this repo retired sequential `B-NNNN` ids for ZetaIds. Counted over every + * `Task:` value in the last 300 commits on `main` (363 values): + * + * 143 passed the old pattern + * 12 were bare ZetaIds — the repo's OWN canonical work-item key, which the pattern could + * not express at all (26 chars, Crockford base32, digit-initial, no dash) + * 208 were slugs naming the work, or a ZetaId joined to one + * + * So it rejected 57% of established practice, including the id format + * `.claude/rules/workitems-mint-with-zetaid.md` mandates. Wired to CI unchanged (#10594) it + * would have reddened the majority of correct commits at the cutover — and a gate that fires + * mostly on correct work gets switched off, which is strictly worse than no gate, because a + * disabled check still occupies the slot where a real one would go. + * + * The field's job is to NAME the work, so a slug always was a legitimate name; shape was + * never the property worth enforcing. What must stay caught is the UNFILLED TEMPLATE, and + * that is now `PLACEHOLDER_TASK_RE` below — so widening the shape does not widen the escape. + * + * The dashed alternative is case-INSENSITIVE deliberately: the commonest real form is a + * ZetaId joined to a slug (`081KZZYWBN2087G0R003NAQQAF-exact-cyclotomic-amplitude-carrier`, + * and the reverse order too), and a lowercase-only slug rule rejected all 23 of those. + */ +const ZETA_ID = "[0-9][0-9A-HJKMNP-TV-Z]{25}"; +const TASK_RE = new RegExp( + `^(?:none|${ZETA_ID}|task-#?\\d+|#?\\d+|[A-Za-z0-9]+(?:-[A-Za-z0-9]+)+)$`, +); + +/** + * The unfilled template, which must stay rejected however the shape widens. + * + * Complements `PLACEHOLDER_RE` below rather than duplicating it: that one catches the + * angle-bracket skeleton `` across every required key, while these are the words a + * human or agent types when they have not decided yet — `TODO`, `tbd`, `task` — none of + * which contain angle brackets and all of which would satisfy the widened shape. + */ +const PLACEHOLDER_TASK_RE = /^(?:<[^>]*>|todo|tbd|xxx+|task|placeholder|fixme|n\/a|-+)$/i; const ENUMS: readonly { readonly key: string; readonly allowed: readonly string[] }[] = [ { key: "Agency-Signature-Version", allowed: ["1", "2"] }, @@ -416,12 +455,24 @@ function checkEnums(trailers: string): ExitCode | null { function checkTaskPattern(trailers: string): ExitCode | null { const value = getValue(trailers, "Task"); + // Placeholder first, and independently of shape: `task` and `todo` both satisfy the widened + // pattern, and an unfilled template passing validation is the exact defect this validator + // exists to catch — attribution to nobody, wearing a green check. + if (PLACEHOLDER_TASK_RE.test(value)) { + process.stdout.write("FAIL: Task is an unfilled placeholder\n"); + process.stdout.write(` Found: '${value}'\n`); + process.stdout.write(" Expected: the work this commit belongs to, or the literal 'none'\n"); + process.stdout.write(` Spec: ${SPEC_DOC} Section 9.2 (Task: none fallback)\n`); + return 1; + } if (TASK_RE.test(value)) return null; process.stdout.write("FAIL: invalid Task value\n"); process.stdout.write(` Found: '${value}'\n`); process.stdout.write( - " Expected: a ticket-id (e.g. Otto-NN, task-#NNN, #NNN, FOO-NN)\n", + " Expected: a ZetaId work-item key (26 chars, e.g. 081M0085XQT087G0R003W4KFS4),\n", ); + process.stdout.write(" a slug naming the work (e.g. fix-merge-duty-ordering),\n"); + process.stdout.write(" a ticket-id (Otto-NN, task-#NNN, #NNN, FOO-NN),\n"); process.stdout.write(" or the literal 'none' fallback\n"); process.stdout.write(` Spec: ${SPEC_DOC} Section 9.2 (Task: none fallback)\n`); return 1;