@fourier-labs/harbour 0.1.43 → 0.1.44-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -200,6 +200,7 @@ The person you are working with may not be a developer. They say what they want
200
200
  - "ship it", "put it online", "let my team try it" → run \`harbour check --app-root . --json\` first and fix everything it finds, every time, unasked: the same gates run again in the cloud, where each failed attempt costs minutes instead of the seconds it costs here. Then each declared connection needs a preview grant (\`harbour integrations request <connection> --environment preview --reason "…" --app-root . --json\`); when the app calls governed AI, \`productionise\` also asks whether the company's AI setup is ready and refuses with \`AI_NOT_READY\` and the one IT step — say that line and nothing more; the app works without AI until then. \`harbour productionise --app-root . --wait --json\` gives them \`result.deployment.protectedUrl\`: a private preview that they, and the people they name, open after company sign-in. If your tool cuts the command off before it finishes, \`${continueCommand("<ref>")}\` continues the same deployment — never start another one to find out what happened. For kit apps, describe \`TRANSFORMING\` as building and checking the app; it does not mean a transformation AI is running. The CLI saves \`operationRef\` in \`.harbour/local/productionise.json\`; repeating \`productionise\` continues that operation. Keep \`operationRef\`; \`harbour setup --operation <ref> --json\` lists what is still missing (name, audience, secrets) and \`harbour profile\` / \`harbour audience\` / \`harbour secrets set\` fill it in.
201
201
  - "make it live for everyone", "go to production" → only after they have tried the preview: \`harbour promote --operation <ref> --json\` with the operation reference from productionise. Report the production link, or that an operator approval is pending.
202
202
  - "stop it" → \`harbour stop --app-root .\` (local data kept). \`harbour dev --reset --app-root .\` deletes local data — only when they explicitly ask to start over.
203
+ - "every day at 2pm", "run this on a schedule", "send this automatically" → create one \`jobs/<name>.ts\` file with a literal UTC cron \`export const schedule = "…"\` and one default async handler. Start \`harbour dev\`, then run the job immediately with \`harbour jobs run <name> --app-root . --scheduled-at <matching-UTC-time> --json\`; this exercises the scheduled code against the local database, files and fixtures without waiting for the clock or deploying. The laptop intentionally runs no timer daemon because it may sleep or stop; after \`productionise\`, Harbour translates the same declaration into the Kubernetes schedule that runs automatically. Never claim this local choice means Harbour has no supported scheduler, and never replace a job with \`setInterval\`, an effect or a browser timer.
203
204
 
204
205
  When a command refuses, the refusal names its own reason and its own fix: change that one thing, then run it again. A failed app needs its reported fix; a wait timeout means work is still running, so continue the saved operation — and never start a second deploy of an app while one is running, because concurrent deploys of one app cancel each other.
205
206
 
@@ -209,7 +210,7 @@ When a command refuses, the refusal names its own reason and its own fix: change
209
210
  - Authentication is Harbour SSO: no login forms, no roles or ids trusted from the browser; row ownership is decided in SQL through \`current_setting('harbour.user_id', true)\`. Every route needs a signed-in person; no public routes.
210
211
  - Schema changes are SQL files in \`migrations/\` with row-level security and GRANTs to \`harbour_app_gateway\`; \`harbour dev\` and \`harbour check\` apply them.
211
212
  - Know the operation's input bounds before writing a call: \`slack.channel.history\` \`input.limit\` 1..15, \`gmail.thread.list\` \`input.limit\` 1..15, \`warehouse.view.read\` \`input.limit\` 1..1000 (the connector's \`VIEW_READ_MAX_LIMIT\`); anything larger is refused with \`INPUT_INVALID\`, so page instead of asking for more.
212
- - A Slack message or an email (\`slack.message.post\`, \`gmail.message.send\`) is sent only from an explicit Send control (pressed by the person, or by you for an explicitly authorized test), with a fresh UUID \`idempotencyKey\` per press (reused only to retry that press). Never send from an effect, a timer, a queue or during checks. Consent (\`harbour.integrations.connect\`) exists only for user-identity operations (\`slack.channel.history\`, \`gmail.thread.list\`, \`gmail.message.read\`, \`gmail.message.send\`, and \`slack.message.post\` declared \`"identity": "user"\`); app-identity operations (\`slack.message.post\` declared \`"identity": "app"\`, \`warehouse.view.read\`) never call it — a connect for them is refused as unapproved user access and puts nothing in IT's queue. Missing consent never falls back to another account.
213
+ - In browser code, a Slack message or an email (\`slack.message.post\`, \`gmail.message.send\`) is sent only from an explicit Send control (pressed by the person, or by you for an explicitly authorized test), with a fresh UUID \`idempotencyKey\` per press (reused only to retry that press). Never send from an effect, a browser timer or during checks. A declared \`jobs/*.ts\` handler is the only scheduled-send path: it acts as the app, so only app-mode operations such as \`slack.message.post\` may run there; user-mode Slack and Gmail need a person and cannot. A real scheduled send requires the person's explicit request, the exact operation and destination declared in \`.harbour/integrations.json\`, and a grant for that environment; use one deterministic idempotency key for the business period and destination so replay does not silently repost. Local job runs and \`harbour check\` use fixtures and send nothing. Consent (\`harbour.integrations.connect\`) exists only for user-identity operations (\`slack.channel.history\`, \`gmail.thread.list\`, \`gmail.message.read\`, \`gmail.message.send\`, and \`slack.message.post\` declared \`"identity": "user"\`); app-identity operations (\`slack.message.post\` declared \`"identity": "app"\`, \`warehouse.view.read\`) never call it — a connect for them is refused as unapproved user access and puts nothing in IT's queue. Missing consent never falls back to another account.
213
214
  - A Slack message is posted either as the app (\`"identity": "app"\` — the company's one Slack bot, Isomorph AI, under the name IT approved: declare \`"presentation": { "displayName": "<app name>", "iconEmoji": ":sandwich:" }\` on the connection and IT sees "posts as" before approving; leave it out to post as Isomorph AI itself) or as the person (\`"identity": "user"\` — their own Slack account, after their consent; an older consent answers \`USER_RECONNECT_REQUIRED\` "reconnect Slack to allow posting as you", so offer Connect again) — never pretend one is the other. The declaration in \`.harbour/integrations.json\` is the mode the app requests access for; a post names the approved mode it runs under with \`mode: "app"\` or \`mode: "user"\` on the execute call — optional while the app is approved for one mode, required once IT approved both (\`MODE_REQUIRED\`), and a mode IT has not approved is refused with \`MODE_NOT_GRANTED\`, never swapped for the other. An app-mode post always ends with "Posted by <app> on Isomorph". A post is refused with \`RESOURCE_NOT_APPROVED\` until the bot is in the channel: say "IT (or anyone in the channel) has to run \`/invite @Isomorph AI\` in #<channel> first".
214
215
  - No secrets, tokens, \`.env\` values or fetched company content in source. \`.harbour/local/\` is never committed; \`.harbour/integrations.json\` and \`.harbour/kit.lock.json\` are.
215
216
 
@@ -93,7 +93,8 @@ export function managedBlock() {
93
93
  "- Identity, data and files go through `@harbour/app-sdk` only: `harbour.identity.current()`, `harbour.data.from(table)`, `harbour.files.*`. Never open a database, storage bucket or company system from browser code, and never `fetch` a company URL directly.",
94
94
  "- Company systems (Slack, Gmail, warehouse views) are reached only through `harbour.integrations.execute(connection, {operation, resource, input})` with the connection and operation declared in `.harbour/integrations.json`. Operations are a closed set: `slack.channel.history` (user identity), `slack.message.post` (declared per app: `app` posts as the company's Slack bot, Isomorph AI, under the name IT approved; `user` posts as the signed-in person after their consent), `gmail.thread.list`, `gmail.message.read` and `gmail.message.send` (user identity — reads and one plain-text send as the signed-in person, resource `inbox`), `warehouse.view.read` (app identity). Resources are logical names, never IDs, URLs or tokens.",
95
95
  "- `.harbour/integrations.json` starts with `\"connections\": {}` and stays that way until the app really calls a company system. Adding one is two steps: declare the connection — by the identifier `harbour integrations catalog --app-root .` lists, with only the operations the app calls and only channels, views and mailboxes the catalog shows as approved; never a guessed name — then `harbour integrations request <connection> --reason \"<why>\" --app-root .` (and the same command with `--environment preview` before shipping). Every declared connection blocks the deploy until IT grants it, so a connection the app does not call is a deploy that never happens; a connection that is not declared cannot be requested at all (`CONNECTION_NOT_DECLARED`), so it never gets a grant. Before a real integration test, read `harbour integrations status --app-root . --json`: show pending IT approval separately from personal consent, and test ready destinations independently. After a partial send, retry only the failed destination with its original idempotency key; do not regenerate or resend a successful destination. The file is strict JSON and cannot hold comments; the worked Slack and warehouse examples are in README.md and in the comment in `src/App.tsx`.",
