octwin-cli 0.8.17 → 0.9.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ repo root [`CHANGELOG.md`](../../CHANGELOG.md); this file is the CLI-only cut th
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.0] - 2026-10-21
11
+
12
+ ### Changed
13
+ - **`octwin automation` reads your RULES — what you wrote — not job rows.** BREAKING for scripts: the list
14
+ is each rule of the project in the platform's own words (the sentence the console's Outbound → Automatic
15
+ tab shows), on or paused, its week (sent / not delivered / failed / skipped) and its next time, under the
16
+ week's numbers and what needs a look; `--json` is `{ summary, rules }`. New: `coming` (every time still to
17
+ come), `happened` (every fire, its final word, why a message did not go) and `why <id>` (one line's steps
18
+ and the words sent — what a scripted test asserts a fire with). `pause` / `resume` / `run` take a rule key;
19
+ the key of the older job in your own `automation.yaml` finds the rule it runs as (`src/lib/rules.ts`). It
20
+ listed job rows until now — since the platform ran every pack's automation as rules, those were only its
21
+ own two sweeps, so nothing you declared showed in the terminal. `campaigns` and `send` are unchanged.
22
+ Upgrade before the platform retires the jobs routes: an older CLI's `octwin automation` will answer 404.
23
+
24
+ ### Fixed
25
+ - **`octwin records` lists a project's handoffs and cases even when the pack declares no types of its
26
+ own.** The catalog now carries the platform's entities with a `listed` flag per entity; the command
27
+ lists the listed ones (a missing flag, from an older server, reads as listed), so it neither prints
28
+ every unused platform type nor says "no record types" over a project full of handoffs.
29
+
10
30
  ## [0.8.17] - 2026-10-12
11
31
 
12
32
  ### Added
package/README.md CHANGED
@@ -89,7 +89,7 @@ refused by name rather than leaving stdout empty.
89
89
  | `octwin analytics` | Stage-by-stage conversion for **any** entity declared with a `pipeline:` (`--overview` / `--milestones` / `--trends` / `--cost`; `--stage <id>` lists the records currently at a stage). Needs `records:read`. Page `--stage` with `octwin analytics <entity> --stage <id> --limit n --offset n`. |
90
90
  | `octwin catalog` | Commerce products with price / availability / stock, plus the WhatsApp catalog binding. `--readiness` runs the Meta Graph checklist. Needs `catalog:read` + the `catalog` plan feature. Page with `octwin catalog --limit n --offset n`. |
91
91
  | `octwin scheduling` | The scheduling engine's state, or `--slots <resourceRecordId>` for the slots one bookable resource actually computes — how you verify the availability rules `deploy --seed` created. Needs `scheduling:read`. `octwin scheduling --slots <resourceRecordId> --from YYYY-MM-DD --days n` reads one resource's computed slots. |
92
- | `octwin automation` | The jobs your pack's automation declaration produced — status, interval and each one's **last result** (matched / acted / errors) — under a health line counted in SQL. `campaigns` lists the campaigns. Jobs are derived from declarations, so there is no `create`. Needs `automation:read`. `octwin automation campaigns` (or `--campaigns`) lists campaigns; page with `octwin automation --limit n --offset n`. |
92
+ | `octwin automation` | Your pack's **rules**, each in the platform's own words — on or paused, its week (sent / not delivered / failed / skipped) and its next time — under the week's numbers and what needs a look. `coming [--range today\|tomorrow\|week\|all] [--entity e] [--q text] [--cursor c]` lists every time still to come, `happened [--chip all\|not_delivered\|failed\|skipped\|moves\|tasks] [--period 7d\|30d] [--q text] [--cursor c]` every fire with its final word and why a message did not go, `why <id> [--kind timer\|fire\|queued]` one line's steps and the words sent. An older `segment` / `stale_records` / `sla_sweep` job or `on:` hook is listed as the rule it runs as. `campaigns` (or `--campaigns`) lists the campaigns, paged with `--limit n --offset n`. Needs `automation:read`. |
93
93
  | `octwin integrations` | What the pack **declares** beside what is actually **configured**, and it flags the gap: a connection declared and never configured is the commonest reason an integration silently never fires, and neither list alone shows it. `preflight <key>` diagnoses without calling out (`integrations:read`); `deliveries [id]` is the outbound log; `events` is what arrived inbound. Needs `integrations:read`. `octwin integrations deliveries --status s --operation id --limit n --offset n` is the outbound delivery log. |
94
94
  | `octwin journeys` | The customer journeys your pack declares, measured: `--funnel` (default) · `--overview` · `--goals` · `--trends` · `--cost` · `--definition` (what was declared, works with no traffic), plus `--stage <id>` for the runs sitting at a stage now. Same flag grammar as `octwin analytics` — a journey funnel and an entity funnel are the same question about different subjects. Needs `journeys:read`. Page `--stage` with `octwin journeys <id> --stage <stageId> --limit n --offset n`. |
95
95
  | `octwin performance` | The project's business indicators — value produced, conversion, duration — each with its delta against the previous window and a `why` naming the declaration it came from. `--detail` adds the breakdown. Needs `records:read`, **not** a `performance:*` scope (there is none). `octwin performance --range <days>` sets the window and `octwin performance --channel <whatsapp|web>` narrows every figure to one channel — the console's two controls. |
@@ -119,7 +119,7 @@ lists the verbs and their exact flags.
119
119
  | `octwin catalog` | `availability <sku> --to "in stock"` · `stock <sku> [--set-on-hand n]` |
120
120
  | `octwin scheduling` | `rules --resource <id>` · `rule add --resource <id> --dow 1 --start 09:00 --end 17:00 [--slot-minutes 30] [--capacity 1]` · `rule rm <ruleId>` · `exception add --resource <id> --date YYYY-MM-DD --kind closed\|extra [--start --end]` · `exception rm <id>` |
121
121
  | `octwin agents` | `set <ref> [--model m] [--enabled true\|false] [--overlay "…"\|none] [--enable-tool t] [--disable-tool t]` |
122
- | `octwin automation` | `run <jobId>` · `pause\|resume <jobId>` · `send <campaignId>` — the id may be the declaration **key** the list prints; the CLI resolves it to the uuid the route wants |
122
+ | `octwin automation` | `pause\|resume <ruleKey>` · `run <ruleKey>` (a rule on a timetable, now) · `send <campaignId>` — a rule key is what the list prints; the key of the older job in your own `automation.yaml` finds the rule it runs as |
123
123
  | `octwin integrations` | `test <key>` (a live call to the connection's `health:` operation) · `retry\|cancel\|send-now <deliveryId>` |
124
124
 
125
125
  `--set k=v` coerces JSON scalars (`--set rating=4.5` sends a number); `--fields-json` takes anything
@@ -155,7 +155,7 @@ octwin orders # then: octwin orders <reference_i
155
155
  octwin analytics # then: octwin analytics <entity> [--stage <id>]
156
156
  octwin catalog # products + stock + the WhatsApp binding
157
157
  octwin scheduling --slots <resourceRecordId> # the slots your availability rules compute
158
- octwin automation # did your declared jobs run, and what did they touch
158
+ octwin automation # your rules in words; then: coming · happened · why <id>
159
159
  octwin integrations # declared vs configured — the silent-never-fires check
160
160
  octwin journeys # then: octwin journeys <id> [--overview|--goals|--cost]
161
161
  octwin performance # value, conversion and duration, each with its `why`
package/dist/cli.js CHANGED
@@ -28,7 +28,7 @@
28
28
  * octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion, any pipelined entity
29
29
  * octwin catalog [--readiness] # commerce products + stock + the WhatsApp catalog binding (catalog:read)
30
30
  * octwin scheduling [--slots <resourceRecordId>] # engine state / computed slots (scheduling:read)
31
- * octwin automation [campaigns] # the jobs your declarations produced + health + last result (automation:read)
31
+ * octwin automation [coming|happened|why <id>|campaigns] # the project's rules in words, what is coming up, what happened (automation:read)
32
32
  * octwin integrations [deliveries|events|preflight|test …] # declared vs configured, and the delivery log (integrations:read)
33
33
  * octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] # declared journeys, measured (journeys:read)
34
34
  * octwin performance [--detail] # the project's business indicators (records:read — there is no performance scope)
@@ -85,6 +85,7 @@ import { classifyPackPath, isSkippedDir } from './lib/pack-source.js';
85
85
  import { BASE_FILE, deployBaseOf, hashBundle, planPull, readBase, writeBase } from './lib/base.js';
86
86
  import { formatChecksRefusal } from './lib/refusal.js';
87
87
  import { readPage, morePageHint } from './lib/page.js';
88
+ import { phraseText, resolveRuleKey, shortTime } from './lib/rules.js';
88
89
  import { chatLabel, answersTheTurn } from './lib/chat-line.js';
89
90
  import { kbOneLiner, buildKbIndexMarkdown, buildKbOutlineMarkdown, } from './lib/kb-index.js';
90
91
  import { buildSymbols, linkExplainers, renderSymbolsMarkdown } from './lib/kb-symbols.js';
@@ -240,9 +241,9 @@ const VERB_REQUIREMENTS = {
240
241
  // which does NOT confer this — that 403 is otherwise baffling.
241
242
  'projects create': { scope: 'projects:write' },
242
243
  'projects rm': { scope: 'projects:write' },
243
- // Jobs are declaration-derived, so there is no `create` — only acting on one.
244
- // `campaigns` is absent on purpose: it is a READ sharing the verb slot, and an
245
- // entry here would print "needs automation:write" on a read failure.
244
+ // Rules are declared, so there is no `create` — only acting on one (or a campaign).
245
+ // `coming` / `happened` / `why` / `campaigns` are absent on purpose: they are READS sharing
246
+ // the verb slot, and an entry here would print "needs automation:write" on a read failure.
246
247
  'automation run': { scope: 'automation:write' },
247
248
  'automation pause': { scope: 'automation:write' },
248
249
  'automation resume': { scope: 'automation:write' },
@@ -2763,12 +2764,16 @@ async function cmdRecords(flags) {
2763
2764
  out.print('No AI Agent is loaded in this project.');
2764
2765
  return;
2765
2766
  }
2766
- if (json?.has_xrm === false) {
2767
- // No record TYPES of its own — but the platform's own records (an order, a case) still read by name.
2767
+ // The LISTED types, as the console's switcher shows them: the catalog carries every platform
2768
+ // entity for lookups by name. A server older than the `listed` field sent declared types only,
2769
+ // so a missing one reads as listed.
2770
+ const ents = (json?.entities ?? []).filter(e => e.listed !== false);
2771
+ if (json?.has_xrm === false && ents.length === 0) {
2772
+ // No record TYPES of its own and no platform records yet — the platform's (an order, a case)
2773
+ // still read by name.
2768
2774
  out.print('This pack declares no record types of its own. Platform records still read by name: octwin records order | product | case | …');
2769
2775
  return;
2770
2776
  }
2771
- const ents = (json?.entities ?? []);
2772
2777
  if (ents.length === 0) {
2773
2778
  out.print('No entities visible — mint a token with the `records:read` scope to inspect data.');
2774
2779
  return;
@@ -4927,49 +4932,51 @@ async function cmdScheduling(flags) {
4927
4932
  out.print(` upcoming slots: ${json?.upcoming_slots ?? 0} booked seats: ${json?.booked_seats ?? 0}`);
4928
4933
  out.print('\nSlots for one resource: octwin scheduling --slots <resourceRecordId> (ids: octwin records <entity>)');
4929
4934
  }
4930
- // ── automation: declaration-derived jobs + campaigns ─────────────────────────
4935
+ // ── automation: the project's rules + campaigns ──────────────────────────────
4931
4936
  /**
4932
- * Verbs that mean "act on a job", not "an id".
4937
+ * Verbs of `octwin automation` — a view (`coming`, `happened`, `why`, `campaigns`) or an act (`run`, `pause`,
4938
+ * `resume`, `send`) — not a rule key.
4933
4939
  *
4934
- * `pause` / `resume` rather than a literal `status active|paused`: the route body
4935
- * takes the status, but the RBAC layer it calls checks the ACTION
4936
- * (`assertMay(…, action: 'pause' | 'resume')`), and `VERB_REQUIREMENTS` is keyed
4937
- * per verb — so one verb per intent makes both the permission hint and the 403
4938
- * say the right thing.
4940
+ * Until 0.9.0 the command read JOB ROWS (`…/automation/jobs`). Since the platform's migration 126 those are only its
4941
+ * own two sweeps: a pack's `rules:` — and its older `segment` / `stale_records` / `sla_sweep` jobs and `on:` hooks,
4942
+ * which run as compiled rules — were invisible in the terminal. It reads the rules routes the console's Outbound →
4943
+ * Automatic tab reads now, so the words are the server's.
4939
4944
  */
4940
- const AUTOMATION_VERBS = new Set(['run', 'pause', 'resume', 'campaigns', 'send']);
4945
+ const AUTOMATION_VERBS = new Set(['coming', 'happened', 'why', 'run', 'pause', 'resume', 'campaigns', 'send']);
4946
+ const AUTOMATION_WRITES = new Set(['run', 'pause', 'resume', 'send']);
4941
4947
  /**
4942
- * Turn whatever the author typed into the UUID the route demands.
4943
- *
4944
- * (`UUID_RE` is the one already declared for `--media`, deliberately reused rather
4945
- * than a second copy of the same pattern.)
4946
- *
4947
- * The write routes take a UUID path param and reject anything else with a bare
4948
- * *"Malformed identifier in the URL — expected a UUID"* 400. But the thing an author
4949
- * has in front of them is the declaration KEY (`cart_recovery_nudge`) — that is what
4950
- * the list prints, and it is the name in their own YAML. Measured: passing the key
4951
- * 400s on all three write verbs.
4952
- *
4953
- * So the key is resolved here, against the list route, rather than documented as a
4954
- * gotcha. A UUID passes straight through, and an unknown key fails naming the keys
4955
- * that DO exist — which is the answer to the question the author is actually asking.
4948
+ * Turn the campaign an author typed into the UUID the send route demands — the id `octwin automation campaigns`
4949
+ * prints, or the campaign's key. An unknown one fails naming those that exist.
4956
4950
  */
4957
- async function resolveAutomationId(kind, typed, base, t) {
4951
+ async function resolveCampaignId(typed, t) {
4958
4952
  if (UUID_RE.test(typed))
4959
4953
  return typed;
4960
- const { status, json } = await apiGet(`${base}/${kind}`, t);
4961
- if (status !== 200) {
4962
- die(`could not resolve '${typed}' — reading ${kind} failed (HTTP ${status})${errDetail(json)}`);
4963
- }
4954
+ const { url } = t;
4955
+ const base = `${url}/api/self/p/automation`;
4956
+ const { status, json } = await apiGet(`${base}/campaigns`, t);
4957
+ if (status !== 200)
4958
+ die(`could not resolve '${typed}' — reading campaigns failed (HTTP ${status})${errDetail(json)}`);
4964
4959
  const rows = readPage(json).rows;
4965
4960
  const hit = rows.find(r => r.key === typed || r.id === typed);
4966
4961
  if (hit?.id)
4967
4962
  return hit.id;
4968
4963
  const keys = rows.map(r => r.key ?? r.id).filter(Boolean);
4969
- die(`no ${kind === 'jobs' ? 'job' : 'campaign'} '${typed}' in this project.`
4970
- + (keys.length ? ` Available: ${keys.join(', ')}` : ` This project declares none.`));
4964
+ die(`no campaign '${typed}' in this project.` + (keys.length ? ` Available: ${keys.join(', ')}` : ' This project has none.'));
4971
4965
  }
4972
- /** `pause`/`resume`/`run`/`send` — the writes behind `octwin automation`. */
4966
+ /** The rule an author typed, against the project's rules (`resolveRuleKey`) — or the command dies naming them. */
4967
+ async function resolveRule(typed, t) {
4968
+ const { url } = t;
4969
+ const base = `${url}/api/self/p/automation`;
4970
+ const { status, json } = await apiGet(`${base}/rules`, t);
4971
+ if (status !== 200)
4972
+ die(`could not resolve '${typed}' — reading the rules failed (HTTP ${status})${errDetail(json)}`);
4973
+ const keys = (json?.rules ?? []).map(r => r.key).filter((k) => typeof k === 'string');
4974
+ const r = resolveRuleKey(typed, keys);
4975
+ if ('error' in r)
4976
+ die(r.error);
4977
+ return r.key;
4978
+ }
4979
+ /** `pause` / `resume` / `run` a rule, `send` a campaign — the writes behind `octwin automation`. */
4973
4980
  async function cmdAutomationWrite(flags) {
4974
4981
  const t = resolveTarget(flags);
4975
4982
  const { url } = t;
@@ -4977,75 +4984,64 @@ async function cmdAutomationWrite(flags) {
4977
4984
  const verb = flags._[0];
4978
4985
  const typed = flags._[1];
4979
4986
  const asJson = flags.json === true;
4980
- if (!typed)
4981
- die(`usage: octwin automation ${verb} <${verb === 'send' ? 'campaignId' : 'jobId'}> (ids: octwin automation${verb === 'send' ? ' campaigns' : ''})`);
4982
- const id = await resolveAutomationId(verb === 'send' ? 'campaigns' : 'jobs', typed, base, t);
4983
- if (verb === 'pause' || verb === 'resume') {
4984
- const { status, json } = await apiSend('PATCH', `${base}/jobs/${encodeURIComponent(id)}/status`, { status: verb === 'pause' ? 'paused' : 'active' }, t);
4985
- if (status === 404)
4986
- die(`job '${typed}' not found in ${targetLabel(t)} (ids: octwin automation)`);
4987
- // RBAC re-checks the ACTION on the job, so a 403 here can be a grant gap
4988
- // rather than a missing scope — same caveat as a record write.
4989
- if (status !== 200)
4990
- writeFail(`${verb} job '${typed}'`, status, json, url, true);
4991
- if (asJson) {
4992
- out.result(json);
4993
- return;
4994
- }
4995
- const j = json?.job ?? {};
4996
- out.print(`✓ job '${j.key ?? typed}' is now ${j.status}`);
4997
- if (j.next_run_at)
4998
- out.print(` next run: ${j.next_run_at}`);
4999
- return;
5000
- }
5001
- if (verb === 'run') {
4987
+ if (verb === 'send') {
4988
+ if (!typed)
4989
+ die('usage: octwin automation send <campaignId> (ids: octwin automation campaigns)');
4990
+ const id = await resolveCampaignId(typed, t);
5002
4991
  if (!asJson)
5003
- out.print(`→ Running job '${typed}' in ${targetLabel(t)} …`);
5004
- const { status, json } = await apiSend('POST', `${base}/jobs/${encodeURIComponent(id)}/run`, undefined, t);
4992
+ out.print(`→ Sending campaign '${typed}' in ${targetLabel(t)} …`);
4993
+ const { status, json } = await apiSend('POST', `${base}/campaigns/${encodeURIComponent(id)}/send`, undefined, t);
5005
4994
  if (status === 404)
5006
- die(`job '${typed}' not found in ${targetLabel(t)} (ids: octwin automation)`);
4995
+ die(`campaign '${typed}' not found in ${targetLabel(t)} (ids: octwin automation campaigns)`);
5007
4996
  if (status !== 200)
5008
- writeFail(`run job '${typed}'`, status, json, url, true);
4997
+ writeFail(`send campaign '${typed}'`, status, json, url, true);
5009
4998
  if (asJson) {
5010
4999
  out.result(json);
5011
5000
  return;
5012
5001
  }
5013
5002
  const r = json?.result ?? {};
5014
- out.print(`✓ ran '${typed}': matched ${r.matched ?? 0}, acted ${r.acted ?? 0}, errors ${r.errors ?? 0}`);
5015
- // `acted < matched` is the route's own definition of a PARTIAL run, so say so
5016
- // rather than leaving three numbers for the author to compare.
5017
- if ((r.errors ?? 0) > 0)
5018
- out.print(' ⚠ some rows errored — see the job\'s last_result in `octwin automation`');
5019
- else if ((r.acted ?? 0) < (r.matched ?? 0))
5020
- out.print(' partial: matched rows were skipped (cooldown, or already acted on)');
5003
+ const already = Number(r.already_queued ?? 0);
5004
+ out.print(`✓ campaign '${typed}': matched ${r.matched ?? 0}, enqueued ${r.enqueued ?? 0}${already ? `, ${already} already had it` : ''} [${r.stage ?? '?'}]`);
5005
+ // A campaign that stopped half-way is FINISHED by the same command: it queues only the contacts
5006
+ // with no delivery yet, so `already` is the earlier send's work, not a skip.
5007
+ if (already)
5008
+ out.print(' finished an earlier send — each contact gets this campaign once');
5009
+ if ((r.enqueued ?? 0) + already < (r.matched ?? 0))
5010
+ out.print(' partial: some matched records were not enqueued (records sharing a contact, or a record with none)');
5011
+ out.print(' enqueued ≠ delivered — watch the sends land with `octwin automation happened`');
5021
5012
  return;
5022
5013
  }
5023
- // send — one campaign
5024
- if (!asJson)
5025
- out.print(`→ Sending campaign '${typed}' in ${targetLabel(t)} …`);
5026
- const { status, json } = await apiSend('POST', `${base}/campaigns/${encodeURIComponent(id)}/send`, undefined, t);
5014
+ if (!typed)
5015
+ die(`usage: octwin automation ${verb} <ruleKey> (keys: octwin automation)`);
5016
+ const key = await resolveRule(typed, t);
5017
+ const path = `${base}/rules/${encodeURIComponent(key)}/${verb}`;
5018
+ if (!asJson && verb === 'run')
5019
+ out.print(`→ Running rule '${key}' in ${targetLabel(t)} …`);
5020
+ const { status, json } = await apiSend('POST', path, undefined, t);
5027
5021
  if (status === 404)
5028
- die(`campaign '${typed}' not found in ${targetLabel(t)} (ids: octwin automation campaigns)`);
5022
+ die(`rule '${key}' not found in ${targetLabel(t)} (keys: octwin automation)`);
5023
+ // RBAC re-checks the ACTION on `automation.rule`, so a 403 can be a grant gap rather than a missing scope; a 409
5024
+ // is the rule saying why not (only a rule on a timetable can be run now) — the server's words.
5029
5025
  if (status !== 200)
5030
- writeFail(`send campaign '${typed}'`, status, json, url, true);
5026
+ writeFail(`${verb} rule '${key}'`, status, json, url, true);
5031
5027
  if (asJson) {
5032
5028
  out.result(json);
5033
5029
  return;
5034
5030
  }
5035
- const r = json?.result ?? {};
5036
- const already = Number(r.already_queued ?? 0);
5037
- out.print(`✓ campaign '${typed}': matched ${r.matched ?? 0}, enqueued ${r.enqueued ?? 0}${already ? `, ${already} already had it` : ''} [${r.stage ?? '?'}]`);
5038
- // A campaign that stopped half-way is FINISHED by the same command: it queues only the contacts
5039
- // with no delivery yet, so `already` is the earlier send's work, not a skip.
5040
- if (already)
5041
- out.print(' finished an earlier send — each contact gets this campaign once');
5042
- if ((r.enqueued ?? 0) + already < (r.matched ?? 0))
5043
- out.print(' partial: some matched records were not enqueued (records sharing a contact, or a record with none)');
5044
- out.print(' enqueued ≠ delivered — watch the sends land with `octwin logs`');
5031
+ if (verb === 'run') {
5032
+ out.print(`✓ '${key}' runs now — it reaches ${json?.reaches ?? 0} record(s); next time after this run: ${shortTime(json?.next)}`);
5033
+ out.print(' see what it did: octwin automation happened');
5034
+ }
5035
+ else if (verb === 'pause') {
5036
+ out.print(`✓ '${key}' is paused — its times still arm, and nothing runs (nothing is caught up when it resumes)`);
5037
+ }
5038
+ else {
5039
+ out.print(json?.resumed === false ? ` '${key}' was not paused` : `✓ '${key}' runs again`);
5040
+ }
5045
5041
  }
5046
5042
  /**
5047
- * A campaign's delivery progress — the per-status counts of the reminders it queued. Not sent yet
5048
- * (a draft) is all zeros, and reads as a dash rather than "sent 0".
5043
+ * A campaign's delivery progress — the per-status counts of its deliveries. Not sent yet (a draft) is all zeros, and
5044
+ * reads as a dash rather than "sent 0".
5049
5045
  */
5050
5046
  function campaignProgressLine(p) {
5051
5047
  const n = (k) => Number(p?.[k] ?? 0);
@@ -5053,27 +5049,147 @@ function campaignProgressLine(p) {
5053
5049
  return '—'.padEnd(28);
5054
5050
  return `sent ${n('sent')} · failed ${n('failed')} · pending ${n('pending')}${n('cancelled') ? ` · cancelled ${n('cancelled')}` : ''}`.padEnd(28);
5055
5051
  }
5052
+ /** A line's record and customer, as one tail — `· Booking #7 · Sara`. */
5053
+ function lineTail(l) {
5054
+ const rec = l?.record ? ` · ${l.record.noun ?? l.record.entity ?? 'record'} #${l.record.number ?? '?'}` : '';
5055
+ const who = l?.contact ? ` · ${l.contact.name ?? 'customer'}` : '';
5056
+ return `${rec}${who}`;
5057
+ }
5058
+ /** A query string of the values that are set — `range=week&q=cairo`. Each caller names the flags it passes. */
5059
+ function setQs(values) {
5060
+ const p = new URLSearchParams();
5061
+ for (const [k, v] of Object.entries(values))
5062
+ if (typeof v === 'string')
5063
+ p.set(k, v);
5064
+ return p.toString();
5065
+ }
5066
+ /** `octwin automation coming` — every time still to come: a rule's, or a flow's own message (a send). */
5067
+ async function automationComing(flags, t) {
5068
+ const { url } = t;
5069
+ const base = `${url}/api/self/p/automation`;
5070
+ const asJson = flags.json === true;
5071
+ if (!asJson)
5072
+ out.print(`→ Reading what is coming up in ${targetLabel(t)} …`);
5073
+ const qs = setQs({ range: flags.range, entity: flags.entity, q: flags.q, cursor: flags.cursor });
5074
+ const { status, json } = await apiGet(`${base}/upcoming?${qs || 'range=week'}`, t);
5075
+ if (status !== 200)
5076
+ die(`could not read what is coming up (HTTP ${status})${errDetail(json)}${authFailureDetail(status, t.url)}`);
5077
+ if (asJson) {
5078
+ out.result(json);
5079
+ return;
5080
+ }
5081
+ const lines = (json?.lines ?? []);
5082
+ out.print(`Coming up in ${targetLabel(t)} (${typeof flags.range === 'string' ? flags.range : 'week'}): ${json?.total ?? lines.length}`);
5083
+ if (lines.length === 0)
5084
+ out.print(' (nothing — a rule\'s time arms when its record is made or changed)');
5085
+ for (const l of lines) {
5086
+ const mark = l.skipped ? ' [skipped]' : l.paused ? ' [paused]' : '';
5087
+ out.print(` ${shortTime(l.at)} ${phraseText(l.headline)}${lineTail(l)}${mark} ${l.kind === 'queued' ? 'message' : 'timer'} ${l.id}`);
5088
+ }
5089
+ if (json?.next)
5090
+ out.print(`\nMore: octwin automation coming --cursor ${json.next}`);
5091
+ if (lines.length)
5092
+ out.print('Why one goes, and what it will say: octwin automation why <id>');
5093
+ }
5094
+ /** `octwin automation happened` — every fire of the period: its final word, and why a message did not go. */
5095
+ async function automationHappened(flags, t) {
5096
+ const { url } = t;
5097
+ const base = `${url}/api/self/p/automation`;
5098
+ const asJson = flags.json === true;
5099
+ if (!asJson)
5100
+ out.print(`→ Reading what happened in ${targetLabel(t)} …`);
5101
+ const qs = setQs({ chip: flags.chip, period: flags.period, q: flags.q, cursor: flags.cursor });
5102
+ const { status, json } = await apiGet(`${base}/history?${qs || 'period=7d'}`, t);
5103
+ if (status !== 200)
5104
+ die(`could not read what happened (HTTP ${status})${errDetail(json)}${authFailureDetail(status, t.url)}`);
5105
+ if (asJson) {
5106
+ out.result(json);
5107
+ return;
5108
+ }
5109
+ const lines = (json?.lines ?? []);
5110
+ const c = json?.counts ?? {};
5111
+ out.print(`Happened in ${targetLabel(t)} (${typeof flags.period === 'string' ? flags.period : '7d'}): ${json?.total ?? lines.length}`
5112
+ + ` — ${c.not_delivered ?? 0} not delivered, ${c.failed ?? 0} failed, ${c.skipped ?? 0} skipped`);
5113
+ if (lines.length === 0)
5114
+ out.print(' (nothing yet)');
5115
+ for (const l of lines) {
5116
+ const why = l.code || l.reason ? ` (${l.code ?? l.reason})` : '';
5117
+ out.print(` ${shortTime(l.at)} ${String(l.outcome ?? '—').padEnd(13)} ${phraseText(l.headline)}${lineTail(l)}${why} ${l.id}`);
5118
+ }
5119
+ if (json?.next)
5120
+ out.print(`\nMore: octwin automation happened --cursor ${json.next}`);
5121
+ if (lines.length)
5122
+ out.print('Each step and the words sent: octwin automation why <id>');
5123
+ }
5056
5124
  /**
5057
- * `octwin automation [--campaigns] [--json]` — the jobs a pack's declarations
5058
- * produced, with their last result, plus the health counts.
5125
+ * `octwin automation why <id>` — one line's *Why?*, as the console's drawer: the rule in words, each step with its
5126
+ * outcome, the words the customer reads. The line's kind picks the route (`--kind timer|fire|queued`); without it
5127
+ * each is asked in turn — a coming time, a fire, a send.
5128
+ */
5129
+ async function automationWhy(flags, t) {
5130
+ const { url } = t;
5131
+ const base = `${url}/api/self/p/automation`;
5132
+ const asJson = flags.json === true;
5133
+ const id = flags._[1];
5134
+ if (!id)
5135
+ die('usage: octwin automation why <id> [--kind timer|fire|queued] (ids: octwin automation coming | happened)');
5136
+ const kinds = typeof flags.kind === 'string' ? [flags.kind] : ['timer', 'fire', 'queued'];
5137
+ for (const kind of kinds) {
5138
+ if (!['timer', 'fire', 'queued'].includes(kind))
5139
+ die(`--kind is timer, fire or queued (got '${kind}')`);
5140
+ const { status, json } = await apiGet(`${base}/why/${kind}/${encodeURIComponent(id)}`, t);
5141
+ if (status === 404)
5142
+ continue;
5143
+ if (status !== 200)
5144
+ die(`could not read why (HTTP ${status})${errDetail(json)}${authFailureDetail(status, t.url)}`);
5145
+ if (asJson) {
5146
+ out.result(json);
5147
+ return;
5148
+ }
5149
+ const v = json ?? {};
5150
+ out.print(`${phraseText(v.line?.headline)}${lineTail(v.line)} — ${shortTime(v.line?.at)}`);
5151
+ if (v.rule)
5152
+ out.print(` rule ${v.rule.key}: ${phraseText(v.rule.sentence)}`);
5153
+ if (v.line?.outcome)
5154
+ out.print(` outcome: ${v.line.outcome}${v.line.code ? ` (${v.line.code})` : ''}${v.line.reason ? ` — ${v.line.reason}` : ''}`);
5155
+ for (const check of (v.checks ?? []))
5156
+ out.print(` checked again before it acts: ${phraseText(check)}`);
5157
+ if (v.words?.text)
5158
+ out.print(` the customer reads${v.words.tag ? ` (${v.words.tag})` : ''}: ${v.words.text}`);
5159
+ for (const s of (v.steps ?? [])) {
5160
+ const send = s.send ? ` · ${s.send.status}${s.send.reason ? ` (${s.send.reason})` : ''}` : '';
5161
+ out.print(` step ${s.verb}: ${s.skipped ? `held back (${s.skipped})` : s.state}${send}${s.detail ? ` — ${s.detail}` : ''}`);
5162
+ }
5163
+ if (v.queued)
5164
+ out.print(` a flow's own message: ${v.queued.status}${v.queued.error ? ` — ${v.queued.error}` : ''}`);
5165
+ return;
5166
+ }
5167
+ die(`nothing under '${id}' in ${targetLabel(t)} — a coming time may have just happened, and a fire is kept 30 days`);
5168
+ }
5169
+ /**
5170
+ * `octwin automation [--json]` — the project's rules, each in the server's words, under the week's numbers.
5059
5171
  *
5060
- * Needs `automation:read`. The job list is CAPPED server-side and the counts are
5061
- * computed in SQL, so the header numbers come from `/health` rather than from
5062
- * filtering the page — past the cap a client-side count would depend on the cap
5063
- * instead of the data.
5172
+ * Needs `automation:read`. The words are the console's (`rules/describe.ts` writes each rule as a sentence), so the
5173
+ * terminal says what the Automatic tab says.
5064
5174
  */
5065
5175
  async function cmdAutomation(flags) {
5066
- if (typeof flags._[0] === 'string' && AUTOMATION_VERBS.has(flags._[0])) {
5067
- // `campaigns` is a READ that shares the verb slot with the writes.
5068
- if (flags._[0] !== 'campaigns')
5069
- return cmdAutomationWrite(flags);
5176
+ const verb = typeof flags._[0] === 'string' ? flags._[0] : null;
5177
+ if (verb && AUTOMATION_WRITES.has(verb))
5178
+ return cmdAutomationWrite(flags);
5179
+ if (verb && !AUTOMATION_VERBS.has(verb)) {
5180
+ die(`unknown automation verb '${verb}' — octwin automation [coming | happened | why <id> | campaigns | run|pause|resume <ruleKey> | send <campaignId>]`);
5070
5181
  }
5071
5182
  const t = resolveTarget(flags);
5072
5183
  const { url } = t;
5073
5184
  const base = `${url}/api/self/p/automation`;
5074
5185
  const asJson = flags.json === true;
5075
- const campaigns = flags._[0] === 'campaigns' || flags.campaigns === true;
5076
- if (campaigns) {
5186
+ if (verb === 'coming')
5187
+ return automationComing(flags, t);
5188
+ if (verb === 'happened')
5189
+ return automationHappened(flags, t);
5190
+ if (verb === 'why')
5191
+ return automationWhy(flags, t);
5192
+ if (verb === 'campaigns' || flags.campaigns === true) {
5077
5193
  if (!asJson)
5078
5194
  out.print(`→ Reading campaigns from ${targetLabel(t)} …`);
5079
5195
  const { status, json } = await apiGet(`${base}/campaigns?${pagingQs(flags)}`, t);
@@ -5100,40 +5216,37 @@ async function cmdAutomation(flags) {
5100
5216
  return;
5101
5217
  }
5102
5218
  if (!asJson)
5103
- out.print(`→ Reading automation jobs from ${targetLabel(t)} …`);
5104
- const [jobs, health] = await Promise.all([
5105
- apiGet(`${base}/jobs`, t),
5106
- apiGet(`${base}/health`, t),
5107
- ]);
5108
- if (jobs.status !== 200)
5109
- die(`could not read automation jobs (HTTP ${jobs.status})${errDetail(jobs.json)}${authFailureDetail(jobs.status, url)}`);
5219
+ out.print(`→ Reading the rules of ${targetLabel(t)} …`);
5220
+ const [rules, summary] = await Promise.all([apiGet(`${base}/rules`, t), apiGet(`${base}/summary`, t)]);
5221
+ if (rules.status !== 200)
5222
+ die(`could not read the rules (HTTP ${rules.status})${errDetail(rules.json)}${authFailureDetail(rules.status, url)}`);
5223
+ const s = summary.status === 200 ? summary.json : null;
5110
5224
  if (asJson) {
5111
- out.result({ jobs: jobs.json, health: health.json });
5225
+ out.result({ summary: s, rules: rules.json?.rules ?? [] });
5112
5226
  return;
5113
5227
  }
5114
- const page = readPage(jobs.json);
5115
- const h = health.status === 200 ? (health.json ?? {}) : {};
5116
- out.print(`Automation in ${targetLabel(t)}: ${h.total ?? page.rows.length} job(s)`
5117
- + ` — ${h.active ?? '?'} active, ${h.paused ?? '?'} paused, ${h.failing ?? '?'} failing, ${h.never_checked ?? '?'} never checked`);
5118
- if (page.rows.length === 0) {
5119
- out.print(' (none — jobs are DERIVED from the pack\'s automation declaration, not created here.');
5120
- out.print(' No `automation.yaml` block → no jobs. `octwin deploy` installs them.)');
5121
- return;
5228
+ const list = (rules.json?.rules ?? []);
5229
+ const w = s?.week ?? {};
5230
+ out.print(`Automation in ${targetLabel(t)}: ${list.length} rule(s)`
5231
+ + (s ? ` — this week ${w.sent ?? 0} sent, ${w.notDelivered ?? 0} not delivered, ${w.failed ?? 0} failed, ${s.counts?.upcoming ?? 0} coming up` : ''));
5232
+ for (const n of (s?.needsLook ?? [])) {
5233
+ out.print(` ⚠ ${n.rule?.key ?? '?'}: ${n.kind === 'paused' ? 'paused' : n.kind === 'not_delivered' ? `${n.n} not delivered${n.codes?.length ? ` (${n.codes.join(', ')})` : ''}` : `${n.n} failed`}`);
5122
5234
  }
5123
- for (const j of page.rows) {
5124
- const r = j.last_result ?? {};
5125
- // A job runs only when its look finds work, so "not run yet" is normal; "never checked" is the dead one.
5126
- const ran = (j.last_run_at ? `last run ${j.last_run_at}` : 'not run yet')
5127
- + (j.last_checked_at ? `, checked ${j.last_checked_at}` : ', never checked');
5128
- const result = j.last_result
5129
- ? ` matched ${r.matched ?? 0}/acted ${r.acted ?? 0}${(r.errors ?? 0) > 0 ? `/ERRORS ${r.errors}` : ''}`
5130
- : '';
5131
- out.print(` ${String(j.key ?? j.id).padEnd(26)} ${String(j.status).padEnd(7)} ${j.kind}/${j.entity ?? '—'}`
5132
- + ` every ${j.interval_seconds}s ${ran}${result}`);
5235
+ if (list.length === 0) {
5236
+ out.print(' (none — a pack writes rules in automation.yaml `rules:`; `octwin deploy` installs them)');
5237
+ return;
5133
5238
  }
5134
- out.print('\nRun one now: octwin automation run <jobId> (jobId = the `key` above, or its uuid)');
5135
- out.print('Pause/resume: octwin automation pause|resume <jobId>');
5136
- out.print('Campaigns: octwin automation campaigns');
5239
+ for (const r of list) {
5240
+ const wk = r.week ?? {};
5241
+ out.print(` ${String(r.key)} [${r.paused ? 'paused' : 'on'}] next ${shortTime(r.next)}`
5242
+ + ` · week ${wk.total ?? 0}: ${wk.sent ?? 0} sent, ${wk.notDelivered ?? 0} not delivered, ${wk.failed ?? 0} failed, ${wk.skipped ?? 0} skipped`);
5243
+ out.print(` ${phraseText(r.sentence)}`);
5244
+ }
5245
+ out.print('\nComing up: octwin automation coming [--range today|tomorrow|week|all]');
5246
+ out.print('Happened: octwin automation happened [--chip not_delivered|failed|skipped|moves|tasks] [--period 7d|30d]');
5247
+ out.print('Why one line: octwin automation why <id>');
5248
+ out.print('Act: octwin automation pause|resume|run <ruleKey> (needs automation:write)');
5249
+ out.print('Campaigns: octwin automation campaigns');
5137
5250
  }
5138
5251
  // ── integrations: declared connections, their credentials, and the delivery log ──
5139
5252
  /** Verbs that act on a connection or a delivery, rather than naming one. */
@@ -5952,7 +6065,7 @@ function help() {
5952
6065
  octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
5953
6066
  octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
5954
6067
  octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
5955
- octwin automation [campaigns] [--json] # the jobs your declarations produced + health, last result each
6068
+ octwin automation [coming|happened|why <id>|campaigns] [--json] # the rules in words; coming up; happened, and why
5956
6069
  octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
5957
6070
  octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
5958
6071
  octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
@@ -5975,7 +6088,7 @@ Writes — exercise the state your pack creates (each needs the matching :write
5975
6088
  octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
5976
6089
  octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
5977
6090
  octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
5978
- octwin automation run <jobId> | pause <jobId> | resume <jobId> | send <campaignId>
6091
+ octwin automation pause|resume|run <ruleKey> | send <campaignId>
5979
6092
  octwin integrations test <key> # a LIVE call to the connection's health: operation
5980
6093
  octwin integrations retry|cancel|send-now <deliveryId>
5981
6094
  (octwin integrations preflight <key> needs only integrations:read — it makes no call)
@@ -6260,24 +6373,35 @@ octwin projects rm <slug> [--yes]
6260
6373
 
6261
6374
  --dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
6262
6375
  \`--slots\` is how you check what a rule actually produces.`,
6263
- automation: `octwin automation [campaigns | --campaigns] [--limit n] [--offset n] [--json]
6264
- No args = every job the pack's automation declaration produced, with its status,
6265
- interval and LAST RESULT (matched / acted / errors), under a health line whose
6266
- counts come from SQL rather than from filtering the page — the job list is capped
6267
- server-side, so a client-side count would depend on the cap. Needs automation:read.
6268
-
6269
- Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
6270
- the pack means no jobs, and \`octwin deploy\` is what installs them.
6271
-
6272
- WRITES (automation:write):
6273
- octwin automation run <jobId> # run once, now — prints matched/acted/errors
6274
- octwin automation pause|resume <jobId>
6275
- octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
6276
-
6277
- <jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
6278
- accept only a uuid — the CLI resolves the key for you, and names the keys that do
6279
- exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
6280
- missing scope: the action is re-checked against the job.`,
6376
+ automation: `octwin automation [coming | happened | why <id> | campaigns | --campaigns] [--json]
6377
+ No args = every RULE of the project — each in the platform's own words (the
6378
+ sentence the console's Outbound → Automatic tab shows), on or paused, its week
6379
+ (sent / not delivered / failed / skipped) and its next time — under the week's
6380
+ numbers and what needs a look. Needs automation:read.
6381
+
6382
+ A pack writes rules in automation.yaml \`rules:\`; its older \`segment\` /
6383
+ \`stale_records\` / \`sla_sweep\` jobs and xrm \`on:\` hooks run as rules too,
6384
+ listed under the key they compile to (\`segment:<job key>\`).
6385
+
6386
+ READ:
6387
+ octwin automation coming [--range today|tomorrow|week|all] [--entity e] [--q text] [--cursor c]
6388
+ # every time still to come — a rule's, or a flow's own message
6389
+ octwin automation happened [--chip all|not_delivered|failed|skipped|moves|tasks] [--period 7d|30d] [--cursor c]
6390
+ # every fire: its final word, and why a message did not go
6391
+ octwin automation why <id> [--kind timer|fire|queued]
6392
+ # one line: the rule in words, each step, the words sent
6393
+ octwin automation campaigns [--limit n] [--offset n]
6394
+
6395
+ WRITES (automation:write):
6396
+ octwin automation pause|resume <ruleKey> # paused: its times arm, nothing runs, nothing caught up
6397
+ octwin automation run <ruleKey> # a rule on a timetable, now
6398
+ octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
6399
+
6400
+ <ruleKey> is the key the list prints; the key of the older job in your own
6401
+ automation.yaml finds the rule it runs as. A 403 on a write can be an RBAC grant
6402
+ gap rather than a missing scope: the action is re-checked on automation.rule.
6403
+ (Until 0.9.0 this command listed job rows — since the platform ran every pack's
6404
+ automation as rules, those were only its own two sweeps.)`,
6281
6405
  integrations: `octwin integrations [--json]
6282
6406
  What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
6283
6407
  connection that is declared and never configured is the commonest reason an
@@ -6410,7 +6534,7 @@ export const COMMANDS = {
6410
6534
  analytics: { run: cmdAnalytics, json: 'document', flags: ['funnel', 'overview', 'milestones', 'trends', 'cost', 'stage', 'limit', 'offset'] },
6411
6535
  catalog: { run: cmdCatalog, json: 'document', flags: ['limit', 'offset', 'readiness', 'set-on-hand', 'to'] },
6412
6536
  scheduling: { run: cmdScheduling, json: 'document', flags: ['capacity', 'date', 'days', 'dow', 'end', 'from', 'kind', 'resource', 'slot-minutes', 'slots', 'start'] },
6413
- automation: { run: cmdAutomation, json: 'document', flags: ['campaigns', 'limit', 'offset'] },
6537
+ automation: { run: cmdAutomation, json: 'document', flags: ['campaigns', 'chip', 'cursor', 'entity', 'kind', 'limit', 'offset', 'period', 'q', 'range'] },
6414
6538
  integrations: { run: cmdIntegrations, json: 'document', flags: ['limit', 'offset', 'operation', 'status'] },
6415
6539
  journeys: { run: cmdJourneys, json: 'document', flags: ['funnel', 'overview', 'goals', 'trends', 'cost', 'definition', 'stage', 'limit', 'offset'] },
6416
6540
  performance: { run: cmdPerformance, json: 'document', flags: ['detail', 'range', 'channel'] },
@@ -0,0 +1,36 @@
1
+ /**
2
+ * What `octwin automation` needs to speak RULES — the project's rules as the platform's Automatic tab reads them
3
+ * (`/api/self/p/automation/…`). Pure, so the two decisions it makes are tested on their own:
4
+ *
5
+ * - **which rule an author means** (`resolveRuleKey`) — the key `octwin automation` prints, or the key of the older
6
+ * `automation.yaml` job it was compiled from: a `segment` job `cart_nudge` runs as the rule `segment:cart_nudge`,
7
+ * and the job key is what the author has in their own YAML;
8
+ * - **the server's words** (`phraseText`) — a rule's sentence and a line's headline arrive as `Phrase`s (text, a
9
+ * strong part, a quote with named slots), the same the console draws; the terminal prints them flat.
10
+ */
11
+ /** A sentence of `Phrase`s as one line of text — a quote in quotes, a slot as `{name}`. */
12
+ export function phraseText(phrases) {
13
+ return (phrases ?? []).map(p => p.text ?? p.strong
14
+ ?? (p.quote ? `"${p.quote.map(q => q.text ?? `{${q.slot ?? ''}}`).join('')}"` : '')).join('');
15
+ }
16
+ /**
17
+ * The rule an author typed: its key exactly, else the one rule whose key ends in `:<typed>` (an older job's key, or a
18
+ * hook's moment). Two such rules, or none, is an answer that names what does exist.
19
+ */
20
+ export function resolveRuleKey(typed, keys) {
21
+ if (keys.includes(typed))
22
+ return { key: typed };
23
+ const hits = keys.filter(k => k.endsWith(`:${typed}`));
24
+ if (hits.length === 1)
25
+ return { key: hits[0] };
26
+ if (hits.length > 1)
27
+ return { error: `'${typed}' could be ${hits.length} rules — ${hits.join(', ')}. Use the full key.` };
28
+ return { error: `no rule '${typed}' in this project.` + (keys.length ? ` Rules: ${keys.join(', ')}` : ' It runs none.') };
29
+ }
30
+ /** A time as the terminal shows it — `2026-10-22 09:00Z`; a missing one is a dash. */
31
+ export function shortTime(iso) {
32
+ if (!iso)
33
+ return '—';
34
+ const d = new Date(iso);
35
+ return Number.isNaN(d.getTime()) ? iso : `${d.toISOString().slice(0, 16).replace('T', ' ')}Z`;
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octwin-cli",
3
- "version": "0.8.17",
3
+ "version": "0.9.0",
4
4
  "description": "Octwin external-pack developer CLI (by CEQUENS) — scaffold, validate, deploy, and check pure-YAML packs on your tenant.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,10 @@
1
+ # What the pack does by itself — rules: WHEN something changes (`on:`) or a time arrives (`at:`), FOR a
2
+ # record, DO actions. Guide: `craft-automation`; every change, time and action: `rules-guide`.
3
+ rules:
4
+ # The journey's goal, reached by the RECORD — so a request an operator creates counts too, not
5
+ # only chat ones. Guide: `craft-measure`.
6
+ - key: request_made
7
+ for: request
8
+ on: created
9
+ do:
10
+ - track: { journey: request, goal_reached: requested }
@@ -16,11 +16,7 @@ entities:
16
16
  pipeline:
17
17
  stages: [new, scheduled, done, cancelled]
18
18
  terminal: [done, cancelled]
19
- # The journey's goal, reached by the RECORD — so a request an operator creates counts too, not
20
- # only chat ones. Guide: `craft-measure`.
21
- on:
22
- create:
23
- - track: { journey: request, goal_reached: requested }
19
+ # What happens by itself when a request is made, moves or goes quiet is a rule — `automation.yaml`.
24
20
 
25
21
  # Rows every new project starts with (`octwin deploy --seed`) — a customer may see them.
26
22
  demo: