Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
7cc7fcf
feat(im): add copyable Tips examples to high-frequency IM shortcuts
luozhixiong01 Jul 9, 2026
aa39825
feat(im): add raw API affordance guidance for the im domain
luozhixiong01 Jul 9, 2026
4de60dd
fix(im): seed failure inventory with recovery hint fix and coverage u…
luozhixiong01 Jul 9, 2026
392c64e
docs(im): relax send confirmation gate for fully specified requests
luozhixiong01 Jul 9, 2026
5d61ecf
feat(im): extend Tips examples to feed and flag shortcuts
luozhixiong01 Jul 13, 2026
9279155
fix(im): add id-source hint and strengthen example and recovery locks
luozhixiong01 Jul 13, 2026
9c3dca2
docs(im): require draft approval when message content is delegated
luozhixiong01 Jul 13, 2026
b90c1a9
docs(im): surface sending approval semantics in the domain skill
luozhixiong01 Jul 13, 2026
c12df3e
docs(im): cover reply target in domain approval rule and tighten hint…
luozhixiong01 Jul 13, 2026
38a68a9
docs(im): forbid downgrading reply intent to a new direct message
luozhixiong01 Jul 13, 2026
2ddf2b4
test(im): drop redundant loop variable copy and type error assertions
luozhixiong01 Jul 14, 2026
43b75d2
docs(im): mention the user-id direct message form for plain text sends
luozhixiong01 Jul 14, 2026
2bbbfc6
fix(im): fail closed on media upload errors instead of rewriting content
luozhixiong01 Jul 17, 2026
6288d72
docs(im): extend approval semantics to all outbound actions and card …
luozhixiong01 Jul 17, 2026
b61ba28
test(im): lock example identity, run all examples, guard affordance d…
luozhixiong01 Jul 17, 2026
bcef511
test(im): route config secret through printf placeholder for content …
luozhixiong01 Jul 17, 2026
7319064
test(im): use the scanner-recognized test-secret placeholder inline
luozhixiong01 Jul 17, 2026
9ede9ae
fix(im): pin explicit identity on user-only examples and run them ver…
luozhixiong01 Jul 17, 2026
d57a86c
docs(im): pin explicit identity on every outbound example command
luozhixiong01 Jul 17, 2026
4344433
docs(im): pin bot identity on image pre-upload examples
luozhixiong01 Jul 17, 2026
fc30060
feat(im): enforce trustworthy completion for IM writes
luozhixiong01 Jul 27, 2026
d639a39
feat(im): expose trustworthy completeness for IM reads
luozhixiong01 Jul 27, 2026
fd6b8d3
chore(im): align help skills and contract coverage
luozhixiong01 Jul 27, 2026
b133051
fix(im): keep contract exemptions public-safe
luozhixiong01 Jul 30, 2026
3d9bb90
refactor(im): make acceptance-only a first-class contract
luozhixiong01 Jul 30, 2026
dd6e8ca
fix(im): close completion contract gaps
luozhixiong01 Jul 28, 2026
db2d687
feat(im): require idempotent chat creation
luozhixiong01 Jul 28, 2026
f195175
feat(im): add structured message mentions
luozhixiong01 Jul 28, 2026
96363db
fix(im): close read completeness gaps
luozhixiong01 Jul 28, 2026
89f88a0
chore(im): align agent guidance and coverage
luozhixiong01 Jul 28, 2026
4e5dcbc
fix(im): make agent guidance task-scoped
luozhixiong01 Jul 30, 2026
eba176e
docs(im): state conservative mention evidence
luozhixiong01 Jul 30, 2026
79bae0e
test(im): align acceptance contract coverage
luozhixiong01 Jul 30, 2026
cbca004
fix(im): close completion contract review gaps
luozhixiong01 Jul 30, 2026
b6c3495
fix(im): expose defaulted identity and align help layout
luozhixiong01 Jul 31, 2026
ac8c577
fix(im): refine identity and mention help signals
luozhixiong01 Jul 31, 2026
bfdd978
fix(im): remove superseded helpers
luozhixiong01 Jul 31, 2026
928a3ca
chore(im): trim review-only artifacts
luozhixiong01 Jul 31, 2026
de3c7ba
test(im): use approved fixture domain
luozhixiong01 Jul 31, 2026
348ed2c
test(im): resolve mention example placeholder
luozhixiong01 Aug 1, 2026
67d71b5
test(contact): skip empty live tenant fixture
luozhixiong01 Aug 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
425 changes: 425 additions & 0 deletions affordance/im.md

Large diffs are not rendered by default.

