@isomorph.ai/cli 0.5.1 → 0.6.0

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.
@@ -118,7 +118,7 @@ Rules live beside this file: \`core.md\` before the first edit; \`integrations.m
118
118
  | "run it", "show me" | \`isomorph dev --app-root . --detach --json\`; give them \`result.origin\` (the first start takes a minute or two). Locally they are a fixture user. |
119
119
  | "check it", "is it ready?" | \`isomorph check --app-root . --json\` (the output is the report). Failures in the app's code are yours: fix and re-check until clean, after every change and before every ship, unasked and never offered as a choice. |
120
120
  | "does it work?" (and before reporting anything as working) | open the dev link in your own browser, press the control you built or changed, read what the app shows; a green \`isomorph check\` is not that proof (fixtures answer AI and company systems). A Send in development is real: reuse explicit authorization or ask once. No browser: say the button is untested. |
121
- | "I need Slack / Gmail / the warehouse" | read \`integrations.md\` beside this skill first; then, in the same turn: catalog, declare only what the app calls, submit the request yourself, say READY or PENDING in one line. |
121
+ | "I need Slack / Gmail / the warehouse" | read \`integrations.md\` beside this skill first; then, in the same turn: catalog, call only what it lists (literals), submit the request yourself, say READY or PENDING in one line. |
122
122
  | "summarise", "draft", "AI" | read \`ai.md\` beside this skill first; then one call behind a control they press. |
123
123
  | "ship it", "let my team try it" | show the person the three values in \`.isomorph/app.json\` (name, description, audience — "only you" when empty) and correct the file as they say; then \`isomorph deploy --app-root . --json\` (it runs the checks when needed and refuses once naming every blocker and its fix: relay that line; the app works meanwhile). Give them \`result.deployment.protectedUrl\`. \`--session-notes\`: what cost you time in Isomorph, or \`""\`. |
124
124
  | "make it live" | only after they have tried the preview: \`isomorph promote --operation <ref> --app-root . --confirm-tested --json\`. Report the production link, or that operator approval is pending. |
@@ -69,14 +69,11 @@ export async function runChecks(root, options) {
69
69
  // failure ended the run as `fetch failed` (CLI_FAILED) with no report written.
70
70
  const skipped = (why) => record({ name: "integrations", status: "not_run", detail: why });
71
71
  const lock = await readKitLock(root).catch(() => undefined);
72
- const { declaration, errors } = await readDeclaration(root);
73
72
  if (!lock?.appId)
74
73
  skipped("the app is not linked yet; `isomorph dev` (signed in) or `isomorph integrations request` links it");
75
- else if (errors.length)
76
- skipped(`.isomorph/integrations.json is invalid: ${errors[0]}`);
77
74
  else {
78
75
  try {
79
- integrations = await testIntegrationReads(lock.appId, options.governance, declaration, output);
76
+ integrations = await testIntegrationReads(lock.appId, options.governance, await readDeclaration(root), output);
80
77
  if (!integrations.length)
81
78
  output("No READY development grants with read operations; real integrations were not exercised.");
82
79
  }
@@ -252,8 +249,8 @@ function indentLines(detail) { return detail.split("\n").map((line, index) => in
252
249
  */
253
250
  export async function writeGateFiles(root, files, output) {
254
251
  for (const [path, content] of Object.entries(files)) {
255
- if (!/^\.isomorph\/checks\/[A-Za-z0-9._-]+$/.test(path))
256
- throw new CliError("LOCAL_RUNTIME_FAILED", `The kit gate named a file outside .isomorph/checks/: ${path}.`);
252
+ if (!/^\.isomorph\/(checks\/[A-Za-z0-9._-]+|integrations\.json)$/.test(path))
253
+ throw new CliError("LOCAL_RUNTIME_FAILED", `The kit gate named a file it does not own: ${path}.`);
257
254
  const target = join(root, path);
258
255
  if (content === null) {
259
256
  await rm(target, { force: true });
@@ -63,9 +63,7 @@ async function runDeploy(rootArg, governance, output, tenantId, options) {
63
63
  // that is blank too; this only says so before the checks spend a minute.
64
64
  if (!profile.name.trim() || !profile.description.trim())
65
65
  throw new CliError("PROFILE_INCOMPLETE", "The app needs a name and a description in .isomorph/app.json before it can deploy.", undefined, "Edit `name` and `description` in .isomorph/app.json (show them to the person first), then run this again.");
66
- const { declaration, errors } = await readDeclaration(root);
67
- if (errors.length && !errors[0].startsWith(".isomorph/integrations.json is missing"))
68
- throw new CliError("DECLARATION_INVALID", `.isomorph/integrations.json is invalid: ${errors[0]}`);
66
+ const declaration = await readDeclaration(root);
69
67
  // Link first: the app's identity lands in `.isomorph/kit.lock.json`, which is
70
68
  // one of the deployable files, so a report pinned after the link stays fresh
71
69
  // through the checks and the package is built from the linked tree.
@@ -53,13 +53,12 @@ Shared-read, owner-write: the same, plus \`CREATE POLICY posts_read ON posts FOR
53
53
  - \`APP_NOT_FOUND\`: clear \`appId\` and \`tenantId\` in \`.isomorph/kit.lock.json\` to relink.`,
54
54
  integrations: `# Company systems (Slack, Gmail, warehouse)
55
55
 
56
- ## Declare
56
+ ## Call shape
57
57
 
58
- Company systems are reached through \`isomorph.integrations.execute(connection, { operation, resource, input })\`, declared in \`.isomorph/integrations.json\` by catalog identifier, with only the operations called. Closed set: \`slack.channel.history\` (user), \`slack.message.post\` (app or user), \`gmail.thread.list\`, \`gmail.message.read\`, \`gmail.message.send\` (user, \`inbox\`), \`warehouse.view.read\` (app). Resources are logical names, never IDs. Strict JSON.
58
+ One call, this exact shape, never a wrapper function; \`connection\`, \`operation\`, \`resource\`, \`mode\` and \`input.columns\` are catalog literals (logical names, never IDs or variables). \`isomorph check\` derives \`.isomorph/integrations.json\` from them (committed, never edited); \`dev\`/\`deploy\` file the request.
59
59
 
60
- \`\`\`json
61
- "company-slack": { "kind": "saas", "operations": { "slack.message.post": { "identity": "app", "resources": ["team-updates"] } } },
62
- "sales-warehouse": { "kind": "database", "operations": { "warehouse.view.read": { "identity": "app", "resources": { "weekly_sales": { "columns": ["week", "total"] } } } } }
60
+ \`\`\`ts
61
+ await isomorph.integrations.execute("sales-warehouse", { operation: "warehouse.view.read", resource: "weekly_sales", input: { columns: ["week", "total"], limit: 100 } });
63
62
  \`\`\`
64
63
 
65
64
  ## Catalog
@@ -70,16 +69,6 @@ Never guess a connection, channel, table, view, mailbox or column: a guess is re
70
69
 
71
70
  \`isomorph integrations request <connection> --reason "<why>" --app-root . --json\` files the one request IT approves once for every environment (\`isomorph dev\` and \`isomorph deploy\` file it too); submit it yourself, in the same turn. \`isomorph integrations status --app-root . --json\`: READY, use it; PENDING, say "IT has to approve this; the app works meanwhile". \`RESOURCE_NOT_APPROVED\`: IT adds the channel, view or mailbox in the console (Controls & integrations); then run the same request again.
72
71
 
73
- ## Call shape
74
-
75
- Always this exact shape, never a wrapper function: the kit gate reads the direct call.
76
-
77
- \`\`\`ts
78
- const report = await isomorph.integrations.execute("sales-warehouse", {
79
- operation: "warehouse.view.read", resource: "weekly_sales", input: { columns: ["week", "total"], limit: 100 }
80
- });
81
- \`\`\`
82
-
83
72
  ## Consent
84
73
 
85
74
  Only user-identity operations need it, built in unasked: the control calls \`isomorph.integrations.connect(connection)\`, follows \`authorizationUrl\` on \`consent_required\`, and returns to a connected state. Never on page load; app-identity operations never call connect.
@@ -90,7 +79,7 @@ A send (\`slack.message.post\`, \`gmail.message.send\`) runs only from an explic
90
79
 
91
80
  ## Slack modes
92
81
 
93
- \`"identity": "app"\` posts as the company's bot under IT's approved \`presentation.displayName\`; \`"identity": "user"\` posts as the person after consent (\`USER_RECONNECT_REQUIRED\`: offer Connect again). Name the \`mode\` when both are approved (\`MODE_REQUIRED\`); an unapproved mode is \`MODE_NOT_GRANTED\`, never swapped. \`RESOURCE_NOT_APPROVED\` on a post: the bot is not in the channel; anyone there runs \`/invite @Isomorph AI\`.`,
82
+ \`execute("company-slack", { …, mode: "app" })\` posts as the bot under \`slack.displayName\`/\`iconEmoji\` in \`.isomorph/app.json\`; \`mode: "user"\` posts as the person after consent (\`USER_RECONNECT_REQUIRED\`: offer Connect again). One mode per connection (\`MODE_REQUIRED\`); an unapproved mode is \`MODE_NOT_GRANTED\`, never swapped. \`RESOURCE_NOT_APPROVED\` on a post: the bot is not in the channel; \`/invite @Isomorph AI\`.`,
94
83
  ai: `# Governed AI
95
84
 
96
85
  AI goes through \`isomorph.ai\` only: one \`isomorph.ai.chat({ messages, maxTokens })\` call on the app's one client (\`src/isomorph.client.ts\`), never through a wrapper function (the gate reads only the direct call), behind a control the person presses — never on load, in an effect or a timer. Never add an OpenAI/Anthropic/Gemini key, SDK or URL: the governed AI gateway holds the key and IT sees every call. 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 not enabled an AI provider yet: say so in one line and keep the app working without AI.
@@ -1,5 +1,5 @@
1
1
  import { basename } from "node:path";
2
- import { linkIdempotencyKey, OPERATIONS, readDeclaration, readKitLock, requestResourceName, requestResources, writeKitLock, newKitLock } from "./kit.js";
2
+ import { linkIdempotencyKey, readDeclaration, readKitLock, requestResourceName, requestResources, writeKitLock, newKitLock } from "./kit.js";
3
3
  import { CliError } from "./output.js";
4
4
  import { CLI_VERSION } from "./version.js";
5
5
  export class GovernanceClient {
@@ -164,12 +164,7 @@ function settled(requests) {
164
164
  * so governance is not called.
165
165
  */
166
166
  export async function fileDeclaredRequests(root, client, tenantId, bundle) {
167
- const { declaration, errors } = await readDeclaration(root);
168
- if (errors.length) {
169
- if (errors[0].startsWith(".isomorph/integrations.json is missing"))
170
- return undefined;
171
- throw new CliError("DECLARATION_INVALID", `.isomorph/integrations.json is invalid: ${errors[0]}`);
172
- }
167
+ const declaration = await readDeclaration(root);
173
168
  const connections = Object.keys(declaration.connections);
174
169
  if (!connections.length)
175
170
  return undefined;
@@ -183,9 +178,7 @@ export async function fileDeclaredRequests(root, client, tenantId, bundle) {
183
178
  * for up to 30 s; PENDING is reported as pending, never as ready.
184
179
  */
185
180
  export async function requestIntegrations(root, client, tenantId, bundle, options) {
186
- const { declaration, errors } = await readDeclaration(root);
187
- if (errors.length)
188
- throw new CliError("DECLARATION_INVALID", `.isomorph/integrations.json is invalid: ${errors[0]}`);
181
+ const declaration = await readDeclaration(root);
189
182
  const appId = await ensureLinkedApp(root, client, tenantId, bundle);
190
183
  const submitted = settled(await submitRequests(client, appId, declaration, root, options.connection, options.reason, options.operations)).map(item => ({ ...item, state: item.outcome.state }));
191
184
  const sleep = options.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
@@ -352,9 +345,7 @@ export function renderGrantGroup(group, nowMs = Date.now()) {
352
345
  * each a separate cycle.
353
346
  */
354
347
  export async function assertDeployReady(root, client, tenantId, bundle, environment, output, callsAi) {
355
- const { declaration, errors } = await readDeclaration(root);
356
- if (errors.length && !errors[0].startsWith(".isomorph/integrations.json is missing"))
357
- throw new CliError("DECLARATION_INVALID", `.isomorph/integrations.json is invalid: ${errors[0]}`);
348
+ const declaration = await readDeclaration(root);
358
349
  const appId = await ensureLinkedApp(root, client, tenantId, bundle);
359
350
  const preflight = await client.deployPreflight(appId, { environment, callsAi, declaration, cliVersion: CLI_VERSION });
360
351
  if (preflight.ready && !blockersOf(preflight.blockers).length) {
@@ -386,25 +377,25 @@ export function deployBlocked(value, environment, output) {
386
377
  /** The code readers already know for a refusal that is all of one kind. */
387
378
  const BLOCKER_CODES = { integration: "INTEGRATIONS_NOT_READY", ai: "AI_NOT_READY", cli: "CLI_UPGRADE_REQUIRED" };
388
379
  /**
389
- * Splits the declared operations of one connection by identity mode — the
390
- * declaration's own `identity` (the gate has checked it against the closed
391
- * set; an operation with one mode is read as that mode). The app-mode part
392
- * that carries `slack.message.post` also carries the connection's declared
393
- * presentation, so IT sees "posts as" on the request.
380
+ * Splits the operations the app calls on one connection by identity mode — the
381
+ * derived declaration's own `identity` (the gate derived it from the call's
382
+ * `mode` or the operation's only identity and checked it against the closed
383
+ * set). The app-mode part that carries `slack.message.post` also carries the
384
+ * connection's presentation (from `.isomorph/app.json`), so IT sees "posts as"
385
+ * on the request. A connection or operation the app does not call cannot be
386
+ * asked for: the declaration is the source, so the fix is the call.
394
387
  */
395
388
  export function requestScope(declaration, connection, only) {
396
389
  const declared = declaration.connections[connection];
397
390
  if (!declared)
398
- throw new CliError("CONNECTION_NOT_DECLARED", `${connection} is not declared in .isomorph/integrations.json.`);
391
+ throw new CliError("CONNECTION_NOT_CALLED", `The app does not call ${connection}; add the isomorph.integrations.execute call first, then run \`isomorph check\`.`);
399
392
  const names = (only?.length ? only : Object.keys(declared.operations));
