harness init writes --lang and --title into harness.yaml unvalidated #271
Labels
No labels
bug
enhancement
needs-triage
ready-for-agent
ready-for-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle#271
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
harness initsubstitutes--titleand--langintoharness.yaml,hugo.tomlandAGENTS.mdas raw text: it neither validates the language tag nor renders the strings as the target file formats require. A bad--langor a--titlecontaining a single quote or a line break produces a site whoseharness.yamlandhugo.tomldo not load, andinitexits 0 anyway.initmust refuse invalid input before writing anything and render valid input safely.Current behavior
Reproduced on
main(3a6c020) with a binary built from./cmd/harness:initexitharness checkon the new siteharness init --site s1 --lang "Bad Code"harness.yaml: languages entry "Bad Code" is not a valid language tagharness init --site s4 --lang "en'"harness.yaml: parse …: [7:13] value is not allowed in this contextharness init --site s2 --title "Bob's Site"harness.yaml: parse …: [1:13] value is not allowed in this context(andhugo.tomlline 5 istitle = 'Bob's Site', equally broken)harness init --site s3 --title $'Line1\nLine2'hugo.toml does not parse: … literal strings cannot have new linesCause:
initcmd.substitutedoesstrings.ReplaceAllof@@TITLE@@,@@LANG@@and@@LANG_LABEL@@into templates that already carry the quotes (title: '@@TITLE@@'inscaffold/site/harness.yaml,title = '@@TITLE@@'anddefaultContentLanguage = '@@LANG@@'inscaffold/site/hugo.toml,# @@TITLE@@ — a deckle siteinscaffold/site/AGENTS.md).initcmd.Runchecks only that the target directory is empty;docs/harness.mdcurrently documents the gap ("the code is written as given,initdoes not validate it").harness lang addalready gets this right for the same two files:langcmd.checkCoderejects a code that failspagepath.ValidLangTag,langcmd.checkLabelrejects a label with a control character, andtomlQuote/yamlQuoteininternal/langcmdrender a string as a TOML value and a single-quoted YAML scalar respectively.Desired behavior
harness init --lang <x>wherexfails the language-tag rule exits 2 with a message on stderr naming the flag and the value, in the wordinglang adduses ("Bad Code" is not a valid language code: use a lowercase BCP-47-ish tag like "de", "fr" or "pt-BR"). Nothing is written: the target directory is exactly as it was (absent stays absent, empty stays empty).harness init --title <t>wheretcontains a line break or any other control character exits 2 the same way, with nothing written. Such a title can be held by none of the three files (hugo.tomlTOML,harness.yamlYAML, the Markdown heading inAGENTS.md).'in a title yields aharness.yamland ahugo.tomlthatsiteconfig.Loadand Hugo accept, with the title read back verbatim.",\and non-ASCII text round-trip too. The default title and default language keep producing byte-identical output to today's scaffold (the manifest and hugo-build tests pin that).English,Deutsch, else the code) is rendered through the same YAML rendering as the title, so the rule is applied in one place.Decisions
--langand an un-storable--titleare usage errors (exit 2), not exit 1.docs/harness.mddefines exit 2 as "a usage error", andservealready exits 2 for an invalid--addror--preview-base-urlvalue; both inputs here are flag values the caller typed. (lang addexits 1 on a bad code because the code is a positional reported through the--jsonstatus object;inithas no--jsonand no status object.) Reason: a scriptedinitmust be able to tell "you passed a bad flag" from "the directory was not empty" (exit 1).CLAUDE.md"Every rule has exactly one owner": the language-tag rule ispagepath.ValidLangTagandinitcalls it. The "no control characters in a one-line config value" rule, the TOML string rendering and the YAML scalar rendering exist once already ininternal/langcmd(checkLabel,tomlQuote,yamlQuote); move them to a package bothinitcmdandlangcmdimport (internal/siteconfigownsharness.yaml's shape and is imported by both, or a small new package) and havelangcmdcall the moved functions. Do not add a secondtomlQuote/yamlQuote/checkLabelininitcmd, and do not importlangcmdfrominitcmd.init's never-writes-on-refusal guarantee.initcmd.Runresolves every precondition before the first byte; flag validation belongs with those preconditions, before the conflict scan writes its verdict.hugo.toml, YAML forharness.yaml, raw for the Markdown heading inAGENTS.md). How the marker expresses that (a per-format marker, or substitution that knows the destination file) is the implementer's call. The existing "unresolved@@marker" guard insubstitutemust keep passing for the default scaffold.Key interfaces
initcmd.Options{SiteDir, Title, Lang}andinitcmd.Run— gain validation ofLang(language-tag rule) andTitle(no control characters) that returns an error beforescan/buildPlan; the CLI maps that error to exit 2, while*ConflictErrorkeeps mapping to exit 1. A typed error (or a sentinel) letscmd/harnesstell the two apart.initcmd.substitute(data, opts)— the markers inharness.yamlandhugo.tomlmust receive a rendered scalar, not raw text;AGENTS.mdkeeps raw text.initSiteincmd/harness/main.go— the exit-code mapping; its stderr message starts withharness init:like every other refusal.langcmd.checkLabel,langcmd.tomlQuote(intomledit.go),langcmd.yamlQuote(inyamledit.go) — the owners to relocate;langcmd.checkCodeis the model for the message text.pagepath.ValidLangTag— the language-tag rule; the only predicateinitmay use for--lang.scaffold/site/harness.yaml,scaffold/site/hugo.toml,scaffold/site/AGENTS.md, listed ininitcmd.templatedFiles.internal/initcmd/init.go,internal/langcmd/{langcmd,tomledit,yamledit}.go,cmd/harness/main.go.Documentation and skills (owners per CLAUDE.md)
docs/harness.md, theinitbullet under "Commands": replace "the code is written as given,initdoes not validate it" with the validated behaviour, the exit-2 refusals and that the title is quoted for each file. Theharness.yamlsection's "harness initwritestitle,languages, …" line stays true. Nothing new goes inREADME.md.scaffold/site/AGENTS.md— documents neitherinit's flags nor the scaffold's quoting; no change.initwrites its own title into the heading, which is not a documented behaviour.scaffold/site/.claude/skills/—setup-site,migrate-siteandredo-themementionharness initonly as a prerequisite that has run; none walks through its flags. No skill change, but re-read those three mentions and confirm.docs/agents/docs-map.mdalready routescmd/harnessandinternal/initcmdtodocs/harness.md; no change.Acceptance criteria
harness init --site <empty-or-absent dir> --lang "Bad Code"exits 2, prints a message on stderr that names--langand the value, and leaves the directory absent/empty.--lang "en'",--lang ""is unaffected (it still defaults toen), and--lang pt-BRstill succeeds with{code: pt-BR, label: pt-BR}.harness init --title $'a\nb'exits 2 with nothing written; a title with a tab or another control character behaves the same.harness init --title "Bob's Site"exits 0;harness check --site <dir>reports 0 errors;siteconfig.LoadreturnsTitle == "Bob's Site";hugo.tomlparses and itstitlereads back asBob's Site;AGENTS.md's first line is# Bob's Site — a deckle site.",\,''and non-ASCII text round-trip throughharness.yamlandhugo.tomlverbatim.title: 'My Site',code: 'en',label: 'English',title = 'My Site'): the default-scaffold manifest, substitution and hugo-build tests pass unchanged apart from any new assertions.checkLabel,tomlQuoteandyamlQuote(or their renamed equivalents) exist once, in a package bothinitcmdandlangcmdimport;langcmdcalls the moved functions;grep -rn "func tomlQuote\|func yamlQuote"finds one definition each.initis a call topagepath.ValidLangTag(directly or through the sharedcheckCode), not a new regexp.docs/harness.mdno longer saysinitdoes not validate--lang, and documents the exit-2 refusals and the quoting;go test ./docs/ ./cmd/harness/(the docs drift tests) pass.harness lang addbehaviour and output are unchanged (its tests pass as they are).How to verify
internal/initcmd/init_test.gofor each refused input (error returned, target directory untouched) and each round-tripped title (loadharness.yamlviasiteconfig.Load, parsehugo.tomlwith the TOML library the repo already uses, read theAGENTS.mdheading). The quoted-title and bad-lang cases fail onmaintoday.cmd/harness/main_test.gonext toTestInitSite…: exit 2 and stderr for the refusals, exit 1 unchanged for a non-empty target.go test ./internal/initcmd/ ./internal/langcmd/ ./internal/siteconfig/ ./cmd/harness/ ./docs/ ./scaffold/and thengo test ./...(needsweb/dist; a stubweb/dist/index.htmlsuffices for the Go build).harness checkreports 0 errors andharness servestarts.Assumptions to check
internal/siteconfigimports onlypagepathamong internal packages,langcmdalready imports it andinitcmdimports no internal package at all, sositeconfigis a cycle-free home for the shared helpers. A small new package is equally fine if the helpers feel out of place there.tomlQuoteemits for every title the tests try (it is the same functionlang addalready relies on, butinit's titles may hold characterslang addlabels never did).goccy/go-yaml, whichsiteconfig.Loaduses, reads''inside a single-quoted scalar as one'(it should; verify with the round-trip test rather than assume)._test.gofile asserts on the literal@@TITLE@@/@@LANG@@template lines, so the marker spelling can change freely.TestEmbeddedThemeMatchesScaffoldcompares the theme tree only.Out of scope
@@is refused today by the unresolved-marker guard with a misleading message; leave that behaviour and its message alone unless the marker redesign removes it for free.--titleis written as is;--title ""keeps defaulting toMy Site. Not changed.init's default label (English/Deutschforen/de, else the code) withlang add's (always the code): two documented rules, not touched here.lang add's exit code for a bad code (1) or its messages.--sitebeyond whatinitdoes now.Blocked by
None - can start immediately
Original report
Found while verifying docs/harness.md for #264.
harness init --lang "Bad Code"writes the value intoharness.yamlas given; the new site'sharness.yamlthen fails to load (reproduced).title: '@@TITLE@@'with no escaping, so a--titlecontaining a single quote very likely produces invalid YAML (read from the template, not run).Expected:
initvalidates--langas a language tag (the rulepagepathowns) and quotes the title safely, refusing with exit 2 / 1 before writing anything.Triage Notes
Reproduced, both halves. On
main(3a6c020)harness init --lang "Bad Code",--lang "en'",--title "Bob's Site"and a title with a line break all exit 0 and leave a site on whichharness checkexits 1 (harness.yamlorhugo.tomldoes not parse). The single-quote case the report only read from the template is confirmed by running it;hugo.tomlbreaks the same way asharness.yaml.Cause.
initcmd.substitutestring-replaces the markers into templates that already carry the quotes.harness lang addalready owns the piecesinitneeds:pagepath.ValidLangTagviacheckCode,checkLabelfor control characters, andtomlQuote/yamlQuoteininternal/langcmd.Recommendation:
bug,ready-for-agent. The body is now the agent brief; nothing is left open for the reporter.One judgment call to confirm or flip: the brief settles both refusals as usage errors (exit 2), following
serve's invalid-flag-value precedent and the docs' exit-code definition, even thoughlang addexits 1 on a bad code (it reports through a--jsonstatus object;inithas none). Reply "exit 1" if you preferlang add's convention and I will fold it in on the next run.Ready to promote.