@amalgm/automations 0.3.2 → 0.4.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.
Files changed (42) hide show
  1. package/AXIOMS.md +16 -1
  2. package/PURPOSE.md +30 -0
  3. package/README.md +53 -4
  4. package/dist/host/auth.js +4 -1
  5. package/dist/host/main.js +11 -1
  6. package/dist/host/notifications.d.ts +4 -0
  7. package/dist/host/notifications.js +19 -0
  8. package/dist/host/server.d.ts +1 -0
  9. package/dist/host/server.js +51 -10
  10. package/dist/host/skill.d.ts +2 -0
  11. package/dist/host/skill.js +33 -0
  12. package/dist/skills/amalgm-automations.5e4e14f0ace632c8383cd014e1f0f34b24ec0d5c5600132a840abc4c313b31d4.tgz +0 -0
  13. package/dist/skills/index.json +1 -0
  14. package/dist/src/cli/run.d.ts +1 -1
  15. package/dist/src/cli/run.js +17 -0
  16. package/dist/src/crud/triggers.js +0 -1
  17. package/dist/src/index.d.ts +1 -0
  18. package/dist/src/index.js +1 -0
  19. package/dist/src/machine-client.d.ts +3 -1
  20. package/dist/src/machine-client.js +6 -0
  21. package/dist/src/machine-event-stream.d.ts +8 -0
  22. package/dist/src/machine-event-stream.js +58 -0
  23. package/dist/src/machine-http.d.ts +2 -0
  24. package/dist/src/machine-http.js +8 -2
  25. package/dist/src/machine-notifications.d.ts +15 -0
  26. package/dist/src/machine-notifications.js +124 -0
  27. package/dist/src/machine.d.ts +1 -0
  28. package/dist/src/mcp.d.ts +2 -1
  29. package/dist/src/mcp.js +4 -2
  30. package/dist/src/runner.d.ts +3 -1
  31. package/dist/src/runner.js +69 -46
  32. package/dist/src/schema.js +9 -3
  33. package/dist/src/supabase-machine.d.ts +1 -0
  34. package/dist/src/supabase-machine.js +3 -0
  35. package/dist/src/tool-surface.js +21 -2
  36. package/package.json +3 -2
  37. package/skills/amalgm-automations/SKILL.md +49 -11
  38. package/skills/amalgm-automations/agents/openai.yaml +2 -2
  39. package/skills/amalgm-automations/references/command-contract.md +7 -0
  40. package/skills/amalgm-automations/references/setup-and-support.md +236 -0
  41. package/supabase/migrations/20260904020000_machine_run_notifications.sql +52 -0
  42. package/supabase/migrations/20260904030000_machine_notification_permissions.sql +4 -0
