Skip to content

feat(trello): add required label filter for webhook processing#2

Open
ada-evorada wants to merge 1 commit into
claude/cranky-johnsonfrom
feature/trello-required-label-filter
Open

feat(trello): add required label filter for webhook processing#2
ada-evorada wants to merge 1 commit into
claude/cranky-johnsonfrom
feature/trello-required-label-filter

Conversation

@ada-evorada
Copy link
Copy Markdown

Summary

  • Adds optional requiredLabelId config field to Trello integration that restricts webhook processing to cards carrying a specific Trello label
  • Implements label check inside TrelloRouterAdapter.dispatchWithCredentials (within credential scope, before ack comments are posted)
  • Adds "Required Label (optional)" searchable dropdown to TrelloFieldMappingStep wizard UI

Details

Config layer

  • Added requiredLabelId?: string to TrelloConfig (src/pm/config.ts), TrelloIntegrationConfig (src/db/repositories/configMapper.ts), and ProjectConfigRaw['trello']
  • Propagated requiredLabelId through buildTrelloConfig and RouterProjectConfig.trello

Backend filtering

  • New checkCardHasRequiredLabel(cardId, requiredLabelId) helper in router/trello.ts: returns true when no filter is set, or checks card labels via trelloClient.getCard() API call
  • Applied filter gate inside withTrelloCredentials callback in dispatchWithCredentials — only fires when requiredLabelId is configured AND workItemId is known. If card lacks the label, logs at info level and returns null

Frontend wizard

  • Added trelloRequiredLabelId field to WizardState, WizardAction, initial state, board reset, and edit state restore (buildEditState)
  • Added SET_TRELLO_REQUIRED_LABEL reducer case
  • Added "Required Label (optional)" section in TrelloFieldMappingStep with help text, below the Cost field, following the same searchable dropdown + manual fallback pattern
  • Included requiredLabelId in useSaveMutation config (only when non-empty)

Unit tests

  • tests/unit/router/trello.test.ts: 5 new tests for checkCardHasRequiredLabel
  • tests/unit/router/adapters/trello.test.ts: 4 new tests for label filtering in dispatchWithCredentials
  • Fixed pre-existing test failures from GitLab integration commit (webhook CLI tests missing gitlabOnly, credential-scoping test not clearing GITHUB_TOKEN_IMPLEMENTER/GITLAB_TOKEN_IMPLEMENTER)

Notes

  • Extra API call only happens when requiredLabelId is configured (opt-in, zero cost otherwise)
  • The 👀 reaction fires before dispatchWithCredentials, so filtered cards still get the emoji (per spec)

Trello card: https://trello.com/c/iELcotDw/11-in-the-trello-integration-configuration-under-field-mappings-custom-field-add-an-option-for-required-label-that-can-be-left-blan

🤖 Generated with Claude Code

@ada-evorada
Copy link
Copy Markdown
Author

CI Failures Resolved

Fixes Applied

  • Critical security vulnerability: Added axios override (>=1.15.0) to package.json to resolve CVE GHSA-3p68-rc4w-qgx5 (Axios NO_PROXY hostname normalization bypass leading to SSRF). The vulnerable version (1.13.5) was a transitive dependency of jira.js and trello.js.

Root Cause

The lint-and-test CI job runs npm audit --omit=dev --audit-level=high, which was failing due to the critical-severity axios vulnerability pulled in transitively.

Verification

  • npm audit --omit=dev --audit-level=high now exits 0 (only moderate vulnerabilities remain)
  • All 7357 unit tests passing
  • Lint and type checks passing
  • Changes pushed to branch

Copy link
Copy Markdown
Member

@suda suda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Clean, correct implementation of the required label filter. Logic is placed in the right layer, follows existing patterns throughout, and all CI checks pass.

Code Issues

Should Fix

Unrelated dependency change in the same PR: package.json and package-lock.json include an axios bump (1.13.5 → 1.15.0) and a proxy-from-env major version bump (1.1.0 → 2.1.0), plus a new axios: >=1.15.0 override. This is unrelated to the Trello label filter feature and has no mention in the PR description. It should either be pulled into a separate PR or at minimum documented in the description so reviewers can evaluate it on its own merits.

