mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-06 23:35:34 +00:00
327 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
477859bb3f
|
feat(channels): support local gh authentication (#8461)
* feat(channels): support local gh authentication Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(channels): align registry catalog test and visuals with optional GitHub token (#8461) * fix(channels): address review feedback for GitHub local gh auth (#8461) Treat a blank replacement of an optional secret as a clear so an existing GitHub channel can no longer ship an empty or whitespace-only PAT to the daemon. Reuse the shared missing-field predicate in the editor's GitHub credential validation, wrap malformed baseUrl failures in an actionable channel error, and surface sanitized gh stderr in local authentication failures. * fix(channels): address second-round review feedback for GitHub local gh auth (#8461) Pin the whitespace-only token gate, the bounded gh stderr sanitization, and the required-secret blank-replacement guard with mutation-resistant tests. Log the authenticated account identity on channel connect so an out-of-band gh auth switch is visible to operators. Align test secret-source fixtures with the SDK union and complete the design doc's change footprint. * fix(channels): address third-round review feedback for GitHub local gh auth (#8461) * fix(channels): address fourth-round review feedback for GitHub local gh auth (#8461) * fix(channels): address fifth-round review feedback for GitHub local gh auth (#8461) * fix(channels): address sixth-round review feedback for GitHub local gh auth (#8461) * fix(channels): address seventh-round review feedback for GitHub local gh auth (#8461) * fix(channels): address eighth-round review feedback for GitHub local gh auth (#8461) --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
a123d0030a
|
ci(review): prepare evidence-image tooling for GitHub-triggered reviews (#8454)
* ci(review): prepare evidence-image tooling for GitHub-triggered reviews Reviews triggered on GitHub cannot attach images today for three reasons; this wires the two that live in the workflow: - Install tmux and freeze (pinned, checksum-verified) before the review runs. Both are optional by contract — the evidence ladder degrades honestly without them (png -> ans-only -> refused, recorded in the capture manifest) — so the step never fails the review; it only decides which rung the runner can reach. tmux mirrors the tolerant install qwen-autofix.yml already uses; freeze falls back to ~/.local/bin when passwordless sudo is absent. - Pass QWEN_REVIEW_ASSETS_REPO from a repository variable to the review step. Publishing stays OPT-IN by design: with the variable unset the env is empty and publish-assets refuses (parseAssetsRepo trims and rejects empty), so nothing changes until a maintainer sets the variable. When set, evidence images land on commit-pinned pr-assets/<pr>-review branches — already covered by the visuals cleanup workflow — pushed with the same CI_BOT_PAT the step uses. The third reason is release lag: the capture producer (capture-tui, #8388) has to merge and ship in a release before rendering claims can generate images on CI at all. This change is inert until then. * fix(ci): capture-tools step review fixes — enforced tolerance, version pin, cached fallback R1-1: the never-fails contract is now enforced twice — continue-on-error at the YAML level (the belt) and set +e with a trailing exit 0 inside (the suspenders); under the runner's default bash -e several statements (mktemp, install, sudo install with an empty path) could previously abort the step and fail the review the comment promised never to fail. R1-6: probe the VERSION, not just the binary — on a persistent self-hosted runner an installed freeze made any FREEZE_VERSION/SHA bump a silent no-op; the pin now forces a refresh when the cached binary does not match. Cached-fallback fix: put ~/.local/bin on PATH (and GITHUB_PATH) before the probe — a sudo-less runner otherwise re-downloads the tarball on every review run forever. R1-3: the step comment says capture-tui is UPCOMING (#8388, not in the released CLI) and names qwen review drive as today's tmux consumer, so the step cannot be mistaken for stale dead weight and deleted from under the follow-up. R1-4: the retention comment scopes the cleanup-workflow claim to the same-repository designation; a fork or scratch destination manages its own retention (docs updated to match, plus a note documenting the repository VARIABLE a maintainer sets to enable publishing). R1-5: the step's real bash now runs in the workflow behavioural harness under bash -e with stubbed sudo/apt/curl/sha256sum/tar/uname: worst-runner and checksum-reject scenarios exit 0 installing nothing, the no-sudo happy path pins the ~/.local/bin + GITHUB_PATH pairing, and the version-pin probe is pinned from both sides (wrong version re-downloads, matching version skips). Real freeze/sudo on a developer machine are shadowed so the tests are deterministic and can never install to /usr/local/bin. Nit: both sudo guards now check sudo -n true. * fix(ci): capture-tools step review fixes — step-owned tool dir, anchored probe, honest failures Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ci): capture-tools test harness — shadow tmux, don't blank its PATH dir The harness dropped every host PATH directory that ships a tmux so the step's apt branch would depend on the scenario, not on the machine hosting the suite. On GitHub-hosted ubuntu runners tmux lives in /usr/bin, so the filter blanked /usr/bin wholesale — bash, grep, mkdir, and tar included — and execFileSync('bash') died of ENOENT: all seven capture-tools tests failed in the Test (ubuntu-latest Node 22.x) job while passing on tmux-less dev machines. Replace the directory-level drop with an entry-level shadow: each tmux-bearing directory is mirrored (symlinks) into a scratch dir minus the tmux entry, in place, preserving PATH order and the empty-entry stripping the old filter did. Hosts without tmux take the map through unchanged, and Windows (no tmux in its PATH, no symlink branch) keeps its current behavior exactly. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ci): capture-tools test review fixes — faithful stubs, env shape pins, missing-branch scenarios Round-3 review findings: the harness executed several of the step's branches but asserted nothing about them, so probe-verified mutants (dropped tmux guard, deleted warning/degradation messages, malformed or missing FREEZE_VERSION/FREEZE_SHA256, wrong hash variable, dropped URL `v` prefix or curl `-L`, severed tarball paths, broken regex boundary, leaked mktemp dir) all shipped green. - Make the curl/sha256sum/tar stubs model their real contracts: exact pinned URL, pinned checksum over a file curl actually wrote, existing -xzf operand - Pin FREEZE_VERSION/FREEZE_SHA256 shape in captureToolsSource - Pin the full curl flag set and the three-site tarball path agreement - Assert the stale-renderer warning (fires on degraded re-download, silent on the happy path) and the tmux-unavailable message - Pin TMPDIR and assert the mktemp cleanup leaves it empty - Add the two missing scenarios: tmux-present skips apt, cached version extending the pin with a leading digit re-downloads Verified by 13 mutation probes: every named mutant now turns the suite red (13/13 killed), baseline 34/34 green. * fix(ci): capture-tools step review fixes — hash-verified cache, per-run PATH promotion * fix(ci): capture-tools step review fixes — verified-bytes-only installs, step timeout Review findings on the capture-tools step: - Drop the PATH-trust branch: a freeze already on PATH was accepted on its own --version and executed to probe it — exactly the self-report the FREEZE_BIN_SHA256 comment declares attacker-controllable, from dirs writable between jobs on both runner classes. The checksummed download always runs now; the cache makes it free after the first run. - Guard $tools_bin in the download branch: with mktemp failing, the unguarded install resolved to /freeze — harmless unprivileged, but a root-in-container self-hosted runner writes it and reports success with nothing on PATH. - Copy-then-verify the cache: install into the fresh per-run dir FIRST, verify THOSE bytes, delete both copies on mismatch — the verified bytes are the bytes later steps execute, closing the check-then-copy race for free. This makes the separate pre-verify block redundant; it is deleted. - Add timeout-minutes: 5 — continue-on-error bounds failure, not duration, and a stalled `sudo apt-get update` mirror had no other bound under the 300-minute job cap. - Report block: say the resolved freeze is likely broken when its --version produces nothing, instead of echoing a blank line and calling it stale; the mismatch wording is direction-neutral now. Tests: replace the PATH-trust scenario with a planted-PATH one (marker outside the scenario dir proves the plant never executes), add the mktemp-failure scenario (the install stub succeeds like root would, so the unguarded mutant is caught) and the promoted-dir 0700 assertion; re-anchor the two digit-boundary tests on the report's warning. 41/41 green; both fix mutants verified killed. * fix(ci): capture-tools review fixes — stale-dir cleanup, pinned guards Address round-5 review: - R5-1 (Critical): the per-run qwen-review-tools.* dir under RUNNER_TEMP was never removed; RUNNER_TEMP survives across jobs on the shared pool, so every review run accumulated one dir + one Go binary, unbounded. 'Clean stale agent state' now removes stale dirs before the install step creates the current run's dir, matching the qwen-triage.yml convention. The harness comment claiming the dirs were runner-cleaned is corrected. - R5-6: the cache re-verification rejection branch now logs why it deletes the cached binary instead of degrading silently. - R5-7: bump-checklist note beside the freeze pins — the harness stubs key on the same env values, so a transposed hash pair must be caught against the real release artifacts at bump time. - R5-2/R5-3/R5-4/R5-5: four unpinned step properties now pinned (the if: guard, the sudo -n probe flag, install-after-context ordering, and the cache branch's tools_bin guard via a new mktemp-fails scenario); six mutation probes confirm each pin kills its mutant. * fix(ci): capture-tools review fixes — curl budget, swept scratch dir, wiring pins * fix(ci): capture-tools review fixes — harness mutation pins, pin-pair self-check Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ci): capture-tools review fixes — shadow-farm cleanup, backoff budget term * fix(ci): capture-tools review fixes — report probes only installed freeze, age-gated sweep --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
1a2bb10298
|
fix(review): admit evidence images by content, not by name — magic-byte sniffing (#8459)
* fix(review): admit evidence images by content, not by name — magic-byte sniffing The publish-assets allowlist was extension-based, and an extension is a claim anyone can make: combined with a prompt-injected review run, whatever could name a file evidence.png could host up to the size cap of arbitrary bytes at a github.com URL through the evidence push (raised in #8454's review as an enablement consideration). sniffImageFormat reads the four admitted signatures (PNG, JPEG, GIF87a/89a, RIFF+WEBP — RIFF alone is not enough, AVI and WAV share the container prefix) and validateAssetContent rules the content against the format the extension claims, fail-closed: an unrecognized signature refuses even when the extension is allowed. publish-assets applies the ruling to every file's first bytes before anything is uploaded — all-or-nothing, same refusal contract as the other gates. Pinned: the four signatures, truncated/empty headers, the RIFF/AVI near-miss, extension-format mismatch, fail-closed unknown extensions, and end-to-end: a shell script named evidence.png refuses with exit 3 and nothing pushed. * fix(ci): re-pin review timeout tests to vars externalization (#8459) * fix(review): single-source the asset allowlist and pin sniff depth (#8459) Address round-1 review suggestions: - ASSET_EXTENSIONS now derives from EXTENSION_FORMAT, so admitting a format is a one-place change the batch gate and the content gate cannot drift on; the duplicated extension extraction moves into one claimedExtension helper shared by both gates. - Pin what the mutation probes showed unpinned: the GIF87a branch (sniff + admission), uppercase extensions at the content gate, the refusal message direction, the full depth of every signature (near-miss negatives), and the publish-time 16-byte slice end to end (a WEBP publishes through runPublishAssets; verified the test fails when the slice shrinks to 8). * fix(review): pin every sniff check and guard the allowlist lookup (#8459) * fix(review): pin every sniff byte and the two-gate format coupling (#8459) * fix(review): align the two-gates comment with the pin that enforces it (#8459) * fix(review): sharpen evidence-gate diagnostics and pins (#8459) - Content refusals now name the JSON-quoted full path, so two same-named files from different directories are tellable apart (the sibling read-error refusal already spent the path). - Export ASSET_HEADER_BYTES from the lib that owns the sniff depth; the publish call site uses it and the two-gates pin slices canonical headers to it, so a future longer signature fails the pin, not real publishes. - One shared refusal builder keeps the allowlist message identical in validateAssetFile and validateAssetContent. - Narrow the sniffImageFormat threat model to what magic bytes buy: binds the claimed type to the leading bytes, does not stop prefixed payloads. - One-byte-off matrix becomes a labeled it.each table (failures name the exact corrupted byte); imports re-alphabetized. * test(review): pin the shared extension refusal across both asset gates (#8459) * test(review): pin lastIndexOf extension parsing for multi-dot asset names (#8459) * fix(review): admit WEBP by its fourcc and name refused files once (#8459) --------- Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
ac67de2e46
|
fix(review): stop the reverse-audit loop while there is still time to report (#8468)
* fix(review): stop the reverse-audit loop while there is still time to report Measured on CI run #8368 (+1699 lines): the iterative reverse audit ran to its 5-round cap, each round a per-chunk fan-out whose findings then went back through verification, and the loop consumed 3.5 of the job's 4 budgeted hours. The outer GNU-timeout kill arrived while round 5's findings were still being verified. The review died holding every confirmed finding it had; nothing reached the pull request. The loop's rounds are driven by the orchestrator, but every round begins at the same place: agent-prompt building the round's prompts. So the builder becomes the loop's clock. When the environment carries a review deadline (QWEN_REVIEW_DEADLINE_EPOCH, exported per attempt by the review workflow) and the remaining time is inside the reserve kept for the last verification, compose-review and submission (default 60 minutes, QWEN_REVIEW_DEADLINE_RESERVE_SECONDS to override), a reverse-audit round is refused: a BUDGET line on stderr, exit code 4, no prompt built and no record written. The message carries the exact unreviewedDimensions entry to file, so the disclosure that caps the verdict is the CLI's text, and Step 6 proceeds with the findings already confirmed. Local runs have no deadline and are untouched. A malformed deadline fails open — the outer kill still bounds the run, and a broken variable must degrade to today's behaviour rather than wedge every budgeted review at round 1. The verifier is deliberately not gated: the reserve exists so it can run. * fixup: scale the deadline reserve to the externally-chosen budget The budget is not this workflow's to assume: it arrives from a repository variable, a workflow input, or a /review --timeout=N comment. A fixed 60-minute reserve would consume most of a 70-minute budget and refuse the audit loop outright on a 30-minute one. The workflow now passes a reserve of a quarter of the attempt, floored at 10 minutes and capped at 60; the CLI constant remains only the fallback for a caller that sets a deadline without a reserve. * review feedback: admit the round only if IT fits, and cap deterministically Three findings from review, all taken: 1. The gate budgeted for the tail but not for the round it admits — the terminal round is by construction the one that starts closest to the boundary, so the killed-mid-verification failure survived one round wide. The gate now requires remaining >= round + reserve, where the round's cost is the previous round's, measured admission-to-admission from a stamp the builder writes (one per round; a same-round rebuild is not a round), falling back to a 30-minute constant for round 1, which starts with the most headroom. 2. The refusal was deterministic; the disclosure that caps the verdict was prose the orchestrator had to carry. The builder now records a budget-stop marker beside the prompt records and compose-review synthesizes the unreviewedDimensions entry from it — deduped against a relayed copy — so a run that drops the sentence still cannot approve past a truncated audit. 3. Exit code 4 is documented in the command's describe. Also restores the Step 5 bullet the previous commit's edit displaced (new findings merge into the cumulative list before the next round). * review feedback: pin the budget gate's all-chunks refusal and ordering Cover the two behaviours the review noted were only asserted on the bare --findings form: an exhausted budget refuses the loop's real --all-chunks round before ANY of the per-chunk records is written, and a malformed call (--round 0) still gets its validation error first — exit 4 is for a well-formed round the budget refuses, never a replacement error. Also name what the code already does: reserve=0 is the deliberate escape hatch (the gate shrinks to the round estimate alone), and the workflow's 3600s cap mirrors DEFAULT_RESERVE_SECONDS. * docs(review): describe the soft-deadline env vars for time-budgeted runs The review noted the two new variables appeared in no user-facing doc; the reserve in particular is an operator-facing knob. State what each does, the fail-open posture, and how the refusal surfaces in the verdict. * fix(cli): align budget-stop disclosure with the gate's refusal (#8468) A round-1 budget refusal left no reverse-audit records, so the Step 4/5 floor reported the deliberate stop as a rogue/unlaunched audit with a rebuild FIX the same gate deterministically rejects; the refusal's own disclosure was swallowed by the caller-echo dedup. The floor now stands down when the budget-stop marker exists, and compose-review renders the disclosure structurally, bilingually, from the marker. Also: `--role reverse-audit` requires `--round <k>` (an unlabeled admission stamps an entry no estimate can attribute), the budget gate runs after the plan/findings reads (a broken plan or unreadable findings deserves its own error, and nothing is stamped ahead of a buildable call), and the gate's admission boundary, measured-cost behaviour, and the workflow env contract are pinned by tests. * review: a budget stop excuses only the round it refused The budget-stop suppression keyed on the marker's existence alone, so every reverse-audit gap shape went silent once any round was refused — including the shapes that describe rounds which RAN before the budget hit. A hand-written round-1 launch is exactly as undelivered when round 3 later hits the budget, and suppressing its disclosure let 'stopped before round 3' imply the rounds that did run were faithful. Exactly one shape is by design under a marker: not-built — the refusal writes no record, so an audit with no records IS the audit the gate stopped, and its FIX (rebuild the round) would be refused by the same gate. The suppression now names that shape and no other; a rewritten, unlaunched or brief-unread round keeps its disclosure and its repair. The new test pins the operative halves: the verdict stays capped, the marker's disclosure posts, and the operator channel carries the rewritten round's exact repair. (The posted body collapses same-subject disclosures — both say 'reverse audit' — so the author sees the stop; repairs are acted on from stderr, where the rewritten fix rides.) * fix(review): fence budget state per run, and let gate errors beat budget stops Address the round-2 review threads on the reverse-audit budget gate: - Fence budget-rounds.json and budget-stop.json by the plan's own mtime. Every run rewrites the plan at its Step 1 capture, so records older than the plan belong to a previous run of the same PR: a run killed before cleanup no longer prices the next run's rounds off stale stamps (an hours-old stamp read as an hours-long round refused round 1 of a fresh budget) and no longer caps a later run's verdict on a stop that did not happen in it (R2-1, R2-2). - Refuse a structurally unbuildable plan (no chunks[], duplicate or non-integer ids) with its own error ahead of the budget gate, so the same corruption gets the same diagnosis whatever the clock says, and no budget-stop marker is written over a corrupt plan (R2-5). - Stamp a round admitted only after its build succeeds: a build that throws leaves no stamp, so the next round's cost is never measured from a build that produced nothing and floored to 600s (R2-6). - Keep the budget entry's 'reverse audit' subject out of the caller-echo prefix filter: other reverse-audit scopes the orchestrator disclosed (a twice-whiffed chunk from the rounds that DID run) are no longer silently dropped in the marker's shadow; the marker's own relays stay deduped by the phrase splice (R2-7). - Render --round unbracketed in the reverse-audit rebuild fix — the CLI refuses a round-less reverse-audit call, so the paste-and-run repair must not present the flag as optional (R2-14). - Document the deliberate one-verification overlap between the measured round estimate and the tail reserve, at both definitions (R2-13). - Test hardening, each assertion mutation-probed to fail its named mutant: a reshaped relay only the marker-phrase splice dedups (R2-8); the stamp's round label and the verifier's no-stamp invariant (R2-9); whole-line, unit-arithmetic and reserve-cap pins on the CI wiring contract (R2-10); the first-wins stamp survivor (R2-11); the reserve=0 escape hatch (R2-12). --------- Co-authored-by: verify <verify@local> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
48d37cdf70
|
docs: document headless Goal workflows (#8503) | ||
|
|
7dfc554dff
|
feat(review): Add structured Web Shell review results (#8402)
* feat(review): add Web Shell review artifacts Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(web-shell): add code review artifact visual scenario (#8402) * fix(review): address Web Shell review artifact feedback (#8402) * save-artifact: document why paths resolve against the daemon workspace root (QWEN_CODE_PROJECT_DIR) instead of cwd, and cover the relative-path form the skill documents with a test where the two roots differ. * CLI/renderer contract: the renderer hand-duplicates the findings vocabulary and fails closed on unknown values, so name the renderer as a second consumer beside the CLI's lists and check in a contract fixture generated through the real pipeline (validateFindings -> buildReport -> save-artifact) that exercises every source, severity, confidence and outcome. Exporting the vocabulary through the SDK stays deferred: it is a public cross-package API change beyond this PR's seam. * resolve-anchors now validates `line` exactly like `findings` does (positive safe integer); the two validators in one pipeline no longer disagree. Note: an in-flight `.qwen/tmp` findings file carrying `line: 0` fails where it previously did not. * The renderer validates markdownReportPath (relative, no ".." segments, .md suffix) before it becomes a readWorkspaceFile call, resets the severity/confidence filters when switching artifacts, and surfaces heldByMeasurement so a nonzero Held count is attributable. * save-artifact refuses low effort structurally (choices and library guard) instead of by prose, stats the Markdown report before reading it so a directory reports "not a file", and the component no longer shadows the DOM `document` global. * The case-insensitive alias test now skips visibly on case-sensitive filesystems instead of passing vacuously. * Comment the kept `turnOutputs.review` key and document the JSON companion in the user docs. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(review): address second Web Shell review artifact feedback round (#8402) --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
0cb109f513
|
fix(core): Avoid replaying unsafe MCP tool calls (#8387)
* fix(core): Avoid replaying unsafe MCP tool calls Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Revalidate MCP replay after reconnect Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
186812694c
|
feat(review): publish evidence images to a user-designated assets repo (#8351)
* feat(review): publish-assets — evidence images for PR review comments
GitHub's API cannot attach images to review comments (the web UI's
drag-and-drop upload has no API equivalent), so a review whose evidence is a
screenshot — a TUI rendering, a before/after comparison — had no way to show
it. New `qwen review publish-assets` hosts evidence images in a
user-designated repository and hands back URLs a comment can embed.
Grew from the maintainer's manual workflow (screenshots pushed to
`pr-assets/<PR>-verify` branches over HTTPS), and inherits the shape of the
skill's only other public write (`submit`) deliberately:
- Designated destination: writes only to QWEN_REVIEW_ASSETS_REPO, an
owner/repo the user set by hand — the reviewed repo for maintainers, a fork
or scratch repo otherwise (fork-vs-in-repo becomes a configuration
difference, not two code paths). A separate variable from
QWEN_REVIEW_SCRATCH_REPO on purpose: that contract forbids PR-derived
content, and evidence screenshots are exactly that. Unset → exit 3.
- Authorised run: the same args-file re-parse and target binding as submit,
now extracted to a shared lib/authorization.ts so the two gates cannot
drift (the target-binding lesson lives in one place). Since an effective
--comment forces high effort, low/medium runs can never publish.
- Images only, capped, all-or-nothing: extension allowlist (SVG excluded — a
script container), per-file and per-batch size caps, one refused file
refuses the batch before anything is pushed.
- Immutable references: files land on pr-assets/<pr>-review via the Contents
API (HTTPS via gh; no clone, no SSH), content-hash-named so re-runs are
idempotent, and every URL is pinned to the commit — a posted comment's
evidence cannot be changed from under it. The web-host /raw/ URL form works
unchanged on GitHub Enterprise.
- Auditable: a manifest names every file pushed and the landing commit,
swept by cleanup with the other review artifacts.
The findings artifact gains per-finding `assetFiles` (local evidence paths)
and `assets` (published URLs); `publish-assets --findings/--findings-out`
publishes everything referenced and weaves the URLs back in, so the comment
builder reads the artifact rather than hand-carrying URLs.
What the command cannot check is stated in SKILL.md instead: image content.
Publish only evidence the review itself produced — never a capture of the
user's own terminal, which can hold an env dump in the scrollback.
Tests: 45 files / 1394 assertions — new suites for the assets naming and
validation rules and the command's gates (refusal without designation,
refusal without authorisation, target binding, branch creation, idempotent
re-run, batch refusal, findings weaving); submit's 42 pass unchanged on the
extracted gate.
* fix(review): publish-assets round-1 self-review — six findings
Round-1 review of this branch, walked with the angles the author-side pass
does not cover:
- submit.ts kept its parseReviewArgs import after the authorization
extraction; vitest does not typecheck, `tsc --build` does, and CI's build
leg failed on TS6133. (The whole first CI round's failures cascade from
this one break.)
- ensureBranch %2F-encoded the slashed ref path; GitHub's documented form is
literal slashes and %2F routes inconsistently across endpoints — a 404
here reads as "branch missing" and turns every re-run into a 422 on the
create. Ref paths are now literal (the branch name is built from a
validated integer, so interpolation is safe); the contents `?ref=` query
VALUE keeps its encoding, which is the correct position for it.
- The authorization gate bound URL-shaped `--comment` arguments against the
ASSETS repo, refusing legitimately authorised runs whenever the assets repo
is a fork rather than the reviewed repo. The shared gate's repo binding is
now optional — submit still always binds it; publish-assets binds the PR
number (and host) alone, with a new optional --reviewed-repo to restore
the stronger binding when the orchestrator knows the reviewed repo.
- URLs were pinned to the last PUT response's commit.sha; on an
identical-content update that field's shape is GitHub's to decide, not
ours to assume. The head is now read from the branch ref after the
uploads — one extra call for independence from the response shape.
- putContent's catch-all retried EVERY failure through the exists path,
answering a 401 with a confusing secondary error from the sha lookup; the
retry now fires only on the 422/needs-sha shape and rethrows the rest.
- --findings without --findings-out silently skipped the URL weaving; it
now warns, and --findings-out implies --findings.
New tests: literal-ref assertion, non-exists rethrow, URL-shaped
authorisation without assets-repo binding, --reviewed-repo mismatch refusal.
45 files / 1399 assertions green; `tsc --build` clean.
* fix(review): publish-assets round-2 — empty-findings no-op, reviewed-repo hint
Round-2 findings on this branch:
- A findings artifact carrying no assetFiles is the ORDINARY case for most
reviews, but publish-assets answered it with exit 3 — a refusal an
orchestrator calling the command unconditionally on every posting run
would read as a failure to repair. It is now a no-op (exit 0,
{published:false, count:0}); a bare --files with nothing named keeps the
exit-3 refusal, because there the emptiness IS the caller error.
- SKILL.md's example now names --reviewed-repo for URL-target reviews, so
the stronger authorisation binding is used where the orchestrator knows
the reviewed repo.
44 files / 1387 assertions green; tsc --build clean.
* test(review): fix invalid two-argument expect in assets.test.ts
Round-3 sweep: vitest's expect takes one argument — the message-style second
argument was a lint error and a weak assertion both. The offending value now
rides inside the asserted object, so a regression names which shape slipped
through instead of reporting 'expected true'.
* test(review): pin the findings schema's evidence-asset validation directly
Round-4 sweep: assetFiles/assets were exercised only through publish-assets'
weaving test — the schema's own rejection paths (non-array, empty-string
entry, empty-array drop) had no direct case, so a validation regression
would have surfaced as a confusing weaving failure two layers up.
* fix(review): address all six findings from the automatic review (R1-1..R1-6)
The /review pipeline's own round-1 findings on this PR, each confirmed and
fixed:
- R1-1 (the real catch): the host-binding check sat nested inside the
`req.repo !== undefined` guard, so a caller omitting --reviewed-repo also
silently skipped the HOST binding — contradicting the documented "binds
the PR number (and host) alone". The host check now stands on its own;
a new test pins an Enterprise-host mismatch refusal with the repo binding
absent.
- R1-2: --pr accepted whatever yargs `type:'number'` passed through (NaN,
0, 3.5), and --user-authorized bypasses the gate that would have
re-parsed the target — `pr-assets/NaN-review` was reachable. A Gate-0
positive-integer check now refuses first, matching submit's sibling
discipline.
- R1-3: the suite drove the skillArgs seam without clearing
QWEN_CODE_SESSION_ID, so running it inside an active Qwen Code session
spuriously failed eight tests. beforeEach now saves/clears the variable
and afterEach restores it.
- R1-4: the 40MB aggregate cap was enforced inline and untested (a mutation
deleting it stayed green). The per-file rules and the total cap now live
in one pure ruling, validateAssetBatch, unit-tested with five 9MB sizes
and no fixtures.
- R1-5: the asset_files snake_case alias was the one untested member of an
otherwise-tested alias family; pinned.
- R1-6: the setGhHost wiring had no command-level assertion; a GHE test now
pins both the call and the host-carrying manifest URLs.
44 files / 1397 assertions green; tsc --build and eslint clean.
* fix(review): address all ten round-2 findings from the automatic review
Round-2 of the /review pipeline on this PR: 2 Critical, 8 Suggestions,
every one confirmed against the code and fixed.
Criticals:
- The round-2 test block added for the empty-findings no-op omitted the
QWEN_CODE_SESSION_ID save/delete/restore its two sibling blocks perform,
so the suite spuriously failed inside an active Qwen Code session — the
exact dogfooding environment this repo reviews from.
- The gh routing and the returned URLs read the host from two different
sources: with --host absent, gh children inherit an operator-exported
GH_HOST (routing at Enterprise) while rawAssetUrl defaulted to
github.com — every returned URL a 404. One effectiveHost (flag, then
GH_HOST env) now feeds both.
Suggestions:
- putContent's retry discriminator matched a bare `422` anywhere in
err.message — which execFileSync fills with the full command line,
including the PR-numbered remote path: evidence for PR #4220 would read
a 401 as "already exists". Anchored to `HTTP 422`.
- ensureBranch's bare catch read every ref-lookup failure (401, 403
rate-limit) as "branch missing"; only HTTP 404 takes the create path
now, and an empty assets repo — whose default_branch resolves while its
head ref 404s — is named as the condition it is, with the fix stated.
- Validation refusals threw (yargs exit 1, stack trace, empty stdout)
while every other gate in the command answers exit 3 +
{"published": false}; unreadable files and batch refusals now speak the
same refusal language.
- The command's idempotent writes (content-hashed PUTs, a ref create
whose duplicate is tolerated) now go through a new ghWithInputRetried —
sharing gh()'s transient-error retry — and ghWithInput's no-retry
docstring names the two-caller split instead of claiming a sole caller.
- parseAssetsRepo admitted dot-segment repos (`owner/..`) its docstring
claimed were path-safe; segments now exclude `.`/`..`, mirroring
submit's isRepo.
- stringArray accepted whitespace-only evidence paths; trim(), matching
the sibling asString.
- The GHE test asserted setGhHost was called but not WHEN; it now asserts
the call precedes the first API invocation.
44 files / 1403 assertions green; tsc --build and eslint clean.
* refactor(review): one refusal helper for every publish-assets gate
Round-2 of this branch's fresh review: the refuse() helper existed below
seven inline copies of the identical three-line refusal — the drift shape
where one site eventually forgets the exit code. Hoisted to the top of
runPublishAssets and used by every gate; message content unchanged where
tests pin it. 26/26; tsc clean.
* fix(review): address the round-3 review — bidirectional host binding and 14 more
The automatic review's third round on this PR: 1 Critical + 14
Suggestions, each verified and addressed.
The Critical (host binding, both halves):
- The gate's `req.host &&` guard bound the host in one direction only —
an Enterprise-URL authorisation admitted a host-less write routed at
github.com (or wherever GH_HOST pointed). The gate now compares the
authorised host against the write's EFFECTIVE host, defaulting an
absent req.host to github.com: a host is a host, not an exemption.
- Both callers fed the gate the flag rather than the route: publish-assets
computed effectiveHost (--host ?? GH_HOST) AFTER the gate and bound
args.host; submit bound args.host while its gh child inherited GH_HOST.
publish-assets now resolves effectiveHost before Gate 2 and binds it;
submit binds the same resolution.
The rest:
- pr-assets/<N>-review registered in the asset-branch cleanup workflow,
per its own every-producer-must-be-added-here rule — a branch nothing
deletes is permanent.
- ghWithInputRetried had been inserted between ghWithInput and its JSDoc,
leaving the does-NOT-retry comment attached to the function that DOES
retry; each function now carries its own doc.
- putContent's retry-path contents-GET is wrapped: when the 422 was not
the sha-missing shape and the path does not exist, the GET's 404 no
longer replaces the PUT error the user needs.
- stringArray treats null as absent like every sibling parser, so an
artifact rendering "no assets" as null canonicalizes instead of
crashing.
- Test isolation, all four describe blocks: GH_HOST save/delete/restore,
setGhHostMock.mockReset (a sibling's persistent throwing implementation
survives mockClear — the malformed-host test also switched to
mockImplementationOnce), and full mock resets in the blocks that lacked
them.
- The two regression-pin tests the review measured vacuous now
discriminate: each fails only the one call under test and asserts the
pipeline stopped THERE (no contents PUT after a bad create; exactly one
gh call after a 403 lookup).
- New positive pins: a double-fired create ("Reference already exists")
succeeds; the canonical report shape this command's own --findings-out
writes round-trips; an Enterprise-URL authorisation refuses a host-less
write while a github.com-URL one passes it.
Not changed: the finding that reverting the Finding-interface hunk leaves
tests green — the fields are type-level and their removal fails
tsc --build (the CI leg that caught this branch's own TS6133); a runtime
pin would duplicate what the round-trip tests already exercise.
47 files / 1495 assertions green; tsc --build 0 errors; actionlint clean
on the cleanup workflow.
* fix(review): address the round-4 review — empty-GH_HOST passthrough and four test pins
Round 4 came back COMMENTED (down from CHANGES_REQUESTED), 5 Suggestions,
0 Criticals — all five confirmed and fixed:
- An exported-but-empty GH_HOST ("" from an unset workflow var) survives
`??`, being non-nullish: effectiveHost became "" and the gate compared
the authorised host against "", refusing a legitimate github.com write.
Both call sites now collapse an empty trim to undefined (`|| undefined`,
parenthesized).
- The gate's URL-shaped repo/host binding was exercised only via
publish-assets' suite; submit.test.ts now pins both directions of the
host binding and the repo binding at its own call site.
- ghWithInputRetried had no retry-contract test; gh.test.ts adds the
symmetric block to ghWithInput's does-NOT-retry pin (transient 500
retried once then succeeds; non-transient 401 single call).
- The publish-assets mock aliased ghWithInput and ghWithInputRetried to
one mock, hiding which variant a write used; they are two mocks now,
and the happy path asserts the non-retrying variant is never touched.
- The Prepared interface's dead `name` field is gone.
46 files / 1476 assertions green; tsc --build and eslint clean.
|
||
|
|
09d818867e
|
fix(review): follow output language in Tip lines and saved reports (#8370)
* fix(review): follow output language in Tip lines and saved reports The /review skill's critical rule 2 already states that terminal output follows the user's output language preference, but three areas lacked explicit guidance, causing the model to output them in English even with a Chinese output language configured: - Follow-up Tip lines (e.g. "Tip: type post comments to ...") were specified as English templates with no translation instruction - The Step 8 saved report file used English section headings and prose - Step 6 labels (e.g. "Balanced review (effort: medium)") had no translation note Add explicit output-language guidance at each point of use, with a Chinese example for Tip lines and Chinese section heading examples for the saved report. Command keywords (post comments, fix these issues) stay verbatim since they are trigger phrases the user types back. * fix(review): disambiguate findings-artifact language boundary and complete low-effort translation notes (#8370) --------- Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> |
||
|
|
eea0a2b3b2
|
fix(github-channel): recover interrupted inbound tasks (#8306)
* fix(github-channel): recover interrupted inbound tasks * fix(github-channel): make inbound recovery bounded * test(github-channel): cover delivery-failure lifecycle and audit-hit recovery Add a direct test for the onTaskLifecycle failed/delivery -> reply_pending transition and a recovery test for the publication-audit match-and-remove path. Restore the blank line between the constructor and createInitialCursor. * fix(github-channel): preserve cancelled inbound tasks and fail closed on bookkeeping (#8306) * fix(github-channel): harden inbound task lifecycle against partial persistence failures (#8306) * fix(github-channel): close crash-window duplicates and make tests load-bearing (#8306) * test(github-channel): add recovery test for suppressed audit outcome (#8306) * fix(github-channel): persist errorCommentPosted after post, add review test coverage (#8306) --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
e967cc9037
|
docs: document compaction and image model selection (#8348)
* docs: document auxiliary model selection * docs: align model option labels * docs: clarify image model HTTPS requirement |
||
|
|
8d6d2ab56a
|
feat(cli): /summary supports custom export path (#8116)
* feat(cli): /summary supports custom export path (#8113) `/summary` now accepts an optional path argument, matching `/export`'s behavior. When a path is provided, the summary is saved there instead of the default `.qwen/PROJECT_SUMMARY.md`. - `/summary` → saves to `.qwen/PROJECT_SUMMARY.md` (unchanged) - `/summary docs/summary.md` → saves to `docs/summary.md` - `/summary /absolute/path/summary.md` → saves to absolute path - `/summary docs/` → saves to `docs/PROJECT_SUMMARY.md` If the path is a directory (existing or ending with `/`), the default filename `PROJECT_SUMMARY.md` is appended. Parent directories are created automatically. * fix(cli): summary custom path dir detection and i18n key (#8116) * test(cli): assert relative display path in summary tests (#8116) * fix(cli): summary path containment, early validation, and mkdir hardening (#8116) * fix(cli): defer summary mkdir to save time so failed generation leaves no empty dir (#8116) * fix(cli): normalize path separators in summary test and assert file content (#8116) * test(cli): assert directory permission mode in summary test (#8116) * fix(cli): resolve symlinks in summary path containment check (#8116) * fix(cli): reject broken symlinks escaping project root in /summary (#8116) * fix(cli): guard /summary overwrite and expand tilde in path (#8116) * fix(cli): re-validate appended default filename for symlink escape in /summary (#8116) * fix(cli): harden /summary symlink chain walk, file mode, and overwrite guard (#8116) * fix(cli): address review feedback on /summary custom path (#8116) - Fix CRLF false-negative in overwrite guard by normalizing line endings - Allow overwriting empty pre-created files (zero-length bypass) - Detect trailing separator on existing file and report clearly - Log chmod failures via debugLogger matching exportCommand convention - Add comment explaining mkdir mode asymmetry - Update docs: /summary usage table and custom-path welcome-back note - Add i18n key for trailing-separator error in all 9 locales - Add tests for CRLF, empty file, and trailing separator cases * fix(cli): address review feedback on /summary custom path (#8116) - Skip symlink-escape check for the default .qwen/ target so a symlinked .qwen/ directory (shared team config, overlay mounts) keeps working, and the check no longer runs after the LLM call - Re-run the overwrite guard immediately before writing to close the TOCTOU window across the slow generation step - Determine isDefaultTarget by comparing the resolved path against the default so `/summary .qwen/` gets the same 0o700 permissions - Only chmod 0o600 on file creation; preserve existing permissions on regeneration - Return empty content in interactive-mode errors to avoid double rendering (failInteractive already adds the error to history) - Tighten the overwrite-guard regex to require `**Update time**: ` after the Summary Metadata heading, preventing false positives - Fix the realpathNearestExisting comment to document the missing containment-during-walk guard vs export/stats copies - Add tests: symlink cycle, default target with symlinked .qwen, TOCTOU overwrite guard, explicit .qwen/ permissions, chmod preservation, interactive error content, regex false-positive * fix(cli): address review feedback on /summary custom path (#8116) * test(cli): cover post-LLM symlink re-check and interactive error UI (#8116) * fix(cli): address review feedback on /summary custom path (#8116) --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com> Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
dbb0349351
|
feat(review): borrow recall, a fix loop, and a size-derived budget from Claude /review (#8315)
* feat(review): borrow recall, a fix loop, and size-derived budget from Claude /review Three changes, from a comparison of this skill against Claude Code's `/code-review`. The orchestration half of that comparison went the other way — nothing there has the worktree isolation, the transcript-backed coverage gate, or the single computed verdict — so what is borrowed is what it does better: how much it surfaces, what a finding *is*, and what a small diff costs. Recall ------ The Exclusion Criteria are a filter on what KIND of thing is a finding. Read as a confidence bar — which is how an agent under "silence is better than noise" reads them — they license dropping anything half-believed, and that drop is invisible: no later stage sees a candidate that was never filed. Every stage after the finders removes wrong findings; none can add a missing one. Each finder brief now carries the counterweight explicitly, and the Step 4 verifier deliberately does not get it. Code quality was one agent holding six unrelated checks — the shape this skill already refuses for invariant agents, on measured evidence (PR #6457: one agent with an eight-item checklist found 1 of 5 defects; the same model split three ways found all 5). Split into 3a reuse/duplication, 3b altitude/abstraction fit, 3c consistency/clarity. 12 -> 14 agents in 3A. Low was one undirected pass capped at 8, and its only alternative was a nine-subagent fan-out. It is now an angle rotation in one context — line-by-line, removed behaviour, language pitfalls, wrapper routing, reuse/dead code, sibling consistency, then a gap sweep — dedup-only, no re-judging, cap 10. Still zero subagents. --fix and findings as data -------------------------- `--fix` is `--comment` reflected and gated on the opposite target: `--comment` writes to a pull request, `--fix` writes to a working tree, so a PR review (whose tree is the ephemeral worktree Step 9 deletes) ignores it with a warning. An effective `--fix` floors the effort at medium — editing the user's files on an unverified finding is the same mistake as posting one. New `qwen review findings` canonicalizes the findings into a JSON artifact the terminal report, the saved report and the review JSON all read, instead of three transcriptions of one list. With `--outcomes` it merges the fixer's ledger and REFUSES one that does not account for every finding: a fixer that applies six of nine and reports six has not lied about any of them, it has silently shortened the list. Size-derived budget ------------------- New `plan.budget`, computed from srcDiffLines the way the topology gate is and recorded in the plan rather than passed as a flag, so every reader sees one number. Scopes the low tier's angle count and sweep, the Agent 8 ceiling (0 below 80 source lines — "one domain dominates" is a judgement, and one made about forty lines finds a dominant domain every time), and the verify shard width. It never scales a dimension away: that is the roster's answer and the roster reads effort. Not included: per-model prompt routing. Claude's table exists because it was measured per model family; shipping an invented mapping into this skill is the kind of change its own review would flag. Tests: 39 files, 1215 assertions. * docs(review): align counts and level descriptions left stale by the 3a/3b/3c split Round-1 self-review findings on this branch: - SKILL.md medium tier still named 'quality (Agent 3)'; the Step-1 low bullet and Step 3C heading hardcoded six angles though plan.budget scales them 3-6; and no fallback was stated for a plan written by an older CLI that carries no budget field (falls back to the flat pre-budget behaviour — more coverage, never less). - DESIGN.md still labelled the 12-agent roster '(current)', kept the ten-lens / crosses-twelve topology arithmetic beside the updated fourteen-agent copy, keyed the re-gating cost row to the 12-agent roster, and described low as one pass ≤8 and medium as unverified inline angles — both contradicting the SKILL.md this PR ships. The LLM-call-budget and Fork-Subagent sections were still summed for 12 agents. - findings.ts: validateFindings accepted outcome but dropped outcomeNote, so the canonical artifact did not round-trip — a skipped finding fed back through --input kept its outcome and lost its reason. Tests: 40 review files green, including two new round-trip cases. * docs(review): one id per finding across the cache ledger and the findings artifact The rebase onto #8218 left two id schemes for one finding: the incremental cache's cross-round ledger names findings R<round>-<n>, while the findings artifact accepted any unique id. Same defect, two names, and the outcome ledger and next round's report could no longer be joined. The artifact now uses the R-ids whenever the run writes the cache ledger. Conflict resolutions from the rebase itself: review.ts keeps both new subcommands (test-delta from main, findings from this branch); the documentation-parity check #8218 added to the old Agent 3 brief lands in 3c, the consistency slice that owns sibling-parity checks. * test(cli): add findings to the pinned review subcommand surface review.test.ts pins the exact subcommand list and sits one directory above the review/ glob the branch's local runs used, so the new findings subcommand never met it until CI. Ubuntu was the only matrix leg that ran. --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
8673151ebd
|
docs: document skill learning and live reload (#8298) | ||
|
|
6f8ad2b4a5
|
feat(review): Include CLI version in attribution (#8294)
* feat(review): Include CLI version in attribution Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(review): Decouple footer test from package version Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
e569734a1e
|
feat(skills): add auto-skill curator (#7846)
* feat(skills): add auto-skill curator * fix(i18n): translate /curator command descriptions for zh and zh-TW The strict-parity locale coverage test failed because the new /curator command and its status/run/restore subcommands fell back to English descriptions in zh-CN and zh-TW. Add Simplified and Traditional Chinese translations for the curator command descriptions and its user-facing output strings. * fix(i18n): add English source keys for /curator command The strict-parity i18n check failed because zh and zh-TW defined the new /curator command keys while en.js (the source of truth) did not, producing extra-key parity errors. Add the matching English source entries so all supported locales share the same key set. * fix(skills): align curator lifecycle safeguards * fix(skills): harden curator trust and name guards * fix(skills): guard curator mutations by workspace trust * test(skills): cover curator stale-to-active reactivation path * test(skills): cover curator rollback and restore-collision paths Add coverage for two previously untested error paths in the auto-skill curator: - restoreArchivedAutoSkill refusing to overwrite an existing active directory, leaving both the reused directory and the archived copy intact. - runAutoSkillCurator rolling back an archive rename when the post-move state persistence fails, returning the skill to the live library and leaving nothing stranded in the archive (new isolated test file that mocks atomicWriteJSON to fail once). * test(cli): cover curator command errors and stacked auto-skill usage - Add mockRejectedValue error-path tests for the /curator status, run, restore, and pin commands, asserting each surfaces messageType 'error' and that a failed run/restore skips skill-discovery refresh. - Add positive stacked auto-skill tests to both the non-interactive and interactive slash-command paths, asserting recordAutoSkillUsage is called once per successful stacked skill carrying project skillDetail. * fix(skills): reject control-byte auto-skill directory names isManagedDirectoryName only checked the auto-skill- prefix and basename, so a crafted directory whose name embeds ANSI/control bytes was treated as a managed skill and its name printed verbatim by the non-interactive /curator output (which, unlike the TUI, does not run escapeAnsiCtrlCodes), enabling terminal control-sequence injection from a cloned repo. Require the directory name to match SKILL_NAME_PATTERN. A managed dir is always auto-skill-<name> where <name> passes validateSkillName and the prefix chars are within the same charset, so this never rejects a legitimately generated directory (including Unicode skill names, which an ASCII-only guard would wrongly drop) while excluding ESC/control bytes. Add a regression test covering a crafted directory with a valid manifest name so only the directory-name guard can exclude it. * fix(skills): guard curator state reads and clarify restore errors Align the curator state read path with the noFollow/lstat guards every write already uses: refuse a symlinked or non-regular-file state file (which could otherwise be followed to an external path, /dev/zero, or a FIFO, causing OOM or a boot hang in untrusted workspaces) and cap the read size. Also distinguish a present-but-ineligible archived skill from a missing one in restore error messages. * fix(skills): close curator TOCTOU reads and preserve seeding baseline Address review feedback on the auto-skill curator: - readManagedSkill previously read the manifest via Promise.all([lstat, lstat, readFile]); the readFile ran concurrently with the lstat guards, so a symlinked SKILL.md pointing at /dev/zero could start an unbounded read before the guard rejected it. Read the manifest with O_NOFOLLOW + an fstat size bound instead (shared readRegularFileNoFollow helper), refusing symlinks atomically and bounding the read. - readState had the same lstat->readFile TOCTOU window; the O_NOFOLLOW read closes it while keeping the existing friendly error messages. - First-run seeding overwrote firstSeenAt/lastActivityAt with now even when recordAutoSkillUsage had already created a record, resetting the inactivity clock. Preserve an existing baseline (like useCount/pinned/ lastUsedAt), while a brand-new skill still gets a fresh now baseline. Adds regression tests for the seeding-baseline preservation and for refusing a symlinked manifest. * fix(core): harden auto-skill curator per review feedback (#7846) - Resolve node:fs constants lazily so importing the curator does not crash tests that mock node:fs without a constants export. - Preserve the original error via cause when a rollback also fails. - Apply the skill-name charset guard to archived directory names reserved in the review-agent task prompt. * test(core): cover curator restore rollback and re-read guard (#7846) * fix(core): record auto-skill usage on re-invocation (#7846) * fix(skills): preserve curator read failures * fix(skills): harden curator lifecycle guards * test(skills): cover curator safety paths * fix(skills): address curator review findings (#7846) - Ignore future manifest mtimes in lastActivityMs so a bogus timestamp cannot make a skill permanently un-curatable - Skip archived status entries whose directory is also live, preventing contradictory double-listing in /curator status - Check the weekly interval before acquiring the cross-process lock in maybeRunAutoSkillCurator so most boots skip the lock entirely - Use handle.readFile() instead of a single handle.read() to avoid silent truncation on short reads * fix(cli): localize curator messages * test(skills): cover curator usage safeguards * fix(skills): address curator review feedback (#7846) - Isolate per-skill rename failures so a transient error no longer aborts the whole pass and prevents state persistence (boot-loop fix) - Make usage recording fire-and-forget (void instead of await) since it is already best-effort and nothing consumes the result - Skip state file creation when no auto-skills exist - Prune dead records whose directory exists in neither root - Sanitize user-supplied directory names in error messages (JSON.stringify) to close the ANSI control-sequence echo path - Split reserved skill names into active/archived lists in the review-agent task prompt - Make collision output actionable with remediation guidance - Fix rollbackMoves mutating its argument (moved.reverse → copy) - Add null guard to isMissing for non-object rejections - Add locale keys for skippedErrors output (9 locales) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com> Co-authored-by: Qwen Autofix <qwen-autofix@users.noreply.github.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> |
||
|
|
907c7dea70
|
fix(cli): stabilize thinking block height, replace transcript overlay with inline Ctrl+O toggle (#8077)
* fix(cli): hide streaming thinking preview, rebind Ctrl+O to inline fullDetail toggle The streaming thinking block showed a 4-line preview that varied in height due to empty lines in the model's reasoning output, causing constant page reflow and flicker during generation. Changes: - ThinkBody now renders nothing when collapsed (both streaming and committed), keeping the block at a stable 1-line header height. - Ctrl+O now toggles inline fullDetail mode (like Claude Code): all thinking blocks, tool groups, and tool results expand/collapse in the main conversation view — no alternate-screen overlay. - Alt+T preserved as hidden shortcut (same toggle, not shown in UI). - MainContent passes fullDetail to HistoryItemDisplay via the existing ThoughtExpandedContext, so the toggle works in both VP and Static rendering paths. - Removed TranscriptView overlay rendering, transcriptItems memo, StreamingContext import, and EMPTY_HISTORY_ITEMS constant. - Removed dead code: tailVisualLines, grow-only height tracker, MAX_STREAMING_THINKING_VISUAL_LINES, openTranscript callback. * refactor(cli): remove orphaned transcript full-detail infrastructure (#8077) * fix(cli): update Ctrl+O help text and add thinking-expansion integration test (#8077) * fix(cli): update docs, help text, and remove transcript dead code (#8077) * fix(cli): strengthen Ctrl+O full-detail tests and refresh stale docs (#8077) * fix(cli): address review feedback on test isolation, dead i18n keys, and missing negative case (#8077) * fix(cli): add M1 mutation-killing assertion to Ctrl+O test (#8077) The existing test only asserted clearTerminal (a refreshStatic side effect) and never verified the setThoughtExpanded state flip. Under the vi.mock('ink') harness the mocked App never re-renders from a directly-called handler, so a behavioural allExpanded assertion is not possible. Add a structural guard on the handler source that fails when setThoughtExpanded is removed (mutation M1 verified). * fix(cli): strengthen Ctrl+O toggle assertion and document non-VP redraw (#8077) Tighten the structural M1 assertion from .toContain to a regex that matches the (prev) => !prev updater pattern, catching mutations like (prev) => true that the old check would miss. Document the non-VP scrollback redraw in keyboard-shortcuts.md per maintainer request. --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com> Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
412eae24b4
|
feat(core): add project-level fork profiles (#8148)
* feat(core): add project-level fork profiles * fix(core): harden fork profile loading --------- Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com> |
||
|
|
4dc50b18e9
|
feat(memory): protect pinned files during forked Dream (#7714)
* feat(memory): protect pinned files during forked Dream * fix(memory): harden pinned path protection * perf(memory): avoid repeated pinned path resolution * fix(memory): protect pinned memory during extraction --------- Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
e3479a6251
|
docs: worked example for a PreToolUse HTTP hook backed by an external judgment service (#8202)
* docs: worked example for a PreToolUse HTTP hook backed by an external judgment service The existing remote-security-check config example points at a service that has to already exist, without showing what that service actually looks like. Adds a minimal, stdlib-only, runnable adapter (invinoveritas /review as the judgment source) implementing the exact contract PreToolUse HTTP hooks expect -- verified live against the real production API, not just written to look plausible: a genuinely destructive shell command returns permissionDecision: "deny" with a real explanation, a benign one returns "allow", and the adapter fails open on any judgment-service-side error so an outage never blocks legitimate tool calls. * docs: address review feedback -- disclose affiliation, note swap point clearly * docs: fix timeout mismatch, add data-handling note, make backend URL configurable, log fail-open state --------- Co-authored-by: babyblueviper1 <babyblueviper1@users.noreply.github.com> |
||
|
|
cf547b6a3c
|
feat(hooks): add SessionDelete event (#8059)
* feat(hooks): add SessionDelete event * test(hooks): cover SessionDelete failure paths * fix(hooks): standardize SessionDelete failure handling * fix(hooks): include session id in SessionDelete hook failure logs (#8059) * docs(hooks): note that transcript_path is empty over ACP for SessionDelete (#8059) * refactor(cli): deduplicate SessionDelete hook dispatch (#8059) * fix(cli): keep SessionDelete hook dispatch ACP-safe * fix(cli): align SessionDelete short description with other references (#8059) --------- Co-authored-by: 欢伯 <ri.xur@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> |
||
|
|
c50137cf86
|
fix(core): prevent subagents from asking users (#8219) | ||
|
|
153d781a34
|
feat(gitlab-channel): add transient 👀 award emoji while agent is working (#8119)
* feat(gitlab-channel): add transient 👀 award emoji while agent is working Adds a working-reaction feature to the GitLab channel adapter, mirroring the GitHub adapter's eyes reaction (PR #8061). When the agent starts processing a note mention, a 👀 award emoji is added to the note; it is removed when the run completes, fails, or is cancelled. Both operations are best-effort and never block the response. Also replaces the custom Todo interface with gitbeaker's TodoSchema, introduces GitlabTarget to consolidate target info, and simplifies processTodo/buildMetadata signatures by passing the parsed target directly instead of redundant targetType/threadId parameters. Co-Authored-By: Qwen Code <noreply@alibaba.com> * fix(gitlab-channel): guard null target and fix reactions leak Restore null guard for todo.target (dropped in the TodoSchema refactor) to prevent TypeError when GitLab returns a todo without target. Use try/finally instead of try/catch for reactions cleanup so entries are removed on all handleInbound return paths, not just throws. Co-Authored-By: Qwen Code <noreply@alibaba.com> * test(gitlab-channel): add poll-driven reaction tests, fix fragile sequencing Add ReactingGitlabChannel that drives real pollOnce → handleInbound → onPromptStart/onPromptEnd path, covering #note_ parse, key derivation, and finally cleanup. Replace the tautological description-mention test with one that exercises the real isNoteMention guard. Replace bare Promise.resolve() microtask waits with vi.waitFor in the award-failure test. Co-Authored-By: Qwen Code <noreply@alibaba.com> * test(gitlab-channel): pin double-award guard with dedup test Add test that calls startPromptForTest twice on the same messageId and asserts award is called exactly once, pinning the `|| entry.award` guard in onPromptStart against surviving mutations. Co-Authored-By: Qwen Code <noreply@alibaba.com> --------- Co-authored-by: Qwen Code <noreply@alibaba.com> |
||
|
|
9a4e924cf1
|
fix(github-channel): retry definite no-write deliveries (#8087)
* fix(github-channel): retry definite no-write deliveries * fix(github-channel): preserve concurrent pending deliveries * fix(github-channel): harden pending retry updates * fix(github-channel): avoid duplicate pending retries on reconnect * fix(github-channel): audit recovered deliveries before cleanup * fix(github-channel): avoid duplicate recovered comments * fix(github-channel): skip malformed audit entries * fix(github-channel): bound pending retry recovery * fix(serve): restore session service import * fix(serve): remove unused session service import * fix(github-channel): harden pending delivery recovery * docs(github): update channel state paths * fix(github-channel): audit ambiguous pending retries * fix(github-channel): guard legacy state migration * fix(github-channel): avoid pending delivery id collisions |
||
|
|
079ce5346a
|
feat(agent): add fork tool execution allowlist (#8066)
* feat(agent): add fork tool execution allowlist * fix(agent): address fork allowlist review feedback --------- Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
953c9d8177
|
feat(core): tag UserPromptSubmit hook context and record display provenance (#7956)
* feat(core): tag UserPromptSubmit hook context and record display provenance UserPromptSubmit additionalContext was appended to the request as a bare text part and persisted verbatim, so hook-injected text was indistinguishable from user-authored text in the transcript, polluted resumed sessions, telemetry, and auto-memory recall queries. - Wrap injected context in a reserved <qwen:user-prompt-submit-context> tag (hook output already escapes angle brackets, so the tag cannot be forged from inside). - Record the pre-injection user prompt as systemPayload.displayText plus the injected string as hookContext on the user record; the model-bound message stays verbatim for faithful resume replay. - Use the pre-injection prompt text for telemetry prompt attributes and managed auto-memory recall. - Resume projection prefers displayText, strips a trailing whole-part tagged block when no payload exists, and leaves legacy bare-injected records unchanged. - Apply the same tag wrapping on the ACP session injection path, which already records the pre-injection prompt. Closes #7940 Co-authored-by: Cursor <cursoragent@cursor.com> * docs: note UPS promptText TDZ ordering and sole-part resume guard Document the conflict-resolution constraint that promptText must be declared before the injection assignment, and the sole-part read-path guard that keeps a user-authored whole-tag message intact. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cli): cover at_command resume with tagged UPS context Confirm the at_command branch still prefers payload.userText when a paired user record carries a trailing tagged hook-context part, and falls back to the tag-stripping projection only when userText is absent. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(core): address PR 7956 review findings and Goal recording spy Omit the optional UserPromptRecordPayload third arg when no hook injected, so Goal admission spies expecting two args stay exact and CI client-goal.test.ts passes. Project plain UserPromptSubmit-augmented records through transcript-replay with the same displayText / trailing-tag strip fallback as the TUI, covering ACP/export surfaces. Strengthen the displayText preference fixture so it disagrees with the tag-strip path, and use the named UserPromptRecordPayload type in resume. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(acp-bridge): import UPS tag helper via Node-free package export transcript-replay is inlined into the browser daemon/transcript SDK bundle. Importing isUserPromptSubmitContextPartText from the core package barrel pulled the whole Node-bound core graph into that bundle and failed CI (esbuild Could not resolve "node:*") across Test, web-shell E2E, and Real daemon E2E. Export the pure helper as @qwen-code/qwen-code-core/userPromptSubmitContext and import that path instead. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(test): alias userPromptSubmitContext for Vitest source resolution CLI and acp-bridge Vitest configs already map goalWire/transcriptRecords to TypeScript sources; without the same alias the new package export fails import analysis and breaks dozens of CLI suites. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(acp-bridge): keep images when projecting displayText user records Preferring UserPromptSubmit displayText previously returned early and skipped projectMessageParts, dropping multimodal inlineData. Rebuild parts so displayText replaces text while images keep their order. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(core): drop unused hookContext and cover image-only displayText UserPromptRecordPayload.hookContext had no read sites; keep displayText only and recover injected text from the tagged message part. Also cover the image-only !replaced append path and simplify the recording guard. Co-authored-by: Cursor <cursoragent@cursor.com> * test: cover remaining UserPromptSubmit provenance Suggestions Share stripTrailingUserPromptSubmitContextPart between TUI resume and ACP replay, assert ACP Session tags additionalContext, and lock telemetry to the pre-injection prompt text. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
f005f3eee8
|
feat(github-channel): add transient working reaction (#8061)
* feat(channels): acknowledge GitHub requests * fix(channels): remove transient GitHub reactions * test(github-channel): cover reaction failures * test(github-channel): cover pending reaction dedup * docs(github-channel): clarify final-only output --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
0a9db38221
|
feat(review): add review run — headless review with a machine-readable verdict (#7983)
* feat(review): add `review run` — headless review with a machine-readable verdict The review pipeline already runs non-interactively: `qwen --prompt "/review …"` expands the bundled skill, launches the dimension agents, and honors the approval mode. What that path lacks is a contract. The verdict lives in the model's prose and in files whose names the caller must simply know, the exit code says nothing about the outcome, and piped stdin silently defeats slash-command detection (the runner prepends piped input, so the leading `/` is no longer first). Anyone who wants "run a review, tell me what it decided" ends up scraping a terminal. `qwen review run [target]` is that contract and nothing more. It assembles the /review invocation from typed flags (--effort, --comment), re-enters this build's own CLI in a child process with stdin closed, streams the child's progress to stderr, and then reads the verdict from the artifact compose-review wrote — the same JSON the skill treats as the verdict authority — never from anything the model printed. stdout carries only the result (human lines, or the full JSON with --json). Exit codes make the outcome scriptable without parsing: 0 = the review completed (whatever it decided), 1 = it never reached a verdict (child failure, timeout, or no composed artifact — a clean child exit without one is a run that wandered off, not an approve), 3 = completed AND --fail-on request-changes AND the event is REQUEST_CHANGES, so a CI gate can tell "blocking verdict" from "the tool broke". Artifact discovery is scoped to this run (mtime cutoff with a small slack for coarse filesystem clocks): a stale composed JSON from an earlier review says whatever THAT review decided, which is exactly the wrong thing to republish. * fix(cli): harden review run against EPIPE, target injection, and drift (#7983) - Use writeStderrLineSafe in the timeout and spawn-error handlers and guard the progress stream, so an EPIPE on stderr can no longer skip the child kill, hang the promise, or orphan the review. - Reject a review target carrying whitespace or a leading dash before it is re-tokenized by the child CLI (e.g. `123 --comment` silently authorising posting). - Constrain --approval-mode to the same choices as the top-level CLI. - Capture the child's exit signal and surface it (OOM/SIGKILL vs spawn fail). - Sync the top-level `qwen --help` review description with the command. - Register `run` in the review.test.ts subcommand expectation and add tests for the timeout branch, the readComposed guard, and target rejection. * fix(cli): kill process group on review run timeout, harden edge cases (#7983) The CLI relaunches itself in a child process (for --max-old-space-size), so child.kill() only reached the relaunch wrapper — the real review was reparented to PID 1 and kept burning API calls. Spawn with detached:true and kill the process group (-pid) so the timeout actually terminates the review. Also: clamp negative --timeout-minutes to a 1-minute floor, distinguish a corrupt composed artifact from a missing one in human-readable output, and add test coverage for the default (non-JSON) output path. * fix(cli): use specific MockInstance type for process.kill spy (#7983) * fix(cli): capture review run verdict before cleanup, forward signals (#7983) * fix(cli): reject quoted review targets, pin signal forwarding (#7983) * fix(cli): keep captured review verdict when timeout fires after compose (#7983) --------- Co-authored-by: verify <verify@local> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: qwen-code-bot <qwen-code-bot@users.noreply.github.com> |
||
|
|
3d5924bd2f
|
fix(github-channel): validate and document reasonFilter (#8035)
* feat(github-channel): add reasonFilter config to skip unwanted notification reasons Adds an optional `reasonFilter` allowlist to the GitHub channel config. When set, notifications whose `reason` is not in the list are skipped before any lane dispatch, reducing unnecessary API calls and agent work for notification types the operator does not care about. - New `reasonFilter?: string[]` field on `GithubConfig` - O(1) Set lookup (`reasonFilterSet`); undefined = no filter (all reasons) - Early-skip in the poll loop, before subject URL extraction and lane dispatch - Two tests: filtered reasons skipped, unset filter processes all Default behavior is unchanged (undefined = process all reasons). * fix(channels): log github reason filter skips * fix(channels): validate github reason filter * fix(channels): address github reason filter comments * fix(github-channel): reject invalid reason filters * fix(github-channel): validate reason filters on connect --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
a771e4449e
|
fix(channels): reject unusable GitHub self-allowlists (#8055) | ||
|
|
c19d321d1f
|
feat(github-channel): filter notification reasons (#8031) | ||
|
|
ec9c36ef82
|
feat(channels): add GitLab polling channel adapter (#7862)
* feat(channels): add GitLab polling channel adapter
Poll GitLab todos via @gitbeaker/rest, dispatch notes through the
existing PollingChannelBase pipeline. Key design points:
- action_prompt_template config drives event filtering and metadata
rendering (unconfigured actions are skipped)
- Per-repo cursor (repo[chatId].last_read) as notes window lower bound,
global lastProcessedAt for todo-level dedup
- mark_done after successful processing; failure skips mark_done for
retry on next poll
- Mention gating delegated to base GroupGate (adapter only sets
isMentioned flag)
- First-contact body fallback for todos with no notes (e.g. mention in
issue description)
* fix(channels/gitlab): persist cursor after each successful todo
Call saveCursor() immediately after advancing lastProcessedAt so that
progress is durable even if the process crashes mid-poll. Also removes
the local watermark variable in favor of direct assignment.
* fix(channels/gitlab): persist cursor on every advancement including skips
* fix(channels/gitlab): address review critical issues
- Remove non-functional proxyAgent (gitbeaker doesn't support it)
- Construct repo_url from host + path (API doesn't return web_url)
- Handle directly_addressed action (falls back to mentioned template)
- First-contact fetches target description instead of using todo.body
- Move todo.project dereference inside try block
- Filter confidential notes
- Update channel-registry.test.ts for gitlab entry
* fix(channels/gitlab): address review suggestions
- Warn on connect if action_prompt_template is not configured
- Guard todo.target.iid before use
- Skip paths now mark_done (best-effort) to clean GitLab UI
- Remove postErrorComment (avoids duplicate comments on retry)
- Fetch only first page of notes (desc, maxPages:1, perPage:100)
instead of paginating entire note history
- Extract fetchRecentNotes for single-page windowed enumeration
* refactor(channels/gitlab): simplify to todo.body dispatch, add description mention support
- Remove notes API fetching; dispatch todo.body directly
- Detect description mentions via target_url anchor (#note_ absence)
- Always fetch target description for %description% metadata
- Remove per-repo cursor; dedup via cursor + mark_done only
- Cursor advances regardless of success/failure (no retry)
- Use zod for cursor validation
- Rename template vars to GitLab terminology:
%project% %project_url% %target_type% %iid% %title% %description% %todo_id%
- Support %% escape for literal percent
* docs(channels): add GitLab adapter documentation
- New user guide: docs/users/features/channels/gitlab.md
- Update _meta.ts navigation
- Update developer adapter matrix and SDK list
* fix(channels/gitlab): use correct Issues.show(issueIid, { projectId }) signature
* chore: regenerate NOTICES.txt for new gitlab channel dependencies
* fix(channels/gitlab): address review suggestions
- Add todo.project null guard (item 2)
- Single-pass regex for %% escape + %var% substitution (item 4)
- sendThreadMessage throws directly on undefined threadId (item 5)
- Dedup fetchDescription with per-poll cache (item 6)
- Remove per-todo saveCursor; base class saves after pollOnce (item 7)
- Add undefined threadId test (item 8)
- Expand confidential notes limitation in docs (item 3)
* test(channels/gitlab): add mention tests, directly_addressed coverage, skip assertions, temp cleanup
- New mention.test.ts: 14 cases for testBotMention/stripBotMention/escapeRegex
- Add directly_addressed fallback test
- Skip tests now assert TodoLists.done + cursor advancement
- afterEach cleans up mkdtempSync temp dirs
* fix(channels/gitlab): address review round 4
- Non-mention actions (assigned, etc.) set forceMentioned=true to bypass GroupGate
- Merge dead note-filter tests into single 'skips todo authored by bot'
- Log fetchDescription errors to stderr instead of silent swallow
- Post error comment on issue/MR when handleInbound fails (best-effort)
* fix(channels/gitlab): always force isMentioned=true, remove regex re-derivation
The action_prompt_template config is already the event filter, and
GitLab has already decided the mention when creating the todo.
Re-deriving isMentioned via regex on todo.body causes permanent
message loss when the regex misses (description mention + fetch
failure, group mentions). Always set forceMentioned=true so
GroupGate never drops a todo that passed the template filter.
* fix(channels/gitlab): propagate fetchDescription errors for description mentions
For note mentions, description is metadata-only — fetch failure is
logged and swallowed. For description mentions, description IS the
message — fetch failure now propagates to the outer catch, which
posts the ⚠️ error comment so the user knows to re-mention.
* perf(channels/gitlab): clean up stale todos, skip unnecessary fetchDescription
- Mark stale todos (updated_at <= cursor) as done on each poll to
prevent perpetual re-fetching of pre-existing pending todos
- Skip fetchDescription for note mentions when template does not
contain %description%, saving one API call per todo
- Update docs: stale todo cleanup, error comment on failure
* docs(channels/gitlab): clarify requireMention is bypassed, template is the real filter
* Apply suggestions from code review
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(channels/gitlab): use todo ID cursor instead of timestamp to eliminate equal-timestamp loss
Timestamp-based cursors (second granularity) could silently destroy
todos sharing the same updated_at as the cursor boundary. Switch to
monotonically increasing todo IDs which are unique and collision-free.
Add initialized flag to preserve first-start drain semantics: pre-existing
pending todos are marked done without dispatch on the first poll cycle.
* fix(channels/gitlab): harden first-poll drain, add ordering tests, fix lockfile
- Replace Math.max(...spread) with reduce to avoid RangeError on large
backlogs (~100k+ todos). Move initialized=true after the drain work so
any throw retries the drain instead of falling through to dispatch.
- Add unit tests: identical-timestamp delivery and id-order-when-updated_at-disagrees
(kills M2 sort mutant).
- Align lockfile: file:../base → ^0.21.0 for channel-base dep.
* fix(channels/gitlab): include dot in mention lookahead for GitLab usernames
GitLab usernames may contain dots (e.g. bot.name). The lookahead
character class inherited from GitHub omitted '.', causing @bot.name
to match as @bot. Add '.' to the negated class.
* docs(channels/gitlab): align docs with ID cursor and drain semantics
- Add first-poll drain as step 2 in How It Works
- Clarify GroupGate always passes (isMentioned forced true)
- Document initialized flag in Known Limitations
* Apply suggestions from code review
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(channels/gitlab): align package version and channel-base dependency to 0.21.1
Bump version from 0.21.0 to 0.21.1 to match other channel packages after
upstream merge. Pin @qwen-code/channel-base to exact 0.21.1 instead of
^0.21.0, matching the convention used by other published channels.
* fix(channels/gitlab): regenerate lockfile to match package.json versions
Manually add only gitlab-related lockfile entries (workspace, @gitbeaker
packages, transitive deps, channel-gitlab link) without unrelated npm
normalization churn.
* test(channels/gitlab): add regression tests for first-poll drain hardening
Two tests that kill the M1 (Math.max spread RangeError) and M2 (flag
ordering) mutants which survived the original 46-test suite:
- 150k todo drain verifies reduce() handles large backlogs without
RangeError and without dispatching
- Drain throw verifies initialized stays false so the next poll retries
the drain instead of falling through to dispatch
Test file duration: ~40ms → ~170ms.
* docs(channels/gitlab): clarify groupPolicy must be "open" and add runtime warning
The default groupPolicy "disabled" silently drops all mentions — todos are
marked done and cursor advances, but no dispatch occurs. Fix misleading docs
that said "GroupGate always passes" (only true at groupPolicy: "open") and
add a connect()-time warning when groupPolicy is not "open".
* fix(channels/gitlab): correct xcase integrity hash in lockfile
The manually added xcase entry had a typo in the sha512 hash (ys → ks),
causing npm ci EINTEGRITY failures in CI.
* fix(channels/gitlab): correct requester-utils integrity hash in lockfile
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(channels/gitlab): allow groupPolicy "allowlist" in warning and docs
The groupPolicy warning and docs incorrectly stated that groupPolicy
must be "open". In reality "allowlist" with the project listed also
works because isMentioned is forced true and GroupGate only requires
the group to be listed. Also fix the inaccurate "no error is logged"
claim — ChannelBase logs preflight rejected reason=group_disabled.
Fixes R5-🟡3 from PR #7862 review.
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
||
|
|
aac663f28a
|
feat(hooks): add security.allowPrivateNetworkHooks to bypass SSRF range checks for trusted scopes (#7968)
* feat(hooks): add security.allowPrivateNetworkHooks to bypass SSRF range checks for trusted scopes HTTP hooks hard-block all private/link-local address ranges via ssrfGuard, which makes them unusable in platform-managed environments where the hook receiver is a first-party, VPC-internal endpoint (e.g. an internal API gateway resolving to 172.16.0.0/12). Add an opt-in setting, security.allowPrivateNetworkHooks (default false), that skips the SSRF IP-range checks in urlValidator.isBlocked (literal IPs) and validateResolvedHost (literal + post-DNS-resolution paths). Security properties: - Honored only from User/System/SystemDefaults scopes; the value is stripped from Workspace settings during the merge (with a startup warning), so a cloned repository can never self-grant the bypass. - BLOCKED_HOSTS (169.254.169.254, metadata.google.internal, ...) remains blocked even when the flag is on. - Default false keeps every code path byte-for-byte compatible with current behavior; bare/safe mode forces it off. * fix(hooks): enforce metadata endpoint blocklist regardless of allowPrivateNetworkHooks Address review findings on #7968: with the flag on, cloud metadata endpoints were reachable through gaps in the relaxed checks. - ssrfGuard: add METADATA_IPS (169.254.169.254, 100.100.100.200) and isMetadataAddress(), which normalizes IPv4-mapped IPv6 forms (::ffff:a9fe:a9fe, ::ffff:6464:64c8, ...) via the existing extractMappedIPv4/expandIPv6Groups helpers. - urlValidator.isBlocked: BLOCKED_HOSTS matching and the literal-IP isMetadataAddress check now run unconditionally; only the general range check (isBlockedAddress) is relaxed by the flag. - httpHookRunner.validateResolvedHost: no longer returns early with the flag on — DNS resolution still runs and resolved addresses are checked against isMetadataAddress, so a hostname resolving to a metadata endpoint is blocked. DNS failures still defer to fetch, as before. - settings warning text now lists User/System/SystemDefaults, matching the schema and docs. - docs: precise wording — the flag relaxes only range checks; metadata endpoints stay blocked in all serialized forms and after DNS resolution. The flag now opens RFC1918/CGNAT/link-local ranges only; cloud metadata endpoints (169.254.169.254, 100.100.100.200 in any form, plus the BLOCKED_HOSTS hostnames) are unreachable in every configuration. --------- Co-authored-by: 欢伯 <ri.xur@alibaba-inc.com> |
||
|
|
dc2f61d910
|
feat(channels): dispatch GitHub notifications by reason (#7826)
* feat(channels): dispatch GitHub notifications by reason Route each GitHub notification by notification.reason into one of five lanes, instead of dispatching every new comment regardless of trigger: - mention: only dispatch comments that actually @ the bot (noise reduction) - review_requested (PR): fetch PR meta via pulls.get and dispatch a review-specific prompt, even with no new comments - assign: fetch issue meta and dispatch a triage-specific prompt - author/comment: aggregate the window's new comments into one check-and- respond prompt - other reasons: generic fallback (current behavior) Add cursor dedup via dispatchedComments (by comment node_id) and dispatchedNotifications (by notification id), surviving a markNotificationsAsRead failure that leaves the cursor un-advanced. Closes #7807 * fix(channels): mark review_requested/assign envelopes as mentioned GroupGate defaults to requireMention: true, which silently drops isMentioned:false envelopes as 'mention_required'. The review_requested and assign lanes are explicit directed triggers — the bot was asked to review or assigned — equivalent to a mention, so set isMentioned: true so they pass the gate instead of being inert on the documented default config. Addresses review Critical on #7826. * fix(channels): resolve github routing review comments * fix(channels): dedupe github meta lane comments * fix(channels): conditional assign framing for PR threads The assign route already detected PR threads to use pulls.get, but the trigger framing text always read 'assigned to this issue' even for PRs. Make it conditional so PR assignments read 'assigned to this pull request'. * fix(channels): dedup meta lane dispatch inputs * fix(channels): simplify GitHub reason dispatch * fix(channels): respect mention gate for github aggregate lane * fix(channels): truncate aggregate comment bodies by code points Match the code-point-aware truncation already used for meta-lane bodies so a supplementary-plane emoji at the MAX_COMMENT_CHARS boundary is not split into a lone surrogate. * fix(channels): harden GitHub dispatch failures, event window, and framing (#7826) - Classify deleted/transferred subjects (404/410) as terminal so a single dead notification is logged and skipped instead of wedging the batch's mark-read and cursor advance every poll. - Widen the review_requested/assign event search to the newest ~100 events by merging the preceding page when the last page is partial, instead of inspecting only the last page (which can hold a single event). - Move the aggregate lane's untrusted-data warning to the head of the prompt text so it precedes the comment text it describes (metadata is appended after text by ChannelBase). - Add regression tests: permanent-failure two-poll advance, terminal 404 no-retry, multi-page event search, prompt caps, and the no-actor guard. * fix(channels): drop lastReadAt filter in findMetaTrigger, add review coverage (#7826) * fix(github): keep aggregate and meta windows bounded * fix(channels): apply windowSince lower bound in findMetaTrigger (#7826) * fix(channels): bound retry wedge, compute aggregate isMentioned, fix pairing pre-filter (#7826) * fix(github): record dispatch before handler * fix(github): persist skipped notifications * fix(github): close dispatch retry loss cases * test(github): cover cursor trim and meta floor validation * fix(github): simplify notification reason dispatch * fix(github): preserve batched dispatch comments * fix(github): restore direct event dedup * fix(github): preserve directed mention context * fix(github): keep review fixes scoped * fix(github): preserve delayed direct triggers --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com> Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com> |
||
|
|
17408f1028
|
feat(hooks): Add submitted prompt provenance (#7762)
* feat(hooks): add submitted prompt provenance Add an optional pre-expansion prompt sidecar for interactive UserQuery hooks while preserving legacy prompt behavior and fail-closed provenance handling across queues, retries, and continuations. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(hooks): harden submitted prompt provenance Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(hooks): tighten submitted prompt provenance Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
d44030a4c0
|
feat(core): add model grade selection for subagent spawn (#7685) (#7702)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* docs: add design placeholder for subagent model grade selection (#7685) * feat(core): add subagent model grade selection * test(subagent): cover resolveModelGrade deep guards and resume else branch - subagent-manager: add tests for non-string grade values, blank values, array-shaped modelGrades, and missing modelGrades (all return undefined) - background-agent-resume: assert configured subagent model is preserved (not forced to 'inherit') when launch flags (model + authType) are absent Addresses test-coverage review findings. * refactor(subagent): extract normalizeModelGradeSettings and merge model validate - Extract normalizeModelGradeSettings helper shared by resolveModelGrade and the Agent tool schema build, so the advertised grades and runtime resolution cannot drift (addresses duplicated shape invariant). - Merge the three model-parameter validate branches under a single `params.model !== undefined` guard. - Update agent.test.ts mock to preserve the real helper while still mocking SubagentManager. * refactor(core): simplify model grade resolution * fix(core): reject unknown model grades * docs(core): clarify model grade precedence * docs: explain subagent model grades * test(core): update subagent manager mock * fix(core): list available model grades * fix(core): trim model grade keys and cover schema removal Grade keys were checked for emptiness via grade.trim() but stored in the map and advertised in the tool schema enum untrimmed, while values were trimmed. A padded key like ' small ' published a padded enum name the model had to reproduce verbatim, and the allowlist check silently excluded it. Normalize the key before storing, allowlist matching, and schema publication. Also adds a test for the delete schema.properties.model branch that fires when grades transition from available to empty, so a regression that breaks the delete leaves no stale model enum in the tool schema. * fix(core): trim allowed model grade filters --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
4958120c21
|
docs(channels): Document loops and proactive delivery (#7628)
* docs(channels): document loops and proactive delivery Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(channels): clarify standalone vs daemon loop storage paths (#7628) --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
4895726600
|
fix(channels): use username as senderId in GitHub adapter to fix allowlist gate (#7727)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
* fix(channels): use username as senderId in GitHub adapter to fix allowlist gate isAuthorizedForSharedSessionTarget compares config.allowedUsers (logins) against envelope.senderId — but senderId was a numeric ID resolved via getByUsername, so every allowlisted user was rejected from /who, /clear, /status, /loop, and channel memory commands. Fix by using user.login as senderId throughout, assuming GitHub users don't change usernames. This also removes the getByUsername resolution step that made connect() non-idempotent on daemon reconnect. - Remove botUserId field; bot self-filter uses botUsername - Remove allowedUsers login-to-ID resolution in connect() - Pass config.allowedUsers logins directly to gate - senderId in envelopes uses user.login * fix(channels): normalize allowlist/senderId to lowercase for case-insensitive matching GitHub logins are case-insensitive but Set.has/Array.includes are not. Without normalization, allowedUsers: ['Alice'] silently rejects a commenter whose canonical login is 'alice' — a regression from the old getByUsername round-trip which normalized casing implicitly. - Normalize config.allowedUsers and gate to lowercase in connect() - Lowercase senderId at both envelope assignment sites - Remove dead != null guard in bot self-comment filter - Add case-insensitive gate test and connect idempotency test - Add senderId/allowedUsers comparability guard to dispatch test - Document username-based allowlist rename risk in security section |
||
|
|
34a3d46006
|
fix(core): fire StopFailure hook on loop detection early returns (#7592)
* fix(core): fire StopFailure hook on loop detection early returns (#7588) When loop detection (always-on safety or heuristic) terminates a turn early via `return turn`, the Stop hook code after the streaming loop was never reached. StopFailure hooks were only fired from the CLI layer for API errors, not from client.ts for loop detection. Added `loop_detected` to StopFailureErrorType and fire the StopFailure hook via MessageBus before each loop detection early return, so cleanup/notification hooks run regardless of how the turn ends. All 284 client tests and 682 hook tests pass. * fix(core): use direct hookSystem call for loop-detection StopFailure (#7588) The MessageBus bridge has no StopFailure case, so the hook never executed. Switch to config.getHookSystem()?.fireStopFailureEvent() (matching the CLI's API-error path), make it fire-and-forget per the StopFailure contract, drop the stale last_assistant_message that carried the previous turn's text, deduplicate via a private helper, update docs with loop_detected, regenerate the settings schema, and add regression tests for both loop-detection paths. * test(core): add negative-path test for StopFailure hook disable guard (#7592) * test(core): add negative-path tests for StopFailure hook guards (#7592) --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
62e009a952
|
feat(channels): GitHub polling adapter with notification-as-wakeup architecture (#7632)
* feat(channels): add GitHub polling adapter with notification-as-wakeup architecture
Introduce a GitHub channel adapter that monitors notifications and
responds to @mentions on issues/PRs by posting comments. Uses
last_read_at as a per-thread watermark for comment enumeration,
replacing the unreliable latest_comment_url approach.
Foundation changes to ChannelBase:
- sendThreadMessage for thread-targeted delivery (IM adapters unchanged)
- Envelope.metadata appended to prompt after command parsing
- chat_thread session scope (channel:chatId:threadId) prevents
cross-repo session collision
- polling-helpers: testBotMention/stripBotMention (separate detection
from stripping, no whitespace collapsing), cursor persistence,
abortableSleep
GitHub adapter design:
- Notifications as wake-up signals only (unread filtering)
- listComments enumeration with last_read_at watermark
- Bot self-comment filtering, case-insensitive mention regex
- In-memory recentlyProcessed set for mark-read failure dedup
- First-contact: new issue body @bot triggers processing
- Error comment + cursor advance on handleInbound failure
- pollInterval minimum 60s, exponential backoff 2s-30s
* refactor(channels): extract PollingChannelBase from polling-helpers
Replace the loose polling-helpers module with a PollingChannelBase<Cursor>
abstract class that encapsulates the poll loop, cursor persistence (JSON,
atomic write), exponential backoff, and start/stop lifecycle. Subclasses
implement only pollOnce() and createInitialCursor().
- Delete polling-helpers.ts (cursor fns + abortableSleep moved into base)
- Move mention utilities (testBotMention/stripBotMention) to github pkg
- GithubAdapter now extends PollingChannelBase<{ lastProcessedAt }>
* fix(channels): remove Gitea/GitLab mention from sendThreadMessage JSDoc
* fix(channels): match /pulls/N in notification subject URL
GitHub PR notifications use /repos/{owner}/{repo}/pulls/{N} in
subject.url, not /issues/{N}. The regex only matched /issues/,
causing PR notifications to be skipped and marked read.
Also sets threadId to 'pr:N' for PRs (was always 'issue:N').
* test(channels): add PR body first-contact unit test
Verify that PR notifications with @mention in the body (not a comment)
correctly trigger the first-contact path: extractFromSubjectUrl matches
/pulls/N, listComments returns empty, tryFirstContactBody fetches the
PR body and dispatches to handleInbound with threadId 'pr:N'.
* feat(channels): read pollInterval from channel config in PollingChannelBase
Move pollInterval config reading from GithubAdapter to the base class.
The user's configured pollInterval in settings.json is now respected
directly without a minimum enforcement. Defaults to 60000ms when not
configured.
* fix(channels): prepend metadata before prompt text
Agent sees issue/PR context (type, title, URL) before the user's
request, improving comprehension. Metadata is still appended after
slash-command parsing so commands are not affected.
* refactor(channels): route all ChannelBase delivery through sendThreadMessage
Replace all internal sendMessage calls with sendThreadMessage, passing
envelope.threadId (or target.threadId / undefined) so polling adapters
can deliver to the correct thread. IM adapters are unaffected — the
default sendThreadMessage falls through to sendMessage.
* docs(channels): document sendThreadMessage delivery architecture
* fix(channels): address review findings
- Cap recentlyProcessed Set at 10k entries to prevent unbounded growth
- Validate cursor JSON shape (non-null object) in loadCursorFromDisk
- sendThreadMessage falls through to sendMessage when threadId is
undefined instead of silently dropping
- Remove duplicate pollInterval from GithubConfig (now in ChannelConfig)
- Fix chat_thread routing key trailing colon when threadId is undefined
* docs(channels): fix metadata JSDoc — prepended, not appended
* fix(channels): use recentlyProcessed dedup for first-contact body
Replace the fragile createdAt-vs-cursor check in tryFirstContactBody
with the recentlyProcessed set. The cursor advances globally based on
notification updated_at — when a different notification with a later
updated_at is processed first, the cursor can advance past the issue's
created_at, causing the first-contact check to incorrectly skip the
issue body (forget reply bug, found in E2E TC-2b).
* refactor(channels): two-layer dedup for GitHub adapter
Layer 1: global cursor filters notifications by updated_at (sorted
ascending, old first). Layer 2: server-side last_read_at filters
comments by created_at (sorted ascending).
- Delete recentlyProcessed Set (no longer needed)
- Sort notifications by updated_at ascending before processing
- Sort comments by created_at ascending before processing
- Pass latest comment created_at to markThreadAsRead as last_read_at
* fix(channels): address review findings on GitHub adapter
Blockers:
- sessionScope: add defaultSessionScope to ChannelPlugin, apply in
parseChannelConfig so router and adapter agree on 'chat_thread'
- channel-registry.test.ts: add 'github' to expected type list
Should-fix:
- Replace per-thread markThreadAsRead (PATCH) with bulk
markNotificationsAsRead (PUT /notifications + last_read_at).
API errors stop the batch without marking failed notifications
read; handleInbound errors still advance (error comment posted).
- connect() throws on bot identity failure instead of failing open
- metadata appended after promptText (inside sender attribution)
- isSharedSessionTarget includes 'chat_thread' scope
Nits:
- startPollLoop re-entrancy guard
- clean-package-build-artifacts.js includes github
- index.ts re-exports GithubChannel
* fix(channels): use max updated_at of all fetched notifications as last_read_at
Prevents re-fetching the same notifications in the next poll cycle.
The bulk PUT /notifications marks all fetched notifications as read
up to the max updated_at, regardless of per-notification success.
* fix(channels): address review round 2 findings
- #12: loadCursorFromDisk rejects arrays
- #13: pollInterval validates positive finite number
- #19: first-contact gate uses dispatchedMention flag (not newComments.length)
- #25: stripBotMention no longer trims (preserves indentation)
- #27: remove adapter-level requireMention, unify on GroupGate
- #31: add chat_thread SessionRouter routing key tests
- #33: clear metadata on collect-mode synthetic envelope
- #35: fix PollingChannelBase.test import path
- #36: add @octokit/rest to 15-channel-adapters.md dependencies
* docs(channels): document known limitations for GitHub adapter
- First start skips existing unread notifications (cursor = now)
- Requires classic PAT (fine-grained PATs lack notifications API)
- PR review comments not enumerated (issue comments only)
* fix(channels): address review round 3 findings
- #9: buildMetadata derives web URL from baseUrl (GHE support)
- #12: sendThreadMessage throws on invalid threadId format
- #19: mention lookbehind matches cc:@bot and "@bot" patterns
- #23: cursor file name uses sha256 hash to prevent collision
- #26: test verifies cursor persistence to disk
- #31: postErrorComment double-failure logs to stderr
- #45: tests use mkdtempSync isolation instead of real QWEN_HOME
* fix(channels): pass threadId through pairing flow + sendResponseMessage test
- #13+16: onPairingRequired receives envelope.threadId and passes it
to sendThreadMessage, so pairing codes are delivered on threaded
channels (GitHub) instead of throwing
- #6: add test verifying sendResponseMessage resolves threadId from
router.getTarget and passes it to sendThreadMessage
* fix(channels): pass proxy to Octokit for daemon-worker environments
- #44: read this.proxy from ChannelBaseOptions and pass
HttpsProxyAgent to Octokit request.agent, matching the
Telegram adapter pattern
* fix(channels): address review findings — immutable senderId, comment time window, validateCursor, retry wrapper
- senderId uses immutable user.id; allowedUsers resolved to IDs at connect
- Comment filter upper bound: updated_at <= maxUpdatedAt (batch window)
- Per-notification errors use continue (best-effort), not break
- validateCursor() virtual hook for subclass cursor shape validation
- sendThreadMessage/postErrorComment wrapped in githubApi() retry
- webOrigin handles default api.github.com → github.com
- Docs: classic PAT only, markNotificationsAsRead, dedup claims removed
- Tests: threadId priority, metadata consumption, defaultSessionScope,
QWEN_HOME isolation, persistent mock rejection
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(channels): mark notifications read before processing to prevent duplicate replies
Bot's own replies bump notification updated_at past the pre-captured
maxUpdatedAt, so markNotificationsAsRead(maxUpdatedAt) failed to mark
them read — the next poll re-fetched the same comments and replied
again.
Move markNotificationsAsRead + cursor advance before the processing
loop (best-effort delivery). This is safe because bot's own comments
do not flip notifications back to unread. Update docs to reflect the
new poll cycle order and best-effort semantics.
* fix(channels): update sender gate after allowedUser ID resolution and harden tests
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(channels): cursor-based comment window to prevent duplicate replies
PUT /notifications is async (202) with a last_read_at cutoff — the
bot's reply bumps updated_at past the cutoff before the server
processes the mark, so the notification is never marked read and gets
re-fetched on the next poll, causing duplicate replies.
Use the cursor value before advancement as an exclusive lower bound
for the comment enumeration window: (windowSince, maxUpdatedAt].
Comments already eligible in a previous poll are excluded regardless
of whether the mark succeeded. Zero new persistent state.
* fix(channels): cursor-based comment window to prevent duplicate replies
PUT /notifications is async (202) with a last_read_at cutoff — the
bot's reply bumps updated_at past the cutoff before the server
processes the mark, so the notification is never marked read and gets
re-fetched on the next poll, causing duplicate replies.
Use the cursor value before advancement as an exclusive lower bound
for the comment enumeration window, with per-notification last_read_at
as the preferred lower bound when available (server-side per-thread
watermark). Comments already eligible in a previous poll are excluded
regardless of whether the mark succeeded. Zero new persistent state.
* fix(channels): address review findings — null guard, cursor validation, metadata dedup, abortable sleep, docs
- Guard against null notification.subject.url in pollOnce
- Validate lastProcessedAt is a parseable date in validateCursor
- Add metadata: undefined to second collect-mode drain path
- Refactor abortableSleep as protected method on PollingChannelBase
- Fix docs: requireMention is nested under groups.*
- Add tests: chat_thread shared session, dispatchedBodies eviction,
cursor enumeration window, last_read_at in mention tests
* docs(channels): sync docs with implementation — cursor shape, error handling, GitHub adapter tables, first-contact
- Design doc: update Cursor to { lastProcessedAt, dispatchedBodies? }, add
validateCursor date check, abortableSleep protected method, break-on-error
semantics, subject.url null guard
- Developer docs: add GitHub to adapter table and adapter matrix
- User guide: add first-contact step to How It Works, clarify mark-before-process
* fix(channels): address review round 2 — error dedup, abortable retry, backoff reset, window test
- Record dispatchedBody on first-contact handleInbound failure to prevent
duplicate error comments when mark-read async hasn't taken effect
- Use abortableSleep instead of raw setTimeout in githubApi retry so
disconnect() can interrupt rate-limit cooldowns
- Reset consecutiveErrors in startPollLoop so stop/restart cycles don't
inherit stale elevated backoff
- Add test for cursor window client-side lower-bound exclusion filter
* fix(channels): address review round 3 — cursor validation, error dedup, sender gate, bot-self body
- validateCursor: normalize falsy non-array dispatchedBodies (false/0/""/null)
to [] instead of passing them through to .includes() which throws TypeError
- Set dispatchedMention after postErrorComment to prevent first-contact from
posting a duplicate error comment on the same thread
- Only set dispatchedMention when the sender passes the sender gate, so a
disallowed commenter's mention no longer suppresses a valid first-contact
body from an allowed issue author
- Skip bot-authored issue bodies in tryFirstContactBody to prevent
self-response loops under open sender policy
* fix(channels): address review suggestions — test coverage, cursor filename, assertion precision
- Pairing flow: add threadId pass-through regression test
- pollInterval: add table-driven edge cases (0, -1, NaN, Infinity, string)
- Add null-URL notification followed by valid notification batch test
- Fix comment window test to assert paginate call 3 (listComments) not call 2
- Truncate cursor filename encoded prefix to 200 chars (filesystem 255 limit)
- Assert mark-read uses batch maxUpdatedAt, not just { read: true }
- Assert real GitHub plugin declares defaultSessionScope chat_thread
- Add invocationCallOrder assertion for mark-before-process ordering
* fix(channels): address review round 4 — allowedUsers throw on resolve failure, crash table fix, mark-read failure test
* fix(channels): address review round 5 — created_at filter, retry-after NaN guard, retry/sendThreadMessage tests, docs fixes
* fix(channels): address ci-bot review 4778587403 — reconnect idempotency, github type enumerations, retry/webOrigin tests
* chore(channels): align channel-github version to 0.21.0 after upstream merge
* chore(channels): update package-lock.json for channel-github 0.21.0
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: OrbitZore <orbitzore@users.noreply.github.com>
|
||
|
|
a470ba626c
|
feat(review): add comment-status helper for existing-thread triage (#7690)
* feat(review): add comment-status helper for existing-thread triage
One deterministic pass over a PR's existing inline comments, replacing
the per-comment `gh api` fetches the orchestrating model used to make
during /review: anchor validity at the live head (outdated detection,
with a file-level exemption), whether the anchored file changed in the
reviewed worktree since each comment's commit and which commits touched
it (the re-check's candidate "fixed by" list), reply participation and
PR-author response, the blocker signal (same carriesBlockerSignal as
pr-context, so the two surfaces agree by construction), and
worktree-vs-live head drift.
Measured on a heavily discussed PR (72+ inline comments), a single
review run burned 20+ model turns re-deriving exactly these fields one
comment id at a time. SKILL.md now runs the subcommand in Step 1 and
routes the Step 6 re-check's status questions at the report; comment
bodies stay in the pr-context file under its untrusted-data preamble,
and a comment-status failure only warns — it is an index, not the
evidence, so it never sets the context-unavailable state.
* test(review): add comment-status to the subcommand registry expectations
* fix(review): comment-status review follow-ups — size warning, --host wiring, scope clauses
Addresses the review at
|
||
|
|
f4e333c580
|
fix(mcp): harden OAuth callback handling (#7510) | ||
|
|
45c8d8f8cc
|
docs: refresh subagent lifecycle guidance (#7624)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
22963d5777
|
feat(core): add fork_turns to fork subagents (#7346)
* feat(core): add fork_turns to subagents * fix(core): preserve nested agent context inheritance * fix(core): isolate inherited subagent history * refactor(core): scope fork_turns to fork agents * test(core): cover zero-real-turns branch in selectForkHistory Add a regression guard asserting selectForkHistory returns [] when a numeric fork window finds no real user turns after the synthetic prefix (e.g. only startup context present). This pins the realUserTurnIndexes.length === 0 branch so a future refactor cannot silently return the full history instead of an empty selection. * docs(core): address fork_turns review feedback - Explain the curated vs uncurated history split between the fork_turns 'all' and numeric paths in createForkSubagent. - Document why includeCompressed is load-bearing in selectForkHistory. - Gate the 'forks inherit ...' prose in the Writing-the-prompt section behind isForkSubagentEnabled so non-interactive sessions no longer advertise fork behavior, and lock it with description assertions. * test(core): cover fork_turns 'all' and getHistoryForForkWindow fallback Add two integration tests for prepareForkConfig fork-history selection: - 'all' path: verify getHistoryShallow(true) sources the curated history and selectForkHistory(history, 'all') seeds the fork with the full history verbatim. - numeric path: verify the getHistoryForForkWindow?.() ?? getHistory(true) fallback still produces a correct bounded window (startup + latest real turn) when getHistoryForForkWindow is unavailable. * fix(core): use uncurated history for fork bounded-window fallback The numeric fork_turns path falls back to geminiClient.getHistory(true) when getHistoryForForkWindow is unavailable. Curated history coalesces the leading startup reminder into the first real user turn, so getStartupContextLength can no longer detect it as a pure prefix. selectForkHistory then leaves the startup text embedded in the first selected turn while the startupContext prefix is prepended separately, duplicating startup context in the fork's initial messages. Fall back to uncurated getHistory() instead, which keeps the startup reminder as its own pure entry that selectForkHistory strips cleanly. Update the fallback-path test to assert the uncurated call and document why curated history is unsafe here. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
2ad63e7ca3
|
fix: ask when auto classifier is unavailable (#7331) | ||
|
|
ff350a16a7
|
feat(web-shell): add git commit history browser (#7204)
* feat(web-shell): add git commit history browser Add a read-only Git Log dialog to the Web Shell, accessible via /log command or the History tab in the Changes dialog. Full-stack implementation across core, daemon, SDK, and web-shell: - core: fetchGitLog (paginated commit list) and fetchGitCommitDetail (message body + per-file numstat) with 12 integration tests - daemon: GET /workspace/git/log and /workspace/git/log/commit routes with bound + qualified dual registration - SDK: DaemonGitLog/DaemonGitCommitDetail types and client methods - web-shell: GitLogDialog with commit list, expandable details, SHA copy icon, Load more pagination, and Changes/History tab switching in both dialogs * fix(web-shell): address review feedback on git log browser - Critical: use first-parent diff for merge commits (diff-tree without -c or explicit parent outputs nothing for merges) - Remove dead embedded prop from GitDiffDialog and GitLogDialog - Replace span role=button with aria-hidden for copy icon (a11y) - Add loadMore error feedback instead of silent catch - Refresh relative timestamps every 60s (useState + interval) - Remove dead branch param from subtitle i18n call * fix(web-shell): address R2 review feedback on git log browser - Use bounded split in parseLogFields (first 7 separators) to prevent subject containing literal \x1f from shifting the parents field - Add SHA hex format validation at daemon route layer (400 for invalid) - Add rendering branch for detail.available === false (error message instead of empty content) * fix(web-shell): count renamed files in commit detail + test git-log route & dialog Follows the R2 review-feedback commit (which fixed the bounded parse, the non-hex SHA 400, and the unavailable-detail render). Remaining items: - Commit detail counts renamed files. diff-tree is plumbing and does not honour diff.renames, so a `git mv` split into a delete + add pair (or an empty-path entry) instead of one file — understating filesCount / linesAdded / linesRemoved. Run diff-tree with -M and give the inline numstat parser the same pending-rename state machine as parseGitNumstat, so a rename is one file keyed by its new path. Covered by a real-repo rename test, plus a merge-commit test that locks the first-parent diff. - Tests for the two previously-untested modules: the workspace-git-log route (list shape, pagination clamping, sha-required + non-hex 400, trust gating) and GitLogDialog (all five list state paths, load-more offset + error, detail expand + both failure branches incl. available:false, and the relative-time render). * fix(web-shell): address R3 review feedback on git log browser - Fix timeAgo '0y ago' for commits ~360-364 days old (Math.max(1, ...)) - Add .catch() to clipboard writeText to prevent unhandled rejection - Reset loadMoreError on initial re-fetch (daemon reconnect) - Preserve prev.available in loadMore merge instead of overwriting - Add ARIA tab semantics (role=tablist/tab, aria-selected) to both Changes and History tab bars * fix(web-shell): address R4 review feedback on git log browser - Fix vacuous limit-clamp test: seed 3 commits, verify limit=2 returns 2 + hasMore, limit=0 clamps to 1 - Add CSS var fallbacks for --subtle-bg and --success-bg (dialog portals outside App.module.css scope) - Extract GIT_DIALOG_SWITCH_DELAY_MS constant with doc comment - Make copy-SHA control keyboard accessible (tabIndex, onKeyDown, aria-label instead of aria-hidden) * fix(web-shell): escape apostrophe in worktree welcome string * fix(web-shell): address git history review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(web-shell): mock git diff content in app tests Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): harden git log metadata parsing Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: wenshao <wenshao@example.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
357619a5fc
|
fix(channels): scope pairing and allowlist state by workspace (#7065)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
* fix(channels): scope pairing and allowlist state by workspace PairingStore keyed its on-disk files by channel name alone, under the global ~/.qwen/channels/ directory. Two workspace-scoped channel configurations using the same channel name therefore shared pairing requests and allowlist entries: a sender approved for workspace A was implicitly approved for workspace B — an authorization-boundary violation in multi-workspace daemon deployments. PairingStore now takes the channel's workspace cwd and stores state under channels/<basename>-<sha256[:12]>/, ChannelBase passes config.cwd, and the pairing CLI commands gain a --cwd option (defaulting to the current directory) so list/approve address the same workspace-scoped store the channel worker uses. Migration is a conservative one-time grandfather: on first scoped use, existing legacy global files are COPIED into the scope (so already-approved senders stay approved and other workspaces can grandfather the same baseline later), after which the stores diverge — no ongoing cross-workspace sharing, and legacy content can never overwrite scoped state. Fixes #7017 * fix(channels): canonicalize scope identity and gate migration per directory Address the review findings on #7065: 1. Scope identity now follows the repo's workspace-canonicalization contract: getWorkspaceScopeDirName realpaths the resolved path (with the same ENOENT fallback as acp-bridge's canonicalizeWorkspace, which channel-base mirrors locally to stay dependency-free). Symlinked and platform-case-variant spellings of one directory — macOS /tmp/ws vs /private/tmp/ws — now address the same store from a daemon worker and from the CLI's --cwd. 2. Legacy grandfathering is gated at the scope-directory level instead of per file: once the scoped directory exists, legacy files are never consulted again. A per-file gate let a legacy allowlist silently re-approve senders an operator had revoked by deleting the scoped allowlist file, and let an in-use scope absorb a legacy file that appeared later. The README now spells out that revocation means removing entries, not deleting files. 3. The empty `pairing list` output names the workspace scope and points at --cwd, mirroring the approve error, since a scope mismatch surfaces there first. Four new regression tests (symlink collapse, ENOENT fallback, no resurrection after revoke, no late-legacy absorption) fail on the previous commit and pass here. Refs #7017 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(channels): state the broad realpath fallback is intentional; make the ENOENT scope assertion meaningful Two round-2 review notes on #7065: - canonicalizeWorkspacePath's docblock claimed to match acp-bridge's ENOENT-only fallback while the catch swallows every realpath error. Keep the broad catch — pairing storage is best-effort and a transient FS error must not stop the channel from starting — and document that divergence explicitly instead. - The ENOENT-fallback test's second assertion compared a scope name to itself. It now compares against the scope computed from the resolved spelling, pinning that the realpath step degrades to a no-op for nonexistent paths. Refs #7017 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(channels): always close the migration gate, normalize nonexistent-path scopes, copy atomically Address the automated round-2 inline findings on #7065: - The migration gate is now closed on the very first construction even when no legacy files existed: the scope directory itself is the "migration decided" marker. Previously a workspace that first ran on new code before any legacy state existed left the gate open, and a legacy allowlist written later by an older version still running concurrently would have been absorbed. - resolvePath now runs every input through path.resolve, so trailing-separator and dot-dot spellings of a path that does not exist on disk (where the realpath step cannot help) canonicalize to the same scope instead of three different ones. - Legacy files are copied via temp file + atomic rename, so a crash mid-copy cannot leave a truncated scoped file behind the now-closed gate, and a concurrent first construction cannot observe a half-written allowlist. Adds three regression tests (late-legacy not absorbed after empty first startup, nonexistent-path spelling collapse, unreadable legacy file keeps the constructor best-effort); the first two fail on the previous commit. Refs #7017 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(channels): encode channel names in scoped paths, gate migration per channel, wire tests into CI Three independently reproduced problems in the workspace-scoping change, found in external review of d8c155ab3: - Channel names come from unrestricted config keys and were joined into the scoped path verbatim, so a name like `../support` climbed out of the scope directory and landed every workspace on one shared file at the channels root — silently undoing the isolation this PR exists to establish. File names now URI-encode the channel name (mirroring GroupHistoryStore), common names encode to themselves, and the legacy source path is containment-checked as defense in depth. - The directory-level migration gate let only the FIRST channel of a workspace migrate: one process starts several channels in turn, and once the first construction created the scope directory, every other channel's legacy state was skipped forever. The gate is now a per-channel `<channel>.migrated` sentinel inside the scope directory, written even when there was nothing to copy. - A single unreadable legacy file aborted the whole migration loop and the gate still closed, so the other (valid) file was never migrated and never retried. Files are now copied independently, best-effort, via uniquely-named temp files + atomic rename, and scoped files are never overwritten. Also adds the missing test/test:ci scripts to channels/base (matching its sibling packages), so the package's 784 tests actually run in CI's `npm run test:ci --workspaces --if-present` sweep. Four new regression tests (traversal-name isolation, multi-channel migration, late-channel migration, unreadable-file independence) all fail on d8c155ab3 and pass here. Refs #7017 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(channels): read legacy files under the raw name, retry partial migrations, log failures Round-3 review findings on #7065: - Legacy sources are read under the RAW channel name again: pre-scoping code wrote them unencoded, so looking them up under the encoded name made any channel whose name changes under encoding (e.g. "my channel") skip its legacy state and permanently lose approved senders behind the sentinel. Encoded names remain in use for the scoped destinations; the containment check keeps traversal-style raw names from reading outside the channels root. - The sentinel is only written when every present legacy file was copied (or already existed). A partial failure (ENOSPC, transient I/O) previously closed the gate with incomplete state; now the next construction retries the failed file, and per-file stderr warnings are emitted so operators can see why senders are missing instead of instrumenting the constructor. - The symlink test cleans up with unlinkSync — rmSync throws EISDIR for a symlink to a directory on macOS. - The pairing CLI gains tests covering --cwd scoping end to end (list isolation, empty-scope hint, approve scoping, cross-workspace code rejection), plus an explicit return after the mocked-in-tests process.exit(1). The raw-name and partial-retry regression tests fail on 954e76af4. Refs #7017 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a9a6ca4ac6
|
feat(channels): observe group names from inbound messages (#7155)
* feat(channels): expose observed workspace contacts * fix(channels): address observed contacts review feedback * docs(channels): design observed group names * docs(channels): plan observed group names * feat(channels): observe inbound group names --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
d4db5fcfab
|
feat(core): improve subagent delegation defaults and guardrails (#7048)
* docs(design): define default background subagents * feat(core): improve subagent delegation defaults * docs(core): cross-reference the three background-classification sites Add pointer comments linking the core dispatch source of truth (AgentTool.execute) and its two UI mirrors (web-shell isBackgroundSubAgentToolCall, desktop detectBackgroundEvents) so the replicated top-level-agent background heuristic is not changed in isolation. Addresses PR review feedback. * fix(core): align background classification for fork and named-teammate launches Address review feedback on the background-classification rule so core dispatch and the two UI classifiers stay consistent: - core: exclude a name-without-active-team launch from the default-background path so it stays foreground, matching both UI classifiers (which exclude name). Previously such a launch was backgrounded by core but tracked as foreground by the UIs. - web-shell and desktop classifiers: exclude subagent_type "fork" from the default-background heuristic, mirroring core's !isForkRequested guard. A top-level fork request with an omitted flag runs foreground in core but was classified as background by the UIs. - add a core dispatch test asserting a working_dir launch with an omitted run_in_background flag stays in the foreground. * test: cover fork/background classification and precedence per review feedback Address unresolved review threads on PR #7048: - Add web-shell and desktop UI classifier tests asserting an omitted-flag `subagent_type: "fork"` launch stays in the foreground, verifying the documented `!isForkRequested` parity with core dispatch. - Add a core AgentTool test asserting an explicit `run_in_background: false` overrides a subagent config with `background: true`, locking in the `run_in_background ?? config` precedence against a `||` regression. - Harden the Explore read-only prompt: pipelines must not send data to a network endpoint (no curl/wget/nc), closing the `cat file | curl` exfiltration gap. * fix(core): restore general no-unnecessary-files guard in general-purpose prompt Address review feedback: the rewritten general-purpose prompt dropped the broad guard against creating unrequested files, keeping only the documentation-specific line. Restore a general 'do not create files unless necessary' guard so speculative utility/config files are not created. * test(desktop): cover named-teammate foreground guard in detectBackgroundEvents Add a desktop tool-matching test asserting a top-level Agent with a `name` set (named teammate) stays foreground and emits no task_backgrounded event, mirroring the web-shell classifier's named-teammate coverage and the existing fork-exclusion test. * test(core): cover named-teammate foreground dispatch when flag omitted Add a core-dispatch test asserting a top-level Agent launch with `name` set and `run_in_background` omitted stays foreground when no team is active, guarding the `this.params.name === undefined` exclusion in backgroundRequested directly (previously only covered by the UI classifiers). --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
02cb3a6380
|
feat(channels): expose workspace-scoped observed contacts (#7109)
* feat(channels): expose observed workspace contacts * fix(channels): address observed contacts review feedback --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |