Compare commits

...

10 Commits

Author SHA1 Message Date
Tak Hoffman
5d8750cdab polish report drafting guidance 2026-03-21 12:39:23 -05:00
Tak Hoffman
a18f9bc29c Revert "fix: preserve runtime CA trust for env proxy model traffic"
This reverts commit 5d0dc24085.
2026-03-21 12:31:40 -05:00
Tak Hoffman
5d0dc24085 fix: preserve runtime CA trust for env proxy model traffic 2026-03-21 12:28:34 -05:00
Tak Hoffman
d4056692d6 refine report approval flow and quiet mode 2026-03-21 12:19:58 -05:00
Tak Hoffman
deaab200f6 add report-driven feedback workflow 2026-03-21 12:16:44 -05:00
Tak Hoffman
71cfa13192 rename github authoring skill to openclaw feedback 2026-03-21 12:14:44 -05:00
Tak Hoffman
d98f4f9df3 tighten security and pr branch guidance 2026-03-21 12:14:44 -05:00
Tak Hoffman
17affb4a61 fix authoring skill review edge cases 2026-03-21 12:14:44 -05:00
Tak Hoffman
cc32d032b5 address github authoring skill review feedback 2026-03-21 12:14:44 -05:00
Tak Hoffman
fa20679dab add openclaw github authoring skill 2026-03-21 12:14:43 -05:00
14 changed files with 2927 additions and 4 deletions

View File

@@ -30,6 +30,7 @@ This page describes the current CLI behavior. If commands change, update this do
- [`status`](/cli/status)
- [`health`](/cli/health)
- [`sessions`](/cli/sessions)
- [`report`](/cli/report)
- [`gateway`](/cli/gateway)
- [`logs`](/cli/logs)
- [`system`](/cli/system)
@@ -151,6 +152,10 @@ openclaw [--dev] [--profile <name>] <command>
status
health
sessions
report
bug
feature
security
gateway
call
health
@@ -712,6 +717,27 @@ Options:
- `--store <path>`
- `--active <minutes>`
### `report`
Prepare sanitized bug reports, feature requests, and private security report packets for `openclaw/openclaw`.
Subcommands:
- `bug`
- `feature`
- `security`
Shared options:
- `--title <text>`
- `--summary <text>`
- `--json`
- `--markdown`
- `--output <file>`
- `--submit`
- `--yes`
- `--non-interactive`
## Reset / Uninstall
### `reset`

204
docs/cli/report.md Normal file
View File

@@ -0,0 +1,204 @@
---
summary: "CLI reference for `openclaw report` (bug reports, feature requests, and private security packets)"
read_when:
- You want to prepare a sanitized GitHub issue draft from local OpenClaw state
- You want to submit a public bug or feature issue with `gh`
- You need a private security report packet instead of a public issue
title: "report"
---
# `openclaw report`
Prepare sanitized reports for `openclaw/openclaw`.
`openclaw report` turns a small amount of user input plus local runtime/config context into:
- public bug report drafts
- public feature request drafts
- private security report packets
Public bug and feature reports can optionally be submitted with `gh`. Security reports never create a public GitHub issue.
## Subcommands
- `openclaw report bug`
- `openclaw report feature`
- `openclaw report security`
## Shared flags
- `--title <text>`: explicit report title
- `--summary <text>`: short summary (used in public reports and as a fallback title)
- `--json`: emit the structured sanitized payload
- `--markdown`: emit only the rendered report body
- `--output <file>`: write the sanitized body to a file
- `--submit`: submit a public bug/feature issue when the report is ready
- `--yes`: skip interactive confirmation for submission
- `--non-interactive`: disable prompts; public submission requires `--yes`
If neither `--json` nor `--markdown` is passed, the default output is a human-readable sanitized preview.
`--yes` only skips the final interactive confirmation. It does not skip draft generation, diagnostics, or probe execution.
## Bug reports
Use `openclaw report bug` for broken behavior, regressions, or operational failures.
Examples:
```bash
openclaw report bug \
--summary "Gateway times out behind mitmproxy" \
--repro "1. Start gateway behind proxy\n2. Send any LLM request" \
--expected "Model responds successfully" \
--actual "Requests fail with timeout" \
--impact "Blocks all LLM traffic"
```
```bash
openclaw report bug \
--summary "Gateway times out behind mitmproxy" \
--repro "1. Start gateway behind proxy\n2. Send any LLM request" \
--expected "Model responds successfully" \
--actual "Requests fail with timeout" \
--impact "Blocks all LLM traffic" \
--probe gateway \
--submit
```
Bug-specific flags:
- `--repro <text>`: steps to reproduce
- `--expected <text>`: expected behavior
- `--actual <text>`: observed behavior
- `--impact <text>`: severity or workflow impact
- `--previous-version <text>`: optional regression context
- `--evidence <text>`: extra evidence to append
- `--additional-information <text>`: broad extra details, clues, timelines, or hypotheses
- `--context <text>`: compatibility alias for `--additional-information`
- `--probe <general|model|channel|gateway|none>`: bounded evidence collection mode
Required fields for a submission-eligible bug report:
- summary
- repro
- expected
- actual
- impact
Auto-collected where available:
- OpenClaw version
- OS/runtime summary
- configured model/provider hints
- a short bounded probe summary when `--probe` is enabled
Probe guidance:
- `general`: runtime summary, proxy env context, gateway/model/channel signals, and one recent sanitized runtime error when available
- `gateway`: gateway reachability, health, and proxy context
- `model`: provider auth overview plus a bounded live model-path check with combined proxy-status output
- `channel`: configured-channel summary plus recent channel/runtime issue hints
## Feature requests
Use `openclaw report feature` for improvements or new capabilities.
Examples:
```bash
openclaw report feature \
--summary "Add a report dry-run flag" \
--problem "Operators want draft output without touching GitHub" \
--solution "Support report --submit only when explicitly requested" \
--impact "Safer issue authoring from scripts"
```
Feature-specific flags:
- `--problem <text>`: problem to solve
- `--solution <text>`: proposed solution
- `--impact <text>`: expected impact
- `--alternatives <text>`: alternatives considered
- `--evidence <text>`: examples or supporting evidence
- `--additional-information <text>`: broad extra details, clues, timelines, or hypotheses
- `--context <text>`: compatibility alias for `--additional-information`
- `--probe <general|model|channel|gateway|none>`: optional bounded evidence collection
Required fields for a submission-eligible feature request:
- summary
- problem
- solution
- impact
## Security reports
Use `openclaw report security` for private vulnerability reports or sensitive disclosures.
Example:
```bash
openclaw report security \
--title "Gateway token exposed in logs" \
--severity high \
--impact "Operator credential disclosure" \
--component "gateway auth logging" \
--reproduction "Run startup flow with verbose logging enabled" \
--demonstrated-impact "Token appears in terminal output" \
--environment "macOS 15.4, OpenClaw 2026.3.x" \
--remediation "Mask auth values before logging"
```
Security-specific flags:
- `--severity <text>`
- `--impact <text>`
- `--component <text>`
- `--reproduction <text>`
- `--demonstrated-impact <text>`
- `--environment <text>`
- `--remediation <text>`
Rules:
- `report security` never calls `gh issue create`
- `--submit` is ignored as a public-issue path and returns a blocked submission status
- terminal output stays private-report-oriented
- use `--output` or `--markdown` to save a private report packet for manual sending
Private route: send completed security reports to `security@openclaw.ai`.
## Redaction and submission behavior
The command sanitizes common sensitive values before rendering output or submitting:
- tokens / bearer values / API keys
- email addresses
- phone numbers
- private user handles
- local user path prefixes such as `/Users/<name>` or `/home/<name>`
For public bug and feature reports:
- `--submit` is required before any GitHub issue is created
- interactive runs ask for confirmation before `gh issue create`
- non-interactive submission requires both `--submit` and `--yes`
- if required fields are missing, the command returns a structured blocked state instead of guessing
- generated report bodies include a short provenance footer noting they were generated via `openclaw report`
## JSON output
`--json` emits a stable sanitized payload with fields such as:
- `kind`
- `title`
- `body`
- `labels`
- `evidence`
- `redactionsApplied`
- `missingFields`
- `submissionEligible`
- `submission`
This is intended for scripting and higher-level automation.

View File

@@ -37,6 +37,7 @@ x-i18n:
- [`status`](/cli/status)
- [`health`](/cli/health)
- [`sessions`](/cli/sessions)
- [`report`](/cli/report)
- [`gateway`](/cli/gateway)
- [`logs`](/cli/logs)
- [`system`](/cli/system)
@@ -156,6 +157,10 @@ openclaw [--dev] [--profile <name>] <command>
status
health
sessions
report
bug
feature
security
gateway
call
health

210
docs/zh-CN/cli/report.md Normal file
View File

@@ -0,0 +1,210 @@
---
read_when:
- 你想基于本地 OpenClaw 状态生成脱敏后的 GitHub issue 草稿
- 你想用 `gh` 提交公开的 bug 或功能请求
- 你需要私下发送安全报告而不是创建公开 issue
summary: "`openclaw report` 的 CLI 参考bug 报告、功能请求和私有安全报告包)"
title: report
x-i18n:
generated_at: "2026-03-21T03:20:00Z"
model: gpt-5.4
provider: openai
source_path: cli/report.md
workflow: 15
---
# `openclaw report`
`openclaw/openclaw` 准备脱敏后的报告。
`openclaw report` 会把少量用户输入与本地运行时/配置上下文组合起来,生成:
- 公开 bug 报告草稿
- 公开功能请求草稿
- 私有安全报告包
公开的 bug 和功能请求可以选择用 `gh` 提交。安全报告永远不会创建公开 GitHub issue。
## 子命令
- `openclaw report bug`
- `openclaw report feature`
- `openclaw report security`
## 共享标志
- `--title <text>`:显式指定报告标题
- `--summary <text>`:简短摘要(公开报告中使用,也可作为回退标题)
- `--json`:输出结构化的脱敏 payload
- `--markdown`:仅输出渲染后的报告正文
- `--output <file>`:将脱敏后的正文写入文件
- `--submit`:在报告完整时提交公开 bug/feature issue
- `--yes`:跳过提交前的交互确认
- `--non-interactive`:禁用提示;公开提交时需要同时传 `--yes`
如果既没有传 `--json` 也没有传 `--markdown`,默认输出为人类可读的脱敏预览。
`--yes` 只会跳过最后的交互确认,不会跳过草稿生成、诊断收集或 probe 执行。
## Bug 报告
`openclaw report bug` 用于故障行为、回归问题或运行失败。
示例:
```bash
openclaw report bug \
--summary "Gateway 在 mitmproxy 后超时" \
--repro "1. 在代理后启动 gateway\n2. 发送任意 LLM 请求" \
--expected "模型成功响应" \
--actual "请求因超时失败" \
--impact "阻塞所有 LLM 流量"
```
```bash
openclaw report bug \
--summary "Gateway 在 mitmproxy 后超时" \
--repro "1. 在代理后启动 gateway\n2. 发送任意 LLM 请求" \
--expected "模型成功响应" \
--actual "请求因超时失败" \
--impact "阻塞所有 LLM 流量" \
--probe gateway \
--submit
```
Bug 专用标志:
- `--repro <text>`:复现步骤
- `--expected <text>`:期望行为
- `--actual <text>`:实际行为
- `--impact <text>`:对用户或运维的影响
- `--previous-version <text>`:可选的回归版本上下文
- `--evidence <text>`:额外证据
- `--additional-information <text>`:更宽泛的附加细节、线索、时间线或假设
- `--context <text>``--additional-information` 的兼容别名
- `--probe <general|model|channel|gateway|none>`:有限的证据采集模式
要达到可提交的 bug 报告状态,至少需要:
- `summary`
- `repro`
- `expected`
- `actual`
- `impact`
如果可用,还会自动收集:
- OpenClaw 版本
- OS / 运行时摘要
- 已配置的 model / provider 线索
- 在启用 `--probe` 时生成的简短探测摘要
Probe 说明:
- `general`运行时摘要、代理环境上下文、gateway/model/channel 信号,以及可用时的一条近期脱敏错误摘要
- `gateway`gateway 可达性、health 和代理上下文
- `model`provider 认证概览,以及带有代理状态组合输出的有限 live model-path 检查
- `channel`:已配置 channel 摘要,以及近期 channel / runtime 问题线索
## 功能请求
`openclaw report feature` 用于产品改进或新能力需求。
示例:
```bash
openclaw report feature \
--summary "添加 report dry-run 标志" \
--problem "运维希望生成草稿而不直接触发 GitHub" \
--solution "只有显式传 --submit 时才允许真正提交" \
--impact "让脚本化 issue 编写更安全"
```
Feature 专用标志:
- `--problem <text>`:要解决的问题
- `--solution <text>`:提议的解决方案
- `--impact <text>`:预期影响
- `--alternatives <text>`:考虑过的替代方案
- `--evidence <text>`:支持证据或示例
- `--additional-information <text>`:更宽泛的附加细节、线索、时间线或假设
- `--context <text>``--additional-information` 的兼容别名
- `--probe <general|model|channel|gateway|none>`:可选的有限证据采集
要达到可提交的功能请求状态,至少需要:
- `summary`
- `problem`
- `solution`
- `impact`
## 安全报告
`openclaw report security` 用于私下提交漏洞或敏感披露。
示例:
```bash
openclaw report security \
--title "Gateway token 出现在日志中" \
--severity high \
--impact "运维凭证泄露" \
--component "gateway auth logging" \
--reproduction "启用 verbose logging 并运行启动流程" \
--demonstrated-impact "token 出现在终端输出中" \
--environment "macOS 15.4, OpenClaw 2026.3.x" \
--remediation "在日志输出前先 mask 授权值"
```
Security 专用标志:
- `--severity <text>`
- `--impact <text>`
- `--component <text>`
- `--reproduction <text>`
- `--demonstrated-impact <text>`
- `--environment <text>`
- `--remediation <text>`
规则:
- `report security` 永远不会调用 `gh issue create`
- `--submit` 不会触发公开 issue 创建,而是返回被阻止的提交状态
- 终端输出会保持为适合私有报告的内容
- 可使用 `--output``--markdown` 保存私有报告包,后续手动发送
私有提交路径:将完整安全报告发送到 `security@openclaw.ai`
## 脱敏与提交流程
该命令会在渲染输出或提交前自动脱敏常见敏感值:
- token / bearer 值 / API key
- 邮箱地址
- 电话号码
- 私人用户句柄
- 本地用户路径前缀,例如 `/Users/<name>``/home/<name>`
对于公开的 bug 和 feature 报告:
- 只有显式传 `--submit` 才会创建 GitHub issue
- 交互式运行时会在 `gh issue create` 前要求确认
- 非交互式提交需要同时传 `--submit``--yes`
- 如果缺少必填字段,命令会返回结构化的阻止状态,而不是猜测内容
- 生成后的报告正文会附带一条简短来源说明,表明该草稿由 `openclaw report` 生成
## JSON 输出
`--json` 会输出稳定的脱敏 payload字段包括
- `kind`
- `title`
- `body`
- `labels`
- `evidence`
- `redactionsApplied`
- `missingFields`
- `submissionEligible`
- `submission`
该输出适合脚本和更高层的自动化流程。

View File

@@ -0,0 +1,261 @@
---
name: openclaw-feedback
description: Invoke when the user starts complaining about a bug, broken behavior, regression, product issue, feature request, or private security problem in `openclaw/openclaw`. Route them into the right `openclaw report` flow.
user-invocable: true
metadata:
{
"openclaw":
{
"emoji": "🐙",
"requires": { "bins": ["gh"] },
"install":
[
{
"id": "brew",
"kind": "brew",
"formula": "gh",
"bins": ["gh"],
"label": "Install GitHub CLI (brew)",
},
],
},
}
---
# OpenClaw Feedback
Use this skill only for `openclaw/openclaw`.
## Goal
- Route the user into the correct `openclaw report` flow with minimal extra questions.
- Treat `openclaw report` as the only authoritative path for drafting, previewing, diagnostics, redaction, and submission results.
## Do
- Tell the user you are using the `openclaw-feedback` skill.
- In that opening, explain that getting enough context matters because stronger context produces a more accurate, actionable GitHub issue and avoids weak or misleading filings.
- In that opening, give the user a clear way to decline or cancel issue filing if they do not want to proceed.
- Decide `bug`, `feature`, or `private security report`.
- Ask only for missing required fields, with at most 1-3 short questions.
- Derive as much as possible from the conversation, the active diagnosis session, and the generated report draft before asking the user anything.
- Treat `openclaw report bug|feature|security` as the source of truth for drafting, redaction, diagnostics, previewing, and submission behavior.
- If you are unsure about flags or subcommand shape, run `openclaw report --help` before invoking the report flow.
- Tell the user report generation can take a moment when diagnostics or probes are included.
- Relay the generated draft or blocked result from `openclaw report` directly.
- Show the full sanitized draft before asking to submit.
- Ask permission in plain English before adding `--submit`.
- Only after approval, use `--submit` for public bug or feature issues.
- If submission succeeds, include the created GitHub issue URL in the final reply to the user.
## Do Not
- Never create the issue before user approval.
- Never file against any repo other than `openclaw/openclaw`.
- Never maintain a separate manual issue-writing path when `openclaw report` is available.
- Never publish a security report as a public issue.
- Never fall back to manual filing if `openclaw report` or `gh` is unavailable.
- Never ask the user for fields the conversation, diagnostics, or report output already make clear.
- Never read report metadata such as labels, submission eligibility, or redactions back to the user unless it is directly useful to the decision.
## If X Then Y
- If the request is a vulnerability, leaked credential, or private security report: use `openclaw report security`; do not create a public issue.
- If the request is clearly a broken behavior or regression: use `openclaw report bug`.
- If the request is clearly asking for a new capability or improvement: use `openclaw report feature`.
- If the type is unclear: ask one short question to decide bug vs feature.
- If the user already gave enough detail: skip extra questions.
- If summary, likely title, environment, diagnosis clues, repro outline, or impact can be derived from the conversation or active diagnosis work: do not ask for them again.
- If diagnostics would materially improve the report: use `--probe general|gateway|model|channel` on the `openclaw report` command instead of assembling standalone diagnostics yourself.
- If the issue is still too weak after a short recovery attempt: return `NOT_ENOUGH_INFO`.
- If unsafe content cannot be safely redacted without losing the technical meaning: return `BLOCKED_UNSAFE_CONTENT`.
- If `openclaw report` or `gh` is unavailable: return `BLOCKED_MISSING_TOOL`.
## Workflow
1. Say: `Im using the openclaw-feedback skill to prepare an OpenClaw GitHub issue. I want to gather enough context to make the issue accurate and useful for maintainers without over-questioning you. Report generation can take a moment if I include diagnostics or probes. If you do not want to file an issue, just tell me and Ill stop.`
2. Decide `bug`, `feature`, or `private security report`.
3. Derive as much as possible from the conversation and current diagnosis context before asking anything.
4. Ask only for missing required user facts:
- bug: summary, steps to reproduce, expected behavior, actual behavior, impact
optional regression context: previous version -> `--previous-version`
- feature: summary, problem to solve, proposed solution, impact
- security: title, severity, impact, affected component, technical reproduction, demonstrated impact, environment, remediation advice
If a field can be inferred with high confidence from the conversation or diagnosis session, infer it instead of asking.
5. Choose the matching command:
- `openclaw report bug`
- `openclaw report feature`
- `openclaw report security`
6. If targeted diagnostics are useful, add one probe mode:
- `--probe general`
- `--probe gateway`
- `--probe model`
- `--probe channel`
Choose `--probe gateway` for proxy, gateway, or timeout/network failures.
Choose `--probe model` for provider auth, model-call, or dispatcher/proxy-path issues.
Choose `--probe channel` for channel integrations or account-specific failures.
7. Run `openclaw report <kind> ...` and trust its output as authoritative.
8. Show the full sanitized draft or blocked result without reformatting it into a separate skill-owned state machine.
9. When showing the draft, emphasize the user-visible problem and the proposed report body, not internal metadata like labels or submission headers.
10. After showing the draft, ask in plain English: `If this draft looks right, I can submit it to GitHub now.`
11. Do not mention CLI flags like `--submit` in the user-facing approval question.
12. Only if the user clearly approves, rerun or continue with `--submit` for public bug or feature issues.
13. If the issue is created successfully, include the created GitHub URL in the final reply.
14. For security, keep the report private and route the user to `security@openclaw.ai`.
## Common Commands
- Help: `openclaw report --help`
- Bug draft: `openclaw report bug`
- Feature draft: `openclaw report feature`
- Security private report draft: `openclaw report security`
- Public issue submission after approval: add `--submit`
## Flag Mapping
- summary -> `--summary`
- repro -> `--repro`
- expected -> `--expected`
- actual -> `--actual`
- impact -> `--impact`
- previous version -> `--previous-version`
- additional information -> `--additional-information`
- feature problem -> `--problem`
- feature solution -> `--solution`
Use `--additional-information` for details that do not fit neatly into `--repro`, `--expected`, `--actual`, or `--evidence`, including:
- useful loose context
- regression clues
- timelines
- unusual observations
- operator hypotheses worth preserving
Prefer deriving these from the conversation when they are already clear instead of asking the user to restate them.
When passing multiline text into the CLI as a single quoted argument, encode line breaks as literal `\n` so `openclaw report` can render them back as real line breaks in the final issue body.
## Private Security Reports
If the request is a security issue:
- use `openclaw report security` as the source of truth for the private report draft
- do not create a public GitHub issue
- do not include exploit details in chat unless needed to route the report
- ask only for missing required private-report fields
- briefly summarize the category, affected area, and impact in private-report-safe terms only
- tell the user to report it privately to `security@openclaw.ai`
## PII And Secret Redaction
- Let `openclaw report` handle redaction by default.
- If you must quote or summarize content before invoking it, redact tokens, passwords, emails, phone numbers, private-person handles, home-directory names, and unnecessary local file paths.
## Permission Preview
Let `openclaw report` define the preview, readiness, blocked-submission, and created-output wording.
Do not restate or simulate `READY_TO_CREATE`, `SUBMISSION_BLOCKED`, or other CLI output formats inside this skill.
If the user has not clearly approved filing after seeing the full draft, stop before adding `--submit`.
## Notes
- `openclaw report` is the authoritative path for title/body formatting, bounded diagnostics, redaction, degraded diagnostics handling, submission gating, and `gh` failure behavior.
- Keep this skill focused on orchestration, not manual issue authoring.
- Do not invent extra workflow, output schema, or issue-body rules beyond what `openclaw report` already implements.
## Examples
### Example: bug with gateway diagnostics
User says: `OpenClaw times out behind mitmproxy.`
Assistant says:
```text
Im using the openclaw-feedback skill to prepare an OpenClaw GitHub issue. I want to gather enough context to make the issue accurate and useful for maintainers without over-questioning you. Report generation can take a moment if I include diagnostics or probes.
If you do not want to file an issue, just tell me and Ill stop.
I need a few bug-report details: what steps reproduce it, what you expected, what actually happened, and the impact.
```
If those fields are not already clear from the diagnosis session, ask for them. Otherwise infer them and run:
```bash
openclaw report bug --summary "Gateway times out behind mitmproxy" --repro "1. Start gateway\n2. Send request\n3. Observe timeout" --expected "..." --actual "..." --impact "..." --probe gateway
```
Then show the draft itself, not internal report metadata. Only add `--submit` after explicit approval.
After showing the draft, ask:
```text
If this draft looks right, I can submit it to GitHub now.
```
If submission succeeds, include the created issue URL in the final reply.
### Example: regression bug with loose extra context
User says: `This worked last week, but after updating it started timing out.`
Assistant says:
```text
Im using the openclaw-feedback skill to prepare an OpenClaw GitHub issue. I want to gather enough context to make the issue accurate and useful for maintainers without over-questioning you. Report generation can take a moment if I include diagnostics or probes.
If you do not want to file an issue, just tell me and Ill stop.
I need the repro steps, expected behavior, actual behavior, and impact. I can also include the previous version and any extra observations that do not fit neatly into the main bug fields.
```
If the missing facts are not already clear from the conversation or diagnosis session, ask for them. Otherwise infer them and run:
```bash
openclaw report bug --summary "Regression after update" --repro "1. Start gateway\n2. Send request\n3. Observe failure" --expected "..." --actual "..." --impact "..." --previous-version "2026.3.14" --additional-information "Worked last week; now every call times out behind the same proxy setup." --probe model
```
Then show the draft and ask:
```text
If this draft looks right, I can submit it to GitHub now.
```
If submission succeeds, include the created issue URL in the final reply.
### Example: feature request
User says: `Please add a way to export a report draft without submitting it.`
If the user already gave enough detail, derive the report directly and run:
```bash
openclaw report feature --summary "Export report drafts without submission" --problem "Operators want a clean draft artifact without creating a GitHub issue" --solution "Allow report generation and file output without submit" --impact "Safer scripted issue authoring"
```
Then show the draft and ask:
```text
If this draft looks right, I can submit it to GitHub now.
```
If submission succeeds, include the created issue URL in the final reply.
### Example: private security report
User says: `I found a token leak in logs.`
Assistant says:
```text
Im using the openclaw-feedback skill to route this into the private OpenClaw security-report flow. I want to gather enough context to make the report accurate and actionable while keeping it private.
If you do not want to file a report, just tell me and Ill stop.
```
Then gather only missing private-report fields and run:
```bash
openclaw report security --title "Token leak in logs" --severity high --impact "..." --component "..." --reproduction "..." --demonstrated-impact "..." --environment "..." --remediation "..."
```
Do not create a public issue.

View File

@@ -216,6 +216,19 @@ const coreEntries: CoreCliEntry[] = [
mod.registerStatusHealthSessionsCommands(program);
},
},
{
commands: [
{
name: "report",
description: "Prepare sanitized bug, feature, and security reports",
hasSubcommands: true,
},
],
register: async ({ program }) => {
const mod = await import("./register.report.js");
mod.registerReportCommand(program);
},
},
{
commands: [
{

View File

@@ -80,6 +80,11 @@ describe("registerPreActionHooks", () => {
function buildProgram() {
const program = new Command().name("openclaw");
program.command("status").action(() => {});
const report = program.command("report");
report
.command("bug")
.option("--json")
.action(() => {});
program
.command("backup")
.command("create")
@@ -254,6 +259,19 @@ describe("registerPreActionHooks", () => {
});
});
it("suppresses doctor stdout for report commands even without --json", async () => {
await runPreAction({
parseArgv: ["report", "bug"],
processArgv: ["node", "openclaw", "report", "bug"],
});
expect(ensureConfigReadyMock).toHaveBeenCalledWith({
runtime: runtimeMock,
commandPath: ["report", "bug"],
suppressDoctorStdout: true,
});
});
it("bypasses config guard for config validate", async () => {
await runPreAction({
parseArgv: ["config", "validate"],

View File

@@ -37,6 +37,7 @@ const PLUGIN_REQUIRED_COMMANDS = new Set([
]);
const CONFIG_GUARD_BYPASS_COMMANDS = new Set(["backup", "doctor", "completion", "secrets"]);
const JSON_PARSE_ONLY_COMMANDS = new Set(["config set"]);
const QUIET_STRUCTURED_COMMANDS = new Set(["report"]);
let configGuardModulePromise: Promise<typeof import("./config-guard.js")> | undefined;
let pluginRegistryModulePromise: Promise<typeof import("../plugin-registry.js")> | undefined;
@@ -115,6 +116,13 @@ function isJsonOutputMode(commandPath: string[], argv: string[]): boolean {
return true;
}
function shouldSuppressDoctorStdout(commandPath: string[], argv: string[]): boolean {
if (QUIET_STRUCTURED_COMMANDS.has(commandPath[0] ?? "")) {
return true;
}
return isJsonOutputMode(commandPath, argv);
}
export function registerPreActionHooks(program: Command, programVersion: string) {
program.hook("preAction", async (_thisCommand, actionCommand) => {
setProcessTitleForCommand(actionCommand);
@@ -143,7 +151,7 @@ export function registerPreActionHooks(program: Command, programVersion: string)
if (shouldBypassConfigGuard(commandPath)) {
return;
}
const suppressDoctorStdout = isJsonOutputMode(commandPath, argv);
const suppressDoctorStdout = shouldSuppressDoctorStdout(commandPath, argv);
const { ensureConfigReady } = await loadConfigGuardModule();
await ensureConfigReady({
runtime: defaultRuntime,

View File

@@ -0,0 +1,151 @@
import { Command } from "commander";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const reportCommand = vi.fn();
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
vi.mock("../../commands/report.js", () => ({
reportCommand,
}));
vi.mock("../../runtime.js", () => ({
defaultRuntime: runtime,
}));
let registerReportCommand: typeof import("./register.report.js").registerReportCommand;
beforeAll(async () => {
({ registerReportCommand } = await import("./register.report.js"));
});
describe("registerReportCommand", () => {
async function runCli(args: string[]) {
const program = new Command();
registerReportCommand(program);
await program.parseAsync(args, { from: "user" });
return program;
}
beforeEach(() => {
vi.clearAllMocks();
reportCommand.mockResolvedValue(undefined);
});
it("registers bug reports with probe mode", async () => {
await runCli([
"report",
"bug",
"--summary",
"Gateway timeout",
"--repro",
"1. Start gateway",
"--expected",
"Model responds",
"--actual",
"Timeout",
"--impact",
"Blocks requests",
"--previous-version",
"2026.3.14",
"--additional-information",
"Worked last week behind mitmproxy.",
"--probe",
"gateway",
]);
expect(reportCommand).toHaveBeenCalledWith({
kind: "bug",
options: expect.objectContaining({
summary: "Gateway timeout",
repro: "1. Start gateway",
expected: "Model responds",
actual: "Timeout",
impact: "Blocks requests",
previousVersion: "2026.3.14",
additionalInformation: "Worked last week behind mitmproxy.",
probe: "gateway",
}),
runtime,
});
});
it("passes both additional-information and context through for merge handling", async () => {
await runCli([
"report",
"feature",
"--summary",
"Need better retry visibility",
"--problem",
"Retries are opaque",
"--solution",
"Show retry state in UI",
"--impact",
"Reduces debugging time",
"--additional-information",
"Users noticed this after rollout.",
"--context",
"Might be related to proxy retries.",
]);
expect(reportCommand).toHaveBeenCalledWith({
kind: "feature",
options: expect.objectContaining({
additionalInformation: "Users noticed this after rollout.",
context: "Might be related to proxy retries.",
}),
runtime,
});
});
it("documents new report options and probe descriptions in help text", async () => {
const program = new Command();
registerReportCommand(program);
const report = program.commands.find((command) => command.name() === "report");
const help =
report?.commands.find((command) => command.name() === "bug")?.helpInformation() ?? "";
expect(help).toContain("--non-interactive");
expect(help).toContain("--previous-version");
expect(help).toContain("--additional-information");
expect(help).toContain("general=runtime+proxy");
expect(help).toContain("model=auth/live check");
});
it("registers security reports without public probe options", async () => {
await runCli([
"report",
"security",
"--title",
"Token leak",
"--severity",
"high",
"--impact",
"Credential exposure",
"--component",
"Gateway auth",
"--reproduction",
"Run startup",
"--demonstrated-impact",
"Token printed",
"--environment",
"macOS",
"--remediation",
"Mask logs",
]);
expect(reportCommand).toHaveBeenCalledWith({
kind: "security",
options: expect.objectContaining({
title: "Token leak",
severity: "high",
component: "Gateway auth",
remediation: "Mask logs",
}),
runtime,
});
});
});

View File

@@ -0,0 +1,167 @@
import type { Command } from "commander";
import {
reportCommand,
type BugProbeMode,
type BugReportOptions,
type FeatureReportOptions,
type SecurityReportOptions,
} from "../../commands/report.js";
import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import { runCommandWithRuntime } from "../cli-utils.js";
import { formatHelpExamples } from "../help-format.js";
function addSharedOptions(command: Command) {
return command
.option("--title <text>", "Report title")
.option("--summary <text>", "Short report summary")
.option("--json", "Output JSON instead of text", false)
.option("--markdown", "Output rendered Markdown body", false)
.option("--output <file>", "Write the sanitized body to a file")
.option("--submit", "Create the GitHub issue when the report is ready", false)
.option("--yes", "Skip interactive confirmation for submission", false)
.option("--non-interactive", "Disable prompts; requires --yes for submission", false);
}
function resolveProbe(value: unknown): BugProbeMode | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim().toLowerCase();
if (
trimmed === "general" ||
trimmed === "model" ||
trimmed === "channel" ||
trimmed === "gateway" ||
trimmed === "none"
) {
return trimmed;
}
throw new Error("--probe must be one of: general, model, channel, gateway, none");
}
export function registerReportCommand(program: Command) {
const report = program
.command("report")
.description("Prepare sanitized bug, feature, and security reports for openclaw/openclaw")
.addHelpText(
"after",
() =>
`\n${theme.heading("Behavior:")}\n- Running ${theme.command("openclaw report ...")} without ${theme.command("--submit")} generates a draft/preview.\n- Add ${theme.command("--submit")} to create the issue after approval.\n- Non-interactive submission requires ${theme.command("--submit --yes")}.\n\n${theme.heading("Examples:")}\n${formatHelpExamples(
[
[
'openclaw report bug --summary "Gateway timeouts" --repro "..."',
"Draft and preview a bug report.",
],
[
'openclaw report bug --summary "Gateway timeouts" --repro "..." --expected "..." --actual "..." --impact "..." --probe gateway',
"Draft a bug report with gateway and proxy diagnostics.",
],
[
'openclaw report bug --summary "Regression after update" --repro "..." --expected "..." --actual "..." --impact "..." --previous-version "2026.3.14"',
"Draft a regression report with previous-version context.",
],
[
'openclaw report bug --summary "Gateway timeouts" --repro "..." --expected "..." --actual "..." --impact "..." --additional-information "Worked last week behind mitmproxy; now every call times out."',
"Draft a bug report with broad additional information.",
],
[
'openclaw report bug --summary "Gateway timeouts" --repro "..." --expected "..." --actual "..." --impact "..." --submit --yes',
"Submit in non-interactive mode when all required fields are present.",
],
[
'openclaw report feature --summary "Add foo" --problem "..." --solution "..." --impact "..."',
"Draft a feature request.",
],
[
'openclaw report security --title "Token leak" --severity high --impact "..."',
"Prepare a private security report packet.",
],
],
)}`,
)
.addHelpText(
"after",
() =>
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/report", "docs.openclaw.ai/cli/report")}\n`,
);
addSharedOptions(
report
.command("bug")
.description("Prepare a public bug report for openclaw/openclaw")
.option("--repro <text>", "Steps to reproduce")
.option("--expected <text>", "Expected behavior")
.option("--actual <text>", "Actual behavior")
.option("--impact <text>", "User or operator impact")
.option("--previous-version <text>", "Optional previous version for regression context")
.option("--evidence <text>", "Extra evidence to include")
.option(
"--additional-information <text>",
"Broad extra details, clues, timelines, hypotheses, or other useful context",
)
.option("--context <text>", "Compatibility alias for additional information")
.option(
"--probe <mode>",
"Targeted diagnostics: general=runtime+proxy, gateway=reachability, model=auth/live check, channel=channel status, none=skip",
)
.action(async (opts) => {
const options: BugReportOptions = {
...opts,
probe: resolveProbe(opts.probe),
};
await runCommandWithRuntime(defaultRuntime, async () => {
await reportCommand({ kind: "bug", options, runtime: defaultRuntime });
});
}),
);
addSharedOptions(
report
.command("feature")
.description("Prepare a public feature request for openclaw/openclaw")
.option("--problem <text>", "Problem to solve")
.option("--solution <text>", "Proposed solution")
.option("--impact <text>", "Expected impact")
.option("--alternatives <text>", "Alternatives considered")
.option("--evidence <text>", "Supporting evidence or examples")
.option(
"--additional-information <text>",
"Broad extra details, clues, timelines, hypotheses, or other useful context",
)
.option("--context <text>", "Compatibility alias for additional information")
.option(
"--probe <mode>",
"Optional diagnostics context: general=runtime+proxy, gateway=reachability, model=auth/live check, channel=channel status, none=skip",
)
.action(async (opts) => {
const options: FeatureReportOptions = {
...opts,
probe: resolveProbe(opts.probe),
};
await runCommandWithRuntime(defaultRuntime, async () => {
await reportCommand({ kind: "feature", options, runtime: defaultRuntime });
});
}),
);
addSharedOptions(
report
.command("security")
.description("Prepare a private security report packet")
.option("--severity <text>", "Severity assessment")
.option("--impact <text>", "Demonstrated impact")
.option("--component <text>", "Affected component")
.option("--reproduction <text>", "Technical reproduction")
.option("--demonstrated-impact <text>", "Observed exploit or impact")
.option("--environment <text>", "Environment details")
.option("--remediation <text>", "Suggested remediation advice")
.action(async (opts) => {
const options: SecurityReportOptions = { ...opts };
await runCommandWithRuntime(defaultRuntime, async () => {
await reportCommand({ kind: "security", options, runtime: defaultRuntime });
});
}),
);
}

583
src/commands/report.test.ts Normal file
View File

@@ -0,0 +1,583 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const readBestEffortConfig = vi.fn();
const resolveCommandSecretRefsViaGateway = vi.fn();
const probeGateway = vi.fn();
const buildGatewayConnectionDetails = vi.fn();
const callGateway = vi.fn();
const resolveGatewayProbeAuthSafe = vi.fn();
const promptYesNo = vi.fn();
const runExec = vi.fn();
const getStatusSummary = vi.fn();
const buildChannelsTable = vi.fn();
const buildChannelSummary = vi.fn();
const collectChannelStatusIssues = vi.fn();
const resolveOpenClawAgentDir = vi.fn();
const ensureAuthProfileStore = vi.fn();
const resolveProviderAuthOverview = vi.fn();
const runAuthProbes = vi.fn();
vi.mock("../config/config.js", () => ({
readBestEffortConfig,
}));
vi.mock("../cli/command-secret-gateway.js", () => ({
resolveCommandSecretRefsViaGateway,
}));
vi.mock("../cli/command-secret-targets.js", () => ({
getStatusCommandSecretTargetIds: () => ["gateway.auth.token"],
}));
vi.mock("../gateway/probe.js", () => ({
probeGateway,
}));
vi.mock("../gateway/probe-auth.js", () => ({
resolveGatewayProbeAuthSafe,
}));
vi.mock("../gateway/call.js", () => ({
buildGatewayConnectionDetails,
callGateway,
}));
vi.mock("../cli/prompt.js", () => ({
promptYesNo,
}));
vi.mock("../process/exec.js", () => ({
runExec,
}));
vi.mock("./status.js", () => ({
getStatusSummary,
}));
vi.mock("./status-all/channels.js", () => ({
buildChannelsTable,
}));
vi.mock("../infra/channel-summary.js", () => ({
buildChannelSummary,
}));
vi.mock("../infra/channels-status-issues.js", () => ({
collectChannelStatusIssues,
}));
vi.mock("../agents/agent-paths.js", () => ({
resolveOpenClawAgentDir,
}));
vi.mock("../agents/auth-profiles.js", () => ({
ensureAuthProfileStore,
}));
vi.mock("./models/list.auth-overview.js", () => ({
resolveProviderAuthOverview,
}));
vi.mock("./models/list.probe.js", () => ({
runAuthProbes,
}));
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
describe("reportCommand", () => {
let buildReportPayload: typeof import("./report.js").buildReportPayload;
let reportCommand: typeof import("./report.js").reportCommand;
let originalIsTTY: boolean | undefined;
beforeEach(async () => {
vi.resetModules();
({ buildReportPayload, reportCommand } = await import("./report.js"));
runtime.log.mockReset();
runtime.error.mockReset();
runtime.exit.mockReset();
const cfg = {
gateway: { mode: "local" },
channels: { telegram: { enabled: true } },
models: { providers: { anthropic: { models: [{ id: "claude-sonnet-4.5" }] } } },
agents: { defaults: { model: "anthropic/claude-sonnet-4.5" } },
};
readBestEffortConfig.mockResolvedValue(cfg);
resolveCommandSecretRefsViaGateway.mockResolvedValue({
resolvedConfig: cfg,
diagnostics: ["token=abc123", "path=/Users/private-user/project"],
});
buildGatewayConnectionDetails.mockReturnValue({
url: "ws://127.0.0.1:9090",
message: "gateway local",
});
callGateway.mockResolvedValue({ ok: true, channelAccounts: {} });
resolveGatewayProbeAuthSafe.mockReturnValue({ auth: {} });
probeGateway.mockResolvedValue({
ok: true,
connectLatencyMs: 42,
});
getStatusSummary.mockResolvedValue({
channelSummary: ["telegram ok", "discord off"],
queuedSystemEvents: [],
runtimeVersion: "2026.3.20",
heartbeat: { defaultAgentId: "main", agents: [] },
sessions: {
paths: [],
count: 0,
defaults: { model: "anthropic/claude-sonnet-4.5", contextTokens: 200000 },
recent: [],
byAgent: [],
},
});
buildChannelsTable.mockResolvedValue({
rows: [{ id: "telegram", label: "Telegram", enabled: true, state: "ok", detail: "linked" }],
details: [],
});
buildChannelSummary.mockResolvedValue(["telegram linked", "discord disabled"]);
collectChannelStatusIssues.mockReturnValue([]);
resolveOpenClawAgentDir.mockReturnValue("/tmp/openclaw-agent");
ensureAuthProfileStore.mockReturnValue({ profiles: {} });
resolveProviderAuthOverview.mockReturnValue({
effective: { kind: "env", detail: "sk-***" },
profiles: { count: 1 },
});
runAuthProbes.mockResolvedValue({
totalTargets: 1,
durationMs: 120,
results: [
{
provider: "anthropic",
model: "claude-sonnet-4.5",
label: "default",
source: "env",
status: "timeout",
error: "proxy timeout via mitmproxy",
latencyMs: 1200,
},
],
});
promptYesNo.mockResolvedValue(true);
runExec.mockResolvedValue({
stdout: "https://github.com/openclaw/openclaw/issues/999\n",
stderr: "",
});
originalIsTTY = process.stdin.isTTY;
Object.defineProperty(process.stdin, "isTTY", {
configurable: true,
value: true,
});
});
afterEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
Object.defineProperty(process.stdin, "isTTY", {
configurable: true,
value: originalIsTTY,
});
});
it("reports missing bug fields and sanitizes sensitive content", async () => {
const payload = await buildReportPayload({
kind: "bug",
options: {
summary: "Gateway timeout for user@example.com",
repro: "Run from /Users/private-user/project",
},
});
expect(payload.missingFields).toEqual(["Expected behavior", "Actual behavior", "Impact"]);
expect(payload.title).toBe("[Bug]: Gateway timeout for [email-redacted]");
expect(payload.body).toContain("[email-redacted]");
expect(payload.body).toContain("/Users/user/project");
expect(payload.body).not.toContain("abc123");
expect(payload.body).toContain("_Generated via `openclaw report`._");
expect(payload.submissionEligible).toBe(false);
});
it("renders literal escaped newline sequences as real line breaks in report sections", async () => {
const payload = await buildReportPayload({
kind: "bug",
options: {
summary: "Gateway timeout",
repro: "1. Start gateway\\n2. Send request\\n3. Observe failure",
expected: "Model responds",
actual: "Timeout",
impact: "Blocks requests",
},
});
expect(payload.body).toContain("1. Start gateway\n2. Send request\n3. Observe failure");
expect(payload.body).not.toContain("\\n2. Send request");
});
it("writes markdown output that matches the generated body", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-report-test-"));
const outputPath = path.join(tmpDir, "bug.md");
const payload = await reportCommand({
kind: "bug",
options: {
summary: "Gateway timeout",
repro: "1. Start gateway\n2. Send request",
expected: "Model responds",
actual: "Timeout",
impact: "Blocks requests",
markdown: true,
output: outputPath,
},
runtime,
});
const written = await fs.readFile(outputPath, "utf8");
expect(runtime.log).toHaveBeenCalledWith(payload.body);
expect(written).toBe(payload.body);
await fs.rm(tmpDir, { recursive: true, force: true });
});
it("suppresses config warning logging while building a report and restores the env", async () => {
process.env.OPENCLAW_SUPPRESS_CONFIG_WARNINGS = "0";
readBestEffortConfig.mockImplementation(async () => {
expect(process.env.OPENCLAW_SUPPRESS_CONFIG_WARNINGS).toBe("1");
return {
gateway: { mode: "local" },
models: { providers: { anthropic: { models: [{ id: "claude-sonnet-4.5" }] } } },
agents: { defaults: { model: "anthropic/claude-sonnet-4.5" } },
};
});
await reportCommand({
kind: "bug",
options: {
summary: "Gateway timeout",
repro: "1. Start gateway\n2. Send request",
expected: "Model responds",
actual: "Timeout",
impact: "Blocks requests",
},
runtime,
});
expect(process.env.OPENCLAW_SUPPRESS_CONFIG_WARNINGS).toBe("0");
});
it("collects gateway evidence for gateway probe mode", async () => {
vi.stubEnv("HTTPS_PROXY", "http://proxy.local:8080");
const payload = await buildReportPayload({
kind: "bug",
options: {
summary: "Gateway timeout",
repro: "1. Start gateway",
expected: "Model responds",
actual: "Timeout",
impact: "Blocks requests",
probe: "gateway",
},
});
expect(payload.evidenceDetails.some((detail) => detail.source === "gateway")).toBe(true);
expect(payload.evidence).toEqual(
expect.arrayContaining([
expect.stringContaining("Gateway target"),
expect.stringContaining("Gateway probe"),
expect.stringContaining("Effective HTTPS proxy"),
]),
);
expect(payload.body).toContain("## Evidence");
expect(payload.body).toContain("Gateway probe");
});
it("collects model evidence for model probe mode", async () => {
vi.stubEnv("HTTPS_PROXY", "http://proxy.local:8080");
const payload = await buildReportPayload({
kind: "bug",
options: {
summary: "Provider auth mismatch",
repro: "1. Run model list",
expected: "Provider is ready",
actual: "Auth mismatch",
impact: "Blocks completions",
probe: "model",
},
});
expect(resolveProviderAuthOverview).toHaveBeenCalled();
expect(runAuthProbes).toHaveBeenCalled();
expect(payload.evidenceDetails).toEqual(
expect.arrayContaining([
expect.objectContaining({ source: "model", label: "Model path" }),
expect.objectContaining({ source: "model", label: "Provider auth" }),
expect.objectContaining({ source: "model", label: "Model probe" }),
expect.objectContaining({ source: "recentErrors", label: "Recent model-call error" }),
]),
);
expect(payload.body).toContain("Provider auth");
expect(payload.body).toContain("Recent model-call error");
expect(payload.body).toContain("Model probe: timeout via env proxy http://proxy.local:8080");
});
it("collects channel evidence for channel probe mode", async () => {
collectChannelStatusIssues.mockReturnValue([{ message: "Telegram account needs relink" }]);
const payload = await buildReportPayload({
kind: "bug",
options: {
summary: "Telegram send failed",
repro: "1. Send a message",
expected: "Message sends",
actual: "Delivery fails",
impact: "Breaks channel delivery",
probe: "channel",
},
});
expect(payload.evidenceDetails).toEqual(
expect.arrayContaining([
expect.objectContaining({ source: "channel", label: "Configured channels" }),
expect.objectContaining({
source: "channel",
label: "Gateway-reported channel issue",
}),
]),
);
expect(payload.body).toContain("Telegram account needs relink");
});
it("bounds public evidence while preserving richer details for general feature probes", async () => {
vi.stubEnv("HTTPS_PROXY", "http://proxy.local:8080");
vi.stubEnv("NO_PROXY", "localhost,127.0.0.1");
const payload = await buildReportPayload({
kind: "feature",
options: {
summary: "Need better retry visibility",
problem: "Retries are opaque",
solution: "Show retry state in UI",
impact: "Reduces debugging time",
evidence: "User report from user@example.com",
probe: "general",
},
});
expect(payload.evidence.length).toBeLessThanOrEqual(5);
expect(payload.evidenceDetails.length).toBeGreaterThan(payload.evidence.length);
expect(payload.body).toContain("## Evidence");
expect(payload.body).not.toContain("Secret diagnostics");
expect(payload.body).not.toContain("Degraded diagnostics");
expect(payload.body).not.toContain("token=abc123");
expect(payload.evidenceDetails).toEqual(
expect.arrayContaining([
expect.objectContaining({ source: "proxy", label: "Proxy env" }),
expect.objectContaining({ source: "proxy", label: "Effective HTTPS proxy" }),
]),
);
});
it("renders previous version when provided", async () => {
const payload = await buildReportPayload({
kind: "bug",
options: {
summary: "Regression after update",
repro: "1. Start gateway",
expected: "Model responds",
actual: "Timeout",
impact: "Blocks requests",
previousVersion: "2026.3.14",
},
});
expect(payload.body).toContain("## Previous version");
expect(payload.body).toContain("2026.3.14");
});
it("renders additional information with the new heading for bug reports", async () => {
const payload = await buildReportPayload({
kind: "bug",
options: {
summary: "Gateway timeout",
repro: "1. Start gateway",
expected: "Model responds",
actual: "Timeout",
impact: "Blocks requests",
additionalInformation:
"Worked last week for user@example.com from /Users/private-user/project",
},
});
expect(payload.body).toContain("## Additional information");
expect(payload.body).not.toContain("## Additional context");
expect(payload.body).toContain("[email-redacted]");
expect(payload.body).toContain("/Users/user/project");
});
it("merges additional information and context for feature reports", async () => {
const payload = await buildReportPayload({
kind: "feature",
options: {
summary: "Need better retry visibility",
problem: "Retries are opaque",
solution: "Show retry state in UI",
impact: "Reduces debugging time",
additionalInformation: "Users noticed this after the last rollout.",
context: "Might be related to proxy retries.",
},
});
expect(payload.body).toContain("## Additional information");
expect(payload.body).toContain("Users noticed this after the last rollout.");
expect(payload.body).toContain("Might be related to proxy retries.");
});
it("submits a public bug report in non-interactive mode with --yes", async () => {
const payload = await reportCommand({
kind: "bug",
options: {
summary: "Gateway timeout",
repro: "1. Start gateway",
expected: "Model responds",
actual: "Timeout",
impact: "Blocks requests",
submit: true,
yes: true,
nonInteractive: true,
json: true,
},
runtime,
});
expect(runExec).toHaveBeenCalledWith(
"gh",
expect.arrayContaining(["issue", "create", "--repo", "openclaw/openclaw"]),
expect.any(Object),
);
expect(payload.submission.created).toBe(true);
expect(payload.submission.url).toBe("https://github.com/openclaw/openclaw/issues/999");
});
it("prints the created issue url in human output after successful submission", async () => {
await reportCommand({
kind: "bug",
options: {
summary: "Gateway timeout",
repro: "1. Start gateway",
expected: "Model responds",
actual: "Timeout",
impact: "Blocks requests",
submit: true,
yes: true,
nonInteractive: true,
},
runtime,
});
expect(runtime.log).toHaveBeenCalledWith("Created: https://github.com/openclaw/openclaw/issues/999");
});
it("keeps draft generation working when config and secret resolution degrade", async () => {
readBestEffortConfig.mockRejectedValueOnce(new Error("config missing"));
resolveCommandSecretRefsViaGateway.mockRejectedValueOnce(new Error("secret refs unavailable"));
const payload = await buildReportPayload({
kind: "bug",
options: {
summary: "Gateway timeout",
repro: "1. Start gateway",
expected: "Model responds",
actual: "Timeout",
impact: "Blocks requests",
probe: "general",
},
});
expect(payload.submissionEligible).toBe(true);
expect(payload.evidenceDetails).toEqual(
expect.arrayContaining([
expect.objectContaining({
source: "secretDiagnostics",
label: "Degraded diagnostics",
includedInPublicBody: false,
}),
]),
);
expect(payload.body).not.toContain("config load degraded");
});
it("blocks public submission for security reports", async () => {
const payload = await reportCommand({
kind: "security",
options: {
title: "Token leak",
severity: "high",
impact: "Credential reuse risk",
component: "Gateway auth",
reproduction: "Trigger auth flow",
demonstratedImpact: "Token exposed in logs",
environment: "macOS",
remediation: "Mask token before logging",
submit: true,
json: true,
},
runtime,
});
expect(runExec).not.toHaveBeenCalled();
expect(payload.submission.created).toBe(false);
expect(payload.submission.blockedReason).toContain("security reports");
});
it("requires confirmation for interactive bug submission", async () => {
promptYesNo.mockResolvedValue(false);
const payload = await reportCommand({
kind: "bug",
options: {
summary: "Gateway timeout",
repro: "1. Start gateway",
expected: "Model responds",
actual: "Timeout",
impact: "Blocks requests",
submit: true,
},
runtime,
});
expect(promptYesNo).toHaveBeenCalled();
expect(runExec).not.toHaveBeenCalled();
expect(payload.submission.blockedReason).toBe("submission cancelled");
});
it("returns a structured blocked submission when gh create fails", async () => {
runExec.mockRejectedValue(new Error("gh auth login required"));
const payload = await reportCommand({
kind: "bug",
options: {
summary: "Gateway timeout",
repro: "1. Start gateway",
expected: "Model responds",
actual: "Timeout",
impact: "Blocks requests",
submit: true,
yes: true,
nonInteractive: true,
},
runtime,
});
expect(payload.submission.created).toBe(false);
expect(payload.submission.blockedReason).toContain("gh issue create failed");
expect(payload.submission.blockedReason).toContain("gh auth login required");
expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("Report status: Ready"));
expect(runtime.log).toHaveBeenCalledWith(
expect.stringContaining("Submission blocked: gh issue create failed"),
);
});
});

1264
src/commands/report.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -86,6 +86,14 @@ const OPEN_DM_POLICY_ALLOW_FROM_RE =
const CONFIG_AUDIT_LOG_FILENAME = "config-audit.jsonl";
const loggedInvalidConfigs = new Set<string>();
function shouldSuppressConfigWarningsFromEnv(value: string | undefined): boolean {
if (!value) {
return false;
}
const normalized = value.trim().toLowerCase();
return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "off";
}
type ConfigWriteAuditResult = "rename" | "copy-fallback" | "failed";
type ConfigWriteAuditRecord = {
@@ -786,7 +794,10 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
(error as { code?: string; details?: string }).details = details;
throw error;
}
if (validated.warnings.length > 0) {
if (
validated.warnings.length > 0 &&
!shouldSuppressConfigWarningsFromEnv(process.env.OPENCLAW_SUPPRESS_CONFIG_WARNINGS)
) {
const details = validated.warnings
.map(
(iss) =>
@@ -1123,7 +1134,10 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
const issueMessage = issue?.message ?? "invalid";
throw new Error(formatConfigValidationFailure(pathLabel, issueMessage));
}
if (validated.warnings.length > 0) {
if (
validated.warnings.length > 0 &&
!shouldSuppressConfigWarningsFromEnv(process.env.OPENCLAW_SUPPRESS_CONFIG_WARNINGS)
) {
const details = validated.warnings
.map((warning) => `- ${warning.path}: ${warning.message}`)
.join("\n");

View File

@@ -339,7 +339,6 @@ describe("chat view", () => {
expect(container.textContent).not.toContain("context used");
});
it("uses the assistant avatar URL for the welcome state when the identity avatar is only initials", () => {
const container = document.createElement("div");
render(