@mmmbuto/nexuscrew 0.9.1 → 0.9.3
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 +171 -0
- package/frontend/dist/assets/{index-Bu_-2-Uu.js → index-uRh_hSop.js} +19 -19
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/cli/init.js +18 -3
- package/lib/files/telemetry.js +89 -0
- package/lib/fleet/builtin.js +258 -49
- package/lib/fleet/definitions.js +226 -0
- package/lib/live-host/bridge.js +82 -22
- package/lib/proxy/panel-proxy.js +30 -3
- package/lib/server.js +5 -0
- package/package.json +1 -1
- package/skills/live/SKILL.md +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,177 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to NexusCrew are tracked here.
|
|
4
4
|
|
|
5
|
+
## 0.9.3 — 2026-08-16 — "A Receipt Is Not an Outcome"
|
|
6
|
+
|
|
7
|
+
The previous release fixed the Desktop engine and shipped the fix inert. Pulling
|
|
8
|
+
on that thread produced most of this one: a repair that was never called, a race
|
|
9
|
+
underneath it, a lock that reopened the race it closed, and — at the end — a
|
|
10
|
+
success reported for a write that never happened. A `200 OK` for a change that
|
|
11
|
+
was silently dropped is worse than a refusal, because nothing downstream has any
|
|
12
|
+
reason to look again.
|
|
13
|
+
|
|
14
|
+
- **Every write to the fleet definitions now goes through a lock, and a
|
|
15
|
+
surrendered write is no longer reported as success.** Ten code paths did
|
|
16
|
+
read-modify-write on the same file with no mutual exclusion, so a concurrent
|
|
17
|
+
update could be read, overwritten, and lost. The lock is taken by creating a
|
|
18
|
+
file exclusively — one syscall, no window between checking and acting — and
|
|
19
|
+
its owner is identified per acquisition rather than per process. Liveness is
|
|
20
|
+
asked of the kernel: a lock is only broken when it is both old *and* its owner
|
|
21
|
+
is gone, because age alone never proved a process dead. The first version of
|
|
22
|
+
this guard evicted **live** owners, which is how a cure reopens the disease
|
|
23
|
+
it was written for; the case is now pinned by tests in both directions.
|
|
24
|
+
|
|
25
|
+
Two callers want opposite behaviour and now get it: an opportunistic
|
|
26
|
+
migration gives up in silence, while a change a person asked for **fails**
|
|
27
|
+
with its own cause — `409` when the lock is busy, `400` when the data is
|
|
28
|
+
invalid. Previously both answered `200 OK`.
|
|
29
|
+
|
|
30
|
+
- **Creating the definitions for the first time also goes through the lock.**
|
|
31
|
+
`init` wrote the file directly, so a first run racing with a live service
|
|
32
|
+
could erase a configuration instead of creating one.
|
|
33
|
+
|
|
34
|
+
- **The panel's own password reaches the panel.** `Authorization` was stripped
|
|
35
|
+
wholesale on the way in, so any panel behind HTTP authentication was
|
|
36
|
+
impossible to log into by construction — not misconfigured, impossible. The
|
|
37
|
+
header now travels for the schemes a panel actually uses (`Basic`, `Digest`,
|
|
38
|
+
`NTLM`, `Negotiate`) and never for a bearer token, which belongs to the
|
|
39
|
+
control plane and must not leak into a proxied application. It is a closed
|
|
40
|
+
list, not a denylist: an unknown scheme is refused rather than forwarded.
|
|
41
|
+
|
|
42
|
+
- **A container that opens over the page instead of taking you away from it.**
|
|
43
|
+
Opening a cell's preview, or its desktop panel, used to mean leaving whatever
|
|
44
|
+
you were looking at. There is now one popup — closed by `Escape`, by clicking
|
|
45
|
+
outside it, never by clicking inside it, and it takes focus when it opens —
|
|
46
|
+
reached from the status dot beside each cell in the list and from the cell's
|
|
47
|
+
own tile in the grid. The panel was previously reachable **only** from an
|
|
48
|
+
enlarged cell, so opening a browser meant leaving the grid first. The popup
|
|
49
|
+
now holds all three sources — preview, live stream, desktop panel — and on
|
|
50
|
+
narrow screens they get real buttons instead of one small dot carrying three
|
|
51
|
+
meanings. Looking at a cell never selects it.
|
|
52
|
+
|
|
53
|
+
One defect found on the way, worth stating because it was invisible: the
|
|
54
|
+
popup held onto the **row** it was opened from — a single frame of a list
|
|
55
|
+
that refreshes every few seconds. Left open across a refresh it could show
|
|
56
|
+
another cell's preview, or a dead one's. It now holds the key and re-resolves
|
|
57
|
+
it on every render; a cell that disappears closes its own popup.
|
|
58
|
+
|
|
59
|
+
- **The panel opened from the list is served from its own origin again.** A
|
|
60
|
+
panel reached through the new popup was passed no port, and with no port the
|
|
61
|
+
frame falls back to a relative path — same-origin with the control plane,
|
|
62
|
+
which is precisely what 0.9.1 separated so that a panel's own scripts cannot
|
|
63
|
+
reach the operator's token. The new entry point had quietly reopened it, with
|
|
64
|
+
nothing to show for it: the panel opened and worked. The port is now resolved
|
|
65
|
+
per row by the same function the grid uses, and a remote cell whose node has
|
|
66
|
+
no negotiated port still gets none rather than borrowing the local one —
|
|
67
|
+
a wrong origin is not a fallback.
|
|
68
|
+
|
|
69
|
+
- **A lock is no longer held by a process that merely inherited its number.**
|
|
70
|
+
Liveness was decided by asking the kernel whether a pid existed, but pids are
|
|
71
|
+
reused: once the number was handed to an unrelated process, the lock was
|
|
72
|
+
never considered abandoned and every write to the fleet definitions gave up
|
|
73
|
+
in silence. The lock's token now also records **when** its owner started, so
|
|
74
|
+
two processes that held the same number at different times are told apart.
|
|
75
|
+
Where that cannot be read — systems without `/proc` — the lock is left alone:
|
|
76
|
+
a delayed write, never an eviction. That case is documented, not closed.
|
|
77
|
+
|
|
78
|
+
Also: a lock whose token could not be written is no longer treated as owned.
|
|
79
|
+
The write was best-effort, so a failure left an **empty** lock that named
|
|
80
|
+
nobody, and thirty seconds later a live owner could be evicted from it.
|
|
81
|
+
|
|
82
|
+
- **A migration no longer overwrites work committed while it was running.**
|
|
83
|
+
The concurrency check compared a snapshot taken *after* the migration's own
|
|
84
|
+
work, so anything written during it was already inside the snapshot: the
|
|
85
|
+
comparison passed and the older state won. The baseline is now taken at the
|
|
86
|
+
source, covering the whole window.
|
|
87
|
+
|
|
88
|
+
- **The cell list shows free context and tier usage, where the cell can report
|
|
89
|
+
them.** The numbers come from a small file written beside the cell's own
|
|
90
|
+
files; a documented snippet is included for producing it
|
|
91
|
+
(`docs/STATUSLINE_TELEMETRY.md`), and nothing is written automatically. Three
|
|
92
|
+
rules govern the display: a reading older than five minutes is dropped rather
|
|
93
|
+
than shown as current, cells that cannot report simply have no such line —
|
|
94
|
+
no dash, no "n/a" — and an unreadable or malformed file degrades to nothing
|
|
95
|
+
without failing the list. Values are accepted only as integers already in
|
|
96
|
+
percent: a `null` would otherwise have been converted to a perfectly credible
|
|
97
|
+
`0%` — and the same conversion was found in the documented snippet, on the
|
|
98
|
+
writing side, where the reader cannot defend against it: a zero written in
|
|
99
|
+
place of nothing is a valid integer and would have passed.
|
|
100
|
+
|
|
101
|
+
Freshness is checked in **both** directions. A reading stamped in the future
|
|
102
|
+
was never older than the limit, so it would have stayed "current" forever;
|
|
103
|
+
beyond a small tolerance for clock skew it is now dropped like any other
|
|
104
|
+
unusable value. And every number carries its own direction in its label —
|
|
105
|
+
`context 71% free · 5h used 33%` — because a comment claiming the labels said
|
|
106
|
+
so was not the same as the labels saying so, and next to a "free" the reader
|
|
107
|
+
supplies the missing word themselves.
|
|
108
|
+
|
|
109
|
+
- **The voice interface knows which cell it is attached to.** It received the
|
|
110
|
+
working directory and a prompt, never an identity, so a Live session had to
|
|
111
|
+
go looking through terminal sessions to guess where it was — and could
|
|
112
|
+
describe another cell's work as its own. The bridge now always prepends the
|
|
113
|
+
designated cell's identity to the instructions it sends, alone when no
|
|
114
|
+
per-cell prompt exists. The identity travels on the bridge rather than in the
|
|
115
|
+
prompt on purpose: a prompt may legitimately be absent, and if identity
|
|
116
|
+
depended on it, a missing prompt would have meant a Live with no idea who it
|
|
117
|
+
was. The exact session name is included only when the roster declares it,
|
|
118
|
+
never inferred.
|
|
119
|
+
|
|
120
|
+
**Please note, if you set `developer_instructions` in your configuration.**
|
|
121
|
+
The bridge sends that field on every Live session now, and the consumer
|
|
122
|
+
*replaces* its configured value with what it receives rather than adding to
|
|
123
|
+
it. A cell with no per-cell prompt therefore no longer receives the global
|
|
124
|
+
developer instructions it used to get. This is accepted rather than worked
|
|
125
|
+
around: changing the field's meaning would be a new contract for every
|
|
126
|
+
client, and the intended place for a cell's own instructions is its
|
|
127
|
+
`LIVE_PROMPT.md`.
|
|
128
|
+
|
|
129
|
+
- **The per-cell prompt is found on machines that name their sessions
|
|
130
|
+
anything.** Its directory was built by pinning the literal prefix `cloud-` in
|
|
131
|
+
front of the cell name, so on a host whose sessions are named otherwise the
|
|
132
|
+
file was either looked for where it does not exist, or under an invented
|
|
133
|
+
path. Both ended in "absent" — the same answer as a file that genuinely is
|
|
134
|
+
not there, so the defect hid inside the branch meant to report it. The
|
|
135
|
+
directory is now the session the roster declares, and when the roster
|
|
136
|
+
declares none there is a distinct outcome: no path is built at all, rather
|
|
137
|
+
than a prefix guessed.
|
|
138
|
+
|
|
139
|
+
- **Ready-to-copy prompt templates** in `docs/live-prompt-templates/`, in
|
|
140
|
+
English, Italian and Spanish, with `docs/LIVE_PROMPT.md` describing where the
|
|
141
|
+
file goes and the four outcomes the bridge can report. They name no cell, no
|
|
142
|
+
operator and no host: the voice takes its identity from the tools at runtime,
|
|
143
|
+
so the file works as copied.
|
|
144
|
+
|
|
145
|
+
## 0.9.2 — 2026-08-16 — "Offered and Then Refused"
|
|
146
|
+
|
|
147
|
+
Both defects in this release were found by using the product, not by a test —
|
|
148
|
+
and both were invisible to the suite for the same reason: each test exercised
|
|
149
|
+
one piece in isolation, and neither defect exists in isolation.
|
|
150
|
+
|
|
151
|
+
- **The Desktop engine could be selected but never saved.** It declared
|
|
152
|
+
`command: 'docker'` — a bare name — while engine validation requires an
|
|
153
|
+
absolute path, so it appeared in the list and was rejected on save with
|
|
154
|
+
*"command must be an absolute path"*: a message describing the shape of the
|
|
155
|
+
value instead of saying the command had not been resolved. The path is now
|
|
156
|
+
resolved from `PATH` through `realpath`, which matters because validation
|
|
157
|
+
uses `lstat` and rejects symlinks — and in many installs the first hit on
|
|
158
|
+
`PATH` is exactly that. Where Docker is absent the fallback makes the
|
|
159
|
+
refusal say *"not accessible (ENOENT)"*, naming the real cause.
|
|
160
|
+
|
|
161
|
+
Installations that already had the engine are **repaired**: the backfill
|
|
162
|
+
skips what already exists, so fixing the default alone would have changed
|
|
163
|
+
nothing precisely where the defect was seen. The repair only touches a
|
|
164
|
+
command that is both non-absolute and still named `docker` — an absolute
|
|
165
|
+
path, or a command you changed yourself, is left alone.
|
|
166
|
+
|
|
167
|
+
- **The panel reloaded without pause, leaving no time to interact with it.**
|
|
168
|
+
A panel behind a login was unusable: the sign-in prompt appeared and the
|
|
169
|
+
frame remounted before it could be completed. `route` is an array, so it was
|
|
170
|
+
a new prop on every parent render — and the parent re-renders continuously
|
|
171
|
+
to poll the fleet. With the array among the effect's dependencies, every
|
|
172
|
+
render requested a fresh ticket and remounted the iframe. The dependency is
|
|
173
|
+
now keyed on content, so the panel reopens when the route actually changes
|
|
174
|
+
and not otherwise.
|
|
175
|
+
|
|
5
176
|
## 0.9.1 — 2026-08-16 — "What the Guard Was Not Guarding"
|
|
6
177
|
|
|
7
178
|
- **The panel now lives on its own origin, so its JavaScript can no longer
|