@uluops/setup 0.10.0 → 0.12.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +772 -0
  2. package/README.md +81 -32
  3. package/dist/cli.js +7 -1
  4. package/dist/commands/helpers.js +70 -7
  5. package/dist/commands/per-harness.d.ts +5 -0
  6. package/dist/commands/per-harness.js +5 -0
  7. package/dist/commands/setup.d.ts +7 -0
  8. package/dist/commands/setup.js +100 -35
  9. package/dist/commands/uninstall.d.ts +7 -0
  10. package/dist/commands/uninstall.js +36 -9
  11. package/dist/commands/verify.d.ts +5 -0
  12. package/dist/commands/verify.js +5 -0
  13. package/dist/harnesses/claude-code.js +15 -7
  14. package/dist/harnesses/codex.js +21 -4
  15. package/dist/harnesses/gemini-cli.js +13 -6
  16. package/dist/harnesses/index.d.ts +8 -0
  17. package/dist/harnesses/index.js +10 -0
  18. package/dist/harnesses/opencode.js +25 -7
  19. package/dist/lib/asset-catalog.js +15 -2
  20. package/dist/lib/atomic-write.d.ts +6 -0
  21. package/dist/lib/atomic-write.js +10 -0
  22. package/dist/lib/config-merger.js +27 -8
  23. package/dist/lib/display.d.ts +8 -0
  24. package/dist/lib/display.js +27 -1
  25. package/dist/lib/file-ops.d.ts +13 -4
  26. package/dist/lib/file-ops.js +69 -18
  27. package/dist/lib/install-lock.js +45 -13
  28. package/dist/lib/json-guards.d.ts +15 -0
  29. package/dist/lib/json-guards.js +30 -0
  30. package/dist/lib/manifest.d.ts +9 -2
  31. package/dist/lib/manifest.js +66 -8
  32. package/dist/lib/mcp-packages.d.ts +17 -15
  33. package/dist/lib/mcp-packages.js +15 -13
  34. package/dist/lib/settings-merger.js +53 -9
  35. package/dist/lib/version.js +19 -2
  36. package/dist/lib/write-coordinator.d.ts +50 -0
  37. package/dist/lib/write-coordinator.js +89 -0
  38. package/dist/steps/agent-metrics-cli.d.ts +6 -0
  39. package/dist/steps/agent-metrics-cli.js +19 -1
  40. package/dist/steps/agents.js +22 -4
  41. package/dist/steps/auth.js +53 -13
  42. package/dist/steps/cli.js +14 -1
  43. package/dist/steps/commands.js +28 -10
  44. package/dist/steps/mcp.js +18 -9
  45. package/dist/steps/metrics.js +77 -7
  46. package/dist/steps/shell.d.ts +4 -1
  47. package/dist/steps/shell.js +44 -6
  48. package/dist/steps/signup.d.ts +4 -0
  49. package/dist/steps/signup.js +18 -2
  50. package/dist/steps/skills.d.ts +11 -0
  51. package/dist/steps/skills.js +35 -6
  52. package/dist/steps/username.js +10 -2
  53. package/dist/steps/verify.js +55 -6
  54. package/package.json +8 -5
  55. package/dist/lib/agent-transform.d.ts +0 -12
  56. package/dist/lib/agent-transform.js +0 -129
package/CHANGELOG.md ADDED
@@ -0,0 +1,772 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@uluops/setup` will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.12.0] - 2026-08-21
8
+
9
+ ### Added
10
+
11
+ - **Per-file write coordinator** (`src/lib/write-coordinator.ts`). Every
12
+ config read-merge-write cycle (MCP step, hook install/remove, both JSON
13
+ profiles) is now serialized per resolved path, closing the Gemini CLI
14
+ same-file pair (`~/.gemini/settings.json` holds both the MCP config and
15
+ the hook) against interleaved cycles — including under any future
16
+ concurrent step orchestration. Every `atomicWrite` additionally attests a
17
+ content hash of what this process wrote (`fileMatchesLastWrite`), so any
18
+ future rollback mechanism can refuse to clobber content it didn't write.
19
+ Deliberately NOT single-write coalescing: the hook entry has a hard data
20
+ dependency on the metrics tool files landing first (a hook pointing at a
21
+ missing hook.js fires a failing command in the user's harness), so
22
+ coalescing would couple MCP-config success to the metrics step.
23
+ - **Metrics-step privacy disclosure.** The install output now states, at the
24
+ point of hook installation, that the hook captures agent token/duration
25
+ metadata to a local buffer and sends nothing itself, with the `--no-metrics`
26
+ opt-out and the privacy-policy URL. A README "Data & privacy" section
27
+ grounds the full picture in the policy's actual terms (local buffer; data
28
+ leaves only on explicit tracker saves; indefinite retention by design;
29
+ org-policy note for shared installs).
30
+
31
+ - **`--verify` checks MCP package resolvability on npm.** The install-time
32
+ probe is non-blocking by design; the harness runs `npx -y <spec>` at
33
+ startup, so an unresolvable package fails long after setup succeeded.
34
+ `--verify` now re-asks the question on demand (a registry outage is
35
+ reported but does not double-fail a run the connectivity checks already
36
+ failed).
37
+
38
+ ### Changed
39
+
40
+ - **MCP server pins bumped to the current release contract:**
41
+ `@uluops/ops-mcp` 0.11.0 → **0.13.0** (the update-run merge-mode + echo
42
+ release), `@uluops/registry-mcp` 0.3.5 → **0.3.7**. A fresh install now
43
+ wires the servers this release was validated against.
44
+ - **The npm availability probe now checks the PINNED VERSIONS, not the bare
45
+ package names** — reversing the earlier deliberate choice. The harness
46
+ runs `npx -y <pinned spec>`, so an unresolvable pin is exactly the
47
+ condition that must fail loudly at install time instead of hours later as
48
+ an opaque npx error at first MCP launch. "A pin is not a publish"; the
49
+ probe now enforces it. `--verify`'s resolvability check inherits the same
50
+ version precision.
51
+ - **Auto-detection now names its exclusions.** When an experimental
52
+ harness's home directory is present, detection prints a dimmed
53
+ `Detected <Name> (experimental) — excluded from auto-detection; opt in
54
+ with --harness <name>` line instead of silently omitting it (the policy —
55
+ detected = safe to install — is unchanged and now visible).
56
+ - **Conflict check distinguishes "fresh install" from "broken bundle".**
57
+ A missing destination dir still skips silently (expected on first
58
+ install); the *bundled assets* being unreadable now warns loudly before
59
+ skipping — that condition means the package is broken, not that the
60
+ machine is fresh.
61
+ - **Dependency refresh to latest minors/patches (exact pins kept):**
62
+ `@inquirer/prompts` 8.5.2 → 8.6.0, `tsx` 4.22.4 → 4.23.12, `vitest`
63
+ 4.1.9 → 4.1.11. The three majors available at review time were deliberately
64
+ held: `chalk` 6 requires Node >= 22 (this package supports >= 20),
65
+ `typescript` 7 is the native-compiler migration and gets its own pass, and
66
+ `@types/node` 26 describes APIs outside the supported Node floor.
67
+ - `CHANGELOG.md` now ships in the npm tarball (added to `files`), and the
68
+ build stamps the executable bit on `dist/cli.js` directly (`postbuild
69
+ chmod +x`) instead of relying on npm's bin-link chmod at install time.
70
+
71
+ ### Fixed
72
+
73
+ - **Fish users no longer get bash syntax written into `config.fish`.**
74
+ `--shell` now writes `set -gx ULUOPS_API_KEY …` for fish (the `export`
75
+ form printed a parse error on every new fish shell while never setting
76
+ the variable — visible breakage plus silent auth failure), and the
77
+ profile's parent directory is created first (a fresh fish user may have
78
+ no `~/.config/fish/` yet).
79
+ - **README honesty pass (anxiety-read findings):** the `HTTPS_PROXY`
80
+ troubleshooting remedy was inert (Node's fetch ignores proxy env vars) —
81
+ replaced with the working `--skip-validation` path; "safe and idempotent"
82
+ / "never touched" absolutes replaced with the two known edges the repo
83
+ itself documents (ownership-marker hook replacement on re-run, and the
84
+ `--local-defs` scope-flip leaving the prior tree untracked).
85
+ - **Round-7: unknown is never observed.** When `hook.js` is absent and the
86
+ settings file cannot be read, the metrics step now returns
87
+ `skippedReason: "hook-state-unknown"` (with a named warning) instead of an
88
+ observed `false` — the manifest keeps its prior hook record and uninstall
89
+ keeps removing the hook. The hookless short-circuit gained the same
90
+ `skippedReason` for shape parity; `defsScope` is validated at manifest
91
+ load (the inheritance gate branches on it); a summary-render failure can
92
+ no longer report a completed install as exit-1 (render is advisory,
93
+ `classifyExit` is the authority) and the catalog's per-file read names
94
+ unreadable bundled files instead of throwing; the skill-dir prune skips
95
+ top-level assets.
96
+ - **Round-6 gate corrections (the falsified-state class, final ring).**
97
+ The defs-scope inheritance gate no longer covers the scope-INDEPENDENT
98
+ hook fields — a `--local-defs` re-run of a global install can no longer
99
+ record `hooksInstalled: false` over a live hook (uninstall/verify branch
100
+ on that field); a scope flip now warns naming the old, now-untracked
101
+ defsPath. An operational conflict-check failure (unreadable destination,
102
+ non-TTY refusal) is classified per-harness and the run continues —
103
+ previously it escaped the loop, leaving installed sibling harnesses with
104
+ no manifest record at all. `skills` entries are element-typed like
105
+ agents/commands; `hookConfigured` consults the settings file when
106
+ hook.js is absent instead of recording false from disk-existence alone;
107
+ the metrics package.json copy and skill-dir prune failures are named.
108
+ - **Failed copies are no longer deleted as "stale", and skipped steps no
109
+ longer falsify the record** (fifth audit round — the falsified-state
110
+ class one ring further out). Stale reconciliation now compares against
111
+ what the package SHIPS, not what copied successfully this run — an
112
+ ENOSPC re-run previously deleted the entire previously-working installed
113
+ set and reported it as routine cleanup; failed-but-previously-installed
114
+ files stay in the manifest record so uninstall can still remove their
115
+ surviving prior copies. `--no-metrics` (and unsupported harnesses) no
116
+ longer downgrade `hooksInstalled` to false — a step that never observed
117
+ the hook state cannot change its record, so uninstall keeps removing the
118
+ hook it previously installed. Prior file lists are inherited only within
119
+ the same defs scope (a `--local-defs` flip no longer points uninstall at
120
+ the wrong tree); a present manifest with an unrecognized shape refuses
121
+ loudly instead of reading as absent (behavior change: was silently
122
+ treated as no-manifest); manifest agents/commands entries are
123
+ element-typed; the non-TTY unknown-conflicts refusal exits 1 as an
124
+ operational failure (not a user-decline exit 0); the tool-file removal
125
+ catch names its error; the unidentifiable-lock message no longer invents
126
+ "PID -1".
127
+ - **The read-error-means-absent inference is now eliminated at every
128
+ read-then-act site, not only the overwrite-shaped ones.** Third audit
129
+ round: the conflict-overwrite guard treated an unreadable destination as
130
+ "no conflicts" and destroyed a user's own agent file with no prompt (now:
131
+ conflicts unknown → explicit confirm, default No); the bundled-asset
132
+ readers returned empty lists on read errors that the manifest then
133
+ recorded as authoritative, orphaning previously-installed files (now:
134
+ only ENOENT means "ships none"; anything else fails the step, records
135
+ partial state, and — fourth round — the manifest entry PRESERVES the
136
+ prior file lists for every step that produced no result, so the partial
137
+ record can never itself become the orphaning vector); an unreadable lock `meta.json` was classified stale and a
138
+ LIVE lock stolen (now: unverifiable = held, never reclaimed), and the
139
+ mkdir→meta window got a grace-recheck before stale-claiming; the metrics
140
+ tool copy verifies its source is readable before wiping the installed
141
+ tree.
142
+ - **Uninstall reports the truth.** `removeShellExport` and `deleteManifest`
143
+ return results their callers consult: an unremovable shell export warns
144
+ that the plaintext key survives (previously "✓ Removed export" over an
145
+ untouched file), an undeletable manifest warns instead of "✓ Manifest
146
+ deleted", and per-file unlink failures during uninstall are named instead
147
+ of silently excluded from a truthful-looking count. Returning-user
148
+ detection (`hasCredentialsFile`) now counts unreadable-but-present as
149
+ present, so a permissions hiccup no longer steers into a duplicate
150
+ signup.
151
+ - **The install manifest can no longer be silently replaced or misread as
152
+ absent.** `readManifestFile` collapsed every read error AND malformed
153
+ JSON into "no manifest" — after which a save would overwrite the file it
154
+ couldn't read, orphaning every recorded agent/command/hook, and uninstall
155
+ would report "nothing to uninstall". Unreadable-but-present now refuses
156
+ loudly; malformed JSON refuses with the recovery path named. (Behavior
157
+ change: malformed manifests previously read as absent.)
158
+ - **`writeCredentialsFile` honors its preserve promise.** The merge only
159
+ starts fresh on genuine absence now — an unreadable or unparseable
160
+ credentials file (which may hold other profiles shared with @uluops/cli)
161
+ refuses instead of being overwritten. (Behavior change: unparseable files
162
+ previously read as absent.)
163
+ - **`--uninstall` with an invalid harness filter exits 1 again** — the
164
+ previous fix's in-try `return` made the trailing exit unreachable, so the
165
+ fatal error exited 0; the path now rides `process.exitCode`.
166
+ - **An unreadable-but-present config can no longer be silently replaced.**
167
+ Every read-then-overwrite path (Claude config, harness settings, Codex
168
+ TOML, OpenCode JSONC, shell profile) treated ANY read error as "file
169
+ absent" and proceeded to write a fresh file over it — an EACCES on a
170
+ root-owned `~/.claude.json` or `~/.zshrc` would have destroyed the user's
171
+ content with a green checkmark. All five sites now discriminate via a
172
+ shared `isEnoent` predicate: only a genuinely missing file reads as
173
+ fresh; anything else refuses loudly with nothing modified. (This class
174
+ was fixed once before at the gitignore path — the predicate exists so it
175
+ cannot recur site-by-site.)
176
+ - **Malformed OpenCode JSONC is refused instead of silently truncated.**
177
+ `jsonc-parser`'s `parse()` is error-recovering and never throws, so the
178
+ previous guard was unreachable: everything after a syntax error was
179
+ dropped, merged, and written back. Parse errors are now collected via
180
+ the errors out-param and refuse the file by name.
181
+ - **`--uninstall` no longer leaks the install lock on an invalid harness
182
+ filter** — same exit-inside-try defect fixed for `runSetup` earlier,
183
+ now fixed as the class: the exit is recorded and fired after the
184
+ `finally` releases the lock.
185
+ - **npm failures diagnose themselves**: a spawn failure (npm not on PATH)
186
+ now reports the real cause instead of `exit null`, with the timeout
187
+ diagnosis taking precedence when both signals are present.
188
+ - **Slow-network timeouts get the friendly message**: `AbortSignal.timeout`
189
+ rejections (DOMException `TimeoutError`) are now classified alongside
190
+ network `TypeError`s in auth, signup, and username flows — previously the
191
+ exact case the "check your connection / --skip-validation" messages were
192
+ written for never triggered them. A 200 with a non-JSON body (captive
193
+ portal) is also handled in signup/username, matching auth.
194
+ - **Codex TOML removal no longer drops a user's block after an unparseable
195
+ header** — array-of-tables (`[[x]]`) and quoted-`]` headers now end the
196
+ skip region instead of leaving it sticky.
197
+ - **`stripDangerousKeys` strips `__proto__` only** — own-property
198
+ `constructor`/`prototype` keys assigned by `Object.assign` are inert data
199
+ properties, and stripping them silently ate legitimate user keys
200
+ (JSON-schema fragments) on the round-trip.
201
+ - **Install-lock release deregisters the dir only after removal completes**,
202
+ closing a signal-window leak; the coordinator's `fileMatchesLastWrite`
203
+ now answers true only on ENOENT (an unverifiable read must never
204
+ authorize a write), and its docblock states plainly that attestation has
205
+ no production consumer until a rollback mechanism exists.
206
+ - **A verify API-key decode failure no longer suppresses the npm
207
+ resolvability check**, and `getVersion` wraps its own JSON parse in the
208
+ deliberate broken-publish error.
209
+ - **Hook ownership is now decided by one predicate across merge/remove/has.**
210
+ The merge tolerated malformed matcher entries while `removeUluopsHook` and
211
+ `hasUluopsHook` dereferenced them unguarded — the same hand-edited
212
+ settings file merged fine but crashed `--uninstall` and `--verify`. All
213
+ three now share `isUluopsMatcher` (anything not positively ours is user
214
+ data: preserved by remove, invisible to has, never a crash), the two
215
+ crash-reachable callers (`uninstallMetrics`, verify's `checkHooks`) are
216
+ wrapped to degrade to a warning/failed check, and the OpenCode config
217
+ reader gained the same top-level shape gate as its siblings.
218
+ - **Agent/command/skill file copies are atomic** (`copyIfChanged`/
219
+ `writeIfChanged` now write via temp+rename) — a crash mid-copy can no
220
+ longer leave a torn definition file for the harness to load.
221
+ - **Parsed configs are stripped of `__proto__`/`constructor`/`prototype`
222
+ own-keys at the read boundary.** Our own merges are spread-based and
223
+ were never pollutable, but a hostile key read from disk would have been
224
+ written back for assign-semantics consumers to trip on. Break-test
225
+ proves an `Object.assign` over the stripped parse cannot pollute.
226
+ - **Pre-existing invalid JSON is named as pre-existing.** Both mergers'
227
+ parse errors now state the file failed to parse *before* any UluOps
228
+ change was made — previously indistinguishable from installer-caused
229
+ corruption.
230
+ - **`npm install -g` EACCES failures explain themselves** (both the CLI and
231
+ agent-metrics installers): the error now names the unwritable-prefix
232
+ cause and points at version managers / the npm permissions doc.
233
+ - **Health-check failures name the endpoint** (Tracker vs Registry) instead
234
+ of "some APIs unreachable".
235
+ - **`getVersion` fails loudly on a malformed package.json** instead of
236
+ stamping `undefined` into banners and the manifest.
237
+ - **Credentials reads only ever return a string key** — a malformed
238
+ `credentials.json` (numeric/object apiKey) reads as "no stored key"
239
+ rather than flowing a non-string into Bearer headers.
240
+ - **Manifest save clones instead of aliasing the loaded manifest**, and the
241
+ gitignore-update warning routes through the standard display helper.
242
+
243
+ - **`process.exit` no longer fires inside `runSetup`'s try block.** The
244
+ non-zero exit-code path skipped the `finally` that releases the install
245
+ lock (the signal handlers were the only cleanup actually running).
246
+ `classifyExit` still runs inside; the exit happens after the lock release.
247
+ - **Settings/config reads now reject unmergeable shapes instead of crashing
248
+ or corrupting.** Valid-JSON-wrong-shape user files (top-level array or
249
+ string; `hooks` as a string — which the merge would have spread into
250
+ per-character keys and written back; a hooks entry that is not an array)
251
+ now throw the same friendly named-path error as invalid JSON. The hook
252
+ merge additionally preserves matcher entries it cannot parse instead of
253
+ TypeErroring on them. Applies to both `settings-merger` and
254
+ `config-merger` reads.
255
+ - **Install-lock `meta.json` is written mode 0600** — it carries the owning
256
+ PID/hostname and was world-readable.
257
+ - **Non-TTY invocation without `-y` no longer dies on a raw inquirer
258
+ cancellation.** The API-key prompt's `interactive` gate now checks
259
+ `process.stdin.isTTY` (mirroring the existing guard on the account prompt),
260
+ so a piped/CI run with no key falls through to the actionable error —
261
+ `No API key found. Pass --api-key or set ULUOPS_API_KEY…` — instead of
262
+ `User force closed the prompt`. Found by live dx validation
263
+ (consumer-validate run #36).
264
+ - **README caught up to the shipped CLI.** The `--username` flag and its
265
+ registry-username step (live since 0.9.9) are now in the Options table,
266
+ the installer step list, and the Examples; the `--list`/`--verify` sample
267
+ outputs were regenerated from v0.11.0 (the old captures showed v0.9.5 and
268
+ pre-rename agent slugs like `code-validator` for what is now `validate`);
269
+ the Node >= 20 requirement is stated at the quick-start instead of only in
270
+ the bottom Requirements section; a contents line was added and all code
271
+ fences carry language tags.
272
+
273
+ ### Known gap (deferred)
274
+
275
+ - **`process.exit` immediately after console output can truncate piped
276
+ stdout** (`npx @uluops/setup | tee` may lose the tail of the summary).
277
+ Converting the exit paths to `process.exitCode` requires an open-handle
278
+ audit first — a lingering inquirer/stdin handle would turn a truncated
279
+ log into a hung process, which is the worse failure. Tracked for its own
280
+ pass; uninstall's filter-error path already rides `process.exitCode`
281
+ (safe there: no prompt has run).
282
+
283
+ ### Security
284
+
285
+ - **Uninstall path containment (CWE-22).** Manifest-supplied file names are
286
+ now resolved and verified to stay inside the managed directory before any
287
+ `unlink` — a hand-edited or foreign-written manifest entry containing
288
+ `../` can no longer turn uninstall into an arbitrary-delete primitive
289
+ (same-UID confused-deputy amplifier; security-analyst ship-gate finding).
290
+ Escape attempts are refused by name; break-tested with traversal and
291
+ absolute entries, outside files surviving.
292
+
293
+ ## [0.11.0] - 2026-07-18
294
+
295
+ ### Changed
296
+
297
+ - **Bumped the pinned MCP server versions to the current release contract:**
298
+ `@uluops/ops-mcp` 0.9.1 → 0.11.0, `@uluops/registry-mcp` 0.2.18 → 0.3.5.
299
+ These are the specs stamped into every harness config (`npx -y <spec>`), so a
300
+ fresh setup install now resolves the current MCP servers — including the
301
+ registry MCP's list-grain risk scalars + `analyzerStale` verdict-currency
302
+ passthrough (registry-sdk 0.45.0) and the mcp-secure-server
303
+ 0.0.19-security hardening — instead of a six-week-old pin. Single source of
304
+ truth: `src/lib/mcp-packages.ts`.
305
+ - **Bumped `@uluops/agent-metrics` 0.4.0 → 0.8.0** (exact pin): the installed
306
+ metrics hook now carries run-scoped token attribution (`[run:]` tag →
307
+ `run_id` on buffer entries → `--run` queries), symlink/TOCTOU hardening, and
308
+ the CODEX guards. The hook a fresh install wires is the one current
309
+ pipelines (pdl-executor Phase 4b `--run` collection) are written against.
310
+
311
+ ## [0.10.0] - 2026-07-06
312
+
313
+ ### Changed
314
+
315
+ - **Bumped the pinned MCP server versions to the current release contract:**
316
+ `@uluops/ops-mcp` `0.5.0` → `0.9.1`, `@uluops/registry-mcp` `0.2.14` → `0.2.18`.
317
+ These are the specs stamped into every harness config (`npx -y <spec>`), so a
318
+ fresh setup install now resolves the tracker/registry MCP servers users are
319
+ actually tested against — including the dataset-export-era tracker tooling — instead
320
+ of a months-old pin. Single source of truth: `src/lib/mcp-packages.ts`.
321
+ - **Codex agent assets regenerated to `gpt-5.5`** (all 23 `assets/codex/agents/*.toml`
322
+ bumped `gpt-5.3` → `gpt-5.5`, with refreshed scoring-calibration examples).
323
+
324
+ ## [0.9.9] - 2026-06-17
325
+
326
+ ### Added
327
+
328
+ - **Optional `--username` step.** Offers to set a registry username during setup —
329
+ the one-time prerequisite for creating/publishing definitions — via native
330
+ fetch `PATCH /auth/profile` with the resolved api key (no SDK dependency).
331
+ Allow, never force: `--username <slug>` sets it non-interactively; an
332
+ interactive run prompts once (Enter skips); non-interactive runs with no flag
333
+ skip silently. Failures warn and never abort setup.
334
+
335
+ ### Changed
336
+
337
+ - **Bumped MCP pins** in `src/lib/mcp-packages.ts`:
338
+ - `OPS_MCP_VERSION` 0.4.7 → **0.5.0** — adds the `update_profile` tool (set/confirm registry username from an MCP client).
339
+ - `REGISTRY_MCP_VERSION` 0.2.13 → **0.2.14** — raises the per-string cap so full definition YAML passes through direct MCP fields.
340
+
341
+ Fresh `npx -y @uluops/setup` installs and harness reattestations after 0.9.9
342
+ stamp these specs into Claude Code / Codex / Gemini / OpenCode configs,
343
+ replacing the prior 0.4.7 / 0.2.13 pins.
344
+
345
+ ## [0.9.8] - 2026-06-17
346
+
347
+ ### Changed
348
+
349
+ - **Bumped MCP pins to current** in `src/lib/mcp-packages.ts`:
350
+ - `OPS_MCP_VERSION` 0.4.4 → **0.4.7**
351
+ - `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.
352
+
353
+ 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.
354
+
355
+ `@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.
356
+
357
+ - **Dependency + toolchain bumps to current.** Two were breaking majors requiring code/config changes:
358
+ - `@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.
359
+ - `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`.
360
+ - `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**.
361
+
362
+ 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).
363
+
364
+ ## [0.9.7] - 2026-06-08
365
+
366
+ ### Changed
367
+
368
+ - **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.
369
+
370
+ 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.
371
+
372
+ `REGISTRY_MCP_VERSION` remains 0.2.9 — no changes in this release.
373
+
374
+ ## [0.9.6] - 2026-06-08
375
+
376
+ ### Changed
377
+
378
+ - **Bumped MCP pins to pick up the live-tests T2 wave.** `src/lib/mcp-packages.ts`:
379
+ - `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.
380
+ - `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.
381
+
382
+ 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.
383
+
384
+ `@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.
385
+
386
+ Suite 360/360 pass on the bumped pins.
387
+
388
+ ## [0.9.5] - 2026-06-08
389
+
390
+ ### Added
391
+
392
+ - **`--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.
393
+
394
+ ### Fixed
395
+
396
+ - **`--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.
397
+
398
+ ### Changed
399
+
400
+ - **`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.
401
+ - **`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.
402
+
403
+ ### Tests
404
+
405
+ - **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.
406
+ - **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.
407
+
408
+ Test suite: 348 → **360 passing**.
409
+
410
+ ## [0.9.4] - 2026-06-07
411
+
412
+ ### Fixed
413
+
414
+ - **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.
415
+ - **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.
416
+
417
+ ## [0.9.3] - 2026-06-07
418
+
419
+ ### Fixed
420
+
421
+ - **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.
422
+
423
+ ## [0.9.2] - 2026-06-07
424
+
425
+ ### Fixed
426
+
427
+ - **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.
428
+
429
+ ### Known gap (deferred)
430
+
431
+ - **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.
432
+
433
+ ## [0.9.1] - 2026-06-07
434
+
435
+ ### Changed
436
+
437
+ - **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.
438
+ - **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.
439
+
440
+ ## [0.9.0] - 2026-06-07
441
+
442
+ ### Added
443
+
444
+ - **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.
445
+ - **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.
446
+
447
+ ### Changed
448
+
449
+ - **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.
450
+ - **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.
451
+
452
+ ### Known issues (deferred from 0.8.1)
453
+
454
+ - **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.
455
+
456
+ ## [0.8.1] - 2026-06-07
457
+
458
+ ### Added
459
+
460
+ - **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).
461
+
462
+ ### Security
463
+
464
+ - **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.
465
+ - **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.
466
+ - **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.
467
+ - **`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.
468
+
469
+ ### Removed
470
+
471
+ - **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.
472
+
473
+ ### Known issues (deferred)
474
+
475
+ - **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.
476
+
477
+ ## [0.8.0] - 2026-06-07
478
+
479
+ ### Added
480
+
481
+ - **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:
482
+ - `--harness all` and `--all-detected` install into every detected stable harness in one run.
483
+ - `--harness claude-code,codex` installs into a specific comma-separated subset.
484
+ - 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.
485
+ - 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`.
486
+ - `--harness <single-name> --all-detected` is a conflicting-flags error that fails fast with no state touched.
487
+ - `--harness all` with zero detected falls back to the default (`claude-code`) so the landing-page "just run npx @uluops/setup" promise is preserved.
488
+ - **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.
489
+ - **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.
490
+ - **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).
491
+ - **`--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.
492
+ - **`--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".
493
+ - **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`.
494
+
495
+ ### Fixed
496
+
497
+ - **`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).
498
+ - **`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.
499
+
500
+ ### Internal
501
+
502
+ - **New module structure** for the multi-target orchestration:
503
+ - `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).
504
+ - `src/commands/errors.ts` — typed `ConflictRejectedError` (replaces `process.exit(0)` in `checkConflicts` so the per-harness loop can catch and continue).
505
+ - `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`).
506
+ - `src/commands/uninstall-filter.ts` — pure filter parser + validator mirroring the install-side syntax.
507
+ - **`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.
508
+ - **`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.
509
+ - **Suite: 240 → 340 tests (+100):**
510
+ - `src/test/select-harnesses.test.ts` (26) — every row of the §5 behavior matrix.
511
+ - `src/test/per-harness.test.ts` (10) — every row of the §7.5 4-tier exit-code table.
512
+ - `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.
513
+ - `src/test/uninstall-filter.test.ts` (16) — CLI matrix + conflict detection + unknown-harness validation + edge cases.
514
+ - `src/test/verify.test.ts` (+2) — partial-install warning emitted; absent partial field does NOT emit the warning row.
515
+ - `src/test/agents.test.ts` (+1 assertion) — failed file not in `installedFiles`.
516
+ - **Docker test substrate: 12 → 16 scenarios:**
517
+ - `multi-all-detected` — 3 detected harnesses install in one invocation; manifest aggregates all three.
518
+ - `multi-explicit-subset` — `--harness claude-code,codex` honors explicit list when 4 harnesses detected; user-typed order preserved; no cross-harness contamination.
519
+ - `multi-flag-conflict` — `--harness codex --all-detected` exits non-zero, no state touched.
520
+ - `multi-non-interactive-default` — CI compatibility: `--yes` + multi-detect preserves first-detected + dimmed notice.
521
+ - `multi-harness-all-zero-detected` — `--harness all` with no detection falls back to claude-code.
522
+ - `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.
523
+ - `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.
524
+ - `multi-uninstall-subset` — installs 3 harnesses, `--uninstall --harness opencode`, asserts opencode removed + others preserved + manifest updated (not deleted) + globals-preservation notice.
525
+ - `multi-uninstall-unknown-harness` — install claude-code, `--uninstall --harness opencode`, asserts non-zero exit + error names unknown harness + lists manifest contents + state untouched.
526
+
527
+ ### Breaking changes
528
+
529
+ - `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]`.
530
+
531
+ ### Spec / process
532
+
533
+ This release ships against a specification authored and reviewed via the pre-implementation pipeline:
534
+ - **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).
535
+ - **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`.
536
+
537
+ ## [0.7.1] - 2026-06-05
538
+
539
+ ### Fixed
540
+
541
+ - **`@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.
542
+ - **`--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.
543
+ - **`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.
544
+ - **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.
545
+ - **`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.
546
+ - **`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.
547
+ - **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.
548
+ - **`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`.
549
+
550
+ ### Internal
551
+
552
+ - 17 new regression tests across the affected modules:
553
+ - 5 for the `agent-metrics` detect fix (covering present/absent/unrelated-deps/missing-version/unparseable-stdout shapes of `npm ls -g --json`).
554
+ - `src/test/config-merger.test.ts` — `checkMcpPackageAvailability` rejection-reason annotation + bare-package-name on registry miss (2 tests, mocked `fetch`).
555
+ - `src/test/manifest.test.ts` — empty-harnesses rejection + legacy-only validate no-false-warning + hash-mismatch tamper detection (3 tests).
556
+ - `src/test/cli.test.ts` — `summarizeSpawnResult` SIGTERM-timeout recognition + stderr-on-non-zero + clean-exit ok-path (3 tests, real subprocesses with tight timeouts).
557
+ - `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`).
558
+ - `summarizeSpawnResult` exported as `@internal` from `src/steps/cli.ts` for direct test access to the timeout branch.
559
+ - `assertOpencodeEnvironment` exported from `src/harnesses/opencode.ts` and invoked from `getProfile` in the harness registry.
560
+ - 1 install-lock test for the missing-parent-dir regression on first-time users.
561
+ - Suite: 223 → 240 tests (+17).
562
+
563
+ ## [0.7.0] - 2026-06-05
564
+
565
+ ### Added
566
+
567
+ - **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.
568
+ - **`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`).
569
+
570
+ ### Known limitations
571
+
572
+ - **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.
573
+
574
+ ### Internal
575
+
576
+ - New `src/lib/install-lock.ts` (~220 lines) with `acquireInstallLock`, `LockHandle.release()`, `InstallLockHeldError`, signal-handler registration, and a test seam for handler reset.
577
+ - New `src/lib/paths.ts:getInstallLockDir()` reusing `getUluopsDir()`.
578
+ - 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.
579
+ - 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.
580
+ - `src/cli.ts` formats `InstallLockHeldError` with a hint about stale-lock auto-recovery rather than emitting a stack trace.
581
+ - New `src/steps/agent-metrics-cli.ts` mirrors `src/steps/cli.ts` — `AgentMetricsCliExecutor` interface with `detect`/`install`/`uninstall`, `installAgentMetricsCli` + `uninstallAgentMetricsCli`, executor injection for tests.
582
+ - 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`.
583
+ - 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).
584
+ - Suite: 200 → 223 tests (+23 total for this release — 12 from install-lock + 11 from agent-metrics-cli).
585
+
586
+ ## [0.6.5] - 2026-06-05
587
+
588
+ ### Fixed
589
+
590
+ - **`.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.
591
+ - **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.
592
+
593
+ ### Internal
594
+
595
+ - 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.
596
+ - New `writeShellExport` and `removeShellExport` tests in `src/test/shell.test.ts` covering the duplicate-fence-block scenario for both install and uninstall.
597
+ - Suite now 200 cases (+12).
598
+
599
+ ## [0.6.4] - 2026-06-05
600
+
601
+ ### Fixed
602
+
603
+ - **`validateKey()` now hits the correct self-identity endpoint.** Server
604
+ validation called `GET /api/v1/registry/users/me` — the registry-api's
605
+ public user-lookup route, which Zod-validates the path param as a UUID and
606
+ returns `400 { id: ["Invalid uuid"] }` for the literal `me`. Endpoint has
607
+ been wrong since the initial `feat: implement @uluops/setup zero-friction
608
+ installer` (commit `70a01a2`); users hit it any time they ran setup with a
609
+ freshly-minted key and no `--skip-validation`. Now points at
610
+ `GET /api/v1/auth/me` (ops-uluops-api) and unwraps the
611
+ `{ data: { email, ... } }` envelope. Five regression tests added covering
612
+ URL, header, response unwrap, 401 path, 500 path, and network-failure path.
613
+
614
+ ### Changed
615
+
616
+ - **Stopped stamping backend URLs into MCP host configs.** Previously
617
+ `mergeUluopsMcp` (Claude) and the OpenCode harness wrote
618
+ `ULUOPS_BASE_URL: "https://api.uluops.ai/api/v1"` for `uluops-tracker` and
619
+ `ULUOPS_REGISTRY_URL: "https://api.uluops.ai/api/v1/registry"` for
620
+ `uluops-registry` into every generated config. Both URLs are already
621
+ resolved automatically by `@uluops/ops-mcp` / `@uluops/registry-mcp` via
622
+ their bundled SDKs (prod by default), so stamping was redundant — and
623
+ worse, would pin every user to a static URL that could go stale if our
624
+ production endpoints ever shifted. The generated `env` block now contains
625
+ only `ULUOPS_API_KEY`. Pairs with `@uluops/ops-mcp@0.2.1` which made
626
+ `ULUOPS_BASE_URL` officially optional on the consumer side.
627
+
628
+ ## [0.6.3] - 2026-06-05
629
+
630
+ ### Changed
631
+
632
+ - **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.
633
+ - 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).
634
+ - 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`.
635
+ - No harnesses detected → fall back to `claude-code` (preserves the landing-page "just works" promise for fresh installs).
636
+ - `--harness <name>` passed explicitly → always honored, detection is skipped.
637
+
638
+ ## [0.6.2] - 2026-06-05
639
+
640
+ ### Changed
641
+
642
+ - **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.
643
+ - 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.
644
+ - `--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.
645
+
646
+ ### Added
647
+
648
+ - **`hasCredentialsFile()` exported from `steps/auth.ts`** — existence-only probe for `~/.uluops/credentials.json` used by the prompt-skip gate.
649
+
650
+ ## [0.6.1] - 2026-06-05
651
+
652
+ ### Changed
653
+
654
+ - **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.
655
+ - **`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.
656
+
657
+ ## [0.6.0] - 2026-06-05
658
+
659
+ ### Added
660
+
661
+ - **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.
662
+ - **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.
663
+
664
+ ### Fixed
665
+
666
+ - **`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.
667
+
668
+ ### Changed
669
+
670
+ - **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.
671
+
672
+ ## [0.5.0] - 2026-05-29
673
+
674
+ ### Added
675
+
676
+ - **`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.
677
+ - **`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.
678
+ - **`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.
679
+ - **`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.
680
+
681
+ ### Changed
682
+
683
+ - **`@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.
684
+ - **`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.
685
+ - **`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.
686
+ - **`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.
687
+ - **`getBackupDir` JSDoc** now discloses that backups cover config files only, not tool files in `~/.claude/tools/agent-metrics/`.
688
+
689
+ ### Tracker
690
+
691
+ - 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.
692
+
693
+ ## [0.4.1] - 2026-05-29
694
+
695
+ ### Fixed
696
+
697
+ - **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.
698
+
699
+ ## [0.4.0] - 2026-05-04
700
+
701
+ ### Added
702
+
703
+ - **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)
704
+ - **Pipelines namespace**: New `pipelines/` subdirectory for pipeline commands (ship, aristotle)
705
+ - **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)
706
+ - `anxiety-reader` agent added to starter pack (required by ship pipeline)
707
+
708
+ ### Changed
709
+
710
+ - Agent assets flattened from `assets/agents/{harness}/` to `assets/agents/` (single source, -19K lines)
711
+ - `ship` pipeline moved from `workflows/` to `pipelines/` (correctly classified as PDL)
712
+ - `aristotle` pipeline moved from `workflows/` to `pipelines/` and regenerated from PDL source
713
+ - Pipeline assets regenerated from actual PDL sources (were incorrectly WDL-rendered)
714
+ - Commands install expanded to 3 subdirs: `agents/`, `workflows/`, `pipelines/`
715
+ - Starter pack: 23 agents, 23 agent commands, 3 workflows, 2 pipelines
716
+
717
+ ### Fixed
718
+
719
+ - 30 validation issues resolved across 4 commits (type safety, test coverage, dead code, security)
720
+ - Manifest contentHash self-referential bug fixed
721
+ - `readCredentialsFile` now throws on malformed JSON instead of swallowing
722
+ - Dev dependency vulnerabilities resolved (picomatch, postcss, vite)
723
+ - Shell profile fence marker ordering guard added
724
+ - MCP config backups now timestamped to prevent overwrites
725
+ - Strict unused checks enabled in test tsconfig
726
+
727
+ ## [0.3.0] - 2026-04-30
728
+
729
+ ### Added
730
+ - Multi-harness architecture: OpenCode, Gemini CLI, and Codex harness profiles
731
+ - Slash command installation (agents + workflows) for Claude Code
732
+ - Agent metrics hook integration with SubagentStop event
733
+ - `--signup` flag for inline account creation (email + password)
734
+ - `--list` flag to preview available agents and workflows without installing
735
+ - `--verify` flag for installation health checks (manifest, files, API connectivity)
736
+ - `--local-defs` flag to install definitions in the project directory
737
+ - Harness aliases (`claude`, `oc`, `gemini`)
738
+ - Manifest-based installation tracking with per-harness state
739
+ - Atomic writes for all config file modifications
740
+ - Backup creation before config changes
741
+ - Dynamic agent/workflow catalog derived from assets at runtime
742
+
743
+ ### Changed
744
+ - Renamed `/agents:validate` to `/agents:code-validate` for naming clarity
745
+ - Config files containing API keys now written with 0o600 permissions (owner-only)
746
+ - readConfig/readSettings now throw on malformed JSON instead of silently returning empty object
747
+ - Hook command paths are now quoted to handle spaces in installation paths
748
+ - .gitignore writes use atomic write pattern for crash safety
749
+ - Extracted display functions to dedicated module (cli.ts reduced from 758 to 647 lines)
750
+
751
+ ### Fixed
752
+ - Package name misattribution in MCP availability check when fetch rejects
753
+ - Hardcoded TOOL_COUNT and AGENT_LIST replaced with dynamic asset scanning
754
+
755
+ ## [0.2.0] - 2026-03-15
756
+
757
+ ### Added
758
+ - Environment variable overrides for all paths
759
+ - Path probing and manifest validation
760
+ - Comprehensive test suite (140 tests across 18 files)
761
+ - Branded CLI banner
762
+
763
+ ## [0.1.0] - 2026-03-01
764
+
765
+ ### Added
766
+ - Initial release: zero-friction installer for Claude Code
767
+ - MCP server configuration (tracker + registry)
768
+ - Agent definition file installation
769
+ - API key resolution (flag, env var, credentials file, interactive prompt)
770
+ - Shell profile export with `--shell` flag
771
+ - `--uninstall` for clean removal
772
+ - `--dry-run` for previewing changes