[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M78DDPMA20MBGSP9CW16J6SD":3,"skill-same:01M78DDPMA20MBGSP9CW16J6SD":43},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":18,"linkOnly":19,"markdownContent":20,"markdownBody":21,"frontMatter":22,"refName":23,"commitSha":24,"skillDirectory":25,"forksCount":13,"repositoryHtmlUrl":26,"repositoryHomepageUrl":6,"ownerHtmlUrl":27,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":28,"install":29,"resources":42},"01M78DDPMA20MBGSP9CW16J6SD","officecli-word-form",null,"Use this skill to create fillable Word forms (.docx) with real Content Controls (SDT) + legacy FormField checkboxes + MERGEFIELD mail-merge placeholders + document protection. Trigger on: 'fillable form', 'form fields', 'content controls', 'SDT', 'word form', 'fill in', 'only editable fields', 'protect document', 'onboarding form', 'HR intake', 'survey template', 'contract \u002F SOW template', 'mail-merge template', 'compliance checklist', 'medical intake questionnaire'. Output is a single .docx where specific fields are editable and the rest is locked. This skill is INDEPENDENT, not a scene layer on docx — payload is `\u003Cw:sdt>` + `\u003Cw:ffData>` + `\u003Cw:fldChar>` + `documentProtection`, none of which docx base skill covers. Do NOT trigger for regular reports, letters, memos, academic papers, pitch decks, or any document with no user-fillable fields — route those to officecli-docx or its scene layers.","Grong\u002Fopenhub-client","Grong","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F4919858?v=4","01M78DABC3WYM9Z67DC6JSDGQF","01M78DABD6RAC4TNESYHD8WT4B",0,"Apache-2.0","https:\u002F\u002Fgithub.com\u002FGrong\u002Fopenhub-client\u002Fblob\u002Fmain\u002Fcrates\u002Fbackend\u002Fopenhub-app\u002Fassets\u002Fbuiltin-skills\u002Fofficecli-word-form\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: officecli-word-form\ndescription: \"Use this skill to create fillable Word forms (.docx) with real Content Controls (SDT) + legacy FormField checkboxes + MERGEFIELD mail-merge placeholders + document protection. Trigger on: 'fillable form', 'form fields', 'content controls', 'SDT', 'word form', 'fill in', 'only editable fields', 'protect document', 'onboarding form', 'HR intake', 'survey template', 'contract \u002F SOW template', 'mail-merge template', 'compliance checklist', 'medical intake questionnaire'. Output is a single .docx where specific fields are editable and the rest is locked. This skill is INDEPENDENT, not a scene layer on docx — payload is `\u003Cw:sdt>` + `\u003Cw:ffData>` + `\u003Cw:fldChar>` + `documentProtection`, none of which docx base skill covers. Do NOT trigger for regular reports, letters, memos, academic papers, pitch decks, or any document with no user-fillable fields — route those to officecli-docx or its scene layers.\"\n---\n\n# OfficeCLI Word-Form Skill\n\n**This skill is INDEPENDENT, not a scene layer on docx.** A form's payload — `\u003Cw:sdt>` controls, `\u003Cw:ffData>` legacy fields, `\u003Cw:fldChar>` mail-merge, `documentProtection` — is a distinct element class from docx's paragraph\u002Fheading\u002Fstyle primitives. Its QA is different too: docx's Delivery Gate cares about visual layout and live PAGE fields, this skill's cares about data plumbing (protection enforced \u002F alias+tag \u002F items injected \u002F name ≤ 20 \u002F no underscore anti-pattern). **Reverse handoff:** if the user's document has no fillable fields (report, letter, memo, thesis, proposal), route to `officecli-docx` or a docx scene skill — don't use this one.\n\n## BEFORE YOU START (CRITICAL)\n\n**If `officecli` is not installed:**\n\n`macOS \u002F Linux`\n\n```bash\nif ! command -v officecli >\u002Fdev\u002Fnull 2>&1; then\n    curl -fsSL https:\u002F\u002Fd.officecli.ai\u002Finstall.sh | bash\nfi\n```\n\n`Windows (PowerShell)`\n\n```powershell\nif (-not (Get-Command officecli -ErrorAction SilentlyContinue)) {\n    irm https:\u002F\u002Fd.officecli.ai\u002Finstall.ps1 | iex\n}\n```\n\nVerify: `officecli --version`\n\nDo not run remote installer scripts without explicit user confirmation. If the\nuser does not approve a pipe-to-shell installer, use the official release page,\nchecksum\u002Fsignature instructions if provided, or a manual binary download from\nhttps:\u002F\u002Fgithub.com\u002FiOfficeAI\u002FOfficeCLI\u002Freleases.\n\nIf `officecli` is still not found after first install, open a new terminal and run the verify command again.\n\nIf the install command above fails (e.g. blocked by security policy, no network access, or insufficient permissions), install manually — download the binary for your platform from https:\u002F\u002Fgithub.com\u002FiOfficeAI\u002FOfficeCLI\u002Freleases — then re-run the verify command.\n\n## Help-First Rule\n\nThis skill teaches what a real form needs, not every CLI flag. When a prop \u002F alias \u002F enum is uncertain, consult help BEFORE guessing: `officecli help docx [element] [--json]` (e.g. `sdt`, `formfield`, `field`). Help is pinned to installed version — when this skill and help disagree, **help wins**. Every `--prop X=` below was verified against `officecli help docx \u003Celement>` on v1.0.63.\n\n## Mental Model & Inheritance\n\nA Word form is a `.docx` plus four OpenXML payload layers plain-docx skills do not touch: **`\u003Cw:sdt>`** content controls (5 types: text \u002F richtext \u002F dropdown \u002F combobox \u002F date), **`\u003Cw:ffData>`** legacy FormField (ONLY way to get a real checkbox on v1.0.63), **`\u003Cw:fldChar>`** complex fields (MERGEFIELD, REF, PAGEREF, SEQ, IF — template-time, not user-fill), and **`documentProtection`** (the lock that makes non-field text read-only in Word).\n\n**No inheritance from docx v2.** docx's Delivery Gate (cover-fill %, live-PAGE check) does NOT apply — form QA is `view forms` + `query sdt alias+tag` + `protectionEnforced`.\n\n**Reverse handoff to docx.** Route back to `officecli-docx` for reports \u002F letters \u002F memos \u002F thesis \u002F pitch decks \u002F any document with no editable fields. Use **this** skill when the document's purpose is data capture or template merge.\n\n## Shell & Execution Discipline\n\n**One command at a time. Read output before the next.** OfficeCLI is incremental — every `add` \u002F `set` \u002F `remove` immediately mutates the file. All recipes below use `FILE=form.docx` as a shell variable.\n\n**Three shell-escape layers:**\n\n1. **Quote every path with `[N]`** — zsh\u002Fbash glob-expand brackets. `officecli get \"$FILE\" \u002Fbody\u002Fsdt[1]` fails with `no matches found`. Correct: `officecli get \"$FILE\" '\u002Fbody\u002Fsdt[1]'`.\n2. **Single-quote any prop containing `$`** — `\"Total: $50,000\"` becomes `\"Total: ,000\"` after `$50` variable expansion. Correct: `'Total: $50,000'`.\n3. **`--after find:\u003Ctext>` uses outer single quotes, never inner double quotes** — `--after find:\"Client Signature:\"` makes the quotes part of the search string; match fails. Correct: `--after 'find:Client Signature:'`.\n\n**`WARNING: UNSUPPORTED` (exit 2) is a silently-wrong element.** The CLI created the element *without* the rejected prop — dropdown with no items, date with default format, SDT with no lock. Any UNSUPPORTED in your build log means your command was wrong: stop, rewrite to Path B (raw-set) or a separate `set`. Do not ship on top.\n\n**`protection=forms` is the LAST command.** Not CLI-enforced — `add` \u002F `set` \u002F `raw-set` still run under any protection mode — but finishing with protection gives Word users a consistent locked experience on first open.\n\n### `--after find:` micro-playbook\n\n`--after find:\u003Ctext>` matches the **first** occurrence. Bad anchor = wrong insertion location, expensive to debug. Three rules:\n\n1. **Anchor must be globally unique.** In bilingual contracts \"甲方签字\" matches both parties — use a unique phrase like \"甲方签字（Service Provider）\" or full English title.\n2. **After insert, `\u002Fbody\u002Fp[last()]` is unreliable** — the find insertion changes `\u003Cw:body>` child order. To continue operating on the new paragraph, read its real paraId: `officecli query \"$FILE\" paragraph --json | jq -r '.data.results[-1].format.paraId'`.\n3. **Chinese + full-width parens `（）`** match literally in `find`, but when unsure, `officecli view \"$FILE\" text | grep -n \"锚点\"` first to confirm the exact bytes in the file.\n\n```bash\n# Trap: first-match hits 甲方 only, 乙方 missed\nofficecli add \"$FILE\" \u002Fbody --type sdt --after 'find:签字'\n\n# Fix: two signatories, two unique anchors\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop alias=Party_A_Name --prop tag=party_a \\\n  --after 'find:甲方签字（Service Provider）'\nPID_A=$(officecli query \"$FILE\" paragraph --json | jq -r '.data.results[-1].format.paraId')\nofficecli add \"$FILE\" \"\u002Fbody\u002Fp[@paraId='$PID_A']\" --type sdt --prop alias=Party_A_Title --prop tag=party_a_title\n```\n\nInline SDT via `--after find:` is added as a child of the matched paragraph, not as a new paragraph — use this when label + SDT must share a line.\n\n## What makes a real form (identity)\n\nA real fillable form requires **structured fields** + **document protection**.\n\n| Approach | Word user sees | CLI-readable | Real form? |\n|---|---|---|---|\n| SDT controls + `protection=forms` | Gray-bordered fields; rest locked | `query sdt` \u002F `view forms` | **YES** |\n| FormField checkbox + `protection=forms` | Real clickable checkbox; rest locked | `query formfield` \u002F `view forms` | **YES** (checkbox only) |\n| MERGEFIELD placeholders | `«CustomerName»` merged by downstream engine | `query field` | **YES** (template-time) |\n| Underscores `___` \u002F blank lines | Visual-only; whole doc editable | No — no structured fields | **NO** |\n\n**Do not simulate fields with underscores.** `姓名：_______________` produces zero structured data and leaks past every verification. Always use `--type sdt` or `--type formfield`.\n\n**Checkbox is formfield, NOT SDT.** `--type sdt --prop type=checkbox` exits 1 (`SDT type 'checkbox' is not implemented`). Every checkbox in every recipe uses `--type formfield --prop type=checkbox`.\n\n**MERGEFIELD is a separate track.** `view forms` lists SDT + formfield only; `query field` lists complex fields only. Two disjoint inventories; both valid in one file.\n\n## Requirements for Outputs (hard floor)\n\nEvery form must satisfy these — Delivery Gate enforces each as an executable check.\n\n1. `protection=forms` enforced (`get $FILE \u002F` → `protectionEnforced=True`).\n2. Every SDT has both `alias` + `tag`.\n3. Every dropdown\u002Fcombobox has non-empty `items=...` in `view forms`.\n4. Every date SDT shows the intended `format=...`.\n5. Every locked SDT shows `lock=sdtLocked` \u002F `contentLocked` \u002F `sdtContentLocked` as intended.\n6. Zero `WARNING: UNSUPPORTED` in build log.\n7. Zero `type=checkbox` on any SDT.\n8. Every formfield `name` ≤ 20 characters.\n9. Zero underscore-line \u002F blank-line placeholders.\n10. Field types match user intent (short text \u002F paragraph \u002F fixed list \u002F list+custom \u002F date \u002F boolean).\n\n## Three Paths (core decision)\n\nCLI v1.0.63 exposes exactly **four canonical props** on SDT: `{type, tag, alias, text}`. Everything else — `items`, `format`, `lock`, `placeholder`, `name`, `maxlength` — is UNSUPPORTED at add-time and silently discarded. The skill therefore splits every SDT need into three paths. **Pick the path before writing a single command.**\n\n### Path A — Pure CLI (simple forms)\n\n**Use when**: the field only needs a label, an initial text, and a type. Acceptable if dropdown\u002Fcombobox items can be empty at first and dates can default to `yyyy-MM-dd`.\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type sdt \\\n  --prop type=text \\\n  --prop alias=\"Full Name\" --prop tag=full_name \\\n  --prop text=\"Enter full name\"\n# Canonical follow-ups (not on add):\n# officecli set \"$FILE\" '\u002Fbody\u002Fsdt[N]' --prop lock=sdtlocked\n# officecli set \"$FILE\" \u002F --prop protection=forms\n```\n\n### Path B — CLI + `raw-set` bridge (complex attrs)\n\n**Use when**: dropdown\u002Fcombobox needs options, or date needs a non-default format. `raw-set` is OfficeCLI's universal OpenXML fallback — `officecli --help` lists it as a top-level command.\n\n```bash\n# Step 1 — Path A skeleton (generates \u003Cw:dropDownList\u002F> automatically)\nofficecli add \"$FILE\" \u002Fbody --type sdt \\\n  --prop type=dropdown --prop alias=\"Department\" --prop tag=dept\n\n# Step 2 — raw-set injects \u003Cw:listItem>s\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='dept']\u002Fw:sdtPr\u002Fw:dropDownList\" \\\n  --action append \\\n  --xml '\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Engineering\" w:value=\"Engineering\"\u002F>\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Finance\" w:value=\"Finance\"\u002F>'\n```\n\n### Path C — Word template (beyond raw-set)\n\n**Use when**: `picture` SDT (signature image), real SDT checkbox (`type=checkbox` exits 1), `placeholderDocPart` prompt text, grouped SDTs wrapping multiple paragraphs, or custom richtext appearance. These involve cross-part relationships or nesting beyond `--prop` reach.\n\n```bash\n# One-time in Word: Developer tab → Insert Content Control → Save as template.docx\ncp templates\u002Fonboarding_with_signature.docx \"$FILE\"\nofficecli open \"$FILE\"\nofficecli view \"$FILE\" forms                 # inspect embedded controls + paths\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[@sdtId=3]' --prop text=\"Jane Smith\"\nofficecli set \"$FILE\" \u002F --prop protection=forms\n```\n\n### Decision table\n\n| Need | Path | Note |\n|---|---|---|\n| text \u002F richtext SDT with default string | **A** | four canonical props cover it |\n| text SDT that must be locked | **A + set lock** | `lock` only takes effect via `set`, not `add` |\n| dropdown \u002F combobox **with options** | **B** | raw-set append `\u003Cw:listItem>` |\n| date SDT with non-default format | **B** | raw-set setattr `w:dateFormat\u002F@w:val` |\n| real checkbox | **FormField** | `--type formfield --prop type=checkbox` (see §Legacy FormField) |\n| mail-merge placeholder | **MERGEFIELD** | `--type field --prop fieldType=mergefield` (see §MERGEFIELD) |\n| signature picture, grouped SDT, placeholder part | **C** | build skeleton in Word, fill via CLI |\n\n## Quick Start — Path A + FormField (minimal intake form)\n\nTwo SDT text fields, one checkbox, protection. Paste and adapt; this is the smallest form worth shipping.\n\n```bash\nFILE=intake.docx\nofficecli close \"$FILE\" 2>\u002Fdev\u002Fnull; rm -f \"$FILE\"   # preflight: clear stale resident \u002F prior file (cold-start after CLI upgrade commonly leaks a resident)\nofficecli create \"$FILE\"\nofficecli open \"$FILE\"\n\nofficecli set \"$FILE\" \u002F --prop title=\"Employee Onboarding Intake\" \\\n  --prop docDefaults.font=\"Calibri\" --prop docDefaults.fontSize=\"12pt\"\n\nofficecli add \"$FILE\" \u002Fbody --type paragraph \\\n  --prop text=\"Employee Onboarding Intake\" --prop style=Heading1 \\\n  --prop size=20 --prop bold=true --prop spaceAfter=18pt\n\nofficecli add \"$FILE\" \u002Fbody --type paragraph \\\n  --prop text=\"Full Name:\" --prop size=11 --prop bold=true --prop spaceAfter=4pt\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=text \\\n  --prop alias=\"Full Name\" --prop tag=full_name --prop text=\"Enter full name\"\n\nofficecli add \"$FILE\" \u002Fbody --type paragraph \\\n  --prop text=\"Start Date:\" --prop size=11 --prop bold=true --prop spaceAfter=4pt\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=date \\\n  --prop alias=\"Start Date\" --prop tag=start_date\n\nofficecli add \"$FILE\" \u002Fbody --type paragraph \\\n  --prop text=\"Read and agree to employee handbook\" --prop size=11 --prop spaceAfter=4pt\nofficecli add \"$FILE\" \u002Fbody --type formfield \\\n  --prop type=checkbox --prop name=agree_handbook --prop checked=false\n\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[1]' --prop lock=sdtlocked\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[2]' --prop lock=sdtlocked\nofficecli set \"$FILE\" \u002F --prop protection=forms\nofficecli close \"$FILE\"\nofficecli view \"$FILE\" forms\n```\n\n## Path B — raw-set recipes\n\nThree recipes cover almost every complex-attr need on SDT forms.\n\n### B1 — Dropdown items (append)\n\n```bash\n# Skeleton (Path A)\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=dropdown \\\n  --prop alias=\"Department\" --prop tag=dept\n\n# Inject items\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='dept']\u002Fw:sdtPr\u002Fw:dropDownList\" \\\n  --action append \\\n  --xml '\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Engineering\" w:value=\"Engineering\"\u002F>\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Finance\" w:value=\"Finance\"\u002F>\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"HR\" w:value=\"HR\"\u002F>'\n\n# Verify\nofficecli get \"$FILE\" '\u002Fbody\u002Fsdt[1]'   # expect: type=dropdown items=Engineering,Finance,HR\n```\n\n**Template.** Swap `\u003CTAG>` \u002F `\u003CLABEL>` \u002F `\u003CVALUE>` only. `xmlns:w=...` is required on every root `\u003Cw:listItem>` — raw-set does not inherit namespace prefixes. Chain multiple `\u003Cw:listItem>`s in one call; option order is preserved.\n\n### B2 — Combobox items (same as B1, different xpath tail)\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=combobox \\\n  --prop alias=\"Current Medication\" --prop tag=current_med\n\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='current_med']\u002Fw:sdtPr\u002Fw:comboBox\" \\\n  --action append \\\n  --xml '\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Antihypertensives\" w:value=\"Antihypertensives\"\u002F>\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Insulin\" w:value=\"Insulin\"\u002F>\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Other (specify)\" w:value=\"Other\"\u002F>'\n```\n\nOnly difference from B1: `w:comboBox` vs `w:dropDownList` in the xpath tail. Combobox lets the user type custom input; dropdown does not.\n\n### B3 — Date format (setattr)\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=date \\\n  --prop alias=\"Contract Start Date\" --prop tag=contract_start\n\n# Chinese: yyyy年MM月dd日\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='contract_start']\u002Fw:sdtPr\u002Fw:date\u002Fw:dateFormat\" \\\n  --action setattr \\\n  --xml \"w:val=yyyy年MM月dd日\"\n\n# US:    w:val=MM\u002Fdd\u002Fyyyy\n# ISO:   w:val=yyyy-MM-dd  (already the default)\n# Long:  w:val=\"MMMM d, yyyy\"\n\nofficecli get \"$FILE\" '\u002Fbody\u002Fsdt[N]'   # expect: type=date format=yyyy年MM月dd日\n```\n\n`setattr` replaces one attribute — do not quote the value inside `--xml`. Only `w:val` is touched; the `\u003Cw:dateFormat>` wrapper is preserved.\n\n### raw-set actions & errors\n\n| `--action` | Form use |\n|---|---|\n| `append` | Insert new child at end of target (B1, B2 — listItem) |\n| `setattr` | Change one attribute; `--xml \"key=value\"` (B3 — dateFormat\u002F@val) |\n| `replace` | Replace entire target (rare — reset a full `\u003Cw:date>` wrapper) |\n| `remove` | Delete the target (clear options before re-populate) |\n\n| Symptom | Fix |\n|---|---|\n| `raw-set: 0 element(s) affected` | XPath did not match. Check the `tag` value and whether the SDT is block or inline. Fall back to `officecli raw $FILE \u002Fdocument` to read the real XML. |\n| `Error: prefix 'w' is not defined` | Missing `xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\"` on the fragment — every root element in `--xml` needs it. |\n| Items readback empty after append | `\u003Cw:dropDownList\u002F>` must already exist (Path A `type=dropdown` ensures this). If absent, append has nowhere to insert. |\n| `VALIDATION: N new error(s) introduced` on same line as success | Your append introduced a schema-invalid child. Treat as stop-and-fix even though `raw-set` exits 0. |\n\n## Path C — Word template workflow\n\nFor fields CLI cannot express (signature `picture` SDT, real SDT checkbox, `placeholderDocPart` prompt text, grouped SDTs, custom richtext styling), build the skeleton once in Word, then fill via CLI.\n\n**One-time in Word:** File → Options → Customise Ribbon → Developer. Developer tab → Insert Picture \u002F Check Box \u002F Grouping Content Control → right-click → Properties → set Title (`alias`) + Tag. Save as `template.docx`.\n\n**Fill via CLI:**\n\n```bash\ncp templates\u002Fonboarding_with_signature.docx \"$FILE\"\nofficecli open \"$FILE\"\nofficecli view \"$FILE\" forms                                    # see \u002Fbody\u002F... paths + sdtId values\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[@sdtId=3]' --prop text=\"Jane Smith\"\nofficecli set \"$FILE\" \u002F --prop protection=forms\nofficecli close \"$FILE\"\n```\n\n## MERGEFIELD (data-driven track)\n\n`help docx field` on v1.0.63 declares a `fieldType` enum of ~30 values including `mergefield`, `ref`, `pageref`, `seq`, `if` — all CLI-expressible with their typed props. MERGEFIELD coexists with SDT in the same file but is reported by `query field` only; `view forms` does NOT list MERGEFIELDs (they are not user-fillable).\n\n**Canonical MERGEFIELD:**\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type paragraph --prop text=\"Dear \"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[1]' --type field --prop fieldType=mergefield --prop name=CustomerName\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[1]' --type run --prop text=\", \"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[1]' --type field --prop fieldType=mergefield --prop name=CompanyName\n# Readback: \"Dear «CustomerName», «CompanyName»\"\n```\n\n**Element-type shortcut** (equivalent): `officecli add \"$FILE\" '\u002Fbody\u002Fp[1]' --type mergefield --prop name=CustomerName`.\n\n### Common field patterns\n\n| Pattern | Call shape |\n|---|---|\n| Mail-merge placeholder | `--type field --prop fieldType=mergefield --prop name=\u003CFieldName>` |\n| Mail-merge with numeric picture (money, percent) | `--type field --prop fieldType=mergefield --prop name=Amount --prop instr='MERGEFIELD Amount \\# \"#,##0.00\"'`. On v1.0.63 the typed `format` prop is ignored for mergefield (prints a warning) — use `instr` (alias `instruction`) to embed the full field code. Verify: `query \"$FILE\" field --json \\| jq '.data.results[].format.instruction'` must contain `\\#` and the picture. |\n| Mail-merge with date picture | `--type field --prop fieldType=mergefield --prop name=StartDate --prop instr='MERGEFIELD StartDate \\@ \"yyyy-MM-dd\"'` |\n| Cross-reference to bookmark text | `--type field --prop fieldType=ref --prop name=\u003CBookmarkName>` |\n| Cross-reference to bookmark's page number | `--type field --prop fieldType=pageref --prop name=\u003CBookmarkName>` |\n| Auto-numbering (Figure 1 \u002F 2 \u002F 3) | `--type field --prop fieldType=seq --prop identifier=Figure` |\n| Page number in footer | `--type field --prop fieldType=page` |\n| \"Page X of Y\" | two fields: `fieldType=page` + `fieldType=numpages` |\n| Conditional text | `--type field --prop fieldType=if --prop expression='{ MERGEFIELD Gender } = \"Male\"' --prop trueText=\"Mr.\" --prop falseText=\"Ms.\"` |\n\n### IF conditional (CLI-expressible on v1.0.63)\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type paragraph --prop text=\"\"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[last()]' --type field --prop fieldType=if \\\n  --prop expression='{ MERGEFIELD Gender } = \"Male\"' \\\n  --prop trueText=\"Mr.\" --prop falseText=\"Ms.\"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[last()]' --type run --prop text=\" \"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[last()]' --type field --prop fieldType=mergefield --prop name=LastName\n# Merge-time result: \"Mr. «LastName»\" or \"Ms. «LastName»\"\n```\n\nNested wrappers like `{ IF { MERGEFIELD X } = \"Y\" { REF bm } \"fallback\" }` are not expressible via `--prop` chaining — drop to raw-set a hand-crafted `\u003Cw:fldChar>` \u002F `\u003Cw:instrText>` fragment, or build once in a Word template (Path C).\n\n**Readback.** `query $FILE field` lists `\u002Ffield[N]` + instruction + `fieldType`. `view $FILE forms` does NOT list MERGEFIELDs (only SDT + formfield) — they are template-time, not end-user fillable. `get $FILE '\u002Fbody\u002Fp[1]'` renders the guillemet-wrapped field name.\n\n## Legacy FormField\n\nUse FormField **when you need a real checkbox**. For text\u002Fdropdown, prefer SDT.\n\n`help docx formfield`: `type` (text\u002Fcheckbox\u002Fcheck\u002Fdropdown), `name` (required, **≤ 20 chars** — OpenXML schema MaxLength; add passes longer but `validate` rejects), `text` (text only, alias `value`), `checked` (checkbox only).\n\n```bash\n# CHECKBOX — the only real checkbox available in v1.0.63\nofficecli add \"$FILE\" \u002Fbody --type formfield --prop type=checkbox \\\n  --prop name=agree_terms --prop checked=false\n\n# TEXT formfield\nofficecli add \"$FILE\" \u002Fbody --type formfield --prop type=text \\\n  --prop name=emp_name --prop text=\"Enter name\"\n\n# DROPDOWN formfield — items NOT settable via CLI; use Word template or SDT Path B\nofficecli add \"$FILE\" \u002Fbody --type formfield --prop type=dropdown --prop name=dept_select\n\n# Read \u002F modify by name (stable) or 1-based index\nofficecli get \"$FILE\" '\u002Fformfield[agree_terms]'\nofficecli set \"$FILE\" '\u002Fformfield[agree_terms]' --prop checked=true\nofficecli set \"$FILE\" '\u002Fformfield[emp_name]' --prop text=\"Jane Smith\"\nofficecli set \"$FILE\" '\u002Fformfield[dept_select]' --prop text=\"Engineering\"\n```\n\nFormField paths (`\u002Fformfield[N]` or `\u002Fformfield[\u003Cname>]`) are separate from SDT paths (`\u002Fbody\u002Fsdt[N]`). Both coexist; `protection=forms` covers both.\n\n**Scale.** Tested with 50+ checkboxes in a single document — no practical cap on formfield count; build and `validate` remain clean. `name` ≤ 20 chars (K13) is the only hard constraint.\n\n**Renderer note — formfield checkbox `[RENDERER-BUG]`.** LibreOffice's PDF export occasionally renders the formfield checkbox as `☐☐` (doubled box). Word and WPS render a single clickable box (toggles ☑). This is a LibreOffice renderer quirk, **not a skill or document quality issue** — see K19. Do not attempt workarounds in the form; if an evaluator screenshots a LibreOffice-generated PDF and sees `☐☐`, attribute to `[RENDERER-BUG]`.\n\n## Document protection & lock\n\n### Enabling form protection\n\n```bash\nofficecli set \"$FILE\" \u002F --prop protection=forms\nofficecli get \"$FILE\" \u002F                                  # look for: protectionEnforced=True\n```\n\n### Protection modes\n\n| Mode | Word user can | CLI behavior |\n|---|---|---|\n| `forms` | Fill SDT + formfield only | All ops work; no `--force` needed |\n| `readOnly` | Read only | All ops work |\n| `comments` | Add comments only | All ops work |\n| `trackedChanges` | Edit with tracked changes only | All ops work |\n| `none` | Full editing | All ops work |\n\n**KEY:** Document protection restricts **Word users**, not the CLI. You can fill \u002F modify \u002F lock a protected form via CLI freely. The CLI does NOT require `--force` on v1.0.63.\n\n### Lock values (applied via `set`, never `add`)\n\n```bash\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[1]' --prop lock=sdtlocked           # content editable; control cannot be deleted\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[1]' --prop lock=contentlocked       # content read-only; control can be deleted\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[1]' --prop lock=sdtcontentlocked    # both locked\n# Omit lock entirely → unlocked (default)\n```\n\n`--prop lock=...` on `add` is UNSUPPORTED (silently discarded). Apply lock via a separate `set`. Readback normalises to camelCase (`sdtLocked`) regardless of input case — both accepted.\n\n### lock × `protection=forms` interaction\n\n| lock value | `protection=forms` active | Word user can edit? | Word user can delete control? |\n|---|---|---|---|\n| (none) | yes | **Yes** | **Yes** |\n| `sdtlocked` | yes | Yes | No |\n| `contentlocked` | yes | No | Yes |\n| `sdtcontentlocked` | yes | No | No |\n| block-level SDT wrap `contentlocked` | any | No (wrapped paragraph read-only regardless of protection) | No |\n| any | `readOnly` mode | No | No |\n\n### Block-level lock (paragraph-wrapping SDT)\n\n`protection=forms` is document-level — once an admin unprotects, every static paragraph (disclaimer, legal attestation, contract clause) becomes editable again. Master templates need defense-in-depth: wrap the critical paragraph in a block-level `\u003Cw:sdt>` with `lock=contentLocked`, so the content stays read-only even after protection is stripped.\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type paragraph \\\n  --prop text=\"I authorize the above and acknowledge all clauses.\" --prop size=11 --prop spaceAfter=12pt\nPID=$(officecli query \"$FILE\" paragraph --json | jq -r '.data.results[-1].format.paraId')\n\n# v1.0.63 raw-set actions: append | prepend | insertbefore | insertafter | replace | remove | setattr\n# No `wrap` action — two-step instead: (1) insertbefore an empty \u003Cw:sdt>\u003Cw:sdtContent\u002F>\u003C\u002Fw:sdt>,\n# (2) move the original \u003Cw:p> inside by `replace` on the sdtContent with a copy of the paragraph XML.\n# Simpler alternative: read the paragraph XML via `officecli raw`, then `replace` the whole \u003Cw:p> with \u003Cw:sdt>...\u003Cw:sdtContent>[original w:p]\u003C\u002Fw:sdtContent>\u003C\u002Fw:sdt>:\nPARA_XML=$(officecli raw \"$FILE\" \u002Fdocument | awk \"\u002Fw14:paraId=\\\"$PID\\\"\u002F,\u002F\u003C\\\\\u002Fw:p>\u002F\" | tr -d '\\n')\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:p[@w14:paraId='$PID']\" \\\n  --action replace \\\n  --xml \"\u003Cw:sdt xmlns:w=\\\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\\\" xmlns:w14=\\\"http:\u002F\u002Fschemas.microsoft.com\u002Foffice\u002Fword\u002F2010\u002Fwordml\\\">\u003Cw:sdtPr>\u003Cw:alias w:val=\\\"Authorization\\\"\u002F>\u003Cw:tag w:val=\\\"auth_para\\\"\u002F>\u003Cw:lock w:val=\\\"contentLocked\\\"\u002F>\u003C\u002Fw:sdtPr>\u003Cw:sdtContent>${PARA_XML}\u003C\u002Fw:sdtContent>\u003C\u002Fw:sdt>\"\n```\n\nVerify with `query sdt --json | jq '.data.results[] | select(.format.lock == \"contentLocked\" and .format.type == \"block\")'`. Use only for legal attestations, compliance disclaimers, confidentiality clauses — regular intake fields do not need this.\n\n### Role-gated fields (multi-role forms)\n\nWhen one form is filled by two roles (patient vs physician; Party A vs Party B), use `lock=contentLocked` on the fields the other role must not touch. Under `protection=forms`, `contentLocked` SDTs display as read-only in Word; the intended role unprotects (or the admin swaps role-specific copies) to fill the other half.\n\n```bash\n# Patient section — editable (no lock, or sdtlocked to prevent accidental deletion only)\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[1]' --prop lock=sdtlocked      # patient_name\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[2]' --prop lock=sdtlocked      # patient_dob\n\n# Physician section — locked against patient edits\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[14]' --prop lock=contentLocked # physician_diagnosis\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[15]' --prop lock=contentLocked # physician_signature\n```\n\nThis is the core pattern for medical intake, two-party contracts, sequential-approval forms.\n\n## Recipe — Contract \u002F SOW template with MERGEFIELD + signature\n\nRow-map across the three sub-recipes: SDT[1]=project_name, SDT[2]=contract_start, SDT[3]=payment_schedule, SDT[4]=signatory_name (inline). Run (sow-a) → (sow-b) → (sow-c) in order on the same `$FILE`; each sub-recipe stays under 20 lines so a shell-escape slip never cascades past one block.\n\n### Recipe (sow-a) Boilerplate + cover + parties\n\nCreates the file, sets docDefaults, writes the title \u002F intro, and drops the two MERGEFIELD placeholders (`CustomerName`, `ContractNo`) that downstream mail-merge will fill.\n\n```bash\nFILE=sow.docx\nofficecli create \"$FILE\"\nofficecli open \"$FILE\"\nofficecli set \"$FILE\" \u002F --prop title=\"Statement of Work\" \\\n  --prop docDefaults.font=\"Calibri\" --prop docDefaults.fontSize=\"12pt\"\n\nofficecli add \"$FILE\" \u002Fbody --type paragraph --prop text=\"Statement of Work\" \\\n  --prop style=Heading1 --prop size=20 --prop bold=true --prop spaceAfter=12pt\nofficecli add \"$FILE\" \u002Fbody --type paragraph \\\n  --prop text=\"This Statement of Work ('SOW') is entered into between the parties identified below and governs the delivery of professional services.\" \\\n  --prop size=11 --prop spaceAfter=12pt\n\nofficecli add \"$FILE\" \u002Fbody --type paragraph --prop text=\"Customer: \"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[last()]' --type field \\\n  --prop fieldType=mergefield --prop name=CustomerName\nofficecli add \"$FILE\" \u002Fbody --type paragraph --prop text=\"Contract #: \"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[last()]' --type field \\\n  --prop fieldType=mergefield --prop name=ContractNo\n```\n\n### Recipe (sow-b) SDT fields + Path B raw-set specials\n\nAdds the three block-level SDTs (project \u002F date \u002F dropdown), the inline signature SDT anchored via `--after 'find:Client Signature:'`, then Path B raw-set to inject the date format and dropdown items (both are UNSUPPORTED via `add --prop`).\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=text \\\n  --prop alias=\"Project Name\" --prop tag=project_name --prop text=\"Enter project name\"\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=date \\\n  --prop alias=\"Contract Start Date\" --prop tag=contract_start\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=dropdown \\\n  --prop alias=\"Payment Schedule\" --prop tag=payment_schedule\nofficecli add \"$FILE\" \u002Fbody --type paragraph --prop text=\"Client Signature:\" \\\n  --prop bold=true --prop spaceBefore=18pt --prop spaceAfter=4pt\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=text \\\n  --prop alias=\"Signatory Name\" --prop tag=signatory_name --prop text=\"Authorized Signatory\" \\\n  --after 'find:Client Signature:'\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='contract_start']\u002Fw:sdtPr\u002Fw:date\u002Fw:dateFormat\" \\\n  --action setattr --xml \"w:val=MM\u002Fdd\u002Fyyyy\"\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='payment_schedule']\u002Fw:sdtPr\u002Fw:dropDownList\" \\\n  --action append \\\n  --xml '\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Full Prepayment\" w:value=\"Full Prepayment\"\u002F>\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Net 30 Upon Delivery\" w:value=\"Net 30 Upon Delivery\"\u002F>'\n```\n\n### Recipe (sow-c) Watermark + locks + document protection\n\nDrops the CONFIDENTIAL watermark (parent is `\u002F`, never `\u002Fbody`), locks the three block-level SDTs, instructs how to lock the inline signatory_name SDT (path only known after `view forms`), then seals the document with `protection=forms` as the last command.\n\n```bash\nofficecli add \"$FILE\" \u002F --type watermark \\\n  --prop text=\"CONFIDENTIAL\" --prop color=FF0000 --prop rotation=315\n\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[1]' --prop lock=sdtlocked\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[2]' --prop lock=sdtlocked\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[3]' --prop lock=sdtlocked\nofficecli view \"$FILE\" forms   # copy signatory_name path, then: set '\u002Fbody\u002Fp[@paraId=...]\u002Fsdt[1]' --prop lock=sdtlocked\n\nofficecli set \"$FILE\" \u002F --prop protection=forms\nofficecli close \"$FILE\"\nofficecli query \"$FILE\" field     # expect 2 MERGEFIELDs: CustomerName, ContractNo\n```\n\n## Design principles (forms)\n\n**Control-type decision tree:**\n\n```\nDate → type=date | Fixed list → type=dropdown | List + custom → type=combobox\nShort text → type=text | Long text → type=richtext | Boolean → formfield checkbox\n```\n\n**Typography scale.** Spacing unit trap: `spaceBefore` \u002F `spaceAfter` \u002F `spaceLine` default to **twips** (1\u002F20 pt) — always write `spaceBefore=18pt`.\n\n| Element | Size | Style | Spacing |\n|---|---|---|---|\n| Form title (H1) | 20pt | Bold | `spaceBefore=0pt`, `spaceAfter=12pt` |\n| Section heading (H2) | 14pt | Bold | `spaceBefore=18pt`, `spaceAfter=8pt` |\n| Field label | 11pt | Bold | `spaceAfter=4pt` |\n| Instructions \u002F notes | 11pt | Italic `color=666666` | `spaceAfter=18pt` |\n\n**Accessibility bump.** For medical \u002F geriatric \u002F accessibility-focused forms, raise field label + instruction to **12pt** (11pt default is tight for older users); keep section headings at 14pt.\n\n**CJK forms:** set `docDefaults.font=\"Microsoft YaHei\"` — Calibri lacks Chinese glyphs.\n\n**Field ordering.** (1) Personal \u002F ID, (2) role \u002F classification, (3) dates, (4) supplemental free-text, (5) confirmation \u002F signature.\n\n**Yes\u002FNo + conditional follow-up** (common in compliance \u002F medical intake): formfield checkbox followed by a richtext SDT whose `alias` carries the cue — e.g. `--type formfield --prop type=checkbox --prop name=has_cond` then `--type sdt --prop type=richtext --prop alias=\"If yes, explain\" --prop tag=cond_detail --prop text=\"If yes, explain here\"`.\n\n**Signature block order.** Label on its own paragraph, SDT on the next paragraph (with `spaceBefore=18pt` on the label, `spaceAfter=4pt` on the SDT). Never `Label: SDT` inline — Word renders the runs as touching, visually stuck together.\n\n**Build order.** create+open → metadata → structure (headings, label paragraphs) → SDT\u002Fformfield skeletons (Path A 4 props) → Path B injections → per-field lock → `protection=forms` LAST → close.\n\n**Header \u002F footer note.** Headers\u002Ffooters are **predefined** when the section is created (default\u002Ffirst\u002Feven, 3 each). The first mutation must be `set` against the existing part, not `add` — `add $FILE \u002Fheader ...` returns `already exists` or silently no-ops. Inspect first with `officecli query \"$FILE\" header --json` to read the `type` values, then `officecli set \"$FILE\" '\u002Fheader[@type=default]' --prop text=...`. Only use `add` when creating an additional section with its own header\u002Ffooter.\n\n## Batch mode (brief)\n\nFor forms with many controls, batch reduces overhead. Path A + Path B coexist in one batch.\n\n```bash\ncat \u003C\u003C'EOF' | officecli batch \"$FILE\"\n[\n  {\"command\":\"add\",\"parent\":\"\u002Fbody\",\"type\":\"sdt\",\"props\":{\"type\":\"text\",\"alias\":\"Full Name\",\"tag\":\"full_name\",\"text\":\"Enter name\"}},\n  {\"command\":\"add\",\"parent\":\"\u002Fbody\",\"type\":\"sdt\",\"props\":{\"type\":\"dropdown\",\"alias\":\"Department\",\"tag\":\"dept\"}},\n  {\"command\":\"raw-set\",\"part\":\"\u002Fdocument\",\"xpath\":\"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='dept']\u002Fw:sdtPr\u002Fw:dropDownList\",\"action\":\"append\",\"xml\":\"\u003Cw:listItem xmlns:w=\\\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\\\" w:displayText=\\\"Engineering\\\" w:value=\\\"Engineering\\\"\u002F>\u003Cw:listItem xmlns:w=\\\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\\\" w:displayText=\\\"Finance\\\" w:value=\\\"Finance\\\"\u002F>\"},\n  {\"command\":\"set\",\"path\":\"\u002Fbody\u002Fsdt[1]\",\"props\":{\"lock\":\"sdtlocked\"}},\n  {\"command\":\"set\",\"path\":\"\u002Fbody\u002Fsdt[2]\",\"props\":{\"lock\":\"sdtlocked\"}}\n]\nEOF\nofficecli set \"$FILE\" \u002F --prop protection=forms\n```\n\n- Escape inner `\"` in `xml` with `\\\"`. Use single-quoted heredoc `\u003C\u003C'EOF'` so `$var` does not expand.\n- **P0 batch trap:** unsupported props in batch are silently dropped, **no WARNING** (interactive `add` would print WARNING: UNSUPPORTED, exit 2). Defence: send only `{type, tag, alias, text}` in SDT entries; put items\u002Fformat into `raw-set` entries in the same batch.\n- `batch` supports `add`, `set`, `get`, `query`, `remove`, `validate`, `raw-set` on v1.0.63.\n\n## Delivery Gate (executable)\n\nRun every gate below after every form. Each gate must print its `OK` line. Any `REJECT` = do not deliver.\n\n```bash\n# Assumes FILE=\u003Cyour-form.docx>, document has been closed with officecli close \"$FILE\"\n\n# Gate 1 — Validate (documentProtection waiver: K8 allows this ONE schema error under protection=forms)\nVAL_OUT=$(officecli validate \"$FILE\" 2>&1)\nVAL_ERRS=$(echo \"$VAL_OUT\" | grep -c '\\[Schema\\]')\nVAL_PROT=$(echo \"$VAL_OUT\" | grep -c 'documentProtection')\nif   [ \"$VAL_ERRS\" -eq 0 ]; then echo \"Gate 1 OK (validate clean)\"\nelif [ \"$VAL_ERRS\" -eq 1 ] && [ \"$VAL_PROT\" -eq 1 ]; then echo \"Gate 1 OK (1 documentProtection waiver — K8)\"\nelse echo \"REJECT Gate 1: $VAL_ERRS schema errors beyond the K8 waiver\"; echo \"$VAL_OUT\"; exit 1\nfi\n\n# Gate 2 — Token \u002F placeholder leak (labels used as visual underscore substitutes)\nLEAK=$(officecli view \"$FILE\" text | grep -niE '_{3,}|TBD|\\(fill in\\)|\\{\\{|xxxx|lorem|placeholder')\n[ -z \"$LEAK\" ] && echo \"Gate 2 OK (no underscore \u002F placeholder leak)\" || { echo \"REJECT Gate 2:\"; echo \"$LEAK\"; exit 1; }\n\n# Gate 3 — At least one structured field exists\nSDT_N=$(officecli query \"$FILE\" sdt --json | jq '.data.results | length')\nFF_N=$(officecli query \"$FILE\" formfield --json | jq '.data.results | length')\nFLD_N=$(officecli query \"$FILE\" field --json | jq '.data.results | length')\nTOTAL=$((SDT_N + FF_N + FLD_N))\n[ \"$TOTAL\" -gt 0 ] && echo \"Gate 3 OK ($SDT_N sdt + $FF_N formfield + $FLD_N field)\" || { echo \"REJECT Gate 3: 0 structured fields — this is not a form\"; exit 1; }\n\n# Gate 4 — Every SDT has alias + tag (skill-imposed H2)\n# NOTE: v1.0.63 `query --json` wraps prop fields under `.format.{prop}` — jq paths below use `.format.alias` \u002F `.format.tag` (not bare `.alias`).\nSDT_MISSING=$(officecli query \"$FILE\" sdt --json | jq '[.data.results[] | select(.format.alias == null or .format.alias == \"\" or .format.tag == null or .format.tag == \"\")] | length')\n[ \"$SDT_MISSING\" -eq 0 ] && echo \"Gate 4 OK (every SDT has alias+tag)\" || { echo \"REJECT Gate 4: $SDT_MISSING SDT(s) missing alias or tag\"; exit 1; }\n\n# Gate 5 — Protection enforced + per-field lock inventory\nPROT=$(officecli get \"$FILE\" \u002F --json | jq -r '.data.format.protection \u002F\u002F \"none\"')\n[ \"$PROT\" = \"forms\" ] && echo \"Gate 5 OK (protection=forms enforced)\" || { echo \"REJECT Gate 5: protection is '$PROT', expected 'forms'\"; exit 1; }\nofficecli view \"$FILE\" forms | head -40   # visual spot-check: every dropdown shows items=; every date shows format=; every locked SDT shows lock=\n\n# Gate 6 — No type=checkbox leaked onto any SDT\nBAD_CB=$(officecli query \"$FILE\" sdt --json | jq '[.data.results[] | select(.format.type == \"checkbox\")] | length')\n[ \"$BAD_CB\" -eq 0 ] && echo \"Gate 6 OK (no SDT checkbox — formfield only)\" || { echo \"REJECT Gate 6: $BAD_CB SDT with type=checkbox\"; exit 1; }\n```\n\n**Why `view issues` is not a gate.** It runs only prose-style checks (first-line-indent, heading size) and flags every form label as `Body paragraph missing first-line indent` — a false-positive avalanche on forms. Ignore for this skill. Use `validate` (schema integrity) and `view forms` (field inventory).\n\n## Known Issues\n\n| # | Issue | Behavior | Workaround |\n|---|---|---|---|\n| K1 | SDT `type=checkbox` not implemented on v1.0.63 | `add ... --type sdt --prop type=checkbox` → `Error: SDT type 'checkbox' is not implemented`, exit 1 | Use `--type formfield --prop type=checkbox`, or Path C template |\n| K2 | SDT `items` \u002F `format` \u002F `lock` UNSUPPORTED on `add` | `WARNING: UNSUPPORTED props`, exit 2; element created without them | Path B `raw-set` for items\u002Fformat; separate `set` for lock |\n| K3 | SDT `placeholder` \u002F `name` \u002F `maxlength` UNSUPPORTED | `WARNING: UNSUPPORTED`, exit 2; element still created | Use `text` for initial content; use `alias`+`tag` instead of `name`; prompt text requires Path C |\n| K4 | SDT `items` \u002F `format` \u002F `type` not settable after creation | `set --prop items=...` → `UNSUPPORTED props (use raw-set instead)` | Path B `raw-set`, or `remove` + re-add |\n| K5 | FormField `maxlength` UNSUPPORTED | `WARNING: UNSUPPORTED: maxlength`; formfield created | Enforce length in downstream validation |\n| K6 | FormField dropdown `items` UNSUPPORTED | Dropdown formfield is created with empty option list | Use SDT dropdown + Path B, or build in Word (Path C) |\n| K7 | Watermark `opacity` \u002F `width` \u002F `height` \u002F `size` UNSUPPORTED | Watermark created without them; `get \u002Fwatermark` still prints hardcoded `opacity=0.5` | Do not set them. For size, open Word + adjust shape (Phase 2) |\n| K8 | `validate` reports a `documentProtection` Schema error under `protection=forms` | Prints the error line, exits **0**. Gate 1 waives this one specific error | Confirm protection with `get $FILE \u002F` → `protectionEnforced=True`. Known validator bug, not a document bug |\n| K9 | Batch mode silently drops UNSUPPORTED props | No `WARNING` line; batch reports \"N succeeded\" even when props were dropped | Pass only `{type, tag, alias, text}` in batch SDT entries; put items\u002Fformat into `raw-set` entries in the same batch |\n| K13 | FormField `name` > 20 characters | `add` returns exit 0 with no warning; `validate` later reports `[Schema] ... MaxLength=20` on `\u002Fw:ffData\u002Fw:name` | Keep `name` ≤ 20 characters (OpenXML schema limit). SDT `alias` \u002F `tag` have no such limit |\n| K14 | `shd.fill` on a paragraph emits schema-invalid `\u003Cw:pPr>\u002F\u003Cw:shd>` | `validate` reports 2 schema errors per instance (`unexpected child element`, `required attribute 'val' missing`); Word renders it anyway | Apply highlight on the run instead (`shading=HEX`, flat canonical), or raw-set `\u003Cw:shd w:val=\"clear\" w:fill=\"HEX\"\u002F>` into the run's `\u003Cw:rPr>` |\n| K15 | `view forms` does NOT list MERGEFIELDs | Only SDT + formfield in output; MERGEFIELDs are template-time, not end-user fillable | Treat `query field` and `view forms` as two disjoint inventories. Every recipe verifies both |\n| K16 | Header \u002F footer are predefined at section creation (default\u002Ffirst\u002Feven, 3 each) | `add $FILE \u002Fheader ...` returns `already exists` or silently no-ops on the first call | First mutation uses `set` against the existing part: `officecli query $FILE header --json` to read `type`, then `set '\u002Fheader[@type=default]' --prop text=...`. Only use `add` for a brand-new section's header\u002Ffooter |\n| K17 | Watermark injected into header emits `\u003Cw:noProof>` child that is schema-invalid | `validate` adds an extra `[Schema]` error at `\u002Fheader[N]\u002Fw:sdt\u002F...\u002Fw:noProof` — NOT covered by K8's documentProtection waiver | After `add $FILE \u002F --type watermark`, run once per header part: `officecli raw-set $FILE \u002Fword\u002Fheader1.xml --xpath \"\u002F\u002Fw:noProof\" --action remove` (repeat for `header2.xml`, `header3.xml` if present) |\n| K18 | `query --json` wraps prop fields under `.format.{prop}` | Writing jq against bare `.alias` \u002F `.tag` \u002F `.protection` returns 0 matches, Gate 4\u002F5 falsely report \"missing=N\" | Always prefix jq with `.format.`: `.data.results[].format.alias`, `.data.results[].format.tag`, `.data.format.protection` (for `get \u002F`). Same for `.format.type` and `.format.paraId` |\n| K19 | LibreOffice renders formfield checkbox as `☐☐` (double box) in PDF export | Cosmetic only — Word \u002F WPS render a single box, clickable to toggle ☑. A LibreOffice renderer quirk, flagged as [RENDERER-BUG] | Do not try to \"fix\" in the skill. If an evaluator screenshots from LibreOffice-generated PDF and sees `☐☐`, attribute to [RENDERER-BUG], not a form-quality defect |\n\n## Phase 2 — enhance in Word\n\nSome polish is out of CLI scope. Hand the file to a human for these; none are required for a valid form.\n\n| Need | Why open Word |\n|---|---|\n| Signature image field (`picture` SDT) | Cross-part relationship + media file |\n| Real SDT checkbox with specific locking | `type=checkbox` exits 1; use Developer → Check Box Content Control |\n| Prompt text (\"Click here to enter a date\") | Needs `placeholderDocPart` in `\u002Fword\u002Fglossary\u002Fdocument.xml` |\n| Grouped SDT wrapping multiple paragraphs | Block-level `\u003Cw:sdt>` nesting beyond `add` |\n| Custom richtext default appearance | Adjust the referenced style in Word's style pane |\n| Watermark resize | `width` \u002F `height` not in schema; drag shape handles |\n\nFor the first four, build the skeleton once (Path C) and reuse.\n\n## Help pointer\n\nWhen in doubt: `officecli help docx`, `officecli help docx \u003Celement>`, `officecli help docx \u003Celement> --json`. Help is the authoritative schema; this skill is the decision guide for building real fillable Word forms on top of it.\n","# OfficeCLI Word-Form Skill\n\n**This skill is INDEPENDENT, not a scene layer on docx.** A form's payload — `\u003Cw:sdt>` controls, `\u003Cw:ffData>` legacy fields, `\u003Cw:fldChar>` mail-merge, `documentProtection` — is a distinct element class from docx's paragraph\u002Fheading\u002Fstyle primitives. Its QA is different too: docx's Delivery Gate cares about visual layout and live PAGE fields, this skill's cares about data plumbing (protection enforced \u002F alias+tag \u002F items injected \u002F name ≤ 20 \u002F no underscore anti-pattern). **Reverse handoff:** if the user's document has no fillable fields (report, letter, memo, thesis, proposal), route to `officecli-docx` or a docx scene skill — don't use this one.\n\n## BEFORE YOU START (CRITICAL)\n\n**If `officecli` is not installed:**\n\n`macOS \u002F Linux`\n\n```bash\nif ! command -v officecli >\u002Fdev\u002Fnull 2>&1; then\n    curl -fsSL https:\u002F\u002Fd.officecli.ai\u002Finstall.sh | bash\nfi\n```\n\n`Windows (PowerShell)`\n\n```powershell\nif (-not (Get-Command officecli -ErrorAction SilentlyContinue)) {\n    irm https:\u002F\u002Fd.officecli.ai\u002Finstall.ps1 | iex\n}\n```\n\nVerify: `officecli --version`\n\nDo not run remote installer scripts without explicit user confirmation. If the\nuser does not approve a pipe-to-shell installer, use the official release page,\nchecksum\u002Fsignature instructions if provided, or a manual binary download from\nhttps:\u002F\u002Fgithub.com\u002FiOfficeAI\u002FOfficeCLI\u002Freleases.\n\nIf `officecli` is still not found after first install, open a new terminal and run the verify command again.\n\nIf the install command above fails (e.g. blocked by security policy, no network access, or insufficient permissions), install manually — download the binary for your platform from https:\u002F\u002Fgithub.com\u002FiOfficeAI\u002FOfficeCLI\u002Freleases — then re-run the verify command.\n\n## Help-First Rule\n\nThis skill teaches what a real form needs, not every CLI flag. When a prop \u002F alias \u002F enum is uncertain, consult help BEFORE guessing: `officecli help docx [element] [--json]` (e.g. `sdt`, `formfield`, `field`). Help is pinned to installed version — when this skill and help disagree, **help wins**. Every `--prop X=` below was verified against `officecli help docx \u003Celement>` on v1.0.63.\n\n## Mental Model & Inheritance\n\nA Word form is a `.docx` plus four OpenXML payload layers plain-docx skills do not touch: **`\u003Cw:sdt>`** content controls (5 types: text \u002F richtext \u002F dropdown \u002F combobox \u002F date), **`\u003Cw:ffData>`** legacy FormField (ONLY way to get a real checkbox on v1.0.63), **`\u003Cw:fldChar>`** complex fields (MERGEFIELD, REF, PAGEREF, SEQ, IF — template-time, not user-fill), and **`documentProtection`** (the lock that makes non-field text read-only in Word).\n\n**No inheritance from docx v2.** docx's Delivery Gate (cover-fill %, live-PAGE check) does NOT apply — form QA is `view forms` + `query sdt alias+tag` + `protectionEnforced`.\n\n**Reverse handoff to docx.** Route back to `officecli-docx` for reports \u002F letters \u002F memos \u002F thesis \u002F pitch decks \u002F any document with no editable fields. Use **this** skill when the document's purpose is data capture or template merge.\n\n## Shell & Execution Discipline\n\n**One command at a time. Read output before the next.** OfficeCLI is incremental — every `add` \u002F `set` \u002F `remove` immediately mutates the file. All recipes below use `FILE=form.docx` as a shell variable.\n\n**Three shell-escape layers:**\n\n1. **Quote every path with `[N]`** — zsh\u002Fbash glob-expand brackets. `officecli get \"$FILE\" \u002Fbody\u002Fsdt[1]` fails with `no matches found`. Correct: `officecli get \"$FILE\" '\u002Fbody\u002Fsdt[1]'`.\n2. **Single-quote any prop containing `$`** — `\"Total: $50,000\"` becomes `\"Total: ,000\"` after `$50` variable expansion. Correct: `'Total: $50,000'`.\n3. **`--after find:\u003Ctext>` uses outer single quotes, never inner double quotes** — `--after find:\"Client Signature:\"` makes the quotes part of the search string; match fails. Correct: `--after 'find:Client Signature:'`.\n\n**`WARNING: UNSUPPORTED` (exit 2) is a silently-wrong element.** The CLI created the element *without* the rejected prop — dropdown with no items, date with default format, SDT with no lock. Any UNSUPPORTED in your build log means your command was wrong: stop, rewrite to Path B (raw-set) or a separate `set`. Do not ship on top.\n\n**`protection=forms` is the LAST command.** Not CLI-enforced — `add` \u002F `set` \u002F `raw-set` still run under any protection mode — but finishing with protection gives Word users a consistent locked experience on first open.\n\n### `--after find:` micro-playbook\n\n`--after find:\u003Ctext>` matches the **first** occurrence. Bad anchor = wrong insertion location, expensive to debug. Three rules:\n\n1. **Anchor must be globally unique.** In bilingual contracts \"甲方签字\" matches both parties — use a unique phrase like \"甲方签字（Service Provider）\" or full English title.\n2. **After insert, `\u002Fbody\u002Fp[last()]` is unreliable** — the find insertion changes `\u003Cw:body>` child order. To continue operating on the new paragraph, read its real paraId: `officecli query \"$FILE\" paragraph --json | jq -r '.data.results[-1].format.paraId'`.\n3. **Chinese + full-width parens `（）`** match literally in `find`, but when unsure, `officecli view \"$FILE\" text | grep -n \"锚点\"` first to confirm the exact bytes in the file.\n\n```bash\n# Trap: first-match hits 甲方 only, 乙方 missed\nofficecli add \"$FILE\" \u002Fbody --type sdt --after 'find:签字'\n\n# Fix: two signatories, two unique anchors\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop alias=Party_A_Name --prop tag=party_a \\\n  --after 'find:甲方签字（Service Provider）'\nPID_A=$(officecli query \"$FILE\" paragraph --json | jq -r '.data.results[-1].format.paraId')\nofficecli add \"$FILE\" \"\u002Fbody\u002Fp[@paraId='$PID_A']\" --type sdt --prop alias=Party_A_Title --prop tag=party_a_title\n```\n\nInline SDT via `--after find:` is added as a child of the matched paragraph, not as a new paragraph — use this when label + SDT must share a line.\n\n## What makes a real form (identity)\n\nA real fillable form requires **structured fields** + **document protection**.\n\n| Approach | Word user sees | CLI-readable | Real form? |\n|---|---|---|---|\n| SDT controls + `protection=forms` | Gray-bordered fields; rest locked | `query sdt` \u002F `view forms` | **YES** |\n| FormField checkbox + `protection=forms` | Real clickable checkbox; rest locked | `query formfield` \u002F `view forms` | **YES** (checkbox only) |\n| MERGEFIELD placeholders | `«CustomerName»` merged by downstream engine | `query field` | **YES** (template-time) |\n| Underscores `___` \u002F blank lines | Visual-only; whole doc editable | No — no structured fields | **NO** |\n\n**Do not simulate fields with underscores.** `姓名：_______________` produces zero structured data and leaks past every verification. Always use `--type sdt` or `--type formfield`.\n\n**Checkbox is formfield, NOT SDT.** `--type sdt --prop type=checkbox` exits 1 (`SDT type 'checkbox' is not implemented`). Every checkbox in every recipe uses `--type formfield --prop type=checkbox`.\n\n**MERGEFIELD is a separate track.** `view forms` lists SDT + formfield only; `query field` lists complex fields only. Two disjoint inventories; both valid in one file.\n\n## Requirements for Outputs (hard floor)\n\nEvery form must satisfy these — Delivery Gate enforces each as an executable check.\n\n1. `protection=forms` enforced (`get $FILE \u002F` → `protectionEnforced=True`).\n2. Every SDT has both `alias` + `tag`.\n3. Every dropdown\u002Fcombobox has non-empty `items=...` in `view forms`.\n4. Every date SDT shows the intended `format=...`.\n5. Every locked SDT shows `lock=sdtLocked` \u002F `contentLocked` \u002F `sdtContentLocked` as intended.\n6. Zero `WARNING: UNSUPPORTED` in build log.\n7. Zero `type=checkbox` on any SDT.\n8. Every formfield `name` ≤ 20 characters.\n9. Zero underscore-line \u002F blank-line placeholders.\n10. Field types match user intent (short text \u002F paragraph \u002F fixed list \u002F list+custom \u002F date \u002F boolean).\n\n## Three Paths (core decision)\n\nCLI v1.0.63 exposes exactly **four canonical props** on SDT: `{type, tag, alias, text}`. Everything else — `items`, `format`, `lock`, `placeholder`, `name`, `maxlength` — is UNSUPPORTED at add-time and silently discarded. The skill therefore splits every SDT need into three paths. **Pick the path before writing a single command.**\n\n### Path A — Pure CLI (simple forms)\n\n**Use when**: the field only needs a label, an initial text, and a type. Acceptable if dropdown\u002Fcombobox items can be empty at first and dates can default to `yyyy-MM-dd`.\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type sdt \\\n  --prop type=text \\\n  --prop alias=\"Full Name\" --prop tag=full_name \\\n  --prop text=\"Enter full name\"\n# Canonical follow-ups (not on add):\n# officecli set \"$FILE\" '\u002Fbody\u002Fsdt[N]' --prop lock=sdtlocked\n# officecli set \"$FILE\" \u002F --prop protection=forms\n```\n\n### Path B — CLI + `raw-set` bridge (complex attrs)\n\n**Use when**: dropdown\u002Fcombobox needs options, or date needs a non-default format. `raw-set` is OfficeCLI's universal OpenXML fallback — `officecli --help` lists it as a top-level command.\n\n```bash\n# Step 1 — Path A skeleton (generates \u003Cw:dropDownList\u002F> automatically)\nofficecli add \"$FILE\" \u002Fbody --type sdt \\\n  --prop type=dropdown --prop alias=\"Department\" --prop tag=dept\n\n# Step 2 — raw-set injects \u003Cw:listItem>s\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='dept']\u002Fw:sdtPr\u002Fw:dropDownList\" \\\n  --action append \\\n  --xml '\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Engineering\" w:value=\"Engineering\"\u002F>\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Finance\" w:value=\"Finance\"\u002F>'\n```\n\n### Path C — Word template (beyond raw-set)\n\n**Use when**: `picture` SDT (signature image), real SDT checkbox (`type=checkbox` exits 1), `placeholderDocPart` prompt text, grouped SDTs wrapping multiple paragraphs, or custom richtext appearance. These involve cross-part relationships or nesting beyond `--prop` reach.\n\n```bash\n# One-time in Word: Developer tab → Insert Content Control → Save as template.docx\ncp templates\u002Fonboarding_with_signature.docx \"$FILE\"\nofficecli open \"$FILE\"\nofficecli view \"$FILE\" forms                 # inspect embedded controls + paths\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[@sdtId=3]' --prop text=\"Jane Smith\"\nofficecli set \"$FILE\" \u002F --prop protection=forms\n```\n\n### Decision table\n\n| Need | Path | Note |\n|---|---|---|\n| text \u002F richtext SDT with default string | **A** | four canonical props cover it |\n| text SDT that must be locked | **A + set lock** | `lock` only takes effect via `set`, not `add` |\n| dropdown \u002F combobox **with options** | **B** | raw-set append `\u003Cw:listItem>` |\n| date SDT with non-default format | **B** | raw-set setattr `w:dateFormat\u002F@w:val` |\n| real checkbox | **FormField** | `--type formfield --prop type=checkbox` (see §Legacy FormField) |\n| mail-merge placeholder | **MERGEFIELD** | `--type field --prop fieldType=mergefield` (see §MERGEFIELD) |\n| signature picture, grouped SDT, placeholder part | **C** | build skeleton in Word, fill via CLI |\n\n## Quick Start — Path A + FormField (minimal intake form)\n\nTwo SDT text fields, one checkbox, protection. Paste and adapt; this is the smallest form worth shipping.\n\n```bash\nFILE=intake.docx\nofficecli close \"$FILE\" 2>\u002Fdev\u002Fnull; rm -f \"$FILE\"   # preflight: clear stale resident \u002F prior file (cold-start after CLI upgrade commonly leaks a resident)\nofficecli create \"$FILE\"\nofficecli open \"$FILE\"\n\nofficecli set \"$FILE\" \u002F --prop title=\"Employee Onboarding Intake\" \\\n  --prop docDefaults.font=\"Calibri\" --prop docDefaults.fontSize=\"12pt\"\n\nofficecli add \"$FILE\" \u002Fbody --type paragraph \\\n  --prop text=\"Employee Onboarding Intake\" --prop style=Heading1 \\\n  --prop size=20 --prop bold=true --prop spaceAfter=18pt\n\nofficecli add \"$FILE\" \u002Fbody --type paragraph \\\n  --prop text=\"Full Name:\" --prop size=11 --prop bold=true --prop spaceAfter=4pt\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=text \\\n  --prop alias=\"Full Name\" --prop tag=full_name --prop text=\"Enter full name\"\n\nofficecli add \"$FILE\" \u002Fbody --type paragraph \\\n  --prop text=\"Start Date:\" --prop size=11 --prop bold=true --prop spaceAfter=4pt\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=date \\\n  --prop alias=\"Start Date\" --prop tag=start_date\n\nofficecli add \"$FILE\" \u002Fbody --type paragraph \\\n  --prop text=\"Read and agree to employee handbook\" --prop size=11 --prop spaceAfter=4pt\nofficecli add \"$FILE\" \u002Fbody --type formfield \\\n  --prop type=checkbox --prop name=agree_handbook --prop checked=false\n\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[1]' --prop lock=sdtlocked\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[2]' --prop lock=sdtlocked\nofficecli set \"$FILE\" \u002F --prop protection=forms\nofficecli close \"$FILE\"\nofficecli view \"$FILE\" forms\n```\n\n## Path B — raw-set recipes\n\nThree recipes cover almost every complex-attr need on SDT forms.\n\n### B1 — Dropdown items (append)\n\n```bash\n# Skeleton (Path A)\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=dropdown \\\n  --prop alias=\"Department\" --prop tag=dept\n\n# Inject items\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='dept']\u002Fw:sdtPr\u002Fw:dropDownList\" \\\n  --action append \\\n  --xml '\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Engineering\" w:value=\"Engineering\"\u002F>\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Finance\" w:value=\"Finance\"\u002F>\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"HR\" w:value=\"HR\"\u002F>'\n\n# Verify\nofficecli get \"$FILE\" '\u002Fbody\u002Fsdt[1]'   # expect: type=dropdown items=Engineering,Finance,HR\n```\n\n**Template.** Swap `\u003CTAG>` \u002F `\u003CLABEL>` \u002F `\u003CVALUE>` only. `xmlns:w=...` is required on every root `\u003Cw:listItem>` — raw-set does not inherit namespace prefixes. Chain multiple `\u003Cw:listItem>`s in one call; option order is preserved.\n\n### B2 — Combobox items (same as B1, different xpath tail)\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=combobox \\\n  --prop alias=\"Current Medication\" --prop tag=current_med\n\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='current_med']\u002Fw:sdtPr\u002Fw:comboBox\" \\\n  --action append \\\n  --xml '\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Antihypertensives\" w:value=\"Antihypertensives\"\u002F>\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Insulin\" w:value=\"Insulin\"\u002F>\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Other (specify)\" w:value=\"Other\"\u002F>'\n```\n\nOnly difference from B1: `w:comboBox` vs `w:dropDownList` in the xpath tail. Combobox lets the user type custom input; dropdown does not.\n\n### B3 — Date format (setattr)\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=date \\\n  --prop alias=\"Contract Start Date\" --prop tag=contract_start\n\n# Chinese: yyyy年MM月dd日\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='contract_start']\u002Fw:sdtPr\u002Fw:date\u002Fw:dateFormat\" \\\n  --action setattr \\\n  --xml \"w:val=yyyy年MM月dd日\"\n\n# US:    w:val=MM\u002Fdd\u002Fyyyy\n# ISO:   w:val=yyyy-MM-dd  (already the default)\n# Long:  w:val=\"MMMM d, yyyy\"\n\nofficecli get \"$FILE\" '\u002Fbody\u002Fsdt[N]'   # expect: type=date format=yyyy年MM月dd日\n```\n\n`setattr` replaces one attribute — do not quote the value inside `--xml`. Only `w:val` is touched; the `\u003Cw:dateFormat>` wrapper is preserved.\n\n### raw-set actions & errors\n\n| `--action` | Form use |\n|---|---|\n| `append` | Insert new child at end of target (B1, B2 — listItem) |\n| `setattr` | Change one attribute; `--xml \"key=value\"` (B3 — dateFormat\u002F@val) |\n| `replace` | Replace entire target (rare — reset a full `\u003Cw:date>` wrapper) |\n| `remove` | Delete the target (clear options before re-populate) |\n\n| Symptom | Fix |\n|---|---|\n| `raw-set: 0 element(s) affected` | XPath did not match. Check the `tag` value and whether the SDT is block or inline. Fall back to `officecli raw $FILE \u002Fdocument` to read the real XML. |\n| `Error: prefix 'w' is not defined` | Missing `xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\"` on the fragment — every root element in `--xml` needs it. |\n| Items readback empty after append | `\u003Cw:dropDownList\u002F>` must already exist (Path A `type=dropdown` ensures this). If absent, append has nowhere to insert. |\n| `VALIDATION: N new error(s) introduced` on same line as success | Your append introduced a schema-invalid child. Treat as stop-and-fix even though `raw-set` exits 0. |\n\n## Path C — Word template workflow\n\nFor fields CLI cannot express (signature `picture` SDT, real SDT checkbox, `placeholderDocPart` prompt text, grouped SDTs, custom richtext styling), build the skeleton once in Word, then fill via CLI.\n\n**One-time in Word:** File → Options → Customise Ribbon → Developer. Developer tab → Insert Picture \u002F Check Box \u002F Grouping Content Control → right-click → Properties → set Title (`alias`) + Tag. Save as `template.docx`.\n\n**Fill via CLI:**\n\n```bash\ncp templates\u002Fonboarding_with_signature.docx \"$FILE\"\nofficecli open \"$FILE\"\nofficecli view \"$FILE\" forms                                    # see \u002Fbody\u002F... paths + sdtId values\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[@sdtId=3]' --prop text=\"Jane Smith\"\nofficecli set \"$FILE\" \u002F --prop protection=forms\nofficecli close \"$FILE\"\n```\n\n## MERGEFIELD (data-driven track)\n\n`help docx field` on v1.0.63 declares a `fieldType` enum of ~30 values including `mergefield`, `ref`, `pageref`, `seq`, `if` — all CLI-expressible with their typed props. MERGEFIELD coexists with SDT in the same file but is reported by `query field` only; `view forms` does NOT list MERGEFIELDs (they are not user-fillable).\n\n**Canonical MERGEFIELD:**\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type paragraph --prop text=\"Dear \"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[1]' --type field --prop fieldType=mergefield --prop name=CustomerName\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[1]' --type run --prop text=\", \"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[1]' --type field --prop fieldType=mergefield --prop name=CompanyName\n# Readback: \"Dear «CustomerName», «CompanyName»\"\n```\n\n**Element-type shortcut** (equivalent): `officecli add \"$FILE\" '\u002Fbody\u002Fp[1]' --type mergefield --prop name=CustomerName`.\n\n### Common field patterns\n\n| Pattern | Call shape |\n|---|---|\n| Mail-merge placeholder | `--type field --prop fieldType=mergefield --prop name=\u003CFieldName>` |\n| Mail-merge with numeric picture (money, percent) | `--type field --prop fieldType=mergefield --prop name=Amount --prop instr='MERGEFIELD Amount \\# \"#,##0.00\"'`. On v1.0.63 the typed `format` prop is ignored for mergefield (prints a warning) — use `instr` (alias `instruction`) to embed the full field code. Verify: `query \"$FILE\" field --json \\| jq '.data.results[].format.instruction'` must contain `\\#` and the picture. |\n| Mail-merge with date picture | `--type field --prop fieldType=mergefield --prop name=StartDate --prop instr='MERGEFIELD StartDate \\@ \"yyyy-MM-dd\"'` |\n| Cross-reference to bookmark text | `--type field --prop fieldType=ref --prop name=\u003CBookmarkName>` |\n| Cross-reference to bookmark's page number | `--type field --prop fieldType=pageref --prop name=\u003CBookmarkName>` |\n| Auto-numbering (Figure 1 \u002F 2 \u002F 3) | `--type field --prop fieldType=seq --prop identifier=Figure` |\n| Page number in footer | `--type field --prop fieldType=page` |\n| \"Page X of Y\" | two fields: `fieldType=page` + `fieldType=numpages` |\n| Conditional text | `--type field --prop fieldType=if --prop expression='{ MERGEFIELD Gender } = \"Male\"' --prop trueText=\"Mr.\" --prop falseText=\"Ms.\"` |\n\n### IF conditional (CLI-expressible on v1.0.63)\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type paragraph --prop text=\"\"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[last()]' --type field --prop fieldType=if \\\n  --prop expression='{ MERGEFIELD Gender } = \"Male\"' \\\n  --prop trueText=\"Mr.\" --prop falseText=\"Ms.\"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[last()]' --type run --prop text=\" \"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[last()]' --type field --prop fieldType=mergefield --prop name=LastName\n# Merge-time result: \"Mr. «LastName»\" or \"Ms. «LastName»\"\n```\n\nNested wrappers like `{ IF { MERGEFIELD X } = \"Y\" { REF bm } \"fallback\" }` are not expressible via `--prop` chaining — drop to raw-set a hand-crafted `\u003Cw:fldChar>` \u002F `\u003Cw:instrText>` fragment, or build once in a Word template (Path C).\n\n**Readback.** `query $FILE field` lists `\u002Ffield[N]` + instruction + `fieldType`. `view $FILE forms` does NOT list MERGEFIELDs (only SDT + formfield) — they are template-time, not end-user fillable. `get $FILE '\u002Fbody\u002Fp[1]'` renders the guillemet-wrapped field name.\n\n## Legacy FormField\n\nUse FormField **when you need a real checkbox**. For text\u002Fdropdown, prefer SDT.\n\n`help docx formfield`: `type` (text\u002Fcheckbox\u002Fcheck\u002Fdropdown), `name` (required, **≤ 20 chars** — OpenXML schema MaxLength; add passes longer but `validate` rejects), `text` (text only, alias `value`), `checked` (checkbox only).\n\n```bash\n# CHECKBOX — the only real checkbox available in v1.0.63\nofficecli add \"$FILE\" \u002Fbody --type formfield --prop type=checkbox \\\n  --prop name=agree_terms --prop checked=false\n\n# TEXT formfield\nofficecli add \"$FILE\" \u002Fbody --type formfield --prop type=text \\\n  --prop name=emp_name --prop text=\"Enter name\"\n\n# DROPDOWN formfield — items NOT settable via CLI; use Word template or SDT Path B\nofficecli add \"$FILE\" \u002Fbody --type formfield --prop type=dropdown --prop name=dept_select\n\n# Read \u002F modify by name (stable) or 1-based index\nofficecli get \"$FILE\" '\u002Fformfield[agree_terms]'\nofficecli set \"$FILE\" '\u002Fformfield[agree_terms]' --prop checked=true\nofficecli set \"$FILE\" '\u002Fformfield[emp_name]' --prop text=\"Jane Smith\"\nofficecli set \"$FILE\" '\u002Fformfield[dept_select]' --prop text=\"Engineering\"\n```\n\nFormField paths (`\u002Fformfield[N]` or `\u002Fformfield[\u003Cname>]`) are separate from SDT paths (`\u002Fbody\u002Fsdt[N]`). Both coexist; `protection=forms` covers both.\n\n**Scale.** Tested with 50+ checkboxes in a single document — no practical cap on formfield count; build and `validate` remain clean. `name` ≤ 20 chars (K13) is the only hard constraint.\n\n**Renderer note — formfield checkbox `[RENDERER-BUG]`.** LibreOffice's PDF export occasionally renders the formfield checkbox as `☐☐` (doubled box). Word and WPS render a single clickable box (toggles ☑). This is a LibreOffice renderer quirk, **not a skill or document quality issue** — see K19. Do not attempt workarounds in the form; if an evaluator screenshots a LibreOffice-generated PDF and sees `☐☐`, attribute to `[RENDERER-BUG]`.\n\n## Document protection & lock\n\n### Enabling form protection\n\n```bash\nofficecli set \"$FILE\" \u002F --prop protection=forms\nofficecli get \"$FILE\" \u002F                                  # look for: protectionEnforced=True\n```\n\n### Protection modes\n\n| Mode | Word user can | CLI behavior |\n|---|---|---|\n| `forms` | Fill SDT + formfield only | All ops work; no `--force` needed |\n| `readOnly` | Read only | All ops work |\n| `comments` | Add comments only | All ops work |\n| `trackedChanges` | Edit with tracked changes only | All ops work |\n| `none` | Full editing | All ops work |\n\n**KEY:** Document protection restricts **Word users**, not the CLI. You can fill \u002F modify \u002F lock a protected form via CLI freely. The CLI does NOT require `--force` on v1.0.63.\n\n### Lock values (applied via `set`, never `add`)\n\n```bash\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[1]' --prop lock=sdtlocked           # content editable; control cannot be deleted\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[1]' --prop lock=contentlocked       # content read-only; control can be deleted\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[1]' --prop lock=sdtcontentlocked    # both locked\n# Omit lock entirely → unlocked (default)\n```\n\n`--prop lock=...` on `add` is UNSUPPORTED (silently discarded). Apply lock via a separate `set`. Readback normalises to camelCase (`sdtLocked`) regardless of input case — both accepted.\n\n### lock × `protection=forms` interaction\n\n| lock value | `protection=forms` active | Word user can edit? | Word user can delete control? |\n|---|---|---|---|\n| (none) | yes | **Yes** | **Yes** |\n| `sdtlocked` | yes | Yes | No |\n| `contentlocked` | yes | No | Yes |\n| `sdtcontentlocked` | yes | No | No |\n| block-level SDT wrap `contentlocked` | any | No (wrapped paragraph read-only regardless of protection) | No |\n| any | `readOnly` mode | No | No |\n\n### Block-level lock (paragraph-wrapping SDT)\n\n`protection=forms` is document-level — once an admin unprotects, every static paragraph (disclaimer, legal attestation, contract clause) becomes editable again. Master templates need defense-in-depth: wrap the critical paragraph in a block-level `\u003Cw:sdt>` with `lock=contentLocked`, so the content stays read-only even after protection is stripped.\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type paragraph \\\n  --prop text=\"I authorize the above and acknowledge all clauses.\" --prop size=11 --prop spaceAfter=12pt\nPID=$(officecli query \"$FILE\" paragraph --json | jq -r '.data.results[-1].format.paraId')\n\n# v1.0.63 raw-set actions: append | prepend | insertbefore | insertafter | replace | remove | setattr\n# No `wrap` action — two-step instead: (1) insertbefore an empty \u003Cw:sdt>\u003Cw:sdtContent\u002F>\u003C\u002Fw:sdt>,\n# (2) move the original \u003Cw:p> inside by `replace` on the sdtContent with a copy of the paragraph XML.\n# Simpler alternative: read the paragraph XML via `officecli raw`, then `replace` the whole \u003Cw:p> with \u003Cw:sdt>...\u003Cw:sdtContent>[original w:p]\u003C\u002Fw:sdtContent>\u003C\u002Fw:sdt>:\nPARA_XML=$(officecli raw \"$FILE\" \u002Fdocument | awk \"\u002Fw14:paraId=\\\"$PID\\\"\u002F,\u002F\u003C\\\\\u002Fw:p>\u002F\" | tr -d '\\n')\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:p[@w14:paraId='$PID']\" \\\n  --action replace \\\n  --xml \"\u003Cw:sdt xmlns:w=\\\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\\\" xmlns:w14=\\\"http:\u002F\u002Fschemas.microsoft.com\u002Foffice\u002Fword\u002F2010\u002Fwordml\\\">\u003Cw:sdtPr>\u003Cw:alias w:val=\\\"Authorization\\\"\u002F>\u003Cw:tag w:val=\\\"auth_para\\\"\u002F>\u003Cw:lock w:val=\\\"contentLocked\\\"\u002F>\u003C\u002Fw:sdtPr>\u003Cw:sdtContent>${PARA_XML}\u003C\u002Fw:sdtContent>\u003C\u002Fw:sdt>\"\n```\n\nVerify with `query sdt --json | jq '.data.results[] | select(.format.lock == \"contentLocked\" and .format.type == \"block\")'`. Use only for legal attestations, compliance disclaimers, confidentiality clauses — regular intake fields do not need this.\n\n### Role-gated fields (multi-role forms)\n\nWhen one form is filled by two roles (patient vs physician; Party A vs Party B), use `lock=contentLocked` on the fields the other role must not touch. Under `protection=forms`, `contentLocked` SDTs display as read-only in Word; the intended role unprotects (or the admin swaps role-specific copies) to fill the other half.\n\n```bash\n# Patient section — editable (no lock, or sdtlocked to prevent accidental deletion only)\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[1]' --prop lock=sdtlocked      # patient_name\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[2]' --prop lock=sdtlocked      # patient_dob\n\n# Physician section — locked against patient edits\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[14]' --prop lock=contentLocked # physician_diagnosis\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[15]' --prop lock=contentLocked # physician_signature\n```\n\nThis is the core pattern for medical intake, two-party contracts, sequential-approval forms.\n\n## Recipe — Contract \u002F SOW template with MERGEFIELD + signature\n\nRow-map across the three sub-recipes: SDT[1]=project_name, SDT[2]=contract_start, SDT[3]=payment_schedule, SDT[4]=signatory_name (inline). Run (sow-a) → (sow-b) → (sow-c) in order on the same `$FILE`; each sub-recipe stays under 20 lines so a shell-escape slip never cascades past one block.\n\n### Recipe (sow-a) Boilerplate + cover + parties\n\nCreates the file, sets docDefaults, writes the title \u002F intro, and drops the two MERGEFIELD placeholders (`CustomerName`, `ContractNo`) that downstream mail-merge will fill.\n\n```bash\nFILE=sow.docx\nofficecli create \"$FILE\"\nofficecli open \"$FILE\"\nofficecli set \"$FILE\" \u002F --prop title=\"Statement of Work\" \\\n  --prop docDefaults.font=\"Calibri\" --prop docDefaults.fontSize=\"12pt\"\n\nofficecli add \"$FILE\" \u002Fbody --type paragraph --prop text=\"Statement of Work\" \\\n  --prop style=Heading1 --prop size=20 --prop bold=true --prop spaceAfter=12pt\nofficecli add \"$FILE\" \u002Fbody --type paragraph \\\n  --prop text=\"This Statement of Work ('SOW') is entered into between the parties identified below and governs the delivery of professional services.\" \\\n  --prop size=11 --prop spaceAfter=12pt\n\nofficecli add \"$FILE\" \u002Fbody --type paragraph --prop text=\"Customer: \"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[last()]' --type field \\\n  --prop fieldType=mergefield --prop name=CustomerName\nofficecli add \"$FILE\" \u002Fbody --type paragraph --prop text=\"Contract #: \"\nofficecli add \"$FILE\" '\u002Fbody\u002Fp[last()]' --type field \\\n  --prop fieldType=mergefield --prop name=ContractNo\n```\n\n### Recipe (sow-b) SDT fields + Path B raw-set specials\n\nAdds the three block-level SDTs (project \u002F date \u002F dropdown), the inline signature SDT anchored via `--after 'find:Client Signature:'`, then Path B raw-set to inject the date format and dropdown items (both are UNSUPPORTED via `add --prop`).\n\n```bash\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=text \\\n  --prop alias=\"Project Name\" --prop tag=project_name --prop text=\"Enter project name\"\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=date \\\n  --prop alias=\"Contract Start Date\" --prop tag=contract_start\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=dropdown \\\n  --prop alias=\"Payment Schedule\" --prop tag=payment_schedule\nofficecli add \"$FILE\" \u002Fbody --type paragraph --prop text=\"Client Signature:\" \\\n  --prop bold=true --prop spaceBefore=18pt --prop spaceAfter=4pt\nofficecli add \"$FILE\" \u002Fbody --type sdt --prop type=text \\\n  --prop alias=\"Signatory Name\" --prop tag=signatory_name --prop text=\"Authorized Signatory\" \\\n  --after 'find:Client Signature:'\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='contract_start']\u002Fw:sdtPr\u002Fw:date\u002Fw:dateFormat\" \\\n  --action setattr --xml \"w:val=MM\u002Fdd\u002Fyyyy\"\nofficecli raw-set \"$FILE\" \u002Fdocument \\\n  --xpath \"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='payment_schedule']\u002Fw:sdtPr\u002Fw:dropDownList\" \\\n  --action append \\\n  --xml '\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Full Prepayment\" w:value=\"Full Prepayment\"\u002F>\u003Cw:listItem xmlns:w=\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\" w:displayText=\"Net 30 Upon Delivery\" w:value=\"Net 30 Upon Delivery\"\u002F>'\n```\n\n### Recipe (sow-c) Watermark + locks + document protection\n\nDrops the CONFIDENTIAL watermark (parent is `\u002F`, never `\u002Fbody`), locks the three block-level SDTs, instructs how to lock the inline signatory_name SDT (path only known after `view forms`), then seals the document with `protection=forms` as the last command.\n\n```bash\nofficecli add \"$FILE\" \u002F --type watermark \\\n  --prop text=\"CONFIDENTIAL\" --prop color=FF0000 --prop rotation=315\n\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[1]' --prop lock=sdtlocked\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[2]' --prop lock=sdtlocked\nofficecli set \"$FILE\" '\u002Fbody\u002Fsdt[3]' --prop lock=sdtlocked\nofficecli view \"$FILE\" forms   # copy signatory_name path, then: set '\u002Fbody\u002Fp[@paraId=...]\u002Fsdt[1]' --prop lock=sdtlocked\n\nofficecli set \"$FILE\" \u002F --prop protection=forms\nofficecli close \"$FILE\"\nofficecli query \"$FILE\" field     # expect 2 MERGEFIELDs: CustomerName, ContractNo\n```\n\n## Design principles (forms)\n\n**Control-type decision tree:**\n\n```\nDate → type=date | Fixed list → type=dropdown | List + custom → type=combobox\nShort text → type=text | Long text → type=richtext | Boolean → formfield checkbox\n```\n\n**Typography scale.** Spacing unit trap: `spaceBefore` \u002F `spaceAfter` \u002F `spaceLine` default to **twips** (1\u002F20 pt) — always write `spaceBefore=18pt`.\n\n| Element | Size | Style | Spacing |\n|---|---|---|---|\n| Form title (H1) | 20pt | Bold | `spaceBefore=0pt`, `spaceAfter=12pt` |\n| Section heading (H2) | 14pt | Bold | `spaceBefore=18pt`, `spaceAfter=8pt` |\n| Field label | 11pt | Bold | `spaceAfter=4pt` |\n| Instructions \u002F notes | 11pt | Italic `color=666666` | `spaceAfter=18pt` |\n\n**Accessibility bump.** For medical \u002F geriatric \u002F accessibility-focused forms, raise field label + instruction to **12pt** (11pt default is tight for older users); keep section headings at 14pt.\n\n**CJK forms:** set `docDefaults.font=\"Microsoft YaHei\"` — Calibri lacks Chinese glyphs.\n\n**Field ordering.** (1) Personal \u002F ID, (2) role \u002F classification, (3) dates, (4) supplemental free-text, (5) confirmation \u002F signature.\n\n**Yes\u002FNo + conditional follow-up** (common in compliance \u002F medical intake): formfield checkbox followed by a richtext SDT whose `alias` carries the cue — e.g. `--type formfield --prop type=checkbox --prop name=has_cond` then `--type sdt --prop type=richtext --prop alias=\"If yes, explain\" --prop tag=cond_detail --prop text=\"If yes, explain here\"`.\n\n**Signature block order.** Label on its own paragraph, SDT on the next paragraph (with `spaceBefore=18pt` on the label, `spaceAfter=4pt` on the SDT). Never `Label: SDT` inline — Word renders the runs as touching, visually stuck together.\n\n**Build order.** create+open → metadata → structure (headings, label paragraphs) → SDT\u002Fformfield skeletons (Path A 4 props) → Path B injections → per-field lock → `protection=forms` LAST → close.\n\n**Header \u002F footer note.** Headers\u002Ffooters are **predefined** when the section is created (default\u002Ffirst\u002Feven, 3 each). The first mutation must be `set` against the existing part, not `add` — `add $FILE \u002Fheader ...` returns `already exists` or silently no-ops. Inspect first with `officecli query \"$FILE\" header --json` to read the `type` values, then `officecli set \"$FILE\" '\u002Fheader[@type=default]' --prop text=...`. Only use `add` when creating an additional section with its own header\u002Ffooter.\n\n## Batch mode (brief)\n\nFor forms with many controls, batch reduces overhead. Path A + Path B coexist in one batch.\n\n```bash\ncat \u003C\u003C'EOF' | officecli batch \"$FILE\"\n[\n  {\"command\":\"add\",\"parent\":\"\u002Fbody\",\"type\":\"sdt\",\"props\":{\"type\":\"text\",\"alias\":\"Full Name\",\"tag\":\"full_name\",\"text\":\"Enter name\"}},\n  {\"command\":\"add\",\"parent\":\"\u002Fbody\",\"type\":\"sdt\",\"props\":{\"type\":\"dropdown\",\"alias\":\"Department\",\"tag\":\"dept\"}},\n  {\"command\":\"raw-set\",\"part\":\"\u002Fdocument\",\"xpath\":\"\u002F\u002Fw:sdt[w:sdtPr\u002Fw:tag\u002F@w:val='dept']\u002Fw:sdtPr\u002Fw:dropDownList\",\"action\":\"append\",\"xml\":\"\u003Cw:listItem xmlns:w=\\\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\\\" w:displayText=\\\"Engineering\\\" w:value=\\\"Engineering\\\"\u002F>\u003Cw:listItem xmlns:w=\\\"http:\u002F\u002Fschemas.openxmlformats.org\u002Fwordprocessingml\u002F2006\u002Fmain\\\" w:displayText=\\\"Finance\\\" w:value=\\\"Finance\\\"\u002F>\"},\n  {\"command\":\"set\",\"path\":\"\u002Fbody\u002Fsdt[1]\",\"props\":{\"lock\":\"sdtlocked\"}},\n  {\"command\":\"set\",\"path\":\"\u002Fbody\u002Fsdt[2]\",\"props\":{\"lock\":\"sdtlocked\"}}\n]\nEOF\nofficecli set \"$FILE\" \u002F --prop protection=forms\n```\n\n- Escape inner `\"` in `xml` with `\\\"`. Use single-quoted heredoc `\u003C\u003C'EOF'` so `$var` does not expand.\n- **P0 batch trap:** unsupported props in batch are silently dropped, **no WARNING** (interactive `add` would print WARNING: UNSUPPORTED, exit 2). Defence: send only `{type, tag, alias, text}` in SDT entries; put items\u002Fformat into `raw-set` entries in the same batch.\n- `batch` supports `add`, `set`, `get`, `query`, `remove`, `validate`, `raw-set` on v1.0.63.\n\n## Delivery Gate (executable)\n\nRun every gate below after every form. Each gate must print its `OK` line. Any `REJECT` = do not deliver.\n\n```bash\n# Assumes FILE=\u003Cyour-form.docx>, document has been closed with officecli close \"$FILE\"\n\n# Gate 1 — Validate (documentProtection waiver: K8 allows this ONE schema error under protection=forms)\nVAL_OUT=$(officecli validate \"$FILE\" 2>&1)\nVAL_ERRS=$(echo \"$VAL_OUT\" | grep -c '\\[Schema\\]')\nVAL_PROT=$(echo \"$VAL_OUT\" | grep -c 'documentProtection')\nif   [ \"$VAL_ERRS\" -eq 0 ]; then echo \"Gate 1 OK (validate clean)\"\nelif [ \"$VAL_ERRS\" -eq 1 ] && [ \"$VAL_PROT\" -eq 1 ]; then echo \"Gate 1 OK (1 documentProtection waiver — K8)\"\nelse echo \"REJECT Gate 1: $VAL_ERRS schema errors beyond the K8 waiver\"; echo \"$VAL_OUT\"; exit 1\nfi\n\n# Gate 2 — Token \u002F placeholder leak (labels used as visual underscore substitutes)\nLEAK=$(officecli view \"$FILE\" text | grep -niE '_{3,}|TBD|\\(fill in\\)|\\{\\{|xxxx|lorem|placeholder')\n[ -z \"$LEAK\" ] && echo \"Gate 2 OK (no underscore \u002F placeholder leak)\" || { echo \"REJECT Gate 2:\"; echo \"$LEAK\"; exit 1; }\n\n# Gate 3 — At least one structured field exists\nSDT_N=$(officecli query \"$FILE\" sdt --json | jq '.data.results | length')\nFF_N=$(officecli query \"$FILE\" formfield --json | jq '.data.results | length')\nFLD_N=$(officecli query \"$FILE\" field --json | jq '.data.results | length')\nTOTAL=$((SDT_N + FF_N + FLD_N))\n[ \"$TOTAL\" -gt 0 ] && echo \"Gate 3 OK ($SDT_N sdt + $FF_N formfield + $FLD_N field)\" || { echo \"REJECT Gate 3: 0 structured fields — this is not a form\"; exit 1; }\n\n# Gate 4 — Every SDT has alias + tag (skill-imposed H2)\n# NOTE: v1.0.63 `query --json` wraps prop fields under `.format.{prop}` — jq paths below use `.format.alias` \u002F `.format.tag` (not bare `.alias`).\nSDT_MISSING=$(officecli query \"$FILE\" sdt --json | jq '[.data.results[] | select(.format.alias == null or .format.alias == \"\" or .format.tag == null or .format.tag == \"\")] | length')\n[ \"$SDT_MISSING\" -eq 0 ] && echo \"Gate 4 OK (every SDT has alias+tag)\" || { echo \"REJECT Gate 4: $SDT_MISSING SDT(s) missing alias or tag\"; exit 1; }\n\n# Gate 5 — Protection enforced + per-field lock inventory\nPROT=$(officecli get \"$FILE\" \u002F --json | jq -r '.data.format.protection \u002F\u002F \"none\"')\n[ \"$PROT\" = \"forms\" ] && echo \"Gate 5 OK (protection=forms enforced)\" || { echo \"REJECT Gate 5: protection is '$PROT', expected 'forms'\"; exit 1; }\nofficecli view \"$FILE\" forms | head -40   # visual spot-check: every dropdown shows items=; every date shows format=; every locked SDT shows lock=\n\n# Gate 6 — No type=checkbox leaked onto any SDT\nBAD_CB=$(officecli query \"$FILE\" sdt --json | jq '[.data.results[] | select(.format.type == \"checkbox\")] | length')\n[ \"$BAD_CB\" -eq 0 ] && echo \"Gate 6 OK (no SDT checkbox — formfield only)\" || { echo \"REJECT Gate 6: $BAD_CB SDT with type=checkbox\"; exit 1; }\n```\n\n**Why `view issues` is not a gate.** It runs only prose-style checks (first-line-indent, heading size) and flags every form label as `Body paragraph missing first-line indent` — a false-positive avalanche on forms. Ignore for this skill. Use `validate` (schema integrity) and `view forms` (field inventory).\n\n## Known Issues\n\n| # | Issue | Behavior | Workaround |\n|---|---|---|---|\n| K1 | SDT `type=checkbox` not implemented on v1.0.63 | `add ... --type sdt --prop type=checkbox` → `Error: SDT type 'checkbox' is not implemented`, exit 1 | Use `--type formfield --prop type=checkbox`, or Path C template |\n| K2 | SDT `items` \u002F `format` \u002F `lock` UNSUPPORTED on `add` | `WARNING: UNSUPPORTED props`, exit 2; element created without them | Path B `raw-set` for items\u002Fformat; separate `set` for lock |\n| K3 | SDT `placeholder` \u002F `name` \u002F `maxlength` UNSUPPORTED | `WARNING: UNSUPPORTED`, exit 2; element still created | Use `text` for initial content; use `alias`+`tag` instead of `name`; prompt text requires Path C |\n| K4 | SDT `items` \u002F `format` \u002F `type` not settable after creation | `set --prop items=...` → `UNSUPPORTED props (use raw-set instead)` | Path B `raw-set`, or `remove` + re-add |\n| K5 | FormField `maxlength` UNSUPPORTED | `WARNING: UNSUPPORTED: maxlength`; formfield created | Enforce length in downstream validation |\n| K6 | FormField dropdown `items` UNSUPPORTED | Dropdown formfield is created with empty option list | Use SDT dropdown + Path B, or build in Word (Path C) |\n| K7 | Watermark `opacity` \u002F `width` \u002F `height` \u002F `size` UNSUPPORTED | Watermark created without them; `get \u002Fwatermark` still prints hardcoded `opacity=0.5` | Do not set them. For size, open Word + adjust shape (Phase 2) |\n| K8 | `validate` reports a `documentProtection` Schema error under `protection=forms` | Prints the error line, exits **0**. Gate 1 waives this one specific error | Confirm protection with `get $FILE \u002F` → `protectionEnforced=True`. Known validator bug, not a document bug |\n| K9 | Batch mode silently drops UNSUPPORTED props | No `WARNING` line; batch reports \"N succeeded\" even when props were dropped | Pass only `{type, tag, alias, text}` in batch SDT entries; put items\u002Fformat into `raw-set` entries in the same batch |\n| K13 | FormField `name` > 20 characters | `add` returns exit 0 with no warning; `validate` later reports `[Schema] ... MaxLength=20` on `\u002Fw:ffData\u002Fw:name` | Keep `name` ≤ 20 characters (OpenXML schema limit). SDT `alias` \u002F `tag` have no such limit |\n| K14 | `shd.fill` on a paragraph emits schema-invalid `\u003Cw:pPr>\u002F\u003Cw:shd>` | `validate` reports 2 schema errors per instance (`unexpected child element`, `required attribute 'val' missing`); Word renders it anyway | Apply highlight on the run instead (`shading=HEX`, flat canonical), or raw-set `\u003Cw:shd w:val=\"clear\" w:fill=\"HEX\"\u002F>` into the run's `\u003Cw:rPr>` |\n| K15 | `view forms` does NOT list MERGEFIELDs | Only SDT + formfield in output; MERGEFIELDs are template-time, not end-user fillable | Treat `query field` and `view forms` as two disjoint inventories. Every recipe verifies both |\n| K16 | Header \u002F footer are predefined at section creation (default\u002Ffirst\u002Feven, 3 each) | `add $FILE \u002Fheader ...` returns `already exists` or silently no-ops on the first call | First mutation uses `set` against the existing part: `officecli query $FILE header --json` to read `type`, then `set '\u002Fheader[@type=default]' --prop text=...`. Only use `add` for a brand-new section's header\u002Ffooter |\n| K17 | Watermark injected into header emits `\u003Cw:noProof>` child that is schema-invalid | `validate` adds an extra `[Schema]` error at `\u002Fheader[N]\u002Fw:sdt\u002F...\u002Fw:noProof` — NOT covered by K8's documentProtection waiver | After `add $FILE \u002F --type watermark`, run once per header part: `officecli raw-set $FILE \u002Fword\u002Fheader1.xml --xpath \"\u002F\u002Fw:noProof\" --action remove` (repeat for `header2.xml`, `header3.xml` if present) |\n| K18 | `query --json` wraps prop fields under `.format.{prop}` | Writing jq against bare `.alias` \u002F `.tag` \u002F `.protection` returns 0 matches, Gate 4\u002F5 falsely report \"missing=N\" | Always prefix jq with `.format.`: `.data.results[].format.alias`, `.data.results[].format.tag`, `.data.format.protection` (for `get \u002F`). Same for `.format.type` and `.format.paraId` |\n| K19 | LibreOffice renders formfield checkbox as `☐☐` (double box) in PDF export | Cosmetic only — Word \u002F WPS render a single box, clickable to toggle ☑. A LibreOffice renderer quirk, flagged as [RENDERER-BUG] | Do not try to \"fix\" in the skill. If an evaluator screenshots from LibreOffice-generated PDF and sees `☐☐`, attribute to [RENDERER-BUG], not a form-quality defect |\n\n## Phase 2 — enhance in Word\n\nSome polish is out of CLI scope. Hand the file to a human for these; none are required for a valid form.\n\n| Need | Why open Word |\n|---|---|\n| Signature image field (`picture` SDT) | Cross-part relationship + media file |\n| Real SDT checkbox with specific locking | `type=checkbox` exits 1; use Developer → Check Box Content Control |\n| Prompt text (\"Click here to enter a date\") | Needs `placeholderDocPart` in `\u002Fword\u002Fglossary\u002Fdocument.xml` |\n| Grouped SDT wrapping multiple paragraphs | Block-level `\u003Cw:sdt>` nesting beyond `add` |\n| Custom richtext default appearance | Adjust the referenced style in Word's style pane |\n| Watermark resize | `width` \u002F `height` not in schema; drag shape handles |\n\nFor the first four, build the skeleton once (Path C) and reuse.\n\n## Help pointer\n\nWhen in doubt: `officecli help docx`, `officecli help docx \u003Celement>`, `officecli help docx \u003Celement> --json`. Help is the authoritative schema; this skill is the decision guide for building real fillable Word forms on top of it.\n",{"name":5,"description":7},"main","89ebf7196eee1581caf96eeee57dd956e5b7c6f2","crates\u002Fbackend\u002Fopenhub-app\u002Fassets\u002Fbuiltin-skills\u002Fofficecli-word-form","https:\u002F\u002Fgithub.com\u002FGrong\u002Fopenhub-client","https:\u002F\u002Fgithub.com\u002FGrong","2026-07-28 18:22:19",{"source":8,"skillName":5,"commands":30,"prompt":40,"zipAvailable":41},[31,34,37],{"tool":32,"command":33},"npx","npx skills add Grong\u002Fopenhub-client --skill officecli-word-form",{"tool":35,"command":36},"bunx","bunx skills add Grong\u002Fopenhub-client --skill officecli-word-form",{"tool":38,"command":39},"pnpm","pnpm dlx skills add Grong\u002Fopenhub-client --skill officecli-word-form","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FGrong\u002Fopenhub-client\u002Fblob\u002Fmain\u002Fcrates\u002Fbackend\u002Fopenhub-app\u002Fassets\u002Fbuiltin-skills\u002Fofficecli-word-form\u002FSKILL.md",true,[],[44,51,58,65,72,79],{"skillNo":45,"name":46,"nameZh":6,"description":47,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":48,"categories":49,"keywords":50,"publishedAt":18},"01M78DF6CKZ6Y19XWFFAFRMPVA","drive-openhub","Use to connect to and drive a OpenHub instance from an external agent (Claude Code \u002F Cursor \u002F any MCP client). Delegate goals to OpenHub's autonomous agent, drive its browser \u002F computer \u002F knowledge base \u002F files, and manage the platform — over MCP or REST, with a per-companion access token (you run AS the bound companion). Use this whenever the user asks to control, automate, or hand work off to \"OpenHub\", \"their OpenHub\", \"the desktop companion\", or a running OpenHub server.","https:\u002F\u002Fgithub.com\u002FGrong\u002Fopenhub-client\u002Fblob\u002Fmain\u002Fdocs\u002Fskills\u002Fdrive-nomifun\u002FSKILL.md",[],[],{"skillNo":52,"name":53,"nameZh":6,"description":54,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":55,"categories":56,"keywords":57,"publishedAt":18},"01M78DF51T081P4306NA1XYJPE","example-skill","Fixture skill used by startup_materialize integration tests.","https:\u002F\u002Fgithub.com\u002FGrong\u002Fopenhub-client\u002Fblob\u002Fmain\u002Fcrates\u002Fbackend\u002Fopenhub-extension\u002Ftests\u002Ffixtures\u002Fbuiltin-skills-fixture\u002Fexample-skill\u002FSKILL.md",[],[],{"skillNo":59,"name":60,"nameZh":6,"description":61,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":62,"categories":63,"keywords":64,"publishedAt":18},"01M78DF0Z8VS064W7300YJZ0KB","xiaohongshu-recruiter","用于准备在小红书上发布高质量的 AI 相关岗位招聘帖子。包含自动生成极客风格的招聘封面图和详情图，并提供确认门控的发布脚本。当用户需要发布招聘信息、寻找 Agent 设计师或其他 AI 领域人才时使用。","https:\u002F\u002Fgithub.com\u002FGrong\u002Fopenhub-client\u002Fblob\u002Fmain\u002Fcrates\u002Fbackend\u002Fopenhub-app\u002Fassets\u002Fbuiltin-skills\u002Fxiaohongshu-recruiter\u002FSKILL.md",[],[],{"skillNo":66,"name":67,"nameZh":6,"description":68,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":69,"categories":70,"keywords":71,"publishedAt":18},"01M78DEZB3EKW0JZNZ21B2E4ZM","x-recruiter","用于在 X (x.com) 发布招聘帖子。包含文案规范、图片生成提示和自动化发布脚本。发布 AI 相关岗位或设计类岗位时优先使用。","https:\u002F\u002Fgithub.com\u002FGrong\u002Fopenhub-client\u002Fblob\u002Fmain\u002Fcrates\u002Fbackend\u002Fopenhub-app\u002Fassets\u002Fbuiltin-skills\u002Fx-recruiter\u002FSKILL.md",[],[],{"skillNo":73,"name":74,"nameZh":6,"description":75,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":76,"categories":77,"keywords":78,"publishedAt":18},"01M78DEW7RT4CVH83HAKB5TQF5","weixin-file-send","Use when the user wants a local file or image sent back, such as \"send me the file\"\nor \"发给我\".","https:\u002F\u002Fgithub.com\u002FGrong\u002Fopenhub-client\u002Fblob\u002Fmain\u002Fcrates\u002Fbackend\u002Fopenhub-app\u002Fassets\u002Fbuiltin-skills\u002Fweixin-file-send\u002FSKILL.md",[],[],{"skillNo":80,"name":81,"nameZh":6,"description":82,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":83,"categories":84,"keywords":85,"publishedAt":18},"01M78DERHW8WYJRWH6QCZ9HMSQ","ui-ux-pro-max","Professional UI\u002FUX design research skill with a bundled searchable database for product patterns, visual styles, color palettes, typography, landing-page structure, dashboard charts, UX practices, and stack guidance. Use when designing, building, reviewing, or improving interfaces.","https:\u002F\u002Fgithub.com\u002FGrong\u002Fopenhub-client\u002Fblob\u002Fmain\u002Fcrates\u002Fbackend\u002Fopenhub-app\u002Fassets\u002Fbuiltin-skills\u002Fui-ux-pro-max\u002FSKILL.md",[],[]]