pi-crew 0.10.6 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +215 -0
- package/README.md +0 -1
- package/dist/index.mjs +30623 -29844
- package/package.json +170 -169
- package/schema.json +10 -0
- package/scripts/watch-agent-transcript.mjs +120 -0
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
- package/skills/real-test-pi-crew/SKILL.md +181 -1
- package/src/config/config-validation.ts +0 -2
- package/src/config/defaults.ts +0 -1
- package/src/config/types.ts +0 -5
- package/src/extension/crew-input-router.ts +2 -0
- package/src/extension/crew-vibes/config.ts +1 -77
- package/src/extension/crew-vibes/index.ts +36 -330
- package/src/extension/crew-vibes/render.ts +1 -106
- package/src/extension/help.ts +1 -2
- package/src/extension/register.ts +7 -2
- package/src/extension/registration/command-registration.ts +5 -1
- package/src/extension/registration/commands/manage.ts +5 -4
- package/src/extension/registration/commands/schedules.ts +212 -0
- package/src/extension/registration/commands/shared.ts +56 -2
- package/src/extension/registration/lifecycle-handlers.ts +16 -2
- package/src/extension/registration/schedule-toast-bridge.ts +109 -0
- package/src/extension/registration/subagent-tools.ts +16 -6
- package/src/extension/registration/team-tool.ts +10 -3
- package/src/extension/registration/terminal-status-wiring.ts +172 -0
- package/src/extension/registration/viewers.ts +72 -1
- package/src/extension/team-tool/handle-schedule.ts +120 -1
- package/src/extension/team-tool/handle-settings.ts +14 -2
- package/src/runtime/scheduling/scheduler.ts +120 -6
- package/src/runtime/settings-store.ts +28 -2
- package/src/runtime/task-display.ts +1 -1
- package/src/schema/config-schema.ts +4 -0
- package/src/state/coordination/locks.ts +17 -2
- package/src/ui/adaptive-card.ts +65 -0
- package/src/ui/agents-jobs-browser.ts +791 -0
- package/src/ui/card-colors.ts +36 -7
- package/src/ui/dashboard-panes/agents-pane.ts +52 -17
- package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
- package/src/ui/dashboard-panes/health-pane.ts +7 -5
- package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
- package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
- package/src/ui/dashboard-panes/pane-theme.ts +21 -0
- package/src/ui/dashboard-panes/plan-pane.ts +63 -30
- package/src/ui/dashboard-panes/progress-pane.ts +3 -2
- package/src/ui/dashboard-panes/schedules-pane.ts +177 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
- package/src/ui/dwf-phase-display.ts +3 -20
- package/src/ui/format-helpers.ts +22 -0
- package/src/ui/inline-panel/crew-editor.ts +32 -9
- package/src/ui/inline-panel/index.ts +66 -4
- package/src/ui/key-utils.ts +3 -0
- package/src/ui/keybinding-map.ts +286 -34
- package/src/ui/live-conversation-overlay.ts +176 -46
- package/src/ui/live-run-sidebar.ts +134 -55
- package/src/ui/mascot.ts +34 -17
- package/src/ui/overlays/agent-picker-overlay.ts +81 -26
- package/src/ui/overlays/confirm-overlay.ts +55 -28
- package/src/ui/overlays/help-overlay.ts +108 -53
- package/src/ui/overlays/mailbox-compose-overlay.ts +89 -49
- package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
- package/src/ui/powerbar-publisher.ts +0 -1
- package/src/ui/rail.ts +333 -0
- package/src/ui/run-dashboard.ts +344 -72
- package/src/ui/settings-overlay.ts +84 -42
- package/src/ui/terminal-status.ts +7 -1
- package/src/ui/theme-adapter.ts +0 -45
- package/src/ui/theme-discovery.ts +12 -6
- package/src/ui/tool-progress-formatter.ts +126 -8
- package/src/ui/tool-renderers/brief-mode.ts +8 -66
- package/src/ui/tool-renderers/index.ts +372 -521
- package/src/ui/transcript-viewer.ts +33 -14
- package/src/ui/widget/index.ts +86 -133
- package/src/ui/widget/task-list.ts +64 -32
- package/src/ui/widget/widget-formatters.ts +3 -399
- package/src/ui/widget/widget-model.ts +28 -7
- package/src/ui/widget/widget-renderer.ts +298 -265
- package/src/ui/widget/widget-types.ts +0 -9
- package/src/utils/relative-time.ts +33 -0
- package/src/utils/visual.ts +0 -4
- package/src/extension/crew-vibes/figures.ts +0 -101
- package/src/extension/crew-vibes/font-detect.ts +0 -71
- package/src/extension/crew-vibes/footer.ts +0 -313
- package/src/extension/crew-vibes/speed.ts +0 -286
- package/src/ui/dock-footer.ts +0 -49
- package/src/ui/dynamic-border.ts +0 -35
- package/src/ui/loaders.ts +0 -6
- package/src/ui/overlay-stack.ts +0 -148
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: real-test-pi-crew
|
|
3
3
|
description: >
|
|
4
|
-
End-to-end verification for pi-crew changes: fast critical tests, 3-path kill-switch proof, bundle md5 sync, live TUI probing, smoke team runs, a live feature-action battery (team tool + subagent tools), a surface-mode battery (workers in real tmux/herdr panes, degrade-to-headless),
|
|
4
|
+
End-to-end verification for pi-crew changes: fast critical tests, 3-path kill-switch proof, bundle md5 sync, live TUI probing, smoke team runs, a live feature-action battery (team tool + subagent tools), a surface-mode battery (workers in real tmux/herdr panes, degrade-to-headless), a resource-contract battery (agent .md frontmatter dual-parse, routing render, output contracts), and a real-run UI render battery (every surface rendered from a real run's on-disk state: state glyphs, invented strings, pluralisation, truncation priority, usage formats, width survival).
|
|
5
5
|
When NOT to use: unit tests for isolated modules (use test runner directly); pure test execution.
|
|
6
6
|
|
|
7
7
|
origin: pi-crew
|
|
@@ -54,6 +54,17 @@ triggers:
|
|
|
54
54
|
- "silent bash"
|
|
55
55
|
- "worker killed mid command"
|
|
56
56
|
- "tier 12"
|
|
57
|
+
- "tier 13"
|
|
58
|
+
- "check the UI"
|
|
59
|
+
- "run the UI"
|
|
60
|
+
- "render every surface"
|
|
61
|
+
- "full UI"
|
|
62
|
+
- "does the card look right"
|
|
63
|
+
- "undefined in the widget"
|
|
64
|
+
- "spinner keeps spinning"
|
|
65
|
+
- "hint cut off"
|
|
66
|
+
- "tofu glyph"
|
|
67
|
+
- "catalog png"
|
|
57
68
|
---
|
|
58
69
|
|
|
59
70
|
# real-test-pi-crew
|
|
@@ -808,6 +819,129 @@ node --experimental-strip-types --no-warnings --test --test-force-exit \
|
|
|
808
819
|
|
|
809
820
|
---
|
|
810
821
|
|
|
822
|
+
## Tier 13 — Real-run UI render battery (every surface, real state, no fixtures)
|
|
823
|
+
|
|
824
|
+
**What**: run a REAL team run, then render **every UI surface from that run's on-disk state** (`manifest.json` / `agents.json` / `tasks.json` / the snapshot cache / the real widget model) at several widths, and assert the cross-surface invariants that per-module unit tests structurally cannot see.
|
|
825
|
+
|
|
826
|
+
**Why this is its own tier**: unit tests assert whatever strings their author chose with fixtures their author built; executor summaries claim behaviour. Neither can catch a surface painting something that maps to **no data field** or **contradicts the state**. Measured 2026-09-16: a 4-executor parallel UI migration + an independent verifier + **7857 green tests** left **7 real defects** in the UI — one render pass over a real run found all of them in ~2 minutes. The battery is cheap; the class of bug it kills is invisible to everything else.
|
|
827
|
+
|
|
828
|
+
| Defect class | Real example (2026-09-16) | Why unit tests missed it |
|
|
829
|
+
|---|---|---|
|
|
830
|
+
| **Invented string** (hardcoded, maps to no field) | sidebar painted `122ab0dd · completed · right default` | no test asserts the ABSENCE of unexplained words |
|
|
831
|
+
| **Cross-surface inconsistency** | `fast-fix/fast-fix` in sidebar AND dashboard (each site joined `team/workflow` itself) | per-file fixtures used differing team names, so neither site looked wrong |
|
|
832
|
+
| **Wrong state glyph** | dock spun `⠹` on a finished run (`0 running · 3/3 done`) | the header always received a spinner frame; no test crossed "0 running" with the glyph |
|
|
833
|
+
| **Truncation eats the important token** | dashboard run list painted `› ✓ 122ab0dd complete…` — the status cut in half | fixture goals fit the width, so the narrow-width fallback never ran |
|
|
834
|
+
| **Wire format leaking into TUI** | sidebar `input=2780, output=3715, cacheRead=57216, cost=0.000000, turns=0` | the shared `formatUsage` helper IS correct for CLI output; only the TUI usage is wrong |
|
|
835
|
+
| **Unit-less / wrong-scale numbers** | `314.7s` instead of `5m44s` in the agents pane | tests asserted the numbers, never the format |
|
|
836
|
+
| **Pluralisation** | `1 runs`, `3 agents`, `1 tools` | fixtures used plural counts |
|
|
837
|
+
| **Optional segment unguarded** | live `undefined — ↓·enter` after a run finished — the pure builder guarded it, the **component path did not** | the builder test passed; the component path had no zero-runs test |
|
|
838
|
+
|
|
839
|
+
**When required**: ANY change under `src/ui/**`, or any change that alters what a surface prints (a formatter, a status-slot map, a label helper). Also required after parallel/subagent UI work — delegated claims are hypotheses until rendered.
|
|
840
|
+
|
|
841
|
+
**How**:
|
|
842
|
+
|
|
843
|
+
```bash
|
|
844
|
+
# 0. Produce real state — a real run (read-only goal keeps it safe + fast):
|
|
845
|
+
# from the parent Pi session: team action='run' team='fast-fix' goal='<read-only 1-question task>'
|
|
846
|
+
# → note the runId; state lands in <workspace>/.crew/state/runs/<runId>/
|
|
847
|
+
# 1. Write the harness to /tmp (NEVER into the repo) and render every surface.
|
|
848
|
+
# Template (adjust imports to the surfaces you touched):
|
|
849
|
+
cat > /tmp/full-ui.ts <<'EOF'
|
|
850
|
+
import * as fs from "node:fs";
|
|
851
|
+
import { asCrewTheme } from "/ABS/PATH/pi-crew/src/ui/theme-adapter.ts";
|
|
852
|
+
import { formatCompactToolProgress } from "/ABS/PATH/pi-crew/src/ui/tool-progress-formatter.ts";
|
|
853
|
+
import { teamToolRenderer } from "/ABS/PATH/pi-crew/src/ui/tool-renderers/index.ts";
|
|
854
|
+
import { buildWidgetLines } from "/ABS/PATH/pi-crew/src/ui/widget/widget-renderer.ts";
|
|
855
|
+
import { buildTaskListLines } from "/ABS/PATH/pi-crew/src/ui/widget/task-list.ts";
|
|
856
|
+
import { LiveRunSidebar } from "/ABS/PATH/pi-crew/src/ui/live-run-sidebar.ts";
|
|
857
|
+
|
|
858
|
+
const CWD = "/ABS/PATH/WORKSPACE"; // where .crew/state lives
|
|
859
|
+
const RUN = `${CWD}/.crew/state/runs/<runId>`;
|
|
860
|
+
const manifest = JSON.parse(fs.readFileSync(`${RUN}/manifest.json`, "utf8"));
|
|
861
|
+
const agents = JSON.parse(fs.readFileSync(`${RUN}/agents.json`, "utf8"));
|
|
862
|
+
const tasks = JSON.parse(fs.readFileSync(`${RUN}/tasks.json`, "utf8"));
|
|
863
|
+
const theme = asCrewTheme({});
|
|
864
|
+
const R = (c: any, w = 118) => c.render(w).map((l: string) => l.replace(/\s+$/, "")).join("\n");
|
|
865
|
+
const L = (ls: string[]) => ls.map((l) => l.replace(/\s+$/, "")).join("\n");
|
|
866
|
+
const hdr = (t: string) => `\n═══ ${t} ═══`;
|
|
867
|
+
const details = { action: "run", status: manifest.status, runId: manifest.runId, team: manifest.team, agentRecords: agents };
|
|
868
|
+
|
|
869
|
+
console.log(hdr("CALL"), R(teamToolRenderer.renderCall({ action: "run", goal: manifest.goal, team: manifest.team }, theme, { argsComplete: true })));
|
|
870
|
+
// STREAMING must go through the PRODUCER, not a hand-built string:
|
|
871
|
+
const stream = formatCompactToolProgress({ agentId: manifest.runId, status: "running", runId: manifest.runId,
|
|
872
|
+
startedAt: new Date(manifest.createdAt).getTime(), manifest, tasks, agents });
|
|
873
|
+
console.log(hdr("STREAMING"), R(teamToolRenderer.renderResult({ details: { action: "run" }, content: [{ type: "text", text: stream }] }, { isPartial: true }, theme, {})));
|
|
874
|
+
console.log(hdr("COLLAPSED"), R(teamToolRenderer.renderResult({ details }, { action: "run" }, theme, {})));
|
|
875
|
+
console.log(hdr("EXPANDED"), R(teamToolRenderer.renderResult({ details }, { action: "run" }, theme, { expanded: true })));
|
|
876
|
+
console.log(hdr("EXPANDED@80"), R(teamToolRenderer.renderResult({ details }, { action: "run" }, theme, { expanded: true }), 80));
|
|
877
|
+
|
|
878
|
+
// WidgetRun NEEDS `snapshot` (the plan card reads snapshot.tasks) — a bare {run, agents} renders EMPTY and looks like a bug:
|
|
879
|
+
const { createRunSnapshotCache } = await import("/ABS/PATH/pi-crew/src/ui/run-snapshot-cache.ts");
|
|
880
|
+
const snap = createRunSnapshotCache(CWD).refreshIfStale(manifest.runId);
|
|
881
|
+
const done = [{ run: manifest, agents, snapshot: snap }];
|
|
882
|
+
const live = [{ run: { ...manifest, status: "running" }, agents: agents.map((a: any, i: number) => ({ ...a, status: i === 0 ? "running" : a.status })), snapshot: snap }];
|
|
883
|
+
const dead = [{ run: { ...manifest, status: "failed" }, agents, snapshot: snap }];
|
|
884
|
+
for (const [name, runs, w] of [["done", done, 118], ["done@50", done, 50], ["running", live, 118], ["failed", dead, 118]] as const) {
|
|
885
|
+
console.log(hdr(`DOCK ${name}`), L(buildWidgetLines(CWD, 0, 8, runs as never, 0, w, {})));
|
|
886
|
+
}
|
|
887
|
+
console.log(hdr("PLAN CARD"), L(buildTaskListLines(done as never, 118, theme)) || "(empty)");
|
|
888
|
+
|
|
889
|
+
const sidebar = new LiveRunSidebar({ cwd: CWD, runId: manifest.runId, done: () => undefined, theme: {}, config: {} as never });
|
|
890
|
+
console.log(hdr("SIDEBAR"), R(sidebar, 118));
|
|
891
|
+
// Dashboard + browser: see docs/ui-samples/capture.ts sections 13/14 for the exact constructors.
|
|
892
|
+
EOF
|
|
893
|
+
node --experimental-strip-types --no-warnings /tmp/full-ui.ts | tee /tmp/full-ui.txt
|
|
894
|
+
|
|
895
|
+
# 2. Invariant sweep over the rendered text (all of these must be ZERO hits):
|
|
896
|
+
grep -nE "undefined|[╭╮╰╯├┤]|->" /tmp/full-ui.txt # unguarded segment / retired frame / legacy separator
|
|
897
|
+
grep -nE "\b1 (runs|tools|agents|tasks|edits)\b" /tmp/full-ui.txt # pluralisation (note: `11 tools` legitimately contains `1 tools` — anchor the match)
|
|
898
|
+
grep -nE "input=|output=|cacheRead=|cost=[0-9]" /tmp/full-ui.txt # wire format leaked into a TUI surface
|
|
899
|
+
python3 - <<'PY'
|
|
900
|
+
import re
|
|
901
|
+
bad = []
|
|
902
|
+
for ln in open("/tmp/full-ui.txt", encoding="utf-8"):
|
|
903
|
+
if "═══" in ln: continue
|
|
904
|
+
# spinner present while nothing is running?
|
|
905
|
+
if re.search(r"[⠁-⣿]", ln) and re.search(r"0 running", ln): bad.append(("spinner+0-running", ln.rstrip()))
|
|
906
|
+
plain = re.sub(r"\x1b\[[0-9;]*m", "", ln)
|
|
907
|
+
if len(plain.rstrip("\n")) > 120: bad.append(("over-width", plain.rstrip()))
|
|
908
|
+
if "··" in plain and "↓·enter" in plain and plain.rstrip().endswith("…"): bad.append(("hint clipped", plain.rstrip()))
|
|
909
|
+
print("BAD:", bad if bad else "none")
|
|
910
|
+
PY
|
|
911
|
+
|
|
912
|
+
# 3. Narrow-width survival: the actionable hint must be the LAST thing to go.
|
|
913
|
+
# Render the dock at 40/50/60 and assert `↓·enter` (or its tail) is still there.
|
|
914
|
+
|
|
915
|
+
# 4. Cross-surface consistency (same run, every surface):
|
|
916
|
+
# - team label identical everywhere (`fast-fix`, never `fast-fix/fast-fix`)
|
|
917
|
+
# - run id rendered at the same width everywhere (shortId = last 8)
|
|
918
|
+
# - durations in ONE format (`5m44s`, never `344.3s`)
|
|
919
|
+
# - usage in ONE format (`↑2.8k ↓3.7k`, never `input=…`)
|
|
920
|
+
# - status word never truncated (`completed`, never `complete…`)
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
**Mandatory rules for this tier**
|
|
924
|
+
|
|
925
|
+
- **Render REAL state, never hand-built fixtures.** Fixtures are how the seven defects above survived: the author picks values that fit and read well. `team action='run'` costs ~120s and gives you goals, ids, counts and usage that are the wrong length, the wrong shape and the wrong scale — which is the point.
|
|
926
|
+
- **Feed STREAMING through the producer** (`formatCompactToolProgress`), not a string you typed. Half the streaming bugs live in the producer→parser contract (`test/unit/runtime/core/tool-progress-formatter.test.ts`).
|
|
927
|
+
- **Render EVERY state you support**: running / done / failed / focused / idle / narrow. A glyph or fallback that is correct in one state is routinely wrong in another (spinner on a finished run; a hint clipped at 50 columns).
|
|
928
|
+
- **`undefined` sweep must cover the COMPONENT path**, not just the pure builder. `widget-renderer.buildWidgetLines` had `if (!zero) return []` for three weeks while `src/ui/widget/index.ts` composed the same row with an unguarded template literal — the live `undefined — ↓·enter`.
|
|
929
|
+
- **Any word you cannot trace to a data field is a bug.** For each literal in the rendered output ask "which field/derivation produced this?" — `right default` had no answer.
|
|
930
|
+
- Keep the harness in `/tmp`. It is a probe, not a deliverable; if it is worth keeping, it belongs in the repo's catalog script (see below), not in a random test file.
|
|
931
|
+
|
|
932
|
+
**Catalog (if the repo ships one, `docs/ui-samples/`)**
|
|
933
|
+
|
|
934
|
+
- `capture.ts` renders real components into `captures/*.txt`; `render_png.py` turns them into terminal-style PNGs. Re-run BOTH after any UI change, then look at one image — text captures hide font-level breakage.
|
|
935
|
+
- `render_png.py` must **fail loudly when the font has no glyph** for a character (the coverage self-check compares each glyph against the `.notdef` box). DejaVu Sans Mono — the best-covered mono font on a stock Linux box — lacks the **braille spinner range** (U+2800–U+28FF) and `⟳ ⏰ ⎿`, so a naive render ships `□` for every running row. Mappings in use: braille → `◐`, `⟳` → `↻`, `⎿` → `└`, `⏰`/`⏱` → `o`.
|
|
936
|
+
- Set **line-height == font-size**, otherwise box-drawing rails paint as a dashed line.
|
|
937
|
+
- Heavy box glyphs (`┏ ┃ ┗`) render with light strokes in DejaVu/Noto — a font trait, not a capture bug. Document it instead of hunting fonts.
|
|
938
|
+
|
|
939
|
+
**Acceptance**: every surface renders; the invariant sweep returns zero hits; each state (running/done/failed/focused/idle/narrow) renders the right glyph and keeps the actionable hint; no `undefined`, no retired frame glyph, no `->`, no wire format, no invented word; durations/usage/plurals consistent across surfaces; catalog (if present) regenerated and one PNG visually inspected.
|
|
940
|
+
|
|
941
|
+
**Reference implementations**: `docs/ui-samples/capture.ts` (sections 13–18 render dashboard/browser/inline panel/transcript/live-conversation/settings from a REAL temp run written through the state-store APIs), `docs/ui-samples/render_png.py` (glyph coverage self-check + substitutions), `docs/UI-DESIGN-SYSTEM.md` (the grammar each surface must follow), `src/ui/rail.ts` (the single source of glyphs/helpers).
|
|
942
|
+
|
|
943
|
+
---
|
|
944
|
+
|
|
811
945
|
## Anti-patterns (the cost is real, observed in this session)
|
|
812
946
|
|
|
813
947
|
| Anti-pattern | Cost | Where fixed | Reference |
|
|
@@ -844,6 +978,14 @@ node --experimental-strip-types --no-warnings --test --test-force-exit \
|
|
|
844
978
|
| **Test vacuous — assert trên fixture chứ không trên wiring** (P1 remediation, `09dda842`): migration-validator test 2 từng assert key tự chế không có trong registry → luôn pass dù validator chưa được wire vào registerPiTeams. | `09dda842` | Test phải dùng key THẬT từ registry (`PI_CREW_BROKER_DIAG_UI` severity "removed"), và wiring test phải prove call-site (register.ts:68), không chỉ prove pure function. |
|
|
845
979
|
| **Folded YAML scalar (`key: >`) in agent/team/workflow frontmatter** (Batch-9 regression, fixed `aa899a1e`): `utils/frontmatter.ts` is line-based — folded descriptions parsed as literal `">"` for ALL 17 agents while typecheck/lint/test:critical stayed green (skills unaffected: real `yaml` package). Symptom: guidance renders `name (builtin): >`, When-NOT text missing. | `aa899a1e` | Agent/teams/workflows frontmatter values stay single-line; quote values containing `": "` (parser strips symmetric quotes); run the Tier 12b dual-parse probe after EVERY resource `.md` frontmatter edit. Folded scalars remain fine in `skills/*/SKILL.md` only. |
|
|
846
980
|
| **Worker killed mid long-silent-bash** (Batch-1 postmortem): one >5–10 min command (full `npm test` ≈ 10 min) emits no LLM activity → the broker's responsiveness check SIGTERMs the worker mid-run — exit 143 WHILE the command runs, not a 600s response timeout. Work was intact; manual re-run green — the kill WAS the "hang". | n/a (quirk — `CONTEXT.md` Flagged #1; P2 candidate) | Split long suites into <5 min chunks or emit progress between commands. On exit-143-mid-command: re-run manually BEFORE diagnosing a code bug. Postmortem: `postmortem-batch-1-sigterm.md` (workspace root). |
|
|
981
|
+
| **`pkill -f "test:unit"` kills your own shell** (2026-09-16): the pattern matches the `bash -c` cmdline of the tool call that runs it, so the command dies before it starts the replacement suite — and the log file simply never appears. | n/a (self-inflicted) | Kill by pid (`pgrep -f test-runner` → `kill <pid>`), or match a pattern the sink does not contain | Tier 13 "How" step 0 — detached start: `(setsid nohup npm run test:unit > /tmp/suite.log 2>&1 < /dev/null &)` |
|
|
982
|
+
| **`pgrep -f test-runner` read as liveness** (2026-09-16): the same self-match makes it report `RUNNING` forever, so a suite that died 10 minutes ago looks alive and the "result" you read is a truncated log. | n/a (self-inflicted) | Liveness = `ps -eo pid,etime,cmd \| grep -E "test-runner\|node --test"` with the grep itself excluded | Tier 13 harness step 1 — a run is DONE only when the `# tests/# pass/# fail` block exists at EOF |
|
|
983
|
+
| **Reading a truncated suite log as a summary** (2026-09-16): the runner died with `Test runner error: spawnSync … ETIMEDOUT` at subtest 3946/7860 under load; the last numbers looked like a verdict. | n/a (runner) | The verdict is the `# tests/# pass/# fail` block at EOF — absent means the run did not finish; re-run in the foreground with nothing else heavy running | Failure symptoms row "Test runner error: spawnSync … ETIMEDOUT" |
|
|
984
|
+
| **Running the full suite on a tree you are still editing** (2026-09-16): a 12-minute suite started before the last fixes reports failures that no longer exist — and passes fixes that were not in yet. | n/a (process) | Freeze the tree first; if you must edit, the run is VOID — say so instead of quoting its numbers | Done-criteria: "Full `test:unit` fresh-run" |
|
|
985
|
+
| **Grepping the bundle without context or escape-awareness** (2026-09-16): esbuild escapes non-ASCII as `\u250F` (uppercase hex) so glyph greps miss, while `"->"` (the progress **wire format**, `roleSeparator`) and `Crew agents` (error message + task-graph markdown) are permanent false positives. | n/a (permanent) | Grep the codepoint escape (`u250F`, `u258F`) and always print ±90 chars of context before concluding | Tier 3 bundle check + Tier 13 step 2 (escape-aware proof snippet) |
|
|
986
|
+
| **Trusting a delegated UI claim** (2026-09-16): four parallel executors + a verifier reported "dedupe applied" and "hints canonical"; the rendered surfaces still showed `fast-fix/fast-fix` and an invented `· right default`. Summaries describe intent. | n/a (process) | Render the surface with REAL state before believing any UI claim — including your own | Tier 13 (this entire tier exists for this class) |
|
|
987
|
+
| **Declaring a red gate "pre-existing" (or "my regression") from reasoning alone** (2026-09-16): `check:env-vars` was red; the file was untouched, but that is an argument, not evidence. | n/a (process) | Prove it in a clean-HEAD worktree: `git worktree add -q /tmp/pc-head HEAD && (cd /tmp/pc-head && node scripts/check-env-vars.mjs); git worktree remove --force /tmp/pc-head` | Same recipe as the broker-flake proof (Tier 1 / Failure symptoms) |
|
|
988
|
+
| **Shipping a catalog PNG with tofu boxes** (2026-09-16): the catalog rendered the braille spinner + `⟳` + `⎿` as `□` — invisible in the `.txt` captures, obvious the moment the image was opened. | `docs/ui-samples/render_png.py` glyph self-check | Make the renderer FAIL on any character it cannot paint (compare against the `.notdef` box), then map the offender | Tier 13 "Catalog" + `render_png.py` (`_has_glyph`, `BRAILLE_TO`) |
|
|
847
989
|
| **Treating `wait-request-broker.test.ts` load-timeout as a product bug**: the test runner's per-file 180s timeout is below this file's full-suite runtime under parallel load — fails only with the whole suite, passes in isolation. Pre-existing flake, NOT a regression from your change. | n/a (test infra) | Re-run the single file before fixing anything: `node scripts/test-runner.mjs test/unit/runtime/broker/wait-request-broker.test.ts`. Green in isolation = infra flake; move on. |
|
|
848
990
|
|
|
849
991
|
---
|
|
@@ -876,6 +1018,14 @@ When a tier fails, the recovery is usually quick. Match the symptom to the cause
|
|
|
876
1018
|
| Surface run >5 phút bị stale-reconcile giết oan (worker khỏe, pane sống) | F1 (đã fix f12f4f5d + af2f8eb4): recorder chỉ flush ở turn boundary → lastSeen đóng băng giữa turn; reconciler cũ time-based không pid-gate. **Bẫy đa host**: MỘT pi session chạy bundle cũ cũng đủ giết run của session khác (sweep quét mọi runs) — tát cả host phải cùng version | Kiểm tra mọi pi process cùng bundle (`ps` lstart vs dist mtime); `PI_CREW_DEBUG_STALE=1` sidecar /tmp/pi-crew-f1-debug.log ghi mọi verdict STALE để bắt hung thủ; kỳ vọng sidecar rỗng khi mọi host đã fix |
|
|
877
1019
|
| Worker exits 143 WHILE a long bash command is still running (no LLM-activity window before the kill) | Broker responsiveness SIGTERM on silent long commands (`CONTEXT.md` Flagged #1) — distinct from `RESPONSE_TIMEOUT_MS` (600s no-response) | Split the command; emit progress between steps; re-run the suite manually — work is usually intact. See `postmortem-batch-1-sigterm.md` |
|
|
878
1020
|
| Full `test:unit` fails ONLY on `wait-request-broker.test.ts` under parallel load | Per-file 180s runner timeout vs the file's real runtime (passes isolated) | `node scripts/test-runner.mjs test/unit/runtime/broker/wait-request-broker.test.ts` — green in isolation = infra flake, not a regression |
|
|
1021
|
+
| `undefined` (or `undefined — …`) painted in a live surface | an optional segment interpolated into a template literal without a guard. **Check BOTH paths**: the pure builder and the component that composes the same row (2026-09-16: `widget-renderer.buildWidgetLines` guarded it, `src/ui/widget/index.ts` did not). | Guard before composing (`if (!x) return [] / return undefined`), add a regression lock that renders the zero-state through the COMPONENT, and sweep every `${optional}` on the surface. |
|
|
1022
|
+
| Spinner keeps spinning after the run finished (`⠹ … 0 running`) | the surface hardcodes a spinner frame instead of deriving the glyph from state | Derive it: spinner only while an agent/run is actually running, otherwise the outcome glyph (`✓`/`✗`). Assert in the battery that "0 running" never coexists with a braille glyph. |
|
|
1023
|
+
| Actionable hint clipped at a narrow width (`···· ↓…`) | the leader/hint is composed at a pinned budget and the WHOLE line is truncated afterwards, so the tail dies first | Compose the tail with the shared leader helper at `budget = min(pinned, width - 2)` so the LEFT segment is trimmed (with `…`) and the hint survives. Probe at 40/50/60 columns. |
|
|
1024
|
+
| A status/word truncated mid-token (`complete…`, `fast-f…`) | a narrow-width fallback truncating `head · meta` as ONE string | Give the fallback an explicit priority order (status > id > goal > meta) and clip the lowest-priority segment; never `truncate()` a concatenation that mixes a must-survive token with a droppable one. |
|
|
1025
|
+
| A surface shows a word nobody can trace to data (`· right default`) | a hardcoded literal left in a template | Delete it or replace it with the real field (`run.workspaceMode`). Add "every literal traces to a field" to the Tier 13 sweep. |
|
|
1026
|
+
| `input=2780, output=3715, cost=0.000000` inside a TUI panel | a `key=value` formatter (CLI/status output) reused where the compact TUI form belongs | Keep the wire formatter for CLI/log output; add a compact form for the rail (`↑2.8k ↓3.7k`, cost only when > 0). Same for durations: `formatDuration` (`5m44s`), never `(ms/1000).toFixed(1)}s`. |
|
|
1027
|
+
| Catalog PNG shows `□` / dashed rails | the render font lacks the glyph (braille spinner, `⟳ ⏰ ⎿`) or `line-height > font-size` | Re-run with the coverage self-check in `render_png.py`; keep the substitution map up to date; set `LH = FS`. |
|
|
1028
|
+
| `Test runner error: spawnSync … ETIMEDOUT` mid-suite | the runner's own spawn deadline hit under load (a real test spawns node children) | Re-run in the foreground with nothing else heavy running; a truncated log is not a verdict. If it repeats on one file, run that file alone. |
|
|
879
1029
|
| Guidance / `team action='list'` shows an agent description as `>` or missing When-NOT text | Folded-scalar frontmatter (`description: >`) — the line-based parser reads `>` literally (CONTEXT.md Flagged #4) | Restore the single-line value (double-quote it if it contains `": "`); re-run the Tier 12b dual-parse probe |
|
|
880
1030
|
|
|
881
1031
|
## Performance budget (per-tier soft limits)
|
|
@@ -894,6 +1044,7 @@ When a tier fails, the recovery is usually quick. Match the symptom to the cause
|
|
|
894
1044
|
| 10a (surface E2E suite) | 90s | 180s | tmux server issue or a real spawn/degrade regression — investigate, don't bump |
|
|
895
1045
|
| 10b (live surface run) | ~120s (one fast-fix run) | 600s (worker hard limit) | Pane never engaged (check `visibleAgents`) or auto-exit failed leaving panes open |
|
|
896
1046
|
| 10c (herdr path) | ~120s | 600s | herdr socket protocol drift — check `herdr api schema --json` against `src/runtime/surface/herdr-provider.ts` |
|
|
1047
|
+
| 13 (real-run UI render: 1 real fast-fix run + harness + sweep) | 150s | 300s | A surface is reading disk on the paint path, or the run itself hung — the render is <10ms, so a slow Tier 13 means the harness is doing I/O it shouldn't |
|
|
897
1048
|
|
|
898
1049
|
If a tier runs over the hard limit, **stop and investigate** — don't bump the budget silently. The budget exists precisely so regressions in test runtime (which usually means a regression in test setup/teardown) are caught early.
|
|
899
1050
|
|
|
@@ -995,6 +1146,8 @@ Use this to answer "đủ full tính năng chưa?" without re-deriving. Every us
|
|
|
995
1146
|
| Surface panes tmux/herdr (A1) | `src/runtime/surface/` | T10 (10a E2E + 10b live + 10c herdr) |
|
|
996
1147
|
| Broker (mailbox, steer, tokens) | `src/runtime/broker/` | T1/T2 + 9c steer/respond + T10a test #2 |
|
|
997
1148
|
| Dashboard + keybindings + overlays | `src/ui/`, commands `src/extension/registration/commands/` | T5/T6 probe + parity golden test |
|
|
1149
|
+
| **UI surfaces render from real run state** (tool card, dock widget, plan card, sidebar, dashboard, browser, overlays) | `src/ui/**`, grammar in `docs/UI-DESIGN-SYSTEM.md`, primitives in `src/ui/rail.ts` | T13 (real-run render battery: state glyphs, invented strings, pluralisation, truncation priority, usage/duration formats, narrow-width hint survival) |
|
|
1150
|
+
| UI catalog artifacts (`captures/*.txt` + `png/*.png`, regenerated from real components) | `docs/ui-samples/capture.ts`, `docs/ui-samples/render_png.py` | T13 catalog step (regenerate both + open one PNG; renderer fails on a missing glyph) |
|
|
998
1151
|
| Slash commands (8: run/status/doctor/help/dashboard/settings/init/config) | `commands/{run,status,manage,dashboard}.ts` | T5 send-keys một lệnh `/team-*` |
|
|
999
1152
|
| team-settings / config | `src/extension/team-tool/handle-settings.ts` | 9a settings get + 10b set visibleAgents |
|
|
1000
1153
|
| Worktree isolation | `src/worktree/` | 9a worktrees + 9b run `workspaceMode='worktree'` |
|
|
@@ -1111,6 +1264,22 @@ node -e 'const yaml=require("yaml"),fs=require("fs");let ok=0;for(const f of fs.
|
|
|
1111
1264
|
node --experimental-strip-types --no-warnings --test --test-force-exit test/unit/bundle-skill-resolution.test.ts test/unit/extension/registration/tool-loop-guard.test.ts test/unit/runtime/core/skill-instructions.test.ts # 12d
|
|
1112
1265
|
node scripts/check-bundle-staleness.mjs # staleness + ARCH-7 path-leak scan (also after every build:bundle)
|
|
1113
1266
|
node scripts/release-smoke.mjs # release cut: peer install + tarball import + shape check (ARCH-6)
|
|
1267
|
+
# Tier 13 (real-run UI render battery — any src/ui/** change, or after ANY delegated UI work)
|
|
1268
|
+
# from the parent Pi session: team action='run' team='fast-fix' goal='<read-only 1-question task>' # ~120s, gives REAL state
|
|
1269
|
+
# then render every surface from that run's on-disk state (harness template in Tier 13; keep it in /tmp):
|
|
1270
|
+
node --experimental-strip-types --no-warnings /tmp/full-ui.ts | tee /tmp/full-ui.txt
|
|
1271
|
+
grep -nE "undefined|[╭╮╰╯├┤]|->" /tmp/full-ui.txt # 0 hits (unguarded segment / retired frame / legacy separator)
|
|
1272
|
+
grep -nE "(^|[^0-9])1 (runs|tools|agents|tasks)\b" /tmp/full-ui.txt # 0 hits (anchor the plural check — `11 tools` contains `1 tools`)
|
|
1273
|
+
grep -nE "input=|output=|cacheRead=|cost=[0-9]" /tmp/full-ui.txt # 0 hits (wire format leaked into a TUI panel)
|
|
1274
|
+
# state glyph: a braille spinner must never coexist with `0 running`; hint (`↓·enter`) must survive at 40/50/60 cols
|
|
1275
|
+
# catalog: regenerate BOTH artifacts, then LOOK at one image
|
|
1276
|
+
node --experimental-strip-types --no-warnings docs/ui-samples/capture.ts # captures/*.txt
|
|
1277
|
+
python3 docs/ui-samples/render_png.py # png/*.png (fails loudly on a glyph the font lacks)
|
|
1278
|
+
# bundle-side proof (escape-aware — esbuild writes \u250F uppercase):
|
|
1279
|
+
python3 - <<'PY'
|
|
1280
|
+
s=open("dist/index.mjs",encoding="utf-8").read()
|
|
1281
|
+
for k,v in {"rail open u250F":"u250F" in s,"section u2523":"u2523" in s,"retired u256D":"u256D" in s,"raw undefined-hint":"undefined \u2014 " in s}.items(): print(f"{k}: {v}")
|
|
1282
|
+
PY
|
|
1114
1283
|
# 11a full gate (after ANY delayed-write conversion program): npm run test:unit # ~7500 tests, 15-18 min
|
|
1115
1284
|
```
|
|
1116
1285
|
|
|
@@ -1128,6 +1297,7 @@ Before claiming "tested":
|
|
|
1128
1297
|
- [ ] Tier 7: smoke team run for any `src/runtime/goal-workflow/plan-templates.ts` or `workflows/*.workflow.md` change — completed, no hang, verifier output under 60s
|
|
1129
1298
|
- [ ] Tier 8: final md5 sync check passed
|
|
1130
1299
|
- [ ] Tier 9: feature battery — **required if you touched `src/schema/team-tool-schema.ts`, `src/extension/registration/team-tool.ts`, any `Type.Unsafe({...})` schema, or any armed-role tool list (`agents/*.md` / `src/config/role-tools.ts`)**. 9a read-only batch all return clean; one probe per 9b spawn path (sync / async / chain / `Agent` / `crew_agent`+`get_subagent_result`) completes with `consistency=1`. Run 9c–9f only when the change touches their code path; **at least one full 9c/9e/9f sweep per release is recommended so the battery stays proven** (see `real-test-2026-08-11-scratchpad-I-batch.md`); 9d (destructive) requires explicit user confirmation. **After every run: `git status` to catch unauthorized agent edits.**
|
|
1300
|
+
- [ ] Tier 13: real-run render battery — **required for ANY `src/ui/**` change** (and after any parallel/delegated UI work). One real `team action='run'` producing on-disk state; every surface rendered from that state at 118 + a narrow width; invariant sweep clean (no `undefined` / retired frame glyph / `->` / wire format / invented word / bad plural); the correct glyph per state (running vs done vs failed — never a spinner with `0 running`); the actionable hint survives the narrowest width; durations, usage and run ids formatted identically on every surface; catalog regenerated (capture + PNG) and one image visually inspected. **Fixtures do not count** — the defects this tier exists to catch (invented strings, wrong-state glyphs, truncation that eats a must-survive token) are invisible to author-chosen fixture values.
|
|
1131
1301
|
- [ ] **Output report**: save `docs/real-test/reports/real-test-<YYYY-MM-DD>-<slug>.md` from `skills/real-test-pi-crew/REPORT-TEMPLATE.md`, filled DURING the run with per-tier evidence (counts/md5/runId) — not reconstructed from memory afterward. This is what makes past runs verifiable instead of trust-the-summary.
|
|
1132
1302
|
- [ ] Tier 10: surface battery — **required if you touched `src/runtime/surface/**`, `src/prompt/surface-worker.ts`, the surface branch of `src/runtime/child-pi/child-pi.ts`, or the surface config keys**. 10a E2E 3/3 per backend available (tmux trong tmux; herdr ngoài tmux + socket sống — skip vì thiếu mux là correct-by-design nhưng KHÔNG tính pass cho backend đó); 10b live run với session ĐÃ reload bundle mới (xem Anti-patterns "file-md5 only") + `visibleAgents` set + pane-level evidence (pane id/title during run, `worker.surface_spawned`/`worker.surface_closed` events, pane auto-closed after — KHÔNG dùng `manifest.surface.panes` làm evidence engage, xem Anti-patterns "panes == {}"); 10c herdr live chỉ khi pi chạy trong herdr pane (skip kèm lý do nếu không).
|
|
1133
1303
|
- [ ] Tier 11: remediation regression battery — **required if you touched `src/state/**` write paths, `migration-validator.ts`/its wiring, `scripts/wc-gate.mjs` or `ci` scripts, `.github/workflows/*` env, EFFECTIVE_DEFAULTS maps, or you are cutting a release**. Sub-checks a–j per Tier 11; 11a item 4 (full `test:unit`) mandatory after any delayed-write conversion program, skippable for doc-only changes. Record: buffered-site census count, wc-gate max, staleness `--committed-hash` result.
|
|
@@ -1199,6 +1369,16 @@ Workflow files:
|
|
|
1199
1369
|
- `workflows/plan-execute.workflow.md:30` — verifier prompt
|
|
1200
1370
|
- `workflows/review.workflow.md:31` — verifier prompt
|
|
1201
1371
|
|
|
1372
|
+
UI design system + catalog (Tier 13):
|
|
1373
|
+
- `docs/UI-DESIGN-SYSTEM.md` — the RAIL grammar every surface must follow (glyphs `┏ ┣ ┃ ┗`, canopy `NAME ▸ SUBJECT`, dot leaders, eighth-block gauge, cursor `›`, overflow `▲/▼`, hint format) + the 7 surface classes + the width contract
|
|
1374
|
+
- `src/ui/rail.ts` — SINGLE SOURCE of the glyphs/helpers (`RAIL`, `canopyLine`, `sectionLine`, `railLine`, `railLeaders`, `gaugeBar`, `statusSlot/Badge/Icon`, `overflowHint`, `formatHint`/`keyToken`, `CURSOR`/`ACTIVE`, `dedupeAgentLabel`, `padVisual`/`truncVisual`). Surfaces must import from here, never re-declare `┏`/`┃`/`▕` locally.
|
|
1375
|
+
- `src/ui/adaptive-card.ts` — width-deferred wrapper (`render(width)` is the real width; a frame baked for 116 columns tears at 100)
|
|
1376
|
+
- `src/ui/format-helpers.ts` — `formatCount` (pluralisation: `1 tool`), `formatDuration` (`5m44s`, never `314.7s`), `teamWorkflowLabel` (collapses `team/team`), `truncLine`
|
|
1377
|
+
- `src/ui/widget/widget-renderer.ts` — dock row (`buildWidgetLines`, `idleWidgetLine`, `widgetActivityGlyph`, `widgetRailSlot`, `dockTail` hint budget) — the zero-runs branch is the live `undefined — ↓·enter` regression site
|
|
1378
|
+
- `src/ui/live-run-sidebar.ts` / `src/ui/run-dashboard.ts` / `src/ui/agents-jobs-browser.ts` / `src/ui/settings-overlay.ts` / `src/ui/dashboard-panes/*` — surfaces migrated to RAIL 2026-09-16
|
|
1379
|
+
- `docs/ui-samples/capture.ts` (real renders incl. sections 13–18 from a run written through the state-store APIs) + `docs/ui-samples/render_png.py` (glyph coverage self-check + substitution map) + `docs/ui-samples/README.md`
|
|
1380
|
+
- Tests: `test/unit/ui/rail.test.ts`, `dock-rail`, `dashboard-rail`, `panes-rail`, `overlays-rail` (grammar locks), `test/unit/ui/tool-renderers-redesign.test.ts` (card), `test/unit/ui/tool-renderers-frame-width.test.ts` (width invariant)
|
|
1381
|
+
|
|
1202
1382
|
Resource-contract files (Tier 12):
|
|
1203
1383
|
- `src/utils/frontmatter.ts` — LINE-BASED parser (`parseLines`): single-line values, symmetric-quote strip (`aa899a1e`); folded scalars unsupported for agents/teams/workflows (skills use the real `yaml` package — folded OK there)
|
|
1204
1384
|
- `src/agents/discover-agents.ts:388-391, 476` — flat routing keys (`useWhen`/`avoidWhen`/`cost`/`category` as top-level CSV); discovery cache TTL ~30s (`invalidateAgentDiscoveryCache()`)
|
|
@@ -499,11 +499,9 @@ function parseUiConfig(value: unknown): CrewUiConfig | undefined {
|
|
|
499
499
|
obj.widgetPlacement,
|
|
500
500
|
);
|
|
501
501
|
const rawDashboardPlacement = parseWithSchema(Type.Union([Type.Literal("center"), Type.Literal("right")]), obj.dashboardPlacement);
|
|
502
|
-
const rawRowStyle = parseWithSchema(Type.Union([Type.Literal("compact"), Type.Literal("detailed")]), obj.widgetRowStyle);
|
|
503
502
|
const ui: CrewUiConfig = {
|
|
504
503
|
widgetPlacement: rawWidgetPlacement,
|
|
505
504
|
widgetMaxLines: parsePositiveInteger(obj.widgetMaxLines, 50),
|
|
506
|
-
widgetRowStyle: rawRowStyle,
|
|
507
505
|
inlinePanel: parseWithSchema(Type.Boolean(), obj.inlinePanel),
|
|
508
506
|
powerbar: parseWithSchema(Type.Boolean(), obj.powerbar),
|
|
509
507
|
dashboardPlacement: rawDashboardPlacement,
|
package/src/config/defaults.ts
CHANGED
|
@@ -136,7 +136,6 @@ export const DEFAULT_UI = {
|
|
|
136
136
|
// quota/meter footer (falls back to belowEditor when no footer sink exists).
|
|
137
137
|
widgetPlacement: "bottom" as const,
|
|
138
138
|
widgetMaxLines: 8,
|
|
139
|
-
widgetRowStyle: "compact" as const,
|
|
140
139
|
inlinePanel: true,
|
|
141
140
|
powerbar: true,
|
|
142
141
|
dashboardPlacement: "center" as const,
|
package/src/config/types.ts
CHANGED
|
@@ -167,11 +167,6 @@ export interface CrewUiConfig {
|
|
|
167
167
|
*/
|
|
168
168
|
widgetPlacement?: "aboveEditor" | "belowEditor" | "bottom";
|
|
169
169
|
widgetMaxLines?: number;
|
|
170
|
-
/**
|
|
171
|
-
* Per-agent row layout in the widget. `compact` is one width-budgeted line
|
|
172
|
-
* per agent; `detailed` keeps the two-line tree (name row + activity row).
|
|
173
|
-
*/
|
|
174
|
-
widgetRowStyle?: "compact" | "detailed";
|
|
175
170
|
/**
|
|
176
171
|
* Keyboard-navigable agent rows under the prompt (`↓` from an empty prompt).
|
|
177
172
|
* Requires owning pi's editor component, so it yields to any other extension
|
|
@@ -34,6 +34,8 @@ export const CREW_PHRASES: ReadonlyArray<{ phrase: string; command: string }> =
|
|
|
34
34
|
{ phrase: "crew commands", command: "/team-help" },
|
|
35
35
|
{ phrase: "crew doctor", command: "/team-doctor" },
|
|
36
36
|
{ phrase: "crew diagnose", command: "/team-doctor" },
|
|
37
|
+
{ phrase: "crew schedule", command: "/schedules" },
|
|
38
|
+
{ phrase: "scheduled jobs", command: "/schedules" },
|
|
37
39
|
{ phrase: "teams", command: "/teams" },
|
|
38
40
|
];
|
|
39
41
|
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { getCrewEnv } from "../../config/env-vars.ts";
|
|
4
|
-
import { hasCrewFontFile, isWebTerminal } from "./font-detect.ts";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
|
-
* Self-contained config for the crew-vibes module (
|
|
6
|
+
* Self-contained config for the crew-vibes module (provider quota).
|
|
8
7
|
* Stored in its own JSON file so it never touches the strict typebox schema
|
|
9
8
|
* used by the rest of pi-crew.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
|
-
export const SPEED_STATUS_ID = "pi-crew-speed";
|
|
13
11
|
export const CAPACITY_STATUS_ID = "pi-crew-bar";
|
|
14
12
|
export const PROVIDER_STATUS_ID = "pi-crew-bar";
|
|
15
13
|
|
|
@@ -23,21 +21,6 @@ export function configPath(): string {
|
|
|
23
21
|
|
|
24
22
|
export type TokenDisplay = "off" | "tokens" | "percentage";
|
|
25
23
|
|
|
26
|
-
export interface SpeedConfig {
|
|
27
|
-
enabled: boolean;
|
|
28
|
-
footer: boolean;
|
|
29
|
-
indicator: boolean;
|
|
30
|
-
label: string;
|
|
31
|
-
renderIntervalMs: number;
|
|
32
|
-
slidingWindowMs: number;
|
|
33
|
-
minReliableDurationMs: number;
|
|
34
|
-
maxDisplayTokS: number;
|
|
35
|
-
defaultIntervalMs: number;
|
|
36
|
-
minIntervalMs: number;
|
|
37
|
-
maxIntervalMs: number;
|
|
38
|
-
scale: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
24
|
export interface CapacityConfig {
|
|
42
25
|
enabled: boolean;
|
|
43
26
|
tokenDisplay: TokenDisplay;
|
|
@@ -51,26 +34,11 @@ export interface CapacityConfig {
|
|
|
51
34
|
|
|
52
35
|
export interface CrewVibesConfig {
|
|
53
36
|
enabled: boolean;
|
|
54
|
-
speed: SpeedConfig;
|
|
55
37
|
capacity: CapacityConfig;
|
|
56
38
|
}
|
|
57
39
|
|
|
58
40
|
export const DEFAULT_CONFIG: CrewVibesConfig = {
|
|
59
41
|
enabled: true,
|
|
60
|
-
speed: {
|
|
61
|
-
enabled: true,
|
|
62
|
-
footer: false,
|
|
63
|
-
indicator: true,
|
|
64
|
-
label: "tok/s",
|
|
65
|
-
renderIntervalMs: 250,
|
|
66
|
-
slidingWindowMs: 1000,
|
|
67
|
-
minReliableDurationMs: 1000,
|
|
68
|
-
maxDisplayTokS: 500,
|
|
69
|
-
defaultIntervalMs: 167,
|
|
70
|
-
minIntervalMs: 50,
|
|
71
|
-
maxIntervalMs: 250,
|
|
72
|
-
scale: 6000,
|
|
73
|
-
},
|
|
74
42
|
capacity: {
|
|
75
43
|
enabled: true,
|
|
76
44
|
tokenDisplay: "tokens",
|
|
@@ -83,26 +51,6 @@ export const DEFAULT_CONFIG: CrewVibesConfig = {
|
|
|
83
51
|
},
|
|
84
52
|
};
|
|
85
53
|
|
|
86
|
-
// Fallback capacity icons using standard Unicode characters that render
|
|
87
|
-
// on any terminal without the crew-vibes PUA font.
|
|
88
|
-
const FALLBACK_CAPACITY_ICONS: [string, string, string, string, string, string] = [
|
|
89
|
-
"\u25CB ", // ○ empty circle (lean)
|
|
90
|
-
"\u25D4 ", // ◔ circle with dot (chonking)
|
|
91
|
-
"\u25D1 ", // ◑ circle half filled (chonky)
|
|
92
|
-
"\u25CF ", // ● filled circle (big chonk)
|
|
93
|
-
"\u2B24 ", // ⬤ large filled circle (mega chonk)
|
|
94
|
-
"\u2B22 ", // ⬢ filled hexagon (oh lawd)
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
/** Return capacity icons: standard Unicode glyphs that render on any terminal.
|
|
98
|
-
* PUA glyphs (U+E710..U+E715) require crew-vibes.ttf AND terminal PUA
|
|
99
|
-
* support — many terminals cannot render them even with the font installed. */
|
|
100
|
-
export function capacityIcons(): [string, string, string, string, string, string] {
|
|
101
|
-
// Web terminals cannot render PUA glyphs — use fallback.
|
|
102
|
-
if (isWebTerminal()) return FALLBACK_CAPACITY_ICONS;
|
|
103
|
-
return hasCrewFontFile() ? DEFAULT_CONFIG.capacity.icons : FALLBACK_CAPACITY_ICONS;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
54
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
107
55
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
108
56
|
}
|
|
@@ -133,29 +81,6 @@ function tokenDisplayFrom(raw: unknown, fallback: TokenDisplay): TokenDisplay {
|
|
|
133
81
|
return raw === "off" || raw === "tokens" || raw === "percentage" ? raw : fallback;
|
|
134
82
|
}
|
|
135
83
|
|
|
136
|
-
function normalizeSpeed(raw: unknown): SpeedConfig {
|
|
137
|
-
const input = asRecord(raw);
|
|
138
|
-
const speed: SpeedConfig = {
|
|
139
|
-
enabled: boolFrom(input.enabled, DEFAULT_CONFIG.speed.enabled),
|
|
140
|
-
footer: boolFrom(input.footer, DEFAULT_CONFIG.speed.footer),
|
|
141
|
-
indicator: boolFrom(input.indicator, DEFAULT_CONFIG.speed.indicator),
|
|
142
|
-
label: stringFrom(input.label, DEFAULT_CONFIG.speed.label),
|
|
143
|
-
renderIntervalMs: positiveFrom(input.renderIntervalMs, DEFAULT_CONFIG.speed.renderIntervalMs),
|
|
144
|
-
slidingWindowMs: positiveFrom(input.slidingWindowMs, DEFAULT_CONFIG.speed.slidingWindowMs),
|
|
145
|
-
minReliableDurationMs: positiveFrom(input.minReliableDurationMs, DEFAULT_CONFIG.speed.minReliableDurationMs),
|
|
146
|
-
maxDisplayTokS: positiveFrom(input.maxDisplayTokS, DEFAULT_CONFIG.speed.maxDisplayTokS),
|
|
147
|
-
defaultIntervalMs: positiveFrom(input.defaultIntervalMs, DEFAULT_CONFIG.speed.defaultIntervalMs),
|
|
148
|
-
minIntervalMs: positiveFrom(input.minIntervalMs, DEFAULT_CONFIG.speed.minIntervalMs),
|
|
149
|
-
maxIntervalMs: positiveFrom(input.maxIntervalMs, DEFAULT_CONFIG.speed.maxIntervalMs),
|
|
150
|
-
scale: positiveFrom(input.scale, DEFAULT_CONFIG.speed.scale),
|
|
151
|
-
};
|
|
152
|
-
if (speed.minIntervalMs > speed.maxIntervalMs) {
|
|
153
|
-
speed.minIntervalMs = DEFAULT_CONFIG.speed.minIntervalMs;
|
|
154
|
-
speed.maxIntervalMs = DEFAULT_CONFIG.speed.maxIntervalMs;
|
|
155
|
-
}
|
|
156
|
-
return speed;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
84
|
function normalizeCapacity(raw: unknown): CapacityConfig {
|
|
160
85
|
const input = asRecord(raw);
|
|
161
86
|
return {
|
|
@@ -174,7 +99,6 @@ export function normalizeConfig(raw: unknown): CrewVibesConfig {
|
|
|
174
99
|
const input = asRecord(raw);
|
|
175
100
|
return {
|
|
176
101
|
enabled: boolFrom(input.enabled, DEFAULT_CONFIG.enabled),
|
|
177
|
-
speed: normalizeSpeed(input.speed),
|
|
178
102
|
capacity: normalizeCapacity(input.capacity),
|
|
179
103
|
};
|
|
180
104
|
}
|