octwin-cli 0.8.1 → 0.8.4

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
@@ -1,681 +1,739 @@
1
- # Changelog — octwin-cli
2
-
3
- All notable changes to the `octwin` CLI (npm package [`octwin-cli`](https://www.npmjs.com/package/octwin-cli)).
4
- Format: [Keep a Changelog](https://keepachangelog.com/) — newest first, bucketed into
5
- **Added · Changed · Deprecated · Removed · Fixed · Security**. The platform-wide view lives in the
6
- repo root [`CHANGELOG.md`](../../CHANGELOG.md); this file is the CLI-only cut that ships with the package.
7
-
8
- ## [0.8.1] - 2026-08-27
9
-
10
- ### Fixed
11
- - **Logging in made `platform-kb pull` WORSE.** The command preferred the tenant-scoped route
12
- whenever a token was saved, and that route is guarded by `pack:deploy` so a developer holding a
13
- token without that scope got a **403 on a pull that succeeds with no token at all**. It is the
14
- likeliest token a new developer holds: no console preset could even reach `pack:deploy` before
15
- 2026-08-26. `platform-kb` now always uses the anonymous route and never sends a credential.
16
- `--token` is accepted and ignored so existing scripts keep working.
17
-
18
- The platform serves the *same* bundle from all three of its views its route file states the
19
- payload is identical and tenant-independent so the authed route returned nothing extra and the
20
- credential could only ever subtract. The two reasons the old code gave did not survive checking:
21
- "keeps the author's own instance the source of truth" is vacuous (both routes are the same
22
- instance), and "works against platforms that predate the public rung" is a backward-compatibility
23
- shim this codebase does not carry.
24
- - **The drift nudge now works without a token.** It polled through the same authed route, so an
25
- author inspecting data with a narrow token was told, every time, that it *could not* check —
26
- advice in place of the signal. Both the poll and the apology are gone: the poll is anonymous, so
27
- it simply works.
28
-
29
- ## [0.8.0] - 2026-08-27
30
-
31
- ### Added
32
- - **Five new command families, closing the gap between what a token reaches and what the CLI can
33
- ask.** A **Full CLI access** token spans the whole scope registry, and four of those domains had
34
- no command at all so the reach existed and there was nothing to type.
35
- - `octwin automation [campaigns]` — every job the pack's declaration produced, with status,
36
- interval and last result (matched / acted / errors), under a health line whose counts come from
37
- SQL rather than from filtering the page (the job list is capped server-side, so a client-side
38
- count would depend on the cap). Writes: `run` · `pause` · `resume` · `send <campaignId>`.
39
- - `octwin integrations` what the pack **declares** beside what is actually **configured**, in
40
- one view, because a connection declared and never configured is the commonest reason an
41
- integration silently never fires and neither list alone can show it. Plus
42
- `preflight <key>` (diagnosis, no outbound call, `integrations:read`), `test <key>` (a live call,
43
- `integrations:write`, exits 1 on failure), `deliveries [id]`, `retry|cancel|send-now <id>`,
44
- and `events` for what arrived inbound.
45
- - `octwin journeys [id] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]`
46
- the declared journeys, measured. Same flag grammar as `octwin analytics` on purpose: a journey
47
- funnel and an entity funnel are the same question about different subjects. `--definition` is
48
- the one view that works with no traffic.
49
- - `octwin performance [--detail]` the project's business indicators, each with its delta against
50
- the previous window and the `why` naming the declaration it came from. Needs `records:read`, not
51
- a `performance:*` scope (there is none), so a read-only token already reaches it.
52
- - `octwin usage` model calls, tokens and cost, by model / kind / agent / channel; project when
53
- one is pinned, workspace otherwise. Needs no particular scope. **Model spend only** —
54
- WhatsApp/Meta message billing is operator-only and outside the token scope registry, which the
55
- output says rather than leaving as an absence.
56
- - **`octwin automation run|pause|resume|send` accept the declaration KEY.** The routes take a UUID
57
- and answer a bare *"Malformed identifier in the URL"* 400 for anything else but the key
58
- (`cart_recovery_nudge`) is what the list prints and what the author wrote in their own YAML. The
59
- key is now resolved against the list route, and an unknown one fails naming the keys that exist.
60
-
61
- ### Fixed
62
- - **A body-less write sent `content-type: application/json` and no body, so Fastify answered a bare
63
- `400 Bad Request`.** `apiSend` set the header unconditionally; every caller until now passed an
64
- object, so the branch was dead and the bug latent. The first body-less POST (`automation run`) hit
65
- it immediately. The header is now omitted when there is no body — rather than each caller learning
66
- to pass `{}`, which is the per-site divergence that helper exists to prevent.
67
- - **Four URLs were exempt from the route guard.** `cli-routes.test.ts` reads a template literal in
68
- the first argument position, so `apiGet(base, t)` — a bare identifier — was never checked. That
69
- covered the `scheduling` engine-state and `agents` roster calls, and would have covered the new
70
- `journeys`, `performance` and `usage` ones. All now spelled as literals: the guard checks **62**
71
- URLs, up from 45.
72
-
73
- ## [0.7.3] - 2026-08-26
74
-
75
- ### Changed
76
- - **A marketplace listing request is now OPT-IN: `octwin deploy --request-listing`.** A plain
77
- `octwin deploy` says nothing about the public marketplace it neither asks for a listing nor
78
- gives one up. `--withdraw-listing` retracts, including an approved listing.
79
- **Why:** the request was read from the manifest's `listing.public` on every deploy, and deploying
80
- is how you *test*. 17 of the 23 published packs carry that key, so an edit-deploy-chat loop filed
81
- a review request every time round. Worse, the *absence* of the key meant **retract** — so an
82
- ordinary deploy of a pack whose manifest you had not yet annotated could wipe a listing you
83
- already had. `--request-listing` still requires `listing.public: true` in the manifest (the
84
- manifest states that the pack is a product; the flag is you choosing to ask), and the flag
85
- without the key is a clean error rather than a silent no-op.
86
- - **The listing verdict distinguishes "you submitted this" from "your approval expired".** They are
87
- both `pending` and they mean opposite things to an author. A deploy that changes the content of an
88
- approved pack now says so in those words — an approval covers only the content it was made
89
- against, so it cannot survive an edit.
90
-
91
- ### Added
92
- - **A one-line nudge when your manifest asks and your deploy did not.** A pack carrying
93
- `listing.public: true` that has never been submitted now prints the flag to use, so an opt-in
94
- default cannot turn into waiting for a review nobody requested.
95
-
96
- ### Fixed
97
- - **The "newer CLI" and "KB drifted" notices now print when a command FAILS.** All three nudges
98
- (CLI upgrade · KB drift · unread memos) were awaited at the very end of `main()`, which silently
99
- meant *only on success* `die()` is a hard exit, so a failed deploy, an auth error or a bad
100
- manifest printed none of them. That is backwards: a stale capability reference is a leading cause
101
- of the failure an author is staring at, so the run that most needed the nudge was the only one
102
- that never got it. They are now computed before the command and printed however it ends.
103
- - **The CLI-upgrade notice is no longer hidden from piped output.** It began with
104
- `if (!process.stdout.isTTY) return`, so every agent-driven and CI run the readers that cannot
105
- notice an outdated CLI on their own was the one audience never told. Its two siblings had
106
- already dropped that gate for exactly this reason; now all three agree. Still skipped under
107
- `npx`, where there is nothing to upgrade.
108
- - **`octwin login` now reports KB drift too.** It is networked and usually the first command of a
109
- session, i.e. the best possible moment to say the reference moved, and it was excluded.
110
- - **A memo poll can no longer fail the command it was annotating.** It went through `apiGet`
111
- `fetchOrDie`, whose job is to `die` on a network failure so an unreachable platform turned an
112
- observer into a command failure. It now uses a plain, timed, fail-silent fetch.
113
- - **A failing command reports its real exit code again on Windows.** Moving the polls before the
114
- command made `process.exit()` abort with `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)`
115
- and return **127** instead of 1 so CI saw neither success nor the documented failure code. The
116
- CLI now unwinds through a `CliExit` and lets the event loop drain, which is the only teardown
117
- that is clean once the polls have opened sockets. `--check`'s exit 2 and the streaming commands
118
- were re-verified.
119
-
120
- ## [0.7.2] - 2026-08-20
121
-
122
- ### Fixed
123
- - **`octwin feedback` now tells you what is already waiting, on stdout.** It discarded the submit
124
- response entirely — destructuring `{ status, json }`, checking the status and never reading the
125
- body then printed *"a reply arrives as a memo, this CLI will tell you when one is waiting"*. The
126
- platform started returning the unread counts on that response (2026-08-20), so the answer was
127
- there to print and was being thrown away.
128
- **Why it matters more than it sounds:** `notifyIfMemosWaiting` already covers this command and was
129
- firing correctly but it writes to **stderr**, and the authors driving this CLI are agents reading
130
- stdout. The one signal that mattered was addressed to a channel nobody was listening on. On
131
- 2026-08-19 an author submitted two reports two hours after three replies were published and
132
- re-reported five findings those replies had already closed. Now the count prints in the same block
133
- as the acknowledgement, naming the command, and says outright when a reply may already answer what
134
- you just sent. Against an older platform the field is absent and the previous line stands.
135
-
136
- ## [0.7.1] - 2026-08-19
137
-
138
- ### Fixed
139
- - **`work decide` now says when an action moves nothing.** `to_stage` is optional — an action may
140
- record a decision without a transition but both outputs communicated that by omission: `--dry-run`
141
- left `to_stage: null` sitting among twenty other keys, and a real apply printed a bare
142
- `✓ Applied '<action>'`. An author read both, shipped `request_more_info` with no `to_stage`, and
143
- lost the `awaiting_customer` their whole customer-reply capture branched on their words: *"Nothing
144
- about this failed loudly; it just quietly never did the one thing the whole feature depends on."*
145
- The preview now states `→ does NOT change the status (this action only records a decision)` and the
146
- apply says `status unchanged (the action declares no to_stage)`. The console's decision panel got the
147
- same sentence in the same change.
148
-
149
- ## [0.7.0] - 2026-08-18
150
-
151
- ### Added
152
- - **`octwin memos` the channel answers now.** `octwin feedback` was one-way: an author reported and
153
- heard nothing, ever. Two kinds arrive here, both printed in full — a **reply** to a report you sent,
154
- and a **notice** published to every author (a new capability, a deprecation, a breaking change) —
155
- each carrying `info` / `action_required` / `breaking`. Reading marks them read; `--all` re-reads
156
- history and acks nothing (also the escape hatch if a piped `--json` consumer died halfway).
157
- - **Every networked command now says when a memo is waiting**, one line to stderr, always naming the
158
- command to run. It rides work that was already networked, never throws, and is **deliberately not
159
- TTY-gated** the same reasoning as the KB drift nudge, whose docblock says it plainly: the primary
160
- reader of this CLI is an authoring agent whose output is piped, and gating on `isTTY` means the one
161
- reader that cannot notice on its own is the only one never told. Unlike that check there is no local
162
- marker file: read state is per-workspace on the platform, so an agent in a fresh container still
163
- learns about an unread reply.
164
-
165
- Why it matters, measured: in August 2026 four reports sat unread for nine days, and one author
166
- replaced every `$t()` call with hardcoded English across all 16 flows of a production pack to work
167
- around a bug that had been fixed two days earlier. Nothing could tell them.
168
-
169
- ## [0.6.1] - 2026-08-18
170
-
171
- ### Fixed
172
- - **`octwin status` worked for no pack at all, and had not since 2026-08-01.** It sent the manifest's
173
- **bare** id (`clinic`) to a route that requires the qualified `<owner>.<name>` and a manifest
174
- cannot legally declare the qualified form, because the owner segment is attached at publish time
175
- from the authenticated publisher (both the platform and this CLI reject a `.` in it). So the route
176
- answered 404 for every pack, every time. It now resolves the owner from `whoami`'s `tenant_slug`,
177
- and takes an explicit `octwin status <packId>` when you already know it (from `octwin agents` or
178
- `octwin projects`, both of which print qualified ids). Reported by two pack authors, who lost time
179
- to a command that said their pack was missing seconds after a deploy the platform had accepted.
180
- - **A 404 from `octwin status` no longer always reads "not installed".** That one branch covered four
181
- different failures a malformed pack id, an unknown tenant, an unknown/pinned-elsewhere project,
182
- and the genuine no-active-install so the message named the *least* likely cause and sent an author
183
- chasing a deploy that had already succeeded. The route says which; the CLI now relays it, and adds
184
- the "run `octwin deploy` first" hint only to the case it fits.
185
- - **`octwin logs` always reported "No conversations"**, including immediately after an `octwin chat`
186
- that had plainly worked. It read `conversations` off the response; the platform moved that endpoint
187
- to the shared page envelope on 2026-08-09 and answers `rows`, which every other list command here
188
- already reads. Both authors filed this as replica lag or a cache gap nothing lagged, the payload
189
- was simply never read. `octwin logs <conversationId>` was unaffected.
190
-
191
- ## [0.6.0] - 2026-08-09
192
-
193
- ### Changed
194
- - **`octwin cases` is now `octwin work`, and it works again.** The platform's work-inbox
195
- consolidation (2026-08-08) replaced the case-only surface with `/work` over every entity a pack
196
- declares worked, and renamed the scopes to `work:read`/`work:write` — which left every `octwin
197
- cases` call a silent 404 in a published CLI. The command family is renamed to match the platform:
198
- `octwin work [recordId] [--queues]`, with writes `assign` / `note` / `decide` (the declared
199
- operator actions, `--dry-run` previews) and `stage <id> --to <s>` riding the one platform-wide
200
- stage verb (`POST …/xrm/records/:id/stage`, needs `records:write`). `transition` is gone with the
201
- route it called.
202
- - **`octwin orders transition` rides the same stage verb.** `POST …/orders/:ref/transition` was
203
- retired server-side in the same consolidation; the CLI now resolves the order's record id from the
204
- detail read and moves the stage through `…/xrm/records/:id/stage` (scope: `records:write`).
205
-
206
- ### Added
207
- - **A route guard so this cannot rot silently again**: `src/lib/cli-routes.test.ts` extracts every
208
- `/api/self/**` URL the CLI builds and asserts it (method included) against the platform's dumped
209
- route table — the gap that let both breakages above ship.
210
-
211
- ## [0.5.1] - 2026-08-01
212
-
213
- ### Fixed
214
- - **`validate --remote` blamed the platform's version for a bad `--project`.** Every 404 from
215
- `POST /api/self/p/packs/validate` printed *"this platform has no /packs/validate endpoint yet
216
- (older version)"*. That was accurate when the route resolved nothing, but it now resolves the
217
- tenant and project **before** validating and an unknown project, or one outside a token's pin,
218
- answers 404 by design. So a typo in `--project` sent you looking for a version mismatch instead of
219
- at the flag. The two cases are now told apart by the response **body**, not the status: a missing
220
- route is Fastify's `error: 'Not Found'` and keeps the old wording, while the platform's own
221
- `project '<slug>' not found under tenant` is printed as-is, with the flags to check.
222
-
223
- ## [0.5.0] - 2026-08-01
224
-
225
- ### Added
226
- - **`octwin seed [--pack <packId>]`** — apply the pack's demo/reference data to the project it is
227
- installed on, without redeploying: `xrm.yaml` `demo:` records + scheduling availability, the
228
- commerce catalog, and the demo operator topology. Reports what **each kind** produced.
229
- Previously seeding was reachable only as `deploy --seed`, because the platform's seed endpoint was
230
- keyed on an install id, guarded platform-admin, and carried no tenant/project segments so the
231
- `/api/self/**` surface could not reach it and a `pack:deploy` token never could. Re-seeding meant
232
- a full redeploy or asking an operator. Idempotent and cheap to re-run: records upsert, and
233
- existing media is REUSED rather than regenerated, so a second pass reports zero images.
234
-
235
- ## [0.4.0] - 2026-08-01
236
-
237
- ### Added
238
- - **`octwin projects create` / `octwin projects rm`** a **disposable end-to-end environment** from
239
- the CLI, which is what marketplace developers have been missing. `octwin deploy` has always needed
240
- a project that already existed and the CLI could only *list* them, so standing up a throwaway
241
- full deployment meant opening the console or asking an operator. Now:
242
- ```bash
243
- octwin projects create "Scratch" # slug `scratch`
244
- octwin deploy --project scratch --seed # publish + install + demo data
245
- octwin chat "hi" --project scratch # talk to it (--script for a whole conversation)
246
- octwin projects rm scratch --yes # throw it away
247
- ```
248
- A throwaway environment is deliberately **not a special kind of thing** — it is an ordinary
249
- project in your own workspace, so it inherits your plan, entitlements, RBAC and teardown with no
250
- separate lifecycle. `create` derives the URL slug from the name (`--slug` pins one, `--pack`
251
- installs an already-published pack). `rm` is a HARD delete — the project and everything cascading
252
- from it, no undo, not the same as archiving — so **without `--yes` it only prints what would be
253
- destroyed**, making the dry run the default; the impact list is derived from `pg_constraint`, the
254
- same payload the console's confirm dialog renders. Both need `projects:write`, which a
255
- `pack:deploy` token does **not** confer the CLI names that scope in the 403 hint.
256
-
257
- ## [0.3.0] - 2026-07-31
258
-
259
- ### Added
260
- - **`octwin projects`** the `--project <slug>` values your token can actually name, with the plan's
261
- project cap. Every project-scoped command takes that flag and nothing printed the list, so an
262
- author whose token was not pinned had to guess, and a wrong guess 404s identically to a project
263
- that exists but has no install. `--archived` includes archived projects; `--json` for raw.
264
- A `pack:deploy` token reaches it: the platform route was widened to accept `pack:deploy` alongside
265
- `projects:read`, because `pack:deploy` is `access: 'special'` and confers nothing else the
266
- command would have 403ed for exactly the person it exists for.
267
- - **`octwin validate --require-kb`** turn a skipped KB-driven check into a failure, for CI.
268
-
269
- ### Fixed
270
- - **`octwin validate` printed ✓ with two of its three checks switched off.** The render-intent and
271
- primitive-argument checks read `<packDir>/.octwin/platform-kb/`, joined onto the pack directory
272
- EXACTLY so an author who pulled once at a repo root and then worked in `packs/<name>/` had both
273
- checks silently disabled, and the ✓ printed above them unconditionally while the per-check ✓s lived
274
- inside the `if`s. An entire batch of platform-backlog entries reached production through that hole.
275
- Three fixes: the KB is now found by **walking up** (one pull at a repo root covers every pack under
276
- it, `src/lib/kb-path.ts`); the readers report **three states** absent, malformed, ok so a
277
- half-written pull no longer reads as "you haven't pulled yet"; and a skip is **announced on the
278
- last line printed**, because a caveat above a does not reach a reader who stopped at the ✓.
279
- Warn-and-exit-0 stays the default (the KB is a gitignored cache and `platform-kb pull` needs
280
- `pack:deploy`, so a hard failure would break a fresh clone) — the defect was the silence, not the skip.
281
- - **`octwin status` said "live and current" for a pack that could not receive a message.** Every field
282
- it printed was process-global: `registered` is true for a pack ANY project on that instance loaded.
283
- Dispatch is per-project and takes the oldest ACTIVE install, ignoring the rest. The route now
284
- filters `status = 'active'` (an archived install used to report live) and compares against the real
285
- dispatch target, so status can say plainly that another pack owns this project's messages.
286
- - **`octwin validate --remote --project does-not-exist` answered `{ ok: true }`** while `deploy` with
287
- the same flag 404ed. The platform route resolves tenant and project before validating now.
288
-
289
- ### Changed
290
- - **`octwin validate --remote` also runs the platform's flow LINT.** It used to run only at pack load,
291
- so a finding of severity `error` an `assign:` whose value is a quoted literal, a `$t()` key with
292
- no namespace — passed validate and failed the deploy. Offline `validate` cannot do this: the lint
293
- needs a template-expanded `FlowDef`, which only the server builds.
294
- - **`record_aggregate` / `record_group` now REQUIRE a scope** (`contact_id` or `all: true`) — a
295
- platform contract change the offline arg check replays from the KB. Re-pull after upgrading.
296
-
297
- ## [0.2.0] - 2026-07-30
298
-
299
- ### Fixed
300
- - **`records`, `cases`, `orders` and `catalog` printed a correct total and ZERO rows.** The platform
301
- collapsed its eight per-domain list envelopes into one `Page<T>` whose collection key is `rows`;
302
- the CLI still read `json.records` / `.cases` / `.orders` / `.products` behind a `?? []`, so the
303
- fallback turned a key rename into an empty list instead of an error. `octwin records doctor`
304
- reported *"5 record(s)"* and then *"(none mint a `records:read` token …)"*, blaming the user's
305
- credentials. All five readers now go through one `readPage` helper
306
- (`src/lib/page.ts`) with a test pinned to the real envelope a hand-copied key name in an
307
- untested CLI is what shipped this, so the fix is the single reader, not five edits.
308
- - **`--flag=value` silently did nothing.** `--limit=5` became the key `limit=5` and the command ran
309
- with its default. Now parsed; `--set`/`--param` also accept repetition.
310
- - **`octwin catalog` always printed `Products: 0`**it counted the page, and the route's `total`
311
- had never been read.
312
-
313
- ### Added
314
- - **Write commands — a pack author can now exercise the state their pack creates, not just read it.**
315
- Every one needs the matching `:write` scope; a leading verb (`create`/`note`/`transition`/…) is
316
- what distinguishes a write from the read that shares its command.
317
- - `records create|patch|stage|note`, `records tasks`, `records task complete`
318
- - `cases assign|note|transition|decide``decide --dry-run` previews the customer-facing copy and
319
- resulting status **without committing** (that route needs only `cases:read`)
320
- - `orders transition|refund` · `catalog availability|stock` · `agents set`
321
- - `scheduling rules|rule add|rule rm|exception add|exception rm`
322
- - **`octwin feedback`** submits the pack's `FEEDBACK.md` to the platform team, attaching the pack
323
- version, this CLI's version and the `content_hash` of the capability reference you pulled — the two
324
- facts that separate a real platform gap from something already fixed or a stale KB. Replaces
325
- "hand the file to the platform team".
326
- - **`--offset` on every list**, and a line saying when a list was truncated and what to run next.
327
- `has_more` was on every response and no command surfaced it, so a partial list looked complete.
328
-
329
- ### Changed
330
- - `orders refund` **exits non-zero when the payment gateway refuses.** The route answers `200` with
331
- the verdict in `gateway`, so reporting the HTTP status would tell an operator money moved when it
332
- did not.
333
- - A 403 on a record or case write now says it may be an **RBAC grant gap** rather than a missing
334
- scope: the scope is checked on the token, then the verb is re-checked against that specific row.
335
-
336
- ## [0.1.21] - 2026-07-30
337
-
338
- > **Never published to npm.** Bumped in-repo, then superseded hours later by 0.2.0 everything
339
- > below ships as part of that release. Kept as its own section because the changes are distinct.
340
-
341
- ### Added
342
- - **`deploy` and `status` report the marketplace-listing verdict.** A manifest carrying
343
- `listing.public: true` put the pack into an operator review queue, and the state existed only in
344
- the console so an author working from the CLI got no acknowledgement that the request had
345
- registered, and never saw a rejection note (which the platform *requires* precisely because it is
346
- their only feedback). Both commands now print it via `printPublicListing`, and stay silent for a
347
- pack that never asked. `status` also distinguishes *approved* from *actually public*: an approval
348
- pins the content sha it reviewed, so any edit returns the pack to the queue on its own.
349
- - **`octwin pull` is discoverable.** It was implemented and answered `--help`, but appeared in
350
- neither `octwin help` nor the README the one command that recovers a deployed pack's only source
351
- copy was invisible unless you already knew its name. Also added to the scope-requirement table, so
352
- a 403 names `pack:deploy` instead of printing the generic hint.
353
-
354
- ### Fixed
355
- - **The capability-reference drift check no longer fails silently on a narrow token.** The `?meta=1`
356
- poll needs `pack:deploy`, but it rides on every networked command — so an author inspecting data
357
- with a `records:read`-only token got no drift signal at all, and a stale reference is exactly what
358
- leads to inventing a primitive from memory. A 401/403 there now says so once.
359
- - **README: casework is declared in `worklist.yaml`, not a `cases.yaml`.** That grammar was retired;
360
- following the README produced a pack whose casework silently never loaded. `octwin media generate`
361
- was also missing from the command table.
362
- - **`records` no longer blames the plan for a route the plan does not gate.** `GET /xrm/entities`
363
- carries only the scope guard, so a 403 on the entity list wrongly cited the `records` plan feature.
364
-
365
- ### Removed
366
- - **Unreachable rename branches in `init`.** The agent-id and `main`-flow renames sat behind
367
- `--agent` / `--flow` options the CLI never parsed, against a template that ships no `main` flow —
368
- both branches could never fire, and their patterns would have matched nothing if they had.
369
-
370
- ## [0.1.20] - 2026-07-29
371
-
372
- ### Added
373
- - **`octwin validate` catches primitive arguments the platform would drop offline, no token.** A
374
- primitive takes the arguments it declares and silently ignored the rest, so an invented or
375
- misspelled `args:` key deployed clean, ran clean, and simply did nothing: `record_list args: {
376
- order: }` (the argument is `sort`) never ordered, and `booking_cancel args: { booking_record_id:
377
- }` meant the REQUIRED `record_id` never arrived, so cancelling just failed. Validate now replays
378
- the platform's contract from the pulled KB (`primitives/*.json` `inputSchema`) and names the bad
379
- argument, what the primitive takes, and any required argument that is missing. Scanning the 21
380
- shipped marketplace packs found **ten** such errors across five packs. Keys are checked, values are
381
- not an `args:` value is normally an expression string resolved at runtime.
382
- [`args-check.ts`](src/lib/args-check.ts).
383
- - Silently skipped when the KB has not been pulled yet, like the render-intent check beside it.
384
- - It cannot see inside a `use:` template body (expanding one is the platform's job) `octwin
385
- validate --remote` covers that case, and now also reports unreachable `$bind.<path>` reads and
386
- `outputs:` port typos, which have no offline equivalent.
387
-
388
- ## [0.1.19] - 2026-07-29
389
-
390
- ### Added
391
- - **`octwin validate` catches render-intent fields the platform would drop offline, no token.** Each
392
- render intent takes a fixed field set; a key outside it used to be swallowed at load and dropped at
393
- render, with no error anywhere — just a card missing what the author wrote. Validate now replays the
394
- platform's own contract from the pulled KB (`render-intents/*.json` → `allowed_keys`) and names the
395
- bad field, the intent, and what is allowed. Scanning the 21 shipped marketplace packs found two real
396
- instances in 223 intents: a `text_card` carrying `buttons` (the fallback's only escape button never
397
- rendered) and a `list_picker` carrying a non-existent `group_by` (a whole mapper step feeding a
398
- fabricated parameter). Silently skipped when the KB has not been pulled yet.
399
-
400
- ### Changed
401
- - **`octwin platform-kb pull` now explodes the reference and writes an `INDEX.md`.** A pull used to
402
- write ~787 KB across 34 flat files about 225k tokens, more than the pack being authored — so
403
- finding one primitive meant reading a 122 KB catalog. Catalogs the platform describes as
404
- enumerable are now written **one file per entry** (`primitives/record_list.json`,
405
- `render-intents/carousel.json`, `declarations/xrm.json`, …), with `INDEX.md` mapping every doc and
406
- all 171 entries to its file with a one-line summary. Reading one primitive went from ~38k tokens to
407
- ~700. The layout comes from the platform (an `entries` descriptor on the bundle index) rather than
408
- being hardcoded here, so this CLI keeps working against any platform version and a platform that
409
- adds a catalog needs no CLI release. The pulled directory is cleared each pull, so a withdrawn
410
- capability cannot linger as a file the authoring agent reads as current its *contents* are
411
- cleared rather than the directory itself, since on Windows a directory that is any shell's working
412
- directory cannot be removed.
413
-
414
- ## [0.1.18] - 2026-07-29
415
-
416
- ### Fixed
417
- - **The bundle collector and the platform's repo import disagreed about what a pack directory
418
- contains.** `collectBundleFiles` had its own rule and kept three things the operator's GitHub
419
- import drops: `*.ts`/`*.tsx`, `__snapshots__/` and `*.example`. Because the server rejects a
420
- `.ts` outright, the same directory imported cleanly from GitHub and failed to deploy from
421
- disk with `'…': executable code is not allowed` an error blaming a file that was never
422
- meant to ship (a snapshot test beside a flow, or the documented `xrm.yaml.example` template).
423
- The rule now lives in a vendored `lib/pack-source.ts` beside the vendored validator, and a
424
- parity test in the platform drives both copies over one table.
425
-
426
- ### Removed
427
- - **`pack.json` is gone — the saved login IS the deploy target.** The file had eroded to a single
428
- field, `platform_url`, which `octwin login --url … --token …` already states; `tenant`/`project`
429
- became optional overrides once deploy tokens started carrying their own tenant and project pin.
430
- So it was a second home for a fact `login` knew, and it cost real things: a machine-specific file
431
- committed into every pack repo, a four-link config-precedence chain to document, and an
432
- exclusion rule duplicated in the bundler and the server (which had already drifted apart).
433
- `octwin init` and `octwin pull` no longer write one, and nothing reads one. **A pack directory is
434
- now pack content and nothing else** — the same repo deploys from any machine, and a pulled pack
435
- redeploys with no flags.
436
-
437
- ### Changed
438
- - **`octwin login` now sets the default deploy target**, not just the token: it writes a
439
- `default_url` key alongside the token in `~/.octwin/credentials.json`. Every command resolves
440
- **flags → env (`PACK_PLATFORM_URL`/`PACK_TENANT`/`PACK_PROJECT`/`PACK_TOKEN`) → saved login**.
441
- An existing credentials file simply has no `default_url` until the next `octwin login` which is
442
- exactly what the "no platform url" error now tells you to run. CI is unaffected (it passes
443
- `PACK_PLATFORM_URL` + `PACK_TOKEN` as env and never had a `pack.json` that file is retired).
444
-
445
- ## [0.1.17] - 2026-07-27
446
-
447
- ### Added
448
- - **`octwin pull <packId>` — write a DEPLOYED pack's source back to disk.** The inverse of
449
- `deploy`. A pack pushed with the CLI lived on the platform as an artifact the runtime served
450
- but nothing could hand back, so its only source copy was the machine that pushed it. Pull it,
451
- fix it, redeploy it. Writes a `pack.json` (retired in 0.1.18 the saved login is now the
452
- target) so the pulled directory redeploys where it came from; `--version` picks a version,
453
- `--force` overwrites a non-empty dir. You may pull a pack
454
- your tenant OWNS (an operator token pulls any) a pack you merely installed is not yours to
455
- read.
456
- - **`octwin chat --script <file>` drive a whole conversation from one invocation.** One turn per
457
- line, in order, in a single process over a single SSE connection, waiting for each turn to settle
458
- before sending the next. Blank lines and `#` comments are skipped; `tap:<id>` presses a rendered
459
- row/button (the rest of the line is kept verbatim, since a tap id is itself colon-delimited),
460
- `media:<path>` uploads a file, and `media:<path> | caption` attaches one.
461
-
462
- This exists because **chaining invocations races the agent loop.** A turn ends on a quiet gap
463
- (`TURN_SETTLE_MS`), which can arrive while the server-side loop is still running — so
464
- `chat A && chat B` lets B's inbound land mid-turn, and the agent fills required fields with
465
- placeholder text (`service_id: "service-id"`) or starts a second workflow run. Those look exactly
466
- like flow bugs and cost a field author three separate investigations. The one-turn-per-invocation
467
- rule is now stated in `chat --help` and the header usage as well.
468
-
469
- ### Changed
470
- - **`octwin deploy` reports the suspended runs it invalidated.** A redeploy rebuilds the pack's
471
- tools, and in-flight flow runs live in those tools — so every suspended conversation is dropped.
472
- The deploy prints `ⓘ N suspended run(s) invalidated` when N > 0, instead of leaving the author to
473
- discover it as a `workflow_resume_stale` on the next tap.
474
-
475
- ## [0.1.16] - 2026-07-26
476
-
477
- ### Added
478
- - **A pack can ship committed images.** `jpg`/`jpeg`/`png`/`webp`/`gif`/`pdf` files in your pack
479
- directory now travel as a separate `blobs` half of the bundle (base64 on the wire, `bytea` in
480
- storage) instead of being rejected as "not an allowed pack file type". Declare each one under the
481
- manifest's `static_assets: [{ file, key }]` and reference it from `config:` with the
482
- `$pack_asset:<key>` sentinel the platform uploads it to the media system at install and the
483
- sentinel resolves to the served URL. `octwin validate` reports the image count alongside the file
484
- count.
485
- - **The size ceilings fail locally, before upload** 2 MB per file, 32 MB of binary per pack,
486
- mirroring the server. `svg` stays rejected: it is script-capable and these assets are served to
487
- browsers.
488
-
489
- ### Fixed
490
- - **Committed images were silently corrupted.** `collectBundleFiles` read *every* file with
491
- `readFileSync(full, 'utf8')`, so a JPEG went through a lossy UTF-8 decode and arrived mangled
492
- server-side a deploy that "succeeded" and rendered a broken image. Binary files now split off
493
- into `blobs` and keep their bytes.
494
- - **`octwin status` reported `loaded=(none)` for every pack, always.** It printed a `loaded_version`
495
- field that stopped existing when reload moved to content-sha keying, which also meant the
496
- version-drift warning underneath it could never fire. It now prints the **content sha** the
497
- instance has loaded and the one the catalog holds (with the artifact's `origin`), and warns off the
498
- platform's own `up_to_date` flag. The sha is the more useful fact anyway: re-publishing the *same*
499
- version changes it, which is exactly the author's inner loop. A pack that is live and current but
500
- **withdrawn** from the catalog now says so, rather than reporting a clean bill of health.
501
-
502
- ## [0.1.15] - 2026-07-25
503
-
504
- ### Added
505
- - **`octwin agents [packId::agentId] [--prompt]` — see what your agent actually runs as.** The platform
506
- gained *governed* agent settings: an operator platform default can override the `default_model`
507
- (and history window / working-memory toggle) your manifest declares, and that override is invisible
508
- from the pack source. The command prints each governed setting's **effective** value, **which layer
509
- won** (project override platform default pack manifest), and warns explicitly when your pack's
510
- declared value is *not* in force. `--prompt` prints the exact system prompt the LLM sees for this
511
- project (pack instructions + platform protocol + any project overlay).
512
- - **`octwin orders [reference_id]` the commerce a conversation produced.** The list (number,
513
- status/payment, total, contact) or one order with its line items, the
514
- subtotal/tax/shipping/discount/total breakdown, `payment_ref`, and the allowed transitions. On a
515
- `pending`/`none` payment it explains *why that's expected*: the forward payment lifecycle is
516
- **webhook-owned** (not patchable), and the default gateway-less `manual` driver makes
517
- `payment_request` take its `empty` port so the flow should confirm pay-on-delivery, not error.
518
- - **`octwin analytics [entity]` stage conversion for any pipelined entity.** The funnel engine that
519
- powered journeys is now generic, so any entity declared with a `pipeline:` charts conversion +
520
- drop-off (`--overview` / `--milestones` / `--trends` / `--cost`, and `--stage <id>` for the records
521
- currently at a stage). An empty result names **both** causes the platform folds into one response
522
- no `pipeline:`, or no `view` grant on `record.<entity>`.
523
- - **`octwin catalog [--readiness]`** commerce products with price, availability and stock
524
- (`untracked` when the SKU isn't inventory-tracked), plus the WhatsApp catalog binding.
525
- `--readiness` runs the Meta Graph checklist with each failing item's suggested fix.
526
- - **`octwin scheduling [--slots <resourceRecordId>]`** — the engine state (bookable resource types,
527
- upcoming slots, booked seats) or the computed slots for one resource. This is how you verify the
528
- availability rules a `deploy --seed` created, which 0.1.14 could create but not read back.
529
-
530
- ### Changed
531
- - **Auth failures now name the scope the command needs.** Every 401/403 appends the required scope
532
- (and the plan feature, where the route is entitlement-gated) from one client-side mirror of the
533
- platform's scope registry — replacing the per-command hardcoded strings, several of which named no
534
- scope at all. It also spells out that `pack:deploy` / `media:generate` are direct-grant only, so a
535
- `tenant:admin` preset token does **not** confer them (the most common "but my token is admin" dead end).
536
-
537
- ## [0.1.14] - 2026-07-23
538
-
539
- ### Fixed
540
- - **A failed demo seed no longer reports a clean `✓`.** `deploy` softens non-fatal
541
- install steps (e.g. a demo-seed row) to warning frames, but the summary still printed
542
- `✓ Deployed` and exited 0 — so a deploy that seeded ZERO records read as success (the
543
- false-✓ trap). `deploy` now collects those step errors, prints `⚠ Deployed with N
544
- warning(s) data may be incomplete` with each message, and exits non-zero so CI /
545
- a `deploy && chat` chain catches it.
546
-
547
- ### Added
548
- - **`login` echoes what the token reaches.** After saving, `octwin login` calls
549
- `/api/self/t/whoami` and prints the workspace, project pin, and scopes so a fresh
550
- token self-identifies without a second `octwin whoami` (best-effort; a network failure
551
- never fails the save).
552
- - **`validate --remote` surfaces warnings + the data-store trap.** The remote validate now
553
- renders advisory `⚠` warnings (which don't block deploy) and, for a pack declaring
554
- `required_adapters: [data-store]`, reports the missing-data-store error the deploy would
555
- have 409'd on plus a warning that an `xrm.yaml` pack usually shouldn't declare that
556
- adapter at all. It also now catches (offline) a list-form `entities:` block and a
557
- `localized: true` field seeded with a bare string traps that previously surfaced only
558
- mid-seed at deploy.
559
-
560
- ## [0.1.13] - 2026-07-22
561
-
562
- ### Added
563
- - **A headless media loopproduce + send.** Two paired additions close the coverage hole where any
564
- media-collect flow (e.g. `activate-app`'s registration / ID uploads) stalled at the upload prompt
565
- because the CLI could neither *make* nor *send* an image:
566
- - `octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]` AI-generates an
567
- image on the platform (needs a `media:generate`-scoped token), stores it as a public asset, and prints
568
- its `MEDIA-` handle + serve URL. `--out` downloads the bytes to a file (WhatsApp renders only
569
- `.png`/`.jpg`); `--json` emits `{ media_id, url, mime, width, height, bytes }`.
570
- - `octwin chat --media <file|media-id>` uploads a local file (or a media id from
571
- `media generate --json`) as an image/document/audio inbound; any accompanying `"message"` rides as
572
- its caption. The platform's media pipeline folds the upload into a running collect, so media flows
573
- are now fully drivable headlessly.
574
-
575
- ### Changed
576
- - **The KB-drift nudge now names what changed.** With the platform serving per-entry content hashes, the
577
- post-command nudge appends a `(N changed · M added · K removed)` summary instead of a bare hash pair.
578
- - **`octwin platform-kb` prints a changelog on pull.** Instead of only a doc/catalog count, the pull now
579
- diffs the fresh index against your last pull and lists exactly which docs/catalogs were added (`+`),
580
- changed (`~`), or removed (`-`) so a replaced schema shape is visible, not silent.
581
-
582
- ## [0.1.12] - 2026-07-22
583
-
584
- ### Fixed
585
- - **The KB-drift nudge now fires after `chat` too.** `chat` the command a debugging session runs most —
586
- was missing from the networked-command list, so a chat-heavy session never noticed the platform's
587
- capability reference had moved. The nudge also now shows the concrete drift
588
- (`old-hash → new-hash`) so you can see it's real, not a heuristic.
589
-
590
- ### Changed
591
- - **Every networked command announces what it's doing before it does it.** `chat` prints
592
- `→ Connecting to <tenant>/<project> as '<handle>' …` before opening the stream and
593
- `… delivered waiting for the reply (up to Ns)` after the send; `status` / `whoami` /
594
- `platform-kb` / `records` / `logs` / `cases` each print a one-line `→ …` header naming the
595
- action and target before the first network call — no more silent seconds followed by a result
596
- (or a hang with no clue what was being attempted). JSON modes (`--json`) stay clean for piping.
597
-
598
- ## [0.1.11] - 2026-07-21
599
-
600
- ### Fixed
601
- - **Auth-failure hint on every command, not just the inspect family** (author-feedback A7). `validate`,
602
- `deploy`, `status`, and the single-record read now print the scope/transient explanation on a 401/403
603
- (the shared `authFailureHint`), instead of a bare `HTTP 401` that left it ambiguous whether the token
604
- lacked a scope or the platform hiccupped.
605
-
606
- ## [0.1.10] - 2026-07-21
607
-
608
- The capability reference is now **pulled, never bundled** the authoring skill dropped its committed KB
609
- snapshot, so `octwin platform-kb pull` is the single source of truth, and the CLI watches it for drift.
610
-
611
- ### Added
612
- - **KB-drift nudge** after any command that already talks to the platform (`deploy` / `status` /
613
- `test` / `validate --remote` / `records` / `cases` / `logs` / `whoami`), the CLI does one tiny
614
- `octwin-platform-kb?meta=1` GET and prints a one-line "the platform capability reference changed —
615
- `octwin platform-kb pull`" when the platform's `content_hash` no longer matches your last pull.
616
- Fail-silent + TTY-only + no-op until you've pulled once — the sibling of the existing CLI-upgrade notice.
617
-
618
- ### Changed
619
- - **`octwin platform-kb pull`** now records the reference's `content_hash` in `.octwin/platform-kb/index.json`
620
- (the fingerprint the drift check compares against).
621
-
622
- The conversation-debugging release driven by the second author-feedback round (xpeng-egypt),
623
- which found the author→validate→deploy loop solid but multi-turn debugging impossible from the CLI.
624
-
625
- ### Added
626
- - **`octwin chat --tap "<tap-id>"`** press a rendered button / list row (sends the web channel's
627
- `type:'interactive'` inbound). Paired with full render printing, any tap-driven flow is now drivable headlessly.
628
- - **Full render printing**every reply prints its complete interactive content: list sections/rows,
629
- buttons, and carousel cards, each with its **tap id**; header/footer/list-button text included.
630
- - **`--json` on `chat` / `logs` / `cases`** raw SSE envelopes (chat) or raw event/case payloads.
631
- - **`octwin cases [caseId] [--queues]`** — inspect casework (support tickets): the inbox, one case +
632
- timeline + applicable decisions, or queue keys with open counts. Casework packs previously had zero
633
- data-inspection path (`records case` was a dead end).
634
- - **Per-subcommand `--help`/`-h`** intercepted before any network or auth work (previously
635
- `octwin logs --help` made a network call and could die with a 401).
636
- - Multi-message turns: `chat` now collects **all** of a turn's renders (quiet-gap detection) instead
637
- of cutting at the first one.
638
-
639
- ### Changed
640
- - **`octwin logs`**: the conversation list shows contact handle + last activity and filters with
641
- `--as <handle>`; the timeline prints **full** event text (no more 240-char truncation) plus the
642
- renders each turn actually sent; footer points at `--json` / `mastra-messages` for verbatim payloads.
643
- - **Network failures name the target URL** (`platform unreachable at <url>`), and 401s explain
644
- invalid-token vs one-off transient instead of a bare status code.
645
- - `octwin records` on an unknown entity now surfaces the server's error body, and for `case`/`cases`
646
- points at `octwin cases`.
647
-
648
- ### Fixed
649
- - **Multi-turn chat actually works.** Two bugs made consecutive `octwin chat` calls look like "every
650
- call starts fresh": (1) the idempotency `local_id` was a **constant** per handle, so the platform's
651
- 5-minute inbound dedup silently dropped the second message a fresh id is now minted per call;
652
- (2) a fresh SSE connect **replays** recent history and the CLI printed the first render frame — i.e.
653
- the *previous* turn's greeting as the reply; the CLI now drains the replay and only accepts frames
654
- newer than the connect boundary. The platform always kept one open conversation per `--as` handle
655
- the same handle genuinely continues the same conversation now.
656
-
657
- ## [0.1.8] - 2026-07-21
658
- - Global-first install guidance (`npm i -g octwin-cli`); upgrade notice suppressed under `npx`.
659
-
660
- ## [0.1.7] - 2026-07-21
661
- - `--version`/`-v` flag; daily fail-silent, TTY-only "newer version available" notice.
662
-
663
- ## [0.1.6] - 2026-07-21
664
- - `octwin init` scaffolds a home-hub menu (`home` list_picker + example `browse` tool) instead of a bare greeting.
665
-
666
- ## [0.1.5] - 2026-07-20
667
- - `octwin validate --remote` (the platform's FULL manifest + flow-DSL check, all errors at once);
668
- headless debugging commands `octwin records` / `octwin logs` / `octwin chat`; `octwin test` became
669
- an alias for `validate --remote`. First author-feedback round.
670
-
671
- ## [0.1.4] - 2026-07-20
672
- - Authoring reliability + DX fixes from the first end-to-end external-pack test.
673
-
674
- ## [0.1.3] - 2026-07-20
675
- - `octwin platform-kb pull` — pull the platform capability reference for the authoring skill.
676
-
677
- ## [0.1.2] - 2026-07-20
678
- - Proper npm README.
679
-
680
- ## [0.1.1] - 2026-07-20
681
- - Initial npm publish: `init` / `validate` / `login` / `whoami` / `deploy` / `status`, `oct_` deploy tokens.
1
+ # Changelog — octwin-cli
2
+
3
+ All notable changes to the `octwin` CLI (npm package [`octwin-cli`](https://www.npmjs.com/package/octwin-cli)).
4
+ Format: [Keep a Changelog](https://keepachangelog.com/) — newest first, bucketed into
5
+ **Added · Changed · Deprecated · Removed · Fixed · Security**. The platform-wide view lives in the
6
+ repo root [`CHANGELOG.md`](../../CHANGELOG.md); this file is the CLI-only cut that ships with the package.
7
+
8
+ ## [0.8.4] - 2026-09-02
9
+
10
+ ### Fixed
11
+ - **A deploy whose seed failed still led with `✓ Deployed`.** The step errors were never
12
+ missing `readDeployProgress` has collected every `status:'error'` frame into `stepErrors`
13
+ since 0.1.14, printed them, and exited non-zero. They were printed *after* `printDeploySuccess`,
14
+ so the first and largest line said `✓ Deployed` and the came underneath. A pack author
15
+ reported their deploy as a clean success while its demo seed had failed; they were reading the
16
+ transcript exactly as written. The count now goes INTO the headline, which reads
17
+ `⚠ Deployed <id>@<v> onto <target> WITH n failure(s) — data may be incomplete`. `octwin seed`
18
+ gets the same treatment. *A signal that arrives after the verdict is not a signal.*
19
+ - **The non-streaming deploy and seed paths had no step-error check at all.** With no SSE frames
20
+ to read, a failed seed printed a clean `✓` on that path even after the streaming one was fixed
21
+ in 0.1.14. Both now read `seed_failed` from the response body (new in the platform alongside
22
+ this release), print each failed kind with its reason, and exit non-zero.
23
+ - **A partial demo seed is now reported as partial.** The platform no longer abandons the rest of
24
+ the seed when one demo row throws, so `Seeded:` gains an `n row(s) FAILED` part without it a
25
+ partial seed reads as a complete one that simply had fewer records than the author wrote.
26
+
27
+ ## [0.8.3] - 2026-09-02
28
+
29
+ ### Fixed
30
+ - **`validate` printed all-green over a pack the platform could not import.** Every YAML file was
31
+ parsed inside a `try { … } catch { return [] }`, so a file that failed to parse was silently
32
+ DROPPED from the list and every later check skipped it. The `catch` was justified in a comment
33
+ "a syntax error is the structural gate's to report" and that was not true: the structural gate
34
+ (`validatePackBundle`) checks paths, extensions and traversal, and never parses YAML.
35
+
36
+ Measured 2026-09-02: a duplicate `s_header:` key in a locale file passed `octwin validate` twice
37
+ and was then refused by the platform importer with `Map keys must be unique at line 24,
38
+ column 3` the same message, from the same `yaml` version the CLI already had. The author's only
39
+ clue was a red box in the console naming no file.
40
+
41
+ YAML syntax is now its own announced gate that FAILS: it names each unparseable file with the
42
+ parser's own message and exits non-zero, before any check reads a document. An unparseable file is
43
+ not "no findings", it is "no idea", and the two must never print the same.
44
+
45
+ - **Three more readers that turned "I could not read this" into a normal answer.** Swept every
46
+ discarding `catch` in the CLI after the above; three collapsed a real fault into an ordinary state:
47
+ - `readCreds` returned `{}` for a CORRUPT `credentials.json` as well as an absent one, so a token
48
+ the author knows they saved simply stopped being found and the advice was "log in" with no
49
+ mention of why. Now names the file and the parse error, once per process (it has three call
50
+ sites and one broken file is one problem).
51
+ - `readLocalKb` returned null for a half-written `.octwin/platform-kb/index.json` as well as a
52
+ never-pulled oneso an interrupted pull read as "you have not pulled yet" and the author
53
+ re-ran the command that left it that way. [`kb-path.ts`](src/lib/kb-path.ts) states this rule
54
+ for the DIRECTORY lookup; the index read beside it kept collapsing anyway. It now says which.
55
+ - The deploy progress stream discarded unparseable SSE frames silently. Skipping one is right;
56
+ skipping ALL of them left `terminal: null`, reported as "the stream ended without a terminal
57
+ event" true, and indistinguishable from a dropped connection. The count is now named when
58
+ (and only when) no terminal frame arrived, pointing at a CLI older than its platform.
59
+
60
+ The other five discarding catches were read and LEFT: each returns a documented fallback for a
61
+ state that is genuinely ordinary (no update-check network, a heartbeat SSE frame with no `data:`).
62
+ `scripts/` was swept too and is clean by design — `kb-dataflow.mjs` skips a fence it cannot parse,
63
+ and `check-kb-examples.mjs` holds a non-vacuity FLOOR of 130 parsed fences so the skip cannot go
64
+ quiet. That is the better pattern where a skip is legitimate.
65
+
66
+ ## [0.8.1] - 2026-08-27
67
+
68
+ ### Fixed
69
+ - **Logging in made `platform-kb pull` WORSE.** The command preferred the tenant-scoped route
70
+ whenever a token was saved, and that route is guarded by `pack:deploy` so a developer holding a
71
+ token without that scope got a **403 on a pull that succeeds with no token at all**. It is the
72
+ likeliest token a new developer holds: no console preset could even reach `pack:deploy` before
73
+ 2026-08-26. `platform-kb` now always uses the anonymous route and never sends a credential.
74
+ `--token` is accepted and ignored so existing scripts keep working.
75
+
76
+ The platform serves the *same* bundle from all three of its views — its route file states the
77
+ payload is identical and tenant-independent so the authed route returned nothing extra and the
78
+ credential could only ever subtract. The two reasons the old code gave did not survive checking:
79
+ "keeps the author's own instance the source of truth" is vacuous (both routes are the same
80
+ instance), and "works against platforms that predate the public rung" is a backward-compatibility
81
+ shim this codebase does not carry.
82
+ - **The drift nudge now works without a token.** It polled through the same authed route, so an
83
+ author inspecting data with a narrow token was told, every time, that it *could not* check —
84
+ advice in place of the signal. Both the poll and the apology are gone: the poll is anonymous, so
85
+ it simply works.
86
+
87
+ ## [0.8.0] - 2026-08-27
88
+
89
+ ### Added
90
+ - **Five new command families, closing the gap between what a token reaches and what the CLI can
91
+ ask.** A **Full CLI access** token spans the whole scope registry, and four of those domains had
92
+ no command at all so the reach existed and there was nothing to type.
93
+ - `octwin automation [campaigns]` every job the pack's declaration produced, with status,
94
+ interval and last result (matched / acted / errors), under a health line whose counts come from
95
+ SQL rather than from filtering the page (the job list is capped server-side, so a client-side
96
+ count would depend on the cap). Writes: `run` · `pause` · `resume` · `send <campaignId>`.
97
+ - `octwin integrations` what the pack **declares** beside what is actually **configured**, in
98
+ one view, because a connection declared and never configured is the commonest reason an
99
+ integration silently never fires and neither list alone can show it. Plus
100
+ `preflight <key>` (diagnosis, no outbound call, `integrations:read`), `test <key>` (a live call,
101
+ `integrations:write`, exits 1 on failure), `deliveries [id]`, `retry|cancel|send-now <id>`,
102
+ and `events` for what arrived inbound.
103
+ - `octwin journeys [id] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]`
104
+ the declared journeys, measured. Same flag grammar as `octwin analytics` on purpose: a journey
105
+ funnel and an entity funnel are the same question about different subjects. `--definition` is
106
+ the one view that works with no traffic.
107
+ - `octwin performance [--detail]` the project's business indicators, each with its delta against
108
+ the previous window and the `why` naming the declaration it came from. Needs `records:read`, not
109
+ a `performance:*` scope (there is none), so a read-only token already reaches it.
110
+ - `octwin usage` model calls, tokens and cost, by model / kind / agent / channel; project when
111
+ one is pinned, workspace otherwise. Needs no particular scope. **Model spend only**
112
+ WhatsApp/Meta message billing is operator-only and outside the token scope registry, which the
113
+ output says rather than leaving as an absence.
114
+ - **`octwin automation run|pause|resume|send` accept the declaration KEY.** The routes take a UUID
115
+ and answer a bare *"Malformed identifier in the URL"* 400 for anything else but the key
116
+ (`cart_recovery_nudge`) is what the list prints and what the author wrote in their own YAML. The
117
+ key is now resolved against the list route, and an unknown one fails naming the keys that exist.
118
+
119
+ ### Fixed
120
+ - **A body-less write sent `content-type: application/json` and no body, so Fastify answered a bare
121
+ `400 Bad Request`.** `apiSend` set the header unconditionally; every caller until now passed an
122
+ object, so the branch was dead and the bug latent. The first body-less POST (`automation run`) hit
123
+ it immediately. The header is now omitted when there is no body rather than each caller learning
124
+ to pass `{}`, which is the per-site divergence that helper exists to prevent.
125
+ - **Four URLs were exempt from the route guard.** `cli-routes.test.ts` reads a template literal in
126
+ the first argument position, so `apiGet(base, t)` a bare identifier was never checked. That
127
+ covered the `scheduling` engine-state and `agents` roster calls, and would have covered the new
128
+ `journeys`, `performance` and `usage` ones. All now spelled as literals: the guard checks **62**
129
+ URLs, up from 45.
130
+
131
+ ## [0.7.3] - 2026-08-26
132
+
133
+ ### Changed
134
+ - **A marketplace listing request is now OPT-IN: `octwin deploy --request-listing`.** A plain
135
+ `octwin deploy` says nothing about the public marketplace — it neither asks for a listing nor
136
+ gives one up. `--withdraw-listing` retracts, including an approved listing.
137
+ **Why:** the request was read from the manifest's `listing.public` on every deploy, and deploying
138
+ is how you *test*. 17 of the 23 published packs carry that key, so an edit-deploy-chat loop filed
139
+ a review request every time round. Worse, the *absence* of the key meant **retract** so an
140
+ ordinary deploy of a pack whose manifest you had not yet annotated could wipe a listing you
141
+ already had. `--request-listing` still requires `listing.public: true` in the manifest (the
142
+ manifest states that the pack is a product; the flag is you choosing to ask), and the flag
143
+ without the key is a clean error rather than a silent no-op.
144
+ - **The listing verdict distinguishes "you submitted this" from "your approval expired".** They are
145
+ both `pending` and they mean opposite things to an author. A deploy that changes the content of an
146
+ approved pack now says so in those words an approval covers only the content it was made
147
+ against, so it cannot survive an edit.
148
+
149
+ ### Added
150
+ - **A one-line nudge when your manifest asks and your deploy did not.** A pack carrying
151
+ `listing.public: true` that has never been submitted now prints the flag to use, so an opt-in
152
+ default cannot turn into waiting for a review nobody requested.
153
+
154
+ ### Fixed
155
+ - **The "newer CLI" and "KB drifted" notices now print when a command FAILS.** All three nudges
156
+ (CLI upgrade · KB drift · unread memos) were awaited at the very end of `main()`, which silently
157
+ meant *only on success* `die()` is a hard exit, so a failed deploy, an auth error or a bad
158
+ manifest printed none of them. That is backwards: a stale capability reference is a leading cause
159
+ of the failure an author is staring at, so the run that most needed the nudge was the only one
160
+ that never got it. They are now computed before the command and printed however it ends.
161
+ - **The CLI-upgrade notice is no longer hidden from piped output.** It began with
162
+ `if (!process.stdout.isTTY) return`, so every agent-driven and CI run the readers that cannot
163
+ notice an outdated CLI on their own — was the one audience never told. Its two siblings had
164
+ already dropped that gate for exactly this reason; now all three agree. Still skipped under
165
+ `npx`, where there is nothing to upgrade.
166
+ - **`octwin login` now reports KB drift too.** It is networked and usually the first command of a
167
+ session, i.e. the best possible moment to say the reference moved, and it was excluded.
168
+ - **A memo poll can no longer fail the command it was annotating.** It went through `apiGet` →
169
+ `fetchOrDie`, whose job is to `die` on a network failure — so an unreachable platform turned an
170
+ observer into a command failure. It now uses a plain, timed, fail-silent fetch.
171
+ - **A failing command reports its real exit code again on Windows.** Moving the polls before the
172
+ command made `process.exit()` abort with `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)`
173
+ and return **127** instead of 1 so CI saw neither success nor the documented failure code. The
174
+ CLI now unwinds through a `CliExit` and lets the event loop drain, which is the only teardown
175
+ that is clean once the polls have opened sockets. `--check`'s exit 2 and the streaming commands
176
+ were re-verified.
177
+
178
+ ## [0.7.2] - 2026-08-20
179
+
180
+ ### Fixed
181
+ - **`octwin feedback` now tells you what is already waiting, on stdout.** It discarded the submit
182
+ response entirely destructuring `{ status, json }`, checking the status and never reading the
183
+ body — then printed *"a reply arrives as a memo, this CLI will tell you when one is waiting"*. The
184
+ platform started returning the unread counts on that response (2026-08-20), so the answer was
185
+ there to print and was being thrown away.
186
+ **Why it matters more than it sounds:** `notifyIfMemosWaiting` already covers this command and was
187
+ firing correctly but it writes to **stderr**, and the authors driving this CLI are agents reading
188
+ stdout. The one signal that mattered was addressed to a channel nobody was listening on. On
189
+ 2026-08-19 an author submitted two reports two hours after three replies were published and
190
+ re-reported five findings those replies had already closed. Now the count prints in the same block
191
+ as the acknowledgement, naming the command, and says outright when a reply may already answer what
192
+ you just sent. Against an older platform the field is absent and the previous line stands.
193
+
194
+ ## [0.7.1] - 2026-08-19
195
+
196
+ ### Fixed
197
+ - **`work decide` now says when an action moves nothing.** `to_stage` is optional an action may
198
+ record a decision without a transition but both outputs communicated that by omission: `--dry-run`
199
+ left `to_stage: null` sitting among twenty other keys, and a real apply printed a bare
200
+ `✓ Applied '<action>'`. An author read both, shipped `request_more_info` with no `to_stage`, and
201
+ lost the `awaiting_customer` their whole customer-reply capture branched on — their words: *"Nothing
202
+ about this failed loudly; it just quietly never did the one thing the whole feature depends on."*
203
+ The preview now states `→ does NOT change the status (this action only records a decision)` and the
204
+ apply says `status unchanged (the action declares no to_stage)`. The console's decision panel got the
205
+ same sentence in the same change.
206
+
207
+ ## [0.7.0] - 2026-08-18
208
+
209
+ ### Added
210
+ - **`octwin memos` — the channel answers now.** `octwin feedback` was one-way: an author reported and
211
+ heard nothing, ever. Two kinds arrive here, both printed in full — a **reply** to a report you sent,
212
+ and a **notice** published to every author (a new capability, a deprecation, a breaking change) —
213
+ each carrying `info` / `action_required` / `breaking`. Reading marks them read; `--all` re-reads
214
+ history and acks nothing (also the escape hatch if a piped `--json` consumer died halfway).
215
+ - **Every networked command now says when a memo is waiting**, one line to stderr, always naming the
216
+ command to run. It rides work that was already networked, never throws, and is **deliberately not
217
+ TTY-gated** the same reasoning as the KB drift nudge, whose docblock says it plainly: the primary
218
+ reader of this CLI is an authoring agent whose output is piped, and gating on `isTTY` means the one
219
+ reader that cannot notice on its own is the only one never told. Unlike that check there is no local
220
+ marker file: read state is per-workspace on the platform, so an agent in a fresh container still
221
+ learns about an unread reply.
222
+
223
+ Why it matters, measured: in August 2026 four reports sat unread for nine days, and one author
224
+ replaced every `$t()` call with hardcoded English across all 16 flows of a production pack to work
225
+ around a bug that had been fixed two days earlier. Nothing could tell them.
226
+
227
+ ## [0.6.1] - 2026-08-18
228
+
229
+ ### Fixed
230
+ - **`octwin status` worked for no pack at all, and had not since 2026-08-01.** It sent the manifest's
231
+ **bare** id (`clinic`) to a route that requires the qualified `<owner>.<name>` and a manifest
232
+ cannot legally declare the qualified form, because the owner segment is attached at publish time
233
+ from the authenticated publisher (both the platform and this CLI reject a `.` in it). So the route
234
+ answered 404 for every pack, every time. It now resolves the owner from `whoami`'s `tenant_slug`,
235
+ and takes an explicit `octwin status <packId>` when you already know it (from `octwin agents` or
236
+ `octwin projects`, both of which print qualified ids). Reported by two pack authors, who lost time
237
+ to a command that said their pack was missing seconds after a deploy the platform had accepted.
238
+ - **A 404 from `octwin status` no longer always reads "not installed".** That one branch covered four
239
+ different failures a malformed pack id, an unknown tenant, an unknown/pinned-elsewhere project,
240
+ and the genuine no-active-install so the message named the *least* likely cause and sent an author
241
+ chasing a deploy that had already succeeded. The route says which; the CLI now relays it, and adds
242
+ the "run `octwin deploy` first" hint only to the case it fits.
243
+ - **`octwin logs` always reported "No conversations"**, including immediately after an `octwin chat`
244
+ that had plainly worked. It read `conversations` off the response; the platform moved that endpoint
245
+ to the shared page envelope on 2026-08-09 and answers `rows`, which every other list command here
246
+ already reads. Both authors filed this as replica lag or a cache gap — nothing lagged, the payload
247
+ was simply never read. `octwin logs <conversationId>` was unaffected.
248
+
249
+ ## [0.6.0] - 2026-08-09
250
+
251
+ ### Changed
252
+ - **`octwin cases` is now `octwin work`, and it works again.** The platform's work-inbox
253
+ consolidation (2026-08-08) replaced the case-only surface with `/work` over every entity a pack
254
+ declares worked, and renamed the scopes to `work:read`/`work:write` which left every `octwin
255
+ cases` call a silent 404 in a published CLI. The command family is renamed to match the platform:
256
+ `octwin work [recordId] [--queues]`, with writes `assign` / `note` / `decide` (the declared
257
+ operator actions, `--dry-run` previews) and `stage <id> --to <s>` riding the one platform-wide
258
+ stage verb (`POST …/xrm/records/:id/stage`, needs `records:write`). `transition` is gone with the
259
+ route it called.
260
+ - **`octwin orders transition` rides the same stage verb.** `POST …/orders/:ref/transition` was
261
+ retired server-side in the same consolidation; the CLI now resolves the order's record id from the
262
+ detail read and moves the stage through `…/xrm/records/:id/stage` (scope: `records:write`).
263
+
264
+ ### Added
265
+ - **A route guard so this cannot rot silently again**: `src/lib/cli-routes.test.ts` extracts every
266
+ `/api/self/**` URL the CLI builds and asserts it (method included) against the platform's dumped
267
+ route tablethe gap that let both breakages above ship.
268
+
269
+ ## [0.5.1] - 2026-08-01
270
+
271
+ ### Fixed
272
+ - **`validate --remote` blamed the platform's version for a bad `--project`.** Every 404 from
273
+ `POST /api/self/p/packs/validate` printed *"this platform has no /packs/validate endpoint yet
274
+ (older version)"*. That was accurate when the route resolved nothing, but it now resolves the
275
+ tenant and project **before** validating and an unknown project, or one outside a token's pin,
276
+ answers 404 by design. So a typo in `--project` sent you looking for a version mismatch instead of
277
+ at the flag. The two cases are now told apart by the response **body**, not the status: a missing
278
+ route is Fastify's `error: 'Not Found'` and keeps the old wording, while the platform's own
279
+ `project '<slug>' not found under tenant` is printed as-is, with the flags to check.
280
+
281
+ ## [0.5.0] - 2026-08-01
282
+
283
+ ### Added
284
+ - **`octwin seed [--pack <packId>]`** apply the pack's demo/reference data to the project it is
285
+ installed on, without redeploying: `xrm.yaml` `demo:` records + scheduling availability, the
286
+ commerce catalog, and the demo operator topology. Reports what **each kind** produced.
287
+ Previously seeding was reachable only as `deploy --seed`, because the platform's seed endpoint was
288
+ keyed on an install id, guarded platform-admin, and carried no tenant/project segments — so the
289
+ `/api/self/**` surface could not reach it and a `pack:deploy` token never could. Re-seeding meant
290
+ a full redeploy or asking an operator. Idempotent and cheap to re-run: records upsert, and
291
+ existing media is REUSED rather than regenerated, so a second pass reports zero images.
292
+
293
+ ## [0.4.0] - 2026-08-01
294
+
295
+ ### Added
296
+ - **`octwin projects create` / `octwin projects rm`** — a **disposable end-to-end environment** from
297
+ the CLI, which is what marketplace developers have been missing. `octwin deploy` has always needed
298
+ a project that already existed and the CLI could only *list* them, so standing up a throwaway
299
+ full deployment meant opening the console or asking an operator. Now:
300
+ ```bash
301
+ octwin projects create "Scratch" # slug `scratch`
302
+ octwin deploy --project scratch --seed # publish + install + demo data
303
+ octwin chat "hi" --project scratch # talk to it (--script for a whole conversation)
304
+ octwin projects rm scratch --yes # throw it away
305
+ ```
306
+ A throwaway environment is deliberately **not a special kind of thing** it is an ordinary
307
+ project in your own workspace, so it inherits your plan, entitlements, RBAC and teardown with no
308
+ separate lifecycle. `create` derives the URL slug from the name (`--slug` pins one, `--pack`
309
+ installs an already-published pack). `rm` is a HARD delete — the project and everything cascading
310
+ from it, no undo, not the same as archiving so **without `--yes` it only prints what would be
311
+ destroyed**, making the dry run the default; the impact list is derived from `pg_constraint`, the
312
+ same payload the console's confirm dialog renders. Both need `projects:write`, which a
313
+ `pack:deploy` token does **not** confer — the CLI names that scope in the 403 hint.
314
+
315
+ ## [0.3.0] - 2026-07-31
316
+
317
+ ### Added
318
+ - **`octwin projects`**the `--project <slug>` values your token can actually name, with the plan's
319
+ project cap. Every project-scoped command takes that flag and nothing printed the list, so an
320
+ author whose token was not pinned had to guess, and a wrong guess 404s identically to a project
321
+ that exists but has no install. `--archived` includes archived projects; `--json` for raw.
322
+ A `pack:deploy` token reaches it: the platform route was widened to accept `pack:deploy` alongside
323
+ `projects:read`, because `pack:deploy` is `access: 'special'` and confers nothing else — the
324
+ command would have 403ed for exactly the person it exists for.
325
+ - **`octwin validate --require-kb`** turn a skipped KB-driven check into a failure, for CI.
326
+
327
+ ### Fixed
328
+ - **`octwin validate` printed ✓ with two of its three checks switched off.** The render-intent and
329
+ primitive-argument checks read `<packDir>/.octwin/platform-kb/`, joined onto the pack directory
330
+ EXACTLY so an author who pulled once at a repo root and then worked in `packs/<name>/` had both
331
+ checks silently disabled, and the printed above them unconditionally while the per-check ✓s lived
332
+ inside the `if`s. An entire batch of platform-backlog entries reached production through that hole.
333
+ Three fixes: the KB is now found by **walking up** (one pull at a repo root covers every pack under
334
+ it, `src/lib/kb-path.ts`); the readers report **three states** absent, malformed, ok so a
335
+ half-written pull no longer reads as "you haven't pulled yet"; and a skip is **announced on the
336
+ last line printed**, because a caveat above a ✓ does not reach a reader who stopped at the ✓.
337
+ Warn-and-exit-0 stays the default (the KB is a gitignored cache and `platform-kb pull` needs
338
+ `pack:deploy`, so a hard failure would break a fresh clone) the defect was the silence, not the skip.
339
+ - **`octwin status` said "live and current" for a pack that could not receive a message.** Every field
340
+ it printed was process-global: `registered` is true for a pack ANY project on that instance loaded.
341
+ Dispatch is per-project and takes the oldest ACTIVE install, ignoring the rest. The route now
342
+ filters `status = 'active'` (an archived install used to report live) and compares against the real
343
+ dispatch target, so status can say plainly that another pack owns this project's messages.
344
+ - **`octwin validate --remote --project does-not-exist` answered `{ ok: true }`** while `deploy` with
345
+ the same flag 404ed. The platform route resolves tenant and project before validating now.
346
+
347
+ ### Changed
348
+ - **`octwin validate --remote` also runs the platform's flow LINT.** It used to run only at pack load,
349
+ so a finding of severity `error` an `assign:` whose value is a quoted literal, a `$t()` key with
350
+ no namespace passed validate and failed the deploy. Offline `validate` cannot do this: the lint
351
+ needs a template-expanded `FlowDef`, which only the server builds.
352
+ - **`record_aggregate` / `record_group` now REQUIRE a scope** (`contact_id` or `all: true`) — a
353
+ platform contract change the offline arg check replays from the KB. Re-pull after upgrading.
354
+
355
+ ## [0.2.0] - 2026-07-30
356
+
357
+ ### Fixed
358
+ - **`records`, `cases`, `orders` and `catalog` printed a correct total and ZERO rows.** The platform
359
+ collapsed its eight per-domain list envelopes into one `Page<T>` whose collection key is `rows`;
360
+ the CLI still read `json.records` / `.cases` / `.orders` / `.products` behind a `?? []`, so the
361
+ fallback turned a key rename into an empty list instead of an error. `octwin records doctor`
362
+ reported *"5 record(s)"* and then *"(none mint a `records:read` token …)"*, blaming the user's
363
+ credentials. All five readers now go through one `readPage` helper
364
+ (`src/lib/page.ts`) with a test pinned to the real envelope — a hand-copied key name in an
365
+ untested CLI is what shipped this, so the fix is the single reader, not five edits.
366
+ - **`--flag=value` silently did nothing.** `--limit=5` became the key `limit=5` and the command ran
367
+ with its default. Now parsed; `--set`/`--param` also accept repetition.
368
+ - **`octwin catalog` always printed `Products: 0`** it counted the page, and the route's `total`
369
+ had never been read.
370
+
371
+ ### Added
372
+ - **Write commands — a pack author can now exercise the state their pack creates, not just read it.**
373
+ Every one needs the matching `:write` scope; a leading verb (`create`/`note`/`transition`/…) is
374
+ what distinguishes a write from the read that shares its command.
375
+ - `records create|patch|stage|note`, `records tasks`, `records task complete`
376
+ - `cases assign|note|transition|decide` `decide --dry-run` previews the customer-facing copy and
377
+ resulting status **without committing** (that route needs only `cases:read`)
378
+ - `orders transition|refund` · `catalog availability|stock` · `agents set`
379
+ - `scheduling rules|rule add|rule rm|exception add|exception rm`
380
+ - **`octwin feedback`** submits the pack's `FEEDBACK.md` to the platform team, attaching the pack
381
+ version, this CLI's version and the `content_hash` of the capability reference you pulled the two
382
+ facts that separate a real platform gap from something already fixed or a stale KB. Replaces
383
+ "hand the file to the platform team".
384
+ - **`--offset` on every list**, and a line saying when a list was truncated and what to run next.
385
+ `has_more` was on every response and no command surfaced it, so a partial list looked complete.
386
+
387
+ ### Changed
388
+ - `orders refund` **exits non-zero when the payment gateway refuses.** The route answers `200` with
389
+ the verdict in `gateway`, so reporting the HTTP status would tell an operator money moved when it
390
+ did not.
391
+ - A 403 on a record or case write now says it may be an **RBAC grant gap** rather than a missing
392
+ scope: the scope is checked on the token, then the verb is re-checked against that specific row.
393
+
394
+ ## [0.1.21] - 2026-07-30
395
+
396
+ > **Never published to npm.** Bumped in-repo, then superseded hours later by 0.2.0 — everything
397
+ > below ships as part of that release. Kept as its own section because the changes are distinct.
398
+
399
+ ### Added
400
+ - **`deploy` and `status` report the marketplace-listing verdict.** A manifest carrying
401
+ `listing.public: true` put the pack into an operator review queue, and the state existed only in
402
+ the console so an author working from the CLI got no acknowledgement that the request had
403
+ registered, and never saw a rejection note (which the platform *requires* precisely because it is
404
+ their only feedback). Both commands now print it via `printPublicListing`, and stay silent for a
405
+ pack that never asked. `status` also distinguishes *approved* from *actually public*: an approval
406
+ pins the content sha it reviewed, so any edit returns the pack to the queue on its own.
407
+ - **`octwin pull` is discoverable.** It was implemented and answered `--help`, but appeared in
408
+ neither `octwin help` nor the README the one command that recovers a deployed pack's only source
409
+ copy was invisible unless you already knew its name. Also added to the scope-requirement table, so
410
+ a 403 names `pack:deploy` instead of printing the generic hint.
411
+
412
+ ### Fixed
413
+ - **The capability-reference drift check no longer fails silently on a narrow token.** The `?meta=1`
414
+ poll needs `pack:deploy`, but it rides on every networked command — so an author inspecting data
415
+ with a `records:read`-only token got no drift signal at all, and a stale reference is exactly what
416
+ leads to inventing a primitive from memory. A 401/403 there now says so once.
417
+ - **README: casework is declared in `worklist.yaml`, not a `cases.yaml`.** That grammar was retired;
418
+ following the README produced a pack whose casework silently never loaded. `octwin media generate`
419
+ was also missing from the command table.
420
+ - **`records` no longer blames the plan for a route the plan does not gate.** `GET /xrm/entities`
421
+ carries only the scope guard, so a 403 on the entity list wrongly cited the `records` plan feature.
422
+
423
+ ### Removed
424
+ - **Unreachable rename branches in `init`.** The agent-id and `main`-flow renames sat behind
425
+ `--agent` / `--flow` options the CLI never parsed, against a template that ships no `main` flow —
426
+ both branches could never fire, and their patterns would have matched nothing if they had.
427
+
428
+ ## [0.1.20] - 2026-07-29
429
+
430
+ ### Added
431
+ - **`octwin validate` catches primitive arguments the platform would drop — offline, no token.** A
432
+ primitive takes the arguments it declares and silently ignored the rest, so an invented or
433
+ misspelled `args:` key deployed clean, ran clean, and simply did nothing: `record_list args: {
434
+ order: }` (the argument is `sort`) never ordered, and `booking_cancel args: { booking_record_id:
435
+ }` meant the REQUIRED `record_id` never arrived, so cancelling just failed. Validate now replays
436
+ the platform's contract from the pulled KB (`primitives/*.json` → `inputSchema`) and names the bad
437
+ argument, what the primitive takes, and any required argument that is missing. Scanning the 21
438
+ shipped marketplace packs found **ten** such errors across five packs. Keys are checked, values are
439
+ not — an `args:` value is normally an expression string resolved at runtime.
440
+ [`args-check.ts`](src/lib/args-check.ts).
441
+ - Silently skipped when the KB has not been pulled yet, like the render-intent check beside it.
442
+ - It cannot see inside a `use:` template body (expanding one is the platform's job) `octwin
443
+ validate --remote` covers that case, and now also reports unreachable `$bind.<path>` reads and
444
+ `outputs:` port typos, which have no offline equivalent.
445
+
446
+ ## [0.1.19] - 2026-07-29
447
+
448
+ ### Added
449
+ - **`octwin validate` catches render-intent fields the platform would drop offline, no token.** Each
450
+ render intent takes a fixed field set; a key outside it used to be swallowed at load and dropped at
451
+ render, with no error anywhere just a card missing what the author wrote. Validate now replays the
452
+ platform's own contract from the pulled KB (`render-intents/*.json` `allowed_keys`) and names the
453
+ bad field, the intent, and what is allowed. Scanning the 21 shipped marketplace packs found two real
454
+ instances in 223 intents: a `text_card` carrying `buttons` (the fallback's only escape button never
455
+ rendered) and a `list_picker` carrying a non-existent `group_by` (a whole mapper step feeding a
456
+ fabricated parameter). Silently skipped when the KB has not been pulled yet.
457
+
458
+ ### Changed
459
+ - **`octwin platform-kb pull` now explodes the reference and writes an `INDEX.md`.** A pull used to
460
+ write ~787 KB across 34 flat files — about 225k tokens, more than the pack being authored — so
461
+ finding one primitive meant reading a 122 KB catalog. Catalogs the platform describes as
462
+ enumerable are now written **one file per entry** (`primitives/record_list.json`,
463
+ `render-intents/carousel.json`, `declarations/xrm.json`, …), with `INDEX.md` mapping every doc and
464
+ all 171 entries to its file with a one-line summary. Reading one primitive went from ~38k tokens to
465
+ ~700. The layout comes from the platform (an `entries` descriptor on the bundle index) rather than
466
+ being hardcoded here, so this CLI keeps working against any platform version and a platform that
467
+ adds a catalog needs no CLI release. The pulled directory is cleared each pull, so a withdrawn
468
+ capability cannot linger as a file the authoring agent reads as current — its *contents* are
469
+ cleared rather than the directory itself, since on Windows a directory that is any shell's working
470
+ directory cannot be removed.
471
+
472
+ ## [0.1.18] - 2026-07-29
473
+
474
+ ### Fixed
475
+ - **The bundle collector and the platform's repo import disagreed about what a pack directory
476
+ contains.** `collectBundleFiles` had its own rule and kept three things the operator's GitHub
477
+ import drops: `*.ts`/`*.tsx`, `__snapshots__/` and `*.example`. Because the server rejects a
478
+ `.ts` outright, the same directory imported cleanly from GitHub and failed to deploy from
479
+ disk with `'…': executable code is not allowed` an error blaming a file that was never
480
+ meant to ship (a snapshot test beside a flow, or the documented `xrm.yaml.example` template).
481
+ The rule now lives in a vendored `lib/pack-source.ts` beside the vendored validator, and a
482
+ parity test in the platform drives both copies over one table.
483
+
484
+ ### Removed
485
+ - **`pack.json` is gone the saved login IS the deploy target.** The file had eroded to a single
486
+ field, `platform_url`, which `octwin login --url --token …` already states; `tenant`/`project`
487
+ became optional overrides once deploy tokens started carrying their own tenant and project pin.
488
+ So it was a second home for a fact `login` knew, and it cost real things: a machine-specific file
489
+ committed into every pack repo, a four-link config-precedence chain to document, and an
490
+ exclusion rule duplicated in the bundler and the server (which had already drifted apart).
491
+ `octwin init` and `octwin pull` no longer write one, and nothing reads one. **A pack directory is
492
+ now pack content and nothing else** the same repo deploys from any machine, and a pulled pack
493
+ redeploys with no flags.
494
+
495
+ ### Changed
496
+ - **`octwin login` now sets the default deploy target**, not just the token: it writes a
497
+ `default_url` key alongside the token in `~/.octwin/credentials.json`. Every command resolves
498
+ **flags env (`PACK_PLATFORM_URL`/`PACK_TENANT`/`PACK_PROJECT`/`PACK_TOKEN`) saved login**.
499
+ An existing credentials file simply has no `default_url` until the next `octwin login` which is
500
+ exactly what the "no platform url" error now tells you to run. CI is unaffected (it passes
501
+ `PACK_PLATFORM_URL` + `PACK_TOKEN` as env and never had a `pack.json` — that file is retired).
502
+
503
+ ## [0.1.17] - 2026-07-27
504
+
505
+ ### Added
506
+ - **`octwin pull <packId>` write a DEPLOYED pack's source back to disk.** The inverse of
507
+ `deploy`. A pack pushed with the CLI lived on the platform as an artifact the runtime served
508
+ but nothing could hand back, so its only source copy was the machine that pushed it. Pull it,
509
+ fix it, redeploy it. Writes a `pack.json` (retired in 0.1.18 the saved login is now the
510
+ target) so the pulled directory redeploys where it came from; `--version` picks a version,
511
+ `--force` overwrites a non-empty dir. You may pull a pack
512
+ your tenant OWNS (an operator token pulls any) — a pack you merely installed is not yours to
513
+ read.
514
+ - **`octwin chat --script <file>` drive a whole conversation from one invocation.** One turn per
515
+ line, in order, in a single process over a single SSE connection, waiting for each turn to settle
516
+ before sending the next. Blank lines and `#` comments are skipped; `tap:<id>` presses a rendered
517
+ row/button (the rest of the line is kept verbatim, since a tap id is itself colon-delimited),
518
+ `media:<path>` uploads a file, and `media:<path> | caption` attaches one.
519
+
520
+ This exists because **chaining invocations races the agent loop.** A turn ends on a quiet gap
521
+ (`TURN_SETTLE_MS`), which can arrive while the server-side loop is still running so
522
+ `chat A && chat B` lets B's inbound land mid-turn, and the agent fills required fields with
523
+ placeholder text (`service_id: "service-id"`) or starts a second workflow run. Those look exactly
524
+ like flow bugs and cost a field author three separate investigations. The one-turn-per-invocation
525
+ rule is now stated in `chat --help` and the header usage as well.
526
+
527
+ ### Changed
528
+ - **`octwin deploy` reports the suspended runs it invalidated.** A redeploy rebuilds the pack's
529
+ tools, and in-flight flow runs live in those tools — so every suspended conversation is dropped.
530
+ The deploy prints `ⓘ N suspended run(s) invalidated` when N > 0, instead of leaving the author to
531
+ discover it as a `workflow_resume_stale` on the next tap.
532
+
533
+ ## [0.1.16] - 2026-07-26
534
+
535
+ ### Added
536
+ - **A pack can ship committed images.** `jpg`/`jpeg`/`png`/`webp`/`gif`/`pdf` files in your pack
537
+ directory now travel as a separate `blobs` half of the bundle (base64 on the wire, `bytea` in
538
+ storage) instead of being rejected as "not an allowed pack file type". Declare each one under the
539
+ manifest's `static_assets: [{ file, key }]` and reference it from `config:` with the
540
+ `$pack_asset:<key>` sentinel the platform uploads it to the media system at install and the
541
+ sentinel resolves to the served URL. `octwin validate` reports the image count alongside the file
542
+ count.
543
+ - **The size ceilings fail locally, before upload** — 2 MB per file, 32 MB of binary per pack,
544
+ mirroring the server. `svg` stays rejected: it is script-capable and these assets are served to
545
+ browsers.
546
+
547
+ ### Fixed
548
+ - **Committed images were silently corrupted.** `collectBundleFiles` read *every* file with
549
+ `readFileSync(full, 'utf8')`, so a JPEG went through a lossy UTF-8 decode and arrived mangled
550
+ server-side a deploy that "succeeded" and rendered a broken image. Binary files now split off
551
+ into `blobs` and keep their bytes.
552
+ - **`octwin status` reported `loaded=(none)` for every pack, always.** It printed a `loaded_version`
553
+ field that stopped existing when reload moved to content-sha keying, which also meant the
554
+ version-drift warning underneath it could never fire. It now prints the **content sha** the
555
+ instance has loaded and the one the catalog holds (with the artifact's `origin`), and warns off the
556
+ platform's own `up_to_date` flag. The sha is the more useful fact anyway: re-publishing the *same*
557
+ version changes it, which is exactly the author's inner loop. A pack that is live and current but
558
+ **withdrawn** from the catalog now says so, rather than reporting a clean bill of health.
559
+
560
+ ## [0.1.15] - 2026-07-25
561
+
562
+ ### Added
563
+ - **`octwin agents [packId::agentId] [--prompt]`see what your agent actually runs as.** The platform
564
+ gained *governed* agent settings: an operator platform default can override the `default_model`
565
+ (and history window / working-memory toggle) your manifest declares, and that override is invisible
566
+ from the pack source. The command prints each governed setting's **effective** value, **which layer
567
+ won** (project override platform default pack manifest), and warns explicitly when your pack's
568
+ declared value is *not* in force. `--prompt` prints the exact system prompt the LLM sees for this
569
+ project (pack instructions + platform protocol + any project overlay).
570
+ - **`octwin orders [reference_id]`the commerce a conversation produced.** The list (number,
571
+ status/payment, total, contact) or one order with its line items, the
572
+ subtotal/tax/shipping/discount/total breakdown, `payment_ref`, and the allowed transitions. On a
573
+ `pending`/`none` payment it explains *why that's expected*: the forward payment lifecycle is
574
+ **webhook-owned** (not patchable), and the default gateway-less `manual` driver makes
575
+ `payment_request` take its `empty` port — so the flow should confirm pay-on-delivery, not error.
576
+ - **`octwin analytics [entity]` stage conversion for any pipelined entity.** The funnel engine that
577
+ powered journeys is now generic, so any entity declared with a `pipeline:` charts conversion +
578
+ drop-off (`--overview` / `--milestones` / `--trends` / `--cost`, and `--stage <id>` for the records
579
+ currently at a stage). An empty result names **both** causes the platform folds into one response —
580
+ no `pipeline:`, or no `view` grant on `record.<entity>`.
581
+ - **`octwin catalog [--readiness]`** — commerce products with price, availability and stock
582
+ (`untracked` when the SKU isn't inventory-tracked), plus the WhatsApp catalog binding.
583
+ `--readiness` runs the Meta Graph checklist with each failing item's suggested fix.
584
+ - **`octwin scheduling [--slots <resourceRecordId>]`** — the engine state (bookable resource types,
585
+ upcoming slots, booked seats) or the computed slots for one resource. This is how you verify the
586
+ availability rules a `deploy --seed` created, which 0.1.14 could create but not read back.
587
+
588
+ ### Changed
589
+ - **Auth failures now name the scope the command needs.** Every 401/403 appends the required scope
590
+ (and the plan feature, where the route is entitlement-gated) from one client-side mirror of the
591
+ platform's scope registry replacing the per-command hardcoded strings, several of which named no
592
+ scope at all. It also spells out that `pack:deploy` / `media:generate` are direct-grant only, so a
593
+ `tenant:admin` preset token does **not** confer them (the most common "but my token is admin" dead end).
594
+
595
+ ## [0.1.14] - 2026-07-23
596
+
597
+ ### Fixed
598
+ - **A failed demo seed no longer reports a clean `✓`.** `deploy` softens non-fatal
599
+ install steps (e.g. a demo-seed row) to warning frames, but the summary still printed
600
+ `✓ Deployed` and exited 0 — so a deploy that seeded ZERO records read as success (the
601
+ false-✓ trap). `deploy` now collects those step errors, prints `⚠ Deployed with N
602
+ warning(s) data may be incomplete` with each message, and exits non-zero so CI /
603
+ a `deploy && chat` chain catches it.
604
+
605
+ ### Added
606
+ - **`login` echoes what the token reaches.** After saving, `octwin login` calls
607
+ `/api/self/t/whoami` and prints the workspace, project pin, and scopes — so a fresh
608
+ token self-identifies without a second `octwin whoami` (best-effort; a network failure
609
+ never fails the save).
610
+ - **`validate --remote` surfaces warnings + the data-store trap.** The remote validate now
611
+ renders advisory `⚠` warnings (which don't block deploy) and, for a pack declaring
612
+ `required_adapters: [data-store]`, reports the missing-data-store error the deploy would
613
+ have 409'd on plus a warning that an `xrm.yaml` pack usually shouldn't declare that
614
+ adapter at all. It also now catches (offline) a list-form `entities:` block and a
615
+ `localized: true` field seeded with a bare string traps that previously surfaced only
616
+ mid-seed at deploy.
617
+
618
+ ## [0.1.13] - 2026-07-22
619
+
620
+ ### Added
621
+ - **A headless media loop — produce + send.** Two paired additions close the coverage hole where any
622
+ media-collect flow (e.g. `activate-app`'s registration / ID uploads) stalled at the upload prompt
623
+ because the CLI could neither *make* nor *send* an image:
624
+ - `octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]` — AI-generates an
625
+ image on the platform (needs a `media:generate`-scoped token), stores it as a public asset, and prints
626
+ its `MEDIA-` handle + serve URL. `--out` downloads the bytes to a file (WhatsApp renders only
627
+ `.png`/`.jpg`); `--json` emits `{ media_id, url, mime, width, height, bytes }`.
628
+ - `octwin chat --media <file|media-id>` uploads a local file (or a media id from
629
+ `media generate --json`) as an image/document/audio inbound; any accompanying `"message"` rides as
630
+ its caption. The platform's media pipeline folds the upload into a running collect, so media flows
631
+ are now fully drivable headlessly.
632
+
633
+ ### Changed
634
+ - **The KB-drift nudge now names what changed.** With the platform serving per-entry content hashes, the
635
+ post-command nudge appends a `(N changed · M added · K removed)` summary instead of a bare hash pair.
636
+ - **`octwin platform-kb` prints a changelog on pull.** Instead of only a doc/catalog count, the pull now
637
+ diffs the fresh index against your last pull and lists exactly which docs/catalogs were added (`+`),
638
+ changed (`~`), or removed (`-`) — so a replaced schema shape is visible, not silent.
639
+
640
+ ## [0.1.12] - 2026-07-22
641
+
642
+ ### Fixed
643
+ - **The KB-drift nudge now fires after `chat` too.** `chat` the command a debugging session runs most —
644
+ was missing from the networked-command list, so a chat-heavy session never noticed the platform's
645
+ capability reference had moved. The nudge also now shows the concrete drift
646
+ (`old-hash new-hash`) so you can see it's real, not a heuristic.
647
+
648
+ ### Changed
649
+ - **Every networked command announces what it's doing before it does it.** `chat` prints
650
+ `→ Connecting to <tenant>/<project> as '<handle>' …` before opening the stream and
651
+ `… delivered waiting for the reply (up to Ns)` after the send; `status` / `whoami` /
652
+ `platform-kb` / `records` / `logs` / `cases` each print a one-line `→ …` header naming the
653
+ action and target before the first network call no more silent seconds followed by a result
654
+ (or a hang with no clue what was being attempted). JSON modes (`--json`) stay clean for piping.
655
+
656
+ ## [0.1.11] - 2026-07-21
657
+
658
+ ### Fixed
659
+ - **Auth-failure hint on every command, not just the inspect family** (author-feedback A7). `validate`,
660
+ `deploy`, `status`, and the single-record read now print the scope/transient explanation on a 401/403
661
+ (the shared `authFailureHint`), instead of a bare `HTTP 401` that left it ambiguous whether the token
662
+ lacked a scope or the platform hiccupped.
663
+
664
+ ## [0.1.10] - 2026-07-21
665
+
666
+ The capability reference is now **pulled, never bundled** — the authoring skill dropped its committed KB
667
+ snapshot, so `octwin platform-kb pull` is the single source of truth, and the CLI watches it for drift.
668
+
669
+ ### Added
670
+ - **KB-drift nudge** — after any command that already talks to the platform (`deploy` / `status` /
671
+ `test` / `validate --remote` / `records` / `cases` / `logs` / `whoami`), the CLI does one tiny
672
+ `octwin-platform-kb?meta=1` GET and prints a one-line "the platform capability reference changed —
673
+ `octwin platform-kb pull`" when the platform's `content_hash` no longer matches your last pull.
674
+ Fail-silent + TTY-only + no-op until you've pulled once — the sibling of the existing CLI-upgrade notice.
675
+
676
+ ### Changed
677
+ - **`octwin platform-kb pull`** now records the reference's `content_hash` in `.octwin/platform-kb/index.json`
678
+ (the fingerprint the drift check compares against).
679
+
680
+ The conversation-debugging release — driven by the second author-feedback round (xpeng-egypt),
681
+ which found the author→validate→deploy loop solid but multi-turn debugging impossible from the CLI.
682
+
683
+ ### Added
684
+ - **`octwin chat --tap "<tap-id>"`** — press a rendered button / list row (sends the web channel's
685
+ `type:'interactive'` inbound). Paired with full render printing, any tap-driven flow is now drivable headlessly.
686
+ - **Full render printing** — every reply prints its complete interactive content: list sections/rows,
687
+ buttons, and carousel cards, each with its **tap id**; header/footer/list-button text included.
688
+ - **`--json` on `chat` / `logs` / `cases`** — raw SSE envelopes (chat) or raw event/case payloads.
689
+ - **`octwin cases [caseId] [--queues]`** — inspect casework (support tickets): the inbox, one case +
690
+ timeline + applicable decisions, or queue keys with open counts. Casework packs previously had zero
691
+ data-inspection path (`records case` was a dead end).
692
+ - **Per-subcommand `--help`/`-h`** — intercepted before any network or auth work (previously
693
+ `octwin logs --help` made a network call and could die with a 401).
694
+ - Multi-message turns: `chat` now collects **all** of a turn's renders (quiet-gap detection) instead
695
+ of cutting at the first one.
696
+
697
+ ### Changed
698
+ - **`octwin logs`**: the conversation list shows contact handle + last activity and filters with
699
+ `--as <handle>`; the timeline prints **full** event text (no more 240-char truncation) plus the
700
+ renders each turn actually sent; footer points at `--json` / `mastra-messages` for verbatim payloads.
701
+ - **Network failures name the target URL** (`platform unreachable at <url>`), and 401s explain
702
+ invalid-token vs one-off transient instead of a bare status code.
703
+ - `octwin records` on an unknown entity now surfaces the server's error body, and for `case`/`cases`
704
+ points at `octwin cases`.
705
+
706
+ ### Fixed
707
+ - **Multi-turn chat actually works.** Two bugs made consecutive `octwin chat` calls look like "every
708
+ call starts fresh": (1) the idempotency `local_id` was a **constant** per handle, so the platform's
709
+ 5-minute inbound dedup silently dropped the second message — a fresh id is now minted per call;
710
+ (2) a fresh SSE connect **replays** recent history and the CLI printed the first render frame — i.e.
711
+ the *previous* turn's greeting — as the reply; the CLI now drains the replay and only accepts frames
712
+ newer than the connect boundary. The platform always kept one open conversation per `--as` handle —
713
+ the same handle genuinely continues the same conversation now.
714
+
715
+ ## [0.1.8] - 2026-07-21
716
+ - Global-first install guidance (`npm i -g octwin-cli`); upgrade notice suppressed under `npx`.
717
+
718
+ ## [0.1.7] - 2026-07-21
719
+ - `--version`/`-v` flag; daily fail-silent, TTY-only "newer version available" notice.
720
+
721
+ ## [0.1.6] - 2026-07-21
722
+ - `octwin init` scaffolds a home-hub menu (`home` list_picker + example `browse` tool) instead of a bare greeting.
723
+
724
+ ## [0.1.5] - 2026-07-20
725
+ - `octwin validate --remote` (the platform's FULL manifest + flow-DSL check, all errors at once);
726
+ headless debugging commands `octwin records` / `octwin logs` / `octwin chat`; `octwin test` became
727
+ an alias for `validate --remote`. First author-feedback round.
728
+
729
+ ## [0.1.4] - 2026-07-20
730
+ - Authoring reliability + DX fixes from the first end-to-end external-pack test.
731
+
732
+ ## [0.1.3] - 2026-07-20
733
+ - `octwin platform-kb pull` — pull the platform capability reference for the authoring skill.
734
+
735
+ ## [0.1.2] - 2026-07-20
736
+ - Proper npm README.
737
+
738
+ ## [0.1.1] - 2026-07-20
739
+ - Initial npm publish: `init` / `validate` / `login` / `whoami` / `deploy` / `status`, `oct_` deploy tokens.