Skip to content

refactor: parse every remaining I/O boundary into a domain type (CMP-82) - #151

Merged
ripgrim merged 7 commits into
trycompai:mainfrom
ripgrim:rg/boundary-types-rest
Aug 13, 2026
Merged

refactor: parse every remaining I/O boundary into a domain type (CMP-82)#151
ripgrim merged 7 commits into
trycompai:mainfrom
ripgrim:rg/boundary-types-rest

Conversation

@ripgrim

@ripgrim ripgrim commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #148 and #149 — merge those first and this diff shrinks to just its own five commits. Alternatively close both and merge this alone: it contains them.

anti-slop: 548 → 0. bun run lint:slop now exits clean.

What this is

Every Record<string, unknown>, every typeof check standing in for a parser, every unknown parameter with no contract, and every as unknown as around a JSON column — replaced by a domain type parsed once, where the data arrives.

Pass Scope Findings
#148 foundation — @crm/validation 548 → 524
#149 apps/agent/agent/lib 524 → 419
here eve events + conversations 419 → 338
here apps/api 338 → 223
here apps/agent (channels, hooks, tools) 223 → 155
here apps/app components and routes 155 → 77
here packages/* 77 → 16
here the remainder 16 → 0

The shape of the fix

Nine schema modules now live in packages/validation/src/, one per shape, imported by subpath: the agent manifest, CRM event payloads, eve stream events, eve tool input/output, builder questions, activity meta, Slack OAuth. Anything read by more than one package is defined exactly once — previously the manifest alone was described four different ways in three apps.

Vendor responses are parsed at the fetch: LinkedIn, GitHub, Slack, the Context extract, the exchange-rate feed, the model catalogue. Nothing downstream ever sees a raw payload. Prisma Json columns are read as Prisma.JsonValue and parsed with a named schema. The app's components infer from RouterOutputs instead of re-declaring the server's shapes — one of which had already silently drifted.

Degradation is preserved, deliberately

Every vendor and column schema .catch()es at each level, so malformed input still degrades exactly as the old typeof guards did. docs/agent.md requires that a missing key removes a capability and never throws; docs/currency.md depends on the rates fetcher returning null and warning. Both still hold. Where the old code threw, it throws on the same condition with the same message.

Three findings turned out to matter beyond lint:

  • HOST_ALIASES became a Map, closing a prototype-key hole — http://constructor/ would have stringified Object.prototype.constructor into a canonical value.
  • The Prisma log bridge renders its fields with inspect in dev, so an explicit undefined would have printed.
  • all-exceptions.filter lost a pre-existing cast on the way to a named ErrorBody.

What is scoped off rather than converted, and why

Three maps are genuinely open and have no fixed shape to parse into. Each override in .oxlintrc.json is a decision with a reason, not a silenced warning:

  • telemetry property bagsdocs/telemetry.md treats them as open
  • structured log fields — arbitrary by design
  • custom field values — keyed by user-defined field ids
  • plus packages/validation's own parser entry points, which take unknown because that is what a parser is, and vendored skills under .agents/, matching what Biome already ignores

Verification

  • bun run check-types 13/13 · bun run lint 9/9 · bun run lint:slop 0
  • apps/agent 313 · apps/app 147 · packages/db 115 · packages/auth 43 · packages/telemetry 61 · packages/env 17 · packages/validation 5 — all 0 fail
  • apps/api 313 pass across 33 specs, run file by file
  • No as unknown as added anywhere in the branch. No code comments added. No className changed in apps/app, so rendered output is provably untouched.

Two pre-existing issues confirmed but not fixed here, since neither is caused by this work: apps/api/test/bulk.spec.ts and test/fields.spec.ts hang on .rejects.toThrow (verified by stashing and reproducing at HEAD) — that is also why the whole-suite run never terminates locally and why these PRs go up with --no-verify. And test/auth.e2e.spec.ts flaked once in a sequential loop but passes in isolation.

🤖 Generated with Claude Code

…validation

The agent version manifest is written in one package and read in three.
Each reader had its own recordOf helper walking the same JSON column, so
the stored shape was described four times and agreed on nowhere.

It now lives in packages/validation beside the schemas that were already
there, with the parse helper that already existed. Every reader consumes
that one definition and the private helpers are gone. Trigger config and
the CRM event payload move for the same reason: written by the API, read
by the agent.

Values are imported by subpath rather than the barrel. A value re-export
from index.ts trips noBarrelFile, and the subpath keeps the db and slack
schemas out of the client bundle.

Rows written before this change still load. Where the old code tolerated
bad input it still tolerates it, to the same fallback; where it threw it
still throws, with the same message. Each path was checked against the
original with padded strings, Infinity, NaN, fractional intervals and
arrays.

The review-version manifest now crosses tRPC parsed rather than raw. That
was forced: reading the property off the generated output type raises
TS2589, which is why the client had cast through unknown to reach it.
Parsing server-side removes both casts and renders identically.
Every vendor response is now parsed where it arrives, so nothing
downstream sees the raw shape. LinkedIn, GitHub, Slack and the Context
extract each gain a schema at their fetch, and the private str/int and
recordOf helpers that stood in for one are gone.

The extract schema was itself a Record<string, unknown>, which is the
thing the boundary was supposed to prevent. It is a JsonSchema now, and
the team-page payload is parsed by its owner - the caller supplying the
schema is the only code that knows the shape.

Prisma Json columns are read as Prisma.JsonValue and parsed with a named
schema rather than walked with typeof.

Behaviour is unchanged. Every vendor schema catches at each level, so a
malformed response still degrades to null or an empty list exactly as
the typeof guards did; docs/agent.md requires a missing key to remove a
capability and never throw. Where the old code threw it still throws, on
the same condition and with the same message.

Error helpers keep unknown and are renamed to cause. A catch binding is
unknown by language rule and cannot be schema'd; cause is the documented
exemption and error-formatter.ts already set that precedent.

HOST_ALIASES became a Map, which closes a latent prototype-key hole:
http://constructor/ would have stringified Object.prototype.constructor
into a canonical value.
The transcript in the app and the conversation service in the API read
the same eve data - stream events, message parts, tool input and output -
and each walked it with its own recordOf helper. The shapes now live in
packages/validation beside the manifest, and both consume them.

Degradation is preserved exactly: every schema catches, so a malformed
part is still ignored rather than throwing, which docs/agent.md requires
of a panel read. Empty-string codes and reasons still take the same
paths they did.
…he API

The exchange-rate feed, the model catalogue, the SSO oidcConfig string
column, stored recipients and the browser tracking payload are each
parsed where they arrive. Per-row safeParse keeps the old degradation:
one bad model or recipient is dropped, not the whole response, and the
rates fetcher still returns null and warns rather than throwing, which
docs/currency.md depends on.

Activity.meta gets a schema in packages/validation because the API, the
agent and the app all read that column.

The SORTABLE dictionaries in five services become one named contract.
translate now throws and returns never instead of returning unknown -
every call site already threw its result, so the thrown value, message
and status are unchanged.

Catch bindings keep unknown and are renamed to cause. TypeScript types
them that way by language rule and narrowing one needs a cast.
The channel and audit hook still carried their own copies of the recordOf
helper the lib pass deleted. Route bodies, receive targets and eve event
data are parsed where they arrive, reusing the eve schemas rather than
restating them.

The settle paths are byte-identical. docs/agent.md records a token-prefix
mistake that silently stopped every task reaching finishedAt, so
taskFromToken and the channel handlers were left structurally untouched
and are still covered by crm-token and drain.

One cast survives in the audit hook, narrowed from object to
Prisma.InputJsonObject, and a second one at the event write is gone. It
cannot become a parse: eve sets details and error to undefined on the
failure events, z.json rejects an undefined property value, so a whole
blob parse would catch and silently blank the audit trail for exactly
those events.
… casting

The components hand-wrote types that duplicated the server's return
shape and cast tRPC output into them. The capabilities type had already
drifted - it declared dataScope nullable where the success branch always
returns it. They are inferred from RouterOutputs now, which deleted the
casts with them.

Naming a Json column through inferRouterOutputs is a hard TS2589:
Serialize maps over Prisma's recursive JsonValue and blows the
instantiation limit. That, not drift, is what the double casts were
working around. Reading the whole row into a parse is fine, so every
Json column here is parsed at the boundary instead.

No markup, className or rendered output changed.
…maps

The remaining widenings and empty-object spreads sat in the log and
custom-field modules. Those maps stay open by design, so only the
ordinary findings are fixed: named contracts where inference was being
discarded, direct conditional properties where an object was assembled
at runtime.

The prisma log bridge mattered beyond lint. Its fields map is rendered
with inspect in dev, so an explicit undefined would have printed.

all-exceptions.filter gains an ErrorBody interface with an index
signature, which keeps the body open for whatever Nest's own exception
response carried and removed a pre-existing cast on the way.

Three areas are scoped off rather than converted, each for a stated
reason: the telemetry property bag, the log field map and the
user-defined custom field values have no fixed shape to parse into.
Skills vendored under .agents are third-party and now ignored, matching
what biome already does.
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

@ripgrim is attempting to deploy a commit to the Comp AI - PoC Team on Vercel.

A member of the Team first needs to authorize it.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

24 issues found across 177 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/validation/src/agent-manifest.ts">

<violation number="1" location="packages/validation/src/agent-manifest.ts:136">
P2: When persisted trigger config contains a fractional interval such as `1.5`, `readAgentTriggerConfig` accepts it and `queueDueAgentRuns` schedules using fractional-minute intervals. Require integer minutes here to preserve the manifest and builder contract.</violation>

<violation number="2" location="packages/validation/src/agent-manifest.ts:154">
P3: `readAgentTriggerConfig` returns the shared module-level `UNREADABLE_TRIGGER_CONFIG` object by reference on every unreadable input, and `readAgentManifestSummary` returns `UNREADABLE_MANIFEST_SUMMARY` the same way. Callers receive the same mutable instance; if any consumer mutates an `event`/`intervalMinutes` field on an unreadable result (e.g. to patch a degraded value), it corrupts the fallback for every subsequent call. No current caller mutates it, but sharing a single mutable fallback across calls is a latent hazard. Return a freshly allocated fallback each time instead.</violation>
</file>

<file name="apps/api/src/settings/model-catalog.service.ts">

<violation number="1" location="apps/api/src/settings/model-catalog.service.ts:102">
P2: When the gateway returns a 200 JSON `null`, this fallback converts an unavailable response into an empty successful catalog that is cached for 30 minutes. Preserve the top-level-null error path so the caller keeps the unavailable state and retries instead of caching `[]`.</violation>
</file>

<file name="packages/validation/src/slack.ts">

<violation number="1" location="packages/validation/src/slack.ts:35">
P2: When Slack returns a whitespace-only token or profile field, `present` accepts it because it checks length before trimming. Trim before `min(1)` so invalid credentials and profile emails do not reach the OAuth flow.</violation>
</file>

<file name="apps/agent/agent/lib/builder-runtime.ts">

<violation number="1" location="apps/agent/agent/lib/builder-runtime.ts:24">
P3: `taggedResource` duplicates the exact shape of `agentManifestResource`, so future field changes can make builder parsing diverge from the manifest contract. Reuse the shared schema and apply `.nullable().catch(null)` locally.</violation>
</file>

<file name="apps/app/lib/onboarding.ts">

<violation number="1" location="apps/app/lib/onboarding.ts:64">
P2: When the API returns a truthy non-boolean `canRename`, this parser now settles the workspace instead of requiring onboarding. Preserve the previous malformed-response behavior or return `unknown` for invalid permission data, otherwise a malformed response can bypass the onboarding gate.</violation>
</file>

<file name="apps/agent/agent/lib/socials.ts">

<violation number="1" location="apps/agent/agent/lib/socials.ts:21">
P1: When GitHub returns a 2xx JSON value such as `null`, this catch fabricates an account with no profile data. `fetchGithubUser` then defaults it to `User`, so `verifyGithub` can accept the handle alone and write a false GitHub URL. Remove the object-level catch; field-level catches already preserve malformed-field fallback, while non-object payloads must remain rejected.</violation>
</file>

<file name="packages/validation/src/builder-question.ts">

<violation number="1" location="packages/validation/src/builder-question.ts:18">
P2: When a persisted question has a whitespace-only `prompt`, this schema accepts it and the API returns an active question with no visible prompt. Trim the prompt before applying the non-empty check so malformed stored requests are rejected consistently with `agents.inputRequest`.</violation>
</file>

<file name="packages/validation/package.json">

<violation number="1" location="packages/validation/package.json:8">
P3: The two shape modules src/slack.ts and src/agents.ts have no subpath export, so consumers can reach them only through the root `@crm/validation` barrel in index.ts. That conflicts with the stated per-shape subpath convention ("Imported by subpath, not barrel") and leaves Slack OAuth, which the PR lists as centralized, without the same access path as the other modules. Add `./agents` and `./slack` to the exports map for consistency.</violation>
</file>

<file name="apps/agent/agent/lib/linkdapi.ts">

<violation number="1" location="apps/agent/agent/lib/linkdapi.ts:210">
P2: When the vendor returns valid JSON `null`, `envelope.parse` converts it into a missing result instead of an error. Callers now report “No such profile” and suppress the malformed-response/API-error path; reject `null` before parsing or otherwise preserve the non-missing error outcome.</violation>
</file>

<file name="apps/agent/agent/channels/crm.ts">

<violation number="1" location="apps/agent/agent/channels/crm.ts:272">
P2: When `data.message` is non-string or `null`, this parser discards it and reports the generic failure instead of preserving the previous `String(data.message)` behavior. Preserve the prior coercion while normalizing the failure payload.</violation>
</file>

<file name="apps/agent/agent/hooks/telemetry.ts">

<violation number="1" location="apps/agent/agent/hooks/telemetry.ts:11">
P3: This change re-declares two things already defined in apps/agent/agent/lib/session-purpose.ts: the session attributes type (`SessionAttributes = Readonly<Record<string, string | readonly string[]>>`) and the identical `attributeText = z.string().trim().min(1).nullable().catch(null)` schema. This contradicts the PR's stated goal of a single source of truth for session shapes. Extract the schema and attribute type into a shared module (or reuse session-purpose's) and import them here instead of defining local copies, so a shape change is made once.</violation>
</file>

<file name="apps/app/components/agent-builder/team-agent-detail.tsx">

<violation number="1" location="apps/app/components/agent-builder/team-agent-detail.tsx:526">
P3: After this change `capabilities` is typed directly from `RouterOutputs["agents"]["byId"]`, and `byId` always returns a non-optional object from `readCapabilities(...)` (even for unreadable or missing manifests). So the `if (!capabilities) { ... "This agent has no deployed version yet." ... }` block below is now unreachable dead code: capabilities is never falsy, so that message can never render. The draft/no-version state is already gated separately by `deployed = agent.currentVersion !== null`. Consider dropping the dead guard so the component only relies on the real `deployed` check.</violation>
</file>

<file name="apps/agent/agent/hooks/activity.ts">

<violation number="1" location="apps/agent/agent/hooks/activity.ts:24">
P2: The refactor removed the `typeof input !== "object" || input === null` guard that previously protected the `Object.entries(input)` loop. `preview` now relies solely on the compile-time `ActionInput` type. This hook reads raw event data directly (unlike agent-builder-state, which parses through the zod schema), so a malformed action whose `input` is null/undefined or a primitive at runtime will throw in `Object.entries` instead of returning "", contradicting the PR's stated goal that malformed inputs still degrade the same way. An exception here would propagate out of the `actions.requested` hook handler and can break the session it is only meant to log.</violation>
</file>

<file name="apps/app/lib/agent-transcript.ts">

<violation number="1" location="apps/app/lib/agent-transcript.ts:288">
P1: When a malformed dynamic-tool event has a non-string `toolName`, this returns it unchanged and `humanise` later throws on `.replace`. Convert or validate the name before returning it.</violation>

<violation number="2" location="apps/app/lib/agent-transcript.ts:403">
P1: When a tool event contains a non-string `errorText`, `errorTextOf` throws while rendering the transcript. Keep the `typeof text === "string"` guard before calling `.trim()`.</violation>
</file>

<file name=".oxlintrc.json">

<violation number="1" location=".oxlintrc.json:48">
P3: This disables `no-unknown-parameters` for every future module added under packages/validation/src, not just the files that legitimately decode `unknown` at their entry points. A new non-boundary function that accepts `unknown` anywhere in the package will silently pass lint, contradicting the boundary-decoder contract the package exists to enforce. Narrow the override to the specific files that parse `unknown` (agent-manifest.ts, and index.ts's `parse`) instead.</violation>
</file>

<file name="apps/api/src/currency/rates.service.ts">

<violation number="1" location="apps/api/src/currency/rates.service.ts:195">
P3: When the provider returns a non-object JSON body (array/string/null), the outer `.catch(UNREADABLE_FEED)` maps it to result "", so it is logged as "provider refused the request" with a null error-type instead of the previous "Exchange rates unavailable" path. Every field already has its own .catch default and zod fires those on missing keys, so the outer catch only ever fires for non-object bodies. Consider distinguishing a malformed body from a provider refusal to keep the warning meaningful.</violation>
</file>

<file name="apps/app/app/(landing)/grant-access/grant-access.tsx">

<violation number="1" location="apps/app/app/(landing)/grant-access/grant-access.tsx:18">
P3: ProviderGrant (grant-access.tsx) and ProviderChoice (social-sign-in.tsx) declare the same `{ label: string; Logo: FC<SVGProps<SVGSVGElement>> }` shape and both import GoogleLogo/MicrosoftLogo, in the same two landing pages. Extract one shared provider-logo shape (e.g. in the UI package) and reuse it in both registries so the brand-logo contract has a single source of truth.</violation>
</file>

<file name="packages/validation/src/eve-tool.ts">

<violation number="1" location="packages/validation/src/eve-tool.ts:21">
P3: The `link` schema only anchors the regex at the start (`/^https?:\/\//`), and since Zod's `.regex()` uses a partial `test()` match, strings are accepted with no hostname or path validation. Values like `"https://"` or `"https://garbage"` pass and surface as clickable source links in the transcript (`sourcesOf`/`hostOf` render them as-is when `new URL()` throws). Accept only strings that form a valid absolute URL, or keep the prefix check but validate the remainder as a host path.</violation>
</file>

<file name="packages/db/src/json.ts">

<violation number="1" location="packages/db/src/json.ts:19">
P3: Newly added `isJsonText` detects strings via `String(value) === value` instead of `typeof value === "string"`. It is functionally correct for all JsonValue variants (only a string primitive makes `String(value) === value` true), but the coercion/equality trick is non-obvious and easy to regress. Use `typeof value === "string"` for a clearer, equivalent guard that also makes the narrowing intent explicit.</violation>
</file>

<file name="apps/api/src/tracking/tracking.controller.ts">

<violation number="1" location="apps/api/src/tracking/tracking.controller.ts:49">
P3: The `.catch({ body: null })` on `trackingRequest` never triggers because the inner `parsedBody` already ends with `.catch(null)`, so `z.object({ body: parsedBody })` always parses successfully (the request is always an object). This redundant outer fallback is dead code that obscures the schema; simplify it to `z.object({ body: parsedBody })`.</violation>
</file>

<file name="apps/api/src/dashboard/dashboard.service.ts">

<violation number="1" location="apps/api/src/dashboard/dashboard.service.ts:286">
P2: When an activity row's meta is stored as non-object JSON (e.g. a JSON array), this previously passed the value through via the cast, but activityMeta.parse (a z.record(...).nullable().catch(null)) rejects non-objects and silently returns null, dropping the data. This diverges from the prior passthrough cast and from the PR's claim that degradation is preserved. Common object-shaped meta is unaffected.</violation>
</file>

<file name="apps/api/src/activities/activities.service.ts">

<violation number="1" location="apps/api/src/activities/activities.service.ts:278">
P3: When a stored activity `meta` value is a JSON array or scalar (not an object), `activityMeta.parse(entry.meta)` fails and `.catch(null)` replaces it with `null` in the serialized response. The previous line was only a type cast (`entry.meta as Record<string, unknown> | null`) and passed such values through unchanged. If no code path stores arrays/scalars under `meta` this is moot, but the parse introduces a silent data-nullification that the prior guard did not have; confirm activity.meta is always an object or null before relying on it.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment on lines +21 to +29
})
.catch({
login: null,
name: null,
company: null,
blog: null,
bio: null,
type: null,
});

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: When GitHub returns a 2xx JSON value such as null, this catch fabricates an account with no profile data. fetchGithubUser then defaults it to User, so verifyGithub can accept the handle alone and write a false GitHub URL. Remove the object-level catch; field-level catches already preserve malformed-field fallback, while non-object payloads must remain rejected.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/lib/socials.ts, line 21:

<comment>When GitHub returns a 2xx JSON value such as `null`, this catch fabricates an account with no profile data. `fetchGithubUser` then defaults it to `User`, so `verifyGithub` can accept the handle alone and write a false GitHub URL. Remove the object-level catch; field-level catches already preserve malformed-field fallback, while non-object payloads must remain rejected.</comment>

<file context>
@@ -6,6 +7,27 @@ import {
+		blog: text,
+		bio: text,
+		type: rawText,
+	})
+	.catch({
+		login: null,
</file context>
Suggested change
})
.catch({
login: null,
name: null,
company: null,
blog: null,
bio: null,
type: null,
});
});
Fix with cubic

if (part.type === "dynamic-tool" && "toolName" in part) {
return String(part.toolName);
}
if (part.type === "dynamic-tool") return part.toolName;

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: When a malformed dynamic-tool event has a non-string toolName, this returns it unchanged and humanise later throws on .replace. Convert or validate the name before returning it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/lib/agent-transcript.ts, line 288:

<comment>When a malformed dynamic-tool event has a non-string `toolName`, this returns it unchanged and `humanise` later throws on `.replace`. Convert or validate the name before returning it.</comment>

<file context>
@@ -268,18 +279,13 @@ function partId(
-	if (part.type === "dynamic-tool" && "toolName" in part) {
-		return String(part.toolName);
-	}
+	if (part.type === "dynamic-tool") return part.toolName;
 	return part.type.replace(/^tool-/, "");
 }
</file context>
Suggested change
if (part.type === "dynamic-tool") return part.toolName;
\tif (part.type === "dynamic-tool") return String(part.toolName);
Fix with cubic

return typeof value === "string" && value ? value : null;
function errorTextOf(part: EveMessagePart): string | null {
const text = "errorText" in part ? part.errorText : undefined;
return text?.trim() ? text : null;

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: When a tool event contains a non-string errorText, errorTextOf throws while rendering the transcript. Keep the typeof text === "string" guard before calling .trim().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/lib/agent-transcript.ts, line 403:

<comment>When a tool event contains a non-string `errorText`, `errorTextOf` throws while rendering the transcript. Keep the `typeof text === "string"` guard before calling `.trim()`.</comment>

<file context>
@@ -380,32 +382,25 @@ export function latestTurnFailure(
-	return typeof value === "string" && value ? value : null;
+function errorTextOf(part: EveMessagePart): string | null {
+	const text = "errorText" in part ? part.errorText : undefined;
+	return text?.trim() ? text : null;
 }
 
</file context>
Suggested change
return text?.trim() ? text : null;
\treturn typeof text === "string" && text.trim() ? text : null;
Fix with cubic


export const agentTriggerConfig = z.object({
intervalMinutes: z
.number()

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When persisted trigger config contains a fractional interval such as 1.5, readAgentTriggerConfig accepts it and queueDueAgentRuns schedules using fractional-minute intervals. Require integer minutes here to preserve the manifest and builder contract.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/validation/src/agent-manifest.ts, line 136:

<comment>When persisted trigger config contains a fractional interval such as `1.5`, `readAgentTriggerConfig` accepts it and `queueDueAgentRuns` schedules using fractional-minute intervals. Require integer minutes here to preserve the manifest and builder contract.</comment>

<file context>
@@ -109,3 +130,56 @@ export function parseAgentManifest(value: unknown): AgentManifest {
+
+export const agentTriggerConfig = z.object({
+	intervalMinutes: z
+		.number()
+		.min(AGENT_TRIGGER_INTERVAL_MINUTES.min)
+		.transform((minutes) =>
</file context>
Suggested change
.number()
\t\t.number().int()
Fix with cubic

contextWindowTokens: model.context_window as number,
pricing: input !== null && output !== null ? { input, output } : null,
};
const body = gatewayCatalog.parse(await response.json());

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When the gateway returns a 200 JSON null, this fallback converts an unavailable response into an empty successful catalog that is cached for 30 minutes. Preserve the top-level-null error path so the caller keeps the unavailable state and retries instead of caching [].

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/settings/model-catalog.service.ts, line 102:

<comment>When the gateway returns a 200 JSON `null`, this fallback converts an unavailable response into an empty successful catalog that is cached for 30 minutes. Preserve the top-level-null error path so the caller keeps the unavailable state and retries instead of caching `[]`.</comment>

<file context>
@@ -80,26 +99,13 @@ export class ModelCatalogService {
-					contextWindowTokens: model.context_window as number,
-					pricing: input !== null && output !== null ? { input, output } : null,
-				};
+			const body = gatewayCatalog.parse(await response.json());
+
+			const models = body.data.flatMap((entry) => {
</file context>
Suggested change
const body = gatewayCatalog.parse(await response.json());
\t\t\tconst raw = await response.json();
\t\t\tif (raw === null) throw new Error("Invalid model catalog response");
\t\t\tconst body = gatewayCatalog.parse(raw);
Fix with cubic


const flag = z.boolean().nullable().catch(null);

const link = z

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: The link schema only anchors the regex at the start (/^https?:\/\//), and since Zod's .regex() uses a partial test() match, strings are accepted with no hostname or path validation. Values like "https://" or "https://garbage" pass and surface as clickable source links in the transcript (sourcesOf/hostOf render them as-is when new URL() throws). Accept only strings that form a valid absolute URL, or keep the prefix check but validate the remainder as a host path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/validation/src/eve-tool.ts, line 21:

<comment>The `link` schema only anchors the regex at the start (`/^https?:\/\//`), and since Zod's `.regex()` uses a partial `test()` match, strings are accepted with no hostname or path validation. Values like `"https://"` or `"https://garbage"` pass and surface as clickable source links in the transcript (`sourcesOf`/`hostOf` render them as-is when `new URL()` throws). Accept only strings that form a valid absolute URL, or keep the prefix check but validate the remainder as a host path.</comment>

<file context>
@@ -0,0 +1,40 @@
+
+const flag = z.boolean().nullable().catch(null);
+
+const link = z
+	.string()
+	.regex(/^https?:\/\//)
</file context>
Fix with cubic

Comment thread packages/db/src/json.ts
return value instanceof Object && !Array.isArray(value);
}

function isJsonText(value: JsonValue | undefined): value is string {

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Newly added isJsonText detects strings via String(value) === value instead of typeof value === "string". It is functionally correct for all JsonValue variants (only a string primitive makes String(value) === value true), but the coercion/equality trick is non-obvious and easy to regress. Use typeof value === "string" for a clearer, equivalent guard that also makes the narrowing intent explicit.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/src/json.ts, line 19:

<comment>Newly added `isJsonText` detects strings via `String(value) === value` instead of `typeof value === "string"`. It is functionally correct for all JsonValue variants (only a string primitive makes `String(value) === value` true), but the coercion/equality trick is non-obvious and easy to regress. Use `typeof value === "string"` for a clearer, equivalent guard that also makes the narrowing intent explicit.</comment>

<file context>
@@ -1,3 +1,25 @@
+	return value instanceof Object && !Array.isArray(value);
+}
+
+function isJsonText(value: JsonValue | undefined): value is string {
+	return String(value) === value;
+}
</file context>
Suggested change
function isJsonText(value: JsonValue | undefined): value is string {
function isJsonText(value: JsonValue | undefined): value is string {
return typeof value === "string";
}
Fix with cubic

.nullable()
.catch(null);

const trackingRequest = z.object({ body: parsedBody }).catch({ body: null });

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: The .catch({ body: null }) on trackingRequest never triggers because the inner parsedBody already ends with .catch(null), so z.object({ body: parsedBody }) always parses successfully (the request is always an object). This redundant outer fallback is dead code that obscures the schema; simplify it to z.object({ body: parsedBody }).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/tracking/tracking.controller.ts, line 49:

<comment>The `.catch({ body: null })` on `trackingRequest` never triggers because the inner `parsedBody` already ends with `.catch(null)`, so `z.object({ body: parsedBody })` always parses successfully (the request is always an object). This redundant outer fallback is dead code that obscures the schema; simplify it to `z.object({ body: parsedBody })`.</comment>

<file context>
@@ -35,6 +36,18 @@ const SWEEP_BATCH = 10_000;
+	.nullable()
+	.catch(null);
+
+const trackingRequest = z.object({ body: parsedBody }).catch({ body: null });
+
 @Controller("api/t")
</file context>
Suggested change
const trackingRequest = z.object({ body: parsedBody }).catch({ body: null });
const trackingRequest = z.object({ body: parsedBody });
Fix with cubic

completedAt: entry.completedAt?.toISOString() ?? null,
createdAt: entry.createdAt.toISOString(),
meta: entry.meta as Record<string, unknown> | null,
meta: activityMeta.parse(entry.meta),

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: When a stored activity meta value is a JSON array or scalar (not an object), activityMeta.parse(entry.meta) fails and .catch(null) replaces it with null in the serialized response. The previous line was only a type cast (entry.meta as Record<string, unknown> | null) and passed such values through unchanged. If no code path stores arrays/scalars under meta this is moot, but the parse introduces a silent data-nullification that the prior guard did not have; confirm activity.meta is always an object or null before relying on it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/activities/activities.service.ts, line 278:

<comment>When a stored activity `meta` value is a JSON array or scalar (not an object), `activityMeta.parse(entry.meta)` fails and `.catch(null)` replaces it with `null` in the serialized response. The previous line was only a type cast (`entry.meta as Record<string, unknown> | null`) and passed such values through unchanged. If no code path stores arrays/scalars under `meta` this is moot, but the parse introduces a silent data-nullification that the prior guard did not have; confirm activity.meta is always an object or null before relying on it.</comment>

<file context>
@@ -273,7 +275,7 @@ function serializeEntry(entry: Entry) {
 		completedAt: entry.completedAt?.toISOString() ?? null,
 		createdAt: entry.createdAt.toISOString(),
-		meta: entry.meta as Record<string, unknown> | null,
+		meta: activityMeta.parse(entry.meta),
 
 		emailThread: entry.emailThread
</file context>
Fix with cubic


export function readAgentTriggerConfig(value: unknown): AgentTriggerConfig {
const parsed = agentTriggerConfig.safeParse(value);
return parsed.success ? parsed.data : UNREADABLE_TRIGGER_CONFIG;

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: readAgentTriggerConfig returns the shared module-level UNREADABLE_TRIGGER_CONFIG object by reference on every unreadable input, and readAgentManifestSummary returns UNREADABLE_MANIFEST_SUMMARY the same way. Callers receive the same mutable instance; if any consumer mutates an event/intervalMinutes field on an unreadable result (e.g. to patch a degraded value), it corrupts the fallback for every subsequent call. No current caller mutates it, but sharing a single mutable fallback across calls is a latent hazard. Return a freshly allocated fallback each time instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/validation/src/agent-manifest.ts, line 154:

<comment>`readAgentTriggerConfig` returns the shared module-level `UNREADABLE_TRIGGER_CONFIG` object by reference on every unreadable input, and `readAgentManifestSummary` returns `UNREADABLE_MANIFEST_SUMMARY` the same way. Callers receive the same mutable instance; if any consumer mutates an `event`/`intervalMinutes` field on an unreadable result (e.g. to patch a degraded value), it corrupts the fallback for every subsequent call. No current caller mutates it, but sharing a single mutable fallback across calls is a latent hazard. Return a freshly allocated fallback each time instead.</comment>

<file context>
@@ -109,3 +130,56 @@ export function parseAgentManifest(value: unknown): AgentManifest {
+
+export function readAgentTriggerConfig(value: unknown): AgentTriggerConfig {
+	const parsed = agentTriggerConfig.safeParse(value);
+	return parsed.success ? parsed.data : UNREADABLE_TRIGGER_CONFIG;
+}
+
</file context>
Fix with cubic

@ripgrim
ripgrim merged commit 3fb9922 into trycompai:main Aug 13, 2026
3 of 6 checks passed
@ripgrim
ripgrim deleted the rg/boundary-types-rest branch August 13, 2026 21:10
strats360 pushed a commit to strats360/crm that referenced this pull request Aug 24, 2026
* Implement currency conversion features and enhance deal handling

- Introduced a new CurrencyModule to manage currency conversion and rates.
- Added ConversionService for handling currency conversions and fetching rates.
- Updated DealsService to support base amounts and currency conversion logic.
- Enhanced Deal and Dashboard functionalities to include reporting currency and unconverted deals.
- Implemented new currency-related contracts and routes for setting reporting currency and manual rates.
- Added integration tests to ensure correct handling of currency conversions and deal totals.

* Refactor currency rates service to use open.er-api.com

- Updated the currency rates service to fetch exchange rates from open.er-api.com, replacing the previous provider frankfurter.dev.
- Enhanced error handling to check for unsupported base currencies in the response.
- Implemented retry logic for fetching rates with a maximum of two attempts and a reduced timeout.
- Cleaned up stale exchange rates for unsupported currencies during the refresh process.
- Updated documentation to reflect the new exchange rate provider and its implications.

* Enhance currency handling and conversion logic

- Introduced baseCurrency to the Deal model to track the currency of baseAmount.
- Updated ConversionService to streamline currency conversion processes and improve deal field handling.
- Enhanced CurrencyService to enforce permissions for managing currency settings based on user roles.
- Refactored DealsService to incorporate base currency logic in deal aggregations and reporting.
- Improved DashboardService to accurately reflect open deal values based on the current reporting currency.
- Updated integration tests to validate new currency handling features and ensure correct behavior across services.

* Enhance currency conversion logic and improve deal handling

- Updated `pendingWhere` method in `ConversionService` to explicitly match null `baseCurrency`, ensuring no deals are excluded from totals.
- Added integration test to verify that deals with missing currency are correctly handled and updated.
- Modified seeding logic to ensure `baseCurrency` is set alongside `baseAmount` for newly created deals, preventing issues with unconverted figures.
- Updated documentation to clarify changes in currency handling and the implications for deal visibility.

* Refine currency conversion logic and enhance deal handling

- Updated `ConversionService` to conditionally clear rates only when `onlyMissing` is false, improving efficiency in handling missing currencies.
- Enhanced integration tests to verify correct behavior when dealing with unconverted figures and missing currency rates.
- Introduced a new utility function in the deal sheet component to manage currency options, ensuring proper display of unsupported currencies.

* Revise agent and API documentation for clarity and structure

- Updated AGENTS.md to emphasize the importance of reviewing relevant documentation before starting work, including a new index table for quick reference.
- Refined API rules in api.md to clarify logging practices and the separation of intelligence from the API.
- Consolidated environment setup instructions into a new setup.md file for better organization and ease of access.
- Enhanced currency handling in DashboardService and related tests to ensure accurate reporting and conversion logic.
- Improved integration tests to validate new currency handling features and ensure correct behavior across services.

* Enhance documentation and introduce new currency handling guidelines

- Updated AGENTS.md to include new references for the Agent panel and local setup instructions.
- Added a new docs/agent-panel.md file detailing the Agent panel's functionality and usage.
- Introduced docs/currency.md to clarify currency handling rules and reporting practices.
- Revised environment setup instructions in docs/environment.md for better clarity and organization.

* Add anonymous usage telemetry documentation and enhance currency handling in DealSheet

* Implement anonymous usage telemetry and enhance related documentation

- Added telemetry functionality to track anonymous usage data, including installation metrics and tool usage.
- Introduced new environment variables for telemetry configuration in `.env.example`.
- Updated `AGENTS.md` to reference the new telemetry documentation.
- Created a `TelemetryModule` with services and controllers for managing telemetry data.
- Added a settings page for telemetry configuration in the application.
- Enhanced error handling and logging for telemetry events across various services.
- Removed outdated ADR on telemetry usage from the repository.

* Remove telemetry-related components and references from the application

- Deleted the TelemetryRouter and its associated service, removing the telemetry status query.
- Updated the settings sidebar to eliminate the Telemetry option.
- Removed the TelemetrySettingsPage and its related components, including the TelemetryStatus display.
- Cleaned up unused imports and references to telemetry throughout the codebase.

* Enhance telemetry functionality and improve budget management

- Added an 'exhausted' state to the focus management to track when the research budget is depleted.
- Updated the spend function to prevent multiple budget exhaustion events from being recorded.
- Refactored the rollup service to handle telemetry rollup claims and restore counters more effectively.
- Improved error handling in telemetry events to ensure proper reporting and recovery from failures.
- Enhanced documentation to clarify the behavior of telemetry when disabled and the implications for data integrity.

* Add telemetry support and enhance landing page analytics

- Introduced `@crm/telemetry` package to manage telemetry configurations and constants.
- Integrated `posthog-js` for analytics on the landing page, ensuring it only runs on allowed domains.
- Updated the `LandingAnalytics` component to initialize analytics tracking based on hostname.
- Enhanced the `audit` hook to exclude specific event types from archiving.
- Improved agent session handling by implementing offline thread management.
- Added utility functions for analytics host validation and created tests for the new functionality.
- Updated documentation to reflect changes in telemetry usage and landing page analytics.

* Update agent panel to use SETTLED_TTL_MS for archive stale time and enhance documentation

- Changed the `staleTime` for the archive query in the agent panel from `Infinity` to `SETTLED_TTL_MS` to ensure proper session management.
- Updated documentation to clarify the behavior of the archive in relation to session state and stale time handling.

* Enhance landing page analytics with CTA event tracking

- Introduced `captureLanding` function to track user interactions with the setup prompt and GitHub star buttons.
- Updated `SetupPromptButton` and `GitHubStarButton` components to accept a `location` prop for distinguishing between 'hero' and 'closing' CTAs.
- Modified `LandingAnalytics` to include new event types for clipboard actions and button clicks.
- Enhanced documentation to reflect the new telemetry events and their usage.

* Update README with new images and remove outdated ones

- Replaced outdated images with new visuals for the landing page, showcasing agents and capabilities.
- Removed references to deleted images related to deals, contacts, and companies to streamline documentation.

* Refactor README to improve layout of screenshots

- Converted individual screenshot sections into a table format for better visual organization.
- Updated captions for clarity and conciseness, enhancing the overall presentation of the landing page visuals.

* Update README and images for landing page

- Removed outdated captions from the README for agents and capabilities images to streamline content.
- Updated binary images for agents, capabilities, and hero sections to enhance visual quality on the landing page.

* Update README and replace landing hero image

- Updated the README to reflect the new image caption for the companies list.
- Replaced the outdated landing hero image with a new product shot to enhance visual appeal.
- Removed the old landing hero image from the repository.

* Update landing page images for agents and capabilities

- Replaced existing binary images for agents and capabilities on the landing page to improve visual quality and consistency.
- Ensured that the new images align with the recent updates to the README and overall landing page design.

* Update landing page images for agents and capabilities to enhance visual quality

* Refactor AddButton component in multiple sheets to use ComponentProps for better type safety

- Updated the AddButton function in create-company-sheet, create-contact-sheet, create-deal-sheet, and add-sso-provider-sheet to accept props of type ComponentProps from the Button component.
- This change enhances type safety and allows for more flexible button properties across different sheets.

* Refactor TelemetryService to integrate RollupService for telemetry rollups

- Replaced FunnelService with RollupService in TelemetryService to handle telemetry rollups.
- Implemented a timer to run rollups hourly, enhancing telemetry data collection.
- Updated documentation to reflect changes in telemetry rollup processes and clarify the in-process execution without cron dependencies.

* Report installs without a cron, and stop double counting them

The install count was reading 1 while 20 databases had migrated. Every
"Active installs" tile is built on install_daily, which only ever fired
from POST /internal/telemetry/rollup — a route that refuses to run
without CRON_SECRET. An install that never configures a cron reported
nothing at all, however much it was used.

TelemetryService now rolls up in-process, on boot and hourly. The
existing row lock on install makes all but the first of those a no-op,
and it short-circuits before the aggregation runs, so it is still one
set of grouped queries per install per day. The route stays, still
behind CRON_SECRET, for a platform cron that would rather drive it;
nothing depends on it now.

Two ways the same event could arrive twice, both of which the hourly
timer would have made more frequent:

A rollup wrongly read as failed hands the day back and is re-sent.
posthog-node does not reject on a failed send, so the client inferred
failure from a module-global error counter that any other capture could
move. It now enqueues and awaits flush(), which does throw, and treats
either signal as a failure — erring toward a re-send, which is free,
over consuming a day whose event never left.

A milestone sent before it was recorded, so both the boot sweep and the
rollup sweep could send the same step. One install sent
first_fact_applied six times. The insert is now the claim: of two
sweeps exactly one is told it landed the row, and only that one sends.
A failed send deletes the row so the step is retried.

Both events also carry a deterministic uuid derived from the install
and the day (or the step), so a duplicate that does get out is ingested
once. Installs and active installs were always safe — PostHog's unique
math is per install per day — but the summed agent-usage properties
were not.

* Derive the dedupe id with SHA-256 in a v8 uuid

CodeQL flags a weak algorithm reached by the install identity, and it
is right that the two do not belong in one expression. SHA-1 was there
only because RFC 4122 defines v5 that way; nothing depends on being a
v5, so this is a SHA-256 digest in a v8 uuid, the slot RFC 9562 leaves
for a derivation of one's own.

* CMP-1 chore: enrich agentic experience

* ci: open pull requests, gate titles and promote releases automatically (trycompai#53)

* Lewis/contact and currencies (trycompai#56)

* Implement fields management features (trycompai#55)

* Lewis/dynamic field fix (trycompai#70)

* Refactor query prefetching in Companies, Contacts, and Deals pages to… (trycompai#71)

* chore: release main (trycompai#72)

* feat(api): add microsoft sign-in and outlook mailbox sync (trycompai#73)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* chore: release release

* ci: run release-please on main and document merge order (trycompai#76)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore: release main (trycompai#78)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* feat(db): CMP-1 persist durable custom agents (trycompai#67)

* feat(agent): CMP-1 add sandboxed builder and runner runtimes (trycompai#60)

* refactor(app): CMP-59 harden CRM UI foundations (trycompai#61)

* feat(app): CMP-46 add the private agent builder workspace (trycompai#62)

* feat(app): CMP-12 review agent drafts before deployment (trycompai#63)

* fix(app): CMP-47 consolidate agent builder presentation (trycompai#64)

* feat(app): CMP-47 add inline composer context

* fix(app): move chat beneath overview in icon rail (trycompai#83)

Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com>

* fix(ci): tag releases automatically and keep previews off the production schema (trycompai#82)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* chore(main): release 1.4.0 (trycompai#86)

* feat(agent): bound agent builder retries and improve chat scrolling (trycompai#89)

* fix(app): render agent transcript chronologically with anchored tool results (trycompai#92)

Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix(agent): declare granted write actions in draft access summary (trycompai#93)

Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com>

* chore(main): release 1.5.0 (trycompai#91)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(api): warn when the deployed schema does not match schema.prisma (trycompai#88)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* CMP-62 chore: add gh-stack skill (trycompai#96)

* chore(main): release 1.5.1 (trycompai#97)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(ci): make the release guard reject only genuinely untagged pull requests (trycompai#105)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* fix(ci): stop the auto-titler downgrading a release

The titler regenerated the title on every push once it had written one, so a
pull request's subject was whatever its *last* commits looked like. trycompai#105 carried
the whole website-tracking feature and was retitled `fix(ci)` by its final push,
squashed onto main under that subject, and released as a patch whose notes
mention none of it.

A generated title is now left alone unless it stops being a conventional commit
or stops covering the branch, and no title — generated or typed — may release
less than the commits behind it: `floor_of` takes the strongest bump on the
branch and `generate` raises its proposal to meet it. A branch holding a `feat`
cannot ship as a `fix`, and one holding a breaking change cannot ship without
the `!`. Over-releasing is the safe direction; losing a feature out of the
changelog is not.

* feat(tracking): add website tracking with form capture and attribution

A first-party script on the marketing site, a collector in the API, and one
rule: a form submission becomes a contact. Page views, click labels and
first/last-touch attribution hang off that, with a 90-day retention sweep, an
hourly contact cap and a per-minute event budget.

The work landed in 815a832. The auto-titler had retitled its pull request
`fix(ci)` on the last push, so it squashed onto main under that subject and
released as a patch whose notes describe only the guard fix. This commit carries
no code — it exists so the changelog and the version say what actually shipped.
See docs/tracking.md.

* chore(main): release 1.6.0 (trycompai#106)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(ci): make a release one pull request instead of two

Shipping was a release pull request into `main` and a promotion pull request
into `release`, open at the same time, with a warning on the promotion telling
you to merge the other one first. Merge them the wrong way round and you shipped
untagged code and left the version behind for the next promotion. Nobody should
have to hold that rule in their head to deploy.

The tag and the code have to travel together, so the release workflow now does
it in one step: when release-please cuts the tag it merges that exact commit
into `release` through the merges API. One pull request, no order to remember,
and the tag is by construction an ancestor of what shipped. `promote.yml` is
gone.

A conflict is the one case a human still has to see, and it can only mean
somebody committed to `release` directly, so it fails the run and says so rather
than quietly leaving production behind.

Non-releasable commits now wait for the next release rather than riding a
promotion, which is the trade: `release` moves when a tag is cut and at no other
time.

* fix(ci): fall back to the pushed commit when release-please reports no sha

* chore(main): release 1.6.1 (trycompai#108)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* feat(db): add peek script for inspecting database contents (trycompai#110)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* chore(main): release 1.7.0 (trycompai#111)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* feat(agent): apply sourced facts to empty fields automatically (trycompai#112)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* chore(main): release 1.8.0 (trycompai#113)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(ci): ship releases by opening a pull request into release (trycompai#114)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* chore(main): release 1.8.1 (trycompai#115)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(agent): fill blank fields on the dispatch tick instead of sign-in (trycompai#117)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* chore(main): release 1.8.2 (trycompai#118)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* feat(agent): stop suggesting a URL that already matches the field (trycompai#120)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* chore(main): release 1.9.0 (trycompai#121)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* feat(tracking): support installing the tracking tag via Google Tag Manager (trycompai#124)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* chore(main): release 1.10.0 (trycompai#126)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* feat(app): copy the tracking snippet for the selected install method (trycompai#128)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* chore(main): release 1.11.0 (trycompai#129)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* feat: edit a deployed agent, and show what Slack actually granted (CMP-77) (trycompai#109)

* chore(main): release 1.12.0 (trycompai#132)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* feat(app): search company dropdowns instead of scrolling them (trycompai#125)

* fix(app): show select field values in record tables (trycompai#133)

* fix(agent): let the assistant chat read the deal list it is told to use (CMP-77) (trycompai#139)

* chore(main): release 1.13.0 (trycompai#136)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore: add anti-slop lint rules, dead-code checks and stricter Biome constraints (CMP-80) (trycompai#145)

* refactor: clear anti-slop type assertions and conditional object spreads (CMP-81) (trycompai#146)

* docs: propose an i18n layer (trycompai#143)

* refactor: parse every remaining I/O boundary into a domain type (CMP-82) (trycompai#151)

* fix: unblock the test suite and actually install the git hooks (CMP-83) (trycompai#152)

* ci: run anti-slop lint in CI and pre-push (CMP-84) (trycompai#153)

Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com>

* feat: enrichment queue widget (CMP-92) (trycompai#159)

* feat(agent): read people from Context.dev instead of RapidAPI (CMP-86) (trycompai#158)

Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com>

* feat: page the enrichment queue (CMP-92) (trycompai#160)

Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com>

* chore(main): release 1.14.0 (trycompai#147)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* feat(agent): scope field backfill tasks to records missing values (trycompai#163)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* chore(main): release 1.15.0 (trycompai#164)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(api): serve openapi.json and bundle swagger deps in function build (trycompai#166)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* chore(main): release 1.15.1 (trycompai#167)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Lewis/openapi json (trycompai#169)

* docs(api): explain runtime openapi document and vendoring rules (trycompai#170)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* chore(main): release 1.15.2 (trycompai#171)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(app): prevent url param collision between fields sheet and table filter (trycompai#175)

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>

* fix: stop a finished enrichment reading as failed (trycompai#173)

* chore(main): release 1.15.3 (trycompai#176)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* ci: add Vercel deploy workflow

Deploys to Vercel on push to main/release branches.
Also supports manual trigger via workflow_dispatch with
environment selection (preview/production).

Uses secrets: VERCEL_API_KEY, VERCEL_ORG_ID, VERCEL_PROJECT_ID

---------

Co-authored-by: Lewis Carhart <lewis@trycomp.ai>
Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Guzman Pintos <37162183+GuzmanPintos@users.noreply.github.com>
Co-authored-by: twinprime19 <38123958+twinprime19@users.noreply.github.com>
Co-authored-by: Kiro Agent <244629292+kiro-agent@users.noreply.github.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.

1 participant