Uh oh!
There was an error while loading. Please reload this page.
fix: publish-html --requires-auth never actually gated with OTP - #56
Open
mayoalexander wants to merge 554 commits into
Open
fix: publish-html --requires-auth never actually gated with OTP#56mayoalexander wants to merge 554 commits into
mayoalexander wants to merge 554 commits into
Conversation
… work (#180527) No quality gate in this repo has produced a verdict in days. Not red — ABSENT. 17 workflows declare runs-on: blacksmith-4vcpu-ubuntu-2404 and those jobs are not being picked up: typecheck.yml 11 runs stuck "queued", oldest ~12h; the last five to reach "completed" all concluded CANCELLED — never pass, never fail test.yml every recent run cancelled at exactly 24h0m1s, the GitHub max job duration, which is the signature of never getting a runner So typecheck.yml — added specifically because typecheck never ran on released code — still never ran on released code. release.yml is the only part of CI that has been working, and the only reason is that it uses GitHub-hosted runners. Both gates move to ubuntu-latest. test.yml's TRIGGERS are deliberately left alone. The suite is 1446 pass / 214 fail, and pointing it at main would make the default branch permanently red, which is how boot-check in fl-iris-api sat broken for four days with nobody reading it. A real red on a PR is useful; a standing red on main is furniture. script/release dispatched publish.yml, which is committed as publish.yml.disabled. It failed loudly — HTTP 404, exit 1, not silently as I first described it — but the obvious entry point for cutting a release did not work, and the procedure that does lived only in somebody's head. It now performs it: bump, commit, push, then push a SINGLE v* tag by full ref, because a bulk --tags push does not reliably fire release.yml. It refuses rather than guesses: - not on main (dev is a decoy default branch) - dirty tree — this is a SHARED checkout, so uncommitted files are routinely not yours, and the tag would exclude them correctly but silently - unpushed or behind origin, either of which releases something that is not what is on main - tag already exists And it runs typecheck first, because release.yml runs neither tests nor typecheck — until the workflow above is proven healthy, that is the only thing between a type error and a published binary. The closing output tells you to verify on the COMPILED binary rather than from source: --compile bundles static imports, the capability index among them, so the two can disagree — which is how a shipped index gets tested by accident instead of on purpose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wLxn8v7hD3rud1G17YjNk
`bug close` auto-stamped the cwd's HEAD and announced it in dim text. Naming the repo (added after #177912) was not enough: closing an iris-opencode bug from the freelabel monorepo recorded a8a9cc45 -- an fl-eco-docker commit -- as the fix, and a wrong hash is indistinguishable from a right one to whoever reads it next. cwd is where you are STANDING. It is not evidence about where the fix LANDED. The batch case already refuses for exactly this reason; the single case is the same guess with a smaller blast radius, and it is the common case. Now stamps only on an explicit yes: - interactive: confirm the detected hash, DEFAULT NO, so a bare Enter records nothing rather than a guess - non-interactive (agent, CI, MCP): refuse and print the three real options, including the exact `--commit <hash>` to paste if HEAD is in fact the fix. Guessing on an agent's behalf is how five bugs got stamped with an unrelated fl-api commit. - `--commit` / `--no-commit` unchanged Verified by running both paths from the dev entry: non-TTY refuses and writes nothing (no "Recording fix" line), `--commit <hash>` still records. Found while closing #180525, which this would have prevented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0121oCnNCeZBewSyiQSQ8bin
The committed index advertised `senders prefer`, whose source is NOT in HEAD — it exists only in another agent's uncommitted working-tree file. My own earlier commit put it there, because the OLD pre-push guard read the working tree and refused to let anything through until the index matched it. That is the corruption described in #180517, caused by the guard meant to prevent it. The new guard caught it on the first run, which is the point: CI checks out a clean tree, so the entry appears as 'indexed but gone' — the unambiguous direction, which still blocks. The first honest verdict this workflow has produced in days was a true positive about damage the previous version caused. Edited surgically rather than regenerated: regenerating here would read the same dirty tree and put the phantom straight back. Formatting matches the generator exactly (JSON.stringify does not escape non-ASCII), so the diff is the one entry and its two counts rather than 484 lines of em-dash churn. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wLxn8v7hD3rud1G17YjNk
capabilities:check could never pass on a CI runner. Playbooks and skills live in the WORKSPACE repo, not in this package, and collectMarkdown returns [] for a directory that is not there — so a checkout of iris-opencode alone reports all 83 of them as 'indexed but gone' and fails regardless of what anyone commits. Nobody had noticed because the workflow was never executing. The first honest run after moving it onto a working runner failed on exactly this. The check now compares only what the checkout can actually see, and WARNS about what it skipped — a narrowed check that does not announce its narrowing reads as full coverage, which is the failure this guard exists to prevent. Commands and how-tos, which this repo owns, are still checked in full. The pre-push hook has always guarded this with a directory test; the guard now lives in the generator, so the answer no longer depends on which harness invoked it. Verified by simulating the CI condition (IRIS_PROJECT_ROOT=/nonexistent): 83 skipped with a note, commands and how-tos still compared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wLxn8v7hD3rud1G17YjNk
…aches for first Mirrors the fl-api side. `iris senders prefer alex-mayo-iris --order email,apple_mail` sets the order; `iris senders bind ... --primary` promotes one channel without restating the whole list. `senders show` now prints the rank next to each binding — (primary), (2), … — because "which two mailboxes are bound" was never the question an operator was asking; "which one does this actually leave from" was. Preferring a channel with no binding is a 422 from the API, surfaced verbatim: it would otherwise queue a send the router refuses at delivery time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qar7NcLqNhHVcUbxrkQccW
The pre-push guard caught this, which is what it is for — an unindexed command is one an agent cannot discover, so it may as well not exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qar7NcLqNhHVcUbxrkQccW
…s no longer unbuilt Adds `iris senders prefer` / `bind --primary` to the how-to, and says the thing that is easy to get wrong: `default` picks the default IDENTITY, `prefer` picks that identity's TRANSPORT — two different questions with adjacent names. Also corrects a stale "not yet built: SN-2" line; the backfill shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qar7NcLqNhHVcUbxrkQccW
…ntity routerSend gains `sender`, and `mail send` exposes it. --sender and --from answer the same question in opposite directions: --from is a raw address nothing has checked, taking the unrouted bridge path; --sender is a registered identity the API verifies and routes on. Passing both is an error rather than letting whichever branch runs first decide, and --sender with --attachment/--cc is refused because that path cannot read a channel binding. Deliberately NOT on `imessage send`: the bridge sends from whatever account Messages.app owns, so the flag could not honour itself — and that command falls back to local AppleScript when the router refuses, which would send anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qar7NcLqNhHVcUbxrkQccW
Says the two constraints that are not guessable: an unverified sender is refused rather than downgraded, and --sender needs a lead because the ad-hoc handle path bypasses the channel bindings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qar7NcLqNhHVcUbxrkQccW
… exception A campaign step declares its own channel, so the sender preference does not apply there — deliberately, since the step author said "email" and a preference must not override that. Which means a mismatch fails at delivery instead of at configuration time. Says so, and points at the command that asks the question early. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qar7NcLqNhHVcUbxrkQccW
The list an apple_mail binding asserts. Until the bridge could enumerate accounts, that assertion was unfalsifiable: verification confirmed the bridge answered, not that the address was a real account. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qar7NcLqNhHVcUbxrkQccW
…aveat was wrong This file said an unknown Mail.app account would send from the default silently. It never did: the bridge's lookup threw -1700 and every Apple Mail send naming a from-address failed outright — the binding path had never worked. The caveat described a plausible failure instead of the real one, which is why it survived unexamined; it read as a known limitation rather than a bug. Documents `iris mail accounts`, that binding and sending both refuse an address Mail.app lacks, and the one claim still outstanding: the account existing is not the same as Mail.app applying it, which only a received header settles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qar7NcLqNhHVcUbxrkQccW
… way to correct it (#180584)
There was create-list and nothing else. A typo or a duplicate list was permanent from the CLI,
while ITEMS had both delete-item and restore-item. The asymmetry was the bug: a command that
creates structure with no command to correct it means every mistake is forever.
Both endpoints existed the whole time — BloqListController::update and ::destroy, routed as
PATCH /user/{userId}/bloqs/list/{listId} and DELETE /user/bloqs/list/{listId}. Only the surface
was missing, so this is a CLI addition and nothing else.
Hit for real today: `iris bloqs create` silently seeds Ideas/Todo/In Progress/Completed/Daily
Diary and its output says only {success, id, name}, so setting up a client project (anomalyco#601, GTC
MediGuide) produced TWO Todo lists, two In Progress and two Completed — on a board whose first
reader would reasonably ask which one is real. Now cleaned up with the new command.
delete-list is deliberately NOT a mirror of create-list. Creating a list costs nothing; deleting
one takes ITS ITEMS WITH IT. So it:
- counts the items first when --bloq-id is given, and REFUSES a non-empty list without --force,
naming the count — a number is what turns "delete list 1964" from a guess into a decision
- WARNS when it could not count, rather than proceeding quietly. A silently skipped safety
check reads exactly like a check that passed, which is the failure mode this codebase keeps
finding.
Verified against the live board rather than asserted:
delete-list 1964 --bloq-id 601 -> success, items_removed 0
delete-list 1974 --bloq-id 601 -> REFUSED: "has 6 item(s)"
delete-list 9999999 (no bloq-id) -> warns the count was NOT checked
Still open in #180584: `bloqs create` should report the lists it seeds, or take
--no-default-lists. Printing them would have prevented the duplication entirely.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011wLxn8v7hD3rud1G17YjNk`resolve` APPENDS a resolution block, so a bug closed more than once carries every stamp it has ever had. Both readers used `String.match()` without /g, which returns the FIRST match — the OLDEST stamp. That breaks the remediation path for the bug fixed in fe89daf. When a close stamps the wrong commit, the correction is to re-close with the right one — and the correction was invisible. #180525 was mis-stamped a8a9cc45 (fl-eco-docker, an unrelated repo), corrected TWICE to ebbf1f7, and still displayed a8a9cc45 on both `bug list` and `bug show`. The wrong hash was what everyone read, and nothing you could type would change it. A record you cannot correct is worse than one that was never written: it looks authoritative and it is wrong. Extracted `latestFixCommit()` (matchAll, take the last) and used it at both call sites. Four tests, including the real #180525 content and a guard that prose mentioning a hash is not mistaken for a stamp. Verified against production: #180525 rendered `✓ FIXED a8a9cc45` before and `✓ FIXED ebbf1f7` after, same ticket, same data. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0121oCnNCeZBewSyiQSQ8bin
… (#180633)
`iris bloqs list` hits /api/v1/user/{id}/bloqs, which excludes system bloqs — agent workspaces
and `app:*` client dashboards. On this account that silently withheld thirteen boards,
including app:pathways-dashboard, app:pathways-clinical-doc, app:drex-dashboard,
app:experience-art-dashboard, app:moody-beauty-dashboard and four agent Workspace boards.
Hiding them by default is right; a project picker should list things a human made. Saying
nothing about it is not. `iris bloqs get 11` returns System Data with its two lists and
`iris bloqs list --limit 500` does not contain it — so a board you can open by ID is absent
from the list with no indication, which is indistinguishable from not having access to it.
That is precisely how a WORKING access grant on bloq anomalyco#600 read as a failed one earlier today:
granted, checked the list, did not see it, concluded the grant had not worked.
Adds --all (include_system=1) and --type, and prints a dim note whenever neither is set.
The note is deliberately not a count. The index endpoint does not report how many rows it
withheld, and printing a number the server never sent would re-introduce the same class of
problem this fixes. Naming the flag is honest; guessing the total is not.
Needs the paired fl-api change (8a8cd43a) — before it, ?type=system returned 0 of 13 because
the hardcoded type filter was ANDed ahead of the declared one.
bun turbo typecheck: 12/12 pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011wLxn8v7hD3rud1G17YjNk`report [title..]` is a GREEDY array positional joined with spaces, so when a callers quoting collapses every loose token slides into the title and the report is filed anyway. Reproduced against the same builder shape: ["report","search returns 0","daycare","Hutto","--json","TX","78634"] -> title "search returns 0 daycare Hutto TX 78634", json=true which is exactly the shape stored on #180697. Nothing errored. That is the actual defect — not that reports break, but that a broken one arrives LOOKING COMPLETE, so four were filed in a row before anyone noticed. CORRECTION to the root cause in #180713: apostrophe stripping and repro commands truncated at the first quote are NOT this tool. They happen in the callers shell before argv exists. Counter-example, filed by this same command earlier today: #180691 stores "person\x27s push" with the apostrophe intact, and #180633 kept its quotes. Anyone hunting for a sanitizer in this file would have found nothing, which is why it is worth writing down. So the guard aims only at what argv can still see: - a flag string absorbed into the title (--description, --severity, --command, --error, --json, --bounty) or a positional that begins like a flag -> refuse, name the token, show the assembled title, print the correct invocation - a title over 220 characters -> refuse; that is body text, not a headline Adds --title, the form that cannot absorb its neighbours. The non-interactive error has been telling people "--title is required" for some time while no such flag existed. yargs folds it into the same array as the positional (verified: --title x yields ["x"]), so it needs no separate branch. And the part that generalises, because collapsed quoting CANNOT be reliably detected — by the time yargs is done, --json has been eaten as a flag and the leftovers look like an ordinary title: the success output now echoes the TITLE THAT WAS STORED. The confirmation used to print only "submitted" and an item id, which is how four corrupted reports got past their own author. Verified from source: absorbed-flag case refuses, 383-char case refuses, ordinary report still files and now shows its stored title. bun turbo typecheck 12/12. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wLxn8v7hD3rud1G17YjNk
Login only ever said "Authenticated (user 193)". A bare id is not an answer when someone legitimately holds two accounts: here the laptop CLI authenticates as one user and the MCP connector as another, and a board owned by the other is not reported as locked — it is reported as "Bloq not found". You cannot notice an account mismatch you were never shown, and today that cost a wrong duplicate-account diagnosis before anyone checked the ids. Prints email + id + admin flag, and WHERE the credential came from: a stale IRIS_API_KEY in the environment silently outranks `iris auth login`, so "I logged in as X but everything acts as Y" was previously invisible. Resolved server-side from the bearer in hand (GET /api/v1/auth/whoami, fl-api d11fc8dc) rather than read back from local config — local config is exactly the value that lies when a stale env var is in play, so it reports the account the API will actually act as. When the API cannot confirm the credential it says so instead of falling back to the stored id; a confident wrong account is the bug being fixed, not a missing one. Verified against production: 200 with the right identity, 401 unauthenticated, and the failure path exercised while the endpoint was still 404. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0121oCnNCeZBewSyiQSQ8bin
typecheck has been red on main since 18:36Z. `iris auth whoami` shipped in 7a90f491e without regenerating the capability index, and capabilities:check is a required step: capabilities.json is STALE — agents cannot discover what is not indexed. missing from the index (1): command:auth whoami That failure message is the point of the check. The index is how agents find commands, so a command that exists but is not indexed is a command no agent will ever call — it works when a human types it and is invisible to everything else. Regenerated locally where the workspace IS present, so playbooks and skills were compared too. CI can only see commands and how-tos (it warns "83 playbook/skill entries were NOT checked — set IRIS_PROJECT_ROOT"), which means a regeneration run in the wrong environment can silently drop project content. Checked the diff for exactly that: 11 insertions, 3 deletions, counts 1199 -> 1200 and 1319 -> 1320, the parent auth haystack gaining "whoami", and the new entry. Nothing removed. bun.lock is also dirty in this checkout from another session and is deliberately not included. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wLxn8v7hD3rud1G17YjNk
…#180713) Adding `--title` alongside the `[title..]` positional made the help WORSE. yargs folds an option and a positional of the same name into one entry, so the option description won and the help read: title bug title (unambiguous alternative to the positional) [array] [default: []] which describes the flag while sitting in the positional slot, and no separate --title line ever appears. Someone reading that learns neither how to pass a title nor that the flag exists — a net loss against the original "short bug title". Both describes now carry the same text, which reads correctly wherever yargs decides to print it, and names the flag: title short bug title — quote it, or pass --title "..." [array] [default: []] Verified by running `bug report --help` against a checkout with workspace deps, not by reading the source — the merge behaviour is the whole point and is only visible in the rendered output. capabilities.json is unaffected: it indexes command describes, not positional ones (checked). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wLxn8v7hD3rud1G17YjNk
The course is real and has been for months — Course/CourseChapter/ CourseProgress/CourseCertificate, scored server-side, verifiable by anyone at /p/verify-certificate without an account. What was new is that a Bounty OS visitor can now reach it. The section spends most of its words on what certification is NOT, because all three confusions are expensive: - it is not a gate (nextStep ranks it BELOW an unclaimed balance on purpose — telling somebody to sit a quiz while their money sits unmentioned is how a dashboard loses trust) - it is not an agreement (same shape, different thing: a signed document has a ledger, an audit trail and revocation that a quiz does not) - the answer key never leaves the server, so a new question path goes through HunterTraining::publicQuestions() or it ships its own answers Plus the two failures worth naming: a per-path proxy that 404s same-origin while the upstream is fine, and an unseeded environment where `training` is null and the section is omitted rather than emptied. Refs #180702 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118r7ZPdSYw7oymTNBoUiqF
… (#180704, #180706)
get_pipeline_summary returns { pipeline: [{stage,count,total_value}], totals: {cases,value} }.
The CLI read data.total_cases, data.stages[].name and .value — none of which exist — so a
system holding 2,156 cases and $17.2M reported "0 cases | $0" on both `pipeline` and `status`.
The data was reachable the whole time: `integrations exec servis-ai list_cases` returns 2,129.
Those two commands are the first thing anyone runs, so a working system looked dead.
Normalised into readPipeline() so both callers agree, with the old field names kept as
fallbacks. status also surfaces stage count and audit_flag_count now that they are free.
`audit` printed fc.patient_name in preference to the case ID, putting patient names into
terminal scrollback, screen-shares and recordings — and it is the command most likely to be
demoed on a client call. Default is now the case ID; names require an explicit --names flag.
pipeline 0 cases | $0 -> 2156 cases | $17,241,820.12
audit "<patient name>: ..." -> "CAS112824: ..." (--names to opt in)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wc96FNjrbQVzzL3EKEPSNo…#180735)
`iris opportunities list --json` emitted INVALID JSON, cut mid-value at an exact 64KiB boundary,
with exit 0 and nothing on stderr. Every automated consumer therefore either failed to parse the
prefix or — worse — parsed it and acted on a partial list.
index.ts ends with process.exit(), which DISCARDS whatever stdout still has buffered when stdout
is a pipe. Interactive use never shows it: TTY writes are synchronous. Only scripts break, which
is the mode nobody is watching.
MEASURED ON THE COMPILED BINARY (source barely reproduces it — 1/10 — which is exactly why the
binary is the thing to test):
before: 6/6 truncated at 65536
exit-site drain: 1/10 truncated <- did not work
stdout.write wrapper: 2/12 truncated <- did not work
writeJson at the site: 0/15
TWO WRONG FIXES FIRST, both of which LOOKED right from source. Probing bun directly explains
why neither could ever have worked:
process.stdout.write("x".repeat(300_000))
process.stdout.writableLength -> 0 (nothing to poll)
process.stdout.writableNeedDrain -> false
process.stdout.write("", cb) -> cb fires SYNCHRONOUSLY (barrier is a no-op)
console.log(...) -> does NOT route through process.stdout.write at all
So the pending bytes are not observable at the exit site, and console.log cannot be wrapped,
counted or drained from outside. A REAL write callback does fire after the flush — which is why
writeJson() in iris-api.ts works, and why it is the only available mechanism. The existing note
in index.ts said the fix belongs at the write site; this confirms it with measurements and
leaves the reasoning where the next person will look.
So: 622 pretty-printed `console.log(JSON.stringify(x, null, 2))` call sites across 134 files are
now `await writeJson(x)`. The transformation is OUTPUT-IDENTICAL by construction — writeJson
emits JSON.stringify(value, null, 2) + "\\n", byte for byte what console.log of the same
expression produced. Only the flushing changes.
Compact `JSON.stringify({success:false,...})` calls are deliberately left alone: they are small
error objects that cannot approach the pipe buffer, and converting them would be churn.
Four sites landed in sync functions and were caught by typecheck, not by review — the reason a
mechanical sweep of this size is safe to attempt at all. Each fixed properly rather than
reverted: a forEach became for..of (a sync callback cannot await, so the payload would have gone
out unflushed again), and printResult/renderLocalUsage became async with their single callers
awaited.
Verified on a locally compiled binary: 0/15 on the reported command, and bug list (161KB),
agents list (145KB) and bloqs list all parse. typecheck 12/12. capabilities.json unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011wLxn8v7hD3rud1G17YjNk…ate shipped fl-api #180702 moves verified-but-unreleased money out of `owed` into a new `held_cents`. This board only rendered `owed`, so the instant that deployed, Rashad ($39) and Flo ($18) went from "owed $39.00 / $18.00" to "owed $0.00" with nothing on the row to say the money still existed. That is wrong in a quieter and worse direction than the overstatement it replaced. $0.00 owed reads as "nothing pending" or "already paid" — an operator scanning this board would conclude two hunters were square when $57 of their verified work was sitting held. Rendered as its own column rather than folded back into `owed`, because they are different facts: one is money that moves if you ask for it, the other is money waiting on something the hunter has to do. The column only appears when somebody actually has some, so the ordinary board stays a four-column read, and the legend says what held MEANS — a board that shows a number nobody can interpret has moved the problem rather than fixed it. Verified by running the real production payload through this exact expression: $39.00 and $18.00 render, the three hunters with nothing show an em dash. Refs #180702 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118r7ZPdSYw7oymTNBoUiqF
d3d26d81 blocks hive run / exec / script / push / deploy / swarm / demo on the iris-exec path. The how-to still read as though `hive run` works everywhere, and the person most likely to hit the new refusal is someone following this page from Claude. Says which commands are refused, which still work (nodes list/show, tasks, status, logs, peers, connections, queue, doctor), and WHY — the restriction is about trust, not capability. A human typing `iris hive run` has intent; a model that just read an email does not. Deliberately NOT changing `iris hive run --help`: the CLI is not restricted, and a warning in its help would be wrong for the terminal, which is where that help is read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0121oCnNCeZBewSyiQSQ8bin
Observed in production 2026-08-17. Asked to fetch projects, the model called the MCP with `iris bloqs list --json` — exactly as a human would type it — and got back `Unknown command "iris"`. It then told the user it could not access the IRIS platform. The tool taught the model to deny a capability it had. The MCP spawns the iris binary directly with the argv it is handed, so a leading "iris" lands in argv[0] where a subcommand belongs. Both forms are now accepted. Expecting every model to remember that this one interface wants the binary name omitted is a convention we would have to re-teach on every model swap — and the default model changed three times in two days. Tests pin both forms, that a value merely CONTAINING "iris" is untouched (only argv[0] is a binary name), and that quoted multi-word arguments survive the shift. One test in the first draft asserted that an unknown command is still rejected. It passed for the wrong reason: the guard is gated on `knownCommands.size > 0` and the registry is not loaded in a unit context, so it never ran. Replaced with an assertion that actually exercises something, and the reason is written down — a test that claims coverage it does not have is worse than no test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… slicing it Observed 2026-08-17: `bloqs get 544 --json` returned 68KB, the MCP sliced it at 100KB-minus-nothing with '...(truncated)', and the agent reported it could not access the IRIS platform. A JSON payload cut mid-object is unparseable, so the model lost the data AND every route back to it — and read the whole thing as a failure. Claude's own harness handles this better: write the full result to a file, tell the model to use offset/limit/jq. That turns a dead end into an artifact. This does that and one thing more. It returns an OUTLINE of the payload inline — top-level keys with types, plus the key shape of the first element of any array. In the transcript that motivated this, the very next thing Claude did after the overflow was `jq keys` to discover the shape. The outline answers that up front, so the model can write a useful filter on its first attempt instead of its second. The message is written as instructions, not as an error, and says explicitly that the command SUCCEEDED. That sentence is load-bearing: the failure being fixed is not 'output too big', it is an agent concluding the platform is down and telling the user so. It also suggests re-running something narrower, because reading a 68KB file is rarely the best answer when a specific list id would do. Truncation stays as the fallback when the spool cannot be written — a degraded answer beats no answer. 17 tests: the complete payload survives byte-for-byte, the outline names keys and element shapes without a round-trip, jq examples for JSON and grep/head for text, and the success wording is asserted rather than assumed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… (#180715) The good search already existed. `bloqs items <bloq-id> --search ... --include-all` fans out across bloq + Obsidian + Drive, returns snippets and reports per-source health. It just required a board id — which you do not have at the moment you are searching, because finding it is why you are searching. Worse, the empty-result hint on `iris search` pointed AT that command: Widen the net: iris bloqs items <bloq-id> --search "..." --include-all offering the one thing the reader cannot run, at exactly the moment they cannot run it. `iris search` now takes --include-all and --source. The hint points at itself: Widen the net: iris search "propulsion" --include-all and disappears once the fan-out is already on, rather than suggesting what you just did. The bloq source is deliberately EXCLUDED from the fan-out here: the cross-board content search this command already runs covers it without a board id. Including it would reintroduce the exact requirement that made the engine unreachable. Source outcomes are always reported, in both text and --json (source_outcomes), because a source that ERRORED must not be indistinguishable from one that found nothing — the same rule federated-search.ts already states for skipped sources. Verified from source: the default path prints the corrected hint; --include-all prints the "Obsidian & Drive" section plus the health line "obsidian 0 · drive 0" and drops the hint. typecheck 12/12. Nothing was rebuilt — this is a front door on an engine that was already working. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wLxn8v7hD3rud1G17YjNk
#182078. The filtering in `leads list` was always correct. The REPORTING failed, and it failed silently — so six funnel KPIs were computed from a page and published as measured fact. Written test-first: the spec existed before `leads-scope.ts` did, and the first run was 0 pass / module not found. Extracting the logic out of the handler was what made it testable at all; it had been inline, which is why nothing covered it. SIX CASES, each one a way the original lied: · a complete answer stays quiet — no false alarm to train people to ignore · truncation names the POPULATION, not the page size · a hidden default filter is disclosed WITH the escape hatch · both conditions report together rather than one masking the other · --json carries the caveat, since that is where the bad KPIs were computed · total below shown is bad data, not truncation — no invented negative remainder PROVEN TO FAIL. Reintroduced the exact defect — `s.shown > s.shown`, the page size read as the total — and three of the six failed immediately, naming the population, the combined report, and the JSON warning. Reverted, back to 6/6. A guard that has never failed is not a guard. The handler now calls the tested function rather than keeping its own copy, and the end-to-end output is byte-identical: 20 lead(s) (62 Prospected hidden — use --all · newest 20 of 28522) Suite 573 pass / 1 fail, stable over three runs; the one failure is `platform-mint.ts · rm <key> · --json` from another session's uncommitted work. Refs #182078, RevOps epic #182075 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZagd8eJFQz593QjSkVG4u
…ilscale query retry + SSH policy Four bugs found while reviewing the hive-reliability epic (cc09c9a), all confirmed against the committed code (not WIP) before fixing: - platform-hive-doctor.ts read an invented field `completed_tasks` instead of the real HiveNode field `total_tasks_completed` (#182103) — always undefined, so `iris hive doctor <node>|--all` printed "done ?" regardless of the node's actual task count. Verified live: real nodes now show their actual completed counts (2798, 186) instead of nothing. - `iris hive doctor --json` was ignored on the local (no-node/--all) path (#182105) — added when remote support was bolted on, the pre-existing local branch was never touched. A script expecting JSON (the flag is advertised command-wide) got decorated text and broke on JSON.parse(). Verified live: --json with no node argument now returns valid JSON. - hive-tailscale.ts's tailscalePeers() had no retry and could not distinguish a query failure from genuinely zero peers, unlike the sibling platform-hive-vpn.ts readStatus() — fixed there after a real 2026-08-16 incident where one transient failure read as "not on the tailnet". This module exists specifically as the reliable fallback BECAUSE it doesn't inherit the primary transport's outages (#182004); shipping without the retry its own sibling already proved necessary undercut that. Added the same one-retry-per-binary pattern, and threaded a queryOk signal into resolveSshTarget()'s error message so "couldn't ask" and "asked, zero peers" read differently instead of both saying "start Tailscale" (verified live: still resolves all 6 real peers correctly, ~900ms). - StrictHostKeyChecking was inconsistent for the same conceptual operation (ssh into a Hive node) — accept-new in hive-tailscale.ts, =no (silently accepts a changed host key forever) at 4 sites in platform-hive-enroll.ts and platform-hive-net.ts, no documented rationale either way. Unified on accept-new (auto-trusts on first contact, but flags an actual key change) — the safer choice, with no functional downside for either enrollment or established-peer SSH. A 5th finding from the same pass, the ANSI/OSC regex in hive-selftest-assert.ts (#182102), turned out to already be fixed in this commit's own code — the ESC/BEL bytes are real control characters in the source, invisible to grep, which is what made my first pass think it was still broken. Left untouched; closing that bug as already-resolved. typecheck clean; existing hive-tailscale/hive-selftest-assert/hive-script- result test suites still pass (64/64). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CpfuupcXvWeSfupPYwhRQ1
…cross your own nodes iris vault create | list | put | get | ls | status WHY, specifically. There is already plenty of storage here — cloud:upload puts a file on the CDN, atlas files attaches it to a bloq, bloq-sync pushes it to Drive. Every one of those puts the bytes on a third party's disk, which rules them out for data this org actually handles: clients/ is gitignored ON PURPOSE because no BAA covers it, PHI cannot go to OpenAI, Tailscale has no BAA either. The current answer to "where does this live" is often "don't store it" — which is how a controller mapping representing two days of work survived only as an untracked working tree on one laptop. MOSTLY WIRING, WHICH WAS THE POINT. Identity and pairing (bridge 9a1821d), node registry and liveness (heartbeat), capacity (#182019 already reads disk headroom), and byte transport with integrity (#182013, sha256 both ends) all existed. New here: chunk, encrypt, place, verify. THE PROPERTY THAT MAKES "SOVEREIGN" MEAN SOMETHING. Blobs are addressed by the sha256 of their CIPHERTEXT. A holding node can verify its copy is intact — recompute, compare to the id — WITHOUT the key. Replication does not imply clearance, so a cheap always-on replica is safe. Demonstrated on MacBookPro: it holds 56,136 bytes for a 56,108-byte file (+12 nonce +16 GCM tag), no plaintext markers, random header. AES-256-GCM, random nonce PER CHUNK — never counter-derived, since two files would then reuse (key, nonce) at the same index and GCM nonce reuse leaks the XOR of plaintexts and forges the tag. Key lives only in ~/.iris/vault-keys.json (0600) and never syncs. status ASKS EACH NODE what it holds rather than reading a local ledger of what we believe we sent, and an unreachable node reports UNKNOWN, not absent. A partial holder is named but never counted, because a partial copy restores nothing — counting it produces "2/2 replicas" over a file that cannot be recovered, which is the reassuring-but-false number this codebase has spent the week removing. Placement is capacity-aware because it has to be: MacBookPro is at 98% disk. A node with UNKNOWN free space is not eligible — unknown is not room. CAUGHT IN MY OWN BUILD: put reported "wanted 2, placed 1" while status reported 2/2, because one counted remote placements and the other counted total holders. That is #182091 (three definitions of MAX_CONCURRENT) reproduced inside a single feature. The target is now TOTAL copies, defined once. VERIFIED LIVE, not just unit-tested: put -> 2/2 copies, plaintext sha256 matching the source exactly status-> 2/2 by querying MacBookPro; names AlexMaysnow1063 as UNREACHABLE remote-> ciphertext only, correct size overhead, no plaintext get -> restores byte-identical AND: deleted every local blob, then restored from MacBookPro alone — "chunks fetched from: MacBookPro", sha256 identical. Tests: 31 pass / 0 fail, 100% line coverage of vault-core; typecheck clean. NOT CLAIMED, and surfaced in `status` rather than left to silence: single-writer only (no conflict resolution), no key escrow, availability follows your devices being awake. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DezvuWKN7k5zUBtJ44ZZM9
The pre-push capabilities:check hook caught it: iris vault create/list/put/ get/ls/status (26606af) never made it into capabilities.json, so none of them would surface in capability search/discovery despite being fully wired and tested. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CpfuupcXvWeSfupPYwhRQ1
…had a blind spot The suite was red — four offenders. Three were the ratchet being wrong, one was real, and the three were what made the one easy to dismiss. FALSE POSITIVES (platform-vault put/get/status --user). The option IS read, through `targetFor(n, argv)` — the handler forwards argv wholesale to a helper. The detector already skipped wholesale forwarding, but only when the param was the FIRST argument, so a helper taking it second read as never using it. Now matched in any argument position, with a regression test pinning the platform-vault shape. That mattered more than three noisy lines: a ratchet that cries wolf gets muted, and the one true violation in the very same run hides in the noise it created. THE REAL ONE (mint group rm --json). Registered, never read. Deactivating a spending group from a script returned prose either way, so a caller parsing JSON got nothing parseable on success and nothing parseable on refusal — and a refusal that produces no output reads to a script as though the group were gone. All three exits are now machine-readable: not_found, bound_budgets, and success. The refusal shape matters most; it is the one a script is likeliest to misread as silence. 575 pass, 0 fail across the cmd suite.
Junaid's Aug 23 call named "the one big thing that is missing is actually KPIs" as the gap in the RevOps build. Adds objectives + key results (quarterly, goal-scoped) and a separate KPIs dataset (ongoing steady-state metrics, no quarter attached) — same Atlas schema-driven pattern as the mentions dataset from #182118. iris okr objectives list/create/show/update iris okr kr add/update iris okr status — dashboard across all tracks iris kpi list/create/update/show Live-verified end-to-end against production with placeholder records, then cleaned up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzGwGaksRciQ6AWi4y7WW3
…shes doctor [name]: catches the version/steps trap where a playbook has real "### step:" blocks in its body but frontmatter's `version` isn't EXACTLY 2 (parsePlan does `fm.version === 2 ? 2 : 1`, an exact-match not a floor) -- steps silently never execute and `run` falls back to a raw text dump with no warning. Also surfaces shadow copies (Skill now tracks every discovered location for a name via Skill.locations()/Skill.duplicates(), not just the first-found winner) plus the existing validatePlan() issues, in one sweep over every playbook. verify <name>: replaces the manual curl/grep/json-parse loop after a publish with one command -- local file parses, API registry has the row, registered content matches the local file (a publish 200 says the request succeeded, not that the body sent was current), and the live public page actually returns 200 when scope is public. Found 3 live, currently-public playbooks broken by the exact bug doctor targets (agentic-loop, x-ads, iris-hive) -- fixed at the data layer (playbook frontmatter, separate commits/repo) and confirmed clean with verify. Filed #182230 for the underlying exact-match coercion in executor.ts, which is unchanged here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DkFsjYp3XWwDQBeTH6cEqi
Export fmtPct from platform-okr.ts so it's testable in isolation, and lock in the null-target, zero-target, no-reading-yet, and real-0% cases — the em-dash vs. 0% distinction is the one this display logic is easiest to get wrong silently. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzGwGaksRciQ6AWi4y7WW3
Two things a concurrent rebase dropped, recovered: 1. THE HIVE REGISTRATION. `iris hive vault` was gone — platform-hive.ts no longer imported or registered VaultCommandExport, so only the top-level `iris vault` survived. That is the placement specifically asked for: vault belongs next to `hive fs`, the primitive it is built on. Re-added; both paths now resolve and both are in the capability index. 2. THE SELF-NODE FIX (was 317b7d6, dropped from HEAD entirely — the code had no detectLocalNodeId at all). `hive vault status` counted the local node's blobs as "this machine" AND listed that same node under "could not reach", in one breath. On a two-node mesh that is half the fleet described wrongly. detectLocalNodeId() now excludes self before dialling, using the daemon's own node_id rather than os.hostname() — macOS rewrites the hostname on every mDNS collision, so a hostname comparison matches the wrong machine or none. Also reindexes capabilities: 1323 commands · 1517 capabilities. The pre-push hook was right to block on this — a capability that exists but is absent from the index is undiscoverable, which is the same defect family as a node advertising what it cannot do. Verified: typecheck clean (0 errors across the workspace), 31 vault tests pass, `iris hive vault --help` lists all six subcommands, and both `iris hive vault` and `iris vault` appear in capabilities.json. NOTE FOR WHOEVER REBASES NEXT: this is the second time these changes have been silently dropped by a `pull --rebase` in this repo (26606af, then 317b7d6). Both were recovered by cherry-pick only because a typecheck failure or a stale capability index happened to surface it. Nothing announces the loss. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DezvuWKN7k5zUBtJ44ZZM9
…are gating, brand-correct how-to social Three independent changes recovered from a stash that a parallel session's work had been swept into. Each stands alone: mcp call / mcp tools (#182089) — an MCP server was reachable by a human in an MCP client and by nothing else: not a playbook, not an agent, not a script. IRIS was already an MCP client; it had no verb that invoked a tool. MCP.callTool THROWS on failure rather than returning undefined, and checks isError separately from transport success — a well-formed response whose payload is an error is not a successful call, and conflating the two is why MCP-backed playbook steps had to be written as `mode: human`. Share gating (allowed-emails / allowed-domains) — threads named-recipient and domain allowlists through apiMakePublic to `iris atlas item share`, so a PHI-classified item can be shared with verified addresses instead of a link anyone holding the URL can open. How-to social (#182088) — the CTA and links followed the docs site rather than the brand. A FREELABEL post told musicians to run `iris how-to view <slug>` (an instruction they cannot follow) and linked to IRIS docs. Nothing errored; the render reported success and the creative belonged to another company. Now --url carries the brand's own link, the CLI incantation is IRIS-only, and a non-IRIS brand with no --url warns loudly instead of silently emitting another company's URL. Verified: typecheck clean, 787 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzGwGaksRciQ6AWi4y7WW3
`current / target` is only correct for an INCREASE key result. Applied to a decrease KR it inverts, and the failure is silent and flattering. Seeding real RevOps data surfaced it immediately. A genuine KR — "cut hours of manual effort per campaign", target 1h, sitting at 6h, i.e. its worst possible value, nothing done yet — evaluated 6/1 = 600%, capped to 100%, and rendered in the "complete" colour. It then dragged its objective's average UP: the marketing_ops row read 60% when true progress was 18%. `direction` was already stored on every KR. It was used for one thing: which arrow glyph to print. It never touched the arithmetic. krProgress() now measures each direction on its own terms — decrease as target/current (guarded so current=0, a total elimination, cannot divide to Infinity), maintain as decay from the target in either direction (overshooting a hold-steady KR is a miss, not a win), increase unchanged. It returns null rather than a number when a pair cannot yield a percentage, and the dashboard EXCLUDES those from the average instead of counting them as 0 — "not measured yet" is not "no progress", and folding them together makes an objective look worse than the evidence supports. The same defect existed one layer over in KPIs, which had no direction field at all: "Lead response time", 45min against a 15min target — three times worse than goal — rendered as 300% in the exceeding-target colour. Added direction to the revops_kpis schema (now v2) and threaded it through create/list/update/ show, with an arrow so a reader can tell whether a low number is the goal or the problem. Verified live, not just in tests: marketing_ops 60% → 18%, the decrease KR 100% → 17%, lead response time 300% → 33%, and both move monotonically the right way as readings improve (6h→3h = 17%→33%; 45min→20min = 33%→75%). 15 tests cover the arithmetic, including the two regressions by name. Full suite: 796 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzGwGaksRciQ6AWi4y7WW3
…ndiscoverable The pre-push gate caught this, which is the gate working. Four new commands existed and ran, but nothing that searches for a capability could find them, so `iris how-to search` and the agent-facing index would both have reported "no such thing" for features that shipped. 1325 commands · 48 how-tos · 73 playbooks · 73 skills = 1519 capabilities. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzGwGaksRciQ6AWi4y7WW3
mayoalexanderforce-pushed
the
fix/publish-html-requireotp-182059
branch
from
August 24, 2026 20:10
831d15c to
2dfcc6eCompare…d the live KPI layer `iris kpi` was a second KPI store standing next to one that already existed and was better. `iris bloq kpis 624` holds 19 KPIs, five computing from real data, each blocked one annotated with WHY, with a gap map (#182060) and build order (#182075) built on top of it. Mine was global instead of bloq-scoped, hand-entered instead of computed, and tracked no blocked reasons. Two stores both called "KPI" is how a number ends up in the one nothing reads, so this removes mine rather than keeping both. What genuinely did not exist is an objective with SEVERAL key results under it. `bloq goals` carries one `target` string and one `--kpi` link. `iris okr` keeps that job and nothing else — it is now a goal layer, not a measurement layer. A key result can now REFERENCE a KPI (`--kpi-bloq 624 --kpi k_zrl88kmtov`) and read its value live instead of having it retyped and left to drift. The reference is deliberately strict about what it will claim: - The KPI is the source of truth for the READING. When it has none — 14 of the 19 on anomalyco#624 are blocked — the key result is UNMEASURED and says so, naming the blocking reason. It does NOT fall back to the KR's own current_value, which defaults to 0: that fallback rendered a confident "0%" for a metric nobody is computing, the same defect as the decrease-KR bug one commit earlier, and it was caught the same way — by pointing it at a real blocked KPI and reading the output instead of trusting it. - The TARGET still falls back, because a KPI can declare a goal before it measures against it. - A reference to a KPI that no longer exists renders loudly as missing rather than silently showing stale local numbers. - Unmeasured and blocked key results are EXCLUDED from an objective's average and reported as "(+N unmeasured)", never averaged in as zero. Verified live against bloq anomalyco#624: MRR 99 / 5000 2% ← live Logo churn 25% / 5% ↓ 20% ← live, decrease applied Lead-to-Contact not measured / 60% ← blocked: no population query crm_ops objective reads "25% avg across 1 KR (+1 unmeasured)" Note: evolving revops_key_results to carry the reference orphaned its existing rows (Atlas #181628 — the read path only sees the newest schema version). Rows were backed up and re-seeded. Anything with real data in it needs that handled before a schema change, not after. 18 tests here; full suite 799 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzGwGaksRciQ6AWi4y7WW3
1324 commands · 48 how-tos · 73 playbooks · 73 skills = 1518 capabilities. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzGwGaksRciQ6AWi4y7WW3
…ility lives in its flags The haystack was built from command name + aliases + one-line describe. Option names and their describe text were never read, so the index knew what a command was CALLED but not what it could DO. Found by trying to use it: `iris atlas:item make-public` carries --allowed-emails and --allowed-domains, whose entire purpose is gating a shared link to named people or domains. Searching "gate", "allowed emails" or "restrict who can read" returned NOTHING across all 1518 capabilities — the only place that language exists is an option description. The feature shipped hours earlier and was already unfindable. Same failure family as a command that registers but is unreachable, one layer up: a capability nobody can find is one nobody uses. Verified: gate · allowed-emails · allowed-domains · phi · verified now all index against atlas:item make-public. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzGwGaksRciQ6AWi4y7WW3
`iris playbook items` lets a playbook hold the written SOPs a person follows (as Atlas items, attached BY REFERENCE) alongside the skills an agent runs. The models, controller, CLI and public gallery rendering have all shipped for a while — and it went almost entirely unused. Every playbook's contents were empty, because nothing in the naming said the capability existed. Nothing here changes behaviour. It changes whether anyone can find it: - `items` describe now names what it is for rather than restating the noun, plus an epilogue explaining the two kinds of thing it holds and the by-reference model (edit the Atlas item, every playbook carrying it updates — nothing is copied, nothing goes stale). - `add` describe and --bloq-item/--skill help say what they attach, with two worked --examples. - The empty state teaches the commands instead of only saying "(none)". That is the one moment a person is definitely looking at this surface, and it was being spent on a shrug. Adds how-to `playbook-sops-and-skills` covering the full path: draft an SOP from a transcript, publish it as an Atlas item, attach it, assign roles — including that a raw meeting note is NOT an SOP (it is organised around when things were said, not what someone has to do) and the version-bump semantics that expire acknowledgements. Follow-up for a provisioning verb (fork/seed a team's library from templates) is tracked separately; the container is done, only provisioning is missing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01792epDfkpAJTSXPpK2HmnU
…files (RO-7 #182271) The RevOps KPI layer was built against one revenue model: a B2B SaaS funnel. The customer it was built toward — GTC MediGuide (bloq anomalyco#601) — is TELEHEALTH, where revenue operations means the revenue CYCLE: eligibility, prior auth, coding, claim submission, adjudication, denials, A/R. Not a missing metric, a missing half (RO-6 #182260). Only the client can say which model applies. Waiting blocks the build; guessing wastes weeks in whichever direction is wrong. So the model stops being a prerequisite and becomes a parameter. Both models share one spine — work item -> staged pipeline -> terminal outcome -> reason taxonomy -> recovery motion -> cycle time -> cost ratio. opportunity /claim, won/paid, lost/denied, loss-reason/CARC, re-engagement/appeal, sales-cycle/days-in-A-R, CAC/cost-to-collect. That mapping came from laying the HFMA MAP Keys beside our own gap map, not from analogy-hunting. Ships: - revenue-models.ts — typed profile registry: subscription, payer_billed, cash_pay. Serialisable by design so it can move to a served registry when agents need it. - `iris bloq models [bloqId]` — profiles are discoverable, and it says which one a given bloq is running. - `iris bloq kpis list` filters to the active profile, ALWAYS prints which profile answered, and distinguishes "declared on this bloq" from "DEFAULT — not declared". A default is not a decision, and reporting one as a choice is the same defect as a gate that says "gated" without saying gated to whom. - `--model <key>` previews another profile without mutating the bloq; an unknown key is REFUSED, never silently defaulted, so a typo cannot read as a deliberate choice. - `--applies-to` on `bloq kpis add`; `--all` to see every profile's KPIs. Two safety properties, both tested: - An untagged KPI applies EVERYWHERE. All 19 KPIs on anomalyco#624 are untagged today; if absence meant hidden, enabling this would empty the board — and a metric that vanishes reads as "we don't track that", which is indistinguishable from "we track it and it's fine". - Filtering reports what it set aside rather than quietly shrinking the list. Default is `subscription`, so no existing board changes meaning. Phase 2 (837/835 ingestion, clearinghouse, CARC parsing) stays correctly blocked. Verified live against bloq anomalyco#624. 20 new tests; 613 pass across the CLI suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzGwGaksRciQ6AWi4y7WW3
`mode: playbook` lets a step hand off to another playbook. It has worked since the executor shipped — recursive execution, depth cap of 3, self-reference guard, arg passing, nested run output. Verified end to end with a throwaway parent/child pair. Playbooks in the registry using it: zero. Because nothing in the help text, the docs, or any existing playbook said it was possible, authors wrote "now go and run X" as prose — a chain edge that only exists as an instruction a person has to notice and follow. Adds an epilogue to `iris playbook --help` with the syntax and the limits, and a how-to covering when to chain versus when a child playbook is just a section of the parent wearing a costume. Both state the limits plainly, since each is currently discovered by hitting it: nesting caps at 3, a playbook cannot call itself, INDIRECT cycles are not detected (A→B→A surfaces as a confusing depth error), args are positional so reordering a child's declarations silently changes every caller, and a typo'd child name passes `playbook test` and fails only at run time. Part of #182309, which tracks the missing primitives — branching, fan-out, named args, and a visible dependency graph. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01792epDfkpAJTSXPpK2HmnU
…annot run a stale copy The CLI half of HF-01 + HF-03 (#182275, #182276). Daemon half: bridge d6d2e70. `iris scripts run` now resolves the slug to a sha256 of its content BEFORE dispatching, and sends it in the task config. The node then runs exactly that version or refuses — it never has to guess whether the copy it cached weeks ago is still current. This is what let the daemon DELETE its slug-keyed cache rather than bolt a TTL onto it. A cache keyed on a mutable name is the part that should not exist; with the content hash as the address, staleness is impossible by construction and verification is free, because the address and the checksum are the same value. Fails open, and says so. If the metadata fetch fails or an older API returns no content, no digest is sent and the CLI prints "this run will be UNVERIFIED" rather than implying a guarantee it did not obtain. Tests: 12 pass / 0 fail. The important ones are cross-language — the digest is computed here in TypeScript and verified in the daemon in JavaScript, so an encoding or normalisation disagreement would fail every run, or worse get "fixed" by weakening the check. Fixtures cover empty scripts, non-ASCII, CRLF and trailing whitespace, and assert that whitespace is NOT normalised away (normalising would give two genuinely different files one address, which is the staleness bug reintroduced through the back door). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DezvuWKN7k5zUBtJ44ZZM9
…just doctor (#182230) parsePlan collapsed any frontmatter `version` that was not EXACTLY the number 2 down to v1, and a v1 plan parses zero steps — so "version: 3", the string "2", 2.0, or a missing field produced a playbook that validated clean, synced clean, and executed nothing. The coercion was only half the bug. `iris playbook doctor` DID catch it, but the check was implemented inline in platform-playbook.ts and only there. `iris playbook test` calls validatePlan(), whose sole step-count rule was gated behind `version === 2` — which a coerced plan never is. Two commands, one file, opposite verdicts; test reported "Steps: 0 / No issues found / Valid" on an 8-step playbook. - SkillPlan gains declaredVersion (raw frontmatter value, pre-coercion) and bodyStepCount (steps parsed from the body regardless of version). parseSteps now always runs; only EXPOSURE as executable steps stays gated on v2. Without both, the parser destroys the evidence before validation can see it: a mis-versioned playbook is otherwise indistinguishable from an honest v1 one. - validatePlan() errors when version !== 2 && bodyStepCount > 0, naming what was actually declared. Silent when bodyStepCount === 0 so genuine v1 prose playbooks stay green. - bodyStepCount is optional: four test files construct SkillPlan literals and a required field would break them for no gain (parsePlan is the only producer). d307ad1 already removed the inline duplicate from platform-playbook.ts, so without this commit HEAD detects the trap in NEITHER command. Verified by reproduction, not inspection: old binary, version: 3 -> Steps: 0, "No issues found", Valid patched, version: 3 -> "8 '### step:' block(s) ...", Validation failed patched, version: 2 -> Steps: 8, Valid Confirmed on the installed binary, and `playbook doctor` over all 69 playbooks reports zero new errors (no false positives on real v1 docs). 5 regression tests; 3 confirmed FAILING with the check disabled — a test that passes either way tests nothing. Suite 205 pass / 0 fail, typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WcmgfXD81U3cgNv9L2ZzfL
…nent library The compiler has always recorded props/emits/slots on every artifact so a builder need not re-parse source, and the API has always had an index. Nothing surfaced either — so composing a page meant remembering slugs and guessing prop names, which is the friction that makes someone write a NEW component instead of naming an existing one. That quietly defeats the point of a stored library. genesis library list [--search] [--stale] props/emits/slots inline; search the declared API genesis library show <slug> [--source] plus the JSON to paste into a page genesis library usage <slug> which pages name it, nested marked, count stated genesis library versions <slug> publish history + staleness genesis library rollback <slug> --version restore; says every page just changed NAMED library, NOT components, because `genesis components <slug>` already exists and means something different — what is on ONE PAGE. One word with two meanings is the drift that makes a CLI unlearnable, and it is the same trap that had dataset meaning two things earlier today. The product purpose line and keywords were widened too: those are what `iris help` and agents read, so a capability absent from them is undiscoverable no matter how good the verb is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…jects Filing something in the wrong project is the normal case, not the exceptional one. Until now the only remedy was to recreate the item, which changes its id and breaks every cross-reference and public share URL pointing at it. Pairs with the fl-api change that accepts bloq_id on update, re-homes the item onto a list in the destination, and refuses a move that would drop a PHI/sensitive boundary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01792epDfkpAJTSXPpK2HmnU
iris bloqs update-item <id> --to-bloq <id> --to-list <id> THE CAPABILITY ALREADY EXISTED — only the flags were missing. BloqItemController::update has accepted `bloq_id` and `bloq_list_id` for a while, and its own comment says why: // Move an item to a different project. There was no way to do this at any layer — // not the CLI, not the API — so the only way to file something in the right place // after the fact was to recreate it, which changes its id and breaks every // cross-reference and public share URL pointing at it. That last clause is the point. An item's public URL is /n/<uuid> keyed to its id, so "recreate it in the right bloq" silently breaks every link already shared — which is exactly the situation this was needed for: four published research items sitting in Published Docs that belonged in the IRIS Capabilities epic, with their URLs already circulated. Verified by read-back rather than by the success message, which printed "updated ()" with no field names and would have looked identical had nothing moved: #182268 #182278 #182315 #182021 now in bloq 503 / list 2171 ✓ (and gone from 522/1568) public /n/<uuid> still 200 ✓ all four Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DezvuWKN7k5zUBtJ44ZZM9
The CLI half of #182312. Daemon half: bridge b920bf7. `hive selftest` now emits `ran-on-the-targeted-node` FIRST, because every other assertion is about a machine — and if that one fails, they are all describing the wrong one. MEASURED 2026-08-24: three consecutive runs of `hive selftest MacBookPro` scored 6/8, 0/1 and 4/8 with different failures each time. That was not flakiness. At least one demonstrably executed on a different machine, and at least one demonstrably ran on MacBookPro, so two of those scores describe two different computers. An instrument that cannot say which machine it measured cannot be used to decide anything. A result that does not say which node ran it is a FAILURE, not a pass — that silence is the pre-fix state, and treating it as "probably fine" is exactly how three scores came to describe two machines. Omitted entirely when no target is supplied, so callers that never named a node do not gain a phantom failure. Tests: 18 pass / 0 fail. They cover the match, the mismatch (asserting BOTH machine names appear, since "it ran somewhere else" is useless without saying where), the silent case, the ordering, and the omission. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DezvuWKN7k5zUBtJ44ZZM9
fromHiveTask read `r.output ?? r.stdout`, so the merged field — which every node sends, and which contains BOTH streams — always won. The "streams come back separate" assertion could not pass however correct the node was, because the mapper discarded the separation before the assertion ran. `output` remains the fallback for nodes that predate separated streams. Refs #182004 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DezvuWKN7k5zUBtJ44ZZM9
…you import it Every other discovery surface requires you to already know the answer: `read` needs a function name, `sync` needs a bloq id AND source AND path, `pulse check` needs a keyword. On day one you have none of those. survey is the read-only manifest that comes first — it imports nothing. It reads BOTH the availability list and the connections list and reports where they DISAGREE, because neither alone is trustworthy. That merge immediately found a bug far larger than the one it was written for: on this account 8 of 16 connected sources are invisible to `data-sources list` — google-drive (3 accounts), courtlistener, tradovate, and the entire social estate (instagram/x/tiktok/threads/linkedin, 17 brand accounts). All are callable via `read`; none are discoverable. Anyone asking "what data do I have" gets an answer wrong by half. Filed as #182323, scope corrected after this ran. Also reports two numbers that are usually different — sources connected vs sources `sync` can actually bulk-ingest (1 of 16 here, since sync only accepts dropbox|google_drive). Conflating those is the mental-model error this exists to prevent. --deep counts what is inside each enumerable source, and records WHY a count failed rather than rendering a blank: "HTTP 500", "requires: vault", "Missing required parameters: query" are all different from "0 items". Also fixes #182326 in the same file: `data-sources list --json` and `read --json` printed the UI banner before the JSON, so stdout would not parse (`Expecting value: line 1 column 1`). Same defect fixed in `playbook verify` earlier today; same fix — gate all chrome behind `if (!json)`, no trailing outro on the JSON path. 18 new tests over the pure helpers, including a regression test built from the real production shape (connected + working + unlisted) and one for the underscore/hyphen spelling split between `sync` (google_drive) and everything else (google-drive), which a naive string compare would double-count. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DkFsjYp3XWwDQBeTH6cEqi
Task ids are UUIDv7 — time-ordered, so runs a second apart share a leading prefix. Three consecutive selftests printed "task 01a0370c" and read as one cached result being replayed; they were three distinct tasks whose ids differed only after the eighth character. That is the same truncated-uuid mistake that produced a wrong high-severity diagnosis in #182312, reproduced by the tool built to catch exactly this class of defect — a display that cannot distinguish two runs from one. Source only: installing it is blocked on disk (volume is 100% full). Refs #182004, #182312 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DezvuWKN7k5zUBtJ44ZZM9
…a playbook The record → draft → publish pipeline was complete except for its first and last links, and both were small. DISCOVERABILITY: mic capture already existed, well-built (level meter, silence warning, keeps the wav when transcription fails) — as `iris listen`/`dictate`. Nobody hunting for "record" guesses either, so the capture step read as missing when it was only misnamed. `record` is now an alias. THE CHAIN: `playbook draft` accepted a transcript all along, but listen transcribed, printed, and DELETED its audio, leaving the user to find the file and re-invoke by hand. --draft (and --sop) now pin the transcript to a known path and hand it to the real drafter — no re-transcription, one drafter, same behaviours. The wav is kept until the draft actually succeeds: a failed draft must never be why the recording is gone. `agent` IS A REAL MODE NOW. The server's WalkthroughStructurer emits `mode: agent` for every step of a drafted playbook, deliberately — a step a model extracted from audio must not be runnable on sight (measured: "bare push" came back as "bear push"), so promoting one to shell is a human edit where someone takes responsibility for what runs. That is good design, but `agent` was never in StepDef's union: it worked only by falling through the executor's `default:` to manual. So validation flagged EVERY drafted playbook as broken — including live-meeting-to-build-pipeline, which is what prompted this. Declaring it keeps the runtime behaviour identical, makes the intent visible instead of accidental, and stops the checker crying wolf on the platform's own output. Verified live: the failure path warns on silence, refuses to draft from an empty transcript, and preserves the recording; the drafter produces an accurate 5-step playbook from a transcript and now passes `playbook doctor` clean. Found and filed while testing, not fixed here: `playbook draft --name` renames the directory but not the frontmatter `name:`, so the result is unaddressable by the name you gave it (#182332). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DkFsjYp3XWwDQBeTH6cEqi
…ion, genesis-regression The index had fallen behind `iris data-sources survey` (7427e64), which is committed, plus the playbook-composition how-to and the genesis-regression playbook/skill. A capability that is not indexed is one agents cannot discover, so a stale index is a silent feature outage rather than a bookkeeping lapse — which is why the pre-push hook guards it. Purely additive: verified that no existing capability name is removed by the regeneration. The other changed lines are haystack/count fields. Regenerated with plain `bun run capabilities`, NOT --prune: five indexed entries have no source in this workspace and belong to a machine that has them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DezvuWKN7k5zUBtJ44ZZM9
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.
Summary
buildBespokeJsonContent()hardcodedrequireOtp:falseon the standalone lane and omitted it on the custom lane, regardless of--requires-auth. Therequires_authrecord column locked the page, but every visitor (including the owner) got the frictionless "instant access, no code, no password" modal instead of a real emailed code — and that path has no code to submit, so it looped./p/mediguide-boundary: the page owner could not get past the email step.requiresAuthintobuildBespokeJsonContent()and settingrequireOtpfrom it on both lanes.Test plan
bun test src/cli/cmd/platform-pages-verify.test.ts— 21/21 pass, including 2 new regression testsjson_content.requireOtp = trueon the affected page, then ran the real flow end to end —send-otp(200), code received via email,verify-otp(200, real session token), gated content unlocked with the token, anonymous request still correctly shows the OTP-mode gate (requireOtp":truein the SSR payload)Fixes #182059.
🤖 Generated with Claude Code
https://claude.ai/code/session_01HzGwGaksRciQ6AWi4y7WW3