@@ -0,0 +1,124 @@
1
+ import { AutomationError } from './errors.js';
2
+ /** Ephemeral wakeups only. Supabase remains the queue and the eligibility clock. */
3
+ export function createMachineRunNotifications(options) {
4
+ const targets = new Map();
5
+ const encoder = new TextEncoder();
6
+ let connected = false;
7
+ const key = (userId, computerId) => JSON.stringify([userId, computerId]);
8
+ const refresh = async (target) => {
9
+ target.dirty = true;
10
+ if (target.refreshing)
11
+ return;
12
+ target.refreshing = true;
13
+ try {
14
+ while (target.dirty && target.listeners.size) {
15
+ target.dirty = false;
16
+ clearTimeout(target.timer);
17
+ const delay = await options.wakeDelay(target.userId, target.computerId);
18
+ if (target.dirty || !target.listeners.size)
19
+ continue;
20
+ if (delay === null)
21
+ continue;
22
+ if (!Number.isFinite(delay) || delay < 0)
23
+ throw new Error('Invalid machine wake delay');
24
+ if (delay === 0) {
25
+ for (const listener of target.listeners)
26
+ listener.send('wake');
27
+ }
28
+ else {
29
+ target.timer = setTimeout(() => void refresh(target), Math.min(delay, 2_147_483_647));
30
+ target.timer.unref();
31
+ }
32
+ }
33
+ }
34
+ catch {
35
+ options.log?.('automations.notifications.failed', { boundary: 'wake-deadline' });
36
+ for (const listener of [...target.listeners])
37
+ listener.close();
38
+ }
39
+ finally {
40
+ target.refreshing = false;
41
+ }
42
+ };
43
+ const disconnect = () => {
44
+ connected = false;
45
+ for (const target of targets.values()) {
46
+ for (const listener of [...target.listeners])
47
+ listener.close();
48
+ }
49
+ };
50
+ return Object.freeze({
51
+ connected() { connected = true; },
52
+ disconnected: disconnect,
53
+ close: disconnect,
54
+ changed(userId, computerId) {
55
+ const target = targets.get(key(userId, computerId));
56
+ if (target)
57
+ void refresh(target);
58
+ },
59
+ open(principal, signal) {
60
+ if (!connected)
61
+ throw new AutomationError('unavailable', 'Notifications are reconnecting', 503);
62
+ signal.throwIfAborted();
63
+ const targetKey = key(principal.userId, principal.computerId);
64
+ let target = targets.get(targetKey);
65
+ if (!target) {
66
+ target = { ...principal, listeners: new Set(), refreshing: false, dirty: false };
67
+ targets.set(targetKey, target);
68
+ }
69
+ const current = target;
70
+ let close = (_closeStream = true) => { };
71
+ const body = new ReadableStream({
72
+ start(controller) {
73
+ let closed = false;
74
+ let heartbeat;
75
+ let expiry;
76
+ const abort = () => close();
77
+ close = (closeStream = true) => {
78
+ if (closed)
79
+ return;
80
+ closed = true;
81
+ clearInterval(heartbeat);
82
+ clearTimeout(expiry);
83
+ signal.removeEventListener('abort', abort);
84
+ current.listeners.delete(listener);
85
+ if (!current.listeners.size) {
86
+ clearTimeout(current.timer);
87
+ targets.delete(targetKey);
88
+ }
89
+ if (closeStream)
90
+ controller.close();
91
+ };
92
+ const listener = {
93
+ send(event) {
94
+ if (closed)
95
+ return;
96
+ if ((controller.desiredSize ?? 0) <= 0)
97
+ return close();
98
+ controller.enqueue(encoder.encode(event === 'heartbeat'
99
+ ? ': keepalive\n\n' : `event: ${event}\ndata: {}\n\n`));
100
+ },
101
+ close,
102
+ };
103
+ current.listeners.add(listener);
104
+ signal.addEventListener('abort', abort, { once: true });
105
+ heartbeat = setInterval(() => listener.send('heartbeat'), options.heartbeatMs ?? 30_000);
106
+ heartbeat.unref();
107
+ const lifetime = Math.min(options.maxStreamMs ?? 300_000, (principal.authorizationExpiresAt ?? Infinity) - Date.now());
108
+ expiry = setTimeout(close, Math.max(0, lifetime));
109
+ expiry.unref();
110
+ listener.send('ready');
111
+ void refresh(current);
112
+ },
113
+ cancel() { close(false); },
114
+ }, { highWaterMark: 8 });
115
+ return new Response(body, {
116
+ headers: {
117
+ 'content-type': 'text/event-stream',
118
+ 'cache-control': 'no-store, no-transform',
119
+ 'x-accel-buffering': 'no',
120
+ },
121
+ });
122
+ },
123
+ });
124
+ }
@@ -4,6 +4,7 @@ export interface AutomationMachinePrincipal {
4
4
  readonly userId: string;
5
5
  readonly computerId: string;
6
6
  readonly scopes: readonly AutomationScope[];
7
+ readonly authorizationExpiresAt?: number;
7
8
  }
8
9
  export interface ClaimedAutomationRun extends AutomationRun {
9
10
  readonly leaseToken: string;
package/dist/src/mcp.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type AutomationCommands } from './command-surface.js';
2
3
  import type { AutomationCrud } from './contract.js';
3
4
  import { createAutomationToolSurface } from './tool-surface.js';
4
5
  export { createAutomationToolSurface };
5
6
  export type { AutomationToolSurface, AutomationView, CreateAutomationDefinition, UpdateAutomationDefinition, WorkflowChange, } from './tool-surface.js';
6
- export declare function createAutomationMcpServer(sdk: AutomationCrud): McpServer;
7
+ export declare function createAutomationMcpServer(sdk: AutomationCrud | AutomationCommands): McpServer;
package/dist/src/mcp.js CHANGED
@@ -14,7 +14,7 @@ const resultSchema = {
14
14
  };
15
15
  export function createAutomationMcpServer(sdk) {
16
16
  const server = new McpServer({ name: 'amalgm-automations-mcp-server', version: '0.1.0' });
17
- const commands = createAutomationCommands(sdk);
17
+ const commands = 'execute' in sdk ? sdk : createAutomationCommands(sdk);
18
18
  for (const definition of automationCommandDefinitions)
19
19
  register(server, commands, definition);
20
20
  return server;
@@ -23,7 +23,9 @@ function register(server, commands, definition) {
23
23
  server.registerTool(definition.mcpName, {
24
24
  title: definition.title,
25
25
  description: definition.description,
26
- inputSchema: definition.inputShape,
26
+ // Forward unknown top-level fields to the SDK's strict parser. The MCP
27
+ // SDK's default object parser strips them before our command sees them.
28
+ inputSchema: z.object(definition.inputShape).passthrough(),
27
29
  outputSchema: resultSchema,
28
30
  annotations: definition.annotations,
29
31
  }, async (input) => {
@@ -1,13 +1,15 @@
1
1
  import type { ClaimedAutomationRun, MachineRuns } from './machine.js';
2
2
  export interface AutomationMachineRunnerOptions {
3
3
  readonly runs: MachineRuns;
4
+ readonly notifications: (signal: AbortSignal) => AsyncIterable<void>;
4
5
  readonly execute: (run: ClaimedAutomationRun) => Promise<void>;
5
6
  readonly batchSize?: number;
6
7
  readonly leaseSeconds?: number;
7
- readonly pollIntervalMs?: number;
8
+ readonly reconnectDelayMs?: number;
8
9
  readonly maxBackoffMs?: number;
9
10
  readonly log?: (event: string, details?: Readonly<Record<string, unknown>>) => void;
10
11
  }
12
+ /** Listen first, then drain. There is no timer that asks an idle machine for work. */
11
13
  export declare function createAutomationMachineRunner(options: AutomationMachineRunnerOptions): Readonly<{
12
14
  start(): void;
13
15
  close(): Promise<void>;
@@ -1,61 +1,84 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ /** Listen first, then drain. There is no timer that asks an idle machine for work. */
1
3
  export function createAutomationMachineRunner(options) {
2
4
  const batchSize = integer(options.batchSize ?? 8, 1, 20, 'batchSize');
3
5
  const leaseSeconds = integer(options.leaseSeconds ?? 60, 30, 900, 'leaseSeconds');
4
- const pollIntervalMs = integer(options.pollIntervalMs ?? 1_000, 1, 60_000, 'pollIntervalMs');
5
- const maxBackoffMs = integer(options.maxBackoffMs ?? Math.max(30_000, pollIntervalMs), pollIntervalMs, 300_000, 'maxBackoffMs');
6
- let active = null;
7
- let timer = null;
8
- let stopped = true;
9
- let failures = 0;
10
- let nextDelay = pollIntervalMs;
11
- const schedule = (delay) => {
12
- if (stopped)
6
+ const reconnectDelayMs = integer(options.reconnectDelayMs ?? 1_000, 1, 60_000, 'reconnectDelayMs');
7
+ const maxBackoffMs = integer(options.maxBackoffMs ?? 30_000, reconnectDelayMs, 300_000, 'maxBackoffMs');
8
+ let lifetime = null;
9
+ let listening = null;
10
+ let draining = null;
11
+ let pending = false;
12
+ const wake = (connection, signal) => {
13
+ pending = true;
14
+ if (draining)
13
15
  return;
14
- timer = setTimeout(drain, delay);
15
- timer.unref?.();
16
- };
17
- const drain = () => {
18
- if (stopped || active)
19
- return active;
20
- active = options.runs.claim({ limit: batchSize, leaseSeconds })
21
- .then(async (runs) => {
22
- failures = 0;
23
- if (runs.length)
24
- options.log?.('automations.claimed', { count: runs.length });
25
- const settled = await Promise.allSettled(runs.map(options.execute));
26
- settled.forEach((result) => {
27
- if (result.status === 'rejected') {
28
- options.log?.('automations.run.failed', { error: safe(result.reason) });
16
+ draining = (async () => {
17
+ while (pending && !signal.aborted) {
18
+ pending = false;
19
+ const runs = await options.runs.claim({ limit: batchSize, leaseSeconds });
20
+ if (runs.length)
21
+ options.log?.('automations.claimed', { count: runs.length });
22
+ const settled = await Promise.allSettled(runs.map(options.execute));
23
+ for (const result of settled) {
24
+ if (result.status === 'rejected')
25
+ options.log?.('automations.run.failed', { error: safe(result.reason) });
29
26
  }
30
- });
31
- nextDelay = runs.length === batchSize ? 0 : pollIntervalMs;
32
- })
33
- .catch((error) => {
34
- failures += 1;
35
- const delayMs = Math.min(maxBackoffMs, pollIntervalMs * (2 ** Math.min(failures - 1, 10)));
36
- options.log?.('automations.poll.failed', { error: safe(error), retryInMs: delayMs });
37
- nextDelay = delayMs;
38
- })
39
- .finally(() => {
40
- active = null;
41
- schedule(nextDelay);
27
+ // Work may arrive while execution is busy, including a partial last batch.
28
+ pending ||= runs.length > 0;
29
+ }
30
+ })().catch((error) => {
31
+ options.log?.('automations.claim.failed', { error: safe(error) });
32
+ connection.abort(error);
33
+ }).finally(() => {
34
+ draining = null;
35
+ if (pending && !signal.aborted && !connection.signal.aborted)
36
+ wake(connection, signal);
42
37
  });
43
- return active;
38
+ };
39
+ const listen = async (signal) => {
40
+ let failures = 0;
41
+ while (!signal.aborted) {
42
+ const connection = new AbortController();
43
+ const connectedSignal = AbortSignal.any([signal, connection.signal]);
44
+ const startedAt = Date.now();
45
+ try {
46
+ for await (const _ of options.notifications(connectedSignal)) {
47
+ if (connectedSignal.aborted)
48
+ break;
49
+ wake(connection, signal);
50
+ }
51
+ }
52
+ catch (error) {
53
+ if (!signal.aborted)
54
+ options.log?.('automations.notifications.disconnected', { error: safe(error) });
55
+ }
56
+ finally {
57
+ connection.abort();
58
+ await draining;
59
+ }
60
+ if (signal.aborted)
61
+ break;
62
+ // A flapping stream must not reset its backoff merely by sending ready.
63
+ failures = Date.now() - startedAt >= 30_000 ? 0 : failures + 1;
64
+ const backoff = Math.min(maxBackoffMs, reconnectDelayMs * 2 ** Math.min(Math.max(0, failures - 1), 10));
65
+ await delay(backoff, undefined, { signal, ref: false }).catch(() => { });
66
+ }
44
67
  };
45
68
  return Object.freeze({
46
69
  start() {
47
- if (!stopped)
70
+ if (lifetime)
48
71
  return;
49
- stopped = false;
50
- failures = 0;
51
- void drain();
72
+ lifetime = new AbortController();
73
+ pending = false;
74
+ listening = listen(lifetime.signal);
52
75
  },
53
76
  async close() {
54
- stopped = true;
55
- if (timer)
56
- clearTimeout(timer);
57
- timer = null;
58
- await active;
77
+ lifetime?.abort();
78
+ await listening;
79
+ await draining;
80
+ lifetime = null;
81
+ listening = null;
59
82
  },
60
83
  });
61
84
  }
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod/v3';
2
2
  import { ValidationError } from './errors.js';
3
3
  import { compiledAutomationPlan } from './plan.js';
4
- import { IDENTIFIER } from './validation.js';
4
+ import { IDENTIFIER, schedule } from './validation.js';
5
5
  const maxPageSize = 100;
6
6
  const identifierMessage = 'Identifier is invalid';
7
7
  const text = (maximum, label) => z.string()
@@ -96,10 +96,16 @@ export function parseListAutomations(value) {
96
96
  return parse(listAutomationsSchema, value);
97
97
  }
98
98
  export function parseCreateScheduleTrigger(value) {
99
- return parse(createScheduleTriggerSchema, value);
99
+ const trigger = parse(createScheduleTriggerSchema, value);
100
+ schedule(trigger.cron, trigger.timezone ?? 'UTC');
101
+ return trigger;
100
102
  }
101
103
  export function parseUpdateScheduleTrigger(value) {
102
- return parse(updateScheduleTriggerSchema, value);
104
+ const patch = parse(updateScheduleTriggerSchema, value);
105
+ if (patch.cron !== undefined || patch.timezone !== undefined) {
106
+ schedule(patch.cron ?? '* * * * *', patch.timezone ?? 'UTC');
107
+ }
108
+ return patch;
103
109
  }
104
110
  export function parseCreateWebhookTrigger(value) {
105
111
  return parse(createWebhookTriggerSchema, value);
@@ -11,6 +11,7 @@ export declare class SupabaseMachineRunRepository implements MachineRunRepositor
11
11
  #private;
12
12
  private readonly client;
13
13
  constructor(client: MachineRpcClient);
14
+ wakeDelay(userId: string, computerId: string): Promise<number | null>;
14
15
  claim(input: Parameters<MachineRunRepository['claim']>[0]): Promise<ClaimedAutomationRun[]>;
15
16
  update(input: Parameters<MachineRunRepository['update']>[0]): Promise<ClaimedAutomationRun | null>;
16
17
  }
@@ -4,6 +4,9 @@ export class SupabaseMachineRunRepository {
4
4
  constructor(client) {
5
5
  this.client = client;
6
6
  }
7
+ wakeDelay(userId, computerId) {
8
+ return this.#call('amalgm_machine_wake_delay', { p_user_id: userId, p_target_id: computerId });
9
+ }
7
10
  async claim(input) {
8
11
  const rows = await this.#call('claim_amalgm_machine_runs', {
9
12
  p_user_id: input.userId,
@@ -1,4 +1,5 @@
1
- import { NotFoundError, ValidationError } from './errors.js';
1
+ import { AutomationError, NotFoundError, ValidationError } from './errors.js';
2
+ import { parseCreateAutomation, parseCreateScheduleTrigger, parseCreateWebhookTrigger, parseCreateWorkflow, parseUpdateAutomation, parseUpdateScheduleTrigger, parseUpdateWebhookTrigger, parseUpdateWorkflow, } from './schema.js';
2
3
  export function createAutomationToolSurface(sdk) {
3
4
  const view = async (automationId, runs = false) => {
4
5
  const automation = await sdk.automations.get(automationId);
@@ -14,6 +15,11 @@ export function createAutomationToolSurface(sdk) {
14
15
  const surface = {
15
16
  async create(input) {
16
17
  const { schedules = [], webhooks = [], workflow, ...automationInput } = input;
18
+ parseCreateAutomation(automationInput);
19
+ schedules.forEach(parseCreateScheduleTrigger);
20
+ webhooks.forEach(parseCreateWebhookTrigger);
21
+ if (workflow)
22
+ parseCreateWorkflow(workflow);
17
23
  validateCreateIds(schedules, webhooks);
18
24
  const staged = schedules.length > 0 || webhooks.length > 0 || workflow !== undefined;
19
25
  const requestedEnabled = automationInput.enabled !== false;
@@ -40,6 +46,7 @@ export function createAutomationToolSurface(sdk) {
40
46
  get: view,
41
47
  async update(automationId, input) {
42
48
  requireChanges(input);
49
+ validateUpdate(input);
43
50
  validateChanges(input.schedules, 'schedule');
44
51
  validateChanges(input.webhooks, 'webhook');
45
52
  const current = await sdk.automations.get(automationId);
@@ -98,6 +105,18 @@ function requireChanges(input) {
98
105
  throw new ValidationError('Automation update must include a change');
99
106
  }
100
107
  }
108
+ function validateUpdate(input) {
109
+ if (input.patch)
110
+ parseUpdateAutomation(input.patch);
111
+ input.schedules?.create?.forEach(parseCreateScheduleTrigger);
112
+ input.schedules?.update?.forEach(({ patch }) => parseUpdateScheduleTrigger(patch));
113
+ input.webhooks?.create?.forEach(parseCreateWebhookTrigger);
114
+ input.webhooks?.update?.forEach(({ patch }) => parseUpdateWebhookTrigger(patch));
115
+ if (input.workflow && 'create' in input.workflow)
116
+ parseCreateWorkflow(input.workflow.create);
117
+ if (input.workflow && 'update' in input.workflow)
118
+ parseUpdateWorkflow(input.workflow.update);
119
+ }
101
120
  function hasChanges(changes) {
102
121
  return Boolean(changes && [changes.create, changes.update, changes.delete]
103
122
  .some((items) => items && items.length > 0));
@@ -122,5 +141,5 @@ function validateCreateIds(schedules, webhooks) {
122
141
  }
123
142
  function disabledDraftError(automationId, error) {
124
143
  const reason = error instanceof Error ? error.message : String(error);
125
- return new Error(`Automation ${automationId} remains disabled because configuration failed: ${reason}`);
144
+ return new AutomationError(error instanceof AutomationError ? error.code : 'internal', `Automation ${automationId} remains disabled because configuration failed: ${reason}`, error instanceof AutomationError ? error.status : undefined);
126
145
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amalgm/automations",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Amalgm's automation SDK: durable trigger admission and target-machine execution.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -45,10 +45,11 @@
45
45
  "README.md"
46
46
  ],
47
47
  "scripts": {
48
- "build": "rm -rf dist && tsc -p tsconfig.build.json && tsx scripts/mark-executables.ts",
48
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && tsx scripts/mark-executables.ts && tsx scripts/build-public-skill.ts",
49
49
  "check": "tsx scripts/check-tree.ts && tsc -p tsconfig.json --noEmit",
50
50
  "test": "tsx --test test/*.test.ts",
51
51
  "test:supabase": "tsx --test test/integration/postgres.test.ts",
52
+ "test:notifications": "tsx scripts/test-notifications.ts",
52
53
  "verify": "npm run check && npm test && npm run build && tsx scripts/check-cli-artifact.ts",
53
54
  "release:check": "npm run verify && npm run test:supabase",
54
55
  "prepack": "npm run build",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: amalgm-automations
3
- description: Operate complete Amalgm automations through the Automations MCP tools or the global `amalgm automations` CLI, including schedules, webhooks, Run Now, run history, native commands, scripts, and Amalgm actions.
3
+ description: Set up, operate, and troubleshoot Amalgm Automations through MCP or the amalgm automations CLI. Use for installation, Google sign-in, scheduled or webhook workflows, Run Now, run history, and Automations support.
4
4
  ---
5
5
 
6
6
  # Amalgm Automations
@@ -10,6 +10,10 @@ workflow into one durable Amalgm automation and verify what actually ran.
10
10
  Automations owns configuration and permanent run history; the selected Amalgm
11
11
  machine executes the workflow.
12
12
 
13
+ Amalgm Automations is free. Guide the person through Google sign-in and computer
14
+ approval when needed; do not introduce a payment step. A native agent or other
15
+ external service still uses its own account and may have its own usage costs.
16
+
13
17
  ## Choose the available adapter
14
18
 
15
19
  Prefer the six `amalgm_automations_*` MCP tools when they are callable. In a
@@ -28,11 +32,22 @@ amalgm_automations_run_now amalgm automations run-now
28
32
  Both adapters accept the same JSON object and call the same SDK command
29
33
  surface. Do not translate one into a different resource-level API.
30
34
 
31
- For CLI use, first check `amalgm automations --help`. A signed-in, running
32
- Amalgm Shell supplies authentication through its loopback runtime. Never ask
33
- the user to copy a runtime token, DPoP proof, device key, or machine credential.
34
- If the runtime is unavailable, report that Shell must be signed in and running;
35
- do not fall back to an invented credential flow.
35
+ Reuse a working connection. For an available MCP connection, a small `list`
36
+ request with `{"limit":1}` proves access; do not install or log in again. For
37
+ CLI use, check `amalgm automations --help` and use
38
+ `amalgm automations list --input '{"limit":1}'`. An empty successful list is
39
+ ready, not a setup failure. Respect an explicitly selected account.
40
+
41
+ If the user asks to install or sign in, or that read fails, read
42
+ [setup and support](references/setup-and-support.md). It covers the supported
43
+ install, browser approval, account selection, runtime restart, and recovery
44
+ commands. Carry setup through to a successful Automations read, then resume
45
+ the original request. Do not stop at “Shell must be running.”
46
+
47
+ Shell owns authentication. Never read, copy, or ask the user to supply a runtime
48
+ token, DPoP proof, device key, or machine credential. The public connection
49
+ commands obtain what they need; environment-variable credential setup is not
50
+ the user login flow.
36
51
 
37
52
  Read [references/command-contract.md](references/command-contract.md) whenever
38
53
  composing `create` or `update`, or when exact fields are uncertain. Read
@@ -57,13 +72,16 @@ input.
57
72
  complex source. Do not expose those values in command arguments or logs.
58
73
  - Delete only after the exact id is established and deletion is within the
59
74
  user's request. Deleting configuration intentionally retains run history.
75
+ Omit `id` when creating a new automation; explicitly reusing a deleted id
76
+ refers to the same logical identity and retains its history and retry keys.
60
77
 
61
78
  ## Build one complete definition
62
79
 
63
80
  Create the automation, its schedule/webhook triggers, and its optional workflow
64
- in one task-level call. The service stages multi-resource changes safely and
65
- enables the definition only after setup succeeds. If setup fails, report the
66
- returned disabled draft id rather than silently creating a replacement.
81
+ in one task-level call. Invalid supplied configuration fails before any write.
82
+ The service stages valid multi-resource changes and enables the definition
83
+ only after setup succeeds. If a later service failure returns a disabled draft
84
+ id, get and repair that draft with `update`; do not create a replacement.
67
85
 
68
86
  Use a finite `maxOccurrences` when the user's request is bounded. Do not turn
69
87
  "ten times" into an unbounded schedule plus a future cleanup promise.
@@ -87,27 +105,47 @@ executables through the selected machine; never guess either.
87
105
  not evidence that work completed.
88
106
 
89
107
  1. Supply a stable, caller-chosen `idempotency_key` whenever the request might
90
- be retried.
108
+ be retried. Use a new key for each new logical run.
91
109
  2. If admission is retried, send the same automation id, input, and key. The
92
110
  returned run id must remain the same.
93
111
  3. Poll `get` with `include_runs: true` or a `run_query` until the admitted run
94
112
  becomes `completed` or `failed`. Match the exact run id; do not assume the
95
- newest unrelated run is yours.
113
+ newest unrelated run is yours. Space out reads and back off while state is
114
+ unchanged; a rate-limit response is a reason to wait, not to submit again.
96
115
  4. Inspect every step's status and bounded output. Report failure honestly,
97
116
  including the failing step and service error.
98
117
 
99
118
  `pending` means durable and awaiting its machine. `sent` or `running` means the
100
119
  machine holds a lease. Only `completed` and `failed` are terminal.
101
120
 
121
+ Keep the user informed while waiting. If the machine stays unavailable or the
122
+ current session cannot keep waiting, report the exact run id and observed
123
+ nonterminal state; do not claim completion, create a replacement, or promise
124
+ background monitoring that has not been set up.
125
+
102
126
  Diagnose at the failing boundary. Examples: `process_execution_unavailable`
103
127
  means that machine lacks the process host; an executable-not-found error is a
104
128
  machine PATH/install problem; `Installed agent has no model` is agent
105
129
  configuration; an action-specific HTTP error belongs to that action service.
106
130
  Do not relabel these as Run Now failures or patch the automation around them.
107
131
 
132
+ ## Help when something goes wrong
133
+
134
+ Use [setup and support](references/setup-and-support.md) for login/runtime
135
+ failures, HTTP 429 or reported rate limits, and contacting support. Preserve
136
+ the actual error code, distinguish the failing service, and use bounded retries.
137
+ Do not invent minute/hour allowances or reset times that the service has not
138
+ reported.
139
+
140
+ For an unresolved issue, offer **Email Aayush at aayush@amalgm.ai** and prepare
141
+ a concise, redacted draft when helpful. Send only with the user's explicit
142
+ authorization and an available email tool; otherwise give them the draft.
143
+
108
144
  ## Report the outcome
109
145
 
110
146
  Return the automation id, target choice if the user selected one, trigger
111
147
  summary, workflow lanes, and—when execution was requested—the exact run id and
112
148
  terminal result. Mention that run history is durable. Never claim a schedule
113
149
  fired merely because configuration creation succeeded.
150
+ Include automation/run links when returned by the product; do not invent a
151
+ deep link or use a private webhook URL as a viewing link.
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Amalgm Automations"
3
- short_description: "Create and verify durable Amalgm automations"
4
- default_prompt: "Use $amalgm-automations to create and verify an automation for me."
3
+ short_description: "Set up, run, and troubleshoot Amalgm automations"
4
+ default_prompt: "Use $amalgm-automations to help me connect my account and create and verify an automation."
@@ -15,6 +15,8 @@ Failures use stderr, a nonzero exit, and:
15
15
  ```
16
16
 
17
17
  Do not scrape prose or infer success from exit code alone; parse the envelope.
18
+ Unknown configuration fields are rejected. Arbitrary JSON inside workflow
19
+ action input and Run Now input remains valid payload data.
18
20
 
19
21
  ## create
20
22
 
@@ -62,6 +64,11 @@ At least one executable step is required when `compiled` is supplied. Omit
62
64
  webhook URL is the provider destination; a configured signing secret is
63
65
  write-only on later reads.
64
66
 
67
+ Omit `id` for a new automation. An explicit id names a persistent logical
68
+ identity: deleting and recreating it retains that identity's run history and
69
+ Run Now idempotency keys. Use a new key for each new logical run and reuse it
70
+ only when retrying that run.
71
+
65
72
  CLI example:
66
73
 
67
74
  ```bash