Skip to content

Commit 6a1287b

Browse files
author
SqlRush
committed
Wire bridge-safe local slash commands
1 parent ee84e5d commit 6a1287b

9 files changed

Lines changed: 352 additions & 1 deletion

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535

3636
M4 补充:tool executor 会围绕 `PreToolUse`、`PostToolUse`、`PermissionDenied` 和 `PermissionRequest` hook 发出 `hook_started`、`hook_completed`、`hook_failed`、`hook_blocked` 进度事件,携带 phase/tool/hook_index 以及阻断、错误、权限行为和 input 更新摘要;`PermissionAsk` 现在走独立 `PermissionRequest` phase 并发出 `permission_requested` 进度,conversation runner 已通过现有 tool progress 通道透出这些事件。settings/local-plugin command/HTTP hook 主路径已接入:支持 matcher/`if` 过滤、JSON stdin/body、stdout/HTTP body JSON `hookSpecificOutput.updatedInput`、exit 2/block、HTTP URL allowlist、HTTP header env allowlist 插值和 `PermissionRequest` allow/deny;conversation runner 也已接入 `UserPromptSubmit`、`Stop`、`SubagentStop` 和 `PreCompact` 同步 hook,支持 prompt 追加上下文、stop/subagent stop progress、compact summary 追加指令和阻断错误;command hook 支持显式 `runInBackground`/`run_in_background` 异步启动并回收进程;headless `--output-format stream-json` 已把 tool/hook progress 作为 `tool_progress` NDJSON 事件暴露。完整 TUI/control-protocol hook surface、更丰富 hook telemetry 和更深后台运行策略仍未完成。
3737

38+
M4/M11 补充:bridge-safe 内置本地命令 `/summary``/release-notes``/files` 已注册并接入 no-query runner 路径;`/summary` 输出本地会话/历史摘要,`/files` 只读列出当前工作目录第一层条目,`/release-notes` 报告当前 Go runtime 未打包 release notes。完整 local-jsx UI surface 和其它本地命令 parity 仍未完成。
39+
3840
M10 补充:plugin command/agent 的 allowed tool frontmatter 解析现在只在顶层逗号或空白处分隔,保留括号、方括号和引号内的逗号/空白,避免 `Bash(git commit -m "x,y")` 这类 tool pattern 被误拆。
3941

4042
M8 补充:CLI `plugin marketplace list` 现在可列出 settings 中已配置的 marketplace;`--json` 输出按名称排序的 source/repo/url/path/package/installLocation 结构,普通文本输出与无配置提示也已覆盖。CLI `plugin marketplace add [--scope user|project|local] [--type ...] <name> <source>` 和 `plugin marketplace remove [--scope user|project|local] <name>` 现在复用 `internal/config` settings 文件写入 helper,可按目标 scope 写入/删除 `extraKnownMarketplaces`,并在写入前复用 marketplace source validation,且 `installLocation` 会校验为 `user|project|local`。CLI `plugin marketplace update [name]` 现在可按全部或指定 marketplace 触发现有 URL/git/github/npm/settings cache 加载刷新路径,命名 update 会用轻量 settings 读取避免启动时提前刷新全部 marketplace。运行时插件发现现在会合并项目链 `.claude/plugins` 和用户级 `${CLAUDE_CONFIG_DIR}/plugins`,项目同名插件优先。CLI `plugin install --scope project|user|local <plugin>` 与 headless `/plugin install [--scope project|user|local] <plugin>` 现在复用 `internal/plugins` 共享安装 API,把 marketplace 插件复制到目标 scope 的 `plugins/<safe-name>`,未显式传 scope 时会尊重 marketplace `installLocation` 默认值,并保留冲突检测、重复安装识别和 symlink/non-regular file 拒绝。CLI `plugin update --scope project|user|local|all <plugin>` 与 headless `/plugin update [--scope project|user|local|all] [plugin]` 现在复用 `internal/plugins` 共享更新 API,把目标 scope 已安装同名插件替换为最新 marketplace 副本,未显式传 scope 的命名更新同样会尊重 marketplace `installLocation`。CLI `plugin enable|disable [--scope user|project|local] <plugin>` 与 `plugin disable --all --scope ...` 现在复用 `internal/config` settings 文件写入 helper,按目标 scope 更新 `enabledPlugins` 并覆盖基本参数冲突。

