Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/BUGBOT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Cursor Bugbot Autofix

## Race condition with concurrent pushes

Cursor Bugbot Autofix pushes commits directly to PR branches. When a human
or agent is also pushing to the same branch, this causes rebase conflicts
and rejected pushes.

### Recommended workflow

1. **Always `git pull --rebase` before pushing** to integrate any autofix
commits that landed while you were working.
2. **If a push is rejected**, fetch and check for autofix commits:
```bash
git fetch origin <branch>
git log --oneline HEAD..origin/<branch>
```
3. **If an autofix commit addresses the same issue you just fixed**, skip
your commit (`git rebase --skip`) and keep the autofix, or resolve the
conflict in favor of whichever fix is more complete.
4. **Do not force-push** to overwrite autofix commits — rebase instead.

### Configuration

Bugbot autofix behavior is configured at
[cursor.com/dashboard/bugbot](https://www.cursor.com/dashboard/bugbot),
not in this repo. To disable auto-push entirely and keep comment-only
reviews, uncheck "Autofix" in the dashboard.

### When autofix is helpful

Autofix is most valuable for generated mirror PRs (`sync/public-release-mirror`)
where no human is actively pushing. For feature branches under active
development, consider disabling autofix to avoid the race.
9 changes: 9 additions & 0 deletions .github/actions/setup-rust/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@ runs:
with:
workspaces: ${{ inputs.workspaces }}

- name: Configure Cargo network resilience
shell: bash
run: |
# Retry transient crates.io fetch failures and bound HTTP timeouts so
# spurious network errors (e.g. "download of ba/se/base64 failed") do
# not fail CI runs that would otherwise pass.
echo "CARGO_NET_RETRY=5" >> "$GITHUB_ENV"
echo "CARGO_HTTP_TIMEOUT=30" >> "$GITHUB_ENV"

- name: Install cargo-llvm-cov
if: ${{ inputs.install-cargo-llvm-cov == 'true' }}
uses: taiki-e/install-action@d850aa816998e5cf15f67a78c7b933f2a5033f8a # v2.63.3
Expand Down
13 changes: 12 additions & 1 deletion packages/control-plane-rs/src/a2a/ledger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,13 @@ fn a2a_merge_history_parts(existing: Option<&Value>, candidate: &Value) -> Value
return candidate.clone();
};
let candidate_parts = candidate.as_array();
// Preserve existing parts that carry no text (attachments, data payloads).
// Drop existing parts that have a `text` field — whether pure plain-text or
// decorated with extra keys like `partId` — so stale summary text does not
// survive alongside the refreshed transcript text.
let mut merged: Vec<Value> = existing_parts
.iter()
.filter(|part| !a2a_history_part_is_plain_text(part))
.filter(|part| !a2a_history_part_has_text(part))
.cloned()
.collect();
if let Some(candidate_parts) = candidate_parts {
Expand All @@ -270,6 +274,13 @@ fn a2a_merge_history_parts(existing: Option<&Value>, candidate: &Value) -> Value
Value::Array(merged)
}

/// True when the part has a `text` field (plain or decorated). Used to decide
/// which existing parts to drop during a transcript-driven refresh.
fn a2a_history_part_has_text(part: &Value) -> bool {
part.as_object()
.is_some_and(|object| object.get("text").is_some_and(Value::is_string))
}

fn a2a_history_part_is_plain_text(part: &Value) -> bool {
let Some(object) = part.as_object() else {
return false;
Expand Down
8 changes: 7 additions & 1 deletion packages/control-plane-rs/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5409,7 +5409,7 @@ async fn a2a_task_load_preserves_rich_parts_when_history_message_is_refreshed()
"contextId": "ctx-rich-parts-refresh",
"role": "ROLE_AGENT",
"parts": [
{ "text": "stale summary" },
{ "text": "stale summary", "partId": "part-stale" },
{ "data": { "attachmentId": "artifact-rich-parts" } }
]
}
Expand Down Expand Up @@ -5452,6 +5452,12 @@ async fn a2a_task_load_preserves_rich_parts_when_history_message_is_refreshed()
"stale summary",
"stale plain-text part must not outlive the transcript refresh"
);
assert!(
!refreshed_parts_array
.iter()
.any(|part| part.get("partId").is_some()),
"decorated text part with partId must be replaced, not preserved with stale text"
);
}

#[tokio::test(flavor = "current_thread")]
Expand Down
7 changes: 6 additions & 1 deletion scripts/check-guardrail-regression-suite.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export const REQUIRED_GUARDRAIL_IDS = [
"opaque-git-parser-state",
"bounded-output-and-json-repair",
"a2a-ledger-evidence-parity",
"a2a-cancel-canonical-state-guard",
"a2a-compound-secret-redaction",
"a2a-history-rich-part-refresh",
"learner-transient-quarantine-parity",
"learner-promote-repair-consistency",
];

export function loadGuardrailManifest(path = DEFAULT_GUARDRAIL_MANIFEST) {
Expand Down Expand Up @@ -151,6 +156,6 @@ function main() {
console.log(`Guardrail regression suite covers ${result.guardrailCount} bug class(es).`);
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
main();
}
117 changes: 117 additions & 0 deletions scripts/guardrail-regression-suite.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,123 @@
]
}
]
},
{
"id": "a2a-cancel-canonical-state-guard",
"title": "A2A cancel canonical state guard",
"owner": "a2a",
"bugClass": "canceled tasks overwritten by later stores when status aliases are not canonicalized",
"why": "store_a2a_task_unless_canceled and a2a_canceled_task must canonicalize the status state before comparing so aliases like CANCELLED or STATE_CANCELED are respected and a canceled row is not overwritten.",
"evidence": [
{
"path": "packages/control-plane-rs/src/a2a/tasks.rs",
"contains": [
"fn a2a_task_is_canceled",
"canonical_a2a_task_state(state) == \"TASK_STATE_CANCELED\"",
"a2a_task_is_canceled(existing)"
]
},
{
"path": "packages/control-plane-rs/src/tests.rs",
"contains": [
"a2a_task_store_preserves_canceled_task_across_canonical_state_aliases"
]
}
]
},
{
"id": "a2a-compound-secret-redaction",
"title": "A2A compound secret redaction",
"owner": "a2a",
"bugClass": "compound credential metadata keys not redacted by exact-match-only secret detection",
"why": "a2a_ledger_metadata_key_is_secret must match suffix forms (webhookSecret, oauthToken, apiPassword) so compound credential fields are redacted, while preserving nonSecret/nonCredentials/notASecret exceptions and token-metric carve-outs.",
"evidence": [
{
"path": "packages/control-plane-rs/src/a2a/ledger.rs",
"contains": [
"fn a2a_ledger_metadata_key_has_secret_suffix",
"SECRET_SUFFIXES",
"NEGATION_PREFIXES"
]
},
{
"path": "packages/control-plane-rs/src/tests.rs",
"contains": [
"a2a_task_ledger_redacts_compound_secret_metadata_keys"
]
}
]
},
{
"id": "a2a-history-rich-part-refresh",
"title": "A2A history rich part refresh",
"owner": "a2a",
"bugClass": "non-text history parts dropped or stale text preserved during transcript-driven refresh",
"why": "a2a_merge_history_parts must preserve non-text parts (attachments, data payloads) while replacing all text-bearing parts (plain or decorated) with the refreshed transcript candidate so stale summary text does not survive.",
"evidence": [
{
"path": "packages/control-plane-rs/src/a2a/ledger.rs",
"contains": [
"fn a2a_merge_history_parts",
"fn a2a_history_part_has_text",
"!a2a_history_part_has_text(part)"
]
},
{
"path": "packages/control-plane-rs/src/tests.rs",
"contains": [
"a2a_task_load_preserves_rich_parts_when_history_message_is_refreshed"
]
}
]
},
{
"id": "learner-transient-quarantine-parity",
"title": "Learner transient quarantine parity",
"owner": "learner",
"bugClass": "transient setup failures depressing durable routing confidence across record, load, and trim paths",
"why": "record_outcome, load, and trim must all exclude transient failures from pattern updates so get_label_success_rate and get_confidence_adjustment stay consistent with get_recommendations. Ambiguous transient phrases must be gated behind environment context.",
"evidence": [
{
"path": "packages/ambient-agent-rs/src/learner.rs",
"contains": [
"fn is_transient_failure_reason",
"has_transient_environment_context",
"fn rebuild_patterns",
"outcome_is_transient_failure"
]
},
{
"path": "packages/ambient-agent-rs/src/learner.rs",
"contains": [
"test_transient_classifier_requires_environment_context_for_product_terms",
"test_recommendations_use_consistent_non_transient_stats_after_trim"
]
}
]
},
{
"id": "learner-promote-repair-consistency",
"title": "Learner promote repair consistency",
"owner": "learner",
"bugClass": "same pattern qualifying for both promote and repair or using inconsistent stats",
"why": "get_recommendations must use pattern_non_transient_stats for both promote and repair decisions and exclude promoted patterns from repair candidates so a single pattern cannot appear in both lists.",
"evidence": [
{
"path": "packages/ambient-agent-rs/src/learner.rs",
"contains": [
"fn pattern_non_transient_stats",
"promoted.contains",
"problematic_pattern_stats"
]
},
{
"path": "packages/ambient-agent-rs/src/learner.rs",
"contains": [
"test_recommendations_do_not_promote_and_repair_the_same_pattern"
]
}
]
}
]
}
22 changes: 0 additions & 22 deletions test/scripts/check-guardrail-regression-suite-entry.test.ts

This file was deleted.

2 changes: 1 addition & 1 deletion test/workflows/tag-release.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ describe("tag-release workflow", () => {
'if (( release_run_count > existing_count )) && [[ "${dispatch_status}" == "0" ]]; then',
);
expect(dispatchStep?.run).toContain(
'if [[ "${dispatch_status}" == "0" && -n "${latest_release_run_id}" && "${latest_release_run_id}" != "${existing_latest_run_id}" ]]; then',
'if [[ "${dispatch_status}" == "0" && -n "${latest_release_run_id}" && "${latest_release_run_id}" -gt "${existing_latest_run_id:-0}" ]]; then',
);
expect(dispatchStep?.run).toContain(
"up from ${existing_count} before dispatch",
Expand Down
Loading