37 changes: 21 additions & 16 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -674,8 +674,8 @@ func installTipsHelpFunc(root *cobra.Command) {
}
}
// Domain and method commands compose their agent guidance into Long lazily
// here (shortcuts attach after service registration); both skip the generic
// bottom-of-help append below.
// here and own their complete layout. Shortcuts compose only affordance and
// contract guidance; Risk/Tips still use the common tail below.
if service.PrepareDomainHelp(cmd, embeddedSkillContent) {
defaultHelp(cmd, args)
return
Expand All @@ -686,22 +686,27 @@ func installTipsHelpFunc(root *cobra.Command) {
}
if service.PrepareShortcutHelp(cmd, embeddedSkillContent) {
defaultHelp(cmd, args)
appendRiskTipsHelp(cmd)
return
}
defaultHelp(cmd, args)
out := cmd.OutOrStdout()
if level, ok := cmdutil.GetRisk(cmd); ok {
fmt.Fprintln(out)
fmt.Fprintln(out, "Risk:", level)
}
tips := cmdutil.GetTips(cmd)
if len(tips) == 0 {
return
}
fmt.Fprintln(out)
fmt.Fprintln(out, "Tips:")
for _, tip := range tips {
fmt.Fprintf(out, " • %s\n", tip)
}
appendRiskTipsHelp(cmd)
})
}