Minor Observations

  • Filter bypass when workItemId is absent: When requiredLabelId is configured but _event.workItemId is undefined, the filter is silently skipped and dispatch proceeds. This is intentional defensive behavior (non-card events pass through), but there is no test covering this scenario. A test asserting that dispatch is not skipped (or is skipped with a log) when workItemId is missing would make the intent explicit.

  • Error propagation path: If trelloClient.getCard() throws (transient API failure), the error propagates up to webhook-processor.ts where it is caught by the existing try/catch and logged as non-fatal. The card already received a 👀 reaction but no ack comment and no job will be queued. This is consistent with the existing error handling pattern across the adapter, so it is not a new problem — just worth knowing as a failure mode.

Everything else looks solid: config propagation is complete across all layers, reducer and buildEditState round-trip correctly, the UI dropdown follows the same FieldMappingRow + manual fallback pattern as the cost field, and the 9 new unit tests cover the meaningful branches.

Copy link
Copy Markdown
Member

@suda suda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Despite both cascade and bdgt having set required label in Cascade, moving a Trello card with the label for the bdgt project still was assigned to the cascade project (I removed some sensitive values):

{
  "model": {
    "id": "69d6891f7325a3eb7e",
    "url": "https://trello.com/b/N1R/cascade",
    "desc": "",
    "name": "Cascade",
    "prefs": {
      "voting": "disabled",
      "canBeOrg": true,
      "comments": "members",
      "selfJoin": true,
      "canInvite": true,
      "cardAging": "regular",
      "hideVotes": false,
      "background": "gradient-bubble",
      "cardCounts": false,
      "cardCovers": true,
      "isTemplate": false,
      "autoArchive": null,
      "canBePublic": true,
      "invitations": "members",
      "canBePrivate": true,
      "switcherViews": [
        {
          "enabled": true,
          "viewType": "Board"
        },
        {
          "enabled": true,
          "viewType": "Table"
        },
        {
          "enabled": false,
          "viewType": "Calendar"
        },
        {
          "enabled": false,
          "viewType": "Dashboard"
        },
        {
          "enabled": false,
          "viewType": "Timeline"
        },
        {
          "enabled": false,
          "viewType": "Map"
        }
      ],
      "backgroundTile": false,
      "backgroundColor": "#DCEAFE",
      "backgroundImage": "https://d2k1ftgv7pobq7.cloudfront.net/images/backgrounds/gradients/bubble.svg",
      "canBeEnterprise": true,
      "permissionLevel": "org",
      "sharedSourceUrl": null,
      "backgroundTopColor": "#E9F2FE",
      "showCompleteStatus": true,
      "backgroundDarkColor": "#172F53",
      "backgroundDarkImage": "https://d2k1ftgv7pobq7.cloudfront.net/images/backgrounds/gradients/bubble-dark.svg",
      "calendarFeedEnabled": false,
      "backgroundBrightness": "dark",
      "backgroundBottomColor": "#CFE1FD",
      "backgroundImageScaled": null,
      "hiddenPluginBoardButtons": []
    },
    "closed": false,
    "pinned": false,
    "descData": null,
    "shortUrl": "https://trello.com/b/NE1R",
    "labelNames": {
      "red": "Error",
      "sky": "project:cascade",
      "lime": "",
      "pink": "",
      "black": "",
      "green": "Processed",
      "orange": "project:bdgt",
      "purple": "Processing",
      "yellow": "Auto",
      "red_dark": "",
      "sky_dark": "",
      "blue_dark": "",
      "lime_dark": "",
      "pink_dark": "",
      "red_light": "",
      "sky_light": "",
      "black_dark": "",
      "blue_light": "",
      "green_dark": "",
      "lime_light": "",
      "pink_light": "",
      "black_light": "",
      "green_light": "",
      "orange_dark": "",
      "purple_dark": "",
      "yellow_dark": "",
      "orange_light": "",
      "purple_light": "",
      "yellow_light": ""
    },
    "idEnterprise": null,
    "idOrganization": "685e770eb2d98a11"
  },
  "action": {
    "id": "69d82b43914131c4a",
    "data": {
      "old": {
        "idList": "69d68913d3c103ebc1"
      },
      "card": {
        "id": "69d82b2306e1456038",
        "name": "Add ability to delete uploaded bank statements",
        "idList": "69d68925a3d3c103ebc3",
        "idShort": 16,
        "shortLink": "lm5G4J"
      },
      "board": {
        "id": "69d689125a3d3c103eb7e",
        "name": "Cascade",
        "shortLink": "NEJV1R"
      },
      "listAfter": {
        "id": "69d6891f7a3d3c103ebc3",
        "name": "Planning"
      },
      "listBefore": {
        "id": "69d6891f7325ac103ebc1",
        "name": "Backlog"
      }
    },
    "date": "2026-04-09T22:42:11.316Z",
    "type": "updateCard",
    "limits": null,
    "display": {
      "entities": {
        "card": {
          "id": "69d82b2306277c1456038",
          "text": "Add ability to delete uploaded bank statements",
          "type": "card",
          "idList": "69d6891f73a3d3c103ebc3",
          "shortLink": "lm554J"
        },
        "listAfter": {
          "id": "69d6891f7323d3c103ebc3",
          "text": "Planning",
          "type": "list"
        },
        "listBefore": {
          "id": "69d6891f25a3d3c103ebc1",
          "text": "Backlog",
          "type": "list"
        },
        "memberCreator": {
        }
      },
      "translationKey": "action_move_card_from_list_to_list"
    },
    "appCreator": null,
    "memberCreator": {

    }
  },
  "webhook": {
    "id": "69d768999c617f2a0",
    "active": true,
    "idModel": "69d6891f7c103eb7e",
    "description": "CASCADE webhook",
    "consecutiveFailures": 0,
    "firstConsecutiveFailDate": null
  }
}

