@adrrr/tarmac 0.9.0 → 0.10.0
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 +17 -13
- package/dist/demo-history.js +72 -12
- package/dist/fleet.js +14 -4
- package/dist/history-range.js +2 -0
- package/dist/history-view.js +239 -58
- package/dist/map.js +6 -22
- package/dist/render.js +609 -111
- package/dist/schema.js +12 -3
- package/dist/sessions.js +25 -0
- package/dist/wrapper.js +27 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ documented surfaces only, never an internal format.
|
|
|
13
13
|
<picture>
|
|
14
14
|
<source media="(prefers-color-scheme: dark)" srcset="docs/media/replay-dark.gif">
|
|
15
15
|
<img src="docs/media/replay-light.gif" width="1100"
|
|
16
|
-
alt="The tarmac map, live and then replayed. It opens on the live fleet
|
|
16
|
+
alt="The tarmac map, live and then replayed. It opens on the live fleet: nodes grouped into a frame per working directory, each frame labelled with its project, sessions drawn as dials whose arc is the context window, background agents docked under the cards as strips of text, and the account's five-hour and seven-day gauges in the header. As the scrubber under the fleet is dragged the frames give way to the flat record, every node in a cell of one size, sessions still dials and agents still strips, each with its project and what it has cost. Sessions appear as the day walks past and the grid gains a cell for each, the arcs fill, the five-hour gauge fills and rolls over to zero, and a banner names the minute being replayed until Back to live is pressed and the frames come back.">
|
|
17
17
|
</picture>
|
|
18
18
|
|
|
19
19
|
[Quickstart](#quickstart) · [The map](#the-map) · [The curves](#the-curves) · [Install](#install) ·
|
|
@@ -26,15 +26,15 @@ documented surfaces only, never an internal format.
|
|
|
26
26
|
npx @adrrr/tarmac # one-shot fleet table
|
|
27
27
|
npx @adrrr/tarmac --watch # the same table, redrawn every 5s until ^C
|
|
28
28
|
npx @adrrr/tarmac serve # the same fleet in the browser
|
|
29
|
-
npx @adrrr/tarmac serve --demo # an invented fleet with a
|
|
29
|
+
npx @adrrr/tarmac serve --demo # an invented fleet with a month of history, to see it full
|
|
30
30
|
npx @adrrr/tarmac install # chain the status line: unlocks ctx, model, effort and cost
|
|
31
31
|
npx @adrrr/tarmac uninstall # hand your status line back
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
On a fresh machine the first three of those are one session and no history, which is not much
|
|
35
35
|
of a dashboard. `serve --demo` is the same dashboard over eight invented sessions, a day of
|
|
36
|
-
their record and a
|
|
37
|
-
curves have something in them. It reads no fleet and writes nothing — the
|
|
36
|
+
their record and a month of journal under that, so the map, the replay and every range of the
|
|
37
|
+
curves have something in them. It reads no fleet and writes nothing — the month it shows is
|
|
38
38
|
invented in memory, like the rest of it — and the page it serves is badged `demo data`
|
|
39
39
|
throughout.
|
|
40
40
|
|
|
@@ -84,7 +84,7 @@ listening rules are [there too](docs/MANUAL.md#what-serve-listens-on).
|
|
|
84
84
|
<picture>
|
|
85
85
|
<source media="(prefers-color-scheme: dark)" srcset="docs/media/map-dark.png">
|
|
86
86
|
<img src="docs/media/map-light.png" width="1100"
|
|
87
|
-
alt="The tarmac map, live.
|
|
87
|
+
alt="The tarmac map, live. Five framed groups, one per working directory and labelled with its project: auth-service, holding a session halted on a permission prompt at 61%; api-gateway, holding a session busy with its context arc at 93% and a background agent docked under the cards of that frame as a strip named after its prompt; data-pipeline, busy at 69% with an agent of its own; docs-site, holding one session reporting a status tarmac does not know and one idle at 39%; and storefront, idle at 23%. A frame says only that its nodes were read in the same directory. Nothing inside one claims that any node dispatched another. The account's five-hour and seven-day gauges sit in the header, one warning above the fleet names the unrecognised status rather than filing it as idle, and the scrubber for the replay sits under the fleet.">
|
|
88
88
|
</picture>
|
|
89
89
|
|
|
90
90
|
The tabs in the header swap the table for the same fleet drawn as nodes: one per session, the
|
|
@@ -103,10 +103,13 @@ session gets its own frame rather than somebody else's.
|
|
|
103
103
|
Above the fleet are the account's five-hour and seven-day
|
|
104
104
|
[gauges](docs/MANUAL.md#the-accounts-two-windows). Under it is a scrubber over the day this
|
|
105
105
|
serve has seen. Drag it and the nodes render the fleet as it was at that minute. Press play
|
|
106
|
-
and the day walks past. Replayed, the nodes are drawn flat and unframed
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
and the day walks past. Replayed, the nodes are drawn flat and unframed, every one of them in a
|
|
107
|
+
cell of one size: a session that appears between two minutes of a scrub moves nothing but the
|
|
108
|
+
nodes after it in the sample's own order, and the banner naming the minute stands in a place the
|
|
109
|
+
page keeps for it. A row the grid gains still pushes what is under it, and on a phone the banner
|
|
110
|
+
wraps where the live line does not. The record keeps a project name and never the directory it
|
|
111
|
+
was read in, and a basename is not a directory. Two checkouts of `atlas` answer to the same
|
|
112
|
+
word, so a frame drawn on it would claim a shared directory nobody can check.
|
|
110
113
|
|
|
111
114
|
The map follows the rules the table follows. A reading past the freshness threshold is drawn
|
|
112
115
|
thin, amber and dated. A percentage nobody measured is an empty dotted dial, never a ring at
|
|
@@ -126,7 +129,7 @@ the journal, if you keep one. `<canvas>` and the page's own script, no library.
|
|
|
126
129
|
<picture>
|
|
127
130
|
<source media="(prefers-color-scheme: dark)" srcset="docs/media/curves-dark.png">
|
|
128
131
|
<img src="docs/media/curves-light.png" width="1100"
|
|
129
|
-
alt="The curves page of the demo fleet, on the 7d range out of the invented journal. Context is a row of small multiples, one
|
|
132
|
+
alt="The curves page of the demo fleet, on the 7d range out of the invented journal. Context is a row of small multiples, one band per project: four sawtooths that climb through the day and break where the session was recycled, and storefront, a short stroke a day near the baseline. None carries the arrow that marks a climbing line, since the panel counts nothing climbing over this range. Cost is one stacked bar per day in a fixed project order, seven days between $128 and $221. Quota is the seven-day line climbing over the five-hour window's highs. A banner above names one unrecognised status, and the header is badged demo data.">
|
|
130
133
|
</picture>
|
|
131
134
|
|
|
132
135
|
A context line breaks where its session did, so a recycle at three in the morning reads as a
|
|
@@ -234,11 +237,12 @@ and what would have worked. Spellings, edge cases and the two health fields
|
|
|
234
237
|
```bash
|
|
235
238
|
npm test # typecheck (src + test + scripts), then run the suite
|
|
236
239
|
npm run build # flat JavaScript into dist/
|
|
237
|
-
node scripts/demo-fleet.ts #
|
|
240
|
+
node scripts/demo-fleet.ts # a second invented fleet, served on a port
|
|
238
241
|
```
|
|
239
242
|
|
|
240
|
-
Every capture on this page is taken of a fleet that does not exist. A
|
|
241
|
-
machine carries working directories, prompts and costs, and nothing real
|
|
243
|
+
Every capture on this page is taken of `serve --demo`, a fleet that does not exist. A
|
|
244
|
+
screenshot of a real machine carries working directories, prompts and costs, and nothing real
|
|
245
|
+
enters this repo.
|
|
242
246
|
What CI covers, which Node version develops and which one ships:
|
|
243
247
|
[the manual](docs/MANUAL.md#developing). Capturing the fixtures for a new Claude Code build:
|
|
244
248
|
[the manual](docs/MANUAL.md#capturing-a-new-claude-code-version).
|
package/dist/demo-history.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The
|
|
1
|
+
// The month of journal `serve --demo` shows, invented in memory.
|
|
2
2
|
//
|
|
3
3
|
// Why it ships. `/history` was the one page of the demo that showed the product switched off:
|
|
4
4
|
// "History is off.", the 7d and 30d pills greyed out, nothing for the range charts or the
|
|
@@ -18,8 +18,11 @@
|
|
|
18
18
|
// and it exists to be reported and never to be opened.
|
|
19
19
|
//
|
|
20
20
|
// One story, not two. Every record is `demoFleetAt` played through the same `record` a real
|
|
21
|
-
// sampler calls and then through the store's own allowlist, so
|
|
22
|
-
//
|
|
21
|
+
// sampler calls and then through the store's own allowlist, so every minute the journal and the
|
|
22
|
+
// ring share is the same reading in both — which is the last TWO calendar days, the ring being
|
|
23
|
+
// twenty-four hours long and midnights being where they are. The days before those are that same
|
|
24
|
+
// day again, scaled: see `demoDayFactor`, which is where a month of columns stops being a month
|
|
25
|
+
// of one column.
|
|
23
26
|
import { demoFleetAt, DEMO_HOME, DEMO_MINUTES } from './demo.js';
|
|
24
27
|
import { createHistory } from './history.js';
|
|
25
28
|
import { readRange } from './history-range.js';
|
|
@@ -27,11 +30,48 @@ import { journalRecordOf } from './history-store.js';
|
|
|
27
30
|
/**
|
|
28
31
|
* How many local days of it there are, today included.
|
|
29
32
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
+
* Thirty, which is what the longest range asks for. A week answered both pills with the same
|
|
34
|
+
* seven columns, so `30d` and `7d` drew the identical picture on the one serve built to show
|
|
35
|
+
* them full — a range showing what exists is honest of a real journal younger than its
|
|
36
|
+
* retention, and pointless of a past that is invented anyway. Four times the records, and the
|
|
37
|
+
* whole month is built in about a third of a second.
|
|
33
38
|
*/
|
|
34
|
-
export const DEMO_JOURNAL_DAYS =
|
|
39
|
+
export const DEMO_JOURNAL_DAYS = 30;
|
|
40
|
+
/** A local day, in milliseconds. Used to count days apart, never to walk from one to the next. */
|
|
41
|
+
const DAY = 86_400_000;
|
|
42
|
+
/**
|
|
43
|
+
* What a day of the invented past cost, as a multiple of the day the ring holds.
|
|
44
|
+
*
|
|
45
|
+
* A month of identical columns at $147 is a chart that says "made up" before the reader has read
|
|
46
|
+
* the axis. A fleet's week has a shape — weekends nobody works, an ordinary run of weekdays, and
|
|
47
|
+
* now and then a day something big shipped — and that shape is the thing the cost chart exists
|
|
48
|
+
* to show, so the invented one has it too.
|
|
49
|
+
*
|
|
50
|
+
* Deterministic and stateless: a function of which day it is and nothing else, so two reads of
|
|
51
|
+
* one demo are one answer and a screenshot can be taken twice. The multiplier off Knuth's
|
|
52
|
+
* constant is a spread, not randomness — it just has to be unmemorable and stable.
|
|
53
|
+
*
|
|
54
|
+
* Never asked about a day the ring reaches into — `demoJournalDay` answers 1 for those without
|
|
55
|
+
* coming here, and there are two of them, the ring being twenty-four hours long. The one thing
|
|
56
|
+
* this journal may not do is disagree with the record behind the scrubber about a minute they
|
|
57
|
+
* both hold. What is left for this to scale is the past nobody can compare against.
|
|
58
|
+
*
|
|
59
|
+
* The long day is a weekday, which is what keeps the shape readable: the peaks are then above
|
|
60
|
+
* every ordinary day and the weekends below every one of them, rather than a Saturday at $220
|
|
61
|
+
* saying the opposite of what the chart is showing. Seventeen apart is at most two of them in a
|
|
62
|
+
* month and never none — seventeen days is three weekdays along, so the pair cannot both land on
|
|
63
|
+
* a weekend.
|
|
64
|
+
*/
|
|
65
|
+
export function demoDayFactor(back, weekday) {
|
|
66
|
+
if (back <= 0)
|
|
67
|
+
return 1;
|
|
68
|
+
const spread = ((back * 2_654_435_761) % 1000) / 1000;
|
|
69
|
+
if (weekday === 0 || weekday === 6)
|
|
70
|
+
return 0.55 + 0.12 * spread;
|
|
71
|
+
if (back % 17 === 6)
|
|
72
|
+
return 1.5;
|
|
73
|
+
return 0.85 + 0.38 * spread;
|
|
74
|
+
}
|
|
35
75
|
/**
|
|
36
76
|
* Where the journal would live if there were one. Named because a store names its directory,
|
|
37
77
|
* never opened by anything here — and under the invented home, so a capture of a demo carries
|
|
@@ -50,8 +90,9 @@ const MINUTE = 60_000;
|
|
|
50
90
|
* whatever this says. A period shorter than the day plays the fleet arriving and going home
|
|
51
91
|
* more than once between two midnights, which charges a fraction of a day's work to each of
|
|
52
92
|
* them and can leave an actor born late in the day out of the week entirely. The suite holds
|
|
53
|
-
* the consequence rather than the number: every full day of the invented
|
|
54
|
-
* and
|
|
93
|
+
* the consequence rather than the number: every full day of the invented past carries all five
|
|
94
|
+
* projects, and each of them costs its own day's scale of one whole day's work — which is true
|
|
95
|
+
* of a whole day repeated and of nothing else.
|
|
55
96
|
*
|
|
56
97
|
* What is NOT held, because it cannot be seen: whether an older cycle replays the day's last
|
|
57
98
|
* minute or stops one short of it. Both tile the past exactly and both leave every day the same
|
|
@@ -136,15 +177,34 @@ export function demoJournalDay(date, dayStart) {
|
|
|
136
177
|
// call — what is wanted is the reduction, and a second copy of that reduction here would be
|
|
137
178
|
// the one thing this module exists not to be.
|
|
138
179
|
const ring = createHistory({ since: dayStart, cadence: MINUTE });
|
|
180
|
+
// What this whole day cost, against the day the ring holds. Decided once for the day rather
|
|
181
|
+
// than per reading: the reader charges a day what a session's highest reading in it exceeds its
|
|
182
|
+
// lowest, and a multiplier that moved inside the day would be charged that movement as spending.
|
|
183
|
+
//
|
|
184
|
+
// A day the RING reaches into is never scaled, and that is two calendar days rather than one:
|
|
185
|
+
// `dayStart` is twenty-four hours back, so the ring straddles a midnight on every clock but
|
|
186
|
+
// midnight itself. Scaled on the older of the two, 719 of the ring's 1440 minutes went into the
|
|
187
|
+
// journal at another price — the cost chart billing yesterday 14% above what the scrubber shows
|
|
188
|
+
// for the same minutes, which is the one disagreement this module exists to prevent. `end` is
|
|
189
|
+
// this day's last moment, so a day the ring reaches into is one that ends at or after its first.
|
|
190
|
+
const factor = end >= dayStart ? 1 : demoDayFactor(Math.round((midnightOf(dayOf(last)) - midnight) / DAY), d.getDay());
|
|
139
191
|
let text = '';
|
|
140
192
|
for (let k = from; k <= to; k++) {
|
|
141
193
|
const t = dayStart + k * MINUTE;
|
|
142
194
|
const cycleStart = cycleStartFor(t, dayStart);
|
|
143
195
|
const minute = Math.round((t - cycleStart) / MINUTE);
|
|
144
196
|
// How many cycles before the newest day this reading belongs to — what lets the seven-day
|
|
145
|
-
// window climb across
|
|
146
|
-
|
|
147
|
-
|
|
197
|
+
// window climb across a whole invented week instead of replaying one day's ramp. Modulo the
|
|
198
|
+
// week, because that is how long the window IS: a ramp running the length of the journal put
|
|
199
|
+
// the account four times over its plan by the middle of the month and then flat on the floor
|
|
200
|
+
// for the three weeks before it, where a real one rolls over every seven days.
|
|
201
|
+
const cyclesBack = Math.round((dayStart - cycleStart) / CYCLE_MS) % 7;
|
|
202
|
+
const record = journalRecordOf(ring.record(demoFleetAt(minute, cycleStart, t, undefined, cyclesBack)));
|
|
203
|
+
if (factor !== 1)
|
|
204
|
+
for (const s of record.sessions)
|
|
205
|
+
if (s.costUsd !== null)
|
|
206
|
+
s.costUsd = Math.round(s.costUsd * factor * 100) / 100;
|
|
207
|
+
text += `${JSON.stringify(record)}\n`;
|
|
148
208
|
}
|
|
149
209
|
return text;
|
|
150
210
|
}
|
package/dist/fleet.js
CHANGED
|
@@ -14,7 +14,7 @@ import path from 'node:path';
|
|
|
14
14
|
import { DEFAULT_STALE_AFTER_MS } from './config.js';
|
|
15
15
|
import { measured, windowsApart } from './limits.js';
|
|
16
16
|
import { guardVersions } from './schema.js';
|
|
17
|
-
import { isWaiting } from './sessions.js';
|
|
17
|
+
import { anchoredOnKind, isBackgroundAgent, isWaiting } from './sessions.js';
|
|
18
18
|
import { SID_NAME } from './wrapper.js';
|
|
19
19
|
export function buildFleet({ sessions, snapshots, now, staleAfterMs = DEFAULT_STALE_AFTER_MS, discovery = null, }) {
|
|
20
20
|
const rows = sessions.map((s) => {
|
|
@@ -43,7 +43,14 @@ export function buildFleet({ sessions, snapshots, now, staleAfterMs = DEFAULT_ST
|
|
|
43
43
|
};
|
|
44
44
|
});
|
|
45
45
|
rows.sort((a, b) => rank(a) - rank(b) || (b.ctxPct ?? -1) - (a.ctxPct ?? -1));
|
|
46
|
-
|
|
46
|
+
// The population the coverage numbers are about. An agent has no TUI, so it never draws a
|
|
47
|
+
// frame and no statusline can ever file a snapshot for it: counted among the blind, one
|
|
48
|
+
// agent beside one chained terminal read as "chained on 1/2 sessions — run tarmac install",
|
|
49
|
+
// remediation that is already done and cannot work for the entry that raised it (#29). The
|
|
50
|
+
// map made the same call for the agent strip; this is the fleet-wide line catching up.
|
|
51
|
+
const anchored = anchoredOnKind(rows);
|
|
52
|
+
const chainable = rows.filter((r) => !isBackgroundAgent(r, anchored));
|
|
53
|
+
const covered = chainable.filter((r) => r.ctxState !== 'absent').length;
|
|
47
54
|
// Blind AND unfilable, in that order — this number exists to say how many of the blind will
|
|
48
55
|
// stay blind, and both renderers subtract it from them. A session can be unfilable and
|
|
49
56
|
// covered at the same time: a snapshot written by a pre-upgrade wrapper under a non-UUID
|
|
@@ -51,8 +58,10 @@ export function buildFleet({ sessions, snapshots, now, staleAfterMs = DEFAULT_ST
|
|
|
51
58
|
// than on the filename. Counting that one would push this past the blind count and make the
|
|
52
59
|
// renderers explain away someone else's missing telemetry.
|
|
53
60
|
// A null id is `noSessionId`'s business — a discovery failure, not a naming one.
|
|
54
|
-
const unfilable =
|
|
55
|
-
|
|
61
|
+
const unfilable = chainable.filter((r) => r.ctxState === 'absent' && r.sessionId !== null && !SID_NAME.test(r.sessionId)).length;
|
|
62
|
+
// Over the same population as `covered`, because `schemaBroken` below compares the two and a
|
|
63
|
+
// comparison between two different populations answers a question nobody asked.
|
|
64
|
+
const drift = chainable.filter((r) => r.ctxState === 'drift').length;
|
|
56
65
|
// Having a snapshot and having a cost are different facts, and only the second one is
|
|
57
66
|
// allowed to feed the total.
|
|
58
67
|
const costs = rows.map((r) => r.costUsd).filter((c) => typeof c === 'number');
|
|
@@ -67,6 +76,7 @@ export function buildFleet({ sessions, snapshots, now, staleAfterMs = DEFAULT_ST
|
|
|
67
76
|
rows,
|
|
68
77
|
health: {
|
|
69
78
|
sessions: rows.length,
|
|
79
|
+
chainable: chainable.length,
|
|
70
80
|
covered,
|
|
71
81
|
unfilable,
|
|
72
82
|
drift,
|
package/dist/history-range.js
CHANGED