Skip to content

ci: add GitHub Actions workflow for Claude MD - #19

Closed
drobnikj wants to merge 1 commit into
developfrom
add-github-workflow-claude-md
Closed

drobnikj wants to merge 1 commit into
developfrom
add-github-workflow-claude-md

Conversation

@drobnikj

Copy link
Copy Markdown
Member

Summary

This PR adds a GitHub Actions workflow for Claude MD maintanace.

Automated PR created by create-workflow-prs.sh.

@drobnikj
drobnikj requested a review from JanHranicky August 11, 2026 13:20
@drobnikj drobnikj closed this Aug 17, 2026
drobnikj pushed a commit that referenced this pull request Sep 3, 2026
…HQ#21493)

* feat(slack_v2): eval-driven fixes for the MCP tool surface

Iterated against the MCP eval suite (pd-connect-eval-monster/evals/slack_v2).
Suite went 23/35 -> 33/35 on Sonnet 5 (pass^2); every change below is tied to a
specific red-to-green flip or a measured payload reduction.

- list-channels: evals #2/#10/#11/#16/#19/PipedreamHQ#35 all failed the same way — the
  action returned every field of every channel with numPages defaulting to 1, so
  a 62-channel workspace produced ~55k chars, blew the 25k-token MCP ceiling, and
  the model was handed a file path instead of data (#2 burned all 20 turns
  re-calling it). Added an additive `fields` projection, a `cursor` prop, and
  `has_more`/`next_cursor` so truncation is visible instead of silent. Payload
  55k -> 5.9k chars; all six evals pass.  [minor]

- slack_v2.app.mjs: `assistantSearch` called `sdk().apiCall()` directly — the one
  path in the app that bypassed `_withRetries` — while the client is built with
  `rejectRateLimitedCalls: true`, so every 429 rejected instantly. 8 of 12 search
  calls errored in one run (#24/#26/#27). Routed through `_withRetries`. Also adds
  `resolveUserId` (id / email / display name) for the invite fix below.

- delete-message: eval #20 ("post a note then take it back") failed every run with
  cant_delete_message — post-message sends no `as_user` so it posts as the USER,
  while this action defaulted `as_user: false`, which routes to the BOT token. A
  message just posted could never be deleted. Now retries with the other identity
  on cant_delete_message; the default is unchanged, so existing workflows are
  unaffected. Description also gained confirmation guidance for the destructive
  path (PipedreamHQ#33).  [minor]

- invite-user-to-channel: eval #10 failed with user_not_found — the agent passed
  the email from the prompt. Now resolves user id / email / display name, and the
  channel by name.  [minor]

- get-channel-details, list-members-in-channel, set-channel-topic: every
  AI-optimized action in this app resolves channel NAMES server-side and these did
  not, so agents that read "#seinfeld-general" from a prompt got channel_not_found
  (#19, #22) or a bare internal_error (#10). All three now use resolveChannelId.
  [minor]

- get-channel-history, get-thread-replies: outputs measured at 17k chars average
  (worst 49k) and 25k average respectively — every call burned that much of the
  agent's context. Added the same additive `fields` projection.  [minor]

- get-user-details, get-current-user: two tools answer "who am I" and the agent
  chose the legacy one in 10/10 trials (#1). Descriptions now differentiate them.
  NOTE: this did NOT change routing — the agent still picks get-current-user. The
  actual fix is removing one of the two from the /v3 component allowlist; these
  edits only make the intended split legible.  [patch]

App package.json bumped 0.6.1 -> 0.7.0.

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

* fix(slack_v2): satisfy CI — lint formatting, dependent version bumps, UI-centric copy

- Lint Code Base: 9 errors, all in the files this branch touches and all
  formatting. Broke the `.split().map().filter()` chains across lines
  (newline-per-chained-call), split the `has_more` spread ternary and its object
  literal (multiline-ternary, object-curly-newline), and expanded the
  `users.list` destructure in resolveUserId (object-curly-newline).

- Ensure component commits modify component versions: this branch edits
  slack_v2.app.mjs, so every component importing it needs a version bump.
  Patch-bumped the 48 actions/sources that were not already versioned in the
  previous commit.

- Removed UI-centric wording from two descriptions, which read oddly for an
  agent calling the tool over MCP and contradicted the resolution behaviour
  added in the previous commit:
  - get-channel-details: "by selecting it or providing an ID" -> "specified by
    ID or by name" (it had claimed both "selecting it" and "Accepts a channel
    ID or NAME" in the same sentence).
  - set-channel-topic: "a selected channel" -> "a channel, specified by ID or
    by name".

Verified locally with ESLint 9 against the repo's own rule options for the three
failing rules: 0 violations across components/slack_v2.

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

* fix(slack_v2): restore comma-separated users on invite, fix delete identity fallback

invite-user-to-channel: conversations.invite documents `users` as comma-separated and
the prop is a plain string, so "U01ABCDEF,U02GHIJKL" was a working input. The new
resolveUserId() matches neither the ID regex (no comma) nor the email regex, then
falls through to an exhaustive paged users.list scan that cannot match, and throws.
Adds resolveUserIds() (plural), which splits on commas — or accepts an array —
resolves each token independently, and rejoins. An all-IDs input still makes zero
extra API calls, so the common path is unchanged in cost as well as result.

delete-message: makeRequest() routes to the bot token only on `as_user === false`, so
`!this.as_user` made the retry a no-op whenever as_user arrived nullish — undefined
and true both route to the user token, so it retried as the identity that had just
been refused. Changed to `this.as_user === false`. Attempt 1 still passes the
configured value verbatim.

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

* docs(slack_v2): reattach resolveChannelId's JSDoc to its function

resolveUserId/resolveUserIds were inserted between resolveChannelId's doc comment
and resolveChannelId itself, leaving two stacked doc blocks above resolveUserId and
no doc on resolveChannelId. Comment move only; no code change.

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

* refactor(slack_v2): move the fields projection into common/utils.mjs

Addresses review feedback on PipedreamHQ#21493: `pickFields` was copy-pasted identically into
get-channel-history, get-thread-replies and list-channels.

Moved to components/slack_v2/common/utils.mjs, alongside the CSV-or-array `fields`
normalizer that was duplicated in the same three places — both halves of one feature,
so splitting them across files would just invite the next divergence.

`projectFields(records, fields)` composes the two and keeps the additive contract:
with no `fields`, it returns the ORIGINAL array, so a caller that omits the prop gets
exactly what these actions have always returned.

Behavior-identical to the three removed copies, verified against them across
undefined / empty array / empty string / array / CSV / CSV-with-spaces / unknown
field / all-unknown inputs, including array identity on the no-fields path.

No version bumps: all three components are already ahead of master on this branch,
so the PR diff still shows a version change for each.

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

* fix(slack_v2): address review feedback on delete-message, resolveUserId, invite-user-to-channel

- delete-message: only retry with the other identity when a bot token exists,
  since otherwise both attempts hit the same token and just repeat the error.
- resolveUserId: scan all users.list pages and require an exact, unambiguous
  name match; throw ConfigurationError instead of silently picking the first.
- invite-user-to-channel: treat already_in_channel as success only for a
  single user; bulk invites are now sent per-user with individual results so
  failures aren't hidden behind a successful summary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(slack_v2): guard invite-user-to-channel against empty resolved user list

resolveUserIds() returns "" when every comma-separated token is blank, and
"".split(",") yields [""] rather than [], which slipped through the
single-user path and sent an empty users value to Slack. Filter empty tokens
and throw ConfigurationError when none remain.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(slack_v2): resolve channel name in delete-message, treat self-invite as no-op

- delete-message now calls resolveChannelId() before deleting, like
  every other AI-optimized action in this app. Previously it passed
  `conversation` straight through, so a channel NAME (rather than ID)
  failed with channel_not_found.
- invite-user-to-channel: Slack refuses to let an identity invite
  itself (cant_invite_self) before it would ever reach an
  already_in_channel check. Treat that the same way — a no-op, not an
  error — since to the caller it means the same thing: the user has no
  further action needed on that channel.

Found via eval-driven regression testing on PR PipedreamHQ#21493.

* chore(slack_v2): revert version bumps on delete-message, invite-user-to-channel

Keep at 0.2.0/0.1.0 rather than bumping per fix.

---------

Co-authored-by: Dylan Sather <Dylan Sather>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Michelle Bergeron <michelle.bergeron@workday.com>
Co-authored-by: michelle0927 <michelle0927@users.noreply.github.com>
daveomri pushed a commit that referenced this pull request Sep 22, 2026
…1929)

* feat(form_io): AI-optimized Form.io CRUD action set for MCP (PipedreamHQ#21667)

Adds the first action set for Form.io, designed for the MCP tool surface and
hardened against the eval suite in pd-connect-eval-monster/evals/form_io.
Suite green on Sonnet 5 at 22/24 (pass^2); the 2 residual non-passes are flaky
create-then-act cases caused by Form.io read-after-write lag over the MCP path,
not component bugs (#14 update-role recovered to a correct final state; #19
update-form-action is a discoverability flake, not a hard error).

Forms, submissions, roles, and form actions each get list/get/create/update/delete:

- list-forms / list-submissions / list-roles / list-form-actions: paginated reads
  with limit/skip/sort props; the app supplies discoverable ID prop descriptions
  ("Run List Forms first to obtain valid form IDs") so IDs are reachable.  [new 0.0.1]
- get-* / create-* / update-* / delete-*: single-record CRUD across all four
  resources, on a shared _makeRequest built from the project-scoped base URL
  (https://{project_id}.form.io) + x-token auth header.  [new 0.0.1]
- common/constants.mjs, common/utils.mjs: shared API header + limit bounds and a
  parseJson helper; extractId resolves Form.io's `_id` response field.  [new]
- form_io.app.mjs: full implementation (base URL, headers, _makeRequest, ID +
  pagination prop definitions) replacing the placeholder scaffold.

App package.json bumped 0.0.3 -> 0.1.0 (minor: adds 20 new actions).

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

* fix(form_io): address PR review — JSON-shape validation, role read-merge-write, sharper docs

Applies the review feedback on PipedreamHQ#21929:

- update-role: Form.io's role PUT replaces the whole document, so a partial
  payload blanked out omitted fields. Now fetches the current role and merges
  only the provided fields before the PUT, and rejects a no-op update (no fields
  provided) instead of sending a destructive partial payload.
- parseJson (common/utils): added an optional `expect` ("object" | "array") shape
  check so a valid-JSON-but-wrong-shape prop fails with a prop-named message
  instead of an opaque API 400.
  - create-submission: `data` validated as a JSON object.
  - update-form: `components` validated as a JSON array, `settings` as a JSON object.
- Docs: get/update/delete-role now show a complete 24-char hex role `_id` example
  and point at List Roles (replaces the truncated `64f...abc` placeholder in
  update-role); update-form-action `settings` gains a concrete inline JSON example.

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

* fix(form_io): avoid false duplicate-key CI hit in parseJson error example

The findDuplicateKeys CI check greps every component file for the first
`key: "..."` and treats the captured value as a component key. The JSON example
in parseJson's object-shape error message (`{ "key": "value" }`) matched that
pattern, so the script read `value` as form_io's key and collided with
ironclad's utils.mjs. Reworded the example to `{ ... }` (matching the array
message's style) — no `key: "..."` literal, no false positive.

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

* fix(form_io): make update-role concurrency-safe by writing only changed fields

PR review flagged a lost update in update-role: it read the whole role and PUT
the snapshot back, so a field changed by a concurrent update in between was
overwritten by the stale snapshot.

Verified against the live Form.io API that PUT /role/{id} MERGES a partial body
server-side (a PUT with only {description} preserves title) — so the read-merge
was both unnecessary and the cause of the race. Now update-role sends only the
fields the user provided; omitted fields are preserved by the server, and two
concurrent updates to different fields both survive (confirmed with a concurrent
integration test). PATCH is not an option here — Form.io returns 405 on the role
resource.

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

* fix(form_io): reject empty required IDs with a clean error (harden path building)

Council PR-verify QA flagged that every id-bearing action, given an empty id,
built a malformed URL (e.g. `/role/`) and got back an opaque HTML 404 from
Form.io instead of a usable error. Added a shared `_requireId` app helper that
validates a required path id is non-empty and URL-encodes it before it enters
the request URL; every path interpolation (formId/submissionId/roleId/actionId)
now goes through it. Non-string ids are coerced (a number becomes a clean 404,
not a 500), and odd characters are encoded rather than breaking the path.

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

* fix(form_io): validate form `type` enum and coerce string props (avoid Form.io 5xx)

Council QA found two body-field cases where Form.io returns a raw 5xx instead of
a clean error: a non-string `path` (e.g. a number) → HTTP 500, and a `type`
outside `form`/`resource` → HTTP 502. Added two shared helpers in common/utils:
`coerceStr` (coerces optional free-text props — title/name/path/display — to a
string, empty stays empty) and `assertEnum` (rejects an out-of-range `type` with
a prop-named, choice-listing ConfigurationError). Applied to create-form and
update-form. Also brought create-form's components/settings up to the same
JSON-shape validation (array/object) as update-form.

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

* refactor(form_io): centralize shared props + drop platform-redundant validation (PR review)

Addresses the PR review on PipedreamHQ#21929. Behavior-preserving: verified the effective
(post-merge) prop metadata is byte-identical to before across all 20 actions
(one intentional exception — canonicalizing update-submission's `data` example),
and the full eval suite is unchanged at 22/24 pass^2 with all 20 tools exercised.

- Centralize every prop used in >1 component into form_io.app.mjs propDefinitions
  (title/name/path/type/components/display/tags/settings/description/data/state/
  fields/handler/method/priority); components reference them via propDefinition and
  override only description/optional/options where they differ. Only actionType
  stays inline (single-use in create-form-action).
- Wire the shared enum constants (FORM_TYPES/SUBMISSION_STATES/ACTION_HANDLERS/
  ACTION_METHODS) from common/constants.mjs into the app-file prop `options`,
  replacing duplicated inline literals.
- Remove assertEnum/coerceStr: the MCP layer rejects out-of-enum (invalid_enum_value)
  and non-string (invalid_type) values before run() executes, so re-validating in
  the component duplicated platform-guaranteed checks.
- Pass the expected shape to every parseJson call (object/array) so a valid-JSON
  but wrong-shape value fails with a clear ConfigurationError instead of an opaque
  API error.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: vetrivigneshwaran <vicky@pipedream.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants