@azure-id/orc 2.0.2 → 2.1.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.
Files changed (129) hide show
  1. package/CHANGELOG.md +455 -0
  2. package/README-id.md +52 -23
  3. package/README.md +36 -20
  4. package/bin/build-agents.js +205 -206
  5. package/bin/clear-logs.js +620 -0
  6. package/bin/cli.js +47417 -46420
  7. package/bin/fix.js +459 -0
  8. package/bin/gotcha.js +288 -8
  9. package/bin/habit.js +1538 -1453
  10. package/bin/mockrun-catalog.js +3 -0
  11. package/bin/onboarding-content.js +2 -1
  12. package/bin/pricing.json +28 -21
  13. package/bin/trace-write.js +869 -657
  14. package/bin/verify-contracts.js +5552 -5471
  15. package/bin/verify-package.js +716 -705
  16. package/bin/webui/api.js +125 -6
  17. package/bin/webui/css/panels/hookui.css +79 -0
  18. package/bin/webui/fixtures/behaviour.js +30 -0
  19. package/bin/webui/fixtures/extra.js +2036 -2036
  20. package/bin/webui/fixtures/flow.js +81 -81
  21. package/bin/webui/fixtures/hookui.js +506 -431
  22. package/bin/webui/fixtures/index.js +32 -3
  23. package/bin/webui/fixtures/knowledge.js +2 -2
  24. package/bin/webui/fixtures/maintenance.js +123 -42
  25. package/bin/webui/fixtures/rules.js +1007 -1002
  26. package/bin/webui/fixtures/settings.js +6 -7
  27. package/bin/webui/fixtures/stats.js +9 -6
  28. package/bin/webui/fixtures/wait.js +98 -97
  29. package/bin/webui/i18n/en/behaviour.json +13 -0
  30. package/bin/webui/i18n/en/hookui.json +290 -194
  31. package/bin/webui/i18n/en/maintenance.json +71 -52
  32. package/bin/webui/i18n/en/settings.json +60 -60
  33. package/bin/webui/i18n/id/behaviour.json +13 -0
  34. package/bin/webui/i18n/id/hookui.json +290 -194
  35. package/bin/webui/i18n/id/maintenance.json +71 -52
  36. package/bin/webui/i18n/id/settings.json +60 -60
  37. package/bin/webui/js/05-banners.js +181 -172
  38. package/bin/webui/js/06-edit.js +193 -183
  39. package/bin/webui/js/panels/behaviour.js +77 -12
  40. package/bin/webui/js/panels/hookui.js +2196 -1691
  41. package/bin/webui/js/panels/maintenance.js +336 -235
  42. package/bin/webui/js/panels/settings.js +726 -719
  43. package/mock-run/INDEX.md +1 -0
  44. package/mock-run/a-normal-day.md +2 -2
  45. package/mock-run/extra-slots.md +177 -177
  46. package/mock-run/habits.md +1 -1
  47. package/mock-run/orc-aftermath.md +392 -392
  48. package/mock-run/orc-budget.md +29 -33
  49. package/mock-run/orc-cli.md +200 -200
  50. package/mock-run/orc-explain.md +86 -86
  51. package/mock-run/orc-extra.md +389 -392
  52. package/mock-run/orc-fast.md +2 -2
  53. package/mock-run/orc-fix.md +87 -0
  54. package/mock-run/orc-pattern.md +112 -112
  55. package/mock-run/orc-quick.md +146 -146
  56. package/mock-run/orc.md +4 -4
  57. package/package.json +1 -1
  58. package/templates/agents/MODEL-MAPPING.md +176 -173
  59. package/templates/agents/orc-analyze-mini-sonnet-5-high.md +2 -2
  60. package/templates/agents/{orc-claude-writer-opus-5-med.md → orc-claude-writer-opus-5-low.md} +4 -4
  61. package/templates/agents/orc-executor-haiku-4-5.md +1 -1
  62. package/templates/agents/orc-executor-opus-5-low.md +1 -1
  63. package/templates/agents/orc-executor-sonnet-4-6-high.md +1 -1
  64. package/templates/agents/orc-executor-sonnet-4-6-med.md +1 -1
  65. package/templates/agents/orc-executor-sonnet-5-high.md +2 -2
  66. package/templates/agents/orc-executor-sonnet-5-low.md +155 -0
  67. package/templates/agents/orc-executor-sonnet-5-med.md +155 -0
  68. package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +2 -2
  69. package/templates/agents/orc-planner-mini-sonnet-5-high.md +2 -2
  70. package/templates/agents/{orc-recon-sonnet-4-6-med.md → orc-recon-sonnet-5-med.md} +4 -4
  71. package/templates/agents/orc-retro-sonnet-5-high.md +2 -2
  72. package/templates/agents/{orc-reviewer-opus-5-med.md → orc-reviewer-opus-5-low.md} +4 -4
  73. package/templates/agents/{orc-wiki-scanner-opus-5-med.md → orc-wiki-scanner-opus-5-low.md} +3 -3
  74. package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +2 -2
  75. package/templates/commands/orc-fast.md +10 -10
  76. package/templates/commands/orc-fix.md +18 -0
  77. package/templates/commands/orc-wiki.md +64 -64
  78. package/templates/hooks/README.md +75 -7
  79. package/templates/hooks/orc-session-hook.js +78 -3
  80. package/templates/hooks/orc-statusline-render.js +317 -23
  81. package/templates/hooks/orc-statusline.js +162 -49
  82. package/templates/hooks/orc-subagent-line.js +23 -5
  83. package/templates/hooks/orc-weather-fetch.js +123 -0
  84. package/templates/skills/_shared/extra-dispatch.md +1331 -1331
  85. package/templates/skills/_shared/gotchas.md +27 -7
  86. package/templates/skills/_shared/habits.md +2 -2
  87. package/templates/skills/_shared/opus5-only.md +137 -137
  88. package/templates/skills/_shared/phases/review.md +2 -2
  89. package/templates/skills/_shared/phases/trace-verbs.md +17 -3
  90. package/templates/skills/_shared/phases/trace.md +136 -136
  91. package/templates/skills/_shared/review-slice.md +28 -1
  92. package/templates/skills/_shared/wait.md +1 -0
  93. package/templates/skills/orc/README.md +2 -2
  94. package/templates/skills/orc/SKILL.md +227 -227
  95. package/templates/skills/orc/config.md +25 -27
  96. package/templates/skills/orc/examples/full-run-mock.md +74 -73
  97. package/templates/skills/orc/references/effort-and-mode.md +222 -222
  98. package/templates/skills/orc/references/preflight-report.md +3 -3
  99. package/templates/skills/orc/references/ultra-mode.md +1 -1
  100. package/templates/skills/orc/schemas/checkpoint.md +122 -122
  101. package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +3 -3
  102. package/templates/skills/orc/subskills/orc-review-verify/core.md +1 -1
  103. package/templates/skills/orc-analyze/SKILL.md +4 -2
  104. package/templates/skills/orc-claude/SKILL.md +14 -14
  105. package/templates/skills/orc-claude/examples/claude-run-mock.md +65 -65
  106. package/templates/skills/orc-diy/SKILL.md +5 -5
  107. package/templates/skills/orc-fast/SKILL.md +210 -208
  108. package/templates/skills/orc-fix/SKILL.md +110 -0
  109. package/templates/skills/orc-grill/SKILL.md +232 -232
  110. package/templates/skills/orc-grill/references/grill-doc.md +1 -1
  111. package/templates/skills/orc-mini/SKILL.md +4 -4
  112. package/templates/skills/orc-mini/examples/mini-run-mock.md +4 -2
  113. package/templates/skills/orc-quick/README.md +536 -536
  114. package/templates/skills/orc-quick/SKILL.md +2 -2
  115. package/templates/skills/orc-quick/references/context-doc.md +145 -145
  116. package/templates/skills/orc-quick/references/dispatch-gate.md +220 -219
  117. package/templates/skills/orc-quick/references/gh-mode.md +4 -2
  118. package/templates/skills/orc-retro/SKILL.md +1 -1
  119. package/templates/skills/orc-retro/examples/retro-mock.md +16 -16
  120. package/templates/skills/orc-wiki/README.md +325 -325
  121. package/templates/skills/orc-wiki/SKILL.md +195 -195
  122. package/templates/skills/orc-wiki/examples/wiki-run-mock.md +37 -37
  123. package/templates/skills/orc-wiki/references/extra.md +2 -2
  124. package/templates/skills/orc-wiki/references/partial-refresh.md +276 -271
  125. package/templates/skills/orc-wiki/references/phases/phase-0.md +1 -1
  126. package/templates/skills/orc-wiki/references/phases/phase-2.md +3 -3
  127. package/templates/skills/orc-wiki/references/phases/phase-3c.md +3 -2
  128. package/templates/agents/orc-claude-writer-opus-4-8-high.md +0 -48
  129. package/templates/agents/orc-wiki-scanner-opus-4-8-high.md +0 -74
@@ -95,6 +95,10 @@ const BINDINGS = {
95
95
  "cost.usd": (c) => num(c.payload.cost && c.payload.cost.total_cost_usd),
96
96
  "cost.rate_usd_h": (c) => ratePerHour(c),
97
97
  "cost.wall_ms": (c) => num(c.payload.cost && c.payload.cost.total_duration_ms),
98
+ // Whole minutes, for the api-time / wall-time parts. A raw ms count read
99
+ // as minutes is `api 960000m`.
100
+ "cost.api_min": (c) => mins(num(c.payload.cost && c.payload.cost.total_api_duration_ms)),
101
+ "cost.wall_min": (c) => mins(num(c.payload.cost && c.payload.cost.total_duration_ms)),
98
102
  "cost.api_ms": (c) => num(c.payload.cost && c.payload.cost.total_api_duration_ms),
99
103
  "cost.api_ratio": (c) => ratioPct(num(c.payload.cost && c.payload.cost.total_api_duration_ms), num(c.payload.cost && c.payload.cost.total_duration_ms)),
100
104
  "cost.lines_added": (c) => num(c.payload.cost && c.payload.cost.total_lines_added),
@@ -292,6 +296,16 @@ const BINDINGS = {
292
296
  "clock.now": (c) => c.now,
293
297
  "session.elapsed_min": (c) => (c.ledger.started_at ? Math.max(0, Math.round((c.now - c.ledger.started_at) / 60000)) : null),
294
298
  "static.text": () => null, // supplied per-op by `lit`; present so the set is total
299
+
300
+ // — weather (v2.0.4): a cache file the detached fetcher writes ————————
301
+ // The hook never waits on the network; these read what the last fetch left.
302
+ "weather.temp": (c) => {
303
+ const w = c.scan.weather;
304
+ if (!w) return null;
305
+ return num(c.param && c.param.units === "us" ? w.temp_f : w.temp_c);
306
+ },
307
+ "weather.state": (c) => (c.scan.weather && c.scan.weather.state) || null,
308
+ "weather.desc": (c) => (c.scan.weather && c.scan.weather.desc) || null,
295
309
  };
296
310
 
297
311
  // ── small helpers the bindings share ────────────────────────────────────────
@@ -468,6 +482,20 @@ function trendOf(series) {
468
482
  }
469
483
 
470
484
  // ── Number formatting (design-language.md §4) ───────────────────────────────
485
+
486
+ function mins(ms) {
487
+ return ms == null ? null : Math.round(ms / 60000);
488
+ }
489
+ // A `tf` on a val op reads the number as epoch ms and prints LOCAL time. The
490
+ // raw epoch is never a clock.
491
+ function formatTime(ms, tf) {
492
+ const d = new Date(ms);
493
+ const p = (n) => (n < 10 ? "0" : "") + n;
494
+ const h = d.getHours();
495
+ if (tf === "h:mm a") return (h % 12 || 12) + ":" + p(d.getMinutes()) + " " + (h < 12 ? "am" : "pm");
496
+ const s = p(h) + ":" + p(d.getMinutes());
497
+ return tf === "HH:mm:ss" ? s + ":" + p(d.getSeconds()) : s;
498
+ }
471
499
  function formatValue(v, f) {
472
500
  if (v == null) return null;
473
501
  if (typeof v !== "number") return String(v);
@@ -524,6 +552,72 @@ function truncate(s, max, mode) {
524
552
  return s.slice(0, max - 1) + "…";
525
553
  }
526
554
 
555
+ // ── Cell width and styled text (v2.0.4) ─────────────────────────────────────
556
+ // Terminal cells, not string length. Only a line that uses flex, align or a
557
+ // caption pays for this; every other line never measures.
558
+ const SGR_RE = new RegExp(ESC + "\\[[0-9;?]*[A-Za-z]", "g");
559
+ const OSC8_RE = new RegExp(ESC + "\\]8;;[^" + BEL + "]*" + BEL, "g");
560
+ const SENT_RE = /\u0000[^\u0000]*\u0000/g;
561
+ const WIDE = [
562
+ [0x1100, 0x115f], [0x231a, 0x231b], [0x23e9, 0x23ec], [0x23f0, 0x23f0], [0x23f3, 0x23f3],
563
+ [0x25fd, 0x25fe], [0x2614, 0x2615], [0x2648, 0x2653], [0x267f, 0x267f], [0x2693, 0x2693],
564
+ [0x26a1, 0x26a1], [0x26aa, 0x26ab], [0x26bd, 0x26be], [0x26c4, 0x26c5], [0x26ce, 0x26ce],
565
+ [0x26d4, 0x26d4], [0x26ea, 0x26ea], [0x26f2, 0x26f3], [0x26f5, 0x26f5], [0x26fa, 0x26fa],
566
+ [0x26fd, 0x26fd], [0x2705, 0x2705], [0x270a, 0x270b], [0x2728, 0x2728], [0x274c, 0x274c],
567
+ [0x274e, 0x274e], [0x2753, 0x2755], [0x2757, 0x2757], [0x2795, 0x2797], [0x27b0, 0x27b0],
568
+ [0x27bf, 0x27bf], [0x2b1b, 0x2b1c], [0x2b50, 0x2b50], [0x2b55, 0x2b55], [0x2e80, 0xa4cf],
569
+ [0xac00, 0xd7a3], [0xf900, 0xfaff], [0xfe30, 0xfe4f], [0xff00, 0xff60], [0xffe0, 0xffe6],
570
+ [0x1f000, 0x1faff],
571
+ ];
572
+ function cellWidth(s) {
573
+ const t = String(s == null ? "" : s).replace(OSC8_RE, "").replace(SGR_RE, "").replace(SENT_RE, "");
574
+ let n = 0;
575
+ for (const ch of t) {
576
+ const cp = ch.codePointAt(0);
577
+ if (cp < 0x300) {
578
+ n += 1;
579
+ continue;
580
+ }
581
+ // Zero width: combining marks, the ZW/direction marks, VS16.
582
+ if ((cp >= 0x300 && cp <= 0x36f) || (cp >= 0x1ab0 && cp <= 0x1aff) || (cp >= 0x1dc0 && cp <= 0x1dff) ||
583
+ (cp >= 0x20d0 && cp <= 0x20ff) || (cp >= 0xfe20 && cp <= 0xfe2f) || (cp >= 0x200b && cp <= 0x200f) || cp === 0xfe0f)
584
+ continue;
585
+ let w = 1;
586
+ for (const r of WIDE) {
587
+ if (cp < r[0]) break;
588
+ if (cp <= r[1]) {
589
+ w = 2;
590
+ break;
591
+ }
592
+ }
593
+ n += w;
594
+ }
595
+ return n;
596
+ }
597
+ function styleMap(from, to) {
598
+ const m = {};
599
+ const a = [...from];
600
+ const b = [...to];
601
+ for (let i = 0; i < a.length; i++) {
602
+ m[a[i]] = b[i];
603
+ m[a[i].toUpperCase()] = b[i];
604
+ }
605
+ return m;
606
+ }
607
+ const STYLES = {
608
+ small: styleMap("abcdefghijklmnopqrstuvwxyz", "ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘǫʀꜱᴛᴜᴠᴡxʏᴢ"),
609
+ super: styleMap("0123456789+-=()abcdefghijklmnoprstuvwxyz", "⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛʷˣʸᶻ"),
610
+ sub: styleMap("0123456789+-=()aehijklmnoprstuvx", "₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ"),
611
+ };
612
+ // A character with no styled form passes through unchanged.
613
+ function styleText(s, style) {
614
+ const m = STYLES[style];
615
+ if (!m || s == null) return s;
616
+ let out = "";
617
+ for (const ch of String(s)) out += m[ch] || ch;
618
+ return out;
619
+ }
620
+
527
621
  // ── The renderers ──────────────────────────────────────────────────────────
528
622
  // Every one takes a value, a glyph set and a width, and returns cells. None of
529
623
  // them decides a colour: colour arrives as precomputed `sgr` ops around them.
@@ -532,6 +626,11 @@ function glyphs(gs, ascii) {
532
626
  return ascii && gs.ascii ? Object.assign({}, gs, gs.ascii) : gs;
533
627
  }
534
628
 
629
+ // D3: a value above zero fills at least one cell. 6% on ten cells is not 0%.
630
+ function fullCells(p, w) {
631
+ const n = Math.floor((p / 100) * w);
632
+ return Math.min(w, p > 0 ? Math.max(1, n) : n);
633
+ }
535
634
  function barCells(pct, g, w, kind) {
536
635
  const p = Math.max(0, Math.min(100, pct == null ? 0 : pct));
537
636
  const exact = (p / 100) * w;
@@ -544,7 +643,8 @@ function barCells(pct, g, w, kind) {
544
643
  if (full < w && idx >= 0) out += g.part[idx];
545
644
  return padCells(out, w, g.empty);
546
645
  }
547
- out = g.fill.repeat(full) + g.empty.repeat(Math.max(0, w - full));
646
+ const n = fullCells(p, w);
647
+ out = g.fill.repeat(n) + g.empty.repeat(Math.max(0, w - n));
548
648
  return out.slice(0, w);
549
649
  }
550
650
  function padCells(s, w, filler) {
@@ -563,7 +663,7 @@ function microCell(pct, g) {
563
663
  }
564
664
  function dotsCells(pct, g, w) {
565
665
  const p = Math.max(0, Math.min(100, pct == null ? 0 : pct));
566
- const on = Math.round((p / 100) * w);
666
+ const on = Math.min(w, p > 0 ? Math.max(1, Math.round((p / 100) * w)) : 0);
567
667
  return g.on.repeat(on) + g.off.repeat(Math.max(0, w - on));
568
668
  }
569
669
  function markerCells(pct, g, w) {
@@ -607,9 +707,12 @@ function trafficCells(idx, n, g) {
607
707
  // advances while the session is active and FREEZES when it is idle, which is
608
708
  // true and is the point. ORC_STATUSLINE_MOTION=0 REMOVES it — a frozen frame of
609
709
  // a cycling animation is a bug that looks like a hang.
610
- function motifFrame(set, now, ms, motion) {
710
+ // v2.1.0 (DE-23): with a refresh timer the hook passes `step` (one per run), and
711
+ // the frame is "the one after the last one shown" — never skipped, never repeated.
712
+ function motifFrame(set, now, ms, motion, step) {
611
713
  if (!set || !set.length) return "";
612
714
  if (!motion) return set[0];
715
+ if (Number.isInteger(step)) return set[step % set.length];
613
716
  return set[Math.floor(now / (ms || 200)) % set.length];
614
717
  }
615
718
 
@@ -677,38 +780,114 @@ function render(prog, ctx) {
677
780
  scan: ctx.scan || {},
678
781
  derived: ctx.derived || { verdict: null, reasons: [], version: null },
679
782
  now: ctx.now || Date.now(),
783
+ // v2.1.0 (DE-23): one step per run when a refresh timer is set; else null.
784
+ step: Number.isInteger(ctx.step) ? ctx.step : null,
680
785
  };
681
786
  const hasRun = !!(rctx.scan.phase || rctx.scan.spawns);
682
787
 
683
788
  // H4 PLAN — one lookup, no measuring. The last plan whose `cols` ≤ COLUMNS
684
789
  // wins; its ids are dropped.
685
790
  const dropped = new Set();
686
- if (cols && Array.isArray(prog.plans)) {
791
+ // A plan applies only when the terminal is narrower than the widest line.
792
+ const need = (prog.lines || []).reduce((m, l) => Math.max(m, Number(l && l.static_width) || 0), 0);
793
+ if (cols && Array.isArray(prog.plans) && !(need && cols >= need)) {
687
794
  let chosen = null;
688
795
  for (const p of prog.plans) if (p.cols <= cols && (!chosen || p.cols >= chosen.cols)) chosen = p;
796
+ // Narrower than every plan: the smallest real plan (the most it drops).
797
+ if (need && (!chosen || !chosen.cols)) {
798
+ for (const p of prog.plans) if (p.cols > 0 && (!chosen || !chosen.cols || p.cols < chosen.cols)) chosen = p;
799
+ }
689
800
  if (chosen) for (const id of chosen.drop) dropped.add(id);
690
801
  }
691
802
 
692
803
  const lines = [];
693
804
  for (const line of prog.lines || []) {
694
805
  let out = "";
806
+ const o = { color, ascii, motion, cols, hasRun, dropped, flex: false, caps: [] };
695
807
  for (const op of line.ops || []) {
696
808
  // H5 FAILURE ISOLATION — each item renders inside its own try. One bad
697
809
  // component never takes the bar down; it emits its unknown form and the
698
810
  // rest of the line survives.
699
811
  try {
700
- out += walk(op, prog, rctx, { color, ascii, motion, cols, hasRun, dropped }, errors);
812
+ out += walk(op, prog, rctx, o, errors);
701
813
  } catch (e) {
702
814
  errors.push(String((e && e.message) || e));
703
815
  }
704
816
  }
705
- lines.push(line.prefix ? line.prefix + out : out);
817
+ let text = line.prefix ? line.prefix + out : out;
818
+ // A line with no flex, no align and no caption stops here, unmeasured.
819
+ const align = line.align === "right" || line.align === "center" ? line.align : null;
820
+ if (!o.flex && !o.caps.length && !(align && cols > 0)) {
821
+ lines.push(text);
822
+ continue;
823
+ }
824
+ if (o.flex) text = applyFlex(text, cols);
825
+ else if (align && cols > 0) {
826
+ const gap = cols - cellWidth(text);
827
+ if (gap > 0) text = " ".repeat(align === "right" ? gap : Math.floor(gap / 2)) + text;
828
+ }
829
+ if (!o.caps.length) {
830
+ lines.push(text);
831
+ continue;
832
+ }
833
+ const rows = captionRows(text, o.caps);
834
+ if (rows.above) lines.push(rows.above);
835
+ lines.push(rows.line);
836
+ if (rows.below) lines.push(rows.below);
706
837
  }
707
838
  // A trailing empty line is not a line. It would print as a blank row.
708
839
  while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
840
+ // D1: Claude Code trims every row, so a caption row or an indent that starts
841
+ // with a space moves to column 0. U+2800 is one blank cell it keeps. A row
842
+ // that starts with ESC is not trimmed and stays as it is.
843
+ if (!ascii) for (let i = 0; i < lines.length; i++) if (lines[i][0] === " ") lines[i] = "\u2800" + lines[i].slice(1);
709
844
  return { text: lines.join("\n"), errors };
710
845
  }
711
846
 
847
+ // FLEX: slack = cols − the line's width, shared by weight (the remainder to the
848
+ // last); each flex prints max(min, share). cols 0 → each prints its min. One
849
+ // column stays free: a row exactly COLUMNS wide can wrap (D5).
850
+ function applyFlex(text, cols) {
851
+ const re = /\u0000F(\d+),(\d+)\u0000/g;
852
+ const fl = [];
853
+ let m;
854
+ while ((m = re.exec(text))) fl.push({ w: Number(m[1]), min: Number(m[2]) });
855
+ const slack = cols > 0 ? Math.max(0, cols - cellWidth(text) - 1) : 0;
856
+ const tot = fl.reduce((a, f) => a + f.w, 0) || 1;
857
+ let used = 0;
858
+ const n = fl.map((f, i) => {
859
+ const share = i === fl.length - 1 ? slack - used : Math.floor((slack * f.w) / tot);
860
+ used += share;
861
+ return Math.max(f.min, cols > 0 ? share : 0);
862
+ });
863
+ let i = 0;
864
+ return text.replace(re, () => " ".repeat(n[i++]));
865
+ }
866
+
867
+ // CAPTIONS: one row above and one below, each placed left to right; a caption
868
+ // that would overlap the previous one starts one space after it.
869
+ function captionRows(text, caps) {
870
+ const re = /\u0000C(\d+)\u0000/g;
871
+ const at = [];
872
+ let m;
873
+ while ((m = re.exec(text))) at.push({ cap: caps[Number(m[1])], col: cellWidth(text.slice(0, m.index)) });
874
+ const row = { above: "", below: "" };
875
+ const cur = { above: 0, below: 0 };
876
+ const any = { above: false, below: false };
877
+ for (const a of at) {
878
+ const k = a.cap;
879
+ const col = a.col + k.off;
880
+ let start = col;
881
+ if (k.align === "center") start = col + Math.max(0, Math.floor((k.content - k.w) / 2));
882
+ else if (k.align === "right") start = col + Math.max(0, k.content - k.w);
883
+ if (any[k.pos] && start < cur[k.pos] + 1) start = cur[k.pos] + 1;
884
+ row[k.pos] += " ".repeat(Math.max(0, start - cur[k.pos])) + k.text;
885
+ cur[k.pos] = Math.max(start, cur[k.pos]) + k.w;
886
+ any[k.pos] = true;
887
+ }
888
+ return { above: row.above, line: text.replace(re, ""), below: row.below };
889
+ }
890
+
712
891
  function walk(op, prog, c, o, errors) {
713
892
  switch (op.op) {
714
893
  case "lit":
@@ -735,7 +914,9 @@ function walk(op, prog, c, o, errors) {
735
914
  return OSC8 + url + BEL + inner + OSC8 + BEL;
736
915
  }
737
916
  case "flex":
738
- return " ".repeat(Math.max(op.min || 0, 0)); // slack is applied in emit()
917
+ // A sentinel; render() swaps it for its share of the slack.
918
+ o.flex = true;
919
+ return "\u0000F" + Math.max(1, op.w || 1) + "," + Math.max(op.min || 0, 0) + "\u0000";
739
920
  case "val":
740
921
  return renderVal(op, prog, c, o);
741
922
  case "bar":
@@ -746,6 +927,8 @@ function walk(op, prog, c, o, errors) {
746
927
  return renderMotif(op, prog, c, o);
747
928
  case "series":
748
929
  return renderSeries(op, prog, c, o);
930
+ case "sprite":
931
+ return renderSprite(op, c, o);
749
932
  default:
750
933
  throw new Error("unknown op: " + op.op);
751
934
  }
@@ -760,7 +943,7 @@ function children(op, prog, c, o, errors) {
760
943
  function walkItem(op, prog, c, o, errors) {
761
944
  if (o.dropped.has(op.id)) return "";
762
945
  let probe = null;
763
- if (op.b) probe = bind(op.b, c);
946
+ if (op.b) probe = bind(op.b, op.p ? Object.assign({}, c, { param: op.p }) : c);
764
947
  if (hidden(op, probe, { cols: o.cols, hasRun: o.hasRun })) return "";
765
948
  let body;
766
949
  try {
@@ -773,13 +956,33 @@ function walkItem(op, prog, c, o, errors) {
773
956
  if (body === "" && !op.draw_empty) return "";
774
957
  const l = " ".repeat(op.pad_l || 0);
775
958
  const r = " ".repeat(op.pad_r || 0);
776
- return l + body + r;
959
+ const all = l + body + r;
960
+ // A CAPTION rides as a sentinel at the item's start; render() measures the
961
+ // column once the line is final (after flex and align) and strips it.
962
+ if (op.cap && body !== "" && o.caps) {
963
+ let t = "";
964
+ for (const ch of op.cap.ops || []) t += walk(ch, prog, c, o, errors);
965
+ if (t === "") return all;
966
+ const sw = op.sw || 0;
967
+ const pl = op.pad_l || 0;
968
+ o.caps.push({
969
+ pos: op.cap.pos === "above" ? "above" : "below",
970
+ align: op.cap.align || "left",
971
+ text: t,
972
+ w: cellWidth(t),
973
+ off: sw + pl,
974
+ content: Math.max(0, cellWidth(all) - sw - pl - (op.pad_r || 0)),
975
+ });
976
+ return "\u0000C" + (o.caps.length - 1) + "\u0000" + all;
977
+ }
978
+ return all;
777
979
  }
778
980
 
779
981
  function renderVal(op, prog, c, o) {
780
982
  const f = prog.formats[op.f] || {};
781
- const v = bind(op.b, c);
782
- let s = formatValue(v, f);
983
+ // `p` is the op's own parameter (the `config` part's key).
984
+ const v = bind(op.b, op.p ? Object.assign({}, c, { param: op.p }) : c);
985
+ let s = op.tf && typeof v === "number" && isFinite(v) ? formatTime(v, op.tf) : formatValue(v, f);
783
986
  // UNKNOWN IS NOT ZERO. An em dash keeps the slot and says "not measured"; a
784
987
  // `0` would say the thing was free. And it carries NEITHER the prefix nor the
785
988
  // suffix — `Dur —m` reads as "minus minutes".
@@ -789,6 +992,7 @@ function renderVal(op, prog, c, o) {
789
992
  if (f.case === "upper") s = s.toUpperCase();
790
993
  else if (f.case === "lower") s = s.toLowerCase();
791
994
  else if (f.case === "title") s = s.replace(/\w/g, (m) => m.toUpperCase());
995
+ if (f.case === "small" || f.case === "super" || f.case === "sub") s = styleText(s, f.case);
792
996
  s = truncate(s, f.max_len, f.truncate);
793
997
  s = (f.prefix || "") + pad(s, f) + (f.suffix || "");
794
998
  // A ramp derives the colour from the VALUE, so it colours a number as much as
@@ -805,9 +1009,27 @@ function renderVal(op, prog, c, o) {
805
1009
 
806
1010
  function renderBar(op, prog, c, o) {
807
1011
  const g = glyphs(prog.glyphsets[op.g] || {}, o.ascii);
808
- const v = bind(op.b, c);
1012
+ let v = bind(op.b, c);
1013
+ // An ordered state (`o`) becomes its rank as a percent; an unlisted state
1014
+ // is unknown. Without this `effort` on `dots` draws nothing for "high".
1015
+ if (Array.isArray(op.o) && typeof v === "string") {
1016
+ const i = op.o.indexOf(v);
1017
+ v = i < 0 ? null : ((i + 1) / op.o.length) * 100;
1018
+ }
809
1019
  const w = op.w || 10;
810
- const sgr = op.r != null ? rampSgr(prog.ramps[op.r], v, o.color) : "";
1020
+ const single = op.k === "gauge" || op.k === "ring" || op.k === "micro";
1021
+ // D2: UNKNOWN IS NOT ZERO. An empty bar or a 0% glyph reads as "0%"; the
1022
+ // unknown form keeps the width and says "not measured".
1023
+ if (v == null || (typeof v === "number" && !Number.isFinite(v))) {
1024
+ const d = o.ascii ? "-" : "—";
1025
+ return single ? d : d + " ".repeat(Math.max(0, w - 1));
1026
+ }
1027
+ const ramp = op.r != null ? rampSgr(prog.ramps[op.r], v, o.color) : "";
1028
+ // Precedence: `gc` (gradient) > `fs`/`es` > the ramp. A single-cell kind
1029
+ // takes `fs` as its whole colour.
1030
+ const split = o.color && (op.fs || op.es);
1031
+ const sgr = split && single && op.fs ? op.fs : ramp;
1032
+ const p = Math.max(0, Math.min(100, v == null ? 0 : v));
811
1033
  let cells;
812
1034
  switch (op.k) {
813
1035
  case "gauge":
@@ -834,16 +1056,20 @@ function renderBar(op, prog, c, o) {
834
1056
  case "gradient": {
835
1057
  // Each cell coloured by ITS OWN position along the ramp. Decoration, and
836
1058
  // the panel labels it as decoration — never a component default.
837
- if (!o.color || op.r == null) {
1059
+ const full = fullCells(p, w);
1060
+ if (o.color && Array.isArray(op.gc) && op.gc.length) {
1061
+ let s1 = "";
1062
+ for (let i = 0; i < w; i++) s1 += (op.gc[i] || op.gc[op.gc.length - 1]) + (i < full ? g.fill : g.empty);
1063
+ return s1 + RESET;
1064
+ }
1065
+ if (split || !o.color || op.r == null) {
838
1066
  cells = barCells(v, g, w, "blocks");
839
1067
  break;
840
1068
  }
841
- const ramp = prog.ramps[op.r];
842
- const p = Math.max(0, Math.min(100, v == null ? 0 : v));
843
- const full = Math.floor((p / 100) * w);
1069
+ const rp = prog.ramps[op.r];
844
1070
  let s2 = "";
845
1071
  for (let i = 0; i < w; i++) {
846
- s2 += rampSgr(ramp, ((i + 1) / w) * 100, true) + (i < full ? g.fill : g.empty);
1072
+ s2 += rampSgr(rp, ((i + 1) / w) * 100, true) + (i < full ? g.fill : g.empty);
847
1073
  }
848
1074
  return s2 + RESET;
849
1075
  }
@@ -856,9 +1082,31 @@ function renderBar(op, prog, c, o) {
856
1082
  default:
857
1083
  cells = barCells(v, g, w, "blocks");
858
1084
  }
1085
+ if (split && !single) {
1086
+ const n = filledCount(op.k, p, w, g, op.thr);
1087
+ const list = [...cells];
1088
+ // A glyph set whose cells are not one code point each cannot be cut by
1089
+ // index; it takes the filled colour whole.
1090
+ if (list.length !== w) return (op.fs || ramp) ? (op.fs || ramp) + cells + RESET : cells;
1091
+ const run = (s, c) => (s === "" ? "" : c ? c + s + RESET : s);
1092
+ return run(list.slice(0, n).join(""), op.fs || ramp) + run(list.slice(n).join(""), op.es || ramp);
1093
+ }
859
1094
  return sgr ? sgr + cells + RESET : cells;
860
1095
  }
861
1096
 
1097
+ // How many leading cells a bar kind draws as FILLED — the cut for `fs`/`es`.
1098
+ function filledCount(k, p, w, g, thr) {
1099
+ if (k === "dots" || k === "meter") return Math.min(w, p > 0 ? Math.max(1, Math.round((p / 100) * w)) : 0);
1100
+ if (k === "marker") return Math.min(w - 1, Math.round((p / 100) * (w - 1))) + 1;
1101
+ const full = Math.floor((p / 100) * w);
1102
+ if (k === "fine" && g.part && g.part.length && full < w) {
1103
+ const idx = Math.min(g.part.length - 1, Math.floor(((p / 100) * w - full) * (g.part.length + 1)) - 1);
1104
+ return idx >= 0 ? full + 1 : full;
1105
+ }
1106
+ if (k === "split" || k === "fine") return Math.min(w, full);
1107
+ return fullCells(p, w);
1108
+ }
1109
+
862
1110
  function renderState(op, prog, c, o) {
863
1111
  const m = prog.statemaps[op.m] || { glyphs: {}, sgr: {} };
864
1112
  const v = bind(op.b, c);
@@ -877,15 +1125,25 @@ function renderMotif(op, prog, c, o) {
877
1125
  const set = (m.motifs && (m.motifs[String(kind)] || m.motifs.generic)) || null;
878
1126
  if (!set) return "";
879
1127
  const frames = o.ascii ? set.a : set.u;
880
- return motifFrame(frames, c.now, set.ms, o.motion);
1128
+ return motifFrame(frames, c.now, set.ms, o.motion, c.step);
881
1129
  }
882
1130
 
883
1131
  function renderSeries(op, prog, c, o) {
884
1132
  const g = glyphs(prog.glyphsets[op.g] || {}, o.ascii);
885
- const s = seriesOf(c, op.s);
1133
+ let s = seriesOf(c, op.s);
886
1134
  const w = op.w || 8;
1135
+ const dash = o.ascii ? "-" : "—";
1136
+ // D4: a spark takes the CURRENT value as its live last point, so it shows
1137
+ // something on the first render and moves on every render.
1138
+ if (op.k === "spark" || op.k === "spark-braille" || !op.k) {
1139
+ const cur = op.b ? bind(op.b, op.p ? Object.assign({}, c, { param: op.p }) : c) : null;
1140
+ if (typeof cur === "number" && Number.isFinite(cur)) s = (s || []).concat([cur]);
1141
+ if (!s || !s.length) return dash + " ".repeat(Math.max(0, w - 1));
1142
+ }
887
1143
  switch (op.k) {
888
1144
  case "trend":
1145
+ // One point has no direction; the flat arrow would claim one.
1146
+ if (!s || s.length < 2) return dash;
889
1147
  return trendGlyph(trendOf(s), g);
890
1148
  case "delta": {
891
1149
  if (!s || s.length < 2) return "—";
@@ -894,7 +1152,7 @@ function renderSeries(op, prog, c, o) {
894
1152
  }
895
1153
  case "spark-braille": {
896
1154
  const set = g.spark_braille || ["⠀", "⣀", "⣤", "⣶", "⣿"];
897
- if (!s || !s.length) return " ".repeat(w);
1155
+ s = s.slice(-2 * w);
898
1156
  const lo = Math.min(...s);
899
1157
  const hi = Math.max(...s);
900
1158
  const span = hi - lo || 1;
@@ -903,20 +1161,56 @@ function renderSeries(op, prog, c, o) {
903
1161
  const v = (s[i] + (s[i + 1] == null ? s[i] : s[i + 1])) / 2;
904
1162
  out += set[Math.min(set.length - 1, Math.round(((v - lo) / span) * (set.length - 1)))];
905
1163
  }
906
- return out;
1164
+ // Exactly `w` cells, like `spark`: the history grows in from the right.
1165
+ const n = [...out].length;
1166
+ return n >= w ? out : " ".repeat(w - n) + out;
907
1167
  }
908
1168
  default:
909
1169
  return sparkCells(s, g, w);
910
1170
  }
911
1171
  }
912
1172
 
1173
+ // ── Sprites (v2.0.4) ───────────────────────────────────────────────────────
1174
+ // A pet on a track of `w` cells. The frame and the column both come off the
1175
+ // wall clock, so it moves only while the line redraws. Every frame cell is one
1176
+ // terminal cell (the CLI checks that), so a cell is a code point here.
1177
+ // MOTION=0 puts frame 0 at the left: motion REMOVED, never frozen mid-run.
1178
+ function renderSprite(op, c, o) {
1179
+ const w = Math.max(1, op.w || 16);
1180
+ const set = o.ascii && Array.isArray(op.a) && op.a.length ? op.a : op.f || [];
1181
+ if (!set.length) return " ".repeat(w);
1182
+ const tick = !o.motion ? 0 : Number.isInteger(c.step) ? c.step : Math.floor(c.now / (op.ms > 0 ? op.ms : 1000));
1183
+ const frame = [...String(set[tick % set.length])];
1184
+ const fw = frame.length;
1185
+ let x = 0;
1186
+ if (o.motion && op.mode === "run") x = (tick % (w + fw - 1)) - (fw - 1); // always one cell on the track
1187
+ else if (o.motion && op.mode === "bounce") {
1188
+ const span = Math.max(1, w - fw);
1189
+ const p = tick % (2 * span);
1190
+ x = p < span ? p : 2 * span - p;
1191
+ }
1192
+ // `ahead_a` / `behind_a` are the ASCII twins of the fillers, when they differ.
1193
+ const ahead = (o.ascii && op.ahead_a != null ? op.ahead_a : op.ahead) || " ";
1194
+ const behind = (o.ascii && op.behind_a != null ? op.behind_a : op.behind) || " ";
1195
+ let out = "";
1196
+ for (let i = 0; i < w; i++) out += i < x ? behind : i >= x + fw ? ahead : frame[i - x];
1197
+ return out;
1198
+ }
1199
+
1200
+ // Every op the walker handles. The hooks check a compiled program against
1201
+ // this before running it, so an op from a newer compiler is a skew, not a dash.
1202
+ const OPS = { lit: 1, sgr: 1, reset: 1, item: 1, cond: 1, link: 1, flex: 1, val: 1, bar: 1, state: 1, motif: 1, series: 1, sprite: 1 };
1203
+
913
1204
  module.exports = {
914
1205
  BINDINGS,
1206
+ OPS,
915
1207
  render,
916
1208
  formatValue,
917
1209
  barCells,
918
1210
  sparkCells,
919
1211
  motifFrame,
1212
+ cellWidth,
1213
+ styleText,
920
1214
  // exported for the CLI's measure pass and its R1/R4 checks
921
- _internals: { pad, truncate, si, bytes, bandState, hidden, rampSgr, glyphs },
1215
+ _internals: { pad, truncate, si, bytes, bandState, hidden, rampSgr, glyphs, burnRate, tokSpeed },
922
1216
  };