docs/claude-code-go-rewrite-plan.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,8 @@ test/parity/ # golden tests against TS/official behavior
878878

879879
- 本轮补充:`/native clipboard status|show` 现在走 no-query 本地状态路径,只展示 session clipboard 文件路径、item 数、更新时间和 adapter 可用性/命令,不读取、不写入、不展示剪贴板文本内容;`/native clipboard` usage 同步暴露 status 子命令。
880880

881+
- 本轮补充:bridge-safe 内置本地命令 `/summary``/release-notes``/files` 现在已注册并接入 no-query runner 路径;`/summary` 输出确定性的会话/历史消息计数、工具使用计数、估算 token 和最近用户/助手预览,`/files` 只读列出当前工作目录第一层条目而不读取文件内容,`/release-notes` 明确报告当前 Go runtime 未打包 release notes。完整 local-jsx UI surface 和其它本地命令 parity 仍需继续补。
882+
881883
### M12: Parity hardening
882884

883885
产出:

docs/first-second-parity-audit.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Anthropic API 和 conversation:
7979
- Slash command execution now has a basic local command result abstraction, and `/clear` follows a no-query local text-result path instead of returning the generic unsupported local-command stderr; full local command result handling and REPL state reset remain incomplete.
8080
- `/status <section>` now shares the same section-alias normalization as `/status show <section>`, so direct invocations such as `/status integrations` and `/status diagnostics` take the no-query local status path instead of falling into the unsupported-subcommand branch.
8181
- `/cost status`, `/cost current`, and `/cost usage` now share the no-query totals path with `/cost`, while `/cost breakdown`/`show` continue to return per-message usage details and unknown subcommands remain explicit.
82+
- `/summary`, `/release-notes`, and `/files` are now registered as bridge-safe built-in local commands and take no-query local result paths: `/summary` reports deterministic session/history counts and previews, `/release-notes` reports the bundled release-notes availability state, and `/files` lists first-level workspace entries without reading file contents.
8283
- `/config <section>` now shares the same section-alias normalization as `/config show <section>` for non-mutating direct section invocations such as `/config env`, `/config permissions`, and `/config integrations`, while preserving existing mutating forms such as `/config model <name>` and `/config output-style <name>`.
8384
- `/memory list`, `/memory files`, and `/memory ls` now take the no-query local file-list path used by `/memory show`, while `/memory status` continues to report the session/relevant-memory summary.
8485
- `/native clipboard status|show` now takes a no-query local status path that reports the session clipboard state and adapter availability without reading, writing, or displaying clipboard text.
@@ -104,7 +105,7 @@ The following items remain incomplete and must not be treated as done:
104105
- Full permission hook flow gaps that remain: deeper async/background hook runtime policy beyond explicit background command hooks, richer hook telemetry, and full TUI/control-protocol surfacing beyond the current executor/conversation hook phase progress and headless stream-json events.
105106
- Auto mode / YOLO classifier: transcript construction, two-stage classifier, XML/tool-use parsing, prompt dump, denial circuit breaker, model gating, and fallback behavior.
106107
- Interactive permission prompt flow: REPL dialogs, bridge/channel/swarm permission relays, user feedback images, prompt race handling, cancellation.
107-
- Full filesystem permission parity gaps that remain: skill-scope allow suggestions, complete local/local-jsx slash-command execution wiring, command permission UI/SDK surfacing, forked/remote/MCP/plugin/bundled SkillTool behavior, skill prompt shell injection, complete auto-memory override policy, and deeper platform-specific Windows/WSL bypass handling.
108+
- Full filesystem permission parity gaps that remain: skill-scope allow suggestions, remaining local/local-jsx slash-command execution wiring beyond the currently covered no-query builtins, command permission UI/SDK surfacing, forked/remote/MCP/plugin/bundled SkillTool behavior, skill prompt shell injection, complete auto-memory override policy, and deeper platform-specific Windows/WSL bypass handling.
108109
- Full tool execution parity gaps that remain: complete hook runtime policy beyond the current settings command hook path, MCP elicitation, complete SDK control event surface beyond current progress NDJSON, mid-call cancellation for concrete tools, background task behavior, telemetry, schema-not-sent hints, and concrete tool-specific semantics.
109110
- Complete Anthropic API parity gaps that remain: dynamic beta-header latching for any remaining provider-specific feature gates beyond prompt-cache/cache-editing/strict-output/1M-context requests, ant-only dump gating and remote `/issue` submission integration beyond the current local context summary, full official cost tracker/session restore lifecycle beyond current transcript usage restore, post-handshake streaming failure semantics, deeper gateway/proxy auth and routing behavior beyond static custom headers, first-party/Bedrock/Vertex/Foundry client setup, deeper OAuth account/profile lifecycle beyond request-time refresh retry, fast-mode retry/cooldown semantics, persistent unattended retry heartbeats, full prompt-cache editing lifecycle, and provider-specific cache behavior.
110111
- Full conversation/query loop: stop hooks, compact/auto-compact, token budget escalation, resume, SDK JSON/NDJSON control events, status updates, rate-limit handling, model switch breadcrumbs, side questions.

