octwin-cli 0.8.16 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,1028 +1,1129 @@
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
- ## [Unreleased]
9
-
10
- ## [0.8.16] - 2026-10-07
11
-
12
- ### Changed
13
- - **`octwin performance --channel` marks a figure that is not narrowed.** Capacity has no channel, so a
14
- band narrowed to one prints the capacity tile with `(all channels)` rather than letting it pass as the
15
- channel's own.
16
- - **`octwin performance` prints a rate's change in points.** The platform
17
- replaced each indicator's `delta_pct` with `change` (one rule for the console, the server and the CLI): a conversion rate going
18
- 42.9% → 57.1% now reads `+14.2 pts vs prev`, not `+33.1%`, and an NPS from −25 to +40 reads `+65`,
19
- not `-260%`. A count still reads as a percent; growth from zero reads `new`.
20
-
21
- ## [0.8.15] - 2026-10-06
22
-
23
- ### Fixed
24
- - **`octwin records` said nothing useful for a pack with no record types of its own.** Its message read
25
- `has_xrm`, which the platform never sent, so it could not show. The platform sends it now; the CLI
26
- says the pack declares no record types and that platform records still read by name
27
- (`octwin records order | product | case`). `records create` and `records tasks` read `has_data` —
28
- "no AI Agent is loaded in this project" — instead of the same unsent field.
29
-
30
- ## [0.8.14] - 2026-10-06
31
-
32
- ### Added
33
- - **`octwin seed --reset`** — archive every row the pack's demo loaded, then load it again. Needed
34
- once for demo rows seeded before 2026-10-05 (the seed says so), and a clean slate on demand; records
35
- a customer made are never touched.
36
- - **`octwin records <entity> --search "<words>"`** — the entity's ranked search (fuzzy, plus semantic
37
- when the entity declares it), best match first, each row printed with its score. The platform has
38
- served it to the console for months; the CLI never asked. `--json` returns the rows with their scores.
39
- - **`press:<label>` in `octwin chat --script`, and `--press <label>`.** Taps the button of the reply just
40
- received by its label — exactly, or the same words without emoji or case, or the one label containing
41
- them. A form or checkout button's tap id carries its run id (`t:resume:…:run_id=…`), which exists only
42
- once the reply arrives, so a script could not get past the first form step: `tap:` needs an id nobody
43
- can write in advance. No match, or several buttons, stops the script and lists the buttons. A lone
44
- `--press` picks from the reply the conversation is waiting on. The rules are the AI Author's `press`,
45
- byte for byte (`src/lib/press.ts`). An older CLI sends a `press:` line as a typed message.
46
-
47
- ### Fixed
48
- - **`octwin chat` prints a card's link buttons and its forms.** A button that opens a link prints as
49
- `[link] title → url` (a card may now carry links beside its reply buttons), and a form prints as
50
- `[flow form] …` — it compared the hint type `'flow'`, which the wire never sends (`form_flow`).
51
- - **`--json` output always parses.** `records --json` and `projects create --json` printed a
52
- `→ Reading …` line in front of their JSON, and `logs --json` with no conversations printed a sentence
53
- and no JSON at all (reported 2026-10-01). Under `--json`, stdout now carries the result and nothing
54
- else — progress, tables and hints go to stderr — and every write verb of a JSON command (`records
55
- create`, `work note`, `agents set`, `catalog stock`, `scheduling rule add`, `projects rm --yes`, …)
56
- returns what the platform answered. A test runs every JSON command and parses its stdout.
57
- - **`octwin chat` prints the pictures, files, voice notes and map pins a reply carries.** Only a reply's
58
- text and buttons were printed, so a reply that sent a picture read as a caption with nothing under it.
59
- Now `[image] <path>` (without the signed query), `[file] <name> → <url>`, `[voice note] 12s → <url>`,
60
- `[location] <name> (lat, long)`, and `(image)` on a carousel card that has a picture
61
- (`src/lib/media-lines.ts`).
62
-
63
- ### Changed
64
- - **`--json` on a command that has no JSON output is refused by name** (`whoami`, `deploy`, `validate`,
65
- …), with the list of commands that have it. It used to print the prose to stderr and leave stdout
66
- empty. `chat --json` and `login --json` print one JSON object per line, as before.
67
- - **Under `--json`, the progress lines move from stdout to stderr.** A script that read them from stdout
68
- reads them from stderr now; human output (no `--json`) is unchanged.
69
- - **`octwin pull` can pull an Octwin (first-party) pack.** The platform now lets a workspace read the
70
- source of its own packs and of Octwin's — never another workspace's — and the 403 hint says so. A
71
- platform change: an older CLI pulls first-party packs too, it only prints the older hint.
72
-
73
- ## [0.8.13] - 2026-09-27
74
-
75
- ### Added
76
- - **`octwin records archive <id>` / `restore <id>`.** Archive is how a record leaves use — test data
77
- included: it drops out of every list, count and report and fires the entity's `on.archived`
78
- reactions, exactly like the console's Archive; `restore` undoes it. There is no delete, on the
79
- platform or here, and `octwin records --help` now says so.
80
-
81
- ### Fixed
82
- - **`octwin chat` said the AI Agent wrote every message.** A reminder that fired mid `--script` (a
83
- `schedule_notify`, a reaction) printed as `← <body>`, was taken for the turn's reply, and ended
84
- the turn on it — so the real reply looked lost while `octwin logs` showed the flow had worked.
85
- Each line now says who sent it: `← [notify] …` (not the turn's answer — the wait for the real
86
- reply goes on), `← [operator: Name] …` (a person in Live chat), `← [platform] …` (a handoff
87
- joining or returning); plain `← …` is the AI Agent. After the last turn the stream stays open
88
- 3 s more, so a late message prints (marked "after the last turn") instead of reaching a closed
89
- connection. Needs a platform that stamps `source: 'notify'` on web renders; an older one just
90
- prints notifies unlabelled, as before.
91
- - **The "no such template" hint named a file the platform never loads.** It said
92
- `templates/<name>.yaml`; pack templates load only as `templates/<name>.template.yaml`, so a
93
- template saved as the hint said was silently ignored.
94
-
95
- ### Changed
96
- - **`octwin validate` runs the platform's FULL check by default — and needs no login for it.** The
97
- offline checks are a subset, and a subset that passes read as "deploy will pass": it passed a pack
98
- the full check refused (a scaffold flow left behind with Arabic copy only, in a pack that ships
99
- English). Now `validate` runs the offline checks, then the full one: through your workspace when you
100
- have a token AND a `--project` (which also confirms the project exists), otherwise through the
101
- platform's public validate route with no credential. When it cannot reach the full check — no url,
102
- offline, rate-limited, an older platform — the LAST line says *"only the OFFLINE check ran — a
103
- PARTIAL check"* and why, and it exits 0 as before. `--remote` now means "the full check is
104
- required" (not reaching it is an error); the new `--offline` runs only the subset.
105
- - **The scaffold's example `browse` flow says it is an example.** Its header, its copy file and its
106
- `manifest.yaml` line now say to replace it, or delete the flow, every `browse.locale.*.yaml` and the
107
- `flows:` line together — a leftover never loads, but a second language makes it fail validation.
108
-
109
- ## [0.8.12] - 2026-09-25
110
-
111
- ### Changed
112
- - **The package pointed its `homepage` at npm — the page it is rendered on.** It is
113
- <https://octwin.ai> now, and the README's Links section gains the platform, the guides and the
114
- marketplace. The README describes Octwin at length and linked only to npm and nodejs.org.
115
- - **`octwin usage` with a project-pinned token reports that project.** The platform now treats a
116
- token's project pin as a ceiling on every route, so a pinned token is refused on the
117
- workspace-wide rollup. `usage` asks `whoami` for the pin and reads the project view instead of
118
- printing a 403 — which is what its help text (`project if pinned, else workspace`) always said.
119
-
120
- ### Fixed
121
- - **`octwin chat` sat out its whole 45-second wait on every message during a live handoff, and
122
- read as hung.** While a person holds the conversation (or one has been asked for) the AI Agent
123
- deliberately answers nothing, and the CLI could not tell that from a slow pack. The platform now
124
- says so with a `held` event on the web stream; `chat` prints
125
- *"⏸ a person is holding this conversation — the AI Agent will not reply…"* (or *"waiting for a
126
- person to join…"*), still collects any operator line replayed by that message, and returns. A
127
- held turn is not a failure: it exits 0, and `--script` carries on to the next line, so the
128
- customer's side of a handoff can be scripted. Needs a platform that sends `held`; an older one
129
- behaves as before. Reported in `xpeng-egypt/FEEDBACK.md`, session 3.
130
-
131
- ## [0.8.11] - 2026-09-07
132
-
133
- ### Fixed
134
- - **`octwin login` erased your own tooling report — the CLI was blanking what it had just
135
- told the platform.** Two calls built their `authorization` header by hand instead of going
136
- through `authHeaders`: `login`'s post-save "here's what your token reaches" echo, and
137
- `chat`'s tenant/project derivation. Both authenticate, so both stamp the token's client
138
- report (added in 0.8.9) — and a hand-built header stamps **NULL**, wiping the CLI version
139
- and reference hash the platform had recorded.
140
-
141
- Caught within an hour of 0.8.9 reaching staging: a workspace reported `0.8.9 · reference
142
- current` at 12:16 and read `not reported` at 12:34. That looked exactly like the documented
143
- "the column holds what ran LAST" behaviour and was really the CLI erasing its own row.
144
- `cli-routes.test.ts` now fails on any hand-built bearer header, with a non-vacuity floor —
145
- this package sits outside the repo's eslint scope, so a text guard is the only rung available.
146
-
147
- ## [0.8.10] - 2026-09-07
148
-
149
- ### Fixed
150
- - **`octwin memos` failed with `400 project required` for a tenant-wide token — the exact
151
- command the nudge tells you to run.** Every networked command can end with
152
- *"✉ N memos from the platform — read them: `octwin memos`"*, and running precisely that with
153
- an unpinned token answered a 400. Measured on staging: the nudge fired during a
154
- `deploy --project …` (which supplied a project) and then named a command that could not run.
155
-
156
- A memo has no project dimension to scope by — neither `pack_memos` nor `pack_memo_reads`
157
- carries a `project_id`, and the read ledger is keyed by tenant — so the three call sites now
158
- use `/api/self/t/memos`. Passing `--project` still works and is no longer needed.
159
-
160
- ## [0.8.9] - 2026-09-07
161
-
162
- ### Added
163
- - **Every authenticated request now says which CLI and which capability reference you are on.**
164
- Two headers off `authHeaders` — `x-octwin-cli` and `x-octwin-kb` (the `content_hash` of your
165
- pulled reference) — so `deploy`, `validate --remote`, `status`, `chat`, `logs` and `memos` all
166
- carry them, from one place. The platform stores them against your deploy token and shows them in
167
- the console (Settings → API tokens, and Platform → Packs → Feedback → Author tooling for us).
168
-
169
- **Why the platform needs this from you.** Almost every field report so far turned out to be
170
- either already fixed in a newer CLI or written against a stale reference — three very different
171
- answers that look identical without these two facts. They were captured on `octwin feedback`
172
- only, so an author who never filed a report was invisible. Nothing here affects auth: a request
173
- that sends neither header authenticates exactly as before, and a value that could not travel as
174
- a header (a corrupt `package.json`, an interrupted pull) is dropped rather than sent.
175
-
176
- ### Fixed
177
- - **The upgrade notice was skipped under `npx` — silencing the population most likely to be
178
- stale.** The premise was that "`@latest` already resolves the newest", which holds for
179
- `npx octwin-cli@latest` and is false for the bare `npx octwin-cli` the docs put in front of
180
- external developers: npm serves that from its **cache**, so it can be arbitrarily old. Under
181
- `@latest` the check is self-silencing anyway (you are already newest), so keeping it costs
182
- nothing — only the advice line differs, because there is no global install to reinstall.
183
-
184
- ## [0.8.8] - 2026-09-06
185
-
186
- ### Added
187
- - **`octwin login` can sign you in through a browser — no token is pasted anywhere.** Run it with
188
- no `--token` and it prints an approval link:
189
-
190
- ```
191
- Open this link to approve: https://app.octwin.ai/cli-login?code=WDJB-MJHT
192
- Code shown on that page: WDJB-MJHT
193
- ```
194
-
195
- A workspace admin opens it, checks which machine is asking, and clicks Approve; the CLI collects a
196
- 30-day token by itself and saves it exactly where `--token` would have. `octwin login --token oct_…`
197
- is unchanged, and CI should still skip login entirely and set `PACK_PLATFORM_URL` + `PACK_TOKEN`.
198
-
199
- **The wait is bounded and resumable, because this CLI is mostly driven by an agent.** An agent's
200
- shell call is killed after about two minutes, and a wait that outlives its caller is worse than a
201
- short one — the process dies mid-handshake and the code the human is still typing belongs to
202
- nothing. So `login` waits 90 seconds, then exits **75** (`EX_TEMPFAIL`) with the handshake kept in
203
- `~/.octwin/pending-login.json`; running it again resumes the **same** code, which the platform
204
- keeps alive for ten minutes. `0` still means signed in and `1` still means failed, so an agent
205
- branches on the exit code instead of parsing prose. `--wait <seconds>` overrides the 90 (`--wait 0`
206
- prints the code and returns at once), and `--json` prints the same facts as an object.
207
-
208
- **Approve only a link you opened because you ran the command.** The link carries the code
209
- (RFC 8628 `verification_uri_complete`), so the click *is* the consent: approving a link somebody
210
- sent you would connect their machine to your workspace. The console says so on the page. The code
211
- is printed here too, and shown there, so you can always tell one request from another.
212
-
213
- The pending handshake gets its own file rather than a new key in `credentials.json`: every value
214
- in that file is a token, and its one reserved key (`default_url`) is collision-free only because a
215
- platform url always contains `://`. A second value shape there would end that argument.
216
-
217
- - **A 401 now points at `octwin login`, not at the console.** With a 30-day browser token, renewing is
218
- one command; the old hint sent people to mint a replacement by hand. Mint a token yourself if you
219
- want a different lifetime — the console offers 30 / 90 days / a year / never.
220
-
221
- ## [0.8.7] - 2026-09-06
222
-
223
- ### Added
224
- - **`validate` now reads a map's KEYS, not only its values.** The declaration walker reported four
225
- things — an unknown key, a missing `required` key, a wrong scalar type, a value outside a closed
226
- `enum` — and every one of them is about a VALUE. But a map's keys carry meaning in several
227
- declarations (`roles.yaml` grant resources and verbs, `xrm.yaml` entity names), and those keys had
228
- no check at all. Rule 5 replays `propertyNames` — both the `enum` and the `pattern` form.
229
-
230
- This is why publishing the RBAC vocabulary was worth doing. A pack author wrote `case:` where the
231
- grant key is `record.case`, passed offline `validate` clean, and learned the truth one bad guess at
232
- a time from `--remote` round-trips. They gave up on custom RBAC over it. The check now says
233
- `not a valid key here — must be one of "record.case", …` before a deploy is attempted.
234
-
235
- It stays inside the file's false-positive rule because it is a **faithful replay**: the same `enum`
236
- membership and the same JS regex engine Zod itself runs, over a pattern the platform generated. A
237
- pattern this engine cannot compile is treated like a combinator — walk away, do not guess.
238
-
239
- Verified 2026-09-06 against the shipped binary: across all 23 marketplace packs rule 5 reports
240
- **nothing**, with 10 of the 12 pulled declaration schemas carrying `propertyNames` (so the silence
241
- is a result, not an unexercised branch). A deliberately broken key is caught on both branches — a
242
- role named `Regional-Agent` against `^[a-z][a-z0-9_]*$`, and a grant key `case:` against an enum
243
- vocabulary — each naming the file, the path and the rule it broke.
244
-
245
- ### Fixed
246
- - **`deploy` had stopped printing its seed counts entirely.** `printDeploySuccess` read
247
- `r.summary.records` / `.updated` / `.images` / `.rules` / `.failed`, but the deploy route's
248
- `summary` is a run-log **string** (`"clinic v1.2.0 — {…}"`), not an object. So every field was
249
- `undefined`, `parts` stayed empty, and the `if (parts.length)` guard turned *"I am reading the
250
- wrong field"* into *"there was nothing to say"* — the block printed nothing and looked like a
251
- deploy that simply seeded no rows. The counts have been on `r.seeded`, keyed by seed kind, and
252
- `printSeedCounts` already renders them (filtering zeros, which is what makes a partial seed
253
- legible). The dead block is gone and `printSeedCounts` is called instead.
254
-
255
- Same shape as the bug 0.8.6 fixed in `validate`: *no data* and *no problem* rendering identically.
256
- Two independent places in this CLI reached it within one week.
257
-
258
- ## [0.8.6] - 2026-09-04
259
-
260
- ### Changed
261
- - **`validate` can no longer print a `✓` for a check that did not run.** Each of the eight
262
- checks now RETURNS an `Outcome` — `passed`, `findings`, or `not-run` — and a single printer
263
- renders all of them, groups the skips by reason, and computes the exit code. Previously each
264
- check printed its own `✓` inside the `if` that held its data, with a `skipped[]` array and a
265
- `skipReasons` Map maintained alongside; it was correct because someone was careful, and the
266
- next check added was one `if` away from lying again.
267
-
268
- It had lied twice, in the same shape: the KB-dependent block once printed ONE `✓` above six
269
- checks whose own `✓`s lived inside their `if`s (so a run with no pulled KB read as "one check,
270
- passed", and an entire backlog batch reached production that way), and `yamlDocs()` once
271
- dropped an unparseable file so `validate` printed all-green over a pack the platform's importer
272
- then refused. **An unparseable file is not "no findings", it is "no idea", and those must never
273
- print the same** — a check that returns nothing now cannot reach the `passed` branch at all.
274
-
275
- Output is unchanged for a normal run. `passed` carries its own line, so the reserved-entity
276
- check keeps the narrower wording it uses when the catalog rules are absent from an older pull.
277
- `--require-kb` is now one line over the returned skip list.
278
-
279
- Verified 2026-09-04: all 23 marketplace packs exit 0 with no findings; a pack with no pulled KB
280
- prints zero `✓` for the six KB-dependent checks plus one grouped ⚠ naming all six, and
281
- `--require-kb` exits 1; a duplicate YAML key still exits 1 naming the file.
282
-
283
- ## [0.8.5] - 2026-09-02
284
-
285
- ### Added
286
- - **`octwin work --unrouted`** — the inbox filtered to items in NO queue. The route has always
287
- supported the filter; the CLI never passed it, so `--queues` could tell you a count and not
288
- show you the items behind it.
289
-
290
- ### Changed
291
- - **`octwin work --queues` leads with the unrouted count.** It was already printed, as
292
- `(unrouted: N open)` tucked under the queue list. A routed item sits in a list someone works;
293
- an unrouted one sits in nobody's, so it is the only line on that screen that means "these are
294
- lost". It is now a warning block that names the likely cause and points at `--unrouted`. Still
295
- silent at zero — a clean project stays quiet, or the warning stops meaning anything.
296
-
297
- ## [0.8.4] - 2026-09-02
298
-
299
- ### Fixed
300
- - **A deploy whose seed failed still led with `✓ Deployed`.** The step errors were never
301
- missing — `readDeployProgress` has collected every `status:'error'` frame into `stepErrors`
302
- since 0.1.14, printed them, and exited non-zero. They were printed *after* `printDeploySuccess`,
303
- so the first and largest line said `✓ Deployed` and the ⚠ came underneath. A pack author
304
- reported their deploy as a clean success while its demo seed had failed; they were reading the
305
- transcript exactly as written. The count now goes INTO the headline, which reads
306
- `⚠ Deployed <id>@<v> onto <target> WITH n failure(s) — data may be incomplete`. `octwin seed`
307
- gets the same treatment. *A signal that arrives after the verdict is not a signal.*
308
- - **The non-streaming deploy and seed paths had no step-error check at all.** With no SSE frames
309
- to read, a failed seed printed a clean `✓` on that path even after the streaming one was fixed
310
- in 0.1.14. Both now read `seed_failed` from the response body (new in the platform alongside
311
- this release), print each failed kind with its reason, and exit non-zero.
312
- - **A partial demo seed is now reported as partial.** The platform no longer abandons the rest of
313
- the seed when one demo row throws, so `Seeded:` gains an `n row(s) FAILED` part — without it a
314
- partial seed reads as a complete one that simply had fewer records than the author wrote.
315
-
316
- ## [0.8.3] - 2026-09-02
317
-
318
- ### Fixed
319
- - **`validate` printed all-green over a pack the platform could not import.** Every YAML file was
320
- parsed inside a `try { … } catch { return [] }`, so a file that failed to parse was silently
321
- DROPPED from the list and every later check skipped it. The `catch` was justified in a comment —
322
- "a syntax error is the structural gate's to report" — and that was not true: the structural gate
323
- (`validatePackBundle`) checks paths, extensions and traversal, and never parses YAML.
324
-
325
- Measured 2026-09-02: a duplicate `s_header:` key in a locale file passed `octwin validate` twice
326
- and was then refused by the platform importer with `Map keys must be unique at line 24,
327
- column 3` — the same message, from the same `yaml` version the CLI already had. The author's only
328
- clue was a red box in the console naming no file.
329
-
330
- YAML syntax is now its own announced gate that FAILS: it names each unparseable file with the
331
- parser's own message and exits non-zero, before any check reads a document. An unparseable file is
332
- not "no findings", it is "no idea", and the two must never print the same.
333
-
334
- - **Three more readers that turned "I could not read this" into a normal answer.** Swept every
335
- discarding `catch` in the CLI after the above; three collapsed a real fault into an ordinary state:
336
- - `readCreds` returned `{}` for a CORRUPT `credentials.json` as well as an absent one, so a token
337
- the author knows they saved simply stopped being found and the advice was "log in" with no
338
- mention of why. Now names the file and the parse error, once per process (it has three call
339
- sites and one broken file is one problem).
340
- - `readLocalKb` returned null for a half-written `.octwin/platform-kb/index.json` as well as a
341
- never-pulled one — so an interrupted pull read as "you have not pulled yet" and the author
342
- re-ran the command that left it that way. [`kb-path.ts`](src/lib/kb-path.ts) states this rule
343
- for the DIRECTORY lookup; the index read beside it kept collapsing anyway. It now says which.
344
- - The deploy progress stream discarded unparseable SSE frames silently. Skipping one is right;
345
- skipping ALL of them left `terminal: null`, reported as "the stream ended without a terminal
346
- event" — true, and indistinguishable from a dropped connection. The count is now named when
347
- (and only when) no terminal frame arrived, pointing at a CLI older than its platform.
348
-
349
- The other five discarding catches were read and LEFT: each returns a documented fallback for a
350
- state that is genuinely ordinary (no update-check network, a heartbeat SSE frame with no `data:`).
351
- `scripts/` was swept too and is clean by design — `kb-dataflow.mjs` skips a fence it cannot parse,
352
- and `check-kb-examples.mjs` holds a non-vacuity FLOOR of 130 parsed fences so the skip cannot go
353
- quiet. That is the better pattern where a skip is legitimate.
354
-
355
- ## [0.8.1] - 2026-08-27
356
-
357
- ### Fixed
358
- - **Logging in made `platform-kb pull` WORSE.** The command preferred the tenant-scoped route
359
- whenever a token was saved, and that route is guarded by `pack:deploy` — so a developer holding a
360
- token without that scope got a **403 on a pull that succeeds with no token at all**. It is the
361
- likeliest token a new developer holds: no console preset could even reach `pack:deploy` before
362
- 2026-08-26. `platform-kb` now always uses the anonymous route and never sends a credential.
363
- `--token` is accepted and ignored so existing scripts keep working.
364
-
365
- The platform serves the *same* bundle from all three of its views — its route file states the
366
- payload is identical and tenant-independent — so the authed route returned nothing extra and the
367
- credential could only ever subtract. The two reasons the old code gave did not survive checking:
368
- "keeps the author's own instance the source of truth" is vacuous (both routes are the same
369
- instance), and "works against platforms that predate the public rung" is a backward-compatibility
370
- shim this codebase does not carry.
371
- - **The drift nudge now works without a token.** It polled through the same authed route, so an
372
- author inspecting data with a narrow token was told, every time, that it *could not* check —
373
- advice in place of the signal. Both the poll and the apology are gone: the poll is anonymous, so
374
- it simply works.
375
-
376
- ## [0.8.0] - 2026-08-27
377
-
378
- ### Added
379
- - **Five new command families, closing the gap between what a token reaches and what the CLI can
380
- ask.** A **Full CLI access** token spans the whole scope registry, and four of those domains had
381
- no command at all — so the reach existed and there was nothing to type.
382
- - `octwin automation [campaigns]` — every job the pack's declaration produced, with status,
383
- interval and last result (matched / acted / errors), under a health line whose counts come from
384
- SQL rather than from filtering the page (the job list is capped server-side, so a client-side
385
- count would depend on the cap). Writes: `run` · `pause` · `resume` · `send <campaignId>`.
386
- - `octwin integrations` — what the pack **declares** beside what is actually **configured**, in
387
- one view, because a connection declared and never configured is the commonest reason an
388
- integration silently never fires and neither list alone can show it. Plus
389
- `preflight <key>` (diagnosis, no outbound call, `integrations:read`), `test <key>` (a live call,
390
- `integrations:write`, exits 1 on failure), `deliveries [id]`, `retry|cancel|send-now <id>`,
391
- and `events` for what arrived inbound.
392
- - `octwin journeys [id] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]`
393
- — the declared journeys, measured. Same flag grammar as `octwin analytics` on purpose: a journey
394
- funnel and an entity funnel are the same question about different subjects. `--definition` is
395
- the one view that works with no traffic.
396
- - `octwin performance [--detail]` — the project's business indicators, each with its delta against
397
- the previous window and the `why` naming the declaration it came from. Needs `records:read`, not
398
- a `performance:*` scope (there is none), so a read-only token already reaches it.
399
- - `octwin usage` — model calls, tokens and cost, by model / kind / agent / channel; project when
400
- one is pinned, workspace otherwise. Needs no particular scope. **Model spend only** —
401
- WhatsApp/Meta message billing is operator-only and outside the token scope registry, which the
402
- output says rather than leaving as an absence.
403
- - **`octwin automation run|pause|resume|send` accept the declaration KEY.** The routes take a UUID
404
- and answer a bare *"Malformed identifier in the URL"* 400 for anything else — but the key
405
- (`cart_recovery_nudge`) is what the list prints and what the author wrote in their own YAML. The
406
- key is now resolved against the list route, and an unknown one fails naming the keys that exist.
407
-
408
- ### Fixed
409
- - **A body-less write sent `content-type: application/json` and no body, so Fastify answered a bare
410
- `400 Bad Request`.** `apiSend` set the header unconditionally; every caller until now passed an
411
- object, so the branch was dead and the bug latent. The first body-less POST (`automation run`) hit
412
- it immediately. The header is now omitted when there is no body — rather than each caller learning
413
- to pass `{}`, which is the per-site divergence that helper exists to prevent.
414
- - **Four URLs were exempt from the route guard.** `cli-routes.test.ts` reads a template literal in
415
- the first argument position, so `apiGet(base, t)` — a bare identifier — was never checked. That
416
- covered the `scheduling` engine-state and `agents` roster calls, and would have covered the new
417
- `journeys`, `performance` and `usage` ones. All now spelled as literals: the guard checks **62**
418
- URLs, up from 45.
419
-
420
- ## [0.7.3] - 2026-08-26
421
-
422
- ### Changed
423
- - **A marketplace listing request is now OPT-IN: `octwin deploy --request-listing`.** A plain
424
- `octwin deploy` says nothing about the public marketplace — it neither asks for a listing nor
425
- gives one up. `--withdraw-listing` retracts, including an approved listing.
426
- **Why:** the request was read from the manifest's `listing.public` on every deploy, and deploying
427
- is how you *test*. 17 of the 23 published packs carry that key, so an edit-deploy-chat loop filed
428
- a review request every time round. Worse, the *absence* of the key meant **retract** — so an
429
- ordinary deploy of a pack whose manifest you had not yet annotated could wipe a listing you
430
- already had. `--request-listing` still requires `listing.public: true` in the manifest (the
431
- manifest states that the pack is a product; the flag is you choosing to ask), and the flag
432
- without the key is a clean error rather than a silent no-op.
433
- - **The listing verdict distinguishes "you submitted this" from "your approval expired".** They are
434
- both `pending` and they mean opposite things to an author. A deploy that changes the content of an
435
- approved pack now says so in those words — an approval covers only the content it was made
436
- against, so it cannot survive an edit.
437
-
438
- ### Added
439
- - **A one-line nudge when your manifest asks and your deploy did not.** A pack carrying
440
- `listing.public: true` that has never been submitted now prints the flag to use, so an opt-in
441
- default cannot turn into waiting for a review nobody requested.
442
-
443
- ### Fixed
444
- - **The "newer CLI" and "KB drifted" notices now print when a command FAILS.** All three nudges
445
- (CLI upgrade · KB drift · unread memos) were awaited at the very end of `main()`, which silently
446
- meant *only on success* — `die()` is a hard exit, so a failed deploy, an auth error or a bad
447
- manifest printed none of them. That is backwards: a stale capability reference is a leading cause
448
- of the failure an author is staring at, so the run that most needed the nudge was the only one
449
- that never got it. They are now computed before the command and printed however it ends.
450
- - **The CLI-upgrade notice is no longer hidden from piped output.** It began with
451
- `if (!process.stdout.isTTY) return`, so every agent-driven and CI run — the readers that cannot
452
- notice an outdated CLI on their own — was the one audience never told. Its two siblings had
453
- already dropped that gate for exactly this reason; now all three agree. Still skipped under
454
- `npx`, where there is nothing to upgrade.
455
- - **`octwin login` now reports KB drift too.** It is networked and usually the first command of a
456
- session, i.e. the best possible moment to say the reference moved, and it was excluded.
457
- - **A memo poll can no longer fail the command it was annotating.** It went through `apiGet` →
458
- `fetchOrDie`, whose job is to `die` on a network failure — so an unreachable platform turned an
459
- observer into a command failure. It now uses a plain, timed, fail-silent fetch.
460
- - **A failing command reports its real exit code again on Windows.** Moving the polls before the
461
- command made `process.exit()` abort with `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)`
462
- and return **127** instead of 1 — so CI saw neither success nor the documented failure code. The
463
- CLI now unwinds through a `CliExit` and lets the event loop drain, which is the only teardown
464
- that is clean once the polls have opened sockets. `--check`'s exit 2 and the streaming commands
465
- were re-verified.
466
-
467
- ## [0.7.2] - 2026-08-20
468
-
469
- ### Fixed
470
- - **`octwin feedback` now tells you what is already waiting, on stdout.** It discarded the submit
471
- response entirely — destructuring `{ status, json }`, checking the status and never reading the
472
- body — then printed *"a reply arrives as a memo, this CLI will tell you when one is waiting"*. The
473
- platform started returning the unread counts on that response (2026-08-20), so the answer was
474
- there to print and was being thrown away.
475
- **Why it matters more than it sounds:** `notifyIfMemosWaiting` already covers this command and was
476
- firing correctly — but it writes to **stderr**, and the authors driving this CLI are agents reading
477
- stdout. The one signal that mattered was addressed to a channel nobody was listening on. On
478
- 2026-08-19 an author submitted two reports two hours after three replies were published and
479
- re-reported five findings those replies had already closed. Now the count prints in the same block
480
- as the acknowledgement, naming the command, and says outright when a reply may already answer what
481
- you just sent. Against an older platform the field is absent and the previous line stands.
482
-
483
- ## [0.7.1] - 2026-08-19
484
-
485
- ### Fixed
486
- - **`work decide` now says when an action moves nothing.** `to_stage` is optional — an action may
487
- record a decision without a transition — but both outputs communicated that by omission: `--dry-run`
488
- left `to_stage: null` sitting among twenty other keys, and a real apply printed a bare
489
- `✓ Applied '<action>'`. An author read both, shipped `request_more_info` with no `to_stage`, and
490
- lost the `awaiting_customer` their whole customer-reply capture branched on — their words: *"Nothing
491
- about this failed loudly; it just quietly never did the one thing the whole feature depends on."*
492
- The preview now states `→ does NOT change the status (this action only records a decision)` and the
493
- apply says `status unchanged (the action declares no to_stage)`. The console's decision panel got the
494
- same sentence in the same change.
495
-
496
- ## [0.7.0] - 2026-08-18
497
-
498
- ### Added
499
- - **`octwin memos` — the channel answers now.** `octwin feedback` was one-way: an author reported and
500
- heard nothing, ever. Two kinds arrive here, both printed in full — a **reply** to a report you sent,
501
- and a **notice** published to every author (a new capability, a deprecation, a breaking change) —
502
- each carrying `info` / `action_required` / `breaking`. Reading marks them read; `--all` re-reads
503
- history and acks nothing (also the escape hatch if a piped `--json` consumer died halfway).
504
- - **Every networked command now says when a memo is waiting**, one line to stderr, always naming the
505
- command to run. It rides work that was already networked, never throws, and is **deliberately not
506
- TTY-gated** — the same reasoning as the KB drift nudge, whose docblock says it plainly: the primary
507
- reader of this CLI is an authoring agent whose output is piped, and gating on `isTTY` means the one
508
- reader that cannot notice on its own is the only one never told. Unlike that check there is no local
509
- marker file: read state is per-workspace on the platform, so an agent in a fresh container still
510
- learns about an unread reply.
511
-
512
- Why it matters, measured: in August 2026 four reports sat unread for nine days, and one author
513
- replaced every `$t()` call with hardcoded English across all 16 flows of a production pack to work
514
- around a bug that had been fixed two days earlier. Nothing could tell them.
515
-
516
- ## [0.6.1] - 2026-08-18
517
-
518
- ### Fixed
519
- - **`octwin status` worked for no pack at all, and had not since 2026-08-01.** It sent the manifest's
520
- **bare** id (`clinic`) to a route that requires the qualified `<owner>.<name>` — and a manifest
521
- cannot legally declare the qualified form, because the owner segment is attached at publish time
522
- from the authenticated publisher (both the platform and this CLI reject a `.` in it). So the route
523
- answered 404 for every pack, every time. It now resolves the owner from `whoami`'s `tenant_slug`,
524
- and takes an explicit `octwin status <packId>` when you already know it (from `octwin agents` or
525
- `octwin projects`, both of which print qualified ids). Reported by two pack authors, who lost time
526
- to a command that said their pack was missing seconds after a deploy the platform had accepted.
527
- - **A 404 from `octwin status` no longer always reads "not installed".** That one branch covered four
528
- different failures — a malformed pack id, an unknown tenant, an unknown/pinned-elsewhere project,
529
- and the genuine no-active-install — so the message named the *least* likely cause and sent an author
530
- chasing a deploy that had already succeeded. The route says which; the CLI now relays it, and adds
531
- the "run `octwin deploy` first" hint only to the case it fits.
532
- - **`octwin logs` always reported "No conversations"**, including immediately after an `octwin chat`
533
- that had plainly worked. It read `conversations` off the response; the platform moved that endpoint
534
- to the shared page envelope on 2026-08-09 and answers `rows`, which every other list command here
535
- already reads. Both authors filed this as replica lag or a cache gap — nothing lagged, the payload
536
- was simply never read. `octwin logs <conversationId>` was unaffected.
537
-
538
- ## [0.6.0] - 2026-08-09
539
-
540
- ### Changed
541
- - **`octwin cases` is now `octwin work`, and it works again.** The platform's work-inbox
542
- consolidation (2026-08-08) replaced the case-only surface with `/work` over every entity a pack
543
- declares worked, and renamed the scopes to `work:read`/`work:write` — which left every `octwin
544
- cases` call a silent 404 in a published CLI. The command family is renamed to match the platform:
545
- `octwin work [recordId] [--queues]`, with writes `assign` / `note` / `decide` (the declared
546
- operator actions, `--dry-run` previews) and `stage <id> --to <s>` riding the one platform-wide
547
- stage verb (`POST …/xrm/records/:id/stage`, needs `records:write`). `transition` is gone with the
548
- route it called.
549
- - **`octwin orders transition` rides the same stage verb.** `POST …/orders/:ref/transition` was
550
- retired server-side in the same consolidation; the CLI now resolves the order's record id from the
551
- detail read and moves the stage through `…/xrm/records/:id/stage` (scope: `records:write`).
552
-
553
- ### Added
554
- - **A route guard so this cannot rot silently again**: `src/lib/cli-routes.test.ts` extracts every
555
- `/api/self/**` URL the CLI builds and asserts it (method included) against the platform's dumped
556
- route table — the gap that let both breakages above ship.
557
-
558
- ## [0.5.1] - 2026-08-01
559
-
560
- ### Fixed
561
- - **`validate --remote` blamed the platform's version for a bad `--project`.** Every 404 from
562
- `POST /api/self/p/packs/validate` printed *"this platform has no /packs/validate endpoint yet
563
- (older version)"*. That was accurate when the route resolved nothing, but it now resolves the
564
- tenant and project **before** validating — and an unknown project, or one outside a token's pin,
565
- answers 404 by design. So a typo in `--project` sent you looking for a version mismatch instead of
566
- at the flag. The two cases are now told apart by the response **body**, not the status: a missing
567
- route is Fastify's `error: 'Not Found'` and keeps the old wording, while the platform's own
568
- `project '<slug>' not found under tenant` is printed as-is, with the flags to check.
569
-
570
- ## [0.5.0] - 2026-08-01
571
-
572
- ### Added
573
- - **`octwin seed [--pack <packId>]`** — apply the pack's demo/reference data to the project it is
574
- installed on, without redeploying: `xrm.yaml` `demo:` records + scheduling availability, the
575
- commerce catalog, and the demo operator topology. Reports what **each kind** produced.
576
- Previously seeding was reachable only as `deploy --seed`, because the platform's seed endpoint was
577
- keyed on an install id, guarded platform-admin, and carried no tenant/project segments — so the
578
- `/api/self/**` surface could not reach it and a `pack:deploy` token never could. Re-seeding meant
579
- a full redeploy or asking an operator. Idempotent and cheap to re-run: records upsert, and
580
- existing media is REUSED rather than regenerated, so a second pass reports zero images.
581
-
582
- ## [0.4.0] - 2026-08-01
583
-
584
- ### Added
585
- - **`octwin projects create` / `octwin projects rm`** — a **disposable end-to-end environment** from
586
- the CLI, which is what marketplace developers have been missing. `octwin deploy` has always needed
587
- a project that already existed and the CLI could only *list* them, so standing up a throwaway
588
- full deployment meant opening the console or asking an operator. Now:
589
- ```bash
590
- octwin projects create "Scratch" # → slug `scratch`
591
- octwin deploy --project scratch --seed # publish + install + demo data
592
- octwin chat "hi" --project scratch # talk to it (--script for a whole conversation)
593
- octwin projects rm scratch --yes # throw it away
594
- ```
595
- A throwaway environment is deliberately **not a special kind of thing** — it is an ordinary
596
- project in your own workspace, so it inherits your plan, entitlements, RBAC and teardown with no
597
- separate lifecycle. `create` derives the URL slug from the name (`--slug` pins one, `--pack`
598
- installs an already-published pack). `rm` is a HARD delete — the project and everything cascading
599
- from it, no undo, not the same as archiving — so **without `--yes` it only prints what would be
600
- destroyed**, making the dry run the default; the impact list is derived from `pg_constraint`, the
601
- same payload the console's confirm dialog renders. Both need `projects:write`, which a
602
- `pack:deploy` token does **not** confer — the CLI names that scope in the 403 hint.
603
-
604
- ## [0.3.0] - 2026-07-31
605
-
606
- ### Added
607
- - **`octwin projects`** — the `--project <slug>` values your token can actually name, with the plan's
608
- project cap. Every project-scoped command takes that flag and nothing printed the list, so an
609
- author whose token was not pinned had to guess, and a wrong guess 404s identically to a project
610
- that exists but has no install. `--archived` includes archived projects; `--json` for raw.
611
- A `pack:deploy` token reaches it: the platform route was widened to accept `pack:deploy` alongside
612
- `projects:read`, because `pack:deploy` is `access: 'special'` and confers nothing else — the
613
- command would have 403ed for exactly the person it exists for.
614
- - **`octwin validate --require-kb`** — turn a skipped KB-driven check into a failure, for CI.
615
-
616
- ### Fixed
617
- - **`octwin validate` printed ✓ with two of its three checks switched off.** The render-intent and
618
- primitive-argument checks read `<packDir>/.octwin/platform-kb/`, joined onto the pack directory
619
- EXACTLY — so an author who pulled once at a repo root and then worked in `packs/<name>/` had both
620
- checks silently disabled, and the ✓ printed above them unconditionally while the per-check ✓s lived
621
- inside the `if`s. An entire batch of platform-backlog entries reached production through that hole.
622
- Three fixes: the KB is now found by **walking up** (one pull at a repo root covers every pack under
623
- it, `src/lib/kb-path.ts`); the readers report **three states** — absent, malformed, ok — so a
624
- half-written pull no longer reads as "you haven't pulled yet"; and a skip is **announced on the
625
- last line printed**, because a caveat above a ✓ does not reach a reader who stopped at the ✓.
626
- Warn-and-exit-0 stays the default (the KB is a gitignored cache and `platform-kb pull` needs
627
- `pack:deploy`, so a hard failure would break a fresh clone) — the defect was the silence, not the skip.
628
- - **`octwin status` said "live and current" for a pack that could not receive a message.** Every field
629
- it printed was process-global: `registered` is true for a pack ANY project on that instance loaded.
630
- Dispatch is per-project and takes the oldest ACTIVE install, ignoring the rest. The route now
631
- filters `status = 'active'` (an archived install used to report live) and compares against the real
632
- dispatch target, so status can say plainly that another pack owns this project's messages.
633
- - **`octwin validate --remote --project does-not-exist` answered `{ ok: true }`** while `deploy` with
634
- the same flag 404ed. The platform route resolves tenant and project before validating now.
635
-
636
- ### Changed
637
- - **`octwin validate --remote` also runs the platform's flow LINT.** It used to run only at pack load,
638
- so a finding of severity `error` — an `assign:` whose value is a quoted literal, a `$t()` key with
639
- no namespace — passed validate and failed the deploy. Offline `validate` cannot do this: the lint
640
- needs a template-expanded `FlowDef`, which only the server builds.
641
- - **`record_aggregate` / `record_group` now REQUIRE a scope** (`contact_id` or `all: true`) — a
642
- platform contract change the offline arg check replays from the KB. Re-pull after upgrading.
643
-
644
- ## [0.2.0] - 2026-07-30
645
-
646
- ### Fixed
647
- - **`records`, `cases`, `orders` and `catalog` printed a correct total and ZERO rows.** The platform
648
- collapsed its eight per-domain list envelopes into one `Page<T>` whose collection key is `rows`;
649
- the CLI still read `json.records` / `.cases` / `.orders` / `.products` behind a `?? []`, so the
650
- fallback turned a key rename into an empty list instead of an error. `octwin records doctor`
651
- reported *"5 record(s)"* and then *"(none — mint a `records:read` token …)"*, blaming the user's
652
- credentials. All five readers now go through one `readPage` helper
653
- (`src/lib/page.ts`) with a test pinned to the real envelope — a hand-copied key name in an
654
- untested CLI is what shipped this, so the fix is the single reader, not five edits.
655
- - **`--flag=value` silently did nothing.** `--limit=5` became the key `limit=5` and the command ran
656
- with its default. Now parsed; `--set`/`--param` also accept repetition.
657
- - **`octwin catalog` always printed `Products: 0`** — it counted the page, and the route's `total`
658
- had never been read.
659
-
660
- ### Added
661
- - **Write commands — a pack author can now exercise the state their pack creates, not just read it.**
662
- Every one needs the matching `:write` scope; a leading verb (`create`/`note`/`transition`/…) is
663
- what distinguishes a write from the read that shares its command.
664
- - `records create|patch|stage|note`, `records tasks`, `records task complete`
665
- - `cases assign|note|transition|decide` — `decide --dry-run` previews the customer-facing copy and
666
- resulting status **without committing** (that route needs only `cases:read`)
667
- - `orders transition|refund` · `catalog availability|stock` · `agents set`
668
- - `scheduling rules|rule add|rule rm|exception add|exception rm`
669
- - **`octwin feedback`** submits the pack's `FEEDBACK.md` to the platform team, attaching the pack
670
- version, this CLI's version and the `content_hash` of the capability reference you pulled — the two
671
- facts that separate a real platform gap from something already fixed or a stale KB. Replaces
672
- "hand the file to the platform team".
673
- - **`--offset` on every list**, and a line saying when a list was truncated and what to run next.
674
- `has_more` was on every response and no command surfaced it, so a partial list looked complete.
675
-
676
- ### Changed
677
- - `orders refund` **exits non-zero when the payment gateway refuses.** The route answers `200` with
678
- the verdict in `gateway`, so reporting the HTTP status would tell an operator money moved when it
679
- did not.
680
- - A 403 on a record or case write now says it may be an **RBAC grant gap** rather than a missing
681
- scope: the scope is checked on the token, then the verb is re-checked against that specific row.
682
-
683
- ## [0.1.21] - 2026-07-30
684
-
685
- > **Never published to npm.** Bumped in-repo, then superseded hours later by 0.2.0 — everything
686
- > below ships as part of that release. Kept as its own section because the changes are distinct.
687
-
688
- ### Added
689
- - **`deploy` and `status` report the marketplace-listing verdict.** A manifest carrying
690
- `listing.public: true` put the pack into an operator review queue, and the state existed only in
691
- the console — so an author working from the CLI got no acknowledgement that the request had
692
- registered, and never saw a rejection note (which the platform *requires* precisely because it is
693
- their only feedback). Both commands now print it via `printPublicListing`, and stay silent for a
694
- pack that never asked. `status` also distinguishes *approved* from *actually public*: an approval
695
- pins the content sha it reviewed, so any edit returns the pack to the queue on its own.
696
- - **`octwin pull` is discoverable.** It was implemented and answered `--help`, but appeared in
697
- neither `octwin help` nor the README — the one command that recovers a deployed pack's only source
698
- copy was invisible unless you already knew its name. Also added to the scope-requirement table, so
699
- a 403 names `pack:deploy` instead of printing the generic hint.
700
-
701
- ### Fixed
702
- - **The capability-reference drift check no longer fails silently on a narrow token.** The `?meta=1`
703
- poll needs `pack:deploy`, but it rides on every networked command — so an author inspecting data
704
- with a `records:read`-only token got no drift signal at all, and a stale reference is exactly what
705
- leads to inventing a primitive from memory. A 401/403 there now says so once.
706
- - **README: casework is declared in `worklist.yaml`, not a `cases.yaml`.** That grammar was retired;
707
- following the README produced a pack whose casework silently never loaded. `octwin media generate`
708
- was also missing from the command table.
709
- - **`records` no longer blames the plan for a route the plan does not gate.** `GET /xrm/entities`
710
- carries only the scope guard, so a 403 on the entity list wrongly cited the `records` plan feature.
711
-
712
- ### Removed
713
- - **Unreachable rename branches in `init`.** The agent-id and `main`-flow renames sat behind
714
- `--agent` / `--flow` options the CLI never parsed, against a template that ships no `main` flow —
715
- both branches could never fire, and their patterns would have matched nothing if they had.
716
-
717
- ## [0.1.20] - 2026-07-29
718
-
719
- ### Added
720
- - **`octwin validate` catches primitive arguments the platform would drop — offline, no token.** A
721
- primitive takes the arguments it declares and silently ignored the rest, so an invented or
722
- misspelled `args:` key deployed clean, ran clean, and simply did nothing: `record_list args: {
723
- order: … }` (the argument is `sort`) never ordered, and `booking_cancel args: { booking_record_id:
724
- … }` meant the REQUIRED `record_id` never arrived, so cancelling just failed. Validate now replays
725
- the platform's contract from the pulled KB (`primitives/*.json` → `inputSchema`) and names the bad
726
- argument, what the primitive takes, and any required argument that is missing. Scanning the 21
727
- shipped marketplace packs found **ten** such errors across five packs. Keys are checked, values are
728
- not — an `args:` value is normally an expression string resolved at runtime.
729
- [`args-check.ts`](src/lib/args-check.ts).
730
- - Silently skipped when the KB has not been pulled yet, like the render-intent check beside it.
731
- - It cannot see inside a `use:` template body (expanding one is the platform's job) — `octwin
732
- validate --remote` covers that case, and now also reports unreachable `$bind.<path>` reads and
733
- `outputs:` port typos, which have no offline equivalent.
734
-
735
- ## [0.1.19] - 2026-07-29
736
-
737
- ### Added
738
- - **`octwin validate` catches render-intent fields the platform would drop — offline, no token.** Each
739
- render intent takes a fixed field set; a key outside it used to be swallowed at load and dropped at
740
- render, with no error anywhere — just a card missing what the author wrote. Validate now replays the
741
- platform's own contract from the pulled KB (`render-intents/*.json` → `allowed_keys`) and names the
742
- bad field, the intent, and what is allowed. Scanning the 21 shipped marketplace packs found two real
743
- instances in 223 intents: a `text_card` carrying `buttons` (the fallback's only escape button never
744
- rendered) and a `list_picker` carrying a non-existent `group_by` (a whole mapper step feeding a
745
- fabricated parameter). Silently skipped when the KB has not been pulled yet.
746
-
747
- ### Changed
748
- - **`octwin platform-kb pull` now explodes the reference and writes an `INDEX.md`.** A pull used to
749
- write ~787 KB across 34 flat files — about 225k tokens, more than the pack being authored — so
750
- finding one primitive meant reading a 122 KB catalog. Catalogs the platform describes as
751
- enumerable are now written **one file per entry** (`primitives/record_list.json`,
752
- `render-intents/carousel.json`, `declarations/xrm.json`, …), with `INDEX.md` mapping every doc and
753
- all 171 entries to its file with a one-line summary. Reading one primitive went from ~38k tokens to
754
- ~700. The layout comes from the platform (an `entries` descriptor on the bundle index) rather than
755
- being hardcoded here, so this CLI keeps working against any platform version and a platform that
756
- adds a catalog needs no CLI release. The pulled directory is cleared each pull, so a withdrawn
757
- capability cannot linger as a file the authoring agent reads as current — its *contents* are
758
- cleared rather than the directory itself, since on Windows a directory that is any shell's working
759
- directory cannot be removed.
760
-
761
- ## [0.1.18] - 2026-07-29
762
-
763
- ### Fixed
764
- - **The bundle collector and the platform's repo import disagreed about what a pack directory
765
- contains.** `collectBundleFiles` had its own rule and kept three things the operator's GitHub
766
- import drops: `*.ts`/`*.tsx`, `__snapshots__/` and `*.example`. Because the server rejects a
767
- `.ts` outright, the same directory imported cleanly from GitHub and failed to deploy from
768
- disk with `'…': executable code is not allowed` — an error blaming a file that was never
769
- meant to ship (a snapshot test beside a flow, or the documented `xrm.yaml.example` template).
770
- The rule now lives in a vendored `lib/pack-source.ts` beside the vendored validator, and a
771
- parity test in the platform drives both copies over one table.
772
-
773
- ### Removed
774
- - **`pack.json` is gone — the saved login IS the deploy target.** The file had eroded to a single
775
- field, `platform_url`, which `octwin login --url … --token …` already states; `tenant`/`project`
776
- became optional overrides once deploy tokens started carrying their own tenant and project pin.
777
- So it was a second home for a fact `login` knew, and it cost real things: a machine-specific file
778
- committed into every pack repo, a four-link config-precedence chain to document, and an
779
- exclusion rule duplicated in the bundler and the server (which had already drifted apart).
780
- `octwin init` and `octwin pull` no longer write one, and nothing reads one. **A pack directory is
781
- now pack content and nothing else** — the same repo deploys from any machine, and a pulled pack
782
- redeploys with no flags.
783
-
784
- ### Changed
785
- - **`octwin login` now sets the default deploy target**, not just the token: it writes a
786
- `default_url` key alongside the token in `~/.octwin/credentials.json`. Every command resolves
787
- **flags → env (`PACK_PLATFORM_URL`/`PACK_TENANT`/`PACK_PROJECT`/`PACK_TOKEN`) → saved login**.
788
- An existing credentials file simply has no `default_url` until the next `octwin login` — which is
789
- exactly what the "no platform url" error now tells you to run. CI is unaffected (it passes
790
- `PACK_PLATFORM_URL` + `PACK_TOKEN` as env and never had a `pack.json` — that file is retired).
791
-
792
- ## [0.1.17] - 2026-07-27
793
-
794
- ### Added
795
- - **`octwin pull <packId>` — write a DEPLOYED pack's source back to disk.** The inverse of
796
- `deploy`. A pack pushed with the CLI lived on the platform as an artifact the runtime served
797
- but nothing could hand back, so its only source copy was the machine that pushed it. Pull it,
798
- fix it, redeploy it. Writes a `pack.json` (retired in 0.1.18 — the saved login is now the
799
- target) so the pulled directory redeploys where it came from; `--version` picks a version,
800
- `--force` overwrites a non-empty dir. You may pull a pack
801
- your tenant OWNS (an operator token pulls any) — a pack you merely installed is not yours to
802
- read.
803
- - **`octwin chat --script <file>` — drive a whole conversation from one invocation.** One turn per
804
- line, in order, in a single process over a single SSE connection, waiting for each turn to settle
805
- before sending the next. Blank lines and `#` comments are skipped; `tap:<id>` presses a rendered
806
- row/button (the rest of the line is kept verbatim, since a tap id is itself colon-delimited),
807
- `media:<path>` uploads a file, and `media:<path> | caption` attaches one.
808
-
809
- This exists because **chaining invocations races the agent loop.** A turn ends on a quiet gap
810
- (`TURN_SETTLE_MS`), which can arrive while the server-side loop is still running — so
811
- `chat A && chat B` lets B's inbound land mid-turn, and the agent fills required fields with
812
- placeholder text (`service_id: "service-id"`) or starts a second workflow run. Those look exactly
813
- like flow bugs and cost a field author three separate investigations. The one-turn-per-invocation
814
- rule is now stated in `chat --help` and the header usage as well.
815
-
816
- ### Changed
817
- - **`octwin deploy` reports the suspended runs it invalidated.** A redeploy rebuilds the pack's
818
- tools, and in-flight flow runs live in those tools — so every suspended conversation is dropped.
819
- The deploy prints `ⓘ N suspended run(s) invalidated` when N > 0, instead of leaving the author to
820
- discover it as a `workflow_resume_stale` on the next tap.
821
-
822
- ## [0.1.16] - 2026-07-26
823
-
824
- ### Added
825
- - **A pack can ship committed images.** `jpg`/`jpeg`/`png`/`webp`/`gif`/`pdf` files in your pack
826
- directory now travel as a separate `blobs` half of the bundle (base64 on the wire, `bytea` in
827
- storage) instead of being rejected as "not an allowed pack file type". Declare each one under the
828
- manifest's `static_assets: [{ file, key }]` and reference it from `config:` with the
829
- `$pack_asset:<key>` sentinel — the platform uploads it to the media system at install and the
830
- sentinel resolves to the served URL. `octwin validate` reports the image count alongside the file
831
- count.
832
- - **The size ceilings fail locally, before upload** — 2 MB per file, 32 MB of binary per pack,
833
- mirroring the server. `svg` stays rejected: it is script-capable and these assets are served to
834
- browsers.
835
-
836
- ### Fixed
837
- - **Committed images were silently corrupted.** `collectBundleFiles` read *every* file with
838
- `readFileSync(full, 'utf8')`, so a JPEG went through a lossy UTF-8 decode and arrived mangled
839
- server-side — a deploy that "succeeded" and rendered a broken image. Binary files now split off
840
- into `blobs` and keep their bytes.
841
- - **`octwin status` reported `loaded=(none)` for every pack, always.** It printed a `loaded_version`
842
- field that stopped existing when reload moved to content-sha keying, which also meant the
843
- version-drift warning underneath it could never fire. It now prints the **content sha** the
844
- instance has loaded and the one the catalog holds (with the artifact's `origin`), and warns off the
845
- platform's own `up_to_date` flag. The sha is the more useful fact anyway: re-publishing the *same*
846
- version changes it, which is exactly the author's inner loop. A pack that is live and current but
847
- **withdrawn** from the catalog now says so, rather than reporting a clean bill of health.
848
-
849
- ## [0.1.15] - 2026-07-25
850
-
851
- ### Added
852
- - **`octwin agents [packId::agentId] [--prompt]` — see what your agent actually runs as.** The platform
853
- gained *governed* agent settings: an operator platform default can override the `default_model`
854
- (and history window / working-memory toggle) your manifest declares, and that override is invisible
855
- from the pack source. The command prints each governed setting's **effective** value, **which layer
856
- won** (project override → platform default → pack manifest), and warns explicitly when your pack's
857
- declared value is *not* in force. `--prompt` prints the exact system prompt the LLM sees for this
858
- project (pack instructions + platform protocol + any project overlay).
859
- - **`octwin orders [reference_id]` — the commerce a conversation produced.** The list (number,
860
- status/payment, total, contact) or one order with its line items, the
861
- subtotal/tax/shipping/discount/total breakdown, `payment_ref`, and the allowed transitions. On a
862
- `pending`/`none` payment it explains *why that's expected*: the forward payment lifecycle is
863
- **webhook-owned** (not patchable), and the default gateway-less `manual` driver makes
864
- `payment_request` take its `empty` port — so the flow should confirm pay-on-delivery, not error.
865
- - **`octwin analytics [entity]` — stage conversion for any pipelined entity.** The funnel engine that
866
- powered journeys is now generic, so any entity declared with a `pipeline:` charts conversion +
867
- drop-off (`--overview` / `--milestones` / `--trends` / `--cost`, and `--stage <id>` for the records
868
- currently at a stage). An empty result names **both** causes the platform folds into one response —
869
- no `pipeline:`, or no `view` grant on `record.<entity>`.
870
- - **`octwin catalog [--readiness]`** — commerce products with price, availability and stock
871
- (`untracked` when the SKU isn't inventory-tracked), plus the WhatsApp catalog binding.
872
- `--readiness` runs the Meta Graph checklist with each failing item's suggested fix.
873
- - **`octwin scheduling [--slots <resourceRecordId>]`** — the engine state (bookable resource types,
874
- upcoming slots, booked seats) or the computed slots for one resource. This is how you verify the
875
- availability rules a `deploy --seed` created, which 0.1.14 could create but not read back.
876
-
877
- ### Changed
878
- - **Auth failures now name the scope the command needs.** Every 401/403 appends the required scope
879
- (and the plan feature, where the route is entitlement-gated) from one client-side mirror of the
880
- platform's scope registry — replacing the per-command hardcoded strings, several of which named no
881
- scope at all. It also spells out that `pack:deploy` / `media:generate` are direct-grant only, so a
882
- `tenant:admin` preset token does **not** confer them (the most common "but my token is admin" dead end).
883
-
884
- ## [0.1.14] - 2026-07-23
885
-
886
- ### Fixed
887
- - **A failed demo seed no longer reports a clean `✓`.** `deploy` softens non-fatal
888
- install steps (e.g. a demo-seed row) to warning frames, but the summary still printed
889
- `✓ Deployed` and exited 0 — so a deploy that seeded ZERO records read as success (the
890
- false-✓ trap). `deploy` now collects those step errors, prints `⚠ Deployed with N
891
- warning(s) — data may be incomplete` with each message, and exits non-zero so CI /
892
- a `deploy && chat` chain catches it.
893
-
894
- ### Added
895
- - **`login` echoes what the token reaches.** After saving, `octwin login` calls
896
- `/api/self/t/whoami` and prints the workspace, project pin, and scopes — so a fresh
897
- token self-identifies without a second `octwin whoami` (best-effort; a network failure
898
- never fails the save).
899
- - **`validate --remote` surfaces warnings + the data-store trap.** The remote validate now
900
- renders advisory `⚠` warnings (which don't block deploy) and, for a pack declaring
901
- `required_adapters: [data-store]`, reports the missing-data-store error the deploy would
902
- have 409'd on — plus a warning that an `xrm.yaml` pack usually shouldn't declare that
903
- adapter at all. It also now catches (offline) a list-form `entities:` block and a
904
- `localized: true` field seeded with a bare string — traps that previously surfaced only
905
- mid-seed at deploy.
906
-
907
- ## [0.1.13] - 2026-07-22
908
-
909
- ### Added
910
- - **A headless media loop — produce + send.** Two paired additions close the coverage hole where any
911
- media-collect flow (e.g. `activate-app`'s registration / ID uploads) stalled at the upload prompt
912
- because the CLI could neither *make* nor *send* an image:
913
- - `octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]` — AI-generates an
914
- image on the platform (needs a `media:generate`-scoped token), stores it as a public asset, and prints
915
- its `MEDIA-` handle + serve URL. `--out` downloads the bytes to a file (WhatsApp renders only
916
- `.png`/`.jpg`); `--json` emits `{ media_id, url, mime, width, height, bytes }`.
917
- - `octwin chat --media <file|media-id>` — uploads a local file (or a media id from
918
- `media generate --json`) as an image/document/audio inbound; any accompanying `"message"` rides as
919
- its caption. The platform's media pipeline folds the upload into a running collect, so media flows
920
- are now fully drivable headlessly.
921
-
922
- ### Changed
923
- - **The KB-drift nudge now names what changed.** With the platform serving per-entry content hashes, the
924
- post-command nudge appends a `(N changed · M added · K removed)` summary instead of a bare hash pair.
925
- - **`octwin platform-kb` prints a changelog on pull.** Instead of only a doc/catalog count, the pull now
926
- diffs the fresh index against your last pull and lists exactly which docs/catalogs were added (`+`),
927
- changed (`~`), or removed (`-`) — so a replaced schema shape is visible, not silent.
928
-
929
- ## [0.1.12] - 2026-07-22
930
-
931
- ### Fixed
932
- - **The KB-drift nudge now fires after `chat` too.** `chat` — the command a debugging session runs most —
933
- was missing from the networked-command list, so a chat-heavy session never noticed the platform's
934
- capability reference had moved. The nudge also now shows the concrete drift
935
- (`old-hash → new-hash`) so you can see it's real, not a heuristic.
936
-
937
- ### Changed
938
- - **Every networked command announces what it's doing before it does it.** `chat` prints
939
- `→ Connecting to <tenant>/<project> as '<handle>' …` before opening the stream and
940
- `… delivered — waiting for the reply (up to Ns)` after the send; `status` / `whoami` /
941
- `platform-kb` / `records` / `logs` / `cases` each print a one-line `→ …` header naming the
942
- action and target before the first network call — no more silent seconds followed by a result
943
- (or a hang with no clue what was being attempted). JSON modes (`--json`) stay clean for piping.
944
-
945
- ## [0.1.11] - 2026-07-21
946
-
947
- ### Fixed
948
- - **Auth-failure hint on every command, not just the inspect family** (author-feedback A7). `validate`,
949
- `deploy`, `status`, and the single-record read now print the scope/transient explanation on a 401/403
950
- (the shared `authFailureHint`), instead of a bare `HTTP 401` that left it ambiguous whether the token
951
- lacked a scope or the platform hiccupped.
952
-
953
- ## [0.1.10] - 2026-07-21
954
-
955
- The capability reference is now **pulled, never bundled** — the authoring skill dropped its committed KB
956
- snapshot, so `octwin platform-kb pull` is the single source of truth, and the CLI watches it for drift.
957
-
958
- ### Added
959
- - **KB-drift nudge** — after any command that already talks to the platform (`deploy` / `status` /
960
- `test` / `validate --remote` / `records` / `cases` / `logs` / `whoami`), the CLI does one tiny
961
- `octwin-platform-kb?meta=1` GET and prints a one-line "the platform capability reference changed —
962
- `octwin platform-kb pull`" when the platform's `content_hash` no longer matches your last pull.
963
- Fail-silent + TTY-only + no-op until you've pulled once — the sibling of the existing CLI-upgrade notice.
964
-
965
- ### Changed
966
- - **`octwin platform-kb pull`** now records the reference's `content_hash` in `.octwin/platform-kb/index.json`
967
- (the fingerprint the drift check compares against).
968
-
969
- The conversation-debugging release — driven by the second author-feedback round (xpeng-egypt),
970
- which found the author→validate→deploy loop solid but multi-turn debugging impossible from the CLI.
971
-
972
- ### Added
973
- - **`octwin chat --tap "<tap-id>"`** — press a rendered button / list row (sends the web channel's
974
- `type:'interactive'` inbound). Paired with full render printing, any tap-driven flow is now drivable headlessly.
975
- - **Full render printing** — every reply prints its complete interactive content: list sections/rows,
976
- buttons, and carousel cards, each with its **tap id**; header/footer/list-button text included.
977
- - **`--json` on `chat` / `logs` / `cases`** — raw SSE envelopes (chat) or raw event/case payloads.
978
- - **`octwin cases [caseId] [--queues]`** — inspect casework (support tickets): the inbox, one case +
979
- timeline + applicable decisions, or queue keys with open counts. Casework packs previously had zero
980
- data-inspection path (`records case` was a dead end).
981
- - **Per-subcommand `--help`/`-h`** — intercepted before any network or auth work (previously
982
- `octwin logs --help` made a network call and could die with a 401).
983
- - Multi-message turns: `chat` now collects **all** of a turn's renders (quiet-gap detection) instead
984
- of cutting at the first one.
985
-
986
- ### Changed
987
- - **`octwin logs`**: the conversation list shows contact handle + last activity and filters with
988
- `--as <handle>`; the timeline prints **full** event text (no more 240-char truncation) plus the
989
- renders each turn actually sent; footer points at `--json` / `mastra-messages` for verbatim payloads.
990
- - **Network failures name the target URL** (`platform unreachable at <url>`), and 401s explain
991
- invalid-token vs one-off transient instead of a bare status code.
992
- - `octwin records` on an unknown entity now surfaces the server's error body, and for `case`/`cases`
993
- points at `octwin cases`.
994
-
995
- ### Fixed
996
- - **Multi-turn chat actually works.** Two bugs made consecutive `octwin chat` calls look like "every
997
- call starts fresh": (1) the idempotency `local_id` was a **constant** per handle, so the platform's
998
- 5-minute inbound dedup silently dropped the second message — a fresh id is now minted per call;
999
- (2) a fresh SSE connect **replays** recent history and the CLI printed the first render frame — i.e.
1000
- the *previous* turn's greeting — as the reply; the CLI now drains the replay and only accepts frames
1001
- newer than the connect boundary. The platform always kept one open conversation per `--as` handle —
1002
- the same handle genuinely continues the same conversation now.
1003
-
1004
- ## [0.1.8] - 2026-07-21
1005
- - Global-first install guidance (`npm i -g octwin-cli`); upgrade notice suppressed under `npx`.
1006
-
1007
- ## [0.1.7] - 2026-07-21
1008
- - `--version`/`-v` flag; daily fail-silent, TTY-only "newer version available" notice.
1009
-
1010
- ## [0.1.6] - 2026-07-21
1011
- - `octwin init` scaffolds a home-hub menu (`home` list_picker + example `browse` tool) instead of a bare greeting.
1012
-
1013
- ## [0.1.5] - 2026-07-20
1014
- - `octwin validate --remote` (the platform's FULL manifest + flow-DSL check, all errors at once);
1015
- headless debugging commands `octwin records` / `octwin logs` / `octwin chat`; `octwin test` became
1016
- an alias for `validate --remote`. First author-feedback round.
1017
-
1018
- ## [0.1.4] - 2026-07-20
1019
- - Authoring reliability + DX fixes from the first end-to-end external-pack test.
1020
-
1021
- ## [0.1.3] - 2026-07-20
1022
- - `octwin platform-kb pull` — pull the platform capability reference for the authoring skill.
1023
-
1024
- ## [0.1.2] - 2026-07-20
1025
- - Proper npm README.
1026
-
1027
- ## [0.1.1] - 2026-07-20
1028
- - 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
+ ## [Unreleased]
9
+
10
+ ## [0.9.0] - 2026-10-21
11
+
12
+ ### Changed
13
+ - **`octwin automation` reads your RULES — what you wrote — not job rows.** BREAKING for scripts: the list
14
+ is each rule of the project in the platform's own words (the sentence the console's Outbound → Automatic
15
+ tab shows), on or paused, its week (sent / not delivered / failed / skipped) and its next time, under the
16
+ week's numbers and what needs a look; `--json` is `{ summary, rules }`. New: `coming` (every time still to
17
+ come), `happened` (every fire, its final word, why a message did not go) and `why <id>` (one line's steps
18
+ and the words sent — what a scripted test asserts a fire with). `pause` / `resume` / `run` take a rule key;
19
+ the key of the older job in your own `automation.yaml` finds the rule it runs as (`src/lib/rules.ts`). It
20
+ listed job rows until now — since the platform ran every pack's automation as rules, those were only its
21
+ own two sweeps, so nothing you declared showed in the terminal. `campaigns` and `send` are unchanged.
22
+ Upgrade before the platform retires the jobs routes: an older CLI's `octwin automation` will answer 404.
23
+
24
+ ### Fixed
25
+ - **`octwin records` lists a project's handoffs and cases even when the pack declares no types of its
26
+ own.** The catalog now carries the platform's entities with a `listed` flag per entity; the command
27
+ lists the listed ones (a missing flag, from an older server, reads as listed), so it neither prints
28
+ every unused platform type nor says "no record types" over a project full of handoffs.
29
+
30
+ ## [0.8.17] - 2026-10-12
31
+
32
+ ### Added
33
+ - **A deploy the checks refuse says what keeps running, then what to fix.** "✗ Not deployed — your live
34
+ version 2.1.0 keeps running, unchanged", then each problem once, grouped by what customers feel (`!`
35
+ now, `·` tidy-up) with your own messages under it. It printed the raw JSON body. `src/lib/refusal.ts`.
36
+ - **A deploy says which copy it was made from, and is refused when someone published since.** `octwin pull`
37
+ and every successful `octwin deploy` write `.octwin-base.json` in the pack folder (commit it; it is
38
+ never deployed). The deploy sends it, and when the live version changed after your copy — Tavi in the
39
+ console, another deploy, a repo import — or your copy is a LOWER version, the platform refuses and
40
+ nothing changes; the message says who published and how to continue. `--force` deploys anyway.
41
+ `octwin status` shows whether your copy is still the live content. Rules: `src/lib/base.ts`.
42
+
43
+ - **`octwin recipe <module>` names the other recipes it needs.** A recipe's buttons can open a flow
44
+ another recipe ships, and its `track:` can name a journey id another declares — booking's "no free
45
+ time" card opens handoff's `reception`, whose own buttons open casework's `ticket`. The command now
46
+ prints each recipe the pack still lacks (`octwin recipe handoff` …), worked out from the recipes' own
47
+ files (`recipeNeeds`, `src/lib/recipe.ts`), instead of the author meeting the chain as one validate
48
+ error after another.
49
+ - **`octwin charts [<key>]`, `octwin surveys [<key>]` and `octwin handoff [<id>]`** — read-only views over
50
+ routes a token already reaches: the pack's own `charts:` (one chart's figure, parts or points, with
51
+ `--from` / `--to`), the surveys with their headline score (one survey's questions, rollup and answers),
52
+ and the Live chat desk (one handoff with its transcript). Accepting, answering and returning a handoff
53
+ stay in the console — those routes need a signed-in person, and a token is not one.
54
+ - **`octwin performance --range <days> --channel <whatsapp|web>`** — the console's two controls, passed to
55
+ the route as `?range=` / `?channel=`.
56
+ - **`octwin platform-kb pull` records the platform version that published the KB** (in `index.json`) and
57
+ prints it, so a FEEDBACK.md finding can cite the build it was read against.
58
+ - **`octwin recipe [<module>]`** lists the recipes in the pulled KB, or writes one into the pack. It reads
59
+ the recipe's starter files from the pulled guide — no second copy to drift — and never overwrites: a file
60
+ you already have is named, a declaration you already have (your `xrm.yaml`) gets the recipe's part
61
+ printed to merge, and it prints the `flows:` / `tools:` lines to add (`src/lib/recipe.ts`).
62
+ - **INDEX.md routes you: "Add per capability", "Start from a recipe", "When something is already
63
+ wrong".** `octwin platform-kb pull` prints the KB's route, its recipes and every guide's symptom rows
64
+ from the index the KB publishes — the tables `craft-capabilities` carries, from the same renderers
65
+ (`src/lib/kb-index.ts`).
66
+ - **`octwin validate` checks journey ids offline.** Every literal id a flow names in `track:` or
67
+ `do: track_event` (`event`, `stage_enter`, `goal_reached`) must be declared by one of the pack's own
68
+ `journeys/*.journey.yaml`, each against its own list — a stage id does not satisfy `event:`. A pack
69
+ with no `journeys/` is told every such id is dead. Until now only the platform's full check caught
70
+ this, so an offline pass could ship a `track:` that fails mid-conversation, or a `track_event` that
71
+ takes its `failed` port on every run. Needs no pulled KB, so it never skips (`src/lib/journey-check.ts`).
72
+
73
+ ### Changed
74
+ - **`octwin pull` updates the pack folder you are in, and never overwrites your own edits.** Run inside
75
+ the pack's folder it writes there (else `./<packId>`; `--dir` overrides). It replaces only files that
76
+ still match your last pull or deploy, removes files the live version dropped, and stops — listing them —
77
+ on a file you changed since, so you can save it first; `--force` overwrites. It used to refuse ANY
78
+ non-empty folder, and with `--force` it kept files the live version had deleted, which the next deploy
79
+ put back.
80
+ - **A flag a command does not take is refused, naming the command's real flags.** It was ignored, so a
81
+ typo — or `performance --channel`, which the command never read — ran with the default and printed an
82
+ answer to another question. One table drives it: each command's `flags:` row plus `GLOBAL_FLAGS`
83
+ (`--url --tenant --project --token --json --help --dir`), and `cli.flags.test.ts` holds that table to the
84
+ code that reads the flags (a scan that fails closed), to `octwin help` / `--help`, and to the README.
85
+ - **`octwin help` and the README now list every command and flag** — `seed`, `memos`, `projects create |
86
+ rm`, `chat --script` / `--press`, `validate --require-kb` / `--strict-primitives`, `agents set --enabled`
87
+ / `--overlay`, the paging flags. The README asks for Node ≥ 22 (as `engines` does), says `work stage`
88
+ and that `work decide --dry-run` needs only `work:read`, and `pull --help` says a first-party (Octwin)
89
+ pack can be pulled too.
90
+ - **`octwin init` writes a bilingual starter that passes the platform's full check with no warning** —
91
+ Arabic and English, a home menu (its footer says `/clear` exists, and it marks the journey's first
92
+ stage), a visit request that is the KB's collect recipe byte for byte, `xrm.yaml` with demo rows, a
93
+ journey whose goal the record's own hook reaches, and `commands.yaml` answered in each customer's
94
+ language. The platform's KB test holds it to the full check alone and with every recipe added (it is the
95
+ cookbook's base). Its next steps and README point at `platform-kb pull`, `recipe` and `validate`.
96
+
97
+ ### Fixed
98
+ - **`octwin init ./clinic` names the pack `clinic`.** The id is the directory's own name; `init` stripped
99
+ the slashes out of the whole path, so `./clinic` became `-clinic` (refused) and `packs/clinic` became
100
+ `packsclinic` (`src/lib/init-id.ts`).
101
+ - **`octwin automation campaigns` prints each campaign** — its number, name, stage, delivery progress and id
102
+ — and says when one can be finished; it read fields the route never returns, so every row showed its uuid
103
+ and a dash. `octwin automation send` finishes a campaign stuck at `sending` and reports how many contacts
104
+ already had it.
105
+ - **`octwin orders` explains the payment state it shows:** `none` is an order no payment was opened for (no
106
+ gateway), `pending` one whose checkout the gateway has not confirmed yet. It called `pending` expected
107
+ without a gateway.
108
+ - **`octwin automation campaigns` with none says how one is made** — a flow's `campaign_create` step. It named a
109
+ `campaigns:` block in the automation declaration, which does not exist.
110
+
111
+ ## [0.8.16] - 2026-10-07
112
+
113
+ ### Changed
114
+ - **`octwin performance --channel` marks a figure that is not narrowed.** Capacity has no channel, so a
115
+ band narrowed to one prints the capacity tile with `(all channels)` rather than letting it pass as the
116
+ channel's own.
117
+ - **`octwin performance` prints a rate's change in points.** The platform
118
+ replaced each indicator's `delta_pct` with `change` (one rule for the console, the server and the CLI): a conversion rate going
119
+ 42.9% → 57.1% now reads `+14.2 pts vs prev`, not `+33.1%`, and an NPS from −25 to +40 reads `+65`,
120
+ not `-260%`. A count still reads as a percent; growth from zero reads `new`.
121
+
122
+ ## [0.8.15] - 2026-10-06
123
+
124
+ ### Fixed
125
+ - **`octwin records` said nothing useful for a pack with no record types of its own.** Its message read
126
+ `has_xrm`, which the platform never sent, so it could not show. The platform sends it now; the CLI
127
+ says the pack declares no record types and that platform records still read by name
128
+ (`octwin records order | product | case`). `records create` and `records tasks` read `has_data` —
129
+ "no AI Agent is loaded in this project" — instead of the same unsent field.
130
+
131
+ ## [0.8.14] - 2026-10-06
132
+
133
+ ### Added
134
+ - **`octwin seed --reset`** — archive every row the pack's demo loaded, then load it again. Needed
135
+ once for demo rows seeded before 2026-10-05 (the seed says so), and a clean slate on demand; records
136
+ a customer made are never touched.
137
+ - **`octwin records <entity> --search "<words>"`** — the entity's ranked search (fuzzy, plus semantic
138
+ when the entity declares it), best match first, each row printed with its score. The platform has
139
+ served it to the console for months; the CLI never asked. `--json` returns the rows with their scores.
140
+ - **`press:<label>` in `octwin chat --script`, and `--press <label>`.** Taps the button of the reply just
141
+ received by its label — exactly, or the same words without emoji or case, or the one label containing
142
+ them. A form or checkout button's tap id carries its run id (`t:resume:…:run_id=…`), which exists only
143
+ once the reply arrives, so a script could not get past the first form step: `tap:` needs an id nobody
144
+ can write in advance. No match, or several buttons, stops the script and lists the buttons. A lone
145
+ `--press` picks from the reply the conversation is waiting on. The rules are the AI Author's `press`,
146
+ byte for byte (`src/lib/press.ts`). An older CLI sends a `press:` line as a typed message.
147
+
148
+ ### Fixed
149
+ - **`octwin chat` prints a card's link buttons and its forms.** A button that opens a link prints as
150
+ `[link] title → url` (a card may now carry links beside its reply buttons), and a form prints as
151
+ `[flow form] …` — it compared the hint type `'flow'`, which the wire never sends (`form_flow`).
152
+ - **`--json` output always parses.** `records --json` and `projects create --json` printed a
153
+ `→ Reading …` line in front of their JSON, and `logs --json` with no conversations printed a sentence
154
+ and no JSON at all (reported 2026-10-01). Under `--json`, stdout now carries the result and nothing
155
+ else — progress, tables and hints go to stderr — and every write verb of a JSON command (`records
156
+ create`, `work note`, `agents set`, `catalog stock`, `scheduling rule add`, `projects rm --yes`, …)
157
+ returns what the platform answered. A test runs every JSON command and parses its stdout.
158
+ - **`octwin chat` prints the pictures, files, voice notes and map pins a reply carries.** Only a reply's
159
+ text and buttons were printed, so a reply that sent a picture read as a caption with nothing under it.
160
+ Now `[image] <path>` (without the signed query), `[file] <name> → <url>`, `[voice note] 12s → <url>`,
161
+ `[location] <name> (lat, long)`, and `(image)` on a carousel card that has a picture
162
+ (`src/lib/media-lines.ts`).
163
+
164
+ ### Changed
165
+ - **`--json` on a command that has no JSON output is refused by name** (`whoami`, `deploy`, `validate`,
166
+ …), with the list of commands that have it. It used to print the prose to stderr and leave stdout
167
+ empty. `chat --json` and `login --json` print one JSON object per line, as before.
168
+ - **Under `--json`, the progress lines move from stdout to stderr.** A script that read them from stdout
169
+ reads them from stderr now; human output (no `--json`) is unchanged.
170
+ - **`octwin pull` can pull an Octwin (first-party) pack.** The platform now lets a workspace read the
171
+ source of its own packs and of Octwin's — never another workspace's — and the 403 hint says so. A
172
+ platform change: an older CLI pulls first-party packs too, it only prints the older hint.
173
+
174
+ ## [0.8.13] - 2026-09-27
175
+
176
+ ### Added
177
+ - **`octwin records archive <id>` / `restore <id>`.** Archive is how a record leaves use — test data
178
+ included: it drops out of every list, count and report and fires the entity's `on.archived`
179
+ reactions, exactly like the console's Archive; `restore` undoes it. There is no delete, on the
180
+ platform or here, and `octwin records --help` now says so.
181
+
182
+ ### Fixed
183
+ - **`octwin chat` said the AI Agent wrote every message.** A reminder that fired mid `--script` (a
184
+ `schedule_notify`, a reaction) printed as `← <body>`, was taken for the turn's reply, and ended
185
+ the turn on it — so the real reply looked lost while `octwin logs` showed the flow had worked.
186
+ Each line now says who sent it: `← [notify] …` (not the turn's answer — the wait for the real
187
+ reply goes on), `← [operator: Name] …` (a person in Live chat), `← [platform] …` (a handoff
188
+ joining or returning); plain `← …` is the AI Agent. After the last turn the stream stays open
189
+ 3 s more, so a late message prints (marked "after the last turn") instead of reaching a closed
190
+ connection. Needs a platform that stamps `source: 'notify'` on web renders; an older one just
191
+ prints notifies unlabelled, as before.
192
+ - **The "no such template" hint named a file the platform never loads.** It said
193
+ `templates/<name>.yaml`; pack templates load only as `templates/<name>.template.yaml`, so a
194
+ template saved as the hint said was silently ignored.
195
+
196
+ ### Changed
197
+ - **`octwin validate` runs the platform's FULL check by default — and needs no login for it.** The
198
+ offline checks are a subset, and a subset that passes read as "deploy will pass": it passed a pack
199
+ the full check refused (a scaffold flow left behind with Arabic copy only, in a pack that ships
200
+ English). Now `validate` runs the offline checks, then the full one: through your workspace when you
201
+ have a token AND a `--project` (which also confirms the project exists), otherwise through the
202
+ platform's public validate route with no credential. When it cannot reach the full check — no url,
203
+ offline, rate-limited, an older platform — the LAST line says *"only the OFFLINE check ran — a
204
+ PARTIAL check"* and why, and it exits 0 as before. `--remote` now means "the full check is
205
+ required" (not reaching it is an error); the new `--offline` runs only the subset.
206
+ - **The scaffold's example `browse` flow says it is an example.** Its header, its copy file and its
207
+ `manifest.yaml` line now say to replace it, or delete the flow, every `browse.locale.*.yaml` and the
208
+ `flows:` line together — a leftover never loads, but a second language makes it fail validation.
209
+
210
+ ## [0.8.12] - 2026-09-25
211
+
212
+ ### Changed
213
+ - **The package pointed its `homepage` at npm — the page it is rendered on.** It is
214
+ <https://octwin.ai> now, and the README's Links section gains the platform, the guides and the
215
+ marketplace. The README describes Octwin at length and linked only to npm and nodejs.org.
216
+ - **`octwin usage` with a project-pinned token reports that project.** The platform now treats a
217
+ token's project pin as a ceiling on every route, so a pinned token is refused on the
218
+ workspace-wide rollup. `usage` asks `whoami` for the pin and reads the project view instead of
219
+ printing a 403 — which is what its help text (`project if pinned, else workspace`) always said.
220
+
221
+ ### Fixed
222
+ - **`octwin chat` sat out its whole 45-second wait on every message during a live handoff, and
223
+ read as hung.** While a person holds the conversation (or one has been asked for) the AI Agent
224
+ deliberately answers nothing, and the CLI could not tell that from a slow pack. The platform now
225
+ says so with a `held` event on the web stream; `chat` prints
226
+ *"⏸ a person is holding this conversation — the AI Agent will not reply…"* (or *"waiting for a
227
+ person to join…"*), still collects any operator line replayed by that message, and returns. A
228
+ held turn is not a failure: it exits 0, and `--script` carries on to the next line, so the
229
+ customer's side of a handoff can be scripted. Needs a platform that sends `held`; an older one
230
+ behaves as before. Reported in `xpeng-egypt/FEEDBACK.md`, session 3.
231
+
232
+ ## [0.8.11] - 2026-09-07
233
+
234
+ ### Fixed
235
+ - **`octwin login` erased your own tooling report — the CLI was blanking what it had just
236
+ told the platform.** Two calls built their `authorization` header by hand instead of going
237
+ through `authHeaders`: `login`'s post-save "here's what your token reaches" echo, and
238
+ `chat`'s tenant/project derivation. Both authenticate, so both stamp the token's client
239
+ report (added in 0.8.9) — and a hand-built header stamps **NULL**, wiping the CLI version
240
+ and reference hash the platform had recorded.
241
+
242
+ Caught within an hour of 0.8.9 reaching staging: a workspace reported `0.8.9 · reference
243
+ current` at 12:16 and read `not reported` at 12:34. That looked exactly like the documented
244
+ "the column holds what ran LAST" behaviour and was really the CLI erasing its own row.
245
+ `cli-routes.test.ts` now fails on any hand-built bearer header, with a non-vacuity floor —
246
+ this package sits outside the repo's eslint scope, so a text guard is the only rung available.
247
+
248
+ ## [0.8.10] - 2026-09-07
249
+
250
+ ### Fixed
251
+ - **`octwin memos` failed with `400 project required` for a tenant-wide token — the exact
252
+ command the nudge tells you to run.** Every networked command can end with
253
+ *"✉ N memos from the platform — read them: `octwin memos`"*, and running precisely that with
254
+ an unpinned token answered a 400. Measured on staging: the nudge fired during a
255
+ `deploy --project …` (which supplied a project) and then named a command that could not run.
256
+
257
+ A memo has no project dimension to scope by — neither `pack_memos` nor `pack_memo_reads`
258
+ carries a `project_id`, and the read ledger is keyed by tenant — so the three call sites now
259
+ use `/api/self/t/memos`. Passing `--project` still works and is no longer needed.
260
+
261
+ ## [0.8.9] - 2026-09-07
262
+
263
+ ### Added
264
+ - **Every authenticated request now says which CLI and which capability reference you are on.**
265
+ Two headers off `authHeaders` — `x-octwin-cli` and `x-octwin-kb` (the `content_hash` of your
266
+ pulled reference) — so `deploy`, `validate --remote`, `status`, `chat`, `logs` and `memos` all
267
+ carry them, from one place. The platform stores them against your deploy token and shows them in
268
+ the console (Settings → API tokens, and Platform → Packs → Feedback → Author tooling for us).
269
+
270
+ **Why the platform needs this from you.** Almost every field report so far turned out to be
271
+ either already fixed in a newer CLI or written against a stale reference — three very different
272
+ answers that look identical without these two facts. They were captured on `octwin feedback`
273
+ only, so an author who never filed a report was invisible. Nothing here affects auth: a request
274
+ that sends neither header authenticates exactly as before, and a value that could not travel as
275
+ a header (a corrupt `package.json`, an interrupted pull) is dropped rather than sent.
276
+
277
+ ### Fixed
278
+ - **The upgrade notice was skipped under `npx` — silencing the population most likely to be
279
+ stale.** The premise was that "`@latest` already resolves the newest", which holds for
280
+ `npx octwin-cli@latest` and is false for the bare `npx octwin-cli` the docs put in front of
281
+ external developers: npm serves that from its **cache**, so it can be arbitrarily old. Under
282
+ `@latest` the check is self-silencing anyway (you are already newest), so keeping it costs
283
+ nothing — only the advice line differs, because there is no global install to reinstall.
284
+
285
+ ## [0.8.8] - 2026-09-06
286
+
287
+ ### Added
288
+ - **`octwin login` can sign you in through a browser — no token is pasted anywhere.** Run it with
289
+ no `--token` and it prints an approval link:
290
+
291
+ ```
292
+ Open this link to approve: https://app.octwin.ai/cli-login?code=WDJB-MJHT
293
+ Code shown on that page: WDJB-MJHT
294
+ ```
295
+
296
+ A workspace admin opens it, checks which machine is asking, and clicks Approve; the CLI collects a
297
+ 30-day token by itself and saves it exactly where `--token` would have. `octwin login --token oct_…`
298
+ is unchanged, and CI should still skip login entirely and set `PACK_PLATFORM_URL` + `PACK_TOKEN`.
299
+
300
+ **The wait is bounded and resumable, because this CLI is mostly driven by an agent.** An agent's
301
+ shell call is killed after about two minutes, and a wait that outlives its caller is worse than a
302
+ short one — the process dies mid-handshake and the code the human is still typing belongs to
303
+ nothing. So `login` waits 90 seconds, then exits **75** (`EX_TEMPFAIL`) with the handshake kept in
304
+ `~/.octwin/pending-login.json`; running it again resumes the **same** code, which the platform
305
+ keeps alive for ten minutes. `0` still means signed in and `1` still means failed, so an agent
306
+ branches on the exit code instead of parsing prose. `--wait <seconds>` overrides the 90 (`--wait 0`
307
+ prints the code and returns at once), and `--json` prints the same facts as an object.
308
+
309
+ **Approve only a link you opened because you ran the command.** The link carries the code
310
+ (RFC 8628 `verification_uri_complete`), so the click *is* the consent: approving a link somebody
311
+ sent you would connect their machine to your workspace. The console says so on the page. The code
312
+ is printed here too, and shown there, so you can always tell one request from another.
313
+
314
+ The pending handshake gets its own file rather than a new key in `credentials.json`: every value
315
+ in that file is a token, and its one reserved key (`default_url`) is collision-free only because a
316
+ platform url always contains `://`. A second value shape there would end that argument.
317
+
318
+ - **A 401 now points at `octwin login`, not at the console.** With a 30-day browser token, renewing is
319
+ one command; the old hint sent people to mint a replacement by hand. Mint a token yourself if you
320
+ want a different lifetime — the console offers 30 / 90 days / a year / never.
321
+
322
+ ## [0.8.7] - 2026-09-06
323
+
324
+ ### Added
325
+ - **`validate` now reads a map's KEYS, not only its values.** The declaration walker reported four
326
+ things — an unknown key, a missing `required` key, a wrong scalar type, a value outside a closed
327
+ `enum` — and every one of them is about a VALUE. But a map's keys carry meaning in several
328
+ declarations (`roles.yaml` grant resources and verbs, `xrm.yaml` entity names), and those keys had
329
+ no check at all. Rule 5 replays `propertyNames` — both the `enum` and the `pattern` form.
330
+
331
+ This is why publishing the RBAC vocabulary was worth doing. A pack author wrote `case:` where the
332
+ grant key is `record.case`, passed offline `validate` clean, and learned the truth one bad guess at
333
+ a time from `--remote` round-trips. They gave up on custom RBAC over it. The check now says
334
+ `not a valid key here — must be one of "record.case", …` before a deploy is attempted.
335
+
336
+ It stays inside the file's false-positive rule because it is a **faithful replay**: the same `enum`
337
+ membership and the same JS regex engine Zod itself runs, over a pattern the platform generated. A
338
+ pattern this engine cannot compile is treated like a combinator — walk away, do not guess.
339
+
340
+ Verified 2026-09-06 against the shipped binary: across all 23 marketplace packs rule 5 reports
341
+ **nothing**, with 10 of the 12 pulled declaration schemas carrying `propertyNames` (so the silence
342
+ is a result, not an unexercised branch). A deliberately broken key is caught on both branches — a
343
+ role named `Regional-Agent` against `^[a-z][a-z0-9_]*$`, and a grant key `case:` against an enum
344
+ vocabulary — each naming the file, the path and the rule it broke.
345
+
346
+ ### Fixed
347
+ - **`deploy` had stopped printing its seed counts entirely.** `printDeploySuccess` read
348
+ `r.summary.records` / `.updated` / `.images` / `.rules` / `.failed`, but the deploy route's
349
+ `summary` is a run-log **string** (`"clinic v1.2.0 — {…}"`), not an object. So every field was
350
+ `undefined`, `parts` stayed empty, and the `if (parts.length)` guard turned *"I am reading the
351
+ wrong field"* into *"there was nothing to say"* — the block printed nothing and looked like a
352
+ deploy that simply seeded no rows. The counts have been on `r.seeded`, keyed by seed kind, and
353
+ `printSeedCounts` already renders them (filtering zeros, which is what makes a partial seed
354
+ legible). The dead block is gone and `printSeedCounts` is called instead.
355
+
356
+ Same shape as the bug 0.8.6 fixed in `validate`: *no data* and *no problem* rendering identically.
357
+ Two independent places in this CLI reached it within one week.
358
+
359
+ ## [0.8.6] - 2026-09-04
360
+
361
+ ### Changed
362
+ - **`validate` can no longer print a `✓` for a check that did not run.** Each of the eight
363
+ checks now RETURNS an `Outcome` — `passed`, `findings`, or `not-run` — and a single printer
364
+ renders all of them, groups the skips by reason, and computes the exit code. Previously each
365
+ check printed its own `✓` inside the `if` that held its data, with a `skipped[]` array and a
366
+ `skipReasons` Map maintained alongside; it was correct because someone was careful, and the
367
+ next check added was one `if` away from lying again.
368
+
369
+ It had lied twice, in the same shape: the KB-dependent block once printed ONE `✓` above six
370
+ checks whose own `✓`s lived inside their `if`s (so a run with no pulled KB read as "one check,
371
+ passed", and an entire backlog batch reached production that way), and `yamlDocs()` once
372
+ dropped an unparseable file so `validate` printed all-green over a pack the platform's importer
373
+ then refused. **An unparseable file is not "no findings", it is "no idea", and those must never
374
+ print the same** — a check that returns nothing now cannot reach the `passed` branch at all.
375
+
376
+ Output is unchanged for a normal run. `passed` carries its own line, so the reserved-entity
377
+ check keeps the narrower wording it uses when the catalog rules are absent from an older pull.
378
+ `--require-kb` is now one line over the returned skip list.
379
+
380
+ Verified 2026-09-04: all 23 marketplace packs exit 0 with no findings; a pack with no pulled KB
381
+ prints zero `✓` for the six KB-dependent checks plus one grouped ⚠ naming all six, and
382
+ `--require-kb` exits 1; a duplicate YAML key still exits 1 naming the file.
383
+
384
+ ## [0.8.5] - 2026-09-02
385
+
386
+ ### Added
387
+ - **`octwin work --unrouted`** — the inbox filtered to items in NO queue. The route has always
388
+ supported the filter; the CLI never passed it, so `--queues` could tell you a count and not
389
+ show you the items behind it.
390
+
391
+ ### Changed
392
+ - **`octwin work --queues` leads with the unrouted count.** It was already printed, as
393
+ `(unrouted: N open)` tucked under the queue list. A routed item sits in a list someone works;
394
+ an unrouted one sits in nobody's, so it is the only line on that screen that means "these are
395
+ lost". It is now a warning block that names the likely cause and points at `--unrouted`. Still
396
+ silent at zero — a clean project stays quiet, or the warning stops meaning anything.
397
+
398
+ ## [0.8.4] - 2026-09-02
399
+
400
+ ### Fixed
401
+ - **A deploy whose seed failed still led with `✓ Deployed`.** The step errors were never
402
+ missing — `readDeployProgress` has collected every `status:'error'` frame into `stepErrors`
403
+ since 0.1.14, printed them, and exited non-zero. They were printed *after* `printDeploySuccess`,
404
+ so the first and largest line said `✓ Deployed` and the ⚠ came underneath. A pack author
405
+ reported their deploy as a clean success while its demo seed had failed; they were reading the
406
+ transcript exactly as written. The count now goes INTO the headline, which reads
407
+ `⚠ Deployed <id>@<v> onto <target> WITH n failure(s) — data may be incomplete`. `octwin seed`
408
+ gets the same treatment. *A signal that arrives after the verdict is not a signal.*
409
+ - **The non-streaming deploy and seed paths had no step-error check at all.** With no SSE frames
410
+ to read, a failed seed printed a clean `✓` on that path even after the streaming one was fixed
411
+ in 0.1.14. Both now read `seed_failed` from the response body (new in the platform alongside
412
+ this release), print each failed kind with its reason, and exit non-zero.
413
+ - **A partial demo seed is now reported as partial.** The platform no longer abandons the rest of
414
+ the seed when one demo row throws, so `Seeded:` gains an `n row(s) FAILED` part — without it a
415
+ partial seed reads as a complete one that simply had fewer records than the author wrote.
416
+
417
+ ## [0.8.3] - 2026-09-02
418
+
419
+ ### Fixed
420
+ - **`validate` printed all-green over a pack the platform could not import.** Every YAML file was
421
+ parsed inside a `try { … } catch { return [] }`, so a file that failed to parse was silently
422
+ DROPPED from the list and every later check skipped it. The `catch` was justified in a comment —
423
+ "a syntax error is the structural gate's to report" — and that was not true: the structural gate
424
+ (`validatePackBundle`) checks paths, extensions and traversal, and never parses YAML.
425
+
426
+ Measured 2026-09-02: a duplicate `s_header:` key in a locale file passed `octwin validate` twice
427
+ and was then refused by the platform importer with `Map keys must be unique at line 24,
428
+ column 3` — the same message, from the same `yaml` version the CLI already had. The author's only
429
+ clue was a red box in the console naming no file.
430
+
431
+ YAML syntax is now its own announced gate that FAILS: it names each unparseable file with the
432
+ parser's own message and exits non-zero, before any check reads a document. An unparseable file is
433
+ not "no findings", it is "no idea", and the two must never print the same.
434
+
435
+ - **Three more readers that turned "I could not read this" into a normal answer.** Swept every
436
+ discarding `catch` in the CLI after the above; three collapsed a real fault into an ordinary state:
437
+ - `readCreds` returned `{}` for a CORRUPT `credentials.json` as well as an absent one, so a token
438
+ the author knows they saved simply stopped being found and the advice was "log in" with no
439
+ mention of why. Now names the file and the parse error, once per process (it has three call
440
+ sites and one broken file is one problem).
441
+ - `readLocalKb` returned null for a half-written `.octwin/platform-kb/index.json` as well as a
442
+ never-pulled one — so an interrupted pull read as "you have not pulled yet" and the author
443
+ re-ran the command that left it that way. [`kb-path.ts`](src/lib/kb-path.ts) states this rule
444
+ for the DIRECTORY lookup; the index read beside it kept collapsing anyway. It now says which.
445
+ - The deploy progress stream discarded unparseable SSE frames silently. Skipping one is right;
446
+ skipping ALL of them left `terminal: null`, reported as "the stream ended without a terminal
447
+ event" — true, and indistinguishable from a dropped connection. The count is now named when
448
+ (and only when) no terminal frame arrived, pointing at a CLI older than its platform.
449
+
450
+ The other five discarding catches were read and LEFT: each returns a documented fallback for a
451
+ state that is genuinely ordinary (no update-check network, a heartbeat SSE frame with no `data:`).
452
+ `scripts/` was swept too and is clean by design — `kb-dataflow.mjs` skips a fence it cannot parse,
453
+ and `check-kb-examples.mjs` holds a non-vacuity FLOOR of 130 parsed fences so the skip cannot go
454
+ quiet. That is the better pattern where a skip is legitimate.
455
+
456
+ ## [0.8.1] - 2026-08-27
457
+
458
+ ### Fixed
459
+ - **Logging in made `platform-kb pull` WORSE.** The command preferred the tenant-scoped route
460
+ whenever a token was saved, and that route is guarded by `pack:deploy` — so a developer holding a
461
+ token without that scope got a **403 on a pull that succeeds with no token at all**. It is the
462
+ likeliest token a new developer holds: no console preset could even reach `pack:deploy` before
463
+ 2026-08-26. `platform-kb` now always uses the anonymous route and never sends a credential.
464
+ `--token` is accepted and ignored so existing scripts keep working.
465
+
466
+ The platform serves the *same* bundle from all three of its views — its route file states the
467
+ payload is identical and tenant-independent — so the authed route returned nothing extra and the
468
+ credential could only ever subtract. The two reasons the old code gave did not survive checking:
469
+ "keeps the author's own instance the source of truth" is vacuous (both routes are the same
470
+ instance), and "works against platforms that predate the public rung" is a backward-compatibility
471
+ shim this codebase does not carry.
472
+ - **The drift nudge now works without a token.** It polled through the same authed route, so an
473
+ author inspecting data with a narrow token was told, every time, that it *could not* check —
474
+ advice in place of the signal. Both the poll and the apology are gone: the poll is anonymous, so
475
+ it simply works.
476
+
477
+ ## [0.8.0] - 2026-08-27
478
+
479
+ ### Added
480
+ - **Five new command families, closing the gap between what a token reaches and what the CLI can
481
+ ask.** A **Full CLI access** token spans the whole scope registry, and four of those domains had
482
+ no command at all — so the reach existed and there was nothing to type.
483
+ - `octwin automation [campaigns]` — every job the pack's declaration produced, with status,
484
+ interval and last result (matched / acted / errors), under a health line whose counts come from
485
+ SQL rather than from filtering the page (the job list is capped server-side, so a client-side
486
+ count would depend on the cap). Writes: `run` · `pause` · `resume` · `send <campaignId>`.
487
+ - `octwin integrations` — what the pack **declares** beside what is actually **configured**, in
488
+ one view, because a connection declared and never configured is the commonest reason an
489
+ integration silently never fires and neither list alone can show it. Plus
490
+ `preflight <key>` (diagnosis, no outbound call, `integrations:read`), `test <key>` (a live call,
491
+ `integrations:write`, exits 1 on failure), `deliveries [id]`, `retry|cancel|send-now <id>`,
492
+ and `events` for what arrived inbound.
493
+ - `octwin journeys [id] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]`
494
+ — the declared journeys, measured. Same flag grammar as `octwin analytics` on purpose: a journey
495
+ funnel and an entity funnel are the same question about different subjects. `--definition` is
496
+ the one view that works with no traffic.
497
+ - `octwin performance [--detail]` — the project's business indicators, each with its delta against
498
+ the previous window and the `why` naming the declaration it came from. Needs `records:read`, not
499
+ a `performance:*` scope (there is none), so a read-only token already reaches it.
500
+ - `octwin usage` — model calls, tokens and cost, by model / kind / agent / channel; project when
501
+ one is pinned, workspace otherwise. Needs no particular scope. **Model spend only** —
502
+ WhatsApp/Meta message billing is operator-only and outside the token scope registry, which the
503
+ output says rather than leaving as an absence.
504
+ - **`octwin automation run|pause|resume|send` accept the declaration KEY.** The routes take a UUID
505
+ and answer a bare *"Malformed identifier in the URL"* 400 for anything else — but the key
506
+ (`cart_recovery_nudge`) is what the list prints and what the author wrote in their own YAML. The
507
+ key is now resolved against the list route, and an unknown one fails naming the keys that exist.
508
+
509
+ ### Fixed
510
+ - **A body-less write sent `content-type: application/json` and no body, so Fastify answered a bare
511
+ `400 Bad Request`.** `apiSend` set the header unconditionally; every caller until now passed an
512
+ object, so the branch was dead and the bug latent. The first body-less POST (`automation run`) hit
513
+ it immediately. The header is now omitted when there is no body — rather than each caller learning
514
+ to pass `{}`, which is the per-site divergence that helper exists to prevent.
515
+ - **Four URLs were exempt from the route guard.** `cli-routes.test.ts` reads a template literal in
516
+ the first argument position, so `apiGet(base, t)` — a bare identifier — was never checked. That
517
+ covered the `scheduling` engine-state and `agents` roster calls, and would have covered the new
518
+ `journeys`, `performance` and `usage` ones. All now spelled as literals: the guard checks **62**
519
+ URLs, up from 45.
520
+
521
+ ## [0.7.3] - 2026-08-26
522
+
523
+ ### Changed
524
+ - **A marketplace listing request is now OPT-IN: `octwin deploy --request-listing`.** A plain
525
+ `octwin deploy` says nothing about the public marketplace — it neither asks for a listing nor
526
+ gives one up. `--withdraw-listing` retracts, including an approved listing.
527
+ **Why:** the request was read from the manifest's `listing.public` on every deploy, and deploying
528
+ is how you *test*. 17 of the 23 published packs carry that key, so an edit-deploy-chat loop filed
529
+ a review request every time round. Worse, the *absence* of the key meant **retract** — so an
530
+ ordinary deploy of a pack whose manifest you had not yet annotated could wipe a listing you
531
+ already had. `--request-listing` still requires `listing.public: true` in the manifest (the
532
+ manifest states that the pack is a product; the flag is you choosing to ask), and the flag
533
+ without the key is a clean error rather than a silent no-op.
534
+ - **The listing verdict distinguishes "you submitted this" from "your approval expired".** They are
535
+ both `pending` and they mean opposite things to an author. A deploy that changes the content of an
536
+ approved pack now says so in those words — an approval covers only the content it was made
537
+ against, so it cannot survive an edit.
538
+
539
+ ### Added
540
+ - **A one-line nudge when your manifest asks and your deploy did not.** A pack carrying
541
+ `listing.public: true` that has never been submitted now prints the flag to use, so an opt-in
542
+ default cannot turn into waiting for a review nobody requested.
543
+
544
+ ### Fixed
545
+ - **The "newer CLI" and "KB drifted" notices now print when a command FAILS.** All three nudges
546
+ (CLI upgrade · KB drift · unread memos) were awaited at the very end of `main()`, which silently
547
+ meant *only on success* — `die()` is a hard exit, so a failed deploy, an auth error or a bad
548
+ manifest printed none of them. That is backwards: a stale capability reference is a leading cause
549
+ of the failure an author is staring at, so the run that most needed the nudge was the only one
550
+ that never got it. They are now computed before the command and printed however it ends.
551
+ - **The CLI-upgrade notice is no longer hidden from piped output.** It began with
552
+ `if (!process.stdout.isTTY) return`, so every agent-driven and CI run — the readers that cannot
553
+ notice an outdated CLI on their own — was the one audience never told. Its two siblings had
554
+ already dropped that gate for exactly this reason; now all three agree. Still skipped under
555
+ `npx`, where there is nothing to upgrade.
556
+ - **`octwin login` now reports KB drift too.** It is networked and usually the first command of a
557
+ session, i.e. the best possible moment to say the reference moved, and it was excluded.
558
+ - **A memo poll can no longer fail the command it was annotating.** It went through `apiGet` →
559
+ `fetchOrDie`, whose job is to `die` on a network failure — so an unreachable platform turned an
560
+ observer into a command failure. It now uses a plain, timed, fail-silent fetch.
561
+ - **A failing command reports its real exit code again on Windows.** Moving the polls before the
562
+ command made `process.exit()` abort with `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)`
563
+ and return **127** instead of 1 — so CI saw neither success nor the documented failure code. The
564
+ CLI now unwinds through a `CliExit` and lets the event loop drain, which is the only teardown
565
+ that is clean once the polls have opened sockets. `--check`'s exit 2 and the streaming commands
566
+ were re-verified.
567
+
568
+ ## [0.7.2] - 2026-08-20
569
+
570
+ ### Fixed
571
+ - **`octwin feedback` now tells you what is already waiting, on stdout.** It discarded the submit
572
+ response entirely — destructuring `{ status, json }`, checking the status and never reading the
573
+ body — then printed *"a reply arrives as a memo, this CLI will tell you when one is waiting"*. The
574
+ platform started returning the unread counts on that response (2026-08-20), so the answer was
575
+ there to print and was being thrown away.
576
+ **Why it matters more than it sounds:** `notifyIfMemosWaiting` already covers this command and was
577
+ firing correctly — but it writes to **stderr**, and the authors driving this CLI are agents reading
578
+ stdout. The one signal that mattered was addressed to a channel nobody was listening on. On
579
+ 2026-08-19 an author submitted two reports two hours after three replies were published and
580
+ re-reported five findings those replies had already closed. Now the count prints in the same block
581
+ as the acknowledgement, naming the command, and says outright when a reply may already answer what
582
+ you just sent. Against an older platform the field is absent and the previous line stands.
583
+
584
+ ## [0.7.1] - 2026-08-19
585
+
586
+ ### Fixed
587
+ - **`work decide` now says when an action moves nothing.** `to_stage` is optional — an action may
588
+ record a decision without a transition — but both outputs communicated that by omission: `--dry-run`
589
+ left `to_stage: null` sitting among twenty other keys, and a real apply printed a bare
590
+ `✓ Applied '<action>'`. An author read both, shipped `request_more_info` with no `to_stage`, and
591
+ lost the `awaiting_customer` their whole customer-reply capture branched on — their words: *"Nothing
592
+ about this failed loudly; it just quietly never did the one thing the whole feature depends on."*
593
+ The preview now states `→ does NOT change the status (this action only records a decision)` and the
594
+ apply says `status unchanged (the action declares no to_stage)`. The console's decision panel got the
595
+ same sentence in the same change.
596
+
597
+ ## [0.7.0] - 2026-08-18
598
+
599
+ ### Added
600
+ - **`octwin memos` — the channel answers now.** `octwin feedback` was one-way: an author reported and
601
+ heard nothing, ever. Two kinds arrive here, both printed in full — a **reply** to a report you sent,
602
+ and a **notice** published to every author (a new capability, a deprecation, a breaking change) —
603
+ each carrying `info` / `action_required` / `breaking`. Reading marks them read; `--all` re-reads
604
+ history and acks nothing (also the escape hatch if a piped `--json` consumer died halfway).
605
+ - **Every networked command now says when a memo is waiting**, one line to stderr, always naming the
606
+ command to run. It rides work that was already networked, never throws, and is **deliberately not
607
+ TTY-gated** — the same reasoning as the KB drift nudge, whose docblock says it plainly: the primary
608
+ reader of this CLI is an authoring agent whose output is piped, and gating on `isTTY` means the one
609
+ reader that cannot notice on its own is the only one never told. Unlike that check there is no local
610
+ marker file: read state is per-workspace on the platform, so an agent in a fresh container still
611
+ learns about an unread reply.
612
+
613
+ Why it matters, measured: in August 2026 four reports sat unread for nine days, and one author
614
+ replaced every `$t()` call with hardcoded English across all 16 flows of a production pack to work
615
+ around a bug that had been fixed two days earlier. Nothing could tell them.
616
+
617
+ ## [0.6.1] - 2026-08-18
618
+
619
+ ### Fixed
620
+ - **`octwin status` worked for no pack at all, and had not since 2026-08-01.** It sent the manifest's
621
+ **bare** id (`clinic`) to a route that requires the qualified `<owner>.<name>` — and a manifest
622
+ cannot legally declare the qualified form, because the owner segment is attached at publish time
623
+ from the authenticated publisher (both the platform and this CLI reject a `.` in it). So the route
624
+ answered 404 for every pack, every time. It now resolves the owner from `whoami`'s `tenant_slug`,
625
+ and takes an explicit `octwin status <packId>` when you already know it (from `octwin agents` or
626
+ `octwin projects`, both of which print qualified ids). Reported by two pack authors, who lost time
627
+ to a command that said their pack was missing seconds after a deploy the platform had accepted.
628
+ - **A 404 from `octwin status` no longer always reads "not installed".** That one branch covered four
629
+ different failures — a malformed pack id, an unknown tenant, an unknown/pinned-elsewhere project,
630
+ and the genuine no-active-install — so the message named the *least* likely cause and sent an author
631
+ chasing a deploy that had already succeeded. The route says which; the CLI now relays it, and adds
632
+ the "run `octwin deploy` first" hint only to the case it fits.
633
+ - **`octwin logs` always reported "No conversations"**, including immediately after an `octwin chat`
634
+ that had plainly worked. It read `conversations` off the response; the platform moved that endpoint
635
+ to the shared page envelope on 2026-08-09 and answers `rows`, which every other list command here
636
+ already reads. Both authors filed this as replica lag or a cache gap — nothing lagged, the payload
637
+ was simply never read. `octwin logs <conversationId>` was unaffected.
638
+
639
+ ## [0.6.0] - 2026-08-09
640
+
641
+ ### Changed
642
+ - **`octwin cases` is now `octwin work`, and it works again.** The platform's work-inbox
643
+ consolidation (2026-08-08) replaced the case-only surface with `/work` over every entity a pack
644
+ declares worked, and renamed the scopes to `work:read`/`work:write` — which left every `octwin
645
+ cases` call a silent 404 in a published CLI. The command family is renamed to match the platform:
646
+ `octwin work [recordId] [--queues]`, with writes `assign` / `note` / `decide` (the declared
647
+ operator actions, `--dry-run` previews) and `stage <id> --to <s>` riding the one platform-wide
648
+ stage verb (`POST …/xrm/records/:id/stage`, needs `records:write`). `transition` is gone with the
649
+ route it called.
650
+ - **`octwin orders transition` rides the same stage verb.** `POST …/orders/:ref/transition` was
651
+ retired server-side in the same consolidation; the CLI now resolves the order's record id from the
652
+ detail read and moves the stage through `…/xrm/records/:id/stage` (scope: `records:write`).
653
+
654
+ ### Added
655
+ - **A route guard so this cannot rot silently again**: `src/lib/cli-routes.test.ts` extracts every
656
+ `/api/self/**` URL the CLI builds and asserts it (method included) against the platform's dumped
657
+ route table — the gap that let both breakages above ship.
658
+
659
+ ## [0.5.1] - 2026-08-01
660
+
661
+ ### Fixed
662
+ - **`validate --remote` blamed the platform's version for a bad `--project`.** Every 404 from
663
+ `POST /api/self/p/packs/validate` printed *"this platform has no /packs/validate endpoint yet
664
+ (older version)"*. That was accurate when the route resolved nothing, but it now resolves the
665
+ tenant and project **before** validating — and an unknown project, or one outside a token's pin,
666
+ answers 404 by design. So a typo in `--project` sent you looking for a version mismatch instead of
667
+ at the flag. The two cases are now told apart by the response **body**, not the status: a missing
668
+ route is Fastify's `error: 'Not Found'` and keeps the old wording, while the platform's own
669
+ `project '<slug>' not found under tenant` is printed as-is, with the flags to check.
670
+
671
+ ## [0.5.0] - 2026-08-01
672
+
673
+ ### Added
674
+ - **`octwin seed [--pack <packId>]`** — apply the pack's demo/reference data to the project it is
675
+ installed on, without redeploying: `xrm.yaml` `demo:` records + scheduling availability, the
676
+ commerce catalog, and the demo operator topology. Reports what **each kind** produced.
677
+ Previously seeding was reachable only as `deploy --seed`, because the platform's seed endpoint was
678
+ keyed on an install id, guarded platform-admin, and carried no tenant/project segments — so the
679
+ `/api/self/**` surface could not reach it and a `pack:deploy` token never could. Re-seeding meant
680
+ a full redeploy or asking an operator. Idempotent and cheap to re-run: records upsert, and
681
+ existing media is REUSED rather than regenerated, so a second pass reports zero images.
682
+
683
+ ## [0.4.0] - 2026-08-01
684
+
685
+ ### Added
686
+ - **`octwin projects create` / `octwin projects rm`** — a **disposable end-to-end environment** from
687
+ the CLI, which is what marketplace developers have been missing. `octwin deploy` has always needed
688
+ a project that already existed and the CLI could only *list* them, so standing up a throwaway
689
+ full deployment meant opening the console or asking an operator. Now:
690
+ ```bash
691
+ octwin projects create "Scratch" # → slug `scratch`
692
+ octwin deploy --project scratch --seed # publish + install + demo data
693
+ octwin chat "hi" --project scratch # talk to it (--script for a whole conversation)
694
+ octwin projects rm scratch --yes # throw it away
695
+ ```
696
+ A throwaway environment is deliberately **not a special kind of thing** — it is an ordinary
697
+ project in your own workspace, so it inherits your plan, entitlements, RBAC and teardown with no
698
+ separate lifecycle. `create` derives the URL slug from the name (`--slug` pins one, `--pack`
699
+ installs an already-published pack). `rm` is a HARD delete — the project and everything cascading
700
+ from it, no undo, not the same as archiving — so **without `--yes` it only prints what would be
701
+ destroyed**, making the dry run the default; the impact list is derived from `pg_constraint`, the
702
+ same payload the console's confirm dialog renders. Both need `projects:write`, which a
703
+ `pack:deploy` token does **not** confer — the CLI names that scope in the 403 hint.
704
+
705
+ ## [0.3.0] - 2026-07-31
706
+
707
+ ### Added
708
+ - **`octwin projects`** — the `--project <slug>` values your token can actually name, with the plan's
709
+ project cap. Every project-scoped command takes that flag and nothing printed the list, so an
710
+ author whose token was not pinned had to guess, and a wrong guess 404s identically to a project
711
+ that exists but has no install. `--archived` includes archived projects; `--json` for raw.
712
+ A `pack:deploy` token reaches it: the platform route was widened to accept `pack:deploy` alongside
713
+ `projects:read`, because `pack:deploy` is `access: 'special'` and confers nothing else — the
714
+ command would have 403ed for exactly the person it exists for.
715
+ - **`octwin validate --require-kb`** — turn a skipped KB-driven check into a failure, for CI.
716
+
717
+ ### Fixed
718
+ - **`octwin validate` printed ✓ with two of its three checks switched off.** The render-intent and
719
+ primitive-argument checks read `<packDir>/.octwin/platform-kb/`, joined onto the pack directory
720
+ EXACTLY — so an author who pulled once at a repo root and then worked in `packs/<name>/` had both
721
+ checks silently disabled, and the ✓ printed above them unconditionally while the per-check ✓s lived
722
+ inside the `if`s. An entire batch of platform-backlog entries reached production through that hole.
723
+ Three fixes: the KB is now found by **walking up** (one pull at a repo root covers every pack under
724
+ it, `src/lib/kb-path.ts`); the readers report **three states** — absent, malformed, ok — so a
725
+ half-written pull no longer reads as "you haven't pulled yet"; and a skip is **announced on the
726
+ last line printed**, because a caveat above a ✓ does not reach a reader who stopped at the ✓.
727
+ Warn-and-exit-0 stays the default (the KB is a gitignored cache and `platform-kb pull` needs
728
+ `pack:deploy`, so a hard failure would break a fresh clone) — the defect was the silence, not the skip.
729
+ - **`octwin status` said "live and current" for a pack that could not receive a message.** Every field
730
+ it printed was process-global: `registered` is true for a pack ANY project on that instance loaded.
731
+ Dispatch is per-project and takes the oldest ACTIVE install, ignoring the rest. The route now
732
+ filters `status = 'active'` (an archived install used to report live) and compares against the real
733
+ dispatch target, so status can say plainly that another pack owns this project's messages.
734
+ - **`octwin validate --remote --project does-not-exist` answered `{ ok: true }`** while `deploy` with
735
+ the same flag 404ed. The platform route resolves tenant and project before validating now.
736
+
737
+ ### Changed
738
+ - **`octwin validate --remote` also runs the platform's flow LINT.** It used to run only at pack load,
739
+ so a finding of severity `error` — an `assign:` whose value is a quoted literal, a `$t()` key with
740
+ no namespace — passed validate and failed the deploy. Offline `validate` cannot do this: the lint
741
+ needs a template-expanded `FlowDef`, which only the server builds.
742
+ - **`record_aggregate` / `record_group` now REQUIRE a scope** (`contact_id` or `all: true`) — a
743
+ platform contract change the offline arg check replays from the KB. Re-pull after upgrading.
744
+
745
+ ## [0.2.0] - 2026-07-30
746
+
747
+ ### Fixed
748
+ - **`records`, `cases`, `orders` and `catalog` printed a correct total and ZERO rows.** The platform
749
+ collapsed its eight per-domain list envelopes into one `Page<T>` whose collection key is `rows`;
750
+ the CLI still read `json.records` / `.cases` / `.orders` / `.products` behind a `?? []`, so the
751
+ fallback turned a key rename into an empty list instead of an error. `octwin records doctor`
752
+ reported *"5 record(s)"* and then *"(none — mint a `records:read` token …)"*, blaming the user's
753
+ credentials. All five readers now go through one `readPage` helper
754
+ (`src/lib/page.ts`) with a test pinned to the real envelope — a hand-copied key name in an
755
+ untested CLI is what shipped this, so the fix is the single reader, not five edits.
756
+ - **`--flag=value` silently did nothing.** `--limit=5` became the key `limit=5` and the command ran
757
+ with its default. Now parsed; `--set`/`--param` also accept repetition.
758
+ - **`octwin catalog` always printed `Products: 0`** — it counted the page, and the route's `total`
759
+ had never been read.
760
+
761
+ ### Added
762
+ - **Write commands — a pack author can now exercise the state their pack creates, not just read it.**
763
+ Every one needs the matching `:write` scope; a leading verb (`create`/`note`/`transition`/…) is
764
+ what distinguishes a write from the read that shares its command.
765
+ - `records create|patch|stage|note`, `records tasks`, `records task complete`
766
+ - `cases assign|note|transition|decide` — `decide --dry-run` previews the customer-facing copy and
767
+ resulting status **without committing** (that route needs only `cases:read`)
768
+ - `orders transition|refund` · `catalog availability|stock` · `agents set`
769
+ - `scheduling rules|rule add|rule rm|exception add|exception rm`
770
+ - **`octwin feedback`** submits the pack's `FEEDBACK.md` to the platform team, attaching the pack
771
+ version, this CLI's version and the `content_hash` of the capability reference you pulled — the two
772
+ facts that separate a real platform gap from something already fixed or a stale KB. Replaces
773
+ "hand the file to the platform team".
774
+ - **`--offset` on every list**, and a line saying when a list was truncated and what to run next.
775
+ `has_more` was on every response and no command surfaced it, so a partial list looked complete.
776
+
777
+ ### Changed
778
+ - `orders refund` **exits non-zero when the payment gateway refuses.** The route answers `200` with
779
+ the verdict in `gateway`, so reporting the HTTP status would tell an operator money moved when it
780
+ did not.
781
+ - A 403 on a record or case write now says it may be an **RBAC grant gap** rather than a missing
782
+ scope: the scope is checked on the token, then the verb is re-checked against that specific row.
783
+
784
+ ## [0.1.21] - 2026-07-30
785
+
786
+ > **Never published to npm.** Bumped in-repo, then superseded hours later by 0.2.0 — everything
787
+ > below ships as part of that release. Kept as its own section because the changes are distinct.
788
+
789
+ ### Added
790
+ - **`deploy` and `status` report the marketplace-listing verdict.** A manifest carrying
791
+ `listing.public: true` put the pack into an operator review queue, and the state existed only in
792
+ the console — so an author working from the CLI got no acknowledgement that the request had
793
+ registered, and never saw a rejection note (which the platform *requires* precisely because it is
794
+ their only feedback). Both commands now print it via `printPublicListing`, and stay silent for a
795
+ pack that never asked. `status` also distinguishes *approved* from *actually public*: an approval
796
+ pins the content sha it reviewed, so any edit returns the pack to the queue on its own.
797
+ - **`octwin pull` is discoverable.** It was implemented and answered `--help`, but appeared in
798
+ neither `octwin help` nor the README — the one command that recovers a deployed pack's only source
799
+ copy was invisible unless you already knew its name. Also added to the scope-requirement table, so
800
+ a 403 names `pack:deploy` instead of printing the generic hint.
801
+
802
+ ### Fixed
803
+ - **The capability-reference drift check no longer fails silently on a narrow token.** The `?meta=1`
804
+ poll needs `pack:deploy`, but it rides on every networked command — so an author inspecting data
805
+ with a `records:read`-only token got no drift signal at all, and a stale reference is exactly what
806
+ leads to inventing a primitive from memory. A 401/403 there now says so once.
807
+ - **README: casework is declared in `worklist.yaml`, not a `cases.yaml`.** That grammar was retired;
808
+ following the README produced a pack whose casework silently never loaded. `octwin media generate`
809
+ was also missing from the command table.
810
+ - **`records` no longer blames the plan for a route the plan does not gate.** `GET /xrm/entities`
811
+ carries only the scope guard, so a 403 on the entity list wrongly cited the `records` plan feature.
812
+
813
+ ### Removed
814
+ - **Unreachable rename branches in `init`.** The agent-id and `main`-flow renames sat behind
815
+ `--agent` / `--flow` options the CLI never parsed, against a template that ships no `main` flow —
816
+ both branches could never fire, and their patterns would have matched nothing if they had.
817
+
818
+ ## [0.1.20] - 2026-07-29
819
+
820
+ ### Added
821
+ - **`octwin validate` catches primitive arguments the platform would drop — offline, no token.** A
822
+ primitive takes the arguments it declares and silently ignored the rest, so an invented or
823
+ misspelled `args:` key deployed clean, ran clean, and simply did nothing: `record_list args: {
824
+ order: … }` (the argument is `sort`) never ordered, and `booking_cancel args: { booking_record_id:
825
+ … }` meant the REQUIRED `record_id` never arrived, so cancelling just failed. Validate now replays
826
+ the platform's contract from the pulled KB (`primitives/*.json` → `inputSchema`) and names the bad
827
+ argument, what the primitive takes, and any required argument that is missing. Scanning the 21
828
+ shipped marketplace packs found **ten** such errors across five packs. Keys are checked, values are
829
+ not — an `args:` value is normally an expression string resolved at runtime.
830
+ [`args-check.ts`](src/lib/args-check.ts).
831
+ - Silently skipped when the KB has not been pulled yet, like the render-intent check beside it.
832
+ - It cannot see inside a `use:` template body (expanding one is the platform's job) — `octwin
833
+ validate --remote` covers that case, and now also reports unreachable `$bind.<path>` reads and
834
+ `outputs:` port typos, which have no offline equivalent.
835
+
836
+ ## [0.1.19] - 2026-07-29
837
+
838
+ ### Added
839
+ - **`octwin validate` catches render-intent fields the platform would drop — offline, no token.** Each
840
+ render intent takes a fixed field set; a key outside it used to be swallowed at load and dropped at
841
+ render, with no error anywhere — just a card missing what the author wrote. Validate now replays the
842
+ platform's own contract from the pulled KB (`render-intents/*.json` → `allowed_keys`) and names the
843
+ bad field, the intent, and what is allowed. Scanning the 21 shipped marketplace packs found two real
844
+ instances in 223 intents: a `text_card` carrying `buttons` (the fallback's only escape button never
845
+ rendered) and a `list_picker` carrying a non-existent `group_by` (a whole mapper step feeding a
846
+ fabricated parameter). Silently skipped when the KB has not been pulled yet.
847
+
848
+ ### Changed
849
+ - **`octwin platform-kb pull` now explodes the reference and writes an `INDEX.md`.** A pull used to
850
+ write ~787 KB across 34 flat files — about 225k tokens, more than the pack being authored — so
851
+ finding one primitive meant reading a 122 KB catalog. Catalogs the platform describes as
852
+ enumerable are now written **one file per entry** (`primitives/record_list.json`,
853
+ `render-intents/carousel.json`, `declarations/xrm.json`, …), with `INDEX.md` mapping every doc and
854
+ all 171 entries to its file with a one-line summary. Reading one primitive went from ~38k tokens to
855
+ ~700. The layout comes from the platform (an `entries` descriptor on the bundle index) rather than
856
+ being hardcoded here, so this CLI keeps working against any platform version and a platform that
857
+ adds a catalog needs no CLI release. The pulled directory is cleared each pull, so a withdrawn
858
+ capability cannot linger as a file the authoring agent reads as current — its *contents* are
859
+ cleared rather than the directory itself, since on Windows a directory that is any shell's working
860
+ directory cannot be removed.
861
+
862
+ ## [0.1.18] - 2026-07-29
863
+
864
+ ### Fixed
865
+ - **The bundle collector and the platform's repo import disagreed about what a pack directory
866
+ contains.** `collectBundleFiles` had its own rule and kept three things the operator's GitHub
867
+ import drops: `*.ts`/`*.tsx`, `__snapshots__/` and `*.example`. Because the server rejects a
868
+ `.ts` outright, the same directory imported cleanly from GitHub and failed to deploy from
869
+ disk with `'…': executable code is not allowed` — an error blaming a file that was never
870
+ meant to ship (a snapshot test beside a flow, or the documented `xrm.yaml.example` template).
871
+ The rule now lives in a vendored `lib/pack-source.ts` beside the vendored validator, and a
872
+ parity test in the platform drives both copies over one table.
873
+
874
+ ### Removed
875
+ - **`pack.json` is gone — the saved login IS the deploy target.** The file had eroded to a single
876
+ field, `platform_url`, which `octwin login --url … --token …` already states; `tenant`/`project`
877
+ became optional overrides once deploy tokens started carrying their own tenant and project pin.
878
+ So it was a second home for a fact `login` knew, and it cost real things: a machine-specific file
879
+ committed into every pack repo, a four-link config-precedence chain to document, and an
880
+ exclusion rule duplicated in the bundler and the server (which had already drifted apart).
881
+ `octwin init` and `octwin pull` no longer write one, and nothing reads one. **A pack directory is
882
+ now pack content and nothing else** — the same repo deploys from any machine, and a pulled pack
883
+ redeploys with no flags.
884
+
885
+ ### Changed
886
+ - **`octwin login` now sets the default deploy target**, not just the token: it writes a
887
+ `default_url` key alongside the token in `~/.octwin/credentials.json`. Every command resolves
888
+ **flags → env (`PACK_PLATFORM_URL`/`PACK_TENANT`/`PACK_PROJECT`/`PACK_TOKEN`) → saved login**.
889
+ An existing credentials file simply has no `default_url` until the next `octwin login` — which is
890
+ exactly what the "no platform url" error now tells you to run. CI is unaffected (it passes
891
+ `PACK_PLATFORM_URL` + `PACK_TOKEN` as env and never had a `pack.json` — that file is retired).
892
+
893
+ ## [0.1.17] - 2026-07-27
894
+
895
+ ### Added
896
+ - **`octwin pull <packId>` — write a DEPLOYED pack's source back to disk.** The inverse of
897
+ `deploy`. A pack pushed with the CLI lived on the platform as an artifact the runtime served
898
+ but nothing could hand back, so its only source copy was the machine that pushed it. Pull it,
899
+ fix it, redeploy it. Writes a `pack.json` (retired in 0.1.18 — the saved login is now the
900
+ target) so the pulled directory redeploys where it came from; `--version` picks a version,
901
+ `--force` overwrites a non-empty dir. You may pull a pack
902
+ your tenant OWNS (an operator token pulls any) — a pack you merely installed is not yours to
903
+ read.
904
+ - **`octwin chat --script <file>` — drive a whole conversation from one invocation.** One turn per
905
+ line, in order, in a single process over a single SSE connection, waiting for each turn to settle
906
+ before sending the next. Blank lines and `#` comments are skipped; `tap:<id>` presses a rendered
907
+ row/button (the rest of the line is kept verbatim, since a tap id is itself colon-delimited),
908
+ `media:<path>` uploads a file, and `media:<path> | caption` attaches one.
909
+
910
+ This exists because **chaining invocations races the agent loop.** A turn ends on a quiet gap
911
+ (`TURN_SETTLE_MS`), which can arrive while the server-side loop is still running — so
912
+ `chat A && chat B` lets B's inbound land mid-turn, and the agent fills required fields with
913
+ placeholder text (`service_id: "service-id"`) or starts a second workflow run. Those look exactly
914
+ like flow bugs and cost a field author three separate investigations. The one-turn-per-invocation
915
+ rule is now stated in `chat --help` and the header usage as well.
916
+
917
+ ### Changed
918
+ - **`octwin deploy` reports the suspended runs it invalidated.** A redeploy rebuilds the pack's
919
+ tools, and in-flight flow runs live in those tools — so every suspended conversation is dropped.
920
+ The deploy prints `ⓘ N suspended run(s) invalidated` when N > 0, instead of leaving the author to
921
+ discover it as a `workflow_resume_stale` on the next tap.
922
+
923
+ ## [0.1.16] - 2026-07-26
924
+
925
+ ### Added
926
+ - **A pack can ship committed images.** `jpg`/`jpeg`/`png`/`webp`/`gif`/`pdf` files in your pack
927
+ directory now travel as a separate `blobs` half of the bundle (base64 on the wire, `bytea` in
928
+ storage) instead of being rejected as "not an allowed pack file type". Declare each one under the
929
+ manifest's `static_assets: [{ file, key }]` and reference it from `config:` with the
930
+ `$pack_asset:<key>` sentinel — the platform uploads it to the media system at install and the
931
+ sentinel resolves to the served URL. `octwin validate` reports the image count alongside the file
932
+ count.
933
+ - **The size ceilings fail locally, before upload** — 2 MB per file, 32 MB of binary per pack,
934
+ mirroring the server. `svg` stays rejected: it is script-capable and these assets are served to
935
+ browsers.
936
+
937
+ ### Fixed
938
+ - **Committed images were silently corrupted.** `collectBundleFiles` read *every* file with
939
+ `readFileSync(full, 'utf8')`, so a JPEG went through a lossy UTF-8 decode and arrived mangled
940
+ server-side — a deploy that "succeeded" and rendered a broken image. Binary files now split off
941
+ into `blobs` and keep their bytes.
942
+ - **`octwin status` reported `loaded=(none)` for every pack, always.** It printed a `loaded_version`
943
+ field that stopped existing when reload moved to content-sha keying, which also meant the
944
+ version-drift warning underneath it could never fire. It now prints the **content sha** the
945
+ instance has loaded and the one the catalog holds (with the artifact's `origin`), and warns off the
946
+ platform's own `up_to_date` flag. The sha is the more useful fact anyway: re-publishing the *same*
947
+ version changes it, which is exactly the author's inner loop. A pack that is live and current but
948
+ **withdrawn** from the catalog now says so, rather than reporting a clean bill of health.
949
+
950
+ ## [0.1.15] - 2026-07-25
951
+
952
+ ### Added
953
+ - **`octwin agents [packId::agentId] [--prompt]` — see what your agent actually runs as.** The platform
954
+ gained *governed* agent settings: an operator platform default can override the `default_model`
955
+ (and history window / working-memory toggle) your manifest declares, and that override is invisible
956
+ from the pack source. The command prints each governed setting's **effective** value, **which layer
957
+ won** (project override → platform default → pack manifest), and warns explicitly when your pack's
958
+ declared value is *not* in force. `--prompt` prints the exact system prompt the LLM sees for this
959
+ project (pack instructions + platform protocol + any project overlay).
960
+ - **`octwin orders [reference_id]` — the commerce a conversation produced.** The list (number,
961
+ status/payment, total, contact) or one order with its line items, the
962
+ subtotal/tax/shipping/discount/total breakdown, `payment_ref`, and the allowed transitions. On a
963
+ `pending`/`none` payment it explains *why that's expected*: the forward payment lifecycle is
964
+ **webhook-owned** (not patchable), and the default gateway-less `manual` driver makes
965
+ `payment_request` take its `empty` port — so the flow should confirm pay-on-delivery, not error.
966
+ - **`octwin analytics [entity]` — stage conversion for any pipelined entity.** The funnel engine that
967
+ powered journeys is now generic, so any entity declared with a `pipeline:` charts conversion +
968
+ drop-off (`--overview` / `--milestones` / `--trends` / `--cost`, and `--stage <id>` for the records
969
+ currently at a stage). An empty result names **both** causes the platform folds into one response —
970
+ no `pipeline:`, or no `view` grant on `record.<entity>`.
971
+ - **`octwin catalog [--readiness]`** — commerce products with price, availability and stock
972
+ (`untracked` when the SKU isn't inventory-tracked), plus the WhatsApp catalog binding.
973
+ `--readiness` runs the Meta Graph checklist with each failing item's suggested fix.
974
+ - **`octwin scheduling [--slots <resourceRecordId>]`** — the engine state (bookable resource types,
975
+ upcoming slots, booked seats) or the computed slots for one resource. This is how you verify the
976
+ availability rules a `deploy --seed` created, which 0.1.14 could create but not read back.
977
+
978
+ ### Changed
979
+ - **Auth failures now name the scope the command needs.** Every 401/403 appends the required scope
980
+ (and the plan feature, where the route is entitlement-gated) from one client-side mirror of the
981
+ platform's scope registry — replacing the per-command hardcoded strings, several of which named no
982
+ scope at all. It also spells out that `pack:deploy` / `media:generate` are direct-grant only, so a
983
+ `tenant:admin` preset token does **not** confer them (the most common "but my token is admin" dead end).
984
+
985
+ ## [0.1.14] - 2026-07-23
986
+
987
+ ### Fixed
988
+ - **A failed demo seed no longer reports a clean `✓`.** `deploy` softens non-fatal
989
+ install steps (e.g. a demo-seed row) to warning frames, but the summary still printed
990
+ `✓ Deployed` and exited 0 — so a deploy that seeded ZERO records read as success (the
991
+ false-✓ trap). `deploy` now collects those step errors, prints `⚠ Deployed with N
992
+ warning(s) — data may be incomplete` with each message, and exits non-zero so CI /
993
+ a `deploy && chat` chain catches it.
994
+
995
+ ### Added
996
+ - **`login` echoes what the token reaches.** After saving, `octwin login` calls
997
+ `/api/self/t/whoami` and prints the workspace, project pin, and scopes — so a fresh
998
+ token self-identifies without a second `octwin whoami` (best-effort; a network failure
999
+ never fails the save).
1000
+ - **`validate --remote` surfaces warnings + the data-store trap.** The remote validate now
1001
+ renders advisory `⚠` warnings (which don't block deploy) and, for a pack declaring
1002
+ `required_adapters: [data-store]`, reports the missing-data-store error the deploy would
1003
+ have 409'd on — plus a warning that an `xrm.yaml` pack usually shouldn't declare that
1004
+ adapter at all. It also now catches (offline) a list-form `entities:` block and a
1005
+ `localized: true` field seeded with a bare string — traps that previously surfaced only
1006
+ mid-seed at deploy.
1007
+
1008
+ ## [0.1.13] - 2026-07-22
1009
+
1010
+ ### Added
1011
+ - **A headless media loop — produce + send.** Two paired additions close the coverage hole where any
1012
+ media-collect flow (e.g. `activate-app`'s registration / ID uploads) stalled at the upload prompt
1013
+ because the CLI could neither *make* nor *send* an image:
1014
+ - `octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]` — AI-generates an
1015
+ image on the platform (needs a `media:generate`-scoped token), stores it as a public asset, and prints
1016
+ its `MEDIA-` handle + serve URL. `--out` downloads the bytes to a file (WhatsApp renders only
1017
+ `.png`/`.jpg`); `--json` emits `{ media_id, url, mime, width, height, bytes }`.
1018
+ - `octwin chat --media <file|media-id>` — uploads a local file (or a media id from
1019
+ `media generate --json`) as an image/document/audio inbound; any accompanying `"message"` rides as
1020
+ its caption. The platform's media pipeline folds the upload into a running collect, so media flows
1021
+ are now fully drivable headlessly.
1022
+
1023
+ ### Changed
1024
+ - **The KB-drift nudge now names what changed.** With the platform serving per-entry content hashes, the
1025
+ post-command nudge appends a `(N changed · M added · K removed)` summary instead of a bare hash pair.
1026
+ - **`octwin platform-kb` prints a changelog on pull.** Instead of only a doc/catalog count, the pull now
1027
+ diffs the fresh index against your last pull and lists exactly which docs/catalogs were added (`+`),
1028
+ changed (`~`), or removed (`-`) — so a replaced schema shape is visible, not silent.
1029
+
1030
+ ## [0.1.12] - 2026-07-22
1031
+
1032
+ ### Fixed
1033
+ - **The KB-drift nudge now fires after `chat` too.** `chat` — the command a debugging session runs most —
1034
+ was missing from the networked-command list, so a chat-heavy session never noticed the platform's
1035
+ capability reference had moved. The nudge also now shows the concrete drift
1036
+ (`old-hash → new-hash`) so you can see it's real, not a heuristic.
1037
+
1038
+ ### Changed
1039
+ - **Every networked command announces what it's doing before it does it.** `chat` prints
1040
+ `→ Connecting to <tenant>/<project> as '<handle>' …` before opening the stream and
1041
+ `… delivered — waiting for the reply (up to Ns)` after the send; `status` / `whoami` /
1042
+ `platform-kb` / `records` / `logs` / `cases` each print a one-line `→ …` header naming the
1043
+ action and target before the first network call — no more silent seconds followed by a result
1044
+ (or a hang with no clue what was being attempted). JSON modes (`--json`) stay clean for piping.
1045
+
1046
+ ## [0.1.11] - 2026-07-21
1047
+
1048
+ ### Fixed
1049
+ - **Auth-failure hint on every command, not just the inspect family** (author-feedback A7). `validate`,
1050
+ `deploy`, `status`, and the single-record read now print the scope/transient explanation on a 401/403
1051
+ (the shared `authFailureHint`), instead of a bare `HTTP 401` that left it ambiguous whether the token
1052
+ lacked a scope or the platform hiccupped.
1053
+
1054
+ ## [0.1.10] - 2026-07-21
1055
+
1056
+ The capability reference is now **pulled, never bundled** — the authoring skill dropped its committed KB
1057
+ snapshot, so `octwin platform-kb pull` is the single source of truth, and the CLI watches it for drift.
1058
+
1059
+ ### Added
1060
+ - **KB-drift nudge** — after any command that already talks to the platform (`deploy` / `status` /
1061
+ `test` / `validate --remote` / `records` / `cases` / `logs` / `whoami`), the CLI does one tiny
1062
+ `octwin-platform-kb?meta=1` GET and prints a one-line "the platform capability reference changed —
1063
+ `octwin platform-kb pull`" when the platform's `content_hash` no longer matches your last pull.
1064
+ Fail-silent + TTY-only + no-op until you've pulled once — the sibling of the existing CLI-upgrade notice.
1065
+
1066
+ ### Changed
1067
+ - **`octwin platform-kb pull`** now records the reference's `content_hash` in `.octwin/platform-kb/index.json`
1068
+ (the fingerprint the drift check compares against).
1069
+
1070
+ The conversation-debugging release — driven by the second author-feedback round (xpeng-egypt),
1071
+ which found the author→validate→deploy loop solid but multi-turn debugging impossible from the CLI.
1072
+
1073
+ ### Added
1074
+ - **`octwin chat --tap "<tap-id>"`** — press a rendered button / list row (sends the web channel's
1075
+ `type:'interactive'` inbound). Paired with full render printing, any tap-driven flow is now drivable headlessly.
1076
+ - **Full render printing** — every reply prints its complete interactive content: list sections/rows,
1077
+ buttons, and carousel cards, each with its **tap id**; header/footer/list-button text included.
1078
+ - **`--json` on `chat` / `logs` / `cases`** — raw SSE envelopes (chat) or raw event/case payloads.
1079
+ - **`octwin cases [caseId] [--queues]`** — inspect casework (support tickets): the inbox, one case +
1080
+ timeline + applicable decisions, or queue keys with open counts. Casework packs previously had zero
1081
+ data-inspection path (`records case` was a dead end).
1082
+ - **Per-subcommand `--help`/`-h`** — intercepted before any network or auth work (previously
1083
+ `octwin logs --help` made a network call and could die with a 401).
1084
+ - Multi-message turns: `chat` now collects **all** of a turn's renders (quiet-gap detection) instead
1085
+ of cutting at the first one.
1086
+
1087
+ ### Changed
1088
+ - **`octwin logs`**: the conversation list shows contact handle + last activity and filters with
1089
+ `--as <handle>`; the timeline prints **full** event text (no more 240-char truncation) plus the
1090
+ renders each turn actually sent; footer points at `--json` / `mastra-messages` for verbatim payloads.
1091
+ - **Network failures name the target URL** (`platform unreachable at <url>`), and 401s explain
1092
+ invalid-token vs one-off transient instead of a bare status code.
1093
+ - `octwin records` on an unknown entity now surfaces the server's error body, and for `case`/`cases`
1094
+ points at `octwin cases`.
1095
+
1096
+ ### Fixed
1097
+ - **Multi-turn chat actually works.** Two bugs made consecutive `octwin chat` calls look like "every
1098
+ call starts fresh": (1) the idempotency `local_id` was a **constant** per handle, so the platform's
1099
+ 5-minute inbound dedup silently dropped the second message — a fresh id is now minted per call;
1100
+ (2) a fresh SSE connect **replays** recent history and the CLI printed the first render frame — i.e.
1101
+ the *previous* turn's greeting — as the reply; the CLI now drains the replay and only accepts frames
1102
+ newer than the connect boundary. The platform always kept one open conversation per `--as` handle —
1103
+ the same handle genuinely continues the same conversation now.
1104
+
1105
+ ## [0.1.8] - 2026-07-21
1106
+ - Global-first install guidance (`npm i -g octwin-cli`); upgrade notice suppressed under `npx`.
1107
+
1108
+ ## [0.1.7] - 2026-07-21
1109
+ - `--version`/`-v` flag; daily fail-silent, TTY-only "newer version available" notice.
1110
+
1111
+ ## [0.1.6] - 2026-07-21
1112
+ - `octwin init` scaffolds a home-hub menu (`home` list_picker + example `browse` tool) instead of a bare greeting.
1113
+
1114
+ ## [0.1.5] - 2026-07-20
1115
+ - `octwin validate --remote` (the platform's FULL manifest + flow-DSL check, all errors at once);
1116
+ headless debugging commands `octwin records` / `octwin logs` / `octwin chat`; `octwin test` became
1117
+ an alias for `validate --remote`. First author-feedback round.
1118
+
1119
+ ## [0.1.4] - 2026-07-20
1120
+ - Authoring reliability + DX fixes from the first end-to-end external-pack test.
1121
+
1122
+ ## [0.1.3] - 2026-07-20
1123
+ - `octwin platform-kb pull` — pull the platform capability reference for the authoring skill.
1124
+
1125
+ ## [0.1.2] - 2026-07-20
1126
+ - Proper npm README.
1127
+
1128
+ ## [0.1.1] - 2026-07-20
1129
+ - Initial npm publish: `init` / `validate` / `login` / `whoami` / `deploy` / `status`, `oct_` deploy tokens.