bullswarm 0.35.2 → 0.35.3
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/CHANGELOG.md +18 -0
- package/docs/design/tidy-0.35.1/frames/colour/real-home-200.txt +2 -2
- package/docs/design/tidy-0.35.1/frames/colour/real-home-55.txt +1 -1
- package/docs/design/tidy-0.35.1/frames/colour/real-run-finished-200.txt +5 -5
- package/docs/design/tidy-0.35.1/frames/colour/real-run-finished-55.txt +5 -5
- package/docs/design/tidy-0.35.1/frames/colour/real-run-running-200.txt +5 -5
- package/docs/design/tidy-0.35.1/frames/colour/real-run-running-55.txt +5 -5
- package/docs/design/tidy-0.35.1/frames/real-home-120.txt +3 -3
- package/docs/design/tidy-0.35.1/frames/real-home-200.txt +2 -2
- package/docs/design/tidy-0.35.1/frames/real-home-55.txt +1 -1
- package/docs/design/tidy-0.35.1/frames/real-run-finished-120.txt +5 -5
- package/docs/design/tidy-0.35.1/frames/real-run-finished-200.txt +5 -5
- package/docs/design/tidy-0.35.1/frames/real-run-finished-55.txt +5 -5
- package/docs/design/tidy-0.35.1/frames/real-run-running-120.txt +5 -5
- package/docs/design/tidy-0.35.1/frames/real-run-running-200.txt +5 -5
- package/docs/design/tidy-0.35.1/frames/real-run-running-55.txt +5 -5
- package/package.json +1 -1
- package/src/lib/tasks.js +24 -0
- package/src/workflow/dashboard.js +23 -18
- package/src/workflow/history-view.js +2 -1
- package/src/workflow/history.js +11 -2
- package/src/workflow/home-model.js +2 -13
- package/src/workflow/home-view.js +81 -25
- package/src/workflow/run-model.js +33 -1
- package/src/workflow/run-view.js +61 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# bullswarm changelog
|
|
2
2
|
|
|
3
|
+
## 0.35.3 — whole right edges in Ghostty and herdr, Enter opens the row you are on, named phases
|
|
4
|
+
|
|
5
|
+
- dashboard: every row is erased before it is painted instead of after, so a
|
|
6
|
+
row that fills the terminal keeps its last cell in Ghostty and herdr (both
|
|
7
|
+
cleared it, turning `50%` into `50` and `16` into `1` on Home's right edge).
|
|
8
|
+
- home: from 110 columns the spent-per-day chart grows to the height of the
|
|
9
|
+
stacked breakdowns beside it; the summary band sizes `Spent` to its text and
|
|
10
|
+
wraps at ` · ` rather than cutting a figure; the closing sentence wraps too.
|
|
11
|
+
- runs: Enter and a click open exactly the row the cursor is on. Tasks recorded
|
|
12
|
+
before the task ledger have no id, and all of them shared one empty key, so
|
|
13
|
+
Enter on a workflow row could open one of those tasks instead.
|
|
14
|
+
- run: phase rules name the phase: `Phase 2 · Build · time-box · docs`
|
|
15
|
+
(Build, Integrate, Verify, Digest, Design, Repair from the steps' kinds,
|
|
16
|
+
joined with ` + ` when a phase mixes them); the phase number and name are
|
|
17
|
+
never cut, step names shorten first.
|
|
18
|
+
- home: day keys no longer build an `Intl.DateTimeFormat` per call to learn
|
|
19
|
+
the zone (it is cached against `TZ`); a Home paint takes about 40% less time.
|
|
20
|
+
|
|
3
21
|
## 0.35.2 — the Step page reads like a transcript; prices without a command, honest totals, a home that stops growing
|
|
4
22
|
|
|
5
23
|
- step: detail is now a scrollable transcript of every turn in order: the full
|
|
@@ -43,8 +43,8 @@ Home · Today · 20 Sep 2026 · top 3 runs (active first)
|
|
|
43
43
|
Mon Tue Wed Thu Fri Sat Sun project-b [38;2;191;108;105m▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░[0m 4
|
|
44
44
|
[2m ~ bars leave 66 unpriced attempts out[0m project-c [38;2;169;156;240m▇▇▇▇▇░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░[0m 1
|
|
45
45
|
|
|
46
|
-
Workflows: [1m[38;2;210;133;107m15[0m[22m · verified [38;2;210;133;107m10[0m (67%)
|
|
47
|
-
Busiest project: [38;2;210;133;107mproject-a[0m (10)
|
|
46
|
+
Workflows: [1m[38;2;210;133;107m15[0m[22m · verified [38;2;210;133;107m10[0m (67%) Favourite pool: [38;2;210;133;107mcodex[0m Spent: [38;2;210;133;107mat least $299.87 api · 66 unmeasured · sub unknown (no plan price)[0m
|
|
47
|
+
Busiest project: [38;2;210;133;107mproject-a[0m (10) Favourite model: [38;2;210;133;107mgpt-5.6-luna[0m Median run: [38;2;210;133;107m1h45m[0m
|
|
48
48
|
|
|
49
49
|
[38;2;169;156;240mYour 15 runs in this period recorded at least $299.87 api · 66 unmeasured of API-equivalent work[0m
|
|
50
50
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[7m[4mH[24mome[0m [4mR[24muns [4mB[24mudget [4mS[24mtats [4mF[24mleet
|
|
2
|
-
bullswarm · home · 1–57/
|
|
2
|
+
bullswarm · home · 1–57/88
|
|
3
3
|
Home · Today · 20 Sep · top 3 runs (active first)
|
|
4
4
|
┌─ sample goal for rensha nostrud lorem nostrud moll…─┐
|
|
5
5
|
│ project-b · completed · verified │
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
[2m──[0m timeline · 5 phases · 5 attempts · Enter on a step opens it [2m─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
|
|
15
15
|
[2m──[0m Preflight [2m───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
|
|
16
16
|
[2m04:05[0m [2m●[0m goal accepted · goal.json
|
|
17
|
-
[2m──[0m [38;2;182;189;115m✓[0m 1 · [
|
|
17
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 1 · [1mMechanical · home-extraction[22m [2m───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m [2m04:05[0m → [2m04:34[0m · [2m28m25s[0m · [2m1/1[0m
|
|
18
18
|
[2m04:05[0m [38;2;182;189;115m✓[0m [1mhome-extraction[22m · [38;2;127;163;224mcodex[0m · gpt-5.6-luna · low [2m28m25s[0m
|
|
19
|
-
[2m──[0m [38;2;182;189;115m✓[0m 2 · [
|
|
19
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 2 · [1mMechanical · runs-extraction[22m [2m───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m [2m04:34[0m → [2m05:04[0m · [2m30m32s[0m · [2m1/1[0m
|
|
20
20
|
[2m04:34[0m [38;2;182;189;115m✓[0m [1mruns-extraction[22m · [38;2;127;163;224mcodex[0m · gpt-5.6-luna · low [2m30m32s[0m
|
|
21
|
-
[2m──[0m [38;2;182;189;115m✓[0m 3 · [
|
|
21
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 3 · [1mMechanical · run-extraction[22m [2m────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m [2m05:04[0m → [2m05:36[0m · [2m31m52s[0m · [2m1/1[0m
|
|
22
22
|
[2m05:04[0m [38;2;182;189;115m✓[0m [1mrun-extraction[22m · [38;2;127;163;224mcodex[0m · gpt-5.6-luna · low [2m31m52s[0m
|
|
23
|
-
[2m──[0m [38;2;182;189;115m✓[0m 4 · [
|
|
23
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 4 · [1mIntegrate · integrate[22m [2m───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m [2m05:36[0m → [2m05:41[0m · [2m4m38s[0m · [2m1/1[0m
|
|
24
24
|
[2m05:36[0m [38;2;182;189;115m✓[0m [1mintegrate[22m · [38;2;127;163;224mcodex[0m · gpt-5.6-sol · high [2m4m38s[0m
|
|
25
|
-
[2m──[0m [38;2;182;189;115m✓[0m 5 · [
|
|
25
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 5 · [1mVerify · verify[22m [2m────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m [2m05:41[0m → [2m06:01[0m · [2m20m27s[0m · [2m1/1[0m
|
|
26
26
|
[2m05:41[0m [38;2;182;189;115m✓[0m [1mverify[22m · [38;2;127;191;168mclaude-code[0m · claude-opus-5 · high [2m20m27s[0m
|
|
27
27
|
|
|
28
28
|
|
|
@@ -19,19 +19,19 @@
|
|
|
19
19
|
[2m──[0m timeline · 5 phases · 5 attempts [2m───────────────────[0m
|
|
20
20
|
[2m──[0m Preflight
|
|
21
21
|
[2m04:05[0m [2m●[0m goal accepted · goal.json
|
|
22
|
-
[2m──[0m [38;2;182;189;115m✓[0m 1 · [
|
|
22
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 1 · [1mMechanical · home-extraction[22m
|
|
23
23
|
[2m04:05 → 04:34 · 28m25s · 1/1[0m
|
|
24
24
|
[2m04:05[0m [38;2;182;189;115m✓[0m [1mhome-extraction[22m · [38;2;127;163;224mcodex[0m [2m28m25s[0m
|
|
25
|
-
[2m──[0m [38;2;182;189;115m✓[0m 2 · [
|
|
25
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 2 · [1mMechanical · runs-extraction[22m
|
|
26
26
|
[2m04:34 → 05:04 · 30m32s · 1/1[0m
|
|
27
27
|
[2m04:34[0m [38;2;182;189;115m✓[0m [1mruns-extraction[22m · [38;2;127;163;224mcodex[0m [2m30m32s[0m
|
|
28
|
-
[2m──[0m [38;2;182;189;115m✓[0m 3 · [
|
|
28
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 3 · [1mMechanical · run-extraction[22m
|
|
29
29
|
[2m05:04 → 05:36 · 31m52s · 1/1[0m
|
|
30
30
|
[2m05:04[0m [38;2;182;189;115m✓[0m [1mrun-extraction[22m · [38;2;127;163;224mcodex[0m [2m31m52s[0m
|
|
31
|
-
[2m──[0m [38;2;182;189;115m✓[0m 4 · [
|
|
31
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 4 · [1mIntegrate · integrate[22m
|
|
32
32
|
[2m05:36 → 05:41 · 4m38s · 1/1[0m
|
|
33
33
|
[2m05:36[0m [38;2;182;189;115m✓[0m [1mintegrate[22m · [38;2;127;163;224mcodex[0m [2m4m38s[0m
|
|
34
|
-
[7m[2m──[0m[7m [38;2;182;189;115m✓[0m[7m 5 · [
|
|
34
|
+
[7m[2m──[0m[7m [38;2;182;189;115m✓[0m[7m Phase 5 · [1mVerify · verify[22m[27m
|
|
35
35
|
[2m05:41 → 06:01 · 20m27s · 1/1[0m
|
|
36
36
|
[2m05:41[0m [38;2;182;189;115m✓[0m [1mverify[22m · [38;2;127;191;168mclaude-code[0m [2m20m27s[0m
|
|
37
37
|
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
[2m──[0m timeline · 5 phases · 4 attempts · Enter on a step opens it [2m─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
|
|
15
15
|
[2m──[0m Preflight [2m───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
|
|
16
16
|
[2m04:05[0m [2m●[0m goal accepted · goal.json
|
|
17
|
-
[2m──[0m [38;2;182;189;115m✓[0m 1 · [
|
|
17
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 1 · [1mMechanical · home-extraction[22m [2m───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m [2m04:05[0m → [2m04:34[0m · [2m28m25s[0m · [2m1/1[0m
|
|
18
18
|
[2m04:05[0m [38;2;182;189;115m✓[0m [1mhome-extraction[22m · [38;2;127;163;224mcodex[0m · gpt-5.6-luna · low [2m28m25s[0m
|
|
19
|
-
[2m──[0m [38;2;182;189;115m✓[0m 2 · [
|
|
19
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 2 · [1mMechanical · runs-extraction[22m [2m───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m [2m04:34[0m → [2m05:04[0m · [2m30m32s[0m · [2m1/1[0m
|
|
20
20
|
[2m04:34[0m [38;2;182;189;115m✓[0m [1mruns-extraction[22m · [38;2;127;163;224mcodex[0m · gpt-5.6-luna · low [2m30m32s[0m
|
|
21
|
-
[2m──[0m [38;2;182;189;115m✓[0m 3 · [
|
|
21
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 3 · [1mMechanical · run-extraction[22m [2m────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m [2m05:04[0m → [2m05:36[0m · [2m31m52s[0m · [2m1/1[0m
|
|
22
22
|
[2m05:04[0m [38;2;182;189;115m✓[0m [1mrun-extraction[22m · [38;2;127;163;224mcodex[0m · gpt-5.6-luna · low [2m31m52s[0m
|
|
23
|
-
[2m──[0m [38;2;233;200;128m▶[0m 4 · [
|
|
23
|
+
[2m──[0m [38;2;233;200;128m▶[0m Phase 4 · [1mIntegrate · integrate[22m [2m─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m [2m05:36[0m → [2mnow[0m · [2m2m19s[0m · [2m0/1[0m
|
|
24
24
|
[2m05:36[0m [38;2;233;200;128m▶[0m [1mintegrate[22m · [38;2;127;163;224mcodex[0m · gpt-5.6-sol · high · [38;2;233;200;128mrunning[0m [2m2m19s[0m
|
|
25
|
-
[2m──[0m [2m○[0m 5 · [
|
|
25
|
+
[2m──[0m [2m○[0m Phase 5 · [1mVerify · verify[22m [2m─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m [2m—[0m → [2m—[0m · [2m—[0m · [2m0/1[0m
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
|
|
@@ -20,19 +20,19 @@
|
|
|
20
20
|
[2m──[0m timeline · 5 phases · 4 attempts [2m───────────────────[0m
|
|
21
21
|
[2m──[0m Preflight
|
|
22
22
|
[2m04:05[0m [2m●[0m goal accepted · goal.json
|
|
23
|
-
[2m──[0m [38;2;182;189;115m✓[0m 1 · [
|
|
23
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 1 · [1mMechanical · home-extraction[22m
|
|
24
24
|
[2m04:05 → 04:34 · 28m25s · 1/1[0m
|
|
25
25
|
[2m04:05[0m [38;2;182;189;115m✓[0m [1mhome-extraction[22m · [38;2;127;163;224mcodex[0m [2m28m25s[0m
|
|
26
|
-
[2m──[0m [38;2;182;189;115m✓[0m 2 · [
|
|
26
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 2 · [1mMechanical · runs-extraction[22m
|
|
27
27
|
[2m04:34 → 05:04 · 30m32s · 1/1[0m
|
|
28
28
|
[2m04:34[0m [38;2;182;189;115m✓[0m [1mruns-extraction[22m · [38;2;127;163;224mcodex[0m [2m30m32s[0m
|
|
29
|
-
[2m──[0m [38;2;182;189;115m✓[0m 3 · [
|
|
29
|
+
[2m──[0m [38;2;182;189;115m✓[0m Phase 3 · [1mMechanical · run-extraction[22m
|
|
30
30
|
[2m05:04 → 05:36 · 31m52s · 1/1[0m
|
|
31
31
|
[2m05:04[0m [38;2;182;189;115m✓[0m [1mrun-extraction[22m · [38;2;127;163;224mcodex[0m [2m31m52s[0m
|
|
32
|
-
[7m[2m──[0m[7m [38;2;233;200;128m▶[0m[7m 4 · [
|
|
32
|
+
[7m[2m──[0m[7m [38;2;233;200;128m▶[0m[7m Phase 4 · [1mIntegrate · integrate[22m[27m
|
|
33
33
|
[2m05:36 → now · 2m19s · 0/1[0m
|
|
34
34
|
[2m05:36[0m [38;2;233;200;128m▶[0m [1mintegrate[22m · [38;2;127;163;224mcodex[0m · [38;2;233;200;128mrunning[0m [2m2m19s[0m
|
|
35
|
-
[2m──[0m [2m○[0m 5 · [
|
|
35
|
+
[2m──[0m [2m○[0m Phase 5 · [1mVerify · verify[22m
|
|
36
36
|
[2m— → — · — · 0/1[0m
|
|
37
37
|
|
|
38
38
|
|
|
@@ -43,8 +43,9 @@ Home · Today · 20 Sep 2026 · top 3 runs (active first)
|
|
|
43
43
|
Mon Tue Wed Thu Fri Sat Sun project-b ▇▇▇▇▇▇▇▇▇▇░░░░░░░░░░░░░░░░░░░░░░░░░░ 4
|
|
44
44
|
~ bars leave 66 unpriced attempts out project-c ▇▇░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 1
|
|
45
45
|
|
|
46
|
-
Workflows: 15 · verified 10 (67%)
|
|
47
|
-
Busiest project: project-a (10)
|
|
46
|
+
Workflows: 15 · verified 10 (67%) Favourite pool: codex Spent: at least $299.87 api · 66 unmeasured
|
|
47
|
+
Busiest project: project-a (10) Favourite model: gpt-5.6-luna sub unknown (no plan price)
|
|
48
|
+
Median run: 1h45m
|
|
48
49
|
|
|
49
50
|
Your 15 runs in this period recorded at least $299.87 api · 66 unmeasured of API-equivalent work
|
|
50
51
|
|
|
@@ -56,5 +57,4 @@ Home · Today · 20 Sep 2026 · top 3 runs (active first)
|
|
|
56
57
|
✓ va7k9a project-a · sample goal for va7k9a proident fugi… 2h46m · at least $15.27 api · 6 unmeasured … 15h56m ago
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
|
|
60
60
|
[ ?.help ] [ quit ]
|
|
@@ -43,8 +43,8 @@ Home · Today · 20 Sep 2026 · top 3 runs (active first)
|
|
|
43
43
|
Mon Tue Wed Thu Fri Sat Sun project-b ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 4
|
|
44
44
|
~ bars leave 66 unpriced attempts out project-c ▇▇▇▇▇░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 1
|
|
45
45
|
|
|
46
|
-
Workflows: 15 · verified 10 (67%)
|
|
47
|
-
Busiest project: project-a (10)
|
|
46
|
+
Workflows: 15 · verified 10 (67%) Favourite pool: codex Spent: at least $299.87 api · 66 unmeasured · sub unknown (no plan price)
|
|
47
|
+
Busiest project: project-a (10) Favourite model: gpt-5.6-luna Median run: 1h45m
|
|
48
48
|
|
|
49
49
|
Your 15 runs in this period recorded at least $299.87 api · 66 unmeasured of API-equivalent work
|
|
50
50
|
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
── timeline · 5 phases · 5 attempts · Enter on a step opens it ─────────────────────────────────────────────────────────
|
|
15
15
|
── Preflight ───────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
16
16
|
04:05 ● goal accepted · goal.json
|
|
17
|
-
── ✓ 1 · home-extraction
|
|
17
|
+
── ✓ Phase 1 · Mechanical · home-extraction ─────────────────────────────────────────────── 04:05 → 04:34 · 28m25s · 1/1
|
|
18
18
|
04:05 ✓ home-extraction · codex · gpt-5.6-luna · low 28m25s
|
|
19
|
-
── ✓ 2 · runs-extraction
|
|
19
|
+
── ✓ Phase 2 · Mechanical · runs-extraction ─────────────────────────────────────────────── 04:34 → 05:04 · 30m32s · 1/1
|
|
20
20
|
04:34 ✓ runs-extraction · codex · gpt-5.6-luna · low 30m32s
|
|
21
|
-
── ✓ 3 · run-extraction
|
|
21
|
+
── ✓ Phase 3 · Mechanical · run-extraction ──────────────────────────────────────────────── 05:04 → 05:36 · 31m52s · 1/1
|
|
22
22
|
05:04 ✓ run-extraction · codex · gpt-5.6-luna · low 31m52s
|
|
23
|
-
── ✓ 4 · integrate
|
|
23
|
+
── ✓ Phase 4 · Integrate · integrate ─────────────────────────────────────────────────────── 05:36 → 05:41 · 4m38s · 1/1
|
|
24
24
|
05:36 ✓ integrate · codex · gpt-5.6-sol · high 4m38s
|
|
25
|
-
── ✓ 5 · verify
|
|
25
|
+
── ✓ Phase 5 · Verify · verify ──────────────────────────────────────────────────────────── 05:41 → 06:01 · 20m27s · 1/1
|
|
26
26
|
05:41 ✓ verify · claude-code · claude-opus-5 · high 20m27s
|
|
27
27
|
|
|
28
28
|
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
── timeline · 5 phases · 5 attempts · Enter on a step opens it ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
15
15
|
── Preflight ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
16
16
|
04:05 ● goal accepted · goal.json
|
|
17
|
-
── ✓ 1 · home-extraction
|
|
17
|
+
── ✓ Phase 1 · Mechanical · home-extraction ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 04:05 → 04:34 · 28m25s · 1/1
|
|
18
18
|
04:05 ✓ home-extraction · codex · gpt-5.6-luna · low 28m25s
|
|
19
|
-
── ✓ 2 · runs-extraction
|
|
19
|
+
── ✓ Phase 2 · Mechanical · runs-extraction ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 04:34 → 05:04 · 30m32s · 1/1
|
|
20
20
|
04:34 ✓ runs-extraction · codex · gpt-5.6-luna · low 30m32s
|
|
21
|
-
── ✓ 3 · run-extraction
|
|
21
|
+
── ✓ Phase 3 · Mechanical · run-extraction ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 05:04 → 05:36 · 31m52s · 1/1
|
|
22
22
|
05:04 ✓ run-extraction · codex · gpt-5.6-luna · low 31m52s
|
|
23
|
-
── ✓ 4 · integrate
|
|
23
|
+
── ✓ Phase 4 · Integrate · integrate ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 05:36 → 05:41 · 4m38s · 1/1
|
|
24
24
|
05:36 ✓ integrate · codex · gpt-5.6-sol · high 4m38s
|
|
25
|
-
── ✓ 5 · verify
|
|
25
|
+
── ✓ Phase 5 · Verify · verify ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 05:41 → 06:01 · 20m27s · 1/1
|
|
26
26
|
05:41 ✓ verify · claude-code · claude-opus-5 · high 20m27s
|
|
27
27
|
|
|
28
28
|
|
|
@@ -19,19 +19,19 @@
|
|
|
19
19
|
── timeline · 5 phases · 5 attempts ───────────────────
|
|
20
20
|
── Preflight
|
|
21
21
|
04:05 ● goal accepted · goal.json
|
|
22
|
-
── ✓ 1 · home-extraction
|
|
22
|
+
── ✓ Phase 1 · Mechanical · home-extraction
|
|
23
23
|
04:05 → 04:34 · 28m25s · 1/1
|
|
24
24
|
04:05 ✓ home-extraction · codex 28m25s
|
|
25
|
-
── ✓ 2 · runs-extraction
|
|
25
|
+
── ✓ Phase 2 · Mechanical · runs-extraction
|
|
26
26
|
04:34 → 05:04 · 30m32s · 1/1
|
|
27
27
|
04:34 ✓ runs-extraction · codex 30m32s
|
|
28
|
-
── ✓ 3 · run-extraction
|
|
28
|
+
── ✓ Phase 3 · Mechanical · run-extraction
|
|
29
29
|
05:04 → 05:36 · 31m52s · 1/1
|
|
30
30
|
05:04 ✓ run-extraction · codex 31m52s
|
|
31
|
-
── ✓ 4 · integrate
|
|
31
|
+
── ✓ Phase 4 · Integrate · integrate
|
|
32
32
|
05:36 → 05:41 · 4m38s · 1/1
|
|
33
33
|
05:36 ✓ integrate · codex 4m38s
|
|
34
|
-
── ✓ 5 · verify
|
|
34
|
+
── ✓ Phase 5 · Verify · verify
|
|
35
35
|
05:41 → 06:01 · 20m27s · 1/1
|
|
36
36
|
05:41 ✓ verify · claude-code 20m27s
|
|
37
37
|
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
── timeline · 5 phases · 4 attempts · Enter on a step opens it ─────────────────────────────────────────────────────────
|
|
15
15
|
── Preflight ───────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
16
16
|
04:05 ● goal accepted · goal.json
|
|
17
|
-
── ✓ 1 · home-extraction
|
|
17
|
+
── ✓ Phase 1 · Mechanical · home-extraction ─────────────────────────────────────────────── 04:05 → 04:34 · 28m25s · 1/1
|
|
18
18
|
04:05 ✓ home-extraction · codex · gpt-5.6-luna · low 28m25s
|
|
19
|
-
── ✓ 2 · runs-extraction
|
|
19
|
+
── ✓ Phase 2 · Mechanical · runs-extraction ─────────────────────────────────────────────── 04:34 → 05:04 · 30m32s · 1/1
|
|
20
20
|
04:34 ✓ runs-extraction · codex · gpt-5.6-luna · low 30m32s
|
|
21
|
-
── ✓ 3 · run-extraction
|
|
21
|
+
── ✓ Phase 3 · Mechanical · run-extraction ──────────────────────────────────────────────── 05:04 → 05:36 · 31m52s · 1/1
|
|
22
22
|
05:04 ✓ run-extraction · codex · gpt-5.6-luna · low 31m52s
|
|
23
|
-
── ▶ 4 · integrate
|
|
23
|
+
── ▶ Phase 4 · Integrate · integrate ───────────────────────────────────────────────────────── 05:36 → now · 2m19s · 0/1
|
|
24
24
|
05:36 ▶ integrate · codex · gpt-5.6-sol · high · running 2m19s
|
|
25
|
-
── ○ 5 · verify
|
|
25
|
+
── ○ Phase 5 · Verify · verify ───────────────────────────────────────────────────────────────────────── — → — · — · 0/1
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
── timeline · 5 phases · 4 attempts · Enter on a step opens it ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
15
15
|
── Preflight ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
16
16
|
04:05 ● goal accepted · goal.json
|
|
17
|
-
── ✓ 1 · home-extraction
|
|
17
|
+
── ✓ Phase 1 · Mechanical · home-extraction ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 04:05 → 04:34 · 28m25s · 1/1
|
|
18
18
|
04:05 ✓ home-extraction · codex · gpt-5.6-luna · low 28m25s
|
|
19
|
-
── ✓ 2 · runs-extraction
|
|
19
|
+
── ✓ Phase 2 · Mechanical · runs-extraction ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 04:34 → 05:04 · 30m32s · 1/1
|
|
20
20
|
04:34 ✓ runs-extraction · codex · gpt-5.6-luna · low 30m32s
|
|
21
|
-
── ✓ 3 · run-extraction
|
|
21
|
+
── ✓ Phase 3 · Mechanical · run-extraction ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 05:04 → 05:36 · 31m52s · 1/1
|
|
22
22
|
05:04 ✓ run-extraction · codex · gpt-5.6-luna · low 31m52s
|
|
23
|
-
── ▶ 4 · integrate
|
|
23
|
+
── ▶ Phase 4 · Integrate · integrate ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 05:36 → now · 2m19s · 0/1
|
|
24
24
|
05:36 ▶ integrate · codex · gpt-5.6-sol · high · running 2m19s
|
|
25
|
-
── ○ 5 · verify
|
|
25
|
+
── ○ Phase 5 · Verify · verify ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── — → — · — · 0/1
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
|
|
@@ -20,19 +20,19 @@
|
|
|
20
20
|
── timeline · 5 phases · 4 attempts ───────────────────
|
|
21
21
|
── Preflight
|
|
22
22
|
04:05 ● goal accepted · goal.json
|
|
23
|
-
── ✓ 1 · home-extraction
|
|
23
|
+
── ✓ Phase 1 · Mechanical · home-extraction
|
|
24
24
|
04:05 → 04:34 · 28m25s · 1/1
|
|
25
25
|
04:05 ✓ home-extraction · codex 28m25s
|
|
26
|
-
── ✓ 2 · runs-extraction
|
|
26
|
+
── ✓ Phase 2 · Mechanical · runs-extraction
|
|
27
27
|
04:34 → 05:04 · 30m32s · 1/1
|
|
28
28
|
04:34 ✓ runs-extraction · codex 30m32s
|
|
29
|
-
── ✓ 3 · run-extraction
|
|
29
|
+
── ✓ Phase 3 · Mechanical · run-extraction
|
|
30
30
|
05:04 → 05:36 · 31m52s · 1/1
|
|
31
31
|
05:04 ✓ run-extraction · codex 31m52s
|
|
32
|
-
── ▶ 4 · integrate
|
|
32
|
+
── ▶ Phase 4 · Integrate · integrate
|
|
33
33
|
05:36 → now · 2m19s · 0/1
|
|
34
34
|
05:36 ▶ integrate · codex · running 2m19s
|
|
35
|
-
── ○ 5 · verify
|
|
35
|
+
── ○ Phase 5 · Verify · verify
|
|
36
36
|
— → — · — · 0/1
|
|
37
37
|
|
|
38
38
|
|
package/package.json
CHANGED
package/src/lib/tasks.js
CHANGED
|
@@ -126,6 +126,30 @@ function finishedRow(entry) {
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// One identity for a finished task row, used everywhere a task can arrive from
|
|
130
|
+
// two sources at once (the day's rows AND `tasks.finished`). A task recorded
|
|
131
|
+
// before the single-task ledger has neither `id` nor `taskFile`, so keying on
|
|
132
|
+
// those alone silently deduplicated nothing and every legacy task was listed
|
|
133
|
+
// and counted twice. The fallback is the tuple the decision log always has.
|
|
134
|
+
export function taskIdentity(task) {
|
|
135
|
+
const id = task?.id ?? task?.taskFile;
|
|
136
|
+
if (id != null && id !== '') return `id:${id}`;
|
|
137
|
+
const at = task?.endedAt ?? task?.finishedAt ?? task?.ts ?? task?.startedAt ?? '';
|
|
138
|
+
const pool = task?.pool ?? task?.picked ?? '';
|
|
139
|
+
return `at:${at}|${pool}|${task?.lane ?? ''}|${task?.durationMs ?? ''}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The id a task row opens by: its ledger id or task file, else the identity
|
|
144
|
+
* tuple above. Never null: a row keyed `null` equalled the dashboard's "no
|
|
145
|
+
* task selected", so Enter on a workflow row opened the first task that
|
|
146
|
+
* predates the ledger instead (seen on the owner's home, 0.35.2).
|
|
147
|
+
*/
|
|
148
|
+
export function taskKey(task) {
|
|
149
|
+
const id = task?.id ?? task?.taskFile;
|
|
150
|
+
return id != null && id !== '' ? id : taskIdentity(task);
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
/**
|
|
130
154
|
* List single-task work from a Bullswarm home.
|
|
131
155
|
*
|
|
@@ -30,7 +30,7 @@ import { dayKey, historyDays } from './history.js';
|
|
|
30
30
|
import { readRollups, rollupIndexPath } from './rollup.js';
|
|
31
31
|
import { loadState } from '../lib/state.js';
|
|
32
32
|
import { readMeterHistoryDays } from '../meters/registry.js';
|
|
33
|
-
import { listTasks } from '../lib/tasks.js';
|
|
33
|
+
import { listTasks, taskKey } from '../lib/tasks.js';
|
|
34
34
|
import { attemptOutputSeries } from './v2-state.js';
|
|
35
35
|
import { formatMoneyPair } from '../lib/usage-basis.js';
|
|
36
36
|
import {
|
|
@@ -2381,13 +2381,20 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2381
2381
|
// frame — the difference between smooth and laggy over a phone or a
|
|
2382
2382
|
// remote terminal. A first paint, a resize, or a return from the setup
|
|
2383
2383
|
// hand-off (lastPaintedFrame reset to null) writes the full frame.
|
|
2384
|
+
//
|
|
2385
|
+
// Each row is erased from its first column before it is painted, never
|
|
2386
|
+
// after: a row that fills the terminal leaves the cursor on its last cell
|
|
2387
|
+
// with a wrap pending, and an erase sent then clears that cell. Ghostty
|
|
2388
|
+
// does exactly that (its eraseLine starts at the cursor's column), which
|
|
2389
|
+
// cost every full-width row its last cell at 200 columns — `40%` read
|
|
2390
|
+
// `40` and a single-digit count vanished.
|
|
2384
2391
|
if (lastPaintedFrame != null && Array.isArray(lastPaintedLines) && lastPaintedLines.length === lines.length) {
|
|
2385
2392
|
let patch = '';
|
|
2386
2393
|
let changed = 0;
|
|
2387
2394
|
for (let row = 0; row < lines.length; row += 1) {
|
|
2388
2395
|
if (lines[row] === lastPaintedLines[row]) continue;
|
|
2389
2396
|
changed += 1;
|
|
2390
|
-
patch += `${ESC}${String(row + 1)};1H${lines[row]}
|
|
2397
|
+
patch += `${ESC}${String(row + 1)};1H${ESC}K${lines[row]}`;
|
|
2391
2398
|
}
|
|
2392
2399
|
if (!changed) return;
|
|
2393
2400
|
lastPaintedLines = lines.slice();
|
|
@@ -2395,7 +2402,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2395
2402
|
output.write(patch);
|
|
2396
2403
|
return;
|
|
2397
2404
|
}
|
|
2398
|
-
const frame = `${ESC}H${lines.map((line) => `${
|
|
2405
|
+
const frame = `${ESC}H${lines.map((line) => `${ESC}K${line}`).join('\n')}`;
|
|
2399
2406
|
lastPaintedLines = lines.slice();
|
|
2400
2407
|
lastPaintedFrame = frame;
|
|
2401
2408
|
output.write(frame);
|
|
@@ -2472,7 +2479,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2472
2479
|
if ((ui.page === 'run' || ui.page === 'step') && !selectedRunId) ui.page = 'home';
|
|
2473
2480
|
const row = ui.page === 'run' || ui.page === 'step' ? currentRow() : null;
|
|
2474
2481
|
const task = ui.page === 'task'
|
|
2475
|
-
? [...(tasks.inflight ?? []), ...(tasks.finished ?? [])].find((entry) => (entry
|
|
2482
|
+
? [...(tasks.inflight ?? []), ...(tasks.finished ?? [])].find((entry) => taskKey(entry) === selectedTaskId) ?? null
|
|
2476
2483
|
: null;
|
|
2477
2484
|
if (row && !row.legacy) {
|
|
2478
2485
|
// The follow flags track whatever the page last resolved as current.
|
|
@@ -2574,7 +2581,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2574
2581
|
const tableKeepsCursor = ui.page === 'runs'
|
|
2575
2582
|
&& (lastFrameResult?.runRows ?? []).some((row) => row.runId === previousRunId);
|
|
2576
2583
|
const taskStillExists = [...(tasks.inflight ?? []), ...(tasks.finished ?? [])]
|
|
2577
|
-
.some((row) => (row
|
|
2584
|
+
.some((row) => taskKey(row) === previousTaskId);
|
|
2578
2585
|
const taskKeepsCursor = ui.page === 'runs'
|
|
2579
2586
|
&& (lastFrameResult?.taskRows ?? []).some((row) => row.taskId === previousTaskId)
|
|
2580
2587
|
&& taskStillExists;
|
|
@@ -2718,7 +2725,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2718
2725
|
/** Opens the compact detail page for one standalone task. */
|
|
2719
2726
|
const openTask = (taskId) => {
|
|
2720
2727
|
const found = [...(tasks.inflight ?? []), ...(tasks.finished ?? [])]
|
|
2721
|
-
.find((entry) => (entry
|
|
2728
|
+
.find((entry) => taskKey(entry) === taskId);
|
|
2722
2729
|
if (!found) {
|
|
2723
2730
|
message = 'That task is no longer in the ledger.';
|
|
2724
2731
|
return paint();
|
|
@@ -3237,7 +3244,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
3237
3244
|
].sort((a, b) => a.y - b.y);
|
|
3238
3245
|
if (listed.length) {
|
|
3239
3246
|
const at = listed.findIndex((entry) => entry.kind === 'task'
|
|
3240
|
-
? entry.taskId === selectedTaskId
|
|
3247
|
+
? selectedTaskId != null && entry.taskId === selectedTaskId
|
|
3241
3248
|
: !selectedTaskId && entry.runId === selectedRunId);
|
|
3242
3249
|
const wanted = at < 0 ? (delta > 0 ? 0 : listed.length - 1) : at + delta;
|
|
3243
3250
|
// Once the cursor reaches the end of the Home list, keep the old
|
|
@@ -3264,7 +3271,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
3264
3271
|
...(lastFrameResult?.taskRows ?? []).map((row) => ({ ...row, kind: 'task' })),
|
|
3265
3272
|
].sort((a, b) => a.y - b.y);
|
|
3266
3273
|
const at = listed.findIndex((entry) => entry.kind === 'task'
|
|
3267
|
-
? entry.taskId === selectedTaskId
|
|
3274
|
+
? selectedTaskId != null && entry.taskId === selectedTaskId
|
|
3268
3275
|
: !selectedTaskId && entry.runId === selectedRunId);
|
|
3269
3276
|
const target = listed[clamp(at < 0 ? (delta > 0 ? 0 : listed.length - 1) : at + delta, 0, Math.max(0, listed.length - 1))];
|
|
3270
3277
|
if (target) {
|
|
@@ -3370,15 +3377,13 @@ export async function runDashboard(bullswarmDir, {
|
|
|
3370
3377
|
/** Enter: open the selected run, then its agents, then the selected step. */
|
|
3371
3378
|
const drillIn = () => {
|
|
3372
3379
|
if (ui.page === 'runs' || ui.page === 'home') {
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
if (action?.kind === 'run') return openRun(action.runId);
|
|
3381
|
-
}
|
|
3380
|
+
// Enter opens the row the page drew its cursor on — the same action a
|
|
3381
|
+
// click on that row runs — never a row found again by id: a task that
|
|
3382
|
+
// predates the ledger had no id, so its row matched "no task selected"
|
|
3383
|
+
// and Enter on a workflow row opened that task.
|
|
3384
|
+
const action = lastFrameResult?.cursorAction;
|
|
3385
|
+
if (action?.kind === 'task') return openTask(action.taskId);
|
|
3386
|
+
if (action?.kind === 'run') return openRun(action.runId);
|
|
3382
3387
|
selectedRunId = ui.page === 'runs'
|
|
3383
3388
|
? (lastFrameResult?.runRows?.find((row) => row.runId === selectedRunId) ?? lastFrameResult?.runRows?.[0])?.runId
|
|
3384
3389
|
: rows[selected]?.runId ?? activeRuns[0]?.runId ?? selectedRunId;
|
|
@@ -3530,7 +3535,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
3530
3535
|
if (stepLikePage) {
|
|
3531
3536
|
const taskRecord = ui.page === 'task'
|
|
3532
3537
|
? [...(tasks.inflight ?? []), ...(tasks.finished ?? [])]
|
|
3533
|
-
.find((entry) => (entry
|
|
3538
|
+
.find((entry) => taskKey(entry) === selectedTaskId) ?? null
|
|
3534
3539
|
: null;
|
|
3535
3540
|
const row = ui.page === 'step' ? detailRow(bullswarmDir, selectedRunId) : null;
|
|
3536
3541
|
const step = ui.page === 'task'
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// does. Its `elapsedMinutes` is measured, never estimated.
|
|
19
19
|
|
|
20
20
|
import { glyphs } from '../lib/glyphs.js';
|
|
21
|
+
import { taskKey } from '../lib/tasks.js';
|
|
21
22
|
import { formatMoney, formatUsageBasis } from '../lib/usage-basis.js';
|
|
22
23
|
import { poolUsageAggregate, spendFacts } from './spend-facts.js';
|
|
23
24
|
import { compactRow, cut, formatDashboardValue, rule } from './dash-kit.js';
|
|
@@ -663,7 +664,7 @@ function taskRow(task, lines, regions, width, ansi, { durationWidth = null } = {
|
|
|
663
664
|
], { width: cols, gap: 1 });
|
|
664
665
|
lines.push(fit(line, cols, ansi));
|
|
665
666
|
addRegion(regions, lines, 4, Math.max(1, visible(line).length - 3), {
|
|
666
|
-
kind: 'task', taskId: task
|
|
667
|
+
kind: 'task', taskId: taskKey(task),
|
|
667
668
|
});
|
|
668
669
|
}
|
|
669
670
|
|
package/src/workflow/history.js
CHANGED
|
@@ -50,9 +50,18 @@ import { readGoalProject } from './goal.js';
|
|
|
50
50
|
|
|
51
51
|
const DAY_MS = 86_400_000;
|
|
52
52
|
|
|
53
|
+
// Resolving the zone builds an Intl.DateTimeFormat, and Home asks once per day
|
|
54
|
+
// key, which made it most of a Home paint. Node only changes the process zone
|
|
55
|
+
// when TZ is assigned, so the answer is cached against TZ's value.
|
|
56
|
+
let zoneTz;
|
|
57
|
+
let zoneName = null;
|
|
53
58
|
function localTimeZone() {
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
const tz = process.env.TZ ?? '';
|
|
60
|
+
if (zoneName !== null && tz === zoneTz) return zoneName;
|
|
61
|
+
try { zoneName = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; }
|
|
62
|
+
catch { zoneName = 'UTC'; }
|
|
63
|
+
zoneTz = tz;
|
|
64
|
+
return zoneName;
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
// Building an Intl.DateTimeFormat costs about 0.1 ms, and a 181-run history
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
worstTokenSource,
|
|
23
23
|
} from './dashboard.js';
|
|
24
24
|
import { withV2Cancellation } from './v2-cancellation.js';
|
|
25
|
+
import { taskIdentity, taskKey } from '../lib/tasks.js';
|
|
25
26
|
import { runClockText, runDurationFacts } from './run-model.js';
|
|
26
27
|
import { verifyRoundLabel } from './verify-rounds.js';
|
|
27
28
|
import { honestApiTotalText, recordSpendFacts, spendFacts } from './spend-facts.js';
|
|
@@ -43,19 +44,6 @@ function isToday(value, nowMs, today = dayKey(nowMs)) {
|
|
|
43
44
|
return dayKey(at) === today;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
// One identity for a finished task row, used everywhere a task can arrive from
|
|
47
|
-
// two sources at once (the day's rows AND `tasks.finished`). A task recorded
|
|
48
|
-
// before the single-task ledger has neither `id` nor `taskFile`, so keying on
|
|
49
|
-
// those alone silently deduplicated nothing and every legacy task was listed
|
|
50
|
-
// and counted twice. The fallback is the tuple the decision log always has.
|
|
51
|
-
function taskIdentity(task) {
|
|
52
|
-
const id = task?.id ?? task?.taskFile;
|
|
53
|
-
if (id != null && id !== '') return `id:${id}`;
|
|
54
|
-
const at = task?.endedAt ?? task?.finishedAt ?? task?.ts ?? task?.startedAt ?? '';
|
|
55
|
-
const pool = task?.pool ?? task?.picked ?? '';
|
|
56
|
-
return `at:${at}|${pool}|${task?.lane ?? ''}|${task?.durationMs ?? ''}`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
47
|
function todayMinutesText(value) {
|
|
60
48
|
const minutes = finiteOrNull(value);
|
|
61
49
|
return minutes == null ? null : `${minutes.toFixed(1)}m`;
|
|
@@ -816,6 +804,7 @@ function recordCostInfo(record) {
|
|
|
816
804
|
export {
|
|
817
805
|
taskToday,
|
|
818
806
|
taskIdentity,
|
|
807
|
+
taskKey,
|
|
819
808
|
todayMinutesText,
|
|
820
809
|
todayMinutesNumberText,
|
|
821
810
|
measuredTaskMinutes,
|
|
@@ -53,9 +53,11 @@ import {
|
|
|
53
53
|
usageBasisText,
|
|
54
54
|
visibleLength,
|
|
55
55
|
workflowRunLabel,
|
|
56
|
+
wrapLines,
|
|
56
57
|
} from './dashboard.js';
|
|
57
58
|
import { apiMoney, formatMoney } from '../lib/usage-basis.js';
|
|
58
59
|
import { honestApiTotalText, recordSpendFacts, spendFacts } from './spend-facts.js';
|
|
60
|
+
import { taskKey } from '../lib/tasks.js';
|
|
59
61
|
|
|
60
62
|
const BUDGET_WEEK_POOLS = 4;
|
|
61
63
|
// From this width up the Today band and the period band are each two halves
|
|
@@ -166,6 +168,20 @@ function todayTableRow(row, width, { header = false } = {}) {
|
|
|
166
168
|
return todayPadded(line, width);
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
/**
|
|
172
|
+
* `parts` joined with ` · ` in lines of at most `width` cells, a line breaking
|
|
173
|
+
* only at a join: a part longer than a whole line wraps at its own spaces.
|
|
174
|
+
*/
|
|
175
|
+
function joinedLines(parts, width) {
|
|
176
|
+
const lines = [];
|
|
177
|
+
for (const part of parts.map(String).filter(Boolean)) {
|
|
178
|
+
const last = lines.at(-1);
|
|
179
|
+
if (last != null && visibleLength(`${last} · ${part}`) <= width) lines[lines.length - 1] = `${last} · ${part}`;
|
|
180
|
+
else lines.push(...wrapLines([part], width));
|
|
181
|
+
}
|
|
182
|
+
return lines.length ? lines : [''];
|
|
183
|
+
}
|
|
184
|
+
|
|
169
185
|
function todayBareRule(width) {
|
|
170
186
|
return '─'.repeat(Math.max(0, width));
|
|
171
187
|
}
|
|
@@ -245,7 +261,7 @@ function taskCardModel(task) {
|
|
|
245
261
|
};
|
|
246
262
|
return {
|
|
247
263
|
record,
|
|
248
|
-
id: task
|
|
264
|
+
id: taskKey(task),
|
|
249
265
|
name: String(task?.goal ?? task?.lane ?? task?.id ?? task?.taskFile ?? 'task')
|
|
250
266
|
.split(/\r?\n/).find((line) => line.trim())?.trim() ?? 'task',
|
|
251
267
|
project: task?.project ?? '—',
|
|
@@ -633,8 +649,8 @@ function homeTodayBand(model, opts, body) {
|
|
|
633
649
|
// workflows, so it remains workflow-only while a task-only home is useful.
|
|
634
650
|
for (const task of today.tasks) {
|
|
635
651
|
if (cards.length >= 3) break;
|
|
636
|
-
const id = task
|
|
637
|
-
if (
|
|
652
|
+
const id = taskKey(task);
|
|
653
|
+
if (used.has(id)) continue;
|
|
638
654
|
const card = taskCardModel(task);
|
|
639
655
|
card.task = true;
|
|
640
656
|
cards.push(card);
|
|
@@ -932,8 +948,14 @@ function slotLabelLine(slots, { cell, weekly, room }) {
|
|
|
932
948
|
* approximate — an estimate, or a day with unpriced attempts left out — and
|
|
933
949
|
* `$400` when every bar is a provider-reported whole), the day under each
|
|
934
950
|
* bar, and at most one dim line saying what the bars leave out.
|
|
951
|
+
*
|
|
952
|
+
* `lines`, when given, is how many lines the chart fills: the half beside the
|
|
953
|
+
* breakdowns asks for their height, so no empty block sits under the chart.
|
|
954
|
+
* Each tick interval then takes a whole number of rows and the bars scale to
|
|
955
|
+
* them; the one or two rows that leave over sit above the top tick as
|
|
956
|
+
* headroom, so every tick still marks its exact amount.
|
|
935
957
|
*/
|
|
936
|
-
function spendChartLines(model, opts, width) {
|
|
958
|
+
function spendChartLines(model, opts, width, { lines: fill = null } = {}) {
|
|
937
959
|
const spend = model.stats?.spendPerDay ?? null;
|
|
938
960
|
const period = PERIOD_ITEMS.find((item) => item.id === opts.period) ?? PERIOD_ITEMS[0];
|
|
939
961
|
const weekly = spend?.bucketBy === 'week';
|
|
@@ -949,8 +971,20 @@ function spendChartLines(model, opts, width) {
|
|
|
949
971
|
const ticks = spendTicks(Math.max(...drawn.map((slot) => slot.usd)));
|
|
950
972
|
const top = ticks.at(-1);
|
|
951
973
|
const intervals = ticks.length - 1;
|
|
952
|
-
const
|
|
953
|
-
const
|
|
974
|
+
const unpriced = slots.reduce((sum, slot) => sum + slot.unpriced, 0);
|
|
975
|
+
const unknownDays = slots.filter((slot) => slot.unknown).length;
|
|
976
|
+
const notes = [
|
|
977
|
+
unpriced ? `~ bars leave ${unpriced} unpriced attempt${unpriced === 1 ? '' : 's'} out` : null,
|
|
978
|
+
unknownDays ? `${blank()} no price recorded` : null,
|
|
979
|
+
].filter(Boolean);
|
|
980
|
+
// The plot's rows: what `lines` leaves after the baseline, the day labels
|
|
981
|
+
// and the note, else the terminal height's share.
|
|
982
|
+
const room = fill == null ? null : fill - 2 - (notes.length ? 1 : 0);
|
|
983
|
+
const perTick = room == null
|
|
984
|
+
? Math.max(2, Math.round(chartRowCount(opts.height ?? 36) / intervals))
|
|
985
|
+
: Math.max(2, Math.floor(room / intervals));
|
|
986
|
+
const scaled = intervals * perTick;
|
|
987
|
+
const rows = room == null ? scaled : Math.max(scaled, room);
|
|
954
988
|
const gutter = Math.max(...ticks.map((tick) => tickText(tick, mark).length));
|
|
955
989
|
const ascii = asciiGlyphsPreferred();
|
|
956
990
|
const plot = Math.max(slots.length, width - gutter - 3);
|
|
@@ -960,10 +994,10 @@ function spendChartLines(model, opts, width) {
|
|
|
960
994
|
// A day that spent anything keeps at least one eighth, so it never reads
|
|
961
995
|
// as the zero a day with no run draws.
|
|
962
996
|
const eighths = slots.map((slot) => (slot.usd > 0
|
|
963
|
-
? Math.max(1, Math.round((slot.usd / top) *
|
|
997
|
+
? Math.max(1, Math.round((slot.usd / top) * scaled * 8)) : 0));
|
|
964
998
|
const lines = [];
|
|
965
999
|
for (let row = rows; row >= 1; row -= 1) {
|
|
966
|
-
const tick = row % perTick === 0 ? ticks[row / perTick] : null;
|
|
1000
|
+
const tick = row % perTick === 0 ? ticks[row / perTick] ?? null : null;
|
|
967
1001
|
const axis = tick == null ? (ascii ? '|' : '│') : (ascii ? '|' : '┤');
|
|
968
1002
|
let line = ` ${(tick == null ? '' : tickText(tick, mark)).padStart(gutter)} ${dimText(axis, 1)}`;
|
|
969
1003
|
slots.forEach((slot, index) => {
|
|
@@ -979,12 +1013,6 @@ function spendChartLines(model, opts, width) {
|
|
|
979
1013
|
const baseline = `${ascii ? '+' : '┼'}${(ascii ? '-' : '─').repeat(cell * slots.length)}`;
|
|
980
1014
|
lines.push(` ${tickText(0, mark).padStart(gutter)} ${dimText(baseline, baseline.length)}`);
|
|
981
1015
|
lines.push(`${' '.repeat(gutter + 3)}${slotLabelLine(slots, { cell, weekly, room: Math.max(cell * slots.length, plot) })}`);
|
|
982
|
-
const unpriced = slots.reduce((sum, slot) => sum + slot.unpriced, 0);
|
|
983
|
-
const unknownDays = slots.filter((slot) => slot.unknown).length;
|
|
984
|
-
const notes = [
|
|
985
|
-
unpriced ? `~ bars leave ${unpriced} unpriced attempt${unpriced === 1 ? '' : 's'} out` : null,
|
|
986
|
-
unknownDays ? `${blank()} no price recorded` : null,
|
|
987
|
-
].filter(Boolean);
|
|
988
1016
|
if (notes.length) lines.push(dimText(` ${notes.join(' · ')}`, width));
|
|
989
1017
|
return lines;
|
|
990
1018
|
}
|
|
@@ -1023,9 +1051,10 @@ function breakdownSection(label, list, { width, role, runs = false, nameWidth })
|
|
|
1023
1051
|
/**
|
|
1024
1052
|
* The period band's cells: the spent-per-day chart, then `by pool`,
|
|
1025
1053
|
* `by model` and `by project`, each `{ action, rows }` and `cellWidth` wide.
|
|
1026
|
-
* The breakdowns share one name column, so their bars start together.
|
|
1054
|
+
* The breakdowns share one name column, so their bars start together. With
|
|
1055
|
+
* `chartLines` the chart cell is that many lines tall, its label included.
|
|
1027
1056
|
*/
|
|
1028
|
-
function breakdownCells(model, opts, { cellWidth }) {
|
|
1057
|
+
function breakdownCells(model, opts, { cellWidth, chartLines = null }) {
|
|
1029
1058
|
const breakdown = model.stats?.overview?.breakdown ?? { pools: [], models: [], projects: [] };
|
|
1030
1059
|
const spend = model.stats?.spendPerDay ?? null;
|
|
1031
1060
|
const lists = {
|
|
@@ -1044,7 +1073,7 @@ function breakdownCells(model, opts, { cellWidth }) {
|
|
|
1044
1073
|
action: { kind: 'trend', metric: 'spend' },
|
|
1045
1074
|
rows: [
|
|
1046
1075
|
dimText(` spent per ${spend?.bucketBy === 'week' ? 'week' : 'day'}`, cellWidth),
|
|
1047
|
-
...spendChartLines(model, opts, cellWidth),
|
|
1076
|
+
...spendChartLines(model, opts, cellWidth, { lines: chartLines == null ? null : chartLines - 1 }),
|
|
1048
1077
|
],
|
|
1049
1078
|
},
|
|
1050
1079
|
// The Stats tab each section opens: its `pool`, `model` and `project`
|
|
@@ -1060,17 +1089,20 @@ function breakdownCells(model, opts, { cellWidth }) {
|
|
|
1060
1089
|
* breakdowns stacked in the right one, a blank row between them; below 110
|
|
1061
1090
|
* columns the four read one under another. Each line keeps its cell's action,
|
|
1062
1091
|
* so a click on the chart opens the spend trend and one on a section its tab.
|
|
1092
|
+
* In halves the chart is as tall as the stack beside it.
|
|
1063
1093
|
*/
|
|
1064
1094
|
function periodBand(model, opts, body) {
|
|
1065
1095
|
const { width } = opts;
|
|
1066
1096
|
const split = width >= HALVES_WIDTH;
|
|
1067
1097
|
const halves = split ? bandHalves(width) : null;
|
|
1068
|
-
const
|
|
1098
|
+
const cellWidth = split ? halves.half : width;
|
|
1099
|
+
const [, ...sections] = breakdownCells(model, opts, { cellWidth });
|
|
1069
1100
|
const linesOf = (cell) => cell.rows.map((text) => ({ text, action: cell.action }));
|
|
1070
1101
|
const stacked = sections.flatMap((cell, index) => [
|
|
1071
1102
|
...(index ? [{ text: '' }] : []),
|
|
1072
1103
|
...linesOf(cell),
|
|
1073
1104
|
]);
|
|
1105
|
+
const [chart] = breakdownCells(model, opts, { cellWidth, chartLines: split ? stacked.length : null });
|
|
1074
1106
|
if (split) pushHalves(body, linesOf(chart), stacked, halves);
|
|
1075
1107
|
else {
|
|
1076
1108
|
pushStack(body, linesOf(chart));
|
|
@@ -1142,22 +1174,46 @@ function summaryBand(body, model, opts) {
|
|
|
1142
1174
|
`Favourite pool: ${tint(named(keys.favouritePool), 'orange')}`,
|
|
1143
1175
|
`Favourite model: ${tint(named(keys.favouriteModel), 'orange')}`,
|
|
1144
1176
|
],
|
|
1145
|
-
[
|
|
1146
|
-
`Spent: ${tint(money, 'orange')}`,
|
|
1147
|
-
`Median run: ${tint(medianRunText(model), 'orange')}`,
|
|
1148
|
-
],
|
|
1149
1177
|
];
|
|
1178
|
+
const median = `Median run: ${tint(medianRunText(model), 'orange')}`;
|
|
1179
|
+
// The amount with its coverage is the longest figure: where its column is
|
|
1180
|
+
// too narrow it wraps at its ` · ` joins under its label, never cut short.
|
|
1181
|
+
const label = 'Spent: ';
|
|
1182
|
+
const spentRows = (own) => joinedLines(money.split(' · '), Math.max(8, own - label.length))
|
|
1183
|
+
.map((line, index) => `${index ? ' '.repeat(label.length) : label}${tint(line, 'orange')}`);
|
|
1150
1184
|
body.push('');
|
|
1151
1185
|
if (narrow) {
|
|
1152
1186
|
for (const figure of figures.flat()) body.push(cut(` ${figure}`, width));
|
|
1187
|
+
for (const row of [...spentRows(width - 1), median]) body.push(cut(` ${row}`, width));
|
|
1153
1188
|
} else {
|
|
1154
|
-
|
|
1189
|
+
// Three even columns while every figure fits its third. Past that, which
|
|
1190
|
+
// an even split met at 200 columns by cutting the coverage off `Spent`,
|
|
1191
|
+
// `Spent` takes the width it needs out of what the other two leave, and
|
|
1192
|
+
// they share whatever is over.
|
|
1193
|
+
const gap = 2;
|
|
1194
|
+
const cells = width - 1;
|
|
1195
|
+
const inner = cells - gap * 2;
|
|
1196
|
+
const natural = [
|
|
1197
|
+
...figures.map((rows) => Math.max(...rows.map((row) => visibleLength(row)))),
|
|
1198
|
+
visibleLength(`${label}${money}`),
|
|
1199
|
+
];
|
|
1200
|
+
const even = Math.floor(inner / 3);
|
|
1201
|
+
const spentWidth = Math.max(even, Math.min(natural[2], inner - natural[0] - natural[1]));
|
|
1202
|
+
const over = inner - spentWidth - natural[0] - natural[1];
|
|
1203
|
+
const widths = natural[2] > even && over >= 0
|
|
1204
|
+
? [natural[0] + Math.ceil(over / 2), natural[1] + Math.floor(over / 2), spentWidth]
|
|
1205
|
+
: null;
|
|
1206
|
+
pushColumns(body, [
|
|
1207
|
+
...figures.map((rows) => ({ rows })),
|
|
1208
|
+
{ rows: [...spentRows(widths ? spentWidth : even), median] },
|
|
1209
|
+
].map((cell, index) => (widths ? { ...cell, width: widths[index] } : cell)), { width: cells, gap });
|
|
1155
1210
|
}
|
|
1156
1211
|
body.push('');
|
|
1157
1212
|
const sentence = apiPart && !apiPart.includes('api unknown')
|
|
1158
1213
|
? `Your ${runs} run${runs === 1 ? '' : 's'} in this period recorded ${apiPart} of API-equivalent work`
|
|
1159
1214
|
: `Your ${runs} run${runs === 1 ? '' : 's'} in this period recorded no API-equivalent estimate`;
|
|
1160
|
-
|
|
1215
|
+
// Wrapped, not cut: a phone would otherwise cut the amount it states.
|
|
1216
|
+
for (const line of wrapLines([sentence], Math.max(8, width - 1))) body.push(` ${tint(line, 'purple')}`);
|
|
1161
1217
|
}
|
|
1162
1218
|
|
|
1163
1219
|
function homePage(model, opts, body) {
|
|
@@ -1262,7 +1318,7 @@ function activeRunLines(model, opts, body, title = 'running') {
|
|
|
1262
1318
|
{ text: elapsed, width: Math.max(5, visibleLength(elapsed)), align: 'right', gap: 2 },
|
|
1263
1319
|
{ text: ' ', width: 1, gap: 0 },
|
|
1264
1320
|
], { width });
|
|
1265
|
-
body.row(line, { kind: 'task', taskId: task
|
|
1321
|
+
body.row(line, { kind: 'task', taskId: taskKey(task) });
|
|
1266
1322
|
}
|
|
1267
1323
|
if (unratedPools.size) {
|
|
1268
1324
|
body.push(dimText(` ${blank()} no measured %/minute rate for ${[...unratedPools].join(', ')}, so no licence draw`, width));
|
|
@@ -8,7 +8,7 @@ import { finiteOrNull } from '../lib/num.js';
|
|
|
8
8
|
import { formatMoney } from '../lib/usage-basis.js';
|
|
9
9
|
import { isProgramWorkflow } from './execution-policy.js';
|
|
10
10
|
import { presentationStageStatus, projectV2DependencyStages } from './v2-presentation.js';
|
|
11
|
-
import { loopStageLabel } from './verify-rounds.js';
|
|
11
|
+
import { kernelRepairActionIds, loopStageLabel } from './verify-rounds.js';
|
|
12
12
|
import { cut, progressBar } from './dash-kit.js';
|
|
13
13
|
import {
|
|
14
14
|
blank,
|
|
@@ -924,6 +924,37 @@ function phaseGlyph(status) {
|
|
|
924
924
|
return table.pending;
|
|
925
925
|
}
|
|
926
926
|
|
|
927
|
+
// What a phase does, named from the kinds of the steps it holds: the owner's
|
|
928
|
+
// words for the program's step kinds (0.35.3). A kind with no entry reads as
|
|
929
|
+
// the kind itself, capitalised.
|
|
930
|
+
const PHASE_KIND_NAMES = Object.freeze({
|
|
931
|
+
implement: 'Build',
|
|
932
|
+
integration: 'Integrate',
|
|
933
|
+
'adversarial-acceptance': 'Verify',
|
|
934
|
+
digest: 'Digest',
|
|
935
|
+
architecture: 'Design',
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* A phase's name from its steps' kinds, in the order each kind first appears,
|
|
940
|
+
* joined with ` + ` when the phase mixes them (`Build + Verify`). A repair
|
|
941
|
+
* step the kernel's verify loop added reads `Repair`, whatever kind it was
|
|
942
|
+
* added as. Null when no step of the phase declares a kind.
|
|
943
|
+
*/
|
|
944
|
+
function phaseKindName(state, actionIds) {
|
|
945
|
+
const repairs = new Set(kernelRepairActionIds(state));
|
|
946
|
+
const kinds = new Map((state?.program?.actions ?? []).map((action) => [action?.id, action?.kind]));
|
|
947
|
+
const names = [];
|
|
948
|
+
for (const id of actionIds) {
|
|
949
|
+
const kind = String(kinds.get(id) ?? '').trim();
|
|
950
|
+
const name = repairs.has(id) ? 'Repair'
|
|
951
|
+
: Object.hasOwn(PHASE_KIND_NAMES, kind) ? PHASE_KIND_NAMES[kind]
|
|
952
|
+
: kind ? `${kind[0].toUpperCase()}${kind.slice(1).replaceAll('-', ' ')}` : null;
|
|
953
|
+
if (name && !names.includes(name)) names.push(name);
|
|
954
|
+
}
|
|
955
|
+
return names.length ? names.join(' + ') : null;
|
|
956
|
+
}
|
|
957
|
+
|
|
927
958
|
/** The tree facts consumed by both the desktop and phone timeline layouts. */
|
|
928
959
|
function runTimelineFacts(row, { nowMs = Date.now() } = {}) {
|
|
929
960
|
const state = row?.state ?? row ?? {};
|
|
@@ -943,6 +974,7 @@ function runTimelineFacts(row, { nowMs = Date.now() } = {}) {
|
|
|
943
974
|
const failed = phaseActions.some((action) => ['failed', 'blocked', 'cancelled', 'interrupted'].includes(action.status));
|
|
944
975
|
return {
|
|
945
976
|
index, id: stage.id, name: planStageStepsName(stage, index), label: stage.label, stage,
|
|
977
|
+
kindName: phaseKindName(state, phaseActions.map((action) => action.id)),
|
|
946
978
|
status: active ? 'active' : failed ? 'failed' : progress.completed === progress.total && progress.total > 0 ? 'completed' : 'pending',
|
|
947
979
|
glyph: phaseGlyph(active ? 'active' : failed ? 'failed' : progress.completed === progress.total && progress.total > 0 ? 'completed' : 'pending'),
|
|
948
980
|
startedAt, finishedAt, endAt: active ? null : finishedAt,
|
package/src/workflow/run-view.js
CHANGED
|
@@ -173,7 +173,9 @@ function paintPhaseRule(line, phase) {
|
|
|
173
173
|
const start = phase?.startedAt ? clockText(phase.startedAt) : '—';
|
|
174
174
|
const end = phase?.status === 'active' ? 'now' : phase?.finishedAt ? clockText(phase.finishedAt) : '—';
|
|
175
175
|
const duration = runClockText(phase?.activeMinutes ?? phase?.spanMinutes);
|
|
176
|
-
|
|
176
|
+
// The facts sit after the label: a step named `now` is not the clock.
|
|
177
|
+
const labelAt = phase?.name ? source.indexOf(phase.name) : -1;
|
|
178
|
+
let factsFrom = labelAt >= 0 ? labelAt + phase.name.length : 0;
|
|
177
179
|
const addFact = (value) => {
|
|
178
180
|
const text = String(value ?? '');
|
|
179
181
|
if (!text) return;
|
|
@@ -829,12 +831,13 @@ function workflowTimelineLines(model, width, spinnerFrame = 0, {
|
|
|
829
831
|
const start = phase.startedAt ? clockText(phase.startedAt) : '—';
|
|
830
832
|
const end = phase.status === 'active' ? 'now' : phase.finishedAt ? clockText(phase.finishedAt) : '—';
|
|
831
833
|
const duration = runClockText(phase.activeMinutes ?? phase.spanMinutes);
|
|
832
|
-
const
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
834
|
+
const tally = `${phase.done}/${phase.total}`;
|
|
835
|
+
const right = `${start} → ${end} · ${duration} · ${tally}`;
|
|
836
|
+
// The phone prints the facts on their own row under the rule.
|
|
837
|
+
const rule = phaseRule(phaseTitle(phase), phaseStepsText(phase), phone ? [] : [right, `${duration} · ${tally}`, tally], safeWidth);
|
|
838
|
+
// The name and the steps are bold, `Phase <n> · ` plain, as `<n> · ` was.
|
|
839
|
+
const named = rule.label.slice(`${phase.glyph} Phase ${phase.index + 1}`.length).replace(/^ · /, '');
|
|
840
|
+
push(paintPhaseRule(rule.line, { ...phase, name: named }), {
|
|
838
841
|
header: true, segment: phase.label, phaseIndex: phase.index, phase,
|
|
839
842
|
});
|
|
840
843
|
if (phone) push(` ${dimCell(right)}`, { segment: phase.label, phaseIndex: phase.index, span: true });
|
|
@@ -899,27 +902,63 @@ function workflowTimelineLines(model, width, spinnerFrame = 0, {
|
|
|
899
902
|
};
|
|
900
903
|
}
|
|
901
904
|
|
|
905
|
+
/** `✓ Phase 2 · Build`: the phase's glyph, number and the name its kinds give it. */
|
|
906
|
+
function phaseTitle(phase) {
|
|
907
|
+
return `${phase.glyph} Phase ${phase.index + 1}${phase.kindName ? ` · ${phase.kindName}` : ''}`;
|
|
908
|
+
}
|
|
909
|
+
|
|
902
910
|
/**
|
|
903
|
-
*
|
|
904
|
-
*
|
|
911
|
+
* The step names a phase rule carries after its title. A kernel loop phase is
|
|
912
|
+
* named by its round (`repair · round 1 · 2 requirements`); the title already
|
|
913
|
+
* says `Repair`, so the round's own leading word is not said twice.
|
|
914
|
+
*/
|
|
915
|
+
function phaseStepsText(phase) {
|
|
916
|
+
const name = String(phase.name ?? '').trim();
|
|
917
|
+
// Only a round's label: a phase whose first step is named `build` keeps it.
|
|
918
|
+
const lead = phase.stage?.loopLabel && phase.kindName ? `${phase.kindName.toLowerCase()} · ` : null;
|
|
919
|
+
return lead && name.toLowerCase().startsWith(lead) ? name.slice(lead.length) : name;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* `── ✓ Phase 2 · Build · time-box · docs ──── <start> → <end> · <duration> · <done>/<total>`:
|
|
924
|
+
* the v2 phase rule spends its dashes between the label and the facts and ends
|
|
905
925
|
* on the tally, so the count is the last thing the row says. Rule 6 of the
|
|
906
926
|
* run-v2 record draws it that way; `rule()` keeps its closing dashes for the
|
|
907
|
-
* rules that have a right-hand label to fence off.
|
|
908
|
-
*
|
|
909
|
-
*
|
|
927
|
+
* rules that have a right-hand label to fence off. `rights` are the facts from
|
|
928
|
+
* the fullest down; the phone passes none, prints the facts on a row of their
|
|
929
|
+
* own, and draws no dashes after the label.
|
|
930
|
+
*
|
|
931
|
+
* The phase number and name are never cut. A phase whose steps are named at
|
|
932
|
+
* length gives way first — its step names shorten, then go — and only then
|
|
933
|
+
* the facts, shortest last: the count, the clock and the duration are what the
|
|
934
|
+
* rule is read for. Returns the line and the label it painted.
|
|
910
935
|
*/
|
|
911
|
-
function phaseRule(
|
|
936
|
+
function phaseRule(title, steps, rights, width) {
|
|
912
937
|
const cols = Math.max(1, Number(width) || 1);
|
|
913
|
-
const
|
|
914
|
-
const
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
938
|
+
const named = String(title ?? '');
|
|
939
|
+
const stepText = String(steps ?? '');
|
|
940
|
+
const fill = rights.length > 0;
|
|
941
|
+
const tails = [...rights.map((right) => ` ${right}`), ''];
|
|
942
|
+
const draw = (label, tail) => {
|
|
943
|
+
const head = `── ${label}`;
|
|
944
|
+
if (!fill) return visibleLength(head) <= cols ? head : null;
|
|
945
|
+
const dashes = cols - visibleLength(head) - 1 - visibleLength(tail);
|
|
946
|
+
return dashes >= (tail ? 2 : 0) ? `${head} ${'─'.repeat(dashes)}${tail}` : null;
|
|
947
|
+
};
|
|
948
|
+
for (const tail of tails) {
|
|
949
|
+
const whole = stepText ? `${named} · ${stepText}` : named;
|
|
950
|
+
const line = draw(whole, tail);
|
|
951
|
+
if (line) return { line, label: whole };
|
|
952
|
+
const room = cols - visibleLength(`── ${named} · `) - (fill ? 1 + (tail ? 2 : 0) + visibleLength(tail) : 0);
|
|
953
|
+
if (stepText && room >= 4) {
|
|
954
|
+
const label = `${named} · ${cut(stepText, room)}`;
|
|
955
|
+
return { line: draw(label, tail), label };
|
|
956
|
+
}
|
|
957
|
+
const bare = draw(named, tail);
|
|
958
|
+
if (bare) return { line: bare, label: named };
|
|
919
959
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
return `${head}${'─'.repeat(Math.max(2, cols - visibleLength(head) - visibleLength(tail)))}${tail}`;
|
|
960
|
+
// Too narrow for the title itself: the one place a cut is left.
|
|
961
|
+
return { line: cut(`── ${named}`, cols), label: named };
|
|
923
962
|
}
|
|
924
963
|
|
|
925
964
|
function segmentHeader(name, elapsed, width) {
|