internal/commands/registry.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,9 @@ func BuiltinCommands() []contracts.Command {
296296
{Type: contracts.CommandLocal, Name: "clear", Aliases: []string{"reset", "new"}, Description: "Clear conversation history and free up context", Source: contracts.CommandSourceBuiltin, SupportsNonInteractive: true},
297297
{Type: contracts.CommandLocal, Name: "compact", Description: "Compact the current conversation", Source: contracts.CommandSourceBuiltin, SupportsNonInteractive: true},
298298
{Type: contracts.CommandLocal, Name: "cost", Description: "Show the total cost and duration of the current session", Source: contracts.CommandSourceBuiltin, SupportsNonInteractive: true},
299+
{Type: contracts.CommandLocal, Name: "summary", Description: "Show a local conversation summary", Source: contracts.CommandSourceBuiltin, SupportsNonInteractive: true},
300+
{Type: contracts.CommandLocal, Name: "release-notes", Description: "Show bundled release notes status", Source: contracts.CommandSourceBuiltin, SupportsNonInteractive: true},
301+
{Type: contracts.CommandLocal, Name: "files", Description: "Show local workspace files", Source: contracts.CommandSourceBuiltin, SupportsNonInteractive: true},
299302
{Type: contracts.CommandLocal, Name: "issue", Description: "Prepare a local issue report context", ArgumentHint: "[description]", Source: contracts.CommandSourceBuiltin, SupportsNonInteractive: true},
300303
{Type: contracts.CommandLocalJSX, Name: "status", Description: "Show Claude Code status including version, model, account, API connectivity, and tool statuses", Source: contracts.CommandSourceBuiltin, Immediate: true},
301304
{Type: contracts.CommandLocalJSX, Name: "model", Description: "Set the AI model for Claude Code", ArgumentHint: "[model]", Source: contracts.CommandSourceBuiltin, Immediate: true},

internal/commands/registry_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ func TestBuiltinCommandsExposeOfficialAliasesAndMetadata(t *testing.T) {
213213
if !ok || issue.Type != contracts.CommandLocal || !issue.SupportsNonInteractive || issue.ArgumentHint != "[description]" {
214214
t.Fatalf("issue metadata = %#v, %v", issue, ok)
215215
}
216+
for _, name := range []string{"summary", "release-notes", "files"} {
217+
cmd, ok := registry.Find(name)
218+
if !ok || cmd.Type != contracts.CommandLocal || !cmd.SupportsNonInteractive {
219+
t.Fatalf("%s metadata = %#v, %v", name, cmd, ok)
220+
}
221+
}
216222
}
217223

218224
func TestFromSourcesUsesCommandOrderAndDedupesDynamicSkills(t *testing.T) {
@@ -613,6 +619,11 @@ func TestIsBridgeSafeCommand(t *testing.T) {
613619
if !IsBridgeSafeCommand(contracts.Command{Name: "compact", Type: contracts.CommandLocal}) {
614620
t.Fatalf("compact should be bridge-safe")
615621
}
622+
for _, name := range []string{"summary", "release-notes", "files"} {
623+
if !IsBridgeSafeCommand(contracts.Command{Name: name, Type: contracts.CommandLocal}) {
624+
t.Fatalf("%s should be bridge-safe", name)
625+
}
626+
}
616627
if IsBridgeSafeCommand(contracts.Command{Name: "model", Type: contracts.CommandLocal}) {
617628
t.Fatalf("model should not be bridge-safe without explicit allowlist")
618629
}

internal/commands/slash.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ const (
4949
LocalCommandResultClear LocalCommandResultType = "clear"
5050
LocalCommandResultCompact LocalCommandResultType = "compact"
5151
LocalCommandResultCost LocalCommandResultType = "cost"
52+
LocalCommandResultSummary LocalCommandResultType = "summary"
53+
LocalCommandResultRelease LocalCommandResultType = "release-notes"
54+
LocalCommandResultFiles LocalCommandResultType = "files"
5255
LocalCommandResultIssue LocalCommandResultType = "issue"
5356
LocalCommandResultStatus LocalCommandResultType = "status"
5457
LocalCommandResultModel LocalCommandResultType = "model"
@@ -209,6 +212,12 @@ func ExecuteBuiltinLocalCommand(registry Registry, cmd contracts.Command, args s
209212
return LocalCommandResult{Type: LocalCommandResultCompact, Value: strings.TrimSpace(args)}, true
210213
case "cost":
211214
return LocalCommandResult{Type: LocalCommandResultCost, Value: strings.TrimSpace(args)}, true
215+
case "summary":
216+
return LocalCommandResult{Type: LocalCommandResultSummary, Value: strings.TrimSpace(args)}, true
217+
case "release-notes":
218+
return LocalCommandResult{Type: LocalCommandResultRelease, Value: strings.TrimSpace(args)}, true
219+
case "files":
220+
return LocalCommandResult{Type: LocalCommandResultFiles, Value: strings.TrimSpace(args)}, true
212221
case "issue":
213222
return LocalCommandResult{Type: LocalCommandResultIssue, Value: strings.TrimSpace(args)}, true
214223
case "status":

internal/commands/slash_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,39 @@ func TestExecuteSlashCostReturnsLocalCostResult(t *testing.T) {
520520
}
521521
}
522522

523+
func TestExecuteSlashBridgeSafeLocalCommandsReturnLocalResults(t *testing.T) {
524+
registry := FromSources(Sources{Builtins: BuiltinCommands()})
525+
tests := []struct {
526+
input string
527+
typ LocalCommandResultType
528+
value string
529+
}{
530+
{input: "/summary recent work", typ: LocalCommandResultSummary, value: "recent work"},
531+
{input: "/release-notes plugins", typ: LocalCommandResultRelease, value: "plugins"},
532+
{input: "/files src", typ: LocalCommandResultFiles, value: "src"},
533+
}
534+
for _, tt := range tests {
535+
t.Run(tt.input, func(t *testing.T) {
536+
result, handled, err := ExecuteSlashCommand(registry, tt.input, SlashOptions{UUID: "user_local"})
537+
if err != nil {
538+
t.Fatal(err)
539+
}
540+
if !handled || result.ShouldQuery || result.Unsupported || result.LocalResult == nil {
541+
t.Fatalf("handled=%v result=%#v", handled, result)
542+
}
543+
if result.LocalResult.Type != tt.typ || result.LocalResult.Value != tt.value {
544+
t.Fatalf("local result = %#v", result.LocalResult)
545+
}
546+
if len(result.Messages) != 1 || result.Messages[0].UUID != "user_local" {
547+
t.Fatalf("messages = %#v", result.Messages)
548+
}
549+
if text := result.Messages[0].Content[0].Text; !strings.Contains(text, "<command-name>/"+strings.TrimPrefix(strings.Fields(tt.input)[0], "/")+"</command-name>") {
550+
t.Fatalf("command message = %q", text)
551+
}
552+
})
553+
}
554+
}
555+
523556
func TestExecuteSlashIssueReturnsLocalIssueResult(t *testing.T) {
524557
registry := FromSources(Sources{Builtins: BuiltinCommands()})
525558
result, handled, err := ExecuteSlashCommand(registry, "/issue auth failed", SlashOptions{UUID: "user_issue"})

0 commit comments

Comments
 (0)