* feat(apps): add +user-id-convert shortcut for Miaoda↔Feishu ID conversion
Wrap the platform id_convert OpenAPI as a read-only shortcut that maps
Miaoda user_id ↔ Feishu open platform IDs (open_id / union_id / Feishu
user_id). It does one thing — conversion — with no local mapping table,
caching, permission pre-check, or direction guessing.
- --convert-type enum → server id_convert_type (10/11/20/21/40)
- --ids: csv / @file / stdin, 1-100 per call, not de-duped, input order
- reconstructs data.missed by diffing input positions against returned
source_ids (server silently drops unresolved IDs), keyed by 0-based index
- meta counters (total/hit_count/missed_count) via pointer fields on
output.Meta so an explicit missed_count: 0 survives omitempty
* test(apps): address review feedback on +user-id-convert
- reject empty --ids CSV entries (e.g. "a,,b") with a typed validation
error instead of silently dropping them, since a dropped entry shifts
every later result's 0-based index and breaks the position-keyed
items/missed contract; add an interior-empty-element test
- reuse common.GetSlice / common.GetString for response projection
(house convention) instead of local asSlice/asString helpers
- requireConvertValidation now asserts CategoryValidation +
SubtypeInvalidArgument via errs.ProblemOf, keeping ValidationError.Param
- table-drive TestResolveConvertType over all five directions so every
--convert-type → id_convert_type mapping (10/11/20/21/40) is protected
* fix(apps): split newline-delimited --ids for +user-id-convert @file/stdin
@file and - (stdin) input arrives verbatim from the framework as
one-ID-per-line text, but parseConvertIDs only split on commas, so such a
block was sent as a single malformed request ID. Treat a newline as
equivalent to a comma, tolerating a file's trailing newline while still
rejecting interior empty entries so position-keyed result indices stay
aligned. Add @file and stdin tests asserting the request body's ids are
split into discrete IDs.
* fix(apps): stringify numeric JSON IDs in +user-id-convert results
Responses decode with json.Number (client.ParseJSONResponse uses
dec.UseNumber()), so a server that emits source_id/target_id as bare
numbers — plausible for the numeric Miaoda user_id form — was silently
coerced to "" by buildConvertResult's strict string assertion: the
source_id got dropped (false not_found) and the target_id blanked
(false success).
Add common.GetStringLoose, which stringifies string/json.Number/int64/
float64 via literal text (large integer IDs keep full precision, never
routed through a lossy float64), and use it for both id reads. Cover it
with a package-level table test plus an end-to-end regression asserting a
numeric-JSON response yields intact, non-blank ids and no false miss.
Also exercise resolveConvertType's non-empty "not a valid direction"
branch directly, since the runner's enum gate preempts it in normal flow.
* test(common): tighten GetStringLoose numeric coverage
Add an int-branch case (was only covering int64) and swap the float64
fixture from 42 — which no formatter would render in exponent form — to
1e-7, whose fixed-point rendering "0.0000001" fails under the 'g' verb.
This turns the "no scientific notation" case into a real guard for the
'f' verb choice, per CodeRabbit review on c64cca39.
* feat: add apps database sync shortcuts
Add Base-to-database sync shortcuts for preview, create, list, get, enable, disable, update, and delete flows.
Cover OpenAPI request contracts, typed sync error classification, dry-run E2E coverage, and lark-apps skill guidance.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(apps): send db-sync task_id and config in request body
The enable/disable/delete/update sync commands placed task_id (and
update's config) in query params, but the OpenAPI contract binds these
fields via api.json (request body). BOE testing returned
"field validation failed" (99992402) because the body was empty.
Move task_id to the request body for enable/disable/delete, and move
both task_id and config to the body for update. Dry-run previews now
render these under body, and unit tests pin the body binding so a
regression to query params fails.
* fix(apps): use POST for db-sync-delete action endpoint
The delete command issued an HTTP DELETE to db/sync_del, but the
action-style endpoint is registered as POST (like sync_create and
sync_disable). The method mismatch made the gateway return a plaintext
404, surfacing as "API returned a non-object JSON response".
Switch the request and dry-run preview to POST, and pin the method in
the delete unit tests so a regression to DELETE fails.
* test: pin db-sync update base_url as optional contract
* test: pin db-sync update omits base_url without silent default
* docs(skills): clarify db-sync source.base_url create-required update-optional contract
* fix(apps): send db-sync env in request body not query params
The +db-sync-create and +db-sync-update endpoints read env from the
request body (peer of config/preview/task_id), not the query string.
Placing env in query params left the body env empty, so the server
treated every request as online and rejected DDL operations
(code 500002776: forbid ddl/dcl operation in online env), making it
impossible to create/update sync tasks against a dev environment.
Move env into the request body via a new dbEnvBody helper that mirrors
dbEnvParams' omit-empty contract, so unset env still lets the server
auto-select the branch. Pin the contract in unit and e2e dry-run tests
by asserting body.env and that env is absent from query params.
* test: align db-sync operate/delete e2e with request-body contract
The enable/disable/delete dry-run e2e still asserted the pre-migration
wire shape: delete on DELETE and task_id in query params. The shortcuts
now POST these actions with task_id in the request body (commits moving
task_id and the delete verb), so the stale assertions failed against a
current binary.
Assert POST + body.task_id and that task_id is absent from query params,
pinning the same body-over-query contract the env fix established.
* fix(apps): improve db-sync create ergonomics and error guidance
Refine +db-sync-create/update validation, error hints, and docs so AI
agents recover from common Base-to-database sync failures without guessing:
- source.table.name: document that a user-named table must be set, name
takes precedence over the base_url ?table= token; fix test fixtures that
used a fictional source.table.url instead of source.base_url.
- Preflight source table locate: reject create locally when base_url has no
?table= and source.table.name is empty, pointing at base +table-list.
- Online DDL ban: attach a precise hint for code 500002776 + subcode
k_dl_4000001 telling multi-env apps to create tables on --environment dev.
- Missing record-id column: extend the 500002783 hint to add a unique text
column via +db-execute before retrying.
- Optional field_maps on create: allow omitted or empty field_maps so the
server auto-matches and creates the task; keep update requiring an enabled
mapping and still reject an all-disabled array.
- Environment default: db-sync commands use online when --environment is
omitted; align help text, comments, and skill docs.
* fix(apps): migrate db-sync error codes to the 4xx client-error range
The backend moved the seven db-sync error codes from the 5000027xx
server-error range to the 4000024xx client-input range to reflect that
they are client-input errors. Mirror the new codes in the CLI so error
classification and recovery hints keep matching:
- 500002783 -> 400002477 (mapping invalid)
- 500002784 -> 400002478 (target schema mismatch)
- 500002785 -> 400002479 (operation not allowed)
- 500002786 -> 400002480 (task not found)
- 500002787 -> 400002481 (invalid task id)
- 500002788 -> 400002482 (source table not found)
- 500002789 -> 400002483 (target table not found)
Category, subtype, hint text, and behavior are unchanged; 500002776
(online DDL ban) is untouched.
* fix(apps): tighten db-sync preview validation and pretty output
Address review follow-ups on the db-sync shortcuts:
- +db-sync-get pretty output no longer prints <nil> for a missing
schema_only nor Go map syntax for statistics; render a bare bool and
deterministic key=value pairs instead.
- Reject a non-array field_maps in +db-sync-create --preview as well as
commit, so the malformed shape is caught locally rather than forwarded
to the backend.
- Clarify in lark-apps-db.md that +db-sync-create --preview needs no
confirmation and only a real create requires --yes.
- Harden the db-sync dry-run validation tests to assert exit code 2 and
the structured stderr envelope (type/subtype/param), and add coverage
for the preview non-array field_maps rejection and batch pretty output.
* fix(apps): guard db-sync preview output and neutralize update hint
Address the next db-sync review round:
- +db-sync-create --preview --output no longer writes a "null" file and
exits success when the response omits data.config; project config into
a typed object and return internal/invalid_response without writing.
- Make the 400002482 code hint command-neutral so +db-sync-update is not
steered into a create-only recovery path that risks duplicate tasks.
- lark-apps-db.md: carry --environment on the update lifecycle examples
and split failure recovery by streaming (can update) vs batch (cannot
update; recreate instead), removing the batch/update contradiction.
---------
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* docs(slides): use built-in --jq in cli examples and fix workflow link labels
* docs(slides): use built-in --jq instead of external jq pipe for output filtering
Agents misread colloquial pronouns when creating events. Two fixes in
the calendar skill:
- SKILL.md: pronouns that are field values (attendees, meeting owner)
do not participate in `--as` identity selection, so "会议 owner 为我"
no longer flips bot-identity creation to user identity. "我" = the
logged-in user, "你" = the application (bot).
- create.md: the raw calendar events/attendees API does not auto-add the
calling identity as +create does; guide callers to add the caller's
open_id as a user attendee when using the full API flow.
* feat(base): require --fields on +table-create
A table created without --fields gets the platform default schema. Those
default fields then sit in the table alongside every field the caller adds
afterwards, and no field command removes them all, so the only clean recovery
is to drop the table and start over.
Make --fields required so the schema is declared up front, the way
+base-create already recommends via --table-name + --fields.
- Mark --fields Required on +table-create, and reject blank / non-array /
empty-array values in Validate: cobra's MarkFlagRequired only checks that the
flag was set, so --fields "" and --fields "[]" would still reach the API with
no fields body and fall back to the default schema.
- Validate runs ahead of the dry-run branch, so --dry-run can no longer preview
an invocation the real call would reject.
- Update the lark-base skill, e2e coverage notes and the live e2e helper.
BREAKING CHANGE: `lark-cli base +table-create --base-token <t> --name <n>`
without --fields now fails with a validation error instead of creating a
default-schema table. Callers that relied on create-empty-then-add-fields must
pass the schema to --fields.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(base): pin typed metadata on the invalid-schema rejection
Address review feedback on the +table-create schema validation.
- The invalid-fields-JSON rejection now asserts category, subtype and param
through assertInvalidArgumentValidation, plus the preserved *json.SyntaxError
cause, instead of only asserting that some error came back.
- Document why the missing-flag test asserts cobra's text rather than errs
metadata, and pin that layer boundary: cobra's ValidateRequiredFlags emits a
plain error and the dispatcher types it later (cmd/root_test.go). The test now
fails if that boundary moves, so the weaker assertion cannot silently outlive
its reason.
- Reword the --fields tip and the lark-base skill note: both described the
fieldless path as if it were still reachable through +table-create. They now
say the command rejects omitted / blank / empty schemas up front, while
keeping why the schema must be declared here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(base): enrich +table-create field example and drop redundant tips
The cobra Required declaration and flag Desc already advertise the
--fields requirement, so the tips paragraph restating it (and its
SKILL.md / coverage.md echoes) is dropped. The select-field example
now carries multiple/hue/lightness so agents copy a complete option
shape.
* chore: remove useless example
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
These two files are CLI command references, not XML schema references.
Move them from references/xml/ to references/cli/ and update all cross-file
links in SKILL.md, cli/*.md, workflow/*.md, and the compatibility stubs.
Add drive +member-remove for removing one collaborator permission from Drive documents, files, folders, wiki nodes, and Miaoda apps. The shortcut validates resource and member contracts before issuing the high-risk DELETE request, preserves structured CLI errors, and documents identity and wiki permission behavior.
Key features:
- Resolve resource type from supported URLs or require it for bare tokens
- Accept Miaoda apps via /page/ URLs or explicit --type=apps
- Support user and bot identities with member-type-specific validation
- Require explicit confirmation and return stable removal metadata
- Reject unsupported slash-containing tokens and member IDs before API calls
- Add unit, dry-run E2E, live workflow, and skill documentation coverage
Normalize deterministic +replace-slide part aliases (replace→block_replace,
target_id→block_id, and payload field folding), and reject semantically
different actions up front.
Point whole-page actions at +update-slide now that it is GA:
- page_replace / slide_replace recovery guidance and the reference error
table now direct callers to `slides +update-slide` (in-place whole-page
rewrite) instead of the deprecated +replace-pages.
- drop the +replace-pages reference doc, completing the #2227 deprecation
(the command stays; its deprecation signal is carried by --help and the
output JSON `deprecated` field).
Generalize whole-page validation gating to the behavior rather than a
command name (SKILL.md, workflow/validation-xml.md): "整页写回后" instead of
"每次通过 +update-slide 整页写回后". Folds in PR #2255.
Extend +download and +preview to accept --url and --wiki-token in
addition to --file-token (mutually exclusive). Wiki nodes are resolved
to their underlying object; only file-backed nodes are supported, and
non-file documents (docx/sheet/bitable/slides) return a typed
validation error hinting to use +export.
Check Drive export permission before file and document media downloads, while preserving whiteboard behavior and adding actionable preview and rate-limit recovery hints.
Spec source: active@ddbecbafcf0d68cc115faf3fcf0214fb6edc30b284147d33e5c9934e7bad82b4
Document the --raw, --slide-id, and --slide-number flags for slides +xml-get, including their mutual-exclusion rules and usage examples.
Clarify single-slide versus full-presentation output behavior, and remove the shallow documentation-only regression test.
Assembling the --slides JSON array by hand is what callers keep getting
wrong. A page of SML is multi-line and quote-heavy, and shell has no
built-in way to JSON-escape it, so callers reached for `jq -n --rawfile`
to build the array. In environments without jq the substitution silently
became an empty string and the command ran on to create an empty deck,
or the half-escaped XML reached the backend and came back as an opaque
3350001 after the presentation already existed.
Two input forms remove the escaping step:
--slides now declares Input{file, stdin}, so a finished array can be
read with `--slides @deck.json` or piped in with `--slides -`.
--slide is repeatable, takes one complete <slide> document (or @path),
and the CLI assembles the array. Repetition order is page order.
The forms are mutually exclusive: merging them would make page order
depend on flag-parsing rules nobody wants to reason about.
Notes on the repeatable flag: the framework only resolves Flag.Input for
single-valued string flags, so --slide resolves @path itself, through
the same cmdutil.ReadInputFile the framework uses, keeping the
"relative path under the current directory" rule identical. It rejects
"-" outright, because a process has one stdin and that cannot mean "this
occurrence" on a repeatable flag; the error names both forms that work.
Structural validation now runs on the assembled array, so both forms
fail the same way, and it runs before the create call so a malformed
page can no longer leave an orphaned empty presentation behind.
Three inputs the first round of review found still slipping through are
now rejected or normalized before the create call. `--slides null` is
valid JSON for a slice, so it parsed without error and left the array
nil, which read as "no pages given" and produced the blank deck
reported as success that the empty-value check exists to prevent. An
`<?xml ...?>` prolog is well-formed XML, so the parser accepted it and
only the backend rejected it, with 4001000 buildSnNode, after the
presentation already existed; it is now caught in the shared slide
validator, which covers +add-slide and +replace-pages too. And a
leading UTF-8 BOM made `--slide @page.xml` reject a file that
`--slides @deck.json` accepted, because the framework strips it for
Input flags and the repeatable flag resolved @path itself;
StripUTF8BOM is exported so both paths normalize the same way.
Also threads the source flag name through uploadSlidesPlaceholders,
which previously reported +add-slide upload failures as --slides.
Docs: the create/troubleshooting references now teach the file inputs
instead of the jq array-building template, and the follow-up snippet
uses the CLI's own --jq instead of piping to an external jq.
The lint gate in SKILL.md now names `slides +create` as a whole rather
than only its --slide form.
* fix(apps): detect the no-database failure by code or message
The recovery flow for "db command against an app that has no database"
keyed on one business code (500002759). The server has since renumbered
that case to 400002465, which silently disabled the flow: users now see
the raw internal message about workspace / app-id mapping and lose the
cloud-development recovery steps entirely.
Nothing catches the regression. There is no compile error, the unit tests
compare against the same constant they set, and the dry-run E2E does not
exercise a real response — the failure only shows up against a server that
has already renumbered.
Detect on code OR message instead. Both known codes are kept, plus narrow
lowercase markers of the server's internal wording. The two channels have
opposite failure modes: a code is precise but gets renumbered, a message
survives renumbering but breaks on rewording or localization. Requiring
either to match means one channel changing degrades nothing, and only a
simultaneous change of both regresses.
Markers stay deliberately narrow. "no db branch" in particular must not
also swallow env-pull's "invalid db branch" case, which needs its own
hint; a comment records that widening them requires a test proving the
neighbours still pass through.
Classification and the cause chain are untouched: the helper still mutates
the problem in place and returns the same error value.
* test(apps): assert the full typed-error contract in no-database cases
Review feedback: the new subtests checked only Message and Hint, so a
change that reclassified the failure — or replaced the error value and
dropped the cause chain — would still have passed.
Each case now asserts Category, Subtype and Code are untouched by the
rewrite, and that the helper returns the same error value. Inputs use a
concrete subtype rather than Unknown, so a clobbered classification is
actually observable. One new case wraps a cause and asserts errors.Is
still finds it through the rewrite.
Also covers the predicate's defensive nil guard, which withAppsHint cannot
reach on its own (ProblemOf returns ok=false for untyped errors), closing
the two uncovered lines the coverage report flagged. Both withAppsHint and
isAppNoDatabaseError are now at 100%.
* fix(errclass): classify the db-domain business codes
Three codes reaching the Apps db commands were absent from the Spark
table, so BuildAPIError fell through to the CategoryAPI + SubtypeUnknown
catch-all and the envelope carried no usable classification.
"App has no database yet" registers as Validation / FailedPrecondition:
the app resolves fine and the request is well-formed, but a prerequisite
the caller must create first is missing, so retrying unchanged can never
succeed. This moves its exit code from 1 to 2 — "fix the state" rather
than "the call failed" — and a test pins that so a future
reclassification has to be deliberate. Two codes cover it because the
server renumbered the case into the 4xx band; the legacy one stays for
older servers.
"Table does not exist" registers as API / NotFound, an ordinary
missing-resource lookup with no exit-code change. SubtypeNotFound has no
APIHint default, which matters here: the Apps layer fills its
command-scoped hint only when the classifier left Hint empty, so a
context-free default would displace the more actionable one. A test
guards that too.
* feat: accept frontend app-type in apps +create
* feat: accept frontend app-type filter in apps +list
* docs: clarify frontend app-type handling in apps +init
* docs: note FRONTEND in queryAppType comment
* docs: add frontend app-type guidance to lark-apps skill
* docs: add frontend app-type to lark-apps command references
SKILL.md's routing table already covered frontend, but the per-command
reference docs still enumerated only html/full_stack. Update create/list/get
enum values, add a frontend local-dev section, and note frontend in the
release-create entry so agents document the third app type consistently.
* docs: address CodeRabbit feedback on frontend app-type refs
- create.md: state the app-type enum is matched exactly (lowercase), drop
the incorrect claim that the CLI normalizes case
- local-dev.md: scope database debugging to full_stack (frontend/html have
no DB) and add the +release-get finished-status poll to the frontend flow
* docs: align app-type enum in apps E2E coverage and test comments
The E2E coverage table and two test comments still described the --app-type
enum as html/full_stack after frontend was added. Update them to
html/frontend/full_stack for consistency; assertions are unaffected (they
match on substrings, not the full enum set).
* docs(lark-apps): cloud-dev honors routed app_type instead of hardcoding full_stack
The main SKILL.md router declares app_type and dev-method orthogonal and
routes no-database interactive tools to frontend, but cloud-dev/create
references still hardcoded `+create --app-type full_stack` for cloud
generation. This forced a frontend-routed request into a full_stack app
(unrecoverable since apps have no +delete).
Align with the 2026-07-24 design decision: cloud generation also splits by
database need — full_stack when persistence is required, frontend by default
when unstated. Verified on BOE that a frontend app runs the full cloud
session+chat pipeline to completed.
Both are registered, reachable business domains — `lark-cli attendance
user_tasks query` and `lark-cli mindnotes nodes create` run, and both appear in
`auth login --domain` — but neither had an entry in
service_descriptions.json. GetServiceTitle/GetServiceDescription returned "",
so buildDomainMeta fell back to the typed service spec, which carries one
string for both languages. The result was visible in `--help`: mindnotes
rendered its Chinese description in the English domain list, and attendance
rendered "attendance record query", a lowercase fragment shown in both locales.
Both keep their own scope namespaces (attendance:task:readonly,
mindnote:node:create/read) and stay independent auth domains, so no
auth_domain is set. whiteboard remains the only domain folded into docs.
TestGetDomainMetadata_HasTitleAndDescription could not catch this: it asserts
on buildDomainMeta's output, which the fallback had already made non-empty.
The new reconciliation test walks EmbeddedServicesTyped plus AllShortcuts and
asserts both languages resolve from the config itself, before any fallback.
EmbeddedServicesTyped is the overlay-free parse, so the test does not depend on
what remote_meta.json happens to hold on the machine — a domain that only ever
arrives via remote overlay stays out of its reach.
Wrap the Drive file-copy endpoint as drive +copy. Accept a document URL
(recommended) or bare token + --type for the source; the target takes a
folder token, a folder URL, or the my_space constant, which resolves the
caller's My Space root folder via the root-folder-meta endpoint (absent
from platform metadata, works for both user and bot). Repeatable --extra
key=value pairs are forwarded verbatim for special copy semantics (e.g.
target_type=docx to convert a legacy doc during copy). Source and folder
tokens are validated with validate.ResourceName before path
interpolation. Reject wiki URLs/tokens with a typed validation error
whose hint carries a wiki +node-copy command template using a fixed
<node-token> placeholder, because a Drive copy of a wiki-backed document
would land in Drive space instead of the wiki tree. In bot mode the CLI
auto-grants the current CLI user full_access on the new copy (same
behavior as +upload/+import), reporting the outcome in the
permission_grant output field without failing the copy.
Declare docs:document:copy (the narrowest scope in the endpoint's any-of
set) plus a conditional drive:drive.metadata:readonly for my_space
resolution. Cover the shortcut with unit tests, dry-run e2e and a
self-contained live workflow (upload -> copy -> download-verify ->
my_space copy -> cleanup), and register it in
tests/cli_e2e/drive/coverage.md. Route copy intents in the lark-drive
skill to the shortcut instead of the raw files copy service command.
* feat(sheets): add --ai-only to +formula-verify for AI formula status polling
BE-2: +formula-verify gains --ai-only, mapping to verify_formula tool
input ai_only=true. AI formulas (AI_WRITE / AI_CLASSIFY / …) compute
asynchronously; --ai-only is a single-shot polling probe (no built-in
wait/timeout) that skips the ordinary 7-Excel-error scan and returns
current AI-formula compute status. Coexists with --sheet-id/--range and
honors --exit-on-error.
BE-4: mirror sheet-skill-spec SoT docs (AI formula list in
lark-sheets-formula-translation, --ai-only + async polling section in
lark-sheets-formula-verify) and regenerate flag_defs_gen.go.
Spec source: active@6fe4ea7389c6d0631dc8279ee7506c357d0600325e28ffabdc97d9a66339e762
* docs(sheets): mirror enriched AI formula params + --ai-only verify wording
Mirror from sheet-skill-spec SoT (ee/sheet-skill-spec MR!55):
- formula-translation: full AI function param details (range rules,
AI_TRANSLATE language keys, AI_EXTRACT type, AI_CLASSIFY modes,
AI_INFER/AI_IMPORTDATA array semantics)
- formula-verify: clarify --ai-only convergence expectation, drop
implementation-leaking phrasing
* docs(sheets): mirror unified AI() formula docs from spec
Sync the lark-sheets AI formula skill from sheet-skill-spec (SoT) after
the product change that merges all AI formulas into one AI() function:
- formula-translation reference: single =AI(prompt, [range]) function with
syntax (incl. variadic =AI(part1, part2, ...) concatenation), per-case
usage table (translate / sentiment / classify / extract / summarize /
rewrite / generate / cleanup / keywords / multi-cell prompt), and
prompt best practices
- formula-verify reference + --ai-only flag def: unified AI() wording
- flag_defs_gen.go regenerated from flag-defs.json
- lark_sheet_formula_verify.go: drop stale AI_WRITE / AI_CLASSIFY names
in the ai_only comment, reference the unified AI() function
* docs(sheets): sync lark-sheets skill from spec
Mirror the lark-sheets generated artifacts from sheet-skill-spec dwc/lark-sheet-ai:
- update AI formula verification delivery guidance
- add read-data inspection/profile helper scripts
- refresh generated flag definitions for +formula-verify --ai-only
* docs(sheets): sync updated AI formula guidance
Mirror the latest lark-sheets generated docs from sheet-skill-spec dwc/lark-sheet-ai after c292420:
- clarify that #ERROR/readback issues can be formula-write escaping failures rather than AI compute failures
- recommend +cells-set JSON formula writes for AI() formulas containing commas and quotes
- document <=1000-row serial batching for AI formula copy-to-range workflows
* docs(sheets): add license headers to helper scripts
Add the repository-standard copyright and SPDX headers to the generated lark-sheets helper scripts so the license-header check passes.
* docs(sheets): regenerate sheet flag defs
Regenerate shortcuts/sheets/flag_defs_gen.go after the latest sheet-skill-spec sync updated flag-defs.json.
* docs(sheets): revert lark-sheets skill sync
#2194 extended `im +messages-search` to `AuthTypes: {user, bot}` but left the
affordance example and the skill reference asserting user-only, so the
dual-identity guard added by #2199 fails on main.