func appendRiskTipsHelp(cmd *cobra.Command) {
out := cmd.OutOrStdout()
if level, ok := cmdutil.GetRisk(cmd); ok {
fmt.Fprintln(out)
fmt.Fprintln(out, cmdutil.RiskHelpText(level))
}
tips := cmdutil.GetTips(cmd)
if len(tips) == 0 {
return
}
fmt.Fprintln(out)
fmt.Fprintln(out, "Tips:")
for _, tip := range tips {
fmt.Fprintf(out, " • %s\n", tip)
}
}
8 changes: 6 additions & 2 deletions cmd/root_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,9 @@ func TestIntegration_StrictModeUser_ProfileOverride_ChatCreateDryRunSucceeds(t *
rootCmd := buildStrictModeIntegrationRootCmd(t, f)

code := executeRootIntegration(t, f, rootCmd, []string{
"im", "+chat-create", "--name", "probe", "--dry-run",
"im", "+chat-create", "--name", "probe",
"--idempotency-key", "test-secret",
"--dry-run",
})

if code != 0 {
Expand All @@ -356,7 +358,9 @@ func TestIntegration_StrictModeUser_ProfileOverride_ShortcutExplicitBotReturnsEn
rootCmd := buildStrictModeIntegrationRootCmd(t, f)

code := executeRootIntegration(t, f, rootCmd, []string{
"im", "+chat-create", "--name", "probe", "--as", "bot", "--dry-run",
"im", "+chat-create", "--name", "probe",
"--idempotency-key", "test-secret",
"--as", "bot", "--dry-run",
})

if code != output.ExitValidation {
Expand Down
42 changes: 42 additions & 0 deletions cmd/root_risk_help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"strings"
"testing"

"github.com/larksuite/cli/internal/cmdmeta"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/imcontract"
"github.com/spf13/cobra"
)

Expand All @@ -34,6 +36,10 @@ func TestHelpFunc_RendersRiskLineWhenAnnotated(t *testing.T) {
if !strings.Contains(out, "Risk: high-risk-write") {
t.Errorf("expected Risk line in help output, got:\n%s", out)
}
if !strings.Contains(out, "requires explicit user confirmation") ||
!strings.Contains(out, "agent must NOT add --yes") {
t.Errorf("high-risk tail lost its confirmation guard:\n%s", out)
}
}

func TestHelpFunc_NoRiskLineWhenUnannotated(t *testing.T) {
Expand Down Expand Up @@ -68,3 +74,39 @@ func TestHelpFunc_RiskLinePrecedesTips(t *testing.T) {
t.Errorf("expected Risk to precede Tips; got Risk@%d, Tips@%d", riskIdx, tipsIdx)
}
}

func TestHelpFunc_PreparedShortcutKeepsContractAndMovesRiskTipsToTail(t *testing.T) {
root := &cobra.Command{Use: "lark-cli"}
installTipsHelpFunc(root)

child := &cobra.Command{
Use: "+chat-list",
Short: "List chats",
Run: func(*cobra.Command, []string) {},
}
cmdmeta.SetSource(child, cmdmeta.SourceShortcut, false)
cmdmeta.SetAffordanceRef(child, "im", "+chat-list")
cmdutil.SetRisk(child, "read")
cmdutil.SetTips(child, []string{"use exhaustive pagination when completeness matters"})
imcontract.AnnotateHelpContract(child, "im +chat-list")
root.AddCommand(child)

out := rendersHelp(t, child)
usageIdx := strings.Index(out, "Usage:")
riskIdx := strings.Index(out, "Risk:")
tipsIdx := strings.Index(out, "Tips:")
if usageIdx == -1 || riskIdx == -1 || tipsIdx == -1 {
t.Fatalf("expected Usage, Risk, and Tips in prepared shortcut help:\n%s", out)
}
if !(usageIdx < riskIdx && riskIdx < tipsIdx) {
t.Fatalf("expected Usage < Risk < Tips; got Usage@%d Risk@%d Tips@%d:\n%s", usageIdx, riskIdx, tipsIdx, out)
}
for _, want := range []string{
imcontract.HelpCompleteness.Text(),
"use exhaustive pagination when completeness matters",
} {
if n := strings.Count(out, want); n != 1 {
t.Fatalf("%q appears %d times, want once:\n%s", want, n, out)
}
}
}
57 changes: 35 additions & 22 deletions cmd/service/affordance.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/larksuite/cli/internal/affordance"
"github.com/larksuite/cli/internal/cmdmeta"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/imcontract"
"github.com/larksuite/cli/internal/meta"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -161,6 +162,7 @@ func PrepareMethodHelp(cmd *cobra.Command, skillFS fs.FS) bool {
}
}

writeContractHelp(&b, cmd)
fmt.Fprintf(&b, "\n\nFull parameter schema:\n lark-cli schema %s", schemaPath)
b.WriteString(ann[paramsOnlyAnnotation])

Expand All @@ -171,11 +173,11 @@ func PrepareMethodHelp(cmd *cobra.Command, skillFS fs.FS) bool {
}

// PrepareShortcutHelp composes a +-prefixed shortcut's Long from its affordance
// overlay — the same top layout as method help (description, Risk, guidance
// block, related skills) minus the schema pointer, which shortcuts have none
// of. Returns false when the command is not a shortcut or carries no overlay
// entry, so shortcuts without guidance keep the default help plus the bottom
// risk/tips append.
// overlay and contract help. Risk and Tips are deliberately not rendered into
// Long: the root help renderer appends them after Usage/Flags for every
// shortcut, so contract-bearing and ordinary shortcuts keep one layout.
// Returns false when the command is not a shortcut or carries neither an
// overlay nor contract help.
//
// The lead is the command's pristine base (captureHelpBase): a shortcut that
// set a hand-authored Long in PostMount (e.g. the docs shortcuts' "agents MUST
Expand All @@ -184,51 +186,62 @@ func PrepareMethodHelp(cmd *cobra.Command, skillFS fs.FS) bool {
//
// Tips precedence (intentional, not a bug): the overlay's ### Tips win. The
// shortcut's declarative Tips (the Go Tips field) are only a fallback used when
// the overlay declares none; when the overlay has tips, the Go tips are dropped
// (replaced, not merged) so tips never render twice. Authoring a ### Tips block
// therefore silently retires that shortcut's Go Tips — consolidate into one.
// the overlay declares none. The selected list is stored back on the command
// and removed from the affordance block so the root renderer emits it once.
func PrepareShortcutHelp(cmd *cobra.Command, skillFS fs.FS) bool {
if src, _ := cmdmeta.SourceOf(cmd); src != cmdmeta.SourceShortcut {
return false
}
raw, ok := affordanceRaw(cmd)
if !ok {
return false
var a meta.Affordance
hasAffordance := false
if raw, ok := affordanceRaw(cmd); ok {
if parsed, parsedOK := (meta.Method{Affordance: raw}).ParsedAffordance(); parsedOK {
a = parsed
hasAffordance = true
}
}
a, ok := (meta.Method{Affordance: raw}).ParsedAffordance()
if !ok {
contractHelp := imcontract.HelpText(cmd)
if !hasAffordance && contractHelp == "" {
return false
}
if len(a.Tips) == 0 {
a.Tips = cmdutil.GetTips(cmd)
tips := a.Tips
if len(tips) == 0 {
tips = cmdutil.GetTips(cmd)
}
cmdutil.SetTips(cmd, tips)
a.Tips = nil

var b strings.Builder
b.WriteString(captureHelpBase(cmd, shortcutBaseAnnotation))
writeRisk(&b, cmd)
if block := renderAffordanceValue(a); block != "" {
b.WriteString("\n\n")
b.WriteString(block)
}
if contractHelp != "" {
b.WriteString("\n\n")
b.WriteString(contractHelp)
}
writeRelatedSkills(&b, a.Skills, skillFS)

cmd.Long = b.String()
return true
}

func writeContractHelp(b *strings.Builder, cmd *cobra.Command) {
if text := imcontract.HelpText(cmd); text != "" {
b.WriteString("\n\n")
b.WriteString(text)
}
}

// writeRisk appends the "Risk: <level>" line, warning agents not to self-approve
// high-risk-write commands. A no-op when the command has no risk annotation.
func writeRisk(b *strings.Builder, cmd *cobra.Command) {
level, ok := cmdutil.GetRisk(cmd)
if !ok {
return
}
// --yes asserts the USER confirmed; the agent must not self-approve.
if level == cmdutil.RiskHighRiskWrite {
fmt.Fprintf(b, "\n\nRisk: %s (requires explicit user confirmation to execute; the agent must NOT add --yes on its own — only pass --yes after the user has confirmed)", level)
} else {
fmt.Fprintf(b, "\n\nRisk: %s", level)
}
fmt.Fprintf(b, "\n\n%s", cmdutil.RiskHelpText(level))
}

// writeRelatedSkills appends the "Related skills" block for the entries that
Expand Down
Loading
Loading