From 834d571174691f046ae8c0bcd802939cc4ca154f Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Wed, 16 Sep 2026 16:01:13 -0600 Subject: [PATCH 1/3] Add: recorded product intent with hi 155 criteria across 11 families, captured with the hi CLI rather than written by hand, saying what rune should be for the people who use it. Criteria are directions, not a status report. A criterion that is false today means the code has not arrived yet, not that the criterion is wrong, so nothing here records whether anything is built. Every criterion was scored for accuracy against this repository's own evidence. Anything a second independent auditor confirmed below 85 was rewritten or retired before this commit. CI gains a `hi check` gate. It fails only on structural problems in hi/*.md and never on unfinished intent, so it cannot go red because somebody wrote down a want that is not built yet. hi is pinned to 0.3.3 --locked. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UcDK2AAqo8w9vjvwA2yitV --- .github/workflows/ci.yml | 17 +++++++++++++++++ .gitignore | 1 + INTENT.md | 19 +++++++++++++++++++ hi/attach.md | 22 ++++++++++++++++++++++ hi/cli.md | 32 ++++++++++++++++++++++++++++++++ hi/discover.md | 25 +++++++++++++++++++++++++ hi/install.md | 18 ++++++++++++++++++ hi/library.md | 22 ++++++++++++++++++++++ hi/read.md | 26 ++++++++++++++++++++++++++ hi/run.md | 32 ++++++++++++++++++++++++++++++++ hi/screen.md | 22 ++++++++++++++++++++++ hi/send.md | 33 +++++++++++++++++++++++++++++++++ hi/session.md | 38 ++++++++++++++++++++++++++++++++++++++ hi/watch.md | 28 ++++++++++++++++++++++++++++ 14 files changed, 335 insertions(+) create mode 100644 INTENT.md create mode 100644 hi/attach.md create mode 100644 hi/cli.md create mode 100644 hi/discover.md create mode 100644 hi/install.md create mode 100644 hi/library.md create mode 100644 hi/read.md create mode 100644 hi/run.md create mode 100644 hi/screen.md create mode 100644 hi/send.md create mode 100644 hi/session.md create mode 100644 hi/watch.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32ebbc1..8f9c460 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,3 +122,20 @@ jobs: version: 6.0.0 require-coverage: "100" lifecycle-enforce: "true" + + # Its own job rather than a step in `verify`: that job is a six-way Ruby + # matrix, and `cargo install` would rebuild hi from source in every one of + # them for a check that has nothing to do with the Ruby version under test. + intent: + name: Intent + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install hi + run: | + cargo install human-intent --version 0.3.3 --locked + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Check intent structure + run: hi check diff --git a/.gitignore b/.gitignore index 29f239b..df7601f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ .fledge/ coverage/ .specsync/hashes.json +intent.html diff --git a/INTENT.md b/INTENT.md new file mode 100644 index 0000000..5bf4b6d --- /dev/null +++ b/INTENT.md @@ -0,0 +1,19 @@ +# rune + +rune exists so that a terminal program can be used by something that is not a person, without pretending the terminal is anything other than what it is. Anything you can do by hand at a keyboard, a program should be able to do through rune and get an answer it can act on; anything a program does through rune, you should be able to take over yourself and hand back. The hardest part is knowing when the thing on the other end has finished talking, and rune should be honest about that — saying plainly where it is guessing rather than quietly answering with your own words echoed back. A wrong answer that looks right is the worst thing this tool can produce, so where rune cannot be sure, it says so in the reply and in its own documentation. + +## Features + + +- [attach](hi/attach.md): ATTACH (9 criteria) +- [cli](hi/cli.md): CLI (19 criteria) +- [discover](hi/discover.md): DISCOVER (12 criteria) +- [install](hi/install.md): INSTALL (5 criteria) +- [library](hi/library.md): LIBRARY (9 criteria) +- [read](hi/read.md): READ (13 criteria) +- [run](hi/run.md): RUN (19 criteria) +- [screen](hi/screen.md): SCREEN (9 criteria) +- [send](hi/send.md): SEND (20 criteria) +- [session](hi/session.md): SESSION (25 criteria) +- [watch](hi/watch.md): WATCH (15 criteria) + diff --git a/hi/attach.md b/hi/attach.md new file mode 100644 index 0000000..f90630f --- /dev/null +++ b/hi/attach.md @@ -0,0 +1,22 @@ +--- +hi: 1 +families: [ATTACH] +--- + +# Taking the wheel + +## Intent + +Anything an agent is driving, a person should be able to step into and drive by hand — and then step back out of without ending it. Attaching should feel like the program was always yours: the current screen is already there, it lays itself out for your window, and your keystrokes go straight through. Leaving is one keystroke and changes nothing about the session. Nothing you do at an attached terminal, including a connection dying badly, may take the session down. + +## Criteria + +- **ATTACH-1** I can attach my own terminal to a running session and drive it myself. +- **ATTACH-2** Attaching replays the current screen, so I am not left staring at a blank one. +- **ATTACH-3** The program is resized to my terminal while I am attached. + - **ATTACH-3.a** The program returns to its usual size when I leave. +- **ATTACH-4** One keystroke detaches me and leaves everything running. + - **ATTACH-4.a** Ctrl-C is not that keystroke, because it has to keep reaching the program so I can interrupt a runaway. +- **ATTACH-5** More than one viewer can be attached at once. + - **ATTACH-5.a** One viewer going away does not disturb the others. +- **ATTACH-6** Nothing I do at an attached terminal, including disconnecting badly, can take the session down. diff --git a/hi/cli.md b/hi/cli.md new file mode 100644 index 0000000..c478925 --- /dev/null +++ b/hi/cli.md @@ -0,0 +1,32 @@ +--- +hi: 1 +families: [CLI] +--- + +# One tool, two audiences + +## Intent + +rune is meant to be used by a person at a keyboard and by a program driving it, with neither getting a second-class version. The same verbs do the same work either way; only the presentation changes, and it changes by itself when nobody is watching. A caller should never have to read English to find out what happened, and should always be able to tell rune's own failure from the failure of the thing rune was wrapping. Flags belong to rune only up to the point where the wrapped command begins, and anything ambiguous is refused rather than guessed at. + +## Criteria + +- **CLI-1** I get the same behaviour from a command whether I type it myself or a program calls it. + - **CLI-1.a** Only the presentation differs between those two, so neither a person nor a program gets a reduced version of the tool. +- **CLI-2** I get coloured, readable output when I am looking at a terminal. +- **CLI-3** I get JSON when something other than a person is reading the output. + - **CLI-3.a** Piping or redirecting rune's output is enough to get JSON, with no flag to remember. +- **CLI-4** I can ask for a one-line JSON envelope instead, when the harness around rune expects every command to answer in that shape. +- **CLI-5** Every reply tells me which version of the wire contract it was written against, so I know what I am reading before I parse it. +- **CLI-6** Everything a program is meant to parse arrives on one channel, with nothing else mixed into it. +- **CLI-7** The command I am wrapping receives its own flags untouched, however much they look like rune's. + - **CLI-7.a** There is one unmistakable place where rune's flags stop and the wrapped command's begin. + - **CLI-7.b** A flag rune does not recognise, in the place rune's own flags go, is refused rather than run or passed along. +- **CLI-8** A flag that would have no effect on the subcommand I called is refused rather than accepted and quietly ignored. +- **CLI-9** Two flags that contradict each other are refused rather than one of them silently winning. +- **CLI-10** A failure explains in plain words what went wrong. +- **CLI-11** A failure carries a stable code a program can branch on instead of prose it has to grep. + - **CLI-11.a** A code a client has never seen still routes somewhere sensible, because every code names the coarse kind of problem it is. + - **CLI-11.b** A failure tells me whether repeating the identical call could plausibly work. +- **CLI-12** A bug in rune reads differently from a mistake in my call. +- **CLI-13** Rune's own failure is never confusable with the failure of the command it was wrapping. diff --git a/hi/discover.md b/hi/discover.md new file mode 100644 index 0000000..10d837f --- /dev/null +++ b/hi/discover.md @@ -0,0 +1,25 @@ +--- +hi: 1 +families: [DISCOVER] +--- + +# Finding out what rune can do + +## Intent + +An agent arriving at rune for the first time should be able to learn the whole surface from rune itself, in one round trip, without scraping text written for people. Help is a normal result, so it is available as data; the wire contract — every field, every failure code, every state — is published the same way. The published description has to be the one rune is actually held to, and it should name the traps as well as the fields, because the dangerous replies here are the ones that say everything worked. + +## Criteria + +- **DISCOVER-1** I can ask rune what commands it has and what each one is for. +- **DISCOVER-2** I can ask one command for its usage and its own flags. +- **DISCOVER-3** A command with subcommands lists them the way the top level lists commands, so one reader handles both levels. +- **DISCOVER-4** Help is available as data, so a program discovers the surface without scraping the version written for people. +- **DISCOVER-5** Asking for help never runs the command I asked about. +- **DISCOVER-6** I can get the whole wire contract — every reply field, every failure code, every state — as machine-readable data. + - **DISCOVER-6.a** The published contract is the same one rune's own tests hold it to, so it cannot describe a rune that does not exist. +- **DISCOVER-7** The contract names the field states that mean the call worked but the thing I wanted did not happen. +- **DISCOVER-8** The contract names the known ways each verb can mislead me. +- **DISCOVER-9** I can ask rune which version it is, so I know what I am talking to before I rely on it. +- **DISCOVER-10** I can read rune's guides in my own language. + - **DISCOVER-10.a** The English guide is authoritative wherever a translation disagrees with it. diff --git a/hi/install.md b/hi/install.md new file mode 100644 index 0000000..768ecc4 --- /dev/null +++ b/hi/install.md @@ -0,0 +1,18 @@ +--- +hi: 1 +families: [INSTALL] +--- + +# Getting the tool + +## Intent + +rune is only useful once it is on the machine, and the first thing a newcomer does should not be the hardest. Getting it should be one step, with no toolchain to set up first, and that step has to be unambiguous about which rune it fetches, because the obvious name belongs to something else entirely. Upgrading later should be the same step again. Nothing about installing it should require reading the repository. + +## Criteria + +- **INSTALL-1** I can install rune in one step, without cloning a repository or setting up a Ruby toolchain first. +- **INSTALL-2** Following rune's own install instructions gets me this rune rather than an unrelated package that happens to hold the name. + - **INSTALL-2.a** What I install is pinned to something I can check, so I can tell it is what the maintainer published. +- **INSTALL-3** I can move to a later release the same way I installed the first one. +- **INSTALL-4** I can install rune into my task runner and call its verbs there the way I would at a prompt. diff --git a/hi/library.md b/hi/library.md new file mode 100644 index 0000000..3f045e7 --- /dev/null +++ b/hi/library.md @@ -0,0 +1,22 @@ +--- +hi: 1 +families: [LIBRARY] +--- + +# Using rune from your own code + +## Intent + +rune is a library as well as a command, and a program that already speaks Ruby should be able to wrap a terminal, script an interactive prompt, and turn a CLI's printed output into data without pulling anything else in. The parsers exist because terminal output is the only interface many tools have; they should do the obvious thing and let me overrule them where the obvious thing is wrong. Above all, a driven program behaving badly is data, not an exception — the caller decides what happens next. + +## Criteria + +- **LIBRARY-1** I can use everything rune does from Ruby, not only from the command line. +- **LIBRARY-2** Adding rune to my own project brings nothing else along with it. +- **LIBRARY-3** I can turn a printed table into a list of records without writing a parser. + - **LIBRARY-3.a** Rune works out for itself whether a table is pipe-drawn or space-aligned. + - **LIBRARY-3.b** I can override that guess where it would be wrong. +- **LIBRARY-4** I can turn key-and-value output into a hash with numbers and booleans already typed. +- **LIBRARY-5** I can clean escape codes out of any captured text. +- **LIBRARY-6** I can script an interactive program as a sequence of waits, keystrokes and pauses. +- **LIBRARY-7** A driven program misbehaving arrives as data on an ordinary result, so my code decides what happens next instead of catching an exception. diff --git a/hi/read.md b/hi/read.md new file mode 100644 index 0000000..9e6b7c2 --- /dev/null +++ b/hi/read.md @@ -0,0 +1,26 @@ +--- +hi: 1 +families: [READ] +--- + +# Reading back what happened + +## Intent + +A session keeps a durable record of everything its program printed, and that record should be reachable long after the turn that produced it — while the session runs, and after it has stopped. Because a driven agent produces enormous amounts of output, reading should be something you do in slices: only what is new, only the last few lines, only the lines that match. The record has to stay bounded so an all-day session does not eat the disk, and any output that had to be dropped must be admitted rather than presented as a continuous stream. + +## Criteria + +- **READ-1** I can read a session's output back at any time, whether it is still running or long finished. +- **READ-2** Each read hands me a position I can pass back next time to get only what is new. + - **READ-2.a** A position stays meaningful even when older output has been dropped. + - **READ-2.b** A position never replays what I already have. +- **READ-3** I can take a slice of a long transcript instead of all of it. + - **READ-3.a** I can ask for just the last few lines. + - **READ-3.b** I can cap how many bytes come back. +- **READ-4** I can search a long transcript for a line and get the lines around it. + - **READ-4.a** A pattern that will not compile returns nothing and says so, so a filter that never ran is distinguishable from a filter that found nothing. +- **READ-5** A read tells me whether the program is still printing, so I never have to grep its own busy marker. + - **READ-5.a** A read tells me how long it has been since the program last printed. +- **READ-6** A transcript stays bounded, so a session left running all day does not grow without limit. +- **READ-7** When output has been dropped, the read says how much, rather than presenting a gap as continuous output. diff --git a/hi/run.md b/hi/run.md new file mode 100644 index 0000000..da76bd4 --- /dev/null +++ b/hi/run.md @@ -0,0 +1,32 @@ +--- +hi: 1 +families: [RUN] +--- + +# Running one command + +## Intent + +The simplest thing rune does: take one command, run it in a real terminal so it behaves the way it would for a person, and hand the whole thing back as data once it finishes. Wrapping a colourful full-screen tool should be as safe as wrapping a plain script, and nothing should ever hang forever waiting for a reader that will never come. The result should be small enough to put in front of a model, honest about anything it had to leave out, and honest about anything it is only guessing at. + +## Criteria + +- **RUN-1** I can run any command inside a real pseudo-terminal and get one structured result once it finishes. +- **RUN-2** The output comes back with escape codes and cursor moves removed, so it reads as plain text. +- **RUN-3** I can also see exactly what the command emitted, escapes and all. +- **RUN-4** A command that would normally page its output answers straight away instead of waiting for a reader. +- **RUN-5** I am told how long the command took. +- **RUN-6** I get the wrapped command's own exit status back, so rune drops into a shell pipeline where the bare command used to be. +- **RUN-7** A command that never finishes is killed after a limit I can set. + - **RUN-7.a** A killed command still returns everything it printed before the kill. + - **RUN-7.b** A timeout comes back as an ordinary result with its own recognisable exit status, rather than as an exception. +- **RUN-8** I can cap how much output comes back. + - **RUN-8.a** I am told how much output was left out. + - **RUN-8.b** The place where output was cut is marked in the text itself, so what comes back never reads as something the command actually printed. +- **RUN-9** I can have the command's error output kept apart from its ordinary output when I need to tell the two apart. +- **RUN-10** Interrupting rune interrupts the command I wrapped, every time and not only the first time. + - **RUN-10.a** Interrupting twice in quick succession ends the run rather than waiting on a command that will not stop. + - **RUN-10.b** Nothing of an interrupted run is left running behind me. +- **RUN-11** A command given as an explicit list of arguments runs exactly as written, with no shell slipped in between. +- **RUN-12** I am told when the output looks like the command is waiting for input. + - **RUN-12.a** Rune is plain that this is a guess rather than a verdict, so I do not build a decision on it. diff --git a/hi/screen.md b/hi/screen.md new file mode 100644 index 0000000..b728b06 --- /dev/null +++ b/hi/screen.md @@ -0,0 +1,22 @@ +--- +hi: 1 +families: [SCREEN] +--- + +# What the terminal actually shows + +## Intent + +A full-screen program repaints constantly, so its byte stream is every frame of every repaint with the answer scattered across them — and the thing it plainly displayed may not be findable in the bytes at all. When the question is what does it say right now, rune should answer with the screen a person would be looking at. That screen is small, which is exactly what you want in front of a model, but it is a snapshot, so rune must be straight about what a snapshot cannot tell you. + +## Criteria + +- **SCREEN-1** I can ask for the screen the program is showing rather than the bytes it sent. +- **SCREEN-2** The screen is available both when I send and when I read. +- **SCREEN-3** The screen is rendered at the size the program is actually running at, not at some fixed default. + - **SCREEN-3.a** The reply tells me the size it rendered at. + - **SCREEN-3.b** The reply tells me whether that size was the program's real one or a fallback. +- **SCREEN-4** A screen is small enough to put in front of a model without my having to bound it. +- **SCREEN-5** Looking at the screen never costs me the transcript, which stays the record of what happened. +- **SCREEN-6** Rune says plainly that a screen caught mid-repaint can be half-painted. + - **SCREEN-6.a** Rune says what to do instead of trusting a single frame. diff --git a/hi/send.md b/hi/send.md new file mode 100644 index 0000000..c661f48 --- /dev/null +++ b/hi/send.md @@ -0,0 +1,33 @@ +--- +hi: 1 +families: [SEND] +--- + +# Driving the program on the other end + +## Intent + +This is what a persistent session is for: ask the program something and get its answer back, the way a function call does. rune should turn an asynchronous terminal into a request and a reply — write the text, wait until the other end has really answered, hand back exactly what this turn produced. Knowing when a program has finished talking is genuinely hard, so rune should give more than one way to decide, say in every reply which way decided it, and be candid about the cases where it cannot tell. Nothing I did not mean to type should ever reach the program, and nothing that failed to reach it should look like it arrived. + +## Criteria + +- **SEND-1** I can send text to a session's program and get back exactly the output that send produced. +- **SEND-2** The call waits until the program has actually answered, so a turn reads as a request and a reply. +- **SEND-3** I can say how long a quiet stretch has to be before rune calls the turn finished. +- **SEND-4** My own words coming back as the terminal's echo do not count as the program having answered. +- **SEND-5** I can wait for output matching a pattern instead, for when I know what the program prints when it is done. + - **SEND-5.a** A pattern is matched against the program's answer rather than against the echo of what I sent. + - **SEND-5.b** A pattern that will not compile is refused before anything is typed at the program. + - **SEND-5.c** Rune says plainly that a pattern can match a repaint of something older than the question I just asked. +- **SEND-6** Every wait has a hard ceiling I can set. + - **SEND-6.a** Reaching that ceiling hands back what was captured rather than failing. +- **SEND-7** The reply says which of the possible reasons ended the wait, so I never have to infer it. +- **SEND-8** I can write something and return immediately when I am not expecting a reply at all. +- **SEND-9** I can write text without submitting it, for composing a line in pieces or driving a program that reads keystrokes. +- **SEND-10** Pressing enter reaches the program the way a real terminal's enter does, so full-screen programs receive it. +- **SEND-11** The reply tells me whether the program was still printing when my text landed, so an answer that belongs to the previous question is recognisable. +- **SEND-12** A send to a program that has already exited comes back as an error naming the state, not as silence. +- **SEND-13** A second send arriving while one is still in flight is refused rather than interleaved with it. +- **SEND-14** Text that never reached the program comes back as a failure rather than as a send that looks like it happened. +- **SEND-15** A mistyped flag is refused rather than typed at the program as though it were part of my message. +- **SEND-16** Rune says plainly where a quiet program cannot be told from a finished turn, instead of guessing on my behalf. diff --git a/hi/session.md b/hi/session.md new file mode 100644 index 0000000..77e6065 --- /dev/null +++ b/hi/session.md @@ -0,0 +1,38 @@ +--- +hi: 1 +families: [SESSION] +--- + +# A session that outlives the call + +## Intent + +An agent CLI is a conversation, and a conversation cannot live inside a single command invocation. A session holds one of those programs open under a name, so you can start it now, talk to it from another process later, and still be talking to the same program with the same state. Sessions should be easy to have several of and hard to lose track of: named, listed with enough information to tell working from stuck, scoped to the project you are standing in, and cleanly disposable when you are done. Nothing rune says about a session should be a stale note on disk when the truth is a process you could have looked at. + +## Criteria + +- **SESSION-1** A session I start keeps its program running after the command that started it has exited. +- **SESSION-2** I can come back to the same session later — from another process, another agent, another hour — and find the same program with the same state. +- **SESSION-3** Every session has a name I can address it by. + - **SESSION-3.a** I only have to choose a name when I care which one it is. + - **SESSION-3.b** A session started without a name gets a memorable codename that nothing else is using. +- **SESSION-4** Sessions belong to the directory I am working in, so the same name in two checkouts is two different sessions. + - **SESSION-4.a** Starting a session tells me which project it landed in, because that is the first thing I need when I cannot find it again. + - **SESSION-4.b** I can list every session everywhere when the one I want is not in this project. + - **SESSION-4.c** When the session I asked for is not in this project but exists in another, the answer tells me where it is instead of just saying no. +- **SESSION-5** A command that cannot be launched at all is a failure rather than a session. +- **SESSION-6** A program that starts and then exits immediately comes back as a launch that worked, not as a session that failed to start. +- **SESSION-7** I can see at a glance which of my sessions are running. + - **SESSION-7.a** The listing tells me how long each session has been quiet. + - **SESSION-7.b** The listing tells me what each session last printed, so I can tell working from stuck. +- **SESSION-8** What rune says about a session being alive comes from looking at the real processes, not from what was written down last. +- **SESSION-9** Stopping a session leaves nothing of it still running. + - **SESSION-9.a** Stopping has finished by the time it returns, rather than merely having been asked for. +- **SESSION-10** A session that has ended records how it ended, so the program finishing, my stopping it, and a launch that never happened are three different answers. +- **SESSION-11** I can archive a stopped session so its name is free to use again. + - **SESSION-11.a** Archiving keeps the transcript, so I can still read what happened. + - **SESSION-11.b** Reusing a name starts genuinely fresh rather than continuing an older session's transcript. +- **SESSION-12** A program that does not speak Ruby can drive a session directly, without going through the command line. +- **SESSION-13** One session going wrong takes down only itself and leaves my other sessions running. +- **SESSION-14** What a session recorded is readable only by me. +- **SESSION-15** I can keep a set of sessions apart from my usual ones, so a sandbox or a test run never touches them. diff --git a/hi/watch.md b/hi/watch.md new file mode 100644 index 0000000..c798a85 --- /dev/null +++ b/hi/watch.md @@ -0,0 +1,28 @@ +--- +hi: 1 +families: [WATCH] +--- + +# Watching while a person drives + +## Intent + +Sometimes the right thing is for a human to sit at the keyboard and do it themselves, while something else follows along. Watching should feel like using the program directly — keystrokes go through as typed, output appears as it happens — while every byte of the session is quietly recorded somewhere an agent can tail in real time. The recording must never get in the way of the session it is recording, and the session must never be left without a way to end it. + +## Criteria + +- **WATCH-1** I can drive an interactive program myself while something else records the whole session. +- **WATCH-2** My keystrokes reach the program as I type them, including arrow keys and other raw escape sequences rather than only whole lines. +- **WATCH-3** The program's output reaches my screen as it happens rather than all at the end. +- **WATCH-4** Every chunk of the session is logged as it happens, so an agent can follow along live from somewhere else. + - **WATCH-4.a** The log goes to its own file rather than into my terminal, so the events never make the session unreadable. + - **WATCH-4.b** Rune tells me once, up front, where that file is. + - **WATCH-4.c** I can put the log somewhere of my own choosing when something is already waiting to read it there. +- **WATCH-5** When a program is parsing rune's output, my live view moves aside so the parseable answer stays clean. +- **WATCH-6** Rune refuses to watch when there is no real terminal to take over, rather than half-working. +- **WATCH-7** The program sees my terminal's real size. + - **WATCH-7.a** Resizing my window resizes the program with it. +- **WATCH-8** I can bound a watch by wall-clock time. +- **WATCH-9** I can bound a watch by how long it has gone with no output and no typing, which is the bound that means the thing has stopped doing anything. + - **WATCH-9.a** The result says which of the two bounds ended the session. +- **WATCH-10** Ending a watch ends the program I was driving, rather than leaving it orphaned. From 67a40af5853e2265e65b1981ecb0cafe801d7c00 Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Wed, 16 Sep 2026 16:05:59 -0600 Subject: [PATCH 2/3] Update: pin hi to 0.4.0 0.4.0 closes four ways hi could break its only promise, that an id is permanent and never reused. Three were silent, with `hi check` reporting no problem: a retire that landed the criterion outside every section hi reads and freed its id for reuse, a retire reason that could forge a criterion and burn an id nobody wrote, and a criterion inside a fence that was invisible to all six checks. The fourth lost concurrent captures to a shared temp path. This gate exists to protect id integrity, so it should not run on the version that could not. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UcDK2AAqo8w9vjvwA2yitV --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f9c460..69937f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,7 +134,7 @@ jobs: - name: Install hi run: | - cargo install human-intent --version 0.3.3 --locked + cargo install human-intent --version 0.4.0 --locked echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Check intent structure From 8b6be6812182c251cc287d4c49e3a47d0be20621 Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Wed, 16 Sep 2026 22:13:40 -0600 Subject: [PATCH 3/3] Update: pin hi to 0.5.0 0.5.0 changes what `hi check` reads: a file in hi/ whose name is not lowercase is hi's own rather than criteria, and a criterion-shaped line inside one is reported as stray-criterion rather than parsed. Pinning an older version checks these files with rules the tool no longer has. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69937f5..025aa73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,7 +134,7 @@ jobs: - name: Install hi run: | - cargo install human-intent --version 0.4.0 --locked + cargo install human-intent --version 0.5.0 --locked echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Check intent structure