Filed by the director seat (summon #20, session_01Tep4AYXZvyBA7jsvne5KZV, os-bill) as the follow-up card owed by a post-merge contract review at CONTRACT_REVIEW_TIER that returned CHANGES REQUIRED on an already-merged head: #8779 (comment) (objectui PR #8779 @ a04441f7c9, merged as 6cc48c4e4c at 2026-09-09T06:42:59Z). Refs objectui#8041 (closed by that PR; its ruling — director batch #91, comment 5583986682, option C — is the standing ruling here, ⛔ not reopened).
What is wrong on main (measured by the review on the merged sources, driven through a real pty / pipe)
- Cancel at the Author or Description prompt →
TypeError, exit 1, an empty packages/plugin-<name>/ left behind. packages/create-plugin/src/index.ts:83 calls prompts([...]) with no onCancel; prompts returns the partial answers on abort, so answers.author is undefined; fs.mkdirpSync(targetDir) at index.ts:116 has already run; the new licenseCopyrightHolder (templates.ts:537, vars.author.trim()) is the first code to dereference it, before any file is written (index.ts:135). The retry then fails at index.ts:108 ("Directory already exists"). Regression: the base commit wrote 9 files and exited 0 on the same cancel. A cancel at the fourth (License:) prompt is fine (MIT + 10 files).
- The non-TTY claim is false.
packages/create-plugin/README.md:66-68 (published) and .changeset/8041-create-plugin-licence-prompt.md assert "A non-interactive run (no TTY, or a cancelled prompt) takes MIT and still writes the text". Measured: </dev/null → nothing written, exit 0, silent; piped `
→ nothing written. The behaviour is pre-existing (base identical) but the claim is new and was never measured — every piece of evidence in the PR was pty-driven. Ruling C's fourth clause ("non-interactive runs take the default and still write it") is therefore **not met**. 3. Non-blocking, same area:templates.ts:551-556says the MIT fallback keeps manifest and text in agreement, butbuildPackageJsonwritesvars.license verbatim (:148) while buildLicenseFile (:558) emits MIT text, so buildPluginFiles({license:'NOT-A-LICENCE'})yields a manifest claimingNOT-A-LICENCEbeside MIT text (internal-only: the package isbin-only). Nothing pins prompt order or the cancel / non-TTY CLI path; index.ts is unimportable (program.parse()at import,index.ts:158`), which is exactly where item 1 lives.
What the fix must do (from the review, verbatim in substance)
- Handle
prompts cancellation in packages/create-plugin/src/index.ts: an onCancel that exits non-zero before fs.mkdirpSync (nothing written, no leftover directory), or at minimum coalesce answers.author ?? '' / answers.description ?? initial so licenseCopyrightHolder cannot throw.
- Make the non-TTY claim true or remove it: either detect
!process.stdin.isTTY and take every prompt's initial (MIT included, still writing LICENSE) — the ruling-conformant direction — or correct README.md:66-68 and the changelog text to the measured behaviour.
- Add one test that drives the bundled CLI (or an extracted
createPlugin) through cancel-at-Author and EOF stdin, so the emitted-file contract is pinned at the CLI level, not only at buildPluginFiles.
- Optional, same commit: make the
templates.ts:551-556 fallback throw (or fix the docblock) so manifest and text cannot disagree.
Acceptance
- Cancel at any of the four prompts: no
TypeError; either nothing is written and the exit is non-zero with a message, or (ruling direction) the defaults are taken and the full 10-file set is written — one behaviour, stated in the README.
create-plugin </dev/null and piped input: the README's sentence and the measured behaviour agree.
- A CLI-level test pins both paths;
templates.test.ts:565's 10-key set and licenses.test.ts stay green.
Constraints
Clause-② at claim time: expect yes — the scaffolder's emitted file set and prompt behaviour are its published behaviour (the ruling's own words); draft PR + needs:contract-review on both carriers; a CONTRACT_REVIEW_TIER verdict on record before enqueue.
- Changeset:
@object-ui/create-plugin — patch if only the crash and the prose move; minor if the non-TTY default path is added (published behaviour grows). Never major (AGENTS.md).
- ⛔ Do not reopen the licence-set question (four licences, no free text) — the maintainer's call, flagged on the PR body; objectui#8778 (copyright holder in the four emitted headers) stays separate.
Generated by Claude Code
Filed by the director seat (summon #20,
session_01Tep4AYXZvyBA7jsvne5KZV,os-bill) as the follow-up card owed by a post-merge contract review atCONTRACT_REVIEW_TIERthat returned CHANGES REQUIRED on an already-merged head: #8779 (comment) (objectui PR #8779 @a04441f7c9, merged as6cc48c4e4cat 2026-09-09T06:42:59Z). Refs objectui#8041 (closed by that PR; its ruling — director batch #91, comment 5583986682, option C — is the standing ruling here, ⛔ not reopened).What is wrong on
main(measured by the review on the merged sources, driven through a real pty / pipe)TypeError, exit 1, an emptypackages/plugin-<name>/left behind.packages/create-plugin/src/index.ts:83callsprompts([...])with noonCancel;promptsreturns the partial answers on abort, soanswers.authorisundefined;fs.mkdirpSync(targetDir)atindex.ts:116has already run; the newlicenseCopyrightHolder(templates.ts:537,vars.author.trim()) is the first code to dereference it, before any file is written (index.ts:135). The retry then fails atindex.ts:108("Directory already exists"). Regression: the base commit wrote 9 files and exited 0 on the same cancel. A cancel at the fourth (License:) prompt is fine (MIT + 10 files).packages/create-plugin/README.md:66-68(published) and.changeset/8041-create-plugin-licence-prompt.mdassert "A non-interactive run (no TTY, or a cancelled prompt) takes MIT and still writes the text". Measured:</dev/null→ nothing written, exit 0, silent; piped `→ nothing written. The behaviour is pre-existing (base identical) but the claim is new and was never measured — every piece of evidence in the PR was pty-driven. Ruling C's fourth clause ("non-interactive runs take the default and still write it") is therefore **not met**. 3. Non-blocking, same area:templates.ts:551-556says the MIT fallback keeps manifest and text in agreement, butbuildPackageJsonwritesvars.licenseverbatim (:148) whilebuildLicenseFile(:558) emits MIT text, sobuildPluginFiles({license:'NOT-A-LICENCE'})yields a manifest claimingNOT-A-LICENCEbeside MIT text (internal-only: the package isbin-only). Nothing pins prompt order or the cancel / non-TTY CLI path;index.tsis unimportable (program.parse()at import,index.ts:158`), which is exactly where item 1 lives.What the fix must do (from the review, verbatim in substance)
promptscancellation inpackages/create-plugin/src/index.ts: anonCancelthat exits non-zero beforefs.mkdirpSync(nothing written, no leftover directory), or at minimum coalesceanswers.author ?? ''/answers.description ?? initialsolicenseCopyrightHoldercannot throw.!process.stdin.isTTYand take every prompt's initial (MIT included, still writingLICENSE) — the ruling-conformant direction — or correctREADME.md:66-68and the changelog text to the measured behaviour.createPlugin) through cancel-at-Author and EOF stdin, so the emitted-file contract is pinned at the CLI level, not only atbuildPluginFiles.templates.ts:551-556fallback throw (or fix the docblock) so manifest and text cannot disagree.Acceptance
TypeError; either nothing is written and the exit is non-zero with a message, or (ruling direction) the defaults are taken and the full 10-file set is written — one behaviour, stated in the README.create-plugin </dev/nulland piped input: the README's sentence and the measured behaviour agree.templates.test.ts:565's 10-key set andlicenses.test.tsstay green.Constraints
Clause-②at claim time: expectyes— the scaffolder's emitted file set and prompt behaviour are its published behaviour (the ruling's own words); draft PR +needs:contract-reviewon both carriers; aCONTRACT_REVIEW_TIERverdict on record before enqueue.@object-ui/create-plugin—patchif only the crash and the prose move;minorif the non-TTY default path is added (published behaviour grows). Nevermajor(AGENTS.md).Generated by Claude Code