Skip to content

Commit ac0c53d

Browse files
author
SqlRush
committed
Write local issue report bundles
1 parent f574a58 commit ac0c53d

3 files changed

Lines changed: 89 additions & 6 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ test/parity/ # golden tests against TS/official behavior
136136
- 本轮补充:动态 beta header 推导继续覆盖 strict tool structured-output 请求和 `[1m]` context model 请求;当 request tools 中存在 `strict:true` 时自动追加 structured-outputs beta,当 request model 以 `[1m]` 结尾时自动追加 context-1m beta,并继续和显式 beta 去重合并。
137137
- 本轮补充:Anthropic client 新增可选 access-token provider,非 streaming 与 streaming request 在 401/403 authentication/permission error 时会强制 refresh OAuth access token 并重试一次当前 payload;headless CLI 的 OAuth credentials 路径会把 file/env refresh token provider 传入 client,刷新后复用既有 credential store 持久化。
138138
- 本轮补充:conversation runner 的 streaming request 现在在未收到任何 stream event 前失败时,会用同一请求安全回退到 non-streaming `CreateMessage`;一旦已有 stream event 对外发出,错误会原样返回,避免重复生成半截输出。
139-
- 本轮补充:新增 no-query `/issue [description]` 本地命令入口,Anthropic client 暴露只读 prompt dump cache 摘要接口;runner 会输出 session/cwd/model、dump path 和最近请求的结构化摘要/hash,不输出 prompt 正文、system 内容或 tool schema 名称,作为后续远端 issue 提交集成的安全上下文基础
139+
- 本轮补充:新增 no-query `/issue [description]` 本地命令入口,Anthropic client 暴露只读 prompt dump cache 摘要接口;runner 会输出 session/cwd/model、dump path 和最近请求的结构化摘要/hash,不输出 prompt 正文、system 内容或 tool schema 名称,并在 session-scoped `issue-report.json` 写入同一份脱敏 issue bundle,供外部 tracker/远端集成直接提交
140140
- 本轮补充:`/cost` 本地命令现在会在 runner 带有 `SessionPath` 时从当前 session transcript 回填历史 usage,并按 message UUID/ID 与内存 history 去重,避免 resume 或新进程场景丢失/重复计算历史 cost。
141141
- 本轮补充:Anthropic CLI/env client 初始化现在支持 `ANTHROPIC_CUSTOM_HEADERS``CLAUDE_CODE_CUSTOM_HEADERS`,可用 JSON object 或 `Header: value`/`Header=value` 行格式注入 gateway/proxy 静态请求头;配置错误会在初始化阶段报错,避免静默遗漏代理必需 header。
142142
- 本轮补充:settings loader 现在会按官方优先级读取 managed policy:macOS MDM plist / Windows HKLM 优先,其次平台 managed file `managed-settings.json``managed-settings.d/*.json` drop-ins,再读可选 remote managed-settings GET source,最后 Windows HKCU;policy settings 已纳入 runner merged settings、headless model/fastMode 解析、permission engine policy source、MCP policy 和 `/config` 可见文件列表;Runner turn-start 和 daemon heartbeat tick 会在 remote managed source 配置存在时 fail-open 刷新 policy settings,并同步影响 merged settings 与 plugin MCP server app-state;`allowManagedPermissionRulesOnly` 会剥离非 policy permission rules。

internal/conversation/run.go

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,10 +1045,18 @@ func (r Runner) formatFilesSummary(raw string) string {
10451045
}
10461046

10471047
func (r Runner) formatIssueSummary(raw string) string {
1048+
description := strings.TrimSpace(raw)
10481049
lines := []string{"Issue report context"}
1049-
if description := strings.TrimSpace(raw); description != "" {
1050+
if description != "" {
10501051
lines = append(lines, "Description: "+description)
10511052
}
1053+
bundle := issueReportBundle{
1054+
GeneratedAt: time.Now().UTC().Format(time.RFC3339Nano),
1055+
Description: description,
1056+
SessionID: r.SessionID,
1057+
WorkingDirectory: strings.TrimSpace(r.WorkingDirectory),
1058+
Model: r.model(),
1059+
}
10521060
if r.SessionID != "" {
10531061
lines = append(lines, "Session ID: "+string(r.SessionID))
10541062
}
@@ -1062,19 +1070,63 @@ func (r Runner) formatIssueSummary(raw string) string {
10621070
path := strings.TrimSpace(provider.PromptDumpPath())
10631071
if path != "" {
10641072
lines = append(lines, "Prompt dump path: "+path)
1073+
bundle.PromptDumpPath = path
10651074
}
10661075
entries := provider.CachedPromptDumpRequests()
10671076
lines = append(lines, fmt.Sprintf("Recent prompt dumps: %d", len(entries)))
1068-
for _, entry := range recentPromptDumpSummaries(entries, 3) {
1069-
lines = append(lines, "- "+entry)
1077+
summaries := recentPromptDumpSummaries(entries, 3)
1078+
bundle.RecentPromptDumps = summaries
1079+
for _, summary := range summaries {
1080+
lines = append(lines, "- "+summary)
10701081
}
10711082
} else {
10721083
lines = append(lines, "Prompt dump cache: unavailable")
10731084
}
1074-
lines = append(lines, "Submission: local context only; remote issue submission is not implemented in the Go runtime yet.")
1085+
if path := r.issueReportBundlePath(); path != "" {
1086+
if err := writeIssueReportBundle(path, bundle); err != nil {
1087+
lines = append(lines, "Issue bundle error: "+err.Error())
1088+
} else {
1089+
lines = append(lines, "Issue bundle path: "+path)
1090+
lines = append(lines, "Submission: local issue bundle prepared.")
1091+
}
1092+
} else {
1093+
lines = append(lines, "Issue bundle path: (not configured)")
1094+
lines = append(lines, "Submission: local issue context prepared.")
1095+
}
10751096
return strings.Join(lines, "\n")
10761097
}
10771098

1099+
type issueReportBundle struct {
1100+
GeneratedAt string `json:"generated_at"`
1101+
Description string `json:"description,omitempty"`
1102+
SessionID contracts.ID `json:"session_id,omitempty"`
1103+
WorkingDirectory string `json:"working_directory,omitempty"`
1104+
Model string `json:"model,omitempty"`
1105+
PromptDumpPath string `json:"prompt_dump_path,omitempty"`
1106+
RecentPromptDumps []string `json:"recent_prompt_dumps,omitempty"`
1107+
}
1108+
1109+
func (r Runner) issueReportBundlePath() string {
1110+
if strings.TrimSpace(r.SessionPath) == "" || r.SessionID == "" {
1111+
return ""
1112+
}
1113+
return filepath.Join(filepath.Dir(r.SessionPath), string(r.SessionID), "issue-report.json")
1114+
}
1115+
1116+
func writeIssueReportBundle(path string, bundle issueReportBundle) error {
1117+
if strings.TrimSpace(path) == "" {
1118+
return nil
1119+
}
1120+
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
1121+
return err
1122+
}
1123+
data, err := json.MarshalIndent(bundle, "", " ")
1124+
if err != nil {
1125+
return err
1126+
}
1127+
return os.WriteFile(path, append(data, '\n'), 0o600)
1128+
}
1129+
10781130
func recentPromptDumpSummaries(entries []anthropic.PromptDumpCacheEntry, limit int) []string {
10791131
if limit <= 0 || len(entries) == 0 {
10801132
return nil

internal/conversation/run_test.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2079,18 +2079,21 @@ func TestRunnerExecutesClearSlashCommandWithoutQuery(t *testing.T) {
20792079
}
20802080

20812081
func TestRunnerExecutesIssueSlashCommandWithoutQuery(t *testing.T) {
2082+
dir := t.TempDir()
20822083
client := &fakeClient{
2083-
dumpPath: filepath.Join(t.TempDir(), "prompt-dump.jsonl"),
2084+
dumpPath: filepath.Join(dir, "prompt-dump.jsonl"),
20842085
dumpCache: []anthropic.PromptDumpCacheEntry{{
20852086
Timestamp: "2026-01-02T03:04:05Z",
20862087
Request: json.RawMessage(`{"model":"sonnet","max_tokens":64,"stream":true,"system":"secret system","messages":[{"role":"user","content":"super secret prompt"}],"tools":[{"name":"SecretTool"}]}`),
20872088
}},
20882089
}
2090+
transcriptPath := filepath.Join(dir, "session.jsonl")
20892091
runner := Runner{
20902092
Client: client,
20912093
Model: "sonnet",
20922094
MaxTokens: 128,
20932095
SessionID: "sess_issue",
2096+
SessionPath: transcriptPath,
20942097
WorkingDirectory: "/repo",
20952098
}
20962099

@@ -2120,6 +2123,8 @@ func TestRunnerExecutesIssueSlashCommandWithoutQuery(t *testing.T) {
21202123
"messages=1",
21212124
"tools=1",
21222125
"system=true",
2126+
"Issue bundle path: " + filepath.Join(dir, "sess_issue", "issue-report.json"),
2127+
"Submission: local issue bundle prepared.",
21232128
} {
21242129
if !strings.Contains(text, want) {
21252130
t.Fatalf("issue summary missing %q in:\n%s", want, text)
@@ -2130,6 +2135,32 @@ func TestRunnerExecutesIssueSlashCommandWithoutQuery(t *testing.T) {
21302135
t.Fatalf("issue summary leaked %q in:\n%s", leaked, text)
21312136
}
21322137
}
2138+
data, err := os.ReadFile(filepath.Join(dir, "sess_issue", "issue-report.json"))
2139+
if err != nil {
2140+
t.Fatal(err)
2141+
}
2142+
var bundle struct {
2143+
Description string `json:"description"`
2144+
SessionID string `json:"session_id"`
2145+
WorkingDirectory string `json:"working_directory"`
2146+
Model string `json:"model"`
2147+
PromptDumpPath string `json:"prompt_dump_path"`
2148+
RecentPromptDumps []string `json:"recent_prompt_dumps"`
2149+
}
2150+
if err := json.Unmarshal(data, &bundle); err != nil {
2151+
t.Fatal(err)
2152+
}
2153+
if bundle.Description != "auth failed" || bundle.SessionID != "sess_issue" || bundle.WorkingDirectory != "/repo" || bundle.Model != "sonnet" || bundle.PromptDumpPath != client.dumpPath {
2154+
t.Fatalf("issue bundle = %#v", bundle)
2155+
}
2156+
if len(bundle.RecentPromptDumps) != 1 || !strings.Contains(bundle.RecentPromptDumps[0], "request_sha256=") {
2157+
t.Fatalf("issue bundle prompt summaries = %#v", bundle.RecentPromptDumps)
2158+
}
2159+
for _, leaked := range []string{"super secret prompt", "secret system", "SecretTool"} {
2160+
if strings.Contains(string(data), leaked) {
2161+
t.Fatalf("issue bundle leaked %q in:\n%s", leaked, data)
2162+
}
2163+
}
21332164
}
21342165

21352166
func TestRunnerExecutesCompactSlashCommandWithoutMainQuery(t *testing.T) {

0 commit comments

Comments
 (0)