@andresmassello/uscha 1.85.0 → 1.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -4
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +282 -3
- package/uscha-kit/.claude/skills/uscha-devloop/uscha_top.py +403 -0
- package/uscha-kit/.claude-plugin/plugin.json +2 -2
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +1 -1
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/install-uscha.py +58 -0
- package/uscha-kit/reports/junit/.top-cases.json +1 -0
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +282 -3
- package/uscha-kit/skills/uscha-devloop/uscha_top.py +403 -0
- package/uscha-kit/uscha.config.json +1 -1
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
uscha_top.py — the terminal projection of the QA ledger (`uscha top`, ADR-031/034).
|
|
4
|
+
|
|
5
|
+
It DERIVES NOTHING. Every state, cardinality, median and percentage on screen is read from
|
|
6
|
+
one read-only engine call, `qa_ledger.py top --json` (ADR-032), and `render()` is a pure
|
|
7
|
+
function of that object: same JSON + same size -> byte-identical lines, no clock, no files,
|
|
8
|
+
no subprocess, no environment. That purity is what lets the golden frames under
|
|
9
|
+
tests/fixtures/uscha-top/golden/ be the oracle: a renderer that quietly shows 100% while a
|
|
10
|
+
criterion is unmeasured fails a snapshot, not a code review (ADR-034).
|
|
11
|
+
|
|
12
|
+
Truth-pass (INV-TOP-05): a field the engine emits as null renders as an em dash, never as a
|
|
13
|
+
zero and never as a guess. In v0.1 that is ETA, every AGE, drift, and the trace column --
|
|
14
|
+
each with its deferred wiring recorded in ADR-035.
|
|
15
|
+
|
|
16
|
+
M1 scope: the read-only BOARD. The live feed (M2) and VERDICTS mode (M3) are not wired; the
|
|
17
|
+
panes that will hold them are labelled as such rather than faked.
|
|
18
|
+
|
|
19
|
+
Stdlib only. Python 3.8+. Runnable directly or via `python -m uscha_top`.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
DEFAULT_LEDGER = "QA-LEDGER.json"
|
|
30
|
+
DEFAULT_SIZE = (100, 32)
|
|
31
|
+
FALLBACK_SIZE = (100, 32)
|
|
32
|
+
|
|
33
|
+
# Lines the board always spends on chrome: the title, 3 rules, 4 KPI lines, the table
|
|
34
|
+
# header, the feed label and the key hint. Everything else is table rows + feed.
|
|
35
|
+
CHROME_LINES = 11
|
|
36
|
+
FEED_MAX = 3
|
|
37
|
+
BURNUP_MAX = 24
|
|
38
|
+
|
|
39
|
+
# ANSI SGR by obligation state. TRACED and TAGGED deliberately share the UNMEASURED gray:
|
|
40
|
+
# the v0.1 engine has no source for either rung (ADR-032), so they must read as "not
|
|
41
|
+
# measured", never as PASS (INV-TOP-02, AC-T-08).
|
|
42
|
+
PALETTE = {
|
|
43
|
+
"MEASURED_PASS": "32",
|
|
44
|
+
"MEASURED_FAIL": "31",
|
|
45
|
+
"QUARANTINE": "33",
|
|
46
|
+
"UNMEASURED": "90",
|
|
47
|
+
"TRACED": "90",
|
|
48
|
+
"TAGGED": "90",
|
|
49
|
+
}
|
|
50
|
+
RESET = "\x1b[0m"
|
|
51
|
+
DASH = "—" # the honest "no source" marker (INV-TOP-05)
|
|
52
|
+
MID = "·"
|
|
53
|
+
RULE = "─"
|
|
54
|
+
BLOCKS = "▁▂▃▄▅▆▇█"
|
|
55
|
+
|
|
56
|
+
# What the reader is expected to DO about a row. Presentation, not a KPI: no number here.
|
|
57
|
+
ACTIONS = {
|
|
58
|
+
"MEASURED_PASS": DASH,
|
|
59
|
+
"MEASURED_FAIL": "machine: fix, then rerun",
|
|
60
|
+
"QUARANTINE": "you: record a verdict",
|
|
61
|
+
"UNMEASURED": "you: tag a test",
|
|
62
|
+
"TRACED": "you: tag a test",
|
|
63
|
+
"TAGGED": "machine: run the case",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# --------------------------------------------------------------------------- #
|
|
68
|
+
# pure rendering #
|
|
69
|
+
# --------------------------------------------------------------------------- #
|
|
70
|
+
def _fit(text, cols):
|
|
71
|
+
"""One line, never wider than the terminal. Wrapping would break the frame's row
|
|
72
|
+
accounting, so an over-long line is cut and marked."""
|
|
73
|
+
if cols <= 0:
|
|
74
|
+
return ""
|
|
75
|
+
if len(text) <= cols:
|
|
76
|
+
return text
|
|
77
|
+
return text[:cols - 1] + "…" if cols > 1 else text[:cols]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _spread(left, right, cols):
|
|
81
|
+
"""left ... right on one line, right-aligned, degrading to just `left` when tight."""
|
|
82
|
+
if len(left) + len(right) + 1 > cols:
|
|
83
|
+
return _fit(left, cols)
|
|
84
|
+
return left + " " * (cols - len(left) - len(right)) + right
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _num(value):
|
|
88
|
+
"""A number the engine emitted, or the em dash when it emitted null."""
|
|
89
|
+
return DASH if value is None else str(value)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _pct_line(terminado):
|
|
93
|
+
"""INV-TOP-01: the DONE bar carries an explicit `N unmeasured` suffix whenever anything
|
|
94
|
+
is unmeasured, and the engine has already capped the percentage below 100 while any
|
|
95
|
+
obligation sits outside MEASURED_PASS -- the renderer republishes that fact, it never
|
|
96
|
+
recomputes it (AC-T-01, AC-T-04, AC-T-23)."""
|
|
97
|
+
done = terminado.get("done")
|
|
98
|
+
total = terminado.get("total")
|
|
99
|
+
pct = terminado.get("pct")
|
|
100
|
+
unm = terminado.get("unmeasured") or 0
|
|
101
|
+
line = "DONE %s/%s (%s%%)" % (_num(done), _num(total), _num(pct))
|
|
102
|
+
if unm:
|
|
103
|
+
line += " %s %d unmeasured" % (MID, unm)
|
|
104
|
+
return line
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _burnup_line(burnup, cols):
|
|
108
|
+
"""The score trend, labelled as a score trend. v0.1 has no obligation-count history
|
|
109
|
+
(ADR-035/2), so calling this a burn-up of closed obligations would be a lie the label
|
|
110
|
+
exists to prevent."""
|
|
111
|
+
points = [p for p in (burnup or {}).get("weeks") or [] if isinstance(p, (int, float))]
|
|
112
|
+
label = "score trend "
|
|
113
|
+
note = " (readiness score, not closed obligations)"
|
|
114
|
+
if not points:
|
|
115
|
+
return label + DASH + " (no `readiness --record` history yet)"
|
|
116
|
+
room = max(4, min(BURNUP_MAX, cols - len(label) - len(note)))
|
|
117
|
+
bars = "".join(BLOCKS[min(len(BLOCKS) - 1, max(0, int(p) * len(BLOCKS) // 101))]
|
|
118
|
+
for p in points[-room:])
|
|
119
|
+
return label + bars + note
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _spec_pin_text(spec_pin):
|
|
123
|
+
"""git HEAD, labelled for what it is. There is no pinned-spec concept in the engine yet
|
|
124
|
+
(ADR-035/4): an unverified sha must SAY it is unverified, and a non-git tree shows the
|
|
125
|
+
em dash rather than a fabricated pin (AC-T-06, INV-TOP-05)."""
|
|
126
|
+
if not spec_pin or not spec_pin.get("sha"):
|
|
127
|
+
return "spec_pin " + DASH
|
|
128
|
+
mark = ("clean-room verified" if spec_pin.get("clean_room_verified")
|
|
129
|
+
else "not clean-room verified")
|
|
130
|
+
return "spec_pin %s (%s)" % (spec_pin["sha"], mark)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _cases_text(ob):
|
|
134
|
+
total = ob.get("cases_total") or 0
|
|
135
|
+
if not total:
|
|
136
|
+
return DASH # no ingested case names this criterion: no count to show
|
|
137
|
+
return "%s/%s" % (_num(ob.get("cases_pass")), total)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _row(ob, selected):
|
|
141
|
+
gutter = "> " if selected else " "
|
|
142
|
+
return "%s%-8s%-9s%-15s%7s%5s %s" % (
|
|
143
|
+
gutter, str(ob.get("id") or "?")[:8], str(ob.get("gate") or DASH)[:8],
|
|
144
|
+
str(ob.get("state") or "?")[:14], _cases_text(ob),
|
|
145
|
+
_num(ob.get("age_hours")), ACTIONS.get(ob.get("state"), DASH))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _colorize(line, state):
|
|
149
|
+
code = PALETTE.get(state)
|
|
150
|
+
if not code or state not in line:
|
|
151
|
+
return line
|
|
152
|
+
return line.replace(state, "\x1b[%sm%s%s" % (code, state, RESET), 1)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def render(state, size, sel=0, plain=True):
|
|
156
|
+
"""The whole board as a list of exactly `rows` lines, none wider than `cols`.
|
|
157
|
+
|
|
158
|
+
PURE (ADR-034): no I/O, no clock, no randomness, no environment. `state` is the parsed
|
|
159
|
+
`qa_ledger.py top --json` object; `size` is (cols, rows); `sel` is the highlighted row.
|
|
160
|
+
plain=True emits no escape sequences at all -- the mode `--once`, CI and the golden
|
|
161
|
+
frames use, so a snapshot compares text and not terminal control codes.
|
|
162
|
+
"""
|
|
163
|
+
cols, rows = size
|
|
164
|
+
cols = max(20, int(cols))
|
|
165
|
+
rows = max(CHROME_LINES + 1, int(rows))
|
|
166
|
+
obligations = state.get("obligations") or []
|
|
167
|
+
terminado = state.get("terminado") or {}
|
|
168
|
+
debtors = state.get("debtors") or {}
|
|
169
|
+
honesty = state.get("honesty") or {}
|
|
170
|
+
|
|
171
|
+
out = []
|
|
172
|
+
out.append(_spread("uscha top %s %s" % (MID, state.get("project") or "(unnamed project)"),
|
|
173
|
+
"step #%s" % _num(state.get("step")), cols))
|
|
174
|
+
out.append(RULE * cols)
|
|
175
|
+
out.append(_pct_line(terminado))
|
|
176
|
+
out.append("machine owes %s %s you owe %s %s untagged %s %s ETA %s"
|
|
177
|
+
% (_num(debtors.get("machine")), MID, _num(debtors.get("you")), MID,
|
|
178
|
+
_num(debtors.get("untagged")), MID, _num(state.get("eta_min"))))
|
|
179
|
+
# honesty travels BESIDE done on purpose (INV-TOP-04): a thin denominator has to be
|
|
180
|
+
# visible at the same glance as the number it flatters.
|
|
181
|
+
out.append("honesty %s/%s (%s%%) measured %s %s"
|
|
182
|
+
% (_num(honesty.get("measured")), _num(honesty.get("total")),
|
|
183
|
+
_num(honesty.get("pct")), MID, _spec_pin_text(state.get("spec_pin"))))
|
|
184
|
+
out.append(_burnup_line(state.get("burnup"), cols))
|
|
185
|
+
out.append(RULE * cols)
|
|
186
|
+
out.append(" %-8s%-9s%-15s%7s%5s %s"
|
|
187
|
+
% ("ID", "GATE", "STATE", "CASES", "AGE", "ACTION"))
|
|
188
|
+
|
|
189
|
+
# Budget: the table is served FIRST and the feed gets only what is left over, so a short
|
|
190
|
+
# terminal shortens the feed and never the board (AC-T-21).
|
|
191
|
+
avail = rows - CHROME_LINES
|
|
192
|
+
want = len(obligations) or 1
|
|
193
|
+
feed_n = min(FEED_MAX, max(0, avail - want))
|
|
194
|
+
table_n = max(1, min(want, avail - feed_n))
|
|
195
|
+
body = table_n - 1 if len(obligations) > table_n else table_n
|
|
196
|
+
body = max(1, body)
|
|
197
|
+
top = 0
|
|
198
|
+
if sel >= body:
|
|
199
|
+
top = min(sel - body + 1, max(0, len(obligations) - body))
|
|
200
|
+
top = max(0, top)
|
|
201
|
+
|
|
202
|
+
table = []
|
|
203
|
+
for i, ob in enumerate(obligations[top:top + body], start=top):
|
|
204
|
+
line = _fit(_row(ob, i == sel), cols)
|
|
205
|
+
table.append(line if plain else _colorize(line, ob.get("state")))
|
|
206
|
+
hidden = len(obligations) - len(table)
|
|
207
|
+
if hidden > 0:
|
|
208
|
+
table.append(_fit(" %s %d more obligation(s) not shown (j/k to move)"
|
|
209
|
+
% (DASH, hidden), cols))
|
|
210
|
+
if not obligations:
|
|
211
|
+
table.append(_fit(" no tagged criterion in the acceptance file "
|
|
212
|
+
"(nothing to measure yet)", cols))
|
|
213
|
+
out.extend(table[:max(1, table_n)])
|
|
214
|
+
|
|
215
|
+
pad = avail - len(table[:max(1, table_n)]) - feed_n
|
|
216
|
+
out.extend([""] * max(0, pad))
|
|
217
|
+
out.append(RULE * cols)
|
|
218
|
+
events = state.get("events_tail") or []
|
|
219
|
+
out.append("feed %s the live event tail is M2; nothing is invented here" % MID)
|
|
220
|
+
for i in range(feed_n):
|
|
221
|
+
if i < len(events):
|
|
222
|
+
ev = events[i]
|
|
223
|
+
out.append(_fit(" %s %s" % (ev.get("ts") or DASH, ev.get("text") or ""), cols))
|
|
224
|
+
else:
|
|
225
|
+
out.append("")
|
|
226
|
+
out.append("[j/k] move %s [r] reload %s [q] quit %s [v] verdicts (M3) %s "
|
|
227
|
+
"[d]/[o] phase 2" % (MID, MID, MID, MID))
|
|
228
|
+
out = [_fit(line, cols) for line in out]
|
|
229
|
+
# exactly `rows` lines: a frame that drifts in height is a frame no snapshot can pin
|
|
230
|
+
out = out[:rows] + [""] * max(0, rows - len(out))
|
|
231
|
+
return out
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# --------------------------------------------------------------------------- #
|
|
235
|
+
# state loading (the ONE read boundary -- it shells out, it never re-derives) #
|
|
236
|
+
# --------------------------------------------------------------------------- #
|
|
237
|
+
def engine_path():
|
|
238
|
+
"""qa_ledger.py inside this kit, in either skill-tree layout -- the same both-layouts
|
|
239
|
+
resolution `install-uscha.py` uses for the mirador renderer."""
|
|
240
|
+
here = os.path.dirname(os.path.realpath(__file__))
|
|
241
|
+
local = os.path.join(here, "qa_ledger.py")
|
|
242
|
+
if os.path.isfile(local):
|
|
243
|
+
return local
|
|
244
|
+
kit = os.path.realpath(os.path.join(here, "..", "..", ".."))
|
|
245
|
+
for rel in (("skills", "uscha-devloop", "qa_ledger.py"),
|
|
246
|
+
(".claude", "skills", "uscha-devloop", "qa_ledger.py")):
|
|
247
|
+
cand = os.path.join(kit, *rel)
|
|
248
|
+
if os.path.isfile(cand):
|
|
249
|
+
return cand
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def load_state(state_path=None, ledger=DEFAULT_LEDGER, engine=None):
|
|
254
|
+
"""The board's state: either a FROZEN `top --json` object (a file -- what the golden
|
|
255
|
+
frames render from, no engine call) or one read-only engine call. Raises RuntimeError
|
|
256
|
+
with the engine's own message rather than inventing an empty board."""
|
|
257
|
+
if state_path:
|
|
258
|
+
with open(state_path, "r", encoding="utf-8") as fh:
|
|
259
|
+
return json.load(fh)
|
|
260
|
+
eng = engine or engine_path()
|
|
261
|
+
if not eng:
|
|
262
|
+
raise RuntimeError("qa_ledger.py not found next to uscha_top.py")
|
|
263
|
+
proc = subprocess.run([sys.executable, eng, "top", "--json", "--ledger", ledger],
|
|
264
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
265
|
+
if proc.returncode != 0:
|
|
266
|
+
raise RuntimeError((proc.stderr or b"").decode("utf-8", "replace").strip()
|
|
267
|
+
or "qa_ledger.py top failed")
|
|
268
|
+
return json.loads(proc.stdout.decode("utf-8"))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# --------------------------------------------------------------------------- #
|
|
272
|
+
# terminal plumbing (mockable, and NOT what the golden frames test) #
|
|
273
|
+
# --------------------------------------------------------------------------- #
|
|
274
|
+
def enable_vt():
|
|
275
|
+
"""Windows is first-class (ADR-031): modern conhost and Windows Terminal handle VT, and
|
|
276
|
+
legacy conhost needs ENABLE_VIRTUAL_TERMINAL_PROCESSING switched on through ctypes.
|
|
277
|
+
Returns False when it cannot be enabled -- the caller then prints a plain frame instead
|
|
278
|
+
of spraying raw escapes at a terminal that would show them literally (AC-T-22)."""
|
|
279
|
+
if os.name != "nt":
|
|
280
|
+
return True
|
|
281
|
+
try:
|
|
282
|
+
import ctypes
|
|
283
|
+
kernel32 = ctypes.windll.kernel32
|
|
284
|
+
handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE
|
|
285
|
+
mode = ctypes.c_uint32()
|
|
286
|
+
if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
287
|
+
return False
|
|
288
|
+
return bool(kernel32.SetConsoleMode(handle, mode.value | 0x0004))
|
|
289
|
+
except Exception:
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def read_key():
|
|
294
|
+
"""One keypress, per platform, detected at runtime. Isolated in one function so the
|
|
295
|
+
dispatch above it can be driven by a scripted sequence in tests -- the driver itself is
|
|
296
|
+
not what is under test (ADR-034)."""
|
|
297
|
+
if os.name == "nt":
|
|
298
|
+
import msvcrt
|
|
299
|
+
ch = msvcrt.getch()
|
|
300
|
+
if ch in (b"\x00", b"\xe0"): # arrow keys arrive as a 2-byte pair
|
|
301
|
+
return {b"H": "k", b"P": "j"}.get(msvcrt.getch(), "")
|
|
302
|
+
return ch.decode("utf-8", "replace")
|
|
303
|
+
import termios
|
|
304
|
+
import tty
|
|
305
|
+
fd = sys.stdin.fileno()
|
|
306
|
+
saved = termios.tcgetattr(fd)
|
|
307
|
+
try:
|
|
308
|
+
tty.setraw(fd)
|
|
309
|
+
return sys.stdin.read(1)
|
|
310
|
+
finally:
|
|
311
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, saved)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def dispatch(key, sel, count):
|
|
315
|
+
"""Key -> (new selection, quit?, reload?). Pure, so the keymap is testable without a
|
|
316
|
+
terminal: the driver below is not what is under test, this dispatch is (ADR-034)."""
|
|
317
|
+
if key in ("q", "Q", "\x03", "\x1b"):
|
|
318
|
+
return sel, True, False
|
|
319
|
+
if key == "j":
|
|
320
|
+
return min(sel + 1, max(0, count - 1)), False, False
|
|
321
|
+
if key == "k":
|
|
322
|
+
return max(0, sel - 1), False, False
|
|
323
|
+
if key == "r":
|
|
324
|
+
return sel, False, True
|
|
325
|
+
return sel, False, False
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def terminal_size(cols=None, rows=None):
|
|
329
|
+
if cols and rows:
|
|
330
|
+
return int(cols), int(rows)
|
|
331
|
+
size = shutil.get_terminal_size(FALLBACK_SIZE)
|
|
332
|
+
return int(cols or size.columns), int(rows or size.lines)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _print_frame(lines):
|
|
336
|
+
sys.stdout.write("\n".join(lines) + "\n")
|
|
337
|
+
sys.stdout.flush()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _loop(state, args):
|
|
341
|
+
sel = 0
|
|
342
|
+
sys.stdout.write("\x1b[?25l")
|
|
343
|
+
try:
|
|
344
|
+
while True:
|
|
345
|
+
frame = render(state, terminal_size(args.cols, args.rows), sel=sel, plain=False)
|
|
346
|
+
sys.stdout.write("\x1b[H\x1b[2J" + "\n".join(frame))
|
|
347
|
+
sys.stdout.flush()
|
|
348
|
+
sel, quit_now, reload_now = dispatch(
|
|
349
|
+
read_key(), sel, len(state.get("obligations") or []))
|
|
350
|
+
if quit_now:
|
|
351
|
+
return 0
|
|
352
|
+
if reload_now:
|
|
353
|
+
state = load_state(args.state, args.ledger)
|
|
354
|
+
except KeyboardInterrupt:
|
|
355
|
+
return 0
|
|
356
|
+
finally:
|
|
357
|
+
sys.stdout.write("\x1b[?25h\x1b[0m\n")
|
|
358
|
+
sys.stdout.flush()
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def build_parser():
|
|
362
|
+
parser = argparse.ArgumentParser(
|
|
363
|
+
prog="uscha top",
|
|
364
|
+
description="terminal projection of the QA ledger (read-only board, ADR-031/034)")
|
|
365
|
+
parser.add_argument("--ledger", default=DEFAULT_LEDGER,
|
|
366
|
+
help="ledger the engine reads (default: %s)" % DEFAULT_LEDGER)
|
|
367
|
+
parser.add_argument("--state", default=None,
|
|
368
|
+
help="render a FROZEN `top --json` file instead of calling the "
|
|
369
|
+
"engine (the golden-frame path)")
|
|
370
|
+
parser.add_argument("--once", action="store_true",
|
|
371
|
+
help="print one plain frame and exit (implied without a TTY)")
|
|
372
|
+
parser.add_argument("--plain", action="store_true",
|
|
373
|
+
help="never emit escape sequences")
|
|
374
|
+
parser.add_argument("--refresh", type=float, default=2.0,
|
|
375
|
+
help="reserved for the M2 timed poll; M1 re-reads on the `r` key")
|
|
376
|
+
parser.add_argument("--cols", type=int, default=None)
|
|
377
|
+
parser.add_argument("--rows", type=int, default=None)
|
|
378
|
+
return parser
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def main(argv=None):
|
|
382
|
+
for stream in (sys.stdout, sys.stderr):
|
|
383
|
+
try:
|
|
384
|
+
stream.reconfigure(encoding="utf-8")
|
|
385
|
+
except Exception:
|
|
386
|
+
pass
|
|
387
|
+
args = build_parser().parse_args(argv)
|
|
388
|
+
try:
|
|
389
|
+
state = load_state(args.state, args.ledger)
|
|
390
|
+
except (OSError, ValueError, RuntimeError) as exc:
|
|
391
|
+
sys.stderr.write("[uscha top] %s\n" % exc)
|
|
392
|
+
return 1
|
|
393
|
+
size = terminal_size(args.cols, args.rows)
|
|
394
|
+
# no TTY (pipe, CI, redirect) behaves as --once, and legacy conhost that refuses VT
|
|
395
|
+
# degrades the same way rather than printing escapes nobody can read (AC-T-20/22).
|
|
396
|
+
if args.once or args.plain or not sys.stdout.isatty() or not enable_vt():
|
|
397
|
+
_print_frame(render(state, size, sel=0, plain=True))
|
|
398
|
+
return 0
|
|
399
|
+
return _loop(state, args)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
if __name__ == "__main__":
|
|
403
|
+
sys.exit(main())
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "uscha",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.86.0",
|
|
5
5
|
"displayName": "Uscha",
|
|
6
|
-
"description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py,
|
|
6
|
+
"description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py, 52 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Andres Massello",
|
|
9
9
|
"url": "https://github.com/andresmassello"
|
package/uscha-kit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uscha-kit
|
|
2
2
|
|
|
3
|
-
**Kit version:** v1.
|
|
3
|
+
**Kit version:** v1.86.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
|
|
4
4
|
|
|
5
5
|
Spec-driven orchestrator + multi-repo QA for Claude Code, with a deterministic ledger.
|
|
6
6
|
**Nine skills** (`uscha-discovery`, `uscha-adr-refine`, `uscha-devloop`, `uscha-sysdoc`, `uscha-reverse-discovery`,
|
package/uscha-kit/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
uscha-kit 1.
|
|
1
|
+
uscha-kit 1.86.0
|
|
@@ -807,6 +807,58 @@ def cmd_mirador(args):
|
|
|
807
807
|
print("\n[uscha mirador] stopped")
|
|
808
808
|
|
|
809
809
|
|
|
810
|
+
def _uscha_top_path():
|
|
811
|
+
"""The `uscha top` renderer inside this kit (either skill-tree layout)."""
|
|
812
|
+
for rel in (("skills", "uscha-devloop", "uscha_top.py"),
|
|
813
|
+
(".claude", "skills", "uscha-devloop", "uscha_top.py")):
|
|
814
|
+
candidate = KIT_ROOT.joinpath(*rel)
|
|
815
|
+
if candidate.is_file():
|
|
816
|
+
return candidate
|
|
817
|
+
return None
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _qa_ledger_path():
|
|
821
|
+
"""The engine inside this kit (either skill-tree layout)."""
|
|
822
|
+
for rel in (("skills", "uscha-devloop", "qa_ledger.py"),
|
|
823
|
+
(".claude", "skills", "uscha-devloop", "qa_ledger.py")):
|
|
824
|
+
candidate = KIT_ROOT.joinpath(*rel)
|
|
825
|
+
if candidate.is_file():
|
|
826
|
+
return candidate
|
|
827
|
+
return None
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def cmd_top(args):
|
|
831
|
+
"""`uscha top` — the live terminal board of the project's ledger (ADR-031).
|
|
832
|
+
Wired exactly like `mirador`: resolve the sibling script inside the kit and exec it with
|
|
833
|
+
this interpreter. `--json` is a passthrough to the engine's own read-only subcommand, so
|
|
834
|
+
a script can consume the contract without going through the renderer at all."""
|
|
835
|
+
if not Path(args.ledger).is_file():
|
|
836
|
+
print("[uscha top] ledger '%s' not found here -- run the dev loop first, or pass "
|
|
837
|
+
"--ledger" % args.ledger, file=sys.stderr)
|
|
838
|
+
raise SystemExit(1)
|
|
839
|
+
if args.json:
|
|
840
|
+
engine = _qa_ledger_path()
|
|
841
|
+
if engine is None:
|
|
842
|
+
print("[uscha top] qa_ledger.py not found in the kit", file=sys.stderr)
|
|
843
|
+
raise SystemExit(1)
|
|
844
|
+
rc = subprocess.call([sys.executable, str(engine), "top", "--json",
|
|
845
|
+
"--ledger", args.ledger])
|
|
846
|
+
if rc:
|
|
847
|
+
raise SystemExit(rc)
|
|
848
|
+
return
|
|
849
|
+
renderer = _uscha_top_path()
|
|
850
|
+
if renderer is None:
|
|
851
|
+
print("[uscha top] uscha_top.py not found in the kit", file=sys.stderr)
|
|
852
|
+
raise SystemExit(1)
|
|
853
|
+
cmd = [sys.executable, str(renderer), "--ledger", args.ledger,
|
|
854
|
+
"--refresh", str(args.refresh)]
|
|
855
|
+
if args.once:
|
|
856
|
+
cmd.append("--once")
|
|
857
|
+
rc = subprocess.call(cmd)
|
|
858
|
+
if rc:
|
|
859
|
+
raise SystemExit(rc)
|
|
860
|
+
|
|
861
|
+
|
|
810
862
|
def settings_without_hook(path):
|
|
811
863
|
"""Return (new_settings, removed_count): the user's settings with OUR PreToolUse entries
|
|
812
864
|
dropped and nothing else touched. A foreign hook -- including one in the same group -- is
|
|
@@ -1006,6 +1058,12 @@ def build_parser():
|
|
|
1006
1058
|
mirador.add_argument("--open", dest="force_open", action="store_true",
|
|
1007
1059
|
help="open the browser even if mirador.html already existed (you closed the tab)")
|
|
1008
1060
|
mirador.set_defaults(func=cmd_mirador)
|
|
1061
|
+
top = sub.add_parser("top", help="live terminal board of the project's obligations, read from QA-LEDGER.json")
|
|
1062
|
+
top.add_argument("--ledger", default="QA-LEDGER.json", help="ledger to read (default: the QA-LEDGER.json convention)")
|
|
1063
|
+
top.add_argument("--once", action="store_true", help="print one plain frame and exit (implied without a TTY)")
|
|
1064
|
+
top.add_argument("--refresh", type=float, default=2.0, help="seconds between polls (reserved for M2; M1 re-reads on the `r` key)")
|
|
1065
|
+
top.add_argument("--json", action="store_true", help="print the engine's read-only `top --json` contract instead of rendering it")
|
|
1066
|
+
top.set_defaults(func=cmd_top)
|
|
1009
1067
|
return parser
|
|
1010
1068
|
|
|
1011
1069
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"AC-T-01": true, "AC-T-02": true, "AC-T-03": true, "AC-T-10": true, "AC-T-04": true, "AC-T-05": true, "AC-T-06": true, "AC-T-09": true, "AC-T-24": true, "AC-T-19": true, "AC-T-23": true, "AC-T-21": true, "AC-T-08": true, "AC-T-07": true, "AC-T-18": true, "AC-T-20": true, "AC-T-22": true}
|