Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
87bf297
feat(pm,github): mutation result contracts and PM timestamp plumbing …
aaight Jun 1, 2026
da88002
feat(scm): structured outputs for PR comment/reply/update/review muta…
aaight Jun 1, 2026
fb3cd36
feat(pm): structured outputs for checklist mutations (MNG-1424) (#1388)
aaight Jun 1, 2026
5a60bb4
feat(pm): structured outputs for work-item and comment mutations (MNG…
aaight Jun 1, 2026
d88a9da
feat(gadgets): document mutation output shapes via metadata (MNG-1427…
aaight Jun 1, 2026
0e9b30e
test(gadgets): pin structured-output regression coverage (MNG-1428) (…
aaight Jun 2, 2026
1bc0870
feat(gadgets): shared CLI fuzzy-matching helper (MNG-1440) (#1393)
aaight Jun 2, 2026
0988dbf
feat(cli): unknown-command suggestions for cascade-tools (MNG-1441) (…
aaight Jun 2, 2026
945107c
feat(cli): unknown-command hook for cascade-tools (MNG-1442) (#1395)
aaight Jun 2, 2026
fa916e6
chore(deps): bump hono from 4.12.18 to 4.12.23 (#1396)
dependabot[bot] Jun 12, 2026
1190a07
chore(deps): bump @grpc/grpc-js from 1.14.3 to 1.14.4 (#1398)
dependabot[bot] Jun 12, 2026
bbb4088
chore(deps): bump ws from 8.19.0 to 8.21.0 (#1385)
dependabot[bot] Jun 12, 2026
04e8ba5
chore(deps): bump qs from 6.15.0 to 6.15.2 (#1384)
dependabot[bot] Jun 12, 2026
c77be1e
chore(deps): bump shell-quote and concurrently (#1397)
dependabot[bot] Jun 12, 2026
935d51d
fix(splitting): remove PR-creation references from prompt template (#…
aaight Jun 12, 2026
7462d2d
fix(trello): refresh API key link + stop stuck 'Waiting' spinner (#1399)
jkwiecien-solvd Jun 12, 2026
faa92dd
fix(web): show real PM webhook callback URL in Trello/JIRA wizard (#1…
jkwiecien-solvd Jun 12, 2026
f089031
feat(backends): add Claude Opus 4.8 model support (#1402)
jkwiecien-solvd Jun 16, 2026
58f5754
fix(router): self-heal on missing base worker image + stub failed run…
zbigniewsobiecki Jun 19, 2026
8a38540
chore(deps): bump @opentelemetry/core and @sentry/node (#1409)
dependabot[bot] Jun 19, 2026
3d7abe7
fix(db): support DATABASE_SSL=no-verify for self-signed managed Postg…
zbigniewsobiecki Jun 19, 2026
3b5d0da
chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#1411)
dependabot[bot] Jun 19, 2026
823ea0a
chore(deps): bump undici from 7.24.1 to 7.28.0 (#1412)
dependabot[bot] Jun 19, 2026
22899dd
feat: add user profile page and password change functionality (#1407)
jkwiecien-solvd Jun 19, 2026
f7c2c0b
chore(deps-dev): bump @babel/core from 7.29.0 to 7.29.7 in /web (#1413)
dependabot[bot] Jun 19, 2026
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
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@ REDIS_URL=redis://localhost:6379
PORT=3000
LOG_LEVEL=info

# Disable SSL for local PostgreSQL (set to false for local dev without SSL)
# PostgreSQL TLS mode:
# false → no TLS (local dev without SSL)
# no-verify → TLS without certificate verification — for managed Postgres that
# requires TLS but presents a self-signed/private-CA cert (e.g. Supabase's
# connection pooler). DATABASE_CA_CERT does NOT help here because spawned
# worker containers receive DATABASE_* env but no mounted cert file.
# (unset) → TLS with verification; optionally pin a CA via DATABASE_CA_CERT.
# DATABASE_SSL=false
# DATABASE_CA_CERT=/path/to/ca.pem

# --- Optional: Security ---

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable user-visible changes to CASCADE are documented here. The format is l

## Unreleased

### Added

- **`cascade-tools` now suggests the closest command when an agent typos a topic or subcommand** ([MNG-1442](https://linear.app/issue/MNG-1442)). `bin/cascade-tools.js` registers an oclif `command_not_found` hook that turns command typos into the same structured spec-014 envelope every other CLI failure emits: JSON on stdout, a one-line prose summary on stderr, and a runnable `did you mean` hint when within the shared Levenshtein budget (MNG-1440). Unknown top-level topics (`cascade-tools sm get-pr-diff`) surface a topic enumeration in `expected` and a hint that preserves the user's trailing segments (`did you mean 'cascade-tools scm get-pr-diff'?`). Known-topic / unknown-subcommand typos (`cascade-tools pm reaad-work-item`) surface the topic's subcommand enumeration and a corrected hint (`did you mean 'cascade-tools pm read-work-item'?`). Far-away typos drop `hint` but still surface `expected` so the agent has a concrete recovery path. Exit code is **`2`** for `unknown-command` — preserved from oclif's historical `command_not_found` default and distinct from every other envelope's exit code `1`; existing exit-code consumers see no change. The hook lives at `src/cli/_shared/command-not-found-hook.ts`, intentionally inside `_shared/` so oclif's command-discovery glob in `bin/cascade-tools.js` excludes it. It is wired through `pjson.oclif.hooks` so oclif loads it dynamically only when needed — no static import is added, which preserves the existing friendly `dist/cli/bootstrap.js` missing path in the entrypoint. The pure suggestion logic lives in `src/cli/_shared/commandSuggestions.ts` (MNG-1441) and is unit-tested directly without booting oclif; candidates come strictly from the loaded oclif config (`config.commandIDs` plus non-hidden `pjson.oclif.topics`), so the `cascade-tools` binary never suggests dashboard topics that its discovery glob excludes. Existing `unknown-flag` handling from `createCLICommand()` is untouched. Closes [MNG-1442](https://linear.app/mongrel/issue/MNG-1442).

### Fixed

- **`cascade-tools` multiline text and large diff I/O are now hardened against shell-quoting footguns and stdout truncation** ([MNG-1059](https://linear.app/issue/MNG-1059)). The shared CLI factory at `src/gadgets/shared/cli/params.ts` now rejects invocations that pass `--*-file -` for two or more file-input flags (e.g. `--body-file - --comments-file -`) before any `readFileSync(0, ...)` call — stdin (fd 0) can only be drained once per process, and the previous behavior silently truncated one of the two agent payloads. The rejection emits a structured `flag-parse` error envelope (`error.flag: "body-file,comments-file"`, `hint: "Pass at most one --*-file -; for the others, write the payload to a temp file and pass --<flag>-file <path>."`) so agents can self-correct on the next attempt. Direct file paths remain pairwise-compatible — `--body-file - --comments-file /tmp/comments.json` and `--body-file /tmp/body.md --comments-file -` both work as before. The native-tool system prompt now renders a "cascade-tools shell-safety rules" section that documents the one-stdin-consumer invariant and provides safe heredoc / temp-file patterns for one and two payloads. The prompt renderer also suppresses inline `--body '...'` / `--text '...'` examples whose content contains backticks, code fences, `$(...)`, or newlines when a file-input companion is declared, redirecting the agent at the safer `--*-file <path>` form instead. File-input flag descriptions for `--body-file`, `--text-file`, `--description-file`, `--details-file`, and `--comments-file` explicitly call out markdown / multiline / backticks. Closes [MNG-908](https://linear.app/mongrel/issue/MNG-908), [MNG-910](https://linear.app/mongrel/issue/MNG-910), [MNG-917](https://linear.app/mongrel/issue/MNG-917), [MNG-1046](https://linear.app/mongrel/issue/MNG-1046).
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ Required:

Optional:

- `DATABASE_SSL=false` to disable SSL locally; `DATABASE_CA_CERT` for managed DBs with a private CA.
- `DATABASE_SSL` — `false` disables SSL (local dev); `no-verify` keeps TLS but skips certificate verification — required for managed Postgres that requires TLS yet presents a self-signed/private-CA cert (e.g. Supabase's connection pooler), where `DATABASE_CA_CERT` can't help because spawned worker containers get `DATABASE_*` env but no mounted cert file; unset → TLS with verification. `DATABASE_CA_CERT` pins a CA for managed DBs with a private CA (verification mode only).
- `CREDENTIAL_MASTER_KEY` — 64-char hex (AES-256 key) to encrypt project credentials at rest. Without it, credentials are stored as plaintext; both modes coexist.
- `GITHUB_WEBHOOK_SECRET` — opt-in HMAC verification; store as the `webhook_secret` role on the GitHub SCM integration.
- `SENTRY_DSN`, `SENTRY_ENVIRONMENT`, `SENTRY_RELEASE`, `SENTRY_TRACES_SAMPLE_RATE` — observability.
Expand Down
17 changes: 17 additions & 0 deletions bin/cascade-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ pjson.oclif = {
globPatterns: ['**/*.js', '!**/dashboard/**', '!**/_shared/**', '!base.js', '!bootstrap.js'],
},
topicSeparator: ' ',
// `command_not_found` hook turns command typos into the structured
// spec-014 envelope (JSON on stdout, prose on stderr, runnable
// `did you mean` hint when within budget) instead of oclif's bare
// `command <id> not found` message. Exit code stays 2 (oclif's
// historical default for command_not_found) via an explicit exit
// delegate inside the hook — see
// `src/cli/_shared/command-not-found-hook.ts` for the full rationale.
//
// The hook is wired via oclif's `pjson.oclif.hooks` so it is loaded
// lazily by `loadWithData` when the hook actually fires — *not*
// statically required at entrypoint time. This preserves the friendly
// `dist/cli/bootstrap.js` missing path above: if the build is absent,
// the bootstrap import throws ERR_MODULE_NOT_FOUND first and exits 1
// with the explainer, before this hook is ever resolved.
hooks: {
command_not_found: './dist/cli/_shared/command-not-found-hook.js',
},
// Explicit topic summaries. Without this block oclif borrows each topic's
// description from its FIRST command (see node_modules/@oclif/core
// /lib/config/config.js — the line `this._topics.set(name, { description:
Expand Down
43 changes: 43 additions & 0 deletions docs/architecture/07-gadgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ The `cascade-tools` binary uses a separate oclif config (`bin/cascade-tools.js`)
|--------|------|
| `commandNames.ts` | Command namespace/name derivation shared by the CLI factory and manifest generator |
| `examples.ts` | Tool example lookup, shell quoting, oclif example rendering, and JSON expected-shape hints |
| `suggestions.ts` | Shared Levenshtein scorer for flag and command typo suggestions (MNG-1440) |
| `flags.ts` | oclif flag construction and flag metadata collection |
| `booleanArgv.ts` | Boolean value-form normalization before oclif parsing |
| `parseErrors.ts` | oclif parse-error classification and unknown-flag suggestions |
Expand All @@ -148,6 +149,48 @@ New domain commands should not add branches in these helpers. They declare behav

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.

### Unknown-command typo suggestions (MNG-1442)

`bin/cascade-tools.js` registers an oclif `command_not_found` hook so command typos emit the same structured envelope every other CLI failure does (spec 014): JSON on stdout, prose on stderr, runnable `did you mean` hint when within the shared Levenshtein budget. Two cases the hook covers:

- **Unknown top-level topic** (`cascade-tools sm get-pr-diff`) — `expected` lists topics, `hint` preserves trailing segments (`did you mean 'cascade-tools scm get-pr-diff'?`).
- **Known topic, unknown subcommand** (`cascade-tools pm reaad-work-item`) — `expected` lists the topic's subcommands, `hint` runs the corrected form (`did you mean 'cascade-tools pm read-work-item'?`).

Far-away typos drop `hint` but still surface `expected` so the agent has a concrete recovery enumeration. Exit code is **`2`** for `unknown-command` — oclif's historical `command_not_found` default — distinct from every other envelope's exit code `1`.

The hook lives at `src/cli/_shared/command-not-found-hook.ts`, intentionally inside `_shared/` because `bin/cascade-tools.js`'s oclif command-discovery glob excludes `**/_shared/**`. The entrypoint wires it via `pjson.oclif.hooks.command_not_found` so oclif loads it dynamically only when needed — no static import is added, which preserves the existing friendly `dist/cli/bootstrap.js` missing path. The pure suggestion logic lives in `src/cli/_shared/commandSuggestions.ts` (MNG-1441) and is unit-tested directly without booting oclif; the hook is a thin wrapper that forwards `{config, id, argv}` into the helper and routes the envelope through `emitCliError` with an explicit exit-code-2 delegate. Candidates come strictly from the loaded oclif config (`config.commandIDs` plus non-hidden `pjson.oclif.topics`), so the `cascade-tools` binary never suggests dashboard topics that its discovery glob excludes.

### Mutation result contract (MNG-1422 → MNG-1428)

Every PM mutation core and the SCM PR comment/reply/update/review mutation cores covered by MNG-1428 return structured objects, never prose. The CLI factory serialises those objects verbatim into `{"success":true,"data":{...}}`, so consumers (downstream agents, sidecars, review/respond workflows) can read structured keys directly.

These targeted mutations surface these contract fields on `success.data`:

| Field | Meaning |
|---|---|
| `status` | The MUTATION OUTCOME — `"created"`/`"updated"`/`"moved"`/`"noop"`/`"aborted"`/`"deleted"` (PM) or `"ok"`/`"no-op"`/`"aborted"` (SCM). Branch on this, not on prose. |
| `updatedAt` | ISO 8601 timestamp string. It is always present and parseable; the source varies by mutation and fallback path. |

Identity and URL fields are mutation-specific. Work-item and comment mutations expose `id` plus their canonical resource URL (`url` or, for PM comments, `workItemUrl`). `AddChecklist` exposes `checklistId` and `workItemUrl`, plus `itemIds` / `itemCount`; `PMUpdateChecklistItem` and `PMDeleteChecklistItem` expose `checkItemId` and `workItemUrl`. Targeted SCM PR comment/reply/update/review mutations additionally surface `id`, `url`, `repoFullName`, and `prNumber` (the latter widens to `number | null` for `UpdatePRComment` when GitHub returns an issue-only comment URL). `CreatePRReview` extends with `reviewUrl`, `event`, `submittedAt`, and `inlineCommentCount`. `CreatePR` is also a structured SCM mutation, but it is outside MNG-1428's shared `status` / `updatedAt` / `id` / `url` contract and keeps its existing shape: `prNumber`, `prUrl`, `repoFullName`, and `alreadyExisted` plus optional commit/push details. The full per-mutation shapes live on the matching `ToolDefinition.outputShape` blocks under `src/gadgets/{pm,github}/definitions.ts`.

**`status` vs `workflowStatus` naming.** `status` is reserved for the mutation outcome alone. The PM provider's workflow state — Linear's "In Progress", a Trello list name, a JIRA status — lives on its own keys: `workflowStatus` (human-readable) and `workflowStatusId` (native ID). `MoveWorkItem` also exposes `previousStatus` / `previousStatusId` for the work item's pre-move workflow state on the guarded path. Mixing the two surfaces once cost ~2½ minutes of agent time (prod run `5d993b04`); the dual-key naming is now load-bearing.

**Fatal failures throw.** Cores propagate runtime/API/provider errors as exceptions; the CLI factory emits the spec-014 runtime envelope (`{"success":false,"error":{"type":"runtime","message":"..."}}`). Do NOT return sentinel strings like `"Error creating work item: ..."` — the CLI cannot distinguish a string return from a successful `data` payload, so the envelope would say `success: true` and the agent would silently mis-act.

**Timestamp fallback semantics.** The stable contract is that `updatedAt` is always present and parseable. `okResult` still rejects empty timestamps, so call sites using the shared success helper must provide one, but some successful PM writes synthesise timestamps today: `PostComment` uses `currentTimestamp()` for `created` / `updated`, and `MoveWorkItem` can fall back through `pickTimestamp(undefined)` for `moved`. `noOpResult` and `abortedResult` synthesise via `currentTimestamp()` because no provider write happened — the synthetic "now" reflects when the gadget evaluated the guard. Read-back failures after a successful checklist mutation fall back to a synthesised URL + timestamp in `readWorkItemContext` rather than masking the mutation success and risking an idempotency retry storm (Trello native-checklist retries duplicate rows).

The regression coverage lives in `tests/unit/cli/pm/pm-commands.test.ts`, `tests/unit/cli/scm/scm-commands.test.ts`, `tests/unit/gadgets/pm/definitions.test.ts`, and `tests/unit/gadgets/github/definitions.test.ts`. Run the focused suite with:

```bash
npx vitest run --project unit-core \
tests/unit/cli/pm/pm-commands.test.ts \
tests/unit/cli/scm/scm-commands.test.ts \
tests/unit/gadgets/pm/definitions.test.ts \
tests/unit/gadgets/github/definitions.test.ts
```

The full pre-PR gate remains `npm run lint && npm run typecheck && npm test`.

### Shell-safety contract (MNG-1059)

cascade-tools commands that accept text bodies, descriptions, or markdown payloads declare a `--*-file <path>` companion via `cli.fileInputAlternatives` (`--body-file`, `--text-file`, `--description-file`, `--details-file`, `--comments-file`). Agents are instructed to prefer the file form for any content containing backticks, code fences, `$(...)`, or newlines — shells expand those tokens even inside single quotes when the command is layered through `bash -c`, and newlines break argv parsing.
Expand Down
8 changes: 7 additions & 1 deletion drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { defineConfig } from 'drizzle-kit';
// drizzle-kit connects via the `url` below and IGNORES a `dbCredentials.ssl` object
// when a `url` is set, so the SSL intent must be encoded in the connection string as
// `sslmode`. `applyDbSslModeToUrl` derives it from DATABASE_SSL (shared with the runtime
// client's resolver), letting `DATABASE_SSL=no-verify` work against managed Postgres with
// self-signed certs (e.g. Supabase's pooler), which strict verification would reject.
import { applyDbSslModeToUrl } from './src/db/ssl-config';

export default defineConfig({
schema: [
Expand All @@ -14,6 +20,6 @@ export default defineConfig({
out: './src/db/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL ?? '',
url: applyDbSslModeToUrl(process.env.DATABASE_URL ?? ''),
},
});
Loading
Loading