HANZO_WEBHOOKS entries are <team url>=<url>: another team pointing at a listed URL is posted as a stranger. A call made as eSign does not follow a redirect, so the token reaches the listed URL and nowhere else.
A team chooses where its webhook points, so the origin was not enough: an admin
could aim eSign's identity at any Hanzo API route and read the answer in the
webhook log. One sender now serves sends and resends: a URL HANZO_WEBHOOKS
names exactly is posted with eSign's IAM token and its answer is not kept;
every other URL gets the webhook's own secret and nothing of ours.
notify's token is minted for the Hanzo API rather than for eSign itself, and a
bearer eSign's own client minted is refused.
An audience proves nothing alone: any client IAM authenticates may mint a token
for this app's resource. So the bearer is an application's access token whose
azp IAM_APP_TEAMS lists, and it acts for exactly the team that list maps it to,
as that team's organisation owner. People sign in; the person bearer and the
X-Team-Id choice are gone, and so is IAM_ORG. A sign-in's id_token is never a
bearer.
A webhook carries eSign's identity only to the Hanzo API, minted for it; any
other endpoint gets the webhook's own secret and nothing of ours.
A call without a session carries a Hanzo IAM access token: signed by a key in
hanzo.id's JWKS, issued by hanzo.id, minted for this app (aud = IAM_CLIENT_ID)
and unexpired. One resolver, getBearer, sits behind /v1/rpc, /v1/rest and the
download routes, and acts in the team X-Team-Id names: a person as the account
their sign-in linked, an application of IAM_ORG as the team's organisation
owner. API tokens no longer get into the API; Zapier, which could carry nothing
else, is deleted.
Outbound, one Machine mints eSign's client_credentials token for the service it
is spent at. notify uses it as before, and every webhook now carries one minted
for its endpoint's origin, so a Hanzo receiver verifies the caller against
hanzo.id instead of holding a shared secret.
Emails, the signing page's title, the sealed PDF's reason, the sign-in
refusal and the dialogs still said Hanzo Sign. The catalogs change with
the strings, message for message, so every translation keeps its id.
The wordmark was an SVG that guessed its own width, and clipped the name
to "Hanzo Sigı". It is now text: NEXT_PUBLIC_APP_NAME, "Hanzo eSign" by
default, measured by the browser and sized by the caller, with no mark in
front of it anywhere. The mobile menu and the public profile header drew
the upstream Documenso logo; the emails and the sealed PDF's certificate
and audit log drew it too, from static/logo.png, which is now the
wordmark set in Zen.
The app header is hanzo.ai's: the wordmark at the left, quiet nav, pill
controls at the right, a hairline under it. A visitor without a session
sees the same bar with the wordmark alone, and a footer with the
copyright and hanzo.ai's terms and privacy policy, on sign-in, the other
unauthenticated pages, a recipient's completion and result pages and
public profiles. The signing pages keep their own bar, which now shows
the wordmark at every width.
The theme takes @hanzo/ui DESIGN.md's values: a true-black page, #0a0a0a
surfaces, #171717 raised, #ededf1 ink and white-alpha hairlines in dark;
white, #0a0a0a ink and black-alpha hairlines in light. Each is an opaque
triplet, so Tailwind's alpha modifiers still apply.
Type is Zen and Zen Mono from @hanzo/font, at Zen's own weights. Buttons
are pills, the primary in the ink colour; the option cards built from a
Button keep a card's corners, and the folder actions wrap on a phone
rather than breaking a pill over two lines.
The socket the app's pages call over (/zap) opened only for a session, so
a recipient signing by their link, who has no account, could not reach
the routes that sign a field or complete a document: every field they
clicked failed with "An error occurred while signing the field". tRPC had
served those as public procedures; the port to ZAP kept their bodies and
lost the door.
A connection from the app's own pages with no session now opens as
anonymous and reaches only the routes tRPC served publicly, each
authorised by the token it carries; every other route answers it 401
before its handler runs. The operation API still refuses a request
without a token.
The signer takes an unset NEXT_PRIVATE_SIGNING_TRANSPORT as local and
reads the certificate; the health check took it as something else and
answered ok. Production had no certificate and said so nowhere.
The form learns server ids only on a step change, so each autosave after
the first sent the recipients it had just created without an id: the
server deleted them and created them again, with new ids and signing
tokens. The provider now keeps the id it got back for each row's formId,
sent as clientId and echoed by envelope.recipient.set.
cmdk 1 writes data-disabled="false" on every enabled item, and the item
matched the bare attribute, so every item in every combobox took
pointer-events: none. The fields step could not select a second
recipient, so the second recipient got no signature field and the send
refused.
@lingui/macro is the Lingui 5 package, and its macro is not one the
Lingui 6 babel plugin can run, so the server build stopped on the two
files still importing it.
Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
An @page nested in a style rule is not CSS: browsers ignore it, and
lightningcss refuses to minify the stylesheet around it. The PDF
routes set their own page size, so nothing printed changes.
Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
The app's config and stylesheet are Tailwind 3: @tailwind directives,
a plugin reading tailwindcss/lib/util/flattenColorPalette, and the
plugin handed to PostCSS directly. Tailwind 4 refuses all three, so
the image build stopped at the stylesheet. The docs app stays on 4.
Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
prisma generate loads prisma.config.ts, and env() throws when
DATABASE_URL is unset, so every build without a database stopped at
prebuild. generate needs no database; migrate still refuses an empty
URL.
Claude-Session: https://claude.ai/code/session_01TUxvfuta1Z44KVNYuAz71F
turbo.json names its tasks the turbo 2 way, which the turbo 1 the image
installed cannot parse, so the build stopped at prune. The patches
directory is gone. Prisma 7 reads the database URL from
prisma.config.ts, so the runner carries it and migrate names it.
Claude-Session: https://claude.ai/code/session_01TUxvfuta1Z44KVNYuAz71F
The resource API is /v1/rest and the operation API is /v1/rpc: named
for what they are, so neither shadows the other, and the v2-beta alias
is gone. Hanzo IAM returns to /auth/callback, the callback path IAM
already derives for this host, so sign-in needs no IAM change.
Claude-Session: https://claude.ai/code/session_01TUxvfuta1Z44KVNYuAz71F
The suppression count for this work is `git grep -c` over '@ts-ignore',
'@ts-expect-error', 'as any', 'as unknown as', ': any' and eslint-disable.
Three comments added in it matched as prose: a quoted index signature in
the v1 contract, "over: any string" in the upload test and "has any meta"
in the field tooltip. They say the same thing in words that do not match,
so the counts read 10 `as any` and 8 `: any`, as before this work.
Co-authored-by: Hanzo Dev <[email protected]>
`Boolean(typeof ClipboardItem && navigator.clipboard.write)` looks like a
check for ClipboardItem, but `typeof` always yields a non-empty string, so
the expression was always navigator.clipboard.write — ESLint 10's
no-constant-binary-expression says so. The check now says only that, which
is what it has always evaluated to, and a comment records why that is
enough: without ClipboardItem, handleClipboardApiCopy throws inside its own
try and falls back to writeText. The ternary used as a statement becomes an
if, as typescript-eslint 8's no-unused-expressions asks.
Co-authored-by: Hanzo Dev <[email protected]>
The lint config makes @typescript-eslint/promise-function-async and
require-await errors. Seven functions in the ZAP React hooks return a
promise without being async — the query and mutation functions and three
cache utilities — and two route handlers are async with nothing to await:
both only log and throw NOT_IMPLEMENTED.
The seven are async. A synchronous throw inside them would now reject
rather than throw, which TanStack Query already turns every queryFn and
mutationFn throw into, and the utilities return what they returned. The two
handlers drop async. The dispatcher is their only caller and awaits them
inside one try, which catches a thrown error and a rejected promise alike.
Co-authored-by: Hanzo Dev <[email protected]>
ESLint 10's recommended set added no-useless-assignment, which fails on a
value no later statement can read. Fourteen places had one:
- An initial value every path overwrites before reading: `let x = null`
ahead of a try that assigns it or leaves, the webhook response body both
branches set, the rate-limit loop's counter, the avatar's previous id and
the license response. They are declared without one; TypeScript still
checks each is assigned before use.
- Konva stages and layers set to null after destroy(), just before the
function returns and the variable goes out of scope. The nulls go, and
the variables are const.
- Two in the v1 PDF field writer: textFieldBoxY recomputed after the only
read of it had already been copied into adjustedFieldY, and a line reset
to '' that both following branches overwrite.
- The folder breadcrumbs' currentFolderId, reassigned in a loop that walks
by currentFolder.parentId; the first query reads folderId directly.
No output changes: each removed value was provably never read.
Co-authored-by: Hanzo Dev <[email protected]>
ESLint 10's recommended set added preserve-caught-error: a catch block that
throws a new error has to pass the caught one as its cause. Five places
caught an error and threw a plainer one in its place — fetching the OIDC
configuration, sending a confirmation email, triggering a webhook,
validating a Zapier API token, rendering an email — and each failed the
lint.
Each now passes `{ cause: err }`. The thrown error keeps its message and
type; what it gains is the original, which Node prints after it as
`[cause]`. render-email-with-i18n.tsx also loses its stale ban-types
disable comment, as the five files in the commit before did.
Co-authored-by: Hanzo Dev <[email protected]>
Six lines disabled @typescript-eslint/ban-types over `string & {}`, the
union that keeps a string type's known values in autocomplete.
typescript-eslint 8 removed ban-types, and ESLint 10 reports a disable
comment for a rule that does not exist as an error — "Definition for rule
'@typescript-eslint/ban-types' was not found" — so each of them failed the
lint. Its successors do not flag `{}` inside an intersection, so with the
comments gone nothing is reported on those lines. Five go here; the sixth,
in render-email-with-i18n.tsx, goes with that file's other lint fix.
Co-authored-by: Hanzo Dev <[email protected]>
packages/app-tests has no typecheck script, and 10 errors had collected in
its specs. None changes what a spec sends or checks:
- Two API specs map over create-many's JSON, which Playwright types as any,
so the callback's parameters were implicitly any. The responses are typed
as TCreateEnvelopeRecipientsResponse, as the file already types its other
responses.
- A PDF built with Buffer.from(Uint8Array) is a Buffer<ArrayBuffer>; its
helpers said Buffer, which @types/node 26 reads as possibly shared memory,
and a Blob takes no shared memory.
- The seed's updateDocumentOptions was typed as Prisma's unchecked update
input, which has no relation fields, while the dictation specs upsert
documentMeta through it — which Prisma takes as the checked input. It is
typed as EnvelopeUpdateInput; every caller passes fields that input has.
- A spec typed a seeded document as Document, a model the schema replaced
with Envelope.
- The alignment spec picked each field's type and fieldMeta apart, which
loses which meta goes with which type; it drops the two keys the payload
does not take and spreads the rest, and types the payload as the schema's
input, which is what a client sends.
Co-authored-by: Hanzo Dev <[email protected]>
upload.test.ts feeds NEXT_PUBLIC_UPLOAD_TRANSPORT values that are not a
transport, to check each one throws. process-env.d.ts declares the variable
as 'database' | 's3', so assigning any other string failed packages/lib's
typecheck; it did before the version sweep too. The test writes through
process.env's string index instead, which is what the process hands over.
3 of 3 pass, as before.
Co-authored-by: Hanzo Dev <[email protected]>
getCompletedFieldsForDocument filtered fields by documentId and
getActiveSubscriptionsByUserId filtered subscriptions by userId. Fields
belong to an envelope and subscriptions to an organisation, and neither
column exists — they were already gone before the version sweep — so both
failed packages/lib's typecheck, and either would have thrown if called.
Nothing calls either.
Co-authored-by: Hanzo Dev <[email protected]>
next-plausible went from 3 to 4, which takes the script's URL instead of a
domain and throws "src is required when not using withPlausibleProxy" when
it has none. The docs layout passed `domain`, so every docs page threw on
render.
3 rendered Plausible's shared script, https://plausible.io/js/script.js,
with data-domain set to the domain. The layout now hands 4 that URL and that
attribute. What differs is 4's own inline stub, which also defines
plausible.init() and calls it, and `async` without 3's `defer`.
Co-authored-by: Hanzo Dev <[email protected]>
lucide-react went from 0.554 to 1 in the version sweep, and 1 dropped its
brand icons. The docs home imports GithubIcon for its two "View on GitHub"
links; the import is undefined, and rendering an undefined component throws,
so the home page could not render.
The page builds the icon with lucide's own createLucideIcon from the path
data 0.554 shipped. Rendered to markup, it is identical to 0.554's
GithubIcon, attributes and class names included.
Co-authored-by: Hanzo Dev <[email protected]>
The v1 contract went through ts-rest's c.router with the authorization
header as baseHeaders. @ts-rest/core 3.52's router types merge that header
through zod 3 types — AnyZodObject, objectUtil.MergeShapes, a ZodObject of
three type arguments — which zod 4 does not have. Behind skipLibCheck each
one is `any`, and the route types they build collapse to
`{ [x: string]: any; method; query }`: no path, no body, no responses. Every
handler lost its params, query and body — 56 errors in the implementation —
and every example's client call stopped being callable.
The routes are now a plain object `as const satisfies AppRouter`, each
naming the header itself. At run time c.router did nothing else here: it
merged baseHeaders into routes that had none, which is the header schema
itself, and copied responses. ts-rest's server and client types read the
plain object correctly under zod 4, and all 19 routes type their handlers.
Two examples were wrong once their types could be read: sendDocument
requires a body — `{}` takes the defaults the server would apply — and
createField answers `{ fields, documentId }`, not a field, so the example
reads the field out of `fields`.
Co-authored-by: Hanzo Dev <[email protected]>
The v1 API's schemas called extendZodWithOpenApi(z) from
@anatine/zod-openapi at load and then .openapi({...}) on six fields.
anatine 1.14 is written for zod 3: it patches z.ZodSchema.prototype, and in
zod 4 ZodSchema is a type with no value behind it. Under zod 4 the call
throws "Cannot read properties of undefined (reading 'prototype')" when
schema.ts loads — and schema.ts loads with the contract, the
implementation and the OpenAPI document, which the app's server router
imports, so since the version sweep that router could not load. The types
said as much: `openapi` does not exist on a zod 4 schema.
The extension goes. The four descriptions and the two lists of allowed time
zones and date formats move to .meta(), which zod 4 keeps in its registry;
the two empty .openapi({}) calls carried nothing and go. No parse changes.
Not fixed here: @ts-rest/open-api 3.52 still converts schemas through
anatine, which reads zod 3's internals, so /api/v1/openapi.json now loads
but every schema in it is empty. Before the sweep it had them.
Co-authored-by: Hanzo Dev <[email protected]>
The number dialog adds too_small and too_big issues for a field's min and
max without a message of its own, so zod's words are the ones shown. zod 4
reads what was measured from `origin`; the dialog still said `type`, zod 3's
name, so the types refused the issue and the message read "Too small:
expected undefined to be >=5". With `origin: 'number'` it reads "Too small:
expected number to be >=5" and "Too big: expected number to be <=10". Before
the sweep zod 3 said "Number must be greater than or equal to 5".
No test here: the schema is built inside the dialog in apps/remix, which has
no test runner. The strings above were measured with zod 4.6.5 and, for the
last one, zod/v3.
Co-authored-by: Hanzo Dev <[email protected]>
Six delete dialogs ask for a name typed exactly, as z.literal(name, {
errorMap: () => ({ message }) }). zod 4 took errorMap out of a schema's
params — it is `error` now — and ignores what it does not know, so since
the version sweep a wrong entry has shown zod's default, `Invalid input:
expected "delete acme"`, instead of "You must enter 'delete acme' to
proceed". The types caught it: the object literal no longer matched, and the
failed overload left the field's value typed as any literal, which the
input would not take.
The six build the field with createConfirmationSchema(name, message), which
spells it the zod 4 way. confirmation.test.ts checks the exact text passes
and near misses fail with the given message; with the errorMap spelling put
back, the second case fails on zod's default. zod 3 kept the message, so
this is the behaviour from before the sweep.
Co-authored-by: Hanzo Dev <[email protected]>
The version sweep resolved @hanzo/insights to 1.358.1, which exports its
client as `insights` and as the default, and no longer as `posthog`. The
analytics hook imported `posthog`, so the binding was undefined — a link
error for the ESM build, and a TypeError on the first capture wherever the
module did load.
The hook imports `insights`. use-analytics.test.ts sets an Insights key,
records what reaches the client and checks an event and an error both get
there, and that nothing does without a key; against the old import the
first case fails on the undefined client.
turbo.json still declared the key under its pre-rename name,
NEXT_PUBLIC_POSTHOG_KEY, which nothing reads; it declares
NEXT_PUBLIC_INSIGHTS_KEY, which the feature flags read and the test sets.
Co-authored-by: Hanzo Dev <[email protected]>
@types/react went from 18 to 19 with React. Three of its changes reached
this code, none of them at run time:
- useRef(null) is a RefObject<T | null>, so the PDF viewer's constraint ref
and the signature pad's validity check take one. The virtual list the
first is handed to already did, and the check already returns false when
current is null.
- useRef takes its initial value; the autosave timer and the signing card's
timeout pass undefined, which is what useRef() gave them.
- The global JSX namespace is gone; the role icons import JSX from react.
Co-authored-by: Hanzo Dev <[email protected]>
recharts went from 2 to 3. A custom tooltip's props are now
TooltipContentProps — TooltipProps no longer carries payload or label — and
a Bar's `label` no longer takes a string.
The tooltips take Partial<TooltipContentProps>: each is rendered as
<CustomTooltip /> with no props, and recharts clones it with the tooltip's.
The three string labels go. recharts 3 renders a label only for true, an
element, a function or an object, and returns null for anything else, as 2
did, so those strings never drew anything.
Co-authored-by: Hanzo Dev <[email protected]>
content-disposition went from 1 to 3, which is ESM with named exports only:
create, format, parse. The file routes still took its default export, which
3 does not have, and an ESM import of a missing default fails when the
module links, so the routes that serve documents could not load.
They import create under the name they already call. It is 1's function
under a new name — attachment by default, an ASCII fallback beside the
UTF-8 filename* — except that 3 leaves a name that is a valid token
unquoted: `filename=Contract.pdf` where 1 wrote `filename="Contract.pdf"`.
Both are RFC 6266.
Co-authored-by: Hanzo Dev <[email protected]>
pdfjs-dist went from 5 to 6 and PDFDocumentProxy lost destroy(). In 5 it
was one line, `return this.loadingTask.destroy()`, so the task is what
there is to destroy. Three calls were left:
- pdfToImages called it after rendering. The call threw before its .catch
could attach, so every render ended in "pdf.destroy is not a function"
and the AI features that read a document got nothing. The task it also
destroyed is the same task, so that call is the one kept.
- The PDF viewer called it when a new document replaced an old one, inside
the load's try, so switching documents showed the load error; and in the
effect's cleanup. Both now destroy the loading task, as 5 did for them.
pdf-to-images.test.ts renders a two-page PDF and checks each page comes
back as a letter-size JPEG in order; against the old code it fails with the
TypeError.
Co-authored-by: Hanzo Dev <[email protected]>
inngest went from 3 to 4 in the version sweep. 4's createFunction takes the
triggers inside its options and the handler second, and throws when handed
the old three arguments. With the Inngest provider selected, the first
defineJob threw "createFunction expected a handler function as the second
argument", so no job could be registered. The trigger now sits in the
options.
Starting a job as a step of another sent `{ ...options, timestamp }`, whose
payload was not the event's `data`, so the job it started parsed an empty
event. inngest 3's types let that through and 4's refuse it. Both ways of
starting a job now send toInngestEvent(options): the payload as `data`, the
timestamp as `ts`, as triggerJob already did. inngest.test.ts holds the
mapping; a run of defineJob against the installed inngest registers each
job with its event or cron trigger and a handler.
Co-authored-by: Hanzo Dev <[email protected]>
@types/nodemailer 8 types a message's `from` as one address or a list, as
nodemailer has always accepted. The transport only knew a string or one
Address, so the list case did not type-check and, at run time, posted a
sender with no email.
The sender now goes through toMailChannelsAddresses, which already handles
all three shapes for to, cc and bcc, and the first address is the sender. A
string or a single Address comes out exactly as before.
mailchannels.test.ts sends each shape through nodemailer and reads what was
posted; against the old code the list case fails with `{}`.
Co-authored-by: Hanzo Dev <[email protected]>
@noble/ciphers 2 moved managedNonce from webcrypto.js to utils.js. The
import left behind named an export that no longer exists, so crypto.ts —
and everything that stores or reads an encrypted secret through it — could
not load: in ESM a missing named export fails the module at link time.
crypto.test.ts round-trips a secret, checks each write draws a new nonce and
that another key is refused, and decrypts a secret written by
@noble/ciphers 0.6.0 through this module's old code, since those are what
the database holds. Against the old import all four fail.
Co-authored-by: Hanzo Dev <[email protected]>
The document preferences form validated its date format with
ZDocumentMetaTimezoneSchema, a bare string, while its type and the settings
routes it submits to say ZDocumentMetaDateFormatSchema, the list of formats.
@hookform/resolvers 3 never compared the two; 5 types the submit handler by
what the schema returns, and a string is not a date format.
The field now validates with the date format schema. The select only offers
DATE_FORMATS and the server has rejected anything else since the routes
were written, so a value the form sends is never newly refused; what
changes is that a bad value stops in the form instead of at the server.
date-formats.test.ts holds that: every format the select offers and the
stored default are in the list the schema is made of, and a time zone is
not.
Co-authored-by: Hanzo Dev <[email protected]>
useEditorFields keeps the envelope editor's fields in a react-hook-form
field array. Under @hookform/resolvers 5 its zod resolver types the form as
the schema's input, where field meta may be `{}`, while every function the
hook returns takes and gives parsed fields. Spelled as the output, the
resolver does not fit the form; left to the resolver, the fields do not fit
the functions.
The resolver never ran. The form is private to the hook, which only reads
it with getValues, rewrites it with reset and edits it through the field
array. Nothing calls handleSubmit or trigger or reads formState; with the
default onSubmit mode a field array validates only after a submit, and
validity is computed only when something subscribes to isValid. So the form
drops the resolver and is typed as what it holds: parsed fields, which is
what the defaults and every write already put in it.
Co-authored-by: Hanzo Dev <[email protected]>
The same @hookform/resolvers 5 change, in the dialogs, settings forms, the
envelope editor and embedded authoring: `useForm<T>` pinned a form to its
schema's output, the resolver no longer matches that, so each form lets the
resolver type it, and whatever takes its control or context says input and
output — Control<Input, unknown, Output>, UseFormReturn likewise.
What the input exposed:
- zod 4 types z.preprocess's input as unknown unless the function names it.
The distribute dialog's and the settings dialog's emailReplyTo hold a
string or nothing, and now say so, as the two flow steps do.
- Combobox and ColorPicker already render an unset value as their
placeholder and default; their props now allow one.
- The embed's signature types and the editor's signers' actionAuth may be
unset in the form. The multiselect reads `?? []` and the signer check
`?? []`, the schemas' own defaults; every writer sets both, so nothing
reachable changes.
Co-authored-by: Hanzo Dev <[email protected]>
@hookform/resolvers 5 types a zod resolver as input in, output out: the
form holds z.input of its schema, and only handleSubmit sees z.output. The
flows spelled their forms as the output and dropped the input on the floor,
where a default or a transform makes the two differ: signers' actionAuth and
distributionMethod have defaults, and field meta takes `{}` and makes it
undefined.
Each flow's schema now exports its input beside its output, and whatever
holds or passes on the form's values uses it: the clipboard and last-active
field, the field item, its content and tooltip (which re-parse the meta
anyway), the email checkboxes, and every autosave. Autosave reads
getValues(), which is what the user typed, not what the schema made of it,
so it is typed as that all the way to the mutation, whose request type is
now z.input of the request schema — what the server parses.
Three reads of form state now apply the schema's own default where the
value may be unset: `actionAuth ?? []` in both signer steps and
`distributionMethod ?? EMAIL` in the subject step. Every writer in those
forms sets the value, so nothing reachable changes. Two `value &&
onChange(value)` statements become ifs, which typescript-eslint 8 asks for.
Co-authored-by: Hanzo Dev <[email protected]>
The field editor's shared inputs — font size, alignment, line height, label,
required, read-only — took a `Control<any>` so one component could serve
every field form. react-hook-form 7.88 types a control's `validate` option
by the form's field names, and a function over "fontSize" | "textAlign" is
not a function over any string, so no form's control was a `Control<any>`
any more: 26 errors, one per shared input per form.
The inputs now read the form from the <Form> they render inside, as
Controller does when it is not handed a control. Every one of them already
did through FormMessage, and every form renders them inside its own <Form>,
so the control each one binds to is the one it was handed before. The
`Control<any>`, its eslint-disable and the formControl prop go.
Two forms spelled their values as the schema's output, `useForm<T>`. Under
@hookform/resolvers 5 a zod resolver takes the schema's input and returns
its output, so they now let the resolver type them. zod 4 types a coerced
number's input as unknown; the field meta and the checkbox form say number,
the type these forms were written against, which changes no parse.
Co-authored-by: Hanzo Dev <[email protected]>
react-hook-form 7.55 gave a form a third type, the values its resolver
hands to submit, and Controller's props carry it. FormField still took two
and passed ControllerProps<Values, Name>, which fixes the third to Values.
Any form whose resolver returns anything else could not hand its control to
a FormField: 97 errors in the app's typecheck and 42 in packages/ui, most of
them one per field of every such form.
FormField takes the third parameter and passes it on, as Controller does.
Co-authored-by: Hanzo Dev <[email protected]>
eslint-config-next went from 15 to 16 in the version sweep and took
eslint-plugin-react-hooks from 5 to 7. Next spreads the plugin's
recommended set, and 7's set is the two rules of hooks plus twelve React
Compiler checks at error. Nothing in the lint config asked for them; they
arrived with the bump and failed 39 places in packages/lib and packages/ui
that were written before the compiler, 24 of them setState in an effect and
11 a ref read during render.
Making those pass means reworking when components render, and nothing here
can test that: there is no DOM in the test runners. So the compiler's checks
report as warnings — listed on every run, blocking nothing — and the rules
of hooks stay errors. react-hooks.test.cjs resolves the shipped config for a
component file and holds both; without the new entry it fails on
set-state-in-effect.
Co-authored-by: Hanzo Dev <[email protected]>
turbo went from 1 to 2 in the version sweep and turbo.json still said
`pipeline`, which turbo 2 refuses before it runs anything: `npm run lint`
and `npm run build` both stopped at "Found `pipeline` field instead of
`tasks`". The key is renamed; nothing under it changes.
Co-authored-by: Hanzo Dev <[email protected]>
react-table went from 8 to 9 with nothing migrated. 9 has no useReactTable
and no getCoreRowModel, so the one component every list in the app renders
through could not be imported, and ColumnDef takes the table's features as
its first type argument, so all 42 column lists stopped typing their rows:
217 of the 498 errors in the app's typecheck.
DataTable now names the four features it uses — paging, row selection,
column visibility, column sizing — and calls useTable. DataTableColumnDef is
ColumnDef over those features, so a column list still says only what a row
is. The second type parameter goes: 9's table takes columns of unknown
value, and no caller ever passed one.
Two things the types turned up. `enableSorting: false` sat on two select
columns of a table that has never sorted; without the sorting feature the
option does not exist, and neither does the sorting. The organisation
insights table erased its three row types through `as unknown as` to feed
one DataTable; it renders one per view, typed, and the casts are gone. The
server rejects any other view before the component renders.
data-table.test.tsx renders the table and drives the instance: headers and
cells, a hidden column, a sized one, one-based paging read from the props
and reported back, selection by the caller's row id. Against the old file
it fails 7 of 7 at the import.
Co-authored-by: Hanzo Dev <[email protected]>
patches/ held one file: a retyping of @ai-sdk/google-vertex 3.0.81 from the
V2 provider interfaces to V3, from when the provider package had moved and
the vertex package had not. What is installed now is 5.0.86, which imports
ProviderV4 and LanguageModelV4 itself, so the patch named a file that no
longer exists in that shape and every install ended in "Failed to apply
patch" — printed, and ignored.
With nothing left to apply, patch-package and its postinstall go too.
Co-authored-by: Hanzo Dev <[email protected]>
Spellings the new majors removed, each fatal at import rather than at a call
site, so the modules that held them could not load:
- `Schema.Enum` — zod 4 dropped the capitalised alias. `.enum` is the
property in both majors.
- `z.string().ip()` — the address formats left `z.string()`; an IP is the
union of `z.ipv4()` and `z.ipv6()`, which is what it always meant.
- `invalid_type_error` — the per-cause message params folded into `error`.
- `@noble/ciphers/chacha` — noble 2 exports a subpath only with its
extension. `@noble/hashes/sha256` is `sha2.js` now. The `.js` form resolves
under the old major too, so every noble import takes it.
Two test files in packages/prisma could not load because of these; they run.
Also drops an assignment nothing reads, in the job runner's failure branch:
ESLint 10 moved no-useless-assignment into its recommended set.
Co-authored-by: Hanzo Dev <[email protected]>
The client and CLI were raised to Prisma 7 without the migration that major
requires, so `prisma generate` stopped at P1012 and nothing downstream of it
could build.
Prisma 7 takes the connection URL out of the schema and has no query engine:
the CLI reads prisma.config.ts, and the client is handed a driver adapter.
Both read the one DATABASE_URL. The adapter holds a single connection, which
is the single writer SQLite wants by construction — the `?connection_limit=1`
suffix the old engine needed is gone, and had to be: the adapter strips
`file:` and takes the rest as the file's name.
THE TRAP IS THE ADAPTER'S DEFAULT. The engine this replaces wrote every
DateTime as an epoch-millisecond INTEGER; that is what a deployed sign.db
holds and what sqlite-sql.ts divides by 1000 to read. The better-sqlite3
adapter defaults to ISO-8601 TEXT. Left alone it would write strings into
columns full of integers, SQLite would order them by storage class rather
than by time, and nothing would throw. `timestampFormat: 'unixepoch-ms'`
keeps one representation, and timestamp.test.ts reads the column's storage
class over a second connection. Checked both ways: on the default it fails
with `createdAt is stored as text: 2026-03-04T05:06:07.890+00:00`.
packages/prisma: 52 of 52.
Co-authored-by: Hanzo Dev <[email protected]>
ESLint was raised to 10, which reads eslint.config.* and nothing else. This
repo still said it all in .eslintrc.cjs and .eslintignore, so every lint run
ended at "couldn't find an eslint.config file" — and the pre-commit hook runs
eslint over staged sources, so no commit touching a .ts file could be made.
The config is the same config in the flat spelling: every rule, every ignore
and the __tests__ override carried over, and the two legacy files are gone
rather than left beside it to disagree.
The transaction rule (inside $transaction, only `tx`) keeps both its callers.
transaction.cjs is a whole flat config, so `lint:tx` still hands it to eslint
alone — with --no-config-lookup, since --no-eslintrc and --ext no longer
exist — and the shared config spreads it in. Its test now gives the linter
the array AS SHIPPED instead of re-stating the parser beside a copy of the
rules: a copy keeps passing after the original stops selecting files. 10 of
10, and `lint:tx` is clean over packages and apps.
eslint-plugin-react finds the React version by asking the rule context for a
filename through an API ESLint 10 removed, so detection threw on the first
file. The version is stated, which never takes that path.
Co-authored-by: Hanzo Dev <[email protected]>
Three majors were raised without what each one requires, and the manifests
are where that starts.
typescript is 6.0.3 for the tools that import it, and the native 7.0.2
compiler comes in as `tsgo` and does the typechecking. typescript@7 has no JS
API at all, and prisma-kysely and typescript-eslint both import `typescript`
by name and call into it — so with 7 under that name, neither could run.
ESLint 10 reads flat config, which is built from @eslint/js, globals and
typescript-eslint; the shared config package declares them. Prisma 7 has no
query engine, so the prisma package declares the better-sqlite3 driver
adapter, and its `prisma` block goes: the CLI reads prisma.config.ts now.
Co-authored-by: Hanzo Dev <[email protected]>
Moved the go directive to 1.27.1 and verified the module with go build,
go vet and the full test suite, all clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
The CLI was declared ^8.0.0-rc.12 while @prisma/client is ^7.10.0, so the
installed CLI answered "No command registered for 'generate'" and the client was
never generated. 1041 of this repo's typecheck errors are @prisma/client having
no exports to import.
This aligns the majors and nothing more. Generate still refuses, now with a
reason worth reading: the schema keeps url = env("DATABASE_URL") in its
datasource and index.ts constructs PrismaClient with datasourceUrl, both of
which Prisma 7 replaced with prisma.config.ts and a constructor adapter (P1012).
That migration is real work on how this service reaches its database, and it is
not a dependency pin.
Co-authored-by: Hanzo Dev <[email protected]>
npm refused to install at all: three entries in the overrides block
contradicted the range the same package was declared with, and an
override that disagrees with a direct dependency is an EOVERRIDE, not a
tie-break. All three were left behind by "Give every package one
version", which raised the declared ranges and never revisited the pins
below them.
zod already showed the way, so ws, pdfjs-dist and typescript now use the
same $name form and follow whatever the manifests declare. The reason
pdfjs-dist was held at 5.4.296 was react-pdf, which this repo no longer
depends on; react-pdf itself now asks for the 6.3.289 we declare.
The same commit pinned @hanzo/insights-node to 6.0.0, which nobody can
install: it depends on insights-node 5.10.4, an unscoped name published
to no registry. Back to 5.26.2, the newest release that resolves, until
a 6.x ships with its scope intact.
Co-authored-by: Hanzo Dev <[email protected]>
The purpose-prefixed labels (hanzo-build-*, hanzo-deploy-*, hanzo-docs-build-*)
are retired in favour of the bare <os>-<arch> every runner already advertises.
They were not a capability, only a habit, and the habit cost builds: papers asked
for hanzo-build-linux-amd64, no online runner offered it, and two idle
linux-amd64 runners sat beside the queue while the build simply never ran. The
lux tree lost three days to lux-linux-amd64 the same way.
What a job is FOR is the workflow's name. runs-on is the machine it needs.
runs-on was a bare scalar. Jobs written that way queue and are never
picked up — link and quest have waited since 10 August, painter since the
5th, none of them with a single success — while repos writing the same
label as a sequence drain within seconds of a push on the same fleet.
Same label, same runners, different shape. It fails silently: nothing errors,
the job simply never starts, so the lane looks configured and does nothing.
certclaims flagged five lines on the compliance page and they were three
different problems:
A CLAIM. The status table and its callout both said SOC 2 was "Certification
scoped per engagement" — a certificate, conditioned. They now say compliant with
the Type II control set, which is what the section below already described:
encryption in transit and at rest, RBAC, audit logging.
TWO HEDGES. ISO 27001 and HIPAA were "Planned". A hedge reads as a claim to
anyone skimming and dates itself the day it ships. They now say controls aligned,
with a BAA on request for HIPAA — the thing a buyer can actually act on.
A FALSE POSITIVE, fixed without weakening the rule. The definition of SOC 2
tripped on "American Institute of Certified Public Accountants" — the auditor
body's own name, beside the framework name. The acronym says the same thing, so
the sentence reads "established by the AICPA" and the gate stays as strict as it
was. Narrowing the rule to let this through would have let real claims through
too.
And the sentence this shares with bootnode: "Formal certification — SOC 2 Type
II, ISO 27001, HIPAA with a BAA — is scoped per enterprise engagement" now offers
what is on the table instead of a certificate that is not.
Nothing about the security posture changed. Only the sentences describing it.
Co-authored-by: Hanzo Dev <[email protected]>
The ISO 27001 and HIPAA rows on this page say Planned, which is true. The
SOC 2 row said Compliant and the section callout linked that word to the
trust page, so a reader took it as a report that exists. It does not — no
auditor, no attestation, no Type II period. The row and the callout now say
what a buyer can act on, and the section states the controls actually in
place and where formal certification gets decided.
upload-artifact@v4 carries @actions/artifact v2, which refuses any host it does
not recognise as github.com: GHESNotSupportedError. It failed the job by itself,
so a passing suite would still have reported red.
deploy.yml, docker-deploy.yml and publish.yml were identical: one job echoing
'native pipeline is .hanzo/workflows/deploy.yml'. They reported success for
doing nothing, which reads greener than having no workflow at all.
e2e-tests.yml was the only file there doing work -- services up, prisma migrate
and seed, then the suite. Its label was warp-ubuntu-2204-x64-8x, a WarpBuild
runner this fleet does not have, and an unserved label queues instead of
failing. .github/actions/{node-install,playwright-install} stay put and still
resolve.
The completion page a recipient saw after signing offered to "claim your
account" with a name, email and password. That is the same local password
account /signup used to create, and it could not sign in either: /signin
offers the identity provider and nothing else.
Removing it also restores the build. The panel imported signupErrorMessages
from the signup form deleted in the previous commit, so the app failed to
typecheck with TS2307 on that module.
The completion page keeps the single-column layout it already rendered for
recipients who had accounts; the conditional two-column variant existed only
to make room for this panel.
Co-authored-by: Hanzo Dev <[email protected]>
/signup rendered a local name/email/password form. The account it produced
could not then sign in: /signin offers the identity provider and nothing else,
so a locally created password had nowhere to be typed. /signup now leads to
the same door as /signin, carrying returnTo through, and the provider offers
account creation on that page.
Drops the form, its bcrypt round trip, and the two end-to-end tests that drove
the local password flow — they covered a path the app no longer offers. The
NEXT_PUBLIC_DISABLE_SIGNUP switch goes with them: there is one way in, so
there is nothing left to switch off.
Co-authored-by: Hanzo Dev <[email protected]>
Adds a notify transport, selected with NEXT_PRIVATE_SMTP_TRANSPORT=notify.
Notify keeps the provider credentials in KMS and picks the provider per org,
so nothing here carries a secret and nothing here names a sender — the from
address belongs to the credential. It authenticates with the same IAM client
that already signs users in, presented as a machine token and cached until
just before expiry.
A refused delivery arrives as HTTP 200 with status "failed", so the transport
reads the outcome in the body rather than the status line; otherwise a send
that never left would be reported as sent.
Also spells the default SMTP host without a port. "127.0.0.1:2500" is not a
hostname, so DNS rejected the name and an unconfigured deployment failed as a
name-lookup error rather than a refused connection.
Co-authored-by: Hanzo Dev <[email protected]>
ESLint normalises a scoped shareable config `@scope/name` to
`@scope/eslint-config-name`. When this fork renamed `@documenso/eslint-config`
to `@hanzo/esign-eslint-config`, that normalisation started looking for
`@hanzo/eslint-config-esign-eslint-config`, which does not exist — so every
`eslint` invocation, including the lint-staged pre-commit hook, died with
"couldn't find the config to extend from" instead of linting. The gate has
been inert since the rename.
Referencing the config by resolved path keeps the package name matching the
`@hanzo/esign-*` convention and skips the normalisation entirely.
Lint runs again and reports 16 pre-existing errors across apps/remix,
packages/lib and packages/trpc (promise-function-async, ban-types,
require-await, undeclared turbo env vars) that were invisible while it was
broken. They are left for their owners; nothing in this repo's CI runs lint,
and lint-staged only checks files a commit touches.
Radix was leaking out of the primitive layer into application code. Thirty
dialog components under apps/remix imported `@radix-ui/react-dialog` purely to
spell `Omit<DialogPrimitive.DialogProps, 'children'>`, and two more imported
`TooltipArrow` directly. Each of those is a place the UI substrate has to be
swapped when it changes, for no gain — the app never touched Radix behaviour,
only a type and one arrow element.
`packages/ui/primitives/dialog.tsx` now exports `DialogProps` and
`tooltip.tsx` exports `TooltipArrow`, so callers name the primitive, not the
library behind it. Radix imports outside packages/ui: 32 -> 0.
Eleven vendored shadcn primitives had no importers at all — aspect-ratio,
auto-sized-text, calendar, collapsible, context-menu, menubar,
navigation-menu, slider, toggle, a stray document-flow dialog and a
signature-pad Canvas class shadowed by skia-canvas. Dead component code reads
as supported API in review, so it is deleted rather than carried. That
retires eight Radix packages plus react-day-picker from packages/ui and
@radix-ui/react-tabs from apps/docs, which was declared and never imported:
28 declared Radix packages -> 19.
The embedding doc taught Tailwind classes in a snippet third parties copy into
their own apps; it now uses the same inline styles as the plain-HTML example
directly above it. `.cursorrules` told every AI touching this repo to write
"Shadcn UI, Radix, and Tailwind Aria" — it now points at @hanzo/gui and says
the inherited layer is to be replaced, not extended.
Verified: apps/remix typecheck 0 errors, client+server build green,
prisma 38/38.
github.com has no runner for hanzo-build-linux-amd64, so a caller under
.github/workflows is a gate that can never be scheduled. This is the ~7-line
caller on the plane that can (git.hanzo.ai git-runner fleet), pinned @v1.
Co-authored-by: Hanzo Dev <[email protected]>
1fa47d4 ended with
Error while stop job container: Cannot connect to the Docker daemon at
unix:///var/run/docker.sock
which is the git-runner fleet ending the job, not this repo failing: a rolling
restart stops act_runner and dockerd concurrently (s6 siblings), so the job
container disappears underneath a build that was fine.
Nothing here changed. This re-runs the server/* path mapping on a stable fleet
so the typecheck fix actually gets exercised.
Co-authored-by: Hanzo Dev <[email protected]>
`turbo run build --filter=@hanzo/esign-remix` fails its typecheck with
error TS2307: Cannot find module 'server/utils/get-loader-session'
error TS2307: Cannot find module 'server/context'
error TS2307: Cannot find module 'server/router'
All three files exist — apps/remix/server/context.ts, server/router.ts,
server/utils/get-loader-session.ts. The problem is that they are imported as
NON-RELATIVE specifiers, and a bare specifier has no root to resolve against
unless it is named in `paths` or `baseUrl` is set. Neither was true, so tsc
looked for an npm package called `server`.
Added `"server/*": ["./server/*"]` rather than setting baseUrl. baseUrl would
change resolution for every bare specifier in the project, letting any
top-level directory shadow a package of the same name; this maps exactly the
one prefix that needs it. paths-without-baseUrl already works in this config —
the existing `~/*` entry relies on it.
Verified by resolving each specifier through the tsconfig's own path rules:
all three land on the real files. A full tsc run is not possible in a bare
clone (no node_modules — it stops earlier on missing @types/node), so CI is
what exercises the whole typecheck.
Co-authored-by: Hanzo Dev <[email protected]>
The first pass left three classes of config that BOTH tsc 5.9 and tsc 7
reject. Each was proven against both compilers before changing:
TS5090 A `paths` target must be relative once `baseUrl` is gone. The
first pass skipped the "./" prefix wherever baseUrl pointed at
the config's own directory, reasoning it was semantically
equivalent. It is not — without baseUrl a non-relative target
is rejected outright, by 5.9 as well as 7.
TS5110 `moduleResolution: node16` requires `module: node16`. The first
pass mapped commonjs projects to node16 resolution alone, which
BROKE those configs for the current toolchain. Both are now set.
TS5102 `downlevelIteration` is also removed in TS7; it was missing from
the dead-flag list.
Verified: repos that tsc 7 previously refused (base-studio, js-sdk, kv-js)
now report zero config errors on tsc 7 AND tsc 5.9.
Co-authored-by: Hanzo Dev <[email protected]>
TypeScript 7 is the native Go compiler and removes `baseUrl` and
`moduleResolution: node|node10`. Both appear here, so `tsc` from TS7
refuses the config outright (TS5102 / TS5108) and cannot typecheck.
`paths` targets resolve relative to `baseUrl` when it is set and relative
to the tsconfig file otherwise. Every `baseUrl` folded here already
pointed at the config's own directory, so dropping it moves nothing and
the targets are left byte-identical. Where a baseUrl pointed elsewhere,
each affected target was rewritten as join(baseUrl, target).
`moduleResolution` was chosen from the declared `module`: commonjs ->
node16, esnext/preserve -> bundler. Configs whose `module` is unset or
exotic were left alone rather than guessed at.
The result is accepted by BOTH toolchains, so nothing has to upgrade
TypeScript in lockstep. Verified on hanzo/chat packages/api: tsc 5.9
779 -> 778 errors (no regression), and tsc 7.0.2 now runs the project
in 2s where it previously refused the config.
Co-authored-by: Hanzo Dev <[email protected]>
The previous run's log ends mid `@hanzo/esign-remix:build` with no error and
no job-failed marker — the truncated signature of a killed container, not a
build defect. It ran at 19:35, before universe 62c92881c bounded the runner
build cache and its emptyDir; at that point two runners shared a node with
27G free and were still growing.
The husky line in that log is a red herring: `prepare` is
`husky && husky install || true` and the very next stage logs
`HUSKY=0 skip install`. Nothing there is fatal.
Empty commit to get a clean signal on healthy runners.
Co-authored-by: Hanzo Dev <[email protected]>
Pushed WITHOUT a manual mirror-sync. Triggering POST /mirror-sync advances the
forge ref but does not raise a push event, and the scheduled sync then finds
nothing new — so the commit lands and CI never runs on it. Confirmed twice
today: refs SYNCED, zero runs.
Co-authored-by: Hanzo Dev <[email protected]>
Superseded: the Hanzo GitHub App pushes a webhook, so the forge tracks GitHub
without a per-repo workflow. This file called git.hanzo.ai/api/v1/.../mirror-sync
— a Gitea API for a system we no longer drive — and would sit inert in every repo.
One mechanism, in one place, instead of ~350 copies of a cron.
The old pipeline called buildctl-daemonless.sh (exit 127, not in the job image)
and read secrets.GIT_CLONE_TOKEN, which exists nowhere. It had never produced
an image, and with ARC decommissioned it was this repo's only CI.
Target picked from evidence, not from guessing among five Dockerfiles:
docker/Dockerfile is the multi-stage node:22-alpine app build that COPYs from
the repo root, and the cluster runs ghcr.io/hanzoai/sign:0.1.0.
The image is `sign`, not `esign` — the GitHub repo was renamed (hanzoai/sign
307s to hanzoai/esign) but the published image name did not follow.
git.hanzo.ai mirrors this repo by PULL on a ~10-minute interval, and arcd runs
CI/CD there — so every push waited out that interval before anything built.
This asks Gitea to pull HEAD immediately.
Latency only: the repo already mirrors via the App webhook, so a missing
HANZO_GIT_TOKEN or a failed curl is non-fatal and never fails the push.
Idempotent (mirror-sync just pulls HEAD) and concurrency-coalesced.
This workflow ran on a self-hosted actions-runner-controller pool. ARC is
being decommissioned: CI executes inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.
Removed rather than repointed — there is no GitHub-side pool to move to, and
leaving it would keep ARC load-bearing.
This workflow ran on a self-hosted actions-runner-controller pool. ARC is
being decommissioned: CI executes inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.
Removed rather than repointed — there is no GitHub-side pool to move to, and
leaving it would keep ARC load-bearing.
This workflow ran on a self-hosted actions-runner-controller pool. ARC is
being decommissioned: CI executes inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.
Removed rather than repointed — there is no GitHub-side pool to move to, and
leaving it would keep ARC load-bearing.
This workflow ran on a self-hosted actions-runner-controller pool. ARC is
being decommissioned: CI executes inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.
Removed rather than repointed — there is no GitHub-side pool to move to, and
leaving it would keep ARC load-bearing.
This workflow ran on a self-hosted actions-runner-controller pool. ARC is
being decommissioned: CI executes inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.
Removed rather than repointed — there is no GitHub-side pool to move to, and
leaving it would keep ARC load-bearing.
This workflow ran on a self-hosted actions-runner-controller pool. ARC is
being decommissioned: CI executes inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.
Removed rather than repointed — there is no GitHub-side pool to move to, and
leaving it would keep ARC load-bearing.
This workflow ran on a self-hosted actions-runner-controller pool. ARC is
being decommissioned: CI executes inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.
Removed rather than repointed — there is no GitHub-side pool to move to, and
leaving it would keep ARC load-bearing.
This workflow ran on a self-hosted actions-runner-controller pool. ARC is
being decommissioned: CI executes inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.
Removed rather than repointed — there is no GitHub-side pool to move to, and
leaving it would keep ARC load-bearing.
This workflow ran on a self-hosted actions-runner-controller pool. ARC is
being decommissioned: CI executes inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.
Removed rather than repointed — there is no GitHub-side pool to move to, and
leaving it would keep ARC load-bearing.
This workflow ran on a self-hosted actions-runner-controller pool. ARC is
being decommissioned: CI executes inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.
Removed rather than repointed — there is no GitHub-side pool to move to, and
leaving it would keep ARC load-bearing.