13 KiB
AGENTS.md
Purpose
lark-cli is the official Lark/Feishu CLI for humans and AI agents. Optimize
for predictable machine-readable behavior without making the human CLI worse.
Keep each PR focused on one goal: CLI UX, reliability, simpler explicit code, or a useful gate. Done means the correct implementation surface, preserved contracts unless a break was requested, and reported checks.
Public behavior, tests, lint, and CI define current contracts. Existing code is
evidence, but a legacy exception is not precedent; Proposal documents are
direction only. Do not mix unrelated cleanup, hand-edit generated files, weaken
gates, or broaden allowlists to make CI pass.
Implementation Discipline
Use this sequence for product behavior or enforcement changes:
- Establish the current contract and owner before choosing a solution. Trace the flow from the command or shortcut through runtime and internal owners to wire output; inspect affected callers and tests, then choose the surface below.
- Apply YAGNI to scope. Implement the behavior required now; do not add speculative modes, configuration, compatibility paths, extension points, or scaffolding.
- Reuse the project's existing machinery before creating a parallel path. Prefer the owning generic, runtime, or internal surface, then the Go standard library or an existing dependency, while preserving the contracts below.
- Fix the root cause at the narrowest cohesive boundary shared by affected callers. Keep command and domain policy at the caller unless there is a real cross-command invariant for an internal owner to enforce.
- Ship the smallest complete change: implementation, a revert-failing regression test for behavior or enforcement, and required contract or guidance updates.
Optimize for minimum owned complexity, not minimum diff or line count. A correct root-cause fix may touch the owner, callers, tests, and docs; a one-line workaround at the wrong layer is not simpler. Prefer deletion and boring explicit code, and avoid one-use abstractions or new dependencies for straightforward behavior.
YAGNI never overrides explicit requirements or output, error, path, security, compatibility, and data-safety contracts. If a deliberately limited solution has a real ceiling, document the ceiling and the concrete condition for expanding it at the decision point.
Build
Run make build for the canonical local build; it writes ./lark-cli with
version metadata and the embedded service catalog. It, make vet,
make unit-test, make live-skills-test, and dependent targets first run
python3 scripts/fetch_meta.py, so Python 3 is required. On a clean checkout,
the ignored internal/registry/meta_data.json is absent and the first run needs
access to open.feishu.cn; a valid existing file is reused.
LARKSUITE_CLI_REMOTE_META=off does not disable this build-time fetch.
make live-skills-test requires working npx and network access.
If the fetch fails before Go starts, report the missing Python or network
prerequisite instead of changing product code or generated metadata. go build .
may compile against the tracked empty fallback metadata, but that is only a
degraded compile check without the full service catalog.
Choose the Correct Surface
| Need | Implement in | Rule |
|---|---|---|
| Agent/human-friendly workflow, composition, or smart defaults | shortcuts/<domain>/ via common.Shortcut |
Must add UX or workflow value beyond exposing one endpoint. |
| One-to-one supported OpenAPI method | Upstream service metadata + generic cmd/service/ machinery |
Verify it with schema after the canonical metadata fetch. internal/registry/meta_data.json is generated and ignored; never hand-edit it or add a shortcut merely to expose a missing catalog method. |
| Arbitrary OpenAPI endpoint | Generic cmd/api/ machinery |
Keep it endpoint-agnostic. |
| Auth, config, profile, update, or CLI lifecycle | cmd/<area>/ plus the owning shared/internal package |
Keep new Cobra code as wiring when a lower owner exists. |
| EventKey, payload shape, or domain projection | events/<domain>/ |
Shared event mechanics stay in internal/event/; CLI assembly stays in cmd/event/. |
| Command-independent mechanism or cross-command invariant | Owning internal/<area>/ package |
Keep UX/domain policy at the caller; use a cohesive owner, not a generic utils package. Test the owner and affected caller contracts. |
| Public plugin or host integration | extension/ |
Exported symbols are compatibility commitments; orchestration stays internal. |
| Per-command decision guidance: when, avoid, prerequisites, tips, or examples | affordance/<domain>.md |
Enrich --help and schema without restating command descriptions, flags, or field schemas. |
| Domain routing, concepts, safety, or cross-command agent workflow | skills/<name>/SKILL.md and references/ |
Keep always-needed decisions in SKILL.md, conditional HOW in references, and link commands from affordance. |
Do not duplicate one command surface inside another. Register new shortcuts in
the domain's Shortcuts(); declare risk, identities/scopes, flags, and dry-run.
Hard Contracts
Read the JSON output contract before changing
wire output. Read the source guard guide and .golangci.yml
before changing or waiving enforcement.
- Command/flag semantics, help/schema metadata, output placement and shapes, errors, exit codes, risk/identity, and exported APIs are compatibility contracts. When forwarding or echoing accepted input, preserve it verbatim unless its contract defines normalization; never silently substitute another behavior.
- Success data goes to stdout; typed failure envelopes, progress, warnings, and hints go to stderr. Predicate/self-contained results and partial failures are documented exceptions whose complete result remains on stdout.
- Keep new or touched Cobra code as wiring. Lark/Feishu API calls in shortcuts go
through
*common.RuntimeContext; direct HTTP is only for non-gateway protocols such as presigned storage and requires a precise//nolint:forbidigoreason. - Keep user/workspace FileIO invocation-scoped: use
runtime.FileIO(),runtime.ValidatePath(), andruntime.ResolveSavePath()so portable commands do not assume a local host or process working directory. - Shortcuts do not use
internal/vfsfor user/workspace files. A narrow//nolint:depguardwaiver is allowed only for CLI-owned state or explicitly CLI-managed host configuration; explain that ownership boundary. Other internal filesystem code usesinternal/vfsand validates paths. - If FileIO lacks a host-local tree operation, prefer an owning
internal/package or optional capability. Directoscalls require a stated local-only boundary, validated and bounded paths, and a precise//nolint:forbidigoreason. - Do not hardcode resolver-owned hosts. At new API boundaries, or when changed behavior consumes fields from a loose map, project that shape into a typed struct before downstream use. Extend published interfaces through optional interfaces rather than breaking external implementations.
- Source guards enforce raw HTTP/os/vfs, resolver-host, and migrated-error constructs; other semantics rely on tests and review. Every exemption must be narrow, local, and explain why the safe path does not apply.
Structured Errors
Before changing a command failure, taxonomy, or error wire field, read the error contract; it owns constructor selection, wrapping, extension fields, stability, and CI guards.
- Command-facing failures use typed
errs.*unless the error contract defines an output-control exception. Never return a final plainfmt.Errorf/errors.Newor ad hoc envelope; pass typed errors through and preserve causes. - Lark API failures use a domain typed wrapper,
runtime.CallAPITyped, orruntime.DoAPIJSONTyped; raw callers useruntime.ClassifyAPIResponseorerrclass.BuildAPIError. paramnames only failing user input; recovery belongs inhint. Populatemissing_scopes,log_id, and similar fields only from known runtime evidence.- Error tests assert typed metadata and cause preservation, not message text alone.
Affordance and Skills
Before editing command guidance, read the affordance guide. For plugin distributions, read Ship skills and command guidance.
- Go metadata/schema owns WHAT; affordance owns command-level WHEN;
SKILL.mdowns domain routing, concepts, safety, and cross-command workflows;references/owns detailed or conditional HOW. - Do not duplicate canonical descriptions, schemas, or generic error taxonomy. Keep workflow-specific recovery in a skill when it changes agent behavior; affordance examples remain runnable, current, and safe.
- Skill frontmatter
descriptionis a concise WHAT/WHEN/NOT routing trigger. Keep always-needed decisions inSKILL.md; move conditional detail toreferences/. - Skill names and reference paths are public pointers. Every path reachable from
shipped docs must ship. Before referencing a new content directory, update
content_embed.goand add an embedded-FS reachability test;assets/andscripts/stay source-only unless the distribution contract changes.
Tests
- Every behavior change needs a nearby test that fails if the implementation is reverted; assert fields, requests, typed errors, or side effects directly.
- Command/shortcut tests needing a Factory use
cmdutil.TestFactory(t, config); isolate config witht.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()). - Tests must not depend on developer profiles, keychains, home directories, execution order, or real credentials unless explicitly live.
- Live E2E flows are self-contained: create, use, and clean up even after failure.
| Shortcut change | Dry-run E2E | Live E2E |
|---|---|---|
| New shortcut | Required | Required |
| Flags or request params | Required | Required if behavior changes |
| Bug fix | Required | Required when risk reaches the API boundary |
| Internal refactor, no behavior change | Not needed | Not needed |
Dry-run tests use placeholder credentials and assert method, URL, params, and
body without a real API call. Confirm contracts with --help and schema first.
If no deterministic, cleanable live flow exists, do not leak tenant state or add
a flaky test; document the blocker, fixture conditions, and substitute evidence.
Validation
Run the narrowest useful check while iterating, then broaden with risk:
| Change | Checks |
|---|---|
| Go package | go test ./path/to/package/..., then the pre-PR Go checks below |
| Broad/cross-cutting | make test |
| Committed command/help/schema surface | make quality-gate |
| Skills | node scripts/skill-format-check/index.js, then make quality-gate after committing the change |
| Affordance | Add/update internal/affordance/*_source_test.go; run go test ./internal/affordance ./cmd/service ./internal/schema |
| Make-covered scripts/workflows | make script-test |
| Public plugin SDK | make examples-build plus relevant tests/plugin_e2e |
| Auth sidecar | make sidecar-test |
| Skills sync behavior | make live-skills-test |
| Dependencies | go mod tidy plus the CI go-licenses check |
make quality-gate and diff-scoped linters compare the base with HEAD; they
ignore staged, unstaged, and untracked changes, so run them after committing.
Some skill-quality signals are warnings; frontmatter has the separate hard check
above. The gate does not validate affordance examples or references in this file:
run the focused source test and verify changed paths/symbols directly.
make test currently leaves root binaries audit-observer and readonly-policy; do not stage them.
Before a Go PR, run make unit-test, make vet, and make fmt-check;
go mod tidy must leave module files unchanged. Set
QUALITY_GATE_CHANGED_FROM to the PR base before diff-scoped checks. Pinned
go run module@version commands need module access unless cached.
go mod tidy
git diff --exit-code -- go.mod go.sum
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev="$QUALITY_GATE_CHANGED_FROM"
go run -C lint . --changed-from "$QUALITY_GATE_CHANGED_FROM" ..
go test -C lint ./... -count=1
go run github.com/google/go-licenses/v2@v2.0.1 check ./... --disallowed_types=forbidden,restricted,reciprocal,unknown
CI is authoritative; state exactly which relevant checks were not run.
Maintaining This File
Add a root rule only when it is repository-specific, non-obvious, actionable, and prevents a recurring failure or high-impact contract breach. Prefer code, tests, or CI for mechanizable constraints; keep only rationale and safe exceptions here. Update named references in the same PR, and delete or move stale, obvious, redundant, task-local, or fully enforced rules. Each edit should reduce ambiguity, not only add text.
Commit and PR
Use English Conventional Commits/PR titles, complete the PR template, and never commit secrets, tokens, internal endpoints, or sensitive test data.