@ada-evorada

Copy link
Copy Markdown
Member

@suda suda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

LGTM — the required-label filter is well-designed and well-tested. One naming inconsistency to fix and two undocumented assumptions worth noting.

Code Issues

Should Fix

  • src/router/adapters/trello.ts, dispatchWithCredentials signature — The first parameter is named _event (underscore prefix = intentionally unused by convention), but the method now actively reads _event.workItemId in the label-check block. Compare github.ts, which names the same parameter event (no underscore) when it is consumed. Rename to event for consistency and to avoid misleading readers.

Comments (no action required)

  • src/router/adapters/trello.ts parseWebhook — Uses config.projects.find() (first match) to evaluate isCardInTriggerList and isReadyToProcessLabelAdded. This is correct when all shared-board projects use identical list IDs and the same readyToProcess label — the expected configuration for the multi-project use case. It would silently drop events if two projects on the same board were configured with different readyToProcess label IDs. Worth a brief inline comment to document the assumption.

  • src/router/webhook-processor.ts, multi-project dispatch loop — The loop breaks on the first non-null dispatch. A project with no requiredLabelId (catch-all) placed before filtered projects will always win, making subsequent filtered projects unreachable for matching events. This is correct by design, but operators setting up shared-board projects need to know about this ordering constraint. Worth a note in CLAUDE.md or the wizard UI help text.

Copy link
Copy Markdown
Member

@suda suda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately the run is still being assigned to the wrong project. Interestingly both projects (cascade and bdgt) have the required label set, therefore it I would assume that the incoming webhook would not be routed at all rather than being routed to the wrong project.
I see that the webhook payload from Trello does not pass the labels assigned to the card, just the information about from which and to which board it was moved. It might be necessary to look up the labels first before routing.

@ada-evorada

@ada-evorada
Copy link
Copy Markdown
Author

@suda Thanks for the detailed diagnosis! You're right — the Trello webhook payload for updateCard doesn't include the card's current labels, so we need an explicit API lookup.

The root issue was architectural: checkCardHasRequiredLabel was called inside dispatchWithCredentials per project in order. If cascade was first in the list and had no requiredLabelId (or an incorrectly matching one), it dispatched before bdgt got a chance.

