@uluops/setup 0.11.0 → 0.13.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 +805 -0
- package/README.md +81 -32
- package/dist/cli.js +7 -1
- package/dist/commands/helpers.js +70 -7
- package/dist/commands/per-harness.d.ts +5 -0
- package/dist/commands/per-harness.js +5 -0
- package/dist/commands/setup.d.ts +7 -0
- package/dist/commands/setup.js +100 -35
- package/dist/commands/uninstall.d.ts +7 -0
- package/dist/commands/uninstall.js +36 -9
- package/dist/commands/verify.d.ts +5 -0
- package/dist/commands/verify.js +5 -0
- package/dist/harnesses/claude-code.js +15 -7
- package/dist/harnesses/codex.js +35 -8
- package/dist/harnesses/gemini-cli.js +13 -6
- package/dist/harnesses/index.d.ts +8 -0
- package/dist/harnesses/index.js +10 -0
- package/dist/harnesses/opencode.js +25 -7
- package/dist/lib/asset-catalog.js +15 -2
- package/dist/lib/atomic-write.d.ts +6 -0
- package/dist/lib/atomic-write.js +10 -0
- package/dist/lib/config-merger.js +27 -8
- package/dist/lib/display.d.ts +8 -0
- package/dist/lib/display.js +27 -1
- package/dist/lib/file-ops.d.ts +13 -4
- package/dist/lib/file-ops.js +69 -18
- package/dist/lib/install-lock.js +45 -13
- package/dist/lib/json-guards.d.ts +15 -0
- package/dist/lib/json-guards.js +30 -0
- package/dist/lib/manifest.d.ts +9 -2
- package/dist/lib/manifest.js +66 -8
- package/dist/lib/mcp-packages.d.ts +17 -15
- package/dist/lib/mcp-packages.js +15 -13
- package/dist/lib/settings-merger.js +53 -9
- package/dist/lib/version.js +19 -2
- package/dist/lib/write-coordinator.d.ts +50 -0
- package/dist/lib/write-coordinator.js +89 -0
- package/dist/steps/agent-metrics-cli.d.ts +6 -0
- package/dist/steps/agent-metrics-cli.js +19 -1
- package/dist/steps/agents.js +22 -4
- package/dist/steps/auth.js +53 -13
- package/dist/steps/cli.js +14 -1
- package/dist/steps/commands.js +28 -10
- package/dist/steps/mcp.js +18 -9
- package/dist/steps/metrics.js +77 -7
- package/dist/steps/shell.d.ts +4 -1
- package/dist/steps/shell.js +44 -6
- package/dist/steps/signup.d.ts +4 -0
- package/dist/steps/signup.js +18 -2
- package/dist/steps/skills.d.ts +11 -0
- package/dist/steps/skills.js +35 -6
- package/dist/steps/username.js +10 -2
- package/dist/steps/verify.js +55 -6
- package/package.json +7 -4
- package/dist/lib/agent-transform.d.ts +0 -12
- package/dist/lib/agent-transform.js +0 -129
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@uluops/setup` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.13.0] - 2026-09-11
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`@uluops/ops-mcp` pin 0.13.0 → 0.17.2** (`OPS_MCP_VERSION`). Every harness
|
|
12
|
+
writer stamps this into the MCP config, so this is the release that moves
|
|
13
|
+
first-launch users off a server that **silently stripped `cluster_key`** from
|
|
14
|
+
every recommendation on `save_run` / `update_run` / `validate_run` — the
|
|
15
|
+
schema was a plain `z.object()` and never declared the field, so within-run
|
|
16
|
+
convergence was recorded as NULL and read by the tracker as a collapsing
|
|
17
|
+
pipeline (ops-mcp 0.17.2 changelog; tracker issue `105c478f`). The jump also
|
|
18
|
+
crosses ops-mcp's 0.17.0 breaking train (strict ops-sdk 6.0.0; `list_agents`
|
|
19
|
+
and the list/query tools now return the `{data, total}` envelope; `get_run`
|
|
20
|
+
is a 14-key read projection). Setup itself calls none of these tools, but an
|
|
21
|
+
MCP client written against the 0.13.0 shapes will see the difference on
|
|
22
|
+
first launch after reattestation. Harness configs already on disk keep
|
|
23
|
+
resolving 0.13.0 until `npx @uluops/setup` runs again.
|
|
24
|
+
- **Codex read-tool seed gains `preview_update_run`** (`src/harnesses/codex.ts`).
|
|
25
|
+
A read tool since ops-mcp 0.14 (2026-08-21) that the hand-maintained
|
|
26
|
+
`TRACKER_READ_TOOLS` list never picked up — three setup releases seeded a
|
|
27
|
+
Codex config that prompted on first use of it. Nothing checks this list
|
|
28
|
+
against the pinned package; the comment now says so, and deriving it is
|
|
29
|
+
tracked separately. Seeded only on fresh Codex configs, as before.
|
|
30
|
+
|
|
31
|
+
### Not changed — deliberately
|
|
32
|
+
|
|
33
|
+
- **`@uluops/registry-mcp` stays pinned at 0.3.7** while npm `latest` is
|
|
34
|
+
0.8.0. Two trees publish under that name — `packages/-uluops-registry-mcp`
|
|
35
|
+
(0.3.7) and `uluops-registry-mcp` (0.8.0), the latter being the live
|
|
36
|
+
registry registration's copy — and the 0.3.7 → 0.8.0 span has not been
|
|
37
|
+
validated against setup's Codex read-tool seed or the harness writers. That
|
|
38
|
+
is its own bump with its own changelog entry, not a rider on this one.
|
|
39
|
+
|
|
40
|
+
## [0.12.0] - 2026-08-21
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **Per-file write coordinator** (`src/lib/write-coordinator.ts`). Every
|
|
45
|
+
config read-merge-write cycle (MCP step, hook install/remove, both JSON
|
|
46
|
+
profiles) is now serialized per resolved path, closing the Gemini CLI
|
|
47
|
+
same-file pair (`~/.gemini/settings.json` holds both the MCP config and
|
|
48
|
+
the hook) against interleaved cycles — including under any future
|
|
49
|
+
concurrent step orchestration. Every `atomicWrite` additionally attests a
|
|
50
|
+
content hash of what this process wrote (`fileMatchesLastWrite`), so any
|
|
51
|
+
future rollback mechanism can refuse to clobber content it didn't write.
|
|
52
|
+
Deliberately NOT single-write coalescing: the hook entry has a hard data
|
|
53
|
+
dependency on the metrics tool files landing first (a hook pointing at a
|
|
54
|
+
missing hook.js fires a failing command in the user's harness), so
|
|
55
|
+
coalescing would couple MCP-config success to the metrics step.
|
|
56
|
+
- **Metrics-step privacy disclosure.** The install output now states, at the
|
|
57
|
+
point of hook installation, that the hook captures agent token/duration
|
|
58
|
+
metadata to a local buffer and sends nothing itself, with the `--no-metrics`
|
|
59
|
+
opt-out and the privacy-policy URL. A README "Data & privacy" section
|
|
60
|
+
grounds the full picture in the policy's actual terms (local buffer; data
|
|
61
|
+
leaves only on explicit tracker saves; indefinite retention by design;
|
|
62
|
+
org-policy note for shared installs).
|
|
63
|
+
|
|
64
|
+
- **`--verify` checks MCP package resolvability on npm.** The install-time
|
|
65
|
+
probe is non-blocking by design; the harness runs `npx -y <spec>` at
|
|
66
|
+
startup, so an unresolvable package fails long after setup succeeded.
|
|
67
|
+
`--verify` now re-asks the question on demand (a registry outage is
|
|
68
|
+
reported but does not double-fail a run the connectivity checks already
|
|
69
|
+
failed).
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
|
|
73
|
+
- **MCP server pins bumped to the current release contract:**
|
|
74
|
+
`@uluops/ops-mcp` 0.11.0 → **0.13.0** (the update-run merge-mode + echo
|
|
75
|
+
release), `@uluops/registry-mcp` 0.3.5 → **0.3.7**. A fresh install now
|
|
76
|
+
wires the servers this release was validated against.
|
|
77
|
+
- **The npm availability probe now checks the PINNED VERSIONS, not the bare
|
|
78
|
+
package names** — reversing the earlier deliberate choice. The harness
|
|
79
|
+
runs `npx -y <pinned spec>`, so an unresolvable pin is exactly the
|
|
80
|
+
condition that must fail loudly at install time instead of hours later as
|
|
81
|
+
an opaque npx error at first MCP launch. "A pin is not a publish"; the
|
|
82
|
+
probe now enforces it. `--verify`'s resolvability check inherits the same
|
|
83
|
+
version precision.
|
|
84
|
+
- **Auto-detection now names its exclusions.** When an experimental
|
|
85
|
+
harness's home directory is present, detection prints a dimmed
|
|
86
|
+
`Detected <Name> (experimental) — excluded from auto-detection; opt in
|
|
87
|
+
with --harness <name>` line instead of silently omitting it (the policy —
|
|
88
|
+
detected = safe to install — is unchanged and now visible).
|
|
89
|
+
- **Conflict check distinguishes "fresh install" from "broken bundle".**
|
|
90
|
+
A missing destination dir still skips silently (expected on first
|
|
91
|
+
install); the *bundled assets* being unreadable now warns loudly before
|
|
92
|
+
skipping — that condition means the package is broken, not that the
|
|
93
|
+
machine is fresh.
|
|
94
|
+
- **Dependency refresh to latest minors/patches (exact pins kept):**
|
|
95
|
+
`@inquirer/prompts` 8.5.2 → 8.6.0, `tsx` 4.22.4 → 4.23.12, `vitest`
|
|
96
|
+
4.1.9 → 4.1.11. The three majors available at review time were deliberately
|
|
97
|
+
held: `chalk` 6 requires Node >= 22 (this package supports >= 20),
|
|
98
|
+
`typescript` 7 is the native-compiler migration and gets its own pass, and
|
|
99
|
+
`@types/node` 26 describes APIs outside the supported Node floor.
|
|
100
|
+
- `CHANGELOG.md` now ships in the npm tarball (added to `files`), and the
|
|
101
|
+
build stamps the executable bit on `dist/cli.js` directly (`postbuild
|
|
102
|
+
chmod +x`) instead of relying on npm's bin-link chmod at install time.
|
|
103
|
+
|
|
104
|
+
### Fixed
|
|
105
|
+
|
|
106
|
+
- **Fish users no longer get bash syntax written into `config.fish`.**
|
|
107
|
+
`--shell` now writes `set -gx ULUOPS_API_KEY …` for fish (the `export`
|
|
108
|
+
form printed a parse error on every new fish shell while never setting
|
|
109
|
+
the variable — visible breakage plus silent auth failure), and the
|
|
110
|
+
profile's parent directory is created first (a fresh fish user may have
|
|
111
|
+
no `~/.config/fish/` yet).
|
|
112
|
+
- **README honesty pass (anxiety-read findings):** the `HTTPS_PROXY`
|
|
113
|
+
troubleshooting remedy was inert (Node's fetch ignores proxy env vars) —
|
|
114
|
+
replaced with the working `--skip-validation` path; "safe and idempotent"
|
|
115
|
+
/ "never touched" absolutes replaced with the two known edges the repo
|
|
116
|
+
itself documents (ownership-marker hook replacement on re-run, and the
|
|
117
|
+
`--local-defs` scope-flip leaving the prior tree untracked).
|
|
118
|
+
- **Round-7: unknown is never observed.** When `hook.js` is absent and the
|
|
119
|
+
settings file cannot be read, the metrics step now returns
|
|
120
|
+
`skippedReason: "hook-state-unknown"` (with a named warning) instead of an
|
|
121
|
+
observed `false` — the manifest keeps its prior hook record and uninstall
|
|
122
|
+
keeps removing the hook. The hookless short-circuit gained the same
|
|
123
|
+
`skippedReason` for shape parity; `defsScope` is validated at manifest
|
|
124
|
+
load (the inheritance gate branches on it); a summary-render failure can
|
|
125
|
+
no longer report a completed install as exit-1 (render is advisory,
|
|
126
|
+
`classifyExit` is the authority) and the catalog's per-file read names
|
|
127
|
+
unreadable bundled files instead of throwing; the skill-dir prune skips
|
|
128
|
+
top-level assets.
|
|
129
|
+
- **Round-6 gate corrections (the falsified-state class, final ring).**
|
|
130
|
+
The defs-scope inheritance gate no longer covers the scope-INDEPENDENT
|
|
131
|
+
hook fields — a `--local-defs` re-run of a global install can no longer
|
|
132
|
+
record `hooksInstalled: false` over a live hook (uninstall/verify branch
|
|
133
|
+
on that field); a scope flip now warns naming the old, now-untracked
|
|
134
|
+
defsPath. An operational conflict-check failure (unreadable destination,
|
|
135
|
+
non-TTY refusal) is classified per-harness and the run continues —
|
|
136
|
+
previously it escaped the loop, leaving installed sibling harnesses with
|
|
137
|
+
no manifest record at all. `skills` entries are element-typed like
|
|
138
|
+
agents/commands; `hookConfigured` consults the settings file when
|
|
139
|
+
hook.js is absent instead of recording false from disk-existence alone;
|
|
140
|
+
the metrics package.json copy and skill-dir prune failures are named.
|
|
141
|
+
- **Failed copies are no longer deleted as "stale", and skipped steps no
|
|
142
|
+
longer falsify the record** (fifth audit round — the falsified-state
|
|
143
|
+
class one ring further out). Stale reconciliation now compares against
|
|
144
|
+
what the package SHIPS, not what copied successfully this run — an
|
|
145
|
+
ENOSPC re-run previously deleted the entire previously-working installed
|
|
146
|
+
set and reported it as routine cleanup; failed-but-previously-installed
|
|
147
|
+
files stay in the manifest record so uninstall can still remove their
|
|
148
|
+
surviving prior copies. `--no-metrics` (and unsupported harnesses) no
|
|
149
|
+
longer downgrade `hooksInstalled` to false — a step that never observed
|
|
150
|
+
the hook state cannot change its record, so uninstall keeps removing the
|
|
151
|
+
hook it previously installed. Prior file lists are inherited only within
|
|
152
|
+
the same defs scope (a `--local-defs` flip no longer points uninstall at
|
|
153
|
+
the wrong tree); a present manifest with an unrecognized shape refuses
|
|
154
|
+
loudly instead of reading as absent (behavior change: was silently
|
|
155
|
+
treated as no-manifest); manifest agents/commands entries are
|
|
156
|
+
element-typed; the non-TTY unknown-conflicts refusal exits 1 as an
|
|
157
|
+
operational failure (not a user-decline exit 0); the tool-file removal
|
|
158
|
+
catch names its error; the unidentifiable-lock message no longer invents
|
|
159
|
+
"PID -1".
|
|
160
|
+
- **The read-error-means-absent inference is now eliminated at every
|
|
161
|
+
read-then-act site, not only the overwrite-shaped ones.** Third audit
|
|
162
|
+
round: the conflict-overwrite guard treated an unreadable destination as
|
|
163
|
+
"no conflicts" and destroyed a user's own agent file with no prompt (now:
|
|
164
|
+
conflicts unknown → explicit confirm, default No); the bundled-asset
|
|
165
|
+
readers returned empty lists on read errors that the manifest then
|
|
166
|
+
recorded as authoritative, orphaning previously-installed files (now:
|
|
167
|
+
only ENOENT means "ships none"; anything else fails the step, records
|
|
168
|
+
partial state, and — fourth round — the manifest entry PRESERVES the
|
|
169
|
+
prior file lists for every step that produced no result, so the partial
|
|
170
|
+
record can never itself become the orphaning vector); an unreadable lock `meta.json` was classified stale and a
|
|
171
|
+
LIVE lock stolen (now: unverifiable = held, never reclaimed), and the
|
|
172
|
+
mkdir→meta window got a grace-recheck before stale-claiming; the metrics
|
|
173
|
+
tool copy verifies its source is readable before wiping the installed
|
|
174
|
+
tree.
|
|
175
|
+
- **Uninstall reports the truth.** `removeShellExport` and `deleteManifest`
|
|
176
|
+
return results their callers consult: an unremovable shell export warns
|
|
177
|
+
that the plaintext key survives (previously "✓ Removed export" over an
|
|
178
|
+
untouched file), an undeletable manifest warns instead of "✓ Manifest
|
|
179
|
+
deleted", and per-file unlink failures during uninstall are named instead
|
|
180
|
+
of silently excluded from a truthful-looking count. Returning-user
|
|
181
|
+
detection (`hasCredentialsFile`) now counts unreadable-but-present as
|
|
182
|
+
present, so a permissions hiccup no longer steers into a duplicate
|
|
183
|
+
signup.
|
|
184
|
+
- **The install manifest can no longer be silently replaced or misread as
|
|
185
|
+
absent.** `readManifestFile` collapsed every read error AND malformed
|
|
186
|
+
JSON into "no manifest" — after which a save would overwrite the file it
|
|
187
|
+
couldn't read, orphaning every recorded agent/command/hook, and uninstall
|
|
188
|
+
would report "nothing to uninstall". Unreadable-but-present now refuses
|
|
189
|
+
loudly; malformed JSON refuses with the recovery path named. (Behavior
|
|
190
|
+
change: malformed manifests previously read as absent.)
|
|
191
|
+
- **`writeCredentialsFile` honors its preserve promise.** The merge only
|
|
192
|
+
starts fresh on genuine absence now — an unreadable or unparseable
|
|
193
|
+
credentials file (which may hold other profiles shared with @uluops/cli)
|
|
194
|
+
refuses instead of being overwritten. (Behavior change: unparseable files
|
|
195
|
+
previously read as absent.)
|
|
196
|
+
- **`--uninstall` with an invalid harness filter exits 1 again** — the
|
|
197
|
+
previous fix's in-try `return` made the trailing exit unreachable, so the
|
|
198
|
+
fatal error exited 0; the path now rides `process.exitCode`.
|
|
199
|
+
- **An unreadable-but-present config can no longer be silently replaced.**
|
|
200
|
+
Every read-then-overwrite path (Claude config, harness settings, Codex
|
|
201
|
+
TOML, OpenCode JSONC, shell profile) treated ANY read error as "file
|
|
202
|
+
absent" and proceeded to write a fresh file over it — an EACCES on a
|
|
203
|
+
root-owned `~/.claude.json` or `~/.zshrc` would have destroyed the user's
|
|
204
|
+
content with a green checkmark. All five sites now discriminate via a
|
|
205
|
+
shared `isEnoent` predicate: only a genuinely missing file reads as
|
|
206
|
+
fresh; anything else refuses loudly with nothing modified. (This class
|
|
207
|
+
was fixed once before at the gitignore path — the predicate exists so it
|
|
208
|
+
cannot recur site-by-site.)
|
|
209
|
+
- **Malformed OpenCode JSONC is refused instead of silently truncated.**
|
|
210
|
+
`jsonc-parser`'s `parse()` is error-recovering and never throws, so the
|
|
211
|
+
previous guard was unreachable: everything after a syntax error was
|
|
212
|
+
dropped, merged, and written back. Parse errors are now collected via
|
|
213
|
+
the errors out-param and refuse the file by name.
|
|
214
|
+
- **`--uninstall` no longer leaks the install lock on an invalid harness
|
|
215
|
+
filter** — same exit-inside-try defect fixed for `runSetup` earlier,
|
|
216
|
+
now fixed as the class: the exit is recorded and fired after the
|
|
217
|
+
`finally` releases the lock.
|
|
218
|
+
- **npm failures diagnose themselves**: a spawn failure (npm not on PATH)
|
|
219
|
+
now reports the real cause instead of `exit null`, with the timeout
|
|
220
|
+
diagnosis taking precedence when both signals are present.
|
|
221
|
+
- **Slow-network timeouts get the friendly message**: `AbortSignal.timeout`
|
|
222
|
+
rejections (DOMException `TimeoutError`) are now classified alongside
|
|
223
|
+
network `TypeError`s in auth, signup, and username flows — previously the
|
|
224
|
+
exact case the "check your connection / --skip-validation" messages were
|
|
225
|
+
written for never triggered them. A 200 with a non-JSON body (captive
|
|
226
|
+
portal) is also handled in signup/username, matching auth.
|
|
227
|
+
- **Codex TOML removal no longer drops a user's block after an unparseable
|
|
228
|
+
header** — array-of-tables (`[[x]]`) and quoted-`]` headers now end the
|
|
229
|
+
skip region instead of leaving it sticky.
|
|
230
|
+
- **`stripDangerousKeys` strips `__proto__` only** — own-property
|
|
231
|
+
`constructor`/`prototype` keys assigned by `Object.assign` are inert data
|
|
232
|
+
properties, and stripping them silently ate legitimate user keys
|
|
233
|
+
(JSON-schema fragments) on the round-trip.
|
|
234
|
+
- **Install-lock release deregisters the dir only after removal completes**,
|
|
235
|
+
closing a signal-window leak; the coordinator's `fileMatchesLastWrite`
|
|
236
|
+
now answers true only on ENOENT (an unverifiable read must never
|
|
237
|
+
authorize a write), and its docblock states plainly that attestation has
|
|
238
|
+
no production consumer until a rollback mechanism exists.
|
|
239
|
+
- **A verify API-key decode failure no longer suppresses the npm
|
|
240
|
+
resolvability check**, and `getVersion` wraps its own JSON parse in the
|
|
241
|
+
deliberate broken-publish error.
|
|
242
|
+
- **Hook ownership is now decided by one predicate across merge/remove/has.**
|
|
243
|
+
The merge tolerated malformed matcher entries while `removeUluopsHook` and
|
|
244
|
+
`hasUluopsHook` dereferenced them unguarded — the same hand-edited
|
|
245
|
+
settings file merged fine but crashed `--uninstall` and `--verify`. All
|
|
246
|
+
three now share `isUluopsMatcher` (anything not positively ours is user
|
|
247
|
+
data: preserved by remove, invisible to has, never a crash), the two
|
|
248
|
+
crash-reachable callers (`uninstallMetrics`, verify's `checkHooks`) are
|
|
249
|
+
wrapped to degrade to a warning/failed check, and the OpenCode config
|
|
250
|
+
reader gained the same top-level shape gate as its siblings.
|
|
251
|
+
- **Agent/command/skill file copies are atomic** (`copyIfChanged`/
|
|
252
|
+
`writeIfChanged` now write via temp+rename) — a crash mid-copy can no
|
|
253
|
+
longer leave a torn definition file for the harness to load.
|
|
254
|
+
- **Parsed configs are stripped of `__proto__`/`constructor`/`prototype`
|
|
255
|
+
own-keys at the read boundary.** Our own merges are spread-based and
|
|
256
|
+
were never pollutable, but a hostile key read from disk would have been
|
|
257
|
+
written back for assign-semantics consumers to trip on. Break-test
|
|
258
|
+
proves an `Object.assign` over the stripped parse cannot pollute.
|
|
259
|
+
- **Pre-existing invalid JSON is named as pre-existing.** Both mergers'
|
|
260
|
+
parse errors now state the file failed to parse *before* any UluOps
|
|
261
|
+
change was made — previously indistinguishable from installer-caused
|
|
262
|
+
corruption.
|
|
263
|
+
- **`npm install -g` EACCES failures explain themselves** (both the CLI and
|
|
264
|
+
agent-metrics installers): the error now names the unwritable-prefix
|
|
265
|
+
cause and points at version managers / the npm permissions doc.
|
|
266
|
+
- **Health-check failures name the endpoint** (Tracker vs Registry) instead
|
|
267
|
+
of "some APIs unreachable".
|
|
268
|
+
- **`getVersion` fails loudly on a malformed package.json** instead of
|
|
269
|
+
stamping `undefined` into banners and the manifest.
|
|
270
|
+
- **Credentials reads only ever return a string key** — a malformed
|
|
271
|
+
`credentials.json` (numeric/object apiKey) reads as "no stored key"
|
|
272
|
+
rather than flowing a non-string into Bearer headers.
|
|
273
|
+
- **Manifest save clones instead of aliasing the loaded manifest**, and the
|
|
274
|
+
gitignore-update warning routes through the standard display helper.
|
|
275
|
+
|
|
276
|
+
- **`process.exit` no longer fires inside `runSetup`'s try block.** The
|
|
277
|
+
non-zero exit-code path skipped the `finally` that releases the install
|
|
278
|
+
lock (the signal handlers were the only cleanup actually running).
|
|
279
|
+
`classifyExit` still runs inside; the exit happens after the lock release.
|
|
280
|
+
- **Settings/config reads now reject unmergeable shapes instead of crashing
|
|
281
|
+
or corrupting.** Valid-JSON-wrong-shape user files (top-level array or
|
|
282
|
+
string; `hooks` as a string — which the merge would have spread into
|
|
283
|
+
per-character keys and written back; a hooks entry that is not an array)
|
|
284
|
+
now throw the same friendly named-path error as invalid JSON. The hook
|
|
285
|
+
merge additionally preserves matcher entries it cannot parse instead of
|
|
286
|
+
TypeErroring on them. Applies to both `settings-merger` and
|
|
287
|
+
`config-merger` reads.
|
|
288
|
+
- **Install-lock `meta.json` is written mode 0600** — it carries the owning
|
|
289
|
+
PID/hostname and was world-readable.
|
|
290
|
+
- **Non-TTY invocation without `-y` no longer dies on a raw inquirer
|
|
291
|
+
cancellation.** The API-key prompt's `interactive` gate now checks
|
|
292
|
+
`process.stdin.isTTY` (mirroring the existing guard on the account prompt),
|
|
293
|
+
so a piped/CI run with no key falls through to the actionable error —
|
|
294
|
+
`No API key found. Pass --api-key or set ULUOPS_API_KEY…` — instead of
|
|
295
|
+
`User force closed the prompt`. Found by live dx validation
|
|
296
|
+
(consumer-validate run #36).
|
|
297
|
+
- **README caught up to the shipped CLI.** The `--username` flag and its
|
|
298
|
+
registry-username step (live since 0.9.9) are now in the Options table,
|
|
299
|
+
the installer step list, and the Examples; the `--list`/`--verify` sample
|
|
300
|
+
outputs were regenerated from v0.11.0 (the old captures showed v0.9.5 and
|
|
301
|
+
pre-rename agent slugs like `code-validator` for what is now `validate`);
|
|
302
|
+
the Node >= 20 requirement is stated at the quick-start instead of only in
|
|
303
|
+
the bottom Requirements section; a contents line was added and all code
|
|
304
|
+
fences carry language tags.
|
|
305
|
+
|
|
306
|
+
### Known gap (deferred)
|
|
307
|
+
|
|
308
|
+
- **`process.exit` immediately after console output can truncate piped
|
|
309
|
+
stdout** (`npx @uluops/setup | tee` may lose the tail of the summary).
|
|
310
|
+
Converting the exit paths to `process.exitCode` requires an open-handle
|
|
311
|
+
audit first — a lingering inquirer/stdin handle would turn a truncated
|
|
312
|
+
log into a hung process, which is the worse failure. Tracked for its own
|
|
313
|
+
pass; uninstall's filter-error path already rides `process.exitCode`
|
|
314
|
+
(safe there: no prompt has run).
|
|
315
|
+
|
|
316
|
+
### Security
|
|
317
|
+
|
|
318
|
+
- **Uninstall path containment (CWE-22).** Manifest-supplied file names are
|
|
319
|
+
now resolved and verified to stay inside the managed directory before any
|
|
320
|
+
`unlink` — a hand-edited or foreign-written manifest entry containing
|
|
321
|
+
`../` can no longer turn uninstall into an arbitrary-delete primitive
|
|
322
|
+
(same-UID confused-deputy amplifier; security-analyst ship-gate finding).
|
|
323
|
+
Escape attempts are refused by name; break-tested with traversal and
|
|
324
|
+
absolute entries, outside files surviving.
|
|
325
|
+
|
|
326
|
+
## [0.11.0] - 2026-07-18
|
|
327
|
+
|
|
328
|
+
### Changed
|
|
329
|
+
|
|
330
|
+
- **Bumped the pinned MCP server versions to the current release contract:**
|
|
331
|
+
`@uluops/ops-mcp` 0.9.1 → 0.11.0, `@uluops/registry-mcp` 0.2.18 → 0.3.5.
|
|
332
|
+
These are the specs stamped into every harness config (`npx -y <spec>`), so a
|
|
333
|
+
fresh setup install now resolves the current MCP servers — including the
|
|
334
|
+
registry MCP's list-grain risk scalars + `analyzerStale` verdict-currency
|
|
335
|
+
passthrough (registry-sdk 0.45.0) and the mcp-secure-server
|
|
336
|
+
0.0.19-security hardening — instead of a six-week-old pin. Single source of
|
|
337
|
+
truth: `src/lib/mcp-packages.ts`.
|
|
338
|
+
- **Bumped `@uluops/agent-metrics` 0.4.0 → 0.8.0** (exact pin): the installed
|
|
339
|
+
metrics hook now carries run-scoped token attribution (`[run:]` tag →
|
|
340
|
+
`run_id` on buffer entries → `--run` queries), symlink/TOCTOU hardening, and
|
|
341
|
+
the CODEX guards. The hook a fresh install wires is the one current
|
|
342
|
+
pipelines (pdl-executor Phase 4b `--run` collection) are written against.
|
|
343
|
+
|
|
344
|
+
## [0.10.0] - 2026-07-06
|
|
345
|
+
|
|
346
|
+
### Changed
|
|
347
|
+
|
|
348
|
+
- **Bumped the pinned MCP server versions to the current release contract:**
|
|
349
|
+
`@uluops/ops-mcp` `0.5.0` → `0.9.1`, `@uluops/registry-mcp` `0.2.14` → `0.2.18`.
|
|
350
|
+
These are the specs stamped into every harness config (`npx -y <spec>`), so a
|
|
351
|
+
fresh setup install now resolves the tracker/registry MCP servers users are
|
|
352
|
+
actually tested against — including the dataset-export-era tracker tooling — instead
|
|
353
|
+
of a months-old pin. Single source of truth: `src/lib/mcp-packages.ts`.
|
|
354
|
+
- **Codex agent assets regenerated to `gpt-5.5`** (all 23 `assets/codex/agents/*.toml`
|
|
355
|
+
bumped `gpt-5.3` → `gpt-5.5`, with refreshed scoring-calibration examples).
|
|
356
|
+
|
|
357
|
+
## [0.9.9] - 2026-06-17
|
|
358
|
+
|
|
359
|
+
### Added
|
|
360
|
+
|
|
361
|
+
- **Optional `--username` step.** Offers to set a registry username during setup —
|
|
362
|
+
the one-time prerequisite for creating/publishing definitions — via native
|
|
363
|
+
fetch `PATCH /auth/profile` with the resolved api key (no SDK dependency).
|
|
364
|
+
Allow, never force: `--username <slug>` sets it non-interactively; an
|
|
365
|
+
interactive run prompts once (Enter skips); non-interactive runs with no flag
|
|
366
|
+
skip silently. Failures warn and never abort setup.
|
|
367
|
+
|
|
368
|
+
### Changed
|
|
369
|
+
|
|
370
|
+
- **Bumped MCP pins** in `src/lib/mcp-packages.ts`:
|
|
371
|
+
- `OPS_MCP_VERSION` 0.4.7 → **0.5.0** — adds the `update_profile` tool (set/confirm registry username from an MCP client).
|
|
372
|
+
- `REGISTRY_MCP_VERSION` 0.2.13 → **0.2.14** — raises the per-string cap so full definition YAML passes through direct MCP fields.
|
|
373
|
+
|
|
374
|
+
Fresh `npx -y @uluops/setup` installs and harness reattestations after 0.9.9
|
|
375
|
+
stamp these specs into Claude Code / Codex / Gemini / OpenCode configs,
|
|
376
|
+
replacing the prior 0.4.7 / 0.2.13 pins.
|
|
377
|
+
|
|
378
|
+
## [0.9.8] - 2026-06-17
|
|
379
|
+
|
|
380
|
+
### Changed
|
|
381
|
+
|
|
382
|
+
- **Bumped MCP pins to current** in `src/lib/mcp-packages.ts`:
|
|
383
|
+
- `OPS_MCP_VERSION` 0.4.4 → **0.4.7**
|
|
384
|
+
- `REGISTRY_MCP_VERSION` 0.2.9 → **0.2.13** — picks up the `@uluops/registry-mcp@0.2.13` `get_language` `format` parameter (compact digest default | full), an MCP-layer transform that cuts the ADL signature-string payload substantially without dropping conditionals/enums/forbidden fields.
|
|
385
|
+
|
|
386
|
+
Fresh `npx -y @uluops/setup` installs and harness reattestations after 0.9.8 will stamp these specs into Claude Code / Codex / Gemini / OpenCode harness configs, replacing the prior 0.4.4 / 0.2.9 pins. Existing installs need a re-attestation (`npx @uluops/setup`) to pick up the new specs — harness configs already on disk continue resolving the old versions.
|
|
387
|
+
|
|
388
|
+
`@uluops/cli` install (`src/steps/cli.ts:56`) remains unpinned (`npm install -g @uluops/cli`) — intentional; the CLI is a user-installable global managed via `npm update -g`, not a harness-stamped MCP spec.
|
|
389
|
+
|
|
390
|
+
- **Dependency + toolchain bumps to current.** Two were breaking majors requiring code/config changes:
|
|
391
|
+
- `@inquirer/prompts` 7.10.1 → **8.5.2** — v8 removed the `instructions` option on `checkbox`. Dropped the one usage in `src/cli.ts`; v8 auto-renders the equivalent "space to toggle, enter to confirm" help tip by default via `theme.style.keysHelpTip`, so behavior is preserved.
|
|
392
|
+
- `typescript` 5.9.3 → **6.0.3** — TS 6.0 no longer auto-includes `@types/*` the way 5.x did, which broke the build with `Cannot find name 'process'` across every Node-importing module. Fixed by adding `"types": ["node"]` to `tsconfig.json`.
|
|
393
|
+
- `commander` 12.1.0 → **15.0.0** (3 majors; CLI `--version`/`--help` parse verified), `@types/node` 22.19.15 → **25.9.3**, `tsx` 4.21.0 → **4.22.4**, `vitest` 4.1.8 → **4.1.9**.
|
|
394
|
+
|
|
395
|
+
Suite 360/360 pass on the bumped deps and pins. Published tarball validated via clean-room install before going live (shasum `fdc83ac0…`, identical npm↔local).
|
|
396
|
+
|
|
397
|
+
## [0.9.7] - 2026-06-08
|
|
398
|
+
|
|
399
|
+
### Changed
|
|
400
|
+
|
|
401
|
+
- **Bumped `OPS_MCP_VERSION` 0.4.3 → 0.4.4** in `src/lib/mcp-packages.ts`. Picks up the `@uluops/ops-mcp@0.4.4` ship: `validate_run` tool now accepts and previews `analysis_records` and `analysis_summary` (mirrors `save_run` shape), tool description advertises the new return fields (`would_create_analysis_records`, `would_create_analysis_summaries`), and the SDK dep moves to `@uluops/ops-sdk@3.2.2` for the matching wire-side forwarding + response parsing. The change cascades through the harness reattestation flow — fresh `npx -y @uluops/setup` installs and existing `npx @uluops/setup` reattestations after 0.9.7 will stamp `@uluops/ops-mcp@0.4.4` into Claude Code / Codex / Gemini / OpenCode harness configs, replacing the prior 0.4.3 pin. Harness configs already on disk continue resolving 0.4.3 until reattestation runs.
|
|
402
|
+
|
|
403
|
+
Companion releases shipped same day: `ops-uluops-api@1.58.1` (dry-run completeness on `/runs/validate` + enriched Zod error envelope with `code`/`expected`/`received` per issue), `@uluops/ops-sdk@3.2.2`. Together these close the Codex friction surfaced on the 2026-06-08 foundations skill run where `validate_run` accepted runs that `save_run` later rejected on analysis-record shape. Tracker: `ops-uluops-api` `c29dd21e` (PRA-DRI/H — dry-run incomplete), `f5a04d90` (EPI-OPA/M — Zod error opacity); `ops-uluops-mcp` `6f3e5b4c` (SEM-VAL/M — analysis_records advertised as any[]), `a2dda4d5` (EPI-OPA/M — record_id maxLength undocumented). All four resolved with this wave.
|
|
404
|
+
|
|
405
|
+
`REGISTRY_MCP_VERSION` remains 0.2.9 — no changes in this release.
|
|
406
|
+
|
|
407
|
+
## [0.9.6] - 2026-06-08
|
|
408
|
+
|
|
409
|
+
### Changed
|
|
410
|
+
|
|
411
|
+
- **Bumped MCP pins to pick up the live-tests T2 wave.** `src/lib/mcp-packages.ts`:
|
|
412
|
+
- `OPS_MCP_VERSION` 0.3.1 → **0.4.3** — F10 `get_issue_history` description rewrite + dropped dead `include_diffs` parameter; F8 `get_analytics` `cross_project_patterns` placeholder note; @uluops/ops-sdk 3.0.4 → 3.2.1 (CWE-20 `.max()` bounds on history-event fields, BREAKING `IssueHistoryEnvelope` return shape for `getHistory` with `transitionType`/`revertedChangeId` tombstone fields); vitest dev pin 2.1.9 → 3.2.6 (closes CVSS 9.8 UI server file-read/exec); description-text anchor tests; `prepublishOnly` safety net added.
|
|
413
|
+
- `REGISTRY_MCP_VERSION` 0.2.7 → **0.2.9** — @uluops/registry-sdk 0.30.2 → 0.31.1 (R12 envelope rewrite — `DependencyGraphResponse` recursive graph + `flat[]` + `totalCount` + `maxDepth`; `DependentsResponse` with `Dependent[].context`; CWE-674 pre-parse depth guard at `MAX_SAFE_GRAPH_DEPTH=50`; CWE-20 `.max()` bounds on `name`/`version`/`context`); `prepublishOnly` safety net added.
|
|
414
|
+
|
|
415
|
+
Fresh `npx -y @uluops/setup` installs and harness reattestations after 0.9.6 will stamp these specs into Claude Code / Codex / Gemini / OpenCode harness configs, replacing the prior 0.3.1 / 0.2.7 pins. Existing installs need a re-attestation (`npx @uluops/setup`) to pick up the new specs — harness configs already on disk continue resolving the old versions.
|
|
416
|
+
|
|
417
|
+
`@uluops/cli` install (`src/steps/cli.ts:56`) remains unpinned (`npm install -g @uluops/cli`) — intentional, since the CLI is a user-installable global tool managed via `npm update -g`, not a harness-stamped MCP spec. Users who want the 0.13.2 CLI run `npm update -g @uluops/cli` independently.
|
|
418
|
+
|
|
419
|
+
Suite 360/360 pass on the bumped pins.
|
|
420
|
+
|
|
421
|
+
## [0.9.5] - 2026-06-08
|
|
422
|
+
|
|
423
|
+
### Added
|
|
424
|
+
|
|
425
|
+
- **`--no-metrics` flag opts out of the agent-metrics hook install.** Threaded through `cli.ts` → `runSetup` → `configureMetricsStep`. When set, the metrics step short-circuits with `skippedReason: "no-metrics-flag"` before any I/O and emits a dim `Metrics hook skipped (--no-metrics)` line in the summary. The downstream `@uluops/agent-metrics` CLI prompt is also suppressed because its gate (`anyHookConfigured`) requires at least one harness with a configured hook. Closes the adoption-drift finding that the metrics install lacked an opt-out vocabulary for privacy- or compliance-sensitive environments; the bigger question (default opt-in vs opt-out, data-minimization surface, organizational consent) remains a roadmap item.
|
|
426
|
+
|
|
427
|
+
### Fixed
|
|
428
|
+
|
|
429
|
+
- **`--local-defs` README description corrected.** README line 140 said `Save definitions to ./uluops/ for review`, implying a review-only export. The flag actually does a project-scoped install — it redirects `installAgents/Commands/Skills` to write into `./uluops/agents/`, `./uluops/commands/`, etc. instead of the harness's home directory. CLI help text (`"Save agents/commands locally (./uluops/) for project isolation"`) was already correct; README now matches reality. Closes the adoption-drift finding that users could mistake the flag's purpose.
|
|
430
|
+
|
|
431
|
+
### Changed
|
|
432
|
+
|
|
433
|
+
- **`asset-catalog.ts` names the canonical-source-of-truth choice via `CATALOG_COMMANDS_DIR` constant.** Two hard-coded `"claude-code"` string literals in `getAgentCommands()` and `getWorkflowCommands()` were replaced by a single named constant with a comment explaining that `assets/claude-code/commands/` is the reference set rendered for all harnesses at install time. Other harnesses (codex, gemini-cli, opencode) do not ship parallel command-markdown trees — the listing is the catalog, not a per-harness manifest. Closes the PRA-MAT finding by making the intentional design choice explicit rather than implicit.
|
|
434
|
+
- **`hintPassword` split into pure `getPasswordHint(): string | null` + I/O wrapper.** The pure function returns the first applicable hint message (or null) and has zero I/O; the wrapper emits the hint via `console.warn` and remains the inquirer `validate` callback. Closes the PRA-MAT finding tangling computation with display. Both functions are exported (internal-only) for testing; 7 new direct tests on the pure path.
|
|
435
|
+
|
|
436
|
+
### Tests
|
|
437
|
+
|
|
438
|
+
- **4 new direct unit tests for `installMetrics` orchestration** in `src/test/metrics.test.ts`: the three no-hook-support short-circuit paths (`hooks` null, `toolsDir` null, `settingsPath` null) and the dry-run no-write contract. Previously `installMetrics` was only exercised indirectly via integration tests.
|
|
439
|
+
- **1 new test for `configureMetricsStep` `--no-metrics` short-circuit** asserting the skipped-via-flag outcome with no I/O attempted, even on a profile that fully supports hooks.
|
|
440
|
+
|
|
441
|
+
Test suite: 348 → **360 passing**.
|
|
442
|
+
|
|
443
|
+
## [0.9.4] - 2026-06-07
|
|
444
|
+
|
|
445
|
+
### Fixed
|
|
446
|
+
|
|
447
|
+
- **Quoted `description` in bundled `assets/codex/skills/uluops-operator/SKILL.md`.** The skill's YAML frontmatter contained `description: Use when operating inside UluOps from Codex: using UluOps MCP tools…` — the second unquoted colon (inside the description value) caused Codex's skill loader to throw `invalid YAML: mapping values are not allowed in this context at line 2` and silently skip the skill on startup. Every user installed via `@uluops/setup` ≤ 0.9.3 received the malformed file. The install summary's `✓ 1 skills → ~/.codex/skills/` line actively masked the failure — the skill landed on disk but never loaded. Wrapping the description in double quotes is the minimal fix; the value is unchanged.
|
|
448
|
+
- **Added bundled-asset frontmatter scanner** (`src/test/asset-frontmatter.test.ts`). Recursively enumerates every `.md` under `assets/`, extracts each file's YAML frontmatter, and asserts no frontmatter line contains more than one top-level (unquoted) colon. Catches the same class of bug for any future asset addition — the scanner is structural, not allow-listed to a specific file. The current asset surface (76 markdown files across all four harnesses) clears the check.
|
|
449
|
+
|
|
450
|
+
## [0.9.3] - 2026-06-07
|
|
451
|
+
|
|
452
|
+
### Fixed
|
|
453
|
+
|
|
454
|
+
- **Bumped `REGISTRY_MCP_VERSION` 0.2.6 → 0.2.7** (`src/lib/mcp-packages.ts:27`) to pin every harness's MCP config at `@uluops/registry-mcp@0.2.7`. The new registry-mcp version pulls in `mcp-secure-server@0.0.15-security`, which closes a `top`/`whoami` false-positive in the COMMAND_INJECTION layer. Pre-fix symptom — calling `get_ecosystem_overview({ fields: ["topPerformers"] })` from Codex or Claude Code was rejected by layer 2 as `Top Process Monitor` before reaching the registry's subscription-tier check. The bug affected every harness simultaneously because the pinned spec lives in the shared `mcp-packages.ts` constants module — the same property that amplified the 0.2.5 silent-exit incident in 0.9.1. Verified end-to-end via Verdaccio: mcp-secure-server 0.0.15-security published locally → installed into registry-mcp → live regex probe confirmed `topPerformers` and friends pass through while real shell invocations remain blocked.
|
|
455
|
+
|
|
456
|
+
## [0.9.2] - 2026-06-07
|
|
457
|
+
|
|
458
|
+
### Fixed
|
|
459
|
+
|
|
460
|
+
- **Bumped `REGISTRY_MCP_VERSION` 0.2.5 → 0.2.6** (`src/lib/mcp-packages.ts:27`). The 0.9.1 pin to `@uluops/registry-mcp@0.2.5` exposed a silent-exit bug in that version's ESM entry-point guard — `argv[1] === fileURLToPath(import.meta.url)` returned false under `npx -y` symlink invocation, so `main()` never ran and the process exited 0 with no output on either stream. Every harness (Claude Code, OpenCode, Gemini CLI, Codex) inherited the broken pin from the shared spec constant and silently failed to connect to the registry MCP server post-setup. `@uluops/registry-mcp@0.2.6` resolves both sides of the entry-guard comparison through `realpathSync` before equality testing; the npx symlink case now succeeds.
|
|
461
|
+
|
|
462
|
+
### Known gap (deferred)
|
|
463
|
+
|
|
464
|
+
- **No npx smoke gate before config-write.** `runHealthCheck` in `src/commands/helpers.ts` probes the API endpoints, not the MCP binaries it's about to stamp into harness configs. A `npx -y <pinned-spec> --version` probe per server would have caught 0.2.5's silent-exit bug before setup declared success — the silent-exit symptom is structurally indistinguishable from a working server until a real handshake is attempted. Tracked for 0.9.3.
|
|
465
|
+
|
|
466
|
+
## [0.9.1] - 2026-06-07
|
|
467
|
+
|
|
468
|
+
### Changed
|
|
469
|
+
|
|
470
|
+
- **Pinned MCP server versions stamped into every harness config.** All four harness writers now emit `@uluops/ops-mcp@0.3.1` and `@uluops/registry-mcp@0.2.5` in their `npx -y …` argument lists instead of bare package names. Pinning makes a `@uluops/setup` release self-contained — what users get on first MCP launch is the exact combination the setup release was tested against, regardless of when later MCP server versions ship. A downstream regression in `@uluops/ops-mcp@0.3.2` or `@uluops/registry-mcp@0.2.6` cannot silently land on a setup user the day after publish; the next setup release picks up the new combination explicitly. Surfaced on 2026-06-07 when `@uluops/registry-mcp@0.2.4` shipped with a stale `@uluops/definition-factory` dep that blocked external connections — without pinning, every setup user picked up the broken version automatically on the first `npx -y` resolution.
|
|
471
|
+
- **Single source of truth for MCP package + version constants** (`src/lib/mcp-packages.ts`). `OPS_MCP_PACKAGE`, `OPS_MCP_VERSION`, `OPS_MCP_SPEC`, `REGISTRY_MCP_PACKAGE`, `REGISTRY_MCP_VERSION`, `REGISTRY_MCP_SPEC` exported from one module. Each harness writer (`config-merger.ts` for Claude Code / Gemini CLI, `opencode.ts`, `codex.ts`) imports the spec constants instead of literal strings. A version bump is now one edit in one file; the prior layout had three independent literal sites that could drift. The npm availability probe still uses the bare package names (`MCP_PACKAGES`) — that probe asks "does this name exist on the registry," not "does this version exist," so a temporary registry blip on an older version cannot fail setup when the latest version is reachable.
|
|
472
|
+
|
|
473
|
+
## [0.9.0] - 2026-06-07
|
|
474
|
+
|
|
475
|
+
### Added
|
|
476
|
+
|
|
477
|
+
- **Codex promoted to stable.** `--all-detected` (and the interactive multi-select checkbox) now includes Codex when `~/.codex/` exists on disk, matching the relational promise made by the other three stable harnesses (Claude Code, OpenCode, Gemini CLI). Previously `codexProfile.status = "experimental"` filtered Codex out of `detectHarnesses()`, so a WSL user with all four harnesses installed had to run `npx @uluops/setup --harness codex` separately to pick it up — silently defeating the multi-target install positioning. The HarnessNotTestedError surface is preserved as a structural slot for the next experimental scaffold; its message now lists all four stable harnesses.
|
|
478
|
+
- **Codex MCP config writer auto-approves read-side tools.** First-time installs now seed `[mcp_servers.uluops-tracker.tools.<NAME>]` and `[mcp_servers.uluops-registry.tools.<NAME>]` blocks with `approval_mode = "approve"` for every `sideEffects: "read"` tool exported by `@uluops/ops-mcp` and `@uluops/registry-mcp` (29 tracker reads + 32 registry reads). Without these, Codex prompts the user on every read call — making interactive sessions hostile to inspection workflows that the harness was specifically promoted to support. Write-side tools (`save_run`, `bulk_update_status`, `publish_definition`, etc.) are deliberately NOT seeded, so state-changing operations retain a per-call approval gate.
|
|
479
|
+
|
|
480
|
+
### Changed
|
|
481
|
+
|
|
482
|
+
- **Codex TOML uses bare keys, not JSON-quoted keys.** `[mcp_servers.uluops-tracker]` rather than `[mcp_servers."uluops-tracker"]`. Both are valid TOML (hyphens are permitted in bare keys per the TOML v1.0.0 spec), but the unquoted form is what Codex's own writer emits — matching it keeps re-merge diffs minimal for users who interleave `npx @uluops/setup` with Codex's interactive config edits. The merge logic accepts either form on read so existing 0.8.x-installed configs are upgraded in place without re-quoting; the strip step continues to match both quoted and unquoted variants.
|
|
483
|
+
- **Re-install preserves user-customized tool approvals.** When the merge detects ANY `[mcp_servers.<SERVER>.tools.*]` block already present under one of the UluOps server names, it treats the whole tools surface for that server as user-managed and skips seeding — leaving denials, additions, and write-tool approvals untouched. A re-install over a hand-tuned config replays only the main + env blocks (with the current API key + canonical package args), preserving every per-tool choice the user made between installs.
|
|
484
|
+
|
|
485
|
+
### Known issues (deferred from 0.8.1)
|
|
486
|
+
|
|
487
|
+
- **Gemini settings.json double-write race** (`src/harnesses/gemini-cli.ts`). Still tracked for a future patch — fix is to extend `HookStrategy` with an optional `installWithMcp` that coalesces the MCP-config write and the hook-settings write into a single atomic boundary.
|
|
488
|
+
|
|
489
|
+
## [0.8.1] - 2026-06-07
|
|
490
|
+
|
|
491
|
+
### Added
|
|
492
|
+
|
|
493
|
+
- **API key persistence on signup and first-key-via-flag/prompt runs.** `signup()` previously returned a freshly-minted key that was embedded in MCP config blocks but never written to `~/.uluops/credentials.json` — the file `@uluops/cli` and the SDK read first when resolving keys. A new user running `npx @uluops/setup --signup` without `--shell` (default off) would open a fresh terminal and discover their account did not exist as far as `ulu` was concerned, with no recovery path short of minting another key. `initContext` now captures `hasCredentialsFile()` before auth runs and calls the new `writeCredentialsFile(apiKey, { email, source })` exporter after a successful signup OR when no prior credentials file was found. File is created with mode `0o600` under a `0o700` parent dir; merging into an existing file preserves any non-`default` profiles. Round-trip with `readCredentialsFile` verified in `src/test/auth.test.ts` (7 new tests).
|
|
494
|
+
|
|
495
|
+
### Security
|
|
496
|
+
|
|
497
|
+
- **Bumped vitest 3.2.4 → 4.1.8** to close GHSA-5xrq-8626-4rwp (CVSS 9.8 — unauthenticated arbitrary file read/exec via Vitest UI server). devDependency only; npm-published artifact (controlled by `files` glob) was never exposed, but local dev/CI machines running `npm test` were. `npm audit` now reports 0 vulnerabilities. 340/340 → 345/345 tests pass on the new major.
|
|
498
|
+
- **Atomic-write symlink race closed** (`src/lib/atomic-write.ts`). Temp filenames are now `${path}.uluops-tmp.${randomBytes(8).hex}` (unpredictable) instead of the fixed `.uluops-tmp` suffix, and `writeFile` opens with `flag: "wx"` (O_CREAT|O_EXCL) so a pre-positioned symlink or file at the temp path causes an atomic failure instead of a follow-through write to the attacker's target. CWE-377 resolved.
|
|
499
|
+
- **Shell profile permission preservation** (`src/steps/shell.ts`). The update-block, append-new-block, and remove-block paths now all pass `{ mode: 0o600 }` to `atomicWrite`. Previously, rewriting an existing `~/.zshrc` that contained the UluOps fence downgraded the file from whatever mode it had (often `0o600` on hardened dotfiles) to the umask default (`0o644`/`0o666`), exposing any other secrets in the profile to group/world readers. CWE-732 resolved on three call sites.
|
|
500
|
+
- **`writeSettings` mode tightened** (`src/lib/settings-merger.ts:117`). Now passes `{ mode: 0o600 }`, matching the security level `config-merger.writeConfig` already applied to MCP config files. Aligns the hook-settings write path with the rest of the credential-bearing file writes.
|
|
501
|
+
|
|
502
|
+
### Removed
|
|
503
|
+
|
|
504
|
+
- **Backup machinery deleted** — `backupFile` from `src/lib/file-ops.ts`, `getBackupDir` from `src/lib/paths.ts`, internal `backupConfig`/`backupProfile` helpers from `src/steps/mcp.ts` and `src/steps/shell.ts`, plus the related test block. The mechanism wrote timestamped `.bak` copies into `~/.uluops/backups/<harness>/` on every install/uninstall, but **no code path in the package ever read them** — backups were forensic-only archaeology that accumulated unboundedly with each install. Recovery was always intended to flow through the manifest's `partial: "<step>"` marker plus idempotent re-run (the path documented in the README), which remains in place. Aligning implementation with the README's actual recovery promise removes ~80 lines of unused-by-the-tool code and closes the unbounded disk accumulation issue.
|
|
505
|
+
|
|
506
|
+
### Known issues (deferred)
|
|
507
|
+
|
|
508
|
+
- **Gemini settings.json double-write race** (`src/harnesses/gemini-cli.ts`). `installMcp` and `installMetrics` both read-merge-atomic-write the same `~/.gemini/settings.json` file sequentially with a tool-file copy in between (Gemini's vendor consolidated MCP config and hook settings into one file; Claude Code's two-step sequence was extended mechanically without recognizing the invariant collapse). A process kill or ENOSPC between the two writes leaves Gemini with MCP-but-no-hooks while the manifest, saved post-loop, has no record of partial state. Tracked for v0.9.0 — fix is to extend `HookStrategy` with an optional `installWithMcp` that coalesces both writes into a single atomic boundary.
|
|
509
|
+
|
|
510
|
+
## [0.8.0] - 2026-06-07
|
|
511
|
+
|
|
512
|
+
### Added
|
|
513
|
+
|
|
514
|
+
- **Multi-target install — one invocation, every detected harness.** `@uluops/setup` is positioned as the zero-friction installer for any agentic stack a user has. Before this release, a user with Claude Code + Codex + Gemini CLI on the same machine had to run setup three separate times, repeating the API-key resolution, signup decision, npm-availability probe, and health check on every invocation. That contradicted the positioning the moment a user had two harnesses. New CLI surface:
|
|
515
|
+
- `--harness all` and `--all-detected` install into every detected stable harness in one run.
|
|
516
|
+
- `--harness claude-code,codex` installs into a specific comma-separated subset.
|
|
517
|
+
- Interactive multi-detection now uses a `@inquirer/prompts/checkbox` with every option checked by default — the "install everywhere" case is a single Enter press; uncheck entries with space to install into a subset.
|
|
518
|
+
- Non-interactive multi-detection preserves today's first-detected behavior to keep CI scripts predictable; CI users opt in to multi-install explicitly with `--all-detected`.
|
|
519
|
+
- `--harness <single-name> --all-detected` is a conflicting-flags error that fails fast with no state touched.
|
|
520
|
+
- `--harness all` with zero detected falls back to the default (`claude-code`) so the landing-page "just run npx @uluops/setup" promise is preserved.
|
|
521
|
+
- **Per-target failure isolation.** One harness failing does not abort the others. The orchestrator splits each per-harness step into its own `try`/`catch`; a failing harness lands as `failed` (operational error) or `declined` (user-rejected conflict prompt) in the per-harness summary while siblings install cleanly. The new `HarnessManifest.partial` field records which step threw when a post-MCP-success step (agents, commands, skills, metrics) fails — earlier steps' file lists are preserved so `--verify` and `--uninstall` operate on honest state.
|
|
522
|
+
- **4-tier exit-code classifier (spec §7.5).** Exit 0 when every harness succeeded, every harness was declined, or the run was a no-op (user unchecked everything on the prompt). Exit 1 only when at least one harness failed operationally (EACCES, ENOSPC, parse error, etc.). User-rejected conflict prompts no longer poison the exit code — CI scripts wrapping `--harness all` only fail on actionable errors.
|
|
523
|
+
- **Multi-harness summary block with per-status icons.** New unified rendering in `src/lib/display.ts` produces a `[<Harness>] installed/failed/skipped` line per target with ✓/✗/⊘/⚠ icons, partial-state markers, and a per-failure `Re-run: npx @uluops/setup --harness <name>` hint. The combined restart instruction at the end names every successfully-installed harness. Single-harness runs preserve today's `Setup complete!` banner format exactly (regression baseline).
|
|
524
|
+
- **`--uninstall --harness <name>` filter (symmetric to install).** Uninstall now accepts the same syntax as install: single name, comma-separated subset, `all` sentinel, `--all-detected` synonym, with the same fail-fast flag-conflict detection. Subset uninstall removes only the named harnesses, updates the manifest in place (instead of deleting it), and **preserves shared infrastructure** — the global `@uluops/cli`, `@uluops/agent-metrics`, and shell-profile export are only removed on a full uninstall, since remaining harnesses still need them. Unknown harness in the filter fails fast with an error message listing what IS in the manifest so the user can correct typos.
|
|
525
|
+
- **`--verify` partial-install warning.** When the manifest records `partial: "<step>"` on a harness entry, verify surfaces a `[<Harness>] partial install — failed at "<step>"` row with a re-run hint. The per-file checks still run because the recorded lists are honest — the warning adds context about why a re-run is needed. Verify exits non-zero on partial state — partial isn't "passes", it's "incomplete".
|
|
526
|
+
- **Full Codex harness implementation** (lifted from scaffold to first-class support). Real TOML `mcp_servers` write/read/remove with nested table + env subtable handling, plus a skills install step delivering `ULUOPS_OPERATOR` under `~/.codex/skills`. Codex is still flagged `status: "experimental"` so it's excluded from `--all-detected` detection; opt in explicitly with `--harness codex`.
|
|
527
|
+
|
|
528
|
+
### Fixed
|
|
529
|
+
|
|
530
|
+
- **`installAgents.files` now tracks only successfully-copied files.** Previously returned the source `readdir` listing including failed files — so a failed copy ended up in `manifest.agents[]` even though the file was never on disk. Subsequent `--uninstall` would attempt to remove a never-written file (harmless but noisy), and `--verify` falsely reported drift. Aligned with `installCommands`/`installSkills` which already only push to their files lists inside the try-block. Prerequisite for the multi-target install partial-state contract (the manifest treats `agents`/`commands`/`skills` as the authoritative list of what's on disk; all three installers must honor that).
|
|
531
|
+
- **`src/cli/select-harnesses.ts` added to the package tarball.** The Phase 2 selection module was missing from `package.json`'s `files` glob — the unit suite imported from source so vitest passed, but the published tarball would have shipped a broken `cli.js` with an unresolvable `ERR_MODULE_NOT_FOUND` import. Caught by the docker test substrate on its first multi-target scenario run. Fixed by adding `dist/cli/**` to the `files` field.
|
|
532
|
+
|
|
533
|
+
### Internal
|
|
534
|
+
|
|
535
|
+
- **New module structure** for the multi-target orchestration:
|
|
536
|
+
- `src/commands/per-harness.ts` — `PerHarnessResult` type + `classifyExit` 4-tier classifier (extracted from `setup.ts` so `display.ts` can import the type without circular dependency).
|
|
537
|
+
- `src/commands/errors.ts` — typed `ConflictRejectedError` (replaces `process.exit(0)` in `checkConflicts` so the per-harness loop can catch and continue).
|
|
538
|
+
- `src/cli/select-harnesses.ts` — pure selection logic for the §5 behavior matrix (prompt callback injected for testability; cli.ts wires the real `@inquirer/prompts/checkbox`).
|
|
539
|
+
- `src/commands/uninstall-filter.ts` — pure filter parser + validator mirroring the install-side syntax.
|
|
540
|
+
- **`runSetup` restructured** into outer (once-per-run: `initContext`, install-lock, manifest load) and inner (per-harness: conflict check, MCP, agents, commands, skills, metrics) phases plus once-per-run-after globals (CLI install, agent-metrics CLI install gated on aggregate `anyHookConfigured`, health check, shell, single `saveManifest`). Each iteration reads its own slice of `existingManifest?.harnesses[harnessName]` for drift detection — no cross-iteration state reuse.
|
|
541
|
+
- **`HarnessManifest.partial?: PartialStep | null`** additive field with `isNewManifest` validation when present. Absent on pre-multi-target manifests (assumed fully installed). Re-runs against a partial entry re-prompt `checkConflicts` (gated on `existingHarness.partial == null`) so the safety check isn't bypassed on the recovery path.
|
|
542
|
+
- **Suite: 240 → 340 tests (+100):**
|
|
543
|
+
- `src/test/select-harnesses.test.ts` (26) — every row of the §5 behavior matrix.
|
|
544
|
+
- `src/test/per-harness.test.ts` (10) — every row of the §7.5 4-tier exit-code table.
|
|
545
|
+
- `src/test/display-summary.test.ts` (10) — single-harness regression baseline + multi-harness mixed-outcome rendering + partial entry + all-declined + `maskKey` behavior; captures stdout, strips ANSI, asserts on substrings.
|
|
546
|
+
- `src/test/uninstall-filter.test.ts` (16) — CLI matrix + conflict detection + unknown-harness validation + edge cases.
|
|
547
|
+
- `src/test/verify.test.ts` (+2) — partial-install warning emitted; absent partial field does NOT emit the warning row.
|
|
548
|
+
- `src/test/agents.test.ts` (+1 assertion) — failed file not in `installedFiles`.
|
|
549
|
+
- **Docker test substrate: 12 → 16 scenarios:**
|
|
550
|
+
- `multi-all-detected` — 3 detected harnesses install in one invocation; manifest aggregates all three.
|
|
551
|
+
- `multi-explicit-subset` — `--harness claude-code,codex` honors explicit list when 4 harnesses detected; user-typed order preserved; no cross-harness contamination.
|
|
552
|
+
- `multi-flag-conflict` — `--harness codex --all-detected` exits non-zero, no state touched.
|
|
553
|
+
- `multi-non-interactive-default` — CI compatibility: `--yes` + multi-detect preserves first-detected + dimmed notice.
|
|
554
|
+
- `multi-harness-all-zero-detected` — `--harness all` with no detection falls back to claude-code.
|
|
555
|
+
- `multi-mcp-fail-one` — sabotages opencode (pre-create `opencode.json` as a directory → EISDIR), asserts failure isolation: siblings install, exit 1, per-harness summary surfaces failure + re-run hint, opencode absent from manifest.
|
|
556
|
+
- `multi-verify-partial` — installs, sabotages manifest to set `partial: "agents"` (with recomputed contentHash), runs `--verify`, asserts partial warning row + non-zero exit + per-file checks still ran.
|
|
557
|
+
- `multi-uninstall-subset` — installs 3 harnesses, `--uninstall --harness opencode`, asserts opencode removed + others preserved + manifest updated (not deleted) + globals-preservation notice.
|
|
558
|
+
- `multi-uninstall-unknown-harness` — install claude-code, `--uninstall --harness opencode`, asserts non-zero exit + error names unknown harness + lists manifest contents + state untouched.
|
|
559
|
+
|
|
560
|
+
### Breaking changes
|
|
561
|
+
|
|
562
|
+
- `runSetup` programmatic signature: `harness: string` → `harnesses: string[]`. The CLI is the only documented caller; internal callers (if any) need a one-line change to wrap their single-harness invocation in `[harnessName]`.
|
|
563
|
+
|
|
564
|
+
### Spec / process
|
|
565
|
+
|
|
566
|
+
This release ships against a specification authored and reviewed via the pre-implementation pipeline:
|
|
567
|
+
- **Spec:** `plans/multi-harness/setup-multi-target-install-spec-v0_1_0.md` (v0.2.2, Option A — multi-select checkbox + `--all-detected` + comma-split — locked in after pre-implementation pipeline produced architect / docs-validator / assumption-excavator reviews; persona-evidence claim was rewritten to ground in product-positioning consistency after the assumption-excavator surfaced the unsourced claim).
|
|
568
|
+
- **Checklist:** `plans/multi-harness/setup-multi-target-install-checklist-v0_2_1.md` tracks each phase with gates between them; every checked item maps to a commit on `feature/multi-target-install`.
|
|
569
|
+
|
|
570
|
+
## [0.7.1] - 2026-06-05
|
|
571
|
+
|
|
572
|
+
### Fixed
|
|
573
|
+
|
|
574
|
+
- **`@uluops/agent-metrics` global-install detection no longer false-positives under `npx`.** v0.7.0's `defaultAgentMetricsExecutor.detect` ran `spawnSync("agent-metrics", ["--version"])` to decide whether to skip the global install. But `@uluops/agent-metrics` is a runtime dependency of `@uluops/setup` itself (used by `findMetricsSource` to resolve files to copy), so when setup runs under `npx @uluops/setup`, npx prepends its transient cache `.bin/` to PATH for the spawned process — the bin resolves there even when the user has nothing installed globally. Detect returned "0.4.0", setup reported "already installed — no change", user hit `command not found` after npx exited. Detect now queries npm directly via `npm ls -g --depth=0 --json` and parses the result, answering the actual question ("is it in the user's global install") instead of a PATH-resolution proxy. Pure JSON-parsing logic split out as `parseGlobalAgentMetricsVersion` for direct unit coverage. 5 regression tests added covering: package-present, package-absent (empty + no-deps shapes), unrelated-deps-only, version-field-missing, and unparseable-stdout. The companion `@uluops/cli` flow does NOT have this bug because setup doesn't depend on `@uluops/cli` transitively; its detect is left as-is.
|
|
575
|
+
- **`--help` and `--uninstall` now work for users with a malformed `XDG_CONFIG_HOME`.** The opencode harness module previously ran a module-load IIFE that threw on a non-absolute or traversal-containing `XDG_CONFIG_HOME` — and the throw fired during `harnesses/index.ts` imports for every CLI entry point, blocking the user from running the very commands they would need to recover. Validation is now deferred to harness selection: the module loads with a fallback path, the error is captured, and `assertOpencodeEnvironment()` is invoked from `getProfile("opencode")` only when the user actually targets opencode. Selecting an unrelated harness (or running `--help`, `--uninstall` of claude-code, etc.) is now unblocked. Surfaced by ship-pipeline code-auditor on `uluops-setup` run #19 as PRA-CON/H.
|
|
576
|
+
- **`checkMcpPackageAvailability` now surfaces the real network failure reason** instead of a literal "unknown" string. The previous `?? "unknown"` fallback could put `unknown` into the missing-packages list, producing the unactionable warning `npm packages not found in registry: unknown`. Per-index correspondence between `Promise.allSettled` results and `MCP_PACKAGES` is now asserted directly; on rejection (DNS, timeout, TLS, etc.) the package name is annotated with `(network: <reason>)`, on non-2xx the bare package name is used. Surfaced as STR-INC/H.
|
|
577
|
+
- **Empty `harnesses: {}` is no longer accepted as a valid manifest.** `isNewManifest` previously iterated `Object.values(harnesses)` and vacuously returned `true` for the zero-entry case. A truncated/partial write produced a file that loaded "successfully" — then uninstall would iterate zero harness entries, delete the manifest, and report `UluOps has been removed` while leaving every MCP config, agent, hook, and shell export in place. `isNewManifest` now requires at least one harness entry. Surfaced as SEM-COM/H.
|
|
578
|
+
- **`validateManifest` no longer emits a false "Cannot read manifest file" warning when the manifest came from legacy.** `loadManifest` migrates a legacy manifest in memory without writing it back to the new location, but `validateManifest` hardcoded `getManifestPath()` (new) for the hash check — the read failed on every uninstall after migration, training users to ignore real corruption signals. The hash verification now reads whichever manifest file actually exists (new path tried first, legacy as fallback) and silently skips when neither is on disk. The "modified since installation" hash-mismatch warning is preserved for the genuine tamper case. Surfaced as SEM-INC/H.
|
|
579
|
+
- **`npm install -g` and `npm uninstall -g` now timeout after 5 minutes.** Both `defaultExecutor` (in `src/steps/cli.ts`) and `defaultAgentMetricsExecutor` (in `src/steps/agent-metrics-cli.ts`) called `spawnSync` without a `timeout` option. A corporate proxy stall, registry slow-response storm, or a lifecycle script awaiting input could block setup indefinitely with no recovery path other than `^C`. Both executors now use a 5-minute upper bound, and the `detect` paths use a 30-second bound. Timeout-driven SIGTERM produces a clear `npm install exceeded 300s timeout and was terminated` error instead of a misleading exit-code failure. Surfaced as SEM-COM/H.
|
|
580
|
+
- **Windows/WSL path resolution for `@uluops/agent-metrics`.** `findMetricsSource` in `src/steps/metrics.ts` accessed `new URL('.', resolved).pathname` to derive the package root from `import.meta.resolve`. On Windows (including WSL when a path surfaces through a Windows mount), `.pathname` yields `/C:/path/...` — the leading slash before the drive letter is invalid, the subsequent `readFile(pkgRoot/package.json)` fails, and `findMetricsSource` returns `null` with `version: null`, defeating verify's drift detection. Now uses `fileURLToPath(resolved)` from `node:url`, which handles drive letters correctly. Surfaced as SEM-COR/H.
|
|
581
|
+
- **`acquireInstallLock` now creates the parent `~/.uluops/` directory before the atomic lock-dir mkdir.** First-time users with no `~/.uluops/` on disk hit `ENOENT: no such file or directory, mkdir '/home/.../.uluops/install.lock'` from `acquireInstallLock` because the lock-dir mkdir uses `recursive: false` (intentional — `mkdir` atomicity is the lock primitive) and ENOENT on the missing parent is not the same as EEXIST on the lock itself. The parent is now pre-created with `recursive: true` while the lock-dir mkdir keeps its atomicity contract. Surfaced by the new `docker/scenarios/fresh-install.sh` substrate on its very first run against a clean WSL-shaped Ubuntu container — exactly the bug class that local `npm test` cannot reproduce because dev machines always have `~/.uluops/` from prior runs. Regression test pinned at `src/test/install-lock.test.ts`.
|
|
582
|
+
|
|
583
|
+
### Internal
|
|
584
|
+
|
|
585
|
+
- 17 new regression tests across the affected modules:
|
|
586
|
+
- 5 for the `agent-metrics` detect fix (covering present/absent/unrelated-deps/missing-version/unparseable-stdout shapes of `npm ls -g --json`).
|
|
587
|
+
- `src/test/config-merger.test.ts` — `checkMcpPackageAvailability` rejection-reason annotation + bare-package-name on registry miss (2 tests, mocked `fetch`).
|
|
588
|
+
- `src/test/manifest.test.ts` — empty-harnesses rejection + legacy-only validate no-false-warning + hash-mismatch tamper detection (3 tests).
|
|
589
|
+
- `src/test/cli.test.ts` — `summarizeSpawnResult` SIGTERM-timeout recognition + stderr-on-non-zero + clean-exit ok-path (3 tests, real subprocesses with tight timeouts).
|
|
590
|
+
- `src/test/harnesses.test.ts` — opencode module-load no-throw under invalid XDG + `assertOpencodeEnvironment` throws on demand + claude-code selection unaffected (3 tests with `vi.resetModules`).
|
|
591
|
+
- `summarizeSpawnResult` exported as `@internal` from `src/steps/cli.ts` for direct test access to the timeout branch.
|
|
592
|
+
- `assertOpencodeEnvironment` exported from `src/harnesses/opencode.ts` and invoked from `getProfile` in the harness registry.
|
|
593
|
+
- 1 install-lock test for the missing-parent-dir regression on first-time users.
|
|
594
|
+
- Suite: 223 → 240 tests (+17).
|
|
595
|
+
|
|
596
|
+
## [0.7.0] - 2026-06-05
|
|
597
|
+
|
|
598
|
+
### Added
|
|
599
|
+
|
|
600
|
+
- **Process-level install lock.** `runSetup` and `runUninstall` now acquire `~/.uluops/install.lock/` before touching shared state. A second concurrent `npx @uluops/setup` (or `uluops-setup --uninstall`) running on the same machine now fails fast with a clear message naming the holding PID, hostname, and how long it has been running — instead of silently racing the read-merge-write windows on `~/.claude.json`, `~/.gemini/settings.json`, `~/.config/opencode/opencode.json`, `~/.claude/settings.json`, `~/.bashrc`/`.zshrc`, and `~/.uluops/manifest.json` (six surfaces, not the one originally identified). Surfaced by ship-pipeline code-auditor as AF-006 on `uluops-setup` run #19. Hand-rolled around `mkdir`-atomicity — no new runtime dependency. Lock metadata `{pid, hostname, startedAt}` is written inside the lock dir; stale locks are reclaimed when the holding PID is detected as dead (same host) or when the lock is older than 30 minutes (cross-host fallback). SIGINT/SIGTERM/uncaughtException all release the lock before exit. Dry-run is read-only and bypasses the lock.
|
|
601
|
+
- **`agent-metrics` CLI prompt.** Setup now offers to install `@uluops/agent-metrics` globally so the `agent-metrics` command is available on PATH after install — previously the package was copied into `~/.claude/tools/agent-metrics/` only so the SubagentStop hook could invoke `dist/hook.js`, but the `bin` entry never reached PATH and users hit `command not found` when trying to inspect captures. The prompt fires only when the metrics hook itself was configured (i.e., when there are captures to read). New `--with-agent-metrics-cli` and `--no-agent-metrics-cli` flags mirror the existing `--with-cli` / `--no-cli` pair. Non-interactive runs (`--yes`, `--api-key`, no TTY) skip the prompt and require the explicit flag to install. Manifest gains `agentMetricsCliInstalled` + `agentMetricsCliInstalledVersion`; uninstall reverses the global install only when this setup performed it (same ownership rule as `@uluops/cli`).
|
|
602
|
+
|
|
603
|
+
### Known limitations
|
|
604
|
+
|
|
605
|
+
- **Setup-vs-harness races remain unaddressed.** This lock excludes other `uluops-setup` processes only. If the user is actively using Claude Code, Gemini CLI, or OpenCode while running setup, the harness CLI may write to its own state file (e.g. `~/.claude.json`) concurrently with our read-merge-write, and those harness writes can still be lost. A future spec will address this via content compare-and-swap on the merge target. Mitigation today: close the harness CLI before running setup.
|
|
606
|
+
|
|
607
|
+
### Internal
|
|
608
|
+
|
|
609
|
+
- New `src/lib/install-lock.ts` (~220 lines) with `acquireInstallLock`, `LockHandle.release()`, `InstallLockHeldError`, signal-handler registration, and a test seam for handler reset.
|
|
610
|
+
- New `src/lib/paths.ts:getInstallLockDir()` reusing `getUluopsDir()`.
|
|
611
|
+
- 11 unit tests in `src/test/install-lock.test.ts` covering acquire/release, fail-fast on held lock, stale-by-dead-PID, stale-by-timeout, stale-by-corrupt-meta, stale-by-missing-meta, `waitMs` polling success and timeout, idempotent release, and cross-host lock semantics.
|
|
612
|
+
- 1 integration test in `src/test/install-lock-integration.test.ts` spawning two real child `node` processes against the compiled dist — true OS-level concurrency serializes as expected.
|
|
613
|
+
- `src/cli.ts` formats `InstallLockHeldError` with a hint about stale-lock auto-recovery rather than emitting a stack trace.
|
|
614
|
+
- New `src/steps/agent-metrics-cli.ts` mirrors `src/steps/cli.ts` — `AgentMetricsCliExecutor` interface with `detect`/`install`/`uninstall`, `installAgentMetricsCli` + `uninstallAgentMetricsCli`, executor injection for tests.
|
|
615
|
+
- New `configureAgentMetricsCliStep` helper in `src/commands/helpers.ts` carries the decision matrix and user-facing prompt; `runSetup` invokes it after `configureMetricsStep`, gated on `metricsResult.hookConfigured`.
|
|
616
|
+
- 11 unit tests in `src/test/agent-metrics-cli.test.ts` covering install (already-present, success, failure, post-install detect miss, dryRun) and uninstall (absent, present, post-uninstall recovery, persistent failure, dryRun).
|
|
617
|
+
- Suite: 200 → 223 tests (+23 total for this release — 12 from install-lock + 11 from agent-metrics-cli).
|
|
618
|
+
|
|
619
|
+
## [0.6.5] - 2026-06-05
|
|
620
|
+
|
|
621
|
+
### Fixed
|
|
622
|
+
|
|
623
|
+
- **`.gitignore` no longer clobbered when `.gitignore` exists but cannot be read.** The previous `addToGitignore` (`src/steps/mcp.ts`) wrapped the `readFile` call in a bare `catch {}` that unconditionally wrote a single-line file. `ENOENT` was the intended trigger — the catch path exists to create `.gitignore` when it doesn't exist yet — but `EACCES`, `EISDIR`, `EBUSY`, and transient I/O errors were silently treated the same way, destroying any existing user content. The new `ensureGitignoreEntry` helper discriminates `err.code === "ENOENT"` for the fresh-write path and warns-and-skips on all other read errors. Surfaced by ship-pipeline code-auditor as AF-002 on `uluops-setup` run #19. The function is now exported from `src/steps/mcp.ts` with an injectable `reader` parameter so the non-ENOENT-no-clobber contract is directly testable.
|
|
624
|
+
- **Shell-profile fence handling now collapses duplicate UluOps blocks** left by earlier buggy installs. `writeShellExport` and `removeShellExport` in `src/steps/shell.ts` used `content.indexOf(FENCE_END)` (first occurrence) while a code comment at line 45 explicitly claimed "use last FENCE_END after FENCE_START to handle duplicates". The mismatch meant: (a) on re-install, the new block replaced only the first half of a duplicate-block region, leaving a stale block — and its stale `ULUOPS_API_KEY` export — sitting below the new one; (b) on uninstall, the second block was never removed. Both sites now use `content.lastIndexOf(FENCE_END)`. Surfaced by code-auditor as a SEM-INC/H finding.
|
|
625
|
+
|
|
626
|
+
### Internal
|
|
627
|
+
|
|
628
|
+
- New `ensureGitignoreEntry` tests in `src/test/mcp.test.ts` covering ENOENT (file creation), append-to-existing, idempotency on already-present entry, and the regression guard — non-ENOENT read failure must not clobber existing content.
|
|
629
|
+
- New `writeShellExport` and `removeShellExport` tests in `src/test/shell.test.ts` covering the duplicate-fence-block scenario for both install and uninstall.
|
|
630
|
+
- Suite now 200 cases (+12).
|
|
631
|
+
|
|
632
|
+
## [0.6.4] - 2026-06-05
|
|
633
|
+
|
|
634
|
+
### Fixed
|
|
635
|
+
|
|
636
|
+
- **`validateKey()` now hits the correct self-identity endpoint.** Server
|
|
637
|
+
validation called `GET /api/v1/registry/users/me` — the registry-api's
|
|
638
|
+
public user-lookup route, which Zod-validates the path param as a UUID and
|
|
639
|
+
returns `400 { id: ["Invalid uuid"] }` for the literal `me`. Endpoint has
|
|
640
|
+
been wrong since the initial `feat: implement @uluops/setup zero-friction
|
|
641
|
+
installer` (commit `70a01a2`); users hit it any time they ran setup with a
|
|
642
|
+
freshly-minted key and no `--skip-validation`. Now points at
|
|
643
|
+
`GET /api/v1/auth/me` (ops-uluops-api) and unwraps the
|
|
644
|
+
`{ data: { email, ... } }` envelope. Five regression tests added covering
|
|
645
|
+
URL, header, response unwrap, 401 path, 500 path, and network-failure path.
|
|
646
|
+
|
|
647
|
+
### Changed
|
|
648
|
+
|
|
649
|
+
- **Stopped stamping backend URLs into MCP host configs.** Previously
|
|
650
|
+
`mergeUluopsMcp` (Claude) and the OpenCode harness wrote
|
|
651
|
+
`ULUOPS_BASE_URL: "https://api.uluops.ai/api/v1"` for `uluops-tracker` and
|
|
652
|
+
`ULUOPS_REGISTRY_URL: "https://api.uluops.ai/api/v1/registry"` for
|
|
653
|
+
`uluops-registry` into every generated config. Both URLs are already
|
|
654
|
+
resolved automatically by `@uluops/ops-mcp` / `@uluops/registry-mcp` via
|
|
655
|
+
their bundled SDKs (prod by default), so stamping was redundant — and
|
|
656
|
+
worse, would pin every user to a static URL that could go stale if our
|
|
657
|
+
production endpoints ever shifted. The generated `env` block now contains
|
|
658
|
+
only `ULUOPS_API_KEY`. Pairs with `@uluops/ops-mcp@0.2.1` which made
|
|
659
|
+
`ULUOPS_BASE_URL` officially optional on the consumer side.
|
|
660
|
+
|
|
661
|
+
## [0.6.3] - 2026-06-05
|
|
662
|
+
|
|
663
|
+
### Changed
|
|
664
|
+
|
|
665
|
+
- **Setup now auto-detects the installed harness** when `--harness` was not passed explicitly. Previously the detection logic ran but its result was discarded — every default invocation wrote Claude Code-shaped config regardless of what was actually present. A Gemini-CLI-only user running `npx @uluops/setup` from the landing page no longer ends up with an inert `~/.claude/` tree.
|
|
666
|
+
- One harness detected → use it silently (no message for Claude Code to keep the common case quiet; a dim "Detected … — using as target" line for the other harnesses).
|
|
667
|
+
- Multiple harnesses detected → interactive runs prompt with a `select`; non-interactive runs (`--yes`, `--api-key`, no TTY) default to the first match and print a hint about `--harness`.
|
|
668
|
+
- No harnesses detected → fall back to `claude-code` (preserves the landing-page "just works" promise for fresh installs).
|
|
669
|
+
- `--harness <name>` passed explicitly → always honored, detection is skipped.
|
|
670
|
+
|
|
671
|
+
## [0.6.2] - 2026-06-05
|
|
672
|
+
|
|
673
|
+
### Changed
|
|
674
|
+
|
|
675
|
+
- **New users now get an "Are you creating a new account?" prompt as the first interactive question** instead of being dropped straight into an API-key input box. Default Y. Picking Y runs the email + password signup flow; picking n falls through to the existing API-key prompt. Eliminates the friction where the landing-page instruction (`npx @uluops/setup`) hit new users with a key prompt before they had any idea where to get a key.
|
|
676
|
+
- The new prompt is skipped automatically when the user has already provided a signal about who they are: `--api-key`, `--signup`, `--yes`, `ULUOPS_API_KEY` set in env, no TTY attached, or `~/.uluops/credentials.json` already on disk. Returning users see zero new prompts.
|
|
677
|
+
- `--signup` is preserved as an explicit override (skips the question, goes straight to signup) — useful for CI scripts or anyone who wants to bypass the confirm step.
|
|
678
|
+
|
|
679
|
+
### Added
|
|
680
|
+
|
|
681
|
+
- **`hasCredentialsFile()` exported from `steps/auth.ts`** — existence-only probe for `~/.uluops/credentials.json` used by the prompt-skip gate.
|
|
682
|
+
|
|
683
|
+
## [0.6.1] - 2026-06-05
|
|
684
|
+
|
|
685
|
+
### Changed
|
|
686
|
+
|
|
687
|
+
- **MCP package names switched to scoped `@uluops/*` form.** Setup now writes `npx -y @uluops/ops-mcp` and `npx -y @uluops/registry-mcp` into harness configs (Claude Code, Gemini CLI, OpenCode) instead of the legacy `uluops-tracker-mcp-client` / `uluops-registry-mcp-client` names. The MCP server names in config (`uluops-tracker`, `uluops-registry`) are unchanged — every `mcp__uluops-tracker__*` reference across the agent corpus keeps working. Only the npm package resolved by `npx` differs.
|
|
688
|
+
- **`checkMcpPackageAvailability` updated** to probe the new package names against the npm registry. Users who run setup before the two MCP packages are published will see the warning name the actual missing packages.
|
|
689
|
+
|
|
690
|
+
## [0.6.0] - 2026-06-05
|
|
691
|
+
|
|
692
|
+
### Added
|
|
693
|
+
|
|
694
|
+
- **Optional global `@uluops/cli` install during setup.** New `--with-cli` flag forces install without prompting; `--no-cli` forces skip. With neither flag, interactive runs prompt (default Y) and non-interactive runs (`--yes`, `--api-key`, no TTY) skip silently. The install step is best-effort — if `npm install -g` fails (permissions, nvm prefix surprise, network), setup surfaces a warning with the one-line cause and a manual install command, but the overall flow does not abort. If `ulu` is already on PATH, the step detects it and makes no changes. `manifest.cliInstalled` records ownership, so `--uninstall` removes the global package only when this setup installed it.
|
|
695
|
+
- **LICENSE file (MIT).** Aligns the setup package with the open-tooling stance for SDKs/CLIs/installers (proprietary surfaces remain in analytics/platform/tier-gate). `package.json` license field updated to `"MIT"` to match.
|
|
696
|
+
|
|
697
|
+
### Fixed
|
|
698
|
+
|
|
699
|
+
- **`dist/commands/**` was missing from the `files` field.** `cli.js` imports `runSetup`, `runUninstall`, and `runVerify` from `./commands/*`, but the `files` array shipped only `dist/cli`, `dist/lib`, `dist/steps`, and `dist/harnesses`. The v0.5.0 tarball crashed on first invocation with `ERR_MODULE_NOT_FOUND` before any user-visible output. v0.5.0 was never published to npm, so no consumers were affected.
|
|
700
|
+
|
|
701
|
+
### Changed
|
|
702
|
+
|
|
703
|
+
- **All `dependencies` and `devDependencies` pinned to exact versions** — removed caret ranges across the board (`@inquirer/prompts`, `@uluops/agent-metrics`, `chalk`, `commander`, `jsonc-parser`, and all dev tooling). Aligns this package with the UluOps-wide exact-pinning policy adopted 2026-06-01 in response to the RedHat-class supply-chain attack pattern.
|
|
704
|
+
|
|
705
|
+
## [0.5.0] - 2026-05-29
|
|
706
|
+
|
|
707
|
+
### Added
|
|
708
|
+
|
|
709
|
+
- **`hooksInstalledVersion` field on `HarnessManifest`** — records the agent-metrics version copied into the harness tree. The shared version ledger across the setup↔agent-metrics seam that the Confucius forecaster named as the missing piece.
|
|
710
|
+
- **`HarnessInstanceKey` type alias on `Manifest.harnesses`** — documents that today's `{profile.name}` keying assumes one install per profile, and names where future multi-instance support would extend.
|
|
711
|
+
- **`HarnessStatus` field on `HarnessProfile` (`"stable" | "experimental"`)** — `detectHarnesses()` now excludes experimental profiles so auto-detection never returns a profile that throws `HarnessNotTestedError`. Codex marked experimental; Claude Code, Gemini CLI, OpenCode marked stable. `getProfile()` still resolves experimental profiles so `--harness <name>` surfaces the explicit error.
|
|
712
|
+
- **`CLAUDE_HOOK_TYPES` and `DEFAULT_CLAUDE_HOOK_TYPE` exported** with anchor tests that surface drift in PR review. When Claude Code's hook schema evolves, the snapshot tests fail and point at downstream surfaces needing re-evaluation.
|
|
713
|
+
|
|
714
|
+
### Changed
|
|
715
|
+
|
|
716
|
+
- **`@uluops/agent-metrics` moved from `optionalDependencies` to `dependencies`** — it was always required for the headline metrics-hook feature; the optionality was a runtime-level skip for harnesses without hook support, not a declaration-level optionality. `installMetrics` still gracefully skips for OpenCode/Codex.
|
|
717
|
+
- **`copyToolFiles` now `rm -rf`s `dist/` before copying** — replaces instead of merges. Stale files from a previous agent-metrics version no longer persist on disk to shadow new files.
|
|
718
|
+
- **`verify` now reads the installed agent-metrics version** and compares it to the manifest's `hooksInstalledVersion`. Existence-only check is gone; drift surfaces as a verify failure with the version delta in the detail string.
|
|
719
|
+
- **`ULUOPS_HOOK_MARKER` renamed to `HOOK_OWNERSHIP_SIGNATURE`** and its value changed from `"tools/agent-metrics"` (path-coupled) to `"agent-metrics/dist/hook.js"` (suffix-based, path-independent). Existing hook commands match the new signature because all real commands end with this suffix; the rename makes the path/sentinel separation explicit in the type.
|
|
720
|
+
- **`getBackupDir` JSDoc** now discloses that backups cover config files only, not tool files in `~/.claude/tools/agent-metrics/`.
|
|
721
|
+
|
|
722
|
+
### Tracker
|
|
723
|
+
|
|
724
|
+
- Closes 11 of 12 Confucius-pair findings on this package. The remaining one (metrics-terminology overspecialization) is deferred — speculative rename pending the SubagentStop hook actually gaining non-metric responsibilities.
|
|
725
|
+
|
|
726
|
+
## [0.4.1] - 2026-05-29
|
|
727
|
+
|
|
728
|
+
### Fixed
|
|
729
|
+
|
|
730
|
+
- **agent-metrics dependency stuck at `^0.2.0`** — bumped to `^0.4.0` so `npx @uluops/setup` installs the v0.4.0 hook (slug-drop fix + explicit-tag-only detection). Previously, the caret range resolved to `>=0.2.0 <0.3.0`, silently excluding both v0.3.x and v0.4.x. Setup users were receiving a hook two minor versions behind npm. Closes the declarative form of the install.sh "stuck at v0.1.0" trap surfaced by Confucius analyst/forecaster runs on this package.
|
|
731
|
+
|
|
732
|
+
## [0.4.0] - 2026-05-04
|
|
733
|
+
|
|
734
|
+
### Added
|
|
735
|
+
|
|
736
|
+
- **Gemini CLI command support**: Commands, workflows, and pipelines now install as `.toml` files for Gemini CLI via transform-at-install (no per-harness asset duplication)
|
|
737
|
+
- **Pipelines namespace**: New `pipelines/` subdirectory for pipeline commands (ship, aristotle)
|
|
738
|
+
- **Agent transform-at-install**: Single source of truth for agent assets — frontmatter is transformed per harness at install time (Claude Code passthrough, Gemini CLI tool name mapping + envelope, OpenCode permission mapping)
|
|
739
|
+
- `anxiety-reader` agent added to starter pack (required by ship pipeline)
|
|
740
|
+
|
|
741
|
+
### Changed
|
|
742
|
+
|
|
743
|
+
- Agent assets flattened from `assets/agents/{harness}/` to `assets/agents/` (single source, -19K lines)
|
|
744
|
+
- `ship` pipeline moved from `workflows/` to `pipelines/` (correctly classified as PDL)
|
|
745
|
+
- `aristotle` pipeline moved from `workflows/` to `pipelines/` and regenerated from PDL source
|
|
746
|
+
- Pipeline assets regenerated from actual PDL sources (were incorrectly WDL-rendered)
|
|
747
|
+
- Commands install expanded to 3 subdirs: `agents/`, `workflows/`, `pipelines/`
|
|
748
|
+
- Starter pack: 23 agents, 23 agent commands, 3 workflows, 2 pipelines
|
|
749
|
+
|
|
750
|
+
### Fixed
|
|
751
|
+
|
|
752
|
+
- 30 validation issues resolved across 4 commits (type safety, test coverage, dead code, security)
|
|
753
|
+
- Manifest contentHash self-referential bug fixed
|
|
754
|
+
- `readCredentialsFile` now throws on malformed JSON instead of swallowing
|
|
755
|
+
- Dev dependency vulnerabilities resolved (picomatch, postcss, vite)
|
|
756
|
+
- Shell profile fence marker ordering guard added
|
|
757
|
+
- MCP config backups now timestamped to prevent overwrites
|
|
758
|
+
- Strict unused checks enabled in test tsconfig
|
|
759
|
+
|
|
760
|
+
## [0.3.0] - 2026-04-30
|
|
761
|
+
|
|
762
|
+
### Added
|
|
763
|
+
- Multi-harness architecture: OpenCode, Gemini CLI, and Codex harness profiles
|
|
764
|
+
- Slash command installation (agents + workflows) for Claude Code
|
|
765
|
+
- Agent metrics hook integration with SubagentStop event
|
|
766
|
+
- `--signup` flag for inline account creation (email + password)
|
|
767
|
+
- `--list` flag to preview available agents and workflows without installing
|
|
768
|
+
- `--verify` flag for installation health checks (manifest, files, API connectivity)
|
|
769
|
+
- `--local-defs` flag to install definitions in the project directory
|
|
770
|
+
- Harness aliases (`claude`, `oc`, `gemini`)
|
|
771
|
+
- Manifest-based installation tracking with per-harness state
|
|
772
|
+
- Atomic writes for all config file modifications
|
|
773
|
+
- Backup creation before config changes
|
|
774
|
+
- Dynamic agent/workflow catalog derived from assets at runtime
|
|
775
|
+
|
|
776
|
+
### Changed
|
|
777
|
+
- Renamed `/agents:validate` to `/agents:code-validate` for naming clarity
|
|
778
|
+
- Config files containing API keys now written with 0o600 permissions (owner-only)
|
|
779
|
+
- readConfig/readSettings now throw on malformed JSON instead of silently returning empty object
|
|
780
|
+
- Hook command paths are now quoted to handle spaces in installation paths
|
|
781
|
+
- .gitignore writes use atomic write pattern for crash safety
|
|
782
|
+
- Extracted display functions to dedicated module (cli.ts reduced from 758 to 647 lines)
|
|
783
|
+
|
|
784
|
+
### Fixed
|
|
785
|
+
- Package name misattribution in MCP availability check when fetch rejects
|
|
786
|
+
- Hardcoded TOOL_COUNT and AGENT_LIST replaced with dynamic asset scanning
|
|
787
|
+
|
|
788
|
+
## [0.2.0] - 2026-03-15
|
|
789
|
+
|
|
790
|
+
### Added
|
|
791
|
+
- Environment variable overrides for all paths
|
|
792
|
+
- Path probing and manifest validation
|
|
793
|
+
- Comprehensive test suite (140 tests across 18 files)
|
|
794
|
+
- Branded CLI banner
|
|
795
|
+
|
|
796
|
+
## [0.1.0] - 2026-03-01
|
|
797
|
+
|
|
798
|
+
### Added
|
|
799
|
+
- Initial release: zero-friction installer for Claude Code
|
|
800
|
+
- MCP server configuration (tracker + registry)
|
|
801
|
+
- Agent definition file installation
|
|
802
|
+
- API key resolution (flag, env var, credentials file, interactive prompt)
|
|
803
|
+
- Shell profile export with `--shell` flag
|
|
804
|
+
- `--uninstall` for clean removal
|
|
805
|
+
- `--dry-run` for previewing changes
|