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.
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.
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.
motir-marketing runs the design-result publisher on every pull_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 --check parses, diff vs origin/main byte-identical).id-token: write while 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.work_item:edit, stored as the MOTIR_UPLOAD_TOKEN repository secret on moooon-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.Published N design artifact(s) to MOTIR-<n> … evidence <id>, and that evidence id is on the card.33171127277, job Design result, 2026-08-28T12:27:18Z:
Design note: 11 changed section(s) from design/marketing/design-notes.md.
Published 3 design artifact(s) to MOTIR-1143 (target from branch); evidence cmtcxfev000b2hwn8sw8kuuja.
That line prints only after POST /api/work-items/MOTIR-1143/design-evidence returns 2xx; the id is the row the server created, which is the row designEvidenceService.getCurrentForWorkItem feeds 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 + DELETE only), 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.classifyDesignPath's note arm either accepts a *.design-notes.md suffix 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 BOTH design-notes.md (the area index) AND create-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 one CLAUDE.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.ts covers 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 by tests/design-three-file-set.test.ts.design/org-admin/create-workspace.design-notes.md's own result is republished or its absence recorded, whichever the decision above implies.design-result.md AMENDMENT 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: resolveTargetKey routes on the first resolvable MOTIR-<n> in DESIGN_TARGET_KEY → the branch ref → the PR title, so touching that file on the fixing branch would have published org-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.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.
motir-core/scripts/upload-design-assets.mjs — classifyDesignPath, main's four exit-0 conditions, resolveTargetKey's DESIGN_TARGET_KEY override, and authHeadersFor'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 and DESIGN_PUBLISH_PERMISSION = 'work_item:edit'.motir-marketing/.github/workflows/ci.yml — the shipped design-result job 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.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.