@fourier-labs/harbour 0.1.44 → 0.1.45

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.
@@ -197,8 +197,8 @@ The person you are working with may not be a developer. They say what they want
197
197
  - "does it work?", and before you report anything as working → open the dev link in your own browser when you have one, press the control you built or changed, and read what the app shows. A green \`harbour check\` is not that proof: it answers governed AI and company systems from fixtures, so the refusals that matter (a field the company's AI route does not accept, a consent the operation does not need, a channel that is not approved) appear only when the control is really pressed. Test rendering, navigation, fixtures and approved reads automatically. In development a Send sends a real email or Slack message: reuse explicit authorization for that bounded test, or ask once if none exists; IT access approval alone is not permission to send. Never ask again for the same authorized test. Before a real integration test, run \`harbour integrations status --app-root . --json\`; explain pending IT approval or missing personal consent before pressing Send, and test the ready parts independently. If you have no browser, say that the button itself is untested.
198
198
  - "I need Slack / Gmail / the warehouse / company data" → a fresh \`harbour init\` declares no connection at all, which is why a new app ships with nothing waiting on IT. Never guess a connection, channel, table, view, mailbox or warehouse column — a guessed name is refused before IT's queue ever sees it. Run \`harbour integrations catalog --app-root . --json\` first and use only what it lists. A warehouse resource whose catalog columns are \`["*"]\` is declared and read with \`["*"]\`; do not invent a friendlier schema or ask the person to get IT to confirm one. Declare only connections and operations the app really calls, then submit both development and preview requests yourself in the same turn with \`harbour integrations request <connection> --reason "<what the app does with it>" --app-root . --json\` and the same command with \`--environment preview\`. Never tell the person to ask IT before you have submitted the request. READY means use it now. PENDING means IT has to approve it: say "IT has to approve this; the app works without it until then", and check later with \`harbour integrations status --app-root . --json\`. A refusal with \`RESOURCE_NOT_APPROVED\` means the named resource is not on the connection yet: IT adds it in the Harbour console under Controls & integrations, and then you run the same request again. Never declare a connection the app does not call — every declared one blocks shipping until IT approves it.
199
199
  - "summarise", "draft", "explain", "AI" → one \`harbour.ai.chat\` call (through \`ai()\` in \`src/harbour.client.ts\`) behind a control the person presses; never an OpenAI/Anthropic key, SDK or URL. \`harbour check\` writes its journey. Send \`messages\` and \`maxTokens\` and nothing else: a refusal with \`unsupported_request_capability\` names a field the company's AI route does not accept — remove that field. A refusal with \`AI_NOT_ENABLED\` means IT has to enable an AI provider: say so in one line and keep the app working without it.
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
- - "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.
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. Before sharing or deploying the private preview, show the person the exact proposed app name, description and audience that will be passed to Harbour (say "only you" when the audience is empty) and ask for one confirmation or correction covering all three; do not run \`productionise\` until they confirm. Pass that confirmed setup in the same command: \`harbour productionise --app-root . --name "<app name>" --description "<description>" --emails "<comma-separated audience>" --wait --json\`; omit \`--emails\` for "only you". The command records the confirmed setup before uploading or deploying the app. 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. The command gives them \`result.deployment.protectedUrl\`: a private preview that they, and the people they confirmed, 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
+ - "make it live for everyone", "go to production" → only after they have tried the preview. Before \`harbour promote --operation <ref> --json\`, show the exact app name, description and production audience again and ask for one confirmation or correction covering all three; never promote a profile or audience the person has not just seen and confirmed. Then promote 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
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.
204
204
 
@@ -210,7 +210,7 @@ When a command refuses, the refusal names its own reason and its own fix: change
210
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.
211
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.
212
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.
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
+ - 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. Whenever the app uses a user-identity operation, build its consent flow into the app during the original implementation without asking whether to add it: the operation's control calls \`harbour.integrations.connect\`, follows \`authorizationUrl\` when the result is \`consent_required\`, returns to a clear connected state, and then lets the person continue the operation. Consent starts only from that person's control, never on page load. Missing consent never falls back to another account.
214
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".
215
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.
216
216
 
@@ -45,6 +45,8 @@ const includePaths = [];
45
45
  let optionError;
46
46
  if (maxWaitSeconds !== undefined && !(Number.isInteger(maxWaitSeconds) && maxWaitSeconds > 0))
47
47
  optionError = "--max-wait takes a whole number of seconds.";
48
+ if (command === "productionise" && ((profileName && !profileDescription) || (!profileName && profileDescription) || (audienceEmails && (!profileName || !profileDescription))))
49
+ optionError = "Productionise takes --name and --description together; --emails may be added only with both.";
48
50
  for (let index = 0; index < args.length; index += 1) {
49
51
  if (args[index] === "--include") {
50
52
  const value = args[index + 1];
@@ -61,7 +63,7 @@ const usage = [
61
63
  "Usage:",
62
64
  " harbour connect <work-email-or-start-url>",
63
65
  " harbour login | logout",
64
- " harbour productionise --app-root <path> [--include <relative-path>]... [--no-wait] [--max-wait <seconds>] [--json]",
66
+ " harbour productionise --app-root <path> [--include <relative-path>]... [--name <text> --description <text> [--emails a@co,b@co]] [--no-wait] [--max-wait <seconds>] [--json]",
65
67
  " harbour status --operation <reference> [--wait] [--max-wait <seconds>] [--json]",
66
68
  " harbour retry --operation <reference> [--no-wait] [--max-wait <seconds>] [--json]",
67
69
  " harbour promote --operation <reference> [--app-root <path>] [--no-wait] [--max-wait <seconds>] [--json] (--app-root: also confirm the company's AI setup for production when the app calls governed AI)",
@@ -246,7 +248,10 @@ else {
246
248
  }
247
249
  else if (command === "productionise") {
248
250
  // Kit apps: preview grants are checked (and the app linked) before any operation starts, so productionise never mints a second app for the same root.
249
- const result = await productionise(root, client, progress, tenant, includePaths, { waitForDeployment: !noWait, waitOptions, integrations: { governance: new GovernanceClient(config.apiUrl, token, tenant), bundle: EMBEDDED_KIT_BUNDLE } });
251
+ const confirmedAppSetup = profileName && profileDescription
252
+ ? { displayName: profileName, description: profileDescription, audienceEmails: audienceEmails ? audienceEmails.split(",").map(value => value.trim()).filter(Boolean) : [] }
253
+ : undefined;
254
+ const result = await productionise(root, client, progress, tenant, includePaths, { waitForDeployment: !noWait, waitOptions, integrations: { governance: new GovernanceClient(config.apiUrl, token, tenant), bundle: EMBEDDED_KIT_BUNDLE }, ...(confirmedAppSetup ? { confirmedAppSetup } : {}) });
250
255
  envelope = operationEnvelope(result.result, result.operationRef, CLI_VERSION);
251
256
  }
252
257
  else {
@@ -6,7 +6,7 @@ import { createSourceManifest } from "../../../src/source-intake.js";
6
6
  import { structured } from "./remote-mcp-client.js";
7
7
  import { archiveForManifest, putMultipart } from "./upload.js";
8
8
  import { CliError } from "./output.js";
9
- import { follow, getAppSetup, pickSourceFailure, readLine, fetchStatus, outcomeFor } from "./operations.js";
9
+ import { confirmAudience, confirmProfile, follow, getAppSetup, pickSourceFailure, readLine, fetchStatus, outcomeFor } from "./operations.js";
10
10
  import { CLI_VERSION } from "./version.js";
11
11
  import { isProhibitedSecretPath } from "../../../src/secret-paths.js";
12
12
  import { assertAiReady, assertPreviewIntegrationsReady } from "./integrations.js";
@@ -122,6 +122,12 @@ export async function productionise(rootArg, client, output, tenantId, includePa
122
122
  await recordKitAppId(root, appId, tenantId);
123
123
  pending = { ...pending, appId, sourceDigest: (await sourceDigest(root)).digest };
124
124
  await writePending(root, pending);
125
+ if (options.confirmedAppSetup && !pending.appSetupConfirmed) {
126
+ await confirmProfile(client, operationRef, options.confirmedAppSetup, output);
127
+ await confirmAudience(client, operationRef, options.confirmedAppSetup.audienceEmails, output);
128
+ pending = { ...pending, appSetupConfirmed: true };
129
+ await writePending(root, pending);
130
+ }
125
131
  const manifest = await createSourceManifest({ tenantId, appId, operationId: operationRef, graphDigest: graph.graphDigest, files });
126
132
  const preliminaryArchive = await archiveForManifest(root, manifest);
127
133
  const prepared = structured(await client.call("harbour_prepare_source_upload", { operationId: operationRef, appId, surface: "codex", graph, format: "zip", filename: `${appId}.zip`, compressedBytes: preliminaryArchive.body.byteLength, manifest: { schema: "harbour.source-package-manifest/1.0", files: manifest.files.map(file => ({ path: file.path, size: file.bytes, sha256: file.sha256 })) } }));
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.1.44";
1
+ export const CLI_VERSION = "0.1.45";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fourier-labs/harbour",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "description": "Harbour productionisation helper",
5
5
  "type": "module",
6
6
  "bin": {