96
- "- A Slack message is sent only after the person presses an explicit Send control; pass a fresh UUID `idempotencyKey` per send action and reuse the same key when retrying that action. Never send from an effect, a timer or a background queue, and never send during checks.",
96
+ "- Scheduled work lives only in `jobs/<name>.ts`: export one literal UTC cron as `schedule` and one default async handler. Test it immediately against the running local services with `harbour jobs run <name> --app-root . --scheduled-at <UTC> --json`; the laptop runs no timer daemon, while Harbour turns the same declaration into an automatic Kubernetes schedule after deployment. Never use `setInterval`, an effect or a browser timer as a scheduler.",
97
+ "- In browser code, a Slack message is sent only after the person presses an explicit Send control; pass a fresh UUID `idempotencyKey` per send action and reuse the same key when retrying that action. Never send from an effect or browser timer, and never send during checks. A `jobs/*.ts` handler is the only scheduled-send path and acts as the app: only an app-mode operation may run there, after the person explicitly requested it and IT granted its exact destination for that environment. Use a deterministic idempotency key for the business period and destination; local job runs and checks use fixtures and send nothing.",
97
98
  "- A Slack message is posted either as the app (the company's Slack bot, Isomorph AI, under the IT-approved name: declare `\"presentation\": { \"displayName\": \"<app name>\" }` on the Slack connection, optional `iconEmoji`; every app-mode post ends with \"Posted by <app> on Isomorph\") or as the person (`\"identity\": \"user\"` on `slack.message.post`, their own consent; an older consent answers `USER_RECONNECT_REQUIRED` — offer Connect again) — never pretend one is the other. The declaration is the mode you request access for; when the app is approved for both, every post says which with `mode: \"app\"` or `mode: \"user\"` on the call (`MODE_REQUIRED` otherwise), and a mode IT has not approved is refused with `MODE_NOT_GRANTED`, never swapped. A post needs the bot in the channel: `/invite @Isomorph AI` there first.",