400
393
  const unknown = names.filter(name => !declared.operations[name]);
401
394
  if (unknown.length)
402
- throw new CliError("OPERATION_NOT_DECLARED", `${unknown.join(", ")} not declared for ${connection}.`);
395
+ throw new CliError("OPERATION_NOT_CALLED", `The app does not call ${unknown.join(", ")} on ${connection}; add the call first, then run \`isomorph check\`.`);
403
396
  const byMode = new Map();
404
397
  for (const name of names) {
405
- const allowed = OPERATIONS[name]?.identities ?? [];
406
- const declaredMode = declared.operations[name].identity;
407
- const mode = allowed.includes(declaredMode) ? declaredMode : allowed[0];
398
+ const mode = declared.operations[name].identity;
408
399
  const entry = byMode.get(mode) ?? { operations: [], resources: new Map() };
409
400
  entry.operations.push(name);
410
401
  for (const resource of requestResources(declared.operations[name]))
@@ -1,6 +1,6 @@
1
1
  export const PUBLISHED_KIT_BUNDLE = {
2
2
  "schema": "isomorph.kit-bundle/1.0",
3
- "kitVersion": "0.5.1",
3
+ "kitVersion": "0.6.0",
4
4
  "sdk": {
5
5
  "package": "@isomorph.ai/app-sdk",
6
6
  "version": "1.2.1",
@@ -8,21 +8,21 @@ export const PUBLISHED_KIT_BUNDLE = {
8
8
  "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:607e3896aa4116d82d0473677d38a68fecd4855a250cd44e038780290c335c67"
9
9
  },
10
10
  "images": {
11
- "appGateway": "public.ecr.aws/y6t4p3i8/harbour-app-gateway@sha256:558e3aa74f315a8bf2ae18f1344be3b8fad0203286f43d14d411a4a1f3e15dc0",
12
- "sessionFixture": "public.ecr.aws/y6t4p3i8/harbour-session-fixture@sha256:578cc8d98ca687dc79d8567f46abc636db2df0b3d8a01ccf38b30d5932c43353"
11
+ "appGateway": "public.ecr.aws/y6t4p3i8/harbour-app-gateway@sha256:7f7ae24c24d81cf2951ce8ae9033e252373c17f3a31299e05714103068273232",
12
+ "sessionFixture": "public.ecr.aws/y6t4p3i8/harbour-session-fixture@sha256:14803bd23b64c0ba5aa68022716c339a772ce0664c739dabda3457ec27978bb2"
13
13
  },
14
14
  "nativeRuntime": {
15
15
  "darwinArm64": {
16
- "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:e36aeea224867ebd7ae4a609284393460f707eaf6c50cb5f8ea1596926bae04b",
17
- "sha256": "e36aeea224867ebd7ae4a609284393460f707eaf6c50cb5f8ea1596926bae04b"
16
+ "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:42705c1b74d45f08b4f926e98b7ae15bcc8688d26d1424a74cf2d03472c9b332",
17
+ "sha256": "42705c1b74d45f08b4f926e98b7ae15bcc8688d26d1424a74cf2d03472c9b332"
18
18
  },
19
19
  "linuxX64": {
20
- "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:0673c75610f7fc8a5f1dbc1ec39570eee2278301430e7bbd4b4323319e7a1e6e",
21
- "sha256": "0673c75610f7fc8a5f1dbc1ec39570eee2278301430e7bbd4b4323319e7a1e6e"
20
+ "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:853563fb277f49eb8862ee06227d5faba8c82cb8de304357083c19d9f616fa81",
21
+ "sha256": "853563fb277f49eb8862ee06227d5faba8c82cb8de304357083c19d9f616fa81"
22
22
  },
23
23
  "windowsX64": {
24
- "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:8c1fc2e83d6b4cfd056b5252b9676a32e1e49e0596bb072f2c33faef63781aab",
25
- "sha256": "8c1fc2e83d6b4cfd056b5252b9676a32e1e49e0596bb072f2c33faef63781aab"
24
+ "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:8709b4922c8c5e3ba19662999df4beb97d01fe8a8a85b32d6025088234714a50",
25
+ "sha256": "8709b4922c8c5e3ba19662999df4beb97d01fe8a8a85b32d6025088234714a50"
26
26
  }
27
27
  },
28
28
  "brief": {
@@ -5,30 +5,18 @@ import { basename, dirname, join, parse, resolve } from "node:path";
5
5
  import { scanWorkspace } from "../../../src/analyzer.js";
6
6
  import { isKitBundle } from "./kit-bundle.js";
7
7
  import { CliError } from "./output.js";
8
- /**
9
- * Closed set: each operation has one connection kind and the identity modes it
10
- * may run under — exactly one, except slack.message.post, which the app
11
- * declares (`app` posts as the company's Slack app under its IT-approved name,
12
- * `user` posts as the signed-in person after their consent). Gmail is
13
- * user-mode only (two reads and one send as the signed-in person).
14
- */
15
- export const OPERATIONS = {
16
- "slack.channel.history": { identities: ["user"], kind: "saas" },
17
- "slack.message.post": { identities: ["app", "user"], kind: "saas" },
18
- "gmail.thread.list": { identities: ["user"], kind: "saas" },
19
- "gmail.message.read": { identities: ["user"], kind: "saas" },
20
- "gmail.message.send": { identities: ["user"], kind: "saas" },
21
- "warehouse.view.read": { identities: ["app"], kind: "database" }
22
- };
23
8
  /** Explicit reads `isomorph check --integrations` may run; every other operation is a send and never runs during checks. */
24
9
  export const READ_OPERATIONS = ["slack.channel.history", "gmail.thread.list", "warehouse.view.read"];
25
10
  /** Reads whose input needs an identifier from a prior read (a message id): listed as reads, exercised only through the list. */
26
11
  export const DEPENDENT_READ_OPERATIONS = ["gmail.message.read"];
27
12
  /**
28
- * Reads the committed declaration as JSON. The closed rules (schema, connection
29
- * kinds, the operation set, identity modes, logical resource names) are the
30
- * kit gate's `declaration` check — the pipeline's own — so a defect is named
31
- * once, in the same words, by `isomorph check` and by CodeBuild.
13
+ * Reads the declaration `isomorph check` derived from the app's
14
+ * `isomorph.integrations.execute` calls and wrote to `.isomorph/integrations.json`
15
+ * (data plane ADR 0016). Nobody edits the file: its closed rules are the kit
16
+ * gate's `declaration` check, so a defect is named once, at the call site, by
17
+ * `isomorph check` and by CodeBuild alike. A missing file is the empty
18
+ * declaration (`init` writes one); a file that is not JSON is not the gate's
19
+ * and the fix is to run the check.
32
20
  */
33
21
  export function emptyDeclaration() { return { schema: "isomorph.app-integrations/2.0", connections: {} }; }
34
22
  export function resourceNames(operation) { return Array.isArray(operation.resources) ? operation.resources : Object.keys(operation.resources); }
@@ -42,20 +30,20 @@ export async function readDeclaration(root) {
42
30
  raw = await readFile(kitPaths(root).declaration, "utf8");
43
31
  }
44
32
  catch {
45
- return { declaration: emptyDeclaration(), errors: [".isomorph/integrations.json is missing (run `isomorph init`)"] };
33
+ return emptyDeclaration();
46
34
  }
47
35
  let parsed;
48
36
  try {
49
37
  parsed = JSON.parse(raw);
50
38
  }
51
39
  catch {
52
- return { declaration: emptyDeclaration(), errors: [".isomorph/integrations.json is not valid JSON"] };
40
+ parsed = undefined;
53
41
  }
54
42
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
55
- return { declaration: emptyDeclaration(), errors: [".isomorph/integrations.json must be a JSON object"] };
43
+ throw new CliError("DECLARATION_UNREADABLE", ".isomorph/integrations.json is not the file `isomorph check` writes.", undefined, "Run `isomorph check --app-root .`: it derives the file from the app's isomorph.integrations.execute calls.");
56
44
  const record = parsed;
57
45
  const connections = record.connections && typeof record.connections === "object" && !Array.isArray(record.connections) ? record.connections : {};
58
- return { declaration: { schema: record.schema, connections }, errors: [] };
46
+ return { schema: record.schema, connections };
59
47
  }
60
48
  export function isKitLock(value) {
61
49
  const record = value;
@@ -156,7 +144,8 @@ export async function readAppProfile(root) {
156
144
  problems.push("audience must be a list of email addresses (an empty list means only you)");
157
145
  if (problems.length)
158
146
  throw new CliError("APP_PROFILE_INVALID", `.isomorph/app.json is invalid: ${problems.join("; ")}.`, undefined, "Fix the file, then run this again.");
159
- return { schema: "isomorph.app/1.0", name: record.name.trim(), description: (record.description ?? "").trim(), audience: record.audience.map(entry => entry.trim()) };
147
+ const slack = record.slack && typeof record.slack === "object" && typeof record.slack.displayName === "string" ? { slack: { displayName: record.slack.displayName, ...(typeof record.slack.iconEmoji === "string" ? { iconEmoji: record.slack.iconEmoji } : {}) } } : {};
148
+ return { schema: "isomorph.app/1.0", name: record.name.trim(), description: (record.description ?? "").trim(), audience: record.audience.map(entry => entry.trim()), ...slack };
160
149
  }
161
150
  /** Stable per-project identity for the kit-managed local state. */
162
151
  export function projectName(root, suffix = "") {
@@ -26,7 +26,13 @@ export function safeError(error) {
26
26
  const paths = error instanceof CliError && error.paths?.length ? error.paths : undefined;
27
27
  // Structured, so it is neither bounded nor redacted: a command's choices are its commands, and a refusal's blockers are what the reader acts on.
28
28
  const details = error instanceof CliError && error.details && Object.keys(error.details).length ? error.details : undefined;
29
- return { code, layer, message: redact(raw) || NO_DETAIL, ...(hint ? { remediationHint: hint } : {}), ...(paths ? { paths } : {}), ...(details ? { details } : {}) };
29
+ // A refusal another layer authored — the gate, governance, the pipeline — is
30
+ // the whole answer and is already bounded where it was written; cutting it
31
+ // here deleted the table name from a pipeline refusal on 2026-09-16 (the cut
32
+ // fell exactly on "No journey was generated for `notes`"). Only this CLI's
33
+ // own prose keeps the cap.
34
+ const message = (layer === "cli" ? redact(raw) : scrub(raw)) || NO_DETAIL;
35
+ return { code, layer, message, ...(hint ? { remediationHint: hint } : {}), ...(paths ? { paths } : {}), ...(details ? { details } : {}) };
30
36
  }
31
37
  /** Anything that must never reach a terminal, a log or an envelope. */
32
38
  const SECRETS = /https?:\/\/\S+|Bearer\s+\S+|(?:x-harbour|authorization|content-type)[^\n]*/gi;
@@ -70,8 +76,12 @@ const ELISION = " \u2026 ";
70
76
  * phrasings, it deletes words the server actually said, and it is the kind of
71
77
  * meaning-guessing the classifier deleted above was deleted for.
72
78
  */
79
+ /** Redacted and whitespace-folded, never bounded: what a server-authored refusal reaches the reader as. */
80
+ function scrub(value) {
81
+ return value.replace(SECRETS, "").replace(/\s+/g, " ").trim();
82
+ }
73
83
  function redact(value) {
74
- const said = value.replace(SECRETS, "").replace(/\s+/g, " ").trim();
84
+ const said = scrub(value);
75
85
  if (said.length <= DETAIL_BOUND)
76
86
  return said;
77
87
  const keep = DETAIL_BOUND - ELISION.length;
@@ -136,14 +136,11 @@ function kitFiles(root) {
136
136
  // Name, description and audience: what `isomorph deploy` prints before it starts and records
137
137
  // with the deployment. The folder name and an empty audience are only a starting point.
138
138
  ".isomorph/app.json": renderAppProfile(defaultAppProfile(root)),
139
- // No connection. The starter calls no company system, and a connection the
140
- // app does not call is a deploy that never happens: `isomorph deploy`
141
- // files the access request for every declared connection and refuses to
142
- // start until IT has approved it. One is added when the app really calls it —
143
- // the steps and the worked Slack and warehouse examples are in integrations.md
144
- // beside the `isomorph` skill (guide.ts). They are not in this file: it is read
145
- // with JSON.parse here and again by the pipeline's source intake, so it cannot
146
- // carry comments.
139
+ // No connection: the starter calls no company system. The kit gate derives
140
+ // this file from the app's isomorph.integrations.execute calls on every
141
+ // `isomorph check` (data plane ADR 0016); it is committed and packaged for
142
+ // governance, never edited. `init` writes the empty one so a fresh app has
143
+ // a declaration before its first check.
147
144
  ".isomorph/integrations.json": `${JSON.stringify(emptyDeclaration(), null, 2)}\n`
148
145
  };
149
146
  }
@@ -196,7 +193,7 @@ Created by \`isomorph init\`: a Vite + React app that runs on Isomorph. Sign-in,
196
193
 
197
194
  The kit's rules are in the \`isomorph\` skill that \`isomorph agent-setup\` installs for Claude Code (\`~/.claude/skills/isomorph/\`) and Codex (\`~/.codex/skills/isomorph/\`): \`core.md\` (identity, data, files, realtime, commands, error codes), \`integrations.md\` (Slack, Gmail, warehouse), \`ai.md\` and \`jobs.md\`. The managed block in CLAUDE.md / AGENTS.md points there.
198
195
 
199
- \`.isomorph/checks/\` is regenerated on each check and gitignored — put your own checks in \`tests/<name>.mjs\`. \`.isomorph/integrations.json\` declares the company systems the app calls: none, to start. \`.isomorph/app.json\` holds the app's name, description and audience.
196
+ \`.isomorph/checks/\` is regenerated on each check and gitignored — put your own checks in \`tests/<name>.mjs\`. \`.isomorph/integrations.json\` is derived by \`isomorph check\` from the app's \`isomorph.integrations.execute\` calls and committed, never edited. \`.isomorph/app.json\` holds the app's name, description and audience.
200
197
  `;
201
198
  const VITE_CONFIG = `import { defineConfig } from "vite";
202
199
  import react from "@vitejs/plugin-react";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/cli",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Isomorph development kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,7 +37,7 @@
37
37
  "harbour": {
38
38
  "kitBundle": {
39
39
  "repository": "public.ecr.aws/y6t4p3i8/harbour-kit-bundle",
40
- "version": "0.5.1"
40
+ "version": "0.6.0"
41
41
  }
42
42
  }
43
43
  }