Fix applied: resolveAllProjects now fetches the card's labels via trelloClient.getCard() before the dispatch loop starts. It then:

  • Returns only projects whose requiredLabelId matches a label on the card (e.g. only bdgt for a card with the bdgt label)
  • Falls back to catch-all projects (no requiredLabelId) if no specific match
  • Falls back to all candidates if the API call fails (the per-project guard in dispatchWithCredentials remains as a secondary check)

This ensures the correct project is selected before any dispatch attempt. The _eventevent rename in dispatchWithCredentials (flagged in your previous approved review) is included as well.

Copy link
Copy Markdown
Member

@suda suda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

The feature is well-structured and the multi-project routing logic is clearly reasoned. However, there is a logic flaw in webhook-processor.ts: returning an empty array from resolveAllProjects (label mismatch — "don't process this event") incorrectly falls through to resolveProject, triggering an extra Trello API call and violating the method's contract. In the happy path this also means a double getCard call per filtered event.

Code Issues

Should Fix

src/router/webhook-processor.ts:337-344resolveAllProjects([]) incorrectly falls through to resolveProject

When resolveAllProjects is implemented and returns [] (label mismatch), the fallback treats it identically to "method not implemented":

const projectsToTry: RouterProjectConfig[] = [];
if (adapter.resolveAllProjects) {
    const allProjects = await adapter.resolveAllProjects(event);
    projectsToTry.push(...allProjects); // pushes nothing when []
}
if (projectsToTry.length === 0) {     // true even when resolveAllProjects returned []
    const singleProject = await adapter.resolveProject(event); // ← incorrectly called
    if (singleProject) projectsToTry.push(singleProject);
}

Concrete scenario: project has requiredLabelId, resolveAllProjects returns [] because the card lacks the label → falls through to resolveProject which returns the project by boardId → dispatchWithCredentials is invoked → secondary guard calls checkCardHasRequiredLabel (extra Trello API call) → correctly returns null. Outcome is correct but the path is wrong and wastes an API call. It also logs "No trigger matched" instead of the accurate "No project config found".

The correct branching:

let projectsToTry: RouterProjectConfig[] = [];
if (adapter.resolveAllProjects) {
    projectsToTry = await adapter.resolveAllProjects(event);
} else {
    const singleProject = await adapter.resolveProject(event);
    if (singleProject) projectsToTry = [singleProject];
}

There is also no test covering this case: resolveAllProjects returns [] → should short-circuit as "no project config found". The existing test "returns No trigger matched when all projects dispatch null" uses a non-empty array from resolveAllProjects and relies on dispatch returning null — a different path.

src/router/adapters/trello.ts:200-213 — double getCard API call in happy path

When resolveAllProjects successfully pre-filters and returns matched projects, dispatchWithCredentials still unconditionally calls checkCardHasRequiredLabel (a second getCard call). In the standard happy path for a project with requiredLabelId:

  1. resolveAllProjectsgetCard (call 1) → returns [project]
  2. dispatchWithCredentialscheckCardHasRequiredLabelgetCard (call 2)

The secondary guard is documented as a safety net for when resolveAllProjects errored and returned all candidates unfiltered. However it fires unconditionally in the happy path too, doubling Trello API consumption for every webhook event on label-filtered projects. If the fallback logic in webhook-processor.ts is fixed per above (so resolveAllProjects([]) short-circuits), the guard could be conditioned on whether pre-filtering was definitive.

Copy link
Copy Markdown
Member

@suda suda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

The feature is well-designed and thoroughly tested, but there is one blocking bug that will silently prevent the label filter from ever activating in production.

Blocking

requiredLabelId stripped by Zod validation in src/config/schema.ts — feature never activates at runtime

ProjectConfigSchema (lines 56–67 of src/config/schema.ts) defines the Zod schema for the Trello config object but does not include requiredLabelId. Zod strips unknown keys by default in .object().parse(), so the field is silently discarded every time config is loaded from the database.

Runtime path:

DB JSONB (has requiredLabelId)
  → mapProjectRow / buildTrelloConfig  ✓ field present
  → validateConfig(rawConfig)          ✗ Zod strips unknown field
  → ProjectConfig.trello.requiredLabelId  === undefined (always)

Both loadAllProjectConfigs (line 81) and findProjectConfigFromDb (line 104) in configRepository.ts call validateConfig, so every router/worker config load hits this path.

Consequence: project.trello?.requiredLabelId is always undefined at runtime. The pre-filter in resolveAllProjects and the secondary guard in dispatchWithCredentials never activate. Every card is processed regardless of its labels.

The unit tests don't catch this because they mock loadProjectConfig directly, bypassing Zod entirely.

Fix — add one line to src/config/schema.ts inside the trello object schema, after the customFields block:

requiredLabelId: z.string().optional(),

Copy link
Copy Markdown
Member

@suda suda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

LGTM — well-designed, correctness-first implementation of label-based routing with solid test coverage across all edge cases.

Architecture & Design

The two-layer defense is appropriate:

  1. **Pre-filter in ** — one API call before the dispatch loop; correct project(s) selected early
  2. **Secondary guard in ** — fallback when errored and returned all candidates unfiltered

This avoids redundant calls in the happy path while preserving correctness under API failure. The flag is a reasonable internal coupling mechanism, well-documented with JSDoc.

Code Issues

Nitpick

  • In (), the as string cast on initialConfig.requiredLabelId is redundant — the field is already typed as string | undefined on TrelloIntegrationConfig. The ?? '' handles the undefined case correctly. Harmless.
  • Catch-all projects in resolveAllProjects are marked _labelPreFiltered: true when returned from the catch-all branch, even though they have no requiredLabelId filter. This is harmless (the secondary guard short-circuits on falsy requiredLabelId regardless), but slightly misleading — it implies label verification occurred when it didn't.

Verified

  • All CI checks passing (lint, typecheck, unit + integration tests)
  • The resolveAllProjects loop correctly uses the first project's credentials to make a single getCard call, then routes all candidates based on those labels — correct since all projects sharing a board ID access the same card data
  • The 👀 reaction intentionally fires before label filtering (step 5 vs step 6); acknowledged in PR description as per spec
  • No-workItemId edge case: both the pre-filter and secondary guard skip label checks when workItemId is absent, so a card without a known ID falls through to all candidates — acceptable for events where card ID cannot be determined
  • Axios upgrade (>=1.15.0 override) is a routine security patch, correct approach

Copy link
Copy Markdown
Member

@suda suda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now that in the Webhook Log Detail the hook has been assigned to the correct project ID bdgt. However I see the agent failing with:

❌ Error: Error: Integration credential 'scm/implementer_token' not found for project 'cascade'

So somewhere next in the flow the wrong project is being picked up.

Copy link
Copy Markdown
Member

@suda suda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

LGTM — the implementation is correct and the defensive two-layer filtering design is sound.

Observations (non-blocking)

on the catch-all path is redundant but harmless

When falls back to catch-all projects (those without a ), it marks them . The secondary guard in checks first, so the flag is never consulted for catch-all projects (they have no label filter). Zero correctness impact, but it slightly overstates what the flag means.

Events without bypass label filtering entirely

If event.workItemId is absent, resolveAllProjects returns all candidates unfiltered, and the secondary guard in dispatchWithCredentials also skips (condition requires event.workItemId). Both projects on a shared board would have triggerRegistry.dispatch called. In practice this shouldn't matter since every processable card event carries a card ID, but it's an edge case worth noting.

Missing test for skipping the secondary guard

The test suite covers dispatching with and without requiredLabelId, and it covers resolveAllProjects pre-filtering, but no test verifies that a project with _labelPreFiltered: true actually skips the checkCardHasRequiredLabel call in dispatchWithCredentials. The optimization works correctly from code inspection, but a test would lock in the behaviour.

👀 reaction fires before label filtering for comment events

Step 5 fires the reaction, label filtering happens at step 7. A comment on a card lacking the required label will receive a 👀 but no agent action. The PR notes this is per-spec, and the reaction is only sent for comment events while label filtering primarily targets card-move/label-add events — so the practical overlap is narrow.

None of these are blocking. All CI checks pass, tests are thorough (9 new test cases for the core filtering logic, 4 for the adapter dispatch paths), and the end-to-end worker path correctly threads projectId through to the PM webhook processor.

@suda suda force-pushed the claude/cranky-johnson branch from 96f5136 to d2014d8 Compare June 1, 2026 16:28
suda pushed a commit that referenced this pull request Jun 1, 2026
Adds src/integrations/pm/trello/ with:
- manifest.ts: PMProviderManifest wiring TrelloIntegration, TrelloRouterAdapter,
  all 7 Trello trigger handlers, and TrelloPlatformClient. verifyWebhookSignature
  wraps the existing verifyTrelloSignature (HMAC-SHA1 of body+callbackUrl) with
  Host/X-Forwarded-Proto header reconstruction — no shared factory because
  Trello's signing scheme is unique among providers.
- index.ts: side-effect module that calls registerPMProvider(trelloManifest).

src/integrations/pm/index.ts: new barrel importing ./trello/index.js for
the side effect. Plans 006/3 and 006/4 append jira + linear.

Contract adjustments surfaced during TDD:
- Dropped parseWebhookPayload field from PMProviderManifest (redundant with
  routerAdapter.parseWebhook; had wrong return type in 006/1). Each caller
  uses the appropriate one: router uses routerAdapter, PM-domain code uses
  pmIntegration.parseWebhookPayload.
- Relaxed conformance harness's platform-client assertion from 3 methods to
  2 (postComment + deleteComment). updateComment/postReaction are provider
  extensions, not contract.
- registerTestProvider is now additive (no longer resets the registry), so
  the conformance harness iterates TestProvider AND every real provider
  side-by-side — validating AC #2 of the spec.

Tests: 15 new Trello manifest tests + conformance now 22 (11 per provider x 2).

Trello's legacy registrations (bootstrap.ts, builtins.ts, worker-env extractor
branch, pm-wizard.tsx branch) still fire — removed in task 3 of this plan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
suda pushed a commit that referenced this pull request Jun 1, 2026
Closes plan 006/5 — the final cleanup plan of spec 006.

Backend:
- src/integrations/bootstrap.ts — DELETED. PM registrations flow through
  src/integrations/pm/index.ts (which also mirrors manifests into
  integrationRegistry). SCM (GitHub) and alerting (Sentry) self-register
  via new side-effect modules.
- src/github/register.ts + src/sentry/register.ts — new minimal
  side-effect modules that replace the respective branches of the
  deleted bootstrap. SCM + alerting stay on the legacy IntegrationModule
  pattern (out of spec 006 scope).
- src/pm/registry.ts — converted to a read-only adapter over
  pmProviderRegistry. get/getOrNull/all/createProvider/
  resolveLifecycleConfig all delegate to the manifest registry.
  register() is a deprecation warn. The ~9 unmigrated call sites
  (webhook handlers, manual runner, credential scope, lifecycle, GitHub
  adapter) keep working unchanged — they now transparently read from
  the manifest registry, so there is no divergent registration.
- src/router/index.ts + src/worker-entry.ts — updated to import the
  three new side-effect modules instead of the deleted bootstrap.
- src/integrations/registry.ts — header comment updated to reflect
  the new population topology.

Tests:
- tests/unit/integrations/bootstrap.test.ts — rewritten to cover the
  new side-effect-module wiring (the file name stays for audit clarity;
  its content asserts the same end-state invariants).
- 8 other test files that imported bootstrap.js for side effect are
  migrated to import the three new modules (pm barrel + github/register
  + sentry/register).

Docs:
- src/integrations/README.md — rewritten. Transitional note + "Legacy
  path" section removed. The README is now the single canonical
  author's guide for adding a new PM provider.
- CLAUDE.md — integration-abstraction pointer updated to final state.
- CHANGELOG.md — entry per plan.

Plan-divergence:
- AC #2 (delete pm/registry.ts) — became: convert to a read-only
  delegate. Deleting it would require migrating 9 call sites that are
  out of spec scope. The delegate preserves the end state (single
  source of truth = pmProviderRegistry) without downstream churn.
- AC #5 (consolidate createXxxLabel tRPC endpoints) — deferred to a
  follow-up PR. Purely additive cleanup; not required for any spec AC.

Both divergences documented in the .done plan.

Tests: 7809/7809 pass. Build + lint + typecheck clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
suda pushed a commit that referenced this pull request Jun 1, 2026
…tructured envelope, --comment alias) (mongrel-intelligence#1190)

* docs(014): spec + plans for cascade-tools agent ergonomics

Adds docs/specs/014-cascade-tools-agent-ergonomics.md plus two plans
covering shared-infra and create-pr-review adoption. Prompted by prod
run 5d993b04-6e05-4ae1-b7de-8c274cf3496b.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(plan-014): lock plan 1 (shared-infra)

* feat(cascade-tools): plan 014/1 shared-infra — truthful prompts + envelope

Ships the root-cause fix for prod run 5d993b04-6e05-4ae1-b7de-8c274cf3496b
plus the shared infrastructure every future gadget inherits:

- System-prompt renderer (src/backends/shared/nativeToolPrompts.ts) stops
  stripping trailing 's' from array param names and claiming '<string>
  (repeatable)' for every array. Array-of-object params now render as
  `--<flag> '<json>'` with aliases appended via `|` and a one-line runnable
  example from the tool definition.
- Factory (src/gadgets/shared/cliCommandFactory.ts) gains oclif flag aliases,
  JSON parsing for array-of-object flags, file-input JSON parsing, `examples`
  wired into oclif `--help`, and Levenshtein-based 'did you mean' suggestions
  for mistyped flags (via fastest-levenshtein).
- New shared error envelope (src/gadgets/shared/errorEnvelope.ts) — every
  CLI failure emits `{"success":false,"error":{type,flag?,message,got?,
  expected?,hint?,example?}}` on stdout plus a one-line prose summary on
  stderr. All prior `this.error()` / flat `{success:false,error:"<string>"}`
  call sites migrated.
- Contracts widened: ParameterDefinition gains `cliAliases`, FileInput-
  Alternative gains `parseAs`, ToolManifest parameters carry `items`,
  `aliases`, `example`.
- Manifest generator threads the new fields through.
- bin/cascade-tools.js wraps `run()` to swallow oclif ExitError cleanly so
  the envelope isn't obscured by Node's default stack dump.

Plan-1 ACs #1mongrel-intelligence#17 all delivered. 8438/8438 unit tests passing.

Test surface delta: 57 new unit tests across errorEnvelope.test.ts,
shared-nativeToolPrompts.test.ts, and factories.test.ts. Seven legacy
assertions encoding the pre-014 error surface updated in cli/cli-command-
factory, cli/file-input-flags, cli/scm/create-pr-sidecar, cli/scm/create-
pr-review-sidecar, backends/claude-code.

Plan 2 adopts the pattern on createPRReviewDef — zero shared-file edits —
proving the declarative-metadata invariant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(plan-014): lock plan 2 (createprreview-adopt)

* feat(cascade-tools): plan 014/2 createprreview-adopt + spec done

Applies the spec-014 declarative-metadata pattern to createPRReviewDef:

- --comment alias for --comments (the exact muscle-memory mistake from
  prod run 5d993b04-6e05-4ae1-b7de-8c274cf3496b).
- --comments-file <path> (and - for stdin) JSON-parsed escape hatch for
  long payloads that don't survive shell quoting.
- Two declarative fields on createPRReviewDef.parameters.comments.cliAliases
  + createPRReviewDef.cli.fileInputAlternatives. Zero edits to shared
  infrastructure (cliCommandFactory, manifestGenerator, nativeToolPrompts,
  errorEnvelope) — proves spec 014's single-entrypoint invariant.

Per-plan ACs #1, #2, #3, #5, #6, mongrel-intelligence#7, mongrel-intelligence#8, mongrel-intelligence#9, mongrel-intelligence#11, mongrel-intelligence#12 auto-verified
(unit tests + build + lint + typecheck). AC #4 (binary-level smoke)
tagged [manual] because vitest fork-pool workers fail to capture
stdout/stderr from spawned binaries that do top-level await import();
the six scenarios were verified manually against the built binary and
the trace is recorded in the plan. AC mongrel-intelligence#10 n/a — integration test path
abandoned for the same reason.

All plans done. Spec 014 marked .done (docs/specs/014-*.md → .done).
CHANGELOG Unreleased updated with a per-plan entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
suda pushed a commit that referenced this pull request Jun 1, 2026
…-intelligence#1261)

* docs(spec): 018 alerting agent + worker boot-failure visibility + plans

Spec captures two related gaps surfaced by the first sentry-bound agent run
(2026-05-06): alerting agent's missing prompt template and the silent worker
boot-failure path that masked it.

Two plans downstream: alerting-prompt (feature) and boot-failure-visibility
(hardening, follow-on to spec 017). Linear DAG, plan 1 → plan 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(plan): 018/1 lock

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(agents): alerting agent system prompt template (spec 018, plan 1)

Author the system prompt template that the alerting agent's worker tries to
load at boot. Every other piece of the alerting agent — YAML definition,
capabilities, trigger handlers, context pipeline, Sentry integration — was
already wired. Missing the .eta produced an ENOENT at agent boot when the
first prod-traffic Sentry alert arrived (cascade project, 2026-05-06). After
this plan, an alerting agent dispatched via existing trigger handlers reaches
its execution phase end-to-end.

The prompt structures the agent's behavior as a three-phase investigator
(parse pre-loaded event → confirm root cause via source reads → file or
comment) with an explicit INVESTIGATE-AND-FILE-ONLY guardrail. Predictable
output structure: 'Investigate: <ErrorType> in <Function> (<file>:<line>)'
title and a 4-6 sentence + bullets description.

The agent's read-only guarantee is enforced statically by the YAML's
capability declaration (no fs:write, no scm:*) — the resolved gadget allowlist
excludes WriteFile, CreatePR, CreatePRReview. Plan task #2 was downgraded from
a heavy E2E integration test to a static capability-allowlist test (more
reliable than behavioral negative-assertions that depend on LLM cooperation);
divergence noted in the plan itself.

Tests: 10 new in tests/unit/agents/prompts.test.ts; 4 new in
tests/unit/agents/definitions/alerting.yaml.test.ts. All green; full unit
suite (8808 tests) green; lint + typecheck clean.

Spec ACs 1-6 satisfied (full). Marks plan 018/1 as .done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rebased onto claude/cranky-johnson (upstream/dev + GitLab SCM provider).
Consolidates the trello-required-label-filter feature branch and its
iterative WIP fix commits into one commit on the new base. The duplicate
GitLab commit and the redundant test-fix/axios commits are dropped (already
present on cranky-johnson); the projectId-threading fix is also redundant
(upstream already threads `preferredProjectId`).

When a Trello project sets `requiredLabelId`, only cards carrying that label
trigger CASCADE; others are skipped. Supports shared boards where multiple
projects map to the same board, distinguished by required label.

- Config: `requiredLabelId` added to the shared `trelloConfigSchema`,
  RouterProjectConfig, configMapper, and pm/config so it survives DB
  round-trips and reaches the router.
- Routing: `resolveAllProjects` pre-filters candidate projects by the card's
  labels (one Trello API lookup), marking matches `_labelPreFiltered`;
  `dispatchWithCredentials` keeps a secondary label guard (skipped when
  pre-filtered) and still wraps dispatch in upstream's `withPMScopeForDispatch`.
- webhook-processor iterates all matching projects, dispatching to the first
  that returns a result, then delegates to `handleTriggerOutcome`.
- Wizard: `trelloRequiredLabelId` wired through the aggregate WizardState, the
  trello provider state slice, and the provider's buildIntegrationConfig /
  buildEditState. The Label Mapping step composes a required-label picker
  (board labels with a manual-ID fallback) onto the shared component.
- Tests updated for the new state field and multi-project routing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@suda suda force-pushed the feature/trello-required-label-filter branch from e6babc9 to 56611d6 Compare June 3, 2026 14:04
@suda
Copy link
Copy Markdown
Member

suda commented Jun 3, 2026

⚠️ review agent failed

Error: Claude Code returned an error result: Failed to authenticate. API Error: 401 terminated

Manual intervention may be required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants