Skip to content

feat(im): make IM operations agent-reliable - #2106

Open
luozhixiong01 wants to merge 41 commits into
mainfrom
feat/im-agent-reliability-closeout
Open

feat(im): make IM operations agent-reliable#2106
luozhixiong01 wants to merge 41 commits into
mainfrom
feat/im-agent-reliability-closeout

Conversation

@luozhixiong01

@luozhixiong01 luozhixiong01 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

IM commands could report success without enough evidence for an agent to know whether a write completed, a read was exhaustive, or a retry was safe. This standalone PR includes and supersedes #1880, makes completion contracts mandatory across IM commands, makes defaulted write identity observable, and keeps IM help guidance consistent without changing non-IM behavior.

Changes

  • Add typed IM write completion, partial-failure, recovery, and read-completeness contracts in internal/imcontract
  • Route shortcut and generated IM results through internal/output.Emitter, including structured error, hint, completeness metadata, and redacted presentation fallback
  • Add task-scoped IM Skill and affordance guidance for outbound confirmation, reply intent, ID sourcing, and copyable command Tips
  • Breaking change: require existing im +chat-create callers to provide a caller-owned --idempotency-key; add structured individual and all-member mentions to im +messages-send and im +messages-reply
  • Materialize IM search results, expose pagination stop reasons, and fail closed when exhaustive reads cannot be proven
  • Emit a structured identity_defaulted notice and stderr warning when a dual-identity contract-managed IM write omits --as
  • Normalize IM shortcut Risk and Tips sections at the end of help while retaining the explicit high-risk confirmation boundary
  • Enforce bidirectional contract coverage for all 60 runnable IM leaves and align leaf help, affordance/im.md, and skills/lark-im

Test Plan

  • Targeted unit and race tests passed for IM contracts, command utilities, shortcuts, and generated commands
  • Targeted go vet and git diff --check passed
  • Harness validate passed after rebasing onto the latest main
  • Local E2E passed (5/5 bot-capable groups; user-only workflows skipped because no user token was available)
  • Acceptance reviewer passed (5/5 no-side-effect scenarios)
  • Scoped completion-contract decision evaluation passed (6/6 for mention_result and retry_scope)
  • Broad Skill Eval was not fully green: 6 passed, 5 existing routing/tool-budget cases failed, and 1 case was ungraded after an external grader rate limit
  • Independent correctness and security reviews passed
  • Manual verification: built help for all 21 direct IM shortcuts and confirmed Risk/Tips follow Usage with no loss or duplication

Related PRs

Summary by CodeRabbit

  • New Features
    • Added comprehensive Lark IM operation guidance and examples.
    • Added structured mentions for sending and replying to messages.
    • Added required idempotency keys for chat creation.
    • Added consistent pagination controls, completeness metadata, and recovery hints.
    • Added clearer partial-result reporting for incomplete reads and writes.
  • Bug Fixes
    • Improved identity-defaulting notices and retry guidance.
    • Media upload failures now stop delivery instead of sending unapproved fallback links.
    • Improved message search reconciliation and mute-status handling.
  • Documentation
    • Clarified approval, bot identity, pagination, and recovery requirements across IM documentation.
    • Added guidance for risk confirmations and safe retries.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds contract-aware IM execution, completion and recovery metadata, shared pagination, structured mentions, idempotency handling, governed media behavior, contract-aware help, quality gates, and expanded IM documentation and coverage.

Changes

IM contract execution

Layer / File(s) Summary
Contract catalog and evidence processing
internal/imcontract/...
Adds registered IM contracts, evidence validation, completion ledgers, materialization tracking, mention reconciliation, retry classification, and safe fallback data.
Pagination and read completeness
internal/client/..., shortcuts/im/pagination.go, shortcuts/im/*list.go, shortcuts/im/*search.go
Adds shared page traversal, stop reasons, partial-result preservation, truncation detection, pagination metadata, and completeness handling.
Runtime and output integration
cmd/service/service.go, shortcuts/common/runner.go, internal/output/..., internal/cmdutil/...
Associates IM commands with contracts, finalizes requests and responses, emits identity notices, preserves typed errors, and supports partial-failure and presentation fallbacks.
Shortcut behavior
shortcuts/im/*
Updates message send/reply, chat creation, feed and flag operations, media handling, search materialization, mentions, and mute filtering.
Help and quality governance
cmd/root.go, cmd/service/affordance.go, internal/qualitygate/...
Centralizes risk and tips rendering, adds lazy contract help, and validates IM command coverage and strategy compatibility.
Documentation and validation
affordance/im.md, skill-template/domains/im.md, skills/lark-im/..., tests/cli_e2e/im/...
Documents raw IM operations, approval rules, identities, idempotency, pagination, completion, recovery, and validates documented examples.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: liangshuo-1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: making IM operations reliable for agents.
Description check ✅ Passed The description includes the required summary, change list, detailed test plan, and related PR context; the missing Related Issues section is non-critical.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/im-agent-reliability-closeout

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added domain/im PR touches the im domain size/XL Architecture-level or global-impact change labels Jul 30, 2026
@luozhixiong01
luozhixiong01 force-pushed the feat/im-agent-reliability-closeout branch from 5da4257 to 60dc511 Compare July 30, 2026 05:05
@luozhixiong01
luozhixiong01 force-pushed the feat/im-completion-contract branch from 09b52a9 to 20a5f50 Compare July 30, 2026 07:36
@luozhixiong01
luozhixiong01 force-pushed the feat/im-agent-reliability-closeout branch from 60dc511 to 24d926e Compare July 30, 2026 07:36
@luozhixiong01
luozhixiong01 force-pushed the feat/im-completion-contract branch from 20a5f50 to 89af6d1 Compare July 31, 2026 05:27
@luozhixiong01
luozhixiong01 force-pushed the feat/im-agent-reliability-closeout branch 2 times, most recently from a03b388 to 2e7007c Compare July 31, 2026 06:21
…pdates

Replay the seeded IM failure cases. Most already give an agent enough signal to recover; deterministic search coverage requires stable fixtures, while the --head/--tail conflict lacked a concrete next action. Add the missing hint, record the inventory, and fold the search fixture prerequisite and dry-run coverage into coverage.md.
@luozhixiong01
luozhixiong01 force-pushed the feat/im-agent-reliability-closeout branch from 2e7007c to c45fad0 Compare July 31, 2026 08:47
@luozhixiong01
luozhixiong01 changed the base branch from feat/im-completion-contract to main July 31, 2026 08:48
@github-actions github-actions Bot added domain/base PR touches the base domain and removed domain/base PR touches the base domain labels Jul 31, 2026
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.13745% with 595 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.75%. Comparing base (5cf09ec) to head (67d71b5).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
cmd/service/service.go 69.69% 81 Missing and 19 partials ⚠️
shortcuts/im/mentions.go 71.91% 54 Missing and 21 partials ⚠️
internal/imcontract/read.go 64.60% 51 Missing and 12 partials ⚠️
internal/client/pagination_status.go 78.08% 28 Missing and 11 partials ⚠️
internal/qualitygate/rules/imcontract.go 82.48% 23 Missing and 15 partials ⚠️
shortcuts/common/runner.go 83.06% 22 Missing and 10 partials ⚠️
internal/imcontract/message_mentions.go 76.15% 19 Missing and 12 partials ⚠️
internal/imcontract/catalog/types.go 0.00% 20 Missing ⚠️
internal/imcontract/ledger.go 87.26% 13 Missing and 7 partials ⚠️
shortcuts/im/pagination.go 84.67% 13 Missing and 6 partials ⚠️
... and 24 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2106      +/-   ##
==========================================
+ Coverage   75.46%   75.75%   +0.29%     
==========================================
  Files         928      946      +18     
  Lines       98479   100984    +2505     
==========================================
+ Hits        74314    76499    +2185     
- Misses      18511    18670     +159     
- Partials     5654     5815     +161     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@luozhixiong01
luozhixiong01 force-pushed the feat/im-agent-reliability-closeout branch from c45fad0 to ac8c577 Compare July 31, 2026 09:06
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@67d71b569751642103a40d4b69cda0d3f4b3caae

🧩 Skill update

npx skills add larksuite/cli#feat/im-agent-reliability-closeout -y -g

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
shortcuts/im/im_messages_search.go (1)

368-377: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the computed page limit in the pagination helper.

messagesSearchPaginationConfig() returns 40 when --page-all is set without --page-limit, but paginateIMWithMode() reads runtime.Int("page-limit") instead, which is the --page-limit flag default. The dry-run description and “stopped after fetching” warning can report 40 while pagination stops at the default page limit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/im/im_messages_search.go` around lines 368 - 377, The pagination
flow must use the pageLimit computed by messagesSearchPaginationConfig(),
including the messagesSearchMaxPageLimit value selected for page-all. Update
paginateIMWithMode() to consume and propagate that returned limit for pagination
and related dry-run or warning messages instead of rereading
runtime.Int("page-limit").
🧹 Nitpick comments (28)
internal/qualitygate/rules/run.go (1)

47-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two mechanisms protect the IM coverage diagnostics; one is unreachable.

Run appends imContractDiags at Line 115, after filterPRDiagnostics runs at Line 114. So IM coverage diagnostics never reach the filter from this path. The rule bypass added in filterPRDiagnostics (Lines 218-221) is therefore dead for production callers, and only TestIMContractCoverageDiagnosticIsNotChangedFileFiltered exercises it.

Keep one mechanism. Either append the IM diagnostics before filtering and rely on the rule bypass, or remove the bypass and keep the post-filter append.

♻️ Option A: rely on the rule bypass in the filter
-	diags = filterPRDiagnostics(opts.Repo, opts.ChangedFrom, scope, m, diags)
-	diags = append(diags, imContractDiags...)
+	diags = append(diags, imContractDiags...)
+	diags = filterPRDiagnostics(opts.Repo, opts.ChangedFrom, scope, m, diags)

Also applies to: 115-115

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/qualitygate/rules/run.go` at line 47, Consolidate IM coverage
diagnostic protection in Run and filterPRDiagnostics by choosing one mechanism:
either append imContractDiags before filtering so the existing bypass handles
them, or remove the bypass and retain the post-filter append. Eliminate the
redundant or unreachable path while preserving IM coverage diagnostics for
production callers and existing tests.
internal/affordance/affordance_im_test.go (1)

8-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use an internal/vfs-backed FS root for this internal test.

internal/affordance/affordance_im_test.go:40 uses os.DirFS under internal/, but the guideline for internal/ tests is to avoid standard-library filesystem APIs. Add or use an internal/vfs helper that builds an io/fs.FS for the repository tree, then pass that root to SetSource.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/affordance/affordance_im_test.go` at line 8, Replace the
os.DirFS-based repository root in the affordance test with an internal/vfs
helper that returns an io/fs.FS for the repository tree, then pass that
vfs-backed root to SetSource. Remove the direct os filesystem usage while
preserving the test’s existing source path and behavior.

Source: Learnings

internal/imcontract/materialization.go (1)

9-16: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding explicit tags to the remaining fields.

RequestedIDs, ResolvedIDs, and Cause carry json:"-". The other three fields carry no tag. Serialization happens through ledger(), so behavior is correct today. The mixed tagging suggests that direct marshaling is supported, which would emit Go field names instead of the snake_case keys used by ledger(). Add explicit tags or drop all tags and rely only on ledger().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/materialization.go` around lines 9 - 16, Add explicit
JSON tags to MissingMessageIDs, UnresolvedHitCount, and UnexpectedMessageCount
in MaterializationStatus, using the snake_case keys emitted by ledger(), while
preserving json:"-" for RequestedIDs, ResolvedIDs, and Cause.
internal/imcontract/http_status_test.go (1)

30-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for a 4xx status with an existing error.

The test covers 503, 429, 404 without an error, and 200. It does not cover the branch that returns the incoming error unchanged for a 4xx status, and it does not cover the generic 4xx mapping to errs.SubtypeUnknown. Add both cases so that a change in that branch fails the test.

💚 Proposed additional cases
 	notFound := NormalizeHTTPError(404, "", nil)
 	notFoundProblem, ok := errs.ProblemOf(notFound)
 	if !ok || notFoundProblem.Subtype != errs.SubtypeNotFound ||
 		notFoundProblem.Code != 404 || notFoundProblem.Retryable {
 		t.Fatalf("not-found problem = %#v, err=%T %v", notFoundProblem, notFound, notFound)
 	}
 
+	if kept := NormalizeHTTPError(400, "log-id", original); kept != original {
+		t.Fatalf("typed lower-layer error was replaced: %T %v", kept, kept)
+	}
+
+	generic := NormalizeHTTPError(400, "", nil)
+	genericProblem, ok := errs.ProblemOf(generic)
+	if !ok || genericProblem.Category != errs.CategoryAPI ||
+		genericProblem.Subtype != errs.SubtypeUnknown ||
+		genericProblem.Code != 400 || genericProblem.Retryable {
+		t.Fatalf("generic 4xx problem = %#v", genericProblem)
+	}
+
 	if unchanged := NormalizeHTTPError(200, "", original); unchanged != original {

As per coding guidelines: "Every behavior change must have an accompanying test, and contract tests must assert the changed field or behavior directly so reverting the implementation causes failure."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/http_status_test.go` around lines 30 - 39, Add test cases
for NormalizeHTTPError covering a generic 4xx status mapping to
errs.SubtypeUnknown and a 4xx status with an existing error returning that same
error unchanged. Assert the relevant subtype and object identity so regressions
in both branches fail the test.

Source: Coding guidelines

internal/imcontract/write.go (1)

140-141: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

root["completion"] can overwrite a server field.

finalizeBatch writes the ledger into the decoded response map. finalizeAssertion does the same at Line 193. If a server response ever contains a completion field, the CLI discards it without notice. Consider rejecting the response with a typed internal error when root already has a completion key, so that server data is never silently replaced.

As per coding guidelines: "When transcribing input or transforming requests, preserve values faithfully; never silently coerce unsupported inputs, ignore unhonored options, default missing identities, or discard writes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/write.go` around lines 140 - 141, Update finalizeBatch
and finalizeAssertion to detect whether the decoded response map already
contains the completion key before assigning the ledger. If present, reject the
response with the established typed internal error mechanism; otherwise preserve
the existing ledger assignment and result construction without overwriting
server data.

Source: Coding guidelines

internal/output/envelope_success_test.go (1)

239-248: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the partial result payload.

The test name states that the envelope carries the partial result, but the assertions cover only ok, hint, and error.type. The Data payload with completion.status == "partial" is not asserted. A regression that drops Data on the partial-failure path would still pass.

💚 Proposed additional assertion
 	if env["error"].(map[string]any)["type"] != "api" {
 		t.Fatalf("typed error missing: %#v", env)
 	}
+	data, ok := env["data"].(map[string]any)
+	if !ok {
+		t.Fatalf("partial result data missing: %#v", env)
+	}
+	completion, ok := data["completion"].(map[string]any)
+	if !ok || completion["status"] != "partial" {
+		t.Fatalf("partial completion missing: %#v", data)
+	}
 }

As per coding guidelines: "contract tests must assert the changed field or behavior directly so reverting the implementation causes failure."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/output/envelope_success_test.go` around lines 239 - 248, Extend the
assertions in the partial-failure envelope test to validate the Data payload
directly, including completion.status == "partial". Reuse the decoded env
structure and assert the nested payload is present and has the expected status,
so dropping Data causes the test to fail.

Source: Coding guidelines

internal/imcontract/write_test.go (2)

400-405: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert Param on the validation error.

This test asserts Category and Subtype only. ObserveRequest returns a validation error, and validation errors carry Param, which identifies the offending field. Use errors.As(err, &ve) with *errs.ValidationError and assert ve.Param so that a change in the reported field breaks the test.

As per coding guidelines: "Error-path tests must assert typed metadata through errs.ProblemOf (category, subtype, and param) and verify cause preservation rather than relying only on message substrings."

Based on learnings: errs.ProblemOf does not expose Param; read it from *errs.ValidationError through errors.As.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/write_test.go` around lines 400 - 405, Update the
ObserveRequest error assertion to extract the error with errors.As into
*errs.ValidationError, while retaining the existing errs.ProblemOf category and
subtype checks. Assert ve.Param matches the offending request field, and verify
the wrapped cause is preserved as required by the error-path test guidelines.

Sources: Coding guidelines, Learnings


41-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Check the ObserveRequest error in these tests.

ObserveRequest returns an error, and several tests discard it (Lines 41, 61, 75, 108, 135, 151, 365, 378, 473, 494). If request-evidence validation starts to fail, s.requested stays empty. Tests such as TestBatchRejectsUnmappableFailureEvidence then still fail on the response, so they pass for the wrong reason. Assert the error, as TestBatchPartialRecoveryMatrix does at Line 295.

💚 Example fix
-	s.ObserveRequest(tc.request)
+	if err := s.ObserveRequest(tc.request); err != nil {
+		t.Fatalf("ObserveRequest() error = %v", err)
+	}

Also applies to: 365-366

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/write_test.go` around lines 41 - 42, Update every listed
test call to ObserveRequest in write_test.go to capture and assert its returned
error, matching the pattern used by TestBatchPartialRecoveryMatrix. Ensure
request-evidence validation completes successfully before FinalizeSuccess or
other response assertions, including the occurrences around lines 41, 61, 75,
108, 135, 151, 365, 378, 473, and 494.
internal/imcontract/registry.go (1)

20-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the unused production helper out of registry.go.

stringsFrom is not referenced by production code and is unrelated to registry lookup. Move it next to the evidence extraction code in internal/imcontract/ledger.go.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/registry.go` around lines 20 - 22, Move the unused
stringsFrom helper from registry.go to ledger.go alongside the evidence
extraction code, preserving its signature and behavior; do not otherwise change
registry lookup logic.
internal/imcontract/message_mentions_test.go (1)

22-114: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding two uncovered builder cases.

The table omits two reachable branches of BuildMessageMentionResult:

  • All: true together with confirmable IDs, which must yield status = "accepted_unverified" and non-empty Confirmed (lines 108-112 of internal/imcontract/message_mentions.go).
  • A non-slice response, for example map[string]any{}, which sets ambiguous in parseResponseMentions and must yield partial_unattributed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/message_mentions_test.go` around lines 22 - 114, The
table for BuildMessageMentionResult is missing coverage for two reachable
branches. Add a case with All true and confirmable IDs, asserting
accepted_unverified with a non-empty Confirmed result, and add a case using a
non-slice response such as map[string]any, asserting partial_unattributed from
the ambiguous parseResponseMentions path.
internal/imcontract/catalog/registry.go (2)

43-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider passing materialization as a parameter instead of matching the key.

search compares key with the literal "im +messages-search" to decide RequiresMaterialization. The builder then depends on one specific catalog entry. A boolean parameter, or an explicit assignment at the call site on line 127, keeps the builder generic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/catalog/registry.go` around lines 43 - 55, Make search
generic by removing its key-specific "im +messages-search" comparison and
accepting materialization explicitly, either through a boolean parameter or
assignment at the relevant call site. Ensure the catalog entry for "im
+messages-search" still sets Strategy.RequiresMaterialization to true while
other search contracts retain the default.

259-274: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider rejecting duplicate contract keys while building the map.

buildContracts writes each entry into out keyed by c.Key. A duplicate key silently overwrites the earlier entry. ValidateRegistry iterates the map, so it cannot detect the loss. The count and key-list assertions in internal/imcontract/registry_test.go would fail, but the failure message would not name the duplicate.

♻️ Proposed fix to detect duplicates at build time
 	out := make(map[ContractKey]Contract, len(all))
+	duplicates := make([]ContractKey, 0)
 	for _, c := range all {
+		if _, exists := out[c.Key]; exists {
+			duplicates = append(duplicates, c.Key)
+		}
 		if c.PartialRecovery == "" &&

Then report duplicates from ValidateRegistry, or panic in an init guard if the catalog must be correct by construction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/catalog/registry.go` around lines 259 - 274, Update
buildContracts to detect repeated c.Key values before assigning to out, and
reject them instead of silently overwriting the existing contract. Propagate the
duplicate keys to ValidateRegistry so its validation error names the conflicting
keys, or use the project’s initialization guard to fail immediately if catalog
correctness is required at startup.
internal/imcontract/catalog/types.go (1)

114-131: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider decoupling the acceptance-only text from one command.

HelpAcceptanceOnly returns a message that names im chat.moderation get. Today only im chat.moderation update uses AcceptanceOnlyKind, so the text is correct. If another acceptance-only contract is added, the text will point to the wrong verification command. Storing the verification command on the Contract (or on Strategy.ReadHint, as im reactions batch_query already does) removes that risk.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/catalog/types.go` around lines 114 - 131, Decouple
HelpAcceptanceOnly from the hard-coded moderation command by storing the
applicable verification command on each Contract or reusing Strategy.ReadHint,
consistent with im reactions batch_query. Update the acceptance-only help
generation around HelpPolicy.Text and its callers so each contract supplies its
own verification guidance, while preserving the existing moderation update
message.
internal/imcontract/identity_notice_test.go (1)

8-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a nil-base case.

WithIdentityDefaultedNotice has a dedicated branch for a nil base map, because maps.Clone(nil) returns nil. No test covers that branch. Add a case that passes nil and asserts the returned map contains the identity notice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/identity_notice_test.go` around lines 8 - 31, Add a
nil-base test case for WithIdentityDefaultedNotice, passing nil and asserting
the returned map contains IdentityDefaultedNoticeKey with the expected resolved
identity and message. Cover the dedicated nil handling branch while preserving
the existing non-mutating merge test.
internal/output/emitter_contract_test.go (1)

152-181: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert the redaction property, not only the failure shape.

The test proves RedactedFallback bypasses the blocking scan. It does not prove the emitted document contains only allowlisted fields. Add an assertion that the decoded data.completion contains the expected keys and no payload text from the blocked result. The assertion then fails if a future change routes the original payload through this path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/output/emitter_contract_test.go` around lines 152 - 181, The test
TestEmitterRedactedFallbackSkipsBlockedPresentationScan must verify the redacted
payload contents, not just the failure envelope. After decoding env, inspect
data.completion and assert it contains only the expected allowlisted keys, with
no payload text from the blocked result; keep the existing env.OK and env.Error
assertions unchanged.
internal/output/emitter.go (1)

102-124: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider routing the hint to stderr automatically for non-envelope formats.

emitHint writes nothing unless HintToStderr is true. For pretty, table, and other non-envelope formats the envelope is not written, so a caller that sets Hint and forgets HintToStderr loses the hint with no signal. Deriving the stderr routing from the selected format would remove that failure mode.

Also applies to: 351-359

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/output/emitter.go` around lines 102 - 124, The Success method should
automatically route hints to stderr when emitting non-envelope formats such as
pretty, table, or other formatted output. Before calling emitHint, derive the
hint options from the selected output format while preserving explicit
HintToStderr behavior and leaving JSON/JQ envelope output unchanged.
internal/imcontract/read_test.go (1)

385-401: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use strings.Contains instead of the hand-rolled substring scan.

The file already imports strings. stringContains duplicates strings.Contains.

♻️ Proposed simplification
 func containsAny(s string, values ...string) bool {
 	for _, value := range values {
-		if value != "" && stringContains(s, value) {
+		if value != "" && strings.Contains(s, value) {
 			return true
 		}
 	}
 	return false
 }
-
-func stringContains(s, substr string) bool {
-	for i := 0; i+len(substr) <= len(s); i++ {
-		if s[i:i+len(substr)] == substr {
-			return true
-		}
-	}
-	return false
-}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/read_test.go` around lines 385 - 401, Replace the
hand-rolled scan in stringContains with the existing strings.Contains helper,
preserving the current boolean result and leaving containsAny’s empty-value
filtering unchanged. Remove stringContains if it is no longer needed and update
its call site accordingly.
internal/client/pagination_status_test.go (1)

279-315: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert typed metadata and cause preservation for the late-error cases.

Both subtests assert only the Go error type. The repository guideline requires error-path tests to assert category, subtype, and param through errs.ProblemOf, and to verify cause preservation. Add those assertions, including the wrapped *net.DNSError for the transport case and the upstream API failure for the API case.

As per coding guidelines: "Error-path tests must assert typed metadata through errs.ProblemOf (category, subtype, and param) and verify cause preservation rather than relying only on message substrings."

♻️ Example assertion for the transport case
 		var networkErr *errs.NetworkError
 		if !errors.As(err, &networkErr) {
 			t.Fatalf("error = %T %v, want typed NetworkError", err, err)
 		}
+		problem, ok := errs.ProblemOf(err)
+		if !ok || problem.Category != errs.CategoryNetwork {
+			t.Fatalf("problem = %#v, want network category", problem)
+		}
+		var dnsErr *net.DNSError
+		if !errors.As(err, &dnsErr) {
+			t.Fatalf("error = %v, want preserved DNS cause", err)
+		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/pagination_status_test.go` around lines 279 - 315, Extend the
late transport-error and API-error subtests around PaginateAllWithStatus to
validate errs.ProblemOf(err) metadata for category, subtype, and param, not just
errors.As type checks. Also assert cause preservation: the transport error must
retain the wrapped *net.DNSError, and the API error must retain the upstream API
failure as its cause, while preserving the existing partial-page and status
assertions.

Source: Coding guidelines

shortcuts/im/im_chat_members_list.go (1)

53-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the non-default page limit.

imPaginationFlags(10) hides why this command differs from the other IM reads, which pass imReadDefaultPageLimit. Declare a named constant next to chatMembersListDefaultPageSize, for example chatMembersListDefaultPageLimit = 10, and pass it here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/im/im_chat_members_list.go` at line 53, Declare a named
chat-members page-limit constant next to chatMembersListDefaultPageSize, then
replace the literal 10 passed to imPaginationFlags in the chat members list
command with that constant; keep the existing pagination behavior unchanged.
shortcuts/common/runner_partial_failure_test.go (3)

108-110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Check the ObserveRequest error here, as the test does at Line 135.

Line 110 discards the error from ObserveRequest. If request observation fails, the ledger has no requested items and the later completion assertions become misleading. Line 135 already uses the checked form; apply it here too.

💚 Proposed fix
-	rt.contractSession.ObserveRequest(map[string]any{"user_id_list": []any{"ou_a", "ou_b"}})
+	if err := rt.contractSession.ObserveRequest(map[string]any{"user_id_list": []any{"ou_a", "ou_b"}}); err != nil {
+		t.Fatal(err)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/common/runner_partial_failure_test.go` around lines 108 - 110,
Update the setup around rt.contractSession.ObserveRequest in the test to capture
and check its returned error, matching the checked form already used at the
later ObserveRequest call around line 135; fail the test immediately when
observation fails instead of discarding the error.

91-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the imcontract.Lookup result in the new contract tests. Every new test discards the ok value from imcontract.Lookup. If a contract key is renamed or removed, NewSession receives a zero Contract and the tests stop covering the intended contract without failing. Add a small helper in the package, for example mustLookupContract(t, key), and use it at each site.

  • shortcuts/common/runner_partial_failure_test.go#L91-L92: use the helper here and at the other lookups in this file (Line 108, Line 133, Line 176, Line 302, Line 321, Line 362, Line 414).
  • shortcuts/common/call_api_typed_test.go#L166-L177: use the helper for im messages urgent_app.
  • shortcuts/common/runner_contentsafety_test.go#L110-L111: use the helper for im chat.moderation update.
  • shortcuts/common/runner_jq_test.go#L182-L183: use the helper for im +messages-send.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/common/runner_partial_failure_test.go` around lines 91 - 92, Add a
package-level mustLookupContract(t, key) helper that asserts the success result
from imcontract.Lookup and returns the contract. Replace every unchecked lookup
in shortcuts/common/runner_partial_failure_test.go at lines 91-92, 108, 133,
176, 302, 321, 362, and 414, plus the lookup in
shortcuts/common/call_api_typed_test.go lines 166-177,
shortcuts/common/runner_contentsafety_test.go lines 110-111, and
shortcuts/common/runner_jq_test.go lines 182-183; each site must use the helper
so missing or renamed contracts fail the tests.

320-323: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fail the test when NewReadSession returns an error.

The assignment discards the error. If construction fails, rt.readSession is nil and the test then asserts legacy non-contract output instead of the read contract. The same pattern appears at Line 363 and Line 415.

💚 Proposed fix
-	rt.readSession, _ = imcontract.NewReadSession(contract, imcontract.ReadOptions{FullRead: true})
+	readSession, err := imcontract.NewReadSession(contract, imcontract.ReadOptions{FullRead: true})
+	if err != nil {
+		t.Fatal(err)
+	}
+	rt.readSession = readSession
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/common/runner_partial_failure_test.go` around lines 320 - 323,
Update the `imcontract.NewReadSession` assignments near the `rt.readSession`
setup, including the corresponding cases around lines 363 and 415, to capture
and assert the returned error immediately. Make each test fail when session
construction fails before using `rt.readSession`, while preserving the existing
contract-based assertions.
cmd/service/service.go (1)

497-507: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the write-side --page-all rejection next to the read-side guard.

The read guard runs at Line 391, before identity resolution, config load, and scope checks. The write guard runs here, after buildServiceRequest and after contractSession.ObserveRequest. Both are pure flag validations. Reject --page-all for contract-managed writes together with the read guard so the command fails before credential and request work.

♻️ Proposed relocation
 	contractManagedWrite := contractFound && contract.Strategy.Kind.IsWrite()
 	contractManagedRead := contractFound && contract.Strategy.Kind.IsRead()
+	if contractManagedWrite && opts.PageAll {
+		return errs.NewValidationError(errs.SubtypeInvalidArgument,
+			"--page-all is not valid for an IM write command").WithParam("--page-all")
+	}
 	if contractManagedRead && opts.PageAll &&
 	if opts.PageAll {
-		if contractSession != nil {
-			return errs.NewValidationError(errs.SubtypeInvalidArgument,
-				"--page-all is not valid for an IM write command").WithParam("--page-all")
-		}
 		if readSession != nil {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/service/service.go` around lines 497 - 507, Move the contract-managed
write rejection for --page-all from the PageAll branch near servicePaginate into
the existing early flag-validation guard alongside the read-side --page-all
check. Ensure contract writes are rejected before identity resolution,
configuration loading, scope checks, buildServiceRequest, and
contractSession.ObserveRequest, while preserving the existing read pagination
and non-contract pagination behavior.
shortcuts/common/runner.go (1)

507-520: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared idempotency-key merge into internal/imcontract. Both runtimes rebuild the observed write payload by copying the request body and adding the uuid query value. The rule is one contract-evidence rule, so a change in one place will silently diverge from the other.

  • shortcuts/common/runner.go#L507-L520: replace the inline clone with a call to a shared helper, for example imcontract.RequestEvidence(body, uuid), and pass query["uuid"].
  • cmd/service/service.go#L473-L488: replace the inline clone with the same helper, and pass request.Params["uuid"].
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/common/runner.go` around lines 507 - 520, Extract the shared
request-evidence merge into an imcontract helper, such as RequestEvidence, that
copies the request body and applies the uuid value. Update
shortcuts/common/runner.go lines 507-520 to use the helper with query["uuid"],
and cmd/service/service.go lines 473-488 to use the same helper with
request.Params["uuid"], removing both inline clone implementations.
shortcuts/im/mentions.go (1)

399-412: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard the slice capacity against an empty request.

len(targets)*2-1 evaluates to -1 when targets is empty, and make panics on a negative capacity. Today every caller passes a requested mention, so the path is unreachable. Compute the capacity so a future caller cannot panic.

♻️ Proposed defensive fix
-	paragraph := make([]map[string]interface{}, 0, len(targets)*2-1)
+	capacity := 0
+	if len(targets) > 0 {
+		capacity = len(targets)*2 - 1
+	}
+	paragraph := make([]map[string]interface{}, 0, capacity)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/im/mentions.go` around lines 399 - 412, Update postMentionParagraph
to compute a non-negative paragraph capacity when targets is empty, while
retaining the existing len(targets)*2-1 capacity for non-empty requests. Ensure
make cannot panic for an empty request and preserve the current paragraph
construction behavior.
shortcuts/im/helpers.go (1)

26-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused legacy inline at-tag helper.

normalizeAtMentions has no callers outside its declaration, so delete normalizeAtMentions and mentionFixRe with this change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/im/helpers.go` around lines 26 - 33, Remove the unused
normalizeAtMentions helper and its associated mentionFixRe declaration from
shortcuts/im/helpers.go, leaving the surrounding imports and helper
functionality intact.
internal/imcontract/output_fallback_test.go (1)

130-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use errs.ProblemOf for typed metadata assertions.

This test asserts Category, Subtype, and Message through a direct type assertion env.Error.(*errs.Problem). As per path instructions for **/*_test.go, "Error-path tests must assert typed metadata through errs.ProblemOf (category, subtype, and param) and verify cause preservation rather than relying only on message substrings." Use errs.ProblemOf(env.Error) instead of the direct type assertion. This also avoids an unhandled panic if env.Error ever holds a different concrete type.

♻️ Proposed fix
-	problem := env.Error.(*errs.Problem)
-	if problem.Category != errs.CategoryPolicy || problem.Subtype != errs.SubtypeContentSafety ||
+	problem, ok := errs.ProblemOf(env.Error)
+	if !ok {
+		t.Fatalf("env.Error is not a typed problem: %#v", env.Error)
+	}
+	if problem.Category != errs.CategoryPolicy || problem.Subtype != errs.SubtypeContentSafety ||
 		problem.Message != "Output blocked after the IM write completed" {
 		t.Fatalf("problem = %#v", problem)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/output_fallback_test.go` around lines 130 - 140, Update
TestContentSafetyOutputFallbackUsesFixedPublicProblem to obtain the typed
problem through errs.ProblemOf(env.Error) instead of directly asserting
*errs.Problem. Assert the returned category, subtype, and param metadata as
required, and verify the original cause is preserved alongside the existing
exit-code validation.

Source: Path instructions

internal/imcontract/session.go (1)

124-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add named constants for the message-mention contract keys.

internal/imcontract/session.go checks key == "im +messages-send" or key == "im +messages-reply" while the catalog registers those keys as required(...) literals. Add catalog ContractKey constants for these operations and use them from supportsMessageMentionResult to keep the selector and catalog together.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/session.go` around lines 124 - 126, Add named ContractKey
constants for the “im +messages-send” and “im +messages-reply” catalog entries,
then update supportsMessageMentionResult to compare against those constants
instead of string literals. Replace the corresponding required(...) literals in
the catalog with the same constants so registration and selection share a single
source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@affordance/im.md`:
- Around line 146-178: Add an `### Examples` block to each of `messages
urgent_app`, `messages urgent_phone`, and `messages urgent_sms`, using a
copyable command consistent with the proposed `lark-cli im messages urgent_*`
invocation and each section’s command name. Place the block after `###
Prerequisites` and preserve the existing guidance and prerequisite text.

In `@internal/client/pagination_status.go`:
- Around line 121-124: Update PaginateAllWithStatus and its related pagination
loop to treat PaginationOptions.PageLimit == 0 as the documented default of 10,
matching PaginateAll and StreamPages rather than fetching unlimited pages.
Preserve explicit nonzero PageLimit values and keep the existing page-delay
handling unchanged.

In `@internal/imcontract/http_status.go`:
- Around line 15-32: Update the 5xx and 429 handling in the HTTP status
normalization function to attach the incoming err via WithCause(err) whenever
err is non-nil, while preserving the existing HTTP-driven classifications and
metadata. Keep nil errors unattached and align this behavior with the existing
4xx branch.

In `@internal/imcontract/message_mentions_test.go`:
- Around line 155-159: Update the error-path table cases “unknown status” and
“replay scope cannot authorize replay” with expected category and subtype
metadata, then extend the test assertions to inspect the typed error via
errs.ProblemOf and verify category, subtype, param, and cause preservation.
Replace the existing err-only checks while keeping the current successful-case
behavior unchanged.

In `@internal/imcontract/output_fallback.go`:
- Around line 42-92: Update allowlistedCompletion to use a conservative fallback
status instead of seeding status as "complete"; use the established
"accepted_unverified" value (and avoid implying retry_scope "none" when the
source status is unknown). Preserve copyCompletionStatus behavior for recognized
statuses while ensuring unrecognized completion statuses cannot be reported as
complete and bypass recovery.

In `@internal/output/envelope_success.go`:
- Around line 55-88: Update WriteEnvelope and its EmitOptions construction to
propagate env.ContentSafetyAlert into the emitter, preserving the supplied
envelope value through emission. Use the existing ContentSafetyAlert field and
avoid silently discarding it.

In `@shortcuts/common/runner.go`:
- Around line 802-808: Update RuntimeContext.OutPartialFailure to return the
contract-provided resultExit after emitFinalized when that result is present,
while preserving the existing outputErr handling and
output.PartialFailure(output.ExitAPI) fallback when no contract result exists.

In `@shortcuts/im/im_chat_messages_list.go`:
- Around line 124-132: Preserve the user-supplied page_token in the
first-request branches of the paginateIM callbacks in
shortcuts/im/im_chat_messages_list.go (lines 124-132) and
shortcuts/im/im_chat_search.go (lines 106-114) instead of deleting it when
pageToken is empty; alternatively, return the established typed validation error
when pagination cannot continue from that requested start token.

In `@shortcuts/im/im_feed_group_item_test.go`:
- Line 426: Extend the error-path assertions in
shortcuts/im/im_feed_group_item_test.go:426-426 for the “list bad page-limit”
case to require problem.Category == errs.CategoryValidation alongside subtype
and parameter metadata. In shortcuts/im/builders_test.go:421-437 and 679-695,
after errs.ProblemOf(err), add the same CategoryValidation assertion while
preserving the existing subtype and param checks.

In `@shortcuts/im/im_feed_shortcut_test.go`:
- Around line 494-495: Validate the contract lookups before creating sessions in
both affected sites: shortcuts/im/im_feed_shortcut_test.go lines 494-495 for “im
+feed-shortcut-create” and lines 703-704 for “im +feed-shortcut-remove”. Capture
the ok result from imcontract.Lookup and call t.Fatal with a clear
missing-contract message when it is false; only pass the validated contract to
imcontract.NewSession.

In `@shortcuts/im/im_flag_cancel.go`:
- Around line 157-167: Update the error branches in the feed-layer resolution
flow around getMessageChatID and resolveThreadFeedItemType to preserve the
specific unknown-feed-layer skip reason in the structured completion/result
payload or tracked stderr entry. Keep recording FactFlagFeedLayerPending, but
ensure finalization no longer relies solely on that fact kind to synthesize a
generic “feed” pending item.

In `@shortcuts/im/im_flag_list.go`:
- Around line 217-219: Update executeListAllPages in
shortcuts/im/im_flag_list.go at lines 217-219 and searchMessages in
shortcuts/im/im_messages_search.go at lines 391-393 to propagate or record
pageErr even when pages were successfully fetched, using the shared pagination
status mechanism such as RecordPagination. Ensure any post-first-page pagination
failure marks the read incomplete and causes callers to fail closed rather than
returning partial data as success.

In `@shortcuts/im/im_messages_send.go`:
- Around line 226-233: Make mention post-processing non-failing after a
successful write by parsing the mention request once before the write in
shortcuts/im/im_messages_send.go lines 226-233 and passing that mentionRequest
into the result builder instead of calling addMessageMentionResult to re-parse
it; apply the same change in shortcuts/im/im_messages_reply.go lines 202-209,
reusing the mention request computed before sending the reply.

In `@skills/lark-im/references/lark-im-chat-create.md`:
- Around line 145-146: Update the chat creation command in Scenario 3 to include
the explicit --as bot option, matching the identity used by the subsequent
messages-send command.

In `@skills/lark-im/references/lark-im-chat-identity.md`:
- Line 43: Pin bot identity consistently in the documented chat workflows:
update the group-creation command in
skills/lark-im/references/lark-im-chat-identity.md:43-43 to run as bot, and
update the preceding +chat-search command in
skills/lark-im/references/lark-im-chat-search.md:107-107 to run as bot as well,
keeping both operations under the same identity.

In `@skills/lark-im/references/lark-im-feed-shortcut-list.md`:
- Around line 9-13: Make the exhaustive-read contract explicit across all listed
references: in skills/lark-im/references/lark-im-feed-shortcut-list.md lines
9-13, document meta.complete in the response structure and JSON example; in
skills/lark-im/references/lark-im-feed-group-list.md lines 3-5 and 26-37, state
that merging covers fetched pages only and require meta.complete=true beside
pagination and output guidance; in
skills/lark-im/references/lark-im-feed-group-list-item.md lines 31-39,
distinguish merged pages from a complete dataset and document the
meta.complete=true gate; in skills/lark-im/references/lark-im-feed-groups.md
lines 33, 245, and 329, add the same completion condition to the overview, list,
and list_item documentation; and in
skills/lark-im/references/lark-im-flag-list.md line 9, replace the vague
completion wording with the exact meta.complete=true check and its output path.

In `@skills/lark-im/references/lark-im-messages-send.md`:
- Line 267: Align the remote Markdown-image upload failure contract in both
skills/lark-im/references/lark-im-messages-send.md:267 and
skills/lark-im/references/lark-im-messages-reply.md:267 with the existing rules
at Lines 72/277 and 73/276: remove the failed image, continue sending the
message, and issue the documented warning. Ensure both references describe the
same behavior and do not claim that nothing is sent or that the command fails.

---

Outside diff comments:
In `@shortcuts/im/im_messages_search.go`:
- Around line 368-377: The pagination flow must use the pageLimit computed by
messagesSearchPaginationConfig(), including the messagesSearchMaxPageLimit value
selected for page-all. Update paginateIMWithMode() to consume and propagate that
returned limit for pagination and related dry-run or warning messages instead of
rereading runtime.Int("page-limit").

---

Nitpick comments:
In `@cmd/service/service.go`:
- Around line 497-507: Move the contract-managed write rejection for --page-all
from the PageAll branch near servicePaginate into the existing early
flag-validation guard alongside the read-side --page-all check. Ensure contract
writes are rejected before identity resolution, configuration loading, scope
checks, buildServiceRequest, and contractSession.ObserveRequest, while
preserving the existing read pagination and non-contract pagination behavior.

In `@internal/affordance/affordance_im_test.go`:
- Line 8: Replace the os.DirFS-based repository root in the affordance test with
an internal/vfs helper that returns an io/fs.FS for the repository tree, then
pass that vfs-backed root to SetSource. Remove the direct os filesystem usage
while preserving the test’s existing source path and behavior.

In `@internal/client/pagination_status_test.go`:
- Around line 279-315: Extend the late transport-error and API-error subtests
around PaginateAllWithStatus to validate errs.ProblemOf(err) metadata for
category, subtype, and param, not just errors.As type checks. Also assert cause
preservation: the transport error must retain the wrapped *net.DNSError, and the
API error must retain the upstream API failure as its cause, while preserving
the existing partial-page and status assertions.

In `@internal/imcontract/catalog/registry.go`:
- Around line 43-55: Make search generic by removing its key-specific "im
+messages-search" comparison and accepting materialization explicitly, either
through a boolean parameter or assignment at the relevant call site. Ensure the
catalog entry for "im +messages-search" still sets
Strategy.RequiresMaterialization to true while other search contracts retain the
default.
- Around line 259-274: Update buildContracts to detect repeated c.Key values
before assigning to out, and reject them instead of silently overwriting the
existing contract. Propagate the duplicate keys to ValidateRegistry so its
validation error names the conflicting keys, or use the project’s initialization
guard to fail immediately if catalog correctness is required at startup.

In `@internal/imcontract/catalog/types.go`:
- Around line 114-131: Decouple HelpAcceptanceOnly from the hard-coded
moderation command by storing the applicable verification command on each
Contract or reusing Strategy.ReadHint, consistent with im reactions batch_query.
Update the acceptance-only help generation around HelpPolicy.Text and its
callers so each contract supplies its own verification guidance, while
preserving the existing moderation update message.

In `@internal/imcontract/http_status_test.go`:
- Around line 30-39: Add test cases for NormalizeHTTPError covering a generic
4xx status mapping to errs.SubtypeUnknown and a 4xx status with an existing
error returning that same error unchanged. Assert the relevant subtype and
object identity so regressions in both branches fail the test.

In `@internal/imcontract/identity_notice_test.go`:
- Around line 8-31: Add a nil-base test case for WithIdentityDefaultedNotice,
passing nil and asserting the returned map contains IdentityDefaultedNoticeKey
with the expected resolved identity and message. Cover the dedicated nil
handling branch while preserving the existing non-mutating merge test.

In `@internal/imcontract/materialization.go`:
- Around line 9-16: Add explicit JSON tags to MissingMessageIDs,
UnresolvedHitCount, and UnexpectedMessageCount in MaterializationStatus, using
the snake_case keys emitted by ledger(), while preserving json:"-" for
RequestedIDs, ResolvedIDs, and Cause.

In `@internal/imcontract/message_mentions_test.go`:
- Around line 22-114: The table for BuildMessageMentionResult is missing
coverage for two reachable branches. Add a case with All true and confirmable
IDs, asserting accepted_unverified with a non-empty Confirmed result, and add a
case using a non-slice response such as map[string]any, asserting
partial_unattributed from the ambiguous parseResponseMentions path.

In `@internal/imcontract/output_fallback_test.go`:
- Around line 130-140: Update
TestContentSafetyOutputFallbackUsesFixedPublicProblem to obtain the typed
problem through errs.ProblemOf(env.Error) instead of directly asserting
*errs.Problem. Assert the returned category, subtype, and param metadata as
required, and verify the original cause is preserved alongside the existing
exit-code validation.

In `@internal/imcontract/read_test.go`:
- Around line 385-401: Replace the hand-rolled scan in stringContains with the
existing strings.Contains helper, preserving the current boolean result and
leaving containsAny’s empty-value filtering unchanged. Remove stringContains if
it is no longer needed and update its call site accordingly.

In `@internal/imcontract/registry.go`:
- Around line 20-22: Move the unused stringsFrom helper from registry.go to
ledger.go alongside the evidence extraction code, preserving its signature and
behavior; do not otherwise change registry lookup logic.

In `@internal/imcontract/session.go`:
- Around line 124-126: Add named ContractKey constants for the “im
+messages-send” and “im +messages-reply” catalog entries, then update
supportsMessageMentionResult to compare against those constants instead of
string literals. Replace the corresponding required(...) literals in the catalog
with the same constants so registration and selection share a single source of
truth.

In `@internal/imcontract/write_test.go`:
- Around line 400-405: Update the ObserveRequest error assertion to extract the
error with errors.As into *errs.ValidationError, while retaining the existing
errs.ProblemOf category and subtype checks. Assert ve.Param matches the
offending request field, and verify the wrapped cause is preserved as required
by the error-path test guidelines.
- Around line 41-42: Update every listed test call to ObserveRequest in
write_test.go to capture and assert its returned error, matching the pattern
used by TestBatchPartialRecoveryMatrix. Ensure request-evidence validation
completes successfully before FinalizeSuccess or other response assertions,
including the occurrences around lines 41, 61, 75, 108, 135, 151, 365, 378, 473,
and 494.

In `@internal/imcontract/write.go`:
- Around line 140-141: Update finalizeBatch and finalizeAssertion to detect
whether the decoded response map already contains the completion key before
assigning the ledger. If present, reject the response with the established typed
internal error mechanism; otherwise preserve the existing ledger assignment and
result construction without overwriting server data.

In `@internal/output/emitter_contract_test.go`:
- Around line 152-181: The test
TestEmitterRedactedFallbackSkipsBlockedPresentationScan must verify the redacted
payload contents, not just the failure envelope. After decoding env, inspect
data.completion and assert it contains only the expected allowlisted keys, with
no payload text from the blocked result; keep the existing env.OK and env.Error
assertions unchanged.

In `@internal/output/emitter.go`:
- Around line 102-124: The Success method should automatically route hints to
stderr when emitting non-envelope formats such as pretty, table, or other
formatted output. Before calling emitHint, derive the hint options from the
selected output format while preserving explicit HintToStderr behavior and
leaving JSON/JQ envelope output unchanged.

In `@internal/output/envelope_success_test.go`:
- Around line 239-248: Extend the assertions in the partial-failure envelope
test to validate the Data payload directly, including completion.status ==
"partial". Reuse the decoded env structure and assert the nested payload is
present and has the expected status, so dropping Data causes the test to fail.

In `@internal/qualitygate/rules/run.go`:
- Line 47: Consolidate IM coverage diagnostic protection in Run and
filterPRDiagnostics by choosing one mechanism: either append imContractDiags
before filtering so the existing bypass handles them, or remove the bypass and
retain the post-filter append. Eliminate the redundant or unreachable path while
preserving IM coverage diagnostics for production callers and existing tests.

In `@shortcuts/common/runner_partial_failure_test.go`:
- Around line 108-110: Update the setup around rt.contractSession.ObserveRequest
in the test to capture and check its returned error, matching the checked form
already used at the later ObserveRequest call around line 135; fail the test
immediately when observation fails instead of discarding the error.
- Around line 91-92: Add a package-level mustLookupContract(t, key) helper that
asserts the success result from imcontract.Lookup and returns the contract.
Replace every unchecked lookup in
shortcuts/common/runner_partial_failure_test.go at lines 91-92, 108, 133, 176,
302, 321, 362, and 414, plus the lookup in
shortcuts/common/call_api_typed_test.go lines 166-177,
shortcuts/common/runner_contentsafety_test.go lines 110-111, and
shortcuts/common/runner_jq_test.go lines 182-183; each site must use the helper
so missing or renamed contracts fail the tests.
- Around line 320-323: Update the `imcontract.NewReadSession` assignments near
the `rt.readSession` setup, including the corresponding cases around lines 363
and 415, to capture and assert the returned error immediately. Make each test
fail when session construction fails before using `rt.readSession`, while
preserving the existing contract-based assertions.

In `@shortcuts/common/runner.go`:
- Around line 507-520: Extract the shared request-evidence merge into an
imcontract helper, such as RequestEvidence, that copies the request body and
applies the uuid value. Update shortcuts/common/runner.go lines 507-520 to use
the helper with query["uuid"], and cmd/service/service.go lines 473-488 to use
the same helper with request.Params["uuid"], removing both inline clone
implementations.

In `@shortcuts/im/helpers.go`:
- Around line 26-33: Remove the unused normalizeAtMentions helper and its
associated mentionFixRe declaration from shortcuts/im/helpers.go, leaving the
surrounding imports and helper functionality intact.

In `@shortcuts/im/im_chat_members_list.go`:
- Line 53: Declare a named chat-members page-limit constant next to
chatMembersListDefaultPageSize, then replace the literal 10 passed to
imPaginationFlags in the chat members list command with that constant; keep the
existing pagination behavior unchanged.

In `@shortcuts/im/mentions.go`:
- Around line 399-412: Update postMentionParagraph to compute a non-negative
paragraph capacity when targets is empty, while retaining the existing
len(targets)*2-1 capacity for non-empty requests. Ensure make cannot panic for
an empty request and preserve the current paragraph construction behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment thread affordance/im.md
Comment on lines +146 to +178
## messages urgent_app
Send an in-app urgent notification for an existing bot-sent message.

### Avoid when
- The user asked for a phone call → use [[messages urgent_phone]]
- The user asked for SMS → use [[messages urgent_sms]]
- The message has not been sent yet → send it first with [[+messages-send]]

### Prerequisites
- message_id of a message sent by the calling bot
- bot identity; the bot must still be in the conversation

## messages urgent_phone
Send a phone urgent notification for an existing bot-sent message.

### Avoid when
- The user asked only for an in-app prompt → use [[messages urgent_app]]
- The user asked for SMS → use [[messages urgent_sms]]

### Prerequisites
- message_id of a message sent by the calling bot
- bot identity; the bot must still be in the conversation

## messages urgent_sms
Send an SMS urgent notification for an existing bot-sent message.

### Avoid when
- The user asked only for an in-app prompt → use [[messages urgent_app]]
- The user asked for a phone call → use [[messages urgent_phone]]

### Prerequisites
- message_id of a message sent by the calling bot
- bot identity; the bot must still be in the conversation

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add example commands to the three messages urgent_* sections.

Every other section in this file ends with an ### Examples block that contains a copyable command. The messages urgent_app, messages urgent_phone, and messages urgent_sms sections stop after ### Prerequisites. Agents reading the rendered affordance get no invocation form for these three commands.

📝 Proposed addition for each urgent section
 ### Prerequisites
 - message_id of a message sent by the calling bot
 - bot identity; the bot must still be in the conversation
+
+### Examples
+
+**Send an in-app urgent notification**
+```bash
+lark-cli im messages urgent_app --message-id <message_id> --user-id-type open_id --data '{"user_id_list":["<open_id>"]}' --as bot
+```
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@affordance/im.md` around lines 146 - 178, Add an `### Examples` block to each
of `messages urgent_app`, `messages urgent_phone`, and `messages urgent_sms`,
using a copyable command consistent with the proposed `lark-cli im messages
urgent_*` invocation and each section’s command name. Place the block after `###
Prerequisites` and preserve the existing guidance and prerequisite text.

Comment on lines +121 to +124
pageDelay := opts.PageDelay
if pageDelay == 0 {
pageDelay = 200
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare page-limit defaulting between the legacy and status pagination loops, and list callers.
set -euo pipefail

fd -t f 'pagination.*\.go' internal/client --exec sh -c 'echo "== $1"; rg -n "PageLimit|pageLimit|PageDelay" "$1"' sh {}

echo "== callers of the status APIs"
rg -n -C4 'PaginateAllWithStatus|StreamPagesWithStatus' --glob '!internal/client/**'

Repository: larksuite/cli

Length of output: 1080


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate pagination files"
fd -t f 'pagination.*\.go' internal/client

echo "== pagination.go relevant lines"
nl -ba internal/client/pagination.go | sed -n '1,140p'

echo "== pagination_status.go relevant ranges"
nl -ba internal/client/pagination_status.go | sed -n '1,280p'

echo "== internal references to PaginationOptions and status APIs"
rg -n "PaginationOptions|PaginateAllWithStatus|StreamPagesWithStatus|PageLimit\s*:" internal/client

Repository: larksuite/cli

Length of output: 362


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== pagination.go relevant lines"
awk '{printf "%6d\t%s\n", NR, $0}' internal/client/pagination.go | sed -n '1,140p'

echo "== pagination_status.go relevant range"
awk '{printf "%6d\t%s\n", NR, $0}' internal/client/pagination_status.go | sed -n '1,280p'

echo "== status test relevant page-limit/default coverage"
awk '{printf "%6d\t%s\n", NR, $0}' internal/client/pagination_status_test.go | sed -n '100,230p'

echo "== internal usages"
rg -n "PaginationOptions|PaginateAllWithStatus|StreamPagesWithStatus|PageLimit\s*:" internal/client || true

Repository: larksuite/cli

Length of output: 21830


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== client.go paginate functions"
awk '{printf "%6d\t%s\n", NR, $0}' internal/client/client.go | sed -n '320,460p'

echo "== client.go existing callers"
rg -n -C5 'PaginateAll\(|StreamPages\(|PaginationOptions{' internal/client/client_test.go internal/client/client.go || true

Repository: larksuite/cli

Length of output: 5880


Align PageLimit == 0 with the documented default.

PaginationOptions.PageLimit documents 0 = unlimited (default: 10), PaginateAll()/StreamPages() default to 10 pages, and PaginateAllWithStatus() currently fetches all pages when PageLimit is zero. Apply the same default in both loops or remove/change the conflicting field documentation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/pagination_status.go` around lines 121 - 124, Update
PaginateAllWithStatus and its related pagination loop to treat
PaginationOptions.PageLimit == 0 as the documented default of 10, matching
PaginateAll and StreamPages rather than fetching unlimited pages. Preserve
explicit nonzero PageLimit values and keep the existing page-delay handling
unchanged.

Comment on lines +15 to +32
if status >= 500 {
normalized := errs.NewNetworkError(
errs.SubtypeNetworkServer,
"HTTP %d server error",
status,
).WithCode(status).WithRetryable()
if logID != "" {
normalized.WithLogID(logID)
}
return normalized
}
if status == 429 {
normalized := errs.NewAPIError(errs.SubtypeRateLimit, "HTTP 429 rate limit").WithCode(status)
if logID != "" {
normalized.WithLogID(logID)
}
return normalized
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Preserve the original error as the cause.

The 5xx branch and the 429 branch drop the incoming err. The classification result must stay HTTP-driven, but the lower-layer business error is then lost for diagnostics and for errors.Is/errors.As checks by callers. The 4xx branch at Line 33 keeps the typed lower-layer error, so the handling is also inconsistent inside this function.

Attach the original error with .WithCause(err) when err != nil.

As per coding guidelines: "Preserve typed lower-layer errors unchanged and preserve causes with .WithCause(err)."

🐛 Proposed fix
 	if status >= 500 {
 		normalized := errs.NewNetworkError(
 			errs.SubtypeNetworkServer,
 			"HTTP %d server error",
 			status,
 		).WithCode(status).WithRetryable()
+		if err != nil {
+			normalized.WithCause(err)
+		}
 		if logID != "" {
 			normalized.WithLogID(logID)
 		}
 		return normalized
 	}
 	if status == 429 {
 		normalized := errs.NewAPIError(errs.SubtypeRateLimit, "HTTP 429 rate limit").WithCode(status)
+		if err != nil {
+			normalized.WithCause(err)
+		}
 		if logID != "" {
 			normalized.WithLogID(logID)
 		}
 		return normalized
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if status >= 500 {
normalized := errs.NewNetworkError(
errs.SubtypeNetworkServer,
"HTTP %d server error",
status,
).WithCode(status).WithRetryable()
if logID != "" {
normalized.WithLogID(logID)
}
return normalized
}
if status == 429 {
normalized := errs.NewAPIError(errs.SubtypeRateLimit, "HTTP 429 rate limit").WithCode(status)
if logID != "" {
normalized.WithLogID(logID)
}
return normalized
}
if status >= 500 {
normalized := errs.NewNetworkError(
errs.SubtypeNetworkServer,
"HTTP %d server error",
status,
).WithCode(status).WithRetryable()
if err != nil {
normalized.WithCause(err)
}
if logID != "" {
normalized.WithLogID(logID)
}
return normalized
}
if status == 429 {
normalized := errs.NewAPIError(errs.SubtypeRateLimit, "HTTP 429 rate limit").WithCode(status)
if err != nil {
normalized.WithCause(err)
}
if logID != "" {
normalized.WithLogID(logID)
}
return normalized
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/http_status.go` around lines 15 - 32, Update the 5xx and
429 handling in the HTTP status normalization function to attach the incoming
err via WithCause(err) whenever err is non-nil, while preserving the existing
HTTP-driven classifications and metadata. Keep nil errors unattached and align
this behavior with the existing 4xx branch.

Source: Coding guidelines

Comment on lines +155 to +159
{name: "unknown status", mention: validMentionResult("mystery"), wantErr: true},
{name: "replay scope cannot authorize replay", mention: MessageMentionResult{
Status: "partial", Requested: []string{"ou_a"}, Confirmed: []MessageMentionConfirmation{},
Missing: []string{"ou_a"}, All: "not_requested", RetryScope: "whole_request",
}, wantErr: true},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert typed error metadata on the two error-path cases.

The unknown status and replay scope cannot authorize replay cases only check that err is non-nil. The repository test guideline requires error-path tests to assert the typed metadata through errs.ProblemOf and to verify cause preservation. Add expected category and subtype fields to the table and assert them, so a change from invalidEvidence to another error class fails the test.

💚 Proposed test change
 	tests := []struct {
 		name     string
 		mention  any
 		wantOK   bool
 		wantExit int
 		wantErr  bool
+		wantCategory errs.Category
+		wantSubtype  errs.Subtype
 	}{

Then in the body:

 			if (err != nil) != tt.wantErr {
 				t.Fatalf("FinalizeSuccess() error = %v, wantErr %v", err, tt.wantErr)
 			}
 			if err != nil {
+				problem, ok := errs.ProblemOf(err)
+				if !ok {
+					t.Fatalf("error is not typed: %v", err)
+				}
+				if problem.Category != tt.wantCategory || problem.Subtype != tt.wantSubtype {
+					t.Fatalf("problem = %#v", problem)
+				}
 				return
 			}

Based on coding guidelines: "Error-path tests must assert typed metadata through errs.ProblemOf (category, subtype, and param) and verify cause preservation rather than relying only on message substrings."

Also applies to: 169-174

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/message_mentions_test.go` around lines 155 - 159, Update
the error-path table cases “unknown status” and “replay scope cannot authorize
replay” with expected category and subtype metadata, then extend the test
assertions to inspect the typed error via errs.ProblemOf and verify category,
subtype, param, and cause preservation. Replace the existing err-only checks
while keeping the current successful-case behavior unchanged.

Source: Coding guidelines

Comment on lines +42 to +92
func allowlistedCompletion(data any) map[string]any {
summary := map[string]any{
"status": "complete",
"retry_scope": "none",
}
root, ok := data.(map[string]any)
if !ok {
return summary
}
completionValue, hasCompletion := root["completion"]
switch completion := completionValue.(type) {
case Completion:
copyCompletionStatus(summary, completion.Status)
summary["requested_count"] = completion.RequestedCount
summary["succeeded_count"] = completion.SucceededCount
summary["failed_count"] = completion.FailedCount
summary["pending_count"] = completion.PendingCount
copyCompletionRetryScope(summary, completion.RetryScope)
return summary
case map[string]any:
if value, ok := completion["status"].(string); ok {
copyCompletionStatus(summary, value)
}
copyCompletionCount(summary, completion, "requested_count")
copyCompletionCount(summary, completion, "succeeded_count")
copyCompletionCount(summary, completion, "failed_count")
copyCompletionCount(summary, completion, "pending_count")
if value, exists := completion["final_state_verified"]; exists {
if verified, valid := value.(bool); valid {
summary["final_state_verified"] = verified
}
}
if value, ok := completion["retry_scope"].(string); ok {
copyCompletionRetryScope(summary, value)
}
}
if !hasCompletion {
if mention, ok := root["mention_result"].(MessageMentionResult); ok {
copyCompletionStatus(summary, mention.Status)
copyCompletionRetryScope(summary, mention.RetryScope)
}
}
return summary
}

func copyCompletionStatus(dst map[string]any, value string) {
switch value {
case "complete", "partial", "accepted_unverified", "partial_unattributed":
dst["status"] = value
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Do not default the fallback status to complete when the real status is unrecognized.

allowlistedCompletion seeds status: "complete" and retry_scope: "none". copyCompletionStatus only overwrites the status for four known values. If the completion carries any other status (for example a future status string), the fallback reports complete with retry_scope: "none" while the counts can still show failures. An agent then skips recovery. Prefer a conservative default, such as accepted_unverified, or omit the fields when the source status is unknown.

🛡️ Proposed conservative default
 func allowlistedCompletion(data any) map[string]any {
 	summary := map[string]any{
-		"status":      "complete",
-		"retry_scope": "none",
+		"status":      "accepted_unverified",
+		"retry_scope": "whole_request",
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func allowlistedCompletion(data any) map[string]any {
summary := map[string]any{
"status": "complete",
"retry_scope": "none",
}
root, ok := data.(map[string]any)
if !ok {
return summary
}
completionValue, hasCompletion := root["completion"]
switch completion := completionValue.(type) {
case Completion:
copyCompletionStatus(summary, completion.Status)
summary["requested_count"] = completion.RequestedCount
summary["succeeded_count"] = completion.SucceededCount
summary["failed_count"] = completion.FailedCount
summary["pending_count"] = completion.PendingCount
copyCompletionRetryScope(summary, completion.RetryScope)
return summary
case map[string]any:
if value, ok := completion["status"].(string); ok {
copyCompletionStatus(summary, value)
}
copyCompletionCount(summary, completion, "requested_count")
copyCompletionCount(summary, completion, "succeeded_count")
copyCompletionCount(summary, completion, "failed_count")
copyCompletionCount(summary, completion, "pending_count")
if value, exists := completion["final_state_verified"]; exists {
if verified, valid := value.(bool); valid {
summary["final_state_verified"] = verified
}
}
if value, ok := completion["retry_scope"].(string); ok {
copyCompletionRetryScope(summary, value)
}
}
if !hasCompletion {
if mention, ok := root["mention_result"].(MessageMentionResult); ok {
copyCompletionStatus(summary, mention.Status)
copyCompletionRetryScope(summary, mention.RetryScope)
}
}
return summary
}
func copyCompletionStatus(dst map[string]any, value string) {
switch value {
case "complete", "partial", "accepted_unverified", "partial_unattributed":
dst["status"] = value
}
}
func allowlistedCompletion(data any) map[string]any {
summary := map[string]any{
"status": "accepted_unverified",
"retry_scope": "whole_request",
}
root, ok := data.(map[string]any)
if !ok {
return summary
}
completionValue, hasCompletion := root["completion"]
switch completion := completionValue.(type) {
case Completion:
copyCompletionStatus(summary, completion.Status)
summary["requested_count"] = completion.RequestedCount
summary["succeeded_count"] = completion.SucceededCount
summary["failed_count"] = completion.FailedCount
summary["pending_count"] = completion.PendingCount
copyCompletionRetryScope(summary, completion.RetryScope)
return summary
case map[string]any:
if value, ok := completion["status"].(string); ok {
copyCompletionStatus(summary, value)
}
copyCompletionCount(summary, completion, "requested_count")
copyCompletionCount(summary, completion, "succeeded_count")
copyCompletionCount(summary, completion, "failed_count")
copyCompletionCount(summary, completion, "pending_count")
if value, exists := completion["final_state_verified"]; exists {
if verified, valid := value.(bool); valid {
summary["final_state_verified"] = verified
}
}
if value, ok := completion["retry_scope"].(string); ok {
copyCompletionRetryScope(summary, value)
}
}
if !hasCompletion {
if mention, ok := root["mention_result"].(MessageMentionResult); ok {
copyCompletionStatus(summary, mention.Status)
copyCompletionRetryScope(summary, mention.RetryScope)
}
}
return summary
}
func copyCompletionStatus(dst map[string]any, value string) {
switch value {
case "complete", "partial", "accepted_unverified", "partial_unattributed":
dst["status"] = value
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontract/output_fallback.go` around lines 42 - 92, Update
allowlistedCompletion to use a conservative fallback status instead of seeding
status as "complete"; use the established "accepted_unverified" value (and avoid
implying retry_scope "none" when the source status is unknown). Preserve
copyCompletionStatus behavior for recognized statuses while ensuring
unrecognized completion statuses cannot be reported as complete and bypass
recovery.

Comment on lines +226 to +233
result := map[string]interface{}{
"message_id": resData["message_id"],
"chat_id": resData["chat_id"],
"create_time": common.FormatTimeWithSeconds(resData["create_time"]),
}, nil)
}
if err := addMessageMentionResult(runtime, resData, result); err != nil {
return err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Mention post-processing can report failure for a completed write. Both shortcuts call addMessageMentionResult after the message was accepted by the server, and that function re-parses the mention flags and can return a validation error. The command then exits with an error even though the message was sent. The mention request is already parsed and validated inside buildMessageRequestBody before the write, so the failure is unreachable today; make the post-write path unable to fail instead of relying on that.

  • shortcuts/im/im_messages_send.go#L226-L233: parse the mention request once before the write and pass the resulting mentionRequest into the result builder, so no error can be returned after DoWriteAPIJSONTyped succeeds.
  • shortcuts/im/im_messages_reply.go#L202-L209: apply the same change, reusing the mention request computed before the reply is sent.
📍 Affects 2 files
  • shortcuts/im/im_messages_send.go#L226-L233 (this comment)
  • shortcuts/im/im_messages_reply.go#L202-L209
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/im/im_messages_send.go` around lines 226 - 233, Make mention
post-processing non-failing after a successful write by parsing the mention
request once before the write in shortcuts/im/im_messages_send.go lines 226-233
and passing that mentionRequest into the result builder instead of calling
addMessageMentionResult to re-parse it; apply the same change in
shortcuts/im/im_messages_reply.go lines 202-209, reusing the mention request
computed before sending the reply.

Comment on lines +145 to +146
CHAT_ID=$(lark-cli im +chat-create --name "New Group" --idempotency-key <generated_uuid> --format json | jq -r '.data.chat_id')
lark-cli im +messages-send --chat-id "$CHAT_ID" --text "Welcome, everyone!" --as bot

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 5 'DefaultAs|default.*(identity|as)|im \+chat-create|chat-create' --glob '*.go' --glob '*.md'

Repository: larksuite/cli

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf 'Tracked files matching chat-create/lark-cli references:\n'
git ls-files | rg '(^skills/lark-im/references/lark-im-chat-create\.md|chat-create|lark-cli|cli)' | head -200

printf '\nDocument excerpt:\n'
sed -n '130,155p' skills/lark-im/references/lark-im-chat-create.md

printf '\nBroader search for --as / bot / identity default:\n'
rg -n --glob '*.go' --glob '*.md' '('\''--as'\''|\b--as\b|\bdefault[ _-]?[A-Za-z]*(identity|user|bot)|Default[A-Za-z]*(Identity|User)|chat-create|messages-send|messages-send --as|DefaultAs)' | head -300

Repository: larksuite/cli

Length of output: 10768


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf 'Files containing --as:\n'
rg -n --glob '*.go' --glob '*.md' --glob '*.sh' '\b--as\b' . | head -300
printf '\nFiles containing Default[A-Za-z]+User / Default[A-Za-z]+Identity / Default[A-Za-z]+Bot:\n'
rg -n --glob '*.go' --glob '*.md' 'Default[A-Za-z]+(User|Identity|Bot|As)|default.*as=default|as.*default' . | head -300
printf '\nChat create command implementation candidates:\n'
git ls-files | rg '(^.*chat.*\.go$|chat.*\.go$)' | xargs -r rg -n 'chat-create|ChatCreate|CreateChat|messages-send|MessageSend|\\b-as\\b|--as|Bot|bot' | head -400

Repository: larksuite/cli

Length of output: 174


Make the bot identity explicit in the create command.

Scenario 3 creates the chat without --as, then sends the welcome message with --as bot. Use --as bot for the create command too so the same identity both creates the chat and sends the message.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-im/references/lark-im-chat-create.md` around lines 145 - 146,
Update the chat creation command in Scenario 3 to include the explicit --as bot
option, matching the identity used by the subsequent messages-send command.

If a bot creates a group and `--users` includes users who are mutually invisible to the bot, the entire request fails with 232043. Use two steps instead:

1. Create the group with the bot first, excluding invisible users: `lark-cli im +chat-create --name "Group Name"`
1. Generate a UUID once with a UUID library or tool, then create the group with the bot first, excluding invisible users: `lark-cli im +chat-create --name "Group Name" --idempotency-key <generated_uuid>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Pin bot identity throughout bot workflows.

The examples describe bot actions but do not consistently use bot identity. A default user identity can create or discover a chat that the bot cannot access.

  • skills/lark-im/references/lark-im-chat-identity.md#L43-L43: Add --as bot to the group-creation command.
  • skills/lark-im/references/lark-im-chat-search.md#L107-L107: Add --as bot to the preceding +chat-search command, or run both commands as user.
Proposed fix
-lark-cli im +chat-create --name "Group Name" --idempotency-key <generated_uuid>
+lark-cli im +chat-create --as bot --name "Group Name" --idempotency-key <generated_uuid>

-CHAT_ID=$(lark-cli im +chat-search --query "daily report" --format json | jq -r '.data.chats[0].chat_id')
+CHAT_ID=$(lark-cli im +chat-search --as bot --query "daily report" --format json | jq -r '.data.chats[0].chat_id')
📍 Affects 2 files
  • skills/lark-im/references/lark-im-chat-identity.md#L43-L43 (this comment)
  • skills/lark-im/references/lark-im-chat-search.md#L107-L107
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-im/references/lark-im-chat-identity.md` at line 43, Pin bot
identity consistently in the documented chat workflows: update the
group-creation command in
skills/lark-im/references/lark-im-chat-identity.md:43-43 to run as bot, and
update the preceding +chat-search command in
skills/lark-im/references/lark-im-chat-search.md:107-107 to run as bot as well,
keeping both operations under the same identity.

Comment on lines +9 to +13
Lists the **current user's** feed shortcuts.

- Only **CHAT-type** shortcuts are exposed via OpenAPI today (others in the IDL are not yet whitelisted).
- The shortcut is a **thin one-page wrapper** — there is no built-in auto-pagination. Callers drive their own loop when they actually need to paginate.
- Server-side page size is controlled by the service; in normal use one page usually covers the list.
- Pagination tokens are opaque. If a token is rejected because the shortcut list changed, restart by omitting `--page-token`.
- Pagination controls are defined by the concrete command's `--help`.
- The default call is bounded. For an exhaustive task, inspect the leaf help and require `meta.complete=true` before reporting a complete list.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make the exhaustive-read contract explicit in every paginated IM reference.

The documentation either requires completion metadata without documenting its location or describes page merging without stating the completion gate. A bounded result can therefore be mistaken for a complete result.

  • skills/lark-im/references/lark-im-feed-shortcut-list.md#L9-L13: document meta.complete in the response structure and JSON example.
  • skills/lark-im/references/lark-im-feed-group-list.md#L3-L5: describe merging as covering fetched pages and require meta.complete=true for an exhaustive list.
  • skills/lark-im/references/lark-im-feed-group-list.md#L26-L37: document the completion check beside pagination controls and output.
  • skills/lark-im/references/lark-im-feed-group-list-item.md#L31-L39: document meta.complete=true and distinguish merged pages from a complete dataset.
  • skills/lark-im/references/lark-im-feed-groups.md#L33-L33: add the completion condition to the shortcut overview.
  • skills/lark-im/references/lark-im-feed-groups.md#L245-L245: add the completion condition to list.
  • skills/lark-im/references/lark-im-feed-groups.md#L329-L329: add the completion condition to list_item.
  • skills/lark-im/references/lark-im-flag-list.md#L9-L9: replace “result reports complete” with the exact meta.complete=true check and document its output path.
📍 Affects 5 files
  • skills/lark-im/references/lark-im-feed-shortcut-list.md#L9-L13 (this comment)
  • skills/lark-im/references/lark-im-feed-group-list.md#L3-L5
  • skills/lark-im/references/lark-im-feed-group-list.md#L26-L37
  • skills/lark-im/references/lark-im-feed-group-list-item.md#L31-L39
  • skills/lark-im/references/lark-im-feed-groups.md#L33-L33
  • skills/lark-im/references/lark-im-feed-groups.md#L245-L245
  • skills/lark-im/references/lark-im-feed-groups.md#L329-L329
  • skills/lark-im/references/lark-im-flag-list.md#L9-L9
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-im/references/lark-im-feed-shortcut-list.md` around lines 9 - 13,
Make the exhaustive-read contract explicit across all listed references: in
skills/lark-im/references/lark-im-feed-shortcut-list.md lines 9-13, document
meta.complete in the response structure and JSON example; in
skills/lark-im/references/lark-im-feed-group-list.md lines 3-5 and 26-37, state
that merging covers fetched pages only and require meta.complete=true beside
pagination and output guidance; in
skills/lark-im/references/lark-im-feed-group-list-item.md lines 31-39,
distinguish merged pages from a complete dataset and document the
meta.complete=true gate; in skills/lark-im/references/lark-im-feed-groups.md
lines 33, 245, and 329, add the same completion condition to the overview, list,
and list_item documentation; and in
skills/lark-im/references/lark-im-flag-list.md line 9, replace the vague
completion wording with the exact meta.complete=true check and its output path.

- `--content` must be valid JSON
- When using `--content`, you are responsible for making the JSON structure match the effective `msg_type`
- `--image`/`--file`/`--video`/`--audio` support existing keys, URLs, and cwd-relative local file paths; the shortcut uploads local paths and URLs first, then sends the message; both the upload and send steps use the same identity (UAT when `--as user`, TAT when `--as bot`)
- If an upload fails (URL media or a markdown image), **nothing is sent** — the command fails with a recovery hint. The CLI never downgrades content on its own (e.g. replacing a failed image with a text link); any degraded form must be shown to the user and re-sent explicitly after their approval

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use one remote Markdown-image failure contract across both references.

Both references say that a failed remote image upload sends nothing, while their earlier rules say that the image is removed and the message continues with a warning. Align all statements with the CLI behavior to prevent duplicate sends.

  • skills/lark-im/references/lark-im-messages-send.md#L267-L267: align Line 267 with Lines 72 and 277, and document the selected behavior consistently.
  • skills/lark-im/references/lark-im-messages-reply.md#L267-L267: align Line 267 with Lines 73 and 276, and document the selected behavior consistently.
📍 Affects 2 files
  • skills/lark-im/references/lark-im-messages-send.md#L267-L267 (this comment)
  • skills/lark-im/references/lark-im-messages-reply.md#L267-L267
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-im/references/lark-im-messages-send.md` at line 267, Align the
remote Markdown-image upload failure contract in both
skills/lark-im/references/lark-im-messages-send.md:267 and
skills/lark-im/references/lark-im-messages-reply.md:267 with the existing rules
at Lines 72/277 and 73/276: remove the failed image, continue sending the
message, and issue the documented warning. Ensure both references describe the
same behavior and do not claim that nothing is sent or that the command fails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/im PR touches the im domain feature size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant