Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.5.0 --locked
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"

- name: Check intent structure
run: hi check
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
.fledge/
coverage/
.specsync/hashes.json
intent.html
19 changes: 19 additions & 0 deletions INTENT.md
Original file line number Diff line number Diff line change
@@ -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

<!-- hi:index -->
- [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)
<!-- /hi:index -->
22 changes: 22 additions & 0 deletions hi/attach.md
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 32 additions & 0 deletions hi/cli.md
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 25 additions & 0 deletions hi/discover.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions hi/install.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 22 additions & 0 deletions hi/library.md
Original file line number Diff line number Diff line change
@@ -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.
26 changes: 26 additions & 0 deletions hi/read.md
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 32 additions & 0 deletions hi/run.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 22 additions & 0 deletions hi/screen.md
Original file line number Diff line number Diff line change
@@ -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.
33 changes: 33 additions & 0 deletions hi/send.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading