Uh oh!
There was an error while loading. Please reload this page.
feat(bloqs): iris bloqs update — rename a bloq - #55
Open
mayoalexander wants to merge 263 commits into
Open
Conversation
…erload Previous fix (spreading the readonly tuple before .sort()) still inferred a RelationType[] result, which doesn't structurally match a plain string[] literal under tsgo's stricter overload resolution.
…uth-gated pages Reusable "doc library → pages": each item in a bloq (e.g. the SOP library anomalyco#517) becomes its own login-gated Genesis page (banner + TextBlock content) with a clean /p/<slug>, so an index can link to individual documents instead of the whole board. - Auth-gated by DEFAULT (requires_auth=true) so internal HIPAA-client docs never land on anyone-with-link public URLs; pass --public to opt out. - --list scopes to one list; --prefix sets the slug prefix; --owner-id sets page owner. - Skips empty/stub items. Slugs are de-duplicated. --json for scripting. - Reuses the exported createPageFromJson (create + publish + cache-purge), extended to accept requires_auth so the gate is set at create (no follow-up PATCH). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
iris discover playlist --upload now computes BPM/key/Camelot/energy from each MP3 (librosa in a lazily-provisioned ~/.iris/audio-analysis venv, like yt-dlp auto-install) and sends them with the track import. Spotify audio-features is 403 for us, so we compute from the file. Analysis is optional — skipped gracefully if python3/librosa is unavailable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surfaces the bloq↔playbook attachment control in the CLI, matching the
Bloq builder's Playbooks tab in the web UI. Hits the fl-api bloq endpoints
that store attachments in bloq.config['playbooks'].
- iris playbook attach <name> --bloq <id> POST /api/v1/bloqs/{id}/attach-playbook
- iris playbook detach <name> --bloq <id> POST /api/v1/bloqs/{id}/detach-playbook
- iris playbook attached --bloq <id> GET /api/v1/bloqs/{id}/playbooks
Registered on both the `playbook` command and the hidden `skill` alias.
Reuses irisFetch/requireAuth/handleApiError; no new deps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>…162209) iris bloqs search used a raw substring .includes(), so a natural name like 'Mayo Life Atlas' never matched the stored 'MAYO — Life Atlas' (the em-dash breaks the contiguous run). Add a pure, tested matchesSearchQuery() that splits the query on whitespace and ANDs the tokens case-insensitively — fixing the match and giving word-order independence. Zero-result search now prints an explicit 'No bloqs matched "query"' hint instead of blank output; --json still returns []. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…162212)
move-item only relocated an item to a different list; there was no way
to reorder within a list or pin an item to the top. The backend already
supports PATCH .../list/item/{id}/position with {list_id, position}, so
this exposes it: 'iris bloqs reorder-item <item-id> --position N' (0 =
top) or '--top' to pin. The command resolves the item's current list via
showById, so callers only need the item id, and it never cross-moves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Stress testing surfaced a silent miss: a name stored decomposed (e.g.
'café' as e + combining accent) never matched a composed query even
though they render identically. Normalize both sides to NFC. Does NOT
accent-fold ('cafe' still won't match 'café' — that's #162213). Added
regression tests for whitespace runs, literal (non-regex) query chars,
tab/newline haystacks, NFC vs NFD, and the no-accent-fold boundary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Bloq items had no way to set a due date from the CLI. Add --due (ISO YYYY-MM-DD) to add-item and update-item, mapping to the item's due_date field (which the API persists). update-item accepts --due none to clear it. Dates are validated up front via normalizeDueDate (rejects Feb 30, month 13, free text) so we fail with a clear message instead of sending garbage. Paired with an fl-api change that now exposes due_date in the item read projections. Verified end-to-end on prod: set, update, clear. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
leads get resolves a name/email, but bloqs get and agents get were ID-only — the exact wall that forced users into the broken bloqs search. Port the leads name-or-id resolver: numeric args pass straight through, names are matched client-side with the tokenized matcher bloqs search uses (0 -> not-found exit 1, 1 -> resolve, many -> ambiguity list / prompt, never a guess). Verified live: 'bloqs get "Mayo Life Atlas"' now resolvesanomalyco#544 'MAYO — Life Atlas'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds two scaffold how-to recipes so they reach all users on install (not just locally via `iris how-to add`): - bloq-relations: link bloqs, the six relation types, filtering, graph view - deploy-elon-build-lock: recover fl-elon-web-ui from the Railway .nuxt build-lock race (wait for solo lane → one clean --from-source redeploy) Recipes are runtime data, not CLI code — no binary release needed; committing to scaffold/how-to/ is what ships them to fresh installs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Brings the Workspace ↔ Google Workspace identity sync (shipped in fl-api + Elon) to the CLI. Parity with the agents-tab Sync button over WorkspaceController: iris workspace show <bloqId> iris workspace bind <bloqId> --domain <d> --admin <email> iris workspace sync <bloqId> [--no-import] iris agents assign <id> --workspace <wsId> (0 to orphan) Closes the CLI half of the Workspace identity spine (#162453 / #162564). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Aligns the CLI push path with the backend EXPORT_PROVIDERS registry so iris bloq-sync trigger <bloq> --provider obsidian works. Folder ops (browse/link) still resolve obsidian but the backend cleanly rejects them (FOLDER_PROVIDERS). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…orkspace badge (#162671) Exposes the shipped workspace_id on the agents list: --workspace <id> and --workspace-orphaned filters, and a per-agent badge (· ws#N · google-synced, or · no workspace) so the diagram's 'Orphan, no workspace attached' state is visible without parsing --json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…62692, #162689)
Both backend paths already existed and were unexposed:
- schemas update <slug> → PATCH /atlas/schemas/{slug} (updateSchema): evolves fields,
creates a NEW version, KEEPS existing records. No more delete-and-recreate (#162692).
- records list --where (alias of --filter) + records search <query> subcommand →
the existing JSON filter[]/search on GET /atlas/datasets/{slug} (#162689 asks 1-2).
(Typesense-on-write indexing, ask 3, remains a separate gap.)
Verified live: evolved a throwaway schema v1→v2 keeping records; search executes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>…343) delete-item (and delete/bloq) deleted silently when stdin was not a TTY and --force was absent — e.g. `iris bloqs delete-item <id> </dev/null` printed "Item deleted" and exited 0. On an agent/CI-operated platform that is the highest-risk finding of the mutation audit: a destructive action bypassed its confirmation gate whenever there was no TTY to confirm at. - delete-item / delete: when stdin is non-interactive and --force is absent, abort with a nonzero exit and a clear message instead of deleting. Only proceed silently when --force is explicitly passed — mirroring add-item's existing non-interactive guard (the inconsistency the report flagged). Bundles related bloq-CLI hardening from the same beta-test-operator audit (non-interactive/--json prompt gating across create, add-item, move-item, create-list, rename, update-item) and a handleApiError cleanup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aset (#162563) Adds --dataset/-d (target Atlas Dataset slug → files become structured, cited records via the server's nano-LLM extractor) and --model (override the nano extraction model) to `iris data-sources sync`. When --dataset is set, the payload carries dataset_slug/extractor_model and the intro shows the dataset target. Bloq-list behavior unchanged when omitted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the client-side publish wire that closes the local↔R2 gap: - 'iris remotion register <files..> --board <id>' uploads local render file(s) to fl-api's /creatives endpoint (hosts to R2 server-side, creates a Pending type=content BloqItem). Token-only — no prod R2 creds, works from any machine. 1 image → image, many → carousel, video → video. - 'iris remotion auto-carousel --register --board <id>' registers the rendered deck straight into Review Studio (one command: generate → in the UI). - Shared registerCreativeFiles() helper via irisFetch FormData upload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
platform-run.ts hardcoded `process.env.COMPOSIO_API_KEY ?? "ak_c2m5…PTCn"` — a dead key. With no env var, every `iris integrations …` call silently used the dead key and 401'd (root cause of the #164644 saga). Require COMPOSIO_API_KEY and throw a clear, actionable error (pointing at dashboard.composio.dev) instead of failing silently on a stale key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…int (#165862) New top-level command: iris post "gm" --to x --profile freelabelnet iris post --video <url> --caption "…" --to x --profile freelabelnet iris post --image <url> --image <url> --to x,instagram --profile freelabelnet POSTs to fl-api /api/v1/social-media/publish (upload-post primary → Buffer fallback), so the CLI shares one bulletproof path with the Review Studio Publish button. Prints provider_used + per-platform post URLs. --profile defaults to IRIS_SOCIAL_PROFILE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#179802, fourth instance — and I had the cause wrong. I reported this as a two-store problem (writing iris-api's copy while the consumer read fl-api's). It is not. `iris pages set <slug> thumbnail_url ""` printed "Updated thumbnail_url" and wrote `json_content.thumbnail_url` — a nested key nothing reads — because thumbnail_url was not in PAGE_COLUMNS and the handler silently fell through to the json_content branch. That is the same failure #137875 already documented for requires_auth, three lines above the list it was missing from. It polluted 11 NCMA article pages with a dead key before anyone noticed, and the "fix" appeared to do nothing because it genuinely did nothing. Three changes: 1. PAGE_COLUMNS gains visibility, slug, owner_type, owner_id. `visibility` was the same latent bug waiting — `set <slug> visibility public` would have nested a dead key too. 2. A single-segment path that is neither a known column nor a known top-level json_content key (version/type/theme/layout/components/requireOtp) is now REFUSED, listing both valid sets and suggesting `json_content.<path>` if the nesting was actually intended. Guessing is what caused this; refusing is the fix. Dotted paths are untouched. 3. Column writes re-read the record and compare. This previously printed "Updated" for a page whose slug did not resolve at all — a 200 is not evidence. A mismatch stops with both values and exits non-zero; an unreadable check warns instead of implying success. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185UB6jHdKrKZ8yb1Ytot7h
The client half of #179932. `hive connect` now sends a hashed machine fingerprint (IOPlatformUUID / machine-id / MachineGuid), so the server recognises the computer and reclaims its existing row instead of minting a ghost. The server side has been live since f4152d19; until this release no client actually sends the value, so the fix has been ready and inert. Production before this work: 8 node rows for 2 live machines, two of them sharing a name. Now 2 rows, no duplicates — 20 stale rows pruned across nine users, soft-deleted with task history intact. Also ships in this window: - `iris hive vpn serve` — publish a local port to the tailnet over HTTPS instead of binding 0.0.0.0, plus a precondition check because `tailscale serve` HANGS rather than erroring when HTTPS certificates are off - a lockout fix in `iris hive vpn grant`, which emitted a single-rule policy that would have revoked the operator's access to every machine they own - `iris playbook draft` / `iris sop draft`, transcribe --remote/--brand, and several verify-the-write-landed fixes from other work in flight Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0121oCnNCeZBewSyiQSQ8bin
iris transcribe --list-treatments iris transcribe standup.m4a --treatment standup iris transcribe client-call.m4a --treatment meeting Treatments were server-side and reachable only from the CardEditor. The CLI is where recordings actually arrive, so this was the surface that mattered most and the one that had none of it. The list is FETCHED, not hardcoded — it shows a tenant's own treatments and cannot drift from what the server will accept. WHEN A TREATMENT RUNS, BOTH FILES ARE WRITTEN: the treated transcript where the reader expects it, and `<name>-transcript.raw.txt` beside it. A treatment is a model rewriting what somebody said; a rewrite you cannot compare against the original is one you cannot audit, and this path handles clinical dictation. The server already returns the original with every result — this just makes sure it survives to disk. Any failure returns the ORIGINAL rather than throwing. The words are the valuable part; a tidy-up pass is a convenience on top of them. `transcribe <url>` became `transcribe [url]` so --list-treatments can answer "what can I do with a recording" without needing one. Missing-and-not-listing gets a real message, not yargs help. Verified on a real meeting recording: decisions, action items with owners, open questions — and it correctly refused to invent an owner for the one task nobody claimed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wLxn8v7hD3rud1G17YjNk
…opted not orphaned Client half of the transition fix. A node registered before fingerprints existed has a null one stored, and a null never matches — so the first fingerprint-aware registration on every existing machine created a new row and abandoned the old. Observed on a real machine: count went 3 → 4. config.node_api_key is proof this process IS that node, so sending it lets the server adopt the row and stamp the fingerprint. One-time by construction: after that registration the machine self-identifies.
hive-local-node.ts reads `node_id` from ~/.iris/config.json as its second-most-authoritative source, and its own header note says "if anything ever writes it". Nothing ever did. So whenever the daemon was not running to answer /health, resolution fell through to the last resort — matching os.hostname() — which on macOS returns LocalHostName and is INCREMENTED by the OS on every mDNS collision. That is why `iris hive nodes list` printed "(you?)" with a question mark instead of "(you)", and it printed it precisely when the daemon was down, which is when someone is most likely to be debugging and least able to afford an ambiguous answer. The value was already in hand. The registration response returns node.id, the handler already destructured it into `nodeId`, and it was dropped on the floor two lines later. This writes it. Completes the identity work: the server now knows which machine a node is (#179932), and the machine now knows which node it is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0121oCnNCeZBewSyiQSQ8bin
…w about `iris daemon stop` killed the pid and printed success. `iris daemon restart` was `stop; sleep 1; start`. Both looked fine and neither worked reliably, because the daemon is managed by a launchd job — io.heyiris.daemon, with KeepAlive — that respawns it within a second. Killing the process is whack-a-mole against its own supervisor. The damage is not the flapping, it is what happens after a key rotation. `hive connect --force` mints a new node key, writes it to config, then calls `start` — which finds the old process still alive, prints "Daemon already running", and exits 0. The old process keeps the OLD key and 401s on every heartbeat, indefinitely, while three separate commands report success. Measured on a real machine 2026-08-12; the only symptom was "Invalid API key" buried in daemon.log. Fixes, in the order they matter: - `hive connect` RESTARTS rather than starts whenever it rotated the key. A rotation invalidates the credential the running process holds, so that process must be replaced; only a fresh install can safely `start`. - `restart` uses `launchctl kickstart -k`, which kills and relaunches in one supervised step so there is no window for `start` to find a survivor and no-op. Verified: pid 69308 → 69712, node back online in under a minute. - `stop` boots the launchd JOB out before touching pids, then verifies the process is actually gone — polling for a clean exit, escalating to SIGKILL, and exiting NON-ZERO if something survived. It used to announce a stop it had not confirmed. - `restart` no longer proceeds when `stop` fails, instead of starting a second daemon beside the first. - `start` loads the launchd job when one exists, so `stop; start` cannot silently downgrade a supervised daemon into an unsupervised nohup that disappears at the next reboot. - `stop` also finds the daemon by process name, not only by whoever holds port 3200 — a daemon that crashed before binding was reported "Not running" while running. Known and deliberately untouched: the iris-bridge ctl has the same unverified-kill shape. It is a different component and I have not traced it, so I would rather leave it visible than blind-patch it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0121oCnNCeZBewSyiQSQ8bin
…rd-wide (#179342, #179337) Two CLI halves of the sharing-safety work; the server side lands in fl-api. `iris bloqs links` now prints each link's scope — "list anomalyco#1844" / "item #179268" / "own rows only" / "WHOLE BOARD". It previously showed permission and use-count and looked complete, while omitting the one field that says whether a link hands over one list or the entire board. Auditing 20 boards for the lead-notes leak (#179373) turned up a live unredeemed link on a 32-lead board whose scope was simply unknowable — the gap blocked the audit looking for a different bug. `iris bloqs invite` now warns when it mints a board-wide link, naming the count of attached leads whose CRM notes go with it. The widest possible grant was what you got by typing the obvious command, with nothing said about it. Only UNSCOPED links warn: since #179373 a scoped member reaches neither the rest of the board nor its attached leads, so there is nothing left to caution them about. Warning on every mint would train people to ignore it, which is how the next real warning gets missed. The warning also prints after the URL, so the happy path still starts with the thing you came for, and the lead count is best-effort — a failed lookup must never take down a mint that already succeeded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HeisVSNVkwQPv3zvoJJUA
…ng prettier countAttachedLeads() fetched the whole bloq over HTTP so the board-wide invite warning could say "and its 32 leads" instead of "and any leads attached to it". An entire round trip, plus a failure path to swallow, bought one nicer sentence. The warning does the same job without it. Deletes 17 lines and one API call from the mint path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HeisVSNVkwQPv3zvoJJUA
publish/attach/sync moved playbooks author → server → the author's own
.claude/skills. Nothing brought a PUBLISHED playbook DOWN to somebody else's
machine, so an operator could install the CLI, wire MCP, open Claude Code — and
receive zero procedures. `sync` is local → local; it only rewrites what is
already on disk.
iris playbook available what you can install, scope-labelled, ✓ if present
iris playbook install <name> → .iris/playbooks/<name>/PLAYBOOK.md, then syncs
to .claude/skills/ so Claude Code sees it
Client-side only: GET /api/v1/playbooks and /{name} already scope with visibleTo(),
and 752e70d5 on fl-iris-api attached auth.platform:optional so a token actually
identifies its owner — before that the read was always anonymous and `project`
scope was unreachable through the API entirely.
install refuses to clobber a local copy without --force, and reports a published
playbook with no stored body rather than writing an empty file that fails to parse
later. --no-sync skips the .claude/skills regeneration.
Verified end to end on a clean directory: 43 visible authenticated / 0 anonymous,
install wrote both PLAYBOOK.md and SKILL.md, and `playbook list` found it runnable.
12 playbooks have since been published public; the other 31 return 404 anonymously
(404 not 403 — confirming a private playbook exists is itself a disclosure).
Note: the sync --api body upload landed independently in 004768a while this was
in progress. That version is kept; this commit's duplicate was dropped in the merge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZ585nfLps5aTSxxAsXeqA…discoverable Adding a command without reindexing leaves it out of the catalog the agent searches, so `iris playbook install` would exist and be unfindable. The pre-push hook caught it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZ585nfLps5aTSxxAsXeqA
…ding types iris brands treatments list <slug> iris brands treatments set <slug> <id> --prompt "..." [--label] [--description] iris brands treatments remove <slug> <id> The platform ships seven treatments. A client's work is not our work — a clinic's intake note and a label's session recap keep entirely different things — and until now defining one meant hand-editing brands.metadata, which is not something to ask a client to do. `set` is read-modify-write: the endpoint takes the whole map, so a naive set would silently drop every other treatment the brand had. It also READS THE WRITE BACK and fails loudly if the treatment is not there — a 200 means the request was accepted, not that the field changed, which is #179802's entire lesson. `list` on a brand with none says so and names the built-ins, rather than showing an empty list that reads as "something went wrong". Empty is a working state here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wLxn8v7hD3rud1G17YjNk
The commands listed what they do and none of the three things that actually
catch people out.
`iris how-to view agreements-and-signing` covers raising, multi-party, the BAA
verification step, gating, revoking, and the refusals — with the refusals as a
table, because a refusal reads identically to a feature that was never built and
people file bugs against them.
Routed in the README on the intents someone actually types ("NDA", "who hasn't
signed", "revoke access") and drawing an explicit boundary against
payment-gate-contracts.md: that recipe SELLS — proposal, invoice, Stripe. This
one GATES. Both are called "contracts" and they are not the same job.
Help text now carries the three facts that are not obvious from a describe
string: the signing link is a bearer credential and appears in `link --help`
where someone is about to paste it; the clause wording is placeholder pending
counsel; and there is deliberately no `sign` command, because a signature has to
be attributable to the person who gave it and an operator running a flag is not
that person.
`raise` gains examples and a note that the expiry is DERIVED from --term, since
that surprises people who pass both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118r7ZPdSYw7oymTNBoUiqFThe installer fetches scaffold/manifest.json and downloads what it lists. Adding a recipe file does nothing on its own — and nothing said so, including the manifest's own description, which claims adding a how-to is "just a PR adding the file + a line here" while the failure mode when you forget the line is silent. Two were missing. Mine, and genesis-design-standard.md — the 10-point page audit that CLAUDE.md calls mandatory reading before writing any Genesis page. It has been in the repo, indexed by `iris find`, and installed on zero machines. Found by shipping a recipe, releasing it, running the documented refresh, and checking whether the file arrived. It had not. `iris update` does not sync recipes at all — that is `install --only-docs` — and even that only fetches what the manifest lists. The pre-push guard makes the next one loud. Same shape as the capability-index check directly above it: a thing that exists, is discoverable in the repo, and reaches nobody. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118r7ZPdSYw7oymTNBoUiqF
The recipes are the agent's on-demand documentation and they were not part of an update. They arrived only from `install --only-docs`, which is mentioned in no help text anywhere. On this machine — updated continuously for months — 26 of 34 were installed and eight were stale, including the design audit CLAUDE.md calls mandatory reading before writing any Genesis page. Driven by the same scaffold/manifest.json the installer reads, so there is ONE list rather than a second copy that drifts apart from it. That mattered here: the manifest is exactly where the previous bug lived. Best-effort and silent on failure. A documentation refresh must never be the reason an upgrade reports failure, and one unreachable recipe must not abandon the rest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118r7ZPdSYw7oymTNBoUiqF
There was no way to rename a bloq from the CLI. `create` existed and `delete`
existed, so a bloq created with a name you later regretted could only be fixed in the
web UI, or by deleting and recreating it — which loses the id that every item, lead,
agent and schedule already points at.
PUT /api/v1/user/{userId}/bloqs/{bloqId}, which BloqController@update validates as
`name` only, so the command does not pretend to edit anything else. Aliased as
`rename` because that is what people will reach for.
Verified against production: bloq 592 renamed to 'heyiris.io — Signup Intent'.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ToSHC9fFDT6d88d1riP95B…ath (#179656) `iris leads merge` had a client-side fallback for when the server merge endpoint was unavailable. That fallback deleted the source lead after copying only the notes that happened to be in the already-fetched payload, and it never touched tasks at all. On 2026-08-10 it destroyed 2 notes and 5 tasks on lead anomalyco#29006, unrecoverably — while printing "✓ Merged 1 lead(s) (legacy)". A fallback that is strictly MORE destructive than the primary path must not be selected automatically and silently. A merge is atomic or it does not happen, so the fallback is gone: the command now refuses, says why, prints the `iris leads pull` commands to back the records up, exits 1, and leaves every lead intact. The preview lied in the same direction. It described the SERVER plan while the legacy path was what ran — promising "2 note(s) will be copied" when none were — and never mentioned tasks, which is why five of them could vanish without ever appearing on screen. A dry run now counts tasks from the server and probes whether the merge endpoint answers, so the preview describes the path that would really run. On the real path the merge call is its own probe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185UB6jHdKrKZ8yb1Ytot7h
…80295 My own fix didn't work, and it failed in the way it was designed not to be noticed. Two faults. `iris update` returns early when the binary is already current, so the refresh only ran when a version actually changed — but recipes are fetched from the scaffold on `main` and move independently of releases. An up-to-date binary is not evidence of up-to-date documentation, which is exactly the state that left the mandatory Genesis design audit installed on zero machines. And it swallowed every error silently, on the reasoning that a docs refresh must never fail an upgrade. That part still holds — but silent success and silent failure looked identical, so the only way to tell whether it had run was to delete a file and check. It now says why it did nothing: manifest status, "no recipe could be fetched", or the thrown message. Same lesson this whole thread keeps producing: a mechanism that covers part of the job and reports nothing is more dangerous than one that is absent, because it answers confidently. Found by deleting a recipe and running update, rather than by reading the log line I had written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118r7ZPdSYw7oymTNBoUiqF
…#180387 It defaulted to a hardcoded opportunity constant and returned HTTP 500. That is the original problem in miniature: every bounty surface is keyed by opportunity, so a hunter had to already know an id to see anything at all — and in practice that meant asking a colleague, which is the behaviour this epic exists to stop. No id now means the whole position: owed, paid to date, bug count, unsigned agreements, and the single next thing to do. An explicit id keeps the existing per-campaign view, because an operator looking at one campaign still wants one campaign. The next-step ORDERING is not repeated here — it comes from the API, which already decided that an unsigned agreement outranks unclaimed money. A second copy of that judgement in the CLI would eventually disagree with the one the gate enforces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118r7ZPdSYw7oymTNBoUiqF
The last two verbs a hunter needs, both wired to endpoints that already existed. Nothing new on the server. `connect` treats "connected but payouts not enabled" as its own state and lists what Stripe is still waiting on. It is the most confusing place to be stuck — the account exists, so the obvious move is to run onboarding again, which does nothing. And it says the thing people do not know: verified bugs already waiting pay out automatically once onboarding completes. Nobody has to come back and claim them. `claim` shows the amount BEFORE asking. "Confirm cashout?" with no number is a prompt people accept without reading, which is the wrong habit to build around money. It also warns when an agreement is still unsigned, because the gate will withhold the payout anyway and finding that out from a failed claim is a worse way to learn it. Failures pass the API's own words through instead of flattening "not available" and "failed" into one generic error — they mean different things and only one of them is the hunter's to fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118r7ZPdSYw7oymTNBoUiqF
…R-14 `iris imessage send` shelled out to osascript; `iris mail send` POSTed straight to the bridge; `iris run send_imessage` / `send_email` mapped onto the bridge directly. All three worked, and all three were invisible — nothing wrote lead_comms, so the log was only ever as fresh as the last time somebody remembered `atlas:comms ingest`. Measured in production (#178647): 27 of 28 leads with iMessage history were more than a week stale, several by ~2 months. The bridge is still the transport. The router is now the bookkeeper. comms-send.ts is the single call into POST /api/v1/comms/send, so there is one place that knows the wire format rather than three. `iris run` deliberately FALLS THROUGH to the bridge if the router is unreachable — it is the low-level escape hatch, and removing someone's ability to send because the API is down would be a worse failure than an unlogged send. Epic: bloq anomalyco#503 / list anomalyco#1922 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t lives on
`iris bloqs search` forwarded to `bloqs list --search`, so it matched board NAMES and
descriptions only. `iris bloqs search "denial risk"` returned nothing, because no board is
called that — which is almost never the question being asked.
Cross-board CONTENT search already existed server-side: GET user/{id}/bloqs/content-items
?search= matches title + content across every board you own. It was named for the Review
Studio feed that shipped first, so nothing pointed at it and nobody could find it. This wires
the existing endpoint up rather than adding a second one.
Reports both halves always, including at zero — an empty "Items (0)" means the phrase is
genuinely absent, not that the capability is missing. Same rule federated-search.ts states
for skipped sources.
Board matching stays client-side via matchesSearchQuery: the index endpoint ACCEPTS ?search=
and ignores it, returning every board, so trusting the server there would report every board
as a match.
Promoted to a top-level `iris search` because a search buried under a noun you have to
already know is a search nobody runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>…overable The behaviour changed under these commands in CR-8 — sends now go through the comms router and land in lead_comms — but the help text still described them as raw macOS/bridge wrappers. Someone reading --help had no way to know the send was recorded, which is the one fact that makes the difference between using this and reaching for osascript. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mmands The pre-push guard caught this: 1302 capabilities indexed, and the new ones (iris search, the router-backed sends) were not among them. An unindexed command is an undiscoverable one, which is the same failure this whole discoverability pass exists to fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…shell Five reconciliation verbs (invariants, audit, balance, sync-ledger, refresh-views) existed only as artisan, so answering "is the ledger sane" meant `railway ssh -s fl-api -- php artisan bounty:invariants`. A READ-ONLY check should not require the ability to run anything else. Reads the result correctly: exit 1 means the CHECK failed, not the request, so a violation prints as a failed check and sets a non-zero exit a script can gate on. Mutating verbs refuse without --confirm and are labelled WRITES in `admin list`. Also recategorises Bounty OS out of "Entity Management" into its own help group. Verified the map is actually consumed by help-renderer.ts first — a recategorisation nothing renders would have been cosmetic — and fixed the order collision it introduced (bounty and communication both at 8) so the sort stays deterministic. Descriptions for bounty/opportunities now name the subsurface, since the describe line is all a reader sees in grouped help. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…session you can work in Three things about the one command people reach for when they are in a hurry. `iris hive vpn connect` with no argument was a dead end — it demanded a name and told you to go run `vpn status`, read a name off it, and type it back. It now lists what is reachable, with online state and the remembered username, and names the exact command to run. Verified against a live tailnet: four peers, correct online state. The username is remembered per host. `--user` was accepted and thrown away, so every session began by retyping a username you had already supplied — or by typing it into the RDP prompt instead, which is the same work moved. The account is per-host and stable by design (`hive host add-user` creates a dedicated one), so the CLI is the right place to hold it. `--forget` clears it. Deliberately NOT the password: that one is one-time and force-rotated at first logon, and it should stay that way. The generated .rdp set three keys and produced a window with NO CLIPBOARD. Copying an account number out of QuickBooks is most of why anyone opens this, and it silently did not work. Now: clipboard both ways, smart sizing so a laptop scales instead of scrolls, autoreconnect so a network roam does not end the session, sound left on the host, and printer/microphone redirection explicitly OFF — nobody's local printers should appear on a client's machine because a default said so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0121oCnNCeZBewSyiQSQ8bin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
There was no way to rename a bloq from the CLI.
createexisted anddeleteexisted, so a bloq created with a name you later regretted could only be fixed in the web UI — or by deleting and recreating it, which loses the id that every item, lead, agent and schedule already points at.Hit this while creating a bloq for pricing-page lead capture and wanting a more identifiable name than the one I first gave it.
What
iris bloqs update <id> --name "...", aliased asrename.Wraps
PUT /api/v1/user/{userId}/bloqs/{bloqId}, whichBloqController@updatevalidates asnameonly — so the command does not pretend to edit anything else. Follows the existingcreatecommand's shape: prompts in a TTY,MissingFlagErrorwith exit 2 when non-interactive,--jsonoutput.Verified
Against production — bloq 592 renamed to "heyiris.io — Signup Intent" and read back:
capabilities.jsonregenerated (bun run capabilities) as the pre-push guard requires.🤖 Generated with Claude Code
https://claude.ai/code/session_01ToSHC9fFDT6d88d1riP95B