@adrrr/tarmac 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -127
- package/dist/fleet.js +66 -2
- package/dist/render.js +54 -13
- package/dist/schema.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,6 +10,26 @@
|
|
|
10
10
|
busy or idle, how full its context is, which model, what it has cost so far. It reads
|
|
11
11
|
documented surfaces only, never an internal format.
|
|
12
12
|
|
|
13
|
+
<picture>
|
|
14
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/media/replay-dark.gif">
|
|
15
|
+
<img src="docs/media/replay-light.gif" width="1100"
|
|
16
|
+
alt="The tarmac map with the scrubber dragged across the record: five sessions drawn as dials, each arc a context window, the account's five-hour and seven-day gauges above them. As the handle moves, sessions appear and disappear, the arcs fill and reset, the five-hour window fills and rolls over, and a banner names the minute being replayed until Back to live is pressed.">
|
|
17
|
+
</picture>
|
|
18
|
+
|
|
19
|
+
[Quickstart](#quickstart) · [The map](#the-map) · [Install](#install) ·
|
|
20
|
+
[Configuration](#configuration) · [Commands](docs/MANUAL.md#commands-and-options) ·
|
|
21
|
+
[Manual](docs/MANUAL.md) · [Changelog](CHANGELOG.md) · [Issues](https://github.com/adrrr/tarmac/issues)
|
|
22
|
+
|
|
23
|
+
## Quickstart
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @adrrr/tarmac # one-shot fleet table
|
|
27
|
+
npx @adrrr/tarmac --watch # the same table, redrawn every 5s until ^C
|
|
28
|
+
npx @adrrr/tarmac serve # the same fleet in the browser
|
|
29
|
+
npx @adrrr/tarmac install # chain the status line: unlocks ctx, model, effort and cost
|
|
30
|
+
npx @adrrr/tarmac uninstall # hand your status line back
|
|
31
|
+
```
|
|
32
|
+
|
|
13
33
|
```
|
|
14
34
|
$ npx @adrrr/tarmac
|
|
15
35
|
|
|
@@ -30,93 +50,52 @@ same command still lists every session, its state and its uptime, straight from
|
|
|
30
50
|
`—`, and the line under the table counts how many sessions are covered.
|
|
31
51
|
|
|
32
52
|
Node ≥ 20. **Zero runtime dependencies** — no framework, no bundler, nothing to audit.
|
|
53
|
+
`--help` works everywhere, and an option handed to a command that does not read it is an
|
|
54
|
+
**error**, not something quietly ignored. Every command, flag and route:
|
|
55
|
+
[the manual](docs/MANUAL.md#commands-and-options).
|
|
33
56
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
## Install
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
npx @adrrr/tarmac # one-shot fleet table
|
|
40
|
-
npx @adrrr/tarmac --watch # the same table, redrawn every 5s until ^C
|
|
41
|
-
npx @adrrr/tarmac serve # the same fleet in the browser
|
|
42
|
-
npx @adrrr/tarmac install # chain the status line: unlocks ctx, model, effort and cost
|
|
43
|
-
npx @adrrr/tarmac uninstall # hand your status line back
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
`install` changes the `statusLine` key of `~/.claude/settings.json`, and never on your
|
|
47
|
-
say-so alone. It prints the whole plan first — including the exact command that undoes it —
|
|
48
|
-
and waits for a **typed word** (`y` is not an answer; scripts pass `--yes`, deliberately):
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
tarmac install — your home
|
|
52
|
-
|
|
53
|
-
file /Users/jane/.claude/settings.json
|
|
54
|
-
statusLine now ~/bin/my-line.sh
|
|
55
|
-
statusLine next /Users/jane/.claude/tarmac/statusline.sh
|
|
56
|
-
↳ which calls ~/bin/my-line.sh (your display is unchanged)
|
|
57
|
-
snapshots /Users/jane/.local/state/tarmac/snapshots
|
|
58
|
-
undo tarmac uninstall
|
|
59
|
-
|
|
60
|
-
Type "install" to proceed, anything else to abort:
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
A status line you already had is **wrapped, not replaced**: its display stays byte-identical,
|
|
64
|
-
and `uninstall` names which of its four restore modes ran — `bytes`, the usual one, puts the
|
|
65
|
-
original file back exactly. Note that the write re-serialises `settings.json`, so a
|
|
66
|
-
version-controlled one shows a formatting diff, not a one-line diff.
|
|
67
|
-
|
|
68
|
-
The only things that land under `~/.claude/` are that wrapper and the `backup.json` that
|
|
69
|
-
undoes it, neither of which changes at runtime: the snapshots the wrapper writes at every
|
|
70
|
-
frame go to `$XDG_STATE_HOME/tarmac/snapshots` (`~/.local/state/tarmac/snapshots` by
|
|
71
|
-
default), because `~/.claude` is a directory people commit. Coming from 0.1.x, `install`
|
|
72
|
-
clears the payloads an older version left in there, says how many and from where, and — when
|
|
73
|
-
`~/.claude` is a git repository — prints the `.gitignore` line worth adding.
|
|
74
|
-
|
|
75
|
-
## The dashboard
|
|
57
|
+
## The map
|
|
76
58
|
|
|
77
59
|
`tarmac serve` puts the same fleet in the browser — every session a row, ages that keep
|
|
78
60
|
climbing, and a banner the moment a refresh fails instead of a table quietly going stale. It
|
|
79
|
-
|
|
61
|
+
binds to loopback and refuses any request whose `Host` is not loopback, or that a browser does
|
|
62
|
+
not mark same-origin: your cwd paths and costs never leave the machine. The listening rules:
|
|
63
|
+
[the manual](docs/MANUAL.md#what-serve-listens-on).
|
|
80
64
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
65
|
+
<picture>
|
|
66
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/media/map-dark.png">
|
|
67
|
+
<img src="docs/media/map-light.png" width="1100"
|
|
68
|
+
alt="The tarmac map, live. Five nodes: beacon halted on a permission prompt, harbor busy with its context arc at 90%, a background agent named after its prompt beside it, quay reporting a status tarmac does not know, and atlas idle at 36%. The account's five-hour and seven-day gauges sit above them, and one warning above the fleet names the unrecognised status rather than filing it as idle.">
|
|
69
|
+
</picture>
|
|
84
70
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
71
|
+
The tab in the header swaps the table for the same fleet as nodes — one per session, the arc
|
|
72
|
+
its context, the shape by the name its state. It is the same reading in the same fragment, so
|
|
73
|
+
the two views can never disagree. Above the fleet are the account's five-hour and seven-day
|
|
74
|
+
[gauges](docs/MANUAL.md#the-accounts-two-windows); under it a **scrubber over the day this
|
|
75
|
+
serve has seen** — drag it and the dials render
|
|
76
|
+
the fleet as it was at that minute, press play and the day walks past.
|
|
89
77
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
78
|
+
The rules the table follows, the map follows: a reading past the freshness threshold is drawn
|
|
79
|
+
thin, amber and dated, a percentage nobody measured is an empty dotted dial rather than a ring
|
|
80
|
+
at zero, and a word `claude agents --json` printed that tarmac has no boolean for is shown as
|
|
81
|
+
it came rather than quietly filed as `idle` — and raises one warning above the fleet naming
|
|
82
|
+
it. The page is JSON underneath: `GET /api/fleet` and `GET /api/history` serve exactly what
|
|
83
|
+
the map draws. Details in
|
|
84
|
+
[the manual](docs/MANUAL.md#the-map).
|
|
97
85
|
|
|
98
|
-
|
|
86
|
+
## Install
|
|
99
87
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
88
|
+
`install` changes the `statusLine` key of `~/.claude/settings.json`, and never on your
|
|
89
|
+
say-so alone. It prints the whole plan first — including the exact command that undoes it —
|
|
90
|
+
and waits for a **typed word** (`y` is not an answer; scripts pass `--yes`, deliberately).
|
|
103
91
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Under the map is a **scrubber over the day this serve has seen**. Drag it and the dials render
|
|
113
|
-
the fleet as it was at that minute; press play and the day walks past. The record is fetched on
|
|
114
|
-
load, never per position, so scrubbing asks the server nothing. A replay never poses as the present: a banner names
|
|
115
|
-
the minute and holds the way back, the live fragment is hidden while it is up, halos stay off
|
|
116
|
-
because a sample never "just landed", and a session absent from a minute is absent from the
|
|
117
|
-
map. The range says what it truly covers — a serve ten minutes old offers ten minutes. The
|
|
118
|
-
account's gauges replay too, counted from the minute being shown rather than from now, so the
|
|
119
|
-
five-hour window can be watched draining and refilling across a day.
|
|
92
|
+
A status line you already had is **wrapped, not replaced**: its display stays byte-identical,
|
|
93
|
+
and `uninstall` names which of its four restore modes ran. Two files land under `~/.claude/` —
|
|
94
|
+
the wrapper and the `backup.json` that undoes it — and neither changes at runtime: the
|
|
95
|
+
snapshots go to `~/.local/state/tarmac/snapshots` (`$XDG_STATE_HOME` when set), because
|
|
96
|
+
`~/.claude` is a directory people commit. The plan, the restore modes and the cleanup of the
|
|
97
|
+
layout that kept snapshots inside `.claude`:
|
|
98
|
+
[the manual](docs/MANUAL.md#installing-safely--the-full-contract).
|
|
120
99
|
|
|
121
100
|
## Why it does not break
|
|
122
101
|
|
|
@@ -135,55 +114,28 @@ That second line is the honest caveat, and it is the reason the real defence is
|
|
|
135
114
|
immunity, it is **visible degradation**. A missing measurement is never a confident `0`: it
|
|
136
115
|
is an em dash that names which kind of missing it is — `absent` for a session no status line
|
|
137
116
|
ever wrote for, `fresh` for one that has taken no turn yet, `drift` for a release that moved
|
|
138
|
-
the payload out from under us
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
table is in [`docs/MANUAL.md`](docs/MANUAL.md).
|
|
143
|
-
|
|
144
|
-
## Commands
|
|
145
|
-
|
|
146
|
-
| Command | What it does | Options |
|
|
147
|
-
|---|---|---|
|
|
148
|
-
| `tarmac list` | one-shot fleet table — the default, so bare `tarmac` runs it | `--home`, `--stale-after`, `--snapshots-dir`, `--claude-bin`, `--json`, `--watch` |
|
|
149
|
-
| `tarmac serve` | local dashboard, `GET /` for the table, `GET /map` for the map, `GET /live` for the fragment both refresh from, `GET /api/fleet` for JSON, `GET /api/history` for the last 24h of readings it took while it ran | `--home`, `--port`, `--stale-after`, `--snapshots-dir`, `--claude-bin` |
|
|
150
|
-
| `tarmac install` | chain the status line under `<home>/.claude/settings.json`, after confirmation | `--home`, `--yes` |
|
|
151
|
-
| `tarmac uninstall` | restore it, and say which of the four restore modes ran | `--home`, `--yes` |
|
|
152
|
-
|
|
153
|
-
`--help` works everywhere. An option handed to a command that does not read it is an
|
|
154
|
-
**error**, not something quietly ignored — and the error names the commands it does belong
|
|
155
|
-
to. Both live views tell you **when the last good reading arrived** and **whether the last
|
|
156
|
-
refresh failed**: ages keep climbing whether or not the refresh works, failures are banners
|
|
157
|
-
with names, and a table is never thrown away for one.
|
|
117
|
+
the payload out from under us. A stale reading keeps its value and gets **dated** with a `!`.
|
|
118
|
+
A Claude Code build no fixture covers gets its version named *before* anything breaks, and
|
|
119
|
+
tarmac keeps reporting. The full state-by-state table is in
|
|
120
|
+
[the manual](docs/MANUAL.md#degradation-state-by-state).
|
|
158
121
|
|
|
159
122
|
## Configuration
|
|
160
123
|
|
|
161
124
|
Three numbers are opinions, not truths, so all three are yours; everything else is
|
|
162
125
|
deliberately not configurable, and all of it works with no configuration at all.
|
|
163
126
|
|
|
164
|
-
| Setting | Flag |
|
|
165
|
-
|
|
166
|
-
| freshness threshold | `--stale-after 90s` \| `15m` \| `2h` | `
|
|
167
|
-
| port | `--port 8080` | `
|
|
168
|
-
| snapshots dir (read side) | `--snapshots-dir DIR` |
|
|
127
|
+
| Setting | Flag | Default |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| freshness threshold | `--stale-after 90s` \| `15m` \| `2h` | `10m` |
|
|
130
|
+
| port | `--port 8080` | `4477` |
|
|
131
|
+
| snapshots dir (read side) | `--snapshots-dir DIR` | the path frozen into the installed wrapper, so the reader follows the writer |
|
|
169
132
|
|
|
133
|
+
Each also has an environment variable and a key in `<home>/.claude/tarmac/config.json`.
|
|
170
134
|
**Flag beats environment beats config file beats default**, settled per setting; `serve`
|
|
171
135
|
opens by printing each effective value and where it came from. Nothing is silently dropped
|
|
172
136
|
or silently corrected — a value that will not parse stops the run and says what it got,
|
|
173
|
-
where it came from, and what would have worked,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
That last default is read out of the installed wrapper, never recomputed: a reader that
|
|
177
|
-
recomputed it would disagree with the writer the moment the two saw different environments
|
|
178
|
-
— `XDG_STATE_HOME` exported in your shell, absent from a LaunchAgent or cron — and the
|
|
179
|
-
symptom is a healthy, empty fleet, the one failure a fleet monitor may not have. Anything
|
|
180
|
-
else reading those payloads can ask where they are: `tarmac list --json` reports the path as
|
|
181
|
-
`health.snapshotsDir`. Full rules and edge cases: [`docs/MANUAL.md`](docs/MANUAL.md).
|
|
182
|
-
|
|
183
|
-
`tarmac list --json` also reports `health.unfilable`: how many live sessions carry an id
|
|
184
|
-
tarmac will never file a snapshot under, so a reader can tell telemetry that is *late* from
|
|
185
|
-
telemetry that is *not coming*. See [`docs/MANUAL.md`](docs/MANUAL.md) for what makes an id
|
|
186
|
-
filable.
|
|
137
|
+
where it came from, and what would have worked. Spellings, edge cases and the two health
|
|
138
|
+
fields `tarmac list --json` reports: [the manual](docs/MANUAL.md#configuration).
|
|
187
139
|
|
|
188
140
|
## What it deliberately does not do
|
|
189
141
|
|
|
@@ -192,25 +144,26 @@ filable.
|
|
|
192
144
|
What it will not do is guess at the rest: a session that asked you a question in prose
|
|
193
145
|
still reports `idle`, and the only way to know better is to read a transcript, which is
|
|
194
146
|
the one thing this tool will not do. The signal is as good as the surface, and no better.
|
|
195
|
-
- **No history.**
|
|
147
|
+
- **No history on disk.** `tarmac list` is a snapshot in time. A running `serve` holds the
|
|
148
|
+
last 24 hours of the readings it took itself, in memory, so the page can replay them — a
|
|
149
|
+
record that reaches no further back than the serve that took it, and goes when it goes.
|
|
150
|
+
None of it is written down.
|
|
196
151
|
- **No Windows.** The generated wrapper is POSIX `sh`.
|
|
197
152
|
- **No remote fleets.** It watches the machine it runs on.
|
|
198
153
|
|
|
199
154
|
## Development
|
|
200
155
|
|
|
201
156
|
```bash
|
|
202
|
-
npm test
|
|
203
|
-
npm run build
|
|
157
|
+
npm test # typecheck (src + test + scripts), then run the suite
|
|
158
|
+
npm run build # flat JavaScript into dist/
|
|
159
|
+
node scripts/demo-fleet.ts # the invented fleet the captures above are taken of
|
|
204
160
|
```
|
|
205
161
|
|
|
206
|
-
|
|
207
|
-
machine
|
|
208
|
-
|
|
209
|
-
the
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
The suite runs the TypeScript sources directly through Node's type stripping, so it needs
|
|
213
|
-
Node ≥ 22.18 to *develop*; what ships in `dist/` is plain ES2022 and runs on Node ≥ 20.
|
|
162
|
+
Every capture on this page is taken of a fleet that does not exist, because a screenshot of a
|
|
163
|
+
real machine carries working directories, prompts and costs, and nothing real enters this
|
|
164
|
+
repo. What CI covers, which Node version develops and which one ships:
|
|
165
|
+
[the manual](docs/MANUAL.md#developing). Capturing the fixtures for a new Claude Code build:
|
|
166
|
+
[the manual](docs/MANUAL.md#capturing-a-new-claude-code-version).
|
|
214
167
|
|
|
215
168
|
## License
|
|
216
169
|
|
package/dist/fleet.js
CHANGED
|
@@ -96,6 +96,62 @@ export function buildFleet({ sessions, snapshots, now, staleAfterMs = DEFAULT_ST
|
|
|
96
96
|
},
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* How many sessions are working RIGHT NOW while looking at a reading that has gone cold, on a
|
|
101
|
+
* fleet where not one reading is fresh — and `0` whenever that whole picture does not hold.
|
|
102
|
+
*
|
|
103
|
+
* It exists because `health.stale` on its own is not news (#53). A statusline is written when
|
|
104
|
+
* a terminal draws a frame, so a session that idles overnight keeps yesterday's number and
|
|
105
|
+
* "N readings are stale" is what a resting fleet LOOKS like — the steady state, said again on
|
|
106
|
+
* every poll. The rows and the map nodes already date each reading one by one; a page-wide
|
|
107
|
+
* banner repeating it is wallpaper, and a warning nobody can ever act on trains the reader to
|
|
108
|
+
* skip the ones they can.
|
|
109
|
+
*
|
|
110
|
+
* What is not the steady state is this shape:
|
|
111
|
+
*
|
|
112
|
+
* • not one reading anywhere is fresh — so nothing is writing, rather than some sessions
|
|
113
|
+
* resting. A single fresh reading is proof the writer works, and ends the question.
|
|
114
|
+
* • and at least one of those cold readings belongs to a session that is BUSY. A busy
|
|
115
|
+
* session redraws its status line constantly, so its snapshot should be seconds old. Cold
|
|
116
|
+
* is the wrapper gone, the snapshot directory unwritable, a disk full — the writer, not
|
|
117
|
+
* the fleet.
|
|
118
|
+
*
|
|
119
|
+
* Both halves are needed. Busy-and-cold beside a fresh reading is one session's business — a
|
|
120
|
+
* terminal in a tmux window nobody has selected draws no frames while its session works, so
|
|
121
|
+
* its reading ages exactly like an idle one; everything cold with nobody busy is just the
|
|
122
|
+
* night.
|
|
123
|
+
*
|
|
124
|
+
* Readings only — a session with no snapshot has nothing that could have gone cold, and is the
|
|
125
|
+
* coverage warning's.
|
|
126
|
+
*
|
|
127
|
+
* Known window, and the reason there is no floor under it: a session that has just been given
|
|
128
|
+
* something to do is `busy` on the spine before its first frame lands, so a fleet waking from
|
|
129
|
+
* a quiet night can raise this for one poll. A grace period would be a second threshold
|
|
130
|
+
* nobody set, and the honest reading of that moment is that the newest reading on the machine
|
|
131
|
+
* is still hours old.
|
|
132
|
+
*/
|
|
133
|
+
export function busyOnStaleFleet(rows) {
|
|
134
|
+
// Every dated snapshot, INCLUDING one dated after the clock that read it. That is the whole
|
|
135
|
+
// handling of clock skew here, and it is deliberate: such a reading is never `stale` (a
|
|
136
|
+
// negative age is not greater than any threshold), so leaving it in the denominator makes
|
|
137
|
+
// `every` below fail and the verdict come to nothing — which is the answer we want. A
|
|
138
|
+
// snapshot the filesystem dates in the future may have been written a second ago, and that
|
|
139
|
+
// is the one fact that would prove the writer is alive, so it must not be filtered into
|
|
140
|
+
// silence. Excluding it was the first cut, and it let the page print "every context reading
|
|
141
|
+
// is stale" directly above its own warning naming the reading that was not.
|
|
142
|
+
//
|
|
143
|
+
// Not the same call `accountLimits` below makes, which refuses a skewed reading so it cannot
|
|
144
|
+
// WIN on freshness. The question there is which reading is youngest; here it is whether
|
|
145
|
+
// anything wrote at all, and for that an unreadable date must not be allowed to accuse.
|
|
146
|
+
const readings = rows.filter((r) => r.snapshotAgeMs !== null);
|
|
147
|
+
// A fleet with no readings needs no clause of its own: nothing is what `every` is vacuously
|
|
148
|
+
// true of, and nothing is also what the count below comes to.
|
|
149
|
+
if (!readings.every((r) => r.stale))
|
|
150
|
+
return 0;
|
|
151
|
+
// Strictly `true`: `null` is "tarmac cannot read this session's status", and a session that
|
|
152
|
+
// may or may not be working is not evidence that anything stopped.
|
|
153
|
+
return readings.filter((r) => r.busy === true).length;
|
|
154
|
+
}
|
|
99
155
|
/**
|
|
100
156
|
* The account's rate limits, as this fleet's sessions report them.
|
|
101
157
|
*
|
|
@@ -124,6 +180,14 @@ export function accountLimits(rows) {
|
|
|
124
180
|
}
|
|
125
181
|
return freshest;
|
|
126
182
|
}
|
|
127
|
-
//
|
|
128
|
-
|
|
183
|
+
// Blocked on a human first, then busy, then unknown (it might be busy), then idle.
|
|
184
|
+
//
|
|
185
|
+
// Waiting leads because it is the only rank that is work for the reader: everything below it
|
|
186
|
+
// is the fleet reporting on itself, while a waiting session has STOPPED and will not start
|
|
187
|
+
// again until someone answers it. Ranking it by how busy it is — the question the rest of this
|
|
188
|
+
// sort asks — is what filed it with `unknown`, one bucket under a fleet that is mostly busy,
|
|
189
|
+
// which on the map is under the fold. It costs the ranks below it almost nothing: a fleet has
|
|
190
|
+
// one or two of these at a time, so busy moves down a row or two, and losing sight of a busy
|
|
191
|
+
// session for a poll is not a thing that can go wrong. Losing sight of a waiting one is.
|
|
192
|
+
const rank = (r) => isWaiting(r) ? 0 : r.busy === true ? 1 : r.busy === null ? 2 : 3;
|
|
129
193
|
const round2 = (n) => Math.round(n * 100) / 100;
|
package/dist/render.js
CHANGED
|
@@ -10,7 +10,7 @@ import { formatDuration } from './config.js';
|
|
|
10
10
|
import { buildMap, INTERACTIVE, stateOf } from './map.js';
|
|
11
11
|
import { schemaNotice } from './schema.js';
|
|
12
12
|
import { LIMIT_WINDOWS, RESET_HORIZON_MS, readLimits } from './limits.js';
|
|
13
|
-
import { accountLimits } from './fleet.js';
|
|
13
|
+
import { accountLimits, busyOnStaleFleet } from './fleet.js';
|
|
14
14
|
/**
|
|
15
15
|
* The other thing this module renders: the plan a user consents to before install or
|
|
16
16
|
* uninstall touches their settings.json. Everything the decision rests on has to be here —
|
|
@@ -278,16 +278,36 @@ export function renderLive(fleet) {
|
|
|
278
278
|
if (health.unknownStatus > 0) {
|
|
279
279
|
warnings.push(`${health.unknownStatus} session(s) report a status tarmac does not know — treated as unknown, not idle.`);
|
|
280
280
|
}
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
// NOT "N readings are stale", which used to live here and was on every hour of every day
|
|
282
|
+
// (#53): a statusline is written when a terminal draws a frame, so a fleet that idles keeps
|
|
283
|
+
// yesterday's numbers and says so on every poll. The rows and the nodes date each reading
|
|
284
|
+
// themselves — a page-wide box repeating it is wallpaper, and wallpaper is what teaches a
|
|
285
|
+
// reader to skip the boxes below. What is left here is the one stale-shaped thing that is
|
|
286
|
+
// an event: `busyOnStaleFleet` (see fleet.ts for why both halves of it are needed).
|
|
287
|
+
const stalled = busyOnStaleFleet(rows);
|
|
288
|
+
if (stalled > 0) {
|
|
289
|
+
warnings.push(`Every context reading is stale, including ${stalled} session(s) busy right now — a busy session redraws its status line, so its reading should not be older than ${formatDuration(health.staleAfterMs)}. The statusline writer looks stopped rather than the fleet idle: check that the wrapper is still installed and that the snapshot directory is writable.`);
|
|
283
290
|
}
|
|
284
291
|
const skewed = rows.filter(ahead).length;
|
|
285
292
|
if (skewed > 0) {
|
|
286
293
|
warnings.push(`${skewed} reading(s) are dated in the future — ${SKEW}. They are shown undated rather than as brand new.`);
|
|
287
294
|
}
|
|
295
|
+
// Under the fleet, at a footnote's weight: two facts that are true, worth keeping, and worth
|
|
296
|
+
// nobody's alarm. The first is the legend for the marks the rows carry — a `!` whose
|
|
297
|
+
// threshold is invisible is a mark the reader cannot argue with, which is why demoting the
|
|
298
|
+
// banner above could not take the number with it. The second is a maintainer's line: it
|
|
299
|
+
// stands for every user of a released tarmac until the next release ships the fixture, so
|
|
300
|
+
// amber would mean amber forever. Both keep every word they had.
|
|
301
|
+
const notes = [];
|
|
302
|
+
// Not under the stall banner, which names the same threshold two lines up: the pair reads as
|
|
303
|
+
// the alarm followed by its own excuse, and the excuse is the reading the alarm exists to
|
|
304
|
+
// tell you not to accept.
|
|
305
|
+
if (health.stale > 0 && stalled === 0) {
|
|
306
|
+
notes.push(`Readings past the ${formatDuration(health.staleAfterMs)} freshness threshold are dated where they sit — a statusline is only written when its terminal draws a frame, so an idle session's number is "as of" its last one. Set another with --stale-after.`);
|
|
307
|
+
}
|
|
288
308
|
const schema = schemaNotice(health.schemaGuard);
|
|
289
309
|
if (schema)
|
|
290
|
-
|
|
310
|
+
notes.push(schema);
|
|
291
311
|
// Both views, every time, out of the one reading the page just asked for. The tabs are
|
|
292
312
|
// links and the shell decides which of the two is visible, so a fleet cannot be drawn as a
|
|
293
313
|
// table of one age beside a map of another.
|
|
@@ -297,17 +317,23 @@ export function renderLive(fleet) {
|
|
|
297
317
|
// on screen and read out all the same by anything going through the markup.
|
|
298
318
|
const body = rows.length === 0
|
|
299
319
|
? empty(health)
|
|
300
|
-
:
|
|
320
|
+
: // `aria-describedby` on both views, because demoting the footnote moved it BELOW every
|
|
321
|
+
// row and every node: a reader going through the markup now meets `! 3h ago` N times
|
|
322
|
+
// before anything says what threshold put it there. Sighted readers glance down; this
|
|
323
|
+
// is the same glance for anyone who cannot. The target is rendered whether or not it
|
|
324
|
+
// has anything in it, so the reference is never dangling.
|
|
325
|
+
`<div class="view view-table"><div class="wrap"><table aria-describedby="fleet-notes">
|
|
301
326
|
<thead><tr>
|
|
302
327
|
<th>Project</th><th>Session</th><th>State</th><th>Context</th><th>Model</th><th>Effort</th><th>Cost</th><th>Uptime</th>
|
|
303
328
|
</tr></thead>
|
|
304
329
|
<tbody>${rows.map(renderRow).join('')}</tbody>
|
|
305
330
|
</table></div></div>
|
|
306
|
-
<div class="view view-map">${renderMap(fleet)}</div>`;
|
|
331
|
+
<div class="view view-map" role="group" aria-label="fleet map" aria-describedby="fleet-notes">${renderMap(fleet)}</div>`;
|
|
307
332
|
return `<div id="limits-src" hidden>${renderLimits(fleet)}</div>
|
|
308
333
|
<div class="meta">${health.sessions} session${health.sessions === 1 ? '' : 's'} · ${health.busy} busy · ${cost(health)} · ${esc(new Date(health.generatedAt).toISOString())}</div>
|
|
309
334
|
${warnings.map((w) => `<div class="warn">${esc(w)}</div>`).join('')}
|
|
310
|
-
${body}
|
|
335
|
+
${body}
|
|
336
|
+
<div id="fleet-notes">${notes.map((n) => `<div class="note">${esc(n)}</div>`).join('')}</div>`;
|
|
311
337
|
}
|
|
312
338
|
/**
|
|
313
339
|
* The account's two windows, for the page's header.
|
|
@@ -463,6 +489,11 @@ export function renderPage(fleet, view = 'table') {
|
|
|
463
489
|
.warn { background:var(--warnbg); color:var(--warn); border:1px solid currentColor; border-radius:6px;
|
|
464
490
|
padding:.35rem .65rem; margin:.3rem 0; font-size:.8rem; line-height:1.45; }
|
|
465
491
|
.warn:last-of-type { margin-bottom:.9rem; }
|
|
492
|
+
/* The footnote: same words, none of the weight. Dim, small, below the fleet and with no box
|
|
493
|
+
around it, because what it carries is true rather than urgent — the threshold that dated a
|
|
494
|
+
reading, the payload shapes nobody has captured yet. It reads as chrome to someone
|
|
495
|
+
scanning their sessions and as an answer to someone who came looking for it. */
|
|
496
|
+
.note { color:var(--dim); font-size:.75rem; line-height:1.5; margin:.9rem 0 0; max-width:95ch; }
|
|
466
497
|
.stale { color:var(--warn); font-weight:600; }
|
|
467
498
|
.wrap { overflow-x:auto; }
|
|
468
499
|
table { border-collapse:collapse; width:100%; min-width:44rem; }
|
|
@@ -479,7 +510,8 @@ export function renderPage(fleet, view = 'table') {
|
|
|
479
510
|
.pill.waiting { color:var(--wait); }
|
|
480
511
|
.pill.unknown { color:var(--warn); }
|
|
481
512
|
.pill.idle { color:var(--dim); font-weight:400; }
|
|
482
|
-
/*
|
|
513
|
+
/* Accented states carry their own hue down the row edge. Bold stays on busy alone — it
|
|
514
|
+
says "working", not "read me first"; waiting's weight is the top of the sort. */
|
|
483
515
|
td:first-child { border-left:3px solid transparent; }
|
|
484
516
|
tr[data-state="busy"] td:first-child { border-left-color:var(--busy); }
|
|
485
517
|
tr[data-state="waiting"] td:first-child { border-left-color:var(--wait); }
|
|
@@ -618,9 +650,17 @@ export function renderPage(fleet, view = 'table') {
|
|
|
618
650
|
/* Once per arrival, not forever: the fragment is replaced on every poll, so a single run
|
|
619
651
|
per swap is what makes the fleet breathe at the rate its frames actually land. A looping
|
|
620
652
|
animation would say "a frame just arrived" for five seconds after it stopped being true. */
|
|
621
|
-
|
|
653
|
+
/* What the halo says is that a frame landed, and it says it by being there at all. Its
|
|
654
|
+
COLOUR is free, and it was spending it on a claim: stroked with the busy hue under a lone
|
|
655
|
+
idle override, it pulsed green over an unrecognised status and — since the fourth state — over
|
|
656
|
+
a session halted on a human, in the hue of the one thing it is certainly not doing. Same
|
|
657
|
+
palette as the glyph under the name, off the same four states, so the two channels drawing
|
|
658
|
+
one node cannot end up disagreeing about it. */
|
|
659
|
+
.halo { fill:none; stroke:var(--dim); stroke-width:2; opacity:0; transform-origin:50% 50%;
|
|
622
660
|
animation:halo 1.6s ease-out 1; }
|
|
623
|
-
.node[data-state="
|
|
661
|
+
.node[data-state="busy"] .halo { stroke:var(--busy); }
|
|
662
|
+
.node[data-state="waiting"] .halo { stroke:var(--wait); }
|
|
663
|
+
.node[data-state="unknown"] .halo { stroke:var(--warn); }
|
|
624
664
|
@keyframes halo { from { opacity:.5; transform:scale(1); } to { opacity:0; transform:scale(1.22); } }
|
|
625
665
|
/* Motion is the one thing here nobody can look away from, so it is the first thing a
|
|
626
666
|
reader who asked for less of it stops getting. The reading is still readable without it. */
|
|
@@ -1208,9 +1248,10 @@ function pageScript(view) {
|
|
|
1208
1248
|
`;
|
|
1209
1249
|
}
|
|
1210
1250
|
/**
|
|
1211
|
-
* The sort puts
|
|
1212
|
-
*
|
|
1213
|
-
* for the ones that are
|
|
1251
|
+
* The sort puts waiting first — the one row that is work for the reader — then busy, then
|
|
1252
|
+
* unknown, idle last. This is where that order is given its weight — an accent down the row
|
|
1253
|
+
* in the state's own hue, a bold name for the ones that are working, a quiet row for the
|
|
1254
|
+
* ones that are not.
|
|
1214
1255
|
*
|
|
1215
1256
|
* The state travels three ways at once: a shape, a word, and an attribute. Colour alone is
|
|
1216
1257
|
* no signal to a reader who cannot separate two of ours, and `data-state` is what the narrow
|
package/dist/schema.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adrrr/tarmac",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Fleet observability for Claude Code — reads documented surfaces only, never an internal format",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
37
37
|
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
38
38
|
"typecheck": "tsc -p tsconfig.json",
|
|
39
|
-
"test": "npm run typecheck && node --test --test-timeout=120000
|
|
39
|
+
"test": "npm run typecheck && node --test --test-timeout=120000 \"test/*.test.ts\"",
|
|
40
40
|
"fixtures:capture": "node scripts/capture-fixtures.ts",
|
|
41
41
|
"prepack": "npm run build",
|
|
42
42
|
"prepublishOnly": "npm run build && TARMAC_REQUIRE_DASH=1 npm test && node dist/cli.js --help > /dev/null"
|