8.45 `motir-marketing` publishes no DESIGN RESULT — it has no publish lane at all, and a `<name>.design-notes.md` basename is invisible to the classifier in either repo
Found by motir run MOTIR-1143, which delivered the first design asset ever to land in motir-marketing and produced no design result on its card. There are two independent causes, and fixing either one alone still leaves the card empty.
▶ FULLY DELIVERED 2026-08-28 — three pull requests, and the last one is motir-core's. All six criteria below are now struck through or dispositioned with their evidence.
half PR the publish lane in motir-marketingmotir-marketing#3 (merged 11:24Z)the keyless id-token: write, once MOTIR-3743 connected the repomotir-marketing#4 (merged 11:52Z)classifyDesignPath's note arm + the ADR amendmentmotir-core#2418 — the pull request LINKED to this card ⚠️
Closes MOTIR-3750in motir-marketing#3's body is an OVERCLAIM — that pull request delivered a proper subset of this card, and so did #4. Neither is linked to it; motir-core#2418 is the one that completes it.
Cause 1 — the repository has no publish lane · FIXED
The design-result publisher is the Publish the design result step of motir-core's ci.yml design-guards job, running scripts/upload-design-assets.mjs. motir-marketing's CI was lint · typecheck · build · deploy and had no equivalent. A design asset merged there reached no card at all.
The script is portable, which is what made this fixable rather than a rewrite: it imports only node:child_process, node:fs and node:path plus global fetch — zero external dependencies — so bare node runs it in any checkout. It is FETCHED from motir-core (public repo) pinned to a commit SHA, never forked.
And the PAT path is repository-AGNOSTIC, which is the discovery that makes this work at all: authenticateCiPublisher tries keyless GitHub OIDC first and falls back to authenticateApiToken(req, DESIGN_PUBLISH_PERMISSION), where DESIGN_PUBLISH_PERMISSION = 'work_item:edit'. That path performs no repository check — it authenticates the token, resolves its workspace, and resolves the card by identifier.
⚠️ The keyless path canNOT be used until MOTIR-3743 lands, and turning it on early is RED, not inert. ⚠️ SUPERSEDED 2026-08-28 — MOTIR-3743 IS done. The reasoning above was correct at the time and is kept because it is the trap: authenticateGithubOidc resolves the tenant from the verified repository claim via githubRepoRepository.findConnectedByName(owner, name) and returns 403 repo_not_connected for an unconnected repo, and the script PREFERS OIDC whenever it can mint one — so id-token: write on an unconnected repository is a red check on every pull request, not a quiet skip. The repository is now connected, so motir-marketing#4 turned the line on and the lane is keyless. The PAT arm stays wired and deliberately UNSET as the fork-PR fallback, where GitHub denies OIDC.
Cause 2 — the note's BASENAME, and it is not only this repository's problem
classifyDesignPath in scripts/upload-design-assets.mjs:
if (filePath.endsWith('.mock.html')) return 'mock';
if (filePath.endsWith('.png')) return 'image';
if (path.basename(filePath) === NOTES_BASENAME) return 'note'; // 'design-notes.md'
return null;
The mock and the export match on SUFFIX; the note matches on exact basename. So a file named <name>.design-notes.md classifies as null, lands in ignored, and is never published — while its sibling .mock.html and .png publish normally. The failure is silent and partial: the card gets pictures and no words.
Measured on motir-core origin/main (2026-08-28), and the predicate is "every .md under design/ that is not exactly design-notes.md", not a grep for one name:
git ls-tree -r --name-only origin/main design/ | grep -E '\.md$' | grep -v -E '/design-notes\.md$'
→ design/org-admin/create-workspace.design-notes.md (1)
git ls-tree -r --name-only origin/main design/ | grep -cE '/design-notes\.md$'
→ 43 ⚠️ CORRECTED — this card said 44
⚠️ TWO CORRECTIONS TO THIS SECTION, made by the run that fixed it (motir-core#2418). Neither changes the fix; both are on the record because a number and a cause were asserted and neither held.
1. The denominator was 43, not 44. Re-run with the card's own command: 43 at
a482d16b8(today'sorigin/main) AND 43 at6e71acf21, the commit motir-marketing's lane pins — andgit log --diff-filter=DRfinds nodesign-notes.mddeleted or renamed in the window, so the number did not drift; it was one high when written. The load-bearing half — exactly ONE non-conforming file — reproduces exactly.2. "That one file's notes have never reached their card either" is TRUE, but NOT because of the classifier.
design/org-admin/create-workspace.design-notes.mdwas last touched 2026-08-10 (#2004, MOTIR-2542);scripts/upload-design-assets.mjsfirst landed 2026-08-12 (cacfe0180, MOTIR-2664). The publisher ispull_request-only, and no pull request has touched that file in the two days since the publisher existed — so the mechanism was never there to drop it. The classifier has cost that file nothing yet; it would have, on its next edit.
Acceptance criteria
MET — motir-marketing#3, merged 2026-08-28. Fetched frommotir-marketingruns the design-result publisher on everypull_request, using motir-core's script rather than a fork of it, and the mechanism for getting it there is stated in the workflow with the reason.motir-core@6e71acf21c1426180bf502c387e221108930e381(checked before trusting: 977 lines, anonymous fetch,node --checkparses,diffvsorigin/mainbyte-identical).The lane is green and inert with no credential configured, and does not carryMET — observed on motir-marketing#3's own run:id-token: writewhile MOTIR-3743 is open. A comment names the one line to add once it closes.Fetched publisher from motir-core@6e71acf…/Nothing to publish — this PR changed no design artifact./Design result pass 11s. The comment named the line; motir-marketing#4 added it once MOTIR-3743 closed.The credential is a named MANUAL step, not an assumption: a Motir API token grantedSUPERSEDED, not delivered — and by the better fix this criterion itself named. MOTIR-3743 iswork_item:edit, stored as theMOTIR_UPLOAD_TOKENrepository secret onmoooon-B-V/motir-marketing.done, the repository is connected, and motir-marketing#4 made the lane keyless. No credential was created and none is owed. The PAT arm stays wired and unset as the fork-PR fallback only.Verified by OUTPUT, not by a green check: the job log readsMET, on the real asset, keyless, in CI. motir-marketing PR #2, runPublished N design artifact(s) to MOTIR-<n> … evidence <id>, and that evidence id is on the card.33171127277, jobDesign result,2026-08-28T12:27:18Z:
That line prints only afterDesign note: 11 changed section(s) from design/marketing/design-notes.md. Published 3 design artifact(s) to MOTIR-1143 (target from branch); evidence cmtcxfev000b2hwn8sw8kuuja.POST /api/work-items/MOTIR-1143/design-evidencereturns 2xx; the id is the row the server created, which is the rowdesignEvidenceService.getCurrentForWorkItemfeeds to the item page's Design result panel. Three assets were PUT to their signed tokens first, and a failed PUT throws. Stated limit: the design result has no GET route (POST+DELETEonly), so its only consumer surface is the authenticated item page, which this run could not render. The evidence is the server's own confirmation, not a page render.DECIDED: WIDEN the note arm (motir-core#2418), and the rename was rejected on shipped reality rather than taste —classifyDesignPath's note arm either accepts a*.design-notes.mdsuffix as it accepts*.mock.html, or the one non-conforming file in motir-core is renamed and a guard in the design lane keeps the convention. Decide which; do not do both.design/org-admin/already carries BOTHdesign-notes.md(the area index) ANDcreate-workspace.design-notes.md, so the rename is a content MERGE of two spec files, and it leaves the trap armed for the next author. The losing name is also the oneCLAUDE.md's three-file rule literally prescribes ("land all three, with a shared basename"), which is why two authors wrote one ten weeks apart in two repositories.tests/design-assets-uploader.test.tscovers the new arm in three cases, including that the.separator is load-bearing.docs/decisions/design-result.md§1 is amended (AMENDMENT 1) because its table documented the exact basename as the contract. The per-area convention is unchanged and still enforced bytests/design-three-file-set.test.ts.ABSENCE RECORDED — indesign/org-admin/create-workspace.design-notes.md's own result is republished or its absence recorded, whichever the decision above implies.design-result.mdAMENDMENT 1 and in correction 2 above. The widening implies it: the file's last edit predates the publisher, so nothing was ever dropped, and the next pull request that touches it publishes it — the first time any pull request could have. Republishing by hand was considered and rejected:resolveTargetKeyroutes on the first resolvableMOTIR-<n>inDESIGN_TARGET_KEY→ the branch ref → the PR title, so touching that file on the fixing branch would have publishedorg-admin's spec onto MOTIR-3750 — the drive-by-asset hazard, arriving green with a real evidence id on a card nobody would think to check.
⚠️ ORDERING — merging motir-marketing#2 early COSTS MOTIR-1143 its design result
The publish step is pull_request-only, deliberately (motir-core does the same, so an identical result is not re-published after the merge — design-result.md §6). On push: main the job's if is false and it is skipped. So there is no post-merge path: if motir-marketing#2 merges before a credential exists, no CI run ever publishes MOTIR-1143's asset.
▶ DISCHARGED — the ordering held. #3 and #4 both merged before #2, #2's next run picked up the keyless lane, and it published (the fourth criterion above). The mechanism it describes is unchanged and still true of any future asset: there is no post-merge path, so a design asset in motir-marketing publishes from its own pull request or not at all.
Context refs
motir-core/scripts/upload-design-assets.mjs—classifyDesignPath,main's four exit-0 conditions,resolveTargetKey'sDESIGN_TARGET_KEYoverride, andauthHeadersFor's OIDC-before-PAT preference.motir-core/lib/github/oidcAuth.ts—findConnectedByName→403 repo_not_connected, the reason the keyless path waited on MOTIR-3743.motir-core/lib/publishAuth/ciPublishAuth.ts+lib/tokens/grant.ts— the PAT path andDESIGN_PUBLISH_PERMISSION = 'work_item:edit'.motir-marketing/.github/workflows/ci.yml— the shippeddesign-resultjob and the reasoning in its comments.motir-core/docs/decisions/design-result.md— what a design result is and where it attaches; §1 + AMENDMENT 1.- MOTIR-3743 — the repository connection; closing it unlocked the keyless path and removed the secret.
- MOTIR-1143 — the asset that surfaced both causes, and the fixture the OUTPUT check was measured on.
- MOTIR-3724 — the precedent for the one thing this card deliberately did NOT decide: where a mechanism shared by two repositories should live. The pinned fetch is the least-committal option, not an answer. STILL OPEN, and now filed as its own card.
Repository
targetRepo: motir-core — the repository the REMAINING deliverable landed in. motir-marketing's half shipped as #3 and #4 before this card was claimed; each delivered a proper subset, so neither is linked (a link asserts a pull request delivers the card, and the sync has no notion of partial completion). motir-core#2418 is the linked pull request that completes it.