98
99
  "- Consent is only for user-identity operations (`slack.channel.history`, `gmail.*`, and `slack.message.post` declared `\"identity\": \"user\"`): call `harbour.integrations.connect(connection)` and, when it returns `consent_required`, open `authorizationUrl`. App-identity operations (`slack.message.post` declared `\"identity\": \"app\"`, `warehouse.view.read`) never call connect — it is refused as unapproved user access. Missing consent never falls back to another account.",
99
100
  "- A retained check that calls `harbour.integrations.execute` is answered, under `harbour check` and in the deployment pipeline alike, by the gate's fixture: the contract's result shape for a connection, operation and resource the app declared (one canned Slack message, one canned Gmail thread, a warehouse view with no rows), a refusal with the platform's own code for anything undeclared, and nothing is ever sent or read. The passed check says so in the report; real access is exercised only by `harbour check --integrations` (reads) and the preview's own smoke test.",
@@ -249,7 +250,7 @@ const report = await integrations().execute("sales-warehouse", {
249
250
  });
250
251
  \`\`\`
251
252
 
252
- A send (\`slack.message.post\`, \`gmail.message.send\`) runs only from an explicit Send control, pressed by the person or by the AI tool for an explicitly authorized test, with a fresh UUID \`idempotencyKey\` per press — never from an effect, a timer or a check. Consent is only for user-identity operations (\`slack.channel.history\`, \`gmail.*\`, and \`slack.message.post\` declared \`"identity": "user"\`): \`integrations().connect(connection)\`, and open its \`authorizationUrl\` when it answers \`consent_required\`; app-identity operations (\`slack.message.post\` declared \`"identity": "app"\`, \`warehouse.view.read\`) never call connect. Anything the app calls needs its own retained check under \`.harbour/checks/\`, and anything it stops calling loses its check in the same edit. Keep request construction in an app function that both the button and its retained check call, so the check exercises the actual payload. Do not bypass the SDK types with a generic string/unknown wrapper. A check that calls \`integrations().execute\` is answered by the gate's fixture — the contract's result shape for the declared connection, operation and resource, nothing sent or read, and the report says so; \`harbour check --integrations\` is what exercises real access.
253
+ In browser code, a send (\`slack.message.post\`, \`gmail.message.send\`) runs only from an explicit Send control, pressed by the person or by the AI tool for an explicitly authorized test, with a fresh UUID \`idempotencyKey\` per press — never from an effect, a browser timer or a check. Scheduled work lives in \`jobs/<name>.ts\`; its handler acts as the app, so only app-mode sends may run there, with an approved destination and a deterministic idempotency key for the business period and destination. Test the schedule immediately with \`harbour jobs run <name> --app-root . --scheduled-at <UTC> --json\`; local job runs and checks use fixtures and send nothing, and the deployed Kubernetes schedule owns the real clock. Consent is only for user-identity operations (\`slack.channel.history\`, \`gmail.*\`, and \`slack.message.post\` declared \`"identity": "user"\`): \`integrations().connect(connection)\`, and open its \`authorizationUrl\` when it answers \`consent_required\`; app-identity operations (\`slack.message.post\` declared \`"identity": "app"\`, \`warehouse.view.read\`) never call connect. Anything the app calls needs its own retained check under \`.harbour/checks/\`, and anything it stops calling loses its check in the same edit. Keep request construction in an app function that both the button and its retained check call, so the check exercises the actual payload. Do not bypass the SDK types with a generic string/unknown wrapper. A check that calls \`integrations().execute\` is answered by the gate's fixture — the contract's result shape for the declared connection, operation and resource, nothing sent or read, and the report says so; \`harbour check --integrations\` is what exercises real access.
253
254
  `;
254
255
  const VITE_CONFIG = `import { defineConfig } from "vite";
255
256
  import react from "@vitejs/plugin-react";
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.1.43";
1
+ export const CLI_VERSION = "0.1.44-rc.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fourier-labs/harbour",
3
- "version": "0.1.43",
3
+ "version": "0.1.44-rc.1",
4
4
  "description": "Harbour productionisation helper",
5
5
  "type": "module",
6
6
  "bin": {