# hlix documentation # This path left the work tree The requested hlix documentation page does not exist. Start from the [documentation home](/), check the [CLI command reference](/reference/cli/), or use site search to find the current guide. --- # API & OpenAPI Use hlix's OpenAPI 3.1 contract directly or generate a client with open-source tooling. **The contract is the machine-readable description of every published hlix API operation.** It is OpenAPI 3.1, generated from the same Zod validators the server enforces at runtime plus explicit response schemas — so a shape it describes is a shape the API actually validates. ## Prerequisites - Nothing to download the contract; the endpoint is unauthenticated - An hlix API key and a workspace ID to call any operation - For code generation: OpenAPI Generator `7.22.0`, or the generator of your choice ## Download the contract The contract endpoint does not require authentication: ```bash curl --fail --silent --show-error \ https://server.hlix.ai/v1/api/openapi.json \ --output hlix-openapi.json ``` Verify it before code generation: ```bash node -e "const d=require('./hlix-openapi.json'); if(d.openapi!=='3.1.0') process.exit(1); console.log(d.info.version)" ``` Expected result: OpenAPI `3.1.0` and the current contract version. ## Call the API Authenticated operations use two headers: - `x-api-key`: authenticates the user - `X-Organization-Id`: selects the workspace ```bash curl --fail --silent --show-error \ --header "x-api-key: $HLIX_API_KEY" \ --header "X-Organization-Id: $HLIX_WORKSPACE_ID" \ https://server.hlix.ai/v1/api/projects ``` Do not put credentials in the URL or command itself. Environment expansion still makes the value available to the process, so use your shell and CI secret controls appropriately. ## How the contract is produced The contract follows one reviewable chain: 1. Hono routes validate requests with exported Zod schemas. 2. The public-operation registry selects which routes form the supported API. 3. The document builder converts route syntax, attaches request/response schemas, security, and API metadata. 4. `apps/backend/openapi.json` is emitted and checked in so contract changes have a visible diff. 5. `openapi-typescript` generates the private transport schema used by `@hlix/api-client`. 6. `@hlix/sdk` reads its public method types directly from those generated paths. Steps 1–4 happen inside hlix; steps 5–6 are what you can reproduce, against the contract you downloaded above. Never hand-edit a generated client to "fix" a mismatch — regenerate it, and if the contract itself is wrong, report it. ## Generate another language The OpenAPI document is the portable integration point. You do not need a commercial code-generation service to start. Common open-source choices include: - [OpenAPI Generator](https://openapi-generator.tech/) for broad language coverage - [Kiota](https://learn.microsoft.com/openapi/kiota/) for strongly typed clients across several Microsoft-supported languages - language-focused generators when you need a smaller, idiomatic surface Every command operates on the contract file you downloaded — nothing here needs access to hlix's source: ```bash npx @openapitools/openapi-generator-cli generate \ -i hlix-openapi.json \ -g python \ -o ./hlix-client ``` Expected result: a client package whose operations match the [API reference](/api/reference/) one for one. Pin the generator version so a regeneration is reproducible; hlix's own previews use OpenAPI Generator `7.22.0`. Example with OpenAPI Generator: ```bash docker run --rm \ --volume "$PWD:/local" \ openapitools/openapi-generator-cli generate \ --input-spec /local/hlix-openapi.json \ --generator-name python \ --output /local/generated/hlix-python ``` ## Compatibility The API contract version is independent of the CLI, SDK, and platform release numbers: - major: a previous client can break, paired with a new URL prefix; - minor: additive route or field changes; - patch: documentation-only contract corrections. The current URL prefix remains `/v1/api`. A package version bump does not automatically bump the API version. ## If a request or a generation fails - `401`: the API key is missing or invalid. - `403`: the authenticated user cannot perform that action in the selected workspace. - `404`: the resource is absent **or not visible**; do not infer cross-tenant existence. - `409`: expected revision/state is stale or another operation currently owns the transition. - A generated field becomes `unknown`: the published response schema may not pin that body yet; do not invent a stronger local type. - Local generated diff after no intended contract change: regenerate from a clean checkout and investigate registry/schema drift. ## Next steps --- # API reference Every operation in the published hlix API contract, grouped by resource and generated from the contract itself. **This is the endpoint-level reference for the hlix API: 90 operations across 8 resources.** Every page under it is generated from `openapi.json` at build time, so it cannot describe an operation the contract does not publish. Contract version `1.0.0`, OpenAPI 3.1. To download the document itself or generate a client from it, see [API & OpenAPI](/api/openapi/). ## Prerequisites - An hlix API key — create one in Developer tools → API keys - The ID of a workspace you can act in ```bash export HLIX_API_KEY='…' export HLIX_WORKSPACE_ID='' export HLIX_BASE_URL='https://server.hlix.ai' ``` ## Resources | Resource | Operations | What it covers | | --- | --- | --- | | [Projects](/api/reference/projects/) | 28 | Projects, their repositories, roadmap, artifacts and Coding Workspace. | | [Tasks](/api/reference/tasks/) | 9 | Standalone tasks and their review evidence. | | [Cycles](/api/reference/cycles/) | 22 | Multi-task units of work and their conversations. | | [Imports](/api/reference/imports/) | 7 | Secure project snapshot upload, verification and finalization. | | [Reports](/api/reference/reports/) | 5 | Agency portfolio health and client-safe reporting views. | | [Client invoices](/api/reference/client-invoices/) | 9 | | | [Client billing profile](/api/reference/client-billing-profile/) | 4 | | | [Revisions](/api/reference/revisions/) | 6 | | ## Authentication Every operation takes the same two headers: ```bash curl -sS "$HLIX_BASE_URL/v1/api/projects" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` `x-api-key` authenticates a **user**; it names no workspace. `X-Organization-Id` selects the tenant that user acts in. A browser client sends its session cookie instead of the key, and may omit the header when the session already has an active organization — but server-side automation should always pass it explicitly, so a prior session cannot decide the tenant for you. ## What is not here The contract publishes the operations above and nothing else. Several surfaces the product exposes are **not** in it, and therefore have no generated client method: - **Memories** (`/v1/api/memories`) — use the dashboard or a direct HTTP call. See [Context, memory & DNA](/concepts/context-memory/). - **Model and harness configuration** (`/v1/api/model-config`, `/v1/api/providers`) — see [Connect your agents](/guides/connect-agents/). - **Logs** (`/v1/api/logs`) — there is no log stream, only plain reads. See [Tasks](/running/tasks/#watching-a-task). - **Audit log**, **pending requests**, **environments**, **packs**, **secrets**, and the live agent stream/steer endpoints. An operation missing from this reference is missing from the contract, which means the SDKs cannot reach it either. That is a deliberate boundary, not an oversight: the published contract is the part hlix commits to keeping compatible. ## If a request fails - `400` — the body failed schema validation, or the request named no active organization. - `401` — no valid session or API key. Check `x-api-key`. - `403` — authenticated, but not a member of the named workspace. - `404` — no such resource in this workspace, or a resource that was never shared with you. - `409` — a compare-and-swap lost, or another operation owns the transition. Re-read the head and retry. - `429` — too many requests. - `5xx` — the operation failed server-side. Retry only where an idempotency key makes it safe. ## Next steps --- # Client billing profile The client billing profile operations in the hlix API contract. The **client billing profile** holds the agency's own billing identity — the details that appear on an invoice it issues. Generated from contract version `1.0.0` — the same document [`/v1/api/openapi.json`](/api/openapi/) serves. ## Prerequisites - An API key in `x-api-key`, or a browser session cookie - `X-Organization-Id` naming the workspace to act in, unless the session already has an active one - The base URL for your deployment, `https://server.hlix.ai` by default ## Operations | Method | Path | Operation | | --- | --- | --- | | `DELETE` | `/v1/api/client-billing/profile` | [Remove the workspace's billing identity](#remove-the-workspaces-billing-identity) | | `GET` | `/v1/api/client-billing/profile` | [Get the workspace's billing identity](#get-the-workspaces-billing-identity) | | `PUT` | `/v1/api/client-billing/profile` | [Replace the workspace's billing identity](#replace-the-workspaces-billing-identity) | | `GET` | `/v1/api/client-billing/profile/logo` | [Download the workspace's logo](#download-the-workspaces-logo) | ## Remove the workspace's billing identity `DELETE /v1/api/client-billing/profile` · operation ID `deleteAgencyBillingProfile` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | AgencyBillingProfileCleared | Removed. New invoices are issued unbranded; ones already issued keep the identity frozen onto them. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a `member`. Changing the legal entity every customer is billed by is restricted to owners and admins. | | `404` | ApiError | No billing profile is set. | ## Get the workspace's billing identity `GET /v1/api/client-billing/profile` · operation ID `getAgencyBillingProfile` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | AgencyBillingProfileState | The profile, or `null` if none was ever saved. `branded` says whether the workspace's plan puts it on invoices — an unbranded workspace still issues complete, correct documents. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a `client` collaborator. The agency's billing identity is agency-only, reads included. | ## Replace the workspace's billing identity `PUT /v1/api/client-billing/profile` · operation ID `upsertAgencyBillingProfile` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `UpsertBillingProfile` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `address` | string | **yes** | min length 1, max length 1000 | | `brandColor` | string | null | no | | | `contactEmail` | string (email) | null | no | | | `defaultPaymentTerms` | string | null | no | | | `invoiceFooter` | string | null | no | | | `legalName` | string | **yes** | min length 1, max length 200 | | `logo` | object | null | no | | | `taxId` | string | null | no | | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | AgencyBillingProfileState | The saved profile. A PUT, not a PATCH: an omitted optional field is CLEARED, which is the only way to remove a logo. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), the request named no active organization, or the logo is not the image format it claims to be — the bytes are decoded, not trusted, and this route reads the file header (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a `member`. Changing the legal entity every customer is billed by is restricted to owners and admins. | ## Download the workspace's logo `GET /v1/api/client-billing/profile/logo` · operation ID `getAgencyBillingProfileLogo` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | string (binary) | The stored image. The `Content-Type` is the format recorded in the database, not one the uploader asserted, and `X-Content-Type-Options: nosniff` accompanies it. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a `client` collaborator. The agency's billing identity is agency-only, reads included. | | `404` | ApiError | No logo is set for this workspace. | ## If a request fails These apply to every operation on this page. - `400` — the body failed schema validation, or the request named no active organization. A `ValidationError` body carries the failing fields; an `ApiError` body carries only `error`. - `401` — no valid session or API key. - `403` — authenticated, but not a member of the named organization, or a client collaborator without the required access. - `404` — no such resource **in this workspace**. Absent and not-visible are deliberately the same answer; do not infer cross-tenant existence from it. - `409` — the expected revision or state is stale, or another operation currently owns the transition. - `500` — the operation failed server-side. Retry only where an idempotency key makes that safe. ## Next steps --- # Client invoices The client invoices operations in the hlix API contract. **Client invoices** are the agency-billing surface: draft, issue, pay, void, and render an invoice for a client account. Generated from contract version `1.0.0` — the same document [`/v1/api/openapi.json`](/api/openapi/) serves. ## Prerequisites - An API key in `x-api-key`, or a browser session cookie - `X-Organization-Id` naming the workspace to act in, unless the session already has an active one - The base URL for your deployment, `https://server.hlix.ai` by default ## Operations | Method | Path | Operation | | --- | --- | --- | | `GET` | `/v1/api/client-billing/invoices` | [List invoices](#list-invoices) | | `POST` | `/v1/api/client-billing/invoices` | [Assemble a draft invoice from billables](#assemble-a-draft-invoice-from-billables) | | `GET` | `/v1/api/client-billing/invoices/{id}` | [Get one invoice with its lines](#get-one-invoice-with-its-lines) | | `POST` | `/v1/api/client-billing/invoices/{id}/issue` | [Issue a draft, assigning its number](#issue-a-draft-assigning-its-number) | | `GET` | `/v1/api/client-billing/invoices/{id}/logo` | [Download the logo frozen onto this invoice](#download-the-logo-frozen-onto-this-invoice) | | `POST` | `/v1/api/client-billing/invoices/{id}/pay` | [Record an out-of-band settlement (bank transfer, cheque)](#record-an-out-of-band-settlement-bank-transfer-cheque) | | `POST` | `/v1/api/client-billing/invoices/{id}/payment-link` | [Open a provider-hosted checkout for an issued invoice](#open-a-provider-hosted-checkout-for-an-issued-invoice) | | `GET` | `/v1/api/client-billing/invoices/{id}/pdf` | [Download the invoice as a PDF](#download-the-invoice-as-a-pdf) | | `POST` | `/v1/api/client-billing/invoices/{id}/void` | [Withdraw an invoice, or discard a draft](#withdraw-an-invoice-or-discard-a-draft) | ## List invoices `GET /v1/api/client-billing/invoices` · operation ID `listClientInvoices` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `clientAccountId` | query | no | string | Only this customer's invoices. | | `status` | query | no | string | One of `draft`, `final`, `void`, `paid`. | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ClientInvoice[] | Invoice headers, newest first. `lines` is empty on this projection — read one invoice to get them. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a `client` collaborator. Invoices are agency-only, reads included. | ## Assemble a draft invoice from billables `POST /v1/api/client-billing/invoices` · operation ID `createClientInvoiceDraft` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `CreateInvoiceDraft` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `billableIds` | string[] | **yes** | | | `buyerAddress` | string | **yes** | min length 1, max length 500 | | `buyerBillingEmail` | string (email) | null | no | | | `buyerLegalName` | string | no | min length 1, max length 200 | | `dueAt` | string (date-time) | **yes** | | | `paymentTerms` | string | no | min length 1, max length 200 | | `replacesInvoiceId` | string | null | no | | | `sellerAddress` | string | no | min length 1, max length 500 | | `sellerLegalName` | string | no | min length 1, max length 200 | | `taxRatesBps` | object | no | | **Responses** | Status | Body | Description | | --- | --- | --- | | `201` | ClientInvoice | The draft, with its lines snapshotted from the ledger and its totals computed once and stored. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a `client` collaborator. Invoices are agency-only, reads included. | | `404` | ApiError | A named billable or the client account is not this workspace's. | | `409` | ApiError | The set cannot be invoiced: it spans two customers or two currencies, a charge nets to zero, a tax class has no rate, a correction's charge was never invoiced, or the total would exceed what an invoice can represent. The message names which. | | `422` | ApiError | A database constraint refused the draft. | ## Get one invoice with its lines `GET /v1/api/client-billing/invoices/{id}` · operation ID `getClientInvoice` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ClientInvoice | The invoice and every line it was built from. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a `client` collaborator. Invoices are agency-only, reads included. | | `404` | ApiError | No such invoice in this workspace. Never a 403 — 'forbidden' would confirm that an invoice exists for a customer the caller was never shown. | ## Issue a draft, assigning its number `POST /v1/api/client-billing/invoices/{id}/issue` · operation ID `issueClientInvoice` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `IssueInvoice` (required) The contract does not pin this body's properties. **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ClientInvoice | The issued invoice. `invoiceNumber` and `invoiceSequence` are assigned by the database at this moment and are gapless per workspace. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a `client` collaborator. Invoices are agency-only, reads included. | | `404` | ApiError | No such invoice in this workspace. Never a 403 — 'forbidden' would confirm that an invoice exists for a customer the caller was never shown. | | `409` | ApiError | Not issuable: already issued, no lines, the header does not reconcile with its lines, a charge on it is already invoiced, or a correction of one is missing from it. | | `422` | ApiError | A database constraint refused the issue. | ## Download the logo frozen onto this invoice `GET /v1/api/client-billing/invoices/{id}/logo` · operation ID `getClientInvoiceLogo` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | string (binary) | The agency's logo AS IT WAS AT ISSUE — not the workspace's current one. The `Content-Type` is the format recorded on the invoice, accompanied by `X-Content-Type-Options: nosniff`. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a `client` collaborator. Invoices are agency-only, reads included. | | `404` | ApiError | No such invoice, or it carries no logo. | ## Record an out-of-band settlement (bank transfer, cheque) `POST /v1/api/client-billing/invoices/{id}/pay` · operation ID `payClientInvoice` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `PayInvoice` (required) The contract does not pin this body's properties. **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ClientInvoice | The settled invoice. This is an OUT-OF-BAND settlement — an agency member asserting money arrived somewhere hlix cannot see — recorded under its own audit action so it is never confused with provider-verified payment. Any outstanding hosted checkout is cancelled, so the invoice cannot also be paid through the provider. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a `client` collaborator. Invoices are agency-only, reads included. | | `404` | ApiError | No such invoice in this workspace. Never a 403 — 'forbidden' would confirm that an invoice exists for a customer the caller was never shown. | | `409` | ApiError | Only an issued, unpaid invoice can be settled. | | `422` | ApiError | A database constraint refused the payment. | ## Open a provider-hosted checkout for an issued invoice `POST /v1/api/client-billing/invoices/{id}/payment-link` · operation ID `createClientInvoicePaymentLink` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `PaymentLink` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `successUrl` | string (uri) | no | max length 2000 | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ClientPaymentLink | A PROVIDER-HOSTED checkout URL and the provider's reference for it — and nothing else. No card field, no payment token and no credential of any kind crosses this boundary: the payer enters those on the provider's own page. Calling this does NOT mark the invoice paid. A PROVIDER payment settles the invoice only through the provider's signed webhook, reconciled on amount, currency and invoice id; the one other route to `paid` is `POST /:id/pay`, an audited out-of-band settlement recorded by an agency member under its own audit action. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a `client` collaborator attempting something other than reading or paying its own invoice. | | `404` | ApiError | No such invoice in this workspace. Never a 403 — 'forbidden' would confirm that an invoice exists for a customer the caller was never shown. | | `409` | ApiError | Not payable: the invoice is a draft, void or already paid, it already has a checkout open with a different provider, or its total is above what the provider will take in one payment. | | `501` | ApiError | No payment provider is connected for this deployment. The invoice can still be issued, sent and paid outside hlix. | ## Download the invoice as a PDF `GET /v1/api/client-billing/invoices/{id}/pdf` · operation ID `getClientInvoicePdf` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | string (binary) | The rendered invoice, every line included. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a `client` collaborator. Invoices are agency-only, reads included. | | `404` | ApiError | No such invoice in this workspace. Never a 403 — 'forbidden' would confirm that an invoice exists for a customer the caller was never shown. | | `409` | ApiError | The invoice is a draft, or its stored totals no longer reconcile with its stored lines. Rendering a document that does not add up is refused rather than attempted. | | `415` | ApiError | The invoice is valid but this renderer cannot encode it — a party name or line description outside Latin-1. Retrying will not help; read the invoice through `getClientInvoice` instead. | ## Withdraw an invoice, or discard a draft `POST /v1/api/client-billing/invoices/{id}/void` · operation ID `voidClientInvoice` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `VoidInvoice` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `reason` | string | **yes** | min length 1, max length 500 | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ClientInvoice | The voided invoice. An issued one KEEPS its number — a hole in the sequence reads as a deleted document. Corrections are a replacement invoice naming this one, never an edit. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a `client` collaborator. Invoices are agency-only, reads included. | | `404` | ApiError | No such invoice in this workspace. Never a 403 — 'forbidden' would confirm that an invoice exists for a customer the caller was never shown. | | `409` | ApiError | The invoice is already void. | | `422` | ApiError | A database constraint refused the void. | ## If a request fails These apply to every operation on this page. - `400` — the body failed schema validation, or the request named no active organization. A `ValidationError` body carries the failing fields; an `ApiError` body carries only `error`. - `401` — no valid session or API key. - `403` — authenticated, but not a member of the named organization, or a client collaborator without the required access. - `404` — no such resource **in this workspace**. Absent and not-visible are deliberately the same answer; do not infer cross-tenant existence from it. - `409` — the expected revision or state is stale, or another operation currently owns the transition. - `500` — the operation failed server-side. Retry only where an idempotency key makes that safe. ## Next steps --- # Cycles Multi-task units of work and their conversations. A **cycle** is a multi-task unit of work with its own conversation. These operations create cycles, dispatch them, add tasks to them, and read their events, timeline, usage and artifacts. Generated from contract version `1.0.0` — the same document [`/v1/api/openapi.json`](/api/openapi/) serves. ## Prerequisites - An API key in `x-api-key`, or a browser session cookie - `X-Organization-Id` naming the workspace to act in, unless the session already has an active one - The base URL for your deployment, `https://server.hlix.ai` by default ## Operations | Method | Path | Operation | | --- | --- | --- | | `GET` | `/v1/api/cycles` | [List cycles](#list-cycles) | | `POST` | `/v1/api/cycles` | [Propose a cycle](#propose-a-cycle) | | `DELETE` | `/v1/api/cycles/{id}` | [Delete a draft or failed cycle](#delete-a-draft-or-failed-cycle) | | `GET` | `/v1/api/cycles/{id}` | [Get one cycle with its tasks](#get-one-cycle-with-its-tasks) | | `PATCH` | `/v1/api/cycles/{id}` | [Update a cycle's title, description or target branch](#update-a-cycles-title-description-or-target-branch) | | `GET` | `/v1/api/cycles/{id}/artifacts` | [List a cycle's stored artifacts](#list-a-cycles-stored-artifacts) | | `GET` | `/v1/api/cycles/{id}/artifacts/{filename}` | [Get a signed download URL for one cycle artifact](#get-a-signed-download-url-for-one-cycle-artifact) | | `GET` | `/v1/api/cycles/{id}/diagnostics` | [Read why the cycle's run failed or is parked](#read-why-the-cycles-run-failed-or-is-parked) | | `GET` | `/v1/api/cycles/{id}/events` | [List a cycle's own events](#list-a-cycles-own-events) | | `POST` | `/v1/api/cycles/{id}/execute` | [Run a cycle's lifecycle](#run-a-cycles-lifecycle) | | `GET` | `/v1/api/cycles/{id}/messages` | [Read a cycle's conversation](#read-a-cycles-conversation) | | `POST` | `/v1/api/cycles/{id}/messages` | [Send a message to a cycle's conversation](#send-a-message-to-a-cycles-conversation) | | `POST` | `/v1/api/cycles/{id}/messages/stream` | [Send a message and stream the reply (SSE)](#send-a-message-and-stream-the-reply-sse) | | `GET` | `/v1/api/cycles/{id}/pending-requests` | [List a cycle's pending requests](#list-a-cycles-pending-requests) | | `POST` | `/v1/api/cycles/{id}/stop` | [Stop a running cycle](#stop-a-running-cycle) | | `GET` | `/v1/api/cycles/{id}/stream` | [Stream a cycle's lifecycle events (SSE)](#stream-a-cycles-lifecycle-events-sse) | | `POST` | `/v1/api/cycles/{id}/tasks` | [Add a task to a cycle](#add-a-task-to-a-cycle) | | `DELETE` | `/v1/api/cycles/{id}/tasks/{taskId}` | [Remove a task from a cycle](#remove-a-task-from-a-cycle) | | `PATCH` | `/v1/api/cycles/{id}/tasks/{taskId}` | [Update a task inside a cycle](#update-a-task-inside-a-cycle) | | `GET` | `/v1/api/cycles/{id}/timeline` | [List a cycle's events plus its tasks'](#list-a-cycles-events-plus-its-tasks) | | `GET` | `/v1/api/cycles/{id}/transcript` | [Get post-hoc step transcripts for a cycle's planner + coder turns](#get-post-hoc-step-transcripts-for-a-cycles-planner--coder-turns) | | `GET` | `/v1/api/cycles/{id}/usage` | [Get the cycle's cost and token roll-up](#get-the-cycles-cost-and-token-roll-up) | ## List cycles `GET /v1/api/cycles` · operation ID `listCycles` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `projectId` | query | no | string | Only this project's cycles. | | `orderBy` | query | no | string | `createdAt` (default, newest first) or `priority` (Triage's urgency tier first, ties keep createdAt order). | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | CycleSummary[] | The matching cycles, newest first (or priority-first when `orderBy=priority`), each with a Linear deep link when it came from Linear. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | ## Propose a cycle `POST /v1/api/cycles` · operation ID `proposeCycle` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `CreateCycle` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `description` | string | **yes** | min length 1, max length 10000 | | `projectId` | string (uuid) | **yes** | | | `targetBranch` | string | no | | | `title` | string | **yes** | min length 1, max length 200 | **Responses** | Status | Body | Description | | --- | --- | --- | | `202` | CycleProposed | The proposal was recorded. `adopted` says whether the orchestrator took it straight away; if not, it is adopted on the next planning pass. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Delete a draft or failed cycle `DELETE /v1/api/cycles/{id}` · operation ID `deleteCycle` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | MessageAck | The cycle was deleted. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such cycle in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | | `409` | ApiError | Only a cycle in `draft` or `failed` status can be deleted. | ## Get one cycle with its tasks `GET /v1/api/cycles/{id}` · operation ID `getCycle` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | CycleDetail | The cycle, its tasks with dependencies, and the live preview URL when one is deployed. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such cycle in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Update a cycle's title, description or target branch `PATCH /v1/api/cycles/{id}` · operation ID `updateCycle` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `UpdateCycle` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `description` | string | no | min length 1, max length 10000 | | `targetBranch` | string | no | | | `title` | string | no | min length 1, max length 200 | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | Cycle | The updated cycle. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such cycle in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## List a cycle's stored artifacts `GET /v1/api/cycles/{id}/artifacts` · operation ID `listCycleArtifacts` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | StoredArtifact[] | The stored objects. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such cycle in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Get a signed download URL for one cycle artifact `GET /v1/api/cycles/{id}/artifacts/{filename}` · operation ID `getCycleArtifactUrl` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `filename` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | SignedArtifactUrl | A short-lived signed URL. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such cycle, or no such artifact under it. | ## Read why the cycle's run failed or is parked `GET /v1/api/cycles/{id}/diagnostics` · operation ID `getCycleRunDiagnostics` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | RunDiagnostics | The run's own account of itself: the failure it recorded, its un-collapsed engine status, and every step it is parked on with the labels a resume may address. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such cycle, or it has no run to diagnose. | ## List a cycle's own events `GET /v1/api/cycles/{id}/events` · operation ID `listCycleEvents` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ActivityEvent[] | The cycle's own activity rows, newest first. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | ## Run a cycle's lifecycle `POST /v1/api/cycles/{id}/execute` · operation ID `executeCycle` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `mode` | query | no | string | `sandbox` (default) or `desktop`. | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | CycleExecutionStarted | The lifecycle run was started. It proceeds asynchronously — watch it on the stream or timeline endpoints. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such cycle, or its project is gone. | | `409` | ApiError | The cycle's status does not allow execution. | | `502` | ApiError | The dispatch could not be queued — the cycle was NOT started. Phase 2.9 commits the state change and the outbox row together, so nothing is half-written; retry is safe. | ## Read a cycle's conversation `GET /v1/api/cycles/{id}/messages` · operation ID `listCycleMessages` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `agentId` | query | no | string | Read this agent's thread instead of the cycle's. | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | WireMessage[] | The transcript, oldest first. An unknown cycle or an unresolvable thread reads as an EMPTY list, not a 404. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | The cycle's project is out of reach — a client collaborator it was never shared with (existence is deliberately not revealed). | ## Send a message to a cycle's conversation `POST /v1/api/cycles/{id}/messages` · operation ID `sendCycleMessage` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `CycleMessage` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `agentId` | string (uuid) | null | no | | | `content` | string | **yes** | min length 1 | | `messageId` | string (uuid) | no | | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | CycleTurn | The turn was stored, and the transcript as it stands afterwards. A view-only client collaborator may store a turn but never drives the agent. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such cycle in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | | `409` | ApiError | The supplied `messageId` is already in use — either on another conversation, or on this one with different content. | ## Send a message and stream the reply (SSE) `POST /v1/api/cycles/{id}/messages/stream` · operation ID `streamCycleMessage` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `CycleMessage` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `agentId` | string (uuid) | null | no | | | `content` | string | **yes** | min length 1 | | `messageId` | string (uuid) | no | | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | string | `user_message` (the echoed turn), then `token` chunks of raw text, `tool_start` / `tool_end` around each tool call, and finally `complete` or `error`. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such cycle in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | | `409` | ApiError | The supplied `messageId` is already in use — either on another conversation, or on this one with different content. | ## List a cycle's pending requests `GET /v1/api/cycles/{id}/pending-requests` · operation ID `listCyclePendingRequests` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | PendingRequest[] | This cycle's requests. Resolved ones are included — `status` says which are still open. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | ## Stop a running cycle `POST /v1/api/cycles/{id}/stop` · operation ID `stopCycle` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | CycleStopped | The run was cancelled and the cycle moved to `canceled` through the governed transition. Cancellation is checked BETWEEN steps: the step in flight completes and nothing after it starts — a coding agent already running in the sandbox is NOT interrupted. `orchestratorNotified` reports whether a parked project run was told; `false` is normal for a cycle executed on its own. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such cycle in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | | `409` | ApiError | There is no running or suspended run to stop; the body names the status that was actually found. | ## Stream a cycle's lifecycle events (SSE) `GET /v1/api/cycles/{id}/stream` · operation ID `streamCycle` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | string | One frame per new timeline event, a `keepalive` roughly every 30s, a `done` event on a terminal status, and a hard stop after ~20 minutes. A cycle that does not exist is reported as a frame on an open 200 stream, not as a 404. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | The cycle's project is out of reach — a client collaborator it was never shared with (existence is deliberately not revealed). | ## Add a task to a cycle `POST /v1/api/cycles/{id}/tasks` · operation ID `addCycleTask` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `CreateCycleTask` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `dependsOn` | string (uuid)[] | no | | | `description` | string | **yes** | min length 1, max length 5000 | | `title` | string | **yes** | min length 1, max length 200 | **Responses** | Status | Body | Description | | --- | --- | --- | | `201` | Task | The created task. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such cycle in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Remove a task from a cycle `DELETE /v1/api/cycles/{id}/tasks/{taskId}` · operation ID `deleteCycleTask` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `taskId` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | MessageAck | The task was deleted. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such task under this cycle. | | `409` | ApiError | Only a task in `queued` or `blocked` status can be deleted. | ## Update a task inside a cycle `PATCH /v1/api/cycles/{id}/tasks/{taskId}` · operation ID `updateCycleTask` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `taskId` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `UpdateCycleTask` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `description` | string | no | min length 1, max length 5000 | | `sortOrder` | integer | no | min -9007199254740991, max 9007199254740991 | | `title` | string | no | min length 1, max length 200 | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | Task | The updated task. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such task under this cycle. | ## List a cycle's events plus its tasks' `GET /v1/api/cycles/{id}/timeline` · operation ID `getCycleTimeline` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ActivityEvent[] | The cycle's activity rows merged with its tasks', oldest first. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | ## Get post-hoc step transcripts for a cycle's planner + coder turns `GET /v1/api/cycles/{id}/transcript` · operation ID `getCycleTranscript` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | CycleTranscript | Every completed agent turn for this cycle — its planner turn and each task's native hlix coder turn — oldest first. A run that used an external ACP coder, or the reviewer's turn, has no entry (see the route's docblock for why). | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such cycle in this workspace. | ## Get the cycle's cost and token roll-up `GET /v1/api/cycles/{id}/usage` · operation ID `getCycleUsage` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | AgentUsageRollup | Totals plus two breakdowns of the same runs: by execution lane (`byRole`) and by worker persona (`byPersona`). A lane that never ran is ABSENT rather than reported as a zero row; `byPersona` instead keeps a `null` bucket for runs whose dispatch knew no persona, so both breakdowns sum to `totals`. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such cycle in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## If a request fails These apply to every operation on this page. - `400` — the body failed schema validation, or the request named no active organization. A `ValidationError` body carries the failing fields; an `ApiError` body carries only `error`. - `401` — no valid session or API key. - `403` — authenticated, but not a member of the named organization, or a client collaborator without the required access. - `404` — no such resource **in this workspace**. Absent and not-visible are deliberately the same answer; do not infer cross-tenant existence from it. - `409` — the expected revision or state is stale, or another operation currently owns the transition. - `500` — the operation failed server-side. Retry only where an idempotency key makes that safe. ## Next steps --- # Imports Secure project snapshot upload, verification and finalization. An **import** moves a local project into a workspace as a new cloud project. It is a three-step protocol — create, upload the bundle, finalize — so the manifest, the bundle hash and the protected data are all bound to one reviewed scan. Generated from contract version `1.0.0` — the same document [`/v1/api/openapi.json`](/api/openapi/) serves. ## Prerequisites - An API key in `x-api-key`, or a browser session cookie - `X-Organization-Id` naming the workspace to act in, unless the session already has an active one - The base URL for your deployment, `https://server.hlix.ai` by default ## Operations | Method | Path | Operation | | --- | --- | --- | | `POST` | `/v1/api/imports` | [Create an idempotent project import session](#create-an-idempotent-project-import-session) | | `GET` | `/v1/api/imports/{id}` | [Get an owned project import session](#get-an-owned-project-import-session) | | `PUT` | `/v1/api/imports/{id}/bundle` | [Stream a Git bundle to a local hlix backend](#stream-a-git-bundle-to-a-local-hlix-backend) | | `POST` | `/v1/api/imports/{id}/finalize` | [Verify, restore, and atomically create an imported project](#verify-restore-and-atomically-create-an-imported-project) | | `POST` | `/v1/api/projects/{id}/resources/agent` | [Create a project-scoped agent from local instructions](#create-a-project-scoped-agent-from-local-instructions) | | `POST` | `/v1/api/projects/{id}/resources/mcp` | [Explicitly activate a validated MCP server for one project](#explicitly-activate-a-validated-mcp-server-for-one-project) | | `POST` | `/v1/api/projects/{id}/resources/skill` | [Explicitly activate a quarantined skill for one project](#explicitly-activate-a-quarantined-skill-for-one-project) | ## Create an idempotent project import session `POST /v1/api/imports` · operation ID `createProjectImport` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `CreateProjectImport` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `bundleSha256` | string | **yes** | | | `bundleSize` | integer | **yes** | max 5368709120 | | `commitSha` | string | **yes** | | | `defaultBranch` | string | **yes** | | | `historyMode` | `preserve` | `abort_on_findings` | **yes** | | | `idempotencyKey` | string | **yes** | min length 16, max length 200 | | `manifest` | object | **yes** | | | `manifestSha256` | string | **yes** | | | `name` | string | **yes** | min length 1, max length 200 | | `stack` | string | **yes** | min length 1, max length 100 | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectImportCreated | The existing idempotent import session. | | `201` | ProjectImportCreated | A new import session and scoped upload target. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `409` | ApiError | The idempotency key names a different payload. | ## Get an owned project import session `GET /v1/api/imports/{id}` · operation ID `getProjectImport` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectImportStatus | The current import state. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | The import does not exist or belongs to another actor. | ## Stream a Git bundle to a local hlix backend `PUT /v1/api/imports/{id}/bundle` · operation ID `uploadProjectImportBundle` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/x-git-bundle` (required) The contract does not pin this body's properties. **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | UploadAccepted | The bundle passed its declared hash and size. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | The import does not exist or belongs to another actor. | | `409` | ApiError | The import no longer accepts an upload. | | `415` | ApiError | The upload is not a Git bundle. | ## Verify, restore, and atomically create an imported project `POST /v1/api/imports/{id}/finalize` · operation ID `finalizeProjectImport` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `FinalizeProjectImport` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `protectedFiles` | object[] | **yes** | | | `secrets` | object | **yes** | | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectImportFinalized | The import was already finalized. | | `201` | ProjectImportFinalized | The project and initial immutable revision were created. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | The import does not exist or belongs to another actor. | | `409` | ApiError | Another finalizer owns the import transition. | | `503` | ApiError \| WorkspaceBudgetError | Encrypted secret storage is not configured, or this backend instance is at its Coding Workspace budget (then `active`/`limit` are present, the import was reset, and a retry is safe). | ## Create a project-scoped agent from local instructions `POST /v1/api/projects/{id}/resources/agent` · operation ID `importProjectAgent` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `ImportAgent` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `description` | string | no | max length 2000 | | `instructions` | string | **yes** | min length 1, max length 20000 | | `name` | string | **yes** | min length 1, max length 120 | | `runtimeKind` | `claude-code` | `codex` | `cursor-agent` | `hlix` | **yes** | | | `sandboxProfile` | `builder` | `desktop` | `e2e` | **yes** | | **Responses** | Status | Body | Description | | --- | --- | --- | | `201` | ImportedAgent | The project-scoped agent was created. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | ## Explicitly activate a validated MCP server for one project `POST /v1/api/projects/{id}/resources/mcp` · operation ID `importProjectMcp` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `ImportMcp` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `args` | string[] | no | | | `command` | string | no | max length 100 | | `envKeys` | string[] | no | | | `headers` | object | no | | | `name` | string | **yes** | min length 1, max length 100 | | `transport` | `stdio` | `http` | `sse` | **yes** | | | `url` | string (uri) | no | max length 2000 | **Responses** | Status | Body | Description | | --- | --- | --- | | `201` | ImportedMcp | The MCP server was activated with secret references. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | ## Explicitly activate a quarantined skill for one project `POST /v1/api/projects/{id}/resources/skill` · operation ID `importProjectSkill` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `ImportSkill` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `content` | string | **yes** | min length 1, max length 200000 | | `slug` | string | **yes** | min length 1, max length 100 | **Responses** | Status | Body | Description | | --- | --- | --- | | `201` | ImportedSkill | The skill is available in this project sandbox. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | ## If a request fails These apply to every operation on this page. - `400` — the body failed schema validation, or the request named no active organization. A `ValidationError` body carries the failing fields; an `ApiError` body carries only `error`. - `401` — no valid session or API key. - `403` — authenticated, but not a member of the named organization, or a client collaborator without the required access. - `404` — no such resource **in this workspace**. Absent and not-visible are deliberately the same answer; do not infer cross-tenant existence from it. - `409` — the expected revision or state is stale, or another operation currently owns the transition. - `500` — the operation failed server-side. Retry only where an idempotency key makes that safe. ## Next steps --- # Projects Projects, their repositories, roadmap, artifacts and Coding Workspace. A **project** is one codebase under hlix's control, with its own Coding Workspace, repositories, roadmap and budget. These operations create projects, start runs against them, and read everything hanging off one. Generated from contract version `1.0.0` — the same document [`/v1/api/openapi.json`](/api/openapi/) serves. ## Prerequisites - An API key in `x-api-key`, or a browser session cookie - `X-Organization-Id` naming the workspace to act in, unless the session already has an active one - The base URL for your deployment, `https://server.hlix.ai` by default ## Operations | Method | Path | Operation | | --- | --- | --- | | `GET` | `/v1/api/projects` | [List the workspace's projects](#list-the-workspaces-projects) | | `POST` | `/v1/api/projects` | [Create a project](#create-a-project) | | `DELETE` | `/v1/api/projects/{id}` | [Delete a project](#delete-a-project) | | `GET` | `/v1/api/projects/{id}` | [Get one project](#get-one-project) | | `PATCH` | `/v1/api/projects/{id}` | [Update a project's configuration](#update-a-projects-configuration) | | `GET` | `/v1/api/projects/{id}/artifacts` | [List a project's stored artifacts](#list-a-projects-stored-artifacts) | | `GET` | `/v1/api/projects/{id}/artifacts/{filename}` | [Get a signed download URL for one project artifact](#get-a-signed-download-url-for-one-project-artifact) | | `GET` | `/v1/api/projects/{id}/budget` | [Get the project's delivery budget and spend to date](#get-the-projects-delivery-budget-and-spend-to-date) | | `GET` | `/v1/api/projects/{id}/cycle-metrics` | [Get a project's flow metrics computed from cycle stage history](#get-a-projects-flow-metrics-computed-from-cycle-stage-history) | | `GET` | `/v1/api/projects/{id}/desktop` | [Report whether a graphical desktop is reachable](#report-whether-a-graphical-desktop-is-reachable) | | `POST` | `/v1/api/projects/{id}/desktop` | [Start the desktop and mint an embeddable URL](#start-the-desktop-and-mint-an-embeddable-url) | | `GET` | `/v1/api/projects/{id}/diagnostics` | [Read why the project's run failed or is parked](#read-why-the-projects-run-failed-or-is-parked) | | `GET` | `/v1/api/projects/{id}/image-builds` | [List the project's sandbox image builds](#list-the-projects-sandbox-image-builds) | | `GET` | `/v1/api/projects/{id}/linear-sessions` | [List the Linear conversations delegated into this project](#list-the-linear-conversations-delegated-into-this-project) | | `GET` | `/v1/api/projects/{id}/messages` | [Read the project's conversation](#read-the-projects-conversation) | | `GET` | `/v1/api/projects/{id}/repos` | [List a project's repositories](#list-a-projects-repositories) | | `POST` | `/v1/api/projects/{id}/repos` | [Attach a repository to a project](#attach-a-repository-to-a-project) | | `DELETE` | `/v1/api/projects/{id}/repos/{repoId}` | [Detach a repository from a project](#detach-a-repository-from-a-project) | | `PATCH` | `/v1/api/projects/{id}/repos/{repoId}` | [Update a repository's branch or label](#update-a-repositorys-branch-or-label) | | `POST` | `/v1/api/projects/{id}/repos/{repoId}/primary` | [Make a repository the project's primary](#make-a-repository-the-projects-primary) | | `POST` | `/v1/api/projects/{id}/repos/connect` | [Connect a repo-less project's existing history to GitHub](#connect-a-repo-less-projects-existing-history-to-github) | | `GET` | `/v1/api/projects/{id}/repos/connect/preview` | [Inspect repo-less history before connecting a remote](#inspect-repo-less-history-before-connecting-a-remote) | | `GET` | `/v1/api/projects/{id}/roadmap` | [Get the project's roadmap](#get-the-projects-roadmap) | | `POST` | `/v1/api/projects/{id}/start` | [Start the project's autonomous run](#start-the-projects-autonomous-run) | | `POST` | `/v1/api/projects/{id}/stop` | [Stop the project's autonomous run](#stop-the-projects-autonomous-run) | | `GET` | `/v1/api/projects/{id}/terminal` | [Mint SSH access into the project's Coding Workspace](#mint-ssh-access-into-the-projects-coding-workspace) | | `GET` | `/v1/api/projects/{id}/threads` | [Get the project's conversation tree](#get-the-projects-conversation-tree) | | `GET` | `/v1/api/projects/{id}/usage` | [Get the project's cost and token roll-up](#get-the-projects-cost-and-token-roll-up) | ## List the workspace's projects `GET /v1/api/projects` · operation ID `listProjects` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectSummary[] | Every project, newest first, each with its attached repositories and the counts the projects index renders. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | ## Create a project `POST /v1/api/projects` · operation ID `createProject` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `CreateProject` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `brief` | string | no | min length 1, max length 5000 | | `claudeMd` | string | no | | | `name` | string | no | min length 1, max length 100 | | `repos` | object[] | no | | | `runtime` | object | no | | | `stack` | string | no | min length 1, max length 200 | **Responses** | Status | Body | Description | | --- | --- | --- | | `201` | Project | The created project and its repositories. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `409` | ApiError | One of the named repositories is already attached to another project in this workspace. | | `500` | ApiError | The project could not be created. | ## Delete a project `DELETE /v1/api/projects/{id}` · operation ID `deleteProject` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | MessageAck | The project was deleted. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Get one project `GET /v1/api/projects/{id}` · operation ID `getProject` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectDetail | The project, its repositories, the viewer's steer permission and whether this deployment can publish previews. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Update a project's configuration `PATCH /v1/api/projects/{id}` · operation ID `updateProject` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `UpdateProject` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `claudeMd` | string | no | | | `costCapUsd` | number | null | no | | | `cycleCap` | integer | null | no | | | `intake` | object | no | | | `maxRevisions` | integer | null | no | | | `name` | string | no | min length 1, max length 100 | | `requiresApproval` | boolean | no | | | `runtime` | object | no | | | `stack` | string | no | min length 1, max length 200 | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | Project | The updated project. Repository membership is never changed here — the collection is echoed back unchanged. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## List a project's stored artifacts `GET /v1/api/projects/{id}/artifacts` · operation ID `listProjectArtifacts` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | StoredArtifact[] | The stored objects. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Get a signed download URL for one project artifact `GET /v1/api/projects/{id}/artifacts/{filename}` · operation ID `getProjectArtifactUrl` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `filename` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | SignedArtifactUrl | A short-lived signed URL. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project, or no such artifact under it. | ## Get the project's delivery budget and spend to date `GET /v1/api/projects/{id}/budget` · operation ID `getProjectBudget` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectBudget | The project's own cap overrides (null means inherited), the caps actually enforced, the platform defaults behind them, and spend to date in USD. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Get a project's flow metrics computed from cycle stage history `GET /v1/api/projects/{id}/cycle-metrics` · operation ID `getProjectCycleMetrics` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | CycleMetrics | Throughput, cycle-time percentiles, per-stage medians, WIP, aging WIP, and stage-automation quality — computed purely from each cycle's stageHistory. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace. | ## Report whether a graphical desktop is reachable `GET /v1/api/projects/{id}/desktop` · operation ID `getProjectDesktop` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectDesktopAccess | Desktop status. Reports only — never starts the VNC stack, and never starts a stopped sandbox. `available: true` with no `desktopUrl` means the sandbox is running but the desktop has not been started. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Start the desktop and mint an embeddable URL `POST /v1/api/projects/{id}/desktop` · operation ID `startProjectDesktop` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectDesktopAccess | The noVNC client URL, or `available: false` with the reason. POST because it spawns processes and resets the sandbox's idle auto-stop — which is why opening the page never does it. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Read why the project's run failed or is parked `GET /v1/api/projects/{id}/diagnostics` · operation ID `getProjectRunDiagnostics` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | RunDiagnostics | The run's own account of itself: the failure it recorded, its un-collapsed engine status, and every step it is parked on with the labels a resume may address. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project, or it has no run to diagnose. | ## List the project's sandbox image builds `GET /v1/api/projects/{id}/image-builds` · operation ID `listProjectImageBuilds` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectImageBuild[] | The build rows, newest first. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## List the Linear conversations delegated into this project `GET /v1/api/projects/{id}/linear-sessions` · operation ID `listProjectLinearSessions` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectLinearSessions | Newest first. Each row carries the Linear issue identifier and deep link, the session's status, and the cycle the ticket became (null while the Orchestrator has yet to adopt it). | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Read the project's conversation `GET /v1/api/projects/{id}/messages` · operation ID `listProjectMessages` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `agentId` | query | no | string | Read this agent's project thread instead of the conductor's. | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | WireMessage[] | The transcript, oldest first. A project or agent thread that cannot be resolved reads as an EMPTY list, not a 404. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## List a project's repositories `GET /v1/api/projects/{id}/repos` · operation ID `listProjectRepos` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectRepo[] | The repositories, primary first then oldest first. An EMPTY array is the repo-less signal — there is no placeholder row. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Attach a repository to a project `POST /v1/api/projects/{id}/repos` · operation ID `attachProjectRepo` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `AttachProjectRepo` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `cloneUrl` | string (uri) | **yes** | | | `defaultBranch` | string | no | min length 1, max length 200 | | `githubInstallationId` | string | null | no | | | `githubRepoId` | integer | null | no | | | `isPrimary` | boolean | no | | | `label` | string | null | no | | | `name` | string | **yes** | min length 1, max length 200 | | `owner` | string | **yes** | min length 1, max length 100 | | `provider` | string | no | | **Responses** | Status | Body | Description | | --- | --- | --- | | `201` | ProjectRepo | The attached repository. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | | `409` | ApiError | That repository is already attached to a project in this workspace. | | `500` | ApiError | The repository could not be attached. | ## Detach a repository from a project `DELETE /v1/api/projects/{id}/repos/{repoId}` · operation ID `detachProjectRepo` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `repoId` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | MessageAck | The repository was detached. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project, or that repository does not belong to it. | | `409` | ApiError | Tasks have already run against that repository, so it cannot be detached. | ## Update a repository's branch or label `PATCH /v1/api/projects/{id}/repos/{repoId}` · operation ID `updateProjectRepo` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `repoId` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `UpdateProjectRepo` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `defaultBranch` | string | no | min length 1, max length 200 | | `label` | string | null | no | | | `setupCommand` | string | null | no | | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectRepo | The updated repository. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project, or that repository does not belong to it. | ## Make a repository the project's primary `POST /v1/api/projects/{id}/repos/{repoId}/primary` · operation ID `setPrimaryProjectRepo` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `repoId` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectRepo | The promoted repository. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project, or that repository does not belong to it. | ## Connect a repo-less project's existing history to GitHub `POST /v1/api/projects/{id}/repos/connect` · operation ID `connectProjectRepo` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `ConnectProjectRepo` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `force` | boolean | no | | | `githubInstallationId` | string | null | no | | | `githubRepoId` | integer | null | no | | | `label` | string | null | no | | | `name` | string | **yes** | min length 1, max length 200 | | `owner` | string | **yes** | min length 1, max length 100 | | `provider` | string | no | | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ConnectProjectRepoResult | The repository was already connected by an earlier completed request. | | `201` | ConnectProjectRepoResult | The existing project history was pushed and the repository was connected. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | | `409` | ApiError | The repository identity is unavailable, the project gained a repository, or the remote contains conflicting refs without force acknowledgement. | | `502` | ApiError | The push stopped before every local ref was confirmed on the remote; retry is safe. | | `503` | WorkspaceBudgetError | This backend instance is at its Coding Workspace budget. Nothing was changed and the request is safe to retry; `Retry-After` carries the hint. | ## Inspect repo-less history before connecting a remote `GET /v1/api/projects/{id}/repos/connect/preview` · operation ID `previewProjectRepoConnection` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | RepoHistorySummary | The local branch, tag and commit counts. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | | `409` | ApiError | The project already has a repository and therefore has no repo-less history to connect. | | `503` | WorkspaceBudgetError | This backend instance is at its Coding Workspace budget. Nothing was changed and the request is safe to retry; `Retry-After` carries the hint. | ## Get the project's roadmap `GET /v1/api/projects/{id}/roadmap` · operation ID `getProjectRoadmap` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectRoadmap | The merged roadmap — persisted cycles, the orchestrator's plan and any un-adopted proposals. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Start the project's autonomous run `POST /v1/api/projects/{id}/start` · operation ID `startProject` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `StartProject` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `brief` | string | **yes** | min length 1 | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectStarted | The run was handed to the engine. It starts asynchronously — watch it over the event or stream endpoints. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | | `409` | ApiError | A run for this project is already running or suspended. | | `502` | ApiError | The dispatch could not be queued — the run was NOT started. Phase 2.9 commits the state change and the outbox row together, so nothing is half-written; retry is safe. | ## Stop the project's autonomous run `POST /v1/api/projects/{id}/stop` · operation ID `stopProject` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectStopped | The run was cancelled. Cancellation is checked BETWEEN steps: the step in flight completes and nothing after it starts. A coding agent already running in the sandbox is NOT interrupted — it runs to completion and its result is still recorded. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | | `409` | ApiError | There is no running or suspended run to stop; the body names the status that was actually found. | ## Mint SSH access into the project's Coding Workspace `GET /v1/api/projects/{id}/terminal` · operation ID `getProjectTerminalAccess` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectTerminalAccess | A ready-to-paste `ssh` command, or `available: false` with the reason. Never starts a stopped sandbox. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Get the project's conversation tree `GET /v1/api/projects/{id}/threads` · operation ID `getProjectThreadDirectory` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectThreadDirectory | The thread directory. It always states whether it KNOWS: a degraded read answers an explicitly non-authoritative directory rather than an empty one. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## Get the project's cost and token roll-up `GET /v1/api/projects/{id}/usage` · operation ID `getProjectUsage` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | AgentUsageRollup | Totals plus two breakdowns of the same runs: by execution lane (`byRole`) and by worker persona (`byPersona`). A lane that never ran is ABSENT rather than reported as a zero row; `byPersona` instead keeps a `null` bucket for runs whose dispatch knew no persona, so both breakdowns sum to `totals`. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | ## If a request fails These apply to every operation on this page. - `400` — the body failed schema validation, or the request named no active organization. A `ValidationError` body carries the failing fields; an `ApiError` body carries only `error`. - `401` — no valid session or API key. - `403` — authenticated, but not a member of the named organization, or a client collaborator without the required access. - `404` — no such resource **in this workspace**. Absent and not-visible are deliberately the same answer; do not infer cross-tenant existence from it. - `409` — the expected revision or state is stale, or another operation currently owns the transition. - `500` — the operation failed server-side. Retry only where an idempotency key makes that safe. ## Next steps --- # Reports Agency portfolio health and client-safe reporting views. **Reports** are aggregate, client-safe views over a workspace: portfolio health, billing, usage, and customers. They read across projects and never expose another workspace's data. Generated from contract version `1.0.0` — the same document [`/v1/api/openapi.json`](/api/openapi/) serves. ## Prerequisites - An API key in `x-api-key`, or a browser session cookie - `X-Organization-Id` naming the workspace to act in, unless the session already has an active one - The base URL for your deployment, `https://server.hlix.ai` by default ## Operations | Method | Path | Operation | | --- | --- | --- | | `GET` | `/v1/api/reports` | [Get the current portfolio overview](#get-the-current-portfolio-overview) | | `GET` | `/v1/api/reports/billing` | [Get billing health](#get-billing-health) | | `GET` | `/v1/api/reports/customers` | [List customer portfolio reports](#list-customer-portfolio-reports) | | `GET` | `/v1/api/reports/projects` | [List project health reports](#list-project-health-reports) | | `GET` | `/v1/api/reports/usage` | [Get agent usage and cost](#get-agent-usage-and-cost) | ## Get the current portfolio overview `GET /v1/api/reports` · operation ID `getOverviewReport` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `period` | query | no | string | Rolling reporting window such as `30d`; clamped to 1–365 days. | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | AgencyOverviewReport \| ClientOverviewReport | The agency portfolio report, or the caller's client-safe overview. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | ## Get billing health `GET /v1/api/reports/billing` · operation ID `getBillingReport` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `period` | query | no | string | Rolling reporting window such as `30d`; clamped to 1–365 days. | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | AgencyBillingReport \| ClientBillingReport | Agency billing health, or the invoices visible to this client. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | ## List customer portfolio reports `GET /v1/api/reports/customers` · operation ID `listCustomerReports` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `period` | query | no | string | Rolling reporting window such as `30d`; clamped to 1–365 days. | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | CustomersReport | Each customer counted once across all projects. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a client collaborator; this portfolio view is agency-only. | ## List project health reports `GET /v1/api/reports/projects` · operation ID `listProjectReports` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `period` | query | no | string | Rolling reporting window such as `30d`; clamped to 1–365 days. | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | AgencyProjectsReport \| ClientProjectsReport | Agency project health, or only the projects visible to this client. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | ## Get agent usage and cost `GET /v1/api/reports/usage` · operation ID `getUsageReport` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `period` | query | no | string | Rolling reporting window such as `30d`; clamped to 1–365 days. | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | UsageReport | Agent runs, tokens, and cost for the reporting window. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | The caller is a client collaborator; this portfolio view is agency-only. | ## If a request fails These apply to every operation on this page. - `400` — the body failed schema validation, or the request named no active organization. A `ValidationError` body carries the failing fields; an `ApiError` body carries only `error`. - `401` — no valid session or API key. - `403` — authenticated, but not a member of the named organization, or a client collaborator without the required access. - `404` — no such resource **in this workspace**. Absent and not-visible are deliberately the same answer; do not infer cross-tenant existence from it. - `409` — the expected revision or state is stale, or another operation currently owns the transition. - `500` — the operation failed server-side. Retry only where an idempotency key makes that safe. ## Next steps --- # Revisions The revisions operations in the hlix API contract. A **revision** is one immutable snapshot of a project's files. These operations read the current head, upload a new revision under compare-and-swap, and download a revision's bundle. This is what `hlix push`, `hlix pull` and `hlix sync` drive. Generated from contract version `1.0.0` — the same document [`/v1/api/openapi.json`](/api/openapi/) serves. ## Prerequisites - An API key in `x-api-key`, or a browser session cookie - `X-Organization-Id` naming the workspace to act in, unless the session already has an active one - The base URL for your deployment, `https://server.hlix.ai` by default ## Operations | Method | Path | Operation | | --- | --- | --- | | `GET` | `/v1/api/projects/{id}/protected-files` | [Download encrypted-at-rest project-local configuration](#download-encrypted-at-rest-project-local-configuration) | | `GET` | `/v1/api/projects/{id}/revisions/{revisionId}/bundle` | [Download the authenticated Git bundle for the current revision](#download-the-authenticated-git-bundle-for-the-current-revision) | | `GET` | `/v1/api/projects/{id}/revisions/head` | [Get the immutable revision at the project head](#get-the-immutable-revision-at-the-project-head) | | `POST` | `/v1/api/projects/{id}/revisions/uploads` | [Create a compare-and-swap revision upload](#create-a-compare-and-swap-revision-upload) | | `PUT` | `/v1/api/projects/{id}/revisions/uploads/{uploadId}/bundle` | [Stream a local revision Git bundle](#stream-a-local-revision-git-bundle) | | `POST` | `/v1/api/projects/{id}/revisions/uploads/{uploadId}/finalize` | [Verify and atomically advance the project revision head](#verify-and-atomically-advance-the-project-revision-head) | ## Download encrypted-at-rest project-local configuration `GET /v1/api/projects/{id}/protected-files` · operation ID `getProjectProtectedFiles` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectProtectedFiles | Protected files decrypted for this authenticated sync. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `409` | ApiError | CLI revision sync could not act on this project — most often because it is repo-backed rather than imported, or its revision state moved underneath the request. | | `503` | ApiError | `SECRETS_ENCRYPTION_KEY` is not configured on this instance, so protected files cannot be decrypted. An operator has to fix it; retrying will not. | ## Download the authenticated Git bundle for the current revision `GET /v1/api/projects/{id}/revisions/{revisionId}/bundle` · operation ID `downloadProjectRevisionBundle` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `revisionId` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | string (binary) | The full-history Git bundle. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | The revision is not the current project head. | | `409` | ApiError | CLI revision sync could not act on this project — most often because it is repo-backed rather than imported, or its revision state moved underneath the request. | ## Get the immutable revision at the project head `GET /v1/api/projects/{id}/revisions/head` · operation ID `getProjectRevisionHead` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectRevisionHead | The current revision and canonical manifest. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | The project has no revision head yet. | | `409` | ApiError | CLI revision sync could not act on this project — most often because it is repo-backed rather than imported, or its revision state moved underneath the request. | | `503` | WorkspaceBudgetError | This backend instance is at its Coding Workspace budget. Nothing was changed and the request is safe to retry; `Retry-After` carries the hint. | ## Create a compare-and-swap revision upload `POST /v1/api/projects/{id}/revisions/uploads` · operation ID `createProjectRevisionUpload` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `CreateProjectRevisionUpload` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `bundleSha256` | string | **yes** | | | `bundleSize` | integer | **yes** | max 5368709120 | | `commitSha` | string | **yes** | | | `expectedGeneration` | integer | **yes** | max 9007199254740991 | | `expectedRevisionId` | string (uuid) | **yes** | | | `idempotencyKey` | string | **yes** | min length 16, max length 200 | | `manifest` | object | **yes** | | | `manifestSha256` | string | **yes** | | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectRevisionUploadCreated | An existing idempotent upload. | | `201` | ProjectRevisionUploadCreated | A new scoped bundle upload. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `409` | ApiError | The cloud head changed or the idempotency payload differs. | | `503` | WorkspaceBudgetError | This backend instance is at its Coding Workspace budget. Nothing was changed and the request is safe to retry; `Retry-After` carries the hint. | ## Stream a local revision Git bundle `PUT /v1/api/projects/{id}/revisions/uploads/{uploadId}/bundle` · operation ID `uploadProjectRevisionBundle` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `uploadId` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/x-git-bundle` (required) The contract does not pin this body's properties. **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | UploadAccepted | The uploaded bytes match their declared integrity. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such upload for this project. | | `409` | ApiError | The upload is no longer accepting a bundle (already finalized, or expired). | | `415` | ApiError | The body must be sent as `application/x-git-bundle`. | ## Verify and atomically advance the project revision head `POST /v1/api/projects/{id}/revisions/uploads/{uploadId}/finalize` · operation ID `finalizeProjectRevisionUpload` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `uploadId` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `FinalizeProjectImport` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `protectedFiles` | object[] | **yes** | | | `secrets` | object | **yes** | | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | ProjectRevisionFinalized | The upload was already finalized. | | `201` | ProjectRevisionFinalized | The revision head advanced. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such upload for this project, or another request claimed it first. | | `409` | ApiError | The cloud head changed or a task is active. | | `503` | WorkspaceBudgetError \| ApiError | This backend instance is at its Coding Workspace budget. Nothing was changed and the request is safe to retry; `Retry-After` carries the hint. Alternatively, `SECRETS_ENCRYPTION_KEY` is not configured on this instance, so protected files cannot be decrypted. An operator has to fix it; retrying will not. | ## If a request fails These apply to every operation on this page. - `400` — the body failed schema validation, or the request named no active organization. A `ValidationError` body carries the failing fields; an `ApiError` body carries only `error`. - `401` — no valid session or API key. - `403` — authenticated, but not a member of the named organization, or a client collaborator without the required access. - `404` — no such resource **in this workspace**. Absent and not-visible are deliberately the same answer; do not infer cross-tenant existence from it. - `409` — the expected revision or state is stale, or another operation currently owns the transition. - `500` — the operation failed server-side. Retry only where an idempotency key makes that safe. ## Next steps --- # Tasks Standalone tasks and their review evidence. A **task** is the atomic unit of execution — one worker agent, one git worktree, one branch, one outcome. These operations create and read tasks, their comments, their verified review evidence, and their status stream. Generated from contract version `1.0.0` — the same document [`/v1/api/openapi.json`](/api/openapi/) serves. ## Prerequisites - An API key in `x-api-key`, or a browser session cookie - `X-Organization-Id` naming the workspace to act in, unless the session already has an active one - The base URL for your deployment, `https://server.hlix.ai` by default ## Operations | Method | Path | Operation | | --- | --- | --- | | `GET` | `/v1/api/tasks` | [List tasks in the workspace](#list-tasks-in-the-workspace) | | `POST` | `/v1/api/tasks` | [Create a standalone task](#create-a-standalone-task) | | `GET` | `/v1/api/tasks/{id}` | [Get one task](#get-one-task) | | `PATCH` | `/v1/api/tasks/{id}` | [Update a task's title, description or status](#update-a-tasks-title-description-or-status) | | `GET` | `/v1/api/tasks/{id}/comments` | [List the comments on a task](#list-the-comments-on-a-task) | | `POST` | `/v1/api/tasks/{id}/comments` | [Comment on a task](#comment-on-a-task) | | `POST` | `/v1/api/tasks/{id}/execute` | [Execute a task standalone (not implemented)](#execute-a-task-standalone-not-implemented) | | `GET` | `/v1/api/tasks/{id}/review` | [Get the verified review evidence for a task](#get-the-verified-review-evidence-for-a-task) | | `GET` | `/v1/api/tasks/{id}/stream` | [Stream a task's status changes (SSE)](#stream-a-tasks-status-changes-sse) | ## List tasks in the workspace `GET /v1/api/tasks` · operation ID `listTasks` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `projectId` | query | no | string | Only tasks in this project. | | `status` | query | no | string | Only tasks in this status. | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | Task[] | The matching tasks. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | ## Create a standalone task `POST /v1/api/tasks` · operation ID `createTask` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `CreateTask` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `description` | string | **yes** | min length 1, max length 5000 | | `projectId` | string (uuid) | **yes** | | | `title` | string | no | min length 1, max length 200 | **Responses** | Status | Body | Description | | --- | --- | --- | | `201` | Task | The created task. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such project in this workspace — or a client collaborator it was never shared with (existence is deliberately not revealed). | | `500` | ApiError | The task could not be created. | ## Get one task `GET /v1/api/tasks/{id}` · operation ID `getTask` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | Task | The task. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such task in this workspace. | ## Update a task's title, description or status `PATCH /v1/api/tasks/{id}` · operation ID `updateTask` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `UpdateTask` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `description` | string | no | min length 1, max length 5000 | | `status` | `queued` | `blocked` | `building` | `testing` | `reviewing` | `approved` | `merging` | `merged` | `pr_created` | `ci_passing` | `done` | `failed` | no | | | `title` | string | no | min length 1, max length 200 | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | Task | The updated task. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such task in this workspace. | ## List the comments on a task `GET /v1/api/tasks/{id}/comments` · operation ID `listTaskComments` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | TaskComment[] | The task's comment thread, oldest first. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such task in this workspace. | ## Comment on a task `POST /v1/api/tasks/{id}/comments` · operation ID `addTaskComment` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Request body** — `application/json`, `AddTaskComment` (required) | Property | Type | Required | Notes | | --- | --- | --- | --- | | `body` | string | **yes** | min length 1, max length 10000 | | `messageId` | string (uuid) | no | | **Responses** | Status | Body | Description | | --- | --- | --- | | `201` | TaskComment | The stored comment. | | `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such task in this workspace. | | `409` | ApiError | The supplied `messageId` is already in use — either on another conversation, or on this one with different content. | ## Execute a task standalone (not implemented) `POST /v1/api/tasks/{id}/execute` · operation ID `executeTask` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such task in this workspace. | | `501` | ApiError | There is no standalone single-task run. Execute the task's cycle instead. The task is left untouched. | ## Get the verified review evidence for a task `GET /v1/api/tasks/{id}/review` · operation ID `getTaskReviewEvidence` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | TaskReviewEvidence | The same observation the QA evaluator reviews. Repo-backed, it is a three-dot compare of the task branch against the commit it was cut from; repo-less, it is the delivered workspace archive and there is no diff. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | No such task, or the task has no evidence to show (no branch, no recorded branch point). | | `502` | ApiError | The repository host could not be reached. | ## Stream a task's status changes (SSE) `GET /v1/api/tasks/{id}/stream` · operation ID `streamTask` **Parameters** | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | path | **yes** | string | | | `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. | **Responses** | Status | Body | Description | | --- | --- | --- | | `200` | string | One frame per status change, a `keepalive` event roughly every 30s, and the stream closes on a terminal status or after ~20 minutes. A task that does not exist is reported as a `{"error":"Task not found"}` FRAME on an open 200 stream, not as a 404. | | `400` | ApiError | The request named no active organization. | | `401` | ApiError | No valid session or API key. | | `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. | | `404` | ApiError | The task's project is out of reach — a client collaborator it was never shared with (existence is deliberately not revealed). | ## If a request fails These apply to every operation on this page. - `400` — the body failed schema validation, or the request named no active organization. A `ValidationError` body carries the failing fields; an `ApiError` body carries only `error`. - `401` — no valid session or API key. - `403` — authenticated, but not a member of the named organization, or a client collaborator without the required access. - `404` — no such resource **in this workspace**. Absent and not-visible are deliberately the same answer; do not infer cross-tenant existence from it. - `409` — the expected revision or state is stale, or another operation currently owns the transition. - `500` — the operation failed server-side. Retry only where an idempotency key makes that safe. ## Next steps --- # Environment discovery How hlix reconstructs a local development environment, collects protected configuration, and produces a reviewable cloud setup plan. `hlix import` treats the repository, its local configuration, and the invoking process environment as one migration source. It does not rely on a single framework detector and it does not silently discard credentials. The result is an integrity-bound snapshot plus a declarative environment plan. Source files and Git history move in the Git bundle. Protected files and secret values take a separate encrypted path. ## Prerequisites - The CLI is [installed](/getting-started/install/) and [authenticated](/getting-started/authentication/) - The shell you run `hlix import` from holds the environment values the project expects — for a key declared or referenced by the project but absent from a file, the invoking process is where hlix looks - You are prepared to read the dry-run report before approving anything ## The model 1. **Contain the scan.** Hlix resolves one real project root, applies Git and hlix ignore rules, refuses symlink traversal and nested repositories, and enforces size and file-count limits. 2. **Read explicit environment intent first.** Cursor environment configuration and Dev Container lifecycle settings outrank inferred commands. Docker, Compose, Gitpod, Procfile, Buildpacks, Railpack, Nixpacks, deployment-platform files, version managers, language manifests, and lockfiles provide additional evidence. 3. **Infer only missing pieces.** Package-manager locks and standard manifests can supply install, build, start, runtime, and port hints. Every inference records its source and confidence; repository commands are never executed during the local scan. 4. **Resolve protected configuration.** Hlix collects recognized project-local dotenv and credential files and key names explicitly required by environment or MCP configuration. It also observes environment references in project files and collects matching local values when present. Missing explicitly declared values block import; a missing source-code observation produces a warning because the application may provide a default or use it only in another environment. Hlix control-plane variables are never collected from the invoking process. 5. **Review once per import invocation, then bind that exact scan.** The report names evidence, setup commands, required and collected secret keys, protected paths, quarantined resources, and blockers. A real import uses one authoritative scan for its prompt, manifest, and upload; it does not rescan after approval. ## Detection order | Priority | Evidence | What hlix uses | | --- | --- | --- | | 1 | `.cursor/environment.json` | install, start, terminal, and working-directory intent | | 1 | `.devcontainer/devcontainer.json`, `devcontainer.json` | lifecycle setup, forwarded ports, container/remote variables, and declared secret names | | 2 | Docker, Compose, `.gitpod.yml`, `Procfile` | explicit platform and process evidence; unparsed commands remain visible for review | | 2 | `project.toml`, `railpack.json`, `nixpacks.toml`, `Aptfile` | Buildpack and build-plan intent | | 2 | `fly.toml`, `railway.json`, `render.yaml`, `vercel.json`, `netlify.toml` | deployment-platform evidence without pretending every field is portable | | 3 | `package.json`, lockfiles, Python manifests, `go.mod` | package manager, runtime, dependency installation, build, and start inference | | 4 | `.node-version`, `.nvmrc`, `.python-version`, `.tool-versions`, `mise.toml` | pinned Node.js, Python, and Bun runtime versions | | 5 | repository references such as `${DATABASE_URL}` | observed environment key names and private-port hints | Explicit configuration fills the plan first. Lower-priority detectors fill gaps rather than replacing reviewed intent. Repeated install commands are deduplicated. ## Secret collection without source leakage Hlix separates four concepts that other environment products often combine: | Concept | Example | Cloud handling | | --- | --- | --- | | Required key name | `DATABASE_URL` declared by Dev Container or MCP config | recorded in the manifest; absence blocks import | | Observed key name | `OPTIONAL_API_URL` referenced by code | matching local value is collected; absence is a warning | | Secret value | the local value of `DATABASE_URL` | sent only during authenticated finalization and encrypted at rest | | Protected file | `.env.local`, `.netrc`, cloud credentials | encrypted separately; excluded from the Git snapshot | | Quarantined executable config | MCP, agent, or skill definition | encrypted and preserved, but inactive until explicit import | The dry-run and JSON report contain key names and paths, never values. Plaintext secret values are not placed in Git, the bundle manifest, CLI arguments, or progress logs. User ignore rules do not hide recognized protected files or quarantined resources: hlix still discovers them inside paths ignored by Git or `.hlixignore` and keeps them out of the Git bundle. Hard built-in exclusions for dependency stores, build output, VCS internals, and `.hlix/` are not traversed. Common protected sources include dotenv profiles, npm and network credentials, Docker registry configuration, AWS and Google application credentials, Cargo and Python registry credentials, and project MCP authentication references. When a declared or observed project key exists only in the invoking shell, Hlix collects that value under the same key instead of asking the developer to re-enter it. Ambient runtime values such as `CI`, `HOME`, `PATH`, `PORT`, and `NODE_ENV`, plus every `HLIX_*` control-plane value, are excluded from this implicit collection path. `.envrc` is preserved as a protected file. Hlix reads literal assignments without executing the file. Dynamic assignments such as `$(op read …)` always produce a key-only non-execution warning; if direnv has already resolved that key into the invoking process, Hlix collects the resolved value. Only a missing resolved value blocks import. Root assignments take precedence over nested files, and a dynamic key is resolved only from the invoking environment, never from an unrelated nested literal. ## Patterns adopted from established tools Hlix combines proven pieces rather than inventing a proprietary environment-file format for every stack: - [Cursor cloud environments](https://cursor.com/changelog/cloud-in-agents-window) separates machine setup from the checked-out workspace and models reusable `.cursor/environment.json` snapshots for cloud agents. - [Dev Containers](https://containers.dev/) provides a portable repository-owned environment description and ordered lifecycle hooks. - [GitHub Codespaces](https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/configuring-dev-containers/specifying-recommended-secrets-for-a-repository) lets a repository declare required secret names, while [prebuilds](https://docs.github.com/en/codespaces/prebuilding-your-codespaces) move expensive setup off the startup path. - [Gitpod tasks](https://www.gitpod.io/docs/configure/workspaces/tasks) separates preparatory, idempotent initialization, and long-running start commands. - [Railpack](https://railpack.com/architecture/overview), [Nixpacks](https://nixpacks.com/docs/how-it-works), and [Cloud Native Buildpacks detection](https://buildpacks.io/docs/for-platform-operators/concepts/lifecycle/detect/) demonstrate ordered source detection that emits a reviewable build plan. - [Vercel framework detection](https://vercel.com/docs/project-configuration) shows the same explicit-config-first, conventional-source-second pattern, while [`vercel env pull`](https://vercel.com/docs/cli/env) makes environment synchronization a separate operation. - [1Password](https://developer.1password.com/docs/cli/secrets-scripts/), [Infisical](https://infisical.com/docs/documentation/platform/secrets-mgmt/concepts/secrets-delivery), and [Doppler](https://docs.doppler.com/docs/cli) demonstrate runtime secret injection without committing values. Hlix adopts that protected lane while also migrating reviewed project-local secret files. The Hlix-specific layer is the migration boundary: one CLI review carries the source, history, configuration, and protected local state into a workspace-scoped cloud project while preserving revision identity for later `push`, `pull`, and `sync`. ## Review the actual plan ```bash hlix import . --dry-run ``` For automation, keep the report private because paths and key names can reveal architecture even though values are excluded: ```bash hlix import . --dry-run --json > /tmp/hlix-import-plan.json ``` If the report is correct, run the real import interactively or use `--yes` in a non-interactive job. That new invocation performs a fresh authoritative scan; the earlier dry-run is evidence for review, not a reusable approval token. The real import binds its own prompt and upload to one scan, and `--yes` cannot bypass missing secrets, unsafe paths, history findings, integrity failures, or size limits. ## If the plan is wrong - **The wrong dotenv profile is active.** Pass `--env-file `; every other profile is still preserved encrypted. - **A required key is missing** and import blocks. Add the value to the declared source, or to the invoking shell, and scan again — a *declared* key is fatal, an *observed* one is only a warning. - **A `.envrc` key resolved to nothing.** A dynamic assignment such as `$(op read …)` is never executed. Let direnv resolve it into your shell first, then re-run the scan. - **A setup command you do not recognise** appears in the review. That is the point of showing it verbatim. Do not approve it; remove or fix the source file first. - **The stack or install command is wrong.** Override with `--stack`, or add explicit intent in `.cursor/environment.json` or a Dev Container file — explicit configuration outranks every inference. - **Confidence is low with no evidence listed.** The scanner found no recognised configuration. Add one explicit source rather than relying on inference. ## Next steps --- # Import a project Scan a local repository, protect its secrets and configuration, upload its history, and create a cloud project. `hlix import .` creates a cloud project from the folder you are standing in. `hlix import ` does the same for another folder. ## Prerequisites - The CLI is [authenticated](/getting-started/authentication/) - Git is installed, even when the source folder is not already a repository - You have reviewed what the selected folder may contain - No existing `.hlix/config.json` binds the folder to a different API or workspace ## The safe import path 1. **Run a dry scan.** ```bash hlix import . --dry-run ``` Expected result: a key-only report of the stack, ordinary files, environment sources, secret-key count, protected resources, historical findings, and blockers. 2. **Resolve every blocker.** Do not treat `--history preserve` as a secret-removal tool. Rotate exposed credentials and remove them from Git history when appropriate. 3. **Choose the environment file when automatic precedence is wrong.** ```bash hlix import . --dry-run --env-file .env.development ``` 4. **Create the cloud project.** ```bash hlix import . --name "Payments service" ``` `--name` defaults to the folder name. `--stack` can override stack detection. When setup commands, protected files, or quarantined resources were found, the CLI shows their commands, paths, and secret **key names** and asks for one explicit trust decision. Automation should inspect a `--dry-run`, then pass `--yes` to a fresh import invocation. That invocation scans again and binds its own exact result to the upload; `--yes` never bypasses a blocker. 5. **Verify both sides.** ```bash hlix projects list test -f .hlix/state.json ``` Refresh the hlix web app in the same workspace. The project should appear with an initial revision. ## What gets moved One folder splits into three lanes: snapshot files go into the uploaded Git bundle, protected files take a separate encrypted path outside Git, and quarantined resources are preserved encrypted but never activated. The importer separates the folder into three lanes: | Lane | Examples | Result | | --- | --- | --- | | Snapshot files | source, tests, lockfiles, docs, untracked work not ignored | copied into a temporary repository and uploaded in a verified Git bundle | | Protected files | dotenv, credentials, keys, `.npmrc`, `terraform.tfvars`, high-confidence secret content | excluded from Git and uploaded through encrypted protected storage | | Quarantined resources | `AGENTS.md`, `CLAUDE.md`, skills, `.mcp.json` | encrypted and preserved, but not activated during project import | Dirty and untracked snapshot files are included in an explicit import-snapshot commit created in a temporary clone. The source repository is not committed, rebased, or rewritten. Git history reachable from the local branches and tags captured by the approved scan is preserved in the bundle. A ref created or changed after approval is excluded or stops the snapshot. For a non-Git folder, hlix creates the history inside the temporary snapshot; it does not run `git init` in your source folder. ## Environment discovery The scan looks for explicit development-environment configuration first, then infers missing pieces from common ecosystem files. It can identify toolchain versions, dependency installation, build and start commands, terminal processes, private ports, explicitly required environment keys, and observed source references. Repository commands remain untrusted strings during the local scan. They are not executed locally by `init`, `import --dry-run`, or `import`. Approved setup runs later inside the isolated Coding Workspace before a cloud task, where it can access project secrets; this is why the import confirmation lists it verbatim. ## Secret handling Dotenv precedence for automatic development selection is: 1. `.env` 2. `.env.development` 3. `.env.local` 4. `.env.development.local` Later files override earlier values. If that is not the intended cloud-development environment, pass `--env-file `. Only key names, hashes, sizes, and paths appear in the manifest and CLI report. Values and exact protected-file bytes are sent separately. See [Secrets & protected files](/security/secrets/) for the full lifecycle. ## Git history policy The default mode is `abort-on-findings`: ```bash hlix import . --history abort-on-findings ``` Use `preserve` only after an explicit review: ```bash hlix import . --history preserve ``` `preserve` means “upload the history despite findings.” Anyone with appropriate access to that repository history may still recover historical values. Rotate exposed credentials before importing. ## If the scan blocks the import | Finding | Resolution | | --- | --- | | Potential secret in history | Rotate it, rewrite/sanitize history where appropriate, then scan again | | History scan incomplete | Review the repository separately, then explicitly choose `--history preserve` if retaining it is intended | | Nested repository | Import the nested repository separately | | Symlink | Replace it with an in-root regular file/directory or exclude it; symlinks are refused in this release | | File or project too large | Exclude generated/local-only data with `.hlixignore` or split the project intentionally | | Protected-data limit | Remove unnecessary credential/config copies and keep only the intended environment | | Dotenv parse error | Fix invalid assignments or select a valid dotenv file | ## Common questions **Does import need a GitHub connection?** No. Import creates the project directly in your workspace from local files and history. Connecting a repository is a separate, later choice. **Are my secrets in the Git bundle?** No. Protected files travel a separate encrypted path and are excluded from the bundle entirely. The manifest and every report carry key *names*, hashes, sizes and paths — never values. **Can `--yes` get me past a blocker?** No. `--yes` records approval for the reviewed upload and cloud setup. Blockers — history findings, unsafe paths, integrity failures, size limits — are refused regardless of it. **Does `--history preserve` remove the secrets it found?** No, and this is the most important misreading on this page. It means "upload the history *despite* the findings". Anyone with access to that history can still recover the values. Rotate first. **I imported the wrong folder. What now?** Nothing was rewritten locally — import never commits, rebases, or alters your source repository. Delete the cloud project and re-import; the local `.hlix/config.json` is the only thing to clean up. ## Integrity and concurrency The CLI hashes the manifest and bundle, verifies the bundle before upload, and rechecks files while building the snapshot. If the source changes between scan and snapshot, import stops instead of uploading a mixed state. Import finalization records an immutable revision. Repeating the same request uses an idempotency key so an interrupted retry does not create duplicate projects. ## Next steps --- # Initialize a project Bind a local folder to the authenticated hlix workspace and create safe, reviewable project configuration. `hlix init` prepares a folder for the CLI. It does not create a cloud project; [`hlix import`](/cli/import/) does that and adds the project and revision IDs. ## Prerequisites - The CLI is [installed](/getting-started/install/) - You are [authenticated](/getting-started/authentication/) - The target is a directory you are authorized to scan and upload ## Initialize the current folder ```bash cd path/to/project hlix init ``` Initialize a different folder without changing directories: ```bash hlix init path/to/project ``` Expected result: ```text Initialized /absolute/path/to/project Project: not imported yet Workspace: ``` The command verifies the current login, scans the folder, and creates: | Path | Purpose | Commit it? | | --- | --- | --- | | `.hlix/config.json` | Stable API URL, workspace ID, and—after import—project ID | Yes | | `.hlix/.gitignore` | Keeps local revision state out of Git | Yes | | `.hlix/state.json` | Last synchronized immutable revision and generation | No; created after import | | `.hlixignore` | Project-local additions to the snapshot ignore list | Yes | ## Review the scan Human output calls out blocking findings. For a complete machine-readable report: ```bash hlix init --json ``` The JSON envelope includes file and byte counts, protected paths, quarantined resources, and blockers. It does not include secret values. ## Customize snapshot scope Add generated or local-only paths to `.hlixignore` using Git-ignore syntax: ```text .playwright-cache/ tmp-fixtures/ large-local-dataset/ ``` The default scanner already excludes `.git`, `.hlix`, dependency folders, common build outputs, caches, coverage, virtual environments, and operating-system metadata. Do not use `.hlixignore` to hide a credential that should be moved safely. Dotenv and recognized credential files belong in the protected-file path described in [Secrets & protected files](/security/secrets/). ## Verify initialization ```bash test -f .hlix/config.json test -f .hlix/.gitignore test -f .hlixignore git status --short ``` Expected result: the three configuration files exist, while `.hlix/state.json` is still absent until a successful import. ## If initialization fails - ``No workspace selected. Run `hlix auth login --workspace ` first.`` — nothing in the [resolution chain](/reference/cli/) named a workspace. - `workspace_mismatch`: a `--workspace`, `--base-url`, or `HLIX_*` value contradicts the binding of the folder being initialized. The message names both sides. Drop the override rather than editing `.hlix/config.json` to match — and never copy that file between projects or tenants. - `Refusing to write through a symlinked …`: replace the symlinked `.hlix` path with a real directory you own. - Scan blockers: follow the recovery steps in [Import a project](/cli/import/#if-the-scan-blocks-the-import). ## Next steps --- # Import skills, agents & MCP servers Explicitly activate project resources that the main import preserves but deliberately does not execute. Project import encrypts and preserves agent instructions, skill content, and MCP configuration, but quarantines them. Activation is a separate command because these files can change model behavior, run tools, reach remote services, or execute local programs in the Coding Workspace. ## Prerequisites - Run from a folder already bound by `hlix import` - Review the complete file you are importing - Confirm that referenced secret keys belong to this project and workspace - For MCP, understand whether the server is remote or starts a process ## Activate a resource Each resource type has its own command and its own approval. Pick the one you are importing. **A skill** is a reviewed `SKILL.md` that gives agents a named capability. Pass a `SKILL.md` file or a folder containing one: ```bash hlix skill import .agents/skills/release/SKILL.md ``` Override the derived slug when needed: ```bash hlix skill import .agents/skills/release --name release-checks ``` Expected result: the CLI prints the imported skill identity. The skill becomes an explicit project resource rather than being activated merely because the scanner found it. Verification: ```bash hlix skill import .agents/skills/release --name release-checks --json ``` The JSON response contains metadata, not secret values. **An agent** is a Markdown instruction brief that becomes a project-scoped agent record. Agent import accepts one Markdown instruction file: ```bash hlix agent import AGENTS.md --name "Repository builder" ``` Select the runtime and sandbox profile explicitly when the defaults are not right: ```bash hlix agent import AGENTS.md \ --name "Repository builder" \ --runtime claude-code \ --profile builder ``` Supported runtime values are `claude-code`, `codex`, `cursor-agent`, and `hlix`. Supported profiles are `builder`, `desktop`, and `e2e`. Expected result: the imported agent is associated with the bound project and carries the exact reviewed Markdown instructions. **An MCP server** is a tool server agents may call — the one resource that can reach the network or execute a local program. For a file containing one server: ```bash hlix mcp import .mcp.json ``` When the file contains several servers, name the one being approved: ```bash hlix mcp import .mcp.json --server github ``` Remote servers must use HTTPS. URLs with embedded credentials, query strings, or fragments are refused, as are endpoints targeting loopback, private, link-local, or cloud-metadata hosts. ### Stdio servers Stdio MCP starts an executable inside the Coding Workspace, so it requires an extra approval flag: ```bash hlix mcp import .mcp.json --server filesystem --allow-stdio ``` The command must be one of the supported direct executables: `bunx`, `deno`, `node`, `npx`, `python`, `python3`, or `uvx`. Shell wrappers and shell syntax are refused; `--allow-stdio` is not permission to run an arbitrary command string through a shell. ### MCP secrets Only environment **key names** are imported from MCP configuration. Literal secret values are not sent as part of the MCP server definition. At task time, hlix resolves those names from the project's encrypted secrets. A remote header must be exactly `${KEY}` or one approved authentication scheme followed by it, such as `Authorization: Bearer ${TOKEN}`; extra prefix or suffix text is refused. A sensitive stdio argument value must be exactly one `${KEY}` placeholder, and high-confidence literal tokens are refused in every argument. For example, configuration may declare that a server needs `GITHUB_TOKEN`; the value must already exist in the imported project's protected environment. If it does not, execution fails closed rather than starting with an empty value. Hlix-owned `.mcp.json` files hydrated into the Coding Workspace are owner-readable only and excluded from Git. ## Verify before the first run For each imported resource: 1. Re-open the source and confirm its hash/content did not change during review. 2. Use `--json` and capture the complete result in your change or approval record. Agent import returns an agent ID; skill and MCP import return the selected resource metadata instead. 3. Confirm the project ID in `.hlix/config.json` is the intended project. 4. For MCP, confirm the selected server name, transport, remote hostname or executable, arguments, and environment key names. 5. Start with the least-privileged credentials the server can use. ## Common failures - `not_initialized`: import the project first and run from its root. - Skill folder has no `SKILL.md`: point at the file or the correct directory. - Agent file is not Markdown: convert the reviewed instructions to a Markdown file. - MCP file contains multiple servers: add `--server `. - Remote MCP URL is not HTTPS: use the service's TLS endpoint. - Stdio MCP is refused: review it, then pass `--allow-stdio`; if it still fails, replace shell syntax with a direct executable and explicit arguments. - Required environment key is missing: add the value through the project's protected secret path, then push a new revision. Re-importing a skill or MCP server with the same project-scoped name updates that resource. Agent import creates a new agent each time so an existing agent identity is never replaced implicitly. ## Next steps --- # Push, pull & sync Reconcile a local project with its immutable cloud revision head without silently guessing through conflicts. Every imported project records a local base in `.hlix/state.json` and a cloud head with an immutable revision ID and monotonically increasing generation. Synchronization compares both sides to that common base. ## Prerequisites - Run commands from the imported project root - `.hlix/config.json` contains a project ID - `.hlix/state.json` contains the last synchronized revision - Finish and test valuable local work before any forced pull ## Choose the command Both sides are compared against the base revision they last agreed on: if only one moved, the direction is unambiguous; if both moved, hlix refuses rather than merging. | Command | Use when | Behavior | | --- | --- | --- | | `hlix push` | Local files changed; cloud did not | uploads a new immutable revision using compare-and-swap | | `hlix pull` | Cloud changed; local did not | verifies and applies the cloud head locally | | `hlix sync` | You want hlix to choose only when one side changed | pushes, pulls, or reports already synchronized | `hlix sync` is intentionally not a merge engine. If local and cloud both changed from the same base, it stops with a conflict. ## Look before you act All three commands accept `--dry-run`. It prints the divergence, takes no action, and reports the verdict the real command would reach. ```bash hlix push --dry-run ``` Expected result: ```text push --dry-run: Would push the local changes as a new cloud revision. local generation 3 (rev_01J8Z4) — modified cloud generation 3 (rev_01J8Z4) files 412 local, 410 cloud — 3 local-only, 1 cloud-only, 2 differing ``` The first line is the verdict; the block below is the evidence for it. The verdict is one of four per command: | Command | Verdict line | | --- | --- | | `push` | `Would push the local changes as a new cloud revision.` · `Nothing to push — local matches the last synced revision.` · `Would refuse: the cloud changed since this folder's base revision.` | | `pull` | `Would replace local files with the cloud revision.` · `Nothing to pull — the cloud head is the local base revision.` · `Would refuse: local files changed. Re-run with --force to replace them.` | | `sync` | `Would push …` · `Would pull the cloud revision into this folder.` · `Already in sync.` · `Would refuse: local and cloud both changed since the last sync.` | A dry run that would end in a conflict **exits 1**, the same way `hlix import --dry-run` exits 1 for a blocker. The report is the deliverable, and the exit code still carries the verdict — so a script can gate on it without parsing the text. **`conflict` is the only verdict that exits 1**; every other dry run exits `0`. ### When the real run would stop to ask A forced pull over local changes is the one case where the verdict alone is not the whole answer. The dry run adds a second line: ```bash hlix pull --force --dry-run ``` ```text pull --dry-run: Would replace local files with the cloud revision. Requires --yes (or interactive approval) to execute. local generation 3 (rev_01J8Z4) — modified cloud generation 4 (rev_01J9A2) — ahead files 412 local, 415 cloud — 2 local-only, 5 cloud-only, 3 differing ``` That run **exits `0`**: needing an answer is not a refusal, and a caller who has one — a terminal, or `--yes` — executes exactly this plan. In `--json`, the same fact is a field rather than a line: ```json {"dryRun":true,"action":"pull","approvalRequired":true,"localChanged":true,"cloudChanged":true} ``` ## Push local changes ```bash hlix push ``` Expected result: ```text push complete. ``` The new revision becomes head only when the cloud still matches the revision and generation in local state. Protected files are uploaded separately from the Git bundle. ## Pull cloud changes ```bash hlix pull ``` Expected result: `pull complete.` The CLI verifies bundle size, SHA-256, Git bundle validity, commit SHA, manifest metadata, and every protected file before applying data. If local managed files changed, pull refuses. After reviewing and backing up those changes, replace them explicitly: ```bash hlix pull --force ``` ### The forced-pull approval gate `pull --force` is the one command that deletes local work, so it asks before replacing anything. The two flags say different things: - **`--force`** states *"replace my changes."* It selects the destructive behavior. - **`--yes`** states *"do not ask."* It answers the prompt in advance. Interactively, `--force` alone prints what it is about to destroy and waits: ```text hlix pull --force will replace this folder with cloud revision 4. Local-only files to delete: 3 Files to replace with the cloud copy: 2 Local changes that were never pushed cannot be recovered by Hlix. ``` Declining exits `cancelled`. A `--json` or non-interactive caller cannot answer a prompt, so it is refused rather than defaulted to yes: ```text `pull --force` replaces local files. Review with `hlix pull --dry-run`, then repeat with `--yes`. ``` That refusal is `approval_required`. `--force` with no local changes replaces nothing and does not prompt. For a Git project, pull updates the managed working tree to the cloud commit. For a non-Git import, it replaces files described by the revision manifest. Ignored local-only files are outside that managed set. ## Reconcile automatically ```bash hlix sync ``` The decision table is exact: | Local since base | Cloud since base | Result | | --- | --- | --- | | no change | no change | `Already in sync.` | | changed | no change | push | | no change | changed | pull | | changed | changed | conflict; no side is overwritten | ## Verify synchronization ```bash hlix sync --json ``` Expected result: a versioned JSON envelope containing `action`, `revisionId`, and `generation`. Run the command again; it should report an action of `none`. ## If synchronization fails - `not_initialized`: run `hlix import .` from the project root. - `workspace_mismatch`: a `--workspace`, `--base-url`, or `HLIX_*` value contradicts this folder's binding. Drop the override, or point `--cwd` at a folder bound to the workspace you meant. Run [`hlix status`](/reference/cli/) to see which source decided what. - `approval_required`: a destructive step needs an approval this caller cannot give. Review with `--dry-run`, then repeat with `--yes`. - `cancelled`: you declined the prompt. Nothing was changed. - ``Cloud changed since the local base. Run `hlix pull` or resolve with `hlix sync`.`` — review before choosing a side. - ``Local files changed since the last sync. Push them, or use `hlix pull --force` to replace them.`` — push first if the local work matters. - `Both local and cloud changed since the last sync. Pull or push explicitly after reviewing the conflict.`: preserve local work in a branch or backup, inspect the cloud state, then choose a side explicitly. - `Project has active tasks`: wait for them to reach a terminal state, pull their resulting head when needed, then retry. - `Downloaded revision … failed integrity verification`: stop. Do not bypass the check; retry over a trusted network and contact support if it repeats. - `SECRETS_ENCRYPTION_KEY is not set`: the deployment cannot decrypt protected state. An operator must repair backend configuration. - `Refusing to replace non-regular protected file`: replace the symlink/device with the intended regular file only after confirming the path. ## Next steps --- # Agents & orchestration One orchestrator and six worker roles per project — how hlix coordinates a team of AI agents to deliver work end to end. The **team** that delivers one project is exactly one **orchestrator** and as many **worker agents** as the work needs. There is one team per project — no master orchestrator above it, no per-task free-for-all beneath it. Standing outside the team is the [Triage Agent](/concepts/triage/), which decides what is admitted as work in the first place. ## When this matters to you You need this page when you are deciding *who* should do something, or when a run behaved in a way you did not expect: - A task went to a worker you did not choose — the orchestrator routes by persona description, and this page lists the six it picks from. - You want to interrupt a run mid-flight rather than wait for it — that is steering, and only the orchestrator accepts it. - You asked an agent to implement something and it described the change instead of making it — that is consultation, and this page explains the difference. - You are wondering why two tasks did not conflict — they never shared a checkout, and that is a property of the team, not of the coding agent. ## The topology One orchestrator sits above six worker personas, and every worker runs a single task in its own git worktree inside the one Coding Workspace the project owns. ```text Project │ └── Orchestrator plans · dispatches · evaluates · replans │ ├── Senior Engineer ├── Product Engineer ├── UI Engineer each worker runs ONE task, ├── QA Engineer in its OWN git worktree, ├── Security Engineer inside the ONE Coding Workspace └── Technical Writer the project owns ``` ## The orchestrator The orchestrator is the project's **directly responsible individual**. It owns the intent end to end: it turns a brief into a roadmap, sequences that roadmap into cycles, dispatches a wave of work, evaluates what comes back, and decides what happens next — continue, retry, replan, or finish. It does not write code. That separation is the reason one orchestrator can run many workers without becoming the bottleneck — it is holding the plan, not the keyboard. ### It reads live state, not a briefing Each turn opens with a deliberately shallow orientation: which project this is, which repositories it has, and what stage the cycle in scope is at and for how long. That is all that is injected. Everything else it pulls when it needs it, from the object that owns the answer — the task breakdown, the triage rationale behind a cycle's lane, the run history and what each run cost, the verified review evidence for a task, the approval queue. Nothing is a snapshot taken at the start of a conversation, which is why an orchestrator asked about a cycle mid-run answers about the cycle as it is now. ### Consultation is not execution The single most expensive misreading of this system is treating an agent's *answer* as work that happened. The orchestrator has both kinds of tool and they are not interchangeable: | | Consultation | Execution | | --- | --- | --- | | What it is | Delegating a question to a specialist persona | Dispatching a task to the Coding Workspace | | What comes back | That specialist's text | A branch, a status, artifacts, and a cost | | What it can touch | Nothing — these personas have no tools, no repository, no shell | The task's own git worktree | If you ask for an implementation and get a description of the change, nothing was implemented. Code is written in exactly one way: a task, dispatched to the project's [Coding Workspace](/concepts/coding-workspace/), running in its own worktree. ### New work still goes through Triage When the orchestrator decides a project needs work it does not yet have, it raises a **proposal**, not a cycle. That proposal lands in the same queue every other intake path uses, becomes a cycle at status `triage`, and is judged like everything else. The orchestrator has no privileged path around that, and it cannot submit a plan for a cycle still in triage. Submitting a plan is what moves a cycle into execution, and it moves it through the same [governed transition](/running/cycles/#stage-history) machinery every other status change uses — attributed to the orchestrator, carrying its stated reason, stamped with the rules version in force. ### Its loop As the project workflow implements it: ```text project intake ──▶ roadmap planner ──▶ dispatch wave ▲ │ │ ▼ └──── evaluate ◀── cycle outcome │ ┌──────────┴──────────┐ ▼ ▼ finalize fail ``` The run suspends at each cycle outcome and resumes when the result arrives, which is what lets a project survive a backend restart mid-delivery. ## The Triage Agent Not part of the team, and deliberately so. The Triage Agent judges a request *before* it becomes the orchestrator's problem: how well understood it is, how costly a mistake would be, how urgently it should jump the queue, and whether it should become a cycle at all. It runs one-shot. It has no tools, no memory, and no conversation — it reads the request and the project it belongs to, and returns a structured judgement plus a rationale. It never names the risk lane or the priority those judgements produce; versioned platform rules do that, so the routing decision is deterministic and auditable rather than something a model improvised. Its model is set separately from the rest of the team, on the **Triage** row of the workspace's AI-models settings. See [Triage](/concepts/triage/). ## Worker roles Six personas. The orchestrator picks one per task by matching the work to the persona's description — you do not assign them by hand. | Role | What it is for | | --- | --- | | **Senior Engineer** | General implementation, refactors, and hard changes | | **Product Engineer** | Feature work driven from product intent | | **UI Engineer** | Interface work, layout, and interaction | | **QA Engineer** | Tests, verification, and reproducing failures | | **Security Engineer** | Security-sensitive changes and review | | **Technical Writer** | Documentation and written deliverables | Each worker persona is defined in the DNA layer, so a role's identity and standards come from one place rather than being restated at each call site. See [Context, memory & DNA](/concepts/context-memory/). ## Which coding agent actually runs A worker role says *what kind of work this is*. The coding agent — the **harness** — says *what program does it*. They are independent, and the second one is yours to choose per task: - **`hlix`** — our own harness, driven from the backend against the Coding Workspace toolbelt. This is what runs when a task names nothing else. - **External ACP coders** — `claude-code`, `codex`, `openai-agents`, `aider`, `cursor-agent`. These run inside the project's sandbox through the in-pod driver. The orchestration is the product, not the coder: the roadmap, the isolation, the context an agent is given, and the QA it answers to are identical whichever harness writes the diff. That is why bringing your own coder costs you nothing — and why we can ship one of our own without asking you to switch. See [Connect your agents](/guides/connect-agents/). ## Watching and steering the team A run is not a black box. Four endpoints, all under `/v1/api/projects/:projectId/agents/:agentId`: | Endpoint | Purpose | | --- | --- | | `POST …/orchestrator/stream` | Start an owned turn and stream it (SSE) | | `GET …/:agentId/attach` | Observe an in-flight run without owning it (SSE) | | `POST …/orchestrator/steer` | Inject a message into the running turn | | `POST …/orchestrator/abort` | Stop the current run | Steering injects a message into the running turn, which means it first stops the turn in flight and then resumes with your input folded in. ## Why one team per project Underneath, four pieces carry the work: Cloudflare serves the dashboard, the backend runs on Cloud Run and persists to Neon, and every agent's code executes in a Daytona sandbox in the EU — never on hlix's own infrastructure. A project is a unit of intent — one product, one client engagement, one initiative. One orchestrator per project means there is always a single answer to "what are we building and why," and a single accountable coordinator for the delivery. Scale comes from adding workers, not from stacking orchestrators. ## Where to go next | If you want to… | Read | | --- | --- | | Understand how the work itself is structured | [Work tree](/concepts/work-tree/) | | See what decides whether a request becomes work | [Triage](/concepts/triage/) | | See where the code actually executes | [The Coding Workspace](/concepts/coding-workspace/) | | Watch and steer the team from the dashboard | [The dashboard](/running/dashboard/#watching-the-team-work) | ## Next steps --- # The Coding Workspace One persistent sandbox per project, one git worktree per task — where agent-generated code actually runs, and how to open a shell in it. A **Coding Workspace** is one project's execution environment: a persistent, isolated sandbox that lives as long as the project does. Every task the project ever runs happens inside it, each in its own git worktree. ## When this matters to you - You want to know where untrusted, agent-generated code runs — it is never your machine and never the hlix backend. - Two tasks edited the same file and you want to know why they did not corrupt each other. - You need a shell inside a running project to debug something the logs will not tell you. - You are budgeting, and want to know what an idle project costs. ## One sandbox, many worktrees One persistent sandbox per project holds a single shared clone, and every task gets its own git worktree and branch **inside that same sandbox** — never a sandbox of its own. ```text Project ──▶ ONE Coding Workspace (named hlix-cw-) │ │ /workspace ├──── project/repos//repo the shared clone, ONE per repo │ ├──── tasks//repo git worktree · branch task-A ├──── tasks//repo git worktree · branch task-B └──── tasks//repo git worktree · branch task-C ``` Each `tasks//repo` is a git worktree of that one shared clone — not a second copy of the repository. The shared clone is the project's durable working state. Each task gets an isolated checkout and its own branch off it, so concurrent tasks — in the same wave or in different cycles — never share a working tree. Task isolation is a worktree inside one sandbox, not a sandbox of its own. The `repos//` level is what makes a project **multi-repo**, and it also makes the git lock per repository: two repos of one project serialize independently instead of queueing behind one project-wide lock. A project with no repository uses a fixed key, so its shared clone reads `project/repos/default/repo`. ## Why per project and not per task | Choice | Consequence | | --- | --- | | One sandbox per **project** | Dependencies install once. The second task starts warm. Build caches, `node_modules`, and toolchains survive. | | One worktree per **task** | Parallel tasks cannot see each other's edits. A failed task's mess is confined to its own directory. | | Sandbox is **non-ephemeral** | The filesystem outlives every individual run, so a project's state accumulates instead of being rebuilt. | The sandbox is reconnected by a stable name derived from the project id, so every acquire — across tasks, across backend instances — reaches the same sandbox. ## Who runs inside it The worktree layout above is the same whichever [harness](/guides/connect-agents/) writes the code. What differs is where the coder's own process lives. - **Hlix**, our own harness and the default, is driven from the backend against the workspace's command and filesystem tools. The backend provisions the worktree, the agent edits files in it, and the runner does the git work. - **External coders** — Claude Code, Codex, Cursor Agent — are launched by a driver process *inside* the sandbox, which mints the worktree in place, runs the coder there, and commits. Either way the work lands in `tasks//repo` on its own branch, and the pipeline that reviews it does not know or care which one produced it. ## Lifecycle and cost An idle Coding Workspace **stops itself** after an idle interval (60 minutes by default). Stopping is not deleting: the filesystem persists and billing falls to roughly storage. The next task's start resumes it. A project that goes quiet for long enough — 14 days by default — has its Coding Workspace destroyed by a maintenance sweep. Activity is re-validated immediately before each destroy, so a project that woke up in the meantime is left alone. The project's durable history is preserved separately, so a destroyed sandbox is recreated rather than lost. ## Secrets in the workspace The platform writes your project's secrets into a root `.env` inside the sandbox, and three independent layers stop it from ever entering a commit: 1. `/.env` is pinned in the shared clone's git exclude file. 2. Staging is `git add -A` followed by an explicit reset of `.env` — never a pathspec that could silently fail. 3. The secret writer refuses to overwrite a `.env` that is already tracked. Tokens are never passed through command environment variables either, because the sandbox provider inlines environment into the session command string — which means argv and persisted shell history. Credentials reach the sandbox through the filesystem instead. [Secrets & protected files](/security/secrets/) covers the full path. ## Opening a shell `GET /v1/api/projects/:id/terminal` returns access to a **running** sandbox. It never cold-starts one — opening a tab should not cost you a sandbox boot. ```bash curl -sS "$HLIX_BASE_URL/v1/api/projects/$PROJECT_ID/terminal" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` Expected result when the sandbox is up: ```json { "available": true, "webTerminalUrl": "https://…", "sshCommand": "ssh …", "expiresAt": "2026-08-07T10:14:22.481Z" } ``` The web terminal URL is a signed preview link and is best-effort — the key is omitted if minting it fails, and `sshCommand` still works. SSH access is short-lived (15 minutes) and suits VS Code Remote-SSH, JetBrains Gateway, or `scp`. When the sandbox is not reachable you get a reason instead of an error: ```json {"available": false, "reason": "sandbox not running (state: stopped)"} ``` Terminal access requires the agency `manage` tier — every client collaborator is refused, because the sandbox holds the project's secrets. ## Local development Setting `SANDBOX_RUNTIME=local` runs the coding agent on the host with a local filesystem root instead of a cloud sandbox. That is a development convenience only: the boot guard refuses it in a cloud deployment, where it would run agent-generated code inside the backend container with the backend's own credentials. ## Common questions **Does every task get its own sandbox?** No — and this is the design decision the page exists to explain. One sandbox lives per *project*, for the project's whole life. Each task gets its own git worktree inside it. Per-task sandboxes would mean paying a cold boot and a fresh clone for every task. **What happens to my files when the sandbox stops?** Nothing. An idle sandbox pauses itself; the filesystem persists and the next task resumes it. Billing while paused is storage, not compute. **Can I get a shell into it?** Yes — `GET /v1/api/projects/:id/terminal` returns a signed web-terminal URL and an `ssh` command suitable for VS Code Remote-SSH, JetBrains Gateway, or `scp`. It never cold-starts a stopped sandbox, because opening a tab should not cost you a boot. **Is my code ever executed on hlix's own infrastructure?** No. Agent-generated code runs in the project's sandbox. A boot guard refuses the local runtime in a cloud deployment specifically because it would run that code inside the backend container with the backend's credentials. **What if the project has no Git remote?** It still works. Task branches are merged inside the Coding Workspace under a lock, the result is snapshotted and downloadable, and the full history is bundled for recovery. GitHub adds an external review surface; it is not where the work happens. ## If the workspace will not open - `{"available": false, "reason": "sandbox not running (state: stopped)"}` — the project is idle and its sandbox has auto-stopped. Run a task; the next acquire resumes it. The terminal endpoint will not cold-start one on your behalf. - `{"available": false, "reason": …}` naming an absent sandbox — the project has never run a task, so no sandbox exists yet. - `403` on the terminal endpoint — terminal access needs the agency `manage` tier. Every client collaborator is refused, because the sandbox holds the project's secrets. - **`webTerminalUrl` is missing but `sshCommand` is present** — minting the signed preview link failed. That is best-effort; use SSH. - **The SSH command stops working after a few minutes** — it is short-lived by design (15 minutes). Request a new one. - **A task fails on a driver-layout mismatch** — the project's sandbox is persistent and keeps the driver it was created with. A backend deploy cannot change that; recovery is deliberately manual. ## Where to go next | If you want to… | Read | | --- | --- | | Understand who runs inside this sandbox | [Agents & orchestration](/concepts/agents/) | | See how tasks map onto branches | [Work tree](/concepts/work-tree/) | | Know what hlix does with your credentials | [Secrets & protected files](/security/secrets/) | | Watch a task use it | [Tasks](/running/tasks/) | ## Next steps --- # Context, memory & DNA DNA is the identity every agent is composed from; memory is what the workspace has learned. Together they are why the tenth task is better than the first. Two different things keep a fleet of agents consistent, and they are worth separating. **DNA** is the composed identity an agent starts with — voice, principles, role, and your standards. **Memory** is what the workspace has accumulated — conventions, decisions, and lessons, retrieved when relevant. ## When this matters to you - Your agents keep re-solving a problem you already decided — that is a memory gap. - Output from two projects reads like it came from two different companies — that is a DNA question. - You are evaluating hlix and want to know what compounds over time and what does not. ## DNA — the identity agents are composed from ```text DNA.md the shared identity: voice, principles, refusals + Persona the role: orchestrator, planner, reviewer, or one of the six worker roles + Context organization · user · project ▼ the instructions ONE agent actually runs with ``` Every prompt an hlix agent runs is composed, never inlined. The shared identity lives in one file; each role is a persona definition with its own inputs; the composition happens at call time with the organization, user, and project in scope. That is what makes "sounds like us" a property of the system rather than of whichever prompt someone last edited. The practical consequence: changing how *every* agent behaves is one edit in one place, and changing how *one role* behaves does not touch the others. ## Memory — what the workspace has learned Memory is scoped, categorized, and retrieved semantically. A memory is written once and surfaces whenever it is relevant, rather than being pasted into a prompt by hand. **Five scopes**, from broadest to narrowest: | Scope | Holds | | --- | --- | | `org` | Standards that apply to everything your workspace builds | | `project` | Decisions specific to one product or engagement | | `cycle` | Context for one feature or phase | | `task` | Detail about a single unit of work | | `agent` | What one agent has learned about doing its job | **Six categories**, which is what makes retrieval useful rather than noisy: | Category | Example | | --- | --- | | `convention` | "Tables use snake_case; TypeScript uses camelCase." | | `decision` | "We chose Postgres over DynamoDB for the ledger." | | `lesson` | "The staging seed script must run before the e2e suite." | | `preference` | "Prefer composition over inheritance in this codebase." | | `failure` | "Retrying the webhook without a jitter caused a thundering herd." | | `heuristic` | "Anything touching billing gets a security review." | Each memory also carries a `source` (`auto`, `user`, `interview`, `review`), a `confidence` between 0 and 1, and a `status` of `tentative`, `approved`, or `archived`. Agent-extracted memories arrive `tentative`; promoting one to `approved` is how you decide what the workspace actually stands behind. ## Reading and writing memory In the dashboard, **Global knowledge** browses the `org` scope — the standards that apply to everything. Over the API: | Operation | Endpoint | | --- | --- | | List | `GET /v1/api/memories` | | Read one | `GET /v1/api/memories/:id` | | Create | `POST /v1/api/memories` | | Update | `PATCH /v1/api/memories/:id` | | Delete | `DELETE /v1/api/memories/:id` | | Semantic search in a scope | `GET /v1/api/memories/search/:scope/:scopeId` | | Extract from a finished cycle | `POST /v1/api/memories/extract/:cycleId` | ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/memories" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{ "scope": "org", "scopeId": "'"$HLIX_WORKSPACE_ID"'", "category": "convention", "key": "database-naming", "value": "Database identifiers are snake_case. TypeScript identifiers are camelCase. Never mix them in one file.", "status": "approved" }' ``` ## How retrieval stays tenant-safe Long-term memory is stored as semantic embeddings namespaced by organization, scope, and scope id. Retrieval is a vector search inside that namespace, so a query in one workspace cannot surface another workspace's memories — the isolation is structural, not a filter applied after the fact. ## Why agencies care Consistency is what clients pay for. Memory is how a fleet of agents produces work that reads as one disciplined team rather than ten strangers, and DNA is how that team sounds like *your* team. Both accumulate: the second task in a project is better informed than the first, and the tenth agent inherits what the first nine learned. It is the one asset here that gets more valuable the longer you use it, and no model provider owns it. ## If memory does not show up in a run - **A memory exists but agents ignore it.** Check its `status`. A memory that is not approved is not composed into a prompt. - **A memory applies too widely or too narrowly.** The scope is what decides. Re-create it at the scope you meant; `scopeId` must match that scope's object. - **No SDK method for the memory endpoints.** They are not in `openapi.json` yet, so no client generates them. Use the dashboard or a direct HTTP call. - `404` on a memory you can see in another workspace — retrieval is namespaced per organization. Absent and not-visible are the same answer. - **Semantic search returns nothing for an obvious phrase.** Search runs inside one namespace. Confirm the `scope` and `scopeId` in the URL are the ones the memory was written under. - **Extraction from a cycle produced nothing.** It reads a *finished* cycle. A run still in flight has nothing settled to extract. ## Where to go next | If you want to… | Read | | --- | --- | | See who consumes this context | [Agents & orchestration](/concepts/agents/) | | Understand the scopes as objects | [Work tree](/concepts/work-tree/) | | Import a written standard as a skill | [Import skills, agents & MCP servers](/cli/resources/) | | See what a worker did with it | [Reviewing output](/running/review/) | ## Next steps --- # Glossary Every hlix term in one place, each linked to the page that owns it, including the four pairs that are routinely confused. **This page defines every term hlix uses in a load-bearing way, and links each one to the page that owns it.** If a word in these docs is doing more work than its everyday meaning, it is here. Start with the four confusable pairs — they cause more misreadings than the rest of the vocabulary combined. ## The four pairs people get wrong | These look alike | …but | And this one | | --- | --- | --- | | **Workspace** — your tenant/organization | ≠ | **Coding Workspace** — one project's sandbox | | **Revision** — an immutable snapshot's identity | ≠ | **Generation** — its monotonic counter | | **Worker persona** — how work is *assigned* | ≠ | **Execution lane** — what run history *reports* | | **Harness** — the coder binary that edits files | ≠ | **Agent role** — the orchestration job an LLM does | ## The work tree **Workspace** · Your tenant. Better-auth calls it an organization, the API header calls it `X-Organization-Id`, the CLI calls it `--workspace`, and the product calls it a Workspace. One entity, named by layer — `workspaceId` and `organizationId` are the same value. → [Work tree](/concepts/work-tree/) **Project** · One codebase under hlix's control. Owns a Coding Workspace, zero or more repositories, a roadmap, a budget, and its own revision history. → [Work tree](/concepts/work-tree/) **Cycle** · A multi-task unit of work with its own conversation — the level a roadmap is planned into, and the level that executes. → [Cycles](/running/cycles/) **Task** · The atomic unit of execution: one worker agent, one git worktree, one branch, one outcome. → [Tasks](/running/tasks/) **Wave** · The set of tasks a cycle dispatches in parallel. Outcomes are collected per task, so one failing task does not cancel its siblings. → [Cycles](/running/cycles/) **Roadmap** · The project-level plan the orchestrator produces from your brief, and re-plans after each cycle outcome. → [Agents & orchestration](/concepts/agents/) **Triage** · The stage every cycle is created at, and the decision made there: forward it to planning, or cancel it. Judged by the Triage Agent from the request's content, never skipped by any intake path. → [Triage](/concepts/triage/) **Lane** · The risk classification a cycle is routed into, derived by versioned platform rules from how well understood and how risky the change is — never named by the model. Four of the nine lanes force a human approval before dispatch on their own. → [Triage](/concepts/triage/) **Priority** · The integer a cycle carries, derived from the urgency tier Triage judged: `300` urgent, `200` high, `100` normal, `0` low. Ready cycles dispatch highest first, and `orderBy=priority` lists them the same way. Not a named level, and not something the planner can change. → [Triage](/concepts/triage/) **Stage history** · The record of every status move a cycle made — from, to, when, who decided (`human`, `agent`, or `rule`), under which rules version, and why. It is what the delivery metrics are computed from, and the reason a stage change is always attributable. → [Cycles](/running/cycles/#stage-history) ## Who does the work **Orchestrator** · The project-level agent that turns a brief into a roadmap, dispatches cycles, and decides what happens after each outcome. One per project — there is no master orchestrator above them, and no path by which the work it creates skips Triage. → [Agents & orchestration](/concepts/agents/) **Triage Agent** · The one-shot, tool-less agent that judges an incoming request's complexity, risk and urgency and decides whether it becomes a cycle. Not part of a project's team; its model is configured separately. → [Triage](/concepts/triage/) **Worker agent** · An agent that executes one task. Which persona it takes decides how the work is framed. → [Agents & orchestration](/concepts/agents/) **Worker persona** · The role a task is assigned under — the six named identities defined in the DNA layer. Personas route work; they are not what run history reports back. → [Agents & orchestration](/concepts/agents/) **Execution lane** · What a run actually records: `planner`, `builder`, `tester`, or `reviewer`. Several personas share one lane, so a Senior Engineer's run and a UI Engineer's run both appear as `builder`. → [Agents & orchestration](/concepts/agents/) **Harness** (also **coding agent**) · The program that edits your code inside a task — `hlix`, our own, or an external coder you already use: `claude-code`, `codex`, `cursor-agent`. Chosen per task by a six-rung cascade whose bottom rung is `hlix`. → [Connect your agents](/guides/connect-agents/) **Hlix** (the harness) · hlix's own coding agent, driven from the backend rather than launched inside the sandbox. The default when a task names no other harness, and the only one whose reasoning streams live and whose turn appears in a cycle's Transcript. → [Connect your agents](/guides/connect-agents/) **Agent role** · An orchestration job an LLM performs — `classifier`, `interviewer`, `planner`, `reviewer`, `chat`, `orchestrator`, `worker`, `triage`. Each maps to a model you can override. Unrelated to which harness runs. → [Connect your agents](/guides/connect-agents/) **ACP driver** · The in-sandbox process that launches an external coder and reports its result. Every harness except `hlix` runs through it. → [Connect your agents](/guides/connect-agents/) ## Execution and isolation **Coding Workspace** · One project's persistent execution environment — a sandbox with a filesystem and a shell, durable across the project's whole life. Idle sandboxes stop themselves; the filesystem survives, and the next task resumes it. → [The Coding Workspace](/concepts/coding-workspace/) **Worktree** · A git working tree checked out from the project's shared clone, one per task, so concurrent tasks never share a checkout. → [The Coding Workspace](/concepts/coding-workspace/) **Task branch** · The branch a task commits to, created off the base branch inside its worktree. Repository-backed projects push it; repo-less projects merge it locally. → [GitHub](/integrations/github/) **Repo-less project** · A project with no Git remote. Task branches are merged inside the Coding Workspace and the result is snapshotted and downloadable — no GitHub required. → [The Coding Workspace](/concepts/coding-workspace/) **Merge lock** · The mutex serializing merges into a project's default branch, held per repository so two repositories in one project do not queue behind each other. → [The Coding Workspace](/concepts/coding-workspace/) ## Files, snapshots, and sync **Revision** · One immutable snapshot of a project's files, identified by a revision ID. Never edited — a change creates a new one. → [Push, pull & sync](/cli/sync/) **Generation** · The monotonically increasing counter on a revision. `push` is a compare-and-swap against the expected revision ID *and* generation, which is what makes a lost race a `conflict` rather than a silent overwrite. → [Push, pull & sync](/cli/sync/) **Manifest** · The hashed inventory of a snapshot: every file's path, size, and SHA-256. Comparing manifests is how local-versus-cloud drift is computed without transferring anything. → [Import a project](/cli/import/) **Bundle** · The Git bundle carrying the snapshot's files and history. Hashed before upload and verified after. → [Import a project](/cli/import/) **Snapshot commit** · The synthetic commit an import creates in a temporary clone so dirty and untracked files can be captured without touching your repository. → [Import a project](/cli/import/) **Protected file** · A recognized credential-bearing file — dotenv, `.netrc`, cloud credentials, `.npmrc`. Excluded from Git and moved through encrypted storage instead. → [Secrets & protected files](/security/secrets/) **Quarantined resource** · An agent brief, skill, or MCP configuration that import preserved encrypted but deliberately did **not** activate. Activating one is a separate, explicit command. → [Import trust model](/security/trust-model/) **`.hlixignore`** · Project-local additions to the snapshot ignore list, in Git-ignore syntax. It cannot hide a protected file; those are found regardless. → [Initialize a project](/cli/init/) ## Context and review **DNA** · The composed identity every hlix agent is built from — voice, principles, and standards, cascading System → Owner → Org → Project. It is why two agents on one project produce consistent work. → [Context, memory & DNA](/concepts/context-memory/) **Memory** · Durable, scoped knowledge the workspace has learned, retrieved by semantic search inside a tenant-namespaced index. Only approved memories are composed into prompts. → [Context, memory & DNA](/concepts/context-memory/) **Scope** · Which level a memory applies at, and the object it is attached to. → [Context, memory & DNA](/concepts/context-memory/) **Review evidence** · The verified record behind a task's change — the diff read back from the repository host, the checks, and the provenance. It is an observation, not the agent's own summary of itself. → [Reviewing output](/running/review/) **Revision request** · Sending a task back with unresolved comments folded into its feedback, re-entering the cycle rather than starting over. → [Reviewing output](/running/review/) **Transcript** · The stored, post-hoc record of what each pipeline agent was told and what it answered — prompt and response, redacted. Distinct from the live streams, which do not survive a reload. Agency-only. → [The dashboard](/running/dashboard/#transcript) **Approval gate** · The human checkpoint before a gated cycle dispatches. Off by default at the project level; a forced lane opens it regardless. → [Approvals](/running/approvals/) **Pending request** · The durable record of a blocking question — an approval, or an agent's elicitation — that a run parks on until it is resolved. → [Approvals](/running/approvals/) ## Extensions and integrations **Skill** · A reviewed `SKILL.md` activated for a project, giving agents a named capability. → [Import skills, agents & MCP servers](/cli/resources/) **MCP server** · A Model Context Protocol server. **Inbound**: one you activate inside a cloud project so agents can use it ([Import skills, agents & MCP servers](/cli/resources/)). **Outbound**: the official `@hlix/mcp` server, which lets a coding agent on your machine read your workspace ([Official MCP server](/mcp/)). Opposite directions; do not confuse them. **Environment** · A per-project bundle of variables, files, skills, and MCP servers that a Coding Workspace loads. → [Environment discovery](/cli/environment-discovery/) **Pack** · A pre-built capability bundle baked into the sandbox image. → [The Coding Workspace](/concepts/coding-workspace/) **Client collaborator** · An external customer invited to one project, fenced by a second authorization layer beneath the workspace boundary. Can read and comment; cannot change execution settings. → [Import trust model](/security/trust-model/) ## Release and audit **Release ledger** · `packages/releases.json` — the single source of truth for which package versions are publicly available. Written last in a publication run, which is why the availability page can be trusted. → [Availability](/releases/availability/) ## If a term here does not match what you see - **Run history shows `builder` but you assigned a UI Engineer.** Expected — personas route work, lanes report it. See the two-column table above. - **The API says `organizationId` where the UI says Workspace.** Same value. The auth and data layers keep better-auth's vocabulary deliberately; never introduce a second tenant concept. - **A term appears in the API reference but not here.** The [generated reference](/api/reference/) describes the contract's own field names, which are not always product vocabulary. ## Next steps --- # Review & QA gates How hlix evaluates agent output, loops failed work back for another pass, and optionally pauses delivery for a human decision. hlix has **two** gates, and they are not the same thing. Automated QA judges work after it exists. An approval gate stops work before it starts. Every cycle passes the first; the second is a project policy you opt into — plus a set of risk lanes that demand it whether you opted in or not. ## When this matters to you - You want to know what happens between an agent finishing and you seeing the result. - A cycle failed and you want to know whether it will retry on its own. - You need a person in the loop before agents touch a production-adjacent project. - A cycle parked for approval on a project where the gate is switched off. ## Gate one: automated QA Work moves from build through automated QA to an evaluator verdict; a retryable failure loops back to build carrying the evaluator’s feedback, and the human approval gate — off by default — sits between approval and deploy. Every cycle runs it. No configuration, no opt-out. ```text intake ──▶ planner ──▶ execute ──▶ QA evaluator ▲ │ │ ├── approved ──▶ deploy │ │ └── optimize ─┤ (retryable) │ └── terminal ──▶ fail ``` The evaluator judges the completed tasks against the plan and the quality criteria and returns a verdict. A **retryable** problem loops through the optimizer and back into evaluation — the task does not just fail, it gets another pass with the failure as input. A **terminal** problem fails the cycle with a recorded reason rather than looping forever. The evaluator's verdict is visible on the task's evidence document, alongside the diff it judged: ```json {"evaluator": {"verdict": "approved", "summary": "…", "headSha": "9f2c1b0…", "stale": false}} ``` `stale: true` means the branch moved after the evaluator looked. The verdict is still shown — it is just no longer about the current commit. See [Reviewing output](/running/review/). ## Gate two: human approval Off by default. Setting `requiresApproval` on a project makes the orchestrator open a blocking `deployment_approval` request **before** launching a gated cycle, then park the run until someone decides. Approving resumes dispatch; denying fails that cycle. With the flag off, an autonomous run is unchanged — the gate adds nothing to a project that has not asked for it. [Approvals](/running/approvals/) covers the request queue and how to resolve one. ## Lanes that demand a human Some work is risky enough that a person signs off first, no matter what the project asked for. Every cycle is classified at [Triage](/concepts/triage/) into a **lane**, derived from how well understood the change is and how costly a wrong answer would be. Four of those lanes open the same blocking approval request the project flag would, on their own: | Lane | The classification behind it | | --- | --- | | `stop` | Simple but high-risk — a human should plan this before code starts | | `design-review` | Ambiguous and high-risk — a senior owner should look at it first | | `architecture-review` | Ambiguous and medium-risk — the shape should be agreed before code | | `manual-lane` | Systemic and high-risk — there is no automated lane for this | The lane check runs per cycle, and it ORs with the project setting: whichever says "gate" wins. So a project with approvals off still parks a `manual-lane` cycle, and a project with approvals on gates everything regardless of lane. The request that opens carries the lane in its `context`, so the queue tells you *why* this particular cycle stopped. The other five lanes — `auto`, `fast-review`, `plan-gate`, `experiment`, `split-work` — do not gate. They are recorded classification you can read, filter, and audit on; `plan-gate` in particular is already satisfied by the planning pass every cycle makes before it executes. ## Sending work back yourself Neither gate replaces your judgement. When you read a diff and disagree with it, `request-revision` folds your unresolved line comments into the task's feedback and re-enters the cycle, so the worker gets your notes as instructions rather than as a rejection. That is the third loop, and it is the one you drive. ## What gets recorded Three planes carry different promises, and the difference is the point: | Plane | Answers | Delivery | | --- | --- | --- | | Audit log | Who did what, to what, when, from where, with what outcome | Guaranteed and immutable | | Events | What is happening right now | Best-effort | | Agent runs and logs | Why a run cost or failed what it did | Best-effort | The audit log is the one that carries a promise. It is append-only at the database level, partitioned by month, and chained per organization with a row hash, so a tampered or missing row is detectable rather than merely unlikely. `GET /v1/api/audit-log/verify` checks that chain. Where a mutation is compliance-critical, the state change and its audit row commit in one database transaction — the record cannot exist without the change, or the change without the record. Reading the audit log requires the workspace `owner` or `admin` role. ## Where to go next | If you want to… | Read | | --- | --- | | Turn the approval gate on | [Approvals](/running/approvals/) | | See how a cycle gets its lane | [Triage](/concepts/triage/) | | Read the diff a gate judged | [Reviewing output](/running/review/) | | See what a cycle run does end to end | [Cycles](/running/cycles/) | ## Next steps --- # Triage Every cycle is judged before it is planned — how the Triage Agent decides forward or cancel, and how platform rules turn that judgement into a lane and a priority. **Every cycle starts at Triage.** Before a roadmap is drawn, before a task exists, before any agent touches a repository, the request is judged on what it actually is: how well understood it is, how costly a wrong answer would be, and how much it should jump the queue. ## When this matters to you - A cycle you asked for came back `canceled`, and you want the reason it was refused. - A cycle parked for approval on a project where you never turned approvals on. - Two cycles were ready at the same time and one dispatched first. ## Where Triage sits ```text request ──▶ Triage ──┬──▶ draft ──▶ planning ──▶ executing ──▶ … │ └──▶ canceled ``` Whatever proposed the work — the dashboard chat, `POST /v1/api/cycles`, a Linear delegation, or the project's own founding brief — none of them create a cycle directly. They all raise a *proposal*, and the roadmap planner is the single place a cycle row comes into existence. It creates every one at status `triage` and triages it immediately. No agent has a path around that, including the orchestrator: a plan cannot be submitted for a cycle that is still in triage. ```text Cycle "…" is still in triage. The Triage Agent decides whether it goes forward; a plan cannot skip that gate. ``` ## What the Triage Agent decides The agent reads the request's title, body, and the project it belongs to, and returns four judgements plus a written rationale. | It judges | Meaning | Values | | --- | --- | --- | | Complexity | How well understood the change is before work starts | `simple` · `ambiguous` · `systemic` | | Risk | How costly a wrong answer would be | `low` · `medium` · `high` | | Urgency | How much this should jump the queue ahead of work already waiting | `urgent` · `high` · `normal` · `low` | | Decision | Whether this becomes a cycle at all | `forward` · `cancel` | Urgency is judged from what the work *is* — something user-facing is broken, or other queued work is blocked on it — and explicitly not from how urgently the request happens to be worded. ## Lanes are derived, not chosen Complexity and risk resolve to one of nine **lanes** through a versioned rule set, `hlix-web-v1`: | | Risk `low` | Risk `medium` | Risk `high` | | --- | --- | --- | --- | | **`simple`** | `auto` | `fast-review` | `stop` | | **`ambiguous`** | `plan-gate` | `architecture-review` | `design-review` | | **`systemic`** | `experiment` | `split-work` | `manual-lane` | The lane lands on the cycle as `lane`, and the rule-set version that resolved it is recorded on the transition — so a lane can always be explained by the rules in force when it was assigned, not by the rules in force when you read it. **Four lanes change behaviour today.** `stop`, `design-review`, `architecture-review`, and `manual-lane` force a human approval before the cycle dispatches, whether or not the project opted into approvals — see [Review & QA gates](/concepts/review-gates/#lanes-that-demand-a-human). The remaining five are recorded as classification and carry no distinct mechanism of their own: `plan-gate` is already satisfied by the planning pass every cycle makes, and `auto`, `fast-review`, `experiment`, and `split-work` are metadata you can read, filter, and audit on. ## Urgency becomes a priority | Urgency | `priority` | | --- | --- | | `urgent` | `300` | | `high` | `200` | | `normal` | `100` | | `low` | `0` | `priority` is a plain integer on the cycle, not a named level, with 100-wide gaps so a later adjustment has room to land between tiers. It orders three things: - **Dispatch.** Ready cycles are launched highest priority first. This is an *order*, not a cap — every ready cycle in the wave still launches on that pass, and equal priorities keep their roadmap order. - **Reading cycles back.** `GET /v1/api/cycles?orderBy=priority` sorts by priority, then newest first; the dashboard's cycles grid offers the same sort. The only other accepted value is `createdAt`, which is the default. - **Planning.** The roadmap planner is shown the priorities already on the board when it sequences and links the new work it proposes. It cannot change an existing cycle's priority. ## What gets recorded A forward moves the cycle to `draft`; a cancel moves it to `canceled`. Either way, two records are written. **The cycle's stage history** gains an entry — this is the governed transition, and it is the same machinery every later status move uses: ```json { "from": "triage", "to": "draft", "at": "2026-08-12T09:41:07.220Z", "actor": { "type": "agent", "id": "triage-agent" }, "ruleSetVersion": "hlix-web-v1", "reason": "Adds a currency column and a backfill; the schema change is understood and reversible." } ``` **An audit row** is written as `cycle.triaged` or `cycle.canceled`, attributed to the Triage Agent, carrying the rationale as its reason and the lane, priority, and raw tiers as metadata. `lane`, `priority`, `stageHistory`, and `revision` are all fields on the cycle itself and are part of the [published contract](/api/reference/cycles/). The complexity and risk tiers behind the lane are deliberately **not**: they exist only in the audit record, which is restricted to workspace `owner` and `admin`. ### Reading a decision back Ask the project's orchestrator in chat — *"why was this cycle canceled?"* — and it will return the decision, its rationale, the lane, and the rule-set version. Owners and admins additionally get the complexity and risk judgement behind the lane. The same material is in the [audit log](/concepts/review-gates/#what-gets-recorded), filtered to that cycle. ## When the Triage Agent cannot answer Triage fails **open**. If the model call errors, the cycle is forwarded rather than stranded, with a rationale saying exactly that, and it lands in the `architecture-review` lane — which forces the human gate. A triage outage costs you an approval, never a lost request. That posture is chosen per signal, not globally. An unreadable *lane* does not gate on its own, because most cycles without one are simply older than the column. An unreadable *project* approval flag does gate, because an operator toggle whose state is unknown must be read as the safer answer. ## Choosing the model that triages Triage is its own orchestration role. Set the model for it under **Settings → AI models**, on the row labelled **Triage**, or over the API: ```bash curl -sS -X PUT "$HLIX_BASE_URL/v1/api/model-config/triage" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"modelIdentifier":"'"$MODEL_IDENTIFIER"'","temperature":0}' ``` `modelIdentifier` is required; `temperature` and `maxTokens` are optional. `DELETE` on the same path clears the override and returns the role to the deployment's default. Writes are restricted to `owner` and `admin`. ## If a cycle is not where you expect - **A cycle reads `canceled` and you did not cancel it.** Triage refused it. The rationale is the `reason` on its last stage-history entry, and on the `cycle.canceled` audit row. - **A cycle parked for approval on a project with the gate off.** Its lane forces one. The pending request carries the lane in its `context`. See [Approvals](/running/approvals/). - **`{"proposed":true,"adopted":false}` and no cycle appeared.** A proposal is not a cycle; the orchestrator did not fold it into the roadmap on that pass. See [Cycles](/running/cycles/#proposing-a-cycle). - **Two cycles dispatched in an order you did not expect.** Compare their `priority` values, not their creation times. A `normal` cycle created first still goes after an `urgent` one. - **A cycle has no `lane`.** It was created before triage ran on it, or triage has not completed yet. A null lane never forces a gate on its own. ## Where to go next | If you want to… | Read | | --- | --- | | See which lanes stop a dispatch | [Review & QA gates](/concepts/review-gates/#lanes-that-demand-a-human) | | Resolve the request a lane opened | [Approvals](/running/approvals/) | | Read a cycle's stage history over the API | [Cycles](/running/cycles/#stage-history) | | Watch how work actually flows through the stages | [Delivery metrics](/running/metrics/) | ## Next steps --- # Work tree How hlix structures work — Workspace, Project, Cycle, Task — and which API object, CLI command, and permission belongs to each level. The **work tree** is how hlix organizes everything you build: four levels, each nested in the one above, each mapping to a real object you can read over the API. ```text Workspace the tenant — your team, your billing, your audit trail └── Project a unit of intent — one product or client engagement └── Cycle a feature, release, or phase — the unit that EXECUTES └── Task one worker agent, one git worktree, one branch ``` ## When this matters to you - You cannot work out why a task will not run — execution belongs to the cycle level, not the task level. - You are wiring permissions and need to know what a client collaborator can reach — access is granted per project, not per task. - You are reading the API and want to know which id goes where. ## The four levels ### Workspace Your team's tenant: everyone you invite, every project, every agent, and the shared audit trail. One workspace is one organization, and it is the `X-Organization-Id` header on every API call. ### Project A unit of intent — one product, one client engagement, one initiative. A project owns exactly one [team of agents](/concepts/agents/), exactly one Coding Workspace, its repositories, its secrets, and its environment configuration. It is also the unit of access: a client collaborator is granted a project, and sees nothing outside it. ### Cycle A meaningful chunk of a project. **This is the level that executes.** The orchestrator plans a project into cycles and works them in sequence, replanning as results arrive. A cycle owns its task set, its QA pass, and its approval gate. Every cycle is created at the `triage` stage and admitted — or refused — by the [Triage Agent](/concepts/triage/) before any of that begins. It carries the classification that decision produced: a `lane`, a `priority`, and a stage history recording every status move it has made since. ### Task The atomic unit of execution: one worker agent, one git worktree, one branch, one outcome. Tasks fan out in parallel — they are what makes a wave a wave. A task never executes on its own; `POST /v1/api/tasks/:id/execute` returns `501` by design. ## The same tree, as objects The four levels nest: a Workspace contains Projects, a Project contains Cycles, and a Cycle contains Tasks — and each level is one API object. | Level | API object | Read it with | Who can change it | | --- | --- | --- | --- | | Workspace | Organization | `X-Organization-Id` header on every call | Workspace owner / admin | | Project | `/v1/api/projects/:id` | `hlix projects list` | Agency roles; clients get view or steer | | Cycle | `/v1/api/cycles/:id` | `GET …?projectId=…` | Agency roles | | Task | `/v1/api/tasks/:id` | `hlix tasks get ` | Agency creates; clients comment | ## A worked example You import an invoicing app and start a run with the brief *"add multi-currency invoices"*. ```text Workspace acme-agency (X-Organization-Id: org_…) └── Project acme-invoices (created by `hlix import .`) ├── Cycle "Currency model" status: pr_created │ ├── Task "Add currency to schema" → done │ └── Task "Migrate existing rows" → done └── Cycle "Invoice rendering" status: executing ├── Task "Format amounts by locale" → building └── Task "Update PDF template" → queued ``` The two tasks in "Currency model" ran at the same time, in separate git worktrees, against the same shared clone. Neither could see the other's edits until they merged. A task's branch name is **derived from the two ids**, never stored — `hlix/ms-/task-`. The `ms-` segment is a fixed literal in that template; it does not abbreviate anything you will see elsewhere in the product. So "Add currency to schema" lands on: ```text hlix/ms-0a9b8c7d-6e5f-4a3b-8c9d-0e1f2a3b4c5d/task-b4a1f0d23c77 ``` ## Branches and previews The tree maps onto your repository topology without forcing every project into one egress model. - **Repository-backed projects** get a task branch per task, pushed to the connected provider so your existing pull-request workflow applies unchanged. - **Repo-less projects** have nowhere to push, so hlix merges each successful task branch into the project's internal default branch under a lock, then snapshots both the deliverable and the full git history. The result is downloadable rather than pushed. Either way, cycle QA evaluates the collected work, and an optional [approval gate](/running/approvals/) can pause dispatch before a gated cycle proceeds. ## Where to go next | If you want to… | Read | | --- | --- | | Meet the team that walks this tree | [Agents & orchestration](/concepts/agents/) | | Know what decides whether a cycle exists at all | [Triage](/concepts/triage/) | | See where a task's worktree physically lives | [The Coding Workspace](/concepts/coding-workspace/) | | Create and run work at the right level | [Cycles](/running/cycles/) | ## Next steps --- # Authenticate Verify an hlix API key, select a workspace, and keep credentials out of shell history. Authentication has two parts: an API key proves who you are, and a workspace ID selects the tenant the CLI is allowed to act in. ## Prerequisites - A working [`hlix` installation](/getting-started/install/) - An hlix API key - The ID of a workspace you can access Create an API key in [Developer tools → API keys](https://app.hlix.ai/developer-tools/api-keys). That page also shows the active workspace ID with a copy button. A newly created key is displayed once, so save it in your password manager before closing the dialog. ## Sign in Pick the path that matches where the command runs. **Interactive login**, on a machine with a terminal: 1. **Start login with the workspace ID.** ```bash hlix auth login --workspace ``` 2. **Paste the API key into the hidden prompt.** The value is not echoed. 3. **Wait for verification.** The CLI makes an authenticated project-list request before storing anything. An invalid key or inaccessible workspace leaves no convincing saved login behind. Expected result: ```text Signed in to https://server.hlix.ai Credential stored at …/hlix/credentials.json ``` The credential file is written with owner-only permissions. Its location follows `HLIX_CONFIG_HOME`, then `XDG_CONFIG_HOME`, then `~/.config`. **In CI**, use secret environment variables instead of writing a credential file. There is **no login step** — the variables are the credential. ```bash export HLIX_API_KEY='…' export HLIX_WORKSPACE_ID='' hlix projects list --json ``` Expected result: a JSON envelope with `schemaVersion` and `data`. `HLIX_BASE_URL` is optional and defaults to `https://server.hlix.ai`. When `HLIX_API_KEY` is set, the CLI uses the environment credential instead of any saved file. That makes a CI override explicit and leaves a machine login untouched. **Against a self-hosted or local API**, point `--base-url` at your deployment: ```bash hlix auth login \ --workspace \ --base-url https://api.example.com ``` The URL must use HTTPS. Plain HTTP is accepted only for `localhost`, `127.0.0.1`, or `::1` — anything else exits `invalid_base_url` rather than sending a key in the clear. The stored credential remembers this origin, so later commands in that folder reach the same deployment without repeating the flag. ## Check which credential is in use ```bash hlix auth status ``` Expected result: ```text credential file (/Users/you/.config/hlix/credentials.json) api https://server.hlix.ai (from credential) workspace org_2p9xk4 (from credential) status valid ``` This names the credential's **source** — the environment or the stored file — and probes it against the API. It never prints the key, not even a prefix, because machine output gets piped into logs. When the probe fails, the failure code is the same one every other command would return, and the context (`credential from file, workspace …, api …`) goes to stderr. `hlix status` answers the wider question — which workspace this *folder* resolves to, and why. See [Where a command points](/reference/cli/). ## Sign out ```bash hlix auth logout ``` This removes the credential **file** only. It asks first; `--yes` skips the prompt, and a `--json` or non-interactive caller is refused with `approval_required` rather than defaulted to yes. A key supplied through `HLIX_API_KEY` is reported and left alone — it belongs to the shell that set it: ```text HLIX_API_KEY is set in this environment; unset it there to sign out. ``` ## If login fails - `--workspace is required`: pass the workspace ID or set `HLIX_WORKSPACE_ID`. - `missing_credential` — ``No API key entered. Run from a terminal or set HLIX_API_KEY for CI.`` Run the command in an interactive terminal, or set `HLIX_API_KEY` for CI. - `unauthenticated`: the key was rejected. Rotate or replace it, then retry. - `forbidden`: the key is valid, but the user cannot act in that workspace. - `invalid_base_url` — ``--base-url must use HTTPS (HTTP is allowed only for localhost).`` Use TLS, or a loopback hostname for local development. - `unreachable`: verify the API URL, DNS, TLS, and network path. - `workspace_mismatch`: you are inside a folder bound to a different workspace and passed a contradicting `--workspace`, `--base-url`, or `HLIX_*` value. `hlix auth login` is exempt from this — signing in to a second workspace from a bound folder works — but every other command refuses rather than guessing. - `approval_required` from `auth logout`: the caller cannot answer a prompt. Repeat with `--yes`. ## Next steps --- # How to use these docs The five page types, the conventions every page follows, and the machine-readable formats built for LLMs and agents. **These docs are written for two audiences: people, and the agents people point at them.** Both get first-class formats. This page explains the page types, the conventions each one follows, and how to feed the whole site to a model. ## Page types Every page is one of five kinds, and knowing which you are on tells you what to expect from it. | Type | Answers | Example | | --- | --- | --- | | **Tutorial** | "Walk me through this once" | [Quickstart](/getting-started/quickstart/) | | **Guide** | "How do I do this specific thing?" | [Import a project](/cli/import/) | | **Concept** | "What is this and when does it matter?" | [Agents & orchestration](/concepts/agents/) | | **Reference** | "What exactly does this accept and return?" | [CLI reference](/reference/cli/), [API reference](/api/reference/) | | **Status** | "What is true right now?" | [Availability](/releases/availability/), [Changelog](/releases/changelog/) | Reference pages are exhaustive and boring on purpose. Concept pages tell you *when* something applies before explaining how it works. If you want to get something done, start from a guide and follow its closing cards. ## Conventions every page follows - **The first sentence is a definition.** If you only read one line, it is that one. - **Prerequisites are a named section**, never buried in prose. - **Every command is followed by its expected output.** If you see something different, that difference is the bug. - **Every page ends with an "If … fails" section** carrying *verbatim* error strings mapped to recovery steps. Search this site for the exact message you saw — it is quoted somewhere. - **Every page closes with 2–4 cards** pointing at where to go next. ## For LLMs and agents Three formats, all generated on every build. | Format | What it is | Use it for | | --- | --- | --- | | [`/llms.txt`](/llms.txt) | An index of every page, with one-line descriptions | Letting a model choose what to read | | [`/llms-full.txt`](/llms-full.txt) | Every page's Markdown concatenated | One-shot ingestion of the whole site | | `.md` | The Markdown twin of any page | Fetching a single page cleanly | Any page's Markdown twin is its URL with `.md` appended: ```bash curl -sS https://docs.hlix.ai/reference/cli/auth.md curl -sS https://docs.hlix.ai/llms.txt ``` Expected result: clean Markdown with no navigation chrome, no cookie banner, and no HTML to strip. The **Copy as Markdown** and **Open in…** actions at the top of every page do the same thing from a browser — copy the page for pasting into a model, or hand it straight to ChatGPT, Claude, Cursor, or Copilot. ### Pointing an agent at these docs The MCP-connected route is the [official hlix MCP server](/mcp/), which gives an agent your actual workspace rather than the documentation. For the docs themselves, `llms-full.txt` in one fetch is usually enough: ```bash curl -sS https://docs.hlix.ai/llms-full.txt -o hlix-docs.txt ``` For a narrower context window, fetch `/llms.txt` first and let the model pick the handful of `.md` twins it needs. ## Finding things - **Search** (`/` or `Ctrl` `K`) indexes full page text, not just titles. - **A verbatim error string** is the fastest search there is — every failure section quotes them exactly. - **[Troubleshooting](/reference/troubleshooting/)** is symptom-first when you do not yet know which command owns your problem. - **[Glossary](/concepts/glossary/)** when a word is doing more work than its everyday meaning. ## If a page seems wrong - **A command behaves differently from its expected output.** Check [Changelog](/releases/changelog/) first; human output can change between releases while the JSON envelope stays fixed. - **A page describes a package you cannot install.** That is deliberate and marked — see [Availability](/releases/availability/) for what has actually shipped. - **An endpoint is missing from the API reference.** It is missing from the published contract, which means the SDKs cannot reach it either. [What is not here](/api/reference/#what-is-not-here) lists the known gaps. - **A `.md` twin still shows a component tag.** That is a build defect, not a documentation choice. The build is supposed to fail on it. - **The sidebar label and the page title disagree.** They are checked against each other; report it. ## Next steps --- # Install the CLI Install the hlix CLI, verify it, and know what to do when the install does not take. **The `hlix` CLI is the local entry point** for moving a project into a hlix workspace and keeping both copies in sync. ```bash npm install --global @hlix/cli hlix --version ``` Run it once without installing: ```bash npx @hlix/cli --help ``` In CI, pin the version instead of floating to `latest`: ```bash npm install --global @hlix/cli@0.2.0 ``` ## Prerequisites - macOS or Linux - Node.js 20 or later - Git - A hlix API key and workspace ID — create both in **Developer tools → API keys** ## Verify the install ```bash hlix --version hlix --help ``` Expected result: the first prints a semantic version, the second prints the registered command table. ## If installation fails - `hlix: command not found` right after a successful install: your global `npm` bin directory is not on `PATH`. Check `npm bin -g`, add it to your shell profile, then open a new shell. - `EACCES: permission denied` during a global install: npm is trying to write to a root-owned prefix. Set a user-writable prefix (`npm config set prefix ~/.npm-global`) rather than installing with `sudo`. - `SyntaxError` or an engine warning on startup: confirm `node --version` is 20 or later. - `npm error E404`: check the package name — `@hlix/cli` is the only CLI package. [Current versions](/releases/availability/) lists what the registry serves. - `hlix --version` prints an older number than you installed: an earlier binary is still first on `PATH`. `which -a hlix` shows which one wins. - `brew` cannot find the formula: the Homebrew tap is announced separately from npm. Use npm meanwhile. ## Next steps --- # Quickstart: ship your first task Import a project, start a run, watch an agent build, and read the verified diff it produced. This quickstart takes one local project from your machine to **agent output you can read** — an imported project, a running orchestrator, a task that changed files, and the verified diff behind it. **Video:** A 64-second walkthrough of the five steps on this page: hlix import binds a folder to a cloud project, a POST to the start endpoint hands a brief to the orchestrator, the dashboard shows one orchestrator and six worker agents, hlix tasks watch streams queued through done, and hlix tasks review prints the evidence document with filesChanged 3 and an approved evaluator verdict. The video is a dramatisation of the steps below — read them for the detail, and for anything the video moves past too quickly. ## Prerequisites - The [`hlix` CLI installed](/getting-started/install/) and [authenticated](/getting-started/authentication/) - A workspace ID and an API key from [Developer tools → API keys](https://app.hlix.ai/developer-tools/api-keys) - Git, and a project directory you are authorized to upload - Model access configured for the workspace — hlix calls models with your provider keys Export the two values once; every API step below reuses them. ```bash export HLIX_API_KEY='…' export HLIX_WORKSPACE_ID='' export HLIX_BASE_URL='https://server.hlix.ai' ``` ## The five steps 1. **Import the project.** Start with a dry run so you see exactly what leaves your machine. ```bash cd path/to/project hlix import . --dry-run hlix import . ``` Expected result: ```text Imported acme-invoices Project ID: 6f1c2a9e-8f0b-4a7d-9d33-2f0b1c7e5a41 Revision: 1 ``` Copy the project ID — the next step needs it. If the dry run reports blockers, resolve them before continuing; [Import a project](/cli/import/) lists each blocker and its fix. ```bash export PROJECT_ID='6f1c2a9e-8f0b-4a7d-9d33-2f0b1c7e5a41' ``` `hlix status` confirms the binding took, and is the command to reach for whenever you are unsure which workspace a folder actually points at: ```text folder /Users/you/acme-invoices workspace org_2p9xk4 (from project) api https://server.hlix.ai (from project) credential file (/Users/you/.config/hlix/credentials.json) project 6f1c2a9e-8f0b-4a7d-9d33-2f0b1c7e5a41 revision generation 1 (rev_01J8Z4) local 412 files, matches the last sync cloud generation 1, up to date ``` 2. **Start the run with a brief.** The brief is the whole instruction — the project orchestrator turns it into a roadmap, then into cycles and tasks. Write an outcome, not a file list. **In the dashboard:** open [app.hlix.ai](https://app.hlix.ai/), pick the imported project, and describe the outcome in the composer. It calls the same endpoint as the API path. **Over the API:** ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/projects/$PROJECT_ID/start" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"brief":"Add a health endpoint at GET /healthz that returns 200 with the build SHA, and cover it with a test."}' ``` Expected result: ```json {"started":true} ``` `started: true` means the run was handed to the engine, not that it has finished. The run proceeds asynchronously from here. 3. **Find the task the orchestrator created.** Planning takes a little time; the first task usually appears within a minute or two. ```bash hlix tasks list ``` Run from inside the imported folder, this needs no arguments — it defaults to the project this folder is bound to. Expected result: ```text ID STATUS TITLE ------------------------------------ -------- ------------------ b4a1f0d2-3c77-4e1a-9a2b-1d5e6f7c8a90 queued Add GET /healthz ``` `No tasks.` means planning has not produced any yet. Wait and re-run it rather than starting a second run. ```bash export TASK_ID='b4a1f0d2-3c77-4e1a-9a2b-1d5e6f7c8a90' ``` 4. **Watch the agent work.** `hlix tasks watch` consumes the server's status stream and exits when the task reaches a terminal status. ```bash hlix tasks watch "$TASK_ID" ``` Expected result — one line per status change, ending at a terminal status: ```text queued building testing reviewing done ``` Add `--json` to get one envelope per line (JSONL) instead, which is what automation should read. 5. **Read what it delivered.** This is the point of the walkthrough — the verified evidence behind the change, the same observation the QA evaluator reviewed. ```bash hlix tasks review "$TASK_ID" ``` Expected result — the evidence document, abridged: ```json { "taskId": "b4a1f0d2-3c77-4e1a-9a2b-1d5e6f7c8a90", "source": "github-compare", "observedAt": "2026-08-07T09:14:22.481Z", "baseRef": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d", "headRef": "hlix/ms-0a9b8c7d-6e5f-4a3b-8c9d-0e1f2a3b4c5d/task-b4a1f0d23c77", "headSha": "9f2c1b0ae4d5768a3c2f1e0d9b8a7c6d5e4f3a2b", "comparison": { "status": "ahead", "aheadBy": 2, "behindBy": 0 }, "filesChanged": 3, "additions": 74, "deletions": 1, "commits": 2, "files": [ { "path": "src/routes/healthz.ts", "status": "added", "additions": 41, "deletions": 0, "patch": "@@ -0,0 +1,41 @@\n+…" } ], "evaluator": { "verdict": "approved", "summary": "Endpoint added with a passing test.", "stale": false } } ``` That `files[].patch` is the diff an agent produced, read back from the repository host — not a summary the agent wrote about itself. Where a change is decisive, [Reviewing output](/running/review/) explains how to comment on a line and send it back. ## What just happened ```text hlix import . your folder ──▶ a cloud Project (revision 1) POST /projects/:id/start your brief ──▶ the project's Orchestrator Orchestrator │ plans a roadmap ▼ Cycle │ dispatches a wave of tasks ▼ Task ──── one worker agent, its own git worktree and branch │ commits, then reports the result back ▼ QA evaluator ──▶ the review evidence you just read ``` One orchestrator ran for the project, a worker agent did the coding in its own git worktree inside the project's [Coding Workspace](/concepts/coding-workspace/), and the QA evaluator judged the result before you saw it. Nothing about that loop is per-task infrastructure: the same sandbox is reused by every task this project ever runs. ## If a step fails - ``No credential. Run `hlix auth login`, or set HLIX_API_KEY for CI.`` — the CLI has no stored login. See [Authenticate](/getting-started/authentication/). - ``This folder is already bound to an Hlix project. Use `hlix push`.`` — the folder was imported before. Use [`hlix push`](/cli/sync/) to send a new revision instead of importing again. - ``Import needs approval for protected files or cloud setup. Review with `hlix import --dry-run`, then repeat with `--yes`.`` — non-interactive import refuses to approve protected data on your behalf. - `workspace_mismatch` from any `hlix` command — a `--workspace`, `--base-url`, or `HLIX_*` value contradicts the folder's binding. Drop the override; `hlix status` shows which source decided what. - `{"error":"Project run already in progress","status":"running"}` — a run is already going. Watch that one; a second `start` is refused rather than queued. - `{"error":"Project not found"}` on `start` — the project ID is wrong, or belongs to another workspace. Check `X-Organization-Id`. - `{"error":"No active organization"}` — the `X-Organization-Id` header is missing and the API key resolved no default tenant. - `{"error":"No review evidence: this task has no branch (it was never dispatched through a cycle)"}` — the task exists but never ran. A task created on its own does not execute; see [Tasks](/running/tasks/). - An empty task list that stays empty — read the project's activity in the dashboard. A run that failed during planning reports there, not on the task list. ## Next steps --- # Connect your agents The coding agents hlix can drive, how one is chosen for a task, and where your provider keys and model choices come from. **A coding agent — a *harness* — is the program that actually edits your code inside a task.** hlix ships its own, **Hlix**, and runs it when a task names nothing else. It also drives the coders you already use — Claude Code, Codex, Cursor Agent — through the same pipeline, with the same isolation, context, and QA around them. That choice is per task, and it is genuinely yours: bring the coder you want, and the orchestration around it does not change. Swapping the harness changes who writes the code; it changes nothing else. ## Prerequisites - A project in your workspace ([import one](/cli/import/), or create one in the dashboard) - A provider API key for the harness you intend to use, stored as a [workspace provider config](#your-keys-your-models) - Agency `owner` or `admin` to change the workspace-wide harness; `manage` access on a project to change that project's ## The harness matrix Six harnesses are defined. **Availability is a property of this deployment**, not a plan tier: an unavailable harness has no adapter here, and hlix refuses to substitute a different one for it. | Harness | Driver | Credentials | Model choice | Available | | --- | --- | --- | --- | --- | | `hlix` | native, backend-driven | none of its own | from the orchestration catalog | **Yes** — the default | | `claude-code` | ACP driver, in-sandbox | `ANTHROPIC_API_KEY` | per run | **Yes** | | `codex` | ACP driver, in-sandbox | `OPENAI_API_KEY` | per run | **Yes** | | `cursor-agent` | ACP driver, in-sandbox | `CURSOR_API_KEY` | fixed by the CLI | **Yes** | | `openai-agents` | ACP driver, in-sandbox | any one of OpenAI, Anthropic, Google, OpenRouter | per run | No adapter yet | | `aider` | ACP driver, in-sandbox | any one of OpenAI, Anthropic, Google, OpenRouter | per run | No adapter yet | `claude-code`, `codex`, and `cursor-agent` report structured results *and* usage, though `cursor-agent` takes no model selection. `aider` reports a structured result without usage. `hlix` reports a structured result and takes its model from the orchestration catalog's `worker` role rather than from a per-run coder selection. ## Two driver kinds The `Driver` column above is the one distinction that changes how a task executes. **Hlix (native).** hlix runs its own agent from the backend against the Coding Workspace's command and filesystem tools. The backend provisions the worktree first, wraps the turn so it survives a backend restart, and owns the git work itself — the agent only edits files; the runner stages, commits, and pushes. Because hlix drives the turn directly, this is the harness whose reasoning you can watch live and whose conversation is recoverable afterwards. It takes no coder credential of its own; its model is the `worker` role from the orchestration catalog. **ACP driver (external coder).** hlix launches an in-sandbox driver process, which dispatches the real coder CLI — `claude`, `codex`, `cursor-agent` — inside the project's [Coding Workspace](/concepts/coding-workspace/). The driver creates the task's git worktree in-pod, runs the coder there, commits, and for a repository-backed project force-pushes the task branch. The coder's exit code is the completion signal; there is no polling. Per-task isolation is real: each task gets its own configuration directory, and automatic memory carry-over between tasks is disabled. Both paths land in the same place: a task branch inside one persistent per-project sandbox, one git worktree per task, merged and reviewed by the same pipeline. ## How a harness is chosen One cascade decides, in one pure function, so the answer cannot differ between the settings screen and the dispatcher. The first rung that names a harness wins. | # | Rung | Set by | `source` | | --- | --- | --- | --- | | 1 | Per-dispatch override | the deployment's `SANDBOX_AGENT` environment variable | `task-override` | | 2 | The task's assigned agent | `hlix agent import --runtime …`, or an agent record's runtime | `agent` | | 3 | Project preference | `PUT /v1/api/model-config/harness/:projectId` | `project-preference` | | 4 | Workspace preference | `PUT /v1/api/model-config/harness` | `organization-preference` | | 5 | Inferred from the planner model | a non-Anthropic `planner` model preference | `planner-model` | | 6 | System default | nothing above matched | `system-default` → `hlix` | Availability is **not** consulted while resolving. Resolution answers "what did the configuration ask for"; the availability check answers "can this deployment run it". Folding them together would let an unavailable preference fall silently through to the next rung. ### Pin a harness Workspace-wide (`owner` or `admin` only — it names the coder for every project): ```bash curl -sS -X PUT "$HLIX_BASE_URL/v1/api/model-config/harness" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"harness":"codex"}' ``` For one project (requires `manage` access; client collaborators are refused): ```bash curl -sS -X PUT "$HLIX_BASE_URL/v1/api/model-config/harness/$PROJECT_ID" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"harness":"claude-code"}' ``` `DELETE` on either path clears that rung, so the project inherits the workspace's setting and the workspace falls back to the default. Both writes are recorded in the [audit log](/reference/cli/) as `harness_preference.changed`. Only an **available** harness can be stored. A preference naming `aider` or `openai-agents` is rejected at the API with a validation error rather than saved as a setting whose only effect would be to fail every future dispatch. ### Ask what would actually run ```bash curl -sS "$HLIX_BASE_URL/v1/api/model-config/harness/$PROJECT_ID/resolved" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` Expected result: ```json { "harness": "codex", "source": "project-preference", "scopeId": "6f1c2a9e-8f0b-4a7d-9d33-2f0b1c7e5a41", "scopeFallback": true } ``` `source` is the point — a bare `"codex"` would send you hunting through four settings. `scopeFallback: true` is honest bookkeeping: this endpoint answers for the two rungs a settings screen can change, plus the default beneath them. It knows nothing about a dispatch override or a task's assigned agent, because neither exists until a task is dispatched. ## Your keys, your models hlix calls models with **your** provider keys. Store one per provider: ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/providers" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"provider":"anthropic","apiKey":"sk-ant-…","enabled":true}' ``` Accepted providers are `anthropic`, `openai`, `openrouter`, `google_genai`, and `local`. The key is verified against that provider's model-list endpoint before it is trusted, stored encrypted, and never returned by any read endpoint. `PATCH /v1/api/providers/:provider` rotates it. `local` additionally requires `baseUrl`, so a self-hosted gateway can be pointed at. Separately, `/v1/api/model-config/:role` sets which model each orchestration role uses — `classifier`, `interviewer`, `planner`, `reviewer`, `chat`, `orchestrator`, `worker`, `triage` — with optional `temperature` and `maxTokens`. The dashboard exposes the same list under **Settings → AI models**, one row per role. `triage` is the model that judges whether an incoming request becomes a cycle and which risk lane it enters; see [Triage](/concepts/triage/). **How a harness gets its credentials** depends on its driver: - `claude-code` receives the workspace's **Anthropic** configuration, including a custom base URL when one is set. - `codex`, `cursor-agent`, and the other external coders receive credentials normalised to the OpenAI-compatible protocol, resolved from the provider named by your **`planner` model preference**. Pointing `planner` at an OpenRouter identifier is therefore what routes a non-Claude coder through OpenRouter. - `hlix` uses no coder credential of its own; its model comes from the orchestration catalog for the `worker` role. ## Let your local agent operate hlix The [official hlix MCP server](/mcp/) points the other way: it connects Cursor, Claude Code, Codex, or Windsurf on your machine to the same workspace-scoped projects, tasks, cycles, comments, and review evidence the CLI exposes. It is read-only unless you explicitly enable its bounded additive tools. That is the outbound direction. Importing a third-party MCP server *into* a cloud project is a separate, inbound flow — see [Import skills, agents & MCP](/cli/resources/). ## Common questions **Can I run a different harness per project?** Yes — that is rung 3 of the cascade. `PUT /v1/api/model-config/harness/:projectId` sets it for one project; the workspace-wide setting is the fallback beneath it. **Why did hlix refuse instead of falling back to a harness that works?** Because substituting a coder you did not choose is a correctness failure you cannot see in the output. The refusal names the setting that selected the unavailable harness, so there is one field to change. **Does hlix see my provider keys?** It stores them encrypted, uses them to call models on your behalf, and never returns them from any read endpoint. Verification happens against the provider's own model-list endpoint before a key is trusted. **Which key does a non-Claude coder use?** The one belonging to the provider named by your `planner` model preference — those coders receive credentials normalised to the OpenAI-compatible protocol. `claude-code` is the exception: it always receives the workspace's Anthropic configuration. **What runs if I never choose a harness?** `hlix`. A task that names no agent runs on our own harness, which needs no coder credential of its own. **Why would I pick an external coder over `hlix`?** Because you already rely on one. If your team's standards, prompts, or muscle memory are built around Claude Code or Codex, pin it and keep them — the isolation, review evidence, and QA around the task are identical either way. That is the point of the choice being per task. **What does `hlix` do that an external CLI cannot?** hlix drives its turn directly rather than shelling out to a coder process, so its reasoning streams live into the worker card and its prompts and answers are readable afterwards in a cycle's Transcript. It is also the only harness where hlix owns the git operations rather than the in-sandbox driver. ## If a task will not start - ``Cannot run this task: … names the harness "aider", which has no adapter in this deployment.`` — clear or change the setting the message names. Do not expect a fallback. - The dispatch fails with a provider `401` — the workspace has no key for the provider that harness needs. `claude-code` needs Anthropic; the other external coders follow the `planner` preference's provider. - `403` — ``Changing the workspace harness is restricted to owners and admins.`` The workspace-wide setting names the coder for every project, so it is agency administration. - `403` on a project harness write — a client collaborator cannot change which coder runs a project, even with steer access. - A validation error storing a preference — the harness is one of the two without an adapter. The API refuses to store a setting that could only fail later. - `404` — ``Project not found.`` The project ID belongs to another workspace, or does not exist. Check `X-Organization-Id`. ## Next steps --- # FAQ Common questions about hlix — how it relates to coding agents, who owns your code, security, and pricing. **The questions people ask before they read anything else.** For a symptom you are actually hitting, go to [Troubleshooting](/reference/troubleshooting/) instead — this page is about what hlix is, not about what went wrong. Feature-specific questions live with their feature: [tasks](/running/tasks/#common-questions), [the Coding Workspace](/concepts/coding-workspace/#common-questions), [connecting agents](/guides/connect-agents/#common-questions), and [import](/cli/import/#common-questions). ## Is hlix another coding agent? hlix is the control plane *over* coding agents, and it ships one of its own — **Hlix** — as the default so you can start without wiring anything up. It orchestrates Claude Code, Codex, and Cursor Agent through exactly the same pipeline, chosen per task. The product is the layer, not the coder: the roadmap, the triage, the isolation, the review evidence, and the QA are identical whichever harness writes the diff. So bringing the agent your team already trusts costs you nothing, and it is not a downgraded path. [Connect your agents](/guides/connect-agents/) lists exactly which ones run today. ## Do I have to switch tools? No. hlix plugs into the agents and stack you already run — your repos, your models, your delivery tools. See [Integrations](/integrations/). ## Who owns my code and prompts? You do. hlix runs work through your own provider keys and your own repos. It orchestrates the work; it doesn't claim ownership of it. ## How does hlix keep agents from breaking client work? Three ways. First, [triage](/concepts/triage/) — every request is classified on complexity and risk before it becomes work, and the riskiest classifications require a human sign-off before any agent is dispatched. Second, isolation — every project has a durable Coding Workspace, and each task receives its own Git worktree and branch inside it. Third, [review gates](/concepts/review-gates/) — every cycle receives automated QA. Projects are autonomous by default; teams can additionally require human approval on every cycle when their policy calls for it. ## Can agents run in parallel? Yes. That's the point of the [team model](/concepts/agents/): one orchestrator dispatches many worker agents at once, each isolated, so a project's tasks fan out instead of queuing. ## Is there an audit trail? Security-sensitive mutations write structured audit events with actor, resource, timestamp, outcome, and request correlation. Compliance-critical mutations commit state and audit event in one database transaction. This supports review and incident investigation without claiming append-only or tamper-evident guarantees that the current data plane does not provide. ## Is hlix for solo developers or teams? Both work, but it's built for agencies and teams running AI on client work at scale — where orchestration, consistency, and review matter most. ## Can I install the CLI, SDK, or MCP server yet? Not from a public registry. All three are implemented and tested; what is missing is the verified public distribution. [Current versions](/releases/availability/) is the single page that reports the published state of each package and the gate it still has to pass. ## How do I get access? Request early access. We're onboarding founding agencies now, with founder-led setup. ## Next steps --- # Move an existing project to hlix Move a local project into hlix, verify it in the workspace, and keep both sides synchronized. This path moves the project in your current directory into the authenticated hlix workspace without executing repository setup commands on your machine. ## Prerequisites - The [`hlix` CLI installed](/getting-started/install/) - A completed [`hlix auth login`](/getting-started/authentication/) - Git installed - A project directory you are authorized to upload 1. **Enter the project.** ```bash cd path/to/project ``` 2. **Inspect the import plan.** ```bash hlix import . --dry-run ``` Review the stack, file and byte counts, active environment files, secret-key count, environment confidence, quarantined resources, history findings, and any blockers. Secret values are never printed. 3. **Import the project.** ```bash hlix import . ``` Review the trust summary and confirm. Import also initializes the folder. It creates the cloud project, uploads a verified Git bundle, sends protected files through the encrypted path, and records the first immutable revision. In non-interactive automation, review a dry run first and then use `hlix import . --yes`. 4. **Verify the local binding.** ```bash test -f .hlix/config.json test -f .hlix/state.json hlix projects list ``` Expected result: the imported project appears in the table. Refreshing the web app shows the same project in the selected workspace. 5. **Make and push a change.** ```bash # edit and test the project first hlix push ``` Expected result: `push complete.` A new cloud revision becomes the project head only if the local base still matches the cloud base. ## What hlix detected The scan builds a declarative environment plan from repository evidence such as: - `.cursor/environment.json` - `.devcontainer/devcontainer.json` or `devcontainer.json` - `.gitpod.yml` - Docker and Compose files - `package.json` and lockfiles - Python, Go, Procfile, and tool-version files - environment-variable references and private ports Detection is read-only. Install, build, start, and terminal commands are recorded for review; they are not executed by the local scan. Approved cloud setup may run later inside the isolated Coding Workspace. See [Environment discovery](/cli/environment-discovery/) for the detector order, protected credential sources, and the established tooling patterns behind this model. ## If the dry run blocks Do not bypass a blocker until you understand it. - **Potential secrets in Git history:** remove and rotate them. `--history preserve` is an explicit choice to keep that history; it does not make exposed credentials safe. - **Incomplete history scan:** repositories above the scan limit require an explicit `--history preserve` decision. - **Symlink:** this release refuses symlinks rather than following a path outside the selected root. - **Nested Git repository:** import each repository independently. - **Ambiguous dotenv selection:** pass `--env-file ` after reviewing which environment belongs in cloud development. - **Size limit:** exclude generated or unnecessary data with `.hlixignore`. ## Next steps --- # Recipe: onboard a client repository Take a codebase you were handed, get it into a workspace safely, and ship the first reviewed change. **The scenario:** a client hands you a repository. You have never run it, you do not know what is in its history, and you need to be delivering against it this week — without their credentials ending up somewhere they should not be. This recipe is the safe path, in the order the risks actually arrive. ## Prerequisites - A hlix workspace and an API key - The client's repository checked out locally - Agency `owner` or `admin` if you will also connect GitHub ## The recipe 1. **Look before you upload.** The first command is not an import. ```bash cd path/to/client-repo hlix import . --dry-run ``` Read three things in the report: **history findings**, **protected files**, and **quarantined resources**. A client repository is exactly where a committed `.env` from two years ago shows up. 2. **Deal with what the scan found — do not override it.** If history findings appear, the credential is in the Git history and anyone with that history can recover it. `--history preserve` uploads the history *despite* the findings; it does not remove them. Rotate first, with the client, then re-scan. ```bash hlix import . --dry-run --history abort-on-findings ``` Expected result: no blocking findings. That is the gate for continuing. 3. **Import for real.** ```bash hlix import . --name "Acme — storefront" ``` Expected result: ```text Imported Acme — storefront Project ID: 6f1c2a9e-8f0b-4a7d-9d33-2f0b1c7e5a41 Revision: 1 ``` Protected files travelled the encrypted path and are out of the Git bundle. Quarantined resources — any `AGENTS.md`, skill, or `.mcp.json` the client's repo carried — are preserved but **inert**. 4. **Decide what you trust.** The client's repository may contain agent instructions written by someone else. They do nothing until you activate them, and you should read them before you do. ```bash hlix agent import AGENTS.md --name "Acme builder" ``` Skip this step entirely if you would not hand those instructions to a contractor. 5. **Attach it to the client account** in the dashboard, under **Customers**, so the project appears in that client's grouping and their portfolio rolls up correctly. 6. **Ship one small thing first.** Not the big refactor. Pick a change whose correctness you can judge at a glance, so the first review tells you whether the setup is right rather than whether the change is right. Start the run from the dashboard, or over the API: ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/projects/$PROJECT_ID/start" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"brief":"Add a health endpoint at GET /healthz returning 200 with the build SHA, and cover it with a test."}' ``` 7. **Read the evidence, not the summary.** ```bash hlix tasks list hlix tasks review ``` The `files[].patch` is the diff read back from the repository host — an observation, not the agent's account of itself. ## What this buys you - The client's secrets never entered a commit, and you found the historical ones before they became your problem. - Someone else's agent instructions did not silently start steering your delivery. - The first change was small enough that a bad result told you about your *setup*. ## If the recipe stalls - `scan_blocked` on a repo the client swears is clean — the blocker list names each finding. See [If the scan blocks the import](/cli/import/#if-the-scan-blocks-the-import). - `already_imported` — the folder is bound from a previous attempt. Use [`hlix push`](/reference/cli/sync/), or delete the cloud project and re-import. - The first task never runs — a task must come from a cycle. A project run creates them; a standalone task does not execute. See [Tasks](/running/tasks/). - Review evidence says it cannot verify the branch — the project has no connected remote yet. Connect GitHub, or read the downloadable workspace archive instead. ## Next steps --- # Recipe: parallel cycle delivery Run many tasks at once against one codebase without collisions, and know what to do when two of them touch the same file. **The scenario:** a cycle decomposes into eight tasks. You want them running at once, not queued — and you want to know, before you start, what happens when two of them edit the same file. The short answer: they cannot corrupt each other, and a genuine collision fails exactly one task. This recipe explains why, and what to do about the failure. ## Prerequisites - An imported project with at least one cycle - Model access configured for the workspace — see [Connect your agents](/guides/connect-agents/) ## Why parallel is safe here Three properties, and they are structural rather than best-effort: | Property | What it means | | --- | --- | | **A worktree per task** | Every task gets its own git working tree off the project's shared clone. Two tasks never share a checkout. | | **One sandbox per project** | All of those worktrees live inside the project's single [Coding Workspace](/concepts/coding-workspace/) — no per-task cold boot, no per-task clone. | | **Per-task outcomes** | The wave collects results independently. One task failing does not cancel its siblings. | Merging is where parallelism actually meets: task branches merge into the default branch under a **per-repository lock**, so merges serialise even though the work did not. ## The recipe 1. **Let the planner decompose.** Start the project run with an outcome, not a task list — the roadmap becomes cycles, and each cycle's planner produces the wave. ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/projects/$PROJECT_ID/start" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"brief":"Add tenant-scoped rate limiting across the public API, with tests and updated docs."}' ``` Expected result: `{"started":true}` — the run was handed to the engine, not finished. 2. **Watch the wave, not one task.** Filter the task list by status to see the shape of the wave: ```bash hlix tasks list --status building ``` ```text ID STATUS TITLE ------------------------------------ -------- ------------------------ b4a1f0d2-3c77-4e1a-9a2b-1d5e6f7c8a90 building Add rate-limit middleware c7d2e1a3-4b88-4f2b-8c3d-2e6f7a8b9c01 building Cover limits with tests ``` 3. **Follow one task when you care about it.** ```bash hlix tasks watch "$TASK_ID" ``` The stream is bounded — roughly 20 minutes — and closing is the only terminal signal. A clean exit at a non-terminal status means reconnect, not finished. 4. **Review per task, not per cycle.** Each task carries its own evidence, and a cycle is approved by its tasks passing: ```bash hlix tasks review "$TASK_ID" ``` 5. **Handle the one that failed to merge.** If a task ends `failed` with a merge conflict, its siblings are unaffected and already merged. Re-dispatch that task; its base is now the merged result, so the conflict that existed against the old base usually does not exist against the new one. ## Keeping waves clean - **Brief for outcomes, not files.** "Add rate limiting to the public API" decomposes into non-overlapping work; "edit `server.ts` and `limits.ts`" invites two tasks into the same file. - **Expect the reviewer role to touch broadly.** A QA or Security Engineer task often reads widely. That is fine — reads never collide. - **Do not scale a wave to hide a slow harness.** Parallelism is bounded by your provider's rate limits; twelve tasks against a throttled key finish no sooner than four. - **Watch the merge lock, not the CPU.** Merges serialise per repository. In a multi-repository project each repository's lock is independent, so two repositories merge concurrently. ## If the wave misbehaves - **Tasks stay `queued` and never start.** The dispatch refused a harness. See [If a task will not start](/guides/connect-agents/#if-a-task-will-not-start) — a preference naming an unavailable coder fails loudly rather than substituting one. - **One task `failed`, the rest are `done`.** Expected on a genuine conflict. Read that task's error, re-dispatch it. - **Every task failed identically.** Not a collision — a shared cause, usually a missing provider key or a project secret that never made it into the environment. See [If a secret does not reach the workspace](/security/secrets/#if-a-secret-does-not-reach-the-workspace). - **`hlix tasks watch` exits while work continues.** The stream is bounded. Reconnect; read the status field rather than the exit code. - **The cycle will not execute at all.** See [If a cycle will not execute](/running/cycles/#if-a-cycle-will-not-execute). ## Next steps --- # Introduction hlix runs coding agents in the cloud — its own harness by default, or Claude Code, Codex & Cursor Agent — with automated QA, human approval, and durable project context. **hlix lets your agency scale output without scaling headcount.** It runs coding agents in the cloud — its own harness, **Hlix**, out of the box, or [Claude Code, Codex, and Cursor Agent](/guides/connect-agents/) when you would rather bring your own — orchestrating them, applying automated QA, and pausing for human approval when the work is risky enough to need it. The operating layer is the product, not the coder. hlix coordinates agents in parallel, keeps every task isolated, triages what is worth building, and gives each project a reviewable delivery path — and none of that changes with the harness you pick. Start on Hlix; swap in the coder your team already trusts whenever you want. ## Why hlix exists Service work scales by hiring — output is bolted to headcount, and good people are slow to find and harder to keep. AI agents could break that coupling, but raw they drift, nothing gets reviewed, and every developer babysits a different tool. hlix replaces that with one operational backbone: Every request is triaged into a risk lane before work starts. One orchestrator per project then plans the roadmap and dispatches a team of worker agents — across cycles and tasks, in parallel. hlix learns your decisions, edits, and standards over time and delivers more like you every day — a per-agency asset that compounds and that no model maker can own. Every cycle receives automated QA. Projects are autonomous by default; enable human deployment approval where your delivery policy needs it. Branch, preview, and deploy from one place, with structured events recording security-sensitive actions and outcomes. ## Start here ## Built for LLMs too Every page here can be copied as clean Markdown or opened directly in ChatGPT, Claude, Cursor, or Copilot — use the actions at the top of any page. Any page's Markdown twin is its URL with `.md` appended. --- # Integrations The systems hlix connects to — GitHub, Linear, Slack, and Telegram — what each one does, and what it does not. **An integration connects hlix to a system your team already runs**, so work arrives and results land where people are already looking. Four exist today, and each has its own page with prerequisites, connect steps, and failure modes. ## Prerequisites - A hlix workspace, and agency `owner` or `admin` — connecting and revoking an installation is workspace administration - Administrative access on the other side: a GitHub organization or account, a Linear workspace, or a Slack workspace. Telegram needs no access of its own — a chat joins with a one-time link code ## What each one does | Integration | Direction | hlix reads | hlix writes back | | --- | --- | --- | --- | | [GitHub](/integrations/github/) | outbound | repositories and branches you grant | task branches, commits, pull requests | | [Linear](/integrations/linear/) | inbound + outbound | the issue that triggered a session | agent activities in the session thread | | [Slack](/integrations/slack/) | inbound + outbound | DMs and `@mentions` in channels the bot is in | the agent's replies, as Block Kit | | [Telegram](/integrations/telegram/) | inbound + outbound | messages in a linked chat | the agent's replies in the chat | ## What is not an integration Two things that look adjacent and behave differently: - **Importing a local project** needs no connection at all. The CLI discovers local environment configuration, preserves Git history, encrypts protected files, and creates the project directly in your workspace. Start at [Import a project](/cli/import/). - **MCP servers** are a project resource, not a workspace integration. A project scan preserves MCP configuration but does not activate it; you import one reviewed server explicitly with `hlix mcp import`. See [Import skills, agents & MCP](/cli/resources/). The [official hlix MCP server](/mcp/) points the other way — it lets a local coding agent operate hlix. ## Building your own The [OpenAPI 3.1 contract](/api/openapi/) covers direct integrations and reproducible code generation. Above it sit the [TypeScript SDK](/sdk/typescript/) and the source-preview [Python](/sdk/python/) and [Go](/sdk/go/) clients. ## If an integration will not connect - `403` — ``admin only`` on a revoke, or on any installation write. Connecting and disconnecting is `owner`/`admin` work. - A callback that redirects back with `?error=…` — the query parameter names the stage that failed. Each page below maps its own values. - An empty installation list for a user who can see the workspace — client collaborators are denied installation rows entirely, by a database policy rather than by the route. - `500` — ``Linear OAuth not configured`` / ``GitHub App not configured``, or `503` — ``Slack is not configured on this server``. The deployment is missing that integration's credentials; this is an operator fix, not a user one. - A chat integration that connects but never answers — the chat agent runs with the *workspace's* identity, not the sender's. Neither Slack nor Telegram maps a platform account to a hlix user; membership of the workspace or chat is the whole authorization. ## Next steps --- # GitHub Install the hlix GitHub App, grant repositories, and get a branch, commits, and a pull request per task. **The GitHub integration gives hlix a place to push task branches and open pull requests.** It is a GitHub App installed on your organization or account, granting hlix access to the repositories you select — nothing more. Without it, a project still runs: task branches are merged inside the project's own [Coding Workspace](/concepts/coding-workspace/) and the result is downloadable. GitHub adds the external review surface your team already uses. ## Prerequisites - Agency `owner` or `admin` in the hlix workspace - Permission to install a GitHub App on the target organization or account. Without it, GitHub records your install as a **request** and an organization owner must approve it. - A repository you want hlix to work in ## Connect it 1. **Start the install.** From the dashboard's integrations screen, or directly: ```bash curl -sS -i "$HLIX_BASE_URL/v1/api/integrations/github/install" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` Expected result: a `302` to `github.com`, carrying an HMAC-signed state that binds the install to your workspace and user. The redirect is the whole point — do not follow it with a script. 2. **Choose the account and the repositories.** GitHub asks whether to grant **all** repositories or a **selected** list. Prefer selected; you can add more later without reinstalling. 3. **Land back on hlix.** The callback exchanges the installation ID, reads the installation's account, repository selection, permissions, and subscribed events, and persists one row for this workspace. Expected result: a redirect to `…/integrations?installed=1&provider=github`. If an organization owner still has to approve, the flag is `pending=1` instead, and the row exists but the installation is not usable yet. 4. **Verify.** ```bash curl -sS "$HLIX_BASE_URL/v1/api/integrations/github" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` Then list what it can actually see: ```bash curl -sS "$HLIX_BASE_URL/v1/api/integrations/github/$INSTALL_ID/repositories" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` Expected result: the repositories you granted. An empty list with a healthy installation means the selection did not include what you expected — change it on GitHub, not in hlix. ## What hlix writes back Per task, against the repository the project is connected to: | Write | When | | --- | --- | | A task branch off the project's base branch | when the task's worktree is created; an existing branch is adopted rather than duplicated | | Commits on that branch | as the coding agent works; the driver force-pushes the task branch | | A pull request | on the cycle's delivery path, targeting the base branch | | A merge of a task branch into the base | on the merge path, when the project is configured for it | Branch creation is idempotent: a lost race that returns GitHub's `422 Reference already exists` is treated as success, because the branch being there is all the caller wanted. hlix does not rewrite your history, force-push your default branch, or touch a branch it did not create. ## Permissions and scopes The installation's granted permissions and subscribed events are read from GitHub at callback time and stored with the row, so what hlix believes it can do always comes from GitHub rather than from a hard-coded list. Inspect them on the installation record, or on GitHub's installation settings page. Two consequences worth knowing: - **Repository selection is GitHub's, not hlix's.** Adding a repository is a change you make on GitHub; hlix picks it up through the `installation_repositories` webhook. - **Suspension is honoured.** A suspended installation answers `409 installation suspended on github` rather than failing deep inside a task. ## Disconnect ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/integrations/github/$INSTALL_ID/revoke" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` `owner` or `admin` only. This marks the hlix-side row revoked. Uninstalling the app on GitHub is the other half, and the one that actually removes hlix's access — do both. ## If GitHub does not connect - `500` — ``GitHub App not configured``. The deployment has no GitHub App credentials. Operator fix. - Redirect with `error=github_missing_installation_id` or `github_missing_state` — the callback was reached without GitHub's parameters. Start the flow again from `/install` rather than opening the callback URL directly. - Redirect with `error=github_invalid_state_…` — the signed state failed verification or expired. Restart the install. - Redirect with `error=github_installation_lookup_failed` — hlix could not read the installation back from GitHub. Usually a revoked or suspended app; check GitHub's installation page. - `pending=1` after install — an organization owner must approve the request. Nothing works until they do. - `409` — ``installation suspended on github``. Unsuspend it on GitHub. - `502` — ``failed to list repositories``. GitHub refused the listing; the installation may have been uninstalled since the row was written. - `Branch "main" not found in owner/repo. Check that the branch exists and the GitHub token has access.` — the project's base branch does not exist in that repository, or was not granted. - A webhook arriving before the callback persisted its row is **acked, not failed** — the callback is the canonical writer, and the webhook retries state changes only. ## Next steps --- # Linear Install the hlix agent in a Linear workspace, then assign or mention it on an issue and read its replies in the session thread. **The Linear integration installs hlix as an agent in your Linear workspace.** Assign it an issue or mention it, and it opens an *agent session* — a thread in which it reports what it is doing and what it decided, and in which you can steer or stop it. This is the only integration that both receives work and reports progress on the same surface. ## Prerequisites - Agency `owner` or `admin` in the hlix workspace - Permission to authorize an OAuth application in the target Linear workspace - At least one hlix project the issue can be resolved to — see [what it needs to key on](#what-it-needs-from-an-issue) ## Connect it 1. **Start the install.** From the dashboard's integrations screen, or directly: ```bash curl -sS -i "$HLIX_BASE_URL/v1/api/integrations/linear/install" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` Expected result: a `302` to `linear.app/oauth/authorize`, carrying an HMAC-signed state that binds the install to your workspace and user. 2. **Authorize in Linear.** hlix requests `read`, `write`, `app:assignable`, and `app:mentionable`, and installs with `actor=app` — so it becomes a workspace-level app user rather than acting as you. The two `app:` scopes are what make it assignable and mentionable at all. 3. **Land back on hlix.** The callback exchanges the code, reads the app user and workspace identity, and stores the installation with its refresh token and expiry. Expected result: a redirect to `…/integrations/linear?installed=1`. 4. **Verify.** ```bash curl -sS "$HLIX_BASE_URL/v1/api/integrations/linear" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` Then, in Linear, assign the hlix agent to a test issue. A `thought` activity — *"Received — gathering context…"* — should appear in the session within seconds. ## What hlix writes back Everything hlix says lands as an **agent activity** on the Linear session, not as an issue comment. Five kinds: | Activity | Meaning | | --- | --- | | `thought` | intermediate reasoning. The immediate acknowledgement is one of these, and is ephemeral | | `action` | a concrete step it took, with its parameter and result | | `response` | its answer for this turn — a plan summary, a status, or the outcome | | `elicitation` | a question for you. With options attached, Linear renders them as choices | | `error` | the run failed, with the message | The session's own status moves alongside: `awaitingInput` when it needs an answer, `complete` when the turn is finished, `error` when it is not. A session stays open only while something is actually going to report back. ## What it needs from an issue hlix has to decide which project — and, for a multi-repository project, which repository — an issue belongs to. When it cannot, it does not guess. It sets the session to `awaitingInput` and asks once, listing the candidates, because running an agent against the wrong repository is worse than a question. Two cases produce that question: - **Nothing to key a project on.** The issue has no signal tying it to a hlix project. - **The project has several repositories** and the issue never said which. Answer in the thread and the session continues. ## Steering a running session - **Prompt it again** by replying in the session. The agent reads the prior activity history, so the reply is a continuation rather than a fresh start. - **Stop it** with Linear's stop signal. hlix honours it immediately, records the stop, replies *"Stopped as requested."*, and closes the session. When hlix queues a cycle proposal it cannot hand to a live orchestrator, it says so and closes the turn rather than leaving the thread showing "Working…" indefinitely. ## Permissions and security - The install uses `actor=app`, so the agent is its own workspace user. Actions are attributable to hlix, not to whoever installed it. - Access tokens last 24 hours. hlix refreshes proactively within five minutes of expiry — but only **after** a webhook's signature has been verified, so a forged payload cannot trigger a token refresh or any database write. - Every webhook is HMAC-verified against the OAuth app's signing secret before any side effect. - A `PermissionChange` or unrecognised event type is acknowledged and ignored rather than processed on a guess. - An `OAuthApp` revocation from Linear's side marks the installation revoked in hlix. ## Disconnect ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/integrations/linear/$INSTALL_ID/revoke" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` `owner` or `admin` only. Revoke the OAuth application in Linear as well — that is the half that removes hlix's access. ## If Linear does not connect - `500` — ``Linear OAuth not configured``. The deployment has no Linear OAuth credentials. Operator fix. - Redirect with `error=missing_code_or_state` — the callback was opened without Linear's parameters. Restart from `/install`. - Redirect with `error=invalid_state_…` — the signed state failed verification or expired. Restart the install. - Redirect with `error=token_exchange_failed` — Linear refused the authorization code. Usually a redirect-URI mismatch in the OAuth app configuration. - Redirect with `error=viewer_lookup_failed` — the token worked but the app user could not be read. Check the granted scopes include `read`. - `401` on the webhook — ``unauthorized``. hlix answers this both when the signature does not verify AND when no installation matches the payload's app user, deliberately: the endpoint is unauthenticated, so a distinct "unknown installation" reply would let anyone probe which Linear workspaces hlix is installed in. To tell the two apart, read the backend log — it records `unknown appUserId` or `signature invalid` with the reason. Confirm the configured signing secret matches the OAuth app's Webhooks tab, and that you are pointing Linear at the right deployment; Linear disables a webhook that keeps failing. - Nothing appears in the session thread — assignment and mention need the `app:assignable` and `app:mentionable` scopes. Reinstall if the original authorization predates them. - The thread asks which project or repository — that is the design, not a failure. Answer in the thread. ## Next steps --- # Slack Install the hlix Slack app into your workspace and talk to the chat agent in a DM or by @mentioning it in a channel. **One hlix Slack app, installed into your workspace with OAuth.** After that, a direct message to the bot or an `@hlix` mention in a channel reaches the same chat agent the dashboard talks to, with your workspace's projects, models and tools. ## What it does | Capability | State | | --- | --- | | Direct message → chat agent | Works | | `@mention` in a channel → chat agent | Works | | Follow-up messages in a thread the agent replied in | Works | | Thread context — recent channel messages sent with the request | Works, capped at 10 messages | | Tool activity rendered as Block Kit cards | Works | | Request verification | Works — Slack's signing secret, checked by the adapter | | Mapping a Slack account to a hlix user | **Not implemented.** Workspace membership is the whole authorization | | Enterprise Grid org-wide installs | **Refused.** See below | Every message is recorded in the [audit log](/concepts/audit/) as `integration.channel.dispatched`, with the Slack user id and name on the row. ## Prerequisites - A hlix workspace, and agency `owner` or `admin` — connecting and revoking an installation is workspace administration - Permission to install an app in the Slack workspace - The deployment configured with the hlix Slack app (operator work, below) ## Connect it 1. **Start the install** from the dashboard under **Integrations → Slack**, or open: ```text https://server.hlix.ai/v1/api/integrations/slack/install ``` The workspace is carried in an HMAC-signed `state` parameter, so the callback binds the installation to the workspace you started from — not to whichever one happens to be active when Slack redirects back. 2. **Approve the scopes** on Slack's consent screen. hlix requests exactly: ```text app_mentions:read assistant:write channels:history chat:write groups:history im:history im:read im:write ``` `users:read.email` is deliberately **not** among them — hlix maps no Slack user to a hlix user, so it has no reason to read anyone's email. 3. **Confirm.** Slack redirects back to the dashboard with `?installed=1`, and the installation appears in the list: ```bash curl https://server.hlix.ai/v1/api/integrations/slack \ -H "X-Organization-Id: $HLIX_ORG" \ -H "Authorization: Bearer $HLIX_TOKEN" ``` 4. **Talk to it.** DM the bot, or invite it to a channel and `@mention` it. ## Disconnect ```bash curl -X POST https://server.hlix.ai/v1/api/integrations/slack/$INSTALL_ID/revoke \ -H "X-Organization-Id: $HLIX_ORG" \ -H "Authorization: Bearer $HLIX_TOKEN" ``` Revoking takes effect on the next message — a revoked installation resolves to no workspace, and the message is dropped. The row is kept as a tombstone rather than deleted, so the record that the workspace *was* connected survives. ## Who can act through Slack Everyone who can reach the bot, with the workspace's full agent toolbelt — which can create tasks and execute cycles. hlix does not map Slack accounts to hlix users, so channel membership is the access boundary. ## Enterprise Grid An org-wide Grid install is **refused** at the callback with `?error=enterprise_install_unsupported`. Its installation key is the `enterprise_id`, while an inbound message names the individual workspace's `team_id` — so most of the Grid's workspaces would resolve to no installation and their messages would be dropped silently. Installing into a single workspace within a Grid works normally. ## Operator configuration 1. **Create the Slack app** at [api.slack.com/apps](https://api.slack.com/apps), with the bot scopes listed above. 2. **Set the Event Subscriptions request URL** to the channel webhook, and subscribe to `app_mention`, `message.im`, and `message.channels`: ```text https://server.hlix.ai/webhooks/channels/slack ``` 3. **Set the OAuth redirect URL** to the callback: ```text https://server.hlix.ai/v1/api/integrations/slack/callback ``` 4. **Set the environment**, all four: ```text SLACK_CLIENT_ID SLACK_CLIENT_SECRET SLACK_SIGNING_SECRET SLACK_REDIRECT_URI # the callback URL from step 3 ``` There is deliberately **no `SLACK_BOT_TOKEN`.** hlix runs one app across many customer workspaces and resolves each workspace's bot token from its own installation row. Setting a static bot token alongside `SLACK_CLIENT_ID` is how a deployment silently becomes single-workspace. ## Security Every request carries Slack's signature, verified against `SLACK_SIGNING_SECRET` by the adapter before hlix sees a message. The workspace is derived server-side from the event's `team_id` and never from anything a caller supplies; a `team_id` with no live installation resolves to no workspace and is dropped without a reply — an unknown team is either a revoked install or a forged request, and answering would confirm the endpoint to the latter. A Slack workspace can be connected to at most **one** hlix workspace at a time. A second workspace trying to connect the same team is refused by a database constraint, not by a check that could be raced. ## If it does not respond - **A callback redirect with `?error=enterprise_install_unsupported`** — an org-wide Grid install. Install into a single workspace instead. - **`?error=workspace_already_connected`** — another hlix workspace holds a live installation of that Slack workspace. Revoke it there first. - **`?error=invalid_state_stale`** — the install took longer than 10 minutes between start and callback. Start again. - **`?error=token_exchange_slack_error`** — Slack rejected the code. Usually a `SLACK_REDIRECT_URI` that does not exactly match the app's configured redirect URL. - **`403` — ``admin only``** on a revoke. Connecting and disconnecting is `owner`/`admin` work. - **`503` — ``Slack is not configured on this server``** — the deployment is missing `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, or `SLACK_SIGNING_SECRET`. Those three are what "configured" means; `SLACK_REDIRECT_URI` is not among them. - **`500` — ``Slack OAuth not configured``** on install or callback — `SLACK_REDIRECT_URI` is missing. The webhook keeps working and only the install flow fails, which is why this is a different status from the one above. Both are operator fixes, not user ones. - **Slack shows a retry / timeout on the event URL** — a cold start exceeded Slack's 3-second acknowledgement window. Slack retries up to three times; the first message after an idle period is the one likely to see it. - **No reply and no error** — check that the workspace's installation is not revoked, and that the bot is actually in the channel you mentioned it from. ## Next steps --- # Telegram Link a Telegram chat to your hlix workspace with a one-time code, then talk to the chat agent from your phone. **One hlix bot serves every workspace, and a chat joins yours by redeeming a short-lived link code.** Once linked, messages in that chat reach the same chat agent the dashboard talks to, with your workspace's projects, models and tools. ## What it does | Capability | State | | --- | --- | | `/link ` binds a chat to a workspace | Works | | Free-text message → chat agent, with its full toolbelt | Works | | Thread context — recent chat messages sent with the request | Works, capped at 10 messages | | Typing indicator while the agent works | Works, best-effort | | Webhook request verification | Works — `TELEGRAM_WEBHOOK_SECRET_TOKEN`, checked by the adapter | | A message from an unlinked chat | Politely refused, naming the link flow | | Mapping a Telegram account to a hlix user | **Not implemented.** The chat's workspace membership is the whole authorization | Every message is recorded in the [audit log](/concepts/audit/) as `integration.channel.dispatched`, with the Telegram user id and name on the row — so the trail names a human even though no hlix session exists for them. ## Prerequisites - A hlix workspace, and agency `owner` or `admin` — minting a link code is workspace administration - The deployment configured with the hlix bot (operator work, below) ## Link a chat 1. **Generate a code** in the dashboard under **Integrations → Telegram**, or directly: ```bash curl -X POST https://server.hlix.ai/v1/api/integrations/telegram/link-code \ -H "X-Organization-Id: $HLIX_ORG" \ -H "Authorization: Bearer $HLIX_TOKEN" ``` ```json { "code": "K7M2PQ9XTVA3JHNR4B0C", "expiresAt": "2026-08-09T12:15:00.000Z", "command": "/link K7M2PQ9XTVA3JHNR4B0C", "botUsername": "hlix_bot" } ``` The response is the **only** copy of the code that will ever exist — the database stores a SHA-256 of it, not the code. 2. **Send it to the bot**, in a direct message or in a group the bot has joined: ```text /link K7M2PQ9XTVA3JHNR4B0C ``` Dashes, spaces and lower case are all accepted. 3. **Confirm.** The bot replies: ```text This chat is now linked to your hlix workspace. Ask me anything — I can list projects, create tasks and run cycles. ``` 4. **Talk to it.** Anything that is not a command goes to the chat agent. ## Who can act through a linked chat Everyone in that chat, with the workspace's full agent toolbelt — which can create tasks and execute cycles. hlix does not map Telegram accounts to hlix users, so the chat's membership is the access boundary, exactly as the chat's membership is on Telegram's side. ## Operator configuration This part is deployment-wide, not per workspace. 1. **Create the bot** with Telegram's [@BotFather](https://t.me/botfather) and copy its token. 2. **Set the environment**, all three: ```text TELEGRAM_BOT_TOKEN # from @BotFather TELEGRAM_WEBHOOK_SECRET_TOKEN # any high-entropy string you choose TELEGRAM_BOT_USERNAME # e.g. hlix_bot, so /link@hlix_bot resolves ``` Without `TELEGRAM_WEBHOOK_SECRET_TOKEN` the platform reports Telegram as **not configured** and refuses the webhook. That is deliberate: the adapter accepts every POST when no secret token is set, and an unverified endpoint is one anyone can deliver updates to. 3. **Register the webhook with Telegram**, passing the same secret: ```bash curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \ -d "url=https://server.hlix.ai/webhooks/channels/telegram" \ -d "secret_token=$TELEGRAM_WEBHOOK_SECRET_TOKEN" ``` 4. **Verify** by sending `/link` with no code. The bot should answer with the instructions, which proves both delivery and verification. ## Security Every inbound request carries `X-Telegram-Bot-Api-Secret-Token`, compared in constant time against `TELEGRAM_WEBHOOK_SECRET_TOKEN`; a mismatch is a `401` before anything else happens. The workspace is derived server-side from the chat id, never from anything in the request body — an unrecognised chat resolves to no workspace and is refused rather than defaulted. ## If it does not respond - **`This chat isn't linked to a hlix workspace yet.`** — the chat has no live installation. Generate a code and send `/link `. - **`That link code is not valid.`** — unknown, already spent, or expired. All three give the same answer on purpose, so redemption cannot be used to discover which codes exist. - **`403` — ``admin only``** on the link-code endpoint. Minting is `owner`/`admin` work. - **`503` — ``Telegram is not configured on this server``** — the deployment is missing `TELEGRAM_BOT_TOKEN` or `TELEGRAM_WEBHOOK_SECRET_TOKEN`. An operator fix, not a user one. - **`401 Invalid secret token`** — Telegram is sending a different secret than the backend holds. Re-run `setWebhook` with the value currently in the environment. - **No reply at all** — Telegram is not delivering. Check `getWebhookInfo` for the registered URL and its `last_error_message`. ## Next steps --- # Official MCP server Connect Cursor, Claude Code, Codex, or Windsurf to a bounded, workspace-scoped Hlix tool server. **`@hlix/mcp` is a local tool server that lets the coding agent already open on your machine read your hlix workspace.** Cursor, Claude Code, Codex, or Windsurf can inspect projects, tasks, cycles, comments, and verified review evidence without you copying API requests into prompts. It uses the same typed client and workspace boundary as the hlix CLI. Your MCP client launches the server itself — there is nothing to install by hand. The version is pinned so a client restart cannot silently upgrade a server holding your workspace credentials. Cursor (`.cursor/mcp.json`), Claude Code (`.mcp.json`), and Windsurf (`~/.codeium/windsurf/mcp_config.json`) use: ```json { "mcpServers": { "hlix": { "command": "npx", "args": [ "-y", "@hlix/mcp@0.2.0" ] } } } ``` Codex uses `.codex/config.toml` in a trusted project or the user config: ```toml [mcp_servers.hlix] command = "npx" args = ["-y", "@hlix/mcp@0.2.0"] default_tools_approval_mode = "writes" ``` Authenticate with `hlix auth login --workspace `. Run `hlix import .` to create and bind the cloud project; `hlix init` alone does not write a project ID. [Current versions](/releases/availability/) reports the same state for all three hlix packages, and names the gate each one still has to pass. ## Prerequisites - Node.js 22.13 or later for the Mastra MCP runtime - a completed `hlix auth login` - a selected workspace - a successful `hlix import` in the repository when you want strict project scope The CLI and TypeScript SDK retain their Node.js 20 floor. Only the MCP server needs Node.js 22.13 or later. ## Read-only by default The default process exposes only these tools: | Tool | Purpose | | --- | --- | | `projects_list` | list the bound project, or every workspace project in global mode | | `projects_get` | read one project without escaping a local binding | | `tasks_list` | list tasks within the local binding, optionally by status | | `tasks_get` | read one task after verifying its project | | `tasks_list_comments` | read task comments | | `tasks_get_review` | read verified review evidence | | `cycles_list` | list cycles | | `cycles_get` | read one cycle | Responses are bounded before they enter the model context. Errors expose a stable code, HTTP status, and request ID where available, but not response bodies, request headers, URLs, credentials, or stack traces. ## Opt into bounded writes Set `HLIX_MCP_ALLOW_WRITES=1` in the MCP server process environment only after reviewing the client’s tool-approval policy. This adds three non-destructive operations: - `tasks_create` - `tasks_add_comment` - `cycles_propose` For JSON clients, add an `env` object to the `hlix` server entry: ```json { "env": { "HLIX_MCP_ALLOW_WRITES": "1" } } ``` For Codex, add: ```toml [mcp_servers.hlix.env] HLIX_MCP_ALLOW_WRITES = "1" ``` ## Authentication and secrets The preferred path contains no secret in client configuration: 1. `hlix auth login` stores the credential in an owner-only file; 2. `hlix init` stores only the API URL, workspace ID, and optional project ID; 3. the MCP child process reads both and rejects symlinked files or configuration directories, non-regular files, foreign ownership, permissive credential modes, cross-project targets, or workspace/API mismatches. Automation may supply `HLIX_API_KEY` and `HLIX_WORKSPACE_ID` from a secret manager. Keep those values in the process environment, never in tool arguments, prompts, committed MCP configuration, or diagnostic output. ## Client notes Where each client keeps its configuration, and what it does about trust. The launch command itself is in the configuration above — paste it verbatim. **Cursor** loads **project** MCP configuration from `.cursor/mcp.json`; the agent and the CLI share that one project definition. Scope is per project, so a server added here travels with the repository. Review it before committing. **Claude Code** stores team-shared project servers in `.mcp.json` and **asks before trusting** a project-scoped server the first time it appears. That prompt is the trust boundary — a server arriving through a pulled branch does not run until someone approves it. **Codex** uses `.codex/config.toml` for trusted-project scope, or `~/.codex/config.toml` for user scope. Set `default_tools_approval_mode = "writes"` to use the server's read-only annotations: reads run, mutations prompt. That pairs with hlix's own write gate rather than replacing it. **Windsurf** reads its **user-level** `~/.codeium/windsurf/mcp_config.json` — not a project file, so a server added here applies everywhere. Open MCP settings and enable only the tools you want exposed in Cascade. ## Verify and troubleshoot After restarting the client: - verify that `hlix` initializes without writing unexpected text to stdout; - inspect the tool list and confirm write tools are absent by default; - list projects and compare the workspace with `hlix projects list --json`; - if the server reports no credential, rerun login from the project root; if it reports no project, import the repository or supply an explicit project ID in global mode; - if the process exits immediately, confirm Node.js 22.13 or later and use the exact version shown by the release status. See [Troubleshooting](/reference/troubleshooting/#the-official-mcp-server) for exact failure modes. To import a third-party MCP definition into a cloud project instead, use [Skills, agents & MCP](/cli/resources/); that is a separate inbound resource flow. ## Source references - [Cursor MCP configuration](https://cursor.com/docs/mcp) - [Claude Code MCP configuration](https://code.claude.com/docs/en/mcp) - [Codex MCP configuration](https://learn.chatgpt.com/docs/extend/mcp) - [Cascade MCP configuration](https://docs.devin.ai/desktop/cascade/mcp) - [MCP Registry publishing](https://modelcontextprotocol.io/registry/quickstart) ## Next steps --- # CLI reference Global options, target resolution, the JSON envelope, error codes, and exit codes shared by every hlix command. **This page documents what every `hlix` command has in common:** the global options, how a command decides which workspace and API it is talking to, the shape of `--json` output, and the codes automation branches on. Each command group has its own page, linked below. Run `hlix --help` for the installed version's source-of-truth usage. The packaged skill guide at `skills/hlix/SKILL.md` is tested against the command table in both directions, so a command documented there but unregistered — or registered but undocumented — fails the build. ## Command groups | Group | Commands | Page | | --- | --- | --- | | Authentication | `auth login`, `auth status`, `auth logout` | [auth](/reference/cli/auth/) | | Orientation | `status` | [status](/reference/cli/status/) | | Local setup | `init` | [init](/reference/cli/init/) | | Project creation | `import` | [import](/reference/cli/import/) | | Reconciliation | `push`, `pull`, `sync` | [push, pull & sync](/reference/cli/sync/) | | Explicit resources | `skill import`, `agent import`, `mcp import` | [resources](/reference/cli/resources/) | | Inspection | `projects list`, `projects get` | [projects](/reference/cli/projects/) | | Inspection | `tasks list`, `tasks get`, `tasks review`, `tasks watch` | [tasks](/reference/cli/tasks/) | ## Global options Accepted by every command, whether or not the command's own usage lists them. - **`--json`** (`boolean`, default `false`): print a stable, versioned machine envelope instead of the human view. Streams emit JSONL. - **`--cwd `** (`string`, default the process working directory): operate on another directory, including the project-binding lookup. A path that is not a directory exits `invalid_usage` before the command runs. - **`--workspace `** (`string`, default resolved — see below): the workspace to act in, overriding the folder's binding. Contradicting a bound folder is refused, not resolved. - **`--base-url `** (`string`, default `https://server.hlix.ai`): the API to talk to, overriding the folder's binding. [`hlix auth login`](/reference/cli/auth/) additionally requires HTTPS before it will store a credential — plain HTTP is accepted there only for `localhost`, `127.0.0.1`, and `::1`. - **`--help`** (`boolean`): show global help, or the matched command's usage. - **`--version`** (`boolean`): print the installed CLI version. A command-specific flag passed to a command that does not accept it exits `invalid_usage` naming the flag, rather than being ignored. ## Where a command points Every command resolves its workspace and API through one chain, highest first: ```text 1. an explicit flag --workspace, --base-url 2. the HLIX_* environment HLIX_WORKSPACE_ID, HLIX_BASE_URL 3. the nearest .hlix/config.json walking up from the working directory 4. the stored credential ~/.config/hlix/credentials.json 5. the built-in default https://server.hlix.ai ``` The folder's binding therefore **outranks the machine-wide credential**: a folder bound to workspace A is queried against workspace A even when the last login named workspace B. Commands that act on a bound project — `push`, `pull`, `sync`, `status`, `projects get`, `tasks list`, and the three resource imports — find that binding by walking **up** from the working directory. Running `hlix push` from `packages/api` pushes the project, not a fragment of it. ## JSON contract Success: ```json { "schemaVersion": 1, "command": "projects list", "data": [] } ``` Failure: ```json { "schemaVersion": 1, "command": "projects list", "error": { "code": "unauthenticated", "message": "…", "status": 401 } } ``` Human formatting may improve between releases. The versioned JSON envelope, command name, documented command data, and stable error code are the automation contract. For inspection commands, `data` is the API's response passed through **unmodified** — the CLI does not reshape, rename, or prune it. Local project commands such as `import`, `push`, `pull`, and `sync` return their documented local result shapes. `tasks watch --json` is the one exception to "one document": it emits one envelope per line (JSONL), because a live stream cannot be a single JSON document. ## Error codes Branch on `error.code`, never on `error.message` — the message is human-readable and not stable. | Code | Raised when | | --- | --- | | `unauthenticated` | No credential, or the key was rejected | | `forbidden` | The key is valid but may not act here | | `not_found` | No such resource in this workspace | | `conflict` | The operation collided with another change | | `invalid_request` | The request failed schema validation | | `bad_request` | The API refused the request | | `rate_limited` | Too many requests | | `not_implemented` | The endpoint exists but does nothing yet | | `server_error` | The API failed | | `unreachable` | The API could not be contacted | | `missing_argument` | A required positional argument was omitted | | `invalid_usage` | Unknown flag, bad flag value, or too many arguments | | `unknown_command` | No such command | | `workspace_mismatch` | An explicit target contradicts the folder's binding | | `workspace_required` | Nothing in the resolution chain named a workspace | | `approval_required` | A destructive step needs approval the caller cannot give | | `cancelled` | An interactive prompt was declined | | `not_initialized` | The folder is not bound to a project | | `already_imported` | The folder is already bound; use `hlix push` | | `scan_blocked` | The import scan found a blocker | | `invalid_base_url` | `--base-url` is not HTTPS, and is not a loopback host | | `missing_credential` | No API key was entered at the prompt | | `unknown` | Unclassified | `approval_required` and `cancelled` are the two halves of every gate: the first means "you cannot answer here — review with `--dry-run` and repeat with `--yes`", the second means "you answered no". ## Exit codes | Code | Meaning | | --- | --- | | `0` | success | | `1` | operational failure or scan blocker | | `2` | usage, parsing, or unknown-command failure | | `3` | authentication or permission failure | A `--dry-run` that would end in `conflict` exits `1`, as `import --dry-run` does for a blocker — the report is the deliverable, and the exit code still carries the verdict. ## Environment variables | Variable | Purpose | | --- | --- | | `HLIX_API_KEY` | API credential; overrides the saved file | | `HLIX_WORKSPACE_ID` | workspace selection | | `HLIX_BASE_URL` | API origin; defaults to production | | `HLIX_CONFIG_HOME` | hlix-specific credential directory root | | `XDG_CONFIG_HOME` | standard config root when `HLIX_CONFIG_HOME` is absent | ## If a command fails before it runs - `unknown_command` — the words did not match a registered command. `hlix --help` lists them. - `invalid_usage` — ``Unsupported option for push: --force`` — the flag exists globally or on another command, not this one. The per-command page lists what each accepts. - `invalid_usage` — ``--cwd is not a directory: /path`` — `--cwd` is checked before dispatch, so this exits `2` even for a command that would otherwise always exit `0`. - `missing_argument` — a required positional was omitted. The message is the command's usage line. - `error: API keys are never accepted as command-line arguments.` — remove `--api-key` and use the hidden prompt or `HLIX_API_KEY`. ## Next steps --- # hlix auth Reference for hlix auth login, auth status, and auth logout — credential storage, source reporting, and removal. **`hlix auth` manages the credential this machine uses.** An API key proves who you are; the workspace ID selects the tenant the key is used against. The two are stored together, and every other command resolves them through the [target chain](/reference/cli/#where-a-command-points). ## Prerequisites - An hlix API key and a workspace ID you can access - Network reach to the API you are signing in to ## `hlix auth login` ```bash hlix auth login --workspace [--base-url ] [--json] ``` Verifies a key against a real API call, then writes it atomically to the credential file with owner-only permissions. The key is never echoed and never printed back. ### Flags - **`--workspace `** (`string`, **required**): the workspace (organization) ID this credential acts in. Falls back to `HLIX_WORKSPACE_ID`. Required because an API key authenticates a *user* and names no workspace, so the caller must state one. Omitting both exits `missing_argument`. - **`--base-url `** (`string`, default `https://server.hlix.ai`, or `HLIX_BASE_URL`): the API to sign in to. Must be HTTPS; plain HTTP is accepted only for `localhost`, `127.0.0.1`, and `::1`. Anything else exits `invalid_base_url`. - **`--json`** (`boolean`, default `false`): print the envelope instead of the human lines. The payload carries `baseUrl`, `organizationId`, and `credentialPath` — **never the key, not even a prefix**, because machine output gets piped into logs. The key itself is read from a hidden prompt, or from `HLIX_API_KEY` when set. There is no flag for it. Expected result: ```text Signed in to https://server.hlix.ai Credential stored at /Users/you/.config/hlix/credentials.json ``` ### Conflicts and validation - `login` is deliberately **outside** the target chain, so signing in to a second workspace from inside a folder bound to a different one works. Every other command refuses that contradiction with `workspace_mismatch`. - The key is probed with a real `projects.list` call **before** anything is written, with retries disabled. A key that does not work is never stored — a stored credential that fails later, somewhere else, is worse than no credential. - No prompt and no `HLIX_API_KEY` exits `missing_credential`, not an empty-string login. ## `hlix auth status` ```bash hlix auth status [--json] ``` Names the credential's **source** — the environment or the stored file — and probes it against the API. ### Flags - **`--json`** (`boolean`, default `false`): returns `credentialSource`, `credentialPath` (`null` for an environment credential), `baseUrl`, `baseUrlSource`, `workspaceId`, `workspaceSource`, and `valid`. Expected result: ```text credential file (/Users/you/.config/hlix/credentials.json) api https://server.hlix.ai (from credential) workspace org_2p9xk4 (from credential) status valid ``` ### Conflicts and validation - A folder whose binding is contradicted reports `workspace_mismatch` **before** the credential is even considered, so the diagnosis does not change with an unrelated variable. - When the probe fails, the failure is re-raised **unchanged** — the envelope's `code` and HTTP `status` match what every other command would return for the same key. The context goes to stderr: `credential from file, workspace org_2p9xk4, api https://server.hlix.ai`. - It never prints the key, not even a prefix. ## `hlix auth logout` ```bash hlix auth logout [--yes] [--json] ``` Removes the credential **file**. It does not, and cannot, unset an environment variable. ### Flags - **`--yes`** (`boolean`, default `false`): skip the confirmation prompt. Without it, an interactive run asks; a `--json` or non-interactive run exits `approval_required` rather than defaulting to yes. - **`--json`** (`boolean`, default `false`): returns `{ removed, path }`, or `{ removed: false, reason: "environment", path }` when an environment credential is in play. ### Conflicts and validation - With `HLIX_API_KEY` set, logout reports it and leaves it alone — it belongs to the shell that set it: `HLIX_API_KEY is set in this environment; unset it there to sign out.` - With `HLIX_API_KEY` set **and** a file underneath it, the file is named and left in place until `--yes` is passed. Removing it silently would have made `logout` a permanent no-op for anyone who had exported the variable: the stored key stayed on disk, unmentioned, and came back the moment the variable went away. - With nothing stored, it is a successful no-op: `No stored credential at …`. - It refuses any path that is not a credential document this CLI wrote. ## If auth fails - `missing_argument` — ``--workspace is required.`` Pass the ID or set `HLIX_WORKSPACE_ID`. - `missing_credential` — ``No API key entered. Run from a terminal or set HLIX_API_KEY for CI.`` Run interactively, or set the variable for CI. - `invalid_base_url` — ``--base-url must use HTTPS (HTTP is allowed only for localhost).`` - `unauthenticated` — the key was rejected. Rotate or replace it. - `forbidden` — the key is valid, but the user cannot act in that workspace. - `unreachable` — check the API URL, DNS, TLS, and network path. - `approval_required` from `auth logout` — the caller cannot answer a prompt. Repeat with `--yes`. ## Next steps --- # hlix import Reference for hlix import — every flag, the trust gate, the dry-run report, and what --yes cannot bypass. **`hlix import` creates a cloud project from a local folder** and records its first immutable revision. It is the one command that uploads your source, its Git history, and its protected configuration, so it is also the one command with a trust gate in front of it. For what moves in which lane and how to resolve each blocker, read [Import a project](/cli/import/). This page is the flag surface. ## Prerequisites - A credential that names a workspace (see [`hlix auth login`](/reference/cli/auth/)) - Git installed, even when the source folder is not already a repository - No existing `.hlix/config.json` binding the folder to a different API or workspace ## Usage ```bash hlix import [folder] \ [--dry-run] \ [--env-file ] \ [--history ] \ [--name ] \ [--stack ] \ [--yes] \ [--json] ``` ### Arguments - **`folder`** (`string`, optional, default `.`): the directory to import, resolved against `--cwd`. At most one; a second exits `invalid_usage`. ### Flags - **`--dry-run`** (`boolean`, default `false`): scan and report; upload nothing, create nothing, and write no `.hlix/config.json`. The report names the stack, file and byte counts, environment evidence, cloud setup commands, required and collected secret **key names**, protected and quarantined paths, environment warnings, and blockers. Exits `1` when the scan found a blocker — the report is still the deliverable. - **`--env-file `** (`string`, default: automatic dotenv precedence): choose which dotenv profile is the active cloud environment when the scan finds several. Every other profile is still preserved encrypted. Precedence when this is omitted is `.env`, `.env.development`, `.env.local`, `.env.development.local`, later overriding earlier. - **`--history `** (`abort-on-findings` | `preserve`, default `abort-on-findings`): the default aborts on possible historical secrets. `preserve` keeps the full Git history **after showing the risk** — it means "upload the history despite findings", not "remove the findings". Any other value exits with ``--history must be `abort-on-findings` or `preserve`.`` - **`--name `** (`string`, default the folder's base name): the cloud project name. - **`--stack `** (`string`, default the detected stack): override stack detection. - **`--yes`** (`boolean`, default `false`): record an explicit approval for the reviewed upload and cloud setup in non-interactive use. It does **not** override history, path, integrity, or size blockers. - **`--json`** (`boolean`, default `false`): the dry-run report, or on a real import `{ projectId, revisionId, generation, commitSha, files, bytes }`. Never contains secret values. Expected result: ```text Imported acme-invoices Project ID: 6f1c2a9e-8f0b-4a7d-9d33-2f0b1c7e5a41 Revision: 1 ``` ## The trust gate When the scan finds cloud setup commands, protected files, quarantined resources, or collected secret keys, the CLI shows them — commands verbatim, paths, and secret **key names** only — and asks for one explicit trust decision before any project is created or uploaded. | Caller | Behaviour | | --- | --- | | Interactive terminal, no `--yes` | prompts once; declining exits `cancelled` | | Interactive terminal, `--yes` | proceeds without prompting | | `--json` or non-interactive, no `--yes` | exits `approval_required` pointing at `--dry-run` | | Any caller, blocker present | exits `scan_blocked` regardless of `--yes` | ### Conflicts and validation - The target is resolved **at the folder being imported**. A contradicting override exits `workspace_mismatch` before a single byte is uploaded. - Both a workspace and a credential are required, or the command exits `workspace_required` — this is where a dry run learns it could not have finished. - A folder already carrying a `projectId` exits `already_imported`: ``This folder is already bound to an Hlix project. Use `hlix push`.`` - Outside `--dry-run`, `.hlix/config.json` is written **before** the authoritative scan, so `.hlixignore` is reviewed, hashed, and uploaded with the project. A blocked or cancelled import therefore leaves the folder initialized but unbound. - Repeating the same import uses an idempotency key derived from the manifest and bundle hashes, so an interrupted retry does not create a duplicate project. - If the source changes between scan and snapshot, import stops rather than uploading a mixed state. ## If import fails - `workspace_required` — ``No workspace selected. Run `hlix auth login --workspace ` first.`` - `already_imported` — the folder is bound. Use [`hlix push`](/reference/cli/sync/). - `scan_blocked` — the message lists every blocker. [If the scan blocks the import](/cli/import/#if-the-scan-blocks-the-import) maps each to its fix. - `approval_required` — ``Import needs approval for protected files or cloud setup. Review with `hlix import --dry-run`, then repeat with `--yes`.`` - `cancelled` — the prompt was declined. Nothing was uploaded. - ``--history must be `abort-on-findings` or `preserve`.`` — the flag takes those two values only. - `Import completed without project revision metadata.` — the upload finished but the API returned no revision. Retry; the idempotency key makes that safe. ## Next steps --- # hlix init Reference for hlix init — the files it writes, what it verifies first, and how it differs from import. **`hlix init` prepares a local folder for the CLI.** It writes committable project configuration and scans the folder, but it creates **no cloud project** — [`hlix import`](/reference/cli/import/) does that and adds the project and revision IDs. ## Prerequisites - A credential that names a workspace (see [`hlix auth login`](/reference/cli/auth/)) - A directory you are authorized to scan ## Usage ```bash hlix init [folder] [--json] ``` ### Arguments - **`folder`** (`string`, optional, default `.`): the directory to initialize, resolved against `--cwd`. At most one may be given; a second exits `invalid_usage`. ### Flags - **`--json`** (`boolean`, default `false`): return `{ root, config, scan }`, where `scan` carries `files`, `bytes`, `secretFiles`, `quarantinedResources`, and `blocking`. Paths and counts only — **no secret values**. Expected result: ```text Initialized /absolute/path/to/project Project: not imported yet Workspace: org_2p9xk4 ``` ## What it writes | Path | Purpose | Commit it? | | --- | --- | --- | | `.hlix/config.json` | Stable API URL, workspace ID, and — after import — project ID | Yes | | `.hlix/.gitignore` | Keeps local revision state out of Git | Yes | | `.hlix/state.json` | Last synchronized immutable revision and generation | No; created by `import` | | `.hlixignore` | Project-local additions to the snapshot ignore list | Yes | ### Conflicts and validation - The target is resolved **at the folder being initialized**, not the shell's working directory. An override that contradicts that folder's existing binding exits `workspace_mismatch` before anything is written. - Nothing in the chain naming a workspace is an error, not a default: ``No workspace selected. Run `hlix auth login --workspace ` first.`` - The credential is proved with a real `projects.list` call **before** the folder is bound. `init` must not leave a convincing config behind for a dead login. - An existing `.hlix/config.json` is preserved rather than rewritten, so a routine `init` cannot silently move work across tenants. - A symlinked `.hlix` path is refused. - Scan blockers are **reported, not fatal**: `init` prints them under `Review before import:` and still exits `0`. They become blocking at [`hlix import`](/reference/cli/import/). ## If init fails - ``No workspace selected. Run `hlix auth login --workspace ` first.`` — nothing in the [resolution chain](/reference/cli/#where-a-command-points) named a workspace. - `workspace_mismatch` — a `--workspace`, `--base-url`, or `HLIX_*` value contradicts the binding of the folder being initialized. The message names both sides. Drop the override rather than editing `.hlix/config.json`, and never copy that file between projects or tenants. - `Refusing to write through a symlinked …` — replace the symlinked `.hlix` path with a real directory you own. - `unauthenticated` — the stored credential no longer works. `init` proves it before writing, so this means the login is stale, not that the folder is wrong. - `invalid_usage` — more than one folder argument was given. ## Next steps --- # hlix projects Reference for hlix projects list and projects get — the default target, the human columns, and the unmodified JSON body. **`hlix projects` reads the projects in the resolved workspace.** Both commands are transport-only: with `--json` the body is the API's response passed through **unmodified**, so a script never finds that the CLI dropped a field the API returned. ## Prerequisites - A credential that reaches the resolved workspace (see [`hlix auth login`](/reference/cli/auth/)) ## `hlix projects list` ```bash hlix projects list [--json] ``` Lists every project in the workspace the [target chain](/reference/cli/#where-a-command-points) resolved. It takes no positional arguments and no command-specific flags. Expected result: ```text ID NAME STACK ------------------------------------ -------------- ------ 6f1c2a9e-8f0b-4a7d-9d33-2f0b1c7e5a41 acme-invoices bun ``` An empty workspace prints `No projects.` and exits `0`. ## `hlix projects get` ```bash hlix projects get [project-id] [--json] ``` ### Arguments - **`project-id`** (`string`, optional, default the bound project): which project to show. With no ID, the project this folder is bound to is used — found by walking **up** from `--cwd`. Outside a bound folder, omitting it exits `missing_argument`: ``A project id is required, or run this inside a folder bound to a project.`` Expected result: ```text id 6f1c2a9e-8f0b-4a7d-9d33-2f0b1c7e5a41 name acme-invoices stack bun status active repos 1 ``` ### Conflicts and validation - The human view selects columns and **omits empty fields**; the `--json` body does not. Read the envelope, not the table. - A project in another workspace answers `not_found` rather than `forbidden` — the API does not confirm cross-tenant existence, and neither does the CLI. - At most one positional argument; a second exits `invalid_usage`. ## If a projects command fails - `unauthenticated` — ``No credential. Run `hlix auth login`, or set HLIX_API_KEY for CI.`` - `forbidden` — the key is valid but the user cannot act in the resolved workspace. - `not_found` on `projects get` — the ID is wrong, or belongs to a workspace this key cannot see. Do not infer which. - `workspace_mismatch` — an override contradicts the folder's binding. [`hlix status`](/reference/cli/status/) shows which source decided what. - `missing_argument` — run it inside a bound folder, or pass the ID. - `unreachable` — the API could not be contacted; the failure is transport, not authorization. ## Next steps --- # hlix skill, agent & mcp import Reference for the three explicit resource-activation commands — flags, accepted values, and every validation that refuses an import. **These three commands activate a resource that project import only quarantines.** A project scan preserves agent instructions, skill content, and MCP configuration encrypted — but inert. Activation is separate because these files can change model behaviour, run tools, reach remote services, or execute local programs inside the Coding Workspace. ## Prerequisites - A folder bound by [`hlix import`](/reference/cli/import/) — all three exit `not_initialized` otherwise - You have read the complete file you are importing ## `hlix skill import` ```bash hlix skill import [--name ] [--json] ``` ### Arguments - **``** (`string`, **required**): a `SKILL.md` file, or a folder containing one. Exactly one; zero exits `missing_argument`, two exits `invalid_usage`. ### Flags - **`--name `** (`string`, default derived from the path): the project-scoped identifier. Must be a lowercase kebab-case slug matching `^[a-z0-9]+(-[a-z0-9]+)*$`. Derivation is the folder name for a directory, the *parent* folder's name for a file called `SKILL.md`, and otherwise the file's base name. - **`--json`** (`boolean`, default `false`): return the stored skill metadata. ### Conflicts and validation - A symlink at the given path is refused outright. - The resolved file must be a regular file of at most 200,000 bytes. - Re-importing with the same project-scoped name **updates** that skill. ## `hlix agent import` ```bash hlix agent import [--name ] [--runtime ] [--profile ] [--json] ``` ### Arguments - **``** (`string`, **required**): one Markdown instruction file. The extension must be `.md` or `.mdx`. ### Flags - **`--name `** (`string`, default the file's base name with `-` and `_` turned into spaces): the agent's display name. Unlike a skill slug, this is free text. - **`--runtime `** (`claude-code` | `codex` | `cursor-agent` | `hlix`, default `hlix`): which coding agent the instructions target. Any other value is refused with the accepted list. This flag names the runtime an *agent record* targets; it is separate from the task-level harness cascade, whose default is also `hlix`. - **`--profile `** (`builder` | `desktop` | `e2e`, default `builder`): the sandbox profile the agent runs under. `desktop` and `e2e` are sandbox-driver profiles, so they require `--runtime claude-code`; combining either with `hlix` is refused rather than accepted and ignored. - **`--json`** (`boolean`, default `false`): return the created agent, including its ID. ### Conflicts and validation - The file must be a regular file of at most 20,000 bytes. - Agent import **creates a new agent each time**, so an existing agent identity is never replaced implicitly. Skill and MCP imports update in place; this one does not. - `--runtime` here names the coder for an agent record. Which harness actually runs a given task is decided by a wider cascade — see [Connect your agents](/guides/connect-agents/). ## `hlix mcp import` ```bash hlix mcp import <.mcp.json> [--server ] [--allow-stdio] [--json] ``` ### Arguments - **`<.mcp.json>`** (`string`, **required**): an MCP configuration file containing an `mcpServers` object. At most 200,000 bytes. ### Flags - **`--server `** (`string`, default the only server): which server to activate. **Required when the file defines more than one** — a file with several servers and no `--server` is refused rather than defaulting to the first. - **`--allow-stdio`** (`boolean`, default `false`): permit a stdio server. Required, because a stdio server executes a local program inside the Coding Workspace. It is not permission to run an arbitrary command string through a shell — the command must be a validated direct executable. - **`--json`** (`boolean`, default `false`): return the stored server definition, including the environment **key names** but no values. ### Conflicts and validation - The transport is taken from `type` when it is `http` or `sse`, otherwise inferred: a `command` means stdio, a `url` means http. Neither exits with ``MCP server has no supported transport.`` - A stdio server without `--allow-stdio` is refused: `Stdio MCP servers execute a local command in the cloud; repeat with --allow-stdio.` - Remote URLs must use public HTTPS. Embedded credentials, query strings, fragments, and loopback, private, link-local, or cloud-metadata hosts are refused. - Every header value must be a string, and must be exactly `${KEY}` or one approved scheme followed by it — `Bearer`, `Basic`, or `Token`. Extra prefix or suffix text is refused, and a headers object with no `${KEY}` placeholder at all is refused as a literal credential. - Only environment **key names** are sent. The values must already exist in the project's encrypted secrets; a missing one fails closed at task time rather than starting with an empty value. - Malformed JSON exits with `MCP config is not valid JSON.`; an empty `mcpServers` exits with `MCP config contains no servers.` ## If a resource import fails - `not_initialized` — ``This folder is not bound to an Hlix project. Run `hlix import .` first.`` The **nearest** binding is used, and only that one; an initialized-but-unimported folder does not fall through to an imported ancestor. - `Skill name must be a lowercase kebab-case slug.` — pass `--name` explicitly. - `Agent imports must be Markdown files.` — convert the reviewed instructions to `.md`. - `MCP config contains multiple servers; select one with --server .` - `MCP remote headers must use ${KEY} placeholders; literal credentials are refused.` - `Refusing to import a symlink: …` or `Refusing to import a non-regular file: …` - `Import file exceeds bytes: …` — 200,000 for skills and MCP configuration, 20,000 for an agent brief. ## Next steps --- # hlix status Reference for hlix status — the resolved workspace and API, the folder binding, and local-versus-cloud drift. **`hlix status` answers "where am I" in one screen:** the resolved workspace and API with the source that decided each, the folder's binding and local revision, and whether the working tree or the cloud head has moved since the last sync. It is the first thing to type in an unfamiliar checkout, and the fastest way to find out why another command is talking to a workspace you did not expect. ## Prerequisites None. `status` runs in any directory, with or without a credential — reporting what is missing is part of its job. ## Usage ```bash hlix status [--cwd ] [--json] ``` ### Flags - **`--cwd `** (`string`, default the process working directory): report on another folder, including its binding lookup. A path that is not a directory exits `invalid_usage` — see the validation note below. - **`--json`** (`boolean`, default `false`): return the structured report instead of the aligned fields. The payload carries `cwd`, `workspace`, `api`, `credential`, `conflict`, `project`, `revision`, `local`, and `cloud`, each with its own `source` or `error` where one applies. Expected result: ```text folder /Users/you/acme-invoices workspace org_2p9xk4 (from project) api https://server.hlix.ai (from project) credential file (/Users/you/.config/hlix/credentials.json) project 6f1c2a9e-8f0b-4a7d-9d33-2f0b1c7e5a41 revision generation 3 (rev_01J8Z4) local 412 files, matches the last sync cloud generation 3, up to date ``` ## What each line can say | Line | Values you will see | | --- | --- | | `workspace` | the ID and its source (`flag`, `env`, `project`, `credential`), or ``none — run `hlix auth login --workspace ` `` | | `credential` | `environment (HLIX_API_KEY)`, `file ()`, or ``none — run `hlix auth login` `` | | `project` | the project ID, `initialized at , not imported yet`, or ``none — run `hlix import .` to bind this folder`` | | `local` | ` files, matches the last sync`, `… modified since the last sync`, `… no synced base revision`, or `could not scan: ` | | `blocking` | present only when the scan found blockers; they are listed inline | | `cloud` | ``generation available — run `hlix pull` ``, `generation , up to date`, or `not checked: ` | | `conflict` | present only when an override contradicts the folder's binding | The `cloud` line is only attempted when the folder is bound to a project, a credential exists, and no conflict was found — so a broken login never turns into a confusing network error. ### Conflicts and validation - A contradicting `--workspace`, `--base-url`, or `HLIX_*` value appears on the `conflict` line rather than aborting the report. Every *other* command exits `workspace_mismatch` on the same input. - An argument error still exits `2` — an unknown flag, a stray positional, or a `--cwd` that is not a directory. Those are rejected before `status` runs at all, which is the one exit code it cannot promise away and does not try to. - The binding is found by walking **up** from `--cwd`, so `hlix status` in `packages/api` reports the project, not a fragment of it. ## If status reports something unexpected - `workspace … (from credential)` where you expected `(from project)` — the folder is not bound. Run [`hlix import .`](/reference/cli/import/). - `cloud not checked: unreachable` — the API could not be contacted. The rest of the report is still accurate; it is computed locally. - `cloud not checked: unauthenticated` — the stored credential no longer works against this workspace. Run [`hlix auth status`](/reference/cli/auth/) for the probe result. - `local could not scan: …` — the scan itself failed. The message is the scanner's own; a symlink or an unreadable path is the usual cause. - A `conflict` line — drop the override rather than editing `.hlix/config.json` to match it. ## Next steps --- # hlix push, pull & sync Reference for the three reconciliation commands — flags, the dry-run verdict table, and the forced-pull approval gate. **These three commands reconcile a bound folder with its cloud revision head.** `push` sends local changes as a new immutable revision, `pull` brings the cloud head down, and `sync` picks whichever direction is unambiguous and refuses the one that is not. `pull --force` is the only command in this CLI that can destroy work which was never uploaded. Two gates sit in front of it. ## Prerequisites - A folder bound by [`hlix import`](/reference/cli/import/) — otherwise every command here exits `not_initialized` - A credential that reaches the bound workspace ## Usage ```bash hlix push [--dry-run] [--json] hlix pull [--force] [--yes] [--dry-run] [--json] hlix sync [--dry-run] [--json] ``` None of the three accepts a positional argument. The bound project is found by walking **up** from `--cwd`, so running from `packages/api` acts on the project, not a fragment of it. ### Flags - **`--dry-run`** (`boolean`, all three, default `false`): print the divergence and the verdict the real command would reach, and change nothing. Exits `1` when the verdict is `conflict`, `0` otherwise — including when the real run would stop to ask. - **`--force`** (`boolean`, `pull` only, default `false`): replace local files with the cloud revision. States "replace my changes". Without local changes it replaces nothing and does not prompt. - **`--yes`** (`boolean`, `pull` only, default `false`): states "do not ask". Separate from `--force` deliberately — the flag that deletes local files used to be the same flag that skipped every question. - **`--json`** (`boolean`, all three, default `false`): the dry-run payload is `{ dryRun: true, action, approvalRequired, ...divergence }`; a real run returns its action and the resulting revision identity. Expected result from a dry run: ```text sync --dry-run: Would push the local changes as a new cloud revision. local generation 3 (rev_01J8Z4) — modified cloud generation 3 (rev_01J8Z4) files 412 local, 410 cloud — 3 local-only, 1 cloud-only, 2 differing ``` ## The verdict table Both sides are compared against the folder's last synced base revision. `local changed` means the manifest hash moved; `cloud changed` means the head's revision ID or generation moved. | local changed | cloud changed | `push` | `pull` | `sync` | | --- | --- | --- | --- | --- | | no | no | `none` | `none` | `none` | | yes | no | `push` | `none` | `push` | | no | yes | `conflict` | `pull` | `pull` | | yes | yes | `conflict` | `conflict`, or `pull` with `--force` | `conflict` | A dry run computes this with the same function the real command uses, so it cannot promise an outcome the command would refuse. **`conflict` is the only verdict that exits `1`.** Every other dry run exits `0`, including one that reports it would have to ask first. ### `approvalRequired` A forced pull over local changes adds one line to the human output, verbatim: ```text pull --dry-run: Would replace local files with the cloud revision. Requires --yes (or interactive approval) to execute. ``` and one field to the envelope's `data`: ```json {"dryRun":true,"action":"pull","approvalRequired":true} ``` That run exits `0`. Needing an answer is not a refusal — a caller who has one executes exactly this plan. ### Conflicts and validation - A scan blocker aborts all three with `scan_blocked` before any comparison — a folder that cannot be scanned cannot be reconciled. - `push` uses a compare-and-swap against the expected revision ID and generation. A cloud head that moved exits `conflict`: ``Cloud changed since the local base. Run `hlix pull` or resolve with `hlix sync`.`` - `push` with an unchanged manifest is a no-op returning `action: "none"` — it does not manufacture an empty revision. - An ordinary `pull` refuses over local changes: ``Local files changed since the last sync. Push them, or use `hlix pull --force` to replace them.`` - `pull --force` over local changes asks first, listing the cloud generation, how many local-only files will be deleted, and how many will be replaced. A `--json` or non-interactive caller cannot answer and exits `approval_required` pointing at `--dry-run`; a declined prompt exits `cancelled`. - After a pull, the local baseline is recomputed from the **checked-out** tree rather than the source manifest — the bundle checkout is a synthetic snapshot commit whose tracked flags can differ. - `sync` never reaches the forced-pull gate: it only pulls when the working tree is unchanged. ## If reconciliation fails - `not_initialized` — ``This folder is not bound to an imported Hlix project. Run `hlix import .` first.`` - `conflict` on `sync` — ``Both local and cloud changed since the last sync. Pull or push explicitly after reviewing the conflict.`` Run `hlix pull --dry-run` to see the file counts on each side first. - `conflict` on `push` — the cloud moved. Pull or sync, then push again. - `approval_required` on `pull --force` — review with `hlix pull --dry-run`, then repeat with `--yes`. - `cancelled` — the forced-pull prompt was declined. Nothing was replaced. - `scan_blocked` — the same blockers `import` reports; see [If the scan blocks the import](/cli/import/#if-the-scan-blocks-the-import). - `Revision upload has no bundle target.` — the API accepted the revision but returned no upload URL. Retry; the idempotency key makes that safe. ## Next steps --- # hlix tasks Reference for hlix tasks list, get, review, and watch — the default project, the JSONL stream, and what this command group deliberately omits. **`hlix tasks` reads tasks and follows their status.** All four commands are transport-only: with `--json` the body is the API's response passed through **unmodified**. None of them starts, dispatches, or cancels work. ## Prerequisites - A credential that reaches the resolved workspace (see [`hlix auth login`](/reference/cli/auth/)) - A task ID for `get`, `review`, and `watch` — [`hlix tasks list`](#hlix-tasks-list) is where you get one ## `hlix tasks list` ```bash hlix tasks list [--project ] [--status ] [--json] ``` ### Flags - **`--project `** (`string`, default the bound project): restrict the listing to one project. With no flag, a folder bound to a project lists that project's tasks; anywhere else it lists the whole workspace's tasks. Unlike [`projects get`](/reference/cli/projects/), the unbound case is a **fallback, not an error**. - **`--status `** (`string`, default: every status): restrict to one [task status](/running/tasks/#task-statuses). The value is passed through to the API; an unrecognised one is refused there, not locally. - **`--json`** (`boolean`, default `false`): the API's task array, unmodified. Expected result: ```bash hlix tasks list --status building ``` ```text ID STATUS TITLE ------------------------------------ -------- ------------------ b4a1f0d2-3c77-4e1a-9a2b-1d5e6f7c8a90 building Add GET /healthz ``` An empty result prints `No tasks.` and exits `0`. The `TITLE` column falls back to the first line of the description when a task has no title. ## `hlix tasks get` ```bash hlix tasks get [--json] ``` - **`task-id`** (`string`, **required**): omitting it exits `missing_argument`; a second positional exits `invalid_usage`. Expected result: ```text id b4a1f0d2-3c77-4e1a-9a2b-1d5e6f7c8a90 status building title Add GET /healthz project 6f1c2a9e-8f0b-4a7d-9d33-2f0b1c7e5a41 description Return 200 with the build SHA and cover it with a test. ``` ## `hlix tasks review` ```bash hlix tasks review [--json] ``` Returns the **verified review evidence**: the comparison the QA evaluator judged, read back from the repository host rather than summarised by the agent. See [Reviewing output](/running/review/) for the document's meaning and for leaving line-level comments. - **`task-id`** (`string`, **required**). - **`--json`** (`boolean`, default `false`): the envelope. Without it the human view prints the evidence as indented JSON — the published contract does not pin this body, so the CLI states what it found rather than pretending to know the shape. ## `hlix tasks watch` ```bash hlix tasks watch [--json] ``` Consumes the task's server-sent status stream and exits when the task reaches a terminal status or the server closes the stream. - **`task-id`** (`string`, **required**). - **`--json`** (`boolean`, default `false`): emit **one envelope per line** (JSONL), so a consumer can read frames as they arrive. This is the one command whose `--json` output is not a single document. Expected result: ```text queued building testing reviewing done ``` A frame carrying an `error` field prints `error: ` instead of a status. Frames whose payload is not JSON are printed verbatim. ### Conflicts and validation - `watch` returns `0` when the stream closes, including a close the server initiated. Check the last status, not only the exit code. - `list` is the only command in the group that takes command-specific flags; passing `--project` or `--status` to `get`, `review`, or `watch` exits `invalid_usage`. ## What this group does not do - **No execution.** There is no way to start a run, execute a cycle, or dispatch a task from the CLI — dispatching from a dropped connection can duplicate work. Use the API or the dashboard; see [Cycles](/running/cycles/). - **No `tasks logs --follow`.** The public API has a task-status stream, not a log stream, so a `--follow` here would be a poll dressed up as a stream. `tasks watch` is named for what it actually does. ## If a tasks command fails - `missing_argument` — ``A task id is required.`` - `not_found` — no such task in this workspace. The API does not confirm cross-tenant existence; do not infer it. - `{"error":"No review evidence: this task has no branch (it was never dispatched through a cycle)"}` — the task exists but never ran. A standalone task does not execute; see [Tasks](/running/tasks/). - `No tasks.` immediately after starting a run — planning has not produced any yet. Wait and re-run rather than starting a second run. - `unreachable` mid-`watch` — the connection dropped. Re-run `watch`; it re-opens from the current status. - `unauthenticated` / `forbidden` — the credential does not reach this workspace. [`hlix auth status`](/reference/cli/auth/) probes it. ## Next steps --- # Troubleshooting A symptom index into every failure section in these docs, plus the cross-cutting diagnostics that belong to no single command. **This page is the index: find what you saw, and it sends you to the page that owns the fix.** Every command, concept, and integration page carries its own `If … fails` section with verbatim error strings; this page exists for when you do not yet know which one owns your problem. Below the index are the diagnostics that genuinely span commands — integrity, environment recreation, and the outbound MCP server — which live here because no single page owns them. ## Prerequisites Collect the exact command, installed version, exit code, and safe JSON error before anything else: ```bash hlix --version hlix --json echo $? ``` Expected result: a versioned envelope whose `error.code` is the value to search for below. ```json {"schemaVersion":1,"command":"push","error":{"code":"conflict","message":"…","status":409}} ``` Do not paste API keys, protected-file values, raw dotenv contents, or full credential files into support requests. ## By error code Branch on `error.code`, never on `error.message`. Every code the CLI can emit is defined in the [CLI reference](/reference/cli/#error-codes); this maps each to the page that explains what to do. | Code | Usually means | Fix lives at | | --- | --- | --- | | `unauthenticated` | No credential, or the key was rejected | [hlix auth](/reference/cli/auth/#if-auth-fails) | | `forbidden` | Valid key, wrong workspace or role | [hlix auth](/reference/cli/auth/#if-auth-fails) | | `missing_credential` | No key entered at the prompt | [hlix auth](/reference/cli/auth/#if-auth-fails) | | `invalid_base_url` | `--base-url` is not HTTPS or loopback | [hlix auth](/reference/cli/auth/#if-auth-fails) | | `workspace_mismatch` | An override contradicts the folder's binding | [CLI reference](/reference/cli/#where-a-command-points) | | `workspace_required` | Nothing in the chain named a workspace | [hlix import](/reference/cli/import/#if-import-fails) | | `not_initialized` | The folder is not bound to a project | [hlix push, pull & sync](/reference/cli/sync/#if-reconciliation-fails) | | `already_imported` | The folder is bound; push instead | [hlix import](/reference/cli/import/#if-import-fails) | | `scan_blocked` | The import scan found a blocker | [Import a project](/cli/import/#if-the-scan-blocks-the-import) | | `conflict` | Both sides moved, or a compare-and-swap lost | [hlix push, pull & sync](/reference/cli/sync/#if-reconciliation-fails) | | `approval_required` | The caller could not be asked | [hlix push, pull & sync](/reference/cli/sync/#if-reconciliation-fails) | | `cancelled` | An interactive prompt was declined | [hlix push, pull & sync](/reference/cli/sync/#if-reconciliation-fails) | | `not_found` | No such resource **in this workspace** | [hlix projects](/reference/cli/projects/#if-a-projects-command-fails) | | `missing_argument` | A required positional was omitted | [CLI reference](/reference/cli/#if-a-command-fails-before-it-runs) | | `invalid_usage` | Unknown flag, bad value, or too many arguments | [CLI reference](/reference/cli/#if-a-command-fails-before-it-runs) | | `unknown_command` | No such command | [CLI reference](/reference/cli/#if-a-command-fails-before-it-runs) | | `unreachable` | The API could not be contacted | [hlix status](/reference/cli/status/#if-status-reports-something-unexpected) | | `rate_limited` · `server_error` · `not_implemented` | The API refused or failed | [API reference](/api/reference/#if-a-request-fails) | ## By what you saw | Symptom | Fix lives at | | --- | --- | | `hlix: command not found`, or npm `E404` | [Install the CLI](/getting-started/install/#if-installation-fails) | | An install command that fails | [Current versions](/releases/availability/#if-a-version-looks-wrong) | | The dry run reports blockers | [Move an existing project](/guides/quickstart/#if-the-dry-run-blocks) | | A detected environment plan looks wrong | [Environment discovery](/cli/environment-discovery/#if-the-plan-is-wrong) | | A secret is empty inside a task | [Secrets & protected files](/security/secrets/#if-a-secret-does-not-reach-the-workspace) | | An imported skill, agent, or MCP server stayed inert | [Import trust model](/security/trust-model/#if-the-boundary-check-surprises-you) | | A resource import was refused | [hlix skill, agent & mcp import](/reference/cli/resources/#if-a-resource-import-fails) | | A task never starts, or names an unavailable harness | [Connect your agents](/guides/connect-agents/#if-a-task-will-not-start) | | A task exists but will not run | [Tasks](/running/tasks/#if-a-task-will-not-run) | | A cycle will not execute | [Cycles](/running/cycles/#if-a-cycle-will-not-execute) | | Review evidence is missing or stale | [Reviewing output](/running/review/#if-review-fails) | | An approval will not resolve | [Approvals](/running/approvals/#if-an-approval-will-not-resolve) | | The workspace terminal will not open | [The Coding Workspace](/concepts/coding-workspace/#if-the-workspace-will-not-open) | | A memory never reaches a run | [Context, memory & DNA](/concepts/context-memory/#if-memory-does-not-show-up-in-a-run) | | GitHub, Linear, or Telegram will not connect | [Integrations](/integrations/#if-an-integration-will-not-connect) | | An SDK call throws | [TypeScript](/sdk/typescript/#if-the-client-fails) · [Python](/sdk/python/#if-the-preview-client-fails) · [Go](/sdk/go/#if-the-preview-client-fails) | | A generated client is missing an endpoint | [API reference](/api/reference/#what-is-not-here) | | Behaviour changed after an upgrade | [Changelog](/releases/changelog/#if-an-upgrade-goes-wrong) | ## Integrity and concurrency These refusals are deliberate and are **not** recoverable with `--force`. They exist so a revision can never contain a mixture of two states. ### Source changed after the scan A file's size or hash moved while the snapshot was being built. Stop background generators, watchers, or editors, then rerun. hlix stops rather than uploading a mixed state. ### Integrity verification failed Retry once over a trusted connection. A repeated hash, size, Git-bundle, commit, base64, or manifest mismatch indicates corrupted or inconsistent data, not a transient fault. Do not reach for `--force`; it does not apply to integrity. ### The project has active tasks A running task can advance the cloud revision underneath you. Wait for a terminal status, then reconcile: ```bash hlix tasks watch hlix sync ``` ### A protected path was refused hlix will not write through a symlink or a non-directory parent, and will not replace a device or directory with protected bytes. Inspect each path with `ls -ld`, remove only the unsafe local entry after confirming what it is, then retry. ## Environment recreation The scan produces a plan; these are the three ways the plan and reality disagree. ### Confidence is low Add an explicit source — `.cursor/environment.json` or a Dev Container file — pin tool versions, and commit the package-manager lockfile. Explicit configuration outranks every inference. Re-run `hlix import --dry-run` and read the plan. ### A setup command fails in the cloud Run the same command locally in a clean environment and review dependency lifecycle scripts. Check whether the required key names appear in the dry-run report: a missing **protected secret** stops dispatch outright, while a variable only the application needs may fail later, inside its own setup command. ### It works locally and not in the cloud Compare runtime versions, architecture-specific dependencies, implicitly-installed global tools, ignored files, ports, and credentials. Make those assumptions declarative in the repository rather than patching the Coding Workspace by hand — a hand-patched sandbox is not reproducible and does not survive a recreate. ## The official MCP server This is the **outbound** `@hlix/mcp` server that local agent clients run. It is a different thing from importing a third-party MCP definition into a cloud project ([that is here](/cli/resources/#common-failures)). ### The package or Registry entry is missing Use the exact version shown on the [MCP server page](/mcp/). A client that cannot resolve it is usually pinned to a version that was never published — check the pin against [Current versions](/releases/availability/). ### The server exits before initialization Confirm `node --version` is 22.13 or later. Then confirm the client launches `npx` directly, with the pinned package as a **separate argument** — not wrapped in a shell string. ### Tools point at the wrong project Start the client from the initialized project root, or pass an explicit project ID. The server reads `.hlix/config.json` from its working directory and deliberately does not search parent or home directories. ### Write tools are absent That is the secure default. Set `HLIX_MCP_ALLOW_WRITES=1` in the **server process** environment and restart the client. The tool list should then add exactly three: task creation, task comments, and cycle proposals. ### The client shows malformed protocol output The server reserves stdout for MCP messages. Capture **stderr only**, redact local paths and IDs as your policy requires, and include the package version plus the client's initialization error. ## Safe support bundle Share only: - CLI version - command name and flags, with sensitive paths generalized - exit code - `error.code`, HTTP status, and request ID - file counts, sizes, hashes, and secret **key names** only when your policy permits Never share credential values, `credentials.json`, protected API responses, or raw environment files. A `--dry-run --json` report contains no secret values, but it does contain paths and key names that reveal architecture — treat it as internal. ## Next steps --- # Current versions The published version of every hlix package, and how to install each one. **The current published version of each hlix package, and how to install it.** Every version on this page is rendered from the release ledger, so it always matches what the registry serves. ## `@hlix/cli` The `hlix` command: import a project, keep it in sync, and inspect projects and tasks from a terminal. ```bash npm install --global @hlix/cli hlix --version ``` Run it once without installing: ```bash npx @hlix/cli --help ``` In CI, pin the version instead of floating to `latest`: ```bash npm install --global @hlix/cli@0.2.0 ``` Start at [Install the CLI](/getting-started/install/), then [Authenticate](/getting-started/authentication/). The full command surface is in the [CLI reference](/reference/cli/). ## `@hlix/sdk` The first-party TypeScript client: authentication, retries, typed errors, uploads, and SSE streaming against the same contract the CLI uses. ```bash npm install @hlix/sdk ``` Pin the version in CI so a build never picks up a client you have not tested against: ```bash npm install @hlix/sdk@0.2.0 ``` See the [TypeScript SDK](/sdk/typescript/) for client creation, the error classes, and the retry policy. To generate a client in another language, use the [OpenAPI contract](/api/openapi/) directly. ## `@hlix/mcp` The official MCP server: it exposes workspace-scoped projects, tasks, cycles, comments, and review evidence to Cursor, Claude Code, Codex, and Windsurf. Your MCP client launches the server itself — there is nothing to install by hand. The version is pinned so a client restart cannot silently upgrade a server holding your workspace credentials. Cursor (`.cursor/mcp.json`), Claude Code (`.mcp.json`), and Windsurf (`~/.codeium/windsurf/mcp_config.json`) use: ```json { "mcpServers": { "hlix": { "command": "npx", "args": [ "-y", "@hlix/mcp@0.2.0" ] } } } ``` Codex uses `.codex/config.toml` in a trusted project or the user config: ```toml [mcp_servers.hlix] command = "npx" args = ["-y", "@hlix/mcp@0.2.0"] default_tools_approval_mode = "writes" ``` Authenticate with `hlix auth login --workspace `. Run `hlix import .` to create and bind the cloud project; `hlix init` alone does not write a project ID. See the [official MCP server](/mcp/) for the tool list, the write gate, and per-client notes. ## Check what you have ```bash hlix --version npm view @hlix/cli version ``` Expected result: two version numbers. When they differ, the installed CLI is older than the registry's current release — upgrade with the install command above, or pin deliberately. ## If a version looks wrong - **`hlix --version` disagrees with `npm view`.** You have an older global install. Re-run the install command, then open a new shell so `PATH` resolves the new binary. - **`npm error E404` on a version you see here.** Check for a typo in the package name; `@hlix/cli`, `@hlix/sdk`, and `@hlix/mcp` are the only three. - **An MCP client keeps starting an old server.** The configuration pins a version. Update the pin in the client config and restart the client — the server does not self-upgrade, deliberately. - **A generated API client is missing an endpoint.** It is missing from the published contract, so no client can reach it. See [what the contract does not cover](/api/reference/#what-is-not-here). ## Next steps --- # Changelog Current release status and notable changes across hlix developer tooling. **A changelog entry is a user-visible behaviour change in one package.** This page summarises them for the developer tooling; package-specific release notes are finalised from Changesets when a concrete version is published. ## Prerequisites Nothing to read this page. Before acting on an upgrade entry, make sure the folder's local work is committed or pushed. ## Unreleased — CLI - Added verified interactive API-key login with explicit workspace selection and safe CI environment overrides. - Added `hlix init` and project-local `.hlix` configuration/state boundaries. - Added read-only environment discovery across Cursor, Dev Containers, package managers, Docker, Gitpod, Procfile, tool versions, ports, and environment references. - Added `hlix import [folder]` with dry-run reporting, Git-history preservation, secret-history findings, encrypted protected files, resumable bundle upload, integrity verification, and idempotent project creation. - Added immutable revision `push`, `pull`, and `sync` with compare-and-swap conflict detection. - Added explicit skill, agent, and MCP import commands with HTTPS and stdio trust gates. - Added stable JSON envelopes, typed error codes, and task status streaming. ## Unreleased — TypeScript SDK - Added workspace-scoped API-key and session credentials. - Added typed errors with validation issues, request IDs, retry metadata, and transport failures. - Added bounded retry for idempotent methods only. - Added typed project, task, cycle, import, and revision operations from the OpenAPI contract. - Added resumable bundle upload/download helpers and Server-Sent Event parsing. ## Unreleased — generated SDK previews - Added reproducible Python and Go source trees generated from OpenAPI 3.1. - Pinned OpenAPI Generator `7.22.0` and its downloaded JAR checksum. - Kept preview installation source-only until language-specific publishing and clean-consumer verification complete. ## Unreleased — official MCP server - Added the `@hlix/mcp` stdio server with strict schemas and the same workspace-scoped TypeScript SDK used by the CLI. - Exposed bounded project, task, cycle, comment, and verified-review reads by default. - Added an explicit process-level write gate for task creation, task comments, and cycle proposals; destructive operations and direct execution endpoints remain unavailable. A proposal on an active autonomous project can resume planning and lead to task dispatch. - Added owner-only CLI credential reuse, project-binding mismatch checks, bounded results, and sanitized structured errors. - Added pinned npm, MCP Registry, clean-client handshake, checksum, GitHub release, and release-ledger gates. ## Release status [Current versions](/releases/availability/) shows the published version of each package, rendered from the release ledger. ## Compatibility notes - Imported projects use manifest schema version 2. - Local project config and revision state use schema version 1. - CLI JSON output uses envelope schema version 1. - The public API contract is OpenAPI 3.1 with independent semantic versioning. ## Before upgrading 1. Read the package's GitHub release notes and migration section. 2. Run `hlix --version` and record the current version. 3. Commit or back up local project work. 4. Run `hlix sync` and resolve divergence before replacing the binary. 5. Verify `hlix --help`, `hlix projects list`, and one dry-run import after upgrade. ## If an upgrade goes wrong - **`hlix --version` still prints the old version** — an older binary is earlier on `PATH`. Check `which -a hlix`. - **`hlix sync` reports a conflict after upgrading** — resolve the divergence, not the version. Run `hlix sync --dry-run` for the file counts on each side. - **A `--json` consumer breaks** — check `schemaVersion` first. The envelope version is the contract; human formatting is not, and may change between releases. - **A flag disappeared** — that is a major change and carries a Changeset saying so. Read the package's release notes before pinning around it. - **`npm view` reports no version at all** — that channel has not opened. See [Current versions](/releases/availability/). ## Next steps --- # Versions & releases Understand independent platform, API, CLI, SDK, MCP, npm, GitHub, Registry, and Homebrew release identities. **A release identity is the version number that describes one surface, and hlix keeps six of them separate.** A web or backend release must not accidentally publish an npm package, and a package release must not promote production. For the current version of each package, see [Current versions](/releases/availability/). This page is the scheme behind those numbers. ## Version identities | Surface | Scheme | Example shape | Meaning | | --- | --- | --- | --- | | Platform | date-based CalVer | `platform-vYYYY.MM.DD` | one reviewed production cut | | CLI | semantic versioning | `cli-v1.2.3` | public command/output package | | TypeScript SDK | semantic versioning | `sdk-v1.2.3` | public library package | | MCP server | semantic versioning | `mcp-v1.2.3` | public stdio tool server and Registry entry | | API contract | semantic versioning | `1.0.0` | compatibility of published request/response shapes | | API URL | major prefix | `/v1/api` | coexistence boundary for breaking API generations | These numbers move independently. An internal platform deployment may not change the API or packages. An SDK patch may improve packaging without changing the API contract. ## Package changes User-visible CLI, SDK, and MCP changes carry a Changeset: - patch: backwards-compatible fix or polish; - minor: new command, method, or optional capability; - major: breaking flag, JSON contract, signature, or behavior. The package changelog is generated from reviewed Changeset summaries. The tag must match the package name and exact manifest version. ## Publication gates A package is not called available merely because source code or a workflow exists. The release path verifies: 1. a real non-private package version and license; 2. typecheck, build, and package tests; 3. exact tarball contents, including README and license; 4. CLI execution from the built artifact; 5. npm trusted publishing with short-lived OIDC identity; 6. registry version lookup; 7. clean-project installation and runtime smoke test; 8. a matching GitHub release and checksums; 9. for MCP, an exact active MCP Registry record naming the same npm package and version; 10. a reconciled release ledger before the docs or application advertise the package. The MCP path is recoverable across partial releases. If npm already contains the exact packed artifact, a rerun verifies its integrity and continues with Registry, GitHub release, and ledger reconciliation. It refuses an existing npm version whose integrity differs, an inconsistent Registry record, or a release asset with different bytes. The npm access token is not part of this flow. ## Homebrew Homebrew is a distribution layer over a verified CLI artifact, not a second build with unrelated contents. A formula should pin a released archive and SHA-256, then pass `hlix --version` and `hlix --help` in a clean environment. The tap is announced only after its repository, formula, checksum, and install test exist. See [Install the CLI](/getting-started/install/) for current availability. ## Platform releases Platform release notes use the `platform-v…` namespace. Publishing a reviewed platform release promotes the backend/web release path. Package tags are excluded from that production trigger. ## Verify what you have ```bash hlix --version npm view @hlix/cli version npm view @hlix/sdk version npm view @hlix/mcp version ``` An npm `E404` means that channel is not yet available; it is not evidence that the source install failed. [Current versions](/releases/availability/) is the page that says which channels are open. For released builds, compare the installed version to the corresponding GitHub release and package changelog. ## If the versions disagree - **`hlix --version` differs from `npm view @hlix/cli version`** — you are running a source build or an older global install, not the registry's latest. Both are valid; know which one you are testing. - **A tag exists but the package does not** — a tag is not a release. Publication runs the ten gates above; only the reconciled ledger marks a package available. - **A package version does not match its GitHub release checksum** — do not use it. Gate 8 refuses that combination, so its presence means the artifact changed after publication. - **A platform release changed nothing in a package** — expected. The numbers move independently by design. - **A Homebrew formula is missing for a released CLI** — the tap is announced separately, after its repository, formula, checksum, and install test exist. ## Next steps --- # Approvals Park a cycle on a human decision before agents start work, and resolve the pending requests that hold it. An **approval** is a blocking pending request that stops a cycle from being dispatched until a person decides. Projects are autonomous by default. Two things can open one: a project policy you turn on, and a [risk lane](/concepts/triage/) severe enough to demand one on its own. Neither is hidden — both name themselves on the request. ## Prerequisites - A project you can administer (agency `owner`, `admin`, or `member`) - A cycle that has not yet been dispatched ## Turning the gate on Approval is the project-level `requiresApproval` flag, default **off**. **In the dashboard:** it is the **approval gate** switch, in two places for the same flag — **Project settings → Agent → Behavior**, and inline on a cycle's **Request Queue** tab for the operator already looking at a run. **Over the API:** ```bash curl -sS -X PATCH "$HLIX_BASE_URL/v1/api/projects/$PROJECT_ID" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"requiresApproval":true}' ``` With the flag set, the project orchestrator opens a blocking `deployment_approval` request **before** launching a gated cycle and parks the run. Approving resumes dispatch; denying fails that cycle. With the flag off, an autonomous run is byte-for-byte unchanged — the gate adds no step it does not need to. ### A default for new projects Set the flag once for the workspace and every project created afterwards starts with it: ```bash curl -sS -X PUT "$HLIX_BASE_URL/v1/api/settings/org/project-defaults" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"requiresApproval":true}' ``` Three things about it are worth being precise on: - **It applies at creation only.** A project's own `requiresApproval` is concrete from the moment the row exists, so changing the workspace default never reaches back and rewrites projects that already exist. Change those individually. - **An explicit choice wins.** Creating a project with `requiresApproval` in the body uses that value; the default fills in only when the request says nothing. - **`null` clears it**, returning the workspace to "no default"; omitting a field leaves it as it was. `GET` on the same path reads the current values. Writing this setting requires `owner` or `admin`. It is not in the published contract, so there is no generated SDK method for it. ## When the lane demands it instead A cycle can park for approval on a project that never turned the gate on. [Triage](/concepts/triage/) classifies every cycle into a risk lane, and four of those lanes — `stop`, `design-review`, `architecture-review`, and `manual-lane` — open the same blocking request on their own. The two conditions OR together: the project's flag, or the cycle's lane. Whichever says "gate" wins, and there is no way to opt a lane-gated cycle out short of re-triaging it. The request's `context` names the cycle's lane whenever it has one: ```json {"projectId":"…","gate":"cycle_dispatch","lane":"architecture-review"} ``` Read it as the classification, and infer from it. One of the four lanes above means this cycle would have parked whatever the project's setting says. Any other lane — or no `lane` key at all — means the project's own policy is what stopped it. [Review & QA gates](/concepts/review-gates/#lanes-that-demand-a-human) explains what each lane is classifying. ## Finding what is waiting ```bash curl -sS "$HLIX_BASE_URL/v1/api/pending-requests?status=pending" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` `status`, `cycleId`, and `taskId` are the supported query filters. A per-cycle read is also available at `GET /v1/api/cycles/:id/pending-requests`. Each row carries `requestType`, `blocking`, `requiredActor`, `title`, `description`, `context`, `status`, `deadline`, and — once decided — `decision`, `decidedBy`, `decidedAt`, and `resultingAction`. In the dashboard, **Approvals** lists every pending request across the workspace. ## Request types Eight types share one queue. Only the first one resumes a parked run. | Type | Raised when | Resolving it | | --- | --- | --- | | `deployment_approval` | A gated cycle is about to be dispatched | Approve resumes dispatch; deny fails the cycle | | `tool_permission` | An agent asks to use a gated tool | The decision is replayed onto the live agent session | | `plan_approval` | A plan wants sign-off | Recorded only | | `code_review` | A change wants human review | Recorded only | | `conflict_resolution` | A merge conflict needs a decision | Recorded only | | `budget_exceeded` | A cost ceiling was hit | Recorded only | | `requirement_clarification` | An agent needs an answer from you or the client | Recorded only | | `cycle_proposal` | A cycle is proposed for adoption | Recorded only | "Recorded only" is deliberate and honest: those types capture the decision and its audit trail, but do not themselves resume a workflow. ## Deciding There is no separate approve or deny endpoint. Both are the `status` field on one call. ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/pending-requests/$REQUEST_ID/resolve" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"status":"approved","decision":"Ship it — the migration is reversible."}' ``` `status` is `approved` or `denied` and is required; `decision` is optional free text up to 5000 characters. The response is the updated request row with `status`, `decision`, `decidedBy`, and `decidedAt` written. Every decision emits an event and writes a guaranteed [audit](/concepts/review-gates/) row. To withdraw a request instead of deciding it: ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/pending-requests/$REQUEST_ID/cancel" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` Cancelling a `deployment_approval` un-parks the cycle as a **deny**. A cancelled gate does not leave a run waiting forever. ## Asking the client a question `POST /v1/api/pending-requests/client-question` opens a non-blocking `requirement_clarification` addressed to the client collaborator: ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/pending-requests/client-question" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"cycleId":"'"$CYCLE_ID"'","title":"Invoice numbering","question":"Should invoice numbers restart each fiscal year?"}' ``` The body accepts exactly `cycleId`, `title`, and `question` — `requestType` and `requiredActor` are set by the server and rejected if you send them. Only the agency can ask; the named client can answer. ## Contract coverage ## If an approval will not resolve - `{"error":"Request already approved"}` — someone decided it first. Re-read the row before acting. - `{"error":"Not found"}` — wrong id, another workspace, or you are a client collaborator and the request was not addressed to you. Existence is deliberately not revealed. - The cycle stays parked after approval — confirm the request was `deployment_approval` and carried a `cycleId`. Only that combination resumes a run. - Approving does nothing for a `plan_approval` or `code_review` row — expected. Those types record the decision without resuming a workflow. - A run parked and no request appears — check that `requiresApproval` is set on the project you are actually running. - A cycle parked although `requiresApproval` is `false` — read the request's `context.lane`. A forced lane gates on its own. See [Triage](/concepts/triage/). - You set the workspace default and existing projects did not change — by design; it applies to projects created after it. Set those projects individually. ## Next steps --- # Cycles Propose a cycle, execute it, stream its timeline, and collect the artifacts it produced. A **cycle** is a meaningful chunk of a project — a feature, a release, a phase — and it is the unit that actually executes. Tasks fan out inside a cycle run; they do not run on their own. ## Prerequisites - A project with at least one cycle, usually planned by a [project run](/getting-started/quickstart/) - Agency role (`owner`, `admin`, `member`); a client collaborator with steer rights can execute but not edit - Model access configured for the workspace ## Proposing a cycle ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/cycles" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"projectId":"'"$PROJECT_ID"'","title":"Health and readiness","description":"Add liveness and readiness endpoints with tests and docs."}' ``` The response shape — `adopted` is `true` only when the orchestrator folded the proposal into the roadmap on this pass: ```json {"proposed":true,"adopted":false} ``` `projectId`, `title` (1–200), and `description` (1–10 000) are required. An adopted proposal becomes a cycle at status `triage`, and the [Triage Agent](/concepts/triage/) decides whether it goes any further. That is true of every intake path — chat, the API, Linear, or the project's founding brief — so a cycle in your list at `triage` has been created but not yet admitted. ## Executing a cycle ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/cycles/$CYCLE_ID/execute" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` Expected result: ```json {"message":"Cycle execution started","threadId":":"} ``` The optional `?mode=` query takes `sandbox` (default) or `desktop`. Execution is gated on status: only `draft`, `planning`, `failed`, and `interviewing` cycles can be executed. A cycle still in `triage` is not executable — Triage admits it first. Re-executing a `failed` or `interviewing` cycle resets it to `draft` and returns every `failed` or `blocked` task to `queued` — a retry is a genuine retry, not a second parallel run. That reset is itself a recorded transition, attributed to you. The response means the run was **queued**, not finished. If queuing itself fails you get a `502` and nothing was started: ```json {"error":"The cycle run could not be queued, so it was not started. Nothing is running — try again."} ``` ## Cycle statuses | Status | Meaning | | --- | --- | | `triage` | Created and awaiting the [Triage Agent's](/concepts/triage/) decision | | `draft` | Admitted, not yet running | | `interviewing` | The orchestrator is clarifying requirements with you | | `planning` | Decomposing the cycle into tasks | | `executing` | Tasks are running | | `e2e` | End-to-end verification | | `pr_created` | Terminal — the work landed as a pull request | | `failed` | Terminal — the cycle failed | | `canceled` | Terminal — Triage refused the request | ## Stage history Every status move is a **governed transition**: it is recorded on the cycle, attributed, and stamped with the platform rules in force at the time. `GET /v1/api/cycles/:id` returns the whole record as `stageHistory`, oldest first, alongside a `revision` counter. ```json { "from": "triage", "to": "draft", "at": "2026-08-12T09:41:07.220Z", "actor": { "type": "agent", "id": "triage-agent" }, "ruleSetVersion": "hlix-web-v1", "reason": "Schema change is understood and reversible." } ``` | Field | What it answers | | --- | --- | | `from` / `to` | Which move this was | | `at` | When | | `actor.type` | Who decided: `human`, `agent`, or `rule` | | `actor.id` | Which one — a user id, or a stable agent/rule identifier such as `triage-agent`, `orchestrator`, `gate-denied` | | `ruleSetVersion` | Which version of the platform rules governed the move | | `reason` | Free text, when the mover supplied one | `actor.type` is the distinction that matters. `human` is a person acting through the dashboard or the API. `agent` is an LLM-driven decision — the Triage Agent forwarding a request, the orchestrator submitting a plan. `rule` is deterministic platform logic with no model involved, such as a denied approval failing the cycle. `revision` is a compare-and-swap counter, incremented once per transition. Two writers racing the same cycle cannot both win: the loser is rejected with the row's real current revision rather than silently overwriting the winner. ## Priority and lane Triage writes two more fields onto every cycle it forwards. - **`priority`** — an integer derived from the urgency tier Triage judged: `300` urgent, `200` high, `100` normal, `0` low. Ready cycles are dispatched highest first. - **`lane`** — the risk lane the platform rules resolved from the complexity and risk judgement. Four lanes force a human approval before dispatch even when the project has not opted in. Both are on the cycle row and in the published contract. [Triage](/concepts/triage/) covers how each is derived. To list a project's cycles in dispatch order rather than creation order: ```bash curl -sS "$HLIX_BASE_URL/v1/api/cycles?projectId=$PROJECT_ID&orderBy=priority" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` `orderBy` accepts `createdAt` (the default, newest first) or `priority` (highest first, ties keeping creation order). Any other value is a `400`. The dashboard's cycles grid offers the same two sorts. ## Watching a cycle `GET /v1/api/cycles/:id/stream` streams the cycle's event timeline rather than a status field, so you see the run's narrative as it happens. ```bash curl -N "$HLIX_BASE_URL/v1/api/cycles/$CYCLE_ID/stream" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` - Unnamed events carry one timeline event row each (`entityType`, `eventType`, `payload`, `actorType`, `traceId`, `timestamp`). - `event: done` fires with `{"status":"pr_created"}` or `{"status":"failed"}` when the cycle reaches a terminal status, then the stream closes. Unlike the task stream, this one *does* announce its ending. - `event: keepalive` with an empty payload arrives roughly every 30 seconds. - The stream is bounded at 600 polls of 2 seconds — about 20 minutes. Reconnect for longer runs. For a non-streaming read of the same material use `GET /v1/api/cycles/:id/timeline` (cycle plus child-task events) or `GET /v1/api/cycles/:id/events` (cycle events only). ## Reading a cycle `GET /v1/api/cycles/:id` returns the cycle row plus its tasks — each with a `dependsOn` array — and `previewUrl` and `linearIssueUrl` when they exist. ```bash curl -sS "$HLIX_BASE_URL/v1/api/cycles/$CYCLE_ID" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` `GET /v1/api/cycles?projectId=…` lists them. ## Managing the task set | Operation | Endpoint | Constraint | | --- | --- | --- | | Add a task | `POST /v1/api/cycles/:id/tasks` | `title` and `description` required; `dependsOn` optional | | Edit a task | `PATCH /v1/api/cycles/:id/tasks/:taskId` | `title`, `description`, `sortOrder` | | Remove a task | `DELETE /v1/api/cycles/:id/tasks/:taskId` | Only `queued` or `blocked` tasks | | Edit the cycle | `PATCH /v1/api/cycles/:id` | `title`, `description`, `targetBranch` | | Delete the cycle | `DELETE /v1/api/cycles/:id` | Only `draft` or `failed` cycles | Deleting a running cycle is refused rather than racing the engine: ```json {"error":"Can only delete cycles in draft or failed status"} ``` ## Talking to a cycle `POST /v1/api/cycles/:id/messages` sends a message into the cycle thread (`content` required; `agentId` addresses one worker's 1:1 thread; `messageId` makes it idempotent). While the cycle is `interviewing`, a message with no `agentId` can advance the lifecycle — the response carries `lifecycleAdvanced` and `lifecycleReason`. `POST /v1/api/cycles/:id/messages/stream` is the streaming form. Its SSE events are named, unlike the task stream: | Event | Payload | | --- | --- | | `user_message` | `{"id":…,"content":…}` — your message, as stored | | `token` | A raw text chunk, not JSON-wrapped | | `tool_start` | `{"name":…,"input":…}` | | `tool_end` | `{"name":…,"output":…}` | | `complete` | `{"id":…,"content":…}` — the final assistant message | | `error` | `{"message":…}` — then the stream returns | ## Artifacts and cost - `GET /v1/api/cycles/:id/artifacts` lists what the run produced. - `GET /v1/api/cycles/:id/artifacts/:filename` returns `{"url":…,"key":…}`, or the raw bytes with `?download=1`. - `GET /v1/api/cycles/:id/usage` returns `{"totals":{…},"byRole":[…]}` with token counts and cost per agent role. ## If a cycle will not execute - `{"error":"Cycle cannot be executed in \"executing\" status"}` — it is already running. Watch it instead of starting a second run. - `{"error":"Cycle not found"}` — wrong id, or another workspace. Check `X-Organization-Id`. - `{"error":"Project not found for cycle"}` — the cycle outlived its project; it cannot be run. - `{"error":"The cycle run could not be queued, so it was not started. Nothing is running — try again."}` — a `502`. Nothing was half-started; retry is safe. - `{"error":"Can only delete tasks in queued or blocked status"}` — the task is past the point where removing it is safe. - `{"proposed":true,"adopted":false}` and no cycle appears — the orchestrator did not adopt the proposal. Re-read the roadmap before proposing again. - `{"error":"Cycle cannot be executed in \"triage\" status"}` — the [Triage Agent](/concepts/triage/) has not admitted it yet. There is no way to execute past that decision. - The cycle reads `canceled` — Triage refused the request. The reason is on the last `stageHistory` entry. - The cycle parks and nothing happens — the project has approval required, **or** its lane demands one regardless. See [Approvals](/running/approvals/). ## Next steps --- # The dashboard A tour of the hlix web app — the portfolio overview, the Canvas, and the screens where review and approval happen. **The dashboard is the full product surface** — everything the CLI and API can do, plus the things they cannot: watching the team work, reviewing a diff line by line, and approving a deployment. It is at [app.hlix.ai](https://app.hlix.ai/). ## Prerequisites - A hlix account and a workspace you are a member of - Agency role (`owner`, `admin`, or `member`) for most screens; client collaborators see a deliberately narrower surface ## How the navigation is organised The sidebar has three bands, and the order is the argument: portfolio first, then the things that need you, then workspace-wide records. | Band | Items | Answers | | --- | --- | --- | | Portfolio | **Overview** · **Projects** · **Customers** · your recent projects | "How is delivery going?" | | Attention | **Inbox** · **Approvals** · **Activity** | "What needs me?" | | Workspace | **Global Knowledge** · **Custom Skills** · **Integrations** · **Invoices** · **Audit Log** | "What is configured, and what happened?" | **Approvals carries a count badge** when something is waiting on a human. It is the only navigation item that does, because it is the only one where work is blocked until you act. ## The Overview screen The portfolio screen: a stat row across the top, then delivery, workload and billing panels, then the two lists that tell you where to go next. **Screenshot:** The Overview screen. A stat row reads Customers 3, Projects 7, Active customers 3, Need attention 3, Pending decisions 1, Outstanding €5,712.00. Below it, a Delivery health donut splits 7 projects into 4 on track, 2 needing attention and 1 at risk; a Customer workload bar chart compares Atlas Commerce, Meridian Health and Northstar Studio; and a Billing health chart shows issued, collected, outstanding and unbilled amounts. A Project health list ranks at-risk work first, beside a Needs attention list of the most urgent delivery and billing signals. **Project health ranks at-risk work first** — a project with a failed cycle appears above one that is merely blocked. **Needs attention** is the narrower list: the signals that will not resolve on their own. This is the screen to open first thing; it is not where you do work. ## Projects Projects are **grouped by customer**, with an `Internal / unassigned` group for work not attached to a client account. **Screenshot:** The Projects screen in Grid view, with view-by controls for Grid, List, Kanban and Timeline. Projects are grouped under customer headings: Atlas Commerce with two projects, Meridian Health with one. Each card shows a small team glyph, a cycle progress bar, the project name, when it last changed, its repository state, spend to date, and a stack tag reading typescript,react,hono. Four views of the same set — **Grid** for scanning, **List** for comparing columns, **Kanban** for delivery stage, **Timeline** for when things land. Each card carries the project's cycle progress, spend so far, and whether it has a connected repository. `No repository — results stay downloadable` is a repo-less project, not an error. ## Customers A customer record owns many projects without double-counting them, which is what makes the portfolio numbers add up. **Screenshot:** The Customers screen. A stat row reads Customers 3, Projects 5, Active work 5, At risk 1. A table lists Atlas Commerce with 2 active projects and a red '1 at risk' delivery-health badge, Meridian Health with 1 project marked On track, and Northstar Studio with 2 projects, €5,712.00 outstanding, and an On track badge. **Delivery health and outstanding balance sit on the same row** — the customer whose work is at risk and the customer who owes you money are rarely the same one, and this is where you see both. ## The Canvas Opening a project lands on its **Canvas**: the orchestrator and the worker agents attached to it, as cards you can watch while they run. **Screenshot:** A project's Canvas. A header reads 'The team — 1 orchestrator + 6 agents · 1/4 cycles · US$0.00'. An orchestrator card sits at the top, labelled 'plans, dispatches and reviews', connected down to six worker cards: Maya Suzuki (UI Engineer), Tom Walker (Security Engineer), Priya Patel (Technical Writer), Marcus Reyes (Product Engineer), Sarah Chen (Senior Engineer) and Alex Park (QA Engineer). Each card shows active tasks, overall progress, and an execution scope. Below the team, cycles sit in status columns — Triage, Running, Done, Failed and Canceled — each holding a cycle card with its status chip, task count and spend. A right-hand rail offers Chat and Logs tabs and a composer asking what you want the agents to work on. This is [agents and orchestration](/concepts/agents/) made concrete: **one orchestrator, six worker personas**, each card carrying its own active-task count and progress. Agents have names from your workspace roster — the persona is the role, the name is the agent. Below the team, the project's cycles sit in **status columns** — Triage · Planned · Running · Done · Failed · Canceled — with a column drawn only once something is in it, and arrows carrying the roadmap order across them. A cycle the [Triage Agent](/concepts/triage/) refused therefore has a place on the board instead of reading as work that never started. The cards are not draggable: every cycle move is a governed transition, so the board reports status rather than setting it. The tabs across the top are **Canvas · Request Queue · Kanban · Context · Memory · Transcript · Usage · Preview · Terminal · Desktop** — the Canvas is one view of a project, not the whole of it. **Repos**, **Share** and **Project settings** sit alongside them, and **Open in Editor** hands the project to your local tooling. **Terminal**, **Desktop** and **Transcript** are agency-only; a client collaborator does not see them, because the backend refuses them anyway. The right rail is where you talk to the team. Its composer — *"What do you want the agents to work on today?"* — is what starts a run. ## Watching the team work Four surfaces show a running project, and they answer different questions. ### The timeline, in the Chat tab The right rail's **Chat** tab interleaves your conversation with the pipeline's own steps as they happen: **Triage & Planning**, **Dispatch**, **Build Planning**, **Build**, **Evaluator**, **Optimizer**, **Evaluate Roadmap**, and the rest, each marked *in progress*, *done*, *failed*, or *waiting*. Steps that reach a decision say what it was. **Triage & Planning** reports each cycle it judged — *"forwarded 'Fix checkout' — high urgency, fast-review lane — user-facing payment path"*, or the reason a cycle was canceled — and **Evaluator** reports its QA verdict with a one-line reason when it asks for changes. A step that fanned out work reports how much — *"6 tasks ready across 2 batches"* — and a step waiting on an approval names the request it is waiting for. The timeline is still a narrative and not the record: the durable version of a triage decision is its [audit row and stage history](/concepts/triage/#what-gets-recorded), and the full QA feedback is on the task's review. The timeline also starts where you attach: it shows the run from the moment you opened it, and reloading the page starts it over. ### Logs **Logs**, beside Chat in the right rail, is the raw output the coding agents wrote while they worked — one line per line, redacted against the project's secrets, with error lines marked. Select an agent card and it narrows to that agent; select nothing and it shows the whole cycle. The same material for the whole project has its own full-page view under **Logs** in the project settings navigation. ### The orchestrator card Expand the orchestrator's card and its **Thinking** tab streams the turn as it happens — its reasoning, its text, and each tool it calls, marked pending then complete. Beneath it is a composer that **steers**: send a message into the turn in flight and it is folded in rather than queued for afterwards. Steering is orchestrator-only. A worker runs inside the orchestrator's turn, so there is nothing to steer independently. ### Worker cards A worker card carries two tabs, and which one is live depends on the [harness](/guides/connect-agents/) that ran the task. **Thinking** streams tokens — reasoning, text, and each tool call — when the task ran on **Hlix**, our own harness, because hlix drives that turn itself and can watch it. **Activity** is the steps the agent actually took: commands, edits, tool calls, read from its sandbox output. This is the live signal for an external coder like Claude Code or Codex, whose conversation happens inside its own process. Those workers open on Activity, and their Thinking tab stays empty rather than inventing a stream that does not exist. ### Transcript Live streams are gone when you reload. **Transcript**, a project tab, is the post-hoc record: for each pipeline agent, what it was actually told and what it answered — the prompt and the response, redacted and truncated. It covers the planner and tasks that ran on **Hlix**. A task run by an external coder is not in it, for the same reason its Thinking tab is empty — that conversation happens inside the coder's own process, not through hlix's agent runtime. Transcript is agency-only, alongside Terminal and Desktop. ## Inbox Everything waiting on a decision from you, newest first — the single queue to clear before you go looking for work. **Screenshot:** The Inbox screen, headed 'Everything waiting on a decision from you, newest first'. A 'Waiting on a decision' group holds one item, 'Approve the customer onboarding flow', asking the reader to review the preview and confirm the onboarding sequence. A separate 'Failed runs' group holds one entry named builder. It groups by **what kind of answer is needed**: a decision that blocks a run, and a failed run that needs a human to look. Approvals is the narrower view of the first group. ## Approvals Where a gated deployment waits. A project with approval required parks before dispatching a gated cycle, and the run resumes only when someone resolves the request here. **Screenshot:** The Approvals screen, listing pending decisions that block a cycle from dispatching until a human approves or denies each one. Approving dispatches; denying fails that cycle. See [Approvals](/running/approvals/). ## Activity and Audit Log Two different records, deliberately separated. **Screenshot:** The Activity screen: a reverse-chronological feed of what is happening across the workspace right now. **Activity** is the product feed — best-effort, for humans following along. **Screenshot:** The Audit Log screen: a filterable table of security-sensitive actions recording who did what, to what, when, and with what outcome. **Audit Log** is the compliance record — immutable, hash-chained, and restricted to `owner` and `admin`. If you are investigating an incident, this is the one that answers; the activity feed may have gaps by design. ## Project settings **Project settings** opens a second navigation, grouped the way an operator reasons about a project rather than the way its routes are filed. Every destination is agency-only. | Group | Items | | --- | --- | | Delivery | **Budget** · **Metrics** · **From Linear** · **Logs** | | Sources | **Repositories** · **Work Intake** · **Connections** | | Coding Workspace | **Runtime** · **Environments** · **Secrets** · **Packs** | | Agent | **Agents** · **Behavior** | | Team | **Collaborators** | Four of these decide how the project behaves rather than what it contains: - **Metrics** reads the project's real flow out of cycle stage history — throughput, cycle time, time per stage, aging work, and how much of the automation held without rework. See [Delivery metrics](/running/metrics/). - **Behavior** is where the approval gate lives. It is the same switch as the one on a cycle's Request Queue tab, in its settings home rather than beside a run. - **Work Intake** controls whether Linear-sourced requests may become cycles in this project. - **Repositories** carries each repo's **worktree setup command** — see below. ### The worktree setup command Every task gets a fresh git worktree, which means a fresh, uninstalled checkout. A repository's setup command is the shell command that prepares it — typically something like `bun install && bun run build`. It runs in **the task's own worktree**, never the shared clone, immediately after the worktree is created and **before the coding agent starts**. Both kinds of harness honour it — Hlix, and an external coder launched through the in-sandbox driver — with one difference worth knowing: on the in-sandbox path the command inherits the project's resolved secrets, so a private-registry token is available to it. Hlix has no secrets plumbing for it yet. The command has ten minutes. If it fails or times out, the task fails and the coder is never launched. That is the intended shape: a coder turned loose on an environment that could not be prepared produces a confident diff against a tree that never built. A retry is safe — the next attempt recreates the worktree rather than reusing a half-finished install. Leave it empty for a repository that needs no preparation. ## Workspace configuration - **Global Knowledge** browses the `org`-scoped [memory](/concepts/context-memory/) — the standards that apply to everything. - **Custom Skills** manages the [skills](/cli/resources/) available to your agents. - **Integrations** connects [GitHub, Linear, Slack, and Telegram](/integrations/). - **Settings** covers the workspace, people and roles, AI models and harness selection, developer tools (API keys), billing, and regional preferences. **AI models** carries one row per orchestration role, including **Triage** — the model that decides whether an incoming request becomes a cycle and which risk lane it enters. ## If a screen looks wrong - **Overview panels read zero across the board.** The workspace has no completed work yet, or you are in a workspace you just joined. Check the account switcher at the bottom of the sidebar. - **A project you expect is missing from Projects.** Client collaborators only see projects shared with them. As an agency member, check the `Internal / unassigned` group — a project with no customer lands there. - **A project card says `No repository — results stay downloadable`.** That is a repo-less project working as designed, not a broken connection. See [the Coding Workspace](/concepts/coding-workspace/). - **The Canvas shows agents but no activity.** Nothing has been dispatched yet. The right-rail composer is what starts a run. - **A worker card's Thinking tab is empty while it is plainly working.** That task is running on an external coder rather than Hlix, and its conversation happens in its own process. Read its **Activity** tab instead — that is the live signal there. - **The chat timeline is shorter than the run.** It streams from the moment you attached and does not survive a reload. The durable records are Logs, the task's own history, and the Transcript tab. - **Metrics says "No cycles yet" on a busy project.** Metrics measures cycles, not tasks. Work that never became a cycle has no stage history to compute from. - **The Approvals badge shows a count but the list is empty.** The request resolved in another session; reload. - **Audit Log returns nothing for a member account.** It is restricted to `owner` and `admin` — intended behaviour, not a permissions bug. - **A settings page refuses a change you can see.** Some settings are `owner`/`admin` only, notably the workspace-wide harness, which names the coder for every project. ## Next steps --- # Delivery metrics Read a project's real flow — throughput, cycle time, time per stage, aging work, and how much of the automation held without rework. **A project's Metrics tab reads its delivery out of what actually happened**, not out of anyone's estimate. Every number on it is computed from cycle [stage history](/running/cycles/#stage-history) — the attributed, timestamped record of each status move — so a metric can always be traced back to specific transitions. ## Prerequisites - Agency role with `manage` access on the project. Metrics is agency-only: stage timing and automation quality are operational data, not a client-facing deliverable view. - At least one cycle that has moved through a stage. A project whose first cycle has not transitioned yet shows an empty state, not zeros. ## Reading it In the dashboard, open a project and go to **Metrics** under **Delivery** in the project settings navigation. Over the API: ```bash curl -sS "$HLIX_BASE_URL/v1/api/projects/$PROJECT_ID/cycle-metrics" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` ## What each number means ### Throughput Counts of cycles by where they ended up: `completed` (a pull request was created), `failed`, and `canceled`. It is a **count, not a rate** — there is no per-week denominator, because there is no window. The dashboard shows completed as the headline figure with failed and canceled beneath it. ### Cycle time `p50` and `p90` measured from a cycle's creation to its last recorded transition, over cycles that reached a terminal status — `pr_created`, `failed`, or `canceled`. `sampleSize` is how many cycles that actually covers. A terminal cycle with no stage history at all is skipped rather than guessed at, so `sampleSize` can be lower than the terminal count. Both percentiles are `null` when the sample is empty, and the dashboard shows `—` rather than a zero that would read as "instant". ### Time per stage The median time a cycle spends in each stage, over **completed** stints only. A stage segment closes when a transition leaves it; the stage a cycle is sitting in right now is not counted, because it has not finished yet. A stage nobody has exited is **absent from the list**, not reported as zero. The chart orders stages by the lifecycle — Draft, Triage, Interviewing, Planning, Executing, End-to-end tests, Pull request created, Failed, Canceled — rather than by how often they were observed. ### Work in progress and aging work **WIP** counts cycles currently in a non-terminal stage: `draft`, `triage`, `interviewing`, `planning`, `executing`, or `e2e`, broken down by stage. **Aging work** lists the in-flight cycles that have been sitting in their current stage longer than that stage's own historical median for this project. The threshold is not a fixed number of days — it is the project's own typical time for that stage, so a project with slow reviews is not permanently red. A stage with no median yet flags nothing, and the list is sorted worst overrun first. ### Automation quality This is the section worth reading carefully, because it deliberately refuses to flatter the automation. Every stage exit — one transition leaving one stage — is attributed to a `human`, an `agent`, or a `rule`. Agent and rule both count as automated. | Figure | What it is | | --- | --- | | Raw automated share | Automated exits ÷ all exits | | Quality-adjusted share | *Clean* automated exits ÷ all exits | An automated exit is **not clean** when the stage it left is re-entered later in the same cycle. That is rework: the automation moved the cycle on, and something moved it back. Only automated exits are checked for rework; a human decision that gets reversed is not counted anywhere. ## If a panel looks wrong - **"No cycles yet" on a project you know has run work.** Metrics reads cycles, not tasks. A project whose work never became a cycle has nothing to measure. - **Every stage median is missing.** Nothing has *exited* a stage yet. A cycle sitting in `executing` since it was created has produced no closed segment. - **Cycle time is `—` but throughput shows completed cycles.** Those cycles reached a terminal status with no stage-history entries to measure between — the sample is empty, and an invented duration would be worse than a dash. - **Aging work is empty while something is obviously stuck.** That stage has no median yet, so there is no threshold to exceed. It will start flagging once comparable cycles have passed through. - **The Metrics entry is missing from the navigation.** You are signed in as a client collaborator. The route is agency-only and the backend refuses it too, so hiding the link is the honest behaviour rather than showing a wall. ## Where to go next | If you want to… | Read | | --- | --- | | Understand the stage history these numbers are computed from | [Cycles](/running/cycles/#stage-history) | | See why a cycle entered the stage it did | [Triage](/concepts/triage/) | | Find the work that is blocked rather than slow | [Approvals](/running/approvals/) | | Read the immutable record instead of the aggregate | [Review & QA gates](/concepts/review-gates/#what-gets-recorded) | ## Next steps --- # Reviewing output Read the verified evidence behind a task, comment on a line, and send the work back for a revision. Review in hlix works on **verified evidence**: the diff is read back from the repository host or the workspace archive, not summarized by the agent that wrote it. Comments anchor to an exact commit, and a revision request folds them into the task and re-enters the cycle. ## Prerequisites - A task that ran through a cycle and produced a branch - Agency role, or a client collaborator the project was shared with (comments need only view access; sending a revision needs steer) ## The evidence document `GET /v1/api/tasks/:id/review` returns the same observation the QA evaluator reviewed. **With the CLI:** ```bash hlix tasks review "$TASK_ID" ``` **With the TypeScript SDK** — see [TypeScript SDK](/sdk/typescript/) for building the `hlix` client: ```ts const evidence = await hlix.tasks.review(taskId) ``` **Over the API:** ```bash curl -sS "$HLIX_BASE_URL/v1/api/tasks/$TASK_ID/review" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` The fields that matter when you are deciding whether to accept the work: | Field | Meaning | | --- | --- | | `source` | `github-compare` when read from the connected provider, `workspace-artifact` when the project has no remote | | `observedAt` | When the evidence was collected — evidence is a snapshot, not a live view | | `baseRef` / `baseSha` | The exact commit the task branch was cut from — a **commit sha, never a branch name** (see below) | | `headRef` / `headSha` | The task branch and the reviewed commit. `headRef` is derived as `hlix/ms-/task-`. Every comment anchors to `headSha` | | `comparison` | `{status, aheadBy, behindBy}` — `ahead`, `behind`, `identical`, `diverged`, or `unknown` | | `filesChanged`, `additions`, `deletions`, `commits` | Change size | | `files[]` | Per file: `path`, `previousPath`, `status`, `additions`, `deletions`, `patch`, `patchTruncated` | | `filesTruncated` | `true` when the file list itself was cut short | | `evaluator` | `{verdict, summary, headSha, observedAt, stale}` — `stale: true` means the evaluator judged an older commit | | `artifact` | For repo-less projects: `{kind:"git-archive", reviewUrl, sizeBytes, capturedAt}` | ## Commenting on a line `GET` and `POST /v1/api/tasks/:id/review/comments` hold the line-level review conversation. Every comment is anchored to `headSha`, which is what stops a comment from silently drifting onto code it was never about. ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/tasks/$TASK_ID/review/comments" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{ "headSha": "9f2c1b0ae4d5768a3c2f1e0d9b8a7c6d5e4f3a2b", "path": "src/routes/healthz.ts", "side": "head", "lineStart": 12, "lineEnd": 14, "body": "Read the SHA from the build env, not from git at request time." }' ``` The anchoring rules are enforced, not advisory: - `headSha` is required and must be a 40-character hex commit sha. - `body` is required, 1–10 000 characters. - `side` requires `path`. `lineStart` requires both `path` and `side`. `lineEnd` requires `lineStart` and must be greater than or equal to it. - Omit `path` entirely for a comment about the change as a whole. A comment on a stale commit is refused with the commit you reviewed and the one that exists now: ```json {"error":"The branch moved on: you reviewed 9f2c1b0…, the current head is 3d7e5a1…. Refresh the diff and comment again.","headSha":"3d7e5a1…"} ``` Listing comments returns the anchor alongside them: ```json { "taskId": "b4a1f0d2-…", "headSha": "9f2c1b0ae4d5768a3c2f1e0d9b8a7c6d5e4f3a2b", "comments": [{ "id": "…", "path": "src/routes/healthz.ts", "side": "head", "lineStart": 12, "lineEnd": 14, "body": "…", "resolution": "open", "revisionIteration": 0, "stale": false }], "visualContext": { "enabled": false, "reasonCode": "no-live-preview", "reason": "…" } } ``` `stale: true` on a comment means the branch has moved past the commit that comment was written against. When the head cannot be observed at all, `headSha` is `null` and every comment reads `stale: true` — that is a degraded read, not an error. Resolve or reopen a comment with `PATCH /v1/api/tasks/:id/review/comments/:commentId` and a body of `{"resolution":"resolved"}` or `{"resolution":"open"}`. The call is idempotent. ## Requesting a revision `POST /v1/api/tasks/:id/review/request-revision` folds every **unresolved** comment on `headSha` into the task's review feedback and re-enters the cycle, so the same worker gets your notes as instructions. ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/tasks/$TASK_ID/review/request-revision" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"headSha":"9f2c1b0ae4d5768a3c2f1e0d9b8a7c6d5e4f3a2b"}' ``` Expected result: ```json { "taskId": "b4a1f0d2-…", "cycleId": "0a9b8c7d-…", "headSha": "9f2c1b0ae4d5768a3c2f1e0d9b8a7c6d5e4f3a2b", "commentIds": ["…"], "staleSkipped": 0, "feedback": "…the composed feedback the worker receives…", "iteration": 1, "started": true, "reason": null } ``` `staleSkipped` counts comments that were anchored to an older commit and therefore left out. `started: true` is the signal that the cycle was actually re-entered. If queuing fails, nothing is recorded — the refusal says so explicitly rather than leaving a half-applied revision: ```json {"error":"The revision could not be queued (…), so it was not recorded. Nothing changed — try again.","started":false} ``` ## Contract coverage ## In the dashboard The cycle queue's review panel renders the same evidence: the file list, the patch per file, and a comment thread anchored to the reviewed commit, with **Request revision** as the action that sends unresolved comments back. It is the fastest path when you are reading a diff rather than automating one. ## If review fails - `{"error":"No review evidence: this task has no branch (it was never dispatched through a cycle)"}` — the task never ran. See [Tasks](/running/tasks/). - `{"error":"No review evidence: this task has no recorded branch point…"}` — the task ran before a base was recorded; there is nothing to diff against. - `{"error":"Could not fetch review evidence — …"}` with status `502` — the repository host could not be reached. Retry; nothing is wrong with the task. - `{"error":"Task repo not found"}` — the project's repository binding is gone. - `{"error":"The branch moved on: you reviewed …, the current head is ….","headSha":"…"}` — refresh the evidence and comment against the new head. - `{"error":"This task has no reviewed commit …"}` with status `409` — the evidence came from a workspace archive with no commit to anchor to. - `{"error":"This task has no cycle to retry through."}` — a standalone task cannot be revised; only cycle-dispatched work can re-enter. - `{"error":"This task's cycle is already running. …"}` — wait for the current run before requesting a revision. - `{"error":"Cannot anchor a comment: …"}` — the evidence itself could not be read, so no anchor exists yet. - `{"error":"Read-only access to this project"}` — you are a client collaborator without steer rights. You may comment; you may not request a revision. ## Next steps --- # Tasks Create a task, understand why execution comes from a cycle, and stream its status until it finishes. A **task** is the atomic unit of execution: one worker agent, one git worktree, one branch, one outcome. Tasks are what fan out in parallel inside a project's [Coding Workspace](/concepts/coding-workspace/). ## Prerequisites - An imported or created project — see the [Quickstart](/getting-started/quickstart/) - An API key and workspace ID, or a signed-in dashboard session - Agency role (`owner`, `admin`, or `member`) to create a task; client collaborators can read and comment but not create ## Where tasks come from Most tasks are not written by hand. A project run (`POST /v1/api/projects/:id/start`) plans a roadmap, the roadmap becomes cycles, and each cycle's planner decomposes it into tasks. Creating a task directly is the exception — useful when you already know the unit of work and want it attached to a specific cycle. | Origin | How | Executes? | | --- | --- | --- | | Project run | `POST /v1/api/projects/:id/start` with a brief | Yes — the orchestrator dispatches it | | Cycle planner | Automatic, during a cycle run | Yes | | `POST /v1/api/cycles/:id/tasks` | You add it to an existing cycle | Yes, on the next cycle run | | `POST /v1/api/tasks` | You create it standalone | **No** — see below | ## Creating a task **Over the API:** ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/tasks" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"projectId":"'"$PROJECT_ID"'","title":"Add GET /healthz","description":"Return 200 with the build SHA and cover it with a test."}' ``` `projectId` (uuid) and `description` (1–5000 characters) are required; `title` (1–200 characters) is optional. A `201` returns the created task row. **With the TypeScript SDK** — see [TypeScript SDK](/sdk/typescript/) for building the `hlix` client: ```ts const task = await hlix.tasks.create({ projectId, title: 'Add GET /healthz', description: 'Return 200 with the build SHA and cover it with a test.', }) ``` **In the dashboard:** go to **Tasks → New task**, pick the project, write the description, and submit. You land on the task detail page. To attach the task to a specific cycle instead — which is what makes it runnable — post it to the cycle: ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/cycles/$CYCLE_ID/tasks" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"title":"Add GET /healthz","description":"…","dependsOn":[]}' ``` `dependsOn` is an array of task UUIDs that must finish first. ## There is no standalone task run Execution always enters through a cycle. Either run the cycle directly ([Cycles](/running/cycles/)) or start a project run and let the orchestrator dispatch it. ## Task statuses Thirteen values, from the `task_status` database enum. A new task starts at `queued`. | Status | Meaning | | --- | --- | | `queued` | Created, waiting for a wave to pick it up | | `blocked` | Held by an unmet dependency or an unresolved request | | `building` | A worker agent is editing the worktree | | `testing` | Automated checks are running | | `reviewing` | The QA evaluator is judging the result | | `approved` | The evaluator approved the change | | `merging` | The task branch is being merged into the project's default branch | | `resolving` | A merge conflict is being worked | | `merged` | The branch landed | | `pr_created` | A pull request was opened on the connected repository | | `ci_passing` | Provider CI reported success | | `done` | Terminal — the task finished | | `failed` | Terminal — the task failed | `PATCH /v1/api/tasks/:id` accepts `title`, `description`, and `status` — but **not** `resolving`, which only the merge path sets. ## Watching a task `GET /v1/api/tasks/:id/stream` is a Server-Sent Events stream of status changes. **With the CLI:** ```bash hlix tasks watch "$TASK_ID" ``` Expected result — one line per status change, exiting at a terminal status: ```text queued building testing reviewing done ``` `--json` prints one envelope per line (JSONL) so a consumer can read frames as they arrive. **Over the API:** ```bash curl -N "$HLIX_BASE_URL/v1/api/tasks/$TASK_ID/stream" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" ``` Four properties of this stream are worth knowing before you build on it: - **Frames are unnamed events.** Each `data:` payload is the full task row, emitted on the first frame and on every status change. - **There is no `done` event.** The stream simply closes once the status reaches `done`, `failed`, `pr_created`, or `ci_passing`. Read the status field; do not wait for a terminal event name. (`merged` is deliberately not a closing status — a merged task usually continues to `done`.) - **`keepalive` is a named event with an empty payload**, roughly every 30 seconds while nothing changes. - **A missing task is a frame, not a 404.** The stream opens `200` and sends `{"error":"Task not found"}` before closing. The status code cannot tell you the task is gone. The stream is bounded: it polls every 2 seconds for at most 600 iterations, so it ends after roughly 20 minutes whether or not the task finished. Long tasks need a reconnect. ## Listing tasks ```bash hlix tasks list ``` Inside a folder bound to a project, this lists that project's tasks; anywhere else it lists the whole workspace. Expected result: ```text ID STATUS TITLE ------------------------------------ -------- ------------------ b4a1f0d2-3c77-4e1a-9a2b-1d5e6f7c8a90 building Add GET /healthz ``` - **`--project `** (`string`, default the bound project): restrict to one project. - **`--status `** (`string`): restrict to one of the statuses above. ```bash hlix tasks list --status failed --json ``` With no matches it prints `No tasks.` and exits `0` — an empty result is not a failure. ## Reading one task ```bash hlix tasks get "$TASK_ID" ``` Expected result: ```text id b4a1f0d2-3c77-4e1a-9a2b-1d5e6f7c8a90 status done title Add GET /healthz project 6f1c2a9e-8f0b-4a7d-9d33-2f0b1c7e5a41 description Return 200 with the build SHA and cover it with a test. ``` `hlix tasks get --json` passes the API response through unchanged — the human table picks columns, the JSON envelope never drops a field. ## Task conversation `GET` and `POST /v1/api/tasks/:id/comments` are the task's **conversation** — messages between you and the agents working it. They are not the same surface as [line-level review comments](/running/review/), which are anchored to a commit and a file. ```bash curl -sS -X POST "$HLIX_BASE_URL/v1/api/tasks/$TASK_ID/comments" \ -H "x-api-key: $HLIX_API_KEY" \ -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"body":"Use the existing logger rather than console."}' ``` `body` is required (1–10 000 characters). An optional `messageId` (uuid) makes the post idempotent: replaying the same id with the same body returns the stored message instead of duplicating it. A view-only client collaborator may comment. ## Common questions **Why did my standalone task never start?** Because a task created on its own is a record, not a run. Execution comes from a cycle — the orchestrator dispatches a wave, and tasks in that wave run. A task created through `POST /v1/api/tasks` waits until something dispatches it. **Can I cancel a running task?** Not from the CLI, which has no execution commands at all. `PATCH /v1/api/tasks/:id` accepts a status change, with one exception: it will not set `resolving`, which only the merge path may write. **Why did `hlix tasks watch` exit while the task was still going?** The stream is bounded — it polls every 2 seconds for at most 600 iterations, so it ends after roughly 20 minutes regardless. A clean exit at a non-terminal status means "reconnect", not "finished". Read the status field, not the exit code. **Where are the logs?** There is no log stream. The log endpoint is not part of the published contract, so no client can reach it, and `tasks watch` streams task *status* — a different thing, named accordingly. **Two tasks touched the same file. What happens?** Each task works in its own git worktree, so they never share a checkout. The collision surfaces at merge time as a genuine conflict and fails that task only; its siblings are unaffected. ## If a task will not run - `{"error":"Standalone task execution is not implemented. Run the task's cycle instead (POST /v1/api/cycles/:id/execute)."}` — expected, and a `501`. Execute the task's cycle instead. - `{"error":"Task not found"}` with a `404` from `/execute` — the task resolved to nothing in this workspace. Check the id and `X-Organization-Id` before assuming the endpoint is at fault. - Task stays `queued` forever — it is not attached to a cycle, or its cycle has not been executed. Check `cycleId` on the task row. - Task stays `blocked` — an unmet `dependsOn`, or a pending request is holding it. See [Approvals](/running/approvals/). - `{"error":"Task not found"}` as a stream frame — the id is wrong, or it belongs to another workspace. Check `X-Organization-Id`. - `{"error":"Project administration is restricted to the agency"}` — you are authenticated as a client collaborator. Clients read and comment; they do not create tasks. - The watch command exits after ~20 minutes with the task still running — the stream's own bound, not a failure. Reconnect. ## Next steps --- # Go SDK preview What the published Go module will look like, and how to generate a client from the OpenAPI contract until it ships. **This page previews the Go module hlix will publish.** It is generated from the public OpenAPI contract, and the shapes below are the ones the published module will expose. ## Prerequisites - Go 1.23 or later - An hlix API key and workspace ID ## Generate a client meanwhile The [OpenAPI contract](/api/openapi/) is public and unauthenticated, so a working Go client is one command away: ```bash curl --fail --silent --show-error \ https://server.hlix.ai/v1/api/openapi.json \ --output hlix-openapi.json npx @openapitools/openapi-generator-cli generate \ -i hlix-openapi.json \ -g go \ -o ./hlix-client ``` Expected result: a module whose operations match the [API reference](/api/reference/) one for one. hlix pins OpenAPI Generator `7.22.0` for its own previews; pin yours so regeneration is reproducible. Identifiers in your generated client will differ from the sample below — that is the cost of generating rather than waiting. The requests and responses are identical, because both come from the one contract. ## List projects Create `main.go`: ```go package main import ( "context" "fmt" "log" "os" // The module path your generated client uses — hlix has not published one. hlix "example.com/hlix-client" ) func main() { workspaceID := os.Getenv("HLIX_WORKSPACE_ID") apiKey := os.Getenv("HLIX_API_KEY") if workspaceID == "" || apiKey == "" { log.Fatal("HLIX_WORKSPACE_ID and HLIX_API_KEY are required") } ctx := context.WithValue( context.Background(), hlix.ContextAPIKeys, map[string]hlix.APIKey{"apiKey": {Key: apiKey}}, ) configuration := hlix.NewConfiguration() client := hlix.NewAPIClient(configuration) projects, _, err := client.ProjectsAPI. ListProjects(ctx). XOrganizationId(workspaceID). Execute() if err != nil { log.Fatal(err) } fmt.Printf("%v\n", projects) } ``` Run it: ```bash HLIX_API_KEY='…' \ HLIX_WORKSPACE_ID='' \ go run . ``` Expected result: the selected workspace's project collection is printed. ## Use another API origin The generated configuration defaults to production. For a self-hosted or staging deployment, replace its server list before creating the client. Keep production credentials out of non-production hosts. ## Preview limitations - Method names, generic response types, and module packaging may change before a stable tag. - The preview does not add the TypeScript SDK's opinionated errors, bounded retry, local import scanner, or sync workflow. - Streaming and bundle upload protocols require application-specific lifecycle handling. - The generated auth context key must be `apiKey`, and tenant selection remains an explicit `XOrganizationId` call option. ## If the preview client fails - `no required module provides package …`: you are fetching a module path hlix has not published. Generate a client from the contract instead, and check [Current versions](/releases/availability/). - `missing go.sum entry`: run `go mod tidy` with network access for the generated client's dependencies. - `401`: confirm the API key is present in `ContextAPIKeys` under `apiKey`. - `403`: confirm the user can access the explicit workspace ID. - `404`: the resource is absent or not visible to this caller. - Regeneration drift: pin your generator version, and re-download the contract — its `info.version` tells you whether the contract itself moved. ## Next steps --- # Python SDK preview What the published Python client will look like, and how to generate one from the OpenAPI contract until it ships. **This page previews the Python client hlix will publish.** It is generated from the same OpenAPI 3.1 contract as the TypeScript SDK, and the shapes below are the ones the published package will expose. ## Prerequisites - Python 3.10 or later - An hlix API key and workspace ID ## Generate a client meanwhile The [OpenAPI contract](/api/openapi/) is public and needs no authentication, so you can generate a working Python client now: ```bash curl --fail --silent --show-error \ https://server.hlix.ai/v1/api/openapi.json \ --output hlix-openapi.json npx @openapitools/openapi-generator-cli generate \ -i hlix-openapi.json \ -g python \ -o ./hlix-client ``` Expected result: a package whose operations match the [API reference](/api/reference/) one for one. hlix pins OpenAPI Generator `7.22.0` for its own previews; pinning yours keeps regeneration reproducible. The generated client's names differ from the hand-written shapes below — that is the cost of generating rather than waiting. The requests and responses are identical, because both come from the one contract. ## List projects Create `list_projects.py`: ```python import asyncio import os import hlix async def main() -> None: configuration = hlix.Configuration( host=os.environ.get("HLIX_BASE_URL", "https://server.hlix.ai") ) configuration.api_key["apiKey"] = os.environ["HLIX_API_KEY"] async with hlix.ApiClient(configuration) as api_client: projects = await hlix.ProjectsApi(api_client).list_projects( x_organization_id=os.environ["HLIX_WORKSPACE_ID"] ) print(projects) asyncio.run(main()) ``` Run it with secret environment variables: ```bash HLIX_API_KEY='…' \ HLIX_WORKSPACE_ID='' \ python list_projects.py ``` Expected result: the selected workspace's project collection is printed. ## Authentication and tenancy The generator names the OpenAPI API-key scheme `apiKey`, which maps to the `x-api-key` header. Pass `x_organization_id` on every call in programmatic use; an API key identifies a user but does not safely imply which workspace automation intended. ## Preview limitations - Generated names and ergonomics may change before the first stable package release. - The client does not provide the TypeScript SDK's opinionated error hierarchy, retry policy, CLI snapshot scanner, or high-level local sync workflow. - Streaming and multi-step bundle protocols need application-level handling and verification. - Some response bodies remain generic when the published contract does not pin a more specific schema. ## If the preview client fails - `ModuleNotFoundError`: activate the virtual environment, and import the module name your generator produced — a generated client does not necessarily import as `hlix`. - `KeyError: HLIX_API_KEY`: provide the secret through your environment or secret manager. - `401`: the API key is invalid or missing. - `403`: the user cannot act in `HLIX_WORKSPACE_ID`. - `404`: the resource is absent or not visible; do not infer cross-tenant existence. - Dependency resolution error: use Python 3.10+ and install into a fresh virtual environment. - Generated source differs after regeneration: pin your generator version, and re-download the contract — its `info.version` tells you whether the contract itself moved. ## Next steps --- # TypeScript SDK Use the typed hlix client for workspace-scoped automation, imports, revisions, tasks, and streams. `@hlix/sdk` is the first-party TypeScript client. Its request and response types come from the published OpenAPI contract, while the SDK adds credentials, explicit workspace selection, typed errors, bounded retry, bundle transfer, and Server-Sent Events. ## Prerequisites - Node.js 20 or later - An hlix API key - A workspace ID - TypeScript is recommended, but the package also exposes ESM JavaScript ## Install ```bash npm install @hlix/sdk ``` Pin the version in CI so a build never picks up a client you have not tested against: ```bash npm install @hlix/sdk@0.2.0 ``` [Current versions](/releases/availability/) reports the same state for all three hlix packages, and names the gate each one still has to pass. Until the package ships, **generate a typed client from the [public OpenAPI contract](/api/openapi/)** — it is the same document this SDK is built from, and it downloads without authentication. The shapes below describe the client hlix will publish; the generated client exposes the same operations with generated names. ## Create a client ```ts import { createHlix, apiKeyCredential } from '@hlix/sdk' const hlix = createHlix({ baseUrl: 'https://server.hlix.ai', credential: apiKeyCredential(process.env.HLIX_API_KEY!), organizationId: process.env.HLIX_WORKSPACE_ID!, }) ``` An API key authenticates a user; it does not imply a workspace. Always pass `organizationId` in server-side automation so a prior browser session cannot select the tenant implicitly. ## Runnable happy path List projects and inspect a task: ```ts import { createHlix, apiKeyCredential, HlixNotFoundError, } from '@hlix/sdk' const hlix = createHlix({ baseUrl: process.env.HLIX_BASE_URL ?? 'https://server.hlix.ai', credential: apiKeyCredential(process.env.HLIX_API_KEY!), organizationId: process.env.HLIX_WORKSPACE_ID!, }) const projects = await hlix.projects.list() console.log(projects) try { const task = await hlix.tasks.get(process.env.HLIX_TASK_ID!) console.log(task) } catch (error) { if (error instanceof HlixNotFoundError) { console.error('Task is absent or not visible to this caller') } else { throw error } } ``` Expected result: the project list is scoped to the selected workspace. A hidden resource and an absent resource both produce `HlixNotFoundError`; the API does not reveal cross-tenant existence. ## Stream task status ```ts const controller = new AbortController() for await (const event of hlix.tasks.stream(taskId, { signal: controller.signal, })) { console.log(event.event, JSON.parse(event.data)) } ``` Streaming does not retry automatically. A reconnected stream could replay events the consumer has already acted on, so reconnection belongs in application logic with its own checkpoint policy. ## Error classes | Class | Meaning | | --- | --- | | `HlixValidationError` | 400 schema failure; exposes decoded `issues` | | `HlixBadRequestError` | other 400 response | | `HlixAuthenticationError` | 401 | | `HlixPermissionError` | 403 | | `HlixNotFoundError` | 404, including not visible | | `HlixConflictError` | 409 compare-and-swap or state conflict | | `HlixRateLimitError` | 429; exposes `retryAfter` | | `HlixNotImplementedError` | 501 permanent refusal | | `HlixServerError` | other 5xx | | `HlixTransportError` | no HTTP response | Every HTTP error also carries the status, parsed body, and request ID when the server provides one. ## Retry policy The default uses bounded exponential backoff with jitter and honors `Retry-After`. Only idempotent verbs are eligible. `POST` and `PATCH` are never retried automatically because the client may not know whether the server already performed the mutation. Disable retry: ```ts const hlix = createHlix({ baseUrl, credential, organizationId, retry: { attempts: 1 }, }) ``` ## Imports and revisions The SDK exposes the same low-level import and revision primitives used by the CLI: - `hlix.imports.create`, `get`, `uploadBundle`, and `finalize` - `hlix.revisions.head`, `createUpload`, `uploadBundle`, `finalize`, `downloadBundle`, and `protectedFiles` These are multi-step protocols. Preserve the server-issued upload identity, declare exact hashes and sizes, and finalize only with protected data that exactly matches the manifest. For ordinary local-to-cloud use, prefer the CLI—it owns scanning, snapshot construction, integrity verification, and local revision state. ## Raw transport `hlix.raw` exposes the generated `openapi-fetch` client for published operations not wrapped by the opinionated facade: ```ts const { data, error } = await hlix.raw.GET('/v1/api/projects') ``` The raw client returns `{ data, error }`; it does not apply the SDK's throwing error model. ## Verify your integration ```bash HLIX_API_KEY='…' \ HLIX_WORKSPACE_ID='' \ node dist/example.js ``` Use a secret manager in CI and avoid printing environment variables. Confirm the result against `hlix projects list --json` or the web app in the same workspace. ## If the client fails - `Cannot find module '@hlix/sdk'` — the workspace package is not built. Run `bun install --frozen-lockfile` then `bun run --cwd packages/sdk build`. There is no registry install yet; see [Current versions](/releases/availability/). - `HlixAuthenticationError` — the key is missing or rejected. It authenticates a *user*, so a valid key with no `organizationId` can still fail the next check. - `HlixPermissionError` — the user cannot act in the selected workspace. Confirm `organizationId` is the one you meant. - `HlixNotFoundError` on a resource you can see in the dashboard — you are pointed at a different workspace. Absent and not-visible are deliberately the same answer; do not infer cross-tenant existence from it. - `HlixConflictError` on a revision call — the expected revision or generation is stale. Read the head again before retrying. - `HlixTransportError` — no HTTP response at all. Check `baseUrl`, DNS, and TLS before suspecting credentials. - **A `POST` that seems to have run twice** — it did not retry. `POST` and `PATCH` are never retried automatically, precisely because the client cannot know whether the server already applied the mutation. - **A stream that stops silently** — streaming does not retry. Reconnection belongs in your code, with its own checkpoint policy, because a replayed event can be acted on twice. ## Next steps --- # Secrets & protected files How hlix discovers, encrypts, restores, and keeps project-local credentials out of Git snapshots and logs. hlix does not solve local-to-cloud by dropping secret files. It moves them through a separate protected-data path so the cloud environment can behave like the local project without committing credentials into its Git history. ## What the scanner collects Recognized protected files include: - `.env` and environment-specific dotenv files, except examples/templates - `.envrc`, with literal assignments collected without executing shell code - credential and service-account JSON - `.npmrc` and `.pypirc` - private-key and certificate-key formats such as `.pem`, `.key`, `.p12`, and `.pfx` - `terraform.tfvars` and `terraform.tfvars.json` - regular text files containing a high-confidence private-key or provider-token pattern - quarantined agent, skill, and MCP configuration The dry-run report lists paths and key names, never values: ```bash hlix import . --dry-run --json ``` ## Dotenv selection When no override is provided, hlix composes development dotenv files in this order: 1. `.env` 2. `.env.development` 3. `.env.local` 4. `.env.development.local` Later assignments win. Use an explicit file when the inferred development environment is not the one intended for cloud: ```bash hlix import . --dry-run --env-file .env.cloud-development hlix import . --env-file .env.cloud-development ``` ### Direnv and dynamic secret managers Literal `.envrc` assignments join the selected development secret set. Dynamic shell expressions are not executed during scanning. If your active shell already contains the resolved project key—for example after direnv called 1Password, Infisical, or Doppler—Hlix collects that resolved value. Otherwise import blocks so you can activate the environment before retrying. ## Data path The seven steps cross three trust zones: your machine records only metadata and sends protected files separately, the backend verifies every hash and encrypts at rest, and the Coding Workspace revalidates the bytes, writes them `0600`, and strips them from every commit. 1. The local scanner reads protected bytes into memory and records only path, kind, size, hash, and key names in the manifest. 2. Protected files are omitted from the Git snapshot and sent separately over the authenticated TLS API request. 3. The backend checks that every supplied path, kind, size, and SHA-256 exactly matches the manifest. 4. Values and file contents are encrypted at rest with AES-GCM under the deployment's secrets-encryption key. 5. For an authorized task, the backend decrypts the protected project files and sends them in the task payload through the sandbox filesystem—not command arguments. 6. The sandbox validates the bytes again, refuses traversal and symlink targets, writes files with mode `0600`, and pins their exact paths in Git exclude rules. 7. Commit staging explicitly removes every protected path as a final guard. The same integrity checks apply in reverse during `hlix pull`. ## Who can access values Encryption at rest is not end-to-end encryption. The hlix backend must be able to decrypt values for authorized project work and authenticated pull. A project task, its selected coding agent, and repository setup commands can read materialized values inside that project's Coding Workspace. Use least-privileged, environment-specific credentials. Do not import a personal all-access token when a project-scoped development token will work. ## Failure behavior Secret resolution is fail-closed. If protected data cannot be decrypted or a required secret cannot be resolved, task dispatch stops instead of starting with an empty or partial environment. Known secret values and protected-file contents are included in execution-log redaction. Redaction covers exact values, lines, and dotenv-style assignment values. It is defense in depth—not a reason to print credentials. Encoded, transformed, split, or novel values may not match exact-value redaction. ## Git history is separate Excluding a current `.env` file does not erase an older committed value. Import scans up to 10,000 commits for sensitive paths and high-confidence patterns and blocks by default when it finds them. If a credential entered Git history: 1. Rotate or revoke it first. 2. Decide whether history must be rewritten. 3. Verify the rewritten repository independently. 4. Re-run `hlix import . --dry-run`. `--history preserve` retains the history despite findings. It is an informed retention decision, not remediation. ## Keep secrets synchronized Protected files participate in the project manifest. Edit them locally, then create a new revision: ```bash hlix push ``` Pulling a cloud revision restores protected bytes with owner-only permissions: ```bash hlix pull ``` If both local and cloud protected state changed, `hlix sync` refuses to guess. Review the conflict and select a direction explicitly. ## If a secret does not reach the workspace - **A task fails immediately with an empty credential.** The key name was imported but its value was not. Add the value at its declared source and push a new revision; MCP and environment configuration carry key *names* only. - **`hlix import` blocks on a missing key.** A key the project *declares* is fatal; one it only *references* in source is a warning. Provide the declared value, or remove the declaration. - **The wrong dotenv profile was activated.** Re-import or push with `--env-file `; every other profile was preserved encrypted, so nothing was lost. - **A `.envrc` key is empty.** Dynamic assignments are never executed. Let direnv resolve it into your shell, then scan again. - **`hlix sync` refuses after protected state changed on both sides.** It does not guess. Review the conflict and choose a direction with `push` or `pull` explicitly. - **A value you rotated at the provider still works in a task.** The project holds its own encrypted copy. Rotating at the provider is not the same as updating the project; push a new revision. - **A secret appears in a dry-run report.** It does not — only key names, hashes, sizes, and paths do. If you believe you are seeing a value, treat it as a defect and report it rather than sharing the report. ## Operational checklist - Use development credentials with narrow project scope. - Rotate anything that was exposed in source control, logs, chat, or issue trackers. - Keep the deployment encryption key configured, access-controlled, and backed up according to your recovery policy. - Treat access to project manage/sync APIs as access to protected project material. - Review MCP environment key names before activation. - Delete stale credentials at the provider, not only from a dotenv file. ## Next steps --- # Import trust model Understand what hlix scans, uploads, executes, quarantines, and refuses during local-to-cloud migration. Seamless setup needs repository discovery; safe setup needs explicit boundaries. hlix treats repository content as untrusted input until a specific product action authorizes its use. ## Trust boundaries at a glance | Phase | Reads repository files | Executes repository commands | Makes data active | | --- | --- | --- | --- | | `hlix init` | yes | no | local binding only | | `hlix import --dry-run` | yes | no | nothing | | `hlix import` | yes | no, locally | cloud project, revision, encrypted protected data, declarative environment plan | | Cloud task setup | cloud snapshot and protected files | yes, inside the project Coding Workspace | dependency/setup environment for that task | | `skill/agent/mcp import` | selected resource | no local shell execution | the explicitly selected project resource | ## Scanner containment The local scan: - resolves and stays inside the selected real directory; - never follows symlinks; - refuses nested Git repositories; - considers tracked files even if a new ignore rule would hide them; - applies `.gitignore`, `.hlixignore`, and conservative built-in ignores to untracked files; - limits file count, individual size, total size, protected-file size, and protected total; - parses environment sources without running their commands; - hashes every included and protected file; - binds local branch and tag names to their approved object IDs; - rechecks snapshot files after the scan to detect concurrent changes. Paths that traverse outside the project, absolute protected paths, duplicate manifest paths, malformed base64, size mismatches, and hash mismatches are refused at both API and sandbox boundaries. ## Environment configuration hlix prefers explicit environment sources—including Cursor environment JSON and Dev Container configuration—then fills gaps from language and package-manager evidence. The result is a declarative plan containing: - runtime/toolchain versions; - install and build commands; - a start command and terminal processes; - private ports; - explicitly required secret key names and non-blocking source observations; - source evidence, confidence, and warnings. The scanner does not claim that an inferred command is safe or correct. It records evidence. Cloud setup executes only inside the durable project Coding Workspace, never in the backend service and never during the local scan. ## Quarantine and activation Agent instructions, skills, and MCP configuration are high-impact resources. Automatic project import encrypts and preserves their exact bytes but does not activate them. Activation requires an explicit, resource-specific command: - `hlix skill import …` - `hlix agent import …` - `hlix mcp import …` MCP adds two additional gates: remote endpoints require HTTPS, while stdio execution requires `--allow-stdio` and a direct executable without shell interpretation. See [Skills, agents & MCP](/cli/resources/) for the exact review path. ## Workspace and tenant isolation The saved project binding names both API and workspace. The CLI refuses to reuse it under a different selection. The backend scopes import records, revisions, protected files, projects, and access checks to the authenticated workspace. A **workspace** is the tenant/organization. A **Coding Workspace** is the durable per-project execution environment. They are different boundaries: tenant access decides who can act; the Coding Workspace contains where untrusted project execution occurs. ## Revision safety Each accepted import or push creates an immutable revision. The mutable project head advances only through compare-and-swap against the caller's expected revision and generation. This prevents: - a stale local push from replacing newer cloud agent work; - an interrupted retry from creating duplicate revisions; - a failed upload from becoming the project head; - a push from finalizing while tasks are actively mutating the same project. `hlix sync` handles only uncontested one-sided change. Divergence is surfaced to a person instead of being auto-merged. ## Remaining responsibilities hlix cannot determine whether every instruction, dependency, remote server, or credential is appropriate for your organization. You remain responsible for: - authorization to upload source and history; - license and customer-data obligations; - rotating credentials already exposed elsewhere; - reviewing setup commands and dependency lifecycle scripts; - approving agent/skill/MCP behavior; - choosing least-privileged provider credentials; - validating application behavior after environment recreation. ## Verify the boundary Before the first cloud task: ```bash hlix import . --dry-run --json > /tmp/hlix-import-report.json hlix projects list hlix sync ``` Review the report locally; do not attach it blindly to a public issue because it includes project paths and secret **key names**, even though it contains no secret values. Expected result: no scan blocker, the intended project appears in the intended workspace, and `sync` reports no change after import. ## If the boundary check surprises you - **A file you expected in the snapshot is missing.** It was recognised as protected or quarantined, and user ignore rules do not override that. The dry-run report names it and its lane. - **A skill or agent the scan found is not influencing runs.** That is the design: import preserves it inert. Activate it explicitly with `hlix skill import` or `hlix agent import`. - **`scan_blocked` on a repository you consider clean.** The blocker list names each finding. `--history preserve` uploads history *despite* findings; it does not remove them, and `--yes` never bypasses a blocker. - **A setup command in the trust prompt you do not recognise.** Do not approve it. It is shown verbatim because it will run inside the Coding Workspace with access to project secrets. - **A client collaborator can see less than you expected.** That is a second, restrictive layer beneath the workspace boundary, not a bug. ## Next steps