Repository navigation
feat(skills): report-feedback skill (report issues from any surface) - #2010
Conversation
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 37%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
The user can report a bug / feature request / feedback by asking Sutando from ANY surface — chat, Discord, Telegram, or voice (via task delegation). One skill, not a per-surface tool, so there's no duplication. - skills/report-feedback/report-feedback.py posts to the cloud /api/feedback route (mirrors into GitHub issues) with kind/severity/title/body + a diagnostic context (platform + a tail of recent <workspace>/logs). - Reads cloud-auth.json on disk (falls back to the metering env the desktop injects); prints NOT_SIGNED_IN + exits 2 when signed out, ERROR on API failure — never leaves the agent guessing. - Owner-tier only. Replaces the earlier voice-only report_issue inline tool (dropped) per owner: the skill covers voice too via delegation, avoiding a duplicate capability. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Zp1T5KEdGsfNRFSLhY3fK
7aa49e9 to
99a75c1
Compare
The auth probe used the wrong path (state/auth/) and required a `signedIn` field. The desktop's readCloudAuth (electron/ipc.cjs) stores the record at <workspace>/cloud-auth.json and treats "signed in" as simply having a `token` (no signedIn field). Match that: read <workspace>/cloud-auth.json, also probe the packaged-app canonical workspace (~/.sutando/repo/workspace) so the skill finds the token across the dev/packaged split, and gate on token presence. Verified end-to-end: a signed-in submission returns 200 and lands in the feedback funnel + mirrored GitHub issue. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Zp1T5KEdGsfNRFSLhY3fK
|
Live test passed ✅ Confirmed the talk/type-to-Sutando path also files issues successfully: reported an issue by talking to Sutando and it went through end to end via this skill (POST to cloud /api/feedback, logs context included). Together with the desktop button path (AG2Platform/sutando-desktop#57, also verified live — report shows in Slack with diagnostics inline), that gives full click / talk / type coverage for issue reporting. This PR is green and ready for review/merge. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Mechanics LGTM — holding the formal approve on the same privacy question I raised on sutando-desktop#57 (this PR's sibling). The two share one decision; worth resolving them together.
Sound parts
Reusing /api/feedback + the metering Bearer the supervisor already injects (no new env wiring), graceful signed-out / API-error handling that never throws mid-conversation, --no-logs escape hatch, small (+186), CI 7/7 green. The "talk to Sutando to file a bug" flow is a genuinely nice complement to the desktop form.
This is the same log-tail-into-GitHub-issues surface as #57 — and here it's a bit sharper
logs_excerpt() is the same shape as #57's (last 40 lines × 4 newest <workspace>/logs/*.log, ~8 KB) posted into context.last_logs_excerpt, and /api/feedback mirrors into GitHub issues. So the same two points apply, plus one:
-
Shared redaction decision. Whatever you decide on #57 (raw vs. a
Bearer …/token=…//Users/<name>/mask) should apply here identically — same context shape, same mirror. Ideally a shared redaction helper so the TS side (#57) and this Python side don't drift to different masking. The deciding factor is the same: is the mirror repo public (redaction should-fix) or private (nice-to-have)? -
Consent is weaker than #57. #57 has a UI checkbox (opt-out, default-on, honest copy). Here consent is mediated by the agent: SKILL.md says "Pass
--no-logsif the user prefers not to attach logs" but doesn't tell the agent to say it's attaching logs first. On voice especially, the user won't see a checkbox — they'll just hear "filed it." Suggest one line in SKILL.md: instruct the agent to mention "I'll attach recent diagnostic logs unless you'd rather I didn't" before sending, so it's an informed opt-out rather than a silent default. -
Confirm the surface/tier gating. The PR body says it's registered in
ownerOnlyTools(good — voice/phone owner only), but the SKILL description says it's usable "from any surface (chat, Discord, Telegram, or a voice-delegated task)." If a non-owner Discord/Telegram user can trigger it, they'd ship the owner's workspace log tail into an issue — worse than the #57 end-user case. Please confirm the non-owner Discord/Telegram path can't reach the cloud-Bearer'd skill (the sandboxed-codex routing suggests it can't, but it's worth stating explicitly).
To flip to approve: same as #57 — confirm the mirror is private, or add the (shared) redaction pass; and a one-line SKILL.md nudge to announce the log attachment on voice. None of it is a mechanical blocker — the code is clean; it's a posture call that's the owner's, and it's the same call for both PRs.
(Reviewed from the mini node; cross-linked to sutando-desktop#57 — same API + context shape.)
|
Follow-up now that the sibling
So the redaction/public-mirror piece is effectively decided (not required). My one remaining ask is the voice-specific consent gap, which doesn't carry over from #57: the desktop form has a visible opt-out checkbox; this voice/chat path has only the Two small residuals, both non-blocking: (1) if (mini node — following up post-#57 merge.) |
…l body Addresses review on sonichi#2010 (qingyun-wu): - SKILL.md: instruct the agent to ANNOUNCE the log attachment before sending ("I'll attach recent diagnostic logs unless you'd rather I didn't") and honor --no-logs — an informed opt-out for voice/chat, matching sonichi#57's checkbox. - report-feedback.py: add _redact() backstop over the log excerpt (Bearer tokens, token=/api_key=/secret= values, common key formats sk-/xoxb-/ghp_/ AKIA, and the home-dir username). Private-Slack mirror makes this non-blocking, but /api/feedback can also open GitHub issues, so scrub before it leaves. - SKILL.md access-tier: state explicitly that non-owner Discord/Slack/Telegram route to sandboxed codex (no cloud token) and cannot reach this skill, so a non-owner can't ship the owner's logs into an issue. Also fixes a real bug found while testing: an empty --body sent body:null, which /api/feedback rejects (400 invalid_payload, 'Expected string, received null'). Now sends a string (falls back to the title), matching the desktop form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DmsnVs5ffwHXdrQsLoZUCb
|
Thanks @qingyun-wu — addressed in
Bonus — fixed a real bug found while testing: an empty CI should re-run on the new commit. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Automated review
[P1] skills/report-feedback/report-feedback.py:84-87 still looks for Cloud auth at <workspace>/cloud-auth.json and ~/.sutando/repo/workspace/cloud-auth.json. Per the current workspace contract and migration rules, cloud-auth.json was moved under <workspace>/state/auth/ (see docs/workspace-contract.md and tests/sutando-migrate*.sh). On a migrated install this skill will report NOT_SIGNED_IN even when the owner is signed in, so the new reporting path breaks for the common upgraded case. Please probe state/auth/cloud-auth.json first, then keep the legacy roots only as fallback.
| """ | ||
| seen: set[str] = set() | ||
| for p in ( | ||
| ws / "cloud-auth.json", # <workspace>/cloud-auth.json — where the desktop writes it |
There was a problem hiding this comment.
Automated review: on current workspaces the cloud token lives under <workspace>/state/auth/cloud-auth.json (see AGENTS.md + workspace contract), but this lookup only probes the pre-migration root-level cloud-auth.json paths. That means signed-in users on post-M1 installs will hit NOT_SIGNED_IN and the new skill won't work. Please check the canonical state/auth/cloud-auth.json location first, then keep the legacy fallbacks for older installs.
There was a problem hiding this comment.
Fixed in 396edd4: read_cloud_auth() now probes <workspace>/state/auth/cloud-auth.json first (M1 canonical), then falls back to the pre-M1 root path (30-day reader window per workspace contract). See the before/after test result in the PR comment above.
| """ | ||
| seen: set[str] = set() | ||
| for p in ( | ||
| ws / "cloud-auth.json", # <workspace>/cloud-auth.json — where the desktop writes it |
There was a problem hiding this comment.
Automated review: this still reads the pre-M1 cloud-auth location only. The workspace contract moved durable auth to state/auth/cloud-auth.json, with readers expected to prefer the new canonical path and fall back to legacy during the transition window. As written, signed-in users on a migrated workspace will get NOT_SIGNED_IN even though the desktop is authenticated. Please check state/auth/cloud-auth.json first, then keep the old locations as fallback.
There was a problem hiding this comment.
Fixed in 396edd4: read_cloud_auth() now probes <workspace>/state/auth/cloud-auth.json first (M1 canonical), then falls back to the pre-M1 root path (30-day reader window per workspace contract). See the before/after test result in the PR comment above.
|
Automated review:
|
|
Automated review: |
qingyun-wu
left a comment
There was a problem hiding this comment.
Automated review: skills/report-feedback/report-feedback.py:73-87 is still looking for cloud auth at <workspace>/cloud-auth.json and a couple of legacy locations. The current workspace contract moved this durable host auth to <workspace>/state/auth/cloud-auth.json (see docs/workspace-contract.md). On a current install, a signed-in user will therefore hit NOT_SIGNED_IN and be unable to file feedback at all. Please add the canonical state/auth/cloud-auth.json path (preferably before the legacy fallbacks).
|
Automated review: one actionable finding.
|
|
Automated review:
|
| """ | ||
| seen: set[str] = set() | ||
| for p in ( | ||
| ws / "cloud-auth.json", # <workspace>/cloud-auth.json — where the desktop writes it |
There was a problem hiding this comment.
[automated review] read_cloud_auth() only checks legacy root-level cloud-auth.json locations, but the repo workspace contract moved this credential to <workspace>/state/auth/cloud-auth.json (see AGENTS.md/docs). On a migrated install this path misses the real token, so the skill returns NOT_SIGNED_IN even though the user is signed in. Please probe the canonical state/auth/cloud-auth.json path first and keep the old root path only as a fallback during the migration window.
There was a problem hiding this comment.
Fixed in 396edd4: read_cloud_auth() now probes <workspace>/state/auth/cloud-auth.json first (M1 canonical), then falls back to the pre-M1 root path (30-day reader window per workspace contract). See the before/after test result in the PR comment above.
| """ | ||
| seen: set[str] = set() | ||
| for p in ( | ||
| ws / "cloud-auth.json", # <workspace>/cloud-auth.json — where the desktop writes it |
There was a problem hiding this comment.
Automated review:
cloud-auth.json was moved under <workspace>/state/auth/ as part of the per-host auth-state migration, but this probe only checks the old root-level locations. On a migrated install the skill will report NOT_SIGNED_IN even though the user is signed into Sutando Cloud, which breaks the new reporting flow for exactly the current workspace layout. Please include the canonical state/auth/cloud-auth.json path before falling back to legacy locations.
There was a problem hiding this comment.
Fixed in 396edd4: read_cloud_auth() now probes <workspace>/state/auth/cloud-auth.json first (M1 canonical), then falls back to the pre-M1 root path (30-day reader window per workspace contract). See the before/after test result in the PR comment above.
qingyun-wu
left a comment
There was a problem hiding this comment.
Automated review: one blocking issue.
| """ | ||
| seen: set[str] = set() | ||
| for p in ( | ||
| ws / "cloud-auth.json", # <workspace>/cloud-auth.json — where the desktop writes it |
There was a problem hiding this comment.
This only probes the legacy cloud-auth.json locations. The current workspace contract moved cloud auth under <workspace>/state/auth/cloud-auth.json, so on a modern signed-in install this skill will fall through to NOT_SIGNED_IN and the new reporting path won’t work at all. Please include the canonical state/auth/cloud-auth.json path (and its packaged-app equivalent) in the search order before the legacy fallbacks.
There was a problem hiding this comment.
Fixed in 396edd4: read_cloud_auth() now probes <workspace>/state/auth/cloud-auth.json first (M1 canonical), then falls back to the pre-M1 root path (30-day reader window per workspace contract). See the before/after test result in the PR comment above.
|
Automated review:
|
| """ | ||
| seen: set[str] = set() | ||
| for p in ( | ||
| ws / "cloud-auth.json", # <workspace>/cloud-auth.json — where the desktop writes it |
There was a problem hiding this comment.
Automated review: this auth lookup still probes the pre-migration cloud-auth.json locations, but the current workspace contract moved cloud credentials under state/auth/cloud-auth.json. On a normal post-M1 workspace the skill will report NOT_SIGNED_IN even though the user is signed in, so the new reporting path fails for exactly the installs it is meant to serve. Please add the canonical state/auth/cloud-auth.json path (and keep any legacy fallback after it).
There was a problem hiding this comment.
Fixed in 396edd4: read_cloud_auth() now probes <workspace>/state/auth/cloud-auth.json first (M1 canonical), then falls back to the pre-M1 root path (30-day reader window per workspace contract). See the before/after test result in the PR comment above.
|
Automated review: [blocker] |
|
Automated review:
|
| def read_cloud_auth(ws: Path): | ||
| """Return (apiBase, token) if signed in to Sutando Cloud, else (None, None). | ||
|
|
||
| Matches the desktop's readCloudAuth (electron/ipc.cjs): the record lives at |
There was a problem hiding this comment.
Automated review: this still looks at the pre-M1 auth locations (<workspace>/cloud-auth.json and the legacy app-support path), but the workspace contract moved durable cloud credentials under state/auth/cloud-auth.json. On a current install the skill will report NOT_SIGNED_IN even when the owner is signed in, so the new reporting path fails for the default case unless the metering env also happens to be present. Please probe the canonical state/auth/cloud-auth.json path first, then keep the legacy locations as fallback.
There was a problem hiding this comment.
Fixed in 396edd4: read_cloud_auth() now probes <workspace>/state/auth/cloud-auth.json first (M1 canonical), then falls back to the pre-M1 root path (30-day reader window per workspace contract). See the before/after test result in the PR comment above.
|
Automated review:
|
| """ | ||
| seen: set[str] = set() | ||
| for p in ( | ||
| ws / "cloud-auth.json", # <workspace>/cloud-auth.json — where the desktop writes it |
There was a problem hiding this comment.
Automated review: cloud-auth.json was moved under <workspace>/state/auth/ in the workspace contract, but this lookup only probes <workspace>/cloud-auth.json plus legacy fallbacks. On migrated installs the skill will incorrectly print NOT_SIGNED_IN even when the desktop is signed in. Please check ws / "state" / "auth" / "cloud-auth.json" first and keep the older locations only as fallback.
There was a problem hiding this comment.
Fixed in 396edd4: read_cloud_auth() now probes <workspace>/state/auth/cloud-auth.json first (M1 canonical), then falls back to the pre-M1 root path (30-day reader window per workspace contract). See the before/after test result in the PR comment above.
|
Updated the branch: merged current |
|
Automated review: Blocking: |
|
Automated review:
|
|
Automated review:
|
qingyun-wu
left a comment
There was a problem hiding this comment.
Automated review: 1 actionable regression found.
| seen: set[str] = set() | ||
| for p in ( | ||
| ws / "cloud-auth.json", # <workspace>/cloud-auth.json — where the desktop writes it | ||
| Path.home() / ".sutando" / "repo" / "workspace" / "cloud-auth.json", # packaged-app default (per workspace contract) |
There was a problem hiding this comment.
The cloud auth record was re-homed to <workspace>/state/auth/cloud-auth.json during the migration work, so probing only <workspace>/cloud-auth.json will make this new skill return NOT_SIGNED_IN on current installs even when the user is signed in. Please check state/auth/cloud-auth.json first and keep the root-level path only as a legacy fallback.
There was a problem hiding this comment.
Fixed in 396edd4: read_cloud_auth() now probes <workspace>/state/auth/cloud-auth.json first (M1 canonical), then falls back to the pre-M1 root path (30-day reader window per workspace contract). See the before/after test result in the PR comment above.
… first Post-M1 the cloud auth record lives at <workspace>/state/auth/cloud-auth.json. Add it as the first probe; retain the pre-M1 root path as a 30-day reader fallback per the workspace contract. Mirror the same M1+fallback pair for the packaged-app workspace so the skill finds the token regardless of checkout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EL2XBcV3RsWhevzTPtW4jw
|
Fixed in |
|
Before / After test result for This demonstrates the scenario @qingyun-wu flagged: user running from the git repo, M1 migration complete — Before (pre-
|
|
Fixed in |
…arator)
AKIA keys have no separator between the prefix and body (AKIAIOSFODNN7EXAMPLE),
so the shared [_-] pattern never matched them. Split out a dedicated
AKIA[A-Z0-9]{16} pattern that fires correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EL2XBcV3RsWhevzTPtW4jw
|
AKIA redaction fix pushed as
# Before (never fired for real AKIA keys):
r"\b(sk|...|AKIA)[_-][A-Za-z0-9_\-]{6,}"
# After (AKIA pattern separated, matches real format):
r"\b(sk|...|github_pat)[_-][A-Za-z0-9_\-]{6,}"
text = re.sub(r"\bAKIA[A-Z0-9]{16}\b", "<redacted-token>", text)Verified: |
qingyun-wu
left a comment
There was a problem hiding this comment.
Automated review: one actionable test gap.
skills/report-feedback/report-feedback.py:49-56and:87-102change security-sensitive parsing logic (AWSAKIA...redaction and the canonicalstate/auth/cloud-auth.jsonlookup), but this PR still has no regression coverage for the skill. Those are both fixes that were found during review, so please add a small test that exercises anAKIAIOSFODNN7EXAMPLEsample and a migrated-workspace auth record understate/auth/cloud-auth.json; otherwise the next refactor can silently reopen them.
|
Automated review:
Please add |
|
Fixed in
Verified locally: python3 tests/report-feedback.test.py |
… excerpt Raises diff coverage on report-feedback.py from 35% to 100%, clearing the Coverage Gate on sonichi#2010. Adds tests for resolve_workspace (import + fallback), the read_cloud_auth metering-env / dedup / malformed-file branches, logs_excerpt (present / absent / unreadable), and main() (blank-title exit, not-signed-in exit, successful POST with and without logs, HTTPError and generic-error exits) via mocked urlopen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DC4oxV4ZwnKTKvE6Hb28Yo
|
Review findings (full detail; the REQUEST_CHANGES verdict is queued on the approval gate): The one blocking item — Google API keys leak through _redact. Reproduced with this PR's code on realistic fleet log lines:
Why it matters here specifically: this fleet is Gemini-heavy — live-transport URLs carry Fix is two lines + a test:
Non-blocking notes:
Once the AIza pattern + test land I'm ready to re-review same-day. |
|
Fixed the Google API key redaction gap in
Verified locally: python3 tests/report-feedback.test.py |
|
The recurring automated-review finding — auth probe only checking the legacy root Hardening since then, driven by the earlier redaction discussion:
CI is green on HEAD: diff coverage 100%, tsc + tests (clean install), CLA signed, and all workspace-path guards pass. Could you re-review current HEAD |
|
@cla-assistant check |
…onichi#2010) * feat(skills): report-feedback skill (single reporting path) The user can report a bug / feature request / feedback by asking Sutando from ANY surface — chat, Discord, Telegram, or voice (via task delegation). One skill, not a per-surface tool, so there's no duplication. - skills/report-feedback/report-feedback.py posts to the cloud /api/feedback route (mirrors into GitHub issues) with kind/severity/title/body + a diagnostic context (platform + a tail of recent <workspace>/logs). - Reads cloud-auth.json on disk (falls back to the metering env the desktop injects); prints NOT_SIGNED_IN + exits 2 when signed out, ERROR on API failure — never leaves the agent guessing. - Owner-tier only. Replaces the earlier voice-only report_issue inline tool (dropped) per owner: the skill covers voice too via delegation, avoiding a duplicate capability. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Zp1T5KEdGsfNRFSLhY3fK * fix(report-feedback): read the cloud token the desktop actually writes The auth probe used the wrong path (state/auth/) and required a `signedIn` field. The desktop's readCloudAuth (electron/ipc.cjs) stores the record at <workspace>/cloud-auth.json and treats "signed in" as simply having a `token` (no signedIn field). Match that: read <workspace>/cloud-auth.json, also probe the packaged-app canonical workspace (~/.sutando/repo/workspace) so the skill finds the token across the dev/packaged split, and gate on token presence. Verified end-to-end: a signed-in submission returns 200 and lands in the feedback funnel + mirrored GitHub issue. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Zp1T5KEdGsfNRFSLhY3fK * fix(report-feedback): announce+redact logs, harden tier note, fix null body Addresses review on sonichi#2010 (qingyun-wu): - SKILL.md: instruct the agent to ANNOUNCE the log attachment before sending ("I'll attach recent diagnostic logs unless you'd rather I didn't") and honor --no-logs — an informed opt-out for voice/chat, matching sonichi#57's checkbox. - report-feedback.py: add _redact() backstop over the log excerpt (Bearer tokens, token=/api_key=/secret= values, common key formats sk-/xoxb-/ghp_/ AKIA, and the home-dir username). Private-Slack mirror makes this non-blocking, but /api/feedback can also open GitHub issues, so scrub before it leaves. - SKILL.md access-tier: state explicitly that non-owner Discord/Slack/Telegram route to sandboxed codex (no cloud token) and cannot reach this skill, so a non-owner can't ship the owner's logs into an issue. Also fixes a real bug found while testing: an empty --body sent body:null, which /api/feedback rejects (400 invalid_payload, 'Expected string, received null'). Now sends a string (falls back to the title), matching the desktop form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DmsnVs5ffwHXdrQsLoZUCb * fix(report-feedback): probe state/auth/cloud-auth.json (M1 canonical) first Post-M1 the cloud auth record lives at <workspace>/state/auth/cloud-auth.json. Add it as the first probe; retain the pre-M1 root path as a 30-day reader fallback per the workspace contract. Mirror the same M1+fallback pair for the packaged-app workspace so the skill finds the token regardless of checkout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EL2XBcV3RsWhevzTPtW4jw * fix(report-feedback): match real AWS access keys in _redact() (no separator) AKIA keys have no separator between the prefix and body (AKIAIOSFODNN7EXAMPLE), so the shared [_-] pattern never matched them. Split out a dedicated AKIA[A-Z0-9]{16} pattern that fires correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EL2XBcV3RsWhevzTPtW4jw * fix(report-feedback): redact Slack app tokens * test(report-feedback): cover main flow, cloud-auth branches, and logs excerpt Raises diff coverage on report-feedback.py from 35% to 100%, clearing the Coverage Gate on sonichi#2010. Adds tests for resolve_workspace (import + fallback), the read_cloud_auth metering-env / dedup / malformed-file branches, logs_excerpt (present / absent / unreadable), and main() (blank-title exit, not-signed-in exit, successful POST with and without logs, HTTPError and generic-error exits) via mocked urlopen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DC4oxV4ZwnKTKvE6Hb28Yo * fix(report-feedback): redact Google API keys --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Qingyun Wu <qingyun0327@gmail.com>
…ezone-aware now(UTC) (#2003) * fix(friction-detector): replace deprecated datetime.utcnow() with timezone-aware now(UTC) datetime.utcnow() was deprecated in Python 3.12 and scheduled for removal. Both `now` and the parsed `updatedAt` are now timezone-aware (UTC), so their difference is still correct. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QB3qZQrwponfum2JFNvHeh * test(friction-detector): cover check_github_issues() datetime comparison for diff-coverage gate Adds two tests that mock subprocess.run to exercise the timezone-aware datetime comparison path without a live gh token, satisfying the >= 95% diff-coverage gate on PR #2003. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QB3qZQrwponfum2JFNvHeh * fix(tests): fix start-cli, startup-env-token-persist, sutando-migrate for Ubuntu CI (#2029) * test(shell-ci): fix two ubuntu-portability failures from shell-ci-known-failures Two tests fail on ubuntu-latest CI (wired in by #1948) due to macOS-specific tooling; both pass on macOS today but gate CI on Ubuntu. start-cli-claude-config-dir.test.sh (2 failures): 1. Ambient CLAUDE_CONFIG_DIR leak — the test runner session has CLAUDE_CONFIG_DIR set, which leaked into the subprocess and broke "not set" assertion. Fix: add `unset CLAUDE_CONFIG_DIR` in setup_sandbox() alongside the HOME/SUTANDO_WORKSPACE exports that already clear the environment. 2. /var/folders vs /private/var symlink mismatch — macOS mktemp returns /var/folders/… but cd+pwd -P resolves to /private/var/folders/…; sutando-config.sh uses the physical path, so the expected vs actual CLAUDE_CONFIG_DIR differed. Fix: resolve SANDBOX to its real path (cd+pwd -P) immediately after mktemp. 3. Stale expected path — expected was keyed on $SUTANDO_WORKSPACE (deprecated v0.8 env-var, no longer read by the resolver) instead of the config-derived $REPO_FAKE/workspace. Fix: align expected to what sutando-config.sh actually computes. startup-env-token-persist.test.sh (would fail on Ubuntu): - stat -f '%Lp' (BSD mode format) and stat -f '%m' (BSD mtime) are macOS-only. Fix: replace both with equivalent Python one-liners (stat.S_IMODE + os.stat.st_mtime) that work on macOS and Linux without any extra deps. After this PR, both suites should be removable from tests/shell-ci-known-failures.txt once that file lands (via #1948). Closes two items from #1961. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EL2XBcV3RsWhevzTPtW4jw * test(shell-ci): fix sutando-migrate.test.sh — wrong DEST + BSD stat + pipefail exit Three bugs caused all 7 assertions to fail: 1. Wrong DEST — test used `SUTANDO_WORKSPACE="$DEST" --respect-env` but the workspace resolver ignores $SUTANDO_WORKSPACE since #1440. The backup landed in the real workspace instead of the temp fixture. Fix: use SUTANDO_MIGRATE_DEST (the documented E2E test hook symmetric with SUTANDO_MIGRATE_SRC_{A,B,C}). 2. Early exit via set -e — `grep -E "^sutando-migrate: backup"` exits 1 when the dest is empty (the backup line reads "dest empty; placeholder backup → …"). With `set -uo pipefail`, the INITIAL_BACKUP_ID assignment aborted the script before the assertion block ran. Fix: broaden the pattern to "migration-backup-.*\.tar\.gz" and add `|| true`. 3. BSD-only stat — `stat -f %m` is macOS-only; GNU stat uses `-c '%Y'`. Affects mtime comparison for notes/shared.md on Ubuntu. Fix: Python `os.stat().st_mtime` (portable, already used in startup-env-token-persist and start-cli fixes on this branch). All 14 assertions now pass locally. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EL2XBcV3RsWhevzTPtW4jw --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Qingyun Wu <qingyun0327@gmail.com> * feat(skills): report-feedback skill (report issues from any surface) (#2010) * feat(skills): report-feedback skill (single reporting path) The user can report a bug / feature request / feedback by asking Sutando from ANY surface — chat, Discord, Telegram, or voice (via task delegation). One skill, not a per-surface tool, so there's no duplication. - skills/report-feedback/report-feedback.py posts to the cloud /api/feedback route (mirrors into GitHub issues) with kind/severity/title/body + a diagnostic context (platform + a tail of recent <workspace>/logs). - Reads cloud-auth.json on disk (falls back to the metering env the desktop injects); prints NOT_SIGNED_IN + exits 2 when signed out, ERROR on API failure — never leaves the agent guessing. - Owner-tier only. Replaces the earlier voice-only report_issue inline tool (dropped) per owner: the skill covers voice too via delegation, avoiding a duplicate capability. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Zp1T5KEdGsfNRFSLhY3fK * fix(report-feedback): read the cloud token the desktop actually writes The auth probe used the wrong path (state/auth/) and required a `signedIn` field. The desktop's readCloudAuth (electron/ipc.cjs) stores the record at <workspace>/cloud-auth.json and treats "signed in" as simply having a `token` (no signedIn field). Match that: read <workspace>/cloud-auth.json, also probe the packaged-app canonical workspace (~/.sutando/repo/workspace) so the skill finds the token across the dev/packaged split, and gate on token presence. Verified end-to-end: a signed-in submission returns 200 and lands in the feedback funnel + mirrored GitHub issue. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Zp1T5KEdGsfNRFSLhY3fK * fix(report-feedback): announce+redact logs, harden tier note, fix null body Addresses review on #2010 (qingyun-wu): - SKILL.md: instruct the agent to ANNOUNCE the log attachment before sending ("I'll attach recent diagnostic logs unless you'd rather I didn't") and honor --no-logs — an informed opt-out for voice/chat, matching #57's checkbox. - report-feedback.py: add _redact() backstop over the log excerpt (Bearer tokens, token=/api_key=/secret= values, common key formats sk-/xoxb-/ghp_/ AKIA, and the home-dir username). Private-Slack mirror makes this non-blocking, but /api/feedback can also open GitHub issues, so scrub before it leaves. - SKILL.md access-tier: state explicitly that non-owner Discord/Slack/Telegram route to sandboxed codex (no cloud token) and cannot reach this skill, so a non-owner can't ship the owner's logs into an issue. Also fixes a real bug found while testing: an empty --body sent body:null, which /api/feedback rejects (400 invalid_payload, 'Expected string, received null'). Now sends a string (falls back to the title), matching the desktop form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DmsnVs5ffwHXdrQsLoZUCb * fix(report-feedback): probe state/auth/cloud-auth.json (M1 canonical) first Post-M1 the cloud auth record lives at <workspace>/state/auth/cloud-auth.json. Add it as the first probe; retain the pre-M1 root path as a 30-day reader fallback per the workspace contract. Mirror the same M1+fallback pair for the packaged-app workspace so the skill finds the token regardless of checkout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EL2XBcV3RsWhevzTPtW4jw * fix(report-feedback): match real AWS access keys in _redact() (no separator) AKIA keys have no separator between the prefix and body (AKIAIOSFODNN7EXAMPLE), so the shared [_-] pattern never matched them. Split out a dedicated AKIA[A-Z0-9]{16} pattern that fires correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EL2XBcV3RsWhevzTPtW4jw * fix(report-feedback): redact Slack app tokens * test(report-feedback): cover main flow, cloud-auth branches, and logs excerpt Raises diff coverage on report-feedback.py from 35% to 100%, clearing the Coverage Gate on #2010. Adds tests for resolve_workspace (import + fallback), the read_cloud_auth metering-env / dedup / malformed-file branches, logs_excerpt (present / absent / unreadable), and main() (blank-title exit, not-signed-in exit, successful POST with and without logs, HTTPError and generic-error exits) via mocked urlopen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DC4oxV4ZwnKTKvE6Hb28Yo * fix(report-feedback): redact Google API keys --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Qingyun Wu <qingyun0327@gmail.com> * fix(voice+phone): add explicit local-machine identity to prevent cloud fabrication (#2014) * fix(voice): add explicit local-machine identity to prevent cloud fabrication Addresses #2011 — when asked where Sutando runs, Gemini was defaulting to its training-data answer ("cloud-based AI"). Add an authoritative system-instruction line so the model answers correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RUA5TZgDa7VmFcNtPka38t * test: update voice-behavior-anchors fixture for identity instruction Regenerate the instructions hash after adding the local-machine identity line to buildInstructions() (deliberate prompt change for #2011). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RUA5TZgDa7VmFcNtPka38t * fix(phone): add explicit local-machine identity to prevent cloud fabrication Same issue as voice (#2011) — phone agent also lacked an authoritative statement about running locally. Adds the same system-instruction line to the inbound/outbound phone session path in buildPhoneInstructions(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RUA5TZgDa7VmFcNtPka38t * test: update phone-behavior-anchors fixture for identity instruction Regenerate hashes after adding the local-machine identity line to buildPhoneInstructions() (deliberate prompt change for #2011). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RUA5TZgDa7VmFcNtPka38t --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Qingyun Wu <qingyun0327@gmail.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Qingyun Wu <qingyun0327@gmail.com> Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>
What
A
report_issueinline tool so the user can talk to Sutando to file a bug / feature request / feedback ("report a bug…") instead of opening the desktop form.How
/api/feedbackroute the desktop form uses (which mirrors into GitHub issues), withkind/severity/title/body+ a diagnosticcontext(platform, arch, node, and a tail of recent<workspace>/logs).SUTANDO_METERING_ENDPOINT/SUTANDO_METERING_HEADERS) — no new env wiring.inlineTools+ownerOnlyToolsso it appears in both voice and phone agents.Complements the desktop UI enrichment in sutando-desktop#57 (same API, same context shape).
pnpm typecheckpasses.