octwin-cli 0.8.11 → 0.8.12

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
@@ -5,6 +5,30 @@ Format: [Keep a Changelog](https://keepachangelog.com/) — newest first, bucket
5
5
  **Added · Changed · Deprecated · Removed · Fixed · Security**. The platform-wide view lives in the
6
6
  repo root [`CHANGELOG.md`](../../CHANGELOG.md); this file is the CLI-only cut that ships with the package.
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.8.12] - 2026-09-25
11
+
12
+ ### Changed
13
+ - **The package pointed its `homepage` at npm — the page it is rendered on.** It is
14
+ <https://octwin.ai> now, and the README's Links section gains the platform, the guides and the
15
+ marketplace. The README describes Octwin at length and linked only to npm and nodejs.org.
16
+ - **`octwin usage` with a project-pinned token reports that project.** The platform now treats a
17
+ token's project pin as a ceiling on every route, so a pinned token is refused on the
18
+ workspace-wide rollup. `usage` asks `whoami` for the pin and reads the project view instead of
19
+ printing a 403 — which is what its help text (`project if pinned, else workspace`) always said.
20
+
21
+ ### Fixed
22
+ - **`octwin chat` sat out its whole 45-second wait on every message during a live handoff, and
23
+ read as hung.** While a person holds the conversation (or one has been asked for) the AI Agent
24
+ deliberately answers nothing, and the CLI could not tell that from a slow pack. The platform now
25
+ says so with a `held` event on the web stream; `chat` prints
26
+ *"⏸ a person is holding this conversation — the AI Agent will not reply…"* (or *"waiting for a
27
+ person to join…"*), still collects any operator line replayed by that message, and returns. A
28
+ held turn is not a failure: it exits 0, and `--script` carries on to the next line, so the
29
+ customer's side of a handoff can be scripted. Needs a platform that sends `held`; an older one
30
+ behaves as before. Reported in `xpeng-egypt/FEEDBACK.md`, session 3.
31
+
8
32
  ## [0.8.11] - 2026-09-07
9
33
 
10
34
  ### Fixed
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![license](https://img.shields.io/npm/l/octwin-cli.svg)](https://www.npmjs.com/package/octwin-cli)
7
7
  [![node](https://img.shields.io/node/v/octwin-cli.svg)](https://nodejs.org)
8
8
 
9
- Octwin is a pack-pluggable conversational-agent platform for WhatsApp and web. A **pack** is a
9
+ [Octwin](https://octwin.ai) is a pack-pluggable conversational-agent platform for WhatsApp and web. A **pack** is a
10
10
  self-contained bot domain — its agent, conversation flows, prompts, and data model — declared
11
11
  entirely in YAML. `octwin-cli` lets you build a **pure-YAML pack** in your own repo and deploy it
12
12
  to a running Octwin platform to test live on your own tenant: no platform checkout, no build step,
@@ -238,6 +238,9 @@ declaration files — read its `INDEX.md` rather than this paragraph.)
238
238
 
239
239
  ## Links
240
240
 
241
+ - **Octwin:** <https://octwin.ai> — the platform this CLI deploys to
242
+ - **Guides & playbooks:** <https://octwin.ai/artifacts> — how each capability works, end to end
243
+ - **Marketplace:** <https://octwin.ai/packs> — published packs you can read before writing your own
241
244
  - **npm:** <https://www.npmjs.com/package/octwin-cli>
242
245
  - **Command help:** `octwin help` (each subcommand also answers `--help`)
243
246
  - **Changelog:** [CHANGELOG.md](./CHANGELOG.md)
package/dist/index.js CHANGED
@@ -66,7 +66,7 @@ import { parse as parseYaml } from 'yaml';
66
66
  import { applyRenames } from './lib/rename.js';
67
67
  import { validatePackBundle, describePackNameProblem, asPackName } from './lib/validate.js';
68
68
  import { loadAllowedRenderKeys, findRenderKeyViolations, describeRenderFinding } from './lib/render-check.js';
69
- import { loadPrimitiveArgSpecs, findArgViolations, describeArgFinding } from './lib/args-check.js';
69
+ import { loadPrimitiveArgSpecs, findArgViolations, describeArgFinding, findArgWarnings, describeArgWarning, } from './lib/args-check.js';
70
70
  import { yamlLineOf } from './lib/yaml-pos.js';
71
71
  import { localhostFamilyHint } from './lib/net-hint.js';
72
72
  import { authHeaders as buildAuthHeaders } from './lib/client-headers.js';
@@ -1049,13 +1049,33 @@ async function cmdValidate(flags) {
1049
1049
  if (!args.specs)
1050
1050
  return { kind: 'not-run', lookup: args.lookup };
1051
1051
  const findings = withLines(yamlDocs().flatMap(([p, doc]) => findArgViolations(doc, p, args.specs)));
1052
- return findings.length
1053
- ? {
1052
+ if (findings.length) {
1053
+ return {
1054
1054
  kind: 'findings', noun: 'primitive-argument error',
1055
1055
  lines: findings.map(describeArgFinding),
1056
1056
  hint: 'fix these before deploying — an undeclared argument is dropped with no error at runtime',
1057
- }
1058
- : { kind: 'passed', line: 'primitive arguments match their declared inputs' };
1057
+ };
1058
+ }
1059
+ /**
1060
+ * Errors first, and only then the warnings — a `findings` outcome exits, so a warning
1061
+ * printed above it would never be read, and one printed below it would never be reached.
1062
+ *
1063
+ * `warnsTogether` is the platform's own declaration (`record_save` with `record_id:` +
1064
+ * `stage:` refuses the whole write on an existing record). It has been enforced server-side
1065
+ * since 2026-09-06 and was reachable only through `--remote`; the author who lost a write
1066
+ * to it was on CLI 0.6.0, offline. Measured before shipping: **0 findings across all 48
1067
+ * `record_save` call sites in the 23 marketplace packs** — the check is silent on a clean
1068
+ * corpus, which is the bar a warning has to clear before it is worth printing at all.
1069
+ */
1070
+ const warnings = withLines(yamlDocs().flatMap(([p, doc]) => findArgWarnings(doc, p, args.specs)));
1071
+ if (warnings.length) {
1072
+ return {
1073
+ kind: 'warnings', noun: 'argument-combination warning',
1074
+ lines: warnings.map(describeArgWarning),
1075
+ hint: 'legal YAML, and usually not what you meant — read each one and decide.',
1076
+ };
1077
+ }
1078
+ return { kind: 'passed', line: 'primitive arguments match their declared inputs' };
1059
1079
  },
1060
1080
  },
1061
1081
  {
@@ -1822,7 +1842,7 @@ async function cmdSeed(flags) {
1822
1842
  if (stepErrors.length) {
1823
1843
  // A kind failed but the rest ran — the reconcile softens each step. Say which,
1824
1844
  // and exit non-zero so a scripted `seed && chat` doesn't read as clean.
1825
- console.error(`
1845
+ console.error(`
1826
1846
  ⚠ ${stepErrors.length} step${stepErrors.length === 1 ? '' : 's'} failed — data may be incomplete:`);
1827
1847
  for (const e of stepErrors)
1828
1848
  console.error(` • ${e}`);
@@ -2281,6 +2301,24 @@ async function apiGet(endpoint, t) {
2281
2301
  }
2282
2302
  return { status: res.status, json };
2283
2303
  }
2304
+ /**
2305
+ * The project this token is pinned to, or null (unpinned, or the lookup failed). A pinned token
2306
+ * cannot reach workspace-wide routes, so a command with a workspace view and a project view uses
2307
+ * this to pick the one the token can actually read. `authHeaders`, never a hand-built header — an
2308
+ * authenticating call without it stamps NULL over the token's client report.
2309
+ */
2310
+ async function pinnedProjectOf(t) {
2311
+ try {
2312
+ const res = await fetch(`${t.url}/api/self/t/whoami`, { headers: authHeaders(t) });
2313
+ if (!res.ok)
2314
+ return null;
2315
+ const j = await res.json();
2316
+ return typeof j.project_slug === 'string' && j.project_slug ? j.project_slug : null;
2317
+ }
2318
+ catch {
2319
+ return null;
2320
+ }
2321
+ }
2284
2322
  /**
2285
2323
  * Send a WRITE to an admin endpoint with the deploy token; returns `{ status, json }`.
2286
2324
  *
@@ -2776,9 +2814,25 @@ async function cmdLogs(flags) {
2776
2814
  console.log(`Timeline for ${convId} (${events.length} events):`);
2777
2815
  for (const e of events) {
2778
2816
  const isErr = e.type === 'tool' && e.subtype === 'platform_error';
2779
- const tag = isErr ? '⚠ ERROR' : `${e.type}${e.subtype ? `/${e.subtype}` : ''}`;
2817
+ /**
2818
+ * An ADVISORY is a turn that worked and that you probably did not mean.
2819
+ *
2820
+ * It reads differently from an error on purpose: nothing failed, so `⚠ ERROR` would be a lie
2821
+ * and the plain `tool/platform_advisory` tag would be scrolled past. The platform raises these
2822
+ * exactly where a silent-but-wrong result used to be invisible — a `record_list` scoped to a
2823
+ * customer who owns none of a shared entity returns zero rows and the `empty` port, which
2824
+ * prints identically to a real no-match. Three separate authors lost debugging sessions to
2825
+ * that one before it had anywhere to appear.
2826
+ */
2827
+ const isAdvisory = e.type === 'tool' && e.subtype === 'platform_advisory';
2828
+ const tag = isErr ? '⚠ ERROR' : isAdvisory ? '! CHECK' : `${e.type}${e.subtype ? `/${e.subtype}` : ''}`;
2780
2829
  const body = typeof e.content === 'string' ? e.content : JSON.stringify(e.content ?? '');
2781
2830
  console.log(` ${e.ts ?? ''} ${tag}: ${body ?? ''}`);
2831
+ // The remedy, indented under its advisory. Printing the observation without the fix is what
2832
+ // the envelope field and the server log already did, and neither reached an author.
2833
+ if (isAdvisory && typeof e.metadata?.remedy === 'string' && e.metadata.remedy) {
2834
+ console.log(` → ${e.metadata.remedy}`);
2835
+ }
2782
2836
  // The renders this turn actually sent (captured outbound payloads) — the
2783
2837
  // other half of the story next to the structured tap lines.
2784
2838
  const captured = Array.isArray(e.metadata?.raw) ? e.metadata.raw : [];
@@ -2938,19 +2992,12 @@ function parseChatScript(body) {
2938
2992
  }
2939
2993
  return turns;
2940
2994
  }
2941
- /**
2942
- * Send ONE turn and print everything it rendered. Returns the new frame boundary
2943
- * (so the next turn only accepts frames newer than this turn's output) and how
2944
- * many renders arrived.
2945
- *
2946
- * Splitting this out is what makes `--script` reliable. Driving a multi-step flow
2947
- * by chaining shell invocations (`chat A && chat B`) races the agent loop: this
2948
- * command ends a turn on a QUIET GAP, and the server-side loop can still be
2949
- * running when the process exits, so the next invocation's inbound lands
2950
- * mid-turn. The agent then fills required fields with placeholder text, or starts
2951
- * a second workflow run. Inside one process the loop simply waits for the settle
2952
- * before sending the next turn, over the same SSE connection.
2953
- */
2995
+ /** The line printed for a held turn — what the author needs to know, not a failure. */
2996
+ function heldLine(voice) {
2997
+ return voice === 'human'
2998
+ ? '⏸ a person is holding this conversation — the AI Agent will not reply. The operator answers from Live chat in the console; nothing more comes back here for this turn.'
2999
+ : '⏸ waiting for a person to join — the AI Agent will not reply until someone accepts in Live chat, or the wait runs out and the conversation returns to it.';
3000
+ }
2954
3001
  async function runChatTurn(args) {
2955
3002
  const { url, tenant, project, from, turn, frames, asJson } = args;
2956
3003
  let boundary = args.boundary;
@@ -2990,15 +3037,23 @@ async function runChatTurn(args) {
2990
3037
  console.log(` … delivered — waiting for the reply (up to ${Math.round(REPLY_TIMEOUT_MS / 1000)}s)`);
2991
3038
  // Collect THIS turn's renders (id > boundary). A turn can send several
2992
3039
  // messages, so keep reading until a quiet gap after the last render.
3040
+ //
3041
+ // A HELD turn ends the wait too. While a person holds the conversation (or one has been
3042
+ // asked for) the platform deliberately sends no AI Agent reply, and says so with a `held`
3043
+ // envelope; before it did, this loop sat out the whole REPLY_TIMEOUT_MS on every held turn
3044
+ // and the author read it as a hang. After `held` the loop still settles for TURN_SETTLE_MS,
3045
+ // because the same customer message replays any operator line queued while they were away.
2993
3046
  const deadline = Date.now() + REPLY_TIMEOUT_MS;
2994
3047
  let rendersSeen = 0;
3048
+ let held = null;
2995
3049
  for (;;) {
2996
3050
  const remaining = deadline - Date.now();
2997
3051
  if (remaining <= 0)
2998
3052
  break;
2999
- const f = await frames.next(rendersSeen > 0 ? TURN_SETTLE_MS : Math.min(remaining, REPLY_TIMEOUT_MS));
3053
+ const settling = rendersSeen > 0 || held !== null;
3054
+ const f = await frames.next(settling ? TURN_SETTLE_MS : Math.min(remaining, REPLY_TIMEOUT_MS));
3000
3055
  if (f === 'timeout') {
3001
- if (rendersSeen > 0)
3056
+ if (settling)
3002
3057
  break;
3003
3058
  else
3004
3059
  continue;
@@ -3009,6 +3064,11 @@ async function runChatTurn(args) {
3009
3064
  continue; // late replay stragglers
3010
3065
  if (f.id != null)
3011
3066
  boundary = f.id;
3067
+ if (f.ev?.kind === 'held' && held === null) {
3068
+ held = f.ev.voice === 'human' ? 'human' : 'pending';
3069
+ if (!asJson)
3070
+ console.log(heldLine(held));
3071
+ }
3012
3072
  if (asJson) {
3013
3073
  console.log(JSON.stringify(f.ev));
3014
3074
  if (f.ev?.kind === 'render')
@@ -3021,7 +3081,7 @@ async function runChatTurn(args) {
3021
3081
  console.log(`← ${f.ev.body ?? '(no text body)'}${f.ev.hint?.type && f.ev.hint.type !== 'text' ? ` (render: ${f.ev.hint.type})` : ''}`);
3022
3082
  printHint(f.ev.hint);
3023
3083
  }
3024
- return { boundary, rendersSeen };
3084
+ return { boundary, rendersSeen, held };
3025
3085
  }
3026
3086
  /** `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--script <file>] [--json]`
3027
3087
  * — drive a turn (or a whole scripted conversation) through the dev web channel
@@ -3122,13 +3182,18 @@ async function cmdChat(flags) {
3122
3182
  // Phase 2+3 — run each turn in order, over the SAME connection. One turn is
3123
3183
  // the normal case; `--script` drives a whole conversation.
3124
3184
  let totalRenders = 0;
3185
+ let anyHeld = false;
3125
3186
  for (const [i, turn] of turns.entries()) {
3126
3187
  if (turns.length > 1)
3127
3188
  console.log(`\n── turn ${i + 1}/${turns.length} ──`);
3128
3189
  const r = await runChatTurn({ url, tenant, project, from, turn, frames, boundary, asJson });
3129
3190
  boundary = r.boundary;
3130
3191
  totalRenders += r.rendersSeen;
3131
- if (r.rendersSeen === 0 && turns.length > 1) {
3192
+ if (r.held)
3193
+ anyHeld = true;
3194
+ // A held turn answered nothing BY DESIGN — the script goes on, so an author can script the
3195
+ // customer's side of a handoff (their lines reach the operator's pane) without it stopping.
3196
+ if (r.rendersSeen === 0 && !r.held && turns.length > 1) {
3132
3197
  // Stop rather than fire the rest of the script into a conversation that
3133
3198
  // isn't answering — the remaining turns would land out of context.
3134
3199
  await cancel();
@@ -3137,7 +3202,7 @@ async function cmdChat(flags) {
3137
3202
  }
3138
3203
  }
3139
3204
  await cancel();
3140
- if (totalRenders === 0) {
3205
+ if (totalRenders === 0 && !anyHeld) {
3141
3206
  console.error(` no reply after ${Math.round(REPLY_TIMEOUT_MS / 1000)}s — the pack may not be warm yet, or the turn produced no render.`);
3142
3207
  exitNow(1);
3143
3208
  }
@@ -5150,9 +5215,13 @@ async function cmdUsage(flags) {
5150
5215
  const t = resolveTarget(flags);
5151
5216
  const { url } = t;
5152
5217
  const asJson = flags.json === true;
5153
- const scoped = Boolean(t.project);
5218
+ // A token pinned to one project is refused on the workspace rollup (the pin is a ceiling on
5219
+ // every route), so for a pinned token the project view IS the answer — ask whoami rather
5220
+ // than print a 403 at someone whose token can only ever see one project.
5221
+ const pinned = t.project ? null : await pinnedProjectOf(t);
5222
+ const scoped = Boolean(t.project || pinned);
5154
5223
  if (!asJson)
5155
- console.log(`→ Reading model usage for ${scoped ? targetLabel(t) : 'the whole workspace'} …`);
5224
+ console.log(`→ Reading model usage for ${scoped ? (pinned ? `project '${pinned}'` : targetLabel(t)) : 'the whole workspace'} …`);
5156
5225
  // Both URLs written out in place, for the same reason as `performance` above: an
5157
5226
  // `endpoint` variable would leave BOTH invisible to `cli-routes.test.ts`.
5158
5227
  const { status, json } = scoped
@@ -5185,388 +5254,388 @@ async function cmdUsage(flags) {
5185
5254
  console.log('\nThis is MODEL spend. WhatsApp/Meta message billing is operator-only — not reachable by an API token.');
5186
5255
  }
5187
5256
  function help() {
5188
- console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
5189
-
5190
- octwin --version # print the CLI version (+ any upgrade notice)
5191
- octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
5192
- octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
5193
- octwin login --url <platformUrl> # approve in a browser; the CLI collects the token
5194
- octwin login --url <platformUrl> --token oct_… # or paste a deploy token from the console
5195
- octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
5196
- octwin projects [--archived] [--json] # the --project slugs this token can name
5197
- octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
5198
- [--request-listing | --withdraw-listing] # public marketplace — opt-in, see: octwin help deploy
5199
- octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
5200
- octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
5201
- octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
5202
- octwin work [recordId] [--queues] [--unrouted] [--json] # inspect the work inbox (worked records) + timelines
5203
- # --queues: per-queue open counts + an UNROUTED warning · --unrouted: only the items in no queue
5204
- octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
5205
- octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
5206
- octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
5207
- octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
5208
- octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
5209
- octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
5210
- octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
5211
- octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
5212
- octwin automation [campaigns] [--json] # the jobs your declarations produced + health, last result each
5213
- octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
5214
- octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
5215
- octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
5216
- octwin performance [--detail] [--json] # the project's business indicators (value/conversion/duration)
5217
- octwin usage [--json] # model calls, tokens and cost (project if pinned, else workspace)
5218
- octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
5219
- octwin test [--dir .] # = validate --remote (the full platform check)
5220
- octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
5221
- octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
5222
-
5223
- Writes — exercise the state your pack creates (each needs the matching :write scope):
5224
- octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
5225
- octwin records tasks | task complete <taskId> [--outcome done|cancelled]
5226
- octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
5227
- octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
5228
- octwin orders transition <ref> --to <status> | refund <ref> --force
5229
- octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
5230
- octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
5231
- octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
5232
- octwin automation run <jobId> | pause <jobId> | resume <jobId> | send <campaignId>
5233
- octwin integrations test <key> # a LIVE call to the connection's health: operation
5234
- octwin integrations retry|cancel|send-now <deliveryId>
5235
- (octwin integrations preflight <key> needs only integrations:read — it makes no call)
5236
-
5237
- Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
5238
- \`octwin chat --as <h>\` calls continue the same conversation; press a rendered
5239
- button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
5240
- Signing in: \`octwin login --url <u>\` prints a link + code to approve in a browser. To pick your own
5241
- scopes instead, mint a token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
5242
- octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
5243
- Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
5257
+ console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
5258
+
5259
+ octwin --version # print the CLI version (+ any upgrade notice)
5260
+ octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
5261
+ octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
5262
+ octwin login --url <platformUrl> # approve in a browser; the CLI collects the token
5263
+ octwin login --url <platformUrl> --token oct_… # or paste a deploy token from the console
5264
+ octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
5265
+ octwin projects [--archived] [--json] # the --project slugs this token can name
5266
+ octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
5267
+ [--request-listing | --withdraw-listing] # public marketplace — opt-in, see: octwin help deploy
5268
+ octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
5269
+ octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
5270
+ octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
5271
+ octwin work [recordId] [--queues] [--unrouted] [--json] # inspect the work inbox (worked records) + timelines
5272
+ # --queues: per-queue open counts + an UNROUTED warning · --unrouted: only the items in no queue
5273
+ octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
5274
+ octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
5275
+ octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
5276
+ octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
5277
+ octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
5278
+ octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
5279
+ octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
5280
+ octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
5281
+ octwin automation [campaigns] [--json] # the jobs your declarations produced + health, last result each
5282
+ octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
5283
+ octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
5284
+ octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
5285
+ octwin performance [--detail] [--json] # the project's business indicators (value/conversion/duration)
5286
+ octwin usage [--json] # model calls, tokens and cost (project if pinned, else workspace)
5287
+ octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
5288
+ octwin test [--dir .] # = validate --remote (the full platform check)
5289
+ octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
5290
+ octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
5291
+
5292
+ Writes — exercise the state your pack creates (each needs the matching :write scope):
5293
+ octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
5294
+ octwin records tasks | task complete <taskId> [--outcome done|cancelled]
5295
+ octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
5296
+ octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
5297
+ octwin orders transition <ref> --to <status> | refund <ref> --force
5298
+ octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
5299
+ octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
5300
+ octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
5301
+ octwin automation run <jobId> | pause <jobId> | resume <jobId> | send <campaignId>
5302
+ octwin integrations test <key> # a LIVE call to the connection's health: operation
5303
+ octwin integrations retry|cancel|send-now <deliveryId>
5304
+ (octwin integrations preflight <key> needs only integrations:read — it makes no call)
5305
+
5306
+ Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
5307
+ \`octwin chat --as <h>\` calls continue the same conversation; press a rendered
5308
+ button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
5309
+ Signing in: \`octwin login --url <u>\` prints a link + code to approve in a browser. To pick your own
5310
+ scopes instead, mint a token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
5311
+ octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
5312
+ Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
5244
5313
  Per-command usage: octwin <command> --help`);
5245
5314
  }
5246
5315
  /** Per-subcommand usage — printed for `octwin <cmd> --help|-h` BEFORE any
5247
5316
  * network/auth work (a --help that 401s is worse than no help at all). */
5248
5317
  const COMMAND_HELP = {
5249
- init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
5318
+ init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
5250
5319
  Scaffold a pure-YAML starter pack into <dir>.`,
5251
- validate: `octwin validate [--dir .] [--remote] [--require-kb] [--strict-primitives]
5252
- Offline structural check, plus two checks driven by the pulled capability
5253
- reference (render-intent fields, primitive arguments). Those two SKIP when the
5254
- reference is missing — the run says so, and --require-kb turns the skip into a
5255
- failure for CI. --remote additionally runs the platform's FULL manifest +
5256
- flow-DSL validation and its flow lint (all errors at once) — same check as deploy.
5257
- --strict-primitives (with --remote) additionally type-checks LITERAL args:
5258
- values against each primitive's declared input schema; expression strings
5320
+ validate: `octwin validate [--dir .] [--remote] [--require-kb] [--strict-primitives]
5321
+ Offline structural check, plus two checks driven by the pulled capability
5322
+ reference (render-intent fields, primitive arguments). Those two SKIP when the
5323
+ reference is missing — the run says so, and --require-kb turns the skip into a
5324
+ failure for CI. --remote additionally runs the platform's FULL manifest +
5325
+ flow-DSL validation and its flow lint (all errors at once) — same check as deploy.
5326
+ --strict-primitives (with --remote) additionally type-checks LITERAL args:
5327
+ values against each primitive's declared input schema; expression strings
5259
5328
  ('$found.id', '{$t(…)}') are always exempt.`,
5260
- login: `octwin login --url <platformUrl> [--token oct_…] [--wait <seconds>] [--json]
5261
- Sign in to a platform and make that url the DEFAULT deploy target for every
5262
- later command.
5263
-
5264
- WITH --token: saves a token you minted yourself (console → Settings → API
5265
- tokens). Unchanged, and still the right answer for CI — there, skip login and
5266
- set PACK_PLATFORM_URL + PACK_TOKEN instead.
5267
-
5268
- WITHOUT --token: prints a link to approve in a browser. A workspace admin opens
5269
- it, checks which machine is asking, and clicks Approve; the CLI then collects a
5270
- 30-day token by itself, so no token is ever pasted between people. The code is
5271
- printed too — it is what the page shows, so you can tell it is your own request.
5272
- Approve only a link you opened because YOU ran this command: a link someone
5273
- sends you would connect THEIR machine to your workspace.
5274
-
5275
- The wait is bounded (90s by default) and RESUMABLE: if nobody has approved yet
5276
- it exits 75 and keeps the request, so running \`octwin login\` again picks up the
5329
+ login: `octwin login --url <platformUrl> [--token oct_…] [--wait <seconds>] [--json]
5330
+ Sign in to a platform and make that url the DEFAULT deploy target for every
5331
+ later command.
5332
+
5333
+ WITH --token: saves a token you minted yourself (console → Settings → API
5334
+ tokens). Unchanged, and still the right answer for CI — there, skip login and
5335
+ set PACK_PLATFORM_URL + PACK_TOKEN instead.
5336
+
5337
+ WITHOUT --token: prints a link to approve in a browser. A workspace admin opens
5338
+ it, checks which machine is asking, and clicks Approve; the CLI then collects a
5339
+ 30-day token by itself, so no token is ever pasted between people. The code is
5340
+ printed too — it is what the page shows, so you can tell it is your own request.
5341
+ Approve only a link you opened because YOU ran this command: a link someone
5342
+ sends you would connect THEIR machine to your workspace.
5343
+
5344
+ The wait is bounded (90s by default) and RESUMABLE: if nobody has approved yet
5345
+ it exits 75 and keeps the request, so running \`octwin login\` again picks up the
5277
5346
  same code. --wait 0 prints the code and returns at once.`,
5278
- whoami: `octwin whoami [--url <url>] [--tenant <slug>]
5347
+ whoami: `octwin whoami [--url <url>] [--tenant <slug>]
5279
5348
  Verify the resolved token authenticates against the tenant.`,
5280
- projects: `octwin projects [--archived] [--json]
5281
- List the workspace's projects — the slugs every --project flag takes, with the
5282
- plan's project cap. --archived includes archived ones. A pack:deploy token
5283
- reaches this (it names a project in every other command).
5284
-
5285
- octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
5286
- Create a project. The URL slug is derived from the name unless --slug pins one.
5287
- --pack installs an ALREADY-published pack; the usual next step is instead
5288
- \`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
5289
-
5290
- octwin projects rm <slug> [--yes]
5291
- HARD delete — the project and everything cascading from it (conversations,
5292
- contacts, records, installs). No undo, and not the same as archiving.
5293
- WITHOUT --yes it only previews what would be destroyed, so the dry run is the
5294
- default. Together these make a disposable end-to-end environment:
5295
- octwin projects create "Scratch" && octwin deploy --project scratch --seed
5296
- octwin chat "hi" --project scratch
5297
- octwin projects rm scratch --yes
5349
+ projects: `octwin projects [--archived] [--json]
5350
+ List the workspace's projects — the slugs every --project flag takes, with the
5351
+ plan's project cap. --archived includes archived ones. A pack:deploy token
5352
+ reaches this (it names a project in every other command).
5353
+
5354
+ octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
5355
+ Create a project. The URL slug is derived from the name unless --slug pins one.
5356
+ --pack installs an ALREADY-published pack; the usual next step is instead
5357
+ \`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
5358
+
5359
+ octwin projects rm <slug> [--yes]
5360
+ HARD delete — the project and everything cascading from it (conversations,
5361
+ contacts, records, installs). No undo, and not the same as archiving.
5362
+ WITHOUT --yes it only previews what would be destroyed, so the dry run is the
5363
+ default. Together these make a disposable end-to-end environment:
5364
+ octwin projects create "Scratch" && octwin deploy --project scratch --seed
5365
+ octwin chat "hi" --project scratch
5366
+ octwin projects rm scratch --yes
5298
5367
  Both verbs need the \`projects:write\` scope — a pack:deploy token does NOT confer it.`,
5299
- deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
5300
- [--request-listing | --withdraw-listing]
5301
- Upload the pack bundle, validate server-side, install onto the project.
5302
- --seed additionally applies the pack's demo seed (streams progress).
5303
-
5304
- A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
5305
- neither asks for a listing nor gives one up. The marketplace flags are opt-in:
5306
-
5307
- --request-listing ask an operator to review this pack for the public marketplace
5308
- (the pre-signup storefront at /packs). Requires 'public: true'
5309
- under 'listing:' in manifest.yaml — the manifest states that the
5310
- pack is a product, the flag is you choosing to ask.
5311
- --withdraw-listing retract the request, including an approved listing.
5312
-
5313
- An approval covers the CONTENT it was made against, so a later deploy that changes the
5368
+ deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
5369
+ [--request-listing | --withdraw-listing]
5370
+ Upload the pack bundle, validate server-side, install onto the project.
5371
+ --seed additionally applies the pack's demo seed (streams progress).
5372
+
5373
+ A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
5374
+ neither asks for a listing nor gives one up. The marketplace flags are opt-in:
5375
+
5376
+ --request-listing ask an operator to review this pack for the public marketplace
5377
+ (the pre-signup storefront at /packs). Requires 'public: true'
5378
+ under 'listing:' in manifest.yaml — the manifest states that the
5379
+ pack is a product, the flag is you choosing to ask.
5380
+ --withdraw-listing retract the request, including an approved listing.
5381
+
5382
+ An approval covers the CONTENT it was made against, so a later deploy that changes the
5314
5383
  pack returns it to the review queue on its own — no flag needed, and the CLI says so.`,
5315
- seed: `octwin seed [--pack <packId>]
5316
- Apply the pack's demo/reference data to the project it is installed on, without
5317
- redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
5318
- and the demo operator topology. Reports what each kind produced.
5319
- Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
5320
- than regenerated, so a second pass costs nothing. --pack is only needed when a
5384
+ seed: `octwin seed [--pack <packId>]
5385
+ Apply the pack's demo/reference data to the project it is installed on, without
5386
+ redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
5387
+ and the demo operator topology. Reports what each kind produced.
5388
+ Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
5389
+ than regenerated, so a second pass costs nothing. --pack is only needed when a
5321
5390
  project somehow runs more than one.`,
5322
- status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
5323
- Show installed vs live version + the flow list for this pack.
5324
- The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
5325
- (a manifest declares a bare name; the owner is attached when you publish). Pass
5326
- <packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
5391
+ status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
5392
+ Show installed vs live version + the flow list for this pack.
5393
+ The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
5394
+ (a manifest declares a bare name; the owner is attached when you publish). Pass
5395
+ <packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
5327
5396
  both print the qualified form.`,
5328
- records: `octwin records [entity] [id] [--limit 50] [--offset n]
5329
- Inspect the pack's XRM data. No args = list entities. Worked records (cases,
5330
- tickets, anything routed to a queue) read best through \`octwin work\`.
5331
-
5332
- WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
5333
- octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
5334
- octwin records patch <recordId> --entity <entity> --set field=value
5335
- octwin records stage <recordId> --to <stage> [--note "..."]
5336
- octwin records note <recordId> "the note text"
5337
- octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
5338
- octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
5339
-
5340
- --set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
5341
- sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
5342
- \`patch\` needs --entity even though it has an id: the route resolves the field
5343
- validator from it. A leading \`create/patch/stage/note/tasks/task\` is read as a
5397
+ records: `octwin records [entity] [id] [--limit 50] [--offset n]
5398
+ Inspect the pack's XRM data. No args = list entities. Worked records (cases,
5399
+ tickets, anything routed to a queue) read best through \`octwin work\`.
5400
+
5401
+ WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
5402
+ octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
5403
+ octwin records patch <recordId> --entity <entity> --set field=value
5404
+ octwin records stage <recordId> --to <stage> [--note "..."]
5405
+ octwin records note <recordId> "the note text"
5406
+ octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
5407
+ octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
5408
+
5409
+ --set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
5410
+ sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
5411
+ \`patch\` needs --entity even though it has an id: the route resolves the field
5412
+ validator from it. A leading \`create/patch/stage/note/tasks/task\` is read as a
5344
5413
  VERB — to list an entity actually named one of those, use \`--entity <name>\`.`,
5345
- work: `octwin work [recordId] [--queues] [--unrouted] [--limit 50] [--offset n] [--json]
5346
- Inspect the work inbox — every entity the pack declares worked (cases, orders
5347
- needing review, applications, …): the inbox, one item + its timeline
5348
- (+ applicable actions), or --queues for queue keys + open counts.
5349
-
5350
- WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
5351
- octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
5352
- octwin work note <recordId> "the note text"
5353
- octwin work stage <recordId> --to <stage> [--note "..."]
5354
- octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
5355
-
5356
- \`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
5357
- lists them with their params. --dry-run previews the customer-facing copy and the
5358
- resulting stage WITHOUT committing (that route needs only \`work:read\`).
5414
+ work: `octwin work [recordId] [--queues] [--unrouted] [--limit 50] [--offset n] [--json]
5415
+ Inspect the work inbox — every entity the pack declares worked (cases, orders
5416
+ needing review, applications, …): the inbox, one item + its timeline
5417
+ (+ applicable actions), or --queues for queue keys + open counts.
5418
+
5419
+ WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
5420
+ octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
5421
+ octwin work note <recordId> "the note text"
5422
+ octwin work stage <recordId> --to <stage> [--note "..."]
5423
+ octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
5424
+
5425
+ \`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
5426
+ lists them with their params. --dry-run previews the customer-facing copy and the
5427
+ resulting stage WITHOUT committing (that route needs only \`work:read\`).
5359
5428
  \`stage\` is the XRM records verb (one transition spelling platform-wide).`,
5360
- logs: `octwin logs [conversationId] [--as <handle>] [--json]
5361
- No id = recent conversations (handle, status, last activity; --as filters).
5362
- With id = the full event timeline including what each turn rendered.
5429
+ logs: `octwin logs [conversationId] [--as <handle>] [--json]
5430
+ No id = recent conversations (handle, status, last activity; --as filters).
5431
+ With id = the full event timeline including what each turn rendered.
5363
5432
  --json = raw events (verbatim payloads).`,
5364
- pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
5365
- Write a DEPLOYED pack's source back to disk — the inverse of deploy.
5366
- A pack pushed with 'octwin deploy' lives on the platform as an artifact the
5367
- runtime serves but nothing hands back, so its only source copy is the machine
5368
- that pushed it. Pull it, fix it, redeploy it.
5369
- Defaults to the version installed on the target project; --version overrides.
5370
- --dir defaults to ./<packId>; a non-empty dir needs --force.
5371
- The pulled dir redeploys where it came from — the target is your saved login.
5433
+ pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
5434
+ Write a DEPLOYED pack's source back to disk — the inverse of deploy.
5435
+ A pack pushed with 'octwin deploy' lives on the platform as an artifact the
5436
+ runtime serves but nothing hands back, so its only source copy is the machine
5437
+ that pushed it. Pull it, fix it, redeploy it.
5438
+ Defaults to the version installed on the target project; --version overrides.
5439
+ --dir defaults to ./<packId>; a non-empty dir needs --force.
5440
+ The pulled dir redeploys where it came from — the target is your saved login.
5372
5441
  You may pull a pack your tenant OWNS (deployed); an operator token pulls any.`,
5373
- chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
5374
- octwin chat --script <file> [--as <handle>] [--json]
5375
- Drive ONE turn through the dev web channel and print every render with its
5376
- tap ids. Same --as handle = same conversation (multi-turn works).
5377
- --tap presses a rendered button/list row instead of sending text.
5378
- --media uploads a local file (or a media id from 'media generate --json') as
5379
- an image/document/audio inbound — any "message" rides as its caption; feeds a
5380
- running media-collect flow (e.g. activate-app).
5381
- --json dumps the raw SSE envelopes for the turn.
5382
-
5383
- --script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
5384
- process over one connection — waiting for each turn to settle before sending
5385
- the next. Use this for any multi-step flow: chaining shell invocations races
5386
- the agent loop, because a turn ends on a quiet gap that can arrive while the
5387
- server is still working (the symptom is placeholder-filled fields or a second
5388
- workflow run). Blank lines and # comments are skipped:
5389
-
5390
- # book an appointment end to end
5391
- احجز موعد
5392
- tap:t:invoke:book-appointment:doctor_id=D1
5393
- media:./licence.jpg | here is my licence
5442
+ chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
5443
+ octwin chat --script <file> [--as <handle>] [--json]
5444
+ Drive ONE turn through the dev web channel and print every render with its
5445
+ tap ids. Same --as handle = same conversation (multi-turn works).
5446
+ --tap presses a rendered button/list row instead of sending text.
5447
+ --media uploads a local file (or a media id from 'media generate --json') as
5448
+ an image/document/audio inbound — any "message" rides as its caption; feeds a
5449
+ running media-collect flow (e.g. activate-app).
5450
+ --json dumps the raw SSE envelopes for the turn.
5451
+
5452
+ --script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
5453
+ process over one connection — waiting for each turn to settle before sending
5454
+ the next. Use this for any multi-step flow: chaining shell invocations races
5455
+ the agent loop, because a turn ends on a quiet gap that can arrive while the
5456
+ server is still working (the symptom is placeholder-filled fields or a second
5457
+ workflow run). Blank lines and # comments are skipped:
5458
+
5459
+ # book an appointment end to end
5460
+ احجز موعد
5461
+ tap:t:invoke:book-appointment:doctor_id=D1
5462
+ media:./licence.jpg | here is my licence
5394
5463
  tap:t:resume:book-appointment:run_id=R1;_ctl_approved=true`,
5395
- media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
5396
- AI-generate an image (needs a media:generate-scoped token), store it as a
5397
- public asset, and print its MEDIA- handle + serve URL. --out downloads the
5398
- bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
5464
+ media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
5465
+ AI-generate an image (needs a media:generate-scoped token), store it as a
5466
+ public asset, and print its MEDIA- handle + serve URL. --out downloads the
5467
+ bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
5399
5468
  bytes }. Pair with 'octwin chat --media' to drive media flows.`,
5400
- agents: `octwin agents [packId::agentId] [--prompt] [--json]
5401
- No args = the roster with each agent's EFFECTIVE model and which layer set it.
5402
- With an agent = every governed setting (model / memory.last_messages /
5403
- working_memory) plus the layer that won — an operator PLATFORM default can
5404
- override what your manifest declares, and this is where you see that.
5405
- --prompt = the exact system prompt the LLM sees for this project (pack
5406
- instructions + platform protocol + any project overlay). Needs agents:read.
5407
- The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
5408
-
5409
- WRITES (need \`agents:write\`):
5410
- octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
5411
- [--enable-tool <toolId>] [--disable-tool <toolId>]
5412
-
5413
- Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
5414
- so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
5469
+ agents: `octwin agents [packId::agentId] [--prompt] [--json]
5470
+ No args = the roster with each agent's EFFECTIVE model and which layer set it.
5471
+ With an agent = every governed setting (model / memory.last_messages /
5472
+ working_memory) plus the layer that won — an operator PLATFORM default can
5473
+ override what your manifest declares, and this is where you see that.
5474
+ --prompt = the exact system prompt the LLM sees for this project (pack
5475
+ instructions + platform protocol + any project overlay). Needs agents:read.
5476
+ The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
5477
+
5478
+ WRITES (need \`agents:write\`):
5479
+ octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
5480
+ [--enable-tool <toolId>] [--disable-tool <toolId>]
5481
+
5482
+ Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
5483
+ so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
5415
5484
  ids refuses --model with a 403 — the platform default governs there.`,
5416
- orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
5417
- No args = the order list (#number, status/payment, total, contact). With a
5418
- reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
5419
- payment_ref, and the allowed status transitions. Needs orders:read + the
5420
- \`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
5421
- so \`pending\` on a gateway-less workspace is expected, not a bug.
5422
-
5423
- WRITES (need \`orders:write\`):
5424
- octwin orders transition <reference_id> --to <status>
5425
- octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
5426
-
5427
- Refund is irreversible and moves money, hence --force. The route answers 200 even
5428
- when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
5429
- on a refusal rather than reporting a refund that never happened. Only a payment in
5485
+ orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
5486
+ No args = the order list (#number, status/payment, total, contact). With a
5487
+ reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
5488
+ payment_ref, and the allowed status transitions. Needs orders:read + the
5489
+ \`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
5490
+ so \`pending\` on a gateway-less workspace is expected, not a bug.
5491
+
5492
+ WRITES (need \`orders:write\`):
5493
+ octwin orders transition <reference_id> --to <status>
5494
+ octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
5495
+
5496
+ Refund is irreversible and moves money, hence --force. The route answers 200 even
5497
+ when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
5498
+ on a refusal rather than reporting a refund that never happened. Only a payment in
5430
5499
  \`captured\` state can be refunded; \`payment_status\` is never settable directly.`,
5431
- analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
5432
- No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
5433
- With an entity = stage-by-stage conversion (default --funnel) over the last 30
5434
- days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
5500
+ analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
5501
+ No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
5502
+ With an entity = stage-by-stage conversion (default --funnel) over the last 30
5503
+ days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
5435
5504
  range-filtered). Needs records:read + a \`view\` grant on \`record.<entity>\`.`,
5436
- catalog: `octwin catalog [--readiness] [--json]
5437
- The commerce \`product\` records + price, availability, stock (null = not
5438
- inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
5439
- Graph checklist (LIVE Graph calls; needs a bound access token). Needs
5440
- catalog:read + the \`catalog\` plan feature.
5441
-
5442
- WRITES (need \`catalog:write\`):
5443
- octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
5444
- octwin catalog stock <retailerId> [--set-on-hand <n>]
5445
-
5446
- \`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
5447
- inventory-tracked (always sellable), which is different from 0. Lowering on_hand
5448
- below the units already reserved for open carts is refused. Creating/deleting
5505
+ catalog: `octwin catalog [--readiness] [--json]
5506
+ The commerce \`product\` records + price, availability, stock (null = not
5507
+ inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
5508
+ Graph checklist (LIVE Graph calls; needs a bound access token). Needs
5509
+ catalog:read + the \`catalog\` plan feature.
5510
+
5511
+ WRITES (need \`catalog:write\`):
5512
+ octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
5513
+ octwin catalog stock <retailerId> [--set-on-hand <n>]
5514
+
5515
+ \`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
5516
+ inventory-tracked (always sellable), which is different from 0. Lowering on_hand
5517
+ below the units already reserved for open carts is refused. Creating/deleting
5449
5518
  products and the Meta catalog binding/sync stay in the console.`,
5450
- scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
5451
- No args = the engine state (bookable resource types, upcoming slots, booked
5452
- seats). --slots <recordId> computes the slots for one bookable resource
5453
- (occupancy included; --days is clamped to 1-31 server-side) — the way to verify
5454
- the availability rules a \`deploy --seed\` created. Needs scheduling:read.
5455
-
5456
- RULES (list needs scheduling:read; add/rm need scheduling:write):
5457
- octwin scheduling rules --resource <resourceRecordId>
5458
- octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
5459
- [--slot-minutes 30] [--capacity 1]
5460
- octwin scheduling rule rm <ruleId>
5461
- octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
5462
- [--start 09:00 --end 13:00]
5463
- octwin scheduling exception rm <exceptionId>
5464
-
5465
- --dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
5519
+ scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
5520
+ No args = the engine state (bookable resource types, upcoming slots, booked
5521
+ seats). --slots <recordId> computes the slots for one bookable resource
5522
+ (occupancy included; --days is clamped to 1-31 server-side) — the way to verify
5523
+ the availability rules a \`deploy --seed\` created. Needs scheduling:read.
5524
+
5525
+ RULES (list needs scheduling:read; add/rm need scheduling:write):
5526
+ octwin scheduling rules --resource <resourceRecordId>
5527
+ octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
5528
+ [--slot-minutes 30] [--capacity 1]
5529
+ octwin scheduling rule rm <ruleId>
5530
+ octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
5531
+ [--start 09:00 --end 13:00]
5532
+ octwin scheduling exception rm <exceptionId>
5533
+
5534
+ --dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
5466
5535
  \`--slots\` is how you check what a rule actually produces.`,
5467
- automation: `octwin automation [campaigns] [--limit n] [--offset n] [--json]
5468
- No args = every job the pack's automation declaration produced, with its status,
5469
- interval and LAST RESULT (matched / acted / errors), under a health line whose
5470
- counts come from SQL rather than from filtering the page — the job list is capped
5471
- server-side, so a client-side count would depend on the cap. Needs automation:read.
5472
-
5473
- Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
5474
- the pack means no jobs, and \`octwin deploy\` is what installs them.
5475
-
5476
- WRITES (automation:write):
5477
- octwin automation run <jobId> # run once, now — prints matched/acted/errors
5478
- octwin automation pause|resume <jobId>
5479
- octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
5480
-
5481
- <jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
5482
- accept only a uuid — the CLI resolves the key for you, and names the keys that do
5483
- exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
5536
+ automation: `octwin automation [campaigns] [--limit n] [--offset n] [--json]
5537
+ No args = every job the pack's automation declaration produced, with its status,
5538
+ interval and LAST RESULT (matched / acted / errors), under a health line whose
5539
+ counts come from SQL rather than from filtering the page — the job list is capped
5540
+ server-side, so a client-side count would depend on the cap. Needs automation:read.
5541
+
5542
+ Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
5543
+ the pack means no jobs, and \`octwin deploy\` is what installs them.
5544
+
5545
+ WRITES (automation:write):
5546
+ octwin automation run <jobId> # run once, now — prints matched/acted/errors
5547
+ octwin automation pause|resume <jobId>
5548
+ octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
5549
+
5550
+ <jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
5551
+ accept only a uuid — the CLI resolves the key for you, and names the keys that do
5552
+ exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
5484
5553
  missing scope: the action is re-checked against the job.`,
5485
- integrations: `octwin integrations [--json]
5486
- What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
5487
- connection that is declared and never configured is the commonest reason an
5488
- integration silently never fires, and neither list alone can show it. Flags the
5489
- gap explicitly. Needs integrations:read.
5490
-
5491
- DIAGNOSE ONE CONNECTION:
5492
- octwin integrations preflight <key> # every check, with a fix hint. Makes NO
5493
- # outbound call — needs only integrations:read
5494
- octwin integrations test <key> # a LIVE call to its health: operation
5495
- # (integrations:write). Exits 1 when it fails.
5496
-
5497
- THE DELIVERY LOG:
5498
- octwin integrations deliveries [--status s] [--operation id] [--limit n]
5499
- octwin integrations deliveries <id> # + the redacted request/response snapshots
5500
- octwin integrations retry|cancel|send-now <id> # integrations:write
5501
- octwin integrations events # INBOUND events (what arrived at your webhook)
5502
-
5503
- retry/cancel answer 409 when the delivery is in the wrong state; the message
5554
+ integrations: `octwin integrations [--json]
5555
+ What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
5556
+ connection that is declared and never configured is the commonest reason an
5557
+ integration silently never fires, and neither list alone can show it. Flags the
5558
+ gap explicitly. Needs integrations:read.
5559
+
5560
+ DIAGNOSE ONE CONNECTION:
5561
+ octwin integrations preflight <key> # every check, with a fix hint. Makes NO
5562
+ # outbound call — needs only integrations:read
5563
+ octwin integrations test <key> # a LIVE call to its health: operation
5564
+ # (integrations:write). Exits 1 when it fails.
5565
+
5566
+ THE DELIVERY LOG:
5567
+ octwin integrations deliveries [--status s] [--operation id] [--limit n]
5568
+ octwin integrations deliveries <id> # + the redacted request/response snapshots
5569
+ octwin integrations retry|cancel|send-now <id> # integrations:write
5570
+ octwin integrations events # INBOUND events (what arrived at your webhook)
5571
+
5572
+ retry/cancel answer 409 when the delivery is in the wrong state; the message
5504
5573
  carries the rule.`,
5505
- journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
5506
- [--stage <stageId>] [--limit n] [--json]
5507
- No args = the journeys the pack declares. With an id, one of six views —
5508
- --funnel (default) stage-by-stage reach and drop-off · --overview entered vs
5509
- converted plus the biggest drop-off · --goals completions, contacts, value and
5510
- p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
5511
- --definition what was DECLARED, unmeasured (the one view that works with no
5512
- traffic). Needs journeys:read.
5513
-
5514
- --stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
5515
- not the funnel's cumulative reached counts).
5516
-
5517
- Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
5518
- entity funnel are the same question about different subjects. Journeys carry RBAC
5519
- on top of the scope, so an empty answer can be a missing \`view\` grant rather
5574
+ journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
5575
+ [--stage <stageId>] [--limit n] [--json]
5576
+ No args = the journeys the pack declares. With an id, one of six views —
5577
+ --funnel (default) stage-by-stage reach and drop-off · --overview entered vs
5578
+ converted plus the biggest drop-off · --goals completions, contacts, value and
5579
+ p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
5580
+ --definition what was DECLARED, unmeasured (the one view that works with no
5581
+ traffic). Needs journeys:read.
5582
+
5583
+ --stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
5584
+ not the funnel's cumulative reached counts).
5585
+
5586
+ Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
5587
+ entity funnel are the same question about different subjects. Journeys carry RBAC
5588
+ on top of the scope, so an empty answer can be a missing \`view\` grant rather
5520
5589
  than missing data — the output says which causes are possible.`,
5521
- performance: `octwin performance [--detail] [--json]
5522
- The project's business indicators — value produced, conversion, duration — each
5523
- with its delta against the previous window and a \`why\` naming the declaration it
5524
- came from. --detail adds the per-indicator breakdown.
5525
-
5526
- Needs records:read, NOT a performance scope (there is none), so a read-only token
5527
- already reaches it. Indicators are DERIVED: a pack that declares no journey goal
5590
+ performance: `octwin performance [--detail] [--json]
5591
+ The project's business indicators — value produced, conversion, duration — each
5592
+ with its delta against the previous window and a \`why\` naming the declaration it
5593
+ came from. --detail adds the per-indicator breakdown.
5594
+
5595
+ Needs records:read, NOT a performance scope (there is none), so a read-only token
5596
+ already reaches it. Indicators are DERIVED: a pack that declares no journey goal
5528
5597
  value and no pipelined entity produces none, which is a different thing from zero.`,
5529
- usage: `octwin usage [--json]
5530
- Model calls, tokens and cost for the resolved scope — project when one is pinned
5531
- or passed with --project, otherwise the whole workspace. Broken down by model,
5532
- kind, agent and channel.
5533
-
5534
- Needs no particular scope: any valid token reaches it.
5535
-
5536
- This is MODEL spend only. WhatsApp/Meta message billing is operator-only and
5598
+ usage: `octwin usage [--json]
5599
+ Model calls, tokens and cost for the resolved scope — project when one is pinned
5600
+ or passed with --project, otherwise the whole workspace. Broken down by model,
5601
+ kind, agent and channel.
5602
+
5603
+ Needs no particular scope: any valid token reaches it.
5604
+
5605
+ This is MODEL spend only. WhatsApp/Meta message billing is operator-only and
5537
5606
  deliberately outside the token scope registry — no API token can read it.`,
5538
- 'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
5539
- Pull the platform capability reference (markdown + JSON catalogs) into
5540
- .octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
5541
- INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
5542
- OUTLINE.md (every heading with its line number).
5543
-
5544
- NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously,
5545
- and this command never sends one. --token is accepted and ignored, so an older
5546
- script that passes it keeps working.
5547
-
5548
- --if-stale poll the platform's content_hash first and skip the download when
5549
- nothing changed. Cheap enough to run at the start of every session.
5550
- --check report only, write nothing. Exit 0 = current, 2 = stale or never
5551
- pulled, 1 = could not tell (offline / no reference served). For
5607
+ 'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
5608
+ Pull the platform capability reference (markdown + JSON catalogs) into
5609
+ .octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
5610
+ INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
5611
+ OUTLINE.md (every heading with its line number).
5612
+
5613
+ NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously,
5614
+ and this command never sends one. --token is accepted and ignored, so an older
5615
+ script that passes it keeps working.
5616
+
5617
+ --if-stale poll the platform's content_hash first and skip the download when
5618
+ nothing changed. Cheap enough to run at the start of every session.
5619
+ --check report only, write nothing. Exit 0 = current, 2 = stale or never
5620
+ pulled, 1 = could not tell (offline / no reference served). For
5552
5621
  scripts and agent loops that want to branch without parsing prose.`,
5553
- test: `octwin test [--dir .]
5622
+ test: `octwin test [--dir .]
5554
5623
  Alias for \`octwin validate --remote\` — the full platform check.`,
5555
- memos: `octwin memos [--all] [--json]
5556
- Read what the platform has told you: a REPLY to a report you sent with
5557
- \`octwin feedback\`, or a NOTICE published to every author (a new capability,
5558
- a deprecation, a breaking change). Bodies are printed in full.
5559
- Reading marks them read, so the reminder stops. --all re-reads history and
5560
- acks nothing. --json to branch on \`severity\`
5624
+ memos: `octwin memos [--all] [--json]
5625
+ Read what the platform has told you: a REPLY to a report you sent with
5626
+ \`octwin feedback\`, or a NOTICE published to every author (a new capability,
5627
+ a deprecation, a breaking change). Bodies are printed in full.
5628
+ Reading marks them read, so the reminder stops. --all re-reads history and
5629
+ acks nothing. --json to branch on \`severity\`
5561
5630
  (info | action_required | breaking).`,
5562
- feedback: `octwin feedback [--dir .]
5563
- Submit this pack's FEEDBACK.md to the platform team.
5564
- The octwin-pack skill writes that file in its last step — findings grouped by
5565
- owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
5566
- you to paste it into a chat.
5567
- Attaches the pack id + version from manifest.yaml, this CLI's version, and the
5568
- content_hash of the capability reference in .octwin/platform-kb/ — triage needs
5569
- the last two to tell "the platform is wrong" from "that was already fixed" or
5631
+ feedback: `octwin feedback [--dir .]
5632
+ Submit this pack's FEEDBACK.md to the platform team.
5633
+ The octwin-pack skill writes that file in its last step — findings grouped by
5634
+ owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
5635
+ you to paste it into a chat.
5636
+ Attaches the pack id + version from manifest.yaml, this CLI's version, and the
5637
+ content_hash of the capability reference in .octwin/platform-kb/ — triage needs
5638
+ the last two to tell "the platform is wrong" from "that was already fixed" or
5570
5639
  "you were reading a stale reference". Needs the \`pack:deploy\` scope.`,
5571
5640
  };
5572
5641
  async function main() {
@@ -58,6 +58,11 @@ export function loadPrimitiveArgSpecs(packDir) {
58
58
  required: Array.isArray(schema.required) ? schema.required : [],
59
59
  open: ap !== undefined && ap !== false,
60
60
  requiresOneOf: Array.isArray(entry.requiresOneOf) ? entry.requiresOneOf : [],
61
+ // Kept only when BOTH halves are present. A pair with no message would print a warning
62
+ // that names two argument keys and says nothing about why they are a trap — which is the
63
+ // sort of warning authors learn to scroll past.
64
+ warnsTogether: (entry.warnsTogether ?? [])
65
+ .filter((w) => Array.isArray(w?.args) && w.args.length > 0 && typeof w?.message === 'string' && w.message.length > 0),
61
66
  });
62
67
  }
63
68
  }
@@ -74,6 +79,10 @@ export function loadPrimitiveArgSpecs(packDir) {
74
79
  * primitive's declared set. Walks the whole document rather than just `steps:`
75
80
  * — a `do:` is legal in a `foreach` body, a `collect` hook, an `outputs:` port
76
81
  * body and `require:`, and a dropped argument is just as invisible there.
82
+ *
83
+ * Warnings come back on their own list rather than mixed into `ArgFinding`. They have a different
84
+ * severity, a different exit code, and a different sentence — folding them together would have
85
+ * meant either failing a build over a warning or hiding an error inside a warning count.
77
86
  */
78
87
  export function findArgViolations(doc, file, specs) {
79
88
  const findings = [];
@@ -107,6 +116,52 @@ export function findArgViolations(doc, file, specs) {
107
116
  walk(doc, []);
108
117
  return findings;
109
118
  }
119
+ /**
120
+ * Walk for `do:` nodes whose `args:` supply every member of a declared `warnsTogether` pair.
121
+ *
122
+ * A separate walk rather than a second output of `findArgViolations`, on purpose: that function
123
+ * skips a primitive whose schema is OPEN (`!spec.open`), because an open schema makes "unknown
124
+ * key" unanswerable. A `warnsTogether` pair is answerable either way — it names two keys that ARE
125
+ * declared — so gating it on closedness would silently drop the check for every passthrough
126
+ * primitive, which is exactly the shape of hole a scan grows without anyone noticing.
127
+ */
128
+ export function findArgWarnings(doc, file, specs) {
129
+ const warnings = [];
130
+ const walk = (node, path) => {
131
+ if (Array.isArray(node)) {
132
+ node.forEach((v, i) => walk(v, [...path, i]));
133
+ return;
134
+ }
135
+ if (!node || typeof node !== 'object')
136
+ return;
137
+ const obj = node;
138
+ if (typeof obj.do === 'string') {
139
+ const spec = specs.get(obj.do);
140
+ if (spec?.warnsTogether.length) {
141
+ const args = (obj.args && typeof obj.args === 'object' && !Array.isArray(obj.args))
142
+ ? obj.args
143
+ : {};
144
+ for (const pair of spec.warnsTogether) {
145
+ // KEY PRESENCE only, matching the platform's own `checkPrimitiveArgs`. The values are
146
+ // almost always expressions (`record_id: '$found.record_id'`), so judging them would
147
+ // mean judging a string — and the trap is about the combination, not the values.
148
+ if (pair.args.every(k => k in args)) {
149
+ warnings.push({ file, primitive: obj.do, path: [...path], args: [...pair.args], message: pair.message });
150
+ }
151
+ }
152
+ }
153
+ }
154
+ for (const [k, v] of Object.entries(obj))
155
+ walk(v, [...path, k]);
156
+ };
157
+ walk(doc, []);
158
+ return warnings;
159
+ }
160
+ /** One line per warning: where, which pair, and the primitive's own reason. */
161
+ export function describeArgWarning(w) {
162
+ const at = w.line ? `${w.file}:${w.line}` : w.file;
163
+ return `${at}: \`${w.primitive}\` passes ${w.args.map(a => `'${a}'`).join(' + ')} together — ${w.message}`;
164
+ }
110
165
  /** One-line human message per finding. */
111
166
  export function describeArgFinding(f) {
112
167
  const parts = [];
@@ -56,6 +56,17 @@ export function reportChecks(checks, io) {
56
56
  io.die(outcome.hint);
57
57
  }
58
58
  // eslint-disable-next-line no-fallthrough -- `die` returns never; the case above cannot exit.
59
+ case 'warnings': {
60
+ // On `log`, not `err`, and the run CONTINUES. A warning that stopped the run, or that
61
+ // arrived on the error stream, would be an error wearing a different word — and the next
62
+ // check is the one the author was actually waiting for.
63
+ const n = outcome.lines.length;
64
+ io.log(`⚠ ${n} ${outcome.noun}${n === 1 ? '' : 's'} — not fatal:`);
65
+ for (const line of outcome.lines)
66
+ io.log(` ⚠ ${line}`);
67
+ io.log(` ${outcome.hint}`);
68
+ break;
69
+ }
59
70
  case 'not-run':
60
71
  notRun.push({ label: check.label, lookup: outcome.lookup });
61
72
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octwin-cli",
3
- "version": "0.8.11",
3
+ "version": "0.8.12",
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": {
@@ -41,6 +41,6 @@
41
41
  "yaml"
42
42
  ],
43
43
  "author": "CEQUENS",
44
- "homepage": "https://www.npmjs.com/package/octwin-cli",
44
+ "homepage": "https://octwin.ai",
45
45
  "license": "MIT"
46
46
  }