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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ All notable user-visible changes to CASCADE are documented here. The format is l

### Fixed

- **`ReadWorkItem` examples now render PM IDs as runnable bare CLI values.** Native-tool prompt guidance and `cascade-tools pm read-work-item --help` now show `--workItemId abc123` instead of JSON-string-literal forms like `--workItemId '"abc123"'`. The CLI also strips one accidental outer quote layer for `ReadWorkItem` IDs only, so a copied bad example no longer sends literal quote characters to the PM provider. See Trello card [M5f9T1D7](https://trello.com/c/M5f9T1D7/673-frictionlow-readworkitem-example-quoting-produced-trello-card-id-with-literal-quotes).

- **`cascade-tools scm create-pr-review` now accepts `--body-file <path>` and `--body-file -`.** This matches the generated CreatePRReview guidance and the existing CreatePR / PostPRComment file-input pattern for long Markdown bodies. See Trello card [7kmo42o6](https://trello.com/c/7kmo42o6/691-friction-tooling-low-createprreview-docs-advertise-body-file-but-cli-rejects-it).

- **PM `cascade-tools` runtime failures now exit non-zero with structured failure envelopes.** PM core commands now throw on fatal provider/API failures, letting `createCLICommand()` emit `{"success":false,"error":{"type":"runtime","message":"..."}}` instead of wrapping prose like `Error posting comment: ...` inside `success:true` data. Intentional non-fatal PM outcomes such as guarded move no-ops and friction retry queueing remain successful command results. See Trello card [lU9mHLJT](https://trello.com/c/lU9mHLJT/686-friction-tooling-low-pm-post-comment-returned-success-envelope-with-embedded-400-error).

- **Native-tool prompt examples for enum, scalar, number, and primitive-array flags now render as runnable CLI syntax instead of JSON string literals.** Agent-facing guidance now shows forms such as `cascade-tools scm create-pr-review --event APPROVE` and repeatable primitive arrays as `--labels bug --labels docs`, while object and array-of-object flags continue to render shell-quoted JSON payloads. See Trello card [l9Sira7y](https://trello.com/c/l9Sira7y/685-friction-tooling-low-create-pr-review-docs-show-quoted-enum-but-cli-requires-raw-enum).

- **`resolve-conflicts` agent no longer silently skips when GitHub's async mergeability computation hasn't resolved by the time the `pull_request` webhook is processed** (spec 020). `PRConflictDetectedTrigger` previously exhausted a 2×2s synchronous retry budget and silently discarded the event when `mergeable === null` — because GitHub never sends a follow-up webhook once mergeability resolves, the `resolve-conflicts` agent never fired. The trigger now returns `TriggerResult.deferredRecheck`, which causes the router to schedule a bare BullMQ delayed re-check job ~45s later via `scheduleCoalescedJob` (deduped per PR). The worker re-dispatches via the trigger registry to get fresh mergeability state. Multiple rapid webhooks for the same PR coalesce to a single re-check job. If mergeability is still `null` after the re-check fires, a Sentry event is captured under tag `mergeability_recheck_exhausted` and a WARN log is emitted — not a silent discard. Observed live on ucho/PR #329 (2026-05-07). See [spec 020](docs/specs/020-github-mergeability-deferred-recheck.md).

### Added
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Flow: `PM/SCM/alerting webhook → Router → Redis → Worker → TriggerRegist

**Capacity-gate invariant.** Every PM router adapter (`src/router/adapters/{linear,trello,jira}.ts`) must wrap `triggerRegistry.dispatch(ctx)` in PM-provider `AsyncLocalStorage` scope via the shared `withPMScopeForDispatch(fullProject, dispatch)` helper at `src/router/adapters/_shared.ts` — in addition to the per-PM-type credential scope (`withLinearCredentials` / `withTrelloCredentials` / `withJiraCredentials`). Without the PM-provider wrapping, the pipeline-capacity gate at `src/triggers/shared/pipeline-capacity-gate.ts` cannot resolve `getPMProvider()`, **fails closed** under the spec-017 fail-closed policy (blocks the run + ERROR + Sentry capture under tag `pipeline_capacity_gate_no_pm_provider`), and `maxInFlightItems` is silently disabled for the PM-source path. Mirror the GitHub adapter's existing correct shape at `src/router/adapters/github.ts:dispatchWithCredentials`. The static guard at `tests/unit/integrations/pm-router-adapter-pm-scope.test.ts` enforces this at CI time — adding a new PM router adapter without the wrapping fails CI with a precise file path.

Integration abstraction lives in `src/integrations/`. For **adding a new PM provider**, see @src/integrations/README.md — PM providers (Trello, JIRA, Linear) use the `PMProviderManifest` registry with a **behavioral conformance harness** (spec 009 — config round-trip, discovery shape, full lifecycle scenario, auth-header provenance, single-entrypoint invariant). Each provider owns its Zod config schema (`src/integrations/pm/<provider>/config-schema.ts`) as the single source of truth — the central `src/config/schema.ts` imports it. PM adapter method signatures use branded `StateId` / `LabelId` / `ContainerId` from `src/pm/ids.ts` to make state-name-vs-ID confusion a compile error at direct-adapter call sites. All runtime surfaces (router, worker, CLI, dashboard) register integrations through a single entrypoint at `src/integrations/entrypoint.ts`. **Spec 010 follow-ups** added generic `pm.discovery.createLabel` / `createCustomField` mutation endpoints + `currentUser` discovery capability + real shared React components for every `StandardStepKind` under `web/src/components/projects/pm-providers/steps/`. **Spec 011** migrated all three production providers (Trello, JIRA, Linear) onto those shared components, added a 7th `StandardStepKind: custom-field-mapping`, widened `container-pick` / `project-scope` / `webhook-url-display` with optional props, and deleted the three legacy `pm-wizard-{trello,jira,linear}-steps.tsx` files. **Spec 012** migrated each provider's webhook UX (programmatic create for Trello/JIRA, signing-secret + instructions for Linear) into per-provider manifest webhook adapters (Fragment compositions around the shared `WebhookUrlDisplayStep`); deleted the legacy `WebhookStep` + `LinearWebhookInfoPanel` + `useWebhookManagement` + `useLinearWebhookInfo`. Every PM wizard step now renders via the manifest path without exception. A new PM provider writes zero edits to shared orchestration (`pm-wizard.tsx`, `pm-wizard-common-steps.tsx`, `pm-wizard-hooks.ts`); provider-specific UI ships either as `kind: 'custom'` steps or as Fragment compositions inside the provider folder's wizard adapters. SCM (GitHub) and alerting (Sentry) still use the legacy `IntegrationModule` pattern via self-registration in `src/github/register.ts` + `src/sentry/register.ts`. Don't improvise; the README covers both patterns.
Integration abstraction lives in `src/integrations/`. For **adding a new PM provider**, see @src/integrations/README.md — PM providers (Trello, JIRA, Linear) use the `PMProviderManifest` registry with a **behavioral conformance harness** (spec 009 — config round-trip, discovery shape, full lifecycle scenario, auth-header provenance, single-entrypoint invariant). Each provider owns its Zod config schema (`src/integrations/pm/<provider>/config-schema.ts`) as the single source of truth — the central `src/config/schema.ts` imports it. PM adapter method signatures use branded `StateId` / `LabelId` / `ContainerId` from `src/pm/ids.ts` to make state-name-vs-ID confusion a compile error at direct-adapter call sites. All runtime surfaces (router, worker, CLI, dashboard) register integrations through a single entrypoint at `src/integrations/entrypoint.ts`. **Spec 010 follow-ups** added generic `pm.discovery.createLabel` / `createCustomField` mutation endpoints + `currentUser` discovery capability + real shared React components for every `StandardStepKind` under `web/src/components/projects/pm-providers/steps/`. **Spec 011** migrated all three production providers (Trello, JIRA, Linear) onto those shared components, added a 7th `StandardStepKind: custom-field-mapping`, widened `container-pick` / `project-scope` / `webhook-url-display` with optional props, and deleted the three legacy `pm-wizard-{trello,jira,linear}-steps.tsx` files. **Spec 012** migrated each provider's webhook UX (programmatic create for Trello/JIRA, signing-secret + instructions for Linear) into per-provider manifest webhook adapters (Fragment compositions around the shared `WebhookUrlDisplayStep`); deleted the legacy `WebhookStep` + `LinearWebhookInfoPanel` + `useWebhookManagement` + `useLinearWebhookInfo`. Every PM wizard step now renders via the manifest path without exception. A new PM provider writes one import in the backend barrel (`src/integrations/pm/index.ts`) and one import in the frontend barrel (`web/src/components/projects/pm-providers/index.ts`); `pm-wizard.tsx`, `pm-wizard-common-steps.tsx`, and `pm-wizard-hooks.ts` receive zero edits. The verification-button readiness path (`areCredentialsReadyFromMetadata` in `pm-wizard-hooks.ts`) and the mutation auth path (`buildProviderAuthArgFromMetadata`) are metadata-driven and require no changes for a new provider. **The shared dashboard state (`pm-wizard-state.ts`) does still require edits**: new providers must add their credential fields to `WizardState` (e.g. `asanaApiKey: string`) and the corresponding action types to `WizardAction`; config-shape hydration belongs on the provider's `ProviderWizardDefinition.buildEditState` — see step 4 of @src/integrations/README.md. Provider-specific hooks, auth metadata, verification display formatting, and UI live inside the provider folder (`kind: 'custom'` steps or Fragment compositions around shared steps). SCM (GitHub) and alerting (Sentry) still use the legacy `IntegrationModule` pattern via self-registration in `src/github/register.ts` + `src/sentry/register.ts`. Don't improvise; the README covers both patterns.

## PR checkout (worker) — gotcha

Expand Down
2 changes: 2 additions & 0 deletions docs/architecture/06-integration-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ const integrationRegistry: IntegrationRegistry; // singleton

The PM barrel (`src/integrations/pm/index.ts`) imports each provider once, then mirrors each manifest's `pmIntegration` into `integrationRegistry`. New PM providers add one provider folder plus one import in that barrel; shared router, worker, dashboard, CLI, and config files are guarded against provider-specific edits by conformance tests.

The dashboard has a matching frontend PM-provider boundary under `web/src/components/projects/pm-providers/<provider>/`. A provider owns its wizard definition, auth metadata, credential persistence metadata, save config serialization, edit-mode hydration, discovery/mutation hooks, webhook UX composition, and provider-specific state slice in that folder. The shared wizard files (`pm-wizard.tsx`, `pm-wizard-hooks.ts`, and `pm-wizard-common-steps.tsx`) only render registered provider definitions and run metadata-driven verification/save helpers, so adding a provider does not require editing them. New providers add one frontend barrel import in `web/src/components/projects/pm-providers/index.ts`; `pm-wizard-state.ts` is the remaining explicit shared-dashboard exception while it composes provider state slices into the aggregate `WizardState` and reducer.

`src/pm/integration.ts` — extends `IntegrationModule` with PM-specific methods:

- `createProvider(project)` — create a `PMProvider` instance for CRUD operations
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture/07-gadgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ The `cascade-tools` binary uses a separate oclif config (`bin/cascade-tools.js`)

New domain commands should not add branches in these helpers. They declare behavior through their `ToolDefinition` metadata (`cliAliases`, examples, file input alternatives, auto-resolution), and the shared generators consume it.

Core functions passed to `createCLICommand()` own domain work only. On fatal runtime/API/provider failures they throw, and the shared factory converts that exception into the structured `{"success":false,"error":{"type":"runtime","message":"..."}}` stdout envelope plus exit code 1. A returned value is always serialized as successful `data`, so gadgets must not return sentinel error strings such as `Error reading work item: ...` for fatal failures. Non-fatal command states that are part of the contract, such as guarded PM move no-ops or friction retry queueing, remain successful returns.

## Session State

`src/gadgets/sessionState.ts`
Expand Down
60 changes: 38 additions & 22 deletions src/backends/shared/nativeToolPrompts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { formatJsonExample, formatShellScalar } from '../../gadgets/shared/cli/shellValues.js';
import type { ContextInjection, ToolManifest } from '../types.js';
import { buildInlineContextSection, offloadLargeContext } from './contextFiles.js';

Expand All @@ -14,6 +15,38 @@ You are operating in a native-tool environment, not a gadget/function-call envir
- If you catch yourself composing a pseudo tool call in plain text, stop and use the real tool instead.
- Trello, JIRA, and GitHub attachment URLs require backend authentication. NEVER curl, wget, or HTTP-fetch them — they return an authorization error. Work item images are pre-fetched and available either as images in your conversation context or as files under \`.cascade/context/images/\` — use whichever is present; never fetch the original URLs.`;

type PromptParamSchema = {
type: string;
required?: boolean;
default?: unknown;
description?: string;
options?: string[];
items?: string;
aliases?: readonly string[];
example?: unknown;
};

function formatExampleInvocation(key: string, schema: PromptParamSchema): string | undefined {
if (schema.example === undefined) return undefined;

if (schema.type === 'boolean') {
return schema.example ? `--${key}` : `--no-${key}`;
}

if (schema.type === 'object' || (schema.type === 'array' && schema.items === 'object')) {
const json = formatJsonExample(schema.example);
return json ? `--${key} ${json}` : undefined;
}

if (schema.type === 'array') {
const examples = Array.isArray(schema.example) ? schema.example : [schema.example];
if (examples.length === 0) return undefined;
return examples.map((value) => `--${key} ${formatShellScalar(value)}`).join(' ');
}

return `--${key} ${formatShellScalar(schema.example)}`;
}

/**
* Format a single CLI parameter for tool guidance documentation.
*
Expand All @@ -30,18 +63,7 @@ You are operating in a native-tool environment, not a gadget/function-call envir
* - `type: 'object'` → renders as a single `'<json>'` blob.
*/
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: parameter-type taxonomy
function formatParam(
key: string,
schema: {
type: string;
required?: boolean;
default?: unknown;
description?: string;
items?: string;
aliases?: readonly string[];
example?: unknown;
},
): string {
function formatParam(key: string, schema: PromptParamSchema): string {
const aliasSuffix = (schema.aliases ?? []).map((a) => `|--${a}`).join('');
const flagHead = `--${key}${aliasSuffix}`;

Expand Down Expand Up @@ -75,15 +97,9 @@ function formatParam(
// per-flag example said exactly that. The synopsis renders the toggle form;
// the example reinforces it concretely.
if (schema.example !== undefined) {
if (schema.type === 'boolean') {
result += schema.example ? `\n # example: --${key}` : `\n # example: --no-${key}`;
} else {
try {
result += `\n # example: --${key} '${JSON.stringify(schema.example)}'`;
} catch {
// JSON.stringify throws on cyclic refs — never in our tool definitions,
// but be defensive so a malformed example never crashes prompt building.
}
const exampleInvocation = formatExampleInvocation(key, schema);
if (exampleInvocation) {
result += `\n # example: ${exampleInvocation}`;
}
}

Expand Down Expand Up @@ -112,7 +128,7 @@ export function buildToolGuidance(tools: ToolManifest[]): string {
guidance += `\`\`\`bash\n${tool.cliCommand}`;

for (const [key, schema] of Object.entries(tool.parameters)) {
guidance += formatParam(key, schema as { type: string; required?: boolean });
guidance += formatParam(key, schema as PromptParamSchema);
}

guidance += '\n```\n\n';
Expand Down
16 changes: 15 additions & 1 deletion src/cli/pm/read-work-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ import { readWorkItem } from '../../gadgets/pm/core/readWorkItem.js';
import { readWorkItemDef } from '../../gadgets/pm/definitions.js';
import { createCLICommand } from '../../gadgets/shared/cliCommandFactory.js';

function normalizeWorkItemId(workItemId: string): string {
if (workItemId.length >= 2) {
const first = workItemId[0];
const last = workItemId.at(-1);
if ((first === '"' || first === "'") && first === last) {
return workItemId.slice(1, -1);
}
}
return workItemId;
}

export default createCLICommand(readWorkItemDef, async (params) => {
return readWorkItem(params.workItemId as string, params.includeComments as boolean | undefined);
return readWorkItem(
normalizeWorkItemId(params.workItemId as string),
params.includeComments as boolean | undefined,
);
